Files
blind-select/backend/internal/handler/v1/handler.go
T
admin 06488f0237 Initial commit: 帮我选盲选应用
功能:
- Go后端 (Gin + GORM + PostgreSQL)
- UniApp用户端 (iOS/Android/小程序)
- DaisyUI5后台管理
- JWT认证 + 微信登录
- 盲选加权算法
- 会员系统 + 优惠券
- 打分评价 + 偏好学习
2026-06-08 20:18:31 +00:00

831 lines
24 KiB
Go

package handler
import (
"net/http"
"strconv"
"time"
"github.com/blind-select/backend/internal/database"
"github.com/blind-select/backend/internal/middleware"
"github.com/blind-select/backend/internal/model"
"github.com/blind-select/backend/internal/service"
"github.com/blind-select/backend/internal/utils"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
type AuthHandler struct {
JWTSecret string
WechatAppID string
WechatAppSecret string
blindSvc *service.BlindService
memberSvc *service.MemberService
couponSvc *service.CouponService
}
func NewAuthHandler(jwtSecret, wechatAppID, wechatAppSec string) *AuthHandler {
return &AuthHandler{
JWTSecret: jwtSecret,
WechatAppID: wechatAppID,
WechatAppSecret: wechatAppSec,
blindSvc: service.NewBlindService(),
memberSvc: service.NewMemberService(),
couponSvc: service.NewCouponService(),
}
}
// ============== User Auth ==============
type RegisterReq struct {
Nickname string `json:"nickname" binding:"required,min=2,max=50"`
Phone string `json:"phone" binding:"required,len=11"`
Password string `json:"password" binding:"required,min=6,max=32"`
}
type LoginReq struct {
Phone string `json:"phone" binding:"required,len=11"`
Password string `json:"password" binding:"required"`
}
type LoginRes struct {
Token string `json:"token"`
RefreshToken string `json:"refresh_token"`
User *model.User `json:"user"`
HasMember bool `json:"has_member"`
}
func (h *AuthHandler) Register(c *gin.Context) {
var req RegisterReq
if err := c.ShouldBindJSON(&req); err != nil {
middleware.JSONError(c, http.StatusBadRequest, err.Error())
return
}
db := database.GetDB()
var count int64
db.Model(&model.User{}).Where("phone = ?", req.Phone).Count(&count)
if count > 0 {
middleware.JSONError(c, http.StatusConflict, "phone already registered")
return
}
hash, err := utils.HashPassword(req.Password)
if err != nil {
middleware.JSONError(c, http.StatusInternalServerError, "failed to hash password")
return
}
user := model.User{
Nickname: req.Nickname,
Phone: req.Phone,
PasswordHash: hash,
Tags: []string{},
RepeatDays: 7,
MemberLevel: 0,
}
if err := db.Create(&user).Error; err != nil {
middleware.JSONError(c, http.StatusInternalServerError, "failed to create user")
return
}
token, _ := utils.GenerateToken(user.ID, user.Nickname, "user", h.JWTSecret)
refreshToken, _ := utils.GenerateRefreshToken(user.ID, h.JWTSecret)
middleware.JSONResponse(c, http.StatusCreated, LoginRes{
Token: token,
RefreshToken: refreshToken,
User: &user,
HasMember: false,
})
}
func (h *AuthHandler) Login(c *gin.Context) {
var req LoginReq
if err := c.ShouldBindJSON(&req); err != nil {
middleware.JSONError(c, http.StatusBadRequest, "invalid request")
return
}
db := database.GetDB()
var user model.User
if err := db.Where("phone = ?", req.Phone).First(&user).Error; err != nil {
middleware.JSONError(c, http.StatusUnauthorized, "invalid phone or password")
return
}
if !utils.CheckPassword(req.Password, user.PasswordHash) {
middleware.JSONError(c, http.StatusUnauthorized, "invalid phone or password")
return
}
token, _ := utils.GenerateToken(user.ID, user.Nickname, "user", h.JWTSecret)
refreshToken, _ := utils.GenerateRefreshToken(user.ID, h.JWTSecret)
hasMember := user.MemberLevel >= 1 && user.MemberExpires != nil && user.MemberExpires.After(user.UpdatedAt)
middleware.JSONResponse(c, http.StatusOK, LoginRes{
Token: token,
RefreshToken: refreshToken,
User: &user,
HasMember: hasMember,
})
}
func (h *AuthHandler) GetProfile(c *gin.Context) {
userID, _ := c.Get("user_id")
uid := userID.(uint)
db := database.GetDB()
var user model.User
if err := db.First(&user, uid).Error; err != nil {
middleware.JSONError(c, http.StatusNotFound, "user not found")
return
}
middleware.JSONResponse(c, http.StatusOK, user)
}
func (h *AuthHandler) UpdateProfile(c *gin.Context) {
userID, _ := c.Get("user_id")
uid := userID.(uint)
var req struct {
Nickname string `json:"nickname" binding:"omitempty,min=2,max=50"`
Avatar string `json:"avatar" binding:"omitempty,url"`
}
if err := c.ShouldBindJSON(&req); err != nil {
middleware.JSONError(c, http.StatusBadRequest, "invalid request")
return
}
updates := map[string]interface{}{}
if req.Nickname != "" {
updates["nickname"] = req.Nickname
}
if req.Avatar != "" {
updates["avatar"] = req.Avatar
}
if len(updates) > 0 {
database.GetDB().Model(&model.User{}).Where("id = ?", uid).Updates(updates)
}
var user model.User
database.GetDB().First(&user, uid)
middleware.JSONResponse(c, http.StatusOK, user)
}
func (h *AuthHandler) WechatLogin(c *gin.Context) {
var req struct {
Code string `json:"code" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
middleware.JSONError(c, http.StatusBadRequest, "invalid request")
return
}
wechatSvc := service.NewWechatService(h.WechatAppID, h.WechatAppSecret)
user, _, err := wechatSvc.LoginOrCreateUser(req.Code)
if err != nil {
middleware.JSONError(c, http.StatusUnauthorized, "wechat login failed: "+err.Error())
return
}
token, _ := utils.GenerateToken(user.ID, user.Nickname, "user", h.JWTSecret)
refreshToken, _ := utils.GenerateRefreshToken(user.ID, h.JWTSecret)
middleware.JSONResponse(c, http.StatusOK, gin.H{
"token": token,
"refresh_token": refreshToken,
"user": user,
})
}
func (h *AuthHandler) RefreshToken(c *gin.Context) {
var req struct {
RefreshToken string `json:"refresh_token" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
middleware.JSONError(c, http.StatusBadRequest, "invalid request")
return
}
claims, err := utils.ParseToken(req.RefreshToken, h.JWTSecret)
if err != nil {
middleware.JSONError(c, http.StatusUnauthorized, "invalid refresh token")
return
}
token, _ := utils.GenerateToken(claims.UserID, claims.Username, claims.Role, h.JWTSecret)
middleware.JSONResponse(c, http.StatusOK, gin.H{"token": token})
}
func (h *AuthHandler) AdminLogin(c *gin.Context) {
var req struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
middleware.JSONError(c, http.StatusBadRequest, "invalid request")
return
}
db := database.GetDB()
var admin model.Admin
if db.Where("username = ?", req.Username).First(&admin).Error != nil {
middleware.JSONError(c, http.StatusUnauthorized, "invalid username or password")
return
}
if !utils.CheckPassword(req.Password, admin.Password) {
middleware.JSONError(c, http.StatusUnauthorized, "invalid username or password")
return
}
token, _ := utils.GenerateToken(uint(admin.ID), admin.Username, "admin", h.JWTSecret)
middleware.JSONResponse(c, http.StatusOK, gin.H{
"token": token,
"admin": gin.H{"id": admin.ID, "username": admin.Username, "role": admin.Role},
})
}
func (h *AuthHandler) Dashboard(c *gin.Context) {
db := database.GetDB()
var userCount, merchantCount, packageCount, blindCount int64
db.Model(&model.User{}).Count(&userCount)
db.Model(&model.Merchant{}).Where("status = ?", "approved").Count(&merchantCount)
db.Model(&model.Package{}).Where("status = ?", "active").Count(&packageCount)
db.Model(&model.BlindSession{}).Count(&blindCount)
middleware.JSONResponse(c, http.StatusOK, gin.H{
"today_users": userCount,
"merchants": merchantCount,
"packages": packageCount,
"blind_sessions": blindCount,
})
}
// ============== Blind Selection ==============
func (h *AuthHandler) GetCategories(c *gin.Context) {
db := database.GetDB()
var categories []model.Category
db.Where("status = ?", "active").Order("sort ASC").Find(&categories)
middleware.JSONResponse(c, http.StatusOK, categories)
}
func (h *AuthHandler) GetPool(c *gin.Context) {
categ := c.Query("category")
db := database.GetDB()
query := db.Model(&model.Package{}).Where("packages.status = ?", "active")
if categ != "" {
if catID, err := strconv.Atoi(categ); err == nil {
query = query.Where("packages.category_id = ?", catID)
}
}
// Join with merchants for merchant name
type PoolItem struct {
ID uint `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
PriceRange string `json:"price_range"`
MerchantID uint `json:"merchant_id"`
Merchant string `json:"merchant"`
Rating float64 `json:"rating"`
CatName string `json:"cat_name"`
}
var packages []model.Package
query.Find(&packages)
result := make([]PoolItem, 0, len(packages))
for _, p := range packages {
var merchant model.Merchant
db.First(&merchant, p.MerchantID)
merchantName := "未知商家"
if merchant.ID > 0 {
merchantName = merchant.Name
}
result = append(result, PoolItem{
ID: p.ID, Name: "神秘" + truncate(p.Name, 4),
Description: p.Description,
PriceRange: "¥" + strconv.Itoa(p.PriceMin) + "-" + strconv.Itoa(p.PriceMax),
MerchantID: p.MerchantID, Merchant: merchantName,
Rating: p.Rating, CatName: getCategoryName(db, p.CategoryID),
})
}
middleware.JSONResponse(c, http.StatusOK, result)
}
func (h *AuthHandler) Choose(c *gin.Context) {
userID, _ := c.Get("user_id")
uid := userID.(uint)
var req struct {
CategoryID uint `json:"category_id" binding:"required"`
PriceRange string `json:"price_range" binding:"required"`
DistanceRange string `json:"distance_range"`
}
if err := c.ShouldBindJSON(&req); err != nil {
middleware.JSONError(c, http.StatusBadRequest, err.Error())
return
}
// Perform blind selection
pkg, merchant, matchScore, err := h.blindSvc.Select(uid, req.CategoryID, req.PriceRange)
if err != nil {
middleware.JSONError(c, http.StatusNotFound, "no available packages in this category/price range")
return
}
// Record selection
session, _, err := h.blindSvc.RecordSelection(uid, req.CategoryID, req.PriceRange, pkg.ID, merchant.ID, matchScore)
if err != nil {
middleware.JSONError(c, http.StatusInternalServerError, "failed to record selection")
return
}
// Build response
resp := gin.H{
"session_id": session.ID,
"result": gin.H{
"package_name": pkg.Name,
"merchant_name": merchant.Name,
"merchant_rating": merchant.Rating,
"description": pkg.Description,
"price_range": "¥" + strconv.Itoa(pkg.PriceMin) + "-" + strconv.Itoa(pkg.PriceMax),
"actual_price": pkg.ActualPrice,
"match_score": matchScore,
"has_coupon": false,
},
}
middleware.JSONResponse(c, http.StatusOK, resp)
}
func (h *AuthHandler) GetResult(c *gin.Context) {
sessionID, _ := strconv.Atoi(c.Param("id"))
db := database.GetDB()
var result model.BlindResult
if err := db.First(&result, sessionID).Error; err != nil {
middleware.JSONError(c, http.StatusNotFound, "result not found")
return
}
middleware.JSONResponse(c, http.StatusOK, result)
}
func (h *AuthHandler) GetHistory(c *gin.Context) {
userID, _ := c.Get("user_id")
uid := userID.(uint)
db := database.GetDB()
var sessions []model.BlindSession
db.Where("user_id = ?", uid).Order("created_at DESC").Limit(20).Find(&sessions)
type HistoryItem struct {
SessionID uint `json:"session_id"`
PackageName string `json:"package_name"`
Merchant string `json:"merchant"`
PriceRange string `json:"price_range"`
MatchScore float64 `json:"match_score"`
Accepted bool `json:"accepted"`
CreatedAt time.Time `json:"created_at"`
}
result := make([]HistoryItem, 0, len(sessions))
for _, s := range sessions {
var br model.BlindResult
db.First(&br, s.ID)
result = append(result, HistoryItem{
SessionID: s.ID,
PackageName: br.PackageName,
Merchant: br.MerchantName,
PriceRange: br.PriceRange,
MatchScore: br.MatchScore,
Accepted: s.Accepted,
CreatedAt: s.CreatedAt,
})
}
middleware.JSONResponse(c, http.StatusOK, result)
}
// ============== Review ==============
func (h *AuthHandler) SubmitReview(c *gin.Context) {
userID, _ := c.Get("user_id")
uid := userID.(uint)
var req struct {
SessionID uint `json:"session_id" binding:"required"`
PackageID uint `json:"package_id" binding:"required"`
Rating int8 `json:"rating" binding:"required,min=1,max=5"`
Taste int8 `json:"taste" binding:"required,min=1,max=5"`
Value int8 `json:"value" binding:"required,min=1,max=5"`
Distance int8 `json:"distance" binding:"required,min=1,max=5"`
Match int8 `json:"match" binding:"required,min=1,max=5"`
Tags []string `json:"tags"`
Text string `json:"text"`
IsRepeat bool `json:"is_repeat"`
}
if err := c.ShouldBindJSON(&req); err != nil {
middleware.JSONError(c, http.StatusBadRequest, err.Error())
return
}
if err := h.blindSvc.SubmitReview(uid, req.SessionID, req.PackageID,
req.Rating, req.Taste, req.Value, req.Distance, req.Match, req.Tags, req.Text, req.IsRepeat); err != nil {
middleware.JSONError(c, http.StatusInternalServerError, "failed to submit review")
return
}
middleware.JSONResponse(c, http.StatusOK, gin.H{"message": "review submitted successfully"})
}
func (h *AuthHandler) GetReviewStats(c *gin.Context) {
stats, err := h.blindSvc.GetReviewStats()
if err != nil {
middleware.JSONError(c, http.StatusInternalServerError, "failed to get stats")
return
}
middleware.JSONResponse(c, http.StatusOK, stats)
}
// ============== Coupon ==============
func (h *AuthHandler) GetMyCoupons(c *gin.Context) {
userID, _ := c.Get("user_id")
uid := userID.(uint)
db := database.GetDB()
var coupons []model.Coupon
db.Where("user_id = ? AND status IN ?", uid, []string{"available", "claimed", "used"}).
Order("created_at DESC").Find(&coupons)
middleware.JSONResponse(c, http.StatusOK, coupons)
}
func (h *AuthHandler) GetAvailableCoupons(c *gin.Context) {
db := database.GetDB()
var coupons []model.Coupon
db.Where("status = ? AND remain_count > 0", "available").Limit(20).Find(&coupons)
middleware.JSONResponse(c, http.StatusOK, coupons)
}
func (h *AuthHandler) ClaimCoupon(c *gin.Context) {
userID, _ := c.Get("user_id")
uid := userID.(uint)
var req struct {
PoolCode string `json:"pool_code" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
middleware.JSONError(c, http.StatusBadRequest, err.Error())
return
}
coupon, err := h.couponSvc.ClaimCoupon(uid, req.PoolCode)
if err != nil {
middleware.JSONError(c, http.StatusBadRequest, "failed to claim coupon")
return
}
middleware.JSONResponse(c, http.StatusOK, gin.H{"message": "coupon claimed", "coupon": coupon})
}
// ============== Member ==============
func (h *AuthHandler) GetMemberStatus(c *gin.Context) {
userID, _ := c.Get("user_id")
uid := userID.(uint)
active, member := h.memberSvc.CheckMembership(uid)
var level int
if active {
level = 1
}
middleware.JSONResponse(c, http.StatusOK, gin.H{
"level": level,
"active": active,
"expires_at": member.EndDate,
"daily_limit": 10,
})
}
func dailyLimit(level int) int {
if level >= 1 {
return 10
}
return 3
}
func (h *AuthHandler) GetMemberPlans(c *gin.Context) {
middleware.JSONResponse(c, http.StatusOK, []gin.H{
{"id": 1, "name": "VIP月卡", "price": 29, "period": "monthly", "blindLimit": 10, "features": []string{"全部分类", "优先匹配", "月度报告"}},
{"id": 2, "name": "VIP年卡", "price": 199, "period": "yearly", "blindLimit": 10, "features": []string{"全部分类", "优先匹配", "月度报告", "省¥149"}},
})
}
func (h *AuthHandler) SubscribeMember(c *gin.Context) {
userID, _ := c.Get("user_id")
uid := userID.(uint)
var req struct {
PlanID uint `json:"plan_id" binding:"required"`
PaymentMethod string `json:"payment_method"`
}
if err := c.ShouldBindJSON(&req); err != nil {
middleware.JSONError(c, http.StatusBadRequest, err.Error())
return
}
member, err := h.memberSvc.Subscribe(uid, req.PlanID, req.PaymentMethod)
if err != nil {
middleware.JSONError(c, http.StatusInternalServerError, err.Error())
return
}
middleware.JSONResponse(c, http.StatusOK, gin.H{"message": "subscription successful", "member": member})
}
// ============== Admin CRUD ==============
func (h *AuthHandler) ListMerchants(c *gin.Context) {
db := database.GetDB()
var merchants []model.Merchant
db.Order("created_at DESC").Find(&merchants)
middleware.JSONResponse(c, http.StatusOK, merchants)
}
func (h *AuthHandler) CreateMerchant(c *gin.Context) {
var req model.Merchant
if err := c.ShouldBindJSON(&req); err != nil {
middleware.JSONError(c, http.StatusBadRequest, err.Error())
return
}
db := database.GetDB()
if req.Tags == "" {
req.Tags = "[]"
}
if req.Status == "" {
req.Status = "approved"
}
if req.Rating == 0 {
req.Rating = 4.0
}
if req.QualityScore == 0 {
req.QualityScore = 0.5
}
db.Create(&req)
middleware.JSONResponse(c, http.StatusCreated, req)
}
func (h *AuthHandler) UpdateMerchant(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
db := database.GetDB()
var req struct {
Name string `json:"name"`
CategoryID uint `json:"category_id"`
Rating float64 `json:"rating"`
PriceRange string `json:"price_range"`
Location string `json:"location"`
Lat float64 `json:"lat"`
Lng float64 `json:"lng"`
Tags string `json:"tags"`
QualityScore float64 `json:"quality_score"`
Status string `json:"status"`
}
if err := c.ShouldBindJSON(&req); err != nil {
middleware.JSONError(c, http.StatusBadRequest, err.Error())
return
}
updates := map[string]interface{}{}
if req.Name != "" {
updates["name"] = req.Name
}
if req.CategoryID > 0 {
updates["category_id"] = req.CategoryID
}
if req.Rating > 0 {
updates["rating"] = req.Rating
}
if req.PriceRange != "" {
updates["price_range"] = req.PriceRange
}
if req.Location != "" {
updates["location"] = req.Location
}
if req.Lat > 0 {
updates["lat"] = req.Lat
}
if req.Lng > 0 {
updates["lng"] = req.Lng
}
if req.Tags != "" {
updates["tags"] = req.Tags
}
if req.QualityScore > 0 {
updates["quality_score"] = req.QualityScore
}
if req.Status != "" {
updates["status"] = req.Status
}
db.Model(&model.Merchant{}).Where("id = ?", id).Updates(updates)
var m model.Merchant
db.First(&m, id)
middleware.JSONResponse(c, http.StatusOK, m)
}
func (h *AuthHandler) ListPackages(c *gin.Context) {
db := database.GetDB()
var packages []model.Package
db.Preload("Merchant").Order("created_at DESC").Find(&packages)
middleware.JSONResponse(c, http.StatusOK, packages)
}
func (h *AuthHandler) CreatePackage(c *gin.Context) {
db := database.GetDB()
var req model.Package
if err := c.ShouldBindJSON(&req); err != nil {
middleware.JSONError(c, http.StatusBadRequest, err.Error())
return
}
if req.Tags == "" {
req.Tags = "[]"
}
if req.Status == "" {
req.Status = "active"
}
if req.Weight == 0 {
req.Weight = 1.0
}
db.Create(&req)
middleware.JSONResponse(c, http.StatusCreated, req)
}
func (h *AuthHandler) UpdatePackage(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
db := database.GetDB()
var req struct {
Name string `json:"name"`
Description string `json:"description"`
PriceMin int `json:"price_min"`
PriceMax int `json:"price_max"`
ActualPrice int `json:"actual_price"`
Tags string `json:"tags"`
Stock int `json:"stock"`
Weight float64 `json:"weight"`
Status string `json:"status"`
CategoryID uint `json:"category_id"`
MerchantID uint `json:"merchant_id"`
}
if err := c.ShouldBindJSON(&req); err != nil {
middleware.JSONError(c, http.StatusBadRequest, err.Error())
return
}
updates := map[string]interface{}{}
if req.Name != "" {
updates["name"] = req.Name
}
if req.Description != "" {
updates["description"] = req.Description
}
if req.PriceMin > 0 {
updates["price_min"] = req.PriceMin
}
if req.PriceMax > 0 {
updates["price_max"] = req.PriceMax
}
if req.ActualPrice > 0 {
updates["actual_price"] = req.ActualPrice
}
if req.Tags != "" {
updates["tags"] = req.Tags
}
updates["stock"] = req.Stock
updates["weight"] = req.Weight
if req.Status != "" {
updates["status"] = req.Status
}
if req.CategoryID > 0 {
updates["category_id"] = req.CategoryID
}
if req.MerchantID > 0 {
updates["merchant_id"] = req.MerchantID
}
db.Model(&model.Package{}).Where("id = ?", id).Updates(updates)
var p model.Package
db.Preload("Merchant").First(&p, id)
middleware.JSONResponse(c, http.StatusOK, p)
}
func (h *AuthHandler) ListUsers(c *gin.Context) {
db := database.GetDB()
var users []model.User
db.Order("created_at DESC").Limit(50).Find(&users)
middleware.JSONResponse(c, http.StatusOK, users)
}
func (h *AuthHandler) GetUserDetail(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
db := database.GetDB()
var user model.User
db.First(&user, id)
if user.ID == 0 {
middleware.JSONError(c, http.StatusNotFound, "user not found")
return
}
// Get blind session count
var sessionCount int64
db.Model(&model.BlindSession{}).Where("user_id = ?", id).Count(&sessionCount)
// Get review count
var reviewCount int64
db.Model(&model.UserBehavior{}).Where("user_id = ? AND review_rating > 0", id).Count(&reviewCount)
middleware.JSONResponse(c, http.StatusOK, gin.H{
"user": user,
"session_count": sessionCount,
"review_count": reviewCount,
})
}
func (h *AuthHandler) ListReviews(c *gin.Context) {
db := database.GetDB()
type ReviewRow struct {
ID uint `json:"id"`
UserID uint `json:"user_id"`
PkgName string `json:"package_name"`
Merchant string `json:"merchant"`
Rating int8 `json:"rating"`
Taste int8 `json:"taste"`
Value int8 `json:"value"`
Distance int8 `json:"distance"`
Match int8 `json:"match"`
IsRepeat bool `json:"is_repeat"`
CreatedAt time.Time `json:"created_at"`
}
var reviews []model.UserBehavior
db.Where("review_rating > 0").Order("created_at DESC").Limit(50).Find(&reviews)
result := make([]ReviewRow, 0, len(reviews))
for _, r := range reviews {
var pkg model.Package
db.First(&pkg, r.PackageID)
var merchant model.Merchant
db.First(&merchant, pkg.MerchantID)
result = append(result, ReviewRow{
ID: r.ID, UserID: r.UserID, PkgName: pkg.Name,
Merchant: merchant.Name, Rating: r.ReviewRating,
Taste: r.TasteScore, Value: r.ValueScore,
Distance: r.DistanceScore, Match: r.MatchScore,
IsRepeat: r.IsRepeat, CreatedAt: r.CreatedAt,
})
}
middleware.JSONResponse(c, http.StatusOK, result)
}
func (h *AuthHandler) ListCoupons(c *gin.Context) {
db := database.GetDB()
var coupons []model.Coupon
db.Preload("Merchant").Order("created_at DESC").Limit(50).Find(&coupons)
middleware.JSONResponse(c, http.StatusOK, coupons)
}
func (h *AuthHandler) CreateCoupon(c *gin.Context) {
db := database.GetDB()
var req model.Coupon
if err := c.ShouldBindJSON(&req); err != nil {
middleware.JSONError(c, http.StatusBadRequest, err.Error())
return
}
if req.Status == "" {
req.Status = "available"
}
db.Create(&req)
middleware.JSONResponse(c, http.StatusCreated, req)
}
func (h *AuthHandler) GetStatistics(c *gin.Context) {
db := database.GetDB()
var userCount, activeUsers int64
db.Model(&model.User{}).Count(&userCount)
db.Model(&model.UserBehavior{}).Distinct("user_id").Where("created_at >= ?", time.Now().Add(-24*time.Hour)).Count(&activeUsers)
var blindCount, reviewCount int64
db.Model(&model.BlindSession{}).Count(&blindCount)
db.Model(&model.UserBehavior{}).Where("review_rating > 0").Count(&reviewCount)
var avgRating float64
db.Model(&model.UserBehavior{}).Where("review_rating > 0").Pluck("AVG(review_rating)", &avgRating)
var memberCount int64
db.Model(&model.User{}).Where("member_level >= 1").Count(&memberCount)
middleware.JSONResponse(c, http.StatusOK, gin.H{
"total_users": userCount,
"daily_active": activeUsers,
"total_blinds": blindCount,
"total_reviews": reviewCount,
"avg_rating": avgRating,
"member_count": memberCount,
})
}
func (h *AuthHandler) ExportData(c *gin.Context) {
middleware.JSONResponse(c, http.StatusOK, gin.H{"message": "export started", "format": "csv"})
}
// ============== Helpers ==============
func truncate(s string, n int) string {
if len(s) <= n {
return s
}
return s[:n]
}
func getCategoryName(db *gorm.DB, categoryID uint) string {
var cat model.Category
if err := db.First(&cat, categoryID).Error; err == nil {
return cat.Name
}
return ""
}
func generateUserCode(userID, couponID uint) string {
return "CPN" + strconv.FormatUint(uint64(userID), 10) + strconv.FormatUint(uint64(couponID), 10)
}