first commit

This commit is contained in:
2026-02-08 16:19:37 +08:00
commit 958df98745
569 changed files with 61311 additions and 0 deletions

View File

@@ -0,0 +1,190 @@
package agent
import (
"context"
"database/sql"
stderrors "errors"
"fmt"
"sim-server/app/main/model"
"sim-server/common/ctxdata"
"sim-server/common/xerr"
"sim-server/pkg/lzkit/crypto"
"os"
"time"
"github.com/google/uuid"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/stores/redis"
"github.com/zeromicro/go-zero/core/stores/sqlx"
"sim-server/app/main/api/internal/svc"
"sim-server/app/main/api/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type ApplyForAgentLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewApplyForAgentLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ApplyForAgentLogic {
return &ApplyForAgentLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *ApplyForAgentLogic) ApplyForAgent(req *types.AgentApplyReq) (resp *types.AgentApplyResp, err error) {
claims, err := ctxdata.GetClaimsFromCtx(l.ctx)
if err != nil && !stderrors.Is(err, ctxdata.ErrNoInCtx) {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "代理申请失败, %v", err)
}
secretKey := l.svcCtx.Config.Encrypt.SecretKey
encryptedMobile, err := crypto.EncryptMobile(req.Mobile, secretKey)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "加密手机号失败: %v", err)
}
// 系统简化:无需邀请码,直接申请成为代理
if req.Referrer == "" {
return nil, errors.Wrapf(xerr.NewErrMsg("请填写邀请信息"), "")
}
// 2. 校验验证码(开发环境下跳过验证码校验)
if os.Getenv("ENV") != "development" {
redisKey := fmt.Sprintf("%s:%s", "agentApply", encryptedMobile)
cacheCode, err := l.svcCtx.Redis.Get(redisKey)
if err != nil {
if stderrors.Is(err, redis.Nil) {
return nil, errors.Wrapf(xerr.NewErrMsg("验证码已过期"), "")
}
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "读取验证码失败, %v", err)
}
if cacheCode != req.Code {
return nil, errors.Wrapf(xerr.NewErrMsg("验证码不正确"), "")
}
}
var userID string
transErr := l.svcCtx.AgentModel.Trans(l.ctx, func(transCtx context.Context, session sqlx.Session) error {
// 1. 处理用户注册/绑定
user, findUserErr := l.svcCtx.UserModel.FindOneByMobile(l.ctx, sql.NullString{String: encryptedMobile, Valid: true})
if findUserErr != nil && !stderrors.Is(findUserErr, model.ErrNotFound) {
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询用户失败, %v", findUserErr)
}
if user == nil {
// 用户不存在,注册新用户
if claims != nil && claims.UserType == model.UserTypeNormal {
return errors.Wrapf(xerr.NewErrMsg("当前用户已注册,请输入注册的手机号"), "")
}
userID, err = l.svcCtx.UserService.RegisterUser(l.ctx, encryptedMobile)
if err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "注册用户失败: %v", err)
}
} else {
// 用户已存在
if claims != nil && claims.UserType == model.UserTypeTemp {
// 临时用户,检查手机号是否已绑定其他微信号
userAuth, findUserAuthErr := l.svcCtx.UserAuthModel.FindOneByUserIdAuthType(l.ctx, user.Id, claims.AuthType)
if findUserAuthErr != nil && !stderrors.Is(findUserAuthErr, model.ErrNotFound) {
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询用户认证失败, %v", findUserAuthErr)
}
if userAuth != nil && userAuth.AuthKey != claims.AuthKey {
return errors.Wrapf(xerr.NewErrMsg("该手机号已绑定其他微信号"), "")
}
// 临时用户,转为正式用户
err = l.svcCtx.UserService.TempUserBindUser(l.ctx, session, user.Id)
if err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "绑定用户失败: %v", err)
}
}
userID = user.Id
}
// 3. 检查是否已是代理
existingAgent, err := l.svcCtx.AgentModel.FindOneByUserId(transCtx, userID)
if err != nil && !stderrors.Is(err, model.ErrNotFound) {
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询代理信息失败, %v", err)
}
if existingAgent != nil {
return errors.Wrapf(xerr.NewErrMsg("您已经是代理"), "")
}
// 系统简化:移除邀请码验证、等级设置、团队关系建立等复杂逻辑
// 4. 分配代理编码
agentCode, err := l.allocateAgentCode(transCtx, session)
if err != nil {
return errors.Wrapf(err, "分配代理编码失败")
}
// 5. 创建代理记录(简化版:无等级、无团队首领)
newAgent := &model.Agent{
Id: uuid.NewString(),
UserId: userID,
AgentCode: agentCode,
Mobile: encryptedMobile,
}
if req.Region != "" {
newAgent.Region = sql.NullString{String: req.Region, Valid: true}
}
_, err = l.svcCtx.AgentModel.Insert(transCtx, session, newAgent)
if err != nil {
return errors.Wrapf(err, "创建代理记录失败")
}
// 6. 初始化钱包(简化版:移除冻结余额、提现金额字段)
wallet := &model.AgentWallet{
Id: uuid.NewString(),
AgentId: newAgent.Id,
}
if _, err := l.svcCtx.AgentWalletModel.Insert(transCtx, session, wallet); err != nil {
return errors.Wrapf(err, "初始化钱包失败")
}
return nil
})
if transErr != nil {
return nil, transErr
}
// 7. 生成并返回token
token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成token失败: %v", err)
}
now := time.Now().Unix()
agent, _ := l.svcCtx.AgentModel.FindOneByUserId(l.ctx, userID)
var code int64
if agent != nil {
code = agent.AgentCode
}
return &types.AgentApplyResp{
AccessToken: token,
AccessExpire: now + l.svcCtx.Config.JwtAuth.AccessExpire,
RefreshAfter: now + l.svcCtx.Config.JwtAuth.RefreshAfter,
AgentCode: code,
}, nil
}
// allocateAgentCode 分配代理编码
func (l *ApplyForAgentLogic) allocateAgentCode(ctx context.Context, session sqlx.Session) (int64, error) {
// 系统简化:不需要事务,因为调用方已经在事务中
builder := l.svcCtx.AgentModel.SelectBuilder().OrderBy("agent_code DESC").Limit(1)
rows, err := l.svcCtx.AgentModel.FindAll(ctx, builder, "")
if err != nil {
return 0, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询代理编码失败, %v", err)
}
var next int64 = 16800
if len(rows) > 0 && rows[0].AgentCode > 0 {
next = rows[0].AgentCode + 1
}
return next, nil
}

