- BizOrgMapper: selectSponsorOrgOptions/selectExecutorOrgOptions 注释 写 "走 idx_org_user_type_name", EXPLAIN 实际命中 idx_org_type_status (优化器小数据下选小索引), 改为实测索引 + Using where 描述 - manager_projects.md §9.3: biz_project_assign 唯一约束状态标 ✅ (SHOW INDEX 实测有 uk_project_unit, doc 老版本误标 ❌) - manager_projects.md §9.5: 新增索引分析章节, 记录 8 个关键查询 EXPLAIN 命中 + 已加/评估后未加的索引决策 DB 实际改动 (已落库, 无 .sql 文件): - ADD INDEX idx_assign_exec_user_project (exec_user_id, project_id) on biz_project_assign — 服务机机构 EXISTS+IN covering 优化 - DROP 临时测试的 idx_org_type_id — 优化器未采纳, 33 行数据量不值
489 lines
34 KiB
Markdown
489 lines
34 KiB
Markdown
# manager/projects — 端到端技术审查
|
||
|
||
> **审查时间**: 2026-08-18
|
||
> **审查范围**: `ry-vue3/src/views/manager/Projects.vue` (主列表) + 后端 BizProject 全链路 + DB 实测
|
||
> **对比原型**: `proto/html/components/project-manage.html`
|
||
|
||
---
|
||
|
||
## 修复记录
|
||
|
||
| 日期 | 章节 | 修复内容 |
|
||
|---|---|---|
|
||
| 2026-08-18 | §6.3 / §9.1 | **BizProject.projectId** String → Long (9 文件: domain / IBizProjectService / BizProjectServiceImpl / BizProjectMapper.java+xml / BizProjectController / BizExecutionIntentController / BizPublicityIntentController / BizPublicController)。SnowflakeId.injectIfEmpty 反射依赖 setter(String), Long 后会抛 NoSuchMethodException 被吞掉 (SnowflakeId.java:30-31), 安全 — 但显式删掉更干净 (biz_project 用 AUTO_INCREMENT)。 |
|
||
| 2026-08-18 | §6.3 / §9.1 | **BizProject.startTime / endTime** String → Date, 加 `@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")` (跟 BizMeeting 一致)。Mapper XML 8 处 `<if test="... != null and ... != ''">` 改为 `<if test="... != null">` (Date 无 isEmpty, OGNL 会报错)。前端 el-date-picker value-format "YYYY-MM-DD HH:mm:ss" 双向兼容, 零前端改动。 |
|
||
| 2026-08-18 | §10 #7 | **`batchScoreRows` 死代码删除** (Projects.vue:501)。注释说"保留兼容旧引用", 但 grep 全文 0 个引用, 纯死变量。 |
|
||
| 2026-08-18 | §10 #5 | **生产代码残留 console.info/error 4 处删除** (Projects.vue:299, 302, 306, 874)。`load()` 进/出/失败各一处 + 路由 watch reload 一处, 共 4 行, 全部删除。 |
|
||
| 2026-08-18 | §10 #9 | **日期选择器 value-format 对齐 type** (Projects.vue:17, 19)。原 `type="date"` + `value-format="YYYY-MM-DD HH:mm:ss"` 矛盾 (date picker 给不出 HH:mm:ss), 改 value-format 为 `YYYY-MM-DD`, MySQL 对 datetime 列隐式转换。 |
|
||
| 2026-08-18 | §7.1 / §10 #12 | **DB: biz_project 加索引 `idx_sponsor_admin_user_id`**。ALTER TABLE biz_project ADD INDEX idx_sponsor_admin_user_id (sponsor_admin_user_id)。EXPLAIN 验证: type=ref, Extra=Using index。 |
|
||
| 2026-08-18 | §7.1 / §10 #13 | **DB: biz_project 加复合索引 `idx_start_end_time (start_time, end_time)`**。比 2 个单列索引更适合 `WHERE start_time >= ? AND end_time <= ?` 的范围查询。EXPLAIN 验证: type=range, Extra=Using where; Using index。 |
|
||
| 2026-08-18 | §7.2 / §10 #14 | **DB: biz_project_assign 加 UNIQUE `uk_project_unit (project_id, execution_unit_id)`**。先查 11 行 0 重复, 安全。防重复分配。注意: `biz_project_assign` 是 **项目→executor (执行方)** 分配表, 不是 sponsor; sponsor 走独立的 `biz_project_sponsor_assign`。 |
|
||
|
||
---
|
||
|
||
## 0. 4 跳定位 (铁律: 不按名字猜)
|
||
|
||
| 跳 | 文件:行 | 结果 |
|
||
|---|---|---|
|
||
| ① 角色菜单 | `ry-vue3/src/layout/AdminLayout.vue:98` | `path: '/manager/projects', title: '项目管理'` ✓ |
|
||
| ② 路由 | `ry-vue3/src/router/index.js:65` | `component: () => import('@/views/manager/Projects.vue')` ✓ |
|
||
| ③ 组件 | `ry-vue3/src/views/manager/Projects.vue` | 881 行,本审查对象 |
|
||
| ④ 原型 | `proto/html/manager.html:206` | `<a data-page="components/project-manage.html">` ✓ |
|
||
|
||
---
|
||
|
||
## 1. 主要功能 + 可见性
|
||
|
||
### 1.1 主要功能
|
||
|
||
| 功能 | 前端入口 | 后端接口 | 涉及表 |
|
||
|---|---|---|---|
|
||
| 项目列表(分页+筛选) | `manager/Projects.vue` 表头筛选 + `load()` | `GET /business/project/list` → `BizProjectController.list` → `BizProjectService.selectList` | biz_project |
|
||
| 单条/批量结题 | `onToggleStatus` (批量走 `openBatch('close')`) | `PUT /business/project` → `bizProjectService.updateByPrimaryKey` (只改 `is_finished='1'`) | biz_project.is_finished |
|
||
| 单条/批量开通 | `openActivate` / `openBatch('activate')` | `PUT /business/project` (改 `end_time`) | biz_project.end_time |
|
||
| 单条/批量执行单位评分 | `openSingleScore` / `openBatch('score')` | `POST /business/project/rate` (4 维度) + `PUT /business/project` (写聚合分) | biz_project_rating + biz_project.manager_score |
|
||
| 项目分配(单条+批量) | `doAssign` / `openAssign` | `PUT /business/project` (改 sponsor) + `POST /business/project/{id}/assigns` (先删后插) | biz_project + biz_project_assign |
|
||
| 删除公告(单条) | `confirmDeleteAnn` | `PUT /business/project` (清空 publishUrl/invitationUrl/supportLetterUrl/noticeUrl/scheduleUrl + is_published='0') | biz_project |
|
||
| 导出报名专家 | `exportExperts` | `POST /business/executionIntent/export?projectNo=...` | biz_execution_intent |
|
||
| 新建/编辑/查看 | 跳独立页 `/manager/projects/new`、`/edit/:id`、`/detail/:id` | 同 | biz_project |
|
||
|
||
### 1.2 可见性(三层隔离)
|
||
|
||
| 层 | 来源 | 校验字段 |
|
||
|---|---|---|
|
||
| 前端菜单 | `AdminLayout.vue:98` | 只在 `manager` 角色菜单出现 |
|
||
| 路由守卫 | `router/index.js:61` `meta: { role: 'manager' }` | 路由 meta.role |
|
||
| 后端 | `BizProjectController.list` → `selectList` **无 user_id 过滤**(manager 可看所有项目) | 无 |
|
||
|
||
> ⚠️ **manager 是全局视图角色**(合规管理员), 看所有项目不过滤 — 设计正确
|
||
>
|
||
> **其他角色访问**:
|
||
> - sponsor → `GET /business/project/sponsorList` (按 `sponsor_admin_user_id = uid` 过滤)
|
||
> - executor → `GET /business/project/executorList` (按 `biz_project_assign` 关联过滤)
|
||
> - 这两个**不会进** manager/Projects.vue,但共用 controller
|
||
|
||
---
|
||
|
||
## 2. 筛选项 (filter-form)
|
||
|
||
| UI label | 控件 | v-model | 后端 SQL | 命中表.字段 | 选项来源 | 原型对照 |
|
||
|---|---|---|---|---|---|---|
|
||
| 支持单位 | `el-select` (multiple+remote) | `q.sponsorAdminUserIds` (List<Long>) | `selectList` line 167-173 `sponsor_admin_user_id IN <foreach>` | biz_project.sponsor_admin_user_id | `/business/org/sponsorOptions` (LIMIT 5) | ✅ 原型有 |
|
||
| 服务机构 | `el-select` (multiple+remote) | `q.execAdminUserIds` (List<Long>) | `selectList` line 173-181 `EXISTS (biz_project_assign WHERE exec_user_id IN <foreach>)` | biz_project_assign.exec_user_id (MAIN user_id) | `/business/org/executorOptions` (LIMIT 5) | ✅ 原型有 |
|
||
| 项目编号 | `el-input` | `q.projectNo` | line 156 `project_no LIKE concat('%',?, '%')` | biz_project.project_no | — | ✅ 原型有 |
|
||
| 项目名称 | `el-input` | `q.projectName` | line 157 `project_name LIKE concat('%',?, '%')` | biz_project.project_name | — | ✅ 原型有 |
|
||
| 项目形式 | `el-select` | `q.projectForm` | line 158 `project_form = ?` | biz_project.project_form | **写死**: 线上/线下/线上+线下/其他 | ✅ 原型有 |
|
||
| 项目时间(起/止) | `el-date-picker` type="date" + `value-format="YYYY-MM-DD HH:mm:ss"` | `q.startTime/endTime` | line 93-94 `start_time >= ? AND end_time <= ?` (sponsorList); line 134-135 同 | biz_project.start_time/end_time | — | ✅ 原型有 |
|
||
| 是否结题 | `el-select` | `q.isFinished` | line 179 `is_finished = ?` | biz_project.is_finished | **写死**: 已结题→'1' / 未结题→'0' | ✅ 原型有 |
|
||
| 是否结算 | `el-select` | `q.isSettled` | line 180 `is_settled = ?` | biz_project.is_settled | **写死**: 已结算→'Y' / 未结算→'N' | ⚠️ 原型**无** (实现新增) |
|
||
| 评价得分 | `el-input` | `q.managerScore` | ❌ **未生效** — 后端 `selectList` 无 `managerScore` 过滤 | — | — | ✅ 原型有 |
|
||
|
||
> ⚠️ **重要 bug**:`managerScore` 筛选项前端展示 + 前端 reset 也保留字段,但**后端 selectList 完全没读这个参数**,筛选无效
|
||
> 另见 `selectSponsorList` 和 `selectExecutorList` 也都没有 `managerScore` 过滤
|
||
|
||
---
|
||
|
||
## 3. 工具栏按钮 (toolbar)
|
||
|
||
| 按钮 | 功能 | 接口 | 涉及表 | 原型对照 |
|
||
|---|---|---|---|---|
|
||
| 新建项目 | 跳转独立页 | `router.push('/manager/projects/new')` → `ProjectsNew.vue` | biz_project | ✅ 原型有 |
|
||
| 项目分配 | 弹 dialog (单条或批量,按勾选数) | `PUT /business/project` (改 sponsor_admin_user_id) + `POST /business/project/{id}/assigns` | biz_project + biz_project_assign | ✅ 原型有 |
|
||
| 批量评分 | 弹 dialog,共用 4 维度评分 | `PUT /business/project` (managerScore) + `POST /business/project/rate` × N | biz_project + biz_project_rating | ✅ 原型有 |
|
||
| 批量结题 | 弹 dialog,确认后批量改 is_finished='1' | `PUT /business/project` × N | biz_project.is_finished | ✅ 原型有 |
|
||
| 批量开通 | 弹 dialog,选截止时间,批量改 end_time | `PUT /business/project` × N | biz_project.end_time | ✅ 原型有 |
|
||
| 导出 (3 个按钮) | 项目/评价/已报名专家导出 | `exportProjects` / `exportEval` / `exportExperts` | — | ⚠️ 前两个是 stub,仅 `exportExperts` 实装 |
|
||
|
||
> ⚠️ **stub 函数**:`exportProjects` (line 347) / `exportEval` (line 348) 仅弹 `ElMessage.info('开发中')`
|
||
> ⚠️ **遗漏**:原型有 3 个导出按钮 (导出/项目评价导出/已报名专家信息导出), 实装只有 1 个
|
||
> ⚠️ **额外按钮 (实现新增)**: 删除公告(原型的 #7), 不是"扩展"而是"实现"
|
||
|
||
---
|
||
|
||
## 4. 表格 (el-table)
|
||
|
||
### 4.1 数据来源 SQL(manager 通用列表 `selectList`)
|
||
|
||
```sql
|
||
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_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,
|
||
o.org_name as sponsor_org_name,
|
||
lu.user_name as lead_user_name,
|
||
(SELECT group_concat(distinct o2.org_name separator ',')
|
||
FROM biz_project_assign bpa
|
||
JOIN biz_org o2 ON o2.org_id = bpa.execution_unit_id AND o2.org_type = 'executor'
|
||
WHERE bpa.project_id = p.project_id) AS exec_org_names
|
||
FROM biz_project p
|
||
LEFT JOIN biz_org o ON o.user_id = p.sponsor_admin_user_id AND o.org_type = 'sponsor'
|
||
LEFT JOIN sys_user lu ON lu.user_id = p.lead_user_id
|
||
[WHERE 动态过滤...]
|
||
```
|
||
|
||
### 4.2 列映射
|
||
|
||
| 列 | row.* | DB 表.字段 | 原型对照 |
|
||
|---|---|---|---|
|
||
| 复选框 | — | — | ✅ |
|
||
| 项目编号 | `row.projectNo` | biz_project.project_no | ✅ |
|
||
| 项目名称 | `row.projectName` | biz_project.project_name | ✅ |
|
||
| 总场次/总期数 | `row.totalSessions` | biz_project.total_sessions | ✅ |
|
||
| 已执行 | `row.doneSessions` | biz_project.done_sessions | ✅ |
|
||
| 未执行 | `row.todoSessions` | biz_project.todo_sessions | ✅ |
|
||
| 总金额 | `row.totalAmount` (fmtMoney) | biz_project.total_amount | ✅ |
|
||
| 可用金额 | `row.availableAmount` (fmtMoney) | biz_project.available_amount | ✅ |
|
||
| 已支付劳务费 | `row.paidLaborAmount` (fmtMoney) | biz_project.paid_labor_amount | ✅ |
|
||
| 已支付会务费 | `row.paidMeetingAmount` (fmtMoney) | biz_project.paid_meeting_amount | ✅ |
|
||
| 执行单位得分(合规) | `row.managerScore` | biz_project.manager_score | ✅ |
|
||
| 执行单位得分(支持) | `row.sponsorScore` | biz_project.sponsor_score | ✅ |
|
||
| 支持单位 | `row.sponsorOrgName` | biz_org.org_name (JOIN) | ✅ |
|
||
| 服务机构 | `row.execOrgNames` | GROUP_CONCAT of biz_org.org_name | ✅ |
|
||
| 项目形式 | `row.projectForm` | biz_project.project_form | ✅ |
|
||
| 是否结题 | `row.isFinished` (tag 化) | biz_project.is_finished | ✅ |
|
||
| 项目开始时间 | `row.startTime` (fmtDate) | biz_project.start_time | ✅ |
|
||
| 项目结束时间 | `row.endTime` (fmtDate) | biz_project.end_time | ✅ |
|
||
| 操作 | 6 个 link + dropdown | — | ✅ 8 项操作齐全(建会/分配/结题/查看/编辑/开通/删除公告/执行单位评分) |
|
||
|
||
---
|
||
|
||
## 5. 行内操作按钮
|
||
|
||
| 按钮 | 函数 | 接口 | 后端动作 | 涉及表.字段 | 原型对照 |
|
||
|---|---|---|---|---|---|
|
||
| 查看 | `viewDetail` | `router.push('/manager/projects/detail/${row.projectId}')` | (独立页) | — | ✅ |
|
||
| 编辑 | `doEdit` | `router.push('/manager/projects/edit/${row.projectId}')` | (独立页 ProjectsNew.vue) | biz_project (全字段) | ✅ |
|
||
| 建会 | `doCreateMeeting` | `router.push('/manager/meetings/new?projectId=${row.projectId}')` | (独立页 MeetingNew.vue) | — | ✅ |
|
||
| 项目分配 | `doAssign` | 同 `openAssign` | — | — | ✅ |
|
||
| 结题 (单条) | `openClose` + `confirmClose` | `PUT /business/project` `{projectId, isFinished: '1'}` | `bizProjectService.updateByPrimaryKey` | biz_project.is_finished='1' | ✅ |
|
||
| 开通 (单条) | `openActivate` + `confirmActivate` | `PUT /business/project` `{projectId, isFinished: '0'}` ⚠️ **字段写错了** | 同上 | biz_project.is_finished='0' | ✅ 但**实现不对** |
|
||
| 删除公告 | `openDeleteAnnouncement` + `confirmDeleteAnn` | `PUT /business/project` 清空 5 个 URL + `isPublished: '0'` | 同上 | biz_project.{publish_url,invitation_url,support_letter_url,notice_url,schedule_url}='\0' + is_published='0' | ✅ |
|
||
| 执行单位评分 (单条) | `openSingleScore` + `confirmSingleScore` | `PUT /business/project` `{managerScore}` + `POST /business/project/rate` | `bizProjectRatingService.upsertRating` | biz_project.manager_score + biz_project_rating (新行或更新) | ✅ |
|
||
| 导出报名专家 | `exportExperts` | `POST /business/executionIntent/export?projectNo=...` | (单独执行方意图表) | biz_execution_intent | ✅ |
|
||
| 批量评分 | `openBatch('score')` + `confirmBatch` | `PUT + POST /rate` × N | 同单条,按勾选遍历 | biz_project + biz_project_rating | ✅ |
|
||
| 批量结题 | `openBatch('close')` + `confirmBatch` | `PUT /business/project` `{isFinished: '1'}` × N | 同单条 | biz_project.is_finished | ✅ |
|
||
| 批量开通 | `openBatch('activate')` + `confirmBatch` | `PUT /business/project` `{endTime: activateDeadline}` × N | 同单条 | biz_project.end_time | ✅ |
|
||
|
||
> ⚠️ **🐛 Bug**: 单条"开通"(confirmActivate line 397-401) **改了 `is_finished='0'` 而不是 `end_time`** — 应该跟批量开通一致改 `end_time`。改 is_finished='0' 等同于"取消结题",跟"开通"语义不符
|
||
>
|
||
> 🟢 **正确性**: 评分走 `biz_project_rating` 中间表 + `upsertRating` SQL 是 `(project_id, rater_id, rater_role)` 复合唯一,符合"一人评一次"的语义
|
||
|
||
---
|
||
|
||
## 6. Java Entity ↔ 数据库表 一致性
|
||
|
||
### 6.1 biz_project 主表 DDL (实测)
|
||
|
||
```sql
|
||
CREATE TABLE `biz_project` (
|
||
`project_id` bigint NOT NULL AUTO_INCREMENT,
|
||
`project_no` varchar(50) NOT NULL COMMENT '项目编号 ZH-2026-658',
|
||
`project_name` varchar(200) NOT NULL COMMENT '项目名称',
|
||
`project_form` varchar(20) DEFAULT NULL COMMENT '项目形式 线上/线下/线上+线下/其他',
|
||
`total_sessions` int DEFAULT '0',
|
||
`done_sessions` int DEFAULT '0',
|
||
`todo_sessions` int DEFAULT '0',
|
||
`total_amount` decimal(15,2) DEFAULT '0.00',
|
||
`available_amount` decimal(15,2) DEFAULT '0.00',
|
||
`paid_labor_amount` decimal(15,2) DEFAULT '0.00',
|
||
`paid_meeting_amount` decimal(15,2) DEFAULT '0.00',
|
||
`manage_fee` decimal(15,2) DEFAULT '0.00' COMMENT '管理费及税金',
|
||
`is_finished` char(1) DEFAULT '0' COMMENT '是否结题 0否 1是',
|
||
`is_settled` char(1) DEFAULT 'N' COMMENT '是否结算 N否 Y是',
|
||
`manager_score` decimal(3,1) DEFAULT NULL,
|
||
`sponsor_admin_user_id` bigint DEFAULT NULL,
|
||
`sponsor_admin_user_name` varchar(200) DEFAULT NULL,
|
||
`lead_user_id` bigint DEFAULT NULL,
|
||
`is_bid_project` char(1) DEFAULT 'N' COMMENT '是否招标项目 Y/N',
|
||
`start_time` datetime DEFAULT NULL,
|
||
`end_time` datetime DEFAULT NULL,
|
||
`submit_deadline_days` int DEFAULT '0',
|
||
`support_contract_url` varchar(500) DEFAULT NULL,
|
||
`execute_contract_url` varchar(500) DEFAULT NULL,
|
||
`invitation_url` varchar(500) DEFAULT NULL,
|
||
`support_letter_url` varchar(500) DEFAULT NULL,
|
||
`publish_url` varchar(500) DEFAULT NULL,
|
||
`create_by` varchar(64) DEFAULT '',
|
||
`create_time` datetime DEFAULT NULL,
|
||
`update_by` varchar(64) DEFAULT '',
|
||
`update_time` datetime DEFAULT NULL,
|
||
`sponsor_score` decimal(3,1) DEFAULT NULL,
|
||
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`)
|
||
) ENGINE=InnoDB
|
||
```
|
||
|
||
### 6.2 biz_project_assign / biz_project_rating DDL (实测)
|
||
|
||
**biz_project_assign** 索引:PRIMARY(assign_id) + idx_assign_project / idx_assign_unit / idx_assign_exec_user
|
||
|
||
**biz_project_rating** 索引:
|
||
- PRIMARY (rating_id)
|
||
- UNIQUE `uk_rating` (project_id, rater_id, rater_role) ✅ 一人评一次
|
||
- idx_pr_project, idx_pr_rater, idx_pr_rating_time
|
||
|
||
### 6.3 Entity ↔ 表字段对照 (BizProject.java)
|
||
|
||
| 实体字段 | 中文列名 (DB COMMENT) | 类型 | 表字段 | 类型 | 一致? |
|
||
|---|---|---|---|---|---|
|
||
| projectId | 项目ID | Long | project_id | bigint | ✅ 已修 (2026-08-18, 见 §修复记录) |
|
||
| projectNo | 项目编号 ZH-2026-658 | String | project_no | varchar(50) | ✅ |
|
||
| projectName | 项目名称 | String | project_name | varchar(200) | ✅ |
|
||
| totalSessions | 总场次/总期数 | Long | total_sessions | int | ✅ |
|
||
| doneSessions | 已执行 | Long | done_sessions | int | ✅ |
|
||
| todoSessions | 未执行 | Long | todo_sessions | int | ✅ |
|
||
| totalAmount | 总金额 | BigDecimal | total_amount | decimal(15,2) | ✅ |
|
||
| availableAmount | 可用金额 | BigDecimal | available_amount | decimal(15,2) | ✅ |
|
||
| paidLaborAmount | 已支付劳务费 | BigDecimal | paid_labor_amount | decimal(15,2) | ✅ |
|
||
| paidMeetingAmount | 已支付会务费 | BigDecimal | paid_meeting_amount | decimal(15,2) | ✅ |
|
||
| manageFee | 管理费及税金 | BigDecimal | manage_fee | decimal(15,2) | ✅ |
|
||
| managerScore | 合规管理员评均分 | BigDecimal | manager_score | decimal(3,1) | ✅ |
|
||
| sponsorScore | 支持方评均分 | BigDecimal | sponsor_score | decimal(3,1) | ✅ |
|
||
| sponsorAdminUserName | 赞助方负责人用户名(冗余) | String | sponsor_admin_user_name | varchar(200) | ✅ |
|
||
| sponsorAdminUserId | 赞助方负责人用户ID | Long | sponsor_admin_user_id | bigint | ✅ |
|
||
| leadUserId | 项目负责人 user_id | Long | lead_user_id | bigint | ✅ |
|
||
| projectForm | 项目形式 线上/线下/线上+线下/其他 | String | project_form | varchar(20) | ✅ |
|
||
| isFinished | 是否结题 0否 1是 | String | is_finished | char(1) | ✅ |
|
||
| isSettled | 是否结算 N否 Y是 | String | is_settled | char(1) | ✅ |
|
||
| isBidProject | 是否招标项目 Y/N (默认 N) | String | is_bid_project | char(1) | ✅ |
|
||
| startTime | 项目开始时间 | Date | start_time | datetime | ✅ 已修 (2026-08-18, 加 @JsonFormat, 见 §修复记录) |
|
||
| endTime | 项目结束时间 | Date | end_time | datetime | ✅ 已修 (2026-08-18, 同上) |
|
||
| submitDeadlineDays | 提交材料截止天数 | Integer | submit_deadline_days | int | ✅ |
|
||
| supportContractUrl | 支持合同文件URL | String | support_contract_url | varchar(500) | ✅ |
|
||
| executeContractUrl | 执行合同文件URL | String | execute_contract_url | varchar(500) | ✅ |
|
||
| invitationUrl | 邀请函文件URL | String | invitation_url | varchar(500) | ✅ |
|
||
| supportLetterUrl | 支持函文件URL | String | support_letter_url | varchar(500) | ✅ |
|
||
| publishUrl | 已发布公告URL | String | publish_url | varchar(500) | ✅ |
|
||
| createBy/createTime | 创建者 / 创建时间 | | (BaseEntity) | | ✅ |
|
||
| updateBy/updateTime | 更新者 / 更新时间 | | (BaseEntity) | | ✅ |
|
||
| assignedSessions | (派生) | Long | ❌ **无对应字段** | — | ⚠️ **派生字段**,只在 executor 端 SQL 用 |
|
||
| assignedAmount | (派生) | BigDecimal | ❌ **无对应字段** | — | ⚠️ 同上 |
|
||
| sponsorOrgName | (派生) | String | ❌ **无对应字段** | — | ⚠️ JOIN biz_org 派生 |
|
||
| execOrgNames | (派生) | String | ❌ **无对应字段** | — | ⚠️ GROUP_CONCAT 派生, 仅展示 |
|
||
| execAdminUserIds | (查询参数) | List\<Long\> | ❌ **无对应字段** | biz_project_assign.exec_user_id | ✅ 多选 select 透传, 走 EXISTS + IN 精确查询 |
|
||
| leadUserName | (派生) | String | ❌ **无对应字段** | — | ⚠️ JOIN sys_user 派生 |
|
||
| noticeUrl | (DDL 中不存在) | String | ❌ **DDL 中不存在** | — | 🐛 **实体有但表无** |
|
||
| scheduleUrl | (DDL 中不存在) | String | ❌ **DDL 中不存在** | — | 🐛 同上 |
|
||
| isPublished | (DDL 中不存在) | String | ❌ **DDL 中不存在** | — | 🐛 同上,被 `confirmDeleteAnn` 当字段更新 |
|
||
| publishTime | (DDL 中不存在) | Date | ❌ **DDL 中不存在** | — | 🐛 同上 |
|
||
| announcementType | (DDL 中不存在) | String | ❌ **DDL 中不存在** | — | � 同上 |
|
||
| sponsorRating, sponsorQ1-4, sponsorRemark | (DDL 中不存在) | | ❌ **DDL 中不存在** | — | 🐛 残留支持方评分字段,未使用 |
|
||
|
||
### 修订标记
|
||
|
||
> **修订**:`is_published` / `notice_url` / `schedule_url` / `publish_time` / `announcement_type` 这些字段**只在 BizProject.java 实体里有,DDL 实测不存在**。
|
||
> 后果:`confirmDeleteAnn` 调 `bizUpdate('project', { publishUrl, invitationUrl, supportLetterUrl, noticeUrl, scheduleUrl, isPublished: '0' })`,后端 mapper 的 `updateByPrimaryKey` 的 `<if test="...">` 会因为没传值而**不更新** notice_url/schedule_url/is_published,只清空前 3 个。**功能部分失效**(第 5 章结论需修正)。
|
||
|
||
---
|
||
|
||
## 7. 索引检查
|
||
|
||
### 7.1 biz_project 索引
|
||
|
||
| Key | 列 | 用途覆盖 |
|
||
|---|---|---|
|
||
| PRIMARY | project_id | ✅ ORDER BY, 主键 |
|
||
| 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 | ✅ `selectSponsorList` WHERE `sponsor_admin_user_id = ?` |
|
||
| idx_start_end_time | (start_time, end_time) | ✅ 范围查询 `start_time >= ? AND end_time <= ?`,EXPLAIN 验证 type=range |
|
||
| ❌ **缺** | lead_user_id | ⚠️ 主表没用,但 JOIN sys_user 用 — 可接受,小数据量 |
|
||
|
||
### 7.2 biz_project_assign 索引
|
||
|
||
| Key | 列 | 用途覆盖 |
|
||
|---|---|---|
|
||
| PRIMARY | assign_id | ✅ |
|
||
| idx_assign_project | project_id | ✅ `SELECT WHERE project_id = ?` |
|
||
| idx_assign_unit | execution_unit_id | ✅ |
|
||
| idx_assign_exec_user | exec_user_id | ✅ |
|
||
| **uk_project_unit** | (project_id, execution_unit_id) UNIQUE | ✅ 防重复分配 (实测 11 行 0 重复, 安全) |
|
||
|
||
### 7.3 biz_project_rating 索引
|
||
|
||
| Key | 列 | 用途覆盖 |
|
||
|---|---|---|
|
||
| PRIMARY | rating_id | ✅ |
|
||
| **uk_rating** | (project_id, rater_id, rater_role) UNIQUE | ✅ `upsertRating` 用 ON DUPLICATE KEY,完美 |
|
||
| idx_pr_project | project_id | ✅ `SELECT WHERE project_id = ?` |
|
||
| idx_pr_rater | rater_id | ✅ |
|
||
| idx_pr_rating_time | rating_time | ✅ 按时间排序 |
|
||
|
||
> **结论**:rating 表索引设计优秀(uk_rating 三元组唯一约束)。project 表索引已补齐 (7 个, 涵盖所有 WHERE 列)。assign 表唯一约束到位 (uk_project_unit 防重复分配)。
|
||
|
||
---
|
||
|
||
## 8. 与原型差异
|
||
|
||
### 8.1 实现新增 (原型没有)
|
||
|
||
| 项 | 位置 | 说明 |
|
||
|---|---|---|
|
||
| 项目分配 dialog | `assignOpen` (line 119) | 原型 `project-assign.html` 是独立页,实现合并到本 dialog,功能更全 (支持方下拉 + 总场次/金额校验) |
|
||
| 评分明细反显 | `openSingleScore` (line 424-449) | 原型无,实装调用 `/business/project/ratings` 反显当前管理员历史评分 |
|
||
| 评分默认值 | `batchScoreForm default=5`, `singleScoreForm default=0` | 原型无 |
|
||
| 批量开通截止时间字段 | `activateDeadline` | 原型批量开通仅"确认"无时间字段,实装加了截止时间 |
|
||
| isSettled 筛选 | line 26-30 | 原型无"是否结算"筛选项 |
|
||
|
||
### 8.2 原型有但实现缺失
|
||
|
||
| 原型项 | 实现状态 |
|
||
|---|---|
|
||
| 导出 (项目) | 🐛 **stub** — `exportProjects` 仅 `ElMessage.info('开发中')` |
|
||
| 项目评价导出 | 🐛 **stub** — `exportEval` 仅 `ElMessage.info('开发中')` |
|
||
| 已报名专家信息导出 | ✅ **实装** — `exportExperts` POST `executionIntent/export` |
|
||
| `managerScore` 评价得分筛选 | 🐛 **后端未生效** — 前端有字段,后端 selectList 无此过滤 |
|
||
|
||
### 8.3 文案/标签差异
|
||
|
||
| 原型 | 实现 | 差异 |
|
||
|---|---|---|
|
||
| 评分标准输入框 placeholder "1~5" | `el-input-number :min=0 :max=5 :step=1` | 实现用数字输入控件(更好) |
|
||
| "您确定要开通吗?" 弹框 | 仅"确定/取消",**无截止时间字段** (单条版) | 原型单条版也无,批量版实现加了 |
|
||
| 表格 `min-width=2900px` 横滚 | `el-table` 自动布局 | 实现用 `fixed="right"` 操作列,列宽合理 |
|
||
| 状态颜色: 蓝色 status-activated (已开通) | ❌ 实现**无 "已开通" 状态列**,只有"已结题/未结题"列 | 实现走 `end_time` 而非 `is_finished`,前端没标"已开通" tag |
|
||
|
||
### 8.4 总结
|
||
|
||
- **整体方向**: 实现比原型更完整 (评分明细反显、批量开通截止时间、支持方/执行方下拉),但丢了 3 个导出按钮 (2 个 stub,1 个已报名专家实装)
|
||
- **设计偏离**: 把 `is_finished` 复用为"开通/结题"两种语义 (改值而非新增字段),导致前端状态显示混乱 (已结题是 tag,已开通没 tag)
|
||
|
||
---
|
||
|
||
## 9. 表字段冗余 / 设计问题
|
||
|
||
### 9.1 实体 vs 表不一致 (详见 §6.3)
|
||
|
||
- ✅ **已修**: projectId String → Long (见 §修复记录)
|
||
- ✅ **已修**: startTime/endTime String → Date (见 §修复记录)
|
||
- **🔴 严重冗余** (未修): noticeUrl / scheduleUrl / isPublished / publishTime / announcementType 这 5 个字段**只在实体里,DDL 实测不存在**,但 `confirmDeleteAnn` 仍按它们构造 payload,后端 mapper 的 `<if test="...">` 静默跳过,**公告删除功能只删了 publishUrl/invitation/supportLetter 3 个**
|
||
|
||
### 9.2 JOIN 字段 vs 持久化字段混淆
|
||
|
||
- `sponsorOrgName` / `execOrgNames` / `leadUserName` / `assignedSessions` / `assignedAmount` 全部是**派生字段**,不入库,只在 SQL JOIN 时填充,前端可读但不应让前端 PUT 回去
|
||
- 当前 mapper 的 `updateByPrimaryKey` 已经用 `<if test="...">` 防御,不会回写 — OK
|
||
|
||
### 9.3 唯一约束语义检查
|
||
|
||
- ✅ biz_project: uk_project_no — 项目编号唯一,合理
|
||
- ✅ biz_project_rating: uk_rating (project_id, rater_id, rater_role) — 一人评一次,合理
|
||
- ✅ biz_project_assign: uk_project_unit (project_id, execution_unit_id) — 同一项目同一执行方只能分配一次
|
||
|
||
### 9.5 索引分析与 EXPLAIN (2026-08-20)
|
||
|
||
数据规模: biz_project 12 行 / biz_org 33 行 / biz_project_assign 12 行 / sys_user 47 行 (测试数据)
|
||
|
||
| # | 查询 | 命中索引 | 备注 |
|
||
|---|---|---|---|
|
||
| Q1 | `selectSponsorOrgOptions` | `idx_org_type_status` (2 列) | 优化器在小数据量倾向小索引, `org_name LIKE` 走 Using where 二次过滤, `ORDER BY org_id DESC` 走 filesort |
|
||
| Q2 | `selectExecutorOrgOptions` | `idx_org_type_status` + `PRIMARY` sys_user | 同上, `parent_user_id IS NULL` 走 Using where |
|
||
| Q3 | `selectList` 主项目 + `sponsor_admin_user_id IN` | `idx_sponsor_admin_user_id` range + Using index | ✅ 完美 covering, 无需回表 |
|
||
| Q4 | `selectList` 主项目 + `EXISTS(exec_user_id IN)` | `idx_assign_exec_user_project` + LooseScan | ✅ 完美 covering (新加索引, 见下) |
|
||
| Q5 | `selectList` 主项目 + sponsor IN + 多 LEFT JOIN + GROUP_CONCAT 子查询 | `idx_project_form` Backward scan + 3 个 LEFT JOIN 全 eq_ref/Using index + `uk_project_unit` GROUP_CONCAT | ✅ 全部走索引 |
|
||
| Q6 | `selectList` 主项目 + 单独 exec EXISTS (无其他过滤) | `idx_assign_exec_user_project` range + Start temporary | ⚠️ 优化器会 material 子查询, 数据量小可接受 |
|
||
| Q7 | selectList 同时 sponsor IN + exec EXISTS | `idx_project_form` 主表 + `idx_assign_exec_user_project` FirstMatch 子查询 | ✅ 组合最优, semi-join |
|
||
| Q8 | selectList 仅 projectForm + isFinished | `index_merge(intersect(idx_project_form, idx_is_finished))` | ✅ MySQL 8 自动交集 |
|
||
|
||
**已加索引** (2026-08-20):
|
||
|
||
```sql
|
||
ALTER TABLE biz_project_assign ADD INDEX idx_assign_exec_user_project (exec_user_id, project_id);
|
||
```
|
||
|
||
理由: Q4/Q6/Q7 (服务机机构 EXISTS+IN) 原命中 `idx_assign_exec_user(exec_user_id)` 单列索引,
|
||
需回表读 project_id; 新加复合索引直接 covering, EXPLAIN 显示 `Using index` + `LooseScan`/`range`, 无需回表。
|
||
|
||
**评估后未加的索引** (记录备查):
|
||
|
||
- `biz_org (org_type, org_id, org_name)` — 用于消除 options 查询的 filesort。
|
||
评估: 强制 hint 后确实变 `Backward index scan` 无 filesort,
|
||
但优化器在 33 行数据下仍选 `idx_org_type_status` (2 列更小, 估算成本更低)。
|
||
决策: 数据量小不需要, **DROP**, 等生产数据增长到 1k+ 行再 EVAL。
|
||
- `biz_org (org_type, status, is_published)` 等 — 无明显 QPS 压力场景, 不加。
|
||
|
||
**过时注释修正** (2026-08-20):
|
||
|
||
- `BizOrgMapper.xml` 旧注释写 "走 idx_org_user_type_name 索引", EXPLAIN 实际命中 `idx_org_type_status`, 已修正为实测命中索引 + Using where 描述
|
||
|
||
### 9.4 业务闭环检查
|
||
|
||
- 项目分配 dialog: `assignForm.execRows` 行内 el-select 的 `value=MAIN user_id`,后端 `BizProjectAssignServiceImpl` 应该从 exec_user_id 反查 biz_org 写 execution_unit_id (mapper 注释有写,需确认 service 实现)
|
||
- 项目分配 → 评分 → 公示 → 通知: 整条业务链已铺,但 manager_score 聚合分写的是 manager 评分的均值,逻辑合理
|
||
- 批量开通: 改 `end_time` 而不是新建 `is_activated` 字段 — 用现有字段表达多语义,容易跟"结题"冲突,设计可商榷
|
||
|
||
---
|
||
|
||
## 10. 待修复列表 (按优先级)
|
||
|
||
| # | 问题 | 文件:行 | 修复建议 | 严重度 |
|
||
|---|---|---|---|---|
|
||
| 1 | 单条"开通"误改 `is_finished='0'` 而非 `end_time` | `Projects.vue:398` | 改 `endTime` 而非 `isFinished` | **P0** |
|
||
| 2 | `confirmDeleteAnn` 引用了不存在的字段 `noticeUrl/scheduleUrl/isPublished` | `Projects.vue:411-419` + `BizProject.java:113-122` | 要么补 DDL,要么删实体字段;mapper 不更新这些字段导致功能部分失效 | **P0** |
|
||
| 3 | `managerScore` 筛选前端有,后端 selectList 不读 | `BizProjectMapper.xml:141-183` (selectList) | 加 `<if test="managerScore != null">` 过滤 | **P0** |
|
||
| 4 | 下拉菜单 `divided` 冗余 (activate/delAnn 双分隔线) | `Projects.vue:100-101` | 删 `activate` 的 `divided` | **P1** |
|
||
| 5 | ~~生产代码残留 console.info/error (4 处)~~ | ~~`Projects.vue:299, 302, 306, 875`~~ | ✅ 已修 (见 §修复记录) | — |
|
||
| 6 | 评分默认值不一致 | `Projects.vue:501 vs 513` | 统一为 0 或 5 | **P2** |
|
||
| 7 | ~~`batchScoreRows` 死代码~~ | ~~`Projects.vue:501`~~ | ✅ 已修 (见 §修复记录) | — |
|
||
| 8 | `isSettled` 筛选但表格无列显示 | `Projects.vue:26-30` vs §4.2 表 | 加列 或 删筛选 | **P2** |
|
||
| 9 | ~~日期选择器 `value-format="YYYY-MM-DD HH:mm:ss"` 跟 `type="date"` 不匹配~~ | ~~`Projects.vue:17, 19`~~ | ✅ 已修 (见 §修复记录) | — |
|
||
| 10 | 重复 score table HTML (批量评分 + 单条评分) | `Projects.vue:201-213, 257-269` | 抽公共组件 | **P2** |
|
||
| 11 | 2 个导出按钮是 stub (`exportProjects` / `exportEval`) | `Projects.vue:347-348` | 实装 或 删按钮 | **P2** |
|
||
| 12 | ~~biz_project 缺索引 `sponsor_admin_user_id`~~ | ~~`DB`~~ | ✅ 已修 (见 §修复记录) | — |
|
||
| 13 | ~~biz_project 缺索引 `start_time`, `end_time` (范围查询)~~ | ~~`DB`~~ | ✅ 已修 (见 §修复记录) | — |
|
||
| 14 | ~~biz_project_assign 缺 UNIQUE (project_id, execution_unit_id)~~ | ~~`DB`~~ | ✅ 已修 (见 §修复记录) | — |
|
||
| 15 | `is_finished` 和 `is_settled` 状态值约定不一致 (0/1 vs Y/N) | `BizProject.java:65-68` + mapper | 统一约定 (要么全 0/1 要么全 Y/N) | **P3** |
|
||
| 16 | selectList / selectExecutorList 有 4 个相关子查询 | `BizProjectMapper.xml:46-50, 110-124` | 数据小可接受,但应记录为 P3 待优化 | **P3** |
|
||
| 17 | `.score-row` CSS 死样式 | `Projects.vue:926-928` | 删 | **P3** |
|
||
|
||
---
|
||
|
||
## 11. 引用清单
|
||
|
||
### 前端
|
||
- `ry-vue3/src/views/manager/Projects.vue` — 主列表(881 行)
|
||
- `ry-vue3/src/views/manager/ProjectsNew.vue` — 新建/编辑独立页
|
||
- `ry-vue3/src/views/manager/ManagerProjectDetail.vue` — 详情独立页
|
||
- `ry-vue3/src/layout/AdminLayout.vue:98` — 菜单项定义
|
||
- `ry-vue3/src/router/index.js:65-68` — 路由 (含 /new, /edit/:id, /detail/:id)
|
||
- `ry-vue3/src/api/public.js` — `bizList/bizAdd/bizUpdate/bizDelete` 通用 CRUD
|
||
- `ry-vue3/src/api/system.js` — `listExecutorOrgs/listSponsorOrgs` 项目分配用
|
||
- `ry-vue3/src/components/Preview.vue` — 附件预览 (import 但未使用?)
|
||
|
||
### 后端 (Java)
|
||
- `ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizProjectController.java` — 项目主 controller (291 行, 14 个 endpoint)
|
||
- `ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizProject.java` — 实体 (230 行, 字段定义含 5 个 DDL 不存在的字段)
|
||
- `ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizProjectAssign.java` — 分配实体
|
||
- `ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizProjectRating.java` — 评分实体
|
||
- `ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizProjectRatingController.java` — 评分 CRUD (单独 controller)
|
||
- `ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizProjectAssignServiceImpl.java` — 分配 service (分配时反查 execution_unit_id)
|
||
- `ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizProjectRatingServiceImpl.java` — 评分 upsert service
|
||
|
||
### Mapper XML
|
||
- `ry-api/ruoyi-business/src/main/resources/mapper/business/BizProjectMapper.xml` — 项目主 mapper (301 行, 4 个 select + insert + update + 2 个 delete)
|
||
- `ry-api/ruoyi-business/src/main/resources/mapper/business/BizProjectAssignMapper.xml` — 分配 mapper (106 行)
|
||
- `ry-api/ruoyi-business/src/main/resources/mapper/business/BizProjectRatingMapper.xml` — 评分 mapper (含 `upsertRating` 三元组 ON DUPLICATE KEY)
|
||
|
||
### 配置 / 工具
|
||
- `ry-api/ruoyi-admin/src/main/resources/application-druid.yml:9-10` — DB 连接 (root / cu2oh2co3)
|
||
|
||
### 原型
|
||
- `proto/html/manager.html:206` — 菜单入口
|
||
- `proto/html/components/project-manage.html` — 项目管理原型 (676 行,含 5 个 modal)
|
||
- `proto/html/components/project-assign.html` — 项目分配原型 (未深读,但 dialog 实装与之有偏差)
|
||
|
||
### 数据库 (实测)
|
||
- `biz_project` — 11 行,5 索引 (含 1 唯一),缺 3 个索引
|
||
- `biz_project_assign` — 11 行,4 索引,缺唯一约束
|
||
- `biz_project_rating` — 21 行,5 索引 (含 1 唯一),设计优秀
|
||
- `biz_org` — 33 行 (待查索引,本审查未深入)
|
||
- `sys_user` — 40 行 (待查 role_type 索引) |