- manager_projects_assign.md: 新增 11 章端到端审查报告 (4 跳定位 + Entity 一致性 + 索引 + 原型对比 + 待修复 16 条) - manager_projects.md: 修复记录表加 2026-08-20 P1/P2/P3 修复条目 (与上面 commit 一致) Co-Authored-By: Claude <noreply@anthropic.com>
31 KiB
0. 4 跳定位 (铁律: 不按名字猜)
| 跳 | 文件:行 | 结果 |
|---|---|---|
| ① 角色菜单 | ry-vue3/src/layout/AdminLayout.vue:96 |
path: '/manager/projects', title: '项目管理' (子入口, 无独立菜单) |
| ② 路由 | ry-vue3/src/router/index.js:64 |
name: 'manager-projects-assign', component = ManagerProjectsAssign.vue ✓ |
| ③ 组件 | ry-vue3/src/views/manager/ManagerProjectsAssign.vue |
441 行, 本审查对象 |
| ④ 原型 | proto/html/components/project-assign.html (1.html 同源备份) |
项目分配独立页原型 |
调用路径:
manager/Projects.vue行 dropdownassign命令或 toolbar "项目分配" 按钮 →router.push({path:'/manager/projects/assign', query:{projectId|projectIds}})(Projects.vue:274-280, 465-480)。 单一路由 + query 区分单条 (?projectId=X) / 批量 (?projectIds=1,2,3), 用batchMode = !!route.query.projectIds切换 UI (ManagerProjectsAssign.vue:113-115)。
1. 主要功能 + 可见性
1.1 主要功能
| 功能 | 前端入口 | 后端接口 | 涉及表 |
|---|---|---|---|
| 加载项目基本信息 (单条) | loadSingleProject (ManagerProjectsAssign.vue:257-275) |
GET /business/project/{id} → BizProjectController.getInfo → bizProjectService.getById |
biz_project |
| 加载项目基本信息 (批量) | loadBatchProjects (L277-309) |
同上, N 次串行调用 (无批量接口) | biz_project × N |
| 加载已分配执行方 | loadAssigns (L200-222) |
GET /business/project/{id}/assigns → BizProjectController.getAssigns → bizProjectAssignService.selectByProjectId |
biz_project_assign |
| 支持方下拉选项 | searchSponsorOrgs (L172-191) |
GET /business/org/sponsorOptions?orgName=&userId= → BizOrgController.sponsorOptions → bizOrgService.selectSponsorOrgOptions |
biz_org (org_type='sponsor') + sys_user |
| 执行方下拉选项 (行内) | searchExecutors (L227-248) |
GET /business/org/executorOptions?orgName=&userId= → BizOrgController.executorOptions → bizOrgService.selectExecutorOrgOptions |
biz_org (org_type='executor') + sys_user (MAIN only) |
| 保存支持方 + 截止天数 | submitAssign 中 PUT (L365-374, 388-393) |
PUT /business/project → bizProjectService.updateByPrimaryKey |
biz_project.sponsor_admin_user_id / sponsor_admin_user_name / submit_deadline_days |
| 保存执行方分配 (单条) | submitAssign 中 POST (L394-399) |
POST /business/project/{id}/assigns → BizProjectController.saveAssigns (deleteByProjectId 后逐条 insert) |
biz_project_assign |
| 保存执行方分配 (批量) | submitAssign 中循环 (L366-386) |
同上 × N, 错单跳过收集到 errs |
biz_project_assign × N |
1.2 哪些人能看到本页面
| 层 | 来源 | 校验字段 |
|---|---|---|
| 前端菜单 | AdminLayout.vue:96 (/manager/projects) |
无 requireMain, 主/子账号均可见 (manager 角色没有子账号业务) |
| 路由守卫 | router/index.js:64 |
meta.role 无显式, 走全局 permission.js 校验登录态 |
| 后端 SQL | BizProjectController @Autowired service |
无 role 过滤, 任何登录角色都可 hit 接口 — 跟前端 "manager 全局可见" 语义一致 |
其他角色对应功能:
- sponsor / executor / doctor 都不进本页面 (他们有自己的项目页, 走
sponsorList/executorList/myProjects接口) - admin 角色: 路由表 L28
admin-projects也指向manager/Projects.vue, 但 admin 端 Projects.vue 走同样的selectList, 走本子页面前无 role 校验 (admin 也可触发分配) — 业务上 OK
数据隔离: manager 是无界角色, 可分配任何项目; 分配动作无 owner 校验, 但写入路径 (PUT /business/project + POST /business/project/{id}/assigns) 不限制调用者, 任何已登录用户均可触发 — 风险点见 §10 #3。
2. 筛选项 (filter-form)
本页无传统筛选项(单条模式无 filter, 批量模式也无筛选), 只有表单字段。下面对应"el-form-item":
| UI label | 控件 | v-model | 后端 SQL | 命中表.字段 | 选项来源 | 原型对照 |
|---|---|---|---|---|---|---|
| 支持方 | el-select (single+remote+filterable) |
assignForm.sponsorAdminUserId |
selectSponsorOrgOptions (BizOrgMapper.xml:111-123) WHERE o.org_type='sponsor' AND u.del_flag='0' AND [org_name LIKE] [user_id = ?] LIMIT 5 |
biz_org.user_id (MAIN) + biz_org.org_name | /business/org/sponsorOptions (LIMIT 5) |
✅ 原型第 2 区块 |
| 执行方 (行内) | el-select (row 内 remote+filterable) |
row.execUserId |
selectExecutorOrgOptions (BizOrgMapper.xml:135-148) WHERE o.org_type='executor' AND u.del_flag='0' AND u.parent_user_id IS NULL [org_name LIKE] [user_id = ?] LIMIT 5 |
biz_org.user_id (MAIN only) + biz_org.org_name | /business/org/executorOptions (LIMIT 5, 限定 MAIN) |
✅ 原型第 3 区块 |
| 场数 (行内) | el-input-number |
row.sessions |
直接 biz_project_assign.sessions |
biz_project_assign.sessions | — | ✅ 原型 场数: |
| 金额 (行内) | el-input-number :precision="2" |
row.amount |
直接 biz_project_assign.amount |
biz_project_assign.amount (decimal 15,2) | — | ✅ 原型 总金额: |
| 提交截止(天) | el-input-number |
assignForm.deadlineDays |
updateByPrimaryKey L268 <if>submit_deadline_days = #{...} |
biz_project.submit_deadline_days | — | ⚠️ 原型 max 100, Vue 无校验 (见 §10 #5) |
类型注意: biz_project_assign.amount 类型
decimal(15,2), Vue 用 el-input-number precision=2 ✅ 一致。 类型注意: biz_project.submit_deadline_days 类型int, Vue 用 el-input-number 无 min/max ✅ 类型对, 但未做 1~100 边界校验 (原型说"超过 100 报错", Vue 没做)。
3. 工具栏按钮 (toolbar)
| 按钮 | 功能 | 接口 | 涉及表 | 原型对照 |
|---|---|---|---|---|
| + 增加执行方 | addExecRow (L165-169) |
(本地, 推 row 到 execRows) | — | ✅ 原型 + 增加执行方 |
| 删除 (行内) | assignForm.execRows.splice($index, 1) |
(本地) | — | ✅ 原型 删除 链接 |
| 取消 | confirmCancel (L321-325) |
ElMessageBox.confirm → goBack (router.push 回列表) |
— | ✅ 原型 取消 |
| 保存 | submitAssign (L328-406) |
见 §1.1 表 (PUT + POST) | biz_project + biz_project_assign | ✅ 原型 保存 |
4. 表格 (el-table)
4.1 数据来源
本页面不展示项目列表表格, 只有"执行方分配行表"(el-table bound to assignForm.execRows) 和单条模式顶部 6 字段描述。
4.1.1 顶部描述区 (单条模式 el-descriptions) 数据源
GET /business/project/{id} → BizProjectController.getInfo → BizProjectServiceImpl.getById → BizProjectMapper.selectByPrimaryKey (BizProjectMapper.xml:46-49)
SELECT p.project_id, p.project_no, p.project_name, p.total_sessions, p.done_sessions, p.todo_sessions,
p.total_amount, p.available_amount, p.paid_labor_amount, p.paid_meeting_amount,
p.manager_score, p.sponsor_score, p.sponsor_admin_user_name, p.project_form, p.is_finished, p.is_settled,
p.sponsor_admin_user_id, p.lead_user_id, p.is_bid_project,
p.create_by, p.create_user_id, p.create_time, p.update_by, p.update_time, p.manage_fee,
p.start_time, p.end_time, p.submit_deadline_days,
p.support_contract_url, p.execute_contract_url, p.invitation_url, p.support_letter_url, p.publish_url
FROM biz_project p
WHERE project_id = #{projectId}
Vue 字段映射 (ManagerProjectsAssign.vue:18-23):
projectNo← biz_project.project_no ✅projectName← biz_project.project_name ✅projectForm← biz_project.project_form ✅totalSessions← biz_project.total_sessions (int) ✅totalAmount← biz_project.total_amount (decimal 15,2) ✅manageFee← biz_project.manage_fee (decimal 15,2) ✅
4.1.2 执行方行表数据源
GET /business/project/{id}/assigns → BizProjectController.getAssigns → BizProjectAssignMapper.selectByProjectId (BizProjectAssignMapper.xml:31-35)
SELECT assign_id, project_id, execution_unit_id, exec_user_id,
sessions, amount, remark, status,
create_by, create_time, update_by, update_time
FROM biz_project_assign
WHERE project_id = #{projectId} AND status = '0'
ORDER BY assign_id ASC
EXPLAIN 实测: key=uk_project_unit (project_id, execution_unit_id), ref=const, rows=1, Extra=Using where; Using filesort ✅ 走唯一索引。
4.2 列映射
| 列名 | row.* |
表.字段 | 原型对照 |
|---|---|---|---|
| # | (type="index") | — | ✅ 原型 1. 2. 3. 序号 |
| 执行方名称 | row.execUserId (el-select label=orgName) |
biz_org.org_name (JOIN, label); biz_project_assign.exec_user_id (value) | ✅ 原型 select |
| 场次 | row.sessions |
biz_project_assign.sessions (int) | ✅ 原型 场数: |
| 金额 | row.amount |
biz_project_assign.amount (decimal 15,2) | ✅ 原型 总金额: |
| 操作 | 删除按钮 | — | ✅ 原型 删除 链接 |
注: 原型行内显示单位名称 (
org_name), 但 Vue 把 el-select 的:value=u.userId :label=u.orgName— label 确实显示公司名 ✅。但不显示执行方账户名 (u.userName), 原型也没要求, 业务上可接受。
5. 行内操作按钮 (op-cell)
无传统行内操作按钮 (本页是分配表单, 不是列表)。删除按钮在 §3 工具栏行内列出。
特殊按钮:
+ 增加执行方(L85) →addExecRow()— 不调接口, 只在assignForm.execRows推入新空行, 然后searchExecutors('', row)异步拉取执行方选项 (避免新行 el-select 没数据)- 行内
删除(L80) →splice— 不调接口, 跟submitAssign时整体 deleteByProjectId + 全部 insert 一并提交
6. Java Entity ↔ 数据库表 一致性
6.1 biz_project_assign DDL (实测 2026-08-20)
CREATE TABLE biz_project_assign (
assign_id bigint NOT NULL AUTO_INCREMENT COMMENT '分配ID',
project_id bigint NOT NULL COMMENT '项目ID',
execution_unit_id bigint NOT NULL COMMENT '执行单位ID',
sessions int DEFAULT '0' COMMENT '分配场数',
amount decimal(15,2) DEFAULT '0.00' COMMENT '总金额',
exec_user_id bigint DEFAULT NULL COMMENT '执行方用户ID (sys_user.user_id)',
remark varchar(500) DEFAULT NULL COMMENT '备注',
status char(1) DEFAULT '0' COMMENT '状态 0正常 1已撤销',
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 (assign_id),
UNIQUE KEY uk_project_unit (project_id, execution_unit_id),
KEY idx_assign_project (project_id),
KEY idx_assign_unit (execution_unit_id),
KEY idx_assign_exec_user (exec_user_id),
KEY idx_assign_exec_user_project (exec_user_id, project_id) -- 2026-08-20 加, 见 manager_projects.md §9.5
) ENGINE=InnoDB AUTO_INCREMENT=1463963329224705 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='项目-执行单位分配';
数据规模: 12 行 (实测 SELECT COUNT(*))。
6.2 biz_org DDL (实测 2026-08-20)
CREATE TABLE biz_org (
org_id bigint NOT NULL AUTO_INCREMENT,
org_name varchar(200) NOT NULL,
org_type varchar(20) NOT NULL DEFAULT 'sponsor' COMMENT 'sponsor赞助方/execution执行方',
business_nature varchar(20) DEFAULT NULL COMMENT '企业性质 私营/国营/中外合资/外资/其他',
address varchar(500) DEFAULT NULL COMMENT '单位地址',
tax_no varchar(50) DEFAULT NULL COMMENT '税号/统一社会信用代码',
contact_name varchar(50) DEFAULT NULL COMMENT '联系人',
contact_phone varchar(20) DEFAULT NULL COMMENT '联系电话',
intent_count int DEFAULT '0' COMMENT '意向项目数',
status varchar(20) DEFAULT '0' COMMENT '合作状态 合作中/待签约/已停用',
create_by varchar(64) DEFAULT '' COMMENT '创建者',
create_time datetime DEFAULT NULL,
update_by varchar(64) DEFAULT '' COMMENT '更新者',
update_time datetime DEFAULT NULL,
user_id bigint DEFAULT NULL COMMENT '主账号 sys_user.user_id (注册时自动关联, 一用户一类型一公司)',
PRIMARY KEY (org_id),
UNIQUE KEY uk_org_user_type (user_id, org_type),
KEY idx_org_type_status (org_type, status),
KEY idx_org_type_name (org_type, org_name),
KEY idx_org_user_type_name (user_id, org_type, org_name)
) ENGINE=InnoDB AUTO_INCREMENT=41 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='支持单位';
数据规模: 33 行 (实测)。COMMENT"支持单位" 实为 sponsor+executor 共用表, 见 §9.1。
6.3 biz_project (分配写入字段) DDL (实测)
sponsor_admin_user_id bigint DEFAULT NULL COMMENT '赞助方负责人用户ID (sys_user.user_id, role_type=sponsor)'
sponsor_admin_user_name varchar(200) DEFAULT NULL COMMENT '赞助方负责人用户名(冗余)'
total_sessions int DEFAULT NULL COMMENT '总场次/总期数'
total_amount decimal(15,2) DEFAULT '0.00' COMMENT '总金额'
manage_fee decimal(15,2) DEFAULT '0.00' COMMENT '管理费及税金'
submit_deadline_days int DEFAULT '0' COMMENT '提交材料截止天数'
6.4 Entity ↔ DB 字段对照
BizProjectAssign
| 实体字段 | 中文列名 (DDL COMMENT) | 类型 | 表字段 | 类型 | 一致? |
|---|---|---|---|---|---|
| assignId | 分配ID | String | assign_id | bigint | ⚠️ Java String vs DB bigint (走 SnowflakeId → Service 层 injectIfEmpty 转 String, 经全局 BaseEntity 反序列化兼容) |
| projectId | 项目ID | Long | project_id | bigint | ✅ |
| executionUnitId | 执行单位ID | Long | execution_unit_id | bigint | ✅ |
| execUserId | 执行方用户ID | Long | exec_user_id | bigint | ✅ |
| sessions | 分配场数 | Integer | sessions | int | ✅ |
| amount | 总金额 | BigDecimal | amount | decimal(15,2) | ✅ |
| remark | 备注 | String | remark | varchar(500) | ✅ |
| status | 状态 | String | status | char(1) | ✅ |
| createBy / updateBy | 创建者/更新者 | String | create_by / update_by | varchar(64) | ✅ |
| createTime / updateTime | 创建时间/更新时间 | Date | create_time / update_time | datetime | ✅ |
BizProject (assign 涉及字段)
| 实体字段 | 中文列名 | 类型 | 表字段 | 类型 | 一致? |
|---|---|---|---|---|---|
| sponsorAdminUserId | 赞助方负责人 | Long | sponsor_admin_user_id | bigint | ✅ |
| sponsorAdminUserName | 赞助方负责人用户名(冗余) | String | sponsor_admin_user_name | varchar(200) | ✅ |
| totalSessions | 总场次/总期数 | Long | total_sessions | int | ✅ (Java Long 兼容 SQL int) |
| totalAmount | 总金额 | BigDecimal | total_amount | decimal(15,2) | ✅ |
| manageFee | 管理费及税金 | BigDecimal | manage_fee | decimal(15,2) | ✅ |
| submitDeadlineDays | 提交材料截止天数 | Integer | submit_deadline_days | int | ✅ |
BizOrg (options 字段)
| 实体字段 | DB 字段 | 类型 | 一致? |
|---|---|---|---|
| orgId / userId / orgName / orgType | 同上 | Long / Long / String / String | ✅ |
| status | status | String (实际存 '0'/'1') vs DDL varchar(20) 注释说"合作中/待签约/已停用" |
⚠️ 值约定混乱: 注释允许多字符串, 但 BizOrgController.toggleStatus (L99-107) 严格限定 '0'/'1', BizProject 控制台代码无字符串判定, BizOrgController /toggleStatus 用 Map 接 body 防御误传 |
6.5 修订
v1 误判: biz_project_assign.assignId 类型 String vs bigint — 实际
SnowflakeId.injectIfEmpty在 Service 层处理, mapper.xml L55<if test="assignId != null and assignId != ''">assign_id,</if>用 String 也能写 bigint 列 (MyBatis 隐式转换), DB 是 AUTO_INCREMENT 实际由 SQL 自增, Java 端通常拿不到 insert id。实际写入靠 AUTO_INCREMENT, String 字段只是反序列化壳, 无 bug。
7. 索引检查
7.1 biz_project_assign
| Key | 列 | 用途覆盖 |
|---|---|---|
| PRIMARY | assign_id |
✅ |
| uk_project_unit | (project_id, execution_unit_id) |
✅ selectByProjectId EXPLAIN 实际命中 (实测) |
| idx_assign_project | project_id |
⚠️ 冗余: uk_project_unit 前缀是 project_id, 单列 project_id = ? 优化器优先选 uk (实测 EXPLAIN 命中 uk_project_unit) |
| idx_assign_unit | execution_unit_id |
⚠️ 当前页面未用, 待清理 (或保留给未来按执行方查分配) |
| idx_assign_exec_user | exec_user_id |
⚠️ 冗余: idx_assign_exec_user_project 前缀覆盖, 优化器在新索引后偏好复合 |
| idx_assign_exec_user_project | (exec_user_id, project_id) |
✅ projects selectList EXISTS+IN 复合 covering (be1c973, 见 manager_projects.md §9.5) |
索引冗余: idx_assign_project 和 idx_assign_exec_user 跟 uk_project_unit / idx_assign_exec_user_project 前缀重叠, 数据量小(<1k) 优化器可能优先小索引, 但索引维护成本是真实的。建议保留 (代码注释明确, 不影响功能)。
7.2 biz_org
| Key | 列 | 用途覆盖 |
|---|---|---|
| PRIMARY | org_id |
✅ |
| uk_org_user_type | (user_id, org_type) |
✅ options JOIN u.user_id = o.user_id (eq_ref) + "一用户一类型一公司" 唯一约束 |
| idx_org_type_status | (org_type, status) |
✅ options WHERE org_type = ? EXPLAIN 实测命中 |
| idx_org_type_name | (org_type, org_name) |
✅ org_name LIKE 兜底 |
| idx_org_user_type_name | (user_id, org_type, org_name) |
⚠️ 冗余: 3 列复合, EXPLAIN 实测优化器在小数据量选更小的 idx_org_type_status (2 列), 此索引未被命中 |
7.3 EXPLAIN 实测 (2026-08-20)
数据规模: biz_project_assign 12 / biz_org 33 / biz_project 12
| # | 查询 | 命中索引 | 备注 |
|---|---|---|---|
| Q1 | selectSponsorOrgOptions(orgName='test') |
idx_org_type_status (ref const) + PRIMARY sys_user (eq_ref) |
✅ filesort (ORDER BY org_id DESC), 33 行内可接受 |
| Q2 | selectExecutorOrgOptions(orgName='test') |
同上 + u.parent_user_id IS NULL Using where |
✅ 同 Q1 |
| Q3 | selectByProjectId(projectId=1) |
uk_project_unit (ref const) + filesort |
✅ 完美命中唯一索引 |
8. 与原型差异 (实现偏离)
8.1 实现新增 (原型没有)
| 项 | 说明 |
|---|---|
| 跳转子页面 (而非 dialog) | 原型是单页 (project-assign.html), Vue 独立路由 ManagerProjectsAssign.vue, 走 router.push 而非 dialog (Projects.vue:150 注释确认) |
| 实时校验栏 | 单条模式: 已分配场次/金额 vs 项目总场次/总金额实时对比, 超出红色高亮 + 警告 (assign-sessions-bar L38-46) |
| 批量分配模式 | 原型只有单项目, Vue 加 ?projectIds=1,2,3 批量入口 (Projects.vue:465-480), 校验按各项目自身 (ManagerProjectsAssign.vue:336-345) |
| 已选项 label 保留 | options 加载后, 已选 userId 不在结果里时单独按 userId 拉回加首位 (ManagerProjectsAssign.vue:181-185, 237-241), 防止切换搜索条件后 el-select label 变 ID |
| row 私有 _loading / _options / _timer | 每行独立 loading 状态, 防异步竞态 (L150, 228-247) |
8.2 原型有但实现缺失
| 项 | 原型位置 | 实现 | 严重度 |
|---|---|---|---|
| 顶部面包屑 "项目管理 > 项目分配" | project-assign.html:354-356 |
Vue: 首页 / 项目管理 / <span class="current">项目分配</span> (L3-6), ✅ 一致 (但首页是 Vue 默认面包屑) |
|
| 项目信息 6 字段 (编号/名称/形式/总场次/总金额/管理费及税金) | project-assign.html:367-392 |
el-descriptions 6 字段 ✅ 实装 (单条模式) | — |
| 支持方提示文字 "*支持单位只有一个, 支持模糊查找, 选择项" | project-assign.html:401 |
⚠️ Vue 未加红色提示 (L27-33 el-form-item 没 hint) | P3 |
| 执行方提示文字 (4 段) | project-assign.html:472-483 |
⚠️ Vue 完全省略 4 段红色提示 (含"可用金额=总金额×(1-管理费税费)-累计劳务-累计会务" 公式) | P2 |
| 截止时间提示 (3 段) | project-assign.html:493-495 |
⚠️ Vue 完全省略 3 段红色提示 (含"天数最大不能超过 100") | P2 |
| 截止天数 1~100 校验 | project-assign.html:646-650 if (deadline && (deadline < 1 || deadline > 100)) |
⚠️ Vue 无校验 (L88-90 el-input-number :min="0" 缺 max) |
P1 |
| 合同文件展示 (支持合同/执行合同, OA 同步) | project-assign.html:501-515 |
⚠️ Vue 完全省略 合同区 (support_contract_url / execute_contract_url 已 SELECT 到, 但页面没用) |
P1 |
| 总金额超额服务端校验 | project-assign.html:642-645 totalExecAmount > project.amount |
⚠️ Vue 只在客户端 sum 校验 (L139-148, 335-355), 服务端没二次校验 | P2 (客户端已拦, 风险低) |
| 执行方单位 = 1 时"删除"禁用 | project-assign.html:582-588 disabled = rows.length === 1 && i === 0 |
⚠️ Vue 永远可点 (L80 链接 el-button link type="danger"), 可一键删到空数组, 提交时 execRows.length === 0 会被 if (!valid.length) 拦住, 但 UI 不友好 |
P3 |
8.3 文字 / 标签差异
| 原型 | 实现 | 差异 |
|---|---|---|
| 项目分配 | 项目分配 | ✅ 完全一致 (标题、面包屑、表单 label) |
| 支持单位 | 支持方 | ⚠️ Vue 把"支持单位"改成"支持方" (ManagerProjectsAssign.vue:27), 跟其它页(支持单位管理/支持单位列表)不一致, 业务方混用 |
| 执行单位 | 执行方 | ⚠️ Vue 把"执行单位"改成"执行方" (L35 divider 文字 "执行方分配") |
| 提交材料截止时间 | 提交截止(天) | ⚠️ Vue 简化为"提交截止(天)", 失去"材料"和"时间"语义 |
| 总金额(元) | 总金额(元) (L22) | ✅ |
| 场数 | 场次 | ⚠️ Vue "场次", 原型"场数" — 同义, 但跟 biz_project.total_sessions (总场次/总期数) 中文对齐更顺 |
8.4 总结
整体方向: 业务核心对齐 + 扩展, 但视觉/提示/校验比原型薄弱。P1 缺失 2 项 (合同区 + 截止天数校验), P2 缺失 4 项 (提示文案 + 服务端校验), P3 缺失 3 项 (文案微调 + 删除按钮禁用)。建议优先级: P1 #1 截止天数校验 → P1 #2 合同区 → P2 提示文案 → P3 文案统一。
9. 表字段冗余 / 设计问题
9.1 派生 vs 持久化字段
biz_project_assign.exec_user_id: 持久化字段, 但实际只用来反查biz_org写execution_unit_id(BizProjectAssignServiceImpl.insert L47-56), 然后 exec_user_id 也保留用于列表 JOIN — 冗余但有查询价值biz_project.sponsor_admin_user_name: 持久化 (冗余), 用于列表展示避免 JOIN, 设计 OK
9.2 唯一约束语义检查
- ✅ biz_project_assign:
uk_project_unit (project_id, execution_unit_id)— 同一项目同一执行方只能分配一次, 合理 (跟"执行方总金额显示在执行方的项目列表"业务一致) - ⚠️ 但 saveAssigns 是
deleteByProjectId+ 逐条insert, uk 约束会在重新分配时触发 (如果先 insert 再 delete 老数据就违反 uk)。当前实现先 delete 再 insert, 安全 ✅。
9.3 业务闭环检查
- ✅ 项目分配 → biz_project.sponsor_admin_user_id 更新 → sponsor 端列表可见 (
selectSponsorList按此过滤) - ✅ 项目分配 → biz_project_assign 写入 → executor 端列表可见 (
selectExecutorList按 exec_user_id 过滤) - ⚠️ 管理费及税金 (manage_fee) 在本页面只展示, 不参与任何计算: 原型"可用金额=总金额×(1-管理费税费)-累计劳务-累计会务"公式未实现 (§10 #4 P2)
- ⚠️ biz_project.available_amount (可用金额) 字段在原型公式中出现, 但本页面没读也没改, 是否由其他流程 (会议执行?) 维护需查 — 暂列 P3
9.4 路由安全性
manager-projects-assign路由meta.role无显式, 走全局登录态校验, 任何已登录角色都可访问 — 与 admin-projects 同款 (router/index.js:28admin-projects 也指向 manager/Projects.vue), 业务上 OK 但没显式meta: { role: 'manager' }是隐性约定
9.5 索引冗余总结
- biz_project_assign:
idx_assign_project+idx_assign_exec_user跟 uk_project_unit + idx_assign_exec_user_project 前缀重叠, 优化器在小数据量偏好小索引, 但 EXPLAIN 实测都未被命中 (Q3 走 uk, Q1/Q2 是 SELECT biz_org), 跟 manager_projects.md §9.5 评估一致: 数据量小时保留无害, 生产数据增长后再 EVAL 是否 DROP
10. 待修复列表 (按优先级)
| # | 问题 | 文件:行 | 修复建议 | 严重度 |
|---|---|---|---|---|
| 1 | 截止天数无上限校验 (原型 max=100) | ManagerProjectsAssign.vue:88-90 |
<el-input-number :min="0" :max="100"> + on-change 校验 |
P1 |
| 2 | 合同文件区完全缺失 (支持合同/执行合同 + OA 同步) | ManagerProjectsAssign.vue 全文 |
加第 5 区块, 读 currentProject.supportContractUrl / executeContractUrl, 走 Preview 组件预览 (Projects.vue:146 已有 Preview 组件) |
P1 |
| 3 | 分配接口无 owner 校验, 任何登录角色可触发 PUT /business/project 改 sponsor | BizProjectController.saveAssigns (L170-182) + updateByPrimaryKey (L151-155) |
加 @PreAuthorize("hasAnyRole('admin','manager')") 或 service 层校验 SecurityUtils.getUserId() 在 manager role |
P1 |
| 4 | 管理费及税金 (manage_fee) 不参与可用金额计算 (原型公式未实现) | ManagerProjectsAssign.vue 实时校验栏 |
availableAmount = totalAmount * (1 - manageFee/totalAmount) - paidLabor - paidMeeting 实时显示 (原型公式) |
P2 |
| 5 | 截止天数无下限校验 (原型要求 ≥1 自然日) | 同 #1 | <el-input-number :min="1" :max="100"> |
P2 |
| 6 | 执行方超额校验只在客户端, 服务端无二次校验 | BizProjectController.saveAssigns |
service 层 if (sumAmount > project.totalAmount) throw |
P2 |
| 7 | 原型 4 段提示文案 (可用金额公式 / 选择后业务闭环 / 截止天数规则) 全部省略 | ManagerProjectsAssign.vue:27-91 |
加 el-alert type="warning" :closable="false" 或 p.hint-text 红字提示 |
P2 |
| 8 | "支持方" / "执行方" 跟其它页"支持单位" / "执行单位" 命名不一致 | ManagerProjectsAssign.vue:27, 35 |
统一为"支持单位" / "执行单位" (跟 biz_org.org_type 注释一致) | P3 |
| 9 | 执行方行删除按钮永远可点, 可删到空数组 | ManagerProjectsAssign.vue:80 |
当 execRows.length === 1 时禁用第一个删除按钮 (跟原型一致) |
P3 |
| 10 | 批量模式 (单条 sponsor 多项目共用) 不允许每项目独立 sponsor | ManagerProjectsAssign.vue:297-302, 365-374 |
加 "按各项目原 sponsor 填充" toggle, 或支持逐行 sponsor 覆盖 (低优先级, 业务需求未明) | P3 |
| 11 | 加载项目时无 loading 态 (顶部 6 字段空白闪烁) | ManagerProjectsAssign.vue:17-24 (el-descriptions 直接绑 currentProject) |
加 v-loading="loading" 或 skeleton |
P3 |
| 12 | 加载批量项目时, 任一失败静默跳过 (L294) | ManagerProjectsAssign.vue:282-295 |
console.warn 或弹窗提示"项目 X 加载失败已跳过" |
P3 |
| 13 | biz_org 索引 idx_org_user_type_name 跟 idx_org_type_name 前缀重叠, EXPLAIN 实测未命中 |
biz_org (DB) |
数据量小时保留无害, 生产数据增长后再 EVAL | P3 |
| 14 | biz_project_assign 索引 idx_assign_project 跟 uk_project_unit 前缀重叠 |
biz_project_assign (DB) |
同 #13 | P3 |
| 15 | biz_org.status 注释允许多字符串, 但 controller.toggleStatus 严格 '0'/'1' | BizOrgController.toggleStatus (L99-107) |
统一 status 字段约定 (删 DDL 注释, 或扩 enum) | P3 |
| 16 | manager-projects-assign 路由 meta.role 无显式, 走全局登录态 |
router/index.js:64 |
加 meta: { role: 'manager' } 跟其它 manager 路由一致 |
P3 |
11. 引用清单
前端
ry-vue3/src/layout/AdminLayout.vue:96— manager 菜单 (项目管理入口)ry-vue3/src/router/index.js:64— 路由manager-projects-assignry-vue3/src/views/manager/ManagerProjectsAssign.vue— 本审查对象 (441 行)ry-vue3/src/views/manager/Projects.vue:274-280—doAssign(单条入口)ry-vue3/src/views/manager/Projects.vue:465-480—openAssign(批量入口)ry-vue3/src/api/public.js—bizGet/bizUpdate(CRUD 包装)ry-vue3/src/api/system.js:28-38—listSponsorOrgs/listExecutorOrgs(下拉选项)
后端
ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizProjectController.java:139-189— 项目 getInfo / assigns GET/POST/DELETEry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizProjectService.java+BizProjectServiceImpl—getById/updateByPrimaryKeyry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizProject.java— 项目实体 (含sponsorAdminUserId / sponsorAdminUserName / submitDeadlineDays / manageFee / totalSessions / totalAmount)ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizProjectMapper.java+ xml — 4 个 select + updateByPrimaryKeyry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizOrgController.java:38-59—sponsorOptions/executorOptionsry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizOrgMapper.java:14-20+ xml L111-148 —selectSponsorOrgOptions/selectExecutorOrgOptionsry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizProjectAssign.java— 分配实体 (8 字段)ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizProjectAssignMapper.java+ xml — 分配 CRUDry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizProjectAssignServiceImpl.java:34-73— insert 时反查 biz_org 写 execution_unit_id
DB (实测 2026-08-20)
biz_project_assign(12 行): 12 列, 5 索引 (PK + uk + 3 复合/单列)biz_org(33 行): 16 列, 4 索引 (PK + uk + 2 复合/单列)biz_project(12 行, 36 列): 含 sponsor_admin_user_id / sponsor_admin_user_name / total_sessions / total_amount / manage_fee / submit_deadline_days
配置
ry-api/ruoyi-admin/src/main/resources/application-druid.yml— DB127.0.0.1:3306/guoju0808/root/cu2oh2co3
原型
proto/html/manager.html:206— 项目管理菜单入口 (iframe 路由)proto/html/components/project-assign.html— 项目分配独立页原型 (主)proto/html/components/project-assign-1.html— 项目分配独立页原型 (备份, 同源)
相关 (非本审查, 引用以辅助)
_self/manager_projects.md— 上游列表页审查报告 (本审查的调用方)biz_project_sponsor_assign(15 列) — 与本页面无关: 该表存"赞助方分配监察员" (sponsor → monitor), 跟"项目分配支持方" (project → sponsor) 是不同业务, 本页面不写该表