View File

@@ -0,0 +1,139 @@
package agent
import (
"context"
"database/sql"
"sim-server/app/main/api/internal/svc"
"sim-server/app/main/api/internal/types"
"sim-server/app/main/model"
"sim-server/common/ctxdata"
"sim-server/common/xerr"
"sim-server/pkg/lzkit/crypto"
"github.com/google/uuid"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/core/stores/sqlx"
)
type CreateWithdrawLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewCreateWithdrawLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateWithdrawLogic {
return &CreateWithdrawLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *CreateWithdrawLogic) CreateWithdraw(req *types.CreateWithdrawReq) (resp *types.CreateWithdrawResp, err error) {
// 1. 获取当前用户ID代理ID
userId, err := ctxdata.GetUidFromCtx(l.ctx)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取用户信息失败: %v", err)
}
// 2. 查询代理信息
agent, err := l.svcCtx.AgentModel.FindOneByUserId(l.ctx, userId)
if err != nil {
if errors.Is(err, model.ErrNotFound) {
return nil, errors.Wrap(xerr.NewErrCode(xerr.CUSTOM_ERROR), "您还不是代理,无法申请提现")
}
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询代理信息失败: %v", err)
}
// 3. 查询钱包信息
wallet, err := l.svcCtx.AgentWalletModel.FindOneByAgentId(l.ctx, agent.Id)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询钱包信息失败: %v", err)
}
// 4. 验证提现金额
if req.WithdrawAmount <= 0 {
return nil, errors.Wrap(xerr.NewErrCode(xerr.REUQEST_PARAM_ERROR), "提现金额必须大于0")
}
minWithdraw := 100.00 // 最低提现金额100元
if req.WithdrawAmount < minWithdraw {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.REUQEST_PARAM_ERROR), "最低提现金额为%.2f元", minWithdraw)
}
if wallet.Balance < req.WithdrawAmount {
return nil, errors.Wrap(xerr.NewErrCode(xerr.CUSTOM_ERROR), "可用余额不足")
}
// 5. 加密银行卡号
encryptedCardNumber, err := crypto.AesEncrypt([]byte(req.BankCardNumber), []byte(l.svcCtx.Config.Encrypt.SecretKey))
if err != nil {
logx.Errorf("加密银行卡号失败: %v", err)
return nil, errors.Wrap(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "加密银行卡号失败")
}
// 6. 生成提现记录ID
withdrawId := uuid.NewString()
// 7. 解密代理手机号(用于冗余存储)
agentMobile, err := crypto.DecryptMobile(agent.Mobile, l.svcCtx.Config.Encrypt.SecretKey)
if err != nil {
logx.Errorf("解密代理手机号失败: %v", err)
agentMobile = ""
}
// 8. 开启事务处理
err = l.svcCtx.AgentWalletModel.Trans(l.ctx, func(ctx context.Context, session sqlx.Session) error {
// 8.1 冻结提现金额
wallet.Balance -= req.WithdrawAmount
wallet.FrozenAmount += req.WithdrawAmount
// 使用版本号乐观锁更新
err := l.svcCtx.AgentWalletModel.UpdateWithVersion(ctx, session, wallet)
if err != nil {
return errors.Wrapf(err, "冻结金额失败")
}
// 8.2 创建提现记录
withdrawRecord := &model.AgentWithdraw{
Id: withdrawId,
AgentId: agent.Id,
WithdrawAmount: req.WithdrawAmount,
TaxAmount: 0.00,
ActualAmount: 0.00,
FrozenAmount: req.WithdrawAmount,
AccountName: req.AccountName,
BankCardNumber: req.BankCardNumber,
BankCardNumberEncrypted: sql.NullString{String: string(encryptedCardNumber), Valid: true},
BankBranch: sql.NullString{String: req.BankBranch, Valid: req.BankBranch != ""},
Status: 0,
}
// 冗余存储代理手机号和编码
if agentMobile != "" {
withdrawRecord.AgentMobile = sql.NullString{String: agentMobile, Valid: true}
}
if agent.AgentCode > 0 {
withdrawRecord.AgentCode = sql.NullInt64{Int64: agent.AgentCode, Valid: true}
}
_, err = l.svcCtx.AgentWithdrawModel.Insert(ctx, session, withdrawRecord)
if err != nil {
return errors.Wrapf(err, "创建提现记录失败")
}
return nil
})
if err != nil {
return nil, errors.Wrap(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), err.Error())
}
logx.Infof("代理 %s 申请提现成功,提现金额: %.2f", agent.Id, req.WithdrawAmount)
return &types.CreateWithdrawResp{
WithdrawId: withdrawId,
}, nil
}

View File

