Files
zkxw/openspec/changes/project-business-statistics/design.md
T
zlwu 79ca01ac0d feat(project): show presign counts as a statistics column
Group presign counts by project and team, include totals and unassigned rows, and reuse count drill-down. Remove the standalone footer while retaining presignTotal compatibility and the four formal business types.

Related to issue #13. Independent source review completed; no tests or builds run under user self-test arrangement.
2026-11-04 14:53:07 +08:00

78 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 实现设计与接口契约
## 预签约统计列(2026-11-04 用户明确调整)
将统计表左下角预签约计数移为独立“预签约”列,放在细胞存储前,复用现有数量列、图标按钮及明细弹窗。项目/项目组行、业务总量行、未归属行均返回 `presign` 数量;只有预签约的项目/组也显示。预签约仍按当前筛选下未删除的全状态主单计数(含已转正式),与四类正式业务分别统计,不相加。点击数量沿用预签约明细与汇总筛选。统计服务局部增加预签约聚合,保留 `presignTotal` 作为 `totals.presign` 的兼容字段;`COUNT_TYPES` 及历史归属四 tab 范围保持四类正式业务不变。无新界面、弹窗、样式或 SQL,继续用户自测。
## 历史归属维护 v2(2026-11-04 已确认)
统计明细后续反馈:用户明确要求业务统计四类明细弹窗复用历史归属维护的全部业务识别列。沿用原明细弹窗、P02 表格和已确认列配置,无新弹窗或视觉布局。新增 `GET /web/admin/project-management/statistics/records`,在路由及服务入口校验 `project:statistics:list`,与归属维护接口复用内部明细查询;两接口分别检查统计/归属维护权限,统计接口只读,不授予调整权限。保留认证租户、主单数据范围、软删除、项目/项目组、日期、未归属、状态条件及主单分页,展示与关键词匹配四业务字段。前端复用同一列配置与查询提示,不显示归属操作;预签约沿用旧通用接口和列。以下 v2 初始方案中“统计接口不返回新增联系信息”的约束仅继续适用于旧 `/records`,本次新统计专用接口按用户明确要求提供这些字段;汇总计数及移动接口不变。
后续用户明确调整:细胞存储、科研捐赠 tab 的单号列替换为“样本编码”,沿用已确认表格的列宽、位置及样式,不新增界面或视觉布局。专用接口新增只读 `sampleCodes`,按主单的同租户有效样本子行批量读取,多个编码稳定去重后用顿号连接,无样本时显示破折号;关键词增加样本编码 EXISTS 匹配。列表仍一张主单一行,归属操作仍作用于主单及关联链;主单编号保留在接口和原关联预览中供现有逻辑使用。此处仅读取本单样本子行,不涉及接收/质检等下游跨表匹配。
用户明确采用 [Figma 原型 v2](https://www.figma.com/design/wOQLDAhr1gdlQEXnVaFO9S?node-id=13-23)。范围为 Web 历史归属维护及其后端读取接口,沿用用户先合入 V2.0 再自测的安排。复用 P02 列表和 `pilot-service/pilot-application/index.vue` 的 `panel-pill-tabs`,操作列固定右侧,搜索右对齐。
`RecordList` 仅在 assignment 模式显示四类正式业务 tab 和业务识别列。细胞存储/科研捐赠展示母亲姓名、电话、采集医院、市场专员及采集/捐赠日期;中试展示申请名称、渠道、服务订单名称、患者姓名/电话;基础实验展示课题、客户名称/联系人/电话。公共列保留序号、主单号、状态、项目组、所属项目、创建时间。多个有效子记录以稳定顺序去重连接展示,缺失值显示破折号,采集日期来自采集信息的样本采集时间。
新增 `GET /web/admin/project-management/assignments/records`,严格要求 `project:assignment:edit`,仅接受四类正式业务。原 `/records` 继续服务统计明细,不扩展联系信息;移动接口不变。专用服务复用主单租户、软删除、数据权限和归属筛选。关键词支持单号、姓名/电话、医院/专员、渠道/服务订单名称、课题/客户信息(日期使用现有创建日期条件);子表使用同租户未删除记录 EXISTS 匹配,不增加主单行数。分页后批量补充项目、医院、人员、患者和服务订单资料,避免逐行查询。
切换 tab 清空关键词、状态、项目组/项目并回第一页,保留日期和仅未归属条件;重新挂载候选选择器避免短暂显示上一个业务的选项。列表请求按序号丢弃过时响应,提供加载、空态和失败重试。归属弹窗绑定选中单据的业务类型,沿用原项目选择权限、关联单据预览、并发快照及审计。预签约不单列 tab,但关联预签约仍随正式单预览和调整。不新增批量操作、数据库字段或迁移。
## 模块边界
新增 models/project_management.py、services/project_management.py、api/project_management.py。现有四种主表(预签约、采集协议、科研捐赠、中试/基础实验申请)增加可空 project_id,不向下游样本表复制归属。不更改已有课题名称 project_title。
项目组包含 teamName、leaderUserId、members[{id?,userId}]、regions[{id?,regionId}]、hospitals[{id?,hospitalId}]、enabled、remark。子行按 sync_child_rows 与自然键更新。负责人应计入可选项目成员判断。项目包含 projectNo、projectName、teamId、leaderUserId、bizTypes[]、startDate、endDate、status(pending/active/completed/disabled)、remark。编号服务端生成。项目已有业务后禁止换组;引用中的项目和组只能停用,不能删除。所有引用租户内校验。
## 权限与兼容
管理功能使用 project:team:list/edit、project:project:list/edit、project:statistics:list、project:assignment:edit 菜单权限,复用有效角色并集;super_admin 仍限定本次认证租户。跨组选择使用独立功能权限 `project:project:all`(全部项目),在现有角色菜单树的“项目”下分配,不默认授予现有角色。`project:assignment:edit` 仅允许维护操作,不能代替全部项目权限;普通 admin 不自动放开。super_admin 沿用系统全部权限语义,但始终受当前认证租户限制。订单数据统计、明细及归属调整应用原有 apply_data_scope;项目成员身份不扩权。
授权维护不能由普通业务管理员自授:系统用户、角色、职位、菜单的变更接口分别严格校验 `system:user:list`、`system:role:list`、`system:post:list`、`system:menu:list`,沿用有效角色并集与 super_admin 全权限语义。角色菜单和数据范围读取校验管理权限及目标租户;通用角色/人员下拉保留原有读取用途。目标和关联 ID 均验证认证租户,super_admin 在这些接口也不跨租户。角色菜单保存补齐每个已选节点的同租户祖先(包含各自 Web/移动虚拟根),拒绝断链或循环,不自动勾选兄弟/子权限。质检金额专用角色的授予/撤销/批量撤销同样使用其既有专用管理权限严格校验,阻止另一条用户角色绑定旁路。
新单据项目必填由 PROJECT_ASSIGNMENT_REQUIRED 配置控制,默认 false 供多端滚动升级;全部入口更新且项目资料准备好后由运维设为 true。数据库历史 NULL 继续合法。编辑未传 projectId 时保留旧值;普通表单不可改变已有非空归属,历史补录与归属调整统一从维护入口审计。未改变归属的历史单据不重验成员或启用状态。
公共服务契约:
```python
validate_assignment(db, tenant_no, user, project_id, biz_type, *, current_project_id=None, required=False) -> str | None
project_brief(db, tenant_no, project_id) -> dict # projectId/projectName/teamId/teamName
```
用户 user 使用认证上下文字典。调用方在创建时传 required=settings.PROJECT_ASSIGNMENT_REQUIRED;传入项目必须存在、启用、组启用、业务类型匹配且用户为项目组负责人/有效成员或有全部项目权限(仅项目负责人身份不替代项目组成员资格)。普通更新若归属不同返回“请通过项目归属维护调整”。转换复制来源项目,不按转换操作人重新分配;转换的目标业务类型仍允许保留来源项目历史归属。
## HTTP契约
均复用 success_response。管理前缀 /web/admin/project-management。
- GET /teams:page/pageSize/keyword/leaderUserId/enabled,返回 {list,total};POST /teams;GET/PUT/DELETE /teams/{id}。
- GET /projects:page/pageSize/keyword/teamId/status,返回 {list,total};POST /projects;GET/PUT/DELETE /projects/{id}。
- GET /references:返回 {users:[{id,name}],regions:[{id,name}],hospitals:[{id,name,regionId}]},仅管理权限。
- GET /statistics:groupBy=team|project、teamId/projectId/startDate/endDate,返回 {items:[{id,name,teamName,cell_storage,research_donation,pilot_service,basic_experiment}],totals:{含未归属的四类业务总量},unassignedTotals:{四类业务计数},presignTotal}。
- GET /records:bizType=presign|cell_storage|research_donation|pilot_service|basic_experiment,page/pageSize/teamId/projectId/unassigned/status/keyword/startDate/endDate,返回 {list:[{id,bizType,bizNo,status,projectId,projectName,teamId,teamName,createTime}],total,statusCounts:{原状态码:数量}}。
- POST /assignments/preview:{bizType,id,projectId},返回 {records:[{bizType,id,bizNo,projectId,projectName}]}。
- POST /assignments:{bizType,id,projectId,reason,expectedAssignments:[{bizType,id,projectId}]};锁定并重新校验关联集合及旧归属后一次事务提交,记录审计;不匹配返回冲突,全部单据须有权操作。
- GET /assignments/history:bizType/id,返回 {list},每行包含 oldProjectName/newProjectName/reason/operatorName/createTime。
- GET /project-management/options(独立认证路由,不要求管理菜单):bizType/keyword/currentProjectId,返回 {list:[{id,projectName,projectNo,teamId,teamName,disabled}],required}。currentProjectId不授权读取任意项目;仅补充本人已有权限单据的历史显示,若难以证明则由单据响应的名称回显,不返回额外敏感资料。
业务既有返回新增 projectId/projectName/teamId/teamName(嵌套单据放到 basic);列表支持 projectId/teamId 过滤。认证租户不由客户端传入。前端选择器复用统一 options 接口。
## 关联与统计
预签约转协议继承归属;存储/捐赠互转继承。归属维护将同租户的预签约→协议→捐赠转换记录组成关联集合,循环防重,锁定顺序固定;任一关联记录不可访问则拒绝整个操作,返回信息不暴露无权查看单据。历史软删除记录不参与活跃统计,不复活。
正式存储按协议、捐赠按主单、中试和基础实验按对应 biz_type 申请单计数;预签约另列,不把四类数量称为独立客户数。日期按创建日期(起日含、结束日次日不含),状态显示当前状态。查询先按租户、软删除、原有数据权限过滤,再聚合,不联接子行产生重复;金额不在本期。
## 界面与验证
Web P01/P02/P09,参考 system/unit/index.vue、cell-storage/protocol/ProtocolFormDialog.vue,复用 panel-top/list-table/list-wrap/list-pager-bar/dialog-form-layout 等。移动复用 src/market 契约与既有选择器,保留 Token,不借任务全局换色。
自动化测试使用隔离 SQLite 内存数据库,运行验收使用独立 MySQL 5.7 合成库;不连接或修改真实业务库。用户已明确授权本任务自行切换 backend/web-admin 服务,需保留原配置和运行来源记录。迁移验证覆盖首次新增、排序规则兼容修正及重复执行。最终服务/真机环境不可用时报告验证缺口,不标记完成合并。
### 查询筛选与新建选择的区别
公共 options 默认用于新建/归属调整,校验项目启用、支持业务类型和项目组成员资格或全部项目权限。`usage=filter` 同样限制项目组/全部项目范围,再按现有可见业务主单返回历史项目(包含完成/停用),不扩大订单数据权限。成员退出后不再作为可选项;已有单据的项目名称仍由单据响应回显,普通编辑不清空历史归属。
移动列表使用 `/mini/market/{specialist|manager}/project-options?bizType=...`,复用对应菜单权限及现场数据范围;不借公共筛选扩大现场范围。
普通创建不能伪造来源预签约/转换来源,普通更新不得改来源关联;只有受控转换入口维护来源图。