feat(c0): agency 机构模块(按租户读写机构信息)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
liuyun 2026-10-14 12:48:27 +08:00
parent 198517bde4
commit 856e906740
6 changed files with 165 additions and 0 deletions

View File

@ -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);
}
}

View File

@ -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 {}

View File

@ -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: '新名称' } });
});
});

View File

@ -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 });
}
}

61
src/agency/dto.ts Normal file
View File

@ -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;
}

View File

@ -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: [