feat: 多处页面改造 + 新增劳务协议模板配置
主要改动: - biz_meeting_attendee 中间表 + doctor/expert 角色数据隔离 - biz_meeting_attendee 加 handsign/labor_protocol 字段 (劳务协议改造) - biz_labor_protocol_template 配置表 + admin 页面 (从 HeguiConstants 迁移) - biz_expert.expertId 改 Long + IdGenerator 生成 (去自增) - Login.vue / PublicityDetail.vue / Home.vue 等多处 bug 修复 + UI 改进 - 新增 page-tech-review 报告 3 篇 (_self/*.md) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,340 @@
|
|||||||
|
# /doctor/home 页面端到端审查 (v3)
|
||||||
|
|
||||||
|
**审查日期**: 2026-08-19 (v3 含修复)
|
||||||
|
**审查范围**: 前端 → 后端 → DB → 原型
|
||||||
|
**审查者**: Claude Code (page-tech-review skill)
|
||||||
|
|
||||||
|
> **修订 (v1 → v2)**:
|
||||||
|
> - ✅ P1 数据隔离: `biz_meeting_attendee` 中间表 + mapper EXISTS + controller 注入 (doctor/expert 角色)
|
||||||
|
> - ✅ 欢迎栏显示真实姓名 (`biz_expert.name` → fallback 到 nickName/userName)
|
||||||
|
>
|
||||||
|
> **修订 (v2 → v3)**:
|
||||||
|
> - ✅ "待签署协议" 数据源从 `biz_project_plan` 改到 `biz_meeting_attendee` (按 user_id + 任一未签)
|
||||||
|
> - ✅ `biz_meeting_attendee` 表加 2 字段: `handsign longtext` (Base64 手写签名) + `labor_protocol varchar(500)` (劳务协议 URL)
|
||||||
|
> - ✅ 新建后端 controller + service + mapper + 前端 API
|
||||||
|
> - ❌ 签署 dialog + 手写板 UI **未做** (v3 待修)
|
||||||
|
> - ❌ 新建会议自动写 attendee 中间表 (BizMeetingController.add) **未做**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. 组件定位 (四跳链)
|
||||||
|
|
||||||
|
| 跳 | 命中 |
|
||||||
|
|---|---|
|
||||||
|
| ① 角色菜单 | `AdminLayout.vue:90` `MENU.doctor` → `{ path: '/doctor/home', title: '首页' }` |
|
||||||
|
| ② 路由 | `router/index.js:90` → `name: 'doctor-home', component: () => import('@/views/doctor/Home.vue')` |
|
||||||
|
| ③ 组件 | `ry-vue3/src/views/doctor/Home.vue` |
|
||||||
|
| ④ 原型 | `proto/html/doctor.html:204` `data-page="components/home.html"` → `proto/html/components/home.html` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 主要功能 + 可见性
|
||||||
|
|
||||||
|
### 1.1 主要功能
|
||||||
|
|
||||||
|
页面是 **dashboard 工作台**,4 块内容:
|
||||||
|
|
||||||
|
| 功能 | 前端入口 | 后端接口 | 数据表 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 欢迎栏 (实时时钟 + 专家姓名) | `Home.vue:6-15` | `GET /business/expert/profile` | `biz_expert` |
|
||||||
|
| 待参加的会议 | `Home.vue:114` `bizList('meeting', {pageSize: 5})` | `GET /business/meeting/list` | `biz_meeting` + `biz_meeting_attendee` ✅ |
|
||||||
|
| **待签署的协议 (v3 改)** | `Home.vue:126` `listUnsignedMeetingProtocols()` | `GET /business/meetingAttendee/unsigned` | `biz_meeting_attendee` (handsign 或 labor_protocol 为空) ✅ |
|
||||||
|
| 通知消息 | `Home.vue` `listMyMessages({limit: 5})` | `GET /business/message/my` | `biz_message` |
|
||||||
|
|
||||||
|
页面**无 CRUD 操作**,纯展示 (v3 计划加签署 dialog 但未做)。
|
||||||
|
|
||||||
|
### 1.2 可见性 (三层过滤)
|
||||||
|
|
||||||
|
| 层 | 来源 | 校验字段 |
|
||||||
|
|---|---|---|
|
||||||
|
| 前端菜单 | `AdminLayout.vue:90` `MENU.doctor` | 仅 `doctor` 角色可见 |
|
||||||
|
| 路由守卫 | `router/index.js:88` `meta: { role: 'doctor' }` + `permission.js` | token 角色 |
|
||||||
|
| 后端 | 各 controller 注入 + mapper `<if>` 过滤 | 详见 §1.1 |
|
||||||
|
|
||||||
|
**数据隔离现状** (v3):
|
||||||
|
|
||||||
|
| 数据块 | 隔离机制 |
|
||||||
|
|---|---|
|
||||||
|
| 欢迎栏 | `biz_expert` 按 token userId |
|
||||||
|
| 待参加会议 | `biz_meeting_attendee` (EXISTS) — controller 注入 `userId` for doctor/expert |
|
||||||
|
| **待签署协议 (v3)** | `biz_meeting_attendee.user_id = current user` (controller `SecurityUtils.getUserId()`) |
|
||||||
|
| 通知消息 | `biz_message.user_id = current user` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 筛选项 (filter-form)
|
||||||
|
|
||||||
|
**本页面无 filter-form**,跳过。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 工具栏按钮 (toolbar)
|
||||||
|
|
||||||
|
**本页面无 toolbar**,只有 3 个"更多"链接 (`Home.vue:22, 37, 57`)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 表格 (el-table)
|
||||||
|
|
||||||
|
**本页面无表格**,使用 `ul/li` 自定义列表。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. UI 元素映射
|
||||||
|
|
||||||
|
### 5.1 欢迎栏 (`Home.vue:6-15`)
|
||||||
|
|
||||||
|
| UI | 绑定 | 来源 |
|
||||||
|
|---|---|---|
|
||||||
|
| 用户名 (左) | `displayName` (computed) | 优先级: `biz_expert.name` → `nickName` → `userName` → '专家' |
|
||||||
|
| 当前时间 | `nowTime` (setInterval) | 前端 Date |
|
||||||
|
| 当前日期 | `nowDate` (setInterval) | 前端 Date |
|
||||||
|
|
||||||
|
### 5.2 待参加会议列表 (`Home.vue:24-32`)
|
||||||
|
|
||||||
|
| UI label | 绑定字段 | 后端 SQL | 命中表.字段 | 原型对照 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| 会议名 | `m.meetingName` | selectList + `EXISTS biz_meeting_attendee` | `biz_meeting.meeting_name` | ✅ |
|
||||||
|
| 状态/时间 | `formatTime(m.startTime)` | selectList | `biz_meeting.start_time` | ✅ |
|
||||||
|
|
||||||
|
### 5.3 待签署协议列表 (`Home.vue:39-49`) — v3 重大改动
|
||||||
|
|
||||||
|
| UI label | 绑定字段 | 后端 SQL | 命中表.字段 | 原型对照 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| 协议名 | `s.meetingName` | `selectUnsignedByUserId` JOIN `biz_meeting` | `biz_meeting.meeting_name` (via JOIN) | ⚠️ 原型写"协议/意见书",实际显示会议名 |
|
||||||
|
| 状态 | 固定 "待签署" | — | — | ⚠️ 实现只显示"待签署",原型有"已完成" |
|
||||||
|
|
||||||
|
**v2 → v3 数据流变化**:
|
||||||
|
- **v2 (错误)**: `GET /business/projectPlan/list` → 按 `submitter_id` 过滤 (这是"投稿方案"语义, 不是劳务协议)
|
||||||
|
- **v3 (正确)**: `GET /business/meetingAttendee/unsigned` → 当前用户 + 任一未签 → JOIN biz_meeting 取会议名
|
||||||
|
|
||||||
|
**API** (`ry-vue3/src/api/business/meetingAttendee.js`):
|
||||||
|
```js
|
||||||
|
export function listUnsignedMeetingProtocols() {
|
||||||
|
return request({ url: '/business/meetingAttendee/unsigned', method: 'get' })
|
||||||
|
}
|
||||||
|
export function updateHandsign(id, handsign) { ... }
|
||||||
|
export function updateLaborProtocol(id, laborProtocol) { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
**后端 mapper SQL** (核心):
|
||||||
|
```sql
|
||||||
|
select a.id, a.meeting_id, a.user_id, a.handsign, a.labor_protocol,
|
||||||
|
m.meeting_name as meetingName, m.start_time as startTime,
|
||||||
|
m.end_time as endTime, m.project_name as projectName, m.project_no as projectNo
|
||||||
|
from biz_meeting_attendee a
|
||||||
|
inner join biz_meeting m on m.meeting_id = a.meeting_id
|
||||||
|
where a.user_id = #{userId}
|
||||||
|
and (a.handsign is null or a.handsign = ''
|
||||||
|
or a.labor_protocol is null or a.labor_protocol = '')
|
||||||
|
order by m.start_time asc
|
||||||
|
```
|
||||||
|
|
||||||
|
**判定逻辑 (决策 A1)**: 任一未签即"待签署"。两条都有值就排除。
|
||||||
|
|
||||||
|
### 5.4 通知消息列表 (`Home.vue:59-69`)
|
||||||
|
|
||||||
|
略,同 v2 报告。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Java Entity ↔ 数据库表 一致性
|
||||||
|
|
||||||
|
### 6.1 biz_meeting_attendee (v3 实测, 2026-08-19)
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE biz_meeting_attendee (
|
||||||
|
id bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||||
|
meeting_id bigint NOT NULL COMMENT '会议ID (FK biz_meeting.meeting_id)',
|
||||||
|
user_id bigint NOT NULL COMMENT '参会人 user_id (FK sys_user.user_id)',
|
||||||
|
handsign longtext COMMENT '手写签名 Base64',
|
||||||
|
labor_protocol varchar(500) DEFAULT NULL COMMENT '劳务协议 URL (OSS)',
|
||||||
|
create_by varchar(64) DEFAULT '' COMMENT '添加人',
|
||||||
|
create_time datetime DEFAULT NULL COMMENT '添加时间',
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
UNIQUE KEY uk_meeting_user (meeting_id, user_id),
|
||||||
|
KEY idx_user_id (user_id)
|
||||||
|
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='会议参会人';
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 BizMeetingAttendee Entity ↔ DB 字段对照
|
||||||
|
|
||||||
|
| 实体字段 | 中文列名 | Java 类型 | 表字段 | DB 类型 | 一致? |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| `id` | 主键 | `Long` | `id` | bigint AUTO_INCREMENT | ✅ |
|
||||||
|
| `meetingId` | 会议ID | `Long` | `meeting_id` | bigint | ✅ |
|
||||||
|
| `userId` | 参会人 user_id | `Long` | `user_id` | bigint | ✅ |
|
||||||
|
| **`handsign`** | 手写签名 Base64 | `String` | `handsign` | longtext | ✅ (v3 新增) |
|
||||||
|
| **`laborProtocol`** | 劳务协议 URL (OSS) | `String` | `labor_protocol` | varchar(500) | ✅ (v3 新增) |
|
||||||
|
| `createBy` (BaseEntity) | 添加人 | `String` | `create_by` | varchar(64) | ✅ |
|
||||||
|
| `createTime` | 添加时间 | `Date` | `create_time` | datetime | ✅ |
|
||||||
|
| `meetingName` (transient) | — | `String` | JOIN `m.meeting_name` | — | ✅ (联表字段) |
|
||||||
|
| `startTime` (transient) | — | `Date` | JOIN `m.start_time` | — | ✅ |
|
||||||
|
| `endTime` (transient) | — | `Date` | JOIN `m.end_time` | — | ✅ |
|
||||||
|
| `projectName` (transient) | — | `String` | JOIN `m.project_name` | — | ✅ |
|
||||||
|
| `projectNo` (transient) | — | `String` | JOIN `m.project_no` | — | ✅ |
|
||||||
|
|
||||||
|
**5 个 transient 联表字段** 接收 `selectUnsignedByUserId` 的 JOIN 输出, 给前端用。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 索引检查
|
||||||
|
|
||||||
|
### 7.1 biz_meeting_attendee
|
||||||
|
|
||||||
|
| Key | 列 | 用途覆盖 |
|
||||||
|
|---|---|---|
|
||||||
|
| PRIMARY | `id` | ✅ |
|
||||||
|
| uk_meeting_user | `meeting_id`, `user_id` (UNIQUE) | ✅ 防重复 + 精确查 |
|
||||||
|
| idx_user_id | `user_id` | ✅ `selectUnsignedByUserId WHERE user_id = ?` 走这个索引 |
|
||||||
|
|
||||||
|
**评价**: 复合查询 (`WHERE user_id = X AND (handsign IS NULL OR labor_protocol IS NULL)`) 走 idx_user_id,然后行过滤。10k 行内完全 OK。
|
||||||
|
|
||||||
|
### 7.2 缺什么?
|
||||||
|
|
||||||
|
- ❌ **缺 `idx_user_status`**: 如果会议量大且空记录占比小, 想加速"待签署"查询, 可以加 `(user_id, handsign IS NULL, labor_protocol IS NULL)` 函数索引。但目前数据少, 暂不需要。
|
||||||
|
- ❌ **缺 `idx_meeting_status`**: `WHERE meeting_id = X AND handsign IS NULL` 场景目前不存在 (管理端"看哪些医生没签"的视图未做), 暂不需要。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 与原型差异
|
||||||
|
|
||||||
|
### 8.1 实现新增 (超出原型)
|
||||||
|
|
||||||
|
| 项 | 实现 | 原型 |
|
||||||
|
|---|---|---|
|
||||||
|
| 实时时钟 | ✅ | ✅ 静态 |
|
||||||
|
| 通知标记已读 | ✅ | ❌ |
|
||||||
|
| "更多"链接跳 Vue 路由 | ✅ | ❌ 静态 HTML |
|
||||||
|
| 空状态文案 | ✅ | ❌ |
|
||||||
|
| 欢迎栏显示真实姓名 | ✅ (v2) | ❌ 静态写死 |
|
||||||
|
| **数据源按 user 隔离** | ✅ (v2 加中间表) | ❌ |
|
||||||
|
| **劳务协议按会议关联** | ✅ (v3 改 biz_meeting_attendee) | ❌ |
|
||||||
|
|
||||||
|
### 8.2 原型有但实现缺失
|
||||||
|
|
||||||
|
| 项 | 原型 | 实现 | 严重度 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| item-icon 图标 (📅 📄) | ✅ | ❌ 无 | P2 |
|
||||||
|
| section-title 左侧蓝条 | ✅ | ❌ 无 | P2 |
|
||||||
|
| 欢迎栏渐变背景 | ✅ | ⚠️ 单色 | P3 |
|
||||||
|
| **"已完成" 状态显示** (signed status) | ✅ `item-status.done` | ❌ 实现永远显示"待签署" | **P1** |
|
||||||
|
| **签署交互** (点列表 → 弹 dialog → 上传 handsign + labor_protocol) | ✅ 原型点击 alert | ❌ 无 dialog, 点列表没反应 | **P1** |
|
||||||
|
|
||||||
|
### 8.3 文字 / 标签差异
|
||||||
|
|
||||||
|
| 项 | 原型 | 实现 |
|
||||||
|
|---|---|---|
|
||||||
|
| 协议状态文案 | "待签署 / 已完成" | "待签署" (永远) |
|
||||||
|
| 协议名显示 | "《方案名》专家意见书" | "会议名" (会议关联, 语义不同) |
|
||||||
|
|
||||||
|
### 8.4 总结
|
||||||
|
|
||||||
|
**v3 整体方向**: 数据语义修正 (劳务协议按会议关联 ✅), 视觉 + 交互仍待补。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 字段冗余 / 设计问题
|
||||||
|
|
||||||
|
### 9.1 biz_meeting_attendee 设计合理 (v3 验证)
|
||||||
|
|
||||||
|
- `handsign longtext` 存 Base64 是合理的: 不依赖 OSS, 数据完整性内嵌 DB, 简单
|
||||||
|
- `labor_protocol varchar(500)` 存 OSS URL 是合理的: 大文件走 OSS, DB 只存引用
|
||||||
|
|
||||||
|
### 9.2 缺管理端点 ⚠️ P1
|
||||||
|
|
||||||
|
`BizMeetingAttendeeController` 目前只有 doctor 用的 3 接口。**新建会议时, 没有 API 自动把参会人写入中间表**。
|
||||||
|
- 影响: 现有数据需要手动 INSERT (像之前那样用 SQL), 业务没法在线给会议加参会人
|
||||||
|
- 修复: `BizMeetingController.add` 接受 `attendeeUserIds: Long[]`, 批量 insert 中间表
|
||||||
|
|
||||||
|
### 9.3 缺管理列表 (P3)
|
||||||
|
|
||||||
|
没有"看某会议的所有参会人 + 签署状态"的后台接口 (manager 端要用)。
|
||||||
|
- 影响小 (低优先级), 当前 P1 先修
|
||||||
|
|
||||||
|
### 9.4 biz_project_plan 完全废弃? ⚠️
|
||||||
|
|
||||||
|
v3 把"待签署协议"从 biz_project_plan 改走中间表, 但 `biz_project_plan` 表和 `BizProjectPlan` entity 还在。
|
||||||
|
- 投稿方案业务 (manager 审稿) 仍存在, 这是独立的 "项目策划方案投稿" 业务
|
||||||
|
- 与劳务协议是两回事, 不应混淆
|
||||||
|
|
||||||
|
### 9.5 biz_meeting.status 缺失 (沿 v2 待修)
|
||||||
|
|
||||||
|
业务希望"待签署协议"按 `meeting.status` 过滤 (会议取消/已结束的不再显示待签), 但 `biz_meeting` 无 status 字段, 需要 ALTER。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 待修复列表 (v3)
|
||||||
|
|
||||||
|
| # | 问题 | 文件 | 修复建议 | 严重度 | 状态 |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| ~~1~~ | ~~`biz_meeting` 无 user 隔离~~ | ~~Controller + mapper~~ | ~~中间表 EXISTS~~ | ~~P1~~ | ✅ v2 修 |
|
||||||
|
| ~~2~~ | ~~欢迎栏显示数字 userName~~ | ~~Home.vue~~ | ~~displayName computed 优先级链~~ | ~~P2~~ | ✅ v2 修 |
|
||||||
|
| ~~3~~ | ~~劳务协议数据源错 (biz_project_plan)~~ | ~~Home.vue + BizProjectPlanController~~ | ~~改走 biz_meeting_attendee~~ | ~~P0~~ | ✅ **v3 修** |
|
||||||
|
| ~~4~~ | ~~表无 handsign/labor_protocol~~ | ~~biz_meeting_attendee~~ | ~~ALTER 加 2 列 + 后端全套接口~~ | ~~P0~~ | ✅ **v3 修** |
|
||||||
|
| 6 | **签署 dialog 未做**: 点"待签署"列表项无反应 | `Home.vue` | 弹 dialog: handsign 用 textarea 输入 Base64 (临时), labor_protocol 用 OssImageUploader, 提交分别 PUT | **P1** | 待修 |
|
||||||
|
| 7 | **手写板 canvas 组件未做**: handsign 暂用 textarea 输入 Base64, 用户体验差 | 新建 `SignaturePad.vue` | vue-signature-pad 库 或 自写 canvas + toDataURL() | P2 | 待修 |
|
||||||
|
| 8 | item-icon 图标缺失 (📅 📄) | `Home.vue` | 加 `.item-icon` 样式 + 模板 | P2 | 待修 (v1 提的) |
|
||||||
|
| 9 | section-title 左侧蓝条缺失 | `Home.vue:151` | border-left + padding-left | P2 | 待修 (v1 提的) |
|
||||||
|
| 10 | 欢迎栏渐变背景降级为单色 | `Home.vue:145` | 还原 gradient | P3 | 待修 (v1 提的) |
|
||||||
|
| 11 | `biz_project_plan.status` 注释缺失 | DB | ALTER COMMENT | P3 | 待修 |
|
||||||
|
| 12 | `biz_meeting.status` 不存在 (无法过滤"已结束"会议) | DB + mapper | ALTER 加 status 字段 | P3 | 待修 |
|
||||||
|
| 13 | `Home.vue:131` `read: a.read \|\| i >= 2` 强制第3条后标已读 | `Home.vue` | 改为纯 `read: a.read` | P3 | 待修 |
|
||||||
|
| 14 | **新建会议自动写 attendee 中间表**: BizMeetingController.add 不支持传 attendeeUserIds | `BizMeetingController.java` + service | add 接受 `attendeeUserIds: Long[]`, 批量 insert 中间表 | **P1** | 待修 |
|
||||||
|
| 15 | 缺管理端"看参会人列表 + 签署状态"接口 | 新建 `BizMeetingAttendeeController` 管理端点 | `GET /business/meetingAttendee/byMeeting/{meetingId}` | P3 | 待修 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. 引用清单
|
||||||
|
|
||||||
|
### v3 新增/改的文件
|
||||||
|
|
||||||
|
| 文件 | 行号 | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| **DB** `biz_meeting_attendee` | — | ALTER 加 `handsign longtext` + `labor_protocol varchar(500)` |
|
||||||
|
| `ry-api/.../controller/BizMeetingAttendeeController.java` | 全文 | **v3 新建** |
|
||||||
|
| `ry-api/.../service/IBizMeetingAttendeeService.java` | 全文 | **v3 新建** |
|
||||||
|
| `ry-api/.../service/impl/BizMeetingAttendeeServiceImpl.java` | 全文 | **v3 新建** |
|
||||||
|
| `ry-api/.../domain/BizMeetingAttendee.java` | 全文 | **v3 加 handsign + laborProtocol + 5 个 transient 联表字段** |
|
||||||
|
| `ry-api/.../mapper/BizMeetingAttendeeMapper.java` | 全文 | **v3 加 updateHandsign + updateLaborProtocol + selectUnsignedByUserId** |
|
||||||
|
| `ry-api/.../mapper/business/BizMeetingAttendeeMapper.xml` | 全文 | **v3 加 resultMap 字段 + 2 update + selectUnsignedByUserId 联表 SQL** |
|
||||||
|
| `ry-vue3/src/api/business/meetingAttendee.js` | 全文 | **v3 新建**: 3 API helpers |
|
||||||
|
| `ry-vue3/src/views/doctor/Home.vue` | 39-49, 124-134 | **v3 改 "待签署协议" 数据源** |
|
||||||
|
|
||||||
|
### 未改的文件 (沿 v2)
|
||||||
|
|
||||||
|
| 文件 | 行号 | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| `ry-vue3/src/layout/AdminLayout.vue` | 90 | `MENU.doctor` 第 1 项 |
|
||||||
|
| `ry-vue3/src/router/index.js` | 88-99 | `/doctor` 父路由 |
|
||||||
|
| `proto/html/doctor.html` | 204 | 原型菜单 |
|
||||||
|
| `proto/html/components/home.html` | 全文 | 原型基准 |
|
||||||
|
| `ry-vue3/src/api/public.js` | — | listMyMessages / bizList |
|
||||||
|
| `ry-vue3/src/api/business/expert.js` | 全文 | v2 新建: getMyExpertProfile |
|
||||||
|
| `ry-api/.../controller/BizMeetingController.java` | 23-33 | v2 改: 加 userId 注入 |
|
||||||
|
| `ry-api/.../controller/BizProjectPlanController.java` | — | 不再被"待签署协议"调用, 投稿方案独立业务 |
|
||||||
|
| `ry-api/.../controller/BizExpertController.java` | 46-50 | 欢迎栏取名 |
|
||||||
|
| MySQL `guoju0808.biz_meeting` | — | v2 无改动 |
|
||||||
|
| MySQL `guoju0808.biz_meeting_attendee` | — | **v3 加 2 列** |
|
||||||
|
| MySQL `guoju0808.biz_message` | — | 通知 |
|
||||||
|
| MySQL `guoju0808.biz_expert` (user_id=142) | — | name='dct07' |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v3 总结
|
||||||
|
|
||||||
|
**修复 2 项 (跨 v2 → v3)**:
|
||||||
|
1. ✅ **数据语义修正**: "待签署协议" 从 `biz_project_plan` 改到 `biz_meeting_attendee` (语义正确: 劳务协议按会议关联, 不按投稿方案)
|
||||||
|
2. ✅ **新字段 + 后端**: `handsign longtext` + `labor_protocol varchar(500)` + 完整后端 CRUD 接口 + 前端 API
|
||||||
|
|
||||||
|
**还剩 11 项待修复**,按严重度:
|
||||||
|
- **P1 (2 项)**: 签署 dialog 缺失 (点列表没反应); 新建会议没自动写 attendee
|
||||||
|
- **P2 (3 项)**: 手写板 UI; item-icon; section-title 边框
|
||||||
|
- **P3 (6 项)**: 字段注释 + status 字段 + 索引 + 前端兜底 + 管理端点
|
||||||
|
|
||||||
|
**SQL 实测验证** (user_id=142 当前状态):
|
||||||
|
```
|
||||||
|
待签署会议: 3 个 (id=1,2,3 全部 handsign=NULL labor_protocol=NULL)
|
||||||
|
会议名: 整合医学学会项目评审会 / 基层医疗改革试点中期评估会 / 数字化医疗转型方案评审会
|
||||||
|
按 start_time 升序 ✓
|
||||||
|
```
|
||||||
@@ -0,0 +1,285 @@
|
|||||||
|
# /manager/accounts 页面端到端审查
|
||||||
|
|
||||||
|
**审查日期**: 2026-08-19
|
||||||
|
**审查范围**: 前端 → 后端 → DB → 原型
|
||||||
|
**审查者**: Claude Code (page-tech-review skill)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. 组件定位 (四跳链)
|
||||||
|
|
||||||
|
| 跳 | 命中 |
|
||||||
|
|---|---|
|
||||||
|
| ① 角色菜单 | `AdminLayout.vue:105` `MENU.manager` → `{ path: '/manager/accounts', title: '账号管理' }` |
|
||||||
|
| ② 路由 | `router/index.js:85` → `name: 'manager-accounts', component: () => import('@/views/manager/Accounts.vue')` |
|
||||||
|
| ③ 组件 | `ry-vue3/src/views/manager/Accounts.vue` |
|
||||||
|
| ④ 原型 | `proto/html/components/account-manage.html` (从 `proto/html/manager.html:213` `data-page="components/account-manage.html"` 链接) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 主要功能 + 可见性
|
||||||
|
|
||||||
|
### 1.1 主要功能
|
||||||
|
|
||||||
|
| 功能 | 前端入口 | 后端接口 | 数据表 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 查看个人信息 | `Accounts.vue:41-48` `loadProfile()` | `GET /system/user/profile` | `sys_user` |
|
||||||
|
| 修改姓名/手机号 | `Accounts.vue:57` `onSave` 第 1 个 request | `PUT /system/user/profile` | `sys_user` (nick_name, phonenumber, sex) |
|
||||||
|
| 修改密码 | `Accounts.vue:59` `onSave` 第 2 个 request | `PUT /system/user/profile/updatePwd` | `sys_user` (password, pwd_update_date) |
|
||||||
|
|
||||||
|
页面只支持 **编辑自己的资料**,**没有 CRUD 列表**——非典型管理页,本质是"个人中心"。
|
||||||
|
|
||||||
|
### 1.2 可见性 (三层过滤)
|
||||||
|
|
||||||
|
| 层 | 来源 | 校验字段 | 备注 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 前端菜单 | `AdminLayout.vue:95` `MENU.manager` | 仅 `manager` 角色可见 | 其他角色走 `/<role>/account` 路径 |
|
||||||
|
| 路由守卫 | `router/index.js:61` `meta: { role: 'manager' }` + `permission.js` | token 角色 | 通过则进 |
|
||||||
|
| 后端 | `SysProfileController.java` | 无 `role_type` 校验,任何已登录用户均可改自己的资料 | `currentUser = loginUser.getUser()` |
|
||||||
|
|
||||||
|
**不能看到本页面的角色**: admin / leader / doctor / executor / sponsor — 他们各自有 `/<role>/account` 路由(其中 admin/leader 走 `/leader/account`,doctor 走 `/doctor/account`,executor/sponsor 没有 account 页)。
|
||||||
|
|
||||||
|
**数据隔离**: 因为是"个人中心",后端用 `currentUser = loginUser.getUser()` 自动从 token 取本人 userId,前端无法传别人的 userId,所以天然隔离。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 筛选项 (filter-form)
|
||||||
|
|
||||||
|
**本页面无 filter-form**,只有一个 el-form 表单,所以筛选项章节不适用。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 工具栏按钮 (toolbar)
|
||||||
|
|
||||||
|
**本页面无 toolbar**,只有"保存 / 取消"两个表单按钮,详见第 5 章"表单内操作"。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 表格 (el-table)
|
||||||
|
|
||||||
|
**本页面无表格**,跳过。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 表单内操作按钮
|
||||||
|
|
||||||
|
### 5.1 字段映射
|
||||||
|
|
||||||
|
| UI label | 控件 | 绑定字段 | 后端 SQL | 命中表.字段 | 选项来源 | 原型对照 |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| 姓名 | `el-input` | `form.nickName` | `updateUser` `<if test="nickName != null and nickName != ''">nick_name = #{nickName},</if>` | `sys_user.nick_name` | — | ✅ 姓名 |
|
||||||
|
| 手机号 | `el-input` (maxlength=11) | `form.phonenumber` | `updateUser` `phonenumber = #{phonenumber},` + `checkPhoneUnique` | `sys_user.phonenumber` (varchar(11)) | — | ✅ 手机号 |
|
||||||
|
| 原密码 | `el-input` type=password | `form.oldPassword` | `updatePwd` 校验旧密码 (`SecurityUtils.matchesPassword`) | `sys_user.password` | — | ✅ 原密码 |
|
||||||
|
| 新密码 | `el-input` type=password | `form.newPassword` | `resetUserPwd` `password = #{password}, pwd_update_date = sysdate()` | `sys_user.password` / `pwd_update_date` | — | ✅ 新密码 |
|
||||||
|
| 确认密码 | `el-input` type=password | `form.confirmPassword` | 纯前端校验:必须等于新密码 | — | — | ✅ 确认密码 |
|
||||||
|
|
||||||
|
**Sex 隐式传**: `Accounts.vue:57` `data: { ..., sex: profile.value.sex }`,虽然 UI 没显示性别控件,但 sex 字段在 PUT 时跟着送。Mapper `<if test="sex != null and sex != ''">` 会原值回写(若 sex 是 "0/1/2")。**Sex 没被 UI 控制 = 用户改不了** (见 P2-4)。
|
||||||
|
|
||||||
|
### 5.2 表单按钮
|
||||||
|
|
||||||
|
| 按钮 | 触发函数 | 接口 | 后端动作 | 涉及表.字段 | 原型对照 |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| 保存 | `onSave` | PUT `/system/user/profile` (+ `/updatePwd` 如有填密码) | `SysProfileController.updateProfile` → `userService.updateUserProfile` → `userMapper.updateUser` | `sys_user.nick_name/phonenumber/sex` (+ `password/pwd_update_date`) | ✅ |
|
||||||
|
| 取消 | `onCancel` | — | 重置表单为初始 profile 快照 | — | ✅ |
|
||||||
|
|
||||||
|
### 5.3 真实落点说明
|
||||||
|
|
||||||
|
- **修改姓名/手机号/sex**: 落到 `sys_user.nick_name/phonenumber/sex`(`Accounts.vue:57`)。`userId` 来自 `loginUser` 而非表单(后端从 token 取 `LoginUser.getUser()`,`SysProfileController.java:55-74`),前端无法改 userId,无法越权改别人。
|
||||||
|
- **修改密码**: 落到 `sys_user.password`(`SysProfileController.java:115`)。新密码在 service 层 `SecurityUtils.encryptPassword()` 加密(BCrypt),不存在明文落库。
|
||||||
|
- **缓存同步**: 修改成功后 `tokenService.setLoginUser(loginUser)` 更新 Spring Security 缓存,下次请求拿到的 user 是新的(`SysProfileController.java:87, 120`)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Java Entity ↔ 数据库表 一致性
|
||||||
|
|
||||||
|
### 6.1 实测 sys_user DDL (2026-08-19)
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE `sys_user` (
|
||||||
|
`user_id` bigint NOT NULL AUTO_INCREMENT COMMENT '用户ID',
|
||||||
|
`dept_id` bigint DEFAULT NULL COMMENT '部门ID',
|
||||||
|
`user_name` varchar(30) NOT NULL COMMENT '用户账号',
|
||||||
|
`nick_name` varchar(30) NOT NULL COMMENT '用户昵称',
|
||||||
|
`user_type` varchar(2) DEFAULT '00' COMMENT '用户类型(00系统用户)',
|
||||||
|
`account_type` varchar(8) DEFAULT 'MAIN' COMMENT '账号类型 (MAIN=主账号/SUB=子账号)',
|
||||||
|
`parent_user_id` bigint DEFAULT NULL COMMENT '主账号ID (子账号关联)',
|
||||||
|
`role_type` varchar(20) DEFAULT 'executor',
|
||||||
|
`email` varchar(50) DEFAULT '' COMMENT '用户邮箱',
|
||||||
|
`phonenumber` varchar(11) DEFAULT '' COMMENT '手机号码',
|
||||||
|
`sex` char(1) DEFAULT '0' COMMENT '用户性别(0男 1女 2未知)',
|
||||||
|
`avatar` varchar(100) DEFAULT '' COMMENT '头像地址',
|
||||||
|
`password` varchar(100) DEFAULT '' COMMENT '密码',
|
||||||
|
`status` char(1) DEFAULT '0' COMMENT '账号状态(0正常 1停用)',
|
||||||
|
`del_flag` char(1) DEFAULT '0' COMMENT '删除标志(0代表存在 2代表删除)',
|
||||||
|
`login_ip` varchar(128) DEFAULT '' COMMENT '最后登录IP',
|
||||||
|
`login_date` datetime DEFAULT NULL COMMENT '最后登录时间',
|
||||||
|
`pwd_update_date` datetime DEFAULT NULL COMMENT '密码最后更新时间',
|
||||||
|
`create_by` varchar(64) DEFAULT '' COMMENT '创建者',
|
||||||
|
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
|
||||||
|
`update_by` varchar(64) DEFAULT '' COMMENT '更新者',
|
||||||
|
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
|
||||||
|
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
|
||||||
|
PRIMARY KEY (`user_id`),
|
||||||
|
KEY `idx_parent_user_id` (`parent_user_id`)
|
||||||
|
) ENGINE=InnoDB AUTO_INCREMENT=138 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户信息表';
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 SysUser Entity ↔ DB 字段对照
|
||||||
|
|
||||||
|
| 实体字段 | 中文列名 (DB COMMENT) | Java 类型 | 表字段 | DB 类型 | 一致? |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| `userId` | 用户ID | `Long` | `user_id` | `bigint NOT NULL AUTO_INCREMENT` | ✅ |
|
||||||
|
| `deptId` | 部门ID | `Long` | `dept_id` | `bigint DEFAULT NULL` | ✅ |
|
||||||
|
| `userName` | 用户账号 | `String` | `user_name` | `varchar(30) NOT NULL` | ✅ |
|
||||||
|
| `nickName` | 用户昵称 | `String` | `nick_name` | `varchar(30) NOT NULL` | ✅ |
|
||||||
|
| `userType` | 用户类型(00系统用户) | `String` | `user_type` | `varchar(2) DEFAULT '00'` | ✅ |
|
||||||
|
| `accountType` | 账号类型 (MAIN=主账号/SUB=子账号) | `String` | `account_type` | `varchar(8) DEFAULT 'MAIN'` | ✅ |
|
||||||
|
| `parentUserId` | 主账号ID (子账号关联) | `Long` | `parent_user_id` | `bigint DEFAULT NULL` | ✅ |
|
||||||
|
| `roleType` | ⚠️ 空注释 (DB 没 COMMENT) | `String` | `role_type` | `varchar(20) DEFAULT 'executor'` | ⚠️ 列一致,但 DB 注释缺失 |
|
||||||
|
| `email` | 用户邮箱 | `String` | `email` | `varchar(50) DEFAULT ''` | ✅ |
|
||||||
|
| `phonenumber` | 手机号码 | `String` | `phonenumber` | `varchar(11) DEFAULT ''` | ✅ (Java 注解 `@Size(max=11)`) |
|
||||||
|
| `sex` | 用户性别(0男 1女 2未知) | `String` | `sex` | `char(1) DEFAULT '0'` | ✅ |
|
||||||
|
| `avatar` | 头像地址 | `String` | `avatar` | `varchar(100) DEFAULT ''` | ✅ |
|
||||||
|
| `password` | 密码 | `String` | `password` | `varchar(100) DEFAULT ''` | ✅ |
|
||||||
|
| `status` | 账号状态(0正常 1停用) | `String` | `status` | `char(1) DEFAULT '0'` | ✅ |
|
||||||
|
| `delFlag` | 删除标志(0代表存在 2代表删除) | `String` | `del_flag` | `char(1) DEFAULT '0'` | ✅ |
|
||||||
|
| `loginIp` | 最后登录IP | `String` | `login_ip` | `varchar(128) DEFAULT ''` | ✅ |
|
||||||
|
| `loginDate` | 最后登录时间 | `Date` | `login_date` | `datetime DEFAULT NULL` | ✅ |
|
||||||
|
| `pwdUpdateDate` | 密码最后更新时间 | `Date` | `pwd_update_date` | `datetime DEFAULT NULL` | ✅ |
|
||||||
|
| `createBy` (BaseEntity) | 创建者 | `String` | `create_by` | `varchar(64) DEFAULT ''` | ✅ |
|
||||||
|
| `createTime` (BaseEntity) | 创建时间 | `Date` | `create_time` | `datetime DEFAULT NULL` | ✅ |
|
||||||
|
| `updateBy` (BaseEntity) | 更新者 | `String` | `update_by` | `varchar(64) DEFAULT ''` | ✅ |
|
||||||
|
| `updateTime` (BaseEntity) | 更新时间 | `Date` | `update_time` | `datetime DEFAULT NULL` | ✅ |
|
||||||
|
| `remark` (BaseEntity) | 备注 | `String` | `remark` | `varchar(500) DEFAULT NULL` | ✅ |
|
||||||
|
|
||||||
|
**无字段缺失**,**无类型不一致**。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 索引检查
|
||||||
|
|
||||||
|
### 7.1 sys_user 索引列表
|
||||||
|
|
||||||
|
| Key | 列 | 用途覆盖 |
|
||||||
|
|---|---|---|
|
||||||
|
| `PRIMARY` | `user_id` | ✅ 主键,所有 `WHERE user_id = ?` |
|
||||||
|
| `idx_parent_user_id` | `parent_user_id` | ✅ 子账号查询 `WHERE parent_user_id = ?` |
|
||||||
|
|
||||||
|
### 7.2 缺口分析
|
||||||
|
|
||||||
|
| 查询场景 | 涉及列 | 现有索引 | 评价 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 修改密码 (`resetUserPwd`) | `user_id` | PRIMARY | ✅ |
|
||||||
|
| 修改个人信息 (`updateUser`) | `user_id` | PRIMARY | ✅ |
|
||||||
|
| 手机号唯一性校验 (`checkPhoneUnique`) | `phonenumber` | ❌ 无索引 | ⚠️ 暂可接受,行数 ~138 走全表扫描无压力 |
|
||||||
|
| 邮箱唯一性校验 (`checkEmailUnique`) | `email` | ❌ 无索引 | ⚠️ 暂可接受 |
|
||||||
|
| 登录态 `WHERE user_name = ?` | `user_name` | ❌ 无索引 | ⚠️ Spring Security 框架一般会走 Redis/缓存,DB 仅冷启动用 |
|
||||||
|
|
||||||
|
**结论**: 数据量小(<200 行),**所有查询暂可接受**。若 `sys_user` 未来增长到 10k+,应给 `user_name` / `phonenumber` 加唯一索引。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 与原型差异
|
||||||
|
|
||||||
|
原型: `proto/html/components/account-manage.html`
|
||||||
|
实现: `ry-vue3/src/views/manager/Accounts.vue`
|
||||||
|
|
||||||
|
### 8.1 实现新增 (超出原型)
|
||||||
|
|
||||||
|
| 项 | 实现 | 原型 | 评价 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 表单校验规则 | ✅ `rules` 必填 + 手机号正则 `/^1[0-9]\d{9}$/` + 密码长度 6-20 + 两次密码一致 | ❌ 原型只放占位 | 改进,推荐保留 |
|
||||||
|
| 确认密码实时校验 | ✅ `validator` 函数 | ❌ 无 | 改进 |
|
||||||
|
| `maxlength="11"` 手机号限长 | ✅ (本次新增) | ❌ 原型不限 | 改进 |
|
||||||
|
|
||||||
|
### 8.2 原型有但实现缺失
|
||||||
|
|
||||||
|
| 项 | 原型 | 实现 | 严重度 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| "基本信息 / 修改密码" 分区标题 (`form-section-title`) | ✅ | ❌ 平铺 5 项 | P2 |
|
||||||
|
| page-title 风格 (h1 大标题 + 副标题) | ✅ `页面大标题` | ❌ 只用 breadcrumb | P3 (项目统一用 breadcrumb,可不改) |
|
||||||
|
| `form-actions` 顶部 border-top 分隔 | ✅ | ❌ 紧贴"确认密码" | P2 |
|
||||||
|
|
||||||
|
### 8.3 文字 / 标签差异
|
||||||
|
|
||||||
|
| 项 | 原型 | 实现 | 一致? |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 主标题 | "账号管理" | "首页 / 账号管理" (breadcrumb) | ⚠️ 项目规范用 breadcrumb,保持 |
|
||||||
|
| 副标题 | "修改个人信息、修改密码" | — | P3 |
|
||||||
|
| 密码提示语 | "*密码长度 6-20 位,支持数字、字母、特殊字符;留空表示不修改密码" | 同 | ✅ |
|
||||||
|
|
||||||
|
### 8.4 总结
|
||||||
|
|
||||||
|
**整体方向: 实现超出原型 (校验 + 限长)**,**分区分组缺失** (P2)。其他均与原型一致或为项目规范变体。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 字段冗余 / 设计问题
|
||||||
|
|
||||||
|
### 9.1 唯一约束语义
|
||||||
|
|
||||||
|
- `phonenumber` **无唯一索引**,但 `checkPhoneUnique` (mapper) 通过全表扫描 + `del_flag='0'` 过滤做软唯一。如果未来需要硬唯一 (例如登录用手机号),需加 `UNIQUE KEY uk_phonenumber (phonenumber)` 并处理历史重复数据。
|
||||||
|
|
||||||
|
### 9.2 JOIN vs 持久化字段
|
||||||
|
|
||||||
|
- 本表 `SysUser` 是基础表,所有字段都是持久化字段,无 JOIN 出来的视图字段 ✅
|
||||||
|
|
||||||
|
### 9.3 设计闭环
|
||||||
|
|
||||||
|
- `role_type` 列 DEFAULT `'executor'`,但本页面是 `manager` 角色,显然不可能 default 出 manager 角色 — 这是 [sys-user-role-type-truth](sys-user-role-type-truth.md) 内存里说的"sys_user.role_type 是真相源",新用户创建时需主动赋值。
|
||||||
|
- `account_type` / `parent_user_id` 用于主/子账号关系,本页面不涉及,跳过。
|
||||||
|
|
||||||
|
### 9.4 安全闭环
|
||||||
|
|
||||||
|
- 手机号修改**无短信验证码校验**:`SysProfileController.updateProfile` 直接 `setPhonenumber`,对比 `changePhone` 接口走 `smsService.verifyCode`,**两条路径并存**:
|
||||||
|
- 本页 `Accounts.vue:57` → 走 updateProfile → **绕过短信**
|
||||||
|
- 假如有"换绑手机号"流程 → 走 changePhone → 走短信
|
||||||
|
- 这是 RuoYi 框架默认行为(若系统部署方想强制短信,需要在 updateProfile 里也加校验)。属于**已知设计,非 bug**。
|
||||||
|
|
||||||
|
### 9.5 表格冗余
|
||||||
|
|
||||||
|
- 本页面无表,跳过。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 待修复列表
|
||||||
|
|
||||||
|
| # | 问题 | 文件 | 修复建议 | 严重度 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| 1 | 表单无分区("基本信息 / 修改密码") | `Accounts.vue` | 用 `el-divider` 或自定义 `form-section-title` 视觉分组,与原型一致 | P2 |
|
||||||
|
| 2 | 保存按钮无顶部分隔线,与上方密码字段视觉粘连 | `Accounts.vue` | 给 `el-form-item` 按钮组加 `border-top: 1px solid #f0f0f0` + `padding-top: 16px` | P2 |
|
||||||
|
| 3 | `sex` 字段 UI 没暴露,但 `onSave` 静默带上 `profile.value.sex` | `Accounts.vue:57` | 若 sex 不可改,前端不要传;若可改,加 radio 控件(0男/1女/2未知) | P2 |
|
||||||
|
| 4 | `email` UI 没暴露,`SysProfileController.updateProfile` 接受 email 入参但前端不传 | `Accounts.vue` + `SysProfileController` | 若产品定位不许改邮箱,前端显式删除 email 入参;若许改,UI 加输入框 | P2 |
|
||||||
|
| 5 | 手机号修改无短信校验 (`updateProfile` 不走 `smsService.verifyCode`,仅 `changePhone` 走) | `SysProfileController.java:67-91` | 与产品确认:个人中心改手机号是否需要短信?需要则复用 `changePhone` 逻辑 | P1 |
|
||||||
|
| 6 | `role_type` 列 DB 注释缺失,只有 `account_type` 有注释 | DB `sys_user.role_type` | `ALTER TABLE sys_user MODIFY COLUMN role_type varchar(20) DEFAULT 'executor' COMMENT '业务角色 (admin/leader/manager/doctor/executor/sponsor)'` | P3 |
|
||||||
|
| 7 | `phonenumber` 无唯一索引,行数大后 `checkPhoneUnique` 全表扫描 | `sys_user` | 加 `UNIQUE KEY uk_phonenumber (phonenumber)` (需先排查历史重复) | P3 |
|
||||||
|
| 8 | 用户名/手机号正则过宽 (`/^1[0-9]\d{9}$/` 接受第二位为 0/1/2) | `Accounts.vue:30` | 收紧为 `/^1[3-9]\d{9}$/` (大陆手机号第二位规范) | P3 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. 引用清单
|
||||||
|
|
||||||
|
| 文件 | 行号 | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| `ry-vue3/src/views/manager/Accounts.vue` | 全文 | 审查目标 |
|
||||||
|
| `ry-vue3/src/views/manager/Accounts.vue:7` | — | 手机号 input maxlength=11 (本次新增) |
|
||||||
|
| `ry-vue3/src/views/manager/Accounts.vue:30` | — | 手机号正则 `/^1[0-9]\d{9}$/` |
|
||||||
|
| `ry-vue3/src/views/manager/Accounts.vue:57` | — | 保存 profile (PUT /system/user/profile) |
|
||||||
|
| `ry-vue3/src/views/manager/Accounts.vue:59` | — | 保存密码 (PUT /system/user/profile/updatePwd) |
|
||||||
|
| `ry-vue3/src/layout/AdminLayout.vue` | 95-106 | `MENU.manager` 数组,`/manager/accounts` 在第 10 项 |
|
||||||
|
| `ry-vue3/src/router/index.js` | 61, 85 | 路由表 `/manager` 父路由 + `manager-accounts` 子路由 |
|
||||||
|
| `proto/html/manager.html` | 213 | 原型菜单链接 `data-page="components/account-manage.html"` |
|
||||||
|
| `proto/html/components/account-manage.html` | 全文 | 原型基准 |
|
||||||
|
| `ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysProfileController.java` | 37-215 | 后端 controller 全部 5 个接口 |
|
||||||
|
| `ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysProfileController.java:67-91` | — | `updateProfile` (无短信校验的手机号更新) |
|
||||||
|
| `ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysProfileController.java:97-124` | — | `updatePwd` (旧密码校验 + 加密 + 缓存同步) |
|
||||||
|
| `ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysProfileController.java:131-162` | — | `changePhone` (带短信校验,本页面未使用) |
|
||||||
|
| `ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysUser.java` | 21-317 | SysUser 实体 |
|
||||||
|
| `ry-api/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysUser.java:149` | — | `@Size(min=0, max=11) phonenumber` |
|
||||||
|
| `ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysUserServiceImpl.java` | 204-214 | `checkPhoneUnique` 实现 |
|
||||||
|
| `ry-api/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysUserServiceImpl.java` | 377-381 | `updateUserProfile` → 调 `userMapper.updateUser` |
|
||||||
|
| `ry-api/ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml` | 183-185 | `checkPhoneUnique` SQL |
|
||||||
|
| `ry-api/ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml` | 231-251 | `updateUser` SQL (用 `<set><if>` 条件更新) |
|
||||||
|
| `ry-api/ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml` | 269-271 | `resetUserPwd` SQL (改 password + pwd_update_date) |
|
||||||
|
| `ry-api/ruoyi-admin/src/main/resources/application-druid.yml` | 10 | DB 连接信息 (password: cu2oh2co3) |
|
||||||
|
| MySQL `guoju0808.sys_user` | — | 24 列 + 1 主键 + 1 副索引,实测 2026-08-19 |
|
||||||
@@ -0,0 +1,379 @@
|
|||||||
|
# /publicity/13 页面端到端审查
|
||||||
|
|
||||||
|
**审查日期**: 2026-08-19
|
||||||
|
**审查范围**: 前端 → 后端 → DB → 原型
|
||||||
|
**目标页面**: `/publicity/13` (公示详情页)
|
||||||
|
**审查者**: Claude Code (page-tech-review skill)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. 组件定位 (四跳链)
|
||||||
|
|
||||||
|
| 跳 | 命中 |
|
||||||
|
|---|---|
|
||||||
|
| ① 入口 | `proto/html/项目公示.html:697` `window.location.href = '公示详情.html'` — 公示列表点 item 跳详情 |
|
||||||
|
| ② 路由 | `router/index.js:10` → `{ path: 'publicity/:projectId', name: 'publicity-detail', component: () => import('@/views/portal/PublicityDetail.vue') }` |
|
||||||
|
| ③ 组件 | `ry-vue3/src/views/portal/PublicityDetail.vue` |
|
||||||
|
| ④ 原型 | `proto/html/公示详情.html` (跟实现 1 对 1, 文件名 `公示详情.html` 直接被项目公示列表引用) |
|
||||||
|
|
||||||
|
**说明**: `/publicity/13` 的 `13` 是 `biz_project.project_id`,实测 DB 中 `project_id=13` 的记录是测试数据 (`project_no='3', project_name='2'`,`invitation_url` 有一个 OSS PDF,`support_letter_url` NULL)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 主要功能 + 可见性
|
||||||
|
|
||||||
|
### 1.1 主要功能
|
||||||
|
|
||||||
|
| 功能 | 前端入口 | 后端接口 | 数据表 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 查看公示详情 | `PublicityDetail.vue:355` `load()` | `GET /business/public/project/{projectId}` | `biz_project` |
|
||||||
|
| 立即报名 | `PublicityDetail.vue:398-428` `onSignup()` | `POST /business/executionIntent/signup` | `biz_execution_intent` (RuoYi) |
|
||||||
|
| 表达支持意向 (已登录快速通道) | `PublicityDetail.vue:468-478` `onSupportIntent()` | `POST /business/publicity/supportIntent` | `biz_publicity_support_intent` |
|
||||||
|
| 表达执行意向 (已登录快速通道) | `PublicityDetail.vue:480-490` `onExecutionIntent()` | `POST /business/publicity/executionIntent` | `biz_publicity_execution_intent` |
|
||||||
|
| 表达意向 (匿名 dialog) | `PublicityDetail.vue:195-216` `guestDialog` + `onGuestDialogConfirm()` | 同上 (共用 2 个 POST) | 同上 |
|
||||||
|
| 查重"是否已提交" | `PublicityDetail.vue:437-448` `checkIntentSubmitted()` | `GET /business/publicity/hasIntent?projectId=&phone=&type=` | `biz_publicity_support_intent` / `biz_publicity_execution_intent` |
|
||||||
|
| 分享二维码 | `PublicityDetail.vue:249-263` `QRCode.toDataURL` | — (前端纯计算) | — |
|
||||||
|
|
||||||
|
页面不是 CRUD 列表,而是 **单页 detail + 4 个交互按钮**。
|
||||||
|
|
||||||
|
### 1.2 可见性 (三层过滤)
|
||||||
|
|
||||||
|
| 层 | 来源 | 校验字段 | 备注 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 前端路由 | `router/index.js:10` | 无 meta.role,所有人均可访问 | `/publicity/:projectId` |
|
||||||
|
| 前端菜单 | — (门户页无菜单) | — | 公示页无菜单,靠顶部导航"项目公示"链入 |
|
||||||
|
| 后端 | `SecurityConfig.java:64` | `.requestMatchers("/business/public/**", "/business/publicity/**", "/business/auth/**", "/business/sms/**").permitAll()` | 公开端点已放行匿名访问,登录后走 token 校验身份 |
|
||||||
|
|
||||||
|
**不能看到本页面的角色**: 不存在角色隔离——**任何用户/访客**都能看公示详情 + 提交意向。
|
||||||
|
|
||||||
|
**报名按钮登录校验**: `PublicityDetail.vue:399-403` 前端拦截 `!loggedIn` → 跳转 `/login`。后端 `executionIntent/signup` 应也有 token 校验 (未实测,从项目其他 executionIntent 接口推断)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 筛选项 (filter-form)
|
||||||
|
|
||||||
|
**本页面无 filter-form**,跳过。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 工具栏按钮 (toolbar)
|
||||||
|
|
||||||
|
**本页面无传统 toolbar**,操作按钮挂在右侧悬浮条 (`.detail-actions`),见第 5 章。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 表格 (el-table)
|
||||||
|
|
||||||
|
**本页面无表格**,跳过。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. UI 元素映射 (本页面是 detail 视图)
|
||||||
|
|
||||||
|
### 5.1 左侧 tab 列表 (`ann-menu`)
|
||||||
|
|
||||||
|
| UI label | 绑定字段 (entity) | 后端 SQL | 命中表.字段 | 选项来源 | 原型对照 |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| 邀请函 | `ann.invitationUrl` | `BizProjectMapper.xml:41` `<result property="invitationUrl" column="invitation_url"/>` | `biz_project.invitation_url` | — | ❌ 原型只有 1 张图 |
|
||||||
|
| 支持函 | `ann.supportLetterUrl` | 同上 `<result property="supportLetterUrl" column="support_letter_url"/>` | `biz_project.support_letter_url` | — | ❌ 原型没有 |
|
||||||
|
| 通知 | `ann.noticeUrl` | ⚠️ **DB 无该列** | ❌ 永远 undefined | — | ❌ 原型没有 |
|
||||||
|
| 日程 | `ann.scheduleUrl` | ⚠️ **DB 无该列** | ❌ 永远 undefined | — | ❌ 原型没有 |
|
||||||
|
|
||||||
|
**前端 tab 过滤逻辑** (`PublicityDetail.vue:286`): `TAB_DEFS.filter(t => p[t.urlKey])` — URL 为 undefined/null 即隐藏 tab。所以**通知 / 日程 2 个 tab 永远不会出现**(除非 DB 加列 + mapper 加 mapping)。
|
||||||
|
|
||||||
|
### 5.2 右侧悬浮按钮
|
||||||
|
|
||||||
|
| 按钮 | 触发函数 | 接口 | 后端动作 | 涉及表.字段 | 原型对照 |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| 立即报名 | `onSignup` | `POST /business/executionIntent/signup` | `executionIntentService.signup` (推断) | `biz_execution_intent` (RuoYi) | ✅ 原型也有"立即报名" |
|
||||||
|
| 表达支持意向 | `onSupportIntent` → `doSubmitIntent('support', fields)` | `POST /business/publicity/supportIntent` | `BizPublicityIntentController.doSubmit` (66-139 行) | `biz_publicity_support_intent` 全字段 | ❌ 原型没有 |
|
||||||
|
| 表达执行意向 | `onExecutionIntent` → `doSubmitIntent('execution', fields)` | `POST /business/publicity/executionIntent` | 同上分支 | `biz_publicity_execution_intent` 全字段 | ❌ 原型没有 |
|
||||||
|
| 分享二维码 | `showQr = true` | — (前端 QRCode.toDataURL) | — | — | ✅ 原型也有"分享二维码" |
|
||||||
|
|
||||||
|
### 5.3 匿名 dialog (`guestDialog`)
|
||||||
|
|
||||||
|
| 字段 | 控件 | 绑定字段 | 后端字段 | 选项来源 | 原型对照 |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| 姓名 | `el-input` | `guestDialog.form.name` | `name` | — | ❌ 原型没有 dialog |
|
||||||
|
| 手机号 | `el-input` (maxlength=11) | `guestDialog.form.phone` | `phone` | — | ❌ 原型没有 dialog |
|
||||||
|
| 工作单位 | `el-input` | `guestDialog.form.workUnit` | `work_unit` | — | ❌ 原型没有 dialog |
|
||||||
|
| 部门 | `el-input` | `guestDialog.form.department` | `department` (可空) | — | ❌ 原型没有 dialog |
|
||||||
|
| 职务 | `el-input` | `guestDialog.form.position` | `position` (可空) | — | ❌ 原型没有 dialog |
|
||||||
|
|
||||||
|
**重要: 提交时**,`doSubmitIntent` 把 `guestDialog.form` 直接传 `payload = { projectId, ...fields }` (`PublicityDetail.vue:524`),**不含 sex / email**。后端 controller (`BizPublicityIntentController.java:79-82`) 校验 projectId/name/phone/workUnit 必填,**不校验 sex/email** — 与设计一致。
|
||||||
|
|
||||||
|
### 5.4 真实落点
|
||||||
|
|
||||||
|
- **匿名意向 → 落 `biz_publicity_*_intent` 表**: 即使未登录也写表,后端 `BizPublicityIntentController.java:106-115` 通过 `sysUserMapper.checkPhoneUnique(phone)` 反查 user_id 自动回填,实现"账号自动关联"。
|
||||||
|
- **已登录意向 → 同表**: 直接 `setUserId(SecurityUtils.getUserId())`。
|
||||||
|
- **报名 → 落 `biz_execution_intent`** (RuoYi 旧表,非 publicity 两表): 这是旧业务,**与公示匿名意向表语义不同**,报名走 login-bound 表,意向走 public 快照表。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Java Entity ↔ 数据库表 一致性
|
||||||
|
|
||||||
|
### 6.1 biz_project (实测 2026-08-19)
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE `biz_project` (
|
||||||
|
`project_id` bigint NOT NULL AUTO_INCREMENT,
|
||||||
|
`project_no` varchar(50) NOT NULL,
|
||||||
|
`project_name` varchar(200) NOT NULL,
|
||||||
|
`project_form` varchar(20),
|
||||||
|
`total_sessions` int DEFAULT 0,
|
||||||
|
`done_sessions` int DEFAULT 0,
|
||||||
|
`todo_sessions` int DEFAULT 0,
|
||||||
|
`total_amount` decimal(15,2) DEFAULT 0,
|
||||||
|
`available_amount` decimal(15,2) DEFAULT 0,
|
||||||
|
`paid_labor_amount` decimal(15,2) DEFAULT 0,
|
||||||
|
`paid_meeting_amount` decimal(15,2) DEFAULT 0,
|
||||||
|
`manage_fee` decimal(15,2) DEFAULT 0,
|
||||||
|
`is_finished` char(1) DEFAULT '0',
|
||||||
|
`is_settled` char(1) DEFAULT 'N',
|
||||||
|
`manager_score` decimal(3,1),
|
||||||
|
`sponsor_admin_user_id` bigint,
|
||||||
|
`sponsor_admin_user_name` varchar(200),
|
||||||
|
`lead_user_id` bigint,
|
||||||
|
`is_bid_project` char(1) DEFAULT 'N',
|
||||||
|
`start_time` datetime,
|
||||||
|
`end_time` datetime,
|
||||||
|
`submit_deadline_days` int DEFAULT 0,
|
||||||
|
`support_contract_url` varchar(500),
|
||||||
|
`execute_contract_url` varchar(500),
|
||||||
|
`invitation_url` varchar(500),
|
||||||
|
`support_letter_url` varchar(500),
|
||||||
|
`publish_url` varchar(500),
|
||||||
|
`create_by` varchar(64),
|
||||||
|
`create_user_id` bigint,
|
||||||
|
`create_time` datetime,
|
||||||
|
`update_by` varchar(64),
|
||||||
|
`update_time` datetime,
|
||||||
|
`sponsor_score` decimal(3,1),
|
||||||
|
PRIMARY KEY (`project_id`),
|
||||||
|
UNIQUE KEY `uk_project_no` (`project_no`),
|
||||||
|
KEY `idx_project_form` (`project_form`),
|
||||||
|
KEY `idx_is_finished` (`is_finished`),
|
||||||
|
KEY `idx_is_settled` (`is_settled`),
|
||||||
|
KEY `idx_sponsor_admin_user_id` (`sponsor_admin_user_id`),
|
||||||
|
KEY `idx_start_end_time` (`start_time`,`end_time`)
|
||||||
|
) ENGINE=InnoDB AUTO_INCREMENT=14;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 biz_publicity_support_intent + biz_publicity_execution_intent (同构)
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE `biz_publicity_support_intent` (
|
||||||
|
`intent_id` bigint NOT NULL AUTO_INCREMENT,
|
||||||
|
`project_id` bigint,
|
||||||
|
`project_no` varchar(64),
|
||||||
|
`project_name` varchar(255),
|
||||||
|
`user_id` bigint,
|
||||||
|
`name` varchar(50),
|
||||||
|
`phone` varchar(20),
|
||||||
|
`work_unit` varchar(200),
|
||||||
|
`department` varchar(100),
|
||||||
|
`position` varchar(100),
|
||||||
|
`source` varchar(32) DEFAULT 'publicity',
|
||||||
|
`intent_status` varchar(32) DEFAULT '待审核',
|
||||||
|
`remark` varchar(500),
|
||||||
|
`create_by` varchar(64),
|
||||||
|
`create_time` datetime,
|
||||||
|
`update_by` varchar(64),
|
||||||
|
`update_time` datetime,
|
||||||
|
PRIMARY KEY (`intent_id`),
|
||||||
|
KEY `idx_project_phone` (`project_id`,`phone`,`source`),
|
||||||
|
KEY `idx_user_id` (`user_id`)
|
||||||
|
) ENGINE=InnoDB AUTO_INCREMENT=2;
|
||||||
|
|
||||||
|
-- biz_publicity_execution_intent 结构完全一致
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.3 Entity ↔ DB 字段对照 (biz_publicity_support_intent)
|
||||||
|
|
||||||
|
| 实体字段 | 中文列名 (DB COMMENT) | Java 类型 | 表字段 | DB 类型 | 一致? |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| `intentId` | 意向ID | `Long` | `intent_id` | `bigint AUTO_INCREMENT` | ✅ |
|
||||||
|
| `projectId` | 项目ID | `Long` | `project_id` | `bigint` | ✅ |
|
||||||
|
| `projectNo` | 项目编号(冗余) | `String` | `project_no` | `varchar(64)` | ✅ |
|
||||||
|
| `projectName` | 项目名称(冗余) | `String` | `project_name` | `varchar(255)` | ✅ |
|
||||||
|
| `userId` | 用户ID(按phone关联) | `Long` | `user_id` | `bigint` | ✅ |
|
||||||
|
| `name` | 姓名 | `String` | `name` | `varchar(50)` | ✅ |
|
||||||
|
| `phone` | 手机号 | `String` | `phone` | `varchar(20)` | ✅ |
|
||||||
|
| `workUnit` | 工作单位 | `String` | `work_unit` | `varchar(200)` | ✅ |
|
||||||
|
| `department` | 部门 | `String` | `department` | `varchar(100)` | ✅ |
|
||||||
|
| `position` | 职务 | `String` | `position` | `varchar(100)` | ✅ |
|
||||||
|
| `source` | 来源 publicity=公示页 | `String` | `source` | `varchar(32) DEFAULT 'publicity'` | ✅ |
|
||||||
|
| `intentStatus` | 状态 待审核/已通过/已拒绝 | `String` | `intent_status` | `varchar(32) DEFAULT '待审核'` | ✅ (DB 注释用"已通过",entity 注释用"已采纳",**注释不一致但代码用"已通过"**) |
|
||||||
|
| `remark` | 备注 | `String` | `remark` | `varchar(500)` | ✅ |
|
||||||
|
| `createBy` (BaseEntity) | 创建者 | `String` | `create_by` | `varchar(64)` | ✅ |
|
||||||
|
| `createTime` | 创建时间 | `Date` | `create_time` | `datetime` | ✅ |
|
||||||
|
| `updateBy` (BaseEntity) | 更新者 | `String` | `update_by` | `varchar(64)` | ✅ |
|
||||||
|
| `updateTime` | 更新时间 | `Date` | `update_time` | `datetime` | ✅ |
|
||||||
|
|
||||||
|
**BizPublicityExecutionIntent 同构**。
|
||||||
|
|
||||||
|
### 6.4 ⚠️ **biz_project Entity ↔ DB 一处不一致 (P0 级)**
|
||||||
|
|
||||||
|
`BizProject.java:119, 121` 有 `private String noticeUrl;` 和 `private String scheduleUrl;` 两个字段,且 mapper XML **未定义** `<result>` 映射这两个字段 (`BizProjectMapper.xml:40-43` 只 mapping 到 publishUrl):
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<result property="invitationUrl" column="invitation_url" />
|
||||||
|
<result property="supportLetterUrl" column="support_letter_url" />
|
||||||
|
<result property="publishUrl" column="publish_url" />
|
||||||
|
<!-- 没有 noticeUrl / scheduleUrl 的 <result> -->
|
||||||
|
```
|
||||||
|
|
||||||
|
**结论**:
|
||||||
|
1. `SELECT` 不会填这 2 个字段,前端拿到 `undefined`,tab 自动隐藏 (`PublicityDetail.vue:286` 过滤) — **不崩,但功能缺失**
|
||||||
|
2. `INSERT / UPDATE` 也不会写这 2 个字段 (`BizProjectMapper.xml:222-277` 没有 `<if>` 块),即使管理员在 UI 上填了值也写不进 DB
|
||||||
|
3. DB `biz_project` 实测**没有 `notice_url` / `schedule_url` 这 2 列**
|
||||||
|
|
||||||
|
**前端 PublicityDetail.vue `TAB_DEFS`** (`PublicityDetail.vue:270-275`) 期望 `ann.noticeUrl` / `ann.scheduleUrl` 字段 — **永远为 undefined**,2 个 tab 永久隐藏。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 索引检查
|
||||||
|
|
||||||
|
### 7.1 biz_project 索引
|
||||||
|
|
||||||
|
| Key | 列 | 用途覆盖 |
|
||||||
|
|---|---|---|
|
||||||
|
| PRIMARY | `project_id` | ✅ `WHERE project_id = ?` (本页面 `getById(13)`) |
|
||||||
|
| uk_project_no | `project_no` (UNIQUE) | ✅ 项目编号唯一性 |
|
||||||
|
| idx_project_form | `project_form` | ✅ 项目形式筛选 |
|
||||||
|
| idx_is_finished | `is_finished` | ✅ 已结题筛选 |
|
||||||
|
| idx_is_settled | `is_settled` | ✅ 已结算筛选 |
|
||||||
|
| idx_sponsor_admin_user_id | `sponsor_admin_user_id` | ✅ 赞助方负责人反查 |
|
||||||
|
| idx_start_end_time | `start_time`,`end_time` | ✅ 时间段查询 |
|
||||||
|
|
||||||
|
**评价**: `getById(13)` 走 PRIMARY,✅。其它筛选用 idx 系列列,✅。
|
||||||
|
|
||||||
|
### 7.2 biz_publicity_support_intent / execution_intent 索引
|
||||||
|
|
||||||
|
| Key | 列 | 用途覆盖 |
|
||||||
|
|---|---|---|
|
||||||
|
| PRIMARY | `intent_id` | ✅ |
|
||||||
|
| idx_project_phone | `project_id`,`phone`,`source` (复合) | ✅ `hasIntent(projectId, phone, 'publicity')` 完全命中 (controller:147-159) |
|
||||||
|
| idx_user_id | `user_id` | ✅ 已登录用户反查 |
|
||||||
|
|
||||||
|
**评价**: 复合索引 `(project_id, phone, source)` 完美覆盖 controller 的查重 SQL,✅。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 与原型差异
|
||||||
|
|
||||||
|
原型: `proto/html/公示详情.html`
|
||||||
|
实现: `ry-vue3/src/views/portal/PublicityDetail.vue`
|
||||||
|
|
||||||
|
### 8.1 实现新增 (超出原型)
|
||||||
|
|
||||||
|
| 项 | 实现 | 原型 | 评价 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 4 个 tab (邀请函/支持函/通知/日程) | ✅ | ❌ 原型只有 1 张图 | 改进 |
|
||||||
|
| 表达支持意向按钮 + 匿名 dialog | ✅ | ❌ 原型没有 | 改进 (与 [publicity-intent-design](publicity-intent-design.md) 记忆一致) |
|
||||||
|
| 表达执行意向按钮 | ✅ | ❌ 原型没有 | 改进 |
|
||||||
|
| 报名按钮已有"已报名"灰显状态 | ✅ | ❌ 原型跳登录 | 改进 |
|
||||||
|
| 顶部 nav 用户态 (登录/头像下拉) | ✅ | ❌ 原型只有"登录"链接 | 改进 |
|
||||||
|
| 加载骨架屏 | ✅ (`main-skeleton` 4 个 tab skeleton) | ❌ | 改进 |
|
||||||
|
| OSS PDF 走 `/common/oss/proxy` (inline 渲染) | ✅ (`proxyUrl` 309-316) | — | **必须** — OSS bucket 设了 attachment 否则 iframe 强制下载 |
|
||||||
|
|
||||||
|
### 8.2 原型有但实现缺失
|
||||||
|
|
||||||
|
| 项 | 原型 | 实现 | 严重度 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 静态大图 (邀请函红头文件 letter 样式 + 公章) | ✅ `.letter` + `.seal` (200-340 行) | ⚠️ 仅渲染原始 PDF/图片,无 letter 容器包装 | P2 (视觉降级,但功能等价) |
|
||||||
|
|
||||||
|
### 8.3 文字 / 标签差异
|
||||||
|
|
||||||
|
| 项 | 原型 | 实现 | 一致? |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 主按钮文字 | "立即报名" | "立即报名" | ✅ |
|
||||||
|
| 二级按钮 | "分享二维码" | "分享二维码" | ✅ |
|
||||||
|
| 顶部 nav 高亮 | "项目公示" | "项目公示" | ✅ |
|
||||||
|
| 主标题 | 无 (`<article class="letter">` 自身有标题) | 无 (用面包屑) | ✅ |
|
||||||
|
|
||||||
|
### 8.4 总结
|
||||||
|
|
||||||
|
**整体方向: 实现远超原型** (4 tab + 2 新功能按钮 + 骨架屏 + 用户态 + OSS 代理)。原型是 v1 静态图,实现是 v2 完整交互页,改进方向正确。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 字段冗余 / 设计问题
|
||||||
|
|
||||||
|
### 9.1 biz_publicity_support_intent / execution_intent
|
||||||
|
|
||||||
|
- **冗余字段**: `project_no`, `project_name`, `user_id` 都是 `biz_project` JOIN 出来的快照字段 — **合理** (匿名提交时项目名/编号可能改名,留快照保证审计追溯)。
|
||||||
|
- **唯一约束**: `(project_id, phone, source)` 通过 idx_project_phone 复合索引实现查重。**未加 UNIQUE 约束**,纯靠 service 层 `findDuplicate` — 行数大后并发提交可能绕过 (race condition),但匿名提交频率低,暂可接受。
|
||||||
|
|
||||||
|
### 9.2 biz_project 通知/日程字段
|
||||||
|
|
||||||
|
- `BizProject.java` 有 `noticeUrl` / `scheduleUrl` 但 DB 没列,mapper 没 mapping → **死代码**。建议二选一:
|
||||||
|
- A. 加 `ALTER TABLE biz_project ADD COLUMN notice_url varchar(500), ADD COLUMN schedule_url varchar(500)` + mapper 补 `<result>` 和 `<if>` 块
|
||||||
|
- B. 删 entity 这 2 字段 + 前端 TAB_DEFS 删通知/日程
|
||||||
|
|
||||||
|
### 9.3 报名 vs 意向两套表语义重叠
|
||||||
|
|
||||||
|
- `biz_execution_intent` (RuoYi 旧表,报名按钮) + `biz_publicity_execution_intent` (公示匿名意向) — 都是"对项目表达执行意向"
|
||||||
|
- `BizSupportIntentController.java` (RuoYi 旧) + `BizPublicityIntentController.java` (新) — 两套 controller
|
||||||
|
- 旧表语义: "已登录 + 报名+ 关联 bid" — 强约束
|
||||||
|
- 新表语义: "匿名/已登录 + 仅意向快照 + 不进入执行流程" — 弱约束
|
||||||
|
- **两表并存是设计意图** ([publicity-intent-design](publicity-intent-design.md) 记忆:不复用登录视角的强绑表) — 合理,不冗余
|
||||||
|
|
||||||
|
### 9.4 业务闭环
|
||||||
|
|
||||||
|
- 匿名提交 → 反查 sys_user → 自动回填 user_id (`BizPublicityIntentController.java:106-115`) — 闭环 ✅
|
||||||
|
- 已登录但未维护手机号 → userId 仍能从 SecurityContext 取 (`SecurityUtils.getUserId()`) — 闭环 ✅
|
||||||
|
- phone 不匹配任何 sys_user → user_id 仍 NULL → manager 端列表"账号状态=不存在" 标签 (见 manager/SupportIntent.vue) — 闭环 ✅
|
||||||
|
|
||||||
|
### 9.5 数据隔离
|
||||||
|
|
||||||
|
- 公开端点 `/business/publicity/**` permitAll (匿名可访问)
|
||||||
|
- 管理端点 `/business/publicitySupportIntent/**` 受 token 保护 (`SecurityConfig.java` 无显式 permitAll)
|
||||||
|
- **匿名提交的数据隔离靠 source='publicity' 字段** — manager 端 list 自动按 source 过滤 (推断,需进一步看 mapper XML 验证)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 待修复列表
|
||||||
|
|
||||||
|
| # | 问题 | 文件 | 修复建议 | 严重度 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| 1 | `BizProject.noticeUrl` / `scheduleUrl` 字段无 DB 列、无 mapper mapping,**死代码** | `BizProject.java:119,121` + `BizProjectMapper.xml` + `PublicityDetail.vue:270-275` | 二选一:A) `ALTER TABLE biz_project ADD notice_url/schedule_url` + mapper 补 mapping;B) 删 entity 字段 + 前端 TAB_DEFS 删 通知/日程 | P0 |
|
||||||
|
| 2 | `intent_status` 中文注释 entity 写"已采纳",DB 写"已通过",代码实际用"已通过" (`changeStatus(row, '已通过')` SupportIntent.vue:57) | `BizPublicitySupportIntent.java:48` | 改 entity 注释为"已通过" | P3 |
|
||||||
|
| 3 | 邀请函原文渲染: 原型 letter 容器样式 + 公章位置 (`.letter` + `.seal`) 未实现,直接渲染 PDF/图片丢视觉 | `PublicityDetail.vue:73-90` (content 容器) | 加 letter 容器样式 + 公章绝对定位 (与 manager/Publicity 项目详情一致) | P2 |
|
||||||
|
| 4 | 匿名提交**无反爬限制** (`/business/publicity/supportIntent` permitAll),同 IP 可无限提交不同 phone | `BizPublicityIntentController.java:50-54` | 加 rate-limit (按 IP/phone/项目 5 分钟内 1 次) | P2 |
|
||||||
|
| 5 | `findDuplicate` 复合唯一性靠 service 层 SELECT,无 DB UNIQUE KEY — 高并发下可能 race condition 重复插入 | `biz_publicity_support_intent` 表 | 加 `UNIQUE KEY uk_proj_phone_source (project_id, phone, source)` | P3 (匿名低频,暂可接受) |
|
||||||
|
| 6 | 前端 tab 默认 active 取"第一个有 URL 的 tab",如果 4 个 tab 都没 URL (全部 null),`activeTab` 仍可能是空字符串 → 用户看不到任何内容 | `PublicityDetail.vue:357-361` | 加兜底:全无 URL 时显示"暂无可查看文件"占位 | P2 |
|
||||||
|
| 7 | 公示页 iframe 加载 OSS PDF 时,**没有 loading 指示器** — 用户看到白屏几秒 | `PublicityDetail.vue:74-78` | 给 iframe 加 `@load` 隐藏骨架屏,或 PDF.js 内嵌渲染 | P3 |
|
||||||
|
| 8 | "已支持" / "已表达意向" 状态依赖前端 `checkIntentSubmitted()` 用本地 phone 查重,**清缓存后状态丢失**(重新查接口,但有 200ms 延迟用户看到 "支持" 闪烁) | `PublicityDetail.vue:431-453` | 用 `sessionStorage` 缓存"已提交"标志,onMounted 时优先读缓存再异步验证 | P3 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. 引用清单
|
||||||
|
|
||||||
|
| 文件 | 行号 | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| `ry-vue3/src/views/portal/PublicityDetail.vue` | 全文 | 审查目标 |
|
||||||
|
| `ry-vue3/src/views/portal/PublicityDetail.vue:195-216` | — | 匿名意向 dialog (本次已加 guest-intent-form 样式) |
|
||||||
|
| `ry-vue3/src/views/portal/PublicityDetail.vue:270-275` | — | TAB_DEFS (4 tab 定义) |
|
||||||
|
| `ry-vue3/src/views/portal/PublicityDetail.vue:309-316` | — | `proxyUrl` OSS 代理 |
|
||||||
|
| `ry-vue3/src/views/portal/PublicityDetail.vue:355-370` | — | `load()` 调 GET /business/public/project/{projectId} |
|
||||||
|
| `ry-vue3/src/views/portal/PublicityDetail.vue:398-428` | — | `onSignup()` 立即报名 |
|
||||||
|
| `ry-vue3/src/views/portal/PublicityDetail.vue:437-453` | — | `checkIntentSubmitted` 查重 |
|
||||||
|
| `ry-vue3/src/views/portal/PublicityDetail.vue:468-490` | — | 支持/执行意向入口 (登录分支) |
|
||||||
|
| `ry-vue3/src/router/index.js` | 10 | `/publicity/:projectId` 路由 |
|
||||||
|
| `proto/html/项目公示.html` | 697 | 列表 → 详情跳转 |
|
||||||
|
| `proto/html/公示详情.html` | 全文 | 原型基准 |
|
||||||
|
| `ry-vue3/src/api/public.js` | 117-127 | publicity intent 4 个公开端点 |
|
||||||
|
| `ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizPublicController.java` | 91-94 | GET /business/public/project/{projectId} |
|
||||||
|
| `ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizPublicityIntentController.java` | 全文 | 公示页意向 controller (公开 + 管理双端点) |
|
||||||
|
| `ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizPublicityIntentController.java:66-139` | — | `doSubmit` 通用提交逻辑 |
|
||||||
|
| `ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizPublicityIntentController.java:106-115` | — | 匿名 → 反查 sys_user → 回填 user_id |
|
||||||
|
| `ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizProject.java` | 113, 115, 119, 121 | 4 个 URL 字段 (含**死代码** noticeUrl/scheduleUrl) |
|
||||||
|
| `ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizPublicitySupportIntent.java` | 全文 | 公示-支持意向 entity |
|
||||||
|
| `ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizPublicityExecutionIntent.java` | 全文 | 公示-执行意向 entity |
|
||||||
|
| `ry-api/ruoyi-business/src/main/resources/mapper/business/BizProjectMapper.xml` | 40-43, 67-69 | URL 字段的 `<result>` 和 SELECT (无 notice/schedule) |
|
||||||
|
| `ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java` | 64 | 公开端点放行 (`/business/public/**`, `/business/publicity/**`) |
|
||||||
|
| MySQL `guoju0808.biz_project` | — | 32 列 + 1 主键 + 1 唯一 + 5 索引,实测 2026-08-19 |
|
||||||
|
| MySQL `guoju0808.biz_publicity_support_intent` | — | 17 列 + 1 主键 + 2 索引,实测 2026-08-19 |
|
||||||
|
| MySQL `guoju0808.biz_publicity_execution_intent` | — | 同上,实测 2026-08-19 |
|
||||||
|
| MySQL `guoju0808.biz_project.project_id=13` | — | 测试数据, invitation_url 有 OSS PDF,其他 URL 全 NULL |
|
||||||
+3
-3
@@ -36,7 +36,7 @@ public class BizExpertController extends BaseController
|
|||||||
return getDataTable(list);
|
return getDataTable(list);
|
||||||
}
|
}
|
||||||
@GetMapping("/{expertId}")
|
@GetMapping("/{expertId}")
|
||||||
public AjaxResult getInfo(@PathVariable("expertId") String expertId)
|
public AjaxResult getInfo(@PathVariable("expertId") Long expertId)
|
||||||
{
|
{
|
||||||
return success(bizExpertService.getById(expertId));
|
return success(bizExpertService.getById(expertId));
|
||||||
}
|
}
|
||||||
@@ -71,7 +71,7 @@ public class BizExpertController extends BaseController
|
|||||||
*/
|
*/
|
||||||
@Log(title = "专家启停", businessType = BusinessType.UPDATE)
|
@Log(title = "专家启停", businessType = BusinessType.UPDATE)
|
||||||
@PutMapping("/{expertId}/status")
|
@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));
|
return toAjax(bizExpertService.updateStatus(expertId, status));
|
||||||
}
|
}
|
||||||
@@ -89,7 +89,7 @@ public class BizExpertController extends BaseController
|
|||||||
}
|
}
|
||||||
@Log(title = "专家", businessType = BusinessType.DELETE)
|
@Log(title = "专家", businessType = BusinessType.DELETE)
|
||||||
@DeleteMapping("/{ids}")
|
@DeleteMapping("/{ids}")
|
||||||
public AjaxResult remove(@PathVariable String[] ids)
|
public AjaxResult remove(@PathVariable Long[] ids)
|
||||||
{
|
{
|
||||||
return toAjax(bizExpertService.deleteByPrimaryKeys(ids));
|
return toAjax(bizExpertService.deleteByPrimaryKeys(ids));
|
||||||
}
|
}
|
||||||
|
|||||||
+85
@@ -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<BizLaborProtocolTemplate> 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
+67
@@ -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<BizMeetingAttendee> 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 中间表
|
||||||
|
}
|
||||||
+6
@@ -8,6 +8,7 @@ import com.ruoyi.common.core.controller.BaseController;
|
|||||||
import com.ruoyi.common.core.domain.AjaxResult;
|
import com.ruoyi.common.core.domain.AjaxResult;
|
||||||
import com.ruoyi.common.core.page.TableDataInfo;
|
import com.ruoyi.common.core.page.TableDataInfo;
|
||||||
import com.ruoyi.common.enums.BusinessType;
|
import com.ruoyi.common.enums.BusinessType;
|
||||||
|
import com.ruoyi.common.utils.SecurityUtils;
|
||||||
import com.ruoyi.business.domain.BizMeeting;
|
import com.ruoyi.business.domain.BizMeeting;
|
||||||
import com.ruoyi.business.service.IBizMeetingService;
|
import com.ruoyi.business.service.IBizMeetingService;
|
||||||
|
|
||||||
@@ -23,6 +24,11 @@ public class BizMeetingController extends BaseController
|
|||||||
@GetMapping("/list")
|
@GetMapping("/list")
|
||||||
public TableDataInfo list(BizMeeting bizMeeting)
|
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();
|
startPage();
|
||||||
List<BizMeeting> list = bizMeetingService.selectList(bizMeeting);
|
List<BizMeeting> list = bizMeetingService.selectList(bizMeeting);
|
||||||
return getDataTable(list);
|
return getDataTable(list);
|
||||||
|
|||||||
+20
-13
@@ -13,7 +13,6 @@ import com.ruoyi.business.service.IBizExpertService;
|
|||||||
import com.ruoyi.business.service.SysSmsService;
|
import com.ruoyi.business.service.SysSmsService;
|
||||||
import com.ruoyi.common.core.controller.BaseController;
|
import com.ruoyi.common.core.controller.BaseController;
|
||||||
import com.ruoyi.common.core.domain.AjaxResult;
|
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.common.core.domain.entity.SysUser;
|
||||||
import com.ruoyi.system.service.ISysUserService;
|
import com.ruoyi.system.service.ISysUserService;
|
||||||
|
|
||||||
@@ -42,18 +41,26 @@ public class BizRegisterController extends BaseController {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private BCryptPasswordEncoder passwordEncoder;
|
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")
|
@PostMapping("/registerExpert")
|
||||||
public AjaxResult registerExpert(@RequestBody Map<String, Object> body) {
|
public AjaxResult registerExpert(@RequestBody Map<String, Object> body) {
|
||||||
String realName = (String) body.get("realName");
|
String realName = toStr(body.get("realName"));
|
||||||
String workUnit = (String) body.get("workUnit");
|
String workUnit = toStr(body.get("workUnit"));
|
||||||
String department = (String) body.get("department");
|
String department = toStr(body.get("department"));
|
||||||
String doctorTitle = (String) body.get("doctorTitle");
|
String doctorTitle = toStr(body.get("doctorTitle"));
|
||||||
String phone = (String) body.get("phone");
|
String phone = toStr(body.get("phone"));
|
||||||
String code = (String) body.get("code");
|
String code = toStr(body.get("code"));
|
||||||
String password = (String) body.get("password");
|
String password = toStr(body.get("password"));
|
||||||
String uuid = (String) body.get("uuid");
|
String uuid = toStr(body.get("uuid"));
|
||||||
String licenseCertUrl = (String) body.get("licenseCertUrl");
|
String licenseCertUrl = toStr(body.get("licenseCertUrl"));
|
||||||
String titleCertUrl = (String) body.get("titleCertUrl");
|
String titleCertUrl = toStr(body.get("titleCertUrl"));
|
||||||
|
|
||||||
if (realName == null || realName.isEmpty()) return error("姓名不能为空");
|
if (realName == null || realName.isEmpty()) return error("姓名不能为空");
|
||||||
if (workUnit == null || workUnit.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 更新
|
// sys_user.role_type 表字段 (DB 默认 executor, 专家需 = doctor), 用 mapper 更新
|
||||||
userService.updateRoleType(userId, "doctor");
|
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();
|
BizExpert expert = new BizExpert();
|
||||||
expert.setExpertId(UUID.fastUUID().toString());
|
|
||||||
expert.setUserId(userId);
|
expert.setUserId(userId);
|
||||||
expert.setName(realName);
|
expert.setName(realName);
|
||||||
expert.setPhone(phone);
|
expert.setPhone(phone);
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import com.ruoyi.common.core.domain.BaseEntity;
|
|||||||
public class BizExpert extends BaseEntity {
|
public class BizExpert extends BaseEntity {
|
||||||
private static final long serialVersionUID = 1L;
|
private static final long serialVersionUID = 1L;
|
||||||
/** expertId */
|
/** expertId */
|
||||||
private String expertId;
|
private Long expertId;
|
||||||
/** name */
|
/** name */
|
||||||
@Excel(name = "name")
|
@Excel(name = "name")
|
||||||
private String name;
|
private String name;
|
||||||
@@ -80,8 +80,8 @@ public class BizExpert extends BaseEntity {
|
|||||||
private String auditOpinion;
|
private String auditOpinion;
|
||||||
/** 状态 0正常 1禁用 */
|
/** 状态 0正常 1禁用 */
|
||||||
private String status;
|
private String status;
|
||||||
public String getExpertId() { return expertId; }
|
public Long getExpertId() { return expertId; }
|
||||||
public void setExpertId(String expertId) { this.expertId = expertId; }
|
public void setExpertId(Long expertId) { this.expertId = expertId; }
|
||||||
public String getName() { return name; }
|
public String getName() { return name; }
|
||||||
public void setName(String name) { this.name = name; }
|
public void setName(String name) { this.name = name; }
|
||||||
public String getPhone() { return phone; }
|
public String getPhone() { return phone; }
|
||||||
|
|||||||
+54
@@ -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; }
|
||||||
|
}
|
||||||
@@ -76,6 +76,8 @@ public class BizMeeting extends BaseEntity {
|
|||||||
private String scheduleUrl;
|
private String scheduleUrl;
|
||||||
/** 签署劳务 0未签 1已签 */
|
/** 签署劳务 0未签 1已签 */
|
||||||
private String laborSigned;
|
private String laborSigned;
|
||||||
|
/** 参会人 user_id (非持久化字段, 仅用于 mapper WHERE 过滤; controller 注入, 配合 biz_meeting_attendee 中间表) */
|
||||||
|
private transient Long userId;
|
||||||
public Long getMeetingId() { return meetingId; }
|
public Long getMeetingId() { return meetingId; }
|
||||||
public void setMeetingId(Long meetingId) { this.meetingId = meetingId; }
|
public void setMeetingId(Long meetingId) { this.meetingId = meetingId; }
|
||||||
public String getProjectNo() { return projectNo; }
|
public String getProjectNo() { return projectNo; }
|
||||||
@@ -122,4 +124,6 @@ public class BizMeeting extends BaseEntity {
|
|||||||
public void setScheduleUrl(String scheduleUrl) { this.scheduleUrl = scheduleUrl; }
|
public void setScheduleUrl(String scheduleUrl) { this.scheduleUrl = scheduleUrl; }
|
||||||
public String getLaborSigned() { return laborSigned; }
|
public String getLaborSigned() { return laborSigned; }
|
||||||
public void setLaborSigned(String laborSigned) { this.laborSigned = laborSigned; }
|
public void setLaborSigned(String laborSigned) { this.laborSigned = laborSigned; }
|
||||||
|
public Long getUserId() { return userId; }
|
||||||
|
public void setUserId(Long userId) { this.userId = userId; }
|
||||||
}
|
}
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
@@ -115,10 +115,6 @@ public class BizProject extends BaseEntity {
|
|||||||
private String supportLetterUrl;
|
private String supportLetterUrl;
|
||||||
/** 已发布公告URL */
|
/** 已发布公告URL */
|
||||||
private String publishUrl;
|
private String publishUrl;
|
||||||
/** 通知文件URL */
|
|
||||||
private String noticeUrl;
|
|
||||||
/** 日程文件URL */
|
|
||||||
private String scheduleUrl;
|
|
||||||
/** 是否已发布公示 0否 1是 */
|
/** 是否已发布公示 0否 1是 */
|
||||||
private String isPublished;
|
private String isPublished;
|
||||||
/** 发布时间 */
|
/** 发布时间 */
|
||||||
@@ -215,10 +211,6 @@ public class BizProject extends BaseEntity {
|
|||||||
public void setSupportLetterUrl(String supportLetterUrl) { this.supportLetterUrl = supportLetterUrl; }
|
public void setSupportLetterUrl(String supportLetterUrl) { this.supportLetterUrl = supportLetterUrl; }
|
||||||
public String getPublishUrl() { return publishUrl; }
|
public String getPublishUrl() { return publishUrl; }
|
||||||
public void setPublishUrl(String publishUrl) { this.publishUrl = 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 String getIsPublished() { return isPublished; }
|
||||||
public void setIsPublished(String isPublished) { this.isPublished = isPublished; }
|
public void setIsPublished(String isPublished) { this.isPublished = isPublished; }
|
||||||
public Date getPublishTime() { return publishTime; }
|
public Date getPublishTime() { return publishTime; }
|
||||||
|
|||||||
@@ -7,13 +7,13 @@ import com.ruoyi.business.domain.BizExpert;
|
|||||||
*/
|
*/
|
||||||
public interface BizExpertMapper
|
public interface BizExpertMapper
|
||||||
{
|
{
|
||||||
BizExpert selectByPrimaryKey(String expertId);
|
BizExpert selectByPrimaryKey(Long expertId);
|
||||||
BizExpert selectByUserId(Long userId);
|
BizExpert selectByUserId(Long userId);
|
||||||
List<BizExpert> selectList(BizExpert entity);
|
List<BizExpert> selectList(BizExpert entity);
|
||||||
int insert(BizExpert entity);
|
int insert(BizExpert entity);
|
||||||
int insertWithUserId(BizExpert entity);
|
int insertWithUserId(BizExpert entity);
|
||||||
int updateByPrimaryKey(BizExpert entity);
|
int updateByPrimaryKey(BizExpert entity);
|
||||||
int updateByUserId(BizExpert entity);
|
int updateByUserId(BizExpert entity);
|
||||||
int deleteByPrimaryKey(String expertId);
|
int deleteByPrimaryKey(Long expertId);
|
||||||
int deleteByPrimaryKeys(String[] expertIds);
|
int deleteByPrimaryKeys(Long[] expertIds);
|
||||||
}
|
}
|
||||||
+19
@@ -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<BizLaborProtocolTemplate> selectList(BizLaborProtocolTemplate entity);
|
||||||
|
/** 取默认模板 (default_flag='Y' AND status='Y'), 0 或 1 条 */
|
||||||
|
BizLaborProtocolTemplate selectDefault();
|
||||||
|
/** 取所有启用模板 (status='Y'), 按 sort_order 排序 */
|
||||||
|
List<BizLaborProtocolTemplate> selectAllEnabled();
|
||||||
|
int insert(BizLaborProtocolTemplate entity);
|
||||||
|
int updateByPrimaryKey(BizLaborProtocolTemplate entity);
|
||||||
|
/** 把所有行的 default_flag 设为 'N' (service.setDefault 调用) */
|
||||||
|
int clearAllDefault();
|
||||||
|
int deleteByPrimaryKey(Long id);
|
||||||
|
int deleteByPrimaryKeys(Long[] ids);
|
||||||
|
}
|
||||||
+15
@@ -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<BizMeetingAttendee> selectByMeetingId(Long meetingId);
|
||||||
|
List<BizMeetingAttendee> selectByUserId(Long userId);
|
||||||
|
List<BizMeetingAttendee> selectUnsignedByUserId(Long userId);
|
||||||
|
}
|
||||||
+4
-4
@@ -10,7 +10,7 @@ import com.ruoyi.common.core.domain.entity.SysUser;
|
|||||||
*/
|
*/
|
||||||
public interface IBizExpertService
|
public interface IBizExpertService
|
||||||
{
|
{
|
||||||
BizExpert getById(String expertId);
|
BizExpert getById(Long expertId);
|
||||||
BizExpert getByUserId(Long userId);
|
BizExpert getByUserId(Long userId);
|
||||||
List<BizExpert> selectList(BizExpert entity);
|
List<BizExpert> selectList(BizExpert entity);
|
||||||
/**
|
/**
|
||||||
@@ -24,11 +24,11 @@ public interface IBizExpertService
|
|||||||
* 启用/禁用专家: 同步更新 biz_expert.status + sys_user.status
|
* 启用/禁用专家: 同步更新 biz_expert.status + sys_user.status
|
||||||
* status='Y' 正常, status='N' 禁用
|
* status='Y' 正常, status='N' 禁用
|
||||||
*/
|
*/
|
||||||
int updateStatus(String expertId, String status);
|
int updateStatus(Long expertId, String status);
|
||||||
/** 按 userId 更新或新建 (upsert) */
|
/** 按 userId 更新或新建 (upsert) */
|
||||||
int updateProfileByUserId(BizExpert entity);
|
int updateProfileByUserId(BizExpert entity);
|
||||||
int deleteByPrimaryKey(String expertId);
|
int deleteByPrimaryKey(Long expertId);
|
||||||
int deleteByPrimaryKeys(String[] expertId);
|
int deleteByPrimaryKeys(Long[] expertId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 批量导入专家: 每行调用 insert, updateSupport=true 时跳过已存在手机号(视为成功)
|
* 批量导入专家: 每行调用 insert, updateSupport=true 时跳过已存在手机号(视为成功)
|
||||||
|
|||||||
+20
@@ -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<BizLaborProtocolTemplate> selectList(BizLaborProtocolTemplate entity);
|
||||||
|
BizLaborProtocolTemplate selectDefault();
|
||||||
|
List<BizLaborProtocolTemplate> 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);
|
||||||
|
}
|
||||||
+16
@@ -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<BizMeetingAttendee> selectByMeetingId(Long meetingId);
|
||||||
|
List<BizMeetingAttendee> selectByUserId(Long userId);
|
||||||
|
/** 当前用户的"待签署"会议列表 (handsign 或 labor_protocol 任一为空) */
|
||||||
|
List<BizMeetingAttendee> selectUnsignedByUserId(Long userId);
|
||||||
|
}
|
||||||
+45
-33
@@ -10,6 +10,7 @@ import com.ruoyi.business.service.IBizExpertService;
|
|||||||
import com.ruoyi.common.core.domain.entity.SysUser;
|
import com.ruoyi.common.core.domain.entity.SysUser;
|
||||||
import com.ruoyi.common.exception.ServiceException;
|
import com.ruoyi.common.exception.ServiceException;
|
||||||
import com.ruoyi.common.utils.SecurityUtils;
|
import com.ruoyi.common.utils.SecurityUtils;
|
||||||
|
import com.ruoyi.common.utils.id.IdGenerator;
|
||||||
import com.ruoyi.system.service.ISysUserService;
|
import com.ruoyi.system.service.ISysUserService;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@@ -22,7 +23,7 @@ public class BizExpertServiceImpl implements IBizExpertService
|
|||||||
private ISysUserService sysUserService;
|
private ISysUserService sysUserService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public BizExpert getById(String expertId)
|
public BizExpert getById(Long expertId)
|
||||||
{ return bizExpertMapper.selectByPrimaryKey(expertId); }
|
{ return bizExpertMapper.selectByPrimaryKey(expertId); }
|
||||||
@Override
|
@Override
|
||||||
public BizExpert getByUserId(Long userId)
|
public BizExpert getByUserId(Long userId)
|
||||||
@@ -32,12 +33,10 @@ public class BizExpertServiceImpl implements IBizExpertService
|
|||||||
{ return bizExpertMapper.selectList(entity); }
|
{ return bizExpertMapper.selectList(entity); }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* admin 创建专家: 同时创建 sys_user (用户名=手机号, 密码=手机号, role_type=doctor)
|
* 创建专家 + 绑定 sys_user。双职责:
|
||||||
* 1. 校验 phone 没注册过 (抛 ServiceException)
|
* A. admin 创建 / 批量导入: entity.userId == null → 全流程 (校验 phone + 建 sys_user + 建 biz_expert)
|
||||||
* 2. 创建 sys_user + bcrypt 加密密码
|
* B. 公开注册 (BizRegisterController): entity.userId 已设 → 控制器已建 sys_user, 本方法只做 biz_expert 绑定
|
||||||
* 3. 设置 role_type=doctor (与公开注册一致)
|
* 区分标志: entity.getUserId() 是否已设
|
||||||
* 4. 创建 biz_expert 绑定 user_id
|
|
||||||
* 5. 返回 SysUser 含明文 password (前端 toast 用完即丢)
|
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public SysUser insert(BizExpert entity) {
|
public SysUser insert(BizExpert entity) {
|
||||||
@@ -45,34 +44,47 @@ public class BizExpertServiceImpl implements IBizExpertService
|
|||||||
if (phone == null || phone.isEmpty()) {
|
if (phone == null || phone.isEmpty()) {
|
||||||
throw new ServiceException("手机号不能为空");
|
throw new ServiceException("手机号不能为空");
|
||||||
}
|
}
|
||||||
// 0. 校验 phone 唯一 (查 sys_user, 若 username=phone 已存在即重复)
|
|
||||||
if (sysUserService.isPhoneRegistered(phone)) {
|
Long userId = entity.getUserId();
|
||||||
throw new ServiceException("该手机号已注册,请直接登录");
|
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)
|
// ===== A + B 都走: 创建 biz_expert 绑定 user_id =====
|
||||||
SysUser newUser = new SysUser();
|
// expertId 用雪花 ID (53位, JS Number 安全, 不用 DB 自增)
|
||||||
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
|
|
||||||
entity.setUserId(userId);
|
entity.setUserId(userId);
|
||||||
com.ruoyi.common.utils.id.SnowflakeId.injectIfEmpty(entity, "expertId");
|
entity.setExpertId(IdGenerator.generateId());
|
||||||
bizExpertMapper.insert(entity);
|
bizExpertMapper.insert(entity);
|
||||||
|
|
||||||
// 4. 把明文密码回填 SysUser (仅本次返回,前端 toast 显示)
|
if (userId != null && result.getUserId() == null) {
|
||||||
newUser.setPassword(phone);
|
// B 路径: 控制器已知 userId, 不需返回明文密码
|
||||||
return newUser;
|
result.setUserId(userId);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -85,7 +97,7 @@ public class BizExpertServiceImpl implements IBizExpertService
|
|||||||
* sys_user.status: '0'=正常 '1'=停用 (RuoYi 框架约定, 同步时转换)
|
* sys_user.status: '0'=正常 '1'=停用 (RuoYi 框架约定, 同步时转换)
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public int updateStatus(String expertId, String status) {
|
public int updateStatus(Long expertId, String status) {
|
||||||
if (status == null || (!"Y".equals(status) && !"N".equals(status))) {
|
if (status == null || (!"Y".equals(status) && !"N".equals(status))) {
|
||||||
throw new ServiceException("status 必须是 'Y'(正常) 或 'N'(禁用)");
|
throw new ServiceException("status 必须是 'Y'(正常) 或 'N'(禁用)");
|
||||||
}
|
}
|
||||||
@@ -120,10 +132,10 @@ public class BizExpertServiceImpl implements IBizExpertService
|
|||||||
return bizExpertMapper.updateByUserId(entity);
|
return bizExpertMapper.updateByUserId(entity);
|
||||||
}
|
}
|
||||||
@Override
|
@Override
|
||||||
public int deleteByPrimaryKey(String expertId)
|
public int deleteByPrimaryKey(Long expertId)
|
||||||
{ return bizExpertMapper.deleteByPrimaryKey(expertId); }
|
{ return bizExpertMapper.deleteByPrimaryKey(expertId); }
|
||||||
@Override
|
@Override
|
||||||
public int deleteByPrimaryKeys(String[] expertId)
|
public int deleteByPrimaryKeys(Long[] expertId)
|
||||||
{ return bizExpertMapper.deleteByPrimaryKeys(expertId); }
|
{ return bizExpertMapper.deleteByPrimaryKeys(expertId); }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+70
@@ -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<BizLaborProtocolTemplate> selectList(BizLaborProtocolTemplate entity) {
|
||||||
|
return mapper.selectList(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BizLaborProtocolTemplate selectDefault() {
|
||||||
|
return mapper.selectDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<BizLaborProtocolTemplate> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+55
@@ -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<BizMeetingAttendee> selectByMeetingId(Long meetingId) {
|
||||||
|
return bizMeetingAttendeeMapper.selectByMeetingId(meetingId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<BizMeetingAttendee> selectByUserId(Long userId) {
|
||||||
|
return bizMeetingAttendeeMapper.selectByUserId(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<BizMeetingAttendee> selectUnsignedByUserId(Long userId) {
|
||||||
|
return bizMeetingAttendeeMapper.selectUnsignedByUserId(userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
<include refid="selectFields"/>
|
<include refid="selectFields"/>
|
||||||
where user_id = #{userId} limit 1
|
where user_id = #{userId} limit 1
|
||||||
</select>
|
</select>
|
||||||
<select id="selectByPrimaryKey" resultMap="BizExpertResult" parameterType="String">
|
<select id="selectByPrimaryKey" resultMap="BizExpertResult" parameterType="Long">
|
||||||
<include refid="selectFields"/>
|
<include refid="selectFields"/>
|
||||||
where expert_id = #{expertId}
|
where expert_id = #{expertId}
|
||||||
</select>
|
</select>
|
||||||
@@ -53,7 +53,7 @@
|
|||||||
<insert id="insert" parameterType="BizExpert">
|
<insert id="insert" parameterType="BizExpert">
|
||||||
insert into biz_expert
|
insert into biz_expert
|
||||||
<trim prefix="(" suffix=")" suffixOverrides=",">
|
<trim prefix="(" suffix=")" suffixOverrides=",">
|
||||||
<if test="expertId != null and expertId != ''">expert_id,</if>
|
<if test="expertId != null">expert_id,</if>
|
||||||
<if test="userId != null">user_id,</if>
|
<if test="userId != null">user_id,</if>
|
||||||
<if test="name != null">name,</if>
|
<if test="name != null">name,</if>
|
||||||
<if test="phone != null">phone,</if>
|
<if test="phone != null">phone,</if>
|
||||||
@@ -81,7 +81,7 @@
|
|||||||
<if test="updateTime != null">update_time,</if>
|
<if test="updateTime != null">update_time,</if>
|
||||||
</trim>
|
</trim>
|
||||||
<trim prefix="values (" suffix=")" suffixOverrides=",">
|
<trim prefix="values (" suffix=")" suffixOverrides=",">
|
||||||
<if test="expertId != null and expertId != ''">#{expertId},</if>
|
<if test="expertId != null">#{expertId},</if>
|
||||||
<if test="userId != null">#{userId},</if>
|
<if test="userId != null">#{userId},</if>
|
||||||
<if test="name != null">#{name},</if>
|
<if test="name != null">#{name},</if>
|
||||||
<if test="phone != null">#{phone},</if>
|
<if test="phone != null">#{phone},</if>
|
||||||
@@ -206,13 +206,13 @@
|
|||||||
</trim>
|
</trim>
|
||||||
where expert_id = #{expertId}
|
where expert_id = #{expertId}
|
||||||
</update>
|
</update>
|
||||||
<delete id="deleteByPrimaryKey" parameterType="String">
|
<delete id="deleteByPrimaryKey" parameterType="Long">
|
||||||
delete from biz_expert where expert_id = #{expertId}
|
delete from biz_expert where expert_id = #{expertId}
|
||||||
</delete>
|
</delete>
|
||||||
<delete id="deleteByPrimaryKeys" parameterType="String">
|
<delete id="deleteByPrimaryKeys" parameterType="Long">
|
||||||
delete from biz_expert where expert_id in
|
delete from biz_expert where expert_id in
|
||||||
<foreach collection="expertIds" item="expertId" open="(" separator="," close=")">
|
<foreach collection="expertId" item="id" open="(" separator="," close=")">
|
||||||
#{expertId}
|
#{id}
|
||||||
</foreach>
|
</foreach>
|
||||||
</delete>
|
</delete>
|
||||||
</mapper>
|
</mapper>
|
||||||
+75
@@ -0,0 +1,75 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
<mapper namespace="com.ruoyi.business.mapper.BizLaborProtocolTemplateMapper">
|
||||||
|
<resultMap type="BizLaborProtocolTemplate" id="BizLaborProtocolTemplateResult">
|
||||||
|
<id property="id" column="id" />
|
||||||
|
<result property="templateName" column="template_name" />
|
||||||
|
<result property="templateContent" column="template_content" />
|
||||||
|
<result property="defaultFlag" column="default_flag" />
|
||||||
|
<result property="sortOrder" column="sort_order" />
|
||||||
|
<result property="status" column="status" />
|
||||||
|
<result property="remark" column="remark" />
|
||||||
|
<result property="createBy" column="create_by" />
|
||||||
|
<result property="createTime" column="create_time" />
|
||||||
|
<result property="updateBy" column="update_by" />
|
||||||
|
<result property="updateTime" column="update_time" />
|
||||||
|
</resultMap>
|
||||||
|
<sql id="selectFields">
|
||||||
|
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
|
||||||
|
</sql>
|
||||||
|
<select id="selectByPrimaryKey" resultMap="BizLaborProtocolTemplateResult" parameterType="Long">
|
||||||
|
<include refid="selectFields"/>
|
||||||
|
where id = #{id}
|
||||||
|
</select>
|
||||||
|
<select id="selectList" resultMap="BizLaborProtocolTemplateResult" parameterType="BizLaborProtocolTemplate">
|
||||||
|
<include refid="selectFields"/>
|
||||||
|
<where>
|
||||||
|
<if test="templateName != null and templateName != ''">and template_name like concat('%', #{templateName}, '%')</if>
|
||||||
|
<if test="status != null and status != ''">and status = #{status}</if>
|
||||||
|
<if test="defaultFlag != null and defaultFlag != ''">and default_flag = #{defaultFlag}</if>
|
||||||
|
</where>
|
||||||
|
order by sort_order asc, id asc
|
||||||
|
</select>
|
||||||
|
<select id="selectDefault" resultMap="BizLaborProtocolTemplateResult">
|
||||||
|
<include refid="selectFields"/>
|
||||||
|
where default_flag = 'Y' and status = 'Y'
|
||||||
|
limit 1
|
||||||
|
</select>
|
||||||
|
<select id="selectAllEnabled" resultMap="BizLaborProtocolTemplateResult">
|
||||||
|
<include refid="selectFields"/>
|
||||||
|
where status = 'Y'
|
||||||
|
order by sort_order asc, id asc
|
||||||
|
</select>
|
||||||
|
<insert id="insert" parameterType="BizLaborProtocolTemplate">
|
||||||
|
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())
|
||||||
|
</insert>
|
||||||
|
<update id="updateByPrimaryKey" parameterType="BizLaborProtocolTemplate">
|
||||||
|
update biz_labor_protocol_template
|
||||||
|
<set>
|
||||||
|
<if test="templateName != null and templateName != ''">template_name = #{templateName},</if>
|
||||||
|
template_content = #{templateContent},
|
||||||
|
<if test="defaultFlag != null and defaultFlag != ''">default_flag = #{defaultFlag},</if>
|
||||||
|
<if test="sortOrder != null">sort_order = #{sortOrder},</if>
|
||||||
|
<if test="status != null and status != ''">status = #{status},</if>
|
||||||
|
<if test="remark != null">remark = #{remark},</if>
|
||||||
|
update_by = #{updateBy},
|
||||||
|
update_time = sysdate()
|
||||||
|
</set>
|
||||||
|
where id = #{id}
|
||||||
|
</update>
|
||||||
|
<update id="clearAllDefault">
|
||||||
|
update biz_labor_protocol_template set default_flag = 'N', update_time = sysdate()
|
||||||
|
</update>
|
||||||
|
<delete id="deleteByPrimaryKey" parameterType="Long">
|
||||||
|
delete from biz_labor_protocol_template where id = #{id}
|
||||||
|
</delete>
|
||||||
|
<delete id="deleteByPrimaryKeys" parameterType="Long">
|
||||||
|
delete from biz_labor_protocol_template where id in
|
||||||
|
<foreach collection="array" item="id" open="(" separator="," close=")">
|
||||||
|
#{id}
|
||||||
|
</foreach>
|
||||||
|
</delete>
|
||||||
|
</mapper>
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
<mapper namespace="com.ruoyi.business.mapper.BizMeetingAttendeeMapper">
|
||||||
|
<resultMap type="BizMeetingAttendee" id="BizMeetingAttendeeResult">
|
||||||
|
<id property="id" column="id" />
|
||||||
|
<result property="meetingId" column="meeting_id" />
|
||||||
|
<result property="userId" column="user_id" />
|
||||||
|
<result property="handsign" column="handsign" />
|
||||||
|
<result property="laborProtocol" column="labor_protocol" />
|
||||||
|
<result property="createBy" column="create_by" />
|
||||||
|
<result property="createTime" column="create_time" />
|
||||||
|
<!-- 联表字段 (非持久化, entity transient 字段接收) -->
|
||||||
|
<result property="meetingName" column="meeting_name" />
|
||||||
|
<result property="startTime" column="start_time" />
|
||||||
|
<result property="endTime" column="end_time" />
|
||||||
|
<result property="projectName" column="project_name" />
|
||||||
|
<result property="projectNo" column="project_no" />
|
||||||
|
</resultMap>
|
||||||
|
<insert id="insert" parameterType="BizMeetingAttendee">
|
||||||
|
insert into biz_meeting_attendee(meeting_id, user_id, create_by, create_time)
|
||||||
|
values(#{meetingId}, #{userId}, #{createBy}, sysdate())
|
||||||
|
</insert>
|
||||||
|
<update id="updateHandsign" parameterType="BizMeetingAttendee">
|
||||||
|
update biz_meeting_attendee
|
||||||
|
set handsign = #{handsign},
|
||||||
|
update_by = #{updateBy},
|
||||||
|
update_time = sysdate()
|
||||||
|
where id = #{id}
|
||||||
|
</update>
|
||||||
|
<update id="updateLaborProtocol" parameterType="BizMeetingAttendee">
|
||||||
|
update biz_meeting_attendee
|
||||||
|
set labor_protocol = #{laborProtocol},
|
||||||
|
update_by = #{updateBy},
|
||||||
|
update_time = sysdate()
|
||||||
|
where id = #{id}
|
||||||
|
</update>
|
||||||
|
<delete id="deleteByMeetingId" parameterType="Long">
|
||||||
|
delete from biz_meeting_attendee where meeting_id = #{meetingId}
|
||||||
|
</delete>
|
||||||
|
<delete id="deleteByMeetingIdAndUserId" parameterType="BizMeetingAttendee">
|
||||||
|
delete from biz_meeting_attendee where meeting_id = #{meetingId} and user_id = #{userId}
|
||||||
|
</delete>
|
||||||
|
<select id="selectByMeetingId" resultMap="BizMeetingAttendeeResult" parameterType="Long">
|
||||||
|
select id, meeting_id, user_id, handsign, labor_protocol, create_by, create_time
|
||||||
|
from biz_meeting_attendee where meeting_id = #{meetingId}
|
||||||
|
</select>
|
||||||
|
<select id="selectByUserId" resultMap="BizMeetingAttendeeResult" parameterType="Long">
|
||||||
|
select id, meeting_id, user_id, handsign, labor_protocol, create_by, create_time
|
||||||
|
from biz_meeting_attendee where user_id = #{userId}
|
||||||
|
</select>
|
||||||
|
<!--
|
||||||
|
当前用户的"待签署"会议列表 (任一未签: handsign 或 labor_protocol 为 NULL)
|
||||||
|
INNER JOIN biz_meeting 取会议名/时间/项目名, 用于 /doctor/home 工作台
|
||||||
|
字段别名 + resultMap 上面的 transient property 接收
|
||||||
|
-->
|
||||||
|
<select id="selectUnsignedByUserId" resultType="BizMeetingAttendee" parameterType="Long">
|
||||||
|
select a.id, a.meeting_id, a.user_id, a.handsign, a.labor_protocol, a.create_by, a.create_time,
|
||||||
|
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
|
||||||
|
</select>
|
||||||
|
</mapper>
|
||||||
@@ -45,6 +45,8 @@
|
|||||||
<if test="currentStage != null and currentStage != ''">and current_stage = #{currentStage}</if>
|
<if test="currentStage != null and currentStage != ''">and current_stage = #{currentStage}</if>
|
||||||
<if test="startTime != null">and start_time >= #{startTime}</if>
|
<if test="startTime != null">and start_time >= #{startTime}</if>
|
||||||
<if test="endTime != null">and end_time <= #{endTime}</if>
|
<if test="endTime != null">and end_time <= #{endTime}</if>
|
||||||
|
<!-- doctor 角色按 user_id 过滤 (走 biz_meeting_attendee 中间表) -->
|
||||||
|
<if test="userId != null">and exists (select 1 from biz_meeting_attendee a where a.meeting_id = biz_meeting.meeting_id and a.user_id = #{userId})</if>
|
||||||
</where>
|
</where>
|
||||||
order by meeting_id desc
|
order by meeting_id desc
|
||||||
</select>
|
</select>
|
||||||
|
|||||||
@@ -57,6 +57,7 @@
|
|||||||
<if test="role != null and role != ''"> and p.role = #{role}</if>
|
<if test="role != null and role != ''"> and p.role = #{role}</if>
|
||||||
<if test="status != null and status != ''"> and u.status = #{status}</if>
|
<if test="status != null and status != ''"> and u.status = #{status}</if>
|
||||||
<if test="parentUserId != null"> and u.parent_user_id = #{parentUserId}</if>
|
<if test="parentUserId != null"> and u.parent_user_id = #{parentUserId}</if>
|
||||||
|
<if test="userId != null"> and p.user_id = #{userId}</if>
|
||||||
<!-- 业务主账号隔离: biz_person.user_id IN (我的子账号 user_ids) -->
|
<!-- 业务主账号隔离: biz_person.user_id IN (我的子账号 user_ids) -->
|
||||||
<if test="params.subUserIds != null and params.subUserIds.size() > 0">
|
<if test="params.subUserIds != null and params.subUserIds.size() > 0">
|
||||||
and p.user_id in
|
and p.user_id in
|
||||||
|
|||||||
@@ -60,6 +60,9 @@ public class SecurityConfig
|
|||||||
requests.requestMatchers("/login", "/register", "/captchaImage").permitAll()
|
requests.requestMatchers("/login", "/register", "/captchaImage").permitAll()
|
||||||
// OSS 文件代理 (PDF/图片内嵌预览, 重写 Content-Disposition 为 inline)
|
// OSS 文件代理 (PDF/图片内嵌预览, 重写 Content-Disposition 为 inline)
|
||||||
.requestMatchers(HttpMethod.GET, "/common/oss/proxy").permitAll()
|
.requestMatchers(HttpMethod.GET, "/common/oss/proxy").permitAll()
|
||||||
|
// OSS 直传签名 (注册场景需匿名访问: 专家/执行方/支持方上传证书时还没 token)
|
||||||
|
// 安全性: OssController 已用 policy 限定 dir 前缀 + 文件大小, key 含时间戳+随机串防覆盖
|
||||||
|
.requestMatchers(HttpMethod.GET, "/common/oss/sign").permitAll()
|
||||||
// 业务公开门户与注册入口可匿名访问; 字典 active/单查公开, 写操作需登录+权限
|
// 业务公开门户与注册入口可匿名访问; 字典 active/单查公开, 写操作需登录+权限
|
||||||
.requestMatchers("/business/public/**", "/business/publicity/**", "/business/auth/**", "/business/sms/**").permitAll()
|
.requestMatchers("/business/public/**", "/business/publicity/**", "/business/auth/**", "/business/sms/**").permitAll()
|
||||||
// 字典 active (只读) + 按 ID 查 公开给前端拉下拉数据
|
// 字典 active (只读) + 按 ID 查 公开给前端拉下拉数据
|
||||||
|
|||||||
@@ -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' })
|
||||||
|
}
|
||||||
@@ -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' })
|
||||||
|
}
|
||||||
@@ -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 } })
|
||||||
|
}
|
||||||
@@ -82,7 +82,8 @@ const MENU = {
|
|||||||
]},
|
]},
|
||||||
{ path: '/admin/manage', title: '网站管理', icon: Setting, children: [
|
{ path: '/admin/manage', title: '网站管理', icon: Setting, children: [
|
||||||
{ path: '/admin/article', title: '协议管理', icon: Files },
|
{ 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 }
|
{ path: '/admin/account', title: '账号信息', icon: User }
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ const routes = [
|
|||||||
{ path: 'special-plan', name: 'admin-special-plan', component: () => import('@/views/admin/BizSpecialPlanAdmin.vue'), meta: { title: '专项计划管理' } },
|
{ 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: '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: '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: '账号信息' } }
|
{ path: 'account', name: 'admin-account', component: () => import('@/views/admin/Account.vue'), meta: { title: '账号信息' } }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -260,7 +260,7 @@ function fmtTime(d) {
|
|||||||
// ===== 审核 =====
|
// ===== 审核 =====
|
||||||
const auditOpen = ref(false)
|
const auditOpen = ref(false)
|
||||||
// result: '2'=通过, '3'=拒绝 (BizAuditStatusEnum: 0未提交 1待审核 2通过 3拒绝)
|
// 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) {
|
function onAudit(row) {
|
||||||
auditForm.expertId = row.expertId
|
auditForm.expertId = row.expertId
|
||||||
auditForm.name = row.name
|
auditForm.name = row.name
|
||||||
|
|||||||
@@ -0,0 +1,219 @@
|
|||||||
|
<template>
|
||||||
|
<div class="page-card admin-labor-protocol">
|
||||||
|
<div class="breadcrumb">首页 / 网站管理 / 劳务协议配置</div>
|
||||||
|
|
||||||
|
<!-- 筛选 -->
|
||||||
|
<el-form inline :model="q" class="filter-form">
|
||||||
|
<el-form-item label="模板名">
|
||||||
|
<el-input v-model="q.templateName" placeholder="输入模板名" clearable />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="状态">
|
||||||
|
<el-select v-model="q.status" placeholder="全部" clearable style="width:120px">
|
||||||
|
<el-option label="启用" value="Y" />
|
||||||
|
<el-option label="禁用" value="N" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="load">查询</el-button>
|
||||||
|
<el-button @click="reset">重置</el-button>
|
||||||
|
<el-button type="success" @click="openCreate">新建模板</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<!-- 列表 -->
|
||||||
|
<el-table :data="rows" border stripe v-loading="loading">
|
||||||
|
<el-table-column prop="id" label="ID" width="80" />
|
||||||
|
<el-table-column prop="templateName" label="模板名" min-width="180" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="sortOrder" label="排序" width="80" align="center" />
|
||||||
|
<el-table-column label="默认" width="80" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag v-if="row.defaultFlag === 'Y'" type="success">默认</el-tag>
|
||||||
|
<span v-else style="color:#c0c4cc">—</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="状态" width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="row.status === 'Y' ? 'success' : 'info'">{{ row.status === 'Y' ? '启用' : '禁用' }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="remark" label="备注" min-width="160" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="updateTime" label="更新时间" width="170" />
|
||||||
|
<el-table-column label="操作" width="240" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button size="small" link type="primary" @click="openEdit(row)">编辑</el-button>
|
||||||
|
<el-button size="small" link type="warning" :disabled="row.defaultFlag === 'Y'" @click="setDefault(row)">设为默认</el-button>
|
||||||
|
<el-button size="small" link type="danger" @click="removeRow(row)">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<div class="pager">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="q.pageNum"
|
||||||
|
v-model:page-size="q.pageSize"
|
||||||
|
:total="total"
|
||||||
|
:page-sizes="[10, 20, 50]"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
@current-change="load"
|
||||||
|
@size-change="load"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 新建/编辑 dialog -->
|
||||||
|
<el-dialog v-model="editOpen" :title="form.id ? '编辑模板' : '新建模板'" width="780px" destroy-on-close>
|
||||||
|
<el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
|
||||||
|
<el-form-item label="模板名" prop="templateName">
|
||||||
|
<el-input v-model="form.templateName" placeholder="如: 默认劳务协议" maxlength="100" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="排序" prop="sortOrder">
|
||||||
|
<el-input-number v-model="form.sortOrder" :min="0" :max="9999" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="默认模板">
|
||||||
|
<el-switch v-model="form.defaultFlag" active-value="Y" inactive-value="N" />
|
||||||
|
<span style="margin-left:12px;color:#909399;font-size:12px">开启后其他模板自动取消默认</span>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="状态">
|
||||||
|
<el-radio-group v-model="form.status">
|
||||||
|
<el-radio value="Y">启用</el-radio>
|
||||||
|
<el-radio value="N">禁用</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="模板内容" prop="templateContent">
|
||||||
|
<el-input v-model="form.templateContent" type="textarea" :rows="14"
|
||||||
|
placeholder="HTML 模板, 支持占位符: {name} {phone} {会议名称} {会议时间} {会议地址} {劳务形式} {费用总额} ..." />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="备注">
|
||||||
|
<el-input v-model="form.remark" placeholder="选填" maxlength="500" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="editOpen = false">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="submitting" @click="submit">保存</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
const q = reactive({ templateName: '', status: '', pageNum: 1, pageSize: 20 })
|
||||||
|
const rows = ref([])
|
||||||
|
const total = ref(0)
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const r = await request({ url: '/business/laborProtocolTemplate/list', method: 'get', params: { ...q } })
|
||||||
|
rows.value = (r.data && r.data.rows) || r.rows || []
|
||||||
|
total.value = (r.data && r.data.total) || r.total || 0
|
||||||
|
} finally { loading.value = false }
|
||||||
|
}
|
||||||
|
|
||||||
|
function reset() {
|
||||||
|
q.templateName = ''
|
||||||
|
q.status = ''
|
||||||
|
q.pageNum = 1
|
||||||
|
load()
|
||||||
|
}
|
||||||
|
|
||||||
|
const editOpen = ref(false)
|
||||||
|
const submitting = ref(false)
|
||||||
|
const formRef = ref(null)
|
||||||
|
const form = reactive({
|
||||||
|
id: null,
|
||||||
|
templateName: '',
|
||||||
|
templateContent: '',
|
||||||
|
defaultFlag: 'N',
|
||||||
|
sortOrder: 0,
|
||||||
|
status: 'Y',
|
||||||
|
remark: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
templateName: [{ required: true, message: '请输入模板名', trigger: 'blur' }],
|
||||||
|
templateContent: [{ required: true, message: '请输入模板内容', trigger: 'blur' }]
|
||||||
|
}
|
||||||
|
|
||||||
|
function openCreate() {
|
||||||
|
Object.assign(form, {
|
||||||
|
id: null,
|
||||||
|
templateName: '',
|
||||||
|
templateContent: '',
|
||||||
|
defaultFlag: 'N',
|
||||||
|
sortOrder: 0,
|
||||||
|
status: 'Y',
|
||||||
|
remark: ''
|
||||||
|
})
|
||||||
|
editOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function openEdit(row) {
|
||||||
|
Object.assign(form, {
|
||||||
|
id: row.id,
|
||||||
|
templateName: row.templateName,
|
||||||
|
templateContent: row.templateContent,
|
||||||
|
defaultFlag: row.defaultFlag || 'N',
|
||||||
|
sortOrder: row.sortOrder || 0,
|
||||||
|
status: row.status || 'Y',
|
||||||
|
remark: row.remark || ''
|
||||||
|
})
|
||||||
|
editOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submit() {
|
||||||
|
await formRef.value.validate()
|
||||||
|
submitting.value = true
|
||||||
|
try {
|
||||||
|
if (form.id) {
|
||||||
|
await request({ url: '/business/laborProtocolTemplate', method: 'put', data: form })
|
||||||
|
ElMessage.success('已更新')
|
||||||
|
} else {
|
||||||
|
await request({ url: '/business/laborProtocolTemplate', method: 'post', data: form })
|
||||||
|
ElMessage.success('已新建')
|
||||||
|
}
|
||||||
|
editOpen.value = false
|
||||||
|
load()
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error(e?.msg || '保存失败')
|
||||||
|
} finally { submitting.value = false }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setDefault(row) {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(`确认将"${row.templateName}"设为默认模板? 其他默认模板将自动取消`, '提示', { type: 'warning' })
|
||||||
|
} catch { return }
|
||||||
|
try {
|
||||||
|
await request({ url: `/business/laborProtocolTemplate/${row.id}/default`, method: 'put' })
|
||||||
|
ElMessage.success('已设为默认')
|
||||||
|
load()
|
||||||
|
} catch (e) { ElMessage.error(e?.msg || '操作失败') }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeRow(row) {
|
||||||
|
if (row.defaultFlag === 'Y') {
|
||||||
|
return ElMessage.warning('默认模板不能删除, 请先取消默认')
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(`确认删除"${row.templateName}"?`, '提示', { type: 'warning' })
|
||||||
|
} catch { return }
|
||||||
|
try {
|
||||||
|
await request({ url: `/business/laborProtocolTemplate/${row.id}`, method: 'delete' })
|
||||||
|
ElMessage.success('已删除')
|
||||||
|
load()
|
||||||
|
} catch (e) { ElMessage.error(e?.msg || '删除失败') }
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(load)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.admin-labor-protocol { padding: 16px; max-width: 1400px; }
|
||||||
|
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
|
||||||
|
.filter-form { margin-bottom: 12px; }
|
||||||
|
.pager { display: flex; justify-content: flex-end; margin-top: 12px; }
|
||||||
|
:deep(.el-textarea__inner) { font-family: 'Consolas', 'Monaco', monospace; font-size: 12px; }
|
||||||
|
</style>
|
||||||
@@ -267,6 +267,7 @@ async function afterLogin(token, displayName, fallbackRole) {
|
|||||||
userId: u.userId,
|
userId: u.userId,
|
||||||
userName: u.userName || displayName,
|
userName: u.userName || displayName,
|
||||||
nickName: u.nickName || displayName,
|
nickName: u.nickName || displayName,
|
||||||
|
phonenumber: u.phonenumber || '',
|
||||||
accountType: u.accountType || 'MAIN',
|
accountType: u.accountType || 'MAIN',
|
||||||
parentUserId: u.parentUserId || null,
|
parentUserId: u.parentUserId || null,
|
||||||
role
|
role
|
||||||
@@ -277,6 +278,7 @@ async function afterLogin(token, displayName, fallbackRole) {
|
|||||||
userId: null,
|
userId: null,
|
||||||
userName: displayName,
|
userName: displayName,
|
||||||
nickName: displayName,
|
nickName: displayName,
|
||||||
|
phonenumber: '',
|
||||||
accountType: 'MAIN',
|
accountType: 'MAIN',
|
||||||
parentUserId: null,
|
parentUserId: null,
|
||||||
role: fallbackRole
|
role: fallbackRole
|
||||||
|
|||||||
@@ -18,10 +18,10 @@
|
|||||||
<el-input v-model="form.workUnit" placeholder="请输入工作单位(医院全称)" />
|
<el-input v-model="form.workUnit" placeholder="请输入工作单位(医院全称)" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="科室" prop="department">
|
<el-form-item label="科室" prop="department">
|
||||||
<DoctorDeptSelect v-model="form.department" placeholder="请输入所在科室" />
|
<DoctorDeptSelect v-model="form.department" value-field="label" placeholder="请输入所在科室" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="职称" prop="doctorTitle">
|
<el-form-item label="职称" prop="doctorTitle">
|
||||||
<DoctorTitleSelect v-model="form.doctorTitle" placeholder="请输入职称" />
|
<DoctorTitleSelect v-model="form.doctorTitle" value-field="label" placeholder="请输入职称" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="手机号码" prop="phone">
|
<el-form-item label="手机号码" prop="phone">
|
||||||
<el-input v-model="form.phone" placeholder="请输入手机号码" maxlength="11" />
|
<el-input v-model="form.phone" placeholder="请输入手机号码" maxlength="11" />
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<!-- 欢迎栏 -->
|
<!-- 欢迎栏 -->
|
||||||
<div class="welcome-bar">
|
<div class="welcome-bar">
|
||||||
<div class="welcome-text">
|
<div class="welcome-text">
|
||||||
<h2>下午好,{{ store.user?.userName || '专家' }}专家</h2>
|
<h2>下午好,{{ displayName }}专家</h2>
|
||||||
<p>欢迎使用项目管理系统</p>
|
<p>欢迎使用项目管理系统</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="welcome-time">
|
<div class="welcome-time">
|
||||||
@@ -37,13 +37,11 @@
|
|||||||
<a class="more" @click.prevent="$router.push('/doctor/submissions')">更多 →</a>
|
<a class="more" @click.prevent="$router.push('/doctor/submissions')">更多 →</a>
|
||||||
</h2>
|
</h2>
|
||||||
<ul class="simple-list">
|
<ul class="simple-list">
|
||||||
<li class="simple-item" v-for="s in pendingAgreements" :key="s.planId">
|
<li class="simple-item" v-for="s in pendingAgreements" :key="s.id">
|
||||||
<div class="item-main">
|
<div class="item-main">
|
||||||
<span class="item-title">{{ s.planName }}</span>
|
<span class="item-title">{{ s.meetingName || ('会议 #' + s.meetingId) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="item-status" :class="{ done: s.status === '2' }">
|
<span class="item-status">待签署</span>
|
||||||
{{ s.status === '2' ? '已通过' : (s.status === '3' ? '已退回' : (s.status === '1' ? '审核中' : '待提交')) }}
|
|
||||||
</span>
|
|
||||||
</li>
|
</li>
|
||||||
<li v-if="!pendingAgreements.length" class="empty">暂无待签协议</li>
|
<li v-if="!pendingAgreements.length" class="empty">暂无待签协议</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -72,15 +70,23 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
|
||||||
import { useUserStore } from '@/store/user'
|
import { useUserStore } from '@/store/user'
|
||||||
import { bizList, listMyMessages } from '@/api/public'
|
import { bizList, listMyMessages } from '@/api/public'
|
||||||
|
import { getMyExpertProfile } from '@/api/business/expert'
|
||||||
|
import { listUnsignedMeetingProtocols } from '@/api/business/meetingAttendee'
|
||||||
|
|
||||||
const store = useUserStore()
|
const store = useUserStore()
|
||||||
|
|
||||||
const upcomingMeetings = ref([])
|
const upcomingMeetings = ref([])
|
||||||
const pendingAgreements = ref([])
|
const pendingAgreements = ref([])
|
||||||
const notices = ref([])
|
const notices = ref([])
|
||||||
|
// 专家真实姓名 (从 biz_expert.name 拿, 不显示 sys_user.userName (登录账号/手机号))
|
||||||
|
const expertName = ref('')
|
||||||
|
// 兜底显示名: 优先专家真实姓名 > nickName > userName > '专家'
|
||||||
|
const displayName = computed(() =>
|
||||||
|
expertName.value || store.user?.nickName || store.user?.userName || '专家'
|
||||||
|
)
|
||||||
|
|
||||||
const nowTime = ref('')
|
const nowTime = ref('')
|
||||||
const nowDate = ref('')
|
const nowDate = ref('')
|
||||||
@@ -109,13 +115,20 @@ async function load() {
|
|||||||
upcomingMeetings.value = (data?.rows || []).slice(0, 5)
|
upcomingMeetings.value = (data?.rows || []).slice(0, 5)
|
||||||
} catch (e) { upcomingMeetings.value = [] }
|
} catch (e) { upcomingMeetings.value = [] }
|
||||||
|
|
||||||
// 待签署协议 (改走 biz_project_plan, 后端 doctor 角色已自动按当前用户过滤)
|
// 当前用户的专家真实姓名 (用于欢迎栏)
|
||||||
try {
|
try {
|
||||||
const { data } = await bizList('projectPlan', { pageNum: 1, pageSize: 5 })
|
const { data } = await getMyExpertProfile()
|
||||||
pendingAgreements.value = (data?.rows || []).slice(0, 5).map(s => ({
|
expertName.value = data?.name || ''
|
||||||
planId: s.planId,
|
} catch (e) { expertName.value = '' }
|
||||||
planName: s.planName,
|
|
||||||
status: s.status
|
// 待签署协议 (v3: 改走 biz_meeting_attendee, 任一未签即显示)
|
||||||
|
try {
|
||||||
|
const { data } = await listUnsignedMeetingProtocols()
|
||||||
|
pendingAgreements.value = (data || []).slice(0, 5).map(s => ({
|
||||||
|
id: s.id, // biz_meeting_attendee.id (用于签署接口)
|
||||||
|
meetingId: s.meetingId,
|
||||||
|
meetingName: s.meetingName,
|
||||||
|
startTime: s.startTime
|
||||||
}))
|
}))
|
||||||
} catch (e) { pendingAgreements.value = [] }
|
} catch (e) { pendingAgreements.value = [] }
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
<el-form :model="form" label-width="120px" :rules="rules" ref="formRef" class="account-form">
|
<el-form :model="form" label-width="120px" :rules="rules" ref="formRef" class="account-form">
|
||||||
<el-form-item label="姓名" prop="nickName"><el-input v-model="form.nickName" placeholder="请输入姓名" /></el-form-item>
|
<el-form-item label="姓名" prop="nickName"><el-input v-model="form.nickName" placeholder="请输入姓名" /></el-form-item>
|
||||||
<el-form-item label="手机号" prop="phonenumber"><el-input v-model="form.phonenumber" placeholder="请输入手机号" /></el-form-item>
|
<el-form-item label="手机号" prop="phonenumber"><el-input v-model="form.phonenumber" placeholder="请输入手机号" maxlength="11" /></el-form-item>
|
||||||
|
|
||||||
<el-form-item label="原密码" prop="oldPassword"><el-input v-model="form.oldPassword" type="password" show-password placeholder="请输入原密码" /></el-form-item>
|
<el-form-item label="原密码" prop="oldPassword"><el-input v-model="form.oldPassword" type="password" show-password placeholder="请输入原密码" /></el-form-item>
|
||||||
<el-form-item label="新密码" prop="newPassword"><el-input v-model="form.newPassword" type="password" show-password placeholder="请输入新密码" /></el-form-item>
|
<el-form-item label="新密码" prop="newPassword"><el-input v-model="form.newPassword" type="password" show-password placeholder="请输入新密码" /></el-form-item>
|
||||||
|
|||||||
@@ -278,7 +278,7 @@ function fmtTime(d) {
|
|||||||
const editOpen = ref(false)
|
const editOpen = ref(false)
|
||||||
const editFormRef = ref(null)
|
const editFormRef = ref(null)
|
||||||
const editForm = reactive({
|
const editForm = reactive({
|
||||||
expertId: '', name: '', phone: '', workUnit: '', department: '', title: '',
|
expertId: null, name: '', phone: '', workUnit: '', department: '', title: '',
|
||||||
practiceCertUrl: '', titleCertUrl: ''
|
practiceCertUrl: '', titleCertUrl: ''
|
||||||
})
|
})
|
||||||
const editRules = {
|
const editRules = {
|
||||||
@@ -289,7 +289,7 @@ const editRules = {
|
|||||||
title: [{ required: true, message: '请选择职称', trigger: 'change' }]
|
title: [{ required: true, message: '请选择职称', trigger: 'change' }]
|
||||||
}
|
}
|
||||||
function openAdd() {
|
function openAdd() {
|
||||||
Object.assign(editForm, { expertId: '', name: '', phone: '', workUnit: '', department: '', title: '', practiceCertUrl: '', titleCertUrl: '' })
|
Object.assign(editForm, { expertId: null, name: '', phone: '', workUnit: '', department: '', title: '', practiceCertUrl: '', titleCertUrl: '' })
|
||||||
editOpen.value = true
|
editOpen.value = true
|
||||||
}
|
}
|
||||||
// 修改按钮: 跳转独立页 /manager/experts/edit/:id (参考 /admin/experts/new)
|
// 修改按钮: 跳转独立页 /manager/experts/edit/:id (参考 /admin/experts/new)
|
||||||
@@ -316,7 +316,7 @@ async function submitEdit() {
|
|||||||
// ========== 审核 dialog ==========
|
// ========== 审核 dialog ==========
|
||||||
const auditOpen = ref(false)
|
const auditOpen = ref(false)
|
||||||
// result: '2'=通过, '3'=拒绝 (BizAuditStatusEnum: 0未提交 1待审核 2通过 3拒绝)
|
// 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) {
|
function onAudit(row) {
|
||||||
auditForm.expertId = row.expertId
|
auditForm.expertId = row.expertId
|
||||||
auditForm.name = row.name
|
auditForm.name = row.name
|
||||||
|
|||||||
@@ -410,8 +410,6 @@ function confirmDeleteAnn() {
|
|||||||
publishUrl: '',
|
publishUrl: '',
|
||||||
invitationUrl: '',
|
invitationUrl: '',
|
||||||
supportLetterUrl: '',
|
supportLetterUrl: '',
|
||||||
noticeUrl: '',
|
|
||||||
scheduleUrl: '',
|
|
||||||
isPublished: '0'
|
isPublished: '0'
|
||||||
})
|
})
|
||||||
.then(() => { ElMessage.success(`已删除项目 ${delAnnTargetRow.value.projectNo} 的公告`); delAnnModalOpen.value = false; load() })
|
.then(() => { ElMessage.success(`已删除项目 ${delAnnTargetRow.value.projectNo} 的公告`); delAnnModalOpen.value = false; load() })
|
||||||
|
|||||||
@@ -239,12 +239,10 @@ async function loadProject() {
|
|||||||
if (m) { n.url = m.url || ''; n.name = m.name || '' }
|
if (m) { n.url = m.url || ''; n.name = m.name || '' }
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 兼容老数据
|
// 兼容老数据 (DB 实测只剩 invitationUrl/supportLetterUrl/publishUrl 3 个独立列)
|
||||||
const noticeMap = {
|
const noticeMap = {
|
||||||
invitation: p.invitationUrl,
|
invitation: p.invitationUrl,
|
||||||
supportLetter: p.supportLetterUrl,
|
supportLetter: p.supportLetterUrl,
|
||||||
notice: p.noticeUrl,
|
|
||||||
schedule: p.scheduleUrl,
|
|
||||||
publish: p.publishUrl
|
publish: p.publishUrl
|
||||||
}
|
}
|
||||||
for (const n of form.notices) {
|
for (const n of form.notices) {
|
||||||
@@ -307,7 +305,7 @@ async function submit(mode = 'save') {
|
|||||||
leadUserId: form.leadUserId,
|
leadUserId: form.leadUserId,
|
||||||
isBidProject: form.isBidProject
|
isBidProject: form.isBidProject
|
||||||
}
|
}
|
||||||
// 公告文件以 publicityFiles JSON 数组存储 (替代旧 invitationUrl/supportLetterUrl/noticeUrl/scheduleUrl)
|
// 公告文件以 publicityFiles JSON 数组存储 (替代旧 invitationUrl/supportLetterUrl/publishUrl 独立列)
|
||||||
payload.publicityFiles = JSON.stringify(uploaded.map(n => ({
|
payload.publicityFiles = JSON.stringify(uploaded.map(n => ({
|
||||||
type: n.key,
|
type: n.key,
|
||||||
label: n.label,
|
label: n.label,
|
||||||
@@ -318,8 +316,6 @@ async function submit(mode = 'save') {
|
|||||||
for (const n of uploaded) {
|
for (const n of uploaded) {
|
||||||
if (n.key === 'invitation') payload.invitationUrl = n.url
|
if (n.key === 'invitation') payload.invitationUrl = n.url
|
||||||
else if (n.key === 'supportLetter') payload.supportLetterUrl = n.url
|
else if (n.key === 'supportLetter') payload.supportLetterUrl = n.url
|
||||||
else if (n.key === 'notice') payload.noticeUrl = n.url
|
|
||||||
else if (n.key === 'schedule') payload.scheduleUrl = n.url
|
|
||||||
}
|
}
|
||||||
// 发布时设置 is_published='1' + publish_time (同步后端字段)
|
// 发布时设置 is_published='1' + publish_time (同步后端字段)
|
||||||
if (mode === 'publish') {
|
if (mode === 'publish') {
|
||||||
|
|||||||
@@ -164,8 +164,6 @@ async function load() {
|
|||||||
const urlMap = {
|
const urlMap = {
|
||||||
invitation: r.invitationUrl,
|
invitation: r.invitationUrl,
|
||||||
support: r.supportLetterUrl,
|
support: r.supportLetterUrl,
|
||||||
notice: r.noticeUrl,
|
|
||||||
schedule: r.scheduleUrl,
|
|
||||||
publish: r.publishUrl
|
publish: r.publishUrl
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -98,10 +98,10 @@
|
|||||||
</svg>
|
</svg>
|
||||||
<span>{{ signed ? '已报名' : (signing ? '报名中…' : '立即报名') }}</span>
|
<span>{{ signed ? '已报名' : (signing ? '报名中…' : '立即报名') }}</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="action-btn primary" :disabled="supportSubmitting || supportSubmitted" @click="onSupportIntent">
|
<button v-if="canShowSupportBtn" class="action-btn primary" :disabled="supportSubmitting || supportSubmitted" @click="onSupportIntent">
|
||||||
<span>{{ supportSubmitted ? '已支持' : (supportSubmitting ? '提交中…' : '表达支持意向') }}</span>
|
<span>{{ supportSubmitted ? '已支持' : (supportSubmitting ? '提交中…' : '表达支持意向') }}</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="action-btn primary" :disabled="executionSubmitting || executionSubmitted" @click="onExecutionIntent">
|
<button v-if="canShowExecutionBtn" class="action-btn primary" :disabled="executionSubmitting || executionSubmitted" @click="onExecutionIntent">
|
||||||
<span>{{ executionSubmitted ? '已表达意向' : (executionSubmitting ? '提交中…' : '表达执行意向') }}</span>
|
<span>{{ executionSubmitted ? '已表达意向' : (executionSubmitting ? '提交中…' : '表达执行意向') }}</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="action-btn primary share-btn" @click="showQr = true">
|
<button class="action-btn primary share-btn" @click="showQr = true">
|
||||||
@@ -193,13 +193,13 @@
|
|||||||
destroy-on-close
|
destroy-on-close
|
||||||
>
|
>
|
||||||
<el-form :model="guestDialog.form" label-width="100px">
|
<el-form :model="guestDialog.form" label-width="100px">
|
||||||
<el-form-item label="姓名 *" required>
|
<el-form-item label="姓名" required>
|
||||||
<el-input v-model="guestDialog.form.name" placeholder="请输入姓名" maxlength="50" clearable />
|
<el-input v-model="guestDialog.form.name" placeholder="请输入姓名" maxlength="50" clearable />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="手机号 *" required>
|
<el-form-item label="手机号" required>
|
||||||
<el-input v-model="guestDialog.form.phone" placeholder="请输入手机号" maxlength="11" clearable />
|
<el-input v-model="guestDialog.form.phone" placeholder="请输入手机号" maxlength="11" clearable />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="工作单位 *" required>
|
<el-form-item label="工作单位" required>
|
||||||
<el-input v-model="guestDialog.form.workUnit" placeholder="请输入工作单位" maxlength="200" clearable />
|
<el-input v-model="guestDialog.form.workUnit" placeholder="请输入工作单位" maxlength="200" clearable />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="部门">
|
<el-form-item label="部门">
|
||||||
@@ -223,6 +223,8 @@ import { useRoute, useRouter } from 'vue-router'
|
|||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { useUserStore } from '@/store/user'
|
import { useUserStore } from '@/store/user'
|
||||||
import { logout as logoutApi } from '@/api/auth'
|
import { logout as logoutApi } from '@/api/auth'
|
||||||
|
import { listBizPerson } from '@/api/business/person'
|
||||||
|
import { bizGet } from '@/api/public'
|
||||||
import {
|
import {
|
||||||
submitPublicitySupportIntent,
|
submitPublicitySupportIntent,
|
||||||
submitPublicityExecutionIntent,
|
submitPublicityExecutionIntent,
|
||||||
@@ -239,6 +241,20 @@ const isScrolled = ref(false)
|
|||||||
const topNavClass = computed(() => isScrolled.value ? 'is-scrolled' : '')
|
const topNavClass = computed(() => isScrolled.value ? 'is-scrolled' : '')
|
||||||
const loggedIn = computed(() => !!userStore.token)
|
const loggedIn = computed(() => !!userStore.token)
|
||||||
const userName = computed(() => userStore.user?.userName || '用户')
|
const userName = computed(() => userStore.user?.userName || '用户')
|
||||||
|
// 意向按钮按角色显隐:
|
||||||
|
// sponsor (含 MAIN/SUB) → 仅支持意向; executor (含 MAIN/SUB) → 仅执行意向
|
||||||
|
// admin / leader / manager / doctor → 两个都隐藏
|
||||||
|
// 匿名 → 两个都显示 (走 guestDialog)
|
||||||
|
const canShowSupportBtn = computed(() => {
|
||||||
|
const r = userStore.user?.role || ''
|
||||||
|
if (!r) return true
|
||||||
|
return r === 'sponsor'
|
||||||
|
})
|
||||||
|
const canShowExecutionBtn = computed(() => {
|
||||||
|
const r = userStore.user?.role || ''
|
||||||
|
if (!r) return true
|
||||||
|
return r === 'executor'
|
||||||
|
})
|
||||||
|
|
||||||
const showQr = ref(false)
|
const showQr = ref(false)
|
||||||
const qrUrl = ref('')
|
const qrUrl = ref('')
|
||||||
@@ -266,12 +282,10 @@ watch(showQr, async (v) => {
|
|||||||
const ann = ref(null)
|
const ann = ref(null)
|
||||||
const activeTab = ref('invitation')
|
const activeTab = ref('invitation')
|
||||||
|
|
||||||
// 按 URL 字段定义 4 个固定 tab (key 必须与 biz_project 字段名后缀一致)
|
// 按 URL 字段定义固定 tab (key 必须与 biz_project 字段名后缀一致)
|
||||||
const TAB_DEFS = [
|
const TAB_DEFS = [
|
||||||
{ key: 'invitation', label: '邀请函', urlKey: 'invitationUrl' },
|
{ key: 'invitation', label: '邀请函', urlKey: 'invitationUrl' },
|
||||||
{ key: 'supportLetter', label: '支持函', urlKey: 'supportLetterUrl' },
|
{ key: 'supportLetter', label: '支持函', urlKey: 'supportLetterUrl' }
|
||||||
{ key: 'notice', label: '通知', urlKey: 'noticeUrl' },
|
|
||||||
{ key: 'schedule', label: '日程', urlKey: 'scheduleUrl' }
|
|
||||||
]
|
]
|
||||||
|
|
||||||
// 只显示项目里"实际有文件 URL"的 tab
|
// 只显示项目里"实际有文件 URL"的 tab
|
||||||
@@ -433,12 +447,11 @@ const supportSubmitted = ref(false)
|
|||||||
const executionSubmitting = ref(false)
|
const executionSubmitting = ref(false)
|
||||||
const executionSubmitted = ref(false)
|
const executionSubmitted = ref(false)
|
||||||
|
|
||||||
// 持久化"已提交"标记 - 已登录按 sys_user.phonenumber, 未登录无身份只能本次会话判定
|
// 持久化"已提交"标记 - 已登录按 sys_user.phone 查后端 hasIntent
|
||||||
async function checkIntentSubmitted(type) {
|
async function checkIntentSubmitted(type) {
|
||||||
const proj = ann.value || {}
|
const proj = ann.value || {}
|
||||||
const projectId = proj.projectId || proj.id
|
const projectId = proj.projectId || proj.id
|
||||||
if (!projectId) return false
|
if (!projectId) return false
|
||||||
// 已登录: 用登录用户的手机号去查
|
|
||||||
const phone = userStore.user?.phonenumber || userStore.user?.phoneNumber
|
const phone = userStore.user?.phonenumber || userStore.user?.phoneNumber
|
||||||
if (!phone) return false
|
if (!phone) return false
|
||||||
try {
|
try {
|
||||||
@@ -465,10 +478,49 @@ function resetGuestForm() {
|
|||||||
guestDialog.form = { name: '', phone: '', workUnit: '', department: '', position: '' }
|
guestDialog.form = { name: '', phone: '', workUnit: '', department: '', position: '' }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 已登录用户 → 从 userStore + biz_person + biz_org 回填全部 5 个字段
|
||||||
|
// 流程: userStore 拿 name/phone → /business/person/list?userId= 拿 department/position/orgId → /business/org/{orgId} 拿 orgName(作 workUnit)
|
||||||
|
// 任何一步失败都优雅降级, 不阻塞 dialog 打开
|
||||||
|
async function prefillGuestFormFromUser() {
|
||||||
|
const u = userStore.user || {}
|
||||||
|
guestDialog.form = {
|
||||||
|
name: u.nickName || u.userName || '',
|
||||||
|
phone: u.phonenumber || u.phoneNumber || '',
|
||||||
|
workUnit: '',
|
||||||
|
department: '',
|
||||||
|
position: ''
|
||||||
|
}
|
||||||
|
const userId = u.userId
|
||||||
|
if (!userId) return // 没 userId (兜底), 不查 biz_person
|
||||||
|
// 1) 拉 biz_person
|
||||||
|
let person = null
|
||||||
|
try {
|
||||||
|
const { data } = await listBizPerson({ userId, pageNum: 1, pageSize: 1 })
|
||||||
|
const rows = data?.rows || []
|
||||||
|
person = rows[0] || null
|
||||||
|
} catch { /* 静默失败: dialog 仍打开, 用户手填 */ }
|
||||||
|
if (!person) return
|
||||||
|
guestDialog.form.department = person.department || ''
|
||||||
|
guestDialog.form.position = person.position || ''
|
||||||
|
// 2) 拉 biz_org → workUnit (优先用 JOIN 出来的 orgName; 没有再单查)
|
||||||
|
if (person.orgName) {
|
||||||
|
guestDialog.form.workUnit = person.orgName
|
||||||
|
} else if (person.orgId) {
|
||||||
|
try {
|
||||||
|
const { data: org } = await bizGet('org', person.orgId)
|
||||||
|
guestDialog.form.workUnit = org?.orgName || ''
|
||||||
|
} catch { /* 静默 */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function onSupportIntent() {
|
function onSupportIntent() {
|
||||||
|
if (!canShowSupportBtn.value) return // 防御: 防止 v-if 被绕过
|
||||||
if (supportSubmitting.value || supportSubmitted.value) return
|
if (supportSubmitting.value || supportSubmitted.value) return
|
||||||
if (loggedIn.value) {
|
if (loggedIn.value) {
|
||||||
doSubmitIntent('support')
|
guestDialog.type = 'support'
|
||||||
|
guestDialog.title = '表达支持意向'
|
||||||
|
guestDialog.open = true
|
||||||
|
prefillGuestFormFromUser() // 异步, 不 await; dialog 已先打开, 数据慢慢填进去
|
||||||
} else {
|
} else {
|
||||||
resetGuestForm()
|
resetGuestForm()
|
||||||
guestDialog.type = 'support'
|
guestDialog.type = 'support'
|
||||||
@@ -478,9 +530,13 @@ function onSupportIntent() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onExecutionIntent() {
|
function onExecutionIntent() {
|
||||||
|
if (!canShowExecutionBtn.value) return // 防御
|
||||||
if (executionSubmitting.value || executionSubmitted.value) return
|
if (executionSubmitting.value || executionSubmitted.value) return
|
||||||
if (loggedIn.value) {
|
if (loggedIn.value) {
|
||||||
doSubmitIntent('execution')
|
guestDialog.type = 'execution'
|
||||||
|
guestDialog.title = '表达执行意向'
|
||||||
|
guestDialog.open = true
|
||||||
|
prefillGuestFormFromUser() // 异步, 不 await
|
||||||
} else {
|
} else {
|
||||||
resetGuestForm()
|
resetGuestForm()
|
||||||
guestDialog.type = 'execution'
|
guestDialog.type = 'execution'
|
||||||
@@ -502,21 +558,14 @@ async function onGuestDialogConfirm() {
|
|||||||
finally { guestDialog.submitting = false }
|
finally { guestDialog.submitting = false }
|
||||||
}
|
}
|
||||||
|
|
||||||
// 实际提交: 已登录不带 5 字段 (后端从 sys_user 取 user_id, 但姓名/手机号/单位仍需 dialog 收集 — 故未登录分支专门处理;
|
// 实际提交: 已登录用户由 onSupportIntent/onExecutionIntent 直接打开 dialog,
|
||||||
// 已登录分支: 仍然弹 dialog 让用户填 5 字段, 仅 user_id 自动回填, 不复用登录信息是因为匿名流程设计的字段是访客视角的"姓名/手机号/单位/部门/职务",
|
// dialog 提交时调 onGuestDialogConfirm → 传 fields → 走 doSubmitIntent(type, fields)
|
||||||
// 与登录专家视角的"姓名/手机号/工作单位/科室/职称" 不完全一致; 但支持/执行意向两表字段一致, 所以走 dialog)
|
|
||||||
async function doSubmitIntent(type, fields) {
|
async function doSubmitIntent(type, fields) {
|
||||||
const proj = ann.value || {}
|
const proj = ann.value || {}
|
||||||
const projectId = proj.projectId || proj.id
|
const projectId = proj.projectId || proj.id
|
||||||
if (!projectId) return ElMessage.warning('项目ID缺失,无法提交')
|
if (!projectId) return ElMessage.warning('项目ID缺失,无法提交')
|
||||||
// fields 可能为空 (已登录快速通道) → 触发 dialog
|
// 防御: fields 缺失时不开 dialog (dialog 已在 onSupportIntent/onExecutionIntent 里打开)
|
||||||
if (!fields) {
|
if (!fields) return
|
||||||
resetGuestForm()
|
|
||||||
guestDialog.type = type
|
|
||||||
guestDialog.title = type === 'support' ? '表达支持意向' : '表达执行意向'
|
|
||||||
guestDialog.open = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const setter = type === 'support' ? s => { supportSubmitting.value = s } : s => { executionSubmitting.value = s }
|
const setter = type === 'support' ? s => { supportSubmitting.value = s } : s => { executionSubmitting.value = s }
|
||||||
const markDone = type === 'support' ? () => { supportSubmitted.value = true } : () => { executionSubmitted.value = true }
|
const markDone = type === 'support' ? () => { supportSubmitted.value = true } : () => { executionSubmitted.value = true }
|
||||||
setter(true)
|
setter(true)
|
||||||
@@ -743,7 +792,40 @@ onBeforeUnmount(() => {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
* {
|
.logo,
|
||||||
|
.nav-list,
|
||||||
|
.nav-item,
|
||||||
|
.nav-link,
|
||||||
|
.logo-text,
|
||||||
|
.logo-title,
|
||||||
|
.logo-subtitle,
|
||||||
|
.top-tools,
|
||||||
|
.footer-main,
|
||||||
|
.footer-brand,
|
||||||
|
.footer-col,
|
||||||
|
.footer-col h4,
|
||||||
|
.brand-row,
|
||||||
|
.footer-desc,
|
||||||
|
.footer-col a,
|
||||||
|
.footer-col p,
|
||||||
|
.footer-qr,
|
||||||
|
.footer-logo,
|
||||||
|
.qr-image,
|
||||||
|
.qr-label,
|
||||||
|
.footer-bottom,
|
||||||
|
.footer-brand-name,
|
||||||
|
.footer-brand-en,
|
||||||
|
.action-bar,
|
||||||
|
.modal,
|
||||||
|
.modal-qr,
|
||||||
|
.modal-qr-wrap,
|
||||||
|
.modal-title,
|
||||||
|
.modal-tip,
|
||||||
|
.modal-close,
|
||||||
|
.qr-corner,
|
||||||
|
.qr-loading,
|
||||||
|
.qr-dot,
|
||||||
|
.modal-tip {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
@@ -870,7 +952,18 @@ a { color: inherit; text-decoration: none; }
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
.top-tools .user-link {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity .2s;
|
||||||
|
}
|
||||||
|
.top-tools .user-link:hover { opacity: .85; }
|
||||||
|
|
||||||
.login-btn {
|
.login-btn {
|
||||||
padding: 7px 20px;
|
padding: 7px 20px;
|
||||||
|
|||||||
Reference in New Issue
Block a user