@@ -0,0 +1,279 @@
package agent
import (
"context"
"database/sql"
"encoding/hex"
"encoding/json"
"fmt"
"net/url"
"strings"
"sim-server/app/main/model"
"sim-server/common/ctxdata"
"sim-server/common/globalkey"
"sim-server/common/tool"
"sim-server/common/xerr"
"sim-server/pkg/lzkit/crypto"
"github.com/Masterminds/squirrel"
"github.com/pkg/errors"
"sim-server/app/main/api/internal/svc"
"sim-server/app/main/api/internal/types"
"github.com/google/uuid"
"github.com/zeromicro/go-zero/core/logx"
)
type GeneratingLinkLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewGeneratingLinkLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GeneratingLinkLogic {
return &GeneratingLinkLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *GeneratingLinkLogic) GeneratingLink(req *types.AgentGeneratingLinkReq) (resp *types.AgentGeneratingLinkResp, err error) {
userID, err := ctxdata.GetUidFromCtx(l.ctx)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成推广链接失败, %v", err)
}
// 1. 获取代理信息
agentModel, 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. 获取产品配置(必须存在)
productConfig, err := l.svcCtx.AgentProductConfigModel.FindOneByProductId(l.ctx, req.ProductId)
if err != nil {
if errors.Is(err, model.ErrNotFound) {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "产品配置不存在, productId: %d请先在后台配置产品价格参数", req.ProductId)
}
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询产品配置失败, productId: %d, %v", req.ProductId, err)
}
// 系统简化:移除等级加成和等级上调金额,直接使用底价和系统最高价
actualBasePrice := productConfig.BasePrice
systemMaxPrice := productConfig.SystemMaxPrice
if req.SetPrice < actualBasePrice || req.SetPrice > systemMaxPrice {
return nil, errors.Wrapf(xerr.NewErrMsg("设定价格必须在 %.2f 到 %.2f 之间"), "设定价格必须在 %.2f 到 %.2f 之间", actualBasePrice, systemMaxPrice)
}
// 检查是否已存在相同的链接(同一代理、同一产品、同一价格)
builder := l.svcCtx.AgentLinkModel.SelectBuilder().Where(squirrel.And{
squirrel.Eq{"agent_id": agentModel.Id},
squirrel.Eq{"product_id": req.ProductId},
squirrel.Eq{"set_price": req.SetPrice},
squirrel.Eq{"del_state": globalkey.DelStateNo},
})
existingLinks, err := l.svcCtx.AgentLinkModel.FindAll(l.ctx, builder, "")
if err != nil && !errors.Is(err, model.ErrNotFound) {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询推广链接失败, %v", err)
}
if len(existingLinks) > 0 {
// 已存在,检查是否有短链,如果没有则生成
targetPath := req.TargetPath
if targetPath == "" {
targetPath = "/agent/promotionInquire/"
}
shortLink, err := l.getOrCreateShortLink(1, existingLinks[0].Id, "", existingLinks[0].LinkIdentifier, "", targetPath)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取或创建短链失败, %v", err)
}
return &types.AgentGeneratingLinkResp{
LinkIdentifier: existingLinks[0].LinkIdentifier,
FullLink: shortLink,
}, nil
}
// 7. 生成推广链接标识
var agentIdentifier types.AgentIdentifier
agentIdentifier.AgentID = agentModel.Id
agentIdentifier.ProductID = req.ProductId
agentIdentifier.SetPrice = req.SetPrice
agentIdentifierByte, err := json.Marshal(agentIdentifier)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "序列化标识失败, %v", err)
}
// 8. 加密链接标识
secretKey := l.svcCtx.Config.Encrypt.SecretKey
key, decodeErr := hex.DecodeString(secretKey)
if decodeErr != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取AES密钥失败: %+v", decodeErr)
}
encrypted, err := crypto.AesEncryptURL(agentIdentifierByte, key)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "加密链接标识失败, %v", err)
}
// 9. 保存推广链接
agentLink := &model.AgentLink{
// Id: uuid.NewString(),
AgentId: agentModel.Id,
UserId: userID,
ProductId: req.ProductId,
LinkIdentifier: encrypted,
SetPrice: req.SetPrice,
ActualBasePrice: actualBasePrice,
}
_, err = l.svcCtx.AgentLinkModel.Insert(l.ctx, nil, agentLink)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "保存推广链接失败, %v", err)
}
linkId := agentLink.Id
// 使用默认target_path如果未提供
targetPath := req.TargetPath
if targetPath == "" {
targetPath = "/agent/promotionInquire/"
}
// 生成短链类型1=推广报告)
shortLink, err := l.createShortLink(1, linkId, "", encrypted, "", targetPath)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "生成短链失败, %v", err)
}
return &types.AgentGeneratingLinkResp{
LinkIdentifier: encrypted,
FullLink: shortLink,
}, nil
}
// getOrCreateShortLink 获取或创建短链
// type: 1=推广报告(promotion), 2=邀请好友(invite)
// linkId: 推广链接ID仅推广报告使用
// inviteCodeId: 邀请码ID仅邀请好友使用
// linkIdentifier: 推广链接标识(仅推广报告使用)
// inviteCode: 邀请码(仅邀请好友使用)
// targetPath: 目标地址(前端传入)
func (l *GeneratingLinkLogic) getOrCreateShortLink(linkType int64, linkId, inviteCodeId string, linkIdentifier, inviteCode, targetPath string) (string, error) {
// 先查询是否已存在短链
var existingShortLink *model.AgentShortLink
var err error
if linkType == 1 {
// 推广报告类型使用link_id查询
if linkId != "" {
existingShortLink, err = l.svcCtx.AgentShortLinkModel.FindOneByLinkIdTypeDelState(l.ctx, sql.NullString{String: linkId, Valid: true}, linkType, globalkey.DelStateNo)
}
} else {
// 邀请好友类型使用invite_code_id查询
if inviteCodeId != "" {
existingShortLink, err = l.svcCtx.AgentShortLinkModel.FindOneByInviteCodeIdTypeDelState(l.ctx, sql.NullString{String: inviteCodeId, Valid: true}, linkType, globalkey.DelStateNo)
}
}
if err == nil && existingShortLink != nil {
// 已存在短链,直接返回
return l.buildShortLinkURL(existingShortLink.ShortCode), nil
}
if err != nil && !errors.Is(err, model.ErrNotFound) {
return "", errors.Wrapf(err, "查询短链失败")
}
// 不存在,创建新的短链
return l.createShortLink(linkType, linkId, inviteCodeId, linkIdentifier, inviteCode, targetPath)
}
// createShortLink 创建短链
// type: 1=推广报告(promotion), 2=邀请好友(invite)
func (l *GeneratingLinkLogic) createShortLink(linkType int64, linkId, inviteCodeId string, linkIdentifier, inviteCode, targetPath string) (string, error) {
promotionConfig := l.svcCtx.Config.Promotion
// 如果没有配置推广域名,返回空字符串(保持向后兼容)
if promotionConfig.PromotionDomain == "" {
l.Errorf("推广域名未配置,返回空链接")
return "", nil
}
// 验证target_path
if targetPath == "" {
return "", errors.Wrapf(xerr.NewErrMsg("目标地址不能为空"), "")
}
// 对于推广报告类型,将 linkIdentifier 拼接到 target_path
if linkType == 1 && linkIdentifier != "" {
// 如果 target_path 以 / 结尾,直接拼接 linkIdentifier
if strings.HasSuffix(targetPath, "/") {
targetPath = targetPath + url.QueryEscape(linkIdentifier)
} else {
// 否则在末尾添加 / 再拼接
targetPath = targetPath + "/" + url.QueryEscape(linkIdentifier)
}
}
// 生成短链标识6位随机字符串大小写字母+数字)
var shortCode string
maxRetries := 10 // 最大重试次数
for retry := 0; retry < maxRetries; retry++ {
shortCode = tool.Krand(6, tool.KC_RAND_KIND_ALL)
// 检查短链标识是否已存在
_, err := l.svcCtx.AgentShortLinkModel.FindOneByShortCodeDelState(l.ctx, shortCode, globalkey.DelStateNo)
if err != nil {
if errors.Is(err, model.ErrNotFound) {
// 短链标识不存在,可以使用
break
}
return "", errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "检查短链标识失败, %v", err)
}
// 短链标识已存在,继续生成
if retry == maxRetries-1 {
return "", errors.Wrapf(xerr.NewErrMsg("生成短链失败,请重试"), "")
}
}
// 创建短链记录
shortLink := &model.AgentShortLink{
Id: uuid.NewString(),
Type: linkType,
ShortCode: shortCode,
TargetPath: targetPath,
PromotionDomain: promotionConfig.PromotionDomain,
}
// 根据类型设置对应字段
if linkType == 1 {
// 推广报告类型
shortLink.LinkId = sql.NullString{String: linkId, Valid: linkId != ""}
if linkIdentifier != "" {
shortLink.LinkIdentifier = sql.NullString{String: linkIdentifier, Valid: true}
}
} else if linkType == 2 {
// 邀请好友类型
shortLink.InviteCodeId = sql.NullString{String: inviteCodeId, Valid: inviteCodeId != ""}
if inviteCode != "" {
shortLink.InviteCode = sql.NullString{String: inviteCode, Valid: true}
}
}
_, err := l.svcCtx.AgentShortLinkModel.Insert(l.ctx, nil, shortLink)
if err != nil {
return "", errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "保存短链失败, %v", err)
}
return l.buildShortLinkURL(shortCode), nil
}
// buildShortLinkURL 构建短链URL
func (l *GeneratingLinkLogic) buildShortLinkURL(shortCode string) string {
promotionConfig := l.svcCtx.Config.Promotion
return fmt.Sprintf("%s/s/%s", promotionConfig.PromotionDomain, shortCode)
}

