v1.0
This commit is contained in:
@@ -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 // 正式站点域名(短链重定向的目标域名)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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{
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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. 验证用户名和密码
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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] {
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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. 返回结果
|
||||
|
||||
@@ -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. 返回结果
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
})
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
72
app/main/api/internal/logic/agent/deleteinvitecodelogic.go
Normal file
72
app/main/api/internal/logic/agent/deleteinvitecodelogic.go
Normal 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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
492
app/main/api/internal/logic/agent/getconversionratelogic.go
Normal file
492
app/main/api/internal/logic/agent/getconversionratelogic.go
Normal 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或下级ID,periodLabel=%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)), // 用户数量(新增)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
188
app/main/api/internal/logic/agent/getlevelprivilegelogic.go
Normal file
188
app/main/api/internal/logic/agent/getlevelprivilegelogic.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
341
app/main/api/internal/logic/agent/getteamlistlogic.go
Normal file
341
app/main/api/internal/logic/agent/getteamlistlogic.go
Normal 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,
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
119
app/main/api/internal/logic/agent/getupgraderebatelistlogic.go
Normal file
119
app/main/api/internal/logic/agent/getupgraderebatelistlogic.go
Normal 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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
58
app/main/api/internal/logic/agent/promotionredirectlogic.go
Normal file
58
app/main/api/internal/logic/agent/promotionredirectlogic.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
})
|
||||
|
||||
114
app/main/api/internal/logic/agent/shortlinkredirectlogic.go
Normal file
114
app/main/api/internal/logic/agent/shortlinkredirectlogic.go
Normal 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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 获取配置值(浮点数)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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})
|
||||
|
||||
63
app/main/api/internal/queue/agentProcess.go
Normal file
63
app/main/api/internal/queue/agentProcess.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
94
app/main/api/internal/queue/unfreezeCommission.go
Normal file
94
app/main/api/internal/queue/unfreezeCommission.go
Normal 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
|
||||
}
|
||||
233
app/main/api/internal/queue/unfreezeCommissionScan.go
Normal file
233
app/main/api/internal/queue/unfreezeCommissionScan.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
@@ -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.1(10%)
|
||||
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
|
||||
}
|
||||
|
||||
@@ -160,7 +160,6 @@ func (s *ApiRegistryService) generateApiName(path string) string {
|
||||
"order": "订单管理",
|
||||
"platform_user": "平台用户",
|
||||
"product": "产品管理",
|
||||
"promotion": "推广管理",
|
||||
"query": "查询管理",
|
||||
"role": "角色管理",
|
||||
"user": "用户管理",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user