This commit is contained in:
2026-03-02 12:57:17 +08:00
45 changed files with 1788 additions and 246 deletions

View File

@@ -4,10 +4,10 @@ import (
"context"
"database/sql"
"fmt"
"time"
"qnc-server/common/globalkey"
"os"
"qnc-server/common/xerr"
"qnc-server/pkg/lzkit/lzUtils"
"time"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/stores/sqlx"
@@ -32,6 +32,13 @@ func NewAdminAuditWithdrawalLogic(ctx context.Context, svcCtx *svc.ServiceContex
}
}
// parseFloat 解析配置中的浮点数
func (l *AdminAuditWithdrawalLogic) parseFloat(s string) (float64, error) {
var result float64
_, err := fmt.Sscanf(s, "%f", &result)
return result, err
}
func (l *AdminAuditWithdrawalLogic) AdminAuditWithdrawal(req *types.AdminAuditWithdrawalReq) (resp *types.AdminAuditWithdrawalResp, err error) {
// 1. 查询提现记录
withdrawal, err := l.svcCtx.AgentWithdrawalModel.FindOne(l.ctx, req.WithdrawalId)
@@ -46,39 +53,126 @@ func (l *AdminAuditWithdrawalLogic) AdminAuditWithdrawal(req *types.AdminAuditWi
// 4. 使用事务处理审核
err = l.svcCtx.AgentWithdrawalModel.Trans(l.ctx, func(transCtx context.Context, session sqlx.Session) error {
if req.Status == 2 { // 审核通过
// 4.1 更新提现记录状态为提现中
withdrawal.Status = 4 // 提现中
withdrawal.Remark = sql.NullString{String: req.Remark, Valid: true}
if err := l.svcCtx.AgentWithdrawalModel.UpdateWithVersion(transCtx, session, withdrawal); err != nil {
return errors.Wrapf(err, "更新提现记录失败")
}
switch req.Status {
case 2: // 审核通过
// 4.1 根据提现方式处理
switch withdrawal.WithdrawalType {
case 1:
// 支付宝提现:审核通过前再次校验月度额度,避免一次性通过多笔超限
now := time.Now()
monthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
nextMonthStart := monthStart.AddDate(0, 1, 0)
// 4.2 调用支付宝转账接口
outBizNo := withdrawal.WithdrawNo
transferResp, err := l.svcCtx.AlipayService.AliTransfer(transCtx, withdrawal.PayeeAccount, withdrawal.PayeeName, withdrawal.ActualAmount, "代理提现", outBizNo)
if err != nil {
// 转账失败,更新状态为失败
withdrawal.Status = 6 // 提现失败
withdrawal.Remark = sql.NullString{String: fmt.Sprintf("转账失败: %v", err), Valid: true}
l.svcCtx.AgentWithdrawalModel.UpdateWithVersion(transCtx, session, withdrawal)
// 解冻余额
wallet, err := l.svcCtx.AgentWalletModel.FindOneByAgentId(transCtx, withdrawal.AgentId)
if err == nil {
wallet.FrozenBalance -= withdrawal.Amount
wallet.Balance += withdrawal.Amount
l.svcCtx.AgentWalletModel.UpdateWithVersion(transCtx, session, wallet)
// 获取支付宝月度额度配置(默认 800 元)
alipayQuota := 800.0
if cfg, cfgErr := l.svcCtx.AgentConfigModel.FindOneByConfigKey(transCtx, "alipay_month_quota"); cfgErr == nil {
if parsed, parseErr := l.parseFloat(cfg.ConfigValue); parseErr == nil && parsed > 0 {
alipayQuota = parsed
}
}
return errors.Wrapf(err, "支付宝转账失败")
}
// 统计本月已成功的支付宝提现金额status=5
withdrawBuilder := l.svcCtx.AgentWithdrawalModel.SelectBuilder().
Where("agent_id = ? AND withdrawal_type = ? AND status = ? AND create_time >= ? AND create_time < ?",
withdrawal.AgentId, 1, 5, monthStart, nextMonthStart)
usedAmount, sumErr := l.svcCtx.AgentWithdrawalModel.FindSum(transCtx, withdrawBuilder, "amount")
if sumErr != nil {
return errors.Wrapf(sumErr, "查询本月支付宝提现额度使用情况失败")
}
// 4.3 根据转账结果更新状态
switch transferResp.Status {
case "SUCCESS":
// 转账成功
if usedAmount+withdrawal.Amount > alipayQuota {
// 超出额度,不允许通过,保持待审核状态并提示原因
withdrawal.Status = 1
withdrawal.Remark = sql.NullString{
String: fmt.Sprintf("超过本月支付宝提现额度(限额:%.2f 元,已用:%.2f 元),请使用银行卡提现或调整金额", alipayQuota, usedAmount),
Valid: true,
}
if err := l.svcCtx.AgentWithdrawalModel.UpdateWithVersion(transCtx, session, withdrawal); err != nil {
return errors.Wrapf(err, "更新提现记录失败")
}
return errors.Wrapf(xerr.NewErrMsg("超过本月支付宝提现额度,无法通过该笔提现"), "")
}
// 支付宝提现:开发环境下做模拟,不调用真实支付宝转账
mockTransferStatus := "SUCCESS"
var transferResp struct {
Status string
SubCode string
}
if os.Getenv("ENV") == "development" {
transferResp.Status = mockTransferStatus
logx.Infof("【DEV】模拟支付宝转账成功withdrawNo=%s, amount=%.2f, payee=%s",
withdrawal.WithdrawNo, withdrawal.ActualAmount, withdrawal.PayeeAccount)
} else {
// 生产环境:同步调用支付宝转账接口
outBizNo := withdrawal.WithdrawNo
resp, err := l.svcCtx.AlipayService.AliTransfer(transCtx, withdrawal.PayeeAccount, withdrawal.PayeeName, withdrawal.ActualAmount, "代理提现", outBizNo)
if err != nil {
// 调用失败保持状态为待审核1只记录备注方便管理员重试
withdrawal.Status = 1 // 待审核
withdrawal.Remark = sql.NullString{String: fmt.Sprintf("支付宝转账调用失败: %v", err), Valid: true}
_ = l.svcCtx.AgentWithdrawalModel.UpdateWithVersion(transCtx, session, withdrawal)
return errors.Wrapf(err, "支付宝转账失败")
}
transferResp.Status = resp.Status
transferResp.SubCode = resp.SubCode
}
// 4.2 根据转账结果更新状态
switch transferResp.Status {
case "SUCCESS":
// 转账成功
withdrawal.Status = 5 // 提现成功
if err := l.svcCtx.AgentWithdrawalModel.UpdateWithVersion(transCtx, session, withdrawal); err != nil {
return errors.Wrapf(err, "更新提现记录失败")
}
// 更新钱包(解冻并扣除)
wallet, err := l.svcCtx.AgentWalletModel.FindOneByAgentId(transCtx, withdrawal.AgentId)
if err != nil {
return errors.Wrapf(err, "查询钱包失败")
}
wallet.FrozenBalance -= withdrawal.Amount
wallet.WithdrawnAmount += withdrawal.Amount
if err := l.svcCtx.AgentWalletModel.UpdateWithVersion(transCtx, session, wallet); err != nil {
return errors.Wrapf(err, "更新钱包失败")
}
// 更新扣税记录状态
taxBuilder := l.svcCtx.AgentWithdrawalTaxModel.SelectBuilder().
Where("withdrawal_id = ?", withdrawal.Id)
taxRecords, err := l.svcCtx.AgentWithdrawalTaxModel.FindAll(transCtx, taxBuilder, "")
if err == nil && len(taxRecords) > 0 {
taxRecord := taxRecords[0]
taxRecord.TaxStatus = 2 // 已扣税
taxRecord.TaxTime = lzUtils.TimeToNullTime(time.Now())
l.svcCtx.AgentWithdrawalTaxModel.UpdateWithVersion(transCtx, session, taxRecord)
}
case "FAIL":
// 转账失败:保持待审核状态,方便人工处理或重试
withdrawal.Status = 1 // 待审核
errorMsg := l.mapAlipayError(transferResp.SubCode)
withdrawal.Remark = sql.NullString{String: errorMsg, Valid: true}
if err := l.svcCtx.AgentWithdrawalModel.UpdateWithVersion(transCtx, session, withdrawal); err != nil {
return errors.Wrapf(err, "更新提现记录失败")
}
case "DEALING":
// 处理中同样保持待审核状态1由管理员后续确认
withdrawal.Status = 1
withdrawal.Remark = sql.NullString{String: "支付宝处理中,请稍后重试或联系平台", Valid: true}
if err := l.svcCtx.AgentWithdrawalModel.UpdateWithVersion(transCtx, session, withdrawal); err != nil {
return errors.Wrapf(err, "更新提现记录失败")
}
}
case 2:
// 银行卡提现:审核通过即视为提现成功(线下已/将立即打款)
withdrawal.Status = 5 // 提现成功
withdrawal.Remark = sql.NullString{String: req.Remark, Valid: true}
if err := l.svcCtx.AgentWithdrawalModel.UpdateWithVersion(transCtx, session, withdrawal); err != nil {
return errors.Wrapf(err, "更新提现记录失败")
}
@@ -96,7 +190,7 @@ func (l *AdminAuditWithdrawalLogic) AdminAuditWithdrawal(req *types.AdminAuditWi
// 更新扣税记录状态
taxBuilder := l.svcCtx.AgentWithdrawalTaxModel.SelectBuilder().
Where("withdrawal_id = ? AND del_state = ?", withdrawal.Id, globalkey.DelStateNo)
Where("withdrawal_id = ?", withdrawal.Id)
taxRecords, err := l.svcCtx.AgentWithdrawalTaxModel.FindAll(transCtx, taxBuilder, "")
if err == nil && len(taxRecords) > 0 {
taxRecord := taxRecords[0]
@@ -104,30 +198,9 @@ func (l *AdminAuditWithdrawalLogic) AdminAuditWithdrawal(req *types.AdminAuditWi
taxRecord.TaxTime = lzUtils.TimeToNullTime(time.Now())
l.svcCtx.AgentWithdrawalTaxModel.UpdateWithVersion(transCtx, session, taxRecord)
}
case "FAIL":
// 转账失败
withdrawal.Status = 6 // 提现失败
errorMsg := l.mapAlipayError(transferResp.SubCode)
withdrawal.Remark = sql.NullString{String: errorMsg, Valid: true}
if err := l.svcCtx.AgentWithdrawalModel.UpdateWithVersion(transCtx, session, withdrawal); err != nil {
return errors.Wrapf(err, "更新提现记录失败")
}
// 解冻余额
wallet, err := l.svcCtx.AgentWalletModel.FindOneByAgentId(transCtx, withdrawal.AgentId)
if err == nil {
wallet.FrozenBalance -= withdrawal.Amount
wallet.Balance += withdrawal.Amount
l.svcCtx.AgentWalletModel.UpdateWithVersion(transCtx, session, wallet)
}
case "DEALING":
// 处理中,保持提现中状态,后续通过轮询更新
// 状态已经是4提现中无需更新
}
} else if req.Status == 3 { // 审核拒绝
case 3: // 审核拒绝
// 4.1 更新提现记录状态为拒绝
withdrawal.Status = 3 // 审核拒绝
withdrawal.Remark = sql.NullString{String: req.Remark, Valid: true}

View File

@@ -49,6 +49,17 @@ func (l *AdminGetAgentWithdrawalListLogic) AdminGetAgentWithdrawalList(req *type
item.Remark = v.Remark.String
}
item.CreateTime = v.CreateTime.Format("2006-01-02 15:04:05")
// 如果是银行卡提现,填充银行卡信息
if v.WithdrawalType == 2 {
if v.BankCardNo.Valid {
item.BankCardNo = v.BankCardNo.String
}
if v.BankName.Valid {
item.BankName = v.BankName.String
}
}
items = append(items, item)
}
resp = &types.AdminGetAgentWithdrawalListResp{

View File

@@ -86,7 +86,7 @@ func (l *AdminGetOrderListLogic) AdminGetOrderList(req *types.AdminGetOrderListR
return nil
}, func() error {
var err error
orders, err = l.svcCtx.OrderModel.FindPageListByPage(l.ctx, builder, req.Page, req.PageSize, "id DESC")
orders, err = l.svcCtx.OrderModel.FindPageListByPage(l.ctx, builder, req.Page, req.PageSize, "update_time DESC")
if err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "AdminGetOrderList, 查询订单列表失败 err: %v", err)
}
@@ -264,6 +264,7 @@ func (l *AdminGetOrderListLogic) AdminGetOrderList(req *types.AdminGetOrderListR
Status: order.Status,
CreateTime: order.CreateTime.Format("2006-01-02 15:04:05"),
QueryState: queryStateMap[order.Id],
UpdateTime: order.UpdateTime.Format("2006-01-02 15:04:05"),
}
if order.PayTime.Valid {
item.PayTime = order.PayTime.Time.Format("2006-01-02 15:04:05")

View File

@@ -3,12 +3,11 @@ package agent
import (
"context"
"fmt"
"time"
"qnc-server/app/main/model"
"qnc-server/common/ctxdata"
"qnc-server/common/globalkey"
"qnc-server/common/xerr"
"qnc-server/pkg/lzkit/lzUtils"
"time"
"github.com/google/uuid"
"github.com/pkg/errors"
@@ -62,53 +61,138 @@ func (l *ApplyWithdrawalLogic) ApplyWithdrawal(req *types.ApplyWithdrawalReq) (r
return nil, errors.Wrapf(xerr.NewErrMsg("请先完成实名认证"), "")
}
// 3. 验证提现金额
// 3. 验证提现方式
if req.WithdrawalType != 1 && req.WithdrawalType != 2 {
return nil, errors.Wrapf(xerr.NewErrMsg("提现方式无效"), "")
}
// 4. 验证提现信息
if req.WithdrawalType == 1 {
// 支付宝提现:验证支付宝账号和姓名
if req.PayeeAccount == "" {
return nil, errors.Wrapf(xerr.NewErrMsg("请输入支付宝账号"), "")
}
if req.PayeeName == "" {
return nil, errors.Wrapf(xerr.NewErrMsg("请输入收款人姓名"), "")
}
} else if req.WithdrawalType == 2 {
// 银行卡提现:验证银行卡号、开户行和姓名
if req.BankCardNo == "" {
return nil, errors.Wrapf(xerr.NewErrMsg("请输入银行卡号"), "")
}
if req.BankName == "" {
return nil, errors.Wrapf(xerr.NewErrMsg("请输入开户行名称"), "")
}
if req.PayeeName == "" {
return nil, errors.Wrapf(xerr.NewErrMsg("请输入收款人姓名"), "")
}
}
// 5. 验证提现金额
if req.Amount <= 0 {
return nil, errors.Wrapf(xerr.NewErrMsg("提现金额必须大于0"), "")
}
// 4. 获取钱包信息
// 6. 获取钱包信息
wallet, err := l.svcCtx.AgentWalletModel.FindOneByAgentId(l.ctx, agent.Id)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询钱包失败, %v", err)
}
// 5. 验证余额
// 7. 验证余额(包括检查是否为负数)
if wallet.Balance < 0 {
return nil, errors.Wrapf(xerr.NewErrMsg(fmt.Sprintf("账户存在欠款,请先补足欠款后再申请提现,当前余额:%.2f", wallet.Balance)), "")
}
if wallet.Balance < req.Amount {
return nil, errors.Wrapf(xerr.NewErrMsg(fmt.Sprintf("余额不足,当前余额:%.2f", wallet.Balance)), "")
}
// 6. 计算税费
// 8. 支付宝月度提现额度校验(仅针对支付宝提现)
if req.WithdrawalType == 1 {
now := time.Now()
monthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
nextMonthStart := monthStart.AddDate(0, 1, 0)
// 8.1 获取支付宝月度额度配置(默认 800 元)
alipayQuota := 800.0
if cfg, cfgErr := l.svcCtx.AgentConfigModel.FindOneByConfigKey(l.ctx, "alipay_month_quota"); cfgErr == nil {
if parsed, parseErr := l.parseFloat(cfg.ConfigValue); parseErr == nil && parsed > 0 {
alipayQuota = parsed
}
}
// 8.2 统计本月已申请/成功的支付宝提现金额status IN (1,5)),避免多次申请占用超额
withdrawBuilder := l.svcCtx.AgentWithdrawalModel.SelectBuilder().
Where("agent_id = ? AND withdrawal_type = ? AND status IN (1,5) AND create_time >= ? AND create_time < ?",
agent.Id, 1, monthStart, nextMonthStart)
usedAmount, sumErr := l.svcCtx.AgentWithdrawalModel.FindSum(l.ctx, withdrawBuilder, "amount")
if sumErr != nil {
return nil, errors.Wrapf(sumErr, "查询本月支付宝提现额度使用情况失败")
}
remainQuota := alipayQuota - usedAmount
if remainQuota <= 0 {
return nil, errors.Wrapf(
xerr.NewErrMsg(fmt.Sprintf("本月支付宝提现额度已用完(额度:%.2f 元),请使用银行卡提现", alipayQuota)),
"",
)
}
if req.Amount > remainQuota {
return nil, errors.Wrapf(
xerr.NewErrMsg(fmt.Sprintf("本月支付宝最高可提现 %.2f 元,请调整提现金额或使用银行卡提现", remainQuota)),
"",
)
}
}
// 9. 计算税费
yearMonth := int64(time.Now().Year()*100 + int(time.Now().Month()))
taxInfo, err := l.calculateTax(l.ctx, agent.Id, req.Amount, yearMonth)
if err != nil {
return nil, errors.Wrapf(err, "计算税费失败")
}
// 7. 生成提现单号
withdrawNo := fmt.Sprintf("WD%d%d", time.Now().Unix(), agent.Id)
// 10. 生成提现单号WD开头 + GenerateOutTradeNo生成的订单号确保总长度不超过32个字符
orderNo := l.svcCtx.AlipayService.GenerateOutTradeNo()
withdrawNo := "WD" + orderNo
// 确保总长度不超过32个字符
if len(withdrawNo) > 32 {
withdrawNo = withdrawNo[:32]
}
// 8. 使用事务处理提现申请
// 11. 使用事务处理提现申请
var withdrawalId string
err = l.svcCtx.AgentWalletModel.Trans(l.ctx, func(transCtx context.Context, session sqlx.Session) error {
// 8.1 冻结余额
// 11.1 冻结余额
wallet.FrozenBalance += req.Amount
wallet.Balance -= req.Amount
if err := l.svcCtx.AgentWalletModel.UpdateWithVersion(transCtx, session, wallet); err != nil {
return errors.Wrapf(err, "冻结余额失败")
}
// 8.2 创建提现记录
// 11.2 创建提现记录
withdrawal := &model.AgentWithdrawal{
Id: uuid.New().String(),
AgentId: agent.Id,
WithdrawNo: withdrawNo,
PayeeAccount: req.PayeeAccount,
PayeeName: req.PayeeName,
Amount: req.Amount,
ActualAmount: taxInfo.ActualAmount,
TaxAmount: taxInfo.TaxAmount,
Status: 1, // 处理中(待审核)
Id: uuid.New().String(),
AgentId: agent.Id,
WithdrawNo: withdrawNo,
WithdrawalType: req.WithdrawalType,
PayeeAccount: req.PayeeAccount,
PayeeName: req.PayeeName,
Amount: req.Amount,
ActualAmount: taxInfo.ActualAmount,
TaxAmount: taxInfo.TaxAmount,
Status: 1, // 待审核
}
// 如果是银行卡提现,设置银行卡相关字段
if req.WithdrawalType == 2 {
withdrawal.BankCardNo = lzUtils.StringToNullString(req.BankCardNo)
withdrawal.BankName = lzUtils.StringToNullString(req.BankName)
// 银行卡提现时payee_account 可以存储银行卡号(便于查询),也可以留空
if req.PayeeAccount == "" {
withdrawal.PayeeAccount = req.BankCardNo
}
}
_, err := l.svcCtx.AgentWithdrawalModel.Insert(transCtx, session, withdrawal)
@@ -117,7 +201,7 @@ func (l *ApplyWithdrawalLogic) ApplyWithdrawal(req *types.ApplyWithdrawalReq) (r
}
withdrawalId = withdrawal.Id
// 8.3 创建扣税记录
// 11.3 创建扣税记录
taxRecord := &model.AgentWithdrawalTax{
AgentId: agent.Id,
WithdrawalId: withdrawalId,
@@ -167,8 +251,10 @@ func (l *ApplyWithdrawalLogic) calculateTax(ctx context.Context, agentId string,
}
// 查询本月已提现金额
// 注意FindAll 方法会自动添加 del_state = ? 条件,所以这里不需要手动添加
// 这里对 year_month 使用反引号包裹,避免与某些数据库版本/SQL 模式下的关键字冲突
builder := l.svcCtx.AgentWithdrawalTaxModel.SelectBuilder().
Where("agent_id = ? AND year_month = ? AND del_state = ?", agentId, yearMonth, globalkey.DelStateNo)
Where("agent_id = ? AND `year_month` = ?", agentId, yearMonth)
taxRecords, err := l.svcCtx.AgentWithdrawalTaxModel.FindAll(ctx, builder, "")
if err != nil {
return nil, errors.Wrapf(err, "查询月度提现记录失败")

View File

@@ -4,12 +4,13 @@ import (
"context"
"strings"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/logx"
"qnc-server/app/main/api/internal/svc"
"qnc-server/app/main/api/internal/types"
"qnc-server/app/main/model"
"qnc-server/common/xerr"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/logx"
)
type CheckFeatureWhitelistStatusLogic struct {

View File

@@ -3,13 +3,14 @@ package agent
import (
"context"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/logx"
"qnc-server/app/main/api/internal/svc"
"qnc-server/app/main/api/internal/types"
"qnc-server/app/main/model"
"qnc-server/common/ctxdata"
"qnc-server/common/xerr"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/logx"
)
type CheckOrderAgentLogic struct {

View File

@@ -4,15 +4,16 @@ import (
"context"
"fmt"
"github.com/google/uuid"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/core/stores/sqlx"
"qnc-server/app/main/api/internal/svc"
"qnc-server/app/main/api/internal/types"
"qnc-server/app/main/model"
"qnc-server/common/ctxdata"
"qnc-server/common/xerr"
"github.com/google/uuid"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/core/stores/sqlx"
)
type CreateWhitelistOrderLogic struct {

View File

@@ -0,0 +1,52 @@
// Code scaffolded by goctl. Safe to edit.
// goctl 1.9.2
package agent
import (
"context"
"encoding/base64"
"fmt"
"qnc-server/app/main/api/internal/svc"
"qnc-server/app/main/api/internal/types"
"qnc-server/common/xerr"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/logx"
)
type GenerateInvitePosterLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewGenerateInvitePosterLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GenerateInvitePosterLogic {
return &GenerateInvitePosterLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *GenerateInvitePosterLogic) GenerateInvitePoster(req *types.GenerateInvitePosterReq) (resp *types.GenerateInvitePosterResp, err error) {
if req.InviteLink == "" {
return nil, errors.Wrapf(xerr.NewErrMsg("邀请链接不能为空"), "")
}
// 调用ImageService生成海报
imageData, mimeType, err := l.svcCtx.ImageService.ProcessImageWithQRCode("invitation", req.InviteLink)
if err != nil {
l.Errorf("生成邀请海报失败: %v", err)
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成海报失败: %v", err)
}
// 将图片数据转换为base64编码的data URL
base64Data := base64.StdEncoding.EncodeToString(imageData)
posterUrl := fmt.Sprintf("data:%s;base64,%s", mimeType, base64Data)
return &types.GenerateInvitePosterResp{
PosterUrl: posterUrl,
}, nil
}

View File

@@ -4,15 +4,15 @@ import (
"context"
"database/sql"
"fmt"
"strconv"
"strings"
"time"
"qnc-server/app/main/model"
"qnc-server/common/ctxdata"
"qnc-server/common/globalkey"
"qnc-server/common/tool"
"qnc-server/common/xerr"
"qnc-server/pkg/lzkit/crypto"
"strconv"
"strings"
"time"
"github.com/pkg/errors"
@@ -124,7 +124,18 @@ func (l *GetInviteLinkLogic) createInviteShortLink(inviteCodeId string, inviteCo
// 先查询是否已存在短链
existingShortLink, err := l.svcCtx.AgentShortLinkModel.FindOneByInviteCodeIdTypeDelState(l.ctx, sql.NullString{String: inviteCodeId, Valid: inviteCodeId != ""}, 2, globalkey.DelStateNo)
if err == nil && existingShortLink != nil {
// 已存在短链,直接返回
// 已存在短链,检查 target_path 是否需要更新
if existingShortLink.TargetPath != targetPath {
// target_path 已变化,更新短链记录
oldTargetPath := existingShortLink.TargetPath
existingShortLink.TargetPath = targetPath
if updateErr := l.svcCtx.AgentShortLinkModel.UpdateWithVersion(l.ctx, nil, existingShortLink); updateErr != nil {
l.Errorf("更新短链 target_path 失败: %v", updateErr)
// 即使更新失败,也返回旧短链,避免影响用户体验
} else {
l.Infof("短链 target_path 已更新: shortCode=%s, old=%s, new=%s", existingShortLink.ShortCode, oldTargetPath, targetPath)
}
}
return fmt.Sprintf("%s/s/%s", promotionConfig.PromotionDomain, existingShortLink.ShortCode), nil
}
@@ -136,6 +147,18 @@ func (l *GetInviteLinkLogic) createInviteShortLink(inviteCodeId string, inviteCo
if inviteCodeId == "" && inviteCode != "" {
existingByCode, err2 := l.svcCtx.AgentShortLinkModel.FindOneByInviteCodeTypeDelState(l.ctx, sql.NullString{String: inviteCode, Valid: true}, 2, globalkey.DelStateNo)
if err2 == nil && existingByCode != nil {
// 已存在短链,检查 target_path 是否需要更新
if existingByCode.TargetPath != targetPath {
// target_path 已变化,更新短链记录
oldTargetPath := existingByCode.TargetPath
existingByCode.TargetPath = targetPath
if updateErr := l.svcCtx.AgentShortLinkModel.UpdateWithVersion(l.ctx, nil, existingByCode); updateErr != nil {
l.Errorf("更新短链 target_path 失败: %v", updateErr)
// 即使更新失败,也返回旧短链,避免影响用户体验
} else {
l.Infof("短链 target_path 已更新: shortCode=%s, old=%s, new=%s", existingByCode.ShortCode, oldTargetPath, targetPath)
}
}
return fmt.Sprintf("%s/s/%s", promotionConfig.PromotionDomain, existingByCode.ShortCode), nil
}
if err2 != nil && !errors.Is(err2, model.ErrNotFound) {

View File

@@ -0,0 +1,95 @@
package agent
import (
"context"
"qnc-server/app/main/model"
"qnc-server/common/ctxdata"
"qnc-server/common/xerr"
"github.com/pkg/errors"
"qnc-server/app/main/api/internal/svc"
"qnc-server/app/main/api/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type GetLastWithdrawalInfoLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewGetLastWithdrawalInfoLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetLastWithdrawalInfoLogic {
return &GetLastWithdrawalInfoLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *GetLastWithdrawalInfoLogic) GetLastWithdrawalInfo(req *types.GetLastWithdrawalInfoReq) (resp *types.GetLastWithdrawalInfoResp, err error) {
userID, err := ctxdata.GetUidFromCtx(l.ctx)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取用户信息失败, %v", err)
}
// 1. 获取代理信息
agent, err := l.svcCtx.AgentModel.FindOneByUserId(l.ctx, userID)
if err != nil {
if errors.Is(err, model.ErrNotFound) {
return nil, errors.Wrapf(xerr.NewErrMsg("您不是代理"), "")
}
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询代理信息失败, %v", err)
}
// 2. 验证提现方式
if req.WithdrawalType != 1 && req.WithdrawalType != 2 {
return nil, errors.Wrapf(xerr.NewErrMsg("提现方式无效"), "")
}
// 3. 查询该代理最近一次该类型的提现记录
// 注意FindAll 方法会自动添加 del_state = ? 条件,所以这里不需要手动添加
builder := l.svcCtx.AgentWithdrawalModel.SelectBuilder().
Where("agent_id = ? AND withdrawal_type = ?", agent.Id, req.WithdrawalType).
OrderBy("create_time DESC").
Limit(1)
withdrawals, err := l.svcCtx.AgentWithdrawalModel.FindAll(l.ctx, builder, "")
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询上次提现记录失败, %v", err)
}
// 4. 如果没有找到记录,返回空信息
if len(withdrawals) == 0 {
return &types.GetLastWithdrawalInfoResp{
WithdrawalType: req.WithdrawalType,
PayeeAccount: "",
PayeeName: "",
BankCardNo: "",
BankName: "",
}, nil
}
// 5. 组装响应
lastWithdrawal := withdrawals[0]
resp = &types.GetLastWithdrawalInfoResp{
WithdrawalType: lastWithdrawal.WithdrawalType,
PayeeAccount: lastWithdrawal.PayeeAccount,
PayeeName: lastWithdrawal.PayeeName,
BankCardNo: "",
BankName: "",
}
// 如果是银行卡提现,填充银行卡信息
if lastWithdrawal.WithdrawalType == 2 {
if lastWithdrawal.BankCardNo.Valid {
resp.BankCardNo = lastWithdrawal.BankCardNo.String
}
if lastWithdrawal.BankName.Valid {
resp.BankName = lastWithdrawal.BankName.String
}
}
return resp, nil
}

View File

@@ -3,11 +3,12 @@ package agent
import (
"context"
"github.com/zeromicro/go-zero/core/logx"
"qnc-server/app/main/api/internal/svc"
"qnc-server/app/main/api/internal/types"
"qnc-server/common/xerr"
"github.com/zeromicro/go-zero/core/logx"
"github.com/pkg/errors"
)

View File

@@ -3,12 +3,13 @@ package agent
import (
"context"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/logx"
"qnc-server/app/main/api/internal/svc"
"qnc-server/app/main/api/internal/types"
"qnc-server/common/ctxdata"
"qnc-server/common/xerr"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/logx"
)
type GetWhitelistListLogic struct {

View File

@@ -81,18 +81,31 @@ func (l *GetWithdrawalListLogic) GetWithdrawalList(req *types.GetWithdrawalListR
remark = withdrawal.Remark.String
}
list = append(list, types.WithdrawalItem{
Id: withdrawal.Id,
WithdrawalNo: withdrawal.WithdrawNo,
Amount: withdrawal.Amount,
TaxAmount: withdrawal.TaxAmount,
ActualAmount: withdrawal.ActualAmount,
Status: withdrawal.Status,
PayeeAccount: withdrawal.PayeeAccount,
PayeeName: withdrawal.PayeeName,
Remark: remark,
CreateTime: withdrawal.CreateTime.Format("2006-01-02 15:04:05"),
})
item := types.WithdrawalItem{
Id: withdrawal.Id,
WithdrawalNo: withdrawal.WithdrawNo,
WithdrawalType: withdrawal.WithdrawalType,
Amount: withdrawal.Amount,
TaxAmount: withdrawal.TaxAmount,
ActualAmount: withdrawal.ActualAmount,
Status: withdrawal.Status,
PayeeAccount: withdrawal.PayeeAccount,
PayeeName: withdrawal.PayeeName,
Remark: remark,
CreateTime: withdrawal.CreateTime.Format("2006-01-02 15:04:05"),
}
// 如果是银行卡提现,填充银行卡信息
if withdrawal.WithdrawalType == 2 {
if withdrawal.BankCardNo.Valid {
item.BankCardNo = withdrawal.BankCardNo.String
}
if withdrawal.BankName.Valid {
item.BankName = withdrawal.BankName.String
}
}
list = append(list, item)
}
return &types.GetWithdrawalListResp{

View File

@@ -4,12 +4,13 @@ import (
"context"
"encoding/json"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/logx"
"qnc-server/app/main/api/internal/svc"
"qnc-server/app/main/api/internal/types"
"qnc-server/common/ctxdata"
"qnc-server/common/xerr"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/logx"
)
type OfflineFeatureLogic struct {

View File

@@ -5,13 +5,13 @@ import (
"database/sql"
"fmt"
"os"
"strconv"
"time"
"qnc-server/app/main/model"
"qnc-server/common/ctxdata"
"qnc-server/common/globalkey"
"qnc-server/common/xerr"
"qnc-server/pkg/lzkit/crypto"
"strconv"
"time"
"github.com/google/uuid"
"github.com/pkg/errors"

View File

@@ -2,10 +2,11 @@ package auth
import (
"context"
"qnc-server/common/xerr"
"qnc-server/pkg/lzkit/crypto"
"fmt"
"math/rand"
"qnc-server/common/xerr"
"qnc-server/pkg/captcha"
"qnc-server/pkg/lzkit/crypto"
"time"
"github.com/pkg/errors"
@@ -34,13 +35,58 @@ func NewSendSmsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *SendSmsLo
}
}
func (l *SendSmsLogic) SendSms(req *types.SendSmsReq) error {
func (l *SendSmsLogic) SendSms(req *types.SendSmsReq, clientIP string, userAgent string) error {
secretKey := l.svcCtx.Config.Encrypt.SecretKey
encryptedMobile, err := crypto.EncryptMobile(req.Mobile, secretKey)
if err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "短信发送, 加密手机号失败: %v", err)
if err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "短信发送, 加密手机号失败: %v", err)
}
// 1. 滑块验证码校验(可选,支持微信环境跳过验证)
cfg := l.svcCtx.Config.Captcha
captchaResult := captcha.VerifyOptionalWithUserAgent(captcha.Config{
AccessKeyID: cfg.AccessKeyID,
AccessKeySecret: cfg.AccessKeySecret,
EndpointURL: cfg.EndpointURL,
SceneID: cfg.SceneID,
}, req.CaptchaVerifyParam, userAgent)
if captchaResult.VerifyErr != nil {
return captchaResult.VerifyErr
}
// 2. 防刷策略
if captchaResult.Skipped {
// 没有滑块验证码,使用更严格的限流策略
// 2.1 IP 限流:同一 IP 每小时最多发送 10 次
ipLimitKey := fmt.Sprintf("ip_limit:%s", clientIP)
ipCount, err := l.svcCtx.Redis.Incr(ipLimitKey)
if err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "短信发送, 读取IP限流缓存失败: %v", err)
}
if ipCount == 1 {
// 第一次访问,设置 1 小时过期
l.svcCtx.Redis.Expire(ipLimitKey, 3600)
}
if ipCount > 10 {
return errors.Wrapf(xerr.NewErrMsg("请求过于频繁,请稍后再试"), "短信发送, IP限流: %s, count: %d", clientIP, ipCount)
}
// 2.2 手机号限流:同一手机号每小时最多发送 5 次(无滑块时更严格)
hourLimitKey := fmt.Sprintf("hour_limit:%s:%s", req.ActionType, encryptedMobile)
hourCount, err := l.svcCtx.Redis.Incr(hourLimitKey)
if err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "短信发送, 读取小时限流缓存失败: %v", err)
}
if hourCount == 1 {
l.svcCtx.Redis.Expire(hourLimitKey, 3600)
}
if hourCount > 5 {
return errors.Wrapf(xerr.NewErrMsg("该手机号请求过于频繁,请稍后再试"), "短信发送, 手机号小时限流: %s, count: %d", encryptedMobile, hourCount)
}
}
// 检查手机号是否在一分钟内已发送过验证码
// 3. 检查手机号是否在一分钟内已发送过验证码(通用)
limitCodeKey := fmt.Sprintf("limit:%s:%s", req.ActionType, encryptedMobile)
exists, err := l.svcCtx.Redis.Exists(limitCodeKey)
if err != nil {
@@ -48,7 +94,6 @@ func (l *SendSmsLogic) SendSms(req *types.SendSmsReq) error {
}
if exists {
// 如果 Redis 中已经存在标记,说明在 1 分钟内请求过,返回错误
return errors.Wrapf(xerr.NewErrMsg("一分钟内不能重复发送验证码"), "短信发送, 手机号1分钟内重复请求发送验证码: %s", encryptedMobile)
}

View File

@@ -0,0 +1,37 @@
package captcha
import (
"context"
"qnc-server/app/main/api/internal/svc"
"qnc-server/app/main/api/internal/types"
"qnc-server/pkg/captcha"
"github.com/zeromicro/go-zero/core/logx"
)
type GetEncryptedSceneIdLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewGetEncryptedSceneIdLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetEncryptedSceneIdLogic {
return &GetEncryptedSceneIdLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *GetEncryptedSceneIdLogic) GetEncryptedSceneId() (*types.GetEncryptedSceneIdResp, error) {
cfg := l.svcCtx.Config.Captcha
encrypted, err := captcha.GenerateEncryptedSceneID(cfg.SceneID, cfg.EKey, 3600)
if err != nil {
l.Errorf("generate encrypted scene id error: %+v", err)
return nil, err
}
return &types.GetEncryptedSceneIdResp{
EncryptedSceneId: encrypted,
}, nil
}

View File

@@ -6,15 +6,15 @@ import (
"encoding/json"
"fmt"
"os"
"strconv"
"strings"
"time"
"qnc-server/app/main/api/internal/svc"
"qnc-server/app/main/api/internal/types"
"qnc-server/app/main/model"
"qnc-server/common/ctxdata"
"qnc-server/common/xerr"
"qnc-server/pkg/lzkit/lzUtils"
"strconv"
"strings"
"time"
"github.com/google/uuid"
"github.com/pkg/errors"
@@ -35,6 +35,9 @@ type PaymentTypeResp struct {
orderID string // 订单ID用于开发环境测试支付模式
}
// enableDevTestPayment 测试支付功能开关,设置为 true 以启用测试支付功能
const enableDevTestPayment = false
func NewPaymentLogic(ctx context.Context, svcCtx *svc.ServiceContext) *PaymentLogic {
return &PaymentLogic{
Logger: logx.WithContext(ctx),
@@ -50,8 +53,8 @@ func (l *PaymentLogic) Payment(req *types.PaymentReq) (resp *types.PaymentResp,
// 检查是否为开发环境的测试支付模式
env := os.Getenv("ENV")
isDevTestPayment := env == "development" && (req.PayMethod == "test" || req.PayMethod == "test_empty")
isEmptyReportMode := env == "development" && req.PayMethod == "test_empty"
isDevTestPayment := enableDevTestPayment && env == "development" && (req.PayMethod == "test" || req.PayMethod == "test_empty")
isEmptyReportMode := enableDevTestPayment && env == "development" && req.PayMethod == "test_empty"
l.svcCtx.OrderModel.Trans(l.ctx, func(ctx context.Context, session sqlx.Session) error {
switch req.PayType {
@@ -95,7 +98,7 @@ func (l *PaymentLogic) Payment(req *types.PaymentReq) (resp *types.PaymentResp,
// 正常支付流程
var createOrderErr error
if req.PayMethod == "wechat" {
prepayData, createOrderErr = l.svcCtx.WechatPayService.CreateWechatOrder(l.ctx, paymentTypeResp.amount, paymentTypeResp.description, paymentTypeResp.outTradeNo)
prepayData, createOrderErr = l.svcCtx.WechatPayService.CreateWechatOrder(l.ctx, paymentTypeResp.amount, paymentTypeResp.description, paymentTypeResp.outTradeNo, req.Code)
} else if req.PayMethod == "alipay" {
prepayData, createOrderErr = l.svcCtx.AlipayService.CreateAlipayOrder(l.ctx, paymentTypeResp.amount, paymentTypeResp.description, paymentTypeResp.outTradeNo)
} else if req.PayMethod == "appleiap" {

View File

@@ -118,7 +118,37 @@ func (l *WxMiniAuthLogic) GetSessionKey(code string) (*SessionKeyResp, error) {
// 检查微信返回的错误码
if sessionKeyResp.ErrCode != 0 {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR),
// 针对不同的微信错误码返回更明确的错误信息
var errMsg string
switch sessionKeyResp.ErrCode {
case 40029:
// code 无效(已使用、已过期或格式错误)
errMsg = "微信授权码无效或已过期,请重新打开小程序"
l.Errorf("微信code无效: errcode=%d, errmsg=%s, code前6位=%s",
sessionKeyResp.ErrCode, sessionKeyResp.ErrMsg,
func() string {
if len(code) > 6 {
return code[:6] + "..."
}
return code
}())
case 40013:
// 无效的 AppID
errMsg = "小程序配置错误,请联系管理员"
l.Errorf("微信AppID无效: errcode=%d, errmsg=%s", sessionKeyResp.ErrCode, sessionKeyResp.ErrMsg)
case 40125:
// 无效的 AppSecret
errMsg = "小程序配置错误,请联系管理员"
l.Errorf("微信AppSecret无效: errcode=%d, errmsg=%s", sessionKeyResp.ErrCode, sessionKeyResp.ErrMsg)
case 45011:
// 频率限制
errMsg = "请求过于频繁,请稍后再试"
l.Errorf("微信API频率限制: errcode=%d, errmsg=%s", sessionKeyResp.ErrCode, sessionKeyResp.ErrMsg)
default:
errMsg = fmt.Sprintf("微信授权失败: %s", sessionKeyResp.ErrMsg)
l.Errorf("微信接口返回错误: errcode=%d, errmsg=%s", sessionKeyResp.ErrCode, sessionKeyResp.ErrMsg)
}
return nil, errors.Wrapf(xerr.NewErrMsg(errMsg),
"微信接口返回错误: errcode=%d, errmsg=%s",
sessionKeyResp.ErrCode, sessionKeyResp.ErrMsg)
}