package iot import ( "context" "database/sql" "errors" "strconv" "strings" "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/batchimport" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/logic/iot/simcardbatch/outbound" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility/iot_excelize" "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/net/ghttp" "github.com/gogf/gf/v2/util/gconv" "github.com/xuri/excelize/v2" ) type sIotTSimCardOutbound struct{} const outboundFinanceAccountStatusNormal = 0 func NewIotTSimCardOutbound() *sIotTSimCardOutbound { return &sIotTSimCardOutbound{} } func init() { service.RegisterIotTSimCardOutbound(NewIotTSimCardOutbound()) } func (s *sIotTSimCardOutbound) DownloadExcelTemplate(ctx context.Context) error { f := excelize.NewFile() defer func() { _ = f.Close() }() headers := outbound.NewProcessor(outbound.PreviewOptions{}).Config().TemplateHeaders if err := f.SetSheetRow("Sheet1", "A1", &headers); err != nil { return err } if err := iot_excelize.ApplyHeaderColumnsTextStyle(f); err != nil { return err } return iot_excelize.WriteHTTP(ctx, f, "sim_card_outbound_template") } func (s *sIotTSimCardOutbound) CustomerBalance(ctx context.Context, in *model.TSimCardOutboundCustomerBalanceParam) (*model.TSimCardOutboundCustomerBalanceRes, error) { if in == nil || in.CustomerAgentId <= 0 { return nil, gerror.New("客户不能为空") } operatorDeptId := contexts.GetDeptId(ctx) tenantId := contexts.GetTenantId(ctx) if operatorDeptId <= 0 || tenantId == "" { return nil, gerror.New("当前用户未绑定组织或租户") } customer := &entity.SysDept{} if err := dao.SysDept.Ctx(ctx). Fields(dao.SysDept.Columns().DeptId, dao.SysDept.Columns().DeptName, dao.SysDept.Columns().ParentId). Where(dao.SysDept.Columns().DeptId, in.CustomerAgentId). Where(dao.SysDept.Columns().TenantId, tenantId). Scan(customer); err != nil && !errors.Is(err, sql.ErrNoRows) { return nil, gerror.Wrap(err, "查询客户信息失败") } if customer.DeptId == 0 || (customer.DeptId != operatorDeptId && customer.ParentId != operatorDeptId) { return nil, gerror.New("客户不存在或不在当前操作范围内") } res := &model.TSimCardOutboundCustomerBalanceRes{ CustomerAgentId: customer.DeptId, CustomerName: customer.DeptName, AccountExists: false, StatusText: "暂无余额账户", } account := &entity.TAgentBalance{} if err := dao.TAgentBalance.Ctx(ctx). Where(dao.TAgentBalance.Columns().DeptId, customer.DeptId). Where(dao.TAgentBalance.Columns().TenantId, tenantId). Scan(account); err != nil && !errors.Is(err, sql.ErrNoRows) { return nil, gerror.Wrap(err, "查询账户余额失败") } if account.Id == 0 { return res, nil } res.AccountExists = true res.Balance = account.Balance res.Status = account.Status if account.Status == outboundFinanceAccountStatusNormal { res.StatusText = "正常" } else { res.StatusText = "禁用" } return res, nil } func (s *sIotTSimCardOutbound) validateBalanceBeforeSubmit(ctx context.Context, in *model.TSimCardOutboundSubmitParam) error { if in == nil || in.OfflineContractCompleted { return nil } amountText := strings.TrimSpace(in.Amount) if amountText == "" { return gerror.New("订单金额不能为空") } amount, err := strconv.ParseFloat(amountText, 64) if err != nil || amount <= 0 { return gerror.New("订单金额必须大于0") } balance, err := s.CustomerBalance(ctx, &model.TSimCardOutboundCustomerBalanceParam{ CustomerAgentId: in.CustomerAgentId, }) if err != nil { return err } if balance == nil || !balance.AccountExists { return gerror.New("客户暂无余额账户,请先充值") } if balance.Status != outboundFinanceAccountStatusNormal { return gerror.New("客户余额账户状态异常,不能出库") } if balance.Balance+0.0001 < amount { return gerror.Newf("客户余额不足,需支付%.2f元,当前余额%.2f元", amount, balance.Balance) } return nil } func (s *sIotTSimCardOutbound) SubmitExcel(ctx context.Context, in *model.TSimCardOutboundExcelSubmitParam) (*model.TSimCardOutboundExcelSubmitRes, error) { if in == nil { in = &model.TSimCardOutboundExcelSubmitParam{} } file, err := s.getUploadFile(ctx) if err != nil { return nil, err } result, err := outbound.SubmitExcelAsync(ctx, file, in) if err != nil { return nil, err } return (*model.TSimCardOutboundExcelSubmitRes)(outboundSubmitResult(result, consts.BatchTypeOutbound)), nil } func (s *sIotTSimCardOutbound) SubmitRange(ctx context.Context, in *model.TSimCardOutboundRangeSubmitParam) (*model.TSimCardOutboundRangeSubmitRes, error) { if in == nil { in = &model.TSimCardOutboundRangeSubmitParam{} } normalizeOutboundSubmitParam(&in.TSimCardOutboundSubmitParam) if err := s.validateBalanceBeforeSubmit(ctx, &in.TSimCardOutboundSubmitParam); err != nil { return nil, err } rows, err := outbound.BuildRangeRows(ctx, in.RangeCodeType, in.StartIccid, in.EndIccid, in.StartMsisdn, in.EndMsisdn, in.StartImsi, in.EndImsi) if err != nil { return nil, err } result, err := outbound.SubmitRowsAsync(ctx, in, rows, consts.BatchSourceTypeRange, "", true) if err != nil { return nil, err } return (*model.TSimCardOutboundRangeSubmitRes)(outboundSubmitResult(result, consts.BatchTypeOutbound)), nil } func (s *sIotTSimCardOutbound) SubmitInput(ctx context.Context, in *model.TSimCardOutboundInputSubmitParam) (*model.TSimCardOutboundInputSubmitRes, error) { if in == nil { in = &model.TSimCardOutboundInputSubmitParam{} } normalizeOutboundSubmitParam(&in.TSimCardOutboundSubmitParam) if err := s.validateBalanceBeforeSubmit(ctx, &in.TSimCardOutboundSubmitParam); err != nil { return nil, err } rows, err := outbound.ParseInputRows(ctx, in.Input) if err != nil { return nil, err } result, err := outbound.SubmitRowsAsync(ctx, in, rows, consts.BatchSourceTypeBatchInput, "", false) if err != nil { return nil, err } return (*model.TSimCardOutboundInputSubmitRes)(outboundSubmitResult(result, consts.BatchTypeOutbound)), nil } func (s *sIotTSimCardOutbound) ListInboundBatches(ctx context.Context, in *model.TSimCardOutboundInboundBatchListParam) (*model.TSimCardOutboundInboundBatchListRes, error) { if in == nil { in = &model.TSimCardOutboundInboundBatchListParam{} } normalizePage(&in.Page, &in.PageSize) base := outboundStockBaseModel(ctx) columns := dao.TSimCardStock.Columns() if batchNo := strings.TrimSpace(in.BatchNo); batchNo != "" { base = base.WhereLike(columns.BatchNo, "%"+batchNo+"%") } base = applyStockBusinessFilters(ctx, base, stockBusinessFilter{ ChanName: in.ChanName, CardType: in.CardType, UpPkgKeyword: in.UpPkgKeyword, AgreementExpiryMonths: in.AgreementExpiryMonths, }) totalValue, err := base.Clone().Fields("COUNT(DISTINCT " + columns.BatchNo + ")").Value() if err != nil { return nil, gerror.Wrap(err, "查询可出库批次列表失败") } total := gconv.Int(totalValue) var items []*model.TSimCardOutboundInboundBatchListItem err = base.Fields( columns.BatchNo, "MIN("+columns.ChanKey+") AS chan_key", "COUNT(1) AS stock_count", "MIN("+columns.CreatedAt+") AS first_created_at", "MAX("+columns.CreatedAt+") AS last_created_at", ). Group(columns.BatchNo). OrderDesc("last_created_at"). Page(in.Page, in.PageSize). Scan(&items) if err != nil { return nil, gerror.Wrap(err, "查询可出库批次列表失败") } if err = enrichOutboundBatchBusinessFields(ctx, items); err != nil { return nil, err } return &model.TSimCardOutboundInboundBatchListRes{ Items: items, Total: total, }, nil } func (s *sIotTSimCardOutbound) ListInboundBatchCards(ctx context.Context, in *model.TSimCardOutboundInboundBatchCardsParam) (*model.TSimCardOutboundInboundBatchCardsRes, error) { if in == nil { in = &model.TSimCardOutboundInboundBatchCardsParam{} } normalizePage(&in.Page, &in.PageSize) batchNo := strings.TrimSpace(in.BatchNo) if batchNo == "" { return nil, gerror.New("批次号不能为空") } mod := outboundStockBaseModel(ctx).Where(dao.TSimCardStock.Columns().BatchNo, batchNo) mod = filterOutboundStockCards(mod, in.Iccid, in.Msisdn, in.Imsi) var items []*model.TSimCardOutboundInboundBatchCardItem total, err := mod.Clone().Count() if err != nil { return nil, gerror.Wrap(err, "查询可出库批次卡列表失败") } err = mod.Fields(outboundBatchCardSelectFields()). OrderAsc(dao.TSimCardStock.Columns().Id). Page(in.Page, in.PageSize). Scan(&items) if err != nil { return nil, gerror.Wrap(err, "查询可出库批次卡列表失败") } if err = enrichOutboundBatchCardBusinessFields(ctx, items); err != nil { return nil, err } return &model.TSimCardOutboundInboundBatchCardsRes{ Items: items, Total: total, }, nil } func (s *sIotTSimCardOutbound) SubmitBatch(ctx context.Context, in *model.TSimCardOutboundBatchSubmitParam) (*model.TSimCardOutboundBatchSubmitRes, error) { if in == nil { in = &model.TSimCardOutboundBatchSubmitParam{} } normalizeOutboundSubmitParam(&in.TSimCardOutboundSubmitParam) if err := s.validateBalanceBeforeSubmit(ctx, &in.TSimCardOutboundSubmitParam); err != nil { return nil, err } batchNo := strings.TrimSpace(in.BatchNo) if batchNo == "" { return nil, gerror.New("批次号不能为空") } rows, err := s.buildBatchOutboundRows(ctx, batchNo) if err != nil { return nil, err } result, err := outbound.SubmitRowsAsync(ctx, in, rows, consts.BatchSourceTypeBatchSelect, batchNo, false) if err != nil { return nil, err } return (*model.TSimCardOutboundBatchSubmitRes)(outboundSubmitResult(result, consts.BatchTypeOutbound)), nil } func (s *sIotTSimCardOutbound) getUploadFile(ctx context.Context) (*ghttp.UploadFile, error) { r := g.RequestFromCtx(ctx) if r == nil { return nil, gerror.New("当前上下文不是HTTP请求") } file := r.GetUploadFile("file") if file == nil { return nil, gerror.New("请选择上传文件") } return file, nil } func (s *sIotTSimCardOutbound) buildBatchOutboundRows(ctx context.Context, batchNo string) ([]batchimport.Row, error) { mod := outboundStockBaseModel(ctx).Where(dao.TSimCardStock.Columns().BatchNo, batchNo) var cards []*model.TSimCardOutboundInboundBatchCardItem err := mod.Fields(outboundBatchCardSelectFields()). OrderAsc(dao.TSimCardStock.Columns().Id). Scan(&cards) if err != nil { return nil, gerror.Wrap(err, "查询可出库批次卡列表失败") } if len(cards) == 0 { return nil, gerror.New("当前批次没有可出库库存卡") } rows := make([]batchimport.Row, 0, len(cards)) for index, card := range cards { rows = append(rows, &outbound.Row{ RowNo: index + 1, Iccid: strings.TrimSpace(card.Iccid), Msisdn: strings.TrimSpace(card.Msisdn), Imsi: strings.TrimSpace(card.Imsi), SourceBatchNo: batchNo, }) } return rows, nil } func outboundStockBaseModel(ctx context.Context) *gdb.Model { columns := dao.TSimCardStock.Columns() return dao.TSimCardStock.Ctx(ctx). Where(columns.TenantId, contexts.GetTenantId(ctx)). Where(columns.CreatedDept, contexts.GetDeptId(ctx)). Where(columns.BatchNo+" <> ?", "") } func filterOutboundStockCards(mod *gdb.Model, iccid, msisdn, imsi string) *gdb.Model { columns := dao.TSimCardStock.Columns() if value := strings.TrimSpace(iccid); value != "" { mod = mod.WhereLike(columns.Iccid, "%"+value+"%") } if value := strings.TrimSpace(msisdn); value != "" { mod = mod.WhereLike(columns.Msisdn, "%"+value+"%") } if value := strings.TrimSpace(imsi); value != "" { mod = mod.WhereLike(columns.Imsi, "%"+value+"%") } return mod } func outboundBatchCardSelectFields() []string { columns := dao.TSimCardStock.Columns() return []string{ columns.Iccid, columns.Msisdn, columns.Imsi, columns.Imei, columns.ChanKey, columns.BatchNo, columns.CreatedAt, } } func outboundSubmitResult(result *batchimport.Result, batchType int) *model.TSimCardOutboundSubmitRes { res := &model.TSimCardOutboundSubmitRes{ BatchType: batchType, BatchAsyncStatus: consts.BatchAsyncStatusPending, Status: "pending", } if result != nil { res.BatchNo = result.BatchNo res.Total = result.TotalRows res.Quantity = result.TotalRows res.ValidCount = result.TotalRows res.FailCount = 0 res.ResultDownloadId = result.ResultDownloadId res.ResultFileOssId = result.ResultFileOssId } return res } func normalizeOutboundSubmitParam(in *model.TSimCardOutboundSubmitParam) { if in == nil { return } if in.DurationMonths <= 0 && in.DurationYears > 0 { in.DurationMonths = in.DurationYears*12 + in.DurationMonthPart } if in.DurationMonths <= 0 && in.DurationMonthPart > 0 { in.DurationMonths = in.DurationMonthPart } if in.DurationYears <= 0 && in.DurationMonthPart <= 0 && in.DurationMonths > 0 { in.DurationYears = in.DurationMonths / 12 in.DurationMonthPart = in.DurationMonths % 12 } if in.GiftDurationMonths <= 0 && in.GiftDurationYears > 0 { in.GiftDurationMonths = in.GiftDurationYears*12 + in.GiftDurationMonthPart } if in.GiftDurationMonths <= 0 && in.GiftDurationMonthPart > 0 { in.GiftDurationMonths = in.GiftDurationMonthPart } if in.GiftDurationYears <= 0 && in.GiftDurationMonthPart <= 0 && in.GiftDurationMonths > 0 { in.GiftDurationYears = in.GiftDurationMonths / 12 in.GiftDurationMonthPart = in.GiftDurationMonths % 12 } disableOutboundPeriodOptions(in) if strings.TrimSpace(in.Remark) != "" { in.BuyerNote = strings.TrimSpace(in.Remark) in.AdminNote = strings.TrimSpace(in.Remark) } } func disableOutboundPeriodOptions(in *model.TSimCardOutboundSubmitParam) { if in == nil { return } // 1 表示否。出库不再暴露测试期、沉默期配置,统一按无测试期/无沉默期保存。 in.HasTestPeriod = 1 in.TestPeriodDays = 0 in.TestPeriodLatestExpiryDate = nil in.HasSilentPeriod = 1 in.SilentPeriodDays = 0 in.SilentPeriodLatestExpiryDate = nil } func normalizePage(page *int, pageSize *int) { if page != nil && *page <= 0 { *page = 1 } if pageSize != nil && *pageSize <= 0 { *pageSize = 20 } if pageSize != nil && *pageSize > 2000 { *pageSize = 2000 } }