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

@@ -0,0 +1,54 @@
<?php
namespace app\command;
use app\services\product\product\StoreProductServices;
use think\console\Command;
use think\console\Input;
use think\console\Output;
use think\console\input\Option;
use think\facade\Db;
class BackfillProductSpu extends Command
{
protected function configure()
{
$this->setName('product:spu:backfill')
->addOption('chunk', null, Option::VALUE_REQUIRED, '单次处理条数', 200)
->addOption('limit', null, Option::VALUE_REQUIRED, '最多处理条数(0不限制)', 0)
->setDescription('Backfill empty product spu');
}
protected function execute(Input $input, Output $output)
{
$chunk = max(1, (int)$input->getOption('chunk'));
$limit = max(0, (int)$input->getOption('limit'));
$handled = 0;
/** @var StoreProductServices $productServices */
$productServices = app()->make(StoreProductServices::class);
while (true) {
$query = Db::name('store_product')->where('spu', '')->where('is_del', 0)->order('id', 'asc')->limit($chunk);
if ($limit > 0) {
$query->limit(min($chunk, $limit - $handled));
}
$rows = $query->field(['id'])->select()->toArray();
if (!$rows) {
break;
}
foreach ($rows as $row) {
$productServices->ensureSpu((int)$row['id']);
$handled++;
if ($limit > 0 && $handled >= $limit) {
break 2;
}
}
$output->info('handled: ' . $handled);
}
$output->info('done: ' . $handled);
return 0;
}
}

View File

