06488f0237
功能: - Go后端 (Gin + GORM + PostgreSQL) - UniApp用户端 (iOS/Android/小程序) - DaisyUI5后台管理 - JWT认证 + 微信登录 - 盲选加权算法 - 会员系统 + 优惠券 - 打分评价 + 偏好学习
831 lines
24 KiB
Go
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)
|
|
}
|