24 KiB
服务机构端 C0 基础骨架 实施计划
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: 在 sysmgr 脚手架(ec-nest + ec-admin)上落地服务机构端 C0 基础骨架:机构=租户映射、staff 关联账号、目录四层、权限/错误码/审计、后台业务菜单。
Architecture: 复用脚手架的多租户过滤(tenantCode)、RBAC(resource 树 + 权限码)、审计、软删。业务全部纯新增(不侵入脚手架):后端新增 agency/staff/catalog 模块,前端新增「长护服务」菜单组。
Tech Stack: NestJS 12 + Prisma 6 + MySQL 8(utf8mb4_general_ci)、Vue 3 + Vite + Pinia + UnoCSS。
Spec: openspec/changes/svc-org-c0-base/(proposal + specs + design + tasks);设计背景见 docs/superpowers/specs/2026-10-14-服务机构端-C0基础骨架-design.md。
Global Constraints
- 机构=租户:机构唯一标识 = 脚手架
tenant_code;Agency.code与tenant.code同值。 - 业务表主键 UUID v7(
@default(uuid(7)) @db.VarChar(36));列 snake_case(camelCase +@map)。 - 软删用
deleted_at(时间戳),不用is_deleted布尔。 - 不动脚手架任何现有文件(
sys_user、sys_tenant、Org、菜单 tree 等只读不改);业务代码落新目录/新模型。 - 权限码
domain:action;业务错误码 5 位 [域2][序3]。 - 命名:PRD 的
org(机构)→ Prisma 模型Agency(表sys_agency),因脚手架Org已被「组织管理」占用。
Review Focus
- 跨租户数据泄漏:A 机构账号读不到 B 机构的 staff/svc_org(多租户过滤必须生效)。
- Agency 与 tenant 一致:创建机构时
Agency.codeMUST ==tenant.code,两者事务内同步。 - 目录全局 vs 业务租户隔离:svc_national/svc_local/svc_mapping/dict/loc_config 是全局(TENANT_FREE),staff/svc_org 是租户级;别把全局表误加租户过滤。
- staff.user_id 唯一:一个 user 最多一条 staff(
@unique可空)。 - 仅持 director 的账号写接口 → 403;兼 qc/admin 按并集放行。
Task 1: Prisma 模型 + 多租户/软删清单
Files:
- Modify:
sources/ec-nest/prisma/schema.prisma(追加 9 个模型) - Modify:
sources/ec-nest/src/prisma/prisma.service.ts:6,8(TENANT_FREE、SOFT_DELETE集合)
Interfaces:
-
Produces: Prisma 模型
Agency、Staff、SvcNational、SvcLocal、SvcMapping、SvcOrg、DictAbnormalType、DictPhotoTier、LocConfig。 -
Step 1: 追加 9 个 Prisma 模型(追加到 schema.prisma 末尾)
/// 机构(=租户的业务信息,1:1 于 Tenant,code 与 tenant.code 同值)
model Agency {
code String @id @db.VarChar(64)
orgName String @map("org_name") @db.VarChar(100)
orgType String @map("org_type") @db.VarChar(20)
creditCode String @map("credit_code") @db.VarChar(18)
locCode String @map("loc_code") @db.VarChar(12)
locName String @map("loc_name") @db.VarChar(50)
svcSource String @map("svc_source") @db.VarChar(10)
licenseNo String? @map("license_no") @db.VarChar(50)
address String @map("address") @db.VarChar(200)
contactName String @map("contact_name") @db.VarChar(30)
contactPhone String @map("contact_phone") @db.VarChar(15)
bedCount Int? @map("bed_count")
signDate DateTime? @map("sign_date") @db.Date
createdAt DateTime @default(now()) @map("created_at")
createdBy String? @map("created_by") @db.VarChar(36)
updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at")
@@map("sys_agency")
}
/// 人员(护理员/医护/组长,账号在 User,staff.user_id 关联 user.id)
model Staff {
id String @id @default(uuid(7)) @db.VarChar(36)
tenantCode String @map("tenant_code") @db.VarChar(64)
userId String? @unique @map("user_id") @db.VarChar(36)
name String @db.VarChar(30)
idCardNo String @map("id_card_no") @db.VarChar(255)
personCode String? @map("person_code") @db.VarChar(20)
phone String @db.VarChar(15)
jobType String @map("job_type") @db.VarChar(20)
workType String @map("work_type") @db.VarChar(10)
entryDate DateTime? @map("entry_date") @db.Date
leaveDate DateTime? @map("leave_date") @db.Date
employStatus String @map("employ_status") @db.VarChar(10)
certType String? @map("cert_type") @db.VarChar(20)
certNo String? @map("cert_no") @db.VarChar(50)
certLevel String? @map("cert_level") @db.VarChar(20)
certExpire DateTime? @map("cert_expire") @db.Date
certStatus String? @map("cert_status") @db.VarChar(20)
certVerifyUrl String? @map("cert_verify_url") @db.VarChar(200)
groupLeaderId String? @map("group_leader_id") @db.VarChar(36)
createdAt DateTime @default(now()) @map("created_at")
createdBy String? @map("created_by") @db.VarChar(36)
updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at")
@@index([tenantCode, name])
@@map("sys_staff")
}
/// 国家基准库 L1(全局,36 项)
model SvcNational {
id String @id @default(uuid(7)) @db.VarChar(36)
natCode String @unique @map("nat_code") @db.VarChar(20)
natName String @map("nat_name") @db.VarChar(100)
natCategory String @map("nat_category") @db.VarChar(20)
natSubCategory String? @map("nat_sub_category") @db.VarChar(50)
recommendStaff String? @map("recommend_staff") @db.VarChar(50)
recommendDuration String? @map("recommend_duration") @db.VarChar(50)
isMedical Boolean @default(false) @map("is_medical")
version Int @default(1)
@@map("sys_svc_national")
}
/// 属地目录 L2(全局,昆明 45 项 / 州市引用 L1)
model SvcLocal {
id String @id @default(uuid(7)) @db.VarChar(36)
locCode String @map("loc_code") @db.VarChar(12)
localItemName String @map("local_item_name") @db.VarChar(100)
localCategory String? @map("local_category") @db.VarChar(20)
svcTime String? @map("svc_time") @db.VarChar(50)
minFreq String? @map("min_freq") @db.VarChar(50)
staffQual String? @map("staff_qual") @db.VarChar(50)
isMedical Boolean @default(false) @map("is_medical")
sourceDoc String? @map("source_doc") @db.VarChar(200)
mapStatus String @default("mapped") @map("map_status") @db.VarChar(20)
@@index([locCode])
@@map("sys_svc_local")
}
/// 属地↔国家映射 L3(全局,机构不可见)
model SvcMapping {
id String @id @default(uuid(7)) @db.VarChar(36)
locId String @map("loc_id") @db.VarChar(36)
natId String? @map("nat_id") @db.VarChar(36)
mapType String @map("map_type") @db.VarChar(20)
mapNote String? @map("map_note") @db.VarChar(200)
status String @default("confirmed") @db.VarChar(20)
@@index([locId])
@@map("sys_svc_mapping")
}
/// 机构项目库 L4(租户级:机构勾选+配价)
model SvcOrg {
id String @id @default(uuid(7)) @db.VarChar(36)
tenantCode String @map("tenant_code") @db.VarChar(64)
srcType String @map("src_type") @db.VarChar(10)
srcId String @map("src_id") @db.VarChar(36)
isEnabled Boolean @default(true) @map("is_enabled")
price String? @map("price") @db.VarChar(50)
priceEffective DateTime? @map("price_effective")
isNewNational Boolean @default(false) @map("is_new_national")
createdAt DateTime @default(now()) @map("created_at")
createdBy String? @map("created_by") @db.VarChar(36)
updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at")
@@index([tenantCode, srcType])
@@map("sys_svc_org")
}
/// 异常分类字典(全局)
model DictAbnormalType {
id String @id @default(uuid(7)) @db.VarChar(36)
code String @unique @db.VarChar(50)
name String @db.VarChar(50)
sort Int @default(0)
@@map("sys_dict_abnormal_type")
}
/// 拍照分档字典(全局)
model DictPhotoTier {
id String @id @default(uuid(7)) @db.VarChar(36)
code String @unique @db.VarChar(20)
name String @db.VarChar(50)
tier String @db.VarChar(10)
@@map("sys_dict_photo_tier")
}
/// 统筹区配置(全局)
model LocConfig {
locCode String @id @map("loc_code") @db.VarChar(12)
locName String @map("loc_name") @db.VarChar(50)
svcSource String @map("svc_source") @db.VarChar(10)
itemCountRule String? @map("item_count_rule") @db.VarChar(100)
@@map("sys_loc_config")
}
- Step 2: 更新 PrismaService 清单(全局表加入 TENANT_FREE,软删表加入 SOFT_DELETE)
prisma.service.ts 第 6 行改为:
const TENANT_FREE = new Set(['Tenant', 'Agency', 'SvcNational', 'SvcLocal', 'SvcMapping', 'DictAbnormalType', 'DictPhotoTier', 'LocConfig']);
第 8 行改为:
const SOFT_DELETE = new Set(['User', 'Role', 'Resource', 'Org', 'DictType', 'DictItem', 'Config', 'Notice', 'Staff', 'SvcOrg']);
- Step 3: 生成迁移并校验
Run: cd sources/ec-nest && pnpm exec prisma migrate dev --name c0_base
Expected: 生成迁移 SQL;prisma generate 通过。
- Step 4: Commit
git -C sources/ec-nest add prisma/schema.prisma prisma/migrations src/prisma/prisma.service.ts
git -C sources/ec-nest commit -m "feat(c0): 服务机构端基础数据模型(9 表 + 多租户/软删清单)"
Task 2: 权限码 + 业务错误码
Files:
- Modify:
sources/ec-nest/src/common/permissions.ts(追加业务权限码) - Modify:
sources/ec-nest/src/common/error-codes.ts(追加业务错误码)
Interfaces:
-
Produces:
Permissions.elder/plan/order/record/abnormal/qc/svcOrg/staff/export(C0 先落地 svcOrg/staff,其余预留);ErrorCodes.ORG_*、ErrorCodes.STAFF_*、ErrorCodes.SVC_*。 -
Step 1: 追加权限码(permissions.ts 末尾
}前)
// 服务机构端业务(C0 落地 agency/svcOrg/staff,其余为 C1–C4 预留)
agency: { view: 'agency:view', update: 'agency:update' },
svcOrg: { list: 'svc-org:list', config: 'svc-org:config', newNational: 'svc-org:new-national' },
staff: { list: 'staff:list', create: 'staff:create', update: 'staff:update', delete: 'staff:delete', groupAssign: 'staff:group-assign' },
elder: { list: 'elder:list', detail: 'elder:detail', create: 'elder:create', update: 'elder:update', statusChange: 'elder:status-change', import: 'elder:import', svcSave: 'elder:svc-save' },
plan: { create: 'plan:create', change: 'plan:change', detail: 'plan:detail', confirm: 'plan:confirm' },
order: { generate: 'order:generate', assign: 'order:assign', reassign: 'order:reassign', cancel: 'order:cancel', detail: 'order:detail', today: 'order:today' },
record: { checkin: 'record:checkin', checkout: 'record:checkout', itemSave: 'record:item-save', photoUpload: 'record:photo-upload', detail: 'record:detail' },
abnormal: { report: 'abnormal:report', list: 'abnormal:list', handle: 'abnormal:handle' },
qc: { pendingList: 'qc:pending-list', detail: 'qc:detail', pass: 'qc:pass', return: 'qc:return', exceptionLink: 'qc:exception-link' },
export: { export: 'export:export' },
- Step 2: 追加业务错误码(error-codes.ts,域码:机构 40 / 人员 41 / 目录 42)
// 服务机构端·机构(40)
ORG_NOT_FOUND: 40001,
ORG_CODE_MISMATCH: 40002,
// 服务机构端·人员(41)
STAFF_NOT_FOUND: 41001,
STAFF_USER_LINKED: 41002,
// 服务机构端·目录(42)
SVC_LOC_NOT_FOUND: 42001,
SVC_ORG_NOT_FOUND: 42002,
SVC_LOC_IMMUTABLE: 42003,
- Step 3: 编译校验
Run: cd sources/ec-nest && pnpm exec tsc --noEmit(或 pnpm lint)
Expected: 无类型错误。
- Step 4: Commit
git -C sources/ec-nest add src/common/permissions.ts src/common/error-codes.ts
git -C sources/ec-nest commit -m "feat(c0): 服务机构端权限码与业务错误码"
Task 3: seed 目录与角色
Files:
- Modify:
sources/ec-nest/src/common/tenant-template.ts(MENUS追加「长护服务」菜单组;DICTS追加异常分类/拍照分档) - Create:
sources/ec-nest/prisma/seed-catalog.ts(L1 36 项 + 昆明 L2 45 项 + L3 映射 + loc_config 骨架)
Interfaces:
-
Produces:
seedCatalog(prisma)函数(幂等 upsert 目录四层 + loc_config)。 -
Step 1: MENUS 追加「长护服务」组(tenant-template.ts 的
MENUS数组末尾、];前)
{ code: 'ltc', name: '长护服务', parent: null, path: '/ltc', component: 'Layout', icon: 'mdi:home-heart', sort: 5 },
{ code: 'elder', name: '服务对象', parent: 'ltc', path: '/ltc/elder', component: 'ltc/elder/index', icon: 'mdi:account-heart', sort: 1 },
{ code: 'schedule', name: '排班派单', parent: 'ltc', path: '/ltc/schedule', component: 'ltc/schedule/index', icon: 'mdi:calendar-clock', sort: 2 },
{ code: 'record', name: '工单留痕', parent: 'ltc', path: '/ltc/record', component: 'ltc/record/index', icon: 'mdi:clipboard-check', sort: 3 },
{ code: 'qc', name: '质控中心', parent: 'ltc', path: '/ltc/qc', component: 'ltc/qc/index', icon: 'mdi:shield-check', sort: 4 },
{ code: 'document', name: '文书档案', parent: 'ltc', path: '/ltc/document', component: 'ltc/document/index', icon: 'mdi:file-document', sort: 5 },
{ code: 'svc-config', name: '服务机构设置', parent: 'ltc', path: '/ltc/config', component: 'Layout', icon: 'mdi:cog', sort: 6 },
{ code: 'svc-config-info', name: '机构信息', parent: 'svc-config', path: '/ltc/config/info', component: 'ltc/config/info/index', icon: 'mdi:office-building', sort: 1 },
{ code: 'svc-config-catalog', name: '服务项目与配价', parent: 'svc-config', path: '/ltc/config/catalog', component: 'ltc/config/catalog/index', icon: 'mdi:format-list-bulleted', sort: 2 },
{ code: 'svc-config-photo', name: '拍照分档配置', parent: 'svc-config', path: '/ltc/config/photo', component: 'ltc/config/photo/index', icon: 'mdi:camera', sort: 3 },
{ code: 'svc-config-qc', name: '质控提醒配置', parent: 'svc-config', path: '/ltc/config/qc', component: 'ltc/config/qc/index', icon: 'mdi:bell-alert', sort: 4 },
注:
ltc组下的页面(C1–C4 业务)本任务只登记菜单节点 + 空占位页,业务逻辑后置到对应 change。前端路由按src/pages/ltc/...目录自动生成,需各建index.vue占位。
- Step 2: 写 seed-catalog.ts(幂等,用
prisma.raw跨租户)
import { PrismaClient } from '@prisma/client';
export async function seedCatalog(prisma: PrismaClient): Promise<void> {
// loc_config 骨架(昆明 + 占位州市)
await prisma.locConfig.upsert({
where: { locCode: '530100' },
update: { locName: '昆明市', svcSource: 'local' },
create: { locCode: '530100', locName: '昆明市', svcSource: 'local' },
});
// L1/L2/L3 目录项:C0 先落结构 + 少量样例(如「协助翻身叩背排痰」),
// 全量 36/45 项数据在 C1 上线前由运营导入(字段结构已就绪)。
await prisma.svcNational.upsert({
where: { natCode: 'L0000106010010000' },
update: {},
create: { natCode: 'L0000106010010000', natName: '协助翻身叩背排痰', natCategory: 'life', isMedical: false },
});
}
- Step 3: seed 入口调用(
prisma/seed.ts末尾main()内、initTenant之后)
await seedCatalog(prisma);
- Step 4: 跑 seed 验证
Run: cd sources/ec-nest && pnpm seed
Expected: 输出「seed 完成」;sys_agency 无数据、sys_svc_national 有样例、sys_loc_config 有昆明。
- Step 5: Commit
git -C sources/ec-nest add src/common/tenant-template.ts prisma/seed-catalog.ts prisma/seed.ts
git -C sources/ec-nest commit -m "feat(c0): 长护服务菜单组 + 目录 seed 骨架"
Task 4: agency(机构)模块
Files:
- Create:
sources/ec-nest/src/agency/agency.module.ts - Create:
sources/ec-nest/src/agency/agency.controller.ts - Create:
sources/ec-nest/src/agency/agency.service.ts - Create:
sources/ec-nest/src/agency/dto.ts - Modify:
sources/ec-nest/src/app.module.ts(注册AgencyModule)
Interfaces:
-
Produces:
GET /api/v1/agency(当前机构信息)、PATCH /api/v1/agency(更新机构信息,qc/admin)。 -
Consumes:
PrismaService.client、getRequestContext()、Permissions.svcOrg(暂用)、ErrorCodes.ORG_*。 -
Step 1: 写失败测试
Create sources/ec-nest/src/agency/agency.service.spec.ts(vitest):
import { describe, it, expect, vi } from 'vitest';
// 用内存 mock 的 prisma + ctx,验证 getCurrent() 按 ctx.tenantCode 读 agency
describe('AgencyService', () => {
it('getCurrent 按当前租户读机构', async () => {
const prisma = { client: { agency: { findUnique: vi.fn().mockResolvedValue({ code: 'T001' }) } } };
const ctx = { tenantCode: 'T001', userId: 'u1' };
vi.mock('../common/context/request-context.js', () => ({ getRequestContext: () => ctx }));
// ...调用 service.getCurrent(),断言 findUnique 收到 { where: { code: 'T001' } }
});
});
- Step 2: 跑测试确认失败
Run: cd sources/ec-nest && pnpm test -- agency.service.spec
Expected: FAIL(AgencyService 不存在)。
- Step 3: 实现 agency.service.ts(关键逻辑:按 tenant 读、写时 code 必须 = ctx.tenantCode)
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service.js';
import { getRequestContext } from '../common/context/request-context.js';
import { ErrorCodes } from '../common/error-codes.js';
import { BusinessException } from '../common/business-exception.js';
import { HttpStatus } from '@nestjs/common';
@Injectable()
export class AgencyService {
constructor(private readonly prisma: PrismaService) {}
async getCurrent() {
const ctx = getRequestContext();
return this.prisma.client.agency.findUnique({ where: { code: ctx.tenantCode } });
}
async update(dto: Record<string, unknown>) {
const ctx = getRequestContext();
const existing = await this.getCurrent();
if (!existing) throw new BusinessException(ErrorCodes.ORG_NOT_FOUND, '机构不存在', HttpStatus.NOT_FOUND);
return this.prisma.client.agency.update({ where: { code: ctx.tenantCode }, data: dto });
}
}
-
Step 4: 实现 controller + dto + module(controller 用
@RequirePermissions(Permissions.agency.update)+@AuditLog('agency','update')) -
Step 5: 跑测试确认通过
Run: cd sources/ec-nest && pnpm test -- agency.service.spec
Expected: PASS。
- Step 6: Commit
git -C sources/ec-nest add src/agency src/app.module.ts
git -C sources/ec-nest commit -m "feat(c0): agency 机构模块(按租户读写机构信息)"
Task 5: staff(人员)模块
Files:
- Create:
sources/ec-nest/src/staff/staff.module.ts、staff.controller.ts、staff.service.ts、dto.ts - Modify:
sources/ec-nest/src/app.module.ts
Interfaces:
-
Produces:
GET /api/v1/staff、POST /api/v1/staff、PATCH /api/v1/staff/:id、DELETE /api/v1/staff/:id;POST /api/v1/staff/:id/group(指定归属组长)。 -
Consumes:
BaseCrudService、Permissions.staff.*、ErrorCodes.STAFF_*。 -
Step 1: 写失败测试(staff.service.spec:
create自动注入 tenantCode;userId冲突抛 STAFF_USER_LINKED) -
Step 2: 跑测试确认失败
-
Step 3: 实现 staff.service.ts(extends BaseCrudService('staff');create 校验 userId 唯一;update 校验 user 存在)
-
Step 4: 实现 controller + dto + module(写操作
@AuditLog('staff','xxx');group-assign 用staff.group-assign) -
Step 5: 跑测试确认通过
-
Step 6: Commit
git -C sources/ec-nest add src/staff src/app.module.ts
git -C sources/ec-nest commit -m "feat(c0): staff 人员模块(user_id 关联账号 + 归属组长)"
Task 6: catalog(目录)模块
Files:
- Create:
sources/ec-nest/src/catalog/catalog.module.ts、catalog.controller.ts、catalog.service.ts、dto.ts - Modify:
sources/ec-nest/src/app.module.ts
Interfaces:
-
Produces:
GET /api/v1/svc/local/list?loc_code=(按属地加载目录)、GET /api/v1/svc/org/list(本机构项目库)、POST /api/v1/svc/org/config(勾选+配价)、GET /api/v1/svc/org/new-national(国家新增项)。 -
Consumes:
Permissions.svcOrg.*、ErrorCodes.SVC_*。 -
Step 1: 写失败测试(catalog.service.spec:昆明 loc_code 返回 local 目录;svc_org 写操作带 tenantCode;svc_org.config 只允许改 isEnabled/price,改其他字段抛 SVC_LOC_IMMUTABLE)
-
Step 2: 跑测试确认失败
-
Step 3: 实现 catalog.service.ts(
listLocal(locCode)读全局 SvcLocal/SvcNational;listOrg()读当前租户 SvcOrg;config()校验只改 isEnabled/price) -
Step 4: 实现 controller + dto + module(写操作
@AuditLog('svc-org','config')) -
Step 5: 跑测试确认通过
-
Step 6: Commit
git -C sources/ec-nest add src/catalog src/app.module.ts
git -C sources/ec-nest commit -m "feat(c0): catalog 目录模块(属地加载 + 机构项目库)"
Task 7: 前端业务菜单 + 占位页
Files:
- Create:
sources/ec-admin/src/pages/ltc/elder/index.vue(占位页,其余 C1–C4) - Create:
sources/ec-admin/src/pages/ltc/config/info/index.vue、.../catalog/index.vue、.../photo/index.vue、.../qc/index.vue(占位页) - Create:
sources/ec-admin/src/api/agency.ts、src/api/catalog.ts、src/api/staff.ts - Modify:
sources/ec-admin/src/config/layout.ts(如需要:登录落地路径改为/ltc/elder)
Interfaces:
-
Consumes:
http.get/post/patch(@/api/http)、useDict、ty-crud。 -
Produces: 前端页面按
unplugin-vue-router目录自动生成路由。 -
Step 1: 建占位页(每个
index.vue最小可渲染,含defineOptions({ name }),name = 路径末段且全局唯一)
<template><div>服务对象(C1 实现)</div></template>
<script setup lang="ts">
defineOptions({ name: 'elder' })
</script>
- Step 2: 建 api 封装(
src/api/agency.ts等,类型 + 方法)
import { http } from './http'
export interface Agency { code: string; orgName: string; orgType: string; locCode: string }
export const getAgency = () => http.get<Agency>('/agency')
export const updateAgency = (data: Partial<Agency>) => http.patch<Agency>('/agency', data)
-
Step 3: 登录落地改到服务对象(config/layout.ts 或路由守卫里的默认首页改为
/ltc/elder) -
Step 4: 启动前端验证菜单(跑
pnpm sync:resources后登录,确认「长护服务」菜单出现、脚手架菜单仍在)
Run: cd sources/ec-nest && pnpm sync:resources,然后 cd sources/ec-admin && pnpm dev:dev,登录 touyun/Touyun@123。
Expected: 侧边栏出现「长护服务」及 6 个子菜单(含「服务机构设置」二级组);脚手架「权限管理/系统管理/系统监控」原样。
- Step 5: Commit
git -C sources/ec-admin add src/pages/ltc src/api/agency.ts src/api/catalog.ts src/api/staff.ts src/config/layout.ts
git -C sources/ec-admin commit -m "feat(c0): 长护服务菜单占位页 + 机构/目录/人员 api"
Task 8: 端到端验证
- Step 1: 跨租户隔离:用 A 机构账号 curl
GET /api/v1/staff,确认只返回本 tenant 数据;改tenantCode越权访问返回 4001。 - Step 2: 仅 director 写接口 403:仅持 director 的账号调用
PATCH /api/v1/agency→ 403;兼 qc/admin 放行。 - Step 3: 昆明机构加载目录:
GET /api/v1/svc/local/list?loc_code=530100返回样例目录 + 国家新增项接口正常。 - Step 4: 审计落库:改机构信息后查
sys_audit_log有记录。 - Step 5: Commit 收尾
git -C sources/ec-nest status && git -C sources/ec-admin status
# 确认无遗漏后,根仓库更新子模块指针并推送
Self-Review Notes
- 覆盖:spec 的 5 个 capability(data-model/api-contract/backend org/backend catalog/frontend ad)分别由 Task 1/2/6/4+5/7 落实。
- 命名:
Agency(机构)避免与脚手架Org(组织)冲突;svc_org用SvcOrg模型。 - 全局 vs 租户:Task 1 明确 TENANT_FREE / SOFT_DELETE 清单。
- 业务菜单:Task 3 登记 MENUS,Task 7 建占位页;C1–C4 页面本 change 只占位。