Files
iotlcm/internal/service/renewchange/renew_refund.go
T
MiFaZhan 2bec257289 chore: 初始开源发布
iotlcm-worker-svr:异步执行中枢(RabbitMQ 消费者 + 全量同步编排)。

- Go module:iotlcm-worker-svr → git.5iot.com/iotlcm-public/iotlcm-worker-svr(214 文件)
- 凭据与内部标识脱敏:MySQL/Redis 凭据、私网与生产 IP、内网域名、
  库名 xiujie_admin → iotlcm、哨兵名占位
- 159 处真实手机号 → 合法 11 位编造号(保留号段与后 4 位,维持互异,
  不破坏「校验 MSISDN 格式」等测试断言)
- 移除内部资产:CI 触发器、Jenkins 触发测试文档、预演门禁与镜像
  digest 记录
- Dockerfile 改用公共基础镜像(golang:1.25-alpine → alpine:3.23),
  移除写死的 git 凭据
- README 重写(架构/全量同步模型/配置/已知失败),补 Apache-2.0
  LICENSE,LF 行尾 + .gitattributes

已知:3 个测试包在源仓同一提交上同样失败(client/upstream、
service/allocate、service/inbound),属既有缺陷,本仓未引入亦未修复。
2026-10-22 16:37:56 +08:00

177 lines
6.4 KiB
Go