@@ -0,0 +1,93 @@
<?php
namespace app\command;
use app\haichao\HaichaoJstServices;
use think\console\Command;
use think\console\Input;
use think\console\input\Argument;
use think\console\input\Option;
use think\console\Output;
class TestJstSpu extends Command
{
protected function configure()
{
$this->setName('test_jst_spu')
->addArgument('spu', Argument::REQUIRED, 'SPU编码')
->setDescription('测试聚水潭按SPU拉取商品接口');
}
protected function execute(Input $input, Output $output)
{
$spu = $input->getArgument('spu');
if (empty($spu)) {
$output->error('请提供SPU编码');
$output->info('用法: php think test_jst_spu SPU编码');
$output->info('示例: php think test_jst_spu test123');
return;
}
$output->info('正在调用聚水潭API获取商品信息...');
$output->info('SPU: ' . $spu);
$output->newLine();
try {
$startTime = microtime(true);
$haichaoJst = HaichaoJstServices::getServices();
$result = $haichaoJst->getProductListKuan($spu);
$endTime = microtime(true);
$costTime = round(($endTime - $startTime) * 1000, 2);
$output->info('✅ API调用成功! 耗时: ' . $costTime . 'ms');
$output->newLine();
if (empty($result)) {
$output->warning('⚠️ 未获取到任何数据');
return;
}
$output->info('📦 获取到的数据:');
$output->write(json_encode($result, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
$output->newLine();
if (isset($result['code'])) {
$output->info('响应码: ' . $result['code']);
}
if (isset($result['msg'])) {
$output->info('响应消息: ' . $result['msg']);
}
if (isset($result['datas'])) {
$output->info('商品数量: ' . count($result['datas']));
if (!empty($result['datas'])) {
$output->newLine();
$output->info('📋 第一个商品信息:');
$firstProduct = $result['datas'][0];
$output->writeln(' - SPU ID: ' . ($firstProduct['i_id'] ?? 'N/A'));
$output->writeln(' - 商品名称: ' . ($firstProduct['name'] ?? 'N/A'));
$output->writeln(' - 售价: ' . ($firstProduct['s_price'] ?? 'N/A'));
$output->writeln(' - 市场价: ' . ($firstProduct['market_price'] ?? 'N/A'));
$output->writeln(' - 成本价: ' . ($firstProduct['c_price'] ?? 'N/A'));
if (isset($firstProduct['skus']) && is_array($firstProduct['skus'])) {
$output->writeln(' - SKU数量: ' . count($firstProduct['skus']));
}
}
}
$output->newLine();
} catch (\Throwable $e) {
$output->error('❌ API调用失败!');
$output->error('错误信息: ' . $e->getMessage());
$output->error('错误文件: ' . $e->getFile() . ':' . $e->getLine());
$output->newLine();
$output->info('堆栈跟踪:');
$output->write($e->getTraceAsString());
}
}
}

View File

@@ -215,7 +215,7 @@ class Login
$verifyCode = CacheService::get('code_' . $phone);
if (!$verifyCode)
return app('json')->fail('请先获取验证码');
$verifyCode = substr($verifyCode, 0, 6);
$verifyCode = substr($verifyCode, 0, 4);
if ($verifyCode != $code) {
CacheService::delete('code_' . $phone);
return app('json')->fail('验证码错误');
@@ -262,7 +262,7 @@ class Login
$verifyCode = CacheService::get('code_' . $phone);
if (!$verifyCode)
return app('json')->fail('请先获取验证码');
$verifyCode = substr($verifyCode, 0, 6);
$verifyCode = substr($verifyCode, 0, 4);
if ($verifyCode != $code) {
CacheService::delete('code_' . $phone);
return app('json')->fail('验证码错误');

View File

@@ -996,37 +996,163 @@ class StoreProduct extends AuthController
public function sync_jst_product()
{
[$spuStr, $sync] = $this->request->postMore([
[$spuStr, $sync, $startTime, $endTime] = $this->request->postMore([
['spu_str', ""],
['sync', 0]
['sync', 0],
['start_time', ""],
['end_time', ""]
], true);
$traceId = uniqid('jst_sync_', true);
Log::info('admin.manual.jst.product.sync.request', [
'trace_id' => $traceId,
'admin_id' => $this->adminId,
'admin_type' => $this->adminType,
'supplier_id' => $this->supplierId,
'client_ip' => $this->request->ip(),
'sync' => (int)$sync,
'spu_str' => (string)$spuStr,
'start_time' => (string)$startTime,
'end_time' => (string)$endTime,
'start_time_is_iso' => is_string($startTime) && str_contains($startTime, 'T'),
'end_time_is_iso' => is_string($endTime) && str_contains($endTime, 'T'),
]);
if ($startTime && $endTime) {
$startTs = strtotime((string)$startTime);
$endTs = strtotime((string)$endTime);
if (!$startTs || !$endTs) {
Log::warning('admin.manual.jst.product.sync_by_time.invalid_time', [
'trace_id' => $traceId,
'start_time' => $startTime,
'end_time' => $endTime,
]);
return $this->fail('时间格式错误');
}
if ($endTs < $startTs) {
Log::warning('admin.manual.jst.product.sync_by_time.invalid_range', [
'trace_id' => $traceId,
'start_time' => $startTime,
'end_time' => $endTime,
]);
return $this->fail('时间范围错误');
}
if (($endTs - $startTs) > 7 * 86400) {
Log::info('admin.manual.jst.product.sync_by_time.range_too_large', [
'trace_id' => $traceId,
'start_time' => $startTime,
'end_time' => $endTime,
'range_seconds' => $endTs - $startTs,
]);
return $this->fail('时间范围不能超过7天');
}
$startTime = date('Y-m-d H:i:s', $startTs);
$endTime = date('Y-m-d H:i:s', $endTs);
if ($sync) {
Log::info('admin.manual.jst.product.sync_by_time.run', [
'trace_id' => $traceId,
'start_time' => $startTime,
'end_time' => $endTime,
'supplier_id' => $this->supplierId,
]);
$result = (new ProductSyncErp())->syncProductByTime($startTime, $endTime, (int)$this->supplierId, $traceId, 'sync');
if (!is_array($result) || empty($result['ok'])) {
return $this->fail('同步失败,请检查日志');
}
return $this->success('同步完成', $result);
} else {
Log::info('admin.manual.jst.product.sync_by_time.dispatch', [
'trace_id' => $traceId,
'start_time' => $startTime,
'end_time' => $endTime,
'supplier_id' => $this->supplierId,
]);
ProductSyncErp::dispatchDo('syncProductByTime', [$startTime, $endTime, (int)$this->supplierId, $traceId]);
return $this->success('正在按时间范围同步中,请稍后查看', ['trace_id' => $traceId]);
}
}
if (!$spuStr) {
return $this->fail('请输入ERP商品SPU');
return $this->fail('请输入ERP商品SPU或选择时间范围');
}
$spus = array_unique(array_filter(explode(',', $spuStr)));
$traceId = uniqid('jst_', true);
$spusLog = $spus;
$spusTotal = count($spus);
if ($spusTotal > 50) {
$spusLog = array_slice($spus, 0, 50);
}
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
'count' => $spusTotal,
'spus' => $spusLog,
'spus_truncated' => $spusTotal > 50
]);
if ($sync) {
$stats = [
'ok' => true,
'mode' => 'spu',
'trace_id' => $traceId,
'spu_count' => $spusTotal,
'fetched' => 0,
'created' => 0,
'updated' => 0,
'skipped' => 0,
'failed' => 0,
];
foreach ($spus as $spu) {
(new ProductSyncErp())->productFromErp($spu, (int)$this->supplierId);
$rt = (new ProductSyncErp())->productFromErp($spu, (int)$this->supplierId, $traceId);
if (is_array($rt)) {
foreach (['fetched', 'created', 'updated', 'skipped', 'failed'] as $k) {
$stats[$k] += (int)($rt[$k] ?? 0);
}
}
}
return $this->success('同步完成');
return $this->success('同步完成', $stats);
} else {
foreach ($spus as $spu) {
ProductSyncErp::dispatchDo('productFromErp', [$spu, (int)$this->supplierId]);
ProductSyncErp::dispatchDo('productFromErp', [$spu, (int)$this->supplierId, $traceId]);
}
return $this->success('正在同步中,请稍后查看');
return $this->success('正在同步中,请稍后查看', ['trace_id' => $traceId, 'task_count' => $spusTotal]);
}
}
public function upload_jst_itemsku()
{
[$ids] = $this->request->postMore([
['ids', []],
], true);
if (is_string($ids)) {
$ids = array_filter(explode(',', $ids));
}
$ids = is_array($ids) ? array_values(array_unique(array_map('intval', $ids))) : [];
if (!$ids) {
return $this->fail('请选择商品');
}
$traceId = uniqid('jst_itemsku_', true);
Log::info('admin.manual.jst.itemsku.upload.start', [
'trace_id' => $traceId,
'admin_id' => $this->adminId,
'admin_type' => $this->adminType,
'supplier_id' => $this->supplierId,
'client_ip' => $this->request->ip(),
'product_count' => count($ids),
'product_ids' => $ids,
]);
$rt = app()->make(\app\haichao\HaichaoJstServices::class)->uploadItemSkuByProductIds($ids);
Log::info('admin.manual.jst.itemsku.upload.end', [
'trace_id' => $traceId,
'upload_count' => $rt['upload_count'] ?? null,
]);
return $this->success($rt);
}
/**
* 商品批量操作
* @param StoreProductBatchProcessServices $batchProcessServices

View File

@@ -14,6 +14,7 @@ namespace app\controller\admin\v1\statistic;
use app\controller\admin\AuthController;
use app\services\statistic\OrderStatisticServices;
use think\annotation\Inject;
use think\facade\Log;
class OrderStatistic extends AuthController
{
@@ -36,6 +37,10 @@ class OrderStatistic extends AuthController
if($this->supplierId > 0){
$where['supplier_id'] = $this->supplierId;
}
$params = $this->request->param();
$paramsJson = json_encode($params, JSON_UNESCAPED_UNICODE);
$whereJson = json_encode($where, JSON_UNESCAPED_UNICODE);
Log::info('adminapi.statistic.order.get_basic supplierId:' . (int)$this->supplierId . ' params:' . ($paramsJson === false ? '' : $paramsJson) . ' where:' . ($whereJson === false ? '' : $whereJson));
$data = $this->services->getBasic($where);
return app('json')->success($data);
}

View File

@@ -86,7 +86,7 @@ class Login
$verifyCode = CacheService::get('code_' . $phone);
if (!$verifyCode)
return app('json')->fail('请先获取验证码');
$verifyCode = substr($verifyCode, 0, 6);
$verifyCode = substr($verifyCode, 0, 4);
if ($verifyCode != $captcha) {
CacheService::delete('code_' . $phone);
return app('json')->fail('验证码错误');
@@ -288,7 +288,7 @@ class Login
$verifyCode = CacheService::get('code_' . $account);
if (!$verifyCode)
return app('json')->fail('请先获取验证码');
$verifyCode = substr($verifyCode, 0, 6);
$verifyCode = substr($verifyCode, 0, 4);
if ($verifyCode != $captcha)
return app('json')->fail('验证码错误');
if (strlen(trim($password)) < 6 || strlen(trim($password)) > 16)
@@ -318,7 +318,7 @@ class Login
$verifyCode = CacheService::get('code_' . $account);
if (!$verifyCode)
return app('json')->fail('请先获取验证码');
$verifyCode = substr($verifyCode, 0, 6);
$verifyCode = substr($verifyCode, 0, 4);
if ($verifyCode != $captcha) {
return app('json')->fail('验证码错误');
}
@@ -353,7 +353,7 @@ class Login
$verifyCode = CacheService::get('code_' . $phone);
if (!$verifyCode)
return app('json')->fail('请先获取验证码');
$verifyCode = substr($verifyCode, 0, 6);
$verifyCode = substr($verifyCode, 0, 4);
if ($verifyCode != $captcha) {
return app('json')->fail('验证码错误');
}
@@ -418,7 +418,7 @@ class Login
$verifyCode = CacheService::get('code_' . $phone);
if (!$verifyCode)
return app('json')->fail('请先获取验证码');
$verifyCode = substr($verifyCode, 0, 6);
$verifyCode = substr($verifyCode, 0, 4);
if ($verifyCode != $captcha) {
CacheService::delete('code_' . $phone);
return app('json')->fail('验证码错误');
@@ -458,7 +458,7 @@ class Login
$verifyCode = CacheService::get('code_' . $phone);
if (!$verifyCode)
return app('json')->fail('请先获取验证码');
$verifyCode = substr($verifyCode, 0, 6);
$verifyCode = substr($verifyCode, 0, 4);
if ($verifyCode != $captcha)
return app('json')->fail('验证码错误');
}
@@ -488,7 +488,7 @@ class Login
$verifyCode = CacheService::get('code_' . $phone);
if (!$verifyCode)
return app('json')->fail('请先获取验证码');
$verifyCode = substr($verifyCode, 0, 6);
$verifyCode = substr($verifyCode, 0, 4);
if ($verifyCode != $captcha)
return app('json')->fail('验证码错误');
$uid = (int)$request->uid();

View File

@@ -104,7 +104,7 @@ class UserApply
$verifyCode = CacheService::get('code_' . $data['phone']);
if (!$verifyCode)
return app('json')->fail('请先获取验证码');
$verifyCode = substr($verifyCode, 0, 6);
$verifyCode = substr($verifyCode, 0, 4);
if ($verifyCode != $captcha) {
return app('json')->fail('验证码错误');
}

View File

@@ -76,7 +76,7 @@ class Channel
$verifyCode = CacheService::get('code_' . $data['phone']);
if (!$verifyCode)
return app('json')->fail('请先获取验证码');
$verifyCode = substr($verifyCode, 0, 6);
$verifyCode = substr($verifyCode, 0, 4);
if ($verifyCode != $data['code']) {
return app('json')->fail('验证码错误');
}

View File

@@ -127,7 +127,7 @@ class Wechat
$verifyCode = CacheService::get('code_' . $phone);
if (!$verifyCode)
return app('json')->fail('请先获取验证码');
$verifyCode = substr($verifyCode, 0, 6);
$verifyCode = substr($verifyCode, 0, 4);
if ($verifyCode != $captcha) {
CacheService::delete('code_' . $phone);
return app('json')->fail('验证码错误');

View File

@@ -72,7 +72,7 @@ class Routine
$verifyCode = CacheService::get('code_' . $phone);
if (!$verifyCode)
return app('json')->fail('请先获取验证码');
$verifyCode = substr($verifyCode, 0, 6);
$verifyCode = substr($verifyCode, 0, 4);
if ($verifyCode != $captcha) {
return app('json')->fail('验证码错误');
}
@@ -170,7 +170,7 @@ class Routine
$verifyCode = CacheService::get('code_' . $phone);
if (!$verifyCode)
return app('json')->fail('请先获取验证码');
$verifyCode = substr($verifyCode, 0, 6);
$verifyCode = substr($verifyCode, 0, 4);
if ($verifyCode != $captcha) {
CacheService::delete('code_' . $phone);
return app('json')->fail('验证码错误');

View File

@@ -112,7 +112,7 @@ class Wechat
$verifyCode = CacheService::get('code_' . $phone);
if (!$verifyCode)
return app('json')->fail('请先获取验证码');
$verifyCode = substr($verifyCode, 0, 6);
$verifyCode = substr($verifyCode, 0, 4);
if ($verifyCode != $captcha) {
CacheService::delete('code_' . $phone);
return app('json')->fail('验证码错误');

View File

@@ -108,6 +108,9 @@ class Common extends AuthController
*/
public function homeStatics(SupplierOrderServices $orderServices)
{
if (!(int)$this->supplierId) {
return $this->fail('登录已失效');
}
[$time] = $this->request->getMore([
['data', '', '', 'time']
], true);
@@ -123,6 +126,9 @@ class Common extends AuthController
*/
public function orderChart(SupplierOrderServices $orderServices)
{
if (!(int)$this->supplierId) {
return $this->fail('登录已失效');
}
[$time] = $this->request->getMore([
['data', '', '', 'time']
], true);
@@ -138,6 +144,9 @@ class Common extends AuthController
*/
public function orderType(SupplierOrderServices $orderServices)
{
if (!(int)$this->supplierId) {
return $this->fail('登录已失效');
}
[$time] = $this->request->getMore([
['data', '', '', 'time']
], true);
@@ -153,6 +162,9 @@ class Common extends AuthController
*/
public function orderChannel(SupplierOrderServices $orderServices)
{
if (!(int)$this->supplierId) {
return $this->fail('登录已失效');
}
[$time] = $this->request->getMore([
['data', '', '', 'time']
], true);
@@ -167,6 +179,9 @@ class Common extends AuthController
*/
public function jnotice()
{
if (!(int)$this->supplierId) {
return $this->fail('登录已失效');
}
/** @var StoreOrderServices $orderServices */
$orderServices = app()->make(StoreOrderServices::class);
$orderNum = $orderServices->storeOrderCount((int)$this->supplierId, -1, 'supplier_id');

View File

@@ -168,7 +168,7 @@ class Login
$verifyCode = CacheService::get('code_' . $account);
if (!$verifyCode)
return app('json')->fail('请先获取验证码');
$verifyCode = substr($verifyCode, 0, 6);
$verifyCode = substr($verifyCode, 0, 4);
if ($verifyCode != $captcha) {
return app('json')->fail('验证码错误');
}

View File

@@ -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));
}
}

View File

@@ -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;
}

View File

@@ -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(),

View File

@@ -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' => '规格',

View File

@@ -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

View File

@@ -42,8 +42,9 @@ class AllowOriginMiddleware implements MiddlewareInterface
$header = Config::get('cookie.header');
$origin = $request->header('origin');
if ($origin && ('' == $this->cookieDomain || strpos($origin, $this->cookieDomain)))
if ($origin && ('' === $this->cookieDomain || str_contains($origin, $this->cookieDomain))) {
$header['Access-Control-Allow-Origin'] = $origin;
}
if ($request->method(true) == 'OPTIONS') {
$response = Response::create('ok')->code(200)->header($header);
} else {

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);
}

View File

@@ -80,6 +80,12 @@ class OrderServices
$biz = array_merge($biz, $this->getAddress($order['user_address']));
Log::info('jst.orders_upload.request ' . json_encode([
'api' => 'open/jushuitan/orders/upload',
'oid' => $oid,
'biz' => $biz,
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
$num = 3;
for ($i = $num; $i >= 0; $i--) {
if ($i <= 0) {
@@ -544,7 +550,7 @@ class OrderServices
public function getShopId(int $erpShopId): int
{
if ($erpShopId < 1) {
$erpShopId = sys_config('jst_default_shopid');
$erpShopId = (int)app('sysConfig')->get('jst_default_shopid');
}
return (int)$erpShopId;

View File

@@ -727,6 +727,19 @@ class StoreOrderRefundServices extends BaseServices
OrderStatusJob::dispatch([$order['id'], 'refund_price', ['change_message' => '退款给用户:' . bcadd($online_refund_amount, $yue_refund_amount, 2) . '元; ' . $admin, 'change_manager_id' => request()->adminId(), 'change_manager_type' => 'admin']]);
return $order;
});
if ($order['private_bind_id'] && (int)$order['private_phone_status'] === 1 && $order['user_phone']) {
try {
$privacyPhoneServices = app()->make(\app\services\other\PrivacyPhoneServices::class);
if ($privacyPhoneServices->unbindForOrder((string)$order['user_phone'], (string)$order['private_bind_id'])) {
$this->storeOrderServices->update($order['id'], ['private_phone_status' => 0]);
}
} catch (\Throwable $e) {
\think\facade\Log::error('退款解绑隐私小号失败 order_id={order_id} error={error}', [
'order_id' => $order['id'],
'error' => $e->getMessage(),
]);
}
}
//订单同意退款事件
event('order.refund', [$refundData, $order, 'order_refund', (int)$id]);
return true;

View File

@@ -2027,6 +2027,19 @@ HTML;
throw new ValidateException('订单号' . $order['order_id'] . ',取消订单失败');
}
});
if ($order['private_bind_id'] && (int)$order['private_phone_status'] === 1 && $order['user_phone']) {
try {
$privacyPhoneServices = app()->make(\app\services\other\PrivacyPhoneServices::class);
if ($privacyPhoneServices->unbindForOrder((string)$order['user_phone'], (string)$order['private_bind_id'])) {
$this->dao->update($order['id'], ['private_phone_status' => 0]);
}
} catch (\Throwable $e) {
\think\facade\Log::error('取消订单解绑隐私小号失败 order_id={order_id} error={error}', [
'order_id' => $order['id'],
'error' => $e->getMessage(),
]);
}
}
//订单取消事件
event('order.cancel', [$order]);
return true;
@@ -2139,7 +2152,12 @@ HTML;
public function getExportList(array $where, array $with = [], int $limit = 0)
{
if ($limit) {
[$page] = $this->getPageValue();
$page = 1;
if (!empty($where['id'])) {
$page = 1;
} else {
[$page] = $this->getPageValue();
}
} else {
[$page, $limit] = $this->getPageValue();
}
@@ -2151,10 +2169,17 @@ HTML;
foreach ($list as &$item) {
/** @var StoreOrderCartInfoServices $orderCart */
$orderCart = app()->make(StoreOrderCartInfoServices::class);
$_info = $orderCart->getCartColunm(['oid' => $item['id']], 'cart_info', 'unique');
$_info = $orderCart->getCartColunm(['oid' => $item['id']], 'cart_id,type,relation_id,is_gift,cart_num,refund_num,cart_info', 'unique');
foreach ($_info as $k => $v) {
$cart_info = is_string($v) ? json_decode($v, true) : $v;
$cart_info = is_string($v['cart_info']) ? json_decode($v['cart_info'], true) : $v['cart_info'];
if (!isset($cart_info['productInfo'])) $cart_info['productInfo'] = [];
$cart_info['cart_num'] = $v['cart_num'] ?? ($cart_info['cart_num'] ?? 0);
$cart_info['refund_num'] = $v['refund_num'] ?? 0;
$cart_info['cart_id'] = $v['cart_id'] ?? ($cart_info['id'] ?? 0);
$cart_info['_relation_type'] = $v['type'] ?? 0;
$cart_info['_relation_id'] = $v['relation_id'] ?? 0;
$cart_info['is_gift'] = $v['is_gift'] ?? ($cart_info['is_gift'] ?? 0);
$cart_info['unique'] = $k;
$_info[$k] = $cart_info;
unset($cart_info);
}

View File

@@ -23,6 +23,35 @@ use think\facade\Log;
*/
class PrivacyPhoneServices extends BaseServices
{
/**
* 标准化AX小号:从后往前截取11位手机号
* @param string $number
* @return string
*/
private function normalizeAxNumber(string $number): string
{
if ($number === '') {
return '';
}
return strlen($number) > 11 ? substr($number, -11) : $number;
}
/**
* 日志上下文转字符串(ThinkPHP日志上下文仅支持占位符替换)
* @param mixed $value
* @return string
*/
private function toLogString($value): string
{
if (is_string($value)) {
return $value;
}
$json = json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
return $json === false ? var_export($value, true) : $json;
}
/**
* API地址
*/
@@ -122,10 +151,10 @@ class PrivacyPhoneServices extends BaseServices
'decoded' => json_decode($response, true) ?? []
];
Log::info('隐私小号API请求', [
Log::info('隐私小号API请求 url={url} params={params} result={result}', [
'url' => $url,
'params' => $params,
'result' => $result
'params' => $this->toLogString($params),
'result' => $this->toLogString($result),
]);
return $result;
@@ -267,23 +296,33 @@ class PrivacyPhoneServices extends BaseServices
return $result;
}
$bindResult = $this->axBind($number, $realPhone);
$bindNumber = $this->normalizeAxNumber($number);
if (strlen($bindNumber) !== 11) {
Log::error('隐私小号格式异常 raw_number={raw_number} bind_number={bind_number}', [
'raw_number' => $number,
'bind_number' => $bindNumber,
]);
return $result;
}
$bindResult = $this->axBind($bindNumber, $realPhone);
if (!empty($bindResult['success']) && $bindResult['code'] == 200) {
$result = [
'user_phone' => $number,
'user_phone' => $bindNumber,
'bind_id' => $realPhone,
'status' => 1
];
Log::info('隐私小号绑定成功', [
'privacy_phone' => $number,
'real_phone' => $realPhone
Log::info('隐私小号绑定成功 privacy_phone={privacy_phone} real_phone={real_phone}', [
'privacy_phone' => $bindNumber,
'real_phone' => $realPhone,
]);
}
} catch (\Exception $e) {
Log::error('隐私小号绑定异常', [
Log::error('隐私小号绑定异常 real_phone={real_phone} ax_number={ax_number} error={error}', [
'real_phone' => $realPhone,
'error' => $e->getMessage()
'ax_number' => $bindNumber ?? ($number ?? ''),
'error' => $e->getMessage(),
]);
}
@@ -306,21 +345,30 @@ class PrivacyPhoneServices extends BaseServices
return false;
}
$unbindPhone = $this->normalizeAxNumber($privacyPhone);
if (strlen($unbindPhone) !== 11) {
Log::error('隐私小号格式异常 privacy_phone={privacy_phone} unbind_phone={unbind_phone}', [
'privacy_phone' => $privacyPhone,
'unbind_phone' => $unbindPhone,
]);
return false;
}
try {
$result = $this->unAxBind($privacyPhone, $realPhone);
$result = $this->unAxBind($unbindPhone, $realPhone);
if (!empty($result['success']) && $result['code'] == 200) {
Log::info('隐私小号解绑成功', [
'privacy_phone' => $privacyPhone,
'real_phone' => $realPhone
Log::info('隐私小号解绑成功 privacy_phone={privacy_phone} real_phone={real_phone}', [
'privacy_phone' => $unbindPhone,
'real_phone' => $realPhone,
]);
return true;
}
} catch (\Exception $e) {
Log::error('隐私小号解绑异常', [
'privacy_phone' => $privacyPhone,
Log::error('隐私小号解绑异常 privacy_phone={privacy_phone} real_phone={real_phone} error={error}', [
'privacy_phone' => $unbindPhone,
'real_phone' => $realPhone,
'error' => $e->getMessage()
'error' => $e->getMessage(),
]);
}

View File

@@ -14,10 +14,14 @@ namespace app\services\other\export;
use app\model\product\stock\StockRecord;
use app\services\BaseServices;
use app\jobs\system\ExportExcelJob;
use app\model\order\StoreOrderRefund;
use app\model\order\StoreOrderStatus;
use app\services\order\StoreOrderServices;
use app\services\pay\PayServices;
use app\services\product\label\StoreProductLabelServices;
use app\services\product\product\StoreDescriptionServices;
use app\services\product\sku\StoreProductAttrResultServices;
use app\services\supplier\SystemSupplierServices;
use app\services\user\UserServices;
use crmeb\services\SpreadsheetExcelService;
@@ -729,9 +733,11 @@ class ExportServices extends BaseServices
$header = ['订单ID', '订单编号', '性别', '电话', '收货人姓名', '收货人电话', '收货地址', '商品信息', '商品总数',
'总价格', '实际支付', '邮费', '会员优惠金额', '优惠卷金额', '积分抵扣金额', '支付状态', '支付时间', '订单状态', '下单时间', '用户备注'];
$header = [
'订单号', '订单类型', '订单状态', '下单时间', '买家姓名(uid)', '买家手机号', '收货地址', '商品名称', '商品数量', '商品金额合计', '邮费',
'会员优惠', '优惠券优惠', '积分抵扣', '活动优惠', '首单优惠', '改价优惠', '实付金额', '实付邮费', '支付方式',
'订单号', '订单类型', '订单状态', '下单时间', '买家姓名(uid)', '用户名称', '买家手机号', '收货地址',
'商品名称', '商品标签', 'SPU', '款式编码', '商品编号', '商品规格', '商品数量', '商品金额合计', '结算价', '商品来源',
'邮费', '会员优惠', '优惠券优惠', '积分抵扣', '活动优惠', '首单优惠', '改价优惠', '实付金额', '实付邮费', '支付方式',
'支付状态', '支付时间', '订单配送方式', '快递公司(单号)/送货人姓名(手机号)',
'退款状态', '退款金额', '分销员', '分销佣金', '订单发货时间',
// '售后状态', '售后类型', '售后件数', '退款金额', '退款时间', '退货备注'
];
$title = ['订单导出', '订单信息' . time(), ' 生成时间:' . date('Y-m-d H:i:s', time())];
@@ -744,6 +750,96 @@ class ExportServices extends BaseServices
$export = [];
$filekey = [];
if (!empty($data)) {
$userList = [];
$spreadList = [];
$orderStatusTime = [];
$refundCartMap = [];
$supplierList = [];
$labelNameMap = [];
if (!$type) {
$userServices = app()->make(UserServices::class);
$uids = array_values(array_unique(array_column($data, 'uid')));
$userList = $uids ? $userServices->getColumn([['uid', 'IN', $uids]], 'uid,nickname,spread_uid', 'uid') : [];
$spreadUids = $userList ? array_values(array_filter(array_unique(array_column($userList, 'spread_uid')))) : [];
$spreadList = $spreadUids ? $userServices->getColumn([['uid', 'IN', $spreadUids]], 'uid,nickname', 'uid') : [];
$orderIds = array_values(array_unique(array_column($data, 'id')));
$statusRows = StoreOrderStatus::whereIn('oid', $orderIds)
->whereIn('change_type', ['delivery', 'delivery_goods', 'delivery_part_split', 'delivery_split', 'delivery_fictitious'])
->field(['oid', 'change_type', 'change_time'])
->order('change_time desc')
->select()
->toArray();
foreach ($statusRows as $row) {
$oid = (int)$row['oid'];
$changeType = (string)$row['change_type'];
if (!isset($orderStatusTime[$oid][$changeType])) {
$orderStatusTime[$oid][$changeType] = (int)$row['change_time'];
}
}
$refundRows = StoreOrderRefund::whereIn('store_order_id', $orderIds)
->where('is_cancel', 0)
->where('is_del', 0)
->whereIn('refund_type', [0, 1, 2, 4, 5, 6])
->field(['store_order_id', 'refund_type', 'cart_info'])
->select()
->toArray();
foreach ($refundRows as $refund) {
$oid = (int)$refund['store_order_id'];
$refundType = (int)$refund['refund_type'];
$cartInfo = $refund['cart_info'] ?? [];
foreach ($cartInfo as $cart) {
$cartId = (int)($cart['id'] ?? 0);
$qty = (int)($cart['cart_num'] ?? 0);
if (!$cartId || !$qty) {
continue;
}
if (!isset($refundCartMap[$oid][$cartId])) {
$refundCartMap[$oid][$cartId] = ['refunding_qty' => 0, 'refunded_qty' => 0];
}
if ($refundType === 6) {
$refundCartMap[$oid][$cartId]['refunded_qty'] += $qty;
} else {
$refundCartMap[$oid][$cartId]['refunding_qty'] += $qty;
}
}
}
$supplierIds = [];
$labelIds = [];
foreach ($data as $o) {
foreach ($o['_info'] ?? [] as $cart) {
$productInfo = $cart['productInfo'] ?? [];
$supplierId = (int)($productInfo['supplier_id'] ?? 0);
if ($supplierId > 0) {
$supplierIds[$supplierId] = $supplierId;
} elseif ((int)($cart['_relation_type'] ?? 0) === 2 && (int)($cart['_relation_id'] ?? 0) > 0) {
$supplierId = (int)$cart['_relation_id'];
$supplierIds[$supplierId] = $supplierId;
}
$storeLabelId = $productInfo['store_label_id'] ?? [];
$storeLabelId = is_string($storeLabelId) ? array_map('intval', array_filter(explode(',', $storeLabelId))) : (is_array($storeLabelId) ? $storeLabelId : []);
foreach ($storeLabelId as $lid) {
$lid = (int)$lid;
if ($lid > 0) {
$labelIds[$lid] = $lid;
}
}
}
}
$supplierService = app()->make(SystemSupplierServices::class);
$supplierList = $supplierIds ? $supplierService->getColumn([['id', 'IN', array_values($supplierIds)]], 'id,supplier_name', 'id') : [];
$labelService = app()->make(StoreProductLabelServices::class);
$labelList = $labelIds ? $labelService->getLabelCache(array_values($labelIds), ['id', 'label_name'], false) : [];
foreach ($labelList as $label) {
if (isset($label['id'])) {
$labelNameMap[(int)$label['id']] = (string)($label['label_name'] ?? '');
}
}
}
$i = 0;
foreach ($data as $item) {
if (!$type) {
@@ -805,24 +901,9 @@ class ExportServices extends BaseServices
} else if ($item['paid'] == 1 && $item['refund_status'] == 3) {
$item['status_name'] = '部分退款';
}
$goodsName = [];
$vip_sum_price = 0;
foreach ($item['_info'] as $k => $v) {
$suk = '';
if (isset($v['productInfo']['attrInfo'])) {
if (isset($v['productInfo']['attrInfo']['suk'])) {
$suk = '(' . $v['productInfo']['attrInfo']['suk'] . ')';
}
}
if (isset($v['productInfo']['store_name'])) {
$goodsName[] = implode(' ',
[
$v['productInfo']['store_name'],
$suk,
"[{$v['cart_num']} * {$v['truePrice']}]",
]);
}
$vip_sum_price = bcadd((string)$vip_sum_price, bcmul($v['vip_truePrice'], $v['cart_num'] ? $v['cart_num'] : 1, 4), 2);
foreach ($item['_info'] as $v) {
$vip_sum_price = bcadd((string)$vip_sum_price, bcmul((string)($v['vip_truePrice'] ?? 0), (string)($v['cart_num'] ? $v['cart_num'] : 1), 4), 2);
}
// if ($item['sex'] == 1) $sex_name = '男';
// else if ($item['sex'] == 2) $sex_name = '女';
@@ -856,32 +937,134 @@ class ExportServices extends BaseServices
'send' => '配送',
default => '暂无',
};
$one_data = [
'order_id' => $item['order_id'],
'order_type' => $item['pink_name'] ?? '普通订单',
'status_name' => $item['status_name'] ?? '未知状态',
'add_time' => empty($item['add_time']) ? '' : date('Y-m-d H:i:s', (int)$item['add_time']),
'real_name' => $item['real_name'] . '(' . $item['uid'] . ')',
'user_phone' => $item['user_phone'],
'user_address' => $item['user_address'],
'goods_name' => $goodsName ? implode("\n", $goodsName) : '',
'total_num' => $item['total_num'],
'total_price' => $item['total_price'],
'total_postage' => $item['total_postage'],
'vip_sum_price' => $vip_sum_price,
'coupon_price' => $item['coupon_price'],
'deduction_price' => $item['deduction_price'] ?? 0,
'promotions_price' => $item['promotions_price'] ?? 0,
'first_order_price' => $item['first_order_price'] ?? 0,
'change_price' => $item['change_price'] ?? 0,
'pay_price' => $item['pay_price'],
'pay_postage' => $item['pay_postage'],
'pay_type_name' => $item['pay_type_name'],
'pay_status' => $item['paid'] ? '已支付' : '未支付',
'pay_time' => empty($item['pay_time']) ? '' : date('Y-m-d H:i:s', (int)$item['pay_time']),
'delivery_type' => $delivery_type,
'delivery_info' => $item['delivery_name'] . '(' . $item['delivery_id'] . ')',
];
$orderLevelDeliveryTime = '';
$oid = (int)$item['id'];
$timeMap = $orderStatusTime[$oid] ?? [];
$deliveryTime = $timeMap['delivery'] ?? ($timeMap['delivery_goods'] ?? ($timeMap['delivery_part_split'] ?? ($timeMap['delivery_split'] ?? ($timeMap['delivery_fictitious'] ?? 0))));
if ($deliveryTime) {
$orderLevelDeliveryTime = date('Y-m-d H:i:s', $deliveryTime);
}
$currentUser = $userList[(int)$item['uid']] ?? [];
$userNickname = (string)($currentUser['nickname'] ?? '');
$spreadUid = (int)($currentUser['spread_uid'] ?? 0);
$spreadNickname = $spreadUid ? (string)($spreadList[$spreadUid]['nickname'] ?? '') : '';
if ($spreadUid && $spreadUid === (int)$item['uid'] && $spreadNickname !== '') {
$spreadNickname .= '(自购)';
}
foreach ($item['_info'] as $v) {
$productInfo = $v['productInfo'] ?? [];
$attrInfo = $productInfo['attrInfo'] ?? [];
$storeName = (string)($productInfo['store_name'] ?? '');
$suk = (string)($attrInfo['suk'] ?? '');
$productLabelNames = [];
if (!empty($productInfo['store_label']) && is_array($productInfo['store_label'])) {
foreach ($productInfo['store_label'] as $label) {
$name = (string)($label['label_name'] ?? ($label['label'] ?? ''));
if ($name !== '') {
$productLabelNames[] = $name;
}
}
} else {
$storeLabelId = $productInfo['store_label_id'] ?? [];
$storeLabelId = is_string($storeLabelId) ? array_map('intval', array_filter(explode(',', $storeLabelId))) : (is_array($storeLabelId) ? $storeLabelId : []);
foreach ($storeLabelId as $lid) {
$name = $labelNameMap[(int)$lid] ?? '';
if ($name !== '') {
$productLabelNames[] = $name;
}
}
}
$productLabelText = $productLabelNames ? implode(',', array_values(array_unique($productLabelNames))) : '';
$supplierId = (int)($productInfo['supplier_id'] ?? 0);
if (!$supplierId && (int)($v['_relation_type'] ?? 0) === 2) {
$supplierId = (int)($v['_relation_id'] ?? 0);
}
$productSource = $supplierId ? (string)($supplierList[$supplierId]['supplier_name'] ?? '') : '平台';
$cartId = (int)($v['cart_id'] ?? 0);
$refundedQty = (int)($refundCartMap[$oid][$cartId]['refunded_qty'] ?? 0);
$refundingQty = (int)($refundCartMap[$oid][$cartId]['refunding_qty'] ?? 0);
$refundQty = $refundedQty + $refundingQty;
$cartNum = (int)($v['cart_num'] ?? 0);
$truePrice = (string)($v['truePrice'] ?? 0);
$productAmount = bcmul($truePrice, (string)$cartNum, 2);
$refundStatus = '';
if ($refundQty > 0) {
if ($refundingQty > 0) {
$refundStatus = '退款中';
} else {
$refundStatus = $refundQty >= $cartNum ? '退款成功(全额退款)' : '退款成功(部分退款)';
}
}
$refundAmount = $refundQty > 0 ? bcmul($truePrice, (string)$refundQty, 2) : '';
$settlePrice = (string)($attrInfo['settle_price'] ?? 0);
$oneBrokerage = $v['one_brokerage'] ?? null;
if ($oneBrokerage === null) {
$oneBrokerage = bcmul((string)($attrInfo['brokerage'] ?? 0), (string)$cartNum, 2);
}
$twoBrokerage = $v['two_brokerage'] ?? null;
if ($twoBrokerage === null) {
$twoBrokerage = bcmul((string)($attrInfo['brokerage_two'] ?? 0), (string)$cartNum, 2);
}
$commission = bcadd((string)$oneBrokerage, (string)$twoBrokerage, 2);
$one_data = [
'order_id' => $item['order_id'],
'order_type' => $item['pink_name'] ?? '普通订单',
'status_name' => $item['status_name'] ?? '未知状态',
'add_time' => empty($item['add_time']) ? '' : date('Y-m-d H:i:s', (int)$item['add_time']),
'real_name' => $item['real_name'] . '(' . $item['uid'] . ')',
'user_nickname' => $userNickname,
'user_phone' => $item['user_phone'],
'user_address' => $item['user_address'],
'goods_name' => $storeName,
'goods_label' => $productLabelText,
'spu' => (string)($productInfo['spu'] ?? ''),
'style_code' => (string)($productInfo['code'] ?? ''),
'sku_code' => (string)($attrInfo['code'] ?? ''),
'goods_suk' => $suk,
'cart_num' => $cartNum,
'product_amount' => $productAmount,
'settle_price' => $settlePrice,
'product_source' => $productSource,
'total_postage' => $item['total_postage'],
'vip_sum_price' => $vip_sum_price,
'coupon_price' => $item['coupon_price'],
'deduction_price' => $item['deduction_price'] ?? 0,
'promotions_price' => $item['promotions_price'] ?? 0,
'first_order_price' => $item['first_order_price'] ?? 0,
'change_price' => $item['change_price'] ?? 0,
'pay_price' => $item['pay_price'],
'pay_postage' => $item['pay_postage'],
'pay_type_name' => $item['pay_type_name'],
'pay_status' => $item['paid'] ? '已支付' : '未支付',
'pay_time' => empty($item['pay_time']) ? '' : date('Y-m-d H:i:s', (int)$item['pay_time']),
'delivery_type' => $delivery_type,
'delivery_info' => $item['delivery_name'] . '(' . $item['delivery_id'] . ')',
'refund_status' => $refundStatus,
'refund_amount' => $refundAmount,
'spread_nickname' => $spreadNickname,
'spread_commission' => $commission,
'delivery_time' => $orderLevelDeliveryTime,
];
if ($export_type == 1) {
$export[] = $one_data;
if ($i == 0) {
$filekey = array_keys($one_data);
}
} else {
$export[] = array_values($one_data);
}
$i++;
}
} else {
if (isset($item['pinkStatus']) && $item['pinkStatus'] != 2) {
continue;
@@ -937,15 +1120,17 @@ class ExportServices extends BaseServices
'pay_time' => $item['pay_time'] ? date('Y-m-d H:i:s', (int)$item['pay_time']) : '暂无',
];
}
if ($export_type == 1) {
$export[] = $one_data;
if ($i == 0) {
$filekey = array_keys($one_data);
if ($type) {
if ($export_type == 1) {
$export[] = $one_data;
if ($i == 0) {
$filekey = array_keys($one_data);
}
} else {
$export[] = array_values($one_data);
}
} else {
$export[] = array_values($one_data);
$i++;
}
$i++;
}
}
if ($export_type == 1) {

View File

@@ -797,6 +797,35 @@ class StoreProductServices extends BaseServices
return substr(implode('', array_map('ord', str_split(substr(uniqid(), 7, 13), 1))), 0, 8) . str_pad((string)mt_rand(1, 99999), 5, '0', STR_PAD_LEFT);
}
protected function createUniqueSpu(): string
{
$tries = 0;
do {
$spu = $this->createSpu();
$tries++;
} while ($tries < 20 && $this->dao->be(['spu' => $spu], 'spu'));
return $spu;
}
public function ensureSpu(int $productId): string
{
$spu = (string)$this->dao->value(['id' => $productId], 'spu');
if ($spu !== '') {
return $spu;
}
$spu = $this->createUniqueSpu();
$this->update($productId, ['spu' => $spu]);
return $spu;
}
public function ErpProductSave(array $data): int
{
if (!isset($data['spu']) || $data['spu'] === '') {
$data['spu'] = $this->createUniqueSpu();
}
return (int)$this->dao->ErpProductSave($data);
}
/**
* 新增编辑商品
* @param int $id
@@ -1531,7 +1560,7 @@ class StoreProductServices extends BaseServices
}
}
[$page, $limit] = $this->getPageValue();
$field = ['id,relation_id,type,pid,delivery_type,product_type,store_name,cate_id,image,IFNULL(sales, 0) + IFNULL(ficti, 0) as sales,price,stock,activity,ot_price,spec_type,recommend_image,unit_name,is_vip,vip_price,is_presale_product,is_vip_product,system_form_id,is_presale_product,presale_start_time,presale_end_time,is_limit,limit_num,video_open,video_link,freight,star,store_label_id,brand_id,min_qty'];
$field = ['id,spu,relation_id,type,pid,delivery_type,product_type,store_name,cate_id,image,IFNULL(sales, 0) + IFNULL(ficti, 0) as sales,price,stock,activity,ot_price,spec_type,recommend_image,unit_name,is_vip,vip_price,is_presale_product,is_vip_product,system_form_id,is_presale_product,presale_start_time,presale_end_time,is_limit,limit_num,video_open,video_link,freight,star,store_label_id,brand_id,min_qty'];
$list = $this->dao->getSearchList($where, $page, $limit, $field, 'sales desc,id desc', ['couponId']);
if ($list) {
/** @var MemberCardServices $memberCardService */

View File

@@ -31,15 +31,13 @@ class OrderStatisticServices extends BaseServices
$orderService = app()->make(StoreOrderServices::class);
$data['pay_price'] = $orderService->sum(['paid' => 1, 'pid' => 0, 'time' => $where['time'], 'supplier_id' => $supplier_id], 'pay_price', true);
$data['pay_count'] = $orderService->count(['paid' => 1, 'pid' => 0, 'time' => $where['time'], 'supplier_id' => $supplier_id]);
$times = explode('-', $where['time']);
if(count($times) > 1){
$times = [
'start_time'=>$times[0],
'end_time'=>$times[1] . ' 23:59:59'
];
$refundWhere = ['refund_type' => 6, 'time' => explode('-', $where['time']), 'is_cancel' => 0, 'is_del' => 0];
if ($supplier_id !== '') {
$refundWhere['supplier_id'] = $supplier_id;
}
$data['refund_price'] = app()->make(StoreOrderRefundServices::class)->sum(['refund_type' => 6, 'time' => explode('-', $where['time'])],'refund_price', true);
$data['refund_count'] = app()->make(StoreOrderRefundServices::class)->count(['refund_type' => 6, 'time' => explode('-', $where['time'])]);
$refundService = app()->make(StoreOrderRefundServices::class);
$data['refund_price'] = $refundService->sum($refundWhere, 'refund_price', true);
$data['refund_count'] = $refundService->count($refundWhere);
$data['coupon_price'] = $orderService->sum(['paid' => 1, 'pid' => 0, 'time' => $where['time'], 'supplier_id' => $supplier_id], 'coupon_price', true);
$data['coupon_count'] = $orderService->search(['paid' => 1, 'pid' => 0, 'time' => $where['time'], 'is_coupon' => 1, 'supplier_id' => $supplier_id])->count();
return $data;

View File

@@ -394,7 +394,7 @@ class SystemAdminServices extends BaseServices
$verifyCode = CacheService::get('code_' . $data['phone']);
if (!$verifyCode)
throw new AdminException('请先获取验证码');
$verifyCode = substr($verifyCode, 0, 6);
$verifyCode = substr($verifyCode, 0, 4);
if ($verifyCode != $data['code']) {
CacheService::delete('code_' . $data['phone']);
throw new AdminException('验证码错误');

View File

@@ -65,7 +65,7 @@ class SystemAttachmentServices extends BaseServices
if ($item['file_type'] == 1) {
if ($site_url) {
$item['satt_dir'] = (strpos($item['satt_dir'], $site_url) !== false || strstr($item['satt_dir'], 'http') !== false) ? $item['satt_dir'] : $site_url . $item['satt_dir'];
$item['att_dir'] = (strpos($item['att_dir'], $site_url) !== false || strstr($item['att_dir'], 'http') !== false) ? $item['satt_dir'] : $site_url . $item['att_dir'];
$item['att_dir'] = (strpos($item['att_dir'], $site_url) !== false || strstr($item['att_dir'], 'http') !== false) ? $item['att_dir'] : $site_url . $item['att_dir'];
}
}
$item['att_size'] = formatFileSize($item['att_size']);

View File

@@ -167,7 +167,7 @@ class LoginServices extends BaseServices
if (CacheService::get('code_' . $phone))
throw new ValidateException($time . '分钟内有效');
mt_srand();
$code = rand(100000, 999999);
$code = rand(1000, 9999);
$data['code'] = $code;
$data['time'] = $time;
$res = SmsJob::dispatch([$phone, $data, 'VERIFICATION_CODE_TIME']);