From 3ba05dc147f8aef18a64c03fa8d349b9c76d3d69 Mon Sep 17 00:00:00 2001 From: czl231 <3286836406@qq.com> Date: Fri, 14 Aug 2026 21:02:02 +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=88=97=E8=A1=A8=E5=8F=AF=E8=AF=BB=E4=BF=A1=E6=81=AF?= =?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 | 1 - .../platform/gasorder/order_list_display.go | 190 ++++++++++++++++++ .../gasorder/order_list_display_test.go | 38 ++++ ...作日志_配送订单列表可读信息优化_20260814.md | 44 ++++ frontend/platform_admin/src/api/resources.ts | 10 +- .../src/views/shared/CrudListPage.vue | 7 + 6 files changed, 286 insertions(+), 4 deletions(-) create mode 100644 backend/api/internal/logic/platform/gasorder/order_list_display.go create mode 100644 backend/api/internal/logic/platform/gasorder/order_list_display_test.go create mode 100644 docs/操作日志_配送订单列表可读信息优化_20260814.md diff --git a/backend/api/internal/logic/platform/gasorder/gasorder.go b/backend/api/internal/logic/platform/gasorder/gasorder.go index 05062d9..28b9033 100644 --- a/backend/api/internal/logic/platform/gasorder/gasorder.go +++ b/backend/api/internal/logic/platform/gasorder/gasorder.go @@ -82,7 +82,6 @@ func ListGasorderContractRevision(ctx *gin.Context) { func GetGasorderContractRevision(ctx *gin.Context) { common.GetResource(ctx, &models.GasorderContractRevision{}) } -func ListGasorderBasic(ctx *gin.Context) { common.ListResource(ctx, &models.GasorderBasic{}) } func GetGasorderBasic(ctx *gin.Context) { getGasorderBasic(ctx) } func ListGasorderAssign(ctx *gin.Context) { common.ListResource(ctx, &models.GasorderAssign{}) } func GetGasorderAssign(ctx *gin.Context) { common.GetResource(ctx, &models.GasorderAssign{}) } diff --git a/backend/api/internal/logic/platform/gasorder/order_list_display.go b/backend/api/internal/logic/platform/gasorder/order_list_display.go new file mode 100644 index 0000000..f48c26f --- /dev/null +++ b/backend/api/internal/logic/platform/gasorder/order_list_display.go @@ -0,0 +1,190 @@ +// 功能描述:为平台配送订单列表补充创建方名称、收货地址和合同气瓶摘要。 +// 版本:v1.0.0 +package gasorder + +import ( + "fmt" + "strings" + + "git.apinb.com/bsm-sdk/core/infra" + "git.apinb.com/heqiapp/platforms/backend/api/internal/impl" + "git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common" + "git.apinb.com/heqiapp/platforms/backend/api/internal/models" + "github.com/gin-gonic/gin" +) + +// gasorderListDisplay 是订单持久化快照与列表专用可读字段组成的响应模型。 +type gasorderListDisplay struct { + models.GasorderBasic + CreatorDisplayName string `json:"creator_display_name"` + ContractProductsSummary string `json:"contract_products_summary"` +} + +// ListGasorderBasic 返回平台订单分页列表,并补充只用于管理端展示的可读摘要。 +func ListGasorderBasic(ctx *gin.Context) { + page, size := common.PageSize(ctx) + var orders []models.GasorderBasic + var total int64 + query := common.ApplyKeywordFilter(ctx, common.ActiveRecords(impl.DBService.Model(&models.GasorderBasic{})), &models.GasorderBasic{}) + if err := query.Count(&total).Error; err != nil { + infra.Response.Error(ctx, err) + return + } + if err := query.Order("created_at desc").Offset((page - 1) * size).Limit(size).Find(&orders).Error; err != nil { + infra.Response.Error(ctx, err) + return + } + displays, err := buildGasorderListDisplays(orders) + if err != nil { + infra.Response.Error(ctx, err) + return + } + response, err := common.PublicResourceResponse(displays) + if err != nil { + infra.Response.Error(ctx, err) + return + } + protected := common.ProtectPreciseLocation(ctx, &models.GasorderBasic{}, response) + infra.Response.Success(ctx, gin.H{"total": total, "list": restoreGasorderListAddresses(protected, orders)}) +} + +// buildGasorderListDisplays 批量读取创建方与有效订单气瓶,避免列表逐行查询。 +func buildGasorderListDisplays(orders []models.GasorderBasic) ([]gasorderListDisplay, error) { + creatorNames, err := loadGasorderCreatorNames(orders) + if err != nil { + return nil, err + } + productSummaries, err := loadGasorderProductSummaries(orders) + if err != nil { + return nil, err + } + displays := make([]gasorderListDisplay, 0, len(orders)) + for _, order := range orders { + creatorName := strings.TrimSpace(creatorNames[creatorLookupKey(order.CreatorType, order.CreatorID)]) + if creatorName == "" { + creatorName = fmt.Sprintf("记录已失效(%s)", order.CreatorIdentity) + } + displays = append(displays, gasorderListDisplay{ + GasorderBasic: order, + CreatorDisplayName: creatorName, + ContractProductsSummary: productSummaries[order.ID], + }) + } + return displays, nil +} + +func creatorLookupKey(creatorType string, creatorID uint64) string { + return fmt.Sprintf("%s:%d", creatorType, creatorID) +} + +// loadGasorderCreatorNames 按四类创建方批量加载当前可读名称,归档或已删除记录不参与展示。 +func loadGasorderCreatorNames(orders []models.GasorderBasic) (map[string]string, error) { + ids := map[string][]uint64{} + for _, order := range orders { + ids[order.CreatorType] = append(ids[order.CreatorType], order.CreatorID) + } + names := map[string]string{} + load := func(creatorType string, records any) error { + if len(ids[creatorType]) == 0 { + return nil + } + return common.ActiveRecords(impl.DBService).Where("id IN ? AND status <> ?", ids[creatorType], common.StatusArchived).Find(records).Error + } + var users []models.UserAccount + if err := load("user", &users); err != nil { + return nil, err + } + for _, item := range users { + names[creatorLookupKey("user", item.ID)] = firstReadableName(item.Name, item.RealName, item.Username) + } + var staff []models.StaffAccount + if err := load("staff", &staff); err != nil { + return nil, err + } + for _, item := range staff { + names[creatorLookupKey("staff", item.ID)] = firstReadableName(item.Name, item.Username) + } + var deliveries []models.DeliveryBasic + if err := load("delivery", &deliveries); err != nil { + return nil, err + } + for _, item := range deliveries { + names[creatorLookupKey("delivery", item.ID)] = item.Name + } + var stations []models.GasBasic + if err := load("gas", &stations); err != nil { + return nil, err + } + for _, item := range stations { + names[creatorLookupKey("gas", item.ID)] = item.Name + } + return names, nil +} + +func firstReadableName(values ...string) string { + for _, value := range values { + if value = strings.TrimSpace(value); value != "" { + return value + } + } + return "" +} + +// loadGasorderProductSummaries 生成“名称(类型)”或“名称(类型)等 N 个”的订单气瓶摘要。 +func loadGasorderProductSummaries(orders []models.GasorderBasic) (map[uint64]string, error) { + orderIDs := make([]uint64, 0, len(orders)) + for _, order := range orders { + orderIDs = append(orderIDs, order.ID) + } + if len(orderIDs) == 0 { + return map[uint64]string{}, nil + } + var items []gasorderItemDisplay + if err := gasorderItemDisplayQuery(impl.DBService, ""). + Where("gasorder_item.gasorder_basic_id IN ? AND gasorder_item.active = ?", orderIDs, true). + Order("gasorder_item.gasorder_basic_id asc, gasorder_item.id asc").Scan(&items).Error; err != nil { + return nil, err + } + type summary struct { + first string + count int + } + grouped := map[uint64]summary{} + for _, item := range items { + current := grouped[item.GasorderBasicID] + if current.count == 0 { + name := firstReadableName(item.ProductName, item.ProductCode, item.Identity) + if productType := strings.TrimSpace(item.ProductTypeName); productType != "" { + name += "(" + productType + ")" + } + current.first = name + } + current.count++ + grouped[item.GasorderBasicID] = current + } + result := map[uint64]string{} + for orderID, item := range grouped { + result[orderID] = item.first + if item.count > 1 { + result[orderID] = fmt.Sprintf("%s等 %d 个", item.first, item.count) + } + } + return result, nil +} + +// restoreGasorderListAddresses 仅在已鉴权的平台订单列表中恢复订单收货地址快照。 +func restoreGasorderListAddresses(response any, orders []models.GasorderBasic) any { + list, ok := response.([]any) + if !ok { + return response + } + for index, item := range list { + if index >= len(orders) { + break + } + if record, ok := item.(map[string]any); ok { + record["address"] = orders[index].Address + } + } + return response +} diff --git a/backend/api/internal/logic/platform/gasorder/order_list_display_test.go b/backend/api/internal/logic/platform/gasorder/order_list_display_test.go new file mode 100644 index 0000000..50bb870 --- /dev/null +++ b/backend/api/internal/logic/platform/gasorder/order_list_display_test.go @@ -0,0 +1,38 @@ +// 功能描述:验证平台配送订单列表的可读名称、地址恢复和空值兜底规则。 +// 版本:v1.0.0 +package gasorder + +import ( + "testing" + + "git.apinb.com/heqiapp/platforms/backend/api/internal/models" +) + +// TestFirstReadableName 验证创建方名称按首个非空业务名称降级。 +func TestFirstReadableName(t *testing.T) { + if got := firstReadableName("", " 张三 ", "staff-01"); got != "张三" { + t.Fatalf("首个可读名称 = %q,期望 张三", got) + } + if got := firstReadableName("", " "); got != "" { + t.Fatalf("全空名称应返回空字符串,实际为 %q", got) + } +} + +// TestRestoreGasorderListAddresses 验证仅按分页记录顺序恢复已鉴权订单的地址快照。 +func TestRestoreGasorderListAddresses(t *testing.T) { + response := []any{ + map[string]any{"identity": "order-1"}, + map[string]any{"identity": "order-2"}, + } + orders := []models.GasorderBasic{ + {Address: "上海市浦东新区客户路 88 号"}, + {Address: ""}, + } + restored := restoreGasorderListAddresses(response, orders).([]any) + if got := restored[0].(map[string]any)["address"]; got != orders[0].Address { + t.Fatalf("订单地址 = %v,期望 %s", got, orders[0].Address) + } + if got := restored[1].(map[string]any)["address"]; got != "" { + t.Fatalf("空地址应保持为空,实际为 %v", got) + } +} diff --git a/docs/操作日志_配送订单列表可读信息优化_20260814.md b/docs/操作日志_配送订单列表可读信息优化_20260814.md new file mode 100644 index 0000000..3b0847a --- /dev/null +++ b/docs/操作日志_配送订单列表可读信息优化_20260814.md @@ -0,0 +1,44 @@ +# 配送订单列表可读信息优化操作日志 + +操作时间:2026-08-14 +操作类型:修改 +影响模块:平台总后台、气体配送订单列表 + +## 操作前状态 + +- 创建方列直接显示唯一标识,管理员无法快速识别实际创建主体。 +- 收货地址已写入订单快照,但经过通用位置保护后列表显示为 `-`。 +- 合同气瓶是订单明细集合,通用列表没有生成可读摘要。 + +## 具体操作 + +- 为平台配送订单列表增加专用只读响应,批量解析用户、工作人员、配送站和气站的当前名称。 +- 在全局位置保护执行后,仅为已鉴权的平台订单列表恢复订单自身的收货地址快照。 +- 批量读取订单当前有效气瓶,生成“智能气阀名称(设备类型)”摘要;多只设备显示“等 N 个”。 +- 前端列表字段支持读取专用展示字段,合同气瓶摘要可直接进入订单详情。 +- 未扩展模糊搜索,未修改数据库结构和订单持久化字段。 + +## 操作后状态 + +- 创建方以当前可读名称展示;关联记录失效时显示“记录已失效(唯一标识)”。 +- 收货地址显示订单创建时保存的地址快照,空值显示“未填写”,长文本沿用表格悬浮全文能力。 +- 合同气瓶显示紧凑摘要,点击后可在详情页查看全部气瓶记录。 + +## 代码变更 + +- `backend/api/internal/logic/platform/gasorder/order_list_display.go` +- `backend/api/internal/logic/platform/gasorder/order_list_display_test.go` +- `backend/api/internal/logic/platform/gasorder/gasorder.go` +- `frontend/platform_admin/src/api/resources.ts` +- `frontend/platform_admin/src/views/shared/CrudListPage.vue` + +## 验证结果 + +- 后端平台配送订单专项测试通过。 +- 平台总后台 TypeScript 类型检查与生产构建通过。 + +## 风险评估 + +- 创建方展示的是关联档案当前名称,档案改名后列表名称会同步变化;订单的创建方唯一标识保持不变。 +- 地址恢复仅位于平台管理端订单列表专用接口,不修改通用敏感字段保护规则。 +- 气瓶摘要只统计订单当前有效明细,完整历史仍以订单详情为准。 diff --git a/frontend/platform_admin/src/api/resources.ts b/frontend/platform_admin/src/api/resources.ts index f6898be..6d08315 100644 --- a/frontend/platform_admin/src/api/resources.ts +++ b/frontend/platform_admin/src/api/resources.ts @@ -78,6 +78,10 @@ export type ResourceField = { readonlyRelationText?: boolean; /** 列表中以可悬浮、可复制的紧凑文本展示普通字段。 */ listCopyable?: boolean; + /** 列表读取另一个只读响应字段展示,表单仍使用原业务字段。 */ + listDisplayKey?: string; + /** 点击列表展示内容时进入当前记录详情。 */ + listDetailLink?: boolean; options?: Array<{ label: string; value: string | number }>; defaultValue?: string | number | boolean; readonlyOnCreate?: boolean; @@ -555,7 +559,7 @@ export const resources: ResourceUiDefinition[] = [ { name: '解绑气瓶', resource: '/gasorder_contract_product/:identity/unbind', danger: true, fields: reason }, ]), define('gasorder_contract_revision', '合同修订记录', 'readonly', []), - define('gasorder_basic', '气体配送订单', 'append_only', [f('request_no', { required: true }), relation('gasorder_contract_identity', '/gasorder_contract', true, { label: '配送合同', 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, placeholder: '请先选择配送合同和创建方类型', readonlyRelationText: true, relationFilters: { status: '1' }, relationEmptyText: '当前合同下暂无可用的创建方', dynamicRelation: { + define('gasorder_basic', '气体配送订单', 'append_only', [f('request_no', { required: true }), relation('gasorder_contract_identity', '/gasorder_contract', true, { label: '配送合同', 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', 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' }, fixedIdentityKeys: { user: 'user_account_identity', gas: 'gas_basic_identity', delivery: 'delivery_basic_identity' }, @@ -567,7 +571,7 @@ export const resources: ResourceUiDefinition[] = [ parentChangeMessage: '创建方类型已变更,已按当前合同重新匹配创建方', contextChangeMessage: '配送合同已变更,创建方已按新合同重新匹配', } }), relation('user_address_identity', '/user_address', true, { - label: '收货地址', placeholder: '请先选择配送合同,再选择该合同用户的收货地址', + label: '收货地址', listDisplayKey: 'address', emptyText: '未填写', placeholder: '请先选择配送合同,再选择该合同用户的收货地址', relationOptionLabelKey: 'address', relationOptionDefaultKey: 'is_default', relationAutoSelectKey: 'is_default', relationEmptyText: '该合同用户暂无收货地址,请先维护用户地址', relationLinkage: { @@ -575,7 +579,7 @@ export const resources: ResourceUiDefinition[] = [ filterKey: 'gasorder_contract_identity', filterOnly: true, requiresParent: true, parentChangeMessage: '配送合同已变更,请重新选择该合同用户的收货地址', }, - }), f('gasorder_contract_product_identities', { label: '合同气瓶', required: true, type: 'identity-list', 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', [ + }), 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), 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/shared/CrudListPage.vue b/frontend/platform_admin/src/views/shared/CrudListPage.vue index 5dacfa6..ad88cc3 100644 --- a/frontend/platform_admin/src/views/shared/CrudListPage.vue +++ b/frontend/platform_admin/src/views/shared/CrudListPage.vue @@ -70,6 +70,9 @@ v-else-if="field.listCopyable && record[field.key]" :value="String(record[field.key])" /> + + {{ displayListField(field, record) }} + @@ -464,6 +467,10 @@ async function changePage(next: number) { /** 使用资源静态选项或运行时平台角色名称显示列表字段。 */ function displayListField(field: ResourceField, row: ResourceRow) { + if (field.listDisplayKey) { + const value = row[field.listDisplayKey]; + return value == null || value === '' ? (field.emptyText ?? '-') : String(value); + } return displayResourceField(field, row, relations.options, fieldOptions); }