订单售后费用明细:接入聚水潭「订单售后费用数据」导入明细 #421
206
app/Console/Commands/ImportAfterSalesCostRows.php
Normal file
206
app/Console/Commands/ImportAfterSalesCostRows.php
Normal file
@ -0,0 +1,206 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Modules\EcommerceFinance\Repositories\AfterSalesCostRowRepository;
|
||||
use PhpOffice\PhpSpreadsheet\IOFactory;
|
||||
use PhpOffice\PhpSpreadsheet\Shared\Date as ExcelDate;
|
||||
|
||||
/**
|
||||
* 导入聚水潭「商品利润 → 导出明细数据 → 订单售后费用数据」Excel。
|
||||
*
|
||||
* 该报表已按商品编码(组合装拆成组件)给出每行和成本,导入后订单售后费用明细页面
|
||||
* 在覆盖的店铺/日期范围内直接按这些明细行展示。
|
||||
*/
|
||||
class ImportAfterSalesCostRows extends Command
|
||||
{
|
||||
protected $signature = 'order-data:import-after-sales-cost {file : Excel 文件路径} {--chunk=300 : 每批写入行数}';
|
||||
|
||||
protected $description = '导入聚水潭「订单售后费用数据」Excel 到订单售后费用明细';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$path = (string) $this->argument('file');
|
||||
|
||||
if (! is_file($path)) {
|
||||
$this->error('文件不存在:' . $path);
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$headers = (array) config('order_after_sales_cost.headers', []);
|
||||
$numberKeys = array_flip((array) config('order_after_sales_cost.number_keys', []));
|
||||
|
||||
if ($headers === []) {
|
||||
$this->error('缺少表头映射:config/order_after_sales_cost.php');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$this->info('读取文件:' . $path);
|
||||
|
||||
try {
|
||||
$reader = IOFactory::createReaderForFile($path);
|
||||
$reader->setReadDataOnly(true);
|
||||
$sheet = $reader->load($path)->getActiveSheet();
|
||||
} catch (\Throwable $exception) {
|
||||
$this->error('读取失败:' . $exception->getMessage());
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$rowIterator = $sheet->getRowIterator();
|
||||
$rowIterator->rewind();
|
||||
|
||||
$rawHeaders = [];
|
||||
$first = true;
|
||||
$columns = [];
|
||||
$chunkSize = max((int) $this->option('chunk'), 50);
|
||||
$buffer = [];
|
||||
$total = 0;
|
||||
$written = 0;
|
||||
$skipped = 0;
|
||||
$shops = [];
|
||||
$minDate = null;
|
||||
$maxDate = null;
|
||||
$importedAt = now()->toDateTimeString();
|
||||
|
||||
foreach ($rowIterator as $row) {
|
||||
$cells = [];
|
||||
foreach ($row->getCellIterator() as $cell) {
|
||||
$cells[] = $cell->getValue();
|
||||
}
|
||||
|
||||
if ($first) {
|
||||
$first = false;
|
||||
$rawHeaders = array_map(static fn ($v) => self::normalizeHeader((string) $v), $cells);
|
||||
|
||||
foreach ($rawHeaders as $index => $header) {
|
||||
if ($header !== '' && isset($headers[$header])) {
|
||||
$columns[$index] = $headers[$header];
|
||||
}
|
||||
}
|
||||
|
||||
if ($columns === []) {
|
||||
$this->error('表头和配置的映射对不上,请检查文件或 config/order_after_sales_cost.php');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$total++;
|
||||
$payload = [];
|
||||
|
||||
foreach ($columns as $index => $key) {
|
||||
$value = $cells[$index] ?? null;
|
||||
|
||||
if (is_string($value)) {
|
||||
$value = trim($value);
|
||||
}
|
||||
|
||||
if ($value === '' || $value === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isset($numberKeys[$key])) {
|
||||
$value = is_numeric($value) ? (float) $value : 0.0;
|
||||
} elseif (self::isDateKey($key)) {
|
||||
$value = self::dateValue($value);
|
||||
}
|
||||
|
||||
$payload[$key] = $value;
|
||||
}
|
||||
|
||||
$shop = trim((string) ($payload['shop_name'] ?? ''));
|
||||
|
||||
if ($shop === '' || empty($payload['product_code'])) {
|
||||
$skipped++;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$buffer[] = AfterSalesCostRowRepository::toRecord($payload, $importedAt);
|
||||
$shops[$shop] = true;
|
||||
$orderTime = $payload['order_time'] ?? null;
|
||||
|
||||
if (is_string($orderTime) && $orderTime !== '') {
|
||||
$minDate = $minDate === null || $orderTime < $minDate ? $orderTime : $minDate;
|
||||
$maxDate = $maxDate === null || $orderTime > $maxDate ? $orderTime : $maxDate;
|
||||
}
|
||||
|
||||
if (count($buffer) >= $chunkSize) {
|
||||
$written += $this->flush($buffer);
|
||||
$buffer = [];
|
||||
$this->output->write("\r写入 " . $written . ' 行');
|
||||
}
|
||||
}
|
||||
|
||||
if ($buffer !== []) {
|
||||
$written += $this->flush($buffer);
|
||||
}
|
||||
|
||||
$this->newLine(2);
|
||||
$this->info(sprintf(
|
||||
'完成:读取 %d 行,写入 %d 行,跳过 %d 行;店铺 %d 个;订单时间 %s ~ %s',
|
||||
$total,
|
||||
$written,
|
||||
$skipped,
|
||||
count($shops),
|
||||
$minDate ?? '-',
|
||||
$maxDate ?? '-',
|
||||
));
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
/** @param list<array<string,mixed>> $rows */
|
||||
private function flush(array $rows): int
|
||||
{
|
||||
DB::table('ops_after_sales_cost_rows')->upsert(
|
||||
$rows,
|
||||
['row_key'],
|
||||
[
|
||||
'shop_name', 'shop_code', 'online_order_no', 'internal_order_no', 'product_code',
|
||||
'order_time', 'product_quantity', 'product_amount', 'product_cost', 'cost_source',
|
||||
'combine_type', 'payload', 'imported_at', 'updated_at',
|
||||
],
|
||||
);
|
||||
|
||||
return count($rows);
|
||||
}
|
||||
|
||||
private static function normalizeHeader(string $header): string
|
||||
{
|
||||
$header = str_replace(["\u{3000}", "\u{00A0}"], ' ', $header);
|
||||
$header = preg_replace('/\s+/u', ' ', $header) ?? $header;
|
||||
|
||||
return trim($header);
|
||||
}
|
||||
|
||||
private static function isDateKey(string $key): bool
|
||||
{
|
||||
return in_array($key, [
|
||||
'pay_time', 'order_complete_time', 'send_time', 'order_time', 'registration_time',
|
||||
'confirmation_time', 'warehouse_entry_time',
|
||||
], true);
|
||||
}
|
||||
|
||||
private static function dateValue(mixed $value): ?string
|
||||
{
|
||||
if ($value instanceof \DateTimeInterface) {
|
||||
return $value->format('Y-m-d H:i:s');
|
||||
}
|
||||
|
||||
if (is_numeric($value) && (float) $value > 20000 && (float) $value < 80000) {
|
||||
return ExcelDate::excelToDateTimeObject((float) $value)->format('Y-m-d H:i:s');
|
||||
}
|
||||
|
||||
$timestamp = strtotime((string) $value);
|
||||
|
||||
return $timestamp === false ? null : date('Y-m-d H:i:s', $timestamp);
|
||||
}
|
||||
}
|
||||
140
config/order_after_sales_cost.php
Normal file
140
config/order_after_sales_cost.php
Normal file
@ -0,0 +1,140 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| 订单售后费用明细(聚水潭「订单售后费用数据」导出)
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| 聚水潭导出文件是中文表头,这里把表头映射成页面字段键,导入时按这个映射入库。
|
||||
| 未列出的表头会忽略;新增表头时补在这里即可。
|
||||
*/
|
||||
|
||||
return [
|
||||
'headers' => [
|
||||
'内部订单号' => 'internal_order_no',
|
||||
'线上订单号' => 'online_order_no',
|
||||
'子订单号' => 'sub_order_no',
|
||||
'售后单号' => 'after_sales_no',
|
||||
'支付时间' => 'pay_time',
|
||||
'订单完成时间' => 'order_complete_time',
|
||||
'发货时间' => 'send_time',
|
||||
'订单时间' => 'order_time',
|
||||
'店铺名称' => 'shop_name',
|
||||
'店铺编码' => 'shop_code',
|
||||
'商品编码' => 'product_code',
|
||||
'款式编码(参考)' => 'style_code',
|
||||
'店铺商品编码' => 'shop_product_code',
|
||||
'店铺款式编码' => 'shop_style_code',
|
||||
'链接' => 'product_link',
|
||||
"链接\u{3000}" => 'product_link',
|
||||
'品牌' => 'brand',
|
||||
'分类' => 'category',
|
||||
'虚拟分类' => 'virtual_category',
|
||||
'颜色及规格' => 'color_spec',
|
||||
'商品名称' => 'product_name',
|
||||
'唯一码' => 'unique_code',
|
||||
'商品数量' => 'product_quantity',
|
||||
'商品金额' => 'product_amount',
|
||||
'商品成本' => 'product_cost',
|
||||
'成本来源' => 'cost_source',
|
||||
'其中:红冲订单成本' => 'reversal_order_cost',
|
||||
'手动调整收入' => 'manual_adjust_income',
|
||||
'手动调整成本' => 'manual_adjust_cost',
|
||||
'商家优惠' => 'merchant_discount',
|
||||
'运费收入' => 'freight_income',
|
||||
'付款金额' => 'payment_amount',
|
||||
'其中:平台优惠' => 'platform_discount',
|
||||
'其中:红冲订单金额' => 'reversal_order_amount',
|
||||
'实发金额' => 'actual_shipped_amount',
|
||||
'实发数量' => 'actual_shipped_quantity',
|
||||
'实发成本' => 'actual_shipped_cost',
|
||||
'订单类型' => 'order_type',
|
||||
'登记时间' => 'registration_time',
|
||||
'确认时间' => 'confirmation_time',
|
||||
'进仓时间' => 'warehouse_entry_time',
|
||||
'线上商品名称' => 'online_product_name',
|
||||
'发货前申请数量' => 'pre_application_quantity',
|
||||
'发货前申请金额' => 'pre_application_amount',
|
||||
'发货前申请成本' => 'pre_application_cost',
|
||||
'发货前退回数量' => 'pre_return_quantity',
|
||||
'发货前退回金额' => 'pre_return_amount',
|
||||
'发货前退回成本' => 'pre_return_cost',
|
||||
'发货前退款金额' => 'pre_refund_amount',
|
||||
'发货前退款成本' => 'pre_refund_cost',
|
||||
'发货前退平台补贴' => 'pre_platform_subsidy',
|
||||
'发货后申请数量' => 'post_application_quantity',
|
||||
'发货后申请金额' => 'post_application_amount',
|
||||
'发货后申请成本' => 'post_application_cost',
|
||||
'发货后退回数量' => 'post_return_quantity',
|
||||
'发货后退回金额' => 'post_return_amount',
|
||||
'发货后退回成本' => 'post_return_cost',
|
||||
'发货后退款金额' => 'post_refund_amount',
|
||||
'发货后退款成本' => 'post_refund_cost',
|
||||
'发货后退平台补贴' => 'post_platform_subsidy',
|
||||
'其中:次品商品数量' => 'defective_quantity',
|
||||
'退款类型' => 'refund_type',
|
||||
'问题类型' => 'question_type',
|
||||
'明细退货类型' => 'detail_return_type',
|
||||
'分类单' => 'classification_order',
|
||||
'赠品' => 'is_gift',
|
||||
'组合装' => 'combine_type',
|
||||
'订单标签' => 'order_tags',
|
||||
'售后单标签' => 'after_sales_tags',
|
||||
'商品标签' => 'item_tags',
|
||||
'发货仓编号' => 'shipping_warehouse_code',
|
||||
'发货仓库' => 'shipping_warehouse',
|
||||
'运营云仓' => 'operation_cloud_warehouse',
|
||||
'收货仓编号' => 'receiving_warehouse_code',
|
||||
'收货仓' => 'receiving_warehouse',
|
||||
'供销商编号' => 'supplier_code',
|
||||
'供销商' => 'supplier_name',
|
||||
'快递单号' => 'express_no',
|
||||
'快递公司' => 'express_company',
|
||||
'省' => 'province',
|
||||
'市' => 'city',
|
||||
'业务员' => 'salesperson',
|
||||
'线下客户编号' => 'offline_customer_code',
|
||||
'线下客户名称' => 'offline_customer_name',
|
||||
'500112冲减收入' => 'account_500112',
|
||||
'60031erp快递费' => 'account_60031',
|
||||
'800101其他服务费' => 'account_800101',
|
||||
'800103其他活动费用' => 'account_800103',
|
||||
'800104违约赔付' => 'account_800104',
|
||||
'800105平台佣金' => 'account_800105',
|
||||
'800106保证金' => 'account_800106',
|
||||
'800107推广充值' => 'account_800107',
|
||||
'800108品牌新享' => 'account_800108',
|
||||
'800109淘宝客/多多进宝' => 'account_800109',
|
||||
'800110运费险' => 'account_800110',
|
||||
'800114公益宝贝' => 'account_800114',
|
||||
'8001201直通车/钻展/快车等' => 'account_8001201',
|
||||
'8001202推广红包冲减' => 'account_8001202',
|
||||
'800122售后补偿' => 'account_800122',
|
||||
'800125红包' => 'account_800125',
|
||||
'800127退货运费' => 'account_800127',
|
||||
'800129退款' => 'account_800129',
|
||||
'800130订单' => 'account_800130',
|
||||
'800132其他费用' => 'account_800132',
|
||||
'800133品牌新享-收入' => 'account_800133',
|
||||
'800304售后补偿-聚工单' => 'account_800304',
|
||||
'800501种菜佣金' => 'account_800501',
|
||||
'800503发空包/赠品费用' => 'account_800503',
|
||||
],
|
||||
|
||||
'number_keys' => [
|
||||
'product_quantity', 'product_amount', 'product_cost', 'reversal_order_cost',
|
||||
'manual_adjust_income', 'manual_adjust_cost', 'merchant_discount', 'freight_income',
|
||||
'payment_amount', 'platform_discount', 'reversal_order_amount', 'actual_shipped_amount',
|
||||
'actual_shipped_quantity', 'actual_shipped_cost', 'pre_application_quantity',
|
||||
'pre_application_amount', 'pre_application_cost', 'pre_return_quantity', 'pre_return_amount',
|
||||
'pre_return_cost', 'pre_refund_amount', 'pre_refund_cost', 'pre_platform_subsidy',
|
||||
'post_application_quantity', 'post_application_amount', 'post_application_cost',
|
||||
'post_return_quantity', 'post_return_amount', 'post_return_cost', 'post_refund_amount',
|
||||
'post_refund_cost', 'post_platform_subsidy', 'defective_quantity',
|
||||
'account_500112', 'account_60031', 'account_800101', 'account_800103', 'account_800104',
|
||||
'account_800105', 'account_800106', 'account_800107', 'account_800108', 'account_800109',
|
||||
'account_800110', 'account_800114', 'account_8001201', 'account_8001202', 'account_800122',
|
||||
'account_800125', 'account_800127', 'account_800129', 'account_800130', 'account_800132',
|
||||
'account_800133', 'account_800304', 'account_800501', 'account_800503',
|
||||
],
|
||||
];
|
||||
@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
/**
|
||||
* 聚水潭「商品利润 → 导出明细数据 → 订单售后费用数据」导入明细。
|
||||
*
|
||||
* 聚水潭报表已按商品编码(组合装拆成组件)给出行和成本,这里原样保留每行字段,
|
||||
* 订单售后费用明细页面在导入数据覆盖的店铺/日期范围内优先读这张表。
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('ops_after_sales_cost_rows', function (Blueprint $table): void {
|
||||
$table->bigIncrements('id');
|
||||
$table->string('row_key', 40)->unique();
|
||||
$table->string('shop_name', 191);
|
||||
$table->string('shop_code', 64)->nullable();
|
||||
$table->string('online_order_no', 64)->nullable();
|
||||
$table->string('internal_order_no', 64)->nullable();
|
||||
$table->string('product_code', 191)->nullable();
|
||||
$table->dateTime('order_time')->nullable();
|
||||
$table->decimal('product_quantity', 14, 2)->default(0);
|
||||
$table->decimal('product_amount', 16, 2)->default(0);
|
||||
$table->decimal('product_cost', 16, 2)->default(0);
|
||||
$table->string('cost_source', 32)->nullable();
|
||||
$table->string('combine_type', 32)->nullable();
|
||||
$table->json('payload');
|
||||
$table->timestamp('imported_at')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['shop_name', 'order_time']);
|
||||
$table->index('online_order_no');
|
||||
$table->index('product_code');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('ops_after_sales_cost_rows');
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,166 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\EcommerceFinance\Repositories;
|
||||
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
/**
|
||||
* 订单售后费用明细(聚水潭「商品利润 → 导出明细数据 → 订单售后费用数据」导入)。
|
||||
*
|
||||
* 聚水潭报表已经按商品编码(组合装拆成组件)给出行和成本,页面直接读这张导入表,
|
||||
* 不再从订单明细里自己拆组合装。
|
||||
*/
|
||||
class AfterSalesCostRowRepository
|
||||
{
|
||||
private const TABLE = 'ops_after_sales_cost_rows';
|
||||
|
||||
private ?bool $available = null;
|
||||
|
||||
public function available(): bool
|
||||
{
|
||||
if ($this->available === null) {
|
||||
try {
|
||||
$this->available = Schema::hasTable(self::TABLE)
|
||||
&& DB::table(self::TABLE)->exists();
|
||||
} catch (\Throwable) {
|
||||
$this->available = false;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->available;
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入数据是否覆盖本次筛选(有至少一行命中即视为覆盖)。
|
||||
*
|
||||
* @param array<string,mixed> $filters
|
||||
*/
|
||||
public function covers(array $filters): bool
|
||||
{
|
||||
if (! $this->available()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->filtered($filters)->limit(1)->exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $filters
|
||||
* @return array{rows:list<array<string,mixed>>,has_more:bool}
|
||||
*/
|
||||
public function paginate(array $filters, int $page, int $limit): array
|
||||
{
|
||||
$page = max($page, 1);
|
||||
$limit = min(max($limit, 1), 100);
|
||||
|
||||
$records = $this->filtered($filters)
|
||||
->orderByDesc('order_time')
|
||||
->orderByDesc('id')
|
||||
->forPage($page, $limit + 1)
|
||||
->get();
|
||||
|
||||
$rows = [];
|
||||
|
||||
foreach ($records as $record) {
|
||||
$payload = json_decode((string) $record->payload, true);
|
||||
$rows[] = is_array($payload) ? $payload : [];
|
||||
}
|
||||
|
||||
return [
|
||||
'rows' => array_slice($rows, 0, $limit),
|
||||
'has_more' => count($rows) > $limit,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入一行(命令内部使用,批量写入)。
|
||||
*
|
||||
* @param array<string,mixed> $payload
|
||||
*/
|
||||
public static function toRecord(array $payload, ?string $importedAt = null): array
|
||||
{
|
||||
$rowKey = sha1(implode('|', [
|
||||
(string) ($payload['shop_name'] ?? ''),
|
||||
(string) ($payload['online_order_no'] ?? ''),
|
||||
(string) ($payload['internal_order_no'] ?? ''),
|
||||
(string) ($payload['sub_order_no'] ?? ''),
|
||||
(string) ($payload['after_sales_no'] ?? ''),
|
||||
(string) ($payload['product_code'] ?? ''),
|
||||
(string) ($payload['product_quantity'] ?? ''),
|
||||
(string) ($payload['product_amount'] ?? ''),
|
||||
]));
|
||||
|
||||
$now = $importedAt ?? now()->toDateTimeString();
|
||||
|
||||
return [
|
||||
'row_key' => $rowKey,
|
||||
'shop_name' => mb_substr((string) ($payload['shop_name'] ?? ''), 0, 191),
|
||||
'shop_code' => mb_substr((string) ($payload['shop_code'] ?? ''), 0, 64),
|
||||
'online_order_no' => mb_substr((string) ($payload['online_order_no'] ?? ''), 0, 64),
|
||||
'internal_order_no' => mb_substr((string) ($payload['internal_order_no'] ?? ''), 0, 64),
|
||||
'product_code' => mb_substr((string) ($payload['product_code'] ?? ''), 0, 191),
|
||||
'order_time' => self::dateTime($payload['order_time'] ?? null),
|
||||
'product_quantity' => (float) ($payload['product_quantity'] ?? 0),
|
||||
'product_amount' => (float) ($payload['product_amount'] ?? 0),
|
||||
'product_cost' => (float) ($payload['product_cost'] ?? 0),
|
||||
'cost_source' => mb_substr((string) ($payload['cost_source'] ?? ''), 0, 32),
|
||||
'combine_type' => mb_substr((string) ($payload['combine_type'] ?? ''), 0, 32),
|
||||
'payload' => json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
||||
'imported_at' => $now,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
];
|
||||
}
|
||||
|
||||
/** @param array<string,mixed> $filters */
|
||||
private function filtered(array $filters): Builder
|
||||
{
|
||||
$query = DB::table(self::TABLE);
|
||||
$from = trim((string) ($filters['from'] ?? ''));
|
||||
$to = trim((string) ($filters['to'] ?? ''));
|
||||
$orderNo = trim((string) ($filters['order_no'] ?? ''));
|
||||
$sku = trim((string) ($filters['sku_id'] ?? ''));
|
||||
$shops = array_values(array_filter((array) ($filters['shop_names'] ?? []), static fn ($v) => trim((string) $v) !== ''));
|
||||
|
||||
if ($from !== '') {
|
||||
$query->where('order_time', '>=', $from . ' 00:00:00');
|
||||
}
|
||||
|
||||
if ($to !== '') {
|
||||
$query->where('order_time', '<=', $to . ' 23:59:59');
|
||||
}
|
||||
|
||||
if ($shops !== []) {
|
||||
$query->whereIn('shop_name', $shops);
|
||||
}
|
||||
|
||||
if ($orderNo !== '') {
|
||||
$query->where(function (Builder $inner) use ($orderNo): void {
|
||||
$inner->where('online_order_no', 'like', $orderNo . '%')
|
||||
->orWhere('internal_order_no', 'like', $orderNo . '%')
|
||||
->orWhere('internal_order_no', '=', $orderNo);
|
||||
});
|
||||
}
|
||||
|
||||
if ($sku !== '') {
|
||||
$query->where('product_code', 'like', '%' . addcslashes($sku, '%_\\') . '%');
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
private static function dateTime(mixed $value): ?string
|
||||
{
|
||||
$value = trim((string) $value);
|
||||
|
||||
if ($value === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$timestamp = strtotime($value);
|
||||
|
||||
return $timestamp === false ? null : date('Y-m-d H:i:s', $timestamp);
|
||||
}
|
||||
}
|
||||
@ -168,6 +168,21 @@ class OrderDataPageData
|
||||
public function afterSalesRows(array $filters, int $page, int $limit): array
|
||||
{
|
||||
$limit = min(max($limit, 1), 100);
|
||||
|
||||
// 聚水潭「商品利润 → 导出明细数据 → 订单售后费用数据」导入的明细:
|
||||
// 已经是按商品编码(组合装拆成组件)的行,覆盖到的店铺/日期范围优先使用。
|
||||
$imported = app(\Modules\EcommerceFinance\Repositories\AfterSalesCostRowRepository::class);
|
||||
|
||||
if ($imported->covers($filters)) {
|
||||
$result = $imported->paginate($filters, $page, $limit);
|
||||
|
||||
return [
|
||||
'rows' => $result['rows'],
|
||||
'has_more' => $result['has_more'],
|
||||
'status' => ['X-Caiwu-Data-Status' => 'imported'],
|
||||
];
|
||||
}
|
||||
|
||||
$mirror = $this->mirrorOrders ?? app(OrderDataMirrorRepository::class);
|
||||
if ($mirror->ready()) {
|
||||
$result = $mirror->afterSales($filters, $page, $limit);
|
||||
|
||||
@ -307,7 +307,7 @@
|
||||
<div class="upstream-data-pagination">
|
||||
<span>
|
||||
当前页 {{ $rows?->count() ?? 0 }} 条
|
||||
@if ($status) · {{ strtoupper($status) }} @endif
|
||||
@if ($status) · {{ $status === 'imported' ? '聚水潭导入' : strtoupper($status) }} @endif
|
||||
</span>
|
||||
<div class="upstream-data-pagination__controls">
|
||||
<a
|
||||
|
||||
65
tests/Feature/Business/AfterSalesCostImportTest.php
Normal file
65
tests/Feature/Business/AfterSalesCostImportTest.php
Normal file
@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Business;
|
||||
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Modules\EcommerceFinance\Repositories\AfterSalesCostRowRepository;
|
||||
use Modules\EcommerceFinance\View\OrderDataPageData;
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
|
||||
use Tests\Feature\FeatureTestCase;
|
||||
|
||||
class AfterSalesCostImportTest extends FeatureTestCase
|
||||
{
|
||||
public function test_imported_rows_are_served_before_the_mirror(): void
|
||||
{
|
||||
DB::table('ops_after_sales_cost_rows')->insert(AfterSalesCostRowRepository::toRecord([
|
||||
'shop_name' => '天猫-亿家达旗舰店',
|
||||
'shop_code' => '18526685',
|
||||
'online_order_no' => '5120748758529014849',
|
||||
'internal_order_no' => '8064260',
|
||||
'product_code' => 'SJ038020N1410V02',
|
||||
'order_time' => '2026-06-21 18:00:00',
|
||||
'product_quantity' => 1,
|
||||
'product_amount' => 385.43,
|
||||
'product_cost' => 110.53,
|
||||
'cost_source' => '商品成本',
|
||||
'combine_type' => '组合单品',
|
||||
]));
|
||||
|
||||
$result = app(OrderDataPageData::class)->afterSalesRows([
|
||||
'from' => '2026-06-01',
|
||||
'to' => '2026-06-30',
|
||||
'shop_names' => ['天猫-亿家达旗舰店'],
|
||||
'order_no' => '',
|
||||
'sku_id' => '',
|
||||
], 1, 20);
|
||||
|
||||
$this->assertSame('imported', $result['status']['X-Caiwu-Data-Status']);
|
||||
$this->assertCount(1, $result['rows']);
|
||||
$this->assertSame('SJ038020N1410V02', $result['rows'][0]['product_code']);
|
||||
$this->assertEquals(110.53, $result['rows'][0]['product_cost']);
|
||||
}
|
||||
|
||||
public function test_import_command_reads_the_jst_export_file(): void
|
||||
{
|
||||
$path = tempnam(sys_get_temp_dir(), 'jst') . '.xlsx';
|
||||
|
||||
$sheet = new Spreadsheet();
|
||||
$sheet->getActiveSheet()->fromArray([
|
||||
['内部订单号', '线上订单号', '店铺名称', '商品编码', '商品数量', '商品金额', '商品成本', '成本来源', '组合装', '订单时间'],
|
||||
['8064260', '5120748758529014849', '天猫-亿家达旗舰店', 'SJ038051N1410V01', 1, 14.54, 4.17, '商品成本', '组合单品', '2026-06-21 18:00:00'],
|
||||
]);
|
||||
(new Xlsx($sheet))->save($path);
|
||||
|
||||
$this->artisan('order-data:import-after-sales-cost', ['file' => $path])->assertSuccessful();
|
||||
|
||||
$this->assertDatabaseHas('ops_after_sales_cost_rows', [
|
||||
'shop_name' => '天猫-亿家达旗舰店',
|
||||
'product_code' => 'SJ038051N1410V01',
|
||||
'cost_source' => '商品成本',
|
||||
]);
|
||||
|
||||
@unlink($path);
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user