feat(api): A4 数据字典与系统配置后端
背景:A4 需要字典三层合并、配置单一来源与阈值同源。\n改动:新增 catalog 模块(类型/条目/三层合并/编码不可改/快照工具);platform 新增 SystemConfigService/Controller、NamingService 与配置版本历史;roles/users/customers/customer-fields 改为读 system_configs 上限。\n影响面:新增 13 个 REST 端点与 4 个模块注入;system_configs 写入走事务/乐观锁/版本历史;OpenAPI 快照更新。\n回滚:回滚本提交并恢复 shared 契约;新增表可保留。\n验证:api typecheck/build;单测 137;集成 58;OpenAPI diff 无破坏性变更。
This commit is contained in:
1 parent
cc6e84cdbc
commit
e4330a43e4
25 files changed
+4189
-30
No files matched your search
@@ -18,6 +18,7 @@ import type { ScopeContext } from '../../common/security/scope/scope-context';
|
||||
import { isCustomerVisible, requireScopeContext } from '../../common/security/scope/scope-context';
|
||||
import type { PrismaService } from '../../infra/prisma/prisma.service';
|
||||
import { PRISMA } from '../../infra/tokens';
|
||||
import { SystemConfigService } from '../platform/system-config/system-config.service';
|
||||
import { type CustomerFieldRow, toCustomerField } from './client.mapper';
|
||||
|
||||
const FIELD_INCLUDE = {
|
||||
@@ -51,7 +52,10 @@ function industryPath(customer: {
|
||||
*/
|
||||
@Injectable()
|
||||
export class CustomerFieldsService {
|
||||
constructor(@Inject(PRISMA) private readonly prisma: PrismaService) {}
|
||||
constructor(
|
||||
@Inject(PRISMA) private readonly prisma: PrismaService,
|
||||
@Inject(SystemConfigService) private readonly systemConfig: SystemConfigService,
|
||||
) {}
|
||||
|
||||
async list(
|
||||
query: CustomerFieldListQuery,
|
||||
@@ -147,12 +151,7 @@ export class CustomerFieldsService {
|
||||
}
|
||||
if (input.entersGeneration) {
|
||||
const used = await this.genUsedCount(customerId);
|
||||
if (used >= CUSTOMER_GEN_FIELD_LIMIT) {
|
||||
throw new AppException({
|
||||
code: ErrorCode.PARAM_INVALID,
|
||||
message: `进入生成字段已达 ${CUSTOMER_GEN_FIELD_LIMIT} 个上限,请先停用或驳回其他字段`,
|
||||
});
|
||||
}
|
||||
await this.assertGenFieldLimit(used);
|
||||
}
|
||||
const id = ulid();
|
||||
await this.prisma.customerField.create({
|
||||
@@ -200,12 +199,7 @@ export class CustomerFieldsService {
|
||||
}
|
||||
if (input.decision === 'approve' && existing.entersGeneration) {
|
||||
const used = await this.genUsedCount(existing.customerId, id);
|
||||
if (used >= CUSTOMER_GEN_FIELD_LIMIT) {
|
||||
throw new AppException({
|
||||
code: ErrorCode.PARAM_INVALID,
|
||||
message: `进入生成字段已达 ${CUSTOMER_GEN_FIELD_LIMIT} 个上限,审批后仍超限`,
|
||||
});
|
||||
}
|
||||
await this.assertGenFieldLimit(used, '审批后仍超限');
|
||||
}
|
||||
await this.prisma.customerField.update({
|
||||
where: { id },
|
||||
@@ -231,6 +225,20 @@ export class CustomerFieldsService {
|
||||
return toCustomerField(row);
|
||||
}
|
||||
|
||||
/** 进入生成字段上限读 system_configs(R-26);shared 常量仅作读失败兜底(默认 5) */
|
||||
private async assertGenFieldLimit(used: number, hint = '请先停用或驳回其他字段'): Promise<void> {
|
||||
const limit = await this.systemConfig.getNumber(
|
||||
'limit.customer.gen_field',
|
||||
CUSTOMER_GEN_FIELD_LIMIT,
|
||||
);
|
||||
if (used >= limit) {
|
||||
throw new AppException({
|
||||
code: ErrorCode.PARAM_INVALID,
|
||||
message: `进入生成字段已达 ${String(limit)} 个上限,${hint}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async genUsedCount(customerId: string, excludeId?: string): Promise<number> {
|
||||
return this.prisma.customerField.count({
|
||||
where: {
|
||||
|
||||
@@ -22,6 +22,7 @@ import type { ScopeContext } from '../../common/security/scope/scope-context';
|
||||
import { isCustomerVisible, requireScopeContext } from '../../common/security/scope/scope-context';
|
||||
import type { PrismaService } from '../../infra/prisma/prisma.service';
|
||||
import { PRISMA } from '../../infra/tokens';
|
||||
import { SystemConfigService } from '../platform/system-config/system-config.service';
|
||||
import { type CustomerSummaryRow, toCustomerDetail, toCustomerSummary } from './client.mapper';
|
||||
import { IndustriesService } from './industries.service';
|
||||
|
||||
@@ -61,6 +62,7 @@ export class CustomersService {
|
||||
constructor(
|
||||
@Inject(PRISMA) private readonly prisma: PrismaService,
|
||||
@Inject(IndustriesService) private readonly industries: IndustriesService,
|
||||
@Inject(SystemConfigService) private readonly systemConfig: SystemConfigService,
|
||||
) {}
|
||||
|
||||
async list(
|
||||
@@ -126,7 +128,7 @@ export class CustomersService {
|
||||
await this.industries.assertLeafIndustry(input.industryId);
|
||||
await this.ensureOwner(input.ownerUserId);
|
||||
const baseFields = input.baseFields ?? [];
|
||||
this.assertBaseFieldLimit(baseFields.length);
|
||||
await this.assertBaseFieldLimit(baseFields.length);
|
||||
const id = ulid();
|
||||
try {
|
||||
await this.prisma.customer.create({
|
||||
@@ -184,7 +186,7 @@ export class CustomersService {
|
||||
if (input.ownerUserId !== undefined && input.ownerUserId !== existing.ownerUserId) {
|
||||
await this.ensureOwner(input.ownerUserId);
|
||||
}
|
||||
if (input.baseFields !== undefined) this.assertBaseFieldLimit(input.baseFields.length);
|
||||
if (input.baseFields !== undefined) await this.assertBaseFieldLimit(input.baseFields.length);
|
||||
if (input.name !== undefined && input.name !== existing.name) {
|
||||
const duplicate = await this.prisma.customer.findFirst({
|
||||
where: {
|
||||
@@ -377,11 +379,16 @@ export class CustomersService {
|
||||
}
|
||||
}
|
||||
|
||||
private assertBaseFieldLimit(count: number): void {
|
||||
if (count > CUSTOMER_BASE_FIELD_LIMIT) {
|
||||
/** 基础字段上限读 system_configs(R-26);shared 常量仅作读失败兜底(默认 10) */
|
||||
private async assertBaseFieldLimit(count: number): Promise<void> {
|
||||
const limit = await this.systemConfig.getNumber(
|
||||
'limit.customer.base_field',
|
||||
CUSTOMER_BASE_FIELD_LIMIT,
|
||||
);
|
||||
if (count > limit) {
|
||||
throw new AppException({
|
||||
code: ErrorCode.PARAM_INVALID,
|
||||
message: `基础字段最多 ${CUSTOMER_BASE_FIELD_LIMIT} 个,当前 ${count} 个`,
|
||||
message: `基础字段最多 ${String(limit)} 个,当前 ${String(count)} 个`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user