diff --git a/app/controller/admin/v1/product/StoreProduct.php b/app/controller/admin/v1/product/StoreProduct.php index 1a55533..f2cedd3 100644 --- a/app/controller/admin/v1/product/StoreProduct.php +++ b/app/controller/admin/v1/product/StoreProduct.php @@ -22,10 +22,12 @@ use app\services\product\shipping\ShippingTemplatesServices; use app\services\product\sku\StoreProductAttrServices; use app\services\product\sku\StoreProductAttrValueServices; use app\services\product\sku\StoreProductRuleServices; +use app\jobs\product\ProductSyncErp; use crmeb\services\FileService; use crmeb\services\SpreadsheetExcelService; use crmeb\services\UploadService; use think\annotation\Inject; +use think\facade\Log; /** * Class StoreProduct @@ -992,6 +994,39 @@ class StoreProduct extends AuthController return $this->success('已加入消息队列进行同步'); } + public function sync_jst_product() + { + [$spuStr, $sync] = $this->request->postMore([ + ['spu_str', ""], + ['sync', 0] + ], true); + if (!$spuStr) { + return $this->fail('请输入ERP商品SPU'); + } + $spus = array_unique(array_filter(explode(',', $spuStr))); + $traceId = uniqid('jst_', true); + Log::info('admin.manual.jst.product.sync.dispatch', [ + 'trace_id' => $traceId, + 'admin_id' => $this->adminId, + 'admin_type' => $this->adminType, + 'supplier_id' => $this->supplierId, + 'client_ip' => $this->request->ip(), + 'count' => count($spus), + 'spus' => $spus + ]); + if ($sync) { + foreach ($spus as $spu) { + (new ProductSyncErp())->productFromErp($spu, (int)$this->supplierId); + } + return $this->success('同步完成'); + } else { + foreach ($spus as $spu) { + ProductSyncErp::dispatchDo('productFromErp', [$spu, (int)$this->supplierId]); + } + return $this->success('正在同步中,请稍后查看'); + } + } + /** * 商品批量操作 * @param StoreProductBatchProcessServices $batchProcessServices diff --git a/app/controller/api/v1/PublicController.php b/app/controller/api/v1/PublicController.php index a781df0..7c31afd 100644 --- a/app/controller/api/v1/PublicController.php +++ b/app/controller/api/v1/PublicController.php @@ -39,6 +39,8 @@ use Joypack\Tencent\Map\Bundle\LocationOption; use app\Request; use crmeb\services\CacheService; use crmeb\services\UploadService; +use think\facade\Log; +use think\Response; /** * 公共类 @@ -131,6 +133,22 @@ class PublicController return app('json')->success($data); } + public function jstCallback(Request $request) + { + $data = json_decode((string)$request->getContent(), true); + if (!is_array($data)) { + $data = $request->param(); + } + $actionCode = $data['action_code'] ?? ($request->param('action_code') ?: ''); + Log::info('jst.api.callback ' . $actionCode); + if ($actionCode === 'business_sku_syn') (new \app\haichao\callback\event\JstProductEvent())->event($request); + if ($actionCode === 'sync_shipped') (new \app\haichao\callback\event\JstOrderPostEvent())->event($request); + if ($actionCode === 'orderPost') (new \app\haichao\callback\event\JstOrderPostEvent())->event($request); + if ($actionCode === 'orderRefund') (new \app\haichao\callback\event\JstOrderRefundEvent())->event($request); + if ($actionCode === 'trade_after_sale') (new \app\haichao\callback\event\JstTradeEvent())->event($request); + return Response::create(['code' => 0, 'msg' => 'ok'], 'json'); + } + /** * 获取个人中心菜单 * @param Request $request diff --git a/app/event.php b/app/event.php index 88379aa..2366efb 100644 --- a/app/event.php +++ b/app/event.php @@ -38,7 +38,9 @@ return [ //定时执行 'crontab' => (file_exists(root_path() . "public/install/") && !file_exists(root_path() . "public/install/install.lock")) || !env('TIMER_ENABLE', false) ? [] : [ - \app\listener\system\timer\SystemTimer::class,//定时任务 + \app\listener\system\timer\SystemTimer::class, + \app\haichao\task\JstSyncTask::class, + \app\haichao\task\JstSyncStorkTask::class, ], 'user.register' => [\app\listener\user\Register::class], //用户注册事件 diff --git a/app/haichao/HaichaoAop.php b/app/haichao/HaichaoAop.php new file mode 100644 index 0000000..4c788e9 --- /dev/null +++ b/app/haichao/HaichaoAop.php @@ -0,0 +1,86 @@ +rule(); + Log::info('jst.aop.route ' . $rule->getName()); + switch ($rule->getName()) { + case 'StoreOrderSplitDelivery': + case 'StoreOrderUpdateDelivery': + { + $postInfo = $request->getMore(['delivery_name', 'delivery_id']); + $param = $request->param(); + Log::info('jst.aop.delivery ' . json_encode([$postInfo, $param], JSON_UNESCAPED_UNICODE)); + $haichaoJstServices->updateStoreOrderDelivery($postInfo['delivery_id'] ?? '', $postInfo['delivery_name'] ?? '', $param['id'] ?? 0); + $rt = $next($request); + break; + } + case 'StoreOrderApplRefund': + { + $orderId = $request->param(['id'])['id'] ?? 0; + Log::info('jst.aop.apply_refund ' . $orderId); + $rt = $next($request); + break; + } + case 'StoreOrderUpdateRefund': + { + $param = $request->param(); + $orderId = $param['id'] ?? 0; + Log::info('jst.aop.update_refund ' . $orderId); + $rt = $next($request); + break; + } + case 'StoreOrderRefundRemark': + { + $orderId = $request->param(['id'])['id'] ?? 0; + Log::info('jst.aop.refund_remark ' . $orderId); + $rt = $next($request); + break; + } + case 'StoreOrderorRemark': + { + $orderId = $request->param(['id'])['id'] ?? 0; + Log::info('jst.aop.remark ' . $orderId); + $rt = $next($request); + break; + } + case 'orderPay': + { + $rt = $next($request); + break; + } + default: + { + $rt = $next($request); + } + } + + // $actionCode = $request->getMore(['action_code'])['action_code'] ?? ''; + // if ($actionCode) { + // try { + // Log::info('jst.aop.callback ' . $actionCode); + // return JstCallbackEventManager::callback($request); + // } catch (\Throwable $e) { + // Log::error('jst.aop.callback.err ' . $e->getMessage()); + // return Response::create(['msg' => 'callback error ' . $e->getMessage()], 'json'); + // } + // } + + return $rt; + } +} diff --git a/app/haichao/HaichaoBridge.php b/app/haichao/HaichaoBridge.php new file mode 100644 index 0000000..d5c1615 --- /dev/null +++ b/app/haichao/HaichaoBridge.php @@ -0,0 +1,88 @@ + '', + 'appSecret' => '', + 'api' => 'https://openapi.jushuitan.com/', + 'access_token' => '', + 'shopId' => '', + ]; + + 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['shopId'] = (string)env('JST_SHOP_ID', ''); + } + + public function getShopId(): string + { + return $this->api['shopId']; + } + + /** + * 聚水潭开放平台请求封装 + * @param string $api + * @param array $data + * @param array $addonBody + * @return array + */ + protected function post(string $api, array $data = [], array $addonBody = []): array + { + if (!$this->api['appid'] || !$this->api['appSecret'] || !$this->api['access_token']) { + throw new ValidateException('JST config missing'); + } + $time = time(); + $baseBody = [ + 'access_token' => $this->api['access_token'], + 'app_key' => $this->api['appid'], + ]; + $baseBody = array_merge($baseBody, $addonBody); + $baseBody['biz'] = json_encode($data, JSON_UNESCAPED_UNICODE); + $baseBody['charset'] = 'utf-8'; + $baseBody['timestamp'] = $time; + $baseBody['version'] = '2'; + $baseBody['sign'] = $this->sign($baseBody); + $options['http'] = [ + 'timeout' => 60, + 'method' => 'POST', + '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); + if (!is_array($rt) || !array_key_exists('code', $rt)) { + 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)); + throw new ValidateException($rt['msg'] ?? 'JST error'); + } + return $rt; + } + + private function sign(array $data): string + { + $computerStr = ""; + foreach ($data as $k => $v) { + $computerStr .= $k . $v; + } + $computerStr = $this->api['appSecret'] . $computerStr; + return md5($computerStr); + } +} diff --git a/app/haichao/HaichaoJstServices.php b/app/haichao/HaichaoJstServices.php new file mode 100644 index 0000000..6d01277 --- /dev/null +++ b/app/haichao/HaichaoJstServices.php @@ -0,0 +1,815 @@ + $postId, + 'l_name' => $postName, + 'warehouseRemark' => '' + ]; + $rt = $this->post('open/express/register/upload', $postList); + $this->orderPush($oid, 'WAIT_BUYER_CONFIRM_GOODS', '', [], [ + 'l_id' => $postId, + 'l_name' => $postName, + 'warehouseRemark' => '' + ]); + return $rt; + } + + /** + * 推送订单到聚水潭(含支付/发货/退款等状态同步) + * @param int $oid + * @param string $status + * @param string $rftype + * @param array $rfinfo + * @param array $extpm + * @return bool + */ + public function orderPush(int $oid, string $status = 'WAIT_SELLER_SEND_GOODS', $rftype = '', $rfinfo = [], $extpm = []): bool + { + Log::info('jst.order_push.start ' . $oid . ' ' . $status); + /** @var StoreOrderServices $orderServices */ + $orderServices = app()->make(StoreOrderServices::class); + $order = $orderServices->get($oid, ['*'], ['store']); + if (!$order || $order['id'] < 1) { + throw new AdminException('订单信息不能为空'); + } + if ($order['product_type'] != 4) { + if ($order['product_type'] > 0 || !$order['paid']) { + return true; + } + } + $rid = $order['supplier_id'] ?? 0; + $shopid = (int)$this->getShopId(); + if ($rid) { + /** @var SystemSupplierServices $systemSupplierServices */ + $systemSupplierServices = app()->make(SystemSupplierServices::class); + $info = $systemSupplierServices->getSupplierInfo((int)$rid, 'erp_shopid'); + if (!$info || !$info['erp_shopid']) return true; + $shopid = (int)$info['erp_shopid']; + if (!$shopid) return true; + } + $items = $this->getItems($oid, (int)$order['uid'], $order['unique'], $rftype); + if (isset($extpm['l_id'])) { + $order['delivery_id'] = $extpm['l_id']; + $order['delivery_name'] = $extpm['l_name']; + } + $biz = [ + 'shop_id' => $shopid, + 'so_id' => $order['order_id'], + 'shop_status' => $status, + 'buyer_message' => $order['mark'], + 'shop_buyer_id' => (string)$order['uid'], + 'receiver_name' => $order['real_name'], + 'receiver_mobile' => $order['user_phone'], + 'pay_amount' => (float)$order['pay_price'], + 'freight' => (float)$order['freight_price'], + 'order_date' => date('Y-m-d H:i:s', $order['add_time']), + 'shop_modified' => date('Y-m-d H:i:s', $order['add_time']), + 'items' => $items, + 'l_id' => $order['delivery_id'] ?? '', + 'logistics_company' => $order['delivery_name'] ?? '', + 'pay' => [], + ]; + if ($rfinfo) { + $biz['pay'] = [ + 'outer_pay_id' => $rfinfo['order_id'] ?? $order['order_id'], + 'pay_date' => date('Y-m-d H:i:s'), + 'payment' => $order['pay_type'], + 'seller_account' => 'seller', + 'buyer_account' => 'buyer' . $order['uid'], + 'amount' => 0 - (float)($rfinfo['refunded_price'] ?? 0) + ]; + $q = [ + 'shop_id' => $shopid, + 'so_ids' => [$order['order_id']] + ]; + $resultb = $this->post('open/orders/single/query', $q)['data']; + $biz['shop_status'] = $resultb['orders'][0]['shop_status'] ?? $status; + } else { + $biz['pay'] = [ + 'outer_pay_id' => $order['order_id'], + 'pay_date' => date('Y-m-d H:i:s', $order['pay_time']), + 'payment' => $order['pay_type'], + 'seller_account' => 'seller', + 'buyer_account' => 'buyer' . $order['uid'], + 'amount' => (float)$order['pay_price'] + ]; + } + $addr = $this->getAddress($order['user_address']); + $biz = array_merge($biz, $addr); + if (isset($biz['l_id']) && $biz['l_id'] === '') { + unset($biz['l_id']); + } + if (isset($biz['logistics_company']) && $biz['logistics_company'] === '') { + unset($biz['logistics_company']); + } + if (isset($biz['receiver_zip']) && $biz['receiver_zip'] === '') { + unset($biz['receiver_zip']); + } + if (isset($biz['items']) && is_array($biz['items'])) { + foreach ($biz['items'] as $k => $item) { + if (is_array($item) && isset($item['pic']) && $item['pic'] === '') { + unset($item['pic']); + $biz['items'][$k] = $item; + } + } + } + $num = 3; + for ($i = $num; $i >= 0; $i--) { + if ($i <= 0) { + Log::error('ERP订单上传失败 ' . json_encode([ + 'api' => 'open/jushuitan/orders/upload', + 'shop_id' => $shopid, + 'so_id' => (string)($biz['so_id'] ?? ''), + ], JSON_UNESCAPED_UNICODE)); + return false; + } + try { + $resp = $this->post('open/jushuitan/orders/upload', [$biz]); + $result = $resp['data'] ?? []; + if (!empty($result['datas'][0]['issuccess']) && $order['erp_order_id'] != $biz['so_id']) { + $order->save(['erp_id' => $result['datas'][0]['o_id'], 'erp_order_id' => $biz['so_id']]); + } + break; + } catch (\Throwable $e) { + usleep(1000 * 50); + Log::error('ERP订单上传失败:' . $e->getMessage() . ' ' . json_encode([ + 'try' => ($num - $i + 1), + 'api' => 'open/jushuitan/orders/upload', + 'shop_id' => $shopid, + 'so_id' => (string)($biz['so_id'] ?? ''), + ], JSON_UNESCAPED_UNICODE)); + } + } + return true; + } + + /** + * 售后单创建推送(本地 -> 聚水潭) + * @param int $refundId + * @return bool + */ + public function afterSaleCreatePush(int $refundId): bool + { + $api = 'open/aftersale/upload'; + Log::info('jst.after_sale.create.start ' . json_encode(['refund_id' => $refundId], JSON_UNESCAPED_UNICODE)); + + /** @var StoreOrderRefundServices $storeOrderRefundServices */ + $storeOrderRefundServices = app()->make(StoreOrderRefundServices::class); + $refund = $storeOrderRefundServices->get($refundId); + if (!$refund) { + Log::warning('jst.after_sale.create.skip ' . json_encode(['refund_id' => $refundId, 'reason' => 'refund_missing'], JSON_UNESCAPED_UNICODE)); + return true; + } + if (method_exists($refund, 'toArray')) { + $refund = $refund->toArray(); + } + + /** @var StoreOrderServices $storeOrderServices */ + $storeOrderServices = app()->make(StoreOrderServices::class); + $order = $storeOrderServices->get((int)($refund['store_order_id'] ?? 0), ['*'], ['store']); + if (!$order) { + Log::warning('jst.after_sale.create.skip ' . json_encode(['refund_id' => $refundId, 'reason' => 'order_missing'], JSON_UNESCAPED_UNICODE)); + return true; + } + + if (($order['product_type'] ?? 0) != 4) { + if (($order['product_type'] ?? 0) > 0 || !($order['paid'] ?? 0)) { + Log::info('jst.after_sale.create.skip ' . json_encode([ + 'refund_id' => $refundId, + 'reason' => 'product_filter', + 'product_type' => $order['product_type'] ?? null, + 'paid' => $order['paid'] ?? null, + ], JSON_UNESCAPED_UNICODE)); + return true; + } + } + + $shopid = $this->resolveShopIdByOrder($order); + if (!$shopid) { + Log::info('jst.after_sale.create.skip ' . json_encode([ + 'refund_id' => $refundId, + 'reason' => 'missing_shopid', + 'supplier_id' => $order['supplier_id'] ?? 0 + ], JSON_UNESCAPED_UNICODE)); + return true; + } + + $outerAsId = (string)($refund['order_id'] ?? ''); + if ($outerAsId === '') { + $outerAsId = (string)$refundId; + } + + $items = $this->buildAfterSaleItemsFromRefund($refund); + + $applyType = (int)($refund['apply_type'] ?? $refund['refund_type'] ?? 0); + if ($applyType == 1) { + $typeStr = '仅退款'; + $goodStatus = 'BUYER_NOT_RECEIVED'; + } else { + $typeStr = '普通退货'; + $goodStatus = 'BUYER_RETURNED_GOODS'; + } + + if ($applyType == 1) { + $items = []; + } elseif (is_array($items)) { + foreach ($items as $idx => $it) { + if (is_array($it)) { + $it['type'] = '退货'; + $items[$idx] = $it; + } + } + } + + $biz = [ + 'shop_id' => $shopid, + 'so_id' => (string)($order['order_id'] ?? ''), + 'outer_as_id' => $outerAsId, + 'type' => $typeStr, + 'good_status' => $goodStatus, + 'shop_status' => 'wait', + 'total_amount' => (float)($order['pay_price'] ?? 0), + 'payment' => 0.00, + 'question_type' => (string)($refund['refund_reason'] ?? $refund['refund_explain'] ?? ''), + 'refund' => (float)($refund['refund_price'] ?? $refund['apply_price'] ?? 0), + 'remark' => (string)($refund['remark'] ?? ''), + 'items' => $items, + ]; + + $this->retryPost($api, [$biz], 'jst.after_sale.create'); + return true; + } + + /** + * 售后单撤销推送(本地 -> 聚水潭) + * @param int $refundId + * @return bool + */ + public function afterSaleCancelPush(int $refundId): bool + { + $api = 'open/webapi/aftersaleapi/open/cancel'; + Log::info('jst.after_sale.cancel.start ' . json_encode(['refund_id' => $refundId], JSON_UNESCAPED_UNICODE)); + + /** @var StoreOrderRefundServices $storeOrderRefundServices */ + $storeOrderRefundServices = app()->make(StoreOrderRefundServices::class); + $refund = $storeOrderRefundServices->get($refundId); + if (!$refund) { + Log::warning('jst.after_sale.cancel.skip ' . json_encode(['refund_id' => $refundId, 'reason' => 'refund_missing'], JSON_UNESCAPED_UNICODE)); + return true; + } + if (method_exists($refund, 'toArray')) { + $refund = $refund->toArray(); + } + + /** @var StoreOrderServices $storeOrderServices */ + $storeOrderServices = app()->make(StoreOrderServices::class); + $order = $storeOrderServices->get((int)($refund['store_order_id'] ?? 0), ['*'], ['store']); + if (!$order) { + Log::warning('jst.after_sale.cancel.skip ' . json_encode(['refund_id' => $refundId, 'reason' => 'order_missing'], JSON_UNESCAPED_UNICODE)); + return true; + } + + if (($order['product_type'] ?? 0) != 4) { + if (($order['product_type'] ?? 0) > 0 || !($order['paid'] ?? 0)) { + Log::info('jst.after_sale.cancel.skip ' . json_encode([ + 'refund_id' => $refundId, + 'reason' => 'product_filter', + 'product_type' => $order['product_type'] ?? null, + 'paid' => $order['paid'] ?? null, + ], JSON_UNESCAPED_UNICODE)); + return true; + } + } + + $shopid = $this->resolveShopIdByOrder($order); + if (!$shopid) { + Log::info('jst.after_sale.cancel.skip ' . json_encode([ + 'refund_id' => $refundId, + 'reason' => 'missing_shopid', + 'supplier_id' => $order['supplier_id'] ?? 0 + ], JSON_UNESCAPED_UNICODE)); + return true; + } + + $outerAsId = (string)($refund['order_id'] ?? ''); + if ($outerAsId === '') { + $outerAsId = (string)$refundId; + } + + $erpInfo = []; + if (!empty($refund['erp'])) { + $erpInfo = json_decode($refund['erp'], true) ?: []; + } + $asId = $erpInfo['as_id'] ?? 0; + + if ($asId) { + $biz = [ + 'as_ids' => [(int)$asId], + ]; + } else { + $biz = [ + 'shop_id' => $shopid, + 'so_id' => (string)($order['order_id'] ?? ''), + 'outer_as_id' => $outerAsId, + 'reason' => (string)($refund['refuse_reason'] ?? $refund['refund_reason'] ?? ''), + ]; + } + + $this->retryPost($api, $biz, 'jst.after_sale.cancel'); + return true; + } + + /** + * 售后单确认退款推送(本地 -> 聚水潭) + * @param int $refundId + * @return bool + */ + public function afterSaleRefundedPush(int $refundId): bool + { + $api = 'open/webapi/aftersaleapi/open/confirm'; + Log::info('jst.after_sale.refunded.start ' . json_encode(['refund_id' => $refundId], JSON_UNESCAPED_UNICODE)); + + /** @var StoreOrderRefundServices $storeOrderRefundServices */ + $storeOrderRefundServices = app()->make(StoreOrderRefundServices::class); + $refund = $storeOrderRefundServices->get($refundId); + if (!$refund) { + Log::warning('jst.after_sale.refunded.skip ' . json_encode(['refund_id' => $refundId, 'reason' => 'refund_missing'], JSON_UNESCAPED_UNICODE)); + return true; + } + if (method_exists($refund, 'toArray')) { + $refund = $refund->toArray(); + } + + /** @var StoreOrderServices $storeOrderServices */ + $storeOrderServices = app()->make(StoreOrderServices::class); + $order = $storeOrderServices->get((int)($refund['store_order_id'] ?? 0), ['*'], ['store']); + if (!$order) { + Log::warning('jst.after_sale.refunded.skip ' . json_encode(['refund_id' => $refundId, 'reason' => 'order_missing'], JSON_UNESCAPED_UNICODE)); + return true; + } + + if (($order['product_type'] ?? 0) != 4) { + if (($order['product_type'] ?? 0) > 0 || !($order['paid'] ?? 0)) { + Log::info('jst.after_sale.refunded.skip ' . json_encode([ + 'refund_id' => $refundId, + 'reason' => 'product_filter', + 'product_type' => $order['product_type'] ?? null, + 'paid' => $order['paid'] ?? null, + ], JSON_UNESCAPED_UNICODE)); + return true; + } + } + + $shopid = $this->resolveShopIdByOrder($order); + if (!$shopid) { + Log::info('jst.after_sale.refunded.skip ' . json_encode([ + 'refund_id' => $refundId, + 'reason' => 'missing_shopid', + 'supplier_id' => $order['supplier_id'] ?? 0 + ], JSON_UNESCAPED_UNICODE)); + return true; + } + + $outerAsId = (string)($refund['order_id'] ?? ''); + if ($outerAsId === '') { + $outerAsId = (string)$refundId; + } + + $erpInfo = []; + if (!empty($refund['erp'])) { + $erpInfo = json_decode($refund['erp'], true) ?: []; + } + $asId = $erpInfo['as_id'] ?? 0; + + if ($asId) { + $biz = [ + 'as_ids' => [(int)$asId], + 'exchange_force' => true, + 'confirm_refund' => true, + ]; + } else { + $biz = [ + 'shop_id' => $shopid, + 'so_id' => (string)($order['order_id'] ?? ''), + 'outer_as_id' => $outerAsId, + 'amount' => sprintf('%.2f', (float)($refund['refund_price'] ?? $refund['apply_price'] ?? 0)), + ]; + } + + $this->retryPost($api, $biz, 'jst.after_sale.refunded'); + return true; + } + + public function getItems(int $oid, int $uid, string $unique, $rftype = ''): array + { + /** @var StoreOrderCartInfoServices $storeOrderCartInfoServices */ + $storeOrderCartInfoServices = app()->make(StoreOrderCartInfoServices::class); + $cartInfo = $storeOrderCartInfoServices->getOrderCartInfo($oid); + if (empty($cartInfo)) { + /** @var StoreOrderServices $orderServices */ + $orderServices = app()->make(StoreOrderServices::class); + $orderInfo = $orderServices->getCacheOrderInfo($uid, $unique); + $cartInfo = $orderInfo['cartInfo'] ?? []; + } + $items = []; + foreach ($cartInfo as $cart) { + $cart = $cart['cart_info'] ?? $cart; + $attrInfo = $cart['productInfo']['attrInfo']; + $truePrice = (string)($cart['sum_true_price'] ?? $cart['truePrice']); + $itemdet = [ + 'sku_id' => $attrInfo['code'] ?: ($attrInfo['unique'] . 'ok'), + 'shop_sku_id' => $attrInfo['code'] ?: ($attrInfo['unique'] . 'ok'), + 'base_price' => (float)($attrInfo['price'] ?? 0), + 'price' => (float)bcdiv($truePrice, (string)$cart['cart_num'], 2), + 'amount' => (float)$truePrice, + 'qty' => $cart['cart_num'], + 'pic' => $cart['productInfo']['image'], + 'name' => $cart['productInfo']['store_name'], + 'properties_value' => $attrInfo['suk'], + 'outer_oi_id' => $cart['product_attr_unique'], + 'refund_qty' => $rftype == 'refund_nosend' ? $cart['cart_num'] : ($cart['refund_num'] ?? 0), + ]; + if ($itemdet['qty'] == $itemdet['refund_qty']) { + $itemdet['refund_status'] = 'success'; + } + $items[] = $itemdet; + } + return $items; + } + + private function resolveShopIdByOrder($order): int + { + $rid = $order['supplier_id'] ?? 0; + $shopid = (int)$this->getShopId(); + if ($rid) { + /** @var SystemSupplierServices $systemSupplierServices */ + $systemSupplierServices = app()->make(SystemSupplierServices::class); + $info = $systemSupplierServices->getSupplierInfo((int)$rid, 'erp_shopid'); + if (!$info || !$info['erp_shopid']) { + return 0; + } + $shopid = (int)$info['erp_shopid']; + } + return (int)$shopid; + } + + private function buildAfterSaleItemsFromRefund(array $refund): array + { + $cartInfo = $refund['cart_info'] ?? []; + if (is_string($cartInfo)) { + $decoded = json_decode($cartInfo, true); + if (is_array($decoded)) { + $cartInfo = $decoded; + } + } + if (!is_array($cartInfo)) { + $cartInfo = []; + } + + $items = []; + foreach ($cartInfo as $cart) { + $cart = $cart['cart_info'] ?? $cart; + $productInfo = $cart['productInfo'] ?? []; + $attrInfo = $productInfo['attrInfo'] ?? []; + $skuId = (string)($attrInfo['code'] ?? ''); + if ($skuId === '') { + $skuId = (string)($attrInfo['unique'] ?? ''); + if ($skuId !== '') { + $skuId .= 'ok'; + } + } + $items[] = [ + 'sku_id' => $skuId, + 'qty' => (int)($cart['cart_num'] ?? 0), + 'price' => sprintf('%.2f', (float)($attrInfo['price'] ?? 0)), + 'amount' => sprintf('%.2f', (float)($cart['sum_true_price'] ?? 0)), + 'outer_oi_id' => (string)($cart['product_attr_unique'] ?? ''), + 'name' => (string)($productInfo['store_name'] ?? ''), + ]; + } + return $items; + } + + private function retryPost(string $api, array $biz, string $logPrefix): void + { + $meta = $this->extractBizMeta($biz); + $num = 3; + for ($i = $num; $i >= 0; $i--) { + if ($i <= 0) { + Log::error($logPrefix . '.failed ' . json_encode(array_merge([ + 'api' => $api, + ], $meta), JSON_UNESCAPED_UNICODE)); + return; + } + try { + $rt = $this->post($api, $biz); + return; + } catch (\Throwable $e) { + usleep(1000 * 50); + Log::error($logPrefix . '.error ' . $e->getMessage() . ' ' . json_encode(array_merge([ + 'try' => ($num - $i + 1), + 'api' => $api, + ], $meta), JSON_UNESCAPED_UNICODE)); + } + } + } + + private function extractBizMeta(array $biz): array + { + $first = $biz; + if ($this->isListArray($biz) && isset($biz[0]) && is_array($biz[0])) { + $first = $biz[0]; + } + if (!is_array($first)) { + return []; + } + $meta = []; + foreach (['shop_id', 'so_id', 'outer_as_id', 'as_ids'] as $k) { + if (array_key_exists($k, $first)) { + $meta[$k] = $first[$k]; + } + } + return $meta; + } + + private function isListArray(array $arr): bool + { + if ($arr === []) { + return true; + } + return array_keys($arr) === range(0, count($arr) - 1); + } + + private function getAddress($address): array + { + if (is_string($address)) { + $decoded = json_decode($address, true); + if (is_array($decoded)) { + $address = $decoded; + } else { + $parts = preg_split('/\s+/u', trim($address)); + if (is_array($parts) && count($parts) >= 3) { + $address = [ + 'province' => $parts[0] ?? '', + 'city' => $parts[1] ?? '', + 'area' => $parts[2] ?? '', + 'detail' => trim(implode(' ', array_slice($parts, 3))), + ]; + if (($address['detail'] ?? '') === '') { + $address['detail'] = $parts[2] ?? ''; + } + } else { + $address = ['detail' => $address]; + } + } + } + if (!is_array($address)) { + $address = []; + } + return [ + 'receiver_address' => $address['detail'] ?? '', + 'receiver_state' => $address['province'] ?? '', + 'receiver_city' => $address['city'] ?? '', + 'receiver_district' => $address['area'] ?? '', + 'receiver_zip' => $address['post_code'] ?? '', + ]; + } + + public static function getServices(): HaichaoJstServices + { + return new HaichaoJstServices(); + } + + public function getProductListShop($key = ''): array + { + $time = 1; + $data = $this->post('open/skumap/query', [ + 'modified_begin' => date('Y-m-d H:i', (time() - (86400 * $time))), + 'modified_end' => date('Y-m-d H:i', time() - (86400 * $time) + 7 * 86400), + 'page_index' => 1, + 'page_size' => 50, + 'shop_id' => $key + ])['data']; + return $data; + } + + public function getProductListName($key = ''): array + { + $data = $this->post('open/sku/query', [ + 'exactly_name' => $key, + 'page_index' => 1, + 'page_size' => 10, + ])['data']; + return $data; + } + + public function syncProductSingle($data, $pid = 0) + { + $jstProductSync = app()->make(\app\haichao\sync\JstProductSync::class); + return $jstProductSync->syncProductSingle($data, $pid); + } + + public function getProductListSku($key = ''): array + { + $data = $this->post('open/sku/query', [ + 'sku_ids' => $key, + 'page_index' => 1, + 'page_size' => 10, + ])['data']; + return $data; + } + + public function getProductListKuan($key = ''): array + { + $data = $this->post('open/mall/item/query', [ + 'i_ids' => [$key], + 'page_index' => 1, + 'page_size' => 10, + 'date_field' => '', + ])['data']; + return $data; + } + + public function getProductListStand(): array + { + $time = 10; + $data = $this->post('open/mall/item/query', [ + 'modified_begin' => date('Y-m-d H:i', (time() - (86400 * $time))), + 'modified_end' => date('Y-m-d H:i', time() - (86400 * $time) + 7 * 86400), + 'i_ids' => [], + 'page_index' => 1, + 'page_size' => 50, + 'date_field' => '', + ])['data']; + return $data; + } + + public function getProductList(): array + { + $time = 1; + if (file_exists(root_path() . 'runtime/jstdata/synctime.txt')) { + $time = file_get_contents(root_path() . 'runtime/jstdata/synctime.txt'); + } + + $syncint = 0; + if (file_exists(root_path() . 'runtime/jstdata/syncint.txt')) { + $syncint = file_get_contents(root_path() . 'runtime/jstdata/syncint.txt'); + } + $syncint++; // 这里的 $syncint 实际上代表要拉取的页码(page_index) + + $data = $this->post('open/mall/item/query', [ + 'modified_begin' => date('Y-m-d H:i', (time() - (86400 * $time))), + 'modified_end' => date('Y-m-d H:i', time() - (86400 * $time) + 7 * 86400), + 'i_ids' => [], + 'page_index' => $syncint, // 将写死的 1 改为动态的页码变量 + 'page_size' => 10, + 'date_field' => '', + ])['data']; + + if ($syncint > $data['page_count']) { + $syncint = 1; + $time += 7; + if ($time > 500) { + $time = 1; + } + } + + if (!is_dir(root_path() . 'runtime/jstdata')) { + mkdir(root_path() . 'runtime/jstdata', 0777, true); + } + file_put_contents(root_path() . 'runtime/jstdata/syncint.txt', $syncint); + file_put_contents(root_path() . 'runtime/jstdata/synctime.txt', $time); + return $data; + } + + public function getStork(string $ids): array + { + return $this->post('open/inventory/query', [ + 'page_index' => 1, + 'page_size' => 100, + 'sku_ids' => $ids, + ]); + } + + public function getInneropenItemList(): array + { + $return = []; + $n = 1; + if (file_exists(root_path() . 'runtime/jstdata/syncintgood.txt')) { + $n = file_get_contents(root_path() . 'runtime/jstdata/syncintgood.txt'); + } + $data = $this->post('open/api/goods/inneropen/goods/querygoodslist', [ + 'page_num' => $n, + 'page_size' => 10, + 'sort' => 'ASC' + ])['data']; + $result = $data['list']; + $return = array_merge($result, $return); + if (file_exists(root_path() . 'runtime/jstdata/syncintgood.txt')) { + $n = file_get_contents(root_path() . 'runtime/jstdata/syncintgood.txt'); + $n++; + if ($n > ceil($data['total'] / 10)) { + $n = 1; + } + } + if (!is_dir(root_path() . 'runtime/jstdata')) { + mkdir(root_path() . 'runtime/jstdata', 0777, true); + } + file_put_contents(root_path() . 'runtime/jstdata/syncintgood.txt', $n); + return $return; + } + + public function getInneropenItemDetail($skuId): array + { + return $this->post('open/api/goods/inneropen/goods/querygoodsdetail', [ + 'item_spuId' => $skuId + ])['data']; + } + + public function findStockBySkuId($skuid = ''): array + { + $result = $this->post('open/inventory/query', [ + 'page_num' => 1, + 'page_size' => 500, + 'sku_ids' => $skuid, + ])['data']['inventorys']; + return $result; + } + + public function getInneropenStockAndDetailList($styleCodes): 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)) // 实际可用库存 + ]; + } + $rt = array_merge($formatResult, $rt); + usleep(550); + } while (count($result) == 100); + return $rt; + } + + public function findAllInneropenItemBySkuId(string $skuIds): array + { + return $this->getInneropenStockAndDetailList($skuIds); + } + + public function getInneropenList(int $status = 2): array + { + $rt = $this->post('open/api/company/inneropen/partner/channel/querymysupplier', [ + 'page_num' => 1, + 'page_size' => 100, + ])['data']['supplier_vos']; + $rtr = []; + foreach ($rt as $inneropen) { + if ($inneropen['status'] == $status) { + $rtr[] = $inneropen; + } + } + return $rtr; + } + + public function getSyncProduct(): \app\haichao\sync\JstProductSync + { + return new \app\haichao\sync\JstProductSync(); + } + + public function getSyncStork(): \app\haichao\sync\JstStorkSync + { + return new \app\haichao\sync\JstStorkSync(); + } +} diff --git a/app/haichao/callback/JstCallbackEventManager.php b/app/haichao/callback/JstCallbackEventManager.php new file mode 100644 index 0000000..e00e776 --- /dev/null +++ b/app/haichao/callback/JstCallbackEventManager.php @@ -0,0 +1,40 @@ + JstProductEvent::class, + 'trade_after_sale' => JstTradeEvent::class, + 'orderCancel' => JstOrderCancelEvent::class, + 'orderPost' => JstOrderPostEvent::class, + 'orderRefund' => JstOrderRefundEvent::class, + ]; + + public static function callback(Request $request): Json + { + $actionCode = $request->getMore([['action_code', false]])['action_code'] ?? ''; + Log::info('JST callback ' . $actionCode); + if ($actionCode && isset(self::$eventList[$actionCode])) { + $reflectionClass = new ReflectionClass(self::$eventList[$actionCode]); + /** @var JstEvent $eventObj */ + $eventObj = $reflectionClass->newInstance(); + $eventObj->event($request); + return json(['code' => 0, 'msg' => 'ok']); + } + throw new ValidateException('unknown action_code'); + } +} diff --git a/app/haichao/callback/event/JstEvent.php b/app/haichao/callback/event/JstEvent.php new file mode 100644 index 0000000..43b211c --- /dev/null +++ b/app/haichao/callback/event/JstEvent.php @@ -0,0 +1,10 @@ +getMore([['so_id', false]])['so_id']; + if ($soId) { + /** @var OrderServices $orderServices */ + $orderServices = app()->make(OrderServices::class); + $orderServices->cancelCallback($request->postMore([ + ['so_id', 0], + ['remark', ''], + ])); + } + return []; + } +} diff --git a/app/haichao/callback/event/JstOrderPostEvent.php b/app/haichao/callback/event/JstOrderPostEvent.php new file mode 100644 index 0000000..434c28b --- /dev/null +++ b/app/haichao/callback/event/JstOrderPostEvent.php @@ -0,0 +1,296 @@ +getContent(); + if ($content !== '') { + $decoded = json_decode($content, true); + if (is_array($decoded)) { + $payload = $decoded; + } else { + $output = []; + parse_str($content, $output); + if (is_array($output)) { + $payload = $output; + } + } + } + if (!$payload) { + $payload = $request->param(); + } + + $biz = []; + if (isset($payload['biz']) && $payload['biz'] !== '') { + if (is_array($payload['biz'])) { + $biz = $payload['biz']; + } else { + $bizDecoded = json_decode((string)$payload['biz'], true); + if (is_array($bizDecoded)) { + $biz = $bizDecoded; + } + } + } + + $oId = $request->getMore([['o_id', false]])['o_id'] ?: (string)($payload['o_id'] ?? $biz['o_id'] ?? ($payload['oId'] ?? $biz['oId'] ?? '')); + $logisticsCompany = $request->getMore([['logistics_company', false]])['logistics_company'] ?: (string)($payload['logistics_company'] ?? $biz['logistics_company'] ?? ($payload['logisticsCompany'] ?? $biz['logisticsCompany'] ?? '')); + $lId = $request->getMore([['l_id', false]])['l_id'] ?: (string)($payload['l_id'] ?? $biz['l_id'] ?? ($payload['lId'] ?? $biz['lId'] ?? '')); + $lcId = $request->getMore([['lc_id', false]])['lc_id'] ?: (string)($payload['lc_id'] ?? $biz['lc_id'] ?? ($payload['lcId'] ?? $biz['lcId'] ?? '')); + $soId = $request->getMore([['so_id', false]])['so_id'] ?: (string)($payload['so_id'] ?? $biz['so_id'] ?? ($payload['soId'] ?? $biz['soId'] ?? '')); + $items = $request->getMore([['items', false]])['items'] ?: ($payload['items'] ?? $biz['items'] ?? ($payload['item_list'] ?? $biz['item_list'] ?? [])); + if (!is_array($items)) { + $items = []; + } + + $skus = []; + if (is_array($items)) { + foreach ($items as $it) { + $skus[] = ['sku' => $it['sku_id'] ?? '', 'qty' => (int)($it['qty'] ?? 0)]; + } + } + Log::info('jst.cb.orderPost.start ' . json_encode([ + 'so_id' => $soId, + 'o_id' => $oId, + 'logistics_company' => $logisticsCompany, + 'l_id' => $lId, + 'lc_id' => $lcId, + 'item_count' => is_array($items) ? count($items) : 0, + 'sku_list' => $skus, + ], JSON_UNESCAPED_UNICODE)); + + /** @var StoreOrderServices $storeOrderServices */ + $storeOrderServices = app()->make(StoreOrderServices::class); + /** @var StoreOrderDeliveryServices $orderDeliverServices */ + $orderDeliverServices = app()->make(StoreOrderDeliveryServices::class); + /** @var StoreOrderRefundServices $storeOrderRefundServices */ + $storeOrderRefundServices = app()->make(StoreOrderRefundServices::class); + /** @var StoreOrderCartInfoServices $storeOrderCartInfoServices */ + $storeOrderCartInfoServices = app()->make(StoreOrderCartInfoServices::class); + + if ($soId === '') { + Log::info('jst.cb.orderPost.skip ' . json_encode(['reason' => 'missing so_id'], JSON_UNESCAPED_UNICODE)); + return 'missing so_id'; + } + + $orderInfo = $storeOrderServices->getOne(['order_id' => $soId]); + if (!$orderInfo) throw new ValidateException('order not found ' . $soId); + $oid = $orderInfo['id']; + $splitOrderList = $storeOrderServices->getSplitOrderList(['pid' => $oid]); + $nol = []; + foreach ($splitOrderList as $splitOrder) { + if ($splitOrder['status'] === 0) $nol[] = $splitOrder; + } + $splitOrderList = $nol; + Log::info('jst.cb.orderPost.split.count ' . json_encode(['so_id' => $soId, 'count' => count($splitOrderList)], JSON_UNESCAPED_UNICODE)); + + $cardIds = $orderInfo['cart_id']; + $num = 0; + foreach ($cardIds as $cardId) { + $cardInfo = $storeOrderCartInfoServices->getOne(['cart_id' => $cardId])->toArray(); + $num += $cardInfo['cart_num']; + } + $num2 = 0; + foreach ($items as $item) { + $num2 += $item['qty']; + } + if ($num !== $num2) { + Log::warning('jst.cb.orderPost.qty_mismatch ' . json_encode(['so_id' => $soId, 'order_cart_num' => $num, 'ship_qty_sum' => $num2], JSON_UNESCAPED_UNICODE)); + } + + $existSo = false; + if ($splitOrderList) { + foreach ($splitOrderList as $splitOrder) { + if ($splitOrder['order_id'] == $soId) { + $existSo = true; + } + } + } else { + $existSo = true; + } + if (!$existSo) { + Log::warning('jst.cb.orderPost.no_exist_so ' . json_encode(['so_id' => $soId], JSON_UNESCAPED_UNICODE)); + foreach ($splitOrderList as $item) { + foreach ($item['cart_id'] as $cartId) { + $cartInfo = $item['_info'][$cartId]['cart_info']; + $productInfo = $cartInfo['productInfo']; + $cartfa = []; + foreach ($items as $sentProduct) { + if ($productInfo['attrInfo']['suk'] == $sentProduct['sku_id'] || + $productInfo['attrInfo']['code'] == $sentProduct['sku_id'] || + ($productInfo['attrInfo']['unique'] . 'ok') == $sentProduct['sku_id']) { + $cartfa[] = [ + 'cart_id' => $cartId, + 'cart_num' => $sentProduct['qty'], + ]; + } + } + try { + $deliverArgs = [ + 'type' => 1, + 'delivery_name' => $logisticsCompany, + 'delivery_id' => $lId, + 'delivery_code' => $lcId, + 'express_record_type' => "1", + 'express_temp_id' => md5(time()), + 'cart_ids' => $cartfa + ]; + $orderDeliverServices->splitDelivery($item['id'], $deliverArgs, 0, true); + Log::info('jst.cb.orderPost.success ' . json_encode(['so_id' => $soId, 'oid' => (int)$item['id'], 'mode' => 'split'], JSON_UNESCAPED_UNICODE)); + return 'ok'; + } catch (\Throwable $exception) { + Log::error('jst.cb.orderPost.split.error ' . json_encode(['oid' => $item['id'], 'msg' => $exception->getMessage()], JSON_UNESCAPED_UNICODE)); + throw new ValidateException('split delivery error ' . $exception->getMessage()); + } + } + } + Log::warning('jst.cb.orderPost.no_match.split ' . json_encode(['so_id' => $soId], JSON_UNESCAPED_UNICODE)); + return 'no match'; + } + try { + if (count($splitOrderList) > 1 || count($orderInfo['cart_id']) > 1) { + if (count($splitOrderList) > 0) { + foreach ($splitOrderList as $item) { + foreach ($item['cart_id'] as $cartId) { + $cartInfo = $item['_info'][$cartId]['cart_info']; + $productInfo = $cartInfo['productInfo']; + foreach ($items as $postItem) { + $postItemSku = $postItem['sku_id']; + if ($productInfo['attrInfo']['suk'] == $postItemSku || $productInfo['attrInfo']['code'] == $postItemSku) { + try { + $deliverArgs = [ + 'type' => 1, + 'delivery_name' => $logisticsCompany, + 'delivery_id' => $lId, + 'delivery_code' => $lcId, + 'express_record_type' => "1", + 'express_temp_id' => md5(time()), + 'cart_ids' => [ + [ + 'cart_id' => $cartId, + 'cart_num' => $cartInfo['cart_num'], + ] + ] + ]; + $orderDeliverServices->splitDelivery($oid, $deliverArgs, 0, true); + Log::info('jst.cb.orderPost.success ' . json_encode(['so_id' => $soId, 'oid' => (int)$oid, 'mode' => 'split.pid', 'cart_id' => (int)$cartId], JSON_UNESCAPED_UNICODE)); + return 'ok'; + } catch (\Throwable $exception) { + Log::error('jst.cb.orderPost.split.pid.error ' . json_encode(['pid' => $oid, 'cart_id' => $cartId, 'msg' => $exception->getMessage()], JSON_UNESCAPED_UNICODE)); + throw new ValidateException('split delivery error ' . $exception->getMessage()); + } + } + } + } + } + Log::warning('jst.cb.orderPost.no_match.pid ' . json_encode(['so_id' => $soId, 'pid' => (int)$oid], JSON_UNESCAPED_UNICODE)); + return 'no match'; + } + if (count($orderInfo['cart_id']) > 1) { + $cartfa = []; + foreach ($items as $sentProduct) { + foreach ($orderInfo['cart_id'] as $cartId) { + $cartInfo = $storeOrderCartInfoServices->getOne(['cart_id' => $cartId]); + $productInfo = $cartInfo['cart_info']['productInfo'] ?? []; + if (!$productInfo) continue; + if ($productInfo['attrInfo']['suk'] == $sentProduct['sku_id'] || + $productInfo['attrInfo']['code'] == $sentProduct['sku_id'] || + ($productInfo['attrInfo']['unique'] . 'ok') == $sentProduct['sku_id']) { + $cartfa[] = [ + 'cart_id' => $cartId, + 'cart_num' => $cartInfo['cart_num'], + ]; + } + } + } + Log::info('jst.cb.orderPost.multi.mapping ' . json_encode(['pid' => $oid, 'match' => $cartfa], JSON_UNESCAPED_UNICODE)); + try { + $deliverArgs = [ + 'type' => 1, + 'delivery_id' => $lId, + 'delivery_code' => $lcId, + 'express_record_type' => "1", + 'express_temp_id' => md5(time()), + 'cart_ids' => $cartfa + ]; + $orderDeliverServices->splitDelivery($oid, $deliverArgs, 0, true); + Log::info('jst.cb.orderPost.success ' . json_encode(['so_id' => $soId, 'oid' => (int)$oid, 'mode' => 'multi'], JSON_UNESCAPED_UNICODE)); + return 'ok'; + } catch (\Throwable $exception) { + Log::error('jst.cb.orderPost.multi.error ' . json_encode(['pid' => $oid, 'msg' => $exception->getMessage()], JSON_UNESCAPED_UNICODE)); + throw new ValidateException('split delivery error ' . $exception->getMessage()); + } + } + } else { + if ($num == $num2) { + $deliverArgs = [ + 'type' => 1, + 'delivery_name' => $logisticsCompany, + 'delivery_id' => $lId, + 'delivery_code' => $lcId, + 'express_record_type' => "1", + 'express_temp_id' => md5(time()), + ]; + $orderDeliverServices->delivery($oid, $deliverArgs); + Log::info('jst.cb.orderPost.success ' . json_encode(['so_id' => $soId, 'oid' => (int)$oid, 'mode' => 'delivery'], JSON_UNESCAPED_UNICODE)); + return 'ok'; + } + } + } catch (\Throwable $e) { + if ($e->getMessage() != '订单已发货请勿重复操作!') { + Log::error('jst.cb.orderPost.error ' . json_encode(['oid' => $oid, 'msg' => $e->getMessage()], JSON_UNESCAPED_UNICODE)); + throw new ValidateException($e->getMessage()); + } + Log::info('jst.cb.orderPost.repeat ' . json_encode(['so_id' => $soId, 'oid' => (int)$oid], JSON_UNESCAPED_UNICODE)); + return 'ok'; + } + $cardIds = $orderInfo['cart_id']; + if (count($cardIds) >= 1) { + $cartfa = []; + foreach ($cardIds as $cartId) { + $cardInfo = $storeOrderCartInfoServices->getOne(['cart_id' => $cartId])->toArray(); + $productInfo = $cardInfo['cart_info']['productInfo'] ?? []; + foreach ($items as $sentProduct) { + if ($productInfo['attrInfo']['suk'] == $sentProduct['sku_id'] || + $productInfo['attrInfo']['code'] == $sentProduct['sku_id'] || + ($productInfo['attrInfo']['unique'] . 'ok') == $sentProduct['sku_id']) { + $cartfa[] = [ + 'cart_id' => $cartId, + 'cart_num' => $sentProduct['qty'], + ]; + } + } + } + try { + $deliverArgs = [ + 'type' => 1, + 'delivery_name' => $logisticsCompany, + 'delivery_id' => $lId, + 'delivery_code' => $lcId, + 'express_record_type' => "1", + 'express_temp_id' => md5(time()), + 'cart_ids' => $cartfa + ]; + $orderDeliverServices->splitDelivery($oid, $deliverArgs); + Log::info('jst.cb.orderPost.success ' . json_encode(['so_id' => $soId, 'oid' => (int)$oid, 'mode' => 'cartfa'], JSON_UNESCAPED_UNICODE)); + return 'ok'; + } catch (\Throwable $exception) { + Log::error('jst.cb.orderPost.cartfa.error ' . json_encode(['pid' => $oid, 'msg' => $exception->getMessage()], JSON_UNESCAPED_UNICODE)); + throw new ValidateException('split delivery error ' . $exception->getMessage()); + } + } + Log::warning('jst.cb.orderPost.unknown ' . json_encode(['so_id' => $soId], JSON_UNESCAPED_UNICODE)); + throw new ValidateException('unknown delivery ' . $soId); + } +} diff --git a/app/haichao/callback/event/JstOrderRefundEvent.php b/app/haichao/callback/event/JstOrderRefundEvent.php new file mode 100644 index 0000000..70aacd6 --- /dev/null +++ b/app/haichao/callback/event/JstOrderRefundEvent.php @@ -0,0 +1,27 @@ +getMore(['action_name'])['action_name']; + switch ($actionName) { + case '确认收货': + { + /** @var StoreOrderServices $storeOrderServices */ + $storeOrderServices = app()->make(StoreOrderServices::class); + $orderInfo = $storeOrderServices->getOne(['erp_id' => $request->getMore([['o_id', -1]])['o_id']]); + if ($orderInfo) { + $storeOrderServices->update(['id' => $orderInfo['id']], ['status' => 3]); + } + break; + } + } + return []; + } +} diff --git a/app/haichao/callback/event/JstProductEvent.php b/app/haichao/callback/event/JstProductEvent.php new file mode 100644 index 0000000..3b50d55 --- /dev/null +++ b/app/haichao/callback/event/JstProductEvent.php @@ -0,0 +1,48 @@ +param(); + $body = json_decode((string)($param['biz'] ?? '{}'), true); + $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); + $skuId = (string)($body['skuId'] ?? ''); + $stock = (int)($body['qty'] ?? 0); + } + if (!$skuId) 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, [ + 'stock' => $stock, + 'sum_stock' => $stock + ]); + } + } + } catch (Throwable $e) { + Log::error('jst.cb.product_stock.error ' . json_encode([ + 'msg' => $e->getMessage(), + ], JSON_UNESCAPED_UNICODE)); + } + return []; + } +} diff --git a/app/haichao/callback/event/JstTradeEvent.php b/app/haichao/callback/event/JstTradeEvent.php new file mode 100644 index 0000000..520dbff --- /dev/null +++ b/app/haichao/callback/event/JstTradeEvent.php @@ -0,0 +1,298 @@ +getContent(); + if ($content !== '') { + $decoded = json_decode($content, true); + if (is_array($decoded)) { + $payload = $decoded; + } else { + $output = []; + parse_str($content, $output); + if (is_array($output)) { + $payload = $output; + } + } + } + if (!$payload) { + $payload = $request->param(); + } + + $biz = []; + if (isset($payload['biz']) && $payload['biz'] !== '') { + if (is_array($payload['biz'])) { + $biz = $payload['biz']; + } else { + $bizDecoded = json_decode((string)$payload['biz'], true); + if (is_array($bizDecoded)) { + $biz = $bizDecoded; + } + } + } + + $actionName = (string)($payload['action_name'] ?? $biz['action_name'] ?? ($payload['action'] ?? $biz['action'] ?? '')); + $shopStatus = strtoupper((string)($payload['shop_status'] ?? $biz['shop_status'] ?? ($payload['shopStatus'] ?? $biz['shopStatus'] ?? ''))); + $outerAsId = (string)($payload['outer_as_id'] ?? $biz['outer_as_id'] ?? ($payload['outerAsId'] ?? $biz['outerAsId'] ?? '')); + $soId = (string)($payload['so_id'] ?? $biz['so_id'] ?? ($payload['soId'] ?? $biz['soId'] ?? '')); + $bizStatus = strtoupper((string)($biz['status'] ?? $payload['status'] ?? '')); + $bizType = (string)($biz['type'] ?? ''); + $bizRemark = (string)($biz['remark'] ?? ''); + + Log::info('jst.cb.trade_after_sale.start ' . json_encode([ + 'action_name' => $actionName, + 'shop_status' => $shopStatus, + 'outer_as_id' => $outerAsId, + 'so_id' => $soId, + 'biz_status' => $bizStatus, + 'biz_type' => $bizType, + 'content_len' => strlen($content), + ], JSON_UNESCAPED_UNICODE)); + + if ($outerAsId === '') { + Log::info('jst.cb.trade_after_sale.skip ' . json_encode([ + 'reason' => 'missing outer_as_id', + 'shop_status' => $shopStatus, + 'action_name' => $actionName, + ], JSON_UNESCAPED_UNICODE)); + return []; + } + + try { + $applyType = 1; + if ($bizType !== '') { + if (mb_strpos($bizType, '仅退款') !== false) { + $applyType = 1; + } elseif (mb_strpos($bizType, '退货') !== false) { + $applyType = 2; + } + } + + /** @var StoreOrderServices $storeOrderServices */ + $storeOrderServices = app()->make(StoreOrderServices::class); + $soIdsToTry = array_values(array_filter(array_unique([ + $soId, + (string)(explode('_', $soId)[0] ?? ''), + ]))); + $order = null; + foreach ($soIdsToTry as $trySoId) { + $order = $storeOrderServices->getOne(['erp_order_id' => $trySoId], 'id,uid,store_id,supplier_id,order_id,erp_order_id,total_num,pay_price,pay_postage,channel,pid'); + if ($order) break; + $order = $storeOrderServices->getOne(['order_id' => $trySoId], 'id,uid,store_id,supplier_id,order_id,erp_order_id,total_num,pay_price,pay_postage,channel,pid'); + if ($order) break; + } + if (!$order) { + Log::error('jst.cb.trade_after_sale.order_missing ' . json_encode([ + 'outer_as_id' => $outerAsId, + 'so_id' => $soId, + 'biz_status' => $bizStatus, + 'biz_type' => $bizType, + ], JSON_UNESCAPED_UNICODE)); + return []; + } + + /** @var StoreOrderRefundServices $storeOrderRefundServices */ + $storeOrderRefundServices = app()->make(StoreOrderRefundServices::class); + $orderRefund = $storeOrderRefundServices->getOne(['order_id' => $outerAsId]); + if (!$orderRefund) { + /** @var StoreOrderCartInfoServices $storeOrderCartInfoServices */ + $storeOrderCartInfoServices = app()->make(StoreOrderCartInfoServices::class); + $items = $biz['items'] ?? []; + $skuQtyMap = []; + if (is_array($items)) { + foreach ($items as $item) { + if (!is_array($item)) continue; + $sku = (string)($item['sku_id'] ?? $item['skuId'] ?? ''); + $qty = (int)($item['qty'] ?? 0); + if ($sku !== '' && $qty > 0) { + $skuQtyMap[$sku] = ($skuQtyMap[$sku] ?? 0) + $qty; + } + } + } + + $targetOrder = $order; + $leftSkuQtyMap = $skuQtyMap; + $cartInfos = $storeOrderCartInfoServices->getCartColunm(['oid' => (int)$order['id']], 'id,cart_id,cart_num,cart_info'); + foreach ($cartInfos as &$cartInfo) { + $cartInfo['cart_info'] = is_string($cartInfo['cart_info']) ? json_decode($cartInfo['cart_info'], true) : $cartInfo['cart_info']; + } + unset($cartInfo); + + if ($skuQtyMap && (int)($order['pid'] ?? 0) === -1) { + $candidateOrders = [$order]; + $subOrders = $storeOrderServices->getColumn(['pid' => (int)$order['id'], 'is_system_del' => 0], 'id,uid,store_id,supplier_id,order_id,erp_order_id,total_num,pay_price,pay_postage,channel,pid'); + if ($subOrders && is_array($subOrders)) { + $candidateOrders = array_merge($candidateOrders, $subOrders); + } + + $bestScore = -1; + $bestCartInfos = $cartInfos; + $bestLeftSkuQtyMap = $skuQtyMap; + foreach ($candidateOrders as $cand) { + if (!is_array($cand) || !isset($cand['id'])) continue; + $candCartInfos = $storeOrderCartInfoServices->getCartColunm(['oid' => (int)$cand['id']], 'id,cart_id,cart_num,cart_info'); + foreach ($candCartInfos as &$candCartInfo) { + $candCartInfo['cart_info'] = is_string($candCartInfo['cart_info']) ? json_decode($candCartInfo['cart_info'], true) : $candCartInfo['cart_info']; + } + unset($candCartInfo); + + $need = $skuQtyMap; + $score = 0; + foreach ($candCartInfos as $candCart) { + $ci = $candCart['cart_info']; + if (!is_array($ci)) continue; + $sku = (string)($ci['productInfo']['attrInfo']['code'] ?? $ci['productInfo']['attrInfo']['sku'] ?? $ci['productInfo']['sku'] ?? ''); + if ($sku === '' || !isset($need[$sku]) || (int)$need[$sku] <= 0) continue; + $cartNum = (int)($candCart['cart_num'] ?? 0); + $useNum = min($cartNum, (int)$need[$sku]); + if ($useNum <= 0) continue; + $score += $useNum; + $need[$sku] -= $useNum; + } + + if ($score > $bestScore) { + $bestScore = $score; + $targetOrder = $cand; + $bestCartInfos = $candCartInfos; + $bestLeftSkuQtyMap = $need; + } + } + $cartInfos = $bestCartInfos; + $leftSkuQtyMap = $bestLeftSkuQtyMap; + Log::info('jst.cb.trade_after_sale.split_match ' . json_encode([ + 'outer_as_id' => $outerAsId, + 'so_id' => $soId, + 'best_score' => $bestScore, + 'target_store_order_id' => (int)($targetOrder['id'] ?? 0), + 'target_order_id' => (string)($targetOrder['order_id'] ?? ''), + 'target_erp_order_id' => (string)($targetOrder['erp_order_id'] ?? ''), + ], JSON_UNESCAPED_UNICODE)); + } + + $selectedCartInfo = []; + if ($skuQtyMap) { + foreach ($cartInfos as $cart) { + $ci = $cart['cart_info']; + if (!is_array($ci)) continue; + $sku = (string)($ci['productInfo']['attrInfo']['code'] ?? $ci['productInfo']['attrInfo']['sku'] ?? $ci['productInfo']['sku'] ?? ''); + if ($sku === '' || !isset($skuQtyMap[$sku])) continue; + $cartNum = (int)($cart['cart_num'] ?? 0); + $applyNum = min($cartNum, (int)$skuQtyMap[$sku]); + if ($applyNum <= 0) continue; + $ci['id'] = $ci['id'] ?? (int)($cart['id'] ?? 0); + $ci['cart_id'] = $ci['cart_id'] ?? (int)($cart['cart_id'] ?? 0); + $ci['cart_num'] = $applyNum; + $selectedCartInfo[] = $ci; + $skuQtyMap[$sku] -= $applyNum; + } + } + if (!$selectedCartInfo) { + $selectedCartInfo = array_values(array_filter(array_column($cartInfos, 'cart_info'), 'is_array')); + } + + $refundNum = 0; + $refundPrice = 0.00; + foreach ($selectedCartInfo as $ci) { + $refundNum += (int)($ci['cart_num'] ?? 0); + if (isset($ci['truePrice'])) { + $refundPrice = bcadd((string)$refundPrice, bcmul((string)$ci['truePrice'], (string)($ci['cart_num'] ?? 0), 4), 2); + } + } + if ($refundPrice <= 0 && (int)($targetOrder['total_num'] ?? 0) > 0 && $refundNum > 0) { + $refundPrice = (float)bcmul((string)$targetOrder['pay_price'], bcdiv((string)$refundNum, (string)$targetOrder['total_num'], 6), 2); + } + + $refundData = [ + 'uid' => (int)$targetOrder['uid'], + 'store_id' => (int)$targetOrder['store_id'], + 'supplier_id' => (int)($targetOrder['supplier_id'] ?? 0), + 'store_order_id' => (int)$targetOrder['id'], + 'refund_num' => $refundNum > 0 ? $refundNum : (int)($targetOrder['total_num'] ?? 0), + 'apply_type' => $applyType, + 'refund_type' => $applyType, + 'refund_goods_type' => in_array($applyType, [2, 3]) ? $applyType : 1, + 'apply_price' => $refundPrice, + 'refund_price' => $refundPrice, + 'refund_reason' => $bizRemark !== '' ? $bizRemark : '聚水潭发起售后', + 'order_id' => $outerAsId, + 'add_time' => time(), + 'cart_info' => json_encode($selectedCartInfo, JSON_UNESCAPED_UNICODE), + 'channel' => (int)($targetOrder['channel'] ?? 0), + ]; + $res = $storeOrderRefundServices->save($refundData); + $orderRefund = $storeOrderRefundServices->get((int)$res->id); + Log::info('jst.cb.trade_after_sale.refund_created ' . json_encode([ + 'outer_as_id' => $outerAsId, + 'so_id' => $soId, + 'store_order_id' => (int)$targetOrder['id'], + 'refund_id' => (int)$res->id, + 'apply_type' => $applyType, + 'refund_num' => $refundData['refund_num'], + 'refund_price' => $refundData['refund_price'], + ], JSON_UNESCAPED_UNICODE)); + } + + if ($shopStatus === 'CLOSED' || $bizStatus === 'CLOSED') { + $storeOrderRefundServices->update((int)$orderRefund['id'], [ + 'refund_type' => 3, + 'refuse_reason' => $bizRemark !== '' ? $bizRemark : '聚水潭关闭售后', + 'refunded_time' => time(), + ]); + Log::info('jst.cb.trade_after_sale.refund_closed ' . json_encode([ + 'outer_as_id' => $outerAsId, + 'so_id' => $soId, + 'refund_id' => (int)$orderRefund['id'], + ], JSON_UNESCAPED_UNICODE)); + return []; + } + + if (in_array($bizStatus, ['REFUNDED', 'REFUND_SUCCESS', 'FINISHED', 'COMPLETED'], true)) { + /** @var OrderServices $orderServices */ + $orderServices = app()->make(OrderServices::class); + $ok = (bool)$orderServices->receivedCallback(['outer_as_id' => $outerAsId, 'so_id' => $soId], 1, $soId !== ''); + Log::info('jst.cb.trade_after_sale.refund_finalize ' . json_encode([ + 'outer_as_id' => $outerAsId, + 'so_id' => $soId, + 'refund_id' => (int)$orderRefund['id'], + 'success' => $ok ? 1 : 0, + ], JSON_UNESCAPED_UNICODE)); + } else { + Log::info('jst.cb.trade_after_sale.synced ' . json_encode([ + 'outer_as_id' => $outerAsId, + 'so_id' => $soId, + 'refund_id' => (int)$orderRefund['id'], + 'apply_type' => (int)($orderRefund['apply_type'] ?? 0), + 'refund_type' => (int)($orderRefund['refund_type'] ?? 0), + ], JSON_UNESCAPED_UNICODE)); + } + } catch (\Throwable $e) { + Log::error('jst.cb.trade_after_sale.error ' . json_encode([ + 'outer_as_id' => $outerAsId, + 'so_id' => $soId, + 'action_name' => $actionName, + 'shop_status' => $shopStatus, + 'biz_status' => $bizStatus, + 'biz_type' => $bizType, + 'error' => [ + 'class' => get_class($e), + 'message' => $e->getMessage(), + 'file' => $e->getFile(), + 'line' => $e->getLine(), + ], + ], JSON_UNESCAPED_UNICODE)); + } + return []; + } +} diff --git a/app/haichao/sync/JstProductSync.php b/app/haichao/sync/JstProductSync.php new file mode 100644 index 0000000..859fb6f --- /dev/null +++ b/app/haichao/sync/JstProductSync.php @@ -0,0 +1,344 @@ +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 .= "