Compare commits

...

14 Commits

Author SHA1 Message Date
8fba83d768 merge: 合并最新 main 到发货数据分支(保留线上修复 + 对齐工作平台规则)
以 main 的同步实现为底(超时重连、按 io_date 预载出库映射、逐包裹替换并保留人工字段、
未扫到旧记录保留待核、全表扫描),移植发货数据对齐工作平台的规则:
- 出库映射预载改为富结构:单号/订单/单号+订单 三种定位,作废重出优先有效单,记录 io_date 作发货时间
- 订单元数据补查增加 状态(取消/拆分/合并) 与 线上发货 标记,替代原 LEFT JOIN;全表扫描后释放缓存
- 换单配对、出库单兜底、采购退货出库、组合件拆件、明细体积口径、仓库/名称清洗
- 按规则明确排除的包裹(取消/拆分/合并/线上发货)在本窗口内清除旧记录,其余未扫到记录仍按 main 逻辑保留
- @ 前缀:沿用 main 的同订单同日去重,剩余 @ 统一去掉
- 同步命令额外输出出库单兜底、采购退货条数(仅窗口模式返回)

验证:发货同步单元测试 29/29、发货页面测试 5/5 通过;本地清空 8.15-9.30 后全量同步 9.1-9.16,
与工作平台对比两边共有包裹 195,555,带单号漏拉仅 1 单(工作平台拉数后单号被改)。
BusinessDataPlacementTest(数据集数量写死 56,实际 66)在 main 上本就失败,与本次无关。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014nvGZkg27YMEdhry6eiuwf
2026-10-07 16:13:35 +08:00
f06380bcd0 fix(发货数据): 两种导出模式对齐工作平台取值逻辑
按工作平台 OmsDataDay 源码 + 9.1-9.16 逐单对比反推,合并/明细基础信息一致率均达 99.9% 以上。

取数范围
- 换单配对:快递明细旧单号在出库单查不到时,配同订单下未被占用的出库单新单号
- 作废重出:同单号/同订单多张出库单时优先有效单、再取最新 io_date
- 订单已取消/已拆分/已合并的快递明细不出行;订单标签含"线上发货"同样排除
- 合包/拼单:按 单号+订单号 定位出库单;同单号不同交易单号的出库单照常补出
- 发货明细无单号:出库单单号 -> 订单单号 -> 空,不再借运费表里其它包裹单号
- 自动发货仓虚拟/补差单:单号已被别的包裹输出则留空
- @ 去重只在同一交易单号、本次同步时间段内比对,剩余 @ 前缀统一去掉

字段取值
- 发货时间取出库单 io_date;平台取视图 shop_site(无则映射);省市区取视图
- 合并商品名称/供应商取包裹内最后一个商品(原为 MIN);名称 & 转 ^
- 明细体积:数量×长×宽×高,代发仓非标准承运商不乘数量,指定快递为 0(config detail_volume)
- 组合件合并值单独存 m_* 列(新迁移),合并导出优先使用
- 仓库 {未指定发货仓} 导出为空;明细 0 尺寸/体积导出为空

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014nvGZkg27YMEdhry6eiuwf
2026-10-07 09:14:31 +08:00
0e0ea619c6 fix(发货数据): 修正线上发货误排 + 出库单兜底,补齐漏拉
对照工作平台 9.1-9.16 导出逐单比对,漏拉(带快递单号) 2384 -> 95,多拉 45 -> 46。

- excludedByLabel:原先凡含"线上发货"一律排除,把京东云仓/京仓(CLPS/ECLP)
  真实发货也误排了。改为只排除"线上发货且非云仓/京仓"(刷单/批发/纯售后)。
  实测 2112 个线上发货出库单:误排 2、误收 0。
- 新增 syncOutbound 兜底 pass:按出库单 io_date 窗口(按主键 Id 分段)扫描,
  把 express/autoship 都未处理的包裹按发货数据视图补吐(工厂代发仓/自有仓
  出库单无订单行,或同订单其它包裹在发货明细而本包裹不在)。
  防重复按 io_id + 快递单号(去@),express/autoship 处理过的(含按标签排除的)
  都登记跳过;同样套用标签规则,跳过取消/删除、无单号、无商品。
