452 lines
14 KiB
Go
452 lines
14 KiB
Go
package synthesis
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"time"
|
|
|
|
"bindbox-game/internal/repository/mysql"
|
|
"bindbox-game/internal/repository/mysql/model"
|
|
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
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)
|
|
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"`
|
|
Materials []UserMaterialView `json:"materials"`
|
|
}
|
|
|
|
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,
|
|
CanSynthesize: true,
|
|
Materials: make([]UserMaterialView, 0, len(materials)),
|
|
}
|
|
|
|
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)
|
|
|
|
if ownedCount < int64(m.RequiredCount) {
|
|
view.CanSynthesize = false
|
|
}
|
|
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,
|
|
})
|
|
}
|
|
result = append(result, view)
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func (s *service) Synthesize(ctx context.Context, userID int64, recipeID int64) (*model.UserInventory, 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))
|
|
|
|
for _, m := range materials {
|
|
var invList []*model.UserInventory
|
|
db.WithContext(ctx).
|
|
Where("user_id = ? AND product_id = ? AND status = 1", userID, m.FragmentProductID).
|
|
Order("id ASC").
|
|
Limit(int(m.RequiredCount)).
|
|
Find(&invList)
|
|
|
|
if int32(len(invList)) < m.RequiredCount {
|
|
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,
|
|
})
|
|
}
|
|
|
|
var newInv model.UserInventory
|
|
wdb := s.repo.GetDbW()
|
|
err := wdb.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
allConsumedIDs := make([]int64, 0)
|
|
for _, mc := range toConsume {
|
|
var locked []model.UserInventory
|
|
if err := tx.Raw("SELECT * FROM user_inventory WHERE id IN ? AND user_id = ? AND status = 1 FOR UPDATE", mc.InventoryIDs, userID).Scan(&locked).Error; err != nil {
|
|
return err
|
|
}
|
|
if int32(len(locked)) < mc.Required {
|
|
return fmt.Errorf("insufficient_fragments")
|
|
}
|
|
if err := tx.Exec(
|
|
"UPDATE user_inventory SET status = 2, updated_at = NOW(3), remark = CONCAT(IFNULL(remark,''), '|synthesis_consumed:recipe_', ?) WHERE id IN ? AND user_id = ? AND status = 1",
|
|
recipeID, mc.InventoryIDs, userID,
|
|
).Error; err != nil {
|
|
return err
|
|
}
|
|
allConsumedIDs = append(allConsumedIDs, mc.InventoryIDs...)
|
|
}
|
|
|
|
newInv = model.UserInventory{
|
|
UserID: userID,
|
|
ProductID: recipe.TargetProductID,
|
|
ValueCents: targetProduct.Price,
|
|
Status: 1,
|
|
Remark: fmt.Sprintf("synthesis_produced:recipe_%d", recipeID),
|
|
}
|
|
if err := tx.Omit("ValueSnapshotAt", "ShippingNo").Create(&newInv).Error; err != nil {
|
|
return err
|
|
}
|
|
|
|
consumedJSON, _ := json.Marshal(allConsumedIDs)
|
|
log := &model.FragmentSynthesisLogs{
|
|
UserID: userID,
|
|
RecipeID: recipeID,
|
|
ConsumedInventoryIDs: string(consumedJSON),
|
|
ProducedInventoryID: newInv.ID,
|
|
}
|
|
return tx.Create(log).Error
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &newInv, 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
|
|
}
|