Files

858 lines
27 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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)
}