外观
后端功能说明 — CodeNote Server
1. 概述
CodeNote Server 是 CodeNote 二维码管理系统的后端服务。
1.1 核心定位
- API 服务中心:为 Android 端和管理后台提供约 140 个 REST API 接口
- 数据管理中心:管理 34 张 MySQL 数据表,实现完整的数据模型和业务关系
- 离线同步仲裁:支持移动端离线优先设计,提供增量同步、冲突仲裁(last-write-wins)、指数退避重试机制
- 公开分享服务:通过 Thymeleaf 模板引擎渲染公开分享页面,无需额外部署
1.2 技术栈
- 框架: Spring Boot 3.2.5 + Kotlin 1.9.23
- 数据库: MySQL 8.0 + JPA (Hibernate) + Flyway 迁移
- 认证: JWT (io.jsonwebtoken) + Spring Security
- 存储: 阿里云 OSS(文件存储)
- 构建工具: Gradle Kotlin DSL
- 部署: Docker + Docker Compose + 阿里云容器镜像服务
- 核心能力: REST API(约140个接口)、34张数据表、会话管理、多账号切换、登录记录审计、离线同步仲裁、全局管理后台 API
1.3 角色体系
Server 端负责所有角色的认证、授权和数据隔离,是权限控制的核心:
系统级角色:
- GUEST(未登录):仅可访问公开分享页
- USER(普通用户):默认角色,可使用所有个人业务功能
- ADMIN(系统管理员):可访问管理后台 API,拥有全局数据查看和管理权限
管理后台角色(PLATFORM 域,RBAC 预置,调整):
- SYSTEM_ADMIN 系统管理员:全量权限(唯一可管理 RBAC 三页/系统配置/配额/操作日志)
- USER_ADMIN 用户管理员:用户管理 + 组织管理(含转让所有权/成员管理)+ 登录记录/账号(已并入原 ORG_ADMIN 的组织/分享/二维码/资产权限)
- BUSINESS_ADMIN 业务管理员:二维码/资源/分类/相册/文件/元字段/元模板/模板绑定(meta_*,不含对象注册)
- OPERATION_ADMIN 运营管理员:活动/签到/通行证/通知/分享/扫码记录
登录方式:
- 用户名/邮箱 + 密码登录
- 手机号 + 短信验证码登录(PNVS 号码认证服务)
- 手机号 + 密码登录
- 一键登录(Android PNVS SDK 取号)
- 设备免注册登录(deviceId)
组织级角色:OWNER / ADMIN / MEMBER / GUEST
1.4 安全架构
安全架构(认证机制、公开路径清单、权限校验、示例)已迁移至
/reference/api.md第 6 节。关键文件:
SecurityConfig.kt— Spring Security 配置JwtAuthFilter.kt— JWT 认证过滤器AuthInterceptor.kt(Android) — 客户端认证拦截器
2. 项目结构
codenote-server/
├── src/main/kotlin/com/codenote/
│ ├── CodenoteApplication.kt # 应用入口
│ ├── common/ # 公共组件
│ │ ├── ApiResponse.kt # 统一响应格式
│ │ ├── BusinessException.kt # 业务异常
│ │ └── GlobalExceptionHandler.kt # 全局异常处理
│ ├── config/ # 配置类
│ │ ├── JacksonConfig.kt # JSON 序列化配置
│ │ ├── JwtAuthFilter.kt # JWT 认证过滤器
│ │ ├── SecurityConfig.kt # Spring Security 配置
│ │ ├── UserIdArgumentResolver.kt # 用户 ID 参数解析器
│ │ └── WebConfig.kt # Web 配置(CORS 等)
│ ├── controller/ # REST API 控制器(26 个控制器文件)
│ │ ├── AccountSwitchController.kt # 会话管理/账号关联
│ │ ├── ActivityController.kt # 活动管理
│ │ ├── AdminChatController.kt # 管理后台聊天设置/统计
│ │ ├── AdminAccountSwitchController.kt # 管理端会话管理/账号关联
│ │ ├── AdminAuthController.kt # 管理端登录
│ │ ├── AdminController.kt # 管理后台 API(14 个子控制器)
│ │ ├── AdminMetaController.kt # 管理端元数据管理(对象/字段/模板)
│ │ ├── AlbumController.kt # 水印相册
│ │ ├── AssetController.kt # 固定资源
│ │ ├── AuthController.kt # 用户认证
│ │ ├── CategoryController.kt # 分类管理
│ │ ├── CheckInController.kt # 签到系统
│ │ ├── FileController.kt # 文件上传
│ │ ├── LoginRecordController.kt # 登录记录
│ │ ├── MetaController.kt # App 端元数据(字段/模板/值)
│ │ ├── NotificationController.kt # 通知中心
│ │ ├── OrgController.kt # 组织管理
│ │ ├── OrgMetaController.kt # 组织级元数据(ORG 字段/模板/fork)
│ │ ├── PassController.kt # 通行证系统
│ │ ├── PublicQrCodeController.kt # 业务二维码渲染/落地页
│ │ ├── PublicShareController.kt # 公开分享
│ │ ├── QrCodeController.kt # 二维码 CRUD
│ │ ├── ScanRecordController.kt # 扫码记录
│ │ └── SyncController.kt # 云同步
│ ├── service/ # 业务逻辑层(24 个 Service)
│ │ ├── AuthService.kt # JWT / BCrypt 密码加密
│ │ ├── ChatService.kt # 会话+消息编排(发送/撤回/已读/转发/草稿)
│ │ ├── ChatSyncService.kt # 增量补拉(afterId 游标)
│ │ ├── ChatSettingService.kt # 聊天全局设置(类型化+缓存+留痕)
│ │ ├── ContactService.kt # 好友关系/申请/黑名单/名片码/隐私
│ │ ├── MessageTypeRegistry.kt # 消息类型注册表(handler/ 8 个实现)
│ │ ├── QrCodeService.kt # 二维码核心业务
│ │ ├── QrCodeImageService.kt # 二维码图片即时渲染
│ │ ├── CategoryService.kt # 分类树管理
│ │ ├── ScanRecordService.kt # 扫码记录
│ │ ├── FileService.kt # OSS 文件上传 + 存储用量自动更新
│ │ ├── SyncService.kt # 离线同步仲裁
│ │ ├── NotificationService.kt # 消息中心
│ │ ├── OrgService.kt # 组织管理
│ │ ├── OrgShareService.kt # 资源共享
│ │ ├── OrgLocationService.kt # 组织位置管理
│ │ ├── AssetService.kt # 固定资源
│ │ ├── MetaObjectService.kt # 业务对象注册/查询
│ │ ├── MetaFieldService.kt # 元字段 CRUD + scope 可见性合并
│ │ ├── MetaTemplateService.kt # 元模板 CRUD + fork + 实体绑定
│ │ ├── MetaValueService.kt # 字段值读写 + 冗余列回填 + 固定列转写
│ │ ├── FieldValidatorRegistry.kt # 字段类型校验器注册表(15 种)
│ │ ├── ActivityService.kt # 活动管理
│ │ ├── CheckInService.kt # 签到系统
│ │ ├── PassService.kt # 通行证系统
│ │ ├── AlbumService.kt # 水印相册
│ │ ├── AdminService.kt # 管理后台业务
│ │ ├── PublicShareService.kt # 公开分享
│ │ ├── AccountSwitchService.kt # 会话管理/账号切换
│ │ ├── LoginRecordService.kt # 登录记录
│ │ ├── JwtService.kt # JWT 令牌管理
│ │ ├── QuotaService.kt # 配额管理
│ ├── entity/ # JPA 实体(45 张表,47 个 @Entity)
│ │ ├── AccountLink.kt # 账号关联
│ │ ├── Activity.kt # 活动
│ │ ├── AdminLog.kt # 操作日志
│ │ ├── AlbumPhoto.kt # 相册照片
│ │ ├── Contact.kt # 好友关系
│ │ ├── ContactBlock.kt # 黑名单
│ │ ├── ContactRequest.kt # 好友申请
│ │ ├── Conversation.kt # 会话
│ │ ├── ConversationMember.kt # 会话成员
│ │ ├── Message.kt # 消息(类型化 content JSON)
│ │ ├── UserChatSetting.kt # 个人聊天隐私设置
│ │ ├── Asset.kt # 资源
│ │ ├── AssetCategory.kt # 资源分类
│ │ ├── AssetInventory.kt # 资源盘点
│ │ ├── AssetInventoryItem.kt # 盘点项
│ │ ├── AssetTransfer.kt # 资源流转
│ │ ├── Category.kt # 分类
│ │ ├── CheckInActivity.kt # 签到活动
│ │ ├── CheckInConfig.kt # 签到配置
│ │ ├── CheckInRecord.kt # 签到记录
│ │ ├── File.kt # 文件
│ │ ├── LoginRecord.kt # 登录记录
│ │ ├── MetaField.kt # 元字段
│ │ ├── MetaObject.kt # 业务对象
│ │ ├── MetaObjectBinding.kt # 实体-模板绑定
│ │ ├── MetaTemplate.kt # 元模板
│ │ ├── MetaTemplateField.kt # 模板-字段明细
│ │ ├── MetaValue.kt # 通用字段值
│ │ ├── Notification.kt # 通知
│ │ ├── OrgInvitation.kt # 组织邀请
│ │ ├── OrgLocation.kt # 组织位置
│ │ ├── OrgMember.kt # 组织成员
│ │ ├── OrgShare.kt # 资源共享
│ │ ├── Organization.kt # 组织
│ │ ├── Pass.kt # 通行证
│ │ ├── PassConfig.kt # 通行配置
│ │ ├── PassTemplate.kt # 通行模板
│ │ ├── PassUsageRecord.kt # 通行使用记录
│ │ ├── PublicLink.kt # 公开链接
│ │ ├── QrCode.kt # 二维码
│ │ ├── ScanRecord.kt # 扫码记录
│ │ ├── SyncMapping.kt # 同步映射
│ │ ├── SystemSetting.kt # 系统配置
│ │ ├── TokenBlacklist.kt # Token 黑名单
│ │ ├── UnsyncedRecord.kt # 未同步记录
│ │ ├── User.kt # 用户
│ │ └── UserSession.kt # 用户会话
│ ├── ws/ # WebSocket 聊天长连接
│ │ └── ChatWsHandler.kt # /ws/chat 多设备长连接 + 事件推送
│ ├── repository/ # JPA Repository(41 个)
│ ├── dto/ # 请求/响应 DTO(21 个)
│ │ ├── ActivityDto.kt
│ │ ├── AdminDto.kt
│ │ ├── AlbumDto.kt
│ │ ├── AssetDto.kt
│ │ ├── AuthDto.kt
│ │ ├── CategoryDto.kt
│ │ ├── CheckInDto.kt
│ │ ├── FileDto.kt
│ │ ├── NotificationDto.kt
│ │ ├── OrgDto.kt
│ │ ├── OrgShareDto.kt
│ │ ├── OrgLocationDto.kt
│ │ ├── PassDto.kt
│ │ ├── QrCodeDto.kt
│ │ ├── ScanRecordDto.kt
│ │ ├── SyncDto.kt
│ │ └── ...其他 DTO
│ └── common/ # 公共异常/响应
├── src/main/resources/
│ ├── application.yml # 应用配置
│ ├── application-prod.yml # 生产环境配置
│ ├── db/migration/ # Flyway 数据库迁移
│ │ └── V1__init_schema.sql # 初始化 34 表 + 默认管理员
│ ├── templates/ # Thymeleaf 模板
│ │ └── public-share.html # 公开分享页
│ └── static/ # 静态资源
├── build.gradle.kts # Gradle 构建配置
├── docker-compose.yml # Docker 编排
├── Dockerfile # Docker 镜像构建
├── deploy.sh # 部署脚本
└── README.md # 项目说明3. API 接口清单
完整 API 接口(约 140 个)参见
/reference/api.md,包含端点、请求/响应格式、安全架构。
3.1 关键架构特性
离线优先设计支持:
- Android 端所有核心操作(扫码、生成、编辑、搜索)完全离线可用
- Room 本地数据库 + FTS4 全文索引
- 离线队列 + WorkManager 后台同步
- 冲突仲裁:last-write-wins,服务端 updated_at 为准
- 冲突解决:
SyncService.resolve()已实现,acceptServer=true确认服务端版本,acceptServer=false允许客户端重新推送覆盖
二维码作为万物入口:
- 普通二维码:URL/WiFi/Contact/TEXT,通过
qr_codes表管理 - 业务二维码(资源/活动/签到/通行):内容存储在各业务表
qr_content字段,通过/api/qrcode/render即时渲染 - 扫码后自动识别内容前缀并路由到对应业务模块
活动容器模式:
- 活动是签到和通行证的容器
- 通过
has_check_in/has_pass开关子功能
组织协作体系:
- 三级角色:OWNER(所有者)/ADMIN(管理员)/MEMBER(成员)
- 转移所有权:OWNER 可转让给其他成员,原 OWNER 降级为 MEMBER
- 成员邀请:邀请状态枚举 PENDING(待处理)/ ACCEPTED(已接受)/ REJECTED(已拒绝)/ CANCELLED(已撤回)/ EXPIRED(已过期),仅 OWNER/ADMIN 可发起与撤回(撤回仅限 PENDING);有效期 7 天(org_invitations.expires_at,惰性过期:读取或操作时超时自动置 EXPIRED)
- 加入通道:① OWNER/ADMIN 搜索用户批量邀请(
POST /orgs/{id}/invite)② 组织码/二维码自由加入 ③ 邀请审批策略组织码加入需 OWNER/ADMIN 审批 ④ 分享申请链接(POST /orgs/{id}/share-join-link发 ORG_JOIN_LINK 通知,接收方点通知/扫码org-join:{code}→ 查看组织信息 → 申请加入;GET /orgs/public/by-id/{orgId}免登录按 ID 反查组织公开信息,供通知跳转)⑤ 发现页加入(§可见性架构):非成员从发现页点公开组织 →GET /api/orgs/{id}放行只读(PUBLIC 非成员 200 + 空权限,INTERNAL 403)→POST /api/orgs/{id}/apply按 joinPolicy 处理(FREE 直接加入 / APPROVAL 提交申请 / INVITE·INTERNAL 拒绝 403,抽 doJoin 与 join-by-code 共用)
管理后台全局审计:
- 管理页面覆盖所有业务模块
- 所有管理员操作自动记录到
admin_logs表
详细 API 接口清单参见
/reference/api.md。
版本更新:Android App 版本更新功能参见 /reference/app-update.md,后端提供 /api/app/update/check 公开接口和 system_settings 存储。
Markdown 统一渲染(原《Markdown渲染通用方案》已合并本文档):
设计原则:内容统一为 Markdown 原文存储与编辑;渲染唯一出口在后端(Markdown → 安全 HTML),App 端零解析(WebView 展示)。一份原文,所有端展示一致。
依赖(build.gradle.kts):org.commonmark:commonmark:0.22.0 + commonmark-ext-gfm-tables:0.22.0(表格)+ commonmark-ext-autolink:0.22.0(自动识别 URL)
渲染服务 util/MarkdownRenderer.kt(render() 输出 HTML 片段;renderPlainText() 用 TextContentRenderer 输出纯文本摘要)。安全配置三条硬性要求(踩坑记录):
- ⚠️
escapeHtml(true)必须显式开启 —— 0.22 默认 false,raw HTML 会原样输出 = XSS 漏洞 - ⚠️ 扩展(Tables/Autolink)必须 Parser 和 HtmlRenderer 双侧注册,否则表格/自动链接不渲染
- ⚠️
urlSanitizer需配合sanitizeUrls(true)开关才生效;丢弃 URL 返回空串而非 null(返回 null 触发 NPE);白名单:http/https + 相对路径保留,其余(javascript:/data:/vbscript: 等)置空
接口约定:任何返回 Markdown 的接口同时带 xxx 原文 + xxxHtml 渲染(摘要场景再加 xxxPlain)。字段名前缀按业务对象命名:协议 content/contentHtml,通知 body/bodyHtml/bodyPlain,更新 releaseNotes/releaseNotesHtml。
应用场景:
| 场景 | 存储字段 | 展示端 | 渲染出口 |
|---|---|---|---|
| 用户服务协议 / 隐私政策 | agreements.content | App 协议页 | 协议接口 contentHtml ✅ |
| 系统通知正文 | notifications.body(Markdown) | App 通知详情页 | 通知接口 bodyHtml + bodyPlain ✅ |
| 更新日志 | app_updates.release_notes | App 更新弹窗 | 更新接口 releaseNotesHtml(待接入) |
| 公告 / 帮助文档(未来) | 新表或 system_settings | App / 后台 | 通用渲染接口 |
协议:agreements 表版本化:只增不改(历史可追溯)、发布即生效(version 自动 +1,旧版本自动失效)、历史版本可回滚激活;GET /api/agreement/current 公开,后台 /api/admin/agreements 管理(列表/发布/激活)。表结构见 /architecture/data-model.md「agreements 用户协议/隐私政策表」,接口见 /reference/api.md M8/A23。 预览:POST /api/admin/common/render-markdown(content:render 权限 + 每用户每分钟 60 次限流)。 Android 展示:core:ui 的 MarkdownView 组件(WebView + 后端 HTML,JS 禁用),详见 /reference/android.md。
附录:认证接口与新增服务
接口清单
- 密码/解绑端点:password/forgot-code、password/reset、password/change-code、password/change、email/unbind、phone/unbind
- 关联账号验证码端点:
POST /api/auth/sms/link-code(需登录):向目标账号手机号发送关联验证码(type=ACCOUNT_LINK);发码前校验手机号已绑定真实账号,且不能是自己的手机号POST /api/auth/email/link-code(需登录):向目标账号邮箱发送关联验证码(type=ACCOUNT_LINK);校验同上POST /api/auth/link支持三种验证方式:verifyType= PASSWORD(默认,linkedUserId+password)/ PHONE_CODE / EMAIL_CODE(credential+code);验证码方式由后端从 user_identities 反查目标 userId;关联成功后向目标账号发送站内通知(type=ACCOUNT_LINK,WebSocket 实时推送)- 验证码类型 ACCOUNT_LINK 已加入 EmailService 限频(60s + 日上限 5),与 LOGIN/BIND 互不通用
- LinkedAccountDto/SwitchAccountResponse 返回 phone;账号显示名由端上按规则拼(用户名>手机号>邮箱>设备);管理端
GET /api/admin/auth/link/list不带 userId 返回全量关联(含 ownerUserId 发起方);POST /api/auth/link/{linkId}/switch携带 X-Device-Name 写入 session/login_records - login_records 新增 city 列(App 定位城市上报,入库时记录);每用户最多保留 50 条(删旧入新);
token_refresh不再写记录(会话保活非用户行为);绑定手机补审计phone_bind;管理端GET /api/login-records/admin/all支持loginType参数筛选(含 account_delete 注销账户审计)
- 昵称词库 admin 端点:/api/admin/nickname/adjectives|nouns CRUD ×8
- 用户名/密码独立设置:
PUT /api/users/me/username(设置用户名)、POST /api/auth/password/set(设置密码,不强制登出) - 身份分离后行为:
device-login自动注册登录,返回统一LoginResponsephone-login/phone-password-login/oneclick-login/email-login返回类型统一为LoginResponseGET /api/auth/devices(登录设备列表:登录时间/最后活跃时间/当前设备标记)、DELETE /api/auth/devices/{deviceId}(移除登录设备 = 吊销该设备全部会话,需重新登录)LoginResponse / UserInfoResponse包含hasRecoverableCredential(仅有设备身份时为 false,客户端提示绑定);UserInfoResponse 包含identities(已绑定的 provider 列表)- users 表认证字段迁至 user_identities;手机/邮箱/设备登录与绑定改走 identity 表
createAdminUser / updateAdminUser返回脱敏UserAdminVO(不含 password 哈希);Android 登录成功且hasRecoverableCredential=false时弹窗提醒绑定- 登录暴力破解限频:login/phonePasswordLogin 同一凭证 5 分钟 5 次失败锁定 5 分钟;createAdminUser 用户名查重返回 400
- 设备不绑定账号:logout(deviceId 路径)删除该设备 DEVICE identity;绑定手机/邮箱/设置密码成功后删除该用户全部 DEVICE identity(此后免注册登录创建新账号);凭据登录不再自动登记设备(移除 device_bind);设备管理 = 会话设备管理(user_sessions),移除 = 吊销会话
- 解绑保护:unbindPhone/unbindEmail 解绑后无任何可登录方式时拒绝(防孤儿账号)
- 注销账户:
POST /api/auth/account/delete(管理员禁止;身份 CASCADE 删除,业务数据保留) verify-mobile:SDK 一键授权 token → 服务端 getMobile 取号后绑定- 管理后台全量登录记录:
GET /api/login-records/admin/all(仅 ADMIN,可选 userId/hours 筛选)
- 个性签名:
PUT /api/users/userinfo支持 signature(≤100 字符,null 不改、空串清空);UserInfoResponse/listUsers 返回 signature - 安全配置:SecurityConfig.permitAll 含 forgot-code/reset;DeviceBindingFilter.skipAllPaths 同步
新增服务
NicknameService:昵称生成(词库随机组合,空词库降级)CredentialNormalizer:normalizePhone / normalizeEmail 统一凭证归一化UserIdentity/UserIdentityRepository:认证身份表(PHONE/EMAIL/DEVICE...),登录/绑定/设备管理统一走 identityhasRecoverableCredential(AuthService):= password 非空 ‖ 有 PHONE/EMAIL identity,驱动设备丢失/更换提醒
RBAC 权限系统(接入)
通用三域 RBAC(方案 v3.3 定稿,实施完成)。三域:PLATFORM(后台)/ APP(App)/ ORG(组织)。
核心组件:
PermissionRegistry:212 权限点常量(PLATFORM 76 / APP 73 / ORG 63),启动幂等同步rbac_permissions(只增不删,停用代替删除)RbacService/RbacServiceImpl:hasPermission / permissionsOf / roleNameOf / grant·revokeRole / 组织生命周期(createOrgRoles·bindOrgOwner·bindOrgMember·transferOrgOwnership·cleanupOrg·removeUserFromOrg·cleanupUser·bindDefaultAppRole);Caffeine 60s 缓存(key=userId:domain:scopeId),写操作主动失效@RequirePermission(domain, resource, action, orgIdParam, fallbackDomain)+PermissionAspect:域/scope 解析、OWNER/SYSTEM_ADMIN 短路、双域降级(orgId 空→APP 域,不回退 current_org_id)RbacController:GET /api/rbac/my-permissions+ 权限点/角色/用户授权 CRUD(PLATFORM 域 rbac_permission:/rbac_role:/rbac_user_role:*;角色管理覆盖 PLATFORM/APP/ORG 域,ORG 域 scopeId=0=初始角色模板、scopeId=orgId=组织实例角色,OWNER 权限绑定被拒)
关键行为变更:
users.role/org_members.role列删除;JWT 不再写 role claim,角色从 RBAC 读取(ROLE_<code>注入)- 后台登录 = PLATFORM 域存在任一角色(AdminAuthController);
requireAdmin同义 - 组织全流程同步 rbac:创建绑 OWNER / 审批通过·接受邀请绑 MEMBER(PENDING 不绑)/ 退出·移除清理绑定 / 转让旧 OWNER 降 ADMIN
- 属主语义(决策 B):组织资源由组织角色管理;个人资源(org_id 空)属主私有;
findOwned双域 - 分享 = 复制副本(qr/album 加 org_id,org_shares 存 source_id+copy_id 映射,取消=物理删副本+删映射)
- 参与语义:checkin:create 成员即可;pass:issue/verify/revoke 活动创建者豁免 + 组织角色
- 注销守卫:PLATFORM 域有角色不可注销;组织唯一 OWNER 不可注销(先转让或解散)
- POST /api/notifications 收紧为 PLATFORM notification:send(原无鉴权漏洞)