MexMatchLogic.php 42 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062
  1. <?php
  2. namespace App\Module\Mex\Logic;
  3. use App\Module\Game\Enums\REWARD_SOURCE_TYPE;
  4. use App\Module\Mex\Models\MexOrder;
  5. use App\Module\Mex\Models\MexWarehouse;
  6. use App\Module\Mex\Models\MexTransaction;
  7. use App\Module\Mex\Models\MexPriceConfig;
  8. use App\Module\Mex\Enums\OrderType;
  9. use App\Module\Mex\Enums\OrderStatus;
  10. use App\Module\Mex\Enums\TransactionType;
  11. use App\Module\Fund\Services\FundService;
  12. use App\Module\Fund\Enums\FUND_TYPE;
  13. use App\Module\Fund\Enums\FUND_CURRENCY_TYPE;
  14. use App\Module\GameItems\Services\ItemService;
  15. use App\Module\Mex\Logic\FundLogic;
  16. use App\Module\Mex\Logic\MexMatchLogLogic;
  17. use App\Module\Mex\Enums\MatchType;
  18. use App\Module\GameItems\Enums\FREEZE_REASON_TYPE;
  19. use Illuminate\Support\Facades\DB;
  20. use Illuminate\Support\Facades\Log;
  21. /**
  22. * 农贸市场撮合逻辑
  23. *
  24. * 处理撮合相关的核心业务逻辑
  25. * 根据文档要求分离用户买入物品和用户卖出物品的撮合逻辑
  26. */
  27. class MexMatchLogic
  28. {
  29. /**
  30. * 仓库账户ID
  31. */
  32. private const WAREHOUSE_USER_ID = 15;
  33. /**
  34. * 调控账户ID
  35. */
  36. private const CONTROL_USER_ID = 16;
  37. /**
  38. * 执行用户买入物品撮合任务
  39. *
  40. * @param int|null $itemId 指定商品ID,null表示处理所有商品
  41. * @param int $batchSize 批处理大小
  42. * @return array 撮合结果
  43. */
  44. public static function executeUserBuyItemMatch(?int $itemId = null, int $batchSize = 100): array
  45. {
  46. $startTime = microtime(true);
  47. $totalMatched = 0;
  48. $totalAmount = '0.00000';
  49. $processedItems = [];
  50. $errors = [];
  51. try {
  52. if ($itemId) {
  53. // 处理指定商品
  54. $result = self::executeUserBuyItemMatchForItem($itemId, $batchSize);
  55. $processedItems[] = $itemId;
  56. $totalMatched += $result['matched_orders'];
  57. $totalAmount = bcadd($totalAmount, $result['total_amount'], 5);
  58. if (!$result['success']) {
  59. $errors[] = "商品 {$itemId}: " . $result['message'];
  60. }
  61. } else {
  62. // 处理所有有待撮合的用户买入物品订单的商品
  63. $itemIds = MexOrder::where('order_type', OrderType::BUY)
  64. ->where('status', OrderStatus::PENDING)
  65. ->distinct()
  66. ->pluck('item_id')
  67. ->toArray();
  68. // 如果没有待撮合的订单,记录一条总体日志表示没有可处理的商品
  69. if (empty($itemIds)) {
  70. $endTime = microtime(true);
  71. $executionTimeMs = round(($endTime - $startTime) * 1000);
  72. // 记录没有待撮合订单的日志(使用商品ID 0 表示全局撮合任务)
  73. MexMatchLogLogic::logMatch(
  74. MatchType::USER_BUY,
  75. 0, // 使用0表示全局撮合任务
  76. $batchSize,
  77. [
  78. 'success' => true,
  79. 'message' => '没有待撮合的用户买入物品订单',
  80. 'matched_orders' => 0,
  81. 'total_amount' => '0.00000',
  82. ],
  83. $executionTimeMs
  84. );
  85. }
  86. foreach ($itemIds as $currentItemId) {
  87. $result = self::executeUserBuyItemMatchForItem($currentItemId, $batchSize);
  88. $processedItems[] = $currentItemId;
  89. $totalMatched += $result['matched_orders'];
  90. $totalAmount = bcadd($totalAmount, $result['total_amount'], 5);
  91. if (!$result['success']) {
  92. $errors[] = "商品 {$currentItemId}: " . $result['message'];
  93. }
  94. }
  95. }
  96. $endTime = microtime(true);
  97. $executionTime = round(($endTime - $startTime) * 1000, 2); // 毫秒
  98. Log::info('Mex用户买入物品撮合任务执行完成', [
  99. 'processed_items' => $processedItems,
  100. 'total_matched' => $totalMatched,
  101. 'total_amount' => $totalAmount,
  102. 'execution_time_ms' => $executionTime,
  103. 'errors' => $errors,
  104. ]);
  105. return [
  106. 'success' => true,
  107. 'message' => '用户买入物品撮合任务执行完成',
  108. 'processed_items' => $processedItems,
  109. 'total_matched' => $totalMatched,
  110. 'total_amount' => $totalAmount,
  111. 'execution_time_ms' => $executionTime,
  112. 'errors' => $errors,
  113. ];
  114. } catch (\Exception $e) {
  115. Log::error('Mex用户买入物品撮合任务执行失败', [
  116. 'error' => $e->getMessage(),
  117. 'trace' => $e->getTraceAsString(),
  118. ]);
  119. return [
  120. 'success' => false,
  121. 'message' => '用户买入物品撮合任务执行失败:' . $e->getMessage(),
  122. 'processed_items' => $processedItems,
  123. 'total_matched' => $totalMatched,
  124. 'total_amount' => $totalAmount,
  125. ];
  126. }
  127. }
  128. /**
  129. * 执行用户卖出物品撮合任务
  130. *
  131. * @param int|null $itemId 指定商品ID,null表示处理所有商品
  132. * @param int $batchSize 批处理大小
  133. * @return array 撮合结果
  134. */
  135. public static function executeUserSellItemMatch(?int $itemId = null, int $batchSize = 100): array
  136. {
  137. $startTime = microtime(true);
  138. $totalMatched = 0;
  139. $totalAmount = '0.00000';
  140. $processedItems = [];
  141. $errors = [];
  142. try {
  143. if ($itemId) {
  144. // 处理指定商品
  145. $result = self::executeUserSellItemMatchForItem($itemId, $batchSize);
  146. $processedItems[] = $itemId;
  147. $totalMatched += $result['matched_orders'];
  148. $totalAmount = bcadd($totalAmount, $result['total_amount'], 5);
  149. if (!$result['success']) {
  150. $errors[] = "商品 {$itemId}: " . $result['message'];
  151. }
  152. } else {
  153. // 处理所有有待撮合的用户卖出物品订单的商品
  154. $itemIds = MexOrder::where('order_type', OrderType::SELL)
  155. ->where('status', OrderStatus::PENDING)
  156. ->distinct()
  157. ->pluck('item_id')
  158. ->toArray();
  159. // 如果没有待撮合的订单,记录一条总体日志表示没有可处理的商品
  160. if (empty($itemIds)) {
  161. $endTime = microtime(true);
  162. $executionTimeMs = round(($endTime - $startTime) * 1000);
  163. // 记录没有待撮合订单的日志(使用商品ID 0 表示全局撮合任务)
  164. MexMatchLogLogic::logMatch(
  165. MatchType::USER_SELL,
  166. 0, // 使用0表示全局撮合任务
  167. $batchSize,
  168. [
  169. 'success' => true,
  170. 'message' => '没有待撮合的用户卖出物品订单',
  171. 'matched_orders' => 0,
  172. 'total_amount' => '0.00000',
  173. ],
  174. $executionTimeMs
  175. );
  176. }
  177. foreach ($itemIds as $currentItemId) {
  178. $result = self::executeUserSellItemMatchForItem($currentItemId, $batchSize);
  179. $processedItems[] = $currentItemId;
  180. $totalMatched += $result['matched_orders'];
  181. $totalAmount = bcadd($totalAmount, $result['total_amount'], 5);
  182. if (!$result['success']) {
  183. $errors[] = "商品 {$currentItemId}: " . $result['message'];
  184. }
  185. }
  186. }
  187. $endTime = microtime(true);
  188. $executionTime = round(($endTime - $startTime) * 1000, 2); // 毫秒
  189. Log::info('Mex用户卖出物品撮合任务执行完成', [
  190. 'processed_items' => $processedItems,
  191. 'total_matched' => $totalMatched,
  192. 'total_amount' => $totalAmount,
  193. 'execution_time_ms' => $executionTime,
  194. 'errors' => $errors,
  195. ]);
  196. return [
  197. 'success' => true,
  198. 'message' => '用户卖出物品撮合任务执行完成',
  199. 'processed_items' => $processedItems,
  200. 'total_matched' => $totalMatched,
  201. 'total_amount' => $totalAmount,
  202. 'execution_time_ms' => $executionTime,
  203. 'errors' => $errors,
  204. ];
  205. } catch (\Exception $e) {
  206. Log::error('Mex用户卖出物品撮合任务执行失败', [
  207. 'error' => $e->getMessage(),
  208. 'trace' => $e->getTraceAsString(),
  209. ]);
  210. return [
  211. 'success' => false,
  212. 'message' => '用户卖出物品撮合任务执行失败:' . $e->getMessage(),
  213. 'processed_items' => $processedItems,
  214. 'total_matched' => $totalMatched,
  215. 'total_amount' => $totalAmount,
  216. ];
  217. }
  218. }
  219. /**
  220. * 执行单个商品的用户买入物品撮合
  221. *
  222. * @param int $itemId 商品ID
  223. * @param int $batchSize 批处理大小
  224. * @return array 撮合结果
  225. */
  226. public static function executeUserBuyItemMatchForItem(int $itemId, int $batchSize = 100): array
  227. {
  228. $startTime = microtime(true);
  229. try {
  230. // 注意:根据文档要求,Logic层不应该开启事务,事务应该在Service层处理
  231. // 检查用户买入物品撮合条件
  232. $conditionCheck = self::checkUserBuyItemMatchConditions($itemId);
  233. if (!$conditionCheck['can_match']) {
  234. $result = [
  235. 'success' => false,
  236. 'message' => $conditionCheck['message'],
  237. 'matched_orders' => 0,
  238. 'total_amount' => '0.00000',
  239. ];
  240. // 记录撮合日志
  241. $endTime = microtime(true);
  242. $executionTimeMs = round(($endTime - $startTime) * 1000);
  243. MexMatchLogLogic::logMatch(MatchType::USER_BUY, $itemId, $batchSize, $result, $executionTimeMs);
  244. return $result;
  245. }
  246. $warehouse = $conditionCheck['warehouse'];
  247. $priceConfig = $conditionCheck['price_config'];
  248. // 获取待撮合的用户买入物品订单(MySQL查询时完成筛选和二级排序)
  249. $buyOrders = MexOrder::where('item_id', $itemId)
  250. ->where('order_type', OrderType::BUY)
  251. ->where('status', OrderStatus::PENDING)
  252. ->where('price', '>=', $priceConfig->max_price) // 价格验证:价格≥最高价
  253. ->where('quantity', '<=', $priceConfig->protection_threshold) // 数量保护:数量≤保护阈值
  254. ->orderBy('price', 'desc') // 价格优先(高价优先)
  255. ->orderBy('created_at', 'asc') // 时间优先(早下单优先)
  256. ->limit($batchSize)
  257. ->get();
  258. // 为价格不符合条件的买入订单记录无法成交原因
  259. MexOrder::where('item_id', $itemId)
  260. ->where('order_type', OrderType::BUY)
  261. ->where('status', OrderStatus::PENDING)
  262. ->where('price', '<', $priceConfig->max_price) // 价格低于最高价
  263. ->update([
  264. 'last_match_failure_reason' => "价格验证失败:买入价格低于最高价格 {$priceConfig->max_price}"
  265. ]);
  266. // 为数量超过保护阈值的买入订单记录无法成交原因
  267. MexOrder::where('item_id', $itemId)
  268. ->where('order_type', OrderType::BUY)
  269. ->where('status', OrderStatus::PENDING)
  270. ->where('price', '>=', $priceConfig->max_price) // 价格符合条件
  271. ->where('quantity', '>', $priceConfig->protection_threshold) // 数量超过保护阈值
  272. ->update([
  273. 'last_match_failure_reason' => "数量保护:订单数量超过保护阈值 {$priceConfig->protection_threshold}"
  274. ]);
  275. if ($buyOrders->isEmpty()) {
  276. $result = [
  277. 'success' => true,
  278. 'message' => '没有符合条件的用户买入物品订单',
  279. 'matched_orders' => 0,
  280. 'total_amount' => '0.00000',
  281. ];
  282. // 记录撮合日志
  283. $endTime = microtime(true);
  284. $executionTimeMs = round(($endTime - $startTime) * 1000);
  285. MexMatchLogLogic::logMatch(MatchType::USER_BUY, $itemId, $batchSize, $result, $executionTimeMs);
  286. return $result;
  287. }
  288. $matchedOrders = 0;
  289. $totalAmount = '0.00000';
  290. $currentStock = $warehouse->quantity;
  291. foreach ($buyOrders as $order) {
  292. // 检查库存是否充足(整单匹配原则)
  293. if ($currentStock < $order->quantity) {
  294. // 记录库存不足的无法成交原因
  295. $order->update([
  296. 'last_match_failure_reason' => "库存不足:当前库存 {$currentStock},需要 {$order->quantity}"
  297. ]);
  298. // 库存不足时结束本次撮合处理,避免无效循环
  299. break;
  300. }
  301. // 执行用户买入物品订单撮合(带事务处理)
  302. $matchResult = \App\Module\Mex\Services\MexMatchService::executeUserBuyItemOrderMatchWithTransaction($order, $warehouse);
  303. if ($matchResult['success']) {
  304. $matchedOrders++;
  305. $totalAmount = bcadd($totalAmount, $matchResult['total_amount'], 5);
  306. $currentStock -= $order->quantity;
  307. // 更新仓库对象的库存(用于后续订单判断)
  308. $warehouse->quantity = $currentStock;
  309. // 清除之前的无法成交原因(如果有的话)
  310. if ($order->last_match_failure_reason) {
  311. $order->update(['last_match_failure_reason' => null]);
  312. }
  313. } else {
  314. // 记录撮合失败的原因
  315. $order->update([
  316. 'last_match_failure_reason' => $matchResult['message']
  317. ]);
  318. }
  319. }
  320. $result = [
  321. 'success' => true,
  322. 'message' => "成功撮合 {$matchedOrders} 个用户买入物品订单",
  323. 'matched_orders' => $matchedOrders,
  324. 'total_amount' => $totalAmount,
  325. ];
  326. // 记录撮合日志
  327. $endTime = microtime(true);
  328. $executionTimeMs = round(($endTime - $startTime) * 1000);
  329. MexMatchLogLogic::logMatch(MatchType::USER_BUY, $itemId, $batchSize, $result, $executionTimeMs);
  330. return $result;
  331. } catch (\Exception $e) {
  332. $result = [
  333. 'success' => false,
  334. 'message' => '用户买入物品撮合执行失败:' . $e->getMessage(),
  335. 'matched_orders' => 0,
  336. 'total_amount' => '0.00000',
  337. ];
  338. // 记录撮合日志(包含错误信息)
  339. $endTime = microtime(true);
  340. $executionTimeMs = round(($endTime - $startTime) * 1000);
  341. MexMatchLogLogic::logMatch(MatchType::USER_BUY, $itemId, $batchSize, $result, $executionTimeMs, $e->getMessage());
  342. return $result;
  343. }
  344. }
  345. /**
  346. * 执行单个商品的用户卖出物品撮合
  347. *
  348. * @param int $itemId 商品ID
  349. * @param int $batchSize 批处理大小
  350. * @return array 撮合结果
  351. */
  352. public static function executeUserSellItemMatchForItem(int $itemId, int $batchSize = 100): array
  353. {
  354. $startTime = microtime(true);
  355. try {
  356. // 注意:根据文档要求,Logic层不应该开启事务,事务应该在Service层处理
  357. // 检查用户卖出物品撮合条件
  358. $conditionCheck = self::checkUserSellItemMatchConditions($itemId);
  359. if (!$conditionCheck['can_match']) {
  360. $result = [
  361. 'success' => false,
  362. 'message' => $conditionCheck['message'],
  363. 'matched_orders' => 0,
  364. 'total_amount' => '0.00000',
  365. ];
  366. // 记录撮合日志
  367. $endTime = microtime(true);
  368. $executionTimeMs = round(($endTime - $startTime) * 1000);
  369. MexMatchLogLogic::logMatch(MatchType::USER_SELL, $itemId, $batchSize, $result, $executionTimeMs);
  370. return $result;
  371. }
  372. $priceConfig = $conditionCheck['price_config'];
  373. // 获取待撮合的用户卖出物品订单
  374. $sellOrders = MexOrder::where('item_id', $itemId)
  375. ->where('order_type', OrderType::SELL)
  376. ->where('status', OrderStatus::PENDING)
  377. ->where('price', '<=',$priceConfig->min_price) // 价格 <= 最低价格
  378. ->where('quantity', '<=', $priceConfig->protection_threshold) // 数量保护:数量≤保护阈值
  379. ->orderBy('price', 'asc')
  380. ->orderBy('id', 'asc')
  381. ->limit($batchSize)
  382. ->get();
  383. if ($sellOrders->isEmpty()) {
  384. $result = [
  385. 'success' => true,
  386. 'message' => '没有待撮合的用户卖出物品订单',
  387. 'matched_orders' => 0,
  388. 'total_amount' => '0.00000',
  389. ];
  390. // 记录撮合日志
  391. $endTime = microtime(true);
  392. $executionTimeMs = round(($endTime - $startTime) * 1000);
  393. MexMatchLogLogic::logMatch(MatchType::USER_SELL, $itemId, $batchSize, $result, $executionTimeMs);
  394. return $result;
  395. }
  396. $matchedOrders = 0;
  397. $totalAmount = '0.00000';
  398. foreach ($sellOrders as $order) {
  399. // 价格验证:用户卖出物品价格≤最低价
  400. if (bccomp($order->price, $priceConfig->min_price, 5) > 0) {
  401. Log::info('卖出订单价格验证失败', [
  402. 'order_id' => $order->id,
  403. 'order_price' => $order->price,
  404. 'min_price' => $priceConfig->min_price,
  405. 'price_compare' => bccomp($order->price, $priceConfig->min_price, 5)
  406. ]);
  407. // 记录价格验证失败的无法成交原因
  408. $order->update([
  409. 'last_match_failure_reason' => "价格验证失败:卖出价格 {$order->price} 高于最低价格 {$priceConfig->min_price}"
  410. ]);
  411. continue; // 价格不符合条件,跳过此订单
  412. }
  413. // 执行用户卖出物品订单撮合(带事务处理)
  414. $matchResult = \App\Module\Mex\Services\MexMatchService::executeUserSellItemOrderMatchWithTransaction($order);
  415. if ($matchResult['success']) {
  416. $matchedOrders++;
  417. $totalAmount = bcadd($totalAmount, $matchResult['total_amount'], 5);
  418. Log::info('卖出订单撮合成功', [
  419. 'order_id' => $order->id,
  420. 'total_amount' => $matchResult['total_amount']
  421. ]);
  422. // 清除之前的无法成交原因(如果有的话)
  423. if ($order->last_match_failure_reason) {
  424. $order->update(['last_match_failure_reason' => null]);
  425. }
  426. } else {
  427. Log::error('卖出订单撮合失败', [
  428. 'order_id' => $order->id,
  429. 'error_message' => $matchResult['message']
  430. ]);
  431. // 记录撮合失败的原因
  432. $order->update([
  433. 'last_match_failure_reason' => $matchResult['message']
  434. ]);
  435. }
  436. }
  437. $result = [
  438. 'success' => true,
  439. 'message' => "成功撮合 {$matchedOrders} 个用户卖出物品订单",
  440. 'matched_orders' => $matchedOrders,
  441. 'total_amount' => $totalAmount,
  442. ];
  443. // 记录撮合日志
  444. $endTime = microtime(true);
  445. $executionTimeMs = round(($endTime - $startTime) * 1000);
  446. MexMatchLogLogic::logMatch(MatchType::USER_SELL, $itemId, $batchSize, $result, $executionTimeMs);
  447. return $result;
  448. } catch (\Exception $e) {
  449. $result = [
  450. 'success' => false,
  451. 'message' => '用户卖出物品撮合执行失败:' . $e->getMessage(),
  452. 'matched_orders' => 0,
  453. 'total_amount' => '0.00000',
  454. ];
  455. // 记录撮合日志(包含错误信息)
  456. $endTime = microtime(true);
  457. $executionTimeMs = round(($endTime - $startTime) * 1000);
  458. MexMatchLogLogic::logMatch(MatchType::USER_SELL, $itemId, $batchSize, $result, $executionTimeMs, $e->getMessage());
  459. return $result;
  460. }
  461. }
  462. /**
  463. * 执行单个用户买入物品订单的撮合
  464. *
  465. * @param MexOrder $order 用户买入物品订单
  466. * @param MexWarehouse $warehouse 仓库信息
  467. * @return array 撮合结果
  468. */
  469. public static function executeUserBuyItemOrderMatch(MexOrder $order, MexWarehouse $warehouse): array
  470. {
  471. try {
  472. // 计算成交金额
  473. $totalAmount = bcmul($order->price, $order->quantity, 9);
  474. // 先执行账户流转逻辑,确保资金和物品流转成功后再更新订单状态和创建成交记录
  475. // 1. 用户冻结资金转入仓库账户
  476. $fundResult = self::transferFrozenFundsToWarehouse($order->user_id, $totalAmount, $order->id, $order->currency_type);
  477. if (!$fundResult['success']) {
  478. throw new \Exception('资金流转失败:' . $fundResult['message']);
  479. }
  480. // 2. 仓库账户物品转出到用户账户
  481. $itemResult = self::transferItemsFromWarehouseToUser($order->user_id, $order->item_id, $order->quantity, $order->id);
  482. if (!$itemResult['success']) {
  483. throw new \Exception('物品流转失败:' . $itemResult['message']);
  484. }
  485. // 资金和物品流转成功后,更新订单状态
  486. $order->update([
  487. 'status' => OrderStatus::COMPLETED,
  488. 'completed_quantity' => $order->quantity,
  489. 'completed_amount' => $totalAmount,
  490. 'completed_at' => now(),
  491. ]);
  492. // 更新仓库库存
  493. $warehouse->quantity -= $order->quantity;
  494. $warehouse->total_sell_quantity += $order->quantity;
  495. $warehouse->total_sell_amount = bcadd($warehouse->total_sell_amount, $totalAmount, 5);
  496. $warehouse->last_transaction_at = now();
  497. $warehouse->save();
  498. // 创建成交记录
  499. $transaction = MexTransaction::create([
  500. 'buy_order_id' => $order->id,
  501. 'sell_order_id' => null,
  502. 'buyer_id' => $order->user_id,
  503. 'seller_id' => self::WAREHOUSE_USER_ID, // 仓库账户作为卖方
  504. 'item_id' => $order->item_id,
  505. 'currency_type' => $order->currency_type->value,
  506. 'quantity' => $order->quantity,
  507. 'price' => $order->price,
  508. 'total_amount' => $totalAmount,
  509. 'transaction_type' => TransactionType::USER_BUY,
  510. 'is_admin_operation' => false,
  511. ]);
  512. // 验证成交记录是否创建成功
  513. if (!$transaction || !$transaction->id) {
  514. throw new \Exception('成交记录创建失败');
  515. }
  516. return [
  517. 'success' => true,
  518. 'message' => '订单撮合成功',
  519. 'order_id' => $order->id,
  520. 'transaction_id' => $transaction->id,
  521. 'total_amount' => $totalAmount,
  522. ];
  523. } catch (\Exception $e) {
  524. return [
  525. 'success' => false,
  526. 'message' => '订单撮合失败:' . $e->getMessage(),
  527. 'order_id' => $order->id,
  528. 'total_amount' => '0.00000',
  529. ];
  530. }
  531. }
  532. /**
  533. * 执行单个用户卖出物品订单的撮合
  534. *
  535. * @param MexOrder $order 用户卖出物品订单
  536. * @return array 撮合结果
  537. */
  538. public static function executeUserSellItemOrderMatch(MexOrder $order): array
  539. {
  540. try {
  541. // 计算成交金额
  542. $totalAmount = bcmul($order->price, $order->quantity, 5);
  543. // 先执行账户流转逻辑,确保资金和物品流转成功后再更新订单状态和创建成交记录
  544. // 1. 用户冻结物品转入仓库账户
  545. $itemResult = self::transferFrozenItemsToWarehouse($order->user_id, $order->item_id, $order->quantity, $order->id);
  546. if (!$itemResult['success']) {
  547. throw new \Exception('物品流转失败:' . $itemResult['message']);
  548. }
  549. // 2. 仓库账户资金转出到用户账户
  550. $fundResult = self::transferFundsFromWarehouseToUser($order->user_id, $totalAmount, $order->id, $order->currency_type);
  551. if (!$fundResult['success']) {
  552. throw new \Exception('资金流转失败:' . $fundResult['message']);
  553. }
  554. // 资金和物品流转成功后,更新订单状态
  555. $order->update([
  556. 'status' => OrderStatus::COMPLETED,
  557. 'completed_quantity' => $order->quantity,
  558. 'completed_amount' => $totalAmount,
  559. 'completed_at' => now(),
  560. ]);
  561. // 更新仓库库存
  562. $warehouse = MexWarehouse::where('item_id', $order->item_id)->first();
  563. if (!$warehouse) {
  564. // 如果仓库记录不存在,创建新记录
  565. $warehouse = MexWarehouse::create([
  566. 'item_id' => $order->item_id,
  567. 'quantity' => $order->quantity,
  568. 'total_buy_amount' => $totalAmount,
  569. 'total_buy_quantity' => $order->quantity,
  570. 'last_transaction_at' => now(),
  571. ]);
  572. } else {
  573. // 更新现有仓库记录
  574. $warehouse->quantity += $order->quantity;
  575. $warehouse->total_buy_quantity += $order->quantity;
  576. $warehouse->total_buy_amount = bcadd($warehouse->total_buy_amount, $totalAmount, 5);
  577. $warehouse->last_transaction_at = now();
  578. $warehouse->save();
  579. }
  580. // 创建成交记录
  581. $transaction = MexTransaction::create([
  582. 'buy_order_id' => null,
  583. 'sell_order_id' => $order->id,
  584. 'buyer_id' => self::WAREHOUSE_USER_ID, // 仓库账户作为买方
  585. 'seller_id' => $order->user_id,
  586. 'item_id' => $order->item_id,
  587. 'currency_type' => $order->currency_type->value,
  588. 'quantity' => $order->quantity,
  589. 'price' => $order->price,
  590. 'total_amount' => $totalAmount,
  591. 'transaction_type' => TransactionType::USER_SELL,
  592. 'is_admin_operation' => false,
  593. ]);
  594. // 验证成交记录是否创建成功
  595. if (!$transaction || !$transaction->id) {
  596. throw new \Exception('成交记录创建失败');
  597. }
  598. return [
  599. 'success' => true,
  600. 'message' => '用户卖出物品订单撮合成功',
  601. 'order_id' => $order->id,
  602. 'transaction_id' => $transaction->id,
  603. 'total_amount' => $totalAmount,
  604. ];
  605. } catch (\Exception $e) {
  606. return [
  607. 'success' => false,
  608. 'message' => '用户卖出物品订单撮合失败:' . $e->getMessage(),
  609. 'order_id' => $order->id,
  610. 'total_amount' => '0.00000',
  611. ];
  612. }
  613. }
  614. /**
  615. * 检查用户买入物品撮合条件
  616. *
  617. * @param int $itemId 商品ID
  618. * @return array 检查结果
  619. */
  620. public static function checkUserBuyItemMatchConditions(int $itemId): array
  621. {
  622. // 检查价格配置
  623. $priceConfig = MexPriceConfig::where('item_id', $itemId)->where('is_enabled', true)->first();
  624. if (!$priceConfig) {
  625. return [
  626. 'can_match' => false,
  627. 'message' => '商品未配置价格信息或已禁用',
  628. ];
  629. }
  630. // 检查仓库库存
  631. $warehouse = MexWarehouse::where('item_id', $itemId)->first();
  632. if (!$warehouse || $warehouse->quantity <= 0) {
  633. return [
  634. 'can_match' => false,
  635. 'message' => '仓库库存不足',
  636. ];
  637. }
  638. // 检查是否有符合条件的待撮合用户买入物品订单
  639. $pendingBuyOrders = MexOrder::where('item_id', $itemId)
  640. ->where('order_type', OrderType::BUY)
  641. ->where('status', OrderStatus::PENDING)
  642. ->where('price', '>=', $priceConfig->max_price) // 价格≥最高价
  643. ->where('quantity', '<=', $priceConfig->protection_threshold) // 数量≤保护阈值
  644. ->count();
  645. if ($pendingBuyOrders === 0) {
  646. return [
  647. 'can_match' => false,
  648. 'message' => '没有符合条件的待撮合用户买入物品订单',
  649. ];
  650. }
  651. return [
  652. 'can_match' => true,
  653. 'message' => '用户买入物品撮合条件满足',
  654. 'warehouse' => $warehouse,
  655. 'price_config' => $priceConfig,
  656. 'pending_orders' => $pendingBuyOrders,
  657. ];
  658. }
  659. /**
  660. * 检查用户卖出物品撮合条件
  661. *
  662. * @param int $itemId 商品ID
  663. * @return array 检查结果
  664. */
  665. public static function checkUserSellItemMatchConditions(int $itemId): array
  666. {
  667. // 检查价格配置
  668. $priceConfig = MexPriceConfig::where('item_id', $itemId)->where('is_enabled', true)->first();
  669. if (!$priceConfig) {
  670. return [
  671. 'can_match' => false,
  672. 'message' => '商品未配置价格信息或已禁用',
  673. ];
  674. }
  675. // 检查是否有待撮合的用户卖出物品订单
  676. $pendingSellOrders = MexOrder::where('item_id', $itemId)
  677. ->where('order_type', OrderType::SELL)
  678. ->where('status', OrderStatus::PENDING)
  679. ->count();
  680. if ($pendingSellOrders === 0) {
  681. return [
  682. 'can_match' => false,
  683. 'message' => '没有待撮合的用户卖出物品订单',
  684. ];
  685. }
  686. return [
  687. 'can_match' => true,
  688. 'message' => '用户卖出物品撮合条件满足',
  689. 'price_config' => $priceConfig,
  690. 'pending_orders' => $pendingSellOrders,
  691. ];
  692. }
  693. /**
  694. * 获取用户买入物品撮合统计信息
  695. *
  696. * @return array 统计信息
  697. */
  698. public static function getUserBuyItemMatchStats(): array
  699. {
  700. // 获取待撮合用户买入物品订单统计
  701. $pendingStats = MexOrder::where('order_type', OrderType::BUY)
  702. ->where('status', OrderStatus::PENDING)
  703. ->selectRaw('
  704. COUNT(*) as total_pending,
  705. COUNT(DISTINCT item_id) as pending_items,
  706. SUM(quantity) as total_quantity,
  707. SUM(total_amount) as total_amount
  708. ')
  709. ->first();
  710. // 获取今日用户买入物品撮合统计
  711. $todayStats = MexTransaction::where('transaction_type', TransactionType::USER_BUY)
  712. ->whereDate('created_at', today())
  713. ->selectRaw('
  714. COUNT(*) as today_matched,
  715. SUM(quantity) as today_quantity,
  716. SUM(total_amount) as today_amount
  717. ')
  718. ->first();
  719. // 获取有库存的商品数量
  720. $availableItems = MexWarehouse::where('quantity', '>', 0)->count();
  721. return [
  722. 'pending_orders' => $pendingStats->total_pending ?? 0,
  723. 'pending_items' => $pendingStats->pending_items ?? 0,
  724. 'pending_quantity' => $pendingStats->total_quantity ?? 0,
  725. 'pending_amount' => $pendingStats->total_amount ?? '0.00000',
  726. 'today_matched' => $todayStats->today_matched ?? 0,
  727. 'today_quantity' => $todayStats->today_quantity ?? 0,
  728. 'today_amount' => $todayStats->today_amount ?? '0.00000',
  729. 'available_items' => $availableItems,
  730. 'stats_time' => now(),
  731. ];
  732. }
  733. /**
  734. * 获取用户卖出物品撮合统计信息
  735. *
  736. * @return array 统计信息
  737. */
  738. public static function getUserSellItemMatchStats(): array
  739. {
  740. // 获取待撮合用户卖出物品订单统计
  741. $pendingStats = MexOrder::where('order_type', OrderType::SELL)
  742. ->where('status', OrderStatus::PENDING)
  743. ->selectRaw('
  744. COUNT(*) as total_pending,
  745. COUNT(DISTINCT item_id) as pending_items,
  746. SUM(quantity) as total_quantity,
  747. SUM(total_amount) as total_amount
  748. ')
  749. ->first();
  750. // 获取今日用户卖出物品撮合统计
  751. $todayStats = MexTransaction::where('transaction_type', TransactionType::USER_SELL)
  752. ->whereDate('created_at', today())
  753. ->selectRaw('
  754. COUNT(*) as today_matched,
  755. SUM(quantity) as today_quantity,
  756. SUM(total_amount) as today_amount
  757. ')
  758. ->first();
  759. return [
  760. 'pending_orders' => $pendingStats->total_pending ?? 0,
  761. 'pending_items' => $pendingStats->pending_items ?? 0,
  762. 'pending_quantity' => $pendingStats->total_quantity ?? 0,
  763. 'pending_amount' => $pendingStats->total_amount ?? '0.00000',
  764. 'today_matched' => $todayStats->today_matched ?? 0,
  765. 'today_quantity' => $todayStats->today_quantity ?? 0,
  766. 'today_amount' => $todayStats->today_amount ?? '0.00000',
  767. 'stats_time' => now(),
  768. ];
  769. }
  770. /**
  771. * 将用户冻结资金转入仓库账户
  772. *
  773. * @param int $userId 用户ID
  774. * @param string $amount 金额
  775. * @param int $orderId 订单ID
  776. * @param FUND_CURRENCY_TYPE|null $currencyType 币种类型,默认使用钻石
  777. * @return array 转移结果
  778. */
  779. private static function transferFrozenFundsToWarehouse(int $userId, string $amount, int $orderId, ?FUND_CURRENCY_TYPE $currencyType = null): array
  780. {
  781. try {
  782. // 获取币种类型,默认使用钻石
  783. $currencyType = $currencyType ?? FundLogic::getDefaultCurrency();
  784. // 获取对应的冻结账户类型
  785. $frozenAccountType = FundLogic::getFrozenAccountType($currencyType);
  786. if (!$frozenAccountType) {
  787. return [
  788. 'success' => false,
  789. 'message' => '不支持的币种类型',
  790. ];
  791. }
  792. // 从用户冻结账户转移到仓库账户
  793. $fundService = new FundService($userId, $frozenAccountType->value);
  794. // 多少钱,就是多少钱,资金模块能够正确处理,不需要外部处理
  795. $result = $fundService->trade(
  796. self::WAREHOUSE_USER_ID,
  797. $amount,
  798. 'MEX_ORDER',
  799. $orderId,
  800. '用户买入物品撮合-资金转移'
  801. );
  802. if (is_string($result)) {
  803. return [
  804. 'success' => false,
  805. 'message' => $result,
  806. ];
  807. }
  808. return [
  809. 'success' => true,
  810. 'message' => '资金转移成功',
  811. 'data' => $result,
  812. ];
  813. } catch (\Exception $e) {
  814. return [
  815. 'success' => false,
  816. 'message' => '资金转移异常:' . $e->getMessage(),
  817. ];
  818. }
  819. }
  820. /**
  821. * 将仓库账户物品转出到用户账户
  822. *
  823. * @param int $userId 用户ID
  824. * @param int $itemId 物品ID
  825. * @param int $quantity 数量
  826. * @param int $orderId 订单ID
  827. * @return array 转移结果
  828. */
  829. private static function transferItemsFromWarehouseToUser(int $userId, int $itemId, int $quantity, int $orderId): array
  830. {
  831. try {
  832. // 添加物品到用户账户
  833. $result = ItemService::addItem($userId, $itemId, $quantity, [
  834. 'source' => 'mex_order',
  835. 'source_type' => REWARD_SOURCE_TYPE::MEX_BUY,
  836. 'source_id' => $orderId,
  837. 'remark' => '用户买入物品撮合-物品转移',
  838. ]);
  839. if (!$result || !isset($result['success']) || !$result['success']) {
  840. return [
  841. 'success' => false,
  842. 'message' => '物品添加失败:' . ($result['message'] ?? '未知错误'),
  843. ];
  844. }
  845. return [
  846. 'success' => true,
  847. 'message' => '物品转移成功',
  848. 'data' => $result,
  849. ];
  850. } catch (\Exception $e) {
  851. return [
  852. 'success' => false,
  853. 'message' => '物品转移异常:' . $e->getMessage(),
  854. ];
  855. }
  856. }
  857. /**
  858. * 将用户冻结物品转入仓库账户
  859. *
  860. * @param int $userId 用户ID
  861. * @param int $itemId 物品ID
  862. * @param int $quantity 数量
  863. * @param int $orderId 订单ID
  864. * @return array 转移结果
  865. */
  866. private static function transferFrozenItemsToWarehouse(int $userId, int $itemId, int $quantity, int $orderId): array
  867. {
  868. try {
  869. // 消耗用户物品(包括冻结的物品)
  870. $result = ItemService::consumeItem($userId, $itemId, null, $quantity, [
  871. 'source' => 'mex_order',
  872. 'source_id' => $orderId,
  873. 'remark' => '用户卖出物品撮合-物品转移',
  874. 'include_frozen' => true, // 包括冻结的物品
  875. ]);
  876. if (!$result || !isset($result['success']) || !$result['success']) {
  877. return [
  878. 'success' => false,
  879. 'message' => '物品消耗失败:' . ($result['message'] ?? '未知错误'),
  880. ];
  881. }
  882. return [
  883. 'success' => true,
  884. 'message' => '物品转移成功',
  885. 'data' => $result,
  886. ];
  887. } catch (\Exception $e) {
  888. return [
  889. 'success' => false,
  890. 'message' => '物品转移异常:' . $e->getMessage(),
  891. ];
  892. }
  893. }
  894. /**
  895. * 将仓库账户资金转出到用户账户
  896. *
  897. * @param int $userId 用户ID
  898. * @param string $amount 金额
  899. * @param int $orderId 订单ID
  900. * @param FUND_CURRENCY_TYPE|null $currencyType 币种类型,默认使用钻石
  901. * @return array 转移结果
  902. */
  903. private static function transferFundsFromWarehouseToUser(int $userId, string $amount, int $orderId, ?FUND_CURRENCY_TYPE $currencyType = null): array
  904. {
  905. try {
  906. // 获取币种类型,默认使用钻石
  907. $currencyType = $currencyType ?? FundLogic::getDefaultCurrency();
  908. // 获取对应的可用账户类型
  909. $availableAccountType = FundLogic::getAvailableAccountType($currencyType);
  910. if (!$availableAccountType) {
  911. return [
  912. 'success' => false,
  913. 'message' => '不支持的币种类型',
  914. ];
  915. }
  916. // 从仓库账户转移到用户账户
  917. $fundService = new FundService(self::WAREHOUSE_USER_ID, $availableAccountType->value);
  918. // 资金系统,能够正确处理金额,不需要外部处理
  919. $result = $fundService->trade(
  920. $userId,
  921. $amount,
  922. 'MEX_ORDER',
  923. $orderId,
  924. '用户卖出物品撮合-资金转移'
  925. );
  926. if (is_string($result)) {
  927. return [
  928. 'success' => false,
  929. 'message' => $result,
  930. ];
  931. }
  932. return [
  933. 'success' => true,
  934. 'message' => '资金转移成功',
  935. 'data' => $result,
  936. ];
  937. } catch (\Exception $e) {
  938. return [
  939. 'success' => false,
  940. 'message' => '资金转移异常:' . $e->getMessage(),
  941. ];
  942. }
  943. }
  944. }