CraftService.php 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  1. <?php
  2. namespace App\Module\GameItems\Services;
  3. use App\Module\GameItems\Logics\Recipe as RecipeLogic;
  4. use App\Module\GameItems\Models\ItemRecipe;
  5. use App\Module\GameItems\Models\ItemRecipeMaterial;
  6. use App\Module\GameItems\Models\ItemCraftLog;
  7. use App\Module\GameItems\Models\ItemUserRecipe;
  8. use Exception;
  9. use Illuminate\Database\Eloquent\Collection;
  10. use Illuminate\Support\Facades\DB;
  11. use Illuminate\Support\Facades\Log;
  12. /**
  13. * 物品合成服务类
  14. *
  15. * 提供物品合成相关的服务,包括合成物品、获取用户可合成配方列表、解锁配方等功能。
  16. * 该类是物品合成模块对外提供服务的主要入口,封装了物品合成的复杂逻辑。
  17. *
  18. * 所有方法均为静态方法,可直接通过类名调用。
  19. */
  20. class CraftService
  21. {
  22. /**
  23. * 合成物品
  24. *
  25. * @param int $userId 用户ID
  26. * @param int $recipeId 配方ID
  27. * @param array $options 选项
  28. * @return array|bool 合成结果或失败标志
  29. */
  30. public static function craftItem(int $userId, int $recipeId, array $options = [])
  31. {
  32. try {
  33. // 获取配方信息
  34. $recipe = ItemRecipe::findOrFail($recipeId);
  35. // 检查配方是否激活
  36. if (!$recipe->is_active) {
  37. throw new Exception("配方未激活");
  38. }
  39. // 检查用户是否可以合成该配方
  40. $canCraft = $recipe->canCraftByUser($userId);
  41. if (!$canCraft['can_craft']) {
  42. throw new Exception($canCraft['reason']);
  43. }
  44. // 获取配方材料
  45. $materials = ItemRecipeMaterial::where('recipe_id', $recipeId)->get();
  46. if ($materials->isEmpty()) {
  47. throw new Exception("配方材料不存在");
  48. }
  49. // 获取合成数量
  50. $quantity = $options['quantity'] ?? 1;
  51. // 开启事务
  52. DB::beginTransaction();
  53. // 消耗材料
  54. $materialsUsed = [];
  55. foreach ($materials as $material) {
  56. $materialItemId = $material->material_item_id;
  57. $materialQuantity = $material->quantity * $quantity;
  58. // 检查用户是否有足够的材料
  59. $userItems = ItemService::getUserItems($userId, ['item_id' => $materialItemId]);
  60. $totalQuantity = $userItems->sum('quantity');
  61. if ($totalQuantity < $materialQuantity) {
  62. throw new Exception("材料不足:" . $material->materialItem->name);
  63. }
  64. // 消耗材料
  65. ItemService::consumeItem($userId, $materialItemId, null, $materialQuantity, [
  66. 'source_type' => 'craft',
  67. 'source_id' => $recipeId,
  68. 'details' => [
  69. 'recipe_id' => $recipeId,
  70. 'recipe_name' => $recipe->name
  71. ]
  72. ]);
  73. $materialsUsed[] = [
  74. 'item_id' => $materialItemId,
  75. 'item_name' => $material->materialItem->name,
  76. 'quantity' => $materialQuantity
  77. ];
  78. }
  79. // 计算成功率
  80. $isSuccess = true;
  81. if ($recipe->success_rate < 1.0) {
  82. $isSuccess = (mt_rand(1, 10000) <= $recipe->success_rate * 10000);
  83. }
  84. // 如果成功,添加结果物品到用户背包
  85. $resultData = null;
  86. if ($isSuccess) {
  87. // 计算结果数量
  88. $resultQuantity = $recipe->result_min_quantity * $quantity;
  89. if ($recipe->result_max_quantity > $recipe->result_min_quantity) {
  90. $resultQuantity = mt_rand($recipe->result_min_quantity, $recipe->result_max_quantity) * $quantity;
  91. }
  92. // 添加物品到用户背包
  93. $resultData = ItemService::addItem($userId, $recipe->result_item_id, $resultQuantity, [
  94. 'source_type' => 'craft',
  95. 'source_id' => $recipeId,
  96. 'details' => [
  97. 'recipe_id' => $recipeId,
  98. 'recipe_name' => $recipe->name
  99. ]
  100. ]);
  101. }
  102. // 更新用户配方使用记录
  103. $userRecipe = ItemUserRecipe::firstOrCreate(
  104. ['user_id' => $userId, 'recipe_id' => $recipeId],
  105. ['is_unlocked' => true, 'unlock_time' => now(), 'craft_count' => 0]
  106. );
  107. $userRecipe->incrementCraftCount($quantity);
  108. // 创建合成记录
  109. $craftLog = new ItemCraftLog([
  110. 'user_id' => $userId,
  111. 'recipe_id' => $recipeId,
  112. 'materials' => $materialsUsed,
  113. 'result_item_id' => $isSuccess ? $recipe->result_item_id : null,
  114. 'result_instance_id' => $isSuccess && isset($resultData['instance_id']) ? $resultData['instance_id'] : null,
  115. 'result_quantity' => $isSuccess ? $resultQuantity : 0,
  116. 'is_success' => $isSuccess,
  117. 'craft_time' => now(),
  118. 'ip_address' => $options['ip_address'] ?? request()->ip(),
  119. 'device_info' => $options['device_info'] ?? request()->userAgent(),
  120. ]);
  121. $craftLog->save();
  122. // 提交事务
  123. DB::commit();
  124. // 返回结果
  125. return [
  126. 'success' => $isSuccess,
  127. 'materials_used' => $materialsUsed,
  128. 'result' => $isSuccess ? [
  129. 'item_id' => $recipe->result_item_id,
  130. 'item_name' => $recipe->resultItem->name,
  131. 'quantity' => $resultQuantity,
  132. 'instance_id' => $resultData['instance_id'] ?? null
  133. ] : null
  134. ];
  135. } catch (Exception $e) {
  136. // 回滚事务
  137. if (DB::transactionLevel() > 0) {
  138. DB::rollBack();
  139. }
  140. Log::error('合成物品失败', [
  141. 'user_id' => $userId,
  142. 'recipe_id' => $recipeId,
  143. 'error' => $e->getMessage(),
  144. 'trace' => $e->getTraceAsString()
  145. ]);
  146. return false;
  147. }
  148. }
  149. /**
  150. * 获取用户可合成配方列表
  151. *
  152. * @param int $userId 用户ID
  153. * @param array $filters 过滤条件
  154. * @return Collection 配方列表
  155. */
  156. public static function getUserAvailableRecipes(int $userId, array $filters = []): Collection
  157. {
  158. $query = ItemRecipe::where('is_active', true)
  159. ->where(function ($q) use ($userId) {
  160. $q->where('is_default_unlocked', true)
  161. ->orWhereHas('userRecipes', function ($q) use ($userId) {
  162. $q->where('user_id', $userId)
  163. ->where('is_unlocked', true);
  164. });
  165. });
  166. // 应用过滤条件
  167. if (isset($filters['category_id'])) {
  168. $query->where('category_id', $filters['category_id']);
  169. }
  170. return $query->orderBy('sort_order', 'asc')->get();
  171. }
  172. /**
  173. * 解锁用户配方
  174. *
  175. * @param int $userId 用户ID
  176. * @param int $recipeId 配方ID
  177. * @return bool 是否成功
  178. */
  179. public static function unlockRecipe(int $userId, int $recipeId): bool
  180. {
  181. try {
  182. $recipe = ItemRecipe::findOrFail($recipeId);
  183. // 检查配方是否激活
  184. if (!$recipe->is_active) {
  185. return false;
  186. }
  187. // 检查是否已解锁
  188. $userRecipe = ItemUserRecipe::where('user_id', $userId)
  189. ->where('recipe_id', $recipeId)
  190. ->first();
  191. if ($userRecipe && $userRecipe->is_unlocked) {
  192. return true; // 已经解锁
  193. }
  194. // 创建或更新用户配方记录
  195. if (!$userRecipe) {
  196. $userRecipe = new ItemUserRecipe([
  197. 'user_id' => $userId,
  198. 'recipe_id' => $recipeId,
  199. 'is_unlocked' => true,
  200. 'unlock_time' => now(),
  201. 'craft_count' => 0
  202. ]);
  203. $userRecipe->save();
  204. } else {
  205. $userRecipe->is_unlocked = true;
  206. $userRecipe->unlock_time = now();
  207. $userRecipe->save();
  208. }
  209. return true;
  210. } catch (Exception $e) {
  211. Log::error('解锁配方失败', [
  212. 'user_id' => $userId,
  213. 'recipe_id' => $recipeId,
  214. 'error' => $e->getMessage(),
  215. 'trace' => $e->getTraceAsString()
  216. ]);
  217. return false;
  218. }
  219. }
  220. }