feat(资产钱包自动续费): 新增全局配置、每日扫描续购与可靠复机
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 10m15s

- 新增单行配置表 tb_asset_auto_renewal_config 与尝试记录表 tb_asset_auto_renewal_attempt(迁移 000229/000230)
- 每日按上海自然日扫描,窗口内以同一资产钱包可用余额续购当前主套餐,资金/订单/套餐/审计同一事务闭合
- 唯一键保证每资产每日至多一次尝试,占位中断由后续扫描收敛,当日不重试
- 四类失败原因向客户与店铺各投递每日至多一条站内通知,并注册通知类型与个人客户白名单
- 续费成功后按条件经 Outbox 可靠投递复机,新增恢复扫描只查询回填,不使用即发即弃调用
- 配置读写仅超级管理员与平台账号,保存记录操作者、前后值快照并登记统一审计
- tasks 7.1–7.15 全部验证通过(本机隔离 PostgreSQL/Redis,零外部渠道调用)
This commit is contained in:
2026-09-17 16:39:26 +08:00
parent 70e6b186df
commit d52be16802
54 changed files with 5164 additions and 97 deletions

View File

@@ -0,0 +1,82 @@
package admin
import (
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
assetAutoRenewalApp "github.com/break/junhong_cmp_fiber/internal/application/assetautorenewal"
"github.com/break/junhong_cmp_fiber/internal/handler/validation"
"github.com/break/junhong_cmp_fiber/internal/model/dto"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/errors"
"github.com/break/junhong_cmp_fiber/pkg/response"
)
// AssetAutoRenewalConfigHandler 资产钱包自动续费配置 Handler。
//
// 路由组已有「仅超级管理员与平台账号」门禁应用层仍会复核账号类型ENG-AUTHZ-001
// 因此代理、企业与个人客户即使绕过路由也无任何入口。
type AssetAutoRenewalConfigHandler struct {
service *assetAutoRenewalApp.Service
validator *validator.Validate
}
// NewAssetAutoRenewalConfigHandler 创建资产钱包自动续费配置 Handler。
func NewAssetAutoRenewalConfigHandler(service *assetAutoRenewalApp.Service, validator *validator.Validate) *AssetAutoRenewalConfigHandler {
return &AssetAutoRenewalConfigHandler{service: service, validator: validator}
}
// GetConfig 读取资产钱包自动续费配置。
// GET /api/admin/asset-auto-renewal-config
func (h *AssetAutoRenewalConfigHandler) GetConfig(c *fiber.Ctx) error {
config, err := h.service.GetConfig(c.UserContext())
if err != nil {
return err
}
return response.Success(c, toAssetAutoRenewalConfigResponse(config))
}
// UpdateConfig 保存资产钱包自动续费配置。
// PUT /api/admin/asset-auto-renewal-config
func (h *AssetAutoRenewalConfigHandler) UpdateConfig(c *fiber.Ctx) error {
var req dto.UpdateAssetAutoRenewalConfigRequest
if err := c.BodyParser(&req); err != nil {
return errors.New(errors.CodeInvalidParam, "请求参数格式不正确")
}
if err := h.validator.Struct(&req); err != nil {
return errors.New(errors.CodeInvalidParam, validation.Message("保存自动续费配置参数不合法", &req, err))
}
config, err := h.service.SaveConfig(c.UserContext(), assetAutoRenewalApp.ConfigRequest{
Enabled: req.Enabled,
Scope: req.Scope,
PackageIDs: req.PackageIDs,
DaysBeforeExpiry: req.DaysBeforeExpiry,
})
if err != nil {
return err
}
return response.Success(c, toAssetAutoRenewalConfigResponse(config))
}
// toAssetAutoRenewalConfigResponse 组装配置响应枚举附加中文名称字段ENG-DTO-001
func toAssetAutoRenewalConfigResponse(config *assetAutoRenewalApp.ConfigView) dto.AssetAutoRenewalConfigResponse {
packageIDs := config.PackageIDs
if packageIDs == nil {
packageIDs = []uint{}
}
enabledName := "关闭"
if config.Enabled == constants.AssetAutoRenewalConfigEnabledOn {
enabledName = "开启"
}
return dto.AssetAutoRenewalConfigResponse{
Enabled: config.Enabled,
Scope: config.Scope,
PackageIDs: packageIDs,
DaysBeforeExpiry: config.DaysBeforeExpiry,
ConfigVersion: config.ConfigVersion,
ScopeName: constants.GetAssetAutoRenewalScopeName(config.Scope),
EnabledName: enabledName,
Updater: config.Updater,
UpdatedAt: config.UpdatedAt.Format("2006-01-02 15:04:05"),
}
}