sync: soul-api 接口逻辑 | 原因: 后端接口逻辑修改

This commit is contained in:
卡若
2026-03-08 08:25:39 +08:00
parent 3824fbbf5f
commit c28798a5e5

View File

@@ -0,0 +1,271 @@
package handler
import (
"math"
"net/http"
"sort"
"strings"
"time"
"soul-api/internal/database"
"soul-api/internal/model"
"github.com/gin-gonic/gin"
)
// rfmUser 计算后的 RFM 用户数据
type rfmUser struct {
ID string `json:"id"`
Nickname string `json:"nickname"`
Phone string `json:"phone,omitempty"`
Avatar string `json:"avatar,omitempty"`
RFMScore float64 `json:"rfmScore"`
RFMLevel string `json:"rfmLevel"`
Recency int `json:"recency"` // 距最近订单天数
Frequency int `json:"frequency"` // 订单数
Monetary float64 `json:"monetary"` // 总消费
LastOrderAt *string `json:"lastOrderAt,omitempty"`
}
// rfmOrder 从 orders 表读取的简化结构
type rfmOrder struct {
UserID string
Amount float64
CreatedAt time.Time
}
func calcRFMLevel(score float64) string {
switch {
case score >= 85:
return "S"
case score >= 70:
return "A"
case score >= 50:
return "B"
case score >= 30:
return "C"
default:
return "D"
}
}
// calcRFMScore 计算单个用户的 RFM 综合分0-100
// R: 距今天数 → 越少越高F: 频次M: 总金额
// 简化打分R(40%) + F(30%) + M(30%),各维度用全量用户相对排位归一化
func calcRFMScoreForUser(recencyDays int, frequency int, monetary float64,
maxRecency, maxFreq int, maxMonetary float64) float64 {
var rScore, fScore, mScore float64
// R 分:天数越少越高,取反归一化
if maxRecency > 0 {
rScore = (1 - float64(recencyDays)/float64(maxRecency)) * 100
}
// F 分:频次越高越好
if maxFreq > 0 {
fScore = float64(frequency) / float64(maxFreq) * 100
}
// M 分:金额越高越好
if maxMonetary > 0 {
mScore = monetary / maxMonetary * 100
}
total := rScore*0.4 + fScore*0.3 + mScore*0.3
// 四舍五入到整数
return math.Round(total)
}
// DBUsersRFM GET /api/db/users/rfm — 全量 RFM 排行
func DBUsersRFM(c *gin.Context) {
db := database.DB()
search := strings.TrimSpace(c.Query("search"))
limitStr := c.DefaultQuery("limit", "100")
// 1. 聚合每个用户的订单数据
type orderAgg struct {
UserID string
OrderCount int
TotalAmount float64
LastOrderAt time.Time
}
var aggs []orderAgg
if err := db.Raw(`
SELECT user_id, COUNT(*) as order_count, SUM(amount) as total_amount, MAX(created_at) as last_order_at
FROM orders
WHERE status IN ('paid','success','completed')
GROUP BY user_id
`).Scan(&aggs).Error; err != nil {
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
return
}
if len(aggs) == 0 {
c.JSON(http.StatusOK, gin.H{"success": true, "users": []rfmUser{}})
return
}
// 2. 计算各维度最大值(用于归一化)
now := time.Now()
var maxRecency int
var maxFreq int
var maxMonetary float64
for _, a := range aggs {
days := int(now.Sub(a.LastOrderAt).Hours() / 24)
if days > maxRecency {
maxRecency = days
}
if a.OrderCount > maxFreq {
maxFreq = a.OrderCount
}
if a.TotalAmount > maxMonetary {
maxMonetary = a.TotalAmount
}
}
// 3. 计算每用户得分
aggMap := make(map[string]orderAgg, len(aggs))
for _, a := range aggs {
aggMap[a.UserID] = a
}
userIDs := make([]string, 0, len(aggs))
for _, a := range aggs {
userIDs = append(userIDs, a.UserID)
}
// 4. 拉取用户基础信息
var users []model.User
q := db.Where("id IN ?", userIDs)
if search != "" {
q = q.Where("nickname LIKE ? OR phone LIKE ?", "%"+search+"%", "%"+search+"%")
}
if err := q.Find(&users).Error; err != nil {
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
return
}
// 5. 组装结果
result := make([]rfmUser, 0, len(users))
for _, u := range users {
agg, ok := aggMap[u.ID]
if !ok {
continue
}
recencyDays := int(now.Sub(agg.LastOrderAt).Hours() / 24)
score := calcRFMScoreForUser(recencyDays, agg.OrderCount, agg.TotalAmount, maxRecency, maxFreq, maxMonetary)
lastAt := agg.LastOrderAt.Format(time.RFC3339)
ru := rfmUser{
ID: u.ID,
RFMScore: score,
RFMLevel: calcRFMLevel(score),
Recency: recencyDays,
Frequency: agg.OrderCount,
Monetary: agg.TotalAmount,
LastOrderAt: &lastAt,
}
if u.Nickname != nil {
ru.Nickname = *u.Nickname
}
if u.Phone != nil {
ru.Phone = *u.Phone
}
if u.Avatar != nil {
ru.Avatar = *u.Avatar
}
result = append(result, ru)
}
// 6. 按 RFM 分降序排序
sort.Slice(result, func(i, j int) bool {
return result[i].RFMScore > result[j].RFMScore
})
// 7. 截取数量
limit := 100
if limitStr != "" {
if n, err := parseInt(limitStr); err == nil && n > 0 {
limit = n
}
}
if limit > len(result) {
limit = len(result)
}
result = result[:limit]
c.JSON(http.StatusOK, gin.H{"success": true, "users": result, "total": len(result)})
}
// DBUserRFMSingle GET /api/db/users/rfm-single?userId=xxx — 单用户 RFM
func DBUserRFMSingle(c *gin.Context) {
db := database.DB()
userID := strings.TrimSpace(c.Query("userId"))
if userID == "" {
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "缺少 userId"})
return
}
type orderAgg struct {
OrderCount int
TotalAmount float64
LastOrderAt *time.Time
}
var agg orderAgg
if err := db.Raw(`
SELECT COUNT(*) as order_count, SUM(amount) as total_amount, MAX(created_at) as last_order_at
FROM orders
WHERE user_id = ? AND status IN ('paid','success','completed')
`, userID).Scan(&agg).Error; err != nil {
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
return
}
if agg.OrderCount == 0 {
c.JSON(http.StatusOK, gin.H{"success": true, "rfm": nil})
return
}
// 获取全量最大值用于打分(简化:用固定上限或查全表最大值)
type maxVals struct {
MaxFreq int
MaxMonetary float64
MaxRecency int
}
var mv maxVals
_ = db.Raw(`
SELECT MAX(cnt) as max_freq, MAX(total) as max_monetary,
MAX(DATEDIFF(NOW(), last_at)) as max_recency
FROM (
SELECT COUNT(*) as cnt, SUM(amount) as total, MAX(created_at) as last_at
FROM orders
WHERE status IN ('paid','success','completed')
GROUP BY user_id
) t
`).Scan(&mv)
now := time.Now()
var recencyDays int
var lastAtStr *string
if agg.LastOrderAt != nil {
recencyDays = int(now.Sub(*agg.LastOrderAt).Hours() / 24)
s := agg.LastOrderAt.Format(time.RFC3339)
lastAtStr = &s
}
score := calcRFMScoreForUser(recencyDays, agg.OrderCount, agg.TotalAmount, mv.MaxRecency, mv.MaxFreq, mv.MaxMonetary)
c.JSON(http.StatusOK, gin.H{
"success": true,
"rfm": gin.H{
"rfmScore": score,
"rfmLevel": calcRFMLevel(score),
"recency": recencyDays,
"frequency": agg.OrderCount,
"monetary": agg.TotalAmount,
"lastOrderAt": lastAtStr,
},
})
}
// parseInt 辅助函数
func parseInt(s string) (int, error) {
var n int
_, err := fmt.Sscanf(s, "%d", &n)
return n, err
}