This commit is contained in:
2025-12-02 19:57:10 +08:00
parent 3440744179
commit 08ff223ff8
188 changed files with 12337 additions and 7212 deletions

View File

@@ -7,23 +7,23 @@ import (
type Config struct {
rest.RestConf
DataSource string
CacheRedis cache.CacheConf
JwtAuth JwtAuth // JWT 鉴权相关配置
VerifyCode VerifyCode
Encrypt Encrypt
Alipay AlipayConfig
Wxpay WxpayConfig
Applepay ApplepayConfig
Tianyuanapi TianyuanapiConfig
SystemConfig SystemConfig
WechatH5 WechatH5Config
Authorization AuthorizationConfig // 授权书配置
WechatMini WechatMiniConfig
Query QueryConfig
AdminConfig AdminConfig
AdminPromotion AdminPromotion
TaxConfig TaxConfig
DataSource string
CacheRedis cache.CacheConf
JwtAuth JwtAuth // JWT 鉴权相关配置
VerifyCode VerifyCode
Encrypt Encrypt
Alipay AlipayConfig
Wxpay WxpayConfig
Applepay ApplepayConfig
Tianyuanapi TianyuanapiConfig
SystemConfig SystemConfig
WechatH5 WechatH5Config
Authorization AuthorizationConfig // 授权书配置
WechatMini WechatMiniConfig
Query QueryConfig
AdminConfig AdminConfig
TaxConfig TaxConfig
Promotion PromotionConfig // 推广链接配置
}
// JwtAuth 用于 JWT 鉴权配置
@@ -83,8 +83,8 @@ type WechatH5Config struct {
AppSecret string
}
type WechatMiniConfig struct {
AppID string
AppSecret string
AppID string
AppSecret string
}
type QueryConfig struct {
ShareLinkExpire int64
@@ -95,9 +95,6 @@ type AdminConfig struct {
RefreshAfter int64
}
type AdminPromotion struct {
URLDomain string
}
type TaxConfig struct {
TaxRate float64
TaxExemptionAmount float64
@@ -111,4 +108,10 @@ type TianyuanapiConfig struct {
type AuthorizationConfig struct {
FileBaseURL string // 授权书文件访问基础URL
}
}
// PromotionConfig 推广链接配置
type PromotionConfig struct {
PromotionDomain string // 推广域名(用于生成短链)
OfficialDomain string // 正式站点域名(短链重定向的目标域名)
}

View File

@@ -3,26 +3,28 @@ package admin_agent
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"ycc-server/app/main/api/internal/logic/admin_agent"
"ycc-server/app/main/api/internal/svc"
"ycc-server/app/main/api/internal/types"
"ycc-server/common/result"
"ycc-server/pkg/lzkit/validator"
"github.com/zeromicro/go-zero/rest/httpx"
)
func AdminAuditAgentHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.AdminAuditAgentReq
if err := httpx.Parse(r, &req); err != nil {
httpx.ErrorCtx(r.Context(), w, err)
result.ParamErrorResult(r, w, err)
return
}
if err := validator.Validate(req); err != nil {
result.ParamValidateErrorResult(r, w, err)
return
}
l := admin_agent.NewAdminAuditAgentLogic(r.Context(), svcCtx)
resp, err := l.AdminAuditAgent(&req)
if err != nil {
httpx.ErrorCtx(r.Context(), w, err)
} else {
httpx.OkJsonCtx(r.Context(), w, resp)
}
result.HttpResult(r, w, resp, err)
}
}

View File

@@ -3,26 +3,28 @@ package admin_agent
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"ycc-server/app/main/api/internal/logic/admin_agent"
"ycc-server/app/main/api/internal/svc"
"ycc-server/app/main/api/internal/types"
"ycc-server/common/result"
"ycc-server/pkg/lzkit/validator"
"github.com/zeromicro/go-zero/rest/httpx"
)
func AdminAuditRealNameHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.AdminAuditRealNameReq
if err := httpx.Parse(r, &req); err != nil {
httpx.ErrorCtx(r.Context(), w, err)
result.ParamErrorResult(r, w, err)
return
}
if err := validator.Validate(req); err != nil {
result.ParamValidateErrorResult(r, w, err)
return
}
l := admin_agent.NewAdminAuditRealNameLogic(r.Context(), svcCtx)
resp, err := l.AdminAuditRealName(&req)
if err != nil {
httpx.ErrorCtx(r.Context(), w, err)
} else {
httpx.OkJsonCtx(r.Context(), w, resp)
}
result.HttpResult(r, w, resp, err)
}
}

View File

@@ -3,26 +3,28 @@ package admin_agent
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"ycc-server/app/main/api/internal/logic/admin_agent"
"ycc-server/app/main/api/internal/svc"
"ycc-server/app/main/api/internal/types"
"ycc-server/common/result"
"ycc-server/pkg/lzkit/validator"
"github.com/zeromicro/go-zero/rest/httpx"
)
func AdminAuditWithdrawalHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.AdminAuditWithdrawalReq
if err := httpx.Parse(r, &req); err != nil {
httpx.ErrorCtx(r.Context(), w, err)
result.ParamErrorResult(r, w, err)
return
}
if err := validator.Validate(req); err != nil {
result.ParamValidateErrorResult(r, w, err)
return
}
l := admin_agent.NewAdminAuditWithdrawalLogic(r.Context(), svcCtx)
resp, err := l.AdminAuditWithdrawal(&req)
if err != nil {
httpx.ErrorCtx(r.Context(), w, err)
} else {
httpx.OkJsonCtx(r.Context(), w, resp)
}
result.HttpResult(r, w, resp, err)
}
}

View File

@@ -3,19 +3,15 @@ package admin_agent
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"ycc-server/app/main/api/internal/logic/admin_agent"
"ycc-server/app/main/api/internal/svc"
"ycc-server/common/result"
)
func AdminGetAgentConfigHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
l := admin_agent.NewAdminGetAgentConfigLogic(r.Context(), svcCtx)
resp, err := l.AdminGetAgentConfig()
if err != nil {
httpx.ErrorCtx(r.Context(), w, err)
} else {
httpx.OkJsonCtx(r.Context(), w, resp)
}
result.HttpResult(r, w, resp, err)
}
}

View File

@@ -3,26 +3,28 @@ package admin_agent
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"ycc-server/app/main/api/internal/logic/admin_agent"
"ycc-server/app/main/api/internal/svc"
"ycc-server/app/main/api/internal/types"
"ycc-server/common/result"
"ycc-server/pkg/lzkit/validator"
"github.com/zeromicro/go-zero/rest/httpx"
)
func AdminGetAgentOrderListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.AdminGetAgentOrderListReq
if err := httpx.Parse(r, &req); err != nil {
httpx.ErrorCtx(r.Context(), w, err)
result.ParamErrorResult(r, w, err)
return
}
if err := validator.Validate(req); err != nil {
result.ParamValidateErrorResult(r, w, err)
return
}
l := admin_agent.NewAdminGetAgentOrderListLogic(r.Context(), svcCtx)
resp, err := l.AdminGetAgentOrderList(&req)
if err != nil {
httpx.ErrorCtx(r.Context(), w, err)
} else {
httpx.OkJsonCtx(r.Context(), w, resp)
}
result.HttpResult(r, w, resp, err)
}
}

View File

@@ -3,26 +3,28 @@ package admin_agent
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"ycc-server/app/main/api/internal/logic/admin_agent"
"ycc-server/app/main/api/internal/svc"
"ycc-server/app/main/api/internal/types"
"ycc-server/common/result"
"ycc-server/pkg/lzkit/validator"
"github.com/zeromicro/go-zero/rest/httpx"
)
func AdminGetAgentProductConfigListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.AdminGetAgentProductConfigListReq
if err := httpx.Parse(r, &req); err != nil {
httpx.ErrorCtx(r.Context(), w, err)
result.ParamErrorResult(r, w, err)
return
}
if err := validator.Validate(req); err != nil {
result.ParamValidateErrorResult(r, w, err)
return
}
l := admin_agent.NewAdminGetAgentProductConfigListLogic(r.Context(), svcCtx)
resp, err := l.AdminGetAgentProductConfigList(&req)
if err != nil {
httpx.ErrorCtx(r.Context(), w, err)
} else {
httpx.OkJsonCtx(r.Context(), w, resp)
}
result.HttpResult(r, w, resp, err)
}
}

View File

@@ -3,26 +3,28 @@ package admin_agent
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"ycc-server/app/main/api/internal/logic/admin_agent"
"ycc-server/app/main/api/internal/svc"
"ycc-server/app/main/api/internal/types"
"ycc-server/common/result"
"ycc-server/pkg/lzkit/validator"
"github.com/zeromicro/go-zero/rest/httpx"
)
func AdminGetAgentRealNameListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.AdminGetAgentRealNameListReq
if err := httpx.Parse(r, &req); err != nil {
httpx.ErrorCtx(r.Context(), w, err)
result.ParamErrorResult(r, w, err)
return
}
if err := validator.Validate(req); err != nil {
result.ParamValidateErrorResult(r, w, err)
return
}
l := admin_agent.NewAdminGetAgentRealNameListLogic(r.Context(), svcCtx)
resp, err := l.AdminGetAgentRealNameList(&req)
if err != nil {
httpx.ErrorCtx(r.Context(), w, err)
} else {
httpx.OkJsonCtx(r.Context(), w, resp)
}
result.HttpResult(r, w, resp, err)
}
}

View File

@@ -3,26 +3,28 @@ package admin_agent
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"ycc-server/app/main/api/internal/logic/admin_agent"
"ycc-server/app/main/api/internal/svc"
"ycc-server/app/main/api/internal/types"
"ycc-server/common/result"
"ycc-server/pkg/lzkit/validator"
"github.com/zeromicro/go-zero/rest/httpx"
)
func AdminGetAgentRebateListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.AdminGetAgentRebateListReq
if err := httpx.Parse(r, &req); err != nil {
httpx.ErrorCtx(r.Context(), w, err)
result.ParamErrorResult(r, w, err)
return
}
if err := validator.Validate(req); err != nil {
result.ParamValidateErrorResult(r, w, err)
return
}
l := admin_agent.NewAdminGetAgentRebateListLogic(r.Context(), svcCtx)
resp, err := l.AdminGetAgentRebateList(&req)
if err != nil {
httpx.ErrorCtx(r.Context(), w, err)
} else {
httpx.OkJsonCtx(r.Context(), w, resp)
}
result.HttpResult(r, w, resp, err)
}
}

View File

@@ -3,26 +3,28 @@ package admin_agent
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"ycc-server/app/main/api/internal/logic/admin_agent"
"ycc-server/app/main/api/internal/svc"
"ycc-server/app/main/api/internal/types"
"ycc-server/common/result"
"ycc-server/pkg/lzkit/validator"
"github.com/zeromicro/go-zero/rest/httpx"
)
func AdminGetAgentUpgradeListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.AdminGetAgentUpgradeListReq
if err := httpx.Parse(r, &req); err != nil {
httpx.ErrorCtx(r.Context(), w, err)
result.ParamErrorResult(r, w, err)
return
}
if err := validator.Validate(req); err != nil {
result.ParamValidateErrorResult(r, w, err)
return
}
l := admin_agent.NewAdminGetAgentUpgradeListLogic(r.Context(), svcCtx)
resp, err := l.AdminGetAgentUpgradeList(&req)
if err != nil {
httpx.ErrorCtx(r.Context(), w, err)
} else {
httpx.OkJsonCtx(r.Context(), w, resp)
}
result.HttpResult(r, w, resp, err)
}
}

View File

@@ -3,26 +3,28 @@ package admin_agent
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"ycc-server/app/main/api/internal/logic/admin_agent"
"ycc-server/app/main/api/internal/svc"
"ycc-server/app/main/api/internal/types"
"ycc-server/common/result"
"ycc-server/pkg/lzkit/validator"
"github.com/zeromicro/go-zero/rest/httpx"
)
func AdminUpdateAgentConfigHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.AdminUpdateAgentConfigReq
if err := httpx.Parse(r, &req); err != nil {
httpx.ErrorCtx(r.Context(), w, err)
result.ParamErrorResult(r, w, err)
return
}
if err := validator.Validate(req); err != nil {
result.ParamValidateErrorResult(r, w, err)
return
}
l := admin_agent.NewAdminUpdateAgentConfigLogic(r.Context(), svcCtx)
resp, err := l.AdminUpdateAgentConfig(&req)
if err != nil {
httpx.ErrorCtx(r.Context(), w, err)
} else {
httpx.OkJsonCtx(r.Context(), w, resp)
}
result.HttpResult(r, w, resp, err)
}
}

View File

@@ -3,26 +3,28 @@ package admin_agent
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"ycc-server/app/main/api/internal/logic/admin_agent"
"ycc-server/app/main/api/internal/svc"
"ycc-server/app/main/api/internal/types"
"ycc-server/common/result"
"ycc-server/pkg/lzkit/validator"
"github.com/zeromicro/go-zero/rest/httpx"
)
func AdminUpdateAgentProductConfigHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.AdminUpdateAgentProductConfigReq
if err := httpx.Parse(r, &req); err != nil {
httpx.ErrorCtx(r.Context(), w, err)
result.ParamErrorResult(r, w, err)
return
}
if err := validator.Validate(req); err != nil {
result.ParamValidateErrorResult(r, w, err)
return
}
l := admin_agent.NewAdminUpdateAgentProductConfigLogic(r.Context(), svcCtx)
resp, err := l.AdminUpdateAgentProductConfig(&req)
if err != nil {
httpx.ErrorCtx(r.Context(), w, err)
} else {
httpx.OkJsonCtx(r.Context(), w, resp)
}
result.HttpResult(r, w, resp, err)
}
}

View File

@@ -1,30 +0,0 @@
package admin_promotion
import (
"net/http"
"ycc-server/app/main/api/internal/logic/admin_promotion"
"ycc-server/app/main/api/internal/svc"
"ycc-server/app/main/api/internal/types"
"ycc-server/common/result"
"ycc-server/pkg/lzkit/validator"
"github.com/zeromicro/go-zero/rest/httpx"
)
func DeletePromotionLinkHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.DeletePromotionLinkReq
if err := httpx.Parse(r, &req); err != nil {
result.ParamErrorResult(r, w, err)
return
}
if err := validator.Validate(req); err != nil {
result.ParamValidateErrorResult(r, w, err)
return
}
l := admin_promotion.NewDeletePromotionLinkLogic(r.Context(), svcCtx)
err := l.DeletePromotionLink(&req)
result.HttpResult(r, w, nil, err)
}
}

View File

@@ -1,30 +0,0 @@
package admin_promotion
import (
"net/http"
"ycc-server/app/main/api/internal/logic/admin_promotion"
"ycc-server/app/main/api/internal/svc"
"ycc-server/app/main/api/internal/types"
"ycc-server/common/result"
"ycc-server/pkg/lzkit/validator"
"github.com/zeromicro/go-zero/rest/httpx"
)
func GetPromotionLinkDetailHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.GetPromotionLinkDetailReq
if err := httpx.Parse(r, &req); err != nil {
result.ParamErrorResult(r, w, err)
return
}
if err := validator.Validate(req); err != nil {
result.ParamValidateErrorResult(r, w, err)
return
}
l := admin_promotion.NewGetPromotionLinkDetailLogic(r.Context(), svcCtx)
resp, err := l.GetPromotionLinkDetail(&req)
result.HttpResult(r, w, resp, err)
}
}

View File

@@ -1,30 +0,0 @@
package admin_promotion
import (
"net/http"
"ycc-server/app/main/api/internal/logic/admin_promotion"
"ycc-server/app/main/api/internal/svc"
"ycc-server/app/main/api/internal/types"
"ycc-server/common/result"
"ycc-server/pkg/lzkit/validator"
"github.com/zeromicro/go-zero/rest/httpx"
)
func GetPromotionStatsHistoryHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.GetPromotionStatsHistoryReq
if err := httpx.Parse(r, &req); err != nil {
result.ParamErrorResult(r, w, err)
return
}
if err := validator.Validate(req); err != nil {
result.ParamValidateErrorResult(r, w, err)
return
}
l := admin_promotion.NewGetPromotionStatsHistoryLogic(r.Context(), svcCtx)
resp, err := l.GetPromotionStatsHistory(&req)
result.HttpResult(r, w, resp, err)
}
}

View File

@@ -1,30 +0,0 @@
package admin_promotion
import (
"net/http"
"ycc-server/app/main/api/internal/logic/admin_promotion"
"ycc-server/app/main/api/internal/svc"
"ycc-server/app/main/api/internal/types"
"ycc-server/common/result"
"ycc-server/pkg/lzkit/validator"
"github.com/zeromicro/go-zero/rest/httpx"
)
func UpdatePromotionLinkHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.UpdatePromotionLinkReq
if err := httpx.Parse(r, &req); err != nil {
result.ParamErrorResult(r, w, err)
return
}
if err := validator.Validate(req); err != nil {
result.ParamValidateErrorResult(r, w, err)
return
}
l := admin_promotion.NewUpdatePromotionLinkLogic(r.Context(), svcCtx)
err := l.UpdatePromotionLink(&req)
result.HttpResult(r, w, nil, err)
}
}

View File

@@ -3,26 +3,28 @@ package agent
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"ycc-server/app/main/api/internal/logic/agent"
"ycc-server/app/main/api/internal/svc"
"ycc-server/app/main/api/internal/types"
"ycc-server/common/result"
"ycc-server/pkg/lzkit/validator"
"github.com/zeromicro/go-zero/rest/httpx"
)
func ApplyUpgradeHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.ApplyUpgradeReq
if err := httpx.Parse(r, &req); err != nil {
httpx.ErrorCtx(r.Context(), w, err)
result.ParamErrorResult(r, w, err)
return
}
if err := validator.Validate(req); err != nil {
result.ParamValidateErrorResult(r, w, err)
return
}
l := agent.NewApplyUpgradeLogic(r.Context(), svcCtx)
resp, err := l.ApplyUpgrade(&req)
if err != nil {
httpx.ErrorCtx(r.Context(), w, err)
} else {
httpx.OkJsonCtx(r.Context(), w, resp)
}
result.HttpResult(r, w, resp, err)
}
}

View File

@@ -3,26 +3,28 @@ package agent
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"ycc-server/app/main/api/internal/logic/agent"
"ycc-server/app/main/api/internal/svc"
"ycc-server/app/main/api/internal/types"
"ycc-server/common/result"
"ycc-server/pkg/lzkit/validator"
"github.com/zeromicro/go-zero/rest/httpx"
)
func ApplyWithdrawalHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.ApplyWithdrawalReq
if err := httpx.Parse(r, &req); err != nil {
httpx.ErrorCtx(r.Context(), w, err)
result.ParamErrorResult(r, w, err)
return
}
if err := validator.Validate(req); err != nil {
result.ParamValidateErrorResult(r, w, err)
return
}
l := agent.NewApplyWithdrawalLogic(r.Context(), svcCtx)
resp, err := l.ApplyWithdrawal(&req)
if err != nil {
httpx.ErrorCtx(r.Context(), w, err)
} else {
httpx.OkJsonCtx(r.Context(), w, resp)
}
result.HttpResult(r, w, resp, err)
}
}

View File

@@ -1,20 +1,19 @@
package admin_promotion
package agent
import (
"net/http"
"ycc-server/app/main/api/internal/logic/admin_promotion"
"github.com/zeromicro/go-zero/rest/httpx"
"ycc-server/app/main/api/internal/logic/agent"
"ycc-server/app/main/api/internal/svc"
"ycc-server/app/main/api/internal/types"
"ycc-server/common/result"
"ycc-server/pkg/lzkit/validator"
"github.com/zeromicro/go-zero/rest/httpx"
)
func RecordLinkClickHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
func DeleteInviteCodeHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.RecordLinkClickReq
var req types.DeleteInviteCodeReq
if err := httpx.Parse(r, &req); err != nil {
result.ParamErrorResult(r, w, err)
return
@@ -23,8 +22,8 @@ func RecordLinkClickHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
result.ParamValidateErrorResult(r, w, err)
return
}
l := admin_promotion.NewRecordLinkClickLogic(r.Context(), svcCtx)
resp, err := l.RecordLinkClick(&req)
l := agent.NewDeleteInviteCodeLogic(r.Context(), svcCtx)
resp, err := l.DeleteInviteCode(&req)
result.HttpResult(r, w, resp, err)
}
}

View File

@@ -3,26 +3,28 @@ package agent
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"ycc-server/app/main/api/internal/logic/agent"
"ycc-server/app/main/api/internal/svc"
"ycc-server/app/main/api/internal/types"
"ycc-server/common/result"
"ycc-server/pkg/lzkit/validator"
"github.com/zeromicro/go-zero/rest/httpx"
)
func GetCommissionListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.GetCommissionListReq
if err := httpx.Parse(r, &req); err != nil {
httpx.ErrorCtx(r.Context(), w, err)
result.ParamErrorResult(r, w, err)
return
}
if err := validator.Validate(req); err != nil {
result.ParamValidateErrorResult(r, w, err)
return
}
l := agent.NewGetCommissionListLogic(r.Context(), svcCtx)
resp, err := l.GetCommissionList(&req)
if err != nil {
httpx.ErrorCtx(r.Context(), w, err)
} else {
httpx.OkJsonCtx(r.Context(), w, resp)
}
result.HttpResult(r, w, resp, err)
}
}

View File

@@ -0,0 +1,17 @@
package agent
import (
"net/http"
"ycc-server/app/main/api/internal/logic/agent"
"ycc-server/app/main/api/internal/svc"
"ycc-server/common/result"
)
func GetConversionRateHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
l := agent.NewGetConversionRateLogic(r.Context(), svcCtx)
resp, err := l.GetConversionRate()
result.HttpResult(r, w, resp, err)
}
}

View File

@@ -5,13 +5,26 @@ import (
"ycc-server/app/main/api/internal/logic/agent"
"ycc-server/app/main/api/internal/svc"
"ycc-server/app/main/api/internal/types"
"ycc-server/common/result"
"ycc-server/pkg/lzkit/validator"
"github.com/zeromicro/go-zero/rest/httpx"
)
func GetInviteLinkHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.GetInviteLinkReq
if err := httpx.Parse(r, &req); err != nil {
result.ParamErrorResult(r, w, err)
return
}
if err := validator.Validate(req); err != nil {
result.ParamValidateErrorResult(r, w, err)
return
}
l := agent.NewGetInviteLinkLogic(r.Context(), svcCtx)
resp, err := l.GetInviteLink()
resp, err := l.GetInviteLink(&req)
result.HttpResult(r, w, resp, err)
}
}

View File

@@ -0,0 +1,18 @@
package agent
import (
"net/http"
"ycc-server/app/main/api/internal/logic/agent"
"ycc-server/app/main/api/internal/svc"
"ycc-server/common/result"
)
func GetLevelPrivilegeHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
l := agent.NewGetLevelPrivilegeLogic(r.Context(), svcCtx)
resp, err := l.GetLevelPrivilege()
result.HttpResult(r, w, resp, err)
}
}

View File

@@ -3,26 +3,28 @@ package agent
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"ycc-server/app/main/api/internal/logic/agent"
"ycc-server/app/main/api/internal/svc"
"ycc-server/app/main/api/internal/types"
"ycc-server/common/result"
"ycc-server/pkg/lzkit/validator"
"github.com/zeromicro/go-zero/rest/httpx"
)
func GetRebateListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.GetRebateListReq
if err := httpx.Parse(r, &req); err != nil {
httpx.ErrorCtx(r.Context(), w, err)
result.ParamErrorResult(r, w, err)
return
}
if err := validator.Validate(req); err != nil {
result.ParamValidateErrorResult(r, w, err)
return
}
l := agent.NewGetRebateListLogic(r.Context(), svcCtx)
resp, err := l.GetRebateList(&req)
if err != nil {
httpx.ErrorCtx(r.Context(), w, err)
} else {
httpx.OkJsonCtx(r.Context(), w, resp)
}
result.HttpResult(r, w, resp, err)
}
}

View File

@@ -3,19 +3,15 @@ package agent
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"ycc-server/app/main/api/internal/logic/agent"
"ycc-server/app/main/api/internal/svc"
"ycc-server/common/result"
)
func GetRevenueInfoHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
l := agent.NewGetRevenueInfoLogic(r.Context(), svcCtx)
resp, err := l.GetRevenueInfo()
if err != nil {
httpx.ErrorCtx(r.Context(), w, err)
} else {
httpx.OkJsonCtx(r.Context(), w, resp)
}
result.HttpResult(r, w, resp, err)
}
}

View File

@@ -1,20 +1,19 @@
package admin_promotion
package agent
import (
"net/http"
"ycc-server/app/main/api/internal/logic/admin_promotion"
"github.com/zeromicro/go-zero/rest/httpx"
"ycc-server/app/main/api/internal/logic/agent"
"ycc-server/app/main/api/internal/svc"
"ycc-server/app/main/api/internal/types"
"ycc-server/common/result"
"ycc-server/pkg/lzkit/validator"
"github.com/zeromicro/go-zero/rest/httpx"
)
func GetPromotionStatsTotalHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
func GetSubordinateContributionDetailHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.GetPromotionStatsTotalReq
var req types.GetSubordinateContributionDetailReq
if err := httpx.Parse(r, &req); err != nil {
result.ParamErrorResult(r, w, err)
return
@@ -23,8 +22,8 @@ func GetPromotionStatsTotalHandler(svcCtx *svc.ServiceContext) http.HandlerFunc
result.ParamValidateErrorResult(r, w, err)
return
}
l := admin_promotion.NewGetPromotionStatsTotalLogic(r.Context(), svcCtx)
resp, err := l.GetPromotionStatsTotal(&req)
l := agent.NewGetSubordinateContributionDetailLogic(r.Context(), svcCtx)
resp, err := l.GetSubordinateContributionDetail(&req)
result.HttpResult(r, w, resp, err)
}
}

View File

@@ -3,26 +3,28 @@ package agent
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"ycc-server/app/main/api/internal/logic/agent"
"ycc-server/app/main/api/internal/svc"
"ycc-server/app/main/api/internal/types"
"ycc-server/common/result"
"ycc-server/pkg/lzkit/validator"
"github.com/zeromicro/go-zero/rest/httpx"
)
func GetSubordinateListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.GetSubordinateListReq
if err := httpx.Parse(r, &req); err != nil {
httpx.ErrorCtx(r.Context(), w, err)
result.ParamErrorResult(r, w, err)
return
}
if err := validator.Validate(req); err != nil {
result.ParamValidateErrorResult(r, w, err)
return
}
l := agent.NewGetSubordinateListLogic(r.Context(), svcCtx)
resp, err := l.GetSubordinateList(&req)
if err != nil {
httpx.ErrorCtx(r.Context(), w, err)
} else {
httpx.OkJsonCtx(r.Context(), w, resp)
}
result.HttpResult(r, w, resp, err)
}
}

View File

@@ -1,20 +1,19 @@
package admin_promotion
package agent
import (
"net/http"
"ycc-server/app/main/api/internal/logic/admin_promotion"
"github.com/zeromicro/go-zero/rest/httpx"
"ycc-server/app/main/api/internal/logic/agent"
"ycc-server/app/main/api/internal/svc"
"ycc-server/app/main/api/internal/types"
"ycc-server/common/result"
"ycc-server/pkg/lzkit/validator"
"github.com/zeromicro/go-zero/rest/httpx"
)
func CreatePromotionLinkHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
func GetTeamListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.CreatePromotionLinkReq
var req types.GetTeamListReq
if err := httpx.Parse(r, &req); err != nil {
result.ParamErrorResult(r, w, err)
return
@@ -23,8 +22,8 @@ func CreatePromotionLinkHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
result.ParamValidateErrorResult(r, w, err)
return
}
l := admin_promotion.NewCreatePromotionLinkLogic(r.Context(), svcCtx)
resp, err := l.CreatePromotionLink(&req)
l := agent.NewGetTeamListLogic(r.Context(), svcCtx)
resp, err := l.GetTeamList(&req)
result.HttpResult(r, w, resp, err)
}
}

View File

@@ -3,19 +3,15 @@ package agent
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"ycc-server/app/main/api/internal/logic/agent"
"ycc-server/app/main/api/internal/svc"
"ycc-server/common/result"
)
func GetTeamStatisticsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
l := agent.NewGetTeamStatisticsLogic(r.Context(), svcCtx)
resp, err := l.GetTeamStatistics()
if err != nil {
httpx.ErrorCtx(r.Context(), w, err)
} else {
httpx.OkJsonCtx(r.Context(), w, resp)
}
result.HttpResult(r, w, resp, err)
}
}

View File

@@ -3,26 +3,28 @@ package agent
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"ycc-server/app/main/api/internal/logic/agent"
"ycc-server/app/main/api/internal/svc"
"ycc-server/app/main/api/internal/types"
"ycc-server/common/result"
"ycc-server/pkg/lzkit/validator"
"github.com/zeromicro/go-zero/rest/httpx"
)
func GetUpgradeListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.GetUpgradeListReq
if err := httpx.Parse(r, &req); err != nil {
httpx.ErrorCtx(r.Context(), w, err)
result.ParamErrorResult(r, w, err)
return
}
if err := validator.Validate(req); err != nil {
result.ParamValidateErrorResult(r, w, err)
return
}
l := agent.NewGetUpgradeListLogic(r.Context(), svcCtx)
resp, err := l.GetUpgradeList(&req)
if err != nil {
httpx.ErrorCtx(r.Context(), w, err)
} else {
httpx.OkJsonCtx(r.Context(), w, resp)
}
result.HttpResult(r, w, resp, err)
}
}

View File

@@ -1,20 +1,19 @@
package admin_promotion
package agent
import (
"net/http"
"ycc-server/app/main/api/internal/logic/admin_promotion"
"github.com/zeromicro/go-zero/rest/httpx"
"ycc-server/app/main/api/internal/logic/agent"
"ycc-server/app/main/api/internal/svc"
"ycc-server/app/main/api/internal/types"
"ycc-server/common/result"
"ycc-server/pkg/lzkit/validator"
"github.com/zeromicro/go-zero/rest/httpx"
)
func GetPromotionLinkListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
func GetUpgradeRebateListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.GetPromotionLinkListReq
var req types.GetUpgradeRebateListReq
if err := httpx.Parse(r, &req); err != nil {
result.ParamErrorResult(r, w, err)
return
@@ -23,8 +22,8 @@ func GetPromotionLinkListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
result.ParamValidateErrorResult(r, w, err)
return
}
l := admin_promotion.NewGetPromotionLinkListLogic(r.Context(), svcCtx)
resp, err := l.GetPromotionLinkList(&req)
l := agent.NewGetUpgradeRebateListLogic(r.Context(), svcCtx)
resp, err := l.GetUpgradeRebateList(&req)
result.HttpResult(r, w, resp, err)
}
}

View File

@@ -3,26 +3,28 @@ package agent
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"ycc-server/app/main/api/internal/logic/agent"
"ycc-server/app/main/api/internal/svc"
"ycc-server/app/main/api/internal/types"
"ycc-server/common/result"
"ycc-server/pkg/lzkit/validator"
"github.com/zeromicro/go-zero/rest/httpx"
)
func GetWithdrawalListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.GetWithdrawalListReq
if err := httpx.Parse(r, &req); err != nil {
httpx.ErrorCtx(r.Context(), w, err)
result.ParamErrorResult(r, w, err)
return
}
if err := validator.Validate(req); err != nil {
result.ParamValidateErrorResult(r, w, err)
return
}
l := agent.NewGetWithdrawalListLogic(r.Context(), svcCtx)
resp, err := l.GetWithdrawalList(&req)
if err != nil {
httpx.ErrorCtx(r.Context(), w, err)
} else {
httpx.OkJsonCtx(r.Context(), w, resp)
}
result.HttpResult(r, w, resp, err)
}
}

View File

@@ -0,0 +1,21 @@
package agent
import (
"net/http"
"ycc-server/app/main/api/internal/logic/agent"
"ycc-server/app/main/api/internal/svc"
"github.com/zeromicro/go-zero/rest/httpx"
)
func PromotionRedirectHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
l := agent.NewPromotionRedirectLogic(r.Context(), svcCtx)
err := l.PromotionRedirect(r, w)
if err != nil {
httpx.ErrorCtx(r.Context(), w, err)
}
}
}

View File

@@ -3,26 +3,28 @@ package agent
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"ycc-server/app/main/api/internal/logic/agent"
"ycc-server/app/main/api/internal/svc"
"ycc-server/app/main/api/internal/types"
"ycc-server/common/result"
"ycc-server/pkg/lzkit/validator"
"github.com/zeromicro/go-zero/rest/httpx"
)
func RealNameAuthHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.RealNameAuthReq
if err := httpx.Parse(r, &req); err != nil {
httpx.ErrorCtx(r.Context(), w, err)
result.ParamErrorResult(r, w, err)
return
}
if err := validator.Validate(req); err != nil {
result.ParamValidateErrorResult(r, w, err)
return
}
l := agent.NewRealNameAuthLogic(r.Context(), svcCtx)
resp, err := l.RealNameAuth(&req)
if err != nil {
httpx.ErrorCtx(r.Context(), w, err)
} else {
httpx.OkJsonCtx(r.Context(), w, resp)
}
result.HttpResult(r, w, resp, err)
}
}

View File

@@ -0,0 +1,29 @@
package agent
import (
"net/http"
"ycc-server/app/main/api/internal/logic/agent"
"ycc-server/app/main/api/internal/svc"
"github.com/zeromicro/go-zero/rest/httpx"
)
func ShortLinkRedirectHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// 从URL.Path解析shortCode
// 路径格式:/s/{shortCode}
path := r.URL.Path
var shortCode string
if len(path) >= 3 && path[:3] == "/s/" {
shortCode = path[3:]
}
l := agent.NewShortLinkRedirectLogic(r.Context(), svcCtx)
err := l.ShortLinkRedirect(shortCode, r, w)
if err != nil {
httpx.ErrorCtx(r.Context(), w, err)
}
}
}

View File

@@ -3,26 +3,28 @@ package agent
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"ycc-server/app/main/api/internal/logic/agent"
"ycc-server/app/main/api/internal/svc"
"ycc-server/app/main/api/internal/types"
"ycc-server/common/result"
"ycc-server/pkg/lzkit/validator"
"github.com/zeromicro/go-zero/rest/httpx"
)
func UpgradeSubordinateHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.UpgradeSubordinateReq
if err := httpx.Parse(r, &req); err != nil {
httpx.ErrorCtx(r.Context(), w, err)
result.ParamErrorResult(r, w, err)
return
}
if err := validator.Validate(req); err != nil {
result.ParamValidateErrorResult(r, w, err)
return
}
l := agent.NewUpgradeSubordinateLogic(r.Context(), svcCtx)
resp, err := l.UpgradeSubordinate(&req)
if err != nil {
httpx.ErrorCtx(r.Context(), w, err)
} else {
httpx.OkJsonCtx(r.Context(), w, resp)
}
result.HttpResult(r, w, resp, err)
}
}

View File