View File

@@ -0,0 +1,157 @@
package agent
import (
"context"
"fmt"
"time"
"sim-server/common/ctxdata"
"sim-server/common/xerr"
"github.com/pkg/errors"
"sim-server/app/main/api/internal/svc"
"sim-server/app/main/api/internal/types"
"sim-server/app/main/model"
"github.com/zeromicro/go-zero/core/logx"
)
type GetAgentDashboardLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewGetAgentDashboardLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetAgentDashboardLogic {
return &GetAgentDashboardLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *GetAgentDashboardLogic) GetAgentDashboard() (resp *types.AgentDashboardResp, err error) {
userID, err := ctxdata.GetUidFromCtx(l.ctx)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取用户信息失败, %v", err)
}
// 查询代理信息
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)
}
// 查询钱包信息
wallet, err := l.svcCtx.AgentWalletModel.FindOneByAgentId(l.ctx, agent.Id)
if err != nil && !errors.Is(err, model.ErrNotFound) {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询钱包信息失败, %v", err)
}
if wallet == nil {
// 如果没有钱包,创建一个空的
wallet = &model.AgentWallet{
Balance: 0,
TotalEarnings: 0,
}
}
// 获取今日统计数据(使用佣金表)
today := time.Now().Format("2006-01-02")
todayStart, _ := time.Parse("2006-01-02", today)
todayEnd := todayStart.Add(24 * time.Hour)
// 今日订单数和佣金
todayOrders, todayEarnings, err := l.getCommissionStats(l.ctx, agent.Id, todayStart, todayEnd)
if err != nil {
logx.Errorf("获取今日佣金统计失败: %v", err)
todayOrders = 0
todayEarnings = 0
}
// 本月订单数和佣金
monthStart := time.Now().AddDate(0, 0, -time.Now().Day()+1) // 本月1号
monthStart = time.Date(monthStart.Year(), monthStart.Month(), monthStart.Day(), 0, 0, 0, 0, monthStart.Location())
_, monthEarnings, err := l.getCommissionStats(l.ctx, agent.Id, monthStart, time.Now())
if err != nil {
logx.Errorf("获取本月佣金统计失败: %v", err)
monthEarnings = 0
}
// 总订单数和佣金
totalOrders, totalEarnings, err := l.getCommissionStats(l.ctx, agent.Id, time.Time{}, time.Now())
if err != nil {
logx.Errorf("获取总佣金统计失败: %v", err)
totalOrders = 0
totalEarnings = 0
}
// 构建推广链接
domain := l.svcCtx.Config.Promotion.OfficialDomain
promotionLink := fmt.Sprintf("%s/pages/query/index?agentCode=%d", domain, agent.AgentCode)
// 获取佣金率从配置中读取默认为20%
commissionRate := l.getCommissionRate()
return &types.AgentDashboardResp{
Statistics: types.DashboardStatistics{
TodayOrders: todayOrders,
TodayEarnings: fmt.Sprintf("%.2f", todayEarnings),
MonthEarnings: fmt.Sprintf("%.2f", monthEarnings),
TotalOrders: totalOrders,
TotalEarnings: fmt.Sprintf("%.2f", totalEarnings),
CommissionRate: commissionRate,
CurrentBalance: fmt.Sprintf("%.2f", wallet.Balance),
FrozenBalance: fmt.Sprintf("%.2f", wallet.FrozenAmount),
},
PromotionLink: promotionLink,
Product: types.DashboardProduct{
Name: "个人大数据",
Price: "29.90",
Description: "全面个人数据风险报告",
},
}, nil
}
// getCommissionStats 获取指定时间段的佣金统计
func (l *GetAgentDashboardLogic) getCommissionStats(ctx context.Context, agentId string, startTime, endTime time.Time) (int64, float64, error) {
builder := l.svcCtx.AgentCommissionModel.SelectBuilder()
// 添加代理ID条件
builder = builder.Where("agent_id = ?", agentId)
// 添加时间范围条件
if !startTime.IsZero() {
builder = builder.Where("create_time >= ?", startTime)
}
if !endTime.IsZero() {
builder = builder.Where("create_time < ?", endTime)
}
// 只统计已发放的佣金
builder = builder.Where("status = ?", 1)
// 获取数量
count, err := l.svcCtx.AgentCommissionModel.FindCount(ctx, builder, "id")
if err != nil {
return 0, 0, err
}
// 获取总额
sum, err := l.svcCtx.AgentCommissionModel.FindSum(ctx, builder, "amount")
if err != nil {
return count, 0, err
}
return count, sum, nil
}
// getCommissionRate 获取佣金率
func (l *GetAgentDashboardLogic) getCommissionRate() string {
// TODO: 从配置或数据库中读取佣金率
// 目前返回固定值20%
return "20%"
}

