From 3cad8fd0ba29e52f279eed49d4d96fb6fb5a5639 Mon Sep 17 00:00:00 2001 From: czl231 <3286836406@qq.com> Date: Wed, 12 Aug 2026 01:09:24 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=E9=85=8D=E9=80=81=E5=90=88?= =?UTF-8?q?=E5=90=8C=E5=85=B3=E8=81=94=E4=BF=A1=E6=81=AF=E5=B1=95=E7=A4=BA?= =?UTF-8?q?=E4=B8=8E=E5=BD=92=E5=B1=9E=E6=A0=A1=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 三端合同页面改为优先显示用户、气站和配送点业务名称,并保留标识与合同编号复制能力。 平台端增加气站与直属配送点联动,后端禁止平台主管及跨气站配送点绑定合同,同时补充测试和中文文档。 --- .../logic/platform/gasorder/gasorder.go | 7 ++- .../logic/platform/gasorder/gasorder_test.go | 12 +++++ .../操作日志_配送合同关联信息展示_20260812.md | 47 +++++++++++++++++++ docs/项目文档_配送合同关联信息展示_v1.0.md | 41 ++++++++++++++++ frontend/delivery_admin/src/api/resources.ts | 4 +- .../src/views/shared/CrudListPage.vue | 7 +++ frontend/gas_admin/src/api/resources.ts | 4 +- .../src/views/shared/CrudListPage.vue | 7 +++ .../check-staff-organization-linkage.mjs | 4 +- frontend/platform_admin/src/api/resources.ts | 25 +++++++++- .../src/views/shared/CrudListPage.vue | 4 ++ 11 files changed, 156 insertions(+), 6 deletions(-) create mode 100644 docs/操作日志_配送合同关联信息展示_20260812.md create mode 100644 docs/项目文档_配送合同关联信息展示_v1.0.md diff --git a/backend/api/internal/logic/platform/gasorder/gasorder.go b/backend/api/internal/logic/platform/gasorder/gasorder.go index 0c47235..e6dec98 100644 --- a/backend/api/internal/logic/platform/gasorder/gasorder.go +++ b/backend/api/internal/logic/platform/gasorder/gasorder.go @@ -739,7 +739,12 @@ func deliveryBelongsToGas(deliveryID, gasID uint64) bool { } var delivery models.DeliveryBasic return impl.DBService.Select("gas_basic_id").First(&delivery, deliveryID).Error == nil && - (delivery.GasBasicID == 0 || delivery.GasBasicID == gasID) + contractDeliveryMatchesGas(delivery.GasBasicID, gasID) +} + +// contractDeliveryMatchesGas 校验合同配送点必须直属于合同气站;平台主管配送点不能绑定到气站合同。 +func contractDeliveryMatchesGas(deliveryGasID, contractGasID uint64) bool { + return deliveryGasID != 0 && deliveryGasID == contractGasID } func gasorderStatusRecord(orderID uint64, from, to int, reason, operatorIdentity, operatorName string) *models.GasorderStatus { diff --git a/backend/api/internal/logic/platform/gasorder/gasorder_test.go b/backend/api/internal/logic/platform/gasorder/gasorder_test.go index 1579ce3..4eb8c41 100644 --- a/backend/api/internal/logic/platform/gasorder/gasorder_test.go +++ b/backend/api/internal/logic/platform/gasorder/gasorder_test.go @@ -106,6 +106,18 @@ func TestContractRevisionKeepsSingleContractHistory(t *testing.T) { } } +func TestContractDeliveryRequiresDirectGasOwnership(t *testing.T) { + if !contractDeliveryMatchesGas(10, 10) { + t.Fatal("合同气站的直属配送点被错误拒绝") + } + if contractDeliveryMatchesGas(0, 10) { + t.Fatal("平台主管配送点不应绑定到气站合同") + } + if contractDeliveryMatchesGas(11, 10) { + t.Fatal("其他气站配送点不应绑定到当前气站合同") + } +} + func TestGasorderSensitiveContactsAreMasked(t *testing.T) { value := map[string]any{ "contact_name": "张三", diff --git a/docs/操作日志_配送合同关联信息展示_20260812.md b/docs/操作日志_配送合同关联信息展示_20260812.md new file mode 100644 index 0000000..6c530e5 --- /dev/null +++ b/docs/操作日志_配送合同关联信息展示_20260812.md @@ -0,0 +1,47 @@ +# 配送合同关联信息展示操作日志 + +操作时间:2026-08-12 + +操作类型:修改、扩展 + +影响模块:平台总后台、气站后台、配送点后台、配送合同后端逻辑 + +## 操作前状态 + +合同列表直接展示用户、气站和配送点唯一标识,业务人员难以辨认;平台合同后端允许将平台主管配送点绑定到必填气站,与气站需求中的直属配送点规则不一致。 + +## 具体操作 + +- 三端合同资源定义增加业务名称列表标签、关系名称展示、标识复制及空值文案。 +- 平台端为气站与默认配送点增加父子联动,按所选气站查询直属配送点。 +- 合同编号使用可复制紧凑文本,悬浮可查看完整值。 +- 气站端和配送点端详情复用关系名称组件展示关联对象。 +- 后端新增合同配送点直属归属判定,排除平台主管配送点和跨气站配送点。 +- 增加后端边界测试,并更新前端联动配置检查数量。 + +## 行为变化 + +- 变更前:关联列主显唯一标识;平台 API 接受平台主管配送点。 +- 变更后:关联列主显业务名称并可复制标识;平台 API 仅接受合同气站直属配送点或空配送点。 + +## 代码变更 + +- `frontend/platform_admin/src/api/resources.ts`:合同字段展示及配送点联动配置。 +- `frontend/platform_admin/src/views/shared/CrudListPage.vue`:普通字段可复制列表展示。 +- `frontend/gas_admin/src/api/resources.ts`、`frontend/delivery_admin/src/api/resources.ts`:各自可见合同关联字段配置。 +- `frontend/gas_admin/src/views/shared/CrudListPage.vue`、`frontend/delivery_admin/src/views/shared/CrudListPage.vue`:详情关系名称展示。 +- `backend/api/internal/logic/platform/gasorder/gasorder.go`:合同配送点直属归属校验。 +- `backend/api/internal/logic/platform/gasorder/gasorder_test.go`:直属、平台主管和跨气站边界测试。 + +## 验证结果 + +- 平台、气站、配送点前端 `type:check` 均通过。 +- 三个 Web 前端生产构建均通过。 +- 平台工作人员/服务关系/合同组织联动检查通过。 +- 平台标准资源独立页面检查通过:详情 46 类、新建 25 类、编辑 23 类。 +- 后端 `go test ./internal/logic/platform/gasorder` 通过,覆盖直属、平台主管和跨气站配送点边界。 + +## 风险评估 + +- 历史不合规合同仍可查看,但编辑保存时会被新规则拦截;处理方式是清空默认配送点或选择合同气站直属配送点。 +- 校验只收紧合同场景,不改变订单分配等其他业务的平台主管组织规则。 diff --git a/docs/项目文档_配送合同关联信息展示_v1.0.md b/docs/项目文档_配送合同关联信息展示_v1.0.md new file mode 100644 index 0000000..9ea6162 --- /dev/null +++ b/docs/项目文档_配送合同关联信息展示_v1.0.md @@ -0,0 +1,41 @@ +# 配送合同关联信息展示项目文档 v1.0 + +## 项目概述 + +本次调整统一平台总后台、气站后台和配送点后台的配送合同关联信息展示。用户、气站和配送点以业务名称为主,唯一标识保留为悬浮查看与点击复制能力。平台总后台创建或编辑合同时,默认配送点只能选择合同气站的直属配送点。 + +## 主要功能 + +- 合同列表、详情、新建及编辑场景优先展示用户、气站、配送点业务名称。 +- 合同唯一标识继续保留;合同编号支持悬浮查看完整值并点击复制。 +- 平台端“所属气站”联动筛选“所属配送点”,配送点允许留空并显示“暂未指定”。 +- 用户和气站作为合同不可变归属,编辑页以只读名称展示并保留标识复制。 +- 平台直属配送点和其他气站配送点不能绑定到气站合同。 + +## 目录与核心文件 + +```text +platforms/ +├── frontend/platform_admin/src/api/resources.ts # 平台合同字段展示与组织联动配置 +├── frontend/gas_admin/src/api/resources.ts # 气站合同可见关联字段配置 +├── frontend/delivery_admin/src/api/resources.ts # 配送点合同用户字段配置 +├── backend/api/internal/logic/platform/gasorder/ # 合同配送点归属校验及测试 +└── docs/ # 项目文档与操作日志 +``` + +## 行为规则 + +1. 平台合同必须选择用户和气站,默认配送点可留空。 +2. 选择默认配送点时,该配送点的 `gas_basic_id` 必须等于合同气站 ID。 +3. `gas_basic_id = 0` 的平台主管配送点不属于具体气站,因此不能用于气站合同。 +4. 历史记录仍可查看;若历史合同保存了不合规配送点,再次编辑保存前必须清空或改选直属配送点。 + +## 维护与验证 + +- 修改关联展示时优先复用 `RelationNameText` 和资源字段配置,避免为单一页面硬编码。 +- 修改组织归属规则时同步运行平台前端联动检查、三端构建和后端 `gasorder` 单元测试。 +- 后端仍接受空配送点,保持原有“默认配送点可选”接口兼容性。 + +## 变更记录 + +- v1.0(2026-08-12):完成三端合同名称展示、标识复制、平台组织联动及后端直属归属校验。 diff --git a/frontend/delivery_admin/src/api/resources.ts b/frontend/delivery_admin/src/api/resources.ts index ee6213e..ae6f99a 100644 --- a/frontend/delivery_admin/src/api/resources.ts +++ b/frontend/delivery_admin/src/api/resources.ts @@ -30,6 +30,8 @@ export type ResourceField = { placeholder?: string; showIdentityCopy?: boolean; readonlyRelationText?: boolean; + /** 列表中以可悬浮、可复制的紧凑文本展示普通字段。 */ + listCopyable?: boolean; options?: Array<{ label: string; value: string | number }>; defaultValue?: string | number | boolean; readonly?: boolean; @@ -490,7 +492,7 @@ const deliveryOverrides: ResourceUiDefinition[] = [ { name: '重置密码', resource: '/user_account/:identity/password', method: 'PUT', fields: [f('password', { required: true })] }, ]), define('user_address', '用户地址', 'writable', [relation('user_account_identity', '/user_account', true), f('address', { required: true }), f('longitude'), f('latitude'), f('is_default')]), - define('gasorder_contract', '配送合同', 'managed', [f('contract_no', { required: true }), relation('user_account_identity', '/user_account', true), f('title', { required: true }), f('terms'), f('file_uri'), f('default_delivery_fee'), f('signed_at', { required: true }), f('effective_at', { required: true }), f('expired_at')], 'list', [ + define('gasorder_contract', '配送合同', 'managed', [f('contract_no', { required: true, listCopyable: true }), relation('user_account_identity', '/user_account', true, { label: '用户账户', listLabel: '用户账户', listRelationNameOnly: true, showIdentityCopy: true, readonlyRelationText: true }), f('title', { required: true }), f('terms'), f('file_uri'), f('default_delivery_fee'), f('signed_at', { required: true }), f('effective_at', { required: true }), f('expired_at')], 'list', [ { name: '启用合同', resource: '/gasorder_contract/:identity/activate', fields: reason, visibleFor: { field: 'contract_status', values: [10] } }, { name: '续签合同', resource: '/gasorder_contract/:identity/renew', fields: [f('effective_at', { required: true }), f('expired_at'), ...reason], visibleFor: { field: 'contract_status', values: [11, 12, 13] } }, { name: '终止合同', resource: '/gasorder_contract/:identity/terminate', danger: true, fields: reason, visibleFor: { field: 'contract_status', values: [11] } }, diff --git a/frontend/delivery_admin/src/views/shared/CrudListPage.vue b/frontend/delivery_admin/src/views/shared/CrudListPage.vue index cd957ce..97d268d 100644 --- a/frontend/delivery_admin/src/views/shared/CrudListPage.vue +++ b/frontend/delivery_admin/src/views/shared/CrudListPage.vue @@ -38,6 +38,7 @@ @@ -189,6 +190,7 @@
{{ formatValue(value) }}
+
@@ -1052,6 +1054,11 @@ function displayValue(key: string, value: unknown) { return String(value); } +// relationDetailField 将详情中的关联标识映射回资源字段,以复用业务名称展示和标识复制。 +function relationDetailField(key: string) { + return props.definition.fields.find((field) => field.key === key && field.relation); +} + /** 将各管理端稳定角色编码转换为中文名称,未知编码保留审计线索。 */ function accountRoleLabel(resource: string | undefined, value: unknown) { const code = String(value ?? ''); diff --git a/frontend/gas_admin/src/api/resources.ts b/frontend/gas_admin/src/api/resources.ts index 54e3f6e..3340859 100644 --- a/frontend/gas_admin/src/api/resources.ts +++ b/frontend/gas_admin/src/api/resources.ts @@ -30,6 +30,8 @@ export type ResourceField = { placeholder?: string; showIdentityCopy?: boolean; readonlyRelationText?: boolean; + /** 列表中以可悬浮、可复制的紧凑文本展示普通字段。 */ + listCopyable?: boolean; options?: Array<{ label: string; value: string | number }>; defaultValue?: string | number | boolean; readonly?: boolean; @@ -451,7 +453,7 @@ const gasOverrides: ResourceUiDefinition[] = [ { name: '重置密码', resource: '/user_account/:identity/password', method: 'PUT', fields: [f('password', { required: true })] }, ]), define('user_address', '用户地址', 'writable', [relation('user_account_identity', '/user_account', true), f('address', { required: true }), f('longitude'), f('latitude'), f('is_default')]), - define('gasorder_contract', '配送合同', 'managed', [f('contract_no', { required: true }), relation('user_account_identity', '/user_account', true), relation('delivery_basic_identity', '/delivery_basic'), f('title', { required: true }), f('terms'), f('file_uri'), f('default_delivery_fee'), f('signed_at', { required: true }), f('effective_at', { required: true }), f('expired_at')], 'list', [ + define('gasorder_contract', '配送合同', 'managed', [f('contract_no', { required: true, listCopyable: true }), relation('user_account_identity', '/user_account', true, { label: '用户账户', listLabel: '用户账户', listRelationNameOnly: true, showIdentityCopy: true, readonlyRelationText: true }), relation('delivery_basic_identity', '/delivery_basic', false, { label: '所属配送点', listLabel: '所属配送点', listRelationNameOnly: true, emptyText: '暂未指定', placeholder: '请选择默认配送点,可留空', showIdentityCopy: true }), f('title', { required: true }), f('terms'), f('file_uri'), f('default_delivery_fee'), f('signed_at', { required: true }), f('effective_at', { required: true }), f('expired_at')], 'list', [ { name: '启用合同', resource: '/gasorder_contract/:identity/activate', fields: reason, visibleFor: { field: 'contract_status', values: [10] } }, { name: '续签合同', resource: '/gasorder_contract/:identity/renew', fields: [f('effective_at', { required: true }), f('expired_at'), ...reason], visibleFor: { field: 'contract_status', values: [11, 12, 13] } }, { name: '终止合同', resource: '/gasorder_contract/:identity/terminate', danger: true, fields: reason, visibleFor: { field: 'contract_status', values: [11] } }, diff --git a/frontend/gas_admin/src/views/shared/CrudListPage.vue b/frontend/gas_admin/src/views/shared/CrudListPage.vue index cd957ce..97d268d 100644 --- a/frontend/gas_admin/src/views/shared/CrudListPage.vue +++ b/frontend/gas_admin/src/views/shared/CrudListPage.vue @@ -38,6 +38,7 @@ @@ -189,6 +190,7 @@
{{ formatValue(value) }}
+
@@ -1052,6 +1054,11 @@ function displayValue(key: string, value: unknown) { return String(value); } +// relationDetailField 将详情中的关联标识映射回资源字段,以复用业务名称展示和标识复制。 +function relationDetailField(key: string) { + return props.definition.fields.find((field) => field.key === key && field.relation); +} + /** 将各管理端稳定角色编码转换为中文名称,未知编码保留审计线索。 */ function accountRoleLabel(resource: string | undefined, value: unknown) { const code = String(value ?? ''); diff --git a/frontend/platform_admin/scripts/check-staff-organization-linkage.mjs b/frontend/platform_admin/scripts/check-staff-organization-linkage.mjs index a69be98..345e70c 100644 --- a/frontend/platform_admin/scripts/check-staff-organization-linkage.mjs +++ b/frontend/platform_admin/scripts/check-staff-organization-linkage.mjs @@ -160,8 +160,8 @@ const resourcesSource = await readFile( ); assert.equal( (resourcesSource.match(/relationLinkage:/g) ?? []).length, - 2, - '联动配置必须显式启用于工作人员和用户服务关系的配送点字段', + 3, + '联动配置必须显式启用于工作人员、用户服务关系和配送合同的配送点字段', ); assert.match(resourcesSource, /filterKey: 'gas_basic_identities'/); assert.match(resourcesSource, /organizationScoped: true/); diff --git a/frontend/platform_admin/src/api/resources.ts b/frontend/platform_admin/src/api/resources.ts index b147692..6440ab6 100644 --- a/frontend/platform_admin/src/api/resources.ts +++ b/frontend/platform_admin/src/api/resources.ts @@ -46,6 +46,8 @@ export type ResourceField = { placeholder?: string; showIdentityCopy?: boolean; readonlyRelationText?: boolean; + /** 列表中以可悬浮、可复制的紧凑文本展示普通字段。 */ + listCopyable?: boolean; options?: Array<{ label: string; value: string | number }>; defaultValue?: string | number | boolean; readonlyOnCreate?: boolean; @@ -429,7 +431,28 @@ export const resources: ResourceUiDefinition[] = [ 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('gasorder_contract', '配送合同', 'managed', [f('contract_no', { required: true }), relation('user_account_identity', '/user_account', true), relation('gas_basic_identity', '/gas_basic', true), relation('delivery_basic_identity', '/delivery_basic'), f('title', { required: true }), f('terms'), f('file_uri'), f('default_delivery_fee'), f('signed_at', { required: true }), f('effective_at', { required: true }), f('expired_at')], 'list', [ + define('gasorder_contract', '配送合同', 'managed', [ + f('contract_no', { required: true, listCopyable: true }), + relation('user_account_identity', '/user_account', true, { + label: '用户账户', listLabel: '用户账户', listRelationNameOnly: true, + displayRelationLabel: true, showIdentityCopy: true, readonlyRelationText: true, + }), + relation('gas_basic_identity', '/gas_basic', true, { + label: '所属气站', listLabel: '所属气站', listRelationNameOnly: true, + displayRelationLabel: true, showIdentityCopy: true, readonlyRelationText: true, + }), + relation('delivery_basic_identity', '/delivery_basic', false, { + label: '所属配送点', listLabel: '所属配送点', listRelationNameOnly: true, + displayRelationLabel: true, emptyText: '暂未指定', placeholder: '请选择默认配送点,可留空', + showIdentityCopy: true, + relationLinkage: { + parentKey: 'gas_basic_identity', optionParentKey: 'gas_basic_identity', + filterKey: 'gas_basic_identities', backfillParent: true, + }, + }), + f('title', { required: true }), f('terms'), f('file_uri'), f('default_delivery_fee'), + f('signed_at', { required: true }), f('effective_at', { required: true }), f('expired_at'), + ], 'list', [ { name: '启用合同', resource: '/gasorder_contract/:identity/activate', fields: reason, visibleFor: { field: 'contract_status', values: [10] } }, { name: '续签合同', resource: '/gasorder_contract/:identity/renew', fields: [f('effective_at', { required: true }), f('expired_at'), ...reason], visibleFor: { field: 'contract_status', values: [11, 12] } }, { name: '终止合同', resource: '/gasorder_contract/:identity/terminate', danger: true, fields: reason, visibleFor: { field: 'contract_status', values: [11] } }, diff --git a/frontend/platform_admin/src/views/shared/CrudListPage.vue b/frontend/platform_admin/src/views/shared/CrudListPage.vue index 1fac29e..015d6c9 100644 --- a/frontend/platform_admin/src/views/shared/CrudListPage.vue +++ b/frontend/platform_admin/src/views/shared/CrudListPage.vue @@ -66,6 +66,10 @@ v-else-if="field.type === 'identity' && !field.displayRelationLabel && identityFieldValue(field, record)" :value="identityFieldValue(field, record)" /> +