mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-08 16:46:37 +08:00
压缩历史至 95081aff
This commit is contained in:
@@ -0,0 +1,192 @@
|
||||
/*
|
||||
Copyright 2025 linux.do
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/hibiken/asynq"
|
||||
"github.com/linux-do/credit/internal/common"
|
||||
"github.com/linux-do/credit/internal/model"
|
||||
"github.com/linux-do/credit/internal/task"
|
||||
"github.com/linux-do/credit/internal/task/scheduler"
|
||||
"github.com/shopspring/decimal"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// BalanceOperation 余额操作类型
|
||||
type BalanceOperation int
|
||||
|
||||
const (
|
||||
BalanceAdd BalanceOperation = iota
|
||||
BalanceDeduct
|
||||
)
|
||||
|
||||
// BalanceUpdateOptions 余额更新选项
|
||||
type BalanceUpdateOptions struct {
|
||||
UserID uint64
|
||||
Amount decimal.Decimal
|
||||
Operation BalanceOperation
|
||||
ScoreChange int64
|
||||
TotalField string // 累计字段:total_payment / total_receive / total_transfer
|
||||
CheckBalance bool
|
||||
AsyncTransfer bool // 异步结算时使用 pending_balance 字段
|
||||
}
|
||||
|
||||
// UpdateBalance 通用余额更新函数
|
||||
func UpdateBalance(tx *gorm.DB, opts BalanceUpdateOptions) error {
|
||||
updates := make(map[string]interface{})
|
||||
|
||||
balanceField := "available_balance"
|
||||
if opts.AsyncTransfer {
|
||||
balanceField = "pending_balance"
|
||||
}
|
||||
|
||||
if opts.Operation == BalanceAdd {
|
||||
updates[balanceField] = gorm.Expr(balanceField+" + ?", opts.Amount)
|
||||
} else {
|
||||
updates[balanceField] = gorm.Expr(balanceField+" - ?", opts.Amount)
|
||||
}
|
||||
|
||||
if opts.TotalField != "" {
|
||||
updates[opts.TotalField] = gorm.Expr(opts.TotalField+" + ?", opts.Amount)
|
||||
}
|
||||
|
||||
if opts.ScoreChange != 0 {
|
||||
updates["pay_score"] = gorm.Expr("pay_score + ?", opts.ScoreChange)
|
||||
}
|
||||
|
||||
query := tx.Model(&model.User{}).Where("id = ?", opts.UserID)
|
||||
if opts.CheckBalance {
|
||||
query = query.Where(balanceField+" >= ?", opts.Amount)
|
||||
}
|
||||
|
||||
result := query.UpdateColumns(updates)
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if opts.CheckBalance && result.RowsAffected == 0 {
|
||||
return errors.New(common.InsufficientBalance)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SettlePendingToAvailable 将资金从 PendingBalance 转入 AvailableBalance
|
||||
func SettlePendingToAvailable(tx *gorm.DB, userID uint64, amount decimal.Decimal) error {
|
||||
result := tx.Model(&model.User{}).
|
||||
Where("id = ? AND pending_balance >= ?", userID, amount).
|
||||
UpdateColumns(map[string]interface{}{
|
||||
"pending_balance": gorm.Expr("pending_balance - ?", amount),
|
||||
"available_balance": gorm.Expr("available_balance + ?", amount),
|
||||
})
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return errors.New(common.InsufficientBalance)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckDailyLimit 检查用户每日支付限额
|
||||
// 返回 nil 表示未超限额,返回 error 表示超限或查询失败
|
||||
func CheckDailyLimit(tx *gorm.DB, userID uint64, amount decimal.Decimal, dailyLimit *int64) error {
|
||||
if dailyLimit == nil || *dailyLimit <= 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
datePart := int64(now.Year()*10000 + int(now.Month())*100 + now.Day())
|
||||
lockID := int64(userID)*100000000 + datePart
|
||||
if err := tx.Exec("SELECT pg_advisory_xact_lock(?)", lockID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
todayUsed, err := GetTodayUsedAmount(tx, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if todayUsed.Add(amount).GreaterThan(decimal.NewFromInt(*dailyLimit)) {
|
||||
return errors.New(common.DailyLimitExceeded)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetTodayUsedAmount 获取用户当日已使用的支付额度
|
||||
func GetTodayUsedAmount(db *gorm.DB, userID uint64) (decimal.Decimal, error) {
|
||||
now := time.Now()
|
||||
todayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
||||
todayEnd := todayStart.Add(24 * time.Hour)
|
||||
|
||||
var total decimal.Decimal
|
||||
err := db.Model(&model.Order{}).
|
||||
Where("payer_user_id = ? AND status = ? AND type IN ? AND trade_time >= ? AND trade_time < ?",
|
||||
userID,
|
||||
model.OrderStatusSuccess,
|
||||
[]model.OrderType{model.OrderTypePayment, model.OrderTypeOnline, model.OrderTypeDistribute, model.OrderTypeTransfer},
|
||||
todayStart,
|
||||
todayEnd).
|
||||
Select("COALESCE(SUM(amount), 0)").
|
||||
Scan(&total).Error
|
||||
|
||||
return total, err
|
||||
}
|
||||
|
||||
// CalculateFee 计算手续费和商户实收金额
|
||||
// 返回:手续费、商户实收金额、手续费百分比
|
||||
func CalculateFee(amount decimal.Decimal, feeRate decimal.Decimal) (fee decimal.Decimal, merchantAmount decimal.Decimal, feePercent int64) {
|
||||
fee = amount.Mul(feeRate).Round(2)
|
||||
merchantAmount = amount.Sub(fee)
|
||||
feePercent = feeRate.Mul(decimal.NewFromInt(100)).IntPart()
|
||||
return
|
||||
}
|
||||
|
||||
// ValidateTestModePayment 验证测试模式下的支付权限
|
||||
// 返回 error:nil 表示允许支付,非 nil 表示拒绝支付
|
||||
func ValidateTestModePayment(currentUserID, merchantUserID uint64, isTestMode bool) error {
|
||||
if currentUserID == merchantUserID {
|
||||
if !isTestMode {
|
||||
return errors.New(common.CannotPaySelf)
|
||||
}
|
||||
} else if isTestMode {
|
||||
return errors.New(common.TestModeCannotProcessOrder)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// EnqueueMerchantNotify 下发商户回调任务
|
||||
func EnqueueMerchantNotify(orderID uint64, clientID string) error {
|
||||
notifyPayload, _ := json.Marshal(map[string]interface{}{
|
||||
"order_id": orderID,
|
||||
"client_id": clientID,
|
||||
})
|
||||
if _, err := scheduler.AsynqClient.Enqueue(
|
||||
asynq.NewTask(task.MerchantPaymentNotifyTask, notifyPayload),
|
||||
asynq.Queue(task.QueueWebhook),
|
||||
asynq.MaxRetry(10),
|
||||
asynq.Timeout(30*time.Second),
|
||||
); err != nil {
|
||||
return fmt.Errorf("下发商户回调任务失败: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user