Files
zkxw/docs/superpowers/plans/2026-05-18-checkup-project-package.md
T
2026-05-18 16:21:38 +08:00

21 KiB
Raw Blame History

体检机构项目套餐(Phase 1)Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: 在体检机构维度维护「项目套餐」(主子表),套餐总价为明细费用之和,明细价可编辑;机构项目软删除不阻断、不级联套餐行。

Architecture: 新增 biz_checkup_institution_package + _package_item 两张表;CRUD 追加到 pilot_service.py;API 在 pilot_service.py 路由文件增加 5 个端点,POST/PUT 一次提交 items[] 并在事务内全量替换明细;前端在 checkup-institution/index.vue 的「项目价格配置」弹窗内用 Tab 分离「项目列表 / 套餐管理」,套餐编辑交互对齐 project-price-system 主子表弹窗。

Tech Stack: Python 3.9 / FastAPI / SQLAlchemy / Pydantic v2;Vue 3 / Element Plus / TypeScript;MySQL 5.7

Design Spec: docs/superpowers/specs/2026-05-18-checkup-project-package-design.md

SQL: docs/sql/checkup_institution_package.sql

Out of Scope (Phase 2): 中试申请单体检「从套餐添加」展开预约明细


File Structure

# 文件 操作 职责
1 docs/sql/checkup_institution_package.sql 已有 建表 DDL(实施前在目标库执行)
2 docs/sql/zkxw.sql 追加 基线库同步两张新表(与增量 SQL 一致)
3 backend/app/models/models.py 追加 BizCheckupInstitutionPackage / Item
4 backend/app/crud/pilot_service.py 追加 套餐 CRUD + 校验辅助函数
5 backend/app/api/pilot_service.py 追加 路由 + Pydantic Body
6 web-admin/src/api/admin.ts 追加 5 个 fetch 函数
7 web-admin/src/views/pilot-service/checkup-institution/CheckupPackagePanel.vue 新建 套餐 Tab:列表 + 主子表弹窗 + 选项目子弹窗
8 web-admin/src/views/pilot-service/checkup-institution/index.vue 修改 项目配置弹窗改为 el-tabs;引入套餐面板
9 web-admin/src/App.vue 按需 若套餐弹窗标题需竖条,在 panelTitleBarTextRules 登记(可选)

Task 1: 数据库

Files:

  • Execute: docs/sql/checkup_institution_package.sql

  • Modify: docs/sql/zkxw.sql(在 biz_checkup_institution_project 表定义之后插入两张新表,内容与增量 SQL 一致)

  • Step 1: 在开发/测试库执行增量 SQL

mysql -h127.0.0.1 -uroot -p zkxw < docs/sql/checkup_institution_package.sql

Expected: Query OK,SHOW TABLES LIKE 'biz_checkup_institution_package%' 返回 2 行。

  • Step 2: 将建表语句合并进 docs/sql/zkxw.sql

在 biz_checkup_institution_project 段落后、biz_clean_room 之前插入 checkup_institution_package.sql 中两张 CREATE TABLE(勿重复 DROP 破坏全量脚本顺序时可只保留 CREATE)。

  • Step 3: Commit
git add docs/sql/checkup_institution_package.sql docs/sql/zkxw.sql
git commit -m "feat(db): add checkup institution package tables"

Task 2: 后端 Model

