feat: 新增数据库迁移,重命名 device_no 为 virtual_no,新增 iot_card.virtual_no 和 package.virtual_ratio 字段
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m3s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m3s
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -56,6 +56,7 @@ const (
|
||||
TaskTypePollingRealname = "polling:realname" // 实名状态检查
|
||||
TaskTypePollingCarddata = "polling:carddata" // 卡流量检查
|
||||
TaskTypePollingPackage = "polling:package" // 套餐流量检查
|
||||
TaskTypePollingProtect = "polling:protect" // 保护期一致性检查
|
||||
|
||||
// 套餐激活任务类型
|
||||
TaskTypePackageFirstActivation = "package:first:activation" // 首次实名激活
|
||||
@@ -203,3 +204,9 @@ const (
|
||||
AuthorizerTypePlatform = UserTypePlatform // 平台用户授权(2)
|
||||
AuthorizerTypeAgent = UserTypeAgent // 代理账号授权(3)
|
||||
)
|
||||
|
||||
// 设备保护期相关时长常量
|
||||
const (
|
||||
DeviceProtectPeriodDuration = 1 * time.Hour // 设备停/复机保护期时长(1小时)
|
||||
DeviceRefreshCooldownDuration = 30 * time.Second // 设备网关刷新冷却时长(30秒)
|
||||
)
|
||||
|
||||
@@ -285,3 +285,31 @@ func RedisOrderIdempotencyKey(businessKey string) string {
|
||||
func RedisOrderCreateLockKey(carrierType string, carrierID uint) string {
|
||||
return fmt.Sprintf("order:create:lock:%s:%d", carrierType, carrierID)
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// 设备保护期相关 Redis Key
|
||||
// ========================================
|
||||
|
||||
// RedisDeviceProtectKey 生成设备保护期 Redis 键
|
||||
// action: "stop"(停机保护期)或 "start"(复机保护期),两者互斥
|
||||
// 过期时间:DeviceProtectPeriodDuration(1 小时)
|
||||
func RedisDeviceProtectKey(deviceID uint, action string) string {
|
||||
return fmt.Sprintf("protect:device:%d:%s", deviceID, action)
|
||||
}
|
||||
|
||||
// RedisDeviceRefreshCooldownKey 生成设备刷新冷却期 Redis 键
|
||||
// 用途:防止同一设备短时间内多次调网关刷新(限频)
|
||||
// 过期时间:DeviceRefreshCooldownDuration(30 秒)
|
||||
func RedisDeviceRefreshCooldownKey(deviceID uint) string {
|
||||
return fmt.Sprintf("refresh:cooldown:device:%d", deviceID)
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// 轮询保护期队列 Redis Key
|
||||
// ========================================
|
||||
|
||||
// RedisPollingQueueProtectKey 保护期一致性检查轮询队列键
|
||||
// 用途:存储需要进行保护期一致性检查的卡 ID(ZSet,按时间排序)
|
||||
func RedisPollingQueueProtectKey() string {
|
||||
return "polling:queue:protect"
|
||||
}
|
||||
|
||||
@@ -52,5 +52,6 @@ func BuildDocHandlers() *bootstrap.Handlers {
|
||||
PollingAlert: admin.NewPollingAlertHandler(nil),
|
||||
PollingCleanup: admin.NewPollingCleanupHandler(nil),
|
||||
PollingManualTrigger: admin.NewPollingManualTriggerHandler(nil),
|
||||
Asset: admin.NewAssetHandler(nil, nil, nil),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,6 +164,9 @@ func (h *Handler) registerPollingHandlers() {
|
||||
|
||||
h.mux.HandleFunc(constants.TaskTypePollingPackage, pollingHandler.HandlePackageCheck)
|
||||
h.logger.Info("注册套餐检查任务处理器", zap.String("task_type", constants.TaskTypePollingPackage))
|
||||
|
||||
h.mux.HandleFunc(constants.TaskTypePollingProtect, pollingHandler.HandleProtectConsistencyCheck)
|
||||
h.logger.Info("注册保护期一致性检查任务处理器", zap.String("task_type", constants.TaskTypePollingProtect))
|
||||
}
|
||||
|
||||
func (h *Handler) registerPackageActivationHandlers() {
|
||||
|
||||
@@ -9,10 +9,11 @@ import (
|
||||
"github.com/xuri/excelize/v2"
|
||||
)
|
||||
|
||||
// CardInfo 卡信息(ICCID + MSISDN)
|
||||
// CardInfo 卡信息(ICCID + MSISDN + VirtualNo)
|
||||
type CardInfo struct {
|
||||
ICCID string
|
||||
MSISDN string
|
||||
ICCID string
|
||||
MSISDN string
|
||||
VirtualNo string
|
||||
}
|
||||
|
||||
// CSVParseResult Excel/CSV 解析结果
|
||||
@@ -33,7 +34,7 @@ type CSVParseError struct {
|
||||
// DeviceRow 设备导入数据行
|
||||
type DeviceRow struct {
|
||||
Line int
|
||||
DeviceNo string
|
||||
VirtualNo string
|
||||
DeviceName string
|
||||
DeviceModel string
|
||||
DeviceType string
|
||||
@@ -128,8 +129,8 @@ func ParseDeviceExcel(filePath string) ([]DeviceRow, int, error) {
|
||||
row := DeviceRow{Line: lineNum}
|
||||
|
||||
// 提取各字段
|
||||
if idx := colIndex["device_no"]; idx >= 0 && idx < len(record) {
|
||||
row.DeviceNo = strings.TrimSpace(record[idx])
|
||||
if idx := colIndex["virtual_no"]; idx >= 0 && idx < len(record) {
|
||||
row.VirtualNo = strings.TrimSpace(record[idx])
|
||||
}
|
||||
if idx := colIndex["device_name"]; idx >= 0 && idx < len(record) {
|
||||
row.DeviceName = strings.TrimSpace(record[idx])
|
||||
@@ -161,8 +162,8 @@ func ParseDeviceExcel(filePath string) ([]DeviceRow, int, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// 跳过设备号为空的行
|
||||
if row.DeviceNo == "" {
|
||||
// 跳过虚拟号为空的行
|
||||
if row.VirtualNo == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -204,47 +205,44 @@ func parseCardRows(rows [][]string) (*CSVParseResult, error) {
|
||||
ParseErrors: make([]CSVParseError, 0),
|
||||
}
|
||||
|
||||
// 检测表头 (第1行)
|
||||
headerSkipped := false
|
||||
iccidCol, msisdnCol := -1, -1
|
||||
iccidCol, msisdnCol, virtualNoCol := -1, -1, -1
|
||||
|
||||
if len(rows) > 0 {
|
||||
iccidCol, msisdnCol = findCardColumns(rows[0])
|
||||
iccidCol, msisdnCol, virtualNoCol = findCardColumns(rows[0])
|
||||
if iccidCol >= 0 && msisdnCol >= 0 {
|
||||
headerSkipped = true
|
||||
}
|
||||
}
|
||||
|
||||
// 确定数据开始行
|
||||
startLine := 0
|
||||
if headerSkipped {
|
||||
startLine = 1
|
||||
}
|
||||
|
||||
// 解析数据行
|
||||
for i := startLine; i < len(rows); i++ {
|
||||
row := rows[i]
|
||||
lineNum := i + 1 // Excel行号从1开始
|
||||
lineNum := i + 1
|
||||
|
||||
// 跳过空行
|
||||
if len(row) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// 提取字段
|
||||
iccid := ""
|
||||
msisdn := ""
|
||||
virtualNo := ""
|
||||
|
||||
if iccidCol >= 0 {
|
||||
// 有表头,使用列索引
|
||||
if iccidCol < len(row) {
|
||||
iccid = strings.TrimSpace(row[iccidCol])
|
||||
}
|
||||
if msisdnCol < len(row) {
|
||||
msisdn = strings.TrimSpace(row[msisdnCol])
|
||||
}
|
||||
if virtualNoCol >= 0 && virtualNoCol < len(row) {
|
||||
virtualNo = strings.TrimSpace(row[virtualNoCol])
|
||||
}
|
||||
} else {
|
||||
// 无表头,假设第一列ICCID,第二列MSISDN
|
||||
if len(row) >= 1 {
|
||||
iccid = strings.TrimSpace(row[0])
|
||||
}
|
||||
@@ -253,11 +251,9 @@ func parseCardRows(rows [][]string) (*CSVParseResult, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// 验证
|
||||
result.TotalCount++
|
||||
|
||||
if iccid == "" && msisdn == "" {
|
||||
// 空行,跳过
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -280,45 +276,50 @@ func parseCardRows(rows [][]string) (*CSVParseResult, error) {
|
||||
}
|
||||
|
||||
result.Cards = append(result.Cards, CardInfo{
|
||||
ICCID: iccid,
|
||||
MSISDN: msisdn,
|
||||
ICCID: iccid,
|
||||
MSISDN: msisdn,
|
||||
VirtualNo: virtualNo,
|
||||
})
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// findCardColumns 查找ICCID和MSISDN列索引
|
||||
// findCardColumns 查找ICCID、MSISDN和VirtualNo列索引
|
||||
// 支持中英文列名识别
|
||||
func findCardColumns(header []string) (iccidCol, msisdnCol int) {
|
||||
iccidCol, msisdnCol = -1, -1
|
||||
func findCardColumns(header []string) (iccidCol, msisdnCol, virtualNoCol int) {
|
||||
iccidCol, msisdnCol, virtualNoCol = -1, -1, -1
|
||||
|
||||
for i, col := range header {
|
||||
colLower := strings.ToLower(strings.TrimSpace(col))
|
||||
|
||||
// 识别ICCID列
|
||||
if colLower == "iccid" || colLower == "卡号" || colLower == "号码" {
|
||||
if iccidCol == -1 { // 只取第一个匹配
|
||||
if iccidCol == -1 {
|
||||
iccidCol = i
|
||||
}
|
||||
}
|
||||
|
||||
// 识别MSISDN列
|
||||
if colLower == "msisdn" || colLower == "接入号" || colLower == "手机号" || colLower == "电话" {
|
||||
if msisdnCol == -1 { // 只取第一个匹配
|
||||
if msisdnCol == -1 {
|
||||
msisdnCol = i
|
||||
}
|
||||
}
|
||||
|
||||
if colLower == "virtual_no" || colLower == "virtualno" || colLower == "虚拟号" || colLower == "设备号" {
|
||||
if virtualNoCol == -1 {
|
||||
virtualNoCol = i
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return iccidCol, msisdnCol
|
||||
return iccidCol, msisdnCol, virtualNoCol
|
||||
}
|
||||
|
||||
// buildDeviceColumnIndex 构建设备导入列索引
|
||||
// 识别表头中的列名,返回列名到列索引的映射
|
||||
func buildDeviceColumnIndex(header []string) map[string]int {
|
||||
index := map[string]int{
|
||||
"device_no": -1,
|
||||
"virtual_no": -1,
|
||||
"device_name": -1,
|
||||
"device_model": -1,
|
||||
"device_type": -1,
|
||||
|
||||
Reference in New Issue
Block a user