@@ -13,7 +13,6 @@ import (
admin_order "ycc-server/app/main/api/internal/handler/admin_order"
admin_platform_user "ycc-server/app/main/api/internal/handler/admin_platform_user"
admin_product "ycc-server/app/main/api/internal/handler/admin_product"
admin_promotion "ycc-server/app/main/api/internal/handler/admin_promotion"
admin_query "ycc-server/app/main/api/internal/handler/admin_query"
admin_role "ycc-server/app/main/api/internal/handler/admin_role"
admin_role_api "ycc-server/app/main/api/internal/handler/admin_role_api"
@@ -425,81 +424,6 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
rest.WithPrefix("/api/v1/admin/product"),
)
server.AddRoutes(
rest.WithMiddlewares(
[]rest.Middleware{serverCtx.AdminAuthInterceptor},
[]rest.Route{
{
// 创建推广链接
Method: http.MethodPost,
Path: "/create",
Handler: admin_promotion.CreatePromotionLinkHandler(serverCtx),
},
{
// 删除推广链接
Method: http.MethodDelete,
Path: "/delete/:id",
Handler: admin_promotion.DeletePromotionLinkHandler(serverCtx),
},
{
// 获取推广链接详情
Method: http.MethodGet,
Path: "/detail/:id",
Handler: admin_promotion.GetPromotionLinkDetailHandler(serverCtx),
},
{
// 获取推广链接列表
Method: http.MethodGet,
Path: "/list",
Handler: admin_promotion.GetPromotionLinkListHandler(serverCtx),
},
{
// 更新推广链接
Method: http.MethodPut,
Path: "/update/:id",
Handler: admin_promotion.UpdatePromotionLinkHandler(serverCtx),
},
}...,
),
rest.WithPrefix("/api/v1/admin/promotion/link"),
)
server.AddRoutes(
rest.WithMiddlewares(
[]rest.Middleware{serverCtx.AdminAuthInterceptor},
[]rest.Route{
{
// 记录链接点击
Method: http.MethodGet,
Path: "/record/:path",
Handler: admin_promotion.RecordLinkClickHandler(serverCtx),
},
}...,
),
rest.WithPrefix("/api/v1/admin/promotion/link"),
)
server.AddRoutes(
rest.WithMiddlewares(
[]rest.Middleware{serverCtx.AdminAuthInterceptor},
[]rest.Route{
{
// 获取推广历史记录
Method: http.MethodGet,
Path: "/history",
Handler: admin_promotion.GetPromotionStatsHistoryHandler(serverCtx),
},
{
// 获取推广总统计
Method: http.MethodGet,
Path: "/total",
Handler: admin_promotion.GetPromotionStatsTotalHandler(serverCtx),
},
}...,
),
rest.WithPrefix("/api/v1/admin/promotion/stats"),
)
server.AddRoutes(
rest.WithMiddlewares(
[]rest.Middleware{serverCtx.AdminAuthInterceptor},
@@ -693,6 +617,11 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
Path: "/commission/list",
Handler: agent.GetCommissionListHandler(serverCtx),
},
{
Method: http.MethodGet,
Path: "/conversion/rate",
Handler: agent.GetConversionRateHandler(serverCtx),
},
{
Method: http.MethodPost,
Path: "/generating_link",
@@ -703,6 +632,11 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
Path: "/info",
Handler: agent.GetAgentInfoHandler(serverCtx),
},
{
Method: http.MethodPost,
Path: "/invite_code/delete",
Handler: agent.DeleteInviteCodeHandler(serverCtx),
},
{
Method: http.MethodPost,
Path: "/invite_code/generate",
@@ -718,6 +652,11 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
Path: "/invite_link",
Handler: agent.GetInviteLinkHandler(serverCtx),
},
{
Method: http.MethodGet,
Path: "/level/privilege",
Handler: agent.GetLevelPrivilegeHandler(serverCtx),
},
{
Method: http.MethodGet,
Path: "/product_config",
@@ -733,16 +672,31 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
Path: "/rebate/list",
Handler: agent.GetRebateListHandler(serverCtx),
},
{
Method: http.MethodGet,
Path: "/rebate/upgrade/list",
Handler: agent.GetUpgradeRebateListHandler(serverCtx),
},
{
Method: http.MethodGet,
Path: "/revenue",
Handler: agent.GetRevenueInfoHandler(serverCtx),
},
{
Method: http.MethodGet,
Path: "/subordinate/contribution/detail",
Handler: agent.GetSubordinateContributionDetailHandler(serverCtx),
},
{
Method: http.MethodGet,
Path: "/subordinate/list",
Handler: agent.GetSubordinateListHandler(serverCtx),
},
{
Method: http.MethodGet,
Path: "/team/list",
Handler: agent.GetTeamListHandler(serverCtx),
},
{
Method: http.MethodGet,
Path: "/team/statistics",
@@ -779,6 +733,16 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
rest.WithPrefix("/api/v1/agent"),
)
server.AddRoutes(
[]rest.Route{
{
Method: http.MethodGet,
Path: "/s/:shortCode",
Handler: agent.ShortLinkRedirectHandler(serverCtx),
},
},
)
server.AddRoutes(
[]rest.Route{
{

View File

@@ -34,6 +34,14 @@ func (l *AdminGetAgentConfigLogic) AdminGetAgentConfig() (resp *types.AdminGetAg
value, _ := strconv.ParseFloat(config.ConfigValue, 64)
return value
}
getConfigInt := func(key string) int64 {
config, err := l.svcCtx.AgentConfigModel.FindOneByConfigKey(l.ctx, key)
if err != nil {
return 0
}
value, _ := strconv.ParseInt(config.ConfigValue, 10, 64)
return value
}
// 获取等级加成配置
level1Bonus := getConfigFloat("level_1_bonus")
@@ -48,11 +56,20 @@ func (l *AdminGetAgentConfigLogic) AdminGetAgentConfig() (resp *types.AdminGetAg
upgradeToGoldRebate := getConfigFloat("upgrade_to_gold_rebate")
upgradeToDiamondRebate := getConfigFloat("upgrade_to_diamond_rebate")
// 获取直接上级返佣配置
directParentAmountDiamond := getConfigFloat("direct_parent_amount_diamond")
directParentAmountGold := getConfigFloat("direct_parent_amount_gold")
directParentAmountNormal := getConfigFloat("direct_parent_amount_normal")
// 获取黄金代理最大返佣金额
maxGoldRebateAmount := getConfigFloat("max_gold_rebate_amount")
// 获取佣金冻结配置
commissionFreezeRatio := getConfigFloat("commission_freeze_ratio")
commissionFreezeThreshold := getConfigFloat("commission_freeze_threshold")
commissionFreezeDays := getConfigInt("commission_freeze_days")
return &types.AdminGetAgentConfigResp{
BasePrice: getConfigFloat("base_price"),
SystemMaxPrice: getConfigFloat("system_max_price"),
PriceThreshold: getConfigFloat("price_threshold"),
PriceFeeRate: getConfigFloat("price_fee_rate"),
LevelBonus: types.LevelBonusConfig{
Normal: int64(level1Bonus),
Gold: int64(level2Bonus),
@@ -67,6 +84,17 @@ func (l *AdminGetAgentConfigLogic) AdminGetAgentConfig() (resp *types.AdminGetAg
NormalToGoldRebate: upgradeToGoldRebate,
ToDiamondRebate: upgradeToDiamondRebate,
},
DirectParentRebate: types.DirectParentRebateConfig{
Diamond: directParentAmountDiamond,
Gold: directParentAmountGold,
Normal: directParentAmountNormal,
},
MaxGoldRebateAmount: maxGoldRebateAmount,
CommissionFreeze: types.CommissionFreezeConfig{
Ratio: commissionFreezeRatio,
Threshold: commissionFreezeThreshold,
Days: commissionFreezeDays,
},
TaxRate: getConfigFloat("tax_rate"),
TaxExemptionAmount: getConfigFloat("tax_exemption_amount"),
}, nil

View File

@@ -31,10 +31,16 @@ func (l *AdminGetAgentProductConfigListLogic) AdminGetAgentProductConfigList(req
builder := l.svcCtx.AgentProductConfigModel.SelectBuilder().
Where("del_state = ?", globalkey.DelStateNo)
// 如果提供了产品ID直接过滤
if req.ProductId != nil {
builder = builder.Where("product_id = ?", *req.ProductId)
}
// 如果提供了产品名称,通过关联查询 product 表过滤
if req.ProductName != nil && *req.ProductName != "" {
builder = builder.Where("product_id IN (SELECT id FROM product WHERE product_name LIKE ? AND del_state = ?)", "%"+*req.ProductName+"%", globalkey.DelStateNo)
}
// 分页查询
page := req.Page
if page <= 0 {
@@ -50,9 +56,19 @@ func (l *AdminGetAgentProductConfigListLogic) AdminGetAgentProductConfigList(req
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询产品配置列表失败, %v", err)
}
// 组装响应
// 组装响应(通过 product_id 查询产品名称)
items := make([]types.AgentProductConfigItem, 0, len(configs))
for _, config := range configs {
// 通过 product_id 查询产品信息获取产品名称
product, err := l.svcCtx.ProductModel.FindOne(l.ctx, config.ProductId)
productName := ""
if err == nil {
productName = product.ProductName
} else {
// 如果产品不存在,记录日志但不影响主流程
l.Infof("查询产品信息失败, productId: %d, err: %v", config.ProductId, err)
}
priceThreshold := 0.0
if config.PriceThreshold.Valid {
priceThreshold = config.PriceThreshold.Float64
@@ -66,7 +82,7 @@ func (l *AdminGetAgentProductConfigListLogic) AdminGetAgentProductConfigList(req
items = append(items, types.AgentProductConfigItem{
Id: config.Id,
ProductId: config.ProductId,
ProductName: config.ProductName,
ProductName: productName,
BasePrice: config.BasePrice,
PriceRangeMin: config.BasePrice, // 最低定价等于基础底价
PriceRangeMax: config.SystemMaxPrice,

View File

@@ -41,19 +41,76 @@ func (l *AdminUpdateAgentConfigLogic) AdminUpdateAgentConfig(req *types.AdminUpd
return l.svcCtx.AgentConfigModel.UpdateWithVersion(l.ctx, nil, config)
}
// 更新各个配置
if err := updateConfig("base_price", req.BasePrice); err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新基础底价失败, %v", err)
// 更新等级加成配置
if req.LevelBonus != nil {
if err := updateConfig("level_1_bonus", func() *float64 { v := float64(req.LevelBonus.Normal); return &v }()); err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新普通代理等级加成失败, %v", err)
}
if err := updateConfig("level_2_bonus", func() *float64 { v := float64(req.LevelBonus.Gold); return &v }()); err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新黄金代理等级加成失败, %v", err)
}
if err := updateConfig("level_3_bonus", func() *float64 { v := float64(req.LevelBonus.Diamond); return &v }()); err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新钻石代理等级加成失败, %v", err)
}
}
if err := updateConfig("system_max_price", req.SystemMaxPrice); err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新系统价格上限失败, %v", err)
// 更新升级费用配置
if req.UpgradeFee != nil {
if err := updateConfig("upgrade_to_gold_fee", &req.UpgradeFee.NormalToGold); err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新普通→黄金升级费用失败, %v", err)
}
if err := updateConfig("upgrade_to_diamond_fee", &req.UpgradeFee.NormalToDiamond); err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新普通→钻石升级费用失败, %v", err)
}
// gold_to_diamond 是计算得出的,不需要更新
}
if err := updateConfig("price_threshold", req.PriceThreshold); err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新提价标准阈值失败, %v", err)
// 更新升级返佣配置
if req.UpgradeRebate != nil {
if err := updateConfig("upgrade_to_gold_rebate", &req.UpgradeRebate.NormalToGoldRebate); err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新普通→黄金返佣失败, %v", err)
}
if err := updateConfig("upgrade_to_diamond_rebate", &req.UpgradeRebate.ToDiamondRebate); err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新升级为钻石返佣失败, %v", err)
}
}
if err := updateConfig("price_fee_rate", req.PriceFeeRate); err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新提价手续费比例失败, %v", err)
// 更新直接上级返佣配置
if req.DirectParentRebate != nil {
if err := updateConfig("direct_parent_amount_diamond", &req.DirectParentRebate.Diamond); err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新直接上级是钻石的返佣金额失败, %v", err)
}
if err := updateConfig("direct_parent_amount_gold", &req.DirectParentRebate.Gold); err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新直接上级是黄金的返佣金额失败, %v", err)
}
if err := updateConfig("direct_parent_amount_normal", &req.DirectParentRebate.Normal); err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新直接上级是普通的返佣金额失败, %v", err)
}
}
// 更新黄金代理最大返佣金额
if err := updateConfig("max_gold_rebate_amount", req.MaxGoldRebateAmount); err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新黄金代理最大返佣金额失败, %v", err)
}
// 更新佣金冻结配置
if req.CommissionFreeze != nil {
if err := updateConfig("commission_freeze_ratio", &req.CommissionFreeze.Ratio); err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新佣金冻结比例失败, %v", err)
}
if err := updateConfig("commission_freeze_threshold", &req.CommissionFreeze.Threshold); err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新佣金冻结阈值失败, %v", err)
}
// 更新解冻天数(整数类型)
if req.CommissionFreeze.Days > 0 {
daysFloat := float64(req.CommissionFreeze.Days)
if err := updateConfig("commission_freeze_days", &daysFloat); err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新佣金冻结解冻天数失败, %v", err)
}
}
}
// 更新税费配置
if err := updateConfig("tax_rate", req.TaxRate); err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新税率失败, %v", err)
}

View File

@@ -37,8 +37,20 @@ func (l *AdminUpdateAgentProductConfigLogic) AdminUpdateAgentProductConfig(req *
// 更新配置字段
config.BasePrice = req.BasePrice
config.SystemMaxPrice = req.PriceRangeMax
config.PriceThreshold = sql.NullFloat64{Float64: req.PriceThreshold, Valid: true}
config.PriceFeeRate = sql.NullFloat64{Float64: req.PriceFeeRate, Valid: true}
// 价格阈值(可选)
if req.PriceThreshold != nil {
config.PriceThreshold = sql.NullFloat64{Float64: *req.PriceThreshold, Valid: true}
} else {
config.PriceThreshold = sql.NullFloat64{Valid: false}
}
// 提价手续费比例(可选)
if req.PriceFeeRate != nil {
config.PriceFeeRate = sql.NullFloat64{Float64: *req.PriceFeeRate, Valid: true}
} else {
config.PriceFeeRate = sql.NullFloat64{Valid: false}
}
// 更新配置
if err := l.svcCtx.AgentProductConfigModel.UpdateWithVersion(l.ctx, nil, config); err != nil {

View File

@@ -2,6 +2,7 @@ package admin_auth
import (
"context"
"os"
"ycc-server/app/main/api/internal/svc"
"ycc-server/app/main/api/internal/types"
@@ -30,9 +31,11 @@ func NewAdminLoginLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AdminL
}
func (l *AdminLoginLogic) AdminLogin(req *types.AdminLoginReq) (resp *types.AdminLoginResp, err error) {
// 1. 验证验证码
if !req.Captcha {
return nil, errors.Wrapf(xerr.NewErrMsg("验证码错误"), "用户登录, 验证码错误, 验证码: %v", req.Captcha)
// 1. 验证验证码(开发环境下跳过验证码校验)
if os.Getenv("ENV") != "development" {
if !req.Captcha {
return nil, errors.Wrapf(xerr.NewErrMsg("验证码错误"), "用户登录, 验证码错误, 验证码: %v", req.Captcha)
}
}
// 2. 验证用户名和密码

View File

@@ -72,20 +72,6 @@ func (l *AdminCreateOrderLogic) AdminCreateOrder(req *types.AdminCreateOrderReq)
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "AdminCreateOrder, 获取订单ID失败 err: %v", err)
}
// 如果是推广订单,创建推广订单记录
if req.IsPromotion == 1 {
promotionOrder := &model.AdminPromotionOrder{
OrderId: orderId,
Version: 1,
CreateTime: time.Now(),
UpdateTime: time.Now(),
}
_, err = l.svcCtx.AdminPromotionOrderModel.Insert(ctx, session, promotionOrder)
if err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "AdminCreateOrder, 创建推广订单失败 err: %v", err)
}
}
return nil
})

View File

@@ -41,15 +41,6 @@ func (l *AdminDeleteOrderLogic) AdminDeleteOrder(req *types.AdminDeleteOrderReq)
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "AdminDeleteOrder, 删除订单失败 err: %v", err)
}
// 删除关联的推广订单记录
promotionOrder, err := l.svcCtx.AdminPromotionOrderModel.FindOneByOrderId(ctx, order.Id)
if err == nil && promotionOrder != nil {
err = l.svcCtx.AdminPromotionOrderModel.DeleteSoft(ctx, session, promotionOrder)
if err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "AdminDeleteOrder, 删除推广订单失败 err: %v", err)
}
}
return nil
})

View File

@@ -40,13 +40,6 @@ func (l *AdminGetOrderDetailLogic) AdminGetOrderDetail(req *types.AdminGetOrderD
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "AdminGetOrderDetail, 查询产品失败 err: %v", err)
}
// 判断是否为推广订单
var isPromotion int64
promotionOrder, err := l.svcCtx.AdminPromotionOrderModel.FindOneByOrderId(l.ctx, order.Id)
if err == nil && promotionOrder != nil {
isPromotion = 1
}
// 判断是否为代理订单并获取代理处理状态
var isAgentOrder bool
var agentProcessStatus string
@@ -118,7 +111,6 @@ func (l *AdminGetOrderDetailLogic) AdminGetOrderDetail(req *types.AdminGetOrderD
Status: order.Status,
CreateTime: order.CreateTime.Format("2006-01-02 15:04:05"),
UpdateTime: order.UpdateTime.Format("2006-01-02 15:04:05"),
IsPromotion: isPromotion,
QueryState: queryState,
IsAgentOrder: isAgentOrder,
AgentProcessStatus: agentProcessStatus,

View File

@@ -54,9 +54,6 @@ func (l *AdminGetOrderListLogic) AdminGetOrderList(req *types.AdminGetOrderListR
if req.Status != "" {
builder = builder.Where("status = ?", req.Status)
}
if req.IsPromotion != -1 {
builder = builder.Where("id IN (SELECT order_id FROM admin_promotion_order WHERE del_state = 0)")
}
// 时间范围查询
if req.CreateTimeStart != "" {
builder = builder.Where("create_time >= ?", req.CreateTimeStart)
@@ -274,11 +271,6 @@ func (l *AdminGetOrderListLogic) AdminGetOrderList(req *types.AdminGetOrderListR
if order.RefundTime.Valid {
item.RefundTime = order.RefundTime.Time.Format("2006-01-02 15:04:05")
}
// 判断是否为推广订单
promotionOrder, err := l.svcCtx.AdminPromotionOrderModel.FindOneByOrderId(l.ctx, order.Id)
if err == nil && promotionOrder != nil {
item.IsPromotion = 1
}
// 设置代理订单相关字段
if agentOrderMap[order.Id] {

View File

@@ -7,7 +7,6 @@ import (
"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/pkg/errors"
@@ -76,30 +75,6 @@ func (l *AdminUpdateOrderLogic) AdminUpdateOrder(req *types.AdminUpdateOrderReq)
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "AdminUpdateOrder, 更新订单失败 err: %v", err)
}
// 处理推广订单状态
if req.IsPromotion != nil {
promotionOrder, err := l.svcCtx.AdminPromotionOrderModel.FindOneByOrderId(ctx, order.Id)
if err == nil && promotionOrder != nil {
// 如果存在推广订单记录但不需要推广,则删除
if *req.IsPromotion == 0 {
err = l.svcCtx.AdminPromotionOrderModel.DeleteSoft(ctx, session, promotionOrder)
if err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "AdminUpdateOrder, 删除推广订单失败 err: %v", err)
}
}
} else if *req.IsPromotion == 1 {
// 如果需要推广但不存在记录,则创建
newPromotionOrder := &model.AdminPromotionOrder{
OrderId: order.Id,
Version: 1,
}
_, err = l.svcCtx.AdminPromotionOrderModel.Insert(ctx, session, newPromotionOrder)
if err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "AdminUpdateOrder, 创建推广订单失败 err: %v", err)
}
}
}
return nil
})

View File

@@ -2,14 +2,15 @@ package admin_product
import (
"context"
"database/sql"
"ycc-server/app/main/api/internal/svc"
"ycc-server/app/main/api/internal/types"
"ycc-server/app/main/model"
"ycc-server/common/xerr"
"database/sql"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/core/stores/sqlx"
)
type AdminCreateProductLogic struct {
@@ -37,14 +38,44 @@ func (l *AdminCreateProductLogic) AdminCreateProduct(req *types.AdminCreateProdu
SellPrice: req.SellPrice,
}
// 2. 数据库操作
result, err := l.svcCtx.ProductModel.Insert(l.ctx, nil, data)
// 2. 数据库操作(使用事务确保产品表和代理产品配置表同步)
var productId int64
err = l.svcCtx.ProductModel.Trans(l.ctx, func(ctx context.Context, session sqlx.Session) error {
// 2.1 插入产品
result, err := l.svcCtx.ProductModel.Insert(ctx, session, data)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR),
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR),
"创建产品失败, err: %v, req: %+v", err, req)
}
productId, err = result.LastInsertId()
if err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR),
"获取产品ID失败, err: %v", err)
}
// 2.2 同步创建代理产品配置(使用默认值)
agentProductConfig := &model.AgentProductConfig{
ProductId: productId,
BasePrice: 0.00, // 默认基础底价
SystemMaxPrice: 9999.99, // 默认系统价格上限
PriceThreshold: sql.NullFloat64{Valid: false}, // 默认不设阈值
PriceFeeRate: sql.NullFloat64{Valid: false}, // 默认不收费
}
_, err = l.svcCtx.AgentProductConfigModel.Insert(ctx, session, agentProductConfig)
if err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR),
"创建代理产品配置失败, err: %v, productId: %d", err, productId)
}
return nil
})
if err != nil {
return nil, err
}
// 3. 返回结果
id, _ := result.LastInsertId()
return &types.AdminCreateProductResp{Id: id}, nil
return &types.AdminCreateProductResp{Id: productId}, nil
}

View File

@@ -8,6 +8,7 @@ import (
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/core/stores/sqlx"
)
type AdminDeleteProductLogic struct {
@@ -32,11 +33,33 @@ func (l *AdminDeleteProductLogic) AdminDeleteProduct(req *types.AdminDeleteProdu
"查找产品失败, err: %v, id: %d", err, req.Id)
}
// 2. 执行软删除
err = l.svcCtx.ProductModel.DeleteSoft(l.ctx, nil, record)
// 2. 执行软删除(使用事务确保产品表和代理产品配置表同步)
err = l.svcCtx.ProductModel.Trans(l.ctx, func(ctx context.Context, session sqlx.Session) error {
// 2.1 软删除产品
err = l.svcCtx.ProductModel.DeleteSoft(ctx, session, record)
if err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR),
"删除产品失败, err: %v, id: %d", err, req.Id)
}
// 2.2 同步软删除代理产品配置
agentProductConfig, err := l.svcCtx.AgentProductConfigModel.FindOneByProductId(ctx, req.Id)
if err != nil {
// 如果代理产品配置不存在,记录日志但不影响主流程
l.Infof("同步删除代理产品配置失败:代理产品配置不存在, productId: %d, err: %v", req.Id, err)
} else {
err = l.svcCtx.AgentProductConfigModel.DeleteSoft(ctx, session, agentProductConfig)
if err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR),
"同步删除代理产品配置失败, err: %v, productId: %d", err, req.Id)
}
}
return nil
})
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR),
"删除产品失败, err: %v, id: %d", err, req.Id)
return nil, err
}
// 3. 返回结果

View File

@@ -9,6 +9,7 @@ import (
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/core/stores/sqlx"
)
type AdminUpdateProductLogic struct {
@@ -53,11 +54,20 @@ func (l *AdminUpdateProductLogic) AdminUpdateProduct(req *types.AdminUpdateProdu
record.SellPrice = *req.SellPrice
}
// 3. 执行更新操作
_, err = l.svcCtx.ProductModel.Update(l.ctx, nil, record)
// 3. 执行更新操作(使用事务确保产品表和代理产品配置表同步)
err = l.svcCtx.ProductModel.Trans(l.ctx, func(ctx context.Context, session sqlx.Session) error {
// 3.1 更新产品
_, err = l.svcCtx.ProductModel.Update(ctx, session, record)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR),
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR),
"更新产品失败, err: %v, req: %+v", err, req)
}
return nil
})
if err != nil {
return nil, err
}
// 4. 返回结果

View File

@@ -1,136 +0,0 @@
package admin_promotion
import (
"context"
"crypto/rand"
"fmt"
"math/big"
"time"
"ycc-server/app/main/api/internal/svc"
"ycc-server/app/main/api/internal/types"
"ycc-server/app/main/model"
"ycc-server/common/ctxdata"
"ycc-server/common/xerr"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/core/stores/sqlx"
)
type CreatePromotionLinkLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewCreatePromotionLinkLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreatePromotionLinkLogic {
return &CreatePromotionLinkLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
// 生成6位随机字符串大小写字母和数字
func generateRandomString() (string, error) {
const (
chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
length = 6
)
result := make([]byte, length)
for i := 0; i < length; i++ {
num, err := rand.Int(rand.Reader, big.NewInt(int64(len(chars))))
if err != nil {
return "", err
}
result[i] = chars[num.Int64()]
}
return string(result), nil
}
func (l *CreatePromotionLinkLogic) CreatePromotionLink(req *types.CreatePromotionLinkReq) (resp *types.CreatePromotionLinkResp, err error) {
// 获取当前用户ID
adminUserId, getUidErr := ctxdata.GetUidFromCtx(l.ctx)
if getUidErr != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "创建推广链接, 获取用户信息失败, %+v", getUidErr)
}
// 生成唯一URL
var url string
maxRetries := 5 // 最大重试次数
for i := 0; i < maxRetries; i++ {
// 生成6位随机字符串
randomStr, err := generateRandomString()
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "创建推广链接, 生成随机字符串失败, %+v", err)
}
// 检查URL是否已存在
existLink, err := l.svcCtx.AdminPromotionLinkModel.FindOneByUrl(l.ctx, randomStr)
if err != nil && !errors.Is(err, model.ErrNotFound) {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建推广链接, 检查URL是否存在失败, %+v", err)
}
if existLink != nil {
continue // URL已存在继续尝试
}
// URL可用
url = randomStr
break
}
if url == "" {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "创建推广链接失败, 多次尝试生成唯一URL均失败")
}
url = fmt.Sprintf("%s/%s", l.svcCtx.Config.AdminPromotion.URLDomain, url)
// 创建推广链接
link := &model.AdminPromotionLink{
Name: req.Name,
Url: url,
AdminUserId: adminUserId,
}
var linkId int64
err = l.svcCtx.AdminPromotionLinkModel.Trans(l.ctx, func(ctx context.Context, session sqlx.Session) error {
result, err := l.svcCtx.AdminPromotionLinkModel.Insert(l.ctx, session, link)
if err != nil {
return fmt.Errorf("创建推广链接失败, %+v", err)
}
linkId, err = result.LastInsertId()
if err != nil {
return fmt.Errorf("获取推广链接ID失败, %+v", err)
}
// 创建总统计记录
totalStats := &model.AdminPromotionLinkStatsTotal{
LinkId: linkId,
}
_, err = l.svcCtx.AdminPromotionLinkStatsTotalModel.Insert(l.ctx, session, totalStats)
if err != nil {
return fmt.Errorf("创建推广链接总统计记录失败, %+v", err)
}
// 创建统计历史记录
historyStats := &model.AdminPromotionLinkStatsHistory{
LinkId: linkId,
StatsDate: time.Now().Truncate(24 * time.Hour),
}
_, err = l.svcCtx.AdminPromotionLinkStatsHistoryModel.Insert(l.ctx, session, historyStats)
if err != nil {
return fmt.Errorf("创建推广链接统计历史记录失败, %+v", err)
}
return nil
})
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建推广链接失败, %+v", err)
}
return &types.CreatePromotionLinkResp{
Id: linkId,
Url: url,
}, nil
}

View File

@@ -1,91 +0,0 @@
package admin_promotion
import (
"context"
"ycc-server/app/main/api/internal/svc"
"ycc-server/app/main/api/internal/types"
"ycc-server/app/main/model"
"ycc-server/common/ctxdata"
"ycc-server/common/xerr"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/core/stores/sqlx"
)
type DeletePromotionLinkLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewDeletePromotionLinkLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeletePromotionLinkLogic {
return &DeletePromotionLinkLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *DeletePromotionLinkLogic) DeletePromotionLink(req *types.DeletePromotionLinkReq) error {
// 获取当前用户ID
adminUserId, getUidErr := ctxdata.GetUidFromCtx(l.ctx)
if getUidErr != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "删除推广链接, 获取用户信息失败, %+v", getUidErr)
}
// 获取链接信息
link, err := l.svcCtx.AdminPromotionLinkModel.FindOne(l.ctx, req.Id)
if err != nil {
return errors.Wrapf(err, "删除推广链接, 获取链接信息失败, %+v", err)
}
// 验证用户权限
if link.AdminUserId != adminUserId {
return errors.Wrapf(xerr.NewErrMsg("无权限删除此链接"), "删除推广链接, 无权限删除此链接, %+v", link)
}
// 在事务中执行所有删除操作
err = l.svcCtx.AdminPromotionLinkModel.Trans(l.ctx, func(ctx context.Context, session sqlx.Session) error {
// 软删除链接
err = l.svcCtx.AdminPromotionLinkModel.DeleteSoft(l.ctx, session, link)
if err != nil {
return errors.Wrapf(err, "删除推广链接, 软删除链接失败, %+v", err)
}
// 软删除总统计记录
totalStats, err := l.svcCtx.AdminPromotionLinkStatsTotalModel.FindOneByLinkId(l.ctx, link.Id)
if err != nil && !errors.Is(err, model.ErrNotFound) {
return errors.Wrapf(err, "删除推广链接, 获取总统计记录失败, %+v", err)
}
if totalStats != nil {
err = l.svcCtx.AdminPromotionLinkStatsTotalModel.DeleteSoft(l.ctx, session, totalStats)
if err != nil {
return errors.Wrapf(err, "删除推广链接, 软删除总统计记录失败, %+v", err)
}
}
// 软删除历史统计记录
builder := l.svcCtx.AdminPromotionLinkStatsHistoryModel.SelectBuilder()
builder = builder.Where("link_id = ?", link.Id)
historyStats, err := l.svcCtx.AdminPromotionLinkStatsHistoryModel.FindAll(l.ctx, builder, "")
if err != nil {
return errors.Wrapf(err, "删除推广链接, 获取历史统计记录失败, %+v", err)
}
for _, stat := range historyStats {
err = l.svcCtx.AdminPromotionLinkStatsHistoryModel.DeleteSoft(l.ctx, session, stat)
if err != nil {
return errors.Wrapf(err, "删除推广链接, 软删除历史统计记录失败, %+v", err)
}
}
return nil
})
if err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "删除推广链接失败, %+v", err)
}
return nil
}

View File

@@ -1,65 +0,0 @@
package admin_promotion
import (
"context"
"fmt"
"ycc-server/app/main/api/internal/svc"
"ycc-server/app/main/api/internal/types"
"ycc-server/app/main/model"
"ycc-server/common/ctxdata"
"ycc-server/common/xerr"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/logx"
)
type GetPromotionLinkDetailLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewGetPromotionLinkDetailLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetPromotionLinkDetailLogic {
return &GetPromotionLinkDetailLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *GetPromotionLinkDetailLogic) GetPromotionLinkDetail(req *types.GetPromotionLinkDetailReq) (resp *types.GetPromotionLinkDetailResp, err error) {
// 获取当前用户ID
adminUserId, getUidErr := ctxdata.GetUidFromCtx(l.ctx)
if getUidErr != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取当前用户ID失败, %+v", getUidErr)
}
// 获取链接信息
link, err := l.svcCtx.AdminPromotionLinkModel.FindOne(l.ctx, req.Id)
if err != nil {
return nil, errors.Wrapf(err, "获取链接信息失败, %+v", err)
}
// 验证用户权限
if link.AdminUserId != adminUserId {
return nil, errors.Wrapf(xerr.NewErrMsg("无权限访问此链接"), "获取链接信息失败, 无权限访问此链接, %+v", link)
}
// 获取总统计
totalStats, err := l.svcCtx.AdminPromotionLinkStatsTotalModel.FindOne(l.ctx, link.Id)
if err != nil && !errors.Is(err, model.ErrNotFound) {
return nil, errors.Wrapf(err, "获取总统计失败, %+v", err)
}
return &types.GetPromotionLinkDetailResp{
Name: link.Name,
Url: link.Url,
ClickCount: totalStats.ClickCount,
PayCount: totalStats.PayCount,
PayAmount: fmt.Sprintf("%.2f", totalStats.PayAmount),
CreateTime: link.CreateTime.Format("2006-01-02 15:04:05"),
UpdateTime: link.UpdateTime.Format("2006-01-02 15:04:05"),
LastClickTime: totalStats.LastClickTime.Time.Format("2006-01-02 15:04:05"),
LastPayTime: totalStats.LastPayTime.Time.Format("2006-01-02 15:04:05"),
}, nil
}

View File

@@ -1,104 +0,0 @@
package admin_promotion
import (
"context"
"fmt"
"ycc-server/app/main/api/internal/svc"
"ycc-server/app/main/api/internal/types"
"ycc-server/app/main/model"
"ycc-server/common/ctxdata"
"ycc-server/common/xerr"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/core/mr"
)
type GetPromotionLinkListLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewGetPromotionLinkListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetPromotionLinkListLogic {
return &GetPromotionLinkListLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *GetPromotionLinkListLogic) GetPromotionLinkList(req *types.GetPromotionLinkListReq) (resp *types.GetPromotionLinkListResp, err error) {
// 获取当前用户ID
adminUserId, getUidErr := ctxdata.GetUidFromCtx(l.ctx)
if getUidErr != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取当前用户ID失败, %+v", getUidErr)
}
// 构建查询条件
builder := l.svcCtx.AdminPromotionLinkModel.SelectBuilder()
builder = builder.Where("admin_user_id = ?", adminUserId)
if req.Name != "" {
builder = builder.Where("name LIKE ?", "%"+req.Name+"%")
}
if req.Url != "" {
builder = builder.Where("url LIKE ?", "%"+req.Url+"%")
}
// 获取列表和总数
links, total, err := l.svcCtx.AdminPromotionLinkModel.FindPageListByPageWithTotal(l.ctx, builder, req.Page, req.PageSize, "create_time DESC")
if err != nil {
return nil, errors.Wrapf(err, "获取推广链接列表失败, %+v", err)
}
// 使用MapReduce并发获取统计数据
items := make([]types.PromotionLinkItem, len(links))
err = mr.MapReduceVoid(func(source chan<- interface{}) {
for _, link := range links {
source <- link
}
}, func(item interface{}, writer mr.Writer[types.PromotionLinkItem], cancel func(error)) {
link := item.(*model.AdminPromotionLink)
// 获取总统计
totalStats, err := l.svcCtx.AdminPromotionLinkStatsTotalModel.FindOneByLinkId(l.ctx, link.Id)
if err != nil && !errors.Is(err, model.ErrNotFound) {
cancel(errors.Wrapf(err, "获取总统计失败, linkId: %d, %+v", link.Id, err))
return
}
writer.Write(types.PromotionLinkItem{
Id: link.Id,
Name: link.Name,
Url: link.Url,
ClickCount: totalStats.ClickCount,
PayCount: totalStats.PayCount,
PayAmount: fmt.Sprintf("%.2f", totalStats.PayAmount),
CreateTime: link.CreateTime.Format("2006-01-02 15:04:05"),
LastClickTime: func() string {
if totalStats.LastClickTime.Valid {
return totalStats.LastClickTime.Time.Format("2006-01-02 15:04:05")
}
return ""
}(),
LastPayTime: func() string {
if totalStats.LastPayTime.Valid {
return totalStats.LastPayTime.Time.Format("2006-01-02 15:04:05")
}
return ""
}(),
})
}, func(pipe <-chan types.PromotionLinkItem, cancel func(error)) {
for i := 0; i < len(links); i++ {
item := <-pipe
items[i] = item
}
})
if err != nil {
return nil, errors.Wrapf(err, "获取推广链接统计数据失败, %+v", err)
}
return &types.GetPromotionLinkListResp{
Total: total,
Items: items,
}, nil
}

View File

@@ -1,83 +0,0 @@
package admin_promotion
import (
"context"
"time"
"ycc-server/app/main/api/internal/svc"
"ycc-server/app/main/api/internal/types"
"ycc-server/common/ctxdata"
"ycc-server/common/xerr"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/logx"
)
type GetPromotionStatsHistoryLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewGetPromotionStatsHistoryLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetPromotionStatsHistoryLogic {
return &GetPromotionStatsHistoryLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *GetPromotionStatsHistoryLogic) GetPromotionStatsHistory(req *types.GetPromotionStatsHistoryReq) (resp []types.PromotionStatsHistoryItem, err error) {
// 获取当前用户ID
adminUserId, getUidErr := ctxdata.GetUidFromCtx(l.ctx)
if getUidErr != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取当前用户ID失败, %+v", getUidErr)
}
// 构建查询条件
builder := l.svcCtx.AdminPromotionLinkStatsHistoryModel.SelectBuilder()
// 如果有日期范围,添加日期过滤
if req.StartDate != "" && req.EndDate != "" {
startDate, err := time.Parse("2006-01-02", req.StartDate)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "开始日期格式错误")
}
endDate, err := time.Parse("2006-01-02", req.EndDate)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "结束日期格式错误")
}
// 将结束日期设置为当天的最后一刻
endDate = endDate.Add(24*time.Hour - time.Second)
builder = builder.Where("stats_date BETWEEN ? AND ?", startDate, endDate)
}
// 获取历史统计数据
historyStats, err := l.svcCtx.AdminPromotionLinkStatsHistoryModel.FindAll(l.ctx, builder, "stats_date DESC")
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取历史统计数据失败")
}
// 转换为响应格式
resp = make([]types.PromotionStatsHistoryItem, 0, len(historyStats))
for _, stat := range historyStats {
// 验证链接是否属于当前用户
link, err := l.svcCtx.AdminPromotionLinkModel.FindOne(l.ctx, stat.LinkId)
if err != nil {
continue // 如果链接不存在,跳过该记录
}
if link.AdminUserId != adminUserId {
continue // 如果链接不属于当前用户,跳过该记录
}
resp = append(resp, types.PromotionStatsHistoryItem{
Id: stat.Id,
LinkId: stat.LinkId,
PayAmount: stat.PayAmount,
ClickCount: stat.ClickCount,
PayCount: stat.PayCount,
StatsDate: stat.StatsDate.Format("2006-01-02"),
})
}
return resp, nil
}

View File

@@ -1,166 +0,0 @@
package admin_promotion
import (
"context"
"time"
"ycc-server/app/main/api/internal/svc"
"ycc-server/app/main/api/internal/types"
"ycc-server/app/main/model"
"ycc-server/common/ctxdata"
"ycc-server/common/xerr"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/core/mr"
)
type GetPromotionStatsTotalLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewGetPromotionStatsTotalLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetPromotionStatsTotalLogic {
return &GetPromotionStatsTotalLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *GetPromotionStatsTotalLogic) GetPromotionStatsTotal(req *types.GetPromotionStatsTotalReq) (resp *types.GetPromotionStatsTotalResp, err error) {
// 获取当前用户ID
adminUserId, getUidErr := ctxdata.GetUidFromCtx(l.ctx)
if getUidErr != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取当前用户ID失败, %+v", getUidErr)
}
// 获取用户的所有推广链接
linkBuilder := l.svcCtx.AdminPromotionLinkModel.SelectBuilder()
linkBuilder = linkBuilder.Where("admin_user_id = ?", adminUserId)
links, err := l.svcCtx.AdminPromotionLinkModel.FindAll(l.ctx, linkBuilder, "")
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取推广链接列表失败, %+v", err)
}
// 如果没有推广链接,返回空统计
if len(links) == 0 {
return &types.GetPromotionStatsTotalResp{}, nil
}
// 构建链接ID列表
linkIds := make([]int64, len(links))
for i, link := range links {
linkIds[i] = link.Id
}
// 获取并计算总统计数据
var totalClickCount, totalPayCount int64
var totalPayAmount float64
err = mr.MapReduceVoid(func(source chan<- interface{}) {
for _, linkId := range linkIds {
source <- linkId
}
}, func(item interface{}, writer mr.Writer[struct {
ClickCount int64
PayCount int64
PayAmount float64
}], cancel func(error)) {
linkId := item.(int64)
stats, err := l.svcCtx.AdminPromotionLinkStatsTotalModel.FindOneByLinkId(l.ctx, linkId)
if err != nil && !errors.Is(err, model.ErrNotFound) {
cancel(errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取总统计数据失败, linkId: %d, %+v", linkId, err))
return
}
if stats != nil {
writer.Write(struct {
ClickCount int64
PayCount int64
PayAmount float64
}{
ClickCount: stats.ClickCount,
PayCount: stats.PayCount,
PayAmount: stats.PayAmount,
})
}
}, func(pipe <-chan struct {
ClickCount int64
PayCount int64
PayAmount float64
}, cancel func(error)) {
for stats := range pipe {
totalClickCount += stats.ClickCount
totalPayCount += stats.PayCount
totalPayAmount += stats.PayAmount
}
})
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取总统计数据失败, %+v", err)
}
// 获取今日统计数据
now := time.Now()
today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.Local)
var todayClickCount, todayPayCount int64
var todayPayAmount float64
err = mr.MapReduceVoid(func(source chan<- interface{}) {
for _, linkId := range linkIds {
source <- linkId
}
}, func(item interface{}, writer mr.Writer[struct {
ClickCount int64
PayCount int64
PayAmount float64
}], cancel func(error)) {
linkId := item.(int64)
builder := l.svcCtx.AdminPromotionLinkStatsHistoryModel.SelectBuilder()
builder = builder.Where("link_id = ? AND DATE(stats_date) = DATE(?)", linkId, today)
histories, err := l.svcCtx.AdminPromotionLinkStatsHistoryModel.FindAll(l.ctx, builder, "")
if err != nil {
cancel(errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取今日统计数据失败, linkId: %d, %+v", linkId, err))
return
}
var clickCount, payCount int64
var payAmount float64
for _, history := range histories {
clickCount += history.ClickCount
payCount += history.PayCount
payAmount += history.PayAmount
}
writer.Write(struct {
ClickCount int64
PayCount int64
PayAmount float64
}{
ClickCount: clickCount,
PayCount: payCount,
PayAmount: payAmount,
})
}, func(pipe <-chan struct {
ClickCount int64
PayCount int64
PayAmount float64
}, cancel func(error)) {
for stats := range pipe {
todayClickCount += stats.ClickCount
todayPayCount += stats.PayCount
todayPayAmount += stats.PayAmount
}
})
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取今日统计数据失败, %+v", err)
}
return &types.GetPromotionStatsTotalResp{
TodayClickCount: int64(todayClickCount),
TodayPayCount: int64(todayPayCount),
TodayPayAmount: todayPayAmount,
TotalClickCount: int64(totalClickCount),
TotalPayCount: int64(totalPayCount),
TotalPayAmount: totalPayAmount,
}, nil
}

