Initial commit: 商品售卖网站

This commit is contained in:
2026-04-13 07:20:09 +08:00
commit c6154273f2
865 changed files with 26573 additions and 0 deletions
+124
View File
@@ -0,0 +1,124 @@
package handlers
import (
"net/http"
"strconv"
"sale/internal/models"
"sale/internal/schemas"
"sale/internal/utils"
"github.com/gin-gonic/gin"
)
type AddressHandler struct{}
func NewAddressHandler() *AddressHandler {
return &AddressHandler{}
}
func (h *AddressHandler) List(c *gin.Context) {
userID := c.GetUint("user_id")
var addresses []models.Address
utils.DB.Where("user_id = ?", userID).Order("is_default DESC, created_at DESC").Find(&addresses)
c.JSON(http.StatusOK, gin.H{"data": addresses})
}
func (h *AddressHandler) Create(c *gin.Context) {
userID := c.GetUint("user_id")
var req schemas.CreateAddressRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if req.IsDefault {
utils.DB.Model(&models.Address{}).Where("user_id = ?", userID).Update("is_default", false)
}
address := models.Address{
UserID: userID,
Name: req.Name,
Phone: req.Phone,
Province: req.Province,
City: req.City,
District: req.District,
Address: req.Address,
IsDefault: req.IsDefault,
}
utils.DB.Create(&address)
c.JSON(http.StatusCreated, gin.H{"data": address})
}
func (h *AddressHandler) Update(c *gin.Context) {
userID := c.GetUint("user_id")
id, _ := strconv.Atoi(c.Param("id"))
var address models.Address
if err := utils.DB.Where("id = ? AND user_id = ?", id, userID).First(&address).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Address not found"})
return
}
var req schemas.UpdateAddressRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
updates := make(map[string]interface{})
if req.Name != nil {
updates["name"] = *req.Name
}
if req.Phone != nil {
updates["phone"] = *req.Phone
}
if req.Province != nil {
updates["province"] = *req.Province
}
if req.City != nil {
updates["city"] = *req.City
}
if req.District != nil {
updates["district"] = *req.District
}
if req.Address != nil {
updates["address"] = *req.Address
}
if req.IsDefault != nil && *req.IsDefault {
utils.DB.Model(&models.Address{}).Where("user_id = ?", userID).Update("is_default", false)
updates["is_default"] = true
}
utils.DB.Model(&address).Updates(updates)
c.JSON(http.StatusOK, gin.H{"data": address})
}
func (h *AddressHandler) Delete(c *gin.Context) {
userID := c.GetUint("user_id")
id, _ := strconv.Atoi(c.Param("id"))
if err := utils.DB.Where("id = ? AND user_id = ?", id, userID).Delete(&models.Address{}).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete address"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Address deleted successfully"})
}
func (h *AddressHandler) SetDefault(c *gin.Context) {
userID := c.GetUint("user_id")
id, _ := strconv.Atoi(c.Param("id"))
var address models.Address
if err := utils.DB.Where("id = ? AND user_id = ?", id, userID).First(&address).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Address not found"})
return
}
utils.DB.Model(&models.Address{}).Where("user_id = ?", userID).Update("is_default", false)
utils.DB.Model(&address).Update("is_default", true)
c.JSON(http.StatusOK, gin.H{"message": "Default address set successfully"})
}
+164
View File
@@ -0,0 +1,164 @@
package handlers
import (
"math"
"net/http"
"strconv"
"sale/internal/models"
"sale/internal/schemas"
"sale/internal/utils"
"github.com/gin-gonic/gin"
)
type ArticleHandler struct{}
func NewArticleHandler() *ArticleHandler {
return &ArticleHandler{}
}
func (h *ArticleHandler) List(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
var total int64
utils.DB.Model(&models.Article{}).Where("is_published = ?", true).Count(&total)
var articles []models.Article
offset := (page - 1) * pageSize
utils.DB.Where("is_published = ?", true).
Order("is_pinned DESC, sort_order ASC, created_at DESC").
Offset(offset).Limit(pageSize).Find(&articles)
totalPages := int(math.Ceil(float64(total) / float64(pageSize)))
c.JSON(http.StatusOK, gin.H{
"data": articles,
"pagination": gin.H{
"page": page,
"page_size": pageSize,
"total": total,
"total_pages": totalPages,
},
})
}
func (h *ArticleHandler) GetByID(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var article models.Article
if err := utils.DB.Preload("Author").First(&article, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Article not found"})
return
}
c.JSON(http.StatusOK, gin.H{"data": article})
}
func (h *ArticleHandler) AdminList(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
var total int64
utils.DB.Model(&models.Article{}).Count(&total)
var articles []models.Article
offset := (page - 1) * pageSize
utils.DB.Preload("Author").
Order("is_pinned DESC, sort_order ASC, created_at DESC").
Offset(offset).Limit(pageSize).Find(&articles)
totalPages := int(math.Ceil(float64(total) / float64(pageSize)))
c.JSON(http.StatusOK, gin.H{
"data": articles,
"pagination": gin.H{
"page": page,
"page_size": pageSize,
"total": total,
"total_pages": totalPages,
},
})
}
func (h *ArticleHandler) Create(c *gin.Context) {
userID := c.GetUint("user_id")
var req schemas.CreateArticleRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
article := models.Article{
Title: req.Title,
Content: req.Content,
Summary: req.Summary,
CoverImage: req.CoverImage,
IsPinned: req.IsPinned,
IsPublished: req.IsPublished,
SortOrder: req.SortOrder,
AuthorID: &userID,
}
utils.DB.Create(&article)
c.JSON(http.StatusCreated, gin.H{"data": article})
}
func (h *ArticleHandler) Update(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var article models.Article
if err := utils.DB.First(&article, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Article not found"})
return
}
var req schemas.UpdateArticleRequest
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.Content != nil {
updates["content"] = *req.Content
}
if req.Summary != nil {
updates["summary"] = *req.Summary
}
if req.CoverImage != nil {
updates["cover_image"] = *req.CoverImage
}
if req.IsPinned != nil {
updates["is_pinned"] = *req.IsPinned
}
if req.IsPublished != nil {
updates["is_published"] = *req.IsPublished
}
if req.SortOrder != nil {
updates["sort_order"] = *req.SortOrder
}
utils.DB.Model(&article).Updates(updates)
c.JSON(http.StatusOK, gin.H{"data": article})
}
func (h *ArticleHandler) Delete(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
if err := utils.DB.Delete(&models.Article{}, id).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete article"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Article deleted successfully"})
}
func (h *ArticleHandler) TogglePin(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var article models.Article
if err := utils.DB.First(&article, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Article not found"})
return
}
utils.DB.Model(&article).Update("is_pinned", !article.IsPinned)
c.JSON(http.StatusOK, gin.H{"data": article})
}
+322
View File
@@ -0,0 +1,322 @@
package handlers
import (
"net/http"
"time"
"sale/internal/models"
"sale/internal/schemas"
"sale/internal/utils"
"github.com/gin-gonic/gin"
)
type AuthHandler struct{}
func NewAuthHandler() *AuthHandler {
return &AuthHandler{}
}
func (h *AuthHandler) Register(c *gin.Context) {
var req schemas.RegisterRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
var existingUser models.User
if err := utils.DB.Where("email = ?", req.Email).First(&existingUser).Error; err == nil {
c.JSON(http.StatusConflict, gin.H{"error": "Email already registered"})
return
}
if err := utils.DB.Where("username = ?", req.Username).First(&existingUser).Error; err == nil {
c.JSON(http.StatusConflict, gin.H{"error": "Username already taken"})
return
}
hashedPassword, err := utils.HashPassword(req.Password)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to hash password"})
return
}
user := models.User{
Username: req.Username,
Email: req.Email,
PasswordHash: hashedPassword,
Role: "user",
InviteCode: utils.GenerateInviteCode(),
IsActive: true,
}
if req.InviteCode != "" {
var referrer models.User
if err := utils.DB.Where("invite_code = ?", req.InviteCode).First(&referrer).Error; err == nil {
user.ReferredBy = &referrer.ID
}
}
if err := utils.DB.Create(&user).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create user"})
return
}
if user.ReferredBy != nil {
utils.DB.Model(&models.User{}).Where("id = ?", *user.ReferredBy).
UpdateColumn("purchase_credits", utils.DB.Raw("purchase_credits + 1"))
}
verifyCode := utils.GenerateVerifyCode()
utils.SendVerifyEmail(req.Email, verifyCode)
token, _ := utils.GenerateToken(user.ID, user.Role)
c.JSON(http.StatusCreated, gin.H{
"token": token,
"user": schemas.UserResponse{
ID: user.ID,
Username: user.Username,
Email: user.Email,
Role: user.Role,
PurchaseCredits: user.PurchaseCredits,
InviteCode: user.InviteCode,
EmailVerified: user.EmailVerified,
},
})
}
func (h *AuthHandler) Login(c *gin.Context) {
var req schemas.LoginRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
var user models.User
if err := utils.DB.Where("email = ?", req.Email).First(&user).Error; err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid email or password"})
return
}
if !utils.CheckPassword(req.Password, user.PasswordHash) {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid email or password"})
return
}
if !user.IsActive {
c.JSON(http.StatusForbidden, gin.H{"error": "Account is disabled"})
return
}
token, _ := utils.GenerateToken(user.ID, user.Role)
c.JSON(http.StatusOK, gin.H{
"token": token,
"user": schemas.UserResponse{
ID: user.ID,
Username: user.Username,
Email: user.Email,
Role: user.Role,
PurchaseCredits: user.PurchaseCredits,
InviteCode: user.InviteCode,
EmailVerified: user.EmailVerified,
},
})
}
func (h *AuthHandler) ForgotPassword(c *gin.Context) {
var req schemas.ForgotPasswordRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
var user models.User
if err := utils.DB.Where("email = ?", req.Email).First(&user).Error; err != nil {
c.JSON(http.StatusOK, gin.H{"message": "If the email exists, a verification code has been sent"})
return
}
code := utils.GenerateVerifyCode()
utils.SendResetPasswordEmail(req.Email, code)
c.JSON(http.StatusOK, gin.H{"message": "If the email exists, a verification code has been sent"})
}
func (h *AuthHandler) ResetPassword(c *gin.Context) {
var req schemas.ResetPasswordRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
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"})
return
}
hashedPassword, err := utils.HashPassword(req.Password)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to hash password"})
return
}
utils.DB.Model(&user).Update("password_hash", hashedPassword)
c.JSON(http.StatusOK, gin.H{"message": "Password reset successfully"})
}
func (h *AuthHandler) VerifyEmail(c *gin.Context) {
var req schemas.VerifyEmailRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
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"})
return
}
utils.DB.Model(&user).Update("email_verified", true)
c.JSON(http.StatusOK, gin.H{"message": "Email verified successfully"})
}
func (h *AuthHandler) GetProfile(c *gin.Context) {
userID := c.GetUint("user_id")
var user models.User
if err := utils.DB.First(&user, userID).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
return
}
c.JSON(http.StatusOK, schemas.UserResponse{
ID: user.ID,
Username: user.Username,
Email: user.Email,
Role: user.Role,
PurchaseCredits: user.PurchaseCredits,
InviteCode: user.InviteCode,
EmailVerified: user.EmailVerified,
})
}
func (h *AuthHandler) UpdateProfile(c *gin.Context) {
userID := c.GetUint("user_id")
var user models.User
if err := utils.DB.First(&user, userID).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
return
}
var updateData map[string]interface{}
if err := c.ShouldBindJSON(&updateData); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if username, ok := updateData["username"].(string); ok && username != "" {
user.Username = username
}
utils.DB.Save(&user)
c.JSON(http.StatusOK, gin.H{"message": "Profile updated successfully"})
}
func (h *AuthHandler) ChangePassword(c *gin.Context) {
userID := c.GetUint("user_id")
var req schemas.ChangePasswordRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
var user models.User
if err := utils.DB.First(&user, userID).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
return
}
if !utils.CheckPassword(req.OldPassword, user.PasswordHash) {
c.JSON(http.StatusBadRequest, gin.H{"error": "Old password is incorrect"})
return
}
hashedPassword, _ := utils.HashPassword(req.NewPassword)
utils.DB.Model(&user).Update("password_hash", hashedPassword)
c.JSON(http.StatusOK, gin.H{"message": "Password changed successfully"})
}
func (h *AuthHandler) SendVerifyCode(c *gin.Context) {
userID := c.GetUint("user_id")
var user models.User
if err := utils.DB.First(&user, userID).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
return
}
if user.EmailVerified {
c.JSON(http.StatusBadRequest, gin.H{"error": "Email already verified"})
return
}
code := utils.GenerateVerifyCode()
utils.SendVerifyEmail(user.Email, code)
c.JSON(http.StatusOK, gin.H{"message": "Verification code sent", "expires_at": time.Now().Add(30 * time.Minute)})
}
func (h *AuthHandler) CheckInstalled(c *gin.Context) {
var count int64
utils.DB.Model(&models.User{}).Where("role = ?", "admin").Count(&count)
c.JSON(http.StatusOK, gin.H{"installed": count > 0})
}
func (h *AuthHandler) Install(c *gin.Context) {
var count int64
utils.DB.Model(&models.User{}).Where("role = ?", "admin").Count(&count)
if count > 0 {
c.JSON(http.StatusForbidden, gin.H{"error": "System already installed"})
return
}
var req schemas.InstallRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
hashedPassword, err := utils.HashPassword(req.Password)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to hash password"})
return
}
admin := models.User{
Username: req.Username,
Email: req.Email,
PasswordHash: hashedPassword,
Role: "admin",
InviteCode: utils.GenerateInviteCode(),
IsActive: true,
}
if err := utils.DB.Create(&admin).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create admin user"})
return
}
token, _ := utils.GenerateToken(admin.ID, admin.Role)
c.JSON(http.StatusCreated, gin.H{
"token": token,
"user": schemas.UserResponse{
ID: admin.ID,
Username: admin.Username,
Email: admin.Email,
Role: admin.Role,
PurchaseCredits: admin.PurchaseCredits,
InviteCode: admin.InviteCode,
EmailVerified: admin.EmailVerified,
},
})
}
+53
View File
@@ -0,0 +1,53 @@
package handlers
import (
"net/http"
"sale/internal/models"
"sale/internal/utils"
"github.com/gin-gonic/gin"
)
type BrandHandler struct{}
func NewBrandHandler() *BrandHandler {
return &BrandHandler{}
}
func (h *BrandHandler) List(c *gin.Context) {
var brands []models.Brand
utils.DB.Where("deleted_at IS NULL").Order("sort_order ASC, name ASC").Find(&brands)
c.JSON(http.StatusOK, gin.H{"data": brands})
}
func (h *BrandHandler) Create(c *gin.Context) {
var brand models.Brand
if err := c.ShouldBindJSON(&brand); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
utils.DB.Create(&brand)
c.JSON(http.StatusOK, brand)
}
func (h *BrandHandler) Update(c *gin.Context) {
id := c.Param("id")
var brand models.Brand
if err := utils.DB.First(&brand, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Brand not found"})
return
}
var input models.Brand
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
utils.DB.Model(&brand).Updates(input)
c.JSON(http.StatusOK, brand)
}
func (h *BrandHandler) Delete(c *gin.Context) {
id := c.Param("id")
utils.DB.Delete(&models.Brand{}, id)
c.JSON(http.StatusOK, gin.H{"message": "Brand deleted"})
}
+263
View File
@@ -0,0 +1,263 @@
package handlers
import (
"math/rand"
"net/http"
"strconv"
"time"
"sale/internal/models"
"sale/internal/schemas"
"sale/internal/utils"
"github.com/gin-gonic/gin"
)
type LotteryHandler struct{}
func NewLotteryHandler() *LotteryHandler {
return &LotteryHandler{}
}
func (h *LotteryHandler) List(c *gin.Context) {
var lotteries []models.Lottery
utils.DB.Where("is_active = ?", true).Preload("Prizes").Order("created_at DESC").Find(&lotteries)
c.JSON(http.StatusOK, gin.H{"data": lotteries})
}
func (h *LotteryHandler) GetByID(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var lottery models.Lottery
if err := utils.DB.Preload("Prizes").First(&lottery, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Lottery not found"})
return
}
c.JSON(http.StatusOK, gin.H{"data": lottery})
}
func (h *LotteryHandler) Register(c *gin.Context) {
userID := c.GetUint("user_id")
id, _ := strconv.Atoi(c.Param("id"))
var lottery models.Lottery
if err := utils.DB.First(&lottery, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Lottery not found"})
return
}
now := time.Now()
if now.Before(lottery.StartTime) || now.After(lottery.EndTime) {
c.JSON(http.StatusBadRequest, gin.H{"error": "Lottery is not in registration period"})
return
}
var existing models.LotteryParticipant
if err := utils.DB.Where("lottery_id = ? AND user_id = ?", id, userID).First(&existing).Error; err == nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Already registered"})
return
}
var totalSpent float64
utils.DB.Model(&models.Order{}).
Where("user_id = ? AND status = ?", userID, models.OrderStatusCompleted).
Select("COALESCE(SUM(total_amount), 0)").
Scan(&totalSpent)
participant := models.LotteryParticipant{
LotteryID: lottery.ID,
UserID: userID,
PurchaseWeight: int(totalSpent),
}
utils.DB.Create(&participant)
c.JSON(http.StatusOK, gin.H{"message": "Registered successfully"})
}
func (h *LotteryHandler) Create(c *gin.Context) {
var req schemas.CreateLotteryRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
startTime, _ := time.Parse(time.RFC3339, req.StartTime)
endTime, _ := time.Parse(time.RFC3339, req.EndTime)
lottery := models.Lottery{
Name: req.Name,
Description: req.Description,
StartTime: startTime,
EndTime: endTime,
Cycle: req.Cycle,
DailyQuota: req.DailyQuota,
TotalQuota: req.TotalQuota,
RegistrationValidity: req.RegistrationValidity,
IsActive: true,
}
utils.DB.Create(&lottery)
c.JSON(http.StatusCreated, gin.H{"data": lottery})
}
func (h *LotteryHandler) Update(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var lottery models.Lottery
if err := utils.DB.First(&lottery, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Lottery not found"})
return
}
var req schemas.UpdateLotteryRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
updates := make(map[string]interface{})
if req.Name != nil {
updates["name"] = *req.Name
}
if req.Description != nil {
updates["description"] = *req.Description
}
if req.StartTime != nil {
t, _ := time.Parse(time.RFC3339, *req.StartTime)
updates["start_time"] = t
}
if req.EndTime != nil {
t, _ := time.Parse(time.RFC3339, *req.EndTime)
updates["end_time"] = t
}
if req.Cycle != nil {
updates["cycle"] = *req.Cycle
}
if req.DailyQuota != nil {
updates["daily_quota"] = *req.DailyQuota
}
if req.TotalQuota != nil {
updates["total_quota"] = *req.TotalQuota
}
if req.RegistrationValidity != nil {
updates["registration_validity"] = *req.RegistrationValidity
}
if req.IsActive != nil {
updates["is_active"] = *req.IsActive
}
utils.DB.Model(&lottery).Updates(updates)
c.JSON(http.StatusOK, gin.H{"data": lottery})
}
func (h *LotteryHandler) Delete(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
utils.DB.Delete(&models.Lottery{}, id)
c.JSON(http.StatusOK, gin.H{"message": "Lottery deleted successfully"})
}
func (h *LotteryHandler) AddPrize(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var req schemas.AddLotteryPrizeRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
prize := models.LotteryPrize{
LotteryID: uint(id),
Name: req.Name,
Type: req.Type,
Quantity: req.Quantity,
Weight: req.Weight,
CreditReward: req.CreditReward,
DrawMode: req.DrawMode,
}
utils.DB.Create(&prize)
c.JSON(http.StatusCreated, gin.H{"data": prize})
}
func (h *LotteryHandler) Draw(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var lottery models.Lottery
if err := utils.DB.Preload("Prizes").First(&lottery, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Lottery not found"})
return
}
var participants []models.LotteryParticipant
utils.DB.Where("lottery_id = ?", id).Find(&participants)
if len(participants) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "No participants"})
return
}
var winners []models.LotteryWinner
rng := rand.New(rand.NewSource(time.Now().UnixNano()))
for _, prize := range lottery.Prizes {
remaining := prize.Quantity
var weightParticipants []models.LotteryParticipant
var randomParticipants []models.LotteryParticipant
if prize.DrawMode == models.DrawModeWeight {
weightParticipants = participants
} else {
randomParticipants = participants
}
selectedParticipants := weightParticipants
if len(selectedParticipants) == 0 {
selectedParticipants = randomParticipants
}
totalWeight := 0
for _, p := range selectedParticipants {
w := p.PurchaseWeight
if w < 1 {
w = 1
}
totalWeight += w
}
selected := make(map[uint]bool)
for remaining > 0 && len(selected) < len(selectedParticipants) {
r := rng.Intn(totalWeight)
cumWeight := 0
for _, p := range selectedParticipants {
if selected[p.UserID] {
continue
}
w := p.PurchaseWeight
if w < 1 {
w = 1
}
cumWeight += w
if cumWeight > r {
winner := models.LotteryWinner{
LotteryID: lottery.ID,
PrizeID: prize.ID,
UserID: p.UserID,
DrawnAt: time.Now(),
}
winners = append(winners, winner)
selected[p.UserID] = true
remaining--
if prize.Type == models.PrizeTypeCredit && prize.CreditReward != nil {
utils.DB.Model(&models.User{}).Where("id = ?", p.UserID).
UpdateColumn("purchase_credits", utils.DB.Raw("purchase_credits + ?", *prize.CreditReward))
}
break
}
}
}
}
if len(winners) > 0 {
utils.DB.Create(&winners)
}
c.JSON(http.StatusOK, gin.H{"data": winners, "message": "Draw completed successfully"})
}
+414
View File
@@ -0,0 +1,414 @@
package handlers
import (
"math"
"net/http"
"strconv"
"sale/internal/models"
"sale/internal/schemas"
"sale/internal/utils"
"github.com/gin-gonic/gin"
)
type CartHandler struct{}
func NewCartHandler() *CartHandler {
return &CartHandler{}
}
func (h *CartHandler) List(c *gin.Context) {
userID := c.GetUint("user_id")
var carts []models.Cart
utils.DB.Where("user_id = ?", userID).Preload("Product").Find(&carts)
c.JSON(http.StatusOK, gin.H{"data": carts})
}
func (h *CartHandler) Add(c *gin.Context) {
userID := c.GetUint("user_id")
var req schemas.AddToCartRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
var cart models.Cart
result := utils.DB.Where("user_id = ? AND product_id = ?", userID, req.ProductID).First(&cart)
if result.Error == nil {
cart.Quantity += req.Quantity
utils.DB.Save(&cart)
} else {
cart = models.Cart{
UserID: userID,
ProductID: req.ProductID,
Quantity: req.Quantity,
}
utils.DB.Create(&cart)
}
utils.DB.Preload("Product").First(&cart, cart.ID)
c.JSON(http.StatusOK, gin.H{"data": cart})
}
func (h *CartHandler) Update(c *gin.Context) {
userID := c.GetUint("user_id")
id, _ := strconv.Atoi(c.Param("id"))
var cart models.Cart
if err := utils.DB.Where("id = ? AND user_id = ?", id, userID).First(&cart).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Cart item not found"})
return
}
var req schemas.UpdateCartRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
cart.Quantity = req.Quantity
utils.DB.Save(&cart)
utils.DB.Preload("Product").First(&cart, cart.ID)
c.JSON(http.StatusOK, gin.H{"data": cart})
}
func (h *CartHandler) Delete(c *gin.Context) {
userID := c.GetUint("user_id")
id, _ := strconv.Atoi(c.Param("id"))
if err := utils.DB.Where("id = ? AND user_id = ?", id, userID).Delete(&models.Cart{}).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete cart item"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Cart item deleted successfully"})
}
type OrderHandler struct{}
func NewOrderHandler() *OrderHandler {
return &OrderHandler{}
}
func (h *OrderHandler) List(c *gin.Context) {
userID := c.GetUint("user_id")
role, _ := c.Get("role")
var orders []models.Order
query := utils.DB.Model(&models.Order{})
if role == "user" {
query = query.Where("user_id = ?", userID)
} else if role == "supplier" {
query = query.Where("supplier_id = ?", userID)
}
query.Preload("OrderItems.Product").Preload("ShippingAddress").
Order("created_at DESC").Find(&orders)
c.JSON(http.StatusOK, gin.H{"data": orders})
}
func (h *OrderHandler) GetByID(c *gin.Context) {
userID := c.GetUint("user_id")
role, _ := c.Get("role")
id, _ := strconv.Atoi(c.Param("id"))
var order models.Order
query := utils.DB.Preload("OrderItems.Product").Preload("ShippingAddress").Preload("User")
if role == "user" {
query = query.Where("user_id = ?", userID)
} else if role == "supplier" {
query = query.Where("supplier_id = ?", userID)
}
if err := query.First(&order, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Order not found"})
return
}
c.JSON(http.StatusOK, gin.H{"data": order})
}
func (h *OrderHandler) Create(c *gin.Context) {
userID := c.GetUint("user_id")
var req schemas.CreateOrderRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
var carts []models.Cart
utils.DB.Where("user_id = ?", userID).Preload("Product").Find(&carts)
if len(carts) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "Cart is empty"})
return
}
var subtotal float64
var totalQuantity int
var orderItems []models.OrderItem
supplierMap := make(map[uint]bool)
for _, cart := range carts {
if cart.Product.RequireCredit {
var user models.User
utils.DB.First(&user, userID)
if user.PurchaseCredits < cart.Product.CreditCost*cart.Quantity {
c.JSON(http.StatusBadRequest, gin.H{"error": "Insufficient purchase credits for " + cart.Product.Name})
return
}
}
var inventory models.Inventory
if err := utils.DB.Where("product_id = ?", cart.ProductID).First(&inventory).Error; err == nil {
if inventory.Quantity < cart.Quantity {
c.JSON(http.StatusBadRequest, gin.H{"error": "Insufficient stock for " + cart.Product.Name})
return
}
}
subtotal += cart.Product.Price * float64(cart.Quantity)
totalQuantity += cart.Quantity
orderItems = append(orderItems, models.OrderItem{
ProductID: cart.ProductID,
Quantity: cart.Quantity,
Price: cart.Product.Price,
})
if inventory.SupplierID != 0 {
supplierMap[inventory.SupplierID] = true
}
}
var supplierID *uint
for sid := range supplierMap {
sid := sid
supplierID = &sid
break
}
var shippingFeeFirstWeight, shippingFeePerGram, serviceFeeRate, taxRate float64
var setting models.SystemSetting
if err := utils.DB.Where("`key` = ?", "shipping_fee_first_weight").First(&setting).Error; err == nil {
shippingFeeFirstWeight, _ = strconv.ParseFloat(setting.Value, 64)
}
if err := utils.DB.Where("`key` = ?", "shipping_fee_per_gram").First(&setting).Error; err == nil {
shippingFeePerGram, _ = strconv.ParseFloat(setting.Value, 64)
}
if err := utils.DB.Where("`key` = ?", "service_fee_rate").First(&setting).Error; err == nil {
serviceFeeRate, _ = strconv.ParseFloat(setting.Value, 64)
}
if err := utils.DB.Where("`key` = ?", "tax_rate").First(&setting).Error; err == nil {
taxRate, _ = strconv.ParseFloat(setting.Value, 64)
}
shippingFee := shippingFeeFirstWeight
if subtotal >= 99 || shippingFeeFirstWeight == 0 {
shippingFee = 0
} else if totalQuantity > 500 {
shippingFee += shippingFeePerGram * float64(totalQuantity-500)
}
serviceFee := subtotal * serviceFeeRate / 100
tax := subtotal * taxRate / 100
totalAmount := subtotal + shippingFee + serviceFee + tax
order := models.Order{
UserID: userID,
SupplierID: supplierID,
Subtotal: subtotal,
ShippingFee: shippingFee,
ServiceFee: serviceFee,
Tax: tax,
TotalAmount: totalAmount,
Status: models.OrderStatusPendingPayment,
ShippingAddressID: &req.ShippingAddressID,
PaymentMethod: req.PaymentMethod,
}
if err := utils.DB.Create(&order).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create order"})
return
}
for i := range orderItems {
orderItems[i].OrderID = order.ID
}
utils.DB.Create(&orderItems)
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 cart.Product.CreditReward > 0 {
utils.DB.Model(&models.User{}).Where("id = ?", userID).
UpdateColumn("purchase_credits", utils.DB.Raw("purchase_credits + ?", cart.Product.CreditReward*cart.Quantity))
}
}
utils.DB.Where("user_id = ?", userID).Delete(&models.Cart{})
utils.DB.Preload("OrderItems.Product").Preload("ShippingAddress").First(&order, order.ID)
c.JSON(http.StatusCreated, gin.H{"data": order})
}
func (h *OrderHandler) Refund(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 || order.Status == models.OrderStatusCompleted {
c.JSON(http.StatusBadRequest, gin.H{"error": "Cannot refund a shipped or completed order"})
return
}
if order.RefundStatus == models.RefundStatusPending {
c.JSON(http.StatusBadRequest, gin.H{"error": "Refund already requested"})
return
}
var req schemas.RefundRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
utils.DB.Model(&order).Updates(map[string]interface{}{
"refund_status": models.RefundStatusPending,
"refund_reason": req.Reason,
"status": models.OrderStatusRefunding,
})
c.JSON(http.StatusOK, gin.H{"message": "Refund request submitted successfully"})
}
func (h *OrderHandler) ConfirmOrder(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 supplier_id = ?", id, userID).First(&order).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Order not found"})
return
}
if order.Status != models.OrderStatusPendingConfirm {
c.JSON(http.StatusBadRequest, gin.H{"error": "Order cannot be confirmed"})
return
}
utils.DB.Model(&order).Update("status", models.OrderStatusPendingShip)
c.JSON(http.StatusOK, gin.H{"message": "Order confirmed successfully"})
}
func (h *OrderHandler) ShipOrder(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 supplier_id = ?", id, userID).First(&order).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Order not found"})
return
}
if order.Status != models.OrderStatusPendingShip {
c.JSON(http.StatusBadRequest, gin.H{"error": "Order cannot be shipped"})
return
}
var req schemas.ShipOrderRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
utils.DB.Model(&order).Updates(map[string]interface{}{
"status": models.OrderStatusShipped,
"tracking_number": req.TrackingNumber,
"shipping_photo": req.ShippingPhoto,
"express_photo": req.ExpressPhoto,
"customs_photo": req.CustomsPhoto,
})
c.JSON(http.StatusOK, gin.H{"message": "Order shipped successfully"})
}
func (h *OrderHandler) AdminList(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
var total int64
utils.DB.Model(&models.Order{}).Count(&total)
var orders []models.Order
offset := (page - 1) * pageSize
utils.DB.Preload("OrderItems.Product").Preload("ShippingAddress").Preload("User").
Order("created_at DESC").Offset(offset).Limit(pageSize).Find(&orders)
totalPages := int(math.Ceil(float64(total) / float64(pageSize)))
c.JSON(http.StatusOK, gin.H{
"data": orders,
"pagination": gin.H{
"page": page,
"page_size": pageSize,
"total": total,
"total_pages": totalPages,
},
})
}
func (h *OrderHandler) ProcessRefund(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var order models.Order
if err := utils.DB.First(&order, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Order not found"})
return
}
var req schemas.ProcessRefundRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if req.Status == "approved" {
var setting models.SystemSetting
feeRate := 0.0
if err := utils.DB.Where("`key` = ?", "payment_channel_fee_rate").First(&setting).Error; err == nil {
feeRate, _ = strconv.ParseFloat(setting.Value, 64)
}
refundAmount := order.TotalAmount * (1 - feeRate/100)
utils.DB.Model(&order).Updates(map[string]interface{}{
"refund_status": models.RefundStatusCompleted,
"refund_amount": refundAmount,
"status": models.OrderStatusRefunded,
})
} else {
utils.DB.Model(&order).Updates(map[string]interface{}{
"refund_status": models.RefundStatusRejected,
"status": models.OrderStatusPendingConfirm,
})
}
c.JSON(http.StatusOK, gin.H{"message": "Refund processed successfully"})
}
func (h *OrderHandler) Export(c *gin.Context) {
var orders []models.Order
utils.DB.Preload("OrderItems.Product").Preload("ShippingAddress").Preload("User").
Order("created_at DESC").Find(&orders)
c.JSON(http.StatusOK, gin.H{"data": orders})
}
+356
View File
@@ -0,0 +1,356 @@
package handlers
import (
"math"
"net/http"
"strconv"
"sale/internal/models"
"sale/internal/schemas"
"sale/internal/utils"
"github.com/gin-gonic/gin"
)
type CategoryHandler struct{}
func NewCategoryHandler() *CategoryHandler {
return &CategoryHandler{}
}
func (h *CategoryHandler) List(c *gin.Context) {
var categories []models.Category
utils.DB.Where("parent_id IS NULL").Order("sort_order ASC, id ASC").Find(&categories)
for i := range categories {
utils.DB.Where("parent_id = ?", categories[i].ID).Order("sort_order ASC, id ASC").Find(&categories[i].Children)
}
c.JSON(http.StatusOK, gin.H{"data": categories})
}
func (h *CategoryHandler) Create(c *gin.Context) {
var req schemas.CreateCategoryRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
category := models.Category{
Name: req.Name,
Description: req.Description,
ParentID: req.ParentID,
MinAmount: req.MinAmount,
MaxAmount: req.MaxAmount,
MinQuantity: req.MinQuantity,
MaxQuantity: req.MaxQuantity,
MinWeight: req.MinWeight,
MaxWeight: req.MaxWeight,
SortOrder: req.SortOrder,
}
if err := utils.DB.Create(&category).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create category"})
return
}
c.JSON(http.StatusCreated, gin.H{"data": category})
}
func (h *CategoryHandler) Update(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var category models.Category
if err := utils.DB.First(&category, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Category not found"})
return
}
var req schemas.UpdateCategoryRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
updates := make(map[string]interface{})
if req.Name != nil {
updates["name"] = *req.Name
}
if req.Description != nil {
updates["description"] = *req.Description
}
if req.ParentID != nil {
updates["parent_id"] = *req.ParentID
}
if req.MinAmount != nil {
updates["min_amount"] = *req.MinAmount
}
if req.MaxAmount != nil {
updates["max_amount"] = *req.MaxAmount
}
if req.MinQuantity != nil {
updates["min_quantity"] = *req.MinQuantity
}
if req.MaxQuantity != nil {
updates["max_quantity"] = *req.MaxQuantity
}
if req.MinWeight != nil {
updates["min_weight"] = *req.MinWeight
}
if req.MaxWeight != nil {
updates["max_weight"] = *req.MaxWeight
}
if req.SortOrder != nil {
updates["sort_order"] = *req.SortOrder
}
utils.DB.Model(&category).Updates(updates)
c.JSON(http.StatusOK, gin.H{"data": category})
}
func (h *CategoryHandler) Delete(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
if err := utils.DB.Delete(&models.Category{}, id).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete category"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Category deleted successfully"})
}
type ProductHandler struct{}
func NewProductHandler() *ProductHandler {
return &ProductHandler{}
}
func (h *ProductHandler) List(c *gin.Context) {
var req schemas.ProductListRequest
if err := c.ShouldBindQuery(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
query := utils.DB.Model(&models.Product{}).Where("is_active = ?", true)
if req.CategoryID != nil {
query = query.Joins("JOIN product_categories ON product_categories.product_id = products.id").
Where("product_categories.category_id = ?", *req.CategoryID)
}
if req.BrandID != nil {
query = query.Where("brand_id = ?", *req.BrandID)
}
if req.Keyword != "" {
query = query.Where("name ILIKE ?", "%"+req.Keyword+"%")
}
if req.MinPrice != nil {
query = query.Where("price >= ?", *req.MinPrice)
}
if req.MaxPrice != nil {
query = query.Where("price <= ?", *req.MaxPrice)
}
var total int64
query.Count(&total)
var products []models.Product
offset := (req.Page - 1) * req.PageSize
query.Preload("Categories").Preload("CustomFields").Preload("Brand").
Order("created_at DESC").
Offset(offset).Limit(req.PageSize).
Find(&products)
totalPages := int(math.Ceil(float64(total) / float64(req.PageSize)))
c.JSON(http.StatusOK, gin.H{
"data": products,
"pagination": gin.H{
"page": req.Page,
"page_size": req.PageSize,
"total": total,
"total_pages": totalPages,
},
})
}
func (h *ProductHandler) GetByID(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var product models.Product
if err := utils.DB.Preload("Categories").Preload("CustomFields").First(&product, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Product not found"})
return
}
c.JSON(http.StatusOK, gin.H{"data": product})
}
func (h *ProductHandler) AdminList(c *gin.Context) {
var products []models.Product
utils.DB.Preload("Categories").Preload("Brand").Order("created_at DESC").Find(&products)
c.JSON(http.StatusOK, gin.H{"data": products})
}
func (h *ProductHandler) Create(c *gin.Context) {
var req schemas.CreateProductRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
product := models.Product{
Name: req.Name,
Description: req.Description,
Price: req.Price,
MinPurchase: req.MinPurchase,
MaxPurchase: req.MaxPurchase,
MinWeight: req.MinWeight,
MaxWeight: req.MaxWeight,
MinAmount: req.MinAmount,
MaxAmount: req.MaxAmount,
RequireCredit: req.RequireCredit,
CreditCost: req.CreditCost,
CreditReward: req.CreditReward,
Images: req.Images,
BrandID: req.BrandID,
IsActive: true,
}
if err := utils.DB.Create(&product).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create product"})
return
}
if len(req.CategoryIDs) > 0 {
var categories []models.Category
utils.DB.Where("id IN ?", req.CategoryIDs).Find(&categories)
utils.DB.Model(&product).Association("Categories").Replace(categories)
}
if len(req.CustomFields) > 0 {
for _, cf := range req.CustomFields {
utils.DB.Create(&models.ProductCustomField{
ProductID: product.ID,
FieldName: cf.FieldName,
FieldValue: cf.FieldValue,
})
}
}
utils.DB.Preload("Categories").Preload("CustomFields").Preload("Brand").First(&product, product.ID)
c.JSON(http.StatusCreated, gin.H{"data": product})
}
func (h *ProductHandler) Update(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var product models.Product
if err := utils.DB.First(&product, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Product not found"})
return
}
var req schemas.UpdateProductRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
updates := make(map[string]interface{})
if req.Name != nil {
updates["name"] = *req.Name
}
if req.Description != nil {
updates["description"] = *req.Description
}
if req.Price != nil {
updates["price"] = *req.Price
}
if req.MinPurchase != nil {
updates["min_purchase"] = *req.MinPurchase
}
if req.MaxPurchase != nil {
updates["max_purchase"] = *req.MaxPurchase
}
if req.MinWeight != nil {
updates["min_weight"] = *req.MinWeight
}
if req.MaxWeight != nil {
updates["max_weight"] = *req.MaxWeight
}
if req.MinAmount != nil {
updates["min_amount"] = *req.MinAmount
}
if req.MaxAmount != nil {
updates["max_amount"] = *req.MaxAmount
}
if req.RequireCredit != nil {
updates["require_credit"] = *req.RequireCredit
}
if req.CreditCost != nil {
updates["credit_cost"] = *req.CreditCost
}
if req.CreditReward != nil {
updates["credit_reward"] = *req.CreditReward
}
if req.Images != nil {
updates["images"] = *req.Images
}
if req.IsActive != nil {
updates["is_active"] = *req.IsActive
}
if req.BrandID != nil {
updates["brand_id"] = *req.BrandID
}
utils.DB.Model(&product).Updates(updates)
if req.CategoryIDs != nil {
var categories []models.Category
utils.DB.Where("id IN ?", req.CategoryIDs).Find(&categories)
utils.DB.Model(&product).Association("Categories").Replace(categories)
}
if req.CustomFields != nil {
utils.DB.Where("product_id = ?", product.ID).Delete(&models.ProductCustomField{})
for _, cf := range req.CustomFields {
utils.DB.Create(&models.ProductCustomField{
ProductID: product.ID,
FieldName: cf.FieldName,
FieldValue: cf.FieldValue,
})
}
}
utils.DB.Preload("Categories").Preload("CustomFields").Preload("Brand").First(&product, product.ID)
c.JSON(http.StatusOK, gin.H{"data": product})
}
func (h *ProductHandler) Delete(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
if err := utils.DB.Delete(&models.Product{}, id).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete product"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Product deleted successfully"})
}
func (h *ProductHandler) AddCustomField(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var product models.Product
if err := utils.DB.First(&product, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Product not found"})
return
}
var req schemas.CustomFieldRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
field := models.ProductCustomField{
ProductID: product.ID,
FieldName: req.FieldName,
FieldValue: req.FieldValue,
}
utils.DB.Create(&field)
c.JSON(http.StatusCreated, gin.H{"data": field})
}
+247
View File
@@ -0,0 +1,247 @@
package handlers
import (
"net/http"
"strconv"
"sale/internal/models"
"sale/internal/schemas"
"sale/internal/utils"
"github.com/gin-gonic/gin"
)
type SystemHandler struct{}
func NewSystemHandler() *SystemHandler {
return &SystemHandler{}
}
func (h *SystemHandler) GetSettings(c *gin.Context) {
var settings []models.SystemSetting
utils.DB.Find(&settings)
result := make(map[string]string)
for _, s := range settings {
result[s.Key] = s.Value
}
c.JSON(http.StatusOK, gin.H{"data": result})
}
func (h *SystemHandler) GetPublicSettings(c *gin.Context) {
publicKeys := []string{
"payment_channel_fee_rate",
"shipping_fee_first_weight",
"shipping_fee_per_gram",
"service_fee_rate",
"tax_rate",
"enabled_payments",
}
var settings []models.SystemSetting
utils.DB.Where("`key` IN ?", publicKeys).Find(&settings)
result := make(map[string]string)
for _, s := range settings {
result[s.Key] = s.Value
}
c.JSON(http.StatusOK, gin.H{"data": result})
}
func (h *SystemHandler) GetStats(c *gin.Context) {
var userCount, productCount, orderCount int64
var totalRevenue float64
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)
c.JSON(http.StatusOK, gin.H{
"data": gin.H{
"users": userCount,
"products": productCount,
"orders": orderCount,
"revenue": totalRevenue,
},
})
}
func (h *SystemHandler) UpdateSettings(c *gin.Context) {
var req schemas.UpdateSettingsRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
for key, value := range req.Settings {
var setting models.SystemSetting
result := utils.DB.Where("`key` = ?", key).First(&setting)
if result.Error != nil {
utils.DB.Create(&models.SystemSetting{Key: key, Value: value})
} else {
utils.DB.Model(&setting).Update("value", value)
}
}
c.JSON(http.StatusOK, gin.H{"message": "Settings updated successfully"})
}
func (h *SystemHandler) UpdateSMTP(c *gin.Context) {
var req map[string]string
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
smtpKeys := []string{"smtp_host", "smtp_port", "smtp_user", "smtp_password", "smtp_from"}
for _, key := range smtpKeys {
if value, ok := req[key]; ok {
var setting models.SystemSetting
result := utils.DB.Where("`key` = ?", key).First(&setting)
if result.Error != nil {
utils.DB.Create(&models.SystemSetting{Key: key, Value: value})
} else {
utils.DB.Model(&setting).Update("value", value)
}
}
}
c.JSON(http.StatusOK, gin.H{"message": "SMTP settings updated successfully"})
}
func (h *SystemHandler) UpdatePayment(c *gin.Context) {
var req map[string]string
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
for key, value := range req {
var setting models.SystemSetting
result := utils.DB.Where("`key` = ?", key).First(&setting)
if result.Error != nil {
utils.DB.Create(&models.SystemSetting{Key: key, Value: value})
} else {
utils.DB.Model(&setting).Update("value", value)
}
}
c.JSON(http.StatusOK, gin.H{"message": "Payment settings updated successfully"})
}
type SupplierHandler struct{}
func NewSupplierHandler() *SupplierHandler {
return &SupplierHandler{}
}
func (h *SupplierHandler) List(c *gin.Context) {
var suppliers []models.User
utils.DB.Where("role = ?", "supplier").Find(&suppliers)
c.JSON(http.StatusOK, gin.H{"data": suppliers})
}
func (h *SupplierHandler) Create(c *gin.Context) {
var req schemas.AddSupplierRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
hashedPassword, _ := utils.HashPassword(req.Password)
supplier := models.User{
Username: req.Username,
Email: req.Email,
PasswordHash: hashedPassword,
Role: "supplier",
InviteCode: utils.GenerateInviteCode(),
IsActive: true,
}
utils.DB.Create(&supplier)
c.JSON(http.StatusCreated, gin.H{"data": supplier})
}
func (h *SupplierHandler) Update(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var supplier models.User
if err := utils.DB.Where("id = ? AND role = ?", id, "supplier").First(&supplier).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Supplier not found"})
return
}
var updateData map[string]interface{}
if err := c.ShouldBindJSON(&updateData); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
utils.DB.Model(&supplier).Updates(updateData)
c.JSON(http.StatusOK, gin.H{"data": supplier})
}
func (h *SupplierHandler) Delete(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
utils.DB.Where("id = ? AND role = ?", id, "supplier").Delete(&models.User{})
c.JSON(http.StatusOK, gin.H{"message": "Supplier deleted successfully"})
}
func (h *SupplierHandler) Authorize(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var req schemas.AuthorizeSupplierRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
auth := models.SupplierAuthorization{
SupplierID: uint(id),
Type: req.Type,
RefID: req.RefID,
}
utils.DB.Create(&auth)
c.JSON(http.StatusCreated, gin.H{"data": auth})
}
type InventoryHandler struct{}
func NewInventoryHandler() *InventoryHandler {
return &InventoryHandler{}
}
func (h *InventoryHandler) SupplierList(c *gin.Context) {
userID := c.GetUint("user_id")
var inventory []models.Inventory
utils.DB.Where("supplier_id = ?", userID).Preload("Product").Find(&inventory)
c.JSON(http.StatusOK, gin.H{"data": inventory})
}
func (h *InventoryHandler) Update(c *gin.Context) {
userID := c.GetUint("user_id")
id, _ := strconv.Atoi(c.Param("id"))
var inventory models.Inventory
if err := utils.DB.Where("id = ? AND supplier_id = ?", id, userID).First(&inventory).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Inventory not found"})
return
}
var req schemas.UpdateInventoryRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
utils.DB.Model(&inventory).Update("quantity", req.Quantity)
c.JSON(http.StatusOK, gin.H{"data": inventory})
}
func (h *InventoryHandler) AdminList(c *gin.Context) {
var inventory []models.Inventory
utils.DB.Preload("Product").Find(&inventory)
c.JSON(http.StatusOK, gin.H{"data": inventory})
}
+166
View File
@@ -0,0 +1,166 @@
package handlers
import (
"math"
"net/http"
"strconv"
"sale/internal/models"
"sale/internal/schemas"
"sale/internal/utils"
"github.com/gin-gonic/gin"
)
type TicketHandler struct{}
func NewTicketHandler() *TicketHandler {
return &TicketHandler{}
}
func (h *TicketHandler) List(c *gin.Context) {
userID := c.GetUint("user_id")
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
var total int64
utils.DB.Model(&models.Ticket{}).Where("user_id = ?", userID).Count(&total)
var tickets []models.Ticket
offset := (page - 1) * pageSize
utils.DB.Where("user_id = ?", userID).Order("created_at DESC").Offset(offset).Limit(pageSize).Find(&tickets)
totalPages := int(math.Ceil(float64(total) / float64(pageSize)))
c.JSON(http.StatusOK, gin.H{
"data": tickets,
"pagination": gin.H{
"page": page,
"page_size": pageSize,
"total": total,
"total_pages": totalPages,
},
})
}
func (h *TicketHandler) Create(c *gin.Context) {
userID := c.GetUint("user_id")
var req schemas.CreateTicketRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
category := req.Category
if category == "" {
category = models.TicketCategoryOther
}
ticket := models.Ticket{
UserID: userID,
Title: req.Title,
Content: req.Content,
Category: category,
Status: models.TicketStatusPending,
}
utils.DB.Create(&ticket)
c.JSON(http.StatusCreated, gin.H{"data": ticket})
}
func (h *TicketHandler) GetByID(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
}
c.JSON(http.StatusOK, gin.H{"data": ticket})
}
func (h *TicketHandler) AdminList(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
status := c.Query("status")
var total int64
query := utils.DB.Model(&models.Ticket{})
if status != "" {
query = query.Where("status = ?", status)
}
query.Count(&total)
var tickets []models.Ticket
offset := (page - 1) * pageSize
q := utils.DB.Preload("User").Preload("Assignee")
if status != "" {
q = q.Where("status = ?", status)
}
q.Order("created_at DESC").Offset(offset).Limit(pageSize).Find(&tickets)
totalPages := int(math.Ceil(float64(total) / float64(pageSize)))
c.JSON(http.StatusOK, gin.H{
"data": tickets,
"pagination": gin.H{
"page": page,
"page_size": pageSize,
"total": total,
"total_pages": totalPages,
},
})
}
func (h *TicketHandler) Update(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var ticket models.Ticket
if err := utils.DB.First(&ticket, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Ticket not found"})
return
}
var req schemas.UpdateTicketRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
updates := make(map[string]interface{})
if req.Status != nil {
updates["status"] = *req.Status
}
if req.AssignedTo != nil {
updates["assigned_to"] = *req.AssignedTo
}
if req.Reply != nil {
updates["reply"] = *req.Reply
}
utils.DB.Model(&ticket).Updates(updates)
c.JSON(http.StatusOK, gin.H{"data": ticket})
}
func (h *TicketHandler) Assign(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var ticket models.Ticket
if err := utils.DB.First(&ticket, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Ticket not found"})
return
}
var req struct {
AssignedTo uint `json:"assigned_to" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
utils.DB.Model(&ticket).Updates(map[string]interface{}{
"assigned_to": req.AssignedTo,
"status": models.TicketStatusProcessing,
})
c.JSON(http.StatusOK, gin.H{"message": "Ticket assigned successfully"})
}
+138
View File
@@ -0,0 +1,138 @@
package handlers
import (
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
type UploadHandler struct{}
func NewUploadHandler() *UploadHandler {
return &UploadHandler{}
}
func (h *UploadHandler) UploadImage(c *gin.Context) {
file, err := c.FormFile("file")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "No file uploaded"})
return
}
ext := strings.ToLower(filepath.Ext(file.Filename))
allowedExts := map[string]bool{".jpg": true, ".jpeg": true, ".png": true, ".gif": true, ".webp": true}
if !allowedExts[ext] {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid file type. Only jpg, jpeg, png, gif, webp are allowed"})
return
}
uploadDir := "uploads/images"
if err := os.MkdirAll(uploadDir, 0755); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create upload directory"})
return
}
filename := fmt.Sprintf("%s%s", uuid.New().String(), ext)
filepath := filepath.Join(uploadDir, filename)
if err := c.SaveUploadedFile(file, filepath); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save file"})
return
}
url := fmt.Sprintf("/uploads/images/%s", filename)
c.JSON(http.StatusOK, gin.H{
"url": url,
"filename": filename,
})
}
func (h *UploadHandler) UploadMultiple(c *gin.Context) {
form, err := c.MultipartForm()
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "No files uploaded"})
return
}
files := form.File["files"]
if len(files) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "No files uploaded"})
return
}
uploadDir := "uploads/images"
if err := os.MkdirAll(uploadDir, 0755); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create upload directory"})
return
}
allowedExts := map[string]bool{".jpg": true, ".jpeg": true, ".png": true, ".gif": true, ".webp": true}
var urls []string
for _, file := range files {
ext := strings.ToLower(filepath.Ext(file.Filename))
if !allowedExts[ext] {
continue
}
filename := fmt.Sprintf("%s%s", uuid.New().String(), ext)
filepath := filepath.Join(uploadDir, filename)
if err := c.SaveUploadedFile(file, filepath); err != nil {
continue
}
urls = append(urls, fmt.Sprintf("/uploads/images/%s", filename))
}
c.JSON(http.StatusOK, gin.H{
"urls": urls,
})
}
func (h *UploadHandler) DeleteImage(c *gin.Context) {
filename := c.Param("filename")
if filename == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Filename is required"})
return
}
filepath := filepath.Join("uploads/images", filename)
if err := os.Remove(filepath); err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "File not found"})
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
}
}
+79
View File
@@ -0,0 +1,79 @@
package middlewares
import (
"net/http"
"strings"
"sale/internal/utils"
"github.com/gin-gonic/gin"
)
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header is required"})
c.Abort()
return
}
parts := strings.SplitN(authHeader, " ", 2)
if len(parts) != 2 || parts[0] != "Bearer" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid authorization header format"})
c.Abort()
return
}
claims, err := utils.ParseToken(parts[1])
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid or expired token"})
c.Abort()
return
}
c.Set("user_id", claims.UserID)
c.Set("role", claims.Role)
c.Next()
}
}
func RoleMiddleware(roles ...string) gin.HandlerFunc {
roleMap := make(map[string]bool)
for _, r := range roles {
roleMap[r] = true
}
return func(c *gin.Context) {
role, exists := c.Get("role")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
c.Abort()
return
}
if !roleMap[role.(string)] {
c.JSON(http.StatusForbidden, gin.H{"error": "Permission denied"})
c.Abort()
return
}
c.Next()
}
}
func CORSMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE")
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(204)
return
}
c.Next()
}
}
+207
View File
@@ -0,0 +1,207 @@
package routes
import (
"sale/internal/api/handlers"
"sale/internal/api/middlewares"
"sale/internal/models"
"sale/internal/utils"
"github.com/gin-gonic/gin"
)
func SetupRoutes(r *gin.Engine) {
authHandler := handlers.NewAuthHandler()
categoryHandler := handlers.NewCategoryHandler()
brandHandler := handlers.NewBrandHandler()
productHandler := handlers.NewProductHandler()
cartHandler := handlers.NewCartHandler()
orderHandler := handlers.NewOrderHandler()
addressHandler := handlers.NewAddressHandler()
lotteryHandler := handlers.NewLotteryHandler()
ticketHandler := handlers.NewTicketHandler()
systemHandler := handlers.NewSystemHandler()
supplierHandler := handlers.NewSupplierHandler()
inventoryHandler := handlers.NewInventoryHandler()
articleHandler := handlers.NewArticleHandler()
uploadHandler := handlers.NewUploadHandler()
r.Use(middlewares.CORSMiddleware())
r.Static("/uploads", "./uploads")
api := r.Group("/api")
{
api.GET("/installed", authHandler.CheckInstalled)
api.POST("/install", authHandler.Install)
auth := api.Group("/auth")
{
auth.POST("/register", authHandler.Register)
auth.POST("/login", authHandler.Login)
auth.POST("/forgot-password", authHandler.ForgotPassword)
auth.POST("/reset-password", authHandler.ResetPassword)
auth.GET("/verify-email", authHandler.VerifyEmail)
}
articles := api.Group("/articles")
{
articles.GET("", articleHandler.List)
articles.GET("/:id", articleHandler.GetByID)
}
api.GET("/categories", categoryHandler.List)
api.GET("/brands", brandHandler.List)
api.GET("/products", productHandler.List)
api.GET("/products/:id", productHandler.GetByID)
api.GET("/lotteries", lotteryHandler.List)
api.GET("/lotteries/:id", lotteryHandler.GetByID)
api.GET("/settings/public", systemHandler.GetPublicSettings)
authed := api.Group("")
authed.Use(middlewares.AuthMiddleware())
{
authed.POST("/upload", uploadHandler.UploadImage)
authed.POST("/upload/multiple", uploadHandler.UploadMultiple)
authed.DELETE("/upload/:filename", uploadHandler.DeleteImage)
users := authed.Group("/users")
{
users.GET("/profile", authHandler.GetProfile)
users.PUT("/profile", authHandler.UpdateProfile)
users.PUT("/password", authHandler.ChangePassword)
users.POST("/send-verify-code", authHandler.SendVerifyCode)
}
addresses := authed.Group("/users/addresses")
{
addresses.GET("", addressHandler.List)
addresses.POST("", addressHandler.Create)
addresses.PUT("/:id", addressHandler.Update)
addresses.DELETE("/:id", addressHandler.Delete)
addresses.PUT("/:id/default", addressHandler.SetDefault)
}
carts := authed.Group("/cart")
{
carts.GET("", cartHandler.List)
carts.POST("", cartHandler.Add)
carts.PUT("/:id", cartHandler.Update)
carts.DELETE("/:id", cartHandler.Delete)
}
orders := authed.Group("/orders")
{
orders.GET("", orderHandler.List)
orders.GET("/:id", orderHandler.GetByID)
orders.POST("", orderHandler.Create)
orders.POST("/:id/refund", orderHandler.Refund)
}
lotteries := authed.Group("/lotteries")
{
lotteries.POST("/:id/register", lotteryHandler.Register)
}
tickets := authed.Group("/tickets")
{
tickets.GET("", ticketHandler.List)
tickets.POST("", ticketHandler.Create)
tickets.GET("/:id", ticketHandler.GetByID)
}
supplier := authed.Group("/supplier")
supplier.Use(middlewares.RoleMiddleware("supplier", "admin"))
{
supplier.GET("/orders", orderHandler.List)
supplier.GET("/orders/:id", orderHandler.GetByID)
supplier.PUT("/orders/:id/confirm", orderHandler.ConfirmOrder)
supplier.PUT("/orders/:id/ship", orderHandler.ShipOrder)
supplier.GET("/inventory", inventoryHandler.SupplierList)
supplier.PUT("/inventory/:id", inventoryHandler.Update)
}
admin := authed.Group("/admin")
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})
})
adminCategories := admin.Group("/categories")
{
adminCategories.POST("", categoryHandler.Create)
adminCategories.PUT("/:id", categoryHandler.Update)
adminCategories.DELETE("/:id", categoryHandler.Delete)
}
adminBrands := admin.Group("/brands")
{
adminBrands.GET("", brandHandler.List)
adminBrands.POST("", brandHandler.Create)
adminBrands.PUT("/:id", brandHandler.Update)
adminBrands.DELETE("/:id", brandHandler.Delete)
}
adminProducts := admin.Group("/products")
{
adminProducts.GET("", productHandler.AdminList)
adminProducts.POST("", productHandler.Create)
adminProducts.PUT("/:id", productHandler.Update)
adminProducts.DELETE("/:id", productHandler.Delete)
adminProducts.POST("/:id/custom-fields", productHandler.AddCustomField)
}
adminOrders := admin.Group("/orders")
{
adminOrders.GET("", orderHandler.AdminList)
adminOrders.GET("/export", orderHandler.Export)
adminOrders.PUT("/:id/refund", orderHandler.ProcessRefund)
}
adminLotteries := admin.Group("/lotteries")
{
adminLotteries.POST("", lotteryHandler.Create)
adminLotteries.PUT("/:id", lotteryHandler.Update)
adminLotteries.DELETE("/:id", lotteryHandler.Delete)
adminLotteries.POST("/:id/prizes", lotteryHandler.AddPrize)
adminLotteries.POST("/:id/draw", lotteryHandler.Draw)
}
adminTickets := admin.Group("/tickets")
{
adminTickets.GET("", ticketHandler.AdminList)
adminTickets.PUT("/:id", ticketHandler.Update)
adminTickets.PUT("/:id/assign", ticketHandler.Assign)
}
admin.GET("/settings", systemHandler.GetSettings)
admin.PUT("/settings", systemHandler.UpdateSettings)
admin.PUT("/settings/smtp", systemHandler.UpdateSMTP)
admin.PUT("/settings/payment", systemHandler.UpdatePayment)
adminSuppliers := admin.Group("/suppliers")
{
adminSuppliers.GET("", supplierHandler.List)
adminSuppliers.POST("", supplierHandler.Create)
adminSuppliers.PUT("/:id", supplierHandler.Update)
adminSuppliers.DELETE("/:id", supplierHandler.Delete)
adminSuppliers.POST("/:id/authorize", supplierHandler.Authorize)
}
admin.GET("/inventory", inventoryHandler.AdminList)
adminArticles := admin.Group("/articles")
{
adminArticles.GET("", articleHandler.AdminList)
adminArticles.POST("", articleHandler.Create)
adminArticles.PUT("/:id", articleHandler.Update)
adminArticles.DELETE("/:id", articleHandler.Delete)
adminArticles.PUT("/:id/pin", articleHandler.TogglePin)
}
}
}
}
}
+98
View File
@@ -0,0 +1,98 @@
package config
import (
"os"
"github.com/joho/godotenv"
)
type Config struct {
Server ServerConfig
Database DatabaseConfig
Redis RedisConfig
JWT JWTConfig
SMTP SMTPConfig
}
type ServerConfig struct {
Port string
Mode string
}
type DatabaseConfig struct {
Driver string
Host string
Port string
User string
Password string
DBName string
SSLMode string
FilePath string
}
type RedisConfig struct {
Host string
Port string
Password string
DB int
}
type JWTConfig struct {
Secret string
ExpireHour int
}
type SMTPConfig struct {
Host string
Port string
User string
Password string
From string
}
var AppConfig *Config
func Load() {
godotenv.Load()
AppConfig = &Config{
Server: ServerConfig{
Port: getEnv("SERVER_PORT", "8080"),
Mode: getEnv("SERVER_MODE", "debug"),
},
Database: DatabaseConfig{
Driver: getEnv("DB_DRIVER", "sqlite"),
Host: getEnv("DB_HOST", "localhost"),
Port: getEnv("DB_PORT", "5432"),
User: getEnv("DB_USER", "postgres"),
Password: getEnv("DB_PASSWORD", "postgres"),
DBName: getEnv("DB_NAME", "sale"),
SSLMode: getEnv("DB_SSLMODE", "disable"),
FilePath: getEnv("DB_FILEPATH", "sale.db"),
},
Redis: RedisConfig{
Host: getEnv("REDIS_HOST", "localhost"),
Port: getEnv("REDIS_PORT", "6379"),
Password: getEnv("REDIS_PASSWORD", ""),
DB: 0,
},
JWT: JWTConfig{
Secret: getEnv("JWT_SECRET", "sale-secret-key"),
ExpireHour: 72,
},
SMTP: SMTPConfig{
Host: getEnv("SMTP_HOST", ""),
Port: getEnv("SMTP_PORT", "587"),
User: getEnv("SMTP_USER", ""),
Password: getEnv("SMTP_PASSWORD", ""),
From: getEnv("SMTP_FROM", ""),
},
}
}
func getEnv(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {
return value
}
return defaultValue
}
+23
View File
@@ -0,0 +1,23 @@
package models
import (
"time"
)
type Address struct {
ID uint `gorm:"primaryKey" json:"id"`
UserID uint `gorm:"index;not null" json:"user_id"`
Name string `gorm:"size:50;not null" json:"name"`
Phone string `gorm:"size:20;not null" json:"phone"`
Province string `gorm:"size:50" json:"province"`
City string `gorm:"size:50" json:"city"`
District string `gorm:"size:50" json:"district"`
Address string `gorm:"type:text;not null" json:"address"`
IsDefault bool `gorm:"default:false" json:"is_default"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
func (Address) TableName() string {
return "addresses"
}
+27
View File
@@ -0,0 +1,27 @@
package models
import (
"time"
"gorm.io/gorm"
)
type Article struct {
ID uint `gorm:"primaryKey" json:"id"`
Title string `gorm:"size:255;not null" json:"title"`
Content string `gorm:"type:text;not null" json:"content"`
Summary string `gorm:"size:500" json:"summary"`
CoverImage string `gorm:"size:500" json:"cover_image"`
IsPinned bool `gorm:"default:false" json:"is_pinned"`
IsPublished bool `gorm:"default:true" json:"is_published"`
SortOrder int `gorm:"default:0" json:"sort_order"`
AuthorID *uint `json:"author_id"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
Author *User `gorm:"foreignKey:AuthorID" json:"author,omitempty"`
}
func (Article) TableName() string {
return "articles"
}
+22
View File
@@ -0,0 +1,22 @@
package models
import (
"time"
"gorm.io/gorm"
)
type Brand struct {
ID uint `gorm:"primaryKey" json:"id"`
Name string `gorm:"size:255;not null;uniqueIndex" json:"name"`
Description string `json:"description"`
Logo string `json:"logo"`
SortOrder int `gorm:"default:0" json:"sort_order"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}
func (Brand) TableName() string {
return "brands"
}
+19
View File
@@ -0,0 +1,19 @@
package models
import (
"time"
)
type Cart struct {
ID uint `gorm:"primaryKey" json:"id"`
UserID uint `gorm:"index;not null" json:"user_id"`
ProductID uint `gorm:"index;not null" json:"product_id"`
Quantity int `gorm:"not null" json:"quantity"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Product Product `json:"product,omitempty"`
}
func (Cart) TableName() string {
return "carts"
}
+29
View File
@@ -0,0 +1,29 @@
package models
import (
"time"
"gorm.io/gorm"
)
type Category struct {
ID uint `gorm:"primaryKey" json:"id"`
Name string `gorm:"size:100;not null" json:"name"`
Description string `json:"description"`
ParentID *uint `json:"parent_id"`
MinAmount *float64 `json:"min_amount"`
MaxAmount *float64 `json:"max_amount"`
MinQuantity *int `json:"min_quantity"`
MaxQuantity *int `json:"max_quantity"`
MinWeight *float64 `json:"min_weight"`
MaxWeight *float64 `json:"max_weight"`
SortOrder int `gorm:"default:0" json:"sort_order"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
Children []Category `gorm:"foreignKey:ParentID" json:"children,omitempty"`
}
func (Category) TableName() string {
return "categories"
}
+17
View File
@@ -0,0 +1,17 @@
package models
import (
"time"
)
type Inventory struct {
ID uint `gorm:"primaryKey" json:"id"`
ProductID uint `gorm:"index;not null" json:"product_id"`
SupplierID uint `gorm:"index;not null" json:"supplier_id"`
Quantity int `gorm:"default:0" json:"quantity"`
UpdatedAt time.Time `json:"updated_at"`
}
func (Inventory) TableName() string {
return "inventory"
}
+80
View File
@@ -0,0 +1,80 @@
package models
import (
"time"
"gorm.io/gorm"
)
type Lottery struct {
ID uint `gorm:"primaryKey" json:"id"`
Name string `gorm:"size:100;not null" json:"name"`
Description string `json:"description"`
StartTime time.Time `json:"start_time"`
EndTime time.Time `json:"end_time"`
Cycle string `gorm:"size:20" json:"cycle"`
DailyQuota *int `json:"daily_quota"`
TotalQuota *int `json:"total_quota"`
RegistrationValidity *int `json:"registration_validity"`
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:"-"`
Prizes []LotteryPrize `json:"prizes,omitempty"`
}
func (Lottery) TableName() string {
return "lotteries"
}
type LotteryPrize struct {
ID uint `gorm:"primaryKey" json:"id"`
LotteryID uint `gorm:"index;not null" json:"lottery_id"`
Name string `gorm:"size:100;not null" json:"name"`
Type string `gorm:"size:20;not null" json:"type"`
Quantity int `gorm:"not null" json:"quantity"`
Weight int `gorm:"default:1" json:"weight"`
CreditReward *int `json:"credit_reward"`
DrawMode string `gorm:"size:20;default:'random'" json:"draw_mode"`
}
func (LotteryPrize) TableName() string {
return "lottery_prizes"
}
type LotteryParticipant struct {
ID uint `gorm:"primaryKey" json:"id"`
LotteryID uint `gorm:"index;not null" json:"lottery_id"`
UserID uint `gorm:"index;not null" json:"user_id"`
PurchaseWeight int `gorm:"default:0" json:"purchase_weight"`
RegisteredAt time.Time `json:"registered_at"`
}
func (LotteryParticipant) TableName() string {
return "lottery_participants"
}
type LotteryWinner struct {
ID uint `gorm:"primaryKey" json:"id"`
LotteryID uint `gorm:"index;not null" json:"lottery_id"`
PrizeID uint `gorm:"index;not null" json:"prize_id"`
UserID uint `gorm:"index;not null" json:"user_id"`
DrawnAt time.Time `json:"drawn_at"`
}
func (LotteryWinner) TableName() string {
return "lottery_winners"
}
const (
PrizeTypePhysical = "physical"
PrizeTypeVirtual = "virtual"
PrizeTypeCredit = "credit"
DrawModeRandom = "random"
DrawModeWeight = "weight"
CycleDaily = "daily"
CycleWeekly = "weekly"
CycleMonthly = "monthly"
)
+70
View File
@@ -0,0 +1,70 @@
package models
import (
"time"
"gorm.io/gorm"
)
type Order struct {
ID uint `gorm:"primaryKey" json:"id"`
UserID uint `gorm:"index;not null" json:"user_id"`
SupplierID *uint `gorm:"index" json:"supplier_id"`
Subtotal float64 `gorm:"type:decimal(10,2);not null" json:"subtotal"`
ShippingFee float64 `gorm:"type:decimal(10,2);default:0" json:"shipping_fee"`
ServiceFee float64 `gorm:"type:decimal(10,2);default:0" json:"service_fee"`
Tax float64 `gorm:"type:decimal(10,2);default:0" json:"tax"`
TotalAmount float64 `gorm:"type:decimal(10,2);not null" json:"total_amount"`
RefundAmount *float64 `gorm:"type:decimal(10,2)" json:"refund_amount"`
RefundStatus string `gorm:"size:20" json:"refund_status"`
RefundReason string `json:"refund_reason"`
Status string `gorm:"size:20;not null;default:'pending_payment'" json:"status"`
ShippingAddressID *uint `json:"shipping_address_id"`
TrackingNumber string `gorm:"size:100" json:"tracking_number"`
ShippingPhoto string `gorm:"type:text" json:"shipping_photo"`
ExpressPhoto string `gorm:"type:text" json:"express_photo"`
CustomsPhoto string `gorm:"type:text" json:"customs_photo"`
PaymentMethod string `gorm:"size:50" json:"payment_method"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
OrderItems []OrderItem `json:"order_items,omitempty"`
ShippingAddress *Address `json:"shipping_address,omitempty"`
User User `json:"user,omitempty"`
}
func (Order) TableName() string {
return "orders"
}
type OrderItem struct {
ID uint `gorm:"primaryKey" json:"id"`
OrderID uint `gorm:"index;not null" json:"order_id"`
ProductID uint `gorm:"index;not null" json:"product_id"`
Quantity int `gorm:"not null" json:"quantity"`
Price float64 `gorm:"type:decimal(10,2);not null" json:"price"`
Product Product `json:"product,omitempty"`
}
func (OrderItem) TableName() string {
return "order_items"
}
const (
OrderStatusPendingPayment = "pending_payment"
OrderStatusPendingConfirm = "pending_confirm"
OrderStatusPendingShip = "pending_ship"
OrderStatusShipped = "shipped"
OrderStatusCompleted = "completed"
OrderStatusRefunding = "refunding"
OrderStatusRefunded = "refunded"
OrderStatusCancelled = "cancelled"
)
const (
RefundStatusNone = ""
RefundStatusPending = "pending"
RefundStatusApproved = "approved"
RefundStatusRejected = "rejected"
RefundStatusCompleted = "completed"
)
+48
View File
@@ -0,0 +1,48 @@
package models
import (
"time"
"gorm.io/gorm"
)
type Product struct {
ID uint `gorm:"primaryKey" json:"id"`
Name string `gorm:"size:255;not null" json:"name"`
Description string `json:"description"`
Price float64 `gorm:"type:decimal(10,2);not null" json:"price"`
MinPurchase int `gorm:"default:1" json:"min_purchase"`
MaxPurchase *int `json:"max_purchase"`
MinWeight *float64 `json:"min_weight"`
MaxWeight *float64 `json:"max_weight"`
MinAmount *float64 `json:"min_amount"`
MaxAmount *float64 `json:"max_amount"`
RequireCredit bool `gorm:"default:false" json:"require_credit"`
CreditCost int `gorm:"default:0" json:"credit_cost"`
CreditReward int `gorm:"default:0" json:"credit_reward"`
Images string `gorm:"type:text" json:"images"`
IsActive bool `gorm:"default:true" json:"is_active"`
BrandID *uint `json:"brand_id"`
Brand *Brand `json:"brand,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
Categories []Category `gorm:"many2many:product_categories;" json:"categories,omitempty"`
CustomFields []ProductCustomField `json:"custom_fields,omitempty"`
}
func (Product) TableName() string {
return "products"
}
type ProductCustomField struct {
ID uint `gorm:"primaryKey" json:"id"`
ProductID uint `gorm:"index;not null" json:"product_id"`
FieldName string `gorm:"size:100;not null" json:"field_name"`
FieldValue string `json:"field_value"`
CreatedAt time.Time `json:"created_at"`
}
func (ProductCustomField) TableName() string {
return "product_custom_fields"
}
+32
View File
@@ -0,0 +1,32 @@
package models
import (
"time"
)
type SystemSetting struct {
ID uint `gorm:"primaryKey" json:"id"`
Key string `gorm:"uniqueIndex;size:100;not null" json:"key"`
Value string `gorm:"type:text" json:"value"`
UpdatedAt time.Time `json:"updated_at"`
}
func (SystemSetting) TableName() string {
return "system_settings"
}
type SupplierAuthorization struct {
ID uint `gorm:"primaryKey" json:"id"`
SupplierID uint `gorm:"index;not null" json:"supplier_id"`
Type string `gorm:"size:20;not null" json:"type"`
RefID uint `gorm:"not null" json:"ref_id"`
}
func (SupplierAuthorization) TableName() string {
return "supplier_authorizations"
}
const (
AuthTypeCategory = "category"
AuthTypeProduct = "product"
)
+39
View File
@@ -0,0 +1,39 @@
package models
import (
"time"
"gorm.io/gorm"
)
type Ticket struct {
ID uint `gorm:"primaryKey" json:"id"`
UserID uint `gorm:"index;not null" json:"user_id"`
Title string `gorm:"size:200;not null" json:"title"`
Content string `gorm:"type:text;not null" json:"content"`
Category string `gorm:"size:50" json:"category"`
Status string `gorm:"size:20;not null;default:'pending'" json:"status"`
AssignedTo *uint `json:"assigned_to"`
Reply string `gorm:"type:text" json:"reply"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
User User `gorm:"foreignKey:UserID" json:"user,omitempty"`
Assignee *User `gorm:"foreignKey:AssignedTo" json:"assignee,omitempty"`
}
func (Ticket) TableName() string {
return "tickets"
}
const (
TicketStatusPending = "pending"
TicketStatusProcessing = "processing"
TicketStatusResolved = "resolved"
TicketStatusClosed = "closed"
TicketCategoryOrder = "order"
TicketCategoryAccount = "account"
TicketCategoryProduct = "product"
TicketCategoryOther = "other"
)
+27
View File
@@ -0,0 +1,27 @@
package models
import (
"time"
"gorm.io/gorm"
)
type User struct {
ID uint `gorm:"primaryKey" json:"id"`
Username string `gorm:"uniqueIndex;size:50;not null" json:"username"`
Email string `gorm:"uniqueIndex;size:100;not null" json:"email"`
PasswordHash string `gorm:"size:255;not null" json:"-"`
Role string `gorm:"size:20;not null;default:'user'" json:"role"`
PurchaseCredits int `gorm:"default:0" json:"purchase_credits"`
InviteCode string `gorm:"uniqueIndex;size:20" json:"invite_code"`
ReferredBy *uint `json:"referred_by"`
EmailVerified bool `gorm:"default:false" json:"email_verified"`
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 (User) TableName() string {
return "users"
}
+21
View File
@@ -0,0 +1,21 @@
package schemas
type CreateAddressRequest struct {
Name string `json:"name" binding:"required"`
Phone string `json:"phone" binding:"required"`
Province string `json:"province"`
City string `json:"city"`
District string `json:"district"`
Address string `json:"address" binding:"required"`
IsDefault bool `json:"is_default"`
}
type UpdateAddressRequest struct {
Name *string `json:"name"`
Phone *string `json:"phone"`
Province *string `json:"province"`
City *string `json:"city"`
District *string `json:"district"`
Address *string `json:"address"`
IsDefault *bool `json:"is_default"`
}
+21
View File
@@ -0,0 +1,21 @@
package schemas
type CreateArticleRequest struct {
Title string `json:"title" binding:"required"`
Content string `json:"content" binding:"required"`
Summary string `json:"summary"`
CoverImage string `json:"cover_image"`
IsPinned bool `json:"is_pinned"`
IsPublished bool `json:"is_published"`
SortOrder int `json:"sort_order"`
}
type UpdateArticleRequest struct {
Title *string `json:"title"`
Content *string `json:"content"`
Summary *string `json:"summary"`
CoverImage *string `json:"cover_image"`
IsPinned *bool `json:"is_pinned"`
IsPublished *bool `json:"is_published"`
SortOrder *int `json:"sort_order"`
}
+54
View File
@@ -0,0 +1,54 @@
package schemas
type RegisterRequest struct {
Email string `json:"email" binding:"required,email"`
Username string `json:"username" binding:"required,min=2,max=50"`
Password string `json:"password" binding:"required,min=6"`
InviteCode string `json:"invite_code"`
}
type LoginRequest struct {
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required"`
}
type ForgotPasswordRequest struct {
Email string `json:"email" binding:"required,email"`
}
type ResetPasswordRequest struct {
Email string `json:"email" binding:"required,email"`
Code string `json:"code" binding:"required"`
Password string `json:"password" binding:"required,min=6"`
}
type VerifyEmailRequest struct {
Email string `json:"email" binding:"required,email"`
Code string `json:"code" binding:"required"`
}
type ChangePasswordRequest struct {
OldPassword string `json:"old_password" binding:"required"`
NewPassword string `json:"new_password" binding:"required,min=6"`
}
type AuthResponse struct {
Token string `json:"token"`
User UserResponse `json:"user"`
}
type UserResponse struct {
ID uint `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
Role string `json:"role"`
PurchaseCredits int `json:"purchase_credits"`
InviteCode string `json:"invite_code"`
EmailVerified bool `json:"email_verified"`
}
type InstallRequest struct {
Username string `json:"username" binding:"required,min=2,max=50"`
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required,min=6"`
}
+37
View File
@@ -0,0 +1,37 @@
package schemas
type CreateLotteryRequest struct {
Name string `json:"name" binding:"required"`
Description string `json:"description"`
StartTime string `json:"start_time" binding:"required"`
EndTime string `json:"end_time" binding:"required"`
Cycle string `json:"cycle"`
DailyQuota *int `json:"daily_quota"`
TotalQuota *int `json:"total_quota"`
RegistrationValidity *int `json:"registration_validity"`
}
type UpdateLotteryRequest struct {
Name *string `json:"name"`
Description *string `json:"description"`
StartTime *string `json:"start_time"`
EndTime *string `json:"end_time"`
Cycle *string `json:"cycle"`
DailyQuota *int `json:"daily_quota"`
TotalQuota *int `json:"total_quota"`
RegistrationValidity *int `json:"registration_validity"`
IsActive *bool `json:"is_active"`
}
type AddLotteryPrizeRequest struct {
Name string `json:"name" binding:"required"`
Type string `json:"type" binding:"required,oneof=physical virtual credit"`
Quantity int `json:"quantity" binding:"required,min=1"`
Weight int `json:"weight"`
CreditReward *int `json:"credit_reward"`
DrawMode string `json:"draw_mode"`
}
type RegisterLotteryRequest struct {
LotteryID uint `json:"lottery_id" binding:"required"`
}
+30
View File
@@ -0,0 +1,30 @@
package schemas
type AddToCartRequest struct {
ProductID uint `json:"product_id" binding:"required"`
Quantity int `json:"quantity" binding:"required,min=1"`
}
type UpdateCartRequest struct {
Quantity int `json:"quantity" binding:"required,min=1"`
}
type CreateOrderRequest struct {
ShippingAddressID uint `json:"shipping_address_id" binding:"required"`
PaymentMethod string `json:"payment_method"`
}
type RefundRequest struct {
Reason string `json:"reason" binding:"required"`
}
type ShipOrderRequest struct {
TrackingNumber string `json:"tracking_number" binding:"required"`
ShippingPhoto string `json:"shipping_photo"`
ExpressPhoto string `json:"express_photo"`
CustomsPhoto string `json:"customs_photo"`
}
type ProcessRefundRequest struct {
Status string `json:"status" binding:"required,oneof=approved rejected"`
}
+85
View File
@@ -0,0 +1,85 @@
package schemas
type CreateCategoryRequest struct {
Name string `json:"name" binding:"required"`
Description string `json:"description"`
ParentID *uint `json:"parent_id"`
MinAmount *float64 `json:"min_amount"`
MaxAmount *float64 `json:"max_amount"`
MinQuantity *int `json:"min_quantity"`
MaxQuantity *int `json:"max_quantity"`
MinWeight *float64 `json:"min_weight"`
MaxWeight *float64 `json:"max_weight"`
SortOrder int `json:"sort_order"`
}
type UpdateCategoryRequest struct {
Name *string `json:"name"`
Description *string `json:"description"`
ParentID *uint `json:"parent_id"`
MinAmount *float64 `json:"min_amount"`
MaxAmount *float64 `json:"max_amount"`
MinQuantity *int `json:"min_quantity"`
MaxQuantity *int `json:"max_quantity"`
MinWeight *float64 `json:"min_weight"`
MaxWeight *float64 `json:"max_weight"`
SortOrder *int `json:"sort_order"`
}
type CreateProductRequest struct {
Name string `json:"name" binding:"required"`
Description string `json:"description"`
Price float64 `json:"price" binding:"required"`
MinPurchase int `json:"min_purchase"`
MaxPurchase *int `json:"max_purchase"`
MinWeight *float64 `json:"min_weight"`
MaxWeight *float64 `json:"max_weight"`
MinAmount *float64 `json:"min_amount"`
MaxAmount *float64 `json:"max_amount"`
RequireCredit bool `json:"require_credit"`
CreditCost int `json:"credit_cost"`
CreditReward int `json:"credit_reward"`
Images string `json:"images"`
BrandID *uint `json:"brand_id"`
CategoryIDs []uint `json:"category_ids"`
CustomFields []CustomFieldRequest `json:"custom_fields"`
}
type UpdateProductRequest struct {
Name *string `json:"name"`
Description *string `json:"description"`
Price *float64 `json:"price"`
MinPurchase *int `json:"min_purchase"`
MaxPurchase *int `json:"max_purchase"`
MinWeight *float64 `json:"min_weight"`
MaxWeight *float64 `json:"max_weight"`
MinAmount *float64 `json:"min_amount"`
MaxAmount *float64 `json:"max_amount"`
RequireCredit *bool `json:"require_credit"`
CreditCost *int `json:"credit_cost"`
CreditReward *int `json:"credit_reward"`
Images *string `json:"images"`
IsActive *bool `json:"is_active"`
BrandID *uint `json:"brand_id"`
CategoryIDs []uint `json:"category_ids"`
CustomFields []CustomFieldRequest `json:"custom_fields"`
}
type CustomFieldRequest struct {
FieldName string `json:"field_name" binding:"required"`
FieldValue string `json:"field_value"`
}
type PaginationRequest struct {
Page int `form:"page,default=1"`
PageSize int `form:"page_size,default=20"`
}
type ProductListRequest struct {
PaginationRequest
CategoryID *uint `form:"category_id"`
BrandID *uint `form:"brand_id"`
Keyword string `form:"keyword"`
MinPrice *float64 `form:"min_price"`
MaxPrice *float64 `form:"max_price"`
}
+20
View File
@@ -0,0 +1,20 @@
package schemas
type UpdateSettingsRequest struct {
Settings map[string]string `json:"settings" binding:"required"`
}
type AuthorizeSupplierRequest struct {
Type string `json:"type" binding:"required,oneof=category product"`
RefID uint `json:"ref_id" binding:"required"`
}
type AddSupplierRequest struct {
Username string `json:"username" binding:"required"`
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required,min=6"`
}
type UpdateInventoryRequest struct {
Quantity int `json:"quantity" binding:"required,min=0"`
}
+13
View File
@@ -0,0 +1,13 @@
package schemas
type CreateTicketRequest struct {
Title string `json:"title" binding:"required"`
Content string `json:"content" binding:"required"`
Category string `json:"category"`
}
type UpdateTicketRequest struct {
Status *string `json:"status"`
AssignedTo *uint `json:"assigned_to"`
Reply *string `json:"reply"`
}
+79
View File
@@ -0,0 +1,79 @@
package utils
import (
"fmt"
"log"
"sale/internal/config"
"sale/internal/models"
sqlite "github.com/glebarez/sqlite"
"gorm.io/driver/mysql"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
var DB *gorm.DB
func InitDB() {
var err error
cfg := config.AppConfig.Database
switch cfg.Driver {
case "mysql":
dsn := fmt.Sprintf(
"%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local",
cfg.User, cfg.Password, cfg.Host, cfg.Port, cfg.DBName,
)
DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{
Logger: logger.Default.LogMode(logger.Info),
})
case "postgres":
dsn := fmt.Sprintf(
"host=%s port=%s user=%s password=%s dbname=%s sslmode=%s",
cfg.Host, cfg.Port, cfg.User, cfg.Password, cfg.DBName, cfg.SSLMode,
)
DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{
Logger: logger.Default.LogMode(logger.Info),
})
default:
DB, err = gorm.Open(sqlite.Open(cfg.FilePath), &gorm.Config{
Logger: logger.Default.LogMode(logger.Info),
})
}
if err != nil {
log.Fatalf("Failed to connect to database: %v", err)
}
log.Printf("Database connected successfully (driver: %s)", cfg.Driver)
}
func AutoMigrate() {
err := DB.AutoMigrate(
&models.User{},
&models.Category{},
&models.Brand{},
&models.Product{},
&models.ProductCustomField{},
&models.Inventory{},
&models.Cart{},
&models.Order{},
&models.OrderItem{},
&models.Address{},
&models.Lottery{},
&models.LotteryPrize{},
&models.LotteryParticipant{},
&models.LotteryWinner{},
&models.Ticket{},
&models.SystemSetting{},
&models.SupplierAuthorization{},
&models.Article{},
)
if err != nil {
log.Fatalf("Failed to migrate database: %v", err)
}
log.Println("Database migrated successfully")
}
+50
View File
@@ -0,0 +1,50 @@
package utils
import (
"fmt"
"net/smtp"
"sale/internal/config"
)
func SendEmail(to, subject, body string) error {
cfg := config.AppConfig.SMTP
if cfg.Host == "" {
return fmt.Errorf("SMTP not configured")
}
auth := smtp.PlainAuth("", cfg.User, cfg.Password, cfg.Host)
msg := fmt.Sprintf(
"From: %s\r\nTo: %s\r\nSubject: %s\r\nContent-Type: text/html; charset=UTF-8\r\n\r\n%s",
cfg.From, to, subject, body,
)
return smtp.SendMail(
fmt.Sprintf("%s:%s", cfg.Host, cfg.Port),
auth,
cfg.From,
[]string{to},
[]byte(msg),
)
}
func SendVerifyEmail(to, code string) error {
subject := "邮箱验证码"
body := fmt.Sprintf(`
<h2>邮箱验证</h2>
<p>您的验证码是:<strong style="font-size:24px;color:#1890ff;">%s</strong></p>
<p>验证码有效期为30分钟,请尽快使用。</p>
`, code)
return SendEmail(to, subject, body)
}
func SendResetPasswordEmail(to, code string) error {
subject := "重置密码验证码"
body := fmt.Sprintf(`
<h2>重置密码</h2>
<p>您的验证码是:<strong style="font-size:24px;color:#1890ff;">%s</strong></p>
<p>验证码有效期为30分钟,请尽快使用。</p>
`, code)
return SendEmail(to, subject, body)
}
+46
View File
@@ -0,0 +1,46 @@
package utils
import (
"errors"
"time"
"sale/internal/config"
"github.com/golang-jwt/jwt/v5"
)
type Claims struct {
UserID uint `json:"user_id"`
Role string `json:"role"`
jwt.RegisteredClaims
}
func GenerateToken(userID uint, role string) (string, error) {
claims := Claims{
UserID: userID,
Role: role,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(config.AppConfig.JWT.ExpireHour) * time.Hour)),
IssuedAt: jwt.NewNumericDate(time.Now()),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(config.AppConfig.JWT.Secret))
}
func ParseToken(tokenString string) (*Claims, error) {
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
return []byte(config.AppConfig.JWT.Secret), nil
})
if err != nil {
return nil, err
}
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
return claims, nil
}
return nil, errors.New("invalid token")
}
+15
View File
@@ -0,0 +1,15 @@
package utils
import (
"golang.org/x/crypto/bcrypt"
)
func HashPassword(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
return string(bytes), err
}
func CheckPassword(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}
+26
View File
@@ -0,0 +1,26 @@
package utils
import (
"crypto/rand"
"math/big"
)
func GenerateInviteCode() string {
const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
code := make([]byte, 8)
for i := range code {
n, _ := rand.Int(rand.Reader, big.NewInt(int64(len(charset))))
code[i] = charset[n.Int64()]
}
return string(code)
}
func GenerateVerifyCode() string {
const charset = "0123456789"
code := make([]byte, 6)
for i := range code {
n, _ := rand.Int(rand.Reader, big.NewInt(int64(len(charset))))
code[i] = charset[n.Int64()]
}
return string(code)
}