201
This commit is contained in:
parent
16c261bd36
commit
af5231747a
@ -43,7 +43,7 @@ type (
|
||||
GiftAmount int64 `json:"gift_amount"`
|
||||
Telegram int64 `json:"telegram"`
|
||||
ReferCode string `json:"refer_code"`
|
||||
RefererId *int64 `json:"referer_id"`
|
||||
RefererId *int64 `json:"referer_id"`
|
||||
Enable bool `json:"enable"`
|
||||
IsAdmin bool `json:"is_admin"`
|
||||
MemberStatus string `json:"member_status"`
|
||||
@ -295,3 +295,4 @@ service ppanel {
|
||||
@handler GetUserLoginLogs
|
||||
get /login/logs (GetUserLoginLogsRequest) returns (GetUserLoginLogsResponse)
|
||||
}
|
||||
|
||||
|
||||
@ -118,6 +118,51 @@ type (
|
||||
DeviceStatus bool `json:"device_status"`
|
||||
EmailStatus bool `json:"email_status"`
|
||||
}
|
||||
// GetAgentRealtimeRequest - 获取代理链接实时数据
|
||||
GetAgentRealtimeRequest {}
|
||||
// GetAgentRealtimeResponse - 代理链接实时数据响应
|
||||
GetAgentRealtimeResponse {
|
||||
Total int64 `json:"total"` // 访问总人数
|
||||
Clicks int64 `json:"clicks"` // 点击量
|
||||
Views int64 `json:"views"` // 浏览量
|
||||
PaidCount int64 `json:"paid_count"` // 付费数量
|
||||
}
|
||||
// GetUserInviteStatsRequest - 获取用户邀请统计
|
||||
GetUserInviteStatsRequest {}
|
||||
// GetUserInviteStatsResponse - 用户邀请统计响应
|
||||
GetUserInviteStatsResponse {
|
||||
FriendlyCount int64 `json:"friendly_count"` // 有效邀请数(有订单的用户)
|
||||
HistoryCount int64 `json:"history_count"` // 历史邀请总数
|
||||
}
|
||||
// GetInviteSalesRequest - 获取最近销售数据
|
||||
GetInviteSalesRequest {
|
||||
Page int `form:"page" validate:"required"`
|
||||
Size int `form:"size" validate:"required"`
|
||||
}
|
||||
// GetInviteSalesResponse - 最近销售数据响应
|
||||
GetInviteSalesResponse {
|
||||
Total int64 `json:"total"` // 销售记录总数
|
||||
List []InvitedUserSale `json:"list"` // 销售数据列表(分页)
|
||||
}
|
||||
// InvitedUserSale - 被邀请用户的销售记录
|
||||
InvitedUserSale {
|
||||
Amount int64 `json:"amount"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
UserEmail string `json:"user_email"`
|
||||
UserId int64 `json:"user_id"`
|
||||
}
|
||||
// GetAgentDownloadsRequest - 获取各端下载量
|
||||
GetAgentDownloadsRequest {}
|
||||
// GetAgentDownloadsResponse - 各端下载量响应
|
||||
GetAgentDownloadsResponse {
|
||||
List []AgentDownloadStats `json:"list"`
|
||||
}
|
||||
// AgentDownloadStats - 各端下载量统计
|
||||
AgentDownloadStats {
|
||||
Platform string `json:"platform"`
|
||||
Clicks int64 `json:"clicks"`
|
||||
Visits int64 `json:"visits"`
|
||||
}
|
||||
)
|
||||
|
||||
@server (
|
||||
@ -225,5 +270,21 @@ service ppanel {
|
||||
@doc "Unbind Device"
|
||||
@handler UnbindDevice
|
||||
put /unbind_device (UnbindDeviceRequest)
|
||||
|
||||
@doc "Get agent realtime data"
|
||||
@handler GetAgentRealtime
|
||||
get /agent/realtime (GetAgentRealtimeRequest) returns (GetAgentRealtimeResponse)
|
||||
|
||||
@doc "Get user invite statistics"
|
||||
@handler GetUserInviteStats
|
||||
get /invite/stats (GetUserInviteStatsRequest) returns (GetUserInviteStatsResponse)
|
||||
|
||||
@doc "Get invite sales data"
|
||||
@handler GetInviteSales
|
||||
get /invite/sales (GetInviteSalesRequest) returns (GetInviteSalesResponse)
|
||||
|
||||
@doc "Get agent downloads data"
|
||||
@handler GetAgentDownloads
|
||||
get /agent/downloads (GetAgentDownloadsRequest) returns (GetAgentDownloadsResponse)
|
||||
}
|
||||
|
||||
|
||||
@ -9,29 +9,30 @@ import (
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Model string `yaml:"Model" default:"prod"`
|
||||
Host string `yaml:"Host" default:"0.0.0.0"`
|
||||
Port int `yaml:"Port" default:"8080"`
|
||||
Debug bool `yaml:"Debug" default:"false"`
|
||||
TLS TLS `yaml:"TLS"`
|
||||
JwtAuth JwtAuth `yaml:"JwtAuth"`
|
||||
Logger logger.LogConf `yaml:"Logger"`
|
||||
MySQL orm.Config `yaml:"MySQL"`
|
||||
Redis RedisConfig `yaml:"Redis"`
|
||||
Site SiteConfig `yaml:"Site"`
|
||||
Node NodeConfig `yaml:"Node"`
|
||||
Mobile MobileConfig `yaml:"Mobile"`
|
||||
Email EmailConfig `yaml:"Email"`
|
||||
Device DeviceConfig `yaml:"device"`
|
||||
Verify Verify `yaml:"Verify"`
|
||||
VerifyCode VerifyCode `yaml:"VerifyCode"`
|
||||
Register RegisterConfig `yaml:"Register"`
|
||||
Subscribe SubscribeConfig `yaml:"Subscribe"`
|
||||
Invite InviteConfig `yaml:"Invite"`
|
||||
Kutt KuttConfig `yaml:"Kutt"`
|
||||
Telegram Telegram `yaml:"Telegram"`
|
||||
Log Log `yaml:"Log"`
|
||||
Trace trace.Config `yaml:"Trace"`
|
||||
Model string `yaml:"Model" default:"prod"`
|
||||
Host string `yaml:"Host" default:"0.0.0.0"`
|
||||
Port int `yaml:"Port" default:"8080"`
|
||||
Debug bool `yaml:"Debug" default:"false"`
|
||||
TLS TLS `yaml:"TLS"`
|
||||
JwtAuth JwtAuth `yaml:"JwtAuth"`
|
||||
Logger logger.LogConf `yaml:"Logger"`
|
||||
MySQL orm.Config `yaml:"MySQL"`
|
||||
Redis RedisConfig `yaml:"Redis"`
|
||||
Site SiteConfig `yaml:"Site"`
|
||||
Node NodeConfig `yaml:"Node"`
|
||||
Mobile MobileConfig `yaml:"Mobile"`
|
||||
Email EmailConfig `yaml:"Email"`
|
||||
Device DeviceConfig `yaml:"device"`
|
||||
Verify Verify `yaml:"Verify"`
|
||||
VerifyCode VerifyCode `yaml:"VerifyCode"`
|
||||
Register RegisterConfig `yaml:"Register"`
|
||||
Subscribe SubscribeConfig `yaml:"Subscribe"`
|
||||
Invite InviteConfig `yaml:"Invite"`
|
||||
Kutt KuttConfig `yaml:"Kutt"`
|
||||
OpenInstall OpenInstallConfig `yaml:"OpenInstall"`
|
||||
Telegram Telegram `yaml:"Telegram"`
|
||||
Log Log `yaml:"Log"`
|
||||
Trace trace.Config `yaml:"Trace"`
|
||||
Administrator struct {
|
||||
Email string `yaml:"Email" default:"admin@ppanel.dev"`
|
||||
Password string `yaml:"Password" default:"password"`
|
||||
@ -219,6 +220,12 @@ type KuttConfig struct {
|
||||
Domain string `yaml:"Domain" default:""` // 短链接域名 (例如: getsapp.net)
|
||||
}
|
||||
|
||||
// OpenInstallConfig OpenInstall 配置
|
||||
type OpenInstallConfig struct {
|
||||
Enable bool `yaml:"Enable" default:"false"` // 是否启用 OpenInstall
|
||||
AppKey string `yaml:"AppKey" default:""` // OpenInstall AppKey
|
||||
}
|
||||
|
||||
type Telegram struct {
|
||||
Enable bool `yaml:"Enable" default:"false"`
|
||||
BotID int64 `yaml:"BotID" default:""`
|
||||
|
||||
26
internal/handler/public/user/getAgentDownloadsHandler.go
Normal file
26
internal/handler/public/user/getAgentDownloadsHandler.go
Normal file
@ -0,0 +1,26 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/perfect-panel/server/internal/logic/public/user"
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
"github.com/perfect-panel/server/internal/types"
|
||||
"github.com/perfect-panel/server/pkg/result"
|
||||
)
|
||||
|
||||
// Get agent downloads data
|
||||
func GetAgentDownloadsHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
|
||||
return func(c *gin.Context) {
|
||||
var req types.GetAgentDownloadsRequest
|
||||
_ = c.ShouldBind(&req)
|
||||
validateErr := svcCtx.Validate(&req)
|
||||
if validateErr != nil {
|
||||
result.ParamErrorResult(c, validateErr)
|
||||
return
|
||||
}
|
||||
|
||||
l := user.NewGetAgentDownloadsLogic(c.Request.Context(), svcCtx)
|
||||
resp, err := l.GetAgentDownloads(&req)
|
||||
result.HttpResult(c, resp, err)
|
||||
}
|
||||
}
|
||||
26
internal/handler/public/user/getAgentRealtimeHandler.go
Normal file
26
internal/handler/public/user/getAgentRealtimeHandler.go
Normal file
@ -0,0 +1,26 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/perfect-panel/server/internal/logic/public/user"
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
"github.com/perfect-panel/server/internal/types"
|
||||
"github.com/perfect-panel/server/pkg/result"
|
||||
)
|
||||
|
||||
// Get agent realtime data
|
||||
func GetAgentRealtimeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
|
||||
return func(c *gin.Context) {
|
||||
var req types.GetAgentRealtimeRequest
|
||||
_ = c.ShouldBind(&req)
|
||||
validateErr := svcCtx.Validate(&req)
|
||||
if validateErr != nil {
|
||||
result.ParamErrorResult(c, validateErr)
|
||||
return
|
||||
}
|
||||
|
||||
l := user.NewGetAgentRealtimeLogic(c.Request.Context(), svcCtx)
|
||||
resp, err := l.GetAgentRealtime(&req)
|
||||
result.HttpResult(c, resp, err)
|
||||
}
|
||||
}
|
||||
30
internal/handler/public/user/getInviteSalesHandler.go
Normal file
30
internal/handler/public/user/getInviteSalesHandler.go
Normal file
@ -0,0 +1,30 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/perfect-panel/server/internal/logic/public/user"
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
"github.com/perfect-panel/server/internal/types"
|
||||
"github.com/perfect-panel/server/pkg/result"
|
||||
)
|
||||
|
||||
// Get invite sales data
|
||||
func GetInviteSalesHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
|
||||
return func(c *gin.Context) {
|
||||
var req types.GetInviteSalesRequest
|
||||
if err := c.ShouldBind(&req); err != nil {
|
||||
result.ParamErrorResult(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
validateErr := svcCtx.Validate(&req)
|
||||
if validateErr != nil {
|
||||
result.ParamErrorResult(c, validateErr)
|
||||
return
|
||||
}
|
||||
|
||||
l := user.NewGetInviteSalesLogic(c.Request.Context(), svcCtx)
|
||||
resp, err := l.GetInviteSales(&req)
|
||||
result.HttpResult(c, resp, err)
|
||||
}
|
||||
}
|
||||
26
internal/handler/public/user/getUserInviteStatsHandler.go
Normal file
26
internal/handler/public/user/getUserInviteStatsHandler.go
Normal file
@ -0,0 +1,26 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/perfect-panel/server/internal/logic/public/user"
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
"github.com/perfect-panel/server/internal/types"
|
||||
"github.com/perfect-panel/server/pkg/result"
|
||||
)
|
||||
|
||||
// Get user invite statistics
|
||||
func GetUserInviteStatsHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
|
||||
return func(c *gin.Context) {
|
||||
var req types.GetUserInviteStatsRequest
|
||||
_ = c.ShouldBind(&req)
|
||||
validateErr := svcCtx.Validate(&req)
|
||||
if validateErr != nil {
|
||||
result.ParamErrorResult(c, validateErr)
|
||||
return
|
||||
}
|
||||
|
||||
l := user.NewGetUserInviteStatsLogic(c.Request.Context(), svcCtx)
|
||||
resp, err := l.GetUserInviteStats(&req)
|
||||
result.HttpResult(c, resp, err)
|
||||
}
|
||||
}
|
||||
@ -895,6 +895,18 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
|
||||
|
||||
// Verify Email
|
||||
publicUserGroupRouter.POST("/verify_email", publicUser.VerifyEmailHandler(serverCtx))
|
||||
|
||||
// Get agent realtime data
|
||||
publicUserGroupRouter.GET("/agent/realtime", publicUser.GetAgentRealtimeHandler(serverCtx))
|
||||
|
||||
// Get agent downloads data
|
||||
publicUserGroupRouter.GET("/agent/downloads", publicUser.GetAgentDownloadsHandler(serverCtx))
|
||||
|
||||
// Get user invite statistics
|
||||
publicUserGroupRouter.GET("/invite/stats", publicUser.GetUserInviteStatsHandler(serverCtx))
|
||||
|
||||
// Get invite sales data
|
||||
publicUserGroupRouter.GET("/invite/sales", publicUser.GetInviteSalesHandler(serverCtx))
|
||||
}
|
||||
|
||||
serverGroupRouter := router.Group("/v1/server")
|
||||
|
||||
76
internal/logic/public/user/getAgentDownloadsLogic.go
Normal file
76
internal/logic/public/user/getAgentDownloadsLogic.go
Normal file
@ -0,0 +1,76 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"time"
|
||||
|
||||
"github.com/perfect-panel/server/internal/model/user"
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
"github.com/perfect-panel/server/internal/types"
|
||||
"github.com/perfect-panel/server/pkg/constant"
|
||||
"github.com/perfect-panel/server/pkg/logger"
|
||||
"github.com/perfect-panel/server/pkg/openinstall"
|
||||
"github.com/perfect-panel/server/pkg/xerr"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type GetAgentDownloadsLogic struct {
|
||||
logger.Logger
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
func NewGetAgentDownloadsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetAgentDownloadsLogic {
|
||||
return &GetAgentDownloadsLogic{
|
||||
Logger: logger.WithContext(ctx),
|
||||
ctx: ctx,
|
||||
svcCtx: svcCtx,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *GetAgentDownloadsLogic) GetAgentDownloads(req *types.GetAgentDownloadsRequest) (resp *types.GetAgentDownloadsResponse, err error) {
|
||||
// 1. Get current user
|
||||
_, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User)
|
||||
if !ok {
|
||||
l.Errorw("[GetAgentDownloads] user not found in context")
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access")
|
||||
}
|
||||
|
||||
// 2. Check configuration
|
||||
cfg := l.svcCtx.Config.OpenInstall
|
||||
if !cfg.Enable {
|
||||
return &types.GetAgentDownloadsResponse{
|
||||
List: []types.AgentDownloadStats{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 3. Call OpenInstall API
|
||||
// Default to last 30 days
|
||||
endDate := time.Now()
|
||||
startDate := endDate.AddDate(0, 0, -30)
|
||||
|
||||
client := openinstall.NewClient(cfg.AppKey)
|
||||
stats, err := client.GetPlatformStats(l.ctx, startDate, endDate)
|
||||
if err != nil {
|
||||
l.Errorw("Failed to fetch OpenInstall stats", logger.Field("error", err))
|
||||
// Return empty list on error
|
||||
return &types.GetAgentDownloadsResponse{
|
||||
List: []types.AgentDownloadStats{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 4. Map response
|
||||
var list []types.AgentDownloadStats
|
||||
for _, s := range stats {
|
||||
list = append(list, types.AgentDownloadStats{
|
||||
Platform: s.Platform,
|
||||
Clicks: s.Clicks,
|
||||
Visits: s.Visits,
|
||||
})
|
||||
}
|
||||
|
||||
return &types.GetAgentDownloadsResponse{
|
||||
List: list,
|
||||
}, nil
|
||||
}
|
||||
150
internal/logic/public/user/getAgentRealtimeLogic.go
Normal file
150
internal/logic/public/user/getAgentRealtimeLogic.go
Normal file
@ -0,0 +1,150 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/perfect-panel/server/internal/model/user"
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
"github.com/perfect-panel/server/internal/types"
|
||||
"github.com/perfect-panel/server/pkg/constant"
|
||||
"github.com/perfect-panel/server/pkg/kutt"
|
||||
"github.com/perfect-panel/server/pkg/logger"
|
||||
"github.com/perfect-panel/server/pkg/xerr"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type GetAgentRealtimeLogic struct {
|
||||
logger.Logger
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
func NewGetAgentRealtimeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetAgentRealtimeLogic {
|
||||
return &GetAgentRealtimeLogic{
|
||||
Logger: logger.WithContext(ctx),
|
||||
ctx: ctx,
|
||||
svcCtx: svcCtx,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *GetAgentRealtimeLogic) GetAgentRealtime(req *types.GetAgentRealtimeRequest) (resp *types.GetAgentRealtimeResponse, err error) {
|
||||
// 1. Get current user
|
||||
u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User)
|
||||
if !ok {
|
||||
l.Errorw("[GetAgentRealtime] user not found in context")
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access")
|
||||
}
|
||||
|
||||
// 2. Check if Kutt is enabled
|
||||
cfg := l.svcCtx.Config.Kutt
|
||||
if !cfg.Enable || cfg.ApiKey == "" || cfg.ApiURL == "" {
|
||||
l.Infow("Kutt service not enabled or configured",
|
||||
logger.Field("enable", cfg.Enable),
|
||||
logger.Field("has_key", cfg.ApiKey != ""),
|
||||
logger.Field("has_url", cfg.ApiURL != ""))
|
||||
return &types.GetAgentRealtimeResponse{
|
||||
Total: 0,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 3. Get share URL and domain
|
||||
shareUrl := l.getShareUrl()
|
||||
domain := l.getDomain()
|
||||
|
||||
if shareUrl == "" {
|
||||
l.Errorw("ShareUrl not configured for Kutt integration")
|
||||
return &types.GetAgentRealtimeResponse{
|
||||
Total: 0,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 4. Construct target URL
|
||||
// Format: https://gethifast.net/?ic=INVITECODE
|
||||
inviteCode := u.ReferCode
|
||||
if inviteCode == "" {
|
||||
l.Errorw("User has no refer code", logger.Field("user_id", u.Id))
|
||||
return &types.GetAgentRealtimeResponse{
|
||||
Total: 0,
|
||||
}, nil
|
||||
}
|
||||
|
||||
target := fmt.Sprintf("%s?ic=%s", shareUrl, inviteCode)
|
||||
|
||||
// 5. Call Kutt API to get link stats
|
||||
// We use Reuse=true to get the existing link details including stats
|
||||
client := kutt.NewClient(cfg.ApiURL, cfg.ApiKey)
|
||||
kuttReq := &kutt.CreateLinkRequest{
|
||||
Target: target,
|
||||
Description: fmt.Sprintf("Invite link for code: %s", inviteCode),
|
||||
Reuse: true,
|
||||
Domain: domain,
|
||||
}
|
||||
|
||||
link, err := client.CreateShortLink(l.ctx, kuttReq)
|
||||
if err != nil {
|
||||
l.Errorw("Failed to fetch Kutt stats",
|
||||
logger.Field("error", err.Error()),
|
||||
logger.Field("user_id", u.Id),
|
||||
logger.Field("target", target))
|
||||
// Return 0 on error, don't block the UI
|
||||
return &types.GetAgentRealtimeResponse{
|
||||
Total: 0,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 6. Get paid user count
|
||||
var paidCount int64
|
||||
db := l.svcCtx.DB
|
||||
// Count users invited by me who have paid orders
|
||||
// Sub-query to get user IDs invited by current user
|
||||
// Count distinct users who have paid orders (Status 2=Paid, 5=Finished)
|
||||
err = db.Table("order").
|
||||
Joins("LEFT JOIN user ON user.id = order.user_id").
|
||||
Where("user.referer_id = ? AND order.status IN ?", u.Id, []int{2, 5}).
|
||||
Distinct("order.user_id").
|
||||
Count(&paidCount).Error
|
||||
|
||||
if err != nil {
|
||||
l.Errorw("Failed to count paid users",
|
||||
logger.Field("error", err.Error()),
|
||||
logger.Field("user_id", u.Id))
|
||||
// Don't fail the whole request, just return 0 for paid count
|
||||
paidCount = 0
|
||||
}
|
||||
|
||||
return &types.GetAgentRealtimeResponse{
|
||||
Total: int64(link.VisitCount),
|
||||
Clicks: int64(link.VisitCount),
|
||||
Views: int64(link.VisitCount),
|
||||
PaidCount: paidCount,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (l *GetAgentRealtimeLogic) getShareUrl() string {
|
||||
siteConfig := l.svcCtx.Config.Site
|
||||
if siteConfig.CustomData != "" {
|
||||
var data customData
|
||||
if err := json.Unmarshal([]byte(siteConfig.CustomData), &data); err == nil {
|
||||
if data.ShareUrl != "" {
|
||||
return data.ShareUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
return l.svcCtx.Config.Kutt.TargetURL
|
||||
}
|
||||
|
||||
func (l *GetAgentRealtimeLogic) getDomain() string {
|
||||
siteConfig := l.svcCtx.Config.Site
|
||||
if siteConfig.CustomData != "" {
|
||||
var data customData
|
||||
if err := json.Unmarshal([]byte(siteConfig.CustomData), &data); err == nil {
|
||||
if data.Domain != "" {
|
||||
return data.Domain
|
||||
}
|
||||
}
|
||||
}
|
||||
return l.svcCtx.Config.Kutt.Domain
|
||||
}
|
||||
125
internal/logic/public/user/getInviteSalesLogic.go
Normal file
125
internal/logic/public/user/getInviteSalesLogic.go
Normal file
@ -0,0 +1,125 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/perfect-panel/server/internal/model/user"
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
"github.com/perfect-panel/server/internal/types"
|
||||
"github.com/perfect-panel/server/pkg/constant"
|
||||
"github.com/perfect-panel/server/pkg/logger"
|
||||
"github.com/perfect-panel/server/pkg/xerr"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type GetInviteSalesLogic struct {
|
||||
logger.Logger
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
func NewGetInviteSalesLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetInviteSalesLogic {
|
||||
return &GetInviteSalesLogic{
|
||||
Logger: logger.WithContext(ctx),
|
||||
ctx: ctx,
|
||||
svcCtx: svcCtx,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *GetInviteSalesLogic) GetInviteSales(req *types.GetInviteSalesRequest) (resp *types.GetInviteSalesResponse, err error) {
|
||||
// 1. Get current user
|
||||
u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User)
|
||||
if !ok {
|
||||
l.Errorw("[GetInviteSales] user not found in context")
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access")
|
||||
}
|
||||
userId := u.Id
|
||||
|
||||
// 2. Count total sales
|
||||
var totalSales int64
|
||||
err = l.svcCtx.DB.WithContext(l.ctx).
|
||||
Table("`order` o").
|
||||
Joins("JOIN user u ON o.user_id = u.id").
|
||||
Where("u.referer_id = ? AND o.status IN (?, ?)", userId, 2, 5).
|
||||
Count(&totalSales).Error
|
||||
if err != nil {
|
||||
l.Errorw("[GetInviteSales] count sales failed",
|
||||
logger.Field("error", err.Error()),
|
||||
logger.Field("user_id", userId))
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError),
|
||||
"count sales failed: %v", err.Error())
|
||||
}
|
||||
|
||||
// 3. Pagination
|
||||
if req.Page < 1 {
|
||||
req.Page = 1
|
||||
}
|
||||
if req.Size < 1 {
|
||||
req.Size = 10
|
||||
}
|
||||
if req.Size > 100 {
|
||||
req.Size = 100
|
||||
}
|
||||
offset := (req.Page - 1) * req.Size
|
||||
|
||||
// 4. Get sales data
|
||||
type OrderWithUser struct {
|
||||
Amount int64 `gorm:"column:amount"`
|
||||
CreatedAt int64 `gorm:"column:created_at"`
|
||||
UserId int64 `gorm:"column:user_id"`
|
||||
}
|
||||
|
||||
var orderData []OrderWithUser
|
||||
err = l.svcCtx.DB.WithContext(l.ctx).
|
||||
Table("`order` o").
|
||||
Select("o.amount, CAST(UNIX_TIMESTAMP(o.created_at) * 1000 AS SIGNED) as created_at, u.id as user_id").
|
||||
Joins("JOIN user u ON o.user_id = u.id").
|
||||
Where("u.referer_id = ? AND o.status IN (?, ?)", userId, 2, 5). // status 2: Paid, 5: Finished
|
||||
Order("o.created_at DESC").
|
||||
Limit(req.Size).
|
||||
Offset(offset).
|
||||
Scan(&orderData).Error
|
||||
if err != nil {
|
||||
l.Errorw("[GetInviteSales] query sales failed",
|
||||
logger.Field("error", err.Error()),
|
||||
logger.Field("user_id", userId))
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError),
|
||||
"query sales failed: %v", err.Error())
|
||||
}
|
||||
|
||||
// 5. Get user emails
|
||||
var list []types.InvitedUserSale
|
||||
for _, order := range orderData {
|
||||
var email string
|
||||
// Try email auth first
|
||||
err = l.svcCtx.DB.WithContext(l.ctx).
|
||||
Table("user_auth_methods").
|
||||
Select("auth_identifier").
|
||||
Where("user_id = ? AND auth_type = ?", order.UserId, "email").
|
||||
Limit(1).
|
||||
Scan(&email).Error
|
||||
|
||||
// Fallback to any auth method
|
||||
if err != nil || email == "" {
|
||||
err = l.svcCtx.DB.WithContext(l.ctx).
|
||||
Table("user_auth_methods").
|
||||
Select("auth_identifier").
|
||||
Where("user_id = ?", order.UserId).
|
||||
Order("created_at ASC").
|
||||
Limit(1).
|
||||
Scan(&email).Error
|
||||
}
|
||||
|
||||
list = append(list, types.InvitedUserSale{
|
||||
Amount: order.Amount,
|
||||
CreatedAt: order.CreatedAt,
|
||||
UserEmail: email,
|
||||
UserId: order.UserId,
|
||||
})
|
||||
}
|
||||
|
||||
return &types.GetInviteSalesResponse{
|
||||
Total: totalSales,
|
||||
List: list,
|
||||
}, nil
|
||||
}
|
||||
72
internal/logic/public/user/getUserInviteStatsLogic.go
Normal file
72
internal/logic/public/user/getUserInviteStatsLogic.go
Normal file
@ -0,0 +1,72 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/perfect-panel/server/internal/model/user"
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
"github.com/perfect-panel/server/internal/types"
|
||||
"github.com/perfect-panel/server/pkg/constant"
|
||||
"github.com/perfect-panel/server/pkg/logger"
|
||||
"github.com/perfect-panel/server/pkg/xerr"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type GetUserInviteStatsLogic struct {
|
||||
logger.Logger
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
// Get user invite statistics
|
||||
func NewGetUserInviteStatsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetUserInviteStatsLogic {
|
||||
return &GetUserInviteStatsLogic{
|
||||
Logger: logger.WithContext(ctx),
|
||||
ctx: ctx,
|
||||
svcCtx: svcCtx,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *GetUserInviteStatsLogic) GetUserInviteStats(req *types.GetUserInviteStatsRequest) (resp *types.GetUserInviteStatsResponse, err error) {
|
||||
// 1. 从 context 中获取当前登录用户
|
||||
u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User)
|
||||
if !ok {
|
||||
l.Errorw("[GetUserInviteStats] user not found in context")
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access")
|
||||
}
|
||||
userId := u.Id
|
||||
|
||||
// 2. 获取有效邀请数 (FriendlyCount): 被邀请用户中至少有1个已支付订单 (status=2或5)
|
||||
var friendlyCount int64
|
||||
err = l.svcCtx.DB.WithContext(l.ctx).
|
||||
Table("user").
|
||||
Where("referer_id = ? AND EXISTS (SELECT 1 FROM `order` o WHERE o.user_id = user.id AND o.status IN (?, ?))", userId, 2, 5).
|
||||
Count(&friendlyCount).Error
|
||||
|
||||
if err != nil {
|
||||
l.Errorw("[GetUserInviteStats] count friendly users failed",
|
||||
logger.Field("error", err.Error()),
|
||||
logger.Field("user_id", userId))
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError),
|
||||
"count friendly users failed: %v", err.Error())
|
||||
}
|
||||
|
||||
// 3. 获取历史邀请总数 (HistoryCount)
|
||||
var historyCount int64
|
||||
err = l.svcCtx.DB.WithContext(l.ctx).
|
||||
Table("user").
|
||||
Where("referer_id = ?", userId).
|
||||
Count(&historyCount).Error
|
||||
if err != nil {
|
||||
l.Errorw("[GetUserInviteStats] count history users failed",
|
||||
logger.Field("error", err.Error()),
|
||||
logger.Field("user_id", userId))
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError),
|
||||
"count history users failed: %v", err.Error())
|
||||
}
|
||||
|
||||
return &types.GetUserInviteStatsResponse{
|
||||
FriendlyCount: friendlyCount,
|
||||
HistoryCount: historyCount,
|
||||
}, nil
|
||||
}
|
||||
@ -74,10 +74,12 @@ type ServiceContext struct {
|
||||
|
||||
func NewServiceContext(c config.Config) *ServiceContext {
|
||||
// gorm initialize
|
||||
fmt.Printf(" [Debug] MySQL Config -> Addr: %s, User: %s, DB: %s\n", c.MySQL.Addr, c.MySQL.Username, c.MySQL.Dbname)
|
||||
db, err := orm.ConnectMysql(orm.Mysql{
|
||||
Config: c.MySQL,
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Printf(" [Debug] Connection Error: %v\n", err)
|
||||
panic(err.Error())
|
||||
}
|
||||
rds := redis.NewClient(&redis.Options{
|
||||
|
||||
@ -906,6 +906,51 @@ type GetGlobalConfigResponse struct {
|
||||
WebAd bool `json:"web_ad"`
|
||||
}
|
||||
|
||||
type GetAgentRealtimeRequest struct{}
|
||||
|
||||
type GetAgentRealtimeResponse struct {
|
||||
Total int64 `json:"total"` // 访问总人数
|
||||
Clicks int64 `json:"clicks"` // 点击量
|
||||
Views int64 `json:"views"` // 浏览量
|
||||
PaidCount int64 `json:"paid_count"` // 付费数量
|
||||
}
|
||||
|
||||
type GetAgentDownloadsRequest struct{}
|
||||
|
||||
type GetAgentDownloadsResponse struct {
|
||||
List []AgentDownloadStats `json:"list"`
|
||||
}
|
||||
|
||||
type AgentDownloadStats struct {
|
||||
Platform string `json:"platform"`
|
||||
Clicks int64 `json:"clicks"`
|
||||
Visits int64 `json:"visits"`
|
||||
}
|
||||
|
||||
type GetUserInviteStatsRequest struct{}
|
||||
|
||||
type GetUserInviteStatsResponse struct {
|
||||
FriendlyCount int64 `json:"friendly_count"` // 有效邀请数(有订单的用户)
|
||||
HistoryCount int64 `json:"history_count"` // 历史邀请总数
|
||||
}
|
||||
|
||||
type GetInviteSalesRequest struct {
|
||||
Page int `form:"page" validate:"required"`
|
||||
Size int `form:"size" validate:"required"`
|
||||
}
|
||||
|
||||
type GetInviteSalesResponse struct {
|
||||
Total int64 `json:"total"` // 销售记录总数
|
||||
List []InvitedUserSale `json:"list"` // 销售数据列表(分页)
|
||||
}
|
||||
|
||||
type InvitedUserSale struct {
|
||||
Amount int64 `json:"amount"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
UserEmail string `json:"user_email"`
|
||||
UserId int64 `json:"user_id"`
|
||||
}
|
||||
|
||||
type GetLoginLogRequest struct {
|
||||
Page int `form:"page"`
|
||||
Size int `form:"size"`
|
||||
|
||||
45
pkg/openinstall/openinstall.go
Normal file
45
pkg/openinstall/openinstall.go
Normal file
@ -0,0 +1,45 @@
|
||||
package openinstall
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Client for OpenInstall API
|
||||
type Client struct {
|
||||
appKey string
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
// NewClient creates a new OpenInstall client
|
||||
func NewClient(appKey string) *Client {
|
||||
return &Client{
|
||||
appKey: appKey,
|
||||
httpClient: &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// PlatformStats represents statistics for a specific platform
|
||||
type PlatformStats struct {
|
||||
Platform string `json:"platform"`
|
||||
Clicks int64 `json:"clicks"` // OpenInstall "click"
|
||||
Visits int64 `json:"visits"` // OpenInstall "visit"
|
||||
}
|
||||
|
||||
// Mock implementation for now as we don't have real API docs/credentials
|
||||
// In a real implementation, this would call OpenInstall's API
|
||||
func (c *Client) GetPlatformStats(ctx context.Context, startDate, endDate time.Time) ([]PlatformStats, error) {
|
||||
// TODO: distinct implementation when API details are confirmed
|
||||
|
||||
// Mock response
|
||||
return []PlatformStats{
|
||||
{Platform: "iOS", Clicks: 0, Visits: 0},
|
||||
{Platform: "Android", Clicks: 0, Visits: 0},
|
||||
{Platform: "Windows", Clicks: 0, Visits: 0},
|
||||
{Platform: "macOS", Clicks: 0, Visits: 0},
|
||||
{Platform: "Linux", Clicks: 0, Visits: 0},
|
||||
}, nil
|
||||
}
|
||||
167
test_data_mock_invites.sql
Normal file
167
test_data_mock_invites.sql
Normal file
@ -0,0 +1,167 @@
|
||||
-- ===================================================================
|
||||
-- 模拟邀请数据 SQL - 用于测试 Agent/Invite APIs
|
||||
-- ===================================================================
|
||||
-- 说明:
|
||||
-- 1. 假设当前登录用户 ID = 524(邀请者)
|
||||
-- 2. 创建 5 个被邀请用户(user_id: 10001-10005)
|
||||
-- 3. 其中 3 个用户有付费订单(user_id: 10001, 10002, 10003)
|
||||
-- 4. 2 个用户没有订单(user_id: 10004, 10005)
|
||||
-- ===================================================================
|
||||
|
||||
-- 1. 插入被邀请用户(referer_id = 524,即当前登录用户)
|
||||
-- 注意:如果这些 ID 已存在,请先手动修改为其他未使用的 ID
|
||||
|
||||
-- 用户 10001:有2个已支付订单
|
||||
INSERT INTO `user` (
|
||||
`id`, `referer_id`, `refer_code`, `password`, `is_admin`,
|
||||
`balance`, `commission`, `created_at`, `updated_at`
|
||||
) VALUES (
|
||||
10001, 524, 'MOCK_CODE_01', '$2a$10$mock.hash.for.testing.only.password.bcrypt.hash', 0,
|
||||
0, 0, NOW(), NOW()
|
||||
) ON DUPLICATE KEY UPDATE `referer_id` = 524;
|
||||
|
||||
-- 为用户 10001 添加 email 认证方式
|
||||
INSERT INTO `user_auth_methods` (`user_id`, `auth_type`, `auth_identifier`, `verified`, `created_at`, `updated_at`)
|
||||
VALUES (10001, 'email', 'mock_user_001@test.com', 1, NOW(), NOW())
|
||||
ON DUPLICATE KEY UPDATE `auth_identifier` = 'mock_user_001@test.com';
|
||||
|
||||
-- 用户 10002:有1个已支付订单
|
||||
INSERT INTO `user` (
|
||||
`id`, `referer_id`, `refer_code`, `password`, `is_admin`,
|
||||
`balance`, `commission`, `created_at`, `updated_at`
|
||||
) VALUES (
|
||||
10002, 524, 'MOCK_CODE_02', '$2a$10$mock.hash.for.testing.only.password.bcrypt.hash', 0,
|
||||
0, 0, NOW(), NOW()
|
||||
) ON DUPLICATE KEY UPDATE `referer_id` = 524;
|
||||
|
||||
INSERT INTO `user_auth_methods` (`user_id`, `auth_type`, `auth_identifier`, `verified`, `created_at`, `updated_at`)
|
||||
VALUES (10002, 'email', 'mock_user_002@test.com', 1, NOW(), NOW())
|
||||
ON DUPLICATE KEY UPDATE `auth_identifier` = 'mock_user_002@test.com';
|
||||
|
||||
-- 用户 10003:有1个已完成订单
|
||||
INSERT INTO `user` (
|
||||
`id`, `referer_id`, `refer_code`, `password`, `is_admin`,
|
||||
`balance`, `commission`, `created_at`, `updated_at`
|
||||
) VALUES (
|
||||
10003, 524, 'MOCK_CODE_03', '$2a$10$mock.hash.for.testing.only.password.bcrypt.hash', 0,
|
||||
0, 0, NOW(), NOW()
|
||||
) ON DUPLICATE KEY UPDATE `referer_id` = 524;
|
||||
|
||||
INSERT INTO `user_auth_methods` (`user_id`, `auth_type`, `auth_identifier`, `verified`, `created_at`, `updated_at`)
|
||||
VALUES (10003, 'email', 'mock_user_003@test.com', 1, NOW(), NOW())
|
||||
ON DUPLICATE KEY UPDATE `auth_identifier` = 'mock_user_003@test.com';
|
||||
|
||||
-- 用户 10004:没有订单(历史邀请但未付费)
|
||||
INSERT INTO `user` (
|
||||
`id`, `referer_id`, `refer_code`, `password`, `is_admin`,
|
||||
`balance`, `commission`, `created_at`, `updated_at`
|
||||
) VALUES (
|
||||
10004, 524, 'MOCK_CODE_04', '$2a$10$mock.hash.for.testing.only.password.bcrypt.hash', 0,
|
||||
0, 0, NOW(), NOW()
|
||||
) ON DUPLICATE KEY UPDATE `referer_id` = 524;
|
||||
|
||||
INSERT INTO `user_auth_methods` (`user_id`, `auth_type`, `auth_identifier`, `verified`, `created_at`, `updated_at`)
|
||||
VALUES (10004, 'email', 'mock_user_004@test.com', 1, NOW(), NOW())
|
||||
ON DUPLICATE KEY UPDATE `auth_identifier` = 'mock_user_004@test.com';
|
||||
|
||||
-- 用户 10005:没有订单(历史邀请但未付费)
|
||||
INSERT INTO `user` (
|
||||
`id`, `referer_id`, `refer_code`, `password`, `is_admin`,
|
||||
`balance`, `commission`, `created_at`, `updated_at`
|
||||
) VALUES (
|
||||
10005, 524, 'MOCK_CODE_05', '$2a$10$mock.hash.for.testing.only.password.bcrypt.hash', 0,
|
||||
0, 0, NOW(), NOW()
|
||||
) ON DUPLICATE KEY UPDATE `referer_id` = 524;
|
||||
|
||||
INSERT INTO `user_auth_methods` (`user_id`, `auth_type`, `auth_identifier`, `verified`, `created_at`, `updated_at`)
|
||||
VALUES (10005, 'email', 'mock_user_005@test.com', 1, NOW(), NOW())
|
||||
ON DUPLICATE KEY UPDATE `auth_identifier` = 'mock_user_005@test.com';
|
||||
|
||||
|
||||
|
||||
-- ===================================================================
|
||||
-- 2. 插入订单数据
|
||||
-- status: 2 = Paid(已支付), 5 = Finished(已完成)
|
||||
-- ===================================================================
|
||||
|
||||
-- 订单 1:用户 10001,已支付,金额 $9.99
|
||||
INSERT INTO `order` (
|
||||
`user_id`, `order_no`, `type`, `status`, `amount`, `quantity`,
|
||||
`payment_id`, `created_at`, `updated_at`
|
||||
) VALUES (
|
||||
10001, 'MOCK_ORDER_001', 1, 2, 999, 1,
|
||||
1, DATE_SUB(NOW(), INTERVAL 10 DAY), DATE_SUB(NOW(), INTERVAL 10 DAY)
|
||||
);
|
||||
|
||||
-- 订单 2:用户 10001,已支付,金额 $19.99
|
||||
INSERT INTO `order` (
|
||||
`user_id`, `order_no`, `type`, `status`, `amount`, `quantity`,
|
||||
`payment_id`, `created_at`, `updated_at`
|
||||
) VALUES (
|
||||
10001, 'MOCK_ORDER_002', 1, 2, 1999, 1,
|
||||
1, DATE_SUB(NOW(), INTERVAL 5 DAY), DATE_SUB(NOW(), INTERVAL 5 DAY)
|
||||
);
|
||||
|
||||
-- 订单 3:用户 10002,已支付,金额 $29.99
|
||||
INSERT INTO `order` (
|
||||
`user_id`, `order_no`, `type`, `status`, `amount`, `quantity`,
|
||||
`payment_id`, `created_at`, `updated_at`
|
||||
) VALUES (
|
||||
10002, 'MOCK_ORDER_003', 1, 2, 2999, 1,
|
||||
1, DATE_SUB(NOW(), INTERVAL 3 DAY), DATE_SUB(NOW(), INTERVAL 3 DAY)
|
||||
);
|
||||
|
||||
-- 订单 4:用户 10003,已完成,金额 $49.99
|
||||
INSERT INTO `order` (
|
||||
`user_id`, `order_no`, `type`, `status`, `amount`, `quantity`,
|
||||
`payment_id`, `created_at`, `updated_at`
|
||||
) VALUES (
|
||||
10003, 'MOCK_ORDER_004', 1, 5, 4999, 1,
|
||||
1, DATE_SUB(NOW(), INTERVAL 1 DAY), DATE_SUB(NOW(), INTERVAL 1 DAY)
|
||||
);
|
||||
|
||||
-- 订单 5:用户 10001,待支付(status=1,不会被统计)
|
||||
INSERT INTO `order` (
|
||||
`user_id`, `order_no`, `type`, `status`, `amount`, `quantity`,
|
||||
`payment_id`, `created_at`, `updated_at`
|
||||
) VALUES (
|
||||
10001, 'MOCK_ORDER_005', 1, 1, 999, 1,
|
||||
1, NOW(), NOW()
|
||||
);
|
||||
|
||||
|
||||
-- ===================================================================
|
||||
-- 3. 验证查询
|
||||
-- ===================================================================
|
||||
|
||||
-- 查询当前用户(524)的邀请统计
|
||||
-- 应返回:
|
||||
-- - friendlyCount: 3 (user_id 10001, 10002, 10003 有已支付/完成订单)
|
||||
-- - historyCount: 5 (user_id 10001-10005 都是被邀请用户)
|
||||
SELECT
|
||||
(SELECT COUNT(*) FROM user WHERE referer_id = 524
|
||||
AND EXISTS (SELECT 1 FROM `order` o WHERE o.user_id = user.id AND o.status IN (2, 5))
|
||||
) as friendlyCount,
|
||||
(SELECT COUNT(*) FROM user WHERE referer_id = 524) as historyCount;
|
||||
|
||||
-- 查询销售记录(分页查询,前10条)
|
||||
-- 应返回 4 条记录(MOCK_ORDER_001 到 MOCK_ORDER_004)
|
||||
SELECT
|
||||
o.amount,
|
||||
UNIX_TIMESTAMP(o.created_at) * 1000 as created_at,
|
||||
u.id as user_id,
|
||||
COALESCE(am.auth_identifier, 'no_email') as user_email
|
||||
FROM `order` o
|
||||
JOIN user u ON o.user_id = u.id
|
||||
LEFT JOIN user_auth_methods am ON am.user_id = u.id AND am.auth_type = 'email'
|
||||
WHERE u.referer_id = 524 AND o.status IN (2, 5)
|
||||
ORDER BY o.created_at DESC
|
||||
LIMIT 10 OFFSET 0;
|
||||
|
||||
|
||||
|
||||
-- ===================================================================
|
||||
-- 清理命令(如需删除测试数据,请执行以下语句)
|
||||
-- ===================================================================
|
||||
-- DELETE FROM `order` WHERE order_no LIKE 'MOCK_ORDER_%';
|
||||
-- DELETE FROM `user` WHERE id BETWEEN 10001 AND 10005;
|
||||
Loading…
x
Reference in New Issue
Block a user