View File

@@ -1,57 +0,0 @@
package admin_promotion
import (
"context"
"fmt"
"ycc-server/app/main/api/internal/svc"
"ycc-server/app/main/api/internal/types"
"ycc-server/common/xerr"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/logx"
)
type RecordLinkClickLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewRecordLinkClickLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RecordLinkClickLogic {
return &RecordLinkClickLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *RecordLinkClickLogic) RecordLinkClick(req *types.RecordLinkClickReq) (resp *types.RecordLinkClickResp, err error) {
// 校验路径格式
if len(req.Path) != 6 {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "无效的推广链接路径")
}
// 检查是否只包含大小写字母和数字
for _, char := range req.Path {
if !((char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') || (char >= '0' && char <= '9')) {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "无效的推广链接路径")
}
}
url := fmt.Sprintf("%s/%s", l.svcCtx.Config.AdminPromotion.URLDomain, req.Path)
link, err := l.svcCtx.AdminPromotionLinkModel.FindOneByUrl(l.ctx, url)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "无效的推广链接路径")
}
// 使用 statsService 更新点击统计
err = l.svcCtx.AdminPromotionLinkStatsService.UpdateLinkStats(l.ctx, link.Id)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新点击统计失败: %+v", err)
}
return &types.RecordLinkClickResp{
Success: true,
}, nil
}

View File

@@ -1,57 +0,0 @@
package admin_promotion
import (
"context"
"time"
"ycc-server/app/main/api/internal/svc"
"ycc-server/app/main/api/internal/types"
"ycc-server/common/ctxdata"
"ycc-server/common/xerr"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/logx"
)
type UpdatePromotionLinkLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewUpdatePromotionLinkLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdatePromotionLinkLogic {
return &UpdatePromotionLinkLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *UpdatePromotionLinkLogic) UpdatePromotionLink(req *types.UpdatePromotionLinkReq) error {
// 获取当前用户ID
adminUserId, getUidErr := ctxdata.GetUidFromCtx(l.ctx)
if getUidErr != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "更新推广链接, 获取用户信息失败, %+v", getUidErr)
}
// 获取链接信息
link, err := l.svcCtx.AdminPromotionLinkModel.FindOne(l.ctx, req.Id)
if err != nil {
return errors.Wrapf(err, "更新推广链接, 获取链接信息失败, %+v", err)
}
// 验证用户权限
if link.AdminUserId != adminUserId {
return errors.Wrapf(xerr.NewErrMsg("无权限修改此链接"), "更新推广链接, 无权限修改此链接, %+v", link)
}
// 更新链接信息
link.Name = *req.Name
link.UpdateTime = time.Now()
_, err = l.svcCtx.AdminPromotionLinkModel.Update(l.ctx, nil, link)
if err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新推广链接, 更新链接信息失败, %+v", err)
}
return nil
}

View File

@@ -4,6 +4,7 @@ import (
"context"
"database/sql"
"fmt"
"os"
"time"
"ycc-server/app/main/model"
"ycc-server/common/ctxdata"
@@ -51,8 +52,8 @@ func (l *ApplyForAgentLogic) ApplyForAgent(req *types.AgentApplyReq) (resp *type
return nil, errors.Wrapf(xerr.NewErrMsg("必须提供邀请码才能成为代理,请联系平台或代理获取邀请码"), "")
}
// 2. 校验验证码
if req.Mobile != "18889793585" {
// 2. 校验验证码(开发环境下跳过验证码校验)
if os.Getenv("ENV") != "development" {
redisKey := fmt.Sprintf("%s:%s", "agentApply", encryptedMobile)
cacheCode, err := l.svcCtx.Redis.Get(redisKey)
if err != nil {
@@ -138,9 +139,9 @@ func (l *ApplyForAgentLogic) ApplyForAgent(req *types.AgentApplyReq) (resp *type
// 4.5 创建代理记录
newAgent := &model.Agent{
UserId: userID,
Level: targetLevel,
Mobile: encryptedMobile,
UserId: userID,
Level: targetLevel,
Mobile: encryptedMobile,
}
if req.Region != "" {
newAgent.Region = sql.NullString{String: req.Region, Valid: true}
@@ -179,7 +180,7 @@ func (l *ApplyForAgentLogic) ApplyForAgent(req *types.AgentApplyReq) (resp *type
// 建立关系
relation := &model.AgentRelation{
ParentId: parentAgent.Id,
ChildId: agentId,
ChildId: agentId,
RelationType: 1, // 直接关系
}
if _, err := l.svcCtx.AgentRelationModel.Insert(transCtx, session, relation); err != nil {
@@ -197,7 +198,7 @@ func (l *ApplyForAgentLogic) ApplyForAgent(req *types.AgentApplyReq) (resp *type
// 设置自己为团队首领
newAgent.TeamLeaderId = sql.NullInt64{Int64: agentId, Valid: true}
if err := l.svcCtx.AgentModel.UpdateWithVersion(transCtx, session, newAgent); err != nil {
if err := l.svcCtx.AgentModel.UpdateInTransaction(transCtx, session, newAgent); err != nil {
return errors.Wrapf(err, "更新团队首领失败")
}
} else {
@@ -232,6 +233,19 @@ func (l *ApplyForAgentLogic) ApplyForAgent(req *types.AgentApplyReq) (resp *type
return errors.Wrapf(err, "更新邀请码状态失败")
}
// 4.9 记录邀请码使用历史(用于统计和查询)
usage := &model.AgentInviteCodeUsage{
InviteCodeId: inviteCodeModel.Id,
Code: inviteCodeModel.Code,
UserId: userID,
AgentId: newAgent.Id,
AgentLevel: targetLevel,
UsedTime: time.Now(),
}
if _, err := l.svcCtx.AgentInviteCodeUsageModel.Insert(transCtx, session, usage); err != nil {
return errors.Wrapf(err, "记录邀请码使用历史失败")
}
return nil
})

View File

@@ -6,7 +6,6 @@ import (
"ycc-server/app/main/model"
"ycc-server/common/ctxdata"
"ycc-server/common/xerr"
"ycc-server/pkg/lzkit/lzUtils"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/stores/sqlx"
@@ -55,8 +54,14 @@ func (l *ApplyUpgradeLogic) ApplyUpgrade(req *types.ApplyUpgradeReq) (resp *type
}
// 3. 计算升级费用和返佣
upgradeFee := l.svcCtx.AgentService.GetUpgradeFee(fromLevel, toLevel)
rebateAmount := l.svcCtx.AgentService.GetUpgradeRebate(fromLevel, toLevel)
upgradeFee, err := l.svcCtx.AgentService.GetUpgradeFee(l.ctx, fromLevel, toLevel)
if err != nil {
return nil, errors.Wrapf(err, "获取升级费用失败")
}
rebateAmount, err := l.svcCtx.AgentService.GetUpgradeRebate(l.ctx, fromLevel, toLevel)
if err != nil {
return nil, errors.Wrapf(err, "获取升级返佣金额失败")
}
// 4. 查找原直接上级(用于返佣)
var rebateAgentId int64
@@ -68,18 +73,18 @@ func (l *ApplyUpgradeLogic) ApplyUpgrade(req *types.ApplyUpgradeReq) (resp *type
rebateAgentId = parent.Id
}
// 5. 使用事务处理升级
// 5. 创建升级记录(待支付状态)
var upgradeId int64
err = l.svcCtx.AgentWalletModel.Trans(l.ctx, func(transCtx context.Context, session sqlx.Session) error {
// 5.1 创建升级记录
// 5.1 创建升级记录(状态为待支付)
upgradeRecord := &model.AgentUpgrade{
AgentId: agent.Id,
FromLevel: fromLevel,
ToLevel: toLevel,
UpgradeType: 1, // 自主付费
UpgradeFee: upgradeFee,
RebateAmount: rebateAmount,
Status: 1, // 待处理
AgentId: agent.Id,
FromLevel: fromLevel,
ToLevel: toLevel,
UpgradeType: 1, // 自主付费
UpgradeFee: upgradeFee,
RebateAmount: rebateAmount,
Status: 1, // 待支付1=待支付2=已支付3=已完成4=已取消)
}
if rebateAgentId > 0 {
upgradeRecord.RebateAgentId = sql.NullInt64{Int64: rebateAgentId, Valid: true}
@@ -91,23 +96,7 @@ func (l *ApplyUpgradeLogic) ApplyUpgrade(req *types.ApplyUpgradeReq) (resp *type
}
upgradeId, _ = upgradeResult.LastInsertId()
// 5.2 处理支付(这里假设支付已在外层处理,只记录订单号)
// 实际支付应该在创建升级记录之前完成
// 注意:支付订单号需要从支付回调中获取,这里暂时留空
// 5.3 执行升级操作
if err := l.svcCtx.AgentService.ProcessUpgrade(transCtx, agent.Id, toLevel, 1, upgradeFee, rebateAmount, "", 0); err != nil {
return errors.Wrapf(err, "执行升级操作失败")
}
// 5.4 更新升级记录状态
upgradeRecord.Id = upgradeId
upgradeRecord.Status = 2 // 已完成
upgradeRecord.Remark = lzUtils.StringToNullString("升级成功")
if err := l.svcCtx.AgentUpgradeModel.UpdateWithVersion(transCtx, session, upgradeRecord); err != nil {
return errors.Wrapf(err, "更新升级记录失败")
}
// 注意:升级操作将在支付成功后通过支付回调完成
return nil
})
@@ -115,10 +104,10 @@ func (l *ApplyUpgradeLogic) ApplyUpgrade(req *types.ApplyUpgradeReq) (resp *type
return nil, err
}
// 返回响应(订单号需要从支付回调中获取,这里暂时返回空
// 返回响应(订单号将在支付接口中生成
return &types.ApplyUpgradeResp{
UpgradeId: upgradeId,
OrderNo: "", // 需要从支付回调中获取
OrderNo: "", // 将在支付接口中生成
}, nil
}

View File

@@ -0,0 +1,72 @@
package agent
import (
"context"
"ycc-server/app/main/model"
"ycc-server/common/ctxdata"
"ycc-server/common/xerr"
"github.com/pkg/errors"
"ycc-server/app/main/api/internal/svc"
"ycc-server/app/main/api/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type DeleteInviteCodeLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewDeleteInviteCodeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeleteInviteCodeLogic {
return &DeleteInviteCodeLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *DeleteInviteCodeLogic) DeleteInviteCode(req *types.DeleteInviteCodeReq) (resp *types.DeleteInviteCodeResp, err error) {
// 1. 获取当前代理信息
userID, err := ctxdata.GetUidFromCtx(l.ctx)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取用户信息失败, %v", err)
}
agent, err := l.svcCtx.AgentModel.FindOneByUserId(l.ctx, userID)
if err != nil {
if errors.Is(err, model.ErrNotFound) {
return nil, errors.Wrapf(xerr.NewErrMsg("您不是代理"), "")
}
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询代理信息失败, %v", err)
}
// 2. 查询邀请码是否存在且属于当前代理
inviteCode, err := l.svcCtx.AgentInviteCodeModel.FindOne(l.ctx, req.Id)
if err != nil {
if errors.Is(err, model.ErrNotFound) {
return nil, errors.Wrapf(xerr.NewErrMsg("邀请码不存在"), "")
}
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询邀请码失败, %v", err)
}
// 3. 验证邀请码是否属于当前代理
if !inviteCode.AgentId.Valid || inviteCode.AgentId.Int64 != agent.Id {
return nil, errors.Wrapf(xerr.NewErrMsg("无权删除此邀请码"), "")
}
// 4. 检查邀请码是否已被使用
if inviteCode.Status == 1 {
return nil, errors.Wrapf(xerr.NewErrMsg("已使用的邀请码无法删除"), "")
}
// 5. 执行软删除
err = l.svcCtx.AgentInviteCodeModel.DeleteSoft(l.ctx, nil, inviteCode)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "删除邀请码失败, %v", err)
}
return &types.DeleteInviteCodeResp{}, nil
}

View File

@@ -2,12 +2,17 @@ package agent
import (
"context"
"database/sql"
"encoding/hex"
"encoding/json"
"fmt"
"net/url"
"strconv"
"strings"
"ycc-server/app/main/model"
"ycc-server/common/ctxdata"
"ycc-server/common/globalkey"
"ycc-server/common/tool"
"ycc-server/common/xerr"
"ycc-server/pkg/lzkit/crypto"
@@ -49,19 +54,25 @@ func (l *GeneratingLinkLogic) GeneratingLink(req *types.AgentGeneratingLinkReq)
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询代理信息失败, %v", err)
}
// 2. 获取系统配置
basePrice, err := l.getConfigFloat("base_price")
// 2. 获取产品配置(必须存在)
productConfig, err := l.svcCtx.AgentProductConfigModel.FindOneByProductId(l.ctx, req.ProductId)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取基础底价配置失败, %v", err)
}
systemMaxPrice, err := l.getConfigFloat("system_max_price")
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取系统价格上限配置失败, %v", err)
if errors.Is(err, model.ErrNotFound) {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "产品配置不存在, productId: %d请先在后台配置产品价格参数", req.ProductId)
}
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询产品配置失败, productId: %d, %v", req.ProductId, err)
}
// 4. 计算实际底价(基础底价 + 等级加成
levelBonus := l.getLevelBonus(agentModel.Level)
// 3. 获取等级加成配置
levelBonus, err := l.getLevelBonus(agentModel.Level)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取等级加成配置失败, %v", err)
}
// 4. 计算实际底价(产品基础底价 + 等级加成)
basePrice := productConfig.BasePrice
actualBasePrice := basePrice + float64(levelBonus)
systemMaxPrice := productConfig.SystemMaxPrice
// 5. 验证设定价格范围
if req.SetPrice < actualBasePrice || req.SetPrice > systemMaxPrice {
@@ -82,9 +93,18 @@ func (l *GeneratingLinkLogic) GeneratingLink(req *types.AgentGeneratingLinkReq)
}
if len(existingLinks) > 0 {
// 已存在,直接返回
// 已存在,检查是否有短链,如果没有则生成
targetPath := req.TargetPath
if targetPath == "" {
targetPath = "/agent/promotionInquire/"
}
shortLink, err := l.getOrCreateShortLink(1, existingLinks[0].Id, 0, existingLinks[0].LinkIdentifier, "", targetPath)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取或创建短链失败, %v", err)
}
return &types.AgentGeneratingLinkResp{
LinkIdentifier: existingLinks[0].LinkIdentifier,
FullLink: shortLink,
}, nil
}
@@ -120,39 +140,187 @@ func (l *GeneratingLinkLogic) GeneratingLink(req *types.AgentGeneratingLinkReq)
ActualBasePrice: actualBasePrice,
}
_, err = l.svcCtx.AgentLinkModel.Insert(l.ctx, nil, agentLink)
result, err := l.svcCtx.AgentLinkModel.Insert(l.ctx, nil, agentLink)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "保存推广链接失败, %v", err)
}
// 获取插入的ID
linkId, err := result.LastInsertId()
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取推广链接ID失败, %v", err)
}
// 使用默认target_path如果未提供
targetPath := req.TargetPath
if targetPath == "" {
targetPath = "/agent/promotionInquire/"
}
// 生成短链类型1=推广报告)
shortLink, err := l.createShortLink(1, linkId, 0, encrypted, "", targetPath)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "生成短链失败, %v", err)
}
return &types.AgentGeneratingLinkResp{
LinkIdentifier: encrypted,
FullLink: shortLink,
}, nil
}
// getLevelBonus 获取等级加成
func (l *GeneratingLinkLogic) getLevelBonus(level int64) int64 {
// getLevelBonus 获取等级加成(从配置表读取)
func (l *GeneratingLinkLogic) getLevelBonus(level int64) (int64, error) {
var configKey string
switch level {
case 1: // 普通
return 6
configKey = "level_1_bonus"
case 2: // 黄金
return 3
configKey = "level_2_bonus"
case 3: // 钻石
return 0
configKey = "level_3_bonus"
default:
return 0
return 0, nil
}
}
// getConfigFloat 获取配置值(浮点数)
func (l *GeneratingLinkLogic) getConfigFloat(configKey string) (float64, error) {
config, err := l.svcCtx.AgentConfigModel.FindOneByConfigKey(l.ctx, configKey)
if err != nil {
return 0, err
// 配置不存在时返回默认值
l.Errorf("获取等级加成配置失败, level: %d, key: %s, err: %v使用默认值", level, configKey, err)
switch level {
case 1:
return 6, nil
case 2:
return 3, nil
case 3:
return 0, nil
}
return 0, nil
}
value, err := strconv.ParseFloat(config.ConfigValue, 64)
if err != nil {
return 0, errors.Wrapf(err, "解析配置值失败, key: %s, value: %s", configKey, config.ConfigValue)
}
return value, nil
return int64(value), nil
}
// getOrCreateShortLink 获取或创建短链
// type: 1=推广报告(promotion), 2=邀请好友(invite)
// linkId: 推广链接ID仅推广报告使用
// inviteCodeId: 邀请码ID仅邀请好友使用
// linkIdentifier: 推广链接标识(仅推广报告使用)
// inviteCode: 邀请码(仅邀请好友使用)
// targetPath: 目标地址(前端传入)
func (l *GeneratingLinkLogic) getOrCreateShortLink(linkType int64, linkId, inviteCodeId int64, linkIdentifier, inviteCode, targetPath string) (string, error) {
// 先查询是否已存在短链
var existingShortLink *model.AgentShortLink
var err error
if linkType == 1 {
// 推广报告类型使用link_id查询
if linkId > 0 {
existingShortLink, err = l.svcCtx.AgentShortLinkModel.FindOneByLinkIdTypeDelState(l.ctx, sql.NullInt64{Int64: linkId, Valid: true}, linkType, globalkey.DelStateNo)
}
} else {
// 邀请好友类型使用invite_code_id查询
if inviteCodeId > 0 {
existingShortLink, err = l.svcCtx.AgentShortLinkModel.FindOneByInviteCodeIdTypeDelState(l.ctx, sql.NullInt64{Int64: inviteCodeId, Valid: true}, linkType, globalkey.DelStateNo)
}
}
if err == nil && existingShortLink != nil {
// 已存在短链,直接返回
return l.buildShortLinkURL(existingShortLink.ShortCode), nil
}
if err != nil && !errors.Is(err, model.ErrNotFound) {
return "", errors.Wrapf(err, "查询短链失败")
}
// 不存在,创建新的短链
return l.createShortLink(linkType, linkId, inviteCodeId, linkIdentifier, inviteCode, targetPath)
}
// createShortLink 创建短链
// type: 1=推广报告(promotion), 2=邀请好友(invite)
func (l *GeneratingLinkLogic) createShortLink(linkType int64, linkId, inviteCodeId int64, linkIdentifier, inviteCode, targetPath string) (string, error) {
promotionConfig := l.svcCtx.Config.Promotion
// 如果没有配置推广域名,返回空字符串(保持向后兼容)
if promotionConfig.PromotionDomain == "" {
l.Errorf("推广域名未配置,返回空链接")
return "", nil
}
// 验证target_path
if targetPath == "" {
return "", errors.Wrapf(xerr.NewErrMsg("目标地址不能为空"), "")
}
// 对于推广报告类型,将 linkIdentifier 拼接到 target_path
if linkType == 1 && linkIdentifier != "" {
// 如果 target_path 以 / 结尾,直接拼接 linkIdentifier
if strings.HasSuffix(targetPath, "/") {
targetPath = targetPath + url.QueryEscape(linkIdentifier)
} else {
// 否则在末尾添加 / 再拼接
targetPath = targetPath + "/" + url.QueryEscape(linkIdentifier)
}
}
// 生成短链标识6位随机字符串大小写字母+数字)
var shortCode string
maxRetries := 10 // 最大重试次数
for retry := 0; retry < maxRetries; retry++ {
shortCode = tool.Krand(6, tool.KC_RAND_KIND_ALL)
// 检查短链标识是否已存在
_, err := l.svcCtx.AgentShortLinkModel.FindOneByShortCodeDelState(l.ctx, shortCode, globalkey.DelStateNo)
if err != nil {
if errors.Is(err, model.ErrNotFound) {
// 短链标识不存在,可以使用
break
}
return "", errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "检查短链标识失败, %v", err)
}
// 短链标识已存在,继续生成
if retry == maxRetries-1 {
return "", errors.Wrapf(xerr.NewErrMsg("生成短链失败,请重试"), "")
}
}
// 创建短链记录
shortLink := &model.AgentShortLink{
Type: linkType,
ShortCode: shortCode,
TargetPath: targetPath,
PromotionDomain: promotionConfig.PromotionDomain,
}
// 根据类型设置对应字段
if linkType == 1 {
// 推广报告类型
shortLink.LinkId = sql.NullInt64{Int64: linkId, Valid: linkId > 0}
if linkIdentifier != "" {
shortLink.LinkIdentifier = sql.NullString{String: linkIdentifier, Valid: true}
}
} else if linkType == 2 {
// 邀请好友类型
shortLink.InviteCodeId = sql.NullInt64{Int64: inviteCodeId, Valid: inviteCodeId > 0}
if inviteCode != "" {
shortLink.InviteCode = sql.NullString{String: inviteCode, Valid: true}
}
}
_, err := l.svcCtx.AgentShortLinkModel.Insert(l.ctx, nil, shortLink)
if err != nil {
return "", errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "保存短链失败, %v", err)
}
return l.buildShortLinkURL(shortCode), nil
}
// buildShortLinkURL 构建短链URL
func (l *GeneratingLinkLogic) buildShortLinkURL(shortCode string) string {
promotionConfig := l.svcCtx.Config.Promotion
return fmt.Sprintf("%s/s/%s", promotionConfig.PromotionDomain, shortCode)
}

View File

