Files
familyactivity-service/docs/后端重构计划-活动信息发布.md
wanghu 33398a7f8a
build / build-push (push) Successful in 1m30s
阅读
2026-09-23 11:28:00 +08:00

43 KiB
Raw Permalink Blame History

活动信息发布模块 · 后端重构计划

项目:E:\YHXT\java\FamilyActivity(Spring Boot 2 + JPA,库 family_activity,ddl-auto: update) 基础包名已更名(✅ 已完成):com.yunhai.notice → com.yunhai.familyactivity,含目录树、全部 package/import、启动类 NoticeApplication → FamilyActivityApplication、切面表达式、pom.xml mainClass 等;artifactId 与 spring.application.name 保持 notice 不变(Nacos dataId 依赖,见 §七-0)。 前端契约:E:\YHXT\view\FamilyActivity\src\types\activity.ts、src\api\module\activity.ts、docs\活动信息发布模块-功能与接口说明-v1.md 参考项目:E:\YHXT\java\UserclassService(UserContext 用户上下文、操作日志表设计)

目标:将现有「通知公告(Notice)」后端改造为「活动信息发布(Activity)」后端 —— 删除旧业务实体,保留字典表与日志能力,按前端契约重建实体与业务逻辑,并引入 UserContext 统一获取当前用户。


一、现状盘点

执行状态(随实施同步更新):步骤 0 包更名 ✅;步骤 1 旧业务删除 ✅;步骤 2 UserContext+切面+操作日志 ✅;步骤 3 实体/仓库/枚举/班级门面 ✅;步骤 4 model 层 ✅;步骤 5 ActivityService ✅;步骤 6 ActivityController + GetClassTree/SetRole ✅;步骤 7 编译验证 ✅ 2026-09-17 已在工具链环境(便携 JDK 8.0_504 + Maven 3.8.8)执行 mvn clean compile,81 个源文件全量 BUILD SUCCESS。建表脚本:sql/fa_activity_tables.sql。前后端联查 bug 修复与优化见 §九(2026-09-17)。

1.1 原代码清单与处置(更名前盘点;现基础包为 com.yunhai.familyactivity)

