复制项目
This commit is contained in:
753
internal/rpc/chat/wallet.go
Normal file
753
internal/rpc/chat/wallet.go
Normal file
@@ -0,0 +1,753 @@
|
||||
// Copyright © 2023 OpenIM open source community. All rights reserved.
|
||||
//
|
||||
// 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 chat
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
chatdb "git.imall.cloud/openim/chat/pkg/common/db/table/chat"
|
||||
"git.imall.cloud/openim/chat/pkg/common/mctx"
|
||||
"git.imall.cloud/openim/chat/pkg/common/util"
|
||||
"git.imall.cloud/openim/chat/pkg/eerrs"
|
||||
chatpb "git.imall.cloud/openim/chat/pkg/protocol/chat"
|
||||
"github.com/google/uuid"
|
||||
"github.com/openimsdk/tools/errs"
|
||||
"github.com/openimsdk/tools/log"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
)
|
||||
|
||||
// GetWalletBalance 获取钱包余额
|
||||
func (o *chatSvr) GetWalletBalance(ctx context.Context, req *chatpb.GetWalletBalanceReq) (*chatpb.GetWalletBalanceResp, error) {
|
||||
// 获取用户ID
|
||||
userID, _, err := mctx.Check(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 获取钱包信息
|
||||
wallet, err := o.Database.GetWallet(ctx, userID)
|
||||
if err != nil {
|
||||
// 如果钱包不存在,返回余额为0
|
||||
if errors.Is(err, mongo.ErrNoDocuments) {
|
||||
return &chatpb.GetWalletBalanceResp{
|
||||
Balance: 0,
|
||||
}, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &chatpb.GetWalletBalanceResp{
|
||||
Balance: wallet.Balance,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetWalletInfo 获取钱包详细信息
|
||||
func (o *chatSvr) GetWalletInfo(ctx context.Context, req *chatpb.GetWalletInfoReq) (*chatpb.GetWalletInfoResp, error) {
|
||||
// 获取用户ID
|
||||
userID, _, err := mctx.Check(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 获取钱包信息
|
||||
wallet, err := o.Database.GetWallet(ctx, userID)
|
||||
if err != nil {
|
||||
// 如果钱包不存在,返回默认值
|
||||
if errors.Is(err, mongo.ErrNoDocuments) {
|
||||
return &chatpb.GetWalletInfoResp{
|
||||
Balance: 0,
|
||||
WithdrawAccount: "",
|
||||
WithdrawAccountType: 0,
|
||||
RealNameAuth: nil,
|
||||
WithdrawReceiveAccount: "",
|
||||
HasPaymentPassword: false,
|
||||
}, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 转换实名认证信息
|
||||
var realNameAuth *chatpb.RealNameAuthInfo
|
||||
if wallet.RealNameAuth.IDCard != "" {
|
||||
realNameAuth = &chatpb.RealNameAuthInfo{
|
||||
IdCard: wallet.RealNameAuth.IDCard,
|
||||
IdCardPhotoFront: wallet.RealNameAuth.IDCardPhotoFront,
|
||||
IdCardPhotoBack: wallet.RealNameAuth.IDCardPhotoBack,
|
||||
Name: wallet.RealNameAuth.Name,
|
||||
AuditStatus: wallet.RealNameAuth.AuditStatus,
|
||||
}
|
||||
}
|
||||
|
||||
return &chatpb.GetWalletInfoResp{
|
||||
Balance: wallet.Balance,
|
||||
WithdrawAccount: wallet.WithdrawAccount,
|
||||
WithdrawAccountType: wallet.WithdrawAccountType,
|
||||
RealNameAuth: realNameAuth,
|
||||
WithdrawReceiveAccount: wallet.WithdrawReceiveAccount,
|
||||
HasPaymentPassword: wallet.PaymentPassword != "",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetWalletBalanceRecords 获取余额明细(余额变动记录)
|
||||
func (o *chatSvr) GetWalletBalanceRecords(ctx context.Context, req *chatpb.GetWalletBalanceRecordsReq) (*chatpb.GetWalletBalanceRecordsResp, error) {
|
||||
// 获取用户ID
|
||||
userID, _, err := mctx.Check(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var total int64
|
||||
var records []*chatdb.WalletBalanceRecord
|
||||
|
||||
// 根据类型查询或查询所有
|
||||
if req.Type > 0 {
|
||||
// 按类型查询
|
||||
total, records, err = o.Database.GetWalletBalanceRecordsByUserIDAndType(ctx, userID, req.Type, req.Pagination)
|
||||
} else {
|
||||
// 查询所有
|
||||
total, records, err = o.Database.GetWalletBalanceRecords(ctx, userID, req.Pagination)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 转换为响应格式
|
||||
recordInfos := make([]*chatpb.WalletBalanceRecordInfo, 0, len(records))
|
||||
for _, record := range records {
|
||||
recordInfos = append(recordInfos, &chatpb.WalletBalanceRecordInfo{
|
||||
Id: record.ID,
|
||||
UserID: record.UserID,
|
||||
Amount: record.Amount,
|
||||
Type: record.Type,
|
||||
BeforeBalance: record.BeforeBalance,
|
||||
AfterBalance: record.AfterBalance,
|
||||
OrderID: record.OrderID,
|
||||
TransactionID: record.TransactionID,
|
||||
RedPacketID: record.RedPacketID,
|
||||
Remark: record.Remark,
|
||||
CreateTime: record.CreateTime.UnixMilli(),
|
||||
})
|
||||
}
|
||||
|
||||
return &chatpb.GetWalletBalanceRecordsResp{
|
||||
Total: uint32(total),
|
||||
Records: recordInfos,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SetWithdrawAccount 设置提现账号
|
||||
func (o *chatSvr) SetWithdrawAccount(ctx context.Context, req *chatpb.SetWithdrawAccountReq) (*chatpb.SetWithdrawAccountResp, error) {
|
||||
// 获取用户ID
|
||||
userID, _, err := mctx.Check(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 验证必填字段
|
||||
if req.Account == "" {
|
||||
return nil, errs.ErrArgs.WrapMsg("提现账号不能为空")
|
||||
}
|
||||
if req.AccountType <= 0 || req.AccountType > 3 {
|
||||
return nil, errs.ErrArgs.WrapMsg("账号类型无效,必须是1-支付宝,2-微信,3-银行卡")
|
||||
}
|
||||
|
||||
// 更新提现账号
|
||||
if err := o.Database.UpdateWalletWithdrawAccountWithType(ctx, userID, req.Account, req.AccountType); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &chatpb.SetWithdrawAccountResp{}, nil
|
||||
}
|
||||
|
||||
// SetPaymentPassword 设置支付密码(首次设置或修改)
|
||||
func (o *chatSvr) SetPaymentPassword(ctx context.Context, req *chatpb.SetPaymentPasswordReq) (*chatpb.SetPaymentPasswordResp, error) {
|
||||
// 获取用户ID
|
||||
userID, _, err := mctx.Check(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 验证必填字段
|
||||
if req.NewPassword == "" {
|
||||
return nil, errs.ErrArgs.WrapMsg("新支付密码不能为空")
|
||||
}
|
||||
|
||||
// 清理新密码(去除首尾空格)
|
||||
newPassword := strings.TrimSpace(req.NewPassword)
|
||||
if newPassword == "" {
|
||||
return nil, errs.ErrArgs.WrapMsg("新支付密码不能为空")
|
||||
}
|
||||
|
||||
// 获取钱包信息
|
||||
wallet, err := o.Database.GetWallet(ctx, userID)
|
||||
if err != nil {
|
||||
// 如果钱包不存在,创建钱包并设置支付密码
|
||||
if errors.Is(err, mongo.ErrNoDocuments) {
|
||||
// 首次设置,不需要验证旧密码
|
||||
if req.OldPassword != "" {
|
||||
return nil, errs.ErrArgs.WrapMsg("首次设置支付密码不需要提供旧密码")
|
||||
}
|
||||
|
||||
// 创建钱包并设置支付密码
|
||||
now := time.Now()
|
||||
newWallet := &chatdb.Wallet{
|
||||
UserID: userID,
|
||||
Balance: 0,
|
||||
PaymentPassword: newPassword,
|
||||
CreateTime: now,
|
||||
UpdateTime: now,
|
||||
}
|
||||
if err := o.Database.CreateWallet(ctx, newWallet); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &chatpb.SetPaymentPasswordResp{}, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 钱包已存在,判断是首次设置还是修改
|
||||
hasPaymentPassword := wallet.PaymentPassword != ""
|
||||
|
||||
if hasPaymentPassword {
|
||||
// 修改支付密码,需要验证旧密码
|
||||
if req.OldPassword == "" {
|
||||
return nil, errs.ErrArgs.WrapMsg("修改支付密码需要提供旧密码")
|
||||
}
|
||||
// 清理旧密码和存储的密码(去除首尾空格)
|
||||
oldPassword := strings.TrimSpace(req.OldPassword)
|
||||
storedPassword := strings.TrimSpace(wallet.PaymentPassword)
|
||||
if storedPassword != oldPassword {
|
||||
return nil, errs.ErrArgs.WrapMsg("旧支付密码错误")
|
||||
}
|
||||
if newPassword == oldPassword {
|
||||
return nil, errs.ErrArgs.WrapMsg("新密码不能与旧密码相同")
|
||||
}
|
||||
} else {
|
||||
// 首次设置支付密码,不需要验证旧密码
|
||||
if req.OldPassword != "" {
|
||||
return nil, errs.ErrArgs.WrapMsg("首次设置支付密码不需要提供旧密码")
|
||||
}
|
||||
}
|
||||
|
||||
// 更新支付密码
|
||||
if err := o.Database.UpdateWalletPaymentPassword(ctx, userID, newPassword); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &chatpb.SetPaymentPasswordResp{}, nil
|
||||
}
|
||||
|
||||
// CreateWithdrawApplication 申请提现
|
||||
func (o *chatSvr) CreateWithdrawApplication(ctx context.Context, req *chatpb.CreateWithdrawApplicationReq) (*chatpb.CreateWithdrawApplicationResp, error) {
|
||||
// 获取用户ID
|
||||
userID, _, err := mctx.Check(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 验证必填字段
|
||||
if req.Amount <= 0 {
|
||||
return nil, errs.NewCodeError(errs.ErrArgs.Code(), "提现金额必须大于0")
|
||||
}
|
||||
if req.PaymentPassword == "" {
|
||||
return nil, errs.NewCodeError(errs.ErrArgs.Code(), "支付密码不能为空")
|
||||
}
|
||||
|
||||
// 从数据库 SystemConfig 集合读取 withdraw_limit 配置并验证提现限额
|
||||
withdrawLimitConfig, _ := o.Database.GetSystemConfig(ctx, "withdraw_limit")
|
||||
if withdrawLimitConfig != nil {
|
||||
// 如果配置存在但未启用,跳过验证
|
||||
if !withdrawLimitConfig.Enabled {
|
||||
log.ZInfo(ctx, "withdraw_limit config is disabled, skipping validation")
|
||||
} else {
|
||||
// 配置存在且启用,必须验证
|
||||
limitValue := strings.TrimSpace(withdrawLimitConfig.Value)
|
||||
if limitValue == "" {
|
||||
log.ZWarn(ctx, "withdraw_limit config value is empty", nil)
|
||||
return nil, errs.NewCodeError(errs.ErrArgs.Code(), "提现限额配置错误,请联系管理员")
|
||||
}
|
||||
|
||||
// 解析提现限制配置(格式:最低限制-最高限制,单位:元,需要转换为分)
|
||||
parts := strings.Split(limitValue, "-")
|
||||
if len(parts) != 2 {
|
||||
log.ZWarn(ctx, "Invalid withdraw_limit config format, expected 'min-max'", nil,
|
||||
"value", limitValue)
|
||||
return nil, errs.NewCodeError(errs.ErrArgs.Code(), "提现限额配置格式错误,请联系管理员")
|
||||
}
|
||||
|
||||
minLimitStr := strings.TrimSpace(parts[0])
|
||||
maxLimitStr := strings.TrimSpace(parts[1])
|
||||
minLimitYuan, err1 := strconv.ParseFloat(minLimitStr, 64)
|
||||
maxLimitYuan, err2 := strconv.ParseFloat(maxLimitStr, 64)
|
||||
|
||||
if err1 != nil || err2 != nil {
|
||||
log.ZWarn(ctx, "Failed to parse withdraw_limit config values", nil,
|
||||
"minLimitStr", minLimitStr,
|
||||
"maxLimitStr", maxLimitStr,
|
||||
"minLimitErr", err1,
|
||||
"maxLimitErr", err2)
|
||||
return nil, errs.NewCodeError(errs.ErrArgs.Code(), "提现限额配置解析失败,请联系管理员")
|
||||
}
|
||||
|
||||
// 将元转换为分(乘以100)
|
||||
minLimit := int64(minLimitYuan * 100)
|
||||
maxLimit := int64(maxLimitYuan * 100)
|
||||
|
||||
// 验证配置值的有效性
|
||||
if minLimit <= 0 {
|
||||
log.ZWarn(ctx, "Invalid withdraw_limit minLimit, must be greater than 0", nil,
|
||||
"minLimitYuan", minLimitYuan,
|
||||
"minLimit", minLimit)
|
||||
return nil, errs.NewCodeError(errs.ErrArgs.Code(), "提现最低限额配置无效,请联系管理员")
|
||||
}
|
||||
if maxLimit <= 0 {
|
||||
log.ZWarn(ctx, "Invalid withdraw_limit maxLimit, must be greater than 0", nil,
|
||||
"maxLimitYuan", maxLimitYuan,
|
||||
"maxLimit", maxLimit)
|
||||
return nil, errs.NewCodeError(errs.ErrArgs.Code(), "提现最高限额配置无效,请联系管理员")
|
||||
}
|
||||
if minLimit > maxLimit {
|
||||
log.ZWarn(ctx, "Invalid withdraw_limit config, minLimit > maxLimit", nil,
|
||||
"minLimitYuan", minLimitYuan,
|
||||
"maxLimitYuan", maxLimitYuan,
|
||||
"minLimit", minLimit,
|
||||
"maxLimit", maxLimit)
|
||||
return nil, errs.NewCodeError(errs.ErrArgs.Code(), "提现限额配置错误(最低限额不能大于最高限额),请联系管理员")
|
||||
}
|
||||
|
||||
// 验证提现金额是否在限制范围内(req.Amount 单位是分)
|
||||
if req.Amount < minLimit {
|
||||
return nil, errs.NewCodeError(errs.ErrArgs.Code(), fmt.Sprintf("提现金额不能少于 %.2f 元(%d 分)", minLimitYuan, minLimit))
|
||||
}
|
||||
if req.Amount > maxLimit {
|
||||
return nil, errs.NewCodeError(errs.ErrArgs.Code(), fmt.Sprintf("提现金额不能超过 %.2f 元(%d 分)", maxLimitYuan, maxLimit))
|
||||
}
|
||||
|
||||
log.ZInfo(ctx, "Withdraw amount validated against withdraw_limit config",
|
||||
"amount", req.Amount,
|
||||
"minLimit", minLimit,
|
||||
"maxLimit", maxLimit)
|
||||
}
|
||||
}
|
||||
|
||||
// 清理支付密码(去除首尾空格)
|
||||
paymentPassword := strings.TrimSpace(req.PaymentPassword)
|
||||
if paymentPassword == "" {
|
||||
return nil, errs.NewCodeError(errs.ErrArgs.Code(), "支付密码不能为空")
|
||||
}
|
||||
|
||||
// 获取钱包信息,验证余额是否足够
|
||||
wallet, err := o.Database.GetWallet(ctx, userID)
|
||||
if err != nil {
|
||||
if errors.Is(err, mongo.ErrNoDocuments) {
|
||||
return nil, errs.NewCodeError(errs.ErrArgs.Code(), "钱包不存在,无法申请提现")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 检查是否已完成实名认证
|
||||
if wallet.RealNameAuth.IDCard == "" || wallet.RealNameAuth.Name == "" {
|
||||
return nil, errs.NewCodeError(errs.ErrArgs.Code(), "请先完成实名认证才能申请提现")
|
||||
}
|
||||
|
||||
// 检查实名认证审核状态,必须为审核通过(1)才能提现
|
||||
if wallet.RealNameAuth.AuditStatus != 1 {
|
||||
switch wallet.RealNameAuth.AuditStatus {
|
||||
case 0:
|
||||
return nil, errs.NewCodeError(errs.ErrArgs.Code(), "实名认证正在审核中,请等待审核通过后再申请提现")
|
||||
case 2:
|
||||
return nil, errs.NewCodeError(errs.ErrArgs.Code(), "实名认证审核未通过,无法申请提现")
|
||||
default:
|
||||
return nil, errs.NewCodeError(errs.ErrArgs.Code(), "实名认证状态异常,无法申请提现")
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否设置了支付密码
|
||||
if wallet.PaymentPassword == "" {
|
||||
return nil, errs.NewCodeError(errs.ErrArgs.Code(), "请先设置支付密码")
|
||||
}
|
||||
|
||||
// 清理数据库中存储的支付密码(去除首尾空格)
|
||||
storedPassword := strings.TrimSpace(wallet.PaymentPassword)
|
||||
|
||||
// 调试日志:打印支付密码验证信息
|
||||
log.ZInfo(ctx, "支付密码验证调试",
|
||||
"userID", userID,
|
||||
"inputPassword", paymentPassword,
|
||||
"inputPasswordLen", len(paymentPassword),
|
||||
"storedPassword", storedPassword,
|
||||
"storedPasswordLen", len(storedPassword),
|
||||
"storedPasswordRaw", wallet.PaymentPassword,
|
||||
"storedPasswordRawLen", len(wallet.PaymentPassword),
|
||||
"match", storedPassword == paymentPassword,
|
||||
)
|
||||
|
||||
// 验证支付密码
|
||||
if storedPassword != paymentPassword {
|
||||
log.ZWarn(ctx, "支付密码验证失败", nil,
|
||||
"userID", userID,
|
||||
"inputPassword", paymentPassword,
|
||||
"storedPassword", storedPassword,
|
||||
)
|
||||
return nil, eerrs.ErrPaymentPassword.WrapMsg("支付密码错误")
|
||||
}
|
||||
|
||||
// 检查余额是否足够
|
||||
if wallet.Balance < req.Amount {
|
||||
return nil, eerrs.ErrInsufficientBalance.WrapMsg("余额不足,无法申请提现")
|
||||
}
|
||||
|
||||
// 从钱包中获取提现账号
|
||||
if wallet.WithdrawAccount == "" {
|
||||
return nil, errs.NewCodeError(errs.ErrArgs.Code(), "请先在钱包中设置提现账号")
|
||||
}
|
||||
|
||||
// 使用事务:扣减余额、创建余额变动记录、创建提现申请
|
||||
applicationID := uuid.New().String()
|
||||
now := time.Now()
|
||||
|
||||
// 扣减余额(使用负数表示扣款)
|
||||
beforeBalance, afterBalance, err := o.Database.IncrementWalletBalance(ctx, userID, -req.Amount)
|
||||
if err != nil {
|
||||
// IncrementWalletBalance 已经返回了具体的错误信息(如余额不足),直接返回
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 创建余额变动记录
|
||||
balanceRecord := &chatdb.WalletBalanceRecord{
|
||||
ID: uuid.New().String(),
|
||||
UserID: userID,
|
||||
Amount: -req.Amount, // 负数表示减少
|
||||
Type: chatdb.BalanceRecordTypeWithdraw, // 提现/提款
|
||||
BeforeBalance: beforeBalance,
|
||||
AfterBalance: afterBalance,
|
||||
Remark: "提现申请",
|
||||
CreateTime: now,
|
||||
}
|
||||
if err := o.Database.CreateWalletBalanceRecord(ctx, balanceRecord); err != nil {
|
||||
// 如果创建记录失败,回滚余额(增加回去)
|
||||
_, _, _ = o.Database.IncrementWalletBalance(ctx, userID, req.Amount)
|
||||
return nil, errs.NewCodeError(errs.ErrArgs.Code(), "创建余额变动记录失败")
|
||||
}
|
||||
|
||||
// 创建提现申请
|
||||
application := &chatdb.WithdrawApplication{
|
||||
ID: applicationID,
|
||||
UserID: userID,
|
||||
Amount: req.Amount,
|
||||
WithdrawAccount: wallet.WithdrawAccount,
|
||||
WithdrawAccountType: wallet.WithdrawAccountType,
|
||||
Status: chatdb.WithdrawApplicationStatusPending, // 待审核
|
||||
IP: req.Ip,
|
||||
DeviceID: req.DeviceID,
|
||||
Platform: req.Platform,
|
||||
DeviceModel: req.DeviceModel,
|
||||
DeviceBrand: req.DeviceBrand,
|
||||
OSVersion: req.OsVersion,
|
||||
AppVersion: req.AppVersion,
|
||||
Remark: "", // 备注由后台管理员填写
|
||||
CreateTime: now,
|
||||
UpdateTime: now,
|
||||
}
|
||||
|
||||
// 保存提现申请
|
||||
if err := o.Database.CreateWithdrawApplication(ctx, application); err != nil {
|
||||
// 如果创建申请失败,回滚余额(增加回去)
|
||||
// 注意:余额变动记录保留,因为余额确实已经扣减了
|
||||
// 如果后续需要,可以通过记录ID删除余额变动记录
|
||||
_, _, _ = o.Database.IncrementWalletBalance(ctx, userID, req.Amount)
|
||||
return nil, errs.NewCodeError(errs.ErrArgs.Code(), "创建提现申请失败,余额已回滚")
|
||||
}
|
||||
|
||||
return &chatpb.CreateWithdrawApplicationResp{
|
||||
ApplicationID: applicationID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetWithdrawApplications 获取用户的提现申请列表
|
||||
func (o *chatSvr) GetWithdrawApplications(ctx context.Context, req *chatpb.GetWithdrawApplicationsReq) (*chatpb.GetWithdrawApplicationsResp, error) {
|
||||
// 获取用户ID
|
||||
userID, _, err := mctx.Check(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 获取提现申请列表
|
||||
total, applications, err := o.Database.GetWithdrawApplicationsByUserID(ctx, userID, req.Pagination)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 转换为响应格式
|
||||
applicationInfos := make([]*chatpb.WithdrawApplicationInfo, 0, len(applications))
|
||||
for _, app := range applications {
|
||||
var auditTime int64
|
||||
if !app.AuditTime.IsZero() {
|
||||
auditTime = app.AuditTime.UnixMilli()
|
||||
}
|
||||
applicationInfos = append(applicationInfos, &chatpb.WithdrawApplicationInfo{
|
||||
Id: app.ID,
|
||||
UserID: app.UserID,
|
||||
Amount: app.Amount,
|
||||
WithdrawAccount: app.WithdrawAccount,
|
||||
WithdrawAccountType: app.WithdrawAccountType,
|
||||
Status: app.Status,
|
||||
AuditorID: app.AuditorID,
|
||||
AuditTime: auditTime,
|
||||
AuditRemark: app.AuditRemark,
|
||||
Ip: app.IP,
|
||||
DeviceID: app.DeviceID,
|
||||
Platform: app.Platform,
|
||||
DeviceModel: app.DeviceModel,
|
||||
DeviceBrand: app.DeviceBrand,
|
||||
OsVersion: app.OSVersion,
|
||||
AppVersion: app.AppVersion,
|
||||
Remark: app.Remark,
|
||||
CreateTime: app.CreateTime.UnixMilli(),
|
||||
UpdateTime: app.UpdateTime.UnixMilli(),
|
||||
})
|
||||
}
|
||||
|
||||
return &chatpb.GetWithdrawApplicationsResp{
|
||||
Total: uint32(total),
|
||||
Applications: applicationInfos,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// RealNameAuth 实名认证
|
||||
func (o *chatSvr) RealNameAuth(ctx context.Context, req *chatpb.RealNameAuthReq) (*chatpb.RealNameAuthResp, error) {
|
||||
// 获取用户ID
|
||||
userID, _, err := mctx.Check(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 检查用户是否已经实名认证且审核通过
|
||||
wallet, err := o.Database.GetWallet(ctx, userID)
|
||||
if err == nil && wallet != nil {
|
||||
// 如果已经实名认证且审核状态为通过(1),不允许重新认证
|
||||
if wallet.RealNameAuth.IDCard != "" && wallet.RealNameAuth.AuditStatus == 1 {
|
||||
return nil, errs.ErrArgs.WrapMsg("您已经完成实名认证,不能重新认证")
|
||||
}
|
||||
} else if err != nil && !errors.Is(err, mongo.ErrNoDocuments) {
|
||||
// 如果不是"文档不存在"的错误,返回错误
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 验证必填字段
|
||||
if req.IdCard == "" {
|
||||
return nil, errs.ErrArgs.WrapMsg("身份证号不能为空")
|
||||
}
|
||||
if req.Name == "" {
|
||||
return nil, errs.ErrArgs.WrapMsg("真实姓名不能为空")
|
||||
}
|
||||
if req.IdCardPhotoFront == "" {
|
||||
return nil, errs.ErrArgs.WrapMsg("身份证正面照片不能为空")
|
||||
}
|
||||
if req.IdCardPhotoBack == "" {
|
||||
return nil, errs.ErrArgs.WrapMsg("身份证反面照片不能为空")
|
||||
}
|
||||
|
||||
// 清理输入(去除首尾空格)
|
||||
idCard := strings.TrimSpace(req.IdCard)
|
||||
name := strings.TrimSpace(req.Name)
|
||||
idCardPhotoFront := strings.TrimSpace(req.IdCardPhotoFront)
|
||||
idCardPhotoBack := strings.TrimSpace(req.IdCardPhotoBack)
|
||||
|
||||
if idCard == "" || name == "" {
|
||||
return nil, errs.ErrArgs.WrapMsg("身份证号和姓名不能为空")
|
||||
}
|
||||
if idCardPhotoFront == "" || idCardPhotoBack == "" {
|
||||
return nil, errs.ErrArgs.WrapMsg("身份证正面照片和反面照片不能为空")
|
||||
}
|
||||
|
||||
// 验证姓名只能包含中文字符(不允许英文、数字和标点符号)
|
||||
chineseRegex := regexp.MustCompile(`^[\p{Han}]+$`)
|
||||
if !chineseRegex.MatchString(name) {
|
||||
return nil, errs.ErrArgs.WrapMsg("真实姓名只能包含中文,不能包含英文、数字或标点符号")
|
||||
}
|
||||
|
||||
// 构建原始数据 JSON
|
||||
rawData := map[string]string{
|
||||
"cardNo": idCard,
|
||||
"realName": name,
|
||||
}
|
||||
rawDataJSON, err := json.Marshal(rawData)
|
||||
if err != nil {
|
||||
return nil, errs.WrapMsg(err, "构建数据失败")
|
||||
}
|
||||
|
||||
// AES 密钥(32字节的十六进制字符串)
|
||||
aesKey := "a7f3c9e2b8d4f1a6c3e9b2d7f4a1c8e5b2d9f6a3c8e1b4d7f2a9c5e8b1d4f7a2"
|
||||
|
||||
// 在客户端本地加密数据(使用 AES-GCM 模式)
|
||||
log.ZInfo(ctx, "开始本地加密实名认证数据", "userID", userID, "rawData", string(rawDataJSON), "idCard", idCard, "name", name)
|
||||
encryptedData, err := util.EncryptRealNameAuthData(string(rawDataJSON), aesKey)
|
||||
if err != nil {
|
||||
log.ZError(ctx, "本地加密失败", err, "userID", userID)
|
||||
return nil, errs.WrapMsg(err, "加密数据失败")
|
||||
}
|
||||
log.ZInfo(ctx, "本地加密成功", "userID", userID, "encryptedLength", len(encryptedData), "encryptedData", encryptedData)
|
||||
|
||||
// 调用验证接口(直接发送加密后的字符串)
|
||||
baseURL := "http://95.40.154.128"
|
||||
verifyURL := baseURL + "/idcheck"
|
||||
|
||||
log.ZInfo(ctx, "准备调用验证接口", "userID", userID, "url", verifyURL, "encryptedLength", len(encryptedData))
|
||||
|
||||
// 创建请求,请求体直接是加密后的字符串
|
||||
httpReq, err := http.NewRequest("POST", verifyURL, bytes.NewBufferString(encryptedData))
|
||||
if err != nil {
|
||||
log.ZError(ctx, "创建验证请求失败", err, "userID", userID, "url", verifyURL)
|
||||
return nil, errs.WrapMsg(err, "创建验证请求失败")
|
||||
}
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
|
||||
// 发送请求
|
||||
client := &http.Client{Timeout: 30 * time.Second}
|
||||
verifyResp, err := client.Do(httpReq)
|
||||
if err != nil {
|
||||
log.ZError(ctx, "调用验证接口失败", err, "userID", userID, "url", verifyURL)
|
||||
return nil, errs.WrapMsg(err, "调用验证接口失败")
|
||||
}
|
||||
defer verifyResp.Body.Close()
|
||||
|
||||
verifyRespBody, err := io.ReadAll(verifyResp.Body)
|
||||
if err != nil {
|
||||
return nil, errs.WrapMsg(err, "读取验证接口响应失败")
|
||||
}
|
||||
|
||||
log.ZInfo(ctx, "验证接口响应", "userID", userID, "statusCode", verifyResp.StatusCode, "responseBody", string(verifyRespBody), "responseLength", len(verifyRespBody))
|
||||
|
||||
// 检查 HTTP 状态码
|
||||
if verifyResp.StatusCode != http.StatusOK {
|
||||
log.ZWarn(ctx, "验证接口返回错误状态码", nil, "userID", userID, "statusCode", verifyResp.StatusCode, "response", string(verifyRespBody))
|
||||
return &chatpb.RealNameAuthResp{
|
||||
Success: false,
|
||||
Message: fmt.Sprintf("验证请求失败,状态码: %d, 响应: %s", verifyResp.StatusCode, string(verifyRespBody)),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 解析响应(格式:{"success": bool, "data": interface{}, "error": string, "message": string})
|
||||
var verifyResult struct {
|
||||
Success bool `json:"success"`
|
||||
Data interface{} `json:"data"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
if err := json.Unmarshal(verifyRespBody, &verifyResult); err != nil {
|
||||
log.ZWarn(ctx, "解析验证接口响应失败", err, "userID", userID, "response", string(verifyRespBody))
|
||||
return &chatpb.RealNameAuthResp{
|
||||
Success: false,
|
||||
Message: fmt.Sprintf("解析验证结果失败: %s, 响应: %s", err.Error(), string(verifyRespBody)),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 检查验证结果
|
||||
if !verifyResult.Success {
|
||||
errorMsg := verifyResult.Error
|
||||
if errorMsg == "" {
|
||||
errorMsg = verifyResult.Message
|
||||
}
|
||||
if errorMsg == "" {
|
||||
errorMsg = "验证失败"
|
||||
}
|
||||
|
||||
return &chatpb.RealNameAuthResp{
|
||||
Success: false,
|
||||
Message: errorMsg,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 验证成功,保存实名认证信息到数据库
|
||||
if verifyResult.Success {
|
||||
// 获取或创建钱包
|
||||
_, err := o.Database.GetWallet(ctx, userID)
|
||||
if err != nil {
|
||||
if errors.Is(err, mongo.ErrNoDocuments) {
|
||||
// 钱包不存在,创建钱包
|
||||
now := time.Now()
|
||||
newWallet := &chatdb.Wallet{
|
||||
UserID: userID,
|
||||
Balance: 0,
|
||||
RealNameAuth: chatdb.RealNameAuth{
|
||||
IDCard: idCard,
|
||||
Name: name,
|
||||
IDCardPhotoFront: idCardPhotoFront,
|
||||
IDCardPhotoBack: idCardPhotoBack,
|
||||
AuditStatus: 0, // 0-未审核
|
||||
},
|
||||
CreateTime: now,
|
||||
UpdateTime: now,
|
||||
}
|
||||
if err := o.Database.CreateWallet(ctx, newWallet); err != nil {
|
||||
return nil, errs.WrapMsg(err, "创建钱包失败")
|
||||
}
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
// 更新实名认证信息(重新提交后,审核状态重置为待审核)
|
||||
realNameAuth := chatdb.RealNameAuth{
|
||||
IDCard: idCard,
|
||||
Name: name,
|
||||
IDCardPhotoFront: idCardPhotoFront,
|
||||
IDCardPhotoBack: idCardPhotoBack,
|
||||
AuditStatus: 0, // 0-未审核(重新提交后重置为待审核状态)
|
||||
}
|
||||
if err := o.Database.UpdateWalletRealNameAuth(ctx, userID, realNameAuth); err != nil {
|
||||
return nil, errs.WrapMsg(err, "更新实名认证信息失败")
|
||||
}
|
||||
log.ZInfo(ctx, "实名认证信息已更新,审核状态重置为待审核", "userID", userID, "idCard", idCard, "name", name, "auditStatus", 0)
|
||||
}
|
||||
|
||||
log.ZInfo(ctx, "实名认证成功并已保存", "userID", userID, "idCard", idCard, "name", name)
|
||||
|
||||
// 获取更新后的钱包信息,返回身份证照片URL
|
||||
updatedWallet, err := o.Database.GetWallet(ctx, userID)
|
||||
var idCardPhotoFront, idCardPhotoBack string
|
||||
if err == nil && updatedWallet != nil {
|
||||
idCardPhotoFront = updatedWallet.RealNameAuth.IDCardPhotoFront
|
||||
idCardPhotoBack = updatedWallet.RealNameAuth.IDCardPhotoBack
|
||||
}
|
||||
|
||||
return &chatpb.RealNameAuthResp{
|
||||
Success: true,
|
||||
Message: "提交成功了,请等待审核",
|
||||
IdCardPhotoFront: idCardPhotoFront,
|
||||
IdCardPhotoBack: idCardPhotoBack,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 这行代码永远不会执行到,因为如果 verifyResult.Success 为 false,已经在前面返回了
|
||||
// 但为了代码完整性保留
|
||||
log.ZError(ctx, "代码逻辑错误:验证失败但未返回", nil, "userID", userID, "verifyResult", verifyResult)
|
||||
return &chatpb.RealNameAuthResp{
|
||||
Success: false,
|
||||
Message: "验证失败",
|
||||
}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user