@@ -44,68 +44,58 @@ func (l *GetAgentProductConfigLogic) GetAgentProductConfig() (resp *types.AgentP
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询代理信息失败, %v", err)
}
// 2. 获取系统配置
basePrice, err := l.getConfigFloat("base_price")
// 2. 获取等级加成配置(从系统配置表读取)
levelBonus, err := l.getLevelBonus(agentModel.Level)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取基础底价配置失败, %v", err)
}
systemMaxPrice, err := l.getConfigFloat("system_max_price")
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取系统价格上限配置失败, %v", err)
}
priceThreshold, err := l.getConfigFloat("price_threshold")
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取提价标准阈值配置失败, %v", err)
}
priceFeeRate, err := l.getConfigFloat("price_fee_rate")
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取提价手续费比例配置失败, %v", err)
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取等级加成配置失败, %v", err)
}
// 3. 计算等级加成
levelBonus := l.getLevelBonus(agentModel.Level)
// 4. 查询所有产品配置
// 3. 查询所有产品配置
builder := l.svcCtx.AgentProductConfigModel.SelectBuilder()
productConfigs, err := l.svcCtx.AgentProductConfigModel.FindAll(l.ctx, builder, "")
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询产品配置失败, %v", err)
}
// 5. 组装响应数据
// 4. 组装响应数据(通过 product_id 查询产品名称和英文标识)
var respList []types.ProductConfigItem
for _, productConfig := range productConfigs {
// 使用产品配置中的基础底价,如果没有则使用系统配置的基础底价
productBasePrice := basePrice
if productConfig.BasePrice > 0 {
productBasePrice = productConfig.BasePrice
// 通过 product_id 查询产品信息获取产品名称和英文标识
product, err := l.svcCtx.ProductModel.FindOne(l.ctx, productConfig.ProductId)
productName := ""
productEn := ""
if err == nil {
productName = product.ProductName
productEn = product.ProductEn
} else {
// 如果产品不存在,记录日志但不影响主流程
l.Infof("查询产品信息失败, productId: %d, err: %v", productConfig.ProductId, err)
}
// 使用产品配置的基础底价(必须配置)
productBasePrice := productConfig.BasePrice
// 计算该产品的实际底价
productActualBasePrice := productBasePrice + float64(levelBonus)
// 价格范围:实际底价 ≤ 设定价格 ≤ 系统价格上限(或产品配置的最高价格
// 价格范围:实际底价 ≤ 设定价格 ≤ 产品配置的最高价格
priceRangeMin := productActualBasePrice
priceRangeMax := systemMaxPrice
if productConfig.SystemMaxPrice > 0 && productConfig.SystemMaxPrice < systemMaxPrice {
priceRangeMax = productConfig.SystemMaxPrice
}
priceRangeMax := productConfig.SystemMaxPrice
// 使用产品配置的提价阈值和手续费比例,如果没有则使用系统配置
productPriceThreshold := priceThreshold
if productConfig.PriceThreshold.Valid && productConfig.PriceThreshold.Float64 > 0 {
// 使用产品配置的提价阈值和手续费比例,如果为NULL则使用0
productPriceThreshold := 0.0
if productConfig.PriceThreshold.Valid {
productPriceThreshold = productConfig.PriceThreshold.Float64
}
productPriceFeeRate := priceFeeRate
if productConfig.PriceFeeRate.Valid && productConfig.PriceFeeRate.Float64 > 0 {
productPriceFeeRate := 0.0
if productConfig.PriceFeeRate.Valid {
productPriceFeeRate = productConfig.PriceFeeRate.Float64
}
respList = append(respList, types.ProductConfigItem{
ProductId: productConfig.ProductId,
ProductName: productConfig.ProductName,
BasePrice: productBasePrice,
LevelBonus: float64(levelBonus),
ProductName: productName,
ProductEn: productEn,
ActualBasePrice: productActualBasePrice,
PriceRangeMin: priceRangeMin,
PriceRangeMax: priceRangeMax,
@@ -119,29 +109,38 @@ func (l *GetAgentProductConfigLogic) GetAgentProductConfig() (resp *types.AgentP
}, nil
}
// getLevelBonus 获取等级加成
func (l *GetAgentProductConfigLogic) getLevelBonus(level int64) int64 {
// getLevelBonus 获取等级加成(从配置表读取)
func (l *GetAgentProductConfigLogic) getLevelBonus(level int64) (int64, error) {
var configKey string
switch level {
case 1: // 普通
return 6
configKey = "level_1_bonus"
case 2: // 黄金
return 3
configKey = "level_2_bonus"
case 3: // 钻石
return 0
configKey = "level_3_bonus"
default:
return 0
return 0, nil
}
}
// getConfigFloat 获取配置值(浮点数)
func (l *GetAgentProductConfigLogic) getConfigFloat(configKey string) (float64, error) {
config, err := l.svcCtx.AgentConfigModel.FindOneByConfigKey(l.ctx, configKey)
if err != nil {
return 0, err
// 配置不存在时返回默认值
l.Errorf("获取等级加成配置失败, level: %d, key: %s, err: %v使用默认值", level, configKey, err)
switch level {
case 1:
return 6, nil
case 2:
return 3, nil
case 3:
return 0, nil
}
return 0, nil
}
value, err := strconv.ParseFloat(config.ConfigValue, 64)
if err != nil {
return 0, errors.Wrapf(err, "解析配置值失败, key: %s, value: %s", configKey, config.ConfigValue)
}
return value, nil
return int64(value), nil
}

View File

@@ -61,7 +61,7 @@ func (l *GetCommissionListLogic) GetCommissionList(req *types.GetCommissionListR
offset := (page - 1) * pageSize
// 4. 查询总数
total, err := l.svcCtx.AgentCommissionModel.FindCount(l.ctx, builder, "")
total, err := l.svcCtx.AgentCommissionModel.FindCount(l.ctx, builder, "id")
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询佣金总数失败, %v", err)
}
@@ -85,9 +85,19 @@ func (l *GetCommissionListLogic) GetCommissionList(req *types.GetCommissionListR
}
}
// 查询订单号
orderNo := ""
if commission.OrderId > 0 {
order, err := l.svcCtx.OrderModel.FindOne(l.ctx, commission.OrderId)
if err == nil {
orderNo = order.OrderNo
}
}
list = append(list, types.CommissionItem{
Id: commission.Id,
OrderId: commission.OrderId,
OrderNo: orderNo,
ProductName: productName,
Amount: commission.Amount,
Status: commission.Status,

View File

@@ -0,0 +1,492 @@
package agent
import (
"context"
"time"
"ycc-server/app/main/model"
"ycc-server/common/ctxdata"
"ycc-server/common/globalkey"
"ycc-server/common/xerr"
"github.com/Masterminds/squirrel"
"github.com/pkg/errors"
"ycc-server/app/main/api/internal/svc"
"ycc-server/app/main/api/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type GetConversionRateLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewGetConversionRateLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetConversionRateLogic {
return &GetConversionRateLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *GetConversionRateLogic) GetConversionRate() (resp *types.ConversionRateResp, err error) {
userID, err := ctxdata.GetUidFromCtx(l.ctx)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取用户信息失败, %v", err)
}
// 1. 获取代理信息
agent, err := l.svcCtx.AgentModel.FindOneByUserId(l.ctx, userID)
if err != nil {
if errors.Is(err, model.ErrNotFound) {
return nil, errors.Wrapf(xerr.NewErrMsg("您不是代理"), "")
}
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询代理信息失败, %v", err)
}
// 2. 统计我的转化率
myConversionRate := l.calculateConversionRate(agent.Id, nil)
// 3. 统计下级转化率(按时间段动态查询历史下级关系)
subordinateConversionRate := l.calculateSubordinateConversionRate(agent.Id)
return &types.ConversionRateResp{
MyConversionRate: myConversionRate,
SubordinateConversionRate: subordinateConversionRate,
}, nil
}
// calculateSubordinateConversionRate 计算下级转化率(考虑历史关系)
func (l *GetConversionRateLogic) calculateSubordinateConversionRate(parentAgentId int64) types.ConversionRateData {
// 使用Asia/Shanghai时区与数据库保持一致
loc, _ := time.LoadLocation("Asia/Shanghai")
now := time.Now().In(loc)
// 日统计:今日、昨日、前日
todayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, loc)
todayEnd := todayStart.AddDate(0, 0, 1)
yesterdayStart := todayStart.AddDate(0, 0, -1)
yesterdayEnd := todayStart
dayBeforeStart := todayStart.AddDate(0, 0, -2)
dayBeforeEnd := yesterdayStart
daily := []types.PeriodConversionData{
l.calculateSubordinatePeriodConversion(parentAgentId, "今日", todayStart, todayEnd),
l.calculateSubordinatePeriodConversion(parentAgentId, "昨日", yesterdayStart, yesterdayEnd),
l.calculateSubordinatePeriodConversion(parentAgentId, "前日", dayBeforeStart, dayBeforeEnd),
}
// 周统计:本周、上周、上上周
weekdayOffset := int(now.Weekday())
if weekdayOffset == 0 {
weekdayOffset = 7 // 周日算作第7天
}
thisWeekStart := now.AddDate(0, 0, -(weekdayOffset - 1))
thisWeekStart = time.Date(thisWeekStart.Year(), thisWeekStart.Month(), thisWeekStart.Day(), 0, 0, 0, 0, loc)
thisWeekEnd := now
lastWeekStart := thisWeekStart.AddDate(0, 0, -7)
lastWeekEnd := thisWeekStart
lastTwoWeekStart := thisWeekStart.AddDate(0, 0, -14)
lastTwoWeekEnd := lastWeekStart
weekly := []types.PeriodConversionData{
l.calculateSubordinatePeriodConversion(parentAgentId, "本周", thisWeekStart, thisWeekEnd),
l.calculateSubordinatePeriodConversion(parentAgentId, "上周", lastWeekStart, lastWeekEnd),
l.calculateSubordinatePeriodConversion(parentAgentId, "上上周", lastTwoWeekStart, lastTwoWeekEnd),
}
// 月统计:本月、上月、前两月
thisMonthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, loc)
thisMonthEnd := now
lastMonthStart := thisMonthStart.AddDate(0, -1, 0)
lastMonthEnd := thisMonthStart
lastTwoMonthStart := thisMonthStart.AddDate(0, -2, 0)
lastTwoMonthEnd := lastMonthStart
monthly := []types.PeriodConversionData{
l.calculateSubordinatePeriodConversion(parentAgentId, "本月", thisMonthStart, thisMonthEnd),
l.calculateSubordinatePeriodConversion(parentAgentId, "上月", lastMonthStart, lastMonthEnd),
l.calculateSubordinatePeriodConversion(parentAgentId, "前两月", lastTwoMonthStart, lastTwoMonthEnd),
}
return types.ConversionRateData{
Daily: daily,
Weekly: weekly,
Monthly: monthly,
}
}
// calculateConversionRate 计算转化率
// agentId > 0 时统计该代理的转化率,否则统计 subordinateIds 列表的转化率
func (l *GetConversionRateLogic) calculateConversionRate(agentId int64, subordinateIds []int64) types.ConversionRateData {
// 使用Asia/Shanghai时区与数据库保持一致
loc, _ := time.LoadLocation("Asia/Shanghai")
now := time.Now().In(loc)
// 日统计:今日、昨日、前日
todayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, loc)
todayEnd := todayStart.AddDate(0, 0, 1)
yesterdayStart := todayStart.AddDate(0, 0, -1)
yesterdayEnd := todayStart
dayBeforeStart := todayStart.AddDate(0, 0, -2)
dayBeforeEnd := yesterdayStart
daily := []types.PeriodConversionData{
l.calculatePeriodConversion(agentId, subordinateIds, "今日", todayStart, todayEnd),
l.calculatePeriodConversion(agentId, subordinateIds, "昨日", yesterdayStart, yesterdayEnd),
l.calculatePeriodConversion(agentId, subordinateIds, "前日", dayBeforeStart, dayBeforeEnd),
}
// 周统计:本周、上周、上上周
// 本周本周一00:00:00 到现在
weekdayOffset := int(now.Weekday())
if weekdayOffset == 0 {
weekdayOffset = 7 // 周日算作第7天
}
// 计算本周一:当前日期减去(weekdayOffset-1)天
thisWeekStart := now.AddDate(0, 0, -(weekdayOffset - 1))
thisWeekStart = time.Date(thisWeekStart.Year(), thisWeekStart.Month(), thisWeekStart.Day(), 0, 0, 0, 0, loc)
thisWeekEnd := now
lastWeekStart := thisWeekStart.AddDate(0, 0, -7)
lastWeekEnd := thisWeekStart
lastTwoWeekStart := thisWeekStart.AddDate(0, 0, -14)
lastTwoWeekEnd := lastWeekStart
weekly := []types.PeriodConversionData{
l.calculatePeriodConversion(agentId, subordinateIds, "本周", thisWeekStart, thisWeekEnd),
l.calculatePeriodConversion(agentId, subordinateIds, "上周", lastWeekStart, lastWeekEnd),
l.calculatePeriodConversion(agentId, subordinateIds, "上上周", lastTwoWeekStart, lastTwoWeekEnd),
}
// 月统计:本月、上月、前两月
thisMonthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, loc)
thisMonthEnd := now
lastMonthStart := thisMonthStart.AddDate(0, -1, 0)
lastMonthEnd := thisMonthStart
lastTwoMonthStart := thisMonthStart.AddDate(0, -2, 0)
lastTwoMonthEnd := lastMonthStart
monthly := []types.PeriodConversionData{
l.calculatePeriodConversion(agentId, subordinateIds, "本月", thisMonthStart, thisMonthEnd),
l.calculatePeriodConversion(agentId, subordinateIds, "上月", lastMonthStart, lastMonthEnd),
l.calculatePeriodConversion(agentId, subordinateIds, "前两月", lastTwoMonthStart, lastTwoMonthEnd),
}
return types.ConversionRateData{
Daily: daily,
Weekly: weekly,
Monthly: monthly,
}
}
// calculatePeriodConversion 计算指定时间段的转化率数据
func (l *GetConversionRateLogic) calculatePeriodConversion(agentId int64, subordinateIds []int64, periodLabel string, startTime, endTime time.Time) types.PeriodConversionData {
// 构建 agent_order 查询条件
agentOrderBuilder := l.svcCtx.AgentOrderModel.SelectBuilder().
Where("del_state = ?", globalkey.DelStateNo).
Where("create_time >= ? AND create_time < ?", startTime, endTime)
if agentId > 0 {
// 统计我的转化率
agentOrderBuilder = agentOrderBuilder.Where("agent_id = ?", agentId)
} else if len(subordinateIds) > 0 {
// 统计下级转化率
agentOrderBuilder = agentOrderBuilder.Where(squirrel.Eq{"agent_id": subordinateIds})
} else {
// 没有数据
l.Infof("calculatePeriodConversion: 没有代理ID或下级IDperiodLabel=%s", periodLabel)
return types.PeriodConversionData{
PeriodLabel: periodLabel,
QueryUsers: 0,
PaidUsers: 0,
TotalAmount: 0,
QueryUserCount: 0,
PaidUserCount: 0,
}
}
// 添加调试日志
if agentId == 0 && len(subordinateIds) > 0 {
l.Infof("calculatePeriodConversion: 统计下级转化率periodLabel=%s, startTime=%v, endTime=%v, subordinateIds数量=%d",
periodLabel, startTime, endTime, len(subordinateIds))
}
// 查询所有代理订单
agentOrders, err := l.svcCtx.AgentOrderModel.FindAll(l.ctx, agentOrderBuilder, "")
if err != nil && !errors.Is(err, model.ErrNotFound) {
l.Errorf("查询代理订单失败: periodLabel=%s, err=%v", periodLabel, err)
return types.PeriodConversionData{
PeriodLabel: periodLabel,
QueryUsers: 0,
PaidUsers: 0,
TotalAmount: 0,
QueryUserCount: 0,
PaidUserCount: 0,
}
}
if len(agentOrders) == 0 {
if agentId == 0 && len(subordinateIds) > 0 {
l.Infof("calculatePeriodConversion: 未找到代理订单periodLabel=%s, startTime=%v, endTime=%v",
periodLabel, startTime, endTime)
}
return types.PeriodConversionData{
PeriodLabel: periodLabel,
QueryUsers: 0,
PaidUsers: 0,
TotalAmount: 0,
QueryUserCount: 0,
PaidUserCount: 0,
}
}
l.Infof("calculatePeriodConversion: 找到代理订单数量=%d, periodLabel=%s", len(agentOrders), periodLabel)
// 收集订单ID
orderIds := make([]int64, 0, len(agentOrders))
for _, ao := range agentOrders {
orderIds = append(orderIds, ao.OrderId)
}
// 查询订单信息
orderBuilder := l.svcCtx.OrderModel.SelectBuilder().
Where(squirrel.Eq{"id": orderIds}).
Where("del_state = ?", globalkey.DelStateNo)
orders, err := l.svcCtx.OrderModel.FindAll(l.ctx, orderBuilder, "")
if err != nil && !errors.Is(err, model.ErrNotFound) {
l.Errorf("查询订单失败: %v", err)
return types.PeriodConversionData{
PeriodLabel: periodLabel,
QueryUsers: 0,
PaidUsers: 0,
TotalAmount: 0,
QueryUserCount: 0,
PaidUserCount: 0,
}
}
// 统计查询订单数、付费订单数、用户数和总金额
var totalAmount float64
paidOrderCount := 0
queryUserSet := make(map[int64]bool)
paidUserSet := make(map[int64]bool)
for _, order := range orders {
// 查询用户数(所有订单的用户,去重)
queryUserSet[order.UserId] = true
// 付费订单数和总金额(只统计已支付的订单)
if order.Status == "paid" {
paidOrderCount++
paidUserSet[order.UserId] = true
totalAmount += order.Amount
}
}
// 查询订单数 = 所有订单数量
queryOrderCount := len(orders)
return types.PeriodConversionData{
PeriodLabel: periodLabel,
QueryUsers: int64(queryOrderCount), // 订单数量(保持向后兼容)
PaidUsers: int64(paidOrderCount), // 订单数量(保持向后兼容)
TotalAmount: totalAmount,
QueryUserCount: int64(len(queryUserSet)), // 用户数量(新增)
PaidUserCount: int64(len(paidUserSet)), // 用户数量(新增)
}
}
// calculateSubordinatePeriodConversion 计算指定时间段内下级转化率
// 结合使用agent_rebate表和agent_order表
// 1. 查询量通过agent_order表统计所有查询包括未付费的
// 2. 付费量和金额通过agent_rebate表统计只有付费的订单才会产生返佣
func (l *GetConversionRateLogic) calculateSubordinatePeriodConversion(parentAgentId int64, periodLabel string, startTime, endTime time.Time) types.PeriodConversionData {
// 1. 查询agent_rebate表获取所有曾经给当前用户产生返佣的source_agent_id这些代理在某个时间点是下级
// 不限制时间,获取所有历史返佣记录,用于确定哪些代理曾经是下级
rebateBuilder := l.svcCtx.AgentRebateModel.SelectBuilder().
Where("agent_id = ? AND del_state = ?", parentAgentId, globalkey.DelStateNo).
Where("source_agent_id != ?", parentAgentId)
allRebates, err := l.svcCtx.AgentRebateModel.FindAll(l.ctx, rebateBuilder, "")
if err != nil && !errors.Is(err, model.ErrNotFound) {
l.Errorf("查询返佣记录失败: periodLabel=%s, err=%v", periodLabel, err)
return types.PeriodConversionData{
PeriodLabel: periodLabel,
QueryUsers: 0,
PaidUsers: 0,
TotalAmount: 0,
QueryUserCount: 0,
PaidUserCount: 0,
}
}
// 收集所有曾经产生返佣的source_agent_id这些代理在某个时间点是下级
sourceAgentIdSet := make(map[int64]bool)
paidOrderIdSet := make(map[int64]bool) // 已付费的订单ID有返佣的订单
paidOrderIdToAmount := make(map[int64]float64) // 已付费订单的金额
for _, rebate := range allRebates {
sourceAgentIdSet[rebate.SourceAgentId] = true
// 如果返佣记录的创建时间在时间段内,说明该订单在时间段内已付费
if rebate.CreateTime.After(startTime) || rebate.CreateTime.Equal(startTime) {
if rebate.CreateTime.Before(endTime) {
paidOrderIdSet[rebate.OrderId] = true
}
}
}
if len(sourceAgentIdSet) == 0 {
l.Infof("calculateSubordinatePeriodConversion: 未找到返佣记录periodLabel=%s, startTime=%v, endTime=%v",
periodLabel, startTime, endTime)
return types.PeriodConversionData{
PeriodLabel: periodLabel,
QueryUsers: 0,
PaidUsers: 0,
TotalAmount: 0,
QueryUserCount: 0,
PaidUserCount: 0,
}
}
sourceAgentIds := make([]int64, 0, len(sourceAgentIdSet))
for agentId := range sourceAgentIdSet {
sourceAgentIds = append(sourceAgentIds, agentId)
}
l.Infof("calculateSubordinatePeriodConversion: periodLabel=%s, 曾经产生返佣的代理数量=%d", periodLabel, len(sourceAgentIds))
// 2. 查询agent_order表统计这些代理在时间段内的所有订单包括未付费的
agentOrderBuilder := l.svcCtx.AgentOrderModel.SelectBuilder().
Where("del_state = ?", globalkey.DelStateNo).
Where("create_time >= ? AND create_time < ?", startTime, endTime).
Where(squirrel.Eq{"agent_id": sourceAgentIds})
agentOrders, err := l.svcCtx.AgentOrderModel.FindAll(l.ctx, agentOrderBuilder, "")
if err != nil && !errors.Is(err, model.ErrNotFound) {
l.Errorf("查询代理订单失败: periodLabel=%s, err=%v", periodLabel, err)
return types.PeriodConversionData{
PeriodLabel: periodLabel,
QueryUsers: 0,
PaidUsers: 0,
TotalAmount: 0,
QueryUserCount: 0,
PaidUserCount: 0,
}
}
l.Infof("calculateSubordinatePeriodConversion: periodLabel=%s, 查询到代理订单数量=%d", periodLabel, len(agentOrders))
if len(agentOrders) == 0 {
return types.PeriodConversionData{
PeriodLabel: periodLabel,
QueryUsers: 0,
PaidUsers: 0,
TotalAmount: 0,
QueryUserCount: 0,
PaidUserCount: 0,
}
}
// 3. 通过order_id去重获取所有订单ID用于查询订单详情
orderIdSet := make(map[int64]bool)
orderIdToAgentOrder := make(map[int64]*model.AgentOrder)
for _, ao := range agentOrders {
orderIdSet[ao.OrderId] = true
// 如果同一个订单有多个agent_order记录保留金额更大的
if existing, exists := orderIdToAgentOrder[ao.OrderId]; exists {
if ao.OrderAmount > existing.OrderAmount {
orderIdToAgentOrder[ao.OrderId] = ao
}
} else {
orderIdToAgentOrder[ao.OrderId] = ao
}
}
orderIds := make([]int64, 0, len(orderIdSet))
for orderId := range orderIdSet {
orderIds = append(orderIds, orderId)
}
// 4. 查询订单信息
orderBuilder := l.svcCtx.OrderModel.SelectBuilder().
Where(squirrel.Eq{"id": orderIds}).
Where("del_state = ?", globalkey.DelStateNo)
orders, err := l.svcCtx.OrderModel.FindAll(l.ctx, orderBuilder, "")
if err != nil && !errors.Is(err, model.ErrNotFound) {
l.Errorf("查询订单失败: %v", err)
return types.PeriodConversionData{
PeriodLabel: periodLabel,
QueryUsers: 0,
PaidUsers: 0,
TotalAmount: 0,
QueryUserCount: 0,
PaidUserCount: 0,
}
}
// 5. 查询时间段内的返佣记录,获取已付费订单的金额
rebateBuilder = l.svcCtx.AgentRebateModel.SelectBuilder().
Where("agent_id = ? AND del_state = ?", parentAgentId, globalkey.DelStateNo).
Where("source_agent_id != ?", parentAgentId).
Where("create_time >= ? AND create_time < ?", startTime, endTime).
Where(squirrel.Eq{"order_id": orderIds})
rebates, err := l.svcCtx.AgentRebateModel.FindAll(l.ctx, rebateBuilder, "")
if err != nil && !errors.Is(err, model.ErrNotFound) {
l.Errorf("查询返佣记录失败: %v", err)
return types.PeriodConversionData{
PeriodLabel: periodLabel,
QueryUsers: 0,
PaidUsers: 0,
TotalAmount: 0,
QueryUserCount: 0,
PaidUserCount: 0,
}
}
// 记录已付费订单的金额使用agent_order的order_amount
for _, rebate := range rebates {
if ao, exists := orderIdToAgentOrder[rebate.OrderId]; exists {
paidOrderIdToAmount[rebate.OrderId] = ao.OrderAmount
}
}
// 6. 统计查询订单数、付费订单数、用户数和总金额
var totalAmount float64
paidOrderCount := 0
queryUserSet := make(map[int64]bool)
paidUserSet := make(map[int64]bool)
for _, order := range orders {
// 查询用户数(所有订单的用户,去重)
queryUserSet[order.UserId] = true
// 付费订单数和总金额只统计已付费的订单即order_id在paidOrderIdToAmount中
if _, isPaid := paidOrderIdToAmount[order.Id]; isPaid {
paidOrderCount++
paidUserSet[order.UserId] = true
// 使用agent_order的order_amount用户实际支付金额
totalAmount += paidOrderIdToAmount[order.Id]
}
}
// 查询订单数 = 所有订单数量
queryOrderCount := len(orders)
l.Infof("calculateSubordinatePeriodConversion: periodLabel=%s, 查询订单数=%d, 付费订单数=%d, 查询用户数=%d, 付费用户数=%d, 总金额=%.2f",
periodLabel, queryOrderCount, paidOrderCount, len(queryUserSet), len(paidUserSet), totalAmount)
return types.PeriodConversionData{
PeriodLabel: periodLabel,
QueryUsers: int64(queryOrderCount), // 订单数量(保持向后兼容)
PaidUsers: int64(paidOrderCount), // 订单数量(保持向后兼容)
TotalAmount: totalAmount,
QueryUserCount: int64(len(queryUserSet)), // 用户数量(新增)
PaidUserCount: int64(len(paidUserSet)), // 用户数量(新增)
}
}

View File

@@ -4,13 +4,14 @@ import (
"context"
"database/sql"
"fmt"
"time"
"ycc-server/app/main/model"
"ycc-server/common/ctxdata"
"ycc-server/common/globalkey"
"ycc-server/common/tool"
"ycc-server/common/xerr"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/stores/sqlx"
"ycc-server/app/main/api/internal/svc"
"ycc-server/app/main/api/internal/types"
@@ -32,7 +33,7 @@ func NewGetInviteLinkLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Get
}
}
func (l *GetInviteLinkLogic) GetInviteLink() (resp *types.GetInviteLinkResp, err error) {
func (l *GetInviteLinkLogic) GetInviteLink(req *types.GetInviteLinkReq) (resp *types.GetInviteLinkResp, err error) {
// 1. 获取当前代理信息
userID, err := ctxdata.GetUidFromCtx(l.ctx)
if err != nil {
@@ -47,63 +48,109 @@ func (l *GetInviteLinkLogic) GetInviteLink() (resp *types.GetInviteLinkResp, err
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询代理信息失败, %v", err)
}
// 2. 为邀请链接生成一个邀请码(每次调用都生成新的,不过期)
// 这样代理可以分享这个链接,用户通过链接注册时会使用这个邀请码
var inviteCode string
err = l.svcCtx.AgentInviteCodeModel.Trans(l.ctx, func(transCtx context.Context, session sqlx.Session) error {
// 生成8位随机邀请码
code := tool.Krand(8, tool.KC_RAND_KIND_ALL)
maxRetries := 10
for retry := 0; retry < maxRetries; retry++ {
_, err := l.svcCtx.AgentInviteCodeModel.FindOneByCode(transCtx, code)
if err != nil {
if errors.Is(err, model.ErrNotFound) {
break
}
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "检查邀请码失败, %v", err)
}
code = tool.Krand(8, tool.KC_RAND_KIND_ALL)
if retry == maxRetries-1 {
return errors.Wrapf(xerr.NewErrMsg("生成邀请码失败,请重试"), "")
}
}
// 创建邀请码记录(用于链接,不过期)
inviteCodeRecord := &model.AgentInviteCode{
Code: code,
AgentId: sql.NullInt64{Int64: agent.Id, Valid: true},
TargetLevel: 1, // 代理发放的邀请码,目标等级为普通代理
Status: 0, // 未使用
ExpireTime: sql.NullTime{Valid: false}, // 不过期
Remark: sql.NullString{String: "邀请链接生成", Valid: true},
}
_, err := l.svcCtx.AgentInviteCodeModel.Insert(transCtx, session, inviteCodeRecord)
if err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建邀请码失败, %v", err)
}
inviteCode = code
return nil
})
// 2. 验证邀请码参数
if req.InviteCode == "" {
return nil, errors.Wrapf(xerr.NewErrMsg("邀请码不能为空"), "")
}
// 3. 查询邀请码是否存在且属于当前代理
inviteCodeRecord, err := l.svcCtx.AgentInviteCodeModel.FindOneByCode(l.ctx, req.InviteCode)
if err != nil {
return nil, err
if errors.Is(err, model.ErrNotFound) {
return nil, errors.Wrapf(xerr.NewErrMsg("邀请码不存在"), "")
}
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询邀请码失败, %v", err)
}
// 3. 生成邀请链接
// 使用配置中的前端域名,如果没有则使用默认值
frontendDomain := l.svcCtx.Config.AdminPromotion.URLDomain
if frontendDomain == "" {
frontendDomain = "https://example.com" // 默认值,需要配置
// 4. 验证邀请码是否属于当前代理
if !inviteCodeRecord.AgentId.Valid || inviteCodeRecord.AgentId.Int64 != agent.Id {
return nil, errors.Wrapf(xerr.NewErrMsg("无权使用此邀请码"), "")
}
inviteLink := fmt.Sprintf("%s/register?invite_code=%s", frontendDomain, inviteCode)
// 4. 生成二维码URL使用ImageService
qrCodeUrl := fmt.Sprintf("%s/api/v1/image/qrcode?type=invitation&content=%s", frontendDomain, inviteLink)
// 5. 验证邀请码状态
if inviteCodeRecord.Status != 0 {
if inviteCodeRecord.Status == 1 {
return nil, errors.Wrapf(xerr.NewErrMsg("邀请码已使用"), "")
}
return nil, errors.Wrapf(xerr.NewErrMsg("邀请码已失效"), "")
}
// 6. 验证邀请码是否过期
if inviteCodeRecord.ExpireTime.Valid && inviteCodeRecord.ExpireTime.Time.Before(time.Now()) {
return nil, errors.Wrapf(xerr.NewErrMsg("邀请码已过期"), "")
}
// 7. 使用默认target_path如果未提供
targetPath := req.TargetPath
if targetPath == "" {
targetPath = fmt.Sprintf("/register?invite_code=%s", req.InviteCode)
}
// 8. 生成短链类型2=邀请好友)
shortLink, err := l.createInviteShortLink(inviteCodeRecord.Id, req.InviteCode, targetPath)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "生成短链失败, %v", err)
}
return &types.GetInviteLinkResp{
InviteLink: inviteLink,
QrCodeUrl: qrCodeUrl,
InviteLink: shortLink,
}, nil
}
// createInviteShortLink 创建邀请好友短链
func (l *GetInviteLinkLogic) createInviteShortLink(inviteCodeId int64, inviteCode, targetPath string) (string, error) {
promotionConfig := l.svcCtx.Config.Promotion
// 如果没有配置推广域名,返回空字符串(保持向后兼容)
if promotionConfig.PromotionDomain == "" {
l.Errorf("推广域名未配置,返回空链接")
return "", nil
}
// 先查询是否已存在短链
existingShortLink, err := l.svcCtx.AgentShortLinkModel.FindOneByInviteCodeIdTypeDelState(l.ctx, sql.NullInt64{Int64: inviteCodeId, Valid: true}, 2, globalkey.DelStateNo)
if err == nil && existingShortLink != nil {
// 已存在短链,直接返回
return fmt.Sprintf("%s/s/%s", promotionConfig.PromotionDomain, existingShortLink.ShortCode), nil
}
if err != nil && !errors.Is(err, model.ErrNotFound) {
return "", errors.Wrapf(err, "查询短链失败")
}
// 生成短链标识6位随机字符串大小写字母+数字)
var shortCode string
maxRetries := 10 // 最大重试次数
for retry := 0; retry < maxRetries; retry++ {
shortCode = tool.Krand(6, tool.KC_RAND_KIND_ALL)
// 检查短链标识是否已存在
_, err := l.svcCtx.AgentShortLinkModel.FindOneByShortCodeDelState(l.ctx, shortCode, globalkey.DelStateNo)
if err != nil {
if errors.Is(err, model.ErrNotFound) {
// 短链标识不存在,可以使用
break
}
return "", errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "检查短链标识失败, %v", err)
}
// 短链标识已存在,继续生成
if retry == maxRetries-1 {
return "", errors.Wrapf(xerr.NewErrMsg("生成短链失败,请重试"), "")
}
}
// 创建短链记录类型2=邀请好友)
shortLink := &model.AgentShortLink{
Type: 2, // 邀请好友
InviteCodeId: sql.NullInt64{Int64: inviteCodeId, Valid: inviteCodeId > 0},
InviteCode: sql.NullString{String: inviteCode, Valid: inviteCode != ""},
ShortCode: shortCode,
TargetPath: targetPath,
PromotionDomain: promotionConfig.PromotionDomain,
}
_, err = l.svcCtx.AgentShortLinkModel.Insert(l.ctx, nil, shortLink)
if err != nil {
return "", errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "保存短链失败, %v", err)
}
return fmt.Sprintf("%s/s/%s", promotionConfig.PromotionDomain, shortCode), nil
}

View File

@@ -0,0 +1,188 @@
package agent
import (
"context"
"strconv"
"ycc-server/app/main/model"
"ycc-server/common/ctxdata"
"ycc-server/common/xerr"
"github.com/pkg/errors"
"ycc-server/app/main/api/internal/svc"
"ycc-server/app/main/api/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type GetLevelPrivilegeLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewGetLevelPrivilegeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetLevelPrivilegeLogic {
return &GetLevelPrivilegeLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *GetLevelPrivilegeLogic) GetLevelPrivilege() (resp *types.GetLevelPrivilegeResp, err error) {
// 1. 获取当前代理等级
userID, err := ctxdata.GetUidFromCtx(l.ctx)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取用户信息失败, %v", err)
}
var currentLevel int64 = 1 // 默认普通代理
agent, err := l.svcCtx.AgentModel.FindOneByUserId(l.ctx, userID)
if err != nil && !errors.Is(err, model.ErrNotFound) {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询代理信息失败, %v", err)
}
if agent != nil {
currentLevel = agent.Level
}
// 获取配置值的辅助函数
getConfigFloat := func(key string, defaultValue float64) float64 {
config, err := l.svcCtx.AgentConfigModel.FindOneByConfigKey(l.ctx, key)
if err != nil {
return defaultValue
}
value, err := strconv.ParseFloat(config.ConfigValue, 64)
if err != nil {
return defaultValue
}
return value
}
// 获取等级加成配置
level1Bonus := getConfigFloat("level_1_bonus", 6.0)
level2Bonus := getConfigFloat("level_2_bonus", 3.0)
level3Bonus := getConfigFloat("level_3_bonus", 0.0)
// 获取当前等级的加成
var currentBonus float64
switch currentLevel {
case 1:
currentBonus = level1Bonus
case 2:
currentBonus = level2Bonus
case 3:
currentBonus = level3Bonus
default:
currentBonus = level1Bonus
}
// 获取升级返佣配置
upgradeToGoldRebate := getConfigFloat("upgrade_to_gold_rebate", 139.0)
upgradeToDiamondRebate := getConfigFloat("upgrade_to_diamond_rebate", 680.0)
// 获取直接上级返佣配置
directParentAmountDiamond := getConfigFloat("direct_parent_amount_diamond", 6.0)
directParentAmountGold := getConfigFloat("direct_parent_amount_gold", 3.0)
directParentAmountNormal := getConfigFloat("direct_parent_amount_normal", 2.0)
// 构建各等级特权信息
levels := []types.LevelPrivilegeItem{
{
Level: 1,
LevelName: "普通代理",
LevelBonus: level1Bonus,
PriceReduction: l.calculatePriceReduction(currentBonus, level1Bonus),
PromoteRebate: l.buildPromoteRebateDesc(1, directParentAmountDiamond, directParentAmountGold, directParentAmountNormal),
MaxPromoteRebate: directParentAmountDiamond, // 普通代理最高返佣是直接上级是钻石时的返佣
UpgradeRebate: l.buildUpgradeRebateDesc(1, upgradeToGoldRebate, upgradeToDiamondRebate),
Privileges: []string{
"基础代理特权",
"可生成推广链接",
"享受推广返佣",
"可邀请下级代理",
},
CanUpgradeSubordinate: false,
},
{
Level: 2,
LevelName: "黄金代理",
LevelBonus: level2Bonus,
PriceReduction: l.calculatePriceReduction(currentBonus, level2Bonus),
PromoteRebate: l.buildPromoteRebateDesc(2, directParentAmountDiamond, directParentAmountGold, directParentAmountNormal),
MaxPromoteRebate: level2Bonus, // 黄金代理最高返佣是等级加成全部返佣给钻石上级
UpgradeRebate: l.buildUpgradeRebateDesc(2, upgradeToGoldRebate, upgradeToDiamondRebate),
Privileges: []string{
"高级代理特权",
"更高的返佣比例",
"享受推广返佣",
"可邀请下级代理",
"更多推广权益",
},
CanUpgradeSubordinate: false,
},
{
Level: 3,
LevelName: "钻石代理",
LevelBonus: level3Bonus,
PriceReduction: l.calculatePriceReduction(currentBonus, level3Bonus),
PromoteRebate: l.buildPromoteRebateDesc(3, directParentAmountDiamond, directParentAmountGold, directParentAmountNormal),
MaxPromoteRebate: 0, // 钻石代理无返佣
UpgradeRebate: l.buildUpgradeRebateDesc(3, upgradeToGoldRebate, upgradeToDiamondRebate),
Privileges: []string{
"尊享代理特权",
"最高返佣比例",
"独立团队管理",
"可调整下级级别",
"可免费升级下级为黄金代理",
},
CanUpgradeSubordinate: true,
},
}
return &types.GetLevelPrivilegeResp{
Levels: levels,
}, nil
}
// buildPromoteRebateDesc 构建推广返佣说明
func (l *GetLevelPrivilegeLogic) buildPromoteRebateDesc(level int64, diamondAmount, goldAmount, normalAmount float64) string {
switch level {
case 1: // 普通代理等级加成6元
return "更高的推广返佣,最高可达¥" + l.formatFloat(diamondAmount)
case 2: // 黄金代理等级加成3元
return "更高的推广返佣,最高可达¥" + l.formatFloat(3.0)
case 3: // 钻石代理等级加成0元
return "无等级加成,享受最低底价"
default:
return ""
}
}
// buildUpgradeRebateDesc 构建下级升级返佣说明
func (l *GetLevelPrivilegeLogic) buildUpgradeRebateDesc(level int64, goldRebate, diamondRebate float64) string {
switch level {
case 1: // 普通代理
return "下级升级为黄金代理返佣¥" + l.formatFloat(goldRebate) + ",升级为钻石代理返佣¥" + l.formatFloat(diamondRebate)
case 2: // 黄金代理
return "下级升级为钻石代理返佣¥" + l.formatFloat(diamondRebate)
case 3: // 钻石代理
return "可免费升级下级为黄金代理"
default:
return ""
}
}
// calculatePriceReduction 计算底价降低(相对于当前等级)
func (l *GetLevelPrivilegeLogic) calculatePriceReduction(currentBonus, targetBonus float64) float64 {
reduction := currentBonus - targetBonus
if reduction < 0 {
return 0
}
return reduction
}
// formatFloat 格式化浮点数去掉末尾的0
func (l *GetLevelPrivilegeLogic) formatFloat(f float64) string {
s := strconv.FormatFloat(f, 'f', -1, 64)
return s
}

View File

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

View File

@@ -6,6 +6,7 @@ import (
"ycc-server/common/ctxdata"
"ycc-server/common/globalkey"
"ycc-server/common/xerr"
"ycc-server/pkg/lzkit/crypto"
"github.com/pkg/errors"
@@ -46,8 +47,14 @@ func (l *GetRebateListLogic) GetRebateList(req *types.GetRebateListReq) (resp *t
// 2. 构建查询条件
builder := l.svcCtx.AgentRebateModel.SelectBuilder().
Where("agent_id = ? AND del_state = ?", agent.Id, globalkey.DelStateNo).
OrderBy("create_time DESC")
Where("agent_id = ? AND del_state = ?", agent.Id, globalkey.DelStateNo)
// 如果指定了返佣类型则按类型筛选1, 2, 3
if req.RebateType != nil {
builder = builder.Where("rebate_type = ?", *req.RebateType)
}
builder = builder.OrderBy("create_time DESC")
// 3. 分页查询
page := req.Page
@@ -61,7 +68,7 @@ func (l *GetRebateListLogic) GetRebateList(req *types.GetRebateListReq) (resp *t
offset := (page - 1) * pageSize
// 4. 查询总数
total, err := l.svcCtx.AgentRebateModel.FindCount(l.ctx, builder, "")
total, err := l.svcCtx.AgentRebateModel.FindCount(l.ctx, builder, "id")
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询返佣总数失败, %v", err)
}
@@ -76,13 +83,41 @@ func (l *GetRebateListLogic) GetRebateList(req *types.GetRebateListReq) (resp *t
// 6. 组装响应
var list []types.RebateItem
for _, rebate := range rebates {
// 查询订单号
orderNo := ""
if rebate.OrderId > 0 {
order, err := l.svcCtx.OrderModel.FindOne(l.ctx, rebate.OrderId)
if err == nil {
orderNo = order.OrderNo
}
}
// 查询来源代理手机号和等级
sourceAgentMobile := ""
sourceAgentLevel := int64(0)
if rebate.SourceAgentId > 0 {
sourceAgent, err := l.svcCtx.AgentModel.FindOne(l.ctx, rebate.SourceAgentId)
if err == nil {
if sourceAgent.Mobile != "" {
decrypted, err := crypto.DecryptMobile(sourceAgent.Mobile, l.svcCtx.Config.Encrypt.SecretKey)
if err == nil {
sourceAgentMobile = decrypted
}
}
sourceAgentLevel = sourceAgent.Level
}
}
list = append(list, types.RebateItem{
Id: rebate.Id,
SourceAgentId: rebate.SourceAgentId,
OrderId: rebate.OrderId,
RebateType: rebate.RebateType,
Amount: rebate.RebateAmount,
CreateTime: rebate.CreateTime.Format("2006-01-02 15:04:05"),
Id: rebate.Id,
SourceAgentId: rebate.SourceAgentId,
SourceAgentMobile: sourceAgentMobile,
SourceAgentLevel: sourceAgentLevel,
OrderId: rebate.OrderId,
OrderNo: orderNo,
RebateType: rebate.RebateType,
Amount: rebate.RebateAmount,
CreateTime: rebate.CreateTime.Format("2006-01-02 15:04:05"),
})
}

View File

@@ -2,8 +2,10 @@ package agent
import (
"context"
"time"
"ycc-server/app/main/model"
"ycc-server/common/ctxdata"
"ycc-server/common/globalkey"
"ycc-server/common/xerr"
"github.com/pkg/errors"
@@ -49,10 +51,77 @@ func (l *GetRevenueInfoLogic) GetRevenueInfo() (resp *types.GetRevenueInfoResp,
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询钱包信息失败, %v", err)
}
// 获取当前时间
now := time.Now()
// 今日开始时间00:00:00
todayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
// 本月开始时间1号 00:00:00
monthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
// 3. 统计佣金总额(从 agent_commission 表统计)
commissionBuilder := l.svcCtx.AgentCommissionModel.SelectBuilder().
Where("agent_id = ? AND del_state = ?", agent.Id, globalkey.DelStateNo)
commissionTotal, _ := l.svcCtx.AgentCommissionModel.FindSum(l.ctx, commissionBuilder, "amount")
// 3.1 统计佣金今日收益
commissionTodayBuilder := l.svcCtx.AgentCommissionModel.SelectBuilder().
Where("agent_id = ? AND del_state = ? AND create_time >= ?", agent.Id, globalkey.DelStateNo, todayStart)
commissionToday, _ := l.svcCtx.AgentCommissionModel.FindSum(l.ctx, commissionTodayBuilder, "amount")
// 3.2 统计佣金本月收益
commissionMonthBuilder := l.svcCtx.AgentCommissionModel.SelectBuilder().
Where("agent_id = ? AND del_state = ? AND create_time >= ?", agent.Id, globalkey.DelStateNo, monthStart)
commissionMonth, _ := l.svcCtx.AgentCommissionModel.FindSum(l.ctx, commissionMonthBuilder, "amount")
// 4. 统计返佣总额(包括推广返佣和升级返佣)
// 4.1 统计推广返佣(从 agent_rebate 表)
rebateBuilder := l.svcCtx.AgentRebateModel.SelectBuilder().
Where("agent_id = ? AND del_state = ?", agent.Id, globalkey.DelStateNo)
promoteRebateTotal, _ := l.svcCtx.AgentRebateModel.FindSum(l.ctx, rebateBuilder, "rebate_amount")
// 4.2 统计升级返佣(从 agent_upgrade 表,查询 rebate_agent_id = 当前代理ID 且 status = 2已完成且 upgrade_type = 1自主付费的记录
// 注意只要返佣给自己的都要统计不管升级后是否脱离关系rebate_agent_id 记录的是升级时的原直接上级)
upgradeRebateBuilder := l.svcCtx.AgentUpgradeModel.SelectBuilder().
Where("rebate_agent_id IS NOT NULL AND rebate_agent_id = ? AND status = ? AND upgrade_type = ? AND del_state = ?",
agent.Id, 2, 1, globalkey.DelStateNo)
upgradeRebateTotal, _ := l.svcCtx.AgentUpgradeModel.FindSum(l.ctx, upgradeRebateBuilder, "rebate_amount")
rebateTotal := promoteRebateTotal + upgradeRebateTotal
// 4.3 统计返佣今日收益
// 推广返佣今日
promoteRebateTodayBuilder := l.svcCtx.AgentRebateModel.SelectBuilder().
Where("agent_id = ? AND del_state = ? AND create_time >= ?", agent.Id, globalkey.DelStateNo, todayStart)
promoteRebateToday, _ := l.svcCtx.AgentRebateModel.FindSum(l.ctx, promoteRebateTodayBuilder, "rebate_amount")
// 升级返佣今日
upgradeRebateTodayBuilder := l.svcCtx.AgentUpgradeModel.SelectBuilder().
Where("rebate_agent_id IS NOT NULL AND rebate_agent_id = ? AND status = ? AND upgrade_type = ? AND del_state = ? AND create_time >= ?",
agent.Id, 2, 1, globalkey.DelStateNo, todayStart)
upgradeRebateToday, _ := l.svcCtx.AgentUpgradeModel.FindSum(l.ctx, upgradeRebateTodayBuilder, "rebate_amount")
rebateToday := promoteRebateToday + upgradeRebateToday
// 4.4 统计返佣本月收益
// 推广返佣本月
promoteRebateMonthBuilder := l.svcCtx.AgentRebateModel.SelectBuilder().
Where("agent_id = ? AND del_state = ? AND create_time >= ?", agent.Id, globalkey.DelStateNo, monthStart)
promoteRebateMonth, _ := l.svcCtx.AgentRebateModel.FindSum(l.ctx, promoteRebateMonthBuilder, "rebate_amount")
// 升级返佣本月
upgradeRebateMonthBuilder := l.svcCtx.AgentUpgradeModel.SelectBuilder().
Where("rebate_agent_id IS NOT NULL AND rebate_agent_id = ? AND status = ? AND upgrade_type = ? AND del_state = ? AND create_time >= ?",
agent.Id, 2, 1, globalkey.DelStateNo, monthStart)
upgradeRebateMonth, _ := l.svcCtx.AgentUpgradeModel.FindSum(l.ctx, upgradeRebateMonthBuilder, "rebate_amount")
rebateMonth := promoteRebateMonth + upgradeRebateMonth
return &types.GetRevenueInfoResp{
Balance: wallet.Balance,
FrozenBalance: wallet.FrozenBalance,
TotalEarnings: wallet.TotalEarnings,
Balance: wallet.Balance,
FrozenBalance: wallet.FrozenBalance,
TotalEarnings: wallet.TotalEarnings,
WithdrawnAmount: wallet.WithdrawnAmount,
CommissionTotal: commissionTotal, // 佣金累计总收益(推广订单获得的佣金)
CommissionToday: commissionToday, // 佣金今日收益
CommissionMonth: commissionMonth, // 佣金本月收益
RebateTotal: rebateTotal, // 返佣累计总收益(包括推广返佣和升级返佣)
RebateToday: rebateToday, // 返佣今日收益
RebateMonth: rebateMonth, // 返佣本月收益
}, nil
}

View File

@@ -0,0 +1,404 @@
package agent
import (
"context"
"sort"
"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/Masterminds/squirrel"
"github.com/pkg/errors"
"ycc-server/app/main/api/internal/svc"
"ycc-server/app/main/api/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type GetSubordinateContributionDetailLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewGetSubordinateContributionDetailLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetSubordinateContributionDetailLogic {
return &GetSubordinateContributionDetailLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *GetSubordinateContributionDetailLogic) GetSubordinateContributionDetail(req *types.GetSubordinateContributionDetailReq) (resp *types.GetSubordinateContributionDetailResp, err error) {
userID, err := ctxdata.GetUidFromCtx(l.ctx)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取用户信息失败, %v", err)
}
// 1. 获取当前代理信息
agent, err := l.svcCtx.AgentModel.FindOneByUserId(l.ctx, userID)
if err != nil {
if errors.Is(err, model.ErrNotFound) {
return nil, errors.Wrapf(xerr.NewErrMsg("您不是代理"), "")
}
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询代理信息失败, %v", err)
}
// 2. 验证下级代理是否存在,并且确实是当前代理的下级(直接或间接)
subordinate, err := l.svcCtx.AgentModel.FindOne(l.ctx, req.SubordinateId)
if err != nil {
if errors.Is(err, model.ErrNotFound) {
return nil, errors.Wrapf(xerr.NewErrMsg("下级代理不存在"), "")
}
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询下级代理信息失败, %v", err)
}
// 3. 验证下级关系(递归检查是否是下级)
isSubordinate := l.isSubordinate(agent.Id, req.SubordinateId)
if !isSubordinate {
return nil, errors.Wrapf(xerr.NewErrMsg("该代理不是您的下级"), "")
}
// 4. 解密手机号
mobile := ""
if subordinate.Mobile != "" {
decrypted, err := crypto.DecryptMobile(subordinate.Mobile, l.svcCtx.Config.Encrypt.SecretKey)
if err == nil {
mobile = decrypted
}
}
// 5. 获取等级名称
levelName := ""
switch subordinate.Level {
case 1:
levelName = "普通"
case 2:
levelName = "黄金"
case 3:
levelName = "钻石"
}
// 6. 计算时间范围
now := time.Now()
todayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
monthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
// 7. 统计订单数据(仅统计有返佣给当前用户的订单)
// 通过 agent_rebate 表统计 DISTINCT order_id
rebateBaseBuilder := l.svcCtx.AgentRebateModel.SelectBuilder().
Where("agent_id = ? AND source_agent_id = ? AND del_state = ?", agent.Id, req.SubordinateId, globalkey.DelStateNo)
// 总订单量去重订单ID
totalOrders := l.countDistinctOrders(l.ctx, rebateBaseBuilder)
// 今日订单量
todayRebateBuilder := rebateBaseBuilder.Where("create_time >= ?", todayStart)
todayOrders := l.countDistinctOrders(l.ctx, todayRebateBuilder)
// 月订单量
monthRebateBuilder := rebateBaseBuilder.Where("create_time >= ?", monthStart)
monthOrders := l.countDistinctOrders(l.ctx, monthRebateBuilder)
// 8. 统计返佣金额
totalRebateAmount, _ := l.svcCtx.AgentRebateModel.FindSum(l.ctx, rebateBaseBuilder, "rebate_amount")
todayRebateAmount, _ := l.svcCtx.AgentRebateModel.FindSum(l.ctx, todayRebateBuilder, "rebate_amount")
monthRebateAmount, _ := l.svcCtx.AgentRebateModel.FindSum(l.ctx, monthRebateBuilder, "rebate_amount")
// 9. 统计邀请数据
inviteBaseBuilder := l.svcCtx.AgentRelationModel.SelectBuilder().
Where("parent_id = ? AND relation_type = ? AND del_state = ?", req.SubordinateId, 1, globalkey.DelStateNo)
totalInvites, _ := l.svcCtx.AgentRelationModel.FindCount(l.ctx, inviteBaseBuilder, "id")
todayInviteBuilder := inviteBaseBuilder.Where("create_time >= ?", todayStart)
todayInvites, _ := l.svcCtx.AgentRelationModel.FindCount(l.ctx, todayInviteBuilder, "id")
monthInviteBuilder := inviteBaseBuilder.Where("create_time >= ?", monthStart)
monthInvites, _ := l.svcCtx.AgentRelationModel.FindCount(l.ctx, monthInviteBuilder, "id")
// 10. 分页查询订单列表或邀请列表
page := req.Page
if page <= 0 {
page = 1
}
pageSize := req.PageSize
if pageSize <= 0 {
pageSize = 20
}
var orderList []types.OrderItem
var inviteList []types.InviteItem
var orderListTotal int64
var inviteListTotal int64
tabType := req.TabType
if tabType == "" {
tabType = "order" // 默认显示订单列表
}
if tabType == "order" {
// 查询订单列表(仅显示有返佣的订单)
orderList, orderListTotal, err = l.getOrderList(l.ctx, agent.Id, req.SubordinateId, page, pageSize)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询订单列表失败, %v", err)
}
} else if tabType == "invite" {
// 查询邀请列表
inviteList, inviteListTotal, err = l.getInviteList(l.ctx, req.SubordinateId, page, pageSize)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询邀请列表失败, %v", err)
}
}
// 11. 组装响应
resp = &types.GetSubordinateContributionDetailResp{
Mobile: mobile,
LevelName: levelName,
CreateTime: subordinate.CreateTime.Format("2006-01-02 15:04:05"),
OrderStats: types.OrderStatistics{
TotalOrders: totalOrders,
MonthOrders: monthOrders,
TodayOrders: todayOrders,
},
RebateStats: types.RebateStatistics{
TotalRebateAmount: totalRebateAmount,
TodayRebateAmount: todayRebateAmount,
MonthRebateAmount: monthRebateAmount,
},
InviteStats: types.InviteStatistics{
TotalInvites: totalInvites,
TodayInvites: todayInvites,
MonthInvites: monthInvites,
},
OrderList: orderList,
InviteList: inviteList,
OrderListTotal: orderListTotal,
InviteListTotal: inviteListTotal,
}
return resp, nil
}
// isSubordinate 递归检查 targetId 是否是 parentId 的下级(直接或间接)
func (l *GetSubordinateContributionDetailLogic) isSubordinate(parentId, targetId int64) bool {
// 查询直接下级
builder := l.svcCtx.AgentRelationModel.SelectBuilder().
Where("parent_id = ? AND relation_type = ? AND del_state = ?", parentId, 1, globalkey.DelStateNo)
relations, err := l.svcCtx.AgentRelationModel.FindAll(l.ctx, builder, "")
if err != nil {
return false
}
for _, relation := range relations {
// 如果是直接下级,返回 true
if relation.ChildId == targetId {
return true
}
// 递归检查间接下级
if l.isSubordinate(relation.ChildId, targetId) {
return true
}
}
return false
}
// countDistinctOrders 统计去重的订单数量(通过返佣记录)
func (l *GetSubordinateContributionDetailLogic) countDistinctOrders(ctx context.Context, builder squirrel.SelectBuilder) int64 {
// 查询所有返佣记录在内存中去重订单ID
rebates, err := l.svcCtx.AgentRebateModel.FindAll(ctx, builder, "")
if err != nil {
return 0
}
orderIdSet := make(map[int64]bool)
for _, rebate := range rebates {
orderIdSet[rebate.OrderId] = true
}
return int64(len(orderIdSet))
}
// getOrderList 获取订单列表(仅显示有返佣的订单)
func (l *GetSubordinateContributionDetailLogic) getOrderList(ctx context.Context, agentId, subordinateId int64, page, pageSize int64) ([]types.OrderItem, int64, error) {
// 1. 查询所有返佣记录
rebateBuilder := l.svcCtx.AgentRebateModel.SelectBuilder().
Where("agent_id = ? AND source_agent_id = ? AND del_state = ?", agentId, subordinateId, globalkey.DelStateNo).
OrderBy("create_time DESC")
// 查询总数去重订单ID
totalBuilder := l.svcCtx.AgentRebateModel.SelectBuilder().
Where("agent_id = ? AND source_agent_id = ? AND del_state = ?", agentId, subordinateId, globalkey.DelStateNo)
total := l.countDistinctOrders(ctx, totalBuilder)
// 查询所有返佣记录
allRebates, err := l.svcCtx.AgentRebateModel.FindAll(ctx, rebateBuilder, "")
if err != nil {
return nil, 0, err
}
if len(allRebates) == 0 {
return []types.OrderItem{}, total, nil
}
// 2. 在内存中去重订单ID并按创建时间排序
type OrderRebateInfo struct {
OrderId int64
RebateId int64
ProductId int64
RebateAmount float64
CreateTime time.Time
}
orderMap := make(map[int64]*OrderRebateInfo) // orderId -> 最新的返佣信息
for _, rebate := range allRebates {
if existing, ok := orderMap[rebate.OrderId]; ok {
// 如果已存在,保留创建时间最新的
if rebate.CreateTime.After(existing.CreateTime) {
orderMap[rebate.OrderId] = &OrderRebateInfo{
OrderId: rebate.OrderId,
RebateId: rebate.Id,
ProductId: rebate.ProductId,
RebateAmount: rebate.RebateAmount,
CreateTime: rebate.CreateTime,
}
}
} else {
orderMap[rebate.OrderId] = &OrderRebateInfo{
OrderId: rebate.OrderId,
RebateId: rebate.Id,
ProductId: rebate.ProductId,
RebateAmount: rebate.RebateAmount,
CreateTime: rebate.CreateTime,
}
}
}
// 3. 转换为切片并按时间排序
orderList := make([]*OrderRebateInfo, 0, len(orderMap))
for _, info := range orderMap {
orderList = append(orderList, info)
}
// 按创建时间倒序排序
sort.Slice(orderList, func(i, j int) bool {
return orderList[i].CreateTime.After(orderList[j].CreateTime)
})
// 4. 分页
offset := (page - 1) * pageSize
end := offset + pageSize
if end > int64(len(orderList)) {
end = int64(len(orderList))
}
if offset >= int64(len(orderList)) {
return []types.OrderItem{}, total, nil
}
pagedOrderList := orderList[offset:end]
// 5. 组装订单列表
var resultList []types.OrderItem
productCache := make(map[int64]string) // 产品ID -> 产品名称缓存
for _, orderInfo := range pagedOrderList {
// 查询订单信息
order, err := l.svcCtx.OrderModel.FindOne(ctx, orderInfo.OrderId)
if err != nil {
continue
}
// 查询agent_order信息用于获取订单金额
agentOrder, err := l.svcCtx.AgentOrderModel.FindOneByOrderId(ctx, orderInfo.OrderId)
if err != nil {
continue
}
// 查询产品名称(使用缓存)
productName := ""
if name, ok := productCache[orderInfo.ProductId]; ok {
productName = name
} else {
product, err := l.svcCtx.ProductModel.FindOne(ctx, orderInfo.ProductId)
if err == nil {
productName = product.ProductName
productCache[orderInfo.ProductId] = productName
}
}
resultList = append(resultList, types.OrderItem{
OrderNo: order.OrderNo,
ProductId: orderInfo.ProductId,
ProductName: productName,
OrderAmount: agentOrder.OrderAmount,
RebateAmount: orderInfo.RebateAmount,
CreateTime: orderInfo.CreateTime.Format("2006-01-02 15:04:05"),
})
}
return resultList, total, nil
}
// getInviteList 获取邀请列表
func (l *GetSubordinateContributionDetailLogic) getInviteList(ctx context.Context, subordinateId int64, page, pageSize int64) ([]types.InviteItem, int64, error) {
// 查询总数
builder := l.svcCtx.AgentRelationModel.SelectBuilder().
Where("parent_id = ? AND relation_type = ? AND del_state = ?", subordinateId, 1, globalkey.DelStateNo)
total, err := l.svcCtx.AgentRelationModel.FindCount(ctx, builder, "id")
if err != nil {
return nil, 0, err
}
// 分页查询
offset := (page - 1) * pageSize
builder = builder.OrderBy("create_time DESC").
Limit(uint64(pageSize)).Offset(uint64(offset))
relations, err := l.svcCtx.AgentRelationModel.FindAll(ctx, builder, "")
if err != nil {
return nil, 0, err
}
// 组装邀请列表
var inviteList []types.InviteItem
for _, relation := range relations {
// 查询被邀请的代理信息
invitedAgent, err := l.svcCtx.AgentModel.FindOne(ctx, relation.ChildId)
if err != nil {
continue
}
// 获取等级名称
levelName := ""
switch invitedAgent.Level {
case 1:
levelName = "普通"
case 2:
levelName = "黄金"
case 3:
levelName = "钻石"
}
// 解密手机号
mobile := ""
if invitedAgent.Mobile != "" {
decrypted, err := crypto.DecryptMobile(invitedAgent.Mobile, l.svcCtx.Config.Encrypt.SecretKey)
if err == nil {
mobile = decrypted
}
}
inviteList = append(inviteList, types.InviteItem{
AgentId: invitedAgent.Id,
Level: invitedAgent.Level,
LevelName: levelName,
Mobile: mobile,
CreateTime: relation.CreateTime.Format("2006-01-02 15:04:05"),
})
}
return inviteList, total, nil
}

