| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806 |
- #!/usr/bin/env php
- <?php
- /**
- * Concurrently calls app-ghs stock-changing APIs,
- * then compares xhGhsItem.stock with the expected stock after successful calls.
- *
- * WARNING: this script sends real requests to api.shop.hzghd.com and creates
- * real business records. It does not rollback stock, orders, or stock-out rows.
- *
- * Usage:
- * php scripts/test_ghs_stock_concurrency.php
- * php scripts/test_ghs_stock_concurrency.php --rounds=10
- * php scripts/test_ghs_stock_concurrency.php --order=2 --stock-out=2 --wastage=2 --hd-purchase=2 --rounds=3
- * php scripts/test_ghs_stock_concurrency.php --stock-in-confirm=RK123 --stock-out-cancel=CK123 --purchase-put-in=123
- */
- require(__DIR__ . '/../vendor/autoload.php');
- require(__DIR__ . '/../env.php');
- require(__DIR__ . '/../vendor/yiisoft/yii2/Yii.php');
- require(__DIR__ . '/../common/config/bootstrap.php');
- require(__DIR__ . '/../console/config/bootstrap.php');
- $config = yii\helpers\ArrayHelper::merge(
- require(__DIR__ . '/../common/config/main.php'),
- require(__DIR__ . '/../common/config/main-local.php'),
- require(__DIR__ . '/../console/config/main.php'),
- require(__DIR__ . '/../console/config/main-local.php')
- );
- unset($config['components']['request']);
- new yii\console\Application($config);
- if (!extension_loaded('curl')) {
- fwrite(STDERR, "ERROR: PHP curl extension is required.\n");
- exit(1);
- }
- $options = getopt('', ['order::', 'stock-out::', 'wastage::', 'hd-purchase::', 'stock-in-confirm::', 'stock-out-cancel::', 'purchase-put-in::', 'rounds::']);
- $orderCount = normalizeNonNegativeInt($options, 'order', 1);
- $stockOutCount = normalizeNonNegativeInt($options, 'stock-out', 1);
- $wastageCount = normalizeNonNegativeInt($options, 'wastage', 1);
- $hdPurchaseCount = normalizeNonNegativeInt($options, 'hd-purchase', 1);
- $rounds = normalizePositiveInt($options, 'rounds', 1);
- $baseUrl = 'http://api.shop.hzghd.com';
- $hdBaseUrl = 'http://api.shop.huaml.com';
- $defaultProductIds = [27286, 27282];
- $commonHeaders = [
- 'Connection: keep-alive',
- 'content-type: application/json;charset=UTF-8',
- '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',
- 'token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImp0aSI6IjBfOTMyIn0.eyJpc3MiOiJodHRwczpcL1wvYXBpLnNob3AuaHpnaGQuY29tIiwiYXVkIjoiaHR0cHM6XC9cL2FwaS5zaG9wLmh6Z2hkLmNvbSIsImp0aSI6IjBfOTMyIiwiaWF0IjoxNzc5Mjc1MzgxLCJuYmYiOjE3NzkyNzUzODEsImV4cCI6MTg3Mzg4MzM4MSwidW5pcXVlSWQiOjkzMiwic291cmNlSWQiOjB9.OVuQTjXMvklAFnYdIIRGVR2D5VK14Dp6QX5bDQAUHyE',
- 'appVersion: 2',
- 'Accept: */*',
- 'Sec-Fetch-Site: cross-site',
- 'Sec-Fetch-Mode: cors',
- 'Sec-Fetch-Dest: empty',
- 'Referer: https://servicewechat.com/wx21b7c3ef12082099/devtools/page-frame.html',
- 'Accept-Language: zh-CN,zh;q=0.9',
- ];
- $hdHeaders = [
- 'Connection: keep-alive',
- 'content-type: application/json;charset=UTF-8',
- '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',
- 'token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImp0aSI6IjBfMTM5MSJ9.eyJpc3MiOiJodHRwczpcL1wvYXBpLnNob3AuaHVhbWwuY29tIiwiYXVkIjoiaHR0cHM6XC9cL2FwaS5zaG9wLmh1YW1sLmNvbSIsImp0aSI6IjBfMTM5MSIsImlhdCI6MTc4MDAzNzMxMSwibmJmIjoxNzgwMDM3MzExLCJleHAiOjE4NzQ2NDUzMTEsInVuaXF1ZUlkIjoiMTM5MSIsInNvdXJjZUlkIjowfQ.BgiDsxOd9cktOLsDe8Ud_hcFqzmVwSzKjCVwUdrSXKw',
- 'appVersion: 2',
- 'Accept: */*',
- 'Sec-Fetch-Site: cross-site',
- 'Sec-Fetch-Mode: cors',
- 'Sec-Fetch-Dest: empty',
- 'Referer: https://servicewechat.com/wxe4675bab299a52f7/devtools/page-frame.html',
- 'Accept-Language: zh-CN,zh;q=0.9',
- ];
- $orderBody = '{
- "shopId": "12986",
- "shopName": "",
- "sendType": "0",
- "transType": "4",
- "sendDate": "",
- "sendTimeWant": "",
- "historyDate": "",
- "sendCost": "",
- "customSendCost": "0",
- "customId": "914",
- "packCost": "",
- "product": [
- {
- "productId": "27286",
- "bigNum": 2,
- "smallNum": 0,
- "classId": "3322",
- "itemId": "1009",
- "price": "27.00",
- "remark": ""
- },
- {
- "productId": "27282",
- "bigNum": 2,
- "smallNum": 0,
- "classId": "3322",
- "itemId": "1005",
- "price": "0.30",
- "remark": ""
- }
- ],
- "remark": "",
- "payWay": "0",
- "needPrint": "1",
- "hasPay": "2",
- "getStaffId": "0",
- "getStaffName": "",
- "dealPrice": "0",
- "wlName": "",
- "reductionRule": "0",
- "callErrand": "0",
- "weight": "1",
- "deliveryRemark": "",
- "customName": "微升漫花",
- "modifyPrice": "54.6",
- "newVersion": "1",
- "book": "0",
- "xj": "\"\"",
- "treeData": "\"\""
- }';
- $stockOutBody = '{
- "confirmIn": "1",
- "date": "2026-05-25",
- "remark": "123",
- "inShopId": "37043",
- "itemInfo": [
- {
- "productId": "27286",
- "bigNum": 1,
- "smallNum": 0,
- "name": "苏醒"
- },
- {
- "productId": "27282",
- "bigNum": 1,
- "smallNum": 0,
- "name": "洛神"
- }
- ],
- "action": "confirm"
- }';
- $hdPurchaseBody = '{
- "getType": "2",
- "sendTimeWant": "",
- "sendType": "0",
- "transType": "4",
- "remark": "",
- "product": [
- {
- "productId": "27286",
- "bigNum": 1,
- "smallNum": 0,
- "classId": "3322",
- "itemId": "1009",
- "weight": "1.20",
- "name": "苏醒"
- }
- ],
- "ghsId": "932",
- "hbId": "0",
- "wlName": "",
- "xj": "\"\"",
- "itemTotalAmount": "27",
- "version": "10",
- "direct": "0"
- }';
- $wastageBody = '{
- "remark": "concurrency-test",
- "product": [
- {
- "productId": "27286",
- "bigNum": 1,
- "smallNum": 0,
- "classId": "3322",
- "itemId": "1009"
- },
- {
- "productId": "27282",
- "bigNum": 1,
- "smallNum": 0,
- "classId": "3322",
- "itemId": "1005"
- }
- ]
- }';
- $orderBody = jsonBodyToJsonRequestBody($orderBody, ['product', 'xj', 'treeData']);
- $stockOutBody = jsonBodyToJsonRequestBody($stockOutBody, ['itemInfo']);
- $hdPurchaseBody = jsonBodyToJsonRequestBody($hdPurchaseBody, ['product', 'xj']);
- $wastageBody = jsonBodyToJsonRequestBody($wastageBody, ['product']);
- $requestDefs = [
- 'order' => [
- 'method' => 'POST',
- 'url' => $baseUrl . '/order/create-order',
- 'body' => $orderBody,
- 'stockDelta' => [27286 => '2', 27282 => '2'],
- ],
- 'stock-out' => [
- 'method' => 'POST',
- 'url' => $baseUrl . '/stock-out/create-order',
- 'body' => $stockOutBody,
- 'stockDelta' => [27286 => '1', 27282 => '1'],
- ],
- 'wastage' => [
- 'method' => 'POST',
- 'url' => $baseUrl . '/wastage/create-order',
- 'body' => $wastageBody,
- 'stockDelta' => [27286 => '1', 27282 => '1'],
- ],
- 'hd-purchase' => [
- 'method' => 'POST',
- 'url' => $hdBaseUrl . '/purchase/create-order',
- 'headers' => $hdHeaders,
- 'body' => $hdPurchaseBody,
- 'stockDelta' => [27286 => '1'],
- ],
- ];
- $requestCounts = ['order' => $orderCount, 'stock-out' => $stockOutCount, 'wastage' => $wastageCount, 'hd-purchase' => $hdPurchaseCount];
- addParameterizedRequests($requestDefs, $requestCounts, $options, $baseUrl);
- $productIds = collectProductIds($requestDefs, $defaultProductIds);
- echo "app-ghs stock concurrency test\n";
- echo "Targets: {$baseUrl}, {$hdBaseUrl}\n";
- echo "Rounds: {$rounds}, order/create-order: {$orderCount}, stock-out/create-order: {$stockOutCount}, wastage/create-order: {$wastageCount}, app-hd purchase/create-order: {$hdPurchaseCount}\n";
- echo "Products: " . implode(', ', $productIds) . "\n";
- echo "WARNING: real requests will create real orders and decrease real stock.\n\n";
- $overallPass = true;
- $overallInvalid = false;
- $overallConcurrencyError = false;
- $appLogFiles = [
- 'app-ghs' => __DIR__ . '/../app-ghs/runtime/logs/app.log',
- 'app-hd' => __DIR__ . '/../app-hd/runtime/logs/app.log',
- ];
- for ($round = 1; $round <= $rounds; $round++) {
- echo "========== Round {$round}/{$rounds} ==========\n";
- $before = loadStocks($productIds);
- printStocks('Before', $before, $productIds);
- $currentRequestDefs = $requestDefs;
- $priceSummary = '';
- $currentRequestDefs['order']['body'] = refreshOrderBodyPrices($orderBody, $priceSummary);
- echo "Order request pricing: {$priceSummary}\n";
- $requests = buildRequests($currentRequestDefs, $requestCounts);
- $logOffsets = getLogOffsets($appLogFiles);
- $responses = runConcurrentRequests($requests, $commonHeaders);
- $concurrencyErrors = findConcurrencyErrorsFromLogs($appLogFiles, $logOffsets);
- foreach ($responses as $response) {
- $ok = $response['businessSuccess'] ? 'OK' : 'ERR';
- echo sprintf(
- "[%s] %-18s HTTP %s %.3fs %s\n",
- $ok,
- $response['label'],
- $response['httpCode'],
- $response['totalTime'],
- $response['summary']
- );
- }
- if (!empty($concurrencyErrors)) {
- echo "Concurrency errors from app log:\n";
- foreach ($concurrencyErrors as $errorLine) {
- echo " - " . $errorLine . "\n";
- }
- }
- $successCounts = countSuccessfulRequests($responses);
- $after = loadStocks($productIds);
- $expected = calcExpectedStocks($before, $currentRequestDefs, $successCounts, $productIds);
- printStocks('Expected after successful requests', $expected, $productIds);
- printStocks('Actual', $after, $productIds);
- printDiff($expected, $after, $productIds);
- $allBusinessSuccess = count($responses) === array_sum($successCounts);
- $stockMatches = stocksMatch($expected, $after, $productIds);
- if (!$allBusinessSuccess && !empty($concurrencyErrors)) {
- $overallConcurrencyError = true;
- echo "Round result: CONCURRENCY_ERROR (business request failed because concurrent stock update hit a database lock/deadlock)\n\n";
- continue;
- }
- if (!$allBusinessSuccess) {
- $overallInvalid = true;
- echo "Round result: INVALID (one or more business requests failed; this round is not a valid concurrency conclusion)\n\n";
- continue;
- }
- if ($stockMatches) {
- echo "Round result: PASS\n\n";
- } else {
- $overallPass = false;
- echo "Round result: FAIL (stock mismatch after all business requests succeeded)\n\n";
- }
- }
- if ($overallConcurrencyError) {
- echo "FINAL: CONCURRENCY_ERROR\n";
- exit(4);
- }
- if ($overallInvalid && $overallPass) {
- echo "FINAL: INVALID (at least one round had failed business responses)\n";
- exit(3);
- }
- if (!$overallInvalid && $overallPass) {
- echo "FINAL: PASS\n";
- exit(0);
- }
- if (!$overallPass) {
- echo "FINAL: FAIL\n";
- exit(2);
- }
- exit(3);
- function normalizePositiveInt($options, $key, $default)
- {
- if (!isset($options[$key]) || $options[$key] === false || $options[$key] === '') {
- return $default;
- }
- $value = (int)$options[$key];
- if ($value < 1) {
- fwrite(STDERR, "ERROR: --{$key} must be a positive integer.\n");
- exit(1);
- }
- return $value;
- }
- function normalizeNonNegativeInt($options, $key, $default)
- {
- if (!isset($options[$key]) || $options[$key] === false || $options[$key] === '') {
- return $default;
- }
- $value = (int)$options[$key];
- if ($value < 0) {
- fwrite(STDERR, "ERROR: --{$key} must be a non-negative integer.\n");
- exit(1);
- }
- return $value;
- }
- function jsonBodyToJsonRequestBody($jsonBody, $jsonStringFields)
- {
- $params = json_decode($jsonBody, true);
- if (!is_array($params)) {
- throw new RuntimeException('Invalid JSON request body: ' . json_last_error_msg());
- }
- return encodeJsonRequestBody($params, $jsonStringFields);
- }
- function encodeJsonRequestBody($params, $jsonStringFields)
- {
- foreach ($jsonStringFields as $field) {
- if (isset($params[$field]) && is_array($params[$field])) {
- $params[$field] = json_encode($params[$field], JSON_UNESCAPED_UNICODE);
- }
- }
- return json_encode($params, JSON_UNESCAPED_UNICODE);
- }
- function loadStocks($productIds)
- {
- $rows = Yii::$app->db->createCommand(
- 'select id, stock from xhGhsItem where id in (' . implode(',', array_map('intval', $productIds)) . ')'
- )->queryAll();
- $stocks = [];
- foreach ($rows as $row) {
- $stocks[(int)$row['id']] = (string)$row['stock'];
- }
- foreach ($productIds as $productId) {
- if (!array_key_exists($productId, $stocks)) {
- throw new RuntimeException("Product {$productId} was not found in xhGhsItem.");
- }
- }
- return $stocks;
- }
- function buildRequests($requestDefs, $requestCounts)
- {
- $requests = [];
- foreach ($requestCounts as $type => $count) {
- if (empty($requestDefs[$type]) || $count <= 0) {
- continue;
- }
- for ($i = 1; $i <= $count; $i++) {
- $requests[] = [
- 'type' => $type,
- 'label' => $type . '#' . $i,
- 'method' => isset($requestDefs[$type]['method']) ? $requestDefs[$type]['method'] : 'POST',
- 'url' => $requestDefs[$type]['url'],
- 'headers' => isset($requestDefs[$type]['headers']) ? $requestDefs[$type]['headers'] : null,
- 'body' => isset($requestDefs[$type]['body']) ? $requestDefs[$type]['body'] : '',
- ];
- }
- }
- return $requests;
- }
- function addParameterizedRequests(&$requestDefs, &$requestCounts, $options, $baseUrl)
- {
- if (!empty($options['stock-in-confirm'])) {
- $orderSn = (string)$options['stock-in-confirm'];
- $requestDefs['stock-in-confirm'] = [
- 'method' => 'GET',
- 'url' => $baseUrl . '/stock-in/confirm-order?orderSn=' . rawurlencode($orderSn),
- 'body' => '',
- 'stockDelta' => loadItemNumDelta('xhGhsStockInOrderItem', $orderSn, -1),
- ];
- $requestCounts['stock-in-confirm'] = 1;
- }
- if (!empty($options['stock-out-cancel'])) {
- $orderSn = (string)$options['stock-out-cancel'];
- $requestDefs['stock-out-cancel'] = [
- 'method' => 'GET',
- 'url' => $baseUrl . '/stock-out/cancel-order?orderSn=' . rawurlencode($orderSn),
- 'body' => '',
- 'stockDelta' => loadItemNumDelta('xhGhsStockOutOrderItem', $orderSn, -1),
- ];
- $requestCounts['stock-out-cancel'] = 1;
- }
- if (!empty($options['purchase-put-in'])) {
- $purchaseId = intval($options['purchase-put-in']);
- $orderSn = Yii::$app->db->createCommand('select orderSn from xhGhsCgOrder where id=:id', [':id' => $purchaseId])->queryScalar();
- if (!empty($orderSn)) {
- $requestDefs['purchase-put-in'] = [
- 'method' => 'GET',
- 'url' => $baseUrl . '/purchase-order/confirm-put-in?id=' . $purchaseId,
- 'body' => '',
- 'stockDelta' => loadItemNumDelta('xhGhsCgOrderItem', $orderSn, -1),
- ];
- $requestCounts['purchase-put-in'] = 1;
- }
- }
- }
- function loadItemNumDelta($tableName, $orderSn, $direction)
- {
- $rows = Yii::$app->db->createCommand(
- "select productId, itemNum from {$tableName} where orderSn=:orderSn",
- [':orderSn' => $orderSn]
- )->queryAll();
- $delta = [];
- foreach ($rows as $row) {
- $productId = intval($row['productId']);
- $itemNum = (string)$row['itemNum'];
- if ($direction < 0) {
- $itemNum = bcmul($itemNum, '-1', 2);
- }
- $delta[$productId] = isset($delta[$productId]) ? bcadd($delta[$productId], $itemNum, 2) : $itemNum;
- }
- return $delta;
- }
- function collectProductIds($requestDefs, $defaultProductIds)
- {
- $ids = $defaultProductIds;
- foreach ($requestDefs as $requestDef) {
- foreach (array_keys($requestDef['stockDelta']) as $productId) {
- $ids[] = intval($productId);
- }
- }
- $ids = array_values(array_unique(array_filter($ids)));
- sort($ids);
- return $ids;
- }
- function refreshOrderBodyPrices($orderBody, &$summary)
- {
- $isJsonBody = false;
- $params = json_decode($orderBody, true);
- if (is_array($params)) {
- $isJsonBody = true;
- } else {
- parse_str($orderBody, $params);
- }
- $customId = isset($params['customId']) ? intval($params['customId']) : 0;
- $products = isset($params['product']) && is_array($params['product'])
- ? $params['product']
- : (isset($params['product']) ? json_decode($params['product'], true) : []);
- if (empty($customId) || empty($products) || !is_array($products)) {
- $summary = 'keep original prices';
- return $orderBody;
- }
- $custom = \bizGhs\custom\classes\CustomClass::getById($customId);
- if (empty($custom)) {
- $summary = 'keep original prices; custom not found';
- return $orderBody;
- }
- $productIds = array_unique(array_filter(array_column($products, 'productId')));
- $productInfo = \bizGhs\product\classes\ProductClass::getByIds($productIds, null, 'id');
- if (empty($productInfo)) {
- $summary = 'keep original prices; products not found';
- return $orderBody;
- }
- $priceMap = \bizGhs\custom\classes\CustomClass::$levelPriceKeyMap;
- $addPriceMap = \bizGhs\custom\classes\CustomClass::$levelAddPriceKeyMap;
- $addPriceMap['risePercent'] = isset($custom['risePercent']) ? $custom['risePercent'] : 0;
- $level = isset($custom['level']) ? $custom['level'] : 1;
- $modifyPrice = '0';
- $parts = [];
- foreach ($products as $key => $product) {
- $productId = isset($product['productId']) ? intval($product['productId']) : 0;
- if (empty($productInfo[$productId])) {
- continue;
- }
- $systemInfo = $productInfo[$productId];
- $price = \bizGhs\product\classes\ProductClass::getFinalPrice($systemInfo, $level, $priceMap, $addPriceMap);
- $bigNum = isset($product['bigNum']) ? (string)$product['bigNum'] : '0';
- $smallNum = isset($product['smallNum']) ? (string)$product['smallNum'] : '0';
- $reachDiscountPrice = '0';
- $reachNum = isset($systemInfo['reachNum']) ? $systemInfo['reachNum'] : 0;
- $reachNumDiscount = isset($systemInfo['reachNumDiscount']) ? $systemInfo['reachNumDiscount'] : 0;
- if ($reachNum > 0 && $bigNum >= $reachNum && $price > $reachNumDiscount) {
- $price = bcsub($price, $reachNumDiscount, 2);
- $reachDiscountPrice = bcmul($reachNumDiscount, $bigNum, 2);
- }
- $products[$key]['price'] = sprintf('%.2f', (float)$price);
- $linePrice = bcmul((string)$products[$key]['price'], $bigNum, 2);
- if ($smallNum > 0) {
- $ratio = isset($systemInfo['ratio']) && $systemInfo['ratio'] > 0 ? $systemInfo['ratio'] : 1;
- $smallItemNum = bcdiv($smallNum, $ratio, 2);
- $linePrice = bcadd($linePrice, bcmul((string)$products[$key]['price'], $smallItemNum, 2), 2);
- }
- $modifyPrice = bcadd($modifyPrice, $linePrice, 2);
- $name = isset($systemInfo['name']) ? $systemInfo['name'] : $productId;
- $parts[] = $productId . '(' . $name . ')=' . $products[$key]['price'] . 'x' . $bigNum . ($reachDiscountPrice > 0 ? ',reachDiscount=' . $reachDiscountPrice : '');
- }
- $params['product'] = json_encode($products, JSON_UNESCAPED_UNICODE);
- $params['modifyPrice'] = $modifyPrice;
- $summary = implode('; ', $parts) . '; modifyPrice=' . $modifyPrice;
- if ($isJsonBody) {
- return encodeJsonRequestBody($params, ['product', 'xj', 'treeData']);
- }
- return http_build_query($params);
- }
- function runConcurrentRequests($requests, $headers)
- {
- $multi = curl_multi_init();
- $handles = [];
- foreach ($requests as $index => $request) {
- $ch = curl_init($request['url']);
- $method = isset($request['method']) ? strtoupper($request['method']) : 'POST';
- $requestHeaders = !empty($request['headers']) ? $request['headers'] : $headers;
- $curlOptions = [
- CURLOPT_HTTPHEADER => $requestHeaders,
- CURLOPT_RETURNTRANSFER => true,
- CURLOPT_ENCODING => '',
- CURLOPT_CONNECTTIMEOUT => 10,
- CURLOPT_TIMEOUT => 60,
- ];
- if ($method === 'POST') {
- $curlOptions[CURLOPT_POST] = true;
- $curlOptions[CURLOPT_POSTFIELDS] = $request['body'];
- }
- curl_setopt_array($ch, $curlOptions);
- curl_multi_add_handle($multi, $ch);
- $handles[$index] = [
- 'handle' => $ch,
- 'request' => $request,
- 'startedAt' => microtime(true),
- ];
- }
- $running = null;
- do {
- $status = curl_multi_exec($multi, $running);
- if ($running) {
- $selected = curl_multi_select($multi, 1.0);
- if ($selected === -1) {
- usleep(1000);
- }
- }
- } while ($running && $status === CURLM_OK);
- $responses = [];
- foreach ($handles as $item) {
- $ch = $item['handle'];
- $body = curl_multi_getcontent($ch);
- $info = curl_getinfo($ch);
- $error = curl_error($ch);
- $httpCode = isset($info['http_code']) ? (int)$info['http_code'] : 0;
- $totalTime = isset($info['total_time']) ? (float)$info['total_time'] : microtime(true) - $item['startedAt'];
- $parsed = parseBusinessResponse($body, $httpCode, $error, $item['request']['type']);
- $responses[] = [
- 'type' => $item['request']['type'],
- 'label' => $item['request']['label'],
- 'httpCode' => $httpCode,
- 'totalTime' => $totalTime,
- 'businessSuccess' => $parsed['success'],
- 'summary' => $parsed['summary'],
- ];
- curl_multi_remove_handle($multi, $ch);
- curl_close($ch);
- }
- curl_multi_close($multi);
- return $responses;
- }
- function parseBusinessResponse($body, $httpCode, $curlError, $requestType)
- {
- if ($curlError !== '') {
- return ['success' => false, 'summary' => 'curl_error=' . $curlError];
- }
- $decoded = json_decode($body, true);
- if (is_array($decoded)) {
- $code = isset($decoded['code']) ? $decoded['code'] : null;
- $msg = isset($decoded['msg']) ? $decoded['msg'] : '';
- $data = isset($decoded['data']) && is_array($decoded['data']) ? $decoded['data'] : [];
- $respondType = isset($data['respondType']) ? (string)$data['respondType'] : '';
- $success = ($httpCode >= 200 && $httpCode < 300 && (string)$code === '1');
- if ($requestType === 'order') {
- $hasOrderIdentity = !empty($data['id']) || !empty($data['orderSn']);
- if ($respondType === 'priceError' || !$hasOrderIdentity) {
- $success = false;
- }
- }
- $extra = [];
- if ($respondType !== '') {
- $extra[] = 'respondType=' . $respondType;
- }
- if (!empty($data['id'])) {
- $extra[] = 'id=' . $data['id'];
- }
- if (!empty($data['orderSn'])) {
- $extra[] = 'orderSn=' . $data['orderSn'];
- }
- return [
- 'success' => $success,
- 'summary' => trimText('code=' . json_encode($code, JSON_UNESCAPED_UNICODE) . ' msg=' . (string)$msg . (!empty($extra) ? ' ' . implode(' ', $extra) : ''), 240),
- ];
- }
- return [
- 'success' => false,
- 'summary' => trimText(preg_replace('/\s+/', ' ', (string)$body), 240),
- ];
- }
- function countSuccessfulRequests($responses)
- {
- $counts = [];
- foreach ($responses as $response) {
- if (!isset($counts[$response['type']])) {
- $counts[$response['type']] = 0;
- }
- if ($response['businessSuccess']) {
- $counts[$response['type']]++;
- }
- }
- return $counts;
- }
- function calcExpectedStocks($before, $requestDefs, $successCounts, $productIds)
- {
- $expected = $before;
- foreach ($successCounts as $type => $count) {
- foreach ($productIds as $productId) {
- $delta = isset($requestDefs[$type]['stockDelta'][$productId])
- ? $requestDefs[$type]['stockDelta'][$productId]
- : '0';
- if ($count > 0 && bccomp($delta, '0', 2) !== 0) {
- $expected[$productId] = bcsub($expected[$productId], bcmul($delta, (string)$count, 2), 2);
- }
- }
- }
- return $expected;
- }
- function stocksMatch($expected, $actual, $productIds)
- {
- foreach ($productIds as $productId) {
- if (bccomp((string)$expected[$productId], (string)$actual[$productId], 2) !== 0) {
- return false;
- }
- }
- return true;
- }
- function printStocks($title, $stocks, $productIds)
- {
- echo $title . ': ';
- $parts = [];
- foreach ($productIds as $productId) {
- $parts[] = $productId . '=' . $stocks[$productId];
- }
- echo implode(', ', $parts) . "\n";
- }
- function printDiff($expected, $actual, $productIds)
- {
- echo 'Diff(actual - expected): ';
- $parts = [];
- foreach ($productIds as $productId) {
- $parts[] = $productId . '=' . bcsub((string)$actual[$productId], (string)$expected[$productId], 2);
- }
- echo implode(', ', $parts) . "\n";
- }
- function trimText($text, $maxLength)
- {
- $text = trim($text);
- if (function_exists('mb_strlen') && function_exists('mb_substr')) {
- return mb_strlen($text, 'UTF-8') > $maxLength
- ? mb_substr($text, 0, $maxLength, 'UTF-8') . '...'
- : $text;
- }
- return strlen($text) > $maxLength ? substr($text, 0, $maxLength) . '...' : $text;
- }
- function getFileSize($file)
- {
- clearstatcache(true, $file);
- return is_file($file) ? filesize($file) : 0;
- }
- function getLogOffsets($files)
- {
- $offsets = [];
- foreach ($files as $name => $file) {
- $offsets[$name] = getFileSize($file);
- }
- return $offsets;
- }
- function findConcurrencyErrorsFromLogs($files, $offsets)
- {
- $errors = [];
- foreach ($files as $name => $file) {
- $offset = isset($offsets[$name]) ? $offsets[$name] : 0;
- foreach (findConcurrencyErrors(readFileFromOffset($file, $offset)) as $error) {
- $errors[] = $name . ': ' . $error;
- }
- }
- return $errors;
- }
- function readFileFromOffset($file, $offset)
- {
- if (!is_file($file)) {
- return '';
- }
- $handle = fopen($file, 'rb');
- if (!$handle) {
- return '';
- }
- fseek($handle, $offset);
- $content = stream_get_contents($handle);
- fclose($handle);
- return $content === false ? '' : $content;
- }
- function findConcurrencyErrors($logContent)
- {
- if ($logContent === '') {
- return [];
- }
- $patterns = [
- 'Deadlock found',
- 'Serialization failure',
- 'Lock wait timeout exceeded',
- ];
- $lines = preg_split('/\r\n|\r|\n/', $logContent);
- $matched = [];
- foreach ($lines as $line) {
- foreach ($patterns as $pattern) {
- if (strpos($line, $pattern) !== false) {
- $matched[] = trimText(trim($line), 240);
- break;
- }
- }
- }
- return array_values(array_unique(array_filter($matched)));
- }
|