CraftService.php 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258
  1. <?php
  2. namespace App\Module\GameItems\Services;
  3. use App\Module\Game\Enums\REWARD_SOURCE_TYPE;
  4. use App\Module\GameItems\Models\ItemRecipe;
  5. use App\Module\GameItems\Models\ItemCraftLog;
  6. use App\Module\GameItems\Models\ItemUserRecipe;
  7. use App\Module\Game\Services\ConsumeService;
  8. use App\Module\Game\Services\RewardService;
  9. use Exception;
  10. use Illuminate\Database\Eloquent\Collection;
  11. use Illuminate\Support\Facades\DB;
  12. use Illuminate\Support\Facades\Log;
  13. use UCore\Dto\Res;
  14. use UCore\Exception\LogicException;
  15. use UCore\Exception\ValidateException;
  16. use UCore\Helper\Logger;
  17. /**
  18. * 物品合成服务类
  19. *
  20. * 提供物品合成相关的服务,包括合成物品、获取用户可合成配方列表、解锁配方等功能。
  21. * 该类是物品合成模块对外提供服务的主要入口,封装了物品合成的复杂逻辑。
  22. *
  23. * 所有方法均为静态方法,可直接通过类名调用。
  24. */
  25. class CraftService
  26. {
  27. /**
  28. * 合成物品
  29. *
  30. * @param int $userId 用户ID
  31. * @param int $recipeId 配方ID
  32. * @param array $options 选项
  33. * @return Res
  34. */
  35. public static function craftItem(int $userId, int $recipeId,int $quantity, array $options = []):Res
  36. {
  37. try {
  38. // 获取配方信息
  39. $recipe = ItemRecipe::with(['consumeGroup', 'rewardGroup'])->findOrFail($recipeId);
  40. // 检查配方是否激活
  41. if (!$recipe->is_active) {
  42. throw new ValidateException("配方未激活");
  43. }
  44. // 检查用户是否可以合成该配方
  45. $canCraft = $recipe->canCraftByUser($userId);
  46. if (!$canCraft['can_craft']) {
  47. throw new ValidateException($canCraft['reason']);
  48. }
  49. // 检查消耗组是否存在
  50. if (!$recipe->consume_group_id || !$recipe->consumeGroup) {
  51. throw new ValidateException("配方消耗组不存在");
  52. }
  53. // 检查奖励组是否存在
  54. if (!$recipe->reward_group_id || !$recipe->rewardGroup) {
  55. throw new ValidateException("配方奖励组不存在");
  56. }
  57. // 获取合成数量
  58. // 开启事务
  59. DB::beginTransaction();
  60. // 执行消耗组
  61. $consumeResult = ConsumeService::executeConsume(
  62. $userId,
  63. $recipe->consume_group_id,
  64. REWARD_SOURCE_TYPE::CRAFT,
  65. $recipeId,
  66. true, // 检查消耗条件
  67. $quantity // 使用数量作为倍数
  68. );
  69. if (!$consumeResult->success) {
  70. throw new LogicException("消耗失败: " . $consumeResult->message);
  71. }
  72. // 判断合成是否成功(基于配方成功率)
  73. $isSuccess = self::rollCraftSuccess($recipe->success_rate);
  74. $rewardItems = [];
  75. // 如果合成成功,执行奖励组
  76. if ($isSuccess) {
  77. $rewardResult = RewardService::grantReward(
  78. $userId,
  79. $recipe->reward_group_id,
  80. REWARD_SOURCE_TYPE::CRAFT,
  81. $recipeId,
  82. $quantity
  83. );
  84. if ($rewardResult->success) {
  85. $rewardItems = $rewardResult->items;
  86. } else {
  87. // 奖励发放失败,视为合成失败
  88. $isSuccess = false;
  89. }
  90. }
  91. // 更新用户配方使用记录
  92. $userRecipe = ItemUserRecipe::firstOrCreate(
  93. ['user_id' => $userId, 'recipe_id' => $recipeId],
  94. ['is_unlocked' => true, 'unlock_time' => now(), 'craft_count' => 0]
  95. );
  96. $userRecipe->incrementCraftCount($quantity);
  97. // 创建合成记录
  98. $craftLog = new ItemCraftLog([
  99. 'user_id' => $userId,
  100. 'recipe_id' => $recipeId,
  101. 'materials' => $consumeResult->data['consumed'] ?? [],
  102. 'result_item_id' => $isSuccess && !empty($rewardItems) ? $rewardItems[0]->targetId : null,
  103. 'result_instance_id' => null,
  104. 'result_quantity' => $isSuccess && !empty($rewardItems) ? $rewardItems[0]->quantity : 0,
  105. 'is_success' => $isSuccess,
  106. 'craft_time' => now(),
  107. 'ip_address' => $options['ip_address'] ?? request()->ip(),
  108. 'device_info' => $options['device_info'] ?? request()->userAgent(),
  109. ]);
  110. $craftLog->save();
  111. // 提交事务
  112. DB::commit();
  113. // 返回结果
  114. return Res::success();
  115. // return [
  116. // 'success' => $isSuccess,
  117. // 'consumed' => $consumeResult['consumed'] ?? [],
  118. // 'rewards' => $isSuccess ? $rewardItems : []
  119. // ];
  120. } catch (\Exception $e) {
  121. // 回滚事务
  122. if (DB::transactionLevel() > 0) {
  123. DB::rollBack();
  124. }
  125. Log::error('合成物品失败', [
  126. 'user_id' => $userId,
  127. 'recipe_id' => $recipeId,
  128. 'error' => $e->getMessage(),
  129. 'trace' => $e->getTraceAsString()
  130. ]);
  131. return Res::error('合成失败: ' . $e->getMessage());
  132. }
  133. }
  134. /**
  135. * 获取用户可合成配方列表
  136. *
  137. * @param int $userId 用户ID
  138. * @param array $filters 过滤条件
  139. * @return Collection 配方列表
  140. */
  141. public static function getUserAvailableRecipes(int $userId, array $filters = []): Collection
  142. {
  143. $query = ItemRecipe::with(['consumeGroup', 'rewardGroup', 'conditionGroup'])
  144. ->where('is_active', true)
  145. ->whereHas('userRecipes', function ($q) use ($userId) {
  146. $q->where('user_id', $userId)
  147. ->where('is_unlocked', true);
  148. });
  149. // 应用过滤条件
  150. if (isset($filters['category_id'])) {
  151. $query->where('category_id', $filters['category_id']);
  152. }
  153. return $query->orderBy('sort_order', 'asc')->get();
  154. }
  155. /**
  156. * 解锁用户配方
  157. *
  158. * @param int $userId 用户ID
  159. * @param int $recipeId 配方ID
  160. * @return bool 是否成功
  161. */
  162. public static function unlockRecipe(int $userId, int $recipeId): bool
  163. {
  164. try {
  165. $recipe = ItemRecipe::findOrFail($recipeId);
  166. // 检查配方是否激活
  167. if (!$recipe->is_active) {
  168. return false;
  169. }
  170. // 检查是否已解锁
  171. $userRecipe = ItemUserRecipe::where('user_id', $userId)
  172. ->where('recipe_id', $recipeId)
  173. ->first();
  174. if ($userRecipe && $userRecipe->is_unlocked) {
  175. return true; // 已经解锁
  176. }
  177. // 创建或更新用户配方记录
  178. if (!$userRecipe) {
  179. $userRecipe = new ItemUserRecipe([
  180. 'user_id' => $userId,
  181. 'recipe_id' => $recipeId,
  182. 'is_unlocked' => true,
  183. 'unlock_time' => now(),
  184. 'craft_count' => 0
  185. ]);
  186. $userRecipe->save();
  187. } else {
  188. $userRecipe->is_unlocked = true;
  189. $userRecipe->unlock_time = now();
  190. $userRecipe->save();
  191. }
  192. return true;
  193. } catch (\Exception $e) {
  194. Logger::error('解锁配方失败', [
  195. 'user_id' => $userId,
  196. 'recipe_id' => $recipeId,
  197. 'error' => $e->getMessage(),
  198. 'trace' => $e->getTraceAsString()
  199. ]);
  200. return false;
  201. }
  202. }
  203. /**
  204. * 根据成功率判断合成是否成功
  205. *
  206. * @param float $successRate 成功率(百分比,100.00 = 100%)
  207. * @return bool 是否成功
  208. */
  209. private static function rollCraftSuccess(float $successRate): bool
  210. {
  211. // 成功率小于等于0,必定失败
  212. if ($successRate <= 0) {
  213. return false;
  214. }
  215. // 成功率大于等于100,必定成功
  216. if ($successRate >= 100) {
  217. return true;
  218. }
  219. // 生成1-100的随机数进行概率判断
  220. $randomNumber = mt_rand(1, 100);
  221. return $randomNumber <= $successRate;
  222. }
  223. }