fix: 全面修复项目问题
后端修复: - 验证码存储与校验机制 - 订单创建事务+库存扣减 - GetStats字段名错误 - VerifyEmail改为POST - 供应商更新字段白名单 - 文件删除安全检查 - 抽奖安全随机数 - 用户管理CRUD - 订单取消/确认收货 - 工单回复 - Toggle返回新数据 - 移除死代码 前端修复: - 404兜底路由 - 401软跳转 - API层统一 - 面包屑补充banners - 国际化完善 - 购物车并行删除 - 退出清理购物车 - 供应商Dashboard数据 - 工单详情页 - 订单取消/确认收货
This commit is contained in:
Binary file not shown.
@@ -161,6 +161,7 @@ func (h *ArticleHandler) TogglePin(c *gin.Context) {
|
||||
}
|
||||
|
||||
utils.DB.Model(&article).Update("is_pinned", !article.IsPinned)
|
||||
utils.DB.First(&article, id)
|
||||
c.JSON(http.StatusOK, gin.H{"data": article})
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@ package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"sale/internal/models"
|
||||
@@ -17,6 +19,43 @@ func NewAuthHandler() *AuthHandler {
|
||||
return &AuthHandler{}
|
||||
}
|
||||
|
||||
var (
|
||||
verifyCodes = make(map[string]verifyCodeEntry)
|
||||
verifyCodesMux sync.Mutex
|
||||
)
|
||||
|
||||
type verifyCodeEntry struct {
|
||||
Code string
|
||||
ExpiresAt time.Time
|
||||
}
|
||||
|
||||
func storeVerifyCode(email, code string) {
|
||||
verifyCodesMux.Lock()
|
||||
defer verifyCodesMux.Unlock()
|
||||
verifyCodes[email] = verifyCodeEntry{
|
||||
Code: code,
|
||||
ExpiresAt: time.Now().Add(30 * time.Minute),
|
||||
}
|
||||
}
|
||||
|
||||
func checkVerifyCode(email, code string) bool {
|
||||
verifyCodesMux.Lock()
|
||||
defer verifyCodesMux.Unlock()
|
||||
entry, ok := verifyCodes[email]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if time.Now().After(entry.ExpiresAt) {
|
||||
delete(verifyCodes, email)
|
||||
return false
|
||||
}
|
||||
if entry.Code != code {
|
||||
return false
|
||||
}
|
||||
delete(verifyCodes, email)
|
||||
return true
|
||||
}
|
||||
|
||||
func (h *AuthHandler) Register(c *gin.Context) {
|
||||
var req schemas.RegisterRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
@@ -68,6 +107,7 @@ func (h *AuthHandler) Register(c *gin.Context) {
|
||||
}
|
||||
|
||||
verifyCode := utils.GenerateVerifyCode()
|
||||
storeVerifyCode(req.Email, verifyCode)
|
||||
utils.SendVerifyEmail(req.Email, verifyCode)
|
||||
|
||||
token, _ := utils.GenerateToken(user.ID, user.Role)
|
||||
@@ -137,6 +177,7 @@ func (h *AuthHandler) ForgotPassword(c *gin.Context) {
|
||||
}
|
||||
|
||||
code := utils.GenerateVerifyCode()
|
||||
storeVerifyCode(req.Email, code)
|
||||
utils.SendResetPasswordEmail(req.Email, code)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "If the email exists, a verification code has been sent"})
|
||||
@@ -149,6 +190,11 @@ func (h *AuthHandler) ResetPassword(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if !checkVerifyCode(req.Email, req.Code) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid or expired verification code"})
|
||||
return
|
||||
}
|
||||
|
||||
var user models.User
|
||||
if err := utils.DB.Where("email = ?", req.Email).First(&user).Error; err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid email"})
|
||||
@@ -172,6 +218,11 @@ func (h *AuthHandler) VerifyEmail(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if !checkVerifyCode(req.Email, req.Code) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid or expired verification code"})
|
||||
return
|
||||
}
|
||||
|
||||
var user models.User
|
||||
if err := utils.DB.Where("email = ?", req.Email).First(&user).Error; err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "User not found"})
|
||||
@@ -261,6 +312,7 @@ func (h *AuthHandler) SendVerifyCode(c *gin.Context) {
|
||||
}
|
||||
|
||||
code := utils.GenerateVerifyCode()
|
||||
storeVerifyCode(user.Email, code)
|
||||
utils.SendVerifyEmail(user.Email, code)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Verification code sent", "expires_at": time.Now().Add(30 * time.Minute)})
|
||||
@@ -320,3 +372,62 @@ func (h *AuthHandler) Install(c *gin.Context) {
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (h *AuthHandler) AdminGetUsers(c *gin.Context) {
|
||||
var users []models.User
|
||||
utils.DB.Select("id, username, email, role, purchase_credits, is_active, email_verified, created_at").Find(&users)
|
||||
c.JSON(http.StatusOK, gin.H{"data": users})
|
||||
}
|
||||
|
||||
func (h *AuthHandler) AdminUpdateUser(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
var user models.User
|
||||
if err := utils.DB.First(&user, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Username *string `json:"username"`
|
||||
Email *string `json:"email"`
|
||||
Role *string `json:"role"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
updates := make(map[string]interface{})
|
||||
if req.Username != nil {
|
||||
updates["username"] = *req.Username
|
||||
}
|
||||
if req.Email != nil {
|
||||
updates["email"] = *req.Email
|
||||
}
|
||||
if req.Role != nil {
|
||||
updates["role"] = *req.Role
|
||||
}
|
||||
if req.IsActive != nil {
|
||||
updates["is_active"] = *req.IsActive
|
||||
}
|
||||
|
||||
utils.DB.Model(&user).Updates(updates)
|
||||
utils.DB.First(&user, id)
|
||||
c.JSON(http.StatusOK, gin.H{"data": user})
|
||||
}
|
||||
|
||||
func (h *AuthHandler) AdminDeleteUser(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
var user models.User
|
||||
if err := utils.DB.First(&user, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
|
||||
return
|
||||
}
|
||||
if user.Role == "admin" {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Cannot delete admin user"})
|
||||
return
|
||||
}
|
||||
utils.DB.Delete(&user)
|
||||
c.JSON(http.StatusOK, gin.H{"message": "User deleted successfully"})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"sale/internal/models"
|
||||
"sale/internal/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type BannerHandler struct{}
|
||||
|
||||
func NewBannerHandler() *BannerHandler {
|
||||
return &BannerHandler{}
|
||||
}
|
||||
|
||||
func (h *BannerHandler) List(c *gin.Context) {
|
||||
var banners []models.Banner
|
||||
utils.DB.Where("is_active = ?", true).
|
||||
Order("sort_order ASC, created_at DESC").
|
||||
Find(&banners)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"data": banners})
|
||||
}
|
||||
|
||||
func (h *BannerHandler) AdminList(c *gin.Context) {
|
||||
var banners []models.Banner
|
||||
utils.DB.Order("sort_order ASC, created_at DESC").Find(&banners)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"data": banners})
|
||||
}
|
||||
|
||||
func (h *BannerHandler) Create(c *gin.Context) {
|
||||
var req struct {
|
||||
Title string `json:"title" binding:"required"`
|
||||
Desc string `json:"desc"`
|
||||
Image string `json:"image" binding:"required"`
|
||||
Link string `json:"link"`
|
||||
Button string `json:"button"`
|
||||
BgColor string `json:"bg_color"`
|
||||
SortOrder int `json:"sort_order"`
|
||||
IsActive bool `json:"is_active"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
banner := models.Banner{
|
||||
Title: req.Title,
|
||||
Desc: req.Desc,
|
||||
Image: req.Image,
|
||||
Link: req.Link,
|
||||
Button: req.Button,
|
||||
BgColor: req.BgColor,
|
||||
SortOrder: req.SortOrder,
|
||||
IsActive: req.IsActive,
|
||||
}
|
||||
|
||||
utils.DB.Create(&banner)
|
||||
c.JSON(http.StatusCreated, gin.H{"data": banner})
|
||||
}
|
||||
|
||||
func (h *BannerHandler) Update(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
var banner models.Banner
|
||||
if err := utils.DB.First(&banner, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Banner not found"})
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Title *string `json:"title"`
|
||||
Desc *string `json:"desc"`
|
||||
Image *string `json:"image"`
|
||||
Link *string `json:"link"`
|
||||
Button *string `json:"button"`
|
||||
BgColor *string `json:"bg_color"`
|
||||
SortOrder *int `json:"sort_order"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
updates := make(map[string]interface{})
|
||||
if req.Title != nil {
|
||||
updates["title"] = *req.Title
|
||||
}
|
||||
if req.Desc != nil {
|
||||
updates["desc"] = *req.Desc
|
||||
}
|
||||
if req.Image != nil {
|
||||
updates["image"] = *req.Image
|
||||
}
|
||||
if req.Link != nil {
|
||||
updates["link"] = *req.Link
|
||||
}
|
||||
if req.Button != nil {
|
||||
updates["button"] = *req.Button
|
||||
}
|
||||
if req.BgColor != nil {
|
||||
updates["bg_color"] = *req.BgColor
|
||||
}
|
||||
if req.SortOrder != nil {
|
||||
updates["sort_order"] = *req.SortOrder
|
||||
}
|
||||
if req.IsActive != nil {
|
||||
updates["is_active"] = *req.IsActive
|
||||
}
|
||||
|
||||
utils.DB.Model(&banner).Updates(updates)
|
||||
c.JSON(http.StatusOK, gin.H{"data": banner})
|
||||
}
|
||||
|
||||
func (h *BannerHandler) Delete(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
if err := utils.DB.Delete(&models.Banner{}, id).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete banner"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Banner deleted successfully"})
|
||||
}
|
||||
|
||||
func (h *BannerHandler) ToggleActive(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
var banner models.Banner
|
||||
if err := utils.DB.First(&banner, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Banner not found"})
|
||||
return
|
||||
}
|
||||
|
||||
utils.DB.Model(&banner).Update("is_active", !banner.IsActive)
|
||||
utils.DB.First(&banner, id)
|
||||
c.JSON(http.StatusOK, gin.H{"data": banner})
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"crypto/rand"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
@@ -193,7 +194,6 @@ func (h *LotteryHandler) Draw(c *gin.Context) {
|
||||
}
|
||||
|
||||
var winners []models.LotteryWinner
|
||||
rng := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
|
||||
for _, prize := range lottery.Prizes {
|
||||
remaining := prize.Quantity
|
||||
@@ -223,7 +223,10 @@ func (h *LotteryHandler) Draw(c *gin.Context) {
|
||||
|
||||
selected := make(map[uint]bool)
|
||||
for remaining > 0 && len(selected) < len(selectedParticipants) {
|
||||
r := rng.Intn(totalWeight)
|
||||
r, err := rand.Int(rand.Reader, big.NewInt(int64(totalWeight)))
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
cumWeight := 0
|
||||
for _, p := range selectedParticipants {
|
||||
if selected[p.UserID] {
|
||||
@@ -234,7 +237,7 @@ func (h *LotteryHandler) Draw(c *gin.Context) {
|
||||
w = 1
|
||||
}
|
||||
cumWeight += w
|
||||
if cumWeight > r {
|
||||
if cumWeight > int(r.Int64()) {
|
||||
winner := models.LotteryWinner{
|
||||
LotteryID: lottery.ID,
|
||||
PrizeID: prize.ID,
|
||||
|
||||
@@ -230,7 +230,9 @@ func (h *OrderHandler) Create(c *gin.Context) {
|
||||
PaymentMethod: req.PaymentMethod,
|
||||
}
|
||||
|
||||
if err := utils.DB.Create(&order).Error; err != nil {
|
||||
tx := utils.DB.Begin()
|
||||
if err := tx.Create(&order).Error; err != nil {
|
||||
tx.Rollback()
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create order"})
|
||||
return
|
||||
}
|
||||
@@ -238,20 +240,41 @@ func (h *OrderHandler) Create(c *gin.Context) {
|
||||
for i := range orderItems {
|
||||
orderItems[i].OrderID = order.ID
|
||||
}
|
||||
utils.DB.Create(&orderItems)
|
||||
if err := tx.Create(&orderItems).Error; err != nil {
|
||||
tx.Rollback()
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create order items"})
|
||||
return
|
||||
}
|
||||
|
||||
for _, cart := range carts {
|
||||
if cart.Product.RequireCredit {
|
||||
utils.DB.Model(&models.User{}).Where("id = ?", userID).
|
||||
UpdateColumn("purchase_credits", utils.DB.Raw("purchase_credits - ?", cart.Product.CreditCost*cart.Quantity))
|
||||
if err := tx.Model(&models.User{}).Where("id = ?", userID).
|
||||
UpdateColumn("purchase_credits", utils.DB.Raw("purchase_credits - ?", cart.Product.CreditCost*cart.Quantity)).Error; err != nil {
|
||||
tx.Rollback()
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to deduct credits"})
|
||||
return
|
||||
}
|
||||
}
|
||||
if cart.Product.CreditReward > 0 {
|
||||
utils.DB.Model(&models.User{}).Where("id = ?", userID).
|
||||
tx.Model(&models.User{}).Where("id = ?", userID).
|
||||
UpdateColumn("purchase_credits", utils.DB.Raw("purchase_credits + ?", cart.Product.CreditReward*cart.Quantity))
|
||||
}
|
||||
|
||||
var inventory models.Inventory
|
||||
if err := tx.Where("product_id = ?", cart.ProductID).First(&inventory).Error; err == nil {
|
||||
if inventory.Quantity >= cart.Quantity {
|
||||
tx.Model(&inventory).UpdateColumn("quantity", inventory.Quantity-cart.Quantity)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
utils.DB.Where("user_id = ?", userID).Delete(&models.Cart{})
|
||||
if err := tx.Where("user_id = ?", userID).Delete(&models.Cart{}).Error; err != nil {
|
||||
tx.Rollback()
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to clear cart"})
|
||||
return
|
||||
}
|
||||
|
||||
tx.Commit()
|
||||
|
||||
utils.DB.Preload("OrderItems.Product").Preload("ShippingAddress").First(&order, order.ID)
|
||||
c.JSON(http.StatusCreated, gin.H{"data": order})
|
||||
@@ -412,3 +435,41 @@ func (h *OrderHandler) Export(c *gin.Context) {
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"data": orders})
|
||||
}
|
||||
|
||||
func (h *OrderHandler) CancelOrder(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
|
||||
var order models.Order
|
||||
if err := utils.DB.Where("id = ? AND user_id = ?", id, userID).First(&order).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Order not found"})
|
||||
return
|
||||
}
|
||||
|
||||
if order.Status != models.OrderStatusPendingPayment && order.Status != models.OrderStatusPendingConfirm {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Order cannot be cancelled"})
|
||||
return
|
||||
}
|
||||
|
||||
utils.DB.Model(&order).Update("status", models.OrderStatusCancelled)
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Order cancelled successfully"})
|
||||
}
|
||||
|
||||
func (h *OrderHandler) ConfirmReceipt(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
|
||||
var order models.Order
|
||||
if err := utils.DB.Where("id = ? AND user_id = ?", id, userID).First(&order).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Order not found"})
|
||||
return
|
||||
}
|
||||
|
||||
if order.Status != models.OrderStatusShipped {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Order cannot be confirmed"})
|
||||
return
|
||||
}
|
||||
|
||||
utils.DB.Model(&order).Update("status", models.OrderStatusCompleted)
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Order confirmed successfully"})
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ func (h *SystemHandler) GetStats(c *gin.Context) {
|
||||
utils.DB.Model(&models.User{}).Count(&userCount)
|
||||
utils.DB.Model(&models.Product{}).Count(&productCount)
|
||||
utils.DB.Model(&models.Order{}).Count(&orderCount)
|
||||
utils.DB.Model(&models.Order{}).Where("status = ?", "completed").Select("COALESCE(SUM(total), 0)").Scan(&totalRevenue)
|
||||
utils.DB.Model(&models.Order{}).Where("status = ?", "completed").Select("COALESCE(SUM(total_amount), 0)").Scan(&totalRevenue)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"data": gin.H{
|
||||
@@ -173,13 +173,29 @@ func (h *SupplierHandler) Update(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
var updateData map[string]interface{}
|
||||
if err := c.ShouldBindJSON(&updateData); err != nil {
|
||||
var req struct {
|
||||
Username *string `json:"username"`
|
||||
Email *string `json:"email"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
utils.DB.Model(&supplier).Updates(updateData)
|
||||
updates := make(map[string]interface{})
|
||||
if req.Username != nil {
|
||||
updates["username"] = *req.Username
|
||||
}
|
||||
if req.Email != nil {
|
||||
updates["email"] = *req.Email
|
||||
}
|
||||
if req.IsActive != nil {
|
||||
updates["is_active"] = *req.IsActive
|
||||
}
|
||||
|
||||
utils.DB.Model(&supplier).Updates(updates)
|
||||
utils.DB.Where("id = ?", id).First(&supplier)
|
||||
c.JSON(http.StatusOK, gin.H{"data": supplier})
|
||||
}
|
||||
|
||||
|
||||
@@ -164,3 +164,36 @@ func (h *TicketHandler) Assign(c *gin.Context) {
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Ticket assigned successfully"})
|
||||
}
|
||||
|
||||
func (h *TicketHandler) Reply(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
|
||||
var ticket models.Ticket
|
||||
if err := utils.DB.Where("id = ? AND user_id = ?", id, userID).First(&ticket).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Ticket not found"})
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Content string `json:"content" binding:"required"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
reply := ticket.Reply
|
||||
if reply != "" {
|
||||
reply += "\n\n---\n\n"
|
||||
}
|
||||
reply += req.Content
|
||||
|
||||
utils.DB.Model(&ticket).Updates(map[string]interface{}{
|
||||
"reply": reply,
|
||||
"status": models.TicketStatusPending,
|
||||
})
|
||||
|
||||
utils.DB.First(&ticket, id)
|
||||
c.JSON(http.StatusOK, gin.H{"data": ticket})
|
||||
}
|
||||
|
||||
@@ -2,12 +2,10 @@ package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
@@ -104,35 +102,33 @@ func (h *UploadHandler) DeleteImage(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
filepath := filepath.Join("uploads/images", filename)
|
||||
if err := os.Remove(filepath); err != nil {
|
||||
if strings.Contains(filename, "..") || strings.Contains(filename, "/") || strings.Contains(filename, "\\") {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid filename"})
|
||||
return
|
||||
}
|
||||
|
||||
filePath := filepath.Join("uploads/images", filename)
|
||||
filePath, err := filepath.Abs(filePath)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid file path"})
|
||||
return
|
||||
}
|
||||
|
||||
absUploadDir, _ := filepath.Abs("uploads/images")
|
||||
if !strings.HasPrefix(filePath, absUploadDir) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"})
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "File not found"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := os.Remove(filePath); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete file"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "File deleted successfully"})
|
||||
}
|
||||
|
||||
func (h *UploadHandler) ExportOrders(c *gin.Context) {
|
||||
filename := fmt.Sprintf("orders_%s.csv", time.Now().Format("20060102150405"))
|
||||
filepath := filepath.Join("uploads", filename)
|
||||
|
||||
file, err := os.Create(filepath)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create file"})
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
c.Header("Content-Description", "File Transfer")
|
||||
c.Header("Content-Transfer-Encoding", "binary")
|
||||
c.Header("Content-Disposition", "attachment; filename="+filename)
|
||||
c.Header("Content-Type", "text/csv")
|
||||
|
||||
file.Seek(0, 0)
|
||||
_, err = io.Copy(c.Writer, file)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to send file"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,6 @@ import (
|
||||
|
||||
"sale/internal/api/handlers"
|
||||
"sale/internal/api/middlewares"
|
||||
"sale/internal/models"
|
||||
"sale/internal/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@@ -26,6 +24,7 @@ func SetupRoutes(r *gin.Engine) {
|
||||
inventoryHandler := handlers.NewInventoryHandler()
|
||||
articleHandler := handlers.NewArticleHandler()
|
||||
uploadHandler := handlers.NewUploadHandler()
|
||||
bannerHandler := handlers.NewBannerHandler()
|
||||
|
||||
r.Use(middlewares.CORSMiddleware())
|
||||
|
||||
@@ -40,7 +39,7 @@ func SetupRoutes(r *gin.Engine) {
|
||||
auth.POST("/login", authHandler.Login)
|
||||
auth.POST("/forgot-password", authHandler.ForgotPassword)
|
||||
auth.POST("/reset-password", authHandler.ResetPassword)
|
||||
auth.GET("/verify-email", authHandler.VerifyEmail)
|
||||
auth.POST("/verify-email", authHandler.VerifyEmail)
|
||||
}
|
||||
|
||||
articles := api.Group("/articles")
|
||||
@@ -51,6 +50,8 @@ func SetupRoutes(r *gin.Engine) {
|
||||
articles.POST("/crawl-ribenyan", articleHandler.CrawlRibenyan)
|
||||
}
|
||||
|
||||
api.GET("/banners", bannerHandler.List)
|
||||
|
||||
api.GET("/categories", categoryHandler.List)
|
||||
api.GET("/brands", brandHandler.List)
|
||||
api.GET("/products", productHandler.List)
|
||||
@@ -97,6 +98,8 @@ func SetupRoutes(r *gin.Engine) {
|
||||
orders.GET("/:id", orderHandler.GetByID)
|
||||
orders.POST("", orderHandler.Create)
|
||||
orders.POST("/:id/refund", orderHandler.Refund)
|
||||
orders.PUT("/:id/cancel", orderHandler.CancelOrder)
|
||||
orders.PUT("/:id/confirm-receipt", orderHandler.ConfirmReceipt)
|
||||
}
|
||||
|
||||
lotteries := authed.Group("/lotteries")
|
||||
@@ -109,6 +112,7 @@ func SetupRoutes(r *gin.Engine) {
|
||||
tickets.GET("", ticketHandler.List)
|
||||
tickets.POST("", ticketHandler.Create)
|
||||
tickets.GET("/:id", ticketHandler.GetByID)
|
||||
tickets.POST("/:id/reply", ticketHandler.Reply)
|
||||
}
|
||||
|
||||
supplier := authed.Group("/supplier")
|
||||
@@ -126,11 +130,9 @@ func SetupRoutes(r *gin.Engine) {
|
||||
admin.Use(middlewares.RoleMiddleware("admin"))
|
||||
{
|
||||
admin.GET("/stats", systemHandler.GetStats)
|
||||
admin.GET("/users", func(c *gin.Context) {
|
||||
var users []models.User
|
||||
utils.DB.Select("id, username, email, role, purchase_credits, is_active, created_at").Find(&users)
|
||||
c.JSON(200, gin.H{"data": users})
|
||||
})
|
||||
admin.GET("/users", authHandler.AdminGetUsers)
|
||||
admin.PUT("/users/:id", authHandler.AdminUpdateUser)
|
||||
admin.DELETE("/users/:id", authHandler.AdminDeleteUser)
|
||||
|
||||
adminCategories := admin.Group("/categories")
|
||||
{
|
||||
@@ -203,6 +205,15 @@ func SetupRoutes(r *gin.Engine) {
|
||||
adminArticles.DELETE("/:id", articleHandler.Delete)
|
||||
adminArticles.PUT("/:id/pin", articleHandler.TogglePin)
|
||||
}
|
||||
|
||||
adminBanners := admin.Group("/banners")
|
||||
{
|
||||
adminBanners.GET("", bannerHandler.AdminList)
|
||||
adminBanners.POST("", bannerHandler.Create)
|
||||
adminBanners.PUT("/:id", bannerHandler.Update)
|
||||
adminBanners.DELETE("/:id", bannerHandler.Delete)
|
||||
adminBanners.PUT("/:id/toggle", bannerHandler.ToggleActive)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Banner struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Title string `gorm:"size:100;not null" json:"title"`
|
||||
Desc string `gorm:"size:200" json:"desc"`
|
||||
Image string `gorm:"size:500;not null" json:"image"`
|
||||
Link string `gorm:"size:200" json:"link"`
|
||||
Button string `gorm:"size:50" json:"button"`
|
||||
BgColor string `gorm:"size:50" json:"bg_color"`
|
||||
SortOrder int `gorm:"default:0" json:"sort_order"`
|
||||
IsActive bool `gorm:"default:true" json:"is_active"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
}
|
||||
|
||||
func (Banner) TableName() string {
|
||||
return "banners"
|
||||
}
|
||||
@@ -10,6 +10,7 @@ type Lottery struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Name string `gorm:"size:100;not null" json:"name"`
|
||||
Description string `json:"description"`
|
||||
Image string `gorm:"size:500" json:"image"`
|
||||
StartTime time.Time `json:"start_time"`
|
||||
EndTime time.Time `json:"end_time"`
|
||||
Cycle string `gorm:"size:20" json:"cycle"`
|
||||
|
||||
@@ -70,6 +70,7 @@ func AutoMigrate() {
|
||||
&models.SystemSetting{},
|
||||
&models.SupplierAuthorization{},
|
||||
&models.Article{},
|
||||
&models.Banner{},
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to migrate database: %v", err)
|
||||
|
||||
Reference in New Issue
Block a user