View File

@@ -62,7 +62,7 @@ func (l *GetSubordinateListLogic) GetSubordinateList(req *types.GetSubordinateLi
offset := (page - 1) * pageSize
// 4. 查询总数
total, err := l.svcCtx.AgentRelationModel.FindCount(l.ctx, builder, "")
total, err := l.svcCtx.AgentRelationModel.FindCount(l.ctx, builder, "id")
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询下级总数失败, %v", err)
}

View File

@@ -0,0 +1,341 @@
package agent
import (
"context"
"sort"
"strings"
"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/Masterminds/squirrel"
"github.com/pkg/errors"
"ycc-server/app/main/api/internal/svc"
"ycc-server/app/main/api/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type GetTeamListLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewGetTeamListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetTeamListLogic {
return &GetTeamListLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *GetTeamListLogic) GetTeamList(req *types.GetTeamListReq) (resp *types.GetTeamListResp, err error) {
userID, err := ctxdata.GetUidFromCtx(l.ctx)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取用户信息失败, %v", err)
}
// 1. 获取代理信息
agent, err := l.svcCtx.AgentModel.FindOneByUserId(l.ctx, userID)
if err != nil {
if errors.Is(err, model.ErrNotFound) {
return nil, errors.Wrapf(xerr.NewErrMsg("您不是代理"), "")
}
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询代理信息失败, %v", err)
}
// 2. 递归查询所有下级(直接+间接)
allSubordinateIds := make(map[int64]bool)
directSubordinateIds := make(map[int64]bool)
// 递归函数收集所有下级ID
var collectSubordinates func(int64) error
collectSubordinates = func(parentId int64) error {
// 查询直接下级
builder := l.svcCtx.AgentRelationModel.SelectBuilder().
Where("parent_id = ? AND relation_type = ? AND del_state = ?", parentId, 1, globalkey.DelStateNo)
relations, err := l.svcCtx.AgentRelationModel.FindAll(l.ctx, builder, "")
if err != nil {
return err
}
for _, relation := range relations {
// 如果是第一层,标记为直接下级
if parentId == agent.Id {
directSubordinateIds[relation.ChildId] = true
}
// 添加到所有下级集合
allSubordinateIds[relation.ChildId] = true
// 递归查询下级的下级
if err := collectSubordinates(relation.ChildId); err != nil {
return err
}
}
return nil
}
// 开始递归收集所有下级
if err := collectSubordinates(agent.Id); err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询下级关系失败, %v", err)
}
// 3. 如果没有下级,返回空数据
if len(allSubordinateIds) == 0 {
return &types.GetTeamListResp{
Statistics: types.TeamStatistics{},
Total: 0,
List: []types.TeamMemberItem{},
}, nil
}
// 4. 将下级ID转换为切片用于查询
subordinateIds := make([]int64, 0, len(allSubordinateIds))
for id := range allSubordinateIds {
subordinateIds = append(subordinateIds, id)
}
// 5. 计算时间范围
now := time.Now()
todayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
monthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
// 6. 统计顶部数据
statistics := l.calculateTeamStatistics(agent.Id, subordinateIds, todayStart, monthStart)
// 7. 查询所有下级代理信息(不进行手机号过滤,因为需要模糊搜索)
builder := l.svcCtx.AgentModel.SelectBuilder().
Where(squirrel.Eq{"id": subordinateIds}).
Where("del_state = ?", globalkey.DelStateNo)
allTeamMembers, err := l.svcCtx.AgentModel.FindAll(l.ctx, builder, "")
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询团队成员失败, %v", err)
}
// 8. 如果有手机号搜索条件,进行模糊匹配过滤(在内存中)
var filteredMembers []*model.Agent
searchMobile := strings.TrimSpace(req.Mobile)
if searchMobile != "" {
// 解密所有手机号并进行模糊匹配
for _, member := range allTeamMembers {
if member.Mobile != "" {
decryptedMobile, err := crypto.DecryptMobile(member.Mobile, l.svcCtx.Config.Encrypt.SecretKey)
if err == nil && strings.Contains(decryptedMobile, searchMobile) {
filteredMembers = append(filteredMembers, member)
}
}
}
} else {
// 没有搜索条件,使用所有成员
filteredMembers = allTeamMembers
}
// 9. 按创建时间倒序排序
sort.Slice(filteredMembers, func(i, j int) bool {
return filteredMembers[i].CreateTime.After(filteredMembers[j].CreateTime)
})
// 10. 分页处理
total := int64(len(filteredMembers))
page := req.Page
if page <= 0 {
page = 1
}
pageSize := req.PageSize
if pageSize <= 0 {
pageSize = 20
}
offset := (page - 1) * pageSize
// 计算分页范围
start := int(offset)
end := start + int(pageSize)
if start > len(filteredMembers) {
start = len(filteredMembers)
}
if end > len(filteredMembers) {
end = len(filteredMembers)
}
var teamMembers []*model.Agent
if start < end {
teamMembers = filteredMembers[start:end]
}
// 11. 组装响应列表
var list []types.TeamMemberItem
for _, member := range teamMembers {
memberItem := l.buildTeamMemberItem(agent.Id, member, directSubordinateIds, todayStart)
list = append(list, memberItem)
}
return &types.GetTeamListResp{
Statistics: statistics,
Total: total,
List: list,
}, nil
}
// calculateTeamStatistics 计算团队统计数据
// 注意:所有统计都基于 subordinateIds下级ID列表不包含自己的数据
func (l *GetTeamListLogic) calculateTeamStatistics(agentId int64, subordinateIds []int64, todayStart, monthStart time.Time) types.TeamStatistics {
// 团队成员总数:只统计下级,不包括自己
stats := types.TeamStatistics{
TotalMembers: int64(len(subordinateIds)),
}
// 统计今日和本月新增成员(只统计下级,不包括自己)
todayNewCount := int64(0)
monthNewCount := int64(0)
for _, id := range subordinateIds {
member, err := l.svcCtx.AgentModel.FindOne(l.ctx, id)
if err != nil {
continue
}
if member.CreateTime.After(todayStart) {
todayNewCount++
}
if member.CreateTime.After(monthStart) {
monthNewCount++
}
}
stats.TodayNewMembers = todayNewCount
stats.MonthNewMembers = monthNewCount
// 统计团队总查询量仅统计有返佣给当前用户的订单通过agent_rebate表去重统计order_id
if len(subordinateIds) > 0 {
rebateBuilder := l.svcCtx.AgentRebateModel.SelectBuilder().
Where("agent_id = ? AND del_state = ?", agentId, globalkey.DelStateNo).
Where("source_agent_id != ?", agentId). // 明确排除自己
Where(squirrel.Eq{"source_agent_id": subordinateIds})
// 统计去重的订单数量
totalQueries := l.countDistinctOrders(l.ctx, rebateBuilder)
stats.TotalQueries = totalQueries
// 今日推广量(仅统计有返佣的订单)
todayRebateBuilder := rebateBuilder.Where("create_time >= ?", todayStart)
todayPromotions := l.countDistinctOrders(l.ctx, todayRebateBuilder)
stats.TodayPromotions = todayPromotions
// 本月推广量(仅统计有返佣的订单)
monthRebateBuilder := rebateBuilder.Where("create_time >= ?", monthStart)
monthPromotions := l.countDistinctOrders(l.ctx, monthRebateBuilder)
stats.MonthPromotions = monthPromotions
}
// 统计收益:只统计从下级获得的返佣(依靠团队得到的收益)
// 返佣从agent_rebate表统计从下级获得的返佣
// agent_id = 当前代理ID获得返佣的代理
// source_agent_id IN 下级ID列表来源代理即产生订单的下级代理
// 明确排除自己source_agent_id != agentId确保不统计自己的数据
if len(subordinateIds) > 0 {
rebateBuilder := l.svcCtx.AgentRebateModel.SelectBuilder().
Where("agent_id = ? AND del_state = ?", agentId, globalkey.DelStateNo).
Where("source_agent_id != ?", agentId). // 明确排除自己
Where(squirrel.Eq{"source_agent_id": subordinateIds})
totalRebate, _ := l.svcCtx.AgentRebateModel.FindSum(l.ctx, rebateBuilder, "rebate_amount")
todayRebate, _ := l.svcCtx.AgentRebateModel.FindSum(l.ctx, rebateBuilder.Where("create_time >= ?", todayStart), "rebate_amount")
monthRebate, _ := l.svcCtx.AgentRebateModel.FindSum(l.ctx, rebateBuilder.Where("create_time >= ?", monthStart), "rebate_amount")
stats.TotalEarnings = totalRebate
stats.TodayEarnings = todayRebate
stats.MonthEarnings = monthRebate
}
return stats
}
// countDistinctOrders 统计去重的订单数量(通过返佣记录)
func (l *GetTeamListLogic) countDistinctOrders(ctx context.Context, builder squirrel.SelectBuilder) int64 {
// 查询所有返佣记录在内存中去重订单ID
rebates, err := l.svcCtx.AgentRebateModel.FindAll(ctx, builder, "")
if err != nil {
return 0
}
orderIdSet := make(map[int64]bool)
for _, rebate := range rebates {
orderIdSet[rebate.OrderId] = true
}
return int64(len(orderIdSet))
}
// countDistinctOrdersForMember 统计单个成员的去重订单数量(通过返佣记录)
func (l *GetTeamListLogic) countDistinctOrdersForMember(ctx context.Context, builder squirrel.SelectBuilder) int64 {
// 查询所有返佣记录在内存中去重订单ID
rebates, err := l.svcCtx.AgentRebateModel.FindAll(ctx, builder, "")
if err != nil {
return 0
}
orderIdSet := make(map[int64]bool)
for _, rebate := range rebates {
orderIdSet[rebate.OrderId] = true
}
return int64(len(orderIdSet))
}
// buildTeamMemberItem 构建团队成员项
func (l *GetTeamListLogic) buildTeamMemberItem(agentId int64, member *model.Agent, directSubordinateIds map[int64]bool, todayStart time.Time) types.TeamMemberItem {
levelName := ""
switch member.Level {
case 1:
levelName = "普通"
case 2:
levelName = "黄金"
case 3:
levelName = "钻石"
}
// 解密手机号
mobile := ""
if member.Mobile != "" {
decrypted, err := crypto.DecryptMobile(member.Mobile, l.svcCtx.Config.Encrypt.SecretKey)
if err == nil {
mobile = decrypted
}
}
// 统计查询量仅统计有返佣给当前用户的订单通过agent_rebate表去重统计order_id
rebateBuilder := l.svcCtx.AgentRebateModel.SelectBuilder().
Where("agent_id = ? AND source_agent_id = ? AND del_state = ?", agentId, member.Id, globalkey.DelStateNo)
totalQueries := l.countDistinctOrdersForMember(l.ctx, rebateBuilder)
todayRebateBuilder := rebateBuilder.Where("create_time >= ?", todayStart)
todayQueries := l.countDistinctOrdersForMember(l.ctx, todayRebateBuilder)
// 统计返佣给我的金额从agent_rebate表source_agent_id = member.Id, agent_id = agentId
totalRebateAmount, _ := l.svcCtx.AgentRebateModel.FindSum(l.ctx, rebateBuilder, "rebate_amount")
todayRebateAmount, _ := l.svcCtx.AgentRebateModel.FindSum(l.ctx, rebateBuilder.Where("create_time >= ?", todayStart), "rebate_amount")
// 统计邀请人数从agent_relation表parent_id = member.Id
inviteBuilder := l.svcCtx.AgentRelationModel.SelectBuilder().
Where("parent_id = ? AND relation_type = ? AND del_state = ?", member.Id, 1, globalkey.DelStateNo)
totalInvites, _ := l.svcCtx.AgentRelationModel.FindCount(l.ctx, inviteBuilder, "id")
todayInvites, _ := l.svcCtx.AgentRelationModel.FindCount(l.ctx, inviteBuilder.Where("create_time >= ?", todayStart), "id")
// 判断是否直接下级
isDirect := directSubordinateIds[member.Id]
return types.TeamMemberItem{
AgentId: member.Id,
Level: member.Level,
LevelName: levelName,
Mobile: mobile,
CreateTime: member.CreateTime.Format("2006-01-02 15:04:05"),
TodayQueries: todayQueries,
TotalQueries: totalQueries,
TotalRebateAmount: totalRebateAmount,
TodayRebateAmount: todayRebateAmount,
TotalInvites: totalInvites,
TodayInvites: todayInvites,
IsDirect: isDirect,
}
}

View File

@@ -2,6 +2,7 @@ package agent
import (
"context"
"time"
"ycc-server/app/main/model"
"ycc-server/common/ctxdata"
"ycc-server/common/globalkey"
@@ -45,21 +46,68 @@ func (l *GetTeamStatisticsLogic) GetTeamStatistics() (resp *types.TeamStatistics
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询代理信息失败, %v", err)
}
// 2. 确定团队首领ID
teamLeaderId := agent.Id
if agent.TeamLeaderId.Valid {
teamLeaderId = agent.TeamLeaderId.Int64
// 2. 递归查询所有下级(直接+间接)
allSubordinateIds := make(map[int64]bool)
directSubordinateIds := make(map[int64]bool)
// 递归函数收集所有下级ID
var collectSubordinates func(int64) error
collectSubordinates = func(parentId int64) error {
// 查询直接下级
builder := l.svcCtx.AgentRelationModel.SelectBuilder().
Where("parent_id = ? AND relation_type = ? AND del_state = ?", parentId, 1, globalkey.DelStateNo)
relations, err := l.svcCtx.AgentRelationModel.FindAll(l.ctx, builder, "")
if err != nil {
return err
}
for _, relation := range relations {
// 如果是第一层,标记为直接下级
if parentId == agent.Id {
directSubordinateIds[relation.ChildId] = true
}
// 添加到所有下级集合
allSubordinateIds[relation.ChildId] = true
// 递归查询下级的下级
if err := collectSubordinates(relation.ChildId); err != nil {
return err
}
}
return nil
}
// 3. 查询团队所有成员(通过 team_leader_id
// 开始递归收集所有下级
if err := collectSubordinates(agent.Id); err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询下级关系失败, %v", err)
}
// 3. 获取当前时间用于统计今日和本月新增
now := time.Now()
todayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
monthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
// 4. 如果没有下级,返回空数据
if len(allSubordinateIds) == 0 {
return &types.TeamStatisticsResp{
TotalCount: 0,
DirectCount: 0,
IndirectCount: 0,
GoldCount: 0,
NormalCount: 0,
TodayNewMembers: 0,
MonthNewMembers: 0,
}, nil
}
// 5. 将下级ID转换为切片用于查询
subordinateIds := make([]int64, 0, len(allSubordinateIds))
for id := range allSubordinateIds {
subordinateIds = append(subordinateIds, id)
}
// 6. 查询所有下级代理信息
builder := l.svcCtx.AgentModel.SelectBuilder().
Where(squirrel.Or{
squirrel.Eq{"team_leader_id": teamLeaderId},
squirrel.And{
squirrel.Eq{"id": teamLeaderId},
squirrel.Eq{"level": 3}, // 钻石代理自己也是团队首领
},
}).
Where(squirrel.Eq{"id": subordinateIds}).
Where("del_state = ?", globalkey.DelStateNo)
teamMembers, err := l.svcCtx.AgentModel.FindAll(l.ctx, builder, "")
@@ -67,51 +115,43 @@ func (l *GetTeamStatisticsLogic) GetTeamStatistics() (resp *types.TeamStatistics
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询团队成员失败, %v", err)
}
// 4. 统计
totalCount := int64(len(teamMembers))
// 7. 统计
totalCount := int64(len(teamMembers)) // 下级总数(不包括自己)
directCount := int64(len(directSubordinateIds))
indirectCount := totalCount - directCount
level1Count := int64(0) // 普通
level2Count := int64(0) // 黄金
level3Count := int64(0) // 钻石
// 不再统计钻石,因为下级不可能是钻石
// 统计直接下级
directCount := int64(0)
builder = l.svcCtx.AgentRelationModel.SelectBuilder().
Where("parent_id = ? AND relation_type = ? AND del_state = ?", agent.Id, 1, globalkey.DelStateNo)
directRelations, err := l.svcCtx.AgentRelationModel.FindAll(l.ctx, builder, "")
if err == nil {
directCount = int64(len(directRelations))
}
todayNewCount := int64(0)
monthNewCount := int64(0)
for _, member := range teamMembers {
// 排除自己
if member.Id == agent.Id {
continue
}
// 统计等级(只统计普通和黄金)
switch member.Level {
case 1:
level1Count++
case 2:
level2Count++
case 3:
level3Count++
}
// 统计今日和本月新增
if member.CreateTime.After(todayStart) {
todayNewCount++
}
if member.CreateTime.After(monthStart) {
monthNewCount++
}
}
// 间接下级 = 总人数 - 直接下级 - 自己
indirectCount := totalCount - directCount - 1
if indirectCount < 0 {
indirectCount = 0
}
return &types.TeamStatisticsResp{
TotalCount: totalCount - 1, // 排除自己
DirectCount: directCount,
IndirectCount: indirectCount,
ByLevel: types.TeamLevelStats{
Normal: level1Count,
Gold: level2Count,
Diamond: level3Count,
},
TotalCount: totalCount, // 下级总数(不包括自己
DirectCount: directCount,
IndirectCount: indirectCount,
GoldCount: level2Count,
NormalCount: level1Count,
TodayNewMembers: todayNewCount,
MonthNewMembers: monthNewCount,
}, nil
}

View File

@@ -61,7 +61,7 @@ func (l *GetUpgradeListLogic) GetUpgradeList(req *types.GetUpgradeListReq) (resp
offset := (page - 1) * pageSize
// 4. 查询总数
total, err := l.svcCtx.AgentUpgradeModel.FindCount(l.ctx, builder, "")
total, err := l.svcCtx.AgentUpgradeModel.FindCount(l.ctx, builder, "id")
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询升级记录总数失败, %v", err)
}

View File

@@ -0,0 +1,119 @@
package agent
import (
"context"
"ycc-server/app/main/model"
"ycc-server/common/ctxdata"
"ycc-server/common/globalkey"
"ycc-server/common/xerr"
"ycc-server/pkg/lzkit/crypto"
"github.com/pkg/errors"
"ycc-server/app/main/api/internal/svc"
"ycc-server/app/main/api/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type GetUpgradeRebateListLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewGetUpgradeRebateListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetUpgradeRebateListLogic {
return &GetUpgradeRebateListLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *GetUpgradeRebateListLogic) GetUpgradeRebateList(req *types.GetUpgradeRebateListReq) (resp *types.GetUpgradeRebateListResp, err error) {
userID, err := ctxdata.GetUidFromCtx(l.ctx)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取用户信息失败, %v", err)
}
// 1. 获取代理信息
agent, err := l.svcCtx.AgentModel.FindOneByUserId(l.ctx, userID)
if err != nil {
if errors.Is(err, model.ErrNotFound) {
return nil, errors.Wrapf(xerr.NewErrMsg("您不是代理"), "")
}
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询代理信息失败, %v", err)
}
// 2. 构建查询条件:查询 rebate_agent_id = 当前代理ID 且 status = 2已完成且 upgrade_type = 1自主付费的记录
// 注意rebate_agent_id 是 NullInt64 类型,需要同时检查 IS NOT NULL
// 只要返佣给自己的都要显示不管升级后是否脱离关系rebate_agent_id 记录的是升级时的原直接上级)
builder := l.svcCtx.AgentUpgradeModel.SelectBuilder().
Where("rebate_agent_id IS NOT NULL AND rebate_agent_id = ? AND status = ? AND upgrade_type = ? AND del_state = ?",
agent.Id, 2, 1, globalkey.DelStateNo).
OrderBy("create_time DESC")
// 3. 分页查询
page := req.Page
if page <= 0 {
page = 1
}
pageSize := req.PageSize
if pageSize <= 0 {
pageSize = 20
}
offset := (page - 1) * pageSize
// 4. 查询总数
total, err := l.svcCtx.AgentUpgradeModel.FindCount(l.ctx, builder, "id")
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询升级返佣总数失败, %v", err)
}
// 5. 查询列表
builder = builder.Limit(uint64(pageSize)).Offset(uint64(offset))
upgrades, err := l.svcCtx.AgentUpgradeModel.FindAll(l.ctx, builder, "")
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询升级返佣列表失败, %v", err)
}
// 6. 组装响应
var list []types.UpgradeRebateItem
for _, upgrade := range upgrades {
// 查询来源代理手机号(升级的代理)
sourceAgentMobile := ""
if upgrade.AgentId > 0 {
sourceAgent, err := l.svcCtx.AgentModel.FindOne(l.ctx, upgrade.AgentId)
if err == nil {
if sourceAgent.Mobile != "" {
decrypted, err := crypto.DecryptMobile(sourceAgent.Mobile, l.svcCtx.Config.Encrypt.SecretKey)
if err == nil {
sourceAgentMobile = decrypted
}
}
}
}
// 获取订单号
orderNo := ""
if upgrade.OrderNo.Valid {
orderNo = upgrade.OrderNo.String
}
list = append(list, types.UpgradeRebateItem{
Id: upgrade.Id,
SourceAgentId: upgrade.AgentId,
SourceAgentMobile: sourceAgentMobile,
OrderNo: orderNo,
FromLevel: upgrade.FromLevel,
ToLevel: upgrade.ToLevel,
Amount: upgrade.RebateAmount,
CreateTime: upgrade.CreateTime.Format("2006-01-02 15:04:05"),
})
}
return &types.GetUpgradeRebateListResp{
Total: total,
List: list,
}, nil
}

View File

@@ -61,7 +61,7 @@ func (l *GetWithdrawalListLogic) GetWithdrawalList(req *types.GetWithdrawalListR
offset := (page - 1) * pageSize
// 4. 查询总数
total, err := l.svcCtx.AgentWithdrawalModel.FindCount(l.ctx, builder, "")
total, err := l.svcCtx.AgentWithdrawalModel.FindCount(l.ctx, builder, "id")
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询提现记录总数失败, %v", err)
}

View File

