Files
junhong_cmp_fiber/internal/handler/validation/validation.go
break 333ba4b647
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m23s
feat(H5弹窗): AUG26-007 风险换卡与运营弹窗投放通知
新增 000225 迁移:运营弹窗配置表 tb_h5_popup_configuration(页面/范围/优先级/频率/受控动作/启停/有效期/版本)
与 tb_notification 可空 JSONB 列 popup_snapshot。

新增通知直建窄接口 DirectWriter.CreateOrGetPersonal:与 Outbox 消费共用 prepareDelivery 的渲染、
展示期与 CreateIdempotent 规则,冲突时回查返回既有行;同步扩展个人通知查询与已读两处类型白名单,
并按个人客户入口补齐投递审计来源。

新增 H5 候选与风险换卡:GET /api/c/v1/popup-candidates 先判风险资格(广电卡 + 风险停机 +
无活动物流换货单),命中只返回风险候选;未命中再按时间/启停/页面/店铺/设备类型/卡类型范围/频率
匹配运营配置。POST /api/c/v1/risk-exchanges/:asset_id/address 锁资产行后幂等创建待发货物流换货单,
首次地址锁定,不沿用资产级群发通知。

新增后台运营弹窗配置 CRUD 与启停(仅超级管理员与平台账号),更新递增版本并刷新最近更新时间,
标题与正文统一拒绝 URL 与前端路由,全部写操作记录操作者、前后值、版本与时间。

同步 OpenAPI(cmd/gendocs、cmd/api/docs.go、pkg/openapi/handlers.go)与参数校验中文提示共用实现。
2026-09-15 15:23:52 +08:00

115 lines
3.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Package validation 提供请求参数校验失败的可定位中文提示。
// 提示只使用字段的 description 与校验规则,不拼接底层错误文本,也不回显字段值。
package validation
import (
"reflect"
"strings"
"github.com/go-playground/validator/v10"
)
// Message 把请求校验失败转换为可定位字段的中文提示。
func Message(prefix string, req any, err error) string {
fieldErrs, ok := err.(validator.ValidationErrors)
if !ok || len(fieldErrs) == 0 {
return prefix
}
return prefix + "" + describeFieldError(req, fieldErrs[0])
}
// describeFieldError 用字段中文名与失败规则描述单个字段错误。
func describeFieldError(req any, fieldErr validator.FieldError) string {
label := fieldDescription(req, fieldErr.StructField())
switch fieldErr.Tag() {
case "required":
// 数字字段的 required 只在零值失败;说“不能为空”会误导为缺字段。
if isNumericField(req, fieldErr.StructField()) {
return label + "必须大于 0"
}
return label + "不能为空"
case "min":
if isNumericField(req, fieldErr.StructField()) {
return label + "不能小于 " + fieldErr.Param()
}
return label + "长度不能小于 " + fieldErr.Param()
case "max":
if isNumericField(req, fieldErr.StructField()) {
return label + "不能超过 " + fieldErr.Param()
}
return label + "长度不能超过 " + fieldErr.Param()
case "oneof":
return label + "必须为 " + strings.ReplaceAll(fieldErr.Param(), " ", "/") + " 之一"
default:
return label + "不合法(" + fieldErr.Tag() + ""
}
}
// isNumericField 判断字段是否为整数或浮点类型。
func isNumericField(req any, fieldName string) bool {
field, ok := lookupField(req, fieldName)
if !ok {
return false
}
switch field.Type.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
reflect.Float32, reflect.Float64:
return true
default:
return false
}
}
// lookupField 在去指针的结构体类型上按名取字段。
func lookupField(req any, fieldName string) (reflect.StructField, bool) {
typ := reflect.TypeOf(req)
for typ != nil && typ.Kind() == reflect.Ptr {
typ = typ.Elem()
}
if typ == nil || typ.Kind() != reflect.Struct {
return reflect.StructField{}, false
}
if field, ok := typ.FieldByName(fieldName); ok {
return field, true
}
// 嵌套(含匿名嵌入)结构体字段:校验错误报告的是内层字段名,提示也要能取到它的 description。
for index := range typ.NumField() {
field := typ.Field(index)
nested := field.Type
for nested.Kind() == reflect.Ptr {
nested = nested.Elem()
}
if nested.Kind() != reflect.Struct {
continue
}
if inner, ok := nested.FieldByName(fieldName); ok {
return inner, true
}
}
return reflect.StructField{}, false
}
// fieldDescription 取字段 description 的首个中文短语作为提示名,缺失时退回字段名。
func fieldDescription(req any, fieldName string) string {
field, ok := lookupField(req, fieldName)
if !ok {
return fieldName
}
description := strings.TrimSpace(field.Tag.Get("description"))
if description == "" {
return fieldName
}
if cut := strings.IndexAny(description, "(:,;"); cut > 0 {
description = strings.TrimSpace(description[:cut])
}
if description == "" {
return fieldName
}
// 提示名以拉丁字母/数字结尾时补一个空格,避免与后续中文粘连。
if last := description[len(description)-1]; last < 0x80 {
description += " "
}
return description
}