Files
guoju0808/_self/portal_publicity_detail.md
郭庆泰andClaude e496f34393 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>
2026-08-19 21:06:49 +08:00

22 KiB

/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/1313biz_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) 原型也有"立即报名"
表达支持意向 onSupportIntentdoSubmitIntent('support', fields) POST /business/publicity/supportIntent BizPublicityIntentController.doSubmit (66-139 行) biz_publicity_support_intent 全字段 原型没有
表达执行意向 onExecutionIntentdoSubmitIntent('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

重要: 提交时,doSubmitIntentguestDialog.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)

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 (同构)

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, 121private String noticeUrl;private String scheduleUrl; 两个字段,且 mapper XML 未定义 <result> 映射这两个字段 (BizProjectMapper.xml:40-43 只 mapping 到 publishUrl):

<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 记忆一致)
表达执行意向按钮 原型没有 改进
报名按钮已有"已报名"灰显状态 原型跳登录 改进
顶部 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.javanoticeUrl / 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 记忆:不复用登录视角的强绑表) — 合理,不冗余

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