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): 中试申请单体检「从套餐添加」展开预约明细
| # | 文件 | 操作 | 职责 |
|---|---|---|---|
| 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 登记(可选) |
Files:
docs/sql/checkup_institution_package.sqlModify: 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 行。
docs/sql/zkxw.sql在 biz_checkup_institution_project 段落后、biz_clean_room 之前插入 checkup_institution_package.sql 中两张 CREATE TABLE(勿重复 DROP 破坏全量脚本顺序时可只保留 CREATE)。
git add docs/sql/checkup_institution_package.sql docs/sql/zkxw.sql
git commit -m "feat(db): add checkup institution package tables"
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="删除标记")
pilot_service.py CRUD 顶部 import在 backend/app/crud/pilot_service.py 的 from app.models.models import ( 中追加:
BizCheckupInstitutionPackage,
BizCheckupInstitutionPackageItem,
cd backend && source venv/bin/activate && python -c "from app.models.models import BizCheckupInstitutionPackage, BizCheckupInstitutionPackageItem; print('OK')"
Expected: OK
git add backend/app/models/models.py backend/app/crud/pilot_service.py
git commit -m "feat: add SQLAlchemy models for checkup institution packages"
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 规则:
items 至少 1 条;同一请求内 checkup_institution_project_id 不重复。project_id 属于 existing_item_project_ids(更新时保留的历史行):允许源项目 del_flag=1,但须校验曾属于本机构。del_flag=0、status=1、且 checkup_institution_id 匹配。status=0:抛出 ValueError("套餐不能包含已停用的体检项目")。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 中注明符合设计)。
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(无异常;测试后可软删该套餐)
git add backend/app/crud/pilot_service.py
git commit -m "feat: add CRUD for checkup institution project packages"
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
| 方法 | 路径 | 处理 |
|---|---|---|
| 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)。
# 登录后替换 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 为数组(可为 [])。
git add backend/app/api/pilot_service.py
git commit -m "feat: add REST API for checkup institution project packages"
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}` })
}
git add web-admin/src/api/admin.ts
git commit -m "feat(web-admin): add API helpers for checkup institution packages"
CheckupPackagePanel.vueFiles:
web-admin/src/views/pilot-service/checkup-institution/CheckupPackagePanel.vueProps:
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
}
ElMessageBox.confirm → fetchDeleteCheckupInstitutionPackage → loadPackages()
git add web-admin/src/views/pilot-service/checkup-institution/CheckupPackagePanel.vue
git commit -m "feat(web-admin): add checkup package panel component"
index.vue TabFiles:
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>
import CheckupPackagePanel from './CheckupPackagePanel.vue'
const itemConfigTab = ref<'projects' | 'packages'>('projects')
openItemDialog 时重置 itemConfigTab = 'projects'
[ ] Step 3: 弹窗宽度
项目配置弹窗 width 调至 960px 或 1000px,避免 Tab 内表格拥挤。
主列表操作列可增加「套餐配置」按钮,openItemDialog(row) 且 itemConfigTab = 'packages'(非必须,Tab 已可达)。
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"
cd backend && source venv/bin/activate && python -c "from app.api.pilot_service import router; print('routes OK')"
cd web-admin && pnpm lint
| # | 场景 | 预期 |
|---|---|---|
| 1 | 新增套餐,编码必填,选 2 个项目并改价 | 保存成功;列表 totalFee = 两行费用之和 |
| 2 | 同机构重复套餐编码 | 接口 400,提示编码已存在 |
| 3 | 套餐含停用项目(新加行) | 保存失败 |
| 4 | 编辑套餐:删除一行、改价 | 合计随动;保存后列表更新 |
| 5 | 软删除某机构项目(被套餐引用) | 删除成功;编辑套餐见「源项目已删除」,合计仍含该行 |
| 6 | 删除套餐 | 列表不再显示 |
git commit -m "fix: checkup package QA adjustments after manual test"
| 设计要求 | 任务 |
|---|---|
| 套餐编码必填、机构内唯一 | 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 从套餐带入 | 未纳入本计划 |
中试申请单体检模块开发时:
del_flag=0 && status=1 的源项目行biz_pilot_checkup_reserve_item 快照详见设计文档 §6。