This commit is contained in:
+201
@@ -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
@@ -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 等)保留在库中不删,仅代码层不再引用。
|
||||
@@ -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 有权访问该接口。
|
||||
Reference in New Issue
Block a user