sync: 同步 v1.0.4 并脱敏配置与镜像
This commit is contained in:
@@ -0,0 +1,330 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user