All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m16s
2051 lines
75 KiB
Go
2051 lines
75 KiB
Go
package main
|
||
|
||
import (
|
||
"context"
|
||
"encoding/csv"
|
||
"flag"
|
||
"fmt"
|
||
"math"
|
||
"math/big"
|
||
"os"
|
||
"path/filepath"
|
||
"regexp"
|
||
"sort"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
|
||
"gorm.io/driver/mysql"
|
||
"gorm.io/gorm"
|
||
"gorm.io/gorm/logger"
|
||
)
|
||
|
||
const (
|
||
defaultLegacySchema = "kyhl"
|
||
defaultBatchSize = 500
|
||
timeFormat = "2006-01-02 15:04:05"
|
||
sourceCardLife = "tbl_card_life"
|
||
sourceNextMonthLife = "tbl_next_month_card_life"
|
||
)
|
||
|
||
var identifierPattern = regexp.MustCompile(`^[A-Za-z0-9_]+$`)
|
||
|
||
type inputAsset struct {
|
||
Line int
|
||
DeviceVirtualNo string
|
||
IMEI string
|
||
SN string
|
||
SlotPosition int
|
||
ICCID string
|
||
ResolvedBy string
|
||
ResolveNote string
|
||
}
|
||
|
||
type cardSnapshot struct {
|
||
ICCID string `gorm:"column:iccid"`
|
||
CardID string `gorm:"column:card_id"`
|
||
Phone string `gorm:"column:phone"`
|
||
PhoneZuhe string `gorm:"column:phone_zuhe"`
|
||
OldIMEI string `gorm:"column:old_imei"`
|
||
OldVCode string `gorm:"column:old_vcode"`
|
||
Status string `gorm:"column:status"`
|
||
Type string `gorm:"column:type"`
|
||
Category string `gorm:"column:category"`
|
||
CategoryName string `gorm:"column:category_name"`
|
||
CardFlow string `gorm:"column:card_flow"`
|
||
SetMealID string `gorm:"column:set_meal_id"`
|
||
CardPackage string `gorm:"column:card_package"`
|
||
ExpireTime string `gorm:"column:expire_time"`
|
||
FlowSize string `gorm:"column:flow_size"`
|
||
TotalBytesCnt string `gorm:"column:total_bytes_cnt"`
|
||
SynchroTime string `gorm:"column:synchro_time"`
|
||
ServCreateAt *time.Time `gorm:"column:serv_create_at"`
|
||
BalanceMoney string `gorm:"column:balance_money"`
|
||
AgentID string `gorm:"column:agent_id"`
|
||
AgentName string `gorm:"column:agent_name"`
|
||
AccountID string `gorm:"column:account_id"`
|
||
AccountName string `gorm:"column:account_name"`
|
||
AgentThreeID string `gorm:"column:agent_three_id"`
|
||
}
|
||
|
||
type packageSnapshot struct {
|
||
SourceTable string `gorm:"column:source_table"`
|
||
ID string `gorm:"column:id"`
|
||
ICCID string `gorm:"column:iccid"`
|
||
MealID string `gorm:"column:meal_id"`
|
||
MealName string `gorm:"column:meal_name"`
|
||
OrderID string `gorm:"column:order_id"`
|
||
StartAt *time.Time `gorm:"column:start_at"`
|
||
ExpiresAt *time.Time `gorm:"column:expires_at"`
|
||
Status string `gorm:"column:status"`
|
||
Type string `gorm:"column:type"`
|
||
FlowSize string `gorm:"column:flow_size"`
|
||
EffectType string `gorm:"column:effect_type"`
|
||
UpdateDateTime string `gorm:"column:update_date_time"`
|
||
EffectBeginTime *time.Time `gorm:"column:effect_begin_time"`
|
||
EffectCompleteTime *time.Time `gorm:"column:effect_complete_time"`
|
||
OrderType string `gorm:"column:order_type"`
|
||
CreatedAt *time.Time `gorm:"column:created_at"`
|
||
UpdatedAt *time.Time `gorm:"column:updated_at"`
|
||
}
|
||
|
||
type walletSnapshot struct {
|
||
ICCID string `gorm:"column:iccid"`
|
||
MoneyBalance string `gorm:"column:money_balance"`
|
||
TxVersion string `gorm:"column:tx_version"`
|
||
MealAutoRenewStatus string `gorm:"column:meal_auto_renew_status"`
|
||
AutoRenewMealID string `gorm:"column:auto_renew_meal_id"`
|
||
AddonAutoRenewStatus string `gorm:"column:addon_auto_renew_status"`
|
||
AddonAutoRenewMealID string `gorm:"column:addon_auto_renew_meal_id"`
|
||
MealAutoRenewUpdatedAt *time.Time `gorm:"column:meal_auto_renew_updated_at"`
|
||
CreatedAt *time.Time `gorm:"column:created_at"`
|
||
}
|
||
|
||
type commissionAccountSnapshot struct {
|
||
AgentID string
|
||
AgentName string
|
||
AgentLoginAccount string
|
||
TotalCommissionAmount string
|
||
TotalActiveAwardCommissionAmount string
|
||
MFCommissionAmount string
|
||
NoMFCommissionAmount string
|
||
TotalDrawAmount string
|
||
CanDrawAmount string
|
||
ApplyingDrawAmount string
|
||
Remark string
|
||
TxVersion string
|
||
CreatedAt *time.Time
|
||
UpdatedAt *time.Time
|
||
}
|
||
|
||
type commissionDetailSnapshot struct {
|
||
ID string `gorm:"column:id"`
|
||
CommissionAccountID string `gorm:"column:commission_account_id"`
|
||
WxOrderID string `gorm:"column:wx_order_id"`
|
||
WxOrderNumber string `gorm:"column:wx_order_number"`
|
||
ICCID string `gorm:"column:iccid"`
|
||
Phone string `gorm:"column:phone"`
|
||
WxOrderAt *time.Time `gorm:"column:wx_order_at"`
|
||
CreatedAt *time.Time `gorm:"column:created_at"`
|
||
CommissionAmount string `gorm:"column:commission_amount"`
|
||
TotalCommissionAmount string `gorm:"column:total_commission_amount"`
|
||
MFStatus string `gorm:"column:mf_status"`
|
||
CommissionType string `gorm:"column:commission_type"`
|
||
ActiveAwardCommissionAmount string `gorm:"column:active_award_commission_amount"`
|
||
TotalActiveAwardCommissionAmount string `gorm:"column:total_active_award_commission_amount"`
|
||
}
|
||
|
||
type legacyCardIdentity struct {
|
||
ICCID string `gorm:"column:iccid"`
|
||
IMEI string `gorm:"column:imei"`
|
||
VCode string `gorm:"column:vcode"`
|
||
ResolvedBy string
|
||
ResolveNote string
|
||
SlotPosition int
|
||
}
|
||
|
||
type cardRelation struct {
|
||
MainICCID string `gorm:"column:main_iccid"`
|
||
FuICCID string `gorm:"column:fu_iccid"`
|
||
FuICCID2 string `gorm:"column:fu_iccid2"`
|
||
}
|
||
|
||
type exportData struct {
|
||
Inputs []inputAsset
|
||
UnresolvedInputs []inputAsset
|
||
InputByICCID map[string][]inputAsset
|
||
Cards map[string]cardSnapshot
|
||
PackagesByICCID map[string][]packageSnapshot
|
||
Wallets map[string]walletSnapshot
|
||
CommissionAccounts map[string]commissionAccountSnapshot
|
||
CommissionDetails []commissionDetailSnapshot
|
||
MissingICCIDs []string
|
||
}
|
||
|
||
func main() {
|
||
inputPath := flag.String("input", "", "设备/卡关系 CSV 路径")
|
||
outputDir := flag.String("out", "legacy_device_export_output", "导出目录")
|
||
dsn := flag.String("dsn", os.Getenv("LEGACY_MYSQL_DSN"), "老 MySQL DSN,也可使用 LEGACY_MYSQL_DSN")
|
||
schema := flag.String("schema", getenvDefault("LEGACY_MYSQL_SCHEMA", defaultLegacySchema), "老库 schema/database 名称")
|
||
includeCommissionDetails := flag.Bool("include-commission-details", false, "是否导出输入 ICCID 对应的佣金明细")
|
||
flag.Parse()
|
||
|
||
if *inputPath == "" {
|
||
exitWithError("请通过 -input 指定设备/卡关系 CSV")
|
||
}
|
||
if *dsn == "" {
|
||
exitWithError("请通过 -dsn 或 LEGACY_MYSQL_DSN 提供老 MySQL 连接串")
|
||
}
|
||
if !identifierPattern.MatchString(*schema) {
|
||
exitWithError("schema 只能包含字母、数字和下划线")
|
||
}
|
||
|
||
ctx := context.Background()
|
||
inputs, err := readInputCSV(*inputPath)
|
||
if err != nil {
|
||
exitWithError("读取输入 CSV 失败: %v", err)
|
||
}
|
||
if len(inputs) == 0 {
|
||
exitWithError("输入 CSV 中没有识别到任何设备/卡线索")
|
||
}
|
||
|
||
db, err := openLegacyDB(*dsn)
|
||
if err != nil {
|
||
exitWithError("连接老 MySQL 失败: %v", err)
|
||
}
|
||
|
||
resolvedInputs, unresolvedInputs, err := resolveInputAssets(ctx, db, *schema, inputs)
|
||
if err != nil {
|
||
exitWithError("按设备号/IMEI 解析 ICCID 失败: %v", err)
|
||
}
|
||
|
||
data := &exportData{
|
||
Inputs: resolvedInputs,
|
||
UnresolvedInputs: unresolvedInputs,
|
||
InputByICCID: groupInputsByICCID(resolvedInputs),
|
||
Cards: make(map[string]cardSnapshot),
|
||
PackagesByICCID: make(map[string][]packageSnapshot),
|
||
Wallets: make(map[string]walletSnapshot),
|
||
CommissionAccounts: make(map[string]commissionAccountSnapshot),
|
||
}
|
||
|
||
iccids := sortedKeys(data.InputByICCID)
|
||
if err := loadCardSnapshots(ctx, db, *schema, iccids, data.Cards); err != nil {
|
||
exitWithError("查询老卡本体失败: %v", err)
|
||
}
|
||
if err := loadPackageSnapshots(ctx, db, *schema, iccids, data.PackagesByICCID); err != nil {
|
||
exitWithError("查询老套餐快照失败: %v", err)
|
||
}
|
||
if err := loadWalletSnapshots(ctx, db, *schema, iccids, data.Wallets); err != nil {
|
||
exitWithError("查询老卡钱包失败: %v", err)
|
||
}
|
||
|
||
agentIDs := collectAgentIDs(data.Cards)
|
||
if len(agentIDs) > 0 {
|
||
if err := loadCommissionAccounts(ctx, db, *schema, agentIDs, data.CommissionAccounts); err != nil {
|
||
exitWithError("查询老佣金账户失败: %v", err)
|
||
}
|
||
}
|
||
if *includeCommissionDetails {
|
||
details, err := loadCommissionDetails(ctx, db, *schema, iccids)
|
||
if err != nil {
|
||
exitWithError("查询老佣金明细失败: %v", err)
|
||
}
|
||
data.CommissionDetails = details
|
||
}
|
||
|
||
for _, iccid := range iccids {
|
||
if _, ok := data.Cards[iccid]; !ok {
|
||
data.MissingICCIDs = append(data.MissingICCIDs, iccid)
|
||
}
|
||
}
|
||
|
||
if err := os.MkdirAll(*outputDir, 0755); err != nil {
|
||
exitWithError("创建导出目录失败: %v", err)
|
||
}
|
||
if err := writeOutputs(*outputDir, data, *includeCommissionDetails); err != nil {
|
||
exitWithError("写入导出文件失败: %v", err)
|
||
}
|
||
|
||
fmt.Printf("导出完成: %s\n", *outputDir)
|
||
fmt.Printf("已解析关系: %d 条,ICCID: %d 个,未解析输入: %d 条,老库缺失: %d 个\n", len(data.Inputs), len(iccids), len(data.UnresolvedInputs), len(data.MissingICCIDs))
|
||
fmt.Println("已跳过日记录导出,仅保留当前套餐/流量/钱包/佣金快照。")
|
||
}
|
||
|
||
func openLegacyDB(rawDSN string) (*gorm.DB, error) {
|
||
dsn := ensureMySQLDSNOptions(rawDSN)
|
||
return gorm.Open(mysql.Open(dsn), &gorm.Config{
|
||
Logger: logger.Default.LogMode(logger.Silent),
|
||
})
|
||
}
|
||
|
||
func ensureMySQLDSNOptions(dsn string) string {
|
||
options := []string{"parseTime=true", "loc=Asia%2FShanghai", "charset=utf8mb4"}
|
||
sep := "?"
|
||
if strings.Contains(dsn, "?") {
|
||
sep = "&"
|
||
}
|
||
lower := strings.ToLower(dsn)
|
||
for _, option := range options {
|
||
key := strings.ToLower(strings.SplitN(option, "=", 2)[0]) + "="
|
||
if strings.Contains(lower, key) {
|
||
continue
|
||
}
|
||
dsn += sep + option
|
||
sep = "&"
|
||
}
|
||
return dsn
|
||
}
|
||
|
||
func readInputCSV(path string) ([]inputAsset, error) {
|
||
file, err := os.Open(path)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer file.Close()
|
||
|
||
reader := csv.NewReader(file)
|
||
reader.FieldsPerRecord = -1
|
||
reader.TrimLeadingSpace = true
|
||
rows, err := reader.ReadAll()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if len(rows) < 2 {
|
||
return nil, nil
|
||
}
|
||
|
||
header := make(map[string]int)
|
||
for i, h := range rows[0] {
|
||
header[normalizeHeader(h)] = i
|
||
}
|
||
|
||
var result []inputAsset
|
||
for rowIndex := 1; rowIndex < len(rows); rowIndex++ {
|
||
row := rows[rowIndex]
|
||
base := inputAsset{
|
||
Line: rowIndex + 1,
|
||
DeviceVirtualNo: pickValue(row, header, "device_virtual_no", "device_no", "virtual_no", "mchno", "设备号", "设备虚拟号", "机器码"),
|
||
IMEI: pickValue(row, header, "imei", "device_imei", "mchno", "设备imei", "机器码"),
|
||
SN: pickValue(row, header, "sn", "设备sn"),
|
||
}
|
||
|
||
if singleICCID := pickValue(row, header, "iccid", "卡号", "主卡", "main_iccid"); singleICCID != "" {
|
||
slot := parseIntDefault(pickValue(row, header, "slot", "slot_position", "卡槽", "槽位"), 0)
|
||
result = append(result, withICCID(base, slot, singleICCID, "input_iccid"))
|
||
}
|
||
|
||
for slot := 1; slot <= 4; slot++ {
|
||
keys := slotHeaderCandidates(slot)
|
||
iccid := pickValue(row, header, keys...)
|
||
if iccid == "" {
|
||
continue
|
||
}
|
||
result = append(result, withICCID(base, slot, iccid, "input_slot_iccid"))
|
||
}
|
||
|
||
if base.ICCID == "" && base.DeviceVirtualNo == "" && base.IMEI == "" && base.SN == "" {
|
||
continue
|
||
}
|
||
if !rowHasICCID(result, base.Line) {
|
||
base.ResolvedBy = "pending_device_key"
|
||
if base.IMEI == "" && base.DeviceVirtualNo == "" {
|
||
base.ResolvedBy = "unresolved"
|
||
base.ResolveNote = "未提供 ICCID 或设备号/IMEI,无法从老库反查卡号"
|
||
}
|
||
result = append(result, base)
|
||
}
|
||
}
|
||
|
||
return dedupeInputAssets(result), nil
|
||
}
|
||
|
||
func normalizeHeader(s string) string {
|
||
s = strings.TrimSpace(strings.ToLower(s))
|
||
s = strings.TrimPrefix(s, "\ufeff")
|
||
replacer := strings.NewReplacer(" ", "_", "-", "_", "(", "", ")", "", "(", "", ")", "")
|
||
return replacer.Replace(s)
|
||
}
|
||
|
||
func slotHeaderCandidates(slot int) []string {
|
||
n := strconv.Itoa(slot)
|
||
return []string{
|
||
"iccid" + n,
|
||
"iccid_" + n,
|
||
"slot" + n + "_iccid",
|
||
"slot_" + n + "_iccid",
|
||
"card" + n,
|
||
"card_" + n,
|
||
"sim" + n,
|
||
"sim_" + n,
|
||
"卡" + n,
|
||
"卡" + n + "iccid",
|
||
"卡" + n + "_iccid",
|
||
}
|
||
}
|
||
|
||
func pickValue(row []string, header map[string]int, keys ...string) string {
|
||
for _, key := range keys {
|
||
idx, ok := header[normalizeHeader(key)]
|
||
if !ok || idx >= len(row) {
|
||
continue
|
||
}
|
||
if value := normalizeCell(row[idx]); value != "" {
|
||
return value
|
||
}
|
||
}
|
||
return ""
|
||
}
|
||
|
||
func normalizeCell(s string) string {
|
||
s = strings.TrimSpace(s)
|
||
s = strings.TrimPrefix(s, "'")
|
||
s = strings.ReplaceAll(s, "\u00a0", "")
|
||
return s
|
||
}
|
||
|
||
func withICCID(base inputAsset, slot int, iccid string, resolvedBy string) inputAsset {
|
||
base.SlotPosition = slot
|
||
base.ICCID = normalizeCell(iccid)
|
||
base.ResolvedBy = resolvedBy
|
||
return base
|
||
}
|
||
|
||
func dedupeInputAssets(inputs []inputAsset) []inputAsset {
|
||
seen := make(map[string]bool)
|
||
result := make([]inputAsset, 0, len(inputs))
|
||
for _, item := range inputs {
|
||
key := fmt.Sprintf("%d|%s|%s|%s|%d", item.Line, item.DeviceVirtualNo, item.IMEI, item.ICCID, item.SlotPosition)
|
||
if seen[key] {
|
||
continue
|
||
}
|
||
seen[key] = true
|
||
result = append(result, item)
|
||
}
|
||
return result
|
||
}
|
||
|
||
func groupInputsByICCID(inputs []inputAsset) map[string][]inputAsset {
|
||
result := make(map[string][]inputAsset)
|
||
for _, item := range inputs {
|
||
if item.ICCID == "" {
|
||
continue
|
||
}
|
||
result[item.ICCID] = append(result[item.ICCID], item)
|
||
}
|
||
return result
|
||
}
|
||
|
||
func rowHasICCID(inputs []inputAsset, line int) bool {
|
||
for _, item := range inputs {
|
||
if item.Line == line && item.ICCID != "" {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
func resolveInputAssets(ctx context.Context, db *gorm.DB, schema string, inputs []inputAsset) ([]inputAsset, []inputAsset, error) {
|
||
deviceKeys := collectPendingDeviceKeys(inputs)
|
||
cardsByDeviceKey := make(map[string][]legacyCardIdentity)
|
||
if len(deviceKeys) > 0 {
|
||
var err error
|
||
cardsByDeviceKey, err = loadCardIdentitiesByDeviceKey(ctx, db, schema, deviceKeys)
|
||
if err != nil {
|
||
return nil, nil, err
|
||
}
|
||
}
|
||
|
||
resolved := make([]inputAsset, 0, len(inputs))
|
||
unresolved := make([]inputAsset, 0)
|
||
for _, item := range inputs {
|
||
if item.ICCID != "" {
|
||
if item.ResolvedBy == "" {
|
||
item.ResolvedBy = "input_iccid"
|
||
}
|
||
resolved = append(resolved, item)
|
||
continue
|
||
}
|
||
keys := inputDeviceKeys(item)
|
||
if len(keys) == 0 {
|
||
item.ResolvedBy = "unresolved"
|
||
if item.ResolveNote == "" {
|
||
item.ResolveNote = "未提供 ICCID 或设备号/IMEI,无法从老库反查卡号"
|
||
}
|
||
unresolved = append(unresolved, item)
|
||
continue
|
||
}
|
||
|
||
matches := uniqueIdentitiesForKeys(cardsByDeviceKey, keys)
|
||
if len(matches) == 0 {
|
||
item.ResolvedBy = "unresolved_device_key"
|
||
item.ResolveNote = "老库 tbl_card 未按 vcode/imei 找到设备号对应的 ICCID"
|
||
unresolved = append(unresolved, item)
|
||
continue
|
||
}
|
||
for _, match := range matches {
|
||
resolvedItem := item
|
||
resolvedItem.ICCID = match.ICCID
|
||
resolvedItem.ResolvedBy = match.ResolvedBy
|
||
resolvedItem.ResolveNote = match.ResolveNote
|
||
if match.SlotPosition > 0 {
|
||
resolvedItem.SlotPosition = match.SlotPosition
|
||
}
|
||
if resolvedItem.ResolveNote == "" && len(matches) > 1 {
|
||
resolvedItem.ResolveNote = fmt.Sprintf("同一设备号在老库命中 %d 张卡,已按 tbl_card_relate 展开", len(matches))
|
||
}
|
||
if resolvedItem.ResolveNote == "" {
|
||
if match.VCode != "" {
|
||
resolvedItem.ResolveNote = "按老库 tbl_card.vcode 反查 ICCID"
|
||
} else if match.IMEI != "" {
|
||
resolvedItem.ResolveNote = "按老库 tbl_card.imei 反查 ICCID"
|
||
}
|
||
}
|
||
resolved = append(resolved, resolvedItem)
|
||
}
|
||
}
|
||
|
||
return dedupeInputAssets(resolved), dedupeInputAssets(unresolved), nil
|
||
}
|
||
|
||
func collectPendingDeviceKeys(inputs []inputAsset) []string {
|
||
set := make(map[string]bool)
|
||
for _, item := range inputs {
|
||
if item.ICCID == "" {
|
||
for _, key := range inputDeviceKeys(item) {
|
||
addNonEmpty(set, key)
|
||
}
|
||
}
|
||
}
|
||
return sortedKeys(set)
|
||
}
|
||
|
||
func inputDeviceKeys(item inputAsset) []string {
|
||
set := make(map[string]bool)
|
||
addNonEmpty(set, item.IMEI)
|
||
addNonEmpty(set, item.DeviceVirtualNo)
|
||
return sortedKeys(set)
|
||
}
|
||
|
||
func uniqueIdentitiesForKeys(source map[string][]legacyCardIdentity, keys []string) []legacyCardIdentity {
|
||
byICCID := make(map[string]legacyCardIdentity)
|
||
for _, key := range keys {
|
||
for _, item := range source[key] {
|
||
if item.ICCID == "" {
|
||
continue
|
||
}
|
||
existing, ok := byICCID[item.ICCID]
|
||
if !ok || identityPriority(item) > identityPriority(existing) {
|
||
byICCID[item.ICCID] = item
|
||
}
|
||
}
|
||
}
|
||
iccids := sortedKeys(byICCID)
|
||
result := make([]legacyCardIdentity, 0, len(iccids))
|
||
for _, iccid := range iccids {
|
||
result = append(result, byICCID[iccid])
|
||
}
|
||
sort.SliceStable(result, func(i, j int) bool {
|
||
if result[i].SlotPosition != result[j].SlotPosition {
|
||
if result[i].SlotPosition == 0 {
|
||
return false
|
||
}
|
||
if result[j].SlotPosition == 0 {
|
||
return true
|
||
}
|
||
return result[i].SlotPosition < result[j].SlotPosition
|
||
}
|
||
return result[i].ICCID < result[j].ICCID
|
||
})
|
||
return result
|
||
}
|
||
|
||
func identityPriority(item legacyCardIdentity) int {
|
||
if item.SlotPosition > 0 && strings.Contains(item.ResolvedBy, "relate") {
|
||
return 3
|
||
}
|
||
if item.VCode != "" {
|
||
return 2
|
||
}
|
||
return 1
|
||
}
|
||
|
||
func loadCardIdentitiesByDeviceKey(ctx context.Context, db *gorm.DB, schema string, keys []string) (map[string][]legacyCardIdentity, error) {
|
||
directByKey, err := loadDirectCardIdentitiesByDeviceKey(ctx, db, schema, keys)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if len(directByKey) == 0 {
|
||
return directByKey, nil
|
||
}
|
||
|
||
directICCIDs := collectDirectIdentityICCIDs(directByKey)
|
||
relations, err := loadCardRelationsByICCIDs(ctx, db, schema, directICCIDs)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if len(relations) == 0 {
|
||
return directByKey, nil
|
||
}
|
||
|
||
expanded := make(map[string][]legacyCardIdentity, len(directByKey))
|
||
for key, directRows := range directByKey {
|
||
byICCID := make(map[string]legacyCardIdentity)
|
||
for _, row := range directRows {
|
||
byICCID[row.ICCID] = row
|
||
for _, relation := range relations {
|
||
if !relationContains(relation, row.ICCID) {
|
||
continue
|
||
}
|
||
addRelationIdentity(byICCID, relation.MainICCID, 1, "主卡,按 tbl_card_relate 从设备号命中卡展开")
|
||
addRelationIdentity(byICCID, relation.FuICCID, 2, "副卡1,按 tbl_card_relate 从设备号命中卡展开")
|
||
addRelationIdentity(byICCID, relation.FuICCID2, 3, "副卡2,按 tbl_card_relate 从设备号命中卡展开")
|
||
}
|
||
}
|
||
expanded[key] = identitiesFromMap(byICCID)
|
||
}
|
||
return expanded, nil
|
||
}
|
||
|
||
func loadDirectCardIdentitiesByDeviceKey(ctx context.Context, db *gorm.DB, schema string, keys []string) (map[string][]legacyCardIdentity, error) {
|
||
result := make(map[string][]legacyCardIdentity)
|
||
keySet := make(map[string]bool, len(keys))
|
||
for _, key := range keys {
|
||
keySet[key] = true
|
||
}
|
||
|
||
for _, batch := range chunkStrings(keys, defaultBatchSize) {
|
||
var rows []legacyCardIdentity
|
||
sql := fmt.Sprintf(`
|
||
SELECT
|
||
iccid_mark AS iccid,
|
||
imei,
|
||
vcode
|
||
FROM %s.tbl_card
|
||
WHERE (vcode IN ? OR imei IN ?)
|
||
AND iccid_mark IS NOT NULL
|
||
AND iccid_mark <> ''
|
||
ORDER BY vcode ASC, imei ASC, iccid_mark ASC
|
||
`, quoteSchema(schema))
|
||
if err := db.WithContext(ctx).Raw(sql, batch, batch).Scan(&rows).Error; err != nil {
|
||
return nil, err
|
||
}
|
||
for _, row := range rows {
|
||
if row.ICCID == "" {
|
||
continue
|
||
}
|
||
if row.VCode != "" && keySet[row.VCode] {
|
||
item := row
|
||
item.ResolvedBy = "old_tbl_card_vcode"
|
||
item.ResolveNote = "按老库 tbl_card.vcode 反查 ICCID"
|
||
result[row.VCode] = append(result[row.VCode], item)
|
||
}
|
||
if row.IMEI != "" && keySet[row.IMEI] {
|
||
item := row
|
||
item.ResolvedBy = "old_tbl_card_imei"
|
||
item.ResolveNote = "按老库 tbl_card.imei 反查 ICCID"
|
||
result[row.IMEI] = append(result[row.IMEI], item)
|
||
}
|
||
}
|
||
}
|
||
return result, nil
|
||
}
|
||
|
||
func collectDirectIdentityICCIDs(directByKey map[string][]legacyCardIdentity) []string {
|
||
set := make(map[string]bool)
|
||
for _, rows := range directByKey {
|
||
for _, row := range rows {
|
||
addNonEmpty(set, row.ICCID)
|
||
}
|
||
}
|
||
return sortedKeys(set)
|
||
}
|
||
|
||
func loadCardRelationsByICCIDs(ctx context.Context, db *gorm.DB, schema string, iccids []string) ([]cardRelation, error) {
|
||
var result []cardRelation
|
||
for _, batch := range chunkStrings(iccids, defaultBatchSize) {
|
||
var rows []cardRelation
|
||
sql := fmt.Sprintf(`
|
||
SELECT
|
||
main_iccid,
|
||
fu_iccid,
|
||
fu_iccid2
|
||
FROM %s.tbl_card_relate
|
||
WHERE main_iccid IN ? OR fu_iccid IN ? OR fu_iccid2 IN ?
|
||
`, quoteSchema(schema))
|
||
if err := db.WithContext(ctx).Raw(sql, batch, batch, batch).Scan(&rows).Error; err != nil {
|
||
return nil, err
|
||
}
|
||
result = append(result, rows...)
|
||
}
|
||
return result, nil
|
||
}
|
||
|
||
func relationContains(relation cardRelation, iccid string) bool {
|
||
return relation.MainICCID == iccid || relation.FuICCID == iccid || relation.FuICCID2 == iccid
|
||
}
|
||
|
||
func addRelationIdentity(byICCID map[string]legacyCardIdentity, iccid string, slot int, note string) {
|
||
if iccid == "" {
|
||
return
|
||
}
|
||
item := legacyCardIdentity{
|
||
ICCID: iccid,
|
||
ResolvedBy: "old_tbl_card_relate",
|
||
ResolveNote: note,
|
||
SlotPosition: slot,
|
||
}
|
||
existing, ok := byICCID[iccid]
|
||
if ok {
|
||
item.IMEI = existing.IMEI
|
||
item.VCode = existing.VCode
|
||
if existing.ResolvedBy == "old_tbl_card_vcode" || existing.ResolvedBy == "old_tbl_card_imei" {
|
||
item.ResolvedBy = existing.ResolvedBy + "+old_tbl_card_relate"
|
||
}
|
||
}
|
||
if !ok || identityPriority(item) > identityPriority(existing) {
|
||
byICCID[iccid] = item
|
||
}
|
||
}
|
||
|
||
func identitiesFromMap(byICCID map[string]legacyCardIdentity) []legacyCardIdentity {
|
||
iccids := sortedKeys(byICCID)
|
||
result := make([]legacyCardIdentity, 0, len(iccids))
|
||
for _, iccid := range iccids {
|
||
result = append(result, byICCID[iccid])
|
||
}
|
||
sort.SliceStable(result, func(i, j int) bool {
|
||
if result[i].SlotPosition != result[j].SlotPosition {
|
||
if result[i].SlotPosition == 0 {
|
||
return false
|
||
} else {
|
||
if result[j].SlotPosition == 0 {
|
||
return true
|
||
}
|
||
}
|
||
return result[i].SlotPosition < result[j].SlotPosition
|
||
}
|
||
return result[i].ICCID < result[j].ICCID
|
||
})
|
||
return result
|
||
}
|
||
|
||
func loadCardSnapshots(ctx context.Context, db *gorm.DB, schema string, iccids []string, out map[string]cardSnapshot) error {
|
||
for _, batch := range chunkStrings(iccids, defaultBatchSize) {
|
||
var rows []cardSnapshot
|
||
sql := fmt.Sprintf(`
|
||
SELECT
|
||
c.id AS card_id,
|
||
c.iccid_mark AS iccid,
|
||
c.phone,
|
||
c.phone_zuhe,
|
||
c.imei AS old_imei,
|
||
c.vcode AS old_vcode,
|
||
CAST(c.status AS CHAR) AS status,
|
||
CAST(c.type AS CHAR) AS type,
|
||
CAST(c.category AS CHAR) AS category,
|
||
vc.category_name AS category_name,
|
||
c.card_flow,
|
||
c.set_meal_id,
|
||
c.card_package,
|
||
c.expire_time,
|
||
CAST(c.flow_size AS CHAR) AS flow_size,
|
||
c.total_bytes_cnt,
|
||
c.synchro_time,
|
||
c.serv_create_date AS serv_create_at,
|
||
c.balance_money,
|
||
c.agent_id,
|
||
c.agent_name,
|
||
c.account_id,
|
||
c.account_name,
|
||
c.agent_three_id
|
||
FROM %s.tbl_card c
|
||
LEFT JOIN %s.tbl_vendor_category vc ON vc.category = c.category
|
||
WHERE c.iccid_mark IN ?
|
||
`, quoteSchema(schema), quoteSchema(schema))
|
||
if err := db.WithContext(ctx).Raw(sql, batch).Scan(&rows).Error; err != nil {
|
||
return err
|
||
}
|
||
for _, row := range rows {
|
||
if row.ICCID == "" {
|
||
continue
|
||
}
|
||
out[row.ICCID] = row
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func loadPackageSnapshots(ctx context.Context, db *gorm.DB, schema string, iccids []string, out map[string][]packageSnapshot) error {
|
||
for _, batch := range chunkStrings(iccids, defaultBatchSize) {
|
||
var currentRows []packageSnapshot
|
||
currentSQL := fmt.Sprintf(`
|
||
SELECT
|
||
'%s' AS source_table,
|
||
id,
|
||
iccid_mark AS iccid,
|
||
meal_id,
|
||
meal_name,
|
||
order_id,
|
||
start_date AS start_at,
|
||
expire_date AS expires_at,
|
||
CAST(status AS CHAR) AS status,
|
||
CAST(type AS CHAR) AS type,
|
||
CAST(flow_size AS CHAR) AS flow_size,
|
||
CAST(effect_type AS CHAR) AS effect_type,
|
||
update_date_time,
|
||
create_time AS created_at,
|
||
update_time AS updated_at
|
||
FROM %s.tbl_card_life
|
||
WHERE iccid_mark IN ?
|
||
`, sourceCardLife, quoteSchema(schema))
|
||
if err := db.WithContext(ctx).Raw(currentSQL, batch).Scan(¤tRows).Error; err != nil {
|
||
return err
|
||
}
|
||
for _, row := range currentRows {
|
||
if row.ICCID == "" {
|
||
continue
|
||
}
|
||
out[row.ICCID] = append(out[row.ICCID], row)
|
||
}
|
||
|
||
var pendingRows []packageSnapshot
|
||
pendingSQL := fmt.Sprintf(`
|
||
SELECT
|
||
'%s' AS source_table,
|
||
id,
|
||
iccid_mark AS iccid,
|
||
meal_id,
|
||
meal_name,
|
||
agent_wx_order_id AS order_id,
|
||
effect_begin_time AS effect_begin_time,
|
||
effect_complete_time AS effect_complete_time,
|
||
CAST(status AS CHAR) AS status,
|
||
CAST(flow_size AS CHAR) AS flow_size,
|
||
order_type,
|
||
create_date AS created_at
|
||
FROM %s.tbl_next_month_card_life
|
||
WHERE iccid_mark IN ?
|
||
`, sourceNextMonthLife, quoteSchema(schema))
|
||
if err := db.WithContext(ctx).Raw(pendingSQL, batch).Scan(&pendingRows).Error; err != nil {
|
||
return err
|
||
}
|
||
for _, row := range pendingRows {
|
||
if row.ICCID == "" {
|
||
continue
|
||
}
|
||
out[row.ICCID] = append(out[row.ICCID], row)
|
||
}
|
||
}
|
||
|
||
for iccid := range out {
|
||
sort.SliceStable(out[iccid], func(i, j int) bool {
|
||
left := out[iccid][i]
|
||
right := out[iccid][j]
|
||
if left.SourceTable != right.SourceTable {
|
||
return left.SourceTable == sourceCardLife
|
||
}
|
||
return timePtrBefore(left.StartAt, right.StartAt)
|
||
})
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func loadWalletSnapshots(ctx context.Context, db *gorm.DB, schema string, iccids []string, out map[string]walletSnapshot) error {
|
||
for _, batch := range chunkStrings(iccids, defaultBatchSize) {
|
||
var rows []walletSnapshot
|
||
sql := fmt.Sprintf(`
|
||
SELECT
|
||
iccid_mark AS iccid,
|
||
CAST(money_balance AS CHAR) AS money_balance,
|
||
CAST(tx_version AS CHAR) AS tx_version,
|
||
CAST(meal_auto_renew_status AS CHAR) AS meal_auto_renew_status,
|
||
auto_renew_meal_id,
|
||
CAST(djb_meal_auto_renew_status AS CHAR) AS addon_auto_renew_status,
|
||
djb_auto_renew_meal_id AS addon_auto_renew_meal_id,
|
||
meal_auto_renew_udpate_time AS meal_auto_renew_updated_at,
|
||
create_date AS created_at
|
||
FROM %s.tbl_card_wallet
|
||
WHERE iccid_mark IN ?
|
||
`, quoteSchema(schema))
|
||
if err := db.WithContext(ctx).Raw(sql, batch).Scan(&rows).Error; err != nil {
|
||
return err
|
||
}
|
||
for _, row := range rows {
|
||
if row.ICCID == "" {
|
||
continue
|
||
}
|
||
out[row.ICCID] = row
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func loadCommissionAccounts(ctx context.Context, db *gorm.DB, schema string, agentIDs []string, out map[string]commissionAccountSnapshot) error {
|
||
for _, batch := range chunkStrings(agentIDs, defaultBatchSize) {
|
||
var rows []commissionAccountSnapshot
|
||
sql := fmt.Sprintf(`
|
||
SELECT
|
||
agent_id,
|
||
agent_name,
|
||
agent_login_account,
|
||
CAST(total_commision_amount AS CHAR) AS total_commission_amount,
|
||
CAST(total_active_award_commision_amount AS CHAR) AS total_active_award_commission_amount,
|
||
CAST(mf_commision_amount AS CHAR) AS mf_commission_amount,
|
||
CAST(nomf_commision_amount AS CHAR) AS no_mf_commission_amount,
|
||
CAST(total_draw_amount AS CHAR) AS total_draw_amount,
|
||
CAST(can_draw_amount AS CHAR) AS can_draw_amount,
|
||
CAST(applying_draw_amount AS CHAR) AS applying_draw_amount,
|
||
remark,
|
||
CAST(tx_version AS CHAR) AS tx_version,
|
||
create_date AS created_at,
|
||
update_date AS updated_at
|
||
FROM %s.tbl_agent_commission_account
|
||
WHERE agent_id IN ?
|
||
`, quoteSchema(schema))
|
||
if err := db.WithContext(ctx).Raw(sql, batch).Scan(&rows).Error; err != nil {
|
||
return err
|
||
}
|
||
for _, row := range rows {
|
||
out[row.AgentID] = row
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func loadCommissionDetails(ctx context.Context, db *gorm.DB, schema string, iccids []string) ([]commissionDetailSnapshot, error) {
|
||
var result []commissionDetailSnapshot
|
||
for _, batch := range chunkStrings(iccids, defaultBatchSize) {
|
||
var rows []commissionDetailSnapshot
|
||
sql := fmt.Sprintf(`
|
||
SELECT
|
||
id,
|
||
commission_account_id,
|
||
wx_order_id,
|
||
wx_order_number,
|
||
iccid_mark AS iccid,
|
||
phone,
|
||
wx_order_date AS wx_order_at,
|
||
create_date AS created_at,
|
||
CAST(commission_amount AS CHAR) AS commission_amount,
|
||
CAST(total_commission_amount AS CHAR) AS total_commission_amount,
|
||
CAST(mf_status AS CHAR) AS mf_status,
|
||
CAST(commission_type AS CHAR) AS commission_type,
|
||
CAST(active_award_commision_amount AS CHAR) AS active_award_commission_amount,
|
||
CAST(total_active_award_commision_amount AS CHAR) AS total_active_award_commission_amount
|
||
FROM %s.tbl_agent_commission_detail
|
||
WHERE iccid_mark IN ?
|
||
ORDER BY create_date ASC
|
||
`, quoteSchema(schema))
|
||
if err := db.WithContext(ctx).Raw(sql, batch).Scan(&rows).Error; err != nil {
|
||
return nil, err
|
||
}
|
||
for _, row := range rows {
|
||
if row.ICCID == "" {
|
||
continue
|
||
}
|
||
result = append(result, row)
|
||
}
|
||
}
|
||
return result, nil
|
||
}
|
||
|
||
func writeOutputs(outputDir string, data *exportData, includeCommissionDetails bool) error {
|
||
writers := []struct {
|
||
name string
|
||
writer func(string, *exportData) error
|
||
}{
|
||
{"input_assets.csv", writeInputAssets},
|
||
{"unresolved_inputs.csv", writeUnresolvedInputs},
|
||
{"card_snapshot.csv", writeCardSnapshot},
|
||
{"package_usage_snapshot.csv", writePackageUsageSnapshot},
|
||
{"wallet_snapshot.csv", writeWalletSnapshot},
|
||
{"commission_account_snapshot.csv", writeCommissionAccountSnapshot},
|
||
{"missing_cards.csv", writeMissingCards},
|
||
{"summary.txt", writeSummary},
|
||
{"field_dictionary.csv", writeFieldDictionary},
|
||
{"enum_dictionary.csv", writeEnumDictionary},
|
||
{"README_zh.md", writeOutputGuide},
|
||
}
|
||
if includeCommissionDetails {
|
||
writers = append(writers, struct {
|
||
name string
|
||
writer func(string, *exportData) error
|
||
}{"commission_detail_snapshot.csv", writeCommissionDetailSnapshot})
|
||
}
|
||
|
||
for _, item := range writers {
|
||
if err := item.writer(filepath.Join(outputDir, item.name), data); err != nil {
|
||
return fmt.Errorf("%s: %w", item.name, err)
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func writeInputAssets(path string, data *exportData) error {
|
||
return writeCSV(path, []string{"line", "device_virtual_no", "imei", "sn", "slot_position", "slot_position_name", "iccid", "resolved_by", "resolved_by_name", "resolve_note"}, func(w *csv.Writer) error {
|
||
for _, item := range data.Inputs {
|
||
if err := w.Write([]string{
|
||
strconv.Itoa(item.Line),
|
||
item.DeviceVirtualNo,
|
||
item.IMEI,
|
||
item.SN,
|
||
intString(item.SlotPosition),
|
||
slotPositionName(item.SlotPosition),
|
||
item.ICCID,
|
||
item.ResolvedBy,
|
||
resolvedByName(item.ResolvedBy),
|
||
item.ResolveNote,
|
||
}); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
return nil
|
||
})
|
||
}
|
||
|
||
func writeUnresolvedInputs(path string, data *exportData) error {
|
||
return writeCSV(path, []string{"line", "device_virtual_no", "imei", "sn", "slot_position", "slot_position_name", "iccid", "resolved_by", "resolved_by_name", "resolve_note"}, func(w *csv.Writer) error {
|
||
for _, item := range data.UnresolvedInputs {
|
||
if err := w.Write([]string{
|
||
strconv.Itoa(item.Line),
|
||
item.DeviceVirtualNo,
|
||
item.IMEI,
|
||
item.SN,
|
||
intString(item.SlotPosition),
|
||
slotPositionName(item.SlotPosition),
|
||
item.ICCID,
|
||
item.ResolvedBy,
|
||
resolvedByName(item.ResolvedBy),
|
||
item.ResolveNote,
|
||
}); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
return nil
|
||
})
|
||
}
|
||
|
||
func writeCardSnapshot(path string, data *exportData) error {
|
||
header := []string{
|
||
"iccid", "input_device_virtual_no", "input_imei", "input_sn", "input_slot_positions",
|
||
"old_card_id", "old_phone", "old_phone_zuhe", "old_imei", "old_vcode", "old_status", "old_status_name",
|
||
"old_type", "old_type_name", "old_category", "old_category_name",
|
||
"old_account_id", "old_account_name", "old_agent_id", "old_agent_name", "old_agent_three_id",
|
||
"old_set_meal_id", "old_card_package", "old_expire_time", "old_flow_size_mb", "old_total_bytes_cnt_mb",
|
||
"current_used_mb_decimal", "current_used_mb_int", "old_synchro_time", "old_serv_create_at", "old_balance_money",
|
||
"suggested_tb_iot_card_data_usage_mb", "suggested_tb_iot_card_current_month_usage_mb", "suggested_tb_iot_card_last_gateway_reading_mb",
|
||
}
|
||
return writeCSV(path, header, func(w *csv.Writer) error {
|
||
for _, iccid := range sortedKeys(data.InputByICCID) {
|
||
card := data.Cards[iccid]
|
||
inputSummary := summarizeInputs(data.InputByICCID[iccid])
|
||
usedDecimal := card.TotalBytesCnt
|
||
usedInt := int64FloorString(usedDecimal)
|
||
if err := w.Write([]string{
|
||
iccid,
|
||
inputSummary.DeviceVirtualNos,
|
||
inputSummary.IMEIs,
|
||
inputSummary.SNs,
|
||
inputSummary.Slots,
|
||
card.CardID,
|
||
card.Phone,
|
||
card.PhoneZuhe,
|
||
card.OldIMEI,
|
||
card.OldVCode,
|
||
card.Status,
|
||
oldCardStatusName(card.Status),
|
||
card.Type,
|
||
oldCardTypeName(card.Type),
|
||
card.Category,
|
||
oldCardCategoryName(card.Category, card.CategoryName),
|
||
card.AccountID,
|
||
card.AccountName,
|
||
card.AgentID,
|
||
card.AgentName,
|
||
card.AgentThreeID,
|
||
card.SetMealID,
|
||
card.CardPackage,
|
||
card.ExpireTime,
|
||
card.FlowSize,
|
||
card.TotalBytesCnt,
|
||
usedDecimal,
|
||
int64String(usedInt),
|
||
card.SynchroTime,
|
||
formatTime(card.ServCreateAt),
|
||
card.BalanceMoney,
|
||
int64String(usedInt),
|
||
usedDecimal,
|
||
usedDecimal,
|
||
}); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
return nil
|
||
})
|
||
}
|
||
|
||
func writePackageUsageSnapshot(path string, data *exportData) error {
|
||
header := []string{
|
||
"iccid", "input_device_virtual_no", "source_table", "source_table_name", "old_usage_id", "old_meal_id", "old_meal_name",
|
||
"old_order_id", "start_at", "expires_at", "effective_start_at", "effective_expires_at", "date_source_note",
|
||
"effect_begin_time", "effect_complete_time", "record_created_at", "record_updated_at",
|
||
"old_status", "old_status_name", "old_type", "old_type_name", "effect_type", "effect_type_name", "order_type", "order_type_name",
|
||
"flow_limit_mb", "card_current_used_mb_decimal", "allocated_used_mb", "remaining_mb",
|
||
"suggested_new_status", "suggested_new_status_name", "is_pending_next_month",
|
||
}
|
||
now := time.Now()
|
||
return writeCSV(path, header, func(w *csv.Writer) error {
|
||
for _, iccid := range sortedKeys(data.InputByICCID) {
|
||
card := data.Cards[iccid]
|
||
currentUsed := int64FloorString(card.TotalBytesCnt)
|
||
remainingUsed := currentUsed
|
||
inputSummary := summarizeInputs(data.InputByICCID[iccid])
|
||
|
||
rows := data.PackagesByICCID[iccid]
|
||
for _, pkg := range rows {
|
||
limit := int64FloorString(pkg.FlowSize)
|
||
allocatedUsed := int64(0)
|
||
if pkg.SourceTable == sourceCardLife && isActiveOldPackage(pkg, now) {
|
||
allocatedUsed = minInt64(remainingUsed, limit)
|
||
remainingUsed -= allocatedUsed
|
||
}
|
||
remaining := limit - allocatedUsed
|
||
if remaining < 0 {
|
||
remaining = 0
|
||
}
|
||
suggestedStatus := suggestPackageUsageStatus(pkg, allocatedUsed, limit, now)
|
||
isPendingNextMonth := ""
|
||
if pkg.SourceTable == sourceNextMonthLife {
|
||
isPendingNextMonth = "true"
|
||
} else {
|
||
isPendingNextMonth = "false"
|
||
}
|
||
|
||
if err := w.Write([]string{
|
||
iccid,
|
||
inputSummary.DeviceVirtualNos,
|
||
pkg.SourceTable,
|
||
sourceTableName(pkg.SourceTable),
|
||
pkg.ID,
|
||
pkg.MealID,
|
||
pkg.MealName,
|
||
pkg.OrderID,
|
||
formatTime(pkg.StartAt),
|
||
formatTime(pkg.ExpiresAt),
|
||
formatTime(effectivePackageStartAt(pkg)),
|
||
formatTime(effectivePackageExpiresAt(pkg)),
|
||
packageDateSourceNote(pkg),
|
||
formatTime(pkg.EffectBeginTime),
|
||
formatTime(pkg.EffectCompleteTime),
|
||
formatTime(pkg.CreatedAt),
|
||
formatTime(pkg.UpdatedAt),
|
||
pkg.Status,
|
||
oldPackageStatusName(pkg.SourceTable, pkg.Status),
|
||
pkg.Type,
|
||
oldPackageTypeName(pkg.Type),
|
||
pkg.EffectType,
|
||
oldEffectTypeName(pkg.EffectType),
|
||
pkg.OrderType,
|
||
oldOrderTypeName(pkg.OrderType),
|
||
int64String(limit),
|
||
card.TotalBytesCnt,
|
||
int64String(allocatedUsed),
|
||
int64String(remaining),
|
||
suggestedStatus,
|
||
newPackageUsageStatusName(suggestedStatus),
|
||
isPendingNextMonth,
|
||
}); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
}
|
||
return nil
|
||
})
|
||
}
|
||
|
||
func writeWalletSnapshot(path string, data *exportData) error {
|
||
header := []string{
|
||
"iccid", "money_balance_yuan", "money_balance_fen", "tx_version",
|
||
"meal_auto_renew_status", "meal_auto_renew_status_name", "auto_renew_meal_id",
|
||
"addon_auto_renew_status", "addon_auto_renew_status_name", "addon_auto_renew_meal_id",
|
||
"meal_auto_renew_updated_at", "created_at",
|
||
}
|
||
return writeCSV(path, header, func(w *csv.Writer) error {
|
||
for _, iccid := range sortedKeys(data.InputByICCID) {
|
||
wallet := data.Wallets[iccid]
|
||
if err := w.Write([]string{
|
||
iccid,
|
||
wallet.MoneyBalance,
|
||
decimalStringToCentsString(wallet.MoneyBalance),
|
||
wallet.TxVersion,
|
||
wallet.MealAutoRenewStatus,
|
||
autoRenewStatusName(wallet.MealAutoRenewStatus),
|
||
wallet.AutoRenewMealID,
|
||
wallet.AddonAutoRenewStatus,
|
||
autoRenewStatusName(wallet.AddonAutoRenewStatus),
|
||
wallet.AddonAutoRenewMealID,
|
||
formatTime(wallet.MealAutoRenewUpdatedAt),
|
||
formatTime(wallet.CreatedAt),
|
||
}); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
return nil
|
||
})
|
||
}
|
||
|
||
func writeCommissionAccountSnapshot(path string, data *exportData) error {
|
||
header := []string{
|
||
"old_agent_id", "old_agent_name", "old_agent_login_account", "suggested_new_shop_id",
|
||
"total_commission_yuan", "total_commission_fen",
|
||
"total_active_award_commission_yuan", "total_active_award_commission_fen",
|
||
"mf_commission_yuan", "mf_commission_fen",
|
||
"nomf_commission_yuan", "nomf_commission_fen",
|
||
"total_draw_yuan", "total_draw_fen",
|
||
"can_draw_yuan", "can_draw_fen",
|
||
"applying_draw_yuan", "applying_draw_fen",
|
||
"remark", "tx_version", "created_at", "updated_at",
|
||
}
|
||
return writeCSV(path, header, func(w *csv.Writer) error {
|
||
for _, agentID := range sortedKeys(data.CommissionAccounts) {
|
||
account := data.CommissionAccounts[agentID]
|
||
if err := w.Write([]string{
|
||
account.AgentID,
|
||
account.AgentName,
|
||
account.AgentLoginAccount,
|
||
"",
|
||
account.TotalCommissionAmount,
|
||
decimalStringToCentsString(account.TotalCommissionAmount),
|
||
account.TotalActiveAwardCommissionAmount,
|
||
decimalStringToCentsString(account.TotalActiveAwardCommissionAmount),
|
||
account.MFCommissionAmount,
|
||
decimalStringToCentsString(account.MFCommissionAmount),
|
||
account.NoMFCommissionAmount,
|
||
decimalStringToCentsString(account.NoMFCommissionAmount),
|
||
account.TotalDrawAmount,
|
||
decimalStringToCentsString(account.TotalDrawAmount),
|
||
account.CanDrawAmount,
|
||
decimalStringToCentsString(account.CanDrawAmount),
|
||
account.ApplyingDrawAmount,
|
||
decimalStringToCentsString(account.ApplyingDrawAmount),
|
||
account.Remark,
|
||
account.TxVersion,
|
||
formatTime(account.CreatedAt),
|
||
formatTime(account.UpdatedAt),
|
||
}); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
return nil
|
||
})
|
||
}
|
||
|
||
func writeCommissionDetailSnapshot(path string, data *exportData) error {
|
||
header := []string{
|
||
"id", "commission_account_id", "wx_order_id", "wx_order_number", "iccid", "phone",
|
||
"wx_order_at", "created_at", "commission_amount_yuan", "commission_amount_fen",
|
||
"total_commission_amount_yuan", "total_commission_amount_fen", "mf_status", "mf_status_name", "commission_type", "commission_type_name",
|
||
"active_award_commission_amount_yuan", "active_award_commission_amount_fen",
|
||
"total_active_award_commission_amount_yuan", "total_active_award_commission_amount_fen",
|
||
}
|
||
return writeCSV(path, header, func(w *csv.Writer) error {
|
||
for _, item := range data.CommissionDetails {
|
||
if err := w.Write([]string{
|
||
item.ID,
|
||
item.CommissionAccountID,
|
||
item.WxOrderID,
|
||
item.WxOrderNumber,
|
||
item.ICCID,
|
||
item.Phone,
|
||
formatTime(item.WxOrderAt),
|
||
formatTime(item.CreatedAt),
|
||
item.CommissionAmount,
|
||
decimalStringToCentsString(item.CommissionAmount),
|
||
item.TotalCommissionAmount,
|
||
decimalStringToCentsString(item.TotalCommissionAmount),
|
||
item.MFStatus,
|
||
mfStatusName(item.MFStatus),
|
||
item.CommissionType,
|
||
commissionTypeName(item.CommissionType),
|
||
item.ActiveAwardCommissionAmount,
|
||
decimalStringToCentsString(item.ActiveAwardCommissionAmount),
|
||
item.TotalActiveAwardCommissionAmount,
|
||
decimalStringToCentsString(item.TotalActiveAwardCommissionAmount),
|
||
}); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
return nil
|
||
})
|
||
}
|
||
|
||
func writeMissingCards(path string, data *exportData) error {
|
||
return writeCSV(path, []string{"iccid"}, func(w *csv.Writer) error {
|
||
for _, iccid := range data.MissingICCIDs {
|
||
if err := w.Write([]string{iccid}); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
return nil
|
||
})
|
||
}
|
||
|
||
func writeSummary(path string, data *exportData) error {
|
||
packageCount := 0
|
||
for _, rows := range data.PackagesByICCID {
|
||
packageCount += len(rows)
|
||
}
|
||
content := strings.Builder{}
|
||
content.WriteString("老系统设备迁移快照导出摘要\n")
|
||
content.WriteString("================================\n")
|
||
content.WriteString(fmt.Sprintf("生成时间: %s\n", time.Now().Format(timeFormat)))
|
||
content.WriteString(fmt.Sprintf("已解析输入关系行数: %d\n", len(data.Inputs)))
|
||
content.WriteString(fmt.Sprintf("未解析输入行数: %d\n", len(data.UnresolvedInputs)))
|
||
content.WriteString(fmt.Sprintf("输入 ICCID 数: %d\n", len(data.InputByICCID)))
|
||
content.WriteString(fmt.Sprintf("老库卡本体命中数: %d\n", len(data.Cards)))
|
||
content.WriteString(fmt.Sprintf("老库缺失 ICCID 数: %d\n", len(data.MissingICCIDs)))
|
||
content.WriteString(fmt.Sprintf("套餐快照行数: %d\n", packageCount))
|
||
content.WriteString(fmt.Sprintf("钱包快照行数: %d\n", len(data.Wallets)))
|
||
content.WriteString(fmt.Sprintf("佣金账户行数: %d\n", len(data.CommissionAccounts)))
|
||
content.WriteString(fmt.Sprintf("佣金明细行数: %d\n", len(data.CommissionDetails)))
|
||
content.WriteString("\n说明:\n")
|
||
content.WriteString("- 本工具只导出当前业务快照,不导出日流量记录。\n")
|
||
content.WriteString("- 输入 CSV 可以不提供 SN 和 ICCID;不提供 ICCID 时会按老库 tbl_card.vcode/imei 反查 ICCID,并通过 tbl_card_relate 展开关联卡。\n")
|
||
content.WriteString("- unresolved_inputs.csv 中的行没有解析出 ICCID,需要补设备号/IMEI/ICCID 后再重新导出。\n")
|
||
content.WriteString("- 字段含义看 field_dictionary.csv;状态码和类型码看 enum_dictionary.csv;阅读入口看 README_zh.md。\n")
|
||
content.WriteString("- package_usage_snapshot.csv 中 allocated_used_mb/remaining_mb 是按老卡当前已用量顺序分摊后的建议值,后续导入新库前仍需结合套餐映射复核。\n")
|
||
content.WriteString("- commission_account_snapshot.csv 预留 suggested_new_shop_id 空列,后续导入时填新系统 shop_id。\n")
|
||
return os.WriteFile(path, []byte(content.String()), 0644)
|
||
}
|
||
|
||
func writeCSV(path string, header []string, fill func(*csv.Writer) error) error {
|
||
file, err := os.Create(path)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
defer file.Close()
|
||
|
||
writer := csv.NewWriter(file)
|
||
if err := writer.Write(header); err != nil {
|
||
return err
|
||
}
|
||
if err := fill(writer); err != nil {
|
||
return err
|
||
}
|
||
writer.Flush()
|
||
return writer.Error()
|
||
}
|
||
|
||
type inputSummary struct {
|
||
DeviceVirtualNos string
|
||
IMEIs string
|
||
SNs string
|
||
Slots string
|
||
}
|
||
|
||
func summarizeInputs(inputs []inputAsset) inputSummary {
|
||
deviceNos := make(map[string]bool)
|
||
imeis := make(map[string]bool)
|
||
sns := make(map[string]bool)
|
||
slots := make(map[string]bool)
|
||
for _, item := range inputs {
|
||
addNonEmpty(deviceNos, item.DeviceVirtualNo)
|
||
addNonEmpty(imeis, item.IMEI)
|
||
addNonEmpty(sns, item.SN)
|
||
if item.SlotPosition > 0 {
|
||
slots[strconv.Itoa(item.SlotPosition)] = true
|
||
}
|
||
}
|
||
return inputSummary{
|
||
DeviceVirtualNos: strings.Join(sortedKeys(deviceNos), "|"),
|
||
IMEIs: strings.Join(sortedKeys(imeis), "|"),
|
||
SNs: strings.Join(sortedKeys(sns), "|"),
|
||
Slots: strings.Join(sortedKeys(slots), "|"),
|
||
}
|
||
}
|
||
|
||
func addNonEmpty(set map[string]bool, value string) {
|
||
if value != "" {
|
||
set[value] = true
|
||
}
|
||
}
|
||
|
||
func collectAgentIDs(cards map[string]cardSnapshot) []string {
|
||
set := make(map[string]bool)
|
||
for _, card := range cards {
|
||
addNonEmpty(set, card.AgentID)
|
||
}
|
||
return sortedKeys(set)
|
||
}
|
||
|
||
func isActiveOldPackage(pkg packageSnapshot, now time.Time) bool {
|
||
if pkg.Status != "1" {
|
||
return false
|
||
}
|
||
if pkg.ExpiresAt != nil && pkg.ExpiresAt.Before(now) {
|
||
return false
|
||
}
|
||
return true
|
||
}
|
||
|
||
func suggestPackageUsageStatus(pkg packageSnapshot, used, limit int64, now time.Time) string {
|
||
if pkg.SourceTable == sourceNextMonthLife {
|
||
return "0"
|
||
}
|
||
if pkg.ExpiresAt != nil && pkg.ExpiresAt.Before(now) {
|
||
return "3"
|
||
}
|
||
if limit > 0 && used >= limit {
|
||
return "2"
|
||
}
|
||
if pkg.Status == "1" {
|
||
return "1"
|
||
}
|
||
return pkg.Status
|
||
}
|
||
|
||
func effectivePackageStartAt(pkg packageSnapshot) *time.Time {
|
||
switch pkg.SourceTable {
|
||
case sourceNextMonthLife:
|
||
if pkg.EffectBeginTime != nil {
|
||
return pkg.EffectBeginTime
|
||
}
|
||
case sourceCardLife:
|
||
if pkg.StartAt != nil {
|
||
return pkg.StartAt
|
||
}
|
||
}
|
||
return pkg.CreatedAt
|
||
}
|
||
|
||
func effectivePackageExpiresAt(pkg packageSnapshot) *time.Time {
|
||
if pkg.ExpiresAt != nil {
|
||
return pkg.ExpiresAt
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func packageDateSourceNote(pkg packageSnapshot) string {
|
||
if pkg.SourceTable == sourceNextMonthLife {
|
||
if pkg.EffectBeginTime != nil {
|
||
return "待生效套餐开始时间来自 tbl_next_month_card_life.effect_begin_time;老库未提供到期时间"
|
||
}
|
||
if pkg.CreatedAt != nil {
|
||
return "待生效套餐 effect_begin_time 为空,effective_start_at 使用 create_date 兜底;老库未提供到期时间"
|
||
}
|
||
return "待生效套餐没有开始/到期时间"
|
||
}
|
||
if pkg.StartAt != nil && pkg.ExpiresAt != nil {
|
||
return "开始时间来自 tbl_card_life.start_date,到期时间来自 tbl_card_life.expire_date"
|
||
}
|
||
if pkg.StartAt == nil && pkg.CreatedAt != nil && pkg.ExpiresAt != nil {
|
||
return "tbl_card_life.start_date 为空,effective_start_at 使用 create_time 兜底;到期时间来自 expire_date"
|
||
}
|
||
if pkg.StartAt == nil && pkg.CreatedAt != nil {
|
||
return "tbl_card_life.start_date 为空,effective_start_at 使用 create_time 兜底"
|
||
}
|
||
if pkg.ExpiresAt != nil {
|
||
return "开始时间为空;到期时间来自 tbl_card_life.expire_date"
|
||
}
|
||
return "老库套餐记录没有开始/到期时间"
|
||
}
|
||
|
||
func decimalStringToCentsString(value string) string {
|
||
if strings.TrimSpace(value) == "" {
|
||
return ""
|
||
}
|
||
r, ok := new(big.Rat).SetString(strings.TrimSpace(value))
|
||
if !ok {
|
||
return ""
|
||
}
|
||
r.Mul(r, big.NewRat(100, 1))
|
||
result := new(big.Int)
|
||
// 金额只用于迁移快照,采用四舍五入到分。
|
||
if r.Sign() >= 0 {
|
||
r.Add(r, big.NewRat(1, 2))
|
||
} else {
|
||
r.Sub(r, big.NewRat(1, 2))
|
||
}
|
||
result.Quo(r.Num(), r.Denom())
|
||
return result.String()
|
||
}
|
||
|
||
func int64FloorString(value string) int64 {
|
||
value = strings.TrimSpace(value)
|
||
if value == "" {
|
||
return 0
|
||
}
|
||
f, err := strconv.ParseFloat(value, 64)
|
||
if err != nil {
|
||
return 0
|
||
}
|
||
return int64(math.Floor(f))
|
||
}
|
||
|
||
func parseIntDefault(value string, defaultValue int) int {
|
||
if value == "" {
|
||
return defaultValue
|
||
}
|
||
n, err := strconv.Atoi(value)
|
||
if err != nil {
|
||
return defaultValue
|
||
}
|
||
return n
|
||
}
|
||
|
||
func formatTime(t *time.Time) string {
|
||
if t == nil || t.IsZero() {
|
||
return ""
|
||
}
|
||
return t.In(time.Local).Format(timeFormat)
|
||
}
|
||
|
||
func timePtrBefore(left, right *time.Time) bool {
|
||
if left == nil && right == nil {
|
||
return false
|
||
}
|
||
if left == nil {
|
||
return false
|
||
}
|
||
if right == nil {
|
||
return true
|
||
}
|
||
return left.Before(*right)
|
||
}
|
||
|
||
func chunkStrings(values []string, size int) [][]string {
|
||
if size <= 0 {
|
||
size = defaultBatchSize
|
||
}
|
||
var chunks [][]string
|
||
for start := 0; start < len(values); start += size {
|
||
end := start + size
|
||
if end > len(values) {
|
||
end = len(values)
|
||
}
|
||
chunks = append(chunks, values[start:end])
|
||
}
|
||
return chunks
|
||
}
|
||
|
||
func sortedKeys[V any](m map[string]V) []string {
|
||
keys := make([]string, 0, len(m))
|
||
for key := range m {
|
||
keys = append(keys, key)
|
||
}
|
||
sort.Strings(keys)
|
||
return keys
|
||
}
|
||
|
||
func quoteSchema(schema string) string {
|
||
return "`" + schema + "`"
|
||
}
|
||
|
||
func minInt64(a, b int64) int64 {
|
||
if a < b {
|
||
return a
|
||
}
|
||
return b
|
||
}
|
||
|
||
func intString(value int) string {
|
||
if value == 0 {
|
||
return ""
|
||
}
|
||
return strconv.Itoa(value)
|
||
}
|
||
|
||
func int64String(value int64) string {
|
||
return strconv.FormatInt(value, 10)
|
||
}
|
||
|
||
func getenvDefault(key, defaultValue string) string {
|
||
value := strings.TrimSpace(os.Getenv(key))
|
||
if value == "" {
|
||
return defaultValue
|
||
}
|
||
return value
|
||
}
|
||
|
||
func exitWithError(format string, args ...any) {
|
||
fmt.Fprintf(os.Stderr, format+"\n", args...)
|
||
os.Exit(1)
|
||
}
|
||
|
||
type fieldDefinition struct {
|
||
FileName string
|
||
ColumnName string
|
||
ChineseName string
|
||
Description string
|
||
}
|
||
|
||
type enumDefinition struct {
|
||
FileName string
|
||
ColumnName string
|
||
Value string
|
||
Meaning string
|
||
Source string
|
||
Note string
|
||
}
|
||
|
||
var oldCardStatusNames = map[string]string{
|
||
"1": "待激活",
|
||
"2": "正常",
|
||
"3": "断网",
|
||
"4": "停机",
|
||
"5": "异常",
|
||
"6": "强制停机",
|
||
"7": "注销",
|
||
}
|
||
|
||
var oldCardTypeNames = map[string]string{
|
||
"1": "包月",
|
||
"2": "包年",
|
||
}
|
||
|
||
var oldCategoryNames = map[string]string{
|
||
"1": "电信",
|
||
"2": "其他卡务",
|
||
"3": "移动",
|
||
"4": "联通",
|
||
"5": "dcp电信",
|
||
"9": "中国联通",
|
||
"10": "D移动",
|
||
"21": "S物联",
|
||
"22": "W物联",
|
||
"23": "L物联",
|
||
"24": "P物联",
|
||
"25": "D物联",
|
||
"26": "M物联",
|
||
"27": "WX物联",
|
||
"28": "CH物联",
|
||
"31": "YW物联",
|
||
"40": "XS物联",
|
||
"56": "ZT物联",
|
||
"67": "TY物联",
|
||
"70": "XR物联",
|
||
"71": "ZYM物联",
|
||
"89": "LW物联",
|
||
"94": "DC物联",
|
||
"108": "CMP5G",
|
||
"112": "GD物联",
|
||
"113": "GDIot",
|
||
"118": "GDWL",
|
||
"124": "GS电信",
|
||
"125": "GS联通",
|
||
"126": "GS移动",
|
||
}
|
||
|
||
var oldCardLifeStatusNames = map[string]string{
|
||
"1": "正常",
|
||
"2": "停卡",
|
||
}
|
||
|
||
var oldNextMonthLifeStatusNames = map[string]string{
|
||
"1": "未生效",
|
||
"2": "已生效,已插入 tbl_card_life",
|
||
}
|
||
|
||
var oldPackageTypeNames = map[string]string{
|
||
"1": "叠加包",
|
||
"2": "月卡",
|
||
"3": "季卡",
|
||
"4": "半年卡",
|
||
"5": "年卡",
|
||
}
|
||
|
||
var oldEffectTypeNames = map[string]string{
|
||
"1": "本月生效",
|
||
"2": "次月生效",
|
||
}
|
||
|
||
var oldOrderTypeNames = map[string]string{
|
||
"1": "微信订单",
|
||
"2": "后台订单",
|
||
}
|
||
|
||
var autoRenewStatusNames = map[string]string{
|
||
"1": "启用",
|
||
"2": "关闭",
|
||
}
|
||
|
||
var newPackageUsageStatusNames = map[string]string{
|
||
"0": "待生效",
|
||
"1": "生效中",
|
||
"2": "已用完",
|
||
"3": "已过期",
|
||
"4": "已失效",
|
||
}
|
||
|
||
var mfStatusNames = map[string]string{
|
||
"0": "成功",
|
||
"1": "失败",
|
||
}
|
||
|
||
var commissionTypeNames = map[string]string{
|
||
"1": "微信订单佣金",
|
||
"2": "激活奖励佣金",
|
||
"3": "微信订单佣金扣回",
|
||
"4": "激活奖励佣金扣回",
|
||
}
|
||
|
||
func writeFieldDictionary(path string, data *exportData) error {
|
||
fields := []fieldDefinition{
|
||
{"input_assets.csv", "line", "输入行号", "原始输入 CSV 中的行号,便于回查业务给的源数据。"},
|
||
{"input_assets.csv", "device_virtual_no", "输入设备号", "输入 CSV 中识别到的设备号/机器码。"},
|
||
{"input_assets.csv", "imei", "输入 IMEI", "输入 CSV 中识别到的 IMEI;老系统接口参数 mchno 实际通常对应 tbl_card.vcode。"},
|
||
{"input_assets.csv", "sn", "输入 SN", "输入 CSV 中透传的 SN,可为空。"},
|
||
{"input_assets.csv", "slot_position", "卡槽序号", "脚本按老系统关联表展开后的卡槽:1 主卡,2 副卡1,3 副卡2。"},
|
||
{"input_assets.csv", "slot_position_name", "卡槽名称", "卡槽序号的中文含义。"},
|
||
{"input_assets.csv", "iccid", "ICCID", "解析出来的老系统卡 ICCID。"},
|
||
{"input_assets.csv", "resolved_by", "解析方式", "说明 ICCID 是由输入直接提供,还是按 tbl_card.vcode/imei 及 tbl_card_relate 反查得到。"},
|
||
{"input_assets.csv", "resolved_by_name", "解析方式名称", "解析方式的中文摘要。"},
|
||
{"input_assets.csv", "resolve_note", "解析说明", "更详细的解析过程说明。"},
|
||
{"unresolved_inputs.csv", "line", "输入行号", "原始输入 CSV 中无法解析出 ICCID 的行号。"},
|
||
{"unresolved_inputs.csv", "device_virtual_no", "输入设备号", "未解析行中的设备号/机器码。"},
|
||
{"unresolved_inputs.csv", "imei", "输入 IMEI", "未解析行中的 IMEI。"},
|
||
{"unresolved_inputs.csv", "sn", "输入 SN", "未解析行中的 SN。"},
|
||
{"unresolved_inputs.csv", "slot_position", "卡槽序号", "未解析行中的卡槽序号。"},
|
||
{"unresolved_inputs.csv", "slot_position_name", "卡槽名称", "卡槽序号的中文含义。"},
|
||
{"unresolved_inputs.csv", "iccid", "ICCID", "未解析行通常为空;若有值仍未解析,需要人工核对。"},
|
||
{"unresolved_inputs.csv", "resolved_by", "解析方式", "未解析状态码。"},
|
||
{"unresolved_inputs.csv", "resolved_by_name", "解析方式名称", "未解析状态的中文摘要。"},
|
||
{"unresolved_inputs.csv", "resolve_note", "未解析原因", "说明为什么无法从老系统反查到 ICCID。"},
|
||
{"card_snapshot.csv", "iccid", "ICCID", "老系统卡 ICCID,也是后续新系统导入时对齐卡的关键标识。"},
|
||
{"card_snapshot.csv", "input_device_virtual_no", "输入设备号", "该卡来自哪些输入设备号;多个值用 | 分隔。"},
|
||
{"card_snapshot.csv", "input_imei", "输入 IMEI", "该卡来自哪些输入 IMEI;多个值用 | 分隔。"},
|
||
{"card_snapshot.csv", "input_sn", "输入 SN", "该卡来自哪些输入 SN;多个值用 | 分隔。"},
|
||
{"card_snapshot.csv", "input_slot_positions", "输入卡槽", "该卡在设备关联中的卡槽序号;多个值用 | 分隔。"},
|
||
{"card_snapshot.csv", "old_card_id", "老卡 ID", "老系统 tbl_card.id。"},
|
||
{"card_snapshot.csv", "old_phone", "老系统号码", "老系统 tbl_card.phone,一般为 MSISDN/卡号展示字段。"},
|
||
{"card_snapshot.csv", "old_phone_zuhe", "组合号码", "老系统 tbl_card.phone_zuhe。"},
|
||
{"card_snapshot.csv", "old_imei", "老系统 IMEI", "老系统 tbl_card.imei;本批样本中很多为空。"},
|
||
{"card_snapshot.csv", "old_vcode", "老系统设备码", "老系统 tbl_card.vcode;老接口 mchno 通常按这个字段查询。"},
|
||
{"card_snapshot.csv", "old_status", "老卡状态值", "老系统 tbl_card.status 原值。"},
|
||
{"card_snapshot.csv", "old_status_name", "老卡状态", "old_status 的中文含义。"},
|
||
{"card_snapshot.csv", "old_type", "老卡类型值", "老系统 tbl_card.type 原值。"},
|
||
{"card_snapshot.csv", "old_type_name", "老卡类型", "old_type 的中文含义。"},
|
||
{"card_snapshot.csv", "old_category", "老运营商分类值", "老系统 tbl_card.category 原值。"},
|
||
{"card_snapshot.csv", "old_category_name", "老运营商分类", "优先来自 tbl_vendor_category.category_name。"},
|
||
{"card_snapshot.csv", "old_account_id", "老供应账号 ID", "老系统 tbl_card.account_id。"},
|
||
{"card_snapshot.csv", "old_account_name", "老供应账号名称", "老系统 tbl_card.account_name。"},
|
||
{"card_snapshot.csv", "old_agent_id", "老代理 ID", "老系统 tbl_card.agent_id。"},
|
||
{"card_snapshot.csv", "old_agent_name", "老代理名称", "老系统 tbl_card.agent_name。"},
|
||
{"card_snapshot.csv", "old_agent_three_id", "三级代理 ID", "老系统 tbl_card.agent_three_id。"},
|
||
{"card_snapshot.csv", "old_set_meal_id", "老套餐 ID", "老系统 tbl_card.set_meal_id。"},
|
||
{"card_snapshot.csv", "old_card_package", "老套餐名称", "老系统 tbl_card.card_package。"},
|
||
{"card_snapshot.csv", "old_expire_time", "老卡到期时间", "老系统 tbl_card.expire_time;行业卡可能有值,普通设备套餐以 package_usage_snapshot.csv 为准。"},
|
||
{"card_snapshot.csv", "old_flow_size_mb", "老套餐总量 MB", "老系统 tbl_card.flow_size。"},
|
||
{"card_snapshot.csv", "old_total_bytes_cnt_mb", "老网关累计已用 MB", "老系统 tbl_card.total_bytes_cnt 原值。"},
|
||
{"card_snapshot.csv", "current_used_mb_decimal", "当前已用 MB", "迁移时建议保留的小数已用量。"},
|
||
{"card_snapshot.csv", "current_used_mb_int", "当前已用 MB 整数", "向下取整后的已用量,便于新系统整数/DECIMAL 字段导入。"},
|
||
{"card_snapshot.csv", "old_synchro_time", "老同步时间", "老系统 tbl_card.synchro_time。"},
|
||
{"card_snapshot.csv", "old_serv_create_at", "运营商开户时间", "老系统 tbl_card.serv_create_date。"},
|
||
{"card_snapshot.csv", "old_balance_money", "老余额字段", "老系统 tbl_card.balance_money。"},
|
||
{"card_snapshot.csv", "suggested_tb_iot_card_data_usage_mb", "建议卡累计用量", "建议导入新系统 tb_iot_card.data_usage_mb。"},
|
||
{"card_snapshot.csv", "suggested_tb_iot_card_current_month_usage_mb", "建议自然月已用量", "建议导入新系统 tb_iot_card.current_month_usage_mb;需结合新系统口径复核。"},
|
||
{"card_snapshot.csv", "suggested_tb_iot_card_last_gateway_reading_mb", "建议网关读数基线", "建议导入新系统 tb_iot_card.last_gateway_reading_mb,避免首轮轮询重复扣历史流量。"},
|
||
{"package_usage_snapshot.csv", "iccid", "ICCID", "套餐所属老卡 ICCID。"},
|
||
{"package_usage_snapshot.csv", "input_device_virtual_no", "输入设备号", "该套餐所属卡来自哪些输入设备号;多个值用 | 分隔。"},
|
||
{"package_usage_snapshot.csv", "source_table", "来源表", "tbl_card_life 表示当前/历史套餐,tbl_next_month_card_life 表示次月待生效套餐。"},
|
||
{"package_usage_snapshot.csv", "source_table_name", "来源表名称", "来源表的中文说明。"},
|
||
{"package_usage_snapshot.csv", "old_usage_id", "老套餐记录 ID", "老系统套餐使用记录主键。"},
|
||
{"package_usage_snapshot.csv", "old_meal_id", "老套餐 ID", "老系统套餐 ID。"},
|
||
{"package_usage_snapshot.csv", "old_meal_name", "老套餐名称", "老系统套餐名称。"},
|
||
{"package_usage_snapshot.csv", "old_order_id", "老订单 ID", "老系统订单 ID。"},
|
||
{"package_usage_snapshot.csv", "start_at", "开始时间", "tbl_card_life.start_date。"},
|
||
{"package_usage_snapshot.csv", "expires_at", "到期时间", "tbl_card_life.expire_date。"},
|
||
{"package_usage_snapshot.csv", "effective_start_at", "有效开始时间", "迁移参考开始时间;优先使用 start_at,老库 start_at 为空时使用记录创建时间兜底。"},
|
||
{"package_usage_snapshot.csv", "effective_expires_at", "有效到期时间", "迁移参考到期时间;当前仅使用老库 expire_date,待生效套餐老库通常没有到期时间。"},
|
||
{"package_usage_snapshot.csv", "date_source_note", "时间来源说明", "说明开始/到期时间来自哪个老库字段,以及是否使用兜底。"},
|
||
{"package_usage_snapshot.csv", "effect_begin_time", "待生效开始时间", "tbl_next_month_card_life.effect_begin_time。"},
|
||
{"package_usage_snapshot.csv", "effect_complete_time", "待生效完成时间", "tbl_next_month_card_life.effect_complete_time。"},
|
||
{"package_usage_snapshot.csv", "record_created_at", "套餐记录创建时间", "tbl_card_life.create_time 或 tbl_next_month_card_life.create_date。"},
|
||
{"package_usage_snapshot.csv", "record_updated_at", "套餐记录更新时间", "tbl_card_life.update_time;次月待生效表没有对应字段。"},
|
||
{"package_usage_snapshot.csv", "old_status", "老套餐状态值", "来源表中的 status 原值。"},
|
||
{"package_usage_snapshot.csv", "old_status_name", "老套餐状态", "old_status 的中文含义;不同来源表含义不同。"},
|
||
{"package_usage_snapshot.csv", "old_type", "老套餐类型值", "tbl_card_life.type 原值。"},
|
||
{"package_usage_snapshot.csv", "old_type_name", "老套餐类型", "old_type 的中文含义。"},
|
||
{"package_usage_snapshot.csv", "effect_type", "生效类型值", "tbl_card_life.effect_type 原值。"},
|
||
{"package_usage_snapshot.csv", "effect_type_name", "生效类型", "effect_type 的中文含义。"},
|
||
{"package_usage_snapshot.csv", "order_type", "订单类型值", "tbl_next_month_card_life.order_type 原值。"},
|
||
{"package_usage_snapshot.csv", "order_type_name", "订单类型", "order_type 的中文含义。"},
|
||
{"package_usage_snapshot.csv", "flow_limit_mb", "套餐总量 MB", "老套餐记录 flow_size。"},
|
||
{"package_usage_snapshot.csv", "card_current_used_mb_decimal", "卡当前已用 MB", "来自 card_snapshot 的当前已用量。"},
|
||
{"package_usage_snapshot.csv", "allocated_used_mb", "建议套餐已用 MB", "按当前生效套餐顺序从卡已用量中分摊出来的迁移建议值。"},
|
||
{"package_usage_snapshot.csv", "remaining_mb", "建议套餐剩余 MB", "flow_limit_mb - allocated_used_mb。"},
|
||
{"package_usage_snapshot.csv", "suggested_new_status", "建议新套餐状态值", "建议导入新系统 tb_package_usage.status。"},
|
||
{"package_usage_snapshot.csv", "suggested_new_status_name", "建议新套餐状态", "suggested_new_status 的中文含义。"},
|
||
{"package_usage_snapshot.csv", "is_pending_next_month", "是否次月待生效", "true 表示来源于 tbl_next_month_card_life。"},
|
||
{"wallet_snapshot.csv", "iccid", "ICCID", "钱包所属老卡 ICCID。"},
|
||
{"wallet_snapshot.csv", "money_balance_yuan", "钱包余额元", "老系统 tbl_card_wallet.money_balance。"},
|
||
{"wallet_snapshot.csv", "money_balance_fen", "钱包余额分", "钱包余额按分换算后的整数值。"},
|
||
{"wallet_snapshot.csv", "tx_version", "钱包版本号", "老系统钱包乐观锁版本。"},
|
||
{"wallet_snapshot.csv", "meal_auto_renew_status", "主套餐自动续费状态值", "老系统 tbl_card_wallet.meal_auto_renew_status。"},
|
||
{"wallet_snapshot.csv", "meal_auto_renew_status_name", "主套餐自动续费状态", "1 启用,2 关闭。"},
|
||
{"wallet_snapshot.csv", "auto_renew_meal_id", "主套餐自动续费套餐 ID", "老系统主套餐自动续费绑定的套餐 ID。"},
|
||
{"wallet_snapshot.csv", "addon_auto_renew_status", "叠加包自动续费状态值", "老系统 tbl_card_wallet.djb_meal_auto_renew_status。"},
|
||
{"wallet_snapshot.csv", "addon_auto_renew_status_name", "叠加包自动续费状态", "1 启用,2 关闭。"},
|
||
{"wallet_snapshot.csv", "addon_auto_renew_meal_id", "叠加包自动续费套餐 ID", "老系统叠加包自动续费绑定的套餐 ID。"},
|
||
{"wallet_snapshot.csv", "meal_auto_renew_updated_at", "自动续费更新时间", "老系统 tbl_card_wallet.meal_auto_renew_udpate_time。"},
|
||
{"wallet_snapshot.csv", "created_at", "钱包创建时间", "老系统 tbl_card_wallet.create_date。"},
|
||
{"commission_account_snapshot.csv", "old_agent_id", "老代理 ID", "老系统佣金账户 agent_id。"},
|
||
{"commission_account_snapshot.csv", "old_agent_name", "老代理名称", "老系统佣金账户 agent_name。"},
|
||
{"commission_account_snapshot.csv", "old_agent_login_account", "老代理登录账号", "老系统佣金账户 agent_login_account。"},
|
||
{"commission_account_snapshot.csv", "suggested_new_shop_id", "建议新店铺 ID", "预留给后续导入新系统时填写 shop_id。"},
|
||
{"commission_account_snapshot.csv", "total_commission_yuan", "累计佣金元", "老佣金账户累计佣金。"},
|
||
{"commission_account_snapshot.csv", "total_commission_fen", "累计佣金分", "累计佣金按分换算后的整数值。"},
|
||
{"commission_account_snapshot.csv", "total_active_award_commission_yuan", "累计激活奖励元", "老佣金账户累计激活奖励佣金。"},
|
||
{"commission_account_snapshot.csv", "total_active_award_commission_fen", "累计激活奖励分", "累计激活奖励按分换算后的整数值。"},
|
||
{"commission_account_snapshot.csv", "mf_commission_yuan", "秒返佣金元", "老佣金账户秒返佣金。"},
|
||
{"commission_account_snapshot.csv", "mf_commission_fen", "秒返佣金分", "秒返佣金按分换算后的整数值。"},
|
||
{"commission_account_snapshot.csv", "nomf_commission_yuan", "非秒返佣金元", "老佣金账户非秒返佣金。"},
|
||
{"commission_account_snapshot.csv", "nomf_commission_fen", "非秒返佣金分", "非秒返佣金按分换算后的整数值。"},
|
||
{"commission_account_snapshot.csv", "total_draw_yuan", "累计提现元", "老佣金账户累计提现金额。"},
|
||
{"commission_account_snapshot.csv", "total_draw_fen", "累计提现分", "累计提现金额按分换算后的整数值。"},
|
||
{"commission_account_snapshot.csv", "can_draw_yuan", "可提现金额元", "老佣金账户可提现余额。"},
|
||
{"commission_account_snapshot.csv", "can_draw_fen", "可提现金额分", "可提现余额按分换算后的整数值。"},
|
||
{"commission_account_snapshot.csv", "applying_draw_yuan", "提现中金额元", "老佣金账户申请提现中金额。"},
|
||
{"commission_account_snapshot.csv", "applying_draw_fen", "提现中金额分", "提现中金额按分换算后的整数值。"},
|
||
{"commission_account_snapshot.csv", "remark", "备注", "老佣金账户备注。"},
|
||
{"commission_account_snapshot.csv", "tx_version", "佣金账户版本号", "老佣金账户乐观锁版本。"},
|
||
{"commission_account_snapshot.csv", "created_at", "佣金账户创建时间", "老佣金账户 create_date。"},
|
||
{"commission_account_snapshot.csv", "updated_at", "佣金账户更新时间", "老佣金账户 update_date。"},
|
||
{"commission_detail_snapshot.csv", "id", "佣金明细 ID", "老系统佣金明细主键。"},
|
||
{"commission_detail_snapshot.csv", "commission_account_id", "佣金账户 ID", "老系统佣金账户 ID。"},
|
||
{"commission_detail_snapshot.csv", "wx_order_id", "微信订单 ID", "老系统微信订单 ID。"},
|
||
{"commission_detail_snapshot.csv", "wx_order_number", "微信订单号", "老系统微信订单号。"},
|
||
{"commission_detail_snapshot.csv", "iccid", "ICCID", "佣金明细关联的老卡 ICCID。"},
|
||
{"commission_detail_snapshot.csv", "phone", "手机号/卡号", "佣金明细记录的老系统 phone。"},
|
||
{"commission_detail_snapshot.csv", "wx_order_at", "微信订单时间", "老系统 wx_order_date。"},
|
||
{"commission_detail_snapshot.csv", "created_at", "佣金明细创建时间", "老系统 create_date。"},
|
||
{"commission_detail_snapshot.csv", "commission_amount_yuan", "本次佣金元", "本条佣金金额。"},
|
||
{"commission_detail_snapshot.csv", "commission_amount_fen", "本次佣金分", "本条佣金金额按分换算后的整数值。"},
|
||
{"commission_detail_snapshot.csv", "total_commission_amount_yuan", "累计佣金元", "该明细发生后的累计佣金金额。"},
|
||
{"commission_detail_snapshot.csv", "total_commission_amount_fen", "累计佣金分", "累计佣金金额按分换算后的整数值。"},
|
||
{"commission_detail_snapshot.csv", "mf_status", "秒返状态值", "老系统 tbl_agent_commission_detail.mf_status。"},
|
||
{"commission_detail_snapshot.csv", "mf_status_name", "秒返状态", "0 成功,1 失败。"},
|
||
{"commission_detail_snapshot.csv", "commission_type", "佣金类型值", "老系统 tbl_agent_commission_detail.commission_type。"},
|
||
{"commission_detail_snapshot.csv", "commission_type_name", "佣金类型", "commission_type 的中文含义。"},
|
||
{"commission_detail_snapshot.csv", "active_award_commission_amount_yuan", "本次激活奖励元", "本条激活奖励佣金金额。"},
|
||
{"commission_detail_snapshot.csv", "active_award_commission_amount_fen", "本次激活奖励分", "本条激活奖励佣金按分换算后的整数值。"},
|
||
{"commission_detail_snapshot.csv", "total_active_award_commission_amount_yuan", "累计激活奖励元", "该明细发生后的累计激活奖励佣金。"},
|
||
{"commission_detail_snapshot.csv", "total_active_award_commission_amount_fen", "累计激活奖励分", "累计激活奖励佣金按分换算后的整数值。"},
|
||
{"missing_cards.csv", "iccid", "缺失 ICCID", "输入中解析到但老系统 tbl_card 未命中的 ICCID。"},
|
||
}
|
||
|
||
return writeCSV(path, []string{"file_name", "column_name", "chinese_name", "description"}, func(w *csv.Writer) error {
|
||
for _, item := range fields {
|
||
if err := w.Write([]string{item.FileName, item.ColumnName, item.ChineseName, item.Description}); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
return nil
|
||
})
|
||
}
|
||
|
||
func writeEnumDictionary(path string, data *exportData) error {
|
||
enums := baseEnumDefinitions()
|
||
enums = append(enums, observedCategoryEnums(data)...)
|
||
|
||
return writeCSV(path, []string{"file_name", "column_name", "value", "meaning", "source", "note"}, func(w *csv.Writer) error {
|
||
for _, item := range enums {
|
||
if err := w.Write([]string{item.FileName, item.ColumnName, item.Value, item.Meaning, item.Source, item.Note}); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
return nil
|
||
})
|
||
}
|
||
|
||
func writeOutputGuide(path string, data *exportData) error {
|
||
packageCount := 0
|
||
for _, rows := range data.PackagesByICCID {
|
||
packageCount += len(rows)
|
||
}
|
||
|
||
content := strings.Builder{}
|
||
content.WriteString("# 老系统设备迁移导出说明\n\n")
|
||
content.WriteString(fmt.Sprintf("生成时间:%s\n\n", time.Now().Format(timeFormat)))
|
||
content.WriteString("## 优先查看\n\n")
|
||
content.WriteString("- `card_snapshot.csv`:卡本体、老卡状态、老套餐字段、当前已用量,以及新系统卡流量字段建议值。\n")
|
||
content.WriteString("- `package_usage_snapshot.csv`:老套餐记录、已用/剩余分摊建议,以及新系统套餐状态建议值。\n")
|
||
content.WriteString("- `field_dictionary.csv`:每个输出字段的中文说明。\n")
|
||
content.WriteString("- `enum_dictionary.csv`:状态码、类型码、来源表等枚举值说明。\n\n")
|
||
content.WriteString("## 本次摘要\n\n")
|
||
content.WriteString(fmt.Sprintf("- 已解析输入关系:%d 行\n", len(data.Inputs)))
|
||
content.WriteString(fmt.Sprintf("- 输入 ICCID:%d 个\n", len(data.InputByICCID)))
|
||
content.WriteString(fmt.Sprintf("- 老库卡本体命中:%d 张\n", len(data.Cards)))
|
||
content.WriteString(fmt.Sprintf("- 套餐快照:%d 行\n", packageCount))
|
||
content.WriteString(fmt.Sprintf("- 钱包快照:%d 行\n", len(data.Wallets)))
|
||
content.WriteString(fmt.Sprintf("- 佣金账户:%d 行\n", len(data.CommissionAccounts)))
|
||
content.WriteString(fmt.Sprintf("- 未解析输入:%d 行\n", len(data.UnresolvedInputs)))
|
||
content.WriteString(fmt.Sprintf("- 老库缺失 ICCID:%d 个\n\n", len(data.MissingICCIDs)))
|
||
content.WriteString("## 口径提醒\n\n")
|
||
content.WriteString("- 英文字段名保留给后续脚本处理,中文含义请看同表的 `*_name` 列和 `field_dictionary.csv`。\n")
|
||
content.WriteString("- 老系统状态值来自老库字段注释;新系统建议状态来自当前项目常量。\n")
|
||
content.WriteString("- `suggested_tb_iot_card_last_gateway_reading_mb` 是新系统轮询的基线,导入时要保留,避免首轮轮询重复计算历史流量。\n")
|
||
content.WriteString("- `allocated_used_mb` 是迁移建议值,正式生成导入 SQL 前仍要结合新系统供应商、套餐系列、套餐映射复核。\n")
|
||
return os.WriteFile(path, []byte(content.String()), 0644)
|
||
}
|
||
|
||
func baseEnumDefinitions() []enumDefinition {
|
||
var result []enumDefinition
|
||
appendMapEnums(&result, "card_snapshot.csv", "old_status", oldCardStatusNames, "老库 tbl_card.status 字段注释", "")
|
||
appendMapEnums(&result, "card_snapshot.csv", "old_type", oldCardTypeNames, "老库 tbl_card.type 字段注释", "")
|
||
appendMapEnums(&result, "package_usage_snapshot.csv", "source_table", map[string]string{
|
||
sourceCardLife: sourceTableName(sourceCardLife),
|
||
sourceNextMonthLife: sourceTableName(sourceNextMonthLife),
|
||
}, "脚本来源表约定", "")
|
||
appendMapEnums(&result, "package_usage_snapshot.csv", "old_status", oldCardLifeStatusNames, "老库 tbl_card_life.status 字段注释", "仅 source_table=tbl_card_life 时适用")
|
||
appendMapEnums(&result, "package_usage_snapshot.csv", "old_status", oldNextMonthLifeStatusNames, "老库 tbl_next_month_card_life.status 字段注释", "仅 source_table=tbl_next_month_card_life 时适用")
|
||
appendMapEnums(&result, "package_usage_snapshot.csv", "old_type", oldPackageTypeNames, "老库 tbl_card_life.type 字段注释", "")
|
||
appendMapEnums(&result, "package_usage_snapshot.csv", "effect_type", oldEffectTypeNames, "老库 tbl_card_life.effect_type 字段注释", "")
|
||
appendMapEnums(&result, "package_usage_snapshot.csv", "order_type", oldOrderTypeNames, "老库 tbl_next_month_card_life.order_type 字段注释", "")
|
||
appendMapEnums(&result, "package_usage_snapshot.csv", "suggested_new_status", newPackageUsageStatusNames, "新系统 pkg/constants PackageUsageStatus*", "建议导入值,生成 SQL 前需复核")
|
||
appendMapEnums(&result, "wallet_snapshot.csv", "meal_auto_renew_status", autoRenewStatusNames, "老库 tbl_card_wallet.meal_auto_renew_status 字段注释", "")
|
||
appendMapEnums(&result, "wallet_snapshot.csv", "addon_auto_renew_status", autoRenewStatusNames, "老库 tbl_card_wallet.djb_meal_auto_renew_status 字段注释", "")
|
||
appendMapEnums(&result, "commission_detail_snapshot.csv", "mf_status", mfStatusNames, "老库 tbl_agent_commission_detail.mf_status 字段注释", "")
|
||
appendMapEnums(&result, "commission_detail_snapshot.csv", "commission_type", commissionTypeNames, "老库 tbl_agent_commission_detail.commission_type 字段注释", "")
|
||
return result
|
||
}
|
||
|
||
func appendMapEnums(result *[]enumDefinition, fileName, columnName string, values map[string]string, source, note string) {
|
||
keys := sortedStringMapKeys(values)
|
||
for _, key := range keys {
|
||
*result = append(*result, enumDefinition{
|
||
FileName: fileName,
|
||
ColumnName: columnName,
|
||
Value: key,
|
||
Meaning: values[key],
|
||
Source: source,
|
||
Note: note,
|
||
})
|
||
}
|
||
}
|
||
|
||
func observedCategoryEnums(data *exportData) []enumDefinition {
|
||
seen := make(map[string]string)
|
||
for _, card := range data.Cards {
|
||
if card.Category == "" {
|
||
continue
|
||
}
|
||
seen[card.Category] = oldCardCategoryName(card.Category, card.CategoryName)
|
||
}
|
||
keys := sortedStringMapKeys(seen)
|
||
result := make([]enumDefinition, 0, len(keys))
|
||
for _, key := range keys {
|
||
result = append(result, enumDefinition{
|
||
FileName: "card_snapshot.csv",
|
||
ColumnName: "old_category",
|
||
Value: key,
|
||
Meaning: seen[key],
|
||
Source: "老库 tbl_vendor_category.category_name",
|
||
Note: "仅列出本次导出中实际出现的运营商分类",
|
||
})
|
||
}
|
||
return result
|
||
}
|
||
|
||
func oldCardStatusName(value string) string {
|
||
return enumName(oldCardStatusNames, value)
|
||
}
|
||
|
||
func oldCardTypeName(value string) string {
|
||
return enumName(oldCardTypeNames, value)
|
||
}
|
||
|
||
func oldCardCategoryName(value, dbName string) string {
|
||
if strings.TrimSpace(dbName) != "" {
|
||
return dbName
|
||
}
|
||
return enumName(oldCategoryNames, value)
|
||
}
|
||
|
||
func oldPackageStatusName(sourceTable, value string) string {
|
||
if sourceTable == sourceNextMonthLife {
|
||
return enumName(oldNextMonthLifeStatusNames, value)
|
||
}
|
||
return enumName(oldCardLifeStatusNames, value)
|
||
}
|
||
|
||
func oldPackageTypeName(value string) string {
|
||
return enumName(oldPackageTypeNames, value)
|
||
}
|
||
|
||
func oldEffectTypeName(value string) string {
|
||
return enumName(oldEffectTypeNames, value)
|
||
}
|
||
|
||
func oldOrderTypeName(value string) string {
|
||
return enumName(oldOrderTypeNames, value)
|
||
}
|
||
|
||
func autoRenewStatusName(value string) string {
|
||
return enumName(autoRenewStatusNames, value)
|
||
}
|
||
|
||
func newPackageUsageStatusName(value string) string {
|
||
return enumName(newPackageUsageStatusNames, value)
|
||
}
|
||
|
||
func mfStatusName(value string) string {
|
||
return enumName(mfStatusNames, value)
|
||
}
|
||
|
||
func commissionTypeName(value string) string {
|
||
return enumName(commissionTypeNames, value)
|
||
}
|
||
|
||
func sourceTableName(value string) string {
|
||
switch value {
|
||
case sourceCardLife:
|
||
return "当前/历史套餐记录"
|
||
case sourceNextMonthLife:
|
||
return "次月待生效套餐记录"
|
||
default:
|
||
return ""
|
||
}
|
||
}
|
||
|
||
func slotPositionName(slot int) string {
|
||
switch slot {
|
||
case 1:
|
||
return "主卡"
|
||
case 2:
|
||
return "副卡1"
|
||
case 3:
|
||
return "副卡2"
|
||
case 4:
|
||
return "卡槽4"
|
||
default:
|
||
return ""
|
||
}
|
||
}
|
||
|
||
func resolvedByName(value string) string {
|
||
switch {
|
||
case value == "input_iccid":
|
||
return "输入直接提供 ICCID"
|
||
case value == "input_slot_iccid":
|
||
return "输入按卡槽提供 ICCID"
|
||
case strings.Contains(value, "old_tbl_card_vcode") && strings.Contains(value, "old_tbl_card_relate"):
|
||
return "按设备码命中后展开关联卡"
|
||
case value == "old_tbl_card_vcode":
|
||
return "按老库 tbl_card.vcode 反查"
|
||
case value == "old_tbl_card_imei":
|
||
return "按老库 tbl_card.imei 反查"
|
||
case value == "old_tbl_card_relate":
|
||
return "按老库 tbl_card_relate 展开"
|
||
case value == "pending_device_key":
|
||
return "待按设备号/IMEI 解析"
|
||
case value == "unresolved":
|
||
return "无法解析"
|
||
case value == "unresolved_device_key":
|
||
return "设备号/IMEI 未命中老库"
|
||
default:
|
||
return ""
|
||
}
|
||
}
|
||
|
||
func enumName(values map[string]string, value string) string {
|
||
value = strings.TrimSpace(value)
|
||
if value == "" {
|
||
return ""
|
||
}
|
||
if name, ok := values[value]; ok {
|
||
return name
|
||
}
|
||
return "未知值"
|
||
}
|
||
|
||
func sortedStringMapKeys(values map[string]string) []string {
|
||
keys := make([]string, 0, len(values))
|
||
for key := range values {
|
||
keys = append(keys, key)
|
||
}
|
||
sort.Strings(keys)
|
||
return keys
|
||
}
|