f
This commit is contained in:
@@ -251,6 +251,7 @@ type (
|
||||
AgentId *int64 `form:"agent_id,optional"` // 代理ID(可选)
|
||||
RelationAgentId *int64 `form:"relation_agent_id,optional"` // 关联代理ID(可选)
|
||||
Type *string `form:"type,optional"` // 奖励类型(可选)
|
||||
Status *int64 `form:"status,optional"` // 状态(可选)
|
||||
}
|
||||
|
||||
// 代理奖励列表项
|
||||
@@ -260,6 +261,8 @@ type (
|
||||
RelationAgentId int64 `json:"relation_agent_id"` // 关联代理ID
|
||||
Amount float64 `json:"amount"` // 金额
|
||||
Type string `json:"type"` // 奖励类型
|
||||
Status int64 `json:"status"` // 状态:0=正常, 1=已取消
|
||||
Remark string `json:"remark"` // 备注
|
||||
CreateTime string `json:"create_time"` // 创建时间
|
||||
}
|
||||
|
||||
@@ -319,6 +322,7 @@ type (
|
||||
AgentId int64 `json:"agent_id"` // 代理ID
|
||||
DeductedAgentId int64 `json:"deducted_agent_id"` // 被扣代理ID
|
||||
Amount float64 `json:"amount"` // 金额
|
||||
RefundedAmount float64 `json:"refunded_amount"` // 已退款金额
|
||||
ProductName string `json:"product_name"` // 产品名
|
||||
Type string `json:"type"` // 类型(cost/pricing)
|
||||
Status int64 `json:"status"` // 状态
|
||||
|
||||
@@ -61,7 +61,6 @@ Applepay:
|
||||
LoadPrivateKeyPath: "etc/merchant/AuthKey_XXXXXXXXXXXX.p8"
|
||||
SystemConfig:
|
||||
ThreeVerify: true
|
||||
CommissionSafeMode: false # 佣金安全防御模式:true-冻结模式,false-直接结算模式
|
||||
WechatH5:
|
||||
AppID: "xxxx"
|
||||
AppSecret: "xxxx"
|
||||
|
||||
@@ -100,8 +100,7 @@ type YushanConfig struct {
|
||||
Url string
|
||||
}
|
||||
type SystemConfig struct {
|
||||
ThreeVerify bool // 是否开启三级实名认证
|
||||
CommissionSafeMode bool // 佣金安全防御模式:true-冻结模式(status=1,进入frozen_balance),false-直接结算(status=0,进入balance)
|
||||
ThreeVerify bool // 是否开启三级实名认证
|
||||
}
|
||||
type WechatH5Config struct {
|
||||
AppID string
|
||||
|
||||
@@ -36,6 +36,9 @@ func (l *AdminGetAgentRewardListLogic) AdminGetAgentRewardList(req *types.AdminG
|
||||
if req.Type != nil && *req.Type != "" {
|
||||
builder = builder.Where(squirrel.Eq{"type": *req.Type})
|
||||
}
|
||||
if req.Status != nil {
|
||||
builder = builder.Where(squirrel.Eq{"status": *req.Status})
|
||||
}
|
||||
list, total, err := l.svcCtx.AgentRewardsModel.FindPageListByPageWithTotal(l.ctx, builder, req.Page, req.PageSize, "create_time DESC")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -25,7 +25,7 @@ func NewAdminGetSystemConfigLogic(ctx context.Context, svcCtx *svc.ServiceContex
|
||||
|
||||
func (l *AdminGetSystemConfigLogic) AdminGetSystemConfig() (resp *types.AdminGetSystemConfigResp, err error) {
|
||||
resp = &types.AdminGetSystemConfigResp{
|
||||
CommissionSafeMode: l.svcCtx.Config.SystemConfig.CommissionSafeMode,
|
||||
CommissionSafeMode: l.svcCtx.AgentConfigModel.IsCommissionSafeMode(l.ctx),
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -24,9 +24,11 @@ func NewAdminUpdateSystemConfigLogic(ctx context.Context, svcCtx *svc.ServiceCon
|
||||
}
|
||||
|
||||
func (l *AdminUpdateSystemConfigLogic) AdminUpdateSystemConfig(req *types.AdminUpdateSystemConfigReq) (resp *types.AdminUpdateSystemConfigResp, err error) {
|
||||
// 更新佣金安全防御模式配置
|
||||
if req.CommissionSafeMode != nil {
|
||||
l.svcCtx.Config.SystemConfig.CommissionSafeMode = *req.CommissionSafeMode
|
||||
if err := l.svcCtx.AgentConfigModel.SetCommissionSafeMode(l.ctx, *req.CommissionSafeMode); err != nil {
|
||||
logx.Errorf("更新佣金安全防御模式失败: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
logx.Infof("更新系统配置:佣金安全防御模式设置为 %v", *req.CommissionSafeMode)
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
paylogic "bdrp-server/app/main/api/internal/logic/pay"
|
||||
"bdrp-server/app/main/api/internal/svc"
|
||||
"bdrp-server/app/main/api/internal/types"
|
||||
"bdrp-server/app/main/model"
|
||||
@@ -93,8 +92,8 @@ func (l *AdminRefundOrderLogic) handleAlipayRefund(order *model.Order, req *type
|
||||
}
|
||||
|
||||
// 退款成功后,按本次退款金额更新代理佣金状态并扣除钱包金额
|
||||
// 注意:refundAmount 为本次实际退款金额,可以是部分退款
|
||||
_ = paylogic.HandleCommissionAndWalletDeduction(l.ctx, l.svcCtx, nil, order, req.RefundAmount)
|
||||
// 使用 AgentService 中的共用退款扣款逻辑
|
||||
l.svcCtx.AgentService.HandleOrderRefundDeduction(l.ctx, nil, order, req.RefundAmount)
|
||||
|
||||
return &types.AdminRefundOrderResp{
|
||||
Status: model.OrderStatusRefunded,
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"bdrp-server/app/main/api/internal/service"
|
||||
"bdrp-server/app/main/model"
|
||||
"bdrp-server/common/ctxdata"
|
||||
"bdrp-server/common/xerr"
|
||||
"bdrp-server/pkg/lzkit/lzUtils"
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/jinzhu/copier"
|
||||
"github.com/pkg/errors"
|
||||
@@ -40,6 +42,9 @@ func (l *GetAgentMembershipProductConfigLogic) GetAgentMembershipProductConfig(r
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取会员用户报告配置,获取代理信息失败: %v", err)
|
||||
}
|
||||
if service.IsMembershipExpired(agentModel, time.Now()) {
|
||||
return nil, errors.Wrapf(xerr.NewErrMsg("会员已过期"), "获取会员用户报告配置,会员已过期: agent_id=%d", agentModel.Id)
|
||||
}
|
||||
if agentModel.LevelName == "" {
|
||||
agentModel.LevelName = model.AgentLeveNameNormal
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"bdrp-server/app/main/api/internal/service"
|
||||
"bdrp-server/app/main/model"
|
||||
"bdrp-server/common/ctxdata"
|
||||
"bdrp-server/common/xerr"
|
||||
"context"
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/zeromicro/go-zero/core/mr"
|
||||
@@ -104,6 +106,10 @@ func (l *GetAgentProductConfigLogic) GetAgentProductConfig() (resp *types.AgentP
|
||||
cancel(findAncestorAgentErr)
|
||||
return
|
||||
}
|
||||
if service.IsMembershipExpired(ancestorAgentModel, time.Now()) {
|
||||
writer.Write(&agentProductConfig)
|
||||
return
|
||||
}
|
||||
if ancestorAgentModel.LevelName == "" {
|
||||
ancestorAgentModel.LevelName = model.AgentLeveNameNormal
|
||||
}
|
||||
|
||||
@@ -175,6 +175,11 @@ func calculateActiveReward(rewards []*model.AgentRewards) types.ActiveReward {
|
||||
last30dStart := now.AddDate(0, 0, -30) // 近30天
|
||||
|
||||
for _, r := range rewards {
|
||||
// 跳过已取消的奖励(退款取消)
|
||||
if r.Status == 1 {
|
||||
continue
|
||||
}
|
||||
|
||||
createTime := r.CreateTime
|
||||
amount := r.Amount
|
||||
|
||||
@@ -219,11 +224,12 @@ func addToPeriods(res *types.ActiveReward, amount float64, today, last7d, last30
|
||||
|
||||
// 分类添加具体字段
|
||||
func addToData(data *types.ActiveRewardData, amount float64, t string) {
|
||||
// 所有类型都累加到总奖励
|
||||
data.NewActiveReward += amount
|
||||
|
||||
switch t {
|
||||
case "withdraw":
|
||||
data.SubWithdrawReward += amount
|
||||
case "new_active":
|
||||
data.NewActiveReward += amount
|
||||
case "upgrade":
|
||||
data.SubUpgradeReward += amount
|
||||
case "promotion":
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"bdrp-server/app/main/api/internal/service"
|
||||
"bdrp-server/app/main/model"
|
||||
"bdrp-server/common/ctxdata"
|
||||
"bdrp-server/common/xerr"
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
@@ -37,6 +39,9 @@ func (l *SaveAgentMembershipUserConfigLogic) SaveAgentMembershipUserConfig(req *
|
||||
if err != nil {
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "保存会员代理报告配置: %v", err)
|
||||
}
|
||||
if service.IsMembershipExpired(agentModel, time.Now()) {
|
||||
return errors.Wrapf(xerr.NewErrMsg("会员已过期"), "保存会员代理报告配置,会员已过期: agent_id=%d", agentModel.Id)
|
||||
}
|
||||
|
||||
var agentMembershipUserConfigModel *model.AgentMembershipUserConfig
|
||||
agentMembershipUserConfigModel, err = l.svcCtx.AgentMembershipUserConfigModel.FindOneByAgentIdProductId(l.ctx, agentModel.Id, req.ProductID)
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Masterminds/squirrel"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/wechatpay-apiv3/wechatpay-go/services/refunddomestic"
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
@@ -22,153 +21,6 @@ func roundMoney(v float64) float64 {
|
||||
return math.Round(v*100) / 100
|
||||
}
|
||||
|
||||
// HandleCommissionAndWalletDeduction 处理退款后的佣金状态更新和钱包金额扣除
|
||||
// refundAmount 为本次实际退款金额(单位:元),从代理侧总共需要承担的金额
|
||||
// 该函数会优先冲减当前订单相关的佣金(基于 RefundedAmount),不足部分再从钱包余额/冻结余额中扣除
|
||||
func HandleCommissionAndWalletDeduction(ctx context.Context, svcCtx *svc.ServiceContext, session sqlx.Session, order *model.Order, refundAmount float64) error {
|
||||
refundAmount = roundMoney(refundAmount)
|
||||
if refundAmount <= 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 查询当前订单关联的所有佣金记录(包括已结算和冻结),剔除已经完全退款的
|
||||
commissionBuilder := svcCtx.AgentCommissionModel.SelectBuilder()
|
||||
commissions, commissionsErr := svcCtx.AgentCommissionModel.FindAll(ctx, commissionBuilder.Where(squirrel.And{
|
||||
squirrel.Eq{"order_id": order.Id},
|
||||
squirrel.NotEq{"status": 2}, // 排除已全部退款的佣金
|
||||
}), "")
|
||||
if commissionsErr != nil {
|
||||
logx.Errorf("查询代理佣金失败,订单ID: %d, 错误: %v", order.Id, commissionsErr)
|
||||
return nil // 返回 nil,因为佣金更新失败不应影响退款流程
|
||||
}
|
||||
|
||||
if len(commissions) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 剩余需要由佣金 + 钱包共同承担的退款金额
|
||||
remainRefundAmount := refundAmount
|
||||
|
||||
// 记录每个代理本次需要从钱包扣除的金额,避免同一代理多条佣金时重复查钱包并产生多条流水
|
||||
type walletAdjust struct {
|
||||
agentId int64
|
||||
amount float64 // 需要从该代理钱包扣除的金额(正数)
|
||||
}
|
||||
walletAdjustMap := make(map[int64]*walletAdjust)
|
||||
|
||||
// 1. 先在佣金记录上做冲减:增加 RefundedAmount,必要时将状态置为已退款
|
||||
for _, commission := range commissions {
|
||||
available := roundMoney(commission.Amount - commission.RefundedAmount)
|
||||
if available <= 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if remainRefundAmount <= 0 {
|
||||
break
|
||||
}
|
||||
|
||||
// 当前这条佣金最多可冲减 available,本次实际冲减 currentRefund
|
||||
currentRefund := available
|
||||
if currentRefund > remainRefundAmount {
|
||||
currentRefund = remainRefundAmount
|
||||
}
|
||||
currentRefund = roundMoney(currentRefund)
|
||||
|
||||
// 更新佣金的已退款金额
|
||||
commission.RefundedAmount = roundMoney(commission.RefundedAmount + currentRefund)
|
||||
// 如果这条佣金已经被完全冲减,则标记为已退款
|
||||
if commission.RefundedAmount >= roundMoney(commission.Amount) {
|
||||
commission.Status = 2
|
||||
}
|
||||
|
||||
// 更新佣金状态到数据库
|
||||
var updateCommissionErr error
|
||||
if session != nil {
|
||||
updateCommissionErr = svcCtx.AgentCommissionModel.UpdateWithVersion(ctx, session, commission)
|
||||
} else {
|
||||
updateCommissionErr = svcCtx.AgentCommissionModel.UpdateWithVersion(ctx, nil, commission)
|
||||
}
|
||||
if updateCommissionErr != nil {
|
||||
logx.Errorf("更新代理佣金状态失败,佣金ID: %d, 订单ID: %d, 错误: %v", commission.Id, order.Id, updateCommissionErr)
|
||||
continue // 如果佣金状态更新失败,就不继续计入本次冲减
|
||||
}
|
||||
|
||||
// 记录该代理需要从钱包扣除的金额(可能后续还有其他佣金叠加)
|
||||
wa, ok := walletAdjustMap[commission.AgentId]
|
||||
if !ok {
|
||||
wa = &walletAdjust{agentId: commission.AgentId}
|
||||
walletAdjustMap[commission.AgentId] = wa
|
||||
}
|
||||
wa.amount = roundMoney(wa.amount + currentRefund)
|
||||
|
||||
remainRefundAmount = roundMoney(remainRefundAmount - currentRefund)
|
||||
}
|
||||
|
||||
// 2. 再按代理维度,从钱包(冻结余额/可用余额)中扣除对应金额
|
||||
for _, wa := range walletAdjustMap {
|
||||
if wa.amount <= 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// 处理用户钱包的金额扣除
|
||||
wallet, err := svcCtx.AgentWalletModel.FindOneByAgentId(ctx, wa.agentId)
|
||||
if err != nil {
|
||||
logx.Errorf("查询代理钱包失败,代理ID: %d, 错误: %v", wa.agentId, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// 记录变动前的余额
|
||||
balanceBefore := wallet.Balance
|
||||
frozenBalanceBefore := wallet.FrozenBalance
|
||||
|
||||
// 优先从冻结余额中扣除(与原先“冻结佣金优先使用冻结余额”的设计一致)
|
||||
deduct := roundMoney(wa.amount)
|
||||
if wallet.FrozenBalance >= deduct {
|
||||
wallet.FrozenBalance = roundMoney(wallet.FrozenBalance - deduct)
|
||||
} else {
|
||||
remaining := roundMoney(deduct - wallet.FrozenBalance)
|
||||
wallet.FrozenBalance = 0
|
||||
// 可用余额可以为负数,由业务承担风险
|
||||
wallet.Balance = roundMoney(wallet.Balance - remaining)
|
||||
}
|
||||
|
||||
// 变动后余额和冻结余额
|
||||
balanceAfter := roundMoney(wallet.Balance)
|
||||
frozenBalanceAfter := roundMoney(wallet.FrozenBalance)
|
||||
// 更新钱包
|
||||
var updateWalletErr error
|
||||
if session != nil {
|
||||
updateWalletErr = svcCtx.AgentWalletModel.UpdateWithVersion(ctx, session, wallet)
|
||||
} else {
|
||||
updateWalletErr = svcCtx.AgentWalletModel.UpdateWithVersion(ctx, nil, wallet)
|
||||
}
|
||||
if updateWalletErr != nil {
|
||||
logx.Errorf("更新代理钱包失败,代理ID: %d, 错误: %v", wa.agentId, updateWalletErr)
|
||||
continue
|
||||
}
|
||||
// 创建钱包交易流水记录(退款)
|
||||
transErr := svcCtx.AgentService.CreateWalletTransaction(
|
||||
ctx,
|
||||
session,
|
||||
wa.agentId,
|
||||
model.WalletTransactionTypeRefund,
|
||||
roundMoney(-wa.amount*-1), // 钱包流水金额为负数
|
||||
balanceBefore,
|
||||
balanceAfter,
|
||||
frozenBalanceBefore,
|
||||
frozenBalanceAfter,
|
||||
order.OrderNo,
|
||||
0, // 这里不强绑到某一条具体佣金记录,按订单维度记录
|
||||
"订单退款,佣金已扣除",
|
||||
)
|
||||
if transErr != nil {
|
||||
logx.Errorf("创建代理钱包流水记录失败,代理ID: %d, 错误: %v", wa.agentId, transErr)
|
||||
continue
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type WechatPayRefundCallbackLogic struct {
|
||||
logx.Logger
|
||||
ctx context.Context
|
||||
@@ -227,8 +79,8 @@ func (l *WechatPayRefundCallbackLogic) handleQueryOrderRefund(orderNo string, st
|
||||
return errors.Wrapf(err, "更新查询订单状态失败: %s", orderNo)
|
||||
}
|
||||
|
||||
// 退款成功时,按本次实际退款金额更新代理佣金状态并扣除钱包金额
|
||||
_ = HandleCommissionAndWalletDeduction(ctx, l.svcCtx, session, order, refundAmountYuan)
|
||||
// 退款成功时,按本次实际退款金额执行共用扣款流程
|
||||
l.svcCtx.AgentService.HandleOrderRefundDeduction(ctx, session, order, refundAmountYuan)
|
||||
}
|
||||
|
||||
// 查找最新的pending状态的退款记录
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"bdrp-server/app/main/api/internal/service"
|
||||
"bdrp-server/app/main/model"
|
||||
jwtx "bdrp-server/common/jwt"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
)
|
||||
|
||||
const (
|
||||
HeaderMembershipExpired = "X-Membership-Expired"
|
||||
)
|
||||
|
||||
// MembershipExpiredInterceptor 检测代理会员是否过期,过期则写入响应头
|
||||
// 依赖 ctx 中的 claims(由 AuthInterceptorMiddleware 注入)和 svcCtx 中的 AgentModel
|
||||
type MembershipExpiredInterceptor struct {
|
||||
AgentModel model.AgentModel
|
||||
}
|
||||
|
||||
func NewMembershipExpiredInterceptor(agentModel model.AgentModel) *MembershipExpiredInterceptor {
|
||||
return &MembershipExpiredInterceptor{
|
||||
AgentModel: agentModel,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MembershipExpiredInterceptor) Handle(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
// 先执行业务逻辑
|
||||
next(w, r)
|
||||
|
||||
// 业务完成后,尝试检测会员过期状态
|
||||
claims, err := getClaimsFromRequest(r)
|
||||
if err != nil || claims == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 只检查正式用户
|
||||
if claims.UserType != model.UserTypeNormal {
|
||||
return
|
||||
}
|
||||
|
||||
agent, err := m.AgentModel.FindOneByUserId(r.Context(), claims.UserId)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if service.IsMembershipExpired(agent, time.Now()) {
|
||||
w.Header().Set(HeaderMembershipExpired, "true")
|
||||
logx.Infof("检测到代理会员已过期,写入响应头,代理ID: %d, 用户ID: %d", agent.Id, claims.UserId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getClaimsFromRequest(r *http.Request) (*jwtx.JwtClaims, error) {
|
||||
value := r.Context().Value(jwtx.ExtraKey)
|
||||
if value == nil {
|
||||
return nil, nil
|
||||
}
|
||||
if claims, ok := value.(*jwtx.JwtClaims); ok {
|
||||
return claims, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
49
app/main/api/internal/queue/agentMembershipExpireHandle.go
Normal file
49
app/main/api/internal/queue/agentMembershipExpireHandle.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package queue
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"bdrp-server/app/main/api/internal/service"
|
||||
"bdrp-server/app/main/api/internal/svc"
|
||||
"bdrp-server/app/main/api/internal/types"
|
||||
|
||||
"github.com/hibiken/asynq"
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
)
|
||||
|
||||
type AgentMembershipExpireHandleHandler struct {
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
func NewAgentMembershipExpireHandleHandler(svcCtx *svc.ServiceContext) *AgentMembershipExpireHandleHandler {
|
||||
return &AgentMembershipExpireHandleHandler{
|
||||
svcCtx: svcCtx,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *AgentMembershipExpireHandleHandler) ProcessTask(ctx context.Context, t *asynq.Task) error {
|
||||
var payload types.MsgAgentMembershipExpireHandlePayload
|
||||
if err := json.Unmarshal(t.Payload(), &payload); err != nil {
|
||||
logx.Errorf("解析会员到期处理任务payload失败: %v", err)
|
||||
return err
|
||||
}
|
||||
if payload.AgentID <= 0 {
|
||||
return fmt.Errorf("无效代理ID: %d", payload.AgentID)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
downgraded, err := service.DowngradeExpiredMembership(ctx, l.svcCtx.AgentModel, payload.AgentID, now)
|
||||
if err != nil {
|
||||
logx.Errorf("会员到期处理失败,代理ID: %d, 错误: %v", payload.AgentID, err)
|
||||
return err
|
||||
}
|
||||
if downgraded {
|
||||
logx.Infof("会员到期处理成功,代理ID: %d 已降级为普通代理", payload.AgentID)
|
||||
} else {
|
||||
logx.Infof("会员到期处理跳过,代理ID: %d 当前无需降级", payload.AgentID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
68
app/main/api/internal/queue/agentMembershipExpireScan.go
Normal file
68
app/main/api/internal/queue/agentMembershipExpireScan.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package queue
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"bdrp-server/app/main/api/internal/service"
|
||||
"bdrp-server/app/main/api/internal/svc"
|
||||
|
||||
"github.com/hibiken/asynq"
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
)
|
||||
|
||||
type AgentMembershipExpireScanHandler struct {
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
func NewAgentMembershipExpireScanHandler(svcCtx *svc.ServiceContext) *AgentMembershipExpireScanHandler {
|
||||
return &AgentMembershipExpireScanHandler{
|
||||
svcCtx: svcCtx,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *AgentMembershipExpireScanHandler) ProcessTask(ctx context.Context, t *asynq.Task) error {
|
||||
now := time.Now()
|
||||
dayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
||||
dayEnd := dayStart.Add(24*time.Hour - time.Second)
|
||||
|
||||
// 1. 扫描当天将到期会员,并为每个会员安排准点处理任务
|
||||
todayAgents, err := service.ListTodayWillExpireAgents(ctx, l.svcCtx.AgentModel, dayStart, dayEnd)
|
||||
if err != nil {
|
||||
logx.Errorf("扫描当天到期会员失败: %v", err)
|
||||
return err
|
||||
}
|
||||
for _, agent := range todayAgents {
|
||||
processAt := agent.MembershipExpiryTime.Time
|
||||
if processAt.Before(now) {
|
||||
processAt = now
|
||||
}
|
||||
if l.svcCtx.AsynqService != nil {
|
||||
if sendErr := l.svcCtx.AsynqService.SendAgentMembershipExpireHandleTask(agent.Id, processAt); sendErr != nil {
|
||||
logx.Errorf("安排会员到期处理任务失败,代理ID: %d, 错误: %v", agent.Id, sendErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 补偿处理:扫描已过期但尚未降级的会员并立即降级
|
||||
expiredAgents, err := service.ListExpiredUnprocessedAgents(ctx, l.svcCtx.AgentModel, now)
|
||||
if err != nil {
|
||||
logx.Errorf("扫描已过期未处理会员失败: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
downgradedCount := 0
|
||||
for _, agent := range expiredAgents {
|
||||
downgraded, degradeErr := service.DowngradeExpiredMembership(ctx, l.svcCtx.AgentModel, agent.Id, now)
|
||||
if degradeErr != nil {
|
||||
logx.Errorf("补偿降级失败,代理ID: %d, 错误: %v", agent.Id, degradeErr)
|
||||
continue
|
||||
}
|
||||
if downgraded {
|
||||
downgradedCount++
|
||||
}
|
||||
}
|
||||
|
||||
logx.Infof("会员到期扫描完成,当天到期会员: %d, 补偿降级数量: %d", len(todayAgents), downgradedCount)
|
||||
return nil
|
||||
}
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
paylogic "bdrp-server/app/main/api/internal/logic/pay"
|
||||
"bdrp-server/app/main/api/internal/service"
|
||||
"bdrp-server/app/main/api/internal/svc"
|
||||
"bdrp-server/app/main/api/internal/types"
|
||||
@@ -283,8 +282,8 @@ func (l *PaySuccessNotifyUserHandler) handleError(ctx context.Context, err error
|
||||
return fmt.Errorf("更新订单状态失败: %v", updateOrderErr)
|
||||
}
|
||||
|
||||
// 使用公共函数按本次退款金额处理佣金和钱包扣除
|
||||
_ = paylogic.HandleCommissionAndWalletDeduction(ctx, l.svcCtx, nil, order, order.Amount)
|
||||
// 使用 AgentService 中的共用退款扣款逻辑
|
||||
l.svcCtx.AgentService.HandleOrderRefundDeduction(ctx, nil, order, order.Amount)
|
||||
|
||||
return asynq.SkipRetry
|
||||
} else {
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
package queue
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"bdrp-server/app/main/api/internal/svc"
|
||||
"bdrp-server/app/main/api/internal/types"
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/hibiken/asynq"
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
)
|
||||
|
||||
type CronJob struct {
|
||||
@@ -14,6 +16,8 @@ type CronJob struct {
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
const AgentMembershipExpireScanTaskTime = "5 0 * * *"
|
||||
|
||||
func NewCronJob(ctx context.Context, svcCtx *svc.ServiceContext) *CronJob {
|
||||
return &CronJob{
|
||||
ctx: ctx,
|
||||
@@ -31,6 +35,29 @@ func (l *CronJob) Register() *asynq.ServeMux {
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("定时任务注册失败:%v", err))
|
||||
}
|
||||
|
||||
// 注册会员到期扫描任务(每天凌晨执行)
|
||||
expireScanTask := asynq.NewTask(types.MsgAgentMembershipExpireScan, nil, nil)
|
||||
_, err = scheduler.Register(AgentMembershipExpireScanTaskTime, expireScanTask)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("会员到期扫描任务注册失败:%v", err))
|
||||
}
|
||||
|
||||
// 启动补偿:服务启动后立即触发一次扫描任务(按日期去重)
|
||||
client := asynq.NewClient(redisClientOpt)
|
||||
startupTaskID := fmt.Sprintf("agent_membership_expire_scan_startup_%s", time.Now().Format("20060102"))
|
||||
_, enqueueErr := client.Enqueue(
|
||||
asynq.NewTask(types.MsgAgentMembershipExpireScan, nil),
|
||||
asynq.MaxRetry(1),
|
||||
asynq.TaskID(startupTaskID),
|
||||
)
|
||||
if enqueueErr != nil {
|
||||
logx.Errorf("启动补偿扫描任务入队失败: %v", enqueueErr)
|
||||
}
|
||||
if closeErr := client.Close(); closeErr != nil {
|
||||
logx.Errorf("关闭启动补偿任务客户端失败: %v", closeErr)
|
||||
}
|
||||
|
||||
scheduler.Start()
|
||||
fmt.Println("定时任务启动!!!")
|
||||
|
||||
@@ -38,6 +65,8 @@ func (l *CronJob) Register() *asynq.ServeMux {
|
||||
mux.Handle(types.MsgPaySuccessQuery, NewPaySuccessNotifyUserHandler(l.svcCtx))
|
||||
mux.Handle(types.MsgCleanQueryData, NewCleanQueryDataHandler(l.svcCtx))
|
||||
mux.Handle(types.MsgUnfreezeCommission, NewUnfreezeCommissionHandler(l.svcCtx))
|
||||
mux.Handle(types.MsgAgentMembershipExpireScan, NewAgentMembershipExpireScanHandler(l.svcCtx))
|
||||
mux.Handle(types.MsgAgentMembershipExpireHandle, NewAgentMembershipExpireHandleHandler(l.svcCtx))
|
||||
|
||||
return mux
|
||||
}
|
||||
|
||||
70
app/main/api/internal/service/agentMembershipService.go
Normal file
70
app/main/api/internal/service/agentMembershipService.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"time"
|
||||
|
||||
"bdrp-server/app/main/model"
|
||||
"bdrp-server/common/globalkey"
|
||||
)
|
||||
|
||||
// IsMembershipExpired 判断代理会员是否已过期(仅VIP/SVIP会被判定)
|
||||
func IsMembershipExpired(agent *model.Agent, now time.Time) bool {
|
||||
if agent == nil {
|
||||
return false
|
||||
}
|
||||
if agent.LevelName != model.AgentLeveNameVIP && agent.LevelName != model.AgentLeveNameSVIP {
|
||||
return false
|
||||
}
|
||||
if !agent.MembershipExpiryTime.Valid {
|
||||
return true
|
||||
}
|
||||
return !agent.MembershipExpiryTime.Time.After(now)
|
||||
}
|
||||
|
||||
// DowngradeExpiredMembership 将已过期会员降级为普通代理,返回是否发生了降级
|
||||
func DowngradeExpiredMembership(ctx context.Context, agentModel model.AgentModel, agentID int64, now time.Time) (bool, error) {
|
||||
agent, err := agentModel.FindOne(ctx, agentID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if !IsMembershipExpired(agent, now) {
|
||||
return false, nil
|
||||
}
|
||||
agent.LevelName = model.AgentLeveNameNormal
|
||||
ClearMembershipOnDowngrade(agent)
|
||||
agent.UpdateTime = now
|
||||
if err = agentModel.UpdateWithVersion(ctx, nil, agent); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// ListTodayWillExpireAgents 查询当天将到期的VIP/SVIP代理
|
||||
func ListTodayWillExpireAgents(ctx context.Context, agentModel model.AgentModel, start, end time.Time) ([]*model.Agent, error) {
|
||||
builder := agentModel.SelectBuilder().
|
||||
Where("level_name IN (?, ?)", model.AgentLeveNameVIP, model.AgentLeveNameSVIP).
|
||||
Where("membership_expiry_time IS NOT NULL").
|
||||
Where("membership_expiry_time >= ?", start).
|
||||
Where("membership_expiry_time <= ?", end).
|
||||
Where("del_state = ?", globalkey.DelStateNo)
|
||||
|
||||
return agentModel.FindAll(ctx, builder, "membership_expiry_time ASC")
|
||||
}
|
||||
|
||||
// ListExpiredUnprocessedAgents 查询已过期但仍为VIP/SVIP的代理
|
||||
func ListExpiredUnprocessedAgents(ctx context.Context, agentModel model.AgentModel, now time.Time) ([]*model.Agent, error) {
|
||||
builder := agentModel.SelectBuilder().
|
||||
Where("level_name IN (?, ?)", model.AgentLeveNameVIP, model.AgentLeveNameSVIP).
|
||||
Where("membership_expiry_time IS NOT NULL").
|
||||
Where("membership_expiry_time <= ?", now).
|
||||
Where("del_state = ?", globalkey.DelStateNo)
|
||||
|
||||
return agentModel.FindAll(ctx, builder, "membership_expiry_time ASC")
|
||||
}
|
||||
|
||||
// ClearMembershipOnDowngrade 降级时清理会员有效期,避免脏状态
|
||||
func ClearMembershipOnDowngrade(agent *model.Agent) {
|
||||
agent.MembershipExpiryTime = sql.NullTime{}
|
||||
}
|
||||
@@ -1,14 +1,17 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"bdrp-server/app/main/api/internal/config"
|
||||
"bdrp-server/app/main/model"
|
||||
"bdrp-server/common/globalkey"
|
||||
"bdrp-server/pkg/lzkit/lzUtils"
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"github.com/Masterminds/squirrel"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
"github.com/zeromicro/go-zero/core/stores/sqlx"
|
||||
@@ -34,6 +37,7 @@ type AgentService struct {
|
||||
AgentActiveStatModel model.AgentActiveStatModel
|
||||
AgentWithdrawalModel model.AgentWithdrawalModel
|
||||
AgentWalletTransactionModel model.AgentWalletTransactionModel
|
||||
AgentConfigModel model.AgentConfigModel
|
||||
AsynqService *AsynqService
|
||||
}
|
||||
|
||||
@@ -44,7 +48,7 @@ func NewAgentService(c config.Config, orderModel model.OrderModel, agentModel mo
|
||||
agentMembershipRechargeOrderModel model.AgentMembershipRechargeOrderModel,
|
||||
agentMembershipUserConfigModel model.AgentMembershipUserConfigModel,
|
||||
agentProductConfigModel model.AgentProductConfigModel, agentPlatformDeductionModel model.AgentPlatformDeductionModel,
|
||||
agentActiveStatModel model.AgentActiveStatModel, agentWithdrawalModel model.AgentWithdrawalModel, agentWalletTransactionModel model.AgentWalletTransactionModel, asynqService *AsynqService) *AgentService {
|
||||
agentActiveStatModel model.AgentActiveStatModel, agentWithdrawalModel model.AgentWithdrawalModel, agentWalletTransactionModel model.AgentWalletTransactionModel, agentConfigModel model.AgentConfigModel, asynqService *AsynqService) *AgentService {
|
||||
|
||||
return &AgentService{
|
||||
config: c,
|
||||
@@ -66,6 +70,7 @@ func NewAgentService(c config.Config, orderModel model.OrderModel, agentModel mo
|
||||
AgentActiveStatModel: agentActiveStatModel,
|
||||
AgentWithdrawalModel: agentWithdrawalModel,
|
||||
AgentWalletTransactionModel: agentWalletTransactionModel,
|
||||
AgentConfigModel: agentConfigModel,
|
||||
AsynqService: asynqService,
|
||||
}
|
||||
}
|
||||
@@ -113,6 +118,9 @@ func (l *AgentService) AgentProcess(ctx context.Context, order *model.Order) err
|
||||
return findAgentModelErr
|
||||
}
|
||||
if AncestorModel != nil {
|
||||
if IsMembershipExpired(AncestorModel, time.Now()) {
|
||||
AncestorModel.LevelName = model.AgentLeveNameNormal
|
||||
}
|
||||
if AncestorModel.LevelName == "" {
|
||||
AncestorModel.LevelName = model.AgentLeveNameNormal
|
||||
}
|
||||
@@ -155,7 +163,7 @@ func (l *AgentService) AgentProcess(ctx context.Context, order *model.Order) err
|
||||
|
||||
// 根据安全防御模式配置决定佣金处理方式
|
||||
var commissionStatus int64
|
||||
if l.config.SystemConfig.CommissionSafeMode {
|
||||
if l.AgentConfigModel.IsCommissionSafeMode(ctx) {
|
||||
// 安全防御模式:佣金冻结在frozen_balance中
|
||||
ancestorWallet.FrozenBalance += ancestorCommissionAmount
|
||||
commissionStatus = 1 // 冻结状态
|
||||
@@ -227,7 +235,7 @@ func (l *AgentService) AgentProcess(ctx context.Context, order *model.Order) err
|
||||
|
||||
// 在事务提交后,仅在安全防御模式下触发解冻任务
|
||||
// 注意:这里发送的是任务,实际解冻将在指定时间后由队列处理
|
||||
if l.AsynqService != nil && l.config.SystemConfig.CommissionSafeMode {
|
||||
if l.AsynqService != nil && l.AgentConfigModel.IsCommissionSafeMode(ctx) {
|
||||
// 仅在安全防御模式下,才需要发送解冻任务
|
||||
// 获取刚创建的佣金记录ID
|
||||
// 由于我们需要佣金记录ID来触发解冻任务,但事务中无法获取,我们可以在事务后查询
|
||||
@@ -275,7 +283,7 @@ func (l *AgentService) AgentCommission(ctx context.Context, agentID int64, order
|
||||
frozenBalanceBefore := agentWalletModel.FrozenBalance
|
||||
|
||||
// 根据安全防御模式配置决定佣金状态和钱包操作
|
||||
if l.config.SystemConfig.CommissionSafeMode {
|
||||
if l.AgentConfigModel.IsCommissionSafeMode(ctx) {
|
||||
// 安全防御模式:佣金冻结在frozen_balance中
|
||||
agentWalletModel.FrozenBalance += finalCommission
|
||||
} else {
|
||||
@@ -286,7 +294,7 @@ func (l *AgentService) AgentCommission(ctx context.Context, agentID int64, order
|
||||
|
||||
// 根据安全防御模式配置决定佣金状态
|
||||
commissionStatus := int64(1) // 默认为冻结状态
|
||||
if !l.config.SystemConfig.CommissionSafeMode {
|
||||
if !l.AgentConfigModel.IsCommissionSafeMode(ctx) {
|
||||
commissionStatus = 0 // 非安全模式直接设置为已结算
|
||||
}
|
||||
|
||||
@@ -343,6 +351,9 @@ func (l *AgentService) AncestorCommission(ctx context.Context, descendantId int6
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if IsMembershipExpired(agentModel, time.Now()) {
|
||||
agentModel.LevelName = model.AgentLeveNameNormal
|
||||
}
|
||||
if agentModel.LevelName == "" {
|
||||
agentModel.LevelName = model.AgentLeveNameNormal
|
||||
}
|
||||
@@ -415,6 +426,17 @@ func (l *AgentService) PlatformPricing(ctx context.Context, agentID int64, order
|
||||
|
||||
// CommissionCost 上级底价成本
|
||||
func (l *AgentService) CommissionCost(ctx context.Context, descendantId int64, AncestorId int64, agentMembershipConfigModel *model.AgentMembershipConfig, productID int64, orderId int64, session sqlx.Session) (float64, error) {
|
||||
ancestorModel, findAncestorErr := l.AgentModel.FindOne(ctx, AncestorId)
|
||||
if findAncestorErr != nil {
|
||||
if errors.Is(findAncestorErr, model.ErrNotFound) {
|
||||
return 0, nil
|
||||
}
|
||||
return 0, findAncestorErr
|
||||
}
|
||||
if IsMembershipExpired(ancestorModel, time.Now()) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
if agentMembershipConfigModel.PriceIncreaseAmount.Valid {
|
||||
// 拥有则查看该上级设定的成本
|
||||
agentMembershipUserConfigModel, findAgentMembershipUserConfigModelErr := l.AgentMembershipUserConfigModel.FindOneByAgentIdProductId(ctx, AncestorId, productID)
|
||||
@@ -449,6 +471,17 @@ func (l *AgentService) CommissionCost(ctx context.Context, descendantId int64, A
|
||||
|
||||
// CommissionPricing 上级提价成本
|
||||
func (l *AgentService) CommissionPricing(ctx context.Context, descendantId int64, AncestorId int64, agentMembershipConfigModel *model.AgentMembershipConfig, productID int64, pricing float64, orderId int64, session sqlx.Session) (float64, error) {
|
||||
ancestorModel, findAncestorErr := l.AgentModel.FindOne(ctx, AncestorId)
|
||||
if findAncestorErr != nil {
|
||||
if errors.Is(findAncestorErr, model.ErrNotFound) {
|
||||
return 0, nil
|
||||
}
|
||||
return 0, findAncestorErr
|
||||
}
|
||||
if IsMembershipExpired(ancestorModel, time.Now()) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
//看上级代理等级否有拥有定价标准收益功能
|
||||
if agentMembershipConfigModel.PriceIncreaseMax.Valid && agentMembershipConfigModel.PriceRatio.Valid {
|
||||
// 拥有则查看该上级设定的成本
|
||||
@@ -517,6 +550,9 @@ func (l *AgentService) GiveUpgradeReward(ctx context.Context, agentID int64, old
|
||||
}
|
||||
|
||||
// 获取上级代理的等级配置
|
||||
if IsMembershipExpired(ancestorModel, time.Now()) {
|
||||
ancestorModel.LevelName = model.AgentLeveNameNormal
|
||||
}
|
||||
if ancestorModel.LevelName == "" {
|
||||
ancestorModel.LevelName = model.AgentLeveNameNormal
|
||||
}
|
||||
@@ -661,6 +697,9 @@ func (l *AgentService) GiveWithdrawReward(ctx context.Context, agentID int64, wi
|
||||
}
|
||||
|
||||
// 获取上级代理的等级配置
|
||||
if IsMembershipExpired(ancestorModel, time.Now()) {
|
||||
ancestorModel.LevelName = model.AgentLeveNameNormal
|
||||
}
|
||||
if ancestorModel.LevelName == "" {
|
||||
ancestorModel.LevelName = model.AgentLeveNameNormal
|
||||
}
|
||||
@@ -848,3 +887,211 @@ func (l *AgentService) CreateWalletTransaction(ctx context.Context, session sqlx
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// HandleOrderRefundDeduction 处理订单退款后的佣金扣款流程
|
||||
// refundAmount: 本次实际退款金额
|
||||
// 扣款顺序:
|
||||
// 1. 扣减推广代理佣金(先扣冻结,再扣余额)
|
||||
// 2. 扣减上级抽佣
|
||||
// 3. 取消上级推广奖励(无论退多少都取消)
|
||||
// 4. 不足部分继续从推广代理钱包扣
|
||||
// 平台抽佣不受影响
|
||||
func (l *AgentService) HandleOrderRefundDeduction(ctx context.Context, session sqlx.Session, order *model.Order, refundAmount float64) {
|
||||
refundAmount = roundRefundMoney(refundAmount)
|
||||
if refundAmount <= 0 {
|
||||
return
|
||||
}
|
||||
|
||||
remainRefundAmount := refundAmount
|
||||
|
||||
// 查找订单关联的代理订单,获取推广人代理ID
|
||||
agentOrder, err := l.AgentOrderModel.FindOneByOrderId(ctx, order.Id)
|
||||
if err != nil || agentOrder == nil {
|
||||
logx.Errorf("退款扣款:查询代理订单失败,订单ID: %d, 错误: %v", order.Id, err)
|
||||
return
|
||||
}
|
||||
promoterAgentId := agentOrder.AgentId
|
||||
|
||||
// 查找上级代理ID
|
||||
var ancestorAgentId int64 = 0
|
||||
agentClosure, closureErr := l.AgentClosureModel.FindOneByDescendantIdDepth(ctx, promoterAgentId, 1)
|
||||
if closureErr == nil && agentClosure != nil {
|
||||
ancestorAgentId = agentClosure.AncestorId
|
||||
}
|
||||
|
||||
// 第1步:扣减推广代理佣金
|
||||
promoterCommissions, _ := l.AgentCommissionModel.FindAll(ctx,
|
||||
l.AgentCommissionModel.SelectBuilder().Where(squirrel.And{
|
||||
squirrel.Eq{"order_id": order.Id},
|
||||
squirrel.Eq{"agent_id": promoterAgentId},
|
||||
squirrel.NotEq{"status": 2},
|
||||
}), "")
|
||||
|
||||
for _, commission := range promoterCommissions {
|
||||
if remainRefundAmount <= 0 {
|
||||
break
|
||||
}
|
||||
available := roundRefundMoney(commission.Amount - commission.RefundedAmount)
|
||||
if available <= 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
currentRefund := available
|
||||
if currentRefund > remainRefundAmount {
|
||||
currentRefund = remainRefundAmount
|
||||
}
|
||||
currentRefund = roundRefundMoney(currentRefund)
|
||||
|
||||
commission.RefundedAmount = roundRefundMoney(commission.RefundedAmount + currentRefund)
|
||||
if commission.RefundedAmount >= roundRefundMoney(commission.Amount) {
|
||||
commission.Status = 2
|
||||
}
|
||||
|
||||
if err := l.AgentCommissionModel.UpdateWithVersion(ctx, session, commission); err != nil {
|
||||
logx.Errorf("退款扣款:更新推广佣金失败,佣金ID: %d, 错误: %v", commission.Id, err)
|
||||
continue
|
||||
}
|
||||
|
||||
deductFromAgentWallet(ctx, l, session, promoterAgentId, currentRefund,
|
||||
fmt.Sprintf("订单退款,推广佣金扣除,订单号: %s", order.OrderNo))
|
||||
|
||||
remainRefundAmount = roundRefundMoney(remainRefundAmount - currentRefund)
|
||||
}
|
||||
|
||||
// 第2步:扣减上级抽佣
|
||||
if ancestorAgentId > 0 && remainRefundAmount > 0 {
|
||||
deductions, _ := l.AgentCommissionDeductionModel.FindAll(ctx,
|
||||
l.AgentCommissionDeductionModel.SelectBuilder().Where(map[string]interface{}{
|
||||
"order_id": order.Id,
|
||||
"agent_id": ancestorAgentId,
|
||||
}), "")
|
||||
|
||||
for _, deduction := range deductions {
|
||||
if deduction.Status == 2 {
|
||||
continue
|
||||
}
|
||||
if remainRefundAmount <= 0 {
|
||||
break
|
||||
}
|
||||
available := roundRefundMoney(deduction.Amount - deduction.RefundedAmount)
|
||||
if available <= 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
currentRefund := available
|
||||
if currentRefund > remainRefundAmount {
|
||||
currentRefund = remainRefundAmount
|
||||
}
|
||||
currentRefund = roundRefundMoney(currentRefund)
|
||||
|
||||
deduction.RefundedAmount = roundRefundMoney(deduction.RefundedAmount + currentRefund)
|
||||
if deduction.RefundedAmount >= roundRefundMoney(deduction.Amount) {
|
||||
deduction.Status = 2
|
||||
}
|
||||
|
||||
if err := l.AgentCommissionDeductionModel.UpdateWithVersion(ctx, session, deduction); err != nil {
|
||||
logx.Errorf("退款扣款:更新上级抽佣失败,ID: %d, 错误: %v", deduction.Id, err)
|
||||
continue
|
||||
}
|
||||
|
||||
deductFromAgentWallet(ctx, l, session, ancestorAgentId, currentRefund,
|
||||
fmt.Sprintf("订单退款,上级抽佣扣除,订单号: %s", order.OrderNo))
|
||||
|
||||
remainRefundAmount = roundRefundMoney(remainRefundAmount - currentRefund)
|
||||
}
|
||||
}
|
||||
|
||||
// 第3步:取消上级推广奖励(无论退多少都取消)
|
||||
if ancestorAgentId > 0 {
|
||||
rewards, _ := l.AgentRewardsModel.FindAll(ctx,
|
||||
l.AgentRewardsModel.SelectBuilder().Where(map[string]interface{}{
|
||||
"agent_id": ancestorAgentId,
|
||||
"relation_agent_id": promoterAgentId,
|
||||
"type": model.AgentRewardsTypeDescendantPromotion,
|
||||
"status": 0,
|
||||
}), "id DESC")
|
||||
|
||||
if len(rewards) > 0 {
|
||||
reward := rewards[0]
|
||||
rewardAmount := roundRefundMoney(reward.Amount)
|
||||
|
||||
reward.Status = 1
|
||||
reward.Remark = fmt.Sprintf("订单退款取消奖励,订单号: %s", order.OrderNo)
|
||||
|
||||
if err := l.AgentRewardsModel.UpdateWithVersion(ctx, session, reward); err != nil {
|
||||
logx.Errorf("退款扣款:取消推广奖励失败,奖励ID: %d, 错误: %v", reward.Id, err)
|
||||
} else {
|
||||
deductFromAgentWallet(ctx, l, session, ancestorAgentId, rewardAmount,
|
||||
fmt.Sprintf("订单退款,取消推广奖励,订单号: %s", order.OrderNo))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 第4步:不足部分从推广代理钱包扣
|
||||
if remainRefundAmount > 0 {
|
||||
deductFromAgentWallet(ctx, l, session, promoterAgentId, remainRefundAmount,
|
||||
fmt.Sprintf("订单退款,不足部分从钱包扣除,订单号: %s", order.OrderNo))
|
||||
}
|
||||
}
|
||||
|
||||
// roundRefundMoney 四舍五入到分
|
||||
func roundRefundMoney(v float64) float64 {
|
||||
return math.Round(v*100) / 100
|
||||
}
|
||||
|
||||
// deductFromAgentWallet 从代理钱包扣除金额(先冻结后余额)
|
||||
func deductFromAgentWallet(ctx context.Context, l *AgentService, session sqlx.Session, agentId int64, amount float64, remark string) {
|
||||
amount = roundRefundMoney(amount)
|
||||
if amount <= 0 {
|
||||
return
|
||||
}
|
||||
|
||||
wallet, err := l.AgentWalletModel.FindOneByAgentId(ctx, agentId)
|
||||
if err != nil {
|
||||
logx.Errorf("退款扣款:查询代理钱包失败,代理ID: %d, 错误: %v", agentId, err)
|
||||
return
|
||||
}
|
||||
|
||||
balanceBefore := wallet.Balance
|
||||
frozenBalanceBefore := wallet.FrozenBalance
|
||||
|
||||
if wallet.FrozenBalance >= amount {
|
||||
wallet.FrozenBalance = roundRefundMoney(wallet.FrozenBalance - amount)
|
||||
} else {
|
||||
remaining := roundRefundMoney(amount - wallet.FrozenBalance)
|
||||
wallet.FrozenBalance = 0
|
||||
wallet.Balance = roundRefundMoney(wallet.Balance - remaining)
|
||||
}
|
||||
|
||||
balanceAfter := roundRefundMoney(wallet.Balance)
|
||||
frozenBalanceAfter := roundRefundMoney(wallet.FrozenBalance)
|
||||
|
||||
var updateErr error
|
||||
if session != nil {
|
||||
updateErr = l.AgentWalletModel.UpdateWithVersion(ctx, session, wallet)
|
||||
} else {
|
||||
updateErr = l.AgentWalletModel.UpdateWithVersion(ctx, nil, wallet)
|
||||
}
|
||||
if updateErr != nil {
|
||||
logx.Errorf("退款扣款:更新代理钱包失败,代理ID: %d, 错误: %v", agentId, updateErr)
|
||||
return
|
||||
}
|
||||
|
||||
transErr := l.CreateWalletTransaction(
|
||||
ctx,
|
||||
session,
|
||||
agentId,
|
||||
model.WalletTransactionTypeRefund,
|
||||
roundRefundMoney(-amount),
|
||||
balanceBefore,
|
||||
balanceAfter,
|
||||
frozenBalanceBefore,
|
||||
frozenBalanceAfter,
|
||||
"",
|
||||
0,
|
||||
remark,
|
||||
)
|
||||
if transErr != nil {
|
||||
logx.Errorf("退款扣款:创建钱包流水失败,代理ID: %d, 错误: %v", agentId, transErr)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"bdrp-server/app/main/api/internal/config"
|
||||
@@ -90,3 +91,32 @@ func (s *AsynqService) SendUnfreezeCommissionTask(commissionID int64) error {
|
||||
logx.Infof("发送佣金解冻任务成功,任务ID: %s, 队列: %s, 佣金ID: %d", info.ID, info.Queue, commissionID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendAgentMembershipExpireHandleTask 发送代理会员到期处理任务
|
||||
func (s *AsynqService) SendAgentMembershipExpireHandleTask(agentID int64, processAt time.Time) error {
|
||||
payload := types.MsgAgentMembershipExpireHandlePayload{
|
||||
AgentID: agentID,
|
||||
}
|
||||
payloadBytes, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
logx.Errorf("发送会员到期处理任务失败 (无法编码 payload): %v, 代理ID: %d", err, agentID)
|
||||
return err
|
||||
}
|
||||
|
||||
taskID := fmt.Sprintf("agent_membership_expire_handle_%d", agentID)
|
||||
options := []asynq.Option{
|
||||
asynq.ProcessAt(processAt),
|
||||
asynq.MaxRetry(5),
|
||||
asynq.TaskID(taskID),
|
||||
}
|
||||
task := asynq.NewTask(types.MsgAgentMembershipExpireHandle, payloadBytes, options...)
|
||||
|
||||
info, err := s.client.Enqueue(task)
|
||||
if err != nil {
|
||||
logx.Errorf("发送会员到期处理任务失败 (加入队列失败): %+v, 代理ID: %d, TaskID: %s", err, agentID, taskID)
|
||||
return err
|
||||
}
|
||||
|
||||
logx.Infof("发送会员到期处理任务成功,任务ID: %s, 队列: %s, 代理ID: %d, 执行时间: %s", info.ID, info.Queue, agentID, processAt.Format("2006-01-02 15:04:05"))
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -65,6 +65,7 @@ type ServiceContext struct {
|
||||
AgentWithdrawalTaxModel model.AgentWithdrawalTaxModel
|
||||
AgentWithdrawalTaxExemptionModel model.AgentWithdrawalTaxExemptionModel
|
||||
AgentWalletTransactionModel model.AgentWalletTransactionModel
|
||||
AgentConfigModel model.AgentConfigModel
|
||||
|
||||
// 管理后台相关模型
|
||||
AdminApiModel model.AdminApiModel
|
||||
@@ -155,6 +156,7 @@ func NewServiceContext(c config.Config) *ServiceContext {
|
||||
agentWithdrawalTaxModel := model.NewAgentWithdrawalTaxModel(db, cacheConf)
|
||||
agentWithdrawalTaxExemptionModel := model.NewAgentWithdrawalTaxExemptionModel(db, cacheConf)
|
||||
agentWalletTransactionModel := model.NewAgentWalletTransactionModel(db, cacheConf)
|
||||
agentConfigModel := model.NewAgentConfigModel(db, cacheConf)
|
||||
// ============================== 管理后台相关模型 ==============================
|
||||
adminApiModel := model.NewAdminApiModel(db, cacheConf)
|
||||
adminMenuModel := model.NewAdminMenuModel(db, cacheConf)
|
||||
@@ -196,7 +198,7 @@ func NewServiceContext(c config.Config) *ServiceContext {
|
||||
agentCommissionModel, agentCommissionDeductionModel, agentWalletModel, agentLinkModel,
|
||||
agentOrderModel, agentRewardsModel, agentMembershipConfigModel, agentMembershipRechargeOrderModel,
|
||||
agentMembershipUserConfigModel, agentProductConfigModel, agentPlatformDeductionModel,
|
||||
agentActiveStatModel, agentWithdrawalModel, agentWalletTransactionModel, asynqService)
|
||||
agentActiveStatModel, agentWithdrawalModel, agentWalletTransactionModel, agentConfigModel, asynqService)
|
||||
userService := service.NewUserService(&c, userModel, userAuthModel, userTempModel, agentModel)
|
||||
dictService := service.NewDictService(adminDictTypeModel, adminDictDataModel)
|
||||
adminPromotionLinkStatsService := service.NewAdminPromotionLinkStatsService(adminPromotionLinkModel,
|
||||
@@ -265,6 +267,7 @@ func NewServiceContext(c config.Config) *ServiceContext {
|
||||
AgentWithdrawalTaxModel: agentWithdrawalTaxModel,
|
||||
AgentWithdrawalTaxExemptionModel: agentWithdrawalTaxExemptionModel,
|
||||
AgentWalletTransactionModel: agentWalletTransactionModel,
|
||||
AgentConfigModel: agentConfigModel,
|
||||
|
||||
// 管理后台相关模型
|
||||
AdminApiModel: adminApiModel,
|
||||
|
||||
@@ -7,3 +7,7 @@ type MsgPaySuccessQueryPayload struct {
|
||||
type MsgUnfreezeCommissionPayload struct {
|
||||
CommissionID int64 `json:"commission_id"`
|
||||
}
|
||||
|
||||
type MsgAgentMembershipExpireHandlePayload struct {
|
||||
AgentID int64 `json:"agent_id"`
|
||||
}
|
||||
|
||||
@@ -3,3 +3,5 @@ package types
|
||||
const MsgPaySuccessQuery = "msg:pay_success:query"
|
||||
const MsgCleanQueryData = "msg:clean_query_data"
|
||||
const MsgUnfreezeCommission = "msg:unfreeze_commission"
|
||||
const MsgAgentMembershipExpireScan = "msg:agent_membership_expire_scan"
|
||||
const MsgAgentMembershipExpireHandle = "msg:agent_membership_expire_handle"
|
||||
|
||||
@@ -339,6 +339,7 @@ type AdminGetAgentRewardListReq struct {
|
||||
AgentId *int64 `form:"agent_id,optional"` // 代理ID(可选)
|
||||
RelationAgentId *int64 `form:"relation_agent_id,optional"` // 关联代理ID(可选)
|
||||
Type *string `form:"type,optional"` // 奖励类型(可选)
|
||||
Status *int64 `form:"status,optional"` // 状态(可选)
|
||||
}
|
||||
|
||||
type AdminGetAgentRewardListResp struct {
|
||||
@@ -1080,6 +1081,7 @@ type AgentCommissionDeductionListItem struct {
|
||||
AgentId int64 `json:"agent_id"` // 代理ID
|
||||
DeductedAgentId int64 `json:"deducted_agent_id"` // 被扣代理ID
|
||||
Amount float64 `json:"amount"` // 金额
|
||||
RefundedAmount float64 `json:"refunded_amount"` // 已退款金额
|
||||
ProductName string `json:"product_name"` // 产品名
|
||||
Type string `json:"type"` // 类型(cost/pricing)
|
||||
Status int64 `json:"status"` // 状态
|
||||
@@ -1255,6 +1257,8 @@ type AgentRewardListItem struct {
|
||||
RelationAgentId int64 `json:"relation_agent_id"` // 关联代理ID
|
||||
Amount float64 `json:"amount"` // 金额
|
||||
Type string `json:"type"` // 奖励类型
|
||||
Status int64 `json:"status"` // 状态:0=正常, 1=已取消
|
||||
Remark string `json:"remark"` // 备注
|
||||
CreateTime string `json:"create_time"` // 创建时间
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"bdrp-server/app/main/api/internal/config"
|
||||
"bdrp-server/app/main/api/internal/handler"
|
||||
"bdrp-server/app/main/api/internal/middleware"
|
||||
"bdrp-server/app/main/api/internal/queue"
|
||||
"bdrp-server/app/main/api/internal/service"
|
||||
"bdrp-server/app/main/api/internal/svc"
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
|
||||
@@ -58,6 +58,7 @@ func main() {
|
||||
|
||||
server := rest.MustNewServer(c.RestConf)
|
||||
server.Use(middleware.GlobalSourceInterceptor)
|
||||
server.Use(middleware.NewMembershipExpiredInterceptor(svcContext.AgentModel).Handle)
|
||||
defer server.Stop()
|
||||
|
||||
handler.RegisterHandlers(server, svcContext)
|
||||
|
||||
Reference in New Issue
Block a user