feat: add shipping template management and update favicon
This commit is contained in:
@@ -300,14 +300,80 @@ func (h *OrderHandler) Create(c *gin.Context) {
|
||||
break
|
||||
}
|
||||
|
||||
var shippingFeeFirstWeight, shippingFeePerGram, serviceFeeRate, taxRate, channelFeeRate float64
|
||||
// 获取收货地址省份
|
||||
var shippingAddress models.Address
|
||||
var province string
|
||||
if err := utils.DB.First(&shippingAddress, req.ShippingAddressID).Error; err == nil {
|
||||
province = shippingAddress.Province
|
||||
}
|
||||
|
||||
// 使用运费模板计算运费
|
||||
shippingFee := 0.0
|
||||
|
||||
// 按运费模板分组计算
|
||||
type TemplateGroup struct {
|
||||
TemplateID *uint
|
||||
Weight float64
|
||||
Quantity int
|
||||
Subtotal float64
|
||||
}
|
||||
templateGroups := make(map[uint]*TemplateGroup)
|
||||
noTemplateGroup := &TemplateGroup{Subtotal: 0, Weight: 0, Quantity: 0}
|
||||
|
||||
for _, cart := range carts {
|
||||
var templateID uint
|
||||
if cart.Product.ShippingTemplateID != nil {
|
||||
templateID = *cart.Product.ShippingTemplateID
|
||||
}
|
||||
|
||||
if templateID == 0 {
|
||||
// 没有运费模板,累加到无模板组
|
||||
noTemplateGroup.Weight += cart.Product.Weight * float64(cart.Quantity)
|
||||
noTemplateGroup.Quantity += cart.Quantity
|
||||
noTemplateGroup.Subtotal += cart.Product.Price * float64(cart.Quantity)
|
||||
} else {
|
||||
if _, ok := templateGroups[templateID]; !ok {
|
||||
templateGroups[templateID] = &TemplateGroup{
|
||||
TemplateID: &templateID,
|
||||
Weight: 0,
|
||||
Quantity: 0,
|
||||
Subtotal: 0,
|
||||
}
|
||||
}
|
||||
templateGroups[templateID].Weight += cart.Product.Weight * float64(cart.Quantity)
|
||||
templateGroups[templateID].Quantity += cart.Quantity
|
||||
templateGroups[templateID].Subtotal += cart.Product.Price * float64(cart.Quantity)
|
||||
}
|
||||
}
|
||||
|
||||
// 计算有模板的运费
|
||||
for _, group := range templateGroups {
|
||||
fee, _ := CalculateShippingFee(group.TemplateID, group.Weight, group.Quantity, group.Subtotal, province)
|
||||
shippingFee += fee
|
||||
}
|
||||
|
||||
// 计算无模板的运费(使用旧的系统设置)
|
||||
if noTemplateGroup.Subtotal > 0 {
|
||||
var shippingFeeFirstWeight, shippingFeePerGram float64
|
||||
var setting models.SystemSetting
|
||||
if err := utils.DB.Where("`key` = ?", "shipping_fee_first_weight").First(&setting).Error; err == nil {
|
||||
shippingFeeFirstWeight, _ = strconv.ParseFloat(setting.Value, 64)
|
||||
}
|
||||
if err := utils.DB.Where("`key` = ?", "shipping_fee_per_gram").First(&setting).Error; err == nil {
|
||||
shippingFeePerGram, _ = strconv.ParseFloat(setting.Value, 64)
|
||||
}
|
||||
if noTemplateGroup.Subtotal < 99 && shippingFeeFirstWeight > 0 {
|
||||
fee := shippingFeeFirstWeight
|
||||
if noTemplateGroup.Weight > 500 {
|
||||
fee += shippingFeePerGram * (noTemplateGroup.Weight - 500)
|
||||
}
|
||||
shippingFee += fee
|
||||
}
|
||||
}
|
||||
|
||||
// 获取其他费率
|
||||
var serviceFeeRate, taxRate, channelFeeRate float64
|
||||
var setting models.SystemSetting
|
||||
if err := utils.DB.Where("`key` = ?", "shipping_fee_first_weight").First(&setting).Error; err == nil {
|
||||
shippingFeeFirstWeight, _ = strconv.ParseFloat(setting.Value, 64)
|
||||
}
|
||||
if err := utils.DB.Where("`key` = ?", "shipping_fee_per_gram").First(&setting).Error; err == nil {
|
||||
shippingFeePerGram, _ = strconv.ParseFloat(setting.Value, 64)
|
||||
}
|
||||
if err := utils.DB.Where("`key` = ?", "service_fee_rate").First(&setting).Error; err == nil {
|
||||
serviceFeeRate, _ = strconv.ParseFloat(setting.Value, 64)
|
||||
}
|
||||
@@ -318,15 +384,6 @@ func (h *OrderHandler) Create(c *gin.Context) {
|
||||
channelFeeRate, _ = strconv.ParseFloat(setting.Value, 64)
|
||||
}
|
||||
|
||||
// 根据商品重量计算运费
|
||||
shippingFee := 0.0
|
||||
if subtotal < 99 && shippingFeeFirstWeight > 0 {
|
||||
shippingFee = shippingFeeFirstWeight
|
||||
if totalWeight > 500 {
|
||||
shippingFee += shippingFeePerGram * (totalWeight - 500)
|
||||
}
|
||||
}
|
||||
|
||||
serviceFee := subtotal * serviceFeeRate / 100
|
||||
tax := subtotal * taxRate / 100
|
||||
channelFee := subtotal * channelFeeRate / 100
|
||||
|
||||
@@ -225,7 +225,7 @@ func (h *ProductHandler) GetByID(c *gin.Context) {
|
||||
|
||||
func (h *ProductHandler) AdminList(c *gin.Context) {
|
||||
var products []models.Product
|
||||
utils.DB.Preload("Categories").Preload("Brand").Order("created_at DESC").Find(&products)
|
||||
utils.DB.Preload("Categories").Preload("Brand").Preload("ShippingTemplate").Order("created_at DESC").Find(&products)
|
||||
c.JSON(http.StatusOK, gin.H{"data": products})
|
||||
}
|
||||
|
||||
@@ -237,22 +237,23 @@ func (h *ProductHandler) Create(c *gin.Context) {
|
||||
}
|
||||
|
||||
product := models.Product{
|
||||
Name: req.Name,
|
||||
Description: req.Description,
|
||||
Price: req.Price,
|
||||
Weight: req.Weight,
|
||||
MinPurchase: req.MinPurchase,
|
||||
MaxPurchase: req.MaxPurchase,
|
||||
MinWeight: req.MinWeight,
|
||||
MaxWeight: req.MaxWeight,
|
||||
MinAmount: req.MinAmount,
|
||||
MaxAmount: req.MaxAmount,
|
||||
RequireCredit: req.RequireCredit,
|
||||
CreditCost: req.CreditCost,
|
||||
CreditReward: req.CreditReward,
|
||||
Images: req.Images,
|
||||
BrandID: req.BrandID,
|
||||
IsActive: true,
|
||||
Name: req.Name,
|
||||
Description: req.Description,
|
||||
Price: req.Price,
|
||||
Weight: req.Weight,
|
||||
MinPurchase: req.MinPurchase,
|
||||
MaxPurchase: req.MaxPurchase,
|
||||
MinWeight: req.MinWeight,
|
||||
MaxWeight: req.MaxWeight,
|
||||
MinAmount: req.MinAmount,
|
||||
MaxAmount: req.MaxAmount,
|
||||
RequireCredit: req.RequireCredit,
|
||||
CreditCost: req.CreditCost,
|
||||
CreditReward: req.CreditReward,
|
||||
Images: req.Images,
|
||||
BrandID: req.BrandID,
|
||||
ShippingTemplateID: req.ShippingTemplateID,
|
||||
IsActive: true,
|
||||
}
|
||||
|
||||
if err := utils.DB.Create(&product).Error; err != nil {
|
||||
@@ -276,7 +277,7 @@ func (h *ProductHandler) Create(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
utils.DB.Preload("Categories").Preload("CustomFields").Preload("Brand").First(&product, product.ID)
|
||||
utils.DB.Preload("Categories").Preload("CustomFields").Preload("Brand").Preload("ShippingTemplate").First(&product, product.ID)
|
||||
c.JSON(http.StatusCreated, gin.H{"data": product})
|
||||
}
|
||||
|
||||
@@ -343,6 +344,9 @@ func (h *ProductHandler) Update(c *gin.Context) {
|
||||
if req.BrandID != nil {
|
||||
updates["brand_id"] = *req.BrandID
|
||||
}
|
||||
if req.ShippingTemplateID != nil {
|
||||
updates["shipping_template_id"] = *req.ShippingTemplateID
|
||||
}
|
||||
|
||||
utils.DB.Model(&product).Updates(updates)
|
||||
|
||||
@@ -363,7 +367,7 @@ func (h *ProductHandler) Update(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
utils.DB.Preload("Categories").Preload("CustomFields").Preload("Brand").First(&product, product.ID)
|
||||
utils.DB.Preload("Categories").Preload("CustomFields").Preload("Brand").Preload("ShippingTemplate").First(&product, product.ID)
|
||||
c.JSON(http.StatusOK, gin.H{"data": product})
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,260 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"math"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"sale/internal/models"
|
||||
"sale/internal/schemas"
|
||||
"sale/internal/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type ShippingTemplateHandler struct{}
|
||||
|
||||
func NewShippingTemplateHandler() *ShippingTemplateHandler {
|
||||
return &ShippingTemplateHandler{}
|
||||
}
|
||||
|
||||
// List 获取运费模板列表
|
||||
func (h *ShippingTemplateHandler) List(c *gin.Context) {
|
||||
var templates []models.ShippingTemplate
|
||||
utils.DB.Order("sort_order ASC, id ASC").Find(&templates)
|
||||
|
||||
// 解析 provinces JSON 为数组
|
||||
type TemplateResponse struct {
|
||||
models.ShippingTemplate
|
||||
ProvincesList []string `json:"provinces_list"`
|
||||
}
|
||||
|
||||
var response []TemplateResponse
|
||||
for _, t := range templates {
|
||||
var provinces []string
|
||||
if t.Provinces != "" {
|
||||
json.Unmarshal([]byte(t.Provinces), &provinces)
|
||||
}
|
||||
response = append(response, TemplateResponse{
|
||||
ShippingTemplate: t,
|
||||
ProvincesList: provinces,
|
||||
})
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"data": response})
|
||||
}
|
||||
|
||||
// GetByID 获取单个运费模板
|
||||
func (h *ShippingTemplateHandler) GetByID(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
var template models.ShippingTemplate
|
||||
if err := utils.DB.First(&template, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "运费模板不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
var provinces []string
|
||||
if template.Provinces != "" {
|
||||
json.Unmarshal([]byte(template.Provinces), &provinces)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"data": gin.H{
|
||||
"ShippingTemplate": template,
|
||||
"provinces_list": provinces,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Create 创建运费模板
|
||||
func (h *ShippingTemplateHandler) Create(c *gin.Context) {
|
||||
var req schemas.CreateShippingTemplateRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 如果设为默认,先取消其他默认模板
|
||||
if req.IsDefault {
|
||||
utils.DB.Model(&models.ShippingTemplate{}).Where("is_default = ?", true).Update("is_default", false)
|
||||
}
|
||||
|
||||
// 序列化省份列表
|
||||
var provincesJSON string
|
||||
if len(req.Provinces) > 0 {
|
||||
data, _ := json.Marshal(req.Provinces)
|
||||
provincesJSON = string(data)
|
||||
}
|
||||
|
||||
template := models.ShippingTemplate{
|
||||
Name: req.Name,
|
||||
CalcType: req.CalcType,
|
||||
FirstUnit: req.FirstUnit,
|
||||
FirstFee: req.FirstFee,
|
||||
AdditionalUnit: req.AdditionalUnit,
|
||||
AdditionalFee: req.AdditionalFee,
|
||||
FreeAmount: req.FreeAmount,
|
||||
Provinces: provincesJSON,
|
||||
IsDefault: req.IsDefault,
|
||||
SortOrder: req.SortOrder,
|
||||
}
|
||||
|
||||
if err := utils.DB.Create(&template).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "创建运费模板失败"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{"data": template})
|
||||
}
|
||||
|
||||
// Update 更新运费模板
|
||||
func (h *ShippingTemplateHandler) Update(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
var template models.ShippingTemplate
|
||||
if err := utils.DB.First(&template, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "运费模板不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
var req schemas.UpdateShippingTemplateRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
updates := make(map[string]interface{})
|
||||
|
||||
if req.Name != nil {
|
||||
updates["name"] = *req.Name
|
||||
}
|
||||
if req.CalcType != nil {
|
||||
updates["calc_type"] = *req.CalcType
|
||||
}
|
||||
if req.FirstUnit != nil {
|
||||
updates["first_unit"] = *req.FirstUnit
|
||||
}
|
||||
if req.FirstFee != nil {
|
||||
updates["first_fee"] = *req.FirstFee
|
||||
}
|
||||
if req.AdditionalUnit != nil {
|
||||
updates["additional_unit"] = *req.AdditionalUnit
|
||||
}
|
||||
if req.AdditionalFee != nil {
|
||||
updates["additional_fee"] = *req.AdditionalFee
|
||||
}
|
||||
if req.FreeAmount != nil {
|
||||
updates["free_amount"] = *req.FreeAmount
|
||||
}
|
||||
if req.Provinces != nil {
|
||||
if len(req.Provinces) > 0 {
|
||||
data, _ := json.Marshal(req.Provinces)
|
||||
updates["provinces"] = string(data)
|
||||
} else {
|
||||
updates["provinces"] = ""
|
||||
}
|
||||
}
|
||||
if req.IsDefault != nil {
|
||||
if *req.IsDefault {
|
||||
utils.DB.Model(&models.ShippingTemplate{}).Where("is_default = ? AND id != ?", true, id).Update("is_default", false)
|
||||
}
|
||||
updates["is_default"] = *req.IsDefault
|
||||
}
|
||||
if req.SortOrder != nil {
|
||||
updates["sort_order"] = *req.SortOrder
|
||||
}
|
||||
|
||||
utils.DB.Model(&template).Updates(updates)
|
||||
utils.DB.First(&template, id)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"data": template})
|
||||
}
|
||||
|
||||
// Delete 删除运费模板
|
||||
func (h *ShippingTemplateHandler) Delete(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
|
||||
var template models.ShippingTemplate
|
||||
if err := utils.DB.First(&template, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "运费模板不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否有关联的商品
|
||||
var count int64
|
||||
utils.DB.Model(&models.Product{}).Where("shipping_template_id = ?", id).Count(&count)
|
||||
if count > 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "该运费模板已被商品关联,无法删除"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := utils.DB.Delete(&template).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "删除运费模板失败"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "删除成功"})
|
||||
}
|
||||
|
||||
// CalculateShippingFee 计算运费(供内部调用)
|
||||
func CalculateShippingFee(templateID *uint, weight float64, quantity int, subtotal float64, province string) (float64, error) {
|
||||
var template models.ShippingTemplate
|
||||
|
||||
// 如果没有指定模板,使用默认模板
|
||||
if templateID == nil || *templateID == 0 {
|
||||
if err := utils.DB.Where("is_default = ?", true).First(&template).Error; err != nil {
|
||||
// 没有默认模板,运费为0
|
||||
return 0, nil
|
||||
}
|
||||
} else {
|
||||
if err := utils.DB.First(&template, *templateID).Error; err != nil {
|
||||
// 模板不存在,使用默认模板
|
||||
if err := utils.DB.Where("is_default = ?", true).First(&template).Error; err != nil {
|
||||
return 0, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检查适用地区
|
||||
if template.Provinces != "" && province != "" {
|
||||
var provinces []string
|
||||
json.Unmarshal([]byte(template.Provinces), &provinces)
|
||||
matched := false
|
||||
for _, p := range provinces {
|
||||
if p == province {
|
||||
matched = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !matched {
|
||||
// 地址不在适用范围内,使用默认模板
|
||||
var defaultTemplate models.ShippingTemplate
|
||||
if err := utils.DB.Where("is_default = ?", true).First(&defaultTemplate).Error; err == nil {
|
||||
template = defaultTemplate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否满足包邮条件
|
||||
if template.FreeAmount > 0 && subtotal >= template.FreeAmount {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// 根据计费方式计算运费
|
||||
var totalUnit float64
|
||||
if template.CalcType == models.CalcTypeWeight {
|
||||
totalUnit = weight // 重量(克)
|
||||
} else {
|
||||
totalUnit = float64(quantity) // 件数
|
||||
}
|
||||
|
||||
// 计算运费
|
||||
if totalUnit <= template.FirstUnit {
|
||||
return template.FirstFee, nil
|
||||
}
|
||||
|
||||
remainingUnit := totalUnit - template.FirstUnit
|
||||
additionalCount := math.Ceil(remainingUnit / template.AdditionalUnit)
|
||||
|
||||
return template.FirstFee + additionalCount*template.AdditionalFee, nil
|
||||
}
|
||||
Reference in New Issue
Block a user