- 新增 LaborProtocolEdit.vue (独立页, 新建/编辑复用, 跟 ExpertNew.vue 风格一致)
- 加路由 /admin/labor-protocol/new 和 /admin/labor-protocol/edit/:id
- LaborProtocol.vue 删 el-dialog + 改为 router.push 跳转
- 新增 .form-card.nested { border: none; box-shadow: none; } 去 el-card 圆角框
- 新增 _self/admin_labor_protocol.md 审查报告
Co-Authored-By: Claude <noreply@anthropic.com>
15 KiB
/admin/labor-protocol 页面端到端审查
审查日期: 2026-08-19 审查范围: 前端 → 后端 → DB → 原型 审查者: Claude Code (page-tech-review skill)
0. 组件定位 (四跳链)
| 跳 | 命中 |
|---|---|
| ① 角色菜单 | AdminLayout.vue:86 MENU.admin[网站管理] → { path: '/admin/labor-protocol', title: '劳务协议配置', icon: Document } |
| ② 路由 | router/index.js:50 → name: 'admin-labor-protocol', component: () => import('@/views/admin/LaborProtocol.vue') |
| ③ 组件 | ry-vue3/src/views/admin/LaborProtocol.vue |
| ④ 原型 | ❌ 无对应原型 (proto/html/components/ 中无 "劳务协议配置" 页面, 最近的是 admin-settings.html 是系统设置, 不同业务) |
注意: 这是新建页面, 没有原型图可对比。从 HeguiConstants.java 的 3 个硬编码 HTML 字符串 (DEFAULT_PROTOCOL / BBS_PROTOCOL / JIANYAN_PROTOCOL) 迁移而来。
1. 主要功能 + 可见性
1.1 主要功能
| 功能 | 前端入口 | 后端接口 | 数据表 |
|---|---|---|---|
| 列表 (分页) | LaborProtocol.vue:25-37 |
GET /business/laborProtocolTemplate/list |
biz_labor_protocol_template |
| 筛选 (按模板名/状态) | LaborProtocol.vue:8-19 |
同 list (mapper <if> 过滤) |
biz_labor_protocol_template |
| 全部启用 (下拉) | (无 UI, 接口预留) | GET /business/laborProtocolTemplate/allEnabled |
同 |
| 默认模板 (单条) | (无 UI, 接口预留) | GET /business/laborProtocolTemplate/default |
同 |
| 详情 | (无 UI, 接口预留) | GET /business/laborProtocolTemplate/{id} |
同 |
| 新建 | LaborProtocol.vue:140-152 |
POST /business/laborProtocolTemplate |
同 |
| 编辑 | 同上 (dialog 复用) | PUT /business/laborProtocolTemplate |
同 |
| 删除 (批量/ 单条) | LaborProtocol.vue:160-167 |
DELETE /business/laborProtocolTemplate/{id} |
同 |
| 设为默认 | LaborProtocol.vue:153-158 |
PUT /business/laborProtocolTemplate/{id}/default |
同 (事务保证全局唯一) |
1.2 可见性 (三层过滤)
| 层 | 来源 | 校验字段 |
|---|---|---|
| 前端菜单 | AdminLayout.vue:86 |
仅 admin 角色可见 |
| 路由守卫 | router/index.js:23 meta: { role: 'admin' } |
admin token 校验 |
| 后端 | 无强制 role 校验 (依赖 Spring Security 注解 @PreAuthorize 缺失) |
⚠️ 任何登录用户都能调 |
不能看到本页面的人群: leader / manager / doctor / executor / sponsor — 他们各自走自己的角色页面。
数据隔离: ⚠️ 没有按角色二次校验 — BizLaborProtocolTemplateController 没有 @PreAuthorize("hasRole('admin')"), 任何已登录用户都能调这 8 个接口。这是设计疏忽。
2. 筛选项 (filter-form)
LaborProtocol.vue:8-19:
| UI label | 控件 | 绑定字段 | 后端 SQL (selectList) |
命中表.字段 | 选项来源 | 原型对照 |
|---|---|---|---|---|---|---|
| 模板名 | el-input |
q.templateName |
<if test="templateName != null and templateName != ''">and template_name like concat('%', #{templateName}, '%')</if> |
biz_labor_protocol_template.template_name (varchar100) |
— | N/A (无原型) |
| 状态 | el-select (Y=启用 / N=禁用) |
q.status |
<if test="status != null and status != ''">and status = #{status}</if> |
biz_labor_protocol_template.status (char(1)) |
写死 (硬编码 Y/N) | N/A |
| 查询/重置/新建 按钮 | — | — | — | — | — | N/A |
3. 工具栏按钮 (toolbar)
LaborProtocol.vue:21-24 + dialog 操作:
| 按钮 | 功能 | 接口 | 涉及表 | 原型对照 |
|---|---|---|---|---|
| 查询 | 触发表单 submit | (前端) | — | N/A |
| 重置 | 清 q, 重 load | (前端) | — | N/A |
| 新建模板 | 打开 dialog | POST /business/laborProtocolTemplate |
biz_labor_protocol_template |
N/A |
| 编辑 (行内) | openEdit(row) 填表单 |
PUT /business/laborProtocolTemplate |
同 | N/A |
| 设为默认 (行内) | setDefault(row) |
PUT /business/laborProtocolTemplate/{id}/default |
同 (事务, 清空其他 default_flag) | N/A |
| 删除 (行内) | removeRow(row) |
DELETE /business/laborProtocolTemplate/{id} |
同 | N/A |
4. 表格 (el-table)
LaborProtocol.vue:28-50
4.1 数据来源
BizLaborProtocolTemplateMapper.xml selectList:
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
[where name like concat('%', #{templateName}, '%')] -- 模板名模糊匹配
[and status = #{status}] -- 状态精确匹配
[and default_flag = #{defaultFlag}] -- 默认标记精确匹配 (预留,前端未用)
order by sort_order asc, id asc
走 startPage() + selectList() 走 RuoYi PageHelper 分页。
4.2 列映射
| 列名 | row.* |
DB 表.字段 | 原型对照 |
|---|---|---|---|
| ID | row.id |
biz_labor_protocol_template.id |
N/A |
| 模板名 | row.templateName |
.template_name (varchar100) |
N/A |
| 排序 | row.sortOrder |
.sort_order (int) |
N/A |
| 默认 (默认 | row.defaultFlag === 'Y' |
) → el-tag success |
.default_flag (char(1)) |
| 状态 (状态 | row.status === 'Y' |
? 'success' : 'info' |
.status (char(1)) |
| 备注 | row.remark |
.remark (varchar500) |
N/A |
| 更新时间 | row.updateTime |
.update_time (datetime) |
N/A |
| 操作 | — | — | — |
5. 行内操作按钮
LaborProtocol.vue:43-48:
| 按钮 | 触发函数 | 接口 | 后端动作 | 涉及表.字段 |
|---|---|---|---|---|
| 编辑 | openEdit(row) |
PUT /business/laborProtocolTemplate |
service.update(entity) → mapper.updateByPrimaryKey |
.template_name/.template_content/.sort_order/.status/.remark/.default_flag |
| 设为默认 | setDefault(row) |
PUT /business/laborProtocolTemplate/{id}/default |
service.setDefault(id) → @Transactional: mapper.clearAllDefault() 全部置 N → mapper.updateByPrimaryKey 目标置 Y |
.default_flag (整张表) |
| 删除 | removeRow(row) |
DELETE /business/laborProtocolTemplate/{id} |
service.deleteByPrimaryKeys([id]) → mapper.deleteByPrimaryKeys |
整行 |
真实落点:
- 设为默认:事务保证全局唯一 (
@Transactional在BizLaborProtocolTemplateServiceImpl.setDefault),先clearAllDefault后改目标 - 删除: 拒绝默认模板删除 (
LaborProtocol.vue:162-163前端校验row.defaultFlag === 'Y')
6. Java Entity ↔ 数据库表 一致性
6.1 实测 DB schema (2026-08-19)
CREATE TABLE biz_labor_protocol_template (
id bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
template_name varchar(100) NOT NULL COMMENT '模板名 (前端下拉显示)',
template_content longtext NOT NULL COMMENT '模板 HTML (含占位符 {name} {phone} 等)',
default_flag char(1) DEFAULT 'N' COMMENT '默认标记: Y=默认, N=非默认',
sort_order int DEFAULT '0' COMMENT '排序 (前端下拉顺序)',
status char(1) DEFAULT 'Y' COMMENT '状态: Y=启用, N=禁用',
remark varchar(500) 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 '更新时间',
PRIMARY KEY (id),
KEY idx_default_flag (default_flag),
KEY idx_status (status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='劳务协议模板配置';
6.2 Entity ↔ DB 字段对照
| 实体字段 | 中文列名 (DB COMMENT) | Java 类型 | 表字段 | DB 类型 | 一致? |
|---|---|---|---|---|---|
id |
主键 | Long |
id |
bigint AUTO_INCREMENT | ✅ |
templateName |
模板名 | String |
template_name |
varchar(100) NOT NULL | ✅ |
templateContent |
模板 HTML (含占位符) | String |
template_content |
longtext NOT NULL | ✅ |
defaultFlag |
默认标记 Y/N | String |
default_flag |
char(1) DEFAULT 'N' | ✅ |
sortOrder |
排序 | Integer |
sort_order |
int DEFAULT 0 | ✅ |
status |
状态 Y启用/N禁用 | String |
status |
char(1) DEFAULT 'Y' | ✅ |
remark |
备注 | String |
remark |
varchar(500) | ✅ |
createBy (BaseEntity) |
创建者 | String |
create_by |
varchar(64) | ✅ |
createTime (BaseEntity) |
创建时间 | Date |
create_time |
datetime | ✅ |
updateBy (BaseEntity) |
更新者 | String |
update_by |
varchar(64) | ✅ |
updateTime (BaseEntity) |
更新时间 | Date |
update_time |
datetime | ✅ |
无字段缺失, 无类型不一致。
7. 索引检查
7.1 biz_labor_protocol_template 索引 (实测)
| Key | 列 | 用途覆盖 |
|---|---|---|
| PRIMARY | id |
✅ WHERE id = ? |
| idx_default_flag | default_flag |
✅ WHERE default_flag = 'Y' (取默认模板) |
| idx_status | status |
✅ WHERE status = 'Y' (下拉只显示启用) |
7.2 缺口分析
| 查询场景 | 涉及列 | 现有索引 | 评价 |
|---|---|---|---|
| 列表分页 (按 sort_order, id 排) | sort_order, id |
PRIMARY | ✅ 走 filesort, OK |
| 模板名模糊筛选 | template_name |
❌ 无索引 | ⚠️ 暂可接受 (数据少) |
| 状态精确筛选 | status |
✅ idx_status | ✅ |
| 默认模板 | default_flag='Y' |
✅ idx_default_flag | ✅ |
| 全启用下拉 | status='Y' |
✅ idx_status | ✅ |
8. 与原型差异
8.1 实现新增 (超出原型)
| 项 | 实现 | 原型 | 评价 |
|---|---|---|---|
| 整个页面 | ✅ | ❌ 无对应原型 | 全新业务 |
| 编辑 dialog + 富文本 textarea | ✅ | ❌ | 实现完整 |
| "设为默认" 按钮 | ✅ | ❌ | 业务创新 |
| default_flag 全局唯一事务保证 | ✅ | ❌ | 数据完整性设计 |
| 3 个 HTML 模板从 HeguiConstants 迁移 | ✅ | ❌ | 内容来源 |
8.2 原型有但实现缺失
❌ 无原型可对比 — 这是新业务页面。
8.3 文字 / 标签差异
N/A
8.4 总结
整体方向: 这是新建页面, 端到端完整 (DB + 后端 + 前端)。没有原型对比, 无从评判偏离度。
9. 字段冗余 / 设计问题
9.1 default_flag 全局唯一 — 设计 OK
- ❌ DB 没有 UNIQUE 约束 (MySQL UNIQUE 不支持 'Y' 唯一,允许多 NULL)
- ✅ App 层事务保证:
BizLaborProtocolTemplateServiceImpl.setDefault用@Transactional,先clearAllDefault再设目标为 Y - ✅ 前端兜底: 编辑/删除按钮在
defaultFlag === 'Y'时禁用
9.2 无 template_type 字段 — 设计 OK
HeguiConstants 有 3 个模板 (北京整合医学学会 / 北京中卫 / 北京医学检验学会), 都属于"劳务协议"类型。用户要求"全局, 不需要加所属机构" → 当前实现一致。
- 后续如要区分协议类型 (劳务 / 保密 / 知识产权 等), 可加
template_type字段, 暂不需要
9.3 template_content 是 longtext (16MB 上限)
3 个 HeguiConstants 模板实际长度: 14K / 11K / 12K (合计 36K)。longtext 容量上限 ~16M, 够用。
- ⚠️ 灌入的 HeguiConstants 是 HTML 转义后的字符串字面量 (Java 转义
\\n等), DB 存的是已 unescape 的真实 HTML。已实测验证。
9.4 没有"template_id" 外键
template_content 里的 {name} {phone} {会议名称} 等占位符在签署时由谁替换? 目前没人替换!
- ⚠️ 闭环问题: 模板只是"配置", 实际签署 (
biz_meeting_attendee.labor_protocol存 URL) 还需要单独的工作流渲染模板 → 替换占位符 → 生成 PDF/HTML - 后续: 单独工作流 (可能用 hwt-serve 的 ActivityProcess 等), 不在本页面范围
10. 待修复列表
| # | 问题 | 文件 | 修复建议 | 严重度 |
|---|---|---|---|---|
| 1 | Controller 无角色校验: 任何已登录用户都能调 8 个接口 | BizLaborProtocolTemplateController.java |
加 @PreAuthorize("hasRole('admin')") 类级别注解 (依赖 RuoYi 注解 + Spring Security 配置) |
P0 |
| 2 | /api/business/laborProtocolTemplate.js default/allEnabled 接口前端未使用 |
api/business/laborProtocolTemplate.js |
暂时保留 (后续签署流程要用), 或删除以减小 API surface | P3 |
| 3 | 富文本编辑器缺失, 现在 textarea 不友好 | LaborProtocol.vue |
集成 Tinymce 或 wangEditor (含 HTML 预览) | P2 |
| 4 | template_content 没"占位符说明"提示 | LaborProtocol.vue |
placeholder 加可用占位符列表 (如 {name}, {phone}, {会议名称}) |
P2 |
| 5 | 入参没有 default_flag 校验: 前端可能传任意字符串 (不是 Y/N) | BizLaborProtocolTemplateController.add/edit |
service 层校验 defaultFlag ∈ {Y, N}, 失败抛 ServiceException |
P2 |
| 6 | update mapper 写了 template_content = #{templateContent} 不在 <if> 内 — 总会覆盖 |
BizLaborProtocolTemplateMapper.xml:53-66 |
改成 <if test="templateContent != null">template_content = #{templateContent},</if> 避免误清空 |
P3 |
| 7 | 删除模板没审计 (硬 DELETE, 不走 del_flag='0'``) |
模板不重要, 可以接受, 不修 | P3 | |
| 8 | template_content 中文乱码: 后端 JSON 响应 Content-Type 没 charset=UTF-8, 浏览器 fallback Latin-1 | ResourcesConfig.java |
加 configureMessageConverters 设 Jackson 默认 UTF-8 (你说暂不改, 留待后续) |
P2 |
11. 引用清单
v1 新建文件 (12 个)
| 文件 | 行号 | 说明 |
|---|---|---|
DB biz_labor_protocol_template |
— | 11 列 + PK + 2 索引 |
ry-api/.../domain/BizLaborProtocolTemplate.java |
全文 | entity |
ry-api/.../mapper/BizLaborProtocolTemplateMapper.java |
全文 | mapper 接口 |
ry-api/.../resources/mapper/business/BizLaborProtocolTemplateMapper.xml |
全文 | mapper SQL |
ry-api/.../service/IBizLaborProtocolTemplateService.java |
全文 | service 接口 |
ry-api/.../service/impl/BizLaborProtocolTemplateServiceImpl.java |
全文 | service 实现 (含 @Transactional setDefault) |
ry-api/.../controller/BizLaborProtocolTemplateController.java |
全文 | 8 个 REST 接口 |
ry-vue3/src/api/business/laborProtocolTemplate.js |
全文 | 前端 API 封装 |
ry-vue3/src/views/admin/LaborProtocol.vue |
全文 | admin 页面 (列表 + dialog) |
ry-vue3/src/router/index.js:50 |
— | 加 /admin/labor-protocol 路由 |
ry-vue3/src/layout/AdminLayout.vue:86 |
— | 加菜单 "劳务协议配置" |
没动
| 文件 | 说明 |
|---|---|
HeguiConstants.java (hwt-serve) |
3 个常量保留, 内容已灌入 DB |
proto/html/components/admin-settings.html |
系统设置原型, 不相关 |
TL;DR (给项目负责人)
新建页面, 端到端完整:
- DB ✅
biz_labor_protocol_template(11 列 + 3 行初始化 + 2 索引) - 后端 ✅ 完整 CRUD + default_flag 事务保证
- 前端 ✅ admin 页面 + 列表 + dialog
P0 安全风险: Controller 没加 @PreAuthorize("hasRole('admin')"), 任何登录用户能调所有接口 → 必须修
P2 体验:
- 富文本编辑器缺失 (现在是 textarea)
- 占位符说明缺失
- template_content 中文乱码 (Content-Type 没 charset,你说暂不改)
正面:
- 设计清晰, default_flag 全局唯一有 app 层事务保证
- 3 个 HeguiConstants 模板完整迁移, DB 实测 14K/11K/12K 字符
- 索引覆盖主要查询场景
- 没有数据隔离 (因为是全局配置,本来就不需要按用户隔离, 只有角色隔离缺失 — 见 P0)