478 lines
20 KiB
Go
478 lines
20 KiB
Go
package store
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"errors"
|
|
|
|
"github.com/jmoiron/sqlx"
|
|
)
|
|
|
|
type PaymentAccount struct {
|
|
ID int64 `db:"id"`
|
|
AccountType int `db:"account_type"`
|
|
BankCode string `db:"bank_code"`
|
|
BankName string `db:"bank_name"`
|
|
HolderCipher string `db:"holder_cipher"`
|
|
HolderMask string `db:"holder_mask"`
|
|
MobileCipher string `db:"mobile_cipher"`
|
|
MobileMask string `db:"mobile_mask"`
|
|
AccountNoCipher string `db:"account_no_cipher"`
|
|
AccountNoHash string `db:"account_no_hash"`
|
|
AccountNoMask string `db:"account_no_mask"`
|
|
QRAttachmentID int64 `db:"qr_attachment_id"`
|
|
VerifyStatus int `db:"verify_status"`
|
|
IsDefault int `db:"is_default"`
|
|
CreatedAt string `db:"created_at"`
|
|
UpdatedAt string `db:"updated_at"`
|
|
}
|
|
|
|
type PaymentAccountCreation struct {
|
|
TenantID int64
|
|
MemberID int64
|
|
ExpectedTradePasswordHash string
|
|
AccountType int
|
|
BankCode string
|
|
BankName string
|
|
HolderCipher string
|
|
HolderMask string
|
|
MobileCipher string
|
|
MobileMask string
|
|
AccountNoCipher string
|
|
AccountNoHash string
|
|
AccountNoMask string
|
|
QRAttachmentID int64
|
|
}
|
|
|
|
type WithdrawalChannel struct {
|
|
ID int64 `db:"id"`
|
|
Code string `db:"channel_code"`
|
|
Name string `db:"name"`
|
|
ChannelType int `db:"channel_type"`
|
|
MinAmount int64 `db:"min_amount"`
|
|
MaxAmount int64 `db:"max_amount"`
|
|
FeeType int `db:"fee_type"`
|
|
FeeFixedAmount int64 `db:"fee_fixed_amount"`
|
|
FeeRate int `db:"fee_rate"`
|
|
}
|
|
|
|
type WithdrawalWallet struct {
|
|
AccountID int64 `db:"account_id"`
|
|
AvailableAmount int64 `db:"available_amount"`
|
|
FrozenAmount int64 `db:"frozen_amount"`
|
|
AccountStatus int `db:"account_status"`
|
|
DailyLimit int `db:"daily_limit"`
|
|
TodayCount int `db:"today_count"`
|
|
}
|
|
|
|
type WithdrawalCreation struct {
|
|
TenantID int64
|
|
MemberID int64
|
|
ExpectedTradePasswordHash string
|
|
WithdrawalNo string
|
|
PaymentAccountID int64
|
|
ExpectedAccountType int
|
|
ExpectedBankCode string
|
|
ExpectedBankName string
|
|
ExpectedHolderCipher string
|
|
ExpectedMobileCipher string
|
|
ExpectedAccountNoCipher string
|
|
ExpectedAccountNoHash string
|
|
ExpectedQRAttachmentID int64
|
|
Channel WithdrawalChannel
|
|
RequestAmount int64
|
|
FeeAmount int64
|
|
ActualAmount int64
|
|
Currency string
|
|
AccountSnapshotCipher string
|
|
AccountMask string
|
|
BankName string
|
|
HolderMask string
|
|
AvailableLedgerNo string
|
|
FrozenLedgerNo string
|
|
}
|
|
|
|
type WithdrawalOrder struct {
|
|
ID int64 `db:"id"`
|
|
WithdrawalNo string `db:"withdrawal_no"`
|
|
PaymentAccountID int64 `db:"payment_account_id"`
|
|
ChannelID int64 `db:"channel_id"`
|
|
ChannelName string `db:"channel_name"`
|
|
RequestAmount int64 `db:"request_amount"`
|
|
FeeAmount int64 `db:"fee_amount"`
|
|
ActualAmount int64 `db:"actual_amount"`
|
|
Currency string `db:"currency"`
|
|
AccountMask string `db:"account_mask"`
|
|
BankName string `db:"bank_name"`
|
|
HolderMask string `db:"holder_mask"`
|
|
RiskResult json.RawMessage `db:"risk_result"`
|
|
Status int `db:"withdrawal_status"`
|
|
ReviewReasonCode string `db:"review_reason_code"`
|
|
ReviewReason string `db:"review_reason"`
|
|
ExternalNo string `db:"external_no"`
|
|
ReviewedAt string `db:"reviewed_at"`
|
|
ProcessedAt string `db:"processed_at"`
|
|
CompletedAt string `db:"completed_at"`
|
|
CreatedAt string `db:"created_at"`
|
|
UpdatedAt string `db:"updated_at"`
|
|
Replayed bool `db:"-"`
|
|
}
|
|
|
|
type WithdrawalPage struct {
|
|
Items []WithdrawalOrder
|
|
Total int64
|
|
}
|
|
|
|
func (s *Store) PaymentAccounts(ctx context.Context, tenantID, memberID int64) ([]PaymentAccount, error) {
|
|
items := make([]PaymentAccount, 0)
|
|
err := s.db.SelectContext(ctx, &items, `SELECT id,account_type,bank_code,bank_name,holder_mask,mobile_mask,
|
|
account_no_mask,qr_attachment_id,verify_status,is_default,
|
|
DATE_FORMAT(created_at,'%Y-%m-%dT%H:%i:%sZ') AS created_at,
|
|
DATE_FORMAT(updated_at,'%Y-%m-%dT%H:%i:%sZ') AS updated_at
|
|
FROM hg_app_member_payment_account WHERE tenant_id=? AND member_id=? AND deleted_at IS NULL
|
|
ORDER BY is_default ASC,id DESC`, tenantID, memberID)
|
|
return items, err
|
|
}
|
|
|
|
func (s *Store) CreatePaymentAccount(ctx context.Context, input PaymentAccountCreation) (*PaymentAccount, error) {
|
|
tx, err := s.db.BeginTxx(ctx, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer tx.Rollback()
|
|
var memberID int64
|
|
err = tx.GetContext(ctx, &memberID, `SELECT id FROM hg_app_member WHERE tenant_id=? AND id=?
|
|
AND trade_password_hash=? AND member_status=1 AND deleted_at IS NULL LIMIT 1 FOR UPDATE`,
|
|
input.TenantID, input.MemberID, input.ExpectedTradePasswordHash)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return nil, ErrConcurrentUpdate
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if input.QRAttachmentID > 0 {
|
|
var attachmentCount int
|
|
err = tx.GetContext(ctx, &attachmentCount, `SELECT COUNT(*) FROM hg_sys_attachment
|
|
WHERE tenant_id=? AND id=? AND app_id='workbench' AND member_id=? AND status=1 AND mime_type LIKE 'image/%'`,
|
|
input.TenantID, input.QRAttachmentID, input.MemberID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if attachmentCount != 1 {
|
|
return nil, ErrAttachmentInvalid
|
|
}
|
|
}
|
|
var accountCount int
|
|
if err = tx.GetContext(ctx, &accountCount, `SELECT COUNT(*) FROM hg_app_member_payment_account
|
|
WHERE tenant_id=? AND member_id=? AND deleted_at IS NULL`, input.TenantID, input.MemberID); err != nil {
|
|
return nil, err
|
|
}
|
|
isDefault := 2
|
|
if accountCount == 0 {
|
|
isDefault = 1
|
|
}
|
|
result, err := tx.ExecContext(ctx, `INSERT INTO hg_app_member_payment_account
|
|
(tenant_id,member_id,account_type,bank_code,bank_name,holder_cipher,holder_mask,mobile_cipher,
|
|
mobile_mask,account_no_cipher,account_no_hash,account_no_mask,qr_attachment_id,verify_status,
|
|
is_default,created_by,updated_by)
|
|
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,1,?,?,?)`, input.TenantID, input.MemberID, input.AccountType,
|
|
input.BankCode, input.BankName, input.HolderCipher, input.HolderMask, nullableString(input.MobileCipher),
|
|
input.MobileMask, input.AccountNoCipher, input.AccountNoHash, input.AccountNoMask, input.QRAttachmentID,
|
|
isDefault, input.MemberID, input.MemberID)
|
|
if err != nil {
|
|
if isDuplicate(err) {
|
|
return nil, ErrPaymentAccountExists
|
|
}
|
|
return nil, err
|
|
}
|
|
id, err := result.LastInsertId()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err = tx.Commit(); err != nil {
|
|
return nil, err
|
|
}
|
|
return &PaymentAccount{ID: id, AccountType: input.AccountType, BankCode: input.BankCode,
|
|
BankName: input.BankName, HolderMask: input.HolderMask, MobileMask: input.MobileMask,
|
|
AccountNoMask: input.AccountNoMask, QRAttachmentID: input.QRAttachmentID,
|
|
VerifyStatus: 1, IsDefault: isDefault}, nil
|
|
}
|
|
|
|
func nullableString(value string) any {
|
|
if value == "" {
|
|
return nil
|
|
}
|
|
return value
|
|
}
|
|
|
|
func (s *Store) PaymentAccountForWithdrawal(ctx context.Context, tenantID, memberID, accountID int64) (*PaymentAccount, error) {
|
|
var row PaymentAccount
|
|
err := s.db.GetContext(ctx, &row, `SELECT id,account_type,bank_code,bank_name,holder_cipher,holder_mask,
|
|
COALESCE(mobile_cipher,'') AS mobile_cipher,mobile_mask,account_no_cipher,account_no_hash,
|
|
account_no_mask,qr_attachment_id,verify_status,is_default,
|
|
DATE_FORMAT(created_at,'%Y-%m-%dT%H:%i:%sZ') AS created_at,
|
|
DATE_FORMAT(updated_at,'%Y-%m-%dT%H:%i:%sZ') AS updated_at
|
|
FROM hg_app_member_payment_account WHERE tenant_id=? AND member_id=? AND id=? AND deleted_at IS NULL LIMIT 1`,
|
|
tenantID, memberID, accountID)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return nil, ErrPaymentAccountInvalid
|
|
}
|
|
return &row, err
|
|
}
|
|
|
|
func (s *Store) ActiveWithdrawalChannel(ctx context.Context, tenantID int64, channelTypes []int) (*WithdrawalChannel, error) {
|
|
if len(channelTypes) == 0 {
|
|
return nil, ErrWithdrawalChannel
|
|
}
|
|
query, args, err := sqlx.In(`SELECT id,channel_code,name,channel_type,min_amount,max_amount,
|
|
fee_type,fee_fixed_amount,fee_rate FROM hg_payment_channel
|
|
WHERE tenant_id=? AND channel_type IN (?) AND direction IN (2,3) AND status=1 AND deleted_at IS NULL
|
|
ORDER BY CASE WHEN channel_type=? THEN 0 ELSE 1 END,sort,id LIMIT 1`, tenantID, channelTypes, channelTypes[0])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var row WithdrawalChannel
|
|
err = s.db.GetContext(ctx, &row, query, args...)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return nil, ErrWithdrawalChannel
|
|
}
|
|
return &row, err
|
|
}
|
|
|
|
func (s *Store) WithdrawalWallet(ctx context.Context, tenantID, memberID int64) (*WithdrawalWallet, error) {
|
|
var row WithdrawalWallet
|
|
err := s.db.GetContext(ctx, &row, `SELECT w.id AS account_id,w.available_amount,w.frozen_amount,w.account_status,
|
|
COALESCE(lv.daily_withdraw_limit,0) AS daily_limit,
|
|
(SELECT COUNT(*) FROM hg_withdrawal_order o WHERE o.tenant_id=w.tenant_id AND o.member_id=w.member_id
|
|
AND o.created_at>=UTC_DATE() AND o.created_at<UTC_DATE()+INTERVAL 1 DAY) AS today_count
|
|
FROM hg_wallet_account w JOIN hg_app_member m ON m.tenant_id=w.tenant_id AND m.id=w.member_id
|
|
LEFT JOIN hg_app_member_level l ON l.tenant_id=m.tenant_id AND l.id=m.level_id AND l.status=1 AND l.deleted_at IS NULL
|
|
LEFT JOIN hg_app_member_level_version lv ON lv.tenant_id=l.tenant_id AND lv.id=l.current_version_id AND lv.publish_status=3
|
|
WHERE w.tenant_id=? AND w.member_id=? AND w.currency='CNY' AND m.member_status=1 AND m.deleted_at IS NULL LIMIT 1`,
|
|
tenantID, memberID)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return nil, ErrAccountRestricted
|
|
}
|
|
return &row, err
|
|
}
|
|
|
|
func (s *Store) CreateWithdrawal(ctx context.Context, input WithdrawalCreation) (*WithdrawalOrder, error) {
|
|
tx, err := s.db.BeginTxx(ctx, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer tx.Rollback()
|
|
|
|
var memberID int64
|
|
err = tx.GetContext(ctx, &memberID, `SELECT id FROM hg_app_member WHERE tenant_id=? AND id=?
|
|
AND trade_password_hash=? AND member_status=1 AND deleted_at IS NULL LIMIT 1 FOR UPDATE`,
|
|
input.TenantID, input.MemberID, input.ExpectedTradePasswordHash)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return nil, ErrConcurrentUpdate
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if existing, findErr := withdrawalByNo(ctx, tx, input.TenantID, input.MemberID, input.WithdrawalNo); findErr == nil {
|
|
if existing.PaymentAccountID != input.PaymentAccountID || existing.RequestAmount != input.RequestAmount || existing.Currency != input.Currency {
|
|
return nil, ErrIdempotencyConflict
|
|
}
|
|
existing.Replayed = true
|
|
if err := tx.Commit(); err != nil {
|
|
return nil, err
|
|
}
|
|
return existing, nil
|
|
} else if !errors.Is(findErr, ErrWithdrawalNotFound) {
|
|
return nil, findErr
|
|
}
|
|
|
|
var account PaymentAccount
|
|
err = tx.GetContext(ctx, &account, `SELECT id,account_type,bank_code,bank_name,holder_cipher,holder_mask,
|
|
COALESCE(mobile_cipher,'') AS mobile_cipher,mobile_mask,account_no_cipher,account_no_hash,
|
|
account_no_mask,qr_attachment_id,verify_status,is_default,'' AS created_at,'' AS updated_at
|
|
FROM hg_app_member_payment_account WHERE tenant_id=? AND member_id=? AND id=?
|
|
AND verify_status=2 AND deleted_at IS NULL LIMIT 1 FOR UPDATE`, input.TenantID, input.MemberID, input.PaymentAccountID)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return nil, ErrPaymentAccountInvalid
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if account.AccountType != input.ExpectedAccountType || account.BankCode != input.ExpectedBankCode ||
|
|
account.BankName != input.ExpectedBankName || account.HolderCipher != input.ExpectedHolderCipher ||
|
|
account.MobileCipher != input.ExpectedMobileCipher || account.AccountNoCipher != input.ExpectedAccountNoCipher ||
|
|
account.AccountNoHash != input.ExpectedAccountNoHash || account.QRAttachmentID != input.ExpectedQRAttachmentID {
|
|
return nil, ErrConcurrentUpdate
|
|
}
|
|
var channel WithdrawalChannel
|
|
err = tx.GetContext(ctx, &channel, `SELECT id,channel_code,name,channel_type,min_amount,max_amount,
|
|
fee_type,fee_fixed_amount,fee_rate FROM hg_payment_channel WHERE tenant_id=? AND id=?
|
|
AND direction IN (2,3) AND status=1 AND deleted_at IS NULL LIMIT 1 FOR UPDATE`, input.TenantID, input.Channel.ID)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return nil, ErrWithdrawalChannel
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if channel != input.Channel {
|
|
return nil, ErrConcurrentUpdate
|
|
}
|
|
if input.RequestAmount < maxInt64(1, channel.MinAmount) || (channel.MaxAmount > 0 && input.RequestAmount > channel.MaxAmount) {
|
|
return nil, ErrWithdrawalAmount
|
|
}
|
|
|
|
var wallet WithdrawalWallet
|
|
err = tx.GetContext(ctx, &wallet, `SELECT w.id AS account_id,w.available_amount,w.frozen_amount,w.account_status,
|
|
COALESCE(lv.daily_withdraw_limit,0) AS daily_limit,
|
|
(SELECT COUNT(*) FROM hg_withdrawal_order o WHERE o.tenant_id=w.tenant_id AND o.member_id=w.member_id
|
|
AND o.created_at>=UTC_DATE() AND o.created_at<UTC_DATE()+INTERVAL 1 DAY) AS today_count
|
|
FROM hg_wallet_account w JOIN hg_app_member m ON m.tenant_id=w.tenant_id AND m.id=w.member_id
|
|
LEFT JOIN hg_app_member_level l ON l.tenant_id=m.tenant_id AND l.id=m.level_id AND l.status=1 AND l.deleted_at IS NULL
|
|
LEFT JOIN hg_app_member_level_version lv ON lv.tenant_id=l.tenant_id AND lv.id=l.current_version_id AND lv.publish_status=3
|
|
WHERE w.tenant_id=? AND w.member_id=? AND w.currency=? LIMIT 1 FOR UPDATE`, input.TenantID, input.MemberID, input.Currency)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return nil, ErrAccountRestricted
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if wallet.AccountStatus != 1 {
|
|
return nil, ErrAccountRestricted
|
|
}
|
|
if wallet.DailyLimit > 0 && wallet.TodayCount >= wallet.DailyLimit {
|
|
return nil, ErrWithdrawalLimit
|
|
}
|
|
if wallet.AvailableAmount < input.RequestAmount {
|
|
return nil, ErrInsufficientBalance
|
|
}
|
|
|
|
riskResult := json.RawMessage(`{"result":"passed","mode":"sandbox"}`)
|
|
result, err := tx.ExecContext(ctx, `INSERT INTO hg_withdrawal_order
|
|
(tenant_id,withdrawal_no,member_id,payment_account_id,channel_id,request_amount,fee_amount,
|
|
actual_amount,currency,account_snapshot_cipher,account_mask,bank_name,holder_mask,risk_result,
|
|
withdrawal_status,created_by,updated_by)
|
|
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,1,?,?)`, input.TenantID, input.WithdrawalNo, input.MemberID,
|
|
input.PaymentAccountID, channel.ID, input.RequestAmount, input.FeeAmount, input.ActualAmount,
|
|
input.Currency, input.AccountSnapshotCipher, input.AccountMask, input.BankName, input.HolderMask,
|
|
riskResult, input.MemberID, input.MemberID)
|
|
if err != nil {
|
|
if isDuplicate(err) {
|
|
return nil, ErrIdempotencyConflict
|
|
}
|
|
return nil, err
|
|
}
|
|
orderID, err := result.LastInsertId()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
beforeAvailable := wallet.AvailableAmount
|
|
afterAvailable := beforeAvailable - input.RequestAmount
|
|
beforeFrozen := wallet.FrozenAmount
|
|
afterFrozen := beforeFrozen + input.RequestAmount
|
|
update, err := tx.ExecContext(ctx, `UPDATE hg_wallet_account SET available_amount=?,frozen_amount=?,version=version+1,
|
|
updated_at=UTC_TIMESTAMP(3) WHERE id=? AND tenant_id=? AND available_amount=? AND frozen_amount=? AND account_status=1`,
|
|
afterAvailable, afterFrozen, wallet.AccountID, input.TenantID, beforeAvailable, beforeFrozen)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if affected, _ := update.RowsAffected(); affected != 1 {
|
|
return nil, ErrConcurrentUpdate
|
|
}
|
|
availableLedgerID, err := insertWithdrawalLedger(ctx, tx, input, wallet.AccountID, input.AvailableLedgerNo,
|
|
"available", 2, beforeAvailable, afterAvailable, "available", "取出申请冻结可用余额")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
_ = availableLedgerID
|
|
frozenLedgerID, err := insertWithdrawalLedger(ctx, tx, input, wallet.AccountID, input.FrozenLedgerNo,
|
|
"frozen", 1, beforeFrozen, afterFrozen, "frozen", "取出申请增加冻结余额")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if _, err = tx.ExecContext(ctx, `UPDATE hg_withdrawal_order SET freeze_ledger_id=? WHERE tenant_id=? AND id=?`,
|
|
frozenLedgerID, input.TenantID, orderID); err != nil {
|
|
return nil, err
|
|
}
|
|
if err = tx.Commit(); err != nil {
|
|
return nil, err
|
|
}
|
|
return &WithdrawalOrder{ID: orderID, WithdrawalNo: input.WithdrawalNo,
|
|
PaymentAccountID: input.PaymentAccountID, ChannelID: channel.ID, ChannelName: channel.Name,
|
|
RequestAmount: input.RequestAmount, FeeAmount: input.FeeAmount, ActualAmount: input.ActualAmount,
|
|
Currency: input.Currency, AccountMask: input.AccountMask, BankName: input.BankName,
|
|
HolderMask: input.HolderMask, RiskResult: riskResult, Status: 1}, nil
|
|
}
|
|
|
|
func insertWithdrawalLedger(ctx context.Context, tx *sqlx.Tx, input WithdrawalCreation, accountID int64,
|
|
ledgerNo, entryType string, direction int, beforeAmount, afterAmount int64, suffix, remark string) (int64, error) {
|
|
result, err := tx.ExecContext(ctx, `INSERT INTO hg_wallet_ledger
|
|
(tenant_id,ledger_no,account_id,member_id,biz_type,biz_no,entry_type,direction,amount,
|
|
before_amount,after_amount,currency,reversal_of,idempotency_key,remark,operator_id,occurred_at)
|
|
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,0,?,?,?,UTC_TIMESTAMP(3))`, input.TenantID, ledgerNo, accountID,
|
|
input.MemberID, "withdraw_freeze", input.WithdrawalNo, entryType, direction, input.RequestAmount,
|
|
beforeAmount, afterAmount, input.Currency, "withdrawal:"+input.WithdrawalNo+":"+suffix, remark, input.MemberID)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return result.LastInsertId()
|
|
}
|
|
|
|
func (s *Store) WithdrawalOrders(ctx context.Context, tenantID, memberID int64, status, offset, limit int) (*WithdrawalPage, error) {
|
|
where := ` WHERE o.tenant_id=? AND o.member_id=?`
|
|
args := []any{tenantID, memberID}
|
|
if status > 0 {
|
|
where += ` AND o.withdrawal_status=?`
|
|
args = append(args, status)
|
|
}
|
|
var total int64
|
|
if err := s.db.GetContext(ctx, &total, `SELECT COUNT(*) FROM hg_withdrawal_order o`+where, args...); err != nil {
|
|
return nil, err
|
|
}
|
|
args = append(args, limit, offset)
|
|
items := make([]WithdrawalOrder, 0)
|
|
err := s.db.SelectContext(ctx, &items, withdrawalSelectSQL()+where+
|
|
` ORDER BY o.created_at DESC,o.id DESC LIMIT ? OFFSET ?`, args...)
|
|
return &WithdrawalPage{Items: items, Total: total}, err
|
|
}
|
|
|
|
func (s *Store) WithdrawalOrder(ctx context.Context, tenantID, memberID, orderID int64) (*WithdrawalOrder, error) {
|
|
var row WithdrawalOrder
|
|
err := s.db.GetContext(ctx, &row, withdrawalSelectSQL()+
|
|
` WHERE o.tenant_id=? AND o.member_id=? AND o.id=? LIMIT 1`, tenantID, memberID, orderID)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return nil, ErrWithdrawalNotFound
|
|
}
|
|
return &row, err
|
|
}
|
|
|
|
func withdrawalByNo(ctx context.Context, db queryer, tenantID, memberID int64, no string) (*WithdrawalOrder, error) {
|
|
query := withdrawalSelectSQL() + ` WHERE o.tenant_id=? AND o.member_id=? AND o.withdrawal_no=? LIMIT 1`
|
|
var row WithdrawalOrder
|
|
err := db.GetContext(ctx, &row, query, tenantID, memberID, no)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return nil, ErrWithdrawalNotFound
|
|
}
|
|
return &row, err
|
|
}
|
|
|
|
func withdrawalSelectSQL() string {
|
|
return `SELECT o.id,o.withdrawal_no,o.payment_account_id,o.channel_id,COALESCE(c.name,'') AS channel_name,
|
|
o.request_amount,o.fee_amount,o.actual_amount,o.currency,o.account_mask,o.bank_name,o.holder_mask,
|
|
COALESCE(o.risk_result,JSON_OBJECT()) AS risk_result,o.withdrawal_status,o.review_reason_code,
|
|
o.review_reason,o.external_no,
|
|
COALESCE(DATE_FORMAT(o.reviewed_at,'%Y-%m-%dT%H:%i:%sZ'),'') AS reviewed_at,
|
|
COALESCE(DATE_FORMAT(o.processed_at,'%Y-%m-%dT%H:%i:%sZ'),'') AS processed_at,
|
|
COALESCE(DATE_FORMAT(o.completed_at,'%Y-%m-%dT%H:%i:%sZ'),'') AS completed_at,
|
|
DATE_FORMAT(o.created_at,'%Y-%m-%dT%H:%i:%sZ') AS created_at,
|
|
DATE_FORMAT(o.updated_at,'%Y-%m-%dT%H:%i:%sZ') AS updated_at
|
|
FROM hg_withdrawal_order o LEFT JOIN hg_payment_channel c ON c.tenant_id=o.tenant_id AND c.id=o.channel_id`
|
|
}
|
|
|
|
func maxInt64(left, right int64) int64 {
|
|
if left > right {
|
|
return left
|
|
}
|
|
return right
|
|
}
|