diff --git a/docs/superpowers/plans/2026-10-14-服务机构端-C0基础骨架.md b/docs/superpowers/plans/2026-10-14-服务机构端-C0基础骨架.md new file mode 100644 index 0000000..0cb8922 --- /dev/null +++ b/docs/superpowers/plans/2026-10-14-服务机构端-C0基础骨架.md @@ -0,0 +1,542 @@ +# 服务机构端 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 只占位。