feat(收口): 补齐 8 月迭代缺口并同步 Spec 与证据链
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:
2026-09-18 15:34:29 +08:00
parent 5e78809b93
commit 5ed6b39deb
142 changed files with 7878 additions and 964 deletions

View File

@@ -14,26 +14,26 @@ import (
"github.com/break/junhong_cmp_fiber/pkg/response"
)
// PriorityPollingHandler 处理卡轮询优先队列的人工入与只读查询请求。
// PriorityPollingHandler 处理卡轮询优先队列的人工入口(入队、关闭、重触发)与只读查询请求。
type PriorityPollingHandler struct {
enqueue *pollingSvc.PriorityEnqueueService
manual *pollingSvc.PriorityManualService
query *prioritypollingquery.Query
validator *validator.Validate
}
// NewPriorityPollingHandler 创建卡轮询优先队列处理器。
func NewPriorityPollingHandler(
enqueue *pollingSvc.PriorityEnqueueService,
manual *pollingSvc.PriorityManualService,
query *prioritypollingquery.Query,
validator *validator.Validate,
) *PriorityPollingHandler {
return &PriorityPollingHandler{enqueue: enqueue, query: query, validator: validator}
return &PriorityPollingHandler{manual: manual, query: query, validator: validator}
}
// Enqueue 人工优先入队。
// POST /api/admin/polling-priority-items
func (h *PriorityPollingHandler) Enqueue(c *fiber.Ctx) error {
if h.enqueue == nil {
if h.manual == nil {
return errors.New(errors.CodeInternalError, "人工优先入队能力未配置")
}
var req dto.EnqueuePriorityPollingRequest
@@ -43,7 +43,7 @@ func (h *PriorityPollingHandler) Enqueue(c *fiber.Ctx) error {
if err := h.validator.Struct(&req); err != nil {
return errors.New(errors.CodeInvalidParam, validation.Message("人工优先入队参数不合法", &req, err))
}
result, err := h.enqueue.Enqueue(c.UserContext(), req.CardID, req.Reason, middleware.GetUserIDFromContext(c.UserContext()))
result, err := h.manual.Enqueue(c.UserContext(), req.CardID, req.Reason, middleware.GetUserIDFromContext(c.UserContext()))
if err != nil {
return err
}
@@ -74,6 +74,84 @@ func (h *PriorityPollingHandler) Enqueue(c *fiber.Ctx) error {
})
}
// Close 人工关闭未完成的优先轮询项。
// 只允许超级管理员与平台账号;原因必填并写入审计,关闭后不再发起上游调用且保留既有执行结果与失败原因。
// POST /api/admin/polling-priority-items/:id/close
func (h *PriorityPollingHandler) Close(c *fiber.Ctx) error {
if h.manual == nil {
return errors.New(errors.CodeInternalError, "人工关闭优先轮询项能力未配置")
}
id, err := pathID(c)
if err != nil {
return err
}
var req dto.ClosePriorityPollingItemRequest
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))
}
result, err := h.manual.Close(c.UserContext(), id, req.Reason, middleware.GetUserIDFromContext(c.UserContext()))
if err != nil {
return err
}
return response.Success(c, dto.ClosePriorityPollingItemResponse{
ItemID: result.ItemID,
CardID: result.CardID,
TaskType: result.TaskType,
TaskTypeName: constants.PollingPriorityTaskTypeName(result.TaskType),
Status: result.Status,
StatusName: constants.PollingPriorityStatusName(result.Status),
DequeuedAt: result.DequeuedAt,
Reason: result.Reason,
})
}
// Retrigger 人工重触发失败出队或已过期的优先轮询项。
// 复用人工入队入口为该卡该任务类型建立新的活动项(尝试次数从零开始),原因必填并写入审计;
// 权限与人工入队一致(含数据范围内的代理账号),不绕过活动项唯一键代表的并发上限。
// POST /api/admin/polling-priority-items/:id/retrigger
func (h *PriorityPollingHandler) Retrigger(c *fiber.Ctx) error {
if h.manual == nil {
return errors.New(errors.CodeInternalError, "人工重触发优先轮询项能力未配置")
}
id, err := pathID(c)
if err != nil {
return err
}
var req dto.RetriggerPriorityPollingItemRequest
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))
}
result, err := h.manual.Retrigger(c.UserContext(), id, req.Reason, middleware.GetUserIDFromContext(c.UserContext()))
if err != nil {
return err
}
view := dto.RetriggerPriorityPollingItemResponse{
SourceItemID: result.SourceItemID,
CardID: result.Enqueue.CardID,
Reason: result.Reason,
}
if len(result.Enqueue.Items) > 0 {
// 重触发只针对来源项的任务类型,因此结果里至多一项;防御性取首项。
item := result.Enqueue.Items[0]
view.TaskType = item.TaskType
view.TaskTypeName = constants.PollingPriorityTaskTypeName(item.TaskType)
view.ItemID = item.ItemID
view.Status = item.Status
view.StatusName = constants.PollingPriorityStatusName(item.Status)
view.AttemptCount = item.AttemptCount
view.TriggerCount = item.TriggerCount
view.LastTriggeredAt = item.LastTriggeredAt
view.Created = item.Created
}
return response.Success(c, view)
}
// ListPriorityItems 分页查询优先轮询项。
// GET /api/admin/polling-priority-items
func (h *PriorityPollingHandler) ListPriorityItems(c *fiber.Ctx) error {