View File

@@ -0,0 +1,79 @@
package agent
import (
"context"
"sim-server/common/ctxdata"
"sim-server/common/xerr"
"sim-server/pkg/lzkit/crypto"
"github.com/pkg/errors"
"sim-server/app/main/api/internal/svc"
"sim-server/app/main/api/internal/types"
"sim-server/app/main/model"
"github.com/zeromicro/go-zero/core/logx"
)
type GetAgentInfoLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewGetAgentInfoLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetAgentInfoLogic {
return &GetAgentInfoLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *GetAgentInfoLogic) GetAgentInfo() (resp *types.AgentInfoResp, err error) {
userID, err := ctxdata.GetUidFromCtx(l.ctx)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取用户信息失败, %v", err)
}
agent, err := l.svcCtx.AgentModel.FindOneByUserId(l.ctx, userID)
if err != nil {
if errors.Is(err, model.ErrNotFound) {
// 不是代理,返回空信息
return &types.AgentInfoResp{
AgentId: "",
Region: "",
Mobile: "",
WechatId: "",
AgentCode: 0,
}, nil
}
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询代理信息失败, %v", err)
}
// 解密手机号
mobile, err := crypto.DecryptMobile(agent.Mobile, l.svcCtx.Config.Encrypt.SecretKey)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "解密手机号失败: %v", err)
}
// 系统简化:移除实名认证查询
// 获取微信号
wechatId := ""
if agent.WechatId.Valid {
wechatId = agent.WechatId.String
}
// 获取区域
region := ""
if agent.Region.Valid {
region = agent.Region.String
}
return &types.AgentInfoResp{
AgentId: agent.Id,
Region: region,
Mobile: mobile,
WechatId: wechatId,
AgentCode: agent.AgentCode,
}, nil
}

View File

@@ -0,0 +1,99 @@
package agent
import (
"context"
"sim-server/app/main/model"
"sim-server/common/ctxdata"
"sim-server/common/xerr"
"github.com/pkg/errors"
"sim-server/app/main/api/internal/svc"
"sim-server/app/main/api/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type GetAgentProductConfigLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewGetAgentProductConfigLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetAgentProductConfigLogic {
return &GetAgentProductConfigLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *GetAgentProductConfigLogic) GetAgentProductConfig() (resp *types.AgentProductConfigResp, err error) {
userID, err := ctxdata.GetUidFromCtx(l.ctx)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取用户信息失败, %v", err)
}
// 1. 验证用户是否为代理
_, 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. 查询所有产品配置
builder := l.svcCtx.AgentProductConfigModel.SelectBuilder()
productConfigs, err := l.svcCtx.AgentProductConfigModel.FindAll(l.ctx, builder, "")
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询产品配置失败, %v", err)
}
// 3. 组装响应数据(通过 product_id 查询产品名称和英文标识)
var respList []types.ProductConfigItem
for _, productConfig := range productConfigs {
// 通过 product_id 查询产品信息获取产品名称和英文标识
product, err := l.svcCtx.ProductModel.FindOne(l.ctx, productConfig.ProductId)
productName := ""
productEn := ""
if err == nil {
productName = product.ProductName
productEn = product.ProductEn
} else {
// 如果产品不存在,记录日志但不影响主流程
l.Infof("查询产品信息失败, productId: %d, err: %v", productConfig.ProductId, err)
}
// 系统简化:直接使用基础底价和系统最高价
actualBasePrice := productConfig.BasePrice
priceRangeMin := actualBasePrice
priceRangeMax := productConfig.SystemMaxPrice
// 使用产品配置的提价阈值和手续费比例如果为NULL则使用0
productPriceThreshold := 0.0
if productConfig.PriceThreshold.Valid {
productPriceThreshold = productConfig.PriceThreshold.Float64
}
productPriceFeeRate := 0.0
if productConfig.PriceFeeRate.Valid {
productPriceFeeRate = productConfig.PriceFeeRate.Float64
}
respList = append(respList, types.ProductConfigItem{
ProductId: productConfig.ProductId,
ProductName: productName,
ProductEn: productEn,
ActualBasePrice: actualBasePrice,
PriceRangeMin: priceRangeMin,
PriceRangeMax: priceRangeMax,
PriceThreshold: productPriceThreshold,
PriceFeeRate: productPriceFeeRate,
})
}
return &types.AgentProductConfigResp{
List: respList,
}, nil
}

View File

