feat(收口): 补齐 8 月迭代缺口并同步 Spec 与证据链
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
- 新增六对成对迁移 000232–000237:H5 弹窗类型、退款结算标识与申请人备注、优先轮询事实字段与两个新终态、通道阈值命中留痕、手机号最近解绑人、提现资格校验留痕 - 退款:原因必填与申请人备注、来源支付与渠道流水冻结、线下处理流水号补录审计、按订单查询可选退款方式、企微审批材料补齐且新增字段缺失映射即明确失败 - 优先轮询:人工关闭、有效期到期独立周期任务、失败与过期人工重触发、事实字段与异常重试查询、资产解析端点只读投影 - 通道阈值:命中事实同事务留痕与命中记录查询;员工账单:列表筛选与详情投影;商户池:列表投影与统计周期语义;H5:弹窗类型与类别排序 - 手机号:有效关联数量与最近解绑人、短信验证码失败次数限制;导出:佣金明细十五列与报表序号列 - 时间筛选:三处新增筛选纳入统一严格解析契约,员工账单产生时间参数改名 - 同步 12 份主 Spec 需求、两端点与异步任务证据链,门禁 context-health 与 OpenSpec 校验通过
This commit is contained in:
@@ -44,6 +44,7 @@ func NewConfigurationService(db *gorm.DB, audit *audit.Writer) *ConfigurationSer
|
||||
type configurationInput struct {
|
||||
Title string
|
||||
Content string
|
||||
PopupType string
|
||||
Pages []string
|
||||
ShopIDs []uint
|
||||
DeviceTypes []string
|
||||
@@ -69,7 +70,12 @@ func (s *ConfigurationService) Create(ctx context.Context, request dto.CreateH5P
|
||||
if request.Enabled != nil && *request.Enabled {
|
||||
enabled = constants.H5PopupStatusEnabled
|
||||
}
|
||||
priority := 0
|
||||
// 弹窗类型必填:它既参与类别排序,也决定缺省优先级的取值来源。
|
||||
popupType := strings.TrimSpace(request.PopupType)
|
||||
if !constants.IsH5PopupType(popupType) {
|
||||
return 0, errors.New(errors.CodeInvalidParam, "运营弹窗类型必须为 promotion(套餐政策推广)或 announcement(通用公告)")
|
||||
}
|
||||
priority := constants.H5PopupTypeDefaultPriority(popupType)
|
||||
if request.Priority != nil {
|
||||
priority = *request.Priority
|
||||
}
|
||||
@@ -78,7 +84,7 @@ func (s *ConfigurationService) Create(ctx context.Context, request dto.CreateH5P
|
||||
actionType = *request.ActionType
|
||||
}
|
||||
normalized, err := normalizeConfigurationInput(configurationInput{
|
||||
Title: request.Title, Content: request.Content, Pages: request.Pages,
|
||||
Title: request.Title, Content: request.Content, PopupType: popupType, Pages: request.Pages,
|
||||
ShopIDs: request.ShopIDs, DeviceTypes: request.DeviceTypes, CardTypes: request.CardTypes,
|
||||
Priority: priority, Frequency: request.Frequency, ActionType: actionType,
|
||||
Enabled: enabled, StartsAt: request.StartsAt, EndsAt: request.EndsAt,
|
||||
@@ -88,7 +94,7 @@ func (s *ConfigurationService) Create(ctx context.Context, request dto.CreateH5P
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
record := &model.H5PopupConfiguration{
|
||||
Title: normalized.Title, Content: normalized.Content,
|
||||
Title: normalized.Title, Content: normalized.Content, PopupType: normalized.PopupType,
|
||||
Pages: model.StringJSONBArray(normalized.Pages), ShopIDs: toJSONBStrings(normalized.ShopIDs),
|
||||
DeviceTypes: model.StringJSONBArray(normalized.DeviceTypes), CardTypes: model.StringJSONBArray(normalized.CardTypes),
|
||||
Priority: normalized.Priority, Frequency: normalized.Frequency, ActionType: normalized.ActionType,
|
||||
@@ -136,7 +142,7 @@ func (s *ConfigurationService) Update(ctx context.Context, id uint, request dto.
|
||||
beforeData := configurationAuditSnapshot(&before)
|
||||
|
||||
merged := configurationInput{
|
||||
Title: record.Title, Content: record.Content, Pages: storePages(record),
|
||||
Title: record.Title, Content: record.Content, PopupType: record.PopupType, Pages: storePages(record),
|
||||
ShopIDs: storeShopIDs(record), DeviceTypes: storeDeviceTypes(record), CardTypes: storeCardTypes(record),
|
||||
Priority: record.Priority, Frequency: record.Frequency, ActionType: record.ActionType,
|
||||
Enabled: record.Enabled, StartsAt: record.StartsAt, EndsAt: record.EndsAt,
|
||||
@@ -147,6 +153,9 @@ func (s *ConfigurationService) Update(ctx context.Context, id uint, request dto.
|
||||
if request.Content != nil {
|
||||
merged.Content = *request.Content
|
||||
}
|
||||
if request.PopupType != nil {
|
||||
merged.PopupType = strings.TrimSpace(*request.PopupType)
|
||||
}
|
||||
if request.Pages != nil {
|
||||
merged.Pages = *request.Pages
|
||||
}
|
||||
@@ -184,6 +193,7 @@ func (s *ConfigurationService) Update(ctx context.Context, id uint, request dto.
|
||||
now := time.Now().UTC()
|
||||
record.Title = normalized.Title
|
||||
record.Content = normalized.Content
|
||||
record.PopupType = normalized.PopupType
|
||||
record.Pages = model.StringJSONBArray(normalized.Pages)
|
||||
record.ShopIDs = toJSONBStrings(normalized.ShopIDs)
|
||||
record.DeviceTypes = model.StringJSONBArray(normalized.DeviceTypes)
|
||||
@@ -311,6 +321,11 @@ func normalizeConfigurationInput(input configurationInput) (configurationInput,
|
||||
if popupURLPattern.MatchString(normalized.Content) || popupRoutePattern.MatchString(normalized.Content) {
|
||||
return configurationInput{}, errors.New(errors.CodeInvalidParam, "运营弹窗正文不接受 URL 或前端路由,只能使用受控动作")
|
||||
}
|
||||
// 类型必填且必须落在受控取值域内:它是候选排序的第一顺位类别键,非法值会让配置落进未定义类别。
|
||||
normalized.PopupType = strings.TrimSpace(input.PopupType)
|
||||
if !constants.IsH5PopupType(normalized.PopupType) {
|
||||
return configurationInput{}, errors.New(errors.CodeInvalidParam, "运营弹窗类型必须为 promotion(套餐政策推广)或 announcement(通用公告)")
|
||||
}
|
||||
normalized.Pages = dedupeStrings(input.Pages)
|
||||
if len(normalized.Pages) == 0 {
|
||||
return configurationInput{}, errors.New(errors.CodeInvalidParam, "运营弹窗至少需要一个命中页面")
|
||||
@@ -471,16 +486,16 @@ func configurationAuditResourceID(id uint) *string {
|
||||
// configurationAuditIdentity 生成配置身份快照,不含正文内容。
|
||||
func configurationAuditIdentity(record *model.H5PopupConfiguration) map[string]any {
|
||||
return map[string]any{
|
||||
"id": record.ID, "title": record.Title, "pages": storePages(record),
|
||||
"id": record.ID, "title": record.Title, "popup_type": record.PopupType, "pages": storePages(record),
|
||||
"priority": record.Priority, "frequency": record.Frequency, "action_type": record.ActionType,
|
||||
"enabled": record.Enabled, "version": record.Version,
|
||||
}
|
||||
}
|
||||
|
||||
// configurationAuditSnapshot 生成配置审计前后值快照,覆盖范围、优先级、频率、受控动作、启停、有效期与版本。
|
||||
// configurationAuditSnapshot 生成配置审计前后值快照,覆盖类型、范围、优先级、频率、受控动作、启停、有效期与版本。
|
||||
func configurationAuditSnapshot(record *model.H5PopupConfiguration) map[string]any {
|
||||
return map[string]any{
|
||||
"id": record.ID, "title": record.Title, "content": record.Content,
|
||||
"id": record.ID, "title": record.Title, "content": record.Content, "popup_type": record.PopupType,
|
||||
"pages": storePages(record), "shop_ids": storeShopIDs(record),
|
||||
"device_types": storeDeviceTypes(record), "card_types": storeCardTypes(record),
|
||||
"priority": record.Priority, "frequency": record.Frequency, "action_type": record.ActionType,
|
||||
|
||||
Reference in New Issue
Block a user