背景:A3 需把行业 / 客户 / 客户组 / 客户级字段立为资产根节点;用户确认两级行业树与混搭 UI(行业 B / 客户列表 A / 详情 A / 新建 B / 客户组 B / 字段 A)。 改动: - 新增 ADR-002,行业改为两级树,客户挂末级行业;同步 PRD / 技术架构 / 开发计划。 - 新增 client 模块:industries / customers / customer-groups / customer-fields 接口、真实 PrismaCustomerDirectory、客户状态机与客户转交、客户组数据范围解析、跨客户 404。 - 新增迁移与幂等种子(电商/金融两级行业、2 客户、1 客户组、4 条字段)。 - 前端新增行业/客户/客户组/字段审批页面与路由、导航更名(客户管理 / 行业管理 / 客户组),复用现有 token。 - 补充 shared 契约单测、client 集成用例、A3 浏览器 e2e。 影响面:client 模块与 client 表、shared 契约、web 路由/导航、ScopeContext 数据范围、OpenAPI 与种子;A1 客户转交占位由 client 实现替换。 回滚:回滚本提交;数据库保留新增表或按报告执行 down SQL;CUSTOMER_DIRECTORY 可切回空集实现。 验证:format:check / lint / typecheck / test 全绿(shared 131 / api 90 / web 125,共 346);test:int 5 文件 39 条;A3 e2e 1/1;build 8/8;真实浏览器六页验证通过。
94 lines
3.4 KiB
TypeScript
94 lines
3.4 KiB
TypeScript
import {
|
||
type ApplyCustomerFieldInput,
|
||
applyCustomerFieldInputSchema,
|
||
type ApproveCustomerFieldInput,
|
||
approveCustomerFieldInputSchema,
|
||
type CustomerField,
|
||
type CustomerFieldListItem,
|
||
type CustomerFieldListQuery,
|
||
customerFieldListQuerySchema,
|
||
ErrorCode,
|
||
type ListResponse,
|
||
Permission,
|
||
} from '@lingxi/shared';
|
||
import { Body, Controller, Get, HttpCode, Inject, Param, Post, Query } from '@nestjs/common';
|
||
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||
|
||
import { AppException } from '../../common/errors/app-exception';
|
||
import { ZodValidationPipe } from '../../common/pipes/zod-validation.pipe';
|
||
import type { AuthContext } from '../../common/security/auth.types';
|
||
import {
|
||
Audit,
|
||
CurrentScope,
|
||
CurrentUser,
|
||
RequirePermissions,
|
||
} from '../../common/security/decorators';
|
||
import type { ScopeContext } from '../../common/security/scope/scope-context';
|
||
import { CustomerFieldsService } from './customer-fields.service';
|
||
|
||
function requireAuth(auth: AuthContext | null): AuthContext {
|
||
if (auth === null) throw new AppException({ code: ErrorCode.UNAUTHENTICATED });
|
||
return auth;
|
||
}
|
||
|
||
@ApiTags('client-customer-fields')
|
||
@ApiBearerAuth()
|
||
@Controller('customer-fields')
|
||
export class CustomerFieldsController {
|
||
constructor(@Inject(CustomerFieldsService) private readonly fields: CustomerFieldsService) {}
|
||
|
||
@Get()
|
||
@RequirePermissions(Permission.CUSTOMER_FIELD_APPROVE, Permission.CUSTOMER_READ)
|
||
@ApiOperation({ summary: '字段审批队列(待审批 / 已生效 / 已驳回)' })
|
||
list(
|
||
@Query(new ZodValidationPipe(customerFieldListQuerySchema)) query: CustomerFieldListQuery,
|
||
@CurrentScope() scope: ScopeContext | null,
|
||
): Promise<ListResponse<CustomerFieldListItem>> {
|
||
return this.fields.list(query, scope);
|
||
}
|
||
|
||
@Post(':id/approve')
|
||
@HttpCode(200)
|
||
@RequirePermissions(Permission.CUSTOMER_FIELD_APPROVE)
|
||
@Audit('client.customer_field.approve')
|
||
@ApiOperation({ summary: '审批客户级字段(通过 / 驳回;驳回原因必填)' })
|
||
approve(
|
||
@Param('id') id: string,
|
||
@Body(new ZodValidationPipe(approveCustomerFieldInputSchema)) input: ApproveCustomerFieldInput,
|
||
@CurrentUser() auth: AuthContext | null,
|
||
@CurrentScope() scope: ScopeContext | null,
|
||
): Promise<CustomerField> {
|
||
return this.fields.approve(id, input, requireAuth(auth).userId, scope);
|
||
}
|
||
}
|
||
|
||
@ApiTags('client-customer-fields')
|
||
@ApiBearerAuth()
|
||
@Controller('customers')
|
||
export class CustomerFieldsOfCustomerController {
|
||
constructor(@Inject(CustomerFieldsService) private readonly fields: CustomerFieldsService) {}
|
||
|
||
@Get(':id/fields')
|
||
@RequirePermissions(Permission.CUSTOMER_READ)
|
||
@ApiOperation({ summary: '客户级字段列表(基础字段在 customers.custom_fields)' })
|
||
list(
|
||
@Param('id') id: string,
|
||
@CurrentScope() scope: ScopeContext | null,
|
||
): Promise<CustomerField[]> {
|
||
return this.fields.listByCustomer(id, scope);
|
||
}
|
||
|
||
@Post(':id/fields')
|
||
@RequirePermissions(Permission.CUSTOMER_FIELD_APPLY)
|
||
@Audit('client.customer_field.apply')
|
||
@ApiOperation({ summary: '申请客户级字段(进入生成字段需审批;每客户 ≤ 5)' })
|
||
apply(
|
||
@Param('id') id: string,
|
||
@Body(new ZodValidationPipe(applyCustomerFieldInputSchema)) input: ApplyCustomerFieldInput,
|
||
@CurrentUser() auth: AuthContext | null,
|
||
@CurrentScope() scope: ScopeContext | null,
|
||
): Promise<CustomerField> {
|
||
return this.fields.apply(id, input, requireAuth(auth).userId, scope);
|
||
}
|
||
}
|