接入淘系支付宝账单副本库全量数据 #208
@ -35,7 +35,7 @@ class TaobaoAlipayBillSpreadsheetImporter
|
||||
'counterparty_account', 'account_order_no', 'business_serial_no', 'goods_name',
|
||||
];
|
||||
|
||||
public function import(string $path, int $companyId): array
|
||||
public function import(string $path, int $companyId, bool $replace = false): array
|
||||
{
|
||||
if (! is_file($path) || ! is_readable($path)) {
|
||||
throw new InvalidArgumentException('导入文件不存在或不可读取。');
|
||||
@ -55,21 +55,27 @@ class TaobaoAlipayBillSpreadsheetImporter
|
||||
throw new InvalidArgumentException('表头与淘系支付宝账户模板不一致。');
|
||||
}
|
||||
|
||||
$rows = [];
|
||||
$skipped = 0;
|
||||
for ($rowNumber = 2; $rowNumber <= $sheet->getHighestDataRow(); $rowNumber++) {
|
||||
$values = $sheet->rangeToArray("A{$rowNumber}:Y{$rowNumber}", null, false, false)[0];
|
||||
if (collect($values)->every(fn ($value) => $value === null || trim((string) $value) === '')) {
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
$rows[] = $this->normalizeRow(array_combine(self::FIELDS, $values), $rowNumber);
|
||||
}
|
||||
|
||||
$inserted = 0;
|
||||
$updated = 0;
|
||||
$skipped = 0;
|
||||
$replaced = 0;
|
||||
|
||||
DB::transaction(function () use ($sheet, $companyId, &$inserted, &$updated, &$skipped) {
|
||||
for ($rowNumber = 2; $rowNumber <= $sheet->getHighestDataRow(); $rowNumber++) {
|
||||
$values = $sheet->rangeToArray("A{$rowNumber}:Y{$rowNumber}", null, false, false)[0];
|
||||
DB::transaction(function () use ($rows, $companyId, $replace, &$inserted, &$updated, &$replaced) {
|
||||
if ($replace) {
|
||||
$replaced = DB::table('ec_alipay_bills')->where('company_id', $companyId)->delete();
|
||||
}
|
||||
|
||||
if (collect($values)->every(fn ($value) => $value === null || trim((string) $value) === '')) {
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$data = array_combine(self::FIELDS, $values);
|
||||
$data = $this->normalizeRow($data, $rowNumber);
|
||||
foreach ($rows as $data) {
|
||||
$record = AlipayBill::query()
|
||||
->where('company_id', $companyId)
|
||||
->where('record_code', $data['record_code'])
|
||||
@ -93,7 +99,7 @@ class TaobaoAlipayBillSpreadsheetImporter
|
||||
|
||||
$spreadsheet->disconnectWorksheets();
|
||||
|
||||
return compact('inserted', 'updated', 'skipped');
|
||||
return compact('inserted', 'updated', 'skipped', 'replaced');
|
||||
}
|
||||
|
||||
private function normalizeRow(array $data, int $rowNumber): array
|
||||
|
||||
@ -10,7 +10,8 @@ class ImportTaobaoAlipayBills extends Command
|
||||
{
|
||||
protected $signature = 'business:import-taobao-alipay-bills
|
||||
{file : 淘系支付宝账户 Excel 文件路径}
|
||||
{--company=1 : 数据所属公司 ID}';
|
||||
{--company=1 : 数据所属公司 ID}
|
||||
{--replace : 校验通过后替换该公司的全部淘系支付宝账单}';
|
||||
|
||||
protected $description = '从标准附件导入店铺收款账户账单的淘系支付宝账户数据';
|
||||
|
||||
@ -20,6 +21,7 @@ class ImportTaobaoAlipayBills extends Command
|
||||
$result = $importer->import(
|
||||
(string) $this->argument('file'),
|
||||
max(1, (int) $this->option('company')),
|
||||
(bool) $this->option('replace'),
|
||||
);
|
||||
} catch (Throwable $e) {
|
||||
$this->components->error($e->getMessage());
|
||||
@ -28,7 +30,7 @@ class ImportTaobaoAlipayBills extends Command
|
||||
}
|
||||
|
||||
$this->components->info(
|
||||
"导入完成:新增 {$result['inserted']} 条,更新 {$result['updated']} 条,跳过 {$result['skipped']} 条。"
|
||||
"导入完成:新增 {$result['inserted']} 条,更新 {$result['updated']} 条,替换旧数据 {$result['replaced']} 条,跳过 {$result['skipped']} 条。"
|
||||
);
|
||||
|
||||
return self::SUCCESS;
|
||||
|
||||
@ -16,17 +16,17 @@ class AlipayBills extends Controller
|
||||
'store_name' => '原始店铺',
|
||||
'custom_remark' => '自定义备注',
|
||||
'bill_item' => '账单项目',
|
||||
'alipay_account' => '平台渠道',
|
||||
'alipay_account' => '支付宝账号',
|
||||
'account_order_no' => '账务订单号',
|
||||
'business_serial_no' => '业务流水号',
|
||||
'merchant_order_no' => '商户订单号',
|
||||
'business_base_order_no' => '业务基础订单号',
|
||||
'goods_name' => '商品编码',
|
||||
'goods_name' => '商品名称',
|
||||
'counterparty_account' => '对方账号',
|
||||
'occurred_at' => '发生时间',
|
||||
'income' => '收入',
|
||||
'expense' => '支出',
|
||||
'balance' => '收支净额',
|
||||
'balance' => '余额',
|
||||
'business_type' => '业务类型',
|
||||
'remark' => '备注',
|
||||
'online_order_no' => '线上单号',
|
||||
@ -52,7 +52,7 @@ class AlipayBills extends Controller
|
||||
$dir = $request->get('dir') === 'asc' ? 'asc' : 'desc';
|
||||
$filters = $request->validate([
|
||||
'store_name' => ['nullable', 'string', 'max:255'],
|
||||
'alipay_account' => ['nullable', 'string', 'max:128'],
|
||||
'source_channel' => ['nullable', 'string', 'max:128'],
|
||||
'bill_item' => ['nullable', 'string', 'max:255'],
|
||||
'business_type' => ['nullable', 'string', 'max:255'],
|
||||
'start' => ['nullable', 'date_format:Y-m-d'],
|
||||
@ -90,7 +90,7 @@ class AlipayBills extends Controller
|
||||
$this->authorizeStoreReceivingBills();
|
||||
$filters = $request->validate([
|
||||
'store_name' => ['nullable', 'string', 'max:255'],
|
||||
'alipay_account' => ['nullable', 'string', 'max:128'],
|
||||
'source_channel' => ['nullable', 'string', 'max:128'],
|
||||
'bill_item' => ['nullable', 'string', 'max:255'],
|
||||
'business_type' => ['nullable', 'string', 'max:255'],
|
||||
'start' => ['nullable', 'date_format:Y-m-d'],
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
namespace Modules\EcommerceFinance\Repositories;
|
||||
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Modules\EcommerceFinance\Services\BusinessMirrorDatasetRegistry;
|
||||
use RuntimeException;
|
||||
@ -17,6 +18,7 @@ class StoreReceivingAlipayBillMirrorRepository
|
||||
|
||||
public const DATASET = 'operations_replica.rpt.jst_originalBillInfo';
|
||||
public const SHOP_DATASET = 'operations_replica.rpt.jst_shopInfo';
|
||||
public const SKU_DATASET = 'operations_replica.rpt.jst_sku';
|
||||
|
||||
public function __construct(
|
||||
private readonly BusinessMirrorDatasetRegistry $datasets,
|
||||
@ -58,7 +60,7 @@ class StoreReceivingAlipayBillMirrorRepository
|
||||
$perPage = min(max($perPage, 1), 100);
|
||||
$direction = $direction === 'asc' ? 'asc' : 'desc';
|
||||
$sortColumn = [
|
||||
'record_code' => in_array($mapping, ['pinduoduo', 'dewu'], true) ? 'b.auto_id' : 'b.Id',
|
||||
'record_code' => in_array($mapping, ['pinduoduo', 'dewu'], true) ? 'b.auto_id' : ($mapping === 'taobao' ? 'b.p_auto_id' : 'b.Id'),
|
||||
'occurred_at' => 'b.fee_time',
|
||||
'income' => 'b.amount',
|
||||
'expense' => 'b.amount',
|
||||
@ -88,8 +90,10 @@ class StoreReceivingAlipayBillMirrorRepository
|
||||
$query, $sortColumn, $direction, $page, $perPage, $filters, $channel, $targetTable,
|
||||
);
|
||||
$shopNames = $this->shopNames($billRows);
|
||||
$productNames = $mapping === 'taobao' ? $this->productNames($billRows) : [];
|
||||
$importedRows = $mapping === 'taobao' ? $this->importedRows($billRows) : [];
|
||||
$rows = collect($billRows)
|
||||
->map(fn (object $row): array => $this->mapRowFor($row, $shopNames[(string) $row->shop_id] ?? null, $mapping))
|
||||
->map(fn (object $row): array => $this->mapRowFor($row, $shopNames[(string) $row->shop_id] ?? null, $mapping, $productNames, $importedRows))
|
||||
->all();
|
||||
$income = $summary['income'];
|
||||
$expense = $summary['expense'];
|
||||
@ -146,7 +150,7 @@ class StoreReceivingAlipayBillMirrorRepository
|
||||
$shopNames = $this->allShopNames();
|
||||
$direction = $direction === 'desc' ? 'desc' : 'asc';
|
||||
$sortColumn = [
|
||||
'record_code' => in_array($mapping, ['pinduoduo', 'dewu'], true) ? 'b.auto_id' : 'b.Id',
|
||||
'record_code' => in_array($mapping, ['pinduoduo', 'dewu'], true) ? 'b.auto_id' : ($mapping === 'taobao' ? 'b.p_auto_id' : 'b.Id'),
|
||||
'occurred_at' => 'b.fee_time',
|
||||
'income' => 'b.amount',
|
||||
'expense' => 'b.amount',
|
||||
@ -238,8 +242,10 @@ class StoreReceivingAlipayBillMirrorRepository
|
||||
}
|
||||
$rows = $batch->select('b.*')->orderBy($sortColumn, $direction)
|
||||
->orderBy('b.Id', $direction)->limit(1000)->get();
|
||||
$productNames = $mapping === 'taobao' ? $this->productNames($rows->all()) : [];
|
||||
$importedRows = $mapping === 'taobao' ? $this->importedRows($rows->all()) : [];
|
||||
foreach ($rows as $row) {
|
||||
yield $this->mapRowFor($row, $shopNames[(string) $row->shop_id] ?? null, $mapping);
|
||||
yield $this->mapRowFor($row, $shopNames[(string) $row->shop_id] ?? null, $mapping, $productNames, $importedRows);
|
||||
}
|
||||
$last = $rows->last();
|
||||
} while ($rows->count() === 1000);
|
||||
@ -321,7 +327,7 @@ class StoreReceivingAlipayBillMirrorRepository
|
||||
|
||||
private function dailySummarySupports(array $filters): bool
|
||||
{
|
||||
return ($filters['alipay_account'] ?? '') === ''
|
||||
return ($filters['source_channel'] ?? '') === ''
|
||||
&& ($filters['bill_item'] ?? '') === ''
|
||||
&& ($filters['keyword'] ?? '') === '';
|
||||
}
|
||||
@ -377,8 +383,8 @@ class StoreReceivingAlipayBillMirrorRepository
|
||||
$query->whereIn('b.shop_id', $shopIds);
|
||||
}
|
||||
}
|
||||
if (($filters['alipay_account'] ?? '') !== '') {
|
||||
$query->where('b.channel', 'like', '%' . $this->escapeLike((string) $filters['alipay_account']) . '%');
|
||||
if (($filters['source_channel'] ?? '') !== '') {
|
||||
$query->where('b.channel', 'like', '%' . $this->escapeLike((string) $filters['source_channel']) . '%');
|
||||
}
|
||||
if (($filters['bill_item'] ?? '') !== '') {
|
||||
$query->where('b.fee_name', 'like', '%' . $this->escapeLike((string) $filters['bill_item']) . '%');
|
||||
@ -488,6 +494,47 @@ class StoreReceivingAlipayBillMirrorRepository
|
||||
->all();
|
||||
}
|
||||
|
||||
/** @param array<int,object> $rows @return array<string,string> */
|
||||
private function productNames(array $rows): array
|
||||
{
|
||||
$skuIds = collect($rows)->map(fn (object $row): ?string => $row->sku_id ?: $row->shop_sku_id)
|
||||
->filter()->unique()->values()->all();
|
||||
if ($skuIds === []) return [];
|
||||
|
||||
try {
|
||||
$dataset = $this->datasets->get(self::SKU_DATASET);
|
||||
} catch (\Throwable) {
|
||||
return [];
|
||||
}
|
||||
if (! $this->mirror->ready($dataset) || ! Schema::connection($this->mirror->connection()->getName())->hasTable('ops_jst_sku')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $this->mirror->connection()->table('ops_jst_sku')
|
||||
->whereIn('sku_id', $skuIds)->pluck('name', 'sku_id')
|
||||
->mapWithKeys(static fn (mixed $name, mixed $id): array => [(string) $id => (string) $name])
|
||||
->all();
|
||||
}
|
||||
|
||||
/** @param array<int,object> $rows @return array<string,object> */
|
||||
private function importedRows(array $rows): array
|
||||
{
|
||||
if (! Schema::hasTable('ec_alipay_bills')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$recordCodes = collect($rows)->pluck('p_auto_id')->filter()->unique()->values()->all();
|
||||
if ($recordCodes === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return DB::table('ec_alipay_bills')
|
||||
->where('company_id', (int) (company_id() ?: 1))
|
||||
->whereNull('deleted_at')
|
||||
->whereIn('record_code', $recordCodes)
|
||||
->get()->keyBy(fn (object $row): string => (string) $row->record_code)->all();
|
||||
}
|
||||
|
||||
/** @param array<int,object> $rows */
|
||||
public function mapDouyinRows(array $rows): array
|
||||
{
|
||||
@ -495,44 +542,63 @@ class StoreReceivingAlipayBillMirrorRepository
|
||||
return array_map(fn (object $row): array => $this->mapRowFor($row, $shops[(string) $row->shop_id] ?? null, 'douyin'), $rows);
|
||||
}
|
||||
|
||||
private function mapRow(object $row, ?string $shopName): array
|
||||
private function mapRow(object $row, ?string $shopName, array $productNames = [], array $importedRows = []): 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;
|
||||
|
||||
return [
|
||||
$mapped = [
|
||||
'id' => (string) $row->Id,
|
||||
'record_code' => (string) $row->Id,
|
||||
'record_code' => (string) ($row->p_auto_id ?: $row->Id),
|
||||
'store_name' => $shopName,
|
||||
'custom_remark' => null,
|
||||
'bill_item' => $row->fee_name,
|
||||
'alipay_account' => $row->channel,
|
||||
'account_order_no' => $row->primary_key,
|
||||
'business_serial_no' => $row->auto_id,
|
||||
'merchant_order_no' => $row->p_auto_id,
|
||||
'business_base_order_no' => $row->so_id,
|
||||
'goods_name' => $row->sku_id ?: $row->shop_sku_id,
|
||||
'alipay_account' => null,
|
||||
'account_order_no' => null,
|
||||
'business_serial_no' => null,
|
||||
'merchant_order_no' => null,
|
||||
'business_base_order_no' => null,
|
||||
'goods_name' => $productNames[(string) ($row->sku_id ?: $row->shop_sku_id)] ?? null,
|
||||
'counterparty_account' => null,
|
||||
'occurred_at' => $this->dateTime($row->fee_time),
|
||||
'income' => $income,
|
||||
'expense' => $expense,
|
||||
'balance' => round($income - $expense, 2),
|
||||
'business_type' => $this->directionName($row->inout_type),
|
||||
'balance' => null,
|
||||
'business_type' => null,
|
||||
'remark' => null,
|
||||
'online_order_no' => $row->so_id,
|
||||
'online_real_store' => $shopName,
|
||||
'business_description' => $row->combine_type,
|
||||
'business_bill_source' => $row->channel,
|
||||
'business_order_no' => $row->p_auto_id,
|
||||
'business_description' => null,
|
||||
'business_bill_source' => null,
|
||||
'business_order_no' => null,
|
||||
'created_at' => $this->dateTime($row->createTime),
|
||||
'updated_at' => $this->dateTime($row->updateTime),
|
||||
'modifier' => '',
|
||||
];
|
||||
|
||||
$imported = $importedRows[$mapped['record_code']] ?? null;
|
||||
if ($imported === null) {
|
||||
return $mapped;
|
||||
}
|
||||
|
||||
foreach ([
|
||||
'custom_remark', 'alipay_account', 'account_order_no', 'business_serial_no',
|
||||
'merchant_order_no', 'business_base_order_no', 'counterparty_account', 'balance',
|
||||
'business_type', 'remark', 'business_description', 'business_bill_source',
|
||||
'business_order_no', 'modifier',
|
||||
] as $field) {
|
||||
$mapped[$field] = $imported->{$field} ?? $mapped[$field];
|
||||
}
|
||||
if ($mapped['goods_name'] === null || $mapped['goods_name'] === '') {
|
||||
$mapped['goods_name'] = $imported->goods_name ?? null;
|
||||
}
|
||||
|
||||
return $mapped;
|
||||
}
|
||||
|
||||
/** @return array<string,mixed> */
|
||||
private function mapRowFor(object $row, ?string $shopName, string $mapping): array
|
||||
private function mapRowFor(object $row, ?string $shopName, string $mapping, array $productNames = [], array $importedRows = []): array
|
||||
{
|
||||
if ($mapping === 'pinduoduo') {
|
||||
return $this->mapPinduoduoRow($row, $shopName);
|
||||
@ -543,7 +609,7 @@ class StoreReceivingAlipayBillMirrorRepository
|
||||
}
|
||||
|
||||
if ($mapping !== 'douyin') {
|
||||
return $this->mapRow($row, $shopName);
|
||||
return $this->mapRow($row, $shopName, $productNames, $importedRows);
|
||||
}
|
||||
|
||||
$amount = abs((float) ($row->amount ?? 0));
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
|
||||
<form class="tw-filter" @submit.prevent="applyFilters">
|
||||
<label>原始店铺<input v-model.trim="filters.store_name" placeholder="如 天猫-亿家达旗舰店" /></label>
|
||||
<label>平台渠道<input v-model.trim="filters.alipay_account" placeholder="如 CommonBillTGCPay" /></label>
|
||||
<label>数据源渠道<input v-model.trim="filters.source_channel" placeholder="如 alipayCSV" /></label>
|
||||
<label>账单项目<input v-model.trim="filters.bill_item" placeholder="如 交易佣金" /></label>
|
||||
<label>收支类型<select v-model="filters.business_type"><option value="">全部类型</option><option>收入</option><option>支出</option></select></label>
|
||||
<label class="tw-date">发生时间<span><input v-model="filters.start" type="date" aria-label="开始日期" /><b>至</b><input v-model="filters.end" type="date" aria-label="结束日期" /></span></label>
|
||||
@ -71,8 +71,8 @@ import TableColumnSettings from './TableColumnSettings.vue';
|
||||
|
||||
const COLUMNS = [
|
||||
['记录编码', 'record_code', 'num', '基础信息'], ['原始店铺', 'store_name', 'text', '基础信息'], ['自定义备注', 'custom_remark', 'text', '基础信息'], ['账单项目', 'bill_item', 'tag', '基础信息'],
|
||||
['平台渠道', 'alipay_account', 'num', '账户与交易'], ['账务订单号', 'account_order_no', 'num', '账户与交易'], ['业务流水号', 'business_serial_no', 'num', '账户与交易'], ['商户订单号', 'merchant_order_no', 'num', '账户与交易'], ['业务基础订单号', 'business_base_order_no', 'num', '账户与交易'], ['商品编码', 'goods_name', 'num', '账户与交易'], ['对方账号', 'counterparty_account', 'text', '账户与交易'], ['发生时间', 'occurred_at', 'date', '账户与交易'],
|
||||
['收入', 'income', 'money_in', '金额(元)'], ['支出', 'expense', 'money_out', '金额(元)'], ['收支净额', 'balance', 'money', '金额(元)'],
|
||||
['支付宝账号', 'alipay_account', 'num', '账户与交易'], ['账务订单号', 'account_order_no', 'num', '账户与交易'], ['业务流水号', 'business_serial_no', 'num', '账户与交易'], ['商户订单号', 'merchant_order_no', 'num', '账户与交易'], ['业务基础订单号', 'business_base_order_no', 'num', '账户与交易'], ['商品名称', 'goods_name', 'text', '账户与交易'], ['对方账号', 'counterparty_account', 'text', '账户与交易'], ['发生时间', 'occurred_at', 'date', '账户与交易'],
|
||||
['收入', 'income', 'money_in', '金额(元)'], ['支出', 'expense', 'money_out', '金额(元)'], ['余额', 'balance', 'money', '金额(元)'],
|
||||
['业务类型', 'business_type', 'tag', '业务关联'], ['备注', 'remark', 'text', '业务关联'], ['线上单号', 'online_order_no', 'num', '业务关联'], ['线上单号对应实际店铺', 'online_real_store', 'text', '业务关联'], ['业务描述', 'business_description', 'text', '业务关联'], ['业务账单来源', 'business_bill_source', 'text', '业务关联'],
|
||||
['业务订单号', 'business_order_no', 'num', '维护信息'], ['创建时间', 'created_at', 'date', '维护信息'], ['修改时间', 'updated_at', 'date', '维护信息'], ['修改人', 'modifier', 'text', '维护信息'],
|
||||
].map(([label, field, kind, group]) => ({ key: field, label, field, kind, group }));
|
||||
@ -81,17 +81,17 @@ const SORTABLE = ['occurred_at', 'income', 'expense', 'balance', 'record_code'];
|
||||
export default {
|
||||
name: 'TaobaoAlipayBills', components: { TableColumnSettings },
|
||||
props: { apiBase: { type: String, required: true }, exportBase: { type: String, required: true }, userKey: { type: [String, Number], required: true } },
|
||||
data() { return { loading: false, error: '', rows: [], configuredColumns: [], summary: { income: 0, expense: 0, balance: 0, count: 0 }, total: 0, currentPage: 1, lastPage: 1, perPage: 20, filters: { store_name: '', alipay_account: '', bill_item: '', business_type: '', start: '', end: '', keyword: '' }, sort: { key: 'occurred_at', dir: 'desc' }, exportMode: 'filtered-visible' }; },
|
||||
data() { return { loading: false, error: '', rows: [], configuredColumns: [], summary: { income: 0, expense: 0, balance: 0, count: 0 }, total: 0, currentPage: 1, lastPage: 1, perPage: 20, filters: { store_name: '', source_channel: '', bill_item: '', business_type: '', start: '', end: '', keyword: '' }, sort: { key: 'occurred_at', dir: 'desc' }, exportMode: 'filtered-visible' }; },
|
||||
computed: {
|
||||
defaultColumns() { return COLUMNS; }, visibleColumns() { return this.configuredColumns.filter((column) => column.visible); },
|
||||
visibleGroups() { return this.visibleColumns.reduce((groups, column) => { 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: '收支净额', value: this.money(this.summary.balance), note: '收入合计 - 支出合计', className: '' }, { label: '交易笔数', value: this.summary.count, note: '当前筛选结果', className: '' }]; },
|
||||
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: '收支差额', value: this.money(this.summary.balance), note: '收入合计 - 支出合计', 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() { this.fetch(); },
|
||||
methods: {
|
||||
async fetch() { this.loading = true; this.error = ''; try { const response = await window.axios.get(this.apiBase, { params: { ...this.filters, sort: this.sort.key, dir: this.sort.dir, limit: this.perPage, page: this.currentPage } }); const body = response.data || {}; this.rows = body.data || []; this.total = body.total || 0; this.currentPage = body.current_page || 1; this.lastPage = body.last_page || 1; this.perPage = body.per_page || this.perPage; this.summary = body.summary || { income: 0, expense: 0, balance: 0, count: 0 }; } catch (error) { this.rows = []; this.total = 0; this.summary = { income: 0, expense: 0, balance: 0, count: 0 }; this.error = (error.response && error.response.data && error.response.data.message) || '淘系支付宝账单暂时无法读取,请稍后重试。'; } finally { this.loading = false; } },
|
||||
applyFilters() { this.currentPage = 1; this.fetch(); }, resetFilters() { this.filters = { store_name: '', alipay_account: '', bill_item: '', business_type: '', start: '', end: '', keyword: '' }; this.applyFilters(); },
|
||||
applyFilters() { this.currentPage = 1; this.fetch(); }, resetFilters() { this.filters = { store_name: '', source_channel: '', bill_item: '', business_type: '', start: '', end: '', keyword: '' }; this.applyFilters(); },
|
||||
changePage(page) { this.currentPage = page; this.fetch(); this.$nextTick(() => { if (this.$refs.scroll) this.$refs.scroll.scrollTop = 0; }); },
|
||||
maybeSort(column) { if (!this.sortableField(column.field)) return; this.sort.dir = this.sort.key === column.field && this.sort.dir === 'desc' ? 'asc' : 'desc'; this.sort.key = column.field; this.applyFilters(); }, sortableField(field) { return SORTABLE.indexOf(field) !== -1; }, sortMark(field) { return this.sort.key === field ? (this.sort.dir === 'asc' ? '↑' : '↓') : '↕'; },
|
||||
rawValue(row, column) { const value = row[column.field]; return value === null || value === undefined ? '' : value; },
|
||||
|
||||
@ -64,6 +64,30 @@ class ImportTaobaoAlipayBillsTest extends TestCase
|
||||
->assertFailed();
|
||||
}
|
||||
|
||||
public function testReplaceValidatesFirstAndAtomicallyRemovesOldRows(): void
|
||||
{
|
||||
$old = AlipayBill::query()->create([
|
||||
'company_id' => 1, 'record_code' => 'OLD-ROW', 'store_name' => '旧店铺',
|
||||
'bill_item' => '旧账单', 'business_type' => '', 'alipay_account' => '',
|
||||
'counterparty_account' => '', 'account_order_no' => '', 'business_serial_no' => '',
|
||||
'goods_name' => '', 'income' => 0, 'expense' => 0, 'balance' => 0,
|
||||
'platform' => 'taobao', 'created_from' => 'old-data',
|
||||
]);
|
||||
|
||||
$this->artisan('business:import-taobao-alipay-bills', [
|
||||
'file' => $this->workbook('错误表头'), '--company' => 1, '--replace' => true,
|
||||
])->assertFailed();
|
||||
$this->assertDatabaseHas('ec_alipay_bills', ['id' => $old->id, 'record_code' => 'OLD-ROW']);
|
||||
|
||||
$this->artisan('business:import-taobao-alipay-bills', [
|
||||
'file' => $this->workbook(), '--company' => 1, '--replace' => true,
|
||||
])->expectsOutputToContain('替换旧数据 1 条')->assertSuccessful();
|
||||
|
||||
$this->assertSame(1, AlipayBill::query()->where('company_id', 1)->count());
|
||||
$this->assertDatabaseMissing('ec_alipay_bills', ['record_code' => 'OLD-ROW']);
|
||||
$this->assertDatabaseHas('ec_alipay_bills', ['record_code' => '2000000001141269665']);
|
||||
}
|
||||
|
||||
private function workbook(?string $firstHeader = null, string $sheetName = '淘系支付宝账户'): string
|
||||
{
|
||||
$spreadsheet = new Spreadsheet();
|
||||
|
||||
@ -90,10 +90,11 @@ class StoreReceivingAlipayBillMirrorRepositoryTest extends TestCase
|
||||
);
|
||||
|
||||
$this->assertSame(2, $result['total']);
|
||||
$this->assertSame('2', $result['data'][0]['record_code']);
|
||||
$this->assertSame('P-2', $result['data'][0]['record_code']);
|
||||
$this->assertSame('天猫-经营店', $result['data'][0]['store_name']);
|
||||
$this->assertSame('支出', $result['data'][0]['business_type']);
|
||||
$this->assertSame('alipayCSV', $result['data'][0]['alipay_account']);
|
||||
$this->assertNull($result['data'][0]['business_type']);
|
||||
$this->assertNull($result['data'][0]['alipay_account']);
|
||||
$this->assertNull($result['data'][0]['balance']);
|
||||
$this->assertSame(100.25, $result['summary']['income']);
|
||||
$this->assertSame(20.10, $result['summary']['expense']);
|
||||
$this->assertSame(80.15, $result['summary']['balance']);
|
||||
@ -107,6 +108,40 @@ class StoreReceivingAlipayBillMirrorRepositoryTest extends TestCase
|
||||
$this->repository->paginate([], 'occurred_at', 'desc', 1, 10);
|
||||
}
|
||||
|
||||
public function test_imported_attachment_rows_enrich_matching_mirror_rows_for_the_taobao_page(): void
|
||||
{
|
||||
$this->markReady();
|
||||
DB::connection('business_mirror')->table('ops_jst_shop_info')->insert([
|
||||
'Id' => 10, 'shop_id' => 7, 'shop_name' => '天猫-经营店', 'shop_site' => '淘宝天猫',
|
||||
]);
|
||||
DB::connection('business_mirror')->table('ops_jst_original_bill_info')->insert(
|
||||
$this->row(1, 7, '2026-08-01 09:00:00', '1', 100.25),
|
||||
);
|
||||
DB::table('ec_alipay_bills')->insert([
|
||||
'company_id' => 1, 'record_code' => 'P-1', 'store_name' => '天猫-附件店铺',
|
||||
'custom_remark' => '模板数据', 'bill_item' => '交易收款', 'alipay_account' => '20880001',
|
||||
'account_order_no' => 'ACCOUNT-1', 'business_serial_no' => 'SERIAL-1',
|
||||
'merchant_order_no' => 'MERCHANT-1', 'business_base_order_no' => 'BASE-1',
|
||||
'goods_name' => '附件商品', 'counterparty_account' => '买家',
|
||||
'occurred_at' => '2026-08-01 09:00:00', 'income' => 100.25, 'expense' => 0, 'balance' => 300.50,
|
||||
'business_type' => '交易付款', 'remark' => '备注', 'online_order_no' => 'ONLINE-1',
|
||||
'online_real_store' => '天猫-附件店铺', 'business_description' => '业务描述',
|
||||
'business_bill_source' => '支付宝', 'business_order_no' => 'BUSINESS-1',
|
||||
'modifier' => '导入人', 'platform' => 'taobao', 'created_from' => 'attachment-import',
|
||||
'created_at' => '2026-08-02 09:00:00', 'updated_at' => '2026-08-02 10:00:00',
|
||||
]);
|
||||
|
||||
$result = $this->repository->paginate([], 'occurred_at', 'desc', 1, 10);
|
||||
|
||||
$this->assertSame(1, $result['total']);
|
||||
$this->assertSame('P-1', $result['data'][0]['record_code']);
|
||||
$this->assertSame('天猫-经营店', $result['data'][0]['store_name']);
|
||||
$this->assertSame('20880001', $result['data'][0]['alipay_account']);
|
||||
$this->assertSame(300.50, $result['data'][0]['balance']);
|
||||
$this->assertSame(100.25, $result['summary']['income']);
|
||||
$this->assertSame('succeeded', $result['sync']['status']);
|
||||
}
|
||||
|
||||
public function test_export_uses_the_same_filters_and_mapping_as_the_page(): void
|
||||
{
|
||||
$this->markReady();
|
||||
@ -119,11 +154,11 @@ class StoreReceivingAlipayBillMirrorRepositoryTest extends TestCase
|
||||
]);
|
||||
|
||||
$rows = iterator_to_array($this->repository->exportRows([
|
||||
'business_type' => '支出', 'alipay_account' => 'alipayCSV', 'keyword' => 'SKU-2',
|
||||
'business_type' => '支出', 'source_channel' => 'alipayCSV', 'keyword' => 'SKU-2',
|
||||
]));
|
||||
|
||||
$this->assertCount(1, $rows);
|
||||
$this->assertSame('2', $rows[0]['record_code']);
|
||||
$this->assertSame('P-2', $rows[0]['record_code']);
|
||||
$this->assertSame(20.10, $rows[0]['expense']);
|
||||
}
|
||||
|
||||
@ -197,7 +232,7 @@ class StoreReceivingAlipayBillMirrorRepositoryTest extends TestCase
|
||||
}
|
||||
foreach (array_chunk($fixtures, 100) as $chunk) $table->insert($chunk);
|
||||
$table->insert($this->row(9999, 7, '2026-08-01 09:00:00', '1', 10, 'OtherChannel'));
|
||||
foreach (['occurred_at' => 'fee_time', 'income' => 'amount', 'record_code' => 'Id'] as $sort => $column) {
|
||||
foreach (['occurred_at' => 'fee_time', 'income' => 'amount', 'record_code' => 'p_auto_id'] as $sort => $column) {
|
||||
foreach (['asc', 'desc'] as $direction) {
|
||||
$expected = (clone $table)->where('channel', 'CommonBillTGCPay')
|
||||
->orderBy($column, $direction)->orderBy('Id', $direction)->pluck('Id')->map('strval')->all();
|
||||
|
||||
@ -14,13 +14,14 @@ class TaobaoAlipayBillsTableTest extends TestCase
|
||||
);
|
||||
|
||||
$this->assertIsString($source);
|
||||
$this->assertStringContainsString('<th colspan="4">基础信息</th>', $source);
|
||||
$this->assertStringContainsString('<td :colspan="25"', $source);
|
||||
$this->assertStringContainsString('v-if="error" class="tb-error"', $source);
|
||||
$this->assertStringContainsString('e.response.data.message', $source);
|
||||
$this->assertStringContainsString('<label>平台渠道</label>', $source);
|
||||
$this->assertStringContainsString('@click="exportRows">导出</button>', $source);
|
||||
$this->assertStringContainsString('收支净额', $source);
|
||||
$this->assertStringContainsString('v-for="(group, index) in visibleGroups"', $source);
|
||||
$this->assertStringContainsString(':colspan="visibleColumns.length"', $source);
|
||||
$this->assertStringContainsString('v-if="error" class="tw-alert"', $source);
|
||||
$this->assertStringContainsString('error.response.data.message', $source);
|
||||
$this->assertStringContainsString('支付宝账号', $source);
|
||||
$this->assertStringContainsString('@click="exportRows"', $source);
|
||||
$this->assertStringContainsString('商品名称', $source);
|
||||
$this->assertStringContainsString('余额', $source);
|
||||
$this->assertStringNotContainsString('<th>财务备注</th>', $source);
|
||||
$this->assertStringNotContainsString('<th>订单号</th>', $source);
|
||||
$this->assertStringNotContainsString('<th class="tb-num">金额</th>', $source);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user