RewardLogic.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
  1. <?php
  2. namespace App\Module\Game\Logics;
  3. use App\Module\Game\Dtos\RewardGroupDto;
  4. use App\Module\Game\Dtos\RewardItemDto;
  5. use App\Module\Game\Dtos\RewardResultDto;
  6. use App\Module\Game\Enums\REWARD_MODE;
  7. use App\Module\Game\Events\RewardGrantedEvent;
  8. use App\Module\Game\Models\GameRewardGroup;
  9. use App\Module\Game\Models\GameRewardLog;
  10. use App\Module\Game\Services\PityService;
  11. use App\Module\Game\Logics\WeightSelectionReward;
  12. use App\Module\Game\Logics\IndependentProbabilityReward;
  13. use App\Module\Game\Logics\RewardProcessors\RewardProcessorDispatcher;
  14. use Exception;
  15. use Illuminate\Support\Facades\Log;
  16. use UCore\Db\Helper;
  17. use UCore\Exception\LogicException;
  18. /**
  19. * 奖励处理逻辑类
  20. *
  21. * 负责处理奖励的发放、记录等内部逻辑
  22. */
  23. class RewardLogic
  24. {
  25. /**
  26. * 获取奖励组
  27. *
  28. * @param int|string $groupIdOrCode 奖励组ID或编码
  29. * @return RewardGroupDto|null
  30. */
  31. public function getRewardGroup($groupIdOrCode): ?RewardGroupDto
  32. {
  33. $query = GameRewardGroup::with('rewardItems');
  34. if (is_numeric($groupIdOrCode)) {
  35. $group = $query->find($groupIdOrCode);
  36. } else {
  37. $group = $query->where('code', $groupIdOrCode)->first();
  38. }
  39. if (!$group) {
  40. return null;
  41. }
  42. return RewardGroupDto::fromModel($group, true);
  43. }
  44. /**
  45. * 发放奖励
  46. *
  47. * @param int $userId 用户ID
  48. * @param int|string $groupIdOrCode 奖励组ID或编码
  49. * @param string $sourceType 来源类型
  50. * @param int $sourceId 来源ID
  51. * @return RewardResultDto 奖励结果
  52. */
  53. public function grantReward(int $userId, $groupIdOrCode, string $sourceType, int $sourceId,int $multiplier = 1): RewardResultDto
  54. {
  55. try {
  56. // 获取奖励组
  57. $groupDto = $this->getRewardGroup($groupIdOrCode);
  58. if (!$groupDto) {
  59. return RewardResultDto::fail("奖励组不存在: {$groupIdOrCode}");
  60. }
  61. // 确定要发放的奖励项
  62. $rewardItems = $this->determineRewardItems($groupDto, $userId,$multiplier);
  63. if (empty($rewardItems)) {
  64. return RewardResultDto::fail("奖励组中没有可发放的奖励项");
  65. }
  66. // 检查事务是否已开启
  67. Helper::check_tr();
  68. // 发放各类奖励
  69. foreach ($rewardItems as $item) {
  70. RewardProcessorDispatcher::process($userId, $item, $sourceType, $sourceId);
  71. }
  72. // 记录奖励日志
  73. $this->logReward($userId, $groupDto->id, $sourceType, $sourceId, $rewardItems);
  74. // 触发奖励发放事件
  75. event(new RewardGrantedEvent($userId, $groupDto->id, $groupDto->code, $sourceType, $sourceId, $rewardItems));
  76. // 返回成功结果
  77. return RewardResultDto::success(
  78. $userId,
  79. $groupDto->id,
  80. $groupDto->code,
  81. $groupDto->name,
  82. $sourceType,
  83. $sourceId,
  84. $rewardItems
  85. );
  86. } catch (Exception $e) {
  87. Log::error("发放奖励失败", [
  88. 'userId' => $userId,
  89. 'groupIdOrCode' => $groupIdOrCode,
  90. 'sourceType' => $sourceType,
  91. 'sourceId' => $sourceId,
  92. 'error' => $e->getMessage(),
  93. 'trace' => $e->getTraceAsString()
  94. ]);
  95. return RewardResultDto::fail("发放奖励失败: " . $e->getMessage());
  96. }
  97. }
  98. /**
  99. * 确定要发放的奖励项
  100. *
  101. * @param RewardGroupDto $groupDto 奖励组DTO
  102. * @param int|null $userId 用户ID(用于保底机制)
  103. * @return RewardItemDto[] 要发放的奖励项
  104. */
  105. private function determineRewardItems(RewardGroupDto $groupDto, ?int $userId = null,int $multiplier = 1): array
  106. {
  107. // 检查奖励模式
  108. $rewardMode = $groupDto->rewardMode ?? REWARD_MODE::WEIGHT_SELECTION->value;
  109. if ($rewardMode == REWARD_MODE::INDEPENDENT_PROBABILITY->value) {
  110. if($multiplier > 1){
  111. throw new LogicException('倍率,只支持权重模式');
  112. }
  113. // 独立概率模式
  114. return IndependentProbabilityReward::process($groupDto, $userId);
  115. } else {
  116. // 权重选择模式(默认)
  117. return WeightSelectionReward::process($groupDto, $userId,$multiplier);
  118. }
  119. }
  120. /**
  121. * 记录奖励日志
  122. *
  123. * @param int $userId 用户ID
  124. * @param int $groupId 奖励组ID
  125. * @param string $sourceType 来源类型
  126. * @param int $sourceId 来源ID
  127. * @param RewardItemDto[] $items 发放的奖励项
  128. * @return GameRewardLog
  129. */
  130. private function logReward(int $userId, int $groupId, string $sourceType, int $sourceId, array $items): GameRewardLog
  131. {
  132. // 将DTO转换为可存储的数组
  133. $itemsData = array_map(function (RewardItemDto $item) {
  134. return [
  135. 'id' => $item->id,
  136. 'reward_type' => $item->rewardType,
  137. 'target_id' => $item->targetId,
  138. 'param1' => $item->param1,
  139. 'param2' => $item->param2,
  140. 'quantity' => $item->quantity,
  141. 'extra_data' => $item->extraData
  142. ];
  143. }, $items);
  144. // 创建日志记录
  145. return GameRewardLog::create([
  146. 'user_id' => $userId,
  147. 'group_id' => $groupId,
  148. 'source_type' => $sourceType,
  149. 'source_id' => $sourceId,
  150. 'reward_items' => $itemsData
  151. ]);
  152. }
  153. /**
  154. * 模拟奖励发放(不实际发放,仅返回奖励结果)
  155. *
  156. * @param int|string $groupIdOrCode 奖励组ID或编码
  157. * @return RewardResultDto 奖励结果
  158. */
  159. public function simulateReward($groupIdOrCode): RewardResultDto
  160. {
  161. try {
  162. // 获取奖励组
  163. $groupDto = $this->getRewardGroup($groupIdOrCode);
  164. if (!$groupDto) {
  165. return RewardResultDto::fail("奖励组不存在: {$groupIdOrCode}");
  166. }
  167. // 确定要发放的奖励项(仅模拟,不实际发放)
  168. $rewardItems = $this->determineRewardItems($groupDto);
  169. if (empty($rewardItems)) {
  170. return RewardResultDto::fail("奖励组中没有可发放的奖励项");
  171. }
  172. // 返回模拟结果(不实际发放奖励)
  173. return RewardResultDto::success(
  174. 0, // 模拟用户ID
  175. $groupDto->id,
  176. $groupDto->code,
  177. $groupDto->name,
  178. 'simulate', // 模拟来源类型
  179. 0, // 模拟来源ID
  180. $rewardItems
  181. );
  182. } catch (\Exception $e) {
  183. return RewardResultDto::fail("模拟奖励失败: " . $e->getMessage());
  184. }
  185. }
  186. /**
  187. * 发放奖励(支持保底机制)
  188. *
  189. * @param int $userId 用户ID
  190. * @param int|string $groupIdOrCode 奖励组ID或编码
  191. * @param string $sourceType 来源类型
  192. * @param int $sourceId 来源ID
  193. * @param bool $enablePity 是否启用保底机制
  194. * @return RewardResultDto 奖励结果
  195. */
  196. public function grantRewardWithPity(int $userId, $groupIdOrCode, string $sourceType, int $sourceId, bool $enablePity = true): RewardResultDto
  197. {
  198. try {
  199. // 获取奖励组
  200. $groupDto = $this->getRewardGroup($groupIdOrCode);
  201. if (!$groupDto) {
  202. return RewardResultDto::fail("奖励组不存在: {$groupIdOrCode}");
  203. }
  204. // 确定要发放的奖励项(支持保底机制)
  205. $rewardItems = $this->determineRewardItemsWithPity($groupDto, $userId, $enablePity);
  206. if (empty($rewardItems)) {
  207. return RewardResultDto::fail("奖励组中没有可发放的奖励项");
  208. }
  209. // 检查事务是否已开启
  210. Helper::check_tr();
  211. // 发放各类奖励
  212. foreach ($rewardItems as $item) {
  213. RewardProcessorDispatcher::process($userId, $item, $sourceType, $sourceId);
  214. }
  215. // 更新保底计数(如果启用保底机制)
  216. if ($enablePity) {
  217. $obtainedItemIds = array_map(function ($item) {
  218. return $item->id;
  219. }, $rewardItems);
  220. PityService::updatePityCounts($userId, $groupDto->id, $obtainedItemIds);
  221. }
  222. // 记录奖励日志
  223. $this->logReward($userId, $groupDto->id, $sourceType, $sourceId, $rewardItems);
  224. // 触发奖励发放事件
  225. event(new RewardGrantedEvent($userId, $groupDto->id, $groupDto->code, $sourceType, $sourceId, $rewardItems));
  226. // 返回成功结果
  227. return RewardResultDto::success(
  228. $userId,
  229. $groupDto->id,
  230. $groupDto->code,
  231. $groupDto->name,
  232. $sourceType,
  233. $sourceId,
  234. $rewardItems
  235. );
  236. } catch (Exception $e) {
  237. Log::error("发放奖励失败(保底机制)", [
  238. 'userId' => $userId,
  239. 'groupIdOrCode' => $groupIdOrCode,
  240. 'sourceType' => $sourceType,
  241. 'sourceId' => $sourceId,
  242. 'enablePity' => $enablePity,
  243. 'error' => $e->getMessage(),
  244. 'trace' => $e->getTraceAsString()
  245. ]);
  246. return RewardResultDto::fail("发放奖励失败: " . $e->getMessage());
  247. }
  248. }
  249. /**
  250. * 确定要发放的奖励项(支持保底机制)
  251. *
  252. * @param RewardGroupDto $groupDto 奖励组DTO
  253. * @param int $userId 用户ID
  254. * @param bool $enablePity 是否启用保底机制
  255. * @return RewardItemDto[] 要发放的奖励项
  256. */
  257. private function determineRewardItemsWithPity(RewardGroupDto $groupDto, int $userId, bool $enablePity): array
  258. {
  259. // 检查奖励模式
  260. $rewardMode = $groupDto->rewardMode ?? REWARD_MODE::WEIGHT_SELECTION->value;
  261. // 如果启用保底机制,传递用户ID;否则传递null
  262. $userIdForPity = $enablePity ? $userId : null;
  263. if ($rewardMode == REWARD_MODE::INDEPENDENT_PROBABILITY->value) {
  264. // 独立概率模式
  265. return IndependentProbabilityReward::process($groupDto, $userIdForPity);
  266. } else {
  267. // 权重选择模式(默认)
  268. return WeightSelectionReward::process($groupDto, $userIdForPity);
  269. }
  270. }
  271. }