728 lines
26 KiB
PHP
728 lines
26 KiB
PHP
<?php
|
||
|
||
namespace App\Console\Commands;
|
||
|
||
use Illuminate\Console\Command;
|
||
use Illuminate\Support\Carbon;
|
||
use Illuminate\Support\Facades\DB;
|
||
use Modules\EcommerceFinance\Services\UpstreamReadOnlyQuery;
|
||
use Modules\EcommerceFinance\Services\UpstreamReadOnlyQueryFactory;
|
||
use Throwable;
|
||
|
||
/**
|
||
* 按「附加费报价维护」里启用的退回费 / 拦截费规则,从查件扣单拉取结算行。
|
||
*
|
||
* 口径:
|
||
* - 规则状态 active、生效条件含「查件扣单」;
|
||
* - 退回费:types 含「退回费」,支持 固定单价 / 按重量;已退回(售后退货退款
|
||
* receive_date 进仓时间 + r_qty 实退数量)按标准计价,未退回计 0;
|
||
* 结算重量取商品资料 ops_jst_sku.weight,按实退数量汇总;未退回时按发货数量展示;
|
||
* - 拦截费:types 含「拦截费」且模式为固定单价,查件扣单即按标准金额计价;
|
||
* - 扣单来源:共享镜像 js_jst_work_order_info 中 title=查件、查件类型=扣单 的工单。
|
||
*/
|
||
class PullCourierSurchargeReturn extends Command
|
||
{
|
||
private const CHECK_TYPE_FIELD = 'SELECT_CASCADEField1413856620250813172526';
|
||
|
||
protected $signature = 'courier-surcharge:pull-return
|
||
{--days=90 : 默认拉取最近多少天(无 --from/--to 时生效,按查件工单创建时间)}
|
||
{--from= : 查件工单创建起(含),Y-m-d}
|
||
{--to= : 查件工单创建止(不含),Y-m-d}
|
||
{--rule=* : 只跑指定规则 ID,可多次传入}
|
||
{--type=* : 只跑指定附加费类型(退回费/拦截费),可多次传入}
|
||
{--limit=0 : 调试用,最多处理多少张扣单工单,0 表示不限}';
|
||
|
||
protected $description = '按启用的退回费/拦截费报价规则,从查件扣单拉取结算行';
|
||
|
||
public function handle(): int
|
||
{
|
||
$from = $this->option('from') !== null && $this->option('from') !== ''
|
||
? Carbon::parse((string) $this->option('from'))->startOfDay()
|
||
: now()->subDays(max((int) $this->option('days'), 1))->startOfDay();
|
||
$to = $this->option('to') !== null && $this->option('to') !== ''
|
||
? Carbon::parse((string) $this->option('to'))->startOfDay()
|
||
: now()->addDay()->startOfDay();
|
||
$onlyRules = array_filter(array_map('intval', (array) $this->option('rule')));
|
||
$onlyTypes = array_values(array_filter(
|
||
array_map(static fn ($type): string => trim((string) $type), (array) $this->option('type')),
|
||
static fn (string $type): bool => in_array($type, ['退回费', '拦截费'], true),
|
||
));
|
||
$limit = max((int) $this->option('limit'), 0);
|
||
|
||
$connection = (string) config('upstream_mirrors.connection', 'business_mirror');
|
||
$schema = DB::connection($connection)->getSchemaBuilder();
|
||
|
||
if (! $schema->hasTable('courier_surcharge_settlement_rows')) {
|
||
$this->error('结算表 courier_surcharge_settlement_rows 不存在,请先执行迁移。');
|
||
|
||
return self::FAILURE;
|
||
}
|
||
|
||
if (! $schema->hasTable('js_jst_work_order_info')) {
|
||
$this->error('查件工单镜像 js_jst_work_order_info 不存在,请先完成镜像同步。');
|
||
|
||
return self::FAILURE;
|
||
}
|
||
|
||
$rules = $this->rules($onlyRules, $onlyTypes);
|
||
|
||
if ($rules === []) {
|
||
$this->warn('没有启用且可自动计算的退回费/拦截费规则。');
|
||
|
||
return self::SUCCESS;
|
||
}
|
||
|
||
$orders = $this->pullDeductOrders($connection, $from, $to, $limit);
|
||
$this->info(sprintf('取数 %s ~ %s,扣单工单 %d 单。', $from->toDateString(), $to->toDateString(), count($orders)));
|
||
|
||
if ($orders === []) {
|
||
return self::SUCCESS;
|
||
}
|
||
|
||
$ordersByCourier = [];
|
||
|
||
foreach ($orders as $order) {
|
||
$ordersByCourier[$order['courier']][] = $order;
|
||
}
|
||
|
||
$totalWritten = 0;
|
||
|
||
foreach ($rules as $rule) {
|
||
$matched = $ordersByCourier[$rule['courier']] ?? [];
|
||
|
||
if ($matched === []) {
|
||
$this->line('规则 #' . $rule['id'] . ' ' . $rule['courier'] . ':没有匹配的扣单工单,跳过。');
|
||
continue;
|
||
}
|
||
|
||
$written = $this->writeSettlementRows($connection, $rule, $matched);
|
||
$totalWritten += $written;
|
||
$this->line('规则 #' . $rule['id'] . ' ' . $rule['courier'] . '(' . $rule['type'] . '/' . $rule['mode'] . '):扣单 ' . count($matched) . ' 单,写入 ' . $written . ' 行。');
|
||
}
|
||
|
||
$this->info('完成,共写入 ' . $totalWritten . ' 行附加费结算数据。');
|
||
|
||
return self::SUCCESS;
|
||
}
|
||
|
||
/**
|
||
* @param list<int> $onlyRules
|
||
* @param list<string> $onlyTypes
|
||
* @return list<array{id:int,type:string,courier:string,mode:string,value:float,effective_at:?string}>
|
||
*/
|
||
private function rules(array $onlyRules, array $onlyTypes): array
|
||
{
|
||
$rules = [];
|
||
|
||
foreach (DB::table('business_courier_surcharge_quotes')->where('status', 'active')->orderBy('id')->get() as $rule) {
|
||
$types = json_decode((string) $rule->types, true) ?: [];
|
||
$candidateTypes = array_values(array_intersect(['退回费', '拦截费'], $types));
|
||
|
||
if ($candidateTypes === []) {
|
||
continue;
|
||
}
|
||
|
||
if ($onlyRules !== [] && ! in_array((int) $rule->id, $onlyRules, true)) {
|
||
continue;
|
||
}
|
||
|
||
$condition = trim((string) $rule->condition);
|
||
|
||
if (! str_contains($condition, '扣单')) {
|
||
$this->warn('规则 #' . $rule->id . ' 的生效条件不含「扣单」,已跳过:' . $condition);
|
||
continue;
|
||
}
|
||
|
||
$mode = trim((string) $rule->mode);
|
||
$standard = trim((string) $rule->standard);
|
||
|
||
foreach ($candidateTypes as $type) {
|
||
if ($onlyTypes !== [] && ! in_array($type, $onlyTypes, true)) {
|
||
continue;
|
||
}
|
||
|
||
if ($type === '退回费') {
|
||
if ($mode === '固定单价') {
|
||
$value = $this->parseFixedAmount($standard);
|
||
} elseif ($mode === '按重量') {
|
||
$value = $this->parseWeightRate($standard);
|
||
} else {
|
||
$this->line('规则 #' . $rule->id . ' ' . $rule->courier . ' 的退回费模式「' . $mode . '」暂不支持自动计算,已跳过。');
|
||
continue;
|
||
}
|
||
} else {
|
||
if ($mode === '固定单价') {
|
||
$value = $this->parseFixedAmount($standard);
|
||
} else {
|
||
$this->line('规则 #' . $rule->id . ' ' . $rule->courier . ' 的拦截费模式「' . $mode . '」暂不支持自动计算,已跳过。');
|
||
continue;
|
||
}
|
||
}
|
||
|
||
if ($value === null) {
|
||
$this->warn('规则 #' . $rule->id . ' 的' . $type . '计价标准解析失败,已跳过:' . $standard);
|
||
continue;
|
||
}
|
||
|
||
$rules[] = [
|
||
'id' => (int) $rule->id,
|
||
'type' => $type,
|
||
'courier' => trim((string) $rule->courier),
|
||
'mode' => $mode,
|
||
'value' => $value,
|
||
'effective_at' => $rule->effective_at !== null && $rule->effective_at !== ''
|
||
? (string) $rule->effective_at
|
||
: null,
|
||
];
|
||
}
|
||
}
|
||
|
||
return $rules;
|
||
}
|
||
|
||
private function parseFixedAmount(string $standard): ?float
|
||
{
|
||
return preg_match('/(\d+(?:\.\d+)?)\s*(?:元)?\s*\/?\s*单/u', $standard, $matched) === 1
|
||
? (float) $matched[1]
|
||
: null;
|
||
}
|
||
|
||
private function parseWeightRate(string $standard): ?float
|
||
{
|
||
return preg_match('/(\d+(?:\.\d+)?)\s*元\s*\/\s*(?:kg|KG|公斤|千克)/u', $standard, $matched) === 1
|
||
? (float) $matched[1]
|
||
: null;
|
||
}
|
||
|
||
/**
|
||
* 从查件工单镜像按创建时间拉取扣单工单。
|
||
*
|
||
* @return list<array<string,mixed>>
|
||
*/
|
||
private function pullDeductOrders(string $connection, Carbon $from, Carbon $to, int $limit): array
|
||
{
|
||
$orders = [];
|
||
$lastId = 0;
|
||
$batchSize = 500;
|
||
|
||
while (true) {
|
||
$rows = DB::connection($connection)->table('js_jst_work_order_info')
|
||
->where('title', '查件')
|
||
->where('businessFieldList', 'like', '%"value":"扣单/%')
|
||
->where('created', '>=', $from->toDateTimeString())
|
||
->where('created', '<', $to->toDateTimeString())
|
||
->where('Id', '>', $lastId)
|
||
->orderBy('Id')
|
||
->limit($batchSize)
|
||
->get(['Id', 'woNo', 'created', 'businessFieldList']);
|
||
|
||
if ($rows->isEmpty()) {
|
||
break;
|
||
}
|
||
|
||
foreach ($rows as $row) {
|
||
$lastId = max($lastId, (int) $row->Id);
|
||
$order = $this->parseWorkOrder((string) $row->businessFieldList, (string) $row->woNo, (string) $row->created);
|
||
|
||
if ($order !== null) {
|
||
$orders[] = $order;
|
||
}
|
||
|
||
if ($limit > 0 && count($orders) >= $limit) {
|
||
break 2;
|
||
}
|
||
}
|
||
|
||
if ($rows->count() < $batchSize) {
|
||
break;
|
||
}
|
||
}
|
||
|
||
return $orders;
|
||
}
|
||
|
||
/**
|
||
* @return array<string,mixed>|null
|
||
*/
|
||
private function parseWorkOrder(string $json, string $woNo, string $created): ?array
|
||
{
|
||
$fields = json_decode($json, true);
|
||
|
||
if (! is_array($fields)) {
|
||
return null;
|
||
}
|
||
|
||
$flat = [];
|
||
|
||
foreach ($fields as $field) {
|
||
if (is_array($field) && isset($field['fieldId'])) {
|
||
$flat[(string) $field['fieldId']] = $field;
|
||
}
|
||
}
|
||
|
||
$waybill = trim((string) ($flat['lIdField']['value'] ?? ''));
|
||
$oId = (int) ($flat['o_id']['value'] ?? 0);
|
||
$courier = trim((string) ($flat['logisticsField']['value'] ?? ''));
|
||
$cascade = trim((string) ($flat[self::CHECK_TYPE_FIELD]['value'] ?? ''));
|
||
|
||
if ($waybill === '' || $courier === '' || ! str_starts_with($cascade, '扣单')) {
|
||
return null;
|
||
}
|
||
|
||
[$inspectionType, $inspectionReason] = array_pad(explode('/', $cascade, 2), 2, '');
|
||
$items = [];
|
||
|
||
foreach (($flat['itemInfo']['childFieldList'] ?? []) as $group) {
|
||
if (! is_array($group)) {
|
||
continue;
|
||
}
|
||
|
||
$item = [];
|
||
|
||
foreach ($group as $field) {
|
||
if (is_array($field) && isset($field['fieldId'])) {
|
||
$item[(string) $field['fieldId']] = $field['value'] ?? null;
|
||
}
|
||
}
|
||
|
||
$skuId = strtoupper(trim((string) ($item['skuId'] ?? '')));
|
||
|
||
if ($skuId === '') {
|
||
continue;
|
||
}
|
||
|
||
$items[] = [
|
||
'sku_id' => $skuId,
|
||
'qty' => (float) ($item['qty'] ?? 0),
|
||
'platform_product_id' => mb_substr(trim((string) ($item['platformItemId'] ?? '')), 0, 191),
|
||
'i_id' => trim((string) ($item['iid'] ?? '')),
|
||
'name' => mb_substr(trim((string) ($item['name'] ?? '')), 0, 191),
|
||
];
|
||
}
|
||
|
||
$createdAt = null;
|
||
|
||
try {
|
||
$createdAt = Carbon::parse($created);
|
||
} catch (Throwable) {
|
||
// created 无法解析时仍保留工单,仅规则生效时间过滤会放宽。
|
||
}
|
||
|
||
return [
|
||
'wo_no' => $woNo,
|
||
'waybill' => $waybill,
|
||
'o_id' => $oId,
|
||
'courier' => $courier,
|
||
'online_order_no' => mb_substr(trim((string) ($flat['orderField']['value'] ?? '')), 0, 191),
|
||
'shop_name' => mb_substr(trim((string) ($flat['shopField']['valueLabel'] ?? ($flat['shopField']['value'] ?? ''))), 0, 191),
|
||
'inspection_type' => mb_substr(trim((string) $inspectionType), 0, 60),
|
||
'inspection_reason' => mb_substr(trim((string) $inspectionReason), 0, 120),
|
||
'send_time' => trim((string) ($flat['orderSendTime']['value'] ?? '')),
|
||
'created_at' => $createdAt,
|
||
'items' => $items,
|
||
];
|
||
}
|
||
|
||
/**
|
||
* @param array{id:int,courier:string,mode:string,value:float,effective_at:?string} $rule
|
||
* @param list<array<string,mixed>> $orders
|
||
*/
|
||
private function writeSettlementRows(string $connection, array $rule, array $orders): int
|
||
{
|
||
$effectiveAt = null;
|
||
|
||
if (($rule['effective_at'] ?? null) !== null) {
|
||
try {
|
||
$effectiveAt = Carbon::parse((string) $rule['effective_at'])->startOfDay();
|
||
} catch (Throwable) {
|
||
// 规则生效时间无法解析时不做下界过滤。
|
||
}
|
||
}
|
||
|
||
if ($effectiveAt !== null) {
|
||
$orders = array_values(array_filter(
|
||
$orders,
|
||
static fn (array $order): bool => ! $order['created_at'] instanceof Carbon
|
||
|| $order['created_at']->greaterThanOrEqualTo($effectiveAt),
|
||
));
|
||
}
|
||
|
||
if ($orders === []) {
|
||
return 0;
|
||
}
|
||
|
||
$oIds = [];
|
||
$waybills = [];
|
||
$skuIds = [];
|
||
|
||
foreach ($orders as $order) {
|
||
if ((int) ($order['o_id'] ?? 0) > 0) {
|
||
$oIds[(int) $order['o_id']] = true;
|
||
}
|
||
|
||
$waybills[(string) $order['waybill']] = true;
|
||
|
||
foreach ($order['items'] as $item) {
|
||
$skuIds[(string) $item['sku_id']] = true;
|
||
}
|
||
}
|
||
|
||
$type = (string) $rule['type'];
|
||
$returns = $type === '退回费' ? $this->pullReturns($orders) : [];
|
||
$weights = $this->pullWeights($connection, array_keys($skuIds));
|
||
$now = now();
|
||
$rows = [];
|
||
$hasReturnByWaybill = [];
|
||
|
||
foreach ($orders as $order) {
|
||
$matched = [];
|
||
$receiveDates = [];
|
||
$returnWaybills = [];
|
||
$hasReturn = false;
|
||
$settlementWeight = 0.0;
|
||
|
||
$orderWeight = 0.0;
|
||
|
||
foreach ($order['items'] as $item) {
|
||
$weight = $weights[(string) $item['sku_id']] ?? null;
|
||
|
||
if ($weight !== null) {
|
||
$orderWeight += (float) $item['qty'] * $weight;
|
||
}
|
||
}
|
||
|
||
if ($type === '拦截费') {
|
||
// 拦截费口径:查件扣单即按固定单价结算,不要求退货进仓。
|
||
$amount = (float) $rule['value'];
|
||
$status = '已计算';
|
||
} else {
|
||
foreach ($returns[(int) ($order['o_id'] ?? 0)] ?? [] as $return) {
|
||
$rQty = (float) ($return['r_qty'] ?? 0);
|
||
$receiveDate = trim((string) ($return['receive_date'] ?? ''));
|
||
|
||
if ($rQty <= 0 || $receiveDate === '') {
|
||
continue;
|
||
}
|
||
|
||
$hasReturn = true;
|
||
$matched[] = $return;
|
||
$receiveDates[] = $receiveDate;
|
||
|
||
$returnWaybill = trim((string) ($return['l_id'] ?? ''));
|
||
|
||
if ($returnWaybill !== '') {
|
||
$returnWaybills[$returnWaybill] = true;
|
||
}
|
||
}
|
||
|
||
$returnedWeight = 0.0;
|
||
|
||
foreach ($matched as $return) {
|
||
$weight = $weights[strtoupper(trim((string) ($return['sku_id'] ?? '')))] ?? null;
|
||
|
||
if ($weight !== null) {
|
||
$returnedWeight += (float) $return['r_qty'] * $weight;
|
||
}
|
||
}
|
||
|
||
$settlementWeight = $hasReturn
|
||
? ($returnedWeight > 0 ? $returnedWeight : $orderWeight)
|
||
: $orderWeight;
|
||
|
||
if ($rule['mode'] === '固定单价') {
|
||
$amount = $hasReturn ? (float) $rule['value'] : 0.0;
|
||
$status = '已计算';
|
||
} elseif (! $hasReturn) {
|
||
$amount = 0.0;
|
||
$status = '已计算';
|
||
} elseif ($settlementWeight > 0) {
|
||
$amount = round($settlementWeight * (float) $rule['value'], 2);
|
||
$status = '已计算';
|
||
} else {
|
||
$amount = 0.0;
|
||
$status = '待核价';
|
||
}
|
||
}
|
||
|
||
$primary = $this->primaryItem($order['items'], $matched);
|
||
$waybill = (string) $order['waybill'];
|
||
|
||
// 同一运单可能有多张扣单工单;只保留一行,优先保留有退回记录的那行。
|
||
if (($hasReturnByWaybill[$waybill] ?? null) === true && ! $hasReturn) {
|
||
continue;
|
||
}
|
||
|
||
$rows[$waybill] = [
|
||
'rule_id' => $rule['id'],
|
||
'surcharge_type' => $type,
|
||
'courier_name' => $rule['courier'],
|
||
'waybill' => $waybill,
|
||
'outbound_io_id' => null,
|
||
'platform_product_id' => $primary['platform_product_id'] !== '' ? $primary['platform_product_id'] : null,
|
||
'product_code' => $primary['sku_id'] !== '' ? $primary['sku_id'] : ($primary['i_id'] !== '' ? $primary['i_id'] : null),
|
||
'shop_name' => $order['shop_name'] !== '' ? $order['shop_name'] : null,
|
||
'online_order_no' => $order['online_order_no'] !== '' ? $order['online_order_no'] : null,
|
||
'length' => null,
|
||
'width' => null,
|
||
'height' => null,
|
||
'volume' => null,
|
||
'inspection_type' => $order['inspection_type'],
|
||
'inspection_reason' => $order['inspection_reason'],
|
||
'return_waybill_no' => $returnWaybills === []
|
||
? null
|
||
: mb_substr(implode(',', array_keys($returnWaybills)), 0, 191),
|
||
'settlement_weight' => $settlementWeight > 0 ? round($settlementWeight, 2) : null,
|
||
'settlement_amount' => $amount,
|
||
'settlement_status' => $status,
|
||
'evidence' => mb_substr($this->evidence($rule, $order, $hasReturn, $matched, $receiveDates, $settlementWeight, $amount), 0, 500),
|
||
'shipped_at' => $this->shippedAt($order),
|
||
'pulled_at' => $now,
|
||
'created_at' => $now,
|
||
'updated_at' => $now,
|
||
];
|
||
$hasReturnByWaybill[$waybill] = $hasReturn;
|
||
}
|
||
|
||
foreach (array_chunk(array_keys($waybills), 500) as $chunk) {
|
||
DB::connection($connection)->table('courier_surcharge_settlement_rows')
|
||
->where('rule_id', $rule['id'])
|
||
->where('surcharge_type', $type)
|
||
->whereIn('waybill', $chunk)
|
||
->delete();
|
||
}
|
||
|
||
foreach (array_chunk(array_values($rows), 500) as $chunk) {
|
||
DB::connection($connection)->table('courier_surcharge_settlement_rows')->insert($chunk);
|
||
}
|
||
|
||
return count($rows);
|
||
}
|
||
|
||
/**
|
||
* 售后退货退款:进仓时间取 receive_date,实退数量取 r_qty。
|
||
*
|
||
* @param list<array<string,mixed>> $orders
|
||
* @return array<int,list<array<string,mixed>>>
|
||
*/
|
||
private function pullReturns(array $orders): array
|
||
{
|
||
$map = [];
|
||
$oIds = [];
|
||
$oIdByWaybill = [];
|
||
|
||
foreach ($orders as $order) {
|
||
$oId = (int) ($order['o_id'] ?? 0);
|
||
$waybill = trim((string) ($order['waybill'] ?? ''));
|
||
|
||
if ($oId > 0) {
|
||
$oIds[$oId] = true;
|
||
}
|
||
|
||
if ($oId > 0 && $waybill !== '') {
|
||
$oIdByWaybill[$waybill] = $oId;
|
||
}
|
||
}
|
||
|
||
$upstream = app(UpstreamReadOnlyQueryFactory::class)->forSource('operations_replica');
|
||
|
||
foreach (array_chunk(array_keys($oIds), 200) as $chunk) {
|
||
$placeholders = implode(',', array_fill(0, count($chunk), '?'));
|
||
|
||
foreach ($this->selectReturns($upstream, 'a.o_id IN (' . $placeholders . ')', $chunk) as $row) {
|
||
$oId = (int) ($row['o_id'] ?? 0);
|
||
|
||
if ($oId > 0) {
|
||
$map[$oId][] = $row;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 部分售后单的 o_id 与查件工单不一致,按原始运单号再兜底一次。
|
||
$remainingWaybills = [];
|
||
|
||
foreach ($oIdByWaybill as $waybill => $oId) {
|
||
if (($map[$oId] ?? []) === []) {
|
||
$remainingWaybills[] = $waybill;
|
||
}
|
||
}
|
||
|
||
foreach (array_chunk($remainingWaybills, 200) as $chunk) {
|
||
$placeholders = implode(',', array_fill(0, count($chunk), '?'));
|
||
|
||
foreach ($this->selectReturns($upstream, 'a.l_id IN (' . $placeholders . ')', $chunk) as $row) {
|
||
$oId = $oIdByWaybill[trim((string) ($row['l_id'] ?? ''))] ?? 0;
|
||
|
||
if ($oId > 0) {
|
||
$map[$oId][] = $row;
|
||
}
|
||
}
|
||
}
|
||
|
||
return $map;
|
||
}
|
||
|
||
/**
|
||
* @param list<mixed> $bindings
|
||
* @return list<array<string,mixed>>
|
||
*/
|
||
private function selectReturns(UpstreamReadOnlyQuery $upstream, string $where, array $bindings): array
|
||
{
|
||
try {
|
||
$rows = $upstream->select(
|
||
'SELECT a.o_id, a.as_id, a.l_id, a.status, CONVERT(varchar(19), a.confirm_date, 120) AS confirm_date, '
|
||
. 'ri.sku_id, ri.qty, ri.r_qty, '
|
||
. 'CONVERT(varchar(19), ri.receive_date, 120) AS receive_date '
|
||
. 'FROM rpt.jst_returnAndRefundInfo a '
|
||
. 'INNER JOIN rpt.jst_returnAndRefundItem ri ON ri.as_id = a.as_id '
|
||
. 'WHERE ' . $where,
|
||
$bindings,
|
||
);
|
||
} catch (Throwable) {
|
||
return [];
|
||
}
|
||
|
||
return array_map(static fn ($row): array => (array) $row, $rows);
|
||
}
|
||
|
||
/**
|
||
* @param list<string> $skuIds
|
||
* @return array<string,float>
|
||
*/
|
||
private function pullWeights(string $connection, array $skuIds): array
|
||
{
|
||
$map = [];
|
||
|
||
foreach (array_chunk($skuIds, 500) as $chunk) {
|
||
$rows = DB::connection($connection)->table('ops_jst_sku')
|
||
->whereIn('sku_id', $chunk)
|
||
->get(['sku_id', 'weight']);
|
||
|
||
foreach ($rows as $row) {
|
||
$skuId = strtoupper(trim((string) $row->sku_id));
|
||
$weight = $row->weight;
|
||
|
||
if ($skuId === '' || $weight === null || $weight === '' || isset($map[$skuId])) {
|
||
continue;
|
||
}
|
||
|
||
$map[$skuId] = (float) $weight;
|
||
}
|
||
}
|
||
|
||
return $map;
|
||
}
|
||
|
||
/**
|
||
* @param list<array<string,mixed>> $items
|
||
* @param list<array<string,mixed>> $returns
|
||
* @return array{sku_id:string,i_id:string,platform_product_id:string,name:string}
|
||
*/
|
||
private function primaryItem(array $items, array $returns): array
|
||
{
|
||
$returnSkus = [];
|
||
|
||
foreach ($returns as $return) {
|
||
$skuId = strtoupper(trim((string) ($return['sku_id'] ?? '')));
|
||
|
||
if ($skuId !== '') {
|
||
$returnSkus[$skuId] = true;
|
||
}
|
||
}
|
||
|
||
foreach ($items as $item) {
|
||
if (isset($returnSkus[$item['sku_id']])) {
|
||
return $item;
|
||
}
|
||
}
|
||
|
||
foreach ($items as $item) {
|
||
if ($item['platform_product_id'] !== '') {
|
||
return $item;
|
||
}
|
||
}
|
||
|
||
return $items[0] ?? ['sku_id' => '', 'i_id' => '', 'platform_product_id' => '', 'name' => ''];
|
||
}
|
||
|
||
/**
|
||
* @param array{id:int,type:string,courier:string,mode:string,value:float,effective_at:?string} $rule
|
||
* @param array<string,mixed> $order
|
||
* @param list<array<string,mixed>> $matched
|
||
* @param list<string> $receiveDates
|
||
*/
|
||
private function evidence(
|
||
array $rule,
|
||
array $order,
|
||
bool $hasReturn,
|
||
array $matched,
|
||
array $receiveDates,
|
||
float $settlementWeight,
|
||
float $amount,
|
||
): string {
|
||
$parts = ['查件扣单/' . $order['inspection_reason']];
|
||
|
||
if (($rule['type'] ?? '退回费') === '拦截费') {
|
||
$parts[] = '拦截费规则 ' . $this->trimNumber((float) $rule['value']) . '元/单';
|
||
$parts[] = '结算金额 ' . number_format($amount, 2, '.', '');
|
||
|
||
return implode(';', $parts);
|
||
}
|
||
|
||
if ($hasReturn) {
|
||
$returnQty = 0.0;
|
||
$confirmDates = [];
|
||
|
||
foreach ($matched as $return) {
|
||
$returnQty += (float) ($return['r_qty'] ?? 0);
|
||
$confirmDate = trim((string) ($return['confirm_date'] ?? ''));
|
||
|
||
if ($confirmDate !== '') {
|
||
$confirmDates[] = $confirmDate;
|
||
}
|
||
}
|
||
|
||
if ($confirmDates !== []) {
|
||
$parts[] = '退款确认 ' . max($confirmDates);
|
||
}
|
||
|
||
$parts[] = '售后退货退款已进仓 ' . ($receiveDates === [] ? '' : max($receiveDates));
|
||
$parts[] = '实退 ' . $this->trimNumber($returnQty);
|
||
} else {
|
||
$parts[] = '售后退货退款无进仓/实退记录';
|
||
}
|
||
|
||
$parts[] = '商品资料重量合计 ' . $this->trimNumber($settlementWeight) . 'kg';
|
||
$parts[] = $rule['mode'] === '固定单价'
|
||
? '规则 ' . $this->trimNumber((float) $rule['value']) . '元/单'
|
||
: '规则 ' . $this->trimNumber((float) $rule['value']) . '元/kg';
|
||
$parts[] = '结算金额 ' . number_format($amount, 2, '.', '');
|
||
|
||
return implode(';', $parts);
|
||
}
|
||
|
||
/**
|
||
* @param array<string,mixed> $order
|
||
*/
|
||
private function shippedAt(array $order): ?string
|
||
{
|
||
$sendTime = trim((string) ($order['send_time'] ?? ''));
|
||
|
||
if ($sendTime !== '') {
|
||
try {
|
||
return Carbon::parse($sendTime)->toDateTimeString();
|
||
} catch (Throwable) {
|
||
// 发货时间无法解析时回退到查件工单创建时间。
|
||
}
|
||
}
|
||
|
||
return $order['created_at'] instanceof Carbon ? $order['created_at']->toDateTimeString() : null;
|
||
}
|
||
|
||
private function trimNumber(float $value): string
|
||
{
|
||
$text = rtrim(rtrim(number_format($value, 2, '.', ''), '0'), '.');
|
||
|
||
return $text === '' ? '0' : $text;
|
||
}
|
||
}
|