@@ -0,0 +1,112 @@
package agent
import (
"context"
"sim-server/app/main/model"
"sim-server/common/ctxdata"
"sim-server/common/globalkey"
"sim-server/common/xerr"
"github.com/pkg/errors"
"sim-server/app/main/api/internal/svc"
"sim-server/app/main/api/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type GetCommissionListLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewGetCommissionListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetCommissionListLogic {
return &GetCommissionListLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *GetCommissionListLogic) GetCommissionList(req *types.GetCommissionListReq) (resp *types.GetCommissionListResp, 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. 构建查询条件
builder := l.svcCtx.AgentCommissionModel.SelectBuilder().
Where("agent_id = ? AND del_state = ?", agent.Id, globalkey.DelStateNo).
OrderBy("create_time DESC")
// 3. 分页查询
page := req.Page
if page <= 0 {
page = 1
}
pageSize := req.PageSize
if pageSize <= 0 {
pageSize = 20
}
offset := (page - 1) * pageSize
// 4. 查询总数
total, err := l.svcCtx.AgentCommissionModel.FindCount(l.ctx, builder, "id")
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询佣金总数失败, %v", err)
}
// 5. 查询列表
builder = builder.Limit(uint64(pageSize)).Offset(uint64(offset))
commissions, err := l.svcCtx.AgentCommissionModel.FindAll(l.ctx, builder, "")
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询佣金列表失败, %v", err)
}
// 6. 组装响应
var list []types.CommissionItem
for _, commission := range commissions {
// 查询产品名称
productName := ""
if commission.ProductId != "" {
product, err := l.svcCtx.ProductModel.FindOne(l.ctx, commission.ProductId)
if err == nil {
productName = product.ProductName
}
}
// 查询订单号
orderNo := ""
if commission.OrderId != "" {
order, err := l.svcCtx.OrderModel.FindOne(l.ctx, commission.OrderId)
if err == nil {
orderNo = order.OrderNo
}
}
list = append(list, types.CommissionItem{
Id: commission.Id,
OrderId: commission.OrderId,
OrderNo: orderNo,
ProductName: productName,
Amount: commission.Amount,
Status: commission.Status,
CreateTime: commission.CreateTime.Format("2006-01-02 15:04:05"),
})
}
return &types.GetCommissionListResp{
Total: total,
List: list,
}, nil
}

View File

@@ -0,0 +1,102 @@
package agent
import (
"context"
"sort"
"sim-server/app/main/model"
"sim-server/common/xerr"
"github.com/Masterminds/squirrel"
"github.com/jinzhu/copier"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/mr"
"sim-server/app/main/api/internal/svc"
"sim-server/app/main/api/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type GetLinkDataLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewGetLinkDataLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetLinkDataLogic {
return &GetLinkDataLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *GetLinkDataLogic) GetLinkData(req *types.GetLinkDataReq) (resp *types.GetLinkDataResp, err error) {
agentLinkModel, err := l.svcCtx.AgentLinkModel.FindOneByLinkIdentifier(l.ctx, req.LinkIdentifier)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取代理链接数据失败, %v", err)
}
productModel, err := l.svcCtx.ProductModel.FindOne(l.ctx, agentLinkModel.ProductId)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取产品信息失败, %v", err)
}
// 获取产品功能列表
build := l.svcCtx.ProductFeatureModel.SelectBuilder().Where(squirrel.Eq{
"product_id": productModel.Id,
})
productFeatureAll, err := l.svcCtx.ProductFeatureModel.FindAll(l.ctx, build, "")
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取产品功能列表失败, %v", err)
}
// 创建featureId到sort的映射用于后续排序
featureSortMap := make(map[string]int64)
for _, productFeature := range productFeatureAll {
featureSortMap[productFeature.FeatureId] = productFeature.Sort
}
var features []types.Feature
mr.MapReduceVoid(func(source chan<- interface{}) {
for _, productFeature := range productFeatureAll {
source <- productFeature.FeatureId
}
}, func(item interface{}, writer mr.Writer[*model.Feature], cancel func(error)) {
id := item.(string)
feature, findFeatureErr := l.svcCtx.FeatureModel.FindOne(l.ctx, id)
if findFeatureErr != nil {
logx.WithContext(l.ctx).Errorf("获取产品功能失败: %s, err:%v", id, findFeatureErr)
return
}
if feature != nil && feature.Id != "" {
writer.Write(feature)
}
}, func(pipe <-chan *model.Feature, cancel func(error)) {
for item := range pipe {
var feature types.Feature
_ = copier.Copy(&feature, item)
features = append(features, feature)
}
})
// 按照productFeature.Sort字段对features进行排序
sort.Slice(features, func(i, j int) bool {
sortI := featureSortMap[features[i].ID]
sortJ := featureSortMap[features[j].ID]
return sortI < sortJ
})
return &types.GetLinkDataResp{
AgentId: agentLinkModel.AgentId,
ProductId: agentLinkModel.ProductId,
SetPrice: agentLinkModel.SetPrice,
ActualBasePrice: agentLinkModel.ActualBasePrice,
ProductName: productModel.ProductName,
ProductEn: productModel.ProductEn,
SellPrice: agentLinkModel.SetPrice, // 使用代理设定价格作为销售价格
Description: productModel.Description,
Features: features,
}, nil
}

View File

