diff --git a/app/Console/Commands/SyncStoreReceiptOrderMatches.php b/app/Console/Commands/SyncStoreReceiptOrderMatches.php index fc8855bd..50ce14ae 100644 --- a/app/Console/Commands/SyncStoreReceiptOrderMatches.php +++ b/app/Console/Commands/SyncStoreReceiptOrderMatches.php @@ -551,10 +551,11 @@ class SyncStoreReceiptOrderMatches extends Command ['ops_jst_orders', 'ops_jst_order_item', 'buyer_paid_amount', 'ops_jst', 'seller_income_amount'], ] as [$ordersTable, $itemsTable, $paidColumn, $tag, $paidAltColumn]) { try { - // 商品金额只取主单(link_o_id 为空/0):聚水潭的拆单子单会把主单的商品行再列一遍, - // 子单的平台子订单号与主单不同时,按「子订单号 + 条码」的去重键拦不住,会把同一批 - // 商品重复累加。整单没有任何主单行时退化为取全部行,避免把真实金额抹掉——与实付 - // 「主单优先,无主单退化」保持同一套规则。 + // 商品金额取数优先级:主单(link_o_id 为空/0)→ 原单行(merge_so_id 为空)→ 全部行。 + // - 拆单:主单优先,子单会把主单的商品行再列一遍,按「子订单号 + 条码」的去重键拦不住; + // - 合单发货(拼多多/京喜/抖音/京东等):原单行与合并汇总行共用同一线上单号、互相 link, + // 没有「主单」。此时整单退化为全部行会把被合并进来的另一单商品算到本单号上; + // 优先只取原单行(merge_so_id 为空),原单行不存在时再退化为全部行。 $mirror->statement('DROP TEMPORARY TABLE IF EXISTS tmp_receipt_main_nos'); $mirror->statement('CREATE TEMPORARY TABLE tmp_receipt_main_nos (no varchar(64) NOT NULL, PRIMARY KEY(no)) ENGINE=InnoDB'); $mirror->statement( @@ -564,6 +565,15 @@ class SyncStoreReceiptOrderMatches extends Command STRAIGHT_JOIN {$ordersTable} o ON o.so_id = t.no WHERE o.link_o_id IS NULL OR o.link_o_id = 0" ); + $mirror->statement('DROP TEMPORARY TABLE IF EXISTS tmp_receipt_own_nos'); + $mirror->statement('CREATE TEMPORARY TABLE tmp_receipt_own_nos (no varchar(64) NOT NULL, PRIMARY KEY(no)) ENGINE=InnoDB'); + $mirror->statement( + "INSERT IGNORE INTO tmp_receipt_own_nos (no) + SELECT DISTINCT o.so_id as no + FROM tmp_receipt_order_nos t + STRAIGHT_JOIN {$ordersTable} o ON o.so_id = t.no + WHERE o.merge_so_id IS NULL OR o.merge_so_id = ''" + ); // 同一线上订单号在聚水潭可能对应多个 o_id(拆单/子单),且子单会重复包含 // 同一批子订单商品行;先按「子订单号 + 条码」去重再求和,避免重复计算。 @@ -580,30 +590,38 @@ class SyncStoreReceiptOrderMatches extends Command STRAIGHT_JOIN {$ordersTable} o ON o.so_id = t.no STRAIGHT_JOIN {$itemsTable} i ON i.o_id = o.o_id LEFT JOIN tmp_receipt_main_nos m ON m.no = o.so_id + LEFT JOIN tmp_receipt_own_nos w ON w.no = o.so_id WHERE (o.link_o_id IS NULL OR o.link_o_id = 0) - OR m.no IS NULL + OR (m.no IS NULL AND (o.merge_so_id IS NULL OR o.merge_so_id = '')) + OR (m.no IS NULL AND w.no IS NULL) GROUP BY o.so_id, o.shop_name, dedup_key ) d GROUP BY so_id, shop_name" ); - // 订单实付:取订单级字段(明细行上的实付会重复);拆单取主单(link_o_id 为空), - // 没有主单时退化为取该单号下的最大值。 + // 订单实付:取订单级字段(明细行上的实付会重复);优先级与商品金额一致: + // 主单(link_o_id 为空)→ 原单行(merge_so_id 为空)→ 兜底取该单号下的最大值。 + // 合单发货两行互相 link、没有主单,直接取最大值会取到合并汇总行(含被合并的另一单), + // 必须让原单行优先;实付、卖家实收、应付三个字段同一套规则。 $paid = $mirror->select( "SELECT so_id, shop_name, COALESCE( MAX(CASE WHEN link_o_id IS NULL OR link_o_id = 0 THEN paid END), + MAX(CASE WHEN merge_so_id IS NULL OR merge_so_id = '' THEN paid END), MAX(paid) ) as paid , COALESCE( MAX(CASE WHEN link_o_id IS NULL OR link_o_id = 0 THEN paid_alt END), + MAX(CASE WHEN merge_so_id IS NULL OR merge_so_id = '' THEN paid_alt END), MAX(paid_alt) ) as paid_alt , COALESCE( MAX(CASE WHEN link_o_id IS NULL OR link_o_id = 0 THEN paid_pay END), + MAX(CASE WHEN merge_so_id IS NULL OR merge_so_id = '' THEN paid_pay END), MAX(paid_pay) ) as paid_pay FROM ( SELECT o.so_id as so_id, o.shop_name as shop_name, o.link_o_id as link_o_id, + o.merge_so_id as merge_so_id, o.{$paidColumn} as paid, o.{$paidAltColumn} as paid_alt, o.pay_amount as paid_pay FROM tmp_receipt_order_nos t STRAIGHT_JOIN {$ordersTable} o ON o.so_id = t.no diff --git a/app/Http/Controllers/Business/Pages.php b/app/Http/Controllers/Business/Pages.php index 1945f322..3b47f032 100644 --- a/app/Http/Controllers/Business/Pages.php +++ b/app/Http/Controllers/Business/Pages.php @@ -292,6 +292,7 @@ class Pages extends Controller . " OR store_name LIKE '京东-亿家达全屋%' OR store_name LIKE '京东-朴美家居%')" . ' AND ' . $effective . ' < -0.005'; $jdSofiaPay = 'COALESCE((SELECT COALESCE(MAX(CASE WHEN o.link_o_id IS NULL OR o.link_o_id = 0 THEN o.pay_amount END),' + . " MAX(CASE WHEN o.merge_so_id IS NULL OR o.merge_so_id = '' THEN o.pay_amount END)," . ' MAX(o.pay_amount)) FROM ops_jst_orders o WHERE o.so_id = store_receipt_order_match_rows.online_order_no), 0)'; $jdSofiaGuobu = '(' . $jdSofiaPay . ' - COALESCE(income_amount, 0)) > 0.005'; $rules['差额=订单实付包含商家承担优惠和以旧换新国补'] = $jdSofiaGuard . ' AND ' . $jdSofiaGuobu; @@ -356,7 +357,7 @@ class Pages extends Controller $row = DB::connection((string) config('upstream_mirrors.connection', 'business_mirror')) ->table('ops_jst_orders') ->where('so_id', $orderNo) - ->selectRaw('COALESCE(MAX(CASE WHEN link_o_id IS NULL OR link_o_id = 0 THEN pay_amount END), MAX(pay_amount)) as pay') + ->selectRaw("COALESCE(MAX(CASE WHEN link_o_id IS NULL OR link_o_id = 0 THEN pay_amount END), MAX(CASE WHEN merge_so_id IS NULL OR merge_so_id = '' THEN pay_amount END), MAX(pay_amount)) as pay") ->first(); $payCache[$orderNo] = (float) ($row->pay ?? 0); } diff --git a/config/business_navigation.php b/config/business_navigation.php index 3139c4ca..b05544a4 100644 --- a/config/business_navigation.php +++ b/config/business_navigation.php @@ -807,7 +807,7 @@ $navigation = [ 'directory' => [ 'icon' => 'receipt_long', 'status' => '共享镜像', - 'description' => '覆盖店铺收款账户账单各平台页签(总账备注=订单收入),按「店铺 + 订单号」匹配订单侧商品金额与订单实付金额;一行 = 一个店铺的一个订单,商品金额只挂一次。「商品金额」只取主单(拆单子单不参与,整单没有主单时退化为取全部行),并排除虚拟条码(X0000x:信誉订单 / 虚拟订单)明细行;「订单实付金额」取订单级实付(拆单取主单)再减去虚拟条码行实付。「优惠金额」「冲减金额」另取总账备注=优惠收入 / 冲减收入的账单金额,不计入收入金额;「退款金额」另取总账备注=退款的账单金额,只单独列示,不计入收入金额、也不参与差额;「种菜金额」是订单号在种菜明细表或聚水潭订单标签明细(是否种菜订单=是)任意一边出现过时,该订单的收入金额(两个条件是或的关系),只是收入金额里的一部分,不参与差额;差额=(收入金额+优惠金额+冲减金额)−订单实付金额,匹配状态与差额异常备注同样按订单实付金额判定,未匹配到订单实付金额的行按订单实付金额=0 计入差额,只反映真正对不上的部分。挂不到订单行的优惠/冲减会补成一行(收入/商品留空)。「差额异常备注」只说明差额为什么产生,按顺序自动判定:订单实付未匹配(未匹配到订单实付金额,具体原因看「未匹配原因」列);订单实付取数为0(种菜)(订单实付=0 且是种菜单)、订单实付取数为0(订单实付=0 但不是种菜单)、订单实付未减冲减(订单实付=收入+优惠 → 差额=冲减金额)、订单实付未减优惠(订单实付=收入+冲减 → 差额=优惠金额)、订单实付未减优惠和冲减(订单实付=收入 → 差额=优惠+冲减)、订单实付含已退款部分(仅抖音负差额:聚水潭退款单/取消子单金额≈差额,或退款合计+账单退款净额≈实付−收入,或整单全退(退款合计≈实付)对上才标注)、订单实付未减联合营销费(仅得物负差额:联合营销费未让利尾数对上才标注)、订单实付取供货价(仅京喜正差额:差额=该单直营服务费(供货模式净收)时标注)、差额=直营补贴款(待开票结算)(仅京喜账单·出类拔萃店负差额且无退款:京东直营补贴款待开票结算时标注)、差额=直营补贴款(订单退款已冲回)(同店负差额但订单已退款:补贴款已随退款冲回)、差额=订单实付包含商家承担优惠和以旧换新国补(仅京东账单·索菲亚客厅/办公/书房/功能、亿家达全屋、朴美家居店负差额、且该单含国补(聚水潭应付−账单收入>0):差额为商家承担优惠(店铺东券/单品折扣等)与以旧换新国补之和,已用营销对账逐单核实)、差额=订单实付包含商家承担优惠(同店负差额但不含国补的单:差额即商家承担优惠);都不满足则留空。「未匹配原因」单独一列,取值:账单无单号 / 账单无单号(采销收款)/ 单号格式异常 / 订单表无此单 / 1688采销收款(非销售单)/ 订单无实付金额 / 无收款账单(「采销收款」两档专指「支付宝-1688采销专用」账户:该账户收的是采销/供货结算款,不是店铺销售单收款,不按丢单处理)。', + 'description' => '覆盖店铺收款账户账单各平台页签(总账备注=订单收入),按「店铺 + 订单号」匹配订单侧商品金额与订单实付金额;一行 = 一个店铺的一个订单,商品金额只挂一次。「商品金额」按「主单(拆单子单不参与)→ 原单行(merge_so_id 为空)→ 全部行」优先级取数,合单发货(原单与合并行共用同一线上单号、没有主单)时只取本单原单行,避免把合并的另一单算进来;并排除虚拟条码(X0000x:信誉订单 / 虚拟订单)明细行;「订单实付金额」取订单级实付(同一套优先级,末档兜底取该单号下最大值)再减去虚拟条码行实付。「优惠金额」「冲减金额」另取总账备注=优惠收入 / 冲减收入的账单金额,不计入收入金额;「退款金额」另取总账备注=退款的账单金额,只单独列示,不计入收入金额、也不参与差额;「种菜金额」是订单号在种菜明细表或聚水潭订单标签明细(是否种菜订单=是)任意一边出现过时,该订单的收入金额(两个条件是或的关系),只是收入金额里的一部分,不参与差额;差额=(收入金额+优惠金额+冲减金额)−订单实付金额,匹配状态与差额异常备注同样按订单实付金额判定,未匹配到订单实付金额的行按订单实付金额=0 计入差额,只反映真正对不上的部分。挂不到订单行的优惠/冲减会补成一行(收入/商品留空)。「差额异常备注」只说明差额为什么产生,按顺序自动判定:订单实付未匹配(未匹配到订单实付金额,具体原因看「匹配原因」列);订单实付取数为0(种菜)(订单实付=0 且是种菜单)、订单实付取数为0(订单实付=0 但不是种菜单)、订单实付未减联合营销费(仅得物负差额:联合营销费未让利尾数对上才标注)、订单实付取供货价(仅京喜正差额:差额=该单直营服务费(供货模式净收)时标注)、差额=直营补贴款(待开票结算)(仅京喜账单·出类拔萃店负差额且无退款:京东直营补贴款待开票结算时标注)、差额=直营补贴款(订单退款已冲回)(同店负差额但订单已退款:补贴款已随退款冲回)、差额=订单实付包含商家承担优惠和以旧换新国补(仅京东账单·索菲亚客厅/办公/书房/功能、亿家达全屋、朴美家居店负差额、且该单含国补(聚水潭应付−账单收入>0):差额为商家承担优惠(店铺东券/单品折扣等)与以旧换新国补之和,已用营销对账逐单核实)、差额=订单实付包含商家承担优惠(同店负差额但不含国补的单:差额即商家承担优惠)、订单实付未减冲减(订单实付=收入+优惠 → 差额=冲减金额)、订单实付未减优惠(订单实付=收入+冲减 → 差额=优惠金额)、订单实付未减优惠和冲减(订单实付=收入 → 差额=优惠+冲减)(以上三档等式对带口径标识的行让位给口径兜底)、订单实付含已退款部分(仅抖音负差额:聚水潭退款单/取消子单金额≈差额,或退款合计+账单退款净额≈实付−收入,或整单全退(退款合计≈实付)对上才标注);都不满足时,若「卖家实收」或「应付/已付」能对上账单,差额按 0 计并在「匹配原因」列标注口径核对;其余标「差额待核实」。「匹配原因」单独一列,取值:账单无单号 / 账单无单号(采销收款)/ 单号格式异常 / 订单表无此单 / 1688采销收款(非销售单)/ 订单无实付金额 / 无收款账单(「采销收款」两档专指「支付宝-1688采销专用」账户:该账户收的是采销/供货结算款,不是店铺销售单收款,不按丢单处理)。', 'empty' => '店铺收款匹配订单明细尚未同步,请先执行 store-receipt:order-match:sync。', 'fields' => [ ['name' => 'platform', 'label' => '平台'],