docs: oapi课表调用手册、课程表改造计划、重构任务计划
build / build-push (push) Successful in 1m25s

This commit is contained in:
wanghu
2026-10-14 16:15:16 +08:00
parent 1d0d5df9ef
commit d4374e3216
3 changed files with 510 additions and 0 deletions
+201
View File
@@ -0,0 +1,201 @@
# oapi 课表查询接口 · 调用手册
> 服务:AcaSyllabu(课程表) 接口:`GET /oapi/timetable/Get` 更新:2026-10-13
按「用户 + 角色 + 学校」查询其可见范围内所有班级的当前学年学期课程表。身份由调用方显式传入(不依赖浏览器登录态),适用于平台内其他应用(首页卡片、移动端、大屏等)服务端直调。
| 调用方身份 | 可见班级 | 可见科目 |
|---|---|---|
| 管理员(roleId=7)/ 校领导(roleId=2) | 全部班级 | 全部科目 |
| 班主任 | 所带班级(正/副班主任) | 全部科目 |
| 任教老师(roleId=1) | 任教班级 | 仅任教科目(其余科目格清空返回) |
| 学生(roleId=3) | 本人所在班级 | 全部科目 |
- `roleId` 不传时**默认按普通教师(1)**处理。
- 一个用户兼任多重身份(如既是班主任又任教其他班)时,取并集:班主任班给全部科目,任教班只给任教科目。
- 无任何可见范围(如普通教师没有任教记录)时返回空数组 `data: []`,不报错。
---
## 1. 请求
```
GET {base}/oapi/timetable/Get?userid={userId}&roleId={roleId}&schId={schId}
```
- 本地联调:`http://localhost:8109`(无 context-path)
- 生产:`http://{host}/acasyllabu`(context-path `/acasyllabu`,以网关实际地址为准)
| 参数 | 必填 | 说明 |
|---|---|---|
| `userid` | 是 | 平台用户ID。教师=教师ID;学生=学生树里的学生节点ID |
| `roleId` | 否 | 角色:1 教师(默认)、2 校领导、3 学生、7 管理员 |
| `schId` | 是 | 学校ID(数据严格限定在该校范围内) |
## 2. 签名鉴权(必须)
接口在 `/oapi/*` 强制验签下:浏览器/匿名直访一律 **401**,只有注册应用带上签名三件套才能通过。
### 2.1 请求头
| Header | 说明 |
|---|---|
| `X-App-Key` | 调用方应用ID(在服务方 `trusted-apps` 白名单登记的 appKey) |
| `X-Timestamp` | 当前毫秒时间戳。**有效期默认 5 分钟**,过期拒绝,请用当前时间 |
| `X-Signature` | 签名,算法见下 |
### 2.2 签名算法
```
待签串 = 请求方法 + "\n" + target + "\n" + X-Timestamp
target = 请求路径 + "?" + 查询参数(按参数名字典序排列后的 k=v&k=v)
签名 = Base64( HmacSHA256( 待签串, appSecret ) )
```
要点:
- 方法用大写 `GET`;分隔符是换行 `\n`,共两处;
- query 参数**按参数名排序**后再拼(如 `roleId` 在 `schId` 在 `userid` 前),排序后**原样拼接、不做 URL 编码**;
- 密钥即服务方 `trusted-apps` 里登记的该 appKey 对应 secret(找 AcaSyllabu 管理员索取)。
### 2.3 接入新调用方
服务方在 `application.yml`(生产为 Nacos `acasyllabu.yaml`)的 `yh.app-auth.trusted-apps` 下追加一行:
```yaml
yh:
app-auth:
trusted-apps:
"100021": "d3ee380e..." # 既有
"100xxx": "新调用方密钥" # 新增
```
> 生产环境接了中央密钥链(Redis 注册表)后可改走 `trust-registry`,届时以平台注册为准,无需手工维护白名单。
## 3. 响应
外层为平台统一包装:
```json
{ "code": 0, "msg": "", "data": [ …班级数组… ] }
```
`data` 数组每项:
| 字段 | 说明 |
|---|---|
| `classId` / `className` / `gradeName` | 班级ID / 班级名 / 年级名 |
| `headTeacher` | 该用户在本班是否班主任 |
| `viewAllSubjects` | 是否全部科目可见(班主任/管理员/校领导/学生=true,仅任教=false) |
| `subjects` | 任教科目列表(仅 viewAllSubjects=false 时非空) |
| `timetable` | 该班课表,结构同 `TimetableVo`(见下) |
`timetable`:
| 字段 | 说明 |
|---|---|
| `year` / `term` | 课表所属学年学期(term:1 上学期 2 下学期) |
| `photoFileId` / `photoName` / `photoSize` | 照片课表附件(未上传为 null) |
| `slots[]` | 节次数组;`slotNo` 从 1 起,`start`/`end` 为 HH:mm |
| `slots[].subjects` | 固定 7 位,下标 0~6 = 周一~周日,空串=未排课 |
示例(节选):
```json
{
"code": 0, "msg": "",
"data": [
{
"classId": 371, "className": "2班", "gradeName": "高一",
"headTeacher": false, "viewAllSubjects": false,
"subjects": ["语文"],
"timetable": {
"classId": 371, "year": 2026, "term": 1,
"className": "2班", "gradeName": "高一",
"photoFileId": null, "photoName": null, "photoSize": null,
"slots": [
{ "slotNo": 1, "start": "08:20", "end": "09:00",
"subjects": ["语文", "", "", "", "", "", ""] }
],
"viewAllSubjects": false, "mySubjects": ["语文"]
}
}
]
}
```
## 4. 错误情况
| 现象 | 原因 |
|---|---|
| HTTP 401 `missing app signature` | 未带签名三件套 |
| HTTP 401 `invalid signature or expired` | 密钥错误、待签串拼错或时间戳超过 5 分钟 |
| HTTP 401 `unknown appKey` | appKey 未在服务方 `trusted-apps` 登记 |
| HTTP 200 `{"code":6,"msg":"内部服务错误"}` | 缺 `userid`/`schId` 等必填参数 |
| HTTP 200 `code=0, data=[]` | 该身份在该校无可见班级(如无任教记录的教师) |
## 5. 调用示例
### 5.1 Node.js(可直接运行)
```js
const crypto = require('crypto');
const BASE = 'http://localhost:8109';
const APP_KEY = '100xxx';
const APP_SECRET = '你的密钥';
function get(pathAndQuery) {
const q = pathAndQuery.indexOf('?');
const path = q < 0 ? pathAndQuery : pathAndQuery.substring(0, q);
const pairs = q < 0 ? [] : pathAndQuery.substring(q + 1).split('&').sort();
const target = path + (pairs.length ? '?' + pairs.join('&') : '');
const ts = String(Date.now());
const sign = crypto.createHmac('sha256', APP_SECRET)
.update(`GET\n${target}\n${ts}`)
.digest('base64');
return fetch(BASE + pathAndQuery, {
headers: { 'X-App-Key': APP_KEY, 'X-Timestamp': ts, 'X-Signature': sign },
}).then(r => r.json());
}
// 管理员查全校
get('/oapi/timetable/Get?userid=1&roleId=7&schId=3').then(console.log);
```
### 5.2 Java(平台内服务,有 yh-utils-core 依赖)
```java
String pathAndQuery = "/oapi/timetable/Get?userid=1&roleId=7&schId=3";
// query 按参数名排序后拼回
String target = "/oapi/timetable/Get?roleId=7&schId=3&userid=1";
String ts = String.valueOf(System.currentTimeMillis());
String sign = SignUtil.signRequest("GET", target, ts, appSecret); // 平台工具,同款算法
HttpHeaders headers = new HttpHeaders();
headers.set("X-App-Key", appKey);
headers.set("X-Timestamp", ts);
headers.set("X-Signature", sign);
restTemplate.exchange(baseUrl + pathAndQuery, HttpMethod.GET, new HttpEntity<>(headers), String.class);
```
> 平台 Feign 客户端加 `SignFeignInterceptor` 可自动签名,无需手工拼。
### 5.3 curl(Git Bash,签名用 openssl 生成)
```bash
SECRET="你的密钥"
TARGET="/oapi/timetable/Get?roleId=7&schId=3&userid=1" # 参数已按名排序
TS=$(date +%s000)
SIGN=$(printf "GET\n%b\n%b" "$TARGET" "$TS" | openssl dgst -sha256 -hmac "$SECRET" -binary | base64)
curl -s "http://localhost:8109$TARGET" \
-H "X-App-Key: 100xxx" -H "X-Timestamp: $TS" -H "X-Signature: $SIGN"
```
> 注意 curl 的 URL 必须与参与签名的 target 完全一致(含参数顺序)。
## 6. 本地联调
- 服务方后端:8109 端口(devtools 热重启);本地验签白名单只收了本应用 `100021`(application.yml `trusted-apps`)。
- 现成冒烟脚本:`E:\YHXT\java\gui-test-screenshots\oapi-sign-smoke.js`(用法 `node oapi-sign-smoke.js "/oapi/timetable/Get?userid=1&roleId=7&schId=3"`,密钥内置 100021)。
- 真实数据参考(schId=3):管理员 userid=1 返回全部班级;教师 10008 = 班主任(1班 全部科目) + 任教(2班 仅语文);学生 10066 = 仅 1 班。
+134
View File
@@ -0,0 +1,134 @@
# AcaSyllabu 课程表改造计划
> 创建日期:2026-10-11
> **执行状态(2026-10-11):全部完成。**
> - 后端:包名已改 `com.yunhai.acasyllabu`,宿舍通知业务已删(保留日志/附件/基建),新增 Timetable/TimetableSlot 实体 + TimetableController(Get/Save/SavePhoto/RemovePhoto/Subjects)+ ClassService 学科接口;`mvn clean compile` BUILD SUCCESS;本地启动冒烟通过(/actuator/health 200,未登录访问 /api/* 正确 302 跳 SSO)。
> - 前端:新增 `views/syllabus/TimetableEdit.vue`(参考页整体移植)+ `api/module/syllabus.ts` + `types/syllabus.ts`,路由/菜单/标题全部切换为课程表,dorm 业务已删;`npm run build` 通过。
> - 待联调:账号 lijing/a123123 为 CAS 平台账号,需平台与班级服务在线后做登录态全流程验证(见任务 7)。
> 后端项目:`E:\YHXT\java\AcaSyllabu`(原包 `com.yunhai.dormnotice`,宿舍通知/家校活动遗留)
> 前端项目:`E:\YHXT\view\AcaSyllabu`(Vue3 + alova + view-ui-plus,原为宿舍通知页面)
> 参考项目:`E:\YHXT\java\UserclassService`(UserContext / SysOperationLogAspect / 学科 oapi 出处)
> 参考 UI:`E:\YHXT\view\AcaSyllabu\timetable-site\index.html`(qoder 站点本地副本,本次改造的界面蓝本)
> 目标:改造为**课程表服务**——包名 `com.yunhai.acasyllabu`,电子课程表(节次×星期编排)+ 照片课程表(图片上传),前后端真实联调。
---
## 一、业务需求
课程表分两种形式,均以「班级」为维度维护:
1. **电子课程表**(参考 timetable-site/index.html):
- 顶部选择班级;「电子版 / 照片版」两个页签。
- 编辑表格:`节次 | 星期一~星期日 | 开始时间 | 结束时间 | 操作(删除)`。
- 节次默认 10 节(可新增/删除/恢复默认);每个单元格下拉选择学科。
- 按开始时间自动判定时段:**上午 < 12:30;下午 12:30–18:30;晚自习 ≥ 18:30**,分组行展示。
- 支持预览课表(时段分栏课表 + 打印)。
2. **照片课程表**:按班级上传一张课表照片(预览/替换/移除)。
## 二、现状调研摘要
### 2.1 后端(com.yunhai.dormnotice)
- Spring Boot 2.7.18 + Java 8 + JPA/Hibernate + MySQL;端口 8105;本地库已是 `aca_syllabu`;生产 context-path `/dormnotice`、库 `bsch_dormnotice`(未改)。
- 上一轮重构已移植:`UserContext`、`ControllerUserContextAspect`、`SysOperationLogAspect`(异步落库)、`SysOperationLog`、`UpFile` 附件体系、`ClassService`(班级树/学生树,5 分钟缓存,直连 `{yh.open.bschclass}/oapi`)。
- **无学科接口**——ClassService 只有教师/学生树,需补 `getSchSubjectList`。
- 宿舍通知业务待删:Activity/ActivityRead/ActivitySignup/ContactGroup/ContactGroupUser 实体 + Contacts/Dict 控制器 + model/activity 15 个 VO。
- 已知隐患:`BaseApiController` import 了 `model.activity.ClassTreeNodeVo`(与 bsch 包同名类重复),删 activity 包前必须改为 `model.bsch.ClassTreeNodeVo`;两个切面的切点/排除名单硬编码 `com.yunhai.dormnotice`。
### 2.2 前端(view/AcaSyllabu)
- dev 端口 8078,代理 `/api → http://localhost:8105`(与后端一致,无需改)。
- 路由静态注册(routes.ts + meta.modId),菜单由后端 `GetUserMod` 下发经 menuRoute.ts 映射;dorm 7 页面 + `api/module/dorm.ts` + `types/dorm.ts` 为待删业务。
- alova 封装(get/postJSON/postForm)+ `{code,msg,data}` 解包 + 302 跳 SSO 均为可复用基建。
- `GradeClassSel`/`BschSubjectSelect`/`UploadImg` 是其它项目拷来的死组件(依赖的 store 字段不存在),不可直接用。
- `timetable-site/index.html`:完整交互蓝本(时段色板、学科色板、预览 sheet、照片 dropzone、打印样式俱全),整体迁入 Vue 页面。
### 2.3 构建环境
- 后端:`D:\Program Files\Java\jdk-1.8` + `D:\Program Files\Java\maven\apache-maven-3.6.1`(需 `JAVA_HOME`);平台依赖已在本地 `~/.m2` 缓存。
- 前端:Node 24 + npm,`node_modules` 缺失需先 `npm install`。
## 三、数据库设计(ddl-auto=update 自动建表,SQL 脚本同步留存)
| 表 | 字段 | 说明 |
|---|---|---|
| `timetable` | id、sch_id、class_id、class_name、grade_name、teacher_id、photo_file_id、create/update 审计 | 每班一条,`(sch_id, class_id)` 唯一;**teacher_id 为平台用户/教师 id,不存账号**;photo_file_id 关联 up_file |
| `timetable_slot` | id、timetable_id、sort_order、start_time、end_time、day_1..day_7 | 每节次一行;start/end 存 `HH:mm`;day_N 存学科名(空=未排) |
| `sys_operation_log` | (沿用) | 操作日志,切面异步落库 |
| `up_file` | (沿用) | 附件表,topic_type=`timetable` |
派生数据(时段分组、统计)不落库,前后端按开始时间实时计算。
> **2026-10-12 变更**:时段判断已移除。所属时段改为显式维护在模板行上(`timetable_template_row.band` = es/am/noon/pm/ev),
> 保存后不随开始时间变化;节次与空白行都落在时段里,启用哪些时段就要维护哪些时段的行——每个启用时段至少保留一行(节次或空白行),
> 只剩一行时该行不允许删除、也不允许移出该时段;课表编排页按生效模板行的 band 分组展示。上文「按开始时间自动判定时段」为改造初期的原始设计,仅作历史记录。
## 四、任务分解
### 任务 1:后端框架改名 `com.yunhai.dormnotice` → `com.yunhai.acasyllabu`
| # | 改动点 |
|---|---|
| 1.1 | 源码目录 `java/com/yunhai/dormnotice/` → `acasyllabu/`,全部 package/import 替换(含两切面切点表达式与 LogController 排除名单) |
| 1.2 | 主类 `DormNoticeApplication` → `AcaSyllabuApplication`(scanBasePackages 同步) |
| 1.3 | pom.xml:artifactId/name/finalName/mainClass → acasyllabu;ci profile 输出目录同步 |
| 1.4 | bootstrap.yml:`spring.application.name: acasyllabu`,Nacos dataId `acasyllabu.yaml` |
| 1.5 | application.yml:sms.module-id 改名;docker/application-prod.yml:context-path `/acasyllabu`、库 `bsch_acasyllabu`;Dockerfile EXPOSE 对齐 |
| 1.6 | logback-spring.xml / SwaggerConfig 标题 / notice.iml 及 .idea 辅助文件同步 |
### 任务 2:删除宿舍通知业务,保留日志/附件/基建
- **删除**:Activity、ActivityRead、ActivitySignup、ContactGroup、ContactGroupUser、SysDict 实体 + 对应 Repository/Service/Controller(ActivityController、ContactsController、DictController)+ `model/activity/` 全部 + `sql/dormnotice_schema.sql`。
- **保留**:BaseEntity、SysOperationLog、UpFile、BaseAuditListener、ValidationGroups、PageUtils、BasePageQuery/Result、UserContext/UserSysRole/UserType、两切面、BaseApiController、FileController、LogController、ClassService、UserCenterService、FileService、SysOperationLogService。
- **前置修复**:BaseApiController 的 ClassTreeNodeVo import 改指 `model.bsch`。
### 任务 3:新实体与业务(不存账号,存教师 id)
- `entity/Timetable`、`entity/TimetableSlot`(见第三节表设计)。
- `service/ITimetableService + impl`:
- `get(classId)`:按 UserContext.schId + classId 取课表,组装 slots(无数据返回空骨架由前端补默认 10 节)。
- `save(request)`:校验(班级属于本校 via ClassService、时间格式与先后、学科 ∈ 本校学科表、day 列数=7)→ 全量替换 slots,teacher_id=UserContext.userId,冗余 class_name/grade_name。
- `savePhoto(classId, fileId)`:绑定照片(up_file topic_type=timetable),替换时清理旧文件。
- `removePhoto(classId)`:解绑并删文件。
### 任务 4:ClassService 扩展学科接口
- `IClassService.getSubjectList(schId)`:GET `{bschclass}/oapi/sch/getSchSubjectList?schId=`,5 分钟缓存(与学生树同款),返回 `[{id, code, name}]`。失败降级返回空列表并 log.warn。
### 任务 5:TimetableController(`/{platform}/timetable/`,PascalCase 惯例)
| 接口 | 方法 | 说明 |
|---|---|---|
| `/Get` | GET | `?classId=` 返回课表 VO(slots + photoFileId/photoUrl) |
| `/Save` | POST @RequestBody | 整表保存(电子版) |
| `/SavePhoto` | POST | `?classId=&fileId=` 绑定照片 |
| `/RemovePhoto` | POST | `?classId=` 移除照片 |
| `/Subjects` | GET | 本校学科列表 |
- FileController 上传沿用:前端先 `/api/file/Upload` 拿 fileid,再 SavePhoto 绑定;Preview 展示。
### 任务 6:前端改造
1. 新增 `src/types/syllabus.ts`(TimetableVo/SlotVo/SubjectVo/SaveRequest)。
2. 新增 `src/api/module/syllabus.ts`(syllabusApi:Get/Save/SavePhoto/RemovePhoto/Subjects;复用 baseApi.GetClassTree/GetUserInfo —— baseApi 移入本模块或新建 `base.ts`)。
3. 新增 `src/views/syllabus/TimetableEdit.vue`:移植 timetable-site 交互(页签、班级选择、编辑表格、时段分组、新增/删除/恢复节次、预览 Modal+打印、照片 dropzone 上传/替换/移除);保存到后端(保存按钮 + 未保存标记,替代原 localStorage)。
4. routes.ts:删 dorm 路由,新增 `/timetable`(meta.title 课程表编排、modId 占位);`/` 默认进课程表。
5. layout.vue `getMenuIcon`、menuRoute.ts 别名、App.vue 应用名、index.html/vite.config 标题 → 「课程表」。
6. 删除 `views/dorm/`、`api/module/dorm.ts`、`types/dorm.ts`;死组件(GradeClassSel/BschSubjectSelect/UploadImg/TreeNode 等)不再被引用,随构建树摇出产物,暂不物理删除。
### 任务 7:bug 审查与优化(账号 lijing / a123123)
已知待修(本计划内执行):
1. BaseApiController 错误 import activity.ClassTreeNodeVo(任务 2 前置)。
2. 切面切点/排除名单硬编码包名(任务 1 一并替换)。
3. 生产 context-path/库名/sms module-id 残留 dormnotice(任务 1.5)。
4. Dockerfile EXPOSE(10002) 与实际端口(8070) 不一致。
5. dorm.ts `ReceiptFormTemplate` 未导入的类型瑕疵(随 dorm 删除消解)。
6. 前端 `postForm` 双发导致 Spring 参数合并 `"x,x"` 的历史坑——新 syllabus.ts postForm 仅在 isMock 时拼 query,沿用防御。
联调项(需平台在线,代码层先保证):
- CAS 登录 → GetUserInfo/GetUserMod → currentRole → UserContext 填充 → schId 隔离;
- lijing 建课表/换班/预览/打印/照片全流程;操作日志落库抽查。
## 五、实施顺序与验收
| 阶段 | 内容 | 验收 |
|---|---|---|
| 一 | 任务 1+2:改名、删业务、前置修复 | `mvn compile` 通过;全局 grep `dormnotice` 仅剩历史文档 |
| 二 | 任务 3+4+5:实体/服务/接口 | `mvn compile` 通过;Swagger 自查接口形状 |
| 三 | 任务 6:前端页面/路由/API | `npm run build` 通过 |
| 四 | 任务 7:bug 审查修复 + 联调准备 | 清单全过;文档勾选执行状态 |
## 六、风险与说明
1. lijing/a123123 为基础平台(CAS/用户中心)账号,代码中不落库不校验密码;联调依赖平台与班级服务在线。
2. 学科 oapi 若因权限/网络不可用, Subjects 返回空列表并前端提示,不阻断电子课表编辑(学科名仍可显示已存数据)。
3. 平台菜单(GetUserMod)未配「课程表」模块时,前端 `/` 兜底直接进 `/timetable`,菜单缺失不影响直访。
4. 旧 dorm 数据表(activity 等)保留在库中不删,仅代码层不再引用。
+175
View File
@@ -0,0 +1,175 @@
# DormNotice 重构任务计划(家校活动发布模块)
> 创建日期:2026-09-18
> **执行状态(2026-09-18):阶段一~四已完成,后端 `mvn compile` BUILD SUCCESS,前端 `npm run build` 构建成功。**
> 阶段五(双账号联调)需 CAS/用户中心与班级服务在线环境,待联调。
> 后端项目:`E:\YHXT\java\DormNotice`(当前根包 `com.yunhai.notice`,通知公告模块)
> 参考项目:`E:\YHXT\java\UserclassService`(用户/班级服务,UserContext 出处)
> 前端项目:`E:\YHXT\view\FamilyActivity`(Vue3 + alova + view-ui-plus,当前跑在 mock 上)
> 目标:将通知公告后端重构为 **家校活动发布(FamilyActivity)后端服务**,包名 `com.yunhai.dormnotice`,前后端真实联调。
---
## 一、现状调研摘要
### 1.1 DormNotice 后端现状
- Spring Boot 2.7.18 + Java 8 + JPA/Hibernate + MySQL,无 Redis / MyBatis / Feign / 测试代码。
- groupId `com.yunhai`,artifactId `notice`,服务名 `notice`,本地端口 **8105**,库 `dorm_notice`,生产 context-path `/notice`(`docker/application-prod.yml`)。
- 平台依赖:`yh-spring-boot-starter`(CAS SSO 认证、统一响应 `{code,msg,data}`、用户中心静态门面 `YschBaseOpenApi`)。
- 现有 10 个实体 + BaseEntity:Notice、NoticeRead、NoticeReceiver、NoticeTemplate、TemplateType、ContactsGroup、ContactsGroupUser、SysDict、UpFile、SmsModule、SmsSchoolConfig。
- **当前不存在任何日志表/日志实体**(grep `SysLog|OperLog` 零命中)。
- 认证:`UserContextHolder.get()`(平台包)+ `@Autowired CurrentBoaUser`,无自研 UserContext。
### 1.2 UserclassService 参考要点
- `UserContext`(`com.yunhai.bschclassmanage.utils.UserContext`):ThreadLocal 静态持有器,字段 userId / userName / roleId / schId / ip / type(1 超管、2 校管、3 普通用户),含 `UserInfo` 快照与 `wrap()` 异步包装。
- 配套组件(需一并复制):
- `aspect/ControllerUserContextAspect.java`——核心填充逻辑,切所有 controller,取用户优先级链:方法参数 `BoaUser` → `BoaUserContext.get()` → request attribute;从 header/query `currentrole`(URL 编码 JSON:`{childId, roleId, schId, userId}`)解析 `UserSysRole`;超管强制 `schId=0`;结束后 `clear()`。
- `model/tool/UserSysRole.java`、`interceptor/UserContextInterceptor.java`、`config/DWebMvcConfig.java`、`enums/UserType.java` / `UserRole.java`(1 教师、2 校领导、3 学生、4 家长…)。
- 日志参考实现:`entity/SysOperationLog.java`(表 `sys_operation_log`)+ `SysOperationLogAspect` / `SysOperationLogController`,字段含 log_level、operation_type、user_id、user_name、sch_id、request_ip/uri/method、class_method、request_params、response_data(GZIP)、status_code、cost_time、error_msg 等。
- 教师/学生业务 ID:`sch_teacher.tea_id`、`sch_student.stu_id`(用户中心业务 id,区别于本服务自增 id)——**新实体存这类 id,不存 account 账号**。
- 服务间调用:HTTP + `appkey/sign/t` 签名(`/oapi`),班级服务地址 `http://base.yunhaixingtong.com/userclass`。
### 1.3 FamilyActivity 前端现状
- Vue 3.4 + vite(端口 8078)+ alova(fetch,`credentials: include`,无 Bearer token,CAS 会话模式;业务码 302 跳 SSO);请求头注入 `yschapirequest/timeout/yschreferer`,localStorage `currentRole` 非空时注入 `currentRole` header。
- **dev 代理 `/api` → `http://localhost:8104`,与后端实际端口 8105 不一致(联调前必须修)**。
- 页面:教师端 ActivityList(我发布的/全部)、NewActivity、ActivityDetails、Signups(报名情况)、ActivityStats、ActivitySetting(字典 + 自定义组);家长端 ParentActivities(活动广场 / 我的报名,路由前缀 `/par`,靠 `SetRole` 切换)。
- 接口蓝本 `src/api/module/activity.ts`(约 40 个接口),类型契约 `src/types/activity.ts`(569 行),mock `src/assets/mock/activityapi.ts`(1552 行,实现了全部契约,可作为后端行为/种子数据规格)。
- **postForm 契约**:POST 参数只拼 URL query、不带 body(Spring `@RequestParam` 会把 query+body 重复参数合并成 `name=a,a`,这正是"字典文字重复"的根源之一,代码注释已记录)。
- 账号 lijing / kegemo 在前端与后端代码中均未出现,应为基础平台(CAS/用户中心)侧账号。
---
## 二、任务分解
### 任务 1:框架改名 `com.yunhai.notice` → `com.yunhai.dormnotice`
| # | 改动点 | 说明 |
|---|---|---|
| 1.1 | 源码目录 | `src/main/java/com/yunhai/notice/` → `com/yunhai/dormnotice/`,全部 `.java` 的 package / import 声明同步替换 |
| 1.2 | 主类 | `NoticeApplication` → `DormNoticeApplication`,`scanBasePackages = "com.yunhai.dormnotice"` |
| 1.3 | pom.xml | artifactId `notice` → `dormnotice`;name/description/finalName 同步改 |
| 1.4 | bootstrap.yml | `spring.application.name: notice` → `dormnotice`;Nacos dataId `notice.yaml` → `dormnotice.yaml`(Nacos 侧需同步建配置,本地开发不受影响) |
| 1.5 | docker/application-prod.yml | context-path `/notice` → `/dormnotice`;对外网关地址同步 |
| 1.6 | logback-spring.xml | 应用名/日志文件名中的 `notice` 字样同步改 |
| 1.7 | README / CI | Gitea 推送说明、`.idea` 库引用等辅助文件按需更新 |
验收:`mvn compile` 通过,全局 grep `com.yunhai.notice` 零残留,本地启动正常。
### 任务 2:删除旧实体,保留字典表,新建日志表
**删除**(9 个实体 + 对应 Repository/Service/Controller/model/enums/sql):
- 实体:Notice、NoticeRead、NoticeReceiver、NoticeTemplate、TemplateType、ContactsGroup、ContactsGroupUser、SmsModule、SmsSchoolConfig
- 连带清理:NoticeController、ContactsController(旧实现)、SmsController、TemplateController、INoticeService 等服务、model/notice 下 11 个 VO、enums 两个、`resources/sql/notice_*.sql` 三个脚本
- **UpFile 实体删除后按前端契约重建**(活动附件需要,见任务 3)
**保留**:
- `SysDict`(`sys_dict` 字典表)——字段微调见任务 3
- 基建:BaseEntity、审计 Listener(改名为通用名,如 `BaseAuditListener`)、BasePageQuery/BasePageResult、PageUtils、ValidationGroups、JacksonConfig、SwaggerConfig、Config(路径常量)
- FileController/FileService(上传下载,供附件复用,改造绑定逻辑)
**新建日志表**(DormNotice 当前没有,参考 UserclassService 移植):
- 实体 `SysOperationLog` → 表 `sys_operation_log`(字段照搬参考项目)
- `SysOperationLogAspect`(AOP 记录 + 异步落库 + 失败重试字段)、`LogController`(日志查询接口,可选)
- 移植时把包名改成 `com.yunhai.dormnotice.*`,依赖 UserContext(任务 4)
### 任务 3:参考前端创建新实体与业务(不存账号,只存教师/学生 id)
设计原则(对应需求 3):
- **实体一律不保存教师/学生/家长账号(account)**;发布人存 `publisher_id`(教师 id),报名存 `student_id`(学生 id),联系组成员存 `user_id`。
- 展示用姓名(publisher_name、student_name、class_name)允许冗余存储,避免列表页频繁调用户中心。
- 多租户:所有业务表带 `sch_id`,写入取 UserContext/BoaUser 当前角色 schId,查询强制过滤。
新实体(字段以 `src/types/activity.ts` + mock `activityapi.ts` 为最终契约,实现时逐一核对):
| 实体 | 表名 | 关键字段 |
|---|---|---|
| Activity 活动 | `activity` | id、sch_id、title、category(存分类字典值)、dept_id/dept_name(主办部门)、publisher_id(教师id)、publisher_name、venue、content(LONGTEXT 富文本)、start_time、end_time、signup_deadline、receipt_mode(none/signup/confirm)、quota(0 不限)、allow_accompany、cost、receive_list("ALL" 或班级 id 逗号串)、topmost、is_pulled、sms_flag、timing_send_time、act_status(draft/signup/closed/finished)+ 审计字段 |
| ActivityRead 已读 | `activity_read` | activity_id、class_id、student_id、read_time;家长打开详情按班级粒度标读(mock 为 classRead 班级粒度,实现时决定班级/学生粒度并与前端 ReadDetail 展示对齐) |
| Signup 报名回执 | `activity_signup` | activity_id、student_id、student_name、class_id、class_name、guardian_name、guardian_phone、join_count、status(signed/confirmed/canceled)、remark、signed_at;`(activity_id, student_id)` 唯一 |
| UpFile 附件(重建) | `up_file` | topic_type("activity")、topic_id、file_true_name、file_name、file_size、file_type |
| ContactGroup 自定义组 | `contact_group` | sch_id、name、create 审计字段 |
| ContactGroupUser 组成员 | `contact_group_user` | group_id、user_id(教师用户 id,**非账号**);`(group_id, user_id)` 唯一 |
| SysDict(保留微调) | `sys_dict` | 现有 dict_type/dict_key/dict_value/sort_order/enable_flag 结构够用;dict_type 取值约定 `dept` / `category` |
派生数据不落库:readCount/allowCount/progress/统计页全部实时聚合(按接收班级 studentCount 累加),与 mock 行为一致。
### 任务 4:移植 UserContext 用户上下文
从 UserclassService 复制并改包名为 `com.yunhai.dormnotice.*`:
1. `utils/UserContext.java`(ThreadLocal 持有器 + UserInfo 快照 + wrap 异步包装 + clear)
2. `aspect/ControllerUserContextAspect.java`(核心:BoaUser 参数 → BoaUserContext → request attribute 三级回退;`currentrole` header 解析 UserSysRole;超管 schId=0;IP 取 X-Forwarded-For 链)
3. `model/UserSysRole.java`、`enums/UserType.java` / `UserRole.java`
4. `config/WebMvcConfig`(如采用拦截器方案则一并移植 `UserContextInterceptor`;二选一,以 Aspect 为主)
5. 所有业务代码取当前用户统一走 `UserContext`;日志切面(任务 2)同样依赖它
外部依赖仅 `yh-auth-spring-boot-starter`(BoaUser/BoaUserContext,DormNotice 已具备)+ lombok,无需新增依赖。
### 任务 5:按前端契约实现后端接口
Controller 全部沿用 `/{platform}` 前缀(api/manage/mobiapi),方法名 PascalCase 对齐前端,写操作 POST;POST form 类接口参数只从 query 接收(`@RequestParam`)。
| Controller | 接口(前端已定义,路径以 activity.ts 为准) |
|---|---|
| BaseController | GetUserInfo、GetUserMod、GetSchoolAll、**GetClassTree(新增:调 userclass `/oapi` 取年级→班级树+studentCount)**、**SetRole(新增:教师/家长角色切换)**、FileUpload、Logout |
| ActivityController | List、InitializeRed、SendList、Detail、GetDetail、GetNextPrev、ReadDetail、ReceiveClasses、Create、Edit、Delete、UserPull、SetTopmost、Expedite、Signups、SignupDetail、Review、Remind、ExportSignups(CSV+BOM)、MyActivities、MySignup、Signup、CancelSignup、AllFileDown(zip)、Statis |
| DictController | List(**兼容 type=1/2 数字与 dept/category 字符串两种入参**)、ManageList、Add、Update、Delete、Sort、**Sync(新增:getSchModuleIsSms)** |
| FileController | Upload、Down、Preview、Delete、Bind、List(保留改造) |
| ContactsController | GetGroup、AddGroup、EditGroup、DelGroup、GetContactsTea、SetContactsTea、GetGroupTree(按新实体重写) |
实现要点:
- 统一响应 `{code,msg,data}` 由平台 ApiResponseAdvice 包装,Controller 返回裸对象。
- 分页返回对齐 `BasePageResult`:total/content/pageIndex/pageSize/totalPages/hasNext。
- 名额校验:Signup 时 quota>0 且按 join_count 合计已满则拒绝(mock 已有此行为,保持一致)。
- 家长端数据按 currentrole 的 childId/studentId 定位班级,只看本班。
### 任务 6:前端修改(删 mock + 字典重复 bug)
1. **删除 mock**:
- 删 `src/assets/mock/activityapi.ts`、`index.ts`,移除 `vite.config.ts` 的 `viteMockServe` 与依赖 `vite-plugin-mock`/`mockjs`,删 `.env.mock`、`dev:mock` 脚本及 `VITE_IS_MOCK` 引用(host.config.ts / Basics.d.ts)。
- mock 内容转做**后端种子数据/行为规格**:字典种子(dept 8 项、category 9 项)写入后端初始化 SQL;接口行为(标读、名额、CSV BOM、actStatus 流转)以 mock 实现为准。
2. **修复字典提交文字重复 bug**(多层防御):
- 前端 `DictCardManager.vue` modalSave:加 submitting 标志 + 提交按钮 `:loading`,防双击重复提交(当前无守卫,是同名卡片重复的直接原因)。
- 前端 `TypeSetBox.vue` 内联改名:`on-blur`/`on-enter` 双触发防护从"500ms 时间窗"改为"提交后直到下次进入编辑才重置"。
- 后端 `DictController.Add/Update`:按 `(sch_id, dict_type, dict_value)` 查重,重复直接报业务错,兜底 query+body 双发导致的 `name=x,x`。
- 回归确认 `postForm` 只拼 query 不带 body 的约定不被破坏。
- 顺带修 `ActivitySetting.vue` 的 groupModalSave 防连点。
3. **修 proxy 端口**:`vite.config.ts` 代理目标 `http://localhost:8104` → `http://localhost:8105`(新后端端口)。
### 任务 7:联调测试与优化(账号 lijing / a123123、kegemo / csxs1)
两个账号在前后端代码中均不存在,属基础平台(CAS SSO / 用户中心)账号;联调时先确认其角色(预期一个教师端、一个家长端)与所属学校 schId。
测试清单:
- 登录链路:CAS 跳转 → 会话建立 → GetUserInfo/GetUserMod 菜单加载 → currentRole header 传递 → UserContext 填充正确(userId/schId/roleId)。
- 教师端全流程:发布活动(立即/草稿/定时)→ 我发布的列表 → 编辑/复用/置顶/撤回/删除 → 报名情况(筛选/代确认/代取消/催报名/导出 CSV)→ 阅读情况 → 统计页 → 设置页(字典 CRUD、排序、启停用、自定义组及成员)。
- 家长端全流程(/par):活动广场分桶 → 打开详情标读 → 报名/改报名/取消 → confirm 型自动确认 → 名额满置灰 → 我的报名。
- 通用:附件上传/下载/打包下载、多租户隔离(换学校账号验证)、日志切面落库、字典文字重复 bug 回归。
已知问题/待优化(联调时逐项核实):
- proxy 端口不一致(任务 6.3)。
- dict type 数字/字符串双契约兼容(任务 5)。
- GetClassTree / SetRole / Dict Sync 三个后端缺失接口(任务 5)。
- ExportSignups CSV 需带 BOM(Excel 中文乱码);AllFileDown 空附件时的表现。
- 定时发布 timingSendTime 到点前家长端不可见——是否需要调度器或查询时过滤(倾向查询时过滤,简单可靠)。
- smsFlag 短信标记:DefaultSmsSender 是空占位,保持占位并打日志即可。
---
## 三、实施顺序
| 阶段 | 内容 | 产出/验收 |
|---|---|---|
| 一 | 任务 1 改名 + 任务 2 删实体、移植日志表 | `mvn compile` 通过,启动无旧实体报错,sys_dict/sys_operation_log 正常 |
| 二 | 任务 4 UserContext 移植 | 任意接口可打印 UserContext 六字段,请求结束 clear |
| 三 | 任务 3 新实体 + 任务 5 接口实现 | 全部 40 个接口按 activity.ts 契约可用(Postman/Swagger 自测) |
| 四 | 任务 6 前端删 mock、修 bug、修 proxy | `npm run dev` 走真实后端,字典新增不再重复 |
| 五 | 任务 7 双账号联调、查 bug、优化 | 测试清单全过,问题记录并修复 |
## 四、风险与待确认
1. **日志表**:DormNotice 现无日志实体,按 UserclassService 的 `SysOperationLog` 全套移植(含 AOP/异步落库),若只需表+基础切面可裁剪重试字段。
2. **测试账号**:lijing / kegemo 的角色、学校、密钥需在基础平台确认;若为本地开发库账号,需在用户中心造数。
3. **Nacos 配置**:服务改名后 dataId 变更,测试/生产环境需运维同步(本地默认关闭 Nacos 不受影响)。
4. **旧数据**:`dorm_notice` 库中 notice 等旧表是否保留、清理或迁移,需确认(计划:保留不删,仅代码层不再引用)。
5. **已读粒度**:mock 按班级标读(classRead),若产品要求学生粒度,`activity_read` 表结构需调整并同步前端 ReadDetail 展示。
6. **userclass 依赖**:GetClassTree 需调班级服务 `/oapi`(appkey 签名),确认 DormNotice 的 app-key 有权访问该接口。