Files:

  • Modify: backend/app/models/models.py(在 BizCheckupInstitutionProject 类之后、# 生产管理 注释之前)

  • Step 1: 追加两个 Model 类

class BizCheckupInstitutionPackage(Base):
    """体检机构项目套餐 biz_checkup_institution_package"""

    __tablename__ = "biz_checkup_institution_package"

    id = Column(String(64), primary_key=True, comment="主键ID")
    checkup_institution_id = Column(String(64), nullable=False, comment="体检机构ID")
    package_name = Column(String(200), nullable=False, comment="套餐名称")
    package_code = Column(String(64), nullable=False, comment="套餐编码")
    status = Column(CHAR(1), nullable=False, default="1", comment="1正常 0停用")
    tenant_no = Column(String(64), nullable=False, default="zkxw", comment="租户编号")
    order_num = Column(Integer, nullable=True, comment="排序号")
    remark = Column(String(512), nullable=True, comment="备注")
    create_by = Column(String(64), nullable=True, comment="创建人id")
    create_dept_id = Column(String(64), nullable=False, default="", comment="创建人部门ID")
    create_time = Column(DateTime, nullable=False, default=datetime.now, comment="创建时间")
    update_by = Column(String(64), nullable=True, comment="更新人id")
    update_time = Column(DateTime, nullable=False, default=datetime.now, onupdate=datetime.now, comment="更新时间")
    del_flag = Column(SmallInteger, nullable=False, default=0, comment="删除标记")


class BizCheckupInstitutionPackageItem(Base):
    """体检机构项目套餐明细 biz_checkup_institution_package_item"""

    __tablename__ = "biz_checkup_institution_package_item"

    id = Column(String(64), primary_key=True, comment="主键ID")
    package_id = Column(String(64), nullable=False, comment="套餐ID")
    checkup_institution_project_id = Column(String(64), nullable=False, comment="机构项目ID")
    project_name = Column(String(200), nullable=False, comment="项目名称快照")
    project_fee = Column(Numeric(18, 2), nullable=True, comment="套餐内项目费用(元)")
    tenant_no = Column(String(64), nullable=False, default="zkxw", comment="租户编号")
    order_num = Column(Integer, nullable=True, comment="排序号")
    remark = Column(String(512), nullable=True, comment="备注")
    create_by = Column(String(64), nullable=True, comment="创建人id")
    create_dept_id = Column(String(64), nullable=False, default="", comment="创建人部门ID")
    create_time = Column(DateTime, nullable=False, default=datetime.now, comment="创建时间")
    update_by = Column(String(64), nullable=True, comment="更新人id")
    update_time = Column(DateTime, nullable=False, default=datetime.now, onupdate=datetime.now, comment="更新时间")
    del_flag = Column(SmallInteger, nullable=False, default=0, comment="删除标记")
  • Step 2: 更新 pilot_service.py CRUD 顶部 import

在 backend/app/crud/pilot_service.py 的 from app.models.models import ( 中追加:

    BizCheckupInstitutionPackage,
    BizCheckupInstitutionPackageItem,
  • Step 3: 验证 import
cd backend && source venv/bin/activate && python -c "from app.models.models import BizCheckupInstitutionPackage, BizCheckupInstitutionPackageItem; print('OK')"

Expected: OK

  • Step 4: Commit
git add backend/app/models/models.py backend/app/crud/pilot_service.py
git commit -m "feat: add SQLAlchemy models for checkup institution packages"

Task 3: 后端 CRUD

Files:

  • Modify: backend/app/crud/pilot_service.py(在 soft_delete_checkup_institution_project 之后、# Shared utility 之前新增 Entity 14 段)

  • Step 1: 实现字典转换与辅助函数

新增函数(命名固定,便于 API 层调用):

函数 职责
_row_to_dict_checkup_institution_package(row, *, total_fee=None, item_count=None) 头表 camelCase + totalFee / itemCount
_row_to_dict_checkup_institution_package_item(row, *, source_project_deleted=False) 明细 + sourceProjectDeleted
_sum_package_item_fees(items) sum(project_fee or 0)
checkup_package_code_exists(db, institution_id, code, tenant_no, exclude_id=None) 同机构 del_flag=0 编码唯一
_get_checkup_project_for_package(db, project_id, institution_id, tenant_no) 查源项目,不存在返回 None
_validate_package_items_for_save(db, institution_id, items, tenant_no, existing_item_project_ids=None) 见下方规则

_validate_package_items_for_save 规则:

  1. items 至少 1 条;同一请求内 checkup_institution_project_id 不重复。
  2. 对每条明细:
    • 若 project_id 属于 existing_item_project_ids(更新时保留的历史行):允许源项目 del_flag=1,但须校验曾属于本机构。
    • 否则:源项目须 del_flag=0、status=1、且 checkup_institution_id 匹配。
  3. 若任一新添加行源项目 status=0:抛出 ValueError("套餐不能包含已停用的体检项目")。
  • Step 2: 实现列表 / 详情 / 创建 / 更新 / 删除
def list_checkup_institution_packages(db, checkup_institution_id, tenant_no=None) -> List[Dict[str, Any]]:
    # 查头表 del_flag=0,按 create_time desc
    # 对每个 package 查明细 del_flag=0,计算 totalFee、itemCount
    ...

def get_checkup_institution_package(db, package_id, tenant_no=None):
    # 头表单条,不存在返回 None
    ...

def get_checkup_institution_package_detail(db, package_id, tenant_no=None) -> Optional[Dict[str, Any]]:
    # 头表 dict + items[](join 源项目 del_flag 填 sourceProjectDeleted)
    ...

def create_checkup_institution_package(
    db, *, checkup_institution_id, package_name, package_code, status="1",
    items, tenant_no, order_num=None, remark=None, user_id=None,
):
    # 1. checkup_package_code_exists → ValueError
    # 2. _validate_package_items_for_save(..., existing_item_project_ids=None)
    # 3. 插入头表 + 循环插入明细(project_name 优先 body,否则源项目名)
    # 4. commit;返回 get_checkup_institution_package_detail(...)
    ...

def update_checkup_institution_package(
    db, package_id, *, package_name=None, package_code=None, status=None,
    items=None, order_num=None, remark=None, user_id=None, tenant_no=None,
):
    # 1. get 头表
    # 2. 若改 package_code 做唯一校验
    # 3. 若 items is not None:
    #    - 读取旧明细 project_id 集合 → existing_item_project_ids
    #    - _validate_package_items_for_save(..., existing_item_project_ids=...)
    #    - 旧明细全部 soft_delete (del_flag=1)
    #    - 插入新明细行(新 id)
    # 4. commit;返回 detail
    ...

def soft_delete_checkup_institution_package(db, package_id, tenant_no=None):
    # 头表 del_flag=1;其下明细 del_flag=1
    ...

机构项目删除: 确认 soft_delete_checkup_institution_project 不增加套餐引用检查(保持现状即可,在 commit message 中注明符合设计)。

  • Step 3: 本地 CRUD 冒烟(可选,Django shell 等价)
cd backend && source venv/bin/activate && python - <<'PY'
from app.core.database import SessionLocal
from app.crud import pilot_service as ps
db = SessionLocal()
# 替换为库内真实 institution_id / project_id
inst_id = "REPLACE"
proj_id = "REPLACE"
try:
    detail = ps.create_checkup_institution_package(
        db,
        checkup_institution_id=inst_id,
        package_name="基础套餐",
        package_code="PKG_BASE_01",
        status="1",
        items=[{"checkup_institution_project_id": proj_id, "project_name": "血常规", "project_fee": 100.0}],
        tenant_no="zkxw",
        user_id="admin",
    )
    print("totalFee", detail.get("totalFee"), "items", len(detail.get("items", [])))
finally:
    db.close()
PY

Expected: 打印 totalFee 100.0 items 1(无异常;测试后可软删该套餐)

  • Step 4: Commit
git add backend/app/crud/pilot_service.py
git commit -m "feat: add CRUD for checkup institution project packages"

Task 4: 后端 API 路由

Files:

  • Modify: backend/app/api/pilot_service.py(在体检机构项目路由段之后追加)

  • Step 1: 定义 Pydantic Body

class CheckupPackageItemBody(BaseModel):
    model_config = ConfigDict(populate_by_name=True)

    checkup_institution_project_id: str = Field(..., alias="checkupInstitutionProjectId")
    project_name: Optional[str] = Field(None, alias="projectName")
    project_fee: Optional[float] = Field(None, alias="projectFee")
    order_num: Optional[int] = Field(None, alias="orderNum")
    remark: Optional[str] = None


class CheckupInstitutionPackageCreateBody(BaseModel):
    model_config = ConfigDict(populate_by_name=True)

    package_name: str = Field(..., alias="packageName")
    package_code: str = Field(..., alias="packageCode")
    status: Optional[str] = Field("1", alias="status")
    order_num: Optional[int] = Field(0, alias="orderNum")
    remark: Optional[str] = None
    items: List[CheckupPackageItemBody] = Field(default_factory=list)


class CheckupInstitutionPackageUpdateBody(BaseModel):
    model_config = ConfigDict(populate_by_name=True)

    package_name: Optional[str] = Field(None, alias="packageName")
    package_code: Optional[str] = Field(None, alias="packageCode")
    status: Optional[str] = Field(None, alias="status")
    order_num: Optional[int] = Field(None, alias="orderNum")
    remark: Optional[str] = None
    items: Optional[List[CheckupPackageItemBody]] = None
  • Step 2: 注册 5 个路由
方法 路径 处理
GET /checkup-institutions/{institution_id}/packages list_checkup_institution_packages → success_response(list)
POST /checkup-institutions/{institution_id}/packages create_checkup_institution_package,body 转 items 为 dict 列表
GET /checkup-institution-packages/{package_id} get_checkup_institution_package_detail
PUT /checkup-institution-packages/{package_id} update_checkup_institution_package
DELETE /checkup-institution-packages/{package_id} soft_delete_checkup_institution_package

权限:require_admin_or_super_admin;tenant_no = _get_tenant_no(db, user)。

  • Step 3: 启动后端并用 curl 验证(需有效 JWT)
# 登录后替换 TOKEN、INSTITUTION_ID
curl -s -H "Authorization: Bearer TOKEN" \
  "http://127.0.0.1:8000/api/web/admin/checkup-institutions/INSTITUTION_ID/packages" | head -c 500

Expected: code:200,data 为数组(可为 [])。

  • Step 4: Commit
git add backend/app/api/pilot_service.py
git commit -m "feat: add REST API for checkup institution project packages"

Task 5: 前端 API

Files:

  • Modify: web-admin/src/api/admin.ts(在 fetchDeleteCheckupInstitutionProject 之后)

  • Step 1: 追加 5 个函数

// 中试服务管理 — 体检机构项目套餐
export function fetchCheckupInstitutionPackages(institutionId: string) {
  return request.get({ url: `/web/admin/checkup-institutions/${institutionId}/packages` })
}

export function fetchCheckupInstitutionPackageDetail(packageId: string) {
  return request.get({ url: `/web/admin/checkup-institution-packages/${packageId}` })
}

export function fetchCreateCheckupInstitutionPackage(
  institutionId: string,
  data: Record<string, unknown>
) {
  return request.post({
    url: `/web/admin/checkup-institutions/${institutionId}/packages`,
    params: data
  })
}

export function fetchUpdateCheckupInstitutionPackage(
  packageId: string,
  data: Record<string, unknown>
) {
  return request.put({ url: `/web/admin/checkup-institution-packages/${packageId}`, params: data })
}

export function fetchDeleteCheckupInstitutionPackage(packageId: string) {
  return request.del({ url: `/web/admin/checkup-institution-packages/${packageId}` })
}
  • Step 2: Commit
git add web-admin/src/api/admin.ts
git commit -m "feat(web-admin): add API helpers for checkup institution packages"

Task 6: 前端 — CheckupPackagePanel.vue

Files:

  • Create: web-admin/src/views/pilot-service/checkup-institution/CheckupPackagePanel.vue

Props:

const props = defineProps<{
  institutionId: string
  institutionName: string
}>()
  • Step 1: 套餐列表区

  • 工具栏:已选 N 项 +「新增套餐」(唯一 type="primary")

  • 表格列:序号、套餐名称、套餐编码、项目数(itemCount)、合计费用(totalFee)、状态(tag)、备注、操作(编辑/删除)

  • loadPackages() → fetchCheckupInstitutionPackages(props.institutionId),解析 Array.isArray(res) ? res : res?.list ?? []

  • Step 2: 套餐新增/编辑弹窗(主子表)

对齐 project-price-system/index.vue:

头部表单(form-grid-2,label-position="top"):

字段 校验
packageName required
packageCode required
status 1 / 0
remark 可选

明细区:

  • 按钮「添加项目」→ 打开子弹窗
  • 表格列:序号、项目名称(+ sourceProjectDeleted 时 <span class="form-label-tip">源项目已删除</span>)、项目费用(可编辑 el-input-number)、操作(删除行)
  • 底部:套餐合计:¥ {{ packageTotalFee }}(computed,detailItems 的 projectFee 求和)

提交 payload:

{
  packageName: string,
  packageCode: string,
  status: string,
  remark?: string,
  items: detailItems.value.map((row, idx) => ({
    checkupInstitutionProjectId: row.checkupInstitutionProjectId,
    projectName: row.projectName,
    projectFee: row.projectFee,
    orderNum: idx + 1
  }))
}
  • 新增:fetchCreateCheckupInstitutionPackage(institutionId, payload)

  • 编辑:fetchUpdateCheckupInstitutionPackage(id, payload);打开时 fetchCheckupInstitutionPackageDetail(id) 填充 detailItems

  • Step 3: 「添加项目」子弹窗

  • 调用已有 fetchCheckupInstitutionProjects(props.institutionId)

  • 仅展示 status == '1' 且未在 detailItems 中的项目

  • 多选确认后 push:

{
  checkupInstitutionProjectId: row.id,
  projectName: row.projectName,
  projectFee: row.projectFee ?? 0,
  sourceProjectDeleted: false
}
  • Step 4: 删除套餐

ElMessageBox.confirm → fetchDeleteCheckupInstitutionPackage → loadPackages()

  • Step 5: Commit
git add web-admin/src/views/pilot-service/checkup-institution/CheckupPackagePanel.vue
git commit -m "feat(web-admin): add checkup package panel component"

Task 7: 前端 — 接入 index.vue Tab

Files:

  • Modify: web-admin/src/views/pilot-service/checkup-institution/index.vue

  • Step 1: 将「项目价格配置」弹窗正文改为 el-tabs

<el-tabs v-model="itemConfigTab" class="item-config-tabs">
  <el-tab-pane label="项目列表" name="projects">
    <!-- 现有 list-toolbar + el-table + 项目嵌套弹窗 移入此 pane -->
  </el-tab-pane>
  <el-tab-pane label="套餐管理" name="packages">
    <CheckupPackagePanel
      v-if="itemConfigTab === 'packages' && currentInstitutionId"
      :institution-id="currentInstitutionId"
      :institution-name="currentInstitutionName"
    />
  </el-tab-pane>
</el-tabs>
  • Step 2: script 增加
import CheckupPackagePanel from './CheckupPackagePanel.vue'
const itemConfigTab = ref<'projects' | 'packages'>('projects')
  • openItemDialog 时重置 itemConfigTab = 'projects'

  • Step 3: 弹窗宽度

项目配置弹窗 width 调至 960px 或 1000px,避免 Tab 内表格拥挤。

  • Step 4: 机构操作列(可选)

主列表操作列可增加「套餐配置」按钮,openItemDialog(row) 且 itemConfigTab = 'packages'(非必须,Tab 已可达)。

  • Step 5: Commit
git add web-admin/src/views/pilot-service/checkup-institution/index.vue
git commit -m "feat(web-admin): integrate package tab in checkup institution config dialog"

Task 8: 联调与验收

  • Step 1: 后端 lint / 启动
cd backend && source venv/bin/activate && python -c "from app.api.pilot_service import router; print('routes OK')"
cd web-admin && pnpm lint
  • Step 2: 手工验收清单(Phase 1)
# 场景 预期
1 新增套餐,编码必填,选 2 个项目并改价 保存成功;列表 totalFee = 两行费用之和
2 同机构重复套餐编码 接口 400,提示编码已存在
3 套餐含停用项目(新加行) 保存失败
4 编辑套餐:删除一行、改价 合计随动;保存后列表更新
5 软删除某机构项目(被套餐引用) 删除成功;编辑套餐见「源项目已删除」,合计仍含该行
6 删除套餐 列表不再显示
  • Step 3: Commit(若有验收期小修)
git commit -m "fix: checkup package QA adjustments after manual test"

Spec Coverage(自检)

设计要求 任务
套餐编码必填、机构内唯一 Task 3 checkup_package_code_exists;Task 6 表单校验
总价 = 明细之和、不落库 Task 3 _sum_package_item_fees;Task 6 computed
明细价可改、不回写机构项目 Task 3 只写 package_item;Task 6 可编辑列
机构项目软删不拦截、明细保留 Task 3 校验 + sourceProjectDeleted;Task 3 不改 delete 逻辑
含停用项禁止保存(新行) Task 3 _validate_package_items_for_save
5 个 API 端点 Task 4
Tab:项目 / 套餐 Task 7
Phase 2 从套餐带入 未纳入本计划

Phase 2 备忘(本计划不实施)

中试申请单体检模块开发时:

  • 选机构 →「从套餐添加」→ 仅展开 del_flag=0 && status=1 的源项目行
  • 写入 biz_pilot_checkup_reserve_item 快照

详见设计文档 §6。