feat: 添加阿里云短信服务支持并优化验证码逻辑
- 引入 alibabacloud/dysmsapi-20170525 依赖,集成阿里云短信服务 - 将短信验证码从6位改为4位,优化用户体验 - 添加聚水潭商品按时间范围同步功能,支持分页拉取 - 增强Cron任务异常处理,添加错误日志记录 - 修复系统附件服务中图片路径处理逻辑 - 更新阿里云SDK相关组件至最新版本 - 添加think-trace调试扩展,便于问题排查 - 优化跨域中间件,使用str_contains替代strpos
This commit is contained in:
@@ -15,12 +15,14 @@ abstract class HaichaoBridge
|
||||
'shopId' => '',
|
||||
];
|
||||
|
||||
private string $tokenFile = 'runtime/jstdata/jst_token_data.json';
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->api['appid'] = env('JST_APP_KEY', '');
|
||||
$this->api['appSecret'] = env('JST_APP_SECRET', '');
|
||||
$this->api['api'] = env('JST_API', 'https://openapi.jushuitan.com/');
|
||||
$this->api['access_token'] = env('JST_ACCESS_TOKEN', '');
|
||||
$this->api['access_token'] = $this->getToken();
|
||||
$this->api['shopId'] = (string)env('JST_SHOP_ID', '');
|
||||
}
|
||||
|
||||
@@ -29,18 +31,46 @@ abstract class HaichaoBridge
|
||||
return $this->api['shopId'];
|
||||
}
|
||||
|
||||
/**
|
||||
* 聚水潭开放平台请求封装
|
||||
* @param string $api
|
||||
* @param array $data
|
||||
* @param array $addonBody
|
||||
* @return array
|
||||
*/
|
||||
/**
|
||||
* 聚水潭开放平台请求封装
|
||||
* @param string $api
|
||||
* @param array $data
|
||||
* @param array $addonBody
|
||||
* @return array
|
||||
*/
|
||||
protected function post(string $api, array $data = [], array $addonBody = []): array
|
||||
{
|
||||
return $this->postInternal($api, $data, $addonBody);
|
||||
}
|
||||
|
||||
private function postInternal(string $api, array $data = [], array $addonBody = [], int $attempt = 0, bool $tokenRefreshed = false): array
|
||||
{
|
||||
if (!$this->api['appid'] || !$this->api['appSecret'] || !$this->api['access_token']) {
|
||||
throw new ValidateException('JST config missing');
|
||||
}
|
||||
$apiName = ltrim($api, '/');
|
||||
if ($apiName === 'open/inventory/query') {
|
||||
$skuIdsRaw = $data['sku_ids'] ?? '';
|
||||
$skuIdsList = [];
|
||||
if (is_array($skuIdsRaw)) {
|
||||
$skuIdsList = array_values(array_filter(array_map('trim', $skuIdsRaw), static fn($v) => $v !== ''));
|
||||
$skuIdsRaw = implode(',', $skuIdsList);
|
||||
} else {
|
||||
$skuIdsRaw = (string)$skuIdsRaw;
|
||||
$skuIdsList = array_values(array_filter(array_map('trim', explode(',', trim($skuIdsRaw, ", \t\n\r\0\x0B"))), static fn($v) => $v !== ''));
|
||||
}
|
||||
Log::info('jst.api.inventory.query.request ' . json_encode([
|
||||
'api' => $apiName,
|
||||
'attempt' => $attempt,
|
||||
'page_index' => $data['page_index'] ?? null,
|
||||
'page_num' => $data['page_num'] ?? null,
|
||||
'page_size' => $data['page_size'] ?? null,
|
||||
'sku_ids_count' => count($skuIdsList),
|
||||
'sku_ids' => $skuIdsList,
|
||||
'sku_ids_raw' => $skuIdsRaw,
|
||||
], JSON_UNESCAPED_UNICODE));
|
||||
}
|
||||
$this->throttle();
|
||||
$time = time();
|
||||
$baseBody = [
|
||||
'access_token' => $this->api['access_token'],
|
||||
@@ -58,24 +88,90 @@ abstract class HaichaoBridge
|
||||
'header' => 'Content-Type: application/json;charset=utf-8',
|
||||
'content' => http_build_query($baseBody)
|
||||
];
|
||||
$rt = json_decode(file_get_contents("{$this->api['api']}$api", false, stream_context_create($options)), true);
|
||||
$raw = file_get_contents("{$this->api['api']}$api", false, stream_context_create($options));
|
||||
$rt = json_decode((string)$raw, true);
|
||||
$respPreview = null;
|
||||
try {
|
||||
$respPreview = mb_substr((string)$raw, 0, 2000);
|
||||
} catch (\Throwable $e) {
|
||||
}
|
||||
Log::info('jst.api.response ' . json_encode([
|
||||
'api' => $api,
|
||||
'code' => $rt['code'] ?? null,
|
||||
'msg' => $rt['msg'] ?? null,
|
||||
'preview' => $respPreview,
|
||||
], JSON_UNESCAPED_UNICODE));
|
||||
if (!is_array($rt) || !array_key_exists('code', $rt)) {
|
||||
Log::error('JST response invalid ' . json_encode([
|
||||
'api' => $api,
|
||||
], JSON_UNESCAPED_UNICODE));
|
||||
Log::error('JST response invalid ' . json_encode([
|
||||
'api' => $api,
|
||||
], JSON_UNESCAPED_UNICODE));
|
||||
throw new ValidateException('JST response invalid');
|
||||
}
|
||||
if ($rt['code'] != 0) {
|
||||
Log::error('JST request failed ' . json_encode([
|
||||
'api' => $api,
|
||||
'code' => $rt['code'],
|
||||
'msg' => (string)($rt['msg'] ?? ''),
|
||||
], JSON_UNESCAPED_UNICODE));
|
||||
$code = (int)$rt['code'];
|
||||
if (!$tokenRefreshed && $this->isTokenError($rt)) {
|
||||
$this->api['access_token'] = $this->getToken(true);
|
||||
return $this->postInternal($api, $data, $addonBody, $attempt + 1, true);
|
||||
}
|
||||
if ($this->isRateLimitError($code) && $attempt < 6) {
|
||||
$sleepMs = $this->getRateLimitSleepMs($code, $attempt);
|
||||
usleep($sleepMs * 1000);
|
||||
return $this->postInternal($api, $data, $addonBody, $attempt + 1, $tokenRefreshed);
|
||||
}
|
||||
Log::error('JST request failed ' . json_encode([
|
||||
'api' => $api,
|
||||
'code' => $code,
|
||||
'msg' => (string)($rt['msg'] ?? ''),
|
||||
], JSON_UNESCAPED_UNICODE));
|
||||
throw new ValidateException($rt['msg'] ?? 'JST error');
|
||||
}
|
||||
return $rt;
|
||||
}
|
||||
|
||||
private function throttle(): void
|
||||
{
|
||||
static $lastAt = 0.0;
|
||||
static $recent = [];
|
||||
|
||||
$minGap = 0.65;
|
||||
while (true) {
|
||||
$now = microtime(true);
|
||||
$gap = $now - $lastAt;
|
||||
if ($gap < $minGap) {
|
||||
usleep((int)(($minGap - $gap) * 1000000));
|
||||
continue;
|
||||
}
|
||||
|
||||
$recent = array_values(array_filter($recent, static fn($t) => ($now - $t) < 60));
|
||||
if (count($recent) >= 95) {
|
||||
$oldest = $recent[0] ?? $now;
|
||||
$wait = 60 - ($now - $oldest) + 0.2;
|
||||
if ($wait > 0) {
|
||||
usleep((int)($wait * 1000000));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$lastAt = microtime(true);
|
||||
$recent[] = $lastAt;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private function isRateLimitError(int $code): bool
|
||||
{
|
||||
return $code === 199 || $code === 200;
|
||||
}
|
||||
|
||||
private function getRateLimitSleepMs(int $code, int $attempt): int
|
||||
{
|
||||
$jitter = random_int(0, 300);
|
||||
if ($code === 200) {
|
||||
return min(60000, (int)(6000 * (1 + $attempt)) + $jitter);
|
||||
}
|
||||
return min(10000, (int)(900 * (1 + $attempt)) + $jitter);
|
||||
}
|
||||
|
||||
private function sign(array $data): string
|
||||
{
|
||||
$computerStr = "";
|
||||
@@ -85,4 +181,167 @@ abstract class HaichaoBridge
|
||||
$computerStr = $this->api['appSecret'] . $computerStr;
|
||||
return md5($computerStr);
|
||||
}
|
||||
|
||||
private function isTokenError(array $rt): bool
|
||||
{
|
||||
$msg = (string)($rt['msg'] ?? '');
|
||||
if (!$msg) return false;
|
||||
$msgLower = strtolower($msg);
|
||||
return strpos($msgLower, 'access_token') !== false || strpos($msgLower, 'token') !== false;
|
||||
}
|
||||
|
||||
private function getToken(bool $forceRefresh = false): string
|
||||
{
|
||||
$tokenInfo = $this->readTokenInfo();
|
||||
if ($tokenInfo === null) {
|
||||
$tokenInfo = $this->autoInitTokenFile();
|
||||
if ($tokenInfo === null) {
|
||||
throw new ValidateException('JST token missing, please check .env JST_ACCESS_TOKEN config');
|
||||
}
|
||||
}
|
||||
if (!$forceRefresh && $tokenInfo) {
|
||||
$accessToken = (string)($tokenInfo['access_token'] ?? '');
|
||||
$expiresIn = (int)($tokenInfo['expires_in'] ?? 0);
|
||||
$tokenTime = (int)($tokenInfo['time'] ?? 0);
|
||||
if ($accessToken && $expiresIn > 0 && $tokenTime > 0) {
|
||||
if (time() < ($tokenTime + $expiresIn - 3600)) {
|
||||
return $accessToken;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$refreshToken = (string)($tokenInfo['refresh_token'] ?? '');
|
||||
if (!$refreshToken) {
|
||||
$refreshToken = trim((string)env('JST_REFRESH_TOKEN', ''));
|
||||
}
|
||||
if ($refreshToken) {
|
||||
$newTokenInfo = $this->refreshAccessToken($refreshToken);
|
||||
$this->writeTokenInfo($newTokenInfo);
|
||||
return (string)$newTokenInfo['access_token'];
|
||||
}
|
||||
|
||||
$code = (string)env('JST_AUTH_CODE', '');
|
||||
if ($code) {
|
||||
$newTokenInfo = $this->code2accessToken($code);
|
||||
$this->writeTokenInfo($newTokenInfo);
|
||||
return (string)$newTokenInfo['access_token'];
|
||||
}
|
||||
|
||||
throw new ValidateException('JST token missing');
|
||||
}
|
||||
|
||||
private function autoInitTokenFile(): ?array
|
||||
{
|
||||
$accessToken = (string)env('JST_ACCESS_TOKEN', '');
|
||||
$refreshToken = (string)env('JST_REFRESH_TOKEN', '');
|
||||
$expiresIn = (int)env('JST_EXPIRES_IN', 2592000);
|
||||
|
||||
if (empty($accessToken)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$tokenInfo = [
|
||||
'access_token' => $accessToken,
|
||||
'expires_in' => $expiresIn,
|
||||
'refresh_token' => $refreshToken,
|
||||
'time' => time(),
|
||||
];
|
||||
|
||||
$this->writeTokenInfo($tokenInfo);
|
||||
return $tokenInfo;
|
||||
}
|
||||
|
||||
private function refreshAccessToken(string $refreshToken): array
|
||||
{
|
||||
$params = [];
|
||||
$params['app_key'] = $this->api['appid'];
|
||||
$params['timestamp'] = time();
|
||||
$params['grant_type'] = 'refresh_token';
|
||||
$params['charset'] = 'utf-8';
|
||||
$params['refresh_token'] = $refreshToken;
|
||||
$params['scope'] = 'all';
|
||||
$params['sign'] = $this->sign($params);
|
||||
|
||||
$url = "{$this->api['api']}openWeb/auth/refreshToken";
|
||||
$raw = file_get_contents($url, false, stream_context_create([
|
||||
'http' => [
|
||||
'timeout' => 60,
|
||||
'method' => 'POST',
|
||||
'header' => 'Content-Type: application/x-www-form-urlencoded;charset=UTF-8',
|
||||
'content' => http_build_query($params),
|
||||
],
|
||||
]));
|
||||
$rt = json_decode((string)$raw, true);
|
||||
if (!is_array($rt) || !array_key_exists('code', $rt)) {
|
||||
throw new ValidateException('JST response invalid');
|
||||
}
|
||||
if ((int)$rt['code'] !== 0) {
|
||||
throw new ValidateException((string)($rt['msg'] ?? 'JST error'));
|
||||
}
|
||||
$data = $rt['data'] ?? [];
|
||||
if (!is_array($data) || empty($data['access_token'])) {
|
||||
throw new ValidateException('JST token invalid');
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
private function code2accessToken(string $code): array
|
||||
{
|
||||
$params = [];
|
||||
$params['app_key'] = $this->api['appid'];
|
||||
$params['timestamp'] = time();
|
||||
$params['grant_type'] = 'authorization_code';
|
||||
$params['charset'] = 'utf-8';
|
||||
$params['code'] = $code;
|
||||
$params['sign'] = $this->sign($params);
|
||||
|
||||
$url = "{$this->api['api']}openWeb/auth/accessToken";
|
||||
$raw = file_get_contents($url, false, stream_context_create([
|
||||
'http' => [
|
||||
'timeout' => 60,
|
||||
'method' => 'POST',
|
||||
'header' => 'Content-Type: application/x-www-form-urlencoded;charset=UTF-8',
|
||||
'content' => http_build_query($params),
|
||||
],
|
||||
]));
|
||||
$rt = json_decode((string)$raw, true);
|
||||
if (!is_array($rt) || !array_key_exists('code', $rt)) {
|
||||
throw new ValidateException('JST response invalid');
|
||||
}
|
||||
if ((int)$rt['code'] !== 0) {
|
||||
throw new ValidateException((string)($rt['msg'] ?? 'JST error'));
|
||||
}
|
||||
$data = $rt['data'] ?? [];
|
||||
if (!is_array($data) || empty($data['access_token'])) {
|
||||
throw new ValidateException('JST token invalid');
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
private function readTokenInfo(): ?array
|
||||
{
|
||||
$file = root_path() . $this->tokenFile;
|
||||
if (!is_file($file)) {
|
||||
Log::info('JST token file not found, will try to auto init from .env');
|
||||
return null;
|
||||
}
|
||||
$raw = file_get_contents($file);
|
||||
if (!$raw) return null;
|
||||
$arr = json_decode((string)$raw, true);
|
||||
if (!is_array($arr) || empty($arr['access_token'])) {
|
||||
Log::warning('JST token file is invalid, will try to auto init from .env');
|
||||
return null;
|
||||
}
|
||||
return $arr;
|
||||
}
|
||||
|
||||
private function writeTokenInfo(array $tokenInfo): void
|
||||
{
|
||||
$dir = root_path() . 'runtime/jstdata';
|
||||
if (!is_dir($dir)) {
|
||||
mkdir($dir, 0777, true);
|
||||
}
|
||||
$tokenInfo['time'] = time();
|
||||
file_put_contents(root_path() . $this->tokenFile, json_encode($tokenInfo, JSON_UNESCAPED_UNICODE));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,6 +135,12 @@ class HaichaoJstServices extends HaichaoBridge
|
||||
}
|
||||
}
|
||||
}
|
||||
Log::info('jst.orders_upload.request ' . json_encode([
|
||||
'api' => 'open/jushuitan/orders/upload',
|
||||
'oid' => $oid,
|
||||
'shop_status' => $status,
|
||||
'biz' => $biz,
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||||
$num = 3;
|
||||
for ($i = $num; $i >= 0; $i--) {
|
||||
if ($i <= 0) {
|
||||
@@ -652,6 +658,26 @@ class HaichaoJstServices extends HaichaoBridge
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按时间范围获取聚水潭商品列表
|
||||
* @param string $startTime 开始时间 Y-m-d H:i:s
|
||||
* @param string $endTime 结束时间 Y-m-d H:i:s
|
||||
* @param int $pageIndex 页码
|
||||
* @param int $pageSize 每页数量
|
||||
* @return array
|
||||
*/
|
||||
public function getProductListByTime(string $startTime, string $endTime, int $pageIndex = 1, int $pageSize = 50): array
|
||||
{
|
||||
$data = $this->post('open/mall/item/query', [
|
||||
'modified_begin' => $startTime,
|
||||
'modified_end' => $endTime,
|
||||
'page_index' => $pageIndex,
|
||||
'page_size' => $pageSize,
|
||||
'date_field' => '',
|
||||
])['data'];
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function getProductListStand(): array
|
||||
{
|
||||
$time = 10;
|
||||
@@ -704,6 +730,94 @@ class HaichaoJstServices extends HaichaoBridge
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function uploadItemSku(array $items): array
|
||||
{
|
||||
return $this->post('open/jushuitan/itemsku/upload', [
|
||||
'items' => $items,
|
||||
]);
|
||||
}
|
||||
|
||||
public function uploadItemSkuByProductIds(array $productIds): array
|
||||
{
|
||||
$productIds = array_values(array_unique(array_map('intval', $productIds)));
|
||||
if (!$productIds) {
|
||||
throw new ValidateException('请选择商品');
|
||||
}
|
||||
|
||||
$products = \app\model\product\product\StoreProduct::whereIn('id', $productIds)
|
||||
->where('is_del', 0)
|
||||
->with(['attrValue'])
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
$items = [];
|
||||
$errors = [];
|
||||
foreach ($products as $product) {
|
||||
$iId = trim((string)($product['code'] ?? ''));
|
||||
$productId = (int)($product['id'] ?? 0);
|
||||
if (!$iId) {
|
||||
$errors[] = [
|
||||
'product_id' => $productId,
|
||||
'msg' => '商品编码(code)为空',
|
||||
];
|
||||
continue;
|
||||
}
|
||||
|
||||
$skuList = $product['attr_value'] ?? ($product['attrValue'] ?? []);
|
||||
if (!is_array($skuList) || !$skuList) {
|
||||
$errors[] = [
|
||||
'product_id' => $productId,
|
||||
'i_id' => $iId,
|
||||
'msg' => '商品规格为空',
|
||||
];
|
||||
continue;
|
||||
}
|
||||
|
||||
$productImage = (string)($product['image'] ?? '');
|
||||
$productName = (string)($product['store_name'] ?? '');
|
||||
$enabled = ((int)($product['is_show'] ?? 0) === 1) ? 1 : -1;
|
||||
foreach ($skuList as $sku) {
|
||||
$skuId = trim((string)($sku['code'] ?? ''));
|
||||
if (!$skuId) {
|
||||
$errors[] = [
|
||||
'product_id' => $productId,
|
||||
'i_id' => $iId,
|
||||
'msg' => '规格编码(code)为空',
|
||||
];
|
||||
continue;
|
||||
}
|
||||
$skuImage = (string)($sku['image'] ?? $productImage);
|
||||
$propertiesValue = (string)($sku['suk'] ?? '');
|
||||
$propertiesValue = str_replace(',', ';', $propertiesValue);
|
||||
|
||||
$items[] = [
|
||||
'i_id' => $iId,
|
||||
'sku_id' => $skuId,
|
||||
'name' => $productName,
|
||||
'properties_value' => $propertiesValue,
|
||||
's_price' => (float)($sku['price'] ?? ($product['price'] ?? 0)),
|
||||
'market_price' => (float)($sku['ot_price'] ?? ($product['ot_price'] ?? 0)),
|
||||
'c_price' => (float)($sku['cost'] ?? ($product['cost'] ?? 0)),
|
||||
'pic' => $productImage,
|
||||
'pic_big' => $productImage,
|
||||
'sku_pic' => $skuImage,
|
||||
'enabled' => $enabled,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (!$items) {
|
||||
throw new ValidateException('没有可同步的商品规格');
|
||||
}
|
||||
|
||||
$rt = $this->uploadItemSku($items);
|
||||
return [
|
||||
'upload_count' => count($items),
|
||||
'errors' => $errors,
|
||||
'jst' => $rt,
|
||||
];
|
||||
}
|
||||
|
||||
public function getStork(string $ids): array
|
||||
{
|
||||
return $this->post('open/inventory/query', [
|
||||
@@ -758,28 +872,42 @@ class HaichaoJstServices extends HaichaoBridge
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function getInneropenStockAndDetailList($styleCodes): array
|
||||
public function getInneropenStockAndDetailList(array|string $styleCodes, ?array &$stats = null): array
|
||||
{
|
||||
$rt = [];
|
||||
$i = 1;
|
||||
do {
|
||||
$result = $this->post('open/inventory/query', [
|
||||
'page_index' => $i++,
|
||||
'page_size' => 100,
|
||||
'sku_ids' => $styleCodes
|
||||
])['data']['inventorys'] ?? [];
|
||||
|
||||
// 适配原来的返回格式结构
|
||||
$formatResult = [];
|
||||
foreach ($result as $item) {
|
||||
$formatResult[] = [
|
||||
'item_code' => $item['sku_id'],
|
||||
'stock' => max(0, ($item['qty'] ?? 0) - ($item['order_lock'] ?? 0)) // 实际可用库存
|
||||
];
|
||||
$codeList = is_array($styleCodes) ? $styleCodes : explode(',', trim($styleCodes, ", \t\n\r\0\x0B"));
|
||||
$codeList = array_values(array_filter(array_map('trim', $codeList), static fn($v) => $v !== ''));
|
||||
|
||||
foreach (array_chunk($codeList, 100) as $chunkCodes) {
|
||||
if ($stats !== null) {
|
||||
$stats['chunk_count'] = (int)($stats['chunk_count'] ?? 0) + 1;
|
||||
$stats['request_sku_count'] = (int)($stats['request_sku_count'] ?? 0) + count($chunkCodes);
|
||||
}
|
||||
$rt = array_merge($formatResult, $rt);
|
||||
usleep(550);
|
||||
} while (count($result) == 100);
|
||||
$i = 1;
|
||||
do {
|
||||
if ($stats !== null) {
|
||||
$stats['api_call_count'] = (int)($stats['api_call_count'] ?? 0) + 1;
|
||||
}
|
||||
$result = $this->post('open/inventory/query', [
|
||||
'page_index' => $i++,
|
||||
'page_size' => 100,
|
||||
'sku_ids' => implode(',', $chunkCodes)
|
||||
])['data']['inventorys'] ?? [];
|
||||
if ($stats !== null) {
|
||||
$stats['api_resp_row_count'] = (int)($stats['api_resp_row_count'] ?? 0) + count($result);
|
||||
}
|
||||
|
||||
$formatResult = [];
|
||||
foreach ($result as $item) {
|
||||
$formatResult[] = [
|
||||
'item_code' => $item['sku_id'],
|
||||
'stock' => max(0, ($item['qty'] ?? 0) - ($item['order_lock'] ?? 0))
|
||||
];
|
||||
}
|
||||
$rt = array_merge($formatResult, $rt);
|
||||
usleep(550000);
|
||||
} while (count($result) == 100);
|
||||
}
|
||||
return $rt;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,31 +13,82 @@ class JstProductEvent implements JstEvent
|
||||
{
|
||||
try {
|
||||
$param = $request->param();
|
||||
$body = json_decode((string)($param['biz'] ?? '{}'), true);
|
||||
$actionCode = (string)($param['action_code'] ?? '');
|
||||
$parseFrom = 'param.biz';
|
||||
$bizRaw = (string)($param['biz'] ?? '');
|
||||
$body = json_decode($bizRaw !== '' ? $bizRaw : '{}', true);
|
||||
if (!is_array($body)) {
|
||||
Log::warning('jst.cb.product_stock.parse_fail ' . json_encode([
|
||||
'action_code' => $actionCode,
|
||||
'from' => $parseFrom,
|
||||
'biz_raw_len' => strlen($bizRaw),
|
||||
], JSON_UNESCAPED_UNICODE));
|
||||
$body = [];
|
||||
}
|
||||
$skuId = (string)($body['skuId'] ?? '');
|
||||
$stock = (int)($body['qty'] ?? 0);
|
||||
if (!$skuId) {
|
||||
$cont = $request->getContent();
|
||||
$output = [];
|
||||
parse_str($cont, $output);
|
||||
$body = json_decode((string)($output['biz'] ?? '{}'), true);
|
||||
$actionCode = (string)($output['action_code'] ?? $actionCode);
|
||||
$parseFrom = 'content.parse_str.biz';
|
||||
$bizRaw = (string)($output['biz'] ?? '');
|
||||
$body = json_decode($bizRaw !== '' ? $bizRaw : '{}', true);
|
||||
if (!is_array($body)) {
|
||||
Log::warning('jst.cb.product_stock.parse_fail ' . json_encode([
|
||||
'action_code' => $actionCode,
|
||||
'from' => $parseFrom,
|
||||
'biz_raw_len' => strlen($bizRaw),
|
||||
'content_len' => strlen((string)$cont),
|
||||
], JSON_UNESCAPED_UNICODE));
|
||||
$body = [];
|
||||
}
|
||||
$skuId = (string)($body['skuId'] ?? '');
|
||||
$stock = (int)($body['qty'] ?? 0);
|
||||
}
|
||||
if (!$skuId) return [];
|
||||
if (!$skuId) {
|
||||
Log::warning('jst.cb.product_stock.miss ' . json_encode([
|
||||
'action_code' => $actionCode,
|
||||
'from' => $parseFrom,
|
||||
'sku_id' => $skuId,
|
||||
], JSON_UNESCAPED_UNICODE));
|
||||
return [];
|
||||
}
|
||||
/** @var StoreProductAttrValueServices $spas */
|
||||
$spas = app()->make(StoreProductAttrValueServices::class);
|
||||
$model = $spas->getOne(['code' => $skuId]);
|
||||
if ($model) {
|
||||
$pdt = $model->toArray();
|
||||
$id = $pdt['id'] ?? 0;
|
||||
if ($id) {
|
||||
$spas->update($id, [
|
||||
if (!$model) {
|
||||
Log::warning('jst.cb.product_stock.not_found ' . json_encode([
|
||||
'action_code' => $actionCode,
|
||||
'from' => $parseFrom,
|
||||
'sku_id' => $skuId,
|
||||
'stock' => $stock,
|
||||
'sum_stock' => $stock
|
||||
]);
|
||||
}
|
||||
], JSON_UNESCAPED_UNICODE));
|
||||
return [];
|
||||
}
|
||||
$pdt = $model->toArray();
|
||||
$id = (int)($pdt['id'] ?? 0);
|
||||
if (!$id) {
|
||||
Log::warning('jst.cb.product_stock.not_found ' . json_encode([
|
||||
'action_code' => $actionCode,
|
||||
'from' => $parseFrom,
|
||||
'sku_id' => $skuId,
|
||||
'stock' => $stock,
|
||||
], JSON_UNESCAPED_UNICODE));
|
||||
return [];
|
||||
}
|
||||
$spas->update($id, [
|
||||
'stock' => $stock,
|
||||
'sum_stock' => $stock
|
||||
]);
|
||||
Log::info('jst.cb.product_stock.updated ' . json_encode([
|
||||
'action_code' => $actionCode,
|
||||
'from' => $parseFrom,
|
||||
'attr_value_id' => $id,
|
||||
'sku_id' => $skuId,
|
||||
'stock' => $stock,
|
||||
], JSON_UNESCAPED_UNICODE));
|
||||
} catch (Throwable $e) {
|
||||
Log::error('jst.cb.product_stock.error ' . json_encode([
|
||||
'msg' => $e->getMessage(),
|
||||
|
||||
@@ -157,6 +157,7 @@ class JstProductSync extends HaichaoJstServices
|
||||
} else {
|
||||
$productServices->update($pidNow, $productInfo);
|
||||
}
|
||||
$productServices->ensureSpu((int)$pidNow);
|
||||
ProductStockTips::dispatch([$pidNow, 0]);
|
||||
$attr = [[
|
||||
'value' => '规格',
|
||||
@@ -324,6 +325,7 @@ class JstProductSync extends HaichaoJstServices
|
||||
} else {
|
||||
$productServices->update($pid, $productInfo);
|
||||
}
|
||||
$productServices->ensureSpu((int)$pid);
|
||||
ProductStockTips::dispatch([$pid, 0]);
|
||||
$attr = [[
|
||||
'value' => '规格',
|
||||
|
||||
@@ -16,45 +16,76 @@ class JstStorkSync extends HaichaoJstServices
|
||||
{
|
||||
public function syncStorkFromInneropenErp(): void
|
||||
{
|
||||
$startAt = microtime(true);
|
||||
/** @var StoreProductAttrValueServices $storeProductAttrValueServices */
|
||||
$storeProductAttrValueServices = app()->make(StoreProductAttrValueServices::class);
|
||||
Log::info('jst.stork.sync.start');
|
||||
$inventorySkuIds = [];
|
||||
$skuIds = '';
|
||||
$index = 1;
|
||||
$count = 0;
|
||||
$codes = [];
|
||||
$spuIds = [];
|
||||
$codeIdMap = [];
|
||||
$codeProductIdMap = [];
|
||||
foreach ($storeProductAttrValueServices->getList(['is_del' => 0]) as $item) {
|
||||
if ($item['code']) {
|
||||
$skuIds .= $item['code'] . ',';
|
||||
$count++;
|
||||
$inventorySkuIds[$index] = [
|
||||
'length' => $count,
|
||||
'ids' => $skuIds,
|
||||
];
|
||||
if ($count >= 500) {
|
||||
$skuIds = '';
|
||||
$index++;
|
||||
$count = 0;
|
||||
$code = (string)($item['code'] ?? '');
|
||||
if ($code !== '') {
|
||||
$codes[] = $code;
|
||||
$id = (int)($item['id'] ?? 0);
|
||||
if ($id > 0) {
|
||||
$codeIdMap[$code] = $id;
|
||||
}
|
||||
$productId = (int)($item['product_id'] ?? 0);
|
||||
if ($productId > 0) {
|
||||
$spuIds[$productId] = 1;
|
||||
$codeProductIdMap[$code] = $productId;
|
||||
}
|
||||
}
|
||||
}
|
||||
$productStock = [];
|
||||
foreach ($inventorySkuIds as $inventorySkuId) {
|
||||
foreach ($this->getInneropenStockAndDetailList($inventorySkuId['ids']) as $product) {
|
||||
$productStock[] = [
|
||||
'code' => $product['item_code'],
|
||||
'stock' => $product['stock']
|
||||
];
|
||||
$codes = array_values(array_unique($codes));
|
||||
Log::info('jst.stork.sync.request_stats ' . json_encode([
|
||||
'spu_count' => count($spuIds),
|
||||
'sku_count' => count($codes),
|
||||
], JSON_UNESCAPED_UNICODE));
|
||||
|
||||
$pullStats = [];
|
||||
$productStockMap = [];
|
||||
foreach ($this->getInneropenStockAndDetailList($codes, $pullStats) as $product) {
|
||||
$productStockMap[(string)$product['item_code']] = (int)$product['stock'];
|
||||
}
|
||||
$respSpuIds = [];
|
||||
foreach (array_keys($productStockMap) as $code) {
|
||||
if (isset($codeProductIdMap[$code])) {
|
||||
$respSpuIds[$codeProductIdMap[$code]] = 1;
|
||||
}
|
||||
}
|
||||
Log::info('jst.stork.sync.merge ' . count($productStock));
|
||||
foreach ($productStock as $item) {
|
||||
$storeProductAttrValueServices->update(['code' => $item['code']], [
|
||||
'stock' => $item['stock'],
|
||||
'sum_stock' => $item['stock']
|
||||
Log::info('jst.stork.sync.response_stats ' . json_encode([
|
||||
'spu_count' => count($respSpuIds),
|
||||
'sku_count' => count($productStockMap),
|
||||
'api_call_count' => (int)($pullStats['api_call_count'] ?? 0),
|
||||
'api_resp_row_count' => (int)($pullStats['api_resp_row_count'] ?? 0),
|
||||
], JSON_UNESCAPED_UNICODE));
|
||||
|
||||
$updatedSkuCount = 0;
|
||||
$notFoundSkuCount = 0;
|
||||
foreach ($productStockMap as $code => $stock) {
|
||||
$id = (int)($codeIdMap[$code] ?? 0);
|
||||
if ($id < 1) {
|
||||
$notFoundSkuCount++;
|
||||
continue;
|
||||
}
|
||||
$storeProductAttrValueServices->update($id, [
|
||||
'stock' => $stock,
|
||||
'sum_stock' => $stock
|
||||
]);
|
||||
$updatedSkuCount++;
|
||||
}
|
||||
Log::info('jst.stork.sync.end');
|
||||
Log::info('jst.stork.sync.finish ' . json_encode([
|
||||
'requested_spu_count' => count($spuIds),
|
||||
'requested_sku_count' => count($codes),
|
||||
'response_spu_count' => count($respSpuIds),
|
||||
'response_sku_count' => count($productStockMap),
|
||||
'update_sku_count' => $updatedSkuCount,
|
||||
'not_found_sku_count' => $notFoundSkuCount,
|
||||
'duration_ms' => (int)round((microtime(true) - $startAt) * 1000),
|
||||
], JSON_UNESCAPED_UNICODE));
|
||||
}
|
||||
|
||||
public function syncStockFromErp(array $ids): bool
|
||||
|
||||
Reference in New Issue
Block a user