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:
122
internal/query/asset/priority_polling.go
Normal file
122
internal/query/asset/priority_polling.go
Normal file
@@ -0,0 +1,122 @@
|
||||
package asset
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
pollingsvc "github.com/break/junhong_cmp_fiber/internal/service/polling"
|
||||
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
)
|
||||
|
||||
// ResolvePriorityPolling 读取资产或卡的优先轮询状态只读投影,落在既有资产解析端点内。
|
||||
//
|
||||
// 口径:卡取自身,设备取其在用绑定卡;存在待执行或执行中的优先项时「是否处于优先轮询中」为真,
|
||||
// 最近触发场景取该资产最近一条事实的触发类型,最近轮询结果取该条事实的结局,
|
||||
// 最近轮询时间取最近一次执行时间(未执行过时退回最近触发时间),
|
||||
// 失败原因取最近一条非空的可安全展示原因。无任何优先轮询事实时以否或空值返回,
|
||||
// MUST NOT 用普通轮询结果填充。
|
||||
//
|
||||
// 只读:只做查询,不创建、不合并、不推进、不出队任何优先项,也不触发上游调用。
|
||||
// 数据范围:资产本身的可读性由资产解析用例保证;此处再按优先项事实的店铺快照下推一次,
|
||||
// 读侧鉴权不通过(企业账号或范围缺失)时返回空投影,不因投影阻断资产详情,也不泄露范围外事实。
|
||||
func (q *ExchangeTraceQuery) ResolvePriorityPolling(ctx context.Context, assetType string, assetID uint) (*dto.AssetPriorityPollingProjection, error) {
|
||||
projection := &dto.AssetPriorityPollingProjection{}
|
||||
if q == nil || q.db == nil || assetID == 0 {
|
||||
return projection, nil
|
||||
}
|
||||
cardIDs, err := q.assetCardIDs(ctx, assetType, assetID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(cardIDs) == 0 {
|
||||
return projection, nil
|
||||
}
|
||||
scope, scopeErr := pollingsvc.PriorityPollingReadScope(ctx)
|
||||
if scopeErr != nil {
|
||||
return projection, nil
|
||||
}
|
||||
summaries, err := postgres.NewPollingPriorityItemStore(q.db).ProjectPriorityPolling(ctx, cardIDs, scope)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return projectPriorityPolling(summaries), nil
|
||||
}
|
||||
|
||||
// assetCardIDs 返回该资产对应的轮询对象卡集合:卡为自身,设备为绑定状态有效的在用卡。
|
||||
func (q *ExchangeTraceQuery) assetCardIDs(ctx context.Context, assetType string, assetID uint) ([]uint, error) {
|
||||
switch assetType {
|
||||
case constants.AssetResolveTypeCard:
|
||||
return []uint{assetID}, nil
|
||||
case constants.AssetTypeDevice:
|
||||
var cardIDs []uint
|
||||
if err := q.db.WithContext(ctx).
|
||||
Model(&model.DeviceSimBinding{}).
|
||||
Where("device_id = ? AND bind_status = ?", assetID, constants.BindStatusBound).
|
||||
Order("slot_position ASC").
|
||||
Pluck("iot_card_id", &cardIDs).Error; err != nil {
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询设备绑定的卡失败")
|
||||
}
|
||||
return cardIDs, nil
|
||||
default:
|
||||
// 其他资产类型没有轮询对象,投影按无事实返回。
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
// projectPriorityPolling 把逐卡摘要聚合为资产级投影。
|
||||
// 多卡资产以最近创建的一条事实决定最近触发场景、结果与轮询时间;
|
||||
// 失败原因单独取最近一条非空原因,避免最近一条事实无原因时丢失可展示的失败说明。
|
||||
func projectPriorityPolling(summaries map[uint]postgres.PriorityPollingCardProjection) *dto.AssetPriorityPollingProjection {
|
||||
projection := &dto.AssetPriorityPollingProjection{}
|
||||
var latest *model.PollingPriorityItem
|
||||
var latestFailed *model.PollingPriorityItem
|
||||
for _, summary := range summaries {
|
||||
if summary.HasActive {
|
||||
projection.InPriorityPolling = true
|
||||
}
|
||||
item := summary.Latest
|
||||
if item == nil {
|
||||
continue
|
||||
}
|
||||
if latest == nil || item.ID > latest.ID {
|
||||
latest = item
|
||||
}
|
||||
if item.FailureReason != "" && (latestFailed == nil || item.ID > latestFailed.ID) {
|
||||
latestFailed = item
|
||||
}
|
||||
}
|
||||
if latest == nil {
|
||||
return projection
|
||||
}
|
||||
projection.LastTriggerType = latest.TriggerType
|
||||
projection.LastTriggerName = constants.PollingPriorityTriggerName(latest.TriggerType)
|
||||
projection.LastResult = constants.PollingPriorityStatusOutcome(latest.Status)
|
||||
projection.LastResultName = constants.PollingPriorityOutcomeName(projection.LastResult)
|
||||
projection.LastPollingAt = priorityPollingLastPolledAt(latest)
|
||||
if latest.FailureReason != "" {
|
||||
projection.FailureReason = latest.FailureReason
|
||||
} else if latestFailed != nil {
|
||||
projection.FailureReason = latestFailed.FailureReason
|
||||
}
|
||||
return projection
|
||||
}
|
||||
|
||||
// priorityPollingLastPolledAt 返回最近轮询时间:优先取执行时间,未执行过时取最近触发时间。
|
||||
func priorityPollingLastPolledAt(item *model.PollingPriorityItem) *time.Time {
|
||||
if item == nil {
|
||||
return nil
|
||||
}
|
||||
switch {
|
||||
case item.FinishedAt != nil:
|
||||
return item.FinishedAt
|
||||
case item.StartedAt != nil:
|
||||
return item.StartedAt
|
||||
default:
|
||||
triggeredAt := item.LastTriggeredAt
|
||||
return &triggeredAt
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user