增加提现相关的与打款相关逻辑
This commit is contained in:
BIN
Binary file not shown.
@@ -9,7 +9,9 @@ type UserRealname struct {
|
||||
ProviderOrderNo string `gorm:"size:128;not null;default:''" json:"provider_order_no"`
|
||||
Status string `gorm:"size:32;not null;default:'pending'" json:"status"`
|
||||
MaskedName string `gorm:"size:64;not null;default:''" json:"masked_name"`
|
||||
EncryptedName string `gorm:"size:255;not null;default:''" json:"encrypted_name"`
|
||||
MaskedIDNo string `gorm:"size:64;not null;default:''" json:"masked_id_no"`
|
||||
EncryptedIDNo string `gorm:"size:255;not null;default:''" json:"encrypted_id_no"`
|
||||
VerifiedAt *time.Time `json:"verified_at"`
|
||||
FailReason string `gorm:"size:255;not null;default:''" json:"fail_reason"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
|
||||
@@ -65,7 +65,7 @@ func (h *Handler) writeObject(c *gin.Context, publicOnly bool) {
|
||||
response.BadRequest(c, "文件 key 不正确")
|
||||
return
|
||||
}
|
||||
if publicOnly && !strings.HasPrefix(key, "home-banner/") && !strings.HasPrefix(key, "avatar/") {
|
||||
if publicOnly && !strings.HasPrefix(key, "home-banner/") && !strings.HasPrefix(key, "avatar/") && !strings.HasPrefix(key, "payment-cert/") {
|
||||
response.Error(c, http.StatusNotFound, "not_found", "文件不存在或暂不可访问")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -105,7 +105,7 @@ func normalizeContentType(contentType string, data []byte) string {
|
||||
|
||||
func fileURLForScene(scene string, key string) string {
|
||||
fileURL := "/api/files/object?key=" + url.QueryEscape(key)
|
||||
if scene == "home-banner" || scene == "avatar" {
|
||||
if scene == "home-banner" || scene == "avatar" || scene == "payment-cert" {
|
||||
fileURL = "/api/public/files/object?key=" + url.QueryEscape(key)
|
||||
}
|
||||
return fileURL
|
||||
@@ -114,7 +114,7 @@ func fileURLForScene(scene string, key string) string {
|
||||
func normalizeScene(scene string) string {
|
||||
scene = strings.TrimSpace(strings.ToLower(scene))
|
||||
switch scene {
|
||||
case "listing", "handoff", "dispute", "realname", "avatar", "home-banner", "chat":
|
||||
case "listing", "handoff", "dispute", "realname", "avatar", "home-banner", "chat", "payment-cert":
|
||||
return scene
|
||||
default:
|
||||
return "misc"
|
||||
|
||||
@@ -1,23 +1,18 @@
|
||||
package paymentaccount
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"hfb_sys/backend/internal/model"
|
||||
"hfb_sys/backend/pkg/crypto"
|
||||
|
||||
"gorm.io/datatypes"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// 加密密钥(生产环境应从配置文件读取)
|
||||
const encryptionKey = "your-32-byte-secret-key-here!!" // 32字节
|
||||
// 删除加密密钥常量
|
||||
|
||||
type Repository struct {
|
||||
db *gorm.DB
|
||||
@@ -75,7 +70,7 @@ func (r *Repository) FindByID(userID, id uint64) (*PaymentAccountDTO, error) {
|
||||
|
||||
func (r *Repository) Create(userID uint64, req CreatePaymentAccountRequest) (*PaymentAccountDTO, error) {
|
||||
// 加密账号
|
||||
encryptedNo, err := encrypt(req.AccountNo)
|
||||
encryptedNo, err := crypto.Encrypt(req.AccountNo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -208,22 +203,54 @@ func (r *Repository) ValidateRealname(userID uint64, accountName string) error {
|
||||
|
||||
// 获取实名信息
|
||||
var realname model.UserRealname
|
||||
if err := r.db.Where("user_id = ? AND status = ?", userID, "success").
|
||||
if err := r.db.Where("user_id = ? AND status = ?", userID, "verified").
|
||||
First(&realname).Error; err != nil {
|
||||
return ErrRealnameRequired
|
||||
}
|
||||
|
||||
// 验证姓名匹配(去除空格后比较)
|
||||
if strings.ReplaceAll(realname.MaskedName, " ", "") != strings.ReplaceAll(accountName, " ", "") {
|
||||
return ErrAccountNameMismatch
|
||||
// 验证姓名匹配 - 使用加密字段进行精确匹配
|
||||
if realname.EncryptedName != "" {
|
||||
// 有加密字段,解密后精确匹配
|
||||
decryptedName, err := crypto.Decrypt(realname.EncryptedName)
|
||||
if err != nil {
|
||||
// 解密失败,降级到前缀匹配
|
||||
return r.validateByMaskedName(realname.MaskedName, accountName)
|
||||
}
|
||||
// 精确匹配(去除空格)
|
||||
if strings.ReplaceAll(decryptedName, " ", "") != strings.ReplaceAll(accountName, " ", "") {
|
||||
return ErrAccountNameMismatch
|
||||
}
|
||||
} else {
|
||||
// 没有加密字段(旧数据),使用前缀匹配
|
||||
return r.validateByMaskedName(realname.MaskedName, accountName)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 使用脱敏姓名进行前缀匹配(兼容旧数据)
|
||||
func (r *Repository) validateByMaskedName(maskedName, accountName string) error {
|
||||
maskedName = strings.ReplaceAll(maskedName, " ", "")
|
||||
inputName := strings.ReplaceAll(accountName, " ", "")
|
||||
|
||||
if strings.Contains(maskedName, "*") {
|
||||
// 提取非星号部分(通常是姓氏)
|
||||
prefix := strings.Split(maskedName, "*")[0]
|
||||
if prefix != "" && !strings.HasPrefix(inputName, prefix) {
|
||||
return ErrAccountNameMismatch
|
||||
}
|
||||
} else {
|
||||
// 完整匹配
|
||||
if maskedName != inputName {
|
||||
return ErrAccountNameMismatch
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Repository) toDTO(account model.UserPaymentAccount) (*PaymentAccountDTO, error) {
|
||||
// 解密账号并脱敏
|
||||
decrypted, err := decrypt(account.AccountNo)
|
||||
decrypted, err := crypto.Decrypt(account.AccountNo)
|
||||
if err != nil {
|
||||
decrypted = account.AccountNo // 降级处理
|
||||
}
|
||||
@@ -275,57 +302,11 @@ func maskAccountNo(accountNo, accountType string) string {
|
||||
return accountNo
|
||||
}
|
||||
|
||||
// AES加密
|
||||
func encrypt(plainText string) (string, error) {
|
||||
block, err := aes.NewCipher([]byte(encryptionKey))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
plainBytes := []byte(plainText)
|
||||
cipherBytes := make([]byte, aes.BlockSize+len(plainBytes))
|
||||
iv := cipherBytes[:aes.BlockSize]
|
||||
|
||||
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
stream := cipher.NewCFBEncrypter(block, iv)
|
||||
stream.XORKeyStream(cipherBytes[aes.BlockSize:], plainBytes)
|
||||
|
||||
return base64.StdEncoding.EncodeToString(cipherBytes), nil
|
||||
}
|
||||
|
||||
// AES解密
|
||||
func decrypt(cipherText string) (string, error) {
|
||||
block, err := aes.NewCipher([]byte(encryptionKey))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
cipherBytes, err := base64.StdEncoding.DecodeString(cipherText)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if len(cipherBytes) < aes.BlockSize {
|
||||
return "", errors.New("ciphertext too short")
|
||||
}
|
||||
|
||||
iv := cipherBytes[:aes.BlockSize]
|
||||
cipherBytes = cipherBytes[aes.BlockSize:]
|
||||
|
||||
stream := cipher.NewCFBDecrypter(block, iv)
|
||||
stream.XORKeyStream(cipherBytes, cipherBytes)
|
||||
|
||||
return string(cipherBytes), nil
|
||||
}
|
||||
|
||||
// 获取解密后的账号(仅供内部使用,如提现申请时)
|
||||
func (r *Repository) GetDecryptedAccountNo(userID, id uint64) (string, error) {
|
||||
var account model.UserPaymentAccount
|
||||
if err := r.db.Where("id = ? AND user_id = ?", id, userID).First(&account).Error; err != nil {
|
||||
return "", err
|
||||
}
|
||||
return decrypt(account.AccountNo)
|
||||
return crypto.Decrypt(account.AccountNo)
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ import (
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"hfb_sys/backend/pkg/crypto"
|
||||
)
|
||||
|
||||
const cloudMarketProviderName = "aliyun_cloudmarket"
|
||||
@@ -105,11 +107,23 @@ func (p *CloudMarketProvider) Start(ctx context.Context, req StartRequest) (Prov
|
||||
return ProviderResult{}, err
|
||||
}
|
||||
|
||||
// 加密完整信息
|
||||
encryptedName, err := crypto.Encrypt(req.Name)
|
||||
if err != nil {
|
||||
return ProviderResult{}, err
|
||||
}
|
||||
encryptedIDNo, err := crypto.Encrypt(req.IDNo)
|
||||
if err != nil {
|
||||
return ProviderResult{}, err
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
base := ProviderResult{
|
||||
ProviderOrderNo: payload.RequestID,
|
||||
MaskedName: maskName(req.Name),
|
||||
EncryptedName: encryptedName,
|
||||
MaskedIDNo: maskIDNo(req.IDNo),
|
||||
EncryptedIDNo: encryptedIDNo,
|
||||
}
|
||||
switch result {
|
||||
case 1:
|
||||
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"hfb_sys/backend/pkg/crypto"
|
||||
)
|
||||
|
||||
type Provider interface {
|
||||
@@ -24,7 +26,9 @@ type ProviderResult struct {
|
||||
ProviderOrderNo string
|
||||
Status string
|
||||
MaskedName string
|
||||
EncryptedName string // 加密的完整姓名
|
||||
MaskedIDNo string
|
||||
EncryptedIDNo string // 加密的完整身份证号
|
||||
VerifiedAt *time.Time
|
||||
FailReason string
|
||||
}
|
||||
@@ -47,12 +51,25 @@ func (p *MockProvider) Start(_ context.Context, req StartRequest) (ProviderResul
|
||||
if err != nil {
|
||||
return ProviderResult{}, err
|
||||
}
|
||||
|
||||
// 加密完整信息
|
||||
encryptedName, err := crypto.Encrypt(req.Name)
|
||||
if err != nil {
|
||||
return ProviderResult{}, err
|
||||
}
|
||||
encryptedIDNo, err := crypto.Encrypt(req.IDNo)
|
||||
if err != nil {
|
||||
return ProviderResult{}, err
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
return ProviderResult{
|
||||
ProviderOrderNo: orderNo,
|
||||
Status: StatusVerified,
|
||||
MaskedName: maskName(req.Name),
|
||||
EncryptedName: encryptedName,
|
||||
MaskedIDNo: maskIDNo(req.IDNo),
|
||||
EncryptedIDNo: encryptedIDNo,
|
||||
VerifiedAt: &now,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -32,7 +32,9 @@ func (r *Repository) SaveResult(userID uint64, provider string, result ProviderR
|
||||
ProviderOrderNo: result.ProviderOrderNo,
|
||||
Status: result.Status,
|
||||
MaskedName: result.MaskedName,
|
||||
EncryptedName: result.EncryptedName,
|
||||
MaskedIDNo: result.MaskedIDNo,
|
||||
EncryptedIDNo: result.EncryptedIDNo,
|
||||
VerifiedAt: result.VerifiedAt,
|
||||
FailReason: result.FailReason,
|
||||
}
|
||||
@@ -45,7 +47,9 @@ func (r *Repository) SaveResult(userID uint64, provider string, result ProviderR
|
||||
"provider_order_no": record.ProviderOrderNo,
|
||||
"status": record.Status,
|
||||
"masked_name": record.MaskedName,
|
||||
"encrypted_name": record.EncryptedName,
|
||||
"masked_id_no": record.MaskedIDNo,
|
||||
"encrypted_id_no": record.EncryptedIDNo,
|
||||
"verified_at": record.VerifiedAt,
|
||||
"fail_reason": record.FailReason,
|
||||
"updated_at": time.Now(),
|
||||
|
||||
@@ -40,6 +40,7 @@ type WithdrawalDetailDTO struct {
|
||||
AccountNo string `json:"account_no"` // 管理员可见完整账号
|
||||
BankName string `json:"bank_name"`
|
||||
BankBranch string `json:"bank_branch"`
|
||||
CertificateURLs []string `json:"certificate_urls"` // 收款二维码图片
|
||||
Status string `json:"status"`
|
||||
ReviewedBy *uint64 `json:"reviewed_by"`
|
||||
ReviewedByName string `json:"reviewed_by_name"`
|
||||
|
||||
@@ -3,12 +3,14 @@ package withdrawal
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"hfb_sys/backend/internal/model"
|
||||
"hfb_sys/backend/internal/modules/wallet"
|
||||
"hfb_sys/backend/pkg/crypto"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
@@ -414,15 +416,21 @@ func (r *Repository) toDetailDTO(w model.WithdrawalRequest) (*WithdrawalDetailDT
|
||||
}
|
||||
}
|
||||
|
||||
// 获取完整账号(管理员可见)
|
||||
// 获取完整账号和收款二维码(管理员可见)
|
||||
fullAccountNo := w.AccountNo
|
||||
var certificateURLs []string
|
||||
if w.PaymentAccountID != nil {
|
||||
var paymentAccount model.UserPaymentAccount
|
||||
if err := r.db.First(&paymentAccount, *w.PaymentAccountID).Error; err == nil {
|
||||
decrypted, err := r.decryptAccountNo(paymentAccount.AccountNo)
|
||||
// 解密账号
|
||||
decrypted, err := crypto.Decrypt(paymentAccount.AccountNo)
|
||||
if err == nil {
|
||||
fullAccountNo = decrypted
|
||||
}
|
||||
// 解析收款二维码
|
||||
if paymentAccount.CertificateURLs != nil {
|
||||
json.Unmarshal(paymentAccount.CertificateURLs, &certificateURLs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -441,6 +449,7 @@ func (r *Repository) toDetailDTO(w model.WithdrawalRequest) (*WithdrawalDetailDT
|
||||
AccountNo: fullAccountNo,
|
||||
BankName: w.BankName,
|
||||
BankBranch: w.BankBranch,
|
||||
CertificateURLs: certificateURLs,
|
||||
Status: w.Status,
|
||||
ReviewedBy: w.ReviewedBy,
|
||||
ReviewedByName: reviewedByName,
|
||||
|
||||
@@ -82,10 +82,11 @@ CREATE TABLE IF NOT EXISTS withdrawal_requests (
|
||||
|
||||
-- 添加提现相关权限(如果不存在)
|
||||
INSERT IGNORE INTO permissions (code, name, resource, action) VALUES
|
||||
('withdrawal:list', '查看提现申请', 'withdrawal', 'list'),
|
||||
('withdrawal:review', '审核提现申请', 'withdrawal', 'review'),
|
||||
('withdrawal:list', '查看提现列表', 'withdrawal', 'list'),
|
||||
('withdrawal:review', '审核提现', 'withdrawal', 'review'),
|
||||
('withdrawal:pay', '确认打款', 'withdrawal', 'pay'),
|
||||
('withdrawal:detail', '查看提现详情', 'withdrawal', 'detail');
|
||||
('withdrawal:detail', '查看提现详情', 'withdrawal', 'detail'),
|
||||
('withdrawal:approve', '提现审核', 'withdrawal', 'approve');
|
||||
|
||||
-- 钱包管理员流水权限(如果不存在)
|
||||
INSERT IGNORE INTO permissions (code, name, resource, action) VALUES
|
||||
@@ -101,6 +102,6 @@ SELECT
|
||||
(SELECT id FROM roles WHERE code = 'finance'),
|
||||
id
|
||||
FROM permissions
|
||||
WHERE code IN ('withdrawal:list', 'withdrawal:review', 'withdrawal:pay', 'withdrawal:detail', 'wallet:admin_ledger');
|
||||
WHERE code IN ('withdrawal:list', 'withdrawal:review', 'withdrawal:pay', 'withdrawal:detail', 'withdrawal:approve', 'wallet:admin_ledger');
|
||||
|
||||
SET FOREIGN_KEY_CHECKS = 1;
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
-- 添加加密字段到实名认证表
|
||||
ALTER TABLE user_realname
|
||||
ADD COLUMN encrypted_name VARCHAR(255) NOT NULL DEFAULT '' COMMENT '加密的完整姓名' AFTER masked_name,
|
||||
ADD COLUMN encrypted_id_no VARCHAR(255) NOT NULL DEFAULT '' COMMENT '加密的完整身份证号' AFTER masked_id_no;
|
||||
|
||||
-- 为已有记录添加索引(加密字段不需要索引)
|
||||
-- 但保留原有的 user_id 唯一索引
|
||||
@@ -0,0 +1,69 @@
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"io"
|
||||
)
|
||||
|
||||
// 加密密钥(生产环境应从配置文件读取)
|
||||
// 必须是 16、24 或 32 字节
|
||||
const encryptionKey = "hfb-sys-2024-secret-key-32bytes!" // 正好32字节
|
||||
|
||||
// Encrypt 使用 AES-GCM 加密文本
|
||||
func Encrypt(plainText string) (string, error) {
|
||||
block, err := aes.NewCipher([]byte(encryptionKey))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
aesGCM, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 创建随机 nonce
|
||||
nonce := make([]byte, aesGCM.NonceSize())
|
||||
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 加密并附加 nonce
|
||||
cipherBytes := aesGCM.Seal(nonce, nonce, []byte(plainText), nil)
|
||||
return base64.StdEncoding.EncodeToString(cipherBytes), nil
|
||||
}
|
||||
|
||||
// Decrypt 使用 AES-GCM 解密文本
|
||||
func Decrypt(cipherText string) (string, error) {
|
||||
block, err := aes.NewCipher([]byte(encryptionKey))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
aesGCM, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
cipherBytes, err := base64.StdEncoding.DecodeString(cipherText)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
nonceSize := aesGCM.NonceSize()
|
||||
if len(cipherBytes) < nonceSize {
|
||||
return "", errors.New("ciphertext too short")
|
||||
}
|
||||
|
||||
// 提取 nonce 和实际密文
|
||||
nonce, cipherBytes := cipherBytes[:nonceSize], cipherBytes[nonceSize:]
|
||||
plainBytes, err := aesGCM.Open(nil, nonce, cipherBytes, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(plainBytes), nil
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
-- 清空测试商品数据
|
||||
-- 注意:这会删除所有待审核和已审核的商品数据
|
||||
|
||||
-- 开始事务
|
||||
START TRANSACTION;
|
||||
|
||||
-- 显示清理前的统计
|
||||
SELECT
|
||||
status,
|
||||
COUNT(*) as count
|
||||
FROM game_listings
|
||||
GROUP BY status;
|
||||
|
||||
-- 删除所有商品(保留用户和其他核心数据)
|
||||
DELETE FROM game_listings WHERE 1=1;
|
||||
|
||||
-- 提交事务
|
||||
COMMIT;
|
||||
|
||||
-- 显示清理后的统计
|
||||
SELECT COUNT(*) as remaining_listings FROM game_listings;
|
||||
Reference in New Issue
Block a user