feat: 整合淘系支付宝账单共享业务镜像 (#109)
This commit is contained in:
parent
a989734b33
commit
050ad0ab97
@ -93,6 +93,7 @@ return [
|
||||
'jiansun.dbo.jst_small_payment_order_map' => ['role' => 'operational', 'target_store' => 'mariadb', 'consumer' => '小额打款工单'],
|
||||
|
||||
// Account bills and supplier pages are bounded serving datasets today.
|
||||
'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' => '拼多多推广账单'],
|
||||
'jiansun.dbo.alibaba_bill' => ['role' => 'serving_fact', 'target_store' => 'mariadb', 'consumer' => '阿里巴巴账单'],
|
||||
|
||||
@ -301,6 +301,24 @@ return [
|
||||
],
|
||||
|
||||
// JianSun 旧业务库:营销账单、供应商应付、板材、封边条和库存。
|
||||
'jiansun.dbo.alipay_Bill_Detail' => [
|
||||
'source_key' => 'jiansun',
|
||||
'schema' => 'dbo',
|
||||
'object' => 'alipay_Bill_Detail',
|
||||
'target' => 'js_alipay_bill_detail',
|
||||
'keys' => ['Id'],
|
||||
'watermark' => null,
|
||||
'columns' => [
|
||||
'Id', 'accountNum', 'businessNum', 'merchantOrder', 'commodityName',
|
||||
'happenTime', 'oppAccount', 'inAmount', 'outAmount', 'accountAmount',
|
||||
'tradeChannel', 'businessType', 'remark', 'shopName', 'date',
|
||||
'busiBillSource', 'busiDescription', 'busiNum', 'busiBasicNum',
|
||||
'buyerRemark', 'storeNum', 'appId', 'account', 'bindingType',
|
||||
],
|
||||
'indexes' => [
|
||||
['happenTime', 'Id'],
|
||||
],
|
||||
],
|
||||
'jiansun.dbo.tmall_spread_bills' => ['source_key' => 'jiansun', 'schema' => 'dbo', 'object' => 'tmall_spread_bills', 'target' => 'js_tmall_spread_bills', 'keys' => ['id']],
|
||||
'jiansun.dbo.pdd_spread_bills' => ['source_key' => 'jiansun', 'schema' => 'dbo', 'object' => 'pdd_spread_bills', 'target' => 'js_pdd_spread_bills', 'keys' => ['id'], 'watermark' => 'createTime'],
|
||||
'jiansun.dbo.alibaba_bill' => ['source_key' => 'jiansun', 'schema' => 'dbo', 'object' => 'alibaba_bill', 'target' => 'js_alibaba_bill', 'keys' => ['id']],
|
||||
|
||||
@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
$schema = Schema::connection($this->connection());
|
||||
|
||||
if ($schema->hasTable('js_alipay_bill_detail')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$schema->create('js_alipay_bill_detail', function (Blueprint $table): void {
|
||||
$table->char('_mirror_key', 64)->primary();
|
||||
$table->integer('Id')->nullable();
|
||||
$table->string('accountNum', 128)->nullable();
|
||||
$table->string('businessNum', 128)->nullable();
|
||||
$table->string('merchantOrder', 255)->nullable();
|
||||
$table->text('commodityName')->nullable();
|
||||
$table->dateTime('happenTime', 6)->nullable();
|
||||
$table->string('oppAccount', 255)->nullable();
|
||||
$table->decimal('inAmount', 20, 6)->nullable();
|
||||
$table->decimal('outAmount', 20, 6)->nullable();
|
||||
$table->decimal('accountAmount', 20, 6)->nullable();
|
||||
$table->string('tradeChannel', 255)->nullable();
|
||||
$table->string('businessType', 255)->nullable();
|
||||
$table->text('remark')->nullable();
|
||||
$table->string('shopName', 255)->nullable();
|
||||
$table->date('date')->nullable();
|
||||
$table->string('busiBillSource', 255)->nullable();
|
||||
$table->text('busiDescription')->nullable();
|
||||
$table->string('busiNum', 255)->nullable();
|
||||
$table->string('busiBasicNum', 255)->nullable();
|
||||
$table->text('buyerRemark')->nullable();
|
||||
$table->string('storeNum', 255)->nullable();
|
||||
$table->string('appId', 128)->nullable();
|
||||
$table->string('account', 255)->nullable();
|
||||
$table->string('bindingType', 128)->nullable();
|
||||
$table->uuid('_mirror_generation')->nullable()->index();
|
||||
$table->dateTime('_mirror_synced_at', 6)->index();
|
||||
|
||||
$table->index(['happenTime', 'Id'], 'alipay_detail_happen_id_index');
|
||||
$table->index(['shopName', 'happenTime', 'Id'], 'alipay_detail_shop_happen_id_index');
|
||||
$table->index(['appId', 'happenTime', 'Id'], 'alipay_detail_app_happen_id_index');
|
||||
$table->index(['businessType', 'happenTime', 'Id'], 'alipay_detail_type_happen_id_index');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::connection($this->connection())->dropIfExists('js_alipay_bill_detail');
|
||||
}
|
||||
|
||||
private function connection(): string
|
||||
{
|
||||
return (string) config('upstream_mirrors.connection', 'business_mirror');
|
||||
}
|
||||
};
|
||||
@ -21,6 +21,7 @@
|
||||
| 已开始逐域切换 | 公司、店铺、商标页面在对应镜像完成首次全量后自动改读完整镜像表;首次全量前保留旧快照作为兼容回滚。其他业务域仍需逐项切换,不得宣称全项目已经完成。 |
|
||||
| 中央同步接入 | 店铺维度报表从 `jiansun.dbo.jst_originalBillInfo` 和 `dbo.fys_shopInfo_flow` 自动、可断点同步;不再依赖开发者电脑、SQLite 或人工 XLSX。 |
|
||||
| 已登记,待首次同步 | `operations_replica/rpt.jst_originalBillInfo` 登记为通用原始账单镜像 `ops_jst_original_bill_info`;保留当前 19 个业务字段,按 `Id` 断点全量、按 `updateTime` 重叠增量。部署迁移只创建空表,不自动启动约 1400 万行的首次同步。 |
|
||||
| 已登记,待首次同步 | 店铺收款账单的淘系支付宝账户已登记 `jiansun.dbo.alipay_Bill_Detail`,页面按共享镜像分页、筛选和汇总;首次全量完成前显示同步中状态。 |
|
||||
|
||||
## 经营分析副本库(operations_replica)
|
||||
|
||||
@ -51,6 +52,7 @@
|
||||
|
||||
| 业务范围 | 上游对象 |
|
||||
| --- | --- |
|
||||
| 店铺收款账单 | `dbo.alipay_Bill_Detail` |
|
||||
| 营销账户 | `dbo.tmall_spread_bills`、`dbo.pdd_spread_bills`、`dbo.alibaba_bill`、`dbo.jd_spreadBills`、`dbo.jd_costBills` |
|
||||
| 供应商应付 | `dbo.finance_purchaseReturn`、`dbo.finance_invoiceDeduction`、`dbo.fys_supplierPaymentSummary`、`dbo.finance_advanceLoans`、`dbo.pm_unOrderDetails`、`dbo.finance_sampleDeduction`、`dbo.fys_express_courier_settlement` |
|
||||
| 安装明细 | `dbo.fys_Ticketing_install_view`、`dbo.fys_Ticketing_install_item` |
|
||||
|
||||
@ -6,6 +6,8 @@ use App\Utilities\SidebarPermissions;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Controller;
|
||||
use Modules\EcommerceFinance\Models\AlipayBill;
|
||||
use Modules\EcommerceFinance\Repositories\StoreReceivingAlipayBillMirrorRepository;
|
||||
use RuntimeException;
|
||||
|
||||
class AlipayBills extends Controller
|
||||
{
|
||||
@ -13,97 +15,46 @@ class AlipayBills extends Controller
|
||||
* 淘系支付宝账户账单列表(店铺收款账户账单 › 淘系支付宝账户)。
|
||||
* 经由 admin 路由组(web 会话)暴露,前端带 CSRF 调用即可。
|
||||
*/
|
||||
public function index(Request $request)
|
||||
public function index(Request $request, StoreReceivingAlipayBillMirrorRepository $repository)
|
||||
{
|
||||
$this->authorizeStoreReceivingBills();
|
||||
|
||||
$query = AlipayBill::query()->where('company_id', company_id());
|
||||
|
||||
if ($store = $request->get('store_name')) {
|
||||
$query->where('store_name', $store);
|
||||
}
|
||||
|
||||
if ($alipay = $request->get('alipay_account')) {
|
||||
$query->where('alipay_account', 'like', '%' . $alipay . '%');
|
||||
}
|
||||
|
||||
if ($item = $request->get('bill_item')) {
|
||||
$query->where('bill_item', $item);
|
||||
}
|
||||
|
||||
if ($type = $request->get('business_type')) {
|
||||
$query->where('business_type', $type);
|
||||
}
|
||||
|
||||
if ($orderNo = $request->get('order_no')) {
|
||||
$query->where('order_no', 'like', '%' . $orderNo . '%');
|
||||
}
|
||||
|
||||
if ($financeRemark = $request->get('finance_remark')) {
|
||||
$query->where('finance_remark', 'like', '%' . $financeRemark . '%');
|
||||
}
|
||||
|
||||
if ($start = $request->get('start')) {
|
||||
$query->where('occurred_at', '>=', $start);
|
||||
}
|
||||
|
||||
if ($end = $request->get('end')) {
|
||||
$query->where('occurred_at', '<=', $end . ' 23:59:59');
|
||||
}
|
||||
|
||||
if ($keyword = $request->get('keyword')) {
|
||||
$query->where(function ($q) use ($keyword) {
|
||||
$like = '%' . $keyword . '%';
|
||||
$q->where('goods_name', 'like', $like)
|
||||
->orWhere('record_code', 'like', $like)
|
||||
->orWhere('order_no', 'like', $like)
|
||||
->orWhere('account_order_no', 'like', $like)
|
||||
->orWhere('business_serial_no', 'like', $like)
|
||||
->orWhere('merchant_order_no', 'like', $like)
|
||||
->orWhere('online_order_no', 'like', $like)
|
||||
->orWhere('finance_remark', 'like', $like)
|
||||
->orWhere('remark', 'like', $like);
|
||||
});
|
||||
}
|
||||
|
||||
$sortable = ['occurred_at', 'income', 'expense', 'balance', 'record_code'];
|
||||
$sort = in_array($request->get('sort'), $sortable, true) ? $request->get('sort') : 'occurred_at';
|
||||
$dir = $request->get('dir') === 'asc' ? 'asc' : 'desc';
|
||||
$query->orderBy($sort, $dir);
|
||||
$filters = $request->validate([
|
||||
'store_name' => ['nullable', 'string', 'max:255'],
|
||||
'alipay_account' => ['nullable', 'string', 'max:128'],
|
||||
'bill_item' => ['nullable', 'string', 'max:255'],
|
||||
'business_type' => ['nullable', 'string', 'max:255'],
|
||||
'start' => ['nullable', 'date_format:Y-m-d'],
|
||||
'end' => ['nullable', 'date_format:Y-m-d', 'after_or_equal:start'],
|
||||
'keyword' => ['nullable', 'string', 'max:255'],
|
||||
]);
|
||||
|
||||
$perPage = min(max((int) $request->get('limit', 10), 1), 100);
|
||||
$rows = $query->paginate($perPage);
|
||||
$rows->getCollection()->transform(function (AlipayBill $bill) {
|
||||
$data = $bill->toArray();
|
||||
|
||||
foreach (['occurred_at', 'created_at', 'updated_at'] as $field) {
|
||||
$data[$field] = $bill->{$field}?->format('Y-m-d H:i:s');
|
||||
try {
|
||||
return response()->json($repository->paginate(
|
||||
array_map(static fn (mixed $value): string => trim((string) $value), $filters),
|
||||
$sort,
|
||||
$dir,
|
||||
max((int) $request->get('page', 1), 1),
|
||||
min(max((int) $request->get('limit', 10), 1), 100),
|
||||
));
|
||||
} catch (RuntimeException $exception) {
|
||||
if ($exception->getMessage() !== 'store_receiving_alipay_bill_mirror_not_ready') {
|
||||
throw $exception;
|
||||
}
|
||||
|
||||
return $data;
|
||||
});
|
||||
|
||||
// 汇总:基于当前筛选结果(分页前)
|
||||
$summaryRow = (clone $query)->selectRaw(
|
||||
'COALESCE(SUM(income), 0) as income, ' .
|
||||
'COALESCE(SUM(expense), 0) as expense, ' .
|
||||
'COUNT(*) as count'
|
||||
)->first();
|
||||
|
||||
$latestBalance = (clone $query)
|
||||
->orderByDesc('occurred_at')
|
||||
->orderByDesc('id')
|
||||
->value('balance');
|
||||
|
||||
$data = $rows->toArray();
|
||||
$data['summary'] = [
|
||||
'income' => (float) ($summaryRow->income ?? 0),
|
||||
'expense' => (float) ($summaryRow->expense ?? 0),
|
||||
'count' => (int) ($summaryRow->count ?? 0),
|
||||
'balance' => $latestBalance !== null ? (float) $latestBalance : 0,
|
||||
];
|
||||
|
||||
return response()->json($data);
|
||||
return response()->json([
|
||||
'message' => '淘系支付宝账单共享业务镜像正在首次同步,完成后将自动显示。',
|
||||
'data' => [],
|
||||
'total' => 0,
|
||||
'current_page' => 1,
|
||||
'last_page' => 1,
|
||||
'per_page' => min(max((int) $request->get('limit', 10), 1), 100),
|
||||
'summary' => ['income' => 0, 'expense' => 0, 'balance' => 0, 'count' => 0],
|
||||
], 503);
|
||||
}
|
||||
}
|
||||
|
||||
public function show(AlipayBill $bill)
|
||||
|
||||
@ -0,0 +1,156 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\EcommerceFinance\Repositories;
|
||||
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Modules\EcommerceFinance\Services\BusinessMirrorDatasetRegistry;
|
||||
use RuntimeException;
|
||||
|
||||
/** 店铺收款账单·淘系支付宝账户只读共享业务镜像。 */
|
||||
class StoreReceivingAlipayBillMirrorRepository
|
||||
{
|
||||
public const DATASET = 'jiansun.dbo.alipay_Bill_Detail';
|
||||
|
||||
public function __construct(
|
||||
private readonly BusinessMirrorDatasetRegistry $datasets,
|
||||
private readonly GenericBusinessMirrorRepository $mirror,
|
||||
) {}
|
||||
|
||||
/** @return array{data:array<int,array<string,mixed>>,total:int,current_page:int,last_page:int,per_page:int,summary:array{income:float,expense:float,balance:float,count:int},sync:array<string,mixed>} */
|
||||
public function paginate(array $filters, string $sort, string $direction, int $page, int $perPage): array
|
||||
{
|
||||
$dataset = $this->datasets->get(self::DATASET);
|
||||
if (! $this->mirror->ready($dataset)) {
|
||||
throw new RuntimeException('store_receiving_alipay_bill_mirror_not_ready');
|
||||
}
|
||||
|
||||
$page = max($page, 1);
|
||||
$perPage = min(max($perPage, 1), 100);
|
||||
$direction = $direction === 'asc' ? 'asc' : 'desc';
|
||||
$sortColumn = [
|
||||
'record_code' => 'Id',
|
||||
'occurred_at' => 'happenTime',
|
||||
'income' => 'inAmount',
|
||||
'expense' => 'outAmount',
|
||||
'balance' => 'accountAmount',
|
||||
][$sort] ?? 'happenTime';
|
||||
|
||||
$query = $this->mirror->connection()->table($dataset->targetTable());
|
||||
$this->applyFilters($query, $filters);
|
||||
$total = (clone $query)->count();
|
||||
$summaryRow = (clone $query)->selectRaw(
|
||||
'COALESCE(SUM(CASE WHEN inAmount > 0 THEN inAmount ELSE 0 END), 0) AS income, '
|
||||
. 'COALESCE(SUM(ABS(COALESCE(outAmount, 0))), 0) AS expense, COUNT(*) AS aggregate_count'
|
||||
)->first();
|
||||
$latestBalance = (clone $query)
|
||||
->orderByDesc('happenTime')
|
||||
->orderByDesc('Id')
|
||||
->value('accountAmount');
|
||||
|
||||
$rows = $query
|
||||
->orderBy($sortColumn, $direction)
|
||||
->orderBy('Id', $direction)
|
||||
->forPage($page, $perPage)
|
||||
->get()
|
||||
->map(fn (object $row): array => $this->mapRow($row))
|
||||
->all();
|
||||
$state = $this->mirror->state($dataset) ?? [];
|
||||
|
||||
return [
|
||||
'data' => $rows,
|
||||
'total' => $total,
|
||||
'current_page' => $page,
|
||||
'last_page' => max((int) ceil($total / $perPage), 1),
|
||||
'per_page' => $perPage,
|
||||
'summary' => [
|
||||
'income' => (float) ($summaryRow->income ?? 0),
|
||||
'expense' => (float) ($summaryRow->expense ?? 0),
|
||||
'balance' => (float) ($latestBalance ?? 0),
|
||||
'count' => (int) ($summaryRow->aggregate_count ?? 0),
|
||||
],
|
||||
'sync' => [
|
||||
'status' => $state['status'] ?? 'unknown',
|
||||
'last_succeeded_at' => $state['last_succeeded_at'] ?? null,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
private function applyFilters(Builder $query, array $filters): void
|
||||
{
|
||||
if (($filters['store_name'] ?? '') !== '') {
|
||||
$query->where('shopName', $filters['store_name']);
|
||||
}
|
||||
if (($filters['alipay_account'] ?? '') !== '') {
|
||||
$value = '%' . $this->escapeLike((string) $filters['alipay_account']) . '%';
|
||||
$query->where(function (Builder $query) use ($value): void {
|
||||
$query->where('appId', 'like', $value)
|
||||
->orWhere('account', 'like', $value);
|
||||
});
|
||||
}
|
||||
if (($filters['bill_item'] ?? '') !== '') {
|
||||
$query->where('tradeChannel', $filters['bill_item']);
|
||||
}
|
||||
if (($filters['business_type'] ?? '') !== '') {
|
||||
$query->where('businessType', $filters['business_type']);
|
||||
}
|
||||
if (($filters['start'] ?? '') !== '') {
|
||||
$query->where('happenTime', '>=', $filters['start'] . ' 00:00:00');
|
||||
}
|
||||
if (($filters['end'] ?? '') !== '') {
|
||||
$query->where('happenTime', '<', date('Y-m-d', strtotime($filters['end'] . ' +1 day')) . ' 00:00:00');
|
||||
}
|
||||
if (($filters['keyword'] ?? '') !== '') {
|
||||
$value = '%' . $this->escapeLike((string) $filters['keyword']) . '%';
|
||||
$query->where(function (Builder $query) use ($value): void {
|
||||
$query->where('commodityName', 'like', $value)
|
||||
->orWhere('accountNum', 'like', $value)
|
||||
->orWhere('businessNum', 'like', $value)
|
||||
->orWhere('merchantOrder', 'like', $value)
|
||||
->orWhere('storeNum', 'like', $value)
|
||||
->orWhere('remark', 'like', $value);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private function mapRow(object $row): array
|
||||
{
|
||||
return [
|
||||
'id' => (string) $row->Id,
|
||||
'record_code' => (string) $row->Id,
|
||||
'store_name' => $row->shopName,
|
||||
'custom_remark' => $row->buyerRemark,
|
||||
'bill_item' => $row->tradeChannel,
|
||||
'alipay_account' => $row->appId ?: $row->account,
|
||||
'account_order_no' => $row->accountNum,
|
||||
'business_serial_no' => $row->businessNum,
|
||||
'merchant_order_no' => $row->merchantOrder,
|
||||
'business_base_order_no' => $row->busiBasicNum,
|
||||
'goods_name' => $row->commodityName,
|
||||
'counterparty_account' => $row->oppAccount,
|
||||
'occurred_at' => $this->dateTime($row->happenTime),
|
||||
'income' => max((float) ($row->inAmount ?? 0), 0),
|
||||
'expense' => abs((float) ($row->outAmount ?? 0)),
|
||||
'balance' => (float) ($row->accountAmount ?? 0),
|
||||
'business_type' => $row->businessType,
|
||||
'remark' => $row->remark,
|
||||
'online_order_no' => $row->storeNum,
|
||||
'online_real_store' => $row->shopName,
|
||||
'business_description' => $row->busiDescription,
|
||||
'business_bill_source' => $row->busiBillSource,
|
||||
'business_order_no' => $row->busiNum,
|
||||
'created_at' => '',
|
||||
'updated_at' => '',
|
||||
'modifier' => '',
|
||||
];
|
||||
}
|
||||
|
||||
private function dateTime(mixed $value): string
|
||||
{
|
||||
return $value === null || $value === '' ? '' : date('Y-m-d H:i:s', strtotime((string) $value));
|
||||
}
|
||||
|
||||
private function escapeLike(string $value): string
|
||||
{
|
||||
return str_replace(['\\', '%', '_'], ['\\\\', '\\%', '\\_'], $value);
|
||||
}
|
||||
}
|
||||
@ -76,6 +76,7 @@
|
||||
|
||||
<!-- 数据表 -->
|
||||
<div class="tb-panel">
|
||||
<div v-if="error" class="tb-error" role="alert">{{ error }}</div>
|
||||
<div class="tb-phead">
|
||||
<div class="tb-title">账单明细</div>
|
||||
<div class="tb-count" v-if="!loading">{{ total }} 条</div>
|
||||
@ -192,6 +193,7 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
error: '',
|
||||
rows: [],
|
||||
summary: { income: 0, expense: 0, balance: 0, count: 0 },
|
||||
total: 0,
|
||||
@ -227,6 +229,7 @@ export default {
|
||||
methods: {
|
||||
async fetch() {
|
||||
this.loading = true;
|
||||
this.error = '';
|
||||
const params = {
|
||||
...this.filters,
|
||||
sort: this.sort.key,
|
||||
@ -249,6 +252,8 @@ export default {
|
||||
this.rows = [];
|
||||
this.total = 0;
|
||||
this.summary = { income: 0, expense: 0, balance: 0, count: 0 };
|
||||
this.error = (e.response && e.response.data && e.response.data.message)
|
||||
|| '淘系支付宝账单暂时无法读取,请稍后重试。';
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
@ -380,6 +385,15 @@ export default {
|
||||
box-shadow: 0 1px 2px rgba(16, 24, 40, .04), 0 1px 3px rgba(16, 24, 40, .06);
|
||||
overflow: hidden;
|
||||
}
|
||||
.tb-error {
|
||||
margin: 1rem;
|
||||
border: 1px solid #d8d2c7;
|
||||
border-radius: 0.5rem;
|
||||
background: #f7f0de;
|
||||
padding: 0.75rem 1rem;
|
||||
color: #935f07;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
.tb-phead { display: flex; align-items: center; gap: 0.625rem; padding: 0.875rem 1rem; border-bottom: 1px solid #e6e9f0; }
|
||||
.tb-title { font-weight: 600; font-size: 0.875rem; }
|
||||
.tb-count { color: #9aa3b2; font-size: 0.75rem; }
|
||||
|
||||
@ -3,8 +3,9 @@
|
||||
namespace Tests\Feature\Business;
|
||||
|
||||
use App\Models\Auth\Permission;
|
||||
use Modules\EcommerceFinance\Models\AlipayBill;
|
||||
use Mockery;
|
||||
use Modules\EcommerceFinance\Repositories\NonJingxiRefundMirrorRepository;
|
||||
use Modules\EcommerceFinance\Repositories\StoreReceivingAlipayBillMirrorRepository;
|
||||
use Tests\Feature\FeatureTestCase;
|
||||
|
||||
class PagesTest extends FeatureTestCase
|
||||
@ -15,22 +16,20 @@ class PagesTest extends FeatureTestCase
|
||||
Permission::where('name', 'read-admin-panel')->firstOrFail()
|
||||
);
|
||||
|
||||
AlipayBill::create([
|
||||
'company_id' => $this->company->id,
|
||||
'record_code' => 'time-format-test',
|
||||
'store_name' => '测试店铺',
|
||||
'bill_item' => '交易收款',
|
||||
'business_type' => '交易付款',
|
||||
'alipay_account' => '2088000000000000',
|
||||
'counterparty_account' => '测试账号',
|
||||
'account_order_no' => 'account-order-test',
|
||||
'business_serial_no' => 'business-serial-test',
|
||||
'goods_name' => '测试商品',
|
||||
'income' => 1,
|
||||
'expense' => 0,
|
||||
'balance' => 1,
|
||||
'occurred_at' => '2026-07-15 23:59:12',
|
||||
$repository = Mockery::mock(StoreReceivingAlipayBillMirrorRepository::class);
|
||||
$repository->shouldReceive('paginate')->once()->andReturn([
|
||||
'current_page' => 1,
|
||||
'last_page' => 1,
|
||||
'per_page' => 10,
|
||||
'total' => 1,
|
||||
'data' => [[
|
||||
'record_code' => 'time-format-test',
|
||||
'occurred_at' => '2026-07-15 23:59:12',
|
||||
]],
|
||||
'summary' => ['income' => 1.0, 'expense' => 0.0, 'count' => 1, 'balance' => 1.0],
|
||||
'sync' => ['status' => 'succeeded', 'last_succeeded_at' => '2026-09-07 09:00:00'],
|
||||
]);
|
||||
$this->app->instance(StoreReceivingAlipayBillMirrorRepository::class, $repository);
|
||||
|
||||
$this->loginAs()
|
||||
->withHeader('X-Requested-With', 'XMLHttpRequest')
|
||||
|
||||
@ -75,7 +75,7 @@ class BusinessDataPlacementTest extends TestCase
|
||||
$upstream = require base_path('config/upstream_mirrors.php');
|
||||
$businessData = require base_path('config/business_data.php');
|
||||
|
||||
$this->assertCount(51, $upstream['datasets']);
|
||||
$this->assertCount(52, $upstream['datasets']);
|
||||
$upstreamKeys = array_keys($upstream['datasets']);
|
||||
$policyKeys = array_keys($businessData['datasets']);
|
||||
sort($upstreamKeys);
|
||||
|
||||
@ -0,0 +1,145 @@
|
||||
<?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\Services\BusinessMirrorDatasetRegistry;
|
||||
use RuntimeException;
|
||||
use Tests\TestCase;
|
||||
|
||||
class StoreReceivingAlipayBillMirrorRepositoryTest extends TestCase
|
||||
{
|
||||
private StoreReceivingAlipayBillMirrorRepository $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' => [
|
||||
StoreReceivingAlipayBillMirrorRepository::DATASET => [
|
||||
'source_key' => 'jiansun',
|
||||
'schema' => 'dbo',
|
||||
'object' => 'alipay_Bill_Detail',
|
||||
'target' => 'js_alipay_bill_detail',
|
||||
'keys' => ['Id'],
|
||||
],
|
||||
],
|
||||
]);
|
||||
DB::purge('business_mirror');
|
||||
|
||||
Schema::connection('business_mirror')->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();
|
||||
});
|
||||
Schema::connection('business_mirror')->create('js_alipay_bill_detail', function (Blueprint $table): void {
|
||||
$table->integer('Id');
|
||||
foreach ([
|
||||
'accountNum', 'businessNum', 'merchantOrder', 'commodityName', 'oppAccount',
|
||||
'tradeChannel', 'businessType', 'remark', 'shopName', 'busiBillSource',
|
||||
'busiDescription', 'busiNum', 'busiBasicNum', 'buyerRemark', 'storeNum',
|
||||
'appId', 'account', 'bindingType',
|
||||
] as $column) {
|
||||
$table->string($column)->nullable();
|
||||
}
|
||||
$table->dateTime('happenTime')->nullable();
|
||||
$table->date('date')->nullable();
|
||||
$table->decimal('inAmount', 20, 6)->nullable();
|
||||
$table->decimal('outAmount', 20, 6)->nullable();
|
||||
$table->decimal('accountAmount', 20, 6)->nullable();
|
||||
});
|
||||
|
||||
$this->repository = new StoreReceivingAlipayBillMirrorRepository(
|
||||
new BusinessMirrorDatasetRegistry(),
|
||||
new GenericBusinessMirrorRepository(app('db')),
|
||||
);
|
||||
}
|
||||
|
||||
public function test_it_returns_filtered_pages_and_summary_using_attachment_field_names(): void
|
||||
{
|
||||
$this->markReady();
|
||||
DB::connection('business_mirror')->table('js_alipay_bill_detail')->insert([
|
||||
$this->row(1, '天猫-测试店', '2026-08-01 09:00:00', 100.25, 0, 500.25),
|
||||
$this->row(2, '天猫-测试店', '2026-08-02 09:00:00', 0, -20.10, 480.15),
|
||||
$this->row(3, '其他店铺', '2026-08-03 09:00:00', 88, 0, 88),
|
||||
]);
|
||||
|
||||
$result = $this->repository->paginate(
|
||||
['store_name' => '天猫-测试店', 'start' => '2026-08-01', 'end' => '2026-08-31'],
|
||||
'occurred_at',
|
||||
'desc',
|
||||
2,
|
||||
1,
|
||||
);
|
||||
|
||||
$this->assertSame(2, $result['total']);
|
||||
$this->assertSame(2, $result['current_page']);
|
||||
$this->assertSame('1', $result['data'][0]['record_code']);
|
||||
$this->assertSame('2088000000000000', $result['data'][0]['alipay_account']);
|
||||
$this->assertSame(100.25, $result['summary']['income']);
|
||||
$this->assertSame(20.10, $result['summary']['expense']);
|
||||
$this->assertSame(480.15, $result['summary']['balance']);
|
||||
$this->assertSame(2, $result['summary']['count']);
|
||||
$this->assertSame('2026-09-07 09:00:00', $result['sync']['last_succeeded_at']);
|
||||
}
|
||||
|
||||
public function test_it_rejects_an_unready_mirror_instead_of_querying_upstream_from_http(): void
|
||||
{
|
||||
$this->expectException(RuntimeException::class);
|
||||
$this->expectExceptionMessage('store_receiving_alipay_bill_mirror_not_ready');
|
||||
|
||||
$this->repository->paginate([], 'occurred_at', 'desc', 1, 10);
|
||||
}
|
||||
|
||||
private function markReady(): void
|
||||
{
|
||||
DB::connection('business_mirror')->table('business_mirror_table_states')->insert([
|
||||
'dataset_key' => StoreReceivingAlipayBillMirrorRepository::DATASET,
|
||||
'initial_completed_at' => '2026-09-07 09:00:00',
|
||||
'status' => 'succeeded',
|
||||
'last_succeeded_at' => '2026-09-07 09:00:00',
|
||||
]);
|
||||
}
|
||||
|
||||
private function row(int $id, string $shop, string $time, float $income, float $expense, float $balance): array
|
||||
{
|
||||
return [
|
||||
'Id' => $id,
|
||||
'accountNum' => 'ACCOUNT-' . $id,
|
||||
'businessNum' => 'BUSINESS-' . $id,
|
||||
'merchantOrder' => 'MERCHANT-' . $id,
|
||||
'commodityName' => '商品' . $id,
|
||||
'happenTime' => $time,
|
||||
'oppAccount' => '对方账号',
|
||||
'inAmount' => $income,
|
||||
'outAmount' => $expense,
|
||||
'accountAmount' => $balance,
|
||||
'tradeChannel' => '交易收款',
|
||||
'businessType' => '交易付款',
|
||||
'remark' => '备注',
|
||||
'shopName' => $shop,
|
||||
'date' => substr($time, 0, 10),
|
||||
'busiBillSource' => '支付宝',
|
||||
'busiDescription' => '业务说明',
|
||||
'busiNum' => 'BILL-' . $id,
|
||||
'busiBasicNum' => 'BASE-' . $id,
|
||||
'buyerRemark' => '自定义备注',
|
||||
'storeNum' => 'ORDER-' . $id,
|
||||
'appId' => '2088000000000000',
|
||||
'account' => 'account@example.test',
|
||||
'bindingType' => '淘系',
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -16,6 +16,8 @@ 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->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