304 lines
8.5 KiB
Go
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
|
|
}
|