All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 14m13s
新增 000228 成对迁移 tb_polling_priority_item:卡、任务类型、状态、触发类型、来源订单/套餐使用记录、
触发次数与来源集合、尝试次数、失败原因、人工原因与操作者、店铺快照与各时间列;以活动项部分唯一索引
uq_polling_priority_item_active(仅 deleted_at IS NULL AND status IN ('pending','processing') 占键位)
表达「同卡同任务类型至多一条活动项」,另有状态/时间索引与全列注释;down 守卫在存在活动项或未终态行时
拒绝回滚并给出中文原因。
新增优先轮询请求可靠事件 polling.priority.requested(载荷版本 v1、事件键前缀 prio:)与消费者:只在原
业务事务内追加、幂等键稳定;消费者按卡 × 纳入任务类型(realname/carddata/card_status/package)逐条
建项并在提交后下发执行提示,重复投递只合并触发次数、来源集合与最近触发时间,不新建行也不重复调用。
触发点为四类自动场景 purchase_activated / renewal_activated(按同载体更早套餐使用记录判定)/
queue_activated / addon_activated 与「无有效套餐」no_valid_package(仅在普通套餐轮询来源且存在待生效
套餐使用记录时追加;事件通道显式拒绝 manual_trigger);入队对象恒为卡,绑定设备资产在触发事务内冻结
在用卡快照逐卡建项,不使用设备当前卡槽口径。
轮询共享基类新增认领接缝:四个 Handler(realname/carddata/card_status/package)在并发信号量之后、调用
上游之前探测活动项——待执行条件认领、执行中且 90 秒租约未到期则跳过并延后、无活动项时行为与既有完全
等价;超租约允许相邻执行接管,尝试次数只在真正发起执行后累加,未达上限(3)回到活动态按既有间隔重排,
达上限或业务校验类失败进入失败终态并保留可安全展示原因;执行前校验卡自身与绑定设备的轮询开关。未引入
通用卡级锁与 Redis 活动标记,分片队列的出队、入队与移除路径未改动。
提示通道按任务类型独立键(polling:priority:{taskType}),与既有手动触发队列分离;调度器在同一周期内先
排空优先提示、再排空手动触发队列,提示排空不受分片背压跳过影响;未新建调度设施或异步任务类型。
新增人工优先入队与只读查询三条路由 POST /api/admin/polling-priority-items、
GET /api/admin/polling-priority-items、GET /api/admin/polling-priority-items/:id:人工入队复用既有轮询
权限判定(抽取为同包共享函数),原因必填,不受每日 500 次上限与 24 小时去重约束,重复抑制由活动项合并
承担;读侧按店铺快照下推数据范围,越权与不存在不可区分,不提供优先级分级、有效期或人工重触发入口。
新增 7 个审计动作(enqueue/claim/fail/retry/complete/dequeue/manual_denied)与资源
polling_priority_item,并按(操作者类型,来源)注册,人工侧与 Worker 侧均通过来源校验。
同步 OpenAPI 文档装配三处与路由注册;归档 Change 至
openspec/changes/archive/2026-09-17-add-priority-polling-queue/ 并同步主 Spec(新增
priority-polling-queue、polling-operations 追加单次执行互斥 Requirement 与三条路由索引)与上下文健康
证据(requirement-evidence 150 行、入口矩阵 http 403 / async 56)。
本机验证:junhong_cmp_test 与隔离 Redis DB 15,未连生产、未启动 Worker/API、未调用运营商上游;迁移
up/down/up 与 down 守卫实测(含 dirty=true 记账口径与 force 恢复),A–F 批 94 PASS、接缝 63 PASS、
提示通道 12 PASS、清理零残留 20 PASS。成功路径 Complete、真并发互斥、尝试上限第 3 次判定、HTTP 层权限
矩阵、通道阈值持锁复机边界与三类生效触发点生产集成留待测试部署验证(见
docs/verification/add-priority-polling-queue-verification.md 第 4 节)。自动化测试按项目决策为 N/A,
未新增 *_test.go。
192 lines
15 KiB
Go
192 lines
15 KiB
Go
package model
|
||
|
||
import (
|
||
"time"
|
||
|
||
"gorm.io/gorm"
|
||
)
|
||
|
||
// PollingConfig 轮询配置表
|
||
type PollingConfig struct {
|
||
ID uint `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
||
ConfigName string `gorm:"column:config_name;type:varchar(100);not null;comment:配置名称" json:"config_name"`
|
||
CardCondition string `gorm:"column:card_condition;type:varchar(50);comment:卡状态条件:not_real_name/real_name/activated/suspended" json:"card_condition"`
|
||
CardCategory string `gorm:"column:card_category;type:varchar(50);comment:卡业务类型:normal/industry" json:"card_category"`
|
||
CarrierID *uint `gorm:"column:carrier_id;comment:运营商ID(可选,精确匹配)" json:"carrier_id"`
|
||
Priority int `gorm:"column:priority;not null;default:100;comment:优先级(数字越小优先级越高)" json:"priority"`
|
||
RealnameCheckInterval *int `gorm:"column:realname_check_interval;comment:实名检查间隔(秒),NULL表示不检查" json:"realname_check_interval"`
|
||
CarddataCheckInterval *int `gorm:"column:carddata_check_interval;comment:流量检查间隔(秒),NULL表示不检查" json:"carddata_check_interval"`
|
||
PackageCheckInterval *int `gorm:"column:package_check_interval;comment:套餐检查间隔(秒),NULL表示不检查" json:"package_check_interval"`
|
||
ProtectCheckInterval *int `gorm:"column:protect_check_interval;comment:保护期一致性检查间隔(秒),NULL=不参与" json:"protect_check_interval"`
|
||
CardStatusCheckInterval *int `gorm:"column:card_status_check_interval;comment:卡状态检查间隔(秒),NULL表示不启用卡状态轮询" json:"card_status_check_interval"`
|
||
Status int16 `gorm:"column:status;type:smallint;not null;default:1;comment:状态:0-禁用,1-启用" json:"status"`
|
||
Description string `gorm:"column:description;type:text;comment:配置说明" json:"description"`
|
||
CreatedAt time.Time `gorm:"column:created_at;not null;default:CURRENT_TIMESTAMP;comment:创建时间" json:"created_at"`
|
||
UpdatedAt time.Time `gorm:"column:updated_at;not null;default:CURRENT_TIMESTAMP;comment:更新时间" json:"updated_at"`
|
||
CreatedBy *uint `gorm:"column:created_by;comment:创建人ID" json:"created_by"`
|
||
UpdatedBy *uint `gorm:"column:updated_by;comment:更新人ID" json:"updated_by"`
|
||
}
|
||
|
||
// TableName 指定表名
|
||
func (PollingConfig) TableName() string {
|
||
return "tb_polling_config"
|
||
}
|
||
|
||
// PollingConcurrencyConfig 并发控制配置表
|
||
type PollingConcurrencyConfig struct {
|
||
ID uint `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
||
TaskType string `gorm:"column:task_type;type:varchar(50);uniqueIndex;not null;comment:任务类型:realname/carddata/package/protect/card_status" json:"task_type"`
|
||
MaxConcurrency int `gorm:"column:max_concurrency;not null;default:50;comment:最大并发数" json:"max_concurrency"`
|
||
Description string `gorm:"column:description;type:text;comment:配置说明" json:"description"`
|
||
CreatedAt time.Time `gorm:"column:created_at;not null;default:CURRENT_TIMESTAMP;comment:创建时间" json:"created_at"`
|
||
UpdatedAt time.Time `gorm:"column:updated_at;not null;default:CURRENT_TIMESTAMP;comment:更新时间" json:"updated_at"`
|
||
UpdatedBy *uint `gorm:"column:updated_by;comment:更新人ID" json:"updated_by"`
|
||
}
|
||
|
||
// TableName 指定表名
|
||
func (PollingConcurrencyConfig) TableName() string {
|
||
return "tb_polling_concurrency_config"
|
||
}
|
||
|
||
// PollingAlertRule 告警规则表
|
||
type PollingAlertRule struct {
|
||
ID uint `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
||
RuleName string `gorm:"column:rule_name;type:varchar(100);not null;comment:规则名称" json:"rule_name"`
|
||
TaskType string `gorm:"column:task_type;type:varchar(50);not null;comment:任务类型:realname/carddata/package" json:"task_type"`
|
||
MetricType string `gorm:"column:metric_type;type:varchar(50);not null;comment:指标类型:queue_size/success_rate/avg_duration/concurrency" json:"metric_type"`
|
||
Operator string `gorm:"column:operator;type:varchar(20);not null;comment:比较运算符:gt/lt/gte/lte/eq" json:"operator"`
|
||
Threshold float64 `gorm:"column:threshold;type:decimal(10,2);not null;comment:阈值" json:"threshold"`
|
||
DurationMinutes int `gorm:"column:duration_minutes;not null;default:5;comment:持续时长(分钟),避免短暂波动" json:"duration_minutes"`
|
||
AlertLevel string `gorm:"column:alert_level;type:varchar(20);not null;default:'warning';comment:告警级别:info/warning/error/critical" json:"alert_level"`
|
||
NotificationChannels string `gorm:"column:notification_channels;type:text;comment:通知渠道(JSON数组):[\"email\",\"sms\",\"webhook\"]" json:"notification_channels"`
|
||
NotificationConfig string `gorm:"column:notification_config;type:text;comment:通知配置(JSON)" json:"notification_config"`
|
||
Status int16 `gorm:"column:status;type:smallint;not null;default:1;comment:状态:0-禁用,1-启用" json:"status"`
|
||
CooldownMinutes int `gorm:"column:cooldown_minutes;not null;default:5;comment:冷却期(分钟),避免重复告警" json:"cooldown_minutes"`
|
||
Description string `gorm:"column:description;type:text;comment:规则说明" json:"description"`
|
||
CreatedAt time.Time `gorm:"column:created_at;not null;default:CURRENT_TIMESTAMP;comment:创建时间" json:"created_at"`
|
||
UpdatedAt time.Time `gorm:"column:updated_at;not null;default:CURRENT_TIMESTAMP;comment:更新时间" json:"updated_at"`
|
||
CreatedBy *uint `gorm:"column:created_by;comment:创建人ID" json:"created_by"`
|
||
UpdatedBy *uint `gorm:"column:updated_by;comment:更新人ID" json:"updated_by"`
|
||
}
|
||
|
||
// TableName 指定表名
|
||
func (PollingAlertRule) TableName() string {
|
||
return "tb_polling_alert_rule"
|
||
}
|
||
|
||
// PollingAlertHistory 告警历史表
|
||
type PollingAlertHistory struct {
|
||
ID uint `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
||
RuleID uint `gorm:"column:rule_id;not null;comment:告警规则ID" json:"rule_id"`
|
||
TaskType string `gorm:"column:task_type;type:varchar(50);not null;comment:任务类型" json:"task_type"`
|
||
MetricType string `gorm:"column:metric_type;type:varchar(50);not null;comment:指标类型" json:"metric_type"`
|
||
AlertLevel string `gorm:"column:alert_level;type:varchar(20);not null;comment:告警级别" json:"alert_level"`
|
||
CurrentValue float64 `gorm:"column:current_value;type:decimal(10,2);not null;comment:当前值" json:"current_value"`
|
||
Threshold float64 `gorm:"column:threshold;type:decimal(10,2);not null;comment:阈值" json:"threshold"`
|
||
AlertMessage string `gorm:"column:alert_message;type:text;not null;comment:告警消息" json:"alert_message"`
|
||
NotificationChannels string `gorm:"column:notification_channels;type:text;comment:通知渠道(JSON数组)" json:"notification_channels"`
|
||
NotificationStatus string `gorm:"column:notification_status;type:varchar(20);comment:通知状态:pending/sent/failed" json:"notification_status"`
|
||
NotificationResult string `gorm:"column:notification_result;type:text;comment:通知结果(JSON)" json:"notification_result"`
|
||
CreatedAt time.Time `gorm:"column:created_at;not null;default:CURRENT_TIMESTAMP;comment:告警时间" json:"created_at"`
|
||
}
|
||
|
||
// TableName 指定表名
|
||
func (PollingAlertHistory) TableName() string {
|
||
return "tb_polling_alert_history"
|
||
}
|
||
|
||
// DataCleanupConfig 数据清理配置表
|
||
type DataCleanupConfig struct {
|
||
ID uint `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
||
TargetTable string `gorm:"column:table_name;type:varchar(100);uniqueIndex;not null;comment:表名" json:"table_name"`
|
||
RetentionDays int `gorm:"column:retention_days;not null;comment:保留天数" json:"retention_days"`
|
||
Enabled int16 `gorm:"column:enabled;type:smallint;not null;default:1;comment:是否启用:0-禁用,1-启用" json:"enabled"`
|
||
BatchSize int `gorm:"column:batch_size;not null;default:10000;comment:每批删除条数" json:"batch_size"`
|
||
Description string `gorm:"column:description;type:text;comment:配置说明" json:"description"`
|
||
CreatedAt time.Time `gorm:"column:created_at;not null;default:CURRENT_TIMESTAMP;comment:创建时间" json:"created_at"`
|
||
UpdatedAt time.Time `gorm:"column:updated_at;not null;default:CURRENT_TIMESTAMP;comment:更新时间" json:"updated_at"`
|
||
UpdatedBy *uint `gorm:"column:updated_by;comment:更新人ID" json:"updated_by"`
|
||
}
|
||
|
||
// TableName 指定表名
|
||
func (DataCleanupConfig) TableName() string {
|
||
return "tb_data_cleanup_config"
|
||
}
|
||
|
||
// DataCleanupLog 数据清理日志表
|
||
type DataCleanupLog struct {
|
||
ID uint `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
||
TargetTable string `gorm:"column:table_name;type:varchar(100);not null;comment:表名" json:"table_name"`
|
||
CleanupType string `gorm:"column:cleanup_type;type:varchar(50);not null;comment:清理类型:scheduled/manual" json:"cleanup_type"`
|
||
RetentionDays int `gorm:"column:retention_days;not null;comment:保留天数" json:"retention_days"`
|
||
DeletedCount int64 `gorm:"column:deleted_count;not null;default:0;comment:删除记录数" json:"deleted_count"`
|
||
DurationMs int64 `gorm:"column:duration_ms;not null;default:0;comment:执行耗时(毫秒)" json:"duration_ms"`
|
||
Status string `gorm:"column:status;type:varchar(20);not null;comment:状态:success/failed/running" json:"status"`
|
||
ErrorMessage string `gorm:"column:error_message;type:text;comment:错误信息" json:"error_message"`
|
||
StartedAt time.Time `gorm:"column:started_at;not null;comment:开始时间" json:"started_at"`
|
||
CompletedAt *time.Time `gorm:"column:completed_at;comment:完成时间" json:"completed_at"`
|
||
TriggeredBy *uint `gorm:"column:triggered_by;comment:触发人ID(手动触发时)" json:"triggered_by"`
|
||
}
|
||
|
||
// TableName 指定表名
|
||
func (DataCleanupLog) TableName() string {
|
||
return "tb_data_cleanup_log"
|
||
}
|
||
|
||
// PollingManualTriggerLog 手动触发日志表
|
||
type PollingManualTriggerLog struct {
|
||
ID uint `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
||
TaskType string `gorm:"column:task_type;type:varchar(50);not null;comment:任务类型:realname/carddata/package" json:"task_type"`
|
||
TriggerType string `gorm:"column:trigger_type;type:varchar(50);not null;comment:触发类型:single/batch/by_condition" json:"trigger_type"`
|
||
CardIDs string `gorm:"column:card_ids;type:text;comment:卡ID列表(JSON数组)" json:"card_ids"`
|
||
ConditionFilter string `gorm:"column:condition_filter;type:text;comment:筛选条件(JSON)" json:"condition_filter"`
|
||
TotalCount int `gorm:"column:total_count;not null;default:0;comment:总卡数" json:"total_count"`
|
||
ProcessedCount int `gorm:"column:processed_count;not null;default:0;comment:已处理数" json:"processed_count"`
|
||
SuccessCount int `gorm:"column:success_count;not null;default:0;comment:成功数" json:"success_count"`
|
||
FailedCount int `gorm:"column:failed_count;not null;default:0;comment:失败数" json:"failed_count"`
|
||
Status string `gorm:"column:status;type:varchar(20);not null;comment:状态:pending/processing/completed/cancelled" json:"status"`
|
||
TriggeredBy uint `gorm:"column:triggered_by;not null;comment:触发人ID" json:"triggered_by"`
|
||
TriggeredAt time.Time `gorm:"column:triggered_at;not null;default:CURRENT_TIMESTAMP;comment:触发时间" json:"triggered_at"`
|
||
CompletedAt *time.Time `gorm:"column:completed_at;comment:完成时间" json:"completed_at"`
|
||
}
|
||
|
||
// TableName 指定表名
|
||
func (PollingManualTriggerLog) TableName() string {
|
||
return "tb_polling_manual_trigger_log"
|
||
}
|
||
|
||
// PollingPriorityItem 是「某卡某轮询任务类型被加急」的持久化事实。
|
||
//
|
||
// 活动项粒度为 (card_id, task_type),由部分唯一索引保证至多一条:Status 取
|
||
// pending-待执行 / processing-执行中 时为活动项,取 completed-已完成 / failed-失败出队 时为终态,
|
||
// 终态行不占用唯一键。TriggerType 是首次触发场景,TriggerTypes 是去重后的全部触发场景集合;
|
||
// ClaimedAt 是认领租约基准,AttemptCount 只在真正发起执行后累加,Result 与 FailureReason
|
||
// 只承载可安全展示的结果与原因,ShopIDSnapshot 是消费时点解析的卡所属店铺快照(平台卡为 NULL),
|
||
// 读侧数据范围按它下推,卡改归属不追溯已入队的加急事实。
|
||
// 本模型不声明任何关联标签与数据库外键,关联一律只存 ID 并由应用层显式查询。
|
||
type PollingPriorityItem struct {
|
||
gorm.Model
|
||
CardID uint `gorm:"column:card_id;type:bigint;not null" json:"card_id"`
|
||
TaskType string `gorm:"column:task_type;type:varchar(50);not null" json:"task_type"`
|
||
Status string `gorm:"column:status;type:varchar(16);not null;default:'pending'" json:"status"`
|
||
TriggerType string `gorm:"column:trigger_type;type:varchar(30);not null" json:"trigger_type"`
|
||
TriggerTypes string `gorm:"column:trigger_types;type:varchar(255);not null;default:''" json:"trigger_types"`
|
||
TriggerCount int `gorm:"column:trigger_count;type:int;not null;default:1" json:"trigger_count"`
|
||
LastTriggeredAt time.Time `gorm:"column:last_triggered_at;type:timestamptz;not null;default:now()" json:"last_triggered_at"`
|
||
SourceOrderID *uint `gorm:"column:source_order_id;type:bigint" json:"source_order_id,omitempty"`
|
||
SourcePackageUsageID *uint `gorm:"column:source_package_usage_id;type:bigint" json:"source_package_usage_id,omitempty"`
|
||
AttemptCount int `gorm:"column:attempt_count;type:int;not null;default:0" json:"attempt_count"`
|
||
ClaimedAt *time.Time `gorm:"column:claimed_at;type:timestamptz" json:"claimed_at,omitempty"`
|
||
ManualReason string `gorm:"column:manual_reason;type:varchar(500);not null;default:''" json:"manual_reason"`
|
||
ManualOperatorID uint `gorm:"column:manual_operator_id;type:bigint;not null;default:0" json:"manual_operator_id"`
|
||
ManualOperatorName string `gorm:"column:manual_operator_name;type:varchar(100);not null;default:''" json:"manual_operator_name"`
|
||
ShopIDSnapshot *uint `gorm:"column:shop_id_snapshot;type:bigint" json:"shop_id_snapshot,omitempty"`
|
||
Result string `gorm:"column:result;type:varchar(16);not null;default:''" json:"result"`
|
||
FailureReason string `gorm:"column:failure_reason;type:varchar(500);not null;default:''" json:"failure_reason"`
|
||
}
|
||
|
||
// TableName 指定表名
|
||
func (PollingPriorityItem) TableName() string {
|
||
return "tb_polling_priority_item"
|
||
}
|