本次提交同时完成碎片批量合成与盒柜发货运费规则改造,减少用户重复操作,并确保不包邮商品在任何件数下都必须支付运费。 - 合成功能:新增批量合成接口与前端一键合成入口,按配方可合成上限批量消耗碎片并生成对应资产,同时补充批量合成测试 - 运费规则:后端新增统一运费判定逻辑,命中 category_id 14/15 的商品时整单强制收取运费,否则继续沿用少于 5 件收运费的旧规则 - 发货流程:新增运费检查接口,前端发货前先向后端确认是否需要支付运费,并根据“件数不足”或“包含不包邮商品”展示不同提示文案 - 接口校验:运费预下单与批量申请发货统一复用后端判定逻辑,避免前端规则被绕过
551 lines
18 KiB
Go
551 lines
18 KiB
Go
package synthesis
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"time"
|
|
|
|
"bindbox-game/internal/repository/mysql"
|
|
"bindbox-game/internal/repository/mysql/model"
|
|
|
|
"gorm.io/gorm"
|
|
"gorm.io/gorm/clause"
|
|
)
|
|
|
|
type Service interface {
|
|
ListRecipes(ctx context.Context, page, size int) (list []*RecipeWithMaterials, total int64, err error)
|
|
GetRecipe(ctx context.Context, id int64) (*RecipeWithMaterials, error)
|
|
CreateRecipe(ctx context.Context, req CreateRecipeRequest) (*model.FragmentSynthesisRecipes, error)
|
|
ModifyRecipe(ctx context.Context, id int64, req CreateRecipeRequest) error
|
|
DeleteRecipe(ctx context.Context, id int64) error
|
|
GetAvailableRecipesForUser(ctx context.Context, userID int64) ([]*UserRecipeView, error)
|
|
Synthesize(ctx context.Context, userID int64, recipeID int64) (*model.UserInventory, error)
|
|
BatchSynthesize(ctx context.Context, userID int64, recipeID int64) (*BatchSynthesizeResult, error)
|
|
ListLogs(ctx context.Context, page, size int, userID *int64) (list []*SynthesisLogView, total int64, err error)
|
|
}
|
|
|
|
type MaterialInput struct {
|
|
FragmentProductID int64 `json:"fragment_product_id"`
|
|
RequiredCount int32 `json:"required_count"`
|
|
}
|
|
|
|
type CreateRecipeRequest struct {
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
TargetProductID int64 `json:"target_product_id"`
|
|
Status int32 `json:"status"`
|
|
Materials []MaterialInput `json:"materials"`
|
|
}
|
|
|
|
type RecipeWithMaterials struct {
|
|
*model.FragmentSynthesisRecipes
|
|
Materials []*model.FragmentSynthesisRecipeMaterials `json:"materials"`
|
|
TargetProduct *model.Products `json:"target_product,omitempty"`
|
|
}
|
|
|
|
type UserMaterialView struct {
|
|
FragmentProductID int64 `json:"fragment_product_id"`
|
|
Name string `json:"name"`
|
|
Image string `json:"image"`
|
|
RequiredCount int32 `json:"required_count"`
|
|
OwnedCount int64 `json:"owned_count"`
|
|
}
|
|
|
|
type UserRecipeView struct {
|
|
ID int64 `json:"id"`
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
TargetProduct *model.Products `json:"target_product"`
|
|
CanSynthesize bool `json:"can_synthesize"`
|
|
MaxSynthesizeCount int64 `json:"max_synthesize_count"`
|
|
Materials []UserMaterialView `json:"materials"`
|
|
}
|
|
|
|
type BatchSynthesizeResult struct {
|
|
RecipeID int64 `json:"recipe_id"`
|
|
TargetProductID int64 `json:"target_product_id"`
|
|
TargetProductName string `json:"target_product_name"`
|
|
SynthesizedCount int64 `json:"synthesized_count"`
|
|
ProducedInventoryIDs []int64 `json:"produced_inventory_ids"`
|
|
ConsumedInventoryCount int `json:"consumed_inventory_count"`
|
|
}
|
|
|
|
type SynthesisLogView struct {
|
|
ID int64 `json:"id"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UserID int64 `json:"user_id"`
|
|
RecipeID int64 `json:"recipe_id"`
|
|
RecipeName string `json:"recipe_name"`
|
|
ProducedInventoryID int64 `json:"produced_inventory_id"`
|
|
TargetProductName string `json:"target_product_name"`
|
|
ConsumedCount int `json:"consumed_count"`
|
|
}
|
|
|
|
type service struct {
|
|
repo mysql.Repo
|
|
}
|
|
|
|
func New(db mysql.Repo) Service {
|
|
return &service{repo: db}
|
|
}
|
|
|
|
func (s *service) ListRecipes(ctx context.Context, page, size int) ([]*RecipeWithMaterials, int64, error) {
|
|
if page <= 0 {
|
|
page = 1
|
|
}
|
|
if size <= 0 {
|
|
size = 20
|
|
}
|
|
db := s.repo.GetDbR()
|
|
var total int64
|
|
if err := db.WithContext(ctx).Model(&model.FragmentSynthesisRecipes{}).Count(&total).Error; err != nil {
|
|
return nil, 0, err
|
|
}
|
|
var recipes []*model.FragmentSynthesisRecipes
|
|
if err := db.WithContext(ctx).Order("id DESC").Offset((page - 1) * size).Limit(size).Find(&recipes).Error; err != nil {
|
|
return nil, 0, err
|
|
}
|
|
result := make([]*RecipeWithMaterials, 0, len(recipes))
|
|
for _, r := range recipes {
|
|
rm := &RecipeWithMaterials{FragmentSynthesisRecipes: r}
|
|
db.WithContext(ctx).Where("recipe_id = ?", r.ID).Find(&rm.Materials)
|
|
var p model.Products
|
|
if err := db.WithContext(ctx).Where("id = ?", r.TargetProductID).First(&p).Error; err == nil {
|
|
rm.TargetProduct = &p
|
|
}
|
|
result = append(result, rm)
|
|
}
|
|
return result, total, nil
|
|
}
|
|
|
|
func (s *service) GetRecipe(ctx context.Context, id int64) (*RecipeWithMaterials, error) {
|
|
db := s.repo.GetDbR()
|
|
var r model.FragmentSynthesisRecipes
|
|
if err := db.WithContext(ctx).Where("id = ?", id).First(&r).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
rm := &RecipeWithMaterials{FragmentSynthesisRecipes: &r}
|
|
db.WithContext(ctx).Where("recipe_id = ?", r.ID).Find(&rm.Materials)
|
|
var p model.Products
|
|
if err := db.WithContext(ctx).Where("id = ?", r.TargetProductID).First(&p).Error; err == nil {
|
|
rm.TargetProduct = &p
|
|
}
|
|
return rm, nil
|
|
}
|
|
|
|
func (s *service) CreateRecipe(ctx context.Context, req CreateRecipeRequest) (*model.FragmentSynthesisRecipes, error) {
|
|
if len(req.Materials) == 0 {
|
|
return nil, fmt.Errorf("materials_required")
|
|
}
|
|
seen := make(map[int64]struct{})
|
|
for _, m := range req.Materials {
|
|
if m.FragmentProductID <= 0 || m.RequiredCount <= 0 {
|
|
return nil, fmt.Errorf("invalid_material")
|
|
}
|
|
if _, ok := seen[m.FragmentProductID]; ok {
|
|
return nil, fmt.Errorf("duplicate_material")
|
|
}
|
|
seen[m.FragmentProductID] = struct{}{}
|
|
}
|
|
if err := s.validateRecipeProducts(ctx, req.TargetProductID, req.Materials); err != nil {
|
|
return nil, err
|
|
}
|
|
recipe := &model.FragmentSynthesisRecipes{
|
|
Name: req.Name,
|
|
Description: req.Description,
|
|
TargetProductID: req.TargetProductID,
|
|
Status: req.Status,
|
|
}
|
|
if recipe.Status == 0 {
|
|
recipe.Status = 1
|
|
}
|
|
db := s.repo.GetDbW()
|
|
return recipe, db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
if err := tx.Create(recipe).Error; err != nil {
|
|
return err
|
|
}
|
|
for _, m := range req.Materials {
|
|
mat := &model.FragmentSynthesisRecipeMaterials{
|
|
RecipeID: recipe.ID,
|
|
FragmentProductID: m.FragmentProductID,
|
|
RequiredCount: m.RequiredCount,
|
|
}
|
|
if err := tx.Create(mat).Error; err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func (s *service) ModifyRecipe(ctx context.Context, id int64, req CreateRecipeRequest) error {
|
|
if len(req.Materials) == 0 {
|
|
return fmt.Errorf("materials_required")
|
|
}
|
|
seen := make(map[int64]struct{})
|
|
for _, m := range req.Materials {
|
|
if m.FragmentProductID <= 0 || m.RequiredCount <= 0 {
|
|
return fmt.Errorf("invalid_material")
|
|
}
|
|
if _, ok := seen[m.FragmentProductID]; ok {
|
|
return fmt.Errorf("duplicate_material")
|
|
}
|
|
seen[m.FragmentProductID] = struct{}{}
|
|
}
|
|
if err := s.validateRecipeProducts(ctx, req.TargetProductID, req.Materials); err != nil {
|
|
return err
|
|
}
|
|
db := s.repo.GetDbW()
|
|
return db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
if err := tx.Model(&model.FragmentSynthesisRecipes{}).Where("id = ?", id).Updates(map[string]interface{}{
|
|
"name": req.Name,
|
|
"description": req.Description,
|
|
"target_product_id": req.TargetProductID,
|
|
"status": req.Status,
|
|
"updated_at": time.Now(),
|
|
}).Error; err != nil {
|
|
return err
|
|
}
|
|
if err := tx.Where("recipe_id = ?", id).Delete(&model.FragmentSynthesisRecipeMaterials{}).Error; err != nil {
|
|
return err
|
|
}
|
|
for _, m := range req.Materials {
|
|
mat := &model.FragmentSynthesisRecipeMaterials{
|
|
RecipeID: id,
|
|
FragmentProductID: m.FragmentProductID,
|
|
RequiredCount: m.RequiredCount,
|
|
}
|
|
if err := tx.Create(mat).Error; err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func (s *service) DeleteRecipe(ctx context.Context, id int64) error {
|
|
db := s.repo.GetDbW()
|
|
return db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
if err := tx.Where("recipe_id = ?", id).Delete(&model.FragmentSynthesisRecipeMaterials{}).Error; err != nil {
|
|
return err
|
|
}
|
|
return tx.Where("id = ?", id).Delete(&model.FragmentSynthesisRecipes{}).Error
|
|
})
|
|
}
|
|
|
|
func (s *service) GetAvailableRecipesForUser(ctx context.Context, userID int64) ([]*UserRecipeView, error) {
|
|
db := s.repo.GetDbR()
|
|
var recipes []*model.FragmentSynthesisRecipes
|
|
if err := db.WithContext(ctx).Where("status = 1").Find(&recipes).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
result := make([]*UserRecipeView, 0, len(recipes))
|
|
for _, r := range recipes {
|
|
var materials []*model.FragmentSynthesisRecipeMaterials
|
|
db.WithContext(ctx).Where("recipe_id = ?", r.ID).Find(&materials)
|
|
|
|
var targetProduct model.Products
|
|
db.WithContext(ctx).Where("id = ?", r.TargetProductID).First(&targetProduct)
|
|
|
|
view := &UserRecipeView{
|
|
ID: r.ID,
|
|
Name: r.Name,
|
|
Description: r.Description,
|
|
TargetProduct: &targetProduct,
|
|
Materials: make([]UserMaterialView, 0, len(materials)),
|
|
}
|
|
|
|
maxSynthesizeCount := int64(0)
|
|
initialized := false
|
|
for _, m := range materials {
|
|
var p model.Products
|
|
db.WithContext(ctx).Where("id = ?", m.FragmentProductID).First(&p)
|
|
|
|
var ownedCount int64
|
|
db.WithContext(ctx).Model(&model.UserInventory{}).
|
|
Where("user_id = ? AND product_id = ? AND status = 1", userID, m.FragmentProductID).
|
|
Count(&ownedCount)
|
|
|
|
currentCount := int64(0)
|
|
if m.RequiredCount > 0 {
|
|
currentCount = ownedCount / int64(m.RequiredCount)
|
|
}
|
|
if !initialized || currentCount < maxSynthesizeCount {
|
|
maxSynthesizeCount = currentCount
|
|
initialized = true
|
|
}
|
|
|
|
image := ""
|
|
if p.ImagesJSON != "" {
|
|
var imgs []string
|
|
if json.Unmarshal([]byte(p.ImagesJSON), &imgs) == nil && len(imgs) > 0 {
|
|
image = imgs[0]
|
|
}
|
|
}
|
|
view.Materials = append(view.Materials, UserMaterialView{
|
|
FragmentProductID: m.FragmentProductID,
|
|
Name: p.Name,
|
|
Image: image,
|
|
RequiredCount: m.RequiredCount,
|
|
OwnedCount: ownedCount,
|
|
})
|
|
}
|
|
view.MaxSynthesizeCount = maxSynthesizeCount
|
|
view.CanSynthesize = maxSynthesizeCount > 0
|
|
result = append(result, view)
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func (s *service) Synthesize(ctx context.Context, userID int64, recipeID int64) (*model.UserInventory, error) {
|
|
result, err := s.batchSynthesize(ctx, userID, recipeID, 1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(result.ProducedInventoryIDs) == 0 {
|
|
return nil, fmt.Errorf("synthesis_failed")
|
|
}
|
|
|
|
var newInv model.UserInventory
|
|
if err := s.repo.GetDbR().WithContext(ctx).Where("id = ?", result.ProducedInventoryIDs[0]).First(&newInv).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
return &newInv, nil
|
|
}
|
|
|
|
func (s *service) BatchSynthesize(ctx context.Context, userID int64, recipeID int64) (*BatchSynthesizeResult, error) {
|
|
return s.batchSynthesize(ctx, userID, recipeID, 0)
|
|
}
|
|
|
|
func (s *service) batchSynthesize(ctx context.Context, userID int64, recipeID int64, limitTimes int64) (*BatchSynthesizeResult, error) {
|
|
db := s.repo.GetDbR()
|
|
|
|
var recipe model.FragmentSynthesisRecipes
|
|
if err := db.WithContext(ctx).Where("id = ? AND status = 1", recipeID).First(&recipe).Error; err != nil {
|
|
return nil, fmt.Errorf("recipe_not_found")
|
|
}
|
|
|
|
var materials []*model.FragmentSynthesisRecipeMaterials
|
|
db.WithContext(ctx).Where("recipe_id = ?", recipeID).Find(&materials)
|
|
if len(materials) == 0 {
|
|
return nil, fmt.Errorf("recipe_no_materials")
|
|
}
|
|
|
|
var targetProduct model.Products
|
|
if err := db.WithContext(ctx).Where("id = ? AND status = 1", recipe.TargetProductID).First(&targetProduct).Error; err != nil {
|
|
return nil, fmt.Errorf("target_product_unavailable")
|
|
}
|
|
|
|
type materialConsume struct {
|
|
ProductID int64
|
|
Required int32
|
|
InventoryIDs []int64
|
|
}
|
|
toConsume := make([]materialConsume, 0, len(materials))
|
|
maxTimes := int64(0)
|
|
initialized := false
|
|
|
|
for _, m := range materials {
|
|
var ownedCount int64
|
|
db.WithContext(ctx).Model(&model.UserInventory{}).
|
|
Where("user_id = ? AND product_id = ? AND status = 1", userID, m.FragmentProductID).
|
|
Count(&ownedCount)
|
|
|
|
currentTimes := int64(0)
|
|
if m.RequiredCount > 0 {
|
|
currentTimes = ownedCount / int64(m.RequiredCount)
|
|
}
|
|
if !initialized || currentTimes < maxTimes {
|
|
maxTimes = currentTimes
|
|
initialized = true
|
|
}
|
|
}
|
|
|
|
if limitTimes > 0 && maxTimes > limitTimes {
|
|
maxTimes = limitTimes
|
|
}
|
|
if maxTimes <= 0 {
|
|
return nil, fmt.Errorf("insufficient_fragments")
|
|
}
|
|
|
|
for _, m := range materials {
|
|
requiredTotal := int(m.RequiredCount) * int(maxTimes)
|
|
var invList []*model.UserInventory
|
|
db.WithContext(ctx).
|
|
Where("user_id = ? AND product_id = ? AND status = 1", userID, m.FragmentProductID).
|
|
Order("id ASC").
|
|
Limit(requiredTotal).
|
|
Find(&invList)
|
|
|
|
if len(invList) < requiredTotal {
|
|
return nil, fmt.Errorf("insufficient_fragments")
|
|
}
|
|
ids := make([]int64, len(invList))
|
|
for i, inv := range invList {
|
|
ids[i] = inv.ID
|
|
}
|
|
toConsume = append(toConsume, materialConsume{
|
|
ProductID: m.FragmentProductID,
|
|
Required: m.RequiredCount,
|
|
InventoryIDs: ids,
|
|
})
|
|
}
|
|
|
|
result := &BatchSynthesizeResult{
|
|
RecipeID: recipeID,
|
|
TargetProductID: recipe.TargetProductID,
|
|
TargetProductName: targetProduct.Name,
|
|
SynthesizedCount: maxTimes,
|
|
}
|
|
|
|
wdb := s.repo.GetDbW()
|
|
err := wdb.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
consumedByRound := make([][]int64, int(maxTimes))
|
|
allConsumedCount := 0
|
|
|
|
for _, mc := range toConsume {
|
|
var locked []model.UserInventory
|
|
query := tx.WithContext(ctx).Where("id IN ? AND user_id = ? AND status = 1", mc.InventoryIDs, userID)
|
|
if tx.Dialector.Name() != "sqlite" {
|
|
query = query.Clauses(clause.Locking{Strength: "UPDATE"})
|
|
}
|
|
if err := query.Find(&locked).Error; err != nil {
|
|
return err
|
|
}
|
|
if len(locked) < len(mc.InventoryIDs) {
|
|
return fmt.Errorf("insufficient_fragments")
|
|
}
|
|
updates := map[string]interface{}{
|
|
"status": 2,
|
|
"updated_at": time.Now(),
|
|
}
|
|
if tx.Dialector.Name() == "sqlite" {
|
|
updates["remark"] = gorm.Expr("COALESCE(remark, '') || ?", fmt.Sprintf("|synthesis_consumed:recipe_%d", recipeID))
|
|
} else {
|
|
updates["remark"] = gorm.Expr("CONCAT(IFNULL(remark,''), ?)", fmt.Sprintf("|synthesis_consumed:recipe_%d", recipeID))
|
|
}
|
|
if err := tx.Model(&model.UserInventory{}).
|
|
Where("id IN ? AND user_id = ? AND status = 1", mc.InventoryIDs, userID).
|
|
Updates(updates).Error; err != nil {
|
|
return err
|
|
}
|
|
allConsumedCount += len(mc.InventoryIDs)
|
|
for round := int64(0); round < maxTimes; round++ {
|
|
start := int(round) * int(mc.Required)
|
|
end := start + int(mc.Required)
|
|
consumedByRound[round] = append(consumedByRound[round], mc.InventoryIDs[start:end]...)
|
|
}
|
|
}
|
|
|
|
result.ConsumedInventoryCount = allConsumedCount
|
|
result.ProducedInventoryIDs = make([]int64, 0, int(maxTimes))
|
|
for round := int64(0); round < maxTimes; round++ {
|
|
newInv := model.UserInventory{
|
|
UserID: userID,
|
|
ProductID: recipe.TargetProductID,
|
|
ValueCents: targetProduct.Price,
|
|
Status: 1,
|
|
Remark: fmt.Sprintf("batch_synthesis_produced:recipe_%d:round_%d", recipeID, round+1),
|
|
}
|
|
if err := tx.Omit("ValueSnapshotAt", "ShippingNo").Create(&newInv).Error; err != nil {
|
|
return err
|
|
}
|
|
result.ProducedInventoryIDs = append(result.ProducedInventoryIDs, newInv.ID)
|
|
|
|
consumedJSON, _ := json.Marshal(consumedByRound[round])
|
|
log := &model.FragmentSynthesisLogs{
|
|
UserID: userID,
|
|
RecipeID: recipeID,
|
|
ConsumedInventoryIDs: string(consumedJSON),
|
|
ProducedInventoryID: newInv.ID,
|
|
}
|
|
if err := tx.Create(log).Error; err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func (s *service) ListLogs(ctx context.Context, page, size int, userID *int64) ([]*SynthesisLogView, int64, error) {
|
|
if page <= 0 {
|
|
page = 1
|
|
}
|
|
if size <= 0 {
|
|
size = 20
|
|
}
|
|
db := s.repo.GetDbR()
|
|
q := db.WithContext(ctx).Model(&model.FragmentSynthesisLogs{})
|
|
if userID != nil && *userID > 0 {
|
|
q = q.Where("user_id = ?", *userID)
|
|
}
|
|
var total int64
|
|
if err := q.Count(&total).Error; err != nil {
|
|
return nil, 0, err
|
|
}
|
|
var logs []*model.FragmentSynthesisLogs
|
|
if err := q.Order("id DESC").Offset((page - 1) * size).Limit(size).Find(&logs).Error; err != nil {
|
|
return nil, 0, err
|
|
}
|
|
result := make([]*SynthesisLogView, 0, len(logs))
|
|
for _, l := range logs {
|
|
view := &SynthesisLogView{
|
|
ID: l.ID,
|
|
CreatedAt: l.CreatedAt,
|
|
UserID: l.UserID,
|
|
RecipeID: l.RecipeID,
|
|
ProducedInventoryID: l.ProducedInventoryID,
|
|
}
|
|
var ids []int64
|
|
json.Unmarshal([]byte(l.ConsumedInventoryIDs), &ids)
|
|
view.ConsumedCount = len(ids)
|
|
|
|
var recipe model.FragmentSynthesisRecipes
|
|
if db.WithContext(ctx).Unscoped().Where("id = ?", l.RecipeID).First(&recipe).Error == nil {
|
|
view.RecipeName = recipe.Name
|
|
var p model.Products
|
|
if db.WithContext(ctx).Where("id = ?", recipe.TargetProductID).First(&p).Error == nil {
|
|
view.TargetProductName = p.Name
|
|
}
|
|
}
|
|
result = append(result, view)
|
|
}
|
|
return result, total, nil
|
|
}
|
|
|
|
// validateRecipeProducts 校验材料必须属于碎片分类,目标商品必须不属于碎片分类
|
|
func (s *service) validateRecipeProducts(ctx context.Context, targetProductID int64, materials []MaterialInput) error {
|
|
db := s.repo.GetDbR()
|
|
|
|
var fragCatIDs []int64
|
|
db.WithContext(ctx).Raw("SELECT id FROM product_categories WHERE is_fragment = 1 AND deleted_at IS NULL").Scan(&fragCatIDs)
|
|
fragCatSet := make(map[int64]struct{}, len(fragCatIDs))
|
|
for _, id := range fragCatIDs {
|
|
fragCatSet[id] = struct{}{}
|
|
}
|
|
|
|
var targetProduct model.Products
|
|
if err := db.WithContext(ctx).Where("id = ?", targetProductID).First(&targetProduct).Error; err != nil {
|
|
return fmt.Errorf("target_product_not_found")
|
|
}
|
|
if _, isFrag := fragCatSet[targetProduct.CategoryID]; isFrag {
|
|
return fmt.Errorf("target_product_cannot_be_fragment")
|
|
}
|
|
|
|
for _, m := range materials {
|
|
var p model.Products
|
|
if err := db.WithContext(ctx).Where("id = ?", m.FragmentProductID).First(&p).Error; err != nil {
|
|
return fmt.Errorf("material_product_not_found:%d", m.FragmentProductID)
|
|
}
|
|
if _, isFrag := fragCatSet[p.CategoryID]; !isFrag {
|
|
return fmt.Errorf("material_must_be_fragment:%d", m.FragmentProductID)
|
|
}
|
|
}
|
|
return nil
|
|
}
|