暂存
This commit is contained in:
@@ -3,7 +3,9 @@ package handler
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"soul-api/internal/database"
|
||||
"soul-api/internal/model"
|
||||
@@ -54,11 +56,17 @@ func AdminDashboardStats(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// AdminDashboardRecentOrders GET /api/admin/dashboard/recent-orders
|
||||
// AdminDashboardRecentOrders GET /api/admin/dashboard/recent-orders?limit=10
|
||||
func AdminDashboardRecentOrders(c *gin.Context) {
|
||||
db := database.DB()
|
||||
limit := 5
|
||||
if l := c.Query("limit"); l != "" {
|
||||
if n, err := strconv.Atoi(l); err == nil && n >= 1 && n <= 20 {
|
||||
limit = n
|
||||
}
|
||||
}
|
||||
var recentOrders []model.Order
|
||||
db.Where("status IN ?", paidStatuses).Order("created_at DESC").Limit(5).Find(&recentOrders)
|
||||
db.Where("status IN ?", paidStatuses).Order("created_at DESC").Limit(limit).Find(&recentOrders)
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "recentOrders": buildRecentOrdersOut(db, recentOrders)})
|
||||
}
|
||||
|
||||
@@ -180,6 +188,101 @@ func buildRecentOrdersOut(db *gorm.DB, recentOrders []model.Order) []gin.H {
|
||||
return out
|
||||
}
|
||||
|
||||
// AdminTrackStats GET /api/admin/track/stats?period=today|week|month|all
|
||||
// 埋点统计:按 extra_data->module 分组,按 action+target 聚合 count
|
||||
func AdminTrackStats(c *gin.Context) {
|
||||
period := c.DefaultQuery("period", "week")
|
||||
if period != "today" && period != "week" && period != "month" && period != "all" {
|
||||
period = "week"
|
||||
}
|
||||
now := time.Now()
|
||||
var start time.Time
|
||||
switch period {
|
||||
case "today":
|
||||
start = time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
||||
case "week":
|
||||
weekday := int(now.Weekday())
|
||||
if weekday == 0 {
|
||||
weekday = 7
|
||||
}
|
||||
start = time.Date(now.Year(), now.Month(), now.Day()-weekday+1, 0, 0, 0, 0, now.Location())
|
||||
case "month":
|
||||
start = time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
|
||||
case "all":
|
||||
start = time.Time{}
|
||||
}
|
||||
db := database.DB()
|
||||
var tracks []model.UserTrack
|
||||
q := db.Model(&model.UserTrack{})
|
||||
if !start.IsZero() {
|
||||
q = q.Where("created_at >= ?", start)
|
||||
}
|
||||
if err := q.Find(&tracks).Error; err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
// byModule: module -> map[key] -> count, key = action + "|" + target
|
||||
type item struct {
|
||||
Action string `json:"action"`
|
||||
Target string `json:"target"`
|
||||
Module string `json:"module"`
|
||||
Page string `json:"page"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
byModule := make(map[string]map[string]*item)
|
||||
total := 0
|
||||
for _, t := range tracks {
|
||||
total++
|
||||
module := "other"
|
||||
page := ""
|
||||
if len(t.ExtraData) > 0 {
|
||||
var extra map[string]interface{}
|
||||
if err := json.Unmarshal(t.ExtraData, &extra); err == nil {
|
||||
if m, ok := extra["module"].(string); ok && m != "" {
|
||||
module = m
|
||||
}
|
||||
if p, ok := extra["page"].(string); ok {
|
||||
page = p
|
||||
}
|
||||
}
|
||||
}
|
||||
target := ""
|
||||
if t.Target != nil {
|
||||
target = *t.Target
|
||||
}
|
||||
key := t.Action + "|" + target
|
||||
if byModule[module] == nil {
|
||||
byModule[module] = make(map[string]*item)
|
||||
}
|
||||
if byModule[module][key] == nil {
|
||||
byModule[module][key] = &item{Action: t.Action, Target: target, Module: module, Page: page, Count: 0}
|
||||
}
|
||||
byModule[module][key].Count++
|
||||
}
|
||||
// 转为前端期望格式:byModule[module] = [{action,target,module,page,count},...]
|
||||
out := make(map[string][]gin.H)
|
||||
for mod, m := range byModule {
|
||||
list := make([]gin.H, 0, len(m))
|
||||
for _, v := range m {
|
||||
list = append(list, gin.H{
|
||||
"action": v.Action, "target": v.Target, "module": v.Module, "page": v.Page, "count": v.Count,
|
||||
})
|
||||
}
|
||||
out[mod] = list
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "total": total, "byModule": out})
|
||||
}
|
||||
|
||||
// AdminBalanceSummary GET /api/admin/balance/summary
|
||||
// 汇总代付金额(product_type=gift_pay 的已支付订单金额),用于 Dashboard 显示「含代付 ¥xx」
|
||||
func AdminBalanceSummary(c *gin.Context) {
|
||||
db := database.DB()
|
||||
var totalGifted float64
|
||||
db.Model(&model.Order{}).Where("product_type = ? AND status IN ?", "gift_pay", paidStatuses).
|
||||
Select("COALESCE(SUM(amount), 0)").Scan(&totalGifted)
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"totalGifted": totalGifted}})
|
||||
}
|
||||
|
||||
// AdminDashboardMerchantBalance GET /api/admin/dashboard/merchant-balance
|
||||
// 查询微信商户号实时余额(可用余额、待结算余额),用于看板展示
|
||||
// 注意:普通商户可能需向微信申请开通权限,未开通时返回 error
|
||||
|
||||
@@ -39,6 +39,8 @@ func GetPublicDBConfig(c *gin.Context) {
|
||||
"minWithdraw": 10,
|
||||
"withdrawSubscribeTmplId": "u3MbZGPRkrZIk-I7QdpwzFxnO_CeQPaCWF2FkiIablE",
|
||||
"mchId": "1318592501",
|
||||
"auditMode": false,
|
||||
"supportWechat": true,
|
||||
}
|
||||
|
||||
out := gin.H{
|
||||
@@ -134,6 +136,14 @@ func GetPublicDBConfig(c *gin.Context) {
|
||||
if _, has := out["linkedMiniprograms"]; !has {
|
||||
out["linkedMiniprograms"] = []gin.H{}
|
||||
}
|
||||
// 明确归一化 auditMode:仅当 DB 显式为 true 时返回 true,否则一律 false(避免历史脏数据/类型异常导致误判)
|
||||
if mp, ok := out["mpConfig"].(gin.H); ok {
|
||||
if v, ok := mp["auditMode"].(bool); ok && v {
|
||||
mp["auditMode"] = true
|
||||
} else {
|
||||
mp["auditMode"] = false
|
||||
}
|
||||
}
|
||||
cache.Set(context.Background(), cache.KeyConfigMiniprogram, out, cache.ConfigTTL)
|
||||
c.JSON(http.StatusOK, out)
|
||||
}
|
||||
@@ -179,10 +189,12 @@ func AdminSettingsGet(c *gin.Context) {
|
||||
"withdrawSubscribeTmplId": "u3MbZGPRkrZIk-I7QdpwzFxnO_CeQPaCWF2FkiIablE",
|
||||
"mchId": "1318592501",
|
||||
"minWithdraw": float64(10),
|
||||
"auditMode": false,
|
||||
"supportWechat": true,
|
||||
}
|
||||
out := gin.H{
|
||||
"success": true,
|
||||
"featureConfig": gin.H{"matchEnabled": true, "referralEnabled": true, "searchEnabled": true},
|
||||
"featureConfig": gin.H{"matchEnabled": true, "referralEnabled": true, "searchEnabled": true, "aboutEnabled": true},
|
||||
"siteSettings": gin.H{"sectionPrice": float64(1), "baseBookPrice": 9.9, "distributorShare": float64(90), "authorInfo": gin.H{}},
|
||||
"mpConfig": defaultMp,
|
||||
"ossConfig": gin.H{},
|
||||
@@ -902,18 +914,24 @@ func randomSuffix() string {
|
||||
return fmt.Sprintf("%d%x", time.Now().UnixNano()%100000, time.Now().UnixNano()&0xfff)
|
||||
}
|
||||
|
||||
// DBUsersDelete DELETE /api/db/users
|
||||
// DBUsersDelete DELETE /api/db/users(软删除:仅设置 deleted_at,用户再次登录会新建账号)
|
||||
func DBUsersDelete(c *gin.Context) {
|
||||
id := c.Query("id")
|
||||
if id == "" {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "用户ID不能为空"})
|
||||
return
|
||||
}
|
||||
if err := database.DB().Where("id = ?", id).Delete(&model.User{}).Error; err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
|
||||
db := database.DB()
|
||||
result := db.Where("id = ?", id).Delete(&model.User{})
|
||||
if result.Error != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": result.Error.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": "用户删除成功"})
|
||||
if result.RowsAffected == 0 {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "用户不存在或已被删除"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": "用户已删除(假删除),该用户再次登录将创建新账号"})
|
||||
}
|
||||
|
||||
// DBUsersReferrals GET /api/db/users/referrals(绑定关系详情弹窗;收益与「已付费」与小程序口径一致:订单+提现表实时计算)
|
||||
|
||||
@@ -69,8 +69,8 @@ func MiniprogramLogin(c *gin.Context) {
|
||||
isNewUser := result.Error != nil
|
||||
|
||||
if isNewUser {
|
||||
// 创建新用户
|
||||
userID := openID // 直接使用 openid 作为用户 ID
|
||||
// 创建新用户(含软删除后再次登录:旧记录 id=openid 仍存在,需用新 id 避免主键冲突)
|
||||
userID := "user_" + randomSuffix()
|
||||
referralCode := "SOUL" + strings.ToUpper(openID[len(openID)-6:])
|
||||
nickname := "微信用户" + openID[len(openID)-4:]
|
||||
avatar := ""
|
||||
@@ -393,9 +393,17 @@ func miniprogramPayPost(c *gin.Context) {
|
||||
clientIP = "127.0.0.1"
|
||||
}
|
||||
|
||||
// userID:优先用客户端传入;为空时按 openid 查用户(排除软删除,避免订单归属到旧账号)
|
||||
userID := req.UserID
|
||||
if userID == "" {
|
||||
userID = req.OpenID
|
||||
if userID == "" && req.OpenID != "" {
|
||||
var u model.User
|
||||
if err := db.Where("open_id = ?", req.OpenID).First(&u).Error; err == nil {
|
||||
userID = u.ID
|
||||
} else {
|
||||
// 查不到用户:可能是未登录或软删除后未重新登录,避免用 openid 导致订单归属到旧账号
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "请先登录后再支付"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
productID := req.ProductID
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
@@ -658,6 +659,11 @@ func MiniprogramTrackPost(c *gin.Context) {
|
||||
if body.Target != "" {
|
||||
t.ChapterID = &chID
|
||||
}
|
||||
if body.ExtraData != nil {
|
||||
if b, err := json.Marshal(body.ExtraData); err == nil {
|
||||
t.ExtraData = b
|
||||
}
|
||||
}
|
||||
if err := db.Create(&t).Error; err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
|
||||
return
|
||||
|
||||
@@ -54,6 +54,8 @@ func WechatPhoneLogin(c *gin.Context) {
|
||||
isNewUser := result.Error != nil
|
||||
|
||||
if isNewUser {
|
||||
// 软删除后再次登录:旧记录 id=openid 仍存在,需用新 id 避免主键冲突
|
||||
userID := "user_" + randomSuffix()
|
||||
referralCode := "SOUL" + strings.ToUpper(openID[len(openID)-6:])
|
||||
nickname := "微信用户" + openID[len(openID)-4:]
|
||||
avatar := ""
|
||||
@@ -67,7 +69,7 @@ func WechatPhoneLogin(c *gin.Context) {
|
||||
phone = "+" + countryCode + " " + phoneNumber
|
||||
}
|
||||
user = model.User{
|
||||
ID: openID,
|
||||
ID: userID,
|
||||
OpenID: &openID,
|
||||
SessionKey: &sessionKey,
|
||||
Nickname: &nickname,
|
||||
|
||||
Reference in New Issue
Block a user