feat(haichao): 新增聚水潭ERP集成功能

- 新增聚水潭回调接口及事件处理机制
- 添加聚水潭商品、订单、售后同步任务
- 实现聚水潭订单推送及售后状态同步
- 新增手动同步聚水潭商品功能
- 在各路由组添加聚水潭中间件支持
This commit is contained in:
2026-04-01 11:55:18 +08:00
parent 359e9eef48
commit 7e422335e4
30 changed files with 2601 additions and 37 deletions

View File

@@ -0,0 +1,344 @@
<?php
namespace app\haichao\sync;
use app\haichao\HaichaoJstServices;
use app\haichao\util\CacheProduct;
use app\haichao\util\ImageUtil;
use app\jobs\product\ProductStockTips;
use app\services\product\product\StoreDescriptionServices;
use app\services\product\product\StoreProductServices;
use app\services\product\sku\StoreProductAttrServices;
use Exception;
use think\facade\Log;
use Throwable;
class JstProductSync extends HaichaoJstServices
{
public function syncImage(): void
{
/** @var StoreProductServices $make */
$make = app()->make(StoreProductServices::class);
$ps = $make->count(['is_del' => 0]);
$iil = $this->getInneropenItemList();
/** @var StoreDescriptionServices $proDesServices */
$proDesServices = app()->make(StoreDescriptionServices::class);
/** @var StoreProductServices $productServices */
$productServices = app()->make(StoreProductServices::class);
foreach ($iil as $item) {
$itemSpuId = $item['item_spu_id'];
try {
$inneropenProduct = $this->getInneropenItemDetail($itemSpuId);
} catch (Throwable $e) {
$inneropenProduct = $this->getInneropenItemDetail($itemSpuId);
}
$images = "";
foreach ($inneropenProduct['item_photo']['item_detail_images'] as $oImage) {
$images .= "<p><img src=\"" . ImageUtil::urlToOssUrl($oImage) . "\"></p>";
}
$pid = $productServices->value(['code' => $item['style_code']], 'id');
if ($pid) {
$proDesServices->saveDescription($pid, $images);
}
}
}
public function syncProductSingle($data, $pid = 0)
{
$productList = $data['datas'];
/** @var StoreProductServices $productServices */
$productServices = app()->make(StoreProductServices::class);
/** @var StoreProductAttrServices $productAttrServices */
$productAttrServices = app()->make(StoreProductAttrServices::class);
try {
$skuParList = [];
$index = 1;
$count = 0;
$skuIds = '';
foreach ($productList as $item) {
$count++;
$skuIds .= ($skuIds ? "," . $item['sku_id'] : $item['sku_id']);
$skuParList[$index] = [
'length' => $count,
'code' => $skuIds
];
}
$stockMap = [];
foreach ($skuParList as $k => $skus) {
$r = $this->findStockBySkuId($skus['code']);
foreach ($r as $v) {
if (!isset($stockMap[$v['sku_id']])) $stockMap[$v['sku_id']] = 0;
$stockMap[$v['sku_id']] += $v['qty'];
}
}
foreach ($productList as $item) {
$pic = ($item['pic'] ? $item['pic'] : '');
if (!$item['sale_price']) {
$item['sale_price'] = 0;
$item['s_price'] = 0;
}
if (!$item['market_price']) {
$item['market_price'] = 0;
}
$item['c_price'] = $item['cost_price'];
$item['i_id'] = $item['sku_id'];
$supplierId = 0;
try {
if (isset($item['shop_id']) && $item['shop_id']) {
/** @var \app\services\supplier\SystemSupplierServices $supplierServices */
$supplierServices = app()->make(\app\services\supplier\SystemSupplierServices::class);
$supplier = $supplierServices->getOne(['erp_shopid' => (int)$item['shop_id'], 'is_del' => 0], 'id');
if ($supplier && isset($supplier['id'])) {
$supplierId = (int)$supplier['id'];
}
}
} catch (\Throwable $e) {
Log::error('jst.product.sync.supplier_map_err ' . $e->getMessage());
}
$relationSupplierId = $pid ?: $supplierId;
$productInfo = [
'image' => $pic,
'slider_image' => json_encode([$pic]),
'store_name' => $item['name'],
'store_info' => $item['name'],
'cate_id' => 0,
'price' => floatval($item['sale_price'] ?? 0),
'ot_price' => floatval($item['market_price']),
'delivery_type' => '1',
'freight' => 1,
'add_time' => time(),
'cost' => floatval($item['c_price']),
'ficti' => 0,
'spec_type' => 1,
'code' => $item['i_id'],
'type' => $relationSupplierId ? 2 : 0,
'relation_id' => $relationSupplierId,
];
$stockSum = 0;
$detail = $details = $value = [];
$items = $item;
if ($items['properties_value']) {
$detail[] = $items['properties_value'];
$details[] = [
'name' => $items['properties_value'],
'select' => false
];
$stock = $stockMap[$items['sku_id']] ?? 0;
$skuCost = floatval($items['cost_price']);
$skuSettle = $relationSupplierId ? $skuCost : 0.0;
$value[] = [
'bar_code' => '',
'brokerage' => 0,
'brokerage_two' => 0,
'code' => $items['sku_id'],
'settle_price' => $skuSettle,
'cost' => $skuCost,
'detail' => ['规格' => $items['properties_value']],
'ot_price' => floatval($items['market_price']),
'pic' => ImageUtil::urlToOssUrl($items['pic']),
'price' => floatval($items['sale_price']),
'select' => true,
'value1' => $items['properties_value'],
'values' => $items['properties_value'],
'vip_price' => 0,
'volume' => 0,
'weight' => 0,
'stock' => $stock,
'sum_stock' => $stock,
];
$stockSum = bcadd($stockSum, $stock, 0);
} else {
return;
}
$productInfo['stock'] = (int)$stockSum;
$pidNow = $productServices->value(['code' => $item['i_id']], 'id');
if (!isset($pidNow)) {
$pidNow = $productServices->ErpProductSave($productInfo);
} else {
$productServices->update($pidNow, $productInfo);
}
ProductStockTips::dispatch([$pidNow, 0]);
$attr = [[
'value' => '规格',
'detail' => $detail,
'details' => $details,
]];
$skuList = $productAttrServices->validateProductAttr($attr, $value, $pidNow, 0, 0, 0);
$productAttrServices->saveProductAttr($skuList, $pidNow);
}
$productServices->cacheTag()->clear();
$productAttrServices->cacheTag()->clear();
} catch (Exception $e) {
Log::error('jst.product.sync.error ' . $e->getMessage());
}
}
public function syncProduct(): void
{
/** @var StoreProductServices $make */
$make = app()->make(StoreProductServices::class);
$ps = $make->count(['is_del' => 0]);
/** @var StoreProductServices $productServices */
$productServices = app()->make(StoreProductServices::class);
/** @var StoreProductAttrServices $productAttrServices */
$productAttrServices = app()->make(StoreProductAttrServices::class);
try {
$pls = $this->getProductList();
$productList = $pls['datas'];
Log::info('jst.product.sync.start ' . count($productList));
$skuParList = [];
$index = 1;
$count = 0;
$skuIds = '';
foreach ($productList as $item) {
if (CacheProduct::isCache(md5($item['i_id']))) continue;
foreach ($item['skus'] as $skus) {
$count++;
$skuIds .= $skus['sku_id'] . ",";
$skuParList[$index] = [
'length' => $count,
'code' => $skuIds
];
if ($count >= 500) {
$index++;
$count = 0;
$skuIds = '';
}
}
}
$stockMap = [];
foreach ($skuParList as $k => $skus) {
$r = $this->findAllInneropenItemBySkuId($skus['code']);
foreach ($r as $v) {
$stockMap[$v['item_code']] = $v['stock'];
}
}
foreach ($productList as $item) {
if (!$item['skus']) continue;
if (CacheProduct::isCache(md5($item['i_id']))) continue;
$pic = ImageUtil::urlToOssUrl($item['pic']);
$supplierId = 0;
try {
if (isset($item['shop_id']) && $item['shop_id']) {
/** @var \app\services\supplier\SystemSupplierServices $supplierServices */
$supplierServices = app()->make(\app\services\supplier\SystemSupplierServices::class);
$supplier = $supplierServices->getOne(['erp_shopid' => (int)$item['shop_id'], 'is_del' => 0], 'id');
if ($supplier && isset($supplier['id'])) {
$supplierId = (int)$supplier['id'];
}
}
} catch (\Throwable $e) {
}
$salePrice = floatval($item['s_price'] ?? 0);
if ($salePrice <= 0 && isset($item['skus']) && is_array($item['skus'])) {
$skuSalePrices = [];
foreach ($item['skus'] 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 && isset($item['skus']) && is_array($item['skus'])) {
$skuMarketPrices = [];
foreach ($item['skus'] 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 && isset($item['skus']) && is_array($item['skus'])) {
$skuCostPrices = [];
foreach ($item['skus'] as $skuRow) {
if (isset($skuRow['cost_price']) && is_numeric($skuRow['cost_price'])) {
$skuCostPrices[] = floatval($skuRow['cost_price']);
}
}
if ($skuCostPrices) {
$costPrice = max($skuCostPrices);
}
}
$productInfo = [
'image' => $pic,
'slider_image' => json_encode([$pic]),
'store_name' => $item['name'],
'store_info' => $item['name'],
'cate_id' => 0,
'price' => $salePrice,
'ot_price' => $marketPrice,
'delivery_type' => '1',
'freight' => 1,
'add_time' => time(),
'cost' => $costPrice,
'ficti' => 0,
'spec_type' => 1,
'code' => $item['i_id'],
'type' => $supplierId ? 2 : 0,
'relation_id' => $supplierId,
];
$stockSum = 0;
$detail = $details = $value = [];
foreach ($item['skus'] as $items) {
if (!$items['properties_value']) continue 2;
$detail[] = $items['properties_value'];
$details[] = [
'name' => $items['properties_value'],
'select' => false
];
$stock = $stockMap[$items['sku_id']] ?? 0;
$skuCost = floatval($items['cost_price']);
$skuSettle = $supplierId ? $skuCost : 0.0;
$value[] = [
'bar_code' => '',
'brokerage' => 0,
'brokerage_two' => 0,
'code' => $items['sku_id'],
'settle_price' => $skuSettle,
'cost' => $skuCost,
'detail' => ['规格' => $items['properties_value']],
'ot_price' => floatval($items['market_price']),
'pic' => ImageUtil::urlToOssUrl($items['pic']),
'price' => floatval($items['sale_price']),
'select' => true,
'value1' => $items['properties_value'],
'values' => $items['properties_value'],
'vip_price' => 0,
'volume' => 0,
'weight' => 0,
'stock' => $stock,
'sum_stock' => $stock,
];
$stockSum = bcadd($stockSum, $stock, 0);
}
$productInfo['stock'] = (int)$stockSum;
$pid = $productServices->value(['code' => $item['i_id']], 'id');
if (!isset($pid)) {
$pid = $productServices->ErpProductSave($productInfo);
} else {
$productServices->update($pid, $productInfo);
}
ProductStockTips::dispatch([$pid, 0]);
$attr = [[
'value' => '规格',
'detail' => $detail,
'details' => $details,
]];
$skuList = $productAttrServices->validateProductAttr($attr, $value, $pid, 0, 0, 0);
$productAttrServices->saveProductAttr($skuList, $pid);
CacheProduct::setCache(md5($item['i_id']), $item['i_id']);
}
$productServices->cacheTag()->clear();
$productAttrServices->cacheTag()->clear();
Log::info('jst.product.sync.end');
} catch (Exception $e) {
Log::error('jst.product.sync.error ' . $e->getMessage());
}
}
}

View File

@@ -0,0 +1,115 @@
<?php
namespace app\haichao\sync;
use app\haichao\HaichaoJstServices;
use app\jobs\product\ProductSyncErp;
use app\services\product\product\StoreProductServices;
use app\services\product\sku\StoreProductAttrServices;
use app\services\product\sku\StoreProductAttrValueServices;
use app\services\store\SystemStoreServices;
use crmeb\exceptions\AdminException;
use think\facade\Log;
use Exception;
class JstStorkSync extends HaichaoJstServices
{
public function syncStorkFromInneropenErp(): void
{
/** @var StoreProductAttrValueServices $storeProductAttrValueServices */
$storeProductAttrValueServices = app()->make(StoreProductAttrValueServices::class);
Log::info('jst.stork.sync.start');
$inventorySkuIds = [];
$skuIds = '';
$index = 1;
$count = 0;
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;
}
}
}
$productStock = [];
foreach ($inventorySkuIds as $inventorySkuId) {
foreach ($this->getInneropenStockAndDetailList($inventorySkuId['ids']) as $product) {
$productStock[] = [
'code' => $product['item_code'],
'stock' => $product['stock']
];
}
}
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.end');
}
public function syncStockFromErp(array $ids): bool
{
try {
/** @var StoreProductServices $storeProductServices */
$storeProductServices = app()->make(StoreProductServices::class);
/** @var StoreProductAttrValueServices $storeProductAttrValueServices */
$storeProductAttrValueServices = app()->make(StoreProductAttrValueServices::class);
$list = $storeProductAttrValueServices->getSkuArray(['product_id' => $ids, 'type' => 0], 'code', 'id');
$values = array_filter(array_values($list));
if (empty($values)) {
throw new AdminException('没有符合同步库存的商品');
}
$skuData = $skuMap = [];
$basic = 20;
$num = count($values);
$rate = ceil($num / $basic);
for ($i = 0; $i < $rate; $i++) {
$code = array_slice($values, $i * $basic, $basic);
$codeStr = implode(',', $code);
$result = $this->getStork($codeStr)['data'];
if (!empty($result['inventorys'])) {
foreach ($result['inventorys'] as $inventory) {
$skuMap[$inventory['sku_id']] = $inventory['qty'] - $inventory['order_lock'];
}
}
}
if (!empty($skuMap)) {
foreach ($skuMap as $key => $item) {
if ($id = array_search($key, $list)) {
$skuData[] = ['id' => $id, 'stock' => $item, 'sum_stock' => $item];
}
}
}
$storeProductServices->transaction(function () use ($ids, $skuData, $storeProductAttrValueServices, $storeProductServices) {
$storeProductAttrValueServices->saveAll($skuData);
$productData = $storeProductAttrValueServices->getProductStockByValues($ids);
$storeProductServices->saveAll($productData);
});
/** @var SystemStoreServices $systemStoreServices */
$systemStoreServices = app()->make(SystemStoreServices::class);
$systemStoreList = $systemStoreServices->getErpStore([['erp_shop_id', '>', 0]]);
if (!empty($systemStoreList)) {
foreach ($systemStoreList as $store) {
ProductSyncErp::dispatchDo('syncBranchProductStock', [$ids[0], $skuMap, $store['id']]);
}
}
$storeProductServices->cacheTag()->clear();
/** @var StoreProductAttrServices $attrService */
$attrService = app()->make(StoreProductAttrServices::class);
$attrService->cacheTag()->clear();
} catch (Exception $e) {
Log::error('jst.stork.sync.error ' . $e->getMessage());
}
return true;
}
}