From a08b28199d7243ed098a05ea7245f652b8ec962d Mon Sep 17 00:00:00 2001 From: qiuxiang <394405259@qq.com> Date: Mon, 28 Sep 2026 17:51:41 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=BA=97=E9=93=BA=E6=94=B6?= =?UTF-8?q?=E6=AC=BE=E5=94=AF=E5=93=81=E4=BC=9AJX=E8=B4=A6=E6=88=B7?= =?UTF-8?q?=E9=A1=B5=E7=AD=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 店铺收款账户账单新增 vipshop-jx 页签:导航配置、列定义、控制器与前端路由;读取共享账单表 ec_account_bills 中的唯品会JX(JITX)原始账单(店铺/类型/二次备注/销售订单号/含税金额),按金额正负拆出收入与支出,订单日期由销售订单号前 6 位还原(原始账单无日期列)。DB 页签日期统一按 Y-m-d H:i:s 返回,JX 页签汇总口径为收支净额,并补齐单元与功能测试。 --- config/business_navigation.php | 1 + .../EcommerceFinance/AccountBillColumns.php | 17 ++++ .../Http/Controllers/AccountBills.php | 30 +++++- .../js/components/business/AccountBills.vue | 4 +- resources/assets/js/views/business/pages.js | 6 +- .../Business/VipshopJxReceivingBillsTest.php | 94 +++++++++++++++++++ tests/Unit/View/AccountBillsTableTest.php | 16 +++- 7 files changed, 158 insertions(+), 10 deletions(-) create mode 100644 tests/Feature/Business/VipshopJxReceivingBillsTest.php diff --git a/config/business_navigation.php b/config/business_navigation.php index 2a20ea75..89d1d078 100644 --- a/config/business_navigation.php +++ b/config/business_navigation.php @@ -409,6 +409,7 @@ $navigation = [ ['slug' => 'douyin', 'title' => '抖音账户'], ['slug' => 'dewu', 'title' => '得物账户'], ['slug' => 'vipshop', 'title' => '唯品会MP账户'], + ['slug' => 'vipshop-jx', 'title' => '唯品会JX账户'], ], ], [ diff --git a/modules/EcommerceFinance/AccountBillColumns.php b/modules/EcommerceFinance/AccountBillColumns.php index 9bc039e9..f43f3129 100644 --- a/modules/EcommerceFinance/AccountBillColumns.php +++ b/modules/EcommerceFinance/AccountBillColumns.php @@ -58,6 +58,23 @@ class AccountBillColumns ['商家应收(G=E-F-G)', 'balance', 'money'], ], ], + + // 唯品会JX(JITX 入仓/经销)账单只有 5 列原始字段(店铺/类型/二次备注/销售订单号/含税金额), + // 没有日期列;订单日期由销售订单号前 6 位(YYMMDD)还原,其余列由金额正负拆出收/支。 + 'vipshop-jx' => [ + 'title' => '唯品会JX账户', + 'columns' => [ + ['记录编码', 'record_code', 'num'], + ['店铺', 'store_name', 'text'], + ['类型', 'bill_item', 'tag'], + ['二次备注', 'payload.second_remark', 'text'], + ['销售订单号', 'order_no', 'num'], + ['订单日期(按订单号)', 'occurred_at', 'date'], + ['含税金额', 'amount', 'money'], + ['收入金额', 'income', 'money_in'], + ['支出金额', 'expense', 'money_out'], + ], + ], 'taobao-wechat' => [ 'title' => '淘系微信账户', 'groups' => [ diff --git a/modules/EcommerceFinance/Http/Controllers/AccountBills.php b/modules/EcommerceFinance/Http/Controllers/AccountBills.php index 880563f5..e97ae04f 100644 --- a/modules/EcommerceFinance/Http/Controllers/AccountBills.php +++ b/modules/EcommerceFinance/Http/Controllers/AccountBills.php @@ -103,11 +103,19 @@ class AccountBills extends Controller ->value('balance'); $data = $rows->toArray(); + $data['data'] = collect($rows->items()) + ->map(fn (AccountBill $bill): array => $this->billRow($bill)) + ->all(); + $income = (float) ($summaryRow->income ?? 0); + $expense = (float) ($summaryRow->expense ?? 0); $data['summary'] = [ - 'income' => (float) ($summaryRow->income ?? 0), - 'expense' => (float) ($summaryRow->expense ?? 0), + 'income' => $income, + 'expense' => $expense, 'count' => (int) ($summaryRow->count ?? 0), - 'balance' => $latestBalance !== null ? (float) $latestBalance : 0, + // 唯品会JX账单没有账户余额列,该页签以收支净额作为汇总口径。 + 'balance' => $billType === 'vipshop-jx' + ? round($income - $expense, 2) + : ($latestBalance !== null ? (float) $latestBalance : 0), ]; $data['columns'] = AccountBillColumns::for($billType); @@ -144,7 +152,7 @@ class AccountBills extends Controller { $this->authorizeStoreReceivingBills(); - if ($bill_type === 'vipshop') { + if (in_array($bill_type, ['vipshop', 'vipshop-jx'], true)) { $query = AccountBill::query() ->where('company_id', company_id()) ->where('bill_type', $bill_type); @@ -185,6 +193,20 @@ class AccountBills extends Controller } } + /** 账单表格按 Y-m-d H:i:s 展示日期,避免 Eloquent 默认序列化成 ISO8601(UTC)。 */ + private function billRow(AccountBill $bill): array + { + return collect($bill->toArray()) + ->map(function (mixed $value, string $field) use ($bill): mixed { + $attribute = $bill->getAttribute($field); + + return $attribute instanceof \DateTimeInterface + ? $attribute->format('Y-m-d H:i:s') + : $value; + }) + ->all(); + } + private function exportQuery(Request $request, $query, string $billType) { $definition = AccountBillColumns::for($billType); diff --git a/resources/assets/js/components/business/AccountBills.vue b/resources/assets/js/components/business/AccountBills.vue index e2c23da4..43f4be91 100644 --- a/resources/assets/js/components/business/AccountBills.vue +++ b/resources/assets/js/components/business/AccountBills.vue @@ -84,7 +84,7 @@ export default { // Every account-bill tab uses the same explicit picker path. Keeping one // control avoids browser-native date inputs appearing on older tabs. usesStableDateControl(){return true;}, - usesMirrorOptions(){return ['douyin','pinduoduo','dewu','taobao-wechat','jd-transactions','jd-wallet','vipshop'].indexOf(this.billType)!==-1;}, + usesMirrorOptions(){return ['douyin','pinduoduo','dewu','taobao-wechat','jd-transactions','jd-wallet','vipshop','vipshop-jx'].indexOf(this.billType)!==-1;}, invalidDateRange(){return Boolean(this.filters.start&&this.filters.end&&this.filters.start>this.filters.end);}, startDateConfig(){return {...dateConfig,maxDate:this.filters.end||null};}, endDateConfig(){return {...dateConfig,minDate:this.filters.start||null};}, @@ -92,7 +92,7 @@ export default { canExport(){return !this.loading&&!this.loadError&&!this.invalidDateRange&&!this.filtersDirty&&this.total>0&&(this.exportMode==='filtered-all'||this.visibleColumns.length>0);}, visibleColumns(){return this.configuredColumns.filter(column=>column.visible);}, visibleGroups(){return this.visibleColumns.reduce((groups,column)=>{if(!column.group)return groups;const last=groups[groups.length-1];if(last&&last.label===column.group)last.span+=1;else groups.push({label:column.group,span:1});return groups;},[]);}, - stats(){return[{label:'收入合计',value:this.money(this.summary.income),note:`${this.summary.count} 笔交易`,className:'is-income'},{label:'支出合计',value:this.money(this.summary.expense),note:'按当前筛选',className:'is-expense'},{label:this.billType==='jd-transactions'?'收支净额':(this.billType==='jd-wallet'?'账户余额合计(最新)':'期末余额(最新)'),value:this.money(this.summary.balance),note:this.billType==='jd-transactions'?'按当前筛选':(this.billType==='jd-wallet'?'筛选结果中每个店铺账户最新一笔':'最新一条记录'),className:''},{label:'交易笔数',value:this.summary.count,note:'当前筛选结果',className:''}];}, + stats(){const netLabel=['jd-transactions','vipshop-jx'].indexOf(this.billType)!==-1;return[{label:'收入合计',value:this.money(this.summary.income),note:`${this.summary.count} 笔交易`,className:'is-income'},{label:'支出合计',value:this.money(this.summary.expense),note:'按当前筛选',className:'is-expense'},{label:netLabel?'收支净额':(this.billType==='jd-wallet'?'账户余额合计(最新)':'期末余额(最新)'),value:this.money(this.summary.balance),note:netLabel?'按当前筛选':(this.billType==='jd-wallet'?'筛选结果中每个店铺账户最新一笔':'最新一条记录'),className:''},{label:'交易笔数',value:this.summary.count,note:'当前筛选结果',className:''}];}, rangeStart(){return this.total===0?0:(this.currentPage-1)*this.perPage+1;},rangeEnd(){return Math.min(this.currentPage*this.perPage,this.total);} }, mounted(){if(this.usesMirrorOptions)this.fetchFilterOptions();this.fetch();}, diff --git a/resources/assets/js/views/business/pages.js b/resources/assets/js/views/business/pages.js index 7576d55c..fb9e24d8 100644 --- a/resources/assets/js/views/business/pages.js +++ b/resources/assets/js/views/business/pages.js @@ -5,7 +5,7 @@ * * 店铺收款账户账单(store-receiving-bills)下各 tab: * - taobao-alipay → 独立实现 TaobaoAlipayBills(经营分析副本共享镜像) - * - taobao-wechat / pinduoduo / jd-wallet / jd-transactions / dewu + * - taobao-wechat / pinduoduo / jd-wallet / jd-transactions / dewu / vipshop / vipshop-jx * → 通用实现 AccountBills(表 ec_account_bills,按 bill_type 区分) * - douyin → AccountBills 展示共享原始账单镜像中的头条放心购渠道 */ @@ -16,8 +16,8 @@ import Vue from 'vue'; import TaobaoAlipayBills from './../../components/business/TaobaoAlipayBills.vue'; import AccountBills from './../../components/business/AccountBills.vue'; -const GENERIC_TABS = ['taobao-wechat', 'pinduoduo', 'jd-wallet', 'jd-transactions', 'douyin', 'dewu', 'vipshop']; -const STORE_RECEIVING_TABS = ['taobao-alipay', 'taobao-wechat', 'pinduoduo', 'jd-wallet', 'jd-transactions', 'douyin', 'dewu', 'vipshop']; +const GENERIC_TABS = ['taobao-wechat', 'pinduoduo', 'jd-wallet', 'jd-transactions', 'douyin', 'dewu', 'vipshop', 'vipshop-jx']; +const STORE_RECEIVING_TABS = ['taobao-alipay', 'taobao-wechat', 'pinduoduo', 'jd-wallet', 'jd-transactions', 'douyin', 'dewu', 'vipshop', 'vipshop-jx']; function pageContext(pathname = window.location.pathname) { const parts = (pathname || '').split('/').filter(Boolean); diff --git a/tests/Feature/Business/VipshopJxReceivingBillsTest.php b/tests/Feature/Business/VipshopJxReceivingBillsTest.php new file mode 100644 index 00000000..010597f0 --- /dev/null +++ b/tests/Feature/Business/VipshopJxReceivingBillsTest.php @@ -0,0 +1,94 @@ +allowPage(); + $this->bill('vipshop-jx', '26060921475309', '销售', '2026-06-09 00:00:00', 82.48, 0); + $this->bill('vipshop-jx', '26070393670736', '客退', '2026-07-03 00:00:00', 0, 68.60); + $this->bill('vipshop', '26060100000001', '订单退款', '2026-06-01 00:00:00', 0, 999.00); + + $response = $this->loginAs() + ->getJson(route('ecommerce-finance.account-bills.index', [ + 'bill_type' => 'vipshop-jx', + 'limit' => 20, + ])) + ->assertOk() + ->assertJsonPath('columns.title', '唯品会JX账户') + ->assertJsonPath('total', 2) + ->assertJsonPath('summary.count', 2) + ->assertJsonPath('summary.income', 82.48) + ->assertJsonPath('summary.expense', 68.60) + // 该页签没有账户余额列,汇总口径为收支净额。 + ->assertJsonPath('summary.balance', 13.88) + // 日期按 Y-m-d H:i:s 返回,前端直接展示,不再出现 ISO8601(UTC)。 + ->assertJsonPath('data.0.occurred_at', '2026-07-03 00:00:00') + ->assertJsonPath('data.1.occurred_at', '2026-06-09 00:00:00'); + + $this->assertSame( + ['记录编码', '店铺', '类型', '二次备注', '销售订单号', '订单日期(按订单号)', '含税金额', '收入金额', '支出金额'], + array_column($response->json('columns.columns'), 'label'), + ); + } + + public function test_vipshop_jx_filter_options_and_export_stay_inside_the_tab(): void + { + $this->allowPage(); + $this->bill('vipshop-jx', '26060921475309', '销售', '2026-06-09 00:00:00', 82.48, 0); + $this->bill('vipshop-jx', '26070393670736', '客退', '2026-07-03 00:00:00', 0, 68.60); + $this->bill('vipshop', '26060100000001', '订单退款', '2026-06-01 00:00:00', 0, 999.00); + + $this->loginAs() + ->getJson(route('ecommerce-finance.account-bills.filter-options', ['bill_type' => 'vipshop-jx'])) + ->assertOk() + ->assertExactJson([ + 'shops' => ['唯品会JITX_测试店'], + 'bill_items' => ['客退', '销售'], + ]); + + $export = $this->loginAs()->get(route('ecommerce-finance.account-bills.export', [ + 'bill_type' => 'vipshop-jx', + 'bill_item' => '销售', + ])); + + $export->assertOk()->assertDownload('vipshop-jx_account_bills.csv'); + $csv = $export->streamedContent(); + $this->assertStringContainsString('26060921475309', $csv); + $this->assertStringNotContainsString('26070393670736', $csv); + $this->assertStringNotContainsString('999.00', $csv); + } + + private function bill(string $billType, string $orderNo, string $item, string $occurredAt, float $income, float $expense): void + { + AccountBill::create([ + 'company_id' => $this->company->id, + 'bill_type' => $billType, + 'record_code' => $orderNo . '-' . $item, + 'store_name' => $billType === 'vipshop-jx' ? '唯品会JITX_测试店' : '唯品会MP-测试店', + 'order_no' => $orderNo, + 'amount' => $income - $expense, + 'bill_item' => $item, + 'occurred_at' => $occurredAt, + 'income' => $income, + 'expense' => $expense, + 'payload' => ['second_remark' => '订单'], + ]); + } + + private function allowPage(): void + { + $this->user->attachPermission( + Permission::where('name', 'read-admin-panel')->firstOrFail() + ); + } +} diff --git a/tests/Unit/View/AccountBillsTableTest.php b/tests/Unit/View/AccountBillsTableTest.php index a1a0b771..66d948de 100644 --- a/tests/Unit/View/AccountBillsTableTest.php +++ b/tests/Unit/View/AccountBillsTableTest.php @@ -14,7 +14,7 @@ class AccountBillsTableTest extends TestCase $this->assertStringContainsString("usesStableDateControl(){return true;}", $source); $this->assertStringContainsString("{ 'is-stable-date': usesStableDateControl }", $source); $this->assertStringContainsString('.tw-date.is-stable-date .tw-date-control', $source); - $this->assertStringContainsString("['douyin','pinduoduo','dewu','jd-transactions','jd-wallet','vipshop'].indexOf(this.billType)!==-1", $source); + $this->assertStringContainsString("['douyin','pinduoduo','dewu','taobao-wechat','jd-transactions','jd-wallet','vipshop','vipshop-jx'].indexOf(this.billType)!==-1", $source); $this->assertStringContainsString('v-if="usesMirrorOptions && !filterOptionsError"', $source); $this->assertStringContainsString('page-key="store-receiving-bills"', $source); $this->assertStringContainsString(':tab-key="billType"', $source); @@ -24,4 +24,18 @@ class AccountBillsTableTest extends TestCase $this->assertStringContainsString('position:sticky', $source); $this->assertStringContainsString('overflow:auto', $source); } + + public function test_store_receiving_tabs_route_the_vipshop_jx_account(): void + { + $source = file_get_contents(__DIR__ . '/../../../resources/assets/js/views/business/pages.js'); + + $this->assertStringContainsString( + "const GENERIC_TABS = ['taobao-wechat', 'pinduoduo', 'jd-wallet', 'jd-transactions', 'douyin', 'dewu', 'vipshop', 'vipshop-jx'];", + $source, + ); + $this->assertStringContainsString( + "const STORE_RECEIVING_TABS = ['taobao-alipay', 'taobao-wechat', 'pinduoduo', 'jd-wallet', 'jd-transactions', 'douyin', 'dewu', 'vipshop', 'vipshop-jx'];", + $source, + ); + } }