From eafb6fadf38bc87c321d08c213984588a60cf927 Mon Sep 17 00:00:00 2001 From: czl231 <3286836406@qq.com> Date: Fri, 14 Aug 2026 00:08:00 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E9=85=8D=E9=80=81=E8=AE=A2?= =?UTF-8?q?=E5=8D=95=E5=90=88=E5=90=8C=E6=B0=94=E7=93=B6=E5=80=99=E9=80=89?= =?UTF-8?q?=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../logic/platform/gasorder/gasorder.go | 25 +++++++- .../logic/platform/gasorder/gasorder_test.go | 28 +++++++++ docs/05-平台总后台需求.md | 2 + .../项目文档_配送订单合同气瓶显示优化_v1.0.md | 59 +++++++++++++++++++ frontend/platform_admin/package.json | 1 + ...heck-gasorder-contract-product-display.mjs | 20 +++++++ frontend/platform_admin/src/api/resources.ts | 2 +- .../src/views/resource/ResourceFieldForm.vue | 10 +++- 8 files changed, 144 insertions(+), 3 deletions(-) create mode 100644 docs/项目文档_配送订单合同气瓶显示优化_v1.0.md create mode 100644 frontend/platform_admin/scripts/check-gasorder-contract-product-display.mjs diff --git a/backend/api/internal/logic/platform/gasorder/gasorder.go b/backend/api/internal/logic/platform/gasorder/gasorder.go index 7980302..282df1f 100644 --- a/backend/api/internal/logic/platform/gasorder/gasorder.go +++ b/backend/api/internal/logic/platform/gasorder/gasorder.go @@ -62,9 +62,32 @@ func filterGasorderContractCandidates(query *gorm.DB, candidate string, now time ) } func GetGasorderContract(ctx *gin.Context) { getGasorderContract(ctx) } + +// ListGasorderContractProduct 在订单候选场景按合同收窄未解绑气瓶,未传合同时保留完整历史列表。 func ListGasorderContractProduct(ctx *gin.Context) { - common.ListResource(ctx, &models.GasorderContractProduct{}) + contractIdentity := strings.TrimSpace(ctx.Query("contract_identity")) + if contractIdentity == "" { + common.ListResource(ctx, &models.GasorderContractProduct{}) + return + } + contractID, err := common.ResolveIdentityID(&models.GasorderContract{}, contractIdentity, true) + if err != nil { + common.RespondRecordError(ctx, err) + return + } + common.ListPageFiltered[models.GasorderContractProduct](ctx, func(query *gorm.DB) *gorm.DB { + return filterGasorderContractProductCandidates(query, contractID) + }) } + +// filterGasorderContractProductCandidates 仅保留当前合同仍有效绑定的气瓶,并按类型和编码排序。 +func filterGasorderContractProductCandidates(query *gorm.DB, contractID uint64) *gorm.DB { + return query. + Where("gasorder_contract_id = ? AND unbound_at IS NULL", contractID). + Order("product_type_name asc"). + Order("product_code asc") +} + func GetGasorderContractProduct(ctx *gin.Context) { common.GetResource(ctx, &models.GasorderContractProduct{}) } diff --git a/backend/api/internal/logic/platform/gasorder/gasorder_test.go b/backend/api/internal/logic/platform/gasorder/gasorder_test.go index dc81855..fb490a7 100644 --- a/backend/api/internal/logic/platform/gasorder/gasorder_test.go +++ b/backend/api/internal/logic/platform/gasorder/gasorder_test.go @@ -71,6 +71,34 @@ func TestFilterGasorderContractCandidates(t *testing.T) { } } +// TestFilterGasorderContractProductCandidates 验证订单候选仅包含当前合同未解绑气瓶并按业务字段排序。 +func TestFilterGasorderContractProductCandidates(t *testing.T) { + sqlDatabase, _, err := sqlmock.New() + if err != nil { + t.Fatalf("创建模拟数据库失败:%v", err) + } + defer sqlDatabase.Close() + database, err := gorm.Open(postgres.New(postgres.Config{Conn: sqlDatabase}), &gorm.Config{}) + if err != nil { + t.Fatalf("创建 GORM 数据库失败:%v", err) + } + statement := database.ToSQL(func(tx *gorm.DB) *gorm.DB { + var products []models.GasorderContractProduct + return filterGasorderContractProductCandidates( + tx.Model(&models.GasorderContractProduct{}), 27, + ).Find(&products) + }) + for _, fragment := range []string{ + "gasorder_contract_id = 27", + "unbound_at IS NULL", + "ORDER BY product_type_name asc,product_code asc", + } { + if !strings.Contains(statement, fragment) { + t.Fatalf("合同气瓶候选缺少 %q:%s", fragment, statement) + } + } +} + func TestGasorderCreatorTypesCoverEveryConfirmedOrigin(t *testing.T) { for _, creatorType := range []string{"user", "staff", "delivery", "gas"} { if gasorderCreatorModels[creatorType] == nil { diff --git a/docs/05-平台总后台需求.md b/docs/05-平台总后台需求.md index 7dcb2ba..ba242e6 100644 --- a/docs/05-平台总后台需求.md +++ b/docs/05-平台总后台需求.md @@ -204,6 +204,8 @@ 创建订单页面必须先选择配送合同,收货地址下拉仅展示该合同签约用户的地址。地址选项以完整地址为主文案并标记默认地址;存在默认地址时自动选中,切换合同时清空原地址。若合同用户没有可用地址,页面应明确引导先维护用户地址并禁止提交。 +合同气瓶下拉仅展示所选合同中尚未解绑的气瓶,按气瓶类型和气瓶编码排序,并支持按气瓶编码或类型搜索。选项以气瓶类型名称展示;切换合同时清空原选择。若合同没有可用气瓶,页面应引导先为合同绑定气瓶并禁止提交。合同气瓶管理列表仍保留完整绑定和解绑历史。 + 订单合同候选必须同时满足“生效中、已到生效时间、尚未到期且到期时间有效”。草稿、未到生效时间、已到期和已终止合同不得进入订单候选;没有可履约合同时应引导管理员先启用或续签合同并禁止提交。 订单状态机如下: diff --git a/docs/项目文档_配送订单合同气瓶显示优化_v1.0.md b/docs/项目文档_配送订单合同气瓶显示优化_v1.0.md new file mode 100644 index 0000000..6973508 --- /dev/null +++ b/docs/项目文档_配送订单合同气瓶显示优化_v1.0.md @@ -0,0 +1,59 @@ +# 项目文档_配送订单合同气瓶显示优化_v1.0 + +## 1. 项目概述 + +本次优化平台总后台创建配送订单时的合同气瓶选择体验。原下拉框使用绑定记录唯一标识作为主文案,业务人员无法直接识别气瓶;同时前端携带的合同筛选参数未被列表接口应用,可能混入其他合同或已经解绑的记录。 + +## 2. 业务规则 + +- 创建订单前必须先选择配送合同。 +- 合同气瓶候选仅包含所选合同中尚未解绑的绑定记录。 +- 候选按气瓶类型名称、气瓶编码升序排列。 +- 支持按气瓶编码和气瓶类型名称搜索。 +- 选项显示气瓶类型名称,类型名称缺失时才降级显示唯一标识。 +- 多选后完整展示全部已选气瓶标签,方便业务人员逐项核对。 +- 切换配送合同时清空已选气瓶,并重新加载当前合同候选。 +- 没有候选时提示先为合同绑定气瓶,并禁止提交。 +- 合同气瓶管理列表不带合同筛选参数时继续展示完整历史。 +- 不新增订单占用规则,不修改数据库结构和现有数据。 + +## 3. 接口说明 + +订单表单继续复用平台接口: + +```text +GET /gasorder_contract_product?contract_identity=<配送合同唯一标识>&keyword=<可选搜索词> +``` + +传入 `contract_identity` 时,接口按合同内部主键和 `unbound_at IS NULL` 精确过滤。未传入时保持原管理列表行为,以兼容历史查询。 + +## 4. 核心文件 + +- `backend/api/internal/logic/platform/gasorder/gasorder.go`:合同气瓶候选过滤与排序。 +- `backend/api/internal/logic/platform/gasorder/gasorder_test.go`:候选 SQL 规则测试。 +- `frontend/platform_admin/src/api/resources.ts`:业务标签和空状态配置。 +- `frontend/platform_admin/src/api/resource-display.ts`:多字段关系标签组合。 +- `frontend/platform_admin/src/views/resource/ResourceFieldForm.vue`:父级选择顺序提示。 +- `frontend/platform_admin/scripts/check-gasorder-contract-product-display.mjs`:前端专项静态检查。 + +## 5. 维护指南 + +- 不得在订单候选中取消合同和未解绑条件,服务端最终创建校验仍需保留。 +- 若未来增加“订单占用气瓶”的排他规则,应先补充业务状态定义,再单独扩展候选和创建校验。 +- 新增组合关系标签时优先配置 `relationOptionLabelKeys`,不要在通用组件中硬编码资源名称。 + +## 6. 变更记录 + +操作时间:2026-08-13 + +操作类型:修改、扩展 + +影响模块:平台总后台、气体配送订单、合同气瓶 + +操作前状态:下拉选项显示绑定记录唯一标识,合同筛选参数未生效,可能出现跨合同或已解绑候选。 + +具体操作:增加服务端候选过滤与排序,扩展前端气瓶类型名称标签、完整多选展示、搜索提示和空候选引导。 + +操作后状态:业务人员可通过气瓶类型名称识别候选,订单页面只展示当前合同尚未解绑的气瓶。 + +风险评估:管理列表不带筛选参数时保持原行为;数据库无变更;订单创建最终校验保持不变。 diff --git a/frontend/platform_admin/package.json b/frontend/platform_admin/package.json index 415afb8..94a4461 100644 --- a/frontend/platform_admin/package.json +++ b/frontend/platform_admin/package.json @@ -24,6 +24,7 @@ "user-address-display:check": "node scripts/check-user-address-relation-display.mjs", "gasorder-address:check": "node scripts/check-gasorder-address-linkage.mjs", "gasorder-contract-candidates:check": "node scripts/check-gasorder-contract-candidates.mjs", + "gasorder-contract-products:check": "node scripts/check-gasorder-contract-product-display.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-gasorder-contract-product-display.mjs b/frontend/platform_admin/scripts/check-gasorder-contract-product-display.mjs new file mode 100644 index 0000000..75a2cf0 --- /dev/null +++ b/frontend/platform_admin/scripts/check-gasorder-contract-product-display.mjs @@ -0,0 +1,20 @@ +/** + * 功能:静态检查配送订单合同气瓶候选的可读显示与交互约束。 + * 版本:v1.0.0 + */ +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; + +const resources = readFileSync(new URL('../src/api/resources.ts', import.meta.url), 'utf8'); +const display = readFileSync(new URL('../src/api/resource-display.ts', import.meta.url), 'utf8'); +const form = readFileSync(new URL('../src/views/resource/ResourceFieldForm.vue', import.meta.url), 'utf8'); + +assert.match(resources, /label: '合同气瓶'/, '订单气瓶字段必须使用业务名称'); +assert.match(resources, /relationOptionLabelKey: 'product_type_name'/, '气瓶候选必须显示气瓶类型名称'); +assert.match(resources, /该合同暂无可用气瓶,请先为合同绑定气瓶/, '缺少无可用气瓶的业务引导'); +assert.match(resources, /filterKey: 'contract_identity'/, '订单气瓶请求必须携带合同标识'); +assert.match(display, /field\.relationOptionLabelKey/, '关系选项必须支持指定业务展示字段'); +assert.doesNotMatch(form, /max-tag-count/, '合同气瓶多选必须完整展示全部已选标签'); +assert.match(form, /'请先选择配送合同'/, '父合同未选时缺少操作顺序提示'); + +console.log('合同气瓶显示检查通过:候选显示气瓶类型名称,并完整展示全部已选标签。'); diff --git a/frontend/platform_admin/src/api/resources.ts b/frontend/platform_admin/src/api/resources.ts index a2ba99f..f779051 100644 --- a/frontend/platform_admin/src/api/resources.ts +++ b/frontend/platform_admin/src/api/resources.ts @@ -532,7 +532,7 @@ export const resources: ResourceUiDefinition[] = [ filterKey: 'gasorder_contract_identity', filterOnly: true, requiresParent: true, parentChangeMessage: '配送合同已变更,请重新选择该合同用户的收货地址', }, - }), f('gasorder_contract_product_identities', { required: true, type: 'identity-list', relation: '/gasorder_contract_product', placeholder: '请先选择配送合同,再选择该合同可用的气瓶', 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', [ + }), f('gasorder_contract_product_identities', { label: '合同气瓶', required: true, type: 'identity-list', relation: '/gasorder_contract_product', placeholder: '请输入气瓶编码或类型搜索', relationOptionLabelKey: 'product_type_name', 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), relation('staff_account_identity', '/staff_account', true, { staffRelation: { roles: ['delivery'], enabledOnly: true, workStatus: 'on_duty' } }), ...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] } }, diff --git a/frontend/platform_admin/src/views/resource/ResourceFieldForm.vue b/frontend/platform_admin/src/views/resource/ResourceFieldForm.vue index ce9b958..eed78ba 100644 --- a/frontend/platform_admin/src/views/resource/ResourceFieldForm.vue +++ b/frontend/platform_admin/src/views/resource/ResourceFieldForm.vue @@ -92,7 +92,7 @@ v-model="model[field.key]" :disabled="disabledSet.has(field.key) || relationRequiresParent(field)" :multiple="field.type === 'identity-list'" - :placeholder="field.placeholder" + :placeholder="relationPlaceholder(field)" :loading="relationLoading[field.relation ?? '']" allow-clear allow-search @@ -203,6 +203,14 @@ function relationRequiresParent(field: ResourceField) { ); } +/** 父级未选择时直接在控件内提示操作顺序,选择后恢复字段搜索提示。 */ +function relationPlaceholder(field: ResourceField) { + if (!relationRequiresParent(field)) return field.placeholder; + return field.relationLinkage?.parentKey === 'gasorder_contract_identity' + ? '请先选择配送合同' + : '请先选择上级数据'; +} + /** 根据父级和候选状态给出明确的关联选择引导。 */ function relationEmptyText(field: ResourceField) { if (relationRequiresParent(field)) {