diff --git a/.gitignore b/.gitignore index 5ac55ea..36ebdcb 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,6 @@ **/.DS_Store #deploy data - data/* !data/.gitkeep @@ -19,6 +18,14 @@ data/* /tmp/ +# 打包出来的可执行文件 /app/api +/app/main/api/main +/app/main/api/debug +/app/main/api/test + +# 文档目录 +documents/* +!documents/.gitkeep deploy/script/js diff --git a/API调用失败退款时的代理处理确认.md b/API调用失败退款时的代理处理确认.md deleted file mode 100644 index f9822f8..0000000 --- a/API调用失败退款时的代理处理确认.md +++ /dev/null @@ -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. **之后**订单被退款(比如管理员手动退款) - -这种情况下需要撤销代理收益(需要另外处理,不是当前场景)。 - diff --git a/app/main/api/internal/logic/agent/applyforagentlogic.go b/app/main/api/internal/logic/agent/applyforagentlogic.go index 12f46d7..a466f21 100644 --- a/app/main/api/internal/logic/agent/applyforagentlogic.go +++ b/app/main/api/internal/logic/agent/applyforagentlogic.go @@ -1,22 +1,22 @@ package agent import ( - "context" - "database/sql" - "fmt" - "os" - "time" - "ycc-server/app/main/model" - "ycc-server/common/ctxdata" - "ycc-server/common/globalkey" - "ycc-server/common/xerr" - "ycc-server/pkg/lzkit/crypto" - "strconv" + "context" + "database/sql" + "fmt" + "os" + "strconv" + "time" + "ycc-server/app/main/model" + "ycc-server/common/ctxdata" + "ycc-server/common/globalkey" + "ycc-server/common/xerr" + "ycc-server/pkg/lzkit/crypto" - "github.com/google/uuid" - "github.com/pkg/errors" - "github.com/zeromicro/go-zero/core/stores/redis" - "github.com/zeromicro/go-zero/core/stores/sqlx" + "github.com/google/uuid" + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/stores/redis" + "github.com/zeromicro/go-zero/core/stores/sqlx" "ycc-server/app/main/api/internal/svc" "ycc-server/app/main/api/internal/types" @@ -50,9 +50,9 @@ func (l *ApplyForAgentLogic) ApplyForAgent(req *types.AgentApplyReq) (resp *type return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "加密手机号失败: %v", err) } - if req.Referrer == "" { - return nil, errors.Wrapf(xerr.NewErrMsg("请填写邀请信息"), "") - } + if req.Referrer == "" { + return nil, errors.Wrapf(xerr.NewErrMsg("请填写邀请信息"), "") + } // 2. 校验验证码(开发环境下跳过验证码校验) if os.Getenv("ENV") != "development" { @@ -115,49 +115,49 @@ func (l *ApplyForAgentLogic) ApplyForAgent(req *types.AgentApplyReq) (resp *type return errors.Wrapf(xerr.NewErrMsg("您已经是代理"), "") } - var inviteCodeModel *model.AgentInviteCode - var parentAgentId string - var targetLevel int64 + var inviteCodeModel *model.AgentInviteCode + var parentAgentId string + var targetLevel int64 - inviteCodeModel, err = l.svcCtx.AgentInviteCodeModel.FindOneByCode(transCtx, req.Referrer) - if err != nil && !errors.Is(err, model.ErrNotFound) { - return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询邀请码失败, %v", err) - } - if inviteCodeModel != nil { - if inviteCodeModel.Status != 0 { - if inviteCodeModel.Status == 1 { - return errors.Wrapf(xerr.NewErrMsg("邀请码已使用"), "") - } - return errors.Wrapf(xerr.NewErrMsg("邀请码已失效"), "") - } - if inviteCodeModel.ExpireTime.Valid && inviteCodeModel.ExpireTime.Time.Before(time.Now()) { - return errors.Wrapf(xerr.NewErrMsg("邀请码已过期"), "") - } - targetLevel = inviteCodeModel.TargetLevel - if inviteCodeModel.AgentId.Valid { - parentAgentId = inviteCodeModel.AgentId.String - } - } else { - if codeVal, parseErr := strconv.ParseInt(req.Referrer, 10, 64); parseErr == nil && codeVal > 0 { - parentAgent, err := l.findAgentByCode(transCtx, codeVal) - if err != nil { - return err - } - parentAgentId = parentAgent.Id - targetLevel = 1 - } else { - encRefMobile, _ := crypto.EncryptMobile(req.Referrer, l.svcCtx.Config.Encrypt.SecretKey) - agents, findErr := l.svcCtx.AgentModel.FindAll(transCtx, l.svcCtx.AgentModel.SelectBuilder().Where("mobile = ? AND del_state = ?", encRefMobile, globalkey.DelStateNo).Limit(1), "") - if findErr != nil { - return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询上级代理失败, %v", findErr) - } - if len(agents) == 0 { - return errors.Wrapf(xerr.NewErrMsg("邀请信息无效"), "") - } - parentAgentId = agents[0].Id - targetLevel = 1 - } - } + inviteCodeModel, err = l.svcCtx.AgentInviteCodeModel.FindOneByCode(transCtx, req.Referrer) + if err != nil && !errors.Is(err, model.ErrNotFound) { + return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询邀请码失败, %v", err) + } + if inviteCodeModel != nil { + if inviteCodeModel.Status != 0 { + if inviteCodeModel.Status == 1 { + return errors.Wrapf(xerr.NewErrMsg("邀请码已使用"), "") + } + return errors.Wrapf(xerr.NewErrMsg("邀请码已失效"), "") + } + if inviteCodeModel.ExpireTime.Valid && inviteCodeModel.ExpireTime.Time.Before(time.Now()) { + return errors.Wrapf(xerr.NewErrMsg("邀请码已过期"), "") + } + targetLevel = inviteCodeModel.TargetLevel + if inviteCodeModel.AgentId.Valid { + parentAgentId = inviteCodeModel.AgentId.String + } + } else { + if codeVal, parseErr := strconv.ParseInt(req.Referrer, 10, 64); parseErr == nil && codeVal > 0 { + parentAgent, err := l.findAgentByCode(transCtx, codeVal) + if err != nil { + return err + } + parentAgentId = parentAgent.Id + targetLevel = 1 + } else { + encRefMobile, _ := crypto.EncryptMobile(req.Referrer, l.svcCtx.Config.Encrypt.SecretKey) + agents, findErr := l.svcCtx.AgentModel.FindAll(transCtx, l.svcCtx.AgentModel.SelectBuilder().Where("mobile = ? AND del_state = ?", encRefMobile, globalkey.DelStateNo).Limit(1), "") + if findErr != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询上级代理失败, %v", findErr) + } + if len(agents) == 0 { + return errors.Wrapf(xerr.NewErrMsg("邀请信息无效"), "") + } + parentAgentId = agents[0].Id + targetLevel = 1 + } + } // 4.5 创建代理记录 newAgent := &model.Agent{ @@ -279,7 +279,7 @@ func (l *ApplyForAgentLogic) ApplyForAgent(req *types.AgentApplyReq) (resp *type } // 6. 生成并返回token - token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID, model.UserTypeNormal) + token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID) if err != nil { return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成token失败: %v", err) } diff --git a/app/main/api/internal/logic/agent/registerbyinvitecodelogic.go b/app/main/api/internal/logic/agent/registerbyinvitecodelogic.go index be625cb..a1dc0eb 100644 --- a/app/main/api/internal/logic/agent/registerbyinvitecodelogic.go +++ b/app/main/api/internal/logic/agent/registerbyinvitecodelogic.go @@ -105,10 +105,16 @@ func (l *RegisterByInviteCodeLogic) RegisterByInviteCode(req *types.RegisterByIn return errors.Wrapf(xerr.NewErrMsg("您已经是代理"), "") } - // 如果是临时用户(微信环境下),检查手机号是否已绑定其他微信号,并绑定临时用户到正式用户 - // 注意:非微信环境下 claims 为 nil,此逻辑不会执行,直接使用已存在的 user.Id + // 检查用户是否有mobile绑定(没有mobile则不能成为代理) + // 如果是临时用户(微信环境下),需要先绑定手机号 claims, err := ctxdata.GetClaimsFromCtx(l.ctx) - if err == nil && claims != nil && claims.UserType == model.UserTypeTemp { + if err == nil && claims != nil { + // 获取用户的mobile信息 + if !user.Mobile.Valid || user.Mobile.String == "" { + // 临时用户(无mobile)不能直接成为代理,需要先绑定mobile + return errors.Wrapf(xerr.NewErrMsg("请先绑定手机号后再申请成为代理"), "") + } + // 检查是否已绑定手机号认证(用于确保后续可通过手机号登录) userAuth, err := l.svcCtx.UserAuthModel.FindOneByUserIdAuthType(l.ctx, user.Id, claims.AuthType) if err != nil && !errors.Is(err, model.ErrNotFound) { return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询用户认证失败, %v", err) @@ -116,11 +122,6 @@ func (l *RegisterByInviteCodeLogic) RegisterByInviteCode(req *types.RegisterByIn if userAuth != nil && userAuth.AuthKey != claims.AuthKey { return errors.Wrapf(xerr.NewErrMsg("该手机号已绑定其他微信号"), "") } - // 绑定临时用户到正式用户 - err = l.svcCtx.UserService.TempUserBindUser(l.ctx, session, user.Id) - if err != nil { - return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "绑定用户失败: %v", err) - } } userID = user.Id @@ -263,7 +264,7 @@ func (l *RegisterByInviteCodeLogic) RegisterByInviteCode(req *types.RegisterByIn } // 5. 生成并返回token - token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID, model.UserTypeNormal) + token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID) if err != nil { return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成token失败: %v", err) } diff --git a/app/main/api/internal/logic/query/queryservicelogic.go b/app/main/api/internal/logic/query/queryservicelogic.go index eb986f5..4830454 100644 --- a/app/main/api/internal/logic/query/queryservicelogic.go +++ b/app/main/api/internal/logic/query/queryservicelogic.go @@ -8,7 +8,6 @@ import ( "os" "time" "ycc-server/app/main/api/internal/service" - "ycc-server/app/main/model" "ycc-server/common/ctxdata" "ycc-server/common/xerr" "ycc-server/pkg/lzkit/crypto" @@ -60,60 +59,60 @@ var productProcessors = map[string]func(*QueryServiceLogic, *types.QueryServiceR func (l *QueryServiceLogic) PreprocessLogic(req *types.QueryServiceReq, product string) (*types.QueryServiceResp, error) { if processor, exists := productProcessors[product]; exists { - return processor(l, req) // 调用对应的处理函数 + return processor(l, req) // 璋冪敤瀵瑰簲鐨勫鐞嗗嚱鏁? } - return nil, errors.New("未找到相应的处理程序") + return nil, errors.New("鏈壘鍒扮浉搴旂殑澶勭悊绋嬪簭") } func (l *QueryServiceLogic) ProcessMarriageLogic(req *types.QueryServiceReq) (*types.QueryServiceResp, error) { - // AES解密 + // AES瑙e瘑 decryptData, DecryptDataErr := l.DecryptData(req.Data) if DecryptDataErr != nil { return nil, DecryptDataErr } - // 校验参数 + // 鏍¢獙鍙傛暟 var data types.MarriageReq if unmarshalErr := json.Unmarshal(decryptData, &data); unmarshalErr != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 解密后的数据格式不正确: %+v", unmarshalErr) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "鏌ヨ鏈嶅姟, 瑙e瘑鍚庣殑鏁版嵁鏍煎紡涓嶆纭? %+v", unmarshalErr) } if validatorErr := validator.Validate(data); validatorErr != nil { - return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, validatorErr.Error()), "查询服务, 参数不正确: %+v", validatorErr) + return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, validatorErr.Error()), "鏌ヨ鏈嶅姟, 鍙傛暟涓嶆纭? %+v", validatorErr) } - // 校验验证码 + // 鏍¢獙楠岃瘉鐮? verifyCodeErr := l.VerifyCode(data.Mobile, data.Code) if verifyCodeErr != nil { return nil, verifyCodeErr } - // 校验三要素 + // 鏍¢獙涓夎绱? verifyErr := l.Verify(data.Name, data.IDCard, data.Mobile) if verifyErr != nil { return nil, verifyErr } - // 缓存 + // 缂撳瓨 params := map[string]interface{}{ "name": data.Name, "id_card": data.IDCard, "mobile": data.Mobile, } - userID, userType, err := l.GetOrCreateUser() - if err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 处理用户失败: %v", err) - } + userID, err := l.GetOrCreateUser() + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "鏌ヨ鏈嶅姟, 澶勭悊鐢ㄦ埛澶辫触: %v", err) + } cacheNo, cacheDataErr := l.CacheData(params, "marriage", userID) if cacheDataErr != nil { return nil, cacheDataErr } - token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID, userType) + token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID) if err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 生成token失败 : %v", err) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "鏌ヨ鏈嶅姟, 鐢熸垚token澶辫触 : %v", err) } - // 获取当前时间戳 + // 鑾峰彇褰撳墠鏃堕棿鎴? now := time.Now().Unix() return &types.QueryServiceResp{ Id: cacheNo, @@ -123,58 +122,58 @@ func (l *QueryServiceLogic) ProcessMarriageLogic(req *types.QueryServiceReq) (*t }, nil } -// 处理家政服务相关逻辑 +// 澶勭悊瀹舵斂鏈嶅姟鐩稿叧閫昏緫 func (l *QueryServiceLogic) ProcessHomeServiceLogic(req *types.QueryServiceReq) (*types.QueryServiceResp, error) { - // AES解密 + // AES瑙e瘑 decryptData, DecryptDataErr := l.DecryptData(req.Data) if DecryptDataErr != nil { return nil, DecryptDataErr } - // 校验参数 + // 鏍¢獙鍙傛暟 var data types.HomeServiceReq if unmarshalErr := json.Unmarshal(decryptData, &data); unmarshalErr != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 解密后的数据格式不正确: %+v", unmarshalErr) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "鏌ヨ鏈嶅姟, 瑙e瘑鍚庣殑鏁版嵁鏍煎紡涓嶆纭? %+v", unmarshalErr) } if validatorErr := validator.Validate(data); validatorErr != nil { - return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, validatorErr.Error()), "查询服务, 参数不正确: %+v", validatorErr) + return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, validatorErr.Error()), "鏌ヨ鏈嶅姟, 鍙傛暟涓嶆纭? %+v", validatorErr) } - // 校验验证码 + // 鏍¢獙楠岃瘉鐮? verifyCodeErr := l.VerifyCode(data.Mobile, data.Code) if verifyCodeErr != nil { return nil, verifyCodeErr } - // 校验三要素 + // 鏍¢獙涓夎绱? verifyErr := l.Verify(data.Name, data.IDCard, data.Mobile) if verifyErr != nil { return nil, verifyErr } - // 缓存 + // 缂撳瓨 params := map[string]interface{}{ "name": data.Name, "id_card": data.IDCard, "mobile": data.Mobile, } - userID, userType, err := l.GetOrCreateUser() - if err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 处理用户失败: %v", err) - } + userID, err := l.GetOrCreateUser() + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "鏌ヨ鏈嶅姟, 澶勭悊鐢ㄦ埛澶辫触: %v", err) + } cacheNo, cacheDataErr := l.CacheData(params, "homeservice", userID) if cacheDataErr != nil { return nil, cacheDataErr } - token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID, userType) + token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID) if err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 生成token失败 : %d", userID) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "鏌ヨ鏈嶅姟, 鐢熸垚token澶辫触 : %d", userID) } - // 获取当前时间戳 + // 鑾峰彇褰撳墠鏃堕棿鎴? now := time.Now().Unix() return &types.QueryServiceResp{ Id: cacheNo, @@ -184,58 +183,58 @@ func (l *QueryServiceLogic) ProcessHomeServiceLogic(req *types.QueryServiceReq) }, nil } -// 处理风险评估相关逻辑 +// 澶勭悊椋庨櫓璇勪及鐩稿叧閫昏緫 func (l *QueryServiceLogic) ProcessRiskAssessmentLogic(req *types.QueryServiceReq) (*types.QueryServiceResp, error) { - // AES解密 + // AES瑙e瘑 decryptData, DecryptDataErr := l.DecryptData(req.Data) if DecryptDataErr != nil { return nil, DecryptDataErr } - // 校验参数 + // 鏍¢獙鍙傛暟 var data types.RiskAssessmentReq if unmarshalErr := json.Unmarshal(decryptData, &data); unmarshalErr != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 解密后的数据格式不正确: %+v", unmarshalErr) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "鏌ヨ鏈嶅姟, 瑙e瘑鍚庣殑鏁版嵁鏍煎紡涓嶆纭? %+v", unmarshalErr) } if validatorErr := validator.Validate(data); validatorErr != nil { - return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, validatorErr.Error()), "查询服务, 参数不正确: %+v", validatorErr) + return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, validatorErr.Error()), "鏌ヨ鏈嶅姟, 鍙傛暟涓嶆纭? %+v", validatorErr) } - // 校验验证码 + // 鏍¢獙楠岃瘉鐮? verifyCodeErr := l.VerifyCode(data.Mobile, data.Code) if verifyCodeErr != nil { return nil, verifyCodeErr } - // 校验三要素 + // 鏍¢獙涓夎绱? verifyErr := l.Verify(data.Name, data.IDCard, data.Mobile) if verifyErr != nil { return nil, verifyErr } - // 缓存 + // 缂撳瓨 params := map[string]interface{}{ "name": data.Name, "id_card": data.IDCard, "mobile": data.Mobile, } - userID, userType, err := l.GetOrCreateUser() - if err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 处理用户失败: %v", err) - } + userID, err := l.GetOrCreateUser() + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "鏌ヨ鏈嶅姟, 澶勭悊鐢ㄦ埛澶辫触: %v", err) + } cacheNo, cacheDataErr := l.CacheData(params, "riskassessment", userID) if cacheDataErr != nil { return nil, cacheDataErr } - token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID, userType) + token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID) if err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 生成token失败 : %d", userID) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "鏌ヨ鏈嶅姟, 鐢熸垚token澶辫触 : %d", userID) } - // 获取当前时间戳 + // 鑾峰彇褰撳墠鏃堕棿鎴? now := time.Now().Unix() return &types.QueryServiceResp{ Id: cacheNo, @@ -245,57 +244,57 @@ func (l *QueryServiceLogic) ProcessRiskAssessmentLogic(req *types.QueryServiceRe }, nil } -// 处理公司信息查询相关逻辑 +// 澶勭悊鍏徃淇℃伅鏌ヨ鐩稿叧閫昏緫 func (l *QueryServiceLogic) ProcessCompanyInfoLogic(req *types.QueryServiceReq) (*types.QueryServiceResp, error) { - // AES解密 + // AES瑙e瘑 decryptData, DecryptDataErr := l.DecryptData(req.Data) if DecryptDataErr != nil { return nil, DecryptDataErr } - // 校验参数 + // 鏍¢獙鍙傛暟 var data types.CompanyInfoReq if unmarshalErr := json.Unmarshal(decryptData, &data); unmarshalErr != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 解密后的数据格式不正确: %+v", unmarshalErr) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "鏌ヨ鏈嶅姟, 瑙e瘑鍚庣殑鏁版嵁鏍煎紡涓嶆纭? %+v", unmarshalErr) } if validatorErr := validator.Validate(data); validatorErr != nil { - return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, validatorErr.Error()), "查询服务, 参数不正确: %+v", validatorErr) + return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, validatorErr.Error()), "鏌ヨ鏈嶅姟, 鍙傛暟涓嶆纭? %+v", validatorErr) } - // 校验验证码 + // 鏍¢獙楠岃瘉鐮? verifyCodeErr := l.VerifyCode(data.Mobile, data.Code) if verifyCodeErr != nil { return nil, verifyCodeErr } - // 校验三要素 + // 鏍¢獙涓夎绱? verifyErr := l.Verify(data.Name, data.IDCard, data.Mobile) if verifyErr != nil { return nil, verifyErr } - // 缓存 + // 缂撳瓨 params := map[string]interface{}{ "name": data.Name, "id_card": data.IDCard, "mobile": data.Mobile, } - userID, userType, err := l.GetOrCreateUser() - if err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 处理用户失败: %v", err) - } + userID, err := l.GetOrCreateUser() + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "鏌ヨ鏈嶅姟, 澶勭悊鐢ㄦ埛澶辫触: %v", err) + } cacheNo, cacheDataErr := l.CacheData(params, "companyinfo", userID) if cacheDataErr != nil { return nil, cacheDataErr } - token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID, userType) + token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID) if err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 生成token失败 : %d", userID) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "鏌ヨ鏈嶅姟, 鐢熸垚token澶辫触 : %d", userID) } - // 获取当前时间戳 + // 鑾峰彇褰撳墠鏃堕棿鎴? now := time.Now().Unix() return &types.QueryServiceResp{ Id: cacheNo, @@ -305,58 +304,58 @@ func (l *QueryServiceLogic) ProcessCompanyInfoLogic(req *types.QueryServiceReq) }, nil } -// 处理租赁信息查询相关逻辑 +// 澶勭悊绉熻祦淇℃伅鏌ヨ鐩稿叧閫昏緫 func (l *QueryServiceLogic) ProcessRentalInfoLogic(req *types.QueryServiceReq) (*types.QueryServiceResp, error) { - // AES解密 + // AES瑙e瘑 decryptData, DecryptDataErr := l.DecryptData(req.Data) if DecryptDataErr != nil { return nil, DecryptDataErr } - // 校验参数 + // 鏍¢獙鍙傛暟 var data types.RentalInfoReq if unmarshalErr := json.Unmarshal(decryptData, &data); unmarshalErr != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 解密后的数据格式不正确: %+v", unmarshalErr) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "鏌ヨ鏈嶅姟, 瑙e瘑鍚庣殑鏁版嵁鏍煎紡涓嶆纭? %+v", unmarshalErr) } if validatorErr := validator.Validate(data); validatorErr != nil { - return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, validatorErr.Error()), "查询服务, 参数不正确: %+v", validatorErr) + return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, validatorErr.Error()), "鏌ヨ鏈嶅姟, 鍙傛暟涓嶆纭? %+v", validatorErr) } - // 校验验证码 + // 鏍¢獙楠岃瘉鐮? verifyCodeErr := l.VerifyCode(data.Mobile, data.Code) if verifyCodeErr != nil { return nil, verifyCodeErr } - // 校验三要素 + // 鏍¢獙涓夎绱? verifyErr := l.Verify(data.Name, data.IDCard, data.Mobile) if verifyErr != nil { return nil, verifyErr } - // 缓存 + // 缂撳瓨 params := map[string]interface{}{ "name": data.Name, "id_card": data.IDCard, "mobile": data.Mobile, } - userID, userType, err := l.GetOrCreateUser() - if err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 处理用户失败: %v", err) - } + userID, err := l.GetOrCreateUser() + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "鏌ヨ鏈嶅姟, 澶勭悊鐢ㄦ埛澶辫触: %v", err) + } cacheNo, cacheDataErr := l.CacheData(params, "rentalinfo", userID) if cacheDataErr != nil { return nil, cacheDataErr } - token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID, userType) + token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID) if err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 生成token失败 : %d", userID) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "鏌ヨ鏈嶅姟, 鐢熸垚token澶辫触 : %d", userID) } - // 获取当前时间戳 + // 鑾峰彇褰撳墠鏃堕棿鎴? now := time.Now().Unix() return &types.QueryServiceResp{ Id: cacheNo, @@ -366,58 +365,58 @@ func (l *QueryServiceLogic) ProcessRentalInfoLogic(req *types.QueryServiceReq) ( }, nil } -// 处理贷前背景检查相关逻辑 +// 澶勭悊璐峰墠鑳屾櫙妫€鏌ョ浉鍏抽€昏緫 func (l *QueryServiceLogic) ProcessPreLoanBackgroundCheckLogic(req *types.QueryServiceReq) (*types.QueryServiceResp, error) { - // AES解密 + // AES瑙e瘑 decryptData, DecryptDataErr := l.DecryptData(req.Data) if DecryptDataErr != nil { return nil, DecryptDataErr } - // 校验参数 + // 鏍¢獙鍙傛暟 var data types.PreLoanBackgroundCheckReq if unmarshalErr := json.Unmarshal(decryptData, &data); unmarshalErr != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 解密后的数据格式不正确: %+v", unmarshalErr) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "鏌ヨ鏈嶅姟, 瑙e瘑鍚庣殑鏁版嵁鏍煎紡涓嶆纭? %+v", unmarshalErr) } if validatorErr := validator.Validate(data); validatorErr != nil { - return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, validatorErr.Error()), "查询服务, 参数不正确: %+v", validatorErr) + return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, validatorErr.Error()), "鏌ヨ鏈嶅姟, 鍙傛暟涓嶆纭? %+v", validatorErr) } - // 校验验证码 + // 鏍¢獙楠岃瘉鐮? verifyCodeErr := l.VerifyCode(data.Mobile, data.Code) if verifyCodeErr != nil { return nil, verifyCodeErr } - // 校验三要素 + // 鏍¢獙涓夎绱? verifyErr := l.Verify(data.Name, data.IDCard, data.Mobile) if verifyErr != nil { return nil, verifyErr } - // 缓存 + // 缂撳瓨 params := map[string]interface{}{ "name": data.Name, "id_card": data.IDCard, "mobile": data.Mobile, } - userID, userType, err := l.GetOrCreateUser() - if err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 处理用户失败: %v", err) - } + userID, err := l.GetOrCreateUser() + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "鏌ヨ鏈嶅姟, 澶勭悊鐢ㄦ埛澶辫触: %v", err) + } cacheNo, cacheDataErr := l.CacheData(params, "preloanbackgroundcheck", userID) if cacheDataErr != nil { return nil, cacheDataErr } - token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID, userType) + token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID) if err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 生成token失败 : %d", userID) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "鏌ヨ鏈嶅姟, 鐢熸垚token澶辫触 : %d", userID) } - // 获取当前时间戳 + // 鑾峰彇褰撳墠鏃堕棿鎴? now := time.Now().Unix() return &types.QueryServiceResp{ Id: cacheNo, @@ -427,57 +426,57 @@ func (l *QueryServiceLogic) ProcessPreLoanBackgroundCheckLogic(req *types.QueryS }, nil } -// 处理人事背调相关逻辑 +// 澶勭悊浜轰簨鑳岃皟鐩稿叧閫昏緫 func (l *QueryServiceLogic) ProcessBackgroundCheckLogic(req *types.QueryServiceReq) (*types.QueryServiceResp, error) { - // AES解密 + // AES瑙e瘑 decryptData, DecryptDataErr := l.DecryptData(req.Data) if DecryptDataErr != nil { return nil, DecryptDataErr } - // 校验参数 + // 鏍¢獙鍙傛暟 var data types.BackgroundCheckReq if unmarshalErr := json.Unmarshal(decryptData, &data); unmarshalErr != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 解密后的数据格式不正确: %+v", unmarshalErr) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "鏌ヨ鏈嶅姟, 瑙e瘑鍚庣殑鏁版嵁鏍煎紡涓嶆纭? %+v", unmarshalErr) } if validatorErr := validator.Validate(data); validatorErr != nil { - return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, validatorErr.Error()), "查询服务, 参数不正确: %+v", validatorErr) + return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, validatorErr.Error()), "鏌ヨ鏈嶅姟, 鍙傛暟涓嶆纭? %+v", validatorErr) } - // 校验验证码 + // 鏍¢獙楠岃瘉鐮? verifyCodeErr := l.VerifyCode(data.Mobile, data.Code) if verifyCodeErr != nil { return nil, verifyCodeErr } - // 校验三要素 + // 鏍¢獙涓夎绱? verifyErr := l.Verify(data.Name, data.IDCard, data.Mobile) if verifyErr != nil { return nil, verifyErr } - // 缓存 + // 缂撳瓨 params := map[string]interface{}{ "name": data.Name, "id_card": data.IDCard, "mobile": data.Mobile, } - userID, userType, err := l.GetOrCreateUser() - if err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 处理用户失败: %v", err) - } + userID, err := l.GetOrCreateUser() + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "鏌ヨ鏈嶅姟, 澶勭悊鐢ㄦ埛澶辫触: %v", err) + } cacheNo, cacheDataErr := l.CacheData(params, "backgroundcheck", userID) if cacheDataErr != nil { return nil, cacheDataErr } - token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID, userType) + token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID) if err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 生成token失败 : %d", userID) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "鏌ヨ鏈嶅姟, 鐢熸垚token澶辫触 : %d", userID) } - // 获取当前时间戳 + // 鑾峰彇褰撳墠鏃堕棿鎴? now := time.Now().Unix() return &types.QueryServiceResp{ Id: cacheNo, @@ -487,55 +486,55 @@ func (l *QueryServiceLogic) ProcessBackgroundCheckLogic(req *types.QueryServiceR }, nil } func (l *QueryServiceLogic) ProcessPersonalDataLogic(req *types.QueryServiceReq) (*types.QueryServiceResp, error) { - // AES解密 + // AES瑙e瘑 decryptData, DecryptDataErr := l.DecryptData(req.Data) if DecryptDataErr != nil { return nil, DecryptDataErr } - // 校验参数 + // 鏍¢獙鍙傛暟 var data types.PersonalDataReq if unmarshalErr := json.Unmarshal(decryptData, &data); unmarshalErr != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 解密后的数据格式不正确: %+v", unmarshalErr) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "鏌ヨ鏈嶅姟, 瑙e瘑鍚庣殑鏁版嵁鏍煎紡涓嶆纭? %+v", unmarshalErr) } if validatorErr := validator.Validate(data); validatorErr != nil { - return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, validatorErr.Error()), "查询服务, 参数不正确: %+v", validatorErr) + return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, validatorErr.Error()), "鏌ヨ鏈嶅姟, 鍙傛暟涓嶆纭? %+v", validatorErr) } - // 校验验证码 + // 鏍¢獙楠岃瘉鐮? verifyCodeErr := l.VerifyCode(data.Mobile, data.Code) if verifyCodeErr != nil { return nil, verifyCodeErr } - // 校验三要素 + // 鏍¢獙涓夎绱? verifyErr := l.Verify(data.Name, data.IDCard, data.Mobile) if verifyErr != nil { return nil, verifyErr } - // 缓存 + // 缂撳瓨 params := map[string]interface{}{ "name": data.Name, "id_card": data.IDCard, "mobile": data.Mobile, } - userID, userType, err := l.GetOrCreateUser() - if err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 处理用户失败: %v", err) - } + userID, err := l.GetOrCreateUser() + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "鏌ヨ鏈嶅姟, 澶勭悊鐢ㄦ埛澶辫触: %v", err) + } cacheNo, cacheDataErr := l.CacheData(params, "personalData", userID) if cacheDataErr != nil { return nil, cacheDataErr } - token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID, userType) + token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID) if err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 生成token失败 : %d", userID) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "鏌ヨ鏈嶅姟, 鐢熸垚token澶辫触 : %d", userID) } - // 获取当前时间戳 + // 鑾峰彇褰撳墠鏃堕棿鎴? now := time.Now().Unix() return &types.QueryServiceResp{ Id: cacheNo, @@ -545,55 +544,55 @@ func (l *QueryServiceLogic) ProcessPersonalDataLogic(req *types.QueryServiceReq) }, nil } func (l *QueryServiceLogic) ProcessConsumerFinanceReportLogic(req *types.QueryServiceReq) (*types.QueryServiceResp, error) { - // AES解密 + // AES瑙e瘑 decryptData, DecryptDataErr := l.DecryptData(req.Data) if DecryptDataErr != nil { return nil, DecryptDataErr } - // 校验参数 + // 鏍¢獙鍙傛暟 var data types.ConsumerFinanceReportReq if unmarshalErr := json.Unmarshal(decryptData, &data); unmarshalErr != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 解密后的数据格式不正确: %+v", unmarshalErr) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "鏌ヨ鏈嶅姟, 瑙e瘑鍚庣殑鏁版嵁鏍煎紡涓嶆纭? %+v", unmarshalErr) } if validatorErr := validator.Validate(data); validatorErr != nil { - return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, validatorErr.Error()), "查询服务, 参数不正确: %+v", validatorErr) + return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, validatorErr.Error()), "鏌ヨ鏈嶅姟, 鍙傛暟涓嶆纭? %+v", validatorErr) } - // 校验验证码 + // 鏍¢獙楠岃瘉鐮? verifyCodeErr := l.VerifyCode(data.Mobile, data.Code) if verifyCodeErr != nil { return nil, verifyCodeErr } - // 校验三要素 + // 鏍¢獙涓夎绱? verifyErr := l.Verify(data.Name, data.IDCard, data.Mobile) if verifyErr != nil { return nil, verifyErr } - // 缓存 + // 缂撳瓨 params := map[string]interface{}{ "name": data.Name, "id_card": data.IDCard, "mobile": data.Mobile, } - userID, userType, err := l.GetOrCreateUser() - if err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 处理用户失败: %v", err) - } + userID, err := l.GetOrCreateUser() + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "鏌ヨ鏈嶅姟, 澶勭悊鐢ㄦ埛澶辫触: %v", err) + } cacheNo, cacheDataErr := l.CacheData(params, "consumerFinanceReport", userID) if cacheDataErr != nil { return nil, cacheDataErr } - token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID, userType) + token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID) if err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 生成token失败 : %d", userID) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "鏌ヨ鏈嶅姟, 鐢熸垚token澶辫触 : %d", userID) } - // 获取当前时间戳 + // 鑾峰彇褰撳墠鏃堕棿鎴? now := time.Now().Unix() return &types.QueryServiceResp{ Id: cacheNo, @@ -606,43 +605,43 @@ func (l *QueryServiceLogic) DecryptData(data string) ([]byte, error) { secretKey := l.svcCtx.Config.Encrypt.SecretKey key, decodeErr := hex.DecodeString(secretKey) if decodeErr != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "密钥获取失败: %+v", decodeErr) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "瀵嗛挜鑾峰彇澶辫触: %+v", decodeErr) } decryptData, aesDecryptErr := crypto.AesDecrypt(data, key) if aesDecryptErr != nil || len(decryptData) == 0 { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "解密失败: %+v", aesDecryptErr) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "瑙e瘑澶辫触: %+v", aesDecryptErr) } return decryptData, nil } -// 校验验证码 +// 鏍¢獙楠岃瘉鐮? func (l *QueryServiceLogic) VerifyCode(mobile string, code string) error { - // 开发环境下跳过验证码校验 + // 寮€鍙戠幆澧冧笅璺宠繃楠岃瘉鐮佹牎楠? if os.Getenv("ENV") == "development" { return nil } secretKey := l.svcCtx.Config.Encrypt.SecretKey encryptedMobile, err := crypto.EncryptMobile(mobile, secretKey) if err != nil { - return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "加密手机号失败: %+v", err) + return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "鍔犲瘑鎵嬫満鍙峰け璐? %+v", err) } codeRedisKey := fmt.Sprintf("%s:%s", "query", encryptedMobile) cacheCode, err := l.svcCtx.Redis.Get(codeRedisKey) if err != nil { if errors.Is(err, redis.Nil) { - return errors.Wrapf(xerr.NewErrMsg("验证码已过期"), "验证码过期: %s", mobile) + return errors.Wrapf(xerr.NewErrMsg("楠岃瘉鐮佸凡杩囨湡"), "楠岃瘉鐮佽繃鏈? %s", mobile) } - return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "读取验证码redis缓存失败, mobile: %s, err: %+v", mobile, err) + return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "璇诲彇楠岃瘉鐮乺edis缂撳瓨澶辫触, mobile: %s, err: %+v", mobile, err) } if cacheCode != code { - return errors.Wrapf(xerr.NewErrMsg("验证码不正确"), "验证码不正确: %s", mobile) + return errors.Wrapf(xerr.NewErrMsg("楠岃瘉鐮佷笉姝g‘"), "楠岃瘉鐮佷笉姝g‘: %s", mobile) } return nil } -// 二、三要素验证 +// 浜屻€佷笁瑕佺礌楠岃瘉 func (l *QueryServiceLogic) Verify(Name string, IDCard string, Mobile string) error { - // 开发环境下跳过二/三要素验证 + // 寮€鍙戠幆澧冧笅璺宠繃浜?涓夎绱犻獙璇? if os.Getenv("ENV") == "development" { return nil } @@ -653,13 +652,13 @@ func (l *QueryServiceLogic) Verify(Name string, IDCard string, Mobile string) er } verification, err := l.svcCtx.VerificationService.TwoFactorVerification(twoVerification) if err != nil { - return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "二要素验证失败: %v", err) + return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "浜岃绱犻獙璇佸け璐? %v", err) } if !verification.Passed { - return errors.Wrapf(xerr.NewErrCodeMsg(xerr.SERVER_COMMON_ERROR, verification.Err.Error()), "二要素验证不通过: %v", err) + return errors.Wrapf(xerr.NewErrCodeMsg(xerr.SERVER_COMMON_ERROR, verification.Err.Error()), "浜岃绱犻獙璇佷笉閫氳繃: %v", err) } } else { - // 三要素验证 + // 涓夎绱犻獙璇? threeVerification := service.ThreeFactorVerificationRequest{ Name: Name, IDCard: IDCard, @@ -667,30 +666,30 @@ func (l *QueryServiceLogic) Verify(Name string, IDCard string, Mobile string) er } verification, err := l.svcCtx.VerificationService.ThreeFactorVerification(threeVerification) if err != nil { - return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "三要素验证失败: %v", err) + return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "涓夎绱犻獙璇佸け璐? %v", err) } if !verification.Passed { - return errors.Wrapf(xerr.NewErrCodeMsg(xerr.SERVER_COMMON_ERROR, verification.Err.Error()), "三要素验证不通过: %v", err) + return errors.Wrapf(xerr.NewErrCodeMsg(xerr.SERVER_COMMON_ERROR, verification.Err.Error()), "涓夎绱犻獙璇佷笉閫氳繃: %v", err) } } return nil } -// 缓存 +// 缂撳瓨 func (l *QueryServiceLogic) CacheData(params map[string]interface{}, Product string, userID string) (string, error) { agentIdentifier, _ := l.ctx.Value("agentIdentifier").(string) secretKey := l.svcCtx.Config.Encrypt.SecretKey key, decodeErr := hex.DecodeString(secretKey) if decodeErr != nil { - return "", errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 获取AES密钥失败: %+v", decodeErr) + return "", errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "鏌ヨ鏈嶅姟, 鑾峰彇AES瀵嗛挜澶辫触: %+v", decodeErr) } paramsMarshal, marshalErr := json.Marshal(params) if marshalErr != nil { - return "", errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 序列化参数失败: %+v", marshalErr) + return "", errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "鏌ヨ鏈嶅姟, 搴忓垪鍖栧弬鏁板け璐? %+v", marshalErr) } encryptParams, aesEncryptErr := crypto.AesEncrypt(paramsMarshal, key) if aesEncryptErr != nil { - return "", errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 加密参数失败: %+v", aesEncryptErr) + return "", errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "鏌ヨ鏈嶅姟, 鍔犲瘑鍙傛暟澶辫触: %+v", aesEncryptErr) } queryCache := types.QueryCacheLoad{ Params: encryptParams, @@ -699,7 +698,7 @@ func (l *QueryServiceLogic) CacheData(params map[string]interface{}, Product str } jsonData, marshalErr := json.Marshal(queryCache) if marshalErr != nil { - return "", errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 序列化参数失败: %+v", marshalErr) + return "", errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "鏌ヨ鏈嶅姟, 搴忓垪鍖栧弬鏁板け璐? %+v", marshalErr) } outTradeNo := "Q_" + l.svcCtx.AlipayService.GenerateOutTradeNo() redisKey := fmt.Sprintf(types.QueryCacheKey, userID, outTradeNo) @@ -710,18 +709,18 @@ func (l *QueryServiceLogic) CacheData(params map[string]interface{}, Product str return outTradeNo, nil } -// GetOrCreateUser 获取或创建用户 -// 1. 如果上下文中已有用户ID,直接返回 -// 2. 如果是代理查询或APP请求,创建新用户 -// 3. 其他情况返回未登录错误 -func (l *QueryServiceLogic) GetOrCreateUser() (string, int64, error) { - claims, err := ctxdata.GetClaimsFromCtx(l.ctx) - if err == nil && claims != nil { - return claims.UserId, claims.UserType, nil - } - userID, regErr := l.svcCtx.UserService.RegisterUUIDUser(l.ctx) - if regErr != nil { - return "", 0, regErr - } - return userID, model.UserTypeTemp, nil +// GetOrCreateUser 鑾峰彇鎴栧垱寤虹敤鎴? +// 1. 濡傛灉涓婁笅鏂囦腑宸叉湁鐢ㄦ埛ID锛岀洿鎺ヨ繑鍥? +// 2. 濡傛灉鏄唬鐞嗘煡璇㈡垨APP璇锋眰锛屽垱寤烘柊鐢ㄦ埛 +// 3. 鍏朵粬鎯呭喌杩斿洖鏈櫥褰曢敊璇? +func (l *QueryServiceLogic) GetOrCreateUser() (string, error) { + claims, err := ctxdata.GetClaimsFromCtx(l.ctx) + if err == nil && claims != nil { + return claims.UserId, nil + } + userID, regErr := l.svcCtx.UserService.RegisterUUIDUser(l.ctx) + if regErr != nil { + return "", regErr + } + return userID, nil } diff --git a/app/main/api/internal/logic/user/authlogic.go b/app/main/api/internal/logic/user/authlogic.go index d67113e..02753ff 100644 --- a/app/main/api/internal/logic/user/authlogic.go +++ b/app/main/api/internal/logic/user/authlogic.go @@ -23,7 +23,7 @@ func NewAuthLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AuthLogic { } func (l *AuthLogic) Auth(req *types.AuthReq) (*types.AuthResp, error) { - var userID string + var userID string var userType int64 var authType string var authKey string @@ -88,7 +88,7 @@ func (l *AuthLogic) Auth(req *types.AuthReq) (*types.AuthResp, error) { return nil, errors.Wrapf(xerr.NewErrMsg("不支持的平台类型"), "platform=%s", req.Platform) } - token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID, userType) + token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID) if err != nil { return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成Token失败: %v", err) } diff --git a/app/main/api/internal/logic/user/bindmobilelogic.go b/app/main/api/internal/logic/user/bindmobilelogic.go index 9d34f41..5fdb0b7 100644 --- a/app/main/api/internal/logic/user/bindmobilelogic.go +++ b/app/main/api/internal/logic/user/bindmobilelogic.go @@ -90,8 +90,8 @@ func (l *BindMobileLogic) BindMobile(req *types.BindMobileReq) (resp *types.Bind if _, err := l.svcCtx.UserAuthModel.Insert(l.ctx, nil, &model.UserAuth{Id: uuid.NewString(), UserId: finalUserID, AuthType: model.UserAuthTypeMobile, AuthKey: encryptedMobile}); err != nil { return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建手机号认证失败: %v", err) } - // 发放正式用户token - token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, finalUserID, model.UserTypeNormal) + // 发放token(userType会根据mobile字段动态计算) + token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, finalUserID) if err != nil { return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成Token失败: %v", err) } @@ -118,7 +118,7 @@ func (l *BindMobileLogic) BindMobile(req *types.BindMobileReq) (resp *types.Bind } // 如果当前认证已属于目标手机号用户,直接发放token(无需合并) if existingAuth != nil && existingAuth.UserId == finalUserID { - token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, finalUserID, model.UserTypeNormal) + token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, finalUserID) if err != nil { return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成Token失败: %v", err) } @@ -232,8 +232,8 @@ func (l *BindMobileLogic) BindMobile(req *types.BindMobileReq) (resp *types.Bind return nil, err } - // 合并完成后生成并返回正式用户token - token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, finalUserID, model.UserTypeNormal) + // 合并完成后生成token(userType会根据mobile字段动态计算) + token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, finalUserID) if err != nil { return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成Token失败: %v", err) } diff --git a/app/main/api/internal/logic/user/detaillogic.go b/app/main/api/internal/logic/user/detaillogic.go index c3d71b3..bbcab09 100644 --- a/app/main/api/internal/logic/user/detaillogic.go +++ b/app/main/api/internal/logic/user/detaillogic.go @@ -36,17 +36,8 @@ func (l *DetailLogic) Detail() (resp *types.UserInfoResp, err error) { } userID := claims.UserId - userType := claims.UserType - if userType != model.UserTypeNormal { - return &types.UserInfoResp{ - UserInfo: types.User{ - Id: userID, - UserType: userType, - Mobile: "", - NickName: "", - }, - }, nil - } + + // 无论是临时用户还是正常用户,都需要从数据库中查询用户信息 user, err := l.svcCtx.UserModel.FindOne(l.ctx, userID) if err != nil { if errors.Is(err, model.ErrNotFound) { @@ -54,6 +45,7 @@ func (l *DetailLogic) Detail() (resp *types.UserInfoResp, err error) { } return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "用户信息, 数据库查询用户信息失败, %v", err) } + var userInfo types.User err = copier.Copy(&userInfo, user) if err != nil { diff --git a/app/main/api/internal/logic/user/gettokenlogic.go b/app/main/api/internal/logic/user/gettokenlogic.go index 95146a8..be9bdb9 100644 --- a/app/main/api/internal/logic/user/gettokenlogic.go +++ b/app/main/api/internal/logic/user/gettokenlogic.go @@ -2,9 +2,9 @@ package user import ( "context" + "time" "ycc-server/common/ctxdata" "ycc-server/common/xerr" - "time" "github.com/pkg/errors" @@ -33,7 +33,7 @@ func (l *GetTokenLogic) GetToken() (resp *types.MobileCodeLoginResp, err error) if err != nil { return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "用户信息, %v", err) } - token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, claims.UserId, claims.UserType) + token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, claims.UserId) if err != nil { return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "用户信息, %v", err) } diff --git a/app/main/api/internal/logic/user/mobilecodeloginlogic.go b/app/main/api/internal/logic/user/mobilecodeloginlogic.go index 4701dff..7b78b0b 100644 --- a/app/main/api/internal/logic/user/mobilecodeloginlogic.go +++ b/app/main/api/internal/logic/user/mobilecodeloginlogic.go @@ -62,7 +62,7 @@ func (l *MobileCodeLoginLogic) MobileCodeLogin(req *types.MobileCodeLoginReq) (r return nil, errors.Wrapf(xerr.NewErrMsg("用户不存在"), "手机登录, 用户不存在: %s", encryptedMobile) } userID = user.Id - token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID, model.UserTypeNormal) + token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID) if err != nil { return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "手机登录, 生成token失败 : %s", userID) } diff --git a/app/main/api/internal/logic/user/wxh5authlogic.go b/app/main/api/internal/logic/user/wxh5authlogic.go index 417bc1e..66da8f0 100644 --- a/app/main/api/internal/logic/user/wxh5authlogic.go +++ b/app/main/api/internal/logic/user/wxh5authlogic.go @@ -1,22 +1,22 @@ package user import ( - "context" - "ycc-server/app/main/model" - "ycc-server/common/xerr" - "encoding/json" - "fmt" - "io" - "net/http" - "time" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "time" + "ycc-server/app/main/model" + "ycc-server/common/xerr" - "github.com/google/uuid" - "github.com/pkg/errors" + "github.com/google/uuid" + "github.com/pkg/errors" - "ycc-server/app/main/api/internal/svc" - "ycc-server/app/main/api/internal/types" + "ycc-server/app/main/api/internal/svc" + "ycc-server/app/main/api/internal/types" - "github.com/zeromicro/go-zero/core/logx" + "github.com/zeromicro/go-zero/core/logx" ) type WxH5AuthLogic struct { @@ -47,29 +47,28 @@ func (l *WxH5AuthLogic) WxH5Auth(req *types.WXH5AuthReq) (resp *types.WXH5AuthRe } // Step 3: 处理用户信息 - var userID string - var userType int64 - if userAuth != nil { - // 已存在用户,直接登录 - userID = userAuth.UserId - userType = model.UserTypeNormal - } else { - user := &model.User{Id: uuid.NewString()} - _, err := l.svcCtx.UserModel.Insert(l.ctx, nil, user) - if err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建用户失败: %v", err) - } - ua := &model.UserAuth{Id: uuid.NewString(), UserId: user.Id, AuthType: model.UserAuthTypeWxh5OpenID, AuthKey: accessTokenResp.Openid} - _, err = l.svcCtx.UserAuthModel.Insert(l.ctx, nil, ua) - if err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建用户授权失败: %v", err) - } - userID = user.Id - userType = model.UserTypeTemp - } + var userID string + if userAuth != nil { + // 已存在用户,直接登录 + userID = userAuth.UserId + } else { + // 新用户创建为临时用户(没有mobile) + user := &model.User{Id: uuid.NewString()} + _, err := l.svcCtx.UserModel.Insert(l.ctx, nil, user) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建用户失败: %v", err) + } + ua := &model.UserAuth{Id: uuid.NewString(), UserId: user.Id, AuthType: model.UserAuthTypeWxh5OpenID, AuthKey: accessTokenResp.Openid} + _, err = l.svcCtx.UserAuthModel.Insert(l.ctx, nil, ua) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建用户授权失败: %v", err) + } + userID = user.Id + l.Infof("Created new weixin user: userID=%s, openid=%s", userID, accessTokenResp.Openid) + } - // Step 4: 生成JWT Token - token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID, userType) + // Step 4: 生成JWT Token(动态计算userType) + token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID) if err != nil { return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成JWT token失败: %v", err) } diff --git a/app/main/api/internal/logic/user/wxminiauthlogic.go b/app/main/api/internal/logic/user/wxminiauthlogic.go index d94842a..89f68d5 100644 --- a/app/main/api/internal/logic/user/wxminiauthlogic.go +++ b/app/main/api/internal/logic/user/wxminiauthlogic.go @@ -1,21 +1,21 @@ package user import ( - "context" - "encoding/json" - "fmt" - "io" - "net/http" - "time" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "time" - "ycc-server/app/main/api/internal/svc" - "ycc-server/app/main/api/internal/types" - "ycc-server/app/main/model" - "ycc-server/common/xerr" + "ycc-server/app/main/api/internal/svc" + "ycc-server/app/main/api/internal/types" + "ycc-server/app/main/model" + "ycc-server/common/xerr" - "github.com/google/uuid" - "github.com/pkg/errors" - "github.com/zeromicro/go-zero/core/logx" + "github.com/google/uuid" + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/logx" ) type WxMiniAuthLogic struct { @@ -46,29 +46,27 @@ func (l *WxMiniAuthLogic) WxMiniAuth(req *types.WXMiniAuthReq) (resp *types.WXMi } // 3. 处理用户信息 - var userID string - var userType int64 - if userAuth != nil { - // 已存在用户,直接登录 - userID = userAuth.UserId - userType = model.UserTypeNormal - } else { - user := &model.User{Id: uuid.NewString()} - _, err := l.svcCtx.UserModel.Insert(l.ctx, nil, user) - if err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建用户失败: %v", err) - } - ua := &model.UserAuth{Id: uuid.NewString(), UserId: user.Id, AuthType: model.UserAuthTypeWxMiniOpenID, AuthKey: sessionKeyResp.Openid} - _, err = l.svcCtx.UserAuthModel.Insert(l.ctx, nil, ua) - if err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建用户授权失败: %v", err) - } - userID = user.Id - userType = model.UserTypeTemp - } + var userID string + if userAuth != nil { + // 已存在用户,直接登录 + userID = userAuth.UserId + } else { + // 新用户创建为临时用户(没有mobile) + user := &model.User{Id: uuid.NewString()} + _, err := l.svcCtx.UserModel.Insert(l.ctx, nil, user) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建用户失败: %v", err) + } + ua := &model.UserAuth{Id: uuid.NewString(), UserId: user.Id, AuthType: model.UserAuthTypeWxMiniOpenID, AuthKey: sessionKeyResp.Openid} + _, err = l.svcCtx.UserAuthModel.Insert(l.ctx, nil, ua) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建用户授权失败: %v", err) + } + userID = user.Id + } - // 4. 生成JWT Token - token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID, userType) + // 4. 生成JWT Token(动态计算userType) + token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID) if err != nil { return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成JWT Token失败: %v", err) } diff --git a/app/main/api/internal/middleware/userauthinterceptormiddleware.go b/app/main/api/internal/middleware/userauthinterceptormiddleware.go index 0f05d1c..730f1ab 100644 --- a/app/main/api/internal/middleware/userauthinterceptormiddleware.go +++ b/app/main/api/internal/middleware/userauthinterceptormiddleware.go @@ -1,10 +1,10 @@ package middleware import ( + "net/http" "ycc-server/app/main/model" "ycc-server/common/ctxdata" "ycc-server/common/xerr" - "net/http" "github.com/pkg/errors" "github.com/zeromicro/go-zero/rest/httpx" @@ -24,8 +24,10 @@ func (m *UserAuthInterceptorMiddleware) Handle(next http.HandlerFunc) http.Handl httpx.Error(w, errors.Wrapf(xerr.NewErrCode(ErrCodeUnauthorized), "token解析失败: %v", err)) return } + // 检查用户是否绑定了mobile(没有mobile表示是临时用户,不允许访问需要认证的接口) + // 注:临时用户现在基于 mobile 字段判断,而不是 UserType if claims.UserType == model.UserTypeTemp { - httpx.Error(w, errors.Wrapf(xerr.NewErrCode(xerr.USER_NEED_BIND_MOBILE), "token解析失败: %v", err)) + httpx.Error(w, errors.Wrapf(xerr.NewErrCode(xerr.USER_NEED_BIND_MOBILE), "请先绑定手机号: %v", err)) return } next(w, r) diff --git a/app/main/api/internal/service/userService.go b/app/main/api/internal/service/userService.go index fb37bbd..8003657 100644 --- a/app/main/api/internal/service/userService.go +++ b/app/main/api/internal/service/userService.go @@ -63,17 +63,38 @@ func (s *UserService) RegisterUUIDUser(ctx context.Context) (string, error) { return userId, nil } -// GeneralUserToken 生成用户token -func (s *UserService) GeneralUserToken(ctx context.Context, userID string, userType int64) (string, error) { +// GetUserType 根据user.Mobile字段动态计算用户类型 +// 如果有mobile,则为正式用户(UserTypeNormal),否则为临时用户(UserTypeTemp) +func (s *UserService) GetUserType(ctx context.Context, userID string) (int64, error) { + user, err := s.userModel.FindOne(ctx, userID) + if err != nil { + return 0, err + } + if user.Mobile.Valid && user.Mobile.String != "" { + return model.UserTypeNormal, nil + } + return model.UserTypeTemp, nil +} + +// GeneralUserToken 生成用户token(动态计算userType) +func (s *UserService) GeneralUserToken(ctx context.Context, userID string) (string, error) { platform, err := ctxdata.GetPlatformFromCtx(ctx) if err != nil { return "", err } + // 动态计算userType + userType, err := s.GetUserType(ctx, userID) + if err != nil { + return "", errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取用户信息失败: %v", err) + } + var isAgent int64 var agentID string var authType string var authKey string + + // 只有正式用户(有mobile)才可能是代理 if userType == model.UserTypeNormal { agent, err := s.agentModel.FindOneByUserId(ctx, userID) if err != nil && !errors.Is(err, model.ErrNotFound) { @@ -89,6 +110,7 @@ func (s *UserService) GeneralUserToken(ctx context.Context, userID string, userT authKey = userAuth.AuthKey } } else { + // 临时用户获取其他平台的auth信息 platAuthType := s.getAuthTypeByPlatform(platform) ua, err := s.userAuthModel.FindOneByUserIdAuthType(ctx, userID, platAuthType) if err == nil && ua != nil { @@ -159,12 +181,16 @@ func (s *UserService) RegisterUser(ctx context.Context, mobile string) (string, return userId, nil } - // 双重判断是否已经注册 - if claims.UserType == model.UserTypeNormal { + // 双重判断是否已经注册(根据mobile判断,而不是userType) + currentUser, err := s.userModel.FindOne(ctx, claims.UserId) + if err != nil && !errors.Is(err, model.ErrNotFound) { + return "", err + } + if currentUser != nil && currentUser.Mobile.Valid && currentUser.Mobile.String != "" { return "", errors.New("用户已注册") } var userId string - // 临时转正式注册 + // 临时用户绑定mobile转正式注册 err = s.userModel.Trans(ctx, func(ctx context.Context, session sqlx.Session) error { user := &model.User{Id: uuid.NewString(), Mobile: sql.NullString{String: mobile, Valid: true}} if _, userInsertErr := s.userModel.Insert(ctx, session, user); userInsertErr != nil { @@ -187,17 +213,26 @@ func (s *UserService) RegisterUser(ctx context.Context, mobile string) (string, return userId, nil } -// TempUserBindUser 临时用户绑定用户 +// TempUserBindUser 临时用户绑定用户(添加mobile使其变为正式用户) func (s *UserService) TempUserBindUser(ctx context.Context, session sqlx.Session, normalUserID string) error { claims, err := ctxdata.GetClaimsFromCtx(ctx) if err != nil && !errors.Is(err, ctxdata.ErrNoInCtx) { return err } - if claims == nil || claims.UserType != model.UserTypeTemp { + if claims == nil { return errors.New("无临时用户") } + // 检查当前用户是否已经绑定了mobile(根据mobile判断,而不是userType) + tempUser, err := s.userModel.FindOne(ctx, claims.UserId) + if err != nil && !errors.Is(err, model.ErrNotFound) { + return err + } + if tempUser != nil && tempUser.Mobile.Valid && tempUser.Mobile.String != "" { + return errors.New("临时用户已注册") + } + existingAuth, err := s.userAuthModel.FindOneByAuthTypeAuthKey(ctx, claims.AuthType, claims.AuthKey) if err != nil && !errors.Is(err, model.ErrNotFound) { return err diff --git a/app/main/api/main.go b/app/main/api/main.go index 3d993ec..4c17ca1 100644 --- a/app/main/api/main.go +++ b/app/main/api/main.go @@ -61,7 +61,7 @@ func main() { defer server.Stop() handler.RegisterHandlers(server, svcContext) - + // 自动注册API到数据库 apiRegistry := service.NewApiRegistryService(svcContext.AdminApiModel) routes := server.Routes() @@ -70,7 +70,7 @@ func main() { } else { logx.Infof("API注册成功,共注册 %d 个路由", len(routes)) } - + fmt.Printf("Starting server at %s:%d...\n", c.Host, c.Port) server.Start() } diff --git a/distributeNormalAgentBonus配置项说明.md b/distributeNormalAgentBonus配置项说明.md deleted file mode 100644 index c7beaa0..0000000 --- a/distributeNormalAgentBonus配置项说明.md +++ /dev/null @@ -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` - diff --git a/代理系统测试用例清单.md b/代理系统测试用例清单.md deleted file mode 100644 index fb0890c..0000000 --- a/代理系统测试用例清单.md +++ /dev/null @@ -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. **日志记录**:测试过程中查看日志,确保业务流程正确 - diff --git a/代理配置表分析和优化建议.md b/代理配置表分析和优化建议.md deleted file mode 100644 index 168ba80..0000000 --- a/代理配置表分析和优化建议.md +++ /dev/null @@ -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. **后台管理系统前端** - - 系统配置页面:移除价格相关配置项 - - 产品配置页面:确保可以正确配置所有价格参数 - diff --git a/佣金冻结功能完成总结.md b/佣金冻结功能完成总结.md deleted file mode 100644 index d184eaa..0000000 --- a/佣金冻结功能完成总结.md +++ /dev/null @@ -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% - - 测试冻结金额超过佣金:应冻结全部佣金 - - 测试解冻任务:在配置的天数后应自动解冻 - diff --git a/佣金冻结功能实现说明.md b/佣金冻结功能实现说明.md deleted file mode 100644 index d9ec35c..0000000 --- a/佣金冻结功能实现说明.md +++ /dev/null @@ -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. 测试解冻任务:在配置的天数后应自动解冻 - diff --git a/删除推广订单功能计划.md b/删除推广订单功能计划.md deleted file mode 100644 index b9e05a2..0000000 --- a/删除推广订单功能计划.md +++ /dev/null @@ -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/*` ✅ - ---- - -## 完成标志 - -- [ ] 所有后端代码修改完成 -- [ ] 所有前端代码修改完成 -- [ ] 代码编译通过 -- [ ] 功能测试通过 -- [ ] 推广链接管理功能正常 -- [ ] 推广数据分析功能正常 -- [ ] 订单管理功能正常(无推广订单相关功能) -- [ ] 无编译错误和警告 - diff --git a/定时清理报告系统问题分析.md b/定时清理报告系统问题分析.md deleted file mode 100644 index 0f2961d..0000000 --- a/定时清理报告系统问题分析.md +++ /dev/null @@ -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. **配置验证** - 提高健壮性 - diff --git a/报告查询链路和代理分配逻辑检查报告.md b/报告查询链路和代理分配逻辑检查报告.md deleted file mode 100644 index 051f13a..0000000 --- a/报告查询链路和代理分配逻辑检查报告.md +++ /dev/null @@ -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` - diff --git a/新代理系统完整文档.md b/新代理系统完整文档.md deleted file mode 100644 index 457468c..0000000 --- a/新代理系统完整文档.md +++ /dev/null @@ -1,1833 +0,0 @@ -# 新代理系统完整文档 - -## 文档说明 - -本文档详细记录了新代理系统的完整链路、业务逻辑、数据表结构、API接口和配置说明,用于后续系统调整和维护。 - -**文档版本**: v1.0 -**最后更新**: 2024年 -**维护人员**: 开发团队 - ---- - -## 目录 - -1. [系统概述](#一系统概述) -2. [数据表结构](#二数据表结构) -3. [核心业务规则](#三核心业务规则) -4. [完整链路流程](#四完整链路流程) -5. [关键代码逻辑](#五关键代码逻辑) -6. [API接口列表](#六api接口列表) -7. [系统配置说明](#七系统配置说明) -8. [数据流转图](#八数据流转图) - ---- - -## 一、系统概述 - -### 1.1 系统定位 - -新代理系统是一个三级代理分销系统,**用户只能通过邀请码成为代理**,支持邀请码管理、推广、收益分配、升级和提现等完整业务流程。 - -### 1.2 核心特性 - -- **三级代理体系**:普通(Level 1) → 黄金(Level 2) → 钻石(Level 3) -- **团队结构管理**:钻石代理作为团队首领,管理整个团队 -- **邀请码机制**:**用户不能自主成为代理,只能通过邀请码成为代理** - - 平台发放钻石邀请码:管理员生成,用户使用后成为钻石代理(**只能使用一次,使用后立即失效**) - - 代理发放邀请码:代理生成,用户使用后成为该代理的下级(**普通邀请码可以无限使用**) - - 邀请链接/二维码:代理生成,用户通过链接注册成为该代理的下级 -- **灵活的价格体系**:代理可自定义推广价格,系统自动计算收益 -- **智能收益分配**:自动计算代理收益和上级返佣 -- **升级机制**:支持自主付费升级和钻石代理升级下级 -- **税费管理**:月度累计提现,自动计算税费 -- **实名认证**:提现前必须完成实名认证(三要素核验,无需审核) - -### 1.3 技术架构 - -- **框架**: Go-Zero -- **数据库**: MySQL -- **缓存**: Redis -- **支付**: 支付宝、微信支付 -- **加密**: AES加密(手机号、身份证号) - ---- - -## 二、数据表结构 - -### 2.1 核心数据表 - -#### 2.1.1 agent(代理基本信息表) - -| 字段 | 类型 | 说明 | -| -------------- | ------------ | -------------------------------- | -| id | bigint | 主键ID | -| user_id | bigint | 用户ID(唯一) | -| level | tinyint | 代理等级:1=普通,2=黄金,3=钻石 | -| region | varchar(50) | 区域(可选) | -| mobile | varchar(50) | 手机号(加密) | -| wechat_id | varchar(100) | 微信号(可选) | -| team_leader_id | bigint | 团队首领ID(钻石代理的ID) | -| create_time | datetime | 创建时间 | -| update_time | datetime | 更新时间 | -| delete_time | datetime | 删除时间 | -| del_state | tinyint | 删除状态:0=未删除,1=已删除 | -| version | bigint | 版本号(乐观锁) | - -**索引**: -- PRIMARY KEY (`id`) -- UNIQUE KEY `uk_user_id` (`user_id`) -- KEY `idx_mobile` (`mobile`) -- KEY `idx_level` (`level`) -- KEY `idx_team_leader_id` (`team_leader_id`) - -#### 2.1.2 agent_wallet(代理钱包表) - -| 字段 | 类型 | 说明 | -| ---------------- | ------------- | -------------- | -| id | bigint | 主键ID | -| agent_id | bigint | 代理ID(唯一) | -| balance | decimal(10,2) | 可用余额 | -| frozen_balance | decimal(10,2) | 冻结余额 | -| total_earnings | decimal(10,2) | 累计收益 | -| withdrawn_amount | decimal(10,2) | 已提现金额 | -| create_time | datetime | 创建时间 | -| update_time | datetime | 更新时间 | -| delete_time | datetime | 删除时间 | -| del_state | tinyint | 删除状态 | -| version | bigint | 版本号 | - -#### 2.1.4 agent_relation(代理关系表) - -| 字段 | 类型 | 说明 | -| ------------- | ------------ | ------------------------------ | -| id | bigint | 主键ID | -| parent_id | bigint | 上级代理ID | -| child_id | bigint | 下级代理ID | -| relation_type | tinyint | 关系类型:1=直接关系,2=已脱离 | -| detach_reason | varchar(200) | 脱离原因 | -| detach_time | datetime | 脱离时间 | -| create_time | datetime | 创建时间 | -| update_time | datetime | 更新时间 | -| delete_time | datetime | 删除时间 | -| del_state | tinyint | 删除状态 | -| version | bigint | 版本号 | - -**关系类型说明**: -- `relation_type = 1`: 直接关系(正常上下级) -- `relation_type = 2`: 已脱离(升级后脱离直接关系) - -**唯一约束**: -- UNIQUE KEY `uk_parent_child_type` (`parent_id`, `child_id`, `relation_type`) - -#### 2.1.5 agent_link(推广链接表) - -| 字段 | 类型 | 说明 | -| ----------------- | ------------- | ----------------------------- | -| id | bigint | 主键ID | -| agent_id | bigint | 代理ID | -| user_id | bigint | 用户ID | -| product_id | bigint | 产品ID | -| link_identifier | varchar(200) | 链接标识(加密) | -| set_price | decimal(10,2) | 设定价格 | -| actual_base_price | decimal(10,2) | 实际底价(基础底价+等级加成) | -| create_time | datetime | 创建时间 | -| update_time | datetime | 更新时间 | -| delete_time | datetime | 删除时间 | -| del_state | tinyint | 删除状态 | -| version | bigint | 版本号 | - -**唯一约束**: -- UNIQUE KEY `uk_agent_product_price` (`agent_id`, `product_id`, `set_price`, `del_state`) - -#### 2.1.6 agent_order(代理订单表) - -| 字段 | 类型 | 说明 | -| ----------------- | ------------- | ------------------------------------------ | -| id | bigint | 主键ID | -| agent_id | bigint | 代理ID | -| order_id | bigint | 订单ID | -| product_id | bigint | 产品ID | -| order_amount | decimal(10,2) | 订单金额(SetPrice) | -| set_price | decimal(10,2) | 设定价格 | -| actual_base_price | decimal(10,2) | 实际底价 | -| price_cost | decimal(10,2) | 提价成本 | -| agent_profit | decimal(10,2) | 代理收益 | -| process_status | tinyint | 处理状态:0=待处理,1=处理成功,2=处理失败 | -| process_time | datetime | 处理时间 | -| process_remark | varchar(500) | 处理备注 | -| create_time | datetime | 创建时间 | -| update_time | datetime | 更新时间 | -| delete_time | datetime | 删除时间 | -| del_state | tinyint | 删除状态 | -| version | bigint | 版本号 | - -**处理状态说明**: -- `process_status = 0`: 待处理(订单创建后) -- `process_status = 1`: 处理成功(收益已分配) -- `process_status = 2`: 处理失败 - -#### 2.1.7 agent_commission(代理佣金表) - -| 字段 | 类型 | 说明 | -| ----------- | ------------- | -------------------- | -| id | bigint | 主键ID | -| agent_id | bigint | 代理ID | -| order_id | bigint | 订单ID | -| product_id | bigint | 产品ID | -| amount | decimal(10,2) | 佣金金额(代理收益) | -| status | tinyint | 状态:1=已发放 | -| create_time | datetime | 创建时间 | -| update_time | datetime | 更新时间 | -| delete_time | datetime | 删除时间 | -| del_state | tinyint | 删除状态 | -| version | bigint | 版本号 | - -#### 2.1.8 agent_rebate(代理返佣表) - -| 字段 | 类型 | 说明 | -| --------------- | ------------- | -------------------------------------------------------- | -| id | bigint | 主键ID | -| agent_id | bigint | 获得返佣的代理ID | -| source_agent_id | bigint | 来源代理ID(产生订单的代理) | -| order_id | bigint | 订单ID | -| product_id | bigint | 产品ID | -| rebate_type | tinyint | 返佣类型:1=直接上级返佣,2=钻石上级返佣,3=黄金上级返佣 | -| level_bonus | decimal(10,2) | 等级加成金额 | -| rebate_amount | decimal(10,2) | 返佣金额 | -| status | tinyint | 状态:1=已发放 | -| create_time | datetime | 创建时间 | -| update_time | datetime | 更新时间 | -| delete_time | datetime | 删除时间 | -| del_state | tinyint | 删除状态 | -| version | bigint | 版本号 | - -**返佣类型说明**: -- `rebate_type = 1`: 直接上级返佣(普通代理的等级加成给直接上级) -- `rebate_type = 2`: 钻石上级返佣(给钻石上级的返佣) -- `rebate_type = 3`: 黄金上级返佣(给黄金上级的返佣) - -#### 2.1.9 agent_upgrade(代理升级表) - -| 字段 | 类型 | 说明 | -| ----------------- | ------------- | ------------------------------------ | -| id | bigint | 主键ID | -| agent_id | bigint | 代理ID | -| from_level | tinyint | 原等级 | -| to_level | tinyint | 目标等级 | -| upgrade_type | tinyint | 升级类型:1=自主付费,2=钻石升级下级 | -| upgrade_fee | decimal(10,2) | 升级费用 | -| rebate_amount | decimal(10,2) | 返佣金额(给原直接上级) | -| rebate_agent_id | bigint | 返佣代理ID(原直接上级) | -| operator_agent_id | bigint | 操作代理ID(钻石升级下级时使用) | -| order_no | varchar(100) | 支付订单号 | -| status | tinyint | 状态:1=待处理,2=已完成,3=已失败 | -| remark | varchar(500) | 备注 | -| create_time | datetime | 创建时间 | -| update_time | datetime | 更新时间 | -| delete_time | datetime | 删除时间 | -| del_state | tinyint | 删除状态 | -| version | bigint | 版本号 | - -**升级类型说明**: -- `upgrade_type = 1`: 自主付费升级(代理自己付费) -- `upgrade_type = 2`: 钻石升级下级(钻石代理操作,免费) - -#### 2.1.10 agent_withdrawal(代理提现表) - -| 字段 | 类型 | 说明 | -| ------------- | ------------- | ------------------------------------------------------------------------ | -| id | bigint | 主键ID | -| agent_id | bigint | 代理ID | -| withdraw_no | varchar(100) | 提现单号(唯一) | -| payee_account | varchar(100) | 收款账户(支付宝账号) | -| payee_name | varchar(100) | 收款人姓名 | -| amount | decimal(10,2) | 提现金额 | -| tax_amount | decimal(10,2) | 税费金额 | -| actual_amount | decimal(10,2) | 实际到账金额 | -| status | tinyint | 状态:1=待审核,2=审核通过,3=审核拒绝,4=提现中,5=提现成功,6=提现失败 | -| remark | varchar(500) | 备注 | -| create_time | datetime | 创建时间 | -| update_time | datetime | 更新时间 | -| delete_time | datetime | 删除时间 | -| del_state | tinyint | 删除状态 | -| version | bigint | 版本号 | - -**状态说明**: -- `status = 1`: 待审核(代理申请后) -- `status = 2`: 审核通过(管理员审核通过,准备转账) -- `status = 3`: 审核拒绝(管理员拒绝) -- `status = 4`: 提现中(已调用支付宝转账接口,处理中) -- `status = 5`: 提现成功(转账成功) -- `status = 6`: 提现失败(转账失败) - -#### 2.1.11 agent_withdrawal_tax(代理提现扣税表) - -| 字段 | 类型 | 说明 | -| ----------------- | ------------- | ------------------------------ | -| id | bigint | 主键ID | -| agent_id | bigint | 代理ID | -| withdrawal_id | bigint | 提现记录ID | -| year_month | bigint | 年月(格式:YYYYMM,如202401) | -| withdrawal_amount | decimal(10,2) | 提现金额 | -| taxable_amount | decimal(10,2) | 应税金额 | -| tax_rate | decimal(5,4) | 税率 | -| tax_amount | decimal(10,2) | 税费金额 | -| actual_amount | decimal(10,2) | 实际到账金额 | -| tax_status | tinyint | 扣税状态:1=待扣税,2=已扣税 | -| tax_time | datetime | 扣税时间 | -| remark | varchar(500) | 备注 | -| create_time | datetime | 创建时间 | -| update_time | datetime | 更新时间 | -| delete_time | datetime | 删除时间 | -| del_state | tinyint | 删除状态 | -| version | bigint | 版本号 | - -**扣税状态说明**: -- `tax_status = 1`: 待扣税(提现申请后) -- `tax_status = 2`: 已扣税(提现成功后) - -#### 2.1.12 agent_config(代理系统配置表) - -| 字段 | 类型 | 说明 | -| ------------ | ------------ | ----------------------------------------------------------------------------- | -| id | bigint | 主键ID | -| config_key | varchar(100) | 配置键(唯一) | -| config_value | varchar(500) | 配置值 | -| config_type | varchar(50) | 配置类型:price=价格,bonus=等级加成,upgrade=升级费用,rebate=返佣,tax=税费 | -| description | varchar(500) | 配置描述 | -| create_time | datetime | 创建时间 | -| update_time | datetime | 更新时间 | -| delete_time | datetime | 删除时间 | -| del_state | tinyint | 删除状态 | -| version | bigint | 版本号 | - -**配置键列表**: -- `base_price`: 基础底价 -- `system_max_price`: 系统价格上限 -- `price_threshold`: 提价标准阈值 -- `price_fee_rate`: 提价手续费比例 -- `level_1_bonus`: 普通代理等级加成(6元) -- `level_2_bonus`: 黄金代理等级加成(3元) -- `level_3_bonus`: 钻石代理等级加成(0元) -- `upgrade_to_gold_fee`: 升级为黄金费用(199元) -- `upgrade_to_diamond_fee`: 升级为钻石费用(980元) -- `upgrade_to_gold_rebate`: 升级为黄金返佣(139元) -- `upgrade_to_diamond_rebate`: 升级为钻石返佣(680元) -- `tax_rate`: 税率(默认0.06,即6%) -- `tax_exemption_amount`: 免税额度(默认0) - -#### 2.1.13 agent_product_config(代理产品配置表) - -| 字段 | 类型 | 说明 | -| ---------------- | ------------- | ------------------------------------ | -| id | bigint | 主键ID | -| product_id | bigint | 产品ID(唯一) | -| product_name | varchar(100) | 产品名称 | -| base_price | decimal(10,2) | 基础底价(可覆盖系统配置) | -| system_max_price | decimal(10,2) | 系统价格上限(可覆盖系统配置) | -| price_threshold | decimal(10,2) | 提价标准阈值(可选,覆盖系统配置) | -| price_fee_rate | decimal(5,4) | 提价手续费比例(可选,覆盖系统配置) | -| create_time | datetime | 创建时间 | -| update_time | datetime | 更新时间 | -| delete_time | datetime | 删除时间 | -| del_state | tinyint | 删除状态 | -| version | bigint | 版本号 | - -#### 2.1.14 agent_real_name(代理实名认证表) - -| 字段 | 类型 | 说明 | -| ----------- | ----------- | ---------------------------------------------- | -| id | bigint | 主键ID | -| agent_id | bigint | 代理ID(唯一) | -| name | varchar(50) | 真实姓名 | -| id_card | varchar(50) | 身份证号(加密) | -| mobile | varchar(50) | 手机号(加密) | -| verify_time | datetime | 验证时间(三要素验证通过时间,NULL表示未验证) | -| create_time | datetime | 创建时间 | -| update_time | datetime | 更新时间 | -| delete_time | datetime | 删除时间 | -| del_state | tinyint | 删除状态 | -| version | bigint | 版本号 | - -**验证说明**: -- `verify_time IS NULL`: 未验证(未通过三要素核验或未提交) -- `verify_time IS NOT NULL`: 已通过(三要素核验通过,可以提现) - -**重要**:实名认证改为三要素核验,无需人工审核。提交实名认证信息后,系统自动进行三要素核验(姓名、身份证号、手机号),核验通过后自动设置 `verify_time`,无需管理员审核。 - -### 2.2 数据表关系图 - -``` -agent (代理表) -├── agent_wallet (钱包表) 1:1 -├── agent_link (推广链接表) 1:N -├── agent_order (代理订单表) 1:N -├── agent_relation (关系表) 1:N (作为parent_id) -│ └── agent_relation (关系表) 1:N (作为child_id) -├── agent_commission (佣金表) 1:N -├── agent_rebate (返佣表) 1:N (作为agent_id) -│ └── agent_rebate (返佣表) 1:N (作为source_agent_id) -├── agent_upgrade (升级表) 1:N -├── agent_withdrawal (提现表) 1:N -│ └── agent_withdrawal_tax (扣税表) 1:N -└── agent_real_name (实名认证表) 1:1 -``` - ---- - -## 三、核心业务规则 - -### 3.1 代理等级体系 - -#### 3.1.1 等级定义 - -| 等级 | 数值 | 名称 | 说明 | -| ------- | ---- | -------- | ---------------------------------- | -| Level 1 | 1 | 普通代理 | 初始等级,所有新代理默认等级 | -| Level 2 | 2 | 黄金代理 | 中级等级,可通过付费或钻石升级获得 | -| Level 3 | 3 | 钻石代理 | 最高等级,团队首领,可通过付费获得 | - -#### 3.1.2 等级加成规则 - -| 等级 | 等级加成 | 说明 | -| -------- | -------- | ------------------------- | -| 普通代理 | +6元 | 实际底价 = 基础底价 + 6元 | -| 黄金代理 | +3元 | 实际底价 = 基础底价 + 3元 | -| 钻石代理 | +0元 | 实际底价 = 基础底价 | - -**计算公式**: -``` -实际底价 = 基础底价 + 等级加成 -``` - -### 3.2 团队结构规则 - -#### 3.2.1 团队定义 - -- **团队**: 由一个钻石代理作为首领,及其所有下级代理组成的层级关系链 -- **团队首领**: 必须是钻石代理,每个团队有且仅有一个首领 -- **团队关系**: 通过 `team_leader_id` 字段关联,所有团队成员指向同一个钻石代理 - -#### 3.2.2 上下级关系约束 - -**核心原则**: -1. **下级不能比上级等级高**: 下级等级必须 ≤ 上级等级 -2. **同级不能作为上下级**(除了普通代理): 黄金和钻石不能作为同级上下级 - -**允许的关系**: -- 普通 → 普通 ✓(同级普通允许) -- 黄金 → 普通 ✓(上级等级高于下级) -- 钻石 → 普通 ✓(上级等级高于下级) -- 钻石 → 黄金 ✓(上级等级高于下级,允许建立关系) - -**禁止的关系**: -- 普通 → 黄金 ✗(下级等级高于上级) -- 普通 → 钻石 ✗(下级等级高于上级) -- 黄金 → 黄金 ✗(同级不能作为上下级) -- 钻石 → 钻石 ✗(同级不能作为上下级) -- 黄金 → 钻石 ✗(下级等级高于上级) - -**升级后脱离关系的规则**: -- 下级等级 > 上级等级 → 脱离关系 -- 同级(黄金或钻石)→ 脱离关系 -- 普通代理同级 → 不脱离关系(允许保持) - -#### 3.2.3 升级规则 - -**核心规则**: 代理升级后,其所有下级(直接+间接)会跟随该代理。 - -**升级场景**: - -1. **普通 → 黄金**: - - 升级后根据上级等级决定是否脱离关系: - * 如果上级是普通代理:必须脱离(下级等级高于上级) - * 如果上级是黄金代理:必须脱离(同级不能作为上下级) - * 如果上级是钻石代理:不脱离(钻石等级高于黄金,允许保持关系) - - 保留团队关系(通过团队首领钻石代理) - - 仍属于原团队 - - 所有下级(直接+间接)继续跟随该代理 - -2. **黄金 → 钻石**: - - 独立成为新团队 - - 成为新团队的首领(`team_leader_id = 自己`) - - 所有下级(直接+间接)跟随该代理到新团队 - -3. **普通 → 钻石**: - - 独立成为新团队 - - 成为新团队的首领 - - 所有下级(直接+间接)跟随该代理到新团队 - -#### 3.2.4 升级方法和费用规则 - -**升级方式**: - -1. **钻石代理升级下级**: - - 钻石代理可以将下级的普通代理升级为黄金代理 - - 升级方式: 钻石代理操作,无需被升级代理付费 - - 限制: 只能升级普通代理为黄金代理 - -2. **代理自主付费升级**: - - 普通代理可以付费升级为黄金代理(199元) - - 普通代理可以付费升级为钻石代理(980元) - - 黄金代理可以付费升级为钻石代理(980元) - -**升级费用和返佣规则**: - -| 升级类型 | 升级费用 | 直接上级返佣 | 说明 | -| ------------------------- | -------- | ------------ | -------------------------------- | -| 普通→黄金 | 199元 | 139元 | 付费后立即返佣给直接上级 | -| 普通→钻石 | 980元 | 680元 | 付费后立即返佣给直接上级 | -| 黄金→钻石 | 980元 | 680元 | 付费后立即返佣给直接上级 | -| 钻石升级下级(普通→黄金) | 免费 | 无 | 钻石代理操作,被升级代理无需付费 | - -**重要规则**: -- ✅ **返佣给原直接上级**: 即使升级后脱离直接上下级关系,返佣仍然给原直接上级 -- ✅ **返佣时机**: 付费成功后立即返佣,然后执行升级操作 -- ✅ **升级流程**: 付费 → 返佣给直接上级 → 升级 → 根据情况脱离关系 - -### 3.3 价格体系规则 - -#### 3.3.1 价格计算公式 - -``` -实际底价 = 基础底价 + 等级加成 -代理收益 = 设定价格 - 实际底价 - 提价成本 -提价成本 = (设定价格 - 提价阈值) × 提价手续费比例(当设定价格 > 提价阈值时) -``` - -#### 3.3.2 价格约束 - -- **最低价格**: 实际底价(基础底价 + 等级加成) -- **最高价格**: 系统价格上限(或产品配置的价格上限) -- **价格范围**: `实际底价 ≤ 设定价格 ≤ 系统价格上限` - -#### 3.3.3 提价成本计算 - -- **当设定价格 ≤ 提价阈值**: 提价成本 = 0 -- **当设定价格 > 提价阈值**: 提价成本 = (设定价格 - 提价阈值) × 提价手续费比例 - -### 3.4 收益分配规则 - -#### 3.4.1 代理收益 - -代理收益 = 设定价格 - 实际底价 - 提价成本 - -**分配流程**: -1. 订单支付成功后,触发 `AgentService.AgentProcess` -2. 计算代理收益并发放到代理钱包 -3. 创建 `agent_commission` 记录 -4. 更新 `agent_wallet` 余额和累计收益 - -#### 3.4.2 等级加成返佣分配 - -**核心原则**: -- **基础底价(BasePrice)**:无论哪个等级,全部归平台(固定收入) -- **等级加成部分**:作为返佣分配给上级链 - -**分配规则总结表**: - -| 代理等级 | 等级加成 | 直接上级 | 等级加成返佣分配规则 | -| -------- | -------- | -------- | ------------------------------------------------------------------------------------------------------------------ | -| 钻石 | 0元 | 无 | 无返佣分配(加成为0) | -| 黄金 | 3元 | 钻石 | 3元全部给钻石上级 | -| 普通 | 6元 | 钻石 | 6元全部给钻石上级 | -| 普通 | 6元 | 黄金 | 3元给黄金上级,3元给钻石上级(上上级) | -| 普通 | 6元 | 普通 | 2元给直接上级普通,剩余4元:
- 有钻石:4元全部给钻石
- 只有黄金:3元给黄金,1元归平台
- 都没有:4元归平台 | - -**详细规则说明**: - -1. **普通代理(等级加成,例如6元)**: - - **直接上级是钻石**:等级加成全部给钻石上级 - - **直接上级是黄金**:一部分给黄金上级(配置:`normal_to_gold_rebate`,默认3元),剩余给钻石上级(上上级) - - **直接上级是普通**: - - 一部分给直接上级普通(配置:`normal_to_normal_rebate`,默认2元) - - 剩余金额: - - 有钻石上级:剩余全部给钻石上级 - - 只有黄金上级:最多给黄金上级(配置:`normal_to_gold_rebate_max`,默认3元),超出部分归平台 - - 都没有:全部归平台 - - **注意**:如果在团队上下级链路中这中间有很多层普通代理,给直接上级的金额只给推广人的直接上级,对于中间的会直接跳过,然后到黄金/钻石代理 - -2. **黄金代理(等级加成,例如3元)**: - - 全部给钻石上级(如有) - - 如果找不到钻石上级,归平台 - -3. **钻石代理(等级加成0元)**: - - 无返佣分配 - -**配置项说明**: -- 返佣金额的具体取值从配置表读取,支持动态配置 -- 配置键: - - `normal_to_normal_rebate`:普通代理给直接上级普通的金额(默认2元) - - `normal_to_gold_rebate`:普通代理给直接上级黄金的金额(默认3元) - - `normal_to_gold_rebate_max`:普通代理给黄金上级的最大金额(默认3元) -- 如果配置不存在,使用默认值 - -#### 3.4.3 返佣记录 - -所有返佣都会记录到 `agent_rebate` 表,包含: -- 获得返佣的代理ID -- 来源代理ID(产生订单的代理) -- 返佣类型(1=直接上级,2=钻石上级,3=黄金上级) -- 返佣金额 -- 等级加成金额 - -### 3.5 提现规则 - -#### 3.5.1 提现条件 - -1. **实名认证**: 必须完成实名认证且三要素核验已通过(`verify_time` 不为空) -2. **余额充足**: 钱包余额 >= 提现金额 -3. **账户信息**: 必须提供支付宝账号和收款人姓名 - -#### 3.5.2 税费计算规则 - -**计算公式**: -``` -本月累计提现金额 = 查询本月所有已提现金额之和 -剩余免税额度 = 免税额度 - 本月累计提现金额 - -如果 本次提现金额 <= 剩余免税额度: - 应税金额 = 0 - 税费 = 0 -否则: - 应税金额 = 本次提现金额 - 剩余免税额度 - 税费 = 应税金额 × 税率 - -实际到账金额 = 提现金额 - 税费 -``` - -**示例**: -- 免税额度: 1000元 -- 税率: 6% -- 本月已提现: 800元 -- 本次提现: 500元 - -计算: -- 剩余免税额度 = 1000 - 800 = 200元 -- 本次提现500元 > 剩余免税额度200元 -- 应税金额 = 500 - 200 = 300元 -- 税费 = 300 × 6% = 18元 -- 实际到账 = 500 - 18 = 482元 - -#### 3.5.3 提现流程 - -1. **代理申请提现**: - - 验证实名认证状态 - - 验证余额 - - 计算税费 - - 冻结余额 - - 创建提现记录和扣税记录 - -2. **管理员审核**: - - 审核通过: 调用支付宝转账接口 - - 审核拒绝: 解冻余额,返回余额 - -3. **转账处理**: - - 转账成功: 解冻并扣除余额,更新扣税状态 - - 转账失败: 解冻余额,返回余额 - - 处理中: 保持提现中状态,后续轮询更新 - ---- - -## 四、完整链路流程 - -### 4.1 通过邀请码成为代理链路 - -**重要规则**:用户不能自主成为代理,只能通过邀请码成为代理。成为代理的唯一途径包括: -1. 平台发放钻石邀请码 -2. 代理发放邀请码 -3. 代理邀请链接/二维码 - -``` -┌─────────────────────────────────────────────────────────────┐ -│ 1. 用户通过邀请码申请成为代理 │ -│ API: POST /api/v1/agent/apply │ -│ Logic: ApplyForAgentLogic │ -│ - 必须提供邀请码(必填) │ -│ - 如果没有邀请码,直接拒绝 │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 2. 验证手机验证码 │ -│ - 从Redis读取验证码 │ -│ - 验证码校验 │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 3. 用户注册/绑定(如需要) │ -│ - 检查用户是否存在 │ -│ - 不存在则注册新用户 │ -│ - 临时用户则绑定为正式用户 │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 4. 检查是否已是代理 │ -│ - 检查是否已是代理(agent表) │ -│ - 如果已是代理,直接拒绝 │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 5. 验证邀请码 │ -│ - 查询邀请码是否存在 │ -│ - 验证邀请码状态(未使用、未过期) │ -│ - 获取邀请码信息(目标等级、发放代理ID) │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 6. 创建代理记录 │ -│ - 根据邀请码的target_level设置代理等级 │ -│ - 如果是代理发放的邀请码,建立上下级关系 │ -│ - 如果是平台发放的钻石邀请码,独立成团队 │ -│ - 初始化钱包 │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 7. 更新邀请码状态 │ -│ - **钻石邀请码**:状态更新为"已使用"(status=1),使用后立即失效│ -│ - **普通邀请码**:不更新状态,保持未使用(status=0),可继续使用│ -│ - 记录使用用户ID和代理ID(用于统计) │ -│ - 记录使用时间 │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 8. 生成并返回Token │ -│ - 生成JWT Token │ -│ - 返回给前端 │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 8. 完成(无需审核,直接成为代理) │ -│ - 代理记录已创建 │ -│ - 钱包已初始化 │ -│ - 关系已建立(如有上级) │ -│ - 团队首领已设置 │ -│ - 微信号和区域为可选字段 │ -└─────────────────────────────────────────────────────────────┘ -``` - -### 4.2 推广链接生成链路 - -``` -┌─────────────────────────────────────────────────────────────┐ -│ 1. 代理生成推广链接 │ -│ API: POST /api/v1/agent/link/generate │ -│ Logic: GeneratingLinkLogic │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 2. 获取代理信息 │ -│ - 从Token获取用户ID │ -│ - 查询 agent 表 │ -│ - 验证代理身份 │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 3. 获取系统配置 │ -│ - base_price(基础底价) │ -│ - system_max_price(系统价格上限) │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 4. 计算实际底价 │ -│ 实际底价 = 基础底价 + 等级加成 │ -│ - 普通代理: +6元 │ -│ - 黄金代理: +3元 │ -│ - 钻石代理: +0元 │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 5. 验证设定价格范围 │ -│ - 实际底价 ≤ 设定价格 ≤ 系统价格上限 │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 6. 检查是否已存在相同链接 │ -│ - 查询 agent_link 表 │ -│ - 条件: agent_id + product_id + set_price + del_state=0 │ -│ - 如果存在,直接返回已有链接 │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 7. 生成推广链接标识 │ -│ - 构建 AgentIdentifier 结构 │ -│ { AgentID, ProductID, SetPrice } │ -│ - JSON序列化 │ -│ - AES加密生成 LinkIdentifier │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 8. 保存推广链接 │ -│ - 插入 agent_link 表 │ -│ - 记录 agent_id, product_id, link_identifier │ -│ - 记录 set_price, actual_base_price │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 9. 返回加密后的 LinkIdentifier │ -│ - 前端使用此标识生成推广链接 │ -└─────────────────────────────────────────────────────────────┘ -``` - -### 4.3 订单处理与收益分配链路 - -``` -┌─────────────────────────────────────────────────────────────┐ -│ 1. 用户通过推广链接下单 │ -│ API: POST /api/v1/pay/payment │ -│ Logic: PaymentLogic │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 2. 解析推广链接标识 │ -│ - 解密 LinkIdentifier │ -│ - 解析 AgentIdentifier │ -│ - 获取 AgentID, ProductID, SetPrice │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 3. 查询推广链接信息 │ -│ - 查询 agent_link 表 │ -│ - 验证链接有效性 │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 4. 创建订单记录 │ -│ - 插入 order 表 │ -│ - 记录订单金额(SetPrice) │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 5. 创建代理订单记录 │ -│ - 插入 agent_order 表 │ -│ - OrderAmount = SetPrice │ -│ - SetPrice = 设定价格 │ -│ - ActualBasePrice = 实际底价(基础底价+等级加成) │ -│ - PriceCost = 提价成本 │ -│ - AgentProfit = 代理收益(SetPrice - ActualBasePrice - PriceCost)│ -│ - ProcessStatus = 0(待处理) │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 6. 用户支付订单 │ -│ - 调用支付宝/微信支付接口 │ -│ - 生成支付订单 │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 7. 支付成功回调 │ -│ - 支付宝/微信支付回调 │ -│ - 验证签名 │ -│ - 更新订单状态 │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 8. 触发代理订单处理 │ -│ Service: AgentService.AgentProcess │ -│ - 检查是否是代理订单 │ -│ - 检查订单是否已处理(防重复) │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 9. 获取代理信息和系统配置 │ -│ - 查询 agent 表 │ -│ - 获取代理等级 │ -│ - 获取系统配置(base_price等) │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 10. 使用事务处理订单(开始事务) │ -│ - 计算实际底价和代理收益 │ -│ - 计算提价成本 │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 11. 更新代理订单状态 │ -│ - ProcessStatus = 1(处理成功) │ -│ - ProcessTime = 当前时间 │ -│ - ProcessRemark = "处理成功" │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 12. 发放代理佣金 │ -│ - 创建 agent_commission 记录 │ -│ - 更新 agent_wallet │ -│ Balance += AgentProfit │ -│ TotalEarnings += AgentProfit │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 13. 分配等级加成返佣 │ -│ - 根据代理等级分配返佣 │ -│ - 普通代理(6元): 给直接上级3元,剩余给钻石/黄金上级 │ -│ - 黄金代理(3元): 全部给钻石上级 │ -│ - 钻石代理(0元): 无返佣 │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 14. 记录返佣 │ -│ - 创建 agent_rebate 记录 │ -│ - 更新上级钱包余额 │ -│ - 记录返佣类型和金额 │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 15. 提交事务(完成) │ -└─────────────────────────────────────────────────────────────┘ -``` - -### 4.4 代理升级链路 - -#### 4.4.1 自主付费升级链路 - -``` -┌─────────────────────────────────────────────────────────────┐ -│ 1. 代理申请升级 │ -│ API: POST /api/v1/agent/upgrade/apply │ -│ Logic: ApplyUpgradeLogic │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 2. 验证升级条件 │ -│ - 验证当前等级和目标等级 │ -│ - 普通→黄金: ✓ │ -│ - 普通→钻石: ✓ │ -│ - 黄金→钻石: ✓ │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 3. 计算升级费用和返佣 │ -│ - 普通→黄金: 199元,返佣139元 │ -│ - 普通→钻石: 980元,返佣680元 │ -│ - 黄金→钻石: 980元,返佣680元 │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 4. 查找原直接上级 │ -│ - 查询 agent_relation 表 │ -│ - RelationType = 1(直接关系) │ -│ - 用于返佣 │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 5. 创建升级记录 │ -│ - 插入 agent_upgrade 表 │ -│ - UpgradeType = 1(自主付费) │ -│ - Status = 1(待处理) │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 6. 返回升级ID和订单号 │ -│ - 用于支付 │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 7. 用户支付升级费用 │ -│ - 调用支付接口 │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 8. 支付成功回调 │ -│ - 触发升级处理 │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 9. 执行升级操作 │ -│ Service: AgentService.ProcessUpgrade │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 10. 返佣给原直接上级(开始事务) │ -│ - 更新上级钱包余额 │ -│ - Balance += RebateAmount │ -│ - TotalEarnings += RebateAmount │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 11. 更新代理等级 │ -│ - agent.Level = toLevel │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 12. 检查是否需要脱离直接上级关系 │ -│ - 检查升级后等级是否高于上级 │ -│ - 检查是否同级(黄金/钻石,普通代理同级除外) │ -└─────────────────────────────────────────────────────────────┘ - ↓ - ┌───────────────────────────────┐ - │ 需要脱离关系 │ - └───────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 13. 脱离直接上级关系 │ -│ - 更新 agent_relation 表 │ -│ - RelationType = 2(已脱离) │ -│ - DetachReason = "upgrade" │ -│ - DetachTime = 当前时间 │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 14. 更新团队首领 │ -│ - 如果升级为钻石: team_leader_id = 自己 │ -│ - 如果升级为黄金: 查找上级链中的钻石代理 │ -│ - 更新所有下级的 team_leader_id │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 15. 更新升级记录状态 │ -│ - Status = 2(已完成) │ -│ - Remark = "升级成功" │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 16. 提交事务(完成) │ -└─────────────────────────────────────────────────────────────┘ -``` - -#### 4.4.2 钻石升级下级链路 - -``` -┌─────────────────────────────────────────────────────────────┐ -│ 1. 钻石代理升级下级 │ -│ API: POST /api/v1/agent/upgrade/subordinate │ -│ Logic: UpgradeSubordinateLogic │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 2. 验证权限 │ -│ - 必须是钻石代理(Level = 3) │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 3. 验证下级等级 │ -│ - 只能是普通代理(Level = 1) │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 4. 验证关系 │ -│ - 必须是直接下级 │ -│ - 查询 agent_relation 表 │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 5. 验证目标等级 │ -│ - 只能升级为黄金(toLevel = 2) │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 6. 创建升级记录 │ -│ - 插入 agent_upgrade 表 │ -│ - UpgradeType = 2(钻石升级下级) │ -│ - UpgradeFee = 0(免费) │ -│ - RebateAmount = 0(无返佣) │ -│ - OperatorAgentId = 操作者代理ID │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 7. 执行升级操作 │ -│ Service: AgentService.ProcessUpgrade │ -│ - 更新下级等级为黄金 │ -│ - 脱离直接上级关系(如需要) │ -│ - 更新团队首领(保持原团队首领) │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 8. 更新升级记录状态 │ -│ - Status = 2(已完成) │ -│ - Remark = "钻石代理升级下级成功" │ -└─────────────────────────────────────────────────────────────┘ -``` - -### 4.5 提现链路 - -``` -┌─────────────────────────────────────────────────────────────┐ -│ 1. 代理申请提现 │ -│ API: POST /api/v1/agent/withdrawal/apply │ -│ Logic: ApplyWithdrawalLogic │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 2. 验证实名认证 │ -│ - 查询 agent_real_name 表 │ -│ - verify_time 必须不为空(三要素核验已通过) │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 3. 验证提现金额 │ -│ - Amount > 0 │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 4. 验证钱包余额 │ -│ - Balance >= Amount │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 5. 计算税费 │ -│ - 查询本月累计提现金额 │ -│ - 计算剩余免税额度 │ -│ - 计算应税金额 │ -│ - 计算税费 = 应税金额 × 税率 │ -│ - 计算实际到账金额 = 提现金额 - 税费 │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 6. 生成提现单号 │ -│ - 格式: WD + timestamp + agentId │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 7. 使用事务处理提现申请(开始事务) │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 8. 冻结余额 │ -│ - FrozenBalance += Amount │ -│ - Balance -= Amount │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 9. 创建提现记录 │ -│ - 插入 agent_withdrawal 表 │ -│ - Status = 1(待审核) │ -│ - 记录 PayeeAccount, PayeeName │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 10. 创建扣税记录 │ -│ - 插入 agent_withdrawal_tax 表 │ -│ - TaxStatus = 1(待扣税) │ -│ - 记录 YearMonth, TaxableAmount, TaxAmount │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 11. 提交事务(完成) │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 12. 管理员审核提现 │ -│ API: POST /api/v1/admin/agent/withdrawal/audit │ -│ Logic: AdminAuditWithdrawalLogic │ -└─────────────────────────────────────────────────────────────┘ - ↓ - ┌───────────────────────────────┐ - │ 审核通过(Status=2) │ - └───────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 13. 更新提现状态为提现中 │ -│ - Status = 4(提现中) │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 14. 调用支付宝转账接口 │ -│ - AliTransfer(account, name, amount, remark, outBizNo) │ -└─────────────────────────────────────────────────────────────┘ - ↓ - ┌───────────────┬───────────────┬───────────────┐ - │ 转账成功 │ 转账失败 │ 处理中 │ - │ (SUCCESS) │ (FAIL) │ (DEALING) │ - └───────────────┴───────────────┴───────────────┘ - ↓ ↓ ↓ - ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ - │ 15. 更新状态 │ │ 15. 更新状态 │ │ 15. 保持状态 │ - │ Status = 5 │ │ Status = 6 │ │ Status = 4 │ - │ (提现成功) │ │ (提现失败) │ │ (提现中) │ - └───────────────┘ └───────────────┘ └───────────────┘ - ↓ ↓ - ┌───────────────┐ ┌───────────────┐ - │ 16. 解冻并扣 │ │ 16. 解冻余额 │ - │ 除余额 │ │ 返回余额 │ - │ - FrozenBalance│ │ - FrozenBalance│ - │ -= Amount │ │ -= Amount │ - │ - WithdrawnAmount│ │ - Balance │ - │ += Amount │ │ += Amount │ - └───────────────┘ └───────────────┘ - ↓ - ┌───────────────┐ - │ 17. 更新扣税 │ - │ 状态 │ - │ TaxStatus = 2 │ - │ (已扣税) │ - └───────────────┘ -``` - -### 4.6 实名认证链路 - -``` -┌─────────────────────────────────────────────────────────────┐ -│ 1. 代理提交实名认证信息 │ -│ API: POST /api/v1/agent/real_name │ -│ Logic: RealNameAuthLogic │ -│ - 姓名、身份证号、手机号 │ -│ - 验证码(手机号验证码) │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 2. 验证代理身份 │ -│ - 从Token获取用户ID │ -│ - 查询 agent 表 │ -│ - 验证手机号是否匹配 │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 3. 验证手机验证码 │ -│ - 从Redis读取验证码 │ -│ - 验证码校验 │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 4. 三要素核验(自动) │ -│ - 调用 VerificationService.ThreeFactorVerification │ -│ - 核验姓名、身份证号、手机号是否一致 │ -│ - 核验通过:继续流程 │ -│ - 核验失败:返回错误 │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 5. 加密敏感信息 │ -│ - 加密身份证号 │ -│ - 加密手机号 │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 6. 保存实名认证记录(开始事务) │ -│ - 检查是否已有记录 │ -│ - 如有记录:更新姓名、身份证号、手机号、verify_time │ -│ - 如无记录:创建新记录,设置 verify_time = 当前时间 │ -│ - verify_time 不为空表示已通过三要素核验 │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 7. 提交事务(完成) │ -│ - 返回成功,实名认证完成 │ -│ - 可以申请提现 │ -└─────────────────────────────────────────────────────────────┘ -``` - -**重要说明**: -- 实名认证改为三要素核验,无需人工审核 -- 核验通过后自动设置 `verify_time`,无需管理员操作 -- `verify_time` 不为空即表示已通过认证,可以提现 - ---- - -## 五、关键代码逻辑 - -### 5.1 AgentService 核心方法 - -#### 5.1.1 AgentProcess(订单处理) - -**文件**: `app/main/api/internal/service/agentService.go` - -**功能**: 处理代理订单,分配收益和返佣 - -**核心逻辑**: -```go -func (s *AgentService) AgentProcess(ctx context.Context, order *model.Order) error { - // 1. 检查是否是代理订单 - agentOrder, err := s.AgentOrderModel.FindOneByOrderId(ctx, order.Id) - if err != nil { - if errors.Is(err, model.ErrNotFound) { - return nil // 不是代理订单 - } - return err - } - - // 2. 检查订单是否已处理(防重复) - if agentOrder.ProcessStatus == 1 { - return nil - } - - // 3. 获取代理信息和系统配置 - agent, err := s.AgentModel.FindOne(ctx, agentOrder.AgentId) - basePrice, err := s.getConfigFloat(ctx, "base_price") - - // 4. 使用事务处理 - return s.AgentWalletModel.Trans(ctx, func(transCtx context.Context, session sqlx.Session) error { - // 4.1 计算实际底价和代理收益 - levelBonus := s.getLevelBonus(agent.Level) - actualBasePrice := basePrice + float64(levelBonus) - priceCost := s.calculatePriceCost(agentOrder.SetPrice, priceThreshold, priceFeeRate) - agentProfit := agentOrder.SetPrice - actualBasePrice - priceCost - - // 4.2 更新代理订单状态 - agentOrder.ProcessStatus = 1 - s.AgentOrderModel.UpdateWithVersion(transCtx, session, agentOrder) - - // 4.3 发放代理佣金 - s.giveAgentCommission(transCtx, session, agentOrder.AgentId, order.Id, order.ProductId, agentProfit) - - // 4.4 分配等级加成返佣 - if levelBonus > 0 { - s.distributeLevelBonus(transCtx, session, agent, order.Id, order.ProductId, float64(levelBonus), levelBonus) - } - - return nil - }) -} -``` - -#### 5.1.2 distributeLevelBonus(等级加成返佣分配) - -**功能**: 根据代理等级分配等级加成返佣给上级链 - -**核心逻辑**: -```go -func (s *AgentService) distributeLevelBonus(ctx context.Context, session sqlx.Session, agent *model.Agent, orderId, productId int64, levelBonus float64, levelBonusInt int64) error { - // 钻石代理:等级加成为0,无返佣分配 - if agent.Level == 3 { - return nil - } - - // 黄金代理:等级加成3元,全部给钻石上级 - if agent.Level == 2 { - diamondParent, err := s.findDiamondParent(ctx, agent.Id) - if diamondParent != nil { -100002 return s.giveRebate(ctx, session, diamondParent.Id, agent.Id, orderId, productId, levelBonus, levelBonusInt, 2) - } - return nil - } - - // 普通代理:等级加成6元,按规则分配给上级链 - if agent.Level == 1 { - return s.distributeNormalAgentBonus(ctx, session, agent, orderId, productId, levelBonus, levelBonusInt) - } - - return nil -} -``` - -#### 5.1.3 ProcessUpgrade(代理升级处理) - -**功能**: 处理代理升级,包括返佣、关系脱离、团队首领更新 - -**核心逻辑**: -```go -func (s *AgentService) ProcessUpgrade(ctx context.Context, agentId, toLevel int64, upgradeType int64, upgradeFee, rebateAmount float64, orderNo string, operatorAgentId int64) error { - return s.AgentWalletModel.Trans(ctx, func(transCtx context.Context, session sqlx.Session) error { - // 1. 获取代理信息 - agent, err := s.AgentModel.FindOne(transCtx, agentId) - - // 2. 如果是自主付费升级,处理返佣 - if upgradeType == 1 { - parent, err := s.findDirectParent(transCtx, agentId) - if parent != nil && rebateAmount > 0 { - s.giveRebateForUpgrade(transCtx, session, parent.Id, agentId, rebateAmount) - } - } - - // 3. 更新代理等级 - agent.Level = toLevel - - // 4. 检查是否需要脱离直接上级关系 - needDetach, err := s.needDetachFromParent(transCtx, agent, toLevel) - if needDetach { - s.detachFromParent(transCtx, session, agentId) - } - - // 5. 如果升级为钻石,独立成新团队 - if toLevel == 3 { - agent.TeamLeaderId = sql.NullInt64{Int64: agentId, Valid: true} - s.updateChildrenTeamLeader(transCtx, session, agentId, agentId) - } else { - // 更新团队首领(查找上级链中的钻石代理) - teamLeaderId, _ := s.findTeamLeaderId(transCtx, agentId) - if teamLeaderId > 0 { - agent.TeamLeaderId = sql.NullInt64{Int64: teamLeaderId, Valid: true} - } - } - - // 6. 更新代理记录 - s.AgentModel.UpdateWithVersion(transCtx, session, agent) - - return nil - }) -} -``` - -#### 5.1.4 calculateTax(税费计算) - -**功能**: 计算提现税费,基于月度累计和免税额度 - -**核心逻辑**: -```go -func calculateTax(ctx context.Context, agentId int64, amount float64, yearMonth int64) (*TaxInfo, error) { - // 1. 获取税率配置(默认6%) - taxRate := 0.06 - config, _ := AgentConfigModel.FindOneByConfigKey(ctx, "tax_rate") - if config != nil { - taxRate = parseFloat(config.ConfigValue) - } - - // 2. 查询本月已提现金额 - taxRecords, _ := AgentWithdrawalTaxModel.FindAll(...) - monthlyTotal := sum(taxRecords.WithdrawalAmount) - - // 3. 获取免税额度配置(默认0) - exemptionAmount := 0.0 - exemptionConfig, _ := AgentConfigModel.FindOneByConfigKey(ctx, "tax_exemption_amount") - if exemptionConfig != nil { - exemptionAmount = parseFloat(exemptionConfig.ConfigValue) - } - - // 4. 计算应税金额 - remainingExemption := exemptionAmount - monthlyTotal - taxableAmount := amount - if remainingExemption > 0 { - if amount <= remainingExemption { - taxableAmount = 0 // 完全免税 - } else { - taxableAmount = amount - remainingExemption // 部分免税 - } - } - - // 5. 计算税费和实际到账金额 - taxAmount := taxableAmount * taxRate - actualAmount := amount - taxAmount - - return &TaxInfo{ - TaxableAmount: taxableAmount, - TaxRate: taxRate, - TaxAmount: taxAmount, - ActualAmount: actualAmount, - }, nil -} -``` - ---- - -## 六、API接口列表 - -### 6.1 前端接口(agent.api) - -#### 6.1.1 公开接口(无需登录) - -| 接口 | 方法 | 路径 | 说明 | -| ---------------------- | ---- | --------------------- | -------------------------------------------- | -| 获取推广链接数据 | GET | `/api/v1/agent/link` | 根据LinkIdentifier获取推广链接信息 | -| 通过邀请码申请成为代理 | POST | `/api/v1/agent/apply` | 用户通过邀请码申请成为代理(必须提供邀请码) | - -#### 6.1.2 需要登录的接口 - -| 接口 | 方法 | 路径 | 说明 | -| -------------------- | ------- | ----------------------------------- | ------------------------------------------------------ | -| ~~查询代理申请状态~~ | ~~GET~~ | ~~`/api/v1/agent/audit/status`~~ | ~~查询当前用户的代理申请审核状态(已废弃:无需审核)~~ | -| 查看代理信息 | GET | `/api/v1/agent/info` | 获取当前代理的详细信息 | -| 生成推广链接 | POST | `/api/v1/agent/generating_link` | 生成产品推广链接 | -| 获取产品配置 | GET | `/api/v1/agent/product_config` | 获取代理可配置的产品价格信息 | -| 获取团队统计 | GET | `/api/v1/agent/team/statistics` | 获取团队统计数据 | -| 获取下级列表 | GET | `/api/v1/agent/subordinate/list` | 分页查询直接下级列表 | -| 获取收益信息 | GET | `/api/v1/agent/revenue` | 获取钱包余额和收益信息 | -| 获取佣金记录 | GET | `/api/v1/agent/commission/list` | 分页查询佣金记录 | -| 获取返佣记录 | GET | `/api/v1/agent/rebate/list` | 分页查询返佣记录 | -| 获取升级记录 | GET | `/api/v1/agent/upgrade/list` | 分页查询升级记录 | -| 申请升级 | POST | `/api/v1/agent/upgrade/apply` | 自主付费升级 | -| 钻石升级下级 | POST | `/api/v1/agent/upgrade/subordinate` | 钻石代理升级下级 | -| 获取提现列表 | GET | `/api/v1/agent/withdrawal/list` | 分页查询提现记录 | -| 申请提现 | POST | `/api/v1/agent/withdrawal/apply` | 申请提现 | -| 实名认证 | POST | `/api/v1/agent/real_name` | 提交实名认证信息(三要素核验,自动通过) | - -### 6.2 后台管理接口(admin_agent.api) - -| 接口 | 方法 | 路径 | 说明 | -| -------------------- | -------- | ------------------------------------------- | ------------------------------------------------------------ | -| 代理分页查询 | GET | `/api/v1/admin/agent/list` | 分页查询代理列表 | -| ~~代理审核~~ | ~~POST~~ | ~~`/api/v1/admin/agent/audit`~~ | ~~审核代理申请(已废弃:通过邀请码直接成为代理,无需审核)~~ | -| 推广链接分页查询 | GET | `/api/v1/admin/agent/link/list` | 分页查询推广链接 | -| 代理订单分页查询 | GET | `/api/v1/admin/agent/order/list` | 分页查询代理订单 | -| 代理佣金分页查询 | GET | `/api/v1/admin/agent/commission/list` | 分页查询佣金记录 | -| 代理返佣分页查询 | GET | `/api/v1/admin/agent/rebate/list` | 分页查询返佣记录 | -| 代理升级记录分页查询 | GET | `/api/v1/admin/agent/upgrade/list` | 分页查询升级记录 | -| 代理提现分页查询 | GET | `/api/v1/admin/agent/withdrawal/list` | 分页查询提现记录 | -| 代理提现审核 | POST | `/api/v1/admin/agent/withdrawal/audit` | 审核提现申请 | -| 代理实名认证分页查询 | GET | `/api/v1/admin/agent/real_name/list` | 分页查询实名认证记录 | -| ~~代理实名认证审核~~ | ~~POST~~ | ~~`/api/v1/admin/agent/real_name/audit`~~ | ~~审核实名认证(已废弃:改为三要素核验,无需审核)~~ | -| 系统配置查询 | GET | `/api/v1/admin/agent/config` | 查询系统配置 | -| 系统配置更新 | POST | `/api/v1/admin/agent/config/update` | 更新系统配置 | -| 产品配置分页查询 | GET | `/api/v1/admin/agent/product_config/list` | 分页查询产品配置 | -| 产品配置更新 | POST | `/api/v1/admin/agent/product_config/update` | 更新产品配置 | - ---- - -## 七、系统配置说明 - -### 7.1 系统配置项(agent_config表) - -| 配置键 | 配置类型 | 默认值 | 说明 | -| --------------------------- | -------- | ------ | ------------------------------- | -| `base_price` | price | - | 基础底价(系统默认) | -| `system_max_price` | price | - | 系统价格上限 | -| `price_threshold` | price | - | 提价标准阈值 | -| `price_fee_rate` | price | - | 提价手续费比例(0-1之间的小数) | -| `level_1_bonus` | bonus | 6 | 普通代理等级加成(元) | -| `level_2_bonus` | bonus | 3 | 黄金代理等级加成(元) | -| `level_3_bonus` | bonus | 0 | 钻石代理等级加成(元) | -| `upgrade_to_gold_fee` | upgrade | 199 | 升级为黄金费用(元) | -| `upgrade_to_diamond_fee` | upgrade | 980 | 升级为钻石费用(元) | -| `upgrade_to_gold_rebate` | rebate | 139 | 升级为黄金返佣(元) | -| `upgrade_to_diamond_rebate` | rebate | 680 | 升级为钻石返佣(元) | -| `tax_rate` | tax | 0.06 | 税率(默认6%,即0.06) | -| `tax_exemption_amount` | tax | 0 | 免税额度(元,默认0) | - -### 7.2 产品配置项(agent_product_config表) - -每个产品可以单独配置以下参数,覆盖系统默认配置: - -| 字段 | 说明 | -| ------------------ | ---------------------------------------- | -| `base_price` | 产品基础底价(覆盖系统配置) | -| `system_max_price` | 产品价格上限(覆盖系统配置) | -| `price_threshold` | 产品提价标准阈值(可选,覆盖系统配置) | -| `price_fee_rate` | 产品提价手续费比例(可选,覆盖系统配置) | - -### 7.3 配置优先级 - -1. **产品配置** > **系统配置** - - 如果产品配置了 `base_price`,使用产品配置 - - 如果产品未配置,使用系统配置 - -2. **系统配置** > **代码默认值** - - 如果系统配置存在,使用系统配置 - - 如果系统配置不存在,使用代码中的默认值 - ---- - -## 八、数据流转图 - -### 8.1 订单处理数据流 - -``` -用户下单 - ↓ -创建 order 记录 - ↓ -创建 agent_order 记录(ProcessStatus=0) - ↓ -用户支付 - ↓ -支付成功回调 - ↓ -调用 AgentService.AgentProcess - ↓ -┌─────────────────────────────────────┐ -│ 事务开始 │ -│ 1. 计算实际底价 = 基础底价 + 等级加成 │ -│ 2. 计算提价成本 │ -│ 3. 计算代理收益 │ -│ 4. 更新 agent_order (ProcessStatus=1) │ -│ 5. 创建 agent_commission 记录 │ -│ 6. 更新 agent_wallet (Balance, TotalEarnings) │ -│ 7. 分配等级加成返佣 │ -│ - 创建 agent_rebate 记录 │ -│ - 更新上级 agent_wallet │ -│ 事务提交 │ -└─────────────────────────────────────┘ -``` - -### 8.2 升级处理数据流 - -``` -代理申请升级 - ↓ -创建 agent_upgrade 记录(Status=1) - ↓ -用户支付升级费用 - ↓ -支付成功回调 - ↓ -调用 AgentService.ProcessUpgrade - ↓ -┌─────────────────────────────────────┐ -│ 事务开始 │ -│ 1. 返佣给原直接上级(如需要) │ -│ - 更新上级 agent_wallet │ -│ 2. 更新 agent.Level │ -│ 3. 检查是否需要脱离关系 │ -│ - 更新 agent_relation (RelationType=2) │ -│ 4. 更新团队首领 │ -│ - 更新 agent.TeamLeaderId │ -│ - 更新所有下级的 TeamLeaderId │ -│ 5. 更新 agent_upgrade (Status=2) │ -│ 事务提交 │ -└─────────────────────────────────────┘ -``` - -### 8.3 提现处理数据流 - -``` -代理申请提现 - ↓ -验证实名认证和余额 - ↓ -计算税费 - ↓ -┌─────────────────────────────────────┐ -│ 事务开始 │ -│ 1. 冻结余额 │ -│ - agent_wallet.FrozenBalance += Amount │ -│ - agent_wallet.Balance -= Amount │ -│ 2. 创建 agent_withdrawal 记录 │ -│ (Status=1, 待审核) │ -│ 3. 创建 agent_withdrawal_tax 记录 │ -│ (TaxStatus=1, 待扣税) │ -│ 事务提交 │ -└─────────────────────────────────────┘ - ↓ -管理员审核 - ↓ -审核通过(Status=2) - ↓ -调用支付宝转账接口 - ↓ -转账成功/失败/处理中 - ↓ -┌─────────────────────────────────────┐ -│ 事务开始 │ -│ 1. 更新 agent_withdrawal.Status │ -│ 2. 解冻并扣除余额(成功) │ -│ - agent_wallet.FrozenBalance -= Amount │ -│ - agent_wallet.WithdrawnAmount += Amount │ -│ 3. 更新 agent_withdrawal_tax.TaxStatus │ -│ 事务提交 │ -└─────────────────────────────────────┘ -``` - ---- - -## 九、关键算法说明 - -### 9.1 团队统计递归算法 - -```go -func getTeamMembers(agentId int64) []int64 { - teamMembers := []int64{agentId} // 包括自己 - - var collectChildren func(int64) - collectChildren = func(parentId int64) { - // 查找直接下级(relation_type=1) - relations := findDirectChildren(parentId) - for _, relation := range relations { - teamMembers = append(teamMembers, relation.ChildId) - collectChildren(relation.ChildId) // 递归 - } - } - - collectChildren(agentId) - return teamMembers -} -``` - -### 9.2 查找上级链算法 - -```go -// 查找直接上级 -func findDirectParent(agentId int64) *Agent { - relation := findRelation(childId=agentId, relationType=1) - return findAgent(relation.ParentId) -} - -// 查找钻石上级(向上递归) -func findDiamondParent(agentId int64) *Agent { - currentId := agentId - maxDepth := 100 - depth := 0 - - for depth < maxDepth { - parent := findDirectParent(currentId) - if parent == nil { - return nil - } - if parent.Level == 3 { // 钻石 - return parent - } - currentId = parent.Id - depth++ - } - return nil -} -``` - -### 9.3 价格计算算法 - -```go -// 计算实际底价 -func calculateActualBasePrice(agentLevel int64, basePrice float64) float64 { - levelBonus := getLevelBonus(agentLevel) // 6, 3, 0 - return basePrice + float64(levelBonus) -} - -// 计算提价成本 -func calculatePriceCost(setPrice, priceThreshold, priceFeeRate float64) float64 { - if setPrice <= priceThreshold { - return 0 - } - return (setPrice - priceThreshold) * priceFeeRate -} - -// 计算代理收益 -func calculateAgentProfit(setPrice, actualBasePrice, priceCost float64) float64 { - return setPrice - actualBasePrice - priceCost -} -``` - ---- - -## 十、注意事项和最佳实践 - -### 10.1 数据一致性 - -1. **事务使用**: 所有涉及多表更新的操作必须使用事务 - - 订单处理(agent_order, agent_commission, agent_wallet, agent_rebate) - - 升级处理(agent, agent_relation, agent_wallet, agent_upgrade) - - 提现处理(agent_withdrawal, agent_withdrawal_tax, agent_wallet) - -2. **乐观锁**: 所有更新操作使用 `version` 字段进行乐观锁控制 - - 使用 `UpdateWithVersion` 方法 - - 更新失败时重试或提示用户 - -3. **防重复处理**: - - 订单处理前检查 `agent_order.ProcessStatus` - - 升级处理前检查 `agent_upgrade.Status` - -### 10.2 性能优化 - -1. **批量查询**: 避免N+1查询问题 - - 列表查询时批量获取产品名称、代理信息等 - -2. **索引使用**: - - `agent_relation`: `idx_parent_relation`, `idx_child_relation` - - `agent_rebate`: `idx_order_rebate_type` - - `agent_order`: `idx_process_status` - -3. **递归深度限制**: - - 查找上级链时设置最大深度(如100层) - - 防止无限循环 - -### 10.3 错误处理 - -1. **配置缺失**: 配置项不存在时使用默认值 - - 税率默认6% - - 等级加成使用代码中的固定值 - -2. **关系异常**: - - 找不到上级时,返佣归平台 - - 升级时找不到直接上级,跳过返佣 - -3. **余额不足**: - - 提现前验证余额 - - 使用事务确保余额扣减的原子性 - -### 10.4 安全考虑 - -1. **数据加密**: - - 手机号、身份证号使用AES加密存储 - - 推广链接标识加密传输 - -2. **权限控制**: - - 代理只能查看自己的数据 - - 管理员需要认证和授权 - -3. **金额精度**: - - 使用 `decimal(10,2)` 类型存储金额 - - 计算时注意浮点数精度问题 - ---- - -## 十一、常见问题解答 - -### 11.1 升级后关系脱离规则 - -**Q**: 普通代理升级为黄金后什么情况下会脱离直接上级关系? - -**A**: 根据关系约束规则: -- **下级不能比上级等级高**:如果直接上级是普通代理,升级后黄金等级高于普通,必须脱离 -- **同级不能作为上下级(除了普通代理)**:如果直接上级是黄金代理,升级后同级,必须脱离 -- **钻石可以拥有黄金下级**:如果直接上级是钻石代理,升级后钻石等级高于黄金,不脱离关系,保持直接下级关系 - -### 11.2 返佣分配规则 - -**Q**: 普通代理的6元等级加成如何分配? - -**A**: 按优先级分配: -1. 给直接上级(根据上级等级:钻石6元,黄金3元,普通2元) -2. 剩余金额给钻石上级(如有) -3. 如果无钻石上级,给黄金上级(最多3元) -4. 都没有,剩余归平台 - -### 11.3 团队归属 - -**Q**: 升级后团队归属如何变化? - -**A**: -- 普通→黄金:仍属于原团队(通过team_leader_id指向原钻石代理) -- 黄金→钻石:独立成新团队(team_leader_id指向自己) -- 普通→钻石:独立成新团队(team_leader_id指向自己) - -### 11.4 税费计算 - -**Q**: 税费如何计算? - -**A**: -1. 查询本月累计提现金额 -2. 计算剩余免税额度 = 免税额度 - 本月累计 -3. 如果本次提现 <= 剩余免税额度,免税 -4. 否则,应税金额 = 本次提现 - 剩余免税额度 -5. 税费 = 应税金额 × 税率 - ---- - -## 十二、后续优化建议 - -### 12.1 功能扩展 - -1. **多级返佣**: 支持更多层级的返佣分配 -2. **业绩统计**: 增加团队业绩统计和排行榜 -3. **消息通知**: 收益到账、升级成功等消息推送 - -### 12.2 性能优化 - -1. **缓存策略**: - - 系统配置缓存 - - 团队统计数据缓存 - - 代理信息缓存 - -2. **异步处理**: - - 订单处理异步化 - - 团队统计异步计算 - -### 12.3 监控和日志 - -1. **关键操作日志**: - - 订单处理日志 - - 升级操作日志 - - 提现操作日志 - -2. **性能监控**: - - 订单处理耗时 - - 数据库查询耗时 - - 接口响应时间 - ---- - -## 附录 - -### A. 数据表索引说明 - -详见 `deploy/sql/agent_system_migration.sql` 文件中的索引定义。 - -### B. API接口详细定义 - -详见 `app/main/api/desc/front/agent.api` 和 `app/main/api/desc/admin/admin_agent.api` 文件。 - -### C. 代码文件清单 - -- **Service层**: `app/main/api/internal/service/agentService.go` -- **Logic层**: `app/main/api/internal/logic/agent/*.go` -- **Model层**: `app/main/model/agent*.go` -- **API定义**: `app/main/api/desc/front/agent.api`, `app/main/api/desc/admin/admin_agent.api` - ---- - -**文档结束** \ No newline at end of file diff --git a/新代理系统检查清单.md b/新代理系统检查清单.md deleted file mode 100644 index 449f137..0000000 --- a/新代理系统检查清单.md +++ /dev/null @@ -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. 进行端到端测试验证 - diff --git a/短链系统实现说明.md b/短链系统实现说明.md deleted file mode 100644 index 5101bd7..0000000 --- a/短链系统实现说明.md +++ /dev/null @@ -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`(如果不传则使用默认值) - diff --git a/解冻任务优化建议.md b/解冻任务优化建议.md deleted file mode 100644 index bad524c..0000000 --- a/解冻任务优化建议.md +++ /dev/null @@ -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. **配置化并发数** - 如果并发数不需要调整可以不做 - -## 实施建议 - -建议先实现**超时控制**和**错误分类处理**,这两个对可靠性影响最大。其他优化可以根据实际运行情况逐步添加。 - diff --git a/解冻任务实现方案说明.md b/解冻任务实现方案说明.md deleted file mode 100644 index 0beb0d3..0000000 --- a/解冻任务实现方案说明.md +++ /dev/null @@ -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. **补偿机制**:提供手动触发扫描的接口,用于紧急情况 - diff --git a/退款时代理处理逻辑分析.md b/退款时代理处理逻辑分析.md deleted file mode 100644 index c1be031..0000000 --- a/退款时代理处理逻辑分析.md +++ /dev/null @@ -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`)后订单退款的情况 -- 需要在退款回调和管理员退款中添加撤销代理收益的逻辑 - diff --git a/邀请码使用历史功能说明.md b/邀请码使用历史功能说明.md deleted file mode 100644 index ce2c789..0000000 --- a/邀请码使用历史功能说明.md +++ /dev/null @@ -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; -``` - diff --git a/邀请链接和二维码生成逻辑说明.md b/邀请链接和二维码生成逻辑说明.md deleted file mode 100644 index 1864d0f..0000000 --- a/邀请链接和二维码生成逻辑说明.md +++ /dev/null @@ -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` -