diff --git a/modules/EcommerceFinance/AccountBillColumns.php b/modules/EcommerceFinance/AccountBillColumns.php new file mode 100644 index 00000000..db4eda34 --- /dev/null +++ b/modules/EcommerceFinance/AccountBillColumns.php @@ -0,0 +1,236 @@ +}> + */ + public static function map(): array + { + return [ + 'taobao-wechat' => [ + 'title' => '淘系微信账户', + 'columns' => [ + ['记录编码', 'record_code', 'num'], + ['店铺', 'store_name', 'text'], + ['财务备注', 'finance_remark', 'text'], + ['订单号', 'order_no', 'num'], + ['金额', 'amount', 'money'], + ['自定义备注', 'custom_remark', 'text'], + ['账单项目', 'bill_item', 'tag'], + ['线上单号', 'payload.online_order_no', 'num'], + ['淘宝订单编号', 'payload.taobao_order_no', 'num'], + ['支付流水号', 'payload.pay_serial_no', 'num'], + ['入账时间', 'occurred_at', 'date'], + ['入账类型', 'payload.entry_type', 'text'], + ['收入金额', 'income', 'money_in'], + ['支出金额', 'expense', 'money_out'], + ['业务描述', 'payload.business_desc', 'text'], + ['备注', 'remark', 'text'], + ['JST拉取时间', 'payload.jst_pulled_at', 'date'], + ['修改人', 'modifier', 'text'], + ], + ], + + 'pinduoduo' => [ + 'title' => '拼多多账户', + 'columns' => [ + ['记录编码', 'record_code', 'num'], + ['店铺', 'store_name', 'text'], + ['财务备注', 'finance_remark', 'text'], + ['订单', 'order_no', 'num'], + ['金额', 'amount', 'money'], + ['自定义备注', 'custom_remark', 'text'], + ['账单项目', 'bill_item', 'tag'], + ['商户订单号', 'payload.merchant_order_no', 'num'], + ['线上单号', 'payload.online_order_no', 'num'], + ['财务类型', 'payload.finance_type', 'text'], + ['类型', 'payload.type', 'text'], + ['发生时间', 'occurred_at', 'date'], + ['收入金额', 'income', 'money_in'], + ['支出金额', 'expense', 'money_out'], + ['备注', 'remark', 'text'], + ['修改时间', 'updated_at', 'date'], + ['修改人', 'modifier', 'text'], + ], + ], + + 'jd-wallet' => [ + 'title' => '京东网银钱包账户', + 'columns' => [ + ['记录编码', 'record_code', 'num'], + ['店铺', 'store_name', 'text'], + ['财务备注', 'finance_remark', 'text'], + ['订单', 'order_no', 'num'], + ['金额', 'amount', 'money'], + ['自定义备注', 'custom_remark', 'text'], + ['账单项目', 'bill_item', 'tag'], + ['线上单号', 'payload.online_order_no', 'num'], + ['单据编号', 'payload.doc_no', 'num'], + ['订单编号', 'payload.order_no_2', 'num'], + ['商户订单号', 'payload.merchant_order_no', 'num'], + ['单据类型', 'payload.doc_type', 'text'], + ['商品编码', 'payload.goods_code', 'num'], + ['商品名称', 'payload.goods_name', 'text'], + ['费用结算时间', 'occurred_at', 'date'], + ['费用发生时间', 'payload.fee_occurred_at', 'date'], + ['费用项', 'payload.fee_item', 'text'], + ['收入金额', 'income', 'money_in'], + ['支出金额', 'expense', 'money_out'], + ['币种', 'payload.currency', 'text'], + ['钱包结算备注', 'payload.wallet_remark', 'text'], + ['备注', 'remark', 'text'], + ['店铺号', 'payload.store_no', 'num'], + ['京东门店编码', 'payload.jd_store_code', 'num'], + ['品牌门店编号', 'payload.brand_store_no', 'num'], + ['门店名称', 'payload.store_name_2', 'text'], + ['账户代码', 'payload.account_code', 'num'], + ['修改时间', 'updated_at', 'date'], + ['修改人', 'modifier', 'text'], + ], + ], + + 'jd-transactions' => [ + 'title' => '京东流水', + 'columns' => [ + ['记录编码', 'record_code', 'num'], + ['店铺', 'store_name', 'text'], + ['财务备注', 'finance_remark', 'text'], + ['订单', 'order_no', 'num'], + ['金额', 'amount', 'money'], + ['自定义备注', 'custom_remark', 'text'], + ['账单项目', 'bill_item', 'tag'], + ['线上单号', 'payload.online_order_no', 'num'], + ['流水号', 'payload.serial_no', 'num'], + ['交易订单号', 'payload.tx_order_no', 'num'], + ['账户名称', 'payload.account_name', 'text'], + ['费用结算时间', 'occurred_at', 'date'], + ['账单日期', 'payload.bill_date', 'date'], + ['商户订单号', 'payload.merchant_order_no', 'num'], + ['交易备注', 'payload.tx_remark', 'text'], + ['交易类型', 'payload.tx_type', 'text'], + ['收入金额', 'income', 'money_in'], + ['支出金额', 'expense', 'money_out'], + ['账户余额', 'balance', 'balance'], + ['币种', 'payload.currency', 'text'], + ['记账请求号', 'payload.book_request_no', 'num'], + ['业务订单号', 'payload.business_order_no', 'num'], + ['修改时间', 'updated_at', 'date'], + ['修改人', 'modifier', 'text'], + ], + ], + + 'douyin' => [ + 'title' => '抖音账户', + 'columns' => [ + ['记录编码', 'record_code', 'num'], + ['店铺', 'store_name', 'text'], + ['财务备注', 'finance_remark', 'text'], + ['订单', 'order_no', 'num'], + ['金额', 'amount', 'money'], + ['自定义备注', 'custom_remark', 'text'], + ['商户主体名称', 'payload.merchant_name', 'text'], + ['账单项目', 'bill_item', 'tag'], + ['动账流水号', 'payload.move_serial_no', 'num'], + ['订单号', 'payload.order_no_2', 'num'], + ['线上单号', 'payload.online_order_no', 'num'], + ['子订单号', 'payload.sub_order_no', 'num'], + ['商品ID', 'payload.goods_id', 'num'], + ['账务类型', 'payload.account_type', 'text'], + ['结算时间', 'occurred_at', 'date'], + ['动账摘要', 'payload.move_summary', 'text'], + ['下单时间', 'payload.order_placed_at', 'date'], + ['业务类型', 'payload.business_type', 'text'], + ['结算账户', 'payload.settle_account', 'text'], + ['动账场景', 'payload.move_scene', 'text'], + ['达人ID', 'payload.talent_id', 'num'], + ['达人名称', 'payload.talent_name', 'text'], + ['备注', 'remark', 'text'], + ['收入', 'income', 'money_in'], + ['支出', 'expense', 'money_out'], + ['创建时间', 'created_at', 'date'], + ['修改时间', 'updated_at', 'date'], + ['修改人', 'modifier', 'text'], + ], + ], + + 'dewu' => [ + 'title' => '得物账户', + 'columns' => [ + ['记录编码', 'record_code', 'num'], + ['店铺', 'store_name', 'text'], + ['财务备注', 'finance_remark', 'text'], + ['金额', 'amount', 'money'], + ['订单号', 'order_no', 'num'], + ['线上单号', 'payload.online_order_no', 'num'], + ['自定义备注', 'custom_remark', 'text'], + ['账单项目', 'bill_item', 'tag'], + ['店铺', 'payload.store_2', 'text'], + ['订单类型', 'payload.order_type', 'text'], + ['商品名称', 'payload.goods_name', 'text'], + ['商品货号', 'payload.goods_no', 'num'], + ['数量', 'payload.quantity', 'num'], + ['渠道', 'payload.channel', 'text'], + ['业务类型', 'payload.business_type', 'text'], + ['收入金额', 'income', 'money_in'], + ['支出金额', 'expense', 'money_out'], + ['线上单号对应实际店铺', 'payload.online_real_store', 'text'], + ['订单创建时间', 'payload.order_created_at', 'date'], + ['支付/退款时间', 'occurred_at', 'date'], + ['发货/退货时间', 'payload.ship_return_at', 'date'], + ['结算状态', 'payload.settle_status', 'text'], + ['收款账期', 'payload.pay_period', 'text'], + ['拉取时间', 'payload.pulled_at', 'date'], + ['修改时间', 'updated_at', 'date'], + ['修改人', 'modifier', 'text'], + ], + ], + ]; + } + + public static function types(): array + { + return array_keys(self::map()); + } + + public static function has(string $type): bool + { + return isset(self::map()[$type]); + } + + /** + * @return array{title:string,columns:array} + */ + public static function for(string $type): ?array + { + $map = self::map(); + + if (! isset($map[$type])) { + return null; + } + + $columns = array_map(function ($c) { + return ['label' => $c[0], 'field' => $c[1], 'kind' => $c[2]]; + }, $map[$type]['columns']); + + return [ + 'title' => $map[$type]['title'], + 'columns' => $columns, + ]; + } +} diff --git a/modules/EcommerceFinance/Database/Migrations/2026_07_21_000002_create_ec_account_bills_table.php b/modules/EcommerceFinance/Database/Migrations/2026_07_21_000002_create_ec_account_bills_table.php new file mode 100644 index 00000000..f35af01d --- /dev/null +++ b/modules/EcommerceFinance/Database/Migrations/2026_07_21_000002_create_ec_account_bills_table.php @@ -0,0 +1,53 @@ +increments('id'); + $table->unsignedInteger('company_id'); + $table->string('bill_type', 32); // taobao-wechat/pinduoduo/jd-wallet/jd-transactions/douyin/dewu + + // 通用列(各类型共有 / 用于筛选汇总) + $table->string('record_code', 64); + $table->string('store_name', 255); + $table->string('finance_remark', 255)->nullable(); // 财务备注 + $table->string('order_no', 64)->nullable(); // 订单 / 订单号 + $table->decimal('amount', 20, 6)->nullable(); // 金额 + $table->string('custom_remark', 255)->nullable(); // 自定义备注 + $table->string('bill_item', 64)->nullable(); // 账单项目 + $table->dateTime('occurred_at')->nullable(); // 主时间(入账/发生/结算...) + $table->decimal('income', 20, 6)->default(0); // 收入(金额) + $table->decimal('expense', 20, 6)->default(0); // 支出(金额) + $table->decimal('balance', 20, 6)->default(0); // 余额/账户余额 + $table->string('remark', 512)->nullable(); // 备注 + $table->string('modifier', 64)->nullable(); // 修改人 + $table->json('payload')->nullable(); // 各类型特有列 + + $table->unsignedInteger('created_by')->nullable(); + $table->string('created_from', 100)->nullable(); + $table->timestamps(); + $table->softDeletes(); + + $table->index(['company_id', 'bill_type']); + $table->index(['company_id', 'bill_type', 'store_name']); + $table->index(['company_id', 'bill_type', 'occurred_at']); + $table->index(['company_id', 'bill_type', 'bill_item']); + }); + } + + public function down(): void + { + Schema::dropIfExists('ec_account_bills'); + } +}; diff --git a/modules/EcommerceFinance/Database/Seeds/AccountBillsTableSeeder.php b/modules/EcommerceFinance/Database/Seeds/AccountBillsTableSeeder.php new file mode 100644 index 00000000..b9bd1b67 --- /dev/null +++ b/modules/EcommerceFinance/Database/Seeds/AccountBillsTableSeeder.php @@ -0,0 +1,168 @@ +value('id') ?? 1); + + $shops = [ + '淘宝-亿家达家居鞋柜电脑桌', + '天猫-亿家达旗舰店', + '天猫-卓禾家具旗舰店', + '拼多多-亿家达家居旗舰店', + '京东-亿家达旗舰店', + '抖音-亿家达旗舰店', + '得物-亿家达旗舰店', + ]; + + $items = ['交易收款', '退款', '提现到银行卡', '服务费', '红包补贴', '佣金扣款', '平台补贴', '以旧换新自补平台过桥']; + + $now = now(); + $seq = 3000000000000000000; + + foreach (AccountBillColumns::types() as $billType) { + $config = AccountBillColumns::for($billType); + DB::table('ec_account_bills') + ->where('company_id', $companyId) + ->where('bill_type', $billType) + ->delete(); + + $balance = 50000.00; + + for ($i = 0; $i < 6; $i++) { + $real = [ + 'company_id' => $companyId, + 'bill_type' => $billType, + 'modifier' => 'api', + 'created_by' => null, + 'created_from' => 'ecommerce-finance', + 'created_at' => $now, + 'updated_at' => $now, + ]; + $payload = []; + + $income = $i % 2 === 0 ? round(60 + ($i * 37 % 600), 2) : 0.00; + $expense = $i % 2 === 1 ? round(8 + ($i * 13 % 120), 2) : 0.00; + $balance += $income - $expense; + + $day = str_pad((string) (1 + ($i % 15)), 2, '0', STR_PAD_LEFT); + $hh = str_pad((string) (8 + ($i % 12)), 2, '0', STR_PAD_LEFT); + $mm = str_pad((string) (($i * 7) % 60), 2, '0', STR_PAD_LEFT); + $occurred = '2026-07-' . $day . ' ' . $hh . ':' . $mm . ':11'; + + foreach ($config['columns'] as $col) { + [$label, $field, $kind] = [$col['label'], $col['field'], $col['kind']]; + $value = $this->valueFor($field, $kind, $label, [ + 'i' => $i, + 'shop' => $shops[($i + crc32($billType)) % count($shops)], + 'item' => $items[$i % count($items)], + 'income' => $income, + 'expense' => $expense, + 'balance' => round($balance, 2), + 'occurred' => $occurred, + 'seq' => $seq, + ]); + + if (strpos($field, 'payload.') === 0) { + $payload[substr($field, 8)] = $value; + } else { + $real[$field] = $value; + } + } + + $real['payload'] = json_encode($payload, JSON_UNESCAPED_UNICODE); + $seq++; + + DB::table('ec_account_bills')->insert($this->normalizeRow($real)); + } + } + } + + private function valueFor($field, $kind, $label, array $c) + { + // 真实列优先按字段定值 + switch ($field) { + case 'record_code': + return (string) ($c['seq']); + case 'store_name': + return $c['shop']; + case 'bill_item': + return $c['item']; + case 'occurred_at': + return $c['occurred']; + case 'income': + return $c['income']; + case 'expense': + return $c['expense']; + case 'balance': + return $c['balance']; + case 'amount': + return $c['income'] + $c['expense']; + case 'order_no': + return (string) (1570700000000000000 + $c['i']); + case 'finance_remark': + case 'custom_remark': + return $c['i'] % 3 === 0 ? '财务备注-' . $c['i'] : null; + case 'remark': + return $c['item'] . ' 演示备注'; + case 'modifier': + return 'api'; + case 'created_at': + case 'updated_at': + return now(); + } + + // payload 列按 kind 推断 + switch ($kind) { + case 'date': + return $c['occurred']; + case 'num': + return (string) (3300000000000000 + $c['i'] + crc32($field)); + case 'money': + case 'balance': + return $c['amount'] ?? 0; + case 'money_in': + return $c['income']; + case 'money_out': + return $c['expense']; + case 'tag': + return $c['item']; + default: + return $label . '-' . $c['i']; + } + } + + /** + * 保证插入行类型与表结构一致(decimal/null)。 + */ + private function normalizeRow(array $real): array + { + $decimals = ['amount', 'income', 'expense', 'balance']; + + foreach ($decimals as $k) { + if (isset($real[$k])) { + $real[$k] = (float) $real[$k]; + } + } + + if (empty($real['finance_remark'])) { + $real['finance_remark'] = null; + } + if (empty($real['custom_remark'])) { + $real['custom_remark'] = null; + } + + return $real; + } +} diff --git a/modules/EcommerceFinance/Http/Controllers/AccountBills.php b/modules/EcommerceFinance/Http/Controllers/AccountBills.php new file mode 100644 index 00000000..a1bf5fb8 --- /dev/null +++ b/modules/EcommerceFinance/Http/Controllers/AccountBills.php @@ -0,0 +1,91 @@ +get('bill_type', ''); + + if (! AccountBillColumns::has($billType)) { + abort(404, '未知的账户类型。'); + } + + $query = AccountBill::query() + ->where('company_id', company_id()) + ->where('bill_type', $billType); + + if ($store = $request->get('store_name')) { + $query->where('store_name', $store); + } + + if ($item = $request->get('bill_item')) { + $query->where('bill_item', $item); + } + + 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('record_code', 'like', $like) + ->orWhere('order_no', 'like', $like) + ->orWhere('remark', 'like', $like) + ->orWhere('bill_item', '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); + + $perPage = min(max((int) $request->get('limit', 10), 1), 100); + $rows = $query->paginate($perPage); + + // 汇总(分页前) + $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, + ]; + $data['columns'] = AccountBillColumns::for($billType); + + return response()->json($data); + } + + public function show(AccountBill $bill) + { + return response()->json($bill); + } +} diff --git a/modules/EcommerceFinance/Models/AccountBill.php b/modules/EcommerceFinance/Models/AccountBill.php new file mode 100644 index 00000000..509d302c --- /dev/null +++ b/modules/EcommerceFinance/Models/AccountBill.php @@ -0,0 +1,42 @@ + 'datetime', + 'amount' => 'decimal:6', + 'income' => 'decimal:6', + 'expense' => 'decimal:6', + 'balance' => 'decimal:6', + 'payload' => 'array', + ]; +} diff --git a/modules/EcommerceFinance/Routes/admin.php b/modules/EcommerceFinance/Routes/admin.php index 5b38223f..bb79602b 100644 --- a/modules/EcommerceFinance/Routes/admin.php +++ b/modules/EcommerceFinance/Routes/admin.php @@ -5,7 +5,11 @@ use Illuminate\Support\Facades\Route; Route::admin('ecommerce-finance', function () { Route::view('/', 'ecommerce-finance::index')->name('index'); - // 店铺收款账户账单 › 淘系支付宝账户 + // 店铺收款账户账单 › 淘系支付宝账户(独立表 ec_alipay_bills) Route::get('alipay-bills', 'AlipayBills@index')->name('alipay-bills.index'); Route::get('alipay-bills/{bill}', 'AlipayBills@show')->name('alipay-bills.show'); + + // 店铺收款账户账单 › 其它账户类型(通用表 ec_account_bills,按 bill_type 区分) + Route::get('account-bills', 'AccountBills@index')->name('account-bills.index'); + Route::get('account-bills/{bill}', 'AccountBills@show')->name('account-bills.show'); }); diff --git a/resources/assets/js/components/business/AccountBills.vue b/resources/assets/js/components/business/AccountBills.vue new file mode 100644 index 00000000..9c366323 --- /dev/null +++ b/resources/assets/js/components/business/AccountBills.vue @@ -0,0 +1,319 @@ + + + + + diff --git a/resources/assets/js/views/business/pages.js b/resources/assets/js/views/business/pages.js index fa6348bf..e194e66e 100644 --- a/resources/assets/js/views/business/pages.js +++ b/resources/assets/js/views/business/pages.js @@ -1,48 +1,74 @@ /** - * 业务工作台(店铺管理 › 各子功能)统一前端入口。 + * 业务工作台(账单流水数据 › 各子功能)统一前端入口。 * 对应 Blade:resources/views/business/pages/show.blade.php - * 从当前 URL(/{company_id}/workspace/{section}/{page}/{tab})取出 - * company_id 与 page slug,挂载 Vue 并按 slug 渲染对应组件。 + * 路由:/{company_id}/workspace/{section}/{page}/{tab} * - * 淘系支付宝账户(store-receiving-bill › taobao-alipay)在此实现; - * 京东流水(jd-transactions)等后续页面复用同一框架,按 slug 增加分支即可。 + * 店铺收款账户账单(store-receiving-bills)下各 tab: + * - taobao-alipay → 独立实现 TaobaoAlipayBills(表 ec_alipay_bills) + * - taobao-wechat / pinduoduo / jd-wallet / jd-transactions / douyin / dewu + * → 通用实现 AccountBills(表 ec_account_bills,按 bill_type 区分,列配置由后端下发) */ require('./../../bootstrap'); 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']; function pageContext() { const parts = (window.location.pathname || '').split('/').filter(Boolean); const idx = parts.indexOf('workspace'); if (idx === -1) { - return { companyId: '', page: '' }; + return { companyId: '', section: '', page: '', tab: '' }; } - const companyId = idx > 0 ? parts[idx - 1] : ''; - const page = parts[idx + 1] || ''; - - return { companyId, page }; + return { + companyId: idx > 0 ? parts[idx - 1] : '', + section: parts[idx + 1] || '', + page: parts[idx + 2] || '', + tab: parts[idx + 3] || '', + }; } -const { companyId, page } = pageContext(); +const { companyId, page, tab } = pageContext(); -const apiBase = companyId +const alipayBase = companyId ? `/${companyId}/ecommerce-finance/alipay-bills` : '/ecommerce-finance/alipay-bills'; +const accountBase = companyId + ? `/${companyId}/ecommerce-finance/account-bills` + : '/ecommerce-finance/account-bills'; + +function resolveComponent() { + // 店铺收款账户账单 › 淘系支付宝账户(独立实现) + if (page === 'store-receiving-bills' && tab === 'taobao-alipay') { + return { Component: TaobaoAlipayBills, props: { apiBase: alipayBase } }; + } + + // 店铺收款账户账单 › 其它账户类型(通用实现) + if (page === 'store-receiving-bills' && GENERIC_TABS.indexOf(tab) !== -1) { + return { Component: AccountBills, props: { apiBase: accountBase, billType: tab } }; + } + + return null; +} + const mount = document.querySelector('.business-blank-page'); if (mount) { + const resolved = resolveComponent(); + /* eslint-disable no-new */ new Vue({ el: mount, render(h) { - if (page === 'taobao-alipay') { - return h(TaobaoAlipayBills, { props: { apiBase } }); + if (resolved) { + return h(resolved.Component, { props: resolved.props }); } return h('div', { class: 'business-page-empty' }, '该页面尚未实现'); diff --git a/resources/views/business/pages/show.blade.php b/resources/views/business/pages/show.blade.php index 32822d6f..e99eebd2 100644 --- a/resources/views/business/pages/show.blade.php +++ b/resources/views/business/pages/show.blade.php @@ -99,7 +99,7 @@
- @if (in_array($page['slug'] ?? null, ['taobao-alipay', 'taobao-wechat', 'jd-wallet', 'jd-transactions'])) + @if (($page['slug'] ?? null) === 'store-receiving-bills') @endif