59eb34976a
iotlcm-admin-svr:管理后台服务。 - Go module:iotlcm → git.5iot.com/iotlcm-public/iotlcm-admin-svr(729 文件) - 凭据与地址脱敏:JWT signingKey(含代码里的 MustGet 默认值)、 MySQL DSN、Redis 口令、告警邮件发件人邮箱全部改为占位; 私网 IP → 文档地址段;库名 xiujie_admin → iotlcm - 真实手机号/邮箱 → 合法 11 位编造号 / example.com - 移除内部资产:CI 触发器、过时 DDL(sql/db_iotlcm.sql)、 私有依赖 go get 行、明文 registry 登录口令 - 启动 banner 统一 IOTLCM 方块字 + 社区惯例署名 - Dockerfile 改用公共基础镜像 - 拼错的 RADEME.md → README.md,README 重写 - 需要真实数据库的用例改为可用 IOTLCM_TEST_WITH_DB=1 显式启用 - 补 Apache-2.0 LICENSE,LF 行尾 + .gitattributes 数据库 schema 不随本仓发布(生产 DDL 已过时),README 已说明。
778 lines
26 KiB
Go
778 lines
26 KiB
Go
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
|
|
}
|