test_ghs_stock_concurrency.php 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806
  1. #!/usr/bin/env php
  2. <?php
  3. /**
  4. * Concurrently calls app-ghs stock-changing APIs,
  5. * then compares xhGhsItem.stock with the expected stock after successful calls.
  6. *
  7. * WARNING: this script sends real requests to api.shop.hzghd.com and creates
  8. * real business records. It does not rollback stock, orders, or stock-out rows.
  9. *
  10. * Usage:
  11. * php scripts/test_ghs_stock_concurrency.php
  12. * php scripts/test_ghs_stock_concurrency.php --rounds=10
  13. * php scripts/test_ghs_stock_concurrency.php --order=2 --stock-out=2 --wastage=2 --hd-purchase=2 --rounds=3
  14. * php scripts/test_ghs_stock_concurrency.php --stock-in-confirm=RK123 --stock-out-cancel=CK123 --purchase-put-in=123
  15. */
  16. require(__DIR__ . '/../vendor/autoload.php');
  17. require(__DIR__ . '/../env.php');
  18. require(__DIR__ . '/../vendor/yiisoft/yii2/Yii.php');
  19. require(__DIR__ . '/../common/config/bootstrap.php');
  20. require(__DIR__ . '/../console/config/bootstrap.php');
  21. $config = yii\helpers\ArrayHelper::merge(
  22. require(__DIR__ . '/../common/config/main.php'),
  23. require(__DIR__ . '/../common/config/main-local.php'),
  24. require(__DIR__ . '/../console/config/main.php'),
  25. require(__DIR__ . '/../console/config/main-local.php')
  26. );
  27. unset($config['components']['request']);
  28. new yii\console\Application($config);
  29. if (!extension_loaded('curl')) {
  30. fwrite(STDERR, "ERROR: PHP curl extension is required.\n");
  31. exit(1);
  32. }
  33. $options = getopt('', ['order::', 'stock-out::', 'wastage::', 'hd-purchase::', 'stock-in-confirm::', 'stock-out-cancel::', 'purchase-put-in::', 'rounds::']);
  34. $orderCount = normalizeNonNegativeInt($options, 'order', 1);
  35. $stockOutCount = normalizeNonNegativeInt($options, 'stock-out', 1);
  36. $wastageCount = normalizeNonNegativeInt($options, 'wastage', 1);
  37. $hdPurchaseCount = normalizeNonNegativeInt($options, 'hd-purchase', 1);
  38. $rounds = normalizePositiveInt($options, 'rounds', 1);
  39. $baseUrl = 'http://api.shop.hzghd.com';
  40. $hdBaseUrl = 'http://api.shop.huaml.com';
  41. $defaultProductIds = [27286, 27282];
  42. $commonHeaders = [
  43. 'Connection: keep-alive',
  44. 'content-type: application/json;charset=UTF-8',
  45. 'User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 17_0_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0.1 Mobile/21A360 Safari/604.1 wechatdevtools/2.01.2510260 MicroMessenger/8.0.5 Language/zh_CN webview/ hash/1237023557 sid/o7x10OLPfZ',
  46. 'token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImp0aSI6IjBfOTMyIn0.eyJpc3MiOiJodHRwczpcL1wvYXBpLnNob3AuaHpnaGQuY29tIiwiYXVkIjoiaHR0cHM6XC9cL2FwaS5zaG9wLmh6Z2hkLmNvbSIsImp0aSI6IjBfOTMyIiwiaWF0IjoxNzc5Mjc1MzgxLCJuYmYiOjE3NzkyNzUzODEsImV4cCI6MTg3Mzg4MzM4MSwidW5pcXVlSWQiOjkzMiwic291cmNlSWQiOjB9.OVuQTjXMvklAFnYdIIRGVR2D5VK14Dp6QX5bDQAUHyE',
  47. 'appVersion: 2',
  48. 'Accept: */*',
  49. 'Sec-Fetch-Site: cross-site',
  50. 'Sec-Fetch-Mode: cors',
  51. 'Sec-Fetch-Dest: empty',
  52. 'Referer: https://servicewechat.com/wx21b7c3ef12082099/devtools/page-frame.html',
  53. 'Accept-Language: zh-CN,zh;q=0.9',
  54. ];
  55. $hdHeaders = [
  56. 'Connection: keep-alive',
  57. 'content-type: application/json;charset=UTF-8',
  58. 'User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1 wechatdevtools/2.01.2510260 MicroMessenger/8.0.5 Language/zh_CN webview/ hash/1009552471 sid/xxB6sCFvvu',
  59. 'token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImp0aSI6IjBfMTM5MSJ9.eyJpc3MiOiJodHRwczpcL1wvYXBpLnNob3AuaHVhbWwuY29tIiwiYXVkIjoiaHR0cHM6XC9cL2FwaS5zaG9wLmh1YW1sLmNvbSIsImp0aSI6IjBfMTM5MSIsImlhdCI6MTc4MDAzNzMxMSwibmJmIjoxNzgwMDM3MzExLCJleHAiOjE4NzQ2NDUzMTEsInVuaXF1ZUlkIjoiMTM5MSIsInNvdXJjZUlkIjowfQ.BgiDsxOd9cktOLsDe8Ud_hcFqzmVwSzKjCVwUdrSXKw',
  60. 'appVersion: 2',
  61. 'Accept: */*',
  62. 'Sec-Fetch-Site: cross-site',
  63. 'Sec-Fetch-Mode: cors',
  64. 'Sec-Fetch-Dest: empty',
  65. 'Referer: https://servicewechat.com/wxe4675bab299a52f7/devtools/page-frame.html',
  66. 'Accept-Language: zh-CN,zh;q=0.9',
  67. ];
  68. $orderBody = '{
  69. "shopId": "12986",
  70. "shopName": "",
  71. "sendType": "0",
  72. "transType": "4",
  73. "sendDate": "",
  74. "sendTimeWant": "",
  75. "historyDate": "",
  76. "sendCost": "",
  77. "customSendCost": "0",
  78. "customId": "914",
  79. "packCost": "",
  80. "product": [
  81. {
  82. "productId": "27286",
  83. "bigNum": 2,
  84. "smallNum": 0,
  85. "classId": "3322",
  86. "itemId": "1009",
  87. "price": "27.00",
  88. "remark": ""
  89. },
  90. {
  91. "productId": "27282",
  92. "bigNum": 2,
  93. "smallNum": 0,
  94. "classId": "3322",
  95. "itemId": "1005",
  96. "price": "0.30",
  97. "remark": ""
  98. }
  99. ],
  100. "remark": "",
  101. "payWay": "0",
  102. "needPrint": "1",
  103. "hasPay": "2",
  104. "getStaffId": "0",
  105. "getStaffName": "",
  106. "dealPrice": "0",
  107. "wlName": "",
  108. "reductionRule": "0",
  109. "callErrand": "0",
  110. "weight": "1",
  111. "deliveryRemark": "",
  112. "customName": "微升漫花",
  113. "modifyPrice": "54.6",
  114. "newVersion": "1",
  115. "book": "0",
  116. "xj": "\"\"",
  117. "treeData": "\"\""
  118. }';
  119. $stockOutBody = '{
  120. "confirmIn": "1",
  121. "date": "2026-05-25",
  122. "remark": "123",
  123. "inShopId": "37043",
  124. "itemInfo": [
  125. {
  126. "productId": "27286",
  127. "bigNum": 1,
  128. "smallNum": 0,
  129. "name": "苏醒"
  130. },
  131. {
  132. "productId": "27282",
  133. "bigNum": 1,
  134. "smallNum": 0,
  135. "name": "洛神"
  136. }
  137. ],
  138. "action": "confirm"
  139. }';
  140. $hdPurchaseBody = '{
  141. "getType": "2",
  142. "sendTimeWant": "",
  143. "sendType": "0",
  144. "transType": "4",
  145. "remark": "",
  146. "product": [
  147. {
  148. "productId": "27286",
  149. "bigNum": 1,
  150. "smallNum": 0,
  151. "classId": "3322",
  152. "itemId": "1009",
  153. "weight": "1.20",
  154. "name": "苏醒"
  155. }
  156. ],
  157. "ghsId": "932",
  158. "hbId": "0",
  159. "wlName": "",
  160. "xj": "\"\"",
  161. "itemTotalAmount": "27",
  162. "version": "10",
  163. "direct": "0"
  164. }';
  165. $wastageBody = '{
  166. "remark": "concurrency-test",
  167. "product": [
  168. {
  169. "productId": "27286",
  170. "bigNum": 1,
  171. "smallNum": 0,
  172. "classId": "3322",
  173. "itemId": "1009"
  174. },
  175. {
  176. "productId": "27282",
  177. "bigNum": 1,
  178. "smallNum": 0,
  179. "classId": "3322",
  180. "itemId": "1005"
  181. }
  182. ]
  183. }';
  184. $orderBody = jsonBodyToJsonRequestBody($orderBody, ['product', 'xj', 'treeData']);
  185. $stockOutBody = jsonBodyToJsonRequestBody($stockOutBody, ['itemInfo']);
  186. $hdPurchaseBody = jsonBodyToJsonRequestBody($hdPurchaseBody, ['product', 'xj']);
  187. $wastageBody = jsonBodyToJsonRequestBody($wastageBody, ['product']);
  188. $requestDefs = [
  189. 'order' => [
  190. 'method' => 'POST',
  191. 'url' => $baseUrl . '/order/create-order',
  192. 'body' => $orderBody,
  193. 'stockDelta' => [27286 => '2', 27282 => '2'],
  194. ],
  195. 'stock-out' => [
  196. 'method' => 'POST',
  197. 'url' => $baseUrl . '/stock-out/create-order',
  198. 'body' => $stockOutBody,
  199. 'stockDelta' => [27286 => '1', 27282 => '1'],
  200. ],
  201. 'wastage' => [
  202. 'method' => 'POST',
  203. 'url' => $baseUrl . '/wastage/create-order',
  204. 'body' => $wastageBody,
  205. 'stockDelta' => [27286 => '1', 27282 => '1'],
  206. ],
  207. 'hd-purchase' => [
  208. 'method' => 'POST',
  209. 'url' => $hdBaseUrl . '/purchase/create-order',
  210. 'headers' => $hdHeaders,
  211. 'body' => $hdPurchaseBody,
  212. 'stockDelta' => [27286 => '1'],
  213. ],
  214. ];
  215. $requestCounts = ['order' => $orderCount, 'stock-out' => $stockOutCount, 'wastage' => $wastageCount, 'hd-purchase' => $hdPurchaseCount];
  216. addParameterizedRequests($requestDefs, $requestCounts, $options, $baseUrl);
  217. $productIds = collectProductIds($requestDefs, $defaultProductIds);
  218. echo "app-ghs stock concurrency test\n";
  219. echo "Targets: {$baseUrl}, {$hdBaseUrl}\n";
  220. echo "Rounds: {$rounds}, order/create-order: {$orderCount}, stock-out/create-order: {$stockOutCount}, wastage/create-order: {$wastageCount}, app-hd purchase/create-order: {$hdPurchaseCount}\n";
  221. echo "Products: " . implode(', ', $productIds) . "\n";
  222. echo "WARNING: real requests will create real orders and decrease real stock.\n\n";
  223. $overallPass = true;
  224. $overallInvalid = false;
  225. $overallConcurrencyError = false;
  226. $appLogFiles = [
  227. 'app-ghs' => __DIR__ . '/../app-ghs/runtime/logs/app.log',
  228. 'app-hd' => __DIR__ . '/../app-hd/runtime/logs/app.log',
  229. ];
  230. for ($round = 1; $round <= $rounds; $round++) {
  231. echo "========== Round {$round}/{$rounds} ==========\n";
  232. $before = loadStocks($productIds);
  233. printStocks('Before', $before, $productIds);
  234. $currentRequestDefs = $requestDefs;
  235. $priceSummary = '';
  236. $currentRequestDefs['order']['body'] = refreshOrderBodyPrices($orderBody, $priceSummary);
  237. echo "Order request pricing: {$priceSummary}\n";
  238. $requests = buildRequests($currentRequestDefs, $requestCounts);
  239. $logOffsets = getLogOffsets($appLogFiles);
  240. $responses = runConcurrentRequests($requests, $commonHeaders);
  241. $concurrencyErrors = findConcurrencyErrorsFromLogs($appLogFiles, $logOffsets);
  242. foreach ($responses as $response) {
  243. $ok = $response['businessSuccess'] ? 'OK' : 'ERR';
  244. echo sprintf(
  245. "[%s] %-18s HTTP %s %.3fs %s\n",
  246. $ok,
  247. $response['label'],
  248. $response['httpCode'],
  249. $response['totalTime'],
  250. $response['summary']
  251. );
  252. }
  253. if (!empty($concurrencyErrors)) {
  254. echo "Concurrency errors from app log:\n";
  255. foreach ($concurrencyErrors as $errorLine) {
  256. echo " - " . $errorLine . "\n";
  257. }
  258. }
  259. $successCounts = countSuccessfulRequests($responses);
  260. $after = loadStocks($productIds);
  261. $expected = calcExpectedStocks($before, $currentRequestDefs, $successCounts, $productIds);
  262. printStocks('Expected after successful requests', $expected, $productIds);
  263. printStocks('Actual', $after, $productIds);
  264. printDiff($expected, $after, $productIds);
  265. $allBusinessSuccess = count($responses) === array_sum($successCounts);
  266. $stockMatches = stocksMatch($expected, $after, $productIds);
  267. if (!$allBusinessSuccess && !empty($concurrencyErrors)) {
  268. $overallConcurrencyError = true;
  269. echo "Round result: CONCURRENCY_ERROR (business request failed because concurrent stock update hit a database lock/deadlock)\n\n";
  270. continue;
  271. }
  272. if (!$allBusinessSuccess) {
  273. $overallInvalid = true;
  274. echo "Round result: INVALID (one or more business requests failed; this round is not a valid concurrency conclusion)\n\n";
  275. continue;
  276. }
  277. if ($stockMatches) {
  278. echo "Round result: PASS\n\n";
  279. } else {
  280. $overallPass = false;
  281. echo "Round result: FAIL (stock mismatch after all business requests succeeded)\n\n";
  282. }
  283. }
  284. if ($overallConcurrencyError) {
  285. echo "FINAL: CONCURRENCY_ERROR\n";
  286. exit(4);
  287. }
  288. if ($overallInvalid && $overallPass) {
  289. echo "FINAL: INVALID (at least one round had failed business responses)\n";
  290. exit(3);
  291. }
  292. if (!$overallInvalid && $overallPass) {
  293. echo "FINAL: PASS\n";
  294. exit(0);
  295. }
  296. if (!$overallPass) {
  297. echo "FINAL: FAIL\n";
  298. exit(2);
  299. }
  300. exit(3);
  301. function normalizePositiveInt($options, $key, $default)
  302. {
  303. if (!isset($options[$key]) || $options[$key] === false || $options[$key] === '') {
  304. return $default;
  305. }
  306. $value = (int)$options[$key];
  307. if ($value < 1) {
  308. fwrite(STDERR, "ERROR: --{$key} must be a positive integer.\n");
  309. exit(1);
  310. }
  311. return $value;
  312. }
  313. function normalizeNonNegativeInt($options, $key, $default)
  314. {
  315. if (!isset($options[$key]) || $options[$key] === false || $options[$key] === '') {
  316. return $default;
  317. }
  318. $value = (int)$options[$key];
  319. if ($value < 0) {
  320. fwrite(STDERR, "ERROR: --{$key} must be a non-negative integer.\n");
  321. exit(1);
  322. }
  323. return $value;
  324. }
  325. function jsonBodyToJsonRequestBody($jsonBody, $jsonStringFields)
  326. {
  327. $params = json_decode($jsonBody, true);
  328. if (!is_array($params)) {
  329. throw new RuntimeException('Invalid JSON request body: ' . json_last_error_msg());
  330. }
  331. return encodeJsonRequestBody($params, $jsonStringFields);
  332. }
  333. function encodeJsonRequestBody($params, $jsonStringFields)
  334. {
  335. foreach ($jsonStringFields as $field) {
  336. if (isset($params[$field]) && is_array($params[$field])) {
  337. $params[$field] = json_encode($params[$field], JSON_UNESCAPED_UNICODE);
  338. }
  339. }
  340. return json_encode($params, JSON_UNESCAPED_UNICODE);
  341. }
  342. function loadStocks($productIds)
  343. {
  344. $rows = Yii::$app->db->createCommand(
  345. 'select id, stock from xhGhsItem where id in (' . implode(',', array_map('intval', $productIds)) . ')'
  346. )->queryAll();
  347. $stocks = [];
  348. foreach ($rows as $row) {
  349. $stocks[(int)$row['id']] = (string)$row['stock'];
  350. }
  351. foreach ($productIds as $productId) {
  352. if (!array_key_exists($productId, $stocks)) {
  353. throw new RuntimeException("Product {$productId} was not found in xhGhsItem.");
  354. }
  355. }
  356. return $stocks;
  357. }
  358. function buildRequests($requestDefs, $requestCounts)
  359. {
  360. $requests = [];
  361. foreach ($requestCounts as $type => $count) {
  362. if (empty($requestDefs[$type]) || $count <= 0) {
  363. continue;
  364. }
  365. for ($i = 1; $i <= $count; $i++) {
  366. $requests[] = [
  367. 'type' => $type,
  368. 'label' => $type . '#' . $i,
  369. 'method' => isset($requestDefs[$type]['method']) ? $requestDefs[$type]['method'] : 'POST',
  370. 'url' => $requestDefs[$type]['url'],
  371. 'headers' => isset($requestDefs[$type]['headers']) ? $requestDefs[$type]['headers'] : null,
  372. 'body' => isset($requestDefs[$type]['body']) ? $requestDefs[$type]['body'] : '',
  373. ];
  374. }
  375. }
  376. return $requests;
  377. }
  378. function addParameterizedRequests(&$requestDefs, &$requestCounts, $options, $baseUrl)
  379. {
  380. if (!empty($options['stock-in-confirm'])) {
  381. $orderSn = (string)$options['stock-in-confirm'];
  382. $requestDefs['stock-in-confirm'] = [
  383. 'method' => 'GET',
  384. 'url' => $baseUrl . '/stock-in/confirm-order?orderSn=' . rawurlencode($orderSn),
  385. 'body' => '',
  386. 'stockDelta' => loadItemNumDelta('xhGhsStockInOrderItem', $orderSn, -1),
  387. ];
  388. $requestCounts['stock-in-confirm'] = 1;
  389. }
  390. if (!empty($options['stock-out-cancel'])) {
  391. $orderSn = (string)$options['stock-out-cancel'];
  392. $requestDefs['stock-out-cancel'] = [
  393. 'method' => 'GET',
  394. 'url' => $baseUrl . '/stock-out/cancel-order?orderSn=' . rawurlencode($orderSn),
  395. 'body' => '',
  396. 'stockDelta' => loadItemNumDelta('xhGhsStockOutOrderItem', $orderSn, -1),
  397. ];
  398. $requestCounts['stock-out-cancel'] = 1;
  399. }
  400. if (!empty($options['purchase-put-in'])) {
  401. $purchaseId = intval($options['purchase-put-in']);
  402. $orderSn = Yii::$app->db->createCommand('select orderSn from xhGhsCgOrder where id=:id', [':id' => $purchaseId])->queryScalar();
  403. if (!empty($orderSn)) {
  404. $requestDefs['purchase-put-in'] = [
  405. 'method' => 'GET',
  406. 'url' => $baseUrl . '/purchase-order/confirm-put-in?id=' . $purchaseId,
  407. 'body' => '',
  408. 'stockDelta' => loadItemNumDelta('xhGhsCgOrderItem', $orderSn, -1),
  409. ];
  410. $requestCounts['purchase-put-in'] = 1;
  411. }
  412. }
  413. }
  414. function loadItemNumDelta($tableName, $orderSn, $direction)
  415. {
  416. $rows = Yii::$app->db->createCommand(
  417. "select productId, itemNum from {$tableName} where orderSn=:orderSn",
  418. [':orderSn' => $orderSn]
  419. )->queryAll();
  420. $delta = [];
  421. foreach ($rows as $row) {
  422. $productId = intval($row['productId']);
  423. $itemNum = (string)$row['itemNum'];
  424. if ($direction < 0) {
  425. $itemNum = bcmul($itemNum, '-1', 2);
  426. }
  427. $delta[$productId] = isset($delta[$productId]) ? bcadd($delta[$productId], $itemNum, 2) : $itemNum;
  428. }
  429. return $delta;
  430. }
  431. function collectProductIds($requestDefs, $defaultProductIds)
  432. {
  433. $ids = $defaultProductIds;
  434. foreach ($requestDefs as $requestDef) {
  435. foreach (array_keys($requestDef['stockDelta']) as $productId) {
  436. $ids[] = intval($productId);
  437. }
  438. }
  439. $ids = array_values(array_unique(array_filter($ids)));
  440. sort($ids);
  441. return $ids;
  442. }
  443. function refreshOrderBodyPrices($orderBody, &$summary)
  444. {
  445. $isJsonBody = false;
  446. $params = json_decode($orderBody, true);
  447. if (is_array($params)) {
  448. $isJsonBody = true;
  449. } else {
  450. parse_str($orderBody, $params);
  451. }
  452. $customId = isset($params['customId']) ? intval($params['customId']) : 0;
  453. $products = isset($params['product']) && is_array($params['product'])
  454. ? $params['product']
  455. : (isset($params['product']) ? json_decode($params['product'], true) : []);
  456. if (empty($customId) || empty($products) || !is_array($products)) {
  457. $summary = 'keep original prices';
  458. return $orderBody;
  459. }
  460. $custom = \bizGhs\custom\classes\CustomClass::getById($customId);
  461. if (empty($custom)) {
  462. $summary = 'keep original prices; custom not found';
  463. return $orderBody;
  464. }
  465. $productIds = array_unique(array_filter(array_column($products, 'productId')));
  466. $productInfo = \bizGhs\product\classes\ProductClass::getByIds($productIds, null, 'id');
  467. if (empty($productInfo)) {
  468. $summary = 'keep original prices; products not found';
  469. return $orderBody;
  470. }
  471. $priceMap = \bizGhs\custom\classes\CustomClass::$levelPriceKeyMap;
  472. $addPriceMap = \bizGhs\custom\classes\CustomClass::$levelAddPriceKeyMap;
  473. $addPriceMap['risePercent'] = isset($custom['risePercent']) ? $custom['risePercent'] : 0;
  474. $level = isset($custom['level']) ? $custom['level'] : 1;
  475. $modifyPrice = '0';
  476. $parts = [];
  477. foreach ($products as $key => $product) {
  478. $productId = isset($product['productId']) ? intval($product['productId']) : 0;
  479. if (empty($productInfo[$productId])) {
  480. continue;
  481. }
  482. $systemInfo = $productInfo[$productId];
  483. $price = \bizGhs\product\classes\ProductClass::getFinalPrice($systemInfo, $level, $priceMap, $addPriceMap);
  484. $bigNum = isset($product['bigNum']) ? (string)$product['bigNum'] : '0';
  485. $smallNum = isset($product['smallNum']) ? (string)$product['smallNum'] : '0';
  486. $reachDiscountPrice = '0';
  487. $reachNum = isset($systemInfo['reachNum']) ? $systemInfo['reachNum'] : 0;
  488. $reachNumDiscount = isset($systemInfo['reachNumDiscount']) ? $systemInfo['reachNumDiscount'] : 0;
  489. if ($reachNum > 0 && $bigNum >= $reachNum && $price > $reachNumDiscount) {
  490. $price = bcsub($price, $reachNumDiscount, 2);
  491. $reachDiscountPrice = bcmul($reachNumDiscount, $bigNum, 2);
  492. }
  493. $products[$key]['price'] = sprintf('%.2f', (float)$price);
  494. $linePrice = bcmul((string)$products[$key]['price'], $bigNum, 2);
  495. if ($smallNum > 0) {
  496. $ratio = isset($systemInfo['ratio']) && $systemInfo['ratio'] > 0 ? $systemInfo['ratio'] : 1;
  497. $smallItemNum = bcdiv($smallNum, $ratio, 2);
  498. $linePrice = bcadd($linePrice, bcmul((string)$products[$key]['price'], $smallItemNum, 2), 2);
  499. }
  500. $modifyPrice = bcadd($modifyPrice, $linePrice, 2);
  501. $name = isset($systemInfo['name']) ? $systemInfo['name'] : $productId;
  502. $parts[] = $productId . '(' . $name . ')=' . $products[$key]['price'] . 'x' . $bigNum . ($reachDiscountPrice > 0 ? ',reachDiscount=' . $reachDiscountPrice : '');
  503. }
  504. $params['product'] = json_encode($products, JSON_UNESCAPED_UNICODE);
  505. $params['modifyPrice'] = $modifyPrice;
  506. $summary = implode('; ', $parts) . '; modifyPrice=' . $modifyPrice;
  507. if ($isJsonBody) {
  508. return encodeJsonRequestBody($params, ['product', 'xj', 'treeData']);
  509. }
  510. return http_build_query($params);
  511. }
  512. function runConcurrentRequests($requests, $headers)
  513. {
  514. $multi = curl_multi_init();
  515. $handles = [];
  516. foreach ($requests as $index => $request) {
  517. $ch = curl_init($request['url']);
  518. $method = isset($request['method']) ? strtoupper($request['method']) : 'POST';
  519. $requestHeaders = !empty($request['headers']) ? $request['headers'] : $headers;
  520. $curlOptions = [
  521. CURLOPT_HTTPHEADER => $requestHeaders,
  522. CURLOPT_RETURNTRANSFER => true,
  523. CURLOPT_ENCODING => '',
  524. CURLOPT_CONNECTTIMEOUT => 10,
  525. CURLOPT_TIMEOUT => 60,
  526. ];
  527. if ($method === 'POST') {
  528. $curlOptions[CURLOPT_POST] = true;
  529. $curlOptions[CURLOPT_POSTFIELDS] = $request['body'];
  530. }
  531. curl_setopt_array($ch, $curlOptions);
  532. curl_multi_add_handle($multi, $ch);
  533. $handles[$index] = [
  534. 'handle' => $ch,
  535. 'request' => $request,
  536. 'startedAt' => microtime(true),
  537. ];
  538. }
  539. $running = null;
  540. do {
  541. $status = curl_multi_exec($multi, $running);
  542. if ($running) {
  543. $selected = curl_multi_select($multi, 1.0);
  544. if ($selected === -1) {
  545. usleep(1000);
  546. }
  547. }
  548. } while ($running && $status === CURLM_OK);
  549. $responses = [];
  550. foreach ($handles as $item) {
  551. $ch = $item['handle'];
  552. $body = curl_multi_getcontent($ch);
  553. $info = curl_getinfo($ch);
  554. $error = curl_error($ch);
  555. $httpCode = isset($info['http_code']) ? (int)$info['http_code'] : 0;
  556. $totalTime = isset($info['total_time']) ? (float)$info['total_time'] : microtime(true) - $item['startedAt'];
  557. $parsed = parseBusinessResponse($body, $httpCode, $error, $item['request']['type']);
  558. $responses[] = [
  559. 'type' => $item['request']['type'],
  560. 'label' => $item['request']['label'],
  561. 'httpCode' => $httpCode,
  562. 'totalTime' => $totalTime,
  563. 'businessSuccess' => $parsed['success'],
  564. 'summary' => $parsed['summary'],
  565. ];
  566. curl_multi_remove_handle($multi, $ch);
  567. curl_close($ch);
  568. }
  569. curl_multi_close($multi);
  570. return $responses;
  571. }
  572. function parseBusinessResponse($body, $httpCode, $curlError, $requestType)
  573. {
  574. if ($curlError !== '') {
  575. return ['success' => false, 'summary' => 'curl_error=' . $curlError];
  576. }
  577. $decoded = json_decode($body, true);
  578. if (is_array($decoded)) {
  579. $code = isset($decoded['code']) ? $decoded['code'] : null;
  580. $msg = isset($decoded['msg']) ? $decoded['msg'] : '';
  581. $data = isset($decoded['data']) && is_array($decoded['data']) ? $decoded['data'] : [];
  582. $respondType = isset($data['respondType']) ? (string)$data['respondType'] : '';
  583. $success = ($httpCode >= 200 && $httpCode < 300 && (string)$code === '1');
  584. if ($requestType === 'order') {
  585. $hasOrderIdentity = !empty($data['id']) || !empty($data['orderSn']);
  586. if ($respondType === 'priceError' || !$hasOrderIdentity) {
  587. $success = false;
  588. }
  589. }
  590. $extra = [];
  591. if ($respondType !== '') {
  592. $extra[] = 'respondType=' . $respondType;
  593. }
  594. if (!empty($data['id'])) {
  595. $extra[] = 'id=' . $data['id'];
  596. }
  597. if (!empty($data['orderSn'])) {
  598. $extra[] = 'orderSn=' . $data['orderSn'];
  599. }
  600. return [
  601. 'success' => $success,
  602. 'summary' => trimText('code=' . json_encode($code, JSON_UNESCAPED_UNICODE) . ' msg=' . (string)$msg . (!empty($extra) ? ' ' . implode(' ', $extra) : ''), 240),
  603. ];
  604. }
  605. return [
  606. 'success' => false,
  607. 'summary' => trimText(preg_replace('/\s+/', ' ', (string)$body), 240),
  608. ];
  609. }
  610. function countSuccessfulRequests($responses)
  611. {
  612. $counts = [];
  613. foreach ($responses as $response) {
  614. if (!isset($counts[$response['type']])) {
  615. $counts[$response['type']] = 0;
  616. }
  617. if ($response['businessSuccess']) {
  618. $counts[$response['type']]++;
  619. }
  620. }
  621. return $counts;
  622. }
  623. function calcExpectedStocks($before, $requestDefs, $successCounts, $productIds)
  624. {
  625. $expected = $before;
  626. foreach ($successCounts as $type => $count) {
  627. foreach ($productIds as $productId) {
  628. $delta = isset($requestDefs[$type]['stockDelta'][$productId])
  629. ? $requestDefs[$type]['stockDelta'][$productId]
  630. : '0';
  631. if ($count > 0 && bccomp($delta, '0', 2) !== 0) {
  632. $expected[$productId] = bcsub($expected[$productId], bcmul($delta, (string)$count, 2), 2);
  633. }
  634. }
  635. }
  636. return $expected;
  637. }
  638. function stocksMatch($expected, $actual, $productIds)
  639. {
  640. foreach ($productIds as $productId) {
  641. if (bccomp((string)$expected[$productId], (string)$actual[$productId], 2) !== 0) {
  642. return false;
  643. }
  644. }
  645. return true;
  646. }
  647. function printStocks($title, $stocks, $productIds)
  648. {
  649. echo $title . ': ';
  650. $parts = [];
  651. foreach ($productIds as $productId) {
  652. $parts[] = $productId . '=' . $stocks[$productId];
  653. }
  654. echo implode(', ', $parts) . "\n";
  655. }
  656. function printDiff($expected, $actual, $productIds)
  657. {
  658. echo 'Diff(actual - expected): ';
  659. $parts = [];
  660. foreach ($productIds as $productId) {
  661. $parts[] = $productId . '=' . bcsub((string)$actual[$productId], (string)$expected[$productId], 2);
  662. }
  663. echo implode(', ', $parts) . "\n";
  664. }
  665. function trimText($text, $maxLength)
  666. {
  667. $text = trim($text);
  668. if (function_exists('mb_strlen') && function_exists('mb_substr')) {
  669. return mb_strlen($text, 'UTF-8') > $maxLength
  670. ? mb_substr($text, 0, $maxLength, 'UTF-8') . '...'
  671. : $text;
  672. }
  673. return strlen($text) > $maxLength ? substr($text, 0, $maxLength) . '...' : $text;
  674. }
  675. function getFileSize($file)
  676. {
  677. clearstatcache(true, $file);
  678. return is_file($file) ? filesize($file) : 0;
  679. }
  680. function getLogOffsets($files)
  681. {
  682. $offsets = [];
  683. foreach ($files as $name => $file) {
  684. $offsets[$name] = getFileSize($file);
  685. }
  686. return $offsets;
  687. }
  688. function findConcurrencyErrorsFromLogs($files, $offsets)
  689. {
  690. $errors = [];
  691. foreach ($files as $name => $file) {
  692. $offset = isset($offsets[$name]) ? $offsets[$name] : 0;
  693. foreach (findConcurrencyErrors(readFileFromOffset($file, $offset)) as $error) {
  694. $errors[] = $name . ': ' . $error;
  695. }
  696. }
  697. return $errors;
  698. }
  699. function readFileFromOffset($file, $offset)
  700. {
  701. if (!is_file($file)) {
  702. return '';
  703. }
  704. $handle = fopen($file, 'rb');
  705. if (!$handle) {
  706. return '';
  707. }
  708. fseek($handle, $offset);
  709. $content = stream_get_contents($handle);
  710. fclose($handle);
  711. return $content === false ? '' : $content;
  712. }
  713. function findConcurrencyErrors($logContent)
  714. {
  715. if ($logContent === '') {
  716. return [];
  717. }
  718. $patterns = [
  719. 'Deadlock found',
  720. 'Serialization failure',
  721. 'Lock wait timeout exceeded',
  722. ];
  723. $lines = preg_split('/\r\n|\r|\n/', $logContent);
  724. $matched = [];
  725. foreach ($lines as $line) {
  726. foreach ($patterns as $pattern) {
  727. if (strpos($line, $pattern) !== false) {
  728. $matched[] = trimText(trim($line), 240);
  729. break;
  730. }
  731. }
  732. }
  733. return array_values(array_unique(array_filter($matched)));
  734. }