mirror of
https://github.com/GMWalletApp/epusdt.git
synced 2026-07-07 18:26:16 +00:00
init
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
package dao
|
||||
|
||||
import (
|
||||
"github.com/assimon/luuu/config"
|
||||
"github.com/assimon/luuu/util/log"
|
||||
"github.com/gookit/color"
|
||||
"github.com/spf13/viper"
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
"gorm.io/gorm/schema"
|
||||
"time"
|
||||
)
|
||||
|
||||
var Mdb *gorm.DB
|
||||
|
||||
// MysqlInit 数据库初始化
|
||||
func MysqlInit() {
|
||||
var err error
|
||||
Mdb, err = gorm.Open(mysql.Open(config.MysqlDns), &gorm.Config{
|
||||
NamingStrategy: schema.NamingStrategy{
|
||||
TablePrefix: viper.GetString("mysql_table_prefix"),
|
||||
SingularTable: true,
|
||||
},
|
||||
Logger: logger.Default.LogMode(logger.Error),
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if config.AppDebug {
|
||||
Mdb = Mdb.Debug()
|
||||
}
|
||||
sqlDB, err := Mdb.DB()
|
||||
if err != nil {
|
||||
color.Red.Printf("[store_db] mysql get DB,err=%s\n", err)
|
||||
panic(err)
|
||||
}
|
||||
sqlDB.SetMaxIdleConns(viper.GetInt("mysql_max_idle_conns"))
|
||||
sqlDB.SetMaxOpenConns(viper.GetInt("mysql_max_open_conns"))
|
||||
sqlDB.SetConnMaxLifetime(time.Hour * time.Duration(viper.GetInt("mysql_max_life_time")))
|
||||
err = sqlDB.Ping()
|
||||
if err != nil {
|
||||
color.Red.Printf("[store_db] mysql connDB err:%s", err.Error())
|
||||
panic(err)
|
||||
}
|
||||
log.Sugar.Debug("[store_db] mysql connDB success")
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package dao
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/assimon/luuu/util/log"
|
||||
"github.com/go-redis/redis/v8"
|
||||
"github.com/gookit/color"
|
||||
"github.com/spf13/viper"
|
||||
"time"
|
||||
)
|
||||
|
||||
var Rdb *redis.Client
|
||||
|
||||
func RedisInit() {
|
||||
options := redis.Options{
|
||||
Addr: fmt.Sprintf(
|
||||
"%s:%s",
|
||||
viper.GetString("redis_host"),
|
||||
viper.GetString("redis_port")), // Redis地址
|
||||
DB: viper.GetInt("redis_db"), // Redis库
|
||||
PoolSize: viper.GetInt("redis_pool_size"), // Redis连接池大小
|
||||
MaxRetries: viper.GetInt("redis_max_retries"), // 最大重试次数
|
||||
IdleTimeout: time.Second * time.Duration(viper.GetInt("redis_idle_timeout")), // 空闲链接超时时间
|
||||
}
|
||||
if viper.GetString("redis_passwd") != "" {
|
||||
options.Password = viper.GetString("redis_passwd")
|
||||
}
|
||||
Rdb = redis.NewClient(&options)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
pong, err := Rdb.Ping(ctx).Result()
|
||||
if err == redis.Nil {
|
||||
log.Sugar.Debug("[store_redis] Nil reply returned by Rdb when key does not exist.")
|
||||
} else if err != nil {
|
||||
color.Red.Printf("[store_redis] redis connRdb err,err=%s", err)
|
||||
panic(err)
|
||||
} else {
|
||||
log.Sugar.Debug("[store_redis] redis connRdb success,suc=%s", pong)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/assimon/luuu/model/dao"
|
||||
"github.com/assimon/luuu/model/mdb"
|
||||
"github.com/assimon/luuu/model/request"
|
||||
"github.com/go-redis/redis/v8"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var (
|
||||
CacheWalletAddressToAvailableAmountKey = "WalletAddressToAvailableAmountKey:" // 钱包 - 可用金额 - 过期时间
|
||||
CacheWalletAddressToOrdersKey = "WalletAddressToOrdersKey:" // 钱包 - 待支付金额 - 订单号
|
||||
)
|
||||
|
||||
// GetOrderInfoByOrderId 通过客户订单号查询订单
|
||||
func GetOrderInfoByOrderId(orderId string) (*mdb.Orders, error) {
|
||||
order := new(mdb.Orders)
|
||||
err := dao.Mdb.Model(order).Limit(1).Find(order, "order_id = ?", orderId).Error
|
||||
return order, err
|
||||
}
|
||||
|
||||
// GetOrderInfoByTradeId 通过交易号查询订单
|
||||
func GetOrderInfoByTradeId(tradeId string) (*mdb.Orders, error) {
|
||||
order := new(mdb.Orders)
|
||||
err := dao.Mdb.Model(order).Limit(1).Find(order, "trade_id = ?", tradeId).Error
|
||||
return order, err
|
||||
}
|
||||
|
||||
// CreateOrderWithTransaction 事务创建订单
|
||||
func CreateOrderWithTransaction(tx *gorm.DB, order *mdb.Orders) error {
|
||||
err := tx.Model(order).Create(order).Error
|
||||
return err
|
||||
}
|
||||
|
||||
// GetOrderByBlockIdWithTransaction 通过区块获取订单
|
||||
func GetOrderByBlockIdWithTransaction(tx *gorm.DB, blockId string) (*mdb.Orders, error) {
|
||||
order := new(mdb.Orders)
|
||||
err := tx.Model(order).Limit(1).Find(order, "block_transaction_id = ?", blockId).Error
|
||||
return order, err
|
||||
}
|
||||
|
||||
// OrderSuccessWithTransaction 事务支付成功
|
||||
func OrderSuccessWithTransaction(tx *gorm.DB, req *request.OrderProcessingRequest) error {
|
||||
err := tx.Model(&mdb.Orders{}).Where("trade_id = ?", req.TradeId).Updates(map[string]interface{}{
|
||||
"block_transaction_id": req.BlockTransactionId,
|
||||
"status": mdb.StatusPaySuccess,
|
||||
"callback_confirm": mdb.CallBackConfirmNo,
|
||||
}).Error
|
||||
return err
|
||||
}
|
||||
|
||||
// GetPendingCallbackOrders 查询出等待回调的订单
|
||||
func GetPendingCallbackOrders() ([]mdb.Orders, error) {
|
||||
var orders []mdb.Orders
|
||||
err := dao.Mdb.Model(orders).
|
||||
Where("callback_num < ?", 5).
|
||||
Where("callback_confirm = ?", mdb.CallBackConfirmNo).
|
||||
Where("status = ?", mdb.StatusPaySuccess).
|
||||
Find(&orders).Error
|
||||
return orders, err
|
||||
}
|
||||
|
||||
// SaveCallBackOrdersResp 保存订单回调结果
|
||||
func SaveCallBackOrdersResp(order *mdb.Orders) error {
|
||||
err := dao.Mdb.Model(order).Where("id = ?", order.ID).Updates(map[string]interface{}{
|
||||
"callback_num": gorm.Expr("callback_num + ?", 1),
|
||||
"callback_confirm": order.CallBackConfirm,
|
||||
}).Error
|
||||
return err
|
||||
}
|
||||
|
||||
// UpdateOrderIsExpirationById 通过id设置订单过期
|
||||
func UpdateOrderIsExpirationById(id uint64) error {
|
||||
err := dao.Mdb.Model(mdb.Orders{}).Where("id = ?", id).Update("status", mdb.StatusExpired).Error
|
||||
return err
|
||||
}
|
||||
|
||||
// LockPayCache 锁定支付池
|
||||
func LockPayCache(token, tradeId, amount string, expirationTime int64) error {
|
||||
// 载入Redis
|
||||
ctx := context.Background()
|
||||
pipe := dao.Rdb.TxPipeline()
|
||||
lockAvailableAmountKey := fmt.Sprintf("%s%s", CacheWalletAddressToAvailableAmountKey, token)
|
||||
// 占用金额
|
||||
pipe.HSet(ctx, lockAvailableAmountKey, amount, expirationTime)
|
||||
// 标记金额对应订单号
|
||||
lockWalletOrdersKey := fmt.Sprintf("%s%s", CacheWalletAddressToOrdersKey, token)
|
||||
pipe.HSet(ctx, lockWalletOrdersKey, amount, tradeId)
|
||||
_, err := pipe.Exec(ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
// ClearPayCache 清理支付池
|
||||
func ClearPayCache(token, amount string) error {
|
||||
ctx := context.Background()
|
||||
lockAvailableAmountKey := fmt.Sprintf("%s%s", CacheWalletAddressToAvailableAmountKey, token)
|
||||
lockWalletOrdersKey := fmt.Sprintf("%s%s", CacheWalletAddressToOrdersKey, token)
|
||||
pipe := dao.Rdb.TxPipeline()
|
||||
pipe.HDel(ctx, lockAvailableAmountKey, amount)
|
||||
pipe.HDel(ctx, lockWalletOrdersKey, amount)
|
||||
_, err := pipe.Exec(ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
func GetTradeIdByAmount(ctx context.Context, token, amount string) (string, error) {
|
||||
cacheKey := fmt.Sprintf("%s%s", CacheWalletAddressToOrdersKey, token)
|
||||
result, err := dao.Rdb.HGet(ctx, cacheKey, amount).Result()
|
||||
if err == redis.Nil {
|
||||
return "", nil
|
||||
}
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func GetExpirationTimeByAmount(ctx context.Context, token, amount string) (string, error) {
|
||||
cacheKey := fmt.Sprintf("%s%s", CacheWalletAddressToAvailableAmountKey, token)
|
||||
result, err := dao.Rdb.HGet(ctx, cacheKey, amount).Result()
|
||||
if err == redis.Nil {
|
||||
return "", nil
|
||||
}
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"github.com/assimon/luuu/model/dao"
|
||||
"github.com/assimon/luuu/model/mdb"
|
||||
"github.com/assimon/luuu/util/constant"
|
||||
)
|
||||
|
||||
// AddWalletAddress 创建钱包
|
||||
func AddWalletAddress(token string) (*mdb.WalletAddress, error) {
|
||||
exist, err := GetWalletAddressByToken(token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if exist.ID > 0 {
|
||||
return nil, constant.WalletAddressAlreadyExists
|
||||
}
|
||||
walletAddress := &mdb.WalletAddress{
|
||||
Token: token,
|
||||
Status: mdb.TokenStatusEnable,
|
||||
}
|
||||
err = dao.Mdb.Create(walletAddress).Error
|
||||
return walletAddress, err
|
||||
}
|
||||
|
||||
// GetWalletAddressByToken 通过钱包地址获取token
|
||||
func GetWalletAddressByToken(token string) (*mdb.WalletAddress, error) {
|
||||
walletAddress := new(mdb.WalletAddress)
|
||||
err := dao.Mdb.Model(walletAddress).Limit(1).Find(walletAddress, "token = ?", token).Error
|
||||
return walletAddress, err
|
||||
}
|
||||
|
||||
// GetWalletAddressById 通过id获取钱包
|
||||
func GetWalletAddressById(id uint64) (*mdb.WalletAddress, error) {
|
||||
walletAddress := new(mdb.WalletAddress)
|
||||
err := dao.Mdb.Model(walletAddress).Limit(1).Find(walletAddress, id).Error
|
||||
return walletAddress, err
|
||||
}
|
||||
|
||||
// DeleteWalletAddressById 通过id删除钱包
|
||||
func DeleteWalletAddressById(id uint64) error {
|
||||
err := dao.Mdb.Where("id = ?", id).Delete(&mdb.WalletAddress{}).Error
|
||||
return err
|
||||
}
|
||||
|
||||
// GetAvailableWalletAddress 获得所有可用的钱包地址
|
||||
func GetAvailableWalletAddress() ([]mdb.WalletAddress, error) {
|
||||
var WalletAddressList []mdb.WalletAddress
|
||||
err := dao.Mdb.Model(WalletAddressList).Where("status = ?", mdb.TokenStatusEnable).Find(&WalletAddressList).Error
|
||||
return WalletAddressList, err
|
||||
}
|
||||
|
||||
// GetAllWalletAddress 获得所有钱包地址
|
||||
func GetAllWalletAddress() ([]mdb.WalletAddress, error) {
|
||||
var WalletAddressList []mdb.WalletAddress
|
||||
err := dao.Mdb.Model(WalletAddressList).Find(&WalletAddressList).Error
|
||||
return WalletAddressList, err
|
||||
}
|
||||
|
||||
// ChangeWalletAddressStatus 启用禁用钱包
|
||||
func ChangeWalletAddressStatus(id uint64, status int) error {
|
||||
err := dao.Mdb.Model(&mdb.WalletAddress{}).Where("id = ?", id).Update("status", status).Error
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package mdb
|
||||
|
||||
import (
|
||||
"github.com/golang-module/carbon/v2"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type BaseModel struct {
|
||||
ID uint64 `gorm:"column:id;primary_key" json:"id"`
|
||||
CreatedAt carbon.Time `gorm:"column:created_at" json:"created_at"`
|
||||
UpdatedAt carbon.Time `gorm:"column:updated_at" json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at"`
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package mdb
|
||||
|
||||
const (
|
||||
StatusWaitPay = 1
|
||||
StatusPaySuccess = 2
|
||||
StatusExpired = 3
|
||||
CallBackConfirmOk = 1
|
||||
CallBackConfirmNo = 2
|
||||
)
|
||||
|
||||
type Orders struct {
|
||||
TradeId string `gorm:"column:trade_id" json:"trade_id"` // epusdt订单号
|
||||
OrderId string `gorm:"column:order_id" json:"order_id"` // 客户交易id
|
||||
BlockTransactionId string `gorm:"column:block_transaction_id" json:"block_transaction_id"` // 区块id
|
||||
Amount float64 `gorm:"column:amount" json:"amount"` // 订单金额,保留4位小数
|
||||
ActualAmount float64 `gorm:"column:actual_amount" json:"actual_amount"` // 订单实际需要支付的金额,保留4位小数
|
||||
Token string `gorm:"column:token" json:"token"` // 所属钱包地址
|
||||
Status int `gorm:"column:status" json:"status"` // 1:等待支付,2:支付成功,3:已过期
|
||||
NotifyUrl string `gorm:"column:notify_url" json:"notify_url"` // 异步回调地址
|
||||
RedirectUrl string `gorm:"column:redirect_url" json:"redirect_url"` // 同步回调地址
|
||||
CallbackNum int `gorm:"column:callback_num" json:"callback_num"` // 回调次数
|
||||
CallBackConfirm int `gorm:"column:callback_confirm" json:"callback_confirm"` // 回调是否已确认 1是 2否
|
||||
BaseModel
|
||||
}
|
||||
|
||||
// TableName sets the insert table name for this struct type
|
||||
func (o *Orders) TableName() string {
|
||||
return "orders"
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package mdb
|
||||
|
||||
const (
|
||||
TokenStatusEnable = 1
|
||||
TokenStatusDisable = 2
|
||||
)
|
||||
|
||||
// WalletAddress 钱包表
|
||||
type WalletAddress struct {
|
||||
Token string `gorm:"column:token" json:"token"` // 钱包token
|
||||
Status int64 `gorm:"column:status" json:"status"` // 1:启用 2:禁用
|
||||
BaseModel
|
||||
}
|
||||
|
||||
// TableName sets the insert table name for this struct type
|
||||
func (w *WalletAddress) TableName() string {
|
||||
return "wallet_address"
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package request
|
||||
|
||||
const (
|
||||
OrderByFuncDesc = "DESC"
|
||||
OrderByFuncAsc = "OrderByFuncASC"
|
||||
)
|
||||
|
||||
var OrderByFuncList = []string{OrderByFuncDesc, OrderByFuncAsc}
|
||||
|
||||
type BaseRequest struct {
|
||||
Page int `json:"page"` // 页数
|
||||
PageSize int `json:"page_size"` // 每页条数
|
||||
OrderField string `json:"order_field"` // 排序字段
|
||||
OrderFunc string `json:"order_func"` // 排序方法
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package request
|
||||
|
||||
import "github.com/gookit/validate"
|
||||
|
||||
// CreateTransactionRequest 创建交易请求
|
||||
type CreateTransactionRequest struct {
|
||||
OrderId string `json:"order_id" validate:"required|maxLen:32"`
|
||||
Amount float64 `json:"amount" validate:"required|isFloat|gt:0.01"`
|
||||
NotifyUrl string `json:"notify_url" validate:"required"`
|
||||
Signature string `json:"signature" validate:"required"`
|
||||
RedirectUrl string `json:"redirect_url"`
|
||||
}
|
||||
|
||||
func (r CreateTransactionRequest) Translates() map[string]string {
|
||||
return validate.MS{
|
||||
"OrderId": "订单号",
|
||||
"Amount": "支付金额",
|
||||
"NotifyUrl": "异步回调网址",
|
||||
"Signature": "签名",
|
||||
}
|
||||
}
|
||||
|
||||
// OrderProcessingRequest 订单处理
|
||||
type OrderProcessingRequest struct {
|
||||
Token string
|
||||
Amount string
|
||||
TradeId string
|
||||
BlockTransactionId string
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package response
|
||||
|
||||
// CreateTransactionResponse 创建订单成功返回
|
||||
type CreateTransactionResponse struct {
|
||||
TradeId string `json:"trade_id"` // epusdt订单号
|
||||
OrderId string `json:"order_id"` // 客户交易id
|
||||
Amount float64 `json:"amount"` // 订单金额,保留4位小数
|
||||
ActualAmount float64 `json:"actual_amount"` // 订单实际需要支付的金额,保留4位小数
|
||||
Token string `json:"token"` // 收款钱包地址
|
||||
ExpirationTime int64 `json:"expiration_time"` // 过期时间 时间戳
|
||||
PaymentUrl string `json:"payment_url"` // 收银台地址
|
||||
}
|
||||
|
||||
// OrderNotifyResponse 订单异步回调结构体
|
||||
type OrderNotifyResponse struct {
|
||||
TradeId string `json:"trade_id"` // epusdt订单号
|
||||
OrderId string `json:"order_id"` // 客户交易id
|
||||
Amount float64 `json:"amount"` // 订单金额,保留4位小数
|
||||
ActualAmount float64 `json:"actual_amount"` // 订单实际需要支付的金额,保留4位小数
|
||||
Token string `json:"token"` // 收款钱包地址
|
||||
BlockTransactionId string `json:"block_transaction_id"` // 区块id
|
||||
Signature string `json:"signature"` // 签名
|
||||
Status int `json:"status"` // 1:等待支付,2:支付成功,3:已过期
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package response
|
||||
|
||||
type CheckoutCounterResponse struct {
|
||||
TradeId string `json:"trade_id"` // epusdt订单号
|
||||
ActualAmount float64 `json:"actual_amount"` // 订单实际需要支付的金额,保留4位小数
|
||||
Token string `json:"token"` // 收款钱包地址
|
||||
ExpirationTime int64 `json:"expiration_time"` // 过期时间 时间戳
|
||||
RedirectUrl string `json:"redirect_url"`
|
||||
}
|
||||
|
||||
type CheckStatusResponse struct {
|
||||
TradeId string `json:"trade_id"` // epusdt订单号
|
||||
Status int `json:"status"`
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/assimon/luuu/config"
|
||||
"github.com/assimon/luuu/model/dao"
|
||||
"github.com/assimon/luuu/model/data"
|
||||
"github.com/assimon/luuu/model/mdb"
|
||||
"github.com/assimon/luuu/model/request"
|
||||
"github.com/assimon/luuu/model/response"
|
||||
"github.com/assimon/luuu/mq"
|
||||
"github.com/assimon/luuu/mq/handle"
|
||||
"github.com/assimon/luuu/telegram"
|
||||
"github.com/assimon/luuu/util/constant"
|
||||
"github.com/golang-module/carbon/v2"
|
||||
"github.com/gookit/goutil/mathutil"
|
||||
"github.com/hibiken/asynq"
|
||||
"github.com/shopspring/decimal"
|
||||
"math/rand"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var gCreateTransactionLock sync.Mutex
|
||||
|
||||
// CreateTransaction 创建订单
|
||||
func CreateTransaction(req *request.CreateTransactionRequest) (*response.CreateTransactionResponse, error) {
|
||||
gCreateTransactionLock.Lock()
|
||||
defer gCreateTransactionLock.Unlock()
|
||||
// 汇率计算金额
|
||||
rmb := decimal.NewFromFloat(req.Amount)
|
||||
rate := decimal.NewFromFloat(config.GetUsdtRate())
|
||||
amount := rmb.Div(rate).InexactFloat64()
|
||||
actualAmountStr := fmt.Sprintf("%.4f", amount)
|
||||
actualAmountFloat, err := strconv.ParseFloat(actualAmountStr, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 是否可以满足最低支付金额
|
||||
if actualAmountFloat <= 0 {
|
||||
return nil, constant.PayAmountErr
|
||||
}
|
||||
// 已经存在了的交易
|
||||
exist, err := data.GetOrderInfoByOrderId(req.OrderId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if exist.ID > 0 {
|
||||
return nil, constant.OrderAlreadyExists
|
||||
}
|
||||
// 有无可用钱包
|
||||
walletAddress, err := data.GetAvailableWalletAddress()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(walletAddress) <= 0 {
|
||||
return nil, constant.NotAvailableWalletAddress
|
||||
}
|
||||
availableToken, availableAmountStr, err := CalculateAvailableWalletTokenAndAmount(actualAmountStr, walletAddress)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if availableToken == "" || availableAmountStr == "" {
|
||||
return nil, constant.NotAvailableAmountErr
|
||||
}
|
||||
availableAmountFloat, err := strconv.ParseFloat(availableAmountStr, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tx := dao.Mdb.Begin()
|
||||
order := &mdb.Orders{
|
||||
TradeId: GenerateCode(),
|
||||
OrderId: req.OrderId,
|
||||
Amount: req.Amount,
|
||||
ActualAmount: availableAmountFloat,
|
||||
Token: availableToken,
|
||||
Status: mdb.StatusWaitPay,
|
||||
NotifyUrl: req.NotifyUrl,
|
||||
RedirectUrl: req.RedirectUrl,
|
||||
}
|
||||
err = data.CreateOrderWithTransaction(tx, order)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return nil, err
|
||||
}
|
||||
ExpirationTime := carbon.Now().AddMinutes(config.GetOrderExpirationTime()).Timestamp()
|
||||
// 锁定支付池
|
||||
err = data.LockPayCache(availableToken, order.TradeId, availableAmountStr, ExpirationTime)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return nil, err
|
||||
}
|
||||
tx.Commit()
|
||||
// 超时过期消息队列
|
||||
orderExpirationQueue, _ := handle.NewOrderExpirationQueue(order.TradeId)
|
||||
mq.MClient.Enqueue(orderExpirationQueue, asynq.ProcessIn(time.Minute*time.Duration(config.GetOrderExpirationTime())))
|
||||
resp := &response.CreateTransactionResponse{
|
||||
TradeId: order.TradeId,
|
||||
OrderId: order.OrderId,
|
||||
Amount: order.Amount,
|
||||
ActualAmount: order.ActualAmount,
|
||||
Token: order.Token,
|
||||
ExpirationTime: ExpirationTime,
|
||||
PaymentUrl: fmt.Sprintf("%s/pay/checkout-counter/%s", config.GetAppUri(), order.TradeId),
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// OrderProcessing 成功处理订单
|
||||
func OrderProcessing(req *request.OrderProcessingRequest) error {
|
||||
tx := dao.Mdb.Begin()
|
||||
exist, err := data.GetOrderByBlockIdWithTransaction(tx, req.BlockTransactionId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if exist.ID > 0 {
|
||||
tx.Rollback()
|
||||
return constant.OrderBlockAlreadyProcess
|
||||
}
|
||||
// 标记订单成功
|
||||
err = data.OrderSuccessWithTransaction(tx, req)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
err = data.ClearPayCache(req.Token, req.Amount)
|
||||
tx.Commit()
|
||||
order, err := data.GetOrderInfoByTradeId(req.TradeId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// 回调队列
|
||||
orderCallbackQueue, _ := handle.NewOrderCallbackQueue(order)
|
||||
mq.MClient.Enqueue(orderCallbackQueue, asynq.MaxRetry(5))
|
||||
// 发送机器人消息
|
||||
msgTpl := `
|
||||
<b>📢📢有新的交易支付成功!</b>
|
||||
<pre>交易号:%s</pre>
|
||||
<pre>订单号:%s</pre>
|
||||
<pre>请求支付金额:%.4f cny</pre>
|
||||
<pre>实际支付金额:%.4f usdt</pre>
|
||||
<pre>钱包地址:%s</pre>
|
||||
<pre>订单创建时间:%s</pre>
|
||||
<pre>支付成功时间:%s</pre>
|
||||
`
|
||||
msg := fmt.Sprintf(msgTpl, order.TradeId, order.OrderId, order.Amount, order.ActualAmount, order.Token, order.CreatedAt.ToDateTimeString(), carbon.Now().ToDateTimeString())
|
||||
telegram.SendToBot(msg)
|
||||
return nil
|
||||
}
|
||||
|
||||
func CalculateAvailableWalletTokenAndAmount(amount string, walletAddress []mdb.WalletAddress) (string, string, error) {
|
||||
calculateAmountStr := amount
|
||||
availableAmountStr := ""
|
||||
availableToken := ""
|
||||
for i := 0; i < 100; i++ {
|
||||
token, err := CalculateAvailableWalletToken(calculateAmountStr, walletAddress)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
// 这个金额没有拿到可用的钱包,重试,金额+0.0001
|
||||
if token == "" {
|
||||
x, err := decimal.NewFromString(calculateAmountStr)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
y, err := decimal.NewFromString("0.0001")
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
calculateAmountStr = x.Add(y).String()
|
||||
continue
|
||||
}
|
||||
availableAmountStr = calculateAmountStr
|
||||
availableToken = token
|
||||
break
|
||||
}
|
||||
return availableToken, availableAmountStr, nil
|
||||
}
|
||||
|
||||
// CalculateAvailableWalletToken 计算可用钱包token
|
||||
func CalculateAvailableWalletToken(payAmount string, walletAddress []mdb.WalletAddress) (string, error) {
|
||||
nowTime := time.Now().Unix()
|
||||
ctx := context.Background()
|
||||
walletToken := ""
|
||||
for _, address := range walletAddress {
|
||||
result, err := data.GetExpirationTimeByAmount(ctx, address.Token, payAmount)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// 这个钱包金额被占用了
|
||||
if result != "" {
|
||||
endTime := mathutil.MustInt64(result)
|
||||
// 但是过期了
|
||||
if endTime < nowTime {
|
||||
// 删掉过期,还能继续用这个地址
|
||||
err = data.ClearPayCache(address.Token, payAmount)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
}
|
||||
walletToken = address.Token
|
||||
break
|
||||
}
|
||||
return walletToken, nil
|
||||
}
|
||||
|
||||
// GenerateCode 订单号生成
|
||||
func GenerateCode() string {
|
||||
date := time.Now().Format("20060102")
|
||||
r := rand.Intn(1000)
|
||||
code := fmt.Sprintf("%s%d%03d", date, time.Now().UnixNano()/1e6, r)
|
||||
return code
|
||||
}
|
||||
|
||||
// GetOrderInfoByTradeId 通过交易号获取订单
|
||||
func GetOrderInfoByTradeId(tradeId string) (*mdb.Orders, error) {
|
||||
order, err := data.GetOrderInfoByTradeId(tradeId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if order.ID <= 0 {
|
||||
return nil, constant.OrderNotExists
|
||||
}
|
||||
return order, nil
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/assimon/luuu/config"
|
||||
"github.com/assimon/luuu/model/data"
|
||||
"github.com/assimon/luuu/model/mdb"
|
||||
"github.com/assimon/luuu/model/response"
|
||||
)
|
||||
|
||||
// GetCheckoutCounterByTradeId 获取收银台详情,通过订单
|
||||
func GetCheckoutCounterByTradeId(tradeId string) (*response.CheckoutCounterResponse, error) {
|
||||
orderInfo, err := data.GetOrderInfoByTradeId(tradeId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if orderInfo.ID <= 0 || orderInfo.Status != mdb.StatusWaitPay {
|
||||
return nil, errors.New("不存在待支付订单或已过期!")
|
||||
}
|
||||
resp := &response.CheckoutCounterResponse{
|
||||
TradeId: orderInfo.TradeId,
|
||||
ActualAmount: orderInfo.ActualAmount,
|
||||
Token: orderInfo.Token,
|
||||
ExpirationTime: orderInfo.CreatedAt.AddMinutes(config.GetOrderExpirationTime()).TimestampWithMillisecond(),
|
||||
RedirectUrl: orderInfo.RedirectUrl,
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/assimon/luuu/model/data"
|
||||
"github.com/assimon/luuu/model/request"
|
||||
"github.com/assimon/luuu/util/http_client"
|
||||
"github.com/assimon/luuu/util/json"
|
||||
"github.com/assimon/luuu/util/log"
|
||||
"github.com/golang-module/carbon/v2"
|
||||
"github.com/gookit/goutil/mathutil"
|
||||
"github.com/gookit/goutil/stdutil"
|
||||
"github.com/shopspring/decimal"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const UsdtTrc20ApiUri = "https://apilist.tronscan.org/api/token_trc20/transfers"
|
||||
|
||||
type UsdtTrc20Resp struct {
|
||||
Total int `json:"total"`
|
||||
RangeTotal int `json:"rangeTotal"`
|
||||
TokenTransfers []TokenTransfers `json:"token_transfers"`
|
||||
}
|
||||
|
||||
type TokenInfo struct {
|
||||
TokenID string `json:"tokenId"`
|
||||
TokenAbbr string `json:"tokenAbbr"`
|
||||
TokenName string `json:"tokenName"`
|
||||
TokenDecimal int `json:"tokenDecimal"`
|
||||
TokenCanShow int `json:"tokenCanShow"`
|
||||
TokenType string `json:"tokenType"`
|
||||
TokenLogo string `json:"tokenLogo"`
|
||||
TokenLevel string `json:"tokenLevel"`
|
||||
Vip bool `json:"vip"`
|
||||
}
|
||||
type TokenTransfers struct {
|
||||
TransactionID string `json:"transaction_id"`
|
||||
BlockTs int64 `json:"block_ts"`
|
||||
FromAddress string `json:"from_address"`
|
||||
ToAddress string `json:"to_address"`
|
||||
Block int `json:"block"`
|
||||
ContractAddress string `json:"contract_address"`
|
||||
Quant string `json:"quant"`
|
||||
ApprovalAmount string `json:"approval_amount"`
|
||||
EventType string `json:"event_type"`
|
||||
ContractType string `json:"contract_type"`
|
||||
Confirmed bool `json:"confirmed"`
|
||||
ContractRet string `json:"contractRet"`
|
||||
FinalResult string `json:"finalResult"`
|
||||
TokenInfo TokenInfo `json:"tokenInfo"`
|
||||
FromAddressIsContract bool `json:"fromAddressIsContract"`
|
||||
ToAddressIsContract bool `json:"toAddressIsContract"`
|
||||
Revert bool `json:"revert"`
|
||||
}
|
||||
|
||||
// Trc20CallBack trc20回调
|
||||
func Trc20CallBack(token string, wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
log.Sugar.Error(err)
|
||||
}
|
||||
}()
|
||||
client := http_client.GetHttpClient()
|
||||
startTime := carbon.Now().AddHours(-24).TimestampWithMillisecond()
|
||||
endTime := carbon.Now().TimestampWithMillisecond()
|
||||
resp, err := client.R().SetQueryParams(map[string]string{
|
||||
"limit": "200",
|
||||
"start": "0",
|
||||
"direction": "in",
|
||||
"tokens": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
|
||||
"relatedAddress": token,
|
||||
"start_timestamp": stdutil.ToString(startTime),
|
||||
"end_timestamp": stdutil.ToString(endTime),
|
||||
}).Get(UsdtTrc20ApiUri)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if resp.StatusCode() != http.StatusOK {
|
||||
panic(err)
|
||||
}
|
||||
var trc20Resp UsdtTrc20Resp
|
||||
err = json.Cjson.Unmarshal(resp.Body(), &trc20Resp)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if trc20Resp.Total <= 0 {
|
||||
return
|
||||
}
|
||||
ctx := context.Background()
|
||||
nowTime := time.Now().Unix()
|
||||
for _, transfer := range trc20Resp.TokenTransfers {
|
||||
if transfer.ToAddress != token || transfer.FinalResult != "SUCCESS" {
|
||||
continue
|
||||
}
|
||||
x, _ := decimal.NewFromString(transfer.Quant)
|
||||
y, _ := decimal.NewFromString("1000000")
|
||||
quant := x.Div(y).InexactFloat64()
|
||||
amount := fmt.Sprintf("%.4f", quant)
|
||||
result, err := data.GetExpirationTimeByAmount(ctx, token, amount)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if result != "" {
|
||||
expTime := mathutil.MustInt64(result)
|
||||
// 但是过期了
|
||||
if expTime < nowTime {
|
||||
// 删掉过期
|
||||
_ = data.ClearPayCache(token, amount)
|
||||
continue
|
||||
}
|
||||
}
|
||||
// 该钱包下有无匹配金额订单
|
||||
tradeId, err := data.GetTradeIdByAmount(ctx, token, amount)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if tradeId == "" {
|
||||
continue
|
||||
}
|
||||
// 到这一步就匹配到金额了
|
||||
req := &request.OrderProcessingRequest{
|
||||
Token: token,
|
||||
TradeId: tradeId,
|
||||
Amount: amount,
|
||||
BlockTransactionId: transfer.TransactionID,
|
||||
}
|
||||
err = OrderProcessing(req)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user