// Package gen // @Link https://github.com/xiujiecn/xiu-admin // @Copyright Copyright (c) 2026 LiXiujie // @Author Lxj // @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE // @AutoGenerate Version package iot import ( "context" "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/contexts" "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" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility/excel" "strings" "github.com/gogf/gf/v2/database/gdb" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gctx" "github.com/gogf/gf/v2/os/gtime" "github.com/gogf/gf/v2/util/gconv" ) 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) { 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 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 } 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 } // Export 导出卡信息表 func (s *sIotTSimCardInfo) Export(ctx context.Context, in *model.TSimCardInfoListParam) (err error) { list, totalCount, err := s.List(ctx, in) if err != nil { return } // 字段的排序是依据tags的字段顺序,如果你不想使用默认的排序方式,可以直接定义 tags = []string{"字段名称", "字段名称2", ...} tags, err := convert.GetEntityDescTags(model.TSimCardInfoExportModel{}) if err != nil { return } var ( fileName = "导出卡信息表-" + gctx.CtxId(ctx) sheetName = fmt.Sprintf("索引条件共%v行,共%v页,当前导出是第%v页,本页共%v行", totalCount, (totalCount+in.PageSize-1)/in.PageSize, in.Page, len(list)) exports []*model.TSimCardInfoExportModel ) if err = gconv.Scan(list, &exports); err != nil { return } err = excel.ExportByStructs(ctx, tags, exports, fileName, sheetName) return } // 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(¤t); 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 { 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 } 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 }