package renewchange
import (
"context"
"database/sql"
"errors"
"fmt"
"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/gtime"
rc "git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/repository/renewchange"
)
const (
renewFinanceIncomeExpenseIncome = 0
renewFinanceTransactionTypeConsume = 1
renewFinanceTransactionChannelBalance = 3
renewFinanceTransactionStatusPaid = 1
)
type renewBalanceAccountRow struct {
ID int64 `orm:"id"`
Balance float64 `orm:"balance"`
}
type renewBalanceDetailRow struct {
ID int64 `orm:"id"`
DeptID int64 `orm:"dept_id"`
SerialNo string `orm:"serial_no"`
Amount float64 `orm:"amount"`
TransactionStatus int `orm:"transaction_status"`
TenantID string `orm:"tenant_id"`
CreatedDept int64 `orm:"created_dept"`
CreatedBy int64 `orm:"created_by"`
}
func refundRenewBalanceIfNeeded(ctx context.Context, tx gdb.TX, main *rc.BatchMainRow, contract *ExecutionContract, reason string, now *gtime.Time) error {
if main == nil || contract == nil {
return nil
}
if contract.PaymentMode != "balance" || strings.TrimSpace(contract.BalanceDetailSerialNo) == "" || contract.BalanceAmount <= 0 {
return nil
}
if strings.TrimSpace(main.OrderSn) != "" {
return nil
}
refundSerialNo := "REFUND" + strings.TrimSpace(contract.BalanceDetailSerialNo)
var existing renewBalanceDetailRow
existingRecord, err := tx.Ctx(ctx).Model(rc.TableBalanceDetail).
Where("tenant_id", contract.TenantID).
Where("serial_no", refundSerialNo).
LockUpdate().
One()
if err != nil && !errors.Is(err, sql.ErrNoRows) {
return gerror.Wrap(err, "query renew refund detail failed")
}
if existingRecord != nil && !existingRecord.IsEmpty() {
if err := existingRecord.Struct(&existing); err != nil {
return gerror.Wrap(err, "decode renew refund detail failed")
}
}
if existing.ID != 0 {
return nil
}
var original renewBalanceDetailRow
originalRecord, err := tx.Ctx(ctx).Model(rc.TableBalanceDetail).
Where("tenant_id", contract.TenantID).
Where("serial_no", strings.TrimSpace(contract.BalanceDetailSerialNo)).
LockUpdate().
One()
if err != nil && !errors.Is(err, sql.ErrNoRows) {
return gerror.Wrap(err, "query renew payment detail failed")
}
if originalRecord != nil && !originalRecord.IsEmpty() {
if err := originalRecord.Struct(&original); err != nil {
return gerror.Wrap(err, "decode renew payment detail failed")
}
}
if original.ID == 0 {
g.Log().Warningf(ctx, "renew balance refund skipped, original payment detail missing, batchNo=%s serialNo=%s",
main.BatchNo, contract.BalanceDetailSerialNo)
return nil
}
payerDeptID := contract.PayerDeptID
if payerDeptID <= 0 {
payerDeptID = original.DeptID
}
var account renewBalanceAccountRow
accountRecord, err := tx.Ctx(ctx).Model(rc.TableAgentBalance).
Where("dept_id", payerDeptID).
Where("tenant_id", contract.TenantID).
LockUpdate().
One()
if err != nil && !errors.Is(err, sql.ErrNoRows) {
return gerror.Wrap(err, "lock renew refund account failed")
}
if accountRecord != nil && !accountRecord.IsEmpty() {
if err := accountRecord.Struct(&account); err != nil {
return gerror.Wrap(err, "decode renew refund account failed")
}
}
if account.ID == 0 {
return gerror.New("renew refund account missing")
}
if now == nil {
now = gtime.Now()
}
amount := roundAmount(original.Amount)
before := roundAmount(account.Balance)
after := roundAmount(before + amount)
if _, err := tx.Ctx(ctx).Model(rc.TableAgentBalance).Where("id", account.ID).Data(g.Map{
"balance": after,
"updated_by": contract.OperatorUserID,
"updated_at": now,
}).Update(); err != nil {
return gerror.Wrap(err, "refund renew balance failed")
}
remark := fmt.Sprintf("SIM卡续费批次失败退款,批次号:%s,原因:%s", main.BatchNo, strings.TrimSpace(reason))
if _, err := tx.Ctx(ctx).Model(rc.TableBalanceDetail).Data(g.Map{
"dept_id": payerDeptID,
"serial_no": refundSerialNo,
"transaction_time": now,
"income_expense_type": renewFinanceIncomeExpenseIncome,
"transaction_type": renewFinanceTransactionTypeConsume,
"transaction_channel": renewFinanceTransactionChannelBalance,
"transaction_status": renewFinanceTransactionStatusPaid,
"amount": amount,
"before_balance": before,
"balance_after": after,
"order_sn": main.BatchNo,
"remark": remark,
"tenant_id": contract.TenantID,
"created_dept": original.CreatedDept,
"created_by": original.CreatedBy,
"created_at": now,
}).Insert(); err != nil {
return gerror.Wrap(err, "insert renew refund detail failed")
}
g.Log().Warningf(ctx, "renew balance refunded, batchNo=%s serialNo=%s refundSerialNo=%s amount=%.2f reason=%s",
main.BatchNo, contract.BalanceDetailSerialNo, refundSerialNo, amount, reason)
return nil
}
func renewRefundContractFromBatch(main *rc.BatchMainRow) *ExecutionContract {
if main == nil || main.BatchInfo == nil || main.BatchInfo.IsNil() {
return nil
}
info := main.BatchInfo
contract := &ExecutionContract{
BatchNo: strings.TrimSpace(main.BatchNo),
BatchType: main.BatchType,
TenantID: strings.TrimSpace(main.TenantId),
CustomerAgentID: firstInt64(info, "customerAgentId", "priceSnapshot.customerAgentId"),
SellerAgentID: main.DeptId,
CreatedDeptID: main.CreatedDept,
OperatorUserID: main.CreatedBy,
PaymentMode: firstString(info, "paymentMode", "paymentSnapshot.paymentMode"),
PayType: firstInt(info, "payType", "paymentSnapshot.payType"),
PayAmount: firstAmount(info, "payAmount", "paymentSnapshot.payAmount"),
BalanceAmount: firstAmount(info, "balanceAmount", "paymentSnapshot.balanceAmount"),
BalanceDetailSerialNo: firstString(info, "balanceDetailSerialNo", "paymentSnapshot.balanceDetailSerialNo"),
PayerDeptID: firstInt64(info, "payerDeptId", "paymentSnapshot.payerDeptId"),
PayerUserID: firstInt64(info, "payerUserId", "paymentSnapshot.payerUserId"),
}
contract.normalizePaymentContract()
if contract.PaymentMode != "balance" || strings.TrimSpace(contract.BalanceDetailSerialNo) == "" || contract.BalanceAmount <= 0 {
return nil
}
return contract
}