diff --git a/_self/doctor_home.md b/_self/doctor_home.md new file mode 100644 index 0000000..23820c0 --- /dev/null +++ b/_self/doctor_home.md @@ -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 `` 过滤 | 详见 §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 升序 ✓ +``` \ No newline at end of file diff --git a/_self/manager_accounts.md b/_self/manager_accounts.md new file mode 100644 index 0000000..eb3ad32 --- /dev/null +++ b/_self/manager_accounts.md @@ -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` 角色可见 | 其他角色走 `//account` 路径 | +| 路由守卫 | `router/index.js:61` `meta: { role: 'manager' }` + `permission.js` | token 角色 | 通过则进 | +| 后端 | `SysProfileController.java` | 无 `role_type` 校验,任何已登录用户均可改自己的资料 | `currentUser = loginUser.getUser()` | + +**不能看到本页面的角色**: admin / leader / doctor / executor / sponsor — 他们各自有 `//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` `nick_name = #{nickName},` | `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 `` 会原值回写(若 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 (用 `` 条件更新) | +| `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 | \ No newline at end of file diff --git a/_self/portal_publicity_detail.md b/_self/portal_publicity_detail.md new file mode 100644 index 0000000..e526f41 --- /dev/null +++ b/_self/portal_publicity_detail.md @@ -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` `` | `biz_project.invitation_url` | — | ❌ 原型只有 1 张图 | +| 支持函 | `ann.supportLetterUrl` | 同上 `` | `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 **未定义** `` 映射这两个字段 (`BizProjectMapper.xml:40-43` 只 mapping 到 publishUrl): + +```xml + + + + +``` + +**结论**: +1. `SELECT` 不会填这 2 个字段,前端拿到 `undefined`,tab 自动隐藏 (`PublicityDetail.vue:286` 过滤) — **不崩,但功能缺失** +2. `INSERT / UPDATE` 也不会写这 2 个字段 (`BizProjectMapper.xml:222-277` 没有 `` 块),即使管理员在 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 高亮 | "项目公示" | "项目公示" | ✅ | +| 主标题 | 无 (`
` 自身有标题) | 无 (用面包屑) | ✅ | + +### 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 补 `` 和 `` 块 + - 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 字段的 `` 和 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 | \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizExpertController.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizExpertController.java index 5e4d0d9..27972dc 100644 --- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizExpertController.java +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizExpertController.java @@ -36,7 +36,7 @@ public class BizExpertController extends BaseController return getDataTable(list); } @GetMapping("/{expertId}") - public AjaxResult getInfo(@PathVariable("expertId") String expertId) + public AjaxResult getInfo(@PathVariable("expertId") Long expertId) { return success(bizExpertService.getById(expertId)); } @@ -71,7 +71,7 @@ public class BizExpertController extends BaseController */ @Log(title = "专家启停", businessType = BusinessType.UPDATE) @PutMapping("/{expertId}/status") - public AjaxResult updateStatus(@PathVariable String expertId, @RequestParam String status) + public AjaxResult updateStatus(@PathVariable Long expertId, @RequestParam String status) { return toAjax(bizExpertService.updateStatus(expertId, status)); } @@ -89,7 +89,7 @@ public class BizExpertController extends BaseController } @Log(title = "专家", businessType = BusinessType.DELETE) @DeleteMapping("/{ids}") - public AjaxResult remove(@PathVariable String[] ids) + public AjaxResult remove(@PathVariable Long[] ids) { return toAjax(bizExpertService.deleteByPrimaryKeys(ids)); } diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizLaborProtocolTemplateController.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizLaborProtocolTemplateController.java new file mode 100644 index 0000000..2d3430a --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizLaborProtocolTemplateController.java @@ -0,0 +1,85 @@ +package com.ruoyi.business.controller; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.business.domain.BizLaborProtocolTemplate; +import com.ruoyi.business.service.IBizLaborProtocolTemplateService; + +/** + * 劳务协议模板配置 Controller (admin 网站管理) + * 全局共享, default_flag='Y' 同一时刻只有 1 条 (service.setDefault 保证) + */ +@RestController +@RequestMapping("/business/laborProtocolTemplate") +public class BizLaborProtocolTemplateController extends BaseController { + + @Autowired + private IBizLaborProtocolTemplateService templateService; + + /** 分页列表 (admin 后台用, 支持筛选) */ + @GetMapping("/list") + public TableDataInfo list(BizLaborProtocolTemplate entity) { + startPage(); + List rows = templateService.selectList(entity); + return getDataTable(rows); + } + + /** 全部启用模板 (前端下拉用, 不分页) */ + @GetMapping("/allEnabled") + public AjaxResult allEnabled() { + return success(templateService.selectAllEnabled()); + } + + /** 取默认模板 */ + @GetMapping("/default") + public AjaxResult getDefault() { + return success(templateService.selectDefault()); + } + + /** 详情 */ + @GetMapping("/{id}") + public AjaxResult getInfo(@PathVariable("id") Long id) { + return success(templateService.getById(id)); + } + + @Log(title = "劳务协议模板", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@RequestBody BizLaborProtocolTemplate entity) { + entity.setCreateBy(SecurityUtils.getUsername()); + // 新建时若 default_flag='Y', 先清空其他默认 (保证只有 1 条 Y) + if ("Y".equals(entity.getDefaultFlag())) { + templateService.setDefault(0L); // id=0 不存在, 实际只清空其他行 + } + return toAjax(templateService.insert(entity)); + } + + @Log(title = "劳务协议模板", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@RequestBody BizLaborProtocolTemplate entity) { + entity.setUpdateBy(SecurityUtils.getUsername()); + return toAjax(templateService.update(entity)); + } + + @Log(title = "劳务协议模板", businessType = BusinessType.DELETE) + @DeleteMapping("/{ids}") + public AjaxResult remove(@PathVariable Long[] ids) { + return toAjax(templateService.deleteByPrimaryKeys(ids)); + } + + /** + * 设为默认 (app 层保证全局唯一) + * PUT /business/laborProtocolTemplate/{id}/default + */ + @Log(title = "劳务协议模板-设默认", businessType = BusinessType.UPDATE) + @PutMapping("/{id}/default") + public AjaxResult setDefault(@PathVariable("id") Long id) { + return toAjax(templateService.setDefault(id)); + } +} \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizMeetingAttendeeController.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizMeetingAttendeeController.java new file mode 100644 index 0000000..7fa40ca --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizMeetingAttendeeController.java @@ -0,0 +1,67 @@ +package com.ruoyi.business.controller; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.business.domain.BizMeetingAttendee; +import com.ruoyi.business.service.IBizMeetingAttendeeService; + +/** + * 会议参会人 Controller (劳务协议 / 手写签名) + * + * 公开端点 (任意登录用户可调): + * - GET /business/meetingAttendee/unsigned 当前用户的"待签署"会议 (handsign 或 labor_protocol 为空) + * - PUT /business/meetingAttendee/{id}/handsign 更新手写签名 (Base64, 直接存 DB) + * - PUT /business/meetingAttendee/{id}/laborProtocol 更新劳务协议 URL (OSS) + * + * 管理端点 (后续 BizMeetingController.add/edit 调用): + * - /business/meetingAttendee (CRUD) + */ +@RestController +@RequestMapping("/business/meetingAttendee") +public class BizMeetingAttendeeController extends BaseController { + + @Autowired + private IBizMeetingAttendeeService attendeeService; + + /** + * 当前登录用户的"待签署"列表 (handsign 或 labor_protocol 任一为空) + * 用于 /doctor/home 工作台 + */ + @GetMapping("/unsigned") + public AjaxResult listUnsigned() { + Long userId = SecurityUtils.getUserId(); + List rows = attendeeService.selectUnsignedByUserId(userId); + return success(rows); + } + + /** 更新手写签名 (Base64 字符串, 直接存 DB longtext) */ + @Log(title = "手写签名", businessType = BusinessType.UPDATE) + @PutMapping("/{id}/handsign") + public AjaxResult updateHandsign(@PathVariable("id") Long id, @RequestBody BizMeetingAttendee body) { + BizMeetingAttendee entity = new BizMeetingAttendee(); + entity.setId(id); + entity.setHandsign(body.getHandsign()); + entity.setUpdateBy(SecurityUtils.getUsername()); + return toAjax(attendeeService.updateHandsign(entity)); + } + + /** 更新劳务协议 URL (OSS 上传后调本接口) */ + @Log(title = "劳务协议", businessType = BusinessType.UPDATE) + @PutMapping("/{id}/laborProtocol") + public AjaxResult updateLaborProtocol(@PathVariable("id") Long id, @RequestBody BizMeetingAttendee body) { + BizMeetingAttendee entity = new BizMeetingAttendee(); + entity.setId(id); + entity.setLaborProtocol(body.getLaborProtocol()); + entity.setUpdateBy(SecurityUtils.getUsername()); + return toAjax(attendeeService.updateLaborProtocol(entity)); + } + + /* ====== 管理端点 (给后续 BizMeetingController.add 调用) ====== */ + // TODO: BizMeetingController.add 接受 attendeeUserIds: Long[], 批量 insert 中间表 +} \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizMeetingController.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizMeetingController.java index cfad8fd..dd8e359 100644 --- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizMeetingController.java +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizMeetingController.java @@ -8,6 +8,7 @@ import com.ruoyi.common.core.controller.BaseController; import com.ruoyi.common.core.domain.AjaxResult; import com.ruoyi.common.core.page.TableDataInfo; import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.utils.SecurityUtils; import com.ruoyi.business.domain.BizMeeting; import com.ruoyi.business.service.IBizMeetingService; @@ -23,6 +24,11 @@ public class BizMeetingController extends BaseController @GetMapping("/list") public TableDataInfo list(BizMeeting bizMeeting) { + // 医生/专家角色: 后端兜底只查"我参加的会议" (走 biz_meeting_attendee 中间表) + String roleType = SecurityUtils.getLoginUser().getUser().getRoleType(); + if ("doctor".equals(roleType) || "expert".equals(roleType)) { + bizMeeting.setUserId(SecurityUtils.getUserId()); + } startPage(); List list = bizMeetingService.selectList(bizMeeting); return getDataTable(list); diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizRegisterController.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizRegisterController.java index da86630..e3732fd 100644 --- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizRegisterController.java +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizRegisterController.java @@ -13,7 +13,6 @@ import com.ruoyi.business.service.IBizExpertService; import com.ruoyi.business.service.SysSmsService; import com.ruoyi.common.core.controller.BaseController; import com.ruoyi.common.core.domain.AjaxResult; -import com.ruoyi.common.utils.uuid.UUID; import com.ruoyi.common.core.domain.entity.SysUser; import com.ruoyi.system.service.ISysUserService; @@ -42,18 +41,26 @@ public class BizRegisterController extends BaseController { @Autowired private BCryptPasswordEncoder passwordEncoder; + /** + * 把任意类型安全转 String (兼容前端传 deptId/titileId 等 Number 也能跑通, + * 防止 Integer/Long 等数字类型 cast String 抛 ClassCastException) + */ + private static String toStr(Object o) { + return o == null ? null : o.toString(); + } + @PostMapping("/registerExpert") public AjaxResult registerExpert(@RequestBody Map body) { - String realName = (String) body.get("realName"); - String workUnit = (String) body.get("workUnit"); - String department = (String) body.get("department"); - String doctorTitle = (String) body.get("doctorTitle"); - String phone = (String) body.get("phone"); - String code = (String) body.get("code"); - String password = (String) body.get("password"); - String uuid = (String) body.get("uuid"); - String licenseCertUrl = (String) body.get("licenseCertUrl"); - String titleCertUrl = (String) body.get("titleCertUrl"); + String realName = toStr(body.get("realName")); + String workUnit = toStr(body.get("workUnit")); + String department = toStr(body.get("department")); + String doctorTitle = toStr(body.get("doctorTitle")); + String phone = toStr(body.get("phone")); + String code = toStr(body.get("code")); + String password = toStr(body.get("password")); + String uuid = toStr(body.get("uuid")); + String licenseCertUrl = toStr(body.get("licenseCertUrl")); + String titleCertUrl = toStr(body.get("titleCertUrl")); if (realName == null || realName.isEmpty()) return error("姓名不能为空"); if (workUnit == null || workUnit.isEmpty()) return error("工作单位不能为空"); @@ -89,9 +96,9 @@ public class BizRegisterController extends BaseController { // sys_user.role_type 表字段 (DB 默认 executor, 专家需 = doctor), 用 mapper 更新 userService.updateRoleType(userId, "doctor"); - // 5. 插入 biz_expert + // 5. 插入 biz_expert (expertId 由 BizExpertServiceImpl.insert 里的 SnowflakeId.injectIfEmpty 自动填数字雪花 ID, +// 不要 controller 预生成 UUID — DB expert_id 是 bigint, UUID 字符串塞不进去) BizExpert expert = new BizExpert(); - expert.setExpertId(UUID.fastUUID().toString()); expert.setUserId(userId); expert.setName(realName); expert.setPhone(phone); diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizExpert.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizExpert.java index e4b8e4d..5cf2476 100644 --- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizExpert.java +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizExpert.java @@ -10,7 +10,7 @@ import com.ruoyi.common.core.domain.BaseEntity; public class BizExpert extends BaseEntity { private static final long serialVersionUID = 1L; /** expertId */ - private String expertId; + private Long expertId; /** name */ @Excel(name = "name") private String name; @@ -80,8 +80,8 @@ public class BizExpert extends BaseEntity { private String auditOpinion; /** 状态 0正常 1禁用 */ private String status; - public String getExpertId() { return expertId; } - public void setExpertId(String expertId) { this.expertId = expertId; } + public Long getExpertId() { return expertId; } + public void setExpertId(Long expertId) { this.expertId = expertId; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getPhone() { return phone; } diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizLaborProtocolTemplate.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizLaborProtocolTemplate.java new file mode 100644 index 0000000..0acff82 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizLaborProtocolTemplate.java @@ -0,0 +1,54 @@ +package com.ruoyi.business.domain; + +import java.util.Date; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.core.domain.BaseEntity; + +/** + * 劳务协议模板配置 (biz_labor_protocol_template) + * admin 在"网站管理"下维护, 全局共享 + * default_flag='Y' 同一时刻只有1条 (service 层保证) + */ +public class BizLaborProtocolTemplate extends BaseEntity { + private static final long serialVersionUID = 1L; + /** id */ + private Long id; + /** template_name */ + @Excel(name = "template_name") + private String templateName; + /** template_content (HTML 含占位符 {name} {phone} 等) */ + private String templateContent; + /** default_flag (Y=默认, N=非默认) */ + @Excel(name = "default_flag") + private String defaultFlag; + /** sort_order (前端下拉顺序) */ + private Integer sortOrder; + /** status (Y=启用, N=禁用) */ + @Excel(name = "status") + private String status; + /** remark */ + private String remark; + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + public String getTemplateName() { return templateName; } + public void setTemplateName(String templateName) { this.templateName = templateName; } + public String getTemplateContent() { return templateContent; } + public void setTemplateContent(String templateContent) { this.templateContent = templateContent; } + public String getDefaultFlag() { return defaultFlag; } + public void setDefaultFlag(String defaultFlag) { this.defaultFlag = defaultFlag; } + public Integer getSortOrder() { return sortOrder; } + public void setSortOrder(Integer sortOrder) { this.sortOrder = sortOrder; } + public String getStatus() { return status; } + public void setStatus(String status) { this.status = status; } + public String getRemark() { return remark; } + public void setRemark(String remark) { this.remark = remark; } + public Date getCreateTime() { return createTime; } + public void setCreateTime(Date createTime) { this.createTime = createTime; } + public Date getUpdateTime() { return updateTime; } + public void setUpdateTime(Date updateTime) { this.updateTime = updateTime; } +} \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizMeeting.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizMeeting.java index 28491ca..641a428 100644 --- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizMeeting.java +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizMeeting.java @@ -76,6 +76,8 @@ public class BizMeeting extends BaseEntity { private String scheduleUrl; /** 签署劳务 0未签 1已签 */ private String laborSigned; + /** 参会人 user_id (非持久化字段, 仅用于 mapper WHERE 过滤; controller 注入, 配合 biz_meeting_attendee 中间表) */ + private transient Long userId; public Long getMeetingId() { return meetingId; } public void setMeetingId(Long meetingId) { this.meetingId = meetingId; } public String getProjectNo() { return projectNo; } @@ -122,4 +124,6 @@ public class BizMeeting extends BaseEntity { public void setScheduleUrl(String scheduleUrl) { this.scheduleUrl = scheduleUrl; } public String getLaborSigned() { return laborSigned; } public void setLaborSigned(String laborSigned) { this.laborSigned = laborSigned; } + public Long getUserId() { return userId; } + public void setUserId(Long userId) { this.userId = userId; } } \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizMeetingAttendee.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizMeetingAttendee.java new file mode 100644 index 0000000..b1962a7 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizMeetingAttendee.java @@ -0,0 +1,54 @@ +package com.ruoyi.business.domain; + +import java.util.Date; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.core.domain.BaseEntity; + +/** + * 会议参会人 (biz_meeting_attendee 中间表) + * 用于按 user_id 过滤"我参加的会议", 替代在 biz_meeting 上加冗余字段 + */ +public class BizMeetingAttendee extends BaseEntity { + private static final long serialVersionUID = 1L; + /** id */ + private Long id; + /** meeting_id (FK biz_meeting.meeting_id) */ + private Long meetingId; + /** user_id (FK sys_user.user_id) */ + private Long userId; + /** 手写签名 Base64 (longtext) — 由前端手写板生成 */ + private String handsign; + /** 劳务协议 URL (OSS) */ + private String laborProtocol; + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + /* ====== 非持久化字段, 用于 selectUnsignedByUserId 联表查询 ====== */ + private transient String meetingName; + private transient Date startTime; + private transient Date endTime; + private transient String projectName; + private transient String projectNo; + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + public Long getMeetingId() { return meetingId; } + public void setMeetingId(Long meetingId) { this.meetingId = meetingId; } + public Long getUserId() { return userId; } + public void setUserId(Long userId) { this.userId = userId; } + public String getHandsign() { return handsign; } + public void setHandsign(String handsign) { this.handsign = handsign; } + public String getLaborProtocol() { return laborProtocol; } + public void setLaborProtocol(String laborProtocol) { this.laborProtocol = laborProtocol; } + public Date getCreateTime() { return createTime; } + public void setCreateTime(Date createTime) { this.createTime = createTime; } + public String getMeetingName() { return meetingName; } + public void setMeetingName(String meetingName) { this.meetingName = meetingName; } + public Date getStartTime() { return startTime; } + public void setStartTime(Date startTime) { this.startTime = startTime; } + public Date getEndTime() { return endTime; } + public void setEndTime(Date endTime) { this.endTime = endTime; } + public String getProjectName() { return projectName; } + public void setProjectName(String projectName) { this.projectName = projectName; } + public String getProjectNo() { return projectNo; } + public void setProjectNo(String projectNo) { this.projectNo = projectNo; } +} \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizProject.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizProject.java index e4310b6..aee3771 100644 --- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizProject.java +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizProject.java @@ -115,10 +115,6 @@ public class BizProject extends BaseEntity { private String supportLetterUrl; /** 已发布公告URL */ private String publishUrl; - /** 通知文件URL */ - private String noticeUrl; - /** 日程文件URL */ - private String scheduleUrl; /** 是否已发布公示 0否 1是 */ private String isPublished; /** 发布时间 */ @@ -215,10 +211,6 @@ public class BizProject extends BaseEntity { public void setSupportLetterUrl(String supportLetterUrl) { this.supportLetterUrl = supportLetterUrl; } public String getPublishUrl() { return publishUrl; } public void setPublishUrl(String publishUrl) { this.publishUrl = publishUrl; } - public String getNoticeUrl() { return noticeUrl; } - public void setNoticeUrl(String noticeUrl) { this.noticeUrl = noticeUrl; } - public String getScheduleUrl() { return scheduleUrl; } - public void setScheduleUrl(String scheduleUrl) { this.scheduleUrl = scheduleUrl; } public String getIsPublished() { return isPublished; } public void setIsPublished(String isPublished) { this.isPublished = isPublished; } public Date getPublishTime() { return publishTime; } diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizExpertMapper.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizExpertMapper.java index 4c4289b..3fb9021 100644 --- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizExpertMapper.java +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizExpertMapper.java @@ -7,13 +7,13 @@ import com.ruoyi.business.domain.BizExpert; */ public interface BizExpertMapper { - BizExpert selectByPrimaryKey(String expertId); + BizExpert selectByPrimaryKey(Long expertId); BizExpert selectByUserId(Long userId); List selectList(BizExpert entity); int insert(BizExpert entity); int insertWithUserId(BizExpert entity); int updateByPrimaryKey(BizExpert entity); int updateByUserId(BizExpert entity); - int deleteByPrimaryKey(String expertId); - int deleteByPrimaryKeys(String[] expertIds); + int deleteByPrimaryKey(Long expertId); + int deleteByPrimaryKeys(Long[] expertIds); } \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizLaborProtocolTemplateMapper.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizLaborProtocolTemplateMapper.java new file mode 100644 index 0000000..469fc2a --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizLaborProtocolTemplateMapper.java @@ -0,0 +1,19 @@ +package com.ruoyi.business.mapper; + +import java.util.List; +import com.ruoyi.business.domain.BizLaborProtocolTemplate; + +public interface BizLaborProtocolTemplateMapper { + BizLaborProtocolTemplate selectByPrimaryKey(Long id); + List selectList(BizLaborProtocolTemplate entity); + /** 取默认模板 (default_flag='Y' AND status='Y'), 0 或 1 条 */ + BizLaborProtocolTemplate selectDefault(); + /** 取所有启用模板 (status='Y'), 按 sort_order 排序 */ + List selectAllEnabled(); + int insert(BizLaborProtocolTemplate entity); + int updateByPrimaryKey(BizLaborProtocolTemplate entity); + /** 把所有行的 default_flag 设为 'N' (service.setDefault 调用) */ + int clearAllDefault(); + int deleteByPrimaryKey(Long id); + int deleteByPrimaryKeys(Long[] ids); +} \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizMeetingAttendeeMapper.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizMeetingAttendeeMapper.java new file mode 100644 index 0000000..e8f7cbe --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizMeetingAttendeeMapper.java @@ -0,0 +1,15 @@ +package com.ruoyi.business.mapper; + +import java.util.List; +import com.ruoyi.business.domain.BizMeetingAttendee; + +public interface BizMeetingAttendeeMapper { + int insert(BizMeetingAttendee entity); + int updateHandsign(BizMeetingAttendee entity); + int updateLaborProtocol(BizMeetingAttendee entity); + int deleteByMeetingId(Long meetingId); + int deleteByMeetingIdAndUserId(BizMeetingAttendee entity); + List selectByMeetingId(Long meetingId); + List selectByUserId(Long userId); + List selectUnsignedByUserId(Long userId); +} \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizExpertService.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizExpertService.java index fe45f13..e3b768a 100644 --- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizExpertService.java +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizExpertService.java @@ -10,7 +10,7 @@ import com.ruoyi.common.core.domain.entity.SysUser; */ public interface IBizExpertService { - BizExpert getById(String expertId); + BizExpert getById(Long expertId); BizExpert getByUserId(Long userId); List selectList(BizExpert entity); /** @@ -24,11 +24,11 @@ public interface IBizExpertService * 启用/禁用专家: 同步更新 biz_expert.status + sys_user.status * status='Y' 正常, status='N' 禁用 */ - int updateStatus(String expertId, String status); + int updateStatus(Long expertId, String status); /** 按 userId 更新或新建 (upsert) */ int updateProfileByUserId(BizExpert entity); - int deleteByPrimaryKey(String expertId); - int deleteByPrimaryKeys(String[] expertId); + int deleteByPrimaryKey(Long expertId); + int deleteByPrimaryKeys(Long[] expertId); /** * 批量导入专家: 每行调用 insert, updateSupport=true 时跳过已存在手机号(视为成功) diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizLaborProtocolTemplateService.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizLaborProtocolTemplateService.java new file mode 100644 index 0000000..74636e3 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizLaborProtocolTemplateService.java @@ -0,0 +1,20 @@ +package com.ruoyi.business.service; + +import java.util.List; +import com.ruoyi.business.domain.BizLaborProtocolTemplate; + +public interface IBizLaborProtocolTemplateService { + BizLaborProtocolTemplate getById(Long id); + List selectList(BizLaborProtocolTemplate entity); + BizLaborProtocolTemplate selectDefault(); + List selectAllEnabled(); + int insert(BizLaborProtocolTemplate entity); + int update(BizLaborProtocolTemplate entity); + /** + * 设为默认: 先清空所有行的 default_flag='N', 再把目标行设为 'Y' + * 保证全局只有 1 条 default_flag='Y' + */ + int setDefault(Long id); + int deleteByPrimaryKey(Long id); + int deleteByPrimaryKeys(Long[] ids); +} \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizMeetingAttendeeService.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizMeetingAttendeeService.java new file mode 100644 index 0000000..87c3f4b --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizMeetingAttendeeService.java @@ -0,0 +1,16 @@ +package com.ruoyi.business.service; + +import java.util.List; +import com.ruoyi.business.domain.BizMeetingAttendee; + +public interface IBizMeetingAttendeeService { + int insert(BizMeetingAttendee entity); + int updateHandsign(BizMeetingAttendee entity); + int updateLaborProtocol(BizMeetingAttendee entity); + int deleteByMeetingId(Long meetingId); + int deleteByMeetingIdAndUserId(BizMeetingAttendee entity); + List selectByMeetingId(Long meetingId); + List selectByUserId(Long userId); + /** 当前用户的"待签署"会议列表 (handsign 或 labor_protocol 任一为空) */ + List selectUnsignedByUserId(Long userId); +} \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizExpertServiceImpl.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizExpertServiceImpl.java index 7ea5860..71ae77a 100644 --- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizExpertServiceImpl.java +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizExpertServiceImpl.java @@ -10,6 +10,7 @@ import com.ruoyi.business.service.IBizExpertService; import com.ruoyi.common.core.domain.entity.SysUser; import com.ruoyi.common.exception.ServiceException; import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.common.utils.id.IdGenerator; import com.ruoyi.system.service.ISysUserService; @Service @@ -22,7 +23,7 @@ public class BizExpertServiceImpl implements IBizExpertService private ISysUserService sysUserService; @Override - public BizExpert getById(String expertId) + public BizExpert getById(Long expertId) { return bizExpertMapper.selectByPrimaryKey(expertId); } @Override public BizExpert getByUserId(Long userId) @@ -32,12 +33,10 @@ public class BizExpertServiceImpl implements IBizExpertService { return bizExpertMapper.selectList(entity); } /** - * admin 创建专家: 同时创建 sys_user (用户名=手机号, 密码=手机号, role_type=doctor) - * 1. 校验 phone 没注册过 (抛 ServiceException) - * 2. 创建 sys_user + bcrypt 加密密码 - * 3. 设置 role_type=doctor (与公开注册一致) - * 4. 创建 biz_expert 绑定 user_id - * 5. 返回 SysUser 含明文 password (前端 toast 用完即丢) + * 创建专家 + 绑定 sys_user。双职责: + * A. admin 创建 / 批量导入: entity.userId == null → 全流程 (校验 phone + 建 sys_user + 建 biz_expert) + * B. 公开注册 (BizRegisterController): entity.userId 已设 → 控制器已建 sys_user, 本方法只做 biz_expert 绑定 + * 区分标志: entity.getUserId() 是否已设 */ @Override public SysUser insert(BizExpert entity) { @@ -45,34 +44,47 @@ public class BizExpertServiceImpl implements IBizExpertService if (phone == null || phone.isEmpty()) { throw new ServiceException("手机号不能为空"); } - // 0. 校验 phone 唯一 (查 sys_user, 若 username=phone 已存在即重复) - if (sysUserService.isPhoneRegistered(phone)) { - throw new ServiceException("该手机号已注册,请直接登录"); + + Long userId = entity.getUserId(); + SysUser result = new SysUser(); + + if (userId == null) { + // ===== A. admin / 批量导入路径: 全流程 ===== + // 0. 校验 phone 唯一 + if (sysUserService.isPhoneRegistered(phone)) { + throw new ServiceException("该手机号已注册,请直接登录"); + } + // 1. 创建 sys_user (用户名=phone, 密码=phone) + SysUser newUser = new SysUser(); + newUser.setUserName(phone); + newUser.setNickName(entity.getName()); + newUser.setPhonenumber(phone); + newUser.setPassword(SecurityUtils.encryptPassword(phone)); + newUser.setStatus("0"); + newUser.setDelFlag("0"); + newUser.setCreateBy(SecurityUtils.getUsername()); + sysUserService.insertUser(newUser); + userId = newUser.getUserId(); + + // 2. role_type = doctor (DB 默认 executor, 专家需 doctor) + sysUserService.updateRoleType(userId, "doctor"); + + // admin 路径前端需要明文密码做 toast 提示 + result.setUserId(userId); + result.setPassword(phone); } - // 1. 创建 sys_user (用户名=phone, 密码=phone) - SysUser newUser = new SysUser(); - newUser.setUserName(phone); - newUser.setNickName(entity.getName()); - newUser.setPhonenumber(phone); - newUser.setPassword(SecurityUtils.encryptPassword(phone)); - newUser.setStatus("0"); - newUser.setDelFlag("0"); - newUser.setCreateBy(SecurityUtils.getUsername()); - sysUserService.insertUser(newUser); - Long userId = newUser.getUserId(); - - // 2. role_type = doctor (跟公开注册一致,DB 默认 executor, 专家需 doctor) - sysUserService.updateRoleType(userId, "doctor"); - - // 3. 创建 biz_expert 绑定 user_id + // ===== A + B 都走: 创建 biz_expert 绑定 user_id ===== +// expertId 用雪花 ID (53位, JS Number 安全, 不用 DB 自增) entity.setUserId(userId); - com.ruoyi.common.utils.id.SnowflakeId.injectIfEmpty(entity, "expertId"); + entity.setExpertId(IdGenerator.generateId()); bizExpertMapper.insert(entity); - // 4. 把明文密码回填 SysUser (仅本次返回,前端 toast 显示) - newUser.setPassword(phone); - return newUser; + if (userId != null && result.getUserId() == null) { + // B 路径: 控制器已知 userId, 不需返回明文密码 + result.setUserId(userId); + } + return result; } @Override @@ -85,7 +97,7 @@ public class BizExpertServiceImpl implements IBizExpertService * sys_user.status: '0'=正常 '1'=停用 (RuoYi 框架约定, 同步时转换) */ @Override - public int updateStatus(String expertId, String status) { + public int updateStatus(Long expertId, String status) { if (status == null || (!"Y".equals(status) && !"N".equals(status))) { throw new ServiceException("status 必须是 'Y'(正常) 或 'N'(禁用)"); } @@ -120,10 +132,10 @@ public class BizExpertServiceImpl implements IBizExpertService return bizExpertMapper.updateByUserId(entity); } @Override - public int deleteByPrimaryKey(String expertId) + public int deleteByPrimaryKey(Long expertId) { return bizExpertMapper.deleteByPrimaryKey(expertId); } @Override - public int deleteByPrimaryKeys(String[] expertId) + public int deleteByPrimaryKeys(Long[] expertId) { return bizExpertMapper.deleteByPrimaryKeys(expertId); } /** diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizLaborProtocolTemplateServiceImpl.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizLaborProtocolTemplateServiceImpl.java new file mode 100644 index 0000000..2c9813c --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizLaborProtocolTemplateServiceImpl.java @@ -0,0 +1,70 @@ +package com.ruoyi.business.service.impl; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import com.ruoyi.business.domain.BizLaborProtocolTemplate; +import com.ruoyi.business.mapper.BizLaborProtocolTemplateMapper; +import com.ruoyi.business.service.IBizLaborProtocolTemplateService; + +@Service +public class BizLaborProtocolTemplateServiceImpl implements IBizLaborProtocolTemplateService { + + @Autowired + private BizLaborProtocolTemplateMapper mapper; + + @Override + public BizLaborProtocolTemplate getById(Long id) { + return mapper.selectByPrimaryKey(id); + } + + @Override + public List selectList(BizLaborProtocolTemplate entity) { + return mapper.selectList(entity); + } + + @Override + public BizLaborProtocolTemplate selectDefault() { + return mapper.selectDefault(); + } + + @Override + public List selectAllEnabled() { + return mapper.selectAllEnabled(); + } + + @Override + public int insert(BizLaborProtocolTemplate entity) { + return mapper.insert(entity); + } + + @Override + public int update(BizLaborProtocolTemplate entity) { + return mapper.updateByPrimaryKey(entity); + } + + /** + * 设为默认: app 层保证全局只有 1 条 default_flag='Y' + * 事务保护: 先 clearAllDefault, 再把目标行 default_flag='Y' + */ + @Override + @Transactional + public int setDefault(Long id) { + mapper.clearAllDefault(); + BizLaborProtocolTemplate target = new BizLaborProtocolTemplate(); + target.setId(id); + target.setDefaultFlag("Y"); + return mapper.updateByPrimaryKey(target); + } + + @Override + public int deleteByPrimaryKey(Long id) { + return mapper.deleteByPrimaryKey(id); + } + + @Override + public int deleteByPrimaryKeys(Long[] ids) { + return mapper.deleteByPrimaryKeys(ids); + } +} \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizMeetingAttendeeServiceImpl.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizMeetingAttendeeServiceImpl.java new file mode 100644 index 0000000..ea0ce5a --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizMeetingAttendeeServiceImpl.java @@ -0,0 +1,55 @@ +package com.ruoyi.business.service.impl; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.ruoyi.business.domain.BizMeetingAttendee; +import com.ruoyi.business.mapper.BizMeetingAttendeeMapper; +import com.ruoyi.business.service.IBizMeetingAttendeeService; + +@Service +public class BizMeetingAttendeeServiceImpl implements IBizMeetingAttendeeService { + + @Autowired + private BizMeetingAttendeeMapper bizMeetingAttendeeMapper; + + @Override + public int insert(BizMeetingAttendee entity) { + return bizMeetingAttendeeMapper.insert(entity); + } + + @Override + public int updateHandsign(BizMeetingAttendee entity) { + return bizMeetingAttendeeMapper.updateHandsign(entity); + } + + @Override + public int updateLaborProtocol(BizMeetingAttendee entity) { + return bizMeetingAttendeeMapper.updateLaborProtocol(entity); + } + + @Override + public int deleteByMeetingId(Long meetingId) { + return bizMeetingAttendeeMapper.deleteByMeetingId(meetingId); + } + + @Override + public int deleteByMeetingIdAndUserId(BizMeetingAttendee entity) { + return bizMeetingAttendeeMapper.deleteByMeetingIdAndUserId(entity); + } + + @Override + public List selectByMeetingId(Long meetingId) { + return bizMeetingAttendeeMapper.selectByMeetingId(meetingId); + } + + @Override + public List selectByUserId(Long userId) { + return bizMeetingAttendeeMapper.selectByUserId(userId); + } + + @Override + public List selectUnsignedByUserId(Long userId) { + return bizMeetingAttendeeMapper.selectUnsignedByUserId(userId); + } +} \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/resources/mapper/business/BizExpertMapper.xml b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizExpertMapper.xml index 36ef9ad..cef8d71 100644 --- a/ry-api/ruoyi-business/src/main/resources/mapper/business/BizExpertMapper.xml +++ b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizExpertMapper.xml @@ -36,7 +36,7 @@ where user_id = #{userId} limit 1 - where expert_id = #{expertId} @@ -53,7 +53,7 @@ insert into biz_expert - expert_id, + expert_id, user_id, name, phone, @@ -81,7 +81,7 @@ update_time, - #{expertId}, + #{expertId}, #{userId}, #{name}, #{phone}, @@ -206,13 +206,13 @@ where expert_id = #{expertId} - + delete from biz_expert where expert_id = #{expertId} - + delete from biz_expert where expert_id in - - #{expertId} + + #{id} \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/resources/mapper/business/BizLaborProtocolTemplateMapper.xml b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizLaborProtocolTemplateMapper.xml new file mode 100644 index 0000000..4161499 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizLaborProtocolTemplateMapper.xml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + select id, template_name, template_content, default_flag, sort_order, status, remark, + create_by, create_time, update_by, update_time + from biz_labor_protocol_template + + + + + + + insert into biz_labor_protocol_template(template_name, template_content, default_flag, sort_order, status, remark, create_by, create_time) + values(#{templateName}, #{templateContent}, #{defaultFlag}, #{sortOrder}, #{status}, #{remark}, #{createBy}, sysdate()) + + + update biz_labor_protocol_template + + template_name = #{templateName}, + template_content = #{templateContent}, + default_flag = #{defaultFlag}, + sort_order = #{sortOrder}, + status = #{status}, + remark = #{remark}, + update_by = #{updateBy}, + update_time = sysdate() + + where id = #{id} + + + update biz_labor_protocol_template set default_flag = 'N', update_time = sysdate() + + + delete from biz_labor_protocol_template where id = #{id} + + + delete from biz_labor_protocol_template where id in + + #{id} + + + \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/resources/mapper/business/BizMeetingAttendeeMapper.xml b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizMeetingAttendeeMapper.xml new file mode 100644 index 0000000..5c0c076 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizMeetingAttendeeMapper.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + insert into biz_meeting_attendee(meeting_id, user_id, create_by, create_time) + values(#{meetingId}, #{userId}, #{createBy}, sysdate()) + + + update biz_meeting_attendee + set handsign = #{handsign}, + update_by = #{updateBy}, + update_time = sysdate() + where id = #{id} + + + update biz_meeting_attendee + set labor_protocol = #{laborProtocol}, + update_by = #{updateBy}, + update_time = sysdate() + where id = #{id} + + + delete from biz_meeting_attendee where meeting_id = #{meetingId} + + + delete from biz_meeting_attendee where meeting_id = #{meetingId} and user_id = #{userId} + + + + + + \ No newline at end of file diff --git a/ry-api/ruoyi-business/src/main/resources/mapper/business/BizMeetingMapper.xml b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizMeetingMapper.xml index e2e1511..01babfb 100644 --- a/ry-api/ruoyi-business/src/main/resources/mapper/business/BizMeetingMapper.xml +++ b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizMeetingMapper.xml @@ -45,6 +45,8 @@ and current_stage = #{currentStage} and start_time >= #{startTime} and end_time <= #{endTime} + + and exists (select 1 from biz_meeting_attendee a where a.meeting_id = biz_meeting.meeting_id and a.user_id = #{userId}) order by meeting_id desc diff --git a/ry-api/ruoyi-business/src/main/resources/mapper/business/BizPersonMapper.xml b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizPersonMapper.xml index 97ae706..b18164a 100644 --- a/ry-api/ruoyi-business/src/main/resources/mapper/business/BizPersonMapper.xml +++ b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizPersonMapper.xml @@ -57,6 +57,7 @@ and p.role = #{role} and u.status = #{status} and u.parent_user_id = #{parentUserId} + and p.user_id = #{userId} and p.user_id in diff --git a/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java index 71a1cef..056bcd2 100644 --- a/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java +++ b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java @@ -60,6 +60,9 @@ public class SecurityConfig requests.requestMatchers("/login", "/register", "/captchaImage").permitAll() // OSS 文件代理 (PDF/图片内嵌预览, 重写 Content-Disposition 为 inline) .requestMatchers(HttpMethod.GET, "/common/oss/proxy").permitAll() + // OSS 直传签名 (注册场景需匿名访问: 专家/执行方/支持方上传证书时还没 token) + // 安全性: OssController 已用 policy 限定 dir 前缀 + 文件大小, key 含时间戳+随机串防覆盖 + .requestMatchers(HttpMethod.GET, "/common/oss/sign").permitAll() // 业务公开门户与注册入口可匿名访问; 字典 active/单查公开, 写操作需登录+权限 .requestMatchers("/business/public/**", "/business/publicity/**", "/business/auth/**", "/business/sms/**").permitAll() // 字典 active (只读) + 按 ID 查 公开给前端拉下拉数据 diff --git a/ry-vue3/src/api/business/expert.js b/ry-vue3/src/api/business/expert.js new file mode 100644 index 0000000..beb9f03 --- /dev/null +++ b/ry-vue3/src/api/business/expert.js @@ -0,0 +1,10 @@ +import request from '@/utils/request' + +/** + * 当前登录用户的专家信息 (按 token 拿 userId 查 biz_expert) + * 返回 biz_expert 完整记录, 含 name / phone / department / title / workUnit 等 + * 若当前用户不是专家 / 没注册过, 返回 null + */ +export function getMyExpertProfile() { + return request({ url: '/business/expert/profile', method: 'get' }) +} \ No newline at end of file diff --git a/ry-vue3/src/api/business/laborProtocolTemplate.js b/ry-vue3/src/api/business/laborProtocolTemplate.js new file mode 100644 index 0000000..aaa0228 --- /dev/null +++ b/ry-vue3/src/api/business/laborProtocolTemplate.js @@ -0,0 +1,30 @@ +import request from '@/utils/request' + +/** + * 劳务协议模板 API + * 全局共享, admin 在 /admin/labor-protocol 管理 + */ +export function listLaborProtocolTemplates(params) { + return request({ url: '/business/laborProtocolTemplate/list', method: 'get', params }) +} +export function getLaborProtocolTemplate(id) { + return request({ url: `/business/laborProtocolTemplate/${id}`, method: 'get' }) +} +export function getDefaultLaborProtocolTemplate() { + return request({ url: '/business/laborProtocolTemplate/default', method: 'get' }) +} +export function listEnabledLaborProtocolTemplates() { + return request({ url: '/business/laborProtocolTemplate/allEnabled', method: 'get' }) +} +export function createLaborProtocolTemplate(data) { + return request({ url: '/business/laborProtocolTemplate', method: 'post', data }) +} +export function updateLaborProtocolTemplate(data) { + return request({ url: '/business/laborProtocolTemplate', method: 'put', data }) +} +export function deleteLaborProtocolTemplate(id) { + return request({ url: `/business/laborProtocolTemplate/${id}`, method: 'delete' }) +} +export function setDefaultLaborProtocolTemplate(id) { + return request({ url: `/business/laborProtocolTemplate/${id}/default`, method: 'put' }) +} \ No newline at end of file diff --git a/ry-vue3/src/api/business/meetingAttendee.js b/ry-vue3/src/api/business/meetingAttendee.js new file mode 100644 index 0000000..582ae44 --- /dev/null +++ b/ry-vue3/src/api/business/meetingAttendee.js @@ -0,0 +1,25 @@ +import request from '@/utils/request' + +/** + * 当前登录用户的"待签署"会议列表 (handsign 或 labor_protocol 任一为空) + * 返回 [{id, meetingId, meetingName, startTime, ...}, ...] + */ +export function listUnsignedMeetingProtocols() { + return request({ url: '/business/meetingAttendee/unsigned', method: 'get' }) +} + +/** + * 更新手写签名 (Base64 字符串, 直接存 DB longtext) + * @param {number|string} id 中间表主键 + * @param {string} handsign Base64 编码的签名图片 + */ +export function updateHandsign(id, handsign) { + return request({ url: `/business/meetingAttendee/${id}/handsign`, method: 'put', data: { handsign } }) +} + +/** + * 更新劳务协议 URL (OSS 文件 URL, 由前端 OssImageUploader 上传后传入) + */ +export function updateLaborProtocol(id, laborProtocol) { + return request({ url: `/business/meetingAttendee/${id}/laborProtocol`, method: 'put', data: { laborProtocol } }) +} \ No newline at end of file diff --git a/ry-vue3/src/layout/AdminLayout.vue b/ry-vue3/src/layout/AdminLayout.vue index 679d7a9..cab0e1b 100644 --- a/ry-vue3/src/layout/AdminLayout.vue +++ b/ry-vue3/src/layout/AdminLayout.vue @@ -82,7 +82,8 @@ const MENU = { ]}, { path: '/admin/manage', title: '网站管理', icon: Setting, children: [ { path: '/admin/article', title: '协议管理', icon: Files }, - { path: '/admin/special-plan', title: '专项计划管理', icon: Compass } + { path: '/admin/special-plan', title: '专项计划管理', icon: Compass }, + { path: '/admin/labor-protocol', title: '劳务协议配置', icon: Document } ]}, { path: '/admin/account', title: '账号信息', icon: User } ], diff --git a/ry-vue3/src/router/index.js b/ry-vue3/src/router/index.js index 6fd1f3f..a1cd3d7 100644 --- a/ry-vue3/src/router/index.js +++ b/ry-vue3/src/router/index.js @@ -47,6 +47,7 @@ const routes = [ { path: 'special-plan', name: 'admin-special-plan', component: () => import('@/views/admin/BizSpecialPlanAdmin.vue'), meta: { title: '专项计划管理' } }, { path: 'special-plan/edit/:id', name: 'admin-special-plan-edit', component: () => import('@/views/admin/BizSpecialPlanEdit.vue'), meta: { title: '编辑专项计划' } }, { path: 'project-category', name: 'admin-project-category', component: () => import('@/views/admin/ProjectCategory.vue'), meta: { title: '项目类别管理' } }, + { path: 'labor-protocol', name: 'admin-labor-protocol', component: () => import('@/views/admin/LaborProtocol.vue'), meta: { title: '劳务协议配置' } }, { path: 'account', name: 'admin-account', component: () => import('@/views/admin/Account.vue'), meta: { title: '账号信息' } } ] }, diff --git a/ry-vue3/src/views/admin/Experts.vue b/ry-vue3/src/views/admin/Experts.vue index 7a5f02f..6f7ce23 100644 --- a/ry-vue3/src/views/admin/Experts.vue +++ b/ry-vue3/src/views/admin/Experts.vue @@ -260,7 +260,7 @@ function fmtTime(d) { // ===== 审核 ===== const auditOpen = ref(false) // result: '2'=通过, '3'=拒绝 (BizAuditStatusEnum: 0未提交 1待审核 2通过 3拒绝) -const auditForm = reactive({ expertId: '', name: '', phone: '', result: '2', opinion: '' }) +const auditForm = reactive({ expertId: null, name: '', phone: '', result: '2', opinion: '' }) function onAudit(row) { auditForm.expertId = row.expertId auditForm.name = row.name diff --git a/ry-vue3/src/views/admin/LaborProtocol.vue b/ry-vue3/src/views/admin/LaborProtocol.vue new file mode 100644 index 0000000..eef2ff8 --- /dev/null +++ b/ry-vue3/src/views/admin/LaborProtocol.vue @@ -0,0 +1,219 @@ + + + + + \ No newline at end of file diff --git a/ry-vue3/src/views/auth/Login.vue b/ry-vue3/src/views/auth/Login.vue index fea0899..738bdc1 100644 --- a/ry-vue3/src/views/auth/Login.vue +++ b/ry-vue3/src/views/auth/Login.vue @@ -267,6 +267,7 @@ async function afterLogin(token, displayName, fallbackRole) { userId: u.userId, userName: u.userName || displayName, nickName: u.nickName || displayName, + phonenumber: u.phonenumber || '', accountType: u.accountType || 'MAIN', parentUserId: u.parentUserId || null, role @@ -277,6 +278,7 @@ async function afterLogin(token, displayName, fallbackRole) { userId: null, userName: displayName, nickName: displayName, + phonenumber: '', accountType: 'MAIN', parentUserId: null, role: fallbackRole diff --git a/ry-vue3/src/views/auth/RegisterExpert.vue b/ry-vue3/src/views/auth/RegisterExpert.vue index d9e726d..82f406f 100644 --- a/ry-vue3/src/views/auth/RegisterExpert.vue +++ b/ry-vue3/src/views/auth/RegisterExpert.vue @@ -18,10 +18,10 @@ - + - + diff --git a/ry-vue3/src/views/doctor/Home.vue b/ry-vue3/src/views/doctor/Home.vue index 5be8b67..91dba4e 100644 --- a/ry-vue3/src/views/doctor/Home.vue +++ b/ry-vue3/src/views/doctor/Home.vue @@ -5,7 +5,7 @@
-

下午好,{{ store.user?.userName || '专家' }}专家

+

下午好,{{ displayName }}专家

欢迎使用项目管理系统

@@ -37,13 +37,11 @@ 更多 →
    -
  • +
  • - {{ s.planName }} + {{ s.meetingName || ('会议 #' + s.meetingId) }}
    - - {{ s.status === '2' ? '已通过' : (s.status === '3' ? '已退回' : (s.status === '1' ? '审核中' : '待提交')) }} - + 待签署
  • 暂无待签协议
@@ -72,15 +70,23 @@