Files
yml2213 6d61027318 新增摸大红业务并修复后台链接按钮白字
支持分类筛选、搜索、下单支付建群与固定二维码;合并迁移种子数据;后台表格编辑/详情链接恢复主题色可见。
2026-07-16 18:08:18 +08:00

304 lines
8.5 KiB
Go

package mohong
import (
"context"
"strings"
"hfb_sys/backend/internal/model"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
func (r *Repository) ListPublicProducts(ctx context.Context, query ProductListQuery) (*PaginatedResult, error) {
query = normalizePage(query)
db := r.db.WithContext(ctx).Model(&model.MohongProduct{}).Where("status = ?", model.MohongProductStatusOnSale)
if query.CategoryID > 0 {
// 多对多分类 + 主分类兜底
db = db.Where(
`(id IN (SELECT product_id FROM mohong_product_categories WHERE category_id = ?) OR category_id = ?)`,
query.CategoryID, query.CategoryID,
)
}
if kw := strings.TrimSpace(query.Keyword); kw != "" {
like := "%" + kw + "%"
db = db.Where("(title LIKE ? OR description LIKE ?)", like, like)
}
var total int64
if err := db.Count(&total).Error; err != nil {
return nil, err
}
var rows []model.MohongProduct
if err := db.Order("sort_order DESC, id DESC").
Offset((query.Page - 1) * query.PageSize).
Limit(query.PageSize).
Find(&rows).Error; err != nil {
return nil, err
}
return r.productPageDTO(ctx, rows, total, query, false)
}
func (r *Repository) FindPublicProduct(ctx context.Context, id uint64) (*ProductDTO, error) {
var row model.MohongProduct
if err := r.db.WithContext(ctx).
Where("id = ? AND status = ?", id, model.MohongProductStatusOnSale).
First(&row).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return nil, ErrProductNotFound
}
return nil, err
}
name := ""
if row.CategoryID != nil {
name = r.loadCategoryNameMap(ctx, []uint64{*row.CategoryID})[*row.CategoryID]
}
dto := toProductDTOWithCategory(row, name, false)
return &dto, nil
}
func (r *Repository) AdminListProducts(ctx context.Context, query ProductListQuery) (*PaginatedResult, error) {
query = normalizePage(query)
db := r.db.WithContext(ctx).Model(&model.MohongProduct{})
if status := strings.TrimSpace(query.Status); status != "" {
db = db.Where("status = ?", status)
}
if query.CategoryID > 0 {
db = db.Where(
`(id IN (SELECT product_id FROM mohong_product_categories WHERE category_id = ?) OR category_id = ?)`,
query.CategoryID, query.CategoryID,
)
}
if kw := strings.TrimSpace(query.Keyword); kw != "" {
like := "%" + kw + "%"
db = db.Where("(title LIKE ? OR description LIKE ?)", like, like)
}
var total int64
if err := db.Count(&total).Error; err != nil {
return nil, err
}
var rows []model.MohongProduct
if err := db.Order("sort_order DESC, id DESC").
Offset((query.Page - 1) * query.PageSize).
Limit(query.PageSize).
Find(&rows).Error; err != nil {
return nil, err
}
return r.productPageDTO(ctx, rows, total, query, true)
}
func (r *Repository) productPageDTO(ctx context.Context, rows []model.MohongProduct, total int64, query ProductListQuery, admin bool) (*PaginatedResult, error) {
ids := make([]uint64, 0, len(rows))
for _, row := range rows {
if row.CategoryID != nil {
ids = append(ids, *row.CategoryID)
}
}
names := r.loadCategoryNameMap(ctx, ids)
items := make([]ProductDTO, 0, len(rows))
for _, row := range rows {
name := ""
if row.CategoryID != nil {
name = names[*row.CategoryID]
}
items = append(items, toProductDTOWithCategory(row, name, admin))
}
return &PaginatedResult{Items: items, Total: total, Page: query.Page, PageSize: query.PageSize}, nil
}
func (r *Repository) AdminFindProduct(ctx context.Context, id uint64) (*ProductDTO, error) {
var row model.MohongProduct
if err := r.db.WithContext(ctx).First(&row, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return nil, ErrProductNotFound
}
return nil, err
}
name := ""
if row.CategoryID != nil {
name = r.loadCategoryNameMap(ctx, []uint64{*row.CategoryID})[*row.CategoryID]
}
dto := toProductDTOWithCategory(row, name, true)
return &dto, nil
}
func (r *Repository) CreateProduct(ctx context.Context, adminID uint64, req CreateProductRequest) (*ProductDTO, error) {
status := strings.TrimSpace(req.Status)
if status == "" {
status = model.MohongProductStatusDraft
}
if !isValidProductStatus(status) {
return nil, ErrInvalidRequest
}
stock := -1
if req.Stock != nil {
stock = *req.Stock
}
unit := strings.TrimSpace(req.Unit)
if unit == "" {
unit = "份"
}
row := model.MohongProduct{
CategoryID: normalizeCategoryID(req.CategoryID),
Title: strings.TrimSpace(req.Title),
CoverURL: strings.TrimSpace(req.CoverURL),
ImageURLs: encodeStringList(req.ImageURLs),
Description: strings.TrimSpace(req.Description),
PriceCent: req.PriceCent,
OriginalPriceCent: maxInt64(req.OriginalPriceCent, 0),
Unit: unit,
Stock: stock,
SortOrder: req.SortOrder,
Status: status,
QrcodeImageURL: strings.TrimSpace(req.QrcodeImageURL),
CreatedBy: &adminID,
}
if row.Title == "" || row.PriceCent <= 0 {
return nil, ErrInvalidRequest
}
if err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Create(&row).Error; err != nil {
return err
}
return syncProductCategories(tx, row.ID, row.CategoryID)
}); err != nil {
return nil, err
}
return r.AdminFindProduct(ctx, row.ID)
}
func (r *Repository) UpdateProduct(ctx context.Context, id uint64, req UpdateProductRequest) (*ProductDTO, error) {
var row model.MohongProduct
if err := r.db.WithContext(ctx).First(&row, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return nil, ErrProductNotFound
}
return nil, err
}
updates := map[string]any{}
if req.ClearCategory {
updates["category_id"] = nil
} else if req.CategoryID != nil {
updates["category_id"] = normalizeCategoryID(req.CategoryID)
}
if req.Title != nil {
title := strings.TrimSpace(*req.Title)
if title == "" {
return nil, ErrInvalidRequest
}
updates["title"] = title
}
if req.CoverURL != nil {
updates["cover_url"] = strings.TrimSpace(*req.CoverURL)
}
if req.ImageURLs != nil {
updates["image_urls"] = encodeStringList(req.ImageURLs)
}
if req.Description != nil {
updates["description"] = strings.TrimSpace(*req.Description)
}
if req.PriceCent != nil {
if *req.PriceCent <= 0 {
return nil, ErrInvalidRequest
}
updates["price_cent"] = *req.PriceCent
}
if req.OriginalPriceCent != nil {
updates["original_price_cent"] = maxInt64(*req.OriginalPriceCent, 0)
}
if req.Unit != nil {
unit := strings.TrimSpace(*req.Unit)
if unit == "" {
unit = "份"
}
updates["unit"] = unit
}
if req.Stock != nil {
updates["stock"] = *req.Stock
}
if req.SortOrder != nil {
updates["sort_order"] = *req.SortOrder
}
if req.Status != nil {
status := strings.TrimSpace(*req.Status)
if !isValidProductStatus(status) {
return nil, ErrInvalidRequest
}
updates["status"] = status
}
if req.QrcodeImageURL != nil {
updates["qrcode_image_url"] = strings.TrimSpace(*req.QrcodeImageURL)
}
if err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if len(updates) > 0 {
if err := tx.Model(&row).Updates(updates).Error; err != nil {
return err
}
}
// 刷新主分类后同步关联表(至少保证主分类在关联里)
var latest model.MohongProduct
if err := tx.First(&latest, id).Error; err != nil {
return err
}
return syncProductCategories(tx, latest.ID, latest.CategoryID)
}); err != nil {
return nil, err
}
return r.AdminFindProduct(ctx, id)
}
// syncProductCategories 保证主分类写入多对多表;不删除其它已有关联(种子数据可挂多分类)。
func syncProductCategories(tx *gorm.DB, productID uint64, primary *uint64) error {
if primary == nil || *primary == 0 {
return nil
}
rel := model.MohongProductCategory{ProductID: productID, CategoryID: *primary}
return tx.Clauses(clause.OnConflict{DoNothing: true}).Create(&rel).Error
}
func (r *Repository) DeleteProduct(ctx context.Context, id uint64) error {
res := r.db.WithContext(ctx).Delete(&model.MohongProduct{}, id)
if res.Error != nil {
return res.Error
}
if res.RowsAffected == 0 {
return ErrProductNotFound
}
return nil
}
func isValidProductStatus(status string) bool {
switch status {
case model.MohongProductStatusDraft, model.MohongProductStatusOnSale, model.MohongProductStatusOffSale:
return true
default:
return false
}
}
func normalizePage(query ProductListQuery) ProductListQuery {
if query.Page < 1 {
query.Page = 1
}
if query.PageSize < 1 {
query.PageSize = 20
}
if query.PageSize > 100 {
query.PageSize = 100
}
return query
}
func maxInt64(a, b int64) int64 {
if a > b {
return a
}
return b
}
func normalizeCategoryID(id *uint64) *uint64 {
if id == nil || *id == 0 {
return nil
}
return id
}