From 2ddaffe3734e21d97a21ccc0e2196df8d571f28b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=82=B9=E8=BF=9B?= Date: Sat, 24 Oct 2026 17:41:52 +0900 Subject: [PATCH] =?UTF-8?q?docs:=20=E8=BD=AE=E8=AF=A2=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E6=96=87=E6=A1=A3=E4=B8=8E=E5=AE=9E=E9=99=85?= =?UTF-8?q?=E8=A1=8C=E4=B8=BA=E5=AF=B9=E9=BD=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 匹配预览 GET 只收 query iccid,POST 只收请求体,去掉 POST 文档中多余的 query 参数 - 预览草稿 overrides 改为独立结构,字段全部可选,最多 20 条 - clear_fields 用具名类型生成数组元素枚举;ordered_ids 说明须为正整数且不重复 --- internal/handler/admin/polling_config.go | 11 ++- internal/model/dto/polling_config_dto.go | 76 +++++++++++++------ internal/routes/polling_config.go | 2 +- internal/service/polling/config_validation.go | 7 +- .../tasks.md | 2 +- 5 files changed, 67 insertions(+), 31 deletions(-) diff --git a/internal/handler/admin/polling_config.go b/internal/handler/admin/polling_config.go index 9762dcbd..8c039b41 100644 --- a/internal/handler/admin/polling_config.go +++ b/internal/handler/admin/polling_config.go @@ -217,8 +217,15 @@ func (h *PollingConfigHandler) MatchPreview(c *fiber.Ctx) error { if err := c.BodyParser(&req); err != nil { return errors.New(errors.CodeInvalidParam, "请求参数解析失败") } - } else if err := c.QueryParser(&req); err != nil { - return errors.New(errors.CodeInvalidParam, "请求参数解析失败") + } else { + var query dto.PollingConfigMatchPreviewQuery + if err := c.QueryParser(&query); err != nil { + return errors.New(errors.CodeInvalidParam, "请求参数解析失败") + } + req.ICCID = query.ICCID + } + if len(req.Overrides) > 20 { + return errors.New(errors.CodeInvalidParam, "匹配预览参数不合法:未保存修改最多 20 条") } if req.ICCID == "" { return errors.New(errors.CodeInvalidParam, "匹配预览参数不合法:ICCID 不能为空") diff --git a/internal/model/dto/polling_config_dto.go b/internal/model/dto/polling_config_dto.go index 6eb0372f..3268ffb3 100644 --- a/internal/model/dto/polling_config_dto.go +++ b/internal/model/dto/polling_config_dto.go @@ -21,22 +21,22 @@ type CreatePollingConfigRequest struct { // UpdatePollingConfigRequest 更新轮询配置请求 type UpdatePollingConfigRequest struct { - ConfigName *string `json:"config_name" validate:"omitempty,min=1,max=100" minLength:"1" maxLength:"100" description:"配置名称"` - CardCondition *string `json:"card_condition" validate:"omitempty,oneof=not_real_name real_name activated suspended" description:"卡状态条件 (not_real_name:未实名, real_name:已实名, activated:已激活, suspended:已停用)"` - CardCategory *string `json:"card_category" validate:"omitempty,oneof=normal industry" description:"卡业务类型 (normal:普通卡, industry:行业卡)"` - CarrierID *uint `json:"carrier_id" validate:"omitempty" description:"运营商ID(可选,精确匹配)"` - Priority *int `json:"priority" validate:"omitempty,min=1,max=1000" minimum:"1" maximum:"1000" description:"优先级(数字越小优先级越高)"` - RealnameCheckInterval *int `json:"realname_check_interval" validate:"omitempty,min=0" minimum:"0" description:"实名检查间隔(秒):正数按间隔(最小30秒),0 不检查且不再继承,不传或为空继承优先级更低的配置"` - CarddataCheckInterval *int `json:"carddata_check_interval" validate:"omitempty,min=0" minimum:"0" description:"流量检查间隔(秒):正数按间隔(最小60秒),0 不检查且不再继承,不传或为空继承优先级更低的配置"` - PackageCheckInterval *int `json:"package_check_interval" validate:"omitempty,min=0" minimum:"0" description:"套餐检查间隔(秒):正数按间隔(最小60秒),0 不检查且不再继承,不传或为空继承优先级更低的配置"` - ProtectCheckInterval *int `json:"protect_check_interval" validate:"omitempty,min=30" minimum:"30" description:"保护期一致性检查间隔(秒),NULL表示不检查,最小30秒"` - CardStatusCheckInterval *int `json:"card_status_check_interval" validate:"omitempty,min=0" minimum:"0" description:"卡状态检查间隔(秒):正数按间隔(最小30秒),0 不检查且不再继承,不传或为空继承优先级更低的配置"` - PackageState *string `json:"package_state" validate:"omitempty,oneof=active depleted pending none" enum:"active,depleted,pending,none" description:"套餐状态匹配 (active:有生效套餐, depleted:套餐已用完, pending:只有待生效套餐, none:无套餐),为空不限"` - AssetStatus *int `json:"asset_status" validate:"omitempty,oneof=1 2 3 4" enum:"1,2,3,4" description:"资产状态匹配 (1:在库, 2:已销售, 3:已换货, 4:已停用),为空不限"` - PreResetWindow *int `json:"pre_reset_window" validate:"omitempty,min=60" minimum:"60" description:"重置日前加密窗口长度(秒),与窗口内流量间隔同时配置才生效"` - PreResetCarddataInterval *int `json:"pre_reset_carddata_interval" validate:"omitempty,min=60" minimum:"60" description:"重置日前窗口内的流量检查间隔(秒),与窗口长度同时配置才生效"` - ClearFields []string `json:"clear_fields" validate:"omitempty,dive,oneof=realname_check_interval carddata_check_interval package_check_interval card_status_check_interval carrier_id package_state asset_status pre_reset_window pre_reset_carddata_interval" description:"要清空为空值的字段(间隔清空即改为继承,匹配条件清空即不限):realname_check_interval/carddata_check_interval/package_check_interval/card_status_check_interval/carrier_id/package_state/asset_status/pre_reset_window/pre_reset_carddata_interval"` - Description *string `json:"description" validate:"omitempty,max=500" maxLength:"500" description:"配置说明"` + ConfigName *string `json:"config_name" validate:"omitempty,min=1,max=100" minLength:"1" maxLength:"100" description:"配置名称"` + CardCondition *string `json:"card_condition" validate:"omitempty,oneof=not_real_name real_name activated suspended" description:"卡状态条件 (not_real_name:未实名, real_name:已实名, activated:已激活, suspended:已停用)"` + CardCategory *string `json:"card_category" validate:"omitempty,oneof=normal industry" description:"卡业务类型 (normal:普通卡, industry:行业卡)"` + CarrierID *uint `json:"carrier_id" validate:"omitempty" description:"运营商ID(可选,精确匹配)"` + Priority *int `json:"priority" validate:"omitempty,min=1,max=1000" minimum:"1" maximum:"1000" description:"优先级(数字越小优先级越高)"` + RealnameCheckInterval *int `json:"realname_check_interval" validate:"omitempty,min=0" minimum:"0" description:"实名检查间隔(秒):正数按间隔(最小30秒),0 不检查且不再继承,不传或为空继承优先级更低的配置"` + CarddataCheckInterval *int `json:"carddata_check_interval" validate:"omitempty,min=0" minimum:"0" description:"流量检查间隔(秒):正数按间隔(最小60秒),0 不检查且不再继承,不传或为空继承优先级更低的配置"` + PackageCheckInterval *int `json:"package_check_interval" validate:"omitempty,min=0" minimum:"0" description:"套餐检查间隔(秒):正数按间隔(最小60秒),0 不检查且不再继承,不传或为空继承优先级更低的配置"` + ProtectCheckInterval *int `json:"protect_check_interval" validate:"omitempty,min=30" minimum:"30" description:"保护期一致性检查间隔(秒),NULL表示不检查,最小30秒"` + CardStatusCheckInterval *int `json:"card_status_check_interval" validate:"omitempty,min=0" minimum:"0" description:"卡状态检查间隔(秒):正数按间隔(最小30秒),0 不检查且不再继承,不传或为空继承优先级更低的配置"` + PackageState *string `json:"package_state" validate:"omitempty,oneof=active depleted pending none" enum:"active,depleted,pending,none" description:"套餐状态匹配 (active:有生效套餐, depleted:套餐已用完, pending:只有待生效套餐, none:无套餐),为空不限"` + AssetStatus *int `json:"asset_status" validate:"omitempty,oneof=1 2 3 4" enum:"1,2,3,4" description:"资产状态匹配 (1:在库, 2:已销售, 3:已换货, 4:已停用),为空不限"` + PreResetWindow *int `json:"pre_reset_window" validate:"omitempty,min=60" minimum:"60" description:"重置日前加密窗口长度(秒),与窗口内流量间隔同时配置才生效"` + PreResetCarddataInterval *int `json:"pre_reset_carddata_interval" validate:"omitempty,min=60" minimum:"60" description:"重置日前窗口内的流量检查间隔(秒),与窗口长度同时配置才生效"` + ClearFields []PollingConfigClearField `json:"clear_fields" validate:"omitempty,dive,oneof=realname_check_interval carddata_check_interval package_check_interval card_status_check_interval carrier_id package_state asset_status pre_reset_window pre_reset_carddata_interval" description:"要清空为空值的字段(间隔清空即改为继承,匹配条件清空即不限):realname_check_interval/carddata_check_interval/package_check_interval/card_status_check_interval/carrier_id/package_state/asset_status/pre_reset_window/pre_reset_carddata_interval"` + Description *string `json:"description" validate:"omitempty,max=500" maxLength:"500" description:"配置说明"` } // PollingConfigListRequest 轮询配置列表请求 @@ -101,20 +101,39 @@ type PollingConfigPageResult struct { // UpdatePollingConfigPrioritiesRequest 整体调整轮询配置优先级请求 type UpdatePollingConfigPrioritiesRequest struct { - OrderedIDs []uint `json:"ordered_ids" validate:"required,min=1,max=1000" required:"true" minItems:"1" description:"按期望生效顺序排列的配置 ID 列表(排在前面的优先级更高);系统按顺序把优先级重排为 1、2、3…,未列出的配置保持原优先级"` + OrderedIDs []uint `json:"ordered_ids" validate:"required,min=1,max=1000,dive,min=1" required:"true" minItems:"1" maxItems:"1000" description:"按期望生效顺序排列的配置 ID 列表(排在前面的优先级更高);ID 须为正整数且不重复;系统按顺序把优先级重排为 1、2、3…,未列出的配置保持原优先级"` } -// PollingConfigOverride 匹配预览中的未保存修改;ID 为 0 表示新增草稿,Disabled 为 true 表示假设该配置停用 +// PollingConfigOverride 匹配预览中的未保存修改;ID 为 0 表示新增草稿,Disabled 为 true 表示假设该配置停用。 +// 字段含义与创建轮询配置相同,均为可选;草稿名称为空时显示"未保存的配置"。 type PollingConfigOverride struct { - ID uint `json:"id" description:"要覆盖的配置 ID,0 表示新增一条草稿配置"` - Disabled bool `json:"disabled" description:"假设该配置停用(不参与匹配)"` - CreatePollingConfigRequest + ID uint `json:"id" description:"要覆盖的配置 ID,0 表示新增一条草稿配置"` + Disabled bool `json:"disabled" description:"假设该配置停用(不参与匹配),为 true 时其余字段忽略"` + ConfigName string `json:"config_name" maxLength:"100" description:"草稿配置名称"` + CardCondition string `json:"card_condition" enum:"not_real_name,activated,suspended" description:"卡状态条件 (not_real_name:未实名, activated:已激活, suspended:已停用),为空不限"` + CardCategory string `json:"card_category" enum:"normal,industry" description:"卡业务类型 (normal:普通卡, industry:行业卡),为空不限"` + CarrierID *uint `json:"carrier_id" description:"运营商账户ID,为空不限"` + Priority int `json:"priority" minimum:"0" description:"优先级(数字越小越优先;与已有配置相同时排在已有配置之后)"` + RealnameCheckInterval *int `json:"realname_check_interval" minimum:"0" description:"实名检查间隔(秒),正数按间隔、0 不检查、空继承"` + CarddataCheckInterval *int `json:"carddata_check_interval" minimum:"0" description:"流量检查间隔(秒),正数按间隔、0 不检查、空继承"` + PackageCheckInterval *int `json:"package_check_interval" minimum:"0" description:"套餐检查间隔(秒),正数按间隔、0 不检查、空继承"` + ProtectCheckInterval *int `json:"protect_check_interval" minimum:"0" description:"保护期检查间隔(秒),正数按间隔、0 不检查、空继承"` + CardStatusCheckInterval *int `json:"card_status_check_interval" minimum:"0" description:"卡状态检查间隔(秒),正数按间隔、0 不检查、空继承"` + PackageState *string `json:"package_state" enum:"active,depleted,pending,none" description:"套餐状态匹配 (active:有生效套餐, depleted:套餐已用完, pending:只有待生效套餐, none:无套餐),为空不限"` + AssetStatus *int `json:"asset_status" enum:"1,2,3,4" description:"资产状态匹配 (1:在库, 2:已销售, 3:已换货, 4:已停用),为空不限"` + PreResetWindow *int `json:"pre_reset_window" minimum:"60" description:"重置日前加密窗口长度(秒),与窗口内流量间隔同时配置才生效"` + PreResetCarddataInterval *int `json:"pre_reset_carddata_interval" minimum:"60" description:"重置日前窗口内的流量检查间隔(秒),与窗口长度同时配置才生效"` } -// PollingConfigMatchPreviewRequest 匹配预览请求 +// PollingConfigMatchPreviewQuery 按当前配置预览的查询参数 +type PollingConfigMatchPreviewQuery struct { + ICCID string `json:"-" query:"iccid" validate:"required,max=30" required:"true" maxLength:"30" description:"要预览的卡 ICCID"` +} + +// PollingConfigMatchPreviewRequest 带未保存修改的匹配预览请求(请求体) type PollingConfigMatchPreviewRequest struct { - ICCID string `json:"iccid" query:"iccid" validate:"required,max=30" required:"true" maxLength:"30" description:"要预览的卡 ICCID"` - Overrides []PollingConfigOverride `json:"overrides" description:"未保存的修改(仅 POST 支持),不写任何数据"` + ICCID string `json:"iccid" validate:"required,max=30" required:"true" maxLength:"30" description:"要预览的卡 ICCID"` + Overrides []PollingConfigOverride `json:"overrides" maxItems:"20" description:"未保存的修改,不写任何数据"` } // PollingCardMatchFacts 卡的匹配维度取值 @@ -218,3 +237,12 @@ type PollingPreResetCarrierStat struct { type PollingPreResetStatsResponse struct { Carriers []PollingPreResetCarrierStat `json:"carriers" description:"有开启轮询卡的运营商账户"` } + +// PollingConfigClearField 是更新轮询配置时可清空为空值的字段名。 +type PollingConfigClearField string + +// Enum 返回可清空字段的全部取值,供接口文档生成数组元素枚举。 +func (PollingConfigClearField) Enum() []interface{} { + return []interface{}{"realname_check_interval", "carddata_check_interval", "package_check_interval", "card_status_check_interval", + "carrier_id", "package_state", "asset_status", "pre_reset_window", "pre_reset_carddata_interval"} +} diff --git a/internal/routes/polling_config.go b/internal/routes/polling_config.go index ddd0405d..b41e3857 100644 --- a/internal/routes/polling_config.go +++ b/internal/routes/polling_config.go @@ -48,7 +48,7 @@ func registerPollingConfigRoutes(router fiber.Router, handler *admin.PollingConf Register(configs, doc, groupPath, "GET", "/match-preview", handler.MatchPreview, RouteSpec{ Summary: "预览卡的轮询配置匹配结果(按当前配置)", Tags: []string{"轮询配置管理"}, - Input: new(dto.PollingConfigMatchPreviewRequest), + Input: new(dto.PollingConfigMatchPreviewQuery), Output: new(dto.PollingConfigMatchPreviewResponse), Auth: true, }) diff --git a/internal/service/polling/config_validation.go b/internal/service/polling/config_validation.go index 3c9ef08b..cc1e97b1 100644 --- a/internal/service/polling/config_validation.go +++ b/internal/service/polling/config_validation.go @@ -3,6 +3,7 @@ package polling import ( "context" "fmt" + "github.com/break/junhong_cmp_fiber/internal/model/dto" "github.com/break/junhong_cmp_fiber/internal/model" "github.com/break/junhong_cmp_fiber/pkg/constants" @@ -74,9 +75,9 @@ func validPackageState(state string) bool { } // applyPollingConfigClearFields 把指定字段清空为 NULL:间隔清空即改为继承,匹配条件清空即不限。 -func applyPollingConfigClearFields(cfg *model.PollingConfig, fields []string) error { +func applyPollingConfigClearFields(cfg *model.PollingConfig, fields []dto.PollingConfigClearField) error { for _, field := range fields { - switch field { + switch string(field) { case "realname_check_interval": cfg.RealnameCheckInterval = nil case "carddata_check_interval": @@ -96,7 +97,7 @@ func applyPollingConfigClearFields(cfg *model.PollingConfig, fields []string) er case "pre_reset_carddata_interval": cfg.PreResetCarddataInterval = nil default: - return errors.New(errors.CodeInvalidParam, "轮询配置参数不合法:不支持清空字段 "+field) + return errors.New(errors.CodeInvalidParam, "轮询配置参数不合法:不支持清空字段 "+string(field)) } } return nil diff --git a/openspec/changes/add-tiered-polling-and-pre-activation-settlement/tasks.md b/openspec/changes/add-tiered-polling-and-pre-activation-settlement/tasks.md index 957b8f53..76cfdb12 100644 --- a/openspec/changes/add-tiered-polling-and-pre-activation-settlement/tasks.md +++ b/openspec/changes/add-tiered-polling-and-pre-activation-settlement/tasks.md @@ -23,7 +23,7 @@ - [x] 4.1 `/polling-configs` DTO 增加 4 个字段,间隔校验改为"0 或不小于最小间隔",说明文字按三态;保存校验无套餐已销售与在库层卡状态必须为正数;优先级重复且条件可能重叠时返回提示。 - [x] 4.2 `PUT /polling-configs/priorities`:事务内整体重排,写统一审计,发配置变更通知。 -- [x] 4.3 `GET /polling-configs/match-preview?iccid=`(可带 `config_overrides`):返回维度取值、命中配置、每类最终间隔与来源;只读。 +- [x] 4.3 `GET /polling-configs/match-preview?iccid=`;`POST /polling-configs/match-preview` 请求体带 `overrides`(未保存修改):返回维度取值、命中配置、每类最终间隔与来源;只读。 - [x] 4.4 `POST /polling-configs/recalculate-due`(可选运营商账户)与 `GET /polling-configs/recalculate-due/:task_id`:异步分批,只提前不推后,不调网关。 - [x] 4.5 `GET /polling-stats/tiers`、`GET /polling-stats/pre-reset`。 - [x] 4.6 同步路由、`cmd/api/docs.go`、`cmd/gendocs/main.go` 并重新生成接口文档。