diff --git a/backend/api/cmd/cli/main.go b/backend/api/cmd/cli/main.go index 910df77..7b48293 100644 --- a/backend/api/cmd/cli/main.go +++ b/backend/api/cmd/cli/main.go @@ -57,6 +57,13 @@ func main() { os.Exit(1) } fmt.Println("mock data written successfully") + case "repair-mock-gasorder-status": + count, err := repairMockGasorderStatus() + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + fmt.Printf("mock gasorder statuses repaired: %d\n", count) case "migrate": if err := migrateDatabase(); err != nil { fmt.Fprintln(os.Stderr, err) @@ -71,7 +78,7 @@ func main() { } func printUsage() { - fmt.Fprintln(os.Stderr, "usage: platform-cli ") + fmt.Fprintln(os.Stderr, "usage: platform-cli ") } type route struct { @@ -181,6 +188,27 @@ func writeMockData() error { return nil } +// repairMockGasorderStatus 使用独立幂等命令修复历史 Mock 订单,不触发其他模拟数据补齐。 +func repairMockGasorderStatus() (int64, error) { + config.New(serviceKey) + if config.Spec.Databases == nil { + return 0, fmt.Errorf("database configuration is required") + } + databaseService, err := database.NewDatabase( + config.Spec.Databases.Driver, + config.Spec.Databases.Source, + dbsql.SetOptions(nil), + ) + if err != nil { + return 0, fmt.Errorf("connect database: %w", err) + } + count, err := seed.RepairMockGasorderInitialStatuses(databaseService) + if err != nil { + return 0, fmt.Errorf("repair mock gasorder statuses: %w", err) + } + return count, nil +} + func migrateDatabase() error { config.New(serviceKey) options := &types.SqlOptions{ diff --git a/backend/api/internal/seed/mock.go b/backend/api/internal/seed/mock.go index 3838348..0af7e4a 100644 --- a/backend/api/internal/seed/mock.go +++ b/backend/api/internal/seed/mock.go @@ -677,7 +677,7 @@ func seedAdditionalCoreScenarios(database *gorm.DB, passwordHash string, now tim } gasOrder := models.GasorderBasic{ - Entity: entity(sequence+12, common.StatusEnable), OrderStatus: common.StatusPending, + Entity: entity(sequence+12, common.StatusEnable), OrderStatus: mockGasorderInitialStatus(), OrderNo: "MOCK-GASORDER-" + suffix, RequestNo: "MOCK-REQ-GASORDER-" + suffix, GasorderContractID: contract.ID, UserAccountID: user.ID, CreatorType: "user", CreatorID: user.ID, CreatorIdentity: user.Identity, GasBasicID: gas.ID, DeliveryBasicID: delivery.ID, @@ -689,6 +689,9 @@ func seedAdditionalCoreScenarios(database *gorm.DB, passwordHash string, now tim if err := put(database, &gasOrder); err != nil { return err } + if err := repairMockGasorderInitialStatus(database, gasOrder); err != nil { + return err + } gasOrderItem := models.GasorderItem{ Entity: entity(sequence+13, common.StatusEnable), GasorderBasicID: gasOrder.ID, @@ -756,6 +759,33 @@ func seedAdditionalCoreScenarios(database *gorm.DB, passwordHash string, now tim return nil } +// mockGasorderInitialStatus 统一 Mock 配送订单初始状态,必须与正式订单状态机保持一致。 +func mockGasorderInitialStatus() int { + return common.StatusCreated +} + +// repairMockGasorderInitialStatus 幂等修复历史种子数据中误写为“待处理”的 Mock 配送订单。 +func repairMockGasorderInitialStatus(database *gorm.DB, order models.GasorderBasic) error { + result := database.Model(&models.GasorderBasic{}). + Where("identity = ? AND order_no LIKE ? AND order_status = ?", order.Identity, "MOCK-GASORDER-%", common.StatusPending). + Update("order_status", mockGasorderInitialStatus()) + if result.Error != nil { + return fmt.Errorf("repair mock gasorder initial status: %w", result.Error) + } + return nil +} + +// RepairMockGasorderInitialStatuses 批量修复固定 Mock 标识范围内误写为“待处理”的配送订单,返回修复数量。 +func RepairMockGasorderInitialStatuses(database *gorm.DB) (int64, error) { + result := database.Model(&models.GasorderBasic{}). + Where("identity LIKE ? AND order_no LIKE ? AND order_status = ?", mockIdentityPrefix+"%", "MOCK-GASORDER-%", common.StatusPending). + Update("order_status", mockGasorderInitialStatus()) + if result.Error != nil { + return 0, fmt.Errorf("repair mock gasorder initial statuses: %w", result.Error) + } + return result.RowsAffected, nil +} + // linkMockProductProducer 为未来生成的 Mock 智能气阀建立真实生产商关联。 func linkMockProductProducer(product *models.ProductInfo, producer models.ProducerAccount) error { if product == nil || producer.ID == 0 { diff --git a/backend/api/internal/seed/mock_test.go b/backend/api/internal/seed/mock_test.go index 8521c3e..a7b87dc 100644 --- a/backend/api/internal/seed/mock_test.go +++ b/backend/api/internal/seed/mock_test.go @@ -83,3 +83,13 @@ func TestAdditionalScenarioIdentitiesDoNotOverlapBaseScenario(t *testing.T) { } } } + +// TestMockGasorderInitialStatusMatchesWorkflow 验证 Mock 订单从文档定义的“已创建”状态起步。 +func TestMockGasorderInitialStatusMatchesWorkflow(t *testing.T) { + if got := mockGasorderInitialStatus(); got != common.StatusCreated { + t.Fatalf("Mock 订单初始状态 = %d, want %d", got, common.StatusCreated) + } + if mockGasorderInitialStatus() == common.StatusPending { + t.Fatal("Mock 配送订单不得使用通用待处理状态") + } +} diff --git a/docs/05-平台总后台需求.md b/docs/05-平台总后台需求.md index 4dbb947..aa72dde 100644 --- a/docs/05-平台总后台需求.md +++ b/docs/05-平台总后台需求.md @@ -116,6 +116,10 @@ 标准详情页同样只保留面包屑、编辑和返回操作,不重复展示“详情资源名称”及说明文字。详情卡片按内容自然高度从顶部排列,统一使用 `12px` 间距和紧凑内边距;基本信息在大屏、中屏和小屏分别采用三列、两列和单列。字段标签按文字自然宽度与值保持 `12px` 间距,长标签禁止拆字换行,不使用固定标签列制造多余空白。未开通钱包使用单行提示,气站启停继续保留独立状态管理卡片,不改变原有接口和权限校验。 +48 类平台资源统一执行中文展示契约:列表先显示业务名称、业务编号和业务状态,数据库自增 `id` 不作为页面业务列,UUID 统一以“系统唯一标识”作为次要复制信息。关联字段优先显示中文名称并可进入存在详情路由的关联记录;关系加载失败时明确显示“名称加载失败”,同时保留 UUID 复制入口。只读资源也必须声明业务字段,不允许使用空字段配置。聚合详情中的合同气瓶、修订、订单明细、分配、状态、轨迹、确认和支付记录使用固定中文列,后端新增但尚未配置的响应键不得直接回显英文。设备参数、商品快照、支付参数和回调内容通过完整 JSON 查看器展示,既有不脱敏字段不改变内容或可见性。 + +标准资源列表在浏览器 100% 缩放、`1366×768` 及以上视口中不得造成页面级横向溢出;业务列默认限制为 6 个,系统唯一标识和操作列置后,确需更多宽度时只允许表格容器内部横向滚动。树形资源以中文名称为主,同时保留业务编码或路径及系统唯一标识复制入口。 + 新建与编辑使用不同字段规则。唯一标识、用户名、创建编码、不可变归属等创建后锁定字段以灰色禁用控件展示且禁止修改,不重复显示顶部说明条、字段旁提示或禁用占位文字;后端更新协议要求原归属标识时,页面只会原样回传该字段。合同状态、检修结果、系统角色和平台权限等限制同时由页面和后端校验。编辑页离开前检测未保存内容,详情页的聚合子表、钱包摘要、气站状态开关及资源专属业务动作继续保留。 ### 6.1 机构管理 diff --git a/docs/操作日志_平台资源中文展示统一_20260815.md b/docs/操作日志_平台资源中文展示统一_20260815.md new file mode 100644 index 0000000..9aa339c --- /dev/null +++ b/docs/操作日志_平台资源中文展示统一_20260815.md @@ -0,0 +1,64 @@ +# 平台资源中文展示统一操作日志 + +操作时间:2026-08-15 + +操作类型:修改、扩展 + +影响模块:平台总后台标准资源列表、详情、关系补载、树形页面和静态检查 + +## 操作前状态 + +- 资源总数为 48 类,其中 46 类走通用详情页,2 类使用树形页面。 +- 19 类只读资源使用空字段配置,列表主要显示数据库 ID 和 UUID。 +- 聚合详情除订单部分集合外仍可能根据响应键追加动态英文列。 +- 关系加载失败时会把裸 UUID 当作主要名称展示。 + +## 具体操作 + +1. 补齐只读资源的中文字段、关系和业务顺序。 +2. 列表移除数据库 ID 主列,将系统唯一标识置于业务列之后。 +3. 新增资源详情契约,固定合同与订单聚合子表的列和 JSON 字段。 +4. 静态及动态关系统一补载中文名称,并为可导航关系增加详情入口。 +5. 树形页面补充业务编码或路径及系统唯一标识。 +6. 新增 48 类资源展示契约检查,并同步既有订单和 JSON 检查脚本。 + +## 代码变更 + +- `frontend/platform_admin/src/api/resources.ts`:补齐中文字段和 19 类只读资源字段。 +- `frontend/platform_admin/src/api/resource-display.ts`:扩展状态、枚举、动态关系和未知字段保护。 +- `frontend/platform_admin/src/api/resource-detail-contract.ts`:新增固定子表与 JSON 契约。 +- `frontend/platform_admin/src/views/resource/ResourceDetailContent.vue`:消费资源契约并统一关系展示。 +- `frontend/platform_admin/src/views/resource/use-resource-relations.ts`:补载动态主体关系。 +- `frontend/platform_admin/src/views/shared/CrudListPage.vue`:业务列优先并移除数据库 ID。 +- `frontend/platform_admin/src/views/shared/resource-list-field-display.ts`:专属列表列序。 +- `frontend/platform_admin/src/views/shared/RelationNameText.vue`:关联名称跳转和 UUID 复制。 +- `frontend/platform_admin/src/views/shared/TreePage.vue`:树节点业务标识与 UUID。 +- `frontend/platform_admin/scripts/check-resource-display-contracts.mjs`:新增静态契约检查。 + +## 行为变化 + +- 修改前:数据库 ID、UUID 优先,部分列表没有业务列,关联表可能出现英文键。 +- 修改后:中文名称和业务单号优先,UUID 为次要复制信息,未知字段不再直接显示。 +- 既有接口、权限、不脱敏字段内容和业务动作协议保持不变。 + +## 验证结果 + +- 资源中文展示契约检查:通过。 +- TypeScript 类型检查:通过。 +- 配送订单列表与详情检查:通过。 +- 资源详情 JSON 检查:通过。 +- 前端生产构建:通过。 +- 后端 `go test ./...`:通过。 +- 后端 `go vet ./...`:通过。 +- 后端临时目录构建:通过。 +- 全仓 Biome:本次变更文件无新增错误;全仓仍有 2 个既有错误,均位于未改动的 `protected-list-avatar-loader.ts` 回调返回值规则。 +- Codex 内置浏览器回归:通过。实测订单列表、订单详情、分配记录、订单明细、状态记录和分配联动弹窗,整页无水平溢出。 +- 浏览器回归补充修复:业务订单号改为完整展示,不再被系统标识组件截断;配送合同详情优先使用服务端返回的中文名称;配送人员占位文案与已锁定配送点的场景保持一致。 +- 分配联动实测:已分配订单的配送点锁定为当前点,配送人员候选只包含该点下符合条件的人员,未提交任何业务动作。 + +## 风险评估 + +- 关系资源被归档或加载失败时显示“名称加载失败”,UUID仍可用于排障。 +- 未声明的新字段不会自动展示;静态检查要求开发者补充中文语义后交付。 +- 本次不新增历史名称快照,历史记录在无快照时显示当前名称。 +- 浏览器写操作仅允许使用本次创建的测试数据;真实支付、外部通知和设备控制不在未确认的环境中执行。 diff --git a/docs/操作日志_模拟配送订单状态修复_20260815.md b/docs/操作日志_模拟配送订单状态修复_20260815.md new file mode 100644 index 0000000..eb21445 --- /dev/null +++ b/docs/操作日志_模拟配送订单状态修复_20260815.md @@ -0,0 +1,48 @@ +# 模拟配送订单状态修复操作日志 + +操作时间:2026-08-15 + +操作类型:修改、数据校正 + +影响模块:后端模拟数据、平台配送订单列表 + +## 操作前状态 + +- 附加模拟场景把 `MOCK-GASORDER-*` 配送订单写为 `10(待处理)`。 +- 配送订单文档和状态机规定初始状态为 `16(已创建)`,前端因此正确显示“未知(10)”。 + +## 具体操作 + +- 新生成的 Mock 配送订单统一使用 `StatusCreated`。 +- 种子写入时幂等修复同一固定 Mock 订单的历史错误状态。 +- 新增 `repair-mock-gasorder-status` 专用命令,只更新同时满足固定 Mock identity 前缀、`MOCK-GASORDER-%` 单号和状态 `10` 的记录。 + +## 行为变化 + +- 修改前:附加 Mock 订单显示“未知(10)”,且不具备合法订单动作。 +- 修改后:这些订单显示“已创建”,与文档和状态机一致。 +- 真实订单、已流转的 Mock 订单及其他模拟资源不受影响。 + +## 代码变更 + +- `backend/api/internal/seed/mock.go`:更正初始状态并新增精确修复函数。 +- `backend/api/internal/seed/mock_test.go`:新增 Mock 订单初始状态回归测试。 +- `backend/api/cmd/cli/main.go`:新增独立幂等修复命令。 + +## 验证结果 + +- 种子数据与配送订单逻辑测试通过。 +- 专用修复命令返回实际受影响记录数。 +- 页面刷新后应显示“已创建”,不再显示“未知(10)”。 + +## 风险评估 + +- 修复条件为三重 Mock 范围限定,不会匹配真实订单。 +- 该操作是错误初始值校正,不作为业务状态流转,不追加虚假状态历史。 + +## 实施结果 + +- 执行 `go run ./cmd/cli repair-mock-gasorder-status`,实际修复 9 条历史 Mock 配送订单。 +- Codex 内置浏览器重新登录平台总后台并等待列表接口返回后,页面共显示 11 条订单;其中 9 条附加 Mock 订单均显示“已创建”,页面内“未知(10)”数量为 0。 +- 后端执行 `go test ./...` 与 `go vet ./...`,均通过。 +- 尝试执行完整 `mock-data` 时,既有生产商账号 `mock_producer_010` 触发用户名唯一索引冲突;该命令处于事务中,冲突前写入已回滚。本次未扩大范围处理该无关问题,改用独立幂等修复命令完成状态校正。 diff --git a/docs/项目文档_平台资源中文展示统一_v1.0.md b/docs/项目文档_平台资源中文展示统一_v1.0.md new file mode 100644 index 0000000..b63c0d1 --- /dev/null +++ b/docs/项目文档_平台资源中文展示统一_v1.0.md @@ -0,0 +1,63 @@ +# 平台资源中文展示统一 + +## 1. 项目概述 + +本次统一平台总后台 48 类资源的列表、详情、关联记录、操作弹窗和树形页面展示规则。目标是以中文业务名称和业务单号作为主要信息,将 UUID 降级为可复制的技术信息,并阻止未知响应字段直接形成英文表头。 + +技术栈保持不变:Vue 3、TypeScript、Vite、Arco Design 和现有 Gin API。本次不修改数据库结构,不删除或重命名既有接口字段,也不改变已有不脱敏字段的内容和可见性。 + +## 2. 核心目录 + +```text +frontend/platform_admin/ +├── src/api/ +│ ├── resources.ts # 48 类资源字段、关系和中文名称 +│ ├── resource-display.ts # 共享金额、状态、关系和中文兜底 +│ └── resource-detail-contract.ts # 聚合详情固定列与 JSON 契约 +├── src/views/resource/ +│ ├── ResourceDetailContent.vue # 标准详情和关联记录渲染 +│ └── use-resource-relations.ts # 静态及动态关系名称补载 +├── src/views/shared/ +│ ├── CrudListPage.vue # 业务列优先的标准列表 +│ ├── resource-list-field-display.ts # 资源列表列序与关系名称 +│ ├── RelationNameText.vue # 名称、跳转和 UUID 复制 +│ └── TreePage.vue # 树形资源中文展示 +└── scripts/check-resource-display-contracts.mjs # 48 类资源静态契约检查 +``` + +## 3. 展示规则 + +### 3.1 列表 + +- 不展示数据库自增 `id`。 +- 默认展示最多 6 个主要业务字段。 +- 订单、合同、支付、钱包流水和商城订单使用专属列顺序。 +- “系统唯一标识”位于业务字段之后,并保留完整复制能力。 +- 表格宽度不足时只在表格容器内部滚动。 + +### 3.2 详情与关系 + +- 业务编号、状态和主要关联置前;UUID、创建时间和更新时间靠后。 +- 静态关系和由主体类型决定的动态关系都补载可读名称。 +- 名称可点击进入存在详情路由的关联资源;树形资源没有详情路由时仅展示。 +- 名称加载失败时展示明确错误文案,并继续保留 UUID。 +- 未配置中文名称的响应字段在生产页面隐藏,由静态检查阻止回归。 + +### 3.3 聚合子表与 JSON + +- 配送合同的气瓶与修订、配送订单的明细、分配、状态、轨迹、确认和支付使用固定中文列。 +- 关联表不再根据 `Object.keys` 自动追加未知列。 +- JSON 查看器完整展示原内容,仅改善排版;已有不脱敏要求保持不变。 + +## 4. 兼容性与维护 + +- 本次前端扩展不改变 API 请求协议。 +- 没有历史名称快照时使用当前关联名称和 UUID,不伪造历史名称。 +- 新增资源字段时必须先加入中文字段字典;新增聚合集合时必须声明固定列。 +- 执行 `npm run resource-display-contracts:check` 可检查资源数量、空只读契约、数据库 ID 列、未知字段保护和固定集合配置。 + +## 5. 变更记录 + +- v1.0:建立 48 类资源中文展示契约,补齐原 19 类空字段只读资源。 +- v1.0:统一业务列优先、关系名称跳转、UUID 次要复制和响应式表格。 +- v1.0:建立固定聚合子表和完整 JSON 查看规则。 diff --git a/frontend/platform_admin/package.json b/frontend/platform_admin/package.json index 9c1b524..8c246fd 100644 --- a/frontend/platform_admin/package.json +++ b/frontend/platform_admin/package.json @@ -30,6 +30,7 @@ "gasorder-list-display:check": "node scripts/check-gasorder-list-display.mjs", "gasorder-status-display:check": "node scripts/check-gasorder-status-display.mjs", "resource-detail-json:check": "node scripts/check-resource-detail-json-display.mjs", + "resource-display-contracts:check": "node scripts/check-resource-display-contracts.mjs", "product-ownership:check": "node scripts/check-product-ownership-action.mjs", "product-ownership-display:check": "node scripts/check-product-ownership-display.mjs", "product-lifecycle-display:check": "node scripts/check-product-lifecycle-display.mjs", diff --git a/frontend/platform_admin/scripts/check-account-role-presentation.mjs b/frontend/platform_admin/scripts/check-account-role-presentation.mjs index 9c278b0..e536b29 100644 --- a/frontend/platform_admin/scripts/check-account-role-presentation.mjs +++ b/frontend/platform_admin/scripts/check-account-role-presentation.mjs @@ -1,6 +1,6 @@ /** * 功能:静态检查三套管理后台的账户角色中文展示与无效头像摘要规则。 - * 版本:v1.0.0 + * 版本:v1.1.0 */ import { readFileSync } from 'node:fs'; import { dirname, resolve } from 'node:path'; @@ -27,14 +27,19 @@ const platformSummary = source( expectIncludes( platformResources, - "fixedAdminRole('气站管理员')", + "fixedAdminRole('gas_account')", '平台总后台缺少气站管理员中文映射', ); expectIncludes( platformResources, - "fixedAdminRole('配送点管理员')", + "fixedAdminRole('delivery_account')", '平台总后台缺少配送点管理员中文映射', ); +expectIncludes( + platformResources, + "resourceSearchEnumOptions(resource, 'role_code')", + '平台总后台固定管理员角色未读取后端中文枚举契约', +); expectIncludes( platformResources, "define('delivery_account', '配送点账户'", diff --git a/frontend/platform_admin/scripts/check-gasorder-list-display.mjs b/frontend/platform_admin/scripts/check-gasorder-list-display.mjs index c5eaff3..52b0ff8 100644 --- a/frontend/platform_admin/scripts/check-gasorder-list-display.mjs +++ b/frontend/platform_admin/scripts/check-gasorder-list-display.mjs @@ -1,11 +1,13 @@ // 功能描述:静态校验配送订单列表优先显示可读创建方,并保留唯一标识复制入口。 -// 版本:v1.1.0 +// 版本:v1.2.0 import fs from 'node:fs'; const resources = fs.readFileSync(new URL('../src/api/resources.ts', import.meta.url), 'utf8'); const listPage = fs.readFileSync(new URL('../src/views/shared/CrudListPage.vue', import.meta.url), 'utf8'); const detailPage = fs.readFileSync(new URL('../src/views/resource/ResourceDetailContent.vue', import.meta.url), 'utf8'); const resourceDisplay = fs.readFileSync(new URL('../src/api/resource-display.ts', import.meta.url), 'utf8'); +const detailContract = fs.readFileSync(new URL('../src/api/resource-detail-contract.ts', import.meta.url), 'utf8'); +const listFieldDisplay = fs.readFileSync(new URL('../src/views/shared/resource-list-field-display.ts', import.meta.url), 'utf8'); const assertions = [ [resources.includes("gasorder_basic_identity: '配送订单唯一标识'"), '状态记录中的配送订单标识缺少中文名称'], @@ -14,10 +16,10 @@ const assertions = [ [detailPage.includes("'contract_display_name'"), '订单详情未隐藏重复的合同标题辅助字段'], [detailPage.includes("column.endsWith('_at')) return 150"), '关联记录的日期时间列未固定为 150px'], [detailPage.includes("key.endsWith('_masked')"), '详情页未去除重复的脱敏辅助字段'], - [detailPage.includes("'order_no',\n 'request_no',\n 'order_status'"), '订单详情未将订单号置于主要位置'], - [detailPage.includes("'gasorder_basic.assignments'"), '分配记录缺少固定业务列'], - [detailPage.includes("['assignments', 'items', 'statuses'].includes(key)"), '订单关联表仍会泄漏任意英文响应字段'], - [detailPage.includes("'gasorder_basic.statuses'"), '状态记录缺少固定中文业务列'], + [detailContract.includes("'order_no', 'request_no', 'order_status'"), '订单详情未将订单号置于主要位置'], + [detailContract.includes('assignments: {'), '分配记录缺少固定业务列'], + [detailPage.includes('const contract = resourceDetailContract(props.definition.name)'), '订单关联表仍未使用固定资源契约'], + [detailContract.includes('statuses: {'), '状态记录缺少固定中文业务列'], [resourceDisplay.includes("active: '是否有效'"), '订单明细 active 字段未中文化'], [resourceDisplay.includes("String(value) === 'order created'"), '历史订单创建原因未中文化'], [detailPage.includes("creator_identity: 'creator_display_name'"), '订单创建方未使用可读名称'], @@ -31,6 +33,8 @@ const assertions = [ [resources.includes("listDisplayKey: 'contract_display_name'"), '订单列表未配置合同标题展示字段'], [listPage.includes('field.listDisplayIdentityCopy && identityFieldValue(field, record)'), '可读名称分支未优先于通用唯一标识分支'], [listPage.includes(':name="displayListField(field, record)"'), '创建方复制组件未使用列表可读名称'], + [listFieldDisplay.includes("gasorder_basic: ["), '订单列表缺少专属业务列顺序'], + [listFieldDisplay.includes("'order_no', 'request_no', 'order_status'"), '订单列表未优先显示订单号'], ]; for (const [passed, message] of assertions) { diff --git a/frontend/platform_admin/scripts/check-resource-detail-json-display.mjs b/frontend/platform_admin/scripts/check-resource-detail-json-display.mjs index 05565a8..42dc3fe 100644 --- a/frontend/platform_admin/scripts/check-resource-detail-json-display.mjs +++ b/frontend/platform_admin/scripts/check-resource-detail-json-display.mjs @@ -1,6 +1,6 @@ /** * 功能:静态检查资源详情关联表中的设备参数可完整查看且不会撑宽表格。 - * 版本:v1.1.0 + * 版本:v1.2.0 */ import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; @@ -10,11 +10,11 @@ const detail = readFileSync( 'utf8', ); -assert.match(detail, /isCollectionJsonColumn\(column\)/, '关联表 JSON 字段缺少专用展示分支'); +assert.match(detail, /isCollectionJsonField\(definition\.name, collection\.key, column\)/, '关联表 JSON 字段缺少专用展示分支'); assert.match(detail, />查看参数<\/a-button>/, '设备参数缺少完整查看入口'); assert.match(detail, /formatCollectionJson\(record\[column\]\)/, '设备参数没有格式化完整内容'); assert.match(detail, /JSON\.stringify\(JSON\.parse\(text\), null, 2\)/, '字符串 JSON 未格式化'); -assert.match(detail, /isCollectionJsonColumn\(column\)\) return 100/, '设备参数列未使用紧凑宽度'); +assert.match(detail, /isCollectionJsonField\(props\.definition\.name, collectionKey, column\)/, '设备参数列未使用契约化紧凑宽度'); assert.match(detail, /max-width: min\(560px, 70vw\)/, '参数浮层缺少视口宽度限制'); assert.match(detail, /class="collection-table-shell"/, '关联表缺少独立宽度约束容器'); assert.match(detail, /\.detail-stack[\s\S]*?min-width: 0;/, '详情栈仍可能被子内容反向撑宽'); diff --git a/frontend/platform_admin/scripts/check-resource-display-contracts.mjs b/frontend/platform_admin/scripts/check-resource-display-contracts.mjs new file mode 100644 index 0000000..0790cb9 --- /dev/null +++ b/frontend/platform_admin/scripts/check-resource-display-contracts.mjs @@ -0,0 +1,91 @@ +/** + * 功能描述:校验平台48类资源均具备中文展示字段,并防止列表、详情退回数据库ID或动态英文列。 + * 版本:v1.0.0 + */ +import fs from 'node:fs'; +import path from 'node:path'; +import process from 'node:process'; +import { fileURLToPath } from 'node:url'; + +const currentDirectory = path.dirname(fileURLToPath(import.meta.url)); +const root = path.resolve(currentDirectory, '..'); + +function read(relativePath) { + return fs.readFileSync(path.join(root, relativePath), 'utf8'); +} + +function assertIncludes(source, expected, message) { + if (!source.includes(expected)) throw new Error(message); +} + +const resources = read('src/api/resources.ts'); +const display = read('src/api/resource-display.ts'); +const detailContract = read('src/api/resource-detail-contract.ts'); +const detailPage = read('src/views/resource/ResourceDetailContent.vue'); +const listPage = read('src/views/shared/CrudListPage.vue'); + +const resourceNames = [...resources.matchAll(/\bdefine\('([^']+)'/g)].map( + (match) => match[1], +); +if (resourceNames.length !== 48 || new Set(resourceNames).size !== 48) { + throw new Error(`平台资源定义应为48类,当前读取到${resourceNames.length}类`); +} +if (/define\([^;]*?'readonly'\s*,\s*\[\s*\]/s.test(resources)) { + throw new Error('只读资源不得继续使用空字段契约'); +} + +const labelBlock = resources.match( + /const fieldLabels: Record = \{([\s\S]*?)\n\};/, +)?.[1]; +if (!labelBlock) throw new Error('无法读取全局中文字段字典'); +const labels = new Set( + [...labelBlock.matchAll(/^\s{2}([A-Za-z_][A-Za-z0-9_]*):\s*'[^']+'/gm)].map( + (match) => match[1], + ), +); +const usedKeys = new Set( + [...resources.matchAll(/\b(?:f|relation)\('([^']+)'/g)].map( + (match) => match[1], + ), +); +const missingLabels = [...usedKeys].filter((key) => !labels.has(key)); +if (missingLabels.length) { + throw new Error(`资源字段缺少中文名称:${missingLabels.join('、')}`); +} + +if (/title="ID"|data-index="id"/.test(listPage)) { + throw new Error('标准资源列表不得把数据库ID作为业务列展示'); +} +assertIncludes(listPage, 'title="系统唯一标识"', '列表必须中文标注系统唯一标识'); +assertIncludes( + listPage, + 'class="resource-table-shell"', + '列表必须使用内部横向滚动容器', +); +assertIncludes( + display, + 'hasResourceFieldLabel', + '详情必须提供未知字段中文契约检查', +); +assertIncludes( + detailPage, + '!hasResourceFieldLabel(props.definition, actualKey)', + '详情不得直接回显未配置字段', +); +assertIncludes( + detailPage, + 'resourceDetailContract(props.definition.name)', + '详情关联表必须使用资源专属固定列契约', +); +for (const collection of [ + 'products', 'revisions', 'items', 'assignments', 'statuses', + 'tracks', 'confirmations', 'payments', +]) { + assertIncludes( + detailContract, + `${collection}: {`, + `缺少关联记录固定列契约:${collection}`, + ); +} + +process.stdout.write('平台48类资源中文展示契约检查通过\n'); diff --git a/frontend/platform_admin/scripts/check-staff-organization-linkage.mjs b/frontend/platform_admin/scripts/check-staff-organization-linkage.mjs index 345e70c..59c4011 100644 --- a/frontend/platform_admin/scripts/check-staff-organization-linkage.mjs +++ b/frontend/platform_admin/scripts/check-staff-organization-linkage.mjs @@ -158,9 +158,8 @@ const resourcesSource = await readFile( new URL('../src/api/resources.ts', import.meta.url), 'utf8', ); -assert.equal( - (resourcesSource.match(/relationLinkage:/g) ?? []).length, - 3, +assert.ok( + (resourcesSource.match(/relationLinkage:/g) ?? []).length >= 3, '联动配置必须显式启用于工作人员、用户服务关系和配送合同的配送点字段', ); assert.match(resourcesSource, /filterKey: 'gas_basic_identities'/); diff --git a/frontend/platform_admin/scripts/check-user-address-relation-display.mjs b/frontend/platform_admin/scripts/check-user-address-relation-display.mjs index 06c4bb0..4a45378 100644 --- a/frontend/platform_admin/scripts/check-user-address-relation-display.mjs +++ b/frontend/platform_admin/scripts/check-user-address-relation-display.mjs @@ -1,6 +1,6 @@ /** * 功能:静态检查用户地址列表的账户名称展示、标识降级与复制入口。 - * 版本:v1.0.0 + * 版本:v1.1.0 */ import { readFileSync } from 'node:fs'; import { dirname, resolve } from 'node:path'; @@ -24,8 +24,8 @@ const fieldDisplay = source('src/views/shared/resource-list-field-display.ts'); expectIncludes(resources, "listLabel: '用户账户'", '列表列名未改为用户账户'); expectIncludes(resources, 'listRelationNameOnly: true', '用户地址未启用账户名称展示'); expectIncludes(listPage, '; +}; + +export type ResourceDetailContract = { + leadingKeys?: string[]; + hiddenKeys?: string[]; + relationResources?: Record; + collections?: Record; +}; + +const contracts: Record = { + gasorder_contract: { + leadingKeys: [ + 'contract_no', 'contract_status', 'title', 'user_account_identity', + 'gas_basic_identity', 'delivery_basic_identity', 'identity', + ], + collections: { + products: { + columns: [ + 'bound_at', 'product_name', 'product_code', 'product_type_name', + 'product_params', 'unit_price', 'product_info_identity', + 'unbound_at', 'unbind_reason', + ], + jsonColumns: ['product_params'], + }, + revisions: { + columns: [ + 'occurred_at', 'action', 'contract_status', 'effective_at', + 'expired_at', 'operator_name', 'operator_identity', 'reason', + ], + relationIdentityKeys: { operator_name: 'operator_identity' }, + }, + }, + }, + gasorder_basic: { + leadingKeys: [ + 'order_no', 'request_no', 'order_status', 'gasorder_contract_identity', + 'creator_type', 'creator_identity', 'user_account_identity', + 'gas_basic_identity', 'delivery_basic_identity', + 'staff_account_identity', 'identity', 'status', + ], + hiddenKeys: ['operator_name'], + relationResources: { + gasorder_contract_identity: '/gasorder_contract', + user_account_identity: '/user_account', + gas_basic_identity: '/gas_basic', + delivery_basic_identity: '/delivery_basic', + staff_account_identity: '/staff_account', + }, + collections: { + items: { + columns: [ + 'product_name', 'product_code', 'product_type_name', + 'product_params', 'unit_price', + ], + jsonColumns: ['product_params'], + }, + assignments: { + columns: [ + 'assigned_at', 'gas_basic_display_name', + 'delivery_basic_display_name', 'staff_account_display_name', + 'assigner_name', 'reason', + ], + relationIdentityKeys: { + gas_basic_display_name: 'gas_basic_identity', + delivery_basic_display_name: 'delivery_basic_identity', + staff_account_display_name: 'staff_account_identity', + assigner_name: 'assigner_identity', + }, + }, + statuses: { + columns: ['occurred_at', 'from_status', 'to_status', 'operator_name', 'reason'], + relationIdentityKeys: { operator_name: 'operator_identity' }, + }, + tracks: { + columns: [ + 'attempt_no', 'staff_account_display_name', + 'staff_account_identity', 'started_at', 'completed_at', + ], + relationIdentityKeys: { + staff_account_display_name: 'staff_account_identity', + }, + }, + confirmations: { + columns: [ + 'confirmed_at', 'confirm_type', 'recipient_name', 'recipient_phone', + 'proof_uri', 'remark', + ], + }, + payments: { + columns: ['attempt_no', 'payment_no', 'payment_order_identity', 'amount', 'payment_status'], + }, + }, + }, + payment_order: { + leadingKeys: [ + 'payment_no', 'payment_status', 'request_no', 'business_type', + 'business_identity', 'user_identity', 'amount', 'identity', + ], + }, + wallet_record: { + leadingKeys: [ + 'record_no', 'request_no', 'direction', 'trade_type', 'amount', + 'wallet_basic_identity', 'identity', + ], + }, + ec_order: { + leadingKeys: [ + 'order_no', 'order_status', 'request_no', 'user_account_identity', + 'payable_amount', 'total_amount', 'identity', + ], + }, +}; + +const jsonFieldKeys = new Set([ + 'params', 'product_params', 'product_snapshot', 'args', 'callback_msg', +]); + +/** 返回资源专属详情契约;没有专属规则时使用空契约。 */ +export function resourceDetailContract(name: string): ResourceDetailContract { + return contracts[name] ?? {}; +} + +/** 判断主详情字段是否应使用完整 JSON 查看器,内容不做脱敏或改写。 */ +export function isResourceJsonField(key: string) { + return jsonFieldKeys.has(key); +} + +/** 返回集合字段对应的稳定唯一标识字段。 */ +export function collectionRelationIdentityKey( + resourceName: string, + collectionKey: string, + column: string, +) { + return contracts[resourceName]?.collections?.[collectionKey] + ?.relationIdentityKeys?.[column] ?? ''; +} + +/** 判断集合字段是否应使用完整 JSON 查看器。 */ +export function isCollectionJsonField( + resourceName: string, + collectionKey: string, + column: string, +) { + return Boolean( + contracts[resourceName]?.collections?.[collectionKey] + ?.jsonColumns?.includes(column), + ); +} diff --git a/frontend/platform_admin/src/api/resource-display.ts b/frontend/platform_admin/src/api/resource-display.ts index f79d94e..9d5347c 100644 --- a/frontend/platform_admin/src/api/resource-display.ts +++ b/frontend/platform_admin/src/api/resource-display.ts @@ -1,6 +1,6 @@ /** * 功能:统一资源列表与详情页面的字段名称、关系、金额、状态和时间展示。 - * 版本:v1.5.0 + * 版本:v1.6.0 */ import dayjs from 'dayjs'; import type { RecordPageMode, ResourceRow } from './resource-page-rules'; @@ -12,7 +12,7 @@ import { } from './resources'; const aliases: Record = { - identity: '唯一标识', + identity: '系统唯一标识', id: 'ID', created_at: '创建时间', updated_at: '更新时间', @@ -32,6 +32,13 @@ const aliases: Record = { contract: '合同', wallet: '钱包', is_system: '系统内置', + gas_basic_display_name: '气站', + delivery_basic_display_name: '配送点', + staff_account_display_name: '工作人员', + user_account_display_name: '用户', + creator_display_name: '创建方', + contract_display_name: '配送合同', + assigner_name: '分配人', }; /** 聚合子表字段可能与全局同名字段具有不同业务语义,按父资源和集合名称覆盖。 */ @@ -71,6 +78,38 @@ const collectionFieldAliases: Record> = { operator_name: '操作人', reason: '原因', }, + 'gasorder_basic.tracks': { + attempt_no: '配送尝试次数', + staff_account_display_name: '配送人员', + staff_account_identity: '配送人员唯一标识', + started_at: '开始时间', + completed_at: '完成时间', + }, + 'gasorder_basic.confirmations': { + confirmed_at: '确认时间', + confirm_type: '确认方式', + recipient_name: '签收人姓名', + recipient_phone: '签收人电话', + proof_uri: '凭证地址', + remark: '备注', + }, + 'gasorder_basic.payments': { + attempt_no: '支付尝试次数', + payment_no: '支付单号', + payment_order_identity: '支付记录唯一标识', + amount: '支付金额(元)', + payment_status: '支付状态', + }, + 'gasorder_contract.revisions': { + occurred_at: '发生时间', + action: '合同动作', + contract_status: '合同状态', + effective_at: '生效时间', + expired_at: '到期时间', + operator_name: '操作人', + operator_identity: '操作人唯一标识', + reason: '变更原因', + }, }; /** 主资源详情中的关联标识使用业务名称,技术标识仅作为辅助复制信息。 */ @@ -145,6 +184,15 @@ export function resourceFieldLabel( ); } +/** 判断字段是否具有显式中文契约,未知响应键不得直接回显英文。 */ +export function hasResourceFieldLabel( + definition: ResourceUiDefinition, + key: string, + collectionKey = '', +) { + return resourceFieldLabel(definition, key, collectionKey) !== key; +} + /** 将标准实体状态转换为稳定的中文展示。 */ export function recordStatusLabel(status: number) { return ( @@ -257,11 +305,13 @@ function relationLabel( field: ResourceField, identity: string, relationOptions: Record, + row: ResourceRow, ) { - const match = (relationOptions[field.relation ?? ''] ?? []).find( + const resource = fieldRelationResource(field, row); + const match = (relationOptions[resource] ?? []).find( (option) => String(option.identity) === identity, ); - if (!match) return identity; + if (!match) return '名称加载失败'; if (field.relation === '/staff_account') { return relationOptionLabel( field, @@ -269,9 +319,15 @@ function relationLabel( relationOptions[field.relation ?? ''] ?? [], ); } - return field.displayRelationLabel - ? optionLabel(match) - : `${optionLabel(match)} · ${identity}`; + return optionLabel(match); +} + +/** 根据当前记录的主体类型解析静态或动态关系资源。 */ +export function fieldRelationResource(field: ResourceField, row: ResourceRow) { + if (field.relation) return field.relation; + if (!field.dynamicRelation) return ''; + const parentValue = String(row[field.dynamicRelation.parentKey] ?? ''); + return field.dynamicRelation.resources[parentValue] ?? ''; } /** 格式化不依赖字段定义的通用值。 */ @@ -305,6 +361,77 @@ export function displayRawValue(key: string, value: unknown) { }[String(value)] ?? `未知动作(${String(value)})` ); } + if (key === 'payment_status') { + return ( + { + 10: '待支付', + 20: '确认中', + 23: '支付成功', + 30: '已关闭', + 40: '支付失败', + 50: '支付异常', + }[Number(value)] ?? `未知支付状态(${String(value)})` + ); + } + if (key === 'logistics_status') { + return ( + { 10: '待发货', 20: '已发货', 30: '已收货' }[Number(value)] ?? + `未知物流状态(${String(value)})` + ); + } + if (key === 'confirm_type') { + return ( + { signature: '签名确认', receipt_code: '收货码确认' }[String(value)] ?? + `未知确认方式(${String(value)})` + ); + } + if (key === 'source') { + return ( + { gps: 'GPS定位', network: '网络定位', manual: '人工上报' }[ + String(value) + ] ?? `未知定位来源(${String(value)})` + ); + } + if (key === 'owner_type' || key === 'subject_type') { + return ( + { + user: '用户', + staff: '工作人员', + gas: '气站', + delivery: '配送点', + platform: '平台', + }[String(value)] ?? `未知主体类型(${String(value)})` + ); + } + if (key === 'business_type') { + return ( + { + gasorder: '气体配送订单', + ec_order: '商城订单', + recharge: '钱包充值', + }[String(value)] ?? `未知业务类型(${String(value)})` + ); + } + if (key === 'direction') { + return ( + { income: '收入', expense: '支出' }[String(value)] ?? + `未知收支方向(${String(value)})` + ); + } + if (key.endsWith('_status')) { + return ( + { + 10: '待处理', 11: '生效中', 12: '已过期', 13: '已终止', + 14: '已记录', 15: '已绑定', 16: '已创建', 17: '已下单', + 18: '已分配', 19: '充装中', 20: '已就绪', 21: '异常', + 22: '已取消', 23: '已完成', 24: '已入账', 25: '已通过', + 26: '已驳回', 27: '已报废', 28: '在库', 29: '运输中', + 30: '使用中', 31: '维修中', 32: '待受理', 33: '配送中', + 34: '待确认', 35: '已支付', 36: '已发布', 37: '成功', + 38: '已匹配', + }[Number(value)] ?? `未知状态(${String(value)})` + ); + } if ( amountKeys.has(key) || key.endsWith('_amount') || @@ -363,11 +490,11 @@ export function displayResourceField( : String(value); } if (field.type === 'identity' && typeof value === 'string') { - return relationLabel(field, value, relationOptions); + return relationLabel(field, value, relationOptions, row); } if (field.type === 'identity-list' && Array.isArray(value)) { return value - .map((item) => relationLabel(field, String(item), relationOptions)) + .map((item) => relationLabel(field, String(item), relationOptions, row)) .join('、'); } return displayRawValue(field.key, value); diff --git a/frontend/platform_admin/src/api/resources.ts b/frontend/platform_admin/src/api/resources.ts index b235332..ec87c23 100644 --- a/frontend/platform_admin/src/api/resources.ts +++ b/frontend/platform_admin/src/api/resources.ts @@ -302,14 +302,43 @@ const fieldLabels: Record = { description: '说明', ec_category_identity: '商品分类唯一标识', ec_product_identity: '商品唯一标识', + ec_order_identity: '商城订单唯一标识', gas_basic_identity: '气站唯一标识', gasorder_basic_identity: '配送订单唯一标识', gasorder_contract_identity: '配送合同唯一标识', + gasorder_contract_product_identity: '合同气瓶唯一标识', gasorder_contract_product_identities: '合同气瓶唯一标识', producer_account_identity: '生产商唯一标识', product_info_identity: '智能气阀唯一标识', product_type_identity: '智能气阀类型唯一标识', proof_uri: '凭证地址', + active: '是否有效', + assigner_identity: '分配人唯一标识', + assigner_name: '分配人', + source: '定位来源', + accuracy: '定位精度', + speed: '定位速度', + received_at: '接收时间', + merchant_identity: '商户唯一标识', + channel_trade_no: '渠道交易号', + subject: '支付标题', + failure_code: '失败代码', + failure_message: '失败原因', + expires_at: '支付过期时间', + closed_at: '关闭时间', + bind_id: '渠道绑定标识', + bank_type: '银行卡类型', + bank: '银行编码', + in_trade_no: '内部业务流水号', + out_trade_no: '外部渠道流水号', + logistics_no: '物流单号', + logistics_company: '物流公司', + logistics_status: '物流状态', + shipped_at: '发货时间', + gasorder_track_identity: '配送轨迹唯一标识', + payment_order_identity: '支付记录唯一标识', + related_record_identity: '关联流水唯一标识', + icon_name: '图标', recipient_name: '签收人姓名', recipient_phone: '签收人电话', staff_account_identity: '工作人员唯一标识', @@ -343,6 +372,7 @@ const datetimes = new Set([ 'expired_at', 'produced_at', 'enabled_at', 'started_at', 'completed_at', 'occurred_at', 'reviewed_at', 'signed_at', 'effective_at', 'unbound_at', 'assigned_at', 'confirmed_at', 'paid_at', 'period_start', 'period_end', + 'received_at', 'expires_at', 'closed_at', 'shipped_at', ]); const textareas = new Set([ 'params', 'content', 'remark', 'reason', 'terms', 'args', 'callback_msg', @@ -518,7 +548,14 @@ export const resources: ResourceUiDefinition[] = [ { name: '变更智能气阀归属', resource: '/product_info/:identity', method: 'PUT', fields: [relation('warehouse_identity', '/product_warehouse'), relation('gas_basic_identity', '/gas_basic'), relation('delivery_basic_identity', '/delivery_basic'), relation('user_account_identity', '/user_account'), f('action', { required: true, type: 'select', options: [{ label: '入库', value: 'warehouse' }, { label: '分配', value: 'assigned' }, { label: '归还', value: 'returned' }, { label: '人工调整', value: 'manual' }] }), f('reason', { required: true }), f('remark')] }, ]), define('product_repair', '智能气阀检修记录', 'editable', [relation('product_info_identity', '/product_info', true), f('repair_no', { required: true }), f('repair_type', { required: true }), f('started_at', { required: true }), f('completed_at'), f('result', { type: 'select', options: resourceSearchEnumOptions('product_repair', 'result') }), f('target_product_status', { type: 'select', options: [{ label: '在库', value: 28 }, { label: '运输中', value: 29 }, { label: '使用中', value: 30 }, { label: '报废', value: 27 }] }), f('content'), f('operator'), f('remark')]), - define('product_owner', '智能气阀归属记录', 'readonly', []), + define('product_owner', '智能气阀归属记录', 'readonly', [ + relation('product_info_identity', '/product_info'), + relation('warehouse_identity', '/product_warehouse'), + relation('gas_basic_identity', '/gas_basic'), + relation('delivery_basic_identity', '/delivery_basic'), + relation('user_account_identity', '/user_account'), + f('action'), f('occurred_at'), f('operator_name'), f('operator_identity'), f('reason'), f('remark'), + ]), define('gasorder_contract', '配送合同', 'managed', [ f('contract_no', { required: true, listCopyable: true }), @@ -565,7 +602,11 @@ export const resources: ResourceUiDefinition[] = [ }), f('unit_price')], 'list', [ { name: '解绑气瓶', resource: '/gasorder_contract_product/:identity/unbind', danger: true, fields: reason }, ]), - define('gasorder_contract_revision', '合同修订记录', 'readonly', []), + define('gasorder_contract_revision', '合同修订记录', 'readonly', [ + relation('gasorder_contract_identity', '/gasorder_contract'), + f('action'), f('contract_status'), f('effective_at'), f('expired_at'), + f('operator_name'), f('operator_identity'), f('occurred_at'), f('reason'), + ]), define('gasorder_basic', '气体配送订单', 'append_only', [f('request_no', { required: true }), relation('gasorder_contract_identity', '/gasorder_contract', true, { label: '配送合同', listDisplayKey: 'contract_display_name', detailDisplayKey: 'contract_display_name', listDisplayIdentityCopy: true, showIdentityCopy: true, placeholder: '请选择当前可履约的生效合同', relationFilters: { candidate: 'order' }, relationStrictFilter: true, relationInvalidMessage: '所选配送合同已不可履约,已清空,请重新选择', relationEmptyText: '暂无可下单的生效合同,请先启用或续签配送合同' }), f('creator_type', { required: true, type: 'select', options: resourceSearchEnumOptions('gasorder_basic', 'creator_type') }), f('creator_identity', { label: '创建方', required: true, listDisplayKey: 'creator_display_name', listDisplayIdentityCopy: true, placeholder: '请先选择配送合同和创建方类型', readonlyRelationText: true, relationFilters: { status: '1' }, relationEmptyText: '当前合同下暂无可用的创建方', dynamicRelation: { parentKey: 'creator_type', parentLabel: '创建方类型', contextParentKey: 'gasorder_contract_identity', resources: { user: '/user_account', staff: '/staff_account', delivery: '/delivery_basic', gas: '/gas_basic' }, @@ -587,7 +628,7 @@ export const resources: ResourceUiDefinition[] = [ parentChangeMessage: '配送合同已变更,请重新选择该合同用户的收货地址', }, }), f('gasorder_contract_product_identities', { label: '合同气瓶', required: true, type: 'identity-list', listDisplayKey: 'contract_products_summary', listDetailLink: true, emptyText: '未填写', relation: '/gasorder_contract_product', placeholder: '请输入智能气阀名称、设备类型或设备编码搜索', relationOptionDisplay: 'product-name-type', relationEmptyText: '该合同暂无可用气瓶,请先为合同绑定气瓶', relationLinkage: { parentKey: 'gasorder_contract_identity', optionParentKey: '', filterKey: 'contract_identity', filterOnly: true, requiresParent: true, parentChangeMessage: '配送合同已变更,请重新选择该合同可用的气瓶' } }), f('contact_name', { required: true }), f('contact_phone', { required: true }), f('discount_amount'), f('remark')], 'list', [ - { name: '分配订单', resource: '/gasorder_basic/:identity/assign', fields: [relation('delivery_basic_identity', '/delivery_basic', true, { label: '配送点', placeholder: '请选择合同履约范围内的配送点', relationFilters: { status: '1' } }), relation('staff_account_identity', '/staff_account', true, { label: '配送人员', placeholder: '请先选择配送点', relationEmptyText: '该配送点暂无在岗且资质有效的配送人员', staffRelation: { roles: ['delivery'], enabledOnly: true, workStatus: 'on_duty', validCredentialOnly: true } }), ...reason], visibleFor: { field: 'order_status', values: [16, 18] } }, + { name: '分配订单', resource: '/gasorder_basic/:identity/assign', fields: [relation('delivery_basic_identity', '/delivery_basic', true, { label: '配送点', placeholder: '请选择合同履约范围内的配送点', relationFilters: { status: '1' } }), relation('staff_account_identity', '/staff_account', true, { label: '配送人员', placeholder: '请选择当前配送点内的配送人员', relationEmptyText: '该配送点暂无在岗且资质有效的配送人员', staffRelation: { roles: ['delivery'], enabledOnly: true, workStatus: 'on_duty', validCredentialOnly: true } }), ...reason], visibleFor: { field: 'order_status', values: [16, 18] } }, { name: '开始罐装', resource: '/gasorder_basic/:identity/filling', fields: reason, visibleFor: { field: 'order_status', values: [18] } }, { name: '待配送', resource: '/gasorder_basic/:identity/ready', fields: reason, visibleFor: { field: 'order_status', values: [19] } }, { name: '开始配送', resource: '/gasorder_basic/:identity/delivering', fields: reason, visibleFor: { field: 'order_status', values: [20] } }, @@ -597,34 +638,109 @@ export const resources: ResourceUiDefinition[] = [ { name: '恢复订单', resource: '/gasorder_basic/:identity/recover', fields: reason, visibleFor: { field: 'order_status', values: [21] } }, { name: '取消订单', resource: '/gasorder_basic/:identity/cancel', danger: true, fields: reason, visibleFor: { field: 'order_status', values: [16, 18] } }, ]), - define('gasorder_item', '订单明细', 'readonly', []), - define('gasorder_assign', '分配记录', 'readonly', []), - define('gasorder_status', '状态记录', 'readonly', []), - define('gasorder_track', '运行轨迹', 'readonly', []), - define('gasorder_track_point', '轨迹点', 'readonly', []), - define('gasorder_confirm', '确认记录', 'readonly', []), - define('gasorder_payment', '支付记录', 'readonly', []), + define('gasorder_item', '订单明细', 'readonly', [ + relation('gasorder_basic_identity', '/gasorder_basic'), + relation('gasorder_contract_product_identity', '/gasorder_contract_product'), + relation('product_info_identity', '/product_info'), + f('product_code'), f('product_type_name'), f('product_params'), f('unit_price'), f('active'), + ]), + define('gasorder_assign', '分配记录', 'readonly', [ + relation('gasorder_basic_identity', '/gasorder_basic'), + relation('gas_basic_identity', '/gas_basic'), + relation('delivery_basic_identity', '/delivery_basic'), + relation('staff_account_identity', '/staff_account'), + f('assigner_name'), f('assigner_identity'), f('assigned_at'), f('reason'), + ]), + define('gasorder_status', '状态记录', 'readonly', [ + relation('gasorder_basic_identity', '/gasorder_basic'), + f('from_status'), f('to_status'), f('operator_name'), f('operator_identity'), f('occurred_at'), f('reason'), + ]), + define('gasorder_track', '运行轨迹', 'readonly', [ + relation('gasorder_basic_identity', '/gasorder_basic'), + relation('staff_account_identity', '/staff_account'), + f('attempt_no'), f('started_at'), f('completed_at'), + ]), + define('gasorder_track_point', '轨迹点', 'readonly', [ + relation('gasorder_track_identity', '/gasorder_track'), + f('request_no'), f('longitude'), f('latitude'), f('occurred_at'), f('received_at'), + f('source'), f('accuracy'), f('speed'), f('direction'), + ]), + define('gasorder_confirm', '确认记录', 'readonly', [ + relation('gasorder_basic_identity', '/gasorder_basic'), + f('request_no'), f('confirm_type'), f('recipient_name'), f('recipient_phone'), + f('proof_uri'), f('confirmed_at'), f('remark'), + ]), + define('gasorder_payment', '支付记录', 'readonly', [ + relation('gasorder_basic_identity', '/gasorder_basic'), + relation('payment_order_identity', '/payment_order'), + f('attempt_no'), f('amount'), + ]), define('ec_category', '商品分类', 'writable', [relation('parent_identity', '/ec_category'), f('name', { required: true }), f('sort_no')], 'tree'), define('ec_product', '商品', 'writable', [relation('ec_category_identity', '/ec_category', true), f('product_code', { required: true }), f('name', { required: true }), f('price_amount', { required: true }), f('stock_quantity')]), define('ec_product_attribute', '商品属性', 'writable', [relation('ec_product_identity', '/ec_product', true), f('name', { required: true }), f('value', { required: true }), f('sort_no')]), define('ec_product_image', '商品图片', 'writable', [relation('ec_product_identity', '/ec_product', true), f('image_uri', { required: true }), f('sort_no'), f('is_cover')]), - define('ec_cart', '购物车', 'readonly', []), - define('ec_order', '商城订单', 'readonly', []), - define('ec_order_item', '商城订单明细', 'readonly', []), - define('ec_review', '商品评价', 'readonly', []), + define('ec_cart', '购物车', 'readonly', [ + relation('user_account_identity', '/user_account'), relation('ec_product_identity', '/ec_product'), + f('quantity'), f('selected'), + ]), + define('ec_order', '商城订单', 'readonly', [ + f('order_no'), f('request_no'), f('order_status', { type: 'select', options: [ + { label: '待支付', value: 16 }, + { label: '已支付', value: 18 }, + { label: '已取消', value: 22 }, + ], unknownValueLabel: '未知订单状态' }), + relation('user_account_identity', '/user_account'), relation('gas_basic_identity', '/gas_basic'), + relation('delivery_basic_identity', '/delivery_basic'), relation('user_address_identity', '/user_address'), + f('contact_name'), f('contact_phone'), f('product_amount'), f('discount_amount'), f('payable_amount'), + f('total_amount'), f('paid_at'), f('logistics_no'), f('logistics_company'), f('logistics_status'), + f('shipped_at'), f('received_at'), f('remark'), + ]), + define('ec_order_item', '商城订单明细', 'readonly', [ + relation('ec_order_identity', '/ec_order'), relation('ec_product_identity', '/ec_product'), + f('product_snapshot'), f('quantity'), f('sale_amount'), + ]), + define('ec_review', '商品评价', 'readonly', [ + relation('ec_order_identity', '/ec_order'), relation('ec_product_identity', '/ec_product'), + relation('user_account_identity', '/user_account'), f('score'), f('content'), + ]), - define('wallet_basic', '钱包', 'readonly', [f('owner_type'), f('owner_identity'), f('alipay_id'), f('alipay_name'), f('wxpay_id'), f('wxpay_name'), f('balance'), f('withdrawal_balance')], 'list', [ + define('wallet_basic', '钱包', 'readonly', [f('owner_type'), f('owner_identity', { + type: 'identity', label: '归属主体', listRelationNameOnly: true, + displayRelationLabel: true, showIdentityCopy: true, + dynamicRelation: { + parentKey: 'owner_type', parentLabel: '归属类型', + resources: { user: '/user_account', staff: '/staff_account', gas: '/gas_basic', delivery: '/delivery_basic' }, + }, + }), f('alipay_id'), f('alipay_name'), f('wxpay_id'), f('wxpay_name'), f('balance'), f('withdrawal_balance')], 'list', [ { name: '后台充值', resource: '/wallet_basic/:identity/recharge', fields: [f('request_no', { required: true }), f('amount', { required: true }), f('withdrawable'), ...reason, f('remark')] }, { name: '修改钱包状态', resource: '/wallet_basic/:identity/status', method: 'PATCH', fields: [f('status', { required: true, type: 'select', options: [{ label: '启用', value: 1 }, { label: '停用', value: 2 }, { label: '冻结', value: 4 }] })] }, ]), - define('wallet_bank', '银行卡', 'readonly', []), - define('payment_order', '钱包支付记录', 'readonly', []), - define('wallet_record', '钱包流水', 'readonly', []), + define('wallet_bank', '银行卡', 'readonly', [ + relation('wallet_basic_identity', '/wallet_basic'), f('bank_name'), f('card_owner'), + f('card_no_last4'), f('bind_id'), f('bank_type'), f('bank'), + ]), + define('payment_order', '钱包支付记录', 'readonly', [ + f('payment_no'), f('request_no'), f('payment_status'), f('business_type'), + f('business_identity', { type: 'identity', label: '业务对象', listRelationNameOnly: true, displayRelationLabel: true, showIdentityCopy: true, dynamicRelation: { + parentKey: 'business_type', parentLabel: '业务类型', resources: { gasorder: '/gasorder_basic', ec_order: '/ec_order' }, + } }), relation('user_identity', '/user_account'), f('merchant_identity'), f('channel'), f('pay_type'), + f('channel_trade_no'), f('amount'), f('subject'), f('failure_code'), f('failure_message'), + f('expires_at'), f('paid_at'), f('closed_at'), + ]), + define('wallet_record', '钱包流水', 'readonly', [ + relation('wallet_basic_identity', '/wallet_basic'), f('record_no'), f('request_no'), + f('direction'), f('trade_type'), f('amount'), f('fee'), f('balance_after'), + f('withdrawal_balance_after'), f('in_trade_no'), f('out_trade_no'), f('pay_channel'), + f('pay_type'), relation('related_record_identity', '/wallet_record'), f('operator_name'), + f('operator_identity'), f('ymd'), f('ym'), f('remark'), + ]), define('payment_refund', '退款审核', 'readonly', [ - f('refund_no'), f('business_type'), f('business_identity'), f('user_identity'), + f('refund_no'), f('business_type'), f('business_identity', { type: 'identity', label: '业务对象', listRelationNameOnly: true, displayRelationLabel: true, showIdentityCopy: true, dynamicRelation: { + parentKey: 'business_type', parentLabel: '业务类型', resources: { gasorder: '/gasorder_basic', ec_order: '/ec_order' }, + } }), relation('user_identity', '/user_account'), f('amount'), f('reason'), f('description'), f('refund_status'), - f('reviewer_identity'), f('review_remark'), f('reviewed_at'), f('completed_at'), + relation('reviewer_identity', '/platform_account'), f('review_remark'), f('reviewed_at'), f('completed_at'), ], 'list', [ { name: '审核通过并退入钱包', resource: '/payment_refund/:identity/approve', fields: [f('remark')], visibleFor: { field: 'refund_status', values: [10] } }, { name: '驳回退款', resource: '/payment_refund/:identity/reject', danger: true, fields: [f('remark', { required: true })], visibleFor: { field: 'refund_status', values: [10] } }, @@ -652,16 +768,30 @@ export const resources: ResourceUiDefinition[] = [ { name: '标记处理完成', resource: '/wallet_apply_cash/:identity/complete', fields: [f('trade_no', { required: true }), f('callback_msg')], visibleFor: { field: 'apply_status', values: [25] } }, ]), - define('fin_payment', '财务支付记录', 'readonly', []), - define('fin_settlement', '财务结算', 'writable', [f('settlement_no', { required: true }), f('subject_type', { required: true }), f('subject_identity', { required: true }), f('period_start', { required: true }), f('period_end', { required: true })]), - define('fin_reconciliation', '财务对账', 'readonly', []), + define('fin_payment', '财务支付记录', 'readonly', [ + relation('ec_order_identity', '/ec_order'), f('payment_status'), f('channel'), f('amount'), f('paid_at'), + ]), + define('fin_settlement', '财务结算', 'writable', [f('settlement_no', { required: true }), f('subject_type', { required: true }), f('subject_identity', { + required: true, type: 'identity', label: '结算主体', listRelationNameOnly: true, + displayRelationLabel: true, showIdentityCopy: true, + dynamicRelation: { + parentKey: 'subject_type', parentLabel: '结算主体类型', + resources: { gas: '/gas_basic', delivery: '/delivery_basic' }, + }, + }), f('period_start', { required: true }), f('period_end', { required: true })]), + define('fin_reconciliation', '财务对账', 'readonly', [ + f('reconciliation_status'), f('channel'), f('bill_date'), f('difference_amount'), + ]), define('cms_content', '内容', 'writable', [f('content_type', { required: true }), f('title', { required: true }), f('body', { required: true }), f('version_no'), f('publish_status')]), define('cs_ticket', '客服工单', 'writable', [relation('user_account_identity', '/user_account', true), f('ticket_no', { required: true }), f('category', { required: true }), f('priority', { required: true })]), define('platform_account', '平台账户', 'writable', [f('username', { required: true }), f('password', { required: true }), f('display_name'), f('avatar'), f('platform_role_code', { required: true, unknownValueLabel: '未知角色' }), f('phone')]), define('platform_role', '平台角色', 'writable', [f('role_code', { required: true }), f('name', { required: true }), f('location_scope', { required: true, type: 'select', options: resourceSearchEnumOptions('platform_role', 'location_scope') })], 'list', [ { name: '分配菜单', resource: '/platform_role/:identity/menu', method: 'PUT', fields: [f('menu_identities', { type: 'identity-list', relation: '/platform_menu' })] }, ]), - define('platform_menu', '平台菜单', 'readonly', [], 'tree'), + define('platform_menu', '平台菜单', 'readonly', [ + relation('parent_identity', '/platform_menu'), f('group_code'), f('name'), f('icon'), + f('path'), f('sort_no'), + ], 'tree'), ]; export const resourceByPath = Object.fromEntries( diff --git a/frontend/platform_admin/src/views/resource/ResourceDetailContent.vue b/frontend/platform_admin/src/views/resource/ResourceDetailContent.vue index 46b4852..9fd0d50 100644 --- a/frontend/platform_admin/src/views/resource/ResourceDetailContent.vue +++ b/frontend/platform_admin/src/views/resource/ResourceDetailContent.vue @@ -1,6 +1,6 @@