@@ -0,0 +1,431 @@
|
||||
# 活动信息发布模块 · 后端重构计划
|
||||
|
||||
> 项目:`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) | **班级粒度已读**(前端关键约定 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` 核心业务规则
|
||||
|
||||
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`。`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 | 回执名单(按班级分组+汇总) |
|
||||
| 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 |
|
||||
|
||||
## 七、风险与决策点
|
||||
|
||||
0. **更名范围**:仅重命名 Java 基础包(`com.yunhai.familyactivity`);`pom.xml` 的 `artifactId=notice` 与 `bootstrap.yml` 的 `spring.application.name=notice` **保持不变**——后者决定 Nacos 配置 dataId(`notice.yaml`),改动会导致配置发现失败。如需连应用名一起更名,必须先在 Nacos 建好 `familyactivity.yaml` 再切换。
|
||||
1. **旧表不迁移**:`notice_*` 历史数据不迁移到新表(新业务新表);如需迁移另行立项。
|
||||
2. **家长视角识别**:家长=学生账号登录。✅ 已实现:`SetRole` 写会话属性 `activityViewRole`(`BaseApiController.SESSION_VIEW_ROLE`);家长身份按当前账号在 `GetSchStulist` 中匹配学生解析班级。联调时如网关/SSO 有独立视角下发,再对齐。
|
||||
3. **班级与学生数据源**:✅ 已实现三级降级——主路径 `GetClaArrStulist` 按班批量;失败兜底 `GetSchStulist` 全校过滤;仍不可达时应收人数降级为回执表实有人数、`studentCount` 记 0,不阻塞主流程。
|
||||
4. **催办短信**:本期占位(记录日志),`smsFlag` 仅存储;后续接回短信通道时恢复 SmsSender。
|
||||
5. **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 || "signup"` 把未知状态误标"报名中";分页键 `customPageCount` 与应用设置页互相污染 | 标题加"已撤回"角标并对已撤回行隐藏撤回/催报名;状态兜底展示原值;分页键改 `activityListPageCount` |
|
||||
| 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 本地联调启动方式
|
||||
|
||||
```bash
|
||||
# 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 自动生成的同名约束(二者等价,冗余无害,可择机合并)。
|
||||
Reference in New Issue
Block a user