Files
guoju0808/_self/manager_projects_assign.md
郭庆泰andClaude d1c755971d docs: 加 _self/manager_projects_assign.md + manager_projects 修复记录
- 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>
2026-08-20 23:31:41 +08:00

31 KiB
Raw Permalink Blame History

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 行 dropdown assign 命令或 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.getInfobizProjectService.getById biz_project
加载项目基本信息 (批量) loadBatchProjects (L277-309) 同上, N 次串行调用 (无批量接口) biz_project × N
加载已分配执行方 loadAssigns (L200-222) GET /business/project/{id}/assignsBizProjectController.getAssignsbizProjectAssignService.selectByProjectId biz_project_assign
支持方下拉选项 searchSponsorOrgs (L172-191) GET /business/org/sponsorOptions?orgName=&userId=BizOrgController.sponsorOptionsbizOrgService.selectSponsorOrgOptions biz_org (org_type='sponsor') + sys_user
执行方下拉选项 (行内) searchExecutors (L227-248) GET /business/org/executorOptions?orgName=&userId=BizOrgController.executorOptionsbizOrgService.selectExecutorOrgOptions biz_org (org_type='executor') + sys_user (MAIN only)
保存支持方 + 截止天数 submitAssign 中 PUT (L365-374, 388-393) PUT /business/projectbizProjectService.updateByPrimaryKey biz_project.sponsor_admin_user_id / sponsor_admin_user_name / submit_deadline_days
保存执行方分配 (单条) submitAssign 中 POST (L394-399) POST /business/project/{id}/assignsBizProjectController.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.getInfoBizProjectServiceImpl.getByIdBizProjectMapper.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}/assignsBizProjectController.getAssignsBizProjectAssignMapper.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_projectidx_assign_exec_useruk_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_orgexecution_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:28 admin-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_nameidx_org_type_name 前缀重叠, EXPLAIN 实测未命中 biz_org (DB) 数据量小时保留无害, 生产数据增长后再 EVAL P3
14 biz_project_assign 索引 idx_assign_projectuk_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-assign
  • ry-vue3/src/views/manager/ManagerProjectsAssign.vue — 本审查对象 (441 行)
  • ry-vue3/src/views/manager/Projects.vue:274-280doAssign (单条入口)
  • ry-vue3/src/views/manager/Projects.vue:465-480openAssign (批量入口)
  • ry-vue3/src/api/public.jsbizGet/bizUpdate (CRUD 包装)
  • ry-vue3/src/api/system.js:28-38listSponsorOrgs/listExecutorOrgs (下拉选项)

后端

  • ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizProjectController.java:139-189 — 项目 getInfo / assigns GET/POST/DELETE
  • ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/IBizProjectService.java + BizProjectServiceImplgetById/updateByPrimaryKey
  • ry-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 + updateByPrimaryKey
  • ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizOrgController.java:38-59sponsorOptions/executorOptions
  • ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizOrgMapper.java:14-20 + xml L111-148 — selectSponsorOrgOptions/selectExecutorOrgOptions
  • ry-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 — 分配 CRUD
  • ry-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 — DB 127.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) 是不同业务, 本页面不写该表