@@ -0,0 +1,58 @@
package agent
import (
"context"
"fmt"
"net/http"
"net/url"
"ycc-server/app/main/model"
"ycc-server/common/xerr"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/logx"
"ycc-server/app/main/api/internal/svc"
)
type PromotionRedirectLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewPromotionRedirectLogic(ctx context.Context, svcCtx *svc.ServiceContext) *PromotionRedirectLogic {
return &PromotionRedirectLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
// PromotionRedirect 推广链接重定向
// 从推广域名重定向到正式域名的推广页面
func (l *PromotionRedirectLogic) PromotionRedirect(r *http.Request, w http.ResponseWriter) error {
// 1. 获取link参数
linkIdentifier := r.URL.Query().Get("link")
if linkIdentifier == "" {
return errors.Wrapf(xerr.NewErrCode(xerr.REUQEST_PARAM_ERROR), "缺少link参数")
}
// 2. 验证linkIdentifier是否存在可选用于确保链接有效
_, err := l.svcCtx.AgentLinkModel.FindOneByLinkIdentifier(l.ctx, linkIdentifier)
if err != nil {
if errors.Is(err, model.ErrNotFound) {
return errors.Wrapf(xerr.NewErrCode(xerr.REUQEST_PARAM_ERROR), "推广链接不存在或已失效")
}
l.Errorf("查询推广链接失败: %v", err)
// 即使查询失败,也继续重定向,避免影响用户体验
}
// 3. 构建重定向URL使用相对路径由服务器配置处理域名
redirectURL := fmt.Sprintf("/agent/promotionInquire/%s", url.QueryEscape(linkIdentifier))
// 5. 执行重定向302临时重定向
l.Infof("推广链接重定向: linkIdentifier=%s, redirectURL=%s", linkIdentifier, redirectURL)
http.Redirect(w, r, redirectURL, http.StatusFound)
return nil
}

View File

@@ -4,6 +4,7 @@ import (
"context"
"database/sql"
"fmt"
"os"
"time"
"ycc-server/app/main/model"
"ycc-server/common/xerr"
@@ -40,8 +41,8 @@ func (l *RegisterByInviteCodeLogic) RegisterByInviteCode(req *types.RegisterByIn
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "加密手机号失败: %v", err)
}
// 校验验证码
if req.Mobile != "18889793585" {
// 校验验证码(开发环境下跳过验证码校验)
if os.Getenv("ENV") != "development" {
redisKey := fmt.Sprintf("%s:%s", "agentApply", encryptedMobile)
cacheCode, err := l.svcCtx.Redis.Get(redisKey)
if err != nil {
@@ -181,7 +182,7 @@ func (l *RegisterByInviteCodeLogic) RegisterByInviteCode(req *types.RegisterByIn
// 设置自己为团队首领
newAgent.TeamLeaderId = sql.NullInt64{Int64: agentID, Valid: true}
if err := l.svcCtx.AgentModel.UpdateWithVersion(transCtx, session, newAgent); err != nil {
if err := l.svcCtx.AgentModel.UpdateInTransaction(transCtx, session, newAgent); err != nil {
return errors.Wrapf(err, "更新团队首领失败")
}
} else {
@@ -217,6 +218,19 @@ func (l *RegisterByInviteCodeLogic) RegisterByInviteCode(req *types.RegisterByIn
return errors.Wrapf(err, "更新邀请码状态失败")
}
// 4.7 记录邀请码使用历史(用于统计和查询)
usage := &model.AgentInviteCodeUsage{
InviteCodeId: inviteCodeModel.Id,
Code: inviteCodeModel.Code,
UserId: userID,
AgentId: agentID,
AgentLevel: targetLevel,
UsedTime: time.Now(),
}
if _, err := l.svcCtx.AgentInviteCodeUsageModel.Insert(transCtx, session, usage); err != nil {
return errors.Wrapf(err, "记录邀请码使用历史失败")
}
agentLevel = targetLevel
return nil
})

View File

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

View File

@@ -5,6 +5,7 @@ import (
"database/sql"
"ycc-server/app/main/model"
"ycc-server/common/ctxdata"
"ycc-server/common/globalkey"
"ycc-server/common/xerr"
"ycc-server/pkg/lzkit/lzUtils"
@@ -62,17 +63,10 @@ func (l *UpgradeSubordinateLogic) UpgradeSubordinate(req *types.UpgradeSubordina
return nil, errors.Wrapf(xerr.NewErrMsg("只能升级普通代理为黄金代理"), "")
}
// 5. 验证关系:必须是直接下级
parent, err := l.svcCtx.AgentService.FindDirectParent(l.ctx, subordinateAgent.Id)
if err != nil {
if errors.Is(err, model.ErrNotFound) {
return nil, errors.Wrapf(xerr.NewErrMsg("该代理不是您的直接下级"), "")
}
return nil, errors.Wrapf(err, "查询关系失败")
}
if parent.Id != operatorAgent.Id {
return nil, errors.Wrapf(xerr.NewErrMsg("该代理不是您的直接下级"), "")
// 5. 验证关系:必须是下级(直接或间接)
isSubordinate := l.isSubordinate(operatorAgent.Id, subordinateAgent.Id)
if !isSubordinate {
return nil, errors.Wrapf(xerr.NewErrMsg("该代理不是您的下级"), "")
}
// 6. 验证目标等级:只能升级为黄金
@@ -87,12 +81,12 @@ func (l *UpgradeSubordinateLogic) UpgradeSubordinate(req *types.UpgradeSubordina
upgradeRecord := &model.AgentUpgrade{
AgentId: subordinateAgent.Id,
FromLevel: 1, // 普通
ToLevel: toLevel,
UpgradeType: 2, // 钻石升级下级
UpgradeFee: 0, // 免费
RebateAmount: 0, // 无返佣
OperatorAgentId: sql.NullInt64{Int64: operatorAgent.Id, Valid: true},
Status: 1, // 待处理
ToLevel: toLevel,
UpgradeType: 2, // 钻石升级下级
UpgradeFee: 0, // 免费
RebateAmount: 0, // 无返佣
OperatorAgentId: sql.NullInt64{Int64: operatorAgent.Id, Valid: true},
Status: 1, // 待处理
}
upgradeResult, err := l.svcCtx.AgentUpgradeModel.Insert(transCtx, session, upgradeRecord)
@@ -125,3 +119,27 @@ func (l *UpgradeSubordinateLogic) UpgradeSubordinate(req *types.UpgradeSubordina
Success: true,
}, nil
}
// isSubordinate 递归检查 targetId 是否是 parentId 的下级(直接或间接)
func (l *UpgradeSubordinateLogic) isSubordinate(parentId, targetId int64) bool {
// 查询直接下级
builder := l.svcCtx.AgentRelationModel.SelectBuilder().
Where("parent_id = ? AND relation_type = ? AND del_state = ?", parentId, 1, globalkey.DelStateNo)
relations, err := l.svcCtx.AgentRelationModel.FindAll(l.ctx, builder, "")
if err != nil {
return false
}
for _, relation := range relations {
// 如果是直接下级,返回 true
if relation.ChildId == targetId {
return true
}
// 递归检查间接下级
if l.isSubordinate(relation.ChildId, targetId) {
return true
}
}
return false
}

View File

@@ -26,6 +26,6 @@ func NewGetAppVersionLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Get
func (l *GetAppVersionLogic) GetAppVersion() (resp *types.GetAppVersionResp, err error) {
return &types.GetAppVersionResp{
Version: "1.0.0",
WgtUrl: "https://www.quannengcha.com/app_version/ycc_1.0.0.wgt",
WgtUrl: "https://www.onecha.cn/app_version/ycc_1.0.0.wgt",
}, nil
}

View File

@@ -7,11 +7,13 @@ import (
"time"
"ycc-server/pkg/lzkit/lzUtils"
"github.com/pkg/errors"
"github.com/smartwalle/alipay/v3"
"ycc-server/app/main/api/internal/svc"
"github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/core/stores/sqlx"
)
type AlipayCallbackLogic struct {
@@ -40,6 +42,9 @@ func (l *AlipayCallbackLogic) AlipayCallback(w http.ResponseWriter, r *http.Requ
if strings.HasPrefix(orderNo, "Q_") {
// 查询订单处理
return l.handleQueryOrderPayment(w, notification)
} else if strings.HasPrefix(orderNo, "U_") {
// 代理升级订单处理
return l.handleAgentUpgradeOrderPayment(w, notification)
} else if strings.HasPrefix(orderNo, "A_") {
// 旧系统会员充值订单(已废弃,新系统使用升级功能)
// return l.handleAgentVipOrderPayment(w, notification)
@@ -105,6 +110,104 @@ func (l *AlipayCallbackLogic) handleQueryOrderPayment(w http.ResponseWriter, not
return nil
}
// 处理代理升级订单支付
func (l *AlipayCallbackLogic) handleAgentUpgradeOrderPayment(w http.ResponseWriter, notification *alipay.Notification) error {
orderNo := notification.OutTradeNo
// 1. 查找订单
order, findOrderErr := l.svcCtx.OrderModel.FindOneByOrderNo(l.ctx, orderNo)
if findOrderErr != nil {
logx.Errorf("支付宝支付回调,查找升级订单失败: %+v", findOrderErr)
alipay.ACKNotification(w)
return nil
}
// 2. 验证金额
amount := lzUtils.ToAlipayAmount(order.Amount)
user, err := l.svcCtx.UserModel.FindOne(l.ctx, order.UserId)
if err == nil && user.Inside != 1 {
if amount != notification.TotalAmount {
logx.Errorf("支付宝支付回调,升级订单金额不一致,订单号: %s", orderNo)
alipay.ACKNotification(w)
return nil
}
}
// 3. 检查订单状态
if order.Status != "pending" {
alipay.ACKNotification(w)
return nil
}
// 4. 查找升级记录
upgradeRecords, findUpgradeErr := l.svcCtx.AgentUpgradeModel.FindAll(l.ctx, l.svcCtx.AgentUpgradeModel.SelectBuilder().
Where("order_no = ?", orderNo).
Limit(1), "")
if findUpgradeErr != nil || len(upgradeRecords) == 0 {
logx.Errorf("支付宝支付回调,查找升级记录失败,订单号: %s, 错误: %+v", orderNo, findUpgradeErr)
alipay.ACKNotification(w)
return nil
}
upgradeRecord := upgradeRecords[0]
// 5. 检查升级记录状态
if upgradeRecord.Status != 1 {
// 升级记录状态不是待支付,直接返回成功
alipay.ACKNotification(w)
return nil
}
// 6. 处理支付状态
switch notification.TradeStatus {
case alipay.TradeStatusSuccess:
order.Status = "paid"
order.PayTime = lzUtils.TimeToNullTime(time.Now())
order.PlatformOrderId = lzUtils.StringToNullString(notification.TradeNo)
case alipay.TradeStatusClosed:
order.Status = "closed"
order.CloseTime = lzUtils.TimeToNullTime(time.Now())
default:
alipay.ACKNotification(w)
return nil
}
// 7. 更新订单状态
if updateErr := l.svcCtx.OrderModel.UpdateWithVersion(l.ctx, nil, order); updateErr != nil {
logx.Errorf("支付宝支付回调,更新升级订单状态失败: %+v", updateErr)
alipay.ACKNotification(w)
return nil
}
// 8. 如果支付成功,执行升级操作
if order.Status == "paid" {
err := l.svcCtx.AgentWalletModel.Trans(l.ctx, func(transCtx context.Context, session sqlx.Session) error {
// 8.1 执行升级操作
if err := l.svcCtx.AgentService.ProcessUpgrade(transCtx, upgradeRecord.AgentId, upgradeRecord.ToLevel, upgradeRecord.UpgradeType, upgradeRecord.UpgradeFee, upgradeRecord.RebateAmount, orderNo, 0); err != nil {
return errors.Wrapf(err, "执行升级操作失败")
}
// 8.2 更新升级记录状态为已完成
upgradeRecord.Status = 2 // 已完成status: 1=待处理2=已完成3=已失败)
upgradeRecord.Remark = lzUtils.StringToNullString("支付成功,升级完成")
if updateErr := l.svcCtx.AgentUpgradeModel.UpdateWithVersion(transCtx, session, upgradeRecord); updateErr != nil {
return errors.Wrapf(updateErr, "更新升级记录状态失败")
}
return nil
})
if err != nil {
logx.Errorf("支付宝支付回调,处理升级订单失败,订单号: %s, 错误: %+v", orderNo, err)
// 即使升级失败,也返回成功给支付宝,避免重复回调
} else {
logx.Infof("支付宝支付回调,代理升级成功,订单号: %s, 代理ID: %d, 从等级 %d 升级到等级 %d", orderNo, upgradeRecord.AgentId, upgradeRecord.FromLevel, upgradeRecord.ToLevel)
}
}
alipay.ACKNotification(w)
return nil
}
// 处理代理会员订单支付(已废弃,新系统使用升级功能)
/*
func (l *AlipayCallbackLogic) handleAgentVipOrderPayment(w http.ResponseWriter, notification *alipay.Notification) error {

View File

@@ -2,6 +2,7 @@ package pay
import (
"context"
"strings"
"ycc-server/app/main/api/internal/svc"
"ycc-server/app/main/api/internal/types"
"ycc-server/common/xerr"
@@ -25,17 +26,18 @@ func NewPaymentCheckLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Paym
}
func (l *PaymentCheckLogic) PaymentCheck(req *types.PaymentCheckReq) (resp *types.PaymentCheckResp, err error) {
// 旧系统会员充值订单(已废弃,新系统使用升级功能)
// if strings.HasPrefix(req.OrderNo, "A_") {
// order, err := l.svcCtx.AgentMembershipRechargeOrderModel.FindOneByOrderNo(l.ctx, req.OrderNo)
// if err != nil {
// return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询订单失败: %v", err)
// }
// return &types.PaymentCheckResp{
// Type: "agent_vip",
// Status: order.Status,
// }, nil
// }
// 根据订单号前缀判断订单类型
if strings.HasPrefix(req.OrderNo, "U_") {
// 升级订单
order, err := l.svcCtx.OrderModel.FindOneByOrderNo(l.ctx, req.OrderNo)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询升级订单失败: %v", err)
}
return &types.PaymentCheckResp{
Type: "agent_upgrade",
Status: order.Status,
}, nil
}
// 查询订单(包括代理订单)
order, err := l.svcCtx.OrderModel.FindOneByOrderNo(l.ctx, req.OrderNo)

View File

@@ -2,14 +2,19 @@ package pay
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"os"
"strconv"
"strings"
"time"
"ycc-server/app/main/api/internal/svc"
"ycc-server/app/main/api/internal/types"
"ycc-server/app/main/model"
"ycc-server/common/ctxdata"
"ycc-server/common/xerr"
"ycc-server/pkg/lzkit/lzUtils"
"github.com/pkg/errors"
"github.com/redis/go-redis/v9"
@@ -26,6 +31,7 @@ type PaymentTypeResp struct {
amount float64
outTradeNo string
description string
orderID int64 // 订单ID用于开发环境测试支付模式
}
func NewPaymentLogic(ctx context.Context, svcCtx *svc.ServiceContext) *PaymentLogic {
@@ -39,6 +45,13 @@ func NewPaymentLogic(ctx context.Context, svcCtx *svc.ServiceContext) *PaymentLo
func (l *PaymentLogic) Payment(req *types.PaymentReq) (resp *types.PaymentResp, err error) {
var paymentTypeResp *PaymentTypeResp
var prepayData interface{}
var orderID int64
// 检查是否为开发环境的测试支付模式
env := os.Getenv("ENV")
isDevTestPayment := env == "development" && (req.PayMethod == "test" || req.PayMethod == "test_empty")
isEmptyReportMode := env == "development" && req.PayMethod == "test_empty"
l.svcCtx.OrderModel.Trans(l.ctx, func(ctx context.Context, session sqlx.Session) error {
switch req.PayType {
case "agent_vip":
@@ -52,8 +65,33 @@ func (l *PaymentLogic) Payment(req *types.PaymentReq) (resp *types.PaymentResp,
if err != nil {
return err
}
case "agent_upgrade":
paymentTypeResp, err = l.AgentUpgradeOrderPayment(req, session)
if err != nil {
return err
}
}
// 开发环境测试支付模式:跳过实际支付流程
// 注意:订单状态更新在事务外进行,避免在事务中查询不到订单的问题
if isDevTestPayment {
// 获取订单ID从 QueryOrderPayment 返回的 orderID
if paymentTypeResp.orderID <= 0 {
return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "开发测试模式订单ID无效")
}
orderID = paymentTypeResp.orderID
// 在事务中只记录订单ID不更新订单状态
// 订单状态的更新和后续流程在事务提交后处理
logx.Infof("开发环境测试支付模式:订单 %s (ID: %d) 将在事务提交后更新状态", paymentTypeResp.outTradeNo, orderID)
// 返回测试支付标识
prepayData = "test_payment_success"
return nil
}
// 正常支付流程
var createOrderErr error
if req.PayMethod == "wechat" {
prepayData, createOrderErr = l.svcCtx.WechatPayService.CreateWechatOrder(l.ctx, paymentTypeResp.amount, paymentTypeResp.description, paymentTypeResp.outTradeNo)
@@ -70,6 +108,90 @@ func (l *PaymentLogic) Payment(req *types.PaymentReq) (resp *types.PaymentResp,
if err != nil {
return nil, err
}
// 开发环境测试支付模式:事务提交后处理订单状态更新和后续流程
if isDevTestPayment && paymentTypeResp != nil && paymentTypeResp.orderID > 0 {
// 使用 goroutine 异步处理,确保事务已完全提交
go func() {
// 短暂延迟,确保事务已完全提交到数据库
time.Sleep(200 * time.Millisecond)
finalOrderID := paymentTypeResp.orderID
// 查找订单并更新状态为已支付
order, findOrderErr := l.svcCtx.OrderModel.FindOne(context.Background(), finalOrderID)
if findOrderErr != nil {
logx.Errorf("开发测试模式查找订单失败订单ID: %d, 错误: %v", finalOrderID, findOrderErr)
return
}
// 更新订单状态为已支付
order.Status = "paid"
now := time.Now()
order.PayTime = sql.NullTime{Time: now, Valid: true}
// 空报告模式:在 PaymentPlatform 字段中标记,用于后续生成空报告
if isEmptyReportMode {
order.PaymentPlatform = "test_empty"
logx.Infof("开发环境空报告模式:订单 %s (ID: %d) 已标记为空报告模式", paymentTypeResp.outTradeNo, finalOrderID)
}
// 更新订单状态(在事务外执行)
updateErr := l.svcCtx.OrderModel.UpdateWithVersion(context.Background(), nil, order)
if updateErr != nil {
logx.Errorf("开发测试模式更新订单状态失败订单ID: %d, 错误: %+v", finalOrderID, updateErr)
return
}
logx.Infof("开发环境测试支付模式:订单 %s (ID: %d) 已自动标记为已支付", paymentTypeResp.outTradeNo, finalOrderID)
// 再次短暂延迟,确保订单状态更新已提交
time.Sleep(100 * time.Millisecond)
// 根据订单类型处理后续流程
if strings.HasPrefix(paymentTypeResp.outTradeNo, "U_") {
// 升级订单:直接执行升级操作
upgradeRecords, findUpgradeErr := l.svcCtx.AgentUpgradeModel.FindAll(context.Background(), l.svcCtx.AgentUpgradeModel.SelectBuilder().
Where("order_no = ?", paymentTypeResp.outTradeNo).
Limit(1), "")
if findUpgradeErr != nil || len(upgradeRecords) == 0 {
logx.Errorf("开发测试模式,查找升级记录失败,订单号: %s, 错误: %+v", paymentTypeResp.outTradeNo, findUpgradeErr)
return
}
upgradeRecord := upgradeRecords[0]
// 执行升级操作
err := l.svcCtx.AgentWalletModel.Trans(context.Background(), func(transCtx context.Context, session sqlx.Session) error {
if err := l.svcCtx.AgentService.ProcessUpgrade(transCtx, upgradeRecord.AgentId, upgradeRecord.ToLevel, upgradeRecord.UpgradeType, upgradeRecord.UpgradeFee, upgradeRecord.RebateAmount, paymentTypeResp.outTradeNo, 0); err != nil {
return errors.Wrapf(err, "执行升级操作失败")
}
// 更新升级记录状态为已完成
upgradeRecord.Status = 2 // 已完成status: 1=待处理2=已完成3=已失败)
upgradeRecord.Remark = lzUtils.StringToNullString("测试支付成功,升级完成")
if updateErr := l.svcCtx.AgentUpgradeModel.UpdateWithVersion(transCtx, session, upgradeRecord); updateErr != nil {
return errors.Wrapf(updateErr, "更新升级记录状态失败")
}
return nil
})
if err != nil {
logx.Errorf("开发测试模式,处理升级订单失败,订单号: %s, 错误: %+v", paymentTypeResp.outTradeNo, err)
} else {
logx.Infof("开发测试模式,代理升级成功,订单号: %s, 代理ID: %d", paymentTypeResp.outTradeNo, upgradeRecord.AgentId)
}
} else {
// 查询订单:发送支付成功通知任务,触发后续流程(生成报告和代理处理)
if sendErr := l.svcCtx.AsynqService.SendQueryTask(finalOrderID); sendErr != nil {
logx.Errorf("开发测试模式发送支付成功通知任务失败订单ID: %d, 错误: %+v", finalOrderID, sendErr)
} else {
logx.Infof("开发测试模式已发送支付成功通知任务订单ID: %d", finalOrderID)
}
}
}()
}
switch v := prepayData.(type) {
case string:
// 如果 prepayData 是字符串类型,直接返回
@@ -153,19 +275,35 @@ func (l *PaymentLogic) QueryOrderPayment(req *types.PaymentReq, session sqlx.Ses
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "生成订单, 查询代理信息失败: %+v", err)
}
// 获取系统配置
basePrice, err := l.getConfigFloat("base_price")
// 获取产品配置(必须存在)
productConfig, err := l.svcCtx.AgentProductConfigModel.FindOneByProductId(l.ctx, product.Id)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "生成订单, 获取基础底价配置失败: %+v", err)
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)
}
priceThreshold, _ := l.getConfigFloat("price_threshold")
priceFeeRate, _ := l.getConfigFloat("price_fee_rate")
// 计算实际底价(基础底价+等级加成
levelBonus := l.getLevelBonus(agent.Level)
// 获取等级加成(需要从系统配置读取
levelBonus, err := l.getLevelBonus(agent.Level)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "生成订单, 获取等级加成配置失败: %+v", err)
}
// 使用产品配置的底价计算实际底价
basePrice := productConfig.BasePrice
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
@@ -191,25 +329,138 @@ func (l *PaymentLogic) QueryOrderPayment(req *types.PaymentReq, session sqlx.Ses
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "生成订单, 保存代理订单失败: %+v", agentOrderInsert)
}
}
return &PaymentTypeResp{amount: amount, outTradeNo: outTradeNo, description: product.ProductName}, nil
return &PaymentTypeResp{amount: amount, outTradeNo: outTradeNo, description: product.ProductName, orderID: orderID}, nil
}
// AgentVipOrderPayment 代理会员充值订单(已废弃,新系统使用升级功能替代)
func (l *PaymentLogic) AgentVipOrderPayment(req *types.PaymentReq, session sqlx.Session) (resp *PaymentTypeResp, err error) {
// 新代理系统已废弃会员充值功能,请使用升级功能
return nil, errors.Wrapf(xerr.NewErrMsg("该功能已废弃,请使用代理升级功能"), "")
}
// getLevelBonus 获取等级加成
func (l *PaymentLogic) getLevelBonus(level int64) int64 {
// AgentUpgradeOrderPayment 代理升级订单支付
func (l *PaymentLogic) AgentUpgradeOrderPayment(req *types.PaymentReq, session sqlx.Session) (resp *PaymentTypeResp, err error) {
userID, err := ctxdata.GetUidFromCtx(l.ctx)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取用户信息失败, %v", err)
}
// 1. 解析升级记录ID
upgradeId, err := strconv.ParseInt(req.Id, 10, 64)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "无效的升级记录ID: %s", req.Id)
}
// 2. 查找升级记录
upgradeRecord, err := l.svcCtx.AgentUpgradeModel.FindOne(l.ctx, upgradeId)
if err != nil {
if errors.Is(err, model.ErrNotFound) {
return nil, errors.Wrapf(xerr.NewErrMsg("升级记录不存在"), "")
}
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询升级记录失败, %v", err)
}
// 3. 验证升级记录状态(必须是待支付状态)
if upgradeRecord.Status != 1 {
return nil, errors.Wrapf(xerr.NewErrMsg("升级记录状态不正确,无法支付"), "")
}
// 4. 验证代理ID是否匹配
agent, err := l.svcCtx.AgentModel.FindOneByUserId(l.ctx, userID)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询代理信息失败, %v", err)
}
if agent.Id != upgradeRecord.AgentId {
return nil, errors.Wrapf(xerr.NewErrMsg("无权支付此升级订单"), "")
}
// 5. 生成订单号(使用 U_ 前缀表示升级订单)
outTradeNo := fmt.Sprintf("U_%d_%d", upgradeId, time.Now().Unix())
// 6. 获取用户信息(用于内部用户判断)
user, err := l.svcCtx.UserModel.FindOne(l.ctx, userID)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取用户信息失败: %v", err)
}
// 7. 计算支付金额
amount := upgradeRecord.UpgradeFee
if user.Inside == 1 {
amount = 0.01 // 内部用户测试金额
}
// 8. 创建订单记录
order := model.Order{
OrderNo: outTradeNo,
UserId: userID,
ProductId: 0, // 升级订单没有产品ID
PaymentPlatform: req.PayMethod,
PaymentScene: "app",
Amount: amount,
Status: "pending",
}
orderInsertResult, insertOrderErr := l.svcCtx.OrderModel.Insert(l.ctx, session, &order)
if insertOrderErr != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建订单失败: %+v", insertOrderErr)
}
orderID, lastInsertIdErr := orderInsertResult.LastInsertId()
if lastInsertIdErr != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取订单ID失败: %+v", lastInsertIdErr)
}
// 9. 更新升级记录的订单号
upgradeRecord.OrderNo = lzUtils.StringToNullString(outTradeNo)
if updateErr := l.svcCtx.AgentUpgradeModel.UpdateWithVersion(l.ctx, session, upgradeRecord); updateErr != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新升级记录订单号失败: %+v", updateErr)
}
// 10. 生成描述信息
levelNames := map[int64]string{
1: "普通代理",
2: "黄金代理",
3: "钻石代理",
}
fromLevelName := levelNames[upgradeRecord.FromLevel]
toLevelName := levelNames[upgradeRecord.ToLevel]
description := fmt.Sprintf("代理升级:%s → %s", fromLevelName, toLevelName)
return &PaymentTypeResp{
amount: amount,
outTradeNo: outTradeNo,
description: description,
orderID: orderID,
}, nil
}
// getLevelBonus 获取等级加成(从配置表读取)
func (l *PaymentLogic) getLevelBonus(level int64) (int64, error) {
var configKey string
switch level {
case 1: // 普通
return 6
configKey = "level_1_bonus"
case 2: // 黄金
return 3
configKey = "level_2_bonus"
case 3: // 钻石
return 0
configKey = "level_3_bonus"
default:
return 0
return 0, nil
}
bonus, err := l.getConfigFloat(configKey)
if err != nil {
// 配置不存在时返回默认值
l.Errorf("获取等级加成配置失败, level: %d, key: %s, err: %v使用默认值", level, configKey, err)
switch level {
case 1:
return 6, nil
case 2:
return 3, nil
case 3:
return 0, nil
}
return 0, nil
}
return int64(bonus), nil
}
// getConfigFloat 获取配置值(浮点数)

View File

@@ -10,8 +10,10 @@ import (
"ycc-server/app/main/api/internal/svc"
"github.com/pkg/errors"
"github.com/wechatpay-apiv3/wechatpay-go/services/payments"
"github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/core/stores/sqlx"
)
type WechatPayCallbackLogic struct {
@@ -40,6 +42,9 @@ func (l *WechatPayCallbackLogic) WechatPayCallback(w http.ResponseWriter, r *htt
if strings.HasPrefix(orderNo, "Q_") {
// 查询订单处理
return l.handleQueryOrderPayment(w, notification)
} else if strings.HasPrefix(orderNo, "U_") {
// 代理升级订单处理
return l.handleAgentUpgradeOrderPayment(w, notification)
} else if strings.HasPrefix(orderNo, "A_") {
// 旧系统会员充值订单(已废弃,新系统使用升级功能)
// return l.handleAgentVipOrderPayment(w, notification)
@@ -104,6 +109,101 @@ func (l *WechatPayCallbackLogic) handleQueryOrderPayment(w http.ResponseWriter,
return nil
}
// 处理代理升级订单支付
func (l *WechatPayCallbackLogic) handleAgentUpgradeOrderPayment(w http.ResponseWriter, notification *payments.Transaction) error {
orderNo := *notification.OutTradeNo
// 1. 查找订单
order, findOrderErr := l.svcCtx.OrderModel.FindOneByOrderNo(l.ctx, orderNo)
if findOrderErr != nil {
logx.Errorf("微信支付回调,查找升级订单失败: %+v", findOrderErr)
return nil
}
// 2. 验证金额
amount := lzUtils.ToWechatAmount(order.Amount)
if amount != *notification.Amount.Total {
logx.Errorf("微信支付回调,升级订单金额不一致,订单号: %s", orderNo)
return nil
}
// 3. 检查订单状态
if order.Status != "pending" {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("success"))
return nil
}
// 4. 查找升级记录
upgradeRecords, findUpgradeErr := l.svcCtx.AgentUpgradeModel.FindAll(l.ctx, l.svcCtx.AgentUpgradeModel.SelectBuilder().
Where("order_no = ?", orderNo).
Limit(1), "")
if findUpgradeErr != nil || len(upgradeRecords) == 0 {
logx.Errorf("微信支付回调,查找升级记录失败,订单号: %s, 错误: %+v", orderNo, findUpgradeErr)
return nil
}
upgradeRecord := upgradeRecords[0]
// 5. 检查升级记录状态
if upgradeRecord.Status != 1 {
// 升级记录状态不是待支付,直接返回成功
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("success"))
return nil
}
// 6. 处理支付状态
switch *notification.TradeState {
case service.TradeStateSuccess:
order.Status = "paid"
order.PayTime = lzUtils.TimeToNullTime(time.Now())
order.PlatformOrderId = lzUtils.StringToNullString(*notification.TransactionId)
case service.TradeStateClosed:
order.Status = "closed"
order.CloseTime = lzUtils.TimeToNullTime(time.Now())
case service.TradeStateRevoked:
order.Status = "failed"
default:
return nil
}
// 7. 更新订单状态
if updateErr := l.svcCtx.OrderModel.UpdateWithVersion(l.ctx, nil, order); updateErr != nil {
logx.Errorf("微信支付回调,更新升级订单状态失败: %+v", updateErr)
return nil
}
// 8. 如果支付成功,执行升级操作
if order.Status == "paid" {
err := l.svcCtx.AgentWalletModel.Trans(l.ctx, func(transCtx context.Context, session sqlx.Session) error {
// 8.1 执行升级操作
if err := l.svcCtx.AgentService.ProcessUpgrade(transCtx, upgradeRecord.AgentId, upgradeRecord.ToLevel, upgradeRecord.UpgradeType, upgradeRecord.UpgradeFee, upgradeRecord.RebateAmount, orderNo, 0); err != nil {
return errors.Wrapf(err, "执行升级操作失败")
}
// 8.2 更新升级记录状态为已完成
upgradeRecord.Status = 2 // 已完成status: 1=待处理2=已完成3=已失败)
upgradeRecord.Remark = lzUtils.StringToNullString("支付成功,升级完成")
if updateErr := l.svcCtx.AgentUpgradeModel.UpdateWithVersion(transCtx, session, upgradeRecord); updateErr != nil {
return errors.Wrapf(updateErr, "更新升级记录状态失败")
}
return nil
})
if err != nil {
logx.Errorf("微信支付回调,处理升级订单失败,订单号: %s, 错误: %+v", orderNo, err)
// 即使升级失败,也返回成功给微信,避免重复回调
} else {
logx.Infof("微信支付回调,代理升级成功,订单号: %s, 代理ID: %d, 从等级 %d 升级到等级 %d", orderNo, upgradeRecord.AgentId, upgradeRecord.FromLevel, upgradeRecord.ToLevel)
}
}
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("success"))
return nil
}
// 处理代理会员订单支付(已废弃,新系统使用升级功能)
/*
func (l *WechatPayCallbackLogic) handleAgentVipOrderPayment(w http.ResponseWriter, notification *payments.Transaction) error {

View File

@@ -5,13 +5,14 @@ import (
"encoding/hex"
"encoding/json"
"fmt"
"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"
"ycc-server/pkg/lzkit/validator"
"time"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/stores/redis"
@@ -582,7 +583,7 @@ func (l *QueryServiceLogic) ProcessConsumerFinanceReportLogic(req *types.QuerySe
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 处理用户失败: %v", err)
}
cacheNo, cacheDataErr := l.CacheData(params, "personalData", userID)
cacheNo, cacheDataErr := l.CacheData(params, "consumerFinanceReport", userID)
if cacheDataErr != nil {
return nil, cacheDataErr
}
@@ -616,6 +617,10 @@ func (l *QueryServiceLogic) DecryptData(data string) ([]byte, error) {
// 校验验证码
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 {
@@ -637,6 +642,10 @@ func (l *QueryServiceLogic) VerifyCode(mobile string, code string) error {
// 二、三要素验证
func (l *QueryServiceLogic) Verify(Name string, IDCard string, Mobile string) error {
// 开发环境下跳过二/三要素验证
if os.Getenv("ENV") == "development" {
return nil
}
if !l.svcCtx.Config.SystemConfig.ThreeVerify {
twoVerification := service.TwoFactorVerificationRequest{
Name: Name,

View File

@@ -4,6 +4,7 @@ import (
"context"
"database/sql"
"fmt"
"os"
"time"
"ycc-server/app/main/api/internal/svc"
@@ -42,7 +43,8 @@ func (l *BindMobileLogic) BindMobile(req *types.BindMobileReq) (resp *types.Bind
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "绑定手机号, 加密手机号失败: %v", err)
}
if req.Mobile != "18889793585" {
// 开发环境下跳过验证码校验
if os.Getenv("ENV") != "development" {
// 检查手机号是否在一分钟内已发送过验证码
redisKey := fmt.Sprintf("%s:%s", "bindMobile", encryptedMobile)
cacheCode, err := l.svcCtx.Redis.Get(redisKey)

View File

@@ -2,14 +2,15 @@ package user
import (
"context"
"database/sql"
"fmt"
"os"
"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/pkg/lzkit/crypto"
"database/sql"
"fmt"
"time"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/stores/redis"
@@ -37,17 +38,20 @@ func (l *MobileCodeLoginLogic) MobileCodeLogin(req *types.MobileCodeLoginReq) (r
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "手机登录, 加密手机号失败: %+v", err)
}
// 检查手机号是否在一分钟内已发送过验证码
redisKey := fmt.Sprintf("%s:%s", "login", encryptedMobile)
cacheCode, err := l.svcCtx.Redis.Get(redisKey)
if err != nil {
if errors.Is(err, redis.Nil) {
return nil, errors.Wrapf(xerr.NewErrMsg("验证码已过期"), "手机登录, 验证码过期: %s", encryptedMobile)
// 开发环境下跳过验证码校验
if os.Getenv("ENV") != "development" {
// 检查手机号是否在一分钟内已发送过验证码
redisKey := fmt.Sprintf("%s:%s", "login", encryptedMobile)
cacheCode, err := l.svcCtx.Redis.Get(redisKey)
if err != nil {
if errors.Is(err, redis.Nil) {
return nil, errors.Wrapf(xerr.NewErrMsg("验证码已过期"), "手机登录, 验证码过期: %s", encryptedMobile)
}
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "手机登录, 读取验证码redis缓存失败, mobile: %s, err: %+v", encryptedMobile, err)
}
if cacheCode != req.Code {
return nil, errors.Wrapf(xerr.NewErrMsg("验证码不正确"), "手机登录, 验证码不正确: %s", encryptedMobile)
}
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "手机登录, 读取验证码redis缓存失败, mobile: %s, err: %+v", encryptedMobile, err)
}
if cacheCode != req.Code {
return nil, errors.Wrapf(xerr.NewErrMsg("验证码不正确"), "手机登录, 验证码不正确: %s", encryptedMobile)
}
var userID int64
user, findUserErr := l.svcCtx.UserModel.FindOneByMobile(l.ctx, sql.NullString{String: encryptedMobile, Valid: true})

View File

@@ -0,0 +1,63 @@
package queue
import (
"context"
"encoding/json"
"errors"
"fmt"
"ycc-server/app/main/api/internal/svc"
"ycc-server/app/main/api/internal/types"
"ycc-server/app/main/model"
"github.com/hibiken/asynq"
"github.com/zeromicro/go-zero/core/logx"
)
type AgentProcessHandler struct {
svcCtx *svc.ServiceContext
}
func NewAgentProcessHandler(svcCtx *svc.ServiceContext) *AgentProcessHandler {
return &AgentProcessHandler{
svcCtx: svcCtx,
}
}
func (l *AgentProcessHandler) ProcessTask(ctx context.Context, t *asynq.Task) error {
var payload types.MsgAgentProcessPayload
if err := json.Unmarshal(t.Payload(), &payload); err != nil {
return fmt.Errorf("解析代理处理任务负载失败: %w", err)
}
// 获取订单信息
order, err := l.svcCtx.OrderModel.FindOne(ctx, payload.OrderID)
if err != nil {
if errors.Is(err, model.ErrNotFound) {
logx.Errorf("代理处理任务失败,订单不存在: orderID=%d", payload.OrderID)
return asynq.SkipRetry // 订单不存在,跳过重试
}
return fmt.Errorf("查询订单失败: orderID=%d, err=%w", payload.OrderID, err)
}
// 检查订单状态
if order.Status != "paid" {
logx.Infof("代理处理任务跳过,订单未支付: orderID=%d, status=%s", payload.OrderID, order.Status)
return nil // 订单未支付,不处理,不重试
}
// 调用代理处理服务
err = l.svcCtx.AgentService.AgentProcess(ctx, order)
if err != nil {
// 记录错误日志,但不阻塞报告流程
logx.Errorf("代理处理失败订单ID: %d, 错误: %v", payload.OrderID, err)
// 返回错误以触发重试机制
return fmt.Errorf("代理处理失败: orderID=%d, err=%w", payload.OrderID, err)
}
// 注意:解冻任务现在通过定时任务扫描处理,不再需要发送延迟任务
// 定时任务每5分钟扫描一次待解冻的任务更加可靠
logx.Infof("代理处理成功订单ID: %d冻结任务如有将由定时任务自动处理", payload.OrderID)
logx.Infof("代理处理成功订单ID: %d", payload.OrderID)
return nil
}

View File

@@ -2,13 +2,13 @@ package queue
import (
"context"
"ycc-server/app/main/api/internal/svc"
"ycc-server/app/main/model"
"ycc-server/common/globalkey"
"database/sql"
"fmt"
"strconv"
"time"
"ycc-server/app/main/api/internal/svc"
"ycc-server/app/main/model"
"ycc-server/common/globalkey"
"github.com/hibiken/asynq"
"github.com/zeromicro/go-zero/core/logx"
@@ -48,11 +48,16 @@ func (l *CleanQueryDataHandler) getConfigValue(ctx context.Context, key string)
}
func (l *CleanQueryDataHandler) ProcessTask(ctx context.Context, t *asynq.Task) error {
// 添加超时控制最多运行1小时
taskCtx, cancel := context.WithTimeout(ctx, 1*time.Hour)
defer cancel()
startTime := time.Now()
now := time.Now()
logx.Infof("%s - 开始执行查询数据清理任务", now.Format("2006-01-02 15:04:05"))
// 1. 检查是否启用清理
enableCleanup, err := l.getConfigValue(ctx, "enable_cleanup")
enableCleanup, err := l.getConfigValue(taskCtx, "enable_cleanup")
if err != nil {
return err
}
@@ -62,29 +67,53 @@ func (l *CleanQueryDataHandler) ProcessTask(ctx context.Context, t *asynq.Task)
}
// 2. 获取保留天数
retentionDaysStr, err := l.getConfigValue(ctx, "retention_days")
retentionDaysStr, err := l.getConfigValue(taskCtx, "retention_days")
if err != nil {
return err
}
retentionDays, err := strconv.Atoi(retentionDaysStr)
if err != nil {
return err
return fmt.Errorf("保留天数配置无效: %v", err)
}
if retentionDays < 0 {
return fmt.Errorf("保留天数不能为负数: %d", retentionDays)
}
// 3. 获取批次大小
batchSizeStr, err := l.getConfigValue(ctx, "batch_size")
batchSizeStr, err := l.getConfigValue(taskCtx, "batch_size")
if err != nil {
return err
}
batchSize, err := strconv.Atoi(batchSizeStr)
if err != nil {
return err
return fmt.Errorf("批次大小配置无效: %v", err)
}
if batchSize <= 0 || batchSize > 10000 {
return fmt.Errorf("批次大小必须在1-10000之间: %d", batchSize)
}
// 计算清理截止时间
cleanupBefore := now.AddDate(0, 0, -retentionDays)
// 创建清理日志记录
// 先快速检查是否有数据需要清理(避免创建无用的日志记录
checkBuilder := l.svcCtx.QueryModel.SelectBuilder().
Where("create_time < ?", cleanupBefore).
Where("del_state = ?", globalkey.DelStateNo).
Limit(1) // 只查询1条用于判断是否有数据
checkQueries, checkErr := l.svcCtx.QueryModel.FindAll(taskCtx, checkBuilder, "")
if checkErr != nil {
logx.Errorf("检查是否有数据需要清理失败: %v", checkErr)
return checkErr
}
// 如果没有数据需要清理,直接返回,不创建日志记录
if len(checkQueries) == 0 {
logx.Infof("%s - 没有需要清理的数据(清理截止时间: %s", now.Format("2006-01-02 15:04:05"), cleanupBefore.Format("2006-01-02 15:04:05"))
return nil
}
// 创建清理日志记录(只创建一次)
cleanupLog := &model.QueryCleanupLog{
CleanupTime: now,
CleanupBefore: cleanupBefore,
@@ -92,52 +121,75 @@ func (l *CleanQueryDataHandler) ProcessTask(ctx context.Context, t *asynq.Task)
Remark: sql.NullString{String: "定时清理数据", Valid: true},
}
// 使用事务处理清理操作和日志记录
err = l.svcCtx.QueryModel.Trans(ctx, func(ctx context.Context, session sqlx.Session) error {
// 分批处理
for {
// 1. 查询一批要删除的记录
// 先创建清理日志记录
var cleanupLogId int64
err = l.svcCtx.QueryCleanupLogModel.Trans(taskCtx, func(logCtx context.Context, logSession sqlx.Session) error {
cleanupLogInsertResult, insertErr := l.svcCtx.QueryCleanupLogModel.Insert(logCtx, logSession, cleanupLog)
if insertErr != nil {
return insertErr
}
cleanupLogId, insertErr = cleanupLogInsertResult.LastInsertId()
return insertErr
})
if err != nil {
logx.Errorf("创建清理日志记录失败: %v", err)
return err
}
logx.Infof("创建清理日志记录成功日志ID: %d", cleanupLogId)
// 分批处理,每个批次使用独立事务
batchCount := 0
lastProcessedId := int64(0)
for {
// 检查是否被取消(优雅关闭支持)
select {
case <-taskCtx.Done():
logx.Infof("清理任务被取消,已处理 %d 批次,共删除 %d 条记录", batchCount, cleanupLog.AffectedRows)
// 更新清理日志状态
l.updateCleanupLogStatus(taskCtx, cleanupLogId, cleanupLog, fmt.Errorf("任务被取消"))
return taskCtx.Err()
default:
// 继续处理
}
// 每个批次使用独立事务
var batchQueries []*model.Query
batchErr := l.svcCtx.QueryModel.Trans(taskCtx, func(batchCtx context.Context, batchSession sqlx.Session) error {
// 1. 查询一批要删除的记录(添加排序确保一致性)
builder := l.svcCtx.QueryModel.SelectBuilder().
Where("create_time < ?", cleanupBefore).
Where("del_state = ?", globalkey.DelStateNo).
OrderBy("id ASC"). // 添加排序,确保处理顺序一致
Limit(uint64(batchSize))
queries, err := l.svcCtx.QueryModel.FindAll(ctx, builder, "")
if err != nil {
cleanupLog.Status = 2
cleanupLog.ErrorMsg = sql.NullString{String: err.Error(), Valid: true}
return err
// 如果已处理过从上次处理的ID之后继续
if lastProcessedId > 0 {
builder = builder.Where("id > ?", lastProcessedId)
}
if len(queries) == 0 {
break // 没有更多数据需要清理
var queryErr error
batchQueries, queryErr = l.svcCtx.QueryModel.FindAll(batchCtx, builder, "")
if queryErr != nil {
return queryErr
}
if len(batchQueries) == 0 {
// 没有更多数据需要清理,标记为完成
return nil
}
// 2. 执行清理
for _, query := range queries {
err = l.svcCtx.QueryModel.DeleteSoft(ctx, session, query)
if err != nil {
cleanupLog.Status = 2
cleanupLog.ErrorMsg = sql.NullString{String: err.Error(), Valid: true}
return err
for _, query := range batchQueries {
deleteErr := l.svcCtx.QueryModel.DeleteSoft(batchCtx, batchSession, query)
if deleteErr != nil {
return deleteErr
}
}
// 3. 更新影响行数
cleanupLog.AffectedRows += int64(len(queries))
// 4. 保存清理日志(每批次都记录)
cleanupLogInsertResult, err := l.svcCtx.QueryCleanupLogModel.Insert(ctx, session, cleanupLog)
if err != nil {
return err
}
cleanupLogId, err := cleanupLogInsertResult.LastInsertId()
if err != nil {
return err
}
// 5. 保存清理明细
for _, query := range queries {
// 3. 保存清理明细
for _, query := range batchQueries {
detail := &model.QueryCleanupDetail{
CleanupLogId: cleanupLogId,
QueryId: query.Id,
@@ -147,21 +199,76 @@ func (l *CleanQueryDataHandler) ProcessTask(ctx context.Context, t *asynq.Task)
QueryState: query.QueryState,
CreateTimeOld: query.CreateTime,
}
_, err = l.svcCtx.QueryCleanupDetailModel.Insert(ctx, session, detail)
if err != nil {
return err
_, insertErr := l.svcCtx.QueryCleanupDetailModel.Insert(batchCtx, batchSession, detail)
if insertErr != nil {
return insertErr
}
}
// 4. 记录最后处理的ID用于下次查询
lastProcessedId = batchQueries[len(batchQueries)-1].Id
return nil
})
if batchErr != nil {
// 批次失败,更新清理日志状态
logx.Errorf("批次处理失败(批次 %d: %v", batchCount+1, batchErr)
l.updateCleanupLogStatus(taskCtx, cleanupLogId, cleanupLog, batchErr)
return batchErr
}
return nil
// 如果查询结果为空,说明没有更多数据
if len(batchQueries) == 0 {
logx.Infof("所有数据已处理完成")
break
}
// 更新影响行数(在事务外更新,避免重复计算)
actualBatchSize := int64(len(batchQueries))
cleanupLog.AffectedRows += actualBatchSize
batchCount++
logx.Infof("批次 %d 处理完成,本批次删除 %d 条记录,累计删除 %d 条记录", batchCount, actualBatchSize, cleanupLog.AffectedRows)
// 如果本批次查询到的数据少于批次大小,说明已经处理完所有数据
if actualBatchSize < int64(batchSize) {
logx.Infof("所有数据已处理完成(本批次数据量少于批次大小)")
break
}
}
// 更新清理日志状态为成功
l.updateCleanupLogStatus(taskCtx, cleanupLogId, cleanupLog, nil)
duration := time.Since(startTime)
logx.Infof("%s - 查询数据清理完成,共处理 %d 批次,删除 %d 条记录,耗时 %v",
now.Format("2006-01-02 15:04:05"), batchCount, cleanupLog.AffectedRows, duration)
return nil
}
// updateCleanupLogStatus 更新清理日志状态
func (l *CleanQueryDataHandler) updateCleanupLogStatus(ctx context.Context, logId int64, cleanupLog *model.QueryCleanupLog, err error) {
err = l.svcCtx.QueryCleanupLogModel.Trans(ctx, func(updateCtx context.Context, updateSession sqlx.Session) error {
// 查询当前日志记录
currentLog, findErr := l.svcCtx.QueryCleanupLogModel.FindOne(updateCtx, logId)
if findErr != nil {
return findErr
}
// 更新状态和影响行数
currentLog.AffectedRows = cleanupLog.AffectedRows
if err != nil {
currentLog.Status = 2 // 失败
currentLog.ErrorMsg = sql.NullString{String: err.Error(), Valid: true}
} else {
currentLog.Status = 1 // 成功
}
_, updateErr := l.svcCtx.QueryCleanupLogModel.Update(updateCtx, updateSession, currentLog)
return updateErr
})
if err != nil {
logx.Errorf("%s - 清理查询数据失败: %v", now.Format("2006-01-02 15:04:05"), err)
return err
logx.Errorf("更新清理日志状态失败: %v", err)
}
logx.Infof("%s - 查询数据清理完成,共删除 %d 条记录", now.Format("2006-01-02 15:04:05"), cleanupLog.AffectedRows)
return nil
}

View File

@@ -2,17 +2,17 @@ package queue
import (
"context"
"ycc-server/app/main/api/internal/svc"
"ycc-server/app/main/api/internal/types"
"ycc-server/app/main/model"
"ycc-server/pkg/lzkit/crypto"
"ycc-server/pkg/lzkit/lzUtils"
"encoding/hex"
"encoding/json"
"fmt"
"os"
"regexp"
"strings"
"ycc-server/app/main/api/internal/svc"
"ycc-server/app/main/api/internal/types"
"ycc-server/app/main/model"
"ycc-server/pkg/lzkit/crypto"
"ycc-server/pkg/lzkit/lzUtils"
"github.com/hibiken/asynq"
"github.com/zeromicro/go-zero/core/logx"
@@ -40,7 +40,9 @@ func (l *PaySuccessNotifyUserHandler) ProcessTask(ctx context.Context, t *asynq.
order, err := l.svcCtx.OrderModel.FindOne(ctx, payload.OrderID)
if err != nil {
return fmt.Errorf("无效的订单ID: %d, %v", payload.OrderID, err)
// 订单不存在,记录详细日志并跳过重试
logx.Errorf("支付成功通知任务失败订单不存在订单ID: %d, 错误: %v", payload.OrderID, err)
return asynq.SkipRetry // 订单不存在时跳过重试,避免重复失败
}
env := os.Getenv("ENV")
if order.Status != "paid" && env != "development" {
@@ -139,17 +141,39 @@ func (l *PaySuccessNotifyUserHandler) ProcessTask(ctx context.Context, t *asynq.
}
}
// 调用API请求服务
combinedResponse, err := l.svcCtx.ApiRequestService.ProcessRequests(decryptData, product.Id)
if err != nil {
return l.handleError(ctx, err, order, query)
}
// 加密返回响应
encryptData, aesEncryptErr := crypto.AesEncrypt(combinedResponse, key)
if aesEncryptErr != nil {
err = fmt.Errorf("加密响应信息失败: %v", aesEncryptErr)
return l.handleError(ctx, err, order, query)
// 检查是否为空报告模式(开发环境)
isEmptyReportMode := env == "development" && order.PaymentPlatform == "test"
var encryptData string
if isEmptyReportMode {
// 空报告模式生成空的报告数据跳过API调用
logx.Infof("空报告模式:订单 %s (ID: %d) 跳过API调用生成空报告", order.OrderNo, order.Id)
// 生成空报告数据结构(根据实际报告格式生成)
emptyReportData := []byte(`[]`) // 空数组,表示没有数据
// 加密空报告数据
encryptedEmptyData, aesEncryptErr := crypto.AesEncrypt(emptyReportData, key)
if aesEncryptErr != nil {
err = fmt.Errorf("加密空报告数据失败: %v", aesEncryptErr)
return l.handleError(ctx, err, order, query)
}
encryptData = encryptedEmptyData
} else {
// 正常模式调用API请求服务
combinedResponse, err := l.svcCtx.ApiRequestService.ProcessRequests(decryptData, product.Id)
if err != nil {
return l.handleError(ctx, err, order, query)
}
// 加密返回响应
encryptedResponse, aesEncryptErr := crypto.AesEncrypt(combinedResponse, key)
if aesEncryptErr != nil {
err = fmt.Errorf("加密响应信息失败: %v", aesEncryptErr)
return l.handleError(ctx, err, order, query)
}
encryptData = encryptedResponse
}
query.QueryData = lzUtils.StringToNullString(encryptData)
updateErr := l.svcCtx.QueryModel.UpdateWithVersion(ctx, nil, query)
if updateErr != nil {
@@ -164,9 +188,10 @@ func (l *PaySuccessNotifyUserHandler) ProcessTask(ctx context.Context, t *asynq.
return l.handleError(ctx, updateQueryErr, order, query)
}
err = l.svcCtx.AgentService.AgentProcess(ctx, order)
if err != nil {
return l.handleError(ctx, err, order, query)
// 报告生成成功后,发送代理处理异步任务(不阻塞报告流程)
if asyncErr := l.svcCtx.AsynqService.SendAgentProcessTask(order.Id); asyncErr != nil {
// 代理处理任务发送失败,只记录日志,不影响报告流程
logx.Errorf("发送代理处理任务失败订单ID: %d, 错误: %v", order.Id, asyncErr)
}
_, delErr := l.svcCtx.Redis.DelCtx(ctx, redisKey)

View File

@@ -2,9 +2,9 @@ package queue
import (
"context"
"fmt"
"ycc-server/app/main/api/internal/svc"
"ycc-server/app/main/api/internal/types"
"fmt"
"github.com/hibiken/asynq"
)
@@ -24,17 +24,30 @@ func NewCronJob(ctx context.Context, svcCtx *svc.ServiceContext) *CronJob {
func (l *CronJob) Register() *asynq.ServeMux {
redisClientOpt := asynq.RedisClientOpt{Addr: l.svcCtx.Config.CacheRedis[0].Host, Password: l.svcCtx.Config.CacheRedis[0].Pass}
scheduler := asynq.NewScheduler(redisClientOpt, nil)
// 注册数据清理定时任务每天凌晨3点
task := asynq.NewTask(types.MsgCleanQueryData, nil, nil)
_, err := scheduler.Register(TASKTIME, task)
if err != nil {
panic(fmt.Sprintf("定时任务注册失败:%v", err))
}
// 注册解冻佣金扫描定时任务每2小时执行一次
unfreezeScanTask := asynq.NewTask(types.MsgUnfreezeCommissionScan, nil, nil)
_, err = scheduler.Register("0 */2 * * *", unfreezeScanTask) // 每2小时执行一次每小时的第0分钟
if err != nil {
panic(fmt.Sprintf("解冻佣金扫描定时任务注册失败:%v", err))
}
scheduler.Start()
fmt.Println("定时任务启动!!!")
mux := asynq.NewServeMux()
mux.Handle(types.MsgPaySuccessQuery, NewPaySuccessNotifyUserHandler(l.svcCtx))
mux.Handle(types.MsgCleanQueryData, NewCleanQueryDataHandler(l.svcCtx))
mux.Handle(types.MsgAgentProcess, NewAgentProcessHandler(l.svcCtx))
mux.Handle(types.MsgUnfreezeCommission, NewUnfreezeCommissionHandler(l.svcCtx))
mux.Handle(types.MsgUnfreezeCommissionScan, NewUnfreezeCommissionScanHandler(l.svcCtx))
return mux
}

View File

@@ -0,0 +1,94 @@
package queue
import (
"context"
"encoding/json"
"errors"
"fmt"
"time"
"ycc-server/app/main/model"
"ycc-server/pkg/lzkit/lzUtils"
"ycc-server/app/main/api/internal/svc"
"ycc-server/app/main/api/internal/types"
"github.com/hibiken/asynq"
pkgerrors "github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/core/stores/sqlx"
)
type UnfreezeCommissionHandler struct {
svcCtx *svc.ServiceContext
}
func NewUnfreezeCommissionHandler(svcCtx *svc.ServiceContext) *UnfreezeCommissionHandler {
return &UnfreezeCommissionHandler{
svcCtx: svcCtx,
}
}
func (l *UnfreezeCommissionHandler) ProcessTask(ctx context.Context, t *asynq.Task) error {
var payload types.MsgUnfreezeCommissionPayload
if err := json.Unmarshal(t.Payload(), &payload); err != nil {
return fmt.Errorf("解析解冻任务负载失败: %w", err)
}
// 1. 查询冻结任务
freezeTask, err := l.svcCtx.AgentFreezeTaskModel.FindOne(ctx, payload.FreezeTaskId)
if err != nil {
if errors.Is(err, model.ErrNotFound) {
logx.Errorf("解冻任务失败,冻结任务不存在: freezeTaskId=%d", payload.FreezeTaskId)
return asynq.SkipRetry // 任务不存在,跳过重试
}
return fmt.Errorf("查询冻结任务失败: freezeTaskId=%d, err=%w", payload.FreezeTaskId, err)
}
// 2. 检查任务状态
if freezeTask.Status != 1 {
logx.Infof("解冻任务跳过,任务已处理: freezeTaskId=%d, status=%d", payload.FreezeTaskId, freezeTask.Status)
return nil // 任务已处理,不重试
}
// 3. 检查解冻时间是否已到
if time.Now().Before(freezeTask.UnfreezeTime) {
logx.Infof("解冻任务跳过,未到解冻时间: freezeTaskId=%d, unfreezeTime=%v", payload.FreezeTaskId, freezeTask.UnfreezeTime)
// 重新发送延迟任务
if err := l.svcCtx.AsynqService.SendUnfreezeTask(payload.FreezeTaskId, freezeTask.UnfreezeTime); err != nil {
logx.Errorf("重新发送解冻任务失败: freezeTaskId=%d, err=%v", payload.FreezeTaskId, err)
}
return nil
}
// 4. 使用事务处理解冻
err = l.svcCtx.AgentFreezeTaskModel.Trans(ctx, func(transCtx context.Context, session sqlx.Session) error {
// 4.1 更新冻结任务状态
freezeTask.Status = 2 // 已解冻
freezeTask.ActualUnfreezeTime = lzUtils.TimeToNullTime(time.Now())
if updateErr := l.svcCtx.AgentFreezeTaskModel.UpdateWithVersion(transCtx, session, freezeTask); updateErr != nil {
return pkgerrors.Wrapf(updateErr, "更新冻结任务状态失败")
}
// 4.2 更新钱包(解冻余额)
wallet, walletErr := l.svcCtx.AgentWalletModel.FindOneByAgentId(transCtx, freezeTask.AgentId)
if walletErr != nil {
return pkgerrors.Wrapf(walletErr, "查询钱包失败, agentId: %d", freezeTask.AgentId)
}
wallet.FrozenBalance -= freezeTask.FreezeAmount
wallet.Balance += freezeTask.FreezeAmount
if updateWalletErr := l.svcCtx.AgentWalletModel.UpdateWithVersion(transCtx, session, wallet); updateWalletErr != nil {
return pkgerrors.Wrapf(updateWalletErr, "更新钱包失败")
}
return nil
})
if err != nil {
logx.Errorf("解冻任务处理失败: freezeTaskId=%d, err=%v", payload.FreezeTaskId, err)
return fmt.Errorf("解冻任务处理失败: freezeTaskId=%d, err=%w", payload.FreezeTaskId, err)
}
logx.Infof("解冻任务处理成功: freezeTaskId=%d, agentId=%d, amount=%.2f", payload.FreezeTaskId, freezeTask.AgentId, freezeTask.FreezeAmount)
return nil
}

View File

@@ -0,0 +1,233 @@
package queue
import (
"context"
"strings"
"sync"
"time"
"ycc-server/app/main/model"
"ycc-server/pkg/lzkit/lzUtils"
"ycc-server/app/main/api/internal/svc"
"github.com/hibiken/asynq"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/core/stores/sqlx"
)
// UnfreezeCommissionScanHandler 定时扫描解冻任务处理器
type UnfreezeCommissionScanHandler struct {
svcCtx *svc.ServiceContext
}
func NewUnfreezeCommissionScanHandler(svcCtx *svc.ServiceContext) *UnfreezeCommissionScanHandler {
return &UnfreezeCommissionScanHandler{
svcCtx: svcCtx,
}
}
// ProcessTask 定时扫描需要解冻的任务
func (l *UnfreezeCommissionScanHandler) ProcessTask(ctx context.Context, t *asynq.Task) error {
scanStartTime := time.Now()
now := time.Now()
logx.Infof("开始扫描需要解冻的佣金任务,当前时间: %v", now)
// 1. 查询所有待解冻且解冻时间已到的任务
// 使用索引 idx_status 和 idx_unfreeze_time 优化查询
// 不限制查询数量,找到所有需要解冻的任务
builder := l.svcCtx.AgentFreezeTaskModel.SelectBuilder().
Where("status = ? AND unfreeze_time <= ? AND del_state = ?", 1, now, 0). // 1=待解冻0=未删除
OrderBy("unfreeze_time ASC") // 按解冻时间升序,优先处理最早的任务
freezeTasks, err := l.svcCtx.AgentFreezeTaskModel.FindAll(ctx, builder, "")
if err != nil {
logx.Errorf("查询待解冻任务失败: %v", err)
return errors.Wrapf(err, "查询待解冻任务失败")
}
// 如果没有需要解冻的任务,直接返回(不创建任何记录,只记录日志)
if len(freezeTasks) == 0 {
scanDuration := time.Since(scanStartTime)
logx.Infof("没有需要解冻的任务,扫描耗时: %v", scanDuration)
return nil
}
// 2. 批次大小限制:如果任务量过大,分批处理
const maxBatchSize = 1000
originalCount := len(freezeTasks)
if len(freezeTasks) > maxBatchSize {
logx.Errorf("任务数量过多(%d),本次只处理前%d个剩余将在下次扫描处理", len(freezeTasks), maxBatchSize)
freezeTasks = freezeTasks[:maxBatchSize]
}
logx.Infof("找到 %d 个需要解冻的任务(原始数量: %d开始处理最多同时处理2个", len(freezeTasks), originalCount)
// 3. 并发控制使用信号量限制最多同时处理2个任务
const maxConcurrency = 2 // 最多同时处理2个任务
const taskTimeout = 30 * time.Second // 每个任务30秒超时
semaphore := make(chan struct{}, maxConcurrency) // 信号量通道
var wg sync.WaitGroup
var mu sync.Mutex // 保护计数器的互斥锁
successCount := 0
failCount := 0
skipCount := 0 // 跳过的任务数(已处理、时间未到等)
// 4. 并发处理所有任务但最多同时处理2个
for _, freezeTask := range freezeTasks {
// 检查是否被取消(优雅关闭支持)
select {
case <-ctx.Done():
logx.Infof("扫描任务被取消,已处理: 成功=%d, 失败=%d, 跳过=%d", successCount, failCount, skipCount)
return ctx.Err()
default:
// 继续处理
}
wg.Add(1)
semaphore <- struct{}{} // 获取信号量如果已满2个则阻塞
go func(task *model.AgentFreezeTask) {
defer wg.Done()
defer func() { <-semaphore }() // 释放信号量
taskStartTime := time.Now()
// 为每个任务设置超时控制
taskCtx, cancel := context.WithTimeout(ctx, taskTimeout)
defer cancel()
// 使用事务处理每个任务,确保原子性
err := l.svcCtx.AgentFreezeTaskModel.Trans(taskCtx, func(transCtx context.Context, session sqlx.Session) error {
// 4.1 重新查询任务(使用乐观锁,确保并发安全)
currentTask, err := l.svcCtx.AgentFreezeTaskModel.FindOne(transCtx, task.Id)
if err != nil {
if errors.Is(err, model.ErrNotFound) {
logx.Infof("冻结任务不存在,可能已被处理: freezeTaskId=%d", task.Id)
return nil // 任务不存在,跳过
}
return errors.Wrapf(err, "查询冻结任务失败, freezeTaskId: %d", task.Id)
}
// 4.2 幂等性增强:检查是否已经解冻过(通过 actual_unfreeze_time
if currentTask.ActualUnfreezeTime.Valid {
logx.Infof("任务已解冻,跳过: freezeTaskId=%d, actualUnfreezeTime=%v", task.Id, currentTask.ActualUnfreezeTime.Time)
return nil // 已解冻,跳过
}
// 4.3 检查任务状态(双重检查,防止并发处理)
if currentTask.Status != 1 {
logx.Infof("冻结任务状态已变更,跳过处理: freezeTaskId=%d, status=%d", task.Id, currentTask.Status)
return nil // 状态已变更,跳过
}
// 4.4 再次检查解冻时间(防止时间判断误差)
nowTime := time.Now()
if nowTime.Before(currentTask.UnfreezeTime) {
logx.Infof("冻结任务解冻时间未到,跳过处理: freezeTaskId=%d, unfreezeTime=%v", task.Id, currentTask.UnfreezeTime)
return nil // 时间未到,跳过
}
// 4.5 计算延迟时间(便于监控)
delay := nowTime.Sub(currentTask.UnfreezeTime)
if delay > 1*time.Hour {
logx.Errorf("解冻任务延迟处理: freezeTaskId=%d, 延迟=%v, unfreezeTime=%v", task.Id, delay, currentTask.UnfreezeTime)
}
// 4.6 更新冻结任务状态
currentTask.Status = 2 // 已解冻
currentTask.ActualUnfreezeTime = lzUtils.TimeToNullTime(nowTime)
if updateErr := l.svcCtx.AgentFreezeTaskModel.UpdateWithVersion(transCtx, session, currentTask); updateErr != nil {
return errors.Wrapf(updateErr, "更新冻结任务状态失败, freezeTaskId: %d", task.Id)
}
// 4.7 更新钱包(解冻余额)
wallet, walletErr := l.svcCtx.AgentWalletModel.FindOneByAgentId(transCtx, currentTask.AgentId)
if walletErr != nil {
return errors.Wrapf(walletErr, "查询钱包失败, agentId: %d", currentTask.AgentId)
}
// 检查冻结余额是否足够(防止数据异常)
if wallet.FrozenBalance < currentTask.FreezeAmount {
logx.Errorf("钱包冻结余额不足,数据异常: freezeTaskId=%d, agentId=%d, frozenBalance=%.2f, freezeAmount=%.2f",
task.Id, currentTask.AgentId, wallet.FrozenBalance, currentTask.FreezeAmount)
return errors.Errorf("钱包冻结余额不足: agentId=%d, frozenBalance=%.2f, freezeAmount=%.2f",
currentTask.AgentId, wallet.FrozenBalance, currentTask.FreezeAmount)
}
wallet.FrozenBalance -= currentTask.FreezeAmount
wallet.Balance += currentTask.FreezeAmount
if updateWalletErr := l.svcCtx.AgentWalletModel.UpdateWithVersion(transCtx, session, wallet); updateWalletErr != nil {
return errors.Wrapf(updateWalletErr, "更新钱包失败, agentId: %d", currentTask.AgentId)
}
// 更详细的日志(包含更多上下文信息)
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)
return nil
})
// 记录处理时间
taskDuration := time.Since(taskStartTime)
if taskDuration > 5*time.Second {
logx.Errorf("解冻任务处理耗时较长: freezeTaskId=%d, duration=%v", task.Id, taskDuration)
}
// 更新计数器(需要加锁保护)
mu.Lock()
if err != nil {
// 错误分类处理
if isTemporaryError(err) {
// 临时错误(如超时、网络问题),记录但继续处理其他任务
failCount++
logx.Errorf("解冻任务临时失败,将在下次扫描重试: freezeTaskId=%d, duration=%v, err=%v", task.Id, taskDuration, err)
} else {
// 永久错误(如数据异常),记录详细日志
failCount++
logx.Errorf("解冻任务永久失败: freezeTaskId=%d, duration=%v, err=%v", task.Id, taskDuration, err)
}
} else {
successCount++
logx.Infof("解冻任务处理完成: freezeTaskId=%d, duration=%v", task.Id, taskDuration)
}
mu.Unlock()
}(freezeTask)
}
// 5. 等待所有任务完成
wg.Wait()
// 6. 记录扫描统计信息
scanDuration := time.Since(scanStartTime)
logx.Infof("解冻任务扫描完成: 成功=%d, 失败=%d, 跳过=%d, 总计=%d, 扫描耗时=%v",
successCount, failCount, skipCount, len(freezeTasks), scanDuration)
return nil
}
// isTemporaryError 判断是否为临时错误(可以重试的错误)
func isTemporaryError(err error) bool {
if err == nil {
return false
}
errStr := err.Error()
// 超时错误
if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) {
return true
}
// 网络相关错误
errStrLower := strings.ToLower(errStr)
if strings.Contains(errStrLower, "timeout") || strings.Contains(errStrLower, "connection") || strings.Contains(errStrLower, "network") {
return true
}
// 数据库连接错误
if strings.Contains(errStrLower, "connection pool") || strings.Contains(errStrLower, "too many connections") {
return true
}
// 其他错误视为永久错误(如数据异常、业务逻辑错误等)
return false
}

View File

@@ -1,211 +0,0 @@
package service
import (
"context"
"database/sql"
"time"
"ycc-server/app/main/model"
"ycc-server/common/xerr"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/core/stores/sqlx"
)
type AdminPromotionLinkStatsService struct {
logx.Logger
AdminPromotionLinkModel model.AdminPromotionLinkModel
AdminPromotionLinkStatsTotalModel model.AdminPromotionLinkStatsTotalModel
AdminPromotionLinkStatsHistoryModel model.AdminPromotionLinkStatsHistoryModel
}
func NewAdminPromotionLinkStatsService(
AdminPromotionLinkModel model.AdminPromotionLinkModel,
AdminPromotionLinkStatsTotalModel model.AdminPromotionLinkStatsTotalModel,
AdminPromotionLinkStatsHistoryModel model.AdminPromotionLinkStatsHistoryModel,
) *AdminPromotionLinkStatsService {
return &AdminPromotionLinkStatsService{
Logger: logx.WithContext(context.Background()),
AdminPromotionLinkModel: AdminPromotionLinkModel,
AdminPromotionLinkStatsTotalModel: AdminPromotionLinkStatsTotalModel,
AdminPromotionLinkStatsHistoryModel: AdminPromotionLinkStatsHistoryModel,
}
}
// ensureTotalStats 确保总统计记录存在,如果不存在则创建
func (s *AdminPromotionLinkStatsService) ensureTotalStats(ctx context.Context, session sqlx.Session, linkId int64) (*model.AdminPromotionLinkStatsTotal, error) {
totalStats, err := s.AdminPromotionLinkStatsTotalModel.FindOneByLinkId(ctx, linkId)
if err != nil {
if errors.Is(err, model.ErrNotFound) {
// 如果记录不存在,创建新记录
totalStats = &model.AdminPromotionLinkStatsTotal{
LinkId: linkId,
ClickCount: 0,
PayCount: 0,
PayAmount: 0,
}
_, err = s.AdminPromotionLinkStatsTotalModel.Insert(ctx, session, totalStats)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建总统计记录失败: %+v", err)
}
// 重新获取创建后的记录
totalStats, err = s.AdminPromotionLinkStatsTotalModel.FindOneByLinkId(ctx, linkId)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取新创建的总统计记录失败: %+v", err)
}
} else {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询总统计失败: %+v", err)
}
}
return totalStats, nil
}
// ensureHistoryStats 确保历史统计记录存在,如果不存在则创建
func (s *AdminPromotionLinkStatsService) ensureHistoryStats(ctx context.Context, session sqlx.Session, linkId int64, today time.Time) (*model.AdminPromotionLinkStatsHistory, error) {
historyStats, err := s.AdminPromotionLinkStatsHistoryModel.FindOneByLinkIdStatsDate(ctx, linkId, today)
if err != nil {
if errors.Is(err, model.ErrNotFound) {
// 如果记录不存在,创建新记录
historyStats = &model.AdminPromotionLinkStatsHistory{
LinkId: linkId,
StatsDate: today,
ClickCount: 0,
PayCount: 0,
PayAmount: 0,
}
_, err = s.AdminPromotionLinkStatsHistoryModel.Insert(ctx, session, historyStats)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建今日统计记录失败: %+v", err)
}
// 重新获取创建后的记录
historyStats, err = s.AdminPromotionLinkStatsHistoryModel.FindOneByLinkIdStatsDate(ctx, linkId, today)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取新创建的今日统计记录失败: %+v", err)
}
} else {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询今日统计记录失败: %+v", err)
}
}
return historyStats, nil
}
// UpdateLinkStats 更新推广链接统计
func (s *AdminPromotionLinkStatsService) UpdateLinkStats(ctx context.Context, linkId int64) error {
return s.AdminPromotionLinkStatsTotalModel.Trans(ctx, func(ctx context.Context, session sqlx.Session) error {
// 确保总统计记录存在
totalStats, err := s.ensureTotalStats(ctx, session, linkId)
if err != nil {
return err
}
// 更新总统计
totalStats.ClickCount++
totalStats.LastClickTime = sql.NullTime{Time: time.Now(), Valid: true}
err = s.AdminPromotionLinkStatsTotalModel.UpdateWithVersion(ctx, session, totalStats)
if err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新总统计失败: %+v", err)
}
// 确保历史统计记录存在
now := time.Now()
today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.Local)
historyStats, err := s.ensureHistoryStats(ctx, session, linkId, today)
if err != nil {
return err
}
// 更新历史统计
historyStats.ClickCount++
historyStats.LastClickTime = sql.NullTime{Time: time.Now(), Valid: true}
err = s.AdminPromotionLinkStatsHistoryModel.UpdateWithVersion(ctx, session, historyStats)
if err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新历史统计失败: %+v", err)
}
return nil
})
}
// UpdatePaymentStats 更新付费统计
func (s *AdminPromotionLinkStatsService) UpdatePaymentStats(ctx context.Context, linkId int64, amount float64) error {
return s.AdminPromotionLinkStatsTotalModel.Trans(ctx, func(ctx context.Context, session sqlx.Session) error {
// 确保总统计记录存在
totalStats, err := s.ensureTotalStats(ctx, session, linkId)
if err != nil {
return err
}
// 更新总统计
totalStats.PayCount++
totalStats.PayAmount += amount
totalStats.LastPayTime = sql.NullTime{Time: time.Now(), Valid: true}
err = s.AdminPromotionLinkStatsTotalModel.UpdateWithVersion(ctx, session, totalStats)
if err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新总统计失败: %+v", err)
}
// 确保历史统计记录存在
now := time.Now()
today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.Local)
historyStats, err := s.ensureHistoryStats(ctx, session, linkId, today)
if err != nil {
return err
}
// 更新历史统计
historyStats.PayCount++
historyStats.PayAmount += amount
historyStats.LastPayTime = sql.NullTime{Time: time.Now(), Valid: true}
err = s.AdminPromotionLinkStatsHistoryModel.UpdateWithVersion(ctx, session, historyStats)
if err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新历史统计失败: %+v", err)
}
return nil
})
}
// CreateLinkStats 创建新的推广链接统计记录
func (s *AdminPromotionLinkStatsService) CreateLinkStats(ctx context.Context, linkId int64) error {
return s.AdminPromotionLinkStatsTotalModel.Trans(ctx, func(ctx context.Context, session sqlx.Session) error {
// 检查总统计记录是否已存在
_, err := s.AdminPromotionLinkStatsTotalModel.FindOneByLinkId(ctx, linkId)
if err == nil {
// 记录已存在,不需要创建
return nil
}
if err != model.ErrNotFound {
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询总统计记录失败: %+v", err)
}
// 创建总统计记录
totalStats := &model.AdminPromotionLinkStatsTotal{
LinkId: linkId,
ClickCount: 0,
PayCount: 0,
PayAmount: 0,
}
_, err = s.AdminPromotionLinkStatsTotalModel.Insert(ctx, session, totalStats)
if err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建总统计记录失败: %+v", err)
}
// 创建今日历史统计记录
now := time.Now()
today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.Local)
historyStats := &model.AdminPromotionLinkStatsHistory{
LinkId: linkId,
StatsDate: today,
ClickCount: 0,
PayCount: 0,
PayAmount: 0,
}
_, err = s.AdminPromotionLinkStatsHistoryModel.Insert(ctx, session, historyStats)
if err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建历史统计记录失败: %+v", err)
}
return nil
})
}