@@ -0,0 +1,124 @@
package agent
import (
"context"
"encoding/hex"
"encoding/json"
"sim-server/app/main/model"
"sim-server/common/ctxdata"
"sim-server/common/xerr"
"sim-server/pkg/lzkit/crypto"
"github.com/jinzhu/copier"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/logx"
"sim-server/app/main/api/internal/svc"
"sim-server/app/main/api/internal/types"
)
type GetPromotionQueryListLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewGetPromotionQueryListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetPromotionQueryListLogic {
return &GetPromotionQueryListLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *GetPromotionQueryListLogic) GetPromotionQueryList(req *types.GetPromotionQueryListReq) (resp *types.GetPromotionQueryListResp, err error) {
userID, err := ctxdata.GetUidFromCtx(l.ctx)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取用户信息失败, %v", err)
}
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)
}
// 查询当前代理的代理订单,按创建时间倒序分页
builder := l.svcCtx.AgentOrderModel.SelectBuilder().
Where("agent_id = ? AND process_status = 1", agent.Id)
orders, total, err := l.svcCtx.AgentOrderModel.FindPageListByPageWithTotal(l.ctx, builder, req.Page, req.PageSize, "create_time DESC")
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询代理订单失败, %v", err)
}
secretKey := l.svcCtx.Config.Encrypt.SecretKey
key, decodeErr := hex.DecodeString(secretKey)
if decodeErr != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR),
"获取AES密钥失败: %v", decodeErr)
}
// 组装查询报告列表(只展示已创建的查询)
list := make([]types.PromotionQueryItem, 0, len(orders))
for _, ao := range orders {
// 查询对应的报告
q, qErr := l.svcCtx.QueryModel.FindOneByOrderId(l.ctx, ao.OrderId)
if qErr != nil {
if errors.Is(qErr, model.ErrNotFound) {
// 订单对应的查询尚未创建,跳过展示
continue
}
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询报告失败, %v", qErr)
}
// 查询订单信息获取order_no
order, oErr := l.svcCtx.OrderModel.FindOne(l.ctx, ao.OrderId)
if oErr != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询订单信息失败, %v", oErr)
}
// 获取产品名称
product, pErr := l.svcCtx.ProductModel.FindOne(l.ctx, ao.ProductId)
if pErr != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询产品信息失败, %v", pErr)
}
item := types.PromotionQueryItem{}
_ = copier.Copy(&item, q)
item.Id = q.Id
item.OrderId = q.OrderId
item.OrderNo = order.OrderNo
item.ProductName = product.ProductName
item.OrderAmount = order.Amount // 订单金额
item.Amount = ao.AgentProfit // 推广收益
item.CreateTime = q.CreateTime.Format("2006-01-02 15:04:05")
item.QueryState = q.QueryState
// 解析query_params获取查询人信息
if q.QueryParams != "" {
queryParamsByte, err := crypto.AesDecrypt(q.QueryParams, key)
var queryParams map[string]interface{}
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "解密查询参数失败, %v", err)
}
if err := json.Unmarshal(queryParamsByte, &queryParams); err == nil {
// 获取姓名
if name, ok := queryParams["name"].(string); ok {
item.QueryName = name
}
// 获取手机号
if mobile, ok := queryParams["mobile"].(string); ok {
item.QueryMobile = mobile
}
}
}
list = append(list, item)
}
return &types.GetPromotionQueryListResp{
Total: total, // 前端仅展示已创建查询条目
List: list,
}, nil
}

View File

@@ -0,0 +1,84 @@
package agent
import (
"context"
"time"
"sim-server/app/main/model"
"sim-server/common/ctxdata"
"sim-server/common/globalkey"
"sim-server/common/xerr"
"github.com/pkg/errors"
"sim-server/app/main/api/internal/svc"
"sim-server/app/main/api/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type GetRevenueInfoLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewGetRevenueInfoLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetRevenueInfoLogic {
return &GetRevenueInfoLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *GetRevenueInfoLogic) GetRevenueInfo() (resp *types.GetRevenueInfoResp, 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. 获取钱包信息
wallet, err := l.svcCtx.AgentWalletModel.FindOneByAgentId(l.ctx, agent.Id)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询钱包信息失败, %v", err)
}
// 获取当前时间
now := time.Now()
// 今日开始时间00:00:00
todayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
// 本月开始时间1号 00:00:00
monthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
// 3. 统计佣金总额(从 agent_commission 表统计)
commissionBuilder := l.svcCtx.AgentCommissionModel.SelectBuilder().
Where("agent_id = ? AND del_state = ?", agent.Id, globalkey.DelStateNo)
commissionTotal, _ := l.svcCtx.AgentCommissionModel.FindSum(l.ctx, commissionBuilder, "amount")
// 3.1 统计佣金今日收益
commissionTodayBuilder := l.svcCtx.AgentCommissionModel.SelectBuilder().
Where("agent_id = ? AND del_state = ? AND create_time >= ?", agent.Id, globalkey.DelStateNo, todayStart)
commissionToday, _ := l.svcCtx.AgentCommissionModel.FindSum(l.ctx, commissionTodayBuilder, "amount")
// 3.2 统计佣金本月收益
commissionMonthBuilder := l.svcCtx.AgentCommissionModel.SelectBuilder().
Where("agent_id = ? AND del_state = ? AND create_time >= ?", agent.Id, globalkey.DelStateNo, monthStart)
commissionMonth, _ := l.svcCtx.AgentCommissionModel.FindSum(l.ctx, commissionMonthBuilder, "amount")
// 系统简化:移除返佣功能(推广返佣和升级返佣)
return &types.GetRevenueInfoResp{
Balance: wallet.Balance,
TotalEarnings: wallet.TotalEarnings,
CommissionTotal: commissionTotal, // 佣金累计总收益
CommissionToday: commissionToday, // 佣金今日收益
CommissionMonth: commissionMonth, // 佣金本月收益
}, nil
}

View File

