v4.0.17企业微信API插件优化
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
Param,
|
||||
Post,
|
||||
Put,
|
||||
Query,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import type {
|
||||
CreateWecomApiPluginRequest,
|
||||
UpdateWecomApiPluginRequest,
|
||||
} from '@dukang/shared-types';
|
||||
import { HqAuthGuard } from '../../common/guards/hq-auth.guard';
|
||||
import {
|
||||
HqPermissionGuard,
|
||||
RequireHqPermissions,
|
||||
} from '../../common/guards/hq-permission.guard';
|
||||
import { HqOperation } from '../../common/hq-operation/hq-operation.decorator';
|
||||
import { HqOperationAction } from '../../common/hq-operation/hq-operation.constants';
|
||||
import { AdminWecomApiPluginsService } from './admin-wecom-api-plugins.service';
|
||||
|
||||
@Controller('admin/wecom-api-plugins')
|
||||
@UseGuards(HqAuthGuard, HqPermissionGuard)
|
||||
@RequireHqPermissions('wecom_bots')
|
||||
export class AdminWecomApiPluginsController {
|
||||
constructor(private readonly service: AdminWecomApiPluginsService) {}
|
||||
|
||||
@Get()
|
||||
list(
|
||||
@Query('name') name?: string,
|
||||
@Query('enabled') enabled?: string,
|
||||
@Query('page') page?: string,
|
||||
@Query('pageSize') pageSize?: string,
|
||||
) {
|
||||
return this.service.list({
|
||||
name,
|
||||
enabled,
|
||||
page: page ? Number(page) : undefined,
|
||||
pageSize: pageSize ? Number(pageSize) : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
detail(@Param('id') id: string) {
|
||||
return this.service.detail(BigInt(id));
|
||||
}
|
||||
|
||||
@Post()
|
||||
@HqOperation({
|
||||
action: HqOperationAction.WECOM_API_PLUGIN_CREATE,
|
||||
refType: 'WECOM_API_PLUGIN',
|
||||
includeBody: false,
|
||||
})
|
||||
create(@Body() body: CreateWecomApiPluginRequest) {
|
||||
return this.service.create(body);
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
@HqOperation({
|
||||
action: HqOperationAction.WECOM_API_PLUGIN_UPDATE,
|
||||
refType: 'WECOM_API_PLUGIN',
|
||||
refIdField: 'id',
|
||||
includeBody: false,
|
||||
})
|
||||
update(@Param('id') id: string, @Body() body: UpdateWecomApiPluginRequest) {
|
||||
return this.service.update(BigInt(id), body);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@HqOperation({
|
||||
action: HqOperationAction.WECOM_API_PLUGIN_DELETE,
|
||||
refType: 'WECOM_API_PLUGIN',
|
||||
refIdField: 'id',
|
||||
})
|
||||
remove(@Param('id') id: string) {
|
||||
return this.service.remove(BigInt(id));
|
||||
}
|
||||
|
||||
@Post(':id/rotate-key')
|
||||
@HqOperation({
|
||||
action: HqOperationAction.WECOM_API_PLUGIN_ROTATE_KEY,
|
||||
refType: 'WECOM_API_PLUGIN',
|
||||
refIdField: 'id',
|
||||
})
|
||||
rotateKey(@Param('id') id: string) {
|
||||
return this.service.rotateKey(BigInt(id));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
import { randomBytes } from 'node:crypto';
|
||||
import {
|
||||
BadRequestException,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import {
|
||||
maskWecomPluginApiKey,
|
||||
parseWecomPluginPermissions,
|
||||
type CreateWecomApiPluginRequest,
|
||||
type UpdateWecomApiPluginRequest,
|
||||
type WecomApiPluginDto,
|
||||
type WecomApiPluginSecretDto,
|
||||
} from '@dukang/shared-types';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { PrismaService } from '../../common/prisma/prisma.module';
|
||||
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
|
||||
|
||||
const MIN_KEY_LEN = 16;
|
||||
|
||||
@Injectable()
|
||||
export class AdminWecomApiPluginsService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
async list(query: { name?: string; enabled?: string; page?: number; pageSize?: number }) {
|
||||
const page = query.page ?? 1;
|
||||
const pageSize = query.pageSize ?? 20;
|
||||
const where: { name?: { contains: string }; enabled?: boolean } = {};
|
||||
if (query.name?.trim()) where.name = { contains: query.name.trim() };
|
||||
if (query.enabled === 'true' || query.enabled === 'false') {
|
||||
where.enabled = query.enabled === 'true';
|
||||
}
|
||||
|
||||
const [items, total] = await Promise.all([
|
||||
this.prisma.wecomApiPlugin.findMany({
|
||||
where,
|
||||
orderBy: [{ sortOrder: 'asc' }, { id: 'asc' }],
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
}),
|
||||
this.prisma.wecomApiPlugin.count({ where }),
|
||||
]);
|
||||
|
||||
return serializeBigInt({
|
||||
items: items.map((row) => this.toDto(row)),
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
});
|
||||
}
|
||||
|
||||
async detail(id: bigint): Promise<WecomApiPluginDto> {
|
||||
return this.toDto(await this.mustGet(id));
|
||||
}
|
||||
|
||||
async create(dto: CreateWecomApiPluginRequest): Promise<WecomApiPluginSecretDto> {
|
||||
const name = dto.name?.trim();
|
||||
if (!name) throw new BadRequestException('请填写名称');
|
||||
const permissions = parseWecomPluginPermissions(dto.permissions);
|
||||
if (!permissions.length) throw new BadRequestException('请至少勾选一项工具权限');
|
||||
const apiKey = this.normalizeApiKey(dto.apiKey, true);
|
||||
|
||||
try {
|
||||
const row = await this.prisma.wecomApiPlugin.create({
|
||||
data: {
|
||||
name,
|
||||
apiKey,
|
||||
permissions: JSON.stringify(permissions),
|
||||
remark: dto.remark?.trim() || null,
|
||||
enabled: dto.enabled !== false,
|
||||
sortOrder: dto.sortOrder ?? 0,
|
||||
},
|
||||
});
|
||||
return { ...this.toDto(row), apiKey };
|
||||
} catch (e) {
|
||||
this.rethrowUniqueKey(e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async update(id: bigint, dto: UpdateWecomApiPluginRequest): Promise<WecomApiPluginDto> {
|
||||
const existing = await this.mustGet(id);
|
||||
const data: Prisma.WecomApiPluginUpdateInput = {};
|
||||
|
||||
if (dto.name !== undefined) {
|
||||
const name = dto.name.trim();
|
||||
if (!name) throw new BadRequestException('请填写名称');
|
||||
data.name = name;
|
||||
}
|
||||
if (dto.permissions !== undefined) {
|
||||
const permissions = parseWecomPluginPermissions(dto.permissions);
|
||||
if (!permissions.length) throw new BadRequestException('请至少勾选一项工具权限');
|
||||
data.permissions = JSON.stringify(permissions);
|
||||
}
|
||||
if (dto.remark !== undefined) data.remark = dto.remark?.trim() || null;
|
||||
if (dto.enabled !== undefined) data.enabled = dto.enabled;
|
||||
if (dto.sortOrder !== undefined) data.sortOrder = dto.sortOrder;
|
||||
if (dto.apiKey !== undefined && String(dto.apiKey).trim()) {
|
||||
data.apiKey = this.normalizeApiKey(dto.apiKey, false);
|
||||
}
|
||||
|
||||
try {
|
||||
const row = await this.prisma.wecomApiPlugin.update({
|
||||
where: { id: existing.id },
|
||||
data,
|
||||
});
|
||||
return this.toDto(row);
|
||||
} catch (e) {
|
||||
this.rethrowUniqueKey(e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async remove(id: bigint) {
|
||||
await this.mustGet(id);
|
||||
await this.prisma.wecomApiPlugin.delete({ where: { id } });
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
async rotateKey(id: bigint): Promise<WecomApiPluginSecretDto> {
|
||||
await this.mustGet(id);
|
||||
const apiKey = generateApiKey();
|
||||
try {
|
||||
const row = await this.prisma.wecomApiPlugin.update({
|
||||
where: { id },
|
||||
data: { apiKey },
|
||||
});
|
||||
return { ...this.toDto(row), apiKey };
|
||||
} catch (e) {
|
||||
this.rethrowUniqueKey(e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private async mustGet(id: bigint) {
|
||||
const row = await this.prisma.wecomApiPlugin.findUnique({ where: { id } });
|
||||
if (!row) throw new NotFoundException('API 插件不存在');
|
||||
return row;
|
||||
}
|
||||
|
||||
private normalizeApiKey(raw: string | undefined, generateIfEmpty: boolean): string {
|
||||
const v = String(raw ?? '').trim();
|
||||
if (!v) {
|
||||
if (generateIfEmpty) return generateApiKey();
|
||||
throw new BadRequestException('请填写 API Key');
|
||||
}
|
||||
if (v.length < MIN_KEY_LEN) {
|
||||
throw new BadRequestException(`API Key 至少 ${MIN_KEY_LEN} 位`);
|
||||
}
|
||||
if (v.length > 128) throw new BadRequestException('API Key 过长');
|
||||
return v;
|
||||
}
|
||||
|
||||
private rethrowUniqueKey(e: unknown): void {
|
||||
if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2002') {
|
||||
throw new BadRequestException('API Key 已存在,请更换');
|
||||
}
|
||||
}
|
||||
|
||||
private toDto(row: {
|
||||
id: bigint;
|
||||
name: string;
|
||||
apiKey: string;
|
||||
permissions: string;
|
||||
remark: string | null;
|
||||
enabled: boolean;
|
||||
sortOrder: number;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}): WecomApiPluginDto {
|
||||
return {
|
||||
id: row.id.toString(),
|
||||
name: row.name,
|
||||
apiKeyConfigured: Boolean(row.apiKey),
|
||||
apiKeyMasked: maskWecomPluginApiKey(row.apiKey),
|
||||
permissions: parseWecomPluginPermissions(row.permissions),
|
||||
remark: row.remark,
|
||||
enabled: row.enabled,
|
||||
sortOrder: row.sortOrder,
|
||||
createdAt: row.createdAt.toISOString(),
|
||||
updatedAt: row.updatedAt.toISOString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function generateApiKey(): string {
|
||||
return `dkp_${randomBytes(24).toString('hex')}`;
|
||||
}
|
||||
@@ -69,6 +69,8 @@ import { AdminWecomBotsController } from './admin-wecom-bots.controller';
|
||||
import { AdminWecomBotsService } from './admin-wecom-bots.service';
|
||||
import { AdminWecomMessagePushesController } from './admin-wecom-message-pushes.controller';
|
||||
import { AdminWecomMessagePushesService } from './admin-wecom-message-pushes.service';
|
||||
import { AdminWecomApiPluginsController } from './admin-wecom-api-plugins.controller';
|
||||
import { AdminWecomApiPluginsService } from './admin-wecom-api-plugins.service';
|
||||
import { AdminWecomReportsController } from './admin-wecom-reports.controller';
|
||||
import { AdminWecomReportsService } from './admin-wecom-reports.service';
|
||||
import { AdminWecomPushTemplatesController } from './admin-wecom-push-templates.controller';
|
||||
@@ -130,6 +132,7 @@ import { PartnerActivityPostersController } from './partner-activity-posters.con
|
||||
AdminSystemConfigController,
|
||||
AdminWecomBotsController,
|
||||
AdminWecomMessagePushesController,
|
||||
AdminWecomApiPluginsController,
|
||||
AdminWecomReportsController,
|
||||
AdminWecomPushTemplatesController,
|
||||
AdminWecomBotLogsController,
|
||||
@@ -169,6 +172,7 @@ import { PartnerActivityPostersController } from './partner-activity-posters.con
|
||||
AdminDeployService,
|
||||
AdminWecomBotsService,
|
||||
AdminWecomMessagePushesService,
|
||||
AdminWecomApiPluginsService,
|
||||
AdminWecomReportsService,
|
||||
AdminWecomBotLogsService,
|
||||
AdminLlmConfigsService,
|
||||
|
||||
Reference in New Issue
Block a user