MexMatchLogic.php 42 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060
  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. ->limit($batchSize)
  378. ->get();
  379. if ($sellOrders->isEmpty()) {
  380. $result = [
  381. 'success' => true,
  382. 'message' => '没有待撮合的用户卖出物品订单',
  383. 'matched_orders' => 0,
  384. 'total_amount' => '0.00000',
  385. ];
  386. // 记录撮合日志
  387. $endTime = microtime(true);
  388. $executionTimeMs = round(($endTime - $startTime) * 1000);
  389. MexMatchLogLogic::logMatch(MatchType::USER_SELL, $itemId, $batchSize, $result, $executionTimeMs);
  390. return $result;
  391. }
  392. $matchedOrders = 0;
  393. $totalAmount = '0.00000';
  394. foreach ($sellOrders as $order) {
  395. // 价格验证:用户卖出物品价格≤最低价
  396. if (bccomp($order->price, $priceConfig->min_price, 5) > 0) {
  397. Log::info('卖出订单价格验证失败', [
  398. 'order_id' => $order->id,
  399. 'order_price' => $order->price,
  400. 'min_price' => $priceConfig->min_price,
  401. 'price_compare' => bccomp($order->price, $priceConfig->min_price, 5)
  402. ]);
  403. // 记录价格验证失败的无法成交原因
  404. $order->update([
  405. 'last_match_failure_reason' => "价格验证失败:卖出价格 {$order->price} 高于最低价格 {$priceConfig->min_price}"
  406. ]);
  407. continue; // 价格不符合条件,跳过此订单
  408. }
  409. // 执行用户卖出物品订单撮合(带事务处理)
  410. $matchResult = \App\Module\Mex\Services\MexMatchService::executeUserSellItemOrderMatchWithTransaction($order);
  411. if ($matchResult['success']) {
  412. $matchedOrders++;
  413. $totalAmount = bcadd($totalAmount, $matchResult['total_amount'], 5);
  414. Log::info('卖出订单撮合成功', [
  415. 'order_id' => $order->id,
  416. 'total_amount' => $matchResult['total_amount']
  417. ]);
  418. // 清除之前的无法成交原因(如果有的话)
  419. if ($order->last_match_failure_reason) {
  420. $order->update(['last_match_failure_reason' => null]);
  421. }
  422. } else {
  423. Log::error('卖出订单撮合失败', [
  424. 'order_id' => $order->id,
  425. 'error_message' => $matchResult['message']
  426. ]);
  427. // 记录撮合失败的原因
  428. $order->update([
  429. 'last_match_failure_reason' => $matchResult['message']
  430. ]);
  431. }
  432. }
  433. $result = [
  434. 'success' => true,
  435. 'message' => "成功撮合 {$matchedOrders} 个用户卖出物品订单",
  436. 'matched_orders' => $matchedOrders,
  437. 'total_amount' => $totalAmount,
  438. ];
  439. // 记录撮合日志
  440. $endTime = microtime(true);
  441. $executionTimeMs = round(($endTime - $startTime) * 1000);
  442. MexMatchLogLogic::logMatch(MatchType::USER_SELL, $itemId, $batchSize, $result, $executionTimeMs);
  443. return $result;
  444. } catch (\Exception $e) {
  445. $result = [
  446. 'success' => false,
  447. 'message' => '用户卖出物品撮合执行失败:' . $e->getMessage(),
  448. 'matched_orders' => 0,
  449. 'total_amount' => '0.00000',
  450. ];
  451. // 记录撮合日志(包含错误信息)
  452. $endTime = microtime(true);
  453. $executionTimeMs = round(($endTime - $startTime) * 1000);
  454. MexMatchLogLogic::logMatch(MatchType::USER_SELL, $itemId, $batchSize, $result, $executionTimeMs, $e->getMessage());
  455. return $result;
  456. }
  457. }
  458. /**
  459. * 执行单个用户买入物品订单的撮合
  460. *
  461. * @param MexOrder $order 用户买入物品订单
  462. * @param MexWarehouse $warehouse 仓库信息
  463. * @return array 撮合结果
  464. */
  465. public static function executeUserBuyItemOrderMatch(MexOrder $order, MexWarehouse $warehouse): array
  466. {
  467. try {
  468. // 计算成交金额
  469. $totalAmount = bcmul($order->price, $order->quantity, 5);
  470. // 先执行账户流转逻辑,确保资金和物品流转成功后再更新订单状态和创建成交记录
  471. // 1. 用户冻结资金转入仓库账户
  472. $fundResult = self::transferFrozenFundsToWarehouse($order->user_id, $totalAmount, $order->id, $order->currency_type);
  473. if (!$fundResult['success']) {
  474. throw new \Exception('资金流转失败:' . $fundResult['message']);
  475. }
  476. // 2. 仓库账户物品转出到用户账户
  477. $itemResult = self::transferItemsFromWarehouseToUser($order->user_id, $order->item_id, $order->quantity, $order->id);
  478. if (!$itemResult['success']) {
  479. throw new \Exception('物品流转失败:' . $itemResult['message']);
  480. }
  481. // 资金和物品流转成功后,更新订单状态
  482. $order->update([
  483. 'status' => OrderStatus::COMPLETED,
  484. 'completed_quantity' => $order->quantity,
  485. 'completed_amount' => $totalAmount,
  486. 'completed_at' => now(),
  487. ]);
  488. // 更新仓库库存
  489. $warehouse->quantity -= $order->quantity;
  490. $warehouse->total_sell_quantity += $order->quantity;
  491. $warehouse->total_sell_amount = bcadd($warehouse->total_sell_amount, $totalAmount, 5);
  492. $warehouse->last_transaction_at = now();
  493. $warehouse->save();
  494. // 创建成交记录
  495. $transaction = MexTransaction::create([
  496. 'buy_order_id' => $order->id,
  497. 'sell_order_id' => null,
  498. 'buyer_id' => $order->user_id,
  499. 'seller_id' => self::WAREHOUSE_USER_ID, // 仓库账户作为卖方
  500. 'item_id' => $order->item_id,
  501. 'currency_type' => $order->currency_type->value,
  502. 'quantity' => $order->quantity,
  503. 'price' => $order->price,
  504. 'total_amount' => $totalAmount,
  505. 'transaction_type' => TransactionType::USER_BUY,
  506. 'is_admin_operation' => false,
  507. ]);
  508. // 验证成交记录是否创建成功
  509. if (!$transaction || !$transaction->id) {
  510. throw new \Exception('成交记录创建失败');
  511. }
  512. return [
  513. 'success' => true,
  514. 'message' => '订单撮合成功',
  515. 'order_id' => $order->id,
  516. 'transaction_id' => $transaction->id,
  517. 'total_amount' => $totalAmount,
  518. ];
  519. } catch (\Exception $e) {
  520. return [
  521. 'success' => false,
  522. 'message' => '订单撮合失败:' . $e->getMessage(),
  523. 'order_id' => $order->id,
  524. 'total_amount' => '0.00000',
  525. ];
  526. }
  527. }
  528. /**
  529. * 执行单个用户卖出物品订单的撮合
  530. *
  531. * @param MexOrder $order 用户卖出物品订单
  532. * @return array 撮合结果
  533. */
  534. public static function executeUserSellItemOrderMatch(MexOrder $order): array
  535. {
  536. try {
  537. // 计算成交金额
  538. $totalAmount = bcmul($order->price, $order->quantity, 5);
  539. // 先执行账户流转逻辑,确保资金和物品流转成功后再更新订单状态和创建成交记录
  540. // 1. 用户冻结物品转入仓库账户
  541. $itemResult = self::transferFrozenItemsToWarehouse($order->user_id, $order->item_id, $order->quantity, $order->id);
  542. if (!$itemResult['success']) {
  543. throw new \Exception('物品流转失败:' . $itemResult['message']);
  544. }
  545. // 2. 仓库账户资金转出到用户账户
  546. $fundResult = self::transferFundsFromWarehouseToUser($order->user_id, $totalAmount, $order->id, $order->currency_type);
  547. if (!$fundResult['success']) {
  548. throw new \Exception('资金流转失败:' . $fundResult['message']);
  549. }
  550. // 资金和物品流转成功后,更新订单状态
  551. $order->update([
  552. 'status' => OrderStatus::COMPLETED,
  553. 'completed_quantity' => $order->quantity,
  554. 'completed_amount' => $totalAmount,
  555. 'completed_at' => now(),
  556. ]);
  557. // 更新仓库库存
  558. $warehouse = MexWarehouse::where('item_id', $order->item_id)->first();
  559. if (!$warehouse) {
  560. // 如果仓库记录不存在,创建新记录
  561. $warehouse = MexWarehouse::create([
  562. 'item_id' => $order->item_id,
  563. 'quantity' => $order->quantity,
  564. 'total_buy_amount' => $totalAmount,
  565. 'total_buy_quantity' => $order->quantity,
  566. 'last_transaction_at' => now(),
  567. ]);
  568. } else {
  569. // 更新现有仓库记录
  570. $warehouse->quantity += $order->quantity;
  571. $warehouse->total_buy_quantity += $order->quantity;
  572. $warehouse->total_buy_amount = bcadd($warehouse->total_buy_amount, $totalAmount, 5);
  573. $warehouse->last_transaction_at = now();
  574. $warehouse->save();
  575. }
  576. // 创建成交记录
  577. $transaction = MexTransaction::create([
  578. 'buy_order_id' => null,
  579. 'sell_order_id' => $order->id,
  580. 'buyer_id' => self::WAREHOUSE_USER_ID, // 仓库账户作为买方
  581. 'seller_id' => $order->user_id,
  582. 'item_id' => $order->item_id,
  583. 'currency_type' => $order->currency_type->value,
  584. 'quantity' => $order->quantity,
  585. 'price' => $order->price,
  586. 'total_amount' => $totalAmount,
  587. 'transaction_type' => TransactionType::USER_SELL,
  588. 'is_admin_operation' => false,
  589. ]);
  590. // 验证成交记录是否创建成功
  591. if (!$transaction || !$transaction->id) {
  592. throw new \Exception('成交记录创建失败');
  593. }
  594. return [
  595. 'success' => true,
  596. 'message' => '用户卖出物品订单撮合成功',
  597. 'order_id' => $order->id,
  598. 'transaction_id' => $transaction->id,
  599. 'total_amount' => $totalAmount,
  600. ];
  601. } catch (\Exception $e) {
  602. return [
  603. 'success' => false,
  604. 'message' => '用户卖出物品订单撮合失败:' . $e->getMessage(),
  605. 'order_id' => $order->id,
  606. 'total_amount' => '0.00000',
  607. ];
  608. }
  609. }
  610. /**
  611. * 检查用户买入物品撮合条件
  612. *
  613. * @param int $itemId 商品ID
  614. * @return array 检查结果
  615. */
  616. public static function checkUserBuyItemMatchConditions(int $itemId): array
  617. {
  618. // 检查价格配置
  619. $priceConfig = MexPriceConfig::where('item_id', $itemId)->where('is_enabled', true)->first();
  620. if (!$priceConfig) {
  621. return [
  622. 'can_match' => false,
  623. 'message' => '商品未配置价格信息或已禁用',
  624. ];
  625. }
  626. // 检查仓库库存
  627. $warehouse = MexWarehouse::where('item_id', $itemId)->first();
  628. if (!$warehouse || $warehouse->quantity <= 0) {
  629. return [
  630. 'can_match' => false,
  631. 'message' => '仓库库存不足',
  632. ];
  633. }
  634. // 检查是否有符合条件的待撮合用户买入物品订单
  635. $pendingBuyOrders = MexOrder::where('item_id', $itemId)
  636. ->where('order_type', OrderType::BUY)
  637. ->where('status', OrderStatus::PENDING)
  638. ->where('price', '>=', $priceConfig->max_price) // 价格≥最高价
  639. ->where('quantity', '<=', $priceConfig->protection_threshold) // 数量≤保护阈值
  640. ->count();
  641. if ($pendingBuyOrders === 0) {
  642. return [
  643. 'can_match' => false,
  644. 'message' => '没有符合条件的待撮合用户买入物品订单',
  645. ];
  646. }
  647. return [
  648. 'can_match' => true,
  649. 'message' => '用户买入物品撮合条件满足',
  650. 'warehouse' => $warehouse,
  651. 'price_config' => $priceConfig,
  652. 'pending_orders' => $pendingBuyOrders,
  653. ];
  654. }
  655. /**
  656. * 检查用户卖出物品撮合条件
  657. *
  658. * @param int $itemId 商品ID
  659. * @return array 检查结果
  660. */
  661. public static function checkUserSellItemMatchConditions(int $itemId): array
  662. {
  663. // 检查价格配置
  664. $priceConfig = MexPriceConfig::where('item_id', $itemId)->where('is_enabled', true)->first();
  665. if (!$priceConfig) {
  666. return [
  667. 'can_match' => false,
  668. 'message' => '商品未配置价格信息或已禁用',
  669. ];
  670. }
  671. // 检查是否有待撮合的用户卖出物品订单
  672. $pendingSellOrders = MexOrder::where('item_id', $itemId)
  673. ->where('order_type', OrderType::SELL)
  674. ->where('status', OrderStatus::PENDING)
  675. ->count();
  676. if ($pendingSellOrders === 0) {
  677. return [
  678. 'can_match' => false,
  679. 'message' => '没有待撮合的用户卖出物品订单',
  680. ];
  681. }
  682. return [
  683. 'can_match' => true,
  684. 'message' => '用户卖出物品撮合条件满足',
  685. 'price_config' => $priceConfig,
  686. 'pending_orders' => $pendingSellOrders,
  687. ];
  688. }
  689. /**
  690. * 获取用户买入物品撮合统计信息
  691. *
  692. * @return array 统计信息
  693. */
  694. public static function getUserBuyItemMatchStats(): array
  695. {
  696. // 获取待撮合用户买入物品订单统计
  697. $pendingStats = MexOrder::where('order_type', OrderType::BUY)
  698. ->where('status', OrderStatus::PENDING)
  699. ->selectRaw('
  700. COUNT(*) as total_pending,
  701. COUNT(DISTINCT item_id) as pending_items,
  702. SUM(quantity) as total_quantity,
  703. SUM(total_amount) as total_amount
  704. ')
  705. ->first();
  706. // 获取今日用户买入物品撮合统计
  707. $todayStats = MexTransaction::where('transaction_type', TransactionType::USER_BUY)
  708. ->whereDate('created_at', today())
  709. ->selectRaw('
  710. COUNT(*) as today_matched,
  711. SUM(quantity) as today_quantity,
  712. SUM(total_amount) as today_amount
  713. ')
  714. ->first();
  715. // 获取有库存的商品数量
  716. $availableItems = MexWarehouse::where('quantity', '>', 0)->count();
  717. return [
  718. 'pending_orders' => $pendingStats->total_pending ?? 0,
  719. 'pending_items' => $pendingStats->pending_items ?? 0,
  720. 'pending_quantity' => $pendingStats->total_quantity ?? 0,
  721. 'pending_amount' => $pendingStats->total_amount ?? '0.00000',
  722. 'today_matched' => $todayStats->today_matched ?? 0,
  723. 'today_quantity' => $todayStats->today_quantity ?? 0,
  724. 'today_amount' => $todayStats->today_amount ?? '0.00000',
  725. 'available_items' => $availableItems,
  726. 'stats_time' => now(),
  727. ];
  728. }
  729. /**
  730. * 获取用户卖出物品撮合统计信息
  731. *
  732. * @return array 统计信息
  733. */
  734. public static function getUserSellItemMatchStats(): array
  735. {
  736. // 获取待撮合用户卖出物品订单统计
  737. $pendingStats = MexOrder::where('order_type', OrderType::SELL)
  738. ->where('status', OrderStatus::PENDING)
  739. ->selectRaw('
  740. COUNT(*) as total_pending,
  741. COUNT(DISTINCT item_id) as pending_items,
  742. SUM(quantity) as total_quantity,
  743. SUM(total_amount) as total_amount
  744. ')
  745. ->first();
  746. // 获取今日用户卖出物品撮合统计
  747. $todayStats = MexTransaction::where('transaction_type', TransactionType::USER_SELL)
  748. ->whereDate('created_at', today())
  749. ->selectRaw('
  750. COUNT(*) as today_matched,
  751. SUM(quantity) as today_quantity,
  752. SUM(total_amount) as today_amount
  753. ')
  754. ->first();
  755. return [
  756. 'pending_orders' => $pendingStats->total_pending ?? 0,
  757. 'pending_items' => $pendingStats->pending_items ?? 0,
  758. 'pending_quantity' => $pendingStats->total_quantity ?? 0,
  759. 'pending_amount' => $pendingStats->total_amount ?? '0.00000',
  760. 'today_matched' => $todayStats->today_matched ?? 0,
  761. 'today_quantity' => $todayStats->today_quantity ?? 0,
  762. 'today_amount' => $todayStats->today_amount ?? '0.00000',
  763. 'stats_time' => now(),
  764. ];
  765. }
  766. /**
  767. * 将用户冻结资金转入仓库账户
  768. *
  769. * @param int $userId 用户ID
  770. * @param string $amount 金额
  771. * @param int $orderId 订单ID
  772. * @param FUND_CURRENCY_TYPE|null $currencyType 币种类型,默认使用钻石
  773. * @return array 转移结果
  774. */
  775. private static function transferFrozenFundsToWarehouse(int $userId, string $amount, int $orderId, ?FUND_CURRENCY_TYPE $currencyType = null): array
  776. {
  777. try {
  778. // 获取币种类型,默认使用钻石
  779. $currencyType = $currencyType ?? FundLogic::getDefaultCurrency();
  780. // 获取对应的冻结账户类型
  781. $frozenAccountType = FundLogic::getFrozenAccountType($currencyType);
  782. if (!$frozenAccountType) {
  783. return [
  784. 'success' => false,
  785. 'message' => '不支持的币种类型',
  786. ];
  787. }
  788. // 从用户冻结账户转移到仓库账户
  789. $fundService = new FundService($userId, $frozenAccountType->value);
  790. // 多少钱,就是多少钱,资金模块能够正确处理,不需要外部处理
  791. $result = $fundService->trade(
  792. self::WAREHOUSE_USER_ID,
  793. $amount,
  794. 'MEX_ORDER',
  795. $orderId,
  796. '用户买入物品撮合-资金转移'
  797. );
  798. if (is_string($result)) {
  799. return [
  800. 'success' => false,
  801. 'message' => $result,
  802. ];
  803. }
  804. return [
  805. 'success' => true,
  806. 'message' => '资金转移成功',
  807. 'data' => $result,
  808. ];
  809. } catch (\Exception $e) {
  810. return [
  811. 'success' => false,
  812. 'message' => '资金转移异常:' . $e->getMessage(),
  813. ];
  814. }
  815. }
  816. /**
  817. * 将仓库账户物品转出到用户账户
  818. *
  819. * @param int $userId 用户ID
  820. * @param int $itemId 物品ID
  821. * @param int $quantity 数量
  822. * @param int $orderId 订单ID
  823. * @return array 转移结果
  824. */
  825. private static function transferItemsFromWarehouseToUser(int $userId, int $itemId, int $quantity, int $orderId): array
  826. {
  827. try {
  828. // 添加物品到用户账户
  829. $result = ItemService::addItem($userId, $itemId, $quantity, [
  830. 'source' => 'mex_order',
  831. 'source_type' => REWARD_SOURCE_TYPE::MEX_BUY,
  832. 'source_id' => $orderId,
  833. 'remark' => '用户买入物品撮合-物品转移',
  834. ]);
  835. if (!$result || !isset($result['success']) || !$result['success']) {
  836. return [
  837. 'success' => false,
  838. 'message' => '物品添加失败:' . ($result['message'] ?? '未知错误'),
  839. ];
  840. }
  841. return [
  842. 'success' => true,
  843. 'message' => '物品转移成功',
  844. 'data' => $result,
  845. ];
  846. } catch (\Exception $e) {
  847. return [
  848. 'success' => false,
  849. 'message' => '物品转移异常:' . $e->getMessage(),
  850. ];
  851. }
  852. }
  853. /**
  854. * 将用户冻结物品转入仓库账户
  855. *
  856. * @param int $userId 用户ID
  857. * @param int $itemId 物品ID
  858. * @param int $quantity 数量
  859. * @param int $orderId 订单ID
  860. * @return array 转移结果
  861. */
  862. private static function transferFrozenItemsToWarehouse(int $userId, int $itemId, int $quantity, int $orderId): array
  863. {
  864. try {
  865. // TODO: 这里需要实现从用户冻结物品转移到仓库的逻辑
  866. // 由于物品冻结功能比较复杂,这里先返回成功,后续完善
  867. // 消耗用户物品(包括冻结的物品)
  868. $result = ItemService::consumeItem($userId, $itemId, null, $quantity, [
  869. 'source' => 'mex_order',
  870. 'source_id' => $orderId,
  871. 'remark' => '用户卖出物品撮合-物品转移',
  872. 'include_frozen' => true, // 包括冻结的物品
  873. ]);
  874. if (!$result || !isset($result['success']) || !$result['success']) {
  875. return [
  876. 'success' => false,
  877. 'message' => '物品消耗失败:' . ($result['message'] ?? '未知错误'),
  878. ];
  879. }
  880. return [
  881. 'success' => true,
  882. 'message' => '物品转移成功',
  883. 'data' => $result,
  884. ];
  885. } catch (\Exception $e) {
  886. return [
  887. 'success' => false,
  888. 'message' => '物品转移异常:' . $e->getMessage(),
  889. ];
  890. }
  891. }
  892. /**
  893. * 将仓库账户资金转出到用户账户
  894. *
  895. * @param int $userId 用户ID
  896. * @param string $amount 金额
  897. * @param int $orderId 订单ID
  898. * @param FUND_CURRENCY_TYPE|null $currencyType 币种类型,默认使用钻石
  899. * @return array 转移结果
  900. */
  901. private static function transferFundsFromWarehouseToUser(int $userId, string $amount, int $orderId, ?FUND_CURRENCY_TYPE $currencyType = null): array
  902. {
  903. try {
  904. // 获取币种类型,默认使用钻石
  905. $currencyType = $currencyType ?? FundLogic::getDefaultCurrency();
  906. // 获取对应的可用账户类型
  907. $availableAccountType = FundLogic::getAvailableAccountType($currencyType);
  908. if (!$availableAccountType) {
  909. return [
  910. 'success' => false,
  911. 'message' => '不支持的币种类型',
  912. ];
  913. }
  914. // 从仓库账户转移到用户账户
  915. $fundService = new FundService(self::WAREHOUSE_USER_ID, $availableAccountType->value);
  916. // 资金系统,能够正确处理金额,不需要外部处理
  917. $result = $fundService->trade(
  918. $userId,
  919. $amount,
  920. 'MEX_ORDER',
  921. $orderId,
  922. '用户卖出物品撮合-资金转移'
  923. );
  924. if (is_string($result)) {
  925. return [
  926. 'success' => false,
  927. 'message' => $result,
  928. ];
  929. }
  930. return [
  931. 'success' => true,
  932. 'message' => '资金转移成功',
  933. 'data' => $result,
  934. ];
  935. } catch (\Exception $e) {
  936. return [
  937. 'success' => false,
  938. 'message' => '资金转移异常:' . $e->getMessage(),
  939. ];
  940. }
  941. }
  942. }