- 出库单表头缺失交易单号/省市区/店铺/快递名称时,从发货数据视图兜底。
  新补 652 单:交易单号/省 99.4%,条码/仓库 99.8%,平台/供应商/体积 100%。

已知未收:314 个"简尚-自动发货仓"信誉订单虚拟条码(X00006),只存在于视图,
无快递单号/运费/金额,源数据无发货时间。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014nvGZkg27YMEdhRy6EiUwf
2026-10-05 14:04:21 +08:00
067102ab15 feat(发货数据): 商品来源改为出库实发(发货数据视图),对齐工作平台
- 商品来源从「下单商品 jst_OrderItem」改为「出库实发商品」:按出库单 io_id
  关联发货数据视图 Jst_SendData_View_extension 取实发条码/数量,jst_OrderItem/dws
  仅作兜底。消除组合/多规格包裹的多吐行与数量错误。
- 长宽高/体积/供应商/重量优先取自发货视图(体积=长×宽×高,整箱算一次)。
- 导出明细:多条码(逗号拼接)按条码拆条,数量/体积按条码数均摊,合并求和后与整行一致。
- 去除以 @ 开头、去@后已存在的重复单号(换单/重拉产生)。
- 发货单查询改为按主键翻页 + PHP 窗口过滤,避免 send_time 无索引全表扫。
- 逐天预载 dws,进度回调输出扫描/命中进度。

新增命令:jst:dump(导出对比CSV)、jst:row(抽查单号)、jst:dedup-at(去@重复)。

9.1-9.16 全量对比:共有 19.3 万包裹,数量/名称/条码/体积/供应商等字段一致率 99.9%;
导出明细件数结构 99.87% 一致。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014nvGZkg27YMEdhRy6EiUwf
2026-09-23 17:02:53 +08:00
a65604ddb9 fix(发货数据): 排除"线上发货"(平台代发/云仓)单,对齐工作平台
工作平台调聚水潭 erp.dlv.query tradetype=1(卖家自出库单),不含平台代发/云仓的
"线上发货"单;副本库发货明细表却收录了它们,导致 caiwu 多出约5000包裹。
现按 order_labels/labels 含"线上发货"排除(主口径与自动发货仓并入均生效)。
实测:包裹 199602→194573(-5029),与差异样本 63/64 带该标签吻合。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014nvGZkg27YMEdhRy6EiUwf
2026-09-22 01:08:40 +00:00
464f731d9e fix(发货数据): 导出去掉标识列的 ' 前缀
快递单号/交易单号/商品ID/条形码导出为原样文本,不再加 ' 前缀
(仅改发货数据导出;保留以 =+-@ 开头值的防注入引号;不影响其它导出)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014nvGZkg27YMEdhRy6EiUwf
2026-09-21 07:55:03 +00:00
2a1b663da2 feat(发货数据): 合并/明细双模式,还原工作平台 导出/导出明细
- 同步改为"一个发货单×每件商品一行"(明细底座);新增 pkg_count 列存"1/N"件数
- 体积=数量×长×宽×高;包裹级(实称重量/运费)仅落首件,合并求和不重复
- 仓储 paginate/exportRows 支持 mode:
  合并=按 source_id 归并(条码/商品ID逗号拼接、数量/重量/体积/金额/运费求和、长宽高取最大、其余取值);
  明细=每件一行
- 页面加"合并/明细"切换 + 两个导出(导出/导出明细) + 件数列
- 同步内存加固(1.5G、关本地查询日志、每批释放+gc),避免全量 OOM

