# 服务机构端 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 1. 跨租户数据泄漏:A 机构账号读不到 B 机构的 staff/svc_org(多租户过滤必须生效)。 2. Agency 与 tenant 一致:创建机构时 `Agency.code` MUST == `tenant.code`,两者事务内同步。 3. 目录全局 vs 业务租户隔离:svc_national/svc_local/svc_mapping/dict/loc_config 是全局(TENANT_FREE),staff/svc_org 是租户级;别把全局表误加租户过滤。 4. staff.user_id 唯一:一个 user 最多一条 staff(`@unique` 可空)。 5. 仅持 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 末尾) ```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 行改为: ```ts const TENANT_FREE = new Set(['Tenant', 'Agency', 'SvcNational', 'SvcLocal', 'SvcMapping', 'DictAbnormalType', 'DictPhotoTier', 'LocConfig']); ``` 第 8 行改为: ```ts 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** ```bash 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 末尾 `}` 前) ```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) ```ts // 服务机构端·机构(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** ```bash 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` 数组末尾、`];` 前) ```ts { 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` 跨租户) ```ts import { PrismaClient } from '@prisma/client'; export async function seedCatalog(prisma: PrismaClient): Promise { // 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` 之后) ```ts 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** ```bash 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): ```ts 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) ```ts 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) { 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** ```bash 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** ```bash 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** ```bash 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 = 路径末段且全局唯一) ```vue ``` - [ ] **Step 2: 建 api 封装**(`src/api/agency.ts` 等,类型 + 方法) ```ts import { http } from './http' export interface Agency { code: string; orgName: string; orgType: string; locCode: string } export const getAgency = () => http.get('/agency') export const updateAgency = (data: Partial) => http.patch('/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** ```bash 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 收尾** ```bash 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 只占位。