@@ -0,0 +1,127 @@
package agent
import (
"context"
"fmt"
"sim-server/app/main/api/internal/svc"
"sim-server/app/main/api/internal/types"
"sim-server/app/main/model"
"sim-server/common/ctxdata"
"sim-server/common/globalkey"
"sim-server/common/xerr"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/logx"
)
type GetWithdrawListLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewGetWithdrawListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetWithdrawListLogic {
return &GetWithdrawListLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *GetWithdrawListLogic) GetWithdrawList(req *types.GetWithdrawListReq) (resp *types.GetWithdrawListResp, err error) {
// 1. 获取当前用户ID代理ID
userId, err := ctxdata.GetUidFromCtx(l.ctx)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取用户信息失败: %v", err)
}
// 2. 查询代理信息
agent, err := l.svcCtx.AgentModel.FindOneByUserId(l.ctx, userId)
if err != nil {
if errors.Is(err, model.ErrNotFound) {
// 不是代理,返回空列表
return &types.GetWithdrawListResp{
Total: 0,
List: []types.WithdrawItem{},
}, nil
}
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询代理信息失败: %v", err)
}
// 3. 构建查询条件
builder := l.svcCtx.AgentWithdrawModel.SelectBuilder().
Where("agent_id = ? AND del_state = ?", agent.Id, globalkey.DelStateNo).
OrderBy("create_time DESC")
// 4. 分页参数
page := req.Page
if page <= 0 {
page = 1
}
pageSize := req.PageSize
if pageSize <= 0 {
pageSize = 20
}
offset := (page - 1) * pageSize
// 5. 查询总数
total, err := l.svcCtx.AgentWithdrawModel.FindCount(l.ctx, builder, "id")
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询提现记录总数失败: %v", err)
}
if total == 0 {
return &types.GetWithdrawListResp{
Total: 0,
List: []types.WithdrawItem{},
}, nil
}
// 6. 查询列表
builder = builder.Limit(uint64(pageSize)).Offset(uint64(offset))
withdrawals, err := l.svcCtx.AgentWithdrawModel.FindAll(l.ctx, builder, "")
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询提现记录列表失败: %v", err)
}
// 7. 组装响应
var list []types.WithdrawItem
for _, withdrawal := range withdrawals {
// 处理银行卡号脱敏只显示前4位和后4位
bankCardNumber := withdrawal.BankCardNumber
if len(bankCardNumber) > 8 {
bankCardNumber = fmt.Sprintf("%s****%s", bankCardNumber[:4], bankCardNumber[len(bankCardNumber)-4:])
}
// 处理审核备注
auditRemark := ""
if withdrawal.AuditRemark.Valid {
auditRemark = withdrawal.AuditRemark.String
}
// 处理审核时间
auditTime := ""
if withdrawal.AuditTime.Valid {
auditTime = withdrawal.AuditTime.Time.Format("2006-01-02 15:04:05")
}
list = append(list, types.WithdrawItem{
Id: withdrawal.Id,
WithdrawAmount: fmt.Sprintf("%.2f", withdrawal.WithdrawAmount),
TaxAmount: fmt.Sprintf("%.2f", withdrawal.TaxAmount),
ActualAmount: fmt.Sprintf("%.2f", withdrawal.ActualAmount),
AccountName: withdrawal.AccountName,
BankCardNumber: bankCardNumber,
Status: withdrawal.Status,
AuditRemark: auditRemark,
CreateTime: withdrawal.CreateTime.Format("2006-01-02 15:04:05"),
AuditTime: auditTime,
})
}
return &types.GetWithdrawListResp{
Total: total,
List: list,
}, nil
}

View File

@@ -0,0 +1,102 @@
package agent
import (
"context"
"fmt"
"net/http"
"strings"
"sim-server/app/main/model"
"sim-server/common/globalkey"
"sim-server/common/xerr"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/logx"
"sim-server/app/main/api/internal/svc"
)
type ShortLinkRedirectLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewShortLinkRedirectLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ShortLinkRedirectLogic {
return &ShortLinkRedirectLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
// ShortLinkRedirect 短链重定向
// 从短链重定向到推广页面
func (l *ShortLinkRedirectLogic) ShortLinkRedirect(shortCode string, r *http.Request, w http.ResponseWriter) error {
// 1. 验证短链标识
if shortCode == "" {
return errors.Wrapf(xerr.NewErrCode(xerr.REUQEST_PARAM_ERROR), "缺少短链标识")
}
// 2. 查询短链记录
shortLink, err := l.svcCtx.AgentShortLinkModel.FindOneByShortCodeDelState(l.ctx, shortCode, globalkey.DelStateNo)
if err != nil {
if errors.Is(err, model.ErrNotFound) {
return errors.Wrapf(xerr.NewErrCode(xerr.REUQEST_PARAM_ERROR), "短链不存在或已失效")
}
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询短链失败, %v", err)
}
// 3. 根据类型验证链接有效性
if shortLink.Type == 1 {
// 推广报告类型验证linkIdentifier是否存在
if shortLink.LinkIdentifier.Valid && shortLink.LinkIdentifier.String != "" {
_, err = l.svcCtx.AgentLinkModel.FindOneByLinkIdentifier(l.ctx, shortLink.LinkIdentifier.String)
if err != nil {
if errors.Is(err, model.ErrNotFound) {
return errors.Wrapf(xerr.NewErrCode(xerr.REUQEST_PARAM_ERROR), "推广链接不存在或已失效")
}
l.Errorf("查询推广链接失败: %v", err)
// 即使查询失败,也继续重定向,避免影响用户体验
}
}
}
// 系统简化:移除邀请码验证功能
// 4. 构建重定向URL
redirectURL := shortLink.TargetPath
if redirectURL == "" {
return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "短链目标地址为空")
}
// 如果 target_path 是相对路径,需要拼接正式域名
// 如果 target_path 已经是完整URL则直接使用
if !strings.HasPrefix(redirectURL, "http://") && !strings.HasPrefix(redirectURL, "https://") {
// 相对路径,需要拼接正式域名
officialDomain := l.svcCtx.Config.Promotion.OfficialDomain
if officialDomain == "" {
// 如果没有配置正式域名,使用当前请求的域名(向后兼容)
scheme := "http"
if r.TLS != nil {
scheme = "https"
}
officialDomain = fmt.Sprintf("%s://%s", scheme, r.Host)
}
// 确保正式域名不以 / 结尾
officialDomain = strings.TrimSuffix(officialDomain, "/")
// 确保 target_path 以 / 开头
if !strings.HasPrefix(redirectURL, "/") {
redirectURL = "/" + redirectURL
}
redirectURL = officialDomain + redirectURL
}
// 5. 执行重定向302临时重定向
linkIdentifierStr := ""
if shortLink.LinkIdentifier.Valid {
linkIdentifierStr = shortLink.LinkIdentifier.String
}
l.Infof("短链重定向: shortCode=%s, type=%d, linkIdentifier=%s, redirectURL=%s", shortCode, shortLink.Type, linkIdentifierStr, redirectURL)
http.Redirect(w, r, redirectURL, http.StatusFound)
return nil
}