Files
iotlcm/server/internal/logic/iot/t_sim_card_info.go
T
2026-10-29 14:36:49 +08:00

1065 lines
39 KiB
Go
Raw 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 gen
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2026 LiXiujie
// @Author Lxj <xxx@example.com>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
// @AutoGenerate Version
package iot
import (
"context"
"errors"
"fmt"
"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/text"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/exportcenter"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/handler"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/hook"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/logic/cardflow"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/logic/iot/simcardbatch/trafficcutoffconfig"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/do"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility/convert"
"strings"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/encoding/gjson"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
)
type sIotTSimCardInfo struct{}
const (
simFlowInternalErrorCode = 9999
simFlowUpstreamUnavailableMessage = "同步卡流量失败,请稍后重试!"
// 移动 OneLink 在自身繁忙时会对逐卡查询随机返回该状态,实测占比接近 50%,
// 属上游侧瞬时状态,与我方系统无关,因此对用户展示为上游服务的提示。
simUpstreamBusyCode = 13000
simUpstreamBusyMessage = "上游系统服务忙,请稍后再试"
simUpstreamBusyForbiddenMessage = "上游系统禁止访问,请稍后再试"
)
func NewIotTSimCardInfo() *sIotTSimCardInfo {
return &sIotTSimCardInfo{}
}
func init() {
service.RegisterIotTSimCardInfo(NewIotTSimCardInfo())
}
// Model 卡信息表ORM模型
func (s *sIotTSimCardInfo) Model(ctx context.Context, option ...*handler.Option) *gdb.Model {
if len(option) == 0 {
option = append(option, &handler.Option{
FilterTenant: true,
FilterAuth: true,
})
}
return handler.Model(dao.TSimCardInfo.Ctx(ctx), option...)
}
// List 获取卡信息表列表
func (s *sIotTSimCardInfo) List(ctx context.Context, in *model.TSimCardInfoListParam) (list []*model.TSimCardInfoListModel, totalCount int, err error) {
var batchCondition string
var batchArgs []interface{}
if in.BatchInput != nil {
batchCondition, batchArgs, err = simCardBatchCondition(ctx, *in.BatchInput)
if err != nil {
return nil, 0, err
}
}
mod := s.Model(ctx)
cardInfoTable := dao.TSimCardInfo.Table()
simChannels := dao.TSimChannels.Table()
sysDept := dao.SysDept.Table()
flowPackage := dao.TSimFlowPackageInfo.Table()
BatchMain := dao.BatchMain.Table()
// 字段过滤
mod = mod.FieldsPrefix(cardInfoTable, model.TSimCardInfoListModel{})
mod = mod.Fields(fmt.Sprintf(
"COALESCE(NULLIF(%s.%s, ''), %s.%s) as chanName, %s.%s as chanAlias",
simChannels,
dao.TSimChannels.Columns().ChanName,
cardInfoTable,
dao.TSimCardInfo.Columns().ChanKey,
simChannels,
dao.TSimChannels.Columns().ChanAlias,
))
// 部门名称改用标量子查询,不再 JOIN sys_dept。
// 原因:sys_dept 带 deleted_at 软删除字段,LEFT JOIN 后框架会把 `sys_dept.deleted_at IS NULL`
// 追加到 WHERE,使优化器对 sys_dept 选用 hash join;hash join 不保证输出行序,
// 导致 `ORDER BY id DESC` 无法借索引序提前终止,只能对全表 19 万行 filesort(生产实测 7.3s)。
// 标量子查询只对 LIMIT 后的 20 行求值,开销可忽略。
mod = mod.Fields(fmt.Sprintf(
"(SELECT d.%s FROM %s AS d WHERE d.%s = %s.%s AND d.%s IS NULL) as deptName",
dao.SysDept.Columns().DeptName,
sysDept,
dao.SysDept.Columns().DeptId,
cardInfoTable,
dao.TSimCardInfo.Columns().DeptId,
dao.SysDept.Columns().DeletedAt,
))
mod = mod.Fields(fmt.Sprintf(
"COALESCE(NULLIF(%s.%s, ''), %s.%s) as packageName",
flowPackage,
dao.TSimFlowPackageInfo.Columns().PackageName,
cardInfoTable,
dao.TSimCardInfo.Columns().CurrentPackageCode,
))
mod = mod.Fields(cardflow.RemainFlowExpr(cardInfoTable, flowPackage) + " as reminFlowMb")
if in.DeptId != 0 {
mod = mod.Where(dao.TSimCardInfo.Columns().DeptId, in.DeptId)
}
if in.CardValue != "" {
cardValueCondition := "(" + dao.TSimCardInfo.Columns().Msisdn + " = ? OR " +
dao.TSimCardInfo.Columns().Iccid + " = ? OR " +
dao.TSimCardInfo.Columns().Imsi + " = ?)"
mod = mod.Where(cardValueCondition, in.CardValue, in.CardValue, in.CardValue)
}
if batchCondition != "" {
mod = mod.Where(batchCondition, batchArgs...)
}
if in.Remark != "" {
mod = mod.Where(dao.TSimCardInfo.Columns().Remark+" LIKE ?", "%"+in.Remark+"%")
}
if in.Imei != "" {
mod = mod.Where(dao.TSimCardInfo.Columns().Imei, in.Imei)
}
if len(in.TrafficCutoffStatus) > 0 {
mod = mod.WhereIn(dao.TSimCardInfo.Columns().TrafficCutoffStatus, in.TrafficCutoffStatus)
}
// 查询运营商卡状态
if in.BossCardStatus != nil {
mod = mod.Where(dao.TSimCardInfo.Columns().BossCardStatus, in.BossCardStatus)
}
// 查询时间类型
if in.TimeType != nil {
switch *in.TimeType {
case 0: // 开卡时间
mod = mod.WhereBetween(dao.TSimCardInfo.Columns().BossOpenAt, in.Time[0], in.Time[1])
case 1: // 激活时间
mod = mod.WhereBetween(dao.TSimCardInfo.Columns().BossActivateAt, in.Time[0], in.Time[1])
case 2: //入库时间
mod = mod.LeftJoin(BatchMain, BatchMain+"."+dao.BatchMain.Columns().SourceBatchNo+"="+cardInfoTable+"."+dao.TSimCardInfo.Columns().InboundBatchNo)
mod = mod.WhereBetween(BatchMain+"."+dao.BatchMain.Columns().StartTime, in.Time[0], in.Time[1])
case 3: //出库时间
mod = mod.LeftJoin(BatchMain, BatchMain+"."+dao.BatchMain.Columns().SourceBatchNo+"="+cardInfoTable+"."+dao.TSimCardInfo.Columns().OutboundBatchNo)
mod = mod.WhereBetween(BatchMain+"."+dao.BatchMain.Columns().StartTime, in.Time[0], in.Time[1])
case 4: //到期时间
mod = mod.WhereBetween(dao.TSimCardInfo.Columns().FlowPkgEndAt, in.Time[0], in.Time[1])
case 5: //创建时间
mod = mod.WhereBetween(dao.TSimCardInfo.Columns().CreatedAt, in.Time[0], in.Time[1])
case 6: //流量同步时间
mod = mod.WhereBetween(dao.TSimCardInfo.Columns().FlowSyncTime, in.Time[0], in.Time[1])
case 7: //状态同步时间
mod = mod.WhereBetween(dao.TSimCardInfo.Columns().StatusSyncTime, in.Time[0], in.Time[1])
default:
mod = mod.WhereBetween(dao.TSimCardInfo.Columns().BossOpenAt, in.Time[0], in.Time[1])
}
}
// 查询查询条件
if in.Condition != nil {
switch *in.Condition {
case "msisdn": // MSISDN
mod = mod.Where(dao.TSimCardInfo.Columns().Msisdn+" >= ?", in.StartNum).
Where(dao.TSimCardInfo.Columns().Msisdn+" <= ?", in.EndNum)
case "iccid": // ICCID
mod = mod.Where(dao.TSimCardInfo.Columns().Iccid+" >= ?", in.StartNum).
Where(dao.TSimCardInfo.Columns().Iccid+" <= ?", in.EndNum)
case "imis": // IMSI
mod = mod.Where(dao.TSimCardInfo.Columns().Imsi+" >= ?", in.StartNum).
Where(dao.TSimCardInfo.Columns().Imsi+" <= ?", in.EndNum)
case "imei": // IMEI
mod = mod.Where(dao.TSimCardInfo.Columns().Imei+" >= ?", in.StartNum).
Where(dao.TSimCardInfo.Columns().Imei+" <= ?", in.EndNum)
}
}
// 查询维度
if in.Dimension != nil && in.DimensionStatus != nil && in.DimensionValue != nil {
switch *in.Dimension {
case 1: // 使用流量
mod = mod.Where(dao.TSimCardInfo.Columns().UsedFlowMb + *in.DimensionStatus + *in.DimensionValue)
case 2: // 剩余流量
mod = mod.Where(cardflow.RemainFlowExpr(cardInfoTable, flowPackage) + *in.DimensionStatus + *in.DimensionValue)
}
}
// 查询排序值
if in.SortValue != nil && in.SortType != nil {
var orderStr string
switch *in.SortValue {
case 1: // iccid
orderStr = dao.TSimCardInfo.Columns().Iccid
case 2: // msisdn
orderStr = dao.TSimCardInfo.Columns().Msisdn
case 3: // imsi
orderStr = dao.TSimCardInfo.Columns().Imsi
case 4: // 流量用量
orderStr = dao.TSimCardInfo.Columns().UsedFlowMb
case 5: // 激活日期
orderStr = dao.TSimCardInfo.Columns().BossActivateAt
case 6: // 流量到期日期
orderStr = dao.TSimCardInfo.Columns().FlowPkgEndAt
case 7: // 状态同步日期
orderStr = dao.TSimCardInfo.Columns().StatusSyncTime
case 8: // 出库时间
orderStr = dao.TSimCardInfo.Columns().OutStockTime
}
if *in.SortType == 0 {
mod = mod.Order(orderStr + " ASC")
} else {
mod = mod.Order(orderStr + " DESC")
}
}
// 查询通道编号
if in.ChanKey != nil {
mod = mod.Where(dao.TSimCardInfo.Columns().ChanKey, *in.ChanKey)
}
// 查询当前套餐编码
if in.CurrentPackageCode != nil {
mod = mod.Where(dao.TSimCardInfo.Columns().CurrentPackageCode, *in.CurrentPackageCode)
}
// 查询IMEI
if in.Imei != "" {
mod = mod.Where(dao.TSimCardInfo.Columns().Imei, in.Imei)
}
// 分页
mod = mod.Page(in.Page, in.PageSize)
// 排序
mod = mod.OrderDesc(dao.TSimCardInfo.Columns().Id)
// 部门软删除过滤改用反连接(NOT EXISTS),不再 JOIN sys_dept。
// 语义与原 LEFT JOIN + WHERE dept.deleted_at IS NULL 完全等价(已被软删除的部门下的卡同样不返回),
// 但不会引入 hash join,优化器得以按主键逆序扫描并在取满 20 行后立即终止。
mod = mod.Where(fmt.Sprintf(
"NOT EXISTS (SELECT 1 FROM %s AS d WHERE d.%s = %s.%s AND d.%s IS NOT NULL)",
sysDept,
dao.SysDept.Columns().DeptId,
cardInfoTable,
dao.TSimCardInfo.Columns().DeptId,
dao.SysDept.Columns().DeletedAt,
))
mod = mod.LeftJoin(simChannels, simChannels+"."+dao.TSimChannels.Columns().ChanKey+"="+cardInfoTable+"."+dao.TSimCardInfo.Columns().ChanKey+" AND "+simChannels+"."+dao.TSimChannels.Columns().TenantId+"="+cardInfoTable+"."+dao.TSimCardInfo.Columns().TenantId)
mod = mod.LeftJoin(flowPackage, flowPackage+"."+dao.TSimFlowPackageInfo.Columns().PackageCode+"="+cardInfoTable+"."+dao.TSimCardInfo.Columns().CurrentPackageCode+" AND "+flowPackage+"."+dao.TSimFlowPackageInfo.Columns().TenantId+"="+cardInfoTable+"."+dao.TSimCardInfo.Columns().TenantId)
// 操作人摘要信息
mod = mod.Hook(hook.MemberSummary)
// 查询数据
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
err = gerror.Wrap(err, "获取卡信息表列表失败,请稍后重试!")
return
}
applyCardChannelDisplayNames(ctx, list)
if resolver, resolverErr := newCardDeptDisplayResolver(ctx); resolverErr != nil {
logCardDeptDisplayResolverError(ctx, resolverErr)
} else {
for _, item := range list {
if item != nil {
item.DeptName = resolver(item.DeptId, item.DeptName)
}
}
}
return
}
// exportColumns 取前端传入的导出列。
// 优先解析 JSON 字符串形式(数组绑定可能丢失内容),失败再退回对象数组。
func (s *sIotTSimCardInfo) exportColumns(in *model.TSimCardInfoListParam) []model.ExportColumn {
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
}
const simCardBatchMaxRows = 1000
// simCardBatchCondition 将批量查询输入编译为查询条件。
// 每行支持 1 至 3 个号码值,同一行的多个值表示同一张卡的多个字段值。
// 同一行的值互不相同,必然落在不同字段上,因此“该行每个值都命中这张卡的某个字段”
// 已等价于“这些值都属于这张卡”,无需判断某个值具体是 ICCID、MSISDN 还是 IMSI。
func simCardBatchCondition(ctx context.Context, input string) (string, []interface{}, error) {
lines, err := text.ParseWhitespaceFields(ctx, input)
if errors.Is(err, text.ErrEmptyInput) {
return "", nil, gerror.New("批量查询卡号不能为空")
}
if err != nil {
return "", nil, err
}
if len(lines) > simCardBatchMaxRows {
return "", nil, gerror.New("批量查询最多支持1000行")
}
columns := dao.TSimCardInfo.Columns()
// 单值行之间相互独立,合并成 IN 列表后语义不变,可显著降低条件求值成本。
singleValues := make([]string, 0, len(lines))
singleIndex := make(map[string]struct{}, len(lines))
// 多值行需要保留同一行内的配对关系,不能跨行合并。
multiConditions := make([]string, 0, len(lines))
multiArgs := make([]interface{}, 0, len(lines)*9)
multiIndex := make(map[string]struct{}, len(lines))
for _, line := range lines {
switch len(line.Fields) {
case 1:
value := line.Fields[0]
if _, exists := singleIndex[value]; exists {
continue
}
singleIndex[value] = struct{}{}
singleValues = append(singleValues, value)
case 2, 3:
key := strings.Join(line.Fields, "\x00")
if _, exists := multiIndex[key]; exists {
continue
}
multiIndex[key] = struct{}{}
groups := make([]string, 0, len(line.Fields))
for _, value := range line.Fields {
groups = append(groups, "("+columns.Iccid+" = ? OR "+
columns.Msisdn+" = ? OR "+
columns.Imsi+" = ?)")
multiArgs = append(multiArgs, value, value, value)
}
multiConditions = append(multiConditions, "("+strings.Join(groups, " AND ")+")")
default:
return "", nil, gerror.Newf("第%d行格式错误,每行支持 1 至 3 个号码值", line.RowNo)
}
}
parts := make([]string, 0, 2)
args := make([]interface{}, 0, len(singleValues)*3+len(multiArgs))
if len(singleValues) > 0 {
placeholders := "(" + strings.TrimSuffix(strings.Repeat("?,", len(singleValues)), ",") + ")"
parts = append(parts, "("+columns.Iccid+" IN "+placeholders+" OR "+
columns.Msisdn+" IN "+placeholders+" OR "+
columns.Imsi+" IN "+placeholders+")")
for repeat := 0; repeat < 3; repeat++ {
for _, value := range singleValues {
args = append(args, value)
}
}
}
if len(multiConditions) > 0 {
parts = append(parts, "("+strings.Join(multiConditions, " OR ")+")")
args = append(args, multiArgs...)
}
return "(" + strings.Join(parts, " OR ") + ")", args, nil
}
// Export 导出卡信息(异步:提交任务后到下载中心取文件)
// 导出的列由前端按当前列表显示列传入,未传时按列表模型字段顺序全量导出。
func (s *sIotTSimCardInfo) Export(ctx context.Context, in *model.TSimCardInfoListParam) (err error) {
if in.BatchInput != nil {
if _, _, err = simCardBatchCondition(ctx, *in.BatchInput); err != nil {
return err
}
}
_, err = exportcenter.Submit(ctx, &exportcenter.Request{
BizKey: "tSimCardInfo",
BizName: "卡信息",
PermCode: "cpm:iot:tSimCardInfo:export",
QueryParam: in,
Columns: s.exportColumns(in),
Tags: func() ([]string, error) {
return convert.GetEntityDescTags(model.TSimCardInfoListModel{})
},
Fetch: func(ctx context.Context, page, pageSize int) (rows []interface{}, total int, err error) {
query := *in
query.Page = page
query.PageSize = pageSize
list, count, err := s.List(ctx, &query)
if err != nil {
return nil, 0, err
}
rows = make([]interface{}, 0, len(list))
for _, item := range list {
rows = append(rows, item)
}
return rows, count, nil
},
})
return err
}
// Edit 修改/新增卡信息表
func (s *sIotTSimCardInfo) Edit(ctx context.Context, in *model.TSimCardInfoEditParam) (err error) {
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
if err = s.validateTrafficCutoffConfig(ctx, tx, in); err != nil {
return
}
// 修改
if in.Id > 0 {
userId := contexts.GetUserId(ctx)
in.UpdatedBy = &userId
if in.TrafficCutoffStatus != nil && *in.TrafficCutoffStatus != 1 {
if _, err = tx.Ctx(ctx).Model(dao.TSimCardInfo.Table()).
Where(dao.TSimCardInfo.Columns().Id, in.Id).
Data(do.TSimCardTrafficCutoffConfig{
TrafficCutoffCrossMonthHold: 0,
TrafficCutoffTriggeredAt: gdb.Raw("NULL"),
UpdatedBy: userId,
}).Update(); err != nil {
return
}
}
if _, err = s.Model(ctx).
Fields(model.TSimCardInfoUpdateFields{}).
WherePri(in.Id).
Data(in).
OmitNil().
Update(); err != nil {
g.Log().Error(ctx, "修改卡信息表失败", "id", in.Id, "error", err)
err = gerror.Wrap(err, "修改卡信息表失败,请稍后重试!")
}
return
}
// 新增
// 验证'Iccid'唯一
if err = xgorm.IsUnique(ctx, &dao.TSimCardInfo, g.Map{dao.TSimCardInfo.Columns().Iccid: in.Iccid}, "ICCID已存在", in.Id); err != nil {
return
}
// 验证'Msisdn'唯一
if err = xgorm.IsUnique(ctx, &dao.TSimCardInfo, g.Map{dao.TSimCardInfo.Columns().Msisdn: in.Msisdn}, "MSISDN/卡号已存在", in.Id); err != nil {
}
deptId := contexts.GetDeptId(ctx)
in.CreatedDept = &deptId
userId := contexts.GetUserId(ctx)
in.CreatedBy = &userId
in.CreatedAt = gtime.Now()
tenantId := contexts.GetTenantId(ctx)
in.TenantId = &tenantId
if _, err = s.Model(ctx, &handler.Option{FilterAuth: false}).
Fields(model.TSimCardInfoInsertFields{}).
Data(in).Insert(); err != nil {
err = gerror.Wrap(err, "新增卡信息表失败,请稍后重试!")
}
return
})
}
func (s *sIotTSimCardInfo) validateTrafficCutoffConfig(ctx context.Context, tx gdb.TX, in *model.TSimCardInfoEditParam) error {
if in == nil || (in.TrafficCutoffStatus == nil && in.TrafficCutoffThreshold == nil) {
return nil
}
if in.TrafficCutoffStatus != nil && *in.TrafficCutoffStatus == 0 && in.TrafficCutoffThreshold == nil {
threshold := float64(0)
in.TrafficCutoffThreshold = &threshold
}
var current struct {
Status int `orm:"traffic_cutoff_status"`
Threshold float64 `orm:"traffic_cutoff_threshold"`
}
if in.Id > 0 {
columns := dao.TSimCardInfo.Columns()
if err := tx.Ctx(ctx).Model(dao.TSimCardInfo.Table()).
Fields(columns.TrafficCutoffStatus, columns.TrafficCutoffThreshold).
Where(columns.Id, in.Id).
Scan(&current); err != nil {
return gerror.Wrap(err, "查询卡达量断网配置失败")
}
}
if in.TrafficCutoffStatus != nil {
current.Status = *in.TrafficCutoffStatus
}
if in.TrafficCutoffThreshold != nil {
current.Threshold = *in.TrafficCutoffThreshold
}
return trafficcutoffconfig.ValidateConfig(current.Status, current.Threshold)
}
// Delete 删除卡信息表
func (s *sIotTSimCardInfo) Delete(ctx context.Context, in *model.TSimCardInfoDeleteParam) (err error) {
if _, err = s.Model(ctx).WherePri(in.Id).Unscoped().Delete(); err != nil {
err = gerror.Wrap(err, "删除卡信息表失败,请稍后重试!")
return
}
return
}
// View 获取卡信息表指定信息
func (s *sIotTSimCardInfo) View(ctx context.Context, in *model.TSimCardInfoViewParam) (res *model.TSimCardInfoViewModel, err error) {
mod := s.Model(ctx)
cardInfoTable := dao.TSimCardInfo.Table()
simChannels := dao.TSimChannels.Table()
sysDept := dao.SysDept.Table()
flowPackage := dao.TSimFlowPackageInfo.Table()
// BatchMain := dao.BatchMain.Table()
mod = mod.LeftJoin(simChannels, simChannels+"."+dao.TSimChannels.Columns().ChanKey+"="+cardInfoTable+"."+dao.TSimCardInfo.Columns().ChanKey+" AND "+simChannels+"."+dao.TSimChannels.Columns().TenantId+"="+cardInfoTable+"."+dao.TSimCardInfo.Columns().TenantId)
mod = mod.LeftJoin(sysDept, sysDept+"."+dao.SysDept.Columns().DeptId+"="+cardInfoTable+"."+dao.TSimCardInfo.Columns().DeptId)
mod = mod.LeftJoin(flowPackage, flowPackage+"."+dao.TSimFlowPackageInfo.Columns().PackageCode+"="+cardInfoTable+"."+dao.TSimCardInfo.Columns().CurrentPackageCode+" AND "+flowPackage+"."+dao.TSimFlowPackageInfo.Columns().TenantId+"="+cardInfoTable+"."+dao.TSimCardInfo.Columns().TenantId)
// 字段过滤
mod = mod.FieldsPrefix(dao.TSimCardInfo.Table(), model.TSimCardInfoListModel{})
mod = mod.Fields(fmt.Sprintf(
"COALESCE(NULLIF(%s.%s, ''), %s.%s) as chanName",
simChannels,
dao.TSimChannels.Columns().ChanName,
cardInfoTable,
dao.TSimCardInfo.Columns().ChanKey,
))
mod = mod.Fields(simChannels + ".chan_alias as chanAlias")
mod = mod.Fields(sysDept + ".dept_name as deptName")
mod = mod.Fields(flowPackage + ".package_name as packageName")
mod = mod.Fields(cardflow.RemainFlowExpr(cardInfoTable, flowPackage) + " as reminFlowMb")
mod = mod.WherePri(in.Id)
err = mod.Hook(hook.MemberSummary).Scan(&res)
if err != nil {
err = gerror.Wrap(err, "获取卡信息表信息,请稍后重试!")
return
}
if res != nil {
res.ChanName = channelDisplayName(canViewChannelFullName(ctx), res.ChanName, res.ChanAlias)
if resolver, resolverErr := newCardDeptDisplayResolver(ctx); resolverErr != nil {
logCardDeptDisplayResolverError(ctx, resolverErr)
} else {
res.DeptName = resolver(res.DeptId, res.DeptName)
}
}
return
}
// SyncSimCardFlow 同步SIM卡流量
func (s *sIotTSimCardInfo) SyncSimCardFlow(ctx context.Context, input *model.SyncSimCardFlowInput) (err error) {
in := model.SimDataUsageInput{
SimKeys: []*model.SimKey{
{
ICCID: input.Iccid,
},
},
}
out, err := service.UpstreamChan().QuerySimDatausage(ctx, &in)
if err != nil {
g.Log().Errorf(ctx, "SyncSimCardFlow query upstream failed, iccid:%s, err:%+v", input.Iccid, err)
return gerror.New(simFlowUpstreamUnavailableMessage)
}
if len(out) > 0 && out[0].Code != 0 {
g.Log().Errorf(ctx, "SyncSimCardFlow upstream business failure, iccid:%s, code:%d, message:%s", input.Iccid, out[0].Code, out[0].Message)
}
usage, err := validateSimDataUsageResult(out)
if err != nil {
return err
}
if _, err = s.Model(ctx).
Where(dao.TSimCardInfo.Columns().Iccid, input.Iccid).
Update(do.TSimCardInfo{
UsedFlowMb: usage.ResData.DataUsage,
FlowSyncTime: gtime.Now(),
}); err != nil {
g.Log().Errorf(ctx, "SyncSimCardFlow update card usage failed, iccid:%s, err:%+v", input.Iccid, err)
err = gerror.New("同步卡流量数据保存失败,请稍后重试!")
return
}
return nil
}
func validateSimDataUsageResult(out []model.SimBaseRes[model.SimDataUsageItem]) (model.SimBaseRes[model.SimDataUsageItem], error) {
if len(out) == 0 {
return model.SimBaseRes[model.SimDataUsageItem]{}, gerror.New(simFlowUpstreamUnavailableMessage)
}
if out[0].Code != 0 {
return model.SimBaseRes[model.SimDataUsageItem]{}, newSimDataUsageError(out[0].Code, out[0].Message)
}
return out[0], nil
}
func newSimDataUsageError(code int, message string) error {
message = strings.TrimSpace(message)
if code == simFlowInternalErrorCode || isInternalUpstreamError(message) {
return gerror.New(simFlowUpstreamUnavailableMessage)
}
// 上游返回的业务状态不直接暴露给用户,避免用户把上游问题误认为本平台故障。
if display := upstreamStatusDisplayMessage(code, message); display != "" {
return gerror.New(display)
}
if message == "" {
return gerror.Newf("同步卡流量失败(错误码:%d),请稍后重试!", code)
}
return gerror.Newf("同步卡流量失败(错误码:%d):%s", code, message)
}
// upstreamStatusDisplayMessage 把上游业务状态翻译成面向用户的提示。
// 返回空字符串表示该状态需要保留原始信息(调用方自行决定如何展示)。
//
// 目前只覆盖上游自身繁忙/受限这类「与用户操作无关、重试即可」的状态:
// 这类状态若把原始错误码抛给用户,会被误认为本平台出错。
func upstreamStatusDisplayMessage(code int, message string) string {
switch code {
case simUpstreamBusyCode:
return simUpstreamBusyMessage
}
// 上游可能用不同状态码返回同一种语义,用文案兜底识别。
normalized := strings.TrimSpace(message)
if normalized != "" && isUpstreamBusyMessage(normalized) {
return simUpstreamBusyMessage
}
if normalized != "" && isUpstreamForbiddenMessage(normalized) {
return simUpstreamBusyForbiddenMessage
}
return ""
}
func isUpstreamBusyMessage(message string) bool {
return strings.Contains(message, "系统忙")
}
func isUpstreamForbiddenMessage(message string) bool {
return strings.Contains(message, "禁止访问")
}
func isInternalUpstreamError(message string) bool {
lower := strings.ToLower(strings.TrimSpace(message))
if lower == "" {
return false
}
for _, marker := range []string{
"request failed",
"no host in request url",
"unsupported protocol scheme",
"context deadline exceeded",
"client.timeout",
"connection refused",
"dial tcp",
"http://",
"https://",
"timeout",
"unexpected eof",
} {
if strings.Contains(lower, marker) {
return true
}
}
return strings.Contains(message, "通道未实例化") || strings.Contains(message, "上游服务")
}
// SyncSimCardStatus 同步SIM卡状态
func (s *sIotTSimCardInfo) SyncSimCardStatus(ctx context.Context, input *model.SyncSimCardStatusInput) (err error) {
in := model.SimCardStatusInput{
SimKeys: []*model.SimKey{
{
ICCID: input.Iccid,
},
},
}
out, err := service.UpstreamChan().QuerySimCardStatus(ctx, &in)
if err != nil {
return gerror.Wrap(err, "同步卡状态失败,请稍后重试!")
}
status, err := validateSimCardStatusResult(out, "同步卡状态")
if err != nil {
return err
}
if _, err = s.Model(ctx).
Where(dao.TSimCardInfo.Columns().Iccid, input.Iccid).
Update(do.TSimCardInfo{
BossCardStatus: status.ResData.CardStatus,
StatusSyncTime: gtime.Now(),
}); err != nil {
err = gerror.Wrap(err, "同步卡状态失败,请稍后重试!")
return
}
s.syncBossActivateAtIfNeeded(ctx, input.Iccid, status.ResData.CardStatus)
return nil
}
func validateSimCardStatusResult(out []model.SimBaseRes[model.SimCardStatusItem], operation string) (model.SimBaseRes[model.SimCardStatusItem], error) {
if len(out) == 0 {
return model.SimBaseRes[model.SimCardStatusItem]{}, gerror.Newf("%s失败,请稍后重试!", operation)
}
if out[0].Code != 0 {
if display := upstreamStatusDisplayMessage(out[0].Code, out[0].Message); display != "" {
return model.SimBaseRes[model.SimCardStatusItem]{}, gerror.New(display)
}
return model.SimBaseRes[model.SimCardStatusItem]{}, gerror.Newf("%s失败,错误码:%d,错误信息:%s", operation, out[0].Code, out[0].Message)
}
if out[0].ResData.CardStatus != 0 {
if _, ok := consts.V3ApiSimBossCardStatusNameMap[out[0].ResData.CardStatus]; !ok {
return model.SimBaseRes[model.SimCardStatusItem]{}, gerror.Newf("%s失败,上游返回了不支持的卡状态:%d", operation, out[0].ResData.CardStatus)
}
}
return out[0], nil
}
func validateSimSessionResult(out []model.SimBaseRes[model.SimSessionItem]) (model.SimBaseRes[model.SimSessionItem], error) {
if len(out) == 0 {
return model.SimBaseRes[model.SimSessionItem]{}, gerror.New("查询SIM卡会话失败,请稍后重试!")
}
if out[0].Code != 0 {
if display := upstreamStatusDisplayMessage(out[0].Code, out[0].Message); display != "" {
return model.SimBaseRes[model.SimSessionItem]{}, gerror.New(display)
}
return model.SimBaseRes[model.SimSessionItem]{}, gerror.Newf("查询SIM卡会话失败,错误码:%d,错误信息:%s", out[0].Code, out[0].Message)
}
return out[0], nil
}
// resolveUpstreamSimKey loads the local channel binding before calling upstream.
func (s *sIotTSimCardInfo) resolveUpstreamSimKey(ctx context.Context, iccid string) (*model.SimKey, error) {
var card struct {
ChanKey string
Iccid string
Imsi string
Msisdn string
}
if err := s.Model(ctx).
Fields(
dao.TSimCardInfo.Columns().ChanKey,
dao.TSimCardInfo.Columns().Iccid,
dao.TSimCardInfo.Columns().Imsi,
dao.TSimCardInfo.Columns().Msisdn,
).
Where(dao.TSimCardInfo.Columns().Iccid, iccid).
Scan(&card); err != nil {
return nil, gerror.Wrap(err, `query SIM card channel failed`)
}
return &model.SimKey{
ChanKey: card.ChanKey,
ICCID: iccid,
IMSI: card.Imsi,
MSISDN: card.Msisdn,
}, nil
}
// QuerySimStatus 查询SIM卡状态
func (s *sIotTSimCardInfo) QuerySimStatus(ctx context.Context, input *model.QuerySimStatusInput) (res *model.QuerySimStatusOutput, err error) {
simKey, err := s.resolveUpstreamSimKey(ctx, input.ICCID)
if err != nil {
return nil, err
}
in := model.SimCardStatusInput{
SimKeys: []*model.SimKey{
simKey,
},
}
out, err := service.UpstreamChan().QuerySimCardStatus(ctx, &in)
if err != nil {
return nil, err
}
status, err := validateSimCardStatusResult(out, "查询SIM卡状态")
if err != nil {
return nil, err
}
res = &status.ResData
if _, err = s.Model(ctx).
Where(dao.TSimCardInfo.Columns().Iccid, input.ICCID).
Data(do.TSimCardInfo{
BossCardStatus: res.CardStatus,
StatusSyncTime: gtime.Now(),
}).Update(); err != nil {
err = gerror.Wrap(err, "更新SIM卡状态失败,请稍后重试!")
return
}
s.syncBossActivateAtIfNeeded(ctx, input.ICCID, res.CardStatus)
return res, nil
}
// syncBossActivateAtIfNeeded records the operator activation date once the
// operator confirms that a card is activated. It intentionally never replaces
// an existing value because activation time is historical source data.
func (s *sIotTSimCardInfo) syncBossActivateAtIfNeeded(ctx context.Context, iccid string, cardStatus int) {
if cardStatus != consts.V3ApiSimBossCardStatusActivated {
return
}
missingCount, err := s.Model(ctx).
Where(dao.TSimCardInfo.Columns().Iccid, iccid).
WhereNull(dao.TSimCardInfo.Columns().BossActivateAt).
Count()
if err != nil {
g.Log().Warningf(ctx, "check missing SIM activation date failed, iccid:%s, err:%v", iccid, err)
return
}
if missingCount == 0 {
return
}
output, err := service.UpstreamChan().QuerySimBasicInfo(ctx, &model.SimBasicInfoInput{
SimKeys: []*model.SimKey{{ICCID: iccid}},
})
if err != nil {
g.Log().Warningf(ctx, "query SIM activation date failed, iccid:%s, err:%v", iccid, err)
return
}
if len(output) == 0 || output[0].Code != 0 {
message := "empty response"
code := 0
if len(output) > 0 {
code = output[0].Code
message = output[0].Message
}
g.Log().Warningf(ctx, "query SIM activation date returned no usable data, iccid:%s, code:%d, message:%s", iccid, code, message)
return
}
activeDate := strings.TrimSpace(output[0].ResData.ActiveDate)
if activeDate == "" {
g.Log().Warningf(ctx, "query SIM activation date returned empty activeDate, iccid:%s", iccid)
return
}
activateAt, err := gtime.StrToTime(activeDate)
if err != nil {
g.Log().Warningf(ctx, "parse SIM activation date failed, iccid:%s, activeDate:%s, err:%v", iccid, activeDate, err)
return
}
if _, err = s.Model(ctx).
Where(dao.TSimCardInfo.Columns().Iccid, iccid).
WhereNull(dao.TSimCardInfo.Columns().BossActivateAt).
Update(do.TSimCardInfo{BossActivateAt: activateAt}); err != nil {
g.Log().Warningf(ctx, "save SIM activation date failed, iccid:%s, err:%v", iccid, err)
}
}
// QuerySimSession 查询SIM卡会话
func (s *sIotTSimCardInfo) QuerySimSession(ctx context.Context, input *model.QuerySimSessionInput) (res *model.QuerySimSessionOutput, err error) {
simKey, err := s.resolveUpstreamSimKey(ctx, input.ICCID)
if err != nil {
return nil, err
}
in := model.SimSessionInput{
SimKeys: []*model.SimKey{
simKey,
},
}
out, err := service.UpstreamChan().QuerySimSession(ctx, &in)
if err != nil {
return nil, err
}
session, err := validateSimSessionResult(out)
if err != nil {
return nil, err
}
res = &session.ResData
if imeiList, imeiErr := service.UpstreamChan().QuerySimImei(ctx, &model.SimImeiInput{
SimKeys: []*model.SimKey{
simKey,
},
}); imeiErr == nil && len(imeiList) > 0 {
res.Imei = imeiList[0].ResData.Imei
} else if imeiErr != nil {
g.Log().Warningf(ctx, "查询SIM卡实时IMEI失败,继续返回会话信息,iccid:%s, err:%v", input.ICCID, imeiErr)
}
return res, nil
}
// GetCardPackageOptions 获取卡套餐下拉选项(去重后的套餐编码列表,套餐状态为生效中)
func (s *sIotTSimCardInfo) GetCardPackageOptions(ctx context.Context, deptId int64) (res []*model.GetCardPackageOptionsRes, err error) {
mod := dao.TSimCardInfo.Ctx(ctx)
err = mod.Fields("DISTINCT "+dao.TSimCardInfo.Columns().CurrentPackageCode).
Where(dao.TSimCardInfo.Columns().PackageStatus, 2).
Where(dao.TSimCardInfo.Columns().DeptId, deptId).
Scan(&res)
if err != nil {
return nil, gerror.Wrap(err, "获取卡套餐选项失败,请稍后重试!")
}
return res, nil
}
// CalculatePoolStatistics 计算流量池统计数据
func (s *sIotTSimCardInfo) CalculatePoolStatistics(ctx context.Context, req *model.CalculatePoolStatisticsReq) (res *model.CalculatePoolStatisticsRes, err error) {
g.Log().Info(ctx, "计算流量池统计数据req:", req)
deptId := req.DeptId
res = &model.CalculatePoolStatisticsRes{}
// 容错处理:如果传入的 102 套餐列表为空,则直接返回空结果
if len(req.CardCodes102) == 0 {
return res, nil
}
// 1. 查询该流量池下所有 102 卡的总数量和总已用流量
type CardInfoStats struct {
TotalCardCount int64 `gconv:"total_card_count"`
TotalUsedFlowMb float64 `gconv:"total_used_flow_mb"`
}
var cardStats CardInfoStats
err = s.Model(ctx).
Where("dept_id", deptId).
WhereIn("current_package_code", req.CardCodes102).
Fields("COUNT(1) AS total_card_count, IFNULL(SUM(used_flow_mb), 0) AS total_used_flow_mb").
Scan(&cardStats)
if err != nil {
return nil, err
}
res.TotalCardCount = cardStats.TotalCardCount
res.TotalUsedFlowMb = cardStats.TotalUsedFlowMb
// 2. 查询单卡流量 MB(仅查第一个套餐,从流程套餐表查询)
type CardFlowInfo struct {
FlowMb float64 `gconv:"flow_mb"`
}
var cardFlowInfo CardFlowInfo
err = dao.TSimFlowPackageInfo.Ctx(ctx).
Where("package_code", req.CardCodes102[0]).
Fields("flow_mb").
Scan(&cardFlowInfo)
if err != nil {
return nil, err
}
res.CardFlowMb = cardFlowInfo.FlowMb
// 3. 计算套内总流量 PoolPackageFlowMb
res.PoolPackageFlowMb = float64(res.TotalCardCount) * res.CardFlowMb
// 4. 计算加油包流量 AddOnFlowMb (104 套餐)
if len(req.CardCodes104) > 0 {
g.Log().Info(ctx, "部门编号:", deptId)
g.Log().Info(ctx, "流量池编码:", req.PoolPackageCode)
g.Log().Info(ctx, "104套餐编码:", req.CardCodes104)
type FlowPackageInfo struct {
FlowMb float64 `gconv:"flow_mb"`
}
var flowPackageInfo FlowPackageInfo
bindTable := dao.TSimPoolBindPackage.Table()
packageTable := dao.TSimFlowPackageInfo.Table()
bindColumns := dao.TSimPoolBindPackage.Columns()
packageColumns := dao.TSimFlowPackageInfo.Columns()
now := gtime.Now()
err = dao.TSimPoolBindPackage.Ctx(ctx).
LeftJoin(packageTable, fmt.Sprintf("%s.%s = %s.%s AND %s.%s = %s.%s", bindTable, bindColumns.PackageCode, packageTable, packageColumns.PackageCode, bindTable, bindColumns.TenantId, packageTable, packageColumns.TenantId)).
Where(bindTable+"."+bindColumns.DeptId, deptId).
Where(bindTable+"."+bindColumns.PoolNo, req.PoolPackageCode).
Where(bindTable+"."+bindColumns.PackageType, 104).
Where(bindTable+"."+bindColumns.EffectiveStatus, consts.EffectiveStatusEffective).
WhereIn(bindTable+"."+bindColumns.PackageCode, req.CardCodes104).
Where(fmt.Sprintf("(%s.%s IS NULL OR %s.%s <= ?)", bindTable, bindColumns.EffectiveTime, bindTable, bindColumns.EffectiveTime), now).
Where(fmt.Sprintf("(%s.%s IS NULL OR %s.%s > ?)", bindTable, bindColumns.ExpectedExpireTime, bindTable, bindColumns.ExpectedExpireTime), now).
Fields(fmt.Sprintf("IFNULL(SUM(%s.%s), 0) AS flow_mb", packageTable, packageColumns.FlowMb)).
Scan(&flowPackageInfo)
if err != nil {
return nil, err
}
res.AddOnFlowMb = flowPackageInfo.FlowMb
}
// 5. 核心公式计算 & 状态判定
// 总流量MB = 套内总流量MB + 加油包流量MB
res.TotalFlowMb = res.PoolPackageFlowMb + res.AddOnFlowMb
// 剩余流量MB = 总流量MB - 总已用流量MB
res.RemainFlowMb = res.TotalFlowMb - res.TotalUsedFlowMb
// 状态判断:大于0正常(0),小于等于0停用(1)
if res.RemainFlowMb > 0 {
res.PoolStatus = 0
} else {
res.PoolStatus = 1
}
return res, nil
}
// GetCardsByPackageCodes 根据套餐编码列表获取卡列表
func (s *sIotTSimCardInfo) GetCardsByPackageCodes(ctx context.Context, req *model.GetCardsByPackageCodesParam) (list []*model.TSimCardInfoListModel, totalCount int, err error) {
mod := dao.TSimCardInfo.Ctx(ctx)
cardInfoTable := dao.TSimCardInfo.Table()
simChannels := dao.TSimChannels.Table()
sysDept := dao.SysDept.Table()
flowPackage := dao.TSimFlowPackageInfo.Table()
// 字段过滤
mod = mod.FieldsPrefix(cardInfoTable, model.TSimCardInfoListModel{})
mod = mod.Fields(fmt.Sprintf(
"COALESCE(NULLIF(%s.%s, ''), %s.%s) as chanName, %s.%s as chanAlias",
simChannels,
dao.TSimChannels.Columns().ChanName,
cardInfoTable,
dao.TSimCardInfo.Columns().ChanKey,
simChannels,
dao.TSimChannels.Columns().ChanAlias,
))
mod = mod.Fields(sysDept + ".dept_name as deptName")
mod = mod.Fields(fmt.Sprintf(
"COALESCE(NULLIF(%s.%s, ''), %s.%s) as packageName",
flowPackage,
dao.TSimFlowPackageInfo.Columns().PackageName,
cardInfoTable,
dao.TSimCardInfo.Columns().CurrentPackageCode,
))
mod = mod.Fields(cardflow.RemainFlowExpr(cardInfoTable, flowPackage) + " as reminFlowMb")
// 部门ID条件
if req.DeptId > 0 {
mod = mod.Where(cardInfoTable+"."+dao.TSimCardInfo.Columns().DeptId, req.DeptId)
}
// ICCID条件
if req.Iccid != "" {
mod = mod.Where(cardInfoTable+"."+dao.TSimCardInfo.Columns().Iccid, req.Iccid)
}
// 通道条件
if req.ChanKey != "" {
mod = mod.Where(cardInfoTable+"."+dao.TSimCardInfo.Columns().ChanKey, req.ChanKey)
}
// 卡状态条件
if req.BossCardStatus != nil {
mod = mod.Where(cardInfoTable+"."+dao.TSimCardInfo.Columns().BossCardStatus, *req.BossCardStatus)
}
if len(req.PackageCodes) > 0 {
mod = mod.WhereIn(cardInfoTable+"."+dao.TSimCardInfo.Columns().CurrentPackageCode, req.PackageCodes)
}
// LeftJoin
mod = mod.LeftJoin(simChannels, simChannels+"."+dao.TSimChannels.Columns().ChanKey+"="+cardInfoTable+"."+dao.TSimCardInfo.Columns().ChanKey+" AND "+simChannels+"."+dao.TSimChannels.Columns().TenantId+"="+cardInfoTable+"."+dao.TSimCardInfo.Columns().TenantId)
mod = mod.LeftJoin(sysDept, sysDept+"."+dao.SysDept.Columns().DeptId+"="+cardInfoTable+"."+dao.TSimCardInfo.Columns().DeptId)
mod = mod.LeftJoin(flowPackage, flowPackage+"."+dao.TSimFlowPackageInfo.Columns().PackageCode+"="+cardInfoTable+"."+dao.TSimCardInfo.Columns().CurrentPackageCode+" AND "+flowPackage+"."+dao.TSimFlowPackageInfo.Columns().TenantId+"="+cardInfoTable+"."+dao.TSimCardInfo.Columns().TenantId)
// 分页
mod = mod.Page(req.Page, req.PageSize)
// 排序
mod = mod.OrderDesc(cardInfoTable + "." + dao.TSimCardInfo.Columns().Id)
// 查询数据
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
err = gerror.Wrap(err, "获取卡列表失败,请稍后重试!")
return
}
applyCardChannelDisplayNames(ctx, list)
if resolver, resolverErr := newCardDeptDisplayResolver(ctx); resolverErr != nil {
logCardDeptDisplayResolverError(ctx, resolverErr)
} else {
for _, item := range list {
if item != nil {
item.DeptName = resolver(item.DeptId, item.DeptName)
}
}
}
return
}