订单售后费用明细:改读聚水潭商品利润原表 jst_goods_profit_kpi_items_afs #425

Merged
renshen merged 1 commits from feature/renshen-goods-profit-afs into main 2026-10-21 14:03:19 +08:00
5 changed files with 228 additions and 1 deletions

View File

@ -22,6 +22,7 @@ return [
'店铺名称' => 'shop_name',
'店铺编码' => 'shop_code',
'商品编码' => 'product_code',
'款式编码' => 'style_code',
'款式编码(参考)' => 'style_code',
'店铺商品编码' => 'shop_product_code',
'店铺款式编码' => 'shop_style_code',
@ -94,6 +95,7 @@ return [
'市' => 'city',
'业务员' => 'salesperson',
'线下客户编号' => 'offline_customer_code',
'线下客户编码' => 'offline_customer_code',
'线下客户名称' => 'offline_customer_name',
'500112冲减收入' => 'account_500112',
'60031erp快递费' => 'account_60031',
@ -101,6 +103,7 @@ return [
'800103其他活动费用' => 'account_800103',
'800104违约赔付' => 'account_800104',
'800105平台佣金' => 'account_800105',
'800105佣金' => 'account_800105',
'800106保证金' => 'account_800106',
'800107推广充值' => 'account_800107',
'800108品牌新享' => 'account_800108',

View File

@ -396,6 +396,32 @@ return [
['as_id', 'asi_id'],
],
],
// 聚水潭「商品利润 → 导出明细数据 → 订单售后费用数据」的原表。
// 列名是 undefined1..undefinedN,中文表头由 ..._schemas 表提供映射。
'operations_replica.rpt.jst_goods_profit_kpi_items_afs' => [
'source_key' => 'operations_replica',
'schema' => 'rpt',
'object' => 'jst_goods_profit_kpi_items_afs',
'target' => 'ops_jst_goods_profit_kpi_items_afs',
'keys' => ['id'],
'watermark' => 'updatedAt',
'batch_size' => 2000,
'max_batches_per_run' => 4,
'indexes' => [
['undefined8', 'undefined7'],
['undefined2'],
['undefined10'],
],
],
'operations_replica.rpt.jst_goods_profit_kpi_items_afs_schemas' => [
'source_key' => 'operations_replica',
'schema' => 'rpt',
'object' => 'jst_goods_profit_kpi_items_afs_schemas',
'target' => 'ops_jst_goods_profit_kpi_items_afs_schemas',
'keys' => ['id'],
'watermark' => 'id',
],
'operations_replica.rpt.jst_originalBillInfo' => [
'source_key' => 'operations_replica',
'schema' => 'rpt',

View File

@ -0,0 +1,184 @@
<?php
namespace Modules\EcommerceFinance\Repositories;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
/**
* 订单售后费用明细:聚水潭「商品利润 → 导出明细数据 → 订单售后费用数据」原表。
*
* 原表 rpt.jst_goods_profit_kpi_items_afs 的列名是 undefined1..undefinedN,
* 中文表头放在 rpt.jst_goods_profit_kpi_items_afs_schemas(column → name)。
* 两张表都镜像到共享业务库后,页面按 schemas 映射读取字段。
*/
class GoodsProfitAfsRepository
{
public const MAIN_DATASET = 'operations_replica.rpt.jst_goods_profit_kpi_items_afs';
public const SCHEMA_DATASET = 'operations_replica.rpt.jst_goods_profit_kpi_items_afs_schemas';
private const MAIN_TABLE = 'ops_jst_goods_profit_kpi_items_afs';
private const SCHEMA_TABLE = 'ops_jst_goods_profit_kpi_items_afs_schemas';
/** @var array<string,string>|null */
private ?array $fieldColumns = null;
public function ready(): bool
{
try {
return Schema::connection($this->connectionName())->hasTable(self::MAIN_TABLE)
&& Schema::connection($this->connectionName())->hasTable(self::SCHEMA_TABLE)
&& $this->connection()->table(self::MAIN_TABLE)->exists();
} catch (\Throwable) {
return false;
}
}
/**
* 页面字段键 => 上游列名(undefinedN)。
*
* @return array<string,string>
*/
public function fieldColumns(): array
{
if ($this->fieldColumns !== null) {
return $this->fieldColumns;
}
$headers = (array) config('order_after_sales_cost.headers', []);
$map = [];
foreach ($this->connection()->table(self::SCHEMA_TABLE)->orderBy('id')->get(['column', 'name']) as $schema) {
$label = self::normalizeLabel((string) $schema->name);
$field = $headers[$label] ?? null;
// 同一科目出现多次(如 800105佣金 / 800105平台佣金)时保留第一次映射。
if ($field !== null && ! isset($map[$field])) {
$map[$field] = (string) $schema->column;
}
}
return $this->fieldColumns = $map;
}
/** @param array<string,mixed> $filters */
public function covers(array $filters): bool
{
return $this->ready() && $this->filtered($filters, $this->fieldColumns())->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);
$columns = $this->fieldColumns();
$select = array_values(array_unique(array_merge(['id'], array_values($columns))));
$orderColumn = $columns['order_time'] ?? null;
$query = $this->filtered($filters, $columns)->select($select);
if ($orderColumn !== null) {
$query->orderByDesc($orderColumn);
}
$records = $query->orderByDesc('id')->forPage($page, $limit + 1)->get();
$numberKeys = array_flip((array) config('order_after_sales_cost.number_keys', []));
$rows = [];
foreach ($records as $record) {
$row = [];
foreach ($columns as $field => $column) {
$value = $record->{$column} ?? null;
if (isset($numberKeys[$field])) {
$value = $value === null ? null : (float) $value;
}
$row[$field] = is_string($value) ? trim($value) : $value;
}
$rows[] = $row;
}
return [
'rows' => array_slice($rows, 0, $limit),
'has_more' => count($rows) > $limit,
];
}
/**
* @param array<string,mixed> $filters
* @param array<string,string> $columns
*/
private function filtered(array $filters, array $columns): \Illuminate\Database\Query\Builder
{
$query = $this->connection()->table(self::MAIN_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 !== '' && isset($columns['order_time'])) {
$query->where($columns['order_time'], '>=', $from . ' 00:00:00');
}
if ($to !== '' && isset($columns['order_time'])) {
$query->where($columns['order_time'], '<=', $to . ' 23:59:59');
}
if ($shops !== [] && isset($columns['shop_name'])) {
$query->whereIn($columns['shop_name'], $shops);
}
if ($orderNo !== '') {
$query->where(function ($inner) use ($columns, $orderNo): void {
$matched = false;
foreach (['online_order_no', 'internal_order_no'] as $field) {
if (! isset($columns[$field])) {
continue;
}
$inner->orWhere($columns[$field], 'like', $orderNo . '%');
$matched = true;
}
if (! $matched) {
$inner->whereRaw('1 = 0');
}
});
}
if ($sku !== '' && isset($columns['product_code'])) {
$query->where($columns['product_code'], 'like', '%' . addcslashes($sku, '%_\\') . '%');
}
return $query;
}
private function connection(): \Illuminate\Database\ConnectionInterface
{
return DB::connection($this->connectionName());
}
private function connectionName(): string
{
return (string) config('upstream_mirrors.connection', 'business_mirror');
}
private static function normalizeLabel(string $label): string
{
$label = str_replace(["\u{3000}", "\u{00A0}"], ' ', $label);
$label = preg_replace('/\s+/u', ' ', $label) ?? $label;
return trim($label);
}
}

View File

@ -169,6 +169,20 @@ class OrderDataPageData
{
$limit = min(max($limit, 1), 100);
// 聚水潭「商品利润 → 导出明细数据 → 订单售后费用数据」原表:
// 已按商品编码(组合装拆成组件)给出行和成本,镜像就绪后优先使用。
$goodsProfit = app(\Modules\EcommerceFinance\Repositories\GoodsProfitAfsRepository::class);
if ($goodsProfit->covers($filters)) {
$result = $goodsProfit->paginate($filters, $page, $limit);
return [
'rows' => $result['rows'],
'has_more' => $result['has_more'],
'status' => ['X-Caiwu-Data-Status' => 'goods_profit'],
];
}
// 聚水潭「商品利润 → 导出明细数据 → 订单售后费用数据」导入的明细:
// 已经是按商品编码(组合装拆成组件)的行,覆盖到的店铺/日期范围优先使用。
$imported = app(\Modules\EcommerceFinance\Repositories\AfterSalesCostRowRepository::class);

View File

@ -307,7 +307,7 @@
<div class="upstream-data-pagination">
<span>
当前页 {{ $rows?->count() ?? 0 }} 条
@if ($status) · {{ $status === 'imported' ? '聚水潭导入' : strtoupper($status) }} @endif
@if ($status) · {{ ['imported' => '聚水潭导入', 'goods_profit' => '聚水潭商品利润'][$status] ?? strtoupper($status) }} @endif
</span>
<div class="upstream-data-pagination__controls">
<a