chore: 初始开源发布
iotlcm-openapi:对外 OpenAPI 网关(V3 接口 + 旧版兼容接口)。
- Go module:iotlcm-openapi → git.5iot.com/iotlcm-public/iotlcm-openapi(178 文件)
- 凭据与地址脱敏:JWT signingKey、MySQL DSN、Redis 口令、
RabbitMQ DSN 全部改为 ${*} 占位;私网 IP → 文档地址段
- 7 处真实手机号 → 合法 11 位编造号
- 启动 banner 统一 IOTLCM 方块字 + 社区惯例署名
- 移除内部资产:CI 触发器
- Dockerfile 改用公共基础镜像,移除 GOPRIVATE 内网设置
- README 重写(原为 GoFrame 模板占位文案),补 Apache-2.0 LICENSE,
LF 行尾 + .gitattributes
This commit is contained in:
@@ -0,0 +1,312 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user