40 KiB
活动信息发布模块 · 后端重构计划
项目:
E:\YHXT\java\FamilyActivity(Spring Boot 2 + JPA,库family_activity,ddl-auto: update) 基础包名已更名(✅ 已完成):com.yunhai.notice→com.yunhai.familyactivity,含目录树、全部 package/import、启动类NoticeApplication→FamilyActivityApplication、切面表达式、pom.xmlmainClass 等;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 现状缺口(对照前端契约)
- 无
Activity业务域代码(实体/服务/接口全部缺失)。 BaseApiController缺少前端已调用的GET /api/base/GetClassTree、POST /api/base/SetRole。- 无 UserContext 机制:当前各接口靠
BoaUser方法参数绑定 /UserContextHolder(yunhai platform)取用户,Service 层无统一上下文。 - 无操作日志表。
二、目标架构(包结构)
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..*.*(..))")):
- 优先从方法参数提取
BoaUser(兼容现有写法); - 缺失时回退
BoaUserContext.get()(平台静态持有者)→ request attribute(boaUser/currentUser/...)→currentroleHeader/Query 解析; - 填充 UserContext(userId、userName、schId=学校 id、type;超管 type=1 强制 schId=0;clientIp);
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) | 班级粒度已读(前端关键约定 4:classRead) |
| account | varchar(50) | 操作账号(家长=学生账号) |
| readTime | datetime | 阅读时间 |
唯一约束 (activity_id, class_id);应收/已读人数按班级 studentCount 累加(班级与学生数取自 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 核心业务规则
- 发布(Create):draft=true 存草稿;否则置已发;
targetClassIds()统一解析receiveList("ALL"=全校班级);发布时按班级×学生预生成回执记录(receiptMode≠none 时,status=none),保证「应收人数」「未回执名单」可查。 - 状态机:
actStatus推导规则 —— status=草稿→draft;now < signupDeadline→signup;signupDeadline ≤ now < endTime→closed;now ≥ endTime→finished(receiptMode=none 时无报名态语义,仅展示)。 - 进度统计(
ActivityProgress):should=应回执人数;signed/confirmed/canceled按 signup 表 status 计数;unsigned=should-signed-confirmed-canceled;usedQuota=ΣjoinCount(canceled 不计);full = quota>0 && usedQuota≥quota;signupRate。receiptMode=none或draft时progress=null。 - 已读:
Detail(type=1)/MySignup标读(家长端打开即确认模式:receiptMode=confirm且状态为 none 时同步置confirmed);InitializeRed一键全部已读;ReadDetail按年级→班级分组返回读/未读。 - 报名(Signup/CancelSignup):校验活动状态(signup 中)、截止时间、名额(quota>0 时 usedQuota+joinCount≤quota)、随行(allowAccompany);成功/失败回执业务码。
- 管理动作:Edit(含 sourceId 复用)、Delete(批量 + 级联删 read/signup)、UserPull 撤回、SetTopmost、ReceiveClasses 补发(新班级追加并补回执记录)、Expedite/Remind 催办(本期日志占位)、ExportSignups 导出 CSV(easyexcel 已在 pom,CSV 直接拼文本返回)。
- 家长端:
MyActivities分桶(todo 待回执/signed 已报名/finished 已结束/all)+ 计数;MySignup活动详情+我的回执(同时标读/自动确认)。 - 统计(Statis):总量/已发布/我发布/累计报名人次/平均回执率/待处理回执、按部门(deptName)、按月(createTime)、按分类(category)、我发布活动明细(含 progress)。
- 数据隔离:全部查询带
UserContext.getSchId()过滤;教师/管理端scope=user只看自己发布。 - 视角(SetRole):
SetRole写入会话(headercurrentrole或 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 | 回执名单(按班级分组+汇总) |
| 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 |
七、风险与决策点
- 更名范围:仅重命名 Java 基础包(
com.yunhai.familyactivity);pom.xml的artifactId=notice与bootstrap.yml的spring.application.name=notice保持不变——后者决定 Nacos 配置 dataId(notice.yaml),改动会导致配置发现失败。如需连应用名一起更名,必须先在 Nacos 建好familyactivity.yaml再切换。 - 旧表不迁移:
notice_*历史数据不迁移到新表(新业务新表);如需迁移另行立项。 - 家长视角识别:家长=学生账号登录。✅ 已实现:
SetRole写会话属性activityViewRole(BaseApiController.SESSION_VIEW_ROLE);家长身份按当前账号在GetSchStulist中匹配学生解析班级。联调时如网关/SSO 有独立视角下发,再对齐。 - 班级与学生数据源:✅ 已实现三级降级——主路径
GetClaArrStulist按班批量;失败兜底GetSchStulist全校过滤;仍不可达时应收人数降级为回执表实有人数、studentCount记 0,不阻塞主流程。 - 催办短信:本期占位(记录日志),
smsFlag仅存储;后续接回短信通道时恢复 SmsSender。 - 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 关键实现决策(与计划的差异)
- 预生成回执覆盖定时发布:定时发布的活动在创建/编辑时同样预生成回执记录,收件箱按
timingSendTime过滤可见性(计划原文仅即时发布生成)。 - waitlist 待定:无独立状态(前端枚举仅 none/signed/confirmed/canceled),落库为
signed+ 备注[待定]前缀。 - pendingReview 口径:名额超额人数
max(0, usedQuota − quota)(名额不限时为 0)。 - UserContext 增补 account:参考项目仅 userId/userName/roleId/schId/ip/type,家校场景发布人/家长都以「账号」为键,故增补 account 字段。
- NoticeAuditListener 保持平台
UserContextHolder取人:与 UserContext 同源(BoaUser/LoginUser),无功能差异,未改动。 - 家长身份解析:
GetSchStulist按账号匹配学生 → 班级;每请求一次远端调用,列表页已做批量/去重优化(receiveText、名额占用均一次取数)。 - 催办占位:
Expedite/Remind落操作日志、返回{reminded: 0};smsFlag仅存储。 - 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 决策与保留项(本轮不改)
- 双 toast(全局 onBizError + 页面 catch 各弹一次):保留现状。页面 catch 同时兜本地异常,全局拦截兜未 catch 路径;后续可统一收敛为全局一处。
- 切面身份兜底:
ControllerUserContextAspect兜底分支(LoginUser/anonymous)无 schId 时业务层按超管口径放行(schId=0 不加过滤)。纵深防御依赖网关/SSO(yh.sso.url-pattern=/api/*)前置拦截未登录请求,维持现状;如需彻底收紧应在业务层显式区分 type=1 超管与无身份。 - 报名名单下拉 pageSize=100:活动多于 100 条时下拉缺项,属低频场景,暂不加搜索。
- 统计
pendingReview口径:当前=名额超额人数(max(0, usedQuota−quota)),与文案"待处理回执"不完全一致,维持现状待产品确认。 - 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 自动生成的同名约束(二者等价,冗余无害,可择机合并)。