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

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"