feat: 多处页面改造 + 新增劳务协议模板配置
主要改动: - biz_meeting_attendee 中间表 + doctor/expert 角色数据隔离 - biz_meeting_attendee 加 handsign/labor_protocol 字段 (劳务协议改造) - biz_labor_protocol_template 配置表 + admin 页面 (从 HeguiConstants 迁移) - biz_expert.expertId 改 Long + IdGenerator 生成 (去自增) - Login.vue / PublicityDetail.vue / Home.vue 等多处 bug 修复 + UI 改进 - 新增 page-tech-review 报告 3 篇 (_self/*.md) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,340 @@
|
||||
# /doctor/home 页面端到端审查 (v3)
|
||||
|
||||
**审查日期**: 2026-08-19 (v3 含修复)
|
||||
**审查范围**: 前端 → 后端 → DB → 原型
|
||||
**审查者**: Claude Code (page-tech-review skill)
|
||||
|
||||
> **修订 (v1 → v2)**:
|
||||
> - ✅ P1 数据隔离: `biz_meeting_attendee` 中间表 + mapper EXISTS + controller 注入 (doctor/expert 角色)
|
||||
> - ✅ 欢迎栏显示真实姓名 (`biz_expert.name` → fallback 到 nickName/userName)
|
||||
>
|
||||
> **修订 (v2 → v3)**:
|
||||
> - ✅ "待签署协议" 数据源从 `biz_project_plan` 改到 `biz_meeting_attendee` (按 user_id + 任一未签)
|
||||
> - ✅ `biz_meeting_attendee` 表加 2 字段: `handsign longtext` (Base64 手写签名) + `labor_protocol varchar(500)` (劳务协议 URL)
|
||||
> - ✅ 新建后端 controller + service + mapper + 前端 API
|
||||
> - ❌ 签署 dialog + 手写板 UI **未做** (v3 待修)
|
||||
> - ❌ 新建会议自动写 attendee 中间表 (BizMeetingController.add) **未做**
|
||||
|
||||
---
|
||||
|
||||
## 0. 组件定位 (四跳链)
|
||||
|
||||
| 跳 | 命中 |
|
||||
|---|---|
|
||||
| ① 角色菜单 | `AdminLayout.vue:90` `MENU.doctor` → `{ path: '/doctor/home', title: '首页' }` |
|
||||
| ② 路由 | `router/index.js:90` → `name: 'doctor-home', component: () => import('@/views/doctor/Home.vue')` |
|
||||
| ③ 组件 | `ry-vue3/src/views/doctor/Home.vue` |
|
||||
| ④ 原型 | `proto/html/doctor.html:204` `data-page="components/home.html"` → `proto/html/components/home.html` |
|
||||
|
||||
---
|
||||
|
||||
## 1. 主要功能 + 可见性
|
||||
|
||||
### 1.1 主要功能
|
||||
|
||||
页面是 **dashboard 工作台**,4 块内容:
|
||||
|
||||
| 功能 | 前端入口 | 后端接口 | 数据表 |
|
||||
|---|---|---|---|
|
||||
| 欢迎栏 (实时时钟 + 专家姓名) | `Home.vue:6-15` | `GET /business/expert/profile` | `biz_expert` |
|
||||
| 待参加的会议 | `Home.vue:114` `bizList('meeting', {pageSize: 5})` | `GET /business/meeting/list` | `biz_meeting` + `biz_meeting_attendee` ✅ |
|
||||
| **待签署的协议 (v3 改)** | `Home.vue:126` `listUnsignedMeetingProtocols()` | `GET /business/meetingAttendee/unsigned` | `biz_meeting_attendee` (handsign 或 labor_protocol 为空) ✅ |
|
||||
| 通知消息 | `Home.vue` `listMyMessages({limit: 5})` | `GET /business/message/my` | `biz_message` |
|
||||
|
||||
页面**无 CRUD 操作**,纯展示 (v3 计划加签署 dialog 但未做)。
|
||||
|
||||
### 1.2 可见性 (三层过滤)
|
||||
|
||||
| 层 | 来源 | 校验字段 |
|
||||
|---|---|---|
|
||||
| 前端菜单 | `AdminLayout.vue:90` `MENU.doctor` | 仅 `doctor` 角色可见 |
|
||||
| 路由守卫 | `router/index.js:88` `meta: { role: 'doctor' }` + `permission.js` | token 角色 |
|
||||
| 后端 | 各 controller 注入 + mapper `<if>` 过滤 | 详见 §1.1 |
|
||||
|
||||
**数据隔离现状** (v3):
|
||||
|
||||
| 数据块 | 隔离机制 |
|
||||
|---|---|
|
||||
| 欢迎栏 | `biz_expert` 按 token userId |
|
||||
| 待参加会议 | `biz_meeting_attendee` (EXISTS) — controller 注入 `userId` for doctor/expert |
|
||||
| **待签署协议 (v3)** | `biz_meeting_attendee.user_id = current user` (controller `SecurityUtils.getUserId()`) |
|
||||
| 通知消息 | `biz_message.user_id = current user` |
|
||||
|
||||
---
|
||||
|
||||
## 2. 筛选项 (filter-form)
|
||||
|
||||
**本页面无 filter-form**,跳过。
|
||||
|
||||
---
|
||||
|
||||
## 3. 工具栏按钮 (toolbar)
|
||||
|
||||
**本页面无 toolbar**,只有 3 个"更多"链接 (`Home.vue:22, 37, 57`)。
|
||||
|
||||
---
|
||||
|
||||
## 4. 表格 (el-table)
|
||||
|
||||
**本页面无表格**,使用 `ul/li` 自定义列表。
|
||||
|
||||
---
|
||||
|
||||
## 5. UI 元素映射
|
||||
|
||||
### 5.1 欢迎栏 (`Home.vue:6-15`)
|
||||
|
||||
| UI | 绑定 | 来源 |
|
||||
|---|---|---|
|
||||
| 用户名 (左) | `displayName` (computed) | 优先级: `biz_expert.name` → `nickName` → `userName` → '专家' |
|
||||
| 当前时间 | `nowTime` (setInterval) | 前端 Date |
|
||||
| 当前日期 | `nowDate` (setInterval) | 前端 Date |
|
||||
|
||||
### 5.2 待参加会议列表 (`Home.vue:24-32`)
|
||||
|
||||
| UI label | 绑定字段 | 后端 SQL | 命中表.字段 | 原型对照 |
|
||||
|---|---|---|---|---|
|
||||
| 会议名 | `m.meetingName` | selectList + `EXISTS biz_meeting_attendee` | `biz_meeting.meeting_name` | ✅ |
|
||||
| 状态/时间 | `formatTime(m.startTime)` | selectList | `biz_meeting.start_time` | ✅ |
|
||||
|
||||
### 5.3 待签署协议列表 (`Home.vue:39-49`) — v3 重大改动
|
||||
|
||||
| UI label | 绑定字段 | 后端 SQL | 命中表.字段 | 原型对照 |
|
||||
|---|---|---|---|---|
|
||||
| 协议名 | `s.meetingName` | `selectUnsignedByUserId` JOIN `biz_meeting` | `biz_meeting.meeting_name` (via JOIN) | ⚠️ 原型写"协议/意见书",实际显示会议名 |
|
||||
| 状态 | 固定 "待签署" | — | — | ⚠️ 实现只显示"待签署",原型有"已完成" |
|
||||
|
||||
**v2 → v3 数据流变化**:
|
||||
- **v2 (错误)**: `GET /business/projectPlan/list` → 按 `submitter_id` 过滤 (这是"投稿方案"语义, 不是劳务协议)
|
||||
- **v3 (正确)**: `GET /business/meetingAttendee/unsigned` → 当前用户 + 任一未签 → JOIN biz_meeting 取会议名
|
||||
|
||||
**API** (`ry-vue3/src/api/business/meetingAttendee.js`):
|
||||
```js
|
||||
export function listUnsignedMeetingProtocols() {
|
||||
return request({ url: '/business/meetingAttendee/unsigned', method: 'get' })
|
||||
}
|
||||
export function updateHandsign(id, handsign) { ... }
|
||||
export function updateLaborProtocol(id, laborProtocol) { ... }
|
||||
```
|
||||
|
||||
**后端 mapper SQL** (核心):
|
||||
```sql
|
||||
select a.id, a.meeting_id, a.user_id, a.handsign, a.labor_protocol,
|
||||
m.meeting_name as meetingName, m.start_time as startTime,
|
||||
m.end_time as endTime, m.project_name as projectName, m.project_no as projectNo
|
||||
from biz_meeting_attendee a
|
||||
inner join biz_meeting m on m.meeting_id = a.meeting_id
|
||||
where a.user_id = #{userId}
|
||||
and (a.handsign is null or a.handsign = ''
|
||||
or a.labor_protocol is null or a.labor_protocol = '')
|
||||
order by m.start_time asc
|
||||
```
|
||||
|
||||
**判定逻辑 (决策 A1)**: 任一未签即"待签署"。两条都有值就排除。
|
||||
|
||||
### 5.4 通知消息列表 (`Home.vue:59-69`)
|
||||
|
||||
略,同 v2 报告。
|
||||
|
||||
---
|
||||
|
||||
## 6. Java Entity ↔ 数据库表 一致性
|
||||
|
||||
### 6.1 biz_meeting_attendee (v3 实测, 2026-08-19)
|
||||
|
||||
```sql
|
||||
CREATE TABLE biz_meeting_attendee (
|
||||
id bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
meeting_id bigint NOT NULL COMMENT '会议ID (FK biz_meeting.meeting_id)',
|
||||
user_id bigint NOT NULL COMMENT '参会人 user_id (FK sys_user.user_id)',
|
||||
handsign longtext COMMENT '手写签名 Base64',
|
||||
labor_protocol varchar(500) DEFAULT NULL COMMENT '劳务协议 URL (OSS)',
|
||||
create_by varchar(64) DEFAULT '' COMMENT '添加人',
|
||||
create_time datetime DEFAULT NULL COMMENT '添加时间',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_meeting_user (meeting_id, user_id),
|
||||
KEY idx_user_id (user_id)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='会议参会人';
|
||||
```
|
||||
|
||||
### 6.2 BizMeetingAttendee Entity ↔ DB 字段对照
|
||||
|
||||
| 实体字段 | 中文列名 | Java 类型 | 表字段 | DB 类型 | 一致? |
|
||||
|---|---|---|---|---|---|
|
||||
| `id` | 主键 | `Long` | `id` | bigint AUTO_INCREMENT | ✅ |
|
||||
| `meetingId` | 会议ID | `Long` | `meeting_id` | bigint | ✅ |
|
||||
| `userId` | 参会人 user_id | `Long` | `user_id` | bigint | ✅ |
|
||||
| **`handsign`** | 手写签名 Base64 | `String` | `handsign` | longtext | ✅ (v3 新增) |
|
||||
| **`laborProtocol`** | 劳务协议 URL (OSS) | `String` | `labor_protocol` | varchar(500) | ✅ (v3 新增) |
|
||||
| `createBy` (BaseEntity) | 添加人 | `String` | `create_by` | varchar(64) | ✅ |
|
||||
| `createTime` | 添加时间 | `Date` | `create_time` | datetime | ✅ |
|
||||
| `meetingName` (transient) | — | `String` | JOIN `m.meeting_name` | — | ✅ (联表字段) |
|
||||
| `startTime` (transient) | — | `Date` | JOIN `m.start_time` | — | ✅ |
|
||||
| `endTime` (transient) | — | `Date` | JOIN `m.end_time` | — | ✅ |
|
||||
| `projectName` (transient) | — | `String` | JOIN `m.project_name` | — | ✅ |
|
||||
| `projectNo` (transient) | — | `String` | JOIN `m.project_no` | — | ✅ |
|
||||
|
||||
**5 个 transient 联表字段** 接收 `selectUnsignedByUserId` 的 JOIN 输出, 给前端用。
|
||||
|
||||
---
|
||||
|
||||
## 7. 索引检查
|
||||
|
||||
### 7.1 biz_meeting_attendee
|
||||
|
||||
| Key | 列 | 用途覆盖 |
|
||||
|---|---|---|
|
||||
| PRIMARY | `id` | ✅ |
|
||||
| uk_meeting_user | `meeting_id`, `user_id` (UNIQUE) | ✅ 防重复 + 精确查 |
|
||||
| idx_user_id | `user_id` | ✅ `selectUnsignedByUserId WHERE user_id = ?` 走这个索引 |
|
||||
|
||||
**评价**: 复合查询 (`WHERE user_id = X AND (handsign IS NULL OR labor_protocol IS NULL)`) 走 idx_user_id,然后行过滤。10k 行内完全 OK。
|
||||
|
||||
### 7.2 缺什么?
|
||||
|
||||
- ❌ **缺 `idx_user_status`**: 如果会议量大且空记录占比小, 想加速"待签署"查询, 可以加 `(user_id, handsign IS NULL, labor_protocol IS NULL)` 函数索引。但目前数据少, 暂不需要。
|
||||
- ❌ **缺 `idx_meeting_status`**: `WHERE meeting_id = X AND handsign IS NULL` 场景目前不存在 (管理端"看哪些医生没签"的视图未做), 暂不需要。
|
||||
|
||||
---
|
||||
|
||||
## 8. 与原型差异
|
||||
|
||||
### 8.1 实现新增 (超出原型)
|
||||
|
||||
| 项 | 实现 | 原型 |
|
||||
|---|---|---|
|
||||
| 实时时钟 | ✅ | ✅ 静态 |
|
||||
| 通知标记已读 | ✅ | ❌ |
|
||||
| "更多"链接跳 Vue 路由 | ✅ | ❌ 静态 HTML |
|
||||
| 空状态文案 | ✅ | ❌ |
|
||||
| 欢迎栏显示真实姓名 | ✅ (v2) | ❌ 静态写死 |
|
||||
| **数据源按 user 隔离** | ✅ (v2 加中间表) | ❌ |
|
||||
| **劳务协议按会议关联** | ✅ (v3 改 biz_meeting_attendee) | ❌ |
|
||||
|
||||
### 8.2 原型有但实现缺失
|
||||
|
||||
| 项 | 原型 | 实现 | 严重度 |
|
||||
|---|---|---|---|
|
||||
| item-icon 图标 (📅 📄) | ✅ | ❌ 无 | P2 |
|
||||
| section-title 左侧蓝条 | ✅ | ❌ 无 | P2 |
|
||||
| 欢迎栏渐变背景 | ✅ | ⚠️ 单色 | P3 |
|
||||
| **"已完成" 状态显示** (signed status) | ✅ `item-status.done` | ❌ 实现永远显示"待签署" | **P1** |
|
||||
| **签署交互** (点列表 → 弹 dialog → 上传 handsign + labor_protocol) | ✅ 原型点击 alert | ❌ 无 dialog, 点列表没反应 | **P1** |
|
||||
|
||||
### 8.3 文字 / 标签差异
|
||||
|
||||
| 项 | 原型 | 实现 |
|
||||
|---|---|---|
|
||||
| 协议状态文案 | "待签署 / 已完成" | "待签署" (永远) |
|
||||
| 协议名显示 | "《方案名》专家意见书" | "会议名" (会议关联, 语义不同) |
|
||||
|
||||
### 8.4 总结
|
||||
|
||||
**v3 整体方向**: 数据语义修正 (劳务协议按会议关联 ✅), 视觉 + 交互仍待补。
|
||||
|
||||
---
|
||||
|
||||
## 9. 字段冗余 / 设计问题
|
||||
|
||||
### 9.1 biz_meeting_attendee 设计合理 (v3 验证)
|
||||
|
||||
- `handsign longtext` 存 Base64 是合理的: 不依赖 OSS, 数据完整性内嵌 DB, 简单
|
||||
- `labor_protocol varchar(500)` 存 OSS URL 是合理的: 大文件走 OSS, DB 只存引用
|
||||
|
||||
### 9.2 缺管理端点 ⚠️ P1
|
||||
|
||||
`BizMeetingAttendeeController` 目前只有 doctor 用的 3 接口。**新建会议时, 没有 API 自动把参会人写入中间表**。
|
||||
- 影响: 现有数据需要手动 INSERT (像之前那样用 SQL), 业务没法在线给会议加参会人
|
||||
- 修复: `BizMeetingController.add` 接受 `attendeeUserIds: Long[]`, 批量 insert 中间表
|
||||
|
||||
### 9.3 缺管理列表 (P3)
|
||||
|
||||
没有"看某会议的所有参会人 + 签署状态"的后台接口 (manager 端要用)。
|
||||
- 影响小 (低优先级), 当前 P1 先修
|
||||
|
||||
### 9.4 biz_project_plan 完全废弃? ⚠️
|
||||
|
||||
v3 把"待签署协议"从 biz_project_plan 改走中间表, 但 `biz_project_plan` 表和 `BizProjectPlan` entity 还在。
|
||||
- 投稿方案业务 (manager 审稿) 仍存在, 这是独立的 "项目策划方案投稿" 业务
|
||||
- 与劳务协议是两回事, 不应混淆
|
||||
|
||||
### 9.5 biz_meeting.status 缺失 (沿 v2 待修)
|
||||
|
||||
业务希望"待签署协议"按 `meeting.status` 过滤 (会议取消/已结束的不再显示待签), 但 `biz_meeting` 无 status 字段, 需要 ALTER。
|
||||
|
||||
---
|
||||
|
||||
## 10. 待修复列表 (v3)
|
||||
|
||||
| # | 问题 | 文件 | 修复建议 | 严重度 | 状态 |
|
||||
|---|---|---|---|---|---|
|
||||
| ~~1~~ | ~~`biz_meeting` 无 user 隔离~~ | ~~Controller + mapper~~ | ~~中间表 EXISTS~~ | ~~P1~~ | ✅ v2 修 |
|
||||
| ~~2~~ | ~~欢迎栏显示数字 userName~~ | ~~Home.vue~~ | ~~displayName computed 优先级链~~ | ~~P2~~ | ✅ v2 修 |
|
||||
| ~~3~~ | ~~劳务协议数据源错 (biz_project_plan)~~ | ~~Home.vue + BizProjectPlanController~~ | ~~改走 biz_meeting_attendee~~ | ~~P0~~ | ✅ **v3 修** |
|
||||
| ~~4~~ | ~~表无 handsign/labor_protocol~~ | ~~biz_meeting_attendee~~ | ~~ALTER 加 2 列 + 后端全套接口~~ | ~~P0~~ | ✅ **v3 修** |
|
||||
| 6 | **签署 dialog 未做**: 点"待签署"列表项无反应 | `Home.vue` | 弹 dialog: handsign 用 textarea 输入 Base64 (临时), labor_protocol 用 OssImageUploader, 提交分别 PUT | **P1** | 待修 |
|
||||
| 7 | **手写板 canvas 组件未做**: handsign 暂用 textarea 输入 Base64, 用户体验差 | 新建 `SignaturePad.vue` | vue-signature-pad 库 或 自写 canvas + toDataURL() | P2 | 待修 |
|
||||
| 8 | item-icon 图标缺失 (📅 📄) | `Home.vue` | 加 `.item-icon` 样式 + 模板 | P2 | 待修 (v1 提的) |
|
||||
| 9 | section-title 左侧蓝条缺失 | `Home.vue:151` | border-left + padding-left | P2 | 待修 (v1 提的) |
|
||||
| 10 | 欢迎栏渐变背景降级为单色 | `Home.vue:145` | 还原 gradient | P3 | 待修 (v1 提的) |
|
||||
| 11 | `biz_project_plan.status` 注释缺失 | DB | ALTER COMMENT | P3 | 待修 |
|
||||
| 12 | `biz_meeting.status` 不存在 (无法过滤"已结束"会议) | DB + mapper | ALTER 加 status 字段 | P3 | 待修 |
|
||||
| 13 | `Home.vue:131` `read: a.read \|\| i >= 2` 强制第3条后标已读 | `Home.vue` | 改为纯 `read: a.read` | P3 | 待修 |
|
||||
| 14 | **新建会议自动写 attendee 中间表**: BizMeetingController.add 不支持传 attendeeUserIds | `BizMeetingController.java` + service | add 接受 `attendeeUserIds: Long[]`, 批量 insert 中间表 | **P1** | 待修 |
|
||||
| 15 | 缺管理端"看参会人列表 + 签署状态"接口 | 新建 `BizMeetingAttendeeController` 管理端点 | `GET /business/meetingAttendee/byMeeting/{meetingId}` | P3 | 待修 |
|
||||
|
||||
---
|
||||
|
||||
## 11. 引用清单
|
||||
|
||||
### v3 新增/改的文件
|
||||
|
||||
| 文件 | 行号 | 说明 |
|
||||
|---|---|---|
|
||||
| **DB** `biz_meeting_attendee` | — | ALTER 加 `handsign longtext` + `labor_protocol varchar(500)` |
|
||||
| `ry-api/.../controller/BizMeetingAttendeeController.java` | 全文 | **v3 新建** |
|
||||
| `ry-api/.../service/IBizMeetingAttendeeService.java` | 全文 | **v3 新建** |
|
||||
| `ry-api/.../service/impl/BizMeetingAttendeeServiceImpl.java` | 全文 | **v3 新建** |
|
||||
| `ry-api/.../domain/BizMeetingAttendee.java` | 全文 | **v3 加 handsign + laborProtocol + 5 个 transient 联表字段** |
|
||||
| `ry-api/.../mapper/BizMeetingAttendeeMapper.java` | 全文 | **v3 加 updateHandsign + updateLaborProtocol + selectUnsignedByUserId** |
|
||||
| `ry-api/.../mapper/business/BizMeetingAttendeeMapper.xml` | 全文 | **v3 加 resultMap 字段 + 2 update + selectUnsignedByUserId 联表 SQL** |
|
||||
| `ry-vue3/src/api/business/meetingAttendee.js` | 全文 | **v3 新建**: 3 API helpers |
|
||||
| `ry-vue3/src/views/doctor/Home.vue` | 39-49, 124-134 | **v3 改 "待签署协议" 数据源** |
|
||||
|
||||
### 未改的文件 (沿 v2)
|
||||
|
||||
| 文件 | 行号 | 说明 |
|
||||
|---|---|---|
|
||||
| `ry-vue3/src/layout/AdminLayout.vue` | 90 | `MENU.doctor` 第 1 项 |
|
||||
| `ry-vue3/src/router/index.js` | 88-99 | `/doctor` 父路由 |
|
||||
| `proto/html/doctor.html` | 204 | 原型菜单 |
|
||||
| `proto/html/components/home.html` | 全文 | 原型基准 |
|
||||
| `ry-vue3/src/api/public.js` | — | listMyMessages / bizList |
|
||||
| `ry-vue3/src/api/business/expert.js` | 全文 | v2 新建: getMyExpertProfile |
|
||||
| `ry-api/.../controller/BizMeetingController.java` | 23-33 | v2 改: 加 userId 注入 |
|
||||
| `ry-api/.../controller/BizProjectPlanController.java` | — | 不再被"待签署协议"调用, 投稿方案独立业务 |
|
||||
| `ry-api/.../controller/BizExpertController.java` | 46-50 | 欢迎栏取名 |
|
||||
| MySQL `guoju0808.biz_meeting` | — | v2 无改动 |
|
||||
| MySQL `guoju0808.biz_meeting_attendee` | — | **v3 加 2 列** |
|
||||
| MySQL `guoju0808.biz_message` | — | 通知 |
|
||||
| MySQL `guoju0808.biz_expert` (user_id=142) | — | name='dct07' |
|
||||
|
||||
---
|
||||
|
||||
## v3 总结
|
||||
|
||||
**修复 2 项 (跨 v2 → v3)**:
|
||||
1. ✅ **数据语义修正**: "待签署协议" 从 `biz_project_plan` 改到 `biz_meeting_attendee` (语义正确: 劳务协议按会议关联, 不按投稿方案)
|
||||
2. ✅ **新字段 + 后端**: `handsign longtext` + `labor_protocol varchar(500)` + 完整后端 CRUD 接口 + 前端 API
|
||||
|
||||
**还剩 11 项待修复**,按严重度:
|
||||
- **P1 (2 项)**: 签署 dialog 缺失 (点列表没反应); 新建会议没自动写 attendee
|
||||
- **P2 (3 项)**: 手写板 UI; item-icon; section-title 边框
|
||||
- **P3 (6 项)**: 字段注释 + status 字段 + 索引 + 前端兜底 + 管理端点
|
||||
|
||||
**SQL 实测验证** (user_id=142 当前状态):
|
||||
```
|
||||
待签署会议: 3 个 (id=1,2,3 全部 handsign=NULL labor_protocol=NULL)
|
||||
会议名: 整合医学学会项目评审会 / 基层医疗改革试点中期评估会 / 数字化医疗转型方案评审会
|
||||
按 start_time 升序 ✓
|
||||
```
|
||||
@@ -0,0 +1,285 @@
|
||||
# /manager/accounts 页面端到端审查
|
||||
|
||||
**审查日期**: 2026-08-19
|
||||
**审查范围**: 前端 → 后端 → DB → 原型
|
||||
**审查者**: Claude Code (page-tech-review skill)
|
||||
|
||||
---
|
||||
|
||||
## 0. 组件定位 (四跳链)
|
||||
|
||||
| 跳 | 命中 |
|
||||
|---|---|
|
||||
| ① 角色菜单 | `AdminLayout.vue:105` `MENU.manager` → `{ path: '/manager/accounts', title: '账号管理' }` |
|
||||
| ② 路由 | `router/index.js:85` → `name: 'manager-accounts', component: () => import('@/views/manager/Accounts.vue')` |
|
||||
| ③ 组件 | `ry-vue3/src/views/manager/Accounts.vue` |
|
||||
| ④ 原型 | `proto/html/components/account-manage.html` (从 `proto/html/manager.html:213` `data-page="components/account-manage.html"` 链接) |
|
||||
|
||||
---
|
||||
|
||||
## 1. 主要功能 + 可见性
|
||||
|
||||
### 1.1 主要功能
|
||||
|
||||
| 功能 | 前端入口 | 后端接口 | 数据表 |
|
||||
|---|---|---|---|
|
||||
| 查看个人信息 | `Accounts.vue:41-48` `loadProfile()` | `GET /system/user/profile` | `sys_user` |
|
||||
| 修改姓名/手机号 | `Accounts.vue:57` `onSave` 第 1 个 request | `PUT /system/user/profile` | `sys_user` (nick_name, phonenumber, sex) |
|
||||
| 修改密码 | `Accounts.vue:59` `onSave` 第 2 个 request | `PUT /system/user/profile/updatePwd` | `sys_user` (password, pwd_update_date) |
|
||||
|
||||
页面只支持 **编辑自己的资料**,**没有 CRUD 列表**——非典型管理页,本质是"个人中心"。
|
||||
|
||||
### 1.2 可见性 (三层过滤)
|
||||
|
||||
| 层 | 来源 | 校验字段 | 备注 |
|
||||
|---|---|---|---|
|
||||
| 前端菜单 | `AdminLayout.vue:95` `MENU.manager` | 仅 `manager` 角色可见 | 其他角色走 `/<role>/account` 路径 |
|
||||
| 路由守卫 | `router/index.js:61` `meta: { role: 'manager' }` + `permission.js` | token 角色 | 通过则进 |
|
||||
| 后端 | `SysProfileController.java` | 无 `role_type` 校验,任何已登录用户均可改自己的资料 | `currentUser = loginUser.getUser()` |
|
||||
|
||||
**不能看到本页面的角色**: admin / leader / doctor / executor / sponsor — 他们各自有 `/<role>/account` 路由(其中 admin/leader 走 `/leader/account`,doctor 走 `/doctor/account`,executor/sponsor 没有 account 页)。
|
||||
|
||||
**数据隔离**: 因为是"个人中心",后端用 `currentUser = loginUser.getUser()` 自动从 token 取本人 userId,前端无法传别人的 userId,所以天然隔离。
|
||||
|
||||
---
|
||||
|
||||
## 2. 筛选项 (filter-form)
|
||||
|
||||
**本页面无 filter-form**,只有一个 el-form 表单,所以筛选项章节不适用。
|
||||
|
||||
---
|
||||
|
||||
## 3. 工具栏按钮 (toolbar)
|
||||
|
||||
**本页面无 toolbar**,只有"保存 / 取消"两个表单按钮,详见第 5 章"表单内操作"。
|
||||
|
||||
---
|
||||
|
||||
## 4. 表格 (el-table)
|
||||
|
||||
**本页面无表格**,跳过。
|
||||
|
||||
---
|
||||
|
||||
## 5. 表单内操作按钮
|
||||
|
||||
### 5.1 字段映射
|
||||
|
||||
| UI label | 控件 | 绑定字段 | 后端 SQL | 命中表.字段 | 选项来源 | 原型对照 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 姓名 | `el-input` | `form.nickName` | `updateUser` `<if test="nickName != null and nickName != ''">nick_name = #{nickName},</if>` | `sys_user.nick_name` | — | ✅ 姓名 |
|
||||
| 手机号 | `el-input` (maxlength=11) | `form.phonenumber` | `updateUser` `phonenumber = #{phonenumber},` + `checkPhoneUnique` | `sys_user.phonenumber` (varchar(11)) | — | ✅ 手机号 |
|
||||
| 原密码 | `el-input` type=password | `form.oldPassword` | `updatePwd` 校验旧密码 (`SecurityUtils.matchesPassword`) | `sys_user.password` | — | ✅ 原密码 |
|
||||
| 新密码 | `el-input` type=password | `form.newPassword` | `resetUserPwd` `password = #{password}, pwd_update_date = sysdate()` | `sys_user.password` / `pwd_update_date` | — | ✅ 新密码 |
|
||||
| 确认密码 | `el-input` type=password | `form.confirmPassword` | 纯前端校验:必须等于新密码 | — | — | ✅ 确认密码 |
|
||||
|
||||
**Sex 隐式传**: `Accounts.vue:57` `data: { ..., sex: profile.value.sex }`,虽然 UI 没显示性别控件,但 sex 字段在 PUT 时跟着送。Mapper `<if test="sex != null and sex != ''">` 会原值回写(若 sex 是 "0/1/2")。**Sex 没被 UI 控制 = 用户改不了** (见 P2-4)。
|
||||
|
||||
### 5.2 表单按钮
|
||||
|
||||
| 按钮 | 触发函数 | 接口 | 后端动作 | 涉及表.字段 | 原型对照 |
|
||||
|---|---|---|---|---|---|
|
||||
| 保存 | `onSave` | PUT `/system/user/profile` (+ `/updatePwd` 如有填密码) | `SysProfileController.updateProfile` → `userService.updateUserProfile` → `userMapper.updateUser` | `sys_user.nick_name/phonenumber/sex` (+ `password/pwd_update_date`) | ✅ |
|
||||
| 取消 | `onCancel` | — | 重置表单为初始 profile 快照 | — | ✅ |
|
||||
|
||||
### 5.3 真实落点说明
|
||||
|
||||
- **修改姓名/手机号/sex**: 落到 `sys_user.nick_name/phonenumber/sex`(`Accounts.vue:57`)。`userId` 来自 `loginUser` 而非表单(后端从 token 取 `LoginUser.getUser()`,`SysProfileController.java:55-74`),前端无法改 userId,无法越权改别人。
|
||||
- **修改密码**: 落到 `sys_user.password`(`SysProfileController.java:115`)。新密码在 service 层 `SecurityUtils.encryptPassword()` 加密(BCrypt),不存在明文落库。
|
||||
- **缓存同步**: 修改成功后 `tokenService.setLoginUser(loginUser)` 更新 Spring Security 缓存,下次请求拿到的 user 是新的(`SysProfileController.java:87, 120`)。
|
||||
|
||||
---
|
||||
|
||||
## 6. Java Entity ↔ 数据库表 一致性
|
||||
|
||||
### 6.1 实测 sys_user DDL (2026-08-19)
|
||||
|
||||
```sql
|
||||
CREATE TABLE `sys_user` (
|
||||
`user_id` bigint NOT NULL AUTO_INCREMENT COMMENT '用户ID',
|
||||
`dept_id` bigint DEFAULT NULL COMMENT '部门ID',
|
||||
`user_name` varchar(30) NOT NULL COMMENT '用户账号',
|
||||
`nick_name` varchar(30) NOT NULL COMMENT '用户昵称',
|
||||
`user_type` varchar(2) DEFAULT '00' COMMENT '用户类型(00系统用户)',
|
||||
`account_type` varchar(8) DEFAULT 'MAIN' COMMENT '账号类型 (MAIN=主账号/SUB=子账号)',
|
||||
`parent_user_id` bigint DEFAULT NULL COMMENT '主账号ID (子账号关联)',
|
||||
`role_type` varchar(20) DEFAULT 'executor',
|
||||
`email` varchar(50) DEFAULT '' COMMENT '用户邮箱',
|
||||
`phonenumber` varchar(11) DEFAULT '' COMMENT '手机号码',
|
||||
`sex` char(1) DEFAULT '0' COMMENT '用户性别(0男 1女 2未知)',
|
||||
`avatar` varchar(100) DEFAULT '' COMMENT '头像地址',
|
||||
`password` varchar(100) DEFAULT '' COMMENT '密码',
|
||||
`status` char(1) DEFAULT '0' COMMENT '账号状态(0正常 1停用)',
|
||||
`del_flag` char(1) DEFAULT '0' COMMENT '删除标志(0代表存在 2代表删除)',
|
||||
`login_ip` varchar(128) DEFAULT '' COMMENT '最后登录IP',
|
||||
`login_date` datetime DEFAULT NULL COMMENT '最后登录时间',
|
||||
`pwd_update_date` datetime DEFAULT NULL COMMENT '密码最后更新时间',
|
||||
`create_by` varchar(64) DEFAULT '' COMMENT '创建者',
|
||||
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
|
||||
`update_by` varchar(64) DEFAULT '' COMMENT '更新者',
|
||||
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
|
||||
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
|
||||
PRIMARY KEY (`user_id`),
|
||||
KEY `idx_parent_user_id` (`parent_user_id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=138 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户信息表';
|
||||
```
|
||||
|
||||
### 6.2 SysUser Entity ↔ DB 字段对照
|
||||
|
||||
| 实体字段 | 中文列名 (DB COMMENT) | Java 类型 | 表字段 | DB 类型 | 一致? |
|
||||
|---|---|---|---|---|---|
|
||||
| `userId` | 用户ID | `Long` | `user_id` | `bigint NOT NULL AUTO_INCREMENT` | ✅ |
|
||||
| `deptId` | 部门ID | `Long` | `dept_id` | `bigint DEFAULT NULL` | ✅ |
|
||||
| `userName` | 用户账号 | `String` | `user_name` | `varchar(30) NOT NULL` | ✅ |
|
||||
| `nickName` | 用户昵称 | `String` | `nick_name` | `varchar(30) NOT NULL` | ✅ |
|
||||
| `userType` | 用户类型(00系统用户) | `String` | `user_type` | `varchar(2) DEFAULT '00'` | ✅ |
|
||||
| `accountType` | 账号类型 (MAIN=主账号/SUB=子账号) | `String` | `account_type` | `varchar(8) DEFAULT 'MAIN'` | ✅ |
|
||||
| `parentUserId` | 主账号ID (子账号关联) | `Long` | `parent_user_id` | `bigint DEFAULT NULL` | ✅ |
|
||||
| `roleType` | ⚠️ 空注释 (DB 没 COMMENT) | `String` | `role_type` | `varchar(20) DEFAULT 'executor'` | ⚠️ 列一致,但 DB 注释缺失 |
|
||||
| `email` | 用户邮箱 | `String` | `email` | `varchar(50) DEFAULT ''` | ✅ |
|
||||
| `phonenumber` | 手机号码 | `String` | `phonenumber` | `varchar(11) DEFAULT ''` | ✅ (Java 注解 `@Size(max=11)`) |
|
||||
| `sex` | 用户性别(0男 1女 2未知) | `String` | `sex` | `char(1) DEFAULT '0'` | ✅ |
|
||||
| `avatar` | 头像地址 | `String` | `avatar` | `varchar(100) DEFAULT ''` | ✅ |
|
||||
| `password` | 密码 | `String` | `password` | `varchar(100) DEFAULT ''` | ✅ |
|
||||
| `status` | 账号状态(0正常 1停用) | `String` | `status` | `char(1) DEFAULT '0'` | ✅ |
|
||||
| `delFlag` | 删除标志(0代表存在 2代表删除) | `String` | `del_flag` | `char(1) DEFAULT '0'` | ✅ |
|
||||
| `loginIp` | 最后登录IP | `String` | `login_ip` | `varchar(128) DEFAULT ''` | ✅ |
|
||||
| `loginDate` | 最后登录时间 | `Date` | `login_date` | `datetime DEFAULT NULL` | ✅ |
|
||||
| `pwdUpdateDate` | 密码最后更新时间 | `Date` | `pwd_update_date` | `datetime DEFAULT NULL` | ✅ |
|
||||
| `createBy` (BaseEntity) | 创建者 | `String` | `create_by` | `varchar(64) DEFAULT ''` | ✅ |
|
||||
| `createTime` (BaseEntity) | 创建时间 | `Date` | `create_time` | `datetime DEFAULT NULL` | ✅ |
|
||||
| `updateBy` (BaseEntity) | 更新者 | `String` | `update_by` | `varchar(64) DEFAULT ''` | ✅ |
|
||||
| `updateTime` (BaseEntity) | 更新时间 | `Date` | `update_time` | `datetime DEFAULT NULL` | ✅ |
|
||||
| `remark` (BaseEntity) | 备注 | `String` | `remark` | `varchar(500) DEFAULT NULL` | ✅ |
|
||||
|
||||
**无字段缺失**,**无类型不一致**。
|
||||
|
||||
---
|
||||
|
||||
## 7. 索引检查
|
||||
|
||||
### 7.1 sys_user 索引列表
|
||||
|
||||
| Key | 列 | 用途覆盖 |
|
||||
|---|---|---|
|
||||
| `PRIMARY` | `user_id` | ✅ 主键,所有 `WHERE user_id = ?` |
|
||||
| `idx_parent_user_id` | `parent_user_id` | ✅ 子账号查询 `WHERE parent_user_id = ?` |
|
||||
|
||||
### 7.2 缺口分析
|
||||
|
||||
| 查询场景 | 涉及列 | 现有索引 | 评价 |
|
||||
|---|---|---|---|
|
||||
| 修改密码 (`resetUserPwd`) | `user_id` | PRIMARY | ✅ |
|
||||
| 修改个人信息 (`updateUser`) | `user_id` | PRIMARY | ✅ |
|
||||
| 手机号唯一性校验 (`checkPhoneUnique`) | `phonenumber` | ❌ 无索引 | ⚠️ 暂可接受,行数 ~138 走全表扫描无压力 |
|
||||
| 邮箱唯一性校验 (`checkEmailUnique`) | `email` | ❌ 无索引 | ⚠️ 暂可接受 |
|
||||
| 登录态 `WHERE user_name = ?` | `user_name` | ❌ 无索引 | ⚠️ Spring Security 框架一般会走 Redis/缓存,DB 仅冷启动用 |
|
||||
|
||||
**结论**: 数据量小(<200 行),**所有查询暂可接受**。若 `sys_user` 未来增长到 10k+,应给 `user_name` / `phonenumber` 加唯一索引。
|
||||
|
||||
---
|
||||
|
||||
## 8. 与原型差异
|
||||
|
||||
原型: `proto/html/components/account-manage.html`
|
||||
实现: `ry-vue3/src/views/manager/Accounts.vue`
|
||||
|
||||
### 8.1 实现新增 (超出原型)
|
||||
|
||||
| 项 | 实现 | 原型 | 评价 |
|
||||
|---|---|---|---|
|
||||
| 表单校验规则 | ✅ `rules` 必填 + 手机号正则 `/^1[0-9]\d{9}$/` + 密码长度 6-20 + 两次密码一致 | ❌ 原型只放占位 | 改进,推荐保留 |
|
||||
| 确认密码实时校验 | ✅ `validator` 函数 | ❌ 无 | 改进 |
|
||||
| `maxlength="11"` 手机号限长 | ✅ (本次新增) | ❌ 原型不限 | 改进 |
|
||||
|
||||
### 8.2 原型有但实现缺失
|
||||
|
||||
| 项 | 原型 | 实现 | 严重度 |
|
||||
|---|---|---|---|
|
||||
| "基本信息 / 修改密码" 分区标题 (`form-section-title`) | ✅ | ❌ 平铺 5 项 | P2 |
|
||||
| page-title 风格 (h1 大标题 + 副标题) | ✅ `页面大标题` | ❌ 只用 breadcrumb | P3 (项目统一用 breadcrumb,可不改) |
|
||||
| `form-actions` 顶部 border-top 分隔 | ✅ | ❌ 紧贴"确认密码" | P2 |
|
||||
|
||||
### 8.3 文字 / 标签差异
|
||||
|
||||
| 项 | 原型 | 实现 | 一致? |
|
||||
|---|---|---|---|
|
||||
| 主标题 | "账号管理" | "首页 / 账号管理" (breadcrumb) | ⚠️ 项目规范用 breadcrumb,保持 |
|
||||
| 副标题 | "修改个人信息、修改密码" | — | P3 |
|
||||
| 密码提示语 | "*密码长度 6-20 位,支持数字、字母、特殊字符;留空表示不修改密码" | 同 | ✅ |
|
||||
|
||||
### 8.4 总结
|
||||
|
||||
**整体方向: 实现超出原型 (校验 + 限长)**,**分区分组缺失** (P2)。其他均与原型一致或为项目规范变体。
|
||||
|
||||
---
|
||||
|
||||
## 9. 字段冗余 / 设计问题
|
||||
|
||||
### 9.1 唯一约束语义
|
||||
|
||||
- `phonenumber` **无唯一索引**,但 `checkPhoneUnique` (mapper) 通过全表扫描 + `del_flag='0'` 过滤做软唯一。如果未来需要硬唯一 (例如登录用手机号),需加 `UNIQUE KEY uk_phonenumber (phonenumber)` 并处理历史重复数据。
|
||||
|
||||
### 9.2 JOIN vs 持久化字段
|
||||
|
||||
- 本表 `SysUser` 是基础表,所有字段都是持久化字段,无 JOIN 出来的视图字段 ✅
|
||||
|
||||
### 9.3 设计闭环
|
||||
|
||||
- `role_type` 列 DEFAULT `'executor'`,但本页面是 `manager` 角色,显然不可能 default 出 manager 角色 — 这是 [sys-user-role-type-truth](sys-user-role-type-truth.md) 内存里说的"sys_user.role_type 是真相源",新用户创建时需主动赋值。
|
||||
- `account_type` / `parent_user_id` 用于主/子账号关系,本页面不涉及,跳过。
|
||||
|
||||
### 9.4 安全闭环
|
||||
|
||||
- 手机号修改**无短信验证码校验**:`SysProfileController.updateProfile` 直接 `setPhonenumber`,对比 `changePhone` 接口走 `smsService.verifyCode`,**两条路径并存**:
|
||||
- 本页 `Accounts.vue:57` → 走 updateProfile → **绕过短信**
|
||||
- 假如有"换绑手机号"流程 → 走 changePhone → 走短信
|
||||
- 这是 RuoYi 框架默认行为(若系统部署方想强制短信,需要在 updateProfile 里也加校验)。属于**已知设计,非 bug**。
|
||||
|
||||
### 9.5 表格冗余
|
||||
|
||||
- 本页面无表,跳过。
|
||||
|
||||
---
|
||||
|
||||
## 10. 待修复列表
|
||||
|
||||
| # | 问题 | 文件 | 修复建议 | 严重度 |
|
||||
|---|---|---|---|---|
|
||||
| 1 | 表单无分区("基本信息 / 修改密码") | `Accounts.vue` | 用 `el-divider` 或自定义 `form-section-title` 视觉分组,与原型一致 | P2 |
|
||||
| 2 | 保存按钮无顶部分隔线,与上方密码字段视觉粘连 | `Accounts.vue` | 给 `el-form-item` 按钮组加 `border-top: 1px solid #f0f0f0` + `padding-top: 16px` | P2 |
|
||||
| 3 | `sex` 字段 UI 没暴露,但 `onSave` 静默带上 `profile.value.sex` | `Accounts.vue:57` | 若 sex 不可改,前端不要传;若可改,加 radio 控件(0男/1女/2未知) | P2 |
|
||||
| 4 | `email` UI 没暴露,`SysProfileController.updateProfile` 接受 email 入参但前端不传 | `Accounts.vue` + `SysProfileController` | 若产品定位不许改邮箱,前端显式删除 email 入参;若许改,UI 加输入框 | P2 |
|
||||
| 5 | 手机号修改无短信校验 (`updateProfile` 不走 `smsService.verifyCode`,仅 `changePhone` 走) | `SysProfileController.java:67-91` | 与产品确认:个人中心改手机号是否需要短信?需要则复用 `changePhone` 逻辑 | P1 |
|
||||
| 6 | `role_type` 列 DB 注释缺失,只有 `account_type` 有注释 | DB `sys_user.role_type` | `ALTER TABLE sys_user MODIFY COLUMN role_type varchar(20) DEFAULT 'executor' COMMENT '业务角色 (admin/leader/manager/doctor/executor/sponsor)'` | P3 |
|
||||
| 7 | `phonenumber` 无唯一索引,行数大后 `checkPhoneUnique` 全表扫描 | `sys_user` | 加 `UNIQUE KEY uk_phonenumber (phonenumber)` (需先排查历史重复) | P3 |
|
||||
| 8 | 用户名/手机号正则过宽 (`/^1[0-9]\d{9}$/` 接受第二位为 0/1/2) | `Accounts.vue:30` | 收紧为 `/^1[3-9]\d{9}$/` (大陆手机号第二位规范) | P3 |
|
||||
|
||||
---
|
||||
|
||||
## 11. 引用清单
|
||||
|
||||
| 文件 | 行号 | 说明 |
|
||||
|---|---|---|
|
||||
| `ry-vue3/src/views/manager/Accounts.vue` | 全文 | 审查目标 |
|
||||
| `ry-vue3/src/views/manager/Accounts.vue:7` | — | 手机号 input maxlength=11 (本次新增) |
|
||||
| `ry-vue3/src/views/manager/Accounts.vue:30` | — | 手机号正则 `/^1[0-9]\d{9}$/` |
|
||||
| `ry-vue3/src/views/manager/Accounts.vue:57` | — | 保存 profile (PUT /system/user/profile) |
|
||||
| `ry-vue3/src/views/manager/Accounts.vue:59` | — | 保存密码 (PUT /system/user/profile/updatePwd) |
|
||||
| `ry-vue3/src/layout/AdminLayout.vue` | 95-106 | `MENU.manager` 数组,`/manager/accounts` 在第 10 项 |
|
||||
| `ry-vue3/src/router/index.js` | 61, 85 | 路由表 `/manager` 父路由 + `manager-accounts` 子路由 |
|
||||
| `proto/html/manager.html` | 213 | 原型菜单链接 `data-page="components/account-manage.html"` |
|
||||
| `proto/html/components/account-manage.html` | 全文 | 原型基准 |
|
||||
| `ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysProfileController.java` | 37-215 | 后端 controller 全部 5 个接口 |
|
||||
| `ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysProfileController.java:67-91` | — | `updateProfile` (无短信校验的手机号更新) |
|
||||
| `ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysProfileController.java:97-124` | — | `updatePwd` (旧密码校验 + 加密 + 缓存同步) |
|
||||
| `ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysProfileController.java:131-162` | — | `changePhone` (带短信校验,本页面未使用) |
|
||||
| `ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysUser.java` | 21-317 | SysUser 实体 |
|
||||
| `ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysUser.java:149` | — | `@Size(min=0, max=11) phonenumber` |
|
||||
| `ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysUserServiceImpl.java` | 204-214 | `checkPhoneUnique` 实现 |
|
||||
| `ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysUserServiceImpl.java` | 377-381 | `updateUserProfile` → 调 `userMapper.updateUser` |
|
||||
| `ry-api/ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml` | 183-185 | `checkPhoneUnique` SQL |
|
||||
| `ry-api/ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml` | 231-251 | `updateUser` SQL (用 `<set><if>` 条件更新) |
|
||||
| `ry-api/ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml` | 269-271 | `resetUserPwd` SQL (改 password + pwd_update_date) |
|
||||
| `ry-api/ruoyi-admin/src/main/resources/application-druid.yml` | 10 | DB 连接信息 (password: cu2oh2co3) |
|
||||
| MySQL `guoju0808.sys_user` | — | 24 列 + 1 主键 + 1 副索引,实测 2026-08-19 |
|
||||
@@ -0,0 +1,379 @@
|
||||
# /publicity/13 页面端到端审查
|
||||
|
||||
**审查日期**: 2026-08-19
|
||||
**审查范围**: 前端 → 后端 → DB → 原型
|
||||
**目标页面**: `/publicity/13` (公示详情页)
|
||||
**审查者**: Claude Code (page-tech-review skill)
|
||||
|
||||
---
|
||||
|
||||
## 0. 组件定位 (四跳链)
|
||||
|
||||
| 跳 | 命中 |
|
||||
|---|---|
|
||||
| ① 入口 | `proto/html/项目公示.html:697` `window.location.href = '公示详情.html'` — 公示列表点 item 跳详情 |
|
||||
| ② 路由 | `router/index.js:10` → `{ path: 'publicity/:projectId', name: 'publicity-detail', component: () => import('@/views/portal/PublicityDetail.vue') }` |
|
||||
| ③ 组件 | `ry-vue3/src/views/portal/PublicityDetail.vue` |
|
||||
| ④ 原型 | `proto/html/公示详情.html` (跟实现 1 对 1, 文件名 `公示详情.html` 直接被项目公示列表引用) |
|
||||
|
||||
**说明**: `/publicity/13` 的 `13` 是 `biz_project.project_id`,实测 DB 中 `project_id=13` 的记录是测试数据 (`project_no='3', project_name='2'`,`invitation_url` 有一个 OSS PDF,`support_letter_url` NULL)。
|
||||
|
||||
---
|
||||
|
||||
## 1. 主要功能 + 可见性
|
||||
|
||||
### 1.1 主要功能
|
||||
|
||||
| 功能 | 前端入口 | 后端接口 | 数据表 |
|
||||
|---|---|---|---|
|
||||
| 查看公示详情 | `PublicityDetail.vue:355` `load()` | `GET /business/public/project/{projectId}` | `biz_project` |
|
||||
| 立即报名 | `PublicityDetail.vue:398-428` `onSignup()` | `POST /business/executionIntent/signup` | `biz_execution_intent` (RuoYi) |
|
||||
| 表达支持意向 (已登录快速通道) | `PublicityDetail.vue:468-478` `onSupportIntent()` | `POST /business/publicity/supportIntent` | `biz_publicity_support_intent` |
|
||||
| 表达执行意向 (已登录快速通道) | `PublicityDetail.vue:480-490` `onExecutionIntent()` | `POST /business/publicity/executionIntent` | `biz_publicity_execution_intent` |
|
||||
| 表达意向 (匿名 dialog) | `PublicityDetail.vue:195-216` `guestDialog` + `onGuestDialogConfirm()` | 同上 (共用 2 个 POST) | 同上 |
|
||||
| 查重"是否已提交" | `PublicityDetail.vue:437-448` `checkIntentSubmitted()` | `GET /business/publicity/hasIntent?projectId=&phone=&type=` | `biz_publicity_support_intent` / `biz_publicity_execution_intent` |
|
||||
| 分享二维码 | `PublicityDetail.vue:249-263` `QRCode.toDataURL` | — (前端纯计算) | — |
|
||||
|
||||
页面不是 CRUD 列表,而是 **单页 detail + 4 个交互按钮**。
|
||||
|
||||
### 1.2 可见性 (三层过滤)
|
||||
|
||||
| 层 | 来源 | 校验字段 | 备注 |
|
||||
|---|---|---|---|
|
||||
| 前端路由 | `router/index.js:10` | 无 meta.role,所有人均可访问 | `/publicity/:projectId` |
|
||||
| 前端菜单 | — (门户页无菜单) | — | 公示页无菜单,靠顶部导航"项目公示"链入 |
|
||||
| 后端 | `SecurityConfig.java:64` | `.requestMatchers("/business/public/**", "/business/publicity/**", "/business/auth/**", "/business/sms/**").permitAll()` | 公开端点已放行匿名访问,登录后走 token 校验身份 |
|
||||
|
||||
**不能看到本页面的角色**: 不存在角色隔离——**任何用户/访客**都能看公示详情 + 提交意向。
|
||||
|
||||
**报名按钮登录校验**: `PublicityDetail.vue:399-403` 前端拦截 `!loggedIn` → 跳转 `/login`。后端 `executionIntent/signup` 应也有 token 校验 (未实测,从项目其他 executionIntent 接口推断)。
|
||||
|
||||
---
|
||||
|
||||
## 2. 筛选项 (filter-form)
|
||||
|
||||
**本页面无 filter-form**,跳过。
|
||||
|
||||
---
|
||||
|
||||
## 3. 工具栏按钮 (toolbar)
|
||||
|
||||
**本页面无传统 toolbar**,操作按钮挂在右侧悬浮条 (`.detail-actions`),见第 5 章。
|
||||
|
||||
---
|
||||
|
||||
## 4. 表格 (el-table)
|
||||
|
||||
**本页面无表格**,跳过。
|
||||
|
||||
---
|
||||
|
||||
## 5. UI 元素映射 (本页面是 detail 视图)
|
||||
|
||||
### 5.1 左侧 tab 列表 (`ann-menu`)
|
||||
|
||||
| UI label | 绑定字段 (entity) | 后端 SQL | 命中表.字段 | 选项来源 | 原型对照 |
|
||||
|---|---|---|---|---|---|
|
||||
| 邀请函 | `ann.invitationUrl` | `BizProjectMapper.xml:41` `<result property="invitationUrl" column="invitation_url"/>` | `biz_project.invitation_url` | — | ❌ 原型只有 1 张图 |
|
||||
| 支持函 | `ann.supportLetterUrl` | 同上 `<result property="supportLetterUrl" column="support_letter_url"/>` | `biz_project.support_letter_url` | — | ❌ 原型没有 |
|
||||
| 通知 | `ann.noticeUrl` | ⚠️ **DB 无该列** | ❌ 永远 undefined | — | ❌ 原型没有 |
|
||||
| 日程 | `ann.scheduleUrl` | ⚠️ **DB 无该列** | ❌ 永远 undefined | — | ❌ 原型没有 |
|
||||
|
||||
**前端 tab 过滤逻辑** (`PublicityDetail.vue:286`): `TAB_DEFS.filter(t => p[t.urlKey])` — URL 为 undefined/null 即隐藏 tab。所以**通知 / 日程 2 个 tab 永远不会出现**(除非 DB 加列 + mapper 加 mapping)。
|
||||
|
||||
### 5.2 右侧悬浮按钮
|
||||
|
||||
| 按钮 | 触发函数 | 接口 | 后端动作 | 涉及表.字段 | 原型对照 |
|
||||
|---|---|---|---|---|---|
|
||||
| 立即报名 | `onSignup` | `POST /business/executionIntent/signup` | `executionIntentService.signup` (推断) | `biz_execution_intent` (RuoYi) | ✅ 原型也有"立即报名" |
|
||||
| 表达支持意向 | `onSupportIntent` → `doSubmitIntent('support', fields)` | `POST /business/publicity/supportIntent` | `BizPublicityIntentController.doSubmit` (66-139 行) | `biz_publicity_support_intent` 全字段 | ❌ 原型没有 |
|
||||
| 表达执行意向 | `onExecutionIntent` → `doSubmitIntent('execution', fields)` | `POST /business/publicity/executionIntent` | 同上分支 | `biz_publicity_execution_intent` 全字段 | ❌ 原型没有 |
|
||||
| 分享二维码 | `showQr = true` | — (前端 QRCode.toDataURL) | — | — | ✅ 原型也有"分享二维码" |
|
||||
|
||||
### 5.3 匿名 dialog (`guestDialog`)
|
||||
|
||||
| 字段 | 控件 | 绑定字段 | 后端字段 | 选项来源 | 原型对照 |
|
||||
|---|---|---|---|---|---|
|
||||
| 姓名 | `el-input` | `guestDialog.form.name` | `name` | — | ❌ 原型没有 dialog |
|
||||
| 手机号 | `el-input` (maxlength=11) | `guestDialog.form.phone` | `phone` | — | ❌ 原型没有 dialog |
|
||||
| 工作单位 | `el-input` | `guestDialog.form.workUnit` | `work_unit` | — | ❌ 原型没有 dialog |
|
||||
| 部门 | `el-input` | `guestDialog.form.department` | `department` (可空) | — | ❌ 原型没有 dialog |
|
||||
| 职务 | `el-input` | `guestDialog.form.position` | `position` (可空) | — | ❌ 原型没有 dialog |
|
||||
|
||||
**重要: 提交时**,`doSubmitIntent` 把 `guestDialog.form` 直接传 `payload = { projectId, ...fields }` (`PublicityDetail.vue:524`),**不含 sex / email**。后端 controller (`BizPublicityIntentController.java:79-82`) 校验 projectId/name/phone/workUnit 必填,**不校验 sex/email** — 与设计一致。
|
||||
|
||||
### 5.4 真实落点
|
||||
|
||||
- **匿名意向 → 落 `biz_publicity_*_intent` 表**: 即使未登录也写表,后端 `BizPublicityIntentController.java:106-115` 通过 `sysUserMapper.checkPhoneUnique(phone)` 反查 user_id 自动回填,实现"账号自动关联"。
|
||||
- **已登录意向 → 同表**: 直接 `setUserId(SecurityUtils.getUserId())`。
|
||||
- **报名 → 落 `biz_execution_intent`** (RuoYi 旧表,非 publicity 两表): 这是旧业务,**与公示匿名意向表语义不同**,报名走 login-bound 表,意向走 public 快照表。
|
||||
|
||||
---
|
||||
|
||||
## 6. Java Entity ↔ 数据库表 一致性
|
||||
|
||||
### 6.1 biz_project (实测 2026-08-19)
|
||||
|
||||
```sql
|
||||
CREATE TABLE `biz_project` (
|
||||
`project_id` bigint NOT NULL AUTO_INCREMENT,
|
||||
`project_no` varchar(50) NOT NULL,
|
||||
`project_name` varchar(200) NOT NULL,
|
||||
`project_form` varchar(20),
|
||||
`total_sessions` int DEFAULT 0,
|
||||
`done_sessions` int DEFAULT 0,
|
||||
`todo_sessions` int DEFAULT 0,
|
||||
`total_amount` decimal(15,2) DEFAULT 0,
|
||||
`available_amount` decimal(15,2) DEFAULT 0,
|
||||
`paid_labor_amount` decimal(15,2) DEFAULT 0,
|
||||
`paid_meeting_amount` decimal(15,2) DEFAULT 0,
|
||||
`manage_fee` decimal(15,2) DEFAULT 0,
|
||||
`is_finished` char(1) DEFAULT '0',
|
||||
`is_settled` char(1) DEFAULT 'N',
|
||||
`manager_score` decimal(3,1),
|
||||
`sponsor_admin_user_id` bigint,
|
||||
`sponsor_admin_user_name` varchar(200),
|
||||
`lead_user_id` bigint,
|
||||
`is_bid_project` char(1) DEFAULT 'N',
|
||||
`start_time` datetime,
|
||||
`end_time` datetime,
|
||||
`submit_deadline_days` int DEFAULT 0,
|
||||
`support_contract_url` varchar(500),
|
||||
`execute_contract_url` varchar(500),
|
||||
`invitation_url` varchar(500),
|
||||
`support_letter_url` varchar(500),
|
||||
`publish_url` varchar(500),
|
||||
`create_by` varchar(64),
|
||||
`create_user_id` bigint,
|
||||
`create_time` datetime,
|
||||
`update_by` varchar(64),
|
||||
`update_time` datetime,
|
||||
`sponsor_score` decimal(3,1),
|
||||
PRIMARY KEY (`project_id`),
|
||||
UNIQUE KEY `uk_project_no` (`project_no`),
|
||||
KEY `idx_project_form` (`project_form`),
|
||||
KEY `idx_is_finished` (`is_finished`),
|
||||
KEY `idx_is_settled` (`is_settled`),
|
||||
KEY `idx_sponsor_admin_user_id` (`sponsor_admin_user_id`),
|
||||
KEY `idx_start_end_time` (`start_time`,`end_time`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=14;
|
||||
```
|
||||
|
||||
### 6.2 biz_publicity_support_intent + biz_publicity_execution_intent (同构)
|
||||
|
||||
```sql
|
||||
CREATE TABLE `biz_publicity_support_intent` (
|
||||
`intent_id` bigint NOT NULL AUTO_INCREMENT,
|
||||
`project_id` bigint,
|
||||
`project_no` varchar(64),
|
||||
`project_name` varchar(255),
|
||||
`user_id` bigint,
|
||||
`name` varchar(50),
|
||||
`phone` varchar(20),
|
||||
`work_unit` varchar(200),
|
||||
`department` varchar(100),
|
||||
`position` varchar(100),
|
||||
`source` varchar(32) DEFAULT 'publicity',
|
||||
`intent_status` varchar(32) DEFAULT '待审核',
|
||||
`remark` varchar(500),
|
||||
`create_by` varchar(64),
|
||||
`create_time` datetime,
|
||||
`update_by` varchar(64),
|
||||
`update_time` datetime,
|
||||
PRIMARY KEY (`intent_id`),
|
||||
KEY `idx_project_phone` (`project_id`,`phone`,`source`),
|
||||
KEY `idx_user_id` (`user_id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=2;
|
||||
|
||||
-- biz_publicity_execution_intent 结构完全一致
|
||||
```
|
||||
|
||||
### 6.3 Entity ↔ DB 字段对照 (biz_publicity_support_intent)
|
||||
|
||||
| 实体字段 | 中文列名 (DB COMMENT) | Java 类型 | 表字段 | DB 类型 | 一致? |
|
||||
|---|---|---|---|---|---|
|
||||
| `intentId` | 意向ID | `Long` | `intent_id` | `bigint AUTO_INCREMENT` | ✅ |
|
||||
| `projectId` | 项目ID | `Long` | `project_id` | `bigint` | ✅ |
|
||||
| `projectNo` | 项目编号(冗余) | `String` | `project_no` | `varchar(64)` | ✅ |
|
||||
| `projectName` | 项目名称(冗余) | `String` | `project_name` | `varchar(255)` | ✅ |
|
||||
| `userId` | 用户ID(按phone关联) | `Long` | `user_id` | `bigint` | ✅ |
|
||||
| `name` | 姓名 | `String` | `name` | `varchar(50)` | ✅ |
|
||||
| `phone` | 手机号 | `String` | `phone` | `varchar(20)` | ✅ |
|
||||
| `workUnit` | 工作单位 | `String` | `work_unit` | `varchar(200)` | ✅ |
|
||||
| `department` | 部门 | `String` | `department` | `varchar(100)` | ✅ |
|
||||
| `position` | 职务 | `String` | `position` | `varchar(100)` | ✅ |
|
||||
| `source` | 来源 publicity=公示页 | `String` | `source` | `varchar(32) DEFAULT 'publicity'` | ✅ |
|
||||
| `intentStatus` | 状态 待审核/已通过/已拒绝 | `String` | `intent_status` | `varchar(32) DEFAULT '待审核'` | ✅ (DB 注释用"已通过",entity 注释用"已采纳",**注释不一致但代码用"已通过"**) |
|
||||
| `remark` | 备注 | `String` | `remark` | `varchar(500)` | ✅ |
|
||||
| `createBy` (BaseEntity) | 创建者 | `String` | `create_by` | `varchar(64)` | ✅ |
|
||||
| `createTime` | 创建时间 | `Date` | `create_time` | `datetime` | ✅ |
|
||||
| `updateBy` (BaseEntity) | 更新者 | `String` | `update_by` | `varchar(64)` | ✅ |
|
||||
| `updateTime` | 更新时间 | `Date` | `update_time` | `datetime` | ✅ |
|
||||
|
||||
**BizPublicityExecutionIntent 同构**。
|
||||
|
||||
### 6.4 ⚠️ **biz_project Entity ↔ DB 一处不一致 (P0 级)**
|
||||
|
||||
`BizProject.java:119, 121` 有 `private String noticeUrl;` 和 `private String scheduleUrl;` 两个字段,且 mapper XML **未定义** `<result>` 映射这两个字段 (`BizProjectMapper.xml:40-43` 只 mapping 到 publishUrl):
|
||||
|
||||
```xml
|
||||
<result property="invitationUrl" column="invitation_url" />
|
||||
<result property="supportLetterUrl" column="support_letter_url" />
|
||||
<result property="publishUrl" column="publish_url" />
|
||||
<!-- 没有 noticeUrl / scheduleUrl 的 <result> -->
|
||||
```
|
||||
|
||||
**结论**:
|
||||
1. `SELECT` 不会填这 2 个字段,前端拿到 `undefined`,tab 自动隐藏 (`PublicityDetail.vue:286` 过滤) — **不崩,但功能缺失**
|
||||
2. `INSERT / UPDATE` 也不会写这 2 个字段 (`BizProjectMapper.xml:222-277` 没有 `<if>` 块),即使管理员在 UI 上填了值也写不进 DB
|
||||
3. DB `biz_project` 实测**没有 `notice_url` / `schedule_url` 这 2 列**
|
||||
|
||||
**前端 PublicityDetail.vue `TAB_DEFS`** (`PublicityDetail.vue:270-275`) 期望 `ann.noticeUrl` / `ann.scheduleUrl` 字段 — **永远为 undefined**,2 个 tab 永久隐藏。
|
||||
|
||||
---
|
||||
|
||||
## 7. 索引检查
|
||||
|
||||
### 7.1 biz_project 索引
|
||||
|
||||
| Key | 列 | 用途覆盖 |
|
||||
|---|---|---|
|
||||
| PRIMARY | `project_id` | ✅ `WHERE project_id = ?` (本页面 `getById(13)`) |
|
||||
| uk_project_no | `project_no` (UNIQUE) | ✅ 项目编号唯一性 |
|
||||
| idx_project_form | `project_form` | ✅ 项目形式筛选 |
|
||||
| idx_is_finished | `is_finished` | ✅ 已结题筛选 |
|
||||
| idx_is_settled | `is_settled` | ✅ 已结算筛选 |
|
||||
| idx_sponsor_admin_user_id | `sponsor_admin_user_id` | ✅ 赞助方负责人反查 |
|
||||
| idx_start_end_time | `start_time`,`end_time` | ✅ 时间段查询 |
|
||||
|
||||
**评价**: `getById(13)` 走 PRIMARY,✅。其它筛选用 idx 系列列,✅。
|
||||
|
||||
### 7.2 biz_publicity_support_intent / execution_intent 索引
|
||||
|
||||
| Key | 列 | 用途覆盖 |
|
||||
|---|---|---|
|
||||
| PRIMARY | `intent_id` | ✅ |
|
||||
| idx_project_phone | `project_id`,`phone`,`source` (复合) | ✅ `hasIntent(projectId, phone, 'publicity')` 完全命中 (controller:147-159) |
|
||||
| idx_user_id | `user_id` | ✅ 已登录用户反查 |
|
||||
|
||||
**评价**: 复合索引 `(project_id, phone, source)` 完美覆盖 controller 的查重 SQL,✅。
|
||||
|
||||
---
|
||||
|
||||
## 8. 与原型差异
|
||||
|
||||
原型: `proto/html/公示详情.html`
|
||||
实现: `ry-vue3/src/views/portal/PublicityDetail.vue`
|
||||
|
||||
### 8.1 实现新增 (超出原型)
|
||||
|
||||
| 项 | 实现 | 原型 | 评价 |
|
||||
|---|---|---|---|
|
||||
| 4 个 tab (邀请函/支持函/通知/日程) | ✅ | ❌ 原型只有 1 张图 | 改进 |
|
||||
| 表达支持意向按钮 + 匿名 dialog | ✅ | ❌ 原型没有 | 改进 (与 [publicity-intent-design](publicity-intent-design.md) 记忆一致) |
|
||||
| 表达执行意向按钮 | ✅ | ❌ 原型没有 | 改进 |
|
||||
| 报名按钮已有"已报名"灰显状态 | ✅ | ❌ 原型跳登录 | 改进 |
|
||||
| 顶部 nav 用户态 (登录/头像下拉) | ✅ | ❌ 原型只有"登录"链接 | 改进 |
|
||||
| 加载骨架屏 | ✅ (`main-skeleton` 4 个 tab skeleton) | ❌ | 改进 |
|
||||
| OSS PDF 走 `/common/oss/proxy` (inline 渲染) | ✅ (`proxyUrl` 309-316) | — | **必须** — OSS bucket 设了 attachment 否则 iframe 强制下载 |
|
||||
|
||||
### 8.2 原型有但实现缺失
|
||||
|
||||
| 项 | 原型 | 实现 | 严重度 |
|
||||
|---|---|---|---|
|
||||
| 静态大图 (邀请函红头文件 letter 样式 + 公章) | ✅ `.letter` + `.seal` (200-340 行) | ⚠️ 仅渲染原始 PDF/图片,无 letter 容器包装 | P2 (视觉降级,但功能等价) |
|
||||
|
||||
### 8.3 文字 / 标签差异
|
||||
|
||||
| 项 | 原型 | 实现 | 一致? |
|
||||
|---|---|---|---|
|
||||
| 主按钮文字 | "立即报名" | "立即报名" | ✅ |
|
||||
| 二级按钮 | "分享二维码" | "分享二维码" | ✅ |
|
||||
| 顶部 nav 高亮 | "项目公示" | "项目公示" | ✅ |
|
||||
| 主标题 | 无 (`<article class="letter">` 自身有标题) | 无 (用面包屑) | ✅ |
|
||||
|
||||
### 8.4 总结
|
||||
|
||||
**整体方向: 实现远超原型** (4 tab + 2 新功能按钮 + 骨架屏 + 用户态 + OSS 代理)。原型是 v1 静态图,实现是 v2 完整交互页,改进方向正确。
|
||||
|
||||
---
|
||||
|
||||
## 9. 字段冗余 / 设计问题
|
||||
|
||||
### 9.1 biz_publicity_support_intent / execution_intent
|
||||
|
||||
- **冗余字段**: `project_no`, `project_name`, `user_id` 都是 `biz_project` JOIN 出来的快照字段 — **合理** (匿名提交时项目名/编号可能改名,留快照保证审计追溯)。
|
||||
- **唯一约束**: `(project_id, phone, source)` 通过 idx_project_phone 复合索引实现查重。**未加 UNIQUE 约束**,纯靠 service 层 `findDuplicate` — 行数大后并发提交可能绕过 (race condition),但匿名提交频率低,暂可接受。
|
||||
|
||||
### 9.2 biz_project 通知/日程字段
|
||||
|
||||
- `BizProject.java` 有 `noticeUrl` / `scheduleUrl` 但 DB 没列,mapper 没 mapping → **死代码**。建议二选一:
|
||||
- A. 加 `ALTER TABLE biz_project ADD COLUMN notice_url varchar(500), ADD COLUMN schedule_url varchar(500)` + mapper 补 `<result>` 和 `<if>` 块
|
||||
- B. 删 entity 这 2 字段 + 前端 TAB_DEFS 删通知/日程
|
||||
|
||||
### 9.3 报名 vs 意向两套表语义重叠
|
||||
|
||||
- `biz_execution_intent` (RuoYi 旧表,报名按钮) + `biz_publicity_execution_intent` (公示匿名意向) — 都是"对项目表达执行意向"
|
||||
- `BizSupportIntentController.java` (RuoYi 旧) + `BizPublicityIntentController.java` (新) — 两套 controller
|
||||
- 旧表语义: "已登录 + 报名+ 关联 bid" — 强约束
|
||||
- 新表语义: "匿名/已登录 + 仅意向快照 + 不进入执行流程" — 弱约束
|
||||
- **两表并存是设计意图** ([publicity-intent-design](publicity-intent-design.md) 记忆:不复用登录视角的强绑表) — 合理,不冗余
|
||||
|
||||
### 9.4 业务闭环
|
||||
|
||||
- 匿名提交 → 反查 sys_user → 自动回填 user_id (`BizPublicityIntentController.java:106-115`) — 闭环 ✅
|
||||
- 已登录但未维护手机号 → userId 仍能从 SecurityContext 取 (`SecurityUtils.getUserId()`) — 闭环 ✅
|
||||
- phone 不匹配任何 sys_user → user_id 仍 NULL → manager 端列表"账号状态=不存在" 标签 (见 manager/SupportIntent.vue) — 闭环 ✅
|
||||
|
||||
### 9.5 数据隔离
|
||||
|
||||
- 公开端点 `/business/publicity/**` permitAll (匿名可访问)
|
||||
- 管理端点 `/business/publicitySupportIntent/**` 受 token 保护 (`SecurityConfig.java` 无显式 permitAll)
|
||||
- **匿名提交的数据隔离靠 source='publicity' 字段** — manager 端 list 自动按 source 过滤 (推断,需进一步看 mapper XML 验证)
|
||||
|
||||
---
|
||||
|
||||
## 10. 待修复列表
|
||||
|
||||
| # | 问题 | 文件 | 修复建议 | 严重度 |
|
||||
|---|---|---|---|---|
|
||||
| 1 | `BizProject.noticeUrl` / `scheduleUrl` 字段无 DB 列、无 mapper mapping,**死代码** | `BizProject.java:119,121` + `BizProjectMapper.xml` + `PublicityDetail.vue:270-275` | 二选一:A) `ALTER TABLE biz_project ADD notice_url/schedule_url` + mapper 补 mapping;B) 删 entity 字段 + 前端 TAB_DEFS 删 通知/日程 | P0 |
|
||||
| 2 | `intent_status` 中文注释 entity 写"已采纳",DB 写"已通过",代码实际用"已通过" (`changeStatus(row, '已通过')` SupportIntent.vue:57) | `BizPublicitySupportIntent.java:48` | 改 entity 注释为"已通过" | P3 |
|
||||
| 3 | 邀请函原文渲染: 原型 letter 容器样式 + 公章位置 (`.letter` + `.seal`) 未实现,直接渲染 PDF/图片丢视觉 | `PublicityDetail.vue:73-90` (content 容器) | 加 letter 容器样式 + 公章绝对定位 (与 manager/Publicity 项目详情一致) | P2 |
|
||||
| 4 | 匿名提交**无反爬限制** (`/business/publicity/supportIntent` permitAll),同 IP 可无限提交不同 phone | `BizPublicityIntentController.java:50-54` | 加 rate-limit (按 IP/phone/项目 5 分钟内 1 次) | P2 |
|
||||
| 5 | `findDuplicate` 复合唯一性靠 service 层 SELECT,无 DB UNIQUE KEY — 高并发下可能 race condition 重复插入 | `biz_publicity_support_intent` 表 | 加 `UNIQUE KEY uk_proj_phone_source (project_id, phone, source)` | P3 (匿名低频,暂可接受) |
|
||||
| 6 | 前端 tab 默认 active 取"第一个有 URL 的 tab",如果 4 个 tab 都没 URL (全部 null),`activeTab` 仍可能是空字符串 → 用户看不到任何内容 | `PublicityDetail.vue:357-361` | 加兜底:全无 URL 时显示"暂无可查看文件"占位 | P2 |
|
||||
| 7 | 公示页 iframe 加载 OSS PDF 时,**没有 loading 指示器** — 用户看到白屏几秒 | `PublicityDetail.vue:74-78` | 给 iframe 加 `@load` 隐藏骨架屏,或 PDF.js 内嵌渲染 | P3 |
|
||||
| 8 | "已支持" / "已表达意向" 状态依赖前端 `checkIntentSubmitted()` 用本地 phone 查重,**清缓存后状态丢失**(重新查接口,但有 200ms 延迟用户看到 "支持" 闪烁) | `PublicityDetail.vue:431-453` | 用 `sessionStorage` 缓存"已提交"标志,onMounted 时优先读缓存再异步验证 | P3 |
|
||||
|
||||
---
|
||||
|
||||
## 11. 引用清单
|
||||
|
||||
| 文件 | 行号 | 说明 |
|
||||
|---|---|---|
|
||||
| `ry-vue3/src/views/portal/PublicityDetail.vue` | 全文 | 审查目标 |
|
||||
| `ry-vue3/src/views/portal/PublicityDetail.vue:195-216` | — | 匿名意向 dialog (本次已加 guest-intent-form 样式) |
|
||||
| `ry-vue3/src/views/portal/PublicityDetail.vue:270-275` | — | TAB_DEFS (4 tab 定义) |
|
||||
| `ry-vue3/src/views/portal/PublicityDetail.vue:309-316` | — | `proxyUrl` OSS 代理 |
|
||||
| `ry-vue3/src/views/portal/PublicityDetail.vue:355-370` | — | `load()` 调 GET /business/public/project/{projectId} |
|
||||
| `ry-vue3/src/views/portal/PublicityDetail.vue:398-428` | — | `onSignup()` 立即报名 |
|
||||
| `ry-vue3/src/views/portal/PublicityDetail.vue:437-453` | — | `checkIntentSubmitted` 查重 |
|
||||
| `ry-vue3/src/views/portal/PublicityDetail.vue:468-490` | — | 支持/执行意向入口 (登录分支) |
|
||||
| `ry-vue3/src/router/index.js` | 10 | `/publicity/:projectId` 路由 |
|
||||
| `proto/html/项目公示.html` | 697 | 列表 → 详情跳转 |
|
||||
| `proto/html/公示详情.html` | 全文 | 原型基准 |
|
||||
| `ry-vue3/src/api/public.js` | 117-127 | publicity intent 4 个公开端点 |
|
||||
| `ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizPublicController.java` | 91-94 | GET /business/public/project/{projectId} |
|
||||
| `ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizPublicityIntentController.java` | 全文 | 公示页意向 controller (公开 + 管理双端点) |
|
||||
| `ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizPublicityIntentController.java:66-139` | — | `doSubmit` 通用提交逻辑 |
|
||||
| `ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizPublicityIntentController.java:106-115` | — | 匿名 → 反查 sys_user → 回填 user_id |
|
||||
| `ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizProject.java` | 113, 115, 119, 121 | 4 个 URL 字段 (含**死代码** noticeUrl/scheduleUrl) |
|
||||
| `ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizPublicitySupportIntent.java` | 全文 | 公示-支持意向 entity |
|
||||
| `ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizPublicityExecutionIntent.java` | 全文 | 公示-执行意向 entity |
|
||||
| `ry-api/ruoyi-business/src/main/resources/mapper/business/BizProjectMapper.xml` | 40-43, 67-69 | URL 字段的 `<result>` 和 SELECT (无 notice/schedule) |
|
||||
| `ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java` | 64 | 公开端点放行 (`/business/public/**`, `/business/publicity/**`) |
|
||||
| MySQL `guoju0808.biz_project` | — | 32 列 + 1 主键 + 1 唯一 + 5 索引,实测 2026-08-19 |
|
||||
| MySQL `guoju0808.biz_publicity_support_intent` | — | 17 列 + 1 主键 + 2 索引,实测 2026-08-19 |
|
||||
| MySQL `guoju0808.biz_publicity_execution_intent` | — | 同上,实测 2026-08-19 |
|
||||
| MySQL `guoju0808.biz_project.project_id=13` | — | 测试数据, invitation_url 有 OSS PDF,其他 URL 全 NULL |
|
||||
Reference in New Issue
Block a user