新增店铺收款唯品会JX账户页签

店铺收款账户账单新增 vipshop-jx 页签:导航配置、列定义、控制器与前端路由;读取共享账单表 ec_account_bills 中的唯品会JX(JITX)原始账单(店铺/类型/二次备注/销售订单号/含税金额),按金额正负拆出收入与支出,订单日期由销售订单号前 6 位还原(原始账单无日期列)。DB 页签日期统一按 Y-m-d H:i:s 返回,JX 页签汇总口径为收支净额,并补齐单元与功能测试。
This commit is contained in:
qiuxiang 2026-09-28 17:51:41 +08:00
parent e73183f52e
commit a08b28199d
7 changed files with 158 additions and 10 deletions

View File

@ -409,6 +409,7 @@ $navigation = [
['slug' => 'douyin', 'title' => '抖音账户'],
['slug' => 'dewu', 'title' => '得物账户'],
['slug' => 'vipshop', 'title' => '唯品会MP账户'],
['slug' => 'vipshop-jx', 'title' => '唯品会JX账户'],
],
],
[

View File

@ -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' => [

View File

@ -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);

View File

@ -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();},

View File

@ -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);

View File

@ -0,0 +1,94 @@
<?php
namespace Tests\Feature\Business;
use App\Models\Auth\Permission;
use Modules\EcommerceFinance\Models\AccountBill;
use Tests\Feature\FeatureTestCase;
/**
* 店铺收款账户账单 › 唯品会JX账户(bill_type = vipshop-jx,读取 ec_account_bills)。
*/
class VipshopJxReceivingBillsTest extends FeatureTestCase
{
public function test_vipshop_jx_tab_reads_only_its_own_bill_type(): 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);
$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()
);
}
}

View File

@ -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,
);
}
}