feat: 添加阿里云短信服务支持并优化验证码逻辑

- 引入 alibabacloud/dysmsapi-20170525 依赖,集成阿里云短信服务
- 将短信验证码从6位改为4位,优化用户体验
- 添加聚水潭商品按时间范围同步功能,支持分页拉取
- 增强Cron任务异常处理,添加错误日志记录
- 修复系统附件服务中图片路径处理逻辑
- 更新阿里云SDK相关组件至最新版本
- 添加think-trace调试扩展,便于问题排查
- 优化跨域中间件,使用str_contains替代strpos
This commit is contained in:
2026-04-30 15:19:40 +08:00
parent 0334fef6c1
commit c7f85ffbda
565 changed files with 59742 additions and 3391 deletions

View File

@@ -8,6 +8,7 @@ use app\services\product\sku\StoreProductAttrValueServices;
use app\haichao\HaichaoJstServices;
use crmeb\basic\BaseJobs;
use crmeb\exceptions\AdminException;
use crmeb\services\erp\Erp as erpServices;
use crmeb\traits\QueueTrait;
use think\facade\Log;
@@ -42,7 +43,7 @@ class ProductSyncErp extends BaseJobs
}
foreach ($attrs as $item) {
if ($item['pic'] && strstr($item['pic'], 'http') === false) {
$siteUrl = sys_config('site_url');
$siteUrl = app()->make(\crmeb\services\SystemConfigService::class)->get('site_url');
$item['pic'] = $siteUrl . $item['pic'];
}
@@ -119,10 +120,18 @@ class ProductSyncErp extends BaseJobs
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
*/
public function productFromErp($spuArr, int $supplierId = 0)
public function productFromErp($spuArr, int $supplierId = 0, string $traceId = '')
{
$stats = [
'trace_id' => $traceId,
'fetched' => 0,
'created' => 0,
'updated' => 0,
'skipped' => 0,
'failed' => 0,
];
try {
Log::info('admin.manual.jst.product.sync.request', ['spu' => $spuArr]);
Log::info('admin.manual.jst.product.sync.request', ['trace_id' => $traceId, 'spu' => $spuArr, 'supplier_id' => $supplierId]);
$result = app()->make(HaichaoJstServices::class)->getProductListKuan($spuArr);
$resultPreview = null;
try {
@@ -130,6 +139,7 @@ class ProductSyncErp extends BaseJobs
} catch (\Throwable $e) {
}
Log::info('admin.manual.jst.product.sync.api_result', [
'trace_id' => $traceId,
'spu' => $spuArr,
'page_index' => $result['page_index'] ?? null,
'page_count' => $result['page_count'] ?? null,
@@ -137,178 +147,29 @@ class ProductSyncErp extends BaseJobs
'result_preview' => $resultPreview,
]);
$productList = $result['datas'] ?? [];
$productInfo = [];
$stats['fetched'] = is_array($productList) ? count($productList) : 0;
$stats = $this->mergeStats($stats, $this->processProductList($productList, $supplierId, $traceId, [
'mode' => 'spu',
'spu' => $spuArr,
]));
/** @var StoreProductServices $productServices */
$productServices = app()->make(StoreProductServices::class);
/** @var StoreProductAttrServices $productAttrServices */
$productAttrServices = app()->make(StoreProductAttrServices::class);
Log::info('admin.manual.jst.product.sync.response', [
'spu' => $spuArr,
'datas_count' => is_array($productList) ? count($productList) : 0
]);
$relationSupplierId = $supplierId > 0 ? $supplierId : 0;
foreach ($productList as $item) {
Log::info('admin.manual.jst.product.sync.item', [
'spu' => $spuArr,
'i_id' => $item['i_id'] ?? '',
'name' => $item['name'] ?? '',
'sku_count' => isset($item['skus']) && is_array($item['skus']) ? count($item['skus']) : 0
]);
$skuRows = isset($item['skus']) && is_array($item['skus']) ? $item['skus'] : [];
$salePrice = floatval($item['s_price'] ?? 0);
if ($salePrice <= 0 && $skuRows) {
$skuSalePrices = [];
foreach ($skuRows as $skuRow) {
if (isset($skuRow['sale_price']) && is_numeric($skuRow['sale_price'])) {
$skuSalePrices[] = floatval($skuRow['sale_price']);
}
}
if ($skuSalePrices) {
$salePrice = min($skuSalePrices);
}
}
$marketPrice = floatval($item['market_price'] ?? 0);
if ($marketPrice <= 0 && $skuRows) {
$skuMarketPrices = [];
foreach ($skuRows as $skuRow) {
if (isset($skuRow['market_price']) && is_numeric($skuRow['market_price'])) {
$skuMarketPrices[] = floatval($skuRow['market_price']);
}
}
if ($skuMarketPrices) {
$marketPrice = max($skuMarketPrices);
}
}
$costPrice = floatval($item['c_price'] ?? ($item['cost_price'] ?? 0));
if ($costPrice <= 0 && $skuRows) {
$skuCostPrices = [];
foreach ($skuRows as $skuRow) {
if (isset($skuRow['cost_price']) && is_numeric($skuRow['cost_price'])) {
$skuCostPrices[] = floatval($skuRow['cost_price']);
}
}
if ($skuCostPrices) {
$costPrice = max($skuCostPrices);
}
}
$pic = (string)($item['pic'] ?? ($skuRows[0]['pic'] ?? ''));
$name = (string)($item['name'] ?? ($skuRows[0]['name'] ?? ''));
$productInfo = [
'image' => $pic,
'slider_image' => json_encode([$pic]),
'store_name' => $name,
'store_info' => $name,
'cate_id' => 0,
'price' => $salePrice,
'ot_price' => $marketPrice,
'delivery_type' => '1,2,3',
'freight' => 1,
'is_show' => 0,
'add_time' => time(),
'cost' => $costPrice,
'ficti' => 0,
'spec_type' => 1,
'code' => $item['i_id'],
'type' => $relationSupplierId ? 2 : 0,
'relation_id' => $relationSupplierId,
];
$detail = $details = $value = [];
$usedSpecValues = [];
foreach ($skuRows as $skuIndex => $items) {
$specValue = trim((string)($items['properties_value'] ?? ''));
if ($specValue === '') {
$specValue = trim((string)($items['name'] ?? ''));
}
if ($specValue === '') {
$specValue = '规格' . ($skuIndex + 1);
}
if (isset($usedSpecValues[$specValue])) {
$specValue = $specValue . '-' . ($skuIndex + 1);
}
$usedSpecValues[$specValue] = 1;
$detail[] = $specValue;
$details[] = [
'name' => $specValue,
'select' => false
];
$skuCost = floatval($items['cost_price'] ?? $costPrice);
$skuSettle = $relationSupplierId ? $skuCost : 0.0;
$value[] = [
'bar_code' => '',
'brokerage' => 0,
'brokerage_two' => 0,
'code' => (string)($items['sku_id'] ?? ''),
'settle_price' => $skuSettle,
'cost' => $skuCost,
'detail' => ['规格' => $specValue],
'ot_price' => floatval($items['market_price'] ?? $marketPrice),
'pic' => (string)$items['pic'],
'price' => floatval($items['sale_price'] ?? $salePrice),
'select' => true,
'value1' => $specValue,
'values' => $specValue,
'vip_price' => 0,
'volume' => 0,
'weight' => 0,
'stock' => 0,
];
}
if (!$value) {
$specValue = '默认';
$detail[] = $specValue;
$details[] = [
'name' => $specValue,
'select' => false
];
$defCost = floatval($item['c_price'] ?? 0);
$defSettle = $relationSupplierId ? $defCost : 0.0;
$value[] = [
'bar_code' => '',
'brokerage' => 0,
'brokerage_two' => 0,
'code' => (string)($item['i_id'] ?? ''),
'settle_price' => $defSettle,
'cost' => $defCost,
'detail' => ['规格' => $specValue],
'ot_price' => floatval($item['market_price'] ?? 0),
'pic' => (string)($item['pic'] ?? ''),
'price' => floatval($item['s_price'] ?? 0),
'select' => true,
'value1' => $specValue,
'values' => $specValue,
'vip_price' => 0,
'volume' => 0,
'weight' => 0,
'stock' => 0,
];
}
$pid = $productServices->value(['code' => $item['i_id']], 'id');
$existed = !!$pid;
if (!$pid) {
$pid = $productServices->ErpProductSave($productInfo);
}
$attr = [[
'value' => '规格',
'detail' => $detail,
'details' => $details,
]];
$skuList = $productAttrServices->validateProductAttr($attr, $value, $pid, 0, 0, 0);
$productAttrServices->saveProductAttr($skuList, $pid);
Log::info('admin.manual.jst.product.sync.save', [
'spu' => $spuArr,
'product_id' => $pid,
'existed' => $existed,
'sku_saved_count' => isset($skuList['value']) && is_array($skuList['value']) ? count($skuList['value']) : 0
]);
}
//清除数据缓存
$productServices->cacheTag()->clear();
$productAttrServices->cacheTag()->clear();
Log::info('admin.manual.jst.product.sync.finish', ['spu' => $spuArr, 'count' => count($productList)]);
Log::info('admin.manual.jst.product.sync.finish', [
'trace_id' => $traceId,
'spu' => $spuArr,
'count' => is_array($productList) ? count($productList) : 0,
'created' => $stats['created'],
'updated' => $stats['updated'],
'skipped' => $stats['skipped'],
'failed' => $stats['failed'],
]);
return $stats;
} catch (\Exception $e) {
response_log_write([
@@ -316,11 +177,433 @@ class ProductSyncErp extends BaseJobs
'file' => $e->getFile(),
'line' => $e->getLine()
]);
Log::error('admin.manual.jst.product.sync.error', ['spu' => $spuArr, 'message' => $e->getMessage()]);
Log::error('admin.manual.jst.product.sync.error', ['trace_id' => $traceId, 'spu' => $spuArr, 'message' => $e->getMessage()]);
$stats['failed']++;
return $stats;
}
}
private function mergeStats(array $base, array $delta): array
{
foreach (['fetched', 'created', 'updated', 'skipped', 'failed'] as $key) {
$base[$key] = (int)($base[$key] ?? 0) + (int)($delta[$key] ?? 0);
}
return $base;
}
return true;
private function processProductList($productList, int $supplierId, string $traceId, array $context = []): array
{
$stats = [
'fetched' => 0,
'created' => 0,
'updated' => 0,
'skipped' => 0,
'failed' => 0,
];
if (!is_array($productList)) {
return $stats;
}
$stats['fetched'] = count($productList);
$seenCodes = [];
foreach ($productList as $item) {
if (!is_array($item)) {
$stats['skipped']++;
continue;
}
$code = (string)($item['i_id'] ?? '');
if ($code === '') {
$stats['skipped']++;
continue;
}
if (isset($seenCodes[$code])) {
$stats['skipped']++;
continue;
}
$seenCodes[$code] = 1;
try {
$save = $this->saveProductFromErpData($item, $supplierId);
if (!empty($save['created'])) {
$stats['created']++;
} else {
$stats['updated']++;
}
} catch (\Throwable $e) {
$stats['failed']++;
Log::error('admin.manual.jst.product.sync.item.error', [
'trace_id' => $traceId,
'i_id' => $code,
'message' => $e->getMessage(),
'context' => $context,
]);
}
}
return $stats;
}
/**
* 按时间范围同步商品
* @param string $startTime
* @param string $endTime
* @param int $supplierId
* @return bool
*/
public function syncProductByTime(string $startTime, string $endTime, int $supplierId = 0, string $traceId = '', string $mode = 'dispatch')
{
$startTs = strtotime($startTime);
$endTs = strtotime($endTime);
if ($startTs && $endTs) {
$maxSeconds = 7 * 86400;
if (($endTs - $startTs) > $maxSeconds) {
Log::warning('admin.manual.jst.product.sync_by_time.range_too_large', [
'trace_id' => $traceId,
'start_time' => $startTime,
'end_time' => $endTime,
'range_seconds' => $endTs - $startTs,
'max_seconds' => $maxSeconds,
'supplier_id' => $supplierId,
]);
return [
'ok' => false,
'error' => '时间范围不能超过7天',
'trace_id' => $traceId,
];
}
}
try {
$stats = [
'ok' => true,
'mode' => $mode,
'trace_id' => $traceId,
'start_time' => $startTime,
'end_time' => $endTime,
'page_count' => 1,
'pages_processed' => 0,
'fetched' => 0,
'created' => 0,
'updated' => 0,
'skipped' => 0,
'failed' => 0,
];
Log::info('admin.manual.jst.product.sync_by_time.start', [
'trace_id' => $traceId,
'start_time' => $startTime,
'end_time' => $endTime,
'supplier_id' => $supplierId
]);
$jstService = app()->make(HaichaoJstServices::class);
$result = $jstService->getProductListByTime($startTime, $endTime, 1, 50);
$pageCount = $result['page_count'] ?? 1;
$stats['page_count'] = (int)$pageCount;
Log::info('admin.manual.jst.product.sync_by_time.api_result', [
'trace_id' => $traceId,
'page' => 1,
'page_count' => $pageCount,
'datas_count' => isset($result['datas']) && is_array($result['datas']) ? count($result['datas']) : 0,
]);
$pageStats = $this->processProductList($result['datas'] ?? [], $supplierId, $traceId, [
'mode' => 'time',
'page' => 1,
]);
$stats = $this->mergeStats($stats, $pageStats);
$stats['pages_processed']++;
if ($pageCount > 1) {
if ($mode === 'sync') {
for ($i = 2; $i <= $pageCount; $i++) {
$pageResult = $jstService->getProductListByTime($startTime, $endTime, $i, 50);
$productList = $pageResult['datas'] ?? [];
$resultPreview = null;
try {
$resultPreview = mb_substr(json_encode($pageResult, JSON_UNESCAPED_UNICODE), 0, 2000);
} catch (\Throwable $e) {
}
Log::info('admin.manual.jst.product.sync_page.api_result', [
'trace_id' => $traceId,
'page' => $i,
'page_count' => $pageResult['page_count'] ?? null,
'datas_count' => is_array($productList) ? count($productList) : 0,
'result_preview' => $resultPreview,
]);
$stats = $this->mergeStats($stats, $this->processProductList($productList, $supplierId, $traceId, [
'mode' => 'time',
'page' => $i,
]));
$stats['pages_processed']++;
}
} else {
for ($i = 2; $i <= $pageCount; $i++) {
self::dispatchDo('syncProductPage', [$startTime, $endTime, $i, $supplierId, $traceId]);
}
}
}
/** @var StoreProductServices $productServices */
$productServices = app()->make(StoreProductServices::class);
/** @var StoreProductAttrServices $productAttrServices */
$productAttrServices = app()->make(StoreProductAttrServices::class);
$productServices->cacheTag()->clear();
$productAttrServices->cacheTag()->clear();
Log::info('admin.manual.jst.product.sync_by_time.finish', [
'trace_id' => $traceId,
'start_time' => $startTime,
'end_time' => $endTime,
'supplier_id' => $supplierId,
'page_count' => $stats['page_count'],
'pages_processed' => $stats['pages_processed'],
'fetched' => $stats['fetched'],
'created' => $stats['created'],
'updated' => $stats['updated'],
'skipped' => $stats['skipped'],
'failed' => $stats['failed'],
]);
return $stats;
} catch (\Exception $e) {
Log::error('admin.manual.jst.product.sync_by_time.error', [
'trace_id' => $traceId,
'message' => $e->getMessage(),
'start_time' => $startTime,
'end_time' => $endTime
]);
return [
'ok' => false,
'error' => $e->getMessage(),
'trace_id' => $traceId,
];
}
}
/**
* 同步特定页码的商品
* @param string $startTime
* @param string $endTime
* @param int $pageIndex
* @param int $supplierId
* @return bool
*/
public function syncProductPage(string $startTime, string $endTime, int $pageIndex, int $supplierId = 0, string $traceId = '')
{
try {
Log::info('admin.manual.jst.product.sync_page.start', [
'trace_id' => $traceId,
'page' => $pageIndex,
'start_time' => $startTime,
'end_time' => $endTime
]);
$jstService = app()->make(HaichaoJstServices::class);
$result = $jstService->getProductListByTime($startTime, $endTime, $pageIndex, 50);
$productList = $result['datas'] ?? [];
$resultPreview = null;
try {
$resultPreview = mb_substr(json_encode($result, JSON_UNESCAPED_UNICODE), 0, 2000);
} catch (\Throwable $e) {
}
Log::info('admin.manual.jst.product.sync_page.api_result', [
'trace_id' => $traceId,
'page' => $pageIndex,
'page_count' => $result['page_count'] ?? null,
'datas_count' => is_array($productList) ? count($productList) : 0,
'result_preview' => $resultPreview,
]);
$stats = $this->processProductList($productList, $supplierId, $traceId, [
'mode' => 'time',
'page' => $pageIndex,
]);
/** @var StoreProductServices $productServices */
$productServices = app()->make(StoreProductServices::class);
/** @var StoreProductAttrServices $productAttrServices */
$productAttrServices = app()->make(StoreProductAttrServices::class);
$productServices->cacheTag()->clear();
$productAttrServices->cacheTag()->clear();
Log::info('admin.manual.jst.product.sync_page.finish', [
'trace_id' => $traceId,
'page' => $pageIndex,
'count' => is_array($productList) ? count($productList) : 0,
'created' => $stats['created'] ?? 0,
'updated' => $stats['updated'] ?? 0,
'skipped' => $stats['skipped'] ?? 0,
'failed' => $stats['failed'] ?? 0,
]);
return true;
} catch (\Exception $e) {
Log::error('admin.manual.jst.product.sync_page.error', [
'trace_id' => $traceId,
'page' => $pageIndex,
'message' => $e->getMessage()
]);
return false;
}
}
/**
* 保存从ERP获取的商品数据
* @param array $item
* @param int $supplierId
*/
private function saveProductFromErpData(array $item, int $supplierId = 0): array
{
/** @var StoreProductServices $productServices */
$productServices = app()->make(StoreProductServices::class);
/** @var StoreProductAttrServices $productAttrServices */
$productAttrServices = app()->make(StoreProductAttrServices::class);
$relationSupplierId = $supplierId > 0 ? $supplierId : 0;
Log::info('admin.manual.jst.product.sync.item.process', [
'i_id' => $item['i_id'] ?? '',
'name' => $item['name'] ?? '',
'sku_count' => isset($item['skus']) && is_array($item['skus']) ? count($item['skus']) : 0
]);
$skuRows = isset($item['skus']) && is_array($item['skus']) ? $item['skus'] : [];
$salePrice = floatval($item['s_price'] ?? 0);
if ($salePrice <= 0 && $skuRows) {
$skuSalePrices = [];
foreach ($skuRows as $skuRow) {
if (isset($skuRow['sale_price']) && is_numeric($skuRow['sale_price'])) {
$skuSalePrices[] = floatval($skuRow['sale_price']);
}
}
if ($skuSalePrices) {
$salePrice = min($skuSalePrices);
}
}
$marketPrice = floatval($item['market_price'] ?? 0);
if ($marketPrice <= 0 && $skuRows) {
$skuMarketPrices = [];
foreach ($skuRows as $skuRow) {
if (isset($skuRow['market_price']) && is_numeric($skuRow['market_price'])) {
$skuMarketPrices[] = floatval($skuRow['market_price']);
}
}
if ($skuMarketPrices) {
$marketPrice = max($skuMarketPrices);
}
}
$costPrice = floatval($item['c_price'] ?? ($item['cost_price'] ?? 0));
if ($costPrice <= 0 && $skuRows) {
$skuCostPrices = [];
foreach ($skuRows as $skuRow) {
if (isset($skuRow['cost_price']) && is_numeric($skuRow['cost_price'])) {
$skuCostPrices[] = floatval($skuRow['cost_price']);
}
}
if ($skuCostPrices) {
$costPrice = max($skuCostPrices);
}
}
$pic = (string)($item['pic'] ?? ($skuRows[0]['pic'] ?? ''));
$name = (string)($item['name'] ?? ($skuRows[0]['name'] ?? ''));
$productInfo = [
'image' => $pic,
'slider_image' => json_encode([$pic]),
'store_name' => $name,
'store_info' => $name,
'cate_id' => 0,
'price' => $salePrice,
'ot_price' => $marketPrice,
'delivery_type' => '1,2,3',
'freight' => 1,
'is_show' => 0,
'add_time' => time(),
'cost' => $costPrice,
'ficti' => 0,
'spec_type' => 1,
'code' => $item['i_id'],
'type' => $relationSupplierId ? 2 : 0,
'relation_id' => $relationSupplierId,
];
$detail = $details = $value = [];
$usedSpecValues = [];
foreach ($skuRows as $skuIndex => $items) {
$specValue = trim((string)($items['properties_value'] ?? ''));
if ($specValue === '') {
$specValue = trim((string)($items['name'] ?? ''));
}
if ($specValue === '') {
$specValue = '规格' . ($skuIndex + 1);
}
if (isset($usedSpecValues[$specValue])) {
$specValue = $specValue . '-' . ($skuIndex + 1);
}
$usedSpecValues[$specValue] = 1;
$detail[] = $specValue;
$details[] = [
'name' => $specValue,
'select' => false
];
$skuCost = floatval($items['cost_price'] ?? $costPrice);
$skuSettle = $relationSupplierId ? $skuCost : 0.0;
$value[] = [
'bar_code' => '',
'brokerage' => 0,
'brokerage_two' => 0,
'code' => (string)($items['sku_id'] ?? ''),
'settle_price' => $skuSettle,
'cost' => $skuCost,
'detail' => ['规格' => $specValue],
'ot_price' => floatval($items['market_price'] ?? $marketPrice),
'pic' => (string)$items['pic'],
'price' => floatval($items['sale_price'] ?? $salePrice),
'select' => true,
'value1' => $specValue,
'values' => $specValue,
'vip_price' => 0,
'volume' => 0,
'weight' => 0,
'stock' => 0,
];
}
if (!$value) {
$specValue = '默认';
$detail[] = $specValue;
$details[] = [
'name' => $specValue,
'select' => false
];
$defCost = floatval($item['c_price'] ?? 0);
$defSettle = $relationSupplierId ? $defCost : 0.0;
$value[] = [
'bar_code' => '',
'brokerage' => 0,
'brokerage_two' => 0,
'code' => (string)($item['i_id'] ?? ''),
'settle_price' => $defSettle,
'cost' => $defCost,
'detail' => ['规格' => $specValue],
'ot_price' => floatval($item['market_price'] ?? 0),
'pic' => (string)($item['pic'] ?? ''),
'price' => floatval($item['s_price'] ?? 0),
'select' => true,
'value1' => $specValue,
'values' => $specValue,
'vip_price' => 0,
'volume' => 0,
'weight' => 0,
'stock' => 0,
];
}
$pid = $productServices->value(['code' => $item['i_id']], 'id');
$existed = !!$pid;
if (!$pid) {
$pid = $productServices->ErpProductSave($productInfo);
}
$productServices->ensureSpu((int)$pid);
$attr = [[
'value' => '规格',
'detail' => $detail,
'details' => $details,
]];
$skuList = $productAttrServices->validateProductAttr($attr, $value, $pid, 0, 0, 0);
$productAttrServices->saveProductAttr($skuList, $pid);
Log::info('admin.manual.jst.product.sync.save', [
'product_id' => $pid,
'existed' => $existed,
'sku_saved_count' => isset($skuList['value']) && is_array($skuList['value']) ? count($skuList['value']) : 0
]);
return ['created' => !$existed, 'product_id' => $pid, 'existed' => $existed];
}
/**
@@ -412,7 +695,7 @@ class ProductSyncErp extends BaseJobs
}
// 更新平台商品库存
$defaultShopId = (int)sys_config('jst_default_shopid');
$defaultShopId = (int)app()->make(\crmeb\services\SystemConfigService::class)->get('jst_default_shopid');
if ($defaultShopId == $erpShopId) {
$this->updateStoreProductValueStock($data);
}