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

24 KiB
Raw Permalink Blame History

服务机构端 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 末尾)

/// 机构(=租户的业务信息,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 只占位。