From b85e879130017414217f719a26f2cdc6f02b958a Mon Sep 17 00:00:00 2001 From: czl231 <3286836406@qq.com> Date: Sat, 22 Aug 2026 10:12:47 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A7=84=E8=8C=83=E6=B0=94=E7=AB=99=E5=AE=A2?= =?UTF-8?q?=E6=9C=8D=E5=B7=A5=E5=8D=95=E6=9E=9A=E4=B8=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/api/internal/logic/gas/ticket.go | 8 ++- .../api/internal/logic/gas/ticket_values.go | 24 +++++++++ .../internal/logic/gas/ticket_values_test.go | 22 +++++++++ docs/06-气站管理系统需求.md | 1 + ...作日志_气站客服工单枚举规范化_20260822.md | 49 +++++++++++++++++++ docs/项目文档_气站标准资源全页管理_v1.0.md | 1 + .../scripts/check-backend-contract.mjs | 20 ++++++++ frontend/gas_admin/src/api/resources.ts | 33 ++++++++++++- 8 files changed, 154 insertions(+), 4 deletions(-) create mode 100644 backend/api/internal/logic/gas/ticket_values.go create mode 100644 backend/api/internal/logic/gas/ticket_values_test.go create mode 100644 docs/操作日志_气站客服工单枚举规范化_20260822.md diff --git a/backend/api/internal/logic/gas/ticket.go b/backend/api/internal/logic/gas/ticket.go index 25ac62a..571b5dc 100644 --- a/backend/api/internal/logic/gas/ticket.go +++ b/backend/api/internal/logic/gas/ticket.go @@ -47,7 +47,9 @@ func CreateTicket(ctx *gin.Context) { return } var request ticketRequest - if err := ctx.ShouldBindJSON(&request); err != nil { + if err := ctx.ShouldBindJSON(&request); err != nil || + !isSupportedTicketCategory(request.Category) || + !isSupportedTicketPriority(request.Priority) { infra.Response.Error(ctx, errcode.ErrInvalidArgument) return } @@ -77,7 +79,9 @@ func UpdateTicket(ctx *gin.Context) { return } var request ticketRequest - if err := ctx.ShouldBindJSON(&request); err != nil { + if err := ctx.ShouldBindJSON(&request); err != nil || + !isSupportedTicketCategory(request.Category) || + !isSupportedTicketPriority(request.Priority) { infra.Response.Error(ctx, errcode.ErrInvalidArgument) return } diff --git a/backend/api/internal/logic/gas/ticket_values.go b/backend/api/internal/logic/gas/ticket_values.go new file mode 100644 index 0000000..9ef8d90 --- /dev/null +++ b/backend/api/internal/logic/gas/ticket_values.go @@ -0,0 +1,24 @@ +// Package gas 定义气站客服工单允许写入的业务枚举。 +// 版本:v1.0.0 +package gas + +var supportedTicketCategories = map[string]struct{}{ + "delivery": {}, "installation": {}, "repair": {}, + "inspection": {}, "reinspection": {}, "customer_service": {}, +} + +var supportedTicketPriorities = map[string]struct{}{ + "low": {}, "normal": {}, "high": {}, "urgent": {}, +} + +// isSupportedTicketCategory 判断工单分类是否属于气站端公开枚举。 +func isSupportedTicketCategory(category string) bool { + _, ok := supportedTicketCategories[category] + return ok +} + +// isSupportedTicketPriority 判断工单优先级是否属于气站端公开枚举。 +func isSupportedTicketPriority(priority string) bool { + _, ok := supportedTicketPriorities[priority] + return ok +} diff --git a/backend/api/internal/logic/gas/ticket_values_test.go b/backend/api/internal/logic/gas/ticket_values_test.go new file mode 100644 index 0000000..3d48ed4 --- /dev/null +++ b/backend/api/internal/logic/gas/ticket_values_test.go @@ -0,0 +1,22 @@ +package gas + +import "testing" + +// TestSupportedTicketValues 验证气站端工单枚举白名单。 +func TestSupportedTicketValues(t *testing.T) { + for _, category := range []string{"delivery", "installation", "repair", "inspection", "reinspection", "customer_service"} { + if !isSupportedTicketCategory(category) { + t.Fatalf("标准工单分类未通过校验:%s", category) + } + } + for _, priority := range []string{"low", "normal", "high", "urgent"} { + if !isSupportedTicketPriority(priority) { + t.Fatalf("标准工单优先级未通过校验:%s", priority) + } + } + for _, value := range []string{"", "unknown", " normal "} { + if isSupportedTicketCategory(value) || isSupportedTicketPriority(value) { + t.Fatalf("非标准工单枚举不应通过校验:%q", value) + } + } +} diff --git a/docs/06-气站管理系统需求.md b/docs/06-气站管理系统需求.md index a85f574..76c2032 100644 --- a/docs/06-气站管理系统需求.md +++ b/docs/06-气站管理系统需求.md @@ -137,6 +137,7 @@ - 支持当前气站用户的客服工单新增、编辑、状态调整和归档。 - 不得读取或操作其他气站用户的工单。 - 不新增重复工单模型,统一复用 `cs_ticket`。 +- 工单分类使用配送、安装、维修、安检、复检、客服咨询六项必选枚举,不设置默认分类;优先级使用低、普通、高、紧急四项必选枚举,默认普通。前端显示中文,接口保存统一编码,气站接口拒绝未定义枚举。 ### 4.9 邀请注册 diff --git a/docs/操作日志_气站客服工单枚举规范化_20260822.md b/docs/操作日志_气站客服工单枚举规范化_20260822.md new file mode 100644 index 0000000..07b016a --- /dev/null +++ b/docs/操作日志_气站客服工单枚举规范化_20260822.md @@ -0,0 +1,49 @@ +# 操作日志:气站客服工单枚举规范化 + +操作时间:2026-08-22 +操作类型:修改 +影响模块:气站管理系统、客服工单创建与编辑接口 + +## 操作前状态 + +- 气站端工单分类和优先级使用自由文本输入,用户无法知道标准填写内容。 +- 服务端仅校验字段非空和长度,可写入任意分类或优先级字符串。 +- 列表和详情可能直接显示技术编码。 + +## 具体操作 + +- 工单分类改为配送、安装、维修、安检、复检、客服咨询六项中文下拉,不设置默认值。 +- 优先级改为低、普通、高、紧急四项中文下拉,默认选择普通。 +- 已知编码在新建、编辑、列表和详情统一显示中文;未知历史值保留原编码提示。 +- 气站工单创建和编辑接口增加枚举白名单,拒绝未定义编码。 +- 增加后端单元测试和前端静态契约检查。 + +## 操作后状态 + +- 用户通过明确的中文选项填写工单,不再猜测技术编码。 +- 绕过页面提交非法分类或优先级时,气站接口返回参数错误。 +- 5173、配送点端、用户客户端及工单状态流程保持不变。 + +## 代码变更 + +- `frontend/gas_admin/src/api/resources.ts`:定义客服工单分类和优先级字段。 +- `frontend/gas_admin/scripts/check-backend-contract.mjs`:增加客服工单枚举回归检查。 +- `backend/api/internal/logic/gas/ticket_values.go`:定义气站工单枚举白名单。 +- `backend/api/internal/logic/gas/ticket.go`:在创建和编辑入口校验枚举。 +- `backend/api/internal/logic/gas/ticket_values_test.go`:验证合法值和非法值边界。 +- `docs/06-气站管理系统需求.md`:同步工单枚举规则。 +- `docs/项目文档_气站标准资源全页管理_v1.0.md`:更新气站资源变更记录。 + +## 验证结果 + +- 后端全量测试 `go test ./...`:通过。 +- 气站端契约检查:通过,20 个资源一致。 +- 气站端独立页面检查:通过,详情 20 类、新建 11 类、编辑 8 类。 +- 修改文件代码检查:通过。 +- 气站端类型检查和生产构建:通过。 + +## 风险评估 + +- 现有标准编码保持兼容,不修改数据库字段或历史记录。 +- 历史未知编码仍可读取和展示,但再次编辑时必须选择标准值。 +- 白名单仅作用于气站工单创建和编辑接口,不改变其他客户端分类范围。 diff --git a/docs/项目文档_气站标准资源全页管理_v1.0.md b/docs/项目文档_气站标准资源全页管理_v1.0.md index a42f16d..219ab0b 100644 --- a/docs/项目文档_气站标准资源全页管理_v1.0.md +++ b/docs/项目文档_气站标准资源全页管理_v1.0.md @@ -76,6 +76,7 @@ platforms/ - 新增 11 类受数据范围保护的详情接口及路由测试。 - 同步气站资源契约和独立页面覆盖检查。 - 为工作人员与用户补充受气站范围保护的头像读取接口,并避免普通资料编辑误清空头像。 +- 客服工单分类和优先级改用中文下拉,优先级默认普通,并由气站接口校验统一枚举。 - 未修改数据库模型、状态机、金额规则或现有写接口。 ## 6. 维护指南 diff --git a/frontend/gas_admin/scripts/check-backend-contract.mjs b/frontend/gas_admin/scripts/check-backend-contract.mjs index ff8b670..5345b38 100644 --- a/frontend/gas_admin/scripts/check-backend-contract.mjs +++ b/frontend/gas_admin/scripts/check-backend-contract.mjs @@ -69,6 +69,26 @@ for (const option of [ } if (!fieldFormSource.includes(':disabled="option.disabled"')) throw new Error('未开通的提现渠道必须在下拉框中禁用'); +if (!source.includes("unknownValueLabel: '未知工单分类'")) + throw new Error('客服工单分类必须保留历史未知值提示'); +if (!source.includes("unknownValueLabel: '未知优先级'")) + throw new Error('客服工单优先级必须保留历史未知值提示'); +if (!source.includes("defaultValue: 'normal'")) + throw new Error('客服工单优先级必须默认选择普通'); +for (const option of [ + "{ label: '配送', value: 'delivery' }", + "{ label: '安装', value: 'installation' }", + "{ label: '维修', value: 'repair' }", + "{ label: '安检', value: 'inspection' }", + "{ label: '复检', value: 'reinspection' }", + "{ label: '客服咨询', value: 'customer_service' }", + "{ label: '低', value: 'low' }", + "{ label: '普通', value: 'normal' }", + "{ label: '高', value: 'high' }", + "{ label: '紧急', value: 'urgent' }", +]) { + if (!source.includes(option)) throw new Error(`客服工单枚举缺失:${option}`); +} const forbidden = [ '/platform/platform_', '/gas/gas_', '/delivery/delivery_', '/staff/', diff --git a/frontend/gas_admin/src/api/resources.ts b/frontend/gas_admin/src/api/resources.ts index b39d6a9..8e9fbd8 100644 --- a/frontend/gas_admin/src/api/resources.ts +++ b/frontend/gas_admin/src/api/resources.ts @@ -400,6 +400,35 @@ function define( const reason = [f('reason', { required: true })]; +/** 客服工单分类沿用平台统一编码,分类必须由创建人主动判断。 */ +const ticketCategoryField = f('category', { + required: true, + type: 'select', + unknownValueLabel: '未知工单分类', + options: [ + { label: '配送', value: 'delivery' }, + { label: '安装', value: 'installation' }, + { label: '维修', value: 'repair' }, + { label: '安检', value: 'inspection' }, + { label: '复检', value: 'reinspection' }, + { label: '客服咨询', value: 'customer_service' }, + ], +}); + +/** 客服工单优先级默认普通,仍允许创建人按实际情况调整。 */ +const ticketPriorityField = f('priority', { + required: true, + type: 'select', + defaultValue: 'normal', + unknownValueLabel: '未知优先级', + options: [ + { label: '低', value: 'low' }, + { label: '普通', value: 'normal' }, + { label: '高', value: 'high' }, + { label: '紧急', value: 'urgent' }, + ], +}); + /** 提现渠道保留未来选项,但当前仅开放银行卡提现。 */ const withdrawalChannelField = f('channel', { required: true, @@ -509,7 +538,7 @@ const platformResources: ResourceUiDefinition[] = [ 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('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('cs_ticket', '客服工单', 'writable', [relation('user_account_identity', '/user_account', true), f('ticket_no', { required: true }), ticketCategoryField, ticketPriorityField]), define('platform_account', '平台账户', 'writable', [f('username', { required: true }), f('password', { required: true }), f('display_name'), f('avatar'), f('platform_role_code', { required: true }), f('phone')]), define('platform_role', '平台角色', 'writable', [f('role_code', { required: true }), f('name', { required: true }), f('location_scope', { required: true, type: 'select', options: [{ label: '脱敏坐标', value: 'standard' }, { label: '精确坐标', value: 'precise' }] })], 'list', [ { name: '分配菜单', resource: '/platform_role/:identity/menu', method: 'PUT', fields: [f('menu_identities', { type: 'identity-list', relation: '/platform_menu' })] }, @@ -556,7 +585,7 @@ const gasOverrides: ResourceUiDefinition[] = [ define('wallet_apply_cash', '提现申请', 'append_only', [relation('wallet_bank_identity', '/wallet_bank'), f('request_no', { required: true }), f('amount', { required: true }), withdrawalChannelField, f('remark')]), define('fin_settlement', '财务结算', 'readonly', []), define('fin_reconciliation', '财务对账', 'readonly', []), - 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('cs_ticket', '客服工单', 'writable', [relation('user_account_identity', '/user_account', true), f('ticket_no', { required: true }), ticketCategoryField, ticketPriorityField]), ]; const gasResourceNames = new Set(gasOverrides.map((item) => item.name));