touyun-elderly-care/docs/superpowers/plans/2026-10-14-服务机构端-C0基础骨架.md
liuyun 547b414bfa docs: 服务机构端 C0 基础骨架实施计划
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-14 12:28:00 +08:00

543 lines
24 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 服务机构端 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<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` 之后)
```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<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**
```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
<template><div>服务对象(C1 实现)</div></template>
<script setup lang="ts">
defineOptions({ name: 'elder' })
</script>
```
- [ ] **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>('/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**
```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 只占位。