This commit is contained in:
liangzai
2025-12-10 13:10:39 +08:00
parent c23ab8338b
commit 0f676d3c76
31 changed files with 375 additions and 5916 deletions

9
.gitignore vendored
View File

@@ -6,7 +6,6 @@
**/.DS_Store
#deploy data
data/*
!data/.gitkeep
@@ -19,6 +18,14 @@ data/*
/tmp/
# 打包出来的可执行文件
/app/api
/app/main/api/main
/app/main/api/debug
/app/main/api/test
# 文档目录
documents/*
!documents/.gitkeep
deploy/script/js

View File

@@ -1,131 +0,0 @@
# API调用失败退款时的代理处理确认
## 执行流程分析
### 关键执行顺序
```
1. 支付成功 → 创建代理订单ProcessStatus = 0未处理
2. 支付回调 → 发送查询任务
3. 查询任务执行:
├─ 创建查询记录query表状态为 "pending" ✅ 第77-87行
├─ 生成授权书 ✅ 第108-142行
├─ 调用API第164行 可能失败
│ ├─ 如果成功 → 继续执行
│ └─ 如果失败 → return handleError第166行 直接返回
├─ [API成功才会执行到这里]
├─ 保存响应数据 ✅ 第177-182行
├─ 更新查询状态为 "success" ✅ 第184-189行
└─ 发送代理处理任务 ✅ 第192行关键只有到这里才会发送
```
### 关键代码位置
**第164-167行**
```go
combinedResponse, err := l.svcCtx.ApiRequestService.ProcessRequests(decryptData, product.Id)
if err != nil {
return l.handleError(ctx, err, order, query) // ← 直接返回,不会继续执行
}
```
**第192行发送代理处理任务**
```go
// 报告生成成功后,发送代理处理异步任务(不阻塞报告流程)
if asyncErr := l.svcCtx.AsynqService.SendAgentProcessTask(order.Id); asyncErr != nil {
logx.Errorf("发送代理处理任务失败订单ID: %d, 错误: %v", order.Id, asyncErr)
}
```
---
## 确认结果
### ✅ API调用失败时的情况第166-167行
**执行流程**
1. 支付时创建代理订单(`ProcessStatus = 0`
2. API调用失败第164行
3. **直接返回** `handleError`第166行
4. 进入退款流程
5. **第192行不会执行**因为已经return了
**代理状态**
- ✅ 代理订单 `ProcessStatus = 0`(未处理)
- ✅ 代理**没有收到**佣金
- ✅ 代理**没有收到**返佣
- ✅ 代理处理任务**没有发送**
---
## 业务逻辑分析
### ✅ 符合业务逻辑
**理由**
1. **服务未完成交付**
- API调用失败意味着无法生成查询报告
- 用户支付了订单,但服务没有成功交付
2. **用户权益保护**
- 平台退款给用户是合理的
- 用户没有获得服务,不应该承担费用
3. **代理收益逻辑**
- 代理的收益应该基于**服务成功交付**
- 而不是仅仅因为用户支付了订单
- 如果服务未成功交付,代理不应该获得收益
4. **当前实现正确**
- 只有在查询成功后第184行更新状态为 "success"
- 才会发送代理处理任务第192行
- API调用失败时查询未成功代理任务不会发送
---
## 结论
### ✅ 当前处理完全正确
**对于 API 调用失败退款的情况**
- ✅ 代理订单未处理(`ProcessStatus = 0`
- ✅ 代理没有收到佣金和返佣
- ✅ 代理处理任务没有发送
-**完全符合业务逻辑**
**业务逻辑合理性**
- ✅ 代理收益应该在服务成功交付后才发放
- ✅ API调用失败意味着服务未交付不应发放收益
- ✅ 退款给用户是合理的,保护用户权益
---
## 不需要修改
当前逻辑已经正确处理了这种情况,**无需修改**。
**关键保护点**
1. 代理处理任务只在查询成功后发送第192行
2. API调用失败会直接返回不会执行到第192行
3. 代理订单状态保持为0代理没有收益
---
## 需要注意的其他场景
虽然当前场景处理正确,但需要注意以下场景:
### ⚠️ 场景:代理已处理但订单被退款
如果:
1. API调用成功
2. 查询状态更新为 "success"
3. 发送代理处理任务
4. 代理处理任务执行,发放佣金和返佣(`ProcessStatus = 1`
5. **之后**订单被退款(比如管理员手动退款)
这种情况下需要撤销代理收益(需要另外处理,不是当前场景)。

View File

@@ -1,22 +1,22 @@
package agent
import (
"context"
"database/sql"
"fmt"
"os"
"time"
"ycc-server/app/main/model"
"ycc-server/common/ctxdata"
"ycc-server/common/globalkey"
"ycc-server/common/xerr"
"ycc-server/pkg/lzkit/crypto"
"strconv"
"context"
"database/sql"
"fmt"
"os"
"strconv"
"time"
"ycc-server/app/main/model"
"ycc-server/common/ctxdata"
"ycc-server/common/globalkey"
"ycc-server/common/xerr"
"ycc-server/pkg/lzkit/crypto"
"github.com/google/uuid"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/stores/redis"
"github.com/zeromicro/go-zero/core/stores/sqlx"
"github.com/google/uuid"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/stores/redis"
"github.com/zeromicro/go-zero/core/stores/sqlx"
"ycc-server/app/main/api/internal/svc"
"ycc-server/app/main/api/internal/types"
@@ -50,9 +50,9 @@ func (l *ApplyForAgentLogic) ApplyForAgent(req *types.AgentApplyReq) (resp *type
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "加密手机号失败: %v", err)
}
if req.Referrer == "" {
return nil, errors.Wrapf(xerr.NewErrMsg("请填写邀请信息"), "")
}
if req.Referrer == "" {
return nil, errors.Wrapf(xerr.NewErrMsg("请填写邀请信息"), "")
}
// 2. 校验验证码(开发环境下跳过验证码校验)
if os.Getenv("ENV") != "development" {
@@ -115,49 +115,49 @@ func (l *ApplyForAgentLogic) ApplyForAgent(req *types.AgentApplyReq) (resp *type
return errors.Wrapf(xerr.NewErrMsg("您已经是代理"), "")
}
var inviteCodeModel *model.AgentInviteCode
var parentAgentId string
var targetLevel int64
var inviteCodeModel *model.AgentInviteCode
var parentAgentId string
var targetLevel int64
inviteCodeModel, err = l.svcCtx.AgentInviteCodeModel.FindOneByCode(transCtx, req.Referrer)
if err != nil && !errors.Is(err, model.ErrNotFound) {
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询邀请码失败, %v", err)
}
if inviteCodeModel != nil {
if inviteCodeModel.Status != 0 {
if inviteCodeModel.Status == 1 {
return errors.Wrapf(xerr.NewErrMsg("邀请码已使用"), "")
}
return errors.Wrapf(xerr.NewErrMsg("邀请码已失效"), "")
}
if inviteCodeModel.ExpireTime.Valid && inviteCodeModel.ExpireTime.Time.Before(time.Now()) {
return errors.Wrapf(xerr.NewErrMsg("邀请码已过期"), "")
}
targetLevel = inviteCodeModel.TargetLevel
if inviteCodeModel.AgentId.Valid {
parentAgentId = inviteCodeModel.AgentId.String
}
} else {
if codeVal, parseErr := strconv.ParseInt(req.Referrer, 10, 64); parseErr == nil && codeVal > 0 {
parentAgent, err := l.findAgentByCode(transCtx, codeVal)
if err != nil {
return err
}
parentAgentId = parentAgent.Id
targetLevel = 1
} else {
encRefMobile, _ := crypto.EncryptMobile(req.Referrer, l.svcCtx.Config.Encrypt.SecretKey)
agents, findErr := l.svcCtx.AgentModel.FindAll(transCtx, l.svcCtx.AgentModel.SelectBuilder().Where("mobile = ? AND del_state = ?", encRefMobile, globalkey.DelStateNo).Limit(1), "")
if findErr != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询上级代理失败, %v", findErr)
}
if len(agents) == 0 {
return errors.Wrapf(xerr.NewErrMsg("邀请信息无效"), "")
}
parentAgentId = agents[0].Id
targetLevel = 1
}
}
inviteCodeModel, err = l.svcCtx.AgentInviteCodeModel.FindOneByCode(transCtx, req.Referrer)
if err != nil && !errors.Is(err, model.ErrNotFound) {
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询邀请码失败, %v", err)
}
if inviteCodeModel != nil {
if inviteCodeModel.Status != 0 {
if inviteCodeModel.Status == 1 {
return errors.Wrapf(xerr.NewErrMsg("邀请码已使用"), "")
}
return errors.Wrapf(xerr.NewErrMsg("邀请码已失效"), "")
}
if inviteCodeModel.ExpireTime.Valid && inviteCodeModel.ExpireTime.Time.Before(time.Now()) {
return errors.Wrapf(xerr.NewErrMsg("邀请码已过期"), "")
}
targetLevel = inviteCodeModel.TargetLevel
if inviteCodeModel.AgentId.Valid {
parentAgentId = inviteCodeModel.AgentId.String
}
} else {
if codeVal, parseErr := strconv.ParseInt(req.Referrer, 10, 64); parseErr == nil && codeVal > 0 {
parentAgent, err := l.findAgentByCode(transCtx, codeVal)
if err != nil {
return err
}
parentAgentId = parentAgent.Id
targetLevel = 1
} else {
encRefMobile, _ := crypto.EncryptMobile(req.Referrer, l.svcCtx.Config.Encrypt.SecretKey)
agents, findErr := l.svcCtx.AgentModel.FindAll(transCtx, l.svcCtx.AgentModel.SelectBuilder().Where("mobile = ? AND del_state = ?", encRefMobile, globalkey.DelStateNo).Limit(1), "")
if findErr != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询上级代理失败, %v", findErr)
}
if len(agents) == 0 {
return errors.Wrapf(xerr.NewErrMsg("邀请信息无效"), "")
}
parentAgentId = agents[0].Id
targetLevel = 1
}
}
// 4.5 创建代理记录
newAgent := &model.Agent{
@@ -279,7 +279,7 @@ func (l *ApplyForAgentLogic) ApplyForAgent(req *types.AgentApplyReq) (resp *type
}
// 6. 生成并返回token
token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID, model.UserTypeNormal)
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)
}

View File

@@ -105,10 +105,16 @@ func (l *RegisterByInviteCodeLogic) RegisterByInviteCode(req *types.RegisterByIn
return errors.Wrapf(xerr.NewErrMsg("您已经是代理"), "")
}
// 如果是临时用户(微信环境下),检查手机号是否已绑定其他微信号,并绑定临时用户到正式用户
// 注意:非微信环境下 claims 为 nil此逻辑不会执行直接使用已存在的 user.Id
// 检查用户是否有mobile绑定没有mobile则不能成为代理
// 如果是临时用户(微信环境下),需要先绑定手机号
claims, err := ctxdata.GetClaimsFromCtx(l.ctx)
if err == nil && claims != nil && claims.UserType == model.UserTypeTemp {
if err == nil && claims != nil {
// 获取用户的mobile信息
if !user.Mobile.Valid || user.Mobile.String == "" {
// 临时用户无mobile不能直接成为代理需要先绑定mobile
return errors.Wrapf(xerr.NewErrMsg("请先绑定手机号后再申请成为代理"), "")
}
// 检查是否已绑定手机号认证(用于确保后续可通过手机号登录)
userAuth, err := l.svcCtx.UserAuthModel.FindOneByUserIdAuthType(l.ctx, user.Id, claims.AuthType)
if err != nil && !errors.Is(err, model.ErrNotFound) {
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询用户认证失败, %v", err)
@@ -116,11 +122,6 @@ func (l *RegisterByInviteCodeLogic) RegisterByInviteCode(req *types.RegisterByIn
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
@@ -263,7 +264,7 @@ func (l *RegisterByInviteCodeLogic) RegisterByInviteCode(req *types.RegisterByIn
}
// 5. 生成并返回token
token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID, model.UserTypeNormal)
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)
}

View File

@@ -8,7 +8,6 @@ import (
"os"
"time"
"ycc-server/app/main/api/internal/service"
"ycc-server/app/main/model"
"ycc-server/common/ctxdata"
"ycc-server/common/xerr"
"ycc-server/pkg/lzkit/crypto"
@@ -60,60 +59,60 @@ var productProcessors = map[string]func(*QueryServiceLogic, *types.QueryServiceR
func (l *QueryServiceLogic) PreprocessLogic(req *types.QueryServiceReq, product string) (*types.QueryServiceResp, error) {
if processor, exists := productProcessors[product]; exists {
return processor(l, req) // 调用对应的处理函数
return processor(l, req) // 璋冪敤瀵瑰簲鐨勫鐞嗗嚱鏁?
}
return nil, errors.New("未找到相应的处理程序")
return nil, errors.New("鏈壘鍒扮浉搴旂殑澶勭悊绋嬪簭")
}
func (l *QueryServiceLogic) ProcessMarriageLogic(req *types.QueryServiceReq) (*types.QueryServiceResp, error) {
// AES解密
// AES瑙e瘑
decryptData, DecryptDataErr := l.DecryptData(req.Data)
if DecryptDataErr != nil {
return nil, DecryptDataErr
}
// 校验参数
// 鏍¢獙鍙傛暟
var data types.MarriageReq
if unmarshalErr := json.Unmarshal(decryptData, &data); unmarshalErr != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 解密后的数据格式不正确: %+v", unmarshalErr)
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "鏌ヨ鏈嶅姟, 瑙e瘑鍚庣殑鏁版嵁鏍煎紡涓嶆纭? %+v", unmarshalErr)
}
if validatorErr := validator.Validate(data); validatorErr != nil {
return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, validatorErr.Error()), "查询服务, 参数不正确: %+v", validatorErr)
return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, validatorErr.Error()), "鏌ヨ鏈嶅姟, 鍙傛暟涓嶆纭? %+v", validatorErr)
}
// 校验验证码
// 鏍¢獙楠岃瘉鐮?
verifyCodeErr := l.VerifyCode(data.Mobile, data.Code)
if verifyCodeErr != nil {
return nil, verifyCodeErr
}
// 校验三要素
// 鏍¢獙涓夎绱?
verifyErr := l.Verify(data.Name, data.IDCard, data.Mobile)
if verifyErr != nil {
return nil, verifyErr
}
// 缓存
// 缂撳瓨
params := map[string]interface{}{
"name": data.Name,
"id_card": data.IDCard,
"mobile": data.Mobile,
}
userID, userType, err := l.GetOrCreateUser()
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 处理用户失败: %v", err)
}
userID, err := l.GetOrCreateUser()
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "鏌ヨ鏈嶅姟, 澶勭悊鐢ㄦ埛澶辫触: %v", err)
}
cacheNo, cacheDataErr := l.CacheData(params, "marriage", userID)
if cacheDataErr != nil {
return nil, cacheDataErr
}
token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID, userType)
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)
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "鏌ヨ鏈嶅姟, 鐢熸垚token澶辫触 : %v", err)
}
// 获取当前时间戳
// 鑾峰彇褰撳墠鏃堕棿鎴?
now := time.Now().Unix()
return &types.QueryServiceResp{
Id: cacheNo,
@@ -123,58 +122,58 @@ func (l *QueryServiceLogic) ProcessMarriageLogic(req *types.QueryServiceReq) (*t
}, nil
}
// 处理家政服务相关逻辑
// 澶勭悊瀹舵斂鏈嶅姟鐩稿叧閫昏緫
func (l *QueryServiceLogic) ProcessHomeServiceLogic(req *types.QueryServiceReq) (*types.QueryServiceResp, error) {
// AES解密
// AES瑙e瘑
decryptData, DecryptDataErr := l.DecryptData(req.Data)
if DecryptDataErr != nil {
return nil, DecryptDataErr
}
// 校验参数
// 鏍¢獙鍙傛暟
var data types.HomeServiceReq
if unmarshalErr := json.Unmarshal(decryptData, &data); unmarshalErr != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 解密后的数据格式不正确: %+v", unmarshalErr)
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "鏌ヨ鏈嶅姟, 瑙e瘑鍚庣殑鏁版嵁鏍煎紡涓嶆纭? %+v", unmarshalErr)
}
if validatorErr := validator.Validate(data); validatorErr != nil {
return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, validatorErr.Error()), "查询服务, 参数不正确: %+v", validatorErr)
return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, validatorErr.Error()), "鏌ヨ鏈嶅姟, 鍙傛暟涓嶆纭? %+v", validatorErr)
}
// 校验验证码
// 鏍¢獙楠岃瘉鐮?
verifyCodeErr := l.VerifyCode(data.Mobile, data.Code)
if verifyCodeErr != nil {
return nil, verifyCodeErr
}
// 校验三要素
// 鏍¢獙涓夎绱?
verifyErr := l.Verify(data.Name, data.IDCard, data.Mobile)
if verifyErr != nil {
return nil, verifyErr
}
// 缓存
// 缂撳瓨
params := map[string]interface{}{
"name": data.Name,
"id_card": data.IDCard,
"mobile": data.Mobile,
}
userID, userType, err := l.GetOrCreateUser()
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 处理用户失败: %v", err)
}
userID, err := l.GetOrCreateUser()
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "鏌ヨ鏈嶅姟, 澶勭悊鐢ㄦ埛澶辫触: %v", err)
}
cacheNo, cacheDataErr := l.CacheData(params, "homeservice", userID)
if cacheDataErr != nil {
return nil, cacheDataErr
}
token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID, userType)
token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 生成token失败 : %d", userID)
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "鏌ヨ鏈嶅姟, 鐢熸垚token澶辫触 : %d", userID)
}
// 获取当前时间戳
// 鑾峰彇褰撳墠鏃堕棿鎴?
now := time.Now().Unix()
return &types.QueryServiceResp{
Id: cacheNo,
@@ -184,58 +183,58 @@ func (l *QueryServiceLogic) ProcessHomeServiceLogic(req *types.QueryServiceReq)
}, nil
}
// 处理风险评估相关逻辑
// 澶勭悊椋庨櫓璇勪及鐩稿叧閫昏緫
func (l *QueryServiceLogic) ProcessRiskAssessmentLogic(req *types.QueryServiceReq) (*types.QueryServiceResp, error) {
// AES解密
// AES瑙e瘑
decryptData, DecryptDataErr := l.DecryptData(req.Data)
if DecryptDataErr != nil {
return nil, DecryptDataErr
}
// 校验参数
// 鏍¢獙鍙傛暟
var data types.RiskAssessmentReq
if unmarshalErr := json.Unmarshal(decryptData, &data); unmarshalErr != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 解密后的数据格式不正确: %+v", unmarshalErr)
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "鏌ヨ鏈嶅姟, 瑙e瘑鍚庣殑鏁版嵁鏍煎紡涓嶆纭? %+v", unmarshalErr)
}
if validatorErr := validator.Validate(data); validatorErr != nil {
return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, validatorErr.Error()), "查询服务, 参数不正确: %+v", validatorErr)
return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, validatorErr.Error()), "鏌ヨ鏈嶅姟, 鍙傛暟涓嶆纭? %+v", validatorErr)
}
// 校验验证码
// 鏍¢獙楠岃瘉鐮?
verifyCodeErr := l.VerifyCode(data.Mobile, data.Code)
if verifyCodeErr != nil {
return nil, verifyCodeErr
}
// 校验三要素
// 鏍¢獙涓夎绱?
verifyErr := l.Verify(data.Name, data.IDCard, data.Mobile)
if verifyErr != nil {
return nil, verifyErr
}
// 缓存
// 缂撳瓨
params := map[string]interface{}{
"name": data.Name,
"id_card": data.IDCard,
"mobile": data.Mobile,
}
userID, userType, err := l.GetOrCreateUser()
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 处理用户失败: %v", err)
}
userID, err := l.GetOrCreateUser()
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "鏌ヨ鏈嶅姟, 澶勭悊鐢ㄦ埛澶辫触: %v", err)
}
cacheNo, cacheDataErr := l.CacheData(params, "riskassessment", userID)
if cacheDataErr != nil {
return nil, cacheDataErr
}
token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID, userType)
token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 生成token失败 : %d", userID)
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "鏌ヨ鏈嶅姟, 鐢熸垚token澶辫触 : %d", userID)
}
// 获取当前时间戳
// 鑾峰彇褰撳墠鏃堕棿鎴?
now := time.Now().Unix()
return &types.QueryServiceResp{
Id: cacheNo,
@@ -245,57 +244,57 @@ func (l *QueryServiceLogic) ProcessRiskAssessmentLogic(req *types.QueryServiceRe
}, nil
}
// 处理公司信息查询相关逻辑
// 澶勭悊鍏徃淇℃伅鏌ヨ鐩稿叧閫昏緫
func (l *QueryServiceLogic) ProcessCompanyInfoLogic(req *types.QueryServiceReq) (*types.QueryServiceResp, error) {
// AES解密
// AES瑙e瘑
decryptData, DecryptDataErr := l.DecryptData(req.Data)
if DecryptDataErr != nil {
return nil, DecryptDataErr
}
// 校验参数
// 鏍¢獙鍙傛暟
var data types.CompanyInfoReq
if unmarshalErr := json.Unmarshal(decryptData, &data); unmarshalErr != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 解密后的数据格式不正确: %+v", unmarshalErr)
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "鏌ヨ鏈嶅姟, 瑙e瘑鍚庣殑鏁版嵁鏍煎紡涓嶆纭? %+v", unmarshalErr)
}
if validatorErr := validator.Validate(data); validatorErr != nil {
return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, validatorErr.Error()), "查询服务, 参数不正确: %+v", validatorErr)
return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, validatorErr.Error()), "鏌ヨ鏈嶅姟, 鍙傛暟涓嶆纭? %+v", validatorErr)
}
// 校验验证码
// 鏍¢獙楠岃瘉鐮?
verifyCodeErr := l.VerifyCode(data.Mobile, data.Code)
if verifyCodeErr != nil {
return nil, verifyCodeErr
}
// 校验三要素
// 鏍¢獙涓夎绱?
verifyErr := l.Verify(data.Name, data.IDCard, data.Mobile)
if verifyErr != nil {
return nil, verifyErr
}
// 缓存
// 缂撳瓨
params := map[string]interface{}{
"name": data.Name,
"id_card": data.IDCard,
"mobile": data.Mobile,
}
userID, userType, err := l.GetOrCreateUser()
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 处理用户失败: %v", err)
}
userID, err := l.GetOrCreateUser()
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "鏌ヨ鏈嶅姟, 澶勭悊鐢ㄦ埛澶辫触: %v", err)
}
cacheNo, cacheDataErr := l.CacheData(params, "companyinfo", userID)
if cacheDataErr != nil {
return nil, cacheDataErr
}
token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID, userType)
token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 生成token失败 : %d", userID)
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "鏌ヨ鏈嶅姟, 鐢熸垚token澶辫触 : %d", userID)
}
// 获取当前时间戳
// 鑾峰彇褰撳墠鏃堕棿鎴?
now := time.Now().Unix()
return &types.QueryServiceResp{
Id: cacheNo,
@@ -305,58 +304,58 @@ func (l *QueryServiceLogic) ProcessCompanyInfoLogic(req *types.QueryServiceReq)
}, nil
}
// 处理租赁信息查询相关逻辑
// 澶勭悊绉熻祦淇℃伅鏌ヨ鐩稿叧閫昏緫
func (l *QueryServiceLogic) ProcessRentalInfoLogic(req *types.QueryServiceReq) (*types.QueryServiceResp, error) {
// AES解密
// AES瑙e瘑
decryptData, DecryptDataErr := l.DecryptData(req.Data)
if DecryptDataErr != nil {
return nil, DecryptDataErr
}
// 校验参数
// 鏍¢獙鍙傛暟
var data types.RentalInfoReq
if unmarshalErr := json.Unmarshal(decryptData, &data); unmarshalErr != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 解密后的数据格式不正确: %+v", unmarshalErr)
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "鏌ヨ鏈嶅姟, 瑙e瘑鍚庣殑鏁版嵁鏍煎紡涓嶆纭? %+v", unmarshalErr)
}
if validatorErr := validator.Validate(data); validatorErr != nil {
return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, validatorErr.Error()), "查询服务, 参数不正确: %+v", validatorErr)
return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, validatorErr.Error()), "鏌ヨ鏈嶅姟, 鍙傛暟涓嶆纭? %+v", validatorErr)
}
// 校验验证码
// 鏍¢獙楠岃瘉鐮?
verifyCodeErr := l.VerifyCode(data.Mobile, data.Code)
if verifyCodeErr != nil {
return nil, verifyCodeErr
}
// 校验三要素
// 鏍¢獙涓夎绱?
verifyErr := l.Verify(data.Name, data.IDCard, data.Mobile)
if verifyErr != nil {
return nil, verifyErr
}
// 缓存
// 缂撳瓨
params := map[string]interface{}{
"name": data.Name,
"id_card": data.IDCard,
"mobile": data.Mobile,
}
userID, userType, err := l.GetOrCreateUser()
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 处理用户失败: %v", err)
}
userID, err := l.GetOrCreateUser()
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "鏌ヨ鏈嶅姟, 澶勭悊鐢ㄦ埛澶辫触: %v", err)
}
cacheNo, cacheDataErr := l.CacheData(params, "rentalinfo", userID)
if cacheDataErr != nil {
return nil, cacheDataErr
}
token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID, userType)
token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 生成token失败 : %d", userID)
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "鏌ヨ鏈嶅姟, 鐢熸垚token澶辫触 : %d", userID)
}
// 获取当前时间戳
// 鑾峰彇褰撳墠鏃堕棿鎴?
now := time.Now().Unix()
return &types.QueryServiceResp{
Id: cacheNo,
@@ -366,58 +365,58 @@ func (l *QueryServiceLogic) ProcessRentalInfoLogic(req *types.QueryServiceReq) (
}, nil
}
// 处理贷前背景检查相关逻辑
// 澶勭悊璐峰墠鑳屾櫙妫€鏌ョ浉鍏抽€昏緫
func (l *QueryServiceLogic) ProcessPreLoanBackgroundCheckLogic(req *types.QueryServiceReq) (*types.QueryServiceResp, error) {
// AES解密
// AES瑙e瘑
decryptData, DecryptDataErr := l.DecryptData(req.Data)
if DecryptDataErr != nil {
return nil, DecryptDataErr
}
// 校验参数
// 鏍¢獙鍙傛暟
var data types.PreLoanBackgroundCheckReq
if unmarshalErr := json.Unmarshal(decryptData, &data); unmarshalErr != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 解密后的数据格式不正确: %+v", unmarshalErr)
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "鏌ヨ鏈嶅姟, 瑙e瘑鍚庣殑鏁版嵁鏍煎紡涓嶆纭? %+v", unmarshalErr)
}
if validatorErr := validator.Validate(data); validatorErr != nil {
return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, validatorErr.Error()), "查询服务, 参数不正确: %+v", validatorErr)
return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, validatorErr.Error()), "鏌ヨ鏈嶅姟, 鍙傛暟涓嶆纭? %+v", validatorErr)
}
// 校验验证码
// 鏍¢獙楠岃瘉鐮?
verifyCodeErr := l.VerifyCode(data.Mobile, data.Code)
if verifyCodeErr != nil {
return nil, verifyCodeErr
}
// 校验三要素
// 鏍¢獙涓夎绱?
verifyErr := l.Verify(data.Name, data.IDCard, data.Mobile)
if verifyErr != nil {
return nil, verifyErr
}
// 缓存
// 缂撳瓨
params := map[string]interface{}{
"name": data.Name,
"id_card": data.IDCard,
"mobile": data.Mobile,
}
userID, userType, err := l.GetOrCreateUser()
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 处理用户失败: %v", err)
}
userID, err := l.GetOrCreateUser()
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "鏌ヨ鏈嶅姟, 澶勭悊鐢ㄦ埛澶辫触: %v", err)
}
cacheNo, cacheDataErr := l.CacheData(params, "preloanbackgroundcheck", userID)
if cacheDataErr != nil {
return nil, cacheDataErr
}
token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID, userType)
token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 生成token失败 : %d", userID)
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "鏌ヨ鏈嶅姟, 鐢熸垚token澶辫触 : %d", userID)
}
// 获取当前时间戳
// 鑾峰彇褰撳墠鏃堕棿鎴?
now := time.Now().Unix()
return &types.QueryServiceResp{
Id: cacheNo,
@@ -427,57 +426,57 @@ func (l *QueryServiceLogic) ProcessPreLoanBackgroundCheckLogic(req *types.QueryS
}, nil
}
// 处理人事背调相关逻辑
// 澶勭悊浜轰簨鑳岃皟鐩稿叧閫昏緫
func (l *QueryServiceLogic) ProcessBackgroundCheckLogic(req *types.QueryServiceReq) (*types.QueryServiceResp, error) {
// AES解密
// AES瑙e瘑
decryptData, DecryptDataErr := l.DecryptData(req.Data)
if DecryptDataErr != nil {
return nil, DecryptDataErr
}
// 校验参数
// 鏍¢獙鍙傛暟
var data types.BackgroundCheckReq
if unmarshalErr := json.Unmarshal(decryptData, &data); unmarshalErr != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 解密后的数据格式不正确: %+v", unmarshalErr)
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "鏌ヨ鏈嶅姟, 瑙e瘑鍚庣殑鏁版嵁鏍煎紡涓嶆纭? %+v", unmarshalErr)
}
if validatorErr := validator.Validate(data); validatorErr != nil {
return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, validatorErr.Error()), "查询服务, 参数不正确: %+v", validatorErr)
return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, validatorErr.Error()), "鏌ヨ鏈嶅姟, 鍙傛暟涓嶆纭? %+v", validatorErr)
}
// 校验验证码
// 鏍¢獙楠岃瘉鐮?
verifyCodeErr := l.VerifyCode(data.Mobile, data.Code)
if verifyCodeErr != nil {
return nil, verifyCodeErr
}
// 校验三要素
// 鏍¢獙涓夎绱?
verifyErr := l.Verify(data.Name, data.IDCard, data.Mobile)
if verifyErr != nil {
return nil, verifyErr
}
// 缓存
// 缂撳瓨
params := map[string]interface{}{
"name": data.Name,
"id_card": data.IDCard,
"mobile": data.Mobile,
}
userID, userType, err := l.GetOrCreateUser()
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 处理用户失败: %v", err)
}
userID, err := l.GetOrCreateUser()
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "鏌ヨ鏈嶅姟, 澶勭悊鐢ㄦ埛澶辫触: %v", err)
}
cacheNo, cacheDataErr := l.CacheData(params, "backgroundcheck", userID)
if cacheDataErr != nil {
return nil, cacheDataErr
}
token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID, userType)
token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 生成token失败 : %d", userID)
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "鏌ヨ鏈嶅姟, 鐢熸垚token澶辫触 : %d", userID)
}
// 获取当前时间戳
// 鑾峰彇褰撳墠鏃堕棿鎴?
now := time.Now().Unix()
return &types.QueryServiceResp{
Id: cacheNo,
@@ -487,55 +486,55 @@ func (l *QueryServiceLogic) ProcessBackgroundCheckLogic(req *types.QueryServiceR
}, nil
}
func (l *QueryServiceLogic) ProcessPersonalDataLogic(req *types.QueryServiceReq) (*types.QueryServiceResp, error) {
// AES解密
// AES瑙e瘑
decryptData, DecryptDataErr := l.DecryptData(req.Data)
if DecryptDataErr != nil {
return nil, DecryptDataErr
}
// 校验参数
// 鏍¢獙鍙傛暟
var data types.PersonalDataReq
if unmarshalErr := json.Unmarshal(decryptData, &data); unmarshalErr != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 解密后的数据格式不正确: %+v", unmarshalErr)
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "鏌ヨ鏈嶅姟, 瑙e瘑鍚庣殑鏁版嵁鏍煎紡涓嶆纭? %+v", unmarshalErr)
}
if validatorErr := validator.Validate(data); validatorErr != nil {
return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, validatorErr.Error()), "查询服务, 参数不正确: %+v", validatorErr)
return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, validatorErr.Error()), "鏌ヨ鏈嶅姟, 鍙傛暟涓嶆纭? %+v", validatorErr)
}
// 校验验证码
// 鏍¢獙楠岃瘉鐮?
verifyCodeErr := l.VerifyCode(data.Mobile, data.Code)
if verifyCodeErr != nil {
return nil, verifyCodeErr
}
// 校验三要素
// 鏍¢獙涓夎绱?
verifyErr := l.Verify(data.Name, data.IDCard, data.Mobile)
if verifyErr != nil {
return nil, verifyErr
}
// 缓存
// 缂撳瓨
params := map[string]interface{}{
"name": data.Name,
"id_card": data.IDCard,
"mobile": data.Mobile,
}
userID, userType, err := l.GetOrCreateUser()
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 处理用户失败: %v", err)
}
userID, err := l.GetOrCreateUser()
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "鏌ヨ鏈嶅姟, 澶勭悊鐢ㄦ埛澶辫触: %v", err)
}
cacheNo, cacheDataErr := l.CacheData(params, "personalData", userID)
if cacheDataErr != nil {
return nil, cacheDataErr
}
token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID, userType)
token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 生成token失败 : %d", userID)
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "鏌ヨ鏈嶅姟, 鐢熸垚token澶辫触 : %d", userID)
}
// 获取当前时间戳
// 鑾峰彇褰撳墠鏃堕棿鎴?
now := time.Now().Unix()
return &types.QueryServiceResp{
Id: cacheNo,
@@ -545,55 +544,55 @@ func (l *QueryServiceLogic) ProcessPersonalDataLogic(req *types.QueryServiceReq)
}, nil
}
func (l *QueryServiceLogic) ProcessConsumerFinanceReportLogic(req *types.QueryServiceReq) (*types.QueryServiceResp, error) {
// AES解密
// AES瑙e瘑
decryptData, DecryptDataErr := l.DecryptData(req.Data)
if DecryptDataErr != nil {
return nil, DecryptDataErr
}
// 校验参数
// 鏍¢獙鍙傛暟
var data types.ConsumerFinanceReportReq
if unmarshalErr := json.Unmarshal(decryptData, &data); unmarshalErr != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 解密后的数据格式不正确: %+v", unmarshalErr)
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "鏌ヨ鏈嶅姟, 瑙e瘑鍚庣殑鏁版嵁鏍煎紡涓嶆纭? %+v", unmarshalErr)
}
if validatorErr := validator.Validate(data); validatorErr != nil {
return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, validatorErr.Error()), "查询服务, 参数不正确: %+v", validatorErr)
return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, validatorErr.Error()), "鏌ヨ鏈嶅姟, 鍙傛暟涓嶆纭? %+v", validatorErr)
}
// 校验验证码
// 鏍¢獙楠岃瘉鐮?
verifyCodeErr := l.VerifyCode(data.Mobile, data.Code)
if verifyCodeErr != nil {
return nil, verifyCodeErr
}
// 校验三要素
// 鏍¢獙涓夎绱?
verifyErr := l.Verify(data.Name, data.IDCard, data.Mobile)
if verifyErr != nil {
return nil, verifyErr
}
// 缓存
// 缂撳瓨
params := map[string]interface{}{
"name": data.Name,
"id_card": data.IDCard,
"mobile": data.Mobile,
}
userID, userType, err := l.GetOrCreateUser()
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 处理用户失败: %v", err)
}
userID, err := l.GetOrCreateUser()
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "鏌ヨ鏈嶅姟, 澶勭悊鐢ㄦ埛澶辫触: %v", err)
}
cacheNo, cacheDataErr := l.CacheData(params, "consumerFinanceReport", userID)
if cacheDataErr != nil {
return nil, cacheDataErr
}
token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID, userType)
token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 生成token失败 : %d", userID)
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "鏌ヨ鏈嶅姟, 鐢熸垚token澶辫触 : %d", userID)
}
// 获取当前时间戳
// 鑾峰彇褰撳墠鏃堕棿鎴?
now := time.Now().Unix()
return &types.QueryServiceResp{
Id: cacheNo,
@@ -606,43 +605,43 @@ func (l *QueryServiceLogic) DecryptData(data string) ([]byte, error) {
secretKey := l.svcCtx.Config.Encrypt.SecretKey
key, decodeErr := hex.DecodeString(secretKey)
if decodeErr != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "密钥获取失败: %+v", decodeErr)
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "瀵嗛挜鑾峰彇澶辫触: %+v", decodeErr)
}
decryptData, aesDecryptErr := crypto.AesDecrypt(data, key)
if aesDecryptErr != nil || len(decryptData) == 0 {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "解密失败: %+v", aesDecryptErr)
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "瑙e瘑澶辫触: %+v", aesDecryptErr)
}
return decryptData, nil
}
// 校验验证码
// 鏍¢獙楠岃瘉鐮?
func (l *QueryServiceLogic) VerifyCode(mobile string, code string) error {
// 开发环境下跳过验证码校验
// 寮€鍙戠幆澧冧笅璺宠繃楠岃瘉鐮佹牎楠?
if os.Getenv("ENV") == "development" {
return nil
}
secretKey := l.svcCtx.Config.Encrypt.SecretKey
encryptedMobile, err := crypto.EncryptMobile(mobile, secretKey)
if err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "加密手机号失败: %+v", err)
return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "鍔犲瘑鎵嬫満鍙峰け璐? %+v", err)
}
codeRedisKey := fmt.Sprintf("%s:%s", "query", encryptedMobile)
cacheCode, err := l.svcCtx.Redis.Get(codeRedisKey)
if err != nil {
if errors.Is(err, redis.Nil) {
return errors.Wrapf(xerr.NewErrMsg("验证码已过期"), "验证码过期: %s", mobile)
return errors.Wrapf(xerr.NewErrMsg("楠岃瘉鐮佸凡杩囨湡"), "楠岃瘉鐮佽繃鏈? %s", mobile)
}
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "读取验证码redis缓存失败, mobile: %s, err: %+v", mobile, err)
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "璇诲彇楠岃瘉鐮乺edis缂撳瓨澶辫触, mobile: %s, err: %+v", mobile, err)
}
if cacheCode != code {
return errors.Wrapf(xerr.NewErrMsg("验证码不正确"), "验证码不正确: %s", mobile)
return errors.Wrapf(xerr.NewErrMsg("楠岃瘉鐮佷笉姝g‘"), "楠岃瘉鐮佷笉姝g‘: %s", mobile)
}
return nil
}
// 二、三要素验证
// 浜屻€佷笁瑕佺礌楠岃瘉
func (l *QueryServiceLogic) Verify(Name string, IDCard string, Mobile string) error {
// 开发环境下跳过二/三要素验证
// 寮€鍙戠幆澧冧笅璺宠繃浜?涓夎绱犻獙璇?
if os.Getenv("ENV") == "development" {
return nil
}
@@ -653,13 +652,13 @@ func (l *QueryServiceLogic) Verify(Name string, IDCard string, Mobile string) er
}
verification, err := l.svcCtx.VerificationService.TwoFactorVerification(twoVerification)
if err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "二要素验证失败: %v", err)
return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "浜岃绱犻獙璇佸け璐? %v", err)
}
if !verification.Passed {
return errors.Wrapf(xerr.NewErrCodeMsg(xerr.SERVER_COMMON_ERROR, verification.Err.Error()), "二要素验证不通过: %v", err)
return errors.Wrapf(xerr.NewErrCodeMsg(xerr.SERVER_COMMON_ERROR, verification.Err.Error()), "浜岃绱犻獙璇佷笉閫氳繃: %v", err)
}
} else {
// 三要素验证
// 涓夎绱犻獙璇?
threeVerification := service.ThreeFactorVerificationRequest{
Name: Name,
IDCard: IDCard,
@@ -667,30 +666,30 @@ func (l *QueryServiceLogic) Verify(Name string, IDCard string, Mobile string) er
}
verification, err := l.svcCtx.VerificationService.ThreeFactorVerification(threeVerification)
if err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "三要素验证失败: %v", err)
return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "涓夎绱犻獙璇佸け璐? %v", err)
}
if !verification.Passed {
return errors.Wrapf(xerr.NewErrCodeMsg(xerr.SERVER_COMMON_ERROR, verification.Err.Error()), "三要素验证不通过: %v", err)
return errors.Wrapf(xerr.NewErrCodeMsg(xerr.SERVER_COMMON_ERROR, verification.Err.Error()), "涓夎绱犻獙璇佷笉閫氳繃: %v", err)
}
}
return nil
}
// 缓存
// 缂撳瓨
func (l *QueryServiceLogic) CacheData(params map[string]interface{}, Product string, userID string) (string, error) {
agentIdentifier, _ := l.ctx.Value("agentIdentifier").(string)
secretKey := l.svcCtx.Config.Encrypt.SecretKey
key, decodeErr := hex.DecodeString(secretKey)
if decodeErr != nil {
return "", errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 获取AES密钥失败: %+v", decodeErr)
return "", errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "鏌ヨ鏈嶅姟, 鑾峰彇AES瀵嗛挜澶辫触: %+v", decodeErr)
}
paramsMarshal, marshalErr := json.Marshal(params)
if marshalErr != nil {
return "", errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 序列化参数失败: %+v", marshalErr)
return "", errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "鏌ヨ鏈嶅姟, 搴忓垪鍖栧弬鏁板け璐? %+v", marshalErr)
}
encryptParams, aesEncryptErr := crypto.AesEncrypt(paramsMarshal, key)
if aesEncryptErr != nil {
return "", errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 加密参数失败: %+v", aesEncryptErr)
return "", errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "鏌ヨ鏈嶅姟, 鍔犲瘑鍙傛暟澶辫触: %+v", aesEncryptErr)
}
queryCache := types.QueryCacheLoad{
Params: encryptParams,
@@ -699,7 +698,7 @@ func (l *QueryServiceLogic) CacheData(params map[string]interface{}, Product str
}
jsonData, marshalErr := json.Marshal(queryCache)
if marshalErr != nil {
return "", errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 序列化参数失败: %+v", marshalErr)
return "", errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "鏌ヨ鏈嶅姟, 搴忓垪鍖栧弬鏁板け璐? %+v", marshalErr)
}
outTradeNo := "Q_" + l.svcCtx.AlipayService.GenerateOutTradeNo()
redisKey := fmt.Sprintf(types.QueryCacheKey, userID, outTradeNo)
@@ -710,18 +709,18 @@ func (l *QueryServiceLogic) CacheData(params map[string]interface{}, Product str
return outTradeNo, nil
}
// GetOrCreateUser 获取或创建用户
// 1. 如果上下文中已有用户ID直接返回
// 2. 如果是代理查询或APP请求创建新用户
// 3. 其他情况返回未登录错误
func (l *QueryServiceLogic) GetOrCreateUser() (string, int64, error) {
claims, err := ctxdata.GetClaimsFromCtx(l.ctx)
if err == nil && claims != nil {
return claims.UserId, claims.UserType, nil
}
userID, regErr := l.svcCtx.UserService.RegisterUUIDUser(l.ctx)
if regErr != nil {
return "", 0, regErr
}
return userID, model.UserTypeTemp, nil
// GetOrCreateUser 鑾峰彇鎴栧垱寤虹敤鎴?
// 1. 濡傛灉涓婁笅鏂囦腑宸叉湁鐢ㄦ埛ID锛岀洿鎺ヨ繑鍥?
// 2. 濡傛灉鏄唬鐞嗘煡璇㈡垨APP璇锋眰锛屽垱寤烘柊鐢ㄦ埛
// 3. 鍏朵粬鎯呭喌杩斿洖鏈櫥褰曢敊璇?
func (l *QueryServiceLogic) GetOrCreateUser() (string, error) {
claims, err := ctxdata.GetClaimsFromCtx(l.ctx)
if err == nil && claims != nil {
return claims.UserId, nil
}
userID, regErr := l.svcCtx.UserService.RegisterUUIDUser(l.ctx)
if regErr != nil {
return "", regErr
}
return userID, nil
}

View File

@@ -23,7 +23,7 @@ func NewAuthLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AuthLogic {
}
func (l *AuthLogic) Auth(req *types.AuthReq) (*types.AuthResp, error) {
var userID string
var userID string
var userType int64
var authType string
var authKey string
@@ -88,7 +88,7 @@ func (l *AuthLogic) Auth(req *types.AuthReq) (*types.AuthResp, error) {
return nil, errors.Wrapf(xerr.NewErrMsg("不支持的平台类型"), "platform=%s", req.Platform)
}
token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID, userType)
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)
}

View File

@@ -90,8 +90,8 @@ func (l *BindMobileLogic) BindMobile(req *types.BindMobileReq) (resp *types.Bind
if _, err := l.svcCtx.UserAuthModel.Insert(l.ctx, nil, &model.UserAuth{Id: uuid.NewString(), UserId: finalUserID, AuthType: model.UserAuthTypeMobile, AuthKey: encryptedMobile}); err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建手机号认证失败: %v", err)
}
// 发放正式用户token
token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, finalUserID, model.UserTypeNormal)
// 发放tokenuserType会根据mobile字段动态计算
token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, finalUserID)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成Token失败: %v", err)
}
@@ -118,7 +118,7 @@ func (l *BindMobileLogic) BindMobile(req *types.BindMobileReq) (resp *types.Bind
}
// 如果当前认证已属于目标手机号用户直接发放token无需合并
if existingAuth != nil && existingAuth.UserId == finalUserID {
token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, finalUserID, model.UserTypeNormal)
token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, finalUserID)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成Token失败: %v", err)
}
@@ -232,8 +232,8 @@ func (l *BindMobileLogic) BindMobile(req *types.BindMobileReq) (resp *types.Bind
return nil, err
}
// 合并完成后生成并返回正式用户token
token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, finalUserID, model.UserTypeNormal)
// 合并完成后生成tokenuserType会根据mobile字段动态计算
token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, finalUserID)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成Token失败: %v", err)
}

View File

@@ -36,17 +36,8 @@ func (l *DetailLogic) Detail() (resp *types.UserInfoResp, err error) {
}
userID := claims.UserId
userType := claims.UserType
if userType != model.UserTypeNormal {
return &types.UserInfoResp{
UserInfo: types.User{
Id: userID,
UserType: userType,
Mobile: "",
NickName: "",
},
}, nil
}
// 无论是临时用户还是正常用户,都需要从数据库中查询用户信息
user, err := l.svcCtx.UserModel.FindOne(l.ctx, userID)
if err != nil {
if errors.Is(err, model.ErrNotFound) {
@@ -54,6 +45,7 @@ func (l *DetailLogic) Detail() (resp *types.UserInfoResp, err error) {
}
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "用户信息, 数据库查询用户信息失败, %v", err)
}
var userInfo types.User
err = copier.Copy(&userInfo, user)
if err != nil {

View File

@@ -2,9 +2,9 @@ package user
import (
"context"
"time"
"ycc-server/common/ctxdata"
"ycc-server/common/xerr"
"time"
"github.com/pkg/errors"
@@ -33,7 +33,7 @@ func (l *GetTokenLogic) GetToken() (resp *types.MobileCodeLoginResp, err error)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "用户信息, %v", err)
}
token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, claims.UserId, claims.UserType)
token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, claims.UserId)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "用户信息, %v", err)
}

View File

@@ -62,7 +62,7 @@ func (l *MobileCodeLoginLogic) MobileCodeLogin(req *types.MobileCodeLoginReq) (r
return nil, errors.Wrapf(xerr.NewErrMsg("用户不存在"), "手机登录, 用户不存在: %s", encryptedMobile)
}
userID = user.Id
token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID, model.UserTypeNormal)
token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "手机登录, 生成token失败 : %s", userID)
}

View File

@@ -1,22 +1,22 @@
package user
import (
"context"
"ycc-server/app/main/model"
"ycc-server/common/xerr"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
"ycc-server/app/main/model"
"ycc-server/common/xerr"
"github.com/google/uuid"
"github.com/pkg/errors"
"github.com/google/uuid"
"github.com/pkg/errors"
"ycc-server/app/main/api/internal/svc"
"ycc-server/app/main/api/internal/types"
"ycc-server/app/main/api/internal/svc"
"ycc-server/app/main/api/internal/types"
"github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/core/logx"
)
type WxH5AuthLogic struct {
@@ -47,29 +47,28 @@ func (l *WxH5AuthLogic) WxH5Auth(req *types.WXH5AuthReq) (resp *types.WXH5AuthRe
}
// Step 3: 处理用户信息
var userID string
var userType int64
if userAuth != nil {
// 已存在用户,直接登录
userID = userAuth.UserId
userType = model.UserTypeNormal
} else {
user := &model.User{Id: uuid.NewString()}
_, err := l.svcCtx.UserModel.Insert(l.ctx, nil, user)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建用户失败: %v", err)
}
ua := &model.UserAuth{Id: uuid.NewString(), UserId: user.Id, AuthType: model.UserAuthTypeWxh5OpenID, AuthKey: accessTokenResp.Openid}
_, err = l.svcCtx.UserAuthModel.Insert(l.ctx, nil, ua)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建用户授权失败: %v", err)
}
userID = user.Id
userType = model.UserTypeTemp
}
var userID string
if userAuth != nil {
// 已存在用户,直接登录
userID = userAuth.UserId
} else {
// 新用户创建为临时用户没有mobile
user := &model.User{Id: uuid.NewString()}
_, err := l.svcCtx.UserModel.Insert(l.ctx, nil, user)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建用户失败: %v", err)
}
ua := &model.UserAuth{Id: uuid.NewString(), UserId: user.Id, AuthType: model.UserAuthTypeWxh5OpenID, AuthKey: accessTokenResp.Openid}
_, err = l.svcCtx.UserAuthModel.Insert(l.ctx, nil, ua)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建用户授权失败: %v", err)
}
userID = user.Id
l.Infof("Created new weixin user: userID=%s, openid=%s", userID, accessTokenResp.Openid)
}
// Step 4: 生成JWT Token
token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID, userType)
// Step 4: 生成JWT Token动态计算userType
token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成JWT token失败: %v", err)
}

View File

@@ -1,21 +1,21 @@
package user
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
"ycc-server/app/main/api/internal/svc"
"ycc-server/app/main/api/internal/types"
"ycc-server/app/main/model"
"ycc-server/common/xerr"
"ycc-server/app/main/api/internal/svc"
"ycc-server/app/main/api/internal/types"
"ycc-server/app/main/model"
"ycc-server/common/xerr"
"github.com/google/uuid"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/logx"
"github.com/google/uuid"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/logx"
)
type WxMiniAuthLogic struct {
@@ -46,29 +46,27 @@ func (l *WxMiniAuthLogic) WxMiniAuth(req *types.WXMiniAuthReq) (resp *types.WXMi
}
// 3. 处理用户信息
var userID string
var userType int64
if userAuth != nil {
// 已存在用户,直接登录
userID = userAuth.UserId
userType = model.UserTypeNormal
} else {
user := &model.User{Id: uuid.NewString()}
_, err := l.svcCtx.UserModel.Insert(l.ctx, nil, user)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建用户失败: %v", err)
}
ua := &model.UserAuth{Id: uuid.NewString(), UserId: user.Id, AuthType: model.UserAuthTypeWxMiniOpenID, AuthKey: sessionKeyResp.Openid}
_, err = l.svcCtx.UserAuthModel.Insert(l.ctx, nil, ua)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建用户授权失败: %v", err)
}
userID = user.Id
userType = model.UserTypeTemp
}
var userID string
if userAuth != nil {
// 已存在用户,直接登录
userID = userAuth.UserId
} else {
// 新用户创建为临时用户没有mobile
user := &model.User{Id: uuid.NewString()}
_, err := l.svcCtx.UserModel.Insert(l.ctx, nil, user)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建用户失败: %v", err)
}
ua := &model.UserAuth{Id: uuid.NewString(), UserId: user.Id, AuthType: model.UserAuthTypeWxMiniOpenID, AuthKey: sessionKeyResp.Openid}
_, err = l.svcCtx.UserAuthModel.Insert(l.ctx, nil, ua)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建用户授权失败: %v", err)
}
userID = user.Id
}
// 4. 生成JWT Token
token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID, userType)
// 4. 生成JWT Token动态计算userType
token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成JWT Token失败: %v", err)
}

View File

@@ -1,10 +1,10 @@
package middleware
import (
"net/http"
"ycc-server/app/main/model"
"ycc-server/common/ctxdata"
"ycc-server/common/xerr"
"net/http"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/rest/httpx"
@@ -24,8 +24,10 @@ func (m *UserAuthInterceptorMiddleware) Handle(next http.HandlerFunc) http.Handl
httpx.Error(w, errors.Wrapf(xerr.NewErrCode(ErrCodeUnauthorized), "token解析失败: %v", err))
return
}
// 检查用户是否绑定了mobile没有mobile表示是临时用户不允许访问需要认证的接口
// 注:临时用户现在基于 mobile 字段判断,而不是 UserType
if claims.UserType == model.UserTypeTemp {
httpx.Error(w, errors.Wrapf(xerr.NewErrCode(xerr.USER_NEED_BIND_MOBILE), "token解析失败: %v", err))
httpx.Error(w, errors.Wrapf(xerr.NewErrCode(xerr.USER_NEED_BIND_MOBILE), "请先绑定手机号: %v", err))
return
}
next(w, r)

View File

@@ -63,17 +63,38 @@ func (s *UserService) RegisterUUIDUser(ctx context.Context) (string, error) {
return userId, nil
}
// GeneralUserToken 生成用户token
func (s *UserService) GeneralUserToken(ctx context.Context, userID string, userType int64) (string, error) {
// GetUserType 根据user.Mobile字段动态计算用户类型
// 如果有mobile则为正式用户(UserTypeNormal),否则为临时用户(UserTypeTemp)
func (s *UserService) GetUserType(ctx context.Context, userID string) (int64, error) {
user, err := s.userModel.FindOne(ctx, userID)
if err != nil {
return 0, err
}
if user.Mobile.Valid && user.Mobile.String != "" {
return model.UserTypeNormal, nil
}
return model.UserTypeTemp, nil
}
// GeneralUserToken 生成用户token动态计算userType
func (s *UserService) GeneralUserToken(ctx context.Context, userID string) (string, error) {
platform, err := ctxdata.GetPlatformFromCtx(ctx)
if err != nil {
return "", err
}
// 动态计算userType
userType, err := s.GetUserType(ctx, userID)
if err != nil {
return "", errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取用户信息失败: %v", err)
}
var isAgent int64
var agentID string
var authType string
var authKey string
// 只有正式用户有mobile才可能是代理
if userType == model.UserTypeNormal {
agent, err := s.agentModel.FindOneByUserId(ctx, userID)
if err != nil && !errors.Is(err, model.ErrNotFound) {
@@ -89,6 +110,7 @@ func (s *UserService) GeneralUserToken(ctx context.Context, userID string, userT
authKey = userAuth.AuthKey
}
} else {
// 临时用户获取其他平台的auth信息
platAuthType := s.getAuthTypeByPlatform(platform)
ua, err := s.userAuthModel.FindOneByUserIdAuthType(ctx, userID, platAuthType)
if err == nil && ua != nil {
@@ -159,12 +181,16 @@ func (s *UserService) RegisterUser(ctx context.Context, mobile string) (string,
return userId, nil
}
// 双重判断是否已经注册
if claims.UserType == model.UserTypeNormal {
// 双重判断是否已经注册根据mobile判断而不是userType
currentUser, err := s.userModel.FindOne(ctx, claims.UserId)
if err != nil && !errors.Is(err, model.ErrNotFound) {
return "", err
}
if currentUser != nil && currentUser.Mobile.Valid && currentUser.Mobile.String != "" {
return "", errors.New("用户已注册")
}
var userId string
// 临时转正式注册
// 临时用户绑定mobile转正式注册
err = s.userModel.Trans(ctx, func(ctx context.Context, session sqlx.Session) error {
user := &model.User{Id: uuid.NewString(), Mobile: sql.NullString{String: mobile, Valid: true}}
if _, userInsertErr := s.userModel.Insert(ctx, session, user); userInsertErr != nil {
@@ -187,17 +213,26 @@ func (s *UserService) RegisterUser(ctx context.Context, mobile string) (string,
return userId, nil
}
// TempUserBindUser 临时用户绑定用户
// TempUserBindUser 临时用户绑定用户添加mobile使其变为正式用户
func (s *UserService) TempUserBindUser(ctx context.Context, session sqlx.Session, normalUserID string) error {
claims, err := ctxdata.GetClaimsFromCtx(ctx)
if err != nil && !errors.Is(err, ctxdata.ErrNoInCtx) {
return err
}
if claims == nil || claims.UserType != model.UserTypeTemp {
if claims == nil {
return errors.New("无临时用户")
}
// 检查当前用户是否已经绑定了mobile根据mobile判断而不是userType
tempUser, err := s.userModel.FindOne(ctx, claims.UserId)
if err != nil && !errors.Is(err, model.ErrNotFound) {
return err
}
if tempUser != nil && tempUser.Mobile.Valid && tempUser.Mobile.String != "" {
return errors.New("临时用户已注册")
}
existingAuth, err := s.userAuthModel.FindOneByAuthTypeAuthKey(ctx, claims.AuthType, claims.AuthKey)
if err != nil && !errors.Is(err, model.ErrNotFound) {
return err

View File

@@ -61,7 +61,7 @@ func main() {
defer server.Stop()
handler.RegisterHandlers(server, svcContext)
// 自动注册API到数据库
apiRegistry := service.NewApiRegistryService(svcContext.AdminApiModel)
routes := server.Routes()
@@ -70,7 +70,7 @@ func main() {
} else {
logx.Infof("API注册成功共注册 %d 个路由", len(routes))
}
fmt.Printf("Starting server at %s:%d...\n", c.Host, c.Port)
server.Start()
}

View File

@@ -1,101 +0,0 @@
# distributeNormalAgentBonus 函数使用的配置项说明
## 函数位置
`ycc-proxy-server/app/main/api/internal/service/agentService.go:254-478`
## 使用的配置项列表
### 1. `direct_parent_amount_normal`
- **配置键**`direct_parent_amount_normal`
- **类型**浮点数float64
- **默认值**2.0元
- **使用位置**第376行
- **用途**:普通代理给直接上级普通代理的返佣金额
- **使用场景**
- 当直接上级是普通代理时使用
- 无论后续层级有多少普通代理,这部分金额只给推广人的直接上级
- **示例**等级加成6元给直接上级普通代理2元
### 2. `direct_parent_amount_gold`
- **配置键**`direct_parent_amount_gold`
- **类型**浮点数float64
- **默认值**3.0元
- **使用位置**第326行
- **用途**:普通代理给直接上级黄金代理的返佣金额
- **使用场景**
- 当直接上级是黄金代理时使用
- 这部分金额给直接上级黄金代理,剩余部分继续向上分配给钻石上级
- **示例**等级加成6元给直接上级黄金代理3元剩余3元给钻石上级
### 3. `max_gold_rebate_amount`
- **配置键**`max_gold_rebate_amount`
- **类型**浮点数float64
- **默认值**3.0元
- **使用位置**第492行
- **用途**:普通代理给黄金上级的最大返佣金额
- **使用场景**
- 当直接上级是普通代理,且只有黄金上级(没有钻石上级)时使用
- 即使剩余金额超过这个值,也只能给黄金上级最多这个金额,超出部分归平台
- **示例**剩余4元最大限额3元则给黄金3元剩余1元归平台
### 4. `level_2_bonus`
- **配置键**`level_2_bonus`
- **类型**整数int64从配置读取时转换为浮点数计算
- **默认值**3元
- **使用位置**第438行通过 `getLevelBonus(ctx, 2)` 调用)
- **用途**:黄金代理的等级加成
- **使用场景**
- 用于计算等级加成的差(普通等级加成 - 黄金等级加成)
- 当直接上级是普通代理,且上级链中有黄金和钻石时,用于计算给黄金的返佣金额
- **计算逻辑**
- 等级加成差 = 普通等级加成6元- 黄金等级加成3元= 3元
- 给黄金的金额 = 等级加成差3元- 已给普通的部分2元= 1元
### 5. `level_1_bonus`
- **配置键**`level_1_bonus`
- **类型**整数int64
- **默认值**6元
- **使用位置**:通过 `amount` 参数传入(在 `AgentProcess` 中通过 `getLevelBonus(ctx, 1)` 获取)
- **用途**:普通代理的等级加成
- **说明**
- 不在 `distributeNormalAgentBonus` 函数中直接读取
- 作为 `amount` 参数传入函数
- 这是需要分配的总额
## 配置项使用总结表
| 配置键 | 默认值 | 使用场景 | 代码行号 |
|--------|--------|---------|---------|
| `direct_parent_amount_normal` | 2.0元 | 直接上级是普通代理时,给直接上级的返佣金额 | 376 |
| `direct_parent_amount_gold` | 3.0元 | 直接上级是黄金代理时,给直接上级的返佣金额 | 326 |
| `max_gold_rebate_amount` | 3.0元 | 只有黄金上级(无钻石)时,给黄金的最大返佣金额 | 492 |
| `level_2_bonus` | 3元 | 黄金代理等级加成,用于计算等级加成差 | 438 |
| `level_1_bonus` | 6元 | 普通代理等级加成,通过参数传入 | - |
## 配置项在代码中的调用关系
```
AgentProcess (处理订单)
调用 getLevelBonus(ctx, agent.Level) 获取普通代理等级加成
传入 distributeNormalAgentBonus(amount=6元, ...)
在函数内使用:
1. getRebateConfigFloat("direct_parent_amount_normal", 2.0) # 第376行
2. getRebateConfigFloat("direct_parent_amount_gold", 3.0) # 第326行
3. getRebateConfigFloat("max_gold_rebate_amount", 3.0) # 第492行
4. getLevelBonus(ctx, 2) -> 读取 level_2_bonus # 第438行
```
## 注意事项
1. **所有配置项都支持动态配置**:如果配置不存在,会使用默认值
2. **配置读取失败会记录日志**:确保配置问题时可以追踪
3. **配置值的类型转换**
- 返佣配置直接使用浮点数(支持小数)
- 等级加成从配置读取时为整数,计算时转换为浮点数
4. **配置项命名规范**
- 返佣配置使用描述性名称:`direct_parent_amount_normal``direct_parent_amount_gold``max_gold_rebate_amount`
- 等级加成配置使用数字后缀:`level_1_bonus``level_2_bonus`

View File

@@ -1,448 +0,0 @@
# 代理系统测试用例清单
## 一、邀请下级代理测试
### 1.1 钻石代理邀请下级
- [ ] **钻石邀请普通代理**
- 预期:建立上下级关系,普通代理的 team_leader_id 指向钻石代理
- 验证:关系类型为直接关系,团队首领正确
- [ ] **钻石邀请黄金代理**
- 预期:建立上下级关系,黄金代理的 team_leader_id 指向钻石代理
- 验证:关系类型为直接关系,团队首领正确
- [ ] **钻石邀请钻石代理(不允许)**
- 预期:拒绝,提示"代理等级不能高于上级代理"
- 验证:邀请失败,无关系建立
### 1.2 黄金代理邀请下级
- [ ] **黄金邀请普通代理**
- 预期:建立上下级关系,普通代理的 team_leader_id 指向上级钻石代理
- 验证:关系类型为直接关系,团队首领正确(向上查找钻石)
- [ ] **黄金邀请黄金代理(不允许)**
- 预期:拒绝,提示"代理等级不能高于上级代理"
- 验证:邀请失败,无关系建立
- [ ] **黄金邀请钻石代理(不允许)**
- 预期:拒绝,提示"代理等级不能高于上级代理"
- 验证:邀请失败,无关系建立
### 1.3 普通代理邀请下级
- [ ] **普通邀请普通代理**
- 预期:建立上下级关系,普通代理的 team_leader_id 向上查找钻石/黄金
- 验证:关系类型为直接关系,团队首领正确
- [ ] **普通邀请黄金代理(不允许)**
- 预期:拒绝,提示"代理等级不能高于上级代理"
- 验证:邀请失败,无关系建立
- [ ] **普通邀请钻石代理(不允许)**
- 预期:拒绝,提示"代理等级不能高于上级代理"
- 验证:邀请失败,无关系建立
### 1.4 多层级邀请测试
- [ ] **钻石 → 黄金 → 普通3层**
- 预期3层关系链所有代理的 team_leader_id 指向钻石
- 验证:关系链正确,团队首领一致
- [ ] **钻石 → 普通 → 普通 → 普通4层**
- 预期4层关系链所有代理的 team_leader_id 指向钻石
- 验证:关系链正确,团队首领一致
- [ ] **钻石 → 普通 → 普通 → 普通 → 普通5层**
- 预期5层关系链所有代理的 team_leader_id 指向钻石
- 验证:关系链正确,团队首领一致
---
## 二、推广报告收益测试(等级加成返佣)
### 2.1 钻石代理推广报告
- [ ] **钻石代理自己推广的报告**
- 等级加成0元
- 预期:无等级加成返佣,全部收益归自己
- 验证agent_rebate 表无记录,代理收益 = 设定价格 - 基础底价 - 提价成本
### 2.2 黄金代理推广报告
- [ ] **黄金代理(上级是钻石)推广报告**
- 等级加成3元
- 预期3元全部返佣给钻石上级
- 验证agent_rebate 表记录正确钻石上级钱包增加3元
- [ ] **黄金代理(无上级/上级不是钻石)推广报告**
- 等级加成3元
- 预期:返佣归平台(异常情况)
- 验证agent_rebate 表无记录
### 2.3 普通代理推广报告等级加成6元
#### 2.3.1 直接上级是钻石
- [ ] **普通代理(上级是钻石)推广报告**
- 等级加成6元
- 预期6元全部返佣给钻石上级
- 验证agent_rebate 表记录正确钻石上级钱包增加6元
#### 2.3.2 直接上级是黄金
- [ ] **普通代理(上级是黄金,黄金上级是钻石)推广报告**
- 等级加成6元
- 预期:
- 3元给黄金上级配置normal_to_gold_rebate
- 3元给钻石上级上上级
- 验证agent_rebate 表有2条记录金额分配正确
- [ ] **普通代理(上级是黄金,无钻石上级)推广报告**
- 等级加成6元
- 预期3元给黄金上级剩余3元归平台
- 验证agent_rebate 表只有1条记录3元给黄金剩余归平台
#### 2.3.3 直接上级是普通(多层普通代理)
- [ ] **普通 → 普通 → 钻石3层**
- 等级加成6元
- 预期:
- 2元给直接上级普通配置normal_to_normal_rebate
- 4元给钻石上级跳过中间普通直接给钻石
- 验证agent_rebate 表有2条记录金额分配正确
- [ ] **普通 → 普通 → 普通 → 钻石4层**
- 等级加成6元
- 预期:
- 2元给直接上级普通
- 4元给钻石上级跳过中间所有普通代理
- 验证agent_rebate 表有2条记录金额分配正确
- [ ] **普通 → 普通 → 黄金无钻石3层**
- 等级加成6元
- 预期:
- 2元给直接上级普通
- 3元给黄金上级配置normal_to_gold_rebate_max
- 1元归平台超出部分
- 验证agent_rebate 表有2条记录金额分配正确剩余归平台
- [ ] **普通 → 普通 → 普通(全部是普通,无钻石/黄金)**
- 等级加成6元
- 预期:
- 2元给直接上级普通
- 4元归平台无钻石/黄金上级)
- 验证agent_rebate 表只有1条记录2元剩余归平台
---
## 三、升级代理收益测试
### 3.1 自主付费升级
#### 3.1.1 普通 → 黄金199元
- [ ] **普通代理升级为黄金(上级是钻石)**
- 升级费用199元
- 返佣139元给原直接上级
- 预期:
- 原直接上级钻石钱包增加139元
- 升级后不脱离关系(钻石 > 黄金)
- 仍属于原团队
- 验证:升级成功,返佣记录在 agent_upgrade 表,钱包余额正确
- [ ] **普通代理升级为黄金(上级是黄金)**
- 升级费用199元
- 返佣139元给原直接上级黄金
- 预期:
- 原直接上级黄金钱包增加139元
- 升级后脱离关系(同级不能作为上下级)
- 保留团队关系(向上查找钻石)
- 验证:升级成功,关系脱离,团队首领正确
- [ ] **普通代理升级为黄金(上级是普通)**
- 升级费用199元
- 返佣139元给原直接上级普通
- 预期:
- 原直接上级普通钱包增加139元
- 升级后脱离关系(下级等级高于上级)
- 保留团队关系(向上查找钻石/黄金)
- 验证:升级成功,关系脱离,团队首领正确
#### 3.1.2 普通 → 钻石980元
- [ ] **普通代理升级为钻石(上级是钻石)**
- 升级费用980元
- 返佣680元给原直接上级钻石
- 预期:
- 原直接上级钻石钱包增加680元
- 升级后脱离关系(同级不能作为上下级)
- 独立成为新团队team_leader_id = 自己
- 所有下级跟随到新团队
- 验证:升级成功,独立成团队,下级团队首领更新
- [ ] **普通代理升级为钻石(上级是黄金)**
- 升级费用980元
- 返佣680元给原直接上级黄金
- 预期:
- 原直接上级黄金钱包增加680元
- 升级后脱离关系(下级等级高于上级)
- 独立成为新团队team_leader_id = 自己
- 所有下级跟随到新团队
- 验证:升级成功,独立成团队,下级团队首领更新
- [ ] **普通代理升级为钻石(上级是普通)**
- 升级费用980元
- 返佣680元给原直接上级普通
- 预期:
- 原直接上级普通钱包增加680元
- 升级后脱离关系(下级等级高于上级)
- 独立成为新团队team_leader_id = 自己
- 所有下级跟随到新团队
- 验证:升级成功,独立成团队,下级团队首领更新
#### 3.1.3 黄金 → 钻石980元
- [ ] **黄金代理升级为钻石(上级是钻石)**
- 升级费用980元
- 返佣680元给原直接上级钻石
- 预期:
- 原直接上级钻石钱包增加680元
- 升级后脱离关系(同级不能作为上下级)
- 独立成为新团队team_leader_id = 自己
- 所有下级跟随到新团队
- 验证:升级成功,独立成团队,下级团队首领更新
- [ ] **黄金代理升级为钻石(无上级)**
- 升级费用980元
- 返佣:无
- 预期:
- 独立成为新团队team_leader_id = 自己
- 所有下级跟随到新团队
- 验证:升级成功,独立成团队,下级团队首领更新
### 3.2 钻石升级下级(免费)
- [ ] **钻石升级下级(普通 → 黄金)**
- 升级费用:免费
- 返佣:无
- 预期:
- 被升级代理无需付费
- 升级后根据原上级等级决定是否脱离关系
- 保留团队关系
- 验证升级成功费用为0关系处理正确
---
## 四、升级后团队转移测试
### 4.1 普通 → 黄金升级(保留团队)
#### 4.1.1 上级是钻石(不脱离)
- [ ] **普通 → 黄金(上级是钻石),有下级**
- 预期:
- 不脱离关系
- 保留原团队team_leader_id 不变)
- 所有下级(直接+间接)的 team_leader_id 不变
- 验证:关系保留,所有下级团队首领不变
#### 4.1.2 上级是黄金(脱离关系)
- [ ] **普通 → 黄金(上级是黄金),有下级**
- 预期:
- 脱离直接上下级关系
- 保留团队关系(向上查找钻石)
- 所有下级(直接+间接)的 team_leader_id 不变
- 验证关系脱离RelationType=2所有下级团队首领不变
#### 4.1.3 上级是普通(脱离关系)
- [ ] **普通 → 黄金(上级是普通),有下级**
- 预期:
- 脱离直接上下级关系
- 保留团队关系(向上查找钻石/黄金)
- 所有下级(直接+间接)的 team_leader_id 不变
- 验证:关系脱离,所有下级团队首领不变
### 4.2 升级为钻石(独立成新团队)
#### 4.2.1 普通 → 钻石
- [ ] **普通 → 钻石(上级是钻石),有下级**
- 预期:
- 脱离关系
- 独立成新团队team_leader_id = 自己)
- 所有直接下级的 team_leader_id 更新为自己
- 所有间接下级的 team_leader_id 更新为自己(递归)
- 验证:
- 升级代理的 team_leader_id = 自己
- 所有下级(直接+间接)的 team_leader_id = 升级代理ID
- 下级数量统计正确
- [ ] **普通 → 钻石上级是黄金有下级2层**
- 预期:
- 脱离关系
- 独立成新团队
- 直接下级跟随
- 间接下级跟随
- 验证:所有下级团队首领更新为新钻石
- [ ] **普通 → 钻石上级是普通有下级3层以上**
- 预期:
- 脱离关系
- 独立成新团队
- 所有层级的下级都跟随
- 验证:所有下级团队首领更新为新钻石
#### 4.2.2 黄金 → 钻石
- [ ] **黄金 → 钻石(上级是钻石),有下级**
- 预期:
- 脱离关系
- 独立成新团队
- 所有下级跟随
- 验证:所有下级团队首领更新为新钻石
### 4.3 复杂团队转移场景
#### 4.3.1 多层级团队
- [ ] **钻石A → 黄金B → 普通C → 普通DB升级为钻石**
- 预期:
- B独立成新团队
- C和D的 team_leader_id 更新为B
- A的团队只剩自己
- B的团队B、C、D
- 验证:团队划分正确,关系链正确
#### 4.3.2 跨团队转移
- [ ] **钻石A → 普通B → 普通CC升级为钻石**
- 预期:
- C独立成新团队
- C无下级团队只有C自己
- A的团队A、B
- 验证:团队划分正确
#### 4.3.3 深度层级转移
- [ ] **钻石A → 普通B → 普通C → 普通D → 普通EC升级为钻石**
- 预期:
- C独立成新团队
- D和E的 team_leader_id 更新为C
- A的团队A、B
- C的团队C、D、E
- 验证:团队划分正确,所有层级更新正确
---
## 五、综合测试场景
### 5.1 完整业务流程
- [ ] **创建团队 → 邀请下级 → 推广报告 → 收益分配 → 升级 → 团队转移**
- 步骤:
1. 创建钻石代理A
2. A邀请黄金代理B
3. B邀请普通代理C
4. C邀请普通代理D
5. D推广报告验证收益分配
6. C升级为黄金验证关系变化
7. B升级为钻石验证团队转移
- 验证:每个步骤的数据正确
### 5.2 收益统计测试
- [ ] **查询代理收益统计(包含佣金和返佣)**
- 验证agent_wallet 表的 Balance 和 TotalEarnings 正确
- [ ] **查询下级列表和统计**
- 验证:下级数量、团队规模统计正确
### 5.3 边界情况测试
- [ ] **钻石代理无下级时升级(边界情况)**
- 验证独立成团队team_leader_id = 自己
- [ ] **普通代理无上级时升级**
- 验证:独立成团队,无返佣
- [ ] **多层普通代理链,无钻石/黄金上级**
- 验证:收益分配正确(部分归平台)
---
## 六、数据验证检查点
### 6.1 关系表验证
- [ ] agent_relation 表的关系类型正确1=直接关系2=已脱离)
- [ ] 脱离关系时DetachReason 和 DetachTime 正确记录
### 6.2 钱包验证
- [ ] agent_wallet 表的 Balance可用余额正确
- [ ] agent_wallet 表的 FrozenBalance冻结余额正确如有
- [ ] agent_wallet 表的 TotalEarnings累计收益正确
### 6.3 返佣记录验证
- [ ] agent_rebate 表的记录完整(推广报告返佣)
- [ ] agent_upgrade 表的返佣记录正确(升级返佣)
- [ ] 返佣金额计算正确
### 6.4 团队验证
- [ ] agent 表的 team_leader_id 正确指向钻石代理
- [ ] 升级后所有下级 team_leader_id 更新正确
### 6.5 订单和佣金验证
- [ ] agent_order 表记录完整
- [ ] agent_commission 表记录完整
- [ ] 佣金金额计算正确
---
## 七、测试数据准备建议
### 7.1 创建测试代理账号
建议准备以下测试账号(可用不同手机号):
1. **钻石代理**
- 钻石A团队首领无上级
- 钻石B团队首领无上级
2. **黄金代理**
- 黄金A上级钻石A
- 黄金B上级钻石A
- 黄金C上级钻石B
3. **普通代理**
- 普通A上级钻石A
- 普通B上级黄金A
- 普通C上级普通B
- 普通D上级普通C
- 普通E上级普通D
### 7.2 测试产品配置
- [ ] 确保有测试产品配置agent_product_config 表)
- [ ] 配置基础底价、提价阈值、提价手续费比例
### 7.3 测试返佣配置
- [ ] normal_to_normal_rebate默认2元
- [ ] normal_to_gold_rebate默认3元
- [ ] normal_to_gold_rebate_max默认3元
---
## 八、测试执行顺序建议
1. **第一阶段**:基础功能测试
- 邀请下级(各种组合)
- 验证关系建立
- 验证团队首领
2. **第二阶段**:收益分配测试
- 推广报告
- 收益计算
- 返佣分配
3. **第三阶段**:升级功能测试
- 自主付费升级
- 钻石升级下级
- 升级返佣
4. **第四阶段**:团队转移测试
- 普通→黄金升级(保留团队)
- 升级为钻石(独立成团队)
- 复杂场景测试
5. **第五阶段**:综合测试
- 完整业务流程
- 边界情况
- 数据一致性验证
---
## 注意事项
1. **开发环境**测试时确保使用开发环境ENV=development可跳过验证码校验
2. **数据清理**:每次测试后建议清理测试数据,避免相互影响
3. **事务验证**:注意验证事务的一致性,确保要么全部成功,要么全部回滚
4. **并发测试**:如有需要,可进行并发场景测试(多代理同时升级等)
5. **日志记录**:测试过程中查看日志,确保业务流程正确

View File

@@ -1,774 +0,0 @@
# 代理配置表分析和优化建议
## 一、当前配置表结构分析
### 1.1 数据库表结构
**表名**: `agent_config`
```sql
CREATE TABLE `agent_config` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`config_key` varchar(100) NOT NULL COMMENT '配置键(唯一)',
`config_value` varchar(500) NOT NULL COMMENT '配置值',
`config_type` varchar(50) NOT NULL COMMENT '配置类型price=价格bonus=等级加成upgrade=升级费用rebate=返佣tax=税费',
`description` varchar(500) DEFAULT NULL COMMENT '配置描述',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`delete_time` datetime DEFAULT NULL,
`del_state` tinyint NOT NULL DEFAULT 0,
`version` bigint NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_config_key` (`config_key`),
KEY `idx_config_type` (`config_type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```
### 1.2 配置表设计评估
**优点**
**键值对存储灵活**:使用 `config_key``config_value` 的键值对模式,易于扩展新配置项
**类型分类清晰**`config_type` 字段将配置分为 price、bonus、upgrade、rebate、tax 等类型,便于管理
**唯一索引合理**`config_key` 的唯一索引确保配置键不重复
**版本控制**:包含 `version` 字段支持乐观锁,适合配置更新场景
**软删除支持**:支持软删除,保留历史配置记录
**缺点/问题**
**配置值类型限制**`config_value``varchar(500)`,所有值都存储为字符串,需要在使用时转换
**缺少验证机制**:数据库层面无法验证配置值的格式和范围
**配置值长度限制**对于复杂配置如JSON对象500字符可能不够
---
## 二、当前配置项清单
### 2.1 SQL初始化脚本中的配置项
根据 `agent_system_migration.sql` 文件,当前初始化的配置项:
| 配置键 | 配置值 | 类型 | 说明 |
| ------------------------------- | ------- | ------- | ------------------------ |
| `base_price` | 0.00 | price | 系统基础底价 |
| `system_max_price` | 9999.99 | price | 系统价格上限 |
| `price_threshold` | 0.00 | price | 提价标准阈值 |
| `price_fee_rate` | 0.0000 | price | 提价手续费比例 |
| `level_bonus_normal` | 6.00 | bonus | 普通代理等级加成 |
| `level_bonus_gold` | 3.00 | bonus | 黄金代理等级加成 |
| `level_bonus_diamond` | 0.00 | bonus | 钻石代理等级加成 |
| `upgrade_fee_normal_to_gold` | 199.00 | upgrade | 普通→黄金升级费用 |
| `upgrade_fee_to_diamond` | 980.00 | upgrade | 升级为钻石费用 |
| `upgrade_rebate_normal_to_gold` | 139.00 | upgrade | 普通→黄金返佣金额 |
| `upgrade_rebate_to_diamond` | 680.00 | upgrade | 升级为钻石返佣金额 |
| `direct_parent_amount_diamond` | 6.00 | rebate | 直接上级是钻石的返佣金额 |
| `direct_parent_amount_gold` | 3.00 | rebate | 直接上级是黄金的返佣金额 |
| `direct_parent_amount_normal` | 2.00 | rebate | 直接上级是普通的返佣金额 |
| `max_gold_rebate_amount` | 3.00 | rebate | 黄金代理最大返佣金额 |
| `tax_rate` | 0.0600 | tax | 提现税率6% |
**共15个配置项**
### 2.2 代码中使用的配置键
#### 2.2.1 AgentService 中使用的配置键
- `base_price` - 基础底价 ✅
- `price_threshold` - 提价标准阈值 ✅
- `price_fee_rate` - 提价手续费比例 ✅
- `level_bonus` - **硬编码,未从配置表读取**
#### 2.2.2 AdminGetAgentConfigLogic 中使用的配置键
- `level_1_bonus` - 普通代理等级加成 ❌ **配置键不匹配**
- `level_2_bonus` - 黄金代理等级加成 ❌ **配置键不匹配**
- `level_3_bonus` - 钻石代理等级加成 ❌ **配置键不匹配**
- `upgrade_to_gold_fee` - 升级为黄金费用 ❌ **配置键不匹配**
- `upgrade_to_diamond_fee` - 升级为钻石费用 ❌ **配置键不匹配**
- `upgrade_to_gold_rebate` - 升级为黄金返佣 ❌ **配置键不匹配**
- `upgrade_to_diamond_rebate` - 升级为钻石返佣 ❌ **配置键不匹配**
- `tax_rate` - 税率 ✅
- `tax_exemption_amount` - 免税额度 ❌ **配置项缺失**
---
## 三、发现的问题
### 3.1 🔴 严重问题:价格配置不应在系统配置表中
**问题描述**
价格相关配置(底价、上限、提价阈值、手续费比例)应该按产品配置,而不是全局系统配置。当前这些配置同时存在于 `agent_config``agent_product_config` 表中,但代码只从系统配置表读取,没有实现产品级别的配置覆盖。
**问题影响**
- ❌ 不同产品无法设置不同的底价和价格策略
- ❌ 代码中只读取系统配置,忽略了产品配置(`AgentService.AgentProcess``PaymentLogic`
- ❌ 产品配置表虽然存在,但在订单处理逻辑中未被使用
**应该移除的系统配置项**
- `base_price` - 应该只在产品配置表中
- `system_max_price` - 应该只在产品配置表中
- `price_threshold` - 应该只在产品配置表中(可选)
- `price_fee_rate` - 应该只在产品配置表中(可选)
**正确的设计**
- ✅ 所有价格相关配置都应该在 `agent_product_config` 表中按产品配置
- ✅ 系统配置表只保留全局配置(等级加成、升级费用、税费等)
- ✅ 代码应该优先从产品配置读取,如果产品未配置,则使用默认值
### 3.2 🔴 严重问题:配置键命名不一致
**问题描述**
SQL初始化脚本和代码逻辑中使用的配置键命名不一致导致配置无法正确读取。
**具体情况**
| 配置项 | SQL初始化脚本 | 代码逻辑期望 | 状态 |
| ---------------- | ------------------------------- | --------------------------- | -------- |
| 普通代理等级加成 | `level_bonus_normal` | `level_1_bonus` | ❌ 不匹配 |
| 黄金代理等级加成 | `level_bonus_gold` | `level_2_bonus` | ❌ 不匹配 |
| 钻石代理等级加成 | `level_bonus_diamond` | `level_3_bonus` | ❌ 不匹配 |
| 升级为黄金费用 | `upgrade_fee_normal_to_gold` | `upgrade_to_gold_fee` | ❌ 不匹配 |
| 升级为钻石费用 | `upgrade_fee_to_diamond` | `upgrade_to_diamond_fee` | ❌ 不匹配 |
| 升级为黄金返佣 | `upgrade_rebate_normal_to_gold` | `upgrade_to_gold_rebate` | ❌ 不匹配 |
| 升级为钻石返佣 | `upgrade_rebate_to_diamond` | `upgrade_to_diamond_rebate` | ❌ 不匹配 |
**影响**
- 后台管理系统无法正确读取和显示配置值
- 配置更新可能无法正常工作
### 3.3 🔴 严重问题:代码中硬编码等级加成
**问题位置**
```go
// agentService.go:144-155
func (s *AgentService) getLevelBonus(level int64) int64 {
switch level {
case 1: // 普通
return 6 // ❌ 硬编码,应该从配置表读取
case 2: // 黄金
return 3 // ❌ 硬编码,应该从配置表读取
case 3: // 钻石
return 0 // ❌ 硬编码,应该从配置表读取
default:
return 0
}
}
```
**问题影响**
- 等级加成值无法通过后台配置动态调整
- 必须修改代码才能更改等级加成
- 违反了配置化的设计原则
### 3.4 🟡 中等问题:缺少配置项
**缺失的配置项**
1. `tax_exemption_amount` - 免税额度前端接口需要但SQL初始化脚本中未包含
2. `upgrade_fee_gold_to_diamond` - 黄金→钻石升级费用(如果独立配置)
### 3.5 🟡 中等问题:配置键命名规范不统一
**当前问题**
- 部分使用下划线分隔:`level_bonus_normal`
- 部分使用数字后缀:`level_1_bonus`
- 部分使用驼峰式:`upgrade_fee_normal_to_gold`
**建议**:统一命名规范
### 3.6 🟢 轻微问题:配置值存储方式
**当前方式**:所有配置值以字符串形式存储,需要在使用时转换
- `strconv.ParseFloat()` 转换为浮点数
- `strconv.ParseInt()` 转换为整数
**影响**:每次读取都需要类型转换,性能影响较小,但容易出错
---
## 四、配置使用情况分析
### 4.1 实际使用的配置项
**在业务逻辑中使用的配置**
1.`base_price` - 订单处理时使用
2.`price_threshold` - 计算提价成本时使用
3.`price_fee_rate` - 计算提价成本时使用
4.`level_bonus_*` - **未使用,代码中硬编码**
**在后台管理中使用的配置**
1.`level_1_bonus` - 读取配置(但键名不匹配)
2.`level_2_bonus` - 读取配置(但键名不匹配)
3.`level_3_bonus` - 读取配置(但键名不匹配)
4.`upgrade_to_gold_fee` - 读取配置(但键名不匹配)
5.`upgrade_to_diamond_fee` - 读取配置(但键名不匹配)
6.`upgrade_to_gold_rebate` - 读取配置(但键名不匹配)
7.`upgrade_to_diamond_rebate` - 读取配置(但键名不匹配)
8.`tax_rate` - 读取配置
9.`tax_exemption_amount` - 读取配置(但配置项缺失)
### 4.2 未使用的配置项
以下配置项在SQL中初始化了但在代码中**似乎未使用**
- `direct_parent_amount_diamond` - 直接上级是钻石的返佣金额
- `direct_parent_amount_gold` - 直接上级是黄金的返佣金额
- `direct_parent_amount_normal` - 直接上级是普通的返佣金额
- `max_gold_rebate_amount` - 黄金代理最大返佣金额
**说明**:这些配置项可能被硬编码在 `distributeNormalAgentBonus` 等函数中,需要确认是否需要配置化。
---
## 五、优化建议
### 5.1 🔴 立即修复:移除系统配置表中的价格配置
**问题**
价格相关配置(`base_price``system_max_price``price_threshold``price_fee_rate`)应该完全由产品配置表管理,系统配置表中不应该存在这些配置。
**修复步骤**
1. **从系统配置表中删除价格相关配置**
```sql
-- 删除系统配置表中的价格相关配置
DELETE FROM `agent_config` WHERE `config_key` IN (
'base_price',
'system_max_price',
'price_threshold',
'price_fee_rate'
);
```
2. **修改订单处理逻辑,从产品配置表读取**
需要修改的文件:
- `app/main/api/internal/service/agentService.go` - `AgentProcess` 方法
- `app/main/api/internal/logic/pay/paymentlogic.go` - 创建订单时的价格计算
**示例代码修改**`agentService.go`
```go
// AgentProcess 处理代理订单(新系统)
func (s *AgentService) AgentProcess(ctx context.Context, order *model.Order) error {
// ... 前面的代码不变 ...
// 4. 获取产品配置(优先使用产品配置,如果不存在则使用默认值)
productConfig, err := s.AgentProductConfigModel.FindOneByProductId(ctx, order.ProductId)
if err != nil && !errors.Is(err, model.ErrNotFound) {
return errors.Wrapf(err, "查询产品配置失败, productId: %d", order.ProductId)
}
// 使用产品配置的底价如果产品未配置则使用默认值0
basePrice := 0.0
if productConfig != nil {
basePrice = productConfig.BasePrice
}
// ... 使用basePrice计算实际底价 ...
// 6.2 计算提价成本(使用产品配置)
priceThreshold := 0.0
priceFeeRate := 0.0
if productConfig != nil {
if productConfig.PriceThreshold.Valid {
priceThreshold = productConfig.PriceThreshold.Float64
}
if productConfig.PriceFeeRate.Valid {
priceFeeRate = productConfig.PriceFeeRate.Float64
}
}
priceCost := s.calculatePriceCost(agentOrder.SetPrice, priceThreshold, priceFeeRate)
// ... 后续代码 ...
}
```
3. **更新后台管理系统**
- 移除系统配置页面中的价格相关配置项
- 确保产品配置页面可以正确配置这些价格参数
### 5.2 🔴 立即修复:统一配置键命名
**方案一修改SQL初始化脚本使用代码期望的键名**
```sql
-- 修改等级加成配置键
UPDATE `agent_config` SET `config_key` = 'level_1_bonus' WHERE `config_key` = 'level_bonus_normal';
UPDATE `agent_config` SET `config_key` = 'level_2_bonus' WHERE `config_key` = 'level_bonus_gold';
UPDATE `agent_config` SET `config_key` = 'level_3_bonus' WHERE `config_key` = 'level_bonus_diamond';
-- 修改升级费用配置键
UPDATE `agent_config` SET `config_key` = 'upgrade_to_gold_fee' WHERE `config_key` = 'upgrade_fee_normal_to_gold';
UPDATE `agent_config` SET `config_key` = 'upgrade_to_diamond_fee' WHERE `config_key` = 'upgrade_fee_to_diamond';
-- 修改升级返佣配置键
UPDATE `agent_config` SET `config_key` = 'upgrade_to_gold_rebate' WHERE `config_key` = 'upgrade_rebate_normal_to_gold';
UPDATE `agent_config` SET `config_key` = 'upgrade_to_diamond_rebate' WHERE `config_key` = 'upgrade_rebate_to_diamond';
```
**方案二修改代码逻辑使用SQL中的键名**
需要修改的文件:
- `app/main/api/internal/logic/admin_agent/admingetagentconfiglogic.go`
- `app/main/api/internal/logic/admin_agent/adminupdateagentconfiglogic.go`
**推荐方案一**,因为:
- 代码中的命名更清晰(`level_1_bonus``level_bonus_normal` 更直观)
- 数字后缀与等级数字1/2/3对应易于理解
### 5.3 🔴 立即修复:从配置表读取等级加成
**修改 `agentService.go` 中的 `getLevelBonus` 函数**
```go
// getLevelBonus 获取等级加成(从配置表读取)
func (s *AgentService) getLevelBonus(ctx context.Context, level int64) (int64, error) {
var configKey string
switch level {
case 1:
configKey = "level_1_bonus"
case 2:
configKey = "level_2_bonus"
case 3:
configKey = "level_3_bonus"
default:
return 0, nil
}
bonus, err := s.getConfigFloat(ctx, configKey)
if err != nil {
// 配置不存在时返回默认值
switch level {
case 1:
return 6, nil
case 2:
return 3, nil
case 3:
return 0, nil
}
return 0, nil
}
return int64(bonus), nil
}
```
**修改调用处**
```go
// agentService.go:108
levelBonus, err := s.getLevelBonus(ctx, agent.Level)
if err != nil {
return errors.Wrapf(err, "获取等级加成配置失败")
}
actualBasePrice := basePrice + float64(levelBonus)
```
### 5.4 🟡 补充缺失的配置项
**添加 `tax_exemption_amount` 配置**
```sql
INSERT INTO `agent_config` (`config_key`, `config_value`, `config_type`, `description`)
VALUES ('tax_exemption_amount', '0.00', 'tax', '提现免税额度默认0');
```
### 5.5 🟡 规范化配置键命名
**建议统一使用以下命名规范**
```
{类型}_{编号/级别}_{属性}
```
**示例**
-`level_1_bonus` - 等级1的加成
-`level_2_bonus` - 等级2的加成
-`level_3_bonus` - 等级3的加成
-`upgrade_to_gold_fee` - 升级到黄金的费用
-`upgrade_to_diamond_fee` - 升级到钻石的费用
-`upgrade_to_gold_rebate` - 升级到黄金的返佣
-`upgrade_to_diamond_rebate` - 升级到钻石的返佣
### 5.6 🟢 优化建议:添加配置验证
**在更新配置时添加验证逻辑**
```go
// 验证配置值的合理性
func validateConfigValue(key string, value float64) error {
switch key {
case "tax_rate":
if value < 0 || value > 1 {
return errors.New("税率必须在0-1之间")
}
case "price_fee_rate":
if value < 0 || value > 1 {
return errors.New("提价费率必须在0-1之间")
}
case "base_price", "system_max_price":
if value < 0 {
return errors.New("价格配置不能为负数")
}
}
return nil
}
```
---
## 六、配置项完整清单(修正后)
### 6.1 价格相关配置(应移除,改为产品配置)
**⚠️ 重要说明**:以下配置项应该从系统配置表(`agent_config`)中移除,改为在产品配置表(`agent_product_config`)中按产品配置。
| 配置键 | 说明 | 配置位置 |
| ---------------------- | ----------------------------- | ----------------------------- |
| ~~`base_price`~~ | ~~系统基础底价~~ | ✅ 应在 `agent_product_config` |
| ~~`system_max_price`~~ | ~~系统价格上限~~ | ✅ 应在 `agent_product_config` |
| ~~`price_threshold`~~ | ~~提价标准阈值~~ | ✅ 应在 `agent_product_config` |
| ~~`price_fee_rate`~~ | ~~提价手续费比例0-1之间~~ | ✅ 应在 `agent_product_config` |
**产品配置表结构**`agent_product_config`
- `base_price` - 产品基础底价(必填)
- `system_max_price` - 产品价格上限(必填)
- `price_threshold` - 提价标准阈值可选NULL时表示不设阈值
- `price_fee_rate` - 提价手续费比例可选NULL时表示不收费
### 6.2 等级加成配置bonus类型
| 配置键 | 默认值 | 说明 |
| --------------- | ------ | ---------------------- |
| `level_1_bonus` | 6.00 | 普通代理等级加成(元) |
| `level_2_bonus` | 3.00 | 黄金代理等级加成(元) |
| `level_3_bonus` | 0.00 | 钻石代理等级加成(元) |
### 6.3 升级费用配置upgrade类型
| 配置键 | 默认值 | 说明 |
| --------------------------- | ------ | ------------------------ |
| `upgrade_to_gold_fee` | 199.00 | 普通→黄金升级费用(元) |
| `upgrade_to_diamond_fee` | 980.00 | 升级为钻石费用(元) |
| `upgrade_to_gold_rebate` | 139.00 | 普通→黄金返佣金额(元) |
| `upgrade_to_diamond_rebate` | 680.00 | 升级为钻石返佣金额(元) |
**注意**`gold_to_diamond` 的费用和返佣可以通过计算得出:
- 费用:`upgrade_to_diamond_fee - upgrade_to_gold_fee = 980 - 199 = 781元`
- 返佣:可以通过前端或后端计算,不单独存储
### 6.4 返佣规则配置rebate类型
| 配置键 | 默认值 | 说明 |
| ------------------------------ | ------ | ------------------------------ |
| `direct_parent_amount_diamond` | 6.00 | 直接上级是钻石的返佣金额(元) |
| `direct_parent_amount_gold` | 3.00 | 直接上级是黄金的返佣金额(元) |
| `direct_parent_amount_normal` | 2.00 | 直接上级是普通的返佣金额(元) |
| `max_gold_rebate_amount` | 3.00 | 黄金代理最大返佣金额(元) |
**建议**:这些配置项目前在代码中硬编码,建议配置化。
### 6.5 税费配置tax类型
| 配置键 | 默认值 | 说明 |
| ---------------------- | ------ | ---------------------- |
| `tax_rate` | 0.0600 | 提现税率6%即0.06 |
| `tax_exemption_amount` | 0.00 | 免税额度默认0 |
---
## 七、配置表的合理性评估
### 7.1 表结构设计
| 评估项 | 评分 | 说明 |
| ------------ | ----- | ---------------------------- |
| **灵活性** | ⭐⭐⭐⭐⭐ | 键值对设计非常灵活,易于扩展 |
| **可维护性** | ⭐⭐⭐⭐ | 类型分类清晰,便于管理 |
| **性能** | ⭐⭐⭐⭐ | 唯一索引优化查询,缓存支持 |
| **类型安全** | ⭐⭐⭐ | 所有值都是字符串,需要转换 |
| **验证机制** | ⭐⭐ | 缺少数据库层面的验证 |
### 7.2 总体评价
**设计优点**
✅ 采用键值对存储,扩展性强
✅ 类型分类清晰,便于管理
✅ 支持版本控制和软删除
**存在的问题**
**价格配置应该在产品配置表,而不是系统配置表**(严重设计问题)
❌ 配置键命名不一致(严重)
❌ 部分配置硬编码(严重)
❌ 缺少部分配置项(中等)
❌ 缺少配置验证机制(中等)
**总体评分**⭐⭐⭐3/5
**结论**:配置表的设计思路基本合理,但存在**价格配置位置设计错误**的严重问题。价格相关配置应该完全由产品配置表管理,系统配置表只应该包含全局配置(等级加成、升级费用、税费等)。修复这些问题后,配置表设计将更加完善和符合项目规范。
---
## 八、修复后的完整配置SQL
```sql
-- ============================================
-- 代理系统配置初始化(修正版)
-- ============================================
-- 删除价格相关配置(应该在产品配置表中)
DELETE FROM `agent_config` WHERE `config_key` IN (
'base_price',
'system_max_price',
'price_threshold',
'price_fee_rate'
);
-- 删除旧的不一致配置(如果存在)
DELETE FROM `agent_config` WHERE `config_key` IN (
'level_bonus_normal',
'level_bonus_gold',
'level_bonus_diamond',
'upgrade_fee_normal_to_gold',
'upgrade_fee_to_diamond',
'upgrade_rebate_normal_to_gold',
'upgrade_rebate_to_diamond'
);
-- 插入修正后的配置项
INSERT INTO `agent_config` (`config_key`, `config_value`, `config_type`, `description`) VALUES
-- 注意价格相关配置base_price, system_max_price, price_threshold, price_fee_rate
-- 已从系统配置表中移除改为在产品配置表agent_product_config中按产品配置
-- 等级加成配置(修正键名)
('level_1_bonus', '6.00', 'bonus', '普通代理等级加成6元'),
('level_2_bonus', '3.00', 'bonus', '黄金代理等级加成3元'),
('level_3_bonus', '0.00', 'bonus', '钻石代理等级加成0元'),
-- 升级费用配置(修正键名)
('upgrade_to_gold_fee', '199.00', 'upgrade', '普通→黄金升级费用199元'),
('upgrade_to_diamond_fee', '980.00', 'upgrade', '升级为钻石费用980元'),
-- 升级返佣配置(修正键名)
('upgrade_to_gold_rebate', '139.00', 'upgrade', '普通→黄金返佣金额139元'),
('upgrade_to_diamond_rebate', '680.00', 'upgrade', '升级为钻石返佣金额680元'),
-- 返佣规则配置
('direct_parent_amount_diamond', '6.00', 'rebate', '直接上级是钻石的返佣金额6元'),
('direct_parent_amount_gold', '3.00', 'rebate', '直接上级是黄金的返佣金额3元'),
('direct_parent_amount_normal', '2.00', 'rebate', '直接上级是普通的返佣金额2元'),
('max_gold_rebate_amount', '3.00', 'rebate', '黄金代理最大返佣金额3元'),
-- 税费配置
('tax_rate', '0.0600', 'tax', '提现税率6%即0.06'),
('tax_exemption_amount', '0.00', 'tax', '提现免税额度默认0')
ON DUPLICATE KEY UPDATE
`config_value` = VALUES(`config_value`),
`update_time` = NOW();
```
---
## 九、建议的改进方案
### 方案A保持当前键值对设计推荐
**优点**
- 灵活性强,易于扩展
- 不需要修改表结构
- 符合项目当前架构
**需要做的修改**
1. 统一配置键命名
2. 修复代码硬编码问题
3. 补充缺失的配置项
4. 添加配置验证逻辑
### 方案B改为结构化JSON配置可选
**如果配置项继续增长,可以考虑**
- 将相关配置组合成JSON对象存储在 `config_value`
- 例如:`level_bonus_config` 存储 `{"1": 6, "2": 3, "3": 0}`
**缺点**
- 需要修改所有读取配置的代码
- 不利于单个配置项的独立更新
**建议**当前配置项数量不多约15个保持键值对设计更合适。
---
## 十、总结
### 当前状态
- ✅ 配置表设计思路合理
-**价格配置设计错误**(应该在产品配置表,不在系统配置表)
- ❌ 配置键命名不一致(需要立即修复)
- ❌ 部分配置硬编码(需要立即修复)
- ⚠️ 缺少部分配置项(需要补充)
### 修复优先级
1. **P0紧急**:移除系统配置表中的价格配置,改为完全由产品配置表管理
2. **P0紧急**:修改订单处理逻辑,从产品配置表读取价格参数
3. **P0紧急**:统一配置键命名,修复不一致问题
4. **P0紧急**:修改代码,从配置表读取等级加成,移除硬编码
5. **P1重要**:补充缺失的配置项(`tax_exemption_amount`
6. **P2建议**:添加配置验证逻辑
7. **P2建议**:将返佣规则配置化(当前硬编码)
### 是否符合项目
**配置表设计**:✅ 基本符合,但价格配置的位置设计错误
**存在的问题**
1.**价格配置应该在产品配置表中,而不是系统配置表**(严重设计问题)
2. ❌ 配置键命名不一致
3. ❌ 部分配置硬编码
4. ⚠️ 缺少部分配置项
**结论**:需要修复上述问题后才能完全符合项目的配置化设计理念。**最重要的是将价格配置从系统配置表移除,改为完全由产品配置表管理。**
---
## 十一、产品配置表使用规范
### 11.1 产品配置表结构
**表名**: `agent_product_config`
每个产品都应该有对应的配置记录,包含以下字段:
| 字段 | 类型 | 必填 | 说明 |
| ------------------ | ------------- | ---- | ----------------------------------------- |
| `product_id` | bigint | ✅ 是 | 产品ID唯一 |
| `product_name` | varchar(100) | ✅ 是 | 产品名称 |
| `base_price` | decimal(10,2) | ✅ 是 | 产品基础底价 |
| `system_max_price` | decimal(10,2) | ✅ 是 | 产品价格上限 |
| `price_threshold` | decimal(10,2) | ❌ 否 | 提价标准阈值NULL表示不设阈值 |
| `price_fee_rate` | decimal(5,4) | ❌ 否 | 提价手续费比例NULL表示不收费0-1之间 |
### 11.2 配置优先级和默认值
**规则**
1. 所有价格参数必须从产品配置表读取
2. 如果产品配置记录不存在,应该报错或使用合理的默认值(建议报错,强制每个产品必须配置)
3. 可选字段(`price_threshold``price_fee_rate`如果为NULL表示不启用该功能
- `price_threshold = NULL` → 不设提价阈值
- `price_fee_rate = NULL` → 不收取提价手续费
### 11.3 代码修改示例
#### 修改 AgentService.AgentProcess 方法
```go
// AgentProcess 处理代理订单(新系统)
func (s *AgentService) AgentProcess(ctx context.Context, order *model.Order) error {
// 1-3. 前面的代码不变...
// 4. 获取产品配置(必须存在)
productConfig, err := s.AgentProductConfigModel.FindOneByProductId(ctx, order.ProductId)
if err != nil {
if errors.Is(err, model.ErrNotFound) {
return errors.Wrapf(err, "产品配置不存在, productId: %d请先在后台配置产品价格参数", order.ProductId)
}
return errors.Wrapf(err, "查询产品配置失败, productId: %d", order.ProductId)
}
// 使用产品配置的底价
basePrice := productConfig.BasePrice
// 6. 使用事务处理订单
return s.AgentWalletModel.Trans(ctx, func(transCtx context.Context, session sqlx.Session) error {
// 6.1 计算实际底价和代理收益
levelBonus, err := s.getLevelBonus(ctx, agent.Level)
if err != nil {
return errors.Wrapf(err, "获取等级加成配置失败")
}
actualBasePrice := basePrice + float64(levelBonus)
// 6.2 计算提价成本(使用产品配置)
priceThreshold := 0.0
priceFeeRate := 0.0
if productConfig.PriceThreshold.Valid {
priceThreshold = productConfig.PriceThreshold.Float64
}
if productConfig.PriceFeeRate.Valid {
priceFeeRate = productConfig.PriceFeeRate.Float64
}
priceCost := s.calculatePriceCost(agentOrder.SetPrice, priceThreshold, priceFeeRate)
// 6.3 计算代理收益
agentProfit := agentOrder.SetPrice - actualBasePrice - priceCost
// ... 后续代码不变 ...
})
}
```
#### 修改 PaymentLogic 创建订单时的价格计算
```go
// 如果是代理推广订单,创建完整的代理订单记录
if data.AgentIdentifier != "" && agentLinkModel != nil {
// ... 获取代理信息 ...
// 获取产品配置(必须存在)
productConfig, err := l.svcCtx.AgentProductConfigModel.FindOneByProductId(l.ctx, product.Id)
if err != nil {
if errors.Is(err, model.ErrNotFound) {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR),
"生成订单失败,产品配置不存在, productId: %d请先在后台配置产品价格参数", product.Id)
}
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR),
"生成订单, 查询产品配置失败: %+v", err)
}
// 使用产品配置的底价
basePrice := productConfig.BasePrice
// 计算实际底价(基础底价+等级加成)
levelBonus, _ := l.getLevelBonus(agent.Level)
actualBasePrice := basePrice + float64(levelBonus)
// 计算提价成本(使用产品配置)
priceThreshold := 0.0
priceFeeRate := 0.0
if productConfig.PriceThreshold.Valid {
priceThreshold = productConfig.PriceThreshold.Float64
}
if productConfig.PriceFeeRate.Valid {
priceFeeRate = productConfig.PriceFeeRate.Float64
}
priceCost := 0.0
if agentLinkModel.SetPrice > priceThreshold {
priceCost = (agentLinkModel.SetPrice - priceThreshold) * priceFeeRate
}
// 计算代理收益
agentProfit := agentLinkModel.SetPrice - actualBasePrice - priceCost
// ... 创建代理订单记录 ...
}
```
### 11.4 需要修改的文件清单
需要修改以支持产品配置的代码文件:
1. **`app/main/api/internal/service/agentService.go`**
- `AgentProcess()` 方法:从产品配置表读取价格参数
2. **`app/main/api/internal/logic/pay/paymentlogic.go`**
- 创建订单时的价格计算:从产品配置表读取价格参数
3. **`app/main/api/internal/logic/agent/getagentproductconfiglogic.go`**
- 移除从系统配置表读取价格参数的逻辑(只保留从产品配置表读取)
4. **`app/main/api/internal/logic/admin_agent/admingetagentconfiglogic.go`**
- 移除价格相关配置项的返回
5. **`app/main/api/internal/logic/admin_agent/adminupdateagentconfiglogic.go`**
- 移除价格相关配置项的更新逻辑
6. **后台管理系统前端**
- 系统配置页面:移除价格相关配置项
- 产品配置页面:确保可以正确配置所有价格参数

View File

@@ -1,136 +0,0 @@
# 佣金冻结功能实现完成总结
## ✅ 已完成的工作
### 1. 数据库表
- ✅ 创建了 `agent_freeze_task` 表(`deploy/sql/agent_freeze_task_migration.sql`
- ✅ 表结构包含代理ID、订单ID、佣金ID、冻结金额、订单单价、冻结比例、状态、冻结时间、解冻时间等
### 2. 核心业务逻辑
- ✅ 修改了 `giveAgentCommission` 函数(`agentService.go`
- 增加订单单价参数
- 实现冻结逻辑:订单单价 >= 100元时冻结订单单价的10%(可配置)
- 创建冻结任务记录
- 更新钱包余额和冻结余额
### 3. 异步任务系统
- ✅ 创建了解冻任务处理器(`queue/unfreezeCommission.go`
- ✅ 添加了 `SendUnfreezeTask` 方法到 `AsynqService`
- ✅ 在 `agentProcess.go` 中,代理处理成功后自动发送解冻任务
- ✅ 注册了解冻任务路由
### 4. 配置支持
- ✅ 冻结比例从配置表 `agent_config` 读取,配置键为 `commission_freeze_ratio`
### 5. ServiceContext 更新
- ✅ 添加了 `AgentFreezeTaskModel` 字段
- ✅ 初始化了 `agentFreezeTaskModel`
- ✅ 传递给 `AgentService`
- ✅ 添加到返回的 `ServiceContext`
## 📋 功能说明
### 触发条件
- 订单单价 >= 配置阈值默认100元可通过 `commission_freeze_threshold` 配置)
### 冻结规则
- 冻结阈值:从配置表读取(配置键:`commission_freeze_threshold`默认100元
- 冻结比例:从配置表读取(配置键:`commission_freeze_ratio`默认10%
- 冻结金额 = 订单单价 × 冻结比例
- 冻结金额不能超过佣金金额
- 例如订单单价140元佣金134元冻结14元140 × 10%实际到账120元134 - 14
### 冻结时长
- 从配置表读取(配置键:`commission_freeze_days`默认30天
- 注意:配置只在创建任务时读取,已创建的任务不受后续配置修改影响
### 解冻机制
- 通过异步任务延迟执行
- 1个月后自动解冻
- 解冻时:`FrozenBalance -= 冻结金额``Balance += 冻结金额`
### 数据持久化
- 冻结任务记录在 `agent_freeze_task` 表中
- 保证任务的一致性和持久化
- 支持任务重试和状态追踪
## 🔧 还需要完成的工作
### 1. 添加配置项
`agent_config` 表中添加配置:
```sql
-- 冻结阈值订单单价达到此金额才触发冻结默认100元
INSERT INTO `agent_config` (`config_key`, `config_value`, `config_type`, `description`)
VALUES ('commission_freeze_threshold', '100', 'rebate', '佣金冻结阈值(订单单价达到此金额才触发冻结,单位:元)');
-- 冻结比例默认10%
INSERT INTO `agent_config` (`config_key`, `config_value`, `config_type`, `description`)
VALUES ('commission_freeze_ratio', '0.1', 'rebate', '佣金冻结比例例如0.1表示10%');
-- 解冻天数默认30天即1个月
INSERT INTO `agent_config` (`config_key`, `config_value`, `config_type`, `description`)
VALUES ('commission_freeze_days', '30', 'rebate', '佣金冻结解冻天数单位例如30表示30天后解冻');
```
### 2. 运行SQL创建表如果还没运行
```bash
mysql -u用户名 -p数据库名 < deploy/sql/agent_freeze_task_migration.sql
```
## 📝 代码文件清单
### 新增文件
1. `deploy/sql/agent_freeze_task_migration.sql` - 冻结任务表SQL
2. `app/main/api/internal/queue/unfreezeCommission.go` - 解冻任务处理器
3. `佣金冻结功能实现说明.md` - 实现说明文档
### 修改文件
1. `app/main/api/internal/service/agentService.go` - 修改佣金发放逻辑
2. `app/main/api/internal/service/asynqService.go` - 添加发送解冻任务方法
3. `app/main/api/internal/queue/agentProcess.go` - 添加发送解冻任务逻辑
4. `app/main/api/internal/queue/routes.go` - 注册解冻任务路由
5. `app/main/api/internal/types/taskname.go` - 添加解冻任务类型
6. `app/main/api/internal/types/payload.go` - 添加解冻任务负载类型
7. `app/main/api/internal/svc/servicecontext.go` - 添加 AgentFreezeTaskModel
## 🎯 功能流程
```
订单支付成功
代理处理AgentProcess
发放佣金giveAgentCommission
判断:订单单价 >= 100元
├─ 是 → 计算冻结金额(订单单价 × 10%
│ 创建冻结任务记录
│ 更新钱包Balance += (佣金-冻结金额), FrozenBalance += 冻结金额
│ 发送解冻异步任务(延迟配置天数)
└─ 否 → 正常发放佣金,不冻结
配置天数后
解冻任务执行UnfreezeCommissionHandler
更新冻结任务状态为已解冻
更新钱包FrozenBalance -= 冻结金额, Balance += 冻结金额
```
## ✅ 代码检查
- ✅ 所有文件已通过编译检查
- ✅ 没有 lint 错误
- ✅ ServiceContext 已正确更新
- ✅ 异步任务路由已注册
## 🚀 下一步
1. 运行SQL创建表如果还没运行
2. 添加配置项到 `agent_config`
3. 测试功能:
- 测试订单单价 < 100元不应冻结
- 测试订单单价 >= 100元应冻结订单单价的10%
- 测试冻结金额超过佣金:应冻结全部佣金
- 测试解冻任务:在配置的天数后应自动解冻

View File

@@ -1,106 +0,0 @@
# 佣金冻结功能实现说明
## 功能需求
当订单单价 >= 配置阈值默认100元抽取订单单价的配置比例默认10%放到冻结余额冻结配置天数默认30天后自动解冻。所有配置项都可在配置表中修改但配置只在创建任务时读取已创建的任务不受后续配置修改影响。
## 实现步骤
### 1. 运行SQL创建冻结任务表
```bash
# 执行SQL文件
mysql -u用户名 -p数据库名 < deploy/sql/agent_freeze_task_migration.sql
```
### 2. 生成Model
运行代码生成工具生成 `AgentFreezeTaskModel`
```bash
# 在 ycc-proxy-server 目录下运行
goctl model mysql datasource -url="数据库连接字符串" -table="agent_freeze_task" -dir="./app/main/model" -cache
```
### 3. 更新ServiceContext
`app/main/api/internal/svc/servicecontext.go` 中:
#### 3.1 添加字段
```go
AgentFreezeTaskModel model.AgentFreezeTaskModel
```
#### 3.2 初始化Model
`NewServiceContext` 函数中添加:
```go
agentFreezeTaskModel := model.NewAgentFreezeTaskModel(db, cacheConf)
```
#### 3.3 传递给AgentService
修改 `NewAgentService` 调用,添加参数:
```go
agentService := service.NewAgentService(c, orderModel, agentModel, agentWalletModel,
agentRelationModel, agentLinkModel, agentOrderModel, agentCommissionModel, agentRebateModel,
agentUpgradeModel, agentWithdrawalModel, agentConfigModel, agentProductConfigModel,
agentRealNameModel, agentWithdrawalTaxModel, agentFreezeTaskModel) // 添加这个参数
```
#### 3.4 添加到ServiceContext返回
在返回的 `ServiceContext` 结构体中添加:
```go
AgentFreezeTaskModel: agentFreezeTaskModel,
```
### 4. 添加配置项
`agent_config` 表中添加配置:
```sql
-- 冻结阈值订单单价达到此金额才触发冻结默认100元
INSERT INTO `agent_config` (`config_key`, `config_value`, `config_type`, `description`)
VALUES ('commission_freeze_threshold', '100', 'rebate', '佣金冻结阈值(订单单价达到此金额才触发冻结,单位:元)');
-- 冻结比例默认10%
INSERT INTO `agent_config` (`config_key`, `config_value`, `config_type`, `description`)
VALUES ('commission_freeze_ratio', '0.1', 'rebate', '佣金冻结比例例如0.1表示10%');
-- 解冻天数默认30天即1个月
INSERT INTO `agent_config` (`config_key`, `config_value`, `config_type`, `description`)
VALUES ('commission_freeze_days', '30', 'rebate', '佣金冻结解冻天数单位例如30表示30天后解冻');
```
### 5. 代码已完成的修改
#### 5.1 已修改的文件
-`deploy/sql/agent_freeze_task_migration.sql` - 创建冻结任务表
-`app/main/api/internal/service/agentService.go` - 修改 `giveAgentCommission` 函数,增加冻结逻辑
-`app/main/api/internal/service/asynqService.go` - 添加 `SendUnfreezeTask` 方法
-`app/main/api/internal/queue/unfreezeCommission.go` - 创建解冻任务处理器
-`app/main/api/internal/queue/routes.go` - 注册解冻任务路由
-`app/main/api/internal/queue/agentProcess.go` - 在代理处理成功后发送解冻任务
-`app/main/api/internal/types/taskname.go` - 添加解冻任务类型
-`app/main/api/internal/types/payload.go` - 添加解冻任务负载类型
#### 5.2 核心逻辑说明
**冻结逻辑**(在 `giveAgentCommission` 中):
1. 判断订单单价是否 >= 100元
2. 如果是从配置表读取冻结比例默认10%
3. 计算冻结金额 = 订单单价 × 冻结比例(不超过佣金金额)
4. 创建冻结任务记录(状态=待解冻,解冻时间=从配置读取的天数后)
5. 更新钱包:`Balance += (佣金金额 - 冻结金额)``FrozenBalance += 冻结金额`
**解冻逻辑**(在 `unfreezeCommission.go` 中):
1. 查询冻结任务
2. 检查解冻时间是否已到
3. 更新冻结任务状态为已解冻
4. 更新钱包:`FrozenBalance -= 冻结金额``Balance += 冻结金额`
### 6. 注意事项
1. **Model生成**必须先运行SQL并生成Model否则代码无法编译
2. **配置项**:需要在 `agent_config` 表中添加 `commission_freeze_ratio` 配置项
3. **异步任务**:解冻任务通过 asynq 延迟执行,确保在配置的天数后自动解冻
4. **数据一致性**:所有操作都在事务中执行,保证数据一致性
### 7. 测试建议
1. 测试订单单价 < 100元不应冻结
2. 测试订单单价 >= 100元应冻结订单单价的10%
3. 测试冻结金额超过佣金:应冻结全部佣金
4. 测试解冻任务:在配置的天数后应自动解冻

View File

@@ -1,213 +0,0 @@
# 删除推广订单功能计划
## 目标
删除后台以及后端中promotion推广订单相关功能但**保留推广数据分析和推广链接管理**功能。
## 功能区分
### ✅ 需要保留的功能
1. **推广链接管理**
- 创建推广链接
- 更新推广链接
- 删除推广链接
- 获取推广链接列表
- 获取推广链接详情
- 记录链接点击
2. **推广数据分析**
- 获取推广总统计
- 获取推广历史记录
### ❌ 需要删除的功能
1. **推广订单功能**
- AdminPromotionOrder 模型及相关代码
- 订单管理中的 IsPromotion 字段
- 订单创建/更新/删除中的推广订单逻辑
- 前端订单管理中的推广订单字段
---
## 详细删除计划
### 阶段一:后端代码清理
#### 1.1 删除推广订单模型文件
- [ ] 删除 `ycc-proxy-server/app/main/model/adminPromotionOrderModel.go`
- [ ] 删除 `ycc-proxy-server/app/main/model/adminPromotionOrderModel_gen.go`
- [ ] 确认 `ycc-proxy-server/deploy/script/gen_models.ps1``admin_promotion_order` 已被注释第57行已确认已注释
#### 1.2 从 ServiceContext 中移除推广订单模型
- [ ]`ycc-proxy-server/app/main/api/internal/svc/servicecontext.go` 中删除:
- `AdminPromotionOrderModel` 字段声明
- `AdminPromotionOrderModel` 初始化代码
#### 1.3 修改订单管理相关逻辑
**文件:`ycc-proxy-server/app/main/api/internal/logic/admin_order/admincreateorderlogic.go`**
- [ ] 删除 `IsPromotion` 字段处理逻辑第75-87行
- [ ] 删除 `AdminPromotionOrderModel` 的引用
**文件:`ycc-proxy-server/app/main/api/internal/logic/admin_order/adminupdateorderlogic.go`**
- [ ] 删除推广订单状态处理逻辑第79-101行
- [ ] 删除 `AdminPromotionOrderModel` 的引用
**文件:`ycc-proxy-server/app/main/api/internal/logic/admin_order/admindeleteorderlogic.go`**
- [ ] 删除删除推广订单记录的逻辑第44-51行
- [ ] 删除 `AdminPromotionOrderModel` 的引用
**文件:`ycc-proxy-server/app/main/api/internal/logic/admin_order/admingetorderlistlogic.go`**
- [ ] 删除 `IsPromotion` 查询条件第57-59行
- [ ] 删除判断是否为推广订单的逻辑第277-280行
- [ ] 删除返回结果中的 `IsPromotion` 字段赋值
**文件:`ycc-proxy-server/app/main/api/internal/logic/admin_order/admingetorderdetaillogic.go`**
- [ ] 删除判断是否为推广订单的逻辑第43-47行
- [ ] 删除返回结果中的 `IsPromotion` 字段赋值第121行
#### 1.4 修改 API 定义文件
**文件:`ycc-proxy-server/app/main/api/desc/admin/order.api`**
- [ ]`AdminGetOrderListReq` 中删除 `IsPromotion` 字段第56行
- [ ]`AdminGetOrderListResp``OrderItem` 中删除 `IsPromotion` 字段第83行
- [ ]`AdminGetOrderDetailResp` 中删除 `IsPromotion` 字段第105行
- [ ]`AdminCreateOrderReq` 中删除 `IsPromotion` 字段第119行
- [ ]`AdminUpdateOrderReq` 中删除 `IsPromotion` 字段第137行
#### 1.5 修改 Types 定义
**文件:`ycc-proxy-server/app/main/api/internal/types/types.go`**
- [ ]`AdminGetOrderListReq` 中删除 `IsPromotion` 字段
- [ ]`AdminGetOrderListResp``OrderItem` 中删除 `IsPromotion` 字段
- [ ]`AdminGetOrderDetailResp` 中删除 `IsPromotion` 字段
- [ ]`AdminCreateOrderReq` 中删除 `IsPromotion` 字段
- [ ]`AdminUpdateOrderResp` 中删除 `IsPromotion` 字段
#### 1.6 重新生成代码
- [ ] 运行 `goctl api go -api desc/admin/order.api -dir . -style gozero`
- [ ] 检查生成的代码是否正确
---
### 阶段二:前端代码清理
#### 2.1 修改订单管理页面
**文件:`ycc-proxy-admin/apps/web-antd/src/views/order/order/data.ts`**
- [ ] 删除推广订单列定义第110-121行
- [ ] 删除表单中的推广订单字段第224-225行
**文件:`ycc-proxy-admin/apps/web-antd/src/api/order/order.ts`**
- [ ] 从接口类型定义中删除 `is_promotion` 字段第19行
#### 2.2 检查其他前端文件
- [ ] 搜索前端代码中是否还有其他地方引用了 `is_promotion``IsPromotion`
- [ ] 删除所有相关引用
---
### 阶段三:数据库清理(可选)
#### 3.1 数据库表处理
- [ ] 确认是否删除 `admin_promotion_order`
- 如果保留历史数据,可以保留表但不使用
- 如果需要完全清理可以执行删除表的SQL
#### 3.2 数据库迁移脚本(如需要)
- [ ] 创建迁移脚本,删除 `admin_promotion_order` 表(如果决定删除)
---
### 阶段四:验证和测试
#### 4.1 功能验证
- [ ] 验证推广链接管理功能正常
- [ ] 验证推广数据分析功能正常
- [ ] 验证订单创建功能正常(不再有推广订单选项)
- [ ] 验证订单更新功能正常(不再有推广订单选项)
- [ ] 验证订单列表查询功能正常(不再显示推广订单字段)
- [ ] 验证订单详情查看功能正常(不再显示推广订单字段)
#### 4.2 代码检查
- [ ] 运行 `go build` 确保后端代码编译通过
- [ ] 运行前端构建确保前端代码正常
- [ ] 检查是否有编译错误或警告
- [ ] 检查是否有未使用的导入
---
## 注意事项
1. **保留的功能不受影响**
- 推广链接管理(`AdminPromotionLink`)完全保留
- 推广数据分析(`AdminPromotionLinkStatsTotal`, `AdminPromotionLinkStatsHistory`)完全保留
- 记录链接点击功能完全保留
2. **数据库表处理**
- `admin_promotion_order` 表可以保留(用于历史数据),但不再使用
- 或者完全删除该表(需要确认是否有历史数据需要保留)
3. **代码生成**
- 修改 `.api` 文件后需要重新生成代码
- 确保生成的代码与手动修改的代码一致
4. **向后兼容**
- 如果前端已经部署,需要确保前端和后端同时更新
- 或者先更新后端,保持向后兼容一段时间
---
## 执行顺序建议
1. **第一步**:修改后端 API 定义文件(`.api` 文件)
2. **第二步**:重新生成后端代码
3. **第三步**:手动修改后端逻辑代码(删除推广订单相关逻辑)
4. **第四步**:修改前端代码
5. **第五步**:测试验证
6. **第六步**:清理数据库(如需要)
---
## 文件清单
### 需要修改的后端文件
1. `ycc-proxy-server/app/main/api/desc/admin/order.api`
2. `ycc-proxy-server/app/main/api/internal/types/types.go`
3. `ycc-proxy-server/app/main/api/internal/logic/admin_order/admincreateorderlogic.go`
4. `ycc-proxy-server/app/main/api/internal/logic/admin_order/adminupdateorderlogic.go`
5. `ycc-proxy-server/app/main/api/internal/logic/admin_order/admindeleteorderlogic.go`
6. `ycc-proxy-server/app/main/api/internal/logic/admin_order/admingetorderlistlogic.go`
7. `ycc-proxy-server/app/main/api/internal/logic/admin_order/admingetorderdetaillogic.go`
8. `ycc-proxy-server/app/main/api/internal/svc/servicecontext.go`
9. `ycc-proxy-server/app/main/model/vars.go`(如需要)
### 需要删除的后端文件
1. `ycc-proxy-server/app/main/model/adminPromotionOrderModel.go`
2. `ycc-proxy-server/app/main/model/adminPromotionOrderModel_gen.go`
### 需要修改的前端文件
1. `ycc-proxy-admin/apps/web-antd/src/views/order/order/data.ts`
2. `ycc-proxy-admin/apps/web-antd/src/api/order/order.ts`
### 需要保留的文件(推广链接和数据分析)
1. `ycc-proxy-server/app/main/api/desc/admin/promotion.api`
2. `ycc-proxy-server/app/main/api/internal/logic/admin_promotion/*`
3. `ycc-proxy-server/app/main/api/internal/handler/admin_promotion/*`
4. `ycc-proxy-server/app/main/model/adminPromotionLinkModel*`
5. `ycc-proxy-server/app/main/model/adminPromotionLinkStats*`
6. `ycc-proxy-server/app/main/api/internal/service/adminPromotionLinkStatsService.go`
7. `ycc-proxy-admin/apps/web-antd/src/views/promotion/*`
8. `ycc-proxy-admin/apps/web-antd/src/api/promotion/*`
---
## 完成标志
- [ ] 所有后端代码修改完成
- [ ] 所有前端代码修改完成
- [ ] 代码编译通过
- [ ] 功能测试通过
- [ ] 推广链接管理功能正常
- [ ] 推广数据分析功能正常
- [ ] 订单管理功能正常(无推广订单相关功能)
- [ ] 无编译错误和警告

View File

@@ -1,111 +0,0 @@
# 定时清理报告系统问题分析
## 🔴 严重问题
### 1. **重复插入清理日志记录第129-137行**
**问题**:每批次都插入新的清理日志记录,导致同一个清理任务产生多条日志记录。
**影响**
- 数据冗余,同一个清理任务会有多条日志
- 无法准确统计单次清理的总影响行数
- 日志记录混乱
**当前代码**
```go
// 4. 保存清理日志(每批次都记录)
cleanupLogInsertResult, err := l.svcCtx.QueryCleanupLogModel.Insert(ctx, session, cleanupLog)
```
**应该**:先创建一条日志记录,然后只更新 `AffectedRows` 字段,或者只在最后插入一次。
### 2. **大事务问题第96行**
**问题**:整个清理过程在一个大事务中,如果数据量很大,会导致:
- 事务持续时间过长(可能几小时)
- 锁等待时间过长
- 可能导致事务超时
- 数据库连接占用时间过长
- 如果中途失败,所有操作回滚,已处理的数据无法恢复
**影响**
- 数据库性能下降
- 可能导致其他操作阻塞
- 数据量大时可能失败
**建议**:每个批次使用独立事务,或者使用小事务分批提交。
### 3. **缺少超时控制**
**问题**:没有为整个清理任务设置超时,如果数据量很大,可能会运行很长时间。
**影响**
- 任务可能无限期运行
- 无法及时响应系统关闭信号
**建议**添加超时控制例如最多运行1小时。
### 4. **查询条件缺少排序第100-103行**
**问题**:查询条件没有排序,可能导致每次查询的结果不一致。
**影响**
- 可能导致某些数据被重复处理或遗漏
- 无法保证处理顺序
**建议**:添加 `OrderBy("id ASC")``OrderBy("create_time ASC")`
## 🟡 中等问题
### 5. **缺少进度监控**
**问题**:没有记录处理进度,如果任务中断,无法知道处理到哪里了。
**影响**
- 任务中断后无法恢复
- 无法监控处理进度
**建议**记录已处理的批次数和最后处理的ID。
### 6. **错误处理不够完善**
**问题**:如果某个批次失败,整个事务回滚,但已经处理的数据无法恢复。
**影响**
- 部分数据可能已经处理,但失败后全部回滚
- 无法知道哪些数据已经处理过
**建议**:每个批次使用独立事务,失败时只回滚当前批次。
### 7. **缺少优雅关闭支持**
**问题**:如果服务关闭,正在处理的任务可能被中断。
**影响**
- 数据可能处于不一致状态
- 无法记录已处理的进度
**建议**:检查 context 是否被取消,优雅退出。
## 🟢 小问题
### 8. **日志信息不够详细**
**问题**:日志信息可以更详细,便于排查问题。
**建议**:记录批次处理情况、处理时间等。
### 9. **配置验证不足**
**问题**:没有验证配置值的合理性(如批次大小、保留天数)。
**建议**:添加配置验证,确保配置值在合理范围内。
## 修复建议优先级
### 🔴 高优先级(必须修复)
1. **修复重复插入日志问题** - 影响数据准确性
2. **修复大事务问题** - 影响性能和可靠性
3. **添加查询排序** - 影响数据一致性
### 🟡 中优先级(建议修复)
4. **添加超时控制** - 提高可靠性
5. **改进错误处理** - 提高容错性
6. **添加进度监控** - 便于排查问题
### 🟢 低优先级(可选)
7. **优雅关闭支持** - 如果服务很少重启可以不做
8. **更详细的日志** - 便于排查问题
9. **配置验证** - 提高健壮性

View File

@@ -1,221 +0,0 @@
# 报告查询链路和代理分配逻辑检查报告
## 一、报告查询链路检查
### 1.1 整体流程
```
用户支付订单
支付回调(支付宝/微信)
更新订单状态为 "paid"
发送异步任务 SendQueryTask(order.Id)
异步任务处理 PaySuccessNotifyUserHandler.ProcessTask
├─ 创建报告记录query表
├─ 生成授权书
├─ 调用API请求服务获取报告数据
├─ 更新报告状态为 "success"
├─ 调用代理处理 AgentService.AgentProcess
└─ 删除Redis缓存
```
### 1.2 链路检查结果
**链路基本通顺**,但存在以下问题:
#### ✅ 问题1代理处理失败时的处理逻辑 - 已修复
**原问题**
- 代理处理在报告生成成功后同步执行
- 如果代理处理失败,会触发 `handleError`,但报告已经生成成功
**修复方案**
1. ✅ 创建了独立的代理处理异步任务 Handler`app/main/api/internal/queue/agentProcess.go`
2. ✅ 修改 `paySuccessNotify.go` 改为发送异步任务,不再阻塞报告流程
3. ✅ 代理处理失败时只记录日志,不影响报告使用
4. ✅ 保留了手动重试接口供管理员使用
**修复后的代码**
```go
// 报告生成成功后,发送代理处理异步任务(不阻塞报告流程)
if asyncErr := l.svcCtx.AsynqService.SendAgentProcessTask(order.Id); asyncErr != nil {
// 代理处理任务发送失败,只记录日志,不影响报告流程
logx.Errorf("发送代理处理任务失败订单ID: %d, 错误: %v", order.Id, asyncErr)
}
```
### 1.3 支付回调链路
**支付回调链路正常**
- 支付宝回调:`AlipayCallbackLogic.handleQueryOrderPayment`
- 微信回调:`WechatPayCallbackLogic.handleQueryOrderPayment`
- 支付成功后正确发送异步任务
---
## 二、代理分配逻辑检查
### 2.1 代理收益计算
**代理收益计算逻辑正确**
**计算公式**
```go
实际底价 = 基础底价 + 等级加成
提价成本 = (设定价格 - 提价阈值) × 提价手续费比例当设定价格 > 提价阈值时
代理收益 = 设定价格 - 实际底价 - 提价成本
```
**位置**`app/main/api/internal/service/agentService.go:131-132`
**验证**
- ✅ 使用产品配置的底价(从 `agent_product_config` 表读取)
- ✅ 等级加成从配置表读取(支持动态配置)
- ✅ 提价成本计算逻辑正确
### 2.2 等级加成返佣分配
#### 2.2.1 黄金代理等级加成3元
**实现正确**
- 全部给钻石上级
- 找不到钻石上级时,返佣归平台
**位置**`app/main/api/internal/service/agentService.go:233-244`
#### 2.2.2 普通代理等级加成6元
**已按照新规则修复**
**新规则**(已确认并实现):
1. **直接上级是钻石**:等级加成全部给钻石上级
2. **直接上级是黄金**:一部分给黄金上级(配置:`normal_to_gold_rebate`默认3元剩余给钻石上级
3. **直接上级是普通**
- 一部分给直接上级普通(配置:`normal_to_normal_rebate`默认2元
- 剩余金额:
- 有钻石上级:剩余全部给钻石上级
- 只有黄金上级:最多给黄金上级(配置:`normal_to_gold_rebate_max`默认3元超出归平台
- 都没有:全部归平台
**代码实现**`app/main/api/internal/service/agentService.go:254-368`
- ✅ 按照新规则完全重写
- ✅ 支持从配置表读取返佣金额(如果配置不存在使用默认值)
- ✅ 跳过多层普通代理,直接查找钻石/黄金上级
**配置项**
- `normal_to_normal_rebate`普通代理给直接上级普通的金额默认2元
- `normal_to_gold_rebate`普通代理给直接上级黄金的金额默认3元
- `normal_to_gold_rebate_max`普通代理给黄金上级的最大金额默认3元
### 2.3 代理收益分配流程
**分配流程正确**
**流程**
1. 检查是否是代理订单
2. 检查订单是否已处理(防重复)
3. 获取代理信息和产品配置
4. 使用事务处理(保证原子性):
- 计算代理收益
- 更新代理订单状态
- 发放代理佣金到代理钱包
- 分配等级加成返佣给上级链
**位置**`app/main/api/internal/service/agentService.go:76-156`
### 2.4 事务处理和错误处理
**事务处理正确**
**验证**
- ✅ 使用 `AgentWalletModel.Trans` 事务处理
- ✅ 所有数据库操作在同一事务中
- ✅ 任何步骤失败都会回滚
**位置**`app/main/api/internal/service/agentService.go:109`
### 2.5 防重复处理
**防重复处理正确**
**验证**
- ✅ 检查 `agent_order.ProcessStatus == 1` 判断是否已处理
- ✅ 已处理的订单直接返回,不重复处理
**位置**`app/main/api/internal/service/agentService.go:87-91`
---
## 三、其他发现
### 3.1 重试机制
**已有手动重试接口**
**位置**
- 接口:`POST /api/v1/admin/order/retry-agent-process/:id`
- Logic`app/main/api/internal/logic/admin_order/adminretryagentprocesslogic.go`
**功能**
- 管理员可以手动触发代理处理重试
- 可以处理代理处理失败的情况
### 3.2 代理处理失败时的错误处理
**当前行为**
- 代理处理失败会触发 `handleError`
- `handleError` 会尝试退款(如果报告状态为 pending
- 但报告已经生成成功,可能不会触发退款
**建议**
- 代理处理失败不应该触发退款(因为报告已经成功生成)
- 应该记录错误日志,供管理员手动重试
---
## 四、总结和建议
### 4.1 需要确认的问题
1. **普通代理等级加成返佣分配规则**
- 文档和代码实现不一致
- 需要确认正确的业务规则
### 4.2 建议改进
1. **代理处理失败的处理**
- 建议将代理处理改为独立的异步任务,或至少确保失败不影响报告使用
- 失败时只记录日志,不触发退款
2. **文档更新**
- 根据最终确认的业务规则,更新文档或代码
3. **日志增强**
- 在代理处理失败时记录更详细的日志,方便排查问题
### 4.3 整体评价
- ✅ 报告查询链路基本通顺
- ✅ 代理收益计算逻辑正确
- ✅ 事务处理和防重复处理正确
- ⚠️ 普通代理等级加成返佣分配规则需要确认
- ⚠️ 代理处理失败时的处理逻辑可以优化
---
## 五、代码位置索引
### 报告查询链路
- 支付回调:`app/main/api/internal/logic/pay/alipaycallbacklogic.go:56-106`
- 异步任务:`app/main/api/internal/queue/paySuccessNotify.go:35-178`
- 报告查询:`app/main/api/internal/logic/query/querylistlogic.go`
### 代理分配逻辑
- 代理处理入口:`app/main/api/internal/service/agentService.go:76-156`
- 收益计算:`app/main/api/internal/service/agentService.go:131-132`
- 等级加成返佣:`app/main/api/internal/service/agentService.go:226-328`
- 普通代理返佣分配:`app/main/api/internal/service/agentService.go:254-328`

File diff suppressed because it is too large Load Diff

View File

@@ -1,506 +0,0 @@
# 新代理系统完整检查清单
## 检查说明
本文档提供新代理系统的完整检查方案,按照业务流程顺序组织,确保系统逻辑正确性和完整性。
**检查原则**
1. 按照业务流程顺序检查(从注册到提现)
2. 先检查核心链路,再检查辅助功能
3. 每个检查点包含:功能点、关键逻辑、预期结果
4. 重点关注数据一致性、事务完整性、边界条件
---
## 一、基础数据检查
### 1.1 数据表结构
- [ ] 确认所有新表已创建(参考 `deploy/sql/agent_system_migration.sql`
- [ ] 检查表结构是否与文档一致
- [ ] 验证索引是否正确创建
- [ ] 确认所有表都有 `id`, `create_time`, `update_time`, `delete_time`, `del_state`, `version` 字段
**关键表**
- `agent` - 代理基本信息
- `agent_wallet` - 代理钱包
- `agent_relation` - 代理关系
- `agent_link` - 推广链接
- `agent_order` - 代理订单
- `agent_commission` - 代理佣金
- `agent_rebate` - 代理返佣
- `agent_upgrade` - 代理升级
- `agent_withdrawal` - 代理提现
- `agent_withdrawal_tax` - 提现扣税
- `agent_config` - 系统配置
- `agent_product_config` - 产品配置
- `agent_real_name` - 实名认证
- `agent_invite_code` - 邀请码
### 1.2 系统配置初始化
- [ ] 检查 `agent_config` 表是否有基础配置数据
- [ ] 验证关键配置项是否存在:
- `base_price` - 基础底价
- `system_max_price` - 系统价格上限
- `level_1_bonus` - 普通代理等级加成6元
- `level_2_bonus` - 黄金代理等级加成3元
- `level_3_bonus` - 钻石代理等级加成0元
- `upgrade_to_gold_fee` - 升级为黄金费用199元
- `upgrade_to_diamond_fee` - 升级为钻石费用980元
- `tax_rate` - 税率0.06
- `tax_exemption_amount` - 免税额度
---
## 二、核心业务流程检查
### 2.1 通过邀请码成为代理链路
**入口**`POST /api/v1/agent/apply``POST /api/v1/agent/register/invite`
**检查点**
#### 2.1.1 邀请码验证
- [ ] **文件**`app/main/api/internal/logic/agent/applyforagentlogic.go`
- [ ] 验证邀请码必填(没有邀请码直接拒绝)
- [ ] 验证邀请码存在性(`agent_invite_code` 表)
- [ ] 验证邀请码状态(`status=0` 未使用)
- [ ] 验证邀请码是否过期(`expire_time`
- [ ] 验证邀请码使用后状态更新为已使用(`status=1`
#### 2.1.2 用户注册/绑定
- [ ] 检查用户是否存在(通过手机号查询)
- [ ] 不存在则注册新用户
- [ ] 临时用户则绑定为正式用户
- [ ] 验证手机号加密存储
#### 2.1.3 代理记录创建
- [ ] 检查是否已是代理(防止重复)
- [ ] 根据邀请码的 `target_level` 设置代理等级
- [ ] 创建 `agent` 记录
- [ ] 初始化 `agent_wallet`余额为0
- [ ] 可选字段处理:`region`, `wechat_id` 可以为空
#### 2.1.4 关系建立
- [ ] **代理发放的邀请码**
- [ ] 验证上级代理存在
- [ ] 验证关系是否允许(下级等级不能高于上级)
- [ ] 创建 `agent_relation` 记录(`relation_type=1` 直接关系)
- [ ] 设置 `team_leader_id`(查找上级链中的钻石代理)
- [ ] **平台发放的钻石邀请码**
- [ ] 独立成团队(`team_leader_id = 自己`
#### 2.1.5 邀请码状态更新
- [ ] 更新邀请码状态为已使用(`status=1`
- [ ] 记录使用用户ID和代理ID
- [ ] 记录使用时间
#### 2.1.6 Token生成
- [ ] 生成JWT Token
- [ ] 返回给前端
**测试场景**
1. 正常流程:使用有效邀请码注册
2. 边界条件:邀请码不存在、已使用、已过期
3. 重复注册:已是代理的用户再次申请
4. 关系验证:下级等级高于上级的情况
---
### 2.2 推广链接生成链路
**入口**`POST /api/v1/agent/generating_link`
**检查点**
#### 2.2.1 代理身份验证
- [ ] **文件**`app/main/api/internal/logic/agent/generatinglinklogic.go`
- [ ] 从Token获取用户ID
- [ ] 查询 `agent` 表验证代理身份
- [ ] 非代理用户拒绝
#### 2.2.2 价格计算
- [ ] 获取系统配置(`base_price`, `system_max_price`
- [ ] 计算实际底价 = 基础底价 + 等级加成
- 普通代理:+6元
- 黄金代理:+3元
- 钻石代理:+0元
- [ ] 验证设定价格范围:`实际底价 ≤ 设定价格 ≤ 系统价格上限`
#### 2.2.3 链接生成
- [ ] 检查是否已存在相同链接(`agent_id + product_id + set_price`
- [ ] 构建 `AgentIdentifier` 结构(`AgentID`, `ProductID`, `SetPrice`
- [ ] JSON序列化并AES加密生成 `LinkIdentifier`
- [ ] 保存到 `agent_link`
- [ ] 记录 `set_price``actual_base_price`
**测试场景**
1. 正常生成:有效代理生成推广链接
2. 价格验证:设定价格低于实际底价或高于系统上限
3. 重复链接:相同代理、产品、价格的链接复用
---
### 2.3 订单处理与收益分配链路
**入口**`POST /api/v1/pay/payment` → 支付回调 → `AgentService.AgentProcess`
**检查点**
#### 2.3.1 订单创建
- [ ] **文件**`app/main/api/internal/logic/pay/paymentlogic.go`
- [ ] 解析推广链接标识(解密 `LinkIdentifier`
- [ ] 查询 `agent_link` 表验证链接有效性
- [ ] 创建 `order` 记录(订单金额 = SetPrice
- [ ] 创建 `agent_order` 记录:
- `order_amount` = SetPrice
- `set_price` = 设定价格
- `actual_base_price` = 实际底价(基础底价+等级加成)
- `price_cost` = 提价成本
- `agent_profit` = 代理收益SetPrice - ActualBasePrice - PriceCost
- `process_status` = 0待处理
#### 2.3.2 支付成功处理
- [ ] 支付回调验证签名
- [ ] 更新订单状态
- [ ] 触发 `AgentService.AgentProcess`
#### 2.3.3 代理订单处理
- [ ] **文件**`app/main/api/internal/service/agentService.go` - `AgentProcess`
- [ ] 检查是否是代理订单(查询 `agent_order` 表)
- [ ] 检查订单是否已处理(`process_status=1` 防重复)
- [ ] 获取代理信息和系统配置
- [ ] 使用事务处理:
- 更新 `agent_order` 状态为处理成功
- 创建 `agent_commission` 记录
- 更新 `agent_wallet``balance += agent_profit`, `total_earnings += agent_profit`
#### 2.3.4 等级加成返佣分配
- [ ] **文件**`app/main/api/internal/service/agentService.go` - `distributeLevelBonus`
- [ ] **普通代理6元**
- [ ] 给直接上级最多3元
- [ ] 剩余给钻石上级(如有)
- [ ] 如果无钻石上级给黄金上级最多3元
- [ ] **黄金代理3元**
- [ ] 全部给钻石上级(如有)
- [ ] **钻石代理0元**
- [ ] 无返佣
- [ ] 创建 `agent_rebate` 记录
- [ ] 更新上级钱包余额
**测试场景**
1. 正常订单:普通代理订单,验证收益和返佣分配
2. 防重复:已处理订单再次处理
3. 返佣分配:不同等级代理的返佣分配逻辑
4. 无上级:没有上级代理时的返佣处理
---
### 2.4 代理升级链路
#### 2.4.1 自主付费升级
**入口**`POST /api/v1/agent/upgrade/apply`
**检查点**
- [ ] **文件**`app/main/api/internal/logic/agent/applyupgradelogic.go`
- [ ] 验证升级条件(普通→黄金、普通→钻石、黄金→钻石)
- [ ] 计算升级费用和返佣
- [ ] 查找原直接上级(用于返佣)
- [ ] 创建 `agent_upgrade` 记录(`status=1` 待处理)
- [ ] 支付成功后调用 `AgentService.ProcessUpgrade`
- [ ] **升级处理**
- [ ] 返佣给原直接上级(如需要)
- [ ] 更新代理等级
- [ ] 检查是否需要脱离直接上级关系
- [ ] 更新团队首领(升级为钻石时独立成团队)
- [ ] 更新所有下级的 `team_leader_id`
#### 2.4.2 钻石升级下级
**入口**`POST /api/v1/agent/upgrade/subordinate`
**检查点**
- [ ] **文件**`app/main/api/internal/logic/agent/upgradesubordinatelogic.go`
- [ ] 验证权限(必须是钻石代理)
- [ ] 验证下级等级(只能是普通代理)
- [ ] 验证关系(必须是直接下级)
- [ ] 验证目标等级(只能升级为黄金)
- [ ] 创建升级记录(`upgrade_type=2``upgrade_fee=0`
- [ ] 执行升级操作
**测试场景**
1. 正常升级:普通→黄金,验证返佣和关系脱离
2. 升级为钻石:验证团队独立
3. 钻石升级下级:验证权限和限制
4. 关系脱离:升级后等级高于上级的情况
---
### 2.5 实名认证链路
**入口**`POST /api/v1/agent/real_name`
**检查点**
- [ ] **文件**`app/main/api/internal/logic/agent/realnameauthlogic.go`
- [ ] 验证代理身份
- [ ] 验证手机号是否匹配
- [ ] 验证手机验证码
- [ ] 三要素核验(姓名、身份证号、手机号)
- [ ] 加密身份证号和手机号
- [ ] 保存实名认证记录(`verify_time` 不为空表示已通过)
- [ ] 无需人工审核,核验通过即生效
**测试场景**
1. 正常认证:三要素核验通过
2. 核验失败:三要素不匹配
3. 手机号不匹配:与代理注册手机号不一致
---
### 2.6 提现链路
**入口**`POST /api/v1/agent/withdrawal/apply`
**检查点**
#### 2.6.1 提现申请
- [ ] **文件**`app/main/api/internal/logic/agent/applywithdrawallogic.go`
- [ ] 验证实名认证(`verify_time` 不为空)
- [ ] 验证提现金额(> 0
- [ ] 验证钱包余额(`balance >= amount`
- [ ] 计算税费:
- 查询本月累计提现金额
- 计算剩余免税额度
- 计算应税金额和税费
- [ ] 冻结余额(`frozen_balance += amount`, `balance -= amount`
- [ ] 创建 `agent_withdrawal` 记录(`status=1` 待审核)
- [ ] 创建 `agent_withdrawal_tax` 记录(`tax_status=1` 待扣税)
#### 2.6.2 提现审核
- [ ] **文件**`app/main/api/internal/logic/admin_agent/adminauditwithdrawallogic.go`
- [ ] 审核通过:调用支付宝转账接口
- [ ] 审核拒绝:解冻余额
- [ ] 转账成功:解冻并扣除余额,更新扣税状态
- [ ] 转账失败:解冻余额
**测试场景**
1. 正常提现:实名认证通过,余额充足
2. 税费计算:验证月度累计和免税额度
3. 余额不足:提现金额大于余额
4. 未实名:未完成实名认证
---
## 三、查询接口检查
### 3.1 代理信息查询
- [ ] **文件**`app/main/api/internal/logic/agent/getagentinfologic.go`
- [ ] 查询代理基本信息
- [ ] 查询实名认证状态(`verify_time` 不为空)
- [ ] 处理可选字段(`region`, `wechat_id`
### 3.2 收益信息查询
- [ ] **文件**`app/main/api/internal/logic/agent/getrevenueinfologic.go`
- [ ] 查询钱包余额和累计收益
- [ ] 返回 `balance`, `frozen_balance`, `total_earnings`, `withdrawn_amount`
### 3.3 佣金记录查询
- [ ] **文件**`app/main/api/internal/logic/agent/getcommissionlistlogic.go`
- [ ] 分页查询佣金记录
- [ ] 关联查询产品名称
### 3.4 返佣记录查询
- [ ] **文件**`app/main/api/internal/logic/agent/getrebatelistlogic.go`
- [ ] 分页查询返佣记录
- [ ] 显示返佣类型和金额
### 3.5 团队统计查询
- [ ] **文件**`app/main/api/internal/logic/agent/getteamstatisticslogic.go`
- [ ] 递归查询所有下级(直接+间接)
- [ ] 统计总人数、按等级统计
- [ ] 权限检查(只能查看自己团队)
### 3.6 下级列表查询
- [ ] **文件**`app/main/api/internal/logic/agent/getsubordinatelistlogic.go`
- [ ] 分页查询直接下级
- [ ] 显示下级等级和信息
---
## 四、邀请码管理检查
### 4.1 生成邀请码
- [ ] **文件**`app/main/api/internal/logic/agent/generateinvitecodelogic.go`
- [ ] 验证代理身份
- [ ] 生成唯一邀请码
- [ ] 设置目标等级默认1=普通)
- [ ] 保存到 `agent_invite_code`
### 4.2 邀请码列表
- [ ] **文件**`app/main/api/internal/logic/agent/getinvitecodelistlogic.go`
- [ ] 查询代理生成的邀请码
- [ ] 显示状态(未使用、已使用、已失效)
### 4.3 邀请链接
- [ ] **文件**`app/main/api/internal/logic/agent/getinvitelinklogic.go`
- [ ] 生成邀请链接和二维码
- [ ] 链接包含邀请码信息
### 4.4 后台生成钻石邀请码
- [ ] **文件**`app/main/api/internal/logic/admin_agent/admingeneratediamondinvitecodelogic.go`
- [ ] 管理员生成钻石邀请码
- [ ] 目标等级为3钻石
- [ ] 无发放代理ID平台发放
---
## 五、后台管理接口检查
### 5.1 代理列表查询
- [ ] **文件**`app/main/api/internal/logic/admin_agent/admingetagentlistlogic.go`
- [ ] 分页查询代理列表
- [ ] 显示代理等级、实名状态
- [ ] 处理可选字段(`region`, `wechat_id`
### 5.2 代理订单查询
- [ ] **文件**`app/main/api/internal/logic/admin_agent/admingetagentorderlistlogic.go`
- [ ] 分页查询代理订单
- [ ] 显示订单金额、代理收益、处理状态
### 5.3 系统配置管理
- [ ] **文件**`app/main/api/internal/logic/admin_agent/admingetagentconfiglogic.go`
- [ ] 查询系统配置
- [ ] **文件**`app/main/api/internal/logic/admin_agent/adminupdateagentconfiglogic.go`
- [ ] 更新系统配置
### 5.4 产品配置管理
- [ ] **文件**`app/main/api/internal/logic/admin_agent/admingetagentproductconfiglistlogic.go`
- [ ] 查询产品配置列表
- [ ] **文件**`app/main/api/internal/logic/admin_agent/adminupdateagentproductconfiglogic.go`
- [ ] 更新产品配置
---
## 六、关键业务逻辑验证
### 6.1 关系约束验证
- [ ] 下级等级不能高于上级
- [ ] 同级不能作为上下级(除了普通代理)
- [ ] 钻石可以拥有黄金下级(钻石等级高于黄金)
- [ ] 升级后关系脱离逻辑(根据上级等级判断)
### 6.2 团队首领逻辑
- [ ] 钻石代理独立成团队(`team_leader_id = 自己`
- [ ] 普通/黄金代理指向上级链中的钻石代理
- [ ] 升级为钻石时,所有下级跟随到新团队
### 6.3 价格计算逻辑
- [ ] 实际底价 = 基础底价 + 等级加成
- [ ] 提价成本计算(当设定价格 > 提价阈值时)
- [ ] 代理收益 = 设定价格 - 实际底价 - 提价成本
### 6.4 返佣分配逻辑
- [ ] 普通代理6元直接上级3元剩余给钻石/黄金上级
- [ ] 黄金代理3元全部给钻石上级
- [ ] 钻石代理0元无返佣
### 6.5 税费计算逻辑
- [ ] 月度累计提现金额查询
- [ ] 剩余免税额度计算
- [ ] 应税金额和税费计算
---
## 七、数据一致性检查
### 7.1 事务完整性
- [ ] 代理注册:用户创建、代理创建、钱包初始化、关系建立、邀请码更新在同一事务
- [ ] 订单处理:订单状态更新、佣金发放、返佣分配在同一事务
- [ ] 升级处理:等级更新、关系脱离、团队首领更新在同一事务
- [ ] 提现申请:余额冻结、提现记录、扣税记录在同一事务
### 7.2 乐观锁
- [ ] 所有更新操作使用 `version` 字段
- [ ] 使用 `UpdateWithVersion` 方法
- [ ] 更新失败时正确处理
### 7.3 防重复处理
- [ ] 订单处理前检查 `process_status`
- [ ] 升级处理前检查 `status`
- [ ] 邀请码使用后立即更新状态
---
## 八、边界条件和异常处理
### 8.1 边界条件
- [ ] 邀请码不存在、已使用、已过期
- [ ] 已是代理的用户再次申请
- [ ] 下级等级高于上级的情况
- [ ] 设定价格低于实际底价或高于系统上限
- [ ] 余额不足的提现申请
- [ ] 未完成实名认证的提现申请
### 8.2 异常处理
- [ ] 数据库操作失败的回滚
- [ ] 三要素核验失败的处理
- [ ] 支付回调失败的处理
- [ ] 转账失败的处理
---
## 九、性能和安全检查
### 9.1 性能优化
- [ ] 批量查询避免N+1问题
- [ ] 索引使用(`agent_relation`, `agent_rebate`, `agent_order`
- [ ] 递归深度限制(查找上级链时)
### 9.2 安全考虑
- [ ] 手机号、身份证号加密存储
- [ ] 推广链接标识加密传输
- [ ] 权限控制(代理只能查看自己的数据)
- [ ] 金额精度(使用 `decimal` 类型)
---
## 十、测试建议
### 10.1 单元测试
- [ ] 价格计算函数
- [ ] 税费计算函数
- [ ] 返佣分配函数
- [ ] 关系验证函数
### 10.2 集成测试
- [ ] 完整注册流程
- [ ] 完整订单处理流程
- [ ] 完整升级流程
- [ ] 完整提现流程
### 10.3 压力测试
- [ ] 并发订单处理
- [ ] 并发提现申请
- [ ] 大量下级查询
---
## 检查顺序建议
1. **第一阶段**基础数据检查1.1, 1.2
2. **第二阶段**核心业务流程2.1, 2.2, 2.3
3. **第三阶段**升级和实名认证2.4, 2.5
4. **第四阶段**提现流程2.6
5. **第五阶段**:查询接口(三、四)
6. **第六阶段**:后台管理(五)
7. **第七阶段**:业务逻辑验证(六、七、八)
8. **第八阶段**:性能和安全(九)
---
**检查完成后,建议**
1. 记录发现的问题
2. 修复问题后重新检查
3. 编写测试用例覆盖关键流程
4. 进行端到端测试验证

View File

@@ -1,135 +0,0 @@
# 短链系统实现说明
## 一、功能说明
短链系统用于生成推广链接和邀请链接的短链,格式为:`https://推广域名/s/{shortCode}`
### 支持两种类型
1. **推广报告promotion**类型值为1
- 用于推广产品查询服务
- 前端传入目标地址,如:`/agent/promotionInquire/{linkIdentifier}`
2. **邀请好友invite**类型值为2
- 用于邀请用户成为代理
- 前端传入目标地址,如:`/register?invite_code=XXXXX`
### 工作流程
1. 前端调用接口时传入 `target_path`(目标地址)
2. 后端生成6位随机短链标识
3. 短链存储在 `agent_short_link` 表中,包含类型和目标地址
4. 返回短链URL`https://推广域名/s/{shortCode}`
5. 用户访问短链时,根据 `target_path` 重定向到对应页面
## 二、执行步骤
### 1. 执行SQL创建表
```bash
# 执行SQL文件创建短链表
mysql -u root -p your_database < deploy/sql/add_agent_short_link_table.sql
```
或者直接在数据库客户端执行 `deploy/sql/add_agent_short_link_table.sql` 文件。
### 2. 生成Model
使用goctl工具生成Model代码
```bash
# Windows PowerShell
cd ycc-proxy-server
goctl model mysql datasource -url="ycc:5vg67b3UNHu8@tcp(127.0.0.1:21001)/ycc" -table="agent_short_link" -dir="./app/main/model" --home="./deploy/template" -cache=true --style=goZero
# Linux/Mac
cd ycc-proxy-server
goctl model mysql datasource -url="ycc:5vg67b3UNHu8@tcp(127.0.0.1:21001)/ycc" -table="agent_short_link" -dir="./app/main/model" --home="./deploy/template" -cache=true --style=goZero
```
### 3. 代码已实现
代码已经实现完成生成Model后即可使用。
### 4. 配置推广域名
在配置文件中设置推广域名:
```yaml
# main.yaml 或 main.dev.yaml
Promotion:
PromotionDomain: "https://promo.example.com" # 推广域名
```
## 三、接口说明
### 短链重定向接口
- **路径**: `/s/{shortCode}`
- **方法**: `GET`
- **说明**: 不需要 `/api/v1` 前缀,直接放在根路径
- **功能**: 根据短链标识查询对应的推广链接,重定向到推广页面
### 生成推广链接接口
- **路径**: `/api/v1/agent/generating_link`
- **方法**: `POST`
- **请求参数**:
```json
{
"product_id": 1,
"set_price": 10.00,
"target_path": "/agent/promotionInquire/{linkIdentifier}" // 前端传入目标地址
}
```
- **返回**:
```json
{
"link_identifier": "加密的链接标识",
"full_link": "https://推广域名/s/xxxxxx"
}
```
### 生成邀请链接接口
- **路径**: `/api/v1/agent/invite_link`
- **方法**: `GET`
- **请求参数**:
- `invite_code`: 邀请码
- `target_path`: 目标地址(可选,默认:`/register?invite_code=xxx`
- **返回**:
```json
{
"invite_link": "https://推广域名/s/xxxxxx"
}
```
## 四、数据库表结构
### agent_short_link 表
| 字段 | 类型 | 说明 |
|------|------|------|
| id | bigint | 主键ID |
| type | tinyint | 类型1=推广报告2=邀请好友 |
| link_id | bigint | 推广链接ID关联agent_link表仅推广报告使用 |
| invite_code_id | bigint | 邀请码ID关联agent_invite_code表仅邀请好友使用 |
| link_identifier | varchar(200) | 推广链接标识(加密,仅推广报告使用) |
| invite_code | varchar(50) | 邀请码(仅邀请好友使用) |
| short_code | varchar(20) | 短链标识6位随机字符串 |
| target_path | varchar(500) | 目标地址(前端传入,如:/agent/promotionInquire/xxx |
| promotion_domain | varchar(200) | 推广域名 |
| create_time | datetime | 创建时间 |
| update_time | datetime | 更新时间 |
| delete_time | datetime | 删除时间 |
| del_state | tinyint | 删除状态0=未删除1=已删除 |
| version | bigint | 版本号(乐观锁) |
## 五、注意事项
1. 短链标识是6位随机字符串大小写字母+数字)
2. 同一推广链接或邀请码同一类型只会生成一个短链(通过唯一索引保证)
3. 短链重定向使用 `target_path`(相对路径),域名切换由服务器配置处理
4. 如果推广域名未配置,生成短链时会返回空字符串
5. **前端必须传入 `target_path` 参数**,后端只负责确定推广域名并生成短链
6. 推广报告类型:前端传入 `/agent/promotionInquire/{linkIdentifier}`
7. 邀请好友类型:前端传入 `/register?invite_code=XXXXX`(如果不传则使用默认值)

View File

@@ -1,177 +0,0 @@
# 解冻任务优化建议
## 当前实现分析
### 优点
✅ 使用信号量控制并发,避免数据库压力过大
✅ 使用事务和乐观锁保证数据一致性
✅ 双重检查防止重复处理
✅ 错误处理不中断整体流程
### 可改进点
## 1. ⚠️ 超时控制(重要)
**问题**:如果某个任务处理时间过长(比如数据库慢查询),会阻塞整个扫描流程。
**建议**:为每个任务添加超时控制
```go
// 为每个任务设置30秒超时
taskCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
err := l.svcCtx.AgentFreezeTaskModel.Trans(taskCtx, func(transCtx context.Context, session sqlx.Session) error {
// ... 处理逻辑
})
```
## 2. 📊 批次大小限制(可选)
**问题**:如果任务量非常大(比如几千个),一次性查询所有会占用大量内存。
**建议**:如果任务量超过一定数量,可以分批处理
```go
const maxBatchSize = 1000 // 每次最多查询1000个
if len(freezeTasks) > maxBatchSize {
logx.Warnf("任务数量过多(%d),本次只处理前%d个剩余将在下次扫描处理", len(freezeTasks), maxBatchSize)
freezeTasks = freezeTasks[:maxBatchSize]
}
```
## 3. 🔄 错误分类处理
**问题**:所有错误都统一处理,没有区分临时错误和永久错误。
**建议**:区分错误类型,临时错误可以重试,永久错误跳过
```go
if err != nil {
// 判断错误类型
if isTemporaryError(err) {
// 临时错误,记录但继续处理其他任务
logx.Errorf("解冻任务临时失败,将在下次扫描重试: freezeTaskId=%d, err=%v", task.Id, err)
} else {
// 永久错误(如数据异常),记录详细日志
logx.Errorf("解冻任务永久失败: freezeTaskId=%d, err=%v", task.Id, err)
}
}
```
## 4. ⏱️ 处理时间监控
**问题**:缺少处理时间统计,无法评估性能。
**建议**:记录处理时间,便于监控和优化
```go
startTime := time.Now()
// ... 处理逻辑
duration := time.Since(startTime)
logx.Infof("解冻任务处理耗时: freezeTaskId=%d, duration=%v", task.Id, duration)
```
## 5. 🛡️ 优雅关闭支持
**问题**:如果服务关闭,正在处理的任务可能被中断。
**建议**:检查 context 是否被取消
```go
select {
case <-ctx.Done():
logx.Infof("扫描任务被取消,已处理: 成功=%d, 失败=%d", successCount, failCount)
return ctx.Err()
default:
// 继续处理
}
```
## 6. 📈 延迟统计
**问题**:无法知道任务延迟了多久才被处理。
**建议**:记录延迟时间,便于监控
```go
delay := time.Since(currentTask.UnfreezeTime)
if delay > 1*time.Hour {
logx.Warnf("解冻任务延迟处理: freezeTaskId=%d, 延迟=%v", task.Id, delay)
}
```
## 7. 🔍 数据库连接池监控
**问题**:并发处理时可能耗尽数据库连接池。
**建议**:监控连接池使用情况,如果连接数不足,降低并发数
```go
// 可以根据数据库连接池情况动态调整并发数
maxConcurrency := 2
if dbConnPoolAvailable < 5 {
maxConcurrency = 1 // 连接池紧张时降低并发
}
```
## 8. 🎯 幂等性增强
**问题**:虽然有乐观锁,但可以进一步优化。
**建议**:添加更多的幂等性检查
```go
// 检查是否已经解冻过(通过 actual_unfreeze_time
if currentTask.ActualUnfreezeTime.Valid {
logx.Infof("任务已解冻,跳过: freezeTaskId=%d", task.Id)
return nil
}
```
## 9. 📝 更详细的日志
**问题**:日志信息可以更详细,便于排查问题。
**建议**:添加更多上下文信息
```go
logx.Infof("解冻任务详情: freezeTaskId=%d, agentId=%d, amount=%.2f, orderPrice=%.2f, freezeTime=%v, unfreezeTime=%v, delay=%v",
task.Id, currentTask.AgentId, currentTask.FreezeAmount, currentTask.OrderPrice,
currentTask.FreezeTime, currentTask.UnfreezeTime, delay)
```
## 10. 🔧 配置化并发数
**问题**并发数硬编码为2不够灵活。
**建议**:从配置表读取并发数
```go
// 从配置表读取并发数默认2
maxConcurrency, err := l.svcCtx.AgentConfigModel.FindOneByConfigKey(ctx, "unfreeze_max_concurrency")
if err != nil || maxConcurrency == nil {
maxConcurrency = 2 // 默认值
}
```
## 优先级建议
### 🔴 高优先级(建议立即实现)
1. **超时控制** - 防止任务卡死
2. **错误分类处理** - 提高可靠性
### 🟡 中优先级(建议后续优化)
3. **处理时间监控** - 便于性能优化
4. **延迟统计** - 便于监控
5. **更详细的日志** - 便于排查问题
### 🟢 低优先级(可选)
6. **批次大小限制** - 如果任务量不大可以不做
7. **优雅关闭** - 如果服务很少重启可以不做
8. **配置化并发数** - 如果并发数不需要调整可以不做
## 实施建议
建议先实现**超时控制**和**错误分类处理**,这两个对可靠性影响最大。其他优化可以根据实际运行情况逐步添加。

View File

@@ -1,132 +0,0 @@
# 解冻任务实现方案说明
## 方案对比
### 方案1Asynq 延迟任务(已实现但未使用)
**优点:**
- ✅ 精确到秒级执行
- ✅ 自动重试机制
- ✅ 无需额外调度器
**缺点:**
- ❌ 依赖 Redis 持久化Redis 数据丢失会导致任务丢失
- ❌ 系统长时间停机可能导致延迟任务过期
- ❌ 需要补偿机制
### 方案2定时任务扫描✅ 已采用)
**优点:**
-**数据持久化在数据库,更可靠**(核心优势)
-**系统停机后重启,定时任务会继续扫描并处理**(核心优势)
- ✅ 可以批量处理,效率高
- ✅ 已有定时任务基础设施
- ✅ 不依赖 Redis 持久化
**缺点:**
- ⚠️ 执行时间不够精确取决于扫描频率如每5分钟扫描一次
- ⚠️ 需要处理并发扫描(已通过乐观锁解决)
## 最终选择:定时任务扫描方案
### 选择理由
1. **金融场景,可靠性优先**:涉及资金解冻,必须保证任务不丢失
2. **解冻时间允许延迟**:解冻时间可以有一定的延迟(比如几分钟内都可以接受)
3. **已有基础设施**:项目中已有定时任务实现(`cleanQueryData.go`
4. **数据库表已设计好**`status``unfreeze_time` 字段支持扫描查询
## 实现细节
### 1. 定时任务配置
- **执行频率**每2小时执行一次`0 */2 * * *`- 节省性能
- **任务类型**`MsgUnfreezeCommissionScan`
- **处理器**`UnfreezeCommissionScanHandler`
- **批次大小**每次最多处理2个任务避免并发太多
### 2. 扫描逻辑
```go
// 查询条件:
// - status = 1待解冻
// - unfreeze_time <= 当前时间
// - del_state = 0未删除
// - 按 unfreeze_time 升序排序
```
### 3. 并发安全
- 使用**乐观锁**`version` 字段)确保并发安全
- 每个任务在事务中处理,确保原子性
- 双重检查:查询后再次检查状态,防止并发处理
### 4. 错误处理
- 单个任务失败不影响其他任务
- 记录详细的错误日志
- 失败的任务会在下次扫描时重试
### 5. 性能优化
- 使用数据库索引优化查询(`idx_status``idx_unfreeze_time`
- **扫描频率**每2小时扫描一次减少数据库查询压力
- **查询所有任务**:每次扫描找到所有需要解冻的任务(不限制数量)
- **并发控制**使用信号量Semaphore限制最多同时处理2个任务
- **批量处理**:所有任务都会处理,但通过并发控制避免同时处理太多,节省性能
## 代码文件
### 新增文件
- `app/main/api/internal/queue/unfreezeCommissionScan.go` - 定时扫描处理器
### 修改文件
- `app/main/api/internal/queue/routes.go` - 注册定时任务
- `app/main/api/internal/queue/agentProcess.go` - 移除发送延迟任务的逻辑
- `app/main/api/internal/types/taskname.go` - 添加任务类型常量
### 保留文件(备用)
- `app/main/api/internal/queue/unfreezeCommission.go` - 延迟任务处理器(保留作为备用)
- `app/main/api/internal/service/asynqService.go` - `SendUnfreezeTask` 方法(保留作为备用)
## 执行流程
```
定时任务启动每2小时
扫描数据库status=1 AND unfreeze_time <= 当前时间
查询所有需要解冻的任务(不限制数量)
并发处理使用信号量限制最多同时2个
├─ 任务1goroutine 1
├─ 任务2goroutine 2
├─ 任务3等待直到前2个完成
├─ 任务4等待直到前2个完成
└─ ...(以此类推,两个两个处理)
每个任务使用事务 + 乐观锁处理
更新任务状态status = 2已解冻
更新钱包FrozenBalance -= 冻结金额, Balance += 冻结金额
等待所有任务完成
记录日志
```
## 监控建议
1. **监控扫描任务执行情况**
- 检查定时任务是否正常执行
- 监控每次扫描找到的任务数量
- 监控成功/失败数量
2. **监控解冻延迟**
- 记录 `actual_unfreeze_time - unfreeze_time` 的差值
- 如果延迟超过10分钟需要检查定时任务是否正常
3. **监控异常情况**
- 冻结余额不足的情况(数据异常)
- 任务状态异常的情况
## 后续优化建议
1. **可配置扫描频率**将扫描频率当前5分钟配置到配置表
2. **批次大小限制**:如果任务量很大,可以限制每次处理的数量
3. **告警机制**:如果连续多次扫描都失败,发送告警
4. **补偿机制**:提供手动触发扫描的接口,用于紧急情况

View File

@@ -1,232 +0,0 @@
# 退款时代理处理逻辑分析
## 当前流程分析
### 1. 订单支付成功后的流程
```
支付成功
支付回调(支付宝/微信)
更新订单状态为 "paid"
发送异步任务 SendQueryTask(order.Id)
PaySuccessNotifyUserHandler.ProcessTask
├─ 创建查询记录query表状态为 "pending"
├─ 生成授权书
├─ 调用API请求服务第164行← 可能失败
├─ 如果API成功
│ ├─ 更新查询状态为 "success"
│ └─ 发送代理处理任务 SendAgentProcessTask第192行
└─ 如果API失败
└─ handleError → 退款 → 更新订单状态为 "refunded"
```
### 2. 代理订单创建时机
**在支付时创建**`paymentlogic.go:316-327`
- 代理订单在用户支付时创建
- `ProcessStatus = 0`(待处理)
- 此时还没有发放佣金和返佣
### 3. 代理处理任务执行时机
**只在查询成功后发送**`paySuccessNotify.go:192`
```go
// 报告生成成功后,发送代理处理异步任务(不阻塞报告流程)
if asyncErr := l.svcCtx.AsynqService.SendAgentProcessTask(order.Id); asyncErr != nil {
// 代理处理任务发送失败,只记录日志,不影响报告流程
logx.Errorf("发送代理处理任务失败订单ID: %d, 错误: %v", order.Id, asyncErr)
}
```
### 4. API调用失败时的处理第164-167行
```go
combinedResponse, err := l.svcCtx.ApiRequestService.ProcessRequests(decryptData, product.Id)
if err != nil {
return l.handleError(ctx, err, order, query)
}
```
**handleError 的处理**第206-257行
1. 删除Redis缓存
2. 更新查询状态为 `failed`
3. **退款**
4. 更新订单状态为 `refunded`
**关键点**:此时代理处理任务**还没有发送**(因为查询还没成功)
---
## 问题分析
### ✅ 情况1API调用失败退款正常情况
**流程**
1. 支付成功,创建代理订单(`ProcessStatus = 0`
2. 调用API失败第164-167行
3. 进入 `handleError`,退款
4. 更新订单状态为 `refunded`
5. **代理处理任务还没发送**(查询未成功)
**代理状态**
- ✅ 代理订单 `ProcessStatus = 0`(未处理)
- ✅ 代理**没有收到**佣金
- ✅ 代理**没有收到**返佣
-**处理正确**
### ⚠️ 情况2查询成功但订单被退款边界情况
**可能的场景**
1. 支付成功,创建代理订单(`ProcessStatus = 0`
2. 调用API成功查询状态更新为 `success`
3. 发送代理处理任务第192行
4. **但在代理处理任务执行前**,订单被退款(比如管理员手动退款)
**代理处理任务的保护机制**`agentProcess.go:43-46`
```go
// 检查订单状态
if order.Status != "paid" {
logx.Infof("代理处理任务跳过,订单未支付: orderID=%d, status=%s", payload.OrderID, order.Status)
return nil // 订单未支付,不处理,不重试
}
```
**代理状态**
- ✅ 如果订单状态是 `refunded`,代理处理任务会跳过
- ✅ 代理订单 `ProcessStatus` 仍然是 0
- ✅ 代理**没有收到**佣金和返佣
-**处理正确**
### ⚠️ 情况3代理已处理但订单被退款需要处理
**可能的场景**
1. 支付成功,创建代理订单(`ProcessStatus = 0`
2. 调用API成功查询状态更新为 `success`
3. 发送代理处理任务
4. 代理处理任务执行,发放佣金和返佣(`ProcessStatus = 1`
5. **之后**订单被退款(比如管理员手动退款)
**当前问题**
-**代理已经收到佣金和返佣**
-**没有撤销代理收益的逻辑**
-**退款回调中也没有处理代理订单的逻辑**
---
## 发现的问题
### 问题1退款回调中缺少代理订单处理
**当前退款回调逻辑**`wechatpayrefundcallbacklogic.go``alipayrefundcallbacklogic.go`
- ✅ 只更新订单状态和退款记录
-**没有检查代理订单**
-**没有撤销代理收益**
### 问题2管理员手动退款时缺少代理订单处理
**当前管理员退款逻辑**`adminrefundorderlogic.go`
- ✅ 创建退款记录,更新订单状态
-**没有检查代理订单**
-**没有撤销代理收益**
---
## 建议的解决方案
### 方案1在退款回调中处理代理订单推荐
在退款成功回调中,检查代理订单并撤销收益:
```go
// 在 handleQueryOrderRefund 中添加代理订单处理
if status == refunddomestic.STATUS_SUCCESS {
// 更新订单状态
order.Status = orderStatus
// ...
// 检查并处理代理订单
agentOrder, err := l.svcCtx.AgentOrderModel.FindOneByOrderId(ctx, order.Id)
if err == nil && agentOrder.ProcessStatus == 1 {
// 代理订单已处理,需要撤销收益
err = l.svcCtx.AgentService.CancelAgentCommission(ctx, order.Id)
if err != nil {
logx.Errorf("撤销代理收益失败订单ID: %d, 错误: %v", order.Id, err)
// 不阻断退款流程,只记录日志
}
}
}
```
### 方案2在管理员退款时处理代理订单
`AdminRefundOrderLogic` 中添加代理订单检查和处理。
### 方案3在代理处理任务中增加订单状态检查已有保护
当前已有保护机制(`agentProcess.go:43-46`),如果订单状态不是 `paid`,会跳过处理。
---
## 当前处理是否正确?
### ✅ 对于 API 调用失败的情况
**完全正确**
- 如果API调用失败第164-167行会进入退款流程
- 此时代理处理任务还没发送(因为查询未成功)
- 代理订单 `ProcessStatus = 0`,代理没有收到收益
- **处理正确,无需修改**
### ⚠️ 对于已处理代理订单的退款情况
**存在问题**
- 如果代理订单已经处理(`ProcessStatus = 1`),代理已收到佣金和返佣
- 此时订单退款,**没有撤销代理收益的逻辑**
- 这会导致:
- 用户收到退款
- 但代理仍然保留佣金和返佣
- **资金不一致**
---
## 建议修改
### 1. 在退款回调中添加代理订单检查
### 2. 在管理员退款中添加代理订单检查
### 3. 创建撤销代理收益的方法
```go
// CancelAgentCommission 撤销代理收益(订单退款时调用)
func (s *AgentService) CancelAgentCommission(ctx context.Context, orderId int64) error {
// 1. 查找代理订单
// 2. 检查是否已处理
// 3. 撤销佣金(从钱包扣除)
// 4. 撤销返佣(从上级钱包扣除)
// 5. 更新代理订单状态
// 6. 创建撤销记录
}
```
---
## 总结
### 当前情况API调用失败退款
**处理正确**
- API调用失败时代理处理任务还没发送
- 代理订单未处理,代理没有收益
- **无需修改**
### 需要补充的场景
⚠️ **需要处理**
- 代理订单已处理(`ProcessStatus = 1`)后订单退款的情况
- 需要在退款回调和管理员退款中添加撤销代理收益的逻辑

View File

@@ -1,107 +0,0 @@
# 邀请码使用历史功能说明
## 问题描述
当前系统中,`agent_invite_code` 表只记录了最后一次使用情况(`used_user_id`, `used_agent_id`, `used_time`)。对于普通邀请码(可以无限使用),每次使用都会覆盖之前的记录,导致:
1. **无法统计**:无法统计每个邀请码总共邀请了多少代理
2. **无法查询**:无法查询某个代理是通过哪个邀请码成为代理的
3. **历史丢失**:无法保留完整的使用历史记录
## 解决方案
### 1. 创建使用历史表
创建 `agent_invite_code_usage` 表,记录每次邀请码使用的详细信息。
### 2. 在 agent 表中添加字段
`agent` 表中添加 `invite_code_id` 字段,便于直接查询代理是通过哪个邀请码成为的。
## 实施步骤
### 第一步:执行 SQL 迁移
```bash
# 执行 SQL 文件
mysql -u root -p your_database < deploy/sql/add_invite_code_usage_table.sql
```
### 第二步:生成 Model
```bash
# 生成 AgentInviteCodeUsage Model
goctl model mysql datasource -url="root:password@tcp(localhost:3306)/database" \
-table="agent_invite_code_usage" \
-dir="app/main/model" \
-cache=true \
--style=goZero \
--home="./deploy/template"
```
### 第三步:更新 ServiceContext
`app/main/api/internal/svc/servicecontext.go` 中添加:
```go
AgentInviteCodeUsageModel model.AgentInviteCodeUsageModel
```
并在 `NewServiceContext` 中初始化:
```go
AgentInviteCodeUsageModel: model.NewAgentInviteCodeUsageModel(db, cacheConf),
```
### 第四步:更新逻辑代码
更新以下文件,在使用邀请码时记录使用历史:
1. `app/main/api/internal/logic/agent/registerbyinvitecodelogic.go`
2. `app/main/api/internal/logic/agent/applyforagentlogic.go`
### 第五步:更新 Agent Model可选
如果 agent 表添加了 `invite_code_id` 字段,需要重新生成 Agent Model 或手动更新。
## 功能说明
### 统计功能
- **统计邀请码邀请数量**:通过 `agent_invite_code_usage` 表,可以统计每个邀请码邀请了多少代理
- **查询代理来源**:可以通过 `agent.invite_code_id``agent_invite_code_usage.agent_id` 查询代理是通过哪个邀请码成为的
### 数据完整性
- **保留完整历史**:每次使用邀请码都会记录一条使用历史,不会丢失
- **支持多次使用**:普通邀请码可以多次使用,每次使用都会记录
## API 查询示例
### 查询某个邀请码邀请了多少代理
```sql
SELECT COUNT(*) FROM agent_invite_code_usage
WHERE invite_code_id = ? AND del_state = 0;
```
### 查询某个代理是通过哪个邀请码成为的
```sql
-- 方式1通过 agent 表(如果添加了 invite_code_id 字段)
SELECT invite_code_id FROM agent WHERE id = ?;
-- 方式2通过使用历史表
SELECT invite_code_id, code FROM agent_invite_code_usage
WHERE agent_id = ? AND del_state = 0
ORDER BY used_time DESC LIMIT 1;
```
### 查询某个邀请码的所有使用记录
```sql
SELECT * FROM agent_invite_code_usage
WHERE invite_code_id = ? AND del_state = 0
ORDER BY used_time DESC;
```

View File

@@ -1,211 +0,0 @@
# 邀请链接和二维码生成逻辑说明
## 一、邀请链接生成逻辑
### 1. API 端点
- **路径**: `GET /agent/invite_link`
- **处理器**: `GetInviteLinkHandler`
- **逻辑**: `GetInviteLinkLogic`
### 2. 生成流程
#### 步骤 1: 验证代理身份
```go
// 获取当前用户ID
userID := ctxdata.GetUidFromCtx(ctx)
// 查询代理信息
agent := AgentModel.FindOneByUserId(userID)
```
#### 步骤 2: 生成邀请码
- 生成一个8位随机邀请码使用 `tool.Krand(8, tool.KC_RAND_KIND_ALL)`
- 检查邀请码是否已存在最多重试10次
- 创建邀请码记录:
- `agent_id`: 当前代理ID
- `target_level`: 1普通代理
- `status`: 0未使用
- `expire_time`: NULL不过期
- `remark`: "邀请链接生成"
#### 步骤 3: 构建邀请链接
```go
frontendDomain := "https://example.com" // TODO: 需要配置
inviteLink := fmt.Sprintf("%s/register?invite_code=%s", frontendDomain, inviteCode)
```
#### 步骤 4: 生成二维码URL
```go
qrCodeUrl := fmt.Sprintf("%s/api/v1/image/qrcode?type=invitation&content=%s", frontendDomain, inviteLink)
```
### 3. 当前问题
-`frontendDomain` 硬编码为 `"https://example.com"`,需要配置化
- ❌ 二维码API (`/api/v1/image/qrcode`) 可能还未实现
## 二、二维码生成逻辑
### 1. 两种生成方式
#### 方式 A: 前端生成(当前使用)
- **位置**: `ycc-proxy-webview/src/components/QRcode.vue`
- **库**: `qrcode` (npm)
- **逻辑**:
```javascript
// 如果提供了后端返回的 qrCodeUrl优先使用
if (mode === "invitation" && qrCodeUrl) {
// 加载后端生成的二维码图片
qrImg.src = qrCodeUrl;
} else {
// 前端生成二维码
QRCode.toDataURL(url, { width: 150, margin: 0 });
}
```
#### 方式 B: 后端生成已实现服务但可能缺少API端点
- **服务**: `ImageService.ProcessImageWithQRCode()`
- **功能**:
- 支持两种类型:`promote`(推广)和 `invitation`(邀请)
- 加载背景图片(`static/images/yq_qrcode_1.png` 或 `tg_qrcode_1.png`
- 生成二维码并合成到背景图上
- 返回 PNG 格式图片
### 2. 二维码海报生成流程
#### 推广海报promote模式
- **背景图**: `static/images/tg_qrcode_1.png` - `tg_qrcode_8.jpg`8张轮播图
- **二维码位置**: 左下角
- 尺寸: 280px
- 位置: X=192px, Y=距离底部190px
- **用途**: 推广产品查询服务
#### 邀请海报invitation模式
- **背景图**: `static/images/yq_qrcode_1.png`
- **二维码位置**: 中间偏上
- 尺寸: 360px
- 位置: 水平居中垂直位置Y=555px
- **用途**: 邀请好友成为下级代理
### 3. 前端海报合成逻辑
```javascript
// 1. 加载海报背景图
posterImg.src = posterImages[index];
// 2. 生成或加载二维码
if (mode === "invitation" && qrCodeUrl) {
// 使用后端返回的二维码
qrImg.src = qrCodeUrl;
} else {
// 前端生成二维码
QRCode.toDataURL(url);
}
// 3. 在Canvas上绘制
ctx.drawImage(posterImg, 0, 0); // 背景
ctx.drawImage(qrCodeImg, x, y, size, size); // 二维码
```
## 三、数据流程
### 邀请链接流程
```
用户点击"生成邀请链接"
前端调用 GET /agent/invite_link
后端逻辑 (GetInviteLinkLogic):
1. 验证代理身份
2. 生成新的邀请码8位随机不过期
3. 保存到 agent_invite_code 表
4. 构建邀请链接: https://domain/register?invite_code=XXXXX
5. 构建二维码URL: https://domain/api/v1/image/qrcode?type=invitation&content=...
返回 { invite_link, qr_code_url }
前端显示链接和二维码
```
### 二维码使用流程
#### 邀请模式 (invitation)
```
后端返回 qrCodeUrl
前端 QRcode 组件加载二维码图片
如果加载成功: 直接使用后端生成的二维码海报
如果加载失败: 降级到前端生成二维码
合成到邀请海报背景图 (yq_qrcode_1.png)
用户可以保存或分享海报
```
#### 推广模式 (promote)
```
前端生成二维码 (使用 qrcode 库)
合成到推广海报背景图 (tg_qrcode_1.png - 8.png)
用户可以在多张海报中切换
用户可以保存或分享海报
```
## 四、相关数据库表
### agent_invite_code
- 存储邀请码信息
- 每个邀请链接对应一个邀请码记录
- 备注:`"邀请链接生成"` 表示这是通过链接生成的
### agent_invite_code_usage
- 存储邀请码使用历史
- 记录每个代理是通过哪个邀请码成为的
- 支持统计和查询
## 五、待完善的问题
### 1. 前端域名配置
- ❌ 当前硬编码为 `"https://example.com"`
- ✅ 应该从配置文件读取
### 2. 二维码API实现
- ❓ `/api/v1/image/qrcode` 端点是否存在?
- ✅ `ImageService.ProcessImageWithQRCode()` 已实现
- ❓ 是否需要创建对应的 Handler 和路由?
### 3. 邀请码复用
- 当前每次调用都生成新的邀请码
- 是否应该复用已有的邀请码?
## 六、链接格式对比
### 邀请链接(成为代理)
```
格式: https://domain/register?invite_code=XXXXX
参数: invite_code (8位邀请码)
用途: 用户通过此链接注册成为代理
```
### 推广链接(推广产品)
```
格式: https://domain/agent/promotionInquire/{linkIdentifier}
参数: linkIdentifier (加密的JSON字符串包含agent_id, product_id, set_price)
用途: 用户通过此链接查询产品
```
## 七、文件位置
### 后端
- **邀请链接逻辑**: `app/main/api/internal/logic/agent/getinvitelinklogic.go`
- **二维码服务**: `app/main/api/internal/service/imageService.go`
- **推广链接逻辑**: `app/main/api/internal/logic/agent/generatinglinklogic.go`
### 前端
- **二维码组件**: `ycc-proxy-webview/src/components/QRcode.vue`
- **邀请页面**: `ycc-proxy-webview/src/views/Invitation.vue`
- **推广查询页**: `ycc-proxy-webview/src/views/PromotionInquire.vue`