package query import ( "context" "strings" "github.com/gogf/gf/v2/errors/gerror" "git.5iot.com/iotlcm-public/iotlcm-openapi/internal/consts" "git.5iot.com/iotlcm-public/iotlcm-openapi/internal/dao" "git.5iot.com/iotlcm-public/iotlcm-openapi/internal/model" "git.5iot.com/iotlcm-public/iotlcm-openapi/internal/model/entity" "git.5iot.com/iotlcm-public/iotlcm-openapi/internal/service" ) // querySimBatchMaxCards 批量查询单次的卡数量上限(与 QueryTrafficCutoffConfig 同量级)。 // 纯本地库读、不调上游,故偏保守;如需更大块,改这一个常量即可(注意 IN 列表与报文大小)。 const querySimBatchMaxCards = 1000 // simBatchIdentifierMaxLen 单标识允许的最大字符数,对齐 t_sim_card_info 列宽(varchar(32))并留余量, // 用于把请求体内存占用钉死在可控范围(1000 张 * 3 字段 * 64 ≈ 192KB),防止超长字符串滥用。 const simBatchIdentifierMaxLen = 64 type simBatchIdentifier struct { Msisdn string Iccid string Imsi string } func validateSimBatchIdentifier(item simBatchIdentifier) error { count := 0 if item.Msisdn != "" { if len(item.Msisdn) > simBatchIdentifierMaxLen { return gerror.New("msisdn长度超限") } count++ } if item.Iccid != "" { if len(item.Iccid) > simBatchIdentifierMaxLen { return gerror.New("iccid长度超限") } count++ } if item.Imsi != "" { if len(item.Imsi) > simBatchIdentifierMaxLen { return gerror.New("imsi长度超限") } count++ } if count == 0 { return gerror.New("msisdn、iccid、imsi至少传一个") } return nil } // simBatchCardMatchesIdentifier 校验返回的卡是否同时匹配输入里提供的所有标识。 // 防止单条输入同时填了多个互相矛盾的标识(如 iccid 属于 A、msisdn 属于 B)时, // 仍返回优先级最高那个标识对应卡的数据。沿用 traffic_cutoff_config.go 的同名校验逻辑。 func simBatchCardMatchesIdentifier(card *entity.TSimCardInfo, identifier simBatchIdentifier) bool { if card == nil { return false } if identifier.Iccid != "" && strings.TrimSpace(card.Iccid) != identifier.Iccid { return false } if identifier.Msisdn != "" && strings.TrimSpace(card.Msisdn) != identifier.Msisdn { return false } if identifier.Imsi != "" && strings.TrimSpace(card.Imsi) != identifier.Imsi { return false } return true } func simBatchIdentifierKey(item simBatchIdentifier) string { if item.Iccid != "" { return "iccid:" + item.Iccid } if item.Msisdn != "" { return "msisdn:" + item.Msisdn } return "imsi:" + item.Imsi } // parseSimBatchIdentifiers 校验并去重,返回按 iccid/msisdn/imsi 分桶的标识列表。 func parseSimBatchIdentifiers(cards []model.QuerySimBatchCard) ([]simBatchIdentifier, error) { identifiers := make([]simBatchIdentifier, 0, len(cards)) seen := make(map[string]struct{}, len(cards)) for _, item := range cards { identifier := simBatchIdentifier{ Msisdn: strings.TrimSpace(item.Msisdn), Iccid: strings.TrimSpace(item.Iccid), Imsi: strings.TrimSpace(item.Imsi), } if err := validateSimBatchIdentifier(identifier); err != nil { return nil, err } key := simBatchIdentifierKey(identifier) if _, ok := seen[key]; ok { return nil, gerror.Newf("卡片标识重复:%s", key) } seen[key] = struct{}{} identifiers = append(identifiers, identifier) } return identifiers, nil } // loadSimBatchCards 按标识分桶做 WhereIn 查询(自动带部门数据权限),返回 type:key -> 卡 的映射。 func loadSimBatchCards(ctx context.Context, identifiers []simBatchIdentifier) (map[string]*entity.TSimCardInfo, error) { columns := dao.TSimCardInfo.Columns() result := make(map[string]*entity.TSimCardInfo, len(identifiers)) lookup := func(field string, values []string) error { if len(values) == 0 { return nil } var cards []*entity.TSimCardInfo err := service.IotTSimCardInfo().Model(ctx). Fields(columns.Iccid, columns.Msisdn, columns.Imsi, columns.BossCardStatus, columns.UsedFlowMb, columns.Imei). WhereIn(field, values).Scan(&cards) if err != nil { return gerror.Wrap(err, "批量查询卡信息失败") } for _, card := range cards { if card == nil { continue } if card.Iccid != "" { result["iccid:"+strings.TrimSpace(card.Iccid)] = card } if card.Msisdn != "" { result["msisdn:"+strings.TrimSpace(card.Msisdn)] = card } if card.Imsi != "" { result["imsi:"+strings.TrimSpace(card.Imsi)] = card } } return nil } iccidValues, msisdnValues, imsiValues := make([]string, 0), make([]string, 0), make([]string, 0) for _, item := range identifiers { switch { case item.Iccid != "": iccidValues = append(iccidValues, item.Iccid) case item.Msisdn != "": msisdnValues = append(msisdnValues, item.Msisdn) default: imsiValues = append(imsiValues, item.Imsi) } } if err := lookup(columns.Iccid, iccidValues); err != nil { return nil, err } if err := lookup(columns.Msisdn, msisdnValues); err != nil { return nil, err } if err := lookup(columns.Imsi, imsiValues); err != nil { return nil, err } return result, nil } // QuerySimStatusBatch 批量查询卡状态(纯读本地库,不调上游通道)。 // 返回结构与单卡 QuerySimStatus 一致(iccid / lifeCycle 内部码 / lifeCycleName), // 但按输入顺序逐卡返回,未匹配项用 Matched=false + errorCode 标记。 func (s *sQuerySim) QuerySimStatusBatch(ctx context.Context, in *model.QuerySimStatusBatchParam) (*model.QuerySimStatusBatchOutput, error) { if in == nil { return nil, gerror.New("查询参数不能为空") } if strings.TrimSpace(in.AppId) == "" { return nil, gerror.New("appId不能为空") } if len(in.Cards) == 0 { return nil, gerror.New("查询卡不能为空") } if len(in.Cards) > querySimBatchMaxCards { return nil, gerror.Newf("单次最多查询%d张卡", querySimBatchMaxCards) } identifiers, err := parseSimBatchIdentifiers(in.Cards) if err != nil { return nil, err } cardMap, err := loadSimBatchCards(ctx, identifiers) if err != nil { return nil, err } output := &model.QuerySimStatusBatchOutput{ Total: len(identifiers), Items: make([]model.QuerySimStatusBatchItem, 0, len(identifiers)), } for _, identifier := range identifiers { result := model.QuerySimStatusBatchItem{} card := cardMap[simBatchIdentifierKey(identifier)] if card == nil || !simBatchCardMatchesIdentifier(card, identifier) { result.ErrorCode = "CARD_NOT_FOUND" result.ErrorMessage = "卡片不存在或无权限" output.NotFoundCount++ output.Items = append(output.Items, result) continue } result.Matched = true result.Iccid = strings.TrimSpace(card.Iccid) statusName, ok := consts.V3ApiSimBossCardStatusNameMap[card.BossCardStatus] if !ok { statusName = "未知" } result.LifeCycle = card.BossCardStatus result.LifeCycleName = statusName output.MatchedCount++ output.Items = append(output.Items, result) } return output, nil } // QuerySimUsageBatch 批量查询卡流量(used_flow_mb,纯读本地库)。 func (s *sQuerySim) QuerySimUsageBatch(ctx context.Context, in *model.QuerySimUsageBatchParam) (*model.QuerySimUsageBatchOutput, error) { if in == nil { return nil, gerror.New("查询参数不能为空") } if strings.TrimSpace(in.AppId) == "" { return nil, gerror.New("appId不能为空") } if len(in.Cards) == 0 { return nil, gerror.New("查询卡不能为空") } if len(in.Cards) > querySimBatchMaxCards { return nil, gerror.Newf("单次最多查询%d张卡", querySimBatchMaxCards) } identifiers, err := parseSimBatchIdentifiers(in.Cards) if err != nil { return nil, err } cardMap, err := loadSimBatchCards(ctx, identifiers) if err != nil { return nil, err } output := &model.QuerySimUsageBatchOutput{ Total: len(identifiers), Items: make([]model.QuerySimUsageBatchItem, 0, len(identifiers)), } for _, identifier := range identifiers { result := model.QuerySimUsageBatchItem{} card := cardMap[simBatchIdentifierKey(identifier)] if card == nil || !simBatchCardMatchesIdentifier(card, identifier) { result.ErrorCode = "CARD_NOT_FOUND" result.ErrorMessage = "卡片不存在或无权限" output.NotFoundCount++ output.Items = append(output.Items, result) continue } result.Matched = true result.Iccid = strings.TrimSpace(card.Iccid) result.UseUnit = "MB" result.UseAmount = card.UsedFlowMb output.MatchedCount++ output.Items = append(output.Items, result) } return output, nil } // QuerySimImeiBatch 批量查询卡 IMEI(纯读本地库,不调上游通道)。 // // 数据来源是 t_sim_card_info.imei 快照,由 worker 的 IMEI 全量同步链路维护; // 另有一个补充来源:单卡 /openapi/v3/query/sim-imei 在库值为空时会实时调运营商 // 并回写(不写 imei_sync_time,只写 imei_update_time)。 // // 语义要点: // - imei 为空**不算 notFound**:卡存在但上游从未查到值(如不支持的模板通道)。 // - 本接口**不触发任何上游请求**,是纯本地库读(与 sim-status-batch 一致)。 func (s *sQuerySim) QuerySimImeiBatch(ctx context.Context, in *model.QuerySimImeiBatchParam) (*model.QuerySimImeiBatchOutput, error) { if in == nil { return nil, gerror.New("查询参数不能为空") } if strings.TrimSpace(in.AppId) == "" { return nil, gerror.New("appId不能为空") } if len(in.Cards) == 0 { return nil, gerror.New("查询卡不能为空") } if len(in.Cards) > querySimBatchMaxCards { return nil, gerror.Newf("单次最多查询%d张卡", querySimBatchMaxCards) } identifiers, err := parseSimBatchIdentifiers(in.Cards) if err != nil { return nil, err } cardMap, err := loadSimBatchCards(ctx, identifiers) if err != nil { return nil, err } output := &model.QuerySimImeiBatchOutput{ Total: len(identifiers), Items: make([]model.QuerySimImeiBatchItem, 0, len(identifiers)), } for _, identifier := range identifiers { result := model.QuerySimImeiBatchItem{} card := cardMap[simBatchIdentifierKey(identifier)] if card == nil || !simBatchCardMatchesIdentifier(card, identifier) { result.ErrorCode = "CARD_NOT_FOUND" result.ErrorMessage = "卡片不存在或无权限" output.NotFoundCount++ output.Items = append(output.Items, result) continue } result.Matched = true result.Iccid = strings.TrimSpace(card.Iccid) result.Imei = strings.TrimSpace(card.Imei) output.MatchedCount++ output.Items = append(output.Items, result) } return output, nil }