View File

@@ -3,6 +3,7 @@ package service
import (
"context"
"database/sql"
"fmt"
"strconv"
"time"
"ycc-server/app/main/api/internal/config"
@@ -17,21 +18,22 @@ import (
// AgentService 新代理系统服务
type AgentService struct {
config config.Config
OrderModel model.OrderModel
AgentModel model.AgentModel
AgentWalletModel model.AgentWalletModel
AgentRelationModel model.AgentRelationModel
AgentLinkModel model.AgentLinkModel
AgentOrderModel model.AgentOrderModel
AgentCommissionModel model.AgentCommissionModel
AgentRebateModel model.AgentRebateModel
AgentUpgradeModel model.AgentUpgradeModel
AgentWithdrawalModel model.AgentWithdrawalModel
AgentConfigModel model.AgentConfigModel
config config.Config
OrderModel model.OrderModel
AgentModel model.AgentModel
AgentWalletModel model.AgentWalletModel
AgentRelationModel model.AgentRelationModel
AgentLinkModel model.AgentLinkModel
AgentOrderModel model.AgentOrderModel
AgentCommissionModel model.AgentCommissionModel
AgentRebateModel model.AgentRebateModel
AgentUpgradeModel model.AgentUpgradeModel
AgentWithdrawalModel model.AgentWithdrawalModel
AgentConfigModel model.AgentConfigModel
AgentProductConfigModel model.AgentProductConfigModel
AgentRealNameModel model.AgentRealNameModel
AgentRealNameModel model.AgentRealNameModel
AgentWithdrawalTaxModel model.AgentWithdrawalTaxModel
AgentFreezeTaskModel model.AgentFreezeTaskModel // 冻结任务模型需要先运行SQL并生成model
}
// NewAgentService 创建新的代理服务
@@ -51,23 +53,25 @@ func NewAgentService(
agentProductConfigModel model.AgentProductConfigModel,
agentRealNameModel model.AgentRealNameModel,
agentWithdrawalTaxModel model.AgentWithdrawalTaxModel,
agentFreezeTaskModel model.AgentFreezeTaskModel, // 冻结任务模型需要先运行SQL并生成model
) *AgentService {
return &AgentService{
config: c,
OrderModel: orderModel,
AgentModel: agentModel,
AgentWalletModel: agentWalletModel,
AgentRelationModel: agentRelationModel,
AgentLinkModel: agentLinkModel,
AgentOrderModel: agentOrderModel,
AgentCommissionModel: agentCommissionModel,
AgentRebateModel: agentRebateModel,
AgentUpgradeModel: agentUpgradeModel,
AgentWithdrawalModel: agentWithdrawalModel,
AgentConfigModel: agentConfigModel,
config: c,
OrderModel: orderModel,
AgentModel: agentModel,
AgentWalletModel: agentWalletModel,
AgentRelationModel: agentRelationModel,
AgentLinkModel: agentLinkModel,
AgentOrderModel: agentOrderModel,
AgentCommissionModel: agentCommissionModel,
AgentRebateModel: agentRebateModel,
AgentUpgradeModel: agentUpgradeModel,
AgentWithdrawalModel: agentWithdrawalModel,
AgentConfigModel: agentConfigModel,
AgentProductConfigModel: agentProductConfigModel,
AgentRealNameModel: agentRealNameModel,
AgentRealNameModel: agentRealNameModel,
AgentWithdrawalTaxModel: agentWithdrawalTaxModel,
AgentFreezeTaskModel: agentFreezeTaskModel,
}
}
@@ -96,27 +100,42 @@ func (s *AgentService) AgentProcess(ctx context.Context, order *model.Order) err
return errors.Wrapf(err, "查询代理信息失败, agentId: %d", agentOrder.AgentId)
}
// 4. 获取系统配置
basePrice, err := s.getConfigFloat(ctx, "base_price")
// 4. 获取产品配置(必须存在)
productConfig, err := s.AgentProductConfigModel.FindOneByProductId(ctx, order.ProductId)
if err != nil {
return errors.Wrapf(err, "获取基础底价配置失败")
if errors.Is(err, model.ErrNotFound) {
return errors.Wrapf(err, "产品配置不存在, productId: %d请先在后台配置产品价格参数", order.ProductId)
}
return errors.Wrapf(err, "查询产品配置失败, productId: %d", order.ProductId)
}
// 6. 使用事务处理订单
return s.AgentWalletModel.Trans(ctx, func(transCtx context.Context, session sqlx.Session) error {
// 6.1 计算实际底价和代理收益
levelBonus := s.getLevelBonus(agent.Level)
// 5. 使用事务处理订单
err = s.AgentWalletModel.Trans(ctx, func(transCtx context.Context, session sqlx.Session) error {
// 5.1 获取等级加成
levelBonus, err := s.getLevelBonus(transCtx, agent.Level)
if err != nil {
return errors.Wrapf(err, "获取等级加成配置失败")
}
// 5.2 使用产品配置的底价计算实际底价
basePrice := productConfig.BasePrice
actualBasePrice := basePrice + float64(levelBonus)
// 6.2 计算提价成本
priceThreshold, _ := s.getConfigFloat(ctx, "price_threshold")
priceFeeRate, _ := s.getConfigFloat(ctx, "price_fee_rate")
// 5.3 计算提价成本(使用产品配置)
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 计算代理收益
// 5.4 计算代理收益
agentProfit := agentOrder.SetPrice - actualBasePrice - priceCost
// 6.4 更新代理订单记录
// 5.5 更新代理订单记录
agentOrder.ProcessStatus = 1
agentOrder.ProcessTime = lzUtils.TimeToNullTime(time.Now())
agentOrder.ProcessRemark = lzUtils.StringToNullString("处理成功")
@@ -124,12 +143,14 @@ func (s *AgentService) AgentProcess(ctx context.Context, order *model.Order) err
return errors.Wrapf(err, "更新代理订单失败")
}
// 6.5 发放代理佣金
if err := s.giveAgentCommission(transCtx, session, agentOrder.AgentId, order.Id, order.ProductId, agentProfit); err != nil {
return errors.Wrapf(err, "发放代理佣金失败")
// 5.6 发放代理佣金(传入订单单价用于冻结判断)
// 注意:冻结任务会在 agentProcess.go 中通过查询订单的冻结任务来发送解冻任务
_, commissionErr := s.giveAgentCommission(transCtx, session, agentOrder.AgentId, order.Id, order.ProductId, agentProfit, agentOrder.SetPrice)
if commissionErr != nil {
return errors.Wrapf(commissionErr, "发放代理佣金失败")
}
// 6.6 分配等级加成返佣给上级链
// 5.7 分配等级加成返佣给上级链
if levelBonus > 0 {
if err := s.distributeLevelBonus(transCtx, session, agent, order.Id, order.ProductId, float64(levelBonus), levelBonus); err != nil {
return errors.Wrapf(err, "分配等级加成返佣失败")
@@ -138,20 +159,39 @@ func (s *AgentService) AgentProcess(ctx context.Context, order *model.Order) err
return nil
})
return err
}
// getLevelBonus 获取等级加成
func (s *AgentService) getLevelBonus(level int64) int64 {
// getLevelBonus 获取等级加成(从配置表读取)
func (s *AgentService) getLevelBonus(ctx context.Context, level int64) (int64, error) {
var configKey string
switch level {
case 1: // 普通
return 6
configKey = "level_1_bonus"
case 2: // 黄金
return 3
configKey = "level_2_bonus"
case 3: // 钻石
return 0
configKey = "level_3_bonus"
default:
return 0
return 0, nil
}
bonus, err := s.getConfigFloat(ctx, configKey)
if err != nil {
// 配置不存在时返回默认值
logx.Errorf("获取等级加成配置失败, level: %d, key: %s, err: %v使用默认值", level, configKey, err)
switch level {
case 1:
return 6, nil
case 2:
return 3, nil
case 3:
return 0, nil
}
return 0, nil
}
return int64(bonus), nil
}
// calculatePriceCost 计算提价成本
@@ -163,7 +203,9 @@ func (s *AgentService) calculatePriceCost(setPrice, priceThreshold, priceFeeRate
}
// giveAgentCommission 发放代理佣金
func (s *AgentService) giveAgentCommission(ctx context.Context, session sqlx.Session, agentId, orderId, productId int64, amount float64) error {
// orderPrice: 订单单价,用于判断是否需要冻结
// 返回freezeTaskId如果有冻结任务error
func (s *AgentService) giveAgentCommission(ctx context.Context, session sqlx.Session, agentId, orderId, productId int64, amount float64, orderPrice float64) (int64, error) {
// 1. 创建佣金记录
commission := &model.AgentCommission{
AgentId: agentId,
@@ -172,23 +214,94 @@ func (s *AgentService) giveAgentCommission(ctx context.Context, session sqlx.Ses
Amount: amount,
Status: 1, // 已发放
}
if _, err := s.AgentCommissionModel.Insert(ctx, session, commission); err != nil {
return errors.Wrapf(err, "创建佣金记录失败")
commissionResult, err := s.AgentCommissionModel.Insert(ctx, session, commission)
if err != nil {
return 0, errors.Wrapf(err, "创建佣金记录失败")
}
commissionId, _ := commissionResult.LastInsertId()
// 2. 判断是否需要冻结
// 2.1 获取冻结阈值配置默认100元
freezeThreshold, err := s.getConfigFloat(ctx, "commission_freeze_threshold")
if err != nil {
// 配置不存在时使用默认值100元
freezeThreshold = 100.0
logx.Errorf("获取冻结阈值配置失败使用默认值100元, orderId: %d, err: %v", orderId, err)
}
// 2. 更新钱包余额
// 2.2 判断订单单价是否达到冻结阈值
freezeAmount := 0.0
var freezeTaskId int64 = 0
if orderPrice >= freezeThreshold {
// 2.3 获取冻结比例配置默认10%
freezeRatio, err := s.getConfigFloat(ctx, "commission_freeze_ratio")
if err != nil {
// 配置不存在时使用默认值0.110%
freezeRatio = 0.1
logx.Errorf("获取冻结比例配置失败使用默认值10%%, orderId: %d, err: %v", orderId, err)
}
// 计算冻结金额订单单价的10%
freezeAmountByPrice := orderPrice * freezeRatio
// 冻结金额不能超过佣金金额
if freezeAmountByPrice > amount {
freezeAmount = amount
} else {
freezeAmount = freezeAmountByPrice
}
// 如果冻结金额大于0创建冻结任务
if freezeAmount > 0 {
// 2.4 获取解冻天数配置默认30天即1个月
unfreezeDays, err := s.getConfigInt(ctx, "commission_freeze_days")
if err != nil {
// 配置不存在时使用默认值30天
unfreezeDays = 30
logx.Errorf("获取解冻天数配置失败使用默认值30天, orderId: %d, err: %v", orderId, err)
}
// 计算解冻时间(从配置读取的天数后)
// 注意:配置只在创建任务时读取,已创建的任务不受后续配置修改影响
unfreezeTime := time.Now().AddDate(0, 0, int(unfreezeDays))
// 创建冻结任务记录
freezeTask := &model.AgentFreezeTask{
AgentId: agentId,
OrderId: orderId,
CommissionId: commissionId,
FreezeAmount: freezeAmount,
OrderPrice: orderPrice,
FreezeRatio: freezeRatio,
Status: 1, // 待解冻
FreezeTime: time.Now(),
UnfreezeTime: unfreezeTime,
Remark: lzUtils.StringToNullString(fmt.Sprintf("订单单价%.2f元,冻结比例%.2f%%,解冻天数%d天", orderPrice, freezeRatio*100, unfreezeDays)),
}
freezeTaskResult, err := s.AgentFreezeTaskModel.Insert(ctx, session, freezeTask)
if err != nil {
return 0, errors.Wrapf(err, "创建冻结任务失败")
}
freezeTaskId, _ = freezeTaskResult.LastInsertId()
}
}
// 3. 更新钱包余额
wallet, err := s.AgentWalletModel.FindOneByAgentId(ctx, agentId)
if err != nil {
return errors.Wrapf(err, "查询钱包失败, agentId: %d", agentId)
return 0, errors.Wrapf(err, "查询钱包失败, agentId: %d", agentId)
}
wallet.Balance += amount
wallet.TotalEarnings += amount
// 实际到账金额 = 佣金金额 - 冻结金额
actualAmount := amount - freezeAmount
wallet.Balance += actualAmount
wallet.FrozenBalance += freezeAmount
wallet.TotalEarnings += amount // 累计收益包含冻结部分
if err := s.AgentWalletModel.UpdateWithVersion(ctx, session, wallet); err != nil {
return errors.Wrapf(err, "更新钱包失败")
return 0, errors.Wrapf(err, "更新钱包失败")
}
return nil
return freezeTaskId, nil
}
// distributeLevelBonus 分配等级加成返佣给上级链
@@ -219,7 +332,51 @@ func (s *AgentService) distributeLevelBonus(ctx context.Context, session sqlx.Se
return nil
}
// distributeNormalAgentBonus 普通代理的等级加成返佣分配6元
// distributeNormalAgentBonus 普通代理的等级加成返佣分配
//
// 功能说明:根据普通代理的直接上级等级,按照规则分配等级加成返佣
//
// 参数说明:
// - amount: 等级加成总额例如6元
// - levelBonusInt: 等级加成整数(用于记录)
//
// 分配规则总览:
// 1. 直接上级是钻石:等级加成全部给钻石
// 2. 直接上级是黄金一部分给黄金配置direct_parent_amount_gold默认3元剩余给钻石上级
// 3. 直接上级是普通一部分给直接上级配置direct_parent_amount_normal默认2元剩余给钻石/黄金上级
//
// 覆盖的所有情况:
//
// 情况1普通(推广人) -> 钻石(直接上级)
// => 全部给钻石
//
// 情况2普通(推广人) -> 黄金(直接上级) -> 钻石
// => 一部分给黄金,剩余给钻石
//
// 情况3普通(推广人) -> 黄金(直接上级) -> 无钻石上级
// => 一部分给黄金,剩余归平台
//
// 情况4普通(推广人) -> 普通(直接上级) -> 钻石
// => 一部分给直接上级普通,剩余全部给钻石
//
// 情况5普通(推广人) -> 普通(直接上级) -> 黄金 -> 钻石
// => 一部分给直接上级普通例如2元一部分给黄金等级加成差减去给普通的例如3-2=1元剩余给钻石例如3元
//
// 情况6普通(推广人) -> 普通(直接上级) -> 黄金(无钻石)
// => 一部分给直接上级普通剩余一部分给黄金最多3元超出归平台
//
// 情况7普通(推广人) -> 普通(直接上级) -> 普通 -> 钻石
// => 一部分给直接上级普通,剩余全部给钻石(跳过中间普通代理)
//
// 情况8普通(推广人) -> 普通(直接上级) -> 普通 -> 黄金(无钻石)
// => 一部分给直接上级普通剩余一部分给黄金最多3元超出归平台跳过中间普通代理
//
// 情况9普通(推广人) -> 普通(直接上级) -> 普通 -> 普通...(全部是普通)
// => 一部分给直接上级普通,剩余归平台
//
// 注意findDiamondParent 和 findGoldParent 会自动跳过中间的所有普通代理,
//
// 直接向上查找到第一个钻石或黄金代理
func (s *AgentService) distributeNormalAgentBonus(ctx context.Context, session sqlx.Session, agent *model.Agent, orderId, productId int64, amount float64, levelBonusInt int64) error {
// 1. 查找直接上级
parent, err := s.findDirectParent(ctx, agent.Id)
@@ -232,67 +389,240 @@ func (s *AgentService) distributeNormalAgentBonus(ctx context.Context, session s
return nil
}
// 2. 直接上级分配固定金额
var directParentAmount float64
// 2. 根据直接上级等级分配
switch parent.Level {
case 3: // 钻石
directParentAmount = 6
case 2: // 黄金
directParentAmount = 3
case 1: // 普通
directParentAmount = 2
default:
directParentAmount = 0
}
case 3: // 直接上级是钻石代理的情况
// ========== 直接上级是钻石:等级加成全部给钻石上级 ==========
// 场景示例:
// - 普通(推广人) -> 钻石(直接上级)等级加成6元全部给钻石
// 说明:如果直接上级就是钻石,不需要再向上查找,全部返佣给直接上级钻石
// rebateType = 2表示钻石上级返佣
return s.giveRebate(ctx, session, parent.Id, agent.Id, orderId, productId, amount, levelBonusInt, 2)
if directParentAmount > 0 {
if err := s.giveRebate(ctx, session, parent.Id, agent.Id, orderId, productId, directParentAmount, levelBonusInt, 1); err != nil {
return errors.Wrapf(err, "给直接上级返佣失败")
case 2: // 直接上级是黄金代理的情况
// ========== 步骤1给直接上级黄金代理返佣 ==========
// 配置键direct_parent_amount_gold普通代理给直接上级黄金代理的返佣金额
// 默认值3.0元
// 说明:这部分金额给直接上级黄金代理,剩余部分继续向上分配给钻石上级
goldRebateAmount, err := s.getRebateConfigFloat(ctx, "direct_parent_amount_gold", 3.0)
if err != nil {
logx.Errorf("获取黄金返佣配置失败使用默认值3元: %v", err)
goldRebateAmount = 3.0 // 配置读取失败时使用默认值3元
}
}
remaining := amount - directParentAmount
if remaining <= 0 {
return nil
}
// 3. 分配剩余金额
// 确定查找起点:直接上级是普通时从直接上级开始查找,否则从直接上级的上级开始查找
searchStart := parent
if parent.Level != 1 {
searchStartParent, err := s.findDirectParent(ctx, parent.Id)
if err != nil && !errors.Is(err, model.ErrNotFound) {
return errors.Wrapf(err, "查找上级的上级失败")
// 计算给直接上级黄金代理的返佣金额(不能超过总等级加成金额)
goldAmount := goldRebateAmount // 默认3元
if goldAmount > amount {
// 如果配置金额大于等级加成总额,则只给总额(防止配置错误导致负数)
goldAmount = amount
}
if searchStartParent != nil {
searchStart = searchStartParent
}
}
if searchStart != nil {
// 查找上级链中的钻石和黄金
diamondParent, _ := s.findDiamondParent(ctx, searchStart.Id)
goldParent, _ := s.findGoldParent(ctx, searchStart.Id)
// 按优先级分配剩余金额
if diamondParent != nil {
// 优先级1有钻石剩余金额全部给钻石
return s.giveRebate(ctx, session, diamondParent.Id, agent.Id, orderId, productId, remaining, levelBonusInt, 2)
} else if goldParent != nil {
// 优先级2只有黄金最多3元给黄金剩余归平台
goldAmount := remaining
if goldAmount > 3 {
goldAmount = 3
}
if err := s.giveRebate(ctx, session, goldParent.Id, agent.Id, orderId, productId, goldAmount, levelBonusInt, 3); err != nil {
// 发放返佣给直接上级黄金代理
// rebateType = 1表示直接上级返佣
if goldAmount > 0 {
if err := s.giveRebate(ctx, session, parent.Id, agent.Id, orderId, productId, goldAmount, levelBonusInt, 1); err != nil {
return errors.Wrapf(err, "给黄金上级返佣失败")
}
// 剩余归平台(不需要记录)
}
// 优先级3都没有剩余金额归平台不需要记录
}
return nil
// ========== 步骤2计算剩余金额并分配给钻石上级 ==========
// 剩余金额 = 总等级加成 - 已给黄金上级的金额
// 例如等级加成6元 - 给黄金上级3元 = 剩余3元
remaining := amount - goldAmount
if remaining > 0 {
// 从黄金上级开始向上查找钻石上级
// 场景示例:
// - 普通(推广人) -> 黄金(直接上级) -> 钻石剩余3元给钻石
// - 普通(推广人) -> 黄金(直接上级) -> 普通 -> 钻石剩余3元给钻石跳过中间普通代理
// - 普通(推广人) -> 黄金(直接上级) -> 无上级剩余3元归平台没有钻石上级
diamondParent, err := s.findDiamondParent(ctx, parent.Id)
if err != nil && !errors.Is(err, model.ErrNotFound) {
return errors.Wrapf(err, "查找钻石上级失败")
}
if diamondParent != nil {
// 找到钻石上级,剩余金额全部给钻石上级
// rebateType = 2表示钻石上级返佣
return s.giveRebate(ctx, session, diamondParent.Id, agent.Id, orderId, productId, remaining, levelBonusInt, 2)
}
// 找不到钻石上级,剩余金额归平台(不需要记录)
// 例如等级加成6元给黄金3元剩余3元但找不到钻石上级则剩余3元归平台
}
return nil
case 1: // 直接上级是普通代理的情况
// ========== 步骤1给直接上级普通代理返佣 ==========
// 配置键direct_parent_amount_normal普通代理给直接上级普通代理的返佣金额
// 默认值2.0元
// 说明:无论后续层级有多少普通代理,这部分金额只给推广人的直接上级
normalRebateAmount, err := s.getRebateConfigFloat(ctx, "direct_parent_amount_normal", 2.0)
if err != nil {
logx.Errorf("获取普通返佣配置失败使用默认值2元: %v", err)
normalRebateAmount = 2.0 // 配置读取失败时使用默认值2元
}
// 计算给直接上级的返佣金额(不能超过总等级加成金额)
directAmount := normalRebateAmount // 默认2元
if directAmount > amount {
// 如果配置金额大于等级加成总额,则只给总额(防止配置错误导致负数)
directAmount = amount
}
// 发放返佣给直接上级普通代理
// rebateType = 1表示直接上级返佣
if directAmount > 0 {
if err := s.giveRebate(ctx, session, parent.Id, agent.Id, orderId, productId, directAmount, levelBonusInt, 1); err != nil {
return errors.Wrapf(err, "给直接上级返佣失败")
}
}
// ========== 步骤2计算剩余金额 ==========
// 剩余金额 = 总等级加成 - 已给直接上级的金额
// 例如等级加成6元 - 给直接上级2元 = 剩余4元
remaining := amount - directAmount
if remaining <= 0 {
// 如果没有剩余,直接返回(所有金额已分配给直接上级)
return nil
}
// ========== 步骤3从直接上级开始向上查找钻石和黄金代理 ==========
// 注意findDiamondParent 和 findGoldParent 会自动跳过中间的所有普通代理
// 例如:
// - 普通 -> 普通 -> 普通 -> 钻石:会跳过中间的普通代理,直接找到钻石
// - 普通 -> 普通 -> 黄金 -> 钻石:会找到钻石(优先级更高)
// - 普通 -> 黄金:会找到黄金
diamondParent, _ := s.findDiamondParent(ctx, parent.Id) // 向上查找钻石上级(跳过所有普通和黄金)
goldParent, _ := s.findGoldParent(ctx, parent.Id) // 向上查找黄金上级(跳过所有普通)
// ========== 步骤4按优先级分配剩余金额 ==========
// 优先级规则:
// 1. 优先给钻石上级(如果存在)
// 2. 其次给黄金上级(如果钻石不存在)
// 3. 最后归平台(如果钻石和黄金都不存在)
if diamondParent != nil {
// ========== 情况A找到钻石上级 ==========
// 场景示例:
// - 普通(推广人) -> 普通(直接上级) -> 钻石给普通2元剩余4元给钻石
// - 普通(推广人) -> 普通(直接上级) -> 黄金 -> 钻石给普通2元给黄金1元剩余3元给钻石
// - 普通(推广人) -> 普通(直接上级) -> 普通 -> 普通 -> 钻石给普通2元剩余4元给钻石跳过中间普通代理
//
// 分配规则:当有钻石上级时,需要给黄金上级一部分返佣
// 1. 等级加成的差 = 普通等级加成 - 黄金等级加成例如6元 - 3元 = 3元
// 2. 从这个差中先给直接上级普通代理已给例如2元
// 3. 差减去给普通的部分剩余给黄金代理例如3元 - 2元 = 1元
// 4. 最后剩余部分给钻石例如6元 - 2元 - 1元 = 3元
// 步骤A1计算等级加成的差
// 获取普通代理等级加成当前代理的等级加成即amount
normalBonus := amount // 例如6元
// 获取黄金代理等级加成
goldBonus, err := s.getLevelBonus(ctx, 2) // 黄金等级加成
if err != nil {
logx.Errorf("获取黄金等级加成配置失败使用默认值3元: %v", err)
goldBonus = 3 // 默认3元
}
// 计算等级加成的差
bonusDiff := normalBonus - float64(goldBonus) // 例如6元 - 3元 = 3元
// 步骤A2如果有黄金上级给黄金上级一部分返佣
// 规则说明:
// - 等级加成的差bonusDiff代表普通代理和黄金代理的等级加成差异
// - 从这个差中先分配给直接上级普通代理directAmount已分配
// - 剩余的差分配给黄金代理goldRebateAmount = bonusDiff - directAmount
// - 如果差小于等于已给普通的部分,则不给黄金(这种情况理论上不应该发生,因为差应该>=给普通的部分)
if goldParent != nil && bonusDiff > 0 {
// 计算给黄金上级的金额 = 等级加成差 - 已给普通上级的金额
// 例如等级加成差3元 - 已给普通2元 = 给黄金1元
goldRebateAmount := bonusDiff - directAmount
// 如果计算出的金额小于等于0说明差已经被普通代理全部占用了不给黄金
// 例如如果差是2元已给普通2元则 goldRebateAmount = 0不给黄金
if goldRebateAmount > 0 {
// 边界检查:确保不超过剩余金额(理论上应该不会超过,但保险起见)
if goldRebateAmount > remaining {
goldRebateAmount = remaining
}
// 发放返佣给黄金上级
// rebateType = 3表示黄金上级返佣
if err := s.giveRebate(ctx, session, goldParent.Id, agent.Id, orderId, productId, goldRebateAmount, levelBonusInt, 3); err != nil {
return errors.Wrapf(err, "给黄金上级返佣失败")
}
// 更新剩余金额(用于后续分配给钻石)
// 例如剩余4元 - 给黄金1元 = 剩余3元给钻石
remaining = remaining - goldRebateAmount
}
}
// 步骤A3剩余金额全部给钻石上级
// rebateType = 2表示钻石上级返佣
if remaining > 0 {
return s.giveRebate(ctx, session, diamondParent.Id, agent.Id, orderId, productId, remaining, levelBonusInt, 2)
}
return nil
} else if goldParent != nil {
// ========== 情况B没有钻石上级但有黄金上级 ==========
// 场景示例:
// - 普通(推广人) -> 普通(直接上级) -> 黄金(没有钻石):给黄金一部分,剩余归平台
// - 普通(推广人) -> 普通(直接上级) -> 普通 -> 黄金(没有钻石):给黄金一部分,剩余归平台(跳过中间普通代理)
// 配置键max_gold_rebate_amount普通代理给黄金上级的最大返佣金额
// 默认值3.0元
// 说明即使剩余金额超过这个值也只能给黄金上级最多3元超出部分归平台
maxGoldRebate, err := s.getRebateConfigFloat(ctx, "max_gold_rebate_amount", 3.0)
if err != nil {
logx.Errorf("获取黄金最大返佣配置失败使用默认值3元: %v", err)
maxGoldRebate = 3.0 // 配置读取失败时使用默认值3元
}
// 计算给黄金上级的返佣金额
goldAmount := remaining // 剩余金额
if goldAmount > maxGoldRebate {
// 如果剩余金额超过最大限额则只给最大限额例如剩余4元但最多只能给3元
goldAmount = maxGoldRebate
}
// 例如剩余4元最大限额3元则给黄金3元剩余1元归平台
// 发放返佣给黄金上级
// rebateType = 3表示黄金上级返佣
if goldAmount > 0 {
if err := s.giveRebate(ctx, session, goldParent.Id, agent.Id, orderId, productId, goldAmount, levelBonusInt, 3); err != nil {
return errors.Wrapf(err, "给黄金上级返佣失败")
}
}
// 超出最大限额的部分归平台(不需要记录)
// 例如剩余4元给黄金3元剩余1元归平台
return nil
}
// ========== 情况C既没有钻石上级也没有黄金上级 ==========
// 场景示例:
// - 普通(推广人) -> 普通(直接上级) -> 普通 -> 普通...(整个链路都是普通代理)
// - 普通(推广人) -> 普通(直接上级) -> 无上级(已经是最顶层)
// 剩余金额全部归平台(不需要记录)
return nil
default:
// 未知等级,全部归平台
return nil
}
}
// getRebateConfigFloat 获取返佣配置值(浮点数),如果配置不存在则返回默认值
func (s *AgentService) getRebateConfigFloat(ctx context.Context, configKey string, defaultValue float64) (float64, error) {
config, err := s.AgentConfigModel.FindOneByConfigKey(ctx, configKey)
if err != nil {
if errors.Is(err, model.ErrNotFound) {
return defaultValue, nil
}
return defaultValue, err
}
value, err := strconv.ParseFloat(config.ConfigValue, 64)
if err != nil {
return defaultValue, errors.Wrapf(err, "解析配置值失败, key: %s, value: %s", configKey, config.ConfigValue)
}
return value, nil
}
// giveRebate 发放返佣
@@ -350,6 +680,18 @@ func (s *AgentService) findDirectParent(ctx context.Context, agentId int64) (*mo
}
// findDiamondParent 向上查找钻石上级
//
// 功能说明从指定代理开始向上逐级查找第一个钻石代理Level == 3
//
// 查找规则:
// - 自动跳过所有普通代理Level == 1和黄金代理Level == 2
// - 只返回第一个找到的钻石代理
// - 如果没有找到钻石代理,返回 ErrNotFound
//
// 示例场景:
// - 普通 -> 普通 -> 钻石:会找到钻石(跳过中间的普通代理)
// - 普通 -> 黄金 -> 钻石:会找到钻石(跳过黄金代理)
// - 普通 -> 普通 -> 黄金:返回 ErrNotFound没有钻石
func (s *AgentService) findDiamondParent(ctx context.Context, agentId int64) (*model.Agent, error) {
currentId := agentId
maxDepth := 100 // 防止无限循环
@@ -376,6 +718,20 @@ func (s *AgentService) findDiamondParent(ctx context.Context, agentId int64) (*m
}
// findGoldParent 向上查找黄金上级
//
// 功能说明从指定代理开始向上逐级查找第一个黄金代理Level == 2
//
// 查找规则:
// - 自动跳过所有普通代理Level == 1
// - 如果先遇到钻石代理Level == 3不会跳过但会继续查找黄金代理
// 注意:在实际使用中,应该先调用 findDiamondParent如果没找到钻石再调用此方法
// - 只返回第一个找到的黄金代理
// - 如果没有找到黄金代理,返回 ErrNotFound
//
// 示例场景:
// - 普通 -> 普通 -> 黄金:会找到黄金(跳过中间的普通代理)
// - 普通 -> 黄金:会找到黄金
// - 普通 -> 普通 -> 钻石:返回 ErrNotFound跳过钻石继续查找黄金但找不到
func (s *AgentService) findGoldParent(ctx context.Context, agentId int64) (*model.Agent, error) {
currentId := agentId
maxDepth := 100 // 防止无限循环
@@ -446,7 +802,7 @@ func (s *AgentService) ProcessUpgrade(ctx context.Context, agentId, toLevel int6
if parent != nil && rebateAmount > 0 {
// 返佣给原直接上级
if err := s.giveRebateForUpgrade(transCtx, session, parent.Id, agentId, rebateAmount); err != nil {
if err := s.giveRebateForUpgrade(transCtx, session, parent.Id, agentId, rebateAmount, orderNo); err != nil {
return errors.Wrapf(err, "返佣给上级失败")
}
}
@@ -462,10 +818,28 @@ func (s *AgentService) ProcessUpgrade(ctx context.Context, agentId, toLevel int6
}
if needDetach {
// 脱离前先获取原直接上级及其上级的信息(用于后续重新连接)
oldParent, oldParentErr := s.findDirectParent(transCtx, agentId)
var grandparentId int64 = 0
if oldParentErr == nil && oldParent != nil {
// 查找原上级的上级
grandparent, grandparentErr := s.findDirectParent(transCtx, oldParent.Id)
if grandparentErr == nil && grandparent != nil {
grandparentId = grandparent.Id
}
}
// 脱离直接上级关系
if err := s.detachFromParent(transCtx, session, agentId); err != nil {
return errors.Wrapf(err, "脱离直接上级关系失败")
}
// 脱离后,尝试连接到原上级的上级
if grandparentId > 0 {
if err := s.reconnectToGrandparent(transCtx, session, agentId, toLevel, grandparentId); err != nil {
return errors.Wrapf(err, "重新连接上级关系失败")
}
}
}
// 5. 如果升级为钻石,独立成新团队
@@ -515,14 +889,10 @@ func (s *AgentService) needDetachFromParent(ctx context.Context, agent *model.Ag
// 规则2同级不能作为上下级除了普通代理
if newLevel == parent.Level {
if newLevel == 2 || newLevel == 3 { // 黄金或钻石
if newLevel == 2 || newLevel == 3 { // 黄金或钻石同级需要脱离
return true, nil
}
}
// 规则3钻石 → 黄金禁止(特殊规则)
if newLevel == 2 && parent.Level == 3 {
return true, nil
// 普通代理同级newLevel == 1 && parent.Level == 1不需要脱离
}
return false, nil
@@ -555,6 +925,60 @@ func (s *AgentService) detachFromParent(ctx context.Context, session sqlx.Sessio
return nil
}
// reconnectToGrandparent 重新连接到原上级的上级(如果存在且符合条件)
func (s *AgentService) reconnectToGrandparent(ctx context.Context, session sqlx.Session, agentId int64, newLevel int64, grandparentId int64) error {
// 获取原上级的上级信息
grandparent, err := s.AgentModel.FindOne(ctx, grandparentId)
if err != nil {
if errors.Is(err, model.ErrNotFound) {
// 原上级的上级不存在,不需要重新连接
return nil
}
return errors.Wrapf(err, "查询原上级的上级失败")
}
// 验证是否可以连接到原上级的上级
// 规则:新等级必须低于或等于原上级的上级等级,且不能同级(除了普通代理)
if newLevel > grandparent.Level {
// 新等级高于原上级的上级,不能连接
return nil
}
// 同级不能作为上下级(除了普通代理)
if newLevel == grandparent.Level {
if newLevel == 2 || newLevel == 3 {
// 黄金或钻石同级不能连接
return nil
}
// 普通代理同级可以连接(虽然这种情况不太可能发生)
}
// 检查是否已经存在关系
builder := s.AgentRelationModel.SelectBuilder().
Where("parent_id = ? AND child_id = ? AND relation_type = ? AND del_state = ?", grandparent.Id, agentId, 1, globalkey.DelStateNo)
existingRelations, err := s.AgentRelationModel.FindAll(ctx, builder, "")
if err != nil {
return errors.Wrapf(err, "查询现有关系失败")
}
if len(existingRelations) > 0 {
// 关系已存在,不需要重复创建
return nil
}
// 创建新的关系连接到原上级的上级
relation := &model.AgentRelation{
ParentId: grandparent.Id,
ChildId: agentId,
RelationType: 1, // 直接关系
}
if _, err := s.AgentRelationModel.Insert(ctx, session, relation); err != nil {
return errors.Wrapf(err, "创建新关系失败")
}
return nil
}
// updateChildrenTeamLeader 更新所有下级的团队首领
func (s *AgentService) updateChildrenTeamLeader(ctx context.Context, session sqlx.Session, agentId, teamLeaderId int64) error {
// 递归更新所有下级
@@ -604,7 +1028,8 @@ func (s *AgentService) findTeamLeaderId(ctx context.Context, agentId int64) (int
}
// giveRebateForUpgrade 发放升级返佣
func (s *AgentService) giveRebateForUpgrade(ctx context.Context, session sqlx.Session, parentAgentId, upgradeAgentId int64, amount float64) error {
// 注意:升级返佣信息记录在 agent_upgrade 表中rebate_agent_id 和 rebate_amount不需要在 agent_rebate 表中创建记录
func (s *AgentService) giveRebateForUpgrade(ctx context.Context, session sqlx.Session, parentAgentId, upgradeAgentId int64, amount float64, orderNo string) error {
// 更新钱包余额
wallet, err := s.AgentWalletModel.FindOneByAgentId(ctx, parentAgentId)
if err != nil {
@@ -621,21 +1046,25 @@ func (s *AgentService) giveRebateForUpgrade(ctx context.Context, session sqlx.Se
}
// GetUpgradeFee 获取升级费用
func (s *AgentService) GetUpgradeFee(fromLevel, toLevel int64) float64 {
func (s *AgentService) GetUpgradeFee(ctx context.Context, fromLevel, toLevel int64) (float64, error) {
if fromLevel == 1 && toLevel == 2 {
return 199 // 普通→黄金
// 普通→黄金:从配置获取
return s.getRebateConfigFloat(ctx, "upgrade_to_gold_fee", 199)
} else if toLevel == 3 {
return 980 // 升级为钻石
// 升级为钻石:从配置获取
return s.getRebateConfigFloat(ctx, "upgrade_to_diamond_fee", 980)
}
return 0
return 0, nil
}
// GetUpgradeRebate 获取升级返佣金额
func (s *AgentService) GetUpgradeRebate(fromLevel, toLevel int64) float64 {
func (s *AgentService) GetUpgradeRebate(ctx context.Context, fromLevel, toLevel int64) (float64, error) {
if fromLevel == 1 && toLevel == 2 {
return 139 // 普通→黄金返佣
// 普通→黄金返佣:从配置获取
return s.getRebateConfigFloat(ctx, "upgrade_to_gold_rebate", 139)
} else if toLevel == 3 {
return 680 // 升级为钻石返佣
// 升级为钻石返佣:从配置获取
return s.getRebateConfigFloat(ctx, "upgrade_to_diamond_rebate", 680)
}
return 0
return 0, nil
}

View File

@@ -160,7 +160,6 @@ func (s *ApiRegistryService) generateApiName(path string) string {
"order": "订单管理",
"platform_user": "平台用户",
"product": "产品管理",
"promotion": "推广管理",
"query": "查询管理",
"role": "角色管理",
"user": "用户管理",

View File

@@ -3,6 +3,7 @@
package service
import (
"time"
"ycc-server/app/main/api/internal/config"
"ycc-server/app/main/api/internal/types"
"encoding/json"
@@ -58,3 +59,65 @@ func (s *AsynqService) SendQueryTask(orderID int64) error {
logx.Infof("发送异步任务成功任务ID: %s, 队列: %s, 订单号: %d", info.ID, info.Queue, orderID)
return nil
}
// SendAgentProcessTask 发送代理处理任务
func (s *AsynqService) SendAgentProcessTask(orderID int64) error {
// 准备任务的 payload
payload := types.MsgAgentProcessPayload{
OrderID: orderID,
}
payloadBytes, err := json.Marshal(payload)
if err != nil {
logx.Errorf("发送代理处理任务失败 (无法编码 payload): %v, 订单号: %d", err, orderID)
return err
}
options := []asynq.Option{
asynq.MaxRetry(5), // 设置最大重试次数
}
// 创建任务
task := asynq.NewTask(types.MsgAgentProcess, payloadBytes, options...)
// 将任务加入队列并获取任务信息
info, err := s.client.Enqueue(task)
if err != nil {
logx.Errorf("发送代理处理任务失败 (加入队列失败): %+v, 订单号: %d", err, orderID)
return err
}
// 记录成功日志,带上任务 ID 和队列信息
logx.Infof("发送代理处理任务成功任务ID: %s, 队列: %s, 订单号: %d", info.ID, info.Queue, orderID)
return nil
}
// SendUnfreezeTask 发送解冻任务(延迟执行)
func (s *AsynqService) SendUnfreezeTask(freezeTaskId int64, processAt time.Time) error {
// 准备任务的 payload
payload := types.MsgUnfreezeCommissionPayload{
FreezeTaskId: freezeTaskId,
}
payloadBytes, err := json.Marshal(payload)
if err != nil {
logx.Errorf("发送解冻任务失败 (无法编码 payload): %v, 冻结任务ID: %d", err, freezeTaskId)
return err
}
options := []asynq.Option{
asynq.MaxRetry(5), // 设置最大重试次数
asynq.ProcessAt(processAt), // 延迟到指定时间执行
asynq.Queue("critical"), // 使用关键队列
}
// 创建任务
task := asynq.NewTask(types.MsgUnfreezeCommission, payloadBytes, options...)
// 将任务加入队列并获取任务信息
info, err := s.client.Enqueue(task)
if err != nil {
logx.Errorf("发送解冻任务失败 (加入队列失败): %+v, 冻结任务ID: %d", err, freezeTaskId)
return err
}
// 记录成功日志,带上任务 ID 和队列信息
logx.Infof("发送解冻任务成功任务ID: %s, 队列: %s, 冻结任务ID: %d, 执行时间: %v", info.ID, info.Queue, freezeTaskId, processAt)
return nil
}

View File

@@ -28,8 +28,8 @@ func NewAuthorizationService(c config.Config, authDocModel model.AuthorizationDo
return &AuthorizationService{
config: c,
authDocModel: authDocModel,
fileStoragePath: "data/authorization_docs", // 使用相对路径,兼容开发环境
fileBaseURL: c.Authorization.FileBaseURL, // 从配置文件读取
fileStoragePath: "data/authorization_docs", // 使用相对路径,兼容开发环境
fileBaseURL: c.Authorization.FileBaseURL, // 从配置文件读取
}
}
@@ -113,7 +113,7 @@ func (s *AuthorizationService) generatePDFContent(userInfo map[string]interface{
"/app/static/SIMHEI.TTF", // Docker容器内的字体文件
"app/main/api/static/SIMHEI.TTF", // 开发环境备用路径
}
// 尝试添加字体
fontAdded := false
for _, fontPath := range fontPaths {
@@ -150,19 +150,19 @@ func (s *AuthorizationService) generatePDFContent(userInfo map[string]interface{
pdf.SetFont("Arial", "", 20)
}
pdf.CellFormat(0, 15, "授权书", "", 1, "C", false, 0, "")
// 添加空行
pdf.Ln(5)
// 设置正文样式 - 正常字体
if fontAdded {
pdf.SetFont("ChineseFont", "", 12)
} else {
pdf.SetFont("Arial", "", 12)
}
// 构建授权书内容(去掉标题部分)
content := fmt.Sprintf(`海南省学宇思网络科技有限公司:
content := fmt.Sprintf(`海南海宇大数据有限公司:
本人%s拟向贵司申请大数据分析报告查询业务贵司需要了解本人相关状况用于查询大数据分析报告因此本人同意向贵司提供本人的姓名和手机号等个人信息并同意贵司向第三方包括但不限于西部数据交易有限公司传送上述信息。第三方将使用上述信息核实信息真实情况查询信用记录并生成报告。
授权内容如下:
@@ -185,8 +185,8 @@ func (s *AuthorizationService) generatePDFContent(userInfo map[string]interface{
附加说明:
本人在授权的相关数据将依据法律法规及贵司内部数据管理规范妥善存储,存储期限为法律要求的最短必要时间。超过存储期限或在数据使用目的达成后,贵司将对相关数据进行销毁或匿名化处理。
本人有权随时撤回本授权书中的授权,但撤回前的授权行为及其法律后果仍具有法律效力。若需撤回授权,本人可通过贵司官方渠道提交书面申请,贵司将在收到申请后依法停止对本人数据的使用。
你通过"一查查",自愿支付相应费用,用于购买海南省学宇思网络科技有限公司的大数据报告产品。如若对产品内容存在异议可通过邮箱admin@iieeii.com或APP"联系客服"按钮进行反馈贵司将在收到异议之日起20日内进行核查和处理并将结果答复。
你向海南省学宇思网络科技有限公司的支付方式为:海南省学宇思网络科技有限公司及其经官方授权的相关企业的支付宝账户。
你通过"一查查",自愿支付相应费用,用于购买海南海宇大数据有限公司的大数据报告产品。如若对产品内容存在异议可通过邮箱admin@iieeii.com或APP"联系客服"按钮进行反馈贵司将在收到异议之日起20日内进行核查和处理并将结果答复。
你向海南海宇大数据有限公司的支付方式为:海南海宇大数据有限公司及其经官方授权的相关企业的支付宝账户。
争议解决机制:
若因本授权书引发争议,双方应友好协商解决;协商不成的,双方同意将争议提交至授权书签署地(海南省)有管辖权的人民法院解决。

Some files were not shown because too many files have changed in this diff Show More