feat(c0): agency 机构模块(按租户读写机构信息)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
198517bde4
commit
856e906740
23
src/agency/agency.controller.ts
Normal file
23
src/agency/agency.controller.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
9
src/agency/agency.module.ts
Normal file
9
src/agency/agency.module.ts
Normal 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 {}
|
||||
39
src/agency/agency.service.spec.ts
Normal file
39
src/agency/agency.service.spec.ts
Normal 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: '新名称' } });
|
||||
});
|
||||
});
|
||||
31
src/agency/agency.service.ts
Normal file
31
src/agency/agency.service.ts
Normal 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
61
src/agency/dto.ts
Normal 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;
|
||||
}
|
||||
@ -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: [
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user