This commit is contained in:
sjk
2025-11-28 15:18:10 +08:00
parent ad4a600af9
commit 5683f35942
188 changed files with 53680 additions and 1062 deletions

View File

@@ -187,6 +187,15 @@ func getConfigName(env string) string {
return "config.test"
case "production", "prod":
return "config.prod"
// 生产环境 - 中国区
case "prod-cn", "production-cn":
return "config.prod-cn"
// 生产环境 - 美国区
case "prod-us", "production-us":
return "config.prod-us"
// 生产环境 - 欧洲区
case "prod-eu", "production-eu":
return "config.prod-eu"
default:
// 如果环境不匹配,尝试使用默认配置文件
if _, err := os.Stat("./configs/config.yaml"); err == nil {

View File

@@ -0,0 +1,466 @@
package handler
import (
"dianshang/internal/service"
"dianshang/pkg/logger"
"dianshang/pkg/response"
"strconv"
"time"
"github.com/gin-gonic/gin"
)
type AdminCouponHandler struct {
couponService *service.CouponService
}
func NewAdminCouponHandler(couponService *service.CouponService) *AdminCouponHandler {
return &AdminCouponHandler{
couponService: couponService,
}
}
// GetCouponList 获取优惠券列表
// @Summary 获取优惠券列表
// @Description 管理员获取所有优惠券(分页)
// @Tags 管理员-优惠券管理
// @Accept json
// @Produce json
// @Param page query int false "页码" default(1)
// @Param page_size query int false "每页数量" default(10)
// @Param status query int false "状态筛选1-启用 0-禁用)"
// @Param type query int false "类型筛选1-满减 2-折扣 3-免邮)"
// @Param keyword query string false "搜索关键词"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.Response
// @Router /admin/coupons [get]
func (h *AdminCouponHandler) GetCouponList(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10"))
status := c.Query("status")
couponType := c.Query("type")
keyword := c.Query("keyword")
if page < 1 {
page = 1
}
if pageSize < 1 || pageSize > 100 {
pageSize = 10
}
coupons, total, err := h.couponService.GetCouponListForAdmin(page, pageSize, status, couponType, keyword)
if err != nil {
logger.Error("获取优惠券列表失败", "error", err)
response.ErrorWithMessage(c, response.ERROR, "获取优惠券列表失败")
return
}
response.Page(c, coupons, total, page, pageSize)
}
// GetCouponDetail 获取优惠券详情
// @Summary 获取优惠券详情
// @Description 获取指定优惠券的详细信息
// @Tags 管理员-优惠券管理
// @Accept json
// @Produce json
// @Param id path int true "优惠券ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.Response
// @Router /admin/coupons/{id} [get]
func (h *AdminCouponHandler) GetCouponDetail(c *gin.Context) {
couponIDStr := c.Param("id")
couponID, err := strconv.ParseUint(couponIDStr, 10, 32)
if err != nil {
response.ErrorWithMessage(c, response.ERROR_INVALID_PARAMS, "无效的优惠券ID")
return
}
coupon, err := h.couponService.GetCouponDetailForAdmin(uint(couponID))
if err != nil {
logger.Error("获取优惠券详情失败", "error", err, "couponID", couponID)
response.ErrorWithMessage(c, response.ERROR, err.Error())
return
}
response.Success(c, coupon)
}
// CreateCoupon 创建优惠券
// @Summary 创建优惠券
// @Description 创建新的优惠券
// @Tags 管理员-优惠券管理
// @Accept json
// @Produce json
// @Param request body CreateCouponRequest true "创建优惠券请求"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.Response
// @Router /admin/coupons [post]
func (h *AdminCouponHandler) CreateCoupon(c *gin.Context) {
var req CreateCouponRequest
if err := c.ShouldBindJSON(&req); err != nil {
logger.Error("绑定创建优惠券参数失败", "error", err)
response.ErrorWithMessage(c, response.ERROR_INVALID_PARAMS, "参数错误: "+err.Error())
return
}
// 获取管理员ID
adminID, exists := c.Get("user_id")
if !exists {
response.Error(c, response.ERROR_UNAUTHORIZED)
return
}
coupon, err := h.couponService.CreateCoupon(&req, adminID.(uint))
if err != nil {
logger.Error("创建优惠券失败", "error", err)
response.ErrorWithMessage(c, response.ERROR, err.Error())
return
}
logger.Info("优惠券创建成功", "couponID", coupon.ID, "name", coupon.Name, "adminID", adminID)
response.Success(c, coupon)
}
// UpdateCoupon 更新优惠券
// @Summary 更新优惠券
// @Description 更新优惠券信息
// @Tags 管理员-优惠券管理
// @Accept json
// @Produce json
// @Param id path int true "优惠券ID"
// @Param request body UpdateCouponRequest true "更新优惠券请求"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.Response
// @Router /admin/coupons/{id} [put]
func (h *AdminCouponHandler) UpdateCoupon(c *gin.Context) {
couponIDStr := c.Param("id")
couponID, err := strconv.ParseUint(couponIDStr, 10, 32)
if err != nil {
response.ErrorWithMessage(c, response.ERROR_INVALID_PARAMS, "无效的优惠券ID")
return
}
var req UpdateCouponRequest
if err := c.ShouldBindJSON(&req); err != nil {
logger.Error("绑定更新优惠券参数失败", "error", err)
response.ErrorWithMessage(c, response.ERROR_INVALID_PARAMS, "参数错误: "+err.Error())
return
}
err = h.couponService.UpdateCoupon(uint(couponID), &req)
if err != nil {
logger.Error("更新优惠券失败", "error", err, "couponID", couponID)
response.ErrorWithMessage(c, response.ERROR, err.Error())
return
}
logger.Info("优惠券更新成功", "couponID", couponID)
response.Success(c, "更新成功")
}
// DeleteCoupon 删除优惠券
// @Summary 删除优惠券
// @Description 删除指定优惠券
// @Tags 管理员-优惠券管理
// @Accept json
// @Produce json
// @Param id path int true "优惠券ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.Response
// @Router /admin/coupons/{id} [delete]
func (h *AdminCouponHandler) DeleteCoupon(c *gin.Context) {
couponIDStr := c.Param("id")
couponID, err := strconv.ParseUint(couponIDStr, 10, 32)
if err != nil {
response.ErrorWithMessage(c, response.ERROR_INVALID_PARAMS, "无效的优惠券ID")
return
}
err = h.couponService.DeleteCoupon(uint(couponID))
if err != nil {
logger.Error("删除优惠券失败", "error", err, "couponID", couponID)
response.ErrorWithMessage(c, response.ERROR, err.Error())
return
}
logger.Info("优惠券删除成功", "couponID", couponID)
response.Success(c, "删除成功")
}
// UpdateCouponStatus 更新优惠券状态
// @Summary 更新优惠券状态
// @Description 启用或禁用优惠券
// @Tags 管理员-优惠券管理
// @Accept json
// @Produce json
// @Param id path int true "优惠券ID"
// @Param request body UpdateStatusRequest true "状态更新请求"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.Response
// @Router /admin/coupons/{id}/status [put]
func (h *AdminCouponHandler) UpdateCouponStatus(c *gin.Context) {
couponIDStr := c.Param("id")
couponID, err := strconv.ParseUint(couponIDStr, 10, 32)
if err != nil {
response.ErrorWithMessage(c, response.ERROR_INVALID_PARAMS, "无效的优惠券ID")
return
}
var req UpdateStatusRequest
if err := c.ShouldBindJSON(&req); err != nil {
logger.Error("绑定状态更新参数失败", "error", err)
response.ErrorWithMessage(c, response.ERROR_INVALID_PARAMS, "参数错误: "+err.Error())
return
}
err = h.couponService.UpdateCouponStatus(uint(couponID), req.Status)
if err != nil {
logger.Error("更新优惠券状态失败", "error", err, "couponID", couponID)
response.ErrorWithMessage(c, response.ERROR, err.Error())
return
}
logger.Info("优惠券状态更新成功", "couponID", couponID, "status", req.Status)
response.Success(c, "状态更新成功")
}
// BatchDeleteCoupons 批量删除优惠券
// @Summary 批量删除优惠券
// @Description 批量删除多个优惠券
// @Tags 管理员-优惠券管理
// @Accept json
// @Produce json
// @Param request body BatchDeleteRequest true "批量删除请求"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.Response
// @Router /admin/coupons/batch [delete]
func (h *AdminCouponHandler) BatchDeleteCoupons(c *gin.Context) {
var req BatchDeleteRequest
if err := c.ShouldBindJSON(&req); err != nil {
logger.Error("绑定批量删除参数失败", "error", err)
response.ErrorWithMessage(c, response.ERROR_INVALID_PARAMS, "参数错误: "+err.Error())
return
}
if len(req.IDs) == 0 {
response.ErrorWithMessage(c, response.ERROR_INVALID_PARAMS, "请选择要删除的优惠券")
return
}
err := h.couponService.BatchDeleteCoupons(req.IDs)
if err != nil {
logger.Error("批量删除优惠券失败", "error", err)
response.ErrorWithMessage(c, response.ERROR, err.Error())
return
}
logger.Info("批量删除优惠券成功", "count", len(req.IDs))
response.Success(c, "批量删除成功")
}
// GetCouponStatistics 获取优惠券统计
// @Summary 获取优惠券统计
// @Description 获取优惠券使用统计数据
// @Tags 管理员-优惠券管理
// @Accept json
// @Produce json
// @Param start_date query string false "开始日期"
// @Param end_date query string false "结束日期"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.Response
// @Router /admin/coupons/statistics [get]
func (h *AdminCouponHandler) GetCouponStatistics(c *gin.Context) {
startDate := c.Query("start_date")
endDate := c.Query("end_date")
// 如果没有提供日期默认查询最近30天
if startDate == "" || endDate == "" {
now := time.Now()
endDate = now.Format("2006-01-02")
startDate = now.AddDate(0, 0, -30).Format("2006-01-02")
}
// 解析日期
startTime, err := time.Parse("2006-01-02", startDate)
if err != nil {
response.ErrorWithMessage(c, response.ERROR_INVALID_PARAMS, "开始日期格式错误")
return
}
endTime, err := time.Parse("2006-01-02", endDate)
if err != nil {
response.ErrorWithMessage(c, response.ERROR_INVALID_PARAMS, "结束日期格式错误")
return
}
endTime = endTime.Add(23*time.Hour + 59*time.Minute + 59*time.Second)
stats, err := h.couponService.GetCouponStatistics(startTime, endTime)
if err != nil {
logger.Error("获取优惠券统计失败", "error", err)
response.ErrorWithMessage(c, response.ERROR, "获取优惠券统计失败")
return
}
response.Success(c, stats)
}
// GetUserCouponList 获取用户优惠券列表
// @Summary 获取用户优惠券列表
// @Description 查看指定用户的优惠券领取记录
// @Tags 管理员-优惠券管理
// @Accept json
// @Produce json
// @Param user_id query int true "用户ID"
// @Param page query int false "页码" default(1)
// @Param page_size query int false "每页数量" default(10)
// @Success 200 {object} response.Response
// @Failure 400 {object} response.Response
// @Router /admin/coupons/user-coupons [get]
func (h *AdminCouponHandler) GetUserCouponList(c *gin.Context) {
userIDStr := c.Query("user_id")
if userIDStr == "" {
response.ErrorWithMessage(c, response.ERROR_INVALID_PARAMS, "用户ID不能为空")
return
}
userID, err := strconv.ParseUint(userIDStr, 10, 32)
if err != nil {
response.ErrorWithMessage(c, response.ERROR_INVALID_PARAMS, "无效的用户ID")
return
}
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10"))
if page < 1 {
page = 1
}
if pageSize < 1 || pageSize > 100 {
pageSize = 10
}
coupons, total, err := h.couponService.GetUserCouponListForAdmin(uint(userID), page, pageSize)
if err != nil {
logger.Error("获取用户优惠券列表失败", "error", err)
response.ErrorWithMessage(c, response.ERROR, "获取用户优惠券列表失败")
return
}
response.Page(c, coupons, total, page, pageSize)
}
// DistributeCoupon 发放优惠券
// @Summary 发放优惠券
// @Description 给用户发放优惠券(单个/批量/全员)
// @Tags 管理员-优惠券管理
// @Accept json
// @Produce json
// @Param request body DistributeCouponRequest true "发放请求"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.Response
// @Router /admin/coupons/distribute [post]
func (h *AdminCouponHandler) DistributeCoupon(c *gin.Context) {
var req DistributeCouponRequest
if err := c.ShouldBindJSON(&req); err != nil {
logger.Error("绑定发放优惠券参数失败", "error", err)
response.ErrorWithMessage(c, response.ERROR_INVALID_PARAMS, "参数错误: "+err.Error())
return
}
// 验证参数
if !req.DistributeAll && len(req.UserIDs) == 0 {
response.ErrorWithMessage(c, response.ERROR_INVALID_PARAMS, "请选择用户或全员发放")
return
}
// 获取管理员ID
adminID, exists := c.Get("user_id")
if !exists {
response.Error(c, response.ERROR_UNAUTHORIZED)
return
}
// 调用服务层发放优惠券
result, err := h.couponService.DistributeCoupon(req.CouponID, req.UserIDs, req.DistributeAll, req.Quantity, adminID.(uint))
if err != nil {
logger.Error("发放优惠券失败", "error", err)
response.ErrorWithMessage(c, response.ERROR, err.Error())
return
}
logger.Info("优惠券发放成功", "couponID", req.CouponID, "totalCount", result["total_count"], "successCount", result["success_count"], "adminID", adminID)
response.Success(c, result)
}
// GetDistributeHistory 获取发放历史
// @Summary 获取发放历史
// @Description 获取优惠券发放记录
// @Tags 管理员-优惠券管理
// @Accept json
// @Produce json
// @Param page query int false "页码" default(1)
// @Param page_size query int false "每页数量" default(10)
// @Success 200 {object} response.Response
// @Failure 400 {object} response.Response
// @Router /admin/coupons/distribute/history [get]
func (h *AdminCouponHandler) GetDistributeHistory(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10"))
if page < 1 {
page = 1
}
if pageSize < 1 || pageSize > 100 {
pageSize = 10
}
history, total, err := h.couponService.GetDistributeHistory(page, pageSize)
if err != nil {
logger.Error("获取发放历史失败", "error", err)
response.ErrorWithMessage(c, response.ERROR, "获取发放历史失败")
return
}
response.Page(c, history, total, page, pageSize)
}
// 请求结构体
type CreateCouponRequest struct {
Name string `json:"name" binding:"required,max=100"`
Type int `json:"type" binding:"required,oneof=1 2 3"`
Value int64 `json:"value" binding:"required,min=1"`
MinAmount int64 `json:"min_amount" binding:"min=0"`
Description string `json:"description" binding:"max=500"`
StartTime time.Time `json:"start_time" binding:"required"`
EndTime time.Time `json:"end_time" binding:"required"`
TotalCount int `json:"total_count" binding:"min=0"`
Status int `json:"status" binding:"oneof=0 1"`
}
type UpdateCouponRequest struct {
Name string `json:"name" binding:"max=100"`
Type int `json:"type" binding:"oneof=1 2 3"`
Value int64 `json:"value" binding:"min=1"`
MinAmount int64 `json:"min_amount" binding:"min=0"`
Description string `json:"description" binding:"max=500"`
StartTime time.Time `json:"start_time"`
EndTime time.Time `json:"end_time"`
TotalCount int `json:"total_count" binding:"min=0"`
Status int `json:"status" binding:"oneof=0 1"`
}
type UpdateStatusRequest struct {
Status int `json:"status" binding:"required,oneof=0 1"`
}
type BatchDeleteRequest struct {
IDs []uint `json:"ids" binding:"required,min=1"`
}
type DistributeCouponRequest struct {
CouponID uint `json:"coupon_id" binding:"required"`
UserIDs []uint `json:"user_ids"`
DistributeAll bool `json:"distribute_all"`
Quantity int `json:"quantity" binding:"required,min=1,max=100"`
}

View File

@@ -109,6 +109,25 @@ func (h *AdminProductHandler) UpdateProduct(c *gin.Context) {
response.BadRequest(c, "请求参数错误")
return
}
// 处理 category_id 字段:转换为 JSONUintSlice 类型
if categoryIDRaw, ok := updates["category_id"]; ok {
switch v := categoryIDRaw.(type) {
case []interface{}:
var categoryIDs []uint
for _, item := range v {
switch id := item.(type) {
case float64:
categoryIDs = append(categoryIDs, uint(id))
case int:
categoryIDs = append(categoryIDs, uint(id))
}
}
updates["category_id"] = model.JSONUintSlice(categoryIDs)
case []uint:
updates["category_id"] = model.JSONUintSlice(v)
}
}
// 商品更新时间会自动设置
@@ -212,6 +231,7 @@ func (h *AdminProductHandler) CreateCategory(c *gin.Context) {
return
}
// 创建分类
if err := h.productService.CreateCategory(&category); err != nil {
response.ErrorWithMessage(c, response.ERROR, err.Error())
return
@@ -235,6 +255,40 @@ func (h *AdminProductHandler) UpdateCategory(c *gin.Context) {
return
}
// 处理 platform 字段:转换为 JSONSlice 类型
if platformRaw, ok := updates["platform"]; ok {
switch v := platformRaw.(type) {
case []interface{}:
// 前端传来的是数组
var platforms []string
for _, item := range v {
if str, ok := item.(string); ok {
platforms = append(platforms, str)
}
}
updates["platform"] = model.JSONSlice(platforms)
case string:
// 前端传来的是单个字符串,转为数组
updates["platform"] = model.JSONSlice([]string{v})
default:
// 其他类型,删除该字段
delete(updates, "platform")
}
}
// 删除只读字段,避免更新时出错
readonlyFields := []string{"id", "created_at", "updated_at", "children", "hasChildren", "level"}
for _, field := range readonlyFields {
delete(updates, field)
}
// 删除不存在的字段
nonExistFields := []string{"is_show", "keywords"}
for _, field := range nonExistFields {
delete(updates, field)
}
// 更新分类基本信息
if err := h.productService.UpdateCategory(uint(id), updates); err != nil {
response.ErrorWithMessage(c, response.ERROR, err.Error())
return

View File

@@ -36,9 +36,16 @@ func (h *CartHandler) GetCart(c *gin.Context) {
return
}
// 计算购物车统计信息
count, _ := h.cartService.GetCartCount(userID.(uint))
total, _ := h.cartService.GetCartTotal(userID.(uint))
// 优化: 在一次查询结果基础上计算统计信息,避免重复查询
var count int
var total float64
for _, item := range cart {
count += item.Quantity
if item.Product.ID != 0 {
// 将价格从分转换为元
total += (float64(item.Product.Price) / 100) * float64(item.Quantity)
}
}
result := map[string]interface{}{
"items": cart,

View File

@@ -137,6 +137,22 @@ func (h *CommentHandler) GetCommentStats(c *gin.Context) {
response.Success(c, stats)
}
// GetHighRatingComments 获取高分评论(用于首页展示)
func (h *CommentHandler) GetHighRatingComments(c *gin.Context) {
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "6"))
comments, err := h.commentService.GetHighRatingComments(limit)
if err != nil {
response.ErrorWithMessage(c, response.ERROR, err.Error())
return
}
// 转换为响应格式
result := h.convertToResponseList(comments)
response.Success(c, result)
}
// GetCommentDetail 获取评论详情
func (h *CommentHandler) GetCommentDetail(c *gin.Context) {
idStr := c.Param("id")

View File

@@ -57,14 +57,14 @@ func (h *FrontendHandler) GetHomeData(c *gin.Context) {
}
// 获取推荐商品
recommendProducts, _, err := h.productService.GetProductList(1, 10, 0, "", 0, 0, "default", "desc")
recommendProducts, _, err := h.productService.GetProductList(1, 10, 0, "", 0, 0, nil, "default", "desc")
if err != nil {
response.ErrorWithMessage(c, response.ERROR, "获取推荐商品失败: "+err.Error())
return
}
// 获取热门商品
hotProducts, _, err := h.productService.GetProductList(1, 10, 0, "", 0, 0, "sales", "desc")
hotProducts, _, err := h.productService.GetProductList(1, 10, 0, "", 0, 0, nil, "sales", "desc")
if err != nil {
response.ErrorWithMessage(c, response.ERROR, "获取热门商品失败: "+err.Error())
return
@@ -91,7 +91,7 @@ func (h *FrontendHandler) GetProductsRecommend(c *gin.Context) {
page := utils.StringToInt(c.DefaultQuery("page", "1"))
pageSize := utils.StringToInt(c.DefaultQuery("page_size", "10"))
products, pagination, err := h.productService.GetProductList(page, pageSize, 0, "", 0, 0, "default", "desc")
products, pagination, err := h.productService.GetProductList(page, pageSize, 0, "", 0, 0, nil, "default", "desc")
if err != nil {
response.ErrorWithMessage(c, response.ERROR, err.Error())
return
@@ -106,7 +106,7 @@ func (h *FrontendHandler) GetProductsHot(c *gin.Context) {
page := utils.StringToInt(c.DefaultQuery("page", "1"))
pageSize := utils.StringToInt(c.DefaultQuery("page_size", "10"))
products, pagination, err := h.productService.GetProductList(page, pageSize, 0, "", 0, 0, "sales", "desc")
products, pagination, err := h.productService.GetProductList(page, pageSize, 0, "", 0, 0, nil, "sales", "desc")
if err != nil {
response.ErrorWithMessage(c, response.ERROR, err.Error())
return
@@ -474,7 +474,13 @@ func (h *FrontendHandler) convertProductToFrontend(product *model.Product) model
SpuStockQuantity: product.Stock,
SoldNum: product.Sales,
IsPutOnSale: 1,
CategoryIds: []string{strconv.Itoa(int(product.CategoryID))},
CategoryIds: func() []string {
ids := make([]string, len(product.CategoryID))
for i, id := range product.CategoryID {
ids[i] = strconv.Itoa(int(id))
}
return ids
}(),
SpecList: specList,
SkuList: skuList,
SpuTagList: spuTagList,

View File

@@ -0,0 +1,243 @@
package handler
import (
"dianshang/internal/model"
"dianshang/internal/service"
"dianshang/pkg/response"
"strconv"
"time"
"github.com/gin-gonic/gin"
)
type LiveStreamHandler struct {
liveStreamService service.LiveStreamService
}
func NewLiveStreamHandler(liveStreamService service.LiveStreamService) *LiveStreamHandler {
return &LiveStreamHandler{
liveStreamService: liveStreamService,
}
}
// GetLiveStreamList 获取投流源列表
func (h *LiveStreamHandler) GetLiveStreamList(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
title := c.Query("title")
platform := c.Query("platform")
var status *int
if statusStr := c.Query("status"); statusStr != "" {
statusVal, err := strconv.Atoi(statusStr)
if err == nil {
status = &statusVal
}
}
streams, total, err := h.liveStreamService.GetLiveStreamList(page, pageSize, title, platform, status)
if err != nil {
response.Error(c, response.ERROR)
return
}
response.Success(c, gin.H{
"list": streams,
"total": total,
"page": page,
"size": pageSize,
})
}
// GetLiveStreamDetail 获取投流源详情
func (h *LiveStreamHandler) GetLiveStreamDetail(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
response.ErrorWithMessage(c, response.ERROR_INVALID_PARAMS, "投流源ID格式错误")
return
}
stream, err := h.liveStreamService.GetLiveStreamByID(uint(id))
if err != nil {
response.Error(c, response.ERROR)
return
}
response.Success(c, stream)
}
// GetActiveLiveStreams 获取启用的投流源(前台接口)
func (h *LiveStreamHandler) GetActiveLiveStreams(c *gin.Context) {
streams, err := h.liveStreamService.GetActiveLiveStreams()
if err != nil {
response.Error(c, response.ERROR)
return
}
response.Success(c, streams)
}
// CreateLiveStream 创建投流源
func (h *LiveStreamHandler) CreateLiveStream(c *gin.Context) {
var req struct {
Title string `json:"title" binding:"required"`
Platform string `json:"platform" binding:"required"`
StreamURL string `json:"stream_url" binding:"required"`
CoverImage string `json:"cover_image"`
Description string `json:"description"`
Status int `json:"status"`
Sort int `json:"sort"`
StartTime *time.Time `json:"start_time"`
EndTime *time.Time `json:"end_time"`
}
if err := c.ShouldBindJSON(&req); err != nil {
response.ErrorWithMessage(c, response.ERROR_INVALID_PARAMS, err.Error())
return
}
stream := &model.LiveStream{
Title: req.Title,
Platform: req.Platform,
StreamURL: req.StreamURL,
CoverImage: req.CoverImage,
Description: req.Description,
Status: req.Status,
Sort: req.Sort,
StartTime: req.StartTime,
EndTime: req.EndTime,
}
if err := h.liveStreamService.CreateLiveStream(stream); err != nil {
response.ErrorWithMessage(c, response.ERROR, err.Error())
return
}
response.Success(c, stream)
}
// UpdateLiveStream 更新投流源
func (h *LiveStreamHandler) UpdateLiveStream(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
response.ErrorWithMessage(c, response.ERROR_INVALID_PARAMS, "投流源ID格式错误")
return
}
var req struct {
Title string `json:"title"`
Platform string `json:"platform"`
StreamURL string `json:"stream_url"`
CoverImage string `json:"cover_image"`
Description string `json:"description"`
Status int `json:"status"`
Sort int `json:"sort"`
StartTime *time.Time `json:"start_time"`
EndTime *time.Time `json:"end_time"`
}
if err := c.ShouldBindJSON(&req); err != nil {
response.ErrorWithMessage(c, response.ERROR_INVALID_PARAMS, err.Error())
return
}
stream := &model.LiveStream{
Title: req.Title,
Platform: req.Platform,
StreamURL: req.StreamURL,
CoverImage: req.CoverImage,
Description: req.Description,
Status: req.Status,
Sort: req.Sort,
StartTime: req.StartTime,
EndTime: req.EndTime,
}
if err := h.liveStreamService.UpdateLiveStream(uint(id), stream); err != nil {
response.ErrorWithMessage(c, response.ERROR, err.Error())
return
}
response.Success(c, stream)
}
// UpdateLiveStreamStatus 更新投流源状态
func (h *LiveStreamHandler) UpdateLiveStreamStatus(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
response.ErrorWithMessage(c, response.ERROR_INVALID_PARAMS, "投流源ID格式错误")
return
}
var req struct {
Status int `json:"status" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
response.ErrorWithMessage(c, response.ERROR_INVALID_PARAMS, err.Error())
return
}
if err := h.liveStreamService.UpdateLiveStreamStatus(uint(id), req.Status); err != nil {
response.ErrorWithMessage(c, response.ERROR, err.Error())
return
}
response.Success(c, nil)
}
// DeleteLiveStream 删除投流源
func (h *LiveStreamHandler) DeleteLiveStream(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
response.ErrorWithMessage(c, response.ERROR_INVALID_PARAMS, "投流源ID格式错误")
return
}
if err := h.liveStreamService.DeleteLiveStream(uint(id)); err != nil {
response.ErrorWithMessage(c, response.ERROR, err.Error())
return
}
response.Success(c, nil)
}
// BatchDeleteLiveStreams 批量删除投流源
func (h *LiveStreamHandler) BatchDeleteLiveStreams(c *gin.Context) {
var req struct {
IDs []uint `json:"ids" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
response.ErrorWithMessage(c, response.ERROR_INVALID_PARAMS, err.Error())
return
}
if err := h.liveStreamService.BatchDeleteLiveStreams(req.IDs); err != nil {
response.ErrorWithMessage(c, response.ERROR, err.Error())
return
}
response.Success(c, nil)
}
// IncrementViewCount 增加观看次数
func (h *LiveStreamHandler) IncrementViewCount(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
response.ErrorWithMessage(c, response.ERROR_INVALID_PARAMS, "投流源ID格式错误")
return
}
if err := h.liveStreamService.IncrementViewCount(uint(id)); err != nil {
response.ErrorWithMessage(c, response.ERROR, err.Error())
return
}
response.Success(c, nil)
}

View File

@@ -11,19 +11,21 @@ import (
"github.com/gin-gonic/gin"
)
// OrderHandler 璁㈠崟澶勭悊鍣?
// OrderHandler 订单处理器
type OrderHandler struct {
orderService *service.OrderService
orderService *service.OrderService
wechatPayService *service.WeChatPayService
}
// NewOrderHandler 鍒涘缓璁㈠崟澶勭悊鍣?
func NewOrderHandler(orderService *service.OrderService) *OrderHandler {
// NewOrderHandler 创建订单处理器
func NewOrderHandler(orderService *service.OrderService, wechatPayService *service.WeChatPayService) *OrderHandler {
return &OrderHandler{
orderService: orderService,
orderService: orderService,
wechatPayService: wechatPayService,
}
}
// CreateOrder 鍒涘缓璁㈠崟
// CreateOrder 创建订单
func (h *OrderHandler) CreateOrder(c *gin.Context) {
userID, exists := c.Get("user_id")
if !exists {
@@ -50,7 +52,7 @@ func (h *OrderHandler) CreateOrder(c *gin.Context) {
response.Success(c, order)
}
// GetUserOrders 鑾峰彇鐢ㄦ埛璁㈠崟鍒楄〃
// GetUserOrders 获取用户订单列表
func (h *OrderHandler) GetUserOrders(c *gin.Context) {
userID, exists := c.Get("user_id")
if !exists {
@@ -422,7 +424,7 @@ func (h *OrderHandler) formatOrderDetail(order *model.Order) *OrderDetailRespons
}
}
// PayOrder 鏀粯璁㈠崟
// PayOrder 支付订单
func (h *OrderHandler) PayOrder(c *gin.Context) {
userID, exists := c.Get("user_id")
if !exists {
@@ -431,13 +433,66 @@ func (h *OrderHandler) PayOrder(c *gin.Context) {
}
// 从URL路径参数获取订单号
orderID := c.Param("id")
if orderID == "" {
orderNo := c.Param("id")
if orderNo == "" {
response.BadRequest(c, "订单号不能为空")
return
}
if err := h.orderService.PayOrder(userID.(uint), orderID); err != nil {
// 解析请求体获取支付方式
var req struct {
PaymentMethod string `json:"payment_method"`
}
if err := c.ShouldBindJSON(&req); err != nil {
// 如果没有提供支付方式,默认使用微信支付
req.PaymentMethod = "wechat"
}
// 获取订单详情
order, err := h.orderService.GetOrderByOrderNo(orderNo)
if err != nil {
response.ErrorWithMessage(c, response.ERROR, "订单不存在")
return
}
// 验证订单归属
if order.UserID != userID.(uint) {
response.ErrorWithMessage(c, response.ERROR, "无权限操作此订单")
return
}
// 验证订单状态
if order.Status != 1 { // 1 = 待付款
response.ErrorWithMessage(c, response.ERROR, "订单状态不允许支付")
return
}
// 如果是微信支付,返回支付二维码
if req.PaymentMethod == "wechat" {
// 调用微信Native扫码支付
if h.wechatPayService != nil {
paymentResp, err := h.wechatPayService.CreateNativeOrder(c.Request.Context(), order)
if err != nil {
response.ErrorWithMessage(c, response.ERROR, err.Error())
return
}
response.Success(c, paymentResp.Data)
return
} else {
// 如果没有微信支付服务,返回模拟数据
response.Success(c, gin.H{
"qrcode_url": "https://api.example.com/qrcode/" + orderNo,
"order_no": orderNo,
"amount": order.TotalAmount,
"sandbox": true,
})
return
}
}
// 其他支付方式,直接标记为已支付
if err := h.orderService.PayOrder(userID.(uint), orderNo); err != nil {
response.ErrorWithMessage(c, response.ERROR, err.Error())
return
}
@@ -445,7 +500,7 @@ func (h *OrderHandler) PayOrder(c *gin.Context) {
response.Success(c, nil)
}
// CancelOrder 鍙栨秷璁㈠崟
// CancelOrder 取消订单
func (h *OrderHandler) CancelOrder(c *gin.Context) {
userID, exists := c.Get("user_id")
if !exists {
@@ -534,7 +589,7 @@ func (h *OrderHandler) RefundOrder(c *gin.Context) {
response.Success(c, gin.H{"message": "退款申请已提交"})
}
// ConfirmReceive 纭鏀惰揣纭鏀惰揣
// ConfirmReceive 确认收货
func (h *OrderHandler) ConfirmReceive(c *gin.Context) {
userID, exists := c.Get("user_id")
if !exists {
@@ -558,7 +613,7 @@ func (h *OrderHandler) ConfirmReceive(c *gin.Context) {
response.Success(c, nil)
}
// GetOrderList 鑾峰彇璁㈠崟鍒楄〃锛堢鐞嗗憳锟?
// GetOrderList 获取订单列表(支持条件查询)
func (h *OrderHandler) GetOrderList(c *gin.Context) {
page := utils.StringToInt(c.DefaultQuery("page", "1"))
pageSize := utils.StringToInt(c.DefaultQuery("page_size", "20"))
@@ -589,7 +644,7 @@ func (h *OrderHandler) GetOrderList(c *gin.Context) {
response.Page(c, orders, pagination.Total, pagination.Page, pagination.PageSize)
}
// ShipOrder 鍙戣揣锛堢鐞嗗憳锟?
// ShipOrder 发货
func (h *OrderHandler) ShipOrder(c *gin.Context) {
var req struct {
OrderNo string `json:"order_no" binding:"required"`
@@ -609,7 +664,7 @@ func (h *OrderHandler) ShipOrder(c *gin.Context) {
response.Success(c, nil)
}
// GetOrderStatistics 鑾峰彇璁㈠崟缁熻
// GetOrderStatistics 获取订单统计
func (h *OrderHandler) GetOrderStatistics(c *gin.Context) {
statistics, err := h.orderService.GetOrderStatistics()
if err != nil {
@@ -620,7 +675,7 @@ func (h *OrderHandler) GetOrderStatistics(c *gin.Context) {
response.Success(c, statistics)
}
// GetDailyOrderStatistics 鑾峰彇姣忔棩璁㈠崟缁熻
// GetDailyOrderStatistics 获取每日订单统计
func (h *OrderHandler) GetDailyOrderStatistics(c *gin.Context) {
days := utils.StringToInt(c.DefaultQuery("days", "30"))

View File

@@ -0,0 +1,51 @@
package handler
import (
"github.com/gin-gonic/gin"
"dianshang/pkg/response"
)
// GetPaymentStatus 获取订单支付状态
func (h *OrderHandler) GetPaymentStatus(c *gin.Context) {
userID, exists := c.Get("user_id")
if !exists {
response.Unauthorized(c)
return
}
// 从URL路径参数获取订单号
orderNo := c.Param("id")
if orderNo == "" {
response.BadRequest(c, "订单号不能为空")
return
}
// 获取订单详情
order, err := h.orderService.GetOrderByOrderNo(orderNo)
if err != nil {
response.ErrorWithMessage(c, response.ERROR, "订单不存在")
return
}
// 验证订单归属
if order.UserID != userID.(uint) {
response.ErrorWithMessage(c, response.ERROR, "无权限操作此订单")
return
}
// 返回支付状态
status := "unpaid"
if order.Status == 2 || order.Status == 3 || order.Status == 4 || order.Status == 5 || order.Status == 6 {
status = "paid"
} else if order.Status == 7 {
status = "canceled"
} else if order.Status == 9 {
status = "refunded"
}
response.Success(c, gin.H{
"status": status,
"order_no": order.OrderNo,
"order_status": order.Status,
})
}

View File

@@ -3,6 +3,7 @@ package handler
import (
"dianshang/internal/service"
"dianshang/pkg/response"
"log"
"strconv"
"github.com/gin-gonic/gin"
@@ -169,37 +170,51 @@ func (h *PaymentHandler) CancelPayment(c *gin.Context) {
// PaymentNotify 支付回调通知
func (h *PaymentHandler) PaymentNotify(c *gin.Context) {
log.Printf("[=== 微信支付回调 ===] 收到回调请求")
log.Printf("[回调请求] 请求方法: %s", c.Request.Method)
log.Printf("[回调请求] 请求路径: %s", c.Request.URL.Path)
log.Printf("[回调请求] 客户端IP: %s", c.ClientIP())
// 读取回调数据
body, err := c.GetRawData()
if err != nil {
log.Printf("[回调错误] 读取回调数据失败: %v", err)
response.ErrorWithMessage(c, response.ERROR, "读取回调数据失败")
return
}
log.Printf("[回调数据] 数据长度: %d bytes", len(body))
// 获取请求头
headers := make(map[string]string)
for key, values := range c.Request.Header {
if len(values) > 0 {
headers[key] = values[0]
log.Printf("[回调请求头] %s: %s", key, values[0])
}
}
// 处理微信支付回调
log.Printf("[回调处理] 开始验证签名并解析数据...")
notify, err := h.wechatPayService.HandleNotify(c.Request.Context(), body, headers)
if err != nil {
log.Printf("[回调错误] 处理支付回调失败: %v", err)
response.ErrorWithMessage(c, response.ERROR, "处理支付回调失败: "+err.Error())
return
}
log.Printf("[回调数据] 事件类型: %s", notify.EventType)
// 根据回调类型处理
if notify.EventType == "TRANSACTION.SUCCESS" {
log.Printf("[支付成功] 开始处理支付成功回调...")
// 支付成功,更新订单状态
err = h.wechatPayService.ProcessPaymentSuccess(c.Request.Context(), notify)
if err != nil {
log.Printf("[回调错误] 处理支付成功回调失败: %v", err)
response.ErrorWithMessage(c, response.ERROR, "处理支付成功回调失败: "+err.Error())
return
}
log.Printf("[支付成功] 订单状态更新成功")
response.Success(c, gin.H{
"code": "SUCCESS",
"message": "处理成功",
@@ -207,6 +222,7 @@ func (h *PaymentHandler) PaymentNotify(c *gin.Context) {
return
}
log.Printf("[回调处理] 非支付成功事件,仅记录")
response.Success(c, gin.H{
"code": "SUCCESS",
"message": "回调已接收",

View File

@@ -0,0 +1,139 @@
package handler
import (
"dianshang/internal/model"
"dianshang/internal/service"
"dianshang/pkg/response"
"github.com/gin-gonic/gin"
"strconv"
)
// PlatformHandler 平台处理器
type PlatformHandler struct {
platformService *service.PlatformService
}
// NewPlatformHandler 创建平台处理器
func NewPlatformHandler(platformService *service.PlatformService) *PlatformHandler {
return &PlatformHandler{
platformService: platformService,
}
}
// GetPlatforms 获取平台列表
func (h *PlatformHandler) GetPlatforms(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10"))
name := c.Query("name")
var status *int
if statusStr := c.Query("status"); statusStr != "" {
if s, err := strconv.Atoi(statusStr); err == nil {
status = &s
}
}
platforms, pagination, err := h.platformService.GetPlatformList(page, pageSize, status, name)
if err != nil {
response.ErrorWithMessage(c, response.ERROR, err.Error())
return
}
if pagination != nil {
response.Page(c, platforms, pagination.Total, pagination.Page, pagination.PageSize)
} else {
response.Success(c, platforms)
}
}
// GetPlatform 获取平台详情
func (h *PlatformHandler) GetPlatform(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
response.ErrorWithMessage(c, response.ERROR_INVALID_PARAMS, "无效的平台ID")
return
}
platform, err := h.platformService.GetPlatformByID(uint(id))
if err != nil {
response.ErrorWithMessage(c, response.ERROR, err.Error())
return
}
response.Success(c, platform)
}
// CreatePlatform 创建平台
func (h *PlatformHandler) CreatePlatform(c *gin.Context) {
var platform model.Platform
if err := c.ShouldBindJSON(&platform); err != nil {
response.ErrorWithMessage(c, response.ERROR_INVALID_PARAMS, err.Error())
return
}
// 验证必填字段
if platform.Code == "" {
response.ErrorWithMessage(c, response.ERROR_INVALID_PARAMS, "平台代码不能为空")
return
}
if platform.Name == "" {
response.ErrorWithMessage(c, response.ERROR_INVALID_PARAMS, "平台名称不能为空")
return
}
if err := h.platformService.CreatePlatform(&platform); err != nil {
response.ErrorWithMessage(c, response.ERROR, err.Error())
return
}
response.Success(c, platform)
}
// UpdatePlatform 更新平台
func (h *PlatformHandler) UpdatePlatform(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
response.ErrorWithMessage(c, response.ERROR_INVALID_PARAMS, "无效的平台ID")
return
}
var updates map[string]interface{}
if err := c.ShouldBindJSON(&updates); err != nil {
response.ErrorWithMessage(c, response.ERROR_INVALID_PARAMS, err.Error())
return
}
if err := h.platformService.UpdatePlatform(uint(id), updates); err != nil {
response.ErrorWithMessage(c, response.ERROR, err.Error())
return
}
response.SuccessWithMessage(c, "平台更新成功", nil)
}
// DeletePlatform 删除平台
func (h *PlatformHandler) DeletePlatform(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
response.ErrorWithMessage(c, response.ERROR_INVALID_PARAMS, "无效的平台ID")
return
}
if err := h.platformService.DeletePlatform(uint(id)); err != nil {
response.ErrorWithMessage(c, response.ERROR, err.Error())
return
}
response.SuccessWithMessage(c, "平台删除成功", nil)
}
// GetAllActivePlatforms 获取所有启用的平台(用于下拉选择)
func (h *PlatformHandler) GetAllActivePlatforms(c *gin.Context) {
platforms, err := h.platformService.GetAllActivePlatforms()
if err != nil {
response.ErrorWithMessage(c, response.ERROR, err.Error())
return
}
response.Success(c, platforms)
}

View File

@@ -6,6 +6,7 @@ import (
"dianshang/pkg/response"
"dianshang/pkg/utils"
"strconv"
"strings"
"github.com/gin-gonic/gin"
)
@@ -26,11 +27,35 @@ func NewProductHandler(productService *service.ProductService) *ProductHandler {
func (h *ProductHandler) GetProductList(c *gin.Context) {
page := utils.StringToInt(c.DefaultQuery("page", "1"))
pageSize := utils.StringToInt(c.DefaultQuery("page_size", "20"))
categoryID := utils.StringToUint(c.Query("category_id"))
// 支持 category_ids (逗号分隔)或 category_id
var categoryID uint
if categoryIDsStr := c.Query("category_ids"); categoryIDsStr != "" {
// 解析第一个分类ID
ids := strings.Split(categoryIDsStr, ",")
if len(ids) > 0 {
categoryID = utils.StringToUint(strings.TrimSpace(ids[0]))
}
} else {
categoryID = utils.StringToUint(c.Query("category_id"))
}
keyword := c.Query("keyword")
minPrice, _ := strconv.ParseFloat(c.Query("min_price"), 64)
maxPrice, _ := strconv.ParseFloat(c.Query("max_price"), 64)
// 库存筛选参数
var inStock *bool
if inStockStr := c.Query("in_stock"); inStockStr != "" {
if inStockStr == "true" {
trueVal := true
inStock = &trueVal
} else if inStockStr == "false" {
falseVal := false
inStock = &falseVal
}
}
// 处理排序参数:将前端传递的数字参数转换为后端期望的字符串参数
sortParam := c.Query("sort")
sortTypeParam := c.Query("sortType")
@@ -52,7 +77,7 @@ func (h *ProductHandler) GetProductList(c *gin.Context) {
sortType = "desc"
}
products, pagination, err := h.productService.GetProductList(page, pageSize, categoryID, keyword, minPrice, maxPrice, sort, sortType)
products, pagination, err := h.productService.GetProductList(page, pageSize, categoryID, keyword, minPrice, maxPrice, inStock, sort, sortType)
if err != nil {
response.ErrorWithMessage(c, response.ERROR, err.Error())
return
@@ -137,7 +162,20 @@ func (h *ProductHandler) DeleteProduct(c *gin.Context) {
// GetCategories 鑾峰彇鍒嗙被鍒楄〃
func (h *ProductHandler) GetCategories(c *gin.Context) {
categories, err := h.productService.GetCategories()
// 支持平台筛选参数platform=web 或 platform=miniprogram
platform := c.Query("platform")
var categories []model.Category
var err error
if platform != "" {
// 根据平台获取分类
categories, err = h.productService.GetCategoriesByPlatform(platform)
} else {
// 获取所有分类
categories, err = h.productService.GetCategories()
}
if err != nil {
response.ErrorWithMessage(c, response.ERROR, err.Error())
return

View File

@@ -36,6 +36,19 @@ type LoginRequest struct {
Code string `json:"code" binding:"required"`
}
// EmailLoginRequest 邮箱登录请求结构
type EmailLoginRequest struct {
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required,min=6"`
}
// EmailRegisterRequest 邮箱注册请求结构
type EmailRegisterRequest struct {
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required,min=6"`
Nickname string `json:"nickname" binding:"required"`
}
// WeChatLoginRequest 微信登录请求结构
type WeChatLoginRequest struct {
Code string `json:"code" binding:"required"`
@@ -128,6 +141,56 @@ func (h *UserHandler) WeChatLogin(c *gin.Context) {
})
}
// EmailLogin 邮箱登录Web端使用
func (h *UserHandler) EmailLogin(c *gin.Context) {
var req EmailLoginRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "参数错误")
return
}
// 获取客户端IP和UserAgent
clientIP := utils.GetClientIP(
c.ClientIP(),
c.GetHeader("X-Forwarded-For"),
c.GetHeader("X-Real-IP"),
)
userAgent := c.Request.UserAgent()
// 调用用户服务进行邮箱登录
user, token, err := h.userService.EmailLogin(req.Email, req.Password, clientIP, userAgent)
if err != nil {
response.ErrorWithMessage(c, response.ERROR, err.Error())
return
}
response.Success(c, gin.H{
"user": user,
"token": token,
})
}
// EmailRegister 邮箱注册Web端使用
func (h *UserHandler) EmailRegister(c *gin.Context) {
var req EmailRegisterRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "参数错误")
return
}
// 调用用户服务进行注册
user, err := h.userService.EmailRegister(req.Email, req.Password, req.Nickname)
if err != nil {
response.ErrorWithMessage(c, response.ERROR, err.Error())
return
}
response.Success(c, gin.H{
"user_id": user.ID,
"message": "注册成功",
})
}
// GetWeChatSession 获取微信会话信息
func (h *UserHandler) GetWeChatSession(c *gin.Context) {
userID, exists := c.Get("user_id")

View File

@@ -3,6 +3,8 @@ package middleware
import (
"net/http"
"dianshang/pkg/logger"
"github.com/gin-gonic/gin"
)
@@ -11,8 +13,11 @@ func CORSMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
method := c.Request.Method
origin := c.Request.Header.Get("Origin")
// 记录 CORS 请求
logger.Debugf("[CORS] Method=%s, Origin=%s, Path=%s", method, origin, c.Request.URL.Path)
// 设置允许的域名
// 允许所有域名跨域访问
if origin != "" {
c.Header("Access-Control-Allow-Origin", origin)
} else {
@@ -21,22 +26,26 @@ func CORSMiddleware() gin.HandlerFunc {
// 设置允许的请求头
c.Header("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With, X-User-ID")
// 设置允许的请求方法
c.Header("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE, PATCH")
// 设置允许携带凭证
// 设置允许携带凭证Cookie等
c.Header("Access-Control-Allow-Credentials", "true")
// 设置预检请求的缓存时间
// 设置预检请求的缓存时间24小时
c.Header("Access-Control-Max-Age", "86400")
// 暴露的响应头(允许前端访问的自定义响应头)
c.Header("Access-Control-Expose-Headers", "Content-Length, Content-Type, Authorization")
// 处理预检请求
if method == "OPTIONS" {
logger.Infof("[CORS] 预检请求 Origin=%s, Path=%s", origin, c.Request.URL.Path)
c.AbortWithStatus(http.StatusNoContent)
return
}
c.Next()
}
}
}

View File

@@ -0,0 +1,27 @@
package model
import (
"time"
)
// LiveStream 直播投流源模型
type LiveStream struct {
ID uint `json:"id" gorm:"primaryKey;autoIncrement"`
Title string `json:"title" gorm:"type:varchar(255);not null;comment:投流源标题"`
Platform string `json:"platform" gorm:"type:varchar(50);not null;comment:平台名称(如:抖音,快手,淘宝,京东,小红书等)"`
StreamURL string `json:"stream_url" gorm:"type:varchar(500);not null;comment:投流URL地址"`
CoverImage string `json:"cover_image" gorm:"type:varchar(500);comment:封面图片URL"`
Description string `json:"description" gorm:"type:text;comment:描述信息"`
Status int `json:"status" gorm:"type:tinyint;not null;default:1;comment:状态:0-禁用,1-启用"`
Sort int `json:"sort" gorm:"type:int;not null;default:0;comment:排序值,数值越大越靠前"`
ViewCount int `json:"view_count" gorm:"type:int;not null;default:0;comment:观看次数"`
StartTime *time.Time `json:"start_time" gorm:"comment:开始时间"`
EndTime *time.Time `json:"end_time" gorm:"comment:结束时间"`
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
}
// TableName 指定表名
func (LiveStream) TableName() string {
return "ai_live_streams"
}

View File

@@ -0,0 +1,21 @@
package model
import "time"
// Platform 平台配置模型
type Platform struct {
ID uint `json:"id" gorm:"primaryKey;autoIncrement"`
Code string `json:"code" gorm:"size:50;uniqueIndex;not null;comment:平台代码,如web,miniprogram"`
Name string `json:"name" gorm:"size:100;not null;comment:平台名称"`
Description string `json:"description" gorm:"size:255;comment:平台描述"`
Icon string `json:"icon" gorm:"size:255;comment:平台图标"`
Sort int `json:"sort" gorm:"default:0;comment:排序值"`
Status int `json:"status" gorm:"default:1;comment:状态:0-禁用,1-启用"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// TableName 指定表名
func (Platform) TableName() string {
return "ai_platforms"
}

View File

@@ -8,6 +8,25 @@ import (
"gorm.io/gorm"
)
// JSONUintSlice 自定义JSON uint切片类型
type JSONUintSlice []uint
func (j JSONUintSlice) Value() (driver.Value, error) {
return json.Marshal(j)
}
func (j *JSONUintSlice) Scan(value interface{}) error {
if value == nil {
*j = nil
return nil
}
bytes, ok := value.([]byte)
if !ok {
return nil
}
return json.Unmarshal(bytes, j)
}
// JSONSlice 自定义JSON切片类型
type JSONSlice []string
@@ -81,47 +100,50 @@ type Category struct {
Level int `json:"level" gorm:"default:1"`
Icon string `json:"icon" gorm:"size:255"`
Description string `json:"description" gorm:"size:255"`
Platform JSONSlice `json:"platform" gorm:"type:json;comment:平台标识列表"`
Sort int `json:"sort" gorm:"default:0"`
Status int `json:"status" gorm:"default:1"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
// 关联字段
Children []Category `json:"children,omitempty" gorm:"-"`
HasChildren bool `json:"hasChildren" gorm:"-"`
}
// Product 商品
type Product struct {
ID uint `json:"id" gorm:"primaryKey;autoIncrement"`
CategoryID uint `json:"category_id" gorm:"not null"`
StoreID uint `json:"store_id" gorm:"default:1"`
Name string `json:"name" gorm:"size:100;not null"`
Description string `json:"description" gorm:"type:text"`
Price float64 `json:"price" gorm:"type:decimal(10,2);not null"`
OrigPrice float64 `json:"orig_price" gorm:"type:decimal(10,2)"`
Stock int `json:"stock" gorm:"default:0"`
Sales int `json:"sales" gorm:"default:0"`
CommentCount int `json:"comment_count" gorm:"default:0"`
AverageRating float64 `json:"average_rating" gorm:"type:decimal(3,2);default:0.00"`
MainImage string `json:"main_image" gorm:"size:255"`
Images JSONSlice `json:"images" gorm:"type:json"`
VideoURL string `json:"video_url" gorm:"size:255"`
DetailImages JSONSlice `json:"detail_images" gorm:"type:json"`
Status int `json:"status" gorm:"default:1"`
IsHot bool `json:"is_hot" gorm:"default:false"`
IsNew bool `json:"is_new" gorm:"default:false"`
IsRecommend bool `json:"is_recommend" gorm:"default:false"`
LimitBuy int `json:"limit_buy" gorm:"default:0"`
Points int `json:"points" gorm:"default:0"`
Level int `json:"level" gorm:"default:0"`
Weight float64 `json:"weight" gorm:"type:decimal(8,2)"`
Unit string `json:"unit" gorm:"size:20"`
Sort int `json:"sort" gorm:"default:0"`
ID uint `json:"id" gorm:"primaryKey;autoIncrement"`
CategoryID JSONUintSlice `json:"category_id" gorm:"type:json;comment:分类ID列表"` // 改为JSON数组支持多分类
StoreID uint `json:"store_id" gorm:"default:1"`
Name string `json:"name" gorm:"size:100;not null"`
Description string `json:"description" gorm:"type:text"`
Price float64 `json:"price" gorm:"type:decimal(10,2);not null"`
OrigPrice float64 `json:"orig_price" gorm:"type:decimal(10,2)"`
Stock int `json:"stock" gorm:"default:0"`
Sales int `json:"sales" gorm:"default:0"`
CommentCount int `json:"comment_count" gorm:"default:0"`
AverageRating float64 `json:"average_rating" gorm:"type:decimal(3,2);default:0.00"`
MainImage string `json:"main_image" gorm:"size:255"`
Images JSONSlice `json:"images" gorm:"type:json"`
VideoURL string `json:"video_url" gorm:"size:255"`
DetailImages JSONSlice `json:"detail_images" gorm:"type:json"`
Status int `json:"status" gorm:"default:1"`
IsHot bool `json:"is_hot" gorm:"default:false"`
IsNew bool `json:"is_new" gorm:"default:false"`
IsRecommend bool `json:"is_recommend" gorm:"default:false"`
LimitBuy int `json:"limit_buy" gorm:"default:0"`
Points int `json:"points" gorm:"default:0"`
Level int `json:"level" gorm:"default:0"`
Weight float64 `json:"weight" gorm:"type:decimal(8,2)"`
Unit string `json:"unit" gorm:"size:20"`
Sort int `json:"sort" gorm:"default:0"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `json:"deleted_at,omitempty" gorm:"index"`
// 关联数据
Category Category `json:"category,omitempty" gorm:"foreignKey:CategoryID"`
Categories []Category `json:"categories,omitempty" gorm:"-"` // 读取时填充的分类信息
Store Store `json:"store,omitempty" gorm:"foreignKey:StoreID"`
SKUs []ProductSKU `json:"skus,omitempty" gorm:"foreignKey:ProductID"`
Tags []ProductTag `json:"tags,omitempty" gorm:"many2many:ai_product_tag_relations;"`

View File

@@ -15,7 +15,8 @@ type User struct {
Avatar string `json:"avatar" gorm:"size:255"`
Gender int `json:"gender" gorm:"default:0"` // 0未知1男2女
Phone string `json:"phone" gorm:"size:20"`
Email string `json:"email" gorm:"size:100"`
Email string `json:"email" gorm:"size:100;index"`
Password string `json:"-" gorm:"size:255"` // Web端邮箱登录密码不返回给前端
Birthday *time.Time `json:"birthday"`
Points int `json:"points" gorm:"default:0"`
Level int `json:"level" gorm:"default:1"`

View File

@@ -202,6 +202,22 @@ func (r *CommentRepository) CreateReply(reply *model.CommentReply) error {
return tx.Commit().Error
}
// GetHighRatingComments 获取高分评论(用于首页展示)
func (r *CommentRepository) GetHighRatingComments(limit int, minRating int) ([]model.Comment, error) {
var comments []model.Comment
// 获取评分>=minRating的评论按评分降序、创建时间降序排列
err := r.db.Model(&model.Comment{}).
Where("status = ? AND rating >= ?", 1, minRating).
Preload("User").
Preload("Product").
Order("rating DESC, created_at DESC").
Limit(limit).
Find(&comments).Error
return comments, err
}
// GetReplies 获取评论回复列表
func (r *CommentRepository) GetReplies(commentID uint) ([]model.CommentReply, error) {
var replies []model.CommentReply

View File

@@ -1,6 +1,7 @@
package repository
import (
"context"
"dianshang/internal/model"
"time"
@@ -121,3 +122,286 @@ func (r *CouponRepository) RestoreCoupon(userCouponID uint) error {
"used_time": nil, // 清除使用时间
}).Error
}
// ==================== 管理端方法 ====================
// GetCouponListForAdmin 获取优惠券列表(管理端)
func (r *CouponRepository) GetCouponListForAdmin(page, pageSize int, status, couponType, keyword string) ([]model.Coupon, int64, error) {
var coupons []model.Coupon
var total int64
query := r.db.Model(&model.Coupon{})
// 状态筛选
if status != "" {
query = query.Where("status = ?", status)
}
// 类型筛选
if couponType != "" {
query = query.Where("type = ?", couponType)
}
// 关键词搜索
if keyword != "" {
query = query.Where("name LIKE ? OR description LIKE ?", "%"+keyword+"%", "%"+keyword+"%")
}
// 获取总数
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
// 分页查询
offset := (page - 1) * pageSize
err := query.Order("created_at DESC").
Limit(pageSize).
Offset(offset).
Find(&coupons).Error
return coupons, total, err
}
// GetCouponUsageStats 获取优惠券使用统计
func (r *CouponRepository) GetCouponUsageStats(couponID uint) (int, int, error) {
// 获取领取数
var receivedCount int64
err := r.db.Model(&model.UserCoupon{}).Where("coupon_id = ?", couponID).Count(&receivedCount).Error
if err != nil {
return 0, 0, err
}
// 获取使用数
var usedCount int64
err = r.db.Model(&model.UserCoupon{}).Where("coupon_id = ? AND status = ?", couponID, 1).Count(&usedCount).Error
if err != nil {
return 0, 0, err
}
return int(receivedCount), int(usedCount), nil
}
// Create 创建优惠券
func (r *CouponRepository) Create(coupon *model.Coupon) error {
return r.db.Create(coupon).Error
}
// Update 更新优惠券
func (r *CouponRepository) Update(couponID uint, updates map[string]interface{}) error {
return r.db.Model(&model.Coupon{}).Where("id = ?", couponID).Updates(updates).Error
}
// Delete 删除优惠券
func (r *CouponRepository) Delete(couponID uint) error {
return r.db.Delete(&model.Coupon{}, couponID).Error
}
// CheckCouponHasUsers 检查优惠券是否有用户领取
func (r *CouponRepository) CheckCouponHasUsers(couponID uint) (bool, error) {
var count int64
err := r.db.Model(&model.UserCoupon{}).Where("coupon_id = ?", couponID).Count(&count).Error
return count > 0, err
}
// BatchDelete 批量删除优惠券
func (r *CouponRepository) BatchDelete(couponIDs []uint) error {
return r.db.Delete(&model.Coupon{}, couponIDs).Error
}
// CountTotalCoupons 统计总优惠券数
func (r *CouponRepository) CountTotalCoupons(ctx context.Context) (int64, error) {
var count int64
err := r.db.WithContext(ctx).Model(&model.Coupon{}).Count(&count).Error
return count, err
}
// CountActiveCoupons 统计启用的优惠券数
func (r *CouponRepository) CountActiveCoupons(ctx context.Context) (int64, error) {
var count int64
now := time.Now()
err := r.db.WithContext(ctx).Model(&model.Coupon{}).
Where("status = ? AND start_time <= ? AND end_time >= ?", 1, now, now).
Count(&count).Error
return count, err
}
// CountTotalReceived 统计总领取数
func (r *CouponRepository) CountTotalReceived(ctx context.Context, startTime, endTime time.Time) (int64, error) {
var count int64
query := r.db.WithContext(ctx).Model(&model.UserCoupon{})
if !startTime.IsZero() && !endTime.IsZero() {
query = query.Where("created_at BETWEEN ? AND ?", startTime, endTime)
}
err := query.Count(&count).Error
return count, err
}
// CountTotalUsed 统计总使用数
func (r *CouponRepository) CountTotalUsed(ctx context.Context, startTime, endTime time.Time) (int64, error) {
var count int64
query := r.db.WithContext(ctx).Model(&model.UserCoupon{}).Where("status = ?", 1)
if !startTime.IsZero() && !endTime.IsZero() {
query = query.Where("used_time BETWEEN ? AND ?", startTime, endTime)
}
err := query.Count(&count).Error
return count, err
}
// GetCouponTypeStats 获取各类型优惠券统计
func (r *CouponRepository) GetCouponTypeStats(ctx context.Context) ([]map[string]interface{}, error) {
var results []map[string]interface{}
rows, err := r.db.WithContext(ctx).Model(&model.Coupon{}).
Select("type, COUNT(*) as count").
Group("type").
Rows()
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
var couponType, count int
if err := rows.Scan(&couponType, &count); err != nil {
return nil, err
}
typeName := ""
switch couponType {
case 1:
typeName = "满减券"
case 2:
typeName = "折扣券"
case 3:
typeName = "免邮券"
}
results = append(results, map[string]interface{}{
"type": couponType,
"type_name": typeName,
"count": count,
})
}
return results, nil
}
// GetTopCoupons 获取热门优惠券
func (r *CouponRepository) GetTopCoupons(ctx context.Context, limit int) ([]map[string]interface{}, error) {
var results []map[string]interface{}
err := r.db.WithContext(ctx).Model(&model.Coupon{}).
Select("id, name, type, received_count, used_count").
Order("received_count DESC").
Limit(limit).
Scan(&results).Error
return results, err
}
// GetUserCouponListForAdmin 获取用户优惠券列表(管理端)
func (r *CouponRepository) GetUserCouponListForAdmin(userID uint, page, pageSize int) ([]model.UserCoupon, int64, error) {
var userCoupons []model.UserCoupon
var total int64
query := r.db.Model(&model.UserCoupon{}).Where("user_id = ?", userID)
// 获取总数
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
// 分页查询
offset := (page - 1) * pageSize
err := query.Preload("Coupon").
Order("created_at DESC").
Limit(pageSize).
Offset(offset).
Find(&userCoupons).Error
return userCoupons, total, err
}
// GetDistributeHistory 获取优惠券发放历史
func (r *CouponRepository) GetDistributeHistory(page, pageSize int) ([]map[string]interface{}, int64, error) {
var history []map[string]interface{}
var total int64
// 先获取总数
type CountResult struct {
Count int64
}
var countResult CountResult
err := r.db.Model(&model.UserCoupon{}).
Select("COUNT(DISTINCT DATE(created_at), coupon_id) as count").
Scan(&countResult).Error
if err != nil {
return nil, 0, err
}
total = countResult.Count
// 分页查询分组数据
type DistributeRecord struct {
DistributeDate string `gorm:"column:distribute_date"`
CouponID uint `gorm:"column:coupon_id"`
TotalCount int `gorm:"column:total_count"`
UnusedCount int `gorm:"column:unused_count"`
UsedCount int `gorm:"column:used_count"`
CreatedAt time.Time `gorm:"column:created_at"`
}
var records []DistributeRecord
offset := (page - 1) * pageSize
err = r.db.Model(&model.UserCoupon{}).
Select(`
DATE(created_at) as distribute_date,
coupon_id,
COUNT(*) as total_count,
SUM(CASE WHEN status = 0 THEN 1 ELSE 0 END) as unused_count,
SUM(CASE WHEN status = 1 THEN 1 ELSE 0 END) as used_count,
MIN(created_at) as created_at
`).
Group("DATE(created_at), coupon_id").
Order("created_at DESC").
Limit(pageSize).
Offset(offset).
Scan(&records).Error
if err != nil {
return nil, 0, err
}
// 处理结果
for i, record := range records {
// 获取优惠券信息
var coupon model.Coupon
r.db.First(&coupon, record.CouponID)
// 判断发放类型
distributeType := "batch"
if record.TotalCount == 1 {
distributeType = "single"
}
history = append(history, map[string]interface{}{
"id": i + 1 + offset,
"coupon_id": record.CouponID,
"coupon_name": coupon.Name,
"distribute_type": distributeType,
"distribute_date": record.DistributeDate,
"total_count": record.TotalCount,
"success_count": record.TotalCount,
"fail_count": 0,
"used_count": record.UsedCount,
"unused_count": record.UnusedCount,
"admin_name": "系统",
"created_at": record.CreatedAt,
})
}
return history, total, nil
}

View File

@@ -0,0 +1,124 @@
package repository
import (
"dianshang/internal/model"
"time"
"gorm.io/gorm"
)
type LiveStreamRepository interface {
GetList(page, pageSize int, title, platform string, status *int) ([]model.LiveStream, int64, error)
GetByID(id uint) (*model.LiveStream, error)
GetActiveLiveStreams() ([]model.LiveStream, error)
Create(stream *model.LiveStream) error
Update(id uint, stream *model.LiveStream) error
UpdateStatus(id uint, status int) error
Delete(id uint) error
BatchDelete(ids []uint) error
IncrementViewCount(id uint) error
}
type liveStreamRepository struct {
db *gorm.DB
}
func NewLiveStreamRepository(db *gorm.DB) LiveStreamRepository {
return &liveStreamRepository{db: db}
}
// GetList 获取投流源列表
func (r *liveStreamRepository) GetList(page, pageSize int, title, platform string, status *int) ([]model.LiveStream, int64, error) {
var streams []model.LiveStream
var total int64
query := r.db.Model(&model.LiveStream{})
// 标题筛选
if title != "" {
query = query.Where("title LIKE ?", "%"+title+"%")
}
// 平台筛选
if platform != "" {
query = query.Where("platform = ?", platform)
}
// 状态筛选
if status != nil {
query = query.Where("status = ?", *status)
}
// 统计总数
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
// 分页查询,按排序和创建时间排序
offset := (page - 1) * pageSize
if err := query.Order("sort DESC, created_at DESC").
Limit(pageSize).
Offset(offset).
Find(&streams).Error; err != nil {
return nil, 0, err
}
return streams, total, nil
}
// GetByID 根据ID获取投流源详情
func (r *liveStreamRepository) GetByID(id uint) (*model.LiveStream, error) {
var stream model.LiveStream
if err := r.db.First(&stream, id).Error; err != nil {
return nil, err
}
return &stream, nil
}
// GetActiveLiveStreams 获取所有启用且在有效期内的投流源
func (r *liveStreamRepository) GetActiveLiveStreams() ([]model.LiveStream, error) {
var streams []model.LiveStream
now := time.Now()
query := r.db.Where("status = ?", 1)
// 查询有效时间范围内的投流源
query = query.Where("(start_time IS NULL OR start_time <= ?) AND (end_time IS NULL OR end_time >= ?)", now, now)
if err := query.Order("sort DESC, created_at DESC").Find(&streams).Error; err != nil {
return nil, err
}
return streams, nil
}
// Create 创建投流源
func (r *liveStreamRepository) Create(stream *model.LiveStream) error {
return r.db.Create(stream).Error
}
// Update 更新投流源
func (r *liveStreamRepository) Update(id uint, stream *model.LiveStream) error {
return r.db.Model(&model.LiveStream{}).Where("id = ?", id).Updates(stream).Error
}
// UpdateStatus 更新投流源状态
func (r *liveStreamRepository) UpdateStatus(id uint, status int) error {
return r.db.Model(&model.LiveStream{}).Where("id = ?", id).Update("status", status).Error
}
// Delete 删除投流源
func (r *liveStreamRepository) Delete(id uint) error {
return r.db.Delete(&model.LiveStream{}, id).Error
}
// BatchDelete 批量删除投流源
func (r *liveStreamRepository) BatchDelete(ids []uint) error {
return r.db.Delete(&model.LiveStream{}, ids).Error
}
// IncrementViewCount 增加观看次数
func (r *liveStreamRepository) IncrementViewCount(id uint) error {
return r.db.Model(&model.LiveStream{}).Where("id = ?", id).
UpdateColumn("view_count", gorm.Expr("view_count + ?", 1)).Error
}

View File

@@ -192,9 +192,12 @@ func (r *OrderRepository) UpdateOrderItem(id uint, updates map[string]interface{
}
// GetCart 获取购物车
// 优化: 减少不必要的Preload,只加载必需的关联数据
func (r *OrderRepository) GetCart(userID uint) ([]model.Cart, error) {
var cart []model.Cart
err := r.db.Preload("Product").Preload("Product.SKUs", "status = ?", 1).Preload("SKU").Where("user_id = ?", userID).Find(&cart).Error
// 移除 Product.SKUs 的预加载,因为购物车已经有单独的SKU字段
// 只保留必要的Product和SKU信息
err := r.db.Preload("Product").Preload("SKU").Where("user_id = ?", userID).Find(&cart).Error
return cart, err
}

View File

@@ -0,0 +1,55 @@
package repository
import (
"dianshang/internal/model"
"gorm.io/gorm"
)
type PlatformRepository struct {
db *gorm.DB
}
func NewPlatformRepository(db *gorm.DB) *PlatformRepository {
return &PlatformRepository{db: db}
}
// GetAll 获取所有平台
func (r *PlatformRepository) GetAll() ([]model.Platform, error) {
var platforms []model.Platform
err := r.db.Where("status = ?", 1).Order("sort DESC, created_at ASC").Find(&platforms).Error
return platforms, err
}
// GetByID 根据ID获取平台
func (r *PlatformRepository) GetByID(id uint) (*model.Platform, error) {
var platform model.Platform
err := r.db.Where("id = ?", id).First(&platform).Error
return &platform, err
}
// GetByCode 根据代码获取平台
func (r *PlatformRepository) GetByCode(code string) (*model.Platform, error) {
var platform model.Platform
err := r.db.Where("code = ? AND status = ?", code, 1).First(&platform).Error
return &platform, err
}
// Create 创建平台
func (r *PlatformRepository) Create(platform *model.Platform) error {
return r.db.Create(platform).Error
}
// Update 更新平台
func (r *PlatformRepository) Update(id uint, updates map[string]interface{}) error {
return r.db.Model(&model.Platform{}).Where("id = ?", id).Updates(updates).Error
}
// Delete 删除平台
func (r *PlatformRepository) Delete(id uint) error {
return r.db.Delete(&model.Platform{}, id).Error
}
// GetDB 获取数据库连接
func (r *PlatformRepository) GetDB() *gorm.DB {
return r.db
}

View File

@@ -2,6 +2,8 @@ package repository
import (
"dianshang/internal/model"
"fmt"
"strings"
"gorm.io/gorm"
)
@@ -46,7 +48,7 @@ func (r *ProductRepository) GetList(offset, limit int, conditions map[string]int
for key, value := range conditions {
switch key {
case "category_id":
// 支持包含子分类的筛选
// category_id 现在是 JSON 数组,使用 JSON_CONTAINS 查询
var catID uint
switch v := value.(type) {
case uint:
@@ -57,12 +59,21 @@ func (r *ProductRepository) GetList(offset, limit int, conditions map[string]int
catID = uint(v)
}
if catID > 0 {
// 获取包含子分类的所有分类ID
categoryIDs, err := r.getCategoryIDsIncludingChildren(catID)
if err == nil && len(categoryIDs) > 0 {
query = query.Where("category_id IN (?)", categoryIDs)
// 使用 JSON_CONTAINS 查询包含任意一个分类ID的商品
// 构建 OR 条件JSON_CONTAINS(category_id, '1') OR JSON_CONTAINS(category_id, '2') ...
conditions := make([]string, len(categoryIDs))
args := make([]interface{}, len(categoryIDs))
for i, id := range categoryIDs {
conditions[i] = "JSON_CONTAINS(category_id, ?)"
args[i] = fmt.Sprintf("%d", id)
}
query = query.Where(strings.Join(conditions, " OR "), args...)
} else {
// 兜底:如果获取子分类失败,退化为当前分类
query = query.Where("category_id = ?", catID)
// 兜底:如果获取子分类失败,只查询当前分类
query = query.Where("JSON_CONTAINS(category_id, ?)", fmt.Sprintf("%d", catID))
}
}
case "keyword":
@@ -71,6 +82,15 @@ func (r *ProductRepository) GetList(offset, limit int, conditions map[string]int
query = query.Where("price >= ?", value)
case "max_price":
query = query.Where("price <= ?", value)
case "in_stock":
// 库存筛选true=有货false=缺货
if inStockValue, ok := value.(bool); ok {
if inStockValue {
query = query.Where("stock > ?", 0)
} else {
query = query.Where("stock = ?", 0)
}
}
case "is_hot":
if value.(string) == "true" {
query = query.Where("is_hot = ?", true)
@@ -129,16 +149,15 @@ func (r *ProductRepository) GetList(offset, limit int, conditions map[string]int
}
}
// 获取列表,预加载分类
err := query.Preload("Category").
Offset(offset).Limit(limit).Order(orderBy).Find(&products).Error
// 获取列表
err := query.Offset(offset).Limit(limit).Order(orderBy).Find(&products).Error
return products, total, err
}
// GetByID 根据ID获取产品详情
func (r *ProductRepository) GetByID(id uint) (*model.Product, error) {
var product model.Product
err := r.db.Preload("Category").Preload("Specs").Preload("SKUs", "status = ?", 1).
err := r.db.Preload("Specs").Preload("SKUs", "status = ?", 1).
Where("id = ?", id).First(&product).Error
return &product, err
}
@@ -216,8 +235,21 @@ func (r *ProductRepository) RestoreStock(id uint, quantity int) error {
// GetCategories 获取分类列表
func (r *ProductRepository) GetCategories() ([]model.Category, error) {
return r.GetCategoriesByPlatform("")
}
// GetCategoriesByPlatform 根据平台获取分类列表
func (r *ProductRepository) GetCategoriesByPlatform(platformCode string) ([]model.Category, error) {
var allCategories []model.Category
err := r.db.Where("status = ?", 1).Order("level ASC, sort DESC, created_at ASC").Find(&allCategories).Error
query := r.db.Where("status = ?", 1)
// 如果指定了平台,筛选包含该平台的分类
if platformCode != "" {
// 使用 JSON_CONTAINS 查询包含指定平台的分类
query = query.Where("JSON_CONTAINS(platform, ?)", `"`+platformCode+`"`)
}
err := query.Order("level ASC, sort DESC, created_at ASC").Find(&allCategories).Error
if err != nil {
return nil, err
}
@@ -409,8 +441,7 @@ func (r *ProductRepository) DeleteProductSpec(id uint) error {
// GetHotProducts 获取热门产品
func (r *ProductRepository) GetHotProducts(limit int) ([]model.Product, error) {
var products []model.Product
err := r.db.Preload("Category").
Where("status = ? AND is_hot = ?", 1, 1).
err := r.db.Where("status = ? AND is_hot = ?", 1, 1).
Order("sales DESC, created_at DESC").Limit(limit).Find(&products).Error
return products, err
}
@@ -418,8 +449,7 @@ func (r *ProductRepository) GetHotProducts(limit int) ([]model.Product, error) {
// GetRecommendProducts 获取推荐产品
func (r *ProductRepository) GetRecommendProducts(limit int) ([]model.Product, error) {
var products []model.Product
err := r.db.Preload("Category").
Where("status = ? AND is_recommend = ?", 1, 1).
err := r.db.Where("status = ? AND is_recommend = ?", 1, 1).
Order("sort DESC, created_at DESC").Limit(limit).Find(&products).Error
return products, err
}
@@ -660,7 +690,7 @@ func (r *ProductRepository) AssignTagsToProduct(productID uint, tagIDs []uint) e
func (r *ProductRepository) GetLowStockProducts(threshold int) ([]model.Product, error) {
var products []model.Product
err := r.db.Where("stock <= ? AND status = ?", threshold, 1).
Preload("Category").Find(&products).Error
Find(&products).Error
return products, err
}
@@ -698,7 +728,7 @@ func (r *ProductRepository) GetInventoryStatistics() (map[string]interface{}, er
// GetProductsForExport 获取用于导出的商品数据
func (r *ProductRepository) GetProductsForExport(conditions map[string]interface{}) ([]model.Product, error) {
var products []model.Product
query := r.db.Model(&model.Product{}).Preload("Category")
query := r.db.Model(&model.Product{})
// 添加查询条件
for key, value := range conditions {

View File

@@ -35,6 +35,13 @@ func (r *UserRepository) GetByOpenID(openID string) (*model.User, error) {
return &user, err
}
// GetByEmail 根据邮箱获取用户
func (r *UserRepository) GetByEmail(email string) (*model.User, error) {
var user model.User
err := r.db.Where("email = ?", email).First(&user).Error
return &user, err
}
// Update 更新用户
func (r *UserRepository) Update(id uint, updates map[string]interface{}) error {
return r.db.Model(&model.User{}).Where("id = ?", id).Updates(updates).Error

View File

@@ -35,6 +35,8 @@ func Setup(db *gorm.DB, cfg *config.Config) *gin.Engine {
pointsRepo := repository.NewPointsRepository(db)
refundRepo := repository.NewRefundRepository(db)
commentRepo := repository.NewCommentRepository(db)
platformRepo := repository.NewPlatformRepository(db)
liveStreamRepo := repository.NewLiveStreamRepository(db)
// 初始化services
userService := service.NewUserService(db)
@@ -49,6 +51,8 @@ func Setup(db *gorm.DB, cfg *config.Config) *gin.Engine {
roleService := service.NewRoleService(db)
logService := service.NewLogService(db)
commentService := service.NewCommentService(commentRepo, orderRepo, productRepo)
platformService := service.NewPlatformService(platformRepo, productRepo)
liveStreamService := service.NewLiveStreamService(liveStreamRepo)
// 初始化微信支付服务 - 使用官方SDK
var wechatPayService *service.WeChatPayService
if cfg.WeChatPay.AppID != "" && cfg.WeChatPay.MchID != "" {
@@ -133,6 +137,8 @@ func Setup(db *gorm.DB, cfg *config.Config) *gin.Engine {
{
userRoutes.POST("/login", userHandler.Login) // 用户登录(兼容旧版本)
userRoutes.POST("/wechat-login", userHandler.WeChatLogin) // 微信登录
userRoutes.POST("/email-login", userHandler.EmailLogin) // 邮箱登录Web端
userRoutes.POST("/email-register", userHandler.EmailRegister) // 邮箱注册Web端
userRoutes.GET("/wechat-session", middleware.AuthMiddleware(), userHandler.GetWeChatSession) // 获取微信会话
userRoutes.POST("/register", userHandler.Register) // 用户注册
userRoutes.GET("/profile", middleware.AuthMiddleware(), userHandler.GetProfile) // 获取用户信息
@@ -166,6 +172,11 @@ func Setup(db *gorm.DB, cfg *config.Config) *gin.Engine {
// 轮播图相关路由
bannerHandler := handler.NewBannerHandler(bannerService)
api.GET("/banners", bannerHandler.GetBanners) // 获取轮播图
// 直播投流源相关路由(前台)
liveStreamHandler := handler.NewLiveStreamHandler(liveStreamService)
api.GET("/livestreams", liveStreamHandler.GetActiveLiveStreams) // 获取启用的投流源
api.POST("/livestreams/:id/view", liveStreamHandler.IncrementViewCount) // 增加观看次数
// 优惠券相关路由
couponHandler := handler.NewCouponHandler(couponService)
@@ -261,7 +272,7 @@ func Setup(db *gorm.DB, cfg *config.Config) *gin.Engine {
}
// 订单相关路由
orderHandler := handler.NewOrderHandler(orderService)
orderHandler := handler.NewOrderHandler(orderService, wechatPayService)
orderSettleHandler := handler.NewOrderSettleHandler(orderService, productService, userService)
orderRoutes := api.Group("/orders", middleware.AuthMiddleware())
{
@@ -271,6 +282,7 @@ func Setup(db *gorm.DB, cfg *config.Config) *gin.Engine {
orderRoutes.POST("/settle", orderSettleHandler.SettleOrder) // 订单结算
orderRoutes.POST("/merge", orderHandler.MergeOrders) // 合并订单
orderRoutes.PUT("/:id/pay", orderHandler.PayOrder) // 支付订单
orderRoutes.GET("/:id/payment/status", orderHandler.GetPaymentStatus) // 获取支付状态
orderRoutes.PUT("/:id/cancel", orderHandler.CancelOrder) // 取消订单
orderRoutes.PUT("/:id/remind-ship", orderHandler.RemindShip) // 提醒发货
orderRoutes.PUT("/:id/receive", orderHandler.ConfirmReceive) // 确认收货
@@ -300,6 +312,7 @@ func Setup(db *gorm.DB, cfg *config.Config) *gin.Engine {
commentRoutes := api.Group("/comments")
{
// 公开路由(无需认证)
commentRoutes.GET("/high-rating", commentHandler.GetHighRatingComments) // 获取高分评论(首页展示)
commentRoutes.GET("/products/:product_id", commentHandler.GetProductComments) // 获取商品评论列表
commentRoutes.GET("/products/:product_id/stats", commentHandler.GetCommentStats) // 获取商品评论统计
commentRoutes.GET("/:id", commentHandler.GetCommentDetail) // 获取评论详情
@@ -406,6 +419,18 @@ func Setup(db *gorm.DB, cfg *config.Config) *gin.Engine {
categoryAdmin.DELETE("/:id", adminProductHandler.DeleteCategory) // 删除分类
}
// 平台管理
platformHandler := handler.NewPlatformHandler(platformService)
platformAdmin := admin.Group("/platforms")
{
platformAdmin.GET("", platformHandler.GetPlatforms) // 获取平台列表
platformAdmin.GET("/all/active", platformHandler.GetAllActivePlatforms) // 获取所有启用平台(用于下拉选择)
platformAdmin.GET("/:id", platformHandler.GetPlatform) // 获取平台详情
platformAdmin.POST("", platformHandler.CreatePlatform) // 创建平台
platformAdmin.PUT("/:id", platformHandler.UpdatePlatform) // 更新平台
platformAdmin.DELETE("/:id", platformHandler.DeletePlatform) // 删除平台
}
// 店铺管理
admin.GET("/stores", adminProductHandler.GetStores) // 获取店铺列表
@@ -503,6 +528,19 @@ func Setup(db *gorm.DB, cfg *config.Config) *gin.Engine {
bannerAdmin.POST("/clean-expired", adminBannerHandler.CleanExpiredBanners) // 清理过期轮播图
}
// 直播投流源管理
adminLiveStreamHandler := handler.NewLiveStreamHandler(liveStreamService)
liveStreamAdmin := admin.Group("/livestreams")
{
liveStreamAdmin.GET("", adminLiveStreamHandler.GetLiveStreamList) // 获取投流源列表
liveStreamAdmin.GET("/:id", adminLiveStreamHandler.GetLiveStreamDetail) // 获取投流源详情
liveStreamAdmin.POST("", adminLiveStreamHandler.CreateLiveStream) // 创建投流源
liveStreamAdmin.PUT("/:id", adminLiveStreamHandler.UpdateLiveStream) // 更新投流源
liveStreamAdmin.DELETE("/:id", adminLiveStreamHandler.DeleteLiveStream) // 删除投流源
liveStreamAdmin.DELETE("/batch", adminLiveStreamHandler.BatchDeleteLiveStreams) // 批量删除投流源
liveStreamAdmin.PUT("/:id/status", adminLiveStreamHandler.UpdateLiveStreamStatus) // 更新投流源状态
}
// 文件上传管理(管理员专用)
uploadAdmin := admin.Group("/upload")
{
@@ -518,6 +556,23 @@ func Setup(db *gorm.DB, cfg *config.Config) *gin.Engine {
pointsAdmin.POST("/users/:id/deduct", pointsHandler.DeductPoints) // 扣除用户积分
}
// 优惠券管理
adminCouponHandler := handler.NewAdminCouponHandler(couponService)
couponAdmin := admin.Group("/coupons")
{
couponAdmin.GET("", adminCouponHandler.GetCouponList) // 获取优惠券列表
couponAdmin.GET("/:id", adminCouponHandler.GetCouponDetail) // 获取优惠券详情
couponAdmin.POST("", adminCouponHandler.CreateCoupon) // 创建优惠券
couponAdmin.PUT("/:id", adminCouponHandler.UpdateCoupon) // 更新优惠券
couponAdmin.DELETE("/:id", adminCouponHandler.DeleteCoupon) // 删除优惠券
couponAdmin.PUT("/:id/status", adminCouponHandler.UpdateCouponStatus) // 更新优惠券状态
couponAdmin.DELETE("/batch", adminCouponHandler.BatchDeleteCoupons) // 批量删除优惠券
couponAdmin.GET("/statistics", adminCouponHandler.GetCouponStatistics) // 获取优惠券统计
couponAdmin.GET("/user-coupons", adminCouponHandler.GetUserCouponList) // 获取用户优惠券列表
couponAdmin.POST("/distribute", adminCouponHandler.DistributeCoupon) // 发放优惠券
couponAdmin.GET("/distribute/history", adminCouponHandler.GetDistributeHistory) // 获取发放历史
}
// 角色权限管理
adminRoleHandler := handler.NewAdminRoleHandler(db, roleService)
roleAdmin := admin.Group("/roles")

View File

@@ -27,13 +27,8 @@ func NewCartService(orderRepo *repository.OrderRepository, productRepo *reposito
}
// GetCart 获取购物车
// 优化: 移除用户存在性检查,因为中间件已经验证过token和用户
func (s *CartService) GetCart(userID uint) ([]model.Cart, error) {
// 检查用户是否存在
_, err := s.userRepo.GetByID(userID)
if err != nil {
return nil, errors.New("用户不存在")
}
return s.orderRepo.GetCart(userID)
}

View File

@@ -113,6 +113,15 @@ func (s *CommentService) GetCommentStats(productID uint) (*model.CommentStats, e
return s.commentRepo.GetStats(productID)
}
// GetHighRatingComments 获取高分评论(用于首页展示)
func (s *CommentService) GetHighRatingComments(limit int) ([]model.Comment, error) {
if limit <= 0 || limit > 50 {
limit = 6 // 默认6条
}
// 获取评分>=4的高分评论
return s.commentRepo.GetHighRatingComments(limit, 4)
}
// GetCommentByID 获取评论详情
func (s *CommentService) GetCommentByID(id uint) (*model.Comment, error) {
return s.commentRepo.GetByID(id)

View File

@@ -1,6 +1,7 @@
package service
import (
"context"
"dianshang/internal/model"
"dianshang/internal/repository"
"errors"
@@ -224,3 +225,338 @@ func (s *CouponService) GetAvailableCouponsForOrder(userID uint, orderAmount flo
return availableCoupons, nil
}
// ==================== 管理端方法 ====================
// GetCouponListForAdmin 获取优惠券列表(管理端)
func (s *CouponService) GetCouponListForAdmin(page, pageSize int, status, couponType, keyword string) ([]model.Coupon, int64, error) {
return s.couponRepo.GetCouponListForAdmin(page, pageSize, status, couponType, keyword)
}
// GetCouponDetailForAdmin 获取优惠券详情(管理端)
func (s *CouponService) GetCouponDetailForAdmin(couponID uint) (*model.Coupon, error) {
coupon, err := s.couponRepo.GetByID(couponID)
if err != nil {
return nil, errors.New("优惠券不存在")
}
// 获取优惠券的使用统计注意领取数和使用数不在Coupon表中而是在UserCoupon表中统计
// 这里仅作为查询不修改coupon对象
_, _, _ = s.couponRepo.GetCouponUsageStats(couponID)
return coupon, nil
}
// CreateCoupon 创建优惠券
func (s *CouponService) CreateCoupon(req interface{}, adminID uint) (*model.Coupon, error) {
// 类型断言
type CreateCouponRequest struct {
Name string
Type int
Value int64
MinAmount int64
Description string
StartTime time.Time
EndTime time.Time
TotalCount int
Status int
}
reqData, ok := req.(*CreateCouponRequest)
if !ok {
return nil, errors.New("无效的请求参数")
}
// 验证时间
if reqData.EndTime.Before(reqData.StartTime) {
return nil, errors.New("结束时间不能早于开始时间")
}
// 验证优惠券类型和值
switch reqData.Type {
case 1: // 满减券value是金额
if reqData.Value <= 0 {
return nil, errors.New("满减券优惠金额必须大于0")
}
case 2: // 折扣券value是折扣85表示8.5折)
if reqData.Value <= 0 || reqData.Value > 100 {
return nil, errors.New("折扣券折扣必须在0-100之间")
}
case 3: // 免邮券
// 免邮券不需要验证value
default:
return nil, errors.New("不支持的优惠券类型")
}
coupon := &model.Coupon{
Name: reqData.Name,
Type: uint8(reqData.Type),
Value: uint(reqData.Value),
MinAmount: uint(reqData.MinAmount),
Description: reqData.Description,
StartTime: reqData.StartTime,
EndTime: reqData.EndTime,
TotalCount: uint(reqData.TotalCount),
UsedCount: 0,
Status: uint8(reqData.Status),
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
err := s.couponRepo.Create(coupon)
if err != nil {
return nil, fmt.Errorf("创建优惠券失败: %v", err)
}
return coupon, nil
}
// UpdateCoupon 更新优惠券
func (s *CouponService) UpdateCoupon(couponID uint, req interface{}) error {
// 检查优惠券是否存在
_, err := s.couponRepo.GetByID(couponID)
if err != nil {
return errors.New("优惠券不存在")
}
// 类型断言
type UpdateCouponRequest struct {
Name string
Type int
Value int64
MinAmount int64
Description string
StartTime time.Time
EndTime time.Time
TotalCount int
Status int
}
reqData, ok := req.(*UpdateCouponRequest)
if !ok {
return errors.New("无效的请求参数")
}
// 验证时间
if !reqData.EndTime.IsZero() && !reqData.StartTime.IsZero() {
if reqData.EndTime.Before(reqData.StartTime) {
return errors.New("结束时间不能早于开始时间")
}
}
updates := make(map[string]interface{})
if reqData.Name != "" {
updates["name"] = reqData.Name
}
if reqData.Type > 0 {
updates["type"] = reqData.Type
}
if reqData.Value > 0 {
updates["value"] = reqData.Value
}
if reqData.MinAmount >= 0 {
updates["min_amount"] = reqData.MinAmount
}
if reqData.Description != "" {
updates["description"] = reqData.Description
}
if !reqData.StartTime.IsZero() {
updates["start_time"] = reqData.StartTime
}
if !reqData.EndTime.IsZero() {
updates["end_time"] = reqData.EndTime
}
if reqData.TotalCount >= 0 {
updates["total_count"] = reqData.TotalCount
}
if reqData.Status >= 0 {
updates["status"] = reqData.Status
}
updates["updated_at"] = time.Now()
return s.couponRepo.Update(couponID, updates)
}
// DeleteCoupon 删除优惠券
func (s *CouponService) DeleteCoupon(couponID uint) error {
// 检查是否有用户已领取
hasUsers, err := s.couponRepo.CheckCouponHasUsers(couponID)
if err != nil {
return err
}
if hasUsers {
return errors.New("该优惠券已被用户领取,无法删除")
}
return s.couponRepo.Delete(couponID)
}
// UpdateCouponStatus 更新优惠券状态
func (s *CouponService) UpdateCouponStatus(couponID uint, status int) error {
_, err := s.couponRepo.GetByID(couponID)
if err != nil {
return errors.New("优惠券不存在")
}
updates := map[string]interface{}{
"status": status,
"updated_at": time.Now(),
}
return s.couponRepo.Update(couponID, updates)
}
// BatchDeleteCoupons 批量删除优惠券
func (s *CouponService) BatchDeleteCoupons(couponIDs []uint) error {
// 检查每个优惠券是否可以删除
for _, id := range couponIDs {
hasUsers, err := s.couponRepo.CheckCouponHasUsers(id)
if err != nil {
return err
}
if hasUsers {
return fmt.Errorf("优惠券ID %d 已被用户领取,无法删除", id)
}
}
return s.couponRepo.BatchDelete(couponIDs)
}
// GetCouponStatistics 获取优惠券统计
func (s *CouponService) GetCouponStatistics(startTime, endTime time.Time) (map[string]interface{}, error) {
ctx := context.Background()
// 获取总优惠券数
totalCoupons, err := s.couponRepo.CountTotalCoupons(ctx)
if err != nil {
return nil, err
}
// 获取启用的优惠券数
activeCoupons, err := s.couponRepo.CountActiveCoupons(ctx)
if err != nil {
return nil, err
}
// 获取总领取数
totalReceived, err := s.couponRepo.CountTotalReceived(ctx, startTime, endTime)
if err != nil {
return nil, err
}
// 获取总使用数
totalUsed, err := s.couponRepo.CountTotalUsed(ctx, startTime, endTime)
if err != nil {
return nil, err
}
// 获取各类型优惠券统计
typeStats, err := s.couponRepo.GetCouponTypeStats(ctx)
if err != nil {
return nil, err
}
// 获取热门优惠券
topCoupons, err := s.couponRepo.GetTopCoupons(ctx, 10)
if err != nil {
return nil, err
}
// 计算使用率
useRate := 0.0
if totalReceived > 0 {
useRate = float64(totalUsed) / float64(totalReceived) * 100
}
stats := map[string]interface{}{
"total_coupons": totalCoupons,
"active_coupons": activeCoupons,
"total_received": totalReceived,
"total_used": totalUsed,
"use_rate": fmt.Sprintf("%.2f%%", useRate),
"type_stats": typeStats,
"top_coupons": topCoupons,
}
return stats, nil
}
// GetUserCouponListForAdmin 获取用户优惠券列表(管理端)
func (s *CouponService) GetUserCouponListForAdmin(userID uint, page, pageSize int) ([]model.UserCoupon, int64, error) {
return s.couponRepo.GetUserCouponListForAdmin(userID, page, pageSize)
}
// DistributeCoupon 发放优惠券
func (s *CouponService) DistributeCoupon(couponID uint, userIDs []uint, distributeAll bool, quantity int, adminID uint) (map[string]interface{}, error) {
ctx := context.Background()
// 验证优惠券是否存在
coupon, err := s.couponRepo.GetByID(couponID)
if err != nil {
return nil, errors.New("优惠券不存在")
}
// 检查优惠券状态
if coupon.Status != 1 {
return nil, errors.New("优惠券已禁用,无法发放")
}
// 如果是全员发放获取所有用户ID
var targetUserIDs []uint
distributeType := "single"
if distributeAll {
distributeType = "all"
// TODO: 从用户表获取所有用户ID
// targetUserIDs, err = s.userRepo.GetAllUserIDs()
// 暂时返回错误,需要注入 userRepo
return nil, errors.New("全员发放功能暂未实现")
} else {
targetUserIDs = userIDs
if len(targetUserIDs) > 1 {
distributeType = "batch"
}
}
// 记录发放结果
successCount := 0
failCount := 0
// 给每个用户发放优惠券
for _, userID := range targetUserIDs {
for i := 0; i < quantity; i++ {
userCoupon := &model.UserCoupon{
UserID: userID,
CouponID: couponID,
Status: 0, // 未使用
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
err := s.couponRepo.CreateUserCoupon(userCoupon)
if err != nil {
failCount++
} else {
successCount++
}
}
}
// TODO: 记录发放历史到数据库
// 需要创建 coupon_distribute_history 表
_ = ctx
_ = adminID
result := map[string]interface{}{
"total_count": len(targetUserIDs) * quantity,
"success_count": successCount,
"fail_count": failCount,
"distribute_type": distributeType,
}
return result, nil
}
// GetDistributeHistory 获取发放历史
func (s *CouponService) GetDistributeHistory(page, pageSize int) ([]map[string]interface{}, int64, error) {
return s.couponRepo.GetDistributeHistory(page, pageSize)
}

View File

@@ -0,0 +1,144 @@
package service
import (
"dianshang/internal/model"
"dianshang/internal/repository"
"errors"
)
type LiveStreamService interface {
GetLiveStreamList(page, pageSize int, title, platform string, status *int) ([]model.LiveStream, int64, error)
GetLiveStreamByID(id uint) (*model.LiveStream, error)
GetActiveLiveStreams() ([]model.LiveStream, error)
CreateLiveStream(stream *model.LiveStream) error
UpdateLiveStream(id uint, stream *model.LiveStream) error
UpdateLiveStreamStatus(id uint, status int) error
DeleteLiveStream(id uint) error
BatchDeleteLiveStreams(ids []uint) error
IncrementViewCount(id uint) error
}
type liveStreamService struct {
liveStreamRepo repository.LiveStreamRepository
}
func NewLiveStreamService(liveStreamRepo repository.LiveStreamRepository) LiveStreamService {
return &liveStreamService{
liveStreamRepo: liveStreamRepo,
}
}
// GetLiveStreamList 获取投流源列表
func (s *liveStreamService) GetLiveStreamList(page, pageSize int, title, platform string, status *int) ([]model.LiveStream, int64, error) {
if page < 1 {
page = 1
}
if pageSize < 1 {
pageSize = 20
}
return s.liveStreamRepo.GetList(page, pageSize, title, platform, status)
}
// GetLiveStreamByID 获取投流源详情
func (s *liveStreamService) GetLiveStreamByID(id uint) (*model.LiveStream, error) {
if id == 0 {
return nil, errors.New("无效的投流源ID")
}
return s.liveStreamRepo.GetByID(id)
}
// GetActiveLiveStreams 获取所有启用的投流源
func (s *liveStreamService) GetActiveLiveStreams() ([]model.LiveStream, error) {
return s.liveStreamRepo.GetActiveLiveStreams()
}
// CreateLiveStream 创建投流源
func (s *liveStreamService) CreateLiveStream(stream *model.LiveStream) error {
if stream.Title == "" {
return errors.New("投流源标题不能为空")
}
if stream.Platform == "" {
return errors.New("平台名称不能为空")
}
if stream.StreamURL == "" {
return errors.New("投流URL不能为空")
}
// 检查该平台是否已有投流源
existingStreams, _, err := s.liveStreamRepo.GetList(1, 1, "", stream.Platform, nil)
if err != nil {
return errors.New("检查平台投流源失败")
}
if len(existingStreams) > 0 {
return errors.New("该平台已存在投流源,一个平台只能设置一个投流源")
}
return s.liveStreamRepo.Create(stream)
}
// UpdateLiveStream 更新投流源
func (s *liveStreamService) UpdateLiveStream(id uint, stream *model.LiveStream) error {
if id == 0 {
return errors.New("无效的投流源ID")
}
// 检查投流源是否存在
existing, err := s.liveStreamRepo.GetByID(id)
if err != nil {
return errors.New("投流源不存在")
}
// 如果修改了平台,检查新平台是否已有其他投流源
if stream.Platform != "" && stream.Platform != existing.Platform {
existingStreams, _, err := s.liveStreamRepo.GetList(1, 1, "", stream.Platform, nil)
if err != nil {
return errors.New("检查平台投流源失败")
}
if len(existingStreams) > 0 {
return errors.New("该平台已存在投流源,一个平台只能设置一个投流源")
}
}
return s.liveStreamRepo.Update(id, stream)
}
// UpdateLiveStreamStatus 更新投流源状态
func (s *liveStreamService) UpdateLiveStreamStatus(id uint, status int) error {
if id == 0 {
return errors.New("无效的投流源ID")
}
if status != 0 && status != 1 {
return errors.New("无效的状态值")
}
return s.liveStreamRepo.UpdateStatus(id, status)
}
// DeleteLiveStream 删除投流源
func (s *liveStreamService) DeleteLiveStream(id uint) error {
if id == 0 {
return errors.New("无效的投流源ID")
}
return s.liveStreamRepo.Delete(id)
}
// BatchDeleteLiveStreams 批量删除投流源
func (s *liveStreamService) BatchDeleteLiveStreams(ids []uint) error {
if len(ids) == 0 {
return errors.New("请选择要删除的投流源")
}
return s.liveStreamRepo.BatchDelete(ids)
}
// IncrementViewCount 增加观看次数
func (s *liveStreamService) IncrementViewCount(id uint) error {
if id == 0 {
return errors.New("无效的投流源ID")
}
return s.liveStreamRepo.IncrementViewCount(id)
}

View File

@@ -0,0 +1,150 @@
package service
import (
"dianshang/internal/model"
"dianshang/internal/repository"
"dianshang/pkg/utils"
"errors"
)
// PlatformService 平台服务
type PlatformService struct {
platformRepo *repository.PlatformRepository
productRepo *repository.ProductRepository
}
// NewPlatformService 创建平台服务
func NewPlatformService(platformRepo *repository.PlatformRepository, productRepo *repository.ProductRepository) *PlatformService {
return &PlatformService{
platformRepo: platformRepo,
productRepo: productRepo,
}
}
// GetPlatformList 获取平台列表
func (s *PlatformService) GetPlatformList(page, pageSize int, status *int, name string) ([]model.Platform, *utils.Pagination, error) {
if page <= 0 {
page = 1
}
if pageSize <= 0 || pageSize > 100 {
pageSize = 10
}
// 如果不需要分页,获取所有平台
if page == 0 && pageSize == 0 {
platforms, err := s.platformRepo.GetAll()
return platforms, nil, err
}
offset := (page - 1) * pageSize
// TODO: 实现带筛选的分页查询
// 暂时先返回所有平台
platforms, err := s.platformRepo.GetAll()
if err != nil {
return nil, nil, err
}
// 简单筛选
var filteredPlatforms []model.Platform
for _, p := range platforms {
if status != nil && p.Status != *status {
continue
}
if name != "" && p.Name != name && p.Code != name {
continue
}
filteredPlatforms = append(filteredPlatforms, p)
}
total := len(filteredPlatforms)
// 分页
start := offset
end := offset + pageSize
if start > total {
start = total
}
if end > total {
end = total
}
result := filteredPlatforms[start:end]
pagination := utils.NewPagination(page, pageSize)
pagination.Total = int64(total)
return result, pagination, nil
}
// GetPlatformByID 根据ID获取平台
func (s *PlatformService) GetPlatformByID(id uint) (*model.Platform, error) {
return s.platformRepo.GetByID(id)
}
// GetPlatformByCode 根据代码获取平台
func (s *PlatformService) GetPlatformByCode(code string) (*model.Platform, error) {
return s.platformRepo.GetByCode(code)
}
// CreatePlatform 创建平台
func (s *PlatformService) CreatePlatform(platform *model.Platform) error {
// 检查平台代码是否已存在
existing, _ := s.platformRepo.GetByCode(platform.Code)
if existing != nil {
return errors.New("平台代码已存在")
}
return s.platformRepo.Create(platform)
}
// UpdatePlatform 更新平台
func (s *PlatformService) UpdatePlatform(id uint, updates map[string]interface{}) error {
// 检查平台是否存在
_, err := s.platformRepo.GetByID(id)
if err != nil {
return errors.New("平台不存在")
}
// 如果更新代码,检查是否与其他平台冲突
if code, ok := updates["code"].(string); ok {
existing, _ := s.platformRepo.GetByCode(code)
if existing != nil && existing.ID != id {
return errors.New("平台代码已被其他平台使用")
}
}
return s.platformRepo.Update(id, updates)
}
// DeletePlatform 删除平台
func (s *PlatformService) DeletePlatform(id uint) error {
// 检查平台是否存在
_, err := s.platformRepo.GetByID(id)
if err != nil {
return errors.New("平台不存在")
}
// TODO: 检查是否有分类关联到该平台
// 可以选择级联删除或禁止删除
return s.platformRepo.Delete(id)
}
// GetAllActivePlatforms 获取所有启用的平台
func (s *PlatformService) GetAllActivePlatforms() ([]model.Platform, error) {
platforms, err := s.platformRepo.GetAll()
if err != nil {
return nil, err
}
// 过滤启用的平台
var activePlatforms []model.Platform
for _, p := range platforms {
if p.Status == 1 {
activePlatforms = append(activePlatforms, p)
}
}
return activePlatforms, nil
}

View File

@@ -25,7 +25,7 @@ func NewProductService(productRepo *repository.ProductRepository, userRepo *repo
}
// GetProductList 获取产品列表(前端用户)
func (s *ProductService) GetProductList(page, pageSize int, categoryID uint, keyword string, minPrice, maxPrice float64, sort, sortType string) ([]model.Product, *utils.Pagination, error) {
func (s *ProductService) GetProductList(page, pageSize int, categoryID uint, keyword string, minPrice, maxPrice float64, inStock *bool, sort, sortType string) ([]model.Product, *utils.Pagination, error) {
if page <= 0 {
page = 1
}
@@ -48,6 +48,9 @@ func (s *ProductService) GetProductList(page, pageSize int, categoryID uint, key
if maxPrice > 0 {
conditions["max_price"] = maxPrice
}
if inStock != nil {
conditions["in_stock"] = *inStock
}
if sort != "" {
conditions["sort"] = sort
}
@@ -128,10 +131,12 @@ func (s *ProductService) GetProductDetail(id uint) (*model.Product, error) {
// CreateProduct 创建产品
func (s *ProductService) CreateProduct(product *model.Product) error {
// 验证分类是否存在
if product.CategoryID > 0 {
_, err := s.productRepo.GetCategoryByID(product.CategoryID)
if err != nil {
return errors.New("分类不存在")
if len(product.CategoryID) > 0 {
for _, catID := range product.CategoryID {
_, err := s.productRepo.GetCategoryByID(catID)
if err != nil {
return errors.New("分类ID" + strconv.Itoa(int(catID)) + "不存在")
}
}
}
@@ -146,28 +151,6 @@ func (s *ProductService) UpdateProduct(id uint, updates map[string]interface{})
return errors.New("产品不存在")
}
// 如果更新分类,验证分类是否存在
if categoryID, ok := updates["category_id"]; ok {
var catID uint
switch v := categoryID.(type) {
case uint:
catID = v
case float64:
catID = uint(v)
case int:
catID = uint(v)
default:
return errors.New("分类ID格式错误")
}
if catID > 0 {
_, err := s.productRepo.GetCategoryByID(catID)
if err != nil {
return errors.New("分类不存在")
}
}
}
// 处理 detail_images 字段 - 确保正确转换为 JSONSlice 类型
if detailImages, ok := updates["detail_images"]; ok {
switch v := detailImages.(type) {
@@ -376,6 +359,11 @@ func (s *ProductService) GetCategories() ([]model.Category, error) {
return s.productRepo.GetCategories()
}
// GetCategoriesByPlatform 根据平台获取分类列表
func (s *ProductService) GetCategoriesByPlatform(platform string) ([]model.Category, error) {
return s.productRepo.GetCategoriesByPlatform(platform)
}
// CreateCategory 创建分类
func (s *ProductService) CreateCategory(category *model.Category) error {
return s.productRepo.CreateCategory(category)
@@ -492,7 +480,8 @@ func (s *ProductService) SearchProducts(keyword string, page, pageSize int, minP
return []model.Product{}, utils.NewPagination(page, pageSize), nil
}
return s.GetProductList(page, pageSize, 0, keyword, minPrice, maxPrice, sort, sortType)
// 搜索不筛选库存,传递 nil
return s.GetProductList(page, pageSize, 0, keyword, minPrice, maxPrice, nil, sort, sortType)
}
// UpdateStock 更新库存

View File

@@ -8,6 +8,7 @@ import (
"fmt"
"time"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
)
@@ -63,6 +64,102 @@ func (s *UserService) WeChatLogin(code string) (*model.User, string, error) {
return user, token, nil
}
// EmailLogin 邮箱登录
func (s *UserService) EmailLogin(email, password, clientIP, userAgent string) (*model.User, string, error) {
// 查找用户
user, err := s.userRepo.GetByEmail(email)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, "", errors.New("邮箱或密码错误")
}
return nil, "", err
}
// 验证密码
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil {
return nil, "", errors.New("邮箱或密码错误")
}
// 检查用户状态
if user.Status == 0 {
return nil, "", errors.New("用户已被禁用")
}
// 生成JWT token (7天有效期)
tokenExpiry := 7 * 24 * 3600
token, err := jwt.GenerateToken(user.ID, "user", tokenExpiry)
if err != nil {
return nil, "", errors.New("生成token失败")
}
// 记录登录日志
s.logUserLogin(user.ID, "email", true, "", clientIP, userAgent)
return user, token, nil
}
// EmailRegister 邮箱注册
func (s *UserService) EmailRegister(email, password, nickname string) (*model.User, error) {
// 检查邮箱是否已注册
_, err := s.userRepo.GetByEmail(email)
if err == nil {
// 找到了用户,说明邮箱已注册
return nil, errors.New("该邮箱已被注册")
}
if !errors.Is(err, gorm.ErrRecordNotFound) {
// 其他数据库错误
return nil, err
}
// 加密密码
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return nil, errors.New("密码加密失败")
}
// 创建用户
// 为邮箱用户生成唯一的 OpenID使用 email: 前缀避免与微信 OpenID 冲突)
user := &model.User{
OpenID: "email:" + email, // 使用邮箱作为 OpenID避免唯一索引冲突
Email: email,
Password: string(hashedPassword),
Nickname: nickname,
Status: 1,
}
if err := s.userRepo.Create(user); err != nil {
return nil, err
}
return user, nil
}
// logUserLogin 记录用户登录日志
func (s *UserService) logUserLogin(userID uint, loginType string, success bool, errorMsg, ip, userAgent string) {
status := 1
if !success {
status = 0
}
remark := loginType + " 登录"
if errorMsg != "" {
remark = errorMsg
}
log := &model.UserLoginLog{
UserID: userID,
LoginIP: ip,
UserAgent: userAgent,
LoginTime: time.Now(),
Status: status,
Remark: remark,
}
if err := s.db.Create(log).Error; err != nil {
fmt.Printf("记录登录日志失败: %v\n", err)
}
}
// CreateUser 创建用户
func (s *UserService) CreateUser(user *model.User) error {
// 检查用户是否已存在

View File

@@ -17,6 +17,7 @@ import (
"github.com/wechatpay-apiv3/wechatpay-go/core"
"github.com/wechatpay-apiv3/wechatpay-go/core/option"
"github.com/wechatpay-apiv3/wechatpay-go/services/payments/jsapi"
"github.com/wechatpay-apiv3/wechatpay-go/services/payments/native"
"github.com/wechatpay-apiv3/wechatpay-go/services/refunddomestic"
wechatutils "github.com/wechatpay-apiv3/wechatpay-go/utils"
)
@@ -25,6 +26,7 @@ type WeChatPayService struct {
config *config.WeChatPayConfig
client *core.Client
jsapiSvc *jsapi.JsapiApiService
nativeSvc *native.NativeApiService
refundSvc *refunddomestic.RefundsApiService
privateKey *rsa.PrivateKey
orderRepo *repository.OrderRepository
@@ -74,6 +76,9 @@ func NewWeChatPayService(cfg *config.WeChatPayConfig, orderRepo *repository.Orde
// 创建JSAPI服务
jsapiSvc := &jsapi.JsapiApiService{Client: client}
// 创建Native扫码支付服务
nativeSvc := &native.NativeApiService{Client: client}
// 创建退款服务
refundSvc := &refunddomestic.RefundsApiService{Client: client}
@@ -86,6 +91,7 @@ func NewWeChatPayService(cfg *config.WeChatPayConfig, orderRepo *repository.Orde
config: cfg,
client: client,
jsapiSvc: jsapiSvc,
nativeSvc: nativeSvc,
refundSvc: refundSvc,
privateKey: privateKey,
orderRepo: orderRepo,
@@ -174,6 +180,99 @@ func (s *WeChatPayService) CreateOrder(ctx context.Context, order *model.Order,
}, nil
}
// CreateNativeOrder 创建Native扫码支付订单用于PC端
func (s *WeChatPayService) CreateNativeOrder(ctx context.Context, order *model.Order) (*WeChatPayResponse, error) {
// 生成唯一的微信支付订单号
wechatOutTradeNo := utils.GenerateWechatOutTradeNo()
logger.Info("开始创建Native扫码支付订单",
"orderNo", order.OrderNo,
"wechatOutTradeNo", wechatOutTradeNo,
"totalAmount", order.TotalAmount,
"hasClient", s.client != nil)
// 更新订单的微信支付订单号
err := s.orderRepo.UpdateByOrderNo(order.OrderNo, map[string]interface{}{
"wechat_out_trade_no": wechatOutTradeNo,
"updated_at": time.Now(),
})
if err != nil {
logger.Error("更新订单微信支付订单号失败", "error", err, "orderNo", order.OrderNo)
return nil, fmt.Errorf("更新订单失败: %v", err)
}
// 如果没有客户端(开发环境),使用模拟数据
if s.client == nil {
logger.Warn("开发环境下使用模拟Native支付数据")
return s.createMockNativePayment(order)
}
// 构建Native预支付请求
req := native.PrepayRequest{
Appid: core.String(s.config.AppID),
Mchid: core.String(s.config.MchID),
Description: core.String(fmt.Sprintf("订单号: %s", order.OrderNo)),
OutTradeNo: core.String(wechatOutTradeNo),
NotifyUrl: core.String(s.config.NotifyURL),
Amount: &native.Amount{
Total: core.Int64(int64(order.TotalAmount)),
Currency: core.String("CNY"),
},
}
// 调用Native预支付API
resp, result, err := s.nativeSvc.Prepay(ctx, req)
if err != nil {
log.Printf("call Native Prepay err:%s", err)
logger.Error("创建Native支付订单失败", "error", err, "orderNo", order.OrderNo)
return nil, fmt.Errorf("创建Native支付订单失败: %v", err)
}
if result.Response.StatusCode != 200 {
log.Printf("Native Prepay status=%d", result.Response.StatusCode)
return nil, fmt.Errorf("Native预支付请求失败状态码: %d", result.Response.StatusCode)
}
log.Printf("Native Prepay success, code_url=%s", *resp.CodeUrl)
logger.Info("微信Native支付API响应",
"codeUrl", *resp.CodeUrl,
"orderNo", order.OrderNo)
return &WeChatPayResponse{
Code: 0,
Message: "success",
Data: map[string]interface{}{
"qrcode_url": *resp.CodeUrl,
"order_no": order.OrderNo,
"amount": order.TotalAmount,
},
}, nil
}
// createMockNativePayment 创建模拟Native支付数据开发环境使用
func (s *WeChatPayService) createMockNativePayment(order *model.Order) (*WeChatPayResponse, error) {
// 生成模拟的二维码URL
mockQRCodeURL := fmt.Sprintf("weixin://wxpay/bizpayurl?pr=mock_%s", utils.GenerateRandomString(10))
logger.Info("生成模拟Native支付参数",
"environment", s.config.Environment,
"qrcodeUrl", mockQRCodeURL,
"orderNo", order.OrderNo,
"totalAmount", order.TotalAmount)
return &WeChatPayResponse{
Code: 0,
Message: "模拟支付创建成功",
Data: map[string]interface{}{
"qrcode_url": mockQRCodeURL,
"order_no": order.OrderNo,
"amount": order.TotalAmount,
"sandbox": true,
"tips": "这是模拟环境的Native支付请使用测试接口模拟支付成功",
},
}, nil
}
// createMockPayment 创建模拟支付数据(沙盒环境使用)
func (s *WeChatPayService) createMockPayment(order *model.Order, openID string) (*WeChatPayResponse, error) {
mockPrepayID := fmt.Sprintf("wx%d%s", time.Now().Unix(), generateNonceStr()[:8])