diff --git a/src/agency/agency.controller.ts b/src/agency/agency.controller.ts new file mode 100644 index 0000000..dd94ab8 --- /dev/null +++ b/src/agency/agency.controller.ts @@ -0,0 +1,23 @@ +import { Body, Controller, Get, Patch } from '@nestjs/common'; +import { Permissions } from '../common/permissions.js'; +import { AuditLog, RequirePermissions } from '../auth/decorators.js'; +import { UpdateAgencyDto } from './dto.js'; +import { AgencyService } from './agency.service.js'; + +@Controller('agency') +export class AgencyController { + constructor(private readonly service: AgencyService) {} + + @Get() + @RequirePermissions(Permissions.agency.view) + get() { + return this.service.getCurrent(); + } + + @Patch() + @RequirePermissions(Permissions.agency.update) + @AuditLog('agency', 'update') + update(@Body() dto: UpdateAgencyDto) { + return this.service.update(dto); + } +} diff --git a/src/agency/agency.module.ts b/src/agency/agency.module.ts new file mode 100644 index 0000000..f817a14 --- /dev/null +++ b/src/agency/agency.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { AgencyController } from './agency.controller.js'; +import { AgencyService } from './agency.service.js'; + +@Module({ + controllers: [AgencyController], + providers: [AgencyService], +}) +export class AgencyModule {} diff --git a/src/agency/agency.service.spec.ts b/src/agency/agency.service.spec.ts new file mode 100644 index 0000000..f52e178 --- /dev/null +++ b/src/agency/agency.service.spec.ts @@ -0,0 +1,39 @@ +import { describe, expect, it, vi } from 'vitest'; +import { AgencyService } from './agency.service.js'; +import type { PrismaService } from '../prisma/prisma.service.js'; +import { runWithRequestContext } from '../common/context/request-context.js'; +import type { RequestContext } from '../common/context/request-context.js'; + +const ctx: RequestContext = { + tenantCode: 'T001', + userId: 'u1', + username: 'admin', + super: false, + dataScope: 'self', + orgCode: null, + orgCodes: [], +}; + +describe('AgencyService', () => { + it('getCurrent 按当前租户 code 读机构', async () => { + const findUnique = vi.fn().mockResolvedValue({ code: 'T001', orgName: '测试机构' }); + const prisma = { client: { agency: { findUnique, update: vi.fn() } } }; + const service = new AgencyService(prisma as unknown as PrismaService); + + const result = await runWithRequestContext(ctx, () => service.getCurrent()); + + expect(findUnique).toHaveBeenCalledWith({ where: { code: 'T001' } }); + expect(result).toEqual({ code: 'T001', orgName: '测试机构' }); + }); + + it('update 按当前租户 code 更新机构', async () => { + const findUnique = vi.fn().mockResolvedValue({ code: 'T001' }); + const update = vi.fn().mockResolvedValue({ code: 'T001', orgName: '新名称' }); + const prisma = { client: { agency: { findUnique, update } } }; + const service = new AgencyService(prisma as unknown as PrismaService); + + await runWithRequestContext(ctx, () => service.update({ orgName: '新名称' })); + + expect(update).toHaveBeenCalledWith({ where: { code: 'T001' }, data: { orgName: '新名称' } }); + }); +}); diff --git a/src/agency/agency.service.ts b/src/agency/agency.service.ts new file mode 100644 index 0000000..338cf7d --- /dev/null +++ b/src/agency/agency.service.ts @@ -0,0 +1,31 @@ +import { HttpStatus, 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 { UpdateAgencyDto } from './dto.js'; + +/** + * 机构(=租户)信息读写。Agency 是 TENANT_FREE 模型,主键 code = 当前租户 code, + * 因此读写一律按 `ctx.tenantCode` 定位,而非 BaseCrudService 的 `findById(id)`。 + */ +@Injectable() +export class AgencyService { + constructor(private readonly prisma: PrismaService) {} + + /** 当前租户的机构信息(code = ctx.tenantCode)。 */ + async getCurrent() { + const ctx = getRequestContext()!; + return this.prisma.client.agency.findUnique({ where: { code: ctx.tenantCode } }); + } + + /** 更新当前机构信息;写操作同样按 code = ctx.tenantCode 定位,禁止按 id。 */ + async update(dto: UpdateAgencyDto) { + 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 }); + } +} diff --git a/src/agency/dto.ts b/src/agency/dto.ts new file mode 100644 index 0000000..6cb6cae --- /dev/null +++ b/src/agency/dto.ts @@ -0,0 +1,61 @@ +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { IsDateString, IsInt, IsOptional, IsString, Min } from 'class-validator'; + +/** + * 更新机构信息。仅暴露业务可编辑字段: + * - 不含主键 `code`(= 租户 code,服务端按 ctx.tenantCode 注入); + * - 不含 `locCode`/`locName`(参保地,PRD 定为只读); + * - 不含 `createdAt/createdBy/updatedAt/deletedAt`(系统字段)。 + */ +export class UpdateAgencyDto { + @ApiPropertyOptional({ description: '机构名称' }) + @IsOptional() + @IsString() + orgName?: string; + + @ApiPropertyOptional({ description: '机构类型' }) + @IsOptional() + @IsString() + orgType?: string; + + @ApiPropertyOptional({ description: '统一社会信用代码' }) + @IsOptional() + @IsString() + creditCode?: string; + + @ApiPropertyOptional({ description: '服务来源' }) + @IsOptional() + @IsString() + svcSource?: string; + + @ApiPropertyOptional({ description: '许可证号' }) + @IsOptional() + @IsString() + licenseNo?: string; + + @ApiPropertyOptional({ description: '地址' }) + @IsOptional() + @IsString() + address?: string; + + @ApiPropertyOptional({ description: '法人/联系人' }) + @IsOptional() + @IsString() + contactName?: string; + + @ApiPropertyOptional({ description: '联系方式' }) + @IsOptional() + @IsString() + contactPhone?: string; + + @ApiPropertyOptional({ description: '床位数' }) + @IsOptional() + @IsInt() + @Min(0) + bedCount?: number; + + @ApiPropertyOptional({ description: '签约日期(YYYY-MM-DD)' }) + @IsOptional() + @IsDateString() + signDate?: string; +} diff --git a/src/app.module.ts b/src/app.module.ts index decb691..44ad83f 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -25,6 +25,7 @@ import { AuditLogsModule } from './audit-logs/audit-logs.module.js'; import { LoginLogsModule } from './login-logs/login-logs.module.js'; import { SessionsModule } from './sessions/sessions.module.js'; import { NoticesModule } from './notices/notices.module.js'; +import { AgencyModule } from './agency/agency.module.js'; import { AppController } from './app.controller.js'; import { AppService } from './app.service.js'; @@ -47,6 +48,7 @@ import { AppService } from './app.service.js'; LoginLogsModule, SessionsModule, NoticesModule, + AgencyModule, ], controllers: [AppController], providers: [