2026-07-07 18:05:02 +08:00

276 lines
8.5 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package financeorder
import (
"context"
"database/sql"
"errors"
"fmt"
"strconv"
"strings"
"sync/atomic"
"time"
"hyapp-admin-server/internal/config"
)
var (
errLegacyCoinSellerNotFound = errors.New("legacy coin seller not found")
errLegacyCoinSellerOrderDuplicate = errors.New("legacy coin seller recharge order already exists")
)
type coinSellerTarget struct {
UserID int64
DisplayUserID string
CountryID int64
RegionID int64
}
type legacyRechargeGrantInput struct {
OrderID uint
UserID int64
ExternalOrderNo string
ProviderCode string
Chain string
USDAmount string
CoinAmount int64
OperatorUserID uint
GrantedAt time.Time
}
type legacyRechargeGrantResult struct {
TransactionID string
BalanceAfter int64
}
// LegacyCoinSellerRechargeWriter 把 admin 订单发放到 Aslan/Yumi 独立 legacy 钱包账套;
// admin 表只保存审计和绑定事实,具体余额/流水仍按旧系统表结构写入。
type LegacyCoinSellerRechargeWriter interface {
AppCode() string
Close() error
ResolveCoinSeller(ctx context.Context, keyword string) (coinSellerTarget, error)
GrantCoinSellerRecharge(ctx context.Context, input legacyRechargeGrantInput) (legacyRechargeGrantResult, error)
}
type mysqlLegacyCoinSellerRechargeWriter struct {
appCode string
sysOrigin string
appDB *sql.DB
walletDB *sql.DB
requestTimeout time.Duration
}
func NewMySQLLegacyCoinSellerRechargeWriter(source config.LegacyCoinSellerRechargeSourceConfig, appDB *sql.DB, walletDB *sql.DB) LegacyCoinSellerRechargeWriter {
timeout := source.RequestTimeout
if timeout <= 0 {
timeout = 5 * time.Second
}
return &mysqlLegacyCoinSellerRechargeWriter{
appCode: strings.ToLower(strings.TrimSpace(source.AppCode)),
sysOrigin: strings.ToUpper(strings.TrimSpace(source.SysOrigin)),
appDB: appDB,
walletDB: walletDB,
requestTimeout: timeout,
}
}
func (w *mysqlLegacyCoinSellerRechargeWriter) AppCode() string {
if w == nil {
return ""
}
return w.appCode
}
func (w *mysqlLegacyCoinSellerRechargeWriter) Close() error {
if w == nil {
return nil
}
var first error
if w.appDB != nil {
first = w.appDB.Close()
}
if w.walletDB != nil {
if err := w.walletDB.Close(); first == nil {
first = err
}
}
return first
}
func (w *mysqlLegacyCoinSellerRechargeWriter) ResolveCoinSeller(ctx context.Context, keyword string) (coinSellerTarget, error) {
if w == nil || w.appDB == nil || w.walletDB == nil {
return coinSellerTarget{}, errors.New("legacy coin seller recharge writer is not configured")
}
user, err := w.resolveLegacyUser(ctx, keyword)
if err != nil {
return coinSellerTarget{}, err
}
if err := w.requireLegacyCoinSeller(ctx, user.UserID); err != nil {
return coinSellerTarget{}, err
}
return user, nil
}
func (w *mysqlLegacyCoinSellerRechargeWriter) resolveLegacyUser(ctx context.Context, keyword string) (coinSellerTarget, error) {
keyword = strings.TrimSpace(keyword)
if keyword == "" {
return coinSellerTarget{}, errLegacyCoinSellerNotFound
}
queryCtx, cancel := context.WithTimeout(ctx, w.requestTimeout)
defer cancel()
args := []any{w.sysOrigin, keyword, keyword}
matchSQL := "(account = ? OR CAST(id AS CHAR) = ?"
if numeric, err := strconv.ParseInt(keyword, 10, 64); err == nil && numeric > 0 {
matchSQL += " OR id = ?"
args = append(args, numeric)
}
matchSQL += ")"
row := w.appDB.QueryRowContext(queryCtx, `
SELECT id, account, COALESCE(country_id, 0)
FROM user_base_info
WHERE origin_sys = ? AND `+matchSQL+` AND COALESCE(is_del, 0) = 0
ORDER BY id DESC
LIMIT 1`, args...)
var target coinSellerTarget
if err := row.Scan(&target.UserID, &target.DisplayUserID, &target.CountryID); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return coinSellerTarget{}, errLegacyCoinSellerNotFound
}
return coinSellerTarget{}, err
}
if strings.TrimSpace(target.DisplayUserID) == "" {
target.DisplayUserID = formatInt64(target.UserID)
}
return target, nil
}
func (w *mysqlLegacyCoinSellerRechargeWriter) requireLegacyCoinSeller(ctx context.Context, userID int64) error {
queryCtx, cancel := context.WithTimeout(ctx, w.requestTimeout)
defer cancel()
row := w.walletDB.QueryRowContext(queryCtx, `
SELECT id
FROM user_freight_balance
WHERE sys_origin = ? AND user_id = ? AND COALESCE(is_close, 0) = 0 AND (COALESCE(dealer, 0) = 1 OR COALESCE(super_dealer, 0) = 1)
LIMIT 1`, w.sysOrigin, userID)
var id int64
if err := row.Scan(&id); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return errLegacyCoinSellerNotFound
}
return err
}
return nil
}
func (w *mysqlLegacyCoinSellerRechargeWriter) GrantCoinSellerRecharge(ctx context.Context, input legacyRechargeGrantInput) (legacyRechargeGrantResult, error) {
if w == nil || w.walletDB == nil {
return legacyRechargeGrantResult{}, errors.New("legacy coin seller recharge writer is not configured")
}
queryCtx, cancel := context.WithTimeout(ctx, w.requestTimeout)
defer cancel()
tx, err := w.walletDB.BeginTx(queryCtx, &sql.TxOptions{})
if err != nil {
return legacyRechargeGrantResult{}, err
}
defer func() {
_ = tx.Rollback()
}()
var (
balanceID int64
earnPointsText string
consumptionText string
)
if err := tx.QueryRowContext(queryCtx, `
SELECT id, CAST(COALESCE(earn_points, 0) AS CHAR), CAST(COALESCE(consumption_points, 0) AS CHAR)
FROM user_freight_balance
WHERE sys_origin = ? AND user_id = ? AND COALESCE(is_close, 0) = 0 AND (COALESCE(dealer, 0) = 1 OR COALESCE(super_dealer, 0) = 1)
ORDER BY id DESC
LIMIT 1
FOR UPDATE`, w.sysOrigin, input.UserID).Scan(&balanceID, &earnPointsText, &consumptionText); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return legacyRechargeGrantResult{}, errLegacyCoinSellerNotFound
}
return legacyRechargeGrantResult{}, err
}
var duplicateID int64
if err := tx.QueryRowContext(queryCtx, `
SELECT id
FROM user_freight_balance_running_water
WHERE sys_origin = ? AND remark = ?
LIMIT 1`, w.sysOrigin, input.ExternalOrderNo).Scan(&duplicateID); err != nil && !errors.Is(err, sql.ErrNoRows) {
return legacyRechargeGrantResult{}, err
} else if duplicateID > 0 {
return legacyRechargeGrantResult{}, errLegacyCoinSellerOrderDuplicate
}
if _, err := tx.ExecContext(queryCtx, `
UPDATE user_freight_balance
SET earn_points = COALESCE(earn_points, 0) + ?, update_user = ?, update_time = ?
WHERE id = ?`, input.CoinAmount, input.UserID, input.GrantedAt, balanceID); err != nil {
return legacyRechargeGrantResult{}, err
}
balanceAfter, err := decimalBalanceAfter(earnPointsText, consumptionText, input.CoinAmount)
if err != nil {
return legacyRechargeGrantResult{}, err
}
waterID := legacySnowflakeLikeID(input.GrantedAt)
rechargeType := legacyRechargeType(input.ProviderCode, input.Chain)
if _, err := tx.ExecContext(queryCtx, `
INSERT INTO user_freight_balance_running_water
(id, sys_origin, user_id, accept_user_id, seller_id, type, quantity, usd_quantity, balance, amount, recharge_type, origin, remark, create_user, update_user, create_time, update_time)
VALUES
(?, ?, ?, ?, ?, 0, ?, 0, ?, ?, ?, 'PURCHASE', ?, ?, ?, ?, ?)`,
waterID,
w.sysOrigin,
input.UserID,
input.UserID,
nil,
input.CoinAmount,
balanceAfter,
input.USDAmount,
rechargeType,
input.ExternalOrderNo,
input.UserID,
input.UserID,
input.GrantedAt,
input.GrantedAt,
); err != nil {
return legacyRechargeGrantResult{}, err
}
if err := tx.Commit(); err != nil {
return legacyRechargeGrantResult{}, err
}
return legacyRechargeGrantResult{
TransactionID: fmt.Sprintf("legacy-freight-purchase-%d", waterID),
BalanceAfter: balanceAfter,
}, nil
}
func decimalBalanceAfter(earnPointsText string, consumptionText string, delta int64) (int64, error) {
earn, err := strconv.ParseFloat(strings.TrimSpace(earnPointsText), 64)
if err != nil {
return 0, err
}
consumption, err := strconv.ParseFloat(strings.TrimSpace(consumptionText), 64)
if err != nil {
return 0, err
}
return int64(earn) + delta - int64(consumption), nil
}
func legacyRechargeType(providerCode string, chain string) string {
providerCode = strings.ToUpper(strings.TrimSpace(providerCode))
if providerCode == "USDT" {
if chain = strings.ToUpper(strings.TrimSpace(chain)); chain != "" {
return "USDT-" + chain + "-进货"
}
return "USDT-进货"
}
return providerCode + "-进货"
}
func legacySnowflakeLikeID(now time.Time) int64 {
// 旧服务用 MyBatis ASSIGN_ID 写 bigint 主键;后台没有接入旧雪花 worker只需保证单进程内足够唯一且单调。
return now.UTC().UnixMilli()*1000 + int64(legacyWaterIDCounter.Add(1)%1000)
}
var legacyWaterIDCounter atomic.Uint32