chore: 并入 iotlcm-admin-svr 历史到 admin-svr/(原提交 49818c6)

This commit is contained in:
MiFaZhan
2026-11-02 13:27:59 +08:00
1250 changed files with 154111 additions and 0 deletions
@@ -0,0 +1,777 @@
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
}