forked from wangziqi/gongxue-base
feat: replace tenants with organization management
This commit is contained in:
62
apps/server/src/organizations/dto/organization.dto.ts
Normal file
62
apps/server/src/organizations/dto/organization.dto.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { IsBoolean, IsEnum, IsNotEmpty, IsOptional, IsString, Matches } from 'class-validator';
|
||||
|
||||
export class CreateOrganizationDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
name: string;
|
||||
|
||||
@IsString()
|
||||
@Matches(/^[A-Z0-9_-]+$/)
|
||||
code: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
isHost?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
contactName?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
phone?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
color?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export class UpdateOrganizationDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
name?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Matches(/^[A-Z0-9_-]+$/)
|
||||
code?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
contactName?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
phone?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
color?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
notes?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(['active', 'archived'])
|
||||
status?: 'active' | 'archived';
|
||||
}
|
||||
98
apps/server/src/organizations/organizations.controller.ts
Normal file
98
apps/server/src/organizations/organizations.controller.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Put,
|
||||
Delete,
|
||||
Body,
|
||||
Param,
|
||||
Query,
|
||||
UseGuards,
|
||||
Request,
|
||||
} from '@nestjs/common';
|
||||
import { OrganizationsService } from './organizations.service';
|
||||
import { CreateOrganizationDto, UpdateOrganizationDto } from './dto/organization.dto';
|
||||
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
||||
import { OperationLogsService } from '../operation-logs/operation-logs.service';
|
||||
import { extractRequestInfo } from '../common/request-utils';
|
||||
import { RequirePermission } from '../auth/decorators/permission.decorator';
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Controller('organizations')
|
||||
export class OrganizationsController {
|
||||
constructor(
|
||||
private service: OrganizationsService,
|
||||
private logService: OperationLogsService,
|
||||
) {}
|
||||
|
||||
@Get()
|
||||
@RequirePermission('organization:view')
|
||||
findAll(
|
||||
@Query('includeArchived') includeArchived?: string,
|
||||
@Query('scope') scope?: 'all' | 'host' | 'external',
|
||||
) {
|
||||
return this.service.findAll({ includeArchived: includeArchived === 'true', scope });
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@RequirePermission('organization:view')
|
||||
findOne(@Param('id') id: string) {
|
||||
return this.service.findOne(+id);
|
||||
}
|
||||
|
||||
@Post()
|
||||
@RequirePermission('organization:create')
|
||||
async create(@Body() dto: CreateOrganizationDto, @Request() req: any) {
|
||||
const { ipAddress, userAgent } = extractRequestInfo(req);
|
||||
const result = await this.service.create(dto);
|
||||
await this.logService.log({
|
||||
userId: req.user?.id,
|
||||
username: req.user?.username,
|
||||
module: '机构管理',
|
||||
action: '新增机构',
|
||||
targetId: result.id,
|
||||
targetType: 'organization',
|
||||
detail: dto.name,
|
||||
ipAddress,
|
||||
userAgent,
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
@RequirePermission('organization:edit')
|
||||
async update(@Param('id') id: string, @Body() dto: UpdateOrganizationDto, @Request() req: any) {
|
||||
const { ipAddress, userAgent } = extractRequestInfo(req);
|
||||
const result = await this.service.update(+id, dto);
|
||||
await this.logService.log({
|
||||
userId: req.user?.id,
|
||||
username: req.user?.username,
|
||||
module: '机构管理',
|
||||
action: '编辑机构',
|
||||
targetId: +id,
|
||||
targetType: 'organization',
|
||||
detail: JSON.stringify(dto),
|
||||
ipAddress,
|
||||
userAgent,
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@RequirePermission('organization:delete')
|
||||
async remove(@Param('id') id: string, @Request() req: any) {
|
||||
const { ipAddress, userAgent } = extractRequestInfo(req);
|
||||
const result = await this.service.remove(+id);
|
||||
await this.logService.log({
|
||||
userId: req.user?.id,
|
||||
username: req.user?.username,
|
||||
module: '机构管理',
|
||||
action: '归档机构',
|
||||
targetId: +id,
|
||||
targetType: 'organization',
|
||||
ipAddress,
|
||||
userAgent,
|
||||
});
|
||||
return result;
|
||||
}
|
||||
}
|
||||
30
apps/server/src/organizations/organizations.module.ts
Normal file
30
apps/server/src/organizations/organizations.module.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { Module, OnModuleInit } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { Organization } from '../entities/organization.entity';
|
||||
import { OrganizationsService } from './organizations.service';
|
||||
import { OrganizationsController } from './organizations.controller';
|
||||
import { OperationLogsModule } from '../operation-logs/operation-logs.module';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Organization]), OperationLogsModule],
|
||||
controllers: [OrganizationsController],
|
||||
providers: [OrganizationsService],
|
||||
exports: [OrganizationsService],
|
||||
})
|
||||
export class OrganizationsModule implements OnModuleInit {
|
||||
constructor(private readonly service: OrganizationsService) {}
|
||||
|
||||
async onModuleInit() {
|
||||
try {
|
||||
await this.service.getHostOrganization();
|
||||
} catch {
|
||||
await this.service.create({
|
||||
name: process.env.HOST_ORGANIZATION_NAME || '本机构',
|
||||
code: process.env.HOST_ORGANIZATION_CODE || 'HOST',
|
||||
isHost: true,
|
||||
color: '#1677ff',
|
||||
notes: '系统默认运营主体',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
30
apps/server/src/organizations/organizations.service.spec.ts
Normal file
30
apps/server/src/organizations/organizations.service.spec.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
import { Repository } from 'typeorm';
|
||||
import { OrganizationsService } from './organizations.service';
|
||||
import { Organization } from '../entities/organization.entity';
|
||||
|
||||
describe('OrganizationsService — host organization rules', () => {
|
||||
let repo: jest.Mocked<
|
||||
Pick<Repository<Organization>, 'findOne' | 'find' | 'count' | 'create' | 'save' | 'update'>
|
||||
>;
|
||||
let service: OrganizationsService;
|
||||
|
||||
beforeEach(() => {
|
||||
repo = {
|
||||
findOne: jest.fn(),
|
||||
find: jest.fn(),
|
||||
count: jest.fn(),
|
||||
create: jest.fn(),
|
||||
save: jest.fn(),
|
||||
update: jest.fn(),
|
||||
} as any;
|
||||
service = new OrganizationsService(repo as Repository<Organization>);
|
||||
});
|
||||
|
||||
it('does not allow the host organization to be archived', async () => {
|
||||
repo.findOne.mockResolvedValue({ id: 1, name: '本机构', isHost: true } as Organization);
|
||||
|
||||
await expect(service.remove(1)).rejects.toBeInstanceOf(BadRequestException);
|
||||
expect(repo.update).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
81
apps/server/src/organizations/organizations.service.ts
Normal file
81
apps/server/src/organizations/organizations.service.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Not, Repository } from 'typeorm';
|
||||
import { uuidV7 } from '../common/uuid-v7';
|
||||
import { Organization } from '../entities/organization.entity';
|
||||
import { CreateOrganizationDto, UpdateOrganizationDto } from './dto/organization.dto';
|
||||
|
||||
const COLOR_PALETTE = [
|
||||
'#ff7875',
|
||||
'#ffa940',
|
||||
'#ffc53d',
|
||||
'#73d13d',
|
||||
'#36cfc9',
|
||||
'#40a9ff',
|
||||
'#597ef7',
|
||||
'#9254de',
|
||||
'#f759ab',
|
||||
'#8c8c8c',
|
||||
];
|
||||
|
||||
@Injectable()
|
||||
export class OrganizationsService {
|
||||
constructor(@InjectRepository(Organization) private repo: Repository<Organization>) {}
|
||||
|
||||
async findAll(query?: { includeArchived?: boolean; scope?: 'all' | 'host' | 'external' }) {
|
||||
const where: Record<string, unknown> = {};
|
||||
if (!query?.includeArchived) where.status = Not('archived');
|
||||
if (query?.scope === 'host') where.isHost = true;
|
||||
if (query?.scope === 'external') where.isHost = false;
|
||||
return this.repo.find({ where, order: { isHost: 'DESC', name: 'ASC' } });
|
||||
}
|
||||
|
||||
async findOne(id: number) {
|
||||
const organization = await this.repo.findOne({ where: { id } });
|
||||
if (!organization) throw new NotFoundException('机构不存在');
|
||||
return organization;
|
||||
}
|
||||
|
||||
async getHostOrganization() {
|
||||
const organization = await this.repo.findOne({ where: { isHost: true, status: 'active' } });
|
||||
if (!organization) throw new NotFoundException('尚未配置本机构');
|
||||
return organization;
|
||||
}
|
||||
|
||||
async create(dto: CreateOrganizationDto) {
|
||||
if (dto.isHost) {
|
||||
const existingHost = await this.repo.findOne({ where: { isHost: true } });
|
||||
if (existingHost) throw new BadRequestException('本机构已存在,只能配置一个本机构');
|
||||
}
|
||||
const color = dto.color || COLOR_PALETTE[(await this.repo.count()) % COLOR_PALETTE.length];
|
||||
return this.repo.save(
|
||||
this.repo.create({
|
||||
...dto,
|
||||
code: dto.code.trim().toUpperCase(),
|
||||
publicId: uuidV7(),
|
||||
color,
|
||||
isHost: dto.isHost ?? false,
|
||||
status: 'active',
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async update(id: number, dto: UpdateOrganizationDto) {
|
||||
const organization = await this.findOne(id);
|
||||
if (organization.isHost && dto.status === 'archived') {
|
||||
throw new BadRequestException('本机构不能归档');
|
||||
}
|
||||
await this.repo.update(id, {
|
||||
...dto,
|
||||
...(dto.code ? { code: dto.code.trim().toUpperCase() } : {}),
|
||||
});
|
||||
return this.repo.findOne({ where: { id } });
|
||||
}
|
||||
|
||||
async remove(id: number) {
|
||||
const organization = await this.findOne(id);
|
||||
if (organization.isHost) throw new BadRequestException('本机构不能归档');
|
||||
await this.repo.update(id, { status: 'archived' });
|
||||
return { message: '已归档' };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user