fix
This commit is contained in:
9
.gitignore
vendored
9
.gitignore
vendored
@@ -6,7 +6,6 @@
|
|||||||
**/.DS_Store
|
**/.DS_Store
|
||||||
|
|
||||||
#deploy data
|
#deploy data
|
||||||
|
|
||||||
data/*
|
data/*
|
||||||
!data/.gitkeep
|
!data/.gitkeep
|
||||||
|
|
||||||
@@ -19,6 +18,14 @@ data/*
|
|||||||
|
|
||||||
/tmp/
|
/tmp/
|
||||||
|
|
||||||
|
# 打包出来的可执行文件
|
||||||
/app/api
|
/app/api
|
||||||
|
/app/main/api/main
|
||||||
|
/app/main/api/debug
|
||||||
|
/app/main/api/test
|
||||||
|
|
||||||
|
# 文档目录
|
||||||
|
documents/*
|
||||||
|
!documents/.gitkeep
|
||||||
|
|
||||||
deploy/script/js
|
deploy/script/js
|
||||||
|
|||||||
@@ -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. **之后**订单被退款(比如管理员手动退款)
|
|
||||||
|
|
||||||
这种情况下需要撤销代理收益(需要另外处理,不是当前场景)。
|
|
||||||
|
|
||||||
@@ -1,22 +1,22 @@
|
|||||||
package agent
|
package agent
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"strconv"
|
||||||
"ycc-server/app/main/model"
|
"time"
|
||||||
"ycc-server/common/ctxdata"
|
"ycc-server/app/main/model"
|
||||||
"ycc-server/common/globalkey"
|
"ycc-server/common/ctxdata"
|
||||||
"ycc-server/common/xerr"
|
"ycc-server/common/globalkey"
|
||||||
"ycc-server/pkg/lzkit/crypto"
|
"ycc-server/common/xerr"
|
||||||
"strconv"
|
"ycc-server/pkg/lzkit/crypto"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/zeromicro/go-zero/core/stores/redis"
|
"github.com/zeromicro/go-zero/core/stores/redis"
|
||||||
"github.com/zeromicro/go-zero/core/stores/sqlx"
|
"github.com/zeromicro/go-zero/core/stores/sqlx"
|
||||||
|
|
||||||
"ycc-server/app/main/api/internal/svc"
|
"ycc-server/app/main/api/internal/svc"
|
||||||
"ycc-server/app/main/api/internal/types"
|
"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)
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "加密手机号失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.Referrer == "" {
|
if req.Referrer == "" {
|
||||||
return nil, errors.Wrapf(xerr.NewErrMsg("请填写邀请信息"), "")
|
return nil, errors.Wrapf(xerr.NewErrMsg("请填写邀请信息"), "")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 校验验证码(开发环境下跳过验证码校验)
|
// 2. 校验验证码(开发环境下跳过验证码校验)
|
||||||
if os.Getenv("ENV") != "development" {
|
if os.Getenv("ENV") != "development" {
|
||||||
@@ -115,49 +115,49 @@ func (l *ApplyForAgentLogic) ApplyForAgent(req *types.AgentApplyReq) (resp *type
|
|||||||
return errors.Wrapf(xerr.NewErrMsg("您已经是代理"), "")
|
return errors.Wrapf(xerr.NewErrMsg("您已经是代理"), "")
|
||||||
}
|
}
|
||||||
|
|
||||||
var inviteCodeModel *model.AgentInviteCode
|
var inviteCodeModel *model.AgentInviteCode
|
||||||
var parentAgentId string
|
var parentAgentId string
|
||||||
var targetLevel int64
|
var targetLevel int64
|
||||||
|
|
||||||
inviteCodeModel, err = l.svcCtx.AgentInviteCodeModel.FindOneByCode(transCtx, req.Referrer)
|
inviteCodeModel, err = l.svcCtx.AgentInviteCodeModel.FindOneByCode(transCtx, req.Referrer)
|
||||||
if err != nil && !errors.Is(err, model.ErrNotFound) {
|
if err != nil && !errors.Is(err, model.ErrNotFound) {
|
||||||
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询邀请码失败, %v", err)
|
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询邀请码失败, %v", err)
|
||||||
}
|
}
|
||||||
if inviteCodeModel != nil {
|
if inviteCodeModel != nil {
|
||||||
if inviteCodeModel.Status != 0 {
|
if inviteCodeModel.Status != 0 {
|
||||||
if inviteCodeModel.Status == 1 {
|
if inviteCodeModel.Status == 1 {
|
||||||
return errors.Wrapf(xerr.NewErrMsg("邀请码已使用"), "")
|
return errors.Wrapf(xerr.NewErrMsg("邀请码已使用"), "")
|
||||||
}
|
}
|
||||||
return errors.Wrapf(xerr.NewErrMsg("邀请码已失效"), "")
|
return errors.Wrapf(xerr.NewErrMsg("邀请码已失效"), "")
|
||||||
}
|
}
|
||||||
if inviteCodeModel.ExpireTime.Valid && inviteCodeModel.ExpireTime.Time.Before(time.Now()) {
|
if inviteCodeModel.ExpireTime.Valid && inviteCodeModel.ExpireTime.Time.Before(time.Now()) {
|
||||||
return errors.Wrapf(xerr.NewErrMsg("邀请码已过期"), "")
|
return errors.Wrapf(xerr.NewErrMsg("邀请码已过期"), "")
|
||||||
}
|
}
|
||||||
targetLevel = inviteCodeModel.TargetLevel
|
targetLevel = inviteCodeModel.TargetLevel
|
||||||
if inviteCodeModel.AgentId.Valid {
|
if inviteCodeModel.AgentId.Valid {
|
||||||
parentAgentId = inviteCodeModel.AgentId.String
|
parentAgentId = inviteCodeModel.AgentId.String
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if codeVal, parseErr := strconv.ParseInt(req.Referrer, 10, 64); parseErr == nil && codeVal > 0 {
|
if codeVal, parseErr := strconv.ParseInt(req.Referrer, 10, 64); parseErr == nil && codeVal > 0 {
|
||||||
parentAgent, err := l.findAgentByCode(transCtx, codeVal)
|
parentAgent, err := l.findAgentByCode(transCtx, codeVal)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
parentAgentId = parentAgent.Id
|
parentAgentId = parentAgent.Id
|
||||||
targetLevel = 1
|
targetLevel = 1
|
||||||
} else {
|
} else {
|
||||||
encRefMobile, _ := crypto.EncryptMobile(req.Referrer, l.svcCtx.Config.Encrypt.SecretKey)
|
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), "")
|
agents, findErr := l.svcCtx.AgentModel.FindAll(transCtx, l.svcCtx.AgentModel.SelectBuilder().Where("mobile = ? AND del_state = ?", encRefMobile, globalkey.DelStateNo).Limit(1), "")
|
||||||
if findErr != nil {
|
if findErr != nil {
|
||||||
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询上级代理失败, %v", findErr)
|
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询上级代理失败, %v", findErr)
|
||||||
}
|
}
|
||||||
if len(agents) == 0 {
|
if len(agents) == 0 {
|
||||||
return errors.Wrapf(xerr.NewErrMsg("邀请信息无效"), "")
|
return errors.Wrapf(xerr.NewErrMsg("邀请信息无效"), "")
|
||||||
}
|
}
|
||||||
parentAgentId = agents[0].Id
|
parentAgentId = agents[0].Id
|
||||||
targetLevel = 1
|
targetLevel = 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4.5 创建代理记录
|
// 4.5 创建代理记录
|
||||||
newAgent := &model.Agent{
|
newAgent := &model.Agent{
|
||||||
@@ -279,7 +279,7 @@ func (l *ApplyForAgentLogic) ApplyForAgent(req *types.AgentApplyReq) (resp *type
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 6. 生成并返回token
|
// 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 {
|
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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -105,10 +105,16 @@ func (l *RegisterByInviteCodeLogic) RegisterByInviteCode(req *types.RegisterByIn
|
|||||||
return errors.Wrapf(xerr.NewErrMsg("您已经是代理"), "")
|
return errors.Wrapf(xerr.NewErrMsg("您已经是代理"), "")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果是临时用户(微信环境下),检查手机号是否已绑定其他微信号,并绑定临时用户到正式用户
|
// 检查用户是否有mobile绑定(没有mobile则不能成为代理)
|
||||||
// 注意:非微信环境下 claims 为 nil,此逻辑不会执行,直接使用已存在的 user.Id
|
// 如果是临时用户(微信环境下),需要先绑定手机号
|
||||||
claims, err := ctxdata.GetClaimsFromCtx(l.ctx)
|
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)
|
userAuth, err := l.svcCtx.UserAuthModel.FindOneByUserIdAuthType(l.ctx, user.Id, claims.AuthType)
|
||||||
if err != nil && !errors.Is(err, model.ErrNotFound) {
|
if err != nil && !errors.Is(err, model.ErrNotFound) {
|
||||||
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询用户认证失败, %v", err)
|
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 {
|
if userAuth != nil && userAuth.AuthKey != claims.AuthKey {
|
||||||
return errors.Wrapf(xerr.NewErrMsg("该手机号已绑定其他微信号"), "")
|
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
|
userID = user.Id
|
||||||
@@ -263,7 +264,7 @@ func (l *RegisterByInviteCodeLogic) RegisterByInviteCode(req *types.RegisterByIn
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 5. 生成并返回token
|
// 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 {
|
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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
"ycc-server/app/main/api/internal/service"
|
"ycc-server/app/main/api/internal/service"
|
||||||
"ycc-server/app/main/model"
|
|
||||||
"ycc-server/common/ctxdata"
|
"ycc-server/common/ctxdata"
|
||||||
"ycc-server/common/xerr"
|
"ycc-server/common/xerr"
|
||||||
"ycc-server/pkg/lzkit/crypto"
|
"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) {
|
func (l *QueryServiceLogic) PreprocessLogic(req *types.QueryServiceReq, product string) (*types.QueryServiceResp, error) {
|
||||||
if processor, exists := productProcessors[product]; exists {
|
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) {
|
func (l *QueryServiceLogic) ProcessMarriageLogic(req *types.QueryServiceReq) (*types.QueryServiceResp, error) {
|
||||||
|
|
||||||
// AES解密
|
// AES瑙e瘑
|
||||||
decryptData, DecryptDataErr := l.DecryptData(req.Data)
|
decryptData, DecryptDataErr := l.DecryptData(req.Data)
|
||||||
if DecryptDataErr != nil {
|
if DecryptDataErr != nil {
|
||||||
return nil, DecryptDataErr
|
return nil, DecryptDataErr
|
||||||
}
|
}
|
||||||
|
|
||||||
// 校验参数
|
// 鏍¢獙鍙傛暟
|
||||||
var data types.MarriageReq
|
var data types.MarriageReq
|
||||||
if unmarshalErr := json.Unmarshal(decryptData, &data); unmarshalErr != nil {
|
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 {
|
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)
|
verifyCodeErr := l.VerifyCode(data.Mobile, data.Code)
|
||||||
if verifyCodeErr != nil {
|
if verifyCodeErr != nil {
|
||||||
return nil, verifyCodeErr
|
return nil, verifyCodeErr
|
||||||
}
|
}
|
||||||
|
|
||||||
// 校验三要素
|
// 鏍¢獙涓夎绱?
|
||||||
verifyErr := l.Verify(data.Name, data.IDCard, data.Mobile)
|
verifyErr := l.Verify(data.Name, data.IDCard, data.Mobile)
|
||||||
if verifyErr != nil {
|
if verifyErr != nil {
|
||||||
return nil, verifyErr
|
return nil, verifyErr
|
||||||
}
|
}
|
||||||
|
|
||||||
// 缓存
|
// 缂撳瓨
|
||||||
params := map[string]interface{}{
|
params := map[string]interface{}{
|
||||||
"name": data.Name,
|
"name": data.Name,
|
||||||
"id_card": data.IDCard,
|
"id_card": data.IDCard,
|
||||||
"mobile": data.Mobile,
|
"mobile": data.Mobile,
|
||||||
}
|
}
|
||||||
userID, userType, err := l.GetOrCreateUser()
|
userID, err := l.GetOrCreateUser()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 处理用户失败: %v", err)
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "鏌ヨ鏈嶅姟, 澶勭悊鐢ㄦ埛澶辫触: %v", err)
|
||||||
}
|
}
|
||||||
cacheNo, cacheDataErr := l.CacheData(params, "marriage", userID)
|
cacheNo, cacheDataErr := l.CacheData(params, "marriage", userID)
|
||||||
if cacheDataErr != nil {
|
if cacheDataErr != nil {
|
||||||
return nil, cacheDataErr
|
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 {
|
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()
|
now := time.Now().Unix()
|
||||||
return &types.QueryServiceResp{
|
return &types.QueryServiceResp{
|
||||||
Id: cacheNo,
|
Id: cacheNo,
|
||||||
@@ -123,58 +122,58 @@ func (l *QueryServiceLogic) ProcessMarriageLogic(req *types.QueryServiceReq) (*t
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理家政服务相关逻辑
|
// 澶勭悊瀹舵斂鏈嶅姟鐩稿叧閫昏緫
|
||||||
func (l *QueryServiceLogic) ProcessHomeServiceLogic(req *types.QueryServiceReq) (*types.QueryServiceResp, error) {
|
func (l *QueryServiceLogic) ProcessHomeServiceLogic(req *types.QueryServiceReq) (*types.QueryServiceResp, error) {
|
||||||
|
|
||||||
// AES解密
|
// AES瑙e瘑
|
||||||
decryptData, DecryptDataErr := l.DecryptData(req.Data)
|
decryptData, DecryptDataErr := l.DecryptData(req.Data)
|
||||||
if DecryptDataErr != nil {
|
if DecryptDataErr != nil {
|
||||||
return nil, DecryptDataErr
|
return nil, DecryptDataErr
|
||||||
}
|
}
|
||||||
|
|
||||||
// 校验参数
|
// 鏍¢獙鍙傛暟
|
||||||
var data types.HomeServiceReq
|
var data types.HomeServiceReq
|
||||||
if unmarshalErr := json.Unmarshal(decryptData, &data); unmarshalErr != nil {
|
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 {
|
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)
|
verifyCodeErr := l.VerifyCode(data.Mobile, data.Code)
|
||||||
if verifyCodeErr != nil {
|
if verifyCodeErr != nil {
|
||||||
return nil, verifyCodeErr
|
return nil, verifyCodeErr
|
||||||
}
|
}
|
||||||
|
|
||||||
// 校验三要素
|
// 鏍¢獙涓夎绱?
|
||||||
verifyErr := l.Verify(data.Name, data.IDCard, data.Mobile)
|
verifyErr := l.Verify(data.Name, data.IDCard, data.Mobile)
|
||||||
if verifyErr != nil {
|
if verifyErr != nil {
|
||||||
return nil, verifyErr
|
return nil, verifyErr
|
||||||
}
|
}
|
||||||
|
|
||||||
// 缓存
|
// 缂撳瓨
|
||||||
params := map[string]interface{}{
|
params := map[string]interface{}{
|
||||||
"name": data.Name,
|
"name": data.Name,
|
||||||
"id_card": data.IDCard,
|
"id_card": data.IDCard,
|
||||||
"mobile": data.Mobile,
|
"mobile": data.Mobile,
|
||||||
}
|
}
|
||||||
userID, userType, err := l.GetOrCreateUser()
|
userID, err := l.GetOrCreateUser()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 处理用户失败: %v", err)
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "鏌ヨ鏈嶅姟, 澶勭悊鐢ㄦ埛澶辫触: %v", err)
|
||||||
}
|
}
|
||||||
cacheNo, cacheDataErr := l.CacheData(params, "homeservice", userID)
|
cacheNo, cacheDataErr := l.CacheData(params, "homeservice", userID)
|
||||||
if cacheDataErr != nil {
|
if cacheDataErr != nil {
|
||||||
return nil, cacheDataErr
|
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 {
|
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()
|
now := time.Now().Unix()
|
||||||
return &types.QueryServiceResp{
|
return &types.QueryServiceResp{
|
||||||
Id: cacheNo,
|
Id: cacheNo,
|
||||||
@@ -184,58 +183,58 @@ func (l *QueryServiceLogic) ProcessHomeServiceLogic(req *types.QueryServiceReq)
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理风险评估相关逻辑
|
// 澶勭悊椋庨櫓璇勪及鐩稿叧閫昏緫
|
||||||
func (l *QueryServiceLogic) ProcessRiskAssessmentLogic(req *types.QueryServiceReq) (*types.QueryServiceResp, error) {
|
func (l *QueryServiceLogic) ProcessRiskAssessmentLogic(req *types.QueryServiceReq) (*types.QueryServiceResp, error) {
|
||||||
|
|
||||||
// AES解密
|
// AES瑙e瘑
|
||||||
decryptData, DecryptDataErr := l.DecryptData(req.Data)
|
decryptData, DecryptDataErr := l.DecryptData(req.Data)
|
||||||
if DecryptDataErr != nil {
|
if DecryptDataErr != nil {
|
||||||
return nil, DecryptDataErr
|
return nil, DecryptDataErr
|
||||||
}
|
}
|
||||||
|
|
||||||
// 校验参数
|
// 鏍¢獙鍙傛暟
|
||||||
var data types.RiskAssessmentReq
|
var data types.RiskAssessmentReq
|
||||||
if unmarshalErr := json.Unmarshal(decryptData, &data); unmarshalErr != nil {
|
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 {
|
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)
|
verifyCodeErr := l.VerifyCode(data.Mobile, data.Code)
|
||||||
if verifyCodeErr != nil {
|
if verifyCodeErr != nil {
|
||||||
return nil, verifyCodeErr
|
return nil, verifyCodeErr
|
||||||
}
|
}
|
||||||
|
|
||||||
// 校验三要素
|
// 鏍¢獙涓夎绱?
|
||||||
verifyErr := l.Verify(data.Name, data.IDCard, data.Mobile)
|
verifyErr := l.Verify(data.Name, data.IDCard, data.Mobile)
|
||||||
if verifyErr != nil {
|
if verifyErr != nil {
|
||||||
return nil, verifyErr
|
return nil, verifyErr
|
||||||
}
|
}
|
||||||
|
|
||||||
// 缓存
|
// 缂撳瓨
|
||||||
params := map[string]interface{}{
|
params := map[string]interface{}{
|
||||||
"name": data.Name,
|
"name": data.Name,
|
||||||
"id_card": data.IDCard,
|
"id_card": data.IDCard,
|
||||||
"mobile": data.Mobile,
|
"mobile": data.Mobile,
|
||||||
}
|
}
|
||||||
userID, userType, err := l.GetOrCreateUser()
|
userID, err := l.GetOrCreateUser()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 处理用户失败: %v", err)
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "鏌ヨ鏈嶅姟, 澶勭悊鐢ㄦ埛澶辫触: %v", err)
|
||||||
}
|
}
|
||||||
cacheNo, cacheDataErr := l.CacheData(params, "riskassessment", userID)
|
cacheNo, cacheDataErr := l.CacheData(params, "riskassessment", userID)
|
||||||
if cacheDataErr != nil {
|
if cacheDataErr != nil {
|
||||||
return nil, cacheDataErr
|
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 {
|
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()
|
now := time.Now().Unix()
|
||||||
return &types.QueryServiceResp{
|
return &types.QueryServiceResp{
|
||||||
Id: cacheNo,
|
Id: cacheNo,
|
||||||
@@ -245,57 +244,57 @@ func (l *QueryServiceLogic) ProcessRiskAssessmentLogic(req *types.QueryServiceRe
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理公司信息查询相关逻辑
|
// 澶勭悊鍏徃淇℃伅鏌ヨ鐩稿叧閫昏緫
|
||||||
func (l *QueryServiceLogic) ProcessCompanyInfoLogic(req *types.QueryServiceReq) (*types.QueryServiceResp, error) {
|
func (l *QueryServiceLogic) ProcessCompanyInfoLogic(req *types.QueryServiceReq) (*types.QueryServiceResp, error) {
|
||||||
// AES解密
|
// AES瑙e瘑
|
||||||
decryptData, DecryptDataErr := l.DecryptData(req.Data)
|
decryptData, DecryptDataErr := l.DecryptData(req.Data)
|
||||||
if DecryptDataErr != nil {
|
if DecryptDataErr != nil {
|
||||||
return nil, DecryptDataErr
|
return nil, DecryptDataErr
|
||||||
}
|
}
|
||||||
|
|
||||||
// 校验参数
|
// 鏍¢獙鍙傛暟
|
||||||
var data types.CompanyInfoReq
|
var data types.CompanyInfoReq
|
||||||
if unmarshalErr := json.Unmarshal(decryptData, &data); unmarshalErr != nil {
|
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 {
|
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)
|
verifyCodeErr := l.VerifyCode(data.Mobile, data.Code)
|
||||||
if verifyCodeErr != nil {
|
if verifyCodeErr != nil {
|
||||||
return nil, verifyCodeErr
|
return nil, verifyCodeErr
|
||||||
}
|
}
|
||||||
|
|
||||||
// 校验三要素
|
// 鏍¢獙涓夎绱?
|
||||||
verifyErr := l.Verify(data.Name, data.IDCard, data.Mobile)
|
verifyErr := l.Verify(data.Name, data.IDCard, data.Mobile)
|
||||||
if verifyErr != nil {
|
if verifyErr != nil {
|
||||||
return nil, verifyErr
|
return nil, verifyErr
|
||||||
}
|
}
|
||||||
|
|
||||||
// 缓存
|
// 缂撳瓨
|
||||||
params := map[string]interface{}{
|
params := map[string]interface{}{
|
||||||
"name": data.Name,
|
"name": data.Name,
|
||||||
"id_card": data.IDCard,
|
"id_card": data.IDCard,
|
||||||
"mobile": data.Mobile,
|
"mobile": data.Mobile,
|
||||||
}
|
}
|
||||||
userID, userType, err := l.GetOrCreateUser()
|
userID, err := l.GetOrCreateUser()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 处理用户失败: %v", err)
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "鏌ヨ鏈嶅姟, 澶勭悊鐢ㄦ埛澶辫触: %v", err)
|
||||||
}
|
}
|
||||||
cacheNo, cacheDataErr := l.CacheData(params, "companyinfo", userID)
|
cacheNo, cacheDataErr := l.CacheData(params, "companyinfo", userID)
|
||||||
if cacheDataErr != nil {
|
if cacheDataErr != nil {
|
||||||
return nil, cacheDataErr
|
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 {
|
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()
|
now := time.Now().Unix()
|
||||||
return &types.QueryServiceResp{
|
return &types.QueryServiceResp{
|
||||||
Id: cacheNo,
|
Id: cacheNo,
|
||||||
@@ -305,58 +304,58 @@ func (l *QueryServiceLogic) ProcessCompanyInfoLogic(req *types.QueryServiceReq)
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理租赁信息查询相关逻辑
|
// 澶勭悊绉熻祦淇℃伅鏌ヨ鐩稿叧閫昏緫
|
||||||
func (l *QueryServiceLogic) ProcessRentalInfoLogic(req *types.QueryServiceReq) (*types.QueryServiceResp, error) {
|
func (l *QueryServiceLogic) ProcessRentalInfoLogic(req *types.QueryServiceReq) (*types.QueryServiceResp, error) {
|
||||||
|
|
||||||
// AES解密
|
// AES瑙e瘑
|
||||||
decryptData, DecryptDataErr := l.DecryptData(req.Data)
|
decryptData, DecryptDataErr := l.DecryptData(req.Data)
|
||||||
if DecryptDataErr != nil {
|
if DecryptDataErr != nil {
|
||||||
return nil, DecryptDataErr
|
return nil, DecryptDataErr
|
||||||
}
|
}
|
||||||
|
|
||||||
// 校验参数
|
// 鏍¢獙鍙傛暟
|
||||||
var data types.RentalInfoReq
|
var data types.RentalInfoReq
|
||||||
if unmarshalErr := json.Unmarshal(decryptData, &data); unmarshalErr != nil {
|
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 {
|
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)
|
verifyCodeErr := l.VerifyCode(data.Mobile, data.Code)
|
||||||
if verifyCodeErr != nil {
|
if verifyCodeErr != nil {
|
||||||
return nil, verifyCodeErr
|
return nil, verifyCodeErr
|
||||||
}
|
}
|
||||||
|
|
||||||
// 校验三要素
|
// 鏍¢獙涓夎绱?
|
||||||
verifyErr := l.Verify(data.Name, data.IDCard, data.Mobile)
|
verifyErr := l.Verify(data.Name, data.IDCard, data.Mobile)
|
||||||
if verifyErr != nil {
|
if verifyErr != nil {
|
||||||
return nil, verifyErr
|
return nil, verifyErr
|
||||||
}
|
}
|
||||||
|
|
||||||
// 缓存
|
// 缂撳瓨
|
||||||
params := map[string]interface{}{
|
params := map[string]interface{}{
|
||||||
"name": data.Name,
|
"name": data.Name,
|
||||||
"id_card": data.IDCard,
|
"id_card": data.IDCard,
|
||||||
"mobile": data.Mobile,
|
"mobile": data.Mobile,
|
||||||
}
|
}
|
||||||
userID, userType, err := l.GetOrCreateUser()
|
userID, err := l.GetOrCreateUser()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 处理用户失败: %v", err)
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "鏌ヨ鏈嶅姟, 澶勭悊鐢ㄦ埛澶辫触: %v", err)
|
||||||
}
|
}
|
||||||
cacheNo, cacheDataErr := l.CacheData(params, "rentalinfo", userID)
|
cacheNo, cacheDataErr := l.CacheData(params, "rentalinfo", userID)
|
||||||
if cacheDataErr != nil {
|
if cacheDataErr != nil {
|
||||||
return nil, cacheDataErr
|
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 {
|
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()
|
now := time.Now().Unix()
|
||||||
return &types.QueryServiceResp{
|
return &types.QueryServiceResp{
|
||||||
Id: cacheNo,
|
Id: cacheNo,
|
||||||
@@ -366,58 +365,58 @@ func (l *QueryServiceLogic) ProcessRentalInfoLogic(req *types.QueryServiceReq) (
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理贷前背景检查相关逻辑
|
// 澶勭悊璐峰墠鑳屾櫙妫€鏌ョ浉鍏抽€昏緫
|
||||||
func (l *QueryServiceLogic) ProcessPreLoanBackgroundCheckLogic(req *types.QueryServiceReq) (*types.QueryServiceResp, error) {
|
func (l *QueryServiceLogic) ProcessPreLoanBackgroundCheckLogic(req *types.QueryServiceReq) (*types.QueryServiceResp, error) {
|
||||||
|
|
||||||
// AES解密
|
// AES瑙e瘑
|
||||||
decryptData, DecryptDataErr := l.DecryptData(req.Data)
|
decryptData, DecryptDataErr := l.DecryptData(req.Data)
|
||||||
if DecryptDataErr != nil {
|
if DecryptDataErr != nil {
|
||||||
return nil, DecryptDataErr
|
return nil, DecryptDataErr
|
||||||
}
|
}
|
||||||
|
|
||||||
// 校验参数
|
// 鏍¢獙鍙傛暟
|
||||||
var data types.PreLoanBackgroundCheckReq
|
var data types.PreLoanBackgroundCheckReq
|
||||||
if unmarshalErr := json.Unmarshal(decryptData, &data); unmarshalErr != nil {
|
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 {
|
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)
|
verifyCodeErr := l.VerifyCode(data.Mobile, data.Code)
|
||||||
if verifyCodeErr != nil {
|
if verifyCodeErr != nil {
|
||||||
return nil, verifyCodeErr
|
return nil, verifyCodeErr
|
||||||
}
|
}
|
||||||
|
|
||||||
// 校验三要素
|
// 鏍¢獙涓夎绱?
|
||||||
verifyErr := l.Verify(data.Name, data.IDCard, data.Mobile)
|
verifyErr := l.Verify(data.Name, data.IDCard, data.Mobile)
|
||||||
if verifyErr != nil {
|
if verifyErr != nil {
|
||||||
return nil, verifyErr
|
return nil, verifyErr
|
||||||
}
|
}
|
||||||
|
|
||||||
// 缓存
|
// 缂撳瓨
|
||||||
params := map[string]interface{}{
|
params := map[string]interface{}{
|
||||||
"name": data.Name,
|
"name": data.Name,
|
||||||
"id_card": data.IDCard,
|
"id_card": data.IDCard,
|
||||||
"mobile": data.Mobile,
|
"mobile": data.Mobile,
|
||||||
}
|
}
|
||||||
userID, userType, err := l.GetOrCreateUser()
|
userID, err := l.GetOrCreateUser()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 处理用户失败: %v", err)
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "鏌ヨ鏈嶅姟, 澶勭悊鐢ㄦ埛澶辫触: %v", err)
|
||||||
}
|
}
|
||||||
cacheNo, cacheDataErr := l.CacheData(params, "preloanbackgroundcheck", userID)
|
cacheNo, cacheDataErr := l.CacheData(params, "preloanbackgroundcheck", userID)
|
||||||
if cacheDataErr != nil {
|
if cacheDataErr != nil {
|
||||||
return nil, cacheDataErr
|
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 {
|
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()
|
now := time.Now().Unix()
|
||||||
return &types.QueryServiceResp{
|
return &types.QueryServiceResp{
|
||||||
Id: cacheNo,
|
Id: cacheNo,
|
||||||
@@ -427,57 +426,57 @@ func (l *QueryServiceLogic) ProcessPreLoanBackgroundCheckLogic(req *types.QueryS
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理人事背调相关逻辑
|
// 澶勭悊浜轰簨鑳岃皟鐩稿叧閫昏緫
|
||||||
func (l *QueryServiceLogic) ProcessBackgroundCheckLogic(req *types.QueryServiceReq) (*types.QueryServiceResp, error) {
|
func (l *QueryServiceLogic) ProcessBackgroundCheckLogic(req *types.QueryServiceReq) (*types.QueryServiceResp, error) {
|
||||||
// AES解密
|
// AES瑙e瘑
|
||||||
decryptData, DecryptDataErr := l.DecryptData(req.Data)
|
decryptData, DecryptDataErr := l.DecryptData(req.Data)
|
||||||
if DecryptDataErr != nil {
|
if DecryptDataErr != nil {
|
||||||
return nil, DecryptDataErr
|
return nil, DecryptDataErr
|
||||||
}
|
}
|
||||||
|
|
||||||
// 校验参数
|
// 鏍¢獙鍙傛暟
|
||||||
var data types.BackgroundCheckReq
|
var data types.BackgroundCheckReq
|
||||||
if unmarshalErr := json.Unmarshal(decryptData, &data); unmarshalErr != nil {
|
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 {
|
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)
|
verifyCodeErr := l.VerifyCode(data.Mobile, data.Code)
|
||||||
if verifyCodeErr != nil {
|
if verifyCodeErr != nil {
|
||||||
return nil, verifyCodeErr
|
return nil, verifyCodeErr
|
||||||
}
|
}
|
||||||
|
|
||||||
// 校验三要素
|
// 鏍¢獙涓夎绱?
|
||||||
verifyErr := l.Verify(data.Name, data.IDCard, data.Mobile)
|
verifyErr := l.Verify(data.Name, data.IDCard, data.Mobile)
|
||||||
if verifyErr != nil {
|
if verifyErr != nil {
|
||||||
return nil, verifyErr
|
return nil, verifyErr
|
||||||
}
|
}
|
||||||
|
|
||||||
// 缓存
|
// 缂撳瓨
|
||||||
params := map[string]interface{}{
|
params := map[string]interface{}{
|
||||||
"name": data.Name,
|
"name": data.Name,
|
||||||
"id_card": data.IDCard,
|
"id_card": data.IDCard,
|
||||||
"mobile": data.Mobile,
|
"mobile": data.Mobile,
|
||||||
}
|
}
|
||||||
userID, userType, err := l.GetOrCreateUser()
|
userID, err := l.GetOrCreateUser()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 处理用户失败: %v", err)
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "鏌ヨ鏈嶅姟, 澶勭悊鐢ㄦ埛澶辫触: %v", err)
|
||||||
}
|
}
|
||||||
cacheNo, cacheDataErr := l.CacheData(params, "backgroundcheck", userID)
|
cacheNo, cacheDataErr := l.CacheData(params, "backgroundcheck", userID)
|
||||||
if cacheDataErr != nil {
|
if cacheDataErr != nil {
|
||||||
return nil, cacheDataErr
|
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 {
|
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()
|
now := time.Now().Unix()
|
||||||
return &types.QueryServiceResp{
|
return &types.QueryServiceResp{
|
||||||
Id: cacheNo,
|
Id: cacheNo,
|
||||||
@@ -487,55 +486,55 @@ func (l *QueryServiceLogic) ProcessBackgroundCheckLogic(req *types.QueryServiceR
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
func (l *QueryServiceLogic) ProcessPersonalDataLogic(req *types.QueryServiceReq) (*types.QueryServiceResp, error) {
|
func (l *QueryServiceLogic) ProcessPersonalDataLogic(req *types.QueryServiceReq) (*types.QueryServiceResp, error) {
|
||||||
// AES解密
|
// AES瑙e瘑
|
||||||
decryptData, DecryptDataErr := l.DecryptData(req.Data)
|
decryptData, DecryptDataErr := l.DecryptData(req.Data)
|
||||||
if DecryptDataErr != nil {
|
if DecryptDataErr != nil {
|
||||||
return nil, DecryptDataErr
|
return nil, DecryptDataErr
|
||||||
}
|
}
|
||||||
|
|
||||||
// 校验参数
|
// 鏍¢獙鍙傛暟
|
||||||
var data types.PersonalDataReq
|
var data types.PersonalDataReq
|
||||||
if unmarshalErr := json.Unmarshal(decryptData, &data); unmarshalErr != nil {
|
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 {
|
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)
|
verifyCodeErr := l.VerifyCode(data.Mobile, data.Code)
|
||||||
if verifyCodeErr != nil {
|
if verifyCodeErr != nil {
|
||||||
return nil, verifyCodeErr
|
return nil, verifyCodeErr
|
||||||
}
|
}
|
||||||
|
|
||||||
// 校验三要素
|
// 鏍¢獙涓夎绱?
|
||||||
verifyErr := l.Verify(data.Name, data.IDCard, data.Mobile)
|
verifyErr := l.Verify(data.Name, data.IDCard, data.Mobile)
|
||||||
if verifyErr != nil {
|
if verifyErr != nil {
|
||||||
return nil, verifyErr
|
return nil, verifyErr
|
||||||
}
|
}
|
||||||
|
|
||||||
// 缓存
|
// 缂撳瓨
|
||||||
params := map[string]interface{}{
|
params := map[string]interface{}{
|
||||||
"name": data.Name,
|
"name": data.Name,
|
||||||
"id_card": data.IDCard,
|
"id_card": data.IDCard,
|
||||||
"mobile": data.Mobile,
|
"mobile": data.Mobile,
|
||||||
}
|
}
|
||||||
userID, userType, err := l.GetOrCreateUser()
|
userID, err := l.GetOrCreateUser()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 处理用户失败: %v", err)
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "鏌ヨ鏈嶅姟, 澶勭悊鐢ㄦ埛澶辫触: %v", err)
|
||||||
}
|
}
|
||||||
cacheNo, cacheDataErr := l.CacheData(params, "personalData", userID)
|
cacheNo, cacheDataErr := l.CacheData(params, "personalData", userID)
|
||||||
if cacheDataErr != nil {
|
if cacheDataErr != nil {
|
||||||
return nil, cacheDataErr
|
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 {
|
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()
|
now := time.Now().Unix()
|
||||||
return &types.QueryServiceResp{
|
return &types.QueryServiceResp{
|
||||||
Id: cacheNo,
|
Id: cacheNo,
|
||||||
@@ -545,55 +544,55 @@ func (l *QueryServiceLogic) ProcessPersonalDataLogic(req *types.QueryServiceReq)
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
func (l *QueryServiceLogic) ProcessConsumerFinanceReportLogic(req *types.QueryServiceReq) (*types.QueryServiceResp, error) {
|
func (l *QueryServiceLogic) ProcessConsumerFinanceReportLogic(req *types.QueryServiceReq) (*types.QueryServiceResp, error) {
|
||||||
// AES解密
|
// AES瑙e瘑
|
||||||
decryptData, DecryptDataErr := l.DecryptData(req.Data)
|
decryptData, DecryptDataErr := l.DecryptData(req.Data)
|
||||||
if DecryptDataErr != nil {
|
if DecryptDataErr != nil {
|
||||||
return nil, DecryptDataErr
|
return nil, DecryptDataErr
|
||||||
}
|
}
|
||||||
|
|
||||||
// 校验参数
|
// 鏍¢獙鍙傛暟
|
||||||
var data types.ConsumerFinanceReportReq
|
var data types.ConsumerFinanceReportReq
|
||||||
if unmarshalErr := json.Unmarshal(decryptData, &data); unmarshalErr != nil {
|
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 {
|
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)
|
verifyCodeErr := l.VerifyCode(data.Mobile, data.Code)
|
||||||
if verifyCodeErr != nil {
|
if verifyCodeErr != nil {
|
||||||
return nil, verifyCodeErr
|
return nil, verifyCodeErr
|
||||||
}
|
}
|
||||||
|
|
||||||
// 校验三要素
|
// 鏍¢獙涓夎绱?
|
||||||
verifyErr := l.Verify(data.Name, data.IDCard, data.Mobile)
|
verifyErr := l.Verify(data.Name, data.IDCard, data.Mobile)
|
||||||
if verifyErr != nil {
|
if verifyErr != nil {
|
||||||
return nil, verifyErr
|
return nil, verifyErr
|
||||||
}
|
}
|
||||||
|
|
||||||
// 缓存
|
// 缂撳瓨
|
||||||
params := map[string]interface{}{
|
params := map[string]interface{}{
|
||||||
"name": data.Name,
|
"name": data.Name,
|
||||||
"id_card": data.IDCard,
|
"id_card": data.IDCard,
|
||||||
"mobile": data.Mobile,
|
"mobile": data.Mobile,
|
||||||
}
|
}
|
||||||
userID, userType, err := l.GetOrCreateUser()
|
userID, err := l.GetOrCreateUser()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 处理用户失败: %v", err)
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "鏌ヨ鏈嶅姟, 澶勭悊鐢ㄦ埛澶辫触: %v", err)
|
||||||
}
|
}
|
||||||
cacheNo, cacheDataErr := l.CacheData(params, "consumerFinanceReport", userID)
|
cacheNo, cacheDataErr := l.CacheData(params, "consumerFinanceReport", userID)
|
||||||
if cacheDataErr != nil {
|
if cacheDataErr != nil {
|
||||||
return nil, cacheDataErr
|
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 {
|
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()
|
now := time.Now().Unix()
|
||||||
return &types.QueryServiceResp{
|
return &types.QueryServiceResp{
|
||||||
Id: cacheNo,
|
Id: cacheNo,
|
||||||
@@ -606,43 +605,43 @@ func (l *QueryServiceLogic) DecryptData(data string) ([]byte, error) {
|
|||||||
secretKey := l.svcCtx.Config.Encrypt.SecretKey
|
secretKey := l.svcCtx.Config.Encrypt.SecretKey
|
||||||
key, decodeErr := hex.DecodeString(secretKey)
|
key, decodeErr := hex.DecodeString(secretKey)
|
||||||
if decodeErr != nil {
|
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)
|
decryptData, aesDecryptErr := crypto.AesDecrypt(data, key)
|
||||||
if aesDecryptErr != nil || len(decryptData) == 0 {
|
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
|
return decryptData, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 校验验证码
|
// 鏍¢獙楠岃瘉鐮?
|
||||||
func (l *QueryServiceLogic) VerifyCode(mobile string, code string) error {
|
func (l *QueryServiceLogic) VerifyCode(mobile string, code string) error {
|
||||||
// 开发环境下跳过验证码校验
|
// 寮€鍙戠幆澧冧笅璺宠繃楠岃瘉鐮佹牎楠?
|
||||||
if os.Getenv("ENV") == "development" {
|
if os.Getenv("ENV") == "development" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
secretKey := l.svcCtx.Config.Encrypt.SecretKey
|
secretKey := l.svcCtx.Config.Encrypt.SecretKey
|
||||||
encryptedMobile, err := crypto.EncryptMobile(mobile, secretKey)
|
encryptedMobile, err := crypto.EncryptMobile(mobile, secretKey)
|
||||||
if err != nil {
|
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)
|
codeRedisKey := fmt.Sprintf("%s:%s", "query", encryptedMobile)
|
||||||
cacheCode, err := l.svcCtx.Redis.Get(codeRedisKey)
|
cacheCode, err := l.svcCtx.Redis.Get(codeRedisKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, redis.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 {
|
if cacheCode != code {
|
||||||
return errors.Wrapf(xerr.NewErrMsg("验证码不正确"), "验证码不正确: %s", mobile)
|
return errors.Wrapf(xerr.NewErrMsg("楠岃瘉鐮佷笉姝g‘"), "楠岃瘉鐮佷笉姝g‘: %s", mobile)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 二、三要素验证
|
// 浜屻€佷笁瑕佺礌楠岃瘉
|
||||||
func (l *QueryServiceLogic) Verify(Name string, IDCard string, Mobile string) error {
|
func (l *QueryServiceLogic) Verify(Name string, IDCard string, Mobile string) error {
|
||||||
// 开发环境下跳过二/三要素验证
|
// 寮€鍙戠幆澧冧笅璺宠繃浜?涓夎绱犻獙璇?
|
||||||
if os.Getenv("ENV") == "development" {
|
if os.Getenv("ENV") == "development" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -653,13 +652,13 @@ func (l *QueryServiceLogic) Verify(Name string, IDCard string, Mobile string) er
|
|||||||
}
|
}
|
||||||
verification, err := l.svcCtx.VerificationService.TwoFactorVerification(twoVerification)
|
verification, err := l.svcCtx.VerificationService.TwoFactorVerification(twoVerification)
|
||||||
if err != nil {
|
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 {
|
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 {
|
} else {
|
||||||
// 三要素验证
|
// 涓夎绱犻獙璇?
|
||||||
threeVerification := service.ThreeFactorVerificationRequest{
|
threeVerification := service.ThreeFactorVerificationRequest{
|
||||||
Name: Name,
|
Name: Name,
|
||||||
IDCard: IDCard,
|
IDCard: IDCard,
|
||||||
@@ -667,30 +666,30 @@ func (l *QueryServiceLogic) Verify(Name string, IDCard string, Mobile string) er
|
|||||||
}
|
}
|
||||||
verification, err := l.svcCtx.VerificationService.ThreeFactorVerification(threeVerification)
|
verification, err := l.svcCtx.VerificationService.ThreeFactorVerification(threeVerification)
|
||||||
if err != nil {
|
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 {
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 缓存
|
// 缂撳瓨
|
||||||
func (l *QueryServiceLogic) CacheData(params map[string]interface{}, Product string, userID string) (string, error) {
|
func (l *QueryServiceLogic) CacheData(params map[string]interface{}, Product string, userID string) (string, error) {
|
||||||
agentIdentifier, _ := l.ctx.Value("agentIdentifier").(string)
|
agentIdentifier, _ := l.ctx.Value("agentIdentifier").(string)
|
||||||
secretKey := l.svcCtx.Config.Encrypt.SecretKey
|
secretKey := l.svcCtx.Config.Encrypt.SecretKey
|
||||||
key, decodeErr := hex.DecodeString(secretKey)
|
key, decodeErr := hex.DecodeString(secretKey)
|
||||||
if decodeErr != nil {
|
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)
|
paramsMarshal, marshalErr := json.Marshal(params)
|
||||||
if marshalErr != nil {
|
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)
|
encryptParams, aesEncryptErr := crypto.AesEncrypt(paramsMarshal, key)
|
||||||
if aesEncryptErr != nil {
|
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{
|
queryCache := types.QueryCacheLoad{
|
||||||
Params: encryptParams,
|
Params: encryptParams,
|
||||||
@@ -699,7 +698,7 @@ func (l *QueryServiceLogic) CacheData(params map[string]interface{}, Product str
|
|||||||
}
|
}
|
||||||
jsonData, marshalErr := json.Marshal(queryCache)
|
jsonData, marshalErr := json.Marshal(queryCache)
|
||||||
if marshalErr != nil {
|
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()
|
outTradeNo := "Q_" + l.svcCtx.AlipayService.GenerateOutTradeNo()
|
||||||
redisKey := fmt.Sprintf(types.QueryCacheKey, userID, outTradeNo)
|
redisKey := fmt.Sprintf(types.QueryCacheKey, userID, outTradeNo)
|
||||||
@@ -710,18 +709,18 @@ func (l *QueryServiceLogic) CacheData(params map[string]interface{}, Product str
|
|||||||
return outTradeNo, nil
|
return outTradeNo, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetOrCreateUser 获取或创建用户
|
// GetOrCreateUser 鑾峰彇鎴栧垱寤虹敤鎴?
|
||||||
// 1. 如果上下文中已有用户ID,直接返回
|
// 1. 濡傛灉涓婁笅鏂囦腑宸叉湁鐢ㄦ埛ID锛岀洿鎺ヨ繑鍥?
|
||||||
// 2. 如果是代理查询或APP请求,创建新用户
|
// 2. 濡傛灉鏄唬鐞嗘煡璇㈡垨APP璇锋眰锛屽垱寤烘柊鐢ㄦ埛
|
||||||
// 3. 其他情况返回未登录错误
|
// 3. 鍏朵粬鎯呭喌杩斿洖鏈櫥褰曢敊璇?
|
||||||
func (l *QueryServiceLogic) GetOrCreateUser() (string, int64, error) {
|
func (l *QueryServiceLogic) GetOrCreateUser() (string, error) {
|
||||||
claims, err := ctxdata.GetClaimsFromCtx(l.ctx)
|
claims, err := ctxdata.GetClaimsFromCtx(l.ctx)
|
||||||
if err == nil && claims != nil {
|
if err == nil && claims != nil {
|
||||||
return claims.UserId, claims.UserType, nil
|
return claims.UserId, nil
|
||||||
}
|
}
|
||||||
userID, regErr := l.svcCtx.UserService.RegisterUUIDUser(l.ctx)
|
userID, regErr := l.svcCtx.UserService.RegisterUUIDUser(l.ctx)
|
||||||
if regErr != nil {
|
if regErr != nil {
|
||||||
return "", 0, regErr
|
return "", regErr
|
||||||
}
|
}
|
||||||
return userID, model.UserTypeTemp, nil
|
return userID, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ func NewAuthLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AuthLogic {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (l *AuthLogic) Auth(req *types.AuthReq) (*types.AuthResp, error) {
|
func (l *AuthLogic) Auth(req *types.AuthReq) (*types.AuthResp, error) {
|
||||||
var userID string
|
var userID string
|
||||||
var userType int64
|
var userType int64
|
||||||
var authType string
|
var authType string
|
||||||
var authKey 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)
|
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 {
|
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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
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)
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建手机号认证失败: %v", err)
|
||||||
}
|
}
|
||||||
// 发放正式用户token
|
// 发放token(userType会根据mobile字段动态计算)
|
||||||
token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, finalUserID, model.UserTypeNormal)
|
token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, finalUserID)
|
||||||
if err != nil {
|
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)
|
||||||
}
|
}
|
||||||
@@ -118,7 +118,7 @@ func (l *BindMobileLogic) BindMobile(req *types.BindMobileReq) (resp *types.Bind
|
|||||||
}
|
}
|
||||||
// 如果当前认证已属于目标手机号用户,直接发放token(无需合并)
|
// 如果当前认证已属于目标手机号用户,直接发放token(无需合并)
|
||||||
if existingAuth != nil && existingAuth.UserId == finalUserID {
|
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 {
|
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)
|
||||||
}
|
}
|
||||||
@@ -232,8 +232,8 @@ func (l *BindMobileLogic) BindMobile(req *types.BindMobileReq) (resp *types.Bind
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 合并完成后生成并返回正式用户token
|
// 合并完成后生成token(userType会根据mobile字段动态计算)
|
||||||
token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, finalUserID, model.UserTypeNormal)
|
token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, finalUserID)
|
||||||
if err != nil {
|
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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,17 +36,8 @@ func (l *DetailLogic) Detail() (resp *types.UserInfoResp, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
userID := claims.UserId
|
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)
|
user, err := l.svcCtx.UserModel.FindOne(l.ctx, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, model.ErrNotFound) {
|
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)
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "用户信息, 数据库查询用户信息失败, %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var userInfo types.User
|
var userInfo types.User
|
||||||
err = copier.Copy(&userInfo, user)
|
err = copier.Copy(&userInfo, user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ package user
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"time"
|
||||||
"ycc-server/common/ctxdata"
|
"ycc-server/common/ctxdata"
|
||||||
"ycc-server/common/xerr"
|
"ycc-server/common/xerr"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ func (l *GetTokenLogic) GetToken() (resp *types.MobileCodeLoginResp, err error)
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "用户信息, %v", err)
|
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 {
|
if err != nil {
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "用户信息, %v", err)
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "用户信息, %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ func (l *MobileCodeLoginLogic) MobileCodeLogin(req *types.MobileCodeLoginReq) (r
|
|||||||
return nil, errors.Wrapf(xerr.NewErrMsg("用户不存在"), "手机登录, 用户不存在: %s", encryptedMobile)
|
return nil, errors.Wrapf(xerr.NewErrMsg("用户不存在"), "手机登录, 用户不存在: %s", encryptedMobile)
|
||||||
}
|
}
|
||||||
userID = user.Id
|
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 {
|
if err != nil {
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "手机登录, 生成token失败 : %s", userID)
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "手机登录, 生成token失败 : %s", userID)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,22 @@
|
|||||||
package user
|
package user
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"ycc-server/app/main/model"
|
"encoding/json"
|
||||||
"ycc-server/common/xerr"
|
"fmt"
|
||||||
"encoding/json"
|
"io"
|
||||||
"fmt"
|
"net/http"
|
||||||
"io"
|
"time"
|
||||||
"net/http"
|
"ycc-server/app/main/model"
|
||||||
"time"
|
"ycc-server/common/xerr"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
"ycc-server/app/main/api/internal/svc"
|
"ycc-server/app/main/api/internal/svc"
|
||||||
"ycc-server/app/main/api/internal/types"
|
"ycc-server/app/main/api/internal/types"
|
||||||
|
|
||||||
"github.com/zeromicro/go-zero/core/logx"
|
"github.com/zeromicro/go-zero/core/logx"
|
||||||
)
|
)
|
||||||
|
|
||||||
type WxH5AuthLogic struct {
|
type WxH5AuthLogic struct {
|
||||||
@@ -47,29 +47,28 @@ func (l *WxH5AuthLogic) WxH5Auth(req *types.WXH5AuthReq) (resp *types.WXH5AuthRe
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Step 3: 处理用户信息
|
// Step 3: 处理用户信息
|
||||||
var userID string
|
var userID string
|
||||||
var userType int64
|
if userAuth != nil {
|
||||||
if userAuth != nil {
|
// 已存在用户,直接登录
|
||||||
// 已存在用户,直接登录
|
userID = userAuth.UserId
|
||||||
userID = userAuth.UserId
|
} else {
|
||||||
userType = model.UserTypeNormal
|
// 新用户创建为临时用户(没有mobile)
|
||||||
} else {
|
user := &model.User{Id: uuid.NewString()}
|
||||||
user := &model.User{Id: uuid.NewString()}
|
_, err := l.svcCtx.UserModel.Insert(l.ctx, nil, user)
|
||||||
_, err := l.svcCtx.UserModel.Insert(l.ctx, nil, user)
|
if err != nil {
|
||||||
if err != nil {
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建用户失败: %v", err)
|
||||||
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}
|
||||||
ua := &model.UserAuth{Id: uuid.NewString(), UserId: user.Id, AuthType: model.UserAuthTypeWxh5OpenID, AuthKey: accessTokenResp.Openid}
|
_, err = l.svcCtx.UserAuthModel.Insert(l.ctx, nil, ua)
|
||||||
_, err = l.svcCtx.UserAuthModel.Insert(l.ctx, nil, ua)
|
if err != nil {
|
||||||
if err != nil {
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建用户授权失败: %v", err)
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建用户授权失败: %v", err)
|
}
|
||||||
}
|
userID = user.Id
|
||||||
userID = user.Id
|
l.Infof("Created new weixin user: userID=%s, openid=%s", userID, accessTokenResp.Openid)
|
||||||
userType = model.UserTypeTemp
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Step 4: 生成JWT Token
|
// Step 4: 生成JWT Token(动态计算userType)
|
||||||
token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID, userType)
|
token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成JWT token失败: %v", err)
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成JWT token失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
package user
|
package user
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"ycc-server/app/main/api/internal/svc"
|
"ycc-server/app/main/api/internal/svc"
|
||||||
"ycc-server/app/main/api/internal/types"
|
"ycc-server/app/main/api/internal/types"
|
||||||
"ycc-server/app/main/model"
|
"ycc-server/app/main/model"
|
||||||
"ycc-server/common/xerr"
|
"ycc-server/common/xerr"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/zeromicro/go-zero/core/logx"
|
"github.com/zeromicro/go-zero/core/logx"
|
||||||
)
|
)
|
||||||
|
|
||||||
type WxMiniAuthLogic struct {
|
type WxMiniAuthLogic struct {
|
||||||
@@ -46,29 +46,27 @@ func (l *WxMiniAuthLogic) WxMiniAuth(req *types.WXMiniAuthReq) (resp *types.WXMi
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 3. 处理用户信息
|
// 3. 处理用户信息
|
||||||
var userID string
|
var userID string
|
||||||
var userType int64
|
if userAuth != nil {
|
||||||
if userAuth != nil {
|
// 已存在用户,直接登录
|
||||||
// 已存在用户,直接登录
|
userID = userAuth.UserId
|
||||||
userID = userAuth.UserId
|
} else {
|
||||||
userType = model.UserTypeNormal
|
// 新用户创建为临时用户(没有mobile)
|
||||||
} else {
|
user := &model.User{Id: uuid.NewString()}
|
||||||
user := &model.User{Id: uuid.NewString()}
|
_, err := l.svcCtx.UserModel.Insert(l.ctx, nil, user)
|
||||||
_, err := l.svcCtx.UserModel.Insert(l.ctx, nil, user)
|
if err != nil {
|
||||||
if err != nil {
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建用户失败: %v", err)
|
||||||
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}
|
||||||
ua := &model.UserAuth{Id: uuid.NewString(), UserId: user.Id, AuthType: model.UserAuthTypeWxMiniOpenID, AuthKey: sessionKeyResp.Openid}
|
_, err = l.svcCtx.UserAuthModel.Insert(l.ctx, nil, ua)
|
||||||
_, err = l.svcCtx.UserAuthModel.Insert(l.ctx, nil, ua)
|
if err != nil {
|
||||||
if err != nil {
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建用户授权失败: %v", err)
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建用户授权失败: %v", err)
|
}
|
||||||
}
|
userID = user.Id
|
||||||
userID = user.Id
|
}
|
||||||
userType = model.UserTypeTemp
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. 生成JWT Token
|
// 4. 生成JWT Token(动态计算userType)
|
||||||
token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID, userType)
|
token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成JWT Token失败: %v", err)
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成JWT Token失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
package middleware
|
package middleware
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net/http"
|
||||||
"ycc-server/app/main/model"
|
"ycc-server/app/main/model"
|
||||||
"ycc-server/common/ctxdata"
|
"ycc-server/common/ctxdata"
|
||||||
"ycc-server/common/xerr"
|
"ycc-server/common/xerr"
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/zeromicro/go-zero/rest/httpx"
|
"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))
|
httpx.Error(w, errors.Wrapf(xerr.NewErrCode(ErrCodeUnauthorized), "token解析失败: %v", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// 检查用户是否绑定了mobile(没有mobile表示是临时用户,不允许访问需要认证的接口)
|
||||||
|
// 注:临时用户现在基于 mobile 字段判断,而不是 UserType
|
||||||
if claims.UserType == model.UserTypeTemp {
|
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
|
return
|
||||||
}
|
}
|
||||||
next(w, r)
|
next(w, r)
|
||||||
|
|||||||
@@ -63,17 +63,38 @@ func (s *UserService) RegisterUUIDUser(ctx context.Context) (string, error) {
|
|||||||
return userId, nil
|
return userId, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GeneralUserToken 生成用户token
|
// GetUserType 根据user.Mobile字段动态计算用户类型
|
||||||
func (s *UserService) GeneralUserToken(ctx context.Context, userID string, userType int64) (string, error) {
|
// 如果有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)
|
platform, err := ctxdata.GetPlatformFromCtx(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
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 isAgent int64
|
||||||
var agentID string
|
var agentID string
|
||||||
var authType string
|
var authType string
|
||||||
var authKey string
|
var authKey string
|
||||||
|
|
||||||
|
// 只有正式用户(有mobile)才可能是代理
|
||||||
if userType == model.UserTypeNormal {
|
if userType == model.UserTypeNormal {
|
||||||
agent, err := s.agentModel.FindOneByUserId(ctx, userID)
|
agent, err := s.agentModel.FindOneByUserId(ctx, userID)
|
||||||
if err != nil && !errors.Is(err, model.ErrNotFound) {
|
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
|
authKey = userAuth.AuthKey
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// 临时用户获取其他平台的auth信息
|
||||||
platAuthType := s.getAuthTypeByPlatform(platform)
|
platAuthType := s.getAuthTypeByPlatform(platform)
|
||||||
ua, err := s.userAuthModel.FindOneByUserIdAuthType(ctx, userID, platAuthType)
|
ua, err := s.userAuthModel.FindOneByUserIdAuthType(ctx, userID, platAuthType)
|
||||||
if err == nil && ua != nil {
|
if err == nil && ua != nil {
|
||||||
@@ -159,12 +181,16 @@ func (s *UserService) RegisterUser(ctx context.Context, mobile string) (string,
|
|||||||
return userId, nil
|
return userId, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 双重判断是否已经注册
|
// 双重判断是否已经注册(根据mobile判断,而不是userType)
|
||||||
if claims.UserType == model.UserTypeNormal {
|
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("用户已注册")
|
return "", errors.New("用户已注册")
|
||||||
}
|
}
|
||||||
var userId string
|
var userId string
|
||||||
// 临时转正式注册
|
// 临时用户绑定mobile转正式注册
|
||||||
err = s.userModel.Trans(ctx, func(ctx context.Context, session sqlx.Session) error {
|
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}}
|
user := &model.User{Id: uuid.NewString(), Mobile: sql.NullString{String: mobile, Valid: true}}
|
||||||
if _, userInsertErr := s.userModel.Insert(ctx, session, user); userInsertErr != nil {
|
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
|
return userId, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TempUserBindUser 临时用户绑定用户
|
// TempUserBindUser 临时用户绑定用户(添加mobile使其变为正式用户)
|
||||||
func (s *UserService) TempUserBindUser(ctx context.Context, session sqlx.Session, normalUserID string) error {
|
func (s *UserService) TempUserBindUser(ctx context.Context, session sqlx.Session, normalUserID string) error {
|
||||||
claims, err := ctxdata.GetClaimsFromCtx(ctx)
|
claims, err := ctxdata.GetClaimsFromCtx(ctx)
|
||||||
if err != nil && !errors.Is(err, ctxdata.ErrNoInCtx) {
|
if err != nil && !errors.Is(err, ctxdata.ErrNoInCtx) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if claims == nil || claims.UserType != model.UserTypeTemp {
|
if claims == nil {
|
||||||
return errors.New("无临时用户")
|
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)
|
existingAuth, err := s.userAuthModel.FindOneByAuthTypeAuthKey(ctx, claims.AuthType, claims.AuthKey)
|
||||||
if err != nil && !errors.Is(err, model.ErrNotFound) {
|
if err != nil && !errors.Is(err, model.ErrNotFound) {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ func main() {
|
|||||||
defer server.Stop()
|
defer server.Stop()
|
||||||
|
|
||||||
handler.RegisterHandlers(server, svcContext)
|
handler.RegisterHandlers(server, svcContext)
|
||||||
|
|
||||||
// 自动注册API到数据库
|
// 自动注册API到数据库
|
||||||
apiRegistry := service.NewApiRegistryService(svcContext.AdminApiModel)
|
apiRegistry := service.NewApiRegistryService(svcContext.AdminApiModel)
|
||||||
routes := server.Routes()
|
routes := server.Routes()
|
||||||
@@ -70,7 +70,7 @@ func main() {
|
|||||||
} else {
|
} else {
|
||||||
logx.Infof("API注册成功,共注册 %d 个路由", len(routes))
|
logx.Infof("API注册成功,共注册 %d 个路由", len(routes))
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("Starting server at %s:%d...\n", c.Host, c.Port)
|
fmt.Printf("Starting server at %s:%d...\n", c.Host, c.Port)
|
||||||
server.Start()
|
server.Start()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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`
|
|
||||||
|
|
||||||
448
代理系统测试用例清单.md
448
代理系统测试用例清单.md
@@ -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 → 普通D,B升级为钻石**
|
|
||||||
- 预期:
|
|
||||||
- B独立成新团队
|
|
||||||
- C和D的 team_leader_id 更新为B
|
|
||||||
- A的团队:只剩自己
|
|
||||||
- B的团队:B、C、D
|
|
||||||
- 验证:团队划分正确,关系链正确
|
|
||||||
|
|
||||||
#### 4.3.2 跨团队转移
|
|
||||||
- [ ] **钻石A → 普通B → 普通C,C升级为钻石**
|
|
||||||
- 预期:
|
|
||||||
- C独立成新团队
|
|
||||||
- C无下级,团队只有C自己
|
|
||||||
- A的团队:A、B
|
|
||||||
- 验证:团队划分正确
|
|
||||||
|
|
||||||
#### 4.3.3 深度层级转移
|
|
||||||
- [ ] **钻石A → 普通B → 普通C → 普通D → 普通E,C升级为钻石**
|
|
||||||
- 预期:
|
|
||||||
- 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. **日志记录**:测试过程中查看日志,确保业务流程正确
|
|
||||||
|
|
||||||
774
代理配置表分析和优化建议.md
774
代理配置表分析和优化建议.md
@@ -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. **后台管理系统前端**
|
|
||||||
- 系统配置页面:移除价格相关配置项
|
|
||||||
- 产品配置页面:确保可以正确配置所有价格参数
|
|
||||||
|
|
||||||
136
佣金冻结功能完成总结.md
136
佣金冻结功能完成总结.md
@@ -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%
|
|
||||||
- 测试冻结金额超过佣金:应冻结全部佣金
|
|
||||||
- 测试解冻任务:在配置的天数后应自动解冻
|
|
||||||
|
|
||||||
106
佣金冻结功能实现说明.md
106
佣金冻结功能实现说明.md
@@ -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. 测试解冻任务:在配置的天数后应自动解冻
|
|
||||||
|
|
||||||
213
删除推广订单功能计划.md
213
删除推广订单功能计划.md
@@ -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/*` ✅
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 完成标志
|
|
||||||
|
|
||||||
- [ ] 所有后端代码修改完成
|
|
||||||
- [ ] 所有前端代码修改完成
|
|
||||||
- [ ] 代码编译通过
|
|
||||||
- [ ] 功能测试通过
|
|
||||||
- [ ] 推广链接管理功能正常
|
|
||||||
- [ ] 推广数据分析功能正常
|
|
||||||
- [ ] 订单管理功能正常(无推广订单相关功能)
|
|
||||||
- [ ] 无编译错误和警告
|
|
||||||
|
|
||||||
111
定时清理报告系统问题分析.md
111
定时清理报告系统问题分析.md
@@ -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. **配置验证** - 提高健壮性
|
|
||||||
|
|
||||||
@@ -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`
|
|
||||||
|
|
||||||
1833
新代理系统完整文档.md
1833
新代理系统完整文档.md
File diff suppressed because it is too large
Load Diff
506
新代理系统检查清单.md
506
新代理系统检查清单.md
@@ -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. 进行端到端测试验证
|
|
||||||
|
|
||||||
135
短链系统实现说明.md
135
短链系统实现说明.md
@@ -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`(如果不传则使用默认值)
|
|
||||||
|
|
||||||
177
解冻任务优化建议.md
177
解冻任务优化建议.md
@@ -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. **配置化并发数** - 如果并发数不需要调整可以不做
|
|
||||||
|
|
||||||
## 实施建议
|
|
||||||
|
|
||||||
建议先实现**超时控制**和**错误分类处理**,这两个对可靠性影响最大。其他优化可以根据实际运行情况逐步添加。
|
|
||||||
|
|
||||||
132
解冻任务实现方案说明.md
132
解冻任务实现方案说明.md
@@ -1,132 +0,0 @@
|
|||||||
# 解冻任务实现方案说明
|
|
||||||
|
|
||||||
## 方案对比
|
|
||||||
|
|
||||||
### 方案1:Asynq 延迟任务(已实现但未使用)
|
|
||||||
**优点:**
|
|
||||||
- ✅ 精确到秒级执行
|
|
||||||
- ✅ 自动重试机制
|
|
||||||
- ✅ 无需额外调度器
|
|
||||||
|
|
||||||
**缺点:**
|
|
||||||
- ❌ 依赖 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个)
|
|
||||||
├─ 任务1(goroutine 1)
|
|
||||||
├─ 任务2(goroutine 2)
|
|
||||||
├─ 任务3(等待,直到前2个完成)
|
|
||||||
├─ 任务4(等待,直到前2个完成)
|
|
||||||
└─ ...(以此类推,两个两个处理)
|
|
||||||
↓
|
|
||||||
每个任务使用事务 + 乐观锁处理
|
|
||||||
↓
|
|
||||||
更新任务状态:status = 2(已解冻)
|
|
||||||
↓
|
|
||||||
更新钱包:FrozenBalance -= 冻结金额, Balance += 冻结金额
|
|
||||||
↓
|
|
||||||
等待所有任务完成
|
|
||||||
↓
|
|
||||||
记录日志
|
|
||||||
```
|
|
||||||
|
|
||||||
## 监控建议
|
|
||||||
|
|
||||||
1. **监控扫描任务执行情况**
|
|
||||||
- 检查定时任务是否正常执行
|
|
||||||
- 监控每次扫描找到的任务数量
|
|
||||||
- 监控成功/失败数量
|
|
||||||
|
|
||||||
2. **监控解冻延迟**
|
|
||||||
- 记录 `actual_unfreeze_time - unfreeze_time` 的差值
|
|
||||||
- 如果延迟超过10分钟,需要检查定时任务是否正常
|
|
||||||
|
|
||||||
3. **监控异常情况**
|
|
||||||
- 冻结余额不足的情况(数据异常)
|
|
||||||
- 任务状态异常的情况
|
|
||||||
|
|
||||||
## 后续优化建议
|
|
||||||
|
|
||||||
1. **可配置扫描频率**:将扫描频率(当前5分钟)配置到配置表
|
|
||||||
2. **批次大小限制**:如果任务量很大,可以限制每次处理的数量
|
|
||||||
3. **告警机制**:如果连续多次扫描都失败,发送告警
|
|
||||||
4. **补偿机制**:提供手动触发扫描的接口,用于紧急情况
|
|
||||||
|
|
||||||
232
退款时代理处理逻辑分析.md
232
退款时代理处理逻辑分析.md
@@ -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`
|
|
||||||
|
|
||||||
**关键点**:此时代理处理任务**还没有发送**(因为查询还没成功)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 问题分析
|
|
||||||
|
|
||||||
### ✅ 情况1:API调用失败,退款(正常情况)
|
|
||||||
|
|
||||||
**流程**:
|
|
||||||
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`)后订单退款的情况
|
|
||||||
- 需要在退款回调和管理员退款中添加撤销代理收益的逻辑
|
|
||||||
|
|
||||||
107
邀请码使用历史功能说明.md
107
邀请码使用历史功能说明.md
@@ -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;
|
|
||||||
```
|
|
||||||
|
|
||||||
@@ -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`
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user