Files
iotlcm/server/internal/logic/iot/simcardquery/columns.go
T
2026-10-29 14:36:49 +08:00

331 lines
11 KiB
Go

package simcardquery
import (
"context"
"math"
"strings"
"time"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility/iot_excelize"
"github.com/gogf/gf/v2/encoding/gjson"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/xuri/excelize/v2"
)
// 导出列白名单。
//
// 只放行白名单内的字段:前端传什么就导出什么,但传了名单外的字段一律忽略,
// 避免误把内部字段(如 id、tenantId)带进对客户的导出文件。
var baseColumnWhitelist = map[string]model.ExportColumn{
"iccid": {
Field: "iccid", Label: "ICCID",
},
"msisdn": {
Field: "msisdn", Label: "MSISDN",
},
"imsi": {
Field: "imsi", Label: "IMSI",
},
"imei": {
Field: "imei", Label: "IMEI",
},
"usedFlowMb": {
Field: "usedFlowMb", Label: "当月用量(MB)",
},
"remainFlowMb": {
Field: "remainFlowMb", Label: "当月剩余用量(MB)",
},
"packageTotalMb": {
Field: "packageTotalMb", Label: "套餐总流量(MB)",
},
"cardStatus": {
Field: "cardStatus", Label: "卡状态", DictType: "t_boss_card_status",
},
"netModel": {
Field: "netModel", Label: "网络类型", DictType: "t_net_model",
},
"operators": {
Field: "operators", Label: "运营商",
},
"activateAt": {
Field: "activateAt", Label: "激活时间",
},
"outStockTime": {
Field: "outStockTime", Label: "出库时间",
},
"expireAt": {
Field: "expireAt", Label: "到期时间",
},
"outboundBatch": {
Field: "outboundBatch", Label: "出库批次",
},
"inboundBatch": {
Field: "inboundBatch", Label: "入库批次",
},
"packageName": {
Field: "packageName", Label: "当前套餐名称",
},
"deptName": {
Field: "deptName", Label: "所属企业",
},
"remark": {
Field: "remark", Label: "备注",
},
"extraRemark": {
Field: "extraRemark", Label: "其他备注",
},
}
// baseDefaultOrder 未勾选列时的默认导出顺序,与卡信息列表的常用列保持一致。
var baseDefaultOrder = []string{
"iccid", "msisdn", "imsi", "imei", "usedFlowMb", "remainFlowMb", "packageTotalMb",
"cardStatus", "netModel", "operators", "activateAt", "outStockTime", "expireAt",
"outboundBatch", "inboundBatch", "packageName", "deptName", "remark", "extraRemark",
}
// 查询结果与失败原因固定追加在末尾,保证「部分成功」时每条失败可追溯。
const (
queryResultField = "queryResult"
failReasonField = "failReason"
)
func baseDefaultTags() []string {
tags := make([]string, 0, len(baseDefaultOrder)+2)
for _, field := range baseDefaultOrder {
tags = append(tags, baseColumnWhitelist[field].Label)
}
return append(tags, "查询结果", "失败原因")
}
// baseMandatoryOrder 三码固定导出列。
// 这三列是对账的锚点,文件里没有 ICCID/MSISDN/IMSI 就无法定位到卡,
// 因此既不允许前端取消勾选,也不允许服务端被传参绕过。
var baseMandatoryOrder = []string{"iccid", "msisdn", "imsi"}
// resolveBaseColumns 归一化前端传入的导出列:三码强制在前,其余去重后按白名单追加,并固定追加结果列。
func resolveBaseColumns(in *model.SimCardQueryExportParam) ([]model.ExportColumn, error) {
requested := decodeExportColumns(in)
columns := make([]model.ExportColumn, 0, len(requested)+5)
seen := make(map[string]struct{}, len(requested)+len(baseMandatoryOrder))
for _, field := range baseMandatoryOrder {
seen[field] = struct{}{}
columns = append(columns, baseColumnWhitelist[field])
}
for _, column := range requested {
field := strings.TrimSpace(column.Field)
allowed, ok := baseColumnWhitelist[field]
if !ok {
continue
}
if _, dup := seen[field]; dup {
continue
}
seen[field] = struct{}{}
// 表头以服务端白名单为准,忽略前端自定义文案,避免导出列与页面口径不一致。
columns = append(columns, allowed)
}
// 未勾选任何可选列时,把剩余默认列补齐;三码已在前面,不会重复。
if len(columns) == len(baseMandatoryOrder) {
for _, field := range baseDefaultOrder {
if _, dup := seen[field]; dup {
continue
}
seen[field] = struct{}{}
columns = append(columns, baseColumnWhitelist[field])
}
}
// 结果列不允许被去掉:部分成功时用户必须能看到每条失败的原因。
columns = append(columns,
model.ExportColumn{Field: queryResultField, Label: "查询结果"},
model.ExportColumn{Field: failReasonField, Label: "失败原因"},
)
return columns, nil
}
// decodeExportColumns 解析前端传入的导出列。
// 优先解析 JSON 字符串(表单提交时数组套对象会被拆散),失败再退回对象数组。
func decodeExportColumns(in *model.SimCardQueryExportParam) []model.ExportColumn {
if in == nil {
return nil
}
if raw := strings.TrimSpace(in.ExportColumnsJson); raw != "" {
cols := make([]model.ExportColumn, 0)
if err := gjson.New(raw).Scan(&cols); err == nil && len(cols) > 0 {
return cols
}
}
return in.ExportColumns
}
// baseQuerySnapshot 落库的筛选条件快照。
// 只留模式、条数和前若干条样本,不存全量号码,避免 query_params 膨胀。
func baseQuerySnapshot(in *model.SimCardQueryExportParam, inputRows int) g.Map {
snapshot := g.Map{
"mode": in.Mode,
"inputRows": inputRows,
}
switch in.Mode {
case model.SimCardQueryModeRange:
snapshot["dimension"] = in.Dimension
snapshot["startNum"] = in.StartNum
snapshot["endNum"] = in.EndNum
case model.SimCardQueryModeInput:
snapshot["batchInputPreview"] = previewText(in.BatchInput, 5)
}
return snapshot
}
func previewText(text string, limit int) []string {
lines := strings.Split(strings.ReplaceAll(text, "\r\n", "\n"), "\n")
out := make([]string, 0, limit)
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" {
continue
}
out = append(out, line)
if len(out) >= limit {
break
}
}
return out
}
// roundMb 统一保留 3 位小数,与库表 decimal(18,3) 精度一致。
func roundMb(value float64) float64 {
return math.Round(value*1000) / 1000
}
// usageHistoryWhitelist 月历史用量查询允许勾选的固定列。
// 月份列是动态生成的,直接按月份值作 Field,不走白名单。
var usageHistoryWhitelist = map[string]model.ExportColumn{
"iccid": {Field: "iccid", Label: "ICCID"},
"msisdn": {Field: "msisdn", Label: "MSISDN"},
"imsi": {Field: "imsi", Label: "IMSI"},
"outStockTime": {Field: "outStockTime", Label: "出库时间"},
"expireAt": {Field: "expireAt", Label: "到期时间"},
}
// usageHistoryOptionalFields 月历史用量里可勾选的固定列(三码固定导出,不在此列)。
var usageHistoryOptionalFields = []string{"outStockTime", "expireAt"}
// resolveUsageHistoryColumns 生成月历史用量导出列。
// 三码固定在最前且不可取消;月份列固定跟随导出口径,不允许前端自行增删。
func resolveUsageHistoryColumns(in *model.SimCardUsageHistoryExportParam, months []string) []model.ExportColumn {
requested := decodeExportColumns(&in.SimCardQueryExportParam)
allowed := make(map[string]struct{}, len(requested))
for _, column := range requested {
field := strings.TrimSpace(column.Field)
if _, ok := usageHistoryWhitelist[field]; ok {
allowed[field] = struct{}{}
}
}
columns := make([]model.ExportColumn, 0, len(usageHistoryFixedFields)+len(months)+2)
for _, field := range baseMandatoryOrder {
columns = append(columns, usageHistoryWhitelist[field])
}
// 未勾选任何可选列时按默认全出,命中筛选时按固定顺序保留。
if len(allowed) == 0 {
for _, field := range usageHistoryOptionalFields {
columns = append(columns, usageHistoryWhitelist[field])
}
} else {
for _, field := range usageHistoryOptionalFields {
if _, ok := allowed[field]; ok {
columns = append(columns, usageHistoryWhitelist[field])
}
}
}
for _, month := range months {
columns = append(columns, model.ExportColumn{Field: month, Label: month})
}
columns = append(columns,
model.ExportColumn{Field: queryResultField, Label: "查询结果"},
model.ExportColumn{Field: failReasonField, Label: "失败原因"},
)
return columns
}
func usageHistoryTags(months []string) []string {
tags := make([]string, 0, len(usageHistoryFixedFields)+len(months)+2)
for _, field := range usageHistoryFixedFields {
tags = append(tags, usageHistoryWhitelist[field].Label)
}
tags = append(tags, months...)
return append(tags, "查询结果", "失败原因")
}
func usageHistorySnapshot(in *model.SimCardUsageHistoryExportParam, months []string, rows int) g.Map {
snapshot := g.Map{
"mode": in.Mode,
"months": months,
"inputRows": rows,
}
if in.Mode == model.SimCardQueryModeDept {
snapshot["deptId"] = in.DeptId
}
return snapshot
}
// UsageHistoryTemplate 生成月历史用量查询模板(与卡基础信息查询同一份模板口径)。
func (s *Service) UsageHistoryTemplate(ctx context.Context) (*excelize.File, string, error) {
return s.BaseTemplate(ctx)
}
// BaseColumnOptions 卡基础信息查询可选导出列,供前端渲染勾选框。
func (s *Service) BaseColumnOptions() []model.ExportColumn {
options := make([]model.ExportColumn, 0, len(baseDefaultOrder))
for _, field := range baseDefaultOrder {
options = append(options, baseColumnWhitelist[field])
}
return options
}
// UsageHistoryColumnOptions 月历史用量查询可选固定列(不含动态月份列)。
func (s *Service) UsageHistoryColumnOptions() []model.ExportColumn {
options := make([]model.ExportColumn, 0, len(usageHistoryFixedFields))
for _, field := range usageHistoryFixedFields {
options = append(options, usageHistoryWhitelist[field])
}
return options
}
// UsageHistoryMonths 当前导出口径的月份清单,前端用它预览列顺序。
func (s *Service) UsageHistoryMonths(now time.Time) []string {
return LastCompleteMonths(now, usageHistoryMonths)
}
// BaseTemplate 生成卡基础信息查询模板。
// 三列都允许为空,但至少填一个;表头与导入解析的 RequiredHeaders 保持一致。
func (s *Service) BaseTemplate(ctx context.Context) (*excelize.File, string, error) {
f := excelize.NewFile()
sheet := "卡基础信息查询"
index, err := f.NewSheet(sheet)
if err != nil {
return nil, "", gerror.Wrap(err, "创建模板工作表失败")
}
f.SetActiveSheet(index)
if err := f.DeleteSheet("Sheet1"); err != nil {
return nil, "", gerror.Wrap(err, "清理默认工作表失败")
}
headers := []string{"ICCID", "MSISDN", "IMSI"}
for i, header := range headers {
cell, _ := excelize.CoordinatesToCellName(i+1, 1)
if err = f.SetCellValue(sheet, cell, header); err != nil {
return nil, "", gerror.Wrap(err, "写入模板表头失败")
}
}
if err = f.SetColWidth(sheet, "A", "C", 26); err != nil {
return nil, "", gerror.Wrap(err, "设置模板列宽失败")
}
if err = iot_excelize.ApplyHeaderColumnsTextStyle(f); err != nil {
g.Log().Warningf(ctx, "apply template style failed: %v", err)
}
return f, "卡基础信息查询模板", nil
}