feat(client): A3 客户与行业目录(两级行业、客户组、字段审批)
背景: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;真实浏览器六页验证通过。
This commit is contained in:
1 parent
8626996bb1
commit
140dc386c1
54 files changed
+9018
-147
No files matched your search
+172
-35
@@ -15,16 +15,16 @@ datasource db {
|
||||
|
||||
/// 事务性发件箱:领域事件先落库,再由 worker 投递(技术架构 §5.3)
|
||||
model OutboxEvent {
|
||||
id String @id @db.Char(26)
|
||||
aggregateType String @map("aggregate_type") @db.VarChar(64)
|
||||
aggregateId String @map("aggregate_id") @db.Char(26)
|
||||
eventType String @map("event_type") @db.VarChar(128)
|
||||
payloadJson Json @map("payload_json") @db.JsonB
|
||||
traceId String? @map("trace_id") @db.VarChar(64)
|
||||
occurredAt DateTime @default(now()) @map("occurred_at") @db.Timestamptz(3)
|
||||
publishedAt DateTime? @map("published_at") @db.Timestamptz(3)
|
||||
attempts Int @default(0)
|
||||
lastError String? @map("last_error") @db.Text
|
||||
id String @id @db.Char(26)
|
||||
aggregateType String @map("aggregate_type") @db.VarChar(64)
|
||||
aggregateId String @map("aggregate_id") @db.Char(26)
|
||||
eventType String @map("event_type") @db.VarChar(128)
|
||||
payloadJson Json @map("payload_json") @db.JsonB
|
||||
traceId String? @map("trace_id") @db.VarChar(64)
|
||||
occurredAt DateTime @default(now()) @map("occurred_at") @db.Timestamptz(3)
|
||||
publishedAt DateTime? @map("published_at") @db.Timestamptz(3)
|
||||
attempts Int @default(0)
|
||||
lastError String? @map("last_error") @db.Text
|
||||
|
||||
@@index([publishedAt, occurredAt])
|
||||
@@index([aggregateType, aggregateId])
|
||||
@@ -33,14 +33,14 @@ model OutboxEvent {
|
||||
|
||||
/// 请求幂等键:任务提交必须带 Idempotency-Key(TTL 24h)
|
||||
model IdempotencyKey {
|
||||
id String @id @db.Char(26)
|
||||
scope String @db.VarChar(64)
|
||||
key String @db.VarChar(128)
|
||||
requestHash String @map("request_hash") @db.VarChar(64)
|
||||
responseStatus Int? @map("response_status")
|
||||
responseBody Json? @map("response_body") @db.JsonB
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
|
||||
expiresAt DateTime @map("expires_at") @db.Timestamptz(3)
|
||||
id String @id @db.Char(26)
|
||||
scope String @db.VarChar(64)
|
||||
key String @db.VarChar(128)
|
||||
requestHash String @map("request_hash") @db.VarChar(64)
|
||||
responseStatus Int? @map("response_status")
|
||||
responseBody Json? @map("response_body") @db.JsonB
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
|
||||
expiresAt DateTime @map("expires_at") @db.Timestamptz(3)
|
||||
|
||||
@@unique([scope, key])
|
||||
@@index([expiresAt])
|
||||
@@ -49,7 +49,7 @@ model IdempotencyKey {
|
||||
|
||||
/// 部门:两级组织(一级部门 → 小组),父子用 parent_id 关联(A1 identity)
|
||||
model Department {
|
||||
id String @id @db.Char(26)
|
||||
id String @id @db.Char(26)
|
||||
code String @unique @db.VarChar(64)
|
||||
name String @db.VarChar(64)
|
||||
parentId String? @map("parent_id") @db.Char(26)
|
||||
@@ -61,8 +61,8 @@ model Department {
|
||||
|
||||
parent Department? @relation("DepartmentTree", fields: [parentId], references: [id])
|
||||
children Department[] @relation("DepartmentTree")
|
||||
leader User? @relation("DepartmentLeader", fields: [leaderUserId], references: [id])
|
||||
users User[] @relation("UserDepartment")
|
||||
leader User? @relation("DepartmentLeader", fields: [leaderUserId], references: [id])
|
||||
users User[] @relation("UserDepartment")
|
||||
|
||||
@@unique([parentId, name])
|
||||
@@index([parentId, sortOrder])
|
||||
@@ -94,10 +94,14 @@ model User {
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
|
||||
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
|
||||
|
||||
department Department @relation("UserDepartment", fields: [departmentId], references: [id])
|
||||
roles UserRole[]
|
||||
refreshTokens RefreshToken[]
|
||||
ledDepartments Department[] @relation("DepartmentLeader")
|
||||
department Department @relation("UserDepartment", fields: [departmentId], references: [id])
|
||||
roles UserRole[]
|
||||
refreshTokens RefreshToken[]
|
||||
ledDepartments Department[] @relation("DepartmentLeader")
|
||||
ownedCustomers Customer[] @relation("CustomerOwner")
|
||||
createdCustomers Customer[] @relation("CustomerCreator")
|
||||
appliedCustomerFields CustomerField[] @relation("CustomerFieldApplicant")
|
||||
approvedCustomerFields CustomerField[] @relation("CustomerFieldApprover")
|
||||
|
||||
@@index([departmentId, status])
|
||||
@@index([status, lastLoginAt])
|
||||
@@ -211,16 +215,149 @@ model RefreshToken {
|
||||
|
||||
/// 系统配置:阈值 / 上限类数字唯一存放处(PRD R-26),带版本与变更审计
|
||||
model SystemConfig {
|
||||
id String @id @db.Char(26)
|
||||
configKey String @unique @map("config_key") @db.VarChar(128)
|
||||
configValue Json @map("config_value") @db.JsonB
|
||||
valueType String @map("value_type") @db.VarChar(16)
|
||||
description String @default("") @db.VarChar(255)
|
||||
version Int @default(1)
|
||||
isEditable Boolean @default(true) @map("is_editable")
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
|
||||
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
|
||||
updatedBy String? @map("updated_by") @db.Char(26)
|
||||
id String @id @db.Char(26)
|
||||
configKey String @unique @map("config_key") @db.VarChar(128)
|
||||
configValue Json @map("config_value") @db.JsonB
|
||||
valueType String @map("value_type") @db.VarChar(16)
|
||||
description String @default("") @db.VarChar(255)
|
||||
version Int @default(1)
|
||||
isEditable Boolean @default(true) @map("is_editable")
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
|
||||
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
|
||||
updatedBy String? @map("updated_by") @db.Char(26)
|
||||
|
||||
@@map("system_configs")
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// A3 客户与行业(client 模块;ADR-002 两级行业)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
enum IndustryStatus {
|
||||
ACTIVE
|
||||
DISABLED
|
||||
}
|
||||
|
||||
/// 行业两级树:一级 parent_id 为空,二级 parent_id 指向一级;最多两级。
|
||||
/// 名称唯一性由迁移中的部分唯一索引保证:一级全局唯一、二级同父唯一。
|
||||
model Industry {
|
||||
id String @id @db.Char(26)
|
||||
parentId String? @map("parent_id") @db.Char(26)
|
||||
level Int @default(1)
|
||||
name String @db.VarChar(64)
|
||||
sortOrder Int @default(10) @map("sort_order")
|
||||
status IndustryStatus @default(ACTIVE)
|
||||
remark String @default("") @db.VarChar(200)
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
|
||||
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
|
||||
|
||||
parent Industry? @relation("IndustryTree", fields: [parentId], references: [id], onDelete: Restrict)
|
||||
children Industry[] @relation("IndustryTree")
|
||||
customers Customer[]
|
||||
|
||||
@@index([parentId, sortOrder])
|
||||
@@index([level, status])
|
||||
@@map("industries")
|
||||
}
|
||||
|
||||
enum CustomerStatus {
|
||||
ACTIVE
|
||||
PAUSED
|
||||
ENDED
|
||||
}
|
||||
|
||||
/// 客户:隔离单元;industry_id 必须指向末级行业;软删除保留历史。
|
||||
model Customer {
|
||||
id String @id @db.Char(26)
|
||||
industryId String @map("industry_id") @db.Char(26)
|
||||
name String @db.VarChar(128)
|
||||
shortName String @map("short_name") @db.VarChar(32)
|
||||
status CustomerStatus @default(ACTIVE)
|
||||
ownerUserId String @map("owner_user_id") @db.Char(26)
|
||||
contactName String? @map("contact_name") @db.VarChar(32)
|
||||
contactPhone String? @map("contact_phone") @db.VarChar(32)
|
||||
preferences Json @default("{}") @db.JsonB
|
||||
customFields Json @default("[]") @map("custom_fields") @db.JsonB
|
||||
remark String? @db.VarChar(500)
|
||||
createdBy String? @map("created_by") @db.Char(26)
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
|
||||
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
|
||||
deletedAt DateTime? @map("deleted_at") @db.Timestamptz(3)
|
||||
|
||||
industry Industry @relation(fields: [industryId], references: [id], onDelete: Restrict)
|
||||
owner User @relation("CustomerOwner", fields: [ownerUserId], references: [id], onDelete: Restrict)
|
||||
creator User? @relation("CustomerCreator", fields: [createdBy], references: [id], onDelete: SetNull)
|
||||
groupMembers CustomerGroupMember[]
|
||||
fields CustomerField[]
|
||||
|
||||
@@unique([industryId, name])
|
||||
@@index([ownerUserId, status])
|
||||
@@index([status, updatedAt])
|
||||
@@index([deletedAt])
|
||||
@@map("customers")
|
||||
}
|
||||
|
||||
/// 客户组:授权与数据范围的最小单位。
|
||||
model CustomerGroup {
|
||||
id String @id @db.Char(26)
|
||||
name String @unique @db.VarChar(64)
|
||||
description String @default("") @db.VarChar(255)
|
||||
createdBy String? @map("created_by") @db.Char(26)
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
|
||||
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
|
||||
|
||||
members CustomerGroupMember[]
|
||||
|
||||
@@map("customer_groups")
|
||||
}
|
||||
|
||||
/// 客户组 ↔ 客户(复合主键)
|
||||
model CustomerGroupMember {
|
||||
groupId String @map("group_id") @db.Char(26)
|
||||
customerId String @map("customer_id") @db.Char(26)
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
|
||||
|
||||
group CustomerGroup @relation(fields: [groupId], references: [id], onDelete: Cascade)
|
||||
customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@id([groupId, customerId])
|
||||
@@index([customerId])
|
||||
@@map("customer_group_members")
|
||||
}
|
||||
|
||||
enum CustomerFieldStatus {
|
||||
PENDING
|
||||
ACTIVE
|
||||
REJECTED
|
||||
DISABLED
|
||||
}
|
||||
|
||||
/// 客户级字段:只有 enters_generation=true 的字段需要审批;
|
||||
/// 基础字段存 customers.custom_fields(JSONB),不进入本表。
|
||||
model CustomerField {
|
||||
id String @id @db.Char(26)
|
||||
customerId String @map("customer_id") @db.Char(26)
|
||||
name String @db.VarChar(32)
|
||||
code String @db.VarChar(32)
|
||||
type String @db.VarChar(16)
|
||||
options Json @default("[]") @db.JsonB
|
||||
purpose String @db.VarChar(200)
|
||||
entersGeneration Boolean @default(false) @map("enters_generation")
|
||||
status CustomerFieldStatus @default(PENDING)
|
||||
applicantUserId String @map("applicant_user_id") @db.Char(26)
|
||||
approverUserId String? @map("approver_user_id") @db.Char(26)
|
||||
appliedAt DateTime @default(now()) @map("applied_at") @db.Timestamptz(3)
|
||||
approvedAt DateTime? @map("approved_at") @db.Timestamptz(3)
|
||||
rejectReason String? @map("reject_reason") @db.VarChar(200)
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
|
||||
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
|
||||
|
||||
customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade)
|
||||
applicant User @relation("CustomerFieldApplicant", fields: [applicantUserId], references: [id], onDelete: Restrict)
|
||||
approver User? @relation("CustomerFieldApprover", fields: [approverUserId], references: [id], onDelete: SetNull)
|
||||
|
||||
@@unique([customerId, code])
|
||||
@@index([status, appliedAt])
|
||||
@@index([customerId, status])
|
||||
@@map("customer_fields")
|
||||
}
|
||||
Reference in new issue
Block a user