From a7ac692110c0118b877e089a48774ca1b131ce31 Mon Sep 17 00:00:00 2001 From: czl231 <3286836406@qq.com> Date: Thu, 13 Aug 2026 00:54:27 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=90=88=E5=90=8C=E5=88=B0?= =?UTF-8?q?=E6=9C=9F=E6=97=B6=E9=97=B4=E4=B8=8E=E7=BB=88=E6=AD=A2=E6=81=A2?= =?UTF-8?q?=E5=A4=8D=E8=A7=84=E5=88=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 合同动作中文化并将到期时间设为必填\n- 隐藏修订记录无业务意义的删除时间\n- 禁止已终止合同直接启用,统一三端状态规则\n- 补充回归测试、模拟数据回填和中文文档 --- .../logic/platform/gasorder/gasorder.go | 19 +++++---- .../logic/platform/gasorder/gasorder_test.go | 12 ++++++ backend/api/internal/seed/mock.go | 10 ++++- docs/05-平台总后台需求.md | 4 +- docs/06-气站管理系统需求.md | 3 ++ docs/07-配送点管理系统需求.md | 1 + ...作日志_合同到期时间与动作中文化_20260813.md | 39 +++++++++++++++++++ ...日志_合同终止恢复与修订字段优化_20260813.md | 39 +++++++++++++++++++ frontend/delivery_admin/src/api/resources.ts | 6 +-- frontend/gas_admin/src/api/resources.ts | 4 +- .../scripts/check-resource-pages.mjs | 10 +++++ .../src/api/resource-display.ts | 11 ++++++ frontend/platform_admin/src/api/resources.ts | 6 +-- .../views/resource/ResourceDetailContent.vue | 6 ++- 14 files changed, 151 insertions(+), 19 deletions(-) create mode 100644 docs/操作日志_合同到期时间与动作中文化_20260813.md create mode 100644 docs/操作日志_合同终止恢复与修订字段优化_20260813.md diff --git a/backend/api/internal/logic/platform/gasorder/gasorder.go b/backend/api/internal/logic/platform/gasorder/gasorder.go index 0c906eb..00fcce2 100644 --- a/backend/api/internal/logic/platform/gasorder/gasorder.go +++ b/backend/api/internal/logic/platform/gasorder/gasorder.go @@ -144,11 +144,11 @@ func CreateGasorderContract(ctx *gin.Context) { DefaultDeliveryFee int64 `json:"default_delivery_fee"` SignedAt time.Time `json:"signed_at" binding:"required"` EffectiveAt time.Time `json:"effective_at" binding:"required"` - ExpiredAt *time.Time `json:"expired_at"` + ExpiredAt *time.Time `json:"expired_at" binding:"required"` } if err := ctx.ShouldBindJSON(&request); err != nil || request.DefaultDeliveryFee < 0 || request.SignedAt.IsZero() || request.EffectiveAt.IsZero() || - (request.ExpiredAt != nil && !request.ExpiredAt.After(request.EffectiveAt)) { + request.ExpiredAt == nil || !request.ExpiredAt.After(request.EffectiveAt) { infra.Response.Error(ctx, errcode.ErrInvalidArgument) return } @@ -213,10 +213,10 @@ func UpdateGasorderContract(ctx *gin.Context) { DefaultDeliveryFee int64 `json:"default_delivery_fee"` SignedAt time.Time `json:"signed_at" binding:"required"` EffectiveAt time.Time `json:"effective_at" binding:"required"` - ExpiredAt *time.Time `json:"expired_at"` + ExpiredAt *time.Time `json:"expired_at" binding:"required"` } if err := ctx.ShouldBindJSON(&request); err != nil || request.DefaultDeliveryFee < 0 || - (request.ExpiredAt != nil && !request.ExpiredAt.After(request.EffectiveAt)) { + request.ExpiredAt == nil || !request.ExpiredAt.After(request.EffectiveAt) { infra.Response.Error(ctx, errcode.ErrInvalidArgument) return } @@ -307,11 +307,11 @@ func TerminateGasorderContract(ctx *gin.Context) { func RenewGasorderContract(ctx *gin.Context) { var request struct { EffectiveAt time.Time `json:"effective_at" binding:"required"` - ExpiredAt *time.Time `json:"expired_at"` + ExpiredAt *time.Time `json:"expired_at" binding:"required"` Reason string `json:"reason" binding:"required"` } if err := ctx.ShouldBindJSON(&request); err != nil || - (request.ExpiredAt != nil && !request.ExpiredAt.After(request.EffectiveAt)) { + request.ExpiredAt == nil || !request.ExpiredAt.After(request.EffectiveAt) { infra.Response.Error(ctx, errcode.ErrInvalidArgument) return } @@ -351,7 +351,7 @@ func changeGasorderContract(ctx *gin.Context, action string, target int) { if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("identity = ?", ctx.Param("identity")).First(&contract).Error; err != nil { return err } - if action == "activate" && contract.ContractStatus != common.StatusDraft && contract.ContractStatus != common.StatusTerminated { + if action == "activate" && !contractCanActivate(contract.ContractStatus) { return errContractCannotActivate } if action == "terminate" && contract.ContractStatus != common.StatusActive { @@ -395,6 +395,11 @@ func changeGasorderContract(ctx *gin.Context, action string, target int) { infra.Response.Success(ctx, gin.H{"updated": true, "contract_status": target}) } +// contractCanActivate 限制启用动作仅用于首次启用草稿合同,终止合同必须通过续签恢复。 +func contractCanActivate(status int) bool { + return status == common.StatusDraft +} + // isContractChangeBusinessError 仅允许预定义业务原因透传,避免泄露数据库内部错误。 func isContractChangeBusinessError(err error) bool { return errors.Is(err, errContractCannotActivate) || diff --git a/backend/api/internal/logic/platform/gasorder/gasorder_test.go b/backend/api/internal/logic/platform/gasorder/gasorder_test.go index 4eb8c41..82af641 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) { } } +// TestTerminatedContractCannotActivate 验证终止合同不能绕过续签流程直接恢复生效。 +func TestTerminatedContractCannotActivate(t *testing.T) { + if !contractCanActivate(common.StatusDraft) { + t.Fatal("草稿合同应允许首次启用") + } + for _, status := range []int{common.StatusActive, common.StatusExpired, common.StatusTerminated} { + if contractCanActivate(status) { + t.Fatalf("合同状态 %d 不应允许直接启用", status) + } + } +} + func TestContractDeliveryRequiresDirectGasOwnership(t *testing.T) { if !contractDeliveryMatchesGas(10, 10) { t.Fatal("合同气站的直属配送点被错误拒绝") diff --git a/backend/api/internal/seed/mock.go b/backend/api/internal/seed/mock.go index a20264f..c596509 100644 --- a/backend/api/internal/seed/mock.go +++ b/backend/api/internal/seed/mock.go @@ -544,6 +544,8 @@ func MockData(database *gorm.DB) error { // seedAdditionalCoreScenarios 补齐十组可独立检索的核心主数据。 // 每组记录都使用本组父表的数据库 ID 建立关联,避免仅有展示字段而无真实关系。 func seedAdditionalCoreScenarios(database *gorm.DB, passwordHash string, now time.Time) error { + // 联调合同统一设置一年有效期,保证合同详情具有明确到期时间。 + nextYear := now.AddDate(1, 0, 0) for scenario := 2; scenario <= 10; scenario++ { sequence := 1000 + scenario*100 suffix := fmt.Sprintf("%03d", scenario) @@ -635,11 +637,17 @@ func seedAdditionalCoreScenarios(database *gorm.DB, passwordHash string, now tim ContractNo: "MOCK-CONTRACT-" + suffix, UserAccountID: user.ID, GasBasicID: gas.ID, DeliveryBasicID: delivery.ID, Title: fmt.Sprintf("示例供气合同 %d", scenario), Terms: "仅用于开发联调。", DefaultDeliveryFee: 500, - SignedAt: now, EffectiveAt: now, + SignedAt: now, EffectiveAt: now, ExpiredAt: &nextYear, } if err := put(database, &contract); err != nil { return err } + // 仅回填本函数生成的联调合同,修复旧种子执行后已存在的空到期时间。 + if err := database.Model(&models.GasorderContract{}). + Where("identity = ? AND contract_no = ? AND expired_at IS NULL", contract.Identity, contract.ContractNo). + Update("expired_at", nextYear).Error; err != nil { + return fmt.Errorf("backfill mock contract expiry: %w", err) + } contractProduct := models.GasorderContractProduct{ Entity: entity(sequence+11, common.StatusEnable), GasorderContractID: contract.ID, diff --git a/docs/05-平台总后台需求.md b/docs/05-平台总后台需求.md index 3fc1463..520592d 100644 --- a/docs/05-平台总后台需求.md +++ b/docs/05-平台总后台需求.md @@ -181,7 +181,7 @@ | 合同气瓶 | `/gasorder_contract_product` | 仅追加 | 绑定气瓶、按理由解绑 | | 合同修订 | `/gasorder_contract_revision` | 只读 | 查询启用、续签和终止形成的版本记录 | -合同关联用户和气站,可选关联配送点,记录合同编号、条款、附件、默认配送费及有效期。合同生命周期为草稿、生效、过期、终止。只有符合当前状态的专用动作可执行,启用、续签、终止和解绑均记录原因及操作者。 +合同关联用户和气站,可选关联配送点,记录合同编号、条款、附件、默认配送费及有效期。合同生命周期为草稿、生效、过期、终止。启用仅适用于草稿合同;已终止合同不得直接启用,需要恢复履约时必须通过续签填写新的生效时间、到期时间和原因。只有符合当前状态的专用动作可执行,启用、续签、终止和解绑均记录原因及操作者。 合同气瓶必须属于合同用户、处于可用状态且未报废;订单只能使用有效合同中仍绑定的气瓶。 @@ -301,7 +301,7 @@ 4. 所有表保留自增 `id` 主键;主资源通过 UUID V7 `identity` 对外访问,前端不依赖内部 ID。 5. 气站必须先审核再启停,驳回原因和审核人可追溯。 6. 智能气阀归属和生命周期变更合法,并自动记录归属历史。 -7. 合同启用、续签、终止和气瓶解绑只能在合法状态执行,并生成修订或操作记录。 +7. 合同启用、续签、终止和气瓶解绑只能在合法状态执行,并生成修订或操作记录;创建、草稿编辑和续签必须提交晚于生效时间的到期时间。 8. 配送订单只能沿规定状态机流转;分配、异常、恢复、取消和完成均生成过程记录。 9. 钱包充值幂等、余额与流水在同一事务更新;提现只能按审核状态机处理。 10. 只读与仅追加资源不存在通用更新、状态修改或删除路由。 diff --git a/docs/06-气站管理系统需求.md b/docs/06-气站管理系统需求.md index 3685c40..d8ce163 100644 --- a/docs/06-气站管理系统需求.md +++ b/docs/06-气站管理系统需求.md @@ -97,9 +97,12 @@ ### 4.5 合同管理 - 配送合同支持创建、草稿编辑、启用、续签和终止。 +- 启用仅适用于草稿合同;已终止合同不得直接启用,只能通过续签填写新的生效时间、到期时间和原因后恢复履约。 +- 配送合同创建、草稿编辑和续签必须填写到期时间,且到期时间必须晚于生效时间;历史缺失值应提示补录,不得推测回填。 - 合同只能选择当前气站用户及当前气站配送点。 - 合同气瓶支持绑定和受控解绑。 - 合同修订记录只读,不允许修改或删除。 +- 合同修订记录不展示公共软删除时间等无业务意义的基础设施字段。 - 合同状态只能通过专用动作接口推进,不能通用编辑 `contract_status`。 ### 4.6 燃气配送订单 diff --git a/docs/07-配送点管理系统需求.md b/docs/07-配送点管理系统需求.md index 56bde0d..bf3dd96 100644 --- a/docs/07-配送点管理系统需求.md +++ b/docs/07-配送点管理系统需求.md @@ -107,6 +107,7 @@ Global: - 配送点可完整管理当前配送点用户的配送合同。 - 支持创建、草稿编辑、启用、续签和终止。 +- 启用仅适用于草稿合同;已终止合同不得直接启用,只能通过续签填写新的生效时间、到期时间和原因后恢复履约。 - 支持绑定和受控解绑当前用户已有气瓶。 - 合同修订记录只读。 - 服务端强制合同所属气站和配送点为当前范围。 diff --git a/docs/操作日志_合同到期时间与动作中文化_20260813.md b/docs/操作日志_合同到期时间与动作中文化_20260813.md new file mode 100644 index 0000000..4d3861d --- /dev/null +++ b/docs/操作日志_合同到期时间与动作中文化_20260813.md @@ -0,0 +1,39 @@ +# 操作日志:合同到期时间与动作中文化 + +操作时间:2026-08-13 +操作类型:修改、扩展 +影响模块:配送合同创建、草稿编辑、续签、详情与模拟数据 + +## 操作前状态 + +合同修订记录直接显示英文动作 `terminate`。合同到期时间允许为空,详情页将历史空值显示为 `-`;部分开发联调合同也未写入到期时间。 + +## 具体操作 + +- 在统一展示层增加合同动作中文映射,将 `terminate` 显示为“终止合同”。 +- 将合同创建、草稿编辑和续签的到期时间调整为前后端必填,并校验其晚于生效时间。 +- 历史空到期时间显示为“待补录”,不使用推测日期替代真实业务数据。 +- 新生成的联调合同写入一年后的到期时间,并对身份和合同编号均匹配的旧联调合同执行幂等回填。 + +## 操作后状态 + +新提交合同不能缺少到期时间;历史真实合同缺失值会明确提示补录;开发联调合同再次执行种子后可显示确定的到期时间;后端稳定动作枚举保持不变。 + +## 代码变更 + +- `frontend/platform_admin/src/api/resource-display.ts`:增加动作中文映射和历史到期时间提示。 +- `frontend/platform_admin/src/api/resources.ts`:合同表单与续签动作要求到期时间必填。 +- `backend/api/internal/logic/platform/gasorder/gasorder.go`:增加服务端必填与时间顺序校验。 +- `backend/api/internal/seed/mock.go`:补齐并定向回填联调合同到期时间。 +- `frontend/platform_admin/scripts/check-resource-pages.mjs`:增加静态回归断言。 + +## 验证结果 + +- `go test ./internal/logic/platform/gasorder ./internal/seed`:通过。 +- `npm run resource-pages:check`:通过,覆盖详情 46 类、新建 25 类、编辑 23 类。 +- `npm run type:check`:通过。 +- `npm run build`:通过,Vite 成功构建 2620 个模块。 + +## 风险评估 + +接口从允许省略到期时间调整为必填,旧客户端若继续省略该字段将收到参数错误;历史真实数据不自动推测回填,需业务人员在草稿编辑或续签时补录。 diff --git a/docs/操作日志_合同终止恢复与修订字段优化_20260813.md b/docs/操作日志_合同终止恢复与修订字段优化_20260813.md new file mode 100644 index 0000000..3dc4a30 --- /dev/null +++ b/docs/操作日志_合同终止恢复与修订字段优化_20260813.md @@ -0,0 +1,39 @@ +# 操作日志:合同终止恢复与修订字段优化 + +操作时间:2026-08-13 +操作类型:修改 +影响模块:合同生命周期、合同详情修订记录、平台/气站/配送点管理端 + +## 操作前状态 + +合同详情的只读修订记录展示公共软删除字段“删除时间”。平台前端和后端允许已终止合同直接启用,各管理端的启用按钮状态配置也不一致。 + +## 具体操作 + +- 聚合详情表过滤 `deleted_at` 和 `DeletedAt`,保留数据库公共软删除能力但不向只读修订记录展示。 +- 后端启用动作仅接受草稿状态,已终止合同必须通过续签恢复。 +- 平台、气站和配送点管理端统一仅在草稿状态显示“启用合同”。 +- 增加终止合同不可直接启用的状态规则测试,并同步合同需求文档。 + +## 操作后状态 + +修订记录不再出现无业务意义的删除时间。已终止合同不能直接启用;恢复履约需要通过续签提交新的生效时间、到期时间和原因。 + +## 代码变更 + +- `backend/api/internal/logic/platform/gasorder/gasorder.go`:收紧合同启用状态。 +- `backend/api/internal/logic/platform/gasorder/gasorder_test.go`:增加状态规则测试。 +- `frontend/platform_admin/src/views/resource/ResourceDetailContent.vue`:隐藏聚合记录软删除字段。 +- 三个管理端的 `src/api/resources.ts`:统一合同启用按钮状态。 +- `frontend/platform_admin/scripts/check-resource-pages.mjs`:增加静态回归断言。 + +## 验证结果 + +- `go test ./internal/logic/platform/gasorder ./internal/logic/gas ./internal/logic/delivery`:通过。 +- 平台总后台 `npm run resource-pages:check`、`npm run type:check`、`npm run build`:通过。 +- 气站管理端 `npm run type:check`、`npm run build`:通过。 +- 配送点管理端 `npm run type:check`、`npm run build`:通过。 + +## 风险评估 + +调用方若尝试对已终止合同执行启用将收到状态错误;应改用续签接口。数据库字段和公开接口路径保持不变。 diff --git a/frontend/delivery_admin/src/api/resources.ts b/frontend/delivery_admin/src/api/resources.ts index ae6f99a..bde507d 100644 --- a/frontend/delivery_admin/src/api/resources.ts +++ b/frontend/delivery_admin/src/api/resources.ts @@ -361,7 +361,7 @@ const platformResources: ResourceUiDefinition[] = [ 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', [ - { name: '启用合同', resource: '/gasorder_contract/:identity/activate', fields: reason, visibleFor: { field: 'contract_status', values: [10] } }, + { name: '启用合同', resource: '/gasorder_contract/:identity/activate', fields: reason, visibleFor: { field: 'contract_status', values: [0] } }, { 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] } }, ], { canCreate: true, canEdit: true }), @@ -454,7 +454,7 @@ const gasOverrides: ResourceUiDefinition[] = [ ]), 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', [ - { name: '启用合同', resource: '/gasorder_contract/:identity/activate', fields: reason, visibleFor: { field: 'contract_status', values: [10] } }, + { name: '启用合同', resource: '/gasorder_contract/:identity/activate', fields: reason, visibleFor: { field: 'contract_status', values: [0] } }, { 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] } }, ], { canCreate: true, canEdit: true }), @@ -493,7 +493,7 @@ const deliveryOverrides: ResourceUiDefinition[] = [ ]), 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, 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/activate', fields: reason, visibleFor: { field: 'contract_status', values: [0] } }, { 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] } }, ], { canCreate: true, canEdit: true }), diff --git a/frontend/gas_admin/src/api/resources.ts b/frontend/gas_admin/src/api/resources.ts index 3340859..4b94d02 100644 --- a/frontend/gas_admin/src/api/resources.ts +++ b/frontend/gas_admin/src/api/resources.ts @@ -361,7 +361,7 @@ const platformResources: ResourceUiDefinition[] = [ 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', [ - { name: '启用合同', resource: '/gasorder_contract/:identity/activate', fields: reason, visibleFor: { field: 'contract_status', values: [10] } }, + { name: '启用合同', resource: '/gasorder_contract/:identity/activate', fields: reason, visibleFor: { field: 'contract_status', values: [0] } }, { 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] } }, ], { canCreate: true, canEdit: true }), @@ -454,7 +454,7 @@ const gasOverrides: ResourceUiDefinition[] = [ ]), 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, 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/activate', fields: reason, visibleFor: { field: 'contract_status', values: [0] } }, { 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] } }, ], { canCreate: true, canEdit: true }), diff --git a/frontend/platform_admin/scripts/check-resource-pages.mjs b/frontend/platform_admin/scripts/check-resource-pages.mjs index 4cc7d35..3ba46ac 100644 --- a/frontend/platform_admin/scripts/check-resource-pages.mjs +++ b/frontend/platform_admin/scripts/check-resource-pages.mjs @@ -17,6 +17,7 @@ const ruleSource = readFileSync(resolve(root, 'src/api/resource-page-rules.ts'), const listSource = readFileSync(resolve(root, 'src/views/shared/CrudListPage.vue'), 'utf8'); const detailSource = readFileSync(resolve(root, 'src/views/resource/ResourceDetailContent.vue'), 'utf8'); const displaySource = readFileSync(resolve(root, 'src/api/resource-display.ts'), 'utf8'); +const resourceSource = readFileSync(resolve(root, 'src/api/resources.ts'), 'utf8'); const treeResources = new Set(['ec_category', 'platform_menu']); const listResources = contract.resources.filter((item) => !treeResources.has(item.name)); const createModes = new Set(['writable', 'editable', 'append_only', 'managed']); @@ -59,9 +60,18 @@ for (const marker of [ "product_info_identity: '气瓶唯一标识'", "product_code: '气瓶编码'", "if (key === 'contract_status') return contractStatusLabel(Number(value))", + "terminate: '终止合同'", + "key === 'expired_at'", + "return '待补录'", ]) { if (!displaySource.includes(marker)) fail(`合同详情中文展示缺少 ${marker}`); } +if (!resourceSource.includes("visibleFor: { field: 'contract_status', values: [0] }")) { + fail('合同启用动作未限制为草稿状态'); +} +if (!detailSource.includes("!['deleted_at', 'DeletedAt'].includes(column)")) { + fail('聚合详情表未隐藏公共软删除字段'); +} for (const marker of [ "Number(row.contract_status) !== 0", "![0, 1].includes(Number(row.status))", diff --git a/frontend/platform_admin/src/api/resource-display.ts b/frontend/platform_admin/src/api/resource-display.ts index db380b0..f175fca 100644 --- a/frontend/platform_admin/src/api/resource-display.ts +++ b/frontend/platform_admin/src/api/resource-display.ts @@ -175,10 +175,21 @@ function relationLabel( /** 格式化不依赖字段定义的通用值。 */ export function displayRawValue(key: string, value: unknown) { + // 历史合同可能缺少到期时间,明确提示补录,避免将数据缺失误认为无期限合同。 + if (key === 'expired_at' && (value == null || value === '')) return '待补录'; if (value == null || value === '') return '-'; if (typeof value === 'boolean') return value ? '是' : '否'; if (key === 'status') return recordStatusLabel(Number(value)); if (key === 'contract_status') return contractStatusLabel(Number(value)); + if (key === 'action') { + return ( + { + activate: '启用合同', + renew: '续签合同', + terminate: '终止合同', + }[String(value)] ?? `未知动作(${String(value)})` + ); + } if ( amountKeys.has(key) || key.endsWith('_amount') || diff --git a/frontend/platform_admin/src/api/resources.ts b/frontend/platform_admin/src/api/resources.ts index 63a3bb1..368350a 100644 --- a/frontend/platform_admin/src/api/resources.ts +++ b/frontend/platform_admin/src/api/resources.ts @@ -464,10 +464,10 @@ export const resources: ResourceUiDefinition[] = [ f('title', { required: true }), f('terms'), f('file_uri', { label: '合同附件', type: 'contract-file' }), f('default_delivery_fee'), - f('signed_at', { required: true }), f('effective_at', { required: true }), f('expired_at'), + f('signed_at', { required: true }), f('effective_at', { required: true }), f('expired_at', { required: true, emptyText: '待补录' }), ], 'list', [ - { name: '启用合同', resource: '/gasorder_contract/:identity/activate', fields: reason, visibleFor: { field: 'contract_status', values: [0, 13] } }, - { 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/activate', fields: reason, visibleFor: { field: 'contract_status', values: [0] } }, + { name: '续签合同', resource: '/gasorder_contract/:identity/renew', fields: [f('effective_at', { required: true }), f('expired_at', { required: true }), ...reason], visibleFor: { field: 'contract_status', values: [11, 12] } }, { name: '终止合同', resource: '/gasorder_contract/:identity/terminate', danger: true, fields: reason, visibleFor: { field: 'contract_status', values: [11] } }, ], { canCreate: true, canEdit: true }), define('gasorder_contract_product', '合同气瓶', 'append_only', [relation('gasorder_contract_identity', '/gasorder_contract', true, { diff --git a/frontend/platform_admin/src/views/resource/ResourceDetailContent.vue b/frontend/platform_admin/src/views/resource/ResourceDetailContent.vue index 3a2612d..4400258 100644 --- a/frontend/platform_admin/src/views/resource/ResourceDetailContent.vue +++ b/frontend/platform_admin/src/views/resource/ResourceDetailContent.vue @@ -147,7 +147,11 @@ const collections = computed(() => ...new Set( rows.flatMap((row) => Object.keys(row).filter( - (column) => column !== 'id' && !column.endsWith('_id'), + // 聚合事实表不展示公共软删除元数据,避免只读修订记录出现无业务意义的“删除时间”。 + (column) => + column !== 'id' && + !column.endsWith('_id') && + !['deleted_at', 'DeletedAt'].includes(column), ), ), ),