层 现有文件 处置
entity BaseEntity 保留(公共基类:自增 id + createBy/createTime/updateBy/updateTime,NoticeAuditListener 填充)
entity SysDict(sys_dict 字典表) 保留(主办部门下拉/管理,前端 /api/dict/* 依赖)
entity UpFile(附件表) 保留(附件上传/下载/预览,前端依赖,属通用基础设施)
entity ContactsGroup / ContactsGroupUser(联系组) 保留(应用设置页「自定义组管理」依赖,非公告业务)
entity Notice / NoticeRead / NoticeReceiver / NoticeTemplate / SmsModule / SmsSchoolConfig / TemplateType 删除
repository NoticeRepository 等 7 个 Notice/Sms/Template 仓库 删除
repository SysDictRepository / UpFileRepository / ContactsGroupRepository / ContactsGroupUserRepository 保留
controller NoticeController / SmsController / TemplateController 删除
controller BaseApiController / DictController / ContactsController / FileController 保留(Base 需补 2 个接口,见 5.4)
service INoticeService/NoticeService(957 行)、ISmsService/SmsService、ITemplateService/TemplateService 删除
service IActivityService/ActivityService 新建(核心业务,见 §5)
service ClassService / ContactsService / DictService / FileService / UserCenterService 保留(按需适配新实体)
model model/notice/Notice* 系列 VO/Query 删除(重建为 model/activity/*)
model UpFileResultVo / UpFileVo / UserModuleVo / SchTeaList、BasePageQuery / BasePageResult 保留(前端契约同名/分页外壳)
enums NoticeStatusEnum / NoticeTypeEnum 删除(重建 Activity 枚举)
utils PageUtils 保留
config Config / JacksonConfig / SwaggerConfig 保留
resources sql/notice_*.sql(3 个旧表脚本) 归档至 sql/archive/

说明:当前项目没有任何日志实体/日志表(已 grep 确认),日志表按参考项目新建(§4)。

1.2 现状缺口(对照前端契约)

  1. 无 Activity 业务域代码(实体/服务/接口全部缺失)。
  2. BaseApiController 缺少前端已调用的 GET /api/base/GetClassTree、POST /api/base/SetRole。
  3. 无 UserContext 机制:当前各接口靠 BoaUser 方法参数绑定 / UserContextHolder(yunhai platform)取用户,Service 层无统一上下文。
  4. 无操作日志表。

二、目标架构(包结构)

com.yunhai.familyactivity
├── aspect/                    # 新增:UserContext 切面、操作日志切面
├── annotation/                # 新增:@OperationLog
├── common/                    # 保留:validation(NoticeAuditListener 随实体保留)
├── config/                    # 保留
├── controller/api/
│   ├── ActivityController     # 新建:/api/activity/*(24 个接口)
│   ├── BaseApiController      # 保留 + 补 GetClassTree / SetRole
│   ├── ContactsController / DictController / FileController   # 保留
├── entity/
│   ├── BaseEntity / SysDict / UpFile / ContactsGroup / ContactsGroupUser   # 保留
│   ├── Activity               # 新建:活动主表
│   ├── ActivityRead           # 新建:已读记录(学生粒度)
│   ├── ActivitySignup         # 新建:回执/报名记录
│   └── OperationLogRecord     # 新建:操作日志表(参考 UserclassService)
├── enums/                     # 新建:ActivityStatusEnum、ReceiptModeEnum、SignupStatusEnum
├── model/
│   ├── activity/              # 新建:request + vo(严格对齐 src/types/activity.ts)
│   └── (BasePage*、UpFile*、UserModuleVo 保留)
├── repository/                # 保留 4 个 + 新建 3 个 Activity 仓库 + OperationLogRepository
├── service/
│   ├── IActivityService / impl/ActivityService    # 新建
│   └── (其余保留)
└── utils/
    ├── PageUtils              # 保留
    └── UserContext            # 新建(参考 UserclassService)

三、任务 1:删除旧业务实体

删除清单(实体 → 仓库 → 服务 → 控制器 → model → 枚举,一并清理引用):

  • entity:Notice、NoticeRead、NoticeReceiver、NoticeTemplate、SmsModule、SmsSchoolConfig、TemplateType
  • repository:NoticeRepository、NoticeReadRepository、NoticeReceiverRepository、NoticeTemplateRepository、SmsModuleRepository、SmsSchoolConfigRepository、TemplateTypeRepository
  • service:INoticeService/impl/NoticeService、ISmsService/impl/SmsService、ITemplateService/impl/TemplateService
  • controller:NoticeController、SmsController、TemplateController
  • model:model/notice/ 下 NoticeVo、NoticeDetailVo、NoticePageResult、NoticeQuery、NoticeRequest、NoticeStatisticsVo、NoticeUserListResult(UpFileResultVo、UpFileVo、UserModuleVo、InitializeRedVo 保留/迁出后删除整个 notice 子包)
  • enums:NoticeStatusEnum、NoticeTypeEnum
  • service/sms/(SmsSender、DefaultSmsSender)
  • SQL 脚本归档:sql/notice_*.sql → sql/archive/

注意事项:

  • ddl-auto: update 只增不删,删除实体后旧表 notice_* 仍留在库中;本次不做数据迁移、不物理删表(新业务建新表),旧表留待 DBA 统一清理,并提供 sql/archive/drop_notice_tables.sql 备查。
  • 短信能力(Sms)删除后,smsFlag 字段仅作为活动上的标记位存储;Expedite/Remind 催办接口本期仅记录日志、返回 {reminded: 0},短信通道后续按需接入。

四、任务 2:引入 UserContext(参考 E:\YHXT\java\UserclassService)

4.1 utils/UserContext.java

照搬参考项目实现:ThreadLocal 持有 userId / userName / roleId / schId / ip / type, 提供 get/set、getCurrentUser()(不可变 UserInfo)、wrap(Runnable)(异步任务传递)、clear()。

4.2 aspect/ControllerUserContextAspect.java

照搬参考项目实现(@Aspect @Order(1),@Around("execution(* com.yunhai.familyactivity.controller..*.*(..))")):

  1. 优先从方法参数提取 BoaUser(兼容现有写法);
  2. 缺失时回退 BoaUserContext.get()(平台静态持有者)→ request attribute(boaUser/currentUser/...)→ currentrole Header/Query 解析;
  3. 填充 UserContext(userId、userName、schId=学校 id、type;超管 type=1 强制 schId=0;clientIp);
  4. finally { UserContext.clear(); }。

4.3 使用约定

  • Service 层一律通过 UserContext 取当前用户(发布人账号/姓名、schId 数据隔离、日志记录),控制器不再逐层传 BoaUser;
  • 控制器保留 BoaUser 参数绑定的接口(GetUserInfo 等平台代理接口)不动,切面同时兼容两种方式;
  • NoticeAuditListener(BaseEntity 审计填充)同步切换为从 UserContext 取 createBy/updateBy。

4.4 操作日志(保留并新建日志表)

当前项目无日志表,参考 UserclassService 三件套引入业务操作日志:

组件 说明
entity/OperationLogRecord(表 operation_log) id、operatorId、operator、operateTime、ip、module、content、schId(照搬参考项目字段)
repository/OperationLogRepository JPA 仓库
annotation/@OperationLog(module, content) 方法级注解
aspect/OperationLogAspect @AfterReturning 记录:从 UserContext 取操作人/IP/schId,异步落库(复用现有 AsyncConfig 风格或简单 @Async)

覆盖点:Create / Edit / Delete / UserPull / SetTopmost / ReceiveClasses / Review / Signup / CancelSignup / dict 增删改 等写操作。


五、任务 3:新实体与业务逻辑(对齐前端契约)

5.1 实体设计

Activity(活动主表,表 fa_activity)

字段 类型 说明(对应前端 ActivityVo)
id int 自增(BaseEntity) 前端 id / serid
schId int 学校隔离(参考 notice_add_sch_id.sql 先例)
title / content varchar / text 标题、富文本内容(详情 Contents)
category varchar(50) 活动分类(武术比赛/家长会/…,字典维护)
venue varchar(200) 活动地点
startTime / endTime / signupDeadline datetime 开始/结束/报名截止
receiptMode varchar(16) none 仅通知 / signup 需报名 / confirm 需确认
quota int 名额上限,0 不限
allowAccompany tinyint 是否允许家长随行
cost decimal(10,2) 活动费用(元)
deptId / deptName varchar 主办部门(字典 code / 名称冗余)
publisherAccount / publisherName varchar 发布人账号/姓名
status tinyint 发送状态(沿用原通知语义:草稿/已发/撤回等)
topmost tinyint 置顶
isPulled tinyint 撤回(家长端不可见)
timingSendTime datetime 定时发送时间
smsFlag tinyint 短信标记(仅存储)
receiveType / receiveList varchar class / "ALL" 或班级 id 逗号串
sourceId int 转发/复用源活动
createBy/createTime/updateBy/updateTime BaseEntity 审计

actStatus(draft/signup/closed/finished)不落库,查询时按 status + signupDeadline/endTime 与当前时间推导(枚举 ActivityStatusEnum 提供计算方法),避免定时任务维护状态。

ActivityRead(已读记录,表 fa_activity_read)

字段 类型 说明
id int 自增
activityId int 活动 id
classId varchar(50) 班级 id
studentId int 阅读学生ID(家长端以学生身份打开;学生档案不可得时为 null)
readTime datetime 阅读时间

唯一约束 (activity_id, class_id, student_id)(uk_activity_class_student;2026-09-23 由班级粒度 (activity_id, class_id) 迁移而来,SQL 见 docs/sql/20260923_activity_read_per_student.sql,ddl-auto=update 不会自动改唯一键,各环境需手动执行)。 已读人数 = 接收班级内的已读记录条数(即实际打开过活动的家长数,每个家长一条,重复打开幂等); 班级「读/未读」= 该班是否存在任一已读记录;应读人数(allowCount)仍为接收班级学生数之和(取自 ClassService/基础平台接口)。

ActivitySignup(回执/报名记录,表 fa_activity_signup)

字段 类型 说明(对应前端 SignupBrief)
id int 自增 signupId
activityId int 活动 id
schId int 学校隔离
studentId / studentName int / varchar 学生
classId / className varchar 班级
guardianAccount varchar 家长登录账号(=学生账号)
guardianName / guardianPhone varchar 家长姓名/电话
joinCount int 参加人数(含学生本人)
status varchar(16) none 未回执 / signed 已报名 / confirmed 已确认 / canceled 已取消
remark varchar(500) 备注(过敏史、随行说明)
signedAt datetime 回执时间
createBy/createTime/... BaseEntity 审计

唯一约束 (activity_id, student_id)(一个学生对一个活动一条回执,可反复修改报名)。

5.2 model(严格对齐 src/types/activity.ts)

  • request:ActivityRequest(@RequestBody JSON)、ActivityQuery(分页/筛选)、SignupRequest、ReviewRequest
  • vo:ActivityVo、ActivityDetailVo(@JsonProperty 保留旧字段名:Contents/DeptID/DeptName/TeaName/EditDate/DsfsTime/isDsfs/ChaoSend/numcount/readnumcount/tlist)、ActivityProgress、ActivityPageResult(继承分页外壳 + unreadCount)、SignupsResultVo/SignupGroup/SignupBrief、SignupDetailVo、ActivityStatisticsVo、MyActivitiesVo/MyActivityVo/ParentActivityVo/MySignupInfo、ReadGroup/ReadGroupUser、NextPrevVo、InitializeRedVo
  • 枚举:ActivityStatusEnum(draft/signup/closed/finished)、ReceiptModeEnum(none/signup/confirm)、SignupStatusEnum(none/signed/confirmed/canceled),各带 desc 字段输出 xxxDesc

5.3 IActivityService / ActivityService 核心业务规则

  1. 发布(Create):draft=true 存草稿;否则置已发;targetClassIds() 统一解析 receiveList("ALL"=全校班级);发布时按班级×学生预生成回执记录(receiptMode≠none 时,status=none),保证「应收人数」「未回执名单」可查。
  2. 状态机:actStatus 推导规则 —— status=草稿→draft;now < signupDeadline→signup;signupDeadline ≤ now < endTime→closed;now ≥ endTime→finished(receiptMode=none 时无报名态语义,仅展示)。
  3. 进度统计(ActivityProgress):should=应回执人数;signed/confirmed/canceled 按 signup 表 status 计数;unsigned=should-signed-confirmed-canceled;usedQuota=ΣjoinCount(canceled 不计);full = quota>0 && usedQuota≥quota;signupRate;readCount=已读人数(学生粒度:接收班级内已读记录条数=实际阅读家长数,仅 /Signups 汇总填充,口径同列表 readCount 与详情 readnumcount)、readRate=阅读率%(readCount/should 取整)。receiptMode=none 或 draft 时 progress=null。
  4. 已读:学生粒度——Detail(type=1) / MySignup 标读(每个家长账号各记一条,重复打开幂等;打开即确认模式 receiptMode=confirm 且状态为 none 时同步置 confirmed);InitializeRed 一键全部已读(为当前学生逐活动补记录);家长端列表/收件箱的已读标志按「本人学号已读」判定;ReadDetail 按年级→班级分组返回读/未读(该班任一学生已读即记为读)。
  5. 报名(Signup/CancelSignup):校验活动状态(signup 中)、截止时间、名额(quota>0 时 usedQuota+joinCount≤quota)、随行(allowAccompany);成功/失败回执业务码。
  6. 管理动作:Edit(含 sourceId 复用)、Delete(批量 + 级联删 read/signup)、UserPull 撤回、SetTopmost、ReceiveClasses 补发(新班级追加并补回执记录)、Expedite/Remind 催办(本期日志占位)、ExportSignups 导出 CSV(easyexcel 已在 pom,CSV 直接拼文本返回)。
  7. 家长端:MyActivities 分桶(todo 待回执/signed 已报名/finished 已结束/all)+ 计数;MySignup 活动详情+我的回执(同时标读/自动确认)。
  8. 统计(Statis):总量/已发布/我发布/累计报名人次/平均回执率/待处理回执、按部门(deptName)、按月(createTime)、按分类(category)、我发布活动明细(含 progress)。
  9. 数据隔离:全部查询带 UserContext.getSchId() 过滤;教师/管理端 scope=user 只看自己发布。
  10. 视角(SetRole):SetRole 写入会话(header currentrole 或 session 属性),GetUserInfo/List/MyActivities 按 teacher/parent 视角返回(家长=学生账号,挂班级→活动过滤)。

5.4 接口清单(ActivityController,前缀 Config.API_PATH + "/activity")

方法 路径 入参 说明
GET /List ActivityQuery 家长端收件箱(返回 ActivityPageResult,含 unreadCount)
GET /SendList ActivityQuery(scope=user) 我发布的 / 全部活动
GET /Detail serid, type type=1 标读
GET /GetDetail serid 编辑回显,不标读
GET /GetNextPrev serid, type 上一篇/下一篇
GET /ReadDetail serid 阅读情况(年级→班级分组)
GET /Statis startTime?, endTime? 活动统计
POST /Create ActivityRequest 新建,返回活动 id
POST /Edit ActivityRequest 编辑
POST /Delete number[] 批量删除(级联清回执/已读)
POST /InitializeRed — 一键全部已读
POST /UserPull serid 撤回
POST /SetTopmost serid, topmost 置顶
POST /Expedite serid 催办未读(日志占位)
POST /ReceiveClasses serid, classes 追加接收班级(补回执)
GET /Signups serid, signupStatus 回执名单(按班级分组+汇总;summary 含 readCount/readRate,名单项含 read 已读/未读)
GET /SignupDetail signupId 单条回执详情
POST /Review ReviewRequest 处理回执 confirm/cancel/waitlist
POST /Remind serid 催报名(日志占位,返回 {reminded})
GET /ExportSignups serid 导出 CSV 文本
GET /MyActivities ActivityQuery 家长端活动广场(分桶计数)
GET /MySignup serid 家长端详情+我的回执(标读/自动确认)
POST /Signup SignupRequest 报名/修改报名
POST /CancelSignup serid 取消报名
GET /AllFileDown dataid 全部附件打包下载(沿用 FileService)

BaseApiController 补充:GET /GetClassTree(年级→班级树,叶子带 studentCount,走 ClassService/基础平台)、POST /SetRole(切换 teacher/parent 视角)。


六、实施步骤与验证(已全部执行)

# 步骤 产出 状态/验证
0 基础包更名 com.yunhai.notice → com.yunhai.familyactivity:目录树平移、全部 package/import、启动类改名 NoticeApplication → FamilyActivityApplication、pom.xml mainClass 同步 ✅ 完成 全局 grep 无 com.yunhai.notice 残留
1 删除旧业务代码(§三清单),全局引用清理;旧 SQL 归档 sql/archive/ 编译通过的最小工程 ✅ 完成(残留 grep 为空;DictService.delDept 引用校验已改接 fa_activity)
2 UserContext + ControllerUserContextAspect + @OperationLog/切面/operation_log 实体与仓库 用户上下文 + 操作日志三件套 ✅ 完成(切点 com.yunhai.familyactivity.controller..*,写操作同步落库)
3 实体 Activity/ActivityRead/ActivitySignup + 仓库 + 3 个枚举 + IClassService 扩展为 bschclass 完整门面(含 6 个 model/bsch DTO) 活动域数据层 ✅ 完成(建表脚本 sql/fa_activity_tables.sql)
4 model 层:model/activity 20 个 request/VO + 枚举 desc 与 types/activity.ts 一一对齐 ✅ 完成(详情 VO @JsonProperty 保留旧字段名)
5 IActivityService/ActivityService 业务实现 全部业务规则(§5.3) ✅ 完成(静态核查:JPA 泛型、派生查询字段、括号配平)
6 ActivityController 25 端点 + BaseApiController 补 GetClassTree/SetRole 接口层 ✅ 完成(写操作带 @OperationLog)
7 保留功能适配:Dict 删除保护接新表、File topicType 改 activity、Swagger/pom 名称更正 全链路可联调 ✅ 完成
8 收尾:建表脚本归档、文档 As-Built 更新 交付物 ✅ 2026-09-17 编译验证完成:便携 JDK 8.0_504 + Maven 3.8.8(E:\YHXT\tools\)执行 mvn clean compile,81 个源文件全量 BUILD SUCCESS

七、风险与决策点

  1. 更名范围:仅重命名 Java 基础包(com.yunhai.familyactivity);pom.xml 的 artifactId=notice 与 bootstrap.yml 的 spring.application.name=notice 保持不变——后者决定 Nacos 配置 dataId(notice.yaml),改动会导致配置发现失败。如需连应用名一起更名,必须先在 Nacos 建好 familyactivity.yaml 再切换。
  2. 旧表不迁移:notice_* 历史数据不迁移到新表(新业务新表);如需迁移另行立项。
  3. 家长视角识别:家长=学生账号登录。✅ 已实现:SetRole 写会话属性 activityViewRole(BaseApiController.SESSION_VIEW_ROLE);家长身份按当前账号在 GetSchStulist 中匹配学生解析班级。联调时如网关/SSO 有独立视角下发,再对齐。
  4. 班级与学生数据源:✅ 已实现三级降级——主路径 GetClaArrStulist 按班批量;失败兜底 GetSchStulist 全校过滤;仍不可达时应收人数降级为回执表实有人数、studentCount 记 0,不阻塞主流程。
  5. 催办短信:本期占位(记录日志),smsFlag 仅存储;后续接回短信通道时恢复 SmsSender。
  6. actStatus 推导 vs 落库:选择「查询时推导」,避免定时任务;列表排序/筛选按推导值在内存/SQL 中处理,数据量大时再优化为落库。

八、实施结果(As-Built)

与前端契约 src/types/activity.ts / src/api/module/activity.ts 逐字段对齐。以下为最终代码清单。

8.1 代码清单(com.yunhai.familyactivity)

annotation/    OperationLog                          # 操作日志注解(module/content)
aspect/        ControllerUserContextAspect            # UserContext 填充/清理(四级身份回退)
               OperationLogAspect                     # @OperationLog 拦截,operation_log 落库
controller/api/ActivityController                 # 25 端点(/api/activity/*)
               BaseApiController                      # 平台代理 + GetClassTree + SetRole
               DictController / ContactsController / FileController   # 保留适配
entity/        Activity(fa_activity)ActivityRead(fa_activity_read)ActivitySignup(fa_activity_signup)
               OperationLogRecord(operation_log)
               BaseEntity / SysDict / UpFile / ContactsGroup / ContactsGroupUser   # 保留
enums/         ActivityStatusEnum / ReceiptModeEnum / SignupStatusEnum
model/         BasePageQuery / BasePageResult / UpFileResultVo / UpFileVo / UserModuleVo / SchTeaList
model/activity/  ActivityRequest ActivityQuery SignupRequest ReviewRequest
                 ActivityVo ActivityDetailVo ActivityDetailFileItem ActivityProgress ActivityPageResult
                 SignupsResultVo SignupGroup SignupBrief SignupDetailVo ReadGroup(+ReadGroupUser)
                 NextPrevVo InitializeRedVo MyActivitiesVo(+Buckets) ParentActivityVo
                 MyActivityVo(+ActivityInfo) MySignupInfo ActivityStatisticsVo(+嵌套)
model/bsch/    SchInfoDto SchClassDto SchStudentDto SchGradeDto SchSystemDto SchTreeNodeDto
repository/    ActivityRepository(含 existsByDeptId 字典删除保护)
               ActivityReadRepository ActivitySignupRepository OperationLogRepository
               SysDict/UpFile/ContactsGroup 仓库(保留)
service/       IActivityService / impl/ActivityService            # 核心业务
               IClassService / impl/ClassService                  # bschclass 完整门面
               IDictService(删除保护接新表) IFileService(topicType=activity) IContactsService IUserCenterService
utils/         UserContext   # ThreadLocal:userId/userName/account/roleId/schId/ip/type + wrap()
resources/sql/ fa_activity_tables.sql(新表脚本);archive/notice_*.sql(旧脚本归档)

8.2 关键实现决策(与计划的差异)

  1. 预生成回执覆盖定时发布:定时发布的活动在创建/编辑时同样预生成回执记录,收件箱按 timingSendTime 过滤可见性(计划原文仅即时发布生成)。
  2. waitlist 待定:无独立状态(前端枚举仅 none/signed/confirmed/canceled),落库为 signed + 备注 [待定] 前缀。
  3. pendingReview 口径:名额超额人数 max(0, usedQuota − quota)(名额不限时为 0)。
  4. UserContext 增补 account:参考项目仅 userId/userName/roleId/schId/ip/type,家校场景发布人/家长都以「账号」为键,故增补 account 字段。
  5. NoticeAuditListener 保持平台 UserContextHolder 取人:与 UserContext 同源(BoaUser/LoginUser),无功能差异,未改动。
  6. 家长身份解析:GetSchStulist 按账号匹配学生 → 班级;每请求一次远端调用,列表页已做批量/去重优化(receiveText、名额占用均一次取数)。
  7. 催办占位:Expedite/Remind 落操作日志、返回 {reminded: 0};smsFlag 仅存储。
  8. actStatus 筛选翻译:SendList 将 draft/signup/closed/finished 翻译为 status + 截止/结束时间谓词,分页在数据库完成;家长端收件箱数据量小,接收班级匹配与未读过滤在内存完成。

8.3 遗留事项

事项 说明
mvn compile ✅ 已完成(2026-09-17,便携 JDK+Maven 全量编译 BUILD SUCCESS,详见 §六步骤 8 / §九)
联调冒烟 Swagger 链路:Create → Detail(type=1) → Signup → Signups → Review → Statis;前端 npm run dev(.env.dev + vite proxy 指向本地后端 8104)
短信通道 催办/催报名短信后续接入时恢复 SmsSender(参考 git 历史或 CampusNotice 同名实现)
旧表清理 notice_* 旧表留在库中(ddl-auto 不删表),DBA 确认后手工 DROP

九、前后端联查 Bug 修复与优化(2026-09-17)

背景:重构后代码从未经过编译验证与真实联调。本轮对前后端做全面静态联查(后端 25 项发现、前端 27 项发现),逐项修复并验证。工具链:便携 JDK 8.0_504 + Maven 3.8.8(E:\YHXT\tools\,永久可用);前端依赖重装(Node 24 下 node-sass 编译失败,以 npm install --ignore-scripts 安装,scss 实际由 dart-sass 编译,node-sass 为冗余废弃依赖可择机移除)。

9.1 后端修复(P1 功能/安全、P2 健壮性)

# 级别 问题 修复
1 P1 越权删除:delete() 无学校归属校验,可跨校按自增 id 枚举删除活动及回执/已读/附件 循环内补 requireActivity(id),不存在/无权的 id 跳过
2 P1 越权处理回执:review() 未校验回执归属,跨校可 confirm/cancel 他人回执 取到记录后补 requireActivity(record.getActivityId())
3 P1 越权打包下载:allFileDown() 无归属校验 补 requireActivity(dataid)
4 P1 导出契约断裂:ExportSignups 返回裸 text/csv,但平台 ApiResponseAdvice 不包装 String 返回、前端拦截器统一 response.json() 解析 → 线上导出必挂 后端改返回 JSON 包装 {csv};前端兼容 {csv}/字符串并统一补 BOM(§9.3-7)
5 P1 家长越权访问:detail(type=1)/mySignup 对草稿/已撤回/定时未到点活动照常返回全文,且触发标读+自动确认污染数据 新增 ensureParentVisible()(已发布+未撤回+定时到点),不满足抛 FORBIDDEN
6 P1 定时未到可报名:定时发布活动创建即 status=1,signup() 未校验 timingSendTime signup 校验链补定时到点检查
7 P1 名额并发超卖:名额校验为读-算-写无锁 新增 ActivityRepository.findByIdForUpdate(@Lock(PESSIMISTIC_WRITE)),signup() 悲观锁加载活动行,串行化同一活动校验(顺带消除并发首报唯一约束 500)
8 P1 编辑清置顶回归:applyRequest 中 topmost=null 强制置 0,而编辑提交从不带该字段 → 每次编辑保存静默取消置顶 topmost 为 null 时保留原值
9 P1 班级数据故障降级为全校可见:收件箱 myClassId=null 时放行全校活动(跨班泄露+未读角标虚高) 改为返回空列表并告警日志;未读计数同步归零
10 P2 receiveClasses 不校验补发班级归属,可把他校学生拉进回执名单 与 schoolClasses(schId).keySet() 求交,越界抛 FORBIDDEN
11 P2 超管(schId=0)创建活动落 schId=0,家长端永远看不到 create() 对 schId≤0 抛 FORBIDDEN("未绑定学校")
12 P2 GetNextPrev 忽略 type:管理视角从草稿进详情后翻页恒 null 按 type 分流:type=1 家长收件箱序列;type=0 本校全量(findBySchId,含草稿/撤回)
13 P2 requireActivity 对 schId=null 的异常数据不隔离 null 一并拒绝(新表无历史数据,纯收紧)
14 P2 关键字 LIKE 未转义 %/_,输入通配符全表模糊匹配 likeEscape() 转义 + cb.like(..., '\\')
15 P2 CSV 公式注入:单元格以 = + - @ 开头会被 Excel 当公式 csvCell() 危险前缀加 '
16 P2 FileController 上传/列表 topicType 残留 "notice",与活动域 "activity" 不一致(改走该端点的附件会查不到) 3 处统一改 "activity"
17 P2 signups(signupStatus) 非法值静默按 none 过滤返回空分组 SignupStatusEnum.isValid() 严格校验,非法值抛 PARAM_ERROR
18 P2 管理端 SendList 的 read 标志口径错误(班级粒度已读表中发布人账号永不出现,恒 false 的假数据) 管理端不再填 read(家长端 list() 另行正确填充)
19 P2 markRead 在同一事务内 catch 唯一约束异常(事务已 rollback-only,catch 无效反掩错误) 移除 catch,保留前置查重
20 P2 重复查询:list() 中 readActivityIds 查两次;preGenerateSignups 重复拉班级表 readIds 一次计算透传 filterInbox;班级表参数化复用

9.2 决策与保留项(本轮不改)

  1. 双 toast(全局 onBizError + 页面 catch 各弹一次):保留现状。页面 catch 同时兜本地异常,全局拦截兜未 catch 路径;后续可统一收敛为全局一处。
  2. 切面身份兜底:ControllerUserContextAspect 兜底分支(LoginUser/anonymous)无 schId 时业务层按超管口径放行(schId=0 不加过滤)。纵深防御依赖网关/SSO(yh.sso.url-pattern=/api/*)前置拦截未登录请求,维持现状;如需彻底收紧应在业务层显式区分 type=1 超管与无身份。
  3. 报名名单下拉 pageSize=100:活动多于 100 条时下拉缺项,属低频场景,暂不加搜索。
  4. 统计 pendingReview 口径:当前=名额超额人数(max(0, usedQuota−quota)),与文案"待处理回执"不完全一致,维持现状待产品确认。
  5. waitlist 待定落库为 signed+[待定] 前缀:维持原设计。

9.3 前端修复(配合后端联动)

# 级别 问题 修复
1 P0 AttachmentUpload.vue 导入不存在的 Homework、Notice 未导入 → 发布/编辑页组件加载即崩(白屏) 改 import { Message, Notice },Homework.info → Notice.info
2 P1 表单校验被架空:view-ui-plus validate() 失败时 resolve(false) 不 reject,.then(() => true) 把失败也映射成 true NewActivity 定时发布弹窗:不选时间点确定会立即发布;ParentActivities 报名表单:空姓名/非法手机号可提交。两处改为 await validate() 后判 false
3 P1 详情页上一篇/下一篇无效:router.replace 异步未等待,load() 读到旧 serid,重复请求当前篇 await router.replace(...) 后再 load()
4 P1 dev 代理配置自相矛盾:.env.dev 配置 VITE_SERVICE_HOST 指向远端,浏览器直连被 CAS 过滤器(不回 CORS 头)拦截,全部请求失败;vite proxy target 端口注释(8101/8102)与实际(8104)不符 VITE_SERVICE_HOST 置空(同源代理,符合 host.config 设计注释);proxy target 改 http://localhost:8104 并更正注释
5 P1 详情页对任意状态开放"编辑",已截止/已结束活动保存后被打回"报名态" 编辑按钮仅 actStatus === 'draft' 显示,非草稿走"复用再发"
6 P1 发布页:复用再发不回显接收班级/定时时间;tlist 非数组误判为"全部班级";复用不落 sourceId;报名截止时间无合理性校验;快速连点可重复创建 回显逻辑编辑/复用共用并兼容逗号串;复用提交带 sourceId;发布前校验 开始<结束、截止>now 且 ≤结束;doAdd 入口防重入
7 P1 导出名单与后端新契约({csv} JSON)联动,前端兼容字符串旧格式、统一补 BOM、导出按钮加 loading 防重复 Signups.vue exportList 重写;mock 同步返回 {csv}
8 P2 alova-core 并发计数 onError 置 0:多请求并发时一个网络错误导致全局 loading 永不结束;beforeRequest 内死代码 改 Math.max(0, reqCount-1),删除死代码
9 P2 家长端"我要报名"不看 canSignup:名额满/已截止仍可点,提交才被后端拒 按钮置灰并显示"名额已满"(signupState 联动列表项与详情)
10 P2 家长端报名/取消后列表-详情错位:活动移出当前 tab 后右侧仍显示其详情 syncAfterAction():目标活动不在当前 tab 时清空详情并落到第一条
11 P2 报名弹窗人数预填可能超上限(程序赋值不触发 InputNumber 钳制) openSignup 预填夹取 min(旧值, allowAccompany?6:1)
12 P2 Signups 切换活动时同参双请求 + 响应乱序覆盖 route watcher 只重置状态由 watch(signupStatus) 统一触发;loadSeq 序号守卫丢弃过期响应
13 P2 列表页离开时(watch route.path flush:pre 早于卸载)多发一次必被丢弃的请求 ActivityList/ParentActivities 路由守卫限定本组件路由前缀
14 P2 撤回后无标记、可再次撤回/催报名;状态兜底 `actStatus
15 P2 统计页未用接口的 startTime/endTime;mock 定时判断硬编码 "2026-09-26" 随时间失效;menu.ts 引用不存在的 IModule 类型;favicon 相对路径 404;分组 :key 用索引 统计页补时间范围筛选;mock 改与 nowStr() 比较;types/index.ts 补 IModule 定义;favicon 改根路径;:key 改稳定值

9.4 本轮验证结果

项 结果
后端全量编译 mvn clean compile(JDK 8.0_504 + Maven 3.8.8):81 个源文件 BUILD SUCCESS(含本轮全部修改)
前端构建 vite build --mode base:✓ built in 16.94s(含本轮全部修改;chunk 体积警告为既有问题)
后端本地启动 mvn spring-boot:run(8104)✅:Nacos/Rabbit 走关闭兜底配置;MySQL 5.7(localhost:3306/family_activity)8 张表全部自动建成(fa_activity / fa_activity_read / fa_activity_signup / operation_log + 保留表);/api/dict/List 302 跳 CAS(SSO 过滤器工作正常)
浏览器冒烟(mock 模式,GUI 黑盒) ① 发布页完整渲染(P0 坏导入修复生效,附件上传组件正常);② 定时发布弹窗不选时间点确定被拦截(内联红字"请选择定时发布时间",修复前会静默立即发布);③ 报名截止时间早于当前时间被拦截(新增校验生效);④ 立即发布全链路打通:发布成功 → 跳回列表 → 新活动数据正确;⑤ 家长端列表分桶计数正确(7/3/3/1)、详情打开正常、我要报名按钮可用(canSignup);⑥ 报名表单清空必填项提交被拦截(内联红字 + 弹窗不关闭,修复前空表单可直接提交)
真实环境登录联调 ✅ 已完成(教师端全闭环,账号 lijing):① 此前"CAS 404"判断有误——CAS context path 已从 /cas 迁移为根路径(/login),本地后端经 Nacos 共享配置自动对齐,登录 → 回调 → SetRole → 重定向回前端全链路打通;② 登录后曾报 NoClassDefFoundError: YschBaseOpenApi——平台 9/16 新快照把该类从 openapi.client 迁至 openapi.base,已同步更新 ContactsService/UserCenterService 的 import(本地 .m2 的 yh-* SNAPSHOT 元数据缓存曾解析到 8 月旧包,已刷新至 9/16 最新并清理缓存元数据);③ 真实数据闭环:发布活动(真实字典"运动/发文部"+ bschclass 真实班级高一~高三 29 班)→ 发布成功 → 按真实学生预生成 36 条回执(含学生"克格莫")→ 报名情况页按班级分组呈现 → 导出接口返回 {csv} JSON(BOM + 完整数据)契约正确;④ 家长端路由整页加载自动切 parent 视角,无学生班级账号显示空列表(可见性隔离生效)
家长账号报名闭环 ⚠️ 未完成:kegemo/csxs1 被 CAS 拒绝("用户名或密码错误",多次重试一致)——确认该家长账号有效凭据后即可走通(学生"克格莫"所在班级已在活动接收范围,回执记录已预生成)
平台 Logout 已知行为 POST /api/base/Logout 返回成功但未失效 HttpSession(平台实现仅清上下文),切换账号需 CAS logout + 后端重启(或等待 session 超时)

9.5 本地联调启动方式

# 1. 后端(首次自动建表;JDK/Maven 已就位于 E:\YHXT\tools\)
cd /e/YHXT/java/FamilyActivity
JAVA_HOME="E:\\YHXT\\tools\\jdk8u504-b01" /e/YHXT/tools/apache-maven-3.8.8/bin/mvn spring-boot:run

# 2. 前端(8078,/api 代理 → localhost:8104)
cd /e/YHXT/view/FamilyActivity
npm run dev        # 首次需 npm install(Node≥20 建议 --ignore-scripts,scss 由 dart-sass 编译)

# 3. 浏览器打开 http://localhost:8078 → CAS 登录(教师 lijing 验证通过;家长 kegemo 凭据待确认)
# 4. mock 演示模式:npm run dev:mock(无需后端与登录)

注意:node-sass 为废弃依赖(Node 24 下无法编译,--ignore-scripts 安装后需手动执行 node node_modules/esbuild/install.js 补 esbuild 0.11.3 二进制,vite dev 才能启动); 项目 scss 实际由 dart-sass 编译,node-sass 可从 package.json 移除(待团队确认 Node 版本基线后处理)。

9.6 后续迭代同步说明(2026-09-17 下午)

§九修复落地后,项目所有者当日又做了一轮自主迭代(11:54~15:45),核心变更:回执记录的家长标识从 guardian_account(账号)切换为 student_id(学生ID)体系——ActivitySignup 实体改为 (activity_id, student_id) 唯一约束、仓库改用 findByActivityIdAndStudentId / findByStudentIdAndActivityIdIn、ActivityService 报名/预生成/我的回执全部按 studentIdOf(pc) 防重;同时清理了 openapi.client 旧包引用(与 §9.4 所述平台包迁移一致)。

经逐项核对,§九的关键修复在该迭代中全部保留:数据隔离(delete/review/allFileDown 补 requireActivity)、家长可见性 ensureParentVisible、报名悲观锁 requireActivityForUpdate、LIKE 转义、CSV 公式注入防护、receiveClasses 本校校验、超管 schId=0 拒绝发布、编辑保留置顶、FileController topicType=activity、导出 {csv} 契约等。迭代后已重新 mvn compile BUILD SUCCESS,并在真实环境(lijing 登录)完成 §9.4 所列闭环验证。

供核对:fa_activity_signup 实际唯一索引为 uk_fa_signup_activity_student 与 Hibernate 自动生成的同名约束(二者等价,冗余无害,可择机合并)。

9.7 字典学校隔离 + 审计列改存用户ID(2026-09-20)

两处收口变更,与 §9.6「账号 → ID」体系一脉相承:

  1. sys_dict 增加 sch_id(学校隔离)
    • 实体 SysDict 增 schId(默认 0);SysDictRepository 增 findByDictTypeAndSchIdIn... 系列(读口径为 [当前学校, 0])。
    • DictService 读写全部限「本校 + 全局种子(sch_id=0)」:下拉/管理列表按范围查;新增归属当前学校;改/删/排序经 findOwned 校验,仅允许操作本校行或全局种子行(历史 sch_id 为空按全局处理),跨校操作抛 无权操作其他学校的字典。
    • resolveDeptName 先在本校范围内解析(本校行优先),未命中退回不区分学校的旧口径,保证历史活动仍能展示部门名。
    • 迁移脚本:src/main/resources/sql/fa_sys_dict_sch_id_and_audit.sql(加列 + 存量置 0 + 索引 + 全局种子 + 验证查询);建表脚本 fa_activity_tables.sql 补 sys_dict DDL。
  2. create_by / update_by 由登录账号改为平台用户ID(INT 列)
    • BaseEntity 两字段改为 Integer,NoticeAuditListener 取 UserContext.getUserId()(BoaUser.id)直接落 INT;UserContextHolder 兜底仅接受纯数字 userId(平台默认 Provider 会放入登录名,非数字则留空)。
    • ActivityVo.createBy/updateBy 同步改 Integer,JSON 输出为数字;前端契约 types/activity.ts 改 number | null(mock 演示数据置 null)。
    • 历史行为账号文本,无法在本库映射为用户ID:迁移脚本先将非纯数字文本置 NULL,再把列 VARCHAR(50) 改为 INT(涉及继承 BaseEntity 的全部 7 张表),纯数字历史值直接转换保留。

验收:mvn compile BUILD SUCCESS。注意:createBy/updateBy 改 INT 后前端契约同步为 number | null,fa_sys_dict_sch_id_and_audit.sql 需由 DBA 执行(含审计列 VARCHAR→INT 转换)。本地库 family_activity 已于 2026-09-20 执行(sch_id 列此前已由 ddl-auto 自动添加,脚本跳过加列直接归零回填;存量账号文本 lijing 置 NULL 后 14 个审计列转为 int(11),并建 idx_sys_dict_sch_type 索引;因库中已有真实字典数据(发文部/运动,归入全局种子),未执行种子 INSERT 段)。