package outbound import ( "context" "database/sql" "errors" "fmt" "math" "sort" "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/logic/iot/cardpackage" "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/model/entity" "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" ) const ( outboundPaymentModeOffline = "offline" outboundPaymentModeBalance = "balance" outboundDetailUpdateChunkSize = 200 outboundFinanceIncomeExpenseExpense = 1 outboundFinanceTransactionTypeConsume = 1 outboundFinanceTransactionChannelBalance = 3 outboundFinanceTransactionStatusPaid = 1 outboundFinanceAccountStatusNormal = 0 ) func (p *Processor) Complete(ctx context.Context, tx gdb.TX, in *CompleteInput) (*CompleteOutput, error) { if in == nil || in.SubmitParam == nil { return nil, gerror.New("出库参数不能为空") } if in.BatchNo == "" { return nil, gerror.New("出库批次号不能为空") } if len(in.Rows) == 0 { return nil, gerror.New("出库卡不能为空") } normalizeSubmitParam(in.SubmitParam) if err := validateSubmitParam(in.SubmitParam); err != nil { return nil, err } iccids, err := strictRowsToIccids(in.Rows) if err != nil { return nil, err } stockByIccid, err := p.repo.LockStockCardsByIccids(ctx, tx, iccids) if err != nil { return nil, gerror.Wrap(err, "锁定库存卡失败") } if len(stockByIccid) != len(iccids) { return nil, gerror.New("部分库存卡已变化,请刷新后重试") } for _, iccid := range iccids { stock := stockByIccid[iccid] if stock == nil { return nil, gerror.Newf("ICCID不存在库存记录:%s", iccid) } if stock.CreatedDept != in.Operator.DeptId { return nil, gerror.Newf("ICCID不属于当前机构库存:%s", iccid) } if in.RequiredSourceBatchNo != "" && strings.TrimSpace(stock.BatchNo) != in.RequiredSourceBatchNo { return nil, gerror.Newf("ICCID 不属于指定批次:%s", iccid) } } if in.StrictStockIdentity { for _, row := range in.Rows { stock := stockByIccid[row.Iccid] if stock == nil { return nil, gerror.Newf("ICCID不存在库存记录:%s", row.Iccid) } if strings.TrimSpace(row.Msisdn) != strings.TrimSpace(stock.Msisdn) { return nil, gerror.Newf("MSISDN与库存记录不一致:%s", row.Iccid) } if strings.TrimSpace(row.Imsi) != strings.TrimSpace(stock.Imsi) { return nil, gerror.Newf("IMSI与库存记录不一致:%s", row.Iccid) } } } formalByIccid, err := p.repo.GetExistingFormalIccids(ctx, iccids) if err != nil { return nil, gerror.Wrap(err, "查询正式卡失败") } for _, iccid := range iccids { if formalByIccid[iccid] { return nil, gerror.Newf("ICCID已存在正式卡记录:%s", iccid) } } quantity := len(iccids) subscribeCards := make([]cardpackage.SubscribeCard, 0, quantity) cardRows := make([]do.TSimCardInfo, 0, quantity) detailUpdates := make(map[string]*gjsonJsonHolder, quantity) sourceBatchNos := make([]string, 0, quantity) sourceBatchSet := make(map[string]struct{}, quantity) for _, row := range in.Rows { stock := stockByIccid[row.Iccid] if stock == nil { return nil, gerror.Newf("ICCID不存在库存记录:%s", row.Iccid) } if strings.TrimSpace(stock.Iccid) != strings.TrimSpace(row.Iccid) { return nil, gerror.Newf("库存卡映射异常:第%d行%s匹配到%s", row.RowNo, row.Iccid, stock.Iccid) } row.SourceBatchNo = strings.TrimSpace(stock.BatchNo) row.Msisdn = chooseString(row.Msisdn, stock.Msisdn) row.Imsi = chooseString(row.Imsi, stock.Imsi) if row.SourceBatchNo != "" { if _, ok := sourceBatchSet[row.SourceBatchNo]; !ok { sourceBatchSet[row.SourceBatchNo] = struct{}{} sourceBatchNos = append(sourceBatchNos, row.SourceBatchNo) } } subscribeCards = append(subscribeCards, cardpackage.SubscribeCard{ Iccid: stock.Iccid, Msisdn: stock.Msisdn, Imsi: stock.Imsi, }) } inboundBatchInfos, err := p.repo.GetInboundBatchInfos(ctx, tx, sourceBatchNos) if err != nil { return nil, gerror.Wrap(err, "查询入库批次信息失败") } payment, err := prepareOutboundPayment(ctx, tx, in.BatchNo, subscriptionAmount(in.SubmitParam), in.Operator, in.SubmitParam) if err != nil { return nil, err } packageSvc := cardpackage.NewService() subscription, err := packageSvc.CreatePackageSubscription(ctx, tx, &cardpackage.CreatePackageSubscriptionInput{ BatchNo: in.BatchNo, Cards: subscribeCards, CustomerAgentId: in.SubmitParam.CustomerAgentId, PackageCode: strings.TrimSpace(in.SubmitParam.PackageCode), PackageBeginTime: in.SubmitParam.PackageBeginTime, DurationMonths: in.SubmitParam.DurationMonths, GiftDurationMonths: in.SubmitParam.GiftDurationMonths, DurationDisplayValue: displayDurationValue(in.SubmitParam.DurationYears, in.SubmitParam.DurationMonths), GiftDurationDisplayValue: displayDurationValue(in.SubmitParam.GiftDurationYears, in.SubmitParam.GiftDurationMonths), DurationDisplayLabel: displayDurationLabel(in.SubmitParam.DurationYears, in.SubmitParam.DurationMonthPart, in.SubmitParam.DurationMonths), GiftDurationDisplayLabel: displayDurationLabel(in.SubmitParam.GiftDurationYears, in.SubmitParam.GiftDurationMonthPart, in.SubmitParam.GiftDurationMonths), DurationUnit: consts.RenewDurationUnitYear, EndAtPreviousMonthEnd: true, Amount: in.SubmitParam.Amount, PayType: payment.PayType, PayStatus: payment.PayStatus, BalanceAmount: payment.BalanceAmount, OfflinePaidAmount: payment.OfflinePaidAmount, PayTime: payment.PaymentTime, OfflineContractCompleted: in.SubmitParam.OfflineContractCompleted, BuyerNote: normalizedRemark(in.SubmitParam), AdminNote: normalizedRemark(in.SubmitParam), BindRemark: in.BatchNo, SkipBindInsert: true, Operator: in.Operator, }) if err != nil { return nil, err } sort.Strings(sourceBatchNos) if err = updateBatchMainAfterComplete( ctx, tx, in, subscription.OrderSn, subscription.Customer, subscription.Package, subscription.Amount, subscription.PackageBeginAt, subscription.PackageEndAt, sourceBatchNos, subscription.PackageStatus, subscription.EffectiveStatus, payment, ); err != nil { return nil, err } now := subscription.SubscribeTime for _, row := range in.Rows { stock := stockByIccid[row.Iccid] bind := subscription.BindsByIccid[row.Iccid] if bind == nil { return nil, gerror.Newf("ICCID订购绑定生成失败:%s", row.Iccid) } cardType := inboundCardTypeValue(inboundBatchInfos[stock.BatchNo]) cardRows = append(cardRows, do.TSimCardInfo{ Iccid: stock.Iccid, Msisdn: stock.Msisdn, Imsi: stock.Imsi, Imei: stock.Imei, DeptId: in.SubmitParam.CustomerAgentId, CurrentPackageCode: subscription.Package.PackageCode, PackageStatus: subscription.PackageStatus, PackageTotalFlowMb: packageTotalFlowMB(subscription.Package), BossCardStatus: consts.V3ApiSimBossCardStatusPendingActivate, CardType: cardType, InboundBatchNo: stock.BatchNo, OutboundBatchNo: in.BatchNo, OutStockTime: now, FlowPkgBeginAt: subscription.PackageBeginAt, FlowPkgEndAt: subscription.PackageEndAt, ChanKey: stock.ChanKey, TenantId: in.Operator.TenantId, CreatedDept: in.Operator.DeptId, CreatedBy: in.Operator.UserId, CreatedAt: now, UpdatedBy: in.Operator.UserId, UpdatedAt: now, }) detailUpdates[row.Iccid] = &gjsonJsonHolder{Data: buildDetailSnapshot(row, stock, cardType, subscription.Customer, subscription.Package, in, subscription.OrderSn, bind.SubscriptionNo, subscription.Amount, subscription.PackageBeginAt, subscription.PackageEndAt)} } for start := 0; start < len(cardRows); start += outboundDatabaseInsertChunkSize { end := start + outboundDatabaseInsertChunkSize if end > len(cardRows) { end = len(cardRows) } if _, err = tx.Ctx(ctx).Model(dao.TSimCardInfo.Table()).Data(cardRows[start:end]).Insert(); err != nil { return nil, gerror.Wrap(err, "新增正式卡失败") } } if err = packageSvc.InsertCardPackageBinds(ctx, tx, subscription.BindRows); err != nil { return nil, err } if err = updateBatchDetailSnapshots(ctx, tx, in.BatchNo, detailUpdates, in.Operator.UserId); err != nil { return nil, err } var affected int64 for start := 0; start < len(iccids); start += outboundDatabaseChunkSize { end := start + outboundDatabaseChunkSize if end > len(iccids) { end = len(iccids) } result, deleteErr := tx.Ctx(ctx).Model(dao.TSimCardStock.Table()). WhereIn(dao.TSimCardStock.Columns().Iccid, iccids[start:end]). Where(dao.TSimCardStock.Columns().CreatedDept, in.Operator.DeptId). Delete() if deleteErr != nil { return nil, gerror.Wrap(deleteErr, "删除库存卡失败") } chunkAffected, rowsErr := result.RowsAffected() if rowsErr != nil { return nil, gerror.Wrap(rowsErr, "读取库存删除数量失败") } affected += chunkAffected } if int(affected) != quantity { return nil, gerror.New("部分库存卡已变化,请刷新后重试") } return &CompleteOutput{ OrderId: subscription.OrderId, OrderSn: subscription.OrderSn, BatchNo: in.BatchNo, BatchType: consts.BatchTypeOutbound, Quantity: quantity, SourceBatchNos: sourceBatchNos, Customer: subscription.Customer, Package: subscription.Package, PackageBeginAt: subscription.PackageBeginAt, PackageEndAt: subscription.PackageEndAt, PackageStatus: subscription.PackageStatus, EffectiveStatus: subscription.EffectiveStatus, }, nil } type gjsonJsonHolder struct { Data any } func validateSubmitParam(in *model.TSimCardOutboundSubmitParam) error { normalizeSubmitParam(in) if in.CustomerAgentId <= 0 { return gerror.New("客户不能为空") } if strings.TrimSpace(in.PackageCode) == "" { return gerror.New("套餐不能为空") } if in.PackageBeginTime == nil { return gerror.New("套餐开始时间不能为空") } if in.DurationMonths <= 0 { return gerror.New("套餐时长必须大于0") } if in.DurationMonthPart < 0 || in.DurationMonthPart > 11 { return gerror.New("套餐购买月份必须在0到11之间") } if in.GiftDurationMonthPart < 0 || in.GiftDurationMonthPart > 11 { return gerror.New("赠送月份必须在0到11之间") } if in.GiftDurationMonths < 0 { return gerror.New("赠送时长不能小于0") } amount, err := parseOutboundAmount(in.Amount) if err != nil { return err } if amount <= 0 { return gerror.New("订单金额必须大于0") } return nil } func rowsToIccids(rows []*Row) []string { iccids := make([]string, 0, len(rows)) for _, row := range rows { if row == nil { continue } iccid := strings.TrimSpace(row.Iccid) if iccid != "" { iccids = append(iccids, iccid) } } return uniqueStrings(iccids) } func strictRowsToIccids(rows []*Row) ([]string, error) { seen := make(map[string]int, len(rows)) iccids := make([]string, 0, len(rows)) for _, row := range rows { if row == nil { return nil, gerror.New("出库行不能为空") } iccid := strings.TrimSpace(row.Iccid) if iccid == "" { return nil, gerror.Newf("第%d行ICCID为空,不能出库", row.RowNo) } if firstRowNo, ok := seen[iccid]; ok { return nil, gerror.Newf("第%d行ICCID与第%d行重复:%s", row.RowNo, firstRowNo, iccid) } seen[iccid] = row.RowNo row.Iccid = iccid iccids = append(iccids, iccid) } return iccids, nil } func updateBatchMainAfterComplete( ctx context.Context, tx gdb.TX, in *CompleteInput, orderSn string, customer *entity.SysDept, pkg *entity.TSimFlowPackageInfo, amount float64, beginAt, endAt *gtime.Time, sourceBatchNos []string, packageStatus, effectiveStatus int, payment *outboundPaymentSnapshot, ) error { current := &struct { BatchInfo *gjson.Json }{} if err := tx.Ctx(ctx).Model(dao.BatchMain.Table()). Fields(dao.BatchMain.Columns().BatchInfo). Where(dao.BatchMain.Columns().BatchNo, in.BatchNo). Scan(current); err != nil { return gerror.Wrap(err, "读取出库批次信息失败") } info := g.Map{} if current.BatchInfo != nil { for key, value := range current.BatchInfo.Map() { info[key] = value } } completedInfo := g.Map{ "orderSn": orderSn, "customerAgentId": in.SubmitParam.CustomerAgentId, "customerName": customer.DeptName, "packageCode": pkg.PackageCode, "packageName": pkg.PackageName, "packageType": pkg.PackageType, "packageBeginTime": formatTime(beginAt), "packageEndTime": formatTime(endAt), "durationYears": in.SubmitParam.DurationYears, "durationMonthPart": in.SubmitParam.DurationMonthPart, "durationMonths": in.SubmitParam.DurationMonths, "giftDurationYears": in.SubmitParam.GiftDurationYears, "giftDurationMonthPart": in.SubmitParam.GiftDurationMonthPart, "giftDurationMonths": in.SubmitParam.GiftDurationMonths, "durationLabel": displayDurationLabel(in.SubmitParam.DurationYears, in.SubmitParam.DurationMonthPart, in.SubmitParam.DurationMonths), "giftDurationLabel": displayDurationLabel(in.SubmitParam.GiftDurationYears, in.SubmitParam.GiftDurationMonthPart, in.SubmitParam.GiftDurationMonths), "amount": amount, "quantity": len(in.Rows), "offlineContractCompleted": in.SubmitParam.OfflineContractCompleted, "remark": normalizedRemark(in.SubmitParam), "buyerNote": normalizedRemark(in.SubmitParam), "adminNote": normalizedRemark(in.SubmitParam), "sourceBatchNos": sourceBatchNos, "packageStatus": packageStatus, "effectiveStatus": effectiveStatus, } for key, value := range completedInfo { info[key] = value } if payment != nil { info["paymentMode"] = payment.PaymentMode info["payType"] = payment.PayType info["payStatus"] = payment.PayStatus info["balanceAmount"] = payment.BalanceAmount info["offlinePaidAmount"] = payment.OfflinePaidAmount info["balanceBefore"] = payment.BalanceBefore info["balanceAfter"] = payment.BalanceAfter info["balanceDetailSerialNo"] = payment.BalanceDetailSerialNo info["payerDeptId"] = payment.PayerDeptId } _, err := tx.Ctx(ctx).Model(dao.BatchMain.Table()). Where(dao.BatchMain.Columns().BatchNo, in.BatchNo). Data(do.BatchMain{ OrderSn: orderSn, TargetDeptId: in.SubmitParam.CustomerAgentId, BatchInfo: gjson.New(info), UpdatedBy: in.Operator.UserId, UpdatedAt: gtime.Now(), }). Update() if err != nil { return gerror.Wrap(err, "更新出库批次失败") } return nil } func updateBatchDetailSnapshots(ctx context.Context, tx gdb.TX, batchNo string, snapshots map[string]*gjsonJsonHolder, userId int64) error { if len(snapshots) == 0 { return nil } iccids := make([]string, 0, len(snapshots)) for iccid := range snapshots { iccids = append(iccids, iccid) } sort.Strings(iccids) columns := dao.BatchDetail.Columns() for start := 0; start < len(iccids); start += outboundDetailUpdateChunkSize { end := start + outboundDetailUpdateChunkSize if end > len(iccids) { end = len(iccids) } chunk := iccids[start:end] var sqlBuilder strings.Builder args := make([]any, 0, len(chunk)*3+2) sqlBuilder.WriteString("UPDATE `") sqlBuilder.WriteString(dao.BatchDetail.Table()) sqlBuilder.WriteString("` SET `") sqlBuilder.WriteString(columns.Snapshot) sqlBuilder.WriteString("` = CASE `") sqlBuilder.WriteString(columns.ItemId) sqlBuilder.WriteString("`") for _, iccid := range chunk { sqlBuilder.WriteString(" WHEN ? THEN ?") args = append(args, iccid, gjson.New(snapshots[iccid].Data).String()) } sqlBuilder.WriteString(" ELSE `") sqlBuilder.WriteString(columns.Snapshot) sqlBuilder.WriteString("` END, `") sqlBuilder.WriteString(columns.UpdatedBy) sqlBuilder.WriteString("` = ? WHERE `") sqlBuilder.WriteString(columns.BatchNo) sqlBuilder.WriteString("` = ? AND `") sqlBuilder.WriteString(columns.ItemId) sqlBuilder.WriteString("` IN (") args = append(args, userId, batchNo) for index, iccid := range chunk { if index > 0 { sqlBuilder.WriteByte(',') } sqlBuilder.WriteByte('?') args = append(args, iccid) } sqlBuilder.WriteByte(')') if _, err := tx.Exec(sqlBuilder.String(), args...); err != nil { return gerror.Wrapf(err, "更新批次明细快照失败,范围:%d-%d", start+1, end) } } return nil } func buildDetailSnapshot(row *Row, stock *entity.TSimCardStock, cardType any, customer *entity.SysDept, pkg *entity.TSimFlowPackageInfo, in *CompleteInput, orderSn, subscriptionNo string, amount float64, beginAt, endAt *gtime.Time) g.Map { return g.Map{ "ICCID": stock.Iccid, "MSISDN": stock.Msisdn, "IMSI": stock.Imsi, "IMEI": stock.Imei, "cardType": cardType, "inboundBatchNo": stock.BatchNo, "outboundBatchNo": in.BatchNo, "orderSn": orderSn, "subscriptionNo": subscriptionNo, "customerAgentId": in.SubmitParam.CustomerAgentId, "customerName": customer.DeptName, "packageCode": pkg.PackageCode, "packageName": pkg.PackageName, "packageType": pkg.PackageType, "packageBeginTime": formatTime(beginAt), "packageEndTime": formatTime(endAt), "durationYears": in.SubmitParam.DurationYears, "durationMonthPart": in.SubmitParam.DurationMonthPart, "durationMonths": in.SubmitParam.DurationMonths, "giftDurationYears": in.SubmitParam.GiftDurationYears, "giftDurationMonthPart": in.SubmitParam.GiftDurationMonthPart, "giftDurationMonths": in.SubmitParam.GiftDurationMonths, "durationLabel": displayDurationLabel(in.SubmitParam.DurationYears, in.SubmitParam.DurationMonthPart, in.SubmitParam.DurationMonths), "giftDurationLabel": displayDurationLabel(in.SubmitParam.GiftDurationYears, in.SubmitParam.GiftDurationMonthPart, in.SubmitParam.GiftDurationMonths), "packageTotalFlowMb": packageTotalFlowMB(pkg), "amount": amount, "remark": normalizedRemark(in.SubmitParam), "rowNo": row.RowNo, "stock": g.Map{ "chanKey": stock.ChanKey, "sourceInboundBatchNo": stock.BatchNo, }, } } func inboundCardTypeValue(info *gjson.Json) any { if info == nil { return nil } value := strings.TrimSpace(info.Get("inboundOptions.cardType").String()) if value == "" { value = strings.TrimSpace(info.Get("cardType").String()) } if value == "" { return nil } cardType, err := strconv.Atoi(value) if err != nil || cardType <= 0 { return nil } return cardType } func formatTime(t *gtime.Time) string { if t == nil { return "" } return t.Format("Y-m-d H:i:s") } func chooseString(primary, fallback string) string { primary = strings.TrimSpace(primary) if primary != "" { return primary } return strings.TrimSpace(fallback) } type outboundPaymentSnapshot struct { PaymentMode string PayType int PayStatus int PayAmount float64 BalanceAmount float64 OfflinePaidAmount float64 BalanceBefore float64 BalanceAfter float64 BalanceDetailSerialNo string PaymentTime *gtime.Time PayerDeptId int64 } func prepareOutboundPayment(ctx context.Context, tx gdb.TX, batchNo string, amount float64, operator batchimport.OperatorContext, in *model.TSimCardOutboundSubmitParam) (*outboundPaymentSnapshot, error) { if in == nil { return nil, gerror.New("出库付款参数不能为空") } if amount < 0 { return nil, gerror.New("订单金额不能小于0") } now := gtime.Now() if in.OfflineContractCompleted { return &outboundPaymentSnapshot{ PaymentMode: outboundPaymentModeOffline, PayType: consts.PayTypeOffline, PayStatus: consts.PayStatusPaid, PayAmount: amount, OfflinePaidAmount: amount, PaymentTime: now, PayerDeptId: in.CustomerAgentId, }, nil } return deductOutboundBalanceTx(ctx, tx, batchNo, amount, now, operator, in.CustomerAgentId) } func deductOutboundBalanceTx(ctx context.Context, tx gdb.TX, batchNo string, amount float64, paymentTime *gtime.Time, operator batchimport.OperatorContext, payerDeptID int64) (*outboundPaymentSnapshot, error) { if tx == nil { return nil, gerror.New("出库余额支付必须在事务中执行") } if operator.DeptId <= 0 || operator.TenantId == "" || operator.UserId <= 0 { return nil, gerror.New("当前用户未绑定组织、租户或用户信息") } if payerDeptID <= 0 { return nil, gerror.New("出库客户不能为空") } if paymentTime == nil { paymentTime = gtime.Now() } serialNo := outboundBalanceSerialNo(batchNo) if serialNo == "" { return nil, gerror.New("出库余额流水号生成失败") } account := &entity.TAgentBalance{} if err := tx.Ctx(ctx).Model(dao.TAgentBalance.Table()). Where(dao.TAgentBalance.Columns().DeptId, payerDeptID). Where(dao.TAgentBalance.Columns().TenantId, operator.TenantId). LockUpdate(). Scan(account); err != nil && !errors.Is(err, sql.ErrNoRows) { return nil, gerror.Wrap(err, "查询账户余额失败") } if account.Id == 0 { return nil, gerror.New("客户暂无余额账户,请先充值") } if account.Status != outboundFinanceAccountStatusNormal { return nil, gerror.New("账户余额已冻结,不能扣余额") } record, err := tx.Ctx(ctx).Model(dao.TBalanceDetail.Table()). Where(dao.TBalanceDetail.Columns().TenantId, operator.TenantId). Where(dao.TBalanceDetail.Columns().SerialNo, serialNo). LockUpdate(). One() if err != nil && !errors.Is(err, sql.ErrNoRows) { return nil, gerror.Wrap(err, "查询出库余额流水失败") } if record != nil && !record.IsEmpty() { return nil, gerror.New("当前出库批次已扣款,请勿重复提交") } before := roundOutboundAmount(account.Balance) if before+0.0001 < amount { return nil, gerror.Newf("账户余额不足,需支付%.2f元,当前余额%.2f元", amount, before) } after := roundOutboundAmount(before - amount) now := gtime.Now() result, err := tx.Ctx(ctx).Model(dao.TAgentBalance.Table()). WherePri(account.Id). Data(g.Map{ dao.TAgentBalance.Columns().Balance: after, dao.TAgentBalance.Columns().UpdatedBy: operator.UserId, dao.TAgentBalance.Columns().UpdatedAt: now, }). Update() if err != nil { return nil, gerror.Wrap(err, "扣减账户余额失败") } affected, err := result.RowsAffected() if err != nil { return nil, gerror.Wrap(err, "读取账户余额更新结果失败") } if affected != 1 { return nil, gerror.New("账户余额已变化,请重试") } if _, err = tx.Ctx(ctx).Model(dao.TBalanceDetail.Table()).Data(g.Map{ dao.TBalanceDetail.Columns().DeptId: payerDeptID, dao.TBalanceDetail.Columns().SerialNo: serialNo, dao.TBalanceDetail.Columns().TransactionTime: paymentTime, dao.TBalanceDetail.Columns().IncomeExpenseType: outboundFinanceIncomeExpenseExpense, dao.TBalanceDetail.Columns().TransactionType: outboundFinanceTransactionTypeConsume, dao.TBalanceDetail.Columns().TransactionChannel: outboundFinanceTransactionChannelBalance, dao.TBalanceDetail.Columns().TransactionStatus: outboundFinanceTransactionStatusPaid, dao.TBalanceDetail.Columns().Amount: amount, dao.TBalanceDetail.Columns().BeforeBalance: before, dao.TBalanceDetail.Columns().BalanceAfter: after, dao.TBalanceDetail.Columns().OrderSn: batchNo, dao.TBalanceDetail.Columns().Remark: fmt.Sprintf("SIM卡出库余额支付,批次号:%s", batchNo), dao.TBalanceDetail.Columns().TenantId: operator.TenantId, dao.TBalanceDetail.Columns().CreatedDept: operator.DeptId, dao.TBalanceDetail.Columns().CreatedBy: operator.UserId, dao.TBalanceDetail.Columns().CreatedAt: now, }).Insert(); err != nil { return nil, gerror.Wrap(err, "写入出库余额流水失败") } return &outboundPaymentSnapshot{ PaymentMode: outboundPaymentModeBalance, PayType: consts.PayTypeBalance, PayStatus: consts.PayStatusPaid, PayAmount: amount, BalanceAmount: amount, BalanceBefore: before, BalanceAfter: after, BalanceDetailSerialNo: serialNo, PaymentTime: paymentTime, PayerDeptId: payerDeptID, }, nil } func subscriptionAmount(in *model.TSimCardOutboundSubmitParam) float64 { amount, _ := parseOutboundAmount(in.Amount) return amount } func parseOutboundAmount(value string) (float64, error) { value = strings.TrimSpace(value) if value == "" { return 0, gerror.New("订单金额不能为空") } amount, err := strconv.ParseFloat(value, 64) if err != nil { return 0, gerror.New("订单金额格式不正确") } if amount < 0 { return 0, gerror.New("订单金额不能小于0") } return roundOutboundAmount(amount), nil } func roundOutboundAmount(value float64) float64 { return math.Round(value*100) / 100 } func outboundBalanceSerialNo(batchNo string) string { batchNo = strings.TrimSpace(batchNo) if batchNo == "" { return "" } return "OUTPAY" + batchNo } func displayDurationValue(years, months int) int { if years > 0 { return years } if months > 0 && months%12 == 0 { return months / 12 } return months } func displayDurationLabel(years, monthPart, totalMonths int) string { if years <= 0 && monthPart <= 0 && totalMonths > 0 { years = totalMonths / 12 monthPart = totalMonths % 12 } parts := make([]string, 0, 2) if years > 0 { parts = append(parts, fmt.Sprintf("%d年", years)) } if monthPart > 0 { parts = append(parts, fmt.Sprintf("%d个月", monthPart)) } if len(parts) == 0 && totalMonths > 0 { parts = append(parts, fmt.Sprintf("%d个月", totalMonths)) } return strings.Join(parts, "") } func packageTotalFlowMB(pkg *entity.TSimFlowPackageInfo) float64 { if pkg == nil { return 0 } if pkg.FlowMb > 0 { return pkg.FlowMb } if pkg.FlowGb > 0 { return pkg.FlowGb * 1024 } return 0 }