// Package simcardquery 卡基础信息查询 与 月历史用量查询。 // // 两个功能都是「按号码批量查卡 → 异步导出」,共用同一套输入解析与导出列机制: // - 输入来源:Excel 模板导入 / 多行文本批量输入 / 连号号段; // - 号码识别:每行 1 个值(ICCID、MSISDN、IMSI 任意一种)或 3 个值(ICCID MSISDN IMSI), // 单值行在三列之间做 OR 匹配,因此不需要用户先选「用三码还是只用 ICCID」; // - 导出走通用异步导出中心,文件末尾固定带「查询结果」「失败原因」两列, // 未命中的输入也会出现在文件里,便于对账。 package simcardquery import ( "context" "errors" "fmt" "io" "math" "math/big" "strings" "sync" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchinput/excel" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchinput/text" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/exportcenter" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/handler" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/logic/cardflow" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/os/gtime" ) // 查询结果列取值,导出文件与执行日志共用同一套文案。 const ( queryResultHit = "成功" queryResultMiss = "失败" ) // chunkSize 单次 IN 查询的号码条数。 // 1000 是 MySQL 解析大量占位符仍能稳定走索引的量级,也和现有批量查询上限一致。 const chunkSize = 1000 type Service struct{} func New() *Service { return &Service{} } // inputLine 一行输入:可能是 1 个号码,也可能是 ICCID/MSISDN/IMSI 三码。 type inputLine struct { RowNo int Values []string } // cardBrief 查询命中的卡基础字段,按号码索引后复用。 type cardBrief struct { Id int64 `orm:"id"` Iccid string `orm:"iccid"` Msisdn string `orm:"msisdn"` Imsi string `orm:"imsi"` Imei string `orm:"imei"` UsedFlowMb float64 `orm:"used_flow_mb"` PackageTotalMb float64 `orm:"query_total_flow_mb"` CardStatus int `orm:"boss_card_status"` NetModel int `orm:"net_model"` ChanKey string `orm:"chan_key"` ActivateAt *gtime.Time `orm:"boss_activate_at"` OutStockTime *gtime.Time `orm:"out_stock_time"` ExpireAt *gtime.Time `orm:"flow_pkg_end_at"` OutboundBatch string `orm:"outbound_batch_no"` InboundBatch string `orm:"inbound_batch_no"` PackageCode string `orm:"current_package_code"` DeptId int64 `orm:"dept_id"` Remark string `orm:"remark"` } // ExcelInput 上传文件的读取器工厂。 // 每次调用返回新的读取器,保证后台解析时读到完整内容。 type ExcelInput func() (io.Reader, error) // ExportBase 卡基础信息查询导出。 func (s *Service) ExportBase(ctx context.Context, in *model.SimCardQueryExportParam, input ExcelInput) (*exportcenter.SubmitResult, error) { lines, err := parseInputs(ctx, in, input) if err != nil { return nil, err } columns, err := resolveBaseColumns(in) if err != nil { return nil, err } stats := &resolveStats{} once := &sync.Once{} var cached []*model.SimCardBaseQueryRow var cacheErr error resolve := func(ctx context.Context) ([]*model.SimCardBaseQueryRow, error) { once.Do(func() { if in.Mode == model.SimCardQueryModeRange { cached, cacheErr = s.resolveRangeRows(ctx, in, stats) return } cached, cacheErr = s.resolveBaseRows(ctx, lines, stats) }) return cached, cacheErr } return exportcenter.Submit(ctx, &exportcenter.Request{ BizKey: "tSimCardBaseQuery", BizName: "卡基础信息查询", PermCode: "cpm:iot:tSimCardBaseQuery:view", SheetName: "卡基础信息", QueryParam: baseQuerySnapshot(in, len(lines)), Columns: columns, Tags: func() ([]string, error) { return baseDefaultTags(), nil }, Fetch: func(ctx context.Context, page, pageSize int) ([]interface{}, int, error) { // exportcenter 提交时会用 pageSize=1 做一次「总数探针」。 // 直接返回输入行数(号段模式未知条数,返回 1 占位),让提交接口立即返回, // 真正的查库放到后台协程里做。 if pageSize <= 1 && page <= 1 { if len(lines) > 0 { return nil, len(lines), nil } // 号段模式:号段包含多少个号码就是预计条数,与「查不查得到」无关。 // 这样用户输入 10 个连续号,即使一条都查不到,页面也会显示「预计 10 条」。 size, sizeErr := rangeSize(in) if sizeErr != nil { return nil, 0, sizeErr } return nil, int(size), nil } rows, err := resolve(ctx) if err != nil { return nil, 0, err } total := len(rows) start := (page - 1) * pageSize if start >= total { return nil, total, nil } end := start + pageSize if end > total { end = total } out := make([]interface{}, 0, end-start) for _, row := range rows[start:end] { out = append(out, row) } return out, total, nil }, Result: stats.resultSummary, }) } // parseInputs 按模式解析输入号码。 func parseInputs(ctx context.Context, in *model.SimCardQueryExportParam, input ExcelInput) ([]inputLine, error) { switch in.Mode { case model.SimCardQueryModeInput: return parseTextInputs(ctx, in.BatchInput) case model.SimCardQueryModeExcel: if input == nil { return nil, gerror.New("请先上传 Excel 文件") } reader, err := input() if err != nil { return nil, err } if reader == nil { return nil, gerror.New("请先上传 Excel 文件") } lines, err := streamExcelInputs(ctx, reader) if err != nil { return nil, err } if len(lines) == 0 { return nil, gerror.New("Excel 中没有可解析的号码") } return lines, nil case model.SimCardQueryModeRange: // 号段模式不展开成逐个号码:连号可能跨上百万个号,展开会把内存打满。 // 只做校验,实际取数交给 resolveRangeRows 用 BETWEEN 条件分页查。 return nil, parseRangeInputs(in) default: return nil, gerror.New("不支持的查询方式") } } // parseRangeInputs 校验连号号段参数。 func parseRangeInputs(in *model.SimCardQueryExportParam) error { start := strings.TrimSpace(in.StartNum) end := strings.TrimSpace(in.EndNum) if start == "" || end == "" { return gerror.New("连号查询需同时填写起始和结束号码") } if len(start) != len(end) || !isDigits(start) || !isDigits(end) { return gerror.New("起止号码必须为等长数字") } if start > end { return gerror.New("起始号码不能大于结束号码") } _, err := rangeColumn(in.Dimension) if err != nil { return err } // 号段按号码逐个输出(查到出行数据、没查到出失败行),条数必须与号段大小一致。 // 因此号段不能超过单次上限,否则无法完整展开。 size, err := rangeSize(in) if err != nil { return err } if size > model.SimCardQueryMaxRows { return gerror.Newf("号段内号码数量超过单次上限 %d 个,请缩小范围或分批查询", model.SimCardQueryMaxRows) } return nil } // parseTextInputs 解析多行文本。每行 1~3 个空白分隔的号码值。 func isDigits(value string) bool { if value == "" { return false } for _, r := range value { if r < '0' || r > '9' { return false } } return true } func parseTextInputs(ctx context.Context, input string) ([]inputLine, error) { lines, err := text.ParseWhitespaceFields(ctx, input) if err != nil { if errors.Is(err, text.ErrEmptyInput) { return nil, gerror.New("批量输入内容不能为空") } return nil, err } if len(lines) > model.SimCardQueryMaxRows { return nil, gerror.Newf("批量输入 %d 行,超过单次上限 %d 行,请拆分后分批查询", len(lines), model.SimCardQueryMaxRows) } out := make([]inputLine, 0, len(lines)) for _, line := range lines { if len(line.Fields) > 3 { return nil, gerror.Newf("第 %d 行格式错误:每行支持 1 至 3 个号码值", line.RowNo) } out = append(out, inputLine{RowNo: line.RowNo, Values: line.Fields}) } return out, nil } // streamExcelInputs 流式解析 Excel 上传。ICCID / MSISDN / IMSI 三列任填其一即可。 func streamExcelInputs(ctx context.Context, reader io.Reader) ([]inputLine, error) { out := make([]inputLine, 0) _, err := excel.StreamReader(ctx, reader, excel.ParseOptions{ SingleSheet: true, MaxDataRows: model.SimCardQueryMaxRows, }, func(row excel.ParsedRow) error { values := make([]string, 0, 3) for _, name := range []string{"ICCID", "MSISDN", "IMSI"} { if value := excel.Field(row, name); value != "" { values = append(values, value) } } if len(values) == 0 { // 空行由解析库跳过;这里保留为失败行,便于用户知道模板里哪一行没填。 values = append(values, "") } out = append(out, inputLine{RowNo: row.RowNo, Values: values}) return nil }) if err != nil { return nil, err } if len(out) == 0 { return nil, gerror.New("Excel 中没有可解析的号码") } if len(out) > model.SimCardQueryMaxRows { return nil, gerror.Newf("Excel 有效数据 %d 行,超过单次上限 %d 行,请拆分后分批查询", len(out), model.SimCardQueryMaxRows) } return out, nil } // resolveStats 记录命中/未命中/去重数量,用于导出结束后的执行日志汇总。 type resolveStats struct { mu sync.Mutex hit int miss int dup int } func (s *resolveStats) add(hit bool) { s.mu.Lock() defer s.mu.Unlock() if hit { s.hit++ return } s.miss++ } // addDup 记录一条因重复而合并的输入。 // 同一张卡可能是同一号码写了两遍,也可能一行 ICCID、另一行 MSISDN 指向同一张卡。 func (s *resolveStats) addDup() { s.mu.Lock() defer s.mu.Unlock() s.dup++ } func (s *resolveStats) resultSummary(_ int) (int, string) { s.mu.Lock() defer s.mu.Unlock() total := s.hit + s.miss if total == 0 { return consts.BatchRunResultFailed, "查询结果:没有可导出的数据" } dupText := "" if s.dup > 0 { dupText = fmt.Sprintf(",重复 %d 条已合并", s.dup) } if s.miss == 0 { return consts.BatchRunResultSuccess, fmt.Sprintf("查询完成:成功 %d 条,失败 0 条%s", s.hit, dupText) } if s.hit == 0 { return consts.BatchRunResultFailed, fmt.Sprintf("查询失败:成功 0 条,失败 %d 条%s;失败原因见导出文件「失败原因」列", s.miss, dupText) } return consts.BatchRunResultPartialSuccess, fmt.Sprintf("部分成功:成功 %d 条,失败 %d 条%s;失败原因见导出文件「失败原因」列", s.hit, s.miss, dupText) } // resolveBaseRows 把输入号码解析成导出行,未命中的行也保留并写明原因。 func (s *Service) resolveBaseRows(ctx context.Context, lines []inputLine, stats *resolveStats) ([]*model.SimCardBaseQueryRow, error) { values := collectValues(lines) cards, err := queryCardsByValues(ctx, values) if err != nil { return nil, err } byIccid := indexCards(cards, func(card *cardBrief) string { return card.Iccid }) byMsisdn := indexCards(cards, func(card *cardBrief) string { return card.Msisdn }) byImsi := indexCards(cards, func(card *cardBrief) string { return card.Imsi }) display := newDisplayResolver(ctx, cards) rows := make([]*model.SimCardBaseQueryRow, 0, len(lines)) // 同一张卡在输入里出现多次时只导出一次: // 可能是同一号码重复填写,也可能一行是 ICCID、另一行是它的 MSISDN。 exported := make(map[int64]struct{}, len(lines)) for _, line := range lines { row := &model.SimCardBaseQueryRow{} card, reason := matchCard(line, byIccid, byMsisdn, byImsi) if card == nil { // 未命中时把输入的第一列原样回填,方便用户定位是哪一条没查到。 row.Iccid = firstValue(line.Values) row.QueryResult = queryResultMiss row.FailReason = inputFailure(line, reason) stats.add(false) rows = append(rows, row) continue } if _, ok := exported[card.Id]; ok { stats.addDup() continue } exported[card.Id] = struct{}{} display.fill(row, card) row.QueryResult = queryResultHit stats.add(true) rows = append(rows, row) } return rows, nil } // resolveRangeRows 号段模式:按起止号段分页取卡,取到的卡全部视为命中。 // 取满上限即停,避免上百万号的号段把导出任务拖死。 func (s *Service) resolveRangeRows(ctx context.Context, in *model.SimCardQueryExportParam, stats *resolveStats) ([]*model.SimCardBaseQueryRow, error) { cards, err := queryCardsByRange(ctx, in) if err != nil { return nil, err } return buildRangeRows(ctx, in, cards, stats), nil } // setRangeNumber 按号段维度把号码写进对应列。 func setRangeNumber(row *model.SimCardBaseQueryRow, dimension, number string) { switch dimension { case "msisdn": row.Msisdn = number case "imsi": row.Imsi = number default: row.Iccid = number } } // rangeCardIndex 按号段维度值索引命中的卡。 func rangeCardIndex(cards []*cardBrief, dimension string) map[string]*cardBrief { index := make(map[string]*cardBrief, len(cards)) for _, card := range cards { var value string switch dimension { case "msisdn": value = card.Msisdn case "imsi": value = card.Imsi default: value = card.Iccid } if value != "" { index[value] = card } } return index } // buildRangeRows 号段模式:按号段逐个号码输出一行,命中出行数据、未命中出失败行。 // // 不能只输出命中的卡:号段里可能一部分在当前账号可见范围内、一部分不在, // 只输出命中部分会让导出条数与号段大小对不上,客户无法按号对账。 func buildRangeRows(ctx context.Context, in *model.SimCardQueryExportParam, cards []*cardBrief, stats *resolveStats) []*model.SimCardBaseQueryRow { dimension := strings.TrimSpace(in.Dimension) index := rangeCardIndex(cards, dimension) size, err := rangeSize(in) if err != nil { size = int64(len(cards)) } numbers := rangeNumbers(in, size) display := newDisplayResolver(ctx, cards) rows := make([]*model.SimCardBaseQueryRow, 0, len(numbers)) for _, number := range numbers { if card, ok := index[number]; ok { row := &model.SimCardBaseQueryRow{QueryResult: queryResultHit} display.fill(row, card) stats.add(true) rows = append(rows, row) continue } row := &model.SimCardBaseQueryRow{ QueryResult: queryResultMiss, FailReason: rangeMissReason, } setRangeNumber(row, dimension, number) stats.add(false) rows = append(rows, row) } return rows } // rangeMissReason 号段/号码查不到数据时的统一说明。 // 不能写「无权访问」:是否因为权限被过滤属于内部口径,不应暴露给客户。 const rangeMissReason = "未查询到数据" // rangeSize 号段包含的号码个数,用于提交时给出「预计 N 条」。 // ICCID 有 20 位,超过 int64 表示范围,用 big.Int 计算。 func rangeSize(in *model.SimCardQueryExportParam) (int64, error) { start := strings.TrimSpace(in.StartNum) end := strings.TrimSpace(in.EndNum) s, ok := new(big.Int).SetString(start, 10) if !ok { return 0, gerror.New("起始号码格式不正确") } e, ok := new(big.Int).SetString(end, 10) if !ok { return 0, gerror.New("结束号码格式不正确") } if e.Cmp(s) < 0 { return 0, gerror.New("起始号码不能大于结束号码") } span := new(big.Int).Sub(e, s) span.Add(span, big.NewInt(1)) if !span.IsInt64() { return math.MaxInt64, nil } return span.Int64(), nil } // padNumber 保持与输入等长,避免前导零被 big.Int 吃掉后与用户输入对不上。 func padNumber(num string, width int) string { if len(num) >= width { return num } return strings.Repeat("0", width-len(num)) + num } // rangeNumbers 按号段依次产出号码,最多 limit 个。 func rangeNumbers(in *model.SimCardQueryExportParam, limit int64) []string { start := strings.TrimSpace(in.StartNum) s, ok := new(big.Int).SetString(start, 10) if !ok { return nil } out := make([]string, 0, limit) cur := new(big.Int).Set(s) for i := int64(0); i < limit; i++ { out = append(out, padNumber(cur.String(), len(start))) cur.Add(cur, big.NewInt(1)) } return out } // rangeColumn 号段维度对应的表字段。 // 只允许三码维度,避免把任意字段拼进 SQL。 func rangeColumn(dimension string) (string, error) { columns := dao.TSimCardInfo.Columns() switch strings.TrimSpace(dimension) { case "iccid": return columns.Iccid, nil case "msisdn": return columns.Msisdn, nil case "imsi": return columns.Imsi, nil default: return "", gerror.New("连号维度只能选 ICCID、MSISDN 或 IMSI") } } // queryCardsByRange 按号段取卡;命中超过上限时拒绝,避免静默截断。 func queryCardsByRange(ctx context.Context, in *model.SimCardQueryExportParam) ([]*cardBrief, error) { column, err := rangeColumn(in.Dimension) if err != nil { return nil, err } columns := dao.TSimCardInfo.Columns() result := make([]*cardBrief, 0) for start := 0; start < model.SimCardQueryMaxRows; start += chunkSize { list := make([]*cardBrief, 0, chunkSize) err := handler.Model(dao.TSimCardInfo.Ctx(ctx), &handler.Option{ FilterTenant: true, FilterAuth: true, }). Fields( columns.Id, columns.Iccid, columns.Msisdn, columns.Imsi, columns.Imei, columns.UsedFlowMb, columns.PackageTotalFlowMb, columns.BossCardStatus, columns.NetModel, columns.ChanKey, columns.BossActivateAt, columns.OutStockTime, columns.FlowPkgEndAt, columns.OutboundBatchNo, columns.InboundBatchNo, columns.CurrentPackageCode, columns.DeptId, columns.Remark, ). Fields(cardflow.TotalFlowExpr(dao.TSimCardInfo.Table(), "")+" AS query_total_flow_mb"). Where(column+" >= ?", strings.TrimSpace(in.StartNum)). Where(column+" <= ?", strings.TrimSpace(in.EndNum)). Where(column+" != ?", ""). OrderAsc(columns.Id). Limit(chunkSize).Offset(start). Scan(&list) if err != nil { return nil, gerror.Wrap(err, "查询卡信息失败,请稍后重试") } result = append(result, list...) if len(list) < chunkSize { break } if len(result) == model.SimCardQueryMaxRows { var extra int extra, err = handler.Model(dao.TSimCardInfo.Ctx(ctx), &handler.Option{ FilterTenant: true, FilterAuth: true, }).Where(column+" >= ?", strings.TrimSpace(in.StartNum)). Where(column+" <= ?", strings.TrimSpace(in.EndNum)). Where(column+" != ?", "").Count() if err != nil { return nil, gerror.Wrap(err, "校验号段查询上限失败") } if extra > model.SimCardQueryMaxRows { return nil, gerror.Newf("号段命中超过单次上限 %d 张卡,请缩小范围", model.SimCardQueryMaxRows) } } } return result, nil } type cardIndex map[string][]*cardBrief func indexCards(cards []*cardBrief, field func(*cardBrief) string) cardIndex { index := make(cardIndex, len(cards)) for _, card := range cards { if card != nil { if value := field(card); value != "" { index[value] = append(index[value], card) } } } return index } // matchCard 对每个号码保留全部候选卡,再求交集;重复 IMSI 和跨字段冲突不按顺序择卡。 func matchCard(line inputLine, byIccid, byMsisdn, byImsi cardIndex) (*cardBrief, string) { var candidates map[int64]*cardBrief for _, value := range line.Values { value = strings.TrimSpace(value) matches := make(map[int64]*cardBrief) for _, index := range []cardIndex{byIccid, byMsisdn, byImsi} { for _, card := range index[value] { matches[card.Id] = card } } if value == "" || len(matches) == 0 { return nil, "未查询到数据:" + value } if candidates == nil { candidates = matches continue } for id := range candidates { if _, ok := matches[id]; !ok { delete(candidates, id) } } } if len(candidates) == 0 { return nil, "同一行内的号码未指向同一张卡" } if len(candidates) > 1 { return nil, "号码匹配到多张卡,请补充同一卡的 ICCID/MSISDN/IMSI 消除歧义" } for _, card := range candidates { return card, "" } return nil, rangeMissReason } func inputFailure(line inputLine, reason string) string { return fmt.Sprintf("第 %d 行,原始输入 [%s]:%s", line.RowNo, strings.Join(line.Values, " | "), reason) } func firstValue(values []string) string { if len(values) == 0 { return "" } return values[0] } // collectValues 汇总需要查库的号码值,去重后返回。 func collectValues(lines []inputLine) []string { seen := make(map[string]struct{}, len(lines)*3) out := make([]string, 0, len(lines)*3) for _, line := range lines { for _, value := range line.Values { value = strings.TrimSpace(value) if value == "" { continue } if _, ok := seen[value]; ok { continue } seen[value] = struct{}{} out = append(out, value) } } return out } // queryCardsByValues 分批按号码值查卡。 // 单值在三列之间 OR 匹配(iccid 唯一索引 + msisdn 唯一索引 + imsi 索引), // 因此用户不需要提前声明用的是 ICCID 还是三码。 func queryCardsByValues(ctx context.Context, values []string) ([]*cardBrief, error) { if len(values) == 0 { return nil, nil } columns := dao.TSimCardInfo.Columns() cardTable := dao.TSimCardInfo.Table() result := make([]*cardBrief, 0, len(values)) seen := make(map[int64]struct{}, len(values)) for start := 0; start < len(values); start += chunkSize { end := start + chunkSize if end > len(values) { end = len(values) } chunk := values[start:end] placeholders := strings.TrimSuffix(strings.Repeat("?,", len(chunk)), ",") condition := fmt.Sprintf("(%s.%s IN (%s) OR %s.%s IN (%s) OR %s.%s IN (%s))", cardTable, columns.Iccid, placeholders, cardTable, columns.Msisdn, placeholders, cardTable, columns.Imsi, placeholders, ) args := make([]interface{}, 0, len(chunk)*3) for repeat := 0; repeat < 3; repeat++ { for _, value := range chunk { args = append(args, value) } } list := make([]*cardBrief, 0, len(chunk)) err := handler.Model(dao.TSimCardInfo.Ctx(ctx), &handler.Option{ FilterTenant: true, FilterAuth: true, }). Fields( columns.Id, columns.Iccid, columns.Msisdn, columns.Imsi, columns.Imei, columns.UsedFlowMb, columns.PackageTotalFlowMb, columns.BossCardStatus, columns.NetModel, columns.ChanKey, columns.BossActivateAt, columns.OutStockTime, columns.FlowPkgEndAt, columns.OutboundBatchNo, columns.InboundBatchNo, columns.CurrentPackageCode, columns.DeptId, columns.Remark, ). Fields(cardflow.TotalFlowExpr(cardTable, "")+" AS query_total_flow_mb"). Where(condition, args...). Scan(&list) if err != nil { return nil, gerror.Wrap(err, "查询卡信息失败,请稍后重试") } for _, card := range list { if _, ok := seen[card.Id]; ok { continue } seen[card.Id] = struct{}{} result = append(result, card) } } return result, nil } // displayResolver 把 chan_key / package_code / dept_id 批量翻译成展示名,避免逐行子查询。 type displayResolver struct { chanNames map[string]string packageNames map[string]string packageFlows map[string]float64 deptNames map[string]string } func newDisplayResolver(ctx context.Context, cards []*cardBrief) *displayResolver { r := &displayResolver{ chanNames: make(map[string]string), packageNames: make(map[string]string), packageFlows: make(map[string]float64), deptNames: make(map[string]string), } if len(cards) == 0 { return r } chanKeys := make([]string, 0) packageCodes := make([]string, 0) deptIds := make([]int64, 0) seenChan := map[string]struct{}{} seenPackage := map[string]struct{}{} seenDept := map[int64]struct{}{} for _, card := range cards { if card.ChanKey != "" { if _, ok := seenChan[card.ChanKey]; !ok { seenChan[card.ChanKey] = struct{}{} chanKeys = append(chanKeys, card.ChanKey) } } if card.PackageCode != "" { if _, ok := seenPackage[card.PackageCode]; !ok { seenPackage[card.PackageCode] = struct{}{} packageCodes = append(packageCodes, card.PackageCode) } } if card.DeptId > 0 { if _, ok := seenDept[card.DeptId]; !ok { seenDept[card.DeptId] = struct{}{} deptIds = append(deptIds, card.DeptId) } } } if len(chanKeys) > 0 { type chanRow struct { ChanKey string `orm:"chan_key"` Operator int `orm:"operator"` } rows := make([]*chanRow, 0, len(chanKeys)) if err := dao.TSimChannels.Ctx(ctx). Fields("chan_key, operator"). WhereIn("chan_key", chanKeys). Scan(&rows); err == nil { for _, row := range rows { switch row.Operator { case 1: r.chanNames[row.ChanKey] = "中国移动" case 2: r.chanNames[row.ChanKey] = "中国联通" case 3: r.chanNames[row.ChanKey] = "中国电信" default: r.chanNames[row.ChanKey] = "未知" } } } } if len(packageCodes) > 0 { type pkgRow struct { PackageCode string `orm:"package_code"` PackageName string `orm:"package_name"` FlowMb float64 `orm:"flow_mb"` } rows := make([]*pkgRow, 0, len(packageCodes)) if err := dao.TSimFlowPackageInfo.Ctx(ctx). Fields("package_code, package_name, flow_mb"). WhereIn("package_code", packageCodes). Scan(&rows); err == nil { for _, row := range rows { r.packageNames[row.PackageCode] = row.PackageName r.packageFlows[row.PackageCode] = row.FlowMb } } } if len(deptIds) > 0 { type deptRow struct { DeptId int64 `orm:"dept_id"` DeptName string `orm:"dept_name"` } rows := make([]*deptRow, 0, len(deptIds)) if err := dao.SysDept.Ctx(ctx). Fields("dept_id, dept_name"). WhereIn("dept_id", deptIds). WhereNull("deleted_at"). Scan(&rows); err == nil { for _, row := range rows { r.deptNames[fmt.Sprintf("%d", row.DeptId)] = row.DeptName } } } return r } // fill 把库里字段落到导出行。 func (r *displayResolver) fill(row *model.SimCardBaseQueryRow, card *cardBrief) { row.Iccid = card.Iccid row.Msisdn = card.Msisdn row.Imsi = card.Imsi row.Imei = card.Imei row.UsedFlowMb = roundMb(card.UsedFlowMb) row.CardStatus = card.CardStatus row.NetModel = card.NetModel row.Operators = r.chanNames[card.ChanKey] row.OutboundBatch = card.OutboundBatch row.InboundBatch = card.InboundBatch row.PackageName = r.packageNames[card.PackageCode] row.DeptName = r.deptNames[fmt.Sprintf("%d", card.DeptId)] row.Remark = card.Remark row.ActivateAt = card.ActivateAt row.OutStockTime = card.OutStockTime row.ExpireAt = card.ExpireAt // 复用 cardflow.TotalFlowExpr:当前主套餐(或卡冗余值)加生效中的单卡加油包。 row.PackageTotalMb = roundMb(card.PackageTotalMb) remain := card.PackageTotalMb - card.UsedFlowMb if remain < 0 { remain = 0 } row.RemainFlowMb = roundMb(remain) }