接入得物收款账单镜像 #185
@ -97,6 +97,9 @@ return [
|
||||
'jiansun.dbo.jst_originalBillInfo.pinduoduo_receiving' => [
|
||||
'role' => 'serving_fact', 'target_store' => 'mariadb', 'consumer' => '店铺收款账单/拼多多账户',
|
||||
],
|
||||
'jiansun.dbo.jst_originalBillInfo.dewu_receiving' => [
|
||||
'role' => 'serving_fact', 'target_store' => 'mariadb', 'consumer' => '店铺收款账单/得物账户',
|
||||
],
|
||||
'jiansun.dbo.alipay_Bill_Detail' => ['role' => 'serving_fact', 'target_store' => 'mariadb', 'consumer' => '店铺收款账单/淘系支付宝账户'],
|
||||
'jiansun.dbo.tmall_spread_bills' => ['role' => 'serving_fact', 'target_store' => 'mariadb', 'consumer' => '淘宝推广账单'],
|
||||
'jiansun.dbo.pdd_spread_bills' => ['role' => 'serving_fact', 'target_store' => 'mariadb', 'consumer' => '拼多多推广账单'],
|
||||
|
||||
@ -436,6 +436,31 @@ return [
|
||||
['fee_name'],
|
||||
],
|
||||
],
|
||||
// 聚水潭得物货款账户:只复制主账单渠道,保证金、跨境和推广流水
|
||||
// 由其他账户域处理,不混入店铺收款账户页面。
|
||||
'jiansun.dbo.jst_originalBillInfo.dewu_receiving' => [
|
||||
'source_key' => 'jiansun',
|
||||
'schema' => 'dbo',
|
||||
'object' => 'jst_originalBillInfo',
|
||||
'target' => 'js_jst_dewu_receiving_bill_rows',
|
||||
'keys' => ['Id'],
|
||||
'watermark' => 'updateTime',
|
||||
'fixed_filters' => [
|
||||
'channel' => 'Dewu',
|
||||
],
|
||||
'columns' => [
|
||||
'Id', 'shop_id', 'channel', 'fee_time', 'del_flag', 'primary_key',
|
||||
'createTime', 'updateTime', 'auto_id', 'fee_name', 'inout_type',
|
||||
'oc_amount', 'p_auto_id', 'shop_i_id', 'shop_sku_id', 'sku_id',
|
||||
'so_id', 'combine_type', 'amount',
|
||||
],
|
||||
'indexes' => [
|
||||
['Id'],
|
||||
['fee_time', 'Id', 'inout_type', 'amount'],
|
||||
['shop_id', 'fee_time', 'Id', 'inout_type', 'amount', 'fee_name'],
|
||||
['fee_name'],
|
||||
],
|
||||
],
|
||||
'jiansun.dbo.alipay_Bill_Detail' => [
|
||||
'source_key' => 'jiansun',
|
||||
'schema' => 'dbo',
|
||||
|
||||
@ -25,6 +25,7 @@
|
||||
| 页面已切换 | 店铺收款账单的淘系支付宝账户读取 `operations_replica.rpt.jst_originalBillInfo`,并关联 `rpt.jst_shopInfo` 展示店铺;分页、筛选、汇总和导出统一读取共享业务镜像。 |
|
||||
| 抖音按月校准 | 抖音账户对已核对的完整月份读取 `douyin_authoritative_bills` 中胜算原始明细,`douyin_authoritative_periods` 原子选择有效版本;该月份不再叠加聚水潭拆分行。未校准月份继续读取原始账单镜像。导入命令为 `business-mirror:import-douyin-authoritative <CSV路径> <YYYY-MM> --count=<核对笔数> --income=<收入> --expense=<支出>`,只接受完整月份和精确核对值,保留旧版本、文件摘要及原镜像供回滚。生产执行前备份状态与受影响月份镜像,并使用 `/run/caiwu-upstream-sync.lock`。实际财务数据只放私有目录,不进入 Git。 |
|
||||
| 已接入待首次同步 | 店铺收款账单的拼多多账户读取 `jiansun.dbo.jst_originalBillInfo` 中固定 `channel = PinduoduoImport` 的独立切片镜像 `js_jst_pdd_receiving_bill_rows`;不使用慢一天的 `operations_replica` 副本或营销账户 `pdd_spread_bills`。 |
|
||||
| 已接入待首次同步 | 店铺收款账单的得物账户读取 `jiansun.dbo.jst_originalBillInfo` 中固定 `channel = Dewu` 的独立切片镜像 `js_jst_dewu_receiving_bill_rows`;`Dewubzj`、`DewuKJ`、`DewuTK` 保留给保证金、跨境或推广等其他账户域,不混入得物收款账户。 |
|
||||
| 已完成首次导入 | 提现管理的每日店铺余额读取聚水潭“平台原始账单[新] → 店铺余额表”接口 JSON/XLSX 及多平台“平台资金账户余额”导出,进入 `jushuitan_shop_balance_snapshots`;已导入 2026-09-11 的淘系、拼多多和抖音共 164 条余额。页面按店铺名称精确匹配并强制使用平台消歧,未匹配和多匹配不会显示为零。 |
|
||||
| 已接入待首次同步 | 店铺营销账户余额的京东账户读取 `js_data_middle_platform.dbo.jd_jzt_finance_records` 镜像 `jsm_jd_jzt_finance_records`;按 `id` 稳定断点、`updated_at` 重叠增量,页面不直连上游。 |
|
||||
| 本次接入 | 普通商品资料的工作平台采购价格接入 `jiansun.dbo.pm_priceAdjustment`(镜像 `js_pm_price_adjustment`):按 `skuCode` 关联 `jst_sku.sku_id`,取 `effectTime` 不晚于当前时间的最新 `purchasePrice`,并在页面和导出中标记“工作平台价格”;源表没有可靠更新时间,按 `Id` 稳定键追加,首次全量完成前只显示普通商品自身采购价。 |
|
||||
|
||||
@ -8,6 +8,7 @@ use Illuminate\Routing\Controller;
|
||||
use Modules\EcommerceFinance\AccountBillColumns;
|
||||
use Modules\EcommerceFinance\Models\AccountBill;
|
||||
use Modules\EcommerceFinance\Repositories\StoreReceivingDouyinBillMirrorRepository;
|
||||
use Modules\EcommerceFinance\Repositories\StoreReceivingDewuBillMirrorRepository;
|
||||
use Modules\EcommerceFinance\Repositories\StoreReceivingPinduoduoBillMirrorRepository;
|
||||
use RuntimeException;
|
||||
|
||||
@ -28,7 +29,7 @@ class AccountBills extends Controller
|
||||
abort(404, '未知的账户类型。');
|
||||
}
|
||||
|
||||
if (in_array($billType, ['douyin', 'pinduoduo'], true)) {
|
||||
if (in_array($billType, ['douyin', 'pinduoduo', 'dewu'], true)) {
|
||||
return $request->boolean('export')
|
||||
? $this->export($request, $billType)
|
||||
: $this->mirrorIndex($request, $billType);
|
||||
@ -117,7 +118,7 @@ class AccountBills extends Controller
|
||||
|
||||
abort_unless(AccountBillColumns::has($bill_type), 404, '未知的账户类型。');
|
||||
|
||||
if (in_array($bill_type, ['douyin', 'pinduoduo'], true)) {
|
||||
if (in_array($bill_type, ['douyin', 'pinduoduo', 'dewu'], true)) {
|
||||
return $this->exportMirror($request, $bill_type);
|
||||
}
|
||||
|
||||
@ -141,7 +142,7 @@ class AccountBills extends Controller
|
||||
{
|
||||
$this->authorizeStoreReceivingBills();
|
||||
|
||||
abort_unless(in_array($bill_type, ['douyin', 'pinduoduo'], true), 404, '该账户类型没有可用的筛选选项。');
|
||||
abort_unless(in_array($bill_type, ['douyin', 'pinduoduo', 'dewu'], true), 404, '该账户类型没有可用的筛选选项。');
|
||||
|
||||
$label = $this->mirrorLabel($bill_type);
|
||||
|
||||
@ -362,14 +363,20 @@ class AccountBills extends Controller
|
||||
|
||||
private function mirrorRepository(string $billType): object
|
||||
{
|
||||
return $billType === 'pinduoduo'
|
||||
? app(StoreReceivingPinduoduoBillMirrorRepository::class)
|
||||
: app(StoreReceivingDouyinBillMirrorRepository::class);
|
||||
return match ($billType) {
|
||||
'pinduoduo' => app(StoreReceivingPinduoduoBillMirrorRepository::class),
|
||||
'dewu' => app(StoreReceivingDewuBillMirrorRepository::class),
|
||||
default => app(StoreReceivingDouyinBillMirrorRepository::class),
|
||||
};
|
||||
}
|
||||
|
||||
private function mirrorLabel(string $billType): string
|
||||
{
|
||||
return $billType === 'pinduoduo' ? '拼多多' : '抖音';
|
||||
return match ($billType) {
|
||||
'pinduoduo' => '拼多多',
|
||||
'dewu' => '得物',
|
||||
default => '抖音',
|
||||
};
|
||||
}
|
||||
|
||||
private function sort(Request $request): string
|
||||
|
||||
@ -58,7 +58,7 @@ class StoreReceivingAlipayBillMirrorRepository
|
||||
$perPage = min(max($perPage, 1), 100);
|
||||
$direction = $direction === 'asc' ? 'asc' : 'desc';
|
||||
$sortColumn = [
|
||||
'record_code' => $mapping === 'pinduoduo' ? 'b.auto_id' : 'b.Id',
|
||||
'record_code' => in_array($mapping, ['pinduoduo', 'dewu'], true) ? 'b.auto_id' : 'b.Id',
|
||||
'occurred_at' => 'b.fee_time',
|
||||
'income' => 'b.amount',
|
||||
'expense' => 'b.amount',
|
||||
@ -103,7 +103,7 @@ class StoreReceivingAlipayBillMirrorRepository
|
||||
'summary' => [
|
||||
'income' => $income,
|
||||
'expense' => $expense,
|
||||
'balance' => $income - $expense,
|
||||
'balance' => round($income - $expense, 2),
|
||||
'count' => $total,
|
||||
],
|
||||
'sync' => [
|
||||
@ -146,7 +146,7 @@ class StoreReceivingAlipayBillMirrorRepository
|
||||
$shopNames = $this->allShopNames();
|
||||
$direction = $direction === 'desc' ? 'desc' : 'asc';
|
||||
$sortColumn = [
|
||||
'record_code' => $mapping === 'pinduoduo' ? 'b.auto_id' : 'b.Id',
|
||||
'record_code' => in_array($mapping, ['pinduoduo', 'dewu'], true) ? 'b.auto_id' : 'b.Id',
|
||||
'occurred_at' => 'b.fee_time',
|
||||
'income' => 'b.amount',
|
||||
'expense' => 'b.amount',
|
||||
@ -491,7 +491,7 @@ class StoreReceivingAlipayBillMirrorRepository
|
||||
'occurred_at' => $this->dateTime($row->fee_time),
|
||||
'income' => $income,
|
||||
'expense' => $expense,
|
||||
'balance' => $income - $expense,
|
||||
'balance' => round($income - $expense, 2),
|
||||
'business_type' => $this->directionName($row->inout_type),
|
||||
'remark' => null,
|
||||
'online_order_no' => $row->so_id,
|
||||
@ -512,6 +512,10 @@ class StoreReceivingAlipayBillMirrorRepository
|
||||
return $this->mapPinduoduoRow($row, $shopName);
|
||||
}
|
||||
|
||||
if ($mapping === 'dewu') {
|
||||
return $this->mapDewuRow($row, $shopName);
|
||||
}
|
||||
|
||||
if ($mapping !== 'douyin') {
|
||||
return $this->mapRow($row, $shopName);
|
||||
}
|
||||
@ -533,7 +537,7 @@ class StoreReceivingAlipayBillMirrorRepository
|
||||
'occurred_at' => $this->dateTime($row->fee_time),
|
||||
'income' => $income,
|
||||
'expense' => $expense,
|
||||
'balance' => $income - $expense,
|
||||
'balance' => round($income - $expense, 2),
|
||||
'remark' => null,
|
||||
'modifier' => '聚水潭',
|
||||
'created_at' => $this->dateTime($row->createTime),
|
||||
@ -576,7 +580,7 @@ class StoreReceivingAlipayBillMirrorRepository
|
||||
'occurred_at' => $this->dateTime($row->fee_time),
|
||||
'income' => $income,
|
||||
'expense' => $expense,
|
||||
'balance' => $income - $expense,
|
||||
'balance' => round($income - $expense, 2),
|
||||
'remark' => null,
|
||||
'updated_at' => $this->dateTime($row->updateTime),
|
||||
'modifier' => 'RPA',
|
||||
@ -589,6 +593,50 @@ class StoreReceivingAlipayBillMirrorRepository
|
||||
];
|
||||
}
|
||||
|
||||
/** @return array<string,mixed> */
|
||||
private function mapDewuRow(object $row, ?string $shopName): array
|
||||
{
|
||||
$amount = abs((float) ($row->amount ?? 0));
|
||||
$income = (string) ($row->inout_type ?? '') === '1' ? $amount : 0.0;
|
||||
$expense = (string) ($row->inout_type ?? '') === '-1' ? $amount : 0.0;
|
||||
$orderNo = $row->so_id ?: $row->p_auto_id;
|
||||
$goodsNo = $row->sku_id ?: $row->shop_sku_id ?: $row->shop_i_id;
|
||||
|
||||
return [
|
||||
'id' => (string) $row->Id,
|
||||
'record_code' => (string) ($row->auto_id ?: $row->Id),
|
||||
'store_name' => $shopName,
|
||||
'finance_remark' => null,
|
||||
'order_no' => $orderNo,
|
||||
'amount' => $amount,
|
||||
'custom_remark' => null,
|
||||
'bill_item' => $row->fee_name,
|
||||
'occurred_at' => $this->dateTime($row->fee_time),
|
||||
'income' => $income,
|
||||
'expense' => $expense,
|
||||
'balance' => round($income - $expense, 2),
|
||||
'remark' => null,
|
||||
'updated_at' => $this->dateTime($row->updateTime),
|
||||
'modifier' => '聚水潭',
|
||||
'payload' => [
|
||||
'online_order_no' => $orderNo,
|
||||
'store_2' => $shopName,
|
||||
'order_type' => null,
|
||||
'goods_name' => null,
|
||||
'goods_no' => $goodsNo,
|
||||
'quantity' => null,
|
||||
'channel' => $row->channel,
|
||||
'business_type' => $this->directionName($row->inout_type),
|
||||
'online_real_store' => $shopName,
|
||||
'order_created_at' => null,
|
||||
'ship_return_at' => null,
|
||||
'settle_status' => null,
|
||||
'pay_period' => null,
|
||||
'pulled_at' => $this->dateTime($row->createTime),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
private function directionCode(string $value): string
|
||||
{
|
||||
return match ($value) {
|
||||
|
||||
@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\EcommerceFinance\Repositories;
|
||||
|
||||
/** 店铺收款账单·得物货款账户只读共享业务镜像。 */
|
||||
class StoreReceivingDewuBillMirrorRepository
|
||||
{
|
||||
public const DATASET = 'jiansun.dbo.jst_originalBillInfo.dewu_receiving';
|
||||
public const SOURCE_CHANNEL = 'Dewu';
|
||||
|
||||
public function __construct(
|
||||
private readonly StoreReceivingAlipayBillMirrorRepository $source,
|
||||
) {
|
||||
}
|
||||
|
||||
/** @return array<string,mixed> */
|
||||
public function paginate(array $filters, string $sort, string $direction, int $page, int $perPage): array
|
||||
{
|
||||
return $this->source->paginateChannel(
|
||||
$filters,
|
||||
$sort,
|
||||
$direction,
|
||||
$page,
|
||||
$perPage,
|
||||
self::SOURCE_CHANNEL,
|
||||
'dewu',
|
||||
self::DATASET,
|
||||
);
|
||||
}
|
||||
|
||||
/** @return iterable<int,array<string,mixed>> */
|
||||
public function exportRows(array $filters, string $sort = 'occurred_at', string $direction = 'asc'): iterable
|
||||
{
|
||||
return $this->source->exportChannelRows(
|
||||
$filters,
|
||||
$sort,
|
||||
$direction,
|
||||
self::SOURCE_CHANNEL,
|
||||
'dewu',
|
||||
self::DATASET,
|
||||
);
|
||||
}
|
||||
|
||||
/** @return array{shops:array<int,string>,bill_items:array<int,string>} */
|
||||
public function filterOptions(): array
|
||||
{
|
||||
return $this->source->filterOptionsForChannel(self::SOURCE_CHANNEL, self::DATASET);
|
||||
}
|
||||
}
|
||||
@ -59,7 +59,7 @@ export default {
|
||||
data(){return{loading:false,loadError:false,rows:[],sourceColumns:[],configuredColumns:[],columnsTitle:'',sourceGroups:[],summary:{income:0,expense:0,balance:0,count:0},total:0,currentPage:1,lastPage:1,perPage:20,filters:{store_name:'',bill_item:'',start:'',end:'',keyword:''},filterOptions:{shops:[],bill_items:[]},filterOptionsLoading:false,filterOptionsError:false,requestSerial:0,sort:{key:'occurred_at',dir:'desc'},exportMode:'filtered-visible'};},
|
||||
computed:{
|
||||
isDouyin(){return this.billType==='douyin';},
|
||||
usesMirrorOptions(){return ['douyin','pinduoduo'].indexOf(this.billType)!==-1;},
|
||||
usesMirrorOptions(){return ['douyin','pinduoduo','dewu'].indexOf(this.billType)!==-1;},
|
||||
invalidDateRange(){return Boolean(this.filters.start&&this.filters.end&&this.filters.start>this.filters.end);},
|
||||
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;},[]);},
|
||||
|
||||
124
tests/Feature/Business/DewuReceivingBillsTest.php
Normal file
124
tests/Feature/Business/DewuReceivingBillsTest.php
Normal file
@ -0,0 +1,124 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Business;
|
||||
|
||||
use App\Models\Auth\Permission;
|
||||
use Mockery;
|
||||
use Modules\EcommerceFinance\Repositories\StoreReceivingDewuBillMirrorRepository;
|
||||
use RuntimeException;
|
||||
use Tests\Feature\FeatureTestCase;
|
||||
|
||||
class DewuReceivingBillsTest extends FeatureTestCase
|
||||
{
|
||||
public function test_endpoint_reads_dewu_receiving_mirror_with_all_filters(): void
|
||||
{
|
||||
$this->allowPage();
|
||||
$filters = [
|
||||
'store_name' => '得物-测试店',
|
||||
'bill_item' => '交易收入',
|
||||
'start' => '2026-08-01',
|
||||
'end' => '2026-08-31',
|
||||
'keyword' => '110211',
|
||||
];
|
||||
$repository = Mockery::mock(StoreReceivingDewuBillMirrorRepository::class);
|
||||
$repository->shouldReceive('paginate')->once()->with(
|
||||
$filters, 'occurred_at', 'desc', 2, 20,
|
||||
)->andReturn([
|
||||
'current_page' => 2,
|
||||
'last_page' => 3,
|
||||
'per_page' => 20,
|
||||
'total' => 41,
|
||||
'data' => [[
|
||||
'id' => '14654480',
|
||||
'record_code' => '2000000001248775179',
|
||||
'store_name' => '得物-测试店',
|
||||
'income' => 21.0,
|
||||
'expense' => 0.0,
|
||||
'payload' => [
|
||||
'online_order_no' => '110211311661075313',
|
||||
'channel' => 'Dewu',
|
||||
],
|
||||
]],
|
||||
'summary' => ['income' => 21.0, 'expense' => 0.0, 'balance' => 21.0, 'count' => 41],
|
||||
]);
|
||||
$this->app->instance(StoreReceivingDewuBillMirrorRepository::class, $repository);
|
||||
|
||||
$this->withExceptionHandling()
|
||||
->loginAs()
|
||||
->getJson(route('ecommerce-finance.account-bills.index', [
|
||||
'bill_type' => 'dewu',
|
||||
...$filters,
|
||||
'page' => 2,
|
||||
'limit' => 20,
|
||||
]))
|
||||
->assertOk()
|
||||
->assertJsonPath('data.0.payload.channel', 'Dewu')
|
||||
->assertJsonPath('columns.title', '得物账户')
|
||||
->assertJsonPath('summary.count', 41);
|
||||
}
|
||||
|
||||
public function test_filter_options_and_selected_column_export_use_the_same_mirror(): void
|
||||
{
|
||||
$this->allowPage();
|
||||
$repository = Mockery::mock(StoreReceivingDewuBillMirrorRepository::class);
|
||||
$repository->shouldReceive('filterOptions')->once()->andReturn([
|
||||
'shops' => ['得物-A店', '得物-B店'],
|
||||
'bill_items' => ['交易收入', '平台服务费'],
|
||||
]);
|
||||
$repository->shouldReceive('exportRows')->once()->with(
|
||||
['store_name' => '得物-A店'], 'record_code', 'asc',
|
||||
)->andReturn([[
|
||||
'record_code' => '2000000001248775179',
|
||||
'store_name' => '得物-A店',
|
||||
'payload' => ['channel' => 'Dewu'],
|
||||
]]);
|
||||
$this->app->instance(StoreReceivingDewuBillMirrorRepository::class, $repository);
|
||||
|
||||
$this->loginAs()
|
||||
->withHeader('X-Requested-With', 'XMLHttpRequest')
|
||||
->get(route('ecommerce-finance.account-bills.filter-options', ['bill_type' => 'dewu']))
|
||||
->assertOk()
|
||||
->assertExactJson([
|
||||
'shops' => ['得物-A店', '得物-B店'],
|
||||
'bill_items' => ['交易收入', '平台服务费'],
|
||||
]);
|
||||
|
||||
$response = $this->loginAs()->get(route('ecommerce-finance.account-bills.export', [
|
||||
'bill_type' => 'dewu',
|
||||
'store_name' => '得物-A店',
|
||||
'sort' => 'record_code',
|
||||
'dir' => 'asc',
|
||||
'columns' => 'payload.channel,record_code,store_name',
|
||||
]));
|
||||
|
||||
$response->assertOk()
|
||||
->assertHeader('content-type', 'text/csv; charset=UTF-8')
|
||||
->assertDownload('dewu_account_bills.csv');
|
||||
$csv = $response->streamedContent();
|
||||
$this->assertStringContainsString('渠道,记录编码,店铺', $csv);
|
||||
$this->assertStringContainsString('Dewu,"=""2000000001248775179""",得物-A店', $csv);
|
||||
}
|
||||
|
||||
public function test_unready_mirror_returns_a_clear_retryable_response(): void
|
||||
{
|
||||
$this->allowPage();
|
||||
$repository = Mockery::mock(StoreReceivingDewuBillMirrorRepository::class);
|
||||
$repository->shouldReceive('paginate')->once()->andThrow(
|
||||
new RuntimeException('store_receiving_alipay_bill_mirror_not_ready')
|
||||
);
|
||||
$this->app->instance(StoreReceivingDewuBillMirrorRepository::class, $repository);
|
||||
|
||||
$this->loginAs()
|
||||
->withHeader('X-Requested-With', 'XMLHttpRequest')
|
||||
->get(route('ecommerce-finance.account-bills.index', ['bill_type' => 'dewu']))
|
||||
->assertStatus(503)
|
||||
->assertJsonPath('message', '得物账户账单共享业务镜像正在首次同步,完成后将自动显示。');
|
||||
}
|
||||
|
||||
private function allowPage(): void
|
||||
{
|
||||
$this->user->attachPermission(
|
||||
Permission::where('name', 'read-admin-panel')->firstOrFail()
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -483,6 +483,12 @@ class BusinessMirrorSynchronizerTest extends TestCase
|
||||
$this->assertSame('jst_originalBillInfo', $pinduoduo->sourceObject());
|
||||
$this->assertSame('js_jst_pdd_receiving_bill_rows', $pinduoduo->targetTable());
|
||||
$this->assertSame(['channel' => 'PinduoduoImport'], $pinduoduo->fixedFilters());
|
||||
|
||||
$dewu = $datasets['jiansun.dbo.jst_originalBillInfo.dewu_receiving'];
|
||||
$this->assertSame('jiansun', $dewu->sourceKey());
|
||||
$this->assertSame('jst_originalBillInfo', $dewu->sourceObject());
|
||||
$this->assertSame('js_jst_dewu_receiving_bill_rows', $dewu->targetTable());
|
||||
$this->assertSame(['channel' => 'Dewu'], $dewu->fixedFilters());
|
||||
}
|
||||
|
||||
/** @return array<string,mixed> */
|
||||
|
||||
@ -0,0 +1,189 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\EcommerceFinance;
|
||||
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Modules\EcommerceFinance\Repositories\GenericBusinessMirrorRepository;
|
||||
use Modules\EcommerceFinance\Repositories\StoreReceivingAlipayBillMirrorRepository;
|
||||
use Modules\EcommerceFinance\Repositories\StoreReceivingDewuBillMirrorRepository;
|
||||
use Modules\EcommerceFinance\Services\BusinessMirrorDatasetRegistry;
|
||||
use Tests\TestCase;
|
||||
|
||||
class StoreReceivingDewuBillMirrorRepositoryTest extends TestCase
|
||||
{
|
||||
private StoreReceivingDewuBillMirrorRepository $repository;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
config([
|
||||
'database.connections.business_mirror' => [
|
||||
'driver' => 'sqlite', 'database' => ':memory:', 'prefix' => '',
|
||||
],
|
||||
'upstream_mirrors.connection' => 'business_mirror',
|
||||
'upstream_mirrors.datasets' => [
|
||||
StoreReceivingDewuBillMirrorRepository::DATASET => [
|
||||
'source_key' => 'jiansun', 'schema' => 'dbo',
|
||||
'object' => 'jst_originalBillInfo', 'target' => 'js_jst_dewu_receiving_bill_rows',
|
||||
'keys' => ['Id'],
|
||||
'fixed_filters' => ['channel' => StoreReceivingDewuBillMirrorRepository::SOURCE_CHANNEL],
|
||||
],
|
||||
StoreReceivingAlipayBillMirrorRepository::SHOP_DATASET => [
|
||||
'source_key' => 'operations_replica', 'schema' => 'rpt',
|
||||
'object' => 'jst_shopInfo', 'target' => 'ops_jst_shop_info', 'keys' => ['Id'],
|
||||
],
|
||||
],
|
||||
]);
|
||||
DB::purge('business_mirror');
|
||||
$schema = Schema::connection('business_mirror');
|
||||
$schema->create('business_mirror_table_states', function (Blueprint $table): void {
|
||||
$table->string('dataset_key')->primary();
|
||||
$table->dateTime('initial_completed_at')->nullable();
|
||||
$table->string('status')->nullable();
|
||||
$table->dateTime('last_succeeded_at')->nullable();
|
||||
$table->unsignedBigInteger('mirror_row_count')->nullable();
|
||||
$table->boolean('mirror_row_count_is_exact')->default(false);
|
||||
});
|
||||
$schema->create('js_jst_dewu_receiving_bill_rows', function (Blueprint $table): void {
|
||||
$table->integer('Id');
|
||||
$table->integer('shop_id')->nullable();
|
||||
foreach (['channel', 'primary_key', 'fee_name', 'inout_type', 'p_auto_id', 'shop_i_id', 'shop_sku_id', 'sku_id', 'so_id', 'combine_type'] as $column) {
|
||||
$table->string($column)->nullable();
|
||||
}
|
||||
$table->dateTime('fee_time')->nullable();
|
||||
$table->dateTime('createTime')->nullable();
|
||||
$table->dateTime('updateTime')->nullable();
|
||||
$table->bigInteger('auto_id')->nullable();
|
||||
$table->decimal('amount', 20, 6)->nullable();
|
||||
});
|
||||
$schema->create('ops_jst_shop_info', function (Blueprint $table): void {
|
||||
$table->integer('Id');
|
||||
$table->integer('shop_id')->nullable();
|
||||
$table->string('shop_name')->nullable();
|
||||
});
|
||||
|
||||
$source = new StoreReceivingAlipayBillMirrorRepository(
|
||||
new BusinessMirrorDatasetRegistry(), new GenericBusinessMirrorRepository(app('db')),
|
||||
);
|
||||
$this->repository = new StoreReceivingDewuBillMirrorRepository($source);
|
||||
$this->markReady();
|
||||
}
|
||||
|
||||
public function test_it_reads_only_the_dewu_dataset_and_maps_available_source_fields(): void
|
||||
{
|
||||
DB::connection('business_mirror')->table('ops_jst_shop_info')->insert([
|
||||
['Id' => 10, 'shop_id' => 7, 'shop_name' => '得物-测试店'],
|
||||
['Id' => 11, 'shop_id' => 8, 'shop_name' => '其他店'],
|
||||
]);
|
||||
DB::connection('business_mirror')->table('js_jst_dewu_receiving_bill_rows')->insert([
|
||||
$this->row(1, 7, '2026-09-13 18:48:12', '1', 21.00, '交易收入'),
|
||||
$this->row(2, 7, '2026-09-13 19:00:00', '-1', 18.07, '平台服务费'),
|
||||
]);
|
||||
|
||||
$result = $this->repository->paginate([], 'occurred_at', 'desc', 1, 10);
|
||||
|
||||
$this->assertSame(2, $result['total']);
|
||||
$this->assertSame('200000000000000002', $result['data'][0]['record_code']);
|
||||
$this->assertSame('得物-测试店', $result['data'][0]['store_name']);
|
||||
$this->assertSame('110211-ORDER-2', $result['data'][0]['order_no']);
|
||||
$this->assertSame('Dewu', $result['data'][0]['payload']['channel']);
|
||||
$this->assertSame('支出', $result['data'][0]['payload']['business_type']);
|
||||
$this->assertSame('SKU-2', $result['data'][0]['payload']['goods_no']);
|
||||
$this->assertSame('聚水潭', $result['data'][0]['modifier']);
|
||||
$this->assertSame(21.0, $result['summary']['income']);
|
||||
$this->assertSame(18.07, $result['summary']['expense']);
|
||||
$this->assertSame(2.93, $result['summary']['balance']);
|
||||
}
|
||||
|
||||
public function test_filters_options_export_and_exact_total_use_the_fixed_channel_slice(): void
|
||||
{
|
||||
DB::connection('business_mirror')->table('ops_jst_shop_info')->insert([
|
||||
['Id' => 10, 'shop_id' => 7, 'shop_name' => '得物-A店'],
|
||||
['Id' => 11, 'shop_id' => 8, 'shop_name' => '得物-B店'],
|
||||
]);
|
||||
DB::connection('business_mirror')->table('js_jst_dewu_receiving_bill_rows')->insert([
|
||||
$this->row(1, 7, '2026-08-12 09:00:00', '1', 22.80, '交易收入'),
|
||||
$this->row(2, 8, '2026-08-13 09:00:00', '-1', 0.14, '平台服务费'),
|
||||
]);
|
||||
$connection = DB::connection('business_mirror');
|
||||
$connection->table('business_mirror_table_states')
|
||||
->where('dataset_key', StoreReceivingDewuBillMirrorRepository::DATASET)
|
||||
->update(['mirror_row_count' => 2, 'mirror_row_count_is_exact' => true]);
|
||||
$connection->enableQueryLog();
|
||||
$connection->flushQueryLog();
|
||||
|
||||
$unfiltered = $this->repository->paginate([], 'record_code', 'asc', 1, 10);
|
||||
$filtered = $this->repository->paginate([
|
||||
'store_name' => 'A店',
|
||||
'bill_item' => '交易',
|
||||
'start' => '2026-08-12',
|
||||
'end' => '2026-08-12',
|
||||
'keyword' => '110211-ORDER-1',
|
||||
], 'record_code', 'asc', 1, 10);
|
||||
$options = $this->repository->filterOptions();
|
||||
$export = iterator_to_array($this->repository->exportRows(['bill_item' => '平台服务费']));
|
||||
|
||||
$this->assertSame(2, $unfiltered['total']);
|
||||
$this->assertSame(1, $filtered['total']);
|
||||
$this->assertSame('200000000000000001', $filtered['data'][0]['record_code']);
|
||||
$this->assertSame(['得物-A店', '得物-B店'], $options['shops']);
|
||||
$this->assertSame(['交易收入', '平台服务费'], $options['bill_items']);
|
||||
$this->assertCount(1, $export);
|
||||
$this->assertSame('200000000000000002', $export[0]['record_code']);
|
||||
|
||||
$targetQueries = array_filter(
|
||||
$connection->getQueryLog(),
|
||||
static fn (array $query): bool => str_contains($query['query'], 'js_jst_dewu_receiving_bill_rows'),
|
||||
);
|
||||
$this->assertNotEmpty($targetQueries);
|
||||
$this->assertCount(0, array_filter(
|
||||
$targetQueries,
|
||||
static fn (array $query): bool => str_contains($query['query'], '"b"."channel" = ?'),
|
||||
));
|
||||
$connection->disableQueryLog();
|
||||
}
|
||||
|
||||
private function markReady(): void
|
||||
{
|
||||
foreach ([StoreReceivingDewuBillMirrorRepository::DATASET, StoreReceivingAlipayBillMirrorRepository::SHOP_DATASET] as $dataset) {
|
||||
DB::connection('business_mirror')->table('business_mirror_table_states')->insert([
|
||||
'dataset_key' => $dataset,
|
||||
'initial_completed_at' => '2026-09-14 09:00:00',
|
||||
'status' => 'succeeded',
|
||||
'last_succeeded_at' => '2026-09-14 09:00:00',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private function row(
|
||||
int $id,
|
||||
int $shopId,
|
||||
string $time,
|
||||
string $direction,
|
||||
float $amount,
|
||||
string $feeName,
|
||||
): array {
|
||||
return [
|
||||
'Id' => $id,
|
||||
'shop_id' => $shopId,
|
||||
'channel' => 'Dewu',
|
||||
'primary_key' => 'KEY-' . $id,
|
||||
'fee_name' => $feeName,
|
||||
'inout_type' => $direction,
|
||||
'p_auto_id' => 'P-' . $id,
|
||||
'shop_i_id' => 'ITEM-' . $id,
|
||||
'shop_sku_id' => 'SHOP-SKU-' . $id,
|
||||
'sku_id' => 'SKU-' . $id,
|
||||
'so_id' => '110211-ORDER-' . $id,
|
||||
'combine_type' => 'normal',
|
||||
'fee_time' => $time,
|
||||
'createTime' => '2026-09-14 13:00:48',
|
||||
'updateTime' => '2026-09-14 13:02:32',
|
||||
'auto_id' => 200000000000000000 + $id,
|
||||
'amount' => $amount,
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -11,7 +11,7 @@ class AccountBillsTableTest extends TestCase
|
||||
$source = file_get_contents(__DIR__ . '/../../../resources/assets/js/components/business/AccountBills.vue');
|
||||
|
||||
$this->assertStringContainsString("isDouyin(){return this.billType==='douyin';}", $source);
|
||||
$this->assertStringContainsString("['douyin','pinduoduo'].indexOf(this.billType)!==-1", $source);
|
||||
$this->assertStringContainsString("['douyin','pinduoduo','dewu'].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);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user