Files
junhong_cmp_fiber/internal/handler/admin/polling_priority.go
break 5ed6b39deb
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
feat(收口): 补齐 8 月迭代缺口并同步 Spec 与证据链
- 新增六对成对迁移 000232–000237:H5 弹窗类型、退款结算标识与申请人备注、优先轮询事实字段与两个新终态、通道阈值命中留痕、手机号最近解绑人、提现资格校验留痕
- 退款:原因必填与申请人备注、来源支付与渠道流水冻结、线下处理流水号补录审计、按订单查询可选退款方式、企微审批材料补齐且新增字段缺失映射即明确失败
- 优先轮询:人工关闭、有效期到期独立周期任务、失败与过期人工重触发、事实字段与异常重试查询、资产解析端点只读投影
- 通道阈值:命中事实同事务留痕与命中记录查询;员工账单:列表筛选与详情投影;商户池:列表投影与统计周期语义;H5:弹窗类型与类别排序
- 手机号:有效关联数量与最近解绑人、短信验证码失败次数限制;导出:佣金明细十五列与报表序号列
- 时间筛选:三处新增筛选纳入统一严格解析契约,员工账单产生时间参数改名
- 同步 12 份主 Spec 需求、两端点与异步任务证据链,门禁 context-health 与 OpenSpec 校验通过
2026-09-18 15:34:29 +08:00

191 lines
7.3 KiB
Go

package admin
import (
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"github.com/break/junhong_cmp_fiber/internal/handler/validation"
"github.com/break/junhong_cmp_fiber/internal/model/dto"
prioritypollingquery "github.com/break/junhong_cmp_fiber/internal/query/prioritypolling"
pollingSvc "github.com/break/junhong_cmp_fiber/internal/service/polling"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/errors"
"github.com/break/junhong_cmp_fiber/pkg/middleware"
"github.com/break/junhong_cmp_fiber/pkg/response"
)
// PriorityPollingHandler 处理卡轮询优先队列的人工入口(入队、关闭、重触发)与只读查询请求。
type PriorityPollingHandler struct {
manual *pollingSvc.PriorityManualService
query *prioritypollingquery.Query
validator *validator.Validate
}
// NewPriorityPollingHandler 创建卡轮询优先队列处理器。
func NewPriorityPollingHandler(
manual *pollingSvc.PriorityManualService,
query *prioritypollingquery.Query,
validator *validator.Validate,
) *PriorityPollingHandler {
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.manual == nil {
return errors.New(errors.CodeInternalError, "人工优先入队能力未配置")
}
var req dto.EnqueuePriorityPollingRequest
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.Enqueue(c.UserContext(), req.CardID, req.Reason, middleware.GetUserIDFromContext(c.UserContext()))
if err != nil {
return err
}
taskTypeNames := make([]string, 0, len(result.TaskTypes))
items := make([]dto.EnqueuePriorityPollingItemView, 0, len(result.Items))
for _, taskType := range result.TaskTypes {
taskTypeNames = append(taskTypeNames, constants.PollingPriorityTaskTypeName(taskType))
}
for _, item := range result.Items {
items = append(items, dto.EnqueuePriorityPollingItemView{
ItemID: item.ItemID,
TaskType: item.TaskType,
TaskTypeName: constants.PollingPriorityTaskTypeName(item.TaskType),
Status: item.Status,
StatusName: constants.PollingPriorityStatusName(item.Status),
TriggerCount: item.TriggerCount,
LastTriggeredAt: item.LastTriggeredAt,
Created: item.Created,
})
}
return response.Success(c, dto.EnqueuePriorityPollingResponse{
CardID: result.CardID,
TaskTypes: result.TaskTypes,
TaskTypeNames: taskTypeNames,
CreatedCount: result.CreatedCount,
MergedCount: result.MergedCount,
Items: items,
})
}
// 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 {
if h.query == nil {
return errors.New(errors.CodeInternalError, "优先轮询项查询能力未配置")
}
var req dto.PriorityPollingItemListRequest
if err := c.QueryParser(&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.query.List(c.UserContext(), req)
if err != nil {
return err
}
return response.SuccessWithPagination(c, result.List, result.Total, result.Page, result.PageSize)
}
// GetPriorityItem 查询优先轮询项详情。
// GET /api/admin/polling-priority-items/:id
func (h *PriorityPollingHandler) GetPriorityItem(c *fiber.Ctx) error {
if h.query == nil {
return errors.New(errors.CodeInternalError, "优先轮询项查询能力未配置")
}
id, err := pathID(c)
if err != nil {
return err
}
result, err := h.query.Detail(c.UserContext(), id)
if err != nil {
return err
}
return response.Success(c, result)
}