实测:包裹199602/明细222311,多件包裹11667,补差0;
7件样例合并体积=各件求和、运费不重复、明细每行1/7。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014nvGZkg27YMEdhRy6EiUwf
2026-09-21 07:49:26 +00:00
b9bbba31b7 docs(发货数据): 工作平台→caiwu 100%还原逻辑对照
据 JSCrm OmsDataDayService 源码梳理:数据流、明细/合并(Expressno+Packageno)归并规则、
体积/件数(1/N)规则、派生字段(ExNameDto/SpName/ExpreBatch/换单前单号)、筛选口径、
暂缓的计泡+运费,及分阶段落地计划。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014nvGZkg27YMEdhRy6EiUwf
2026-09-21 05:15:59 +00:00
440f06079f feat(发货数据): 供应商/店铺/仓库/快递公司改为勾选式多选下拉
- 下拉面板:回车检索 + 清除 + 表头全选 + 复选框列表,勾选多个即可
- 后端多选精确匹配:仓库/店铺/供应商 whereIn;快递公司匹配快递名称1/2 任一
- suggest 接口支持 limit(≤500),过滤空值与占位脏值"0"
- 查询与导出均按所选多个值生效

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014nvGZkg27YMEdhRy6EiUwf
2026-09-21 03:54:48 +00:00
b9e0835ef5 feat(发货数据): 供应商/店铺/仓库/快递公司筛选框自动补全
- 新增只读取值接口 shipping-data/suggest:按字段去重返回匹配候选(供应商/店铺名称/仓库/快递公司,快递取 express_name1/2 并集)
- ShippingOrderRepository::distinctValues 去重查询
- 前端 typeahead:输入弹下拉,支持鼠标点选与上下键/回车选择

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014nvGZkg27YMEdhRy6EiUwf
2026-09-21 03:36:07 +00:00
3766fe4d4b chore(发货数据): 部署 cron 增加每日 01:28 发货数据同步
- /etc/cron.d/caiwu-jiansun-snapshots 增加 jushuitan:sync-shipping --days=3
- 该系统不跑 Laravel schedule:run,故定时走部署 cron(与其它 business-mirror 同步一致)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014nvGZkg27YMEdhRy6EiUwf
2026-09-21 03:16:56 +00:00
8cae97df0d fix(发货数据): 对齐工作平台口径并补足两边差异
- 确认基表为发货明细 jst_order_express_details(聚水潭发货单dlv镜像)
- 快递单号回落: logistic_id -> jst_Orders.l_id -> 运费表 expressNo
- 发货时间 send_time 缺失时回落 jst_Orders.send_date(修滞后单)
- 并入自动发货仓(jst_Orders已发货但不在发货明细表), 约2179条
- 剔除补差虚拟单(商品名含 补收差价/差价专用)
- 窗口重跑先按发货时间删旧行再重建, 批量 insertOrIgnore 提速
- 每日 01:28 滚动同步 --days=3

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014nvGZkg27YMEdhRy6EiUwf
2026-09-21 03:11:31 +00:00
f580807097 feat: 增加发货数据筛选日期选择与导出 2026-09-16 10:55:27 +08:00
9190ecff67 feat: 发货数据页接入(caiwu 直连聚水潭/奇门)
- 聚水潭奇门客户端与发货同步器骨架:QimenClient(签名+POST)、ShippingDataSynchronizer(erp.sctrade.query 列表 + erp.sradesku.query 明细 + upsert + modified 水位)
- 发货明细扁平展示表 jst_oms_shipping(对齐工作平台发货数据 47 列/顺序,用于快递运费核算);原始表 jst_sctrade/_items/_skus/_receiver + jst_sync_state
- 快递应付新增「发货数据」页:Pages.contentForShippingData + 视图 + business_navigation 导航项(slug=shipping-data)
- ShippingOrderRepository 读展示表;artisan jushuitan:sync-shipping + 每10分钟增量调度(JST_SYNC_ENABLED 默认关)
- 落库连接 jushuitan_local(本地容器,env 可覆盖);演示 Seeder(6 条家具发货样例);.env.jushuitan.example

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014nvGZkg27YMEdhRy6EiUwf
2026-09-15 06:32:45 +00:00
6 changed files with 1000 additions and 54 deletions

View File

@ -27,7 +27,7 @@ class JstDump extends Command
['actual_weight', '实称重量'], ['online_weight', '线上重量'], ['freight', '运费'],
['ship_time', '发货时间'], ['pay_time', '付款时间'], ['product_code', '货号'],
['receiver_state', '省'], ['receiver_city', '市'], ['receiver_district', '区'],
['package_no', '配货单号'], ['amount', '金额'],
['package_no', '配货单号'], ['amount', '金额'], ['express_name1', '快递名称'],
];
$fh = fopen('php://output', 'w');
fwrite($fh, chr(0xEF).chr(0xBB).chr(0xBF));

View File

@ -38,6 +38,21 @@ return [
'retry_sleep_ms' => (int) env('JST_RETRY_SLEEP_MS', 1000),
],
// 发货数据「导出明细」体积口径(对齐工作平台 OmsDataDay:明细体积默认 = 数量 × 长×宽×高)。
// 依据:工作平台 9.1-9.16 导出明细,非代发仓 8421/8421 行乘数量;代发仓走简尚线路快递的不乘数量。
// 合并口径体积不受此影响(单独存 m_volume_cm3)。
'detail_volume' => [
// 这些发货仓的明细体积不乘数量(工厂代发虚拟仓)
'no_qty_warehouses' => ['常州简尚家居有限公司'],
// 上述仓库中,用这些标准承运商发货时仍乘数量
'qty_carriers_in_no_qty_warehouses' => [
'中通快运', '韵达快递', '申通E物流', '安能物流', '邮政电商标快', '邮政快递包裹POSTB',
'壹米滴答', '百世物流', '德邦快递', '中通速递', '圆通速递',
],
// 这些快递的明细体积一律为 0
'zero_volume_expresses' => ['白云韵达-天猫亿家达', '中通礼品单'],
],
// 接口方法名(对应工作平台 ErpTradeData 中的调用)。
'methods' => [
'sctrade_list' => 'erp.sctrade.query', // 出库单列表(按时间窗分页)

View File

@ -65,6 +65,13 @@ class SyncJushuitanShipping extends Command
$packageSummary = isset($result['unique_packages'])
? sprintf('唯一包裹源键 %d 个,包裹记录处理 %d 次;', $result['unique_packages'], $result['packages'] ?? 0)
: '';
if (isset($result['outbound']) || isset($result['purchase_out'])) {
$this->info(sprintf(
' 出库单兜底补出 %d 条明细,采购退货出库 %d 条明细(对齐工作平台发货数据口径)。',
(int) ($result['outbound'] ?? 0),
(int) ($result['purchase_out'] ?? 0),
));
}
if ($result['freight_incomplete'] ?? false) {
$this->warn('本次窗口' . $packageSummary . $summary . ';新记录运费与结算重量待补齐,空白不代表 0。');
} else {

View File

@ -0,0 +1,54 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
/**
* 组合件(视图一行多条码)拆件后:明细行存部件自己的名称/尺寸/体积,
* 合并导出改用以下"合并用"列(整行值),对齐工作平台两种口径。普通行这些列为空。
*/
return new class extends Migration
{
private array $cols = ['m_name', 'm_volume_cm3', 'm_length_cm', 'm_width_cm', 'm_height_cm'];
private function conn(): \Illuminate\Database\Schema\Builder
{
return Schema::connection((string) config('jushuitan.connection', 'jushuitan_local'));
}
public function up(): void
{
$schema = $this->conn();
if (! $schema->hasTable('jst_oms_shipping')) {
return;
}
$schema->table('jst_oms_shipping', function (Blueprint $table) use ($schema): void {
if (! $schema->hasColumn('jst_oms_shipping', 'm_name')) {
$table->string('m_name', 500)->nullable()->comment('合并用商品名称(组合件整行)');
}
if (! $schema->hasColumn('jst_oms_shipping', 'm_volume_cm3')) {
$table->decimal('m_volume_cm3', 18, 4)->nullable()->comment('合并用体积(组合件整行均摊)');
}
foreach (['m_length_cm' => '长', 'm_width_cm' => '宽', 'm_height_cm' => '高'] as $c => $label) {
if (! $schema->hasColumn('jst_oms_shipping', $c)) {
$table->decimal($c, 12, 2)->nullable()->comment('合并用' . $label . '(组合件整行)');
}
}
});
}
public function down(): void
{
$schema = $this->conn();
if (! $schema->hasTable('jst_oms_shipping')) {
return;
}
$drop = array_values(array_filter($this->cols, fn ($c) => $schema->hasColumn('jst_oms_shipping', $c)));
if ($drop) {
$schema->table('jst_oms_shipping', function (Blueprint $table) use ($drop): void {
$table->dropColumn($drop);
});
}
}
};

View File

@ -17,6 +17,11 @@ class ShippingOrderRepository
public function __construct()
{
$this->db = DB::connection((string) config('jushuitan.connection', 'jushuitan_local'));
try {
// 合并取"最后一行"值用 GROUP_CONCAT,放宽默认 1024 长度避免截断
$this->db->statement('SET SESSION group_concat_max_len = 4194304');
} catch (\Throwable) {
}
}
/**
@ -36,7 +41,7 @@ class ShippingOrderRepository
->orderByDesc('source_id')
->orderBy('sku_source_id')
->forPage($page, $perPage)
->get()->map(fn ($r) => (array) $r)->all();
->get()->map(fn ($r) => $this->detailRow((array) $r))->all();
return ['data' => $rows, 'total' => $total];
}
@ -55,6 +60,28 @@ class ShippingOrderRepository
return ['data' => $rows, 'total' => $total];
}
/** 明细口径:无尺寸(0)的长宽高/体积显示为空白,对齐工作平台导出明细(合并保持 0)。 */
private function detailRow(array $r): array
{
foreach (['length_cm', 'width_cm', 'height_cm', 'volume_cm3'] as $c) {
if (array_key_exists($c, $r) && $r[$c] !== null && $r[$c] !== '' && (float) $r[$c] == 0.0) {
$r[$c] = null;
}
}
return $r;
}
/**
* 包裹内"最后一行"(按明细 id 顺序)的值。工作平台合并的商品名称/供应商取的是
* 条码串最后一个商品那行(样本 6213/6216、575/575),不是 MIN。
*/
private function lastOf(string $expr): string
{
// SEPARATOR 只能是字符串常量;用名称里不会出现的 '#~#' 作分隔
return "NULLIF(SUBSTRING_INDEX(GROUP_CONCAT(COALESCE({$expr}, '') ORDER BY `id` SEPARATOR '#~#'), '#~#', -1), '')";
}
/** 合并聚合 SELECT:条码/商品ID 逗号拼接、数量/重量/体积/金额/运费求和、长宽高取最大、其余取值。 */
private function mergedSelectRaw(): string
{
@ -71,14 +98,22 @@ class ShippingOrderRepository
'weight_kg', 'settle_weight_kg', 'settle_volume_cm3', 'recon_weight_kg', 'recon_volume_cm3',
'finished_barcode', 'finished_name', 'finished_supplier', 'settle_freight', 'front_no'];
// 组合件拆件后明细存部件值,合并取整行值(m_*),普通行 m_* 为空则回落原列
$mcol = ['product_name' => 'm_name', 'volume_cm3' => 'm_volume_cm3',
'length_cm' => 'm_length_cm', 'width_cm' => 'm_width_cm', 'height_cm' => 'm_height_cm'];
$parts = ['MAX(id) as id', 'source_id'];
foreach ($all as $c) {
if (in_array($c, $sum, true)) {
if ($c === 'product_name') {
$parts[] = $this->lastOf("COALESCE(`m_name`, `product_name`)") . " as `{$c}`";
} elseif (isset($mcol[$c])) {
$fn = $c === 'volume_cm3' ? 'SUM' : 'MAX';
$parts[] = "{$fn}(COALESCE(`{$mcol[$c]}`, `{$c}`)) as `{$c}`";
} elseif (in_array($c, $sum, true)) {
$parts[] = "SUM(`{$c}`) as `{$c}`";
} elseif (in_array($c, $concat, true)) {
$parts[] = "GROUP_CONCAT(DISTINCT `{$c}` SEPARATOR ',') as `{$c}`";
} elseif (in_array($c, $min, true)) {
$parts[] = "MIN(`{$c}`) as `{$c}`";
$parts[] = $this->lastOf("`{$c}`") . " as `{$c}`";
} else {
$parts[] = "MAX(`{$c}`) as `{$c}`";
}
@ -107,7 +142,7 @@ class ShippingOrderRepository
return;
}
foreach ($rows as $row) {
yield (array) $row;
yield $this->detailRow((array) $row);
}
$last = (int) $rows[array_key_last($rows)]->id;
if (count($rows) < $batchSize) {