合并:保留业务更新并恢复生产部署规则

This commit is contained in:
WuYongJie 2026-07-21 18:49:28 +08:00
commit 6dfda671c4
67 changed files with 5959 additions and 30 deletions

2
.gitignore vendored
View File

@ -107,3 +107,5 @@ modules/*
/artifacts/
/runtime/
/.caiwu-build/
.workbuddy/
runtime/.env

119
LOCAL_DEPLOY.md Normal file
View File

@ -0,0 +1,119 @@
# CaiWuXiTong 本地部署说明
## 项目概况
- 基于 [Akaunting](https://akaunting.com) 二次开发的"江陵"版财务系统
- 技术栈:PHP 8.3 + Laravel + Vue + Apache + MySQL 8.0
- 访问端口:**18088**
## 文件清单(本地部署新增)
| 文件 | 作用 |
|------|------|
| `docker-compose.local.yml` | 本地 compose:app + mysql,bridge 网络 |
| `deploy/Dockerfile.local` | 本地 Dockerfile:基于原 Dockerfile,增加 composer/npm 依赖安装 |
| `deploy/local-init.sh` | 一键构建+启动+初始化脚本 |
| `runtime/.env` | 应用环境配置(挂载到容器 `/var/www/html/.env`) |
| `LOCAL_DEPLOY.md` | 本说明 |
> 原 `compose.jiangling.yml` / `deploy/Dockerfile` 未改动,保留团队主线一致性。
## 一键部署
```bash
cd CaiWuXiTong_src
bash deploy/local-init.sh
```
## 手动步骤
```bash
# 1. 启动
docker compose -f docker-compose.local.yml up -d --build
# 2. 初始化数据库与管理员
docker compose -f docker-compose.local.yml exec app \
php artisan install --no-interaction \
--db-host=mysql --db-name=akaunting \
--db-username=akaunting --db-password=akaunting123 \
--admin-email=admin@company.com --admin-password=admin123 \
--locale=zh-CN
# 3. 访问
open http://127.0.0.1:18088
```
## 登录信息
- **地址**:http://127.0.0.1:18088
- **邮箱**:`admin@company.com`
- **密码**:`admin123`
## 常用维护命令
```bash
# 查看日志
docker compose -f docker-compose.local.yml logs -f app
# 重启
docker compose -f docker-compose.local.yml restart app
# 进入容器
docker compose -f docker-compose.local.yml exec app bash
# 进入 MySQL
docker compose -f docker-compose.local.yml exec mysql mysql -uakaunting -pakaunting123 akaunting
# 清缓存
docker compose -f docker-compose.local.yml exec app php artisan optimize:clear
# 重新迁移
docker compose -f docker-compose.local.yml exec app php artisan migrate --force
# 停止 / 启动 / 销毁
docker compose -f docker-compose.local.yml stop
docker compose -f docker-compose.local.yml start
docker compose -f docker-compose.local.yml down # 仅停容器
docker compose -f docker-compose.local.yml down -v # 同时删除数据卷(会清空数据库!)
```
## 端口映射
| 服务 | 宿主机端口 | 容器端口 |
|------|-----------|----------|
| Akaunting (Apache) | 18088 | 18088 |
| MySQL | 13306 | 3306 |
## 数据卷
- `jiangling_storage` —— 应用上传文件、日志、缓存
- `jiangling_mysql` —— MySQL 数据文件
## 注意事项
- 修改 `.env` 后需重启 app 容器:`docker compose -f docker-compose.local.yml restart app`
- 修改源码后需重新构建:`docker compose -f docker-compose.local.yml up -d --build app`
- 此本地配置仅供开发/测试,**不要用于生产**(密码弱、APP_DEBUG=true)
## 重要:DB_PREFIX 必须用短前缀
项目某个模块迁移创建了较长的唯一索引
(`ec_stores_company_id_channel_id_external_id_deleted_at_unique`,60 字符),
MySQL 标识符上限 64 字符,所以 `DB_PREFIX` 必须 ≤ 4 字符。本部署用 `a_`。
⚠️ **不要**让 `php artisan install` 自动生成前缀(它默认用 `str_random(3) . '_'` 是 4 字符,
刚好不超),但**首次 install 必须显式传 `--db-prefix=a_`** 或其他 ≤4 字符值,
否则可能随机到 5+ 字符前缀触发 1059 错误。
## 重置数据(清空重来)
```bash
docker compose -f docker-compose.local.yml exec mysql \
mysql -uroot -prootpass -e "DROP DATABASE IF EXISTS akaunting; CREATE DATABASE akaunting CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
docker compose -f docker-compose.local.yml exec app php artisan install --no-interaction \
--db-host=mysql --db-name=akaunting \
--db-username=akaunting --db-password=akaunting123 \
--db-prefix=a_ \
--company-name="本地测试公司" --company-email=admin@company.com \
--admin-email=admin@company.com --admin-password=admin123 \
--locale=zh-CN
```
## 部署中遇到并已解决的问题(备忘)
1. **Docker Hub 国内访问**:daemon.json 配置 `https://docker.1ms.run` 镜像源
2. **containerd snapshotter 导致 binary 全 0 字节**:在 Docker Desktop 设置里关闭 `UseContainerdSnapshotter`,改用 overlay2
3. **apt 交互卡住**:Dockerfile 加 `DEBIAN_FRONTEND=noninteractive` + `/etc/apt/apt.conf.d/99force-conf`
4. **deb.debian.org 不稳**:Dockerfile 切到清华 Debian 镜像
5. **node-sass 需要 python/make/g++**:Dockerfile 补装 `python3 make g++`
6. **npm run prod OOM**:改用 `npm run dev` + `NODE_OPTIONS=--max-old-space-size=2048`
7. **ec_stores 索引名超长**:用 `--db-prefix=a_`(见上)

View File

@ -0,0 +1,313 @@
<?php
namespace App\Http\Controllers\Banking;
use App\Abstracts\Http\Controller;
use App\Http\Requests\Banking\MarketingAccountBill as Request;
use App\Models\Banking\MarketingAccountBill;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request as HttpRequest;
class MarketingAccountBills extends Controller
{
/**
* 支持的营销账户平台列表
*/
public const PLATFORMS = [
'douyin' => 'platform_douyin',
'taobao-alipay' => 'platform_taobao-alipay',
'taobao-wechat' => 'platform_taobao-wechat',
'pinduoduo' => 'platform_pinduoduo',
'jd-wallet' => 'platform_jd-wallet',
'dewu' => 'platform_dewu',
'vipshop' => 'platform_vipshop',
];
public function __construct()
{
$this->middleware('permission:read-banking-accounts')->only('storeIndex', 'platformIndex');
$this->middleware('permission:create-banking-accounts')->only('create', 'store', 'import', 'template');
$this->middleware('permission:update-banking-accounts')->only('edit', 'update', 'export');
$this->middleware('permission:delete-banking-accounts')->only('destroy');
}
/**
* 平台选择页
*/
public function storeIndex()
{
return view('banking.marketing_account_bills.store-index', [
'platforms' => self::PLATFORMS,
]);
}
/**
* 单平台账单列表
*/
public function platformIndex(string $platform)
{
return $this->index($platform);
}
/**
* 创建表单
*/
public function create(string $platform)
{
$platform = $this->resolvePlatform($platform);
return view('banking.marketing_account_bills.form', [
'platform' => $platform,
'account' => new MarketingAccountBill([
'company_id' => company_id(),
'bill_date' => now()->toDateString(),
]),
]);
}
/**
* 下载导入模板
*/
public function template(string $platform)
{
$this->resolvePlatform($platform);
return response()->streamDownload(function () {
$csv = fopen('php://output', 'w');
fputs($csv, chr(0xEF) . chr(0xBB) . chr(0xBF)); // BOM for Excel
fputcsv($csv, [
'店铺名称', '账单日期', '总存入金额', '总转入金额', '总转出金额',
'非赠款消耗金额', '赠款消耗金额', '消返红包消耗金额', '立减红包消耗金额',
'共享钱包消耗金额', '共享赠款消耗金额', '当日资金总金额', '期末总余额',
'赠款余额', '非赠款余额', '备注',
]);
fclose($csv);
}, "marketing_account_bills_{$platform}_template.csv", [
'Content-Type' => 'text/csv; charset=UTF-8',
]);
}
/**
* 批量导出
*/
public function export(HttpRequest $request, string $platform)
{
$this->resolvePlatform($platform);
$ids = $request->validate([
'ids' => ['nullable', 'array', 'max:5000'],
'ids.*' => ['integer'],
])['ids'] ?? [];
$query = MarketingAccountBill::platform($platform)->oldest('id');
if (!empty($ids)) {
$query->whereKey($ids);
}
$bills = $query->get();
return response()->streamDownload(function () use ($bills) {
$csv = fopen('php://output', 'w');
fputs($csv, chr(0xEF) . chr(0xBB) . chr(0xBF));
fputcsv($csv, [
'店铺名称', '账单日期', '总存入金额', '总转入金额', '总转出金额',
'非赠款消耗金额', '赠款消耗金额', '消返红包消耗金额', '立减红包消耗金额',
'共享钱包消耗金额', '共享赠款消耗金额', '当日资金总金额', '期末总余额',
'赠款余额', '非赠款余额', '备注',
]);
foreach ($bills as $bill) {
fputcsv($csv, [
$bill->store_name,
$bill->bill_date->toDateString(),
$bill->total_deposit,
$bill->total_transfer_in,
$bill->total_transfer_out,
$bill->non_grant_consumption,
$bill->grant_consumption,
$bill->rebate_redpack_consumption,
$bill->discount_redpack_consumption,
$bill->shared_wallet_consumption,
$bill->shared_grant_consumption,
$bill->daily_total_amount,
$bill->period_end_balance,
$bill->grant_balance,
$bill->non_grant_balance,
$bill->notes,
]);
}
fclose($csv);
}, 'marketing_account_bills_export.csv', [
'Content-Type' => 'text/csv; charset=UTF-8',
]);
}
/**
* 批量导入
*/
public function import(HttpRequest $request, string $platform): RedirectResponse
{
$this->resolvePlatform($platform);
$validated = $request->validate([
'import' => ['required', 'file', 'mimes:xlsx,xls,csv', 'max:5120'],
], [
'import.required' => trans('marketing_account_bills.import_required'),
'import.mimes' => trans('marketing_account_bills.import_file_type'),
'import.max' => trans('marketing_account_bills.import_file_size'),
]);
$file = $validated['import'];
$handle = fopen($file->getRealPath(), 'r');
// Skip BOM
$bom = fread($handle, 3);
if ($bom !== chr(0xEF) . chr(0xBB) . chr(0xBF)) {
rewind($handle);
}
// Skip header
fgetcsv($handle);
$count = 0;
$companyId = company_id();
$userId = user()->id;
while (($row = fgetcsv($handle)) !== false) {
if (empty($row[0]) && empty($row[1])) {
continue;
}
MarketingAccountBill::create([
'company_id' => $companyId,
'platform' => $platform,
'store_name' => $row[0] ?? '',
'bill_date' => $row[1] ?? now()->toDateString(),
'total_deposit' => (float) ($row[2] ?? 0),
'total_transfer_in' => (float) ($row[3] ?? 0),
'total_transfer_out' => (float) ($row[4] ?? 0),
'non_grant_consumption' => (float) ($row[5] ?? 0),
'grant_consumption' => (float) ($row[6] ?? 0),
'rebate_redpack_consumption' => (float) ($row[7] ?? 0),
'discount_redpack_consumption' => (float) ($row[8] ?? 0),
'shared_wallet_consumption' => (float) ($row[9] ?? 0),
'shared_grant_consumption' => (float) ($row[10] ?? 0),
'daily_total_amount' => (float) ($row[11] ?? 0),
'period_end_balance' => (float) ($row[12] ?? 0),
'grant_balance' => (float) ($row[13] ?? 0),
'non_grant_balance' => (float) ($row[14] ?? 0),
'notes' => $row[15] ?? null,
'created_from' => 'core::ui',
'created_by' => $userId,
]);
$count++;
}
fclose($handle);
flash(trans('marketing_account_bills.import_success', ['count' => $count]))->success();
return redirect()->route('marketing-account-bills.platform.index', $platform);
}
/**
* 存储新账单
*/
public function store(Request $request, string $platform): RedirectResponse
{
$this->resolvePlatform($platform);
$data = $request->validated();
MarketingAccountBill::create(array_merge($data, [
'company_id' => company_id(),
'platform' => $platform,
'created_from' => 'core::ui',
'created_by' => user()->id,
]));
flash(trans('messages.success.created', ['type' => $this->platformTitle($platform)]))->success();
return redirect()->route('marketing-account-bills.platform.index', $platform);
}
/**
* 编辑表单
*/
public function edit(string $platform, MarketingAccountBill $marketingAccountBill)
{
abort_unless($marketingAccountBill->platform === $this->resolvePlatform($platform), 404);
return view('banking.marketing_account_bills.form', [
'platform' => $marketingAccountBill->platform,
'account' => $marketingAccountBill,
]);
}
/**
* 更新账单
*/
public function update(Request $request, string $platform, MarketingAccountBill $marketingAccountBill): RedirectResponse
{
abort_unless($marketingAccountBill->platform === $this->resolvePlatform($platform), 404);
$marketingAccountBill->update($request->validated());
flash(trans('messages.success.updated', ['type' => $this->platformTitle($platform)]))->success();
return redirect()->route('marketing-account-bills.platform.index', $platform);
}
/**
* 删除账单
*/
public function destroy(string $platform, MarketingAccountBill $marketingAccountBill): RedirectResponse
{
abort_unless($marketingAccountBill->platform === $this->resolvePlatform($platform), 404);
$marketingAccountBill->delete();
flash(trans('messages.success.deleted', ['type' => $this->platformTitle($platform)]))->success();
return redirect()->route('marketing-account-bills.platform.index', $platform);
}
/**
* 列表页
*/
private function index(string $platform)
{
$platform = $this->resolvePlatform($platform);
return view('banking.marketing_account_bills.index', [
'platform' => $platform,
'accounts' => MarketingAccountBill::platform($platform)
->orderBy('bill_date', 'desc')
->orderBy('id', 'desc')
->paginate(setting('default.list_limit', 25)),
]);
}
/**
* 校验并返回有效平台标识
*/
private function resolvePlatform(string $platform): string
{
abort_unless(array_key_exists($platform, self::PLATFORMS), 404);
return $platform;
}
/**
* 获取平台中文标题
*/
private function platformTitle(string $platform): string
{
return trans('marketing_account_bills.' . $platform);
}
}

View File

@ -0,0 +1,226 @@
<?php
namespace App\Http\Controllers\Banking;
use App\Abstracts\Http\Controller;
use App\Http\Requests\Banking\MarketingPinduoduoBill as Request;
use App\Models\Banking\MarketingPinduoduoBill;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request as HttpRequest;
class MarketingPinduoduoBills extends Controller
{
public function __construct()
{
$this->middleware('permission:read-banking-accounts')->only('index');
$this->middleware('permission:create-banking-accounts')->only('create', 'store', 'import', 'template');
$this->middleware('permission:update-banking-accounts')->only('edit', 'update', 'export');
$this->middleware('permission:delete-banking-accounts')->only('destroy');
}
/**
* 列表页
*/
public function index()
{
return view('banking.marketing_pinduoduo_bills.index', [
'bills' => MarketingPinduoduoBill::orderBy('transaction_time', 'desc')
->orderBy('id', 'desc')
->paginate(setting('default.list_limit', 25)),
]);
}
/**
* 创建表单
*/
public function create()
{
return view('banking.marketing_pinduoduo_bills.form', [
'bill' => new MarketingPinduoduoBill([
'company_id' => company_id(),
'transaction_time' => now(),
]),
]);
}
/**
* 下载导入模板
*/
public function template()
{
return response()->streamDownload(function () {
$csv = fopen('php://output', 'w');
fputs($csv, chr(0xEF) . chr(0xBB) . chr(0xBF));
fputcsv($csv, [
'店铺名称', '时间', '资金类型', '流水类型',
'交易金额', '余额', '交易摘要',
]);
fclose($csv);
}, 'marketing_pinduoduo_bills_template.csv', [
'Content-Type' => 'text/csv; charset=UTF-8',
]);
}
/**
* 批量导出
*/
public function export(HttpRequest $request)
{
$ids = $request->validate([
'ids' => ['nullable', 'array', 'max:5000'],
'ids.*' => ['integer'],
])['ids'] ?? [];
$query = MarketingPinduoduoBill::oldest('id');
if (!empty($ids)) {
$query->whereKey($ids);
}
$bills = $query->get();
return response()->streamDownload(function () use ($bills) {
$csv = fopen('php://output', 'w');
fputs($csv, chr(0xEF) . chr(0xBB) . chr(0xBF));
fputcsv($csv, [
'店铺名称', '时间', '资金类型', '流水类型',
'交易金额', '余额', '交易摘要',
]);
foreach ($bills as $bill) {
fputcsv($csv, [
$bill->store_name,
$bill->transaction_time?->format('Y-m-d H:i:s'),
$bill->fund_type,
$bill->flow_type,
$bill->transaction_amount,
$bill->balance,
$bill->transaction_summary,
]);
}
fclose($csv);
}, 'marketing_pinduoduo_bills_export.csv', [
'Content-Type' => 'text/csv; charset=UTF-8',
]);
}
/**
* 批量导入
*/
public function import(HttpRequest $request): RedirectResponse
{
$validated = $request->validate([
'import' => ['required', 'file', 'mimes:xlsx,xls,csv', 'max:5120'],
], [
'import.required' => trans('marketing_pinduoduo_bills.import_required'),
'import.mimes' => trans('marketing_pinduoduo_bills.import_file_type'),
'import.max' => trans('marketing_pinduoduo_bills.import_file_size'),
]);
$file = $validated['import'];
$handle = fopen($file->getRealPath(), 'r');
$bom = fread($handle, 3);
if ($bom !== chr(0xEF) . chr(0xBB) . chr(0xBF)) {
rewind($handle);
}
fgetcsv($handle);
$count = 0;
$companyId = company_id();
$userId = user()->id;
while (($row = fgetcsv($handle)) !== false) {
if (empty($row[0]) && empty($row[1])) {
continue;
}
$transactionTime = null;
if (!empty($row[1])) {
try {
$transactionTime = \Carbon\Carbon::parse($row[1]);
} catch (\Exception $e) {
$transactionTime = null;
}
}
MarketingPinduoduoBill::create([
'company_id' => $companyId,
'store_name' => $row[0] ?? '',
'transaction_time' => $transactionTime,
'fund_type' => $row[2] ?? '',
'flow_type' => $row[3] ?? null,
'transaction_amount' => (float) ($row[4] ?? 0),
'balance' => (float) ($row[5] ?? 0),
'transaction_summary' => $row[6] ?? null,
'created_from' => 'core::ui',
'created_by' => $userId,
]);
$count++;
}
fclose($handle);
flash(trans('marketing_pinduoduo_bills.import_success', ['count' => $count]))->success();
return redirect()->route('marketing-pinduoduo-bills.index');
}
/**
* 存储新账单
*/
public function store(Request $request): RedirectResponse
{
$data = $request->validated();
MarketingPinduoduoBill::create(array_merge($data, [
'company_id' => company_id(),
'created_from' => 'core::ui',
'created_by' => user()->id,
]));
flash(trans('messages.success.created', ['type' => trans('marketing_pinduoduo_bills.title')]))->success();
return redirect()->route('marketing-pinduoduo-bills.index');
}
/**
* 编辑表单
*/
public function edit(MarketingPinduoduoBill $marketingPinduoduoBill)
{
return view('banking.marketing_pinduoduo_bills.form', [
'bill' => $marketingPinduoduoBill,
]);
}
/**
* 更新账单
*/
public function update(Request $request, MarketingPinduoduoBill $marketingPinduoduoBill): RedirectResponse
{
$marketingPinduoduoBill->update($request->validated());
flash(trans('messages.success.updated', ['type' => trans('marketing_pinduoduo_bills.title')]))->success();
return redirect()->route('marketing-pinduoduo-bills.index');
}
/**
* 删除账单
*/
public function destroy(MarketingPinduoduoBill $marketingPinduoduoBill): RedirectResponse
{
$marketingPinduoduoBill->delete();
flash(trans('messages.success.deleted', ['type' => trans('marketing_pinduoduo_bills.title')]))->success();
return redirect()->route('marketing-pinduoduo-bills.index');
}
}

View File

@ -0,0 +1,232 @@
<?php
namespace App\Http\Controllers\Banking;
use App\Abstracts\Http\Controller;
use App\Http\Requests\Banking\MarketingTaobaoAlipayBill as Request;
use App\Models\Banking\MarketingTaobaoAlipayBill;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request as HttpRequest;
class MarketingTaobaoAlipayBills extends Controller
{
public function __construct()
{
$this->middleware('permission:read-banking-accounts')->only('index');
$this->middleware('permission:create-banking-accounts')->only('create', 'store', 'import', 'template');
$this->middleware('permission:update-banking-accounts')->only('edit', 'update', 'export');
$this->middleware('permission:delete-banking-accounts')->only('destroy');
}
/**
* 列表页
*/
public function index()
{
return view('banking.marketing_taobao_alipay_bills.index', [
'bills' => MarketingTaobaoAlipayBill::orderBy('transaction_date', 'desc')
->orderBy('id', 'desc')
->paginate(setting('default.list_limit', 25)),
]);
}
/**
* 创建表单
*/
public function create()
{
return view('banking.marketing_taobao_alipay_bills.form', [
'bill' => new MarketingTaobaoAlipayBill([
'company_id' => company_id(),
'transaction_date' => now()->toDateString(),
'bookkeeping_time' => now(),
]),
]);
}
/**
* 下载导入模板
*/
public function template()
{
return response()->streamDownload(function () {
$csv = fopen('php://output', 'w');
fputs($csv, chr(0xEF) . chr(0xBB) . chr(0xBF)); // BOM for Excel
fputcsv($csv, [
'店铺名称', '交易日期', '记账时间', '收支类型', '交易类型',
'操作金额(元)', '操作后余额(元)', '备注',
]);
fclose($csv);
}, 'marketing_taobao_alipay_bills_template.csv', [
'Content-Type' => 'text/csv; charset=UTF-8',
]);
}
/**
* 批量导出
*/
public function export(HttpRequest $request)
{
$ids = $request->validate([
'ids' => ['nullable', 'array', 'max:5000'],
'ids.*' => ['integer'],
])['ids'] ?? [];
$query = MarketingTaobaoAlipayBill::oldest('id');
if (!empty($ids)) {
$query->whereKey($ids);
}
$bills = $query->get();
return response()->streamDownload(function () use ($bills) {
$csv = fopen('php://output', 'w');
fputs($csv, chr(0xEF) . chr(0xBB) . chr(0xBF));
fputcsv($csv, [
'店铺名称', '交易日期', '记账时间', '收支类型', '交易类型',
'操作金额(元)', '操作后余额(元)', '备注',
]);
foreach ($bills as $bill) {
fputcsv($csv, [
$bill->store_name,
$bill->transaction_date->toDateString(),
$bill->bookkeeping_time?->format('Y-m-d H:i:s'),
$bill->income_expense_type,
$bill->transaction_type,
$bill->operation_amount,
$bill->balance_after,
$bill->notes,
]);
}
fclose($csv);
}, 'marketing_taobao_alipay_bills_export.csv', [
'Content-Type' => 'text/csv; charset=UTF-8',
]);
}
/**
* 批量导入
*/
public function import(HttpRequest $request): RedirectResponse
{
$validated = $request->validate([
'import' => ['required', 'file', 'mimes:xlsx,xls,csv', 'max:5120'],
], [
'import.required' => trans('marketing_taobao_alipay_bills.import_required'),
'import.mimes' => trans('marketing_taobao_alipay_bills.import_file_type'),
'import.max' => trans('marketing_taobao_alipay_bills.import_file_size'),
]);
$file = $validated['import'];
$handle = fopen($file->getRealPath(), 'r');
// Skip BOM
$bom = fread($handle, 3);
if ($bom !== chr(0xEF) . chr(0xBB) . chr(0xBF)) {
rewind($handle);
}
// Skip header
fgetcsv($handle);
$count = 0;
$companyId = company_id();
$userId = user()->id;
while (($row = fgetcsv($handle)) !== false) {
if (empty($row[0]) && empty($row[1])) {
continue;
}
// Parse bookkeeping time - handle various formats
$bookkeepingTime = null;
if (!empty($row[2])) {
try {
$bookkeepingTime = \Carbon\Carbon::parse($row[2]);
} catch (\Exception $e) {
$bookkeepingTime = null;
}
}
MarketingTaobaoAlipayBill::create([
'company_id' => $companyId,
'store_name' => $row[0] ?? '',
'transaction_date' => $row[1] ?? now()->toDateString(),
'bookkeeping_time' => $bookkeepingTime,
'income_expense_type' => $row[3] ?? '支出',
'transaction_type' => $row[4] ?? null,
'operation_amount' => (float) ($row[5] ?? 0),
'balance_after' => (float) ($row[6] ?? 0),
'notes' => $row[7] ?? null,
'created_from' => 'core::ui',
'created_by' => $userId,
]);
$count++;
}
fclose($handle);
flash(trans('marketing_taobao_alipay_bills.import_success', ['count' => $count]))->success();
return redirect()->route('marketing-taobao-alipay-bills.index');
}
/**
* 存储新账单
*/
public function store(Request $request): RedirectResponse
{
$data = $request->validated();
MarketingTaobaoAlipayBill::create(array_merge($data, [
'company_id' => company_id(),
'created_from' => 'core::ui',
'created_by' => user()->id,
]));
flash(trans('messages.success.created', ['type' => trans('marketing_taobao_alipay_bills.title')]))->success();
return redirect()->route('marketing-taobao-alipay-bills.index');
}
/**
* 编辑表单
*/
public function edit(MarketingTaobaoAlipayBill $marketingTaobaoAlipayBill)
{
return view('banking.marketing_taobao_alipay_bills.form', [
'bill' => $marketingTaobaoAlipayBill,
]);
}
/**
* 更新账单
*/
public function update(Request $request, MarketingTaobaoAlipayBill $marketingTaobaoAlipayBill): RedirectResponse
{
$marketingTaobaoAlipayBill->update($request->validated());
flash(trans('messages.success.updated', ['type' => trans('marketing_taobao_alipay_bills.title')]))->success();
return redirect()->route('marketing-taobao-alipay-bills.index');
}
/**
* 删除账单
*/
public function destroy(MarketingTaobaoAlipayBill $marketingTaobaoAlipayBill): RedirectResponse
{
$marketingTaobaoAlipayBill->delete();
flash(trans('messages.success.deleted', ['type' => trans('marketing_taobao_alipay_bills.title')]))->success();
return redirect()->route('marketing-taobao-alipay-bills.index');
}
}

View File

@ -2,11 +2,22 @@
namespace App\Http\Controllers\Business;
use App\Models\Banking\MarketingAccountBill;
use App\Models\Banking\MarketingTaobaoAlipayBill;
use App\Models\Banking\MarketingPinduoduoBill;
use App\Utilities\SidebarPermissions;
use Illuminate\Routing\Controller;
class Pages extends Controller
{
/**
* 支持的营销账户平台列表
*/
protected const MARKETING_PLATFORMS = [
'taobao-alipay', 'taobao-wechat', 'pinduoduo',
'jd-wallet', 'douyin', 'dewu', 'vipshop',
];
public function home()
{
$section = collect(SidebarPermissions::navigationFor(user()))->first();
@ -75,14 +86,69 @@ class Pages extends Controller
abort_if(empty($activeTab), 404);
$content = $this->resolveContent($page, $tab);
return view('business.pages.show', [
'section' => $sectionConfig,
'page' => $pageConfig,
'tabs' => $tabs,
'activeTab' => $activeTab,
'content' => $content,
]);
}
/**
* 根据页面和标签加载对应的业务内容数据
*/
protected function resolveContent(string $page, string $tab): ?array
{
return match ($page) {
'marketing-account-bills' => $this->contentForMarketingAccountBills($tab),
default => null,
};
}
/**
* 营销账户账单 - 加载指定平台的数据
* 淘系支付宝、拼多多使用独立的流水账单表,其他平台使用通用汇总账单表
*/
protected function contentForMarketingAccountBills(string $tab): ?array
{
if (!in_array($tab, self::MARKETING_PLATFORMS, true)) {
return null;
}
// 淘系支付宝 - 使用独立的流水账单表(8字段)
if ($tab === 'taobao-alipay') {
return [
'view' => 'banking.marketing_taobao_alipay_bills.partials.embedded-table',
'bills' => MarketingTaobaoAlipayBill::orderBy('transaction_date', 'desc')
->orderBy('id', 'desc')
->paginate(setting('default.list_limit', 25)),
];
}
// 拼多多 - 使用独立的流水账单表(7字段)
if ($tab === 'pinduoduo') {
return [
'view' => 'banking.marketing_pinduoduo_bills.partials.embedded-table',
'bills' => MarketingPinduoduoBill::orderBy('transaction_time', 'desc')
->orderBy('id', 'desc')
->paginate(setting('default.list_limit', 25)),
];
}
// 其他平台 - 使用通用汇总账单表(15字段)
return [
'view' => 'banking.marketing_account_bills.partials.embedded-table',
'platform' => $tab,
'accounts' => MarketingAccountBill::platform($tab)
->orderBy('bill_date', 'desc')
->orderBy('id', 'desc')
->paginate(setting('default.list_limit', 25)),
];
}
protected function redirectToPage(array $section, array $page)
{
$parameters = [

View File

@ -0,0 +1,35 @@
<?php
namespace App\Http\Requests\Banking;
use App\Abstracts\Http\FormRequest;
class MarketingAccountBill extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'store_name' => ['required', 'string', 'max:191'],
'bill_date' => ['required', 'date'],
'total_deposit' => ['nullable', 'numeric', 'min:0'],
'total_transfer_in' => ['nullable', 'numeric', 'min:0'],
'total_transfer_out' => ['nullable', 'numeric', 'min:0'],
'non_grant_consumption' => ['nullable', 'numeric', 'min:0'],
'grant_consumption' => ['nullable', 'numeric', 'min:0'],
'rebate_redpack_consumption' => ['nullable', 'numeric', 'min:0'],
'discount_redpack_consumption' => ['nullable', 'numeric', 'min:0'],
'shared_wallet_consumption' => ['nullable', 'numeric', 'min:0'],
'shared_grant_consumption' => ['nullable', 'numeric', 'min:0'],
'daily_total_amount' => ['nullable', 'numeric', 'min:0'],
'period_end_balance' => ['nullable', 'numeric', 'min:0'],
'grant_balance' => ['nullable', 'numeric', 'min:0'],
'non_grant_balance' => ['nullable', 'numeric', 'min:0'],
'notes' => ['nullable', 'string', 'max:500'],
];
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace App\Http\Requests\Banking;
use App\Abstracts\Http\FormRequest;
class MarketingPinduoduoBill extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'store_name' => ['required', 'string', 'max:191'],
'transaction_time' => ['required', 'date'],
'fund_type' => ['required', 'string', 'max:50'],
'flow_type' => ['nullable', 'string', 'max:100'],
'transaction_amount' => ['required', 'numeric'],
'balance' => ['nullable', 'numeric'],
'transaction_summary' => ['nullable', 'string', 'max:1000'],
];
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace App\Http\Requests\Banking;
use App\Abstracts\Http\FormRequest;
class MarketingTaobaoAlipayBill extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'store_name' => ['required', 'string', 'max:191'],
'transaction_date' => ['required', 'date'],
'bookkeeping_time' => ['nullable', 'date'],
'income_expense_type' => ['required', 'string', 'max:50'],
'transaction_type' => ['nullable', 'string', 'max:100'],
'operation_amount' => ['required', 'numeric'],
'balance_after' => ['nullable', 'numeric'],
'notes' => ['nullable', 'string', 'max:500'],
];
}
}

View File

@ -0,0 +1,56 @@
<?php
namespace App\Models\Banking;
use App\Abstracts\Model;
class MarketingAccountBill extends Model
{
protected $table = 'marketing_account_bills';
protected $fillable = [
'company_id',
'platform',
'store_name',
'bill_date',
'total_deposit',
'total_transfer_in',
'total_transfer_out',
'non_grant_consumption',
'grant_consumption',
'rebate_redpack_consumption',
'discount_redpack_consumption',
'shared_wallet_consumption',
'shared_grant_consumption',
'daily_total_amount',
'period_end_balance',
'grant_balance',
'non_grant_balance',
'notes',
'created_from',
'created_by',
];
protected $casts = [
'bill_date' => 'date',
'total_deposit' => 'double',
'total_transfer_in' => 'double',
'total_transfer_out' => 'double',
'non_grant_consumption' => 'double',
'grant_consumption' => 'double',
'rebate_redpack_consumption' => 'double',
'discount_redpack_consumption' => 'double',
'shared_wallet_consumption' => 'double',
'shared_grant_consumption' => 'double',
'daily_total_amount' => 'double',
'period_end_balance' => 'double',
'grant_balance' => 'double',
'non_grant_balance' => 'double',
'deleted_at' => 'datetime',
];
public function scopePlatform($query, string $platform)
{
return $query->where('platform', $platform);
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace App\Models\Banking;
use App\Abstracts\Model;
class MarketingPinduoduoBill extends Model
{
protected $table = 'marketing_pinduoduo_bills';
protected $fillable = [
'company_id',
'store_name',
'transaction_time',
'fund_type',
'flow_type',
'transaction_amount',
'balance',
'transaction_summary',
'created_from',
'created_by',
];
protected $casts = [
'transaction_time' => 'datetime',
'transaction_amount' => 'double',
'balance' => 'double',
'deleted_at' => 'datetime',
];
}

View File

@ -0,0 +1,32 @@
<?php
namespace App\Models\Banking;
use App\Abstracts\Model;
class MarketingTaobaoAlipayBill extends Model
{
protected $table = 'marketing_taobao_alipay_bills';
protected $fillable = [
'company_id',
'store_name',
'transaction_date',
'bookkeeping_time',
'income_expense_type',
'transaction_type',
'operation_amount',
'balance_after',
'notes',
'created_from',
'created_by',
];
protected $casts = [
'transaction_date' => 'date',
'bookkeeping_time' => 'datetime',
'operation_amount' => 'double',
'balance_after' => 'double',
'deleted_at' => 'datetime',
];
}

View File

@ -11,10 +11,7 @@ class Console
{
public static function run($string, $timeout = 0)
{
// PHP_BINARY is empty under some web SAPIs (including Apache mod_php).
// PHP_BINDIR still points to the directory containing the active PHP binary.
$phpBinary = PHP_BINARY ?: PHP_BINDIR . DIRECTORY_SEPARATOR . 'php';
$command = sprintf('%s %s %s', escapeshellarg($phpBinary), Application::artisanBinary(), $string);
$command = sprintf('%s %s %s', escapeshellarg(PHP_BINARY), Application::artisanBinary(), $string);
logger('Console command:: ' . $command);

View File

@ -4,8 +4,6 @@ namespace App\Utilities;
use App\Jobs\Auth\CreateUser;
use App\Jobs\Common\CreateCompany;
use App\Models\Auth\Permission;
use App\Models\Auth\Role;
use App\Utilities\Console;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Config;
@ -246,31 +244,17 @@ class Installer
'locale' => $locale,
'enabled' => '1',
]));
// This application uses one local ledger, so no setup wizard is needed.
setting()->set('wizard.completed', '1');
setting()->save();
}
public static function createUser($email, $password, $locale)
{
$role = Role::where('name', SystemAccess::SUPER_ADMIN)->firstOrFail();
$permission = Permission::firstOrCreate(
['name' => 'read-admin-panel'],
['display_name' => 'Read Admin Panel']
);
if (! $role->hasPermission($permission->name)) {
$role->attachPermission($permission);
}
dispatch_sync(new CreateUser([
'name' => '',
'email' => $email,
'password' => $password,
'locale' => $locale,
'companies' => ['1'],
'roles' => [$role->id],
'roles' => ['1'],
'enabled' => '1',
]));
}

View File

@ -2,10 +2,6 @@
use Illuminate\Support\Str;
$mysqlSslCaOption = class_exists(\Pdo\Mysql::class)
? \Pdo\Mysql::ATTR_SSL_CA
: \PDO::MYSQL_ATTR_SSL_CA;
return [
/*
@ -81,7 +77,7 @@ return [
'NO_ENGINE_SUBSTITUTION',
],
'options' => extension_loaded('pdo_mysql') ? array_filter([
$mysqlSslCaOption => env('MYSQL_ATTR_SSL_CA'),
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
]) : [],
],
@ -119,7 +115,7 @@ return [
'NO_ENGINE_SUBSTITUTION',
],
'options' => extension_loaded('pdo_mysql') ? array_filter([
$mysqlSslCaOption => env('MYSQL_ATTR_SSL_CA'),
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
]) : [],
],

View File

@ -0,0 +1,45 @@
<?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::create('marketing_account_bills', function (Blueprint $table) {
$table->increments('id');
$table->unsignedInteger('company_id');
$table->string('platform', 50)->comment('平台标识');
$table->string('store_name')->comment('店铺名称');
$table->date('bill_date')->comment('账单日期');
$table->decimal('total_deposit', 15, 2)->default(0)->comment('总存入金额');
$table->decimal('total_transfer_in', 15, 2)->default(0)->comment('总转入金额');
$table->decimal('total_transfer_out', 15, 2)->default(0)->comment('总转出金额');
$table->decimal('non_grant_consumption', 15, 2)->default(0)->comment('非赠款消耗金额');
$table->decimal('grant_consumption', 15, 2)->default(0)->comment('赠款消耗金额');
$table->decimal('rebate_redpack_consumption', 15, 2)->default(0)->comment('消返红包消耗金额');
$table->decimal('discount_redpack_consumption', 15, 2)->default(0)->comment('立减红包消耗金额');
$table->decimal('shared_wallet_consumption', 15, 2)->default(0)->comment('共享钱包消耗金额');
$table->decimal('shared_grant_consumption', 15, 2)->default(0)->comment('共享赠款消耗金额');
$table->decimal('daily_total_amount', 15, 2)->default(0)->comment('当日资金总金额');
$table->decimal('period_end_balance', 15, 2)->default(0)->comment('期末总余额');
$table->decimal('grant_balance', 15, 2)->default(0)->comment('赠款余额');
$table->decimal('non_grant_balance', 15, 2)->default(0)->comment('非赠款余额');
$table->text('notes')->nullable()->comment('备注');
$table->string('created_from', 50)->nullable();
$table->unsignedInteger('created_by')->nullable();
$table->timestamps();
$table->softDeletes();
$table->index(['company_id', 'platform']);
$table->index('bill_date');
});
}
public function down(): void
{
Schema::dropIfExists('marketing_account_bills');
}
};

View File

@ -0,0 +1,37 @@
<?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::create('marketing_taobao_alipay_bills', function (Blueprint $table) {
$table->increments('id');
$table->unsignedInteger('company_id');
$table->string('store_name')->comment('店铺名称');
$table->date('transaction_date')->comment('交易日期');
$table->datetime('bookkeeping_time')->nullable()->comment('记账时间');
$table->string('income_expense_type', 50)->comment('收支类型');
$table->string('transaction_type', 100)->nullable()->comment('交易类型');
$table->decimal('operation_amount', 15, 2)->default(0)->comment('操作金额(元)');
$table->decimal('balance_after', 15, 2)->default(0)->comment('操作后余额(元)');
$table->text('notes')->nullable()->comment('备注');
$table->string('created_from', 50)->nullable();
$table->unsignedInteger('created_by')->nullable();
$table->timestamps();
$table->softDeletes();
$table->index(['company_id', 'store_name']);
$table->index('transaction_date');
$table->index('bookkeeping_time');
});
}
public function down(): void
{
Schema::dropIfExists('marketing_taobao_alipay_bills');
}
};

View File

@ -0,0 +1,35 @@
<?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::create('marketing_pinduoduo_bills', function (Blueprint $table) {
$table->increments('id');
$table->unsignedInteger('company_id');
$table->string('store_name')->comment('店铺名称');
$table->datetime('transaction_time')->comment('时间');
$table->string('fund_type', 50)->comment('资金类型');
$table->string('flow_type', 100)->nullable()->comment('流水类型');
$table->decimal('transaction_amount', 15, 2)->default(0)->comment('交易金额');
$table->decimal('balance', 15, 2)->default(0)->comment('余额');
$table->text('transaction_summary')->nullable()->comment('交易摘要');
$table->string('created_from', 50)->nullable();
$table->unsignedInteger('created_by')->nullable();
$table->timestamps();
$table->softDeletes();
$table->index(['company_id', 'store_name']);
$table->index('transaction_time');
});
}
public function down(): void
{
Schema::dropIfExists('marketing_pinduoduo_bills');
}
};

View File

@ -26,7 +26,7 @@ class Permissions extends Seeder
private function create()
{
$rows = [
'super_admin' => [
'admin' => [
'admin-panel' => 'r',
'api' => 'r',
'auth-profile' => 'r,u',
@ -80,7 +80,7 @@ class Permissions extends Seeder
'widgets-profit-loss' => 'r',
'widgets-receivables' => 'r',
],
'admin' => [
'manager' => [
'admin-panel' => 'r',
'auth-profile' => 'r,u',
'banking-accounts' => 'c,r,u,d',

100
deploy/Dockerfile.local Normal file
View File

@ -0,0 +1,100 @@
# 本地部署用 Dockerfile
# 基于 deploy/Dockerfile,额外安装 composer 依赖与前端构建产物
# 由 docker-compose.local.yml 使用
FROM php:8.3-apache-bookworm
# ---------------- 系统依赖 + PHP 扩展 ----------------
ENV DEBIAN_FRONTEND=noninteractive
# 切换到清华 Debian 镜像(国内访问 deb.debian.org 不稳)
RUN sed -i 's|http://deb.debian.org|https://mirrors.tuna.tsinghua.edu.cn|g' /etc/apt/sources.list.d/debian.sources 2>/dev/null \
|| sed -i 's|http://deb.debian.org|https://mirrors.tuna.tsinghua.edu.cn|g' /etc/apt/sources.list 2>/dev/null \
|| true
# 强制非交互式 dpkg 配置(避免 media-types 等包 conffile 提示卡住)
RUN echo 'Dpkg::Options {"--force-confnew"; "--force-confdef";};' > /etc/apt/apt.conf.d/99force-conf \
&& echo 'APT::Get::Assume-Yes "true";' >> /etc/apt/apt.conf.d/99force-conf
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
ca-certificates \
curl \
libcurl4-openssl-dev \
libfreetype6-dev \
libicu-dev \
libjpeg62-turbo-dev \
libonig-dev \
libpng-dev \
libxml2-dev \
libzip-dev \
unzip \
zip \
git \
python3 \
make \
g++ \
&& docker-php-ext-configure gd --with-freetype --with-jpeg \
&& docker-php-ext-install -j"$(nproc)" \
bcmath \
curl \
dom \
exif \
gd \
intl \
mbstring \
pdo_mysql \
simplexml \
xml \
zip \
&& a2enmod expires headers rewrite \
&& rm -rf /var/lib/apt/lists/*
# ---------------- Composer ----------------
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
# ---------------- Node.js (LTS) ----------------
COPY --from=node:20-bookworm-slim /usr/local/bin/node /usr/local/bin/node
COPY --from=node:20-bookworm-slim /usr/local/lib/node_modules /usr/local/lib/node_modules
RUN ln -s /usr/local/lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm \
&& ln -s /usr/local/lib/node_modules/npm/bin/npx-cli.js /usr/local/bin/npx
WORKDIR /var/www/html
# ---------------- 复制源码 ----------------
COPY . /var/www/html
COPY deploy/apache-vhost.conf /etc/apache2/sites-available/000-default.conf
COPY deploy/php.ini /usr/local/etc/php/conf.d/jiangling.ini
COPY deploy/entrypoint.sh /usr/local/bin/jiangling-entrypoint
# ---------------- 安装 PHP 依赖(生产模式,跳过 ide-helper 等脚本)----------------
RUN composer install --prefer-dist \
--no-interaction \
--no-scripts \
--no-progress \
--no-ansi \
--no-dev \
&& composer dump-autoload --no-scripts --no-dev --optimize
# ---------------- 构建前端资源 ----------------
# 先装依赖(python3/make/g++ 让 node-sass 能编译)
RUN npm install --no-audit --no-fund --legacy-peer-deps
# 提升 node 堆内存上限;dev 模式无压缩,避免 mix/webpack 生产构建 OOM
ENV NODE_OPTIONS="--max-old-space-size=2048"
RUN npm run dev
# ---------------- 目录与权限 ----------------
RUN chmod +x /usr/local/bin/jiangling-entrypoint \
&& mkdir -p \
storage/app/public \
storage/app/temp \
storage/app/uploads \
storage/framework/cache/data \
storage/framework/sessions \
storage/framework/views \
storage/logs \
bootstrap/cache \
&& chown -R www-data:www-data storage bootstrap/cache public
ENTRYPOINT ["jiangling-entrypoint"]
CMD ["apache2-foreground"]

60
deploy/local-init.sh Normal file
View File

@ -0,0 +1,60 @@
#!/bin/bash
# =====================================================
# CaiWuXiTong 本地部署一键初始化脚本
# 用法:bash deploy/local-init.sh
# 前提:Docker Desktop 已启动
# =====================================================
set -e
cd "$(dirname "$0")/.."
echo "==> 工作目录: $(pwd)"
# 1. 构建并启动服务
echo ""
echo "==> [1/4] 构建镜像并启动容器(首次需要 5-15 分钟下载 PHP/MySQL 镜像并安装依赖)..."
docker compose -f docker-compose.local.yml up -d --build
# 2. 等待 MySQL 就绪
echo ""
echo "==> [2/4] 等待 MySQL 就绪..."
for i in $(seq 1 60); do
if docker compose -f docker-compose.local.yml exec -T mysql \
mysqladmin ping -h 127.0.0.1 -u akaunting -pakaunting123 >/dev/null 2>&1; then
echo " MySQL 已就绪。"
break
fi
echo " 等待中... ($i/60)"
sleep 2
done
# 3. 初始化数据库与管理员账号(幂等:已安装会自动跳过)
echo ""
echo "==> [3/4] 执行 php artisan install 初始化数据库与管理员..."
docker compose -f docker-compose.local.yml exec -T app \
php artisan install --no-interaction \
--db-host=mysql \
--db-port=3306 \
--db-name=akaunting \
--db-username=akaunting \
--db-password=akaunting123 \
--company-name="本地测试公司" \
--company-email=admin@company.com \
--admin-email=admin@company.com \
--admin-password=admin123 \
--locale=zh-CN \
|| echo " (install 已执行过则忽略此错误)"
# 4. 最终验证
echo ""
echo "==> [4/4] 验证访问..."
sleep 3
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:18088/auth/login || echo "000")
echo " http://127.0.0.1:18088/auth/login -> HTTP $HTTP_CODE"
echo ""
echo "====================================================="
echo "部署完成!"
echo " 访问地址: http://127.0.0.1:18088"
echo " 登录账号: admin@company.com"
echo " 登录密码: admin123"
echo "====================================================="

76
docker-compose.local.yml Normal file
View File

@ -0,0 +1,76 @@
# 本地开发/部署用 Compose 文件
# - 基于 compose.jiangling.yml 改造
# - 新增 mysql 服务(原文件依赖宿主机 MySQL)
# - 改用 bridge 网络替代 host 模式,便于本机访问
#
# 启动: docker compose -f docker-compose.local.yml up -d --build
# 停止: docker compose -f docker-compose.local.yml down
# 日志: docker compose -f docker-compose.local.yml logs -f app
# 初始化:docker compose -f docker-compose.local.yml exec app php artisan install --db-host=mysql --db-name=akaunting --db-username=akaunting --db-password=akaunting123 --admin-email=admin@company.com --admin-password=admin123 --locale=zh-CN
services:
app:
build:
context: .
dockerfile: deploy/Dockerfile.local
image: jiangling_akaunting:local
container_name: jiangling_app
restart: unless-stopped
depends_on:
mysql:
condition: service_healthy
ports:
- "18088:18088"
volumes:
- ./runtime/.env:/var/www/html/.env
- jiangling_storage:/var/www/html/storage
- ./app:/var/www/html/app
- ./config:/var/www/html/config
- ./database:/var/www/html/database
- ./resources:/var/www/html/resources
- ./routes:/var/www/html/routes
networks:
- jiangling_net
healthcheck:
test: ["CMD", "curl", "-fsS", "-o", "/dev/null", "http://127.0.0.1:18088/auth/login"]
interval: 15s
timeout: 5s
retries: 10
start_period: 60s
mysql:
image: mysql:8.0
container_name: jiangling_mysql
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: rootpass
MYSQL_DATABASE: akaunting
MYSQL_USER: akaunting
MYSQL_PASSWORD: akaunting123
command:
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_unicode_ci
- --default-authentication-plugin=mysql_native_password
volumes:
- jiangling_mysql:/var/lib/mysql
ports:
- "13306:3306"
networks:
- jiangling_net
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-u", "root", "-p$$MYSQL_ROOT_PASSWORD"]
interval: 5s
timeout: 5s
retries: 20
start_period: 30s
volumes:
jiangling_storage:
name: jiangling_storage
jiangling_mysql:
name: jiangling_mysql
networks:
jiangling_net:
name: jiangling_net
driver: bridge

View File

@ -0,0 +1,115 @@
<?php
namespace Modules\EcommerceFinance\Http\Controllers\Api;
use App\Abstracts\Http\ApiController;
use Modules\EcommerceFinance\Repositories\GoodsReconciliationRepository;
/**
* 货款对账 API
*
* 提供供应商应付-货款对账页面的全部数据接口:
* GET /api/ecommerce-finance/goods-reconciliations 列表(分页 + 筛选)
* GET /api/ecommerce-finance/goods-reconciliations/summary 统计卡片
* GET /api/ecommerce-finance/goods-reconciliations/options 筛选器选项
*/
class GoodsReconciliations extends ApiController
{
private GoodsReconciliationRepository $repo;
public function __construct()
{
$this->repo = new GoodsReconciliationRepository();
}
/**
* 货款对账列表(分页 + 筛选)
*/
public function index()
{
$filters = $this->buildFilters();
$page = max((int) request('page', 1), 1);
$limit = $this->limit();
$result = $this->repo->list($filters, $page, $limit);
return response()->json([
'data' => $result['data'],
'total' => $result['total'],
'page' => $page,
'per_page' => $limit,
'last_page' => (int) ceil($result['total'] / $limit),
]);
}
/**
* 汇总统计卡片
*/
public function summary()
{
$filters = $this->buildFilters();
$summary = $this->repo->summary($filters);
return response()->json($summary);
}
/**
* 筛选器选项(供应商/仓库下拉列表等)
*/
public function options()
{
$options = $this->repo->filterOptions();
return response()->json($options);
}
// ===================================================================
// Helpers
// ===================================================================
/**
* 从请求参数构建筛选条件
*/
private function buildFilters(): array
{
$filters = [];
// 记录类型
if ($type = request('record_type')) {
$filters['record_type'] = $type; // purchase | sale | all
}
// 文本筛选
$textFields = ['supplier', 'stock_no', 'purchase_no', 'product_code', 'express_no', 'warehouse'];
foreach ($textFields as $field) {
if ($val = request($field)) {
$filters[$field] = trim($val);
}
}
// 日期范围
$dateFields = ['send_time_from', 'send_time_to', 'create_time_from', 'create_time_to'];
foreach ($dateFields as $field) {
if ($val = request($field)) {
$filters[$field] = $val;
}
}
// 发出状态
if (($sent = request('sent_status')) && $sent !== 'all') {
$filters['sent_status'] = $sent; // sent | unsent
}
// 是否有差异
if (request('has_diff')) {
$filters['has_diff'] = true;
}
return $filters;
}
private function limit(): int
{
return min(max((int) request('limit', 25), 1), 100);
}
}

View File

@ -0,0 +1,196 @@
<?php
namespace Modules\EcommerceFinance\Http\Controllers\Api;
use App\Abstracts\Http\ApiController;
use Modules\EcommerceFinance\Repositories\SupplierPayableRepository;
/**
* 供应商应付 - 子功能页面 API
*
* 为 5 个子页面提供数据接口:
* 退货对账 : /return-reconciliations + /return-reconciliations/summary + /return-reconciliations/options
* 发票扣款 : /invoice-deductions + /invoice-deductions/summary + /invoice-deductions/options
* 赔偿明细 : /compensation-details + /compensation-details/summary + /compensation-details/options
* 借支明细 : /advance-details + /advance-details/summary + /advance-details/options
* 样品扣款 : /sample-deductions + /sample-deductions/summary + /sample-deductions/options
*/
class SupplierPayables extends ApiController
{
private SupplierPayableRepository $repo;
public function __construct()
{
$this->repo = new SupplierPayableRepository();
}
// ============================================================
// 退货对账
// ============================================================
public function returnReconciliationsIndex()
{
$filters = $this->filters('return');
$result = $this->repo->returnReconciliations($filters, $this->page(), $this->limit());
return response()->json([
'data' => $result['data'],
'total' => $result['total'],
'page' => $this->page(),
'per_page' => $this->limit(),
'last_page' => (int) ceil(max($result['total'], 0) / $this->limit()),
]);
}
public function returnReconciliationsSummary()
{
return response()->json($this->repo->returnReconciliationSummary($this->filters('return')));
}
public function returnReconciliationsOptions()
{
return response()->json($this->repo->filterOptionsForReturn());
}
// ============================================================
// 发票扣款
// ============================================================
public function invoiceDeductionsIndex()
{
$filters = $this->filters('invoice');
$result = $this->repo->invoiceDeductions($filters, $this->page(), $this->limit());
return response()->json([
'data' => $result['data'],
'total' => $result['total'],
'page' => $this->page(),
'per_page' => $this->limit(),
'last_page' => (int) ceil(max($result['total'], 0) / $this->limit()),
]);
}
public function invoiceDeductionsSummary()
{
return response()->json($this->repo->invoiceDeductionSummary($this->filters('invoice')));
}
public function invoiceDeductionsOptions()
{
return response()->json($this->repo->filterOptionsForInvoice());
}
// ============================================================
// 赔偿明细
// ============================================================
public function compensationDetailsIndex()
{
$filters = $this->filters('compensation');
$result = $this->repo->compensationDetails($filters, $this->page(), $this->limit());
return response()->json([
'data' => $result['data'],
'total' => $result['total'],
'page' => $this->page(),
'per_page' => $this->limit(),
'last_page' => (int) ceil(max($result['total'], 0) / $this->limit()),
]);
}
public function compensationDetailsSummary()
{
return response()->json($this->repo->compensationDetailSummary($this->filters('compensation')));
}
public function compensationDetailsOptions()
{
return response()->json($this->repo->filterOptionsForCompensation());
}
// ============================================================
// 借支明细
// ============================================================
public function advanceDetailsIndex()
{
$filters = $this->filters('advance');
$result = $this->repo->advanceDetails($filters, $this->page(), $this->limit());
return response()->json([
'data' => $result['data'],
'total' => $result['total'],
'page' => $this->page(),
'per_page' => $this->limit(),
'last_page' => (int) ceil(max($result['total'], 0) / $this->limit()),
]);
}
public function advanceDetailsSummary()
{
return response()->json($this->repo->advanceDetailSummary($this->filters('advance')));
}
public function advanceDetailsOptions()
{
return response()->json($this->repo->filterOptionsForAdvance());
}
// ============================================================
// 样品扣款
// ============================================================
public function sampleDeductionsIndex()
{
$filters = $this->filters('sample');
$result = $this->repo->sampleDeductions($filters, $this->page(), $this->limit());
return response()->json([
'data' => $result['data'],
'total' => $result['total'],
'page' => $this->page(),
'per_page' => $this->limit(),
'last_page' => (int) ceil(max($result['total'], 0) / $this->limit()),
]);
}
public function sampleDeductionsSummary()
{
return response()->json($this->repo->sampleDeductionSummary($this->filters('sample')));
}
public function sampleDeductionsOptions()
{
return response()->json($this->repo->filterOptionsForSample());
}
// ============================================================
// Helpers
// ============================================================
private function filters(string $type): array
{
$filters = [];
// 通用文本字段
$text = ['supplier', 'warehouse', 'sku_id', 'express_no', 'creator', 'department'];
foreach ($text as $field) {
if ($val = request($field)) {
$filters[$field] = trim($val);
}
}
// 日期
foreach (['date_from', 'date_to', 'month_from', 'month_to'] as $field) {
if ($val = request($field)) {
$filters[$field] = $val;
}
}
return $filters;
}
private function page(): int
{
return max((int) request('page', 1), 1);
}
private function limit(): int
{
return min(max((int) request('limit', 25), 1), 100);
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace Modules\EcommerceFinance\Models\JianSun;
/**
* 加权平均单价表
* pm_averageUnitPrice
*
* 按货号+供应商存储的加权平均单价(262条)。
* 用于计算"预估金额"。
*
* @property int $id
* @property string $number 货号
* @property string $supplier 供应商名称
* @property float $price 加权平均单价(页面表头第11列来源)
* @property string $name 商品名称
*/
class AverageUnitPrice extends JianSunModel
{
protected $table = 'pm_averageUnitPrice';
protected $fillable = ['number', 'supplier', 'price', 'name'];
protected $casts = [
'price' => 'decimal:4',
];
}

View File

@ -0,0 +1,27 @@
<?php
namespace Modules\EcommerceFinance\Models\JianSun;
use Illuminate\Database\Eloquent\Model;
/**
* Base Model for JianSun SQL Server database tables.
* All models extending this will automatically use the 'jiansun' connection.
*/
abstract class JianSunModel extends Model
{
/** @var string */
protected $connection = 'jiansun';
/** @var bool JianSun tables don't use Laravel timestamps */
public $timestamps = false;
/** @var string No prefix on JianSun tables */
protected $prefix = '';
/** @var string */
protected $primaryKey = 'id';
/** @var bool */
public $incrementing = true;
}

View File

@ -0,0 +1,45 @@
<?php
namespace Modules\EcommerceFinance\Models\JianSun;
/**
* 订单快递详情表(代发记录)
* jst_order_express_details
*
* 存储所有代发订单的快递信息(2,812,915条)。
* 页面中"代发记录"的快递名称和快递单号来源于此表。
*
* @property int $id
* @property string $so_id 订单编号
* @property string $logistic_id 快递单号(页面表头第18列)
* @property string $logistic_company_name 快递名称(页面表头第17列)
* @property string $logistic_status_name 物流状态
* @property string $warehouse_name 仓库名称
* @property string $send_time 发货时间(页面表头第3列)
* @property string $sign_time 签收时间
* @property float $weight 重量
*/
class JstOrderExpressDetails extends JianSunModel
{
protected $table = 'jst_order_express_details';
protected $fillable = [
'createdAt', 'updatedAt', 'inner_order_id', 'so_id', 'logistic_id',
'logistic_company_name', 'logistic_status_name', 'logistic_return_flag',
'order_status_name', 'first_question_type', 'first_question_time',
'aftersale_status_name', 'order_labels', 'remark', 'buyer_message',
'shop_name', 'platform_name', 'dis_co_name', 'warehouse_name',
'receiver_address', 'pay_time', 'first_oa_confirm_time',
'last_oa_confirm_time', 'send_time', 'fetch_time', 'sign_time',
'logistic_time', 'weight', 'f_weight', 'ship_time_by_pdh',
];
protected $casts = [
'send_time' => 'datetime',
'sign_time' => 'datetime',
'pay_time' => 'datetime',
'fetch_time' => 'datetime',
'weight' => 'decimal:4',
'f_weight' => 'decimal:4',
];
}

View File

@ -0,0 +1,31 @@
<?php
namespace Modules\EcommerceFinance\Models\JianSun;
/**
* 聚水潭供应商表
* jst_supplier
*
* 聚水潭ERP系统的供应商数据(2,075条)。
*
* @property int $Id
* @property int $supplier_id 供应商ID
* @property string $supplier_code 供应商编码
* @property string $name 供应商名称
* @property string $modified 修改时间
* @property string $remark 备注
* @property string $group 分组
* @property int $enabled 是否启用
*/
class JstSupplier extends JianSunModel
{
protected $table = 'jst_supplier';
protected $primaryKey = 'Id';
protected $fillable = [
'supplier_id', 'supplier_code', 'name', 'modified', 'remark',
'remark2', 'remark3', 'group', 'enabled', 'depositbank',
'bankacount', 'acountnumber',
];
}

View File

@ -0,0 +1,45 @@
<?php
namespace Modules\EcommerceFinance\Models\JianSun;
/**
* 采购入库单主表
* pm_purchaseIn
*
* 每次采购入库的汇总记录(52,219条)。
* 通过 id 关联 pm_purchaseInItem 获取明细。
*
* @property int $id
* @property string $hno 入库单号(页面表头第1列)
* @property string $whcode 仓库编码
* @property string $sourceno 来源单号
* @property string $title 标题
* @property string $scmname 供应商名称
* @property string $scmcode 供应商编码
* @property int $scmid 供应商ID
* @property int $incount 入库总数
* @property string $created 创建时间(页面表头第16列)
* @property string $memo 备注(页面表头第14列)
* @property string $expcode 快递编码
* @property string $expname 快递名称
*/
class PurchaseIn extends JianSunModel
{
protected $table = 'pm_purchaseIn';
protected $fillable = [
'whcode', 'hno', 'sourceno', 'title', 'scmname', 'scmcode', 'scmid',
'incount', 'created', 'memo', 'expcode', 'expname', 'postcode', 'postfee',
'creater', 'createrid', 'inwhdate', 'finishdate', 'procode', 'proname',
'skucode', 'skuname',
];
protected $casts = [
'incount' => 'integer',
'scmid' => 'integer',
'postfee' => 'decimal:2',
'created' => 'datetime',
'inwhdate' => 'datetime',
'finishdate' => 'datetime',
];
}

View File

@ -0,0 +1,39 @@
<?php
namespace Modules\EcommerceFinance\Models\JianSun;
/**
* 采购入库单明细表
* pm_purchaseInItem
*
* 每次采购入库的商品明细(223,382条)。
* 通过 hid 关联 pm_purchaseIn.id。
*
* @property int $id
* @property int $hid 关联 PurchaseIn.id
* @property string $skucode 商品编码(页面表头第6列)
* @property string $skuname 规格名称(页面表头第7列)
* @property string $skuoid 外部商品编码
* @property int $incount 入库数量(页面表头第8列)
* @property int $skucount SKU计数
* @property float $payment 单价
* @property string $created 创建时间
*/
class PurchaseInItem extends JianSunModel
{
protected $table = 'pm_purchaseInItem';
protected $fillable = [
'hid', 'skucode', 'skuname', 'skuoid', 'incount', 'skucount',
'payment', 'created', 'procode', 'proname', 'batchCode',
'inventoryType', 'productDate', 'expireDate', 'produceCode',
];
protected $casts = [
'hid' => 'integer',
'incount' => 'integer',
'skucount' => 'integer',
'payment' => 'decimal:2',
'created' => 'datetime',
];
}

View File

@ -0,0 +1,29 @@
<?php
namespace Modules\EcommerceFinance\Models\JianSun;
/**
* SKU信息管理表
* js_sku_info_manage
*
* 商品SKU管理信息(8,394条),包含货号、商品编码等。
*
* @property int $id
* @property string $sku_id 商品编码(页面表头第6列)
* @property string $sku 货号(页面表头第5列)
* @property string $spu SPU
* @property string $sku_manager SKU负责人
* @property string $operation_dept_manager 运营部门负责人
* @property string $designer 设计师
* @property string $designer_dept 设计部门
*/
class SkuInfoManage extends JianSunModel
{
protected $table = 'js_sku_info_manage';
protected $fillable = [
'sku_id', 'sku', 'spu', 'sku_manager',
'operation_dept_manager', 'sku_business_unit_manager',
'designer', 'designer_dept', 'create_time', 'update_time',
];
}

View File

@ -0,0 +1,40 @@
<?php
namespace Modules\EcommerceFinance\Models\JianSun;
/**
* 供应商基础信息表
* js_supplier
*
* 供应商主数据(1,479条),包含供应商编码、名称、公司等。
*
* @property int $Id
* @property string $scmcode 供应商编码
* @property string $scmname 供应商名称
* @property string $scmtype 供应商类型
* @property string $company 公司全称
* @property string $paycyclesgn 结算周期标记
* @property string $reconciliter 对账人
* @property string $buyer 采购员
* @property string $coopersgn 合作状态标记
* @property string $levelname 等级名称
* @property string $memo 备注
* @property string $enterpriseType 企业类型
* @property string $creditcode 统一社会信用代码
*/
class Supplier extends JianSunModel
{
protected $table = 'js_supplier';
protected $primaryKey = 'Id';
protected $fillable = [
'scmcode', 'scmname', 'scmtype', 'tradebody', 'company',
'paycyclesgn', 'accountbankno', 'corpmobile', 'tradebtime', 'tradeetime',
'coopersgn', 'cooperbtime', 'cooperetime', 'dsperid', 'dsper',
'reconciliter', 'levelname', 'buyerid', 'buyer', 'memo',
'guid', 'craftType', 'outline', 'capacity', 'freereason', 'area',
'stocker', 'region', 'sindou', 'follower', 'presc', 'strategy',
'enterpriseType', 'creditcode', 'handlePerson', 'legalPerson',
];
}

View File

@ -0,0 +1,49 @@
<?php
namespace Modules\EcommerceFinance\Models\JianSun;
/**
* 供应商采购结算明细表
* fys_supplierGoods_sttlement_purchase
*
* 采购入库结算明细,包含每个SKU的结算数量和价格。
* 是货款对账页面"入库记录"的核心数据源(228,039条)。
*
* @property int $Id
* @property string $created 创建时间
* @property int $stock 入库数量
* @property int $settlementStock 结算数量
* @property float $singlePrice 单价
* @property float $settlementPrice 结算价格
* @property string $priceType 价格类型
* @property string $ownername 货主
* @property string $scmname 供应商名称
* @property string $procode 货号
* @property string $whname 仓库名称
* @property string $skuoid 商品编码
* @property string $skuname 规格名称
* @property string $bizpo 业务采购单号
* @property string $spo 采购单号
* @property string $remark 备注
*/
class SupplierGoodsSttlementPurchase extends JianSunModel
{
protected $table = 'fys_supplierGoods_sttlement_purchase';
protected $primaryKey = 'Id';
protected $fillable = [
'created', 'stock', 'settlementStock', 'singlePrice', 'settlementPrice',
'priceType', 'ownername', 'scmname', 'procode', 'whname',
'skuoid', 'skuname', 'bizpo', 'spo', 'remark', 'createTime',
];
protected $casts = [
'stock' => 'integer',
'settlementStock' => 'integer',
'singlePrice' => 'decimal:2',
'settlementPrice' => 'decimal:2',
'created' => 'datetime',
'createTime' => 'datetime',
];
}

View File

@ -0,0 +1,36 @@
<?php
namespace Modules\EcommerceFinance\Models\JianSun;
/**
* 供应商付款明细表
* fys_supplierPaymentDetail
*
* @property int $Id
* @property string $created 创建时间
* @property string $month 月份
* @property string $date 日期
* @property string $scmname 供应商名称
* @property string $scmcode 供应商编码
* @property float $payment 付款金额
* @property string $reason 原因
* @property string $remark 备注
* @property int $billstatus 单据状态
*/
class SupplierPaymentDetail extends JianSunModel
{
protected $table = 'fys_supplierPaymentDetail';
protected $primaryKey = 'Id';
protected $fillable = [
'created', 'month', 'date', 'scmname', 'scmcode',
'payment', 'reason', 'remark', 'billstatus',
];
protected $casts = [
'payment' => 'decimal:2',
'created' => 'datetime',
'date' => 'date',
];
}

View File

@ -0,0 +1,85 @@
<?php
namespace Modules\EcommerceFinance\Models\JianSun;
/**
* 供应商付款汇总表
* fys_supplierPaymentSummary
*
* 核心对账数据源,每条记录代表一个供应商在某月份的应付汇总
*
* @property int $id
* @property string $company 公司
* @property string $supplier 供应商名称
* @property string $checker 对账人
* @property string $settleway 结算方式
* @property string $Month 对账月份
* @property float $ExpFieght 快递费
* @property float $bginMonthPay 月初应付
* @property float $unpriced 未定价
* @property float $advloan 预付
* @property float $loan 借款
* @property float $refund 退款
* @property float $procurement 采购
* @property float $invoiceDct 发票扣款
* @property float $boardDct 板材扣款
* @property float $specimenDct 样品扣款
* @property float $Totalindemnity 总赔偿
* @property float $other 其他
* @property float $supplySup 供应商补贴
* @property float $ShouldPay 应付金额
* @property float $factPay 实付金额
* @property float $guessPrice 预估金额
* @property float $toCorporate 到公账
* @property float $toPrivate 到私账
* @property string $remarks 备注
* @property string $reason 原因
* @property string $guid GUID
* @property int $isExamine 是否审核
* @property int $isPayment 是否付款
* @property string $scmcode 供应商编码
* @property int $supid 供应商ID
* @property string $createTime 创建时间
*/
class SupplierPaymentSummary extends JianSunModel
{
protected $table = 'fys_supplierPaymentSummary';
protected $fillable = [
'company', 'supplier', 'checker', 'settleway', 'Month',
'ExpFieght', 'bginMonthPay', 'unpriced', 'advloan', 'loan',
'refund', 'procurement', 'invoiceDct', 'boardDct', 'specimenDct',
'Totalindemnity', 'other', 'supplySup', 'ShouldPay', 'factPay',
'guessPrice', 'toCorporate', 'toPrivate', 'remarks', 'reason',
'mark', 'guid', 'isExamine', 'isPayment', 'Month',
'billstatus', 'opstatus', 'elstatu', 'taskid', 'supid',
'depositMoney', 'depositType', 'cashierbypaid', 'push', 'scmcode',
'billRemaining',
];
protected $casts = [
'ExpFieght' => 'decimal:2',
'bginMonthPay' => 'decimal:2',
'unpriced' => 'decimal:2',
'advloan' => 'decimal:2',
'loan' => 'decimal:2',
'refund' => 'decimal:2',
'procurement' => 'decimal:2',
'invoiceDct' => 'decimal:2',
'boardDct' => 'decimal:2',
'specimenDct' => 'decimal:2',
'Totalindemnity' => 'decimal:2',
'other' => 'decimal:2',
'supplySup' => 'decimal:2',
'ShouldPay' => 'decimal:2',
'factPay' => 'decimal:2',
'guessPrice' => 'decimal:2',
'toCorporate' => 'decimal:2',
'toPrivate' => 'decimal:2',
'depositMoney' => 'decimal:2',
'cashierbypaid' => 'decimal:2',
'billRemaining' => 'decimal:2',
'createTime' => 'datetime',
'modifiedTime' => 'datetime',
];
}

View File

@ -0,0 +1,43 @@
<?php
namespace Modules\EcommerceFinance\Models\JianSun;
/**
* 未发出订单明细表
* pm_unOrderDetails
*
* 记录已采购但尚未发出的商品明细。
*
* @property int $Id
* @property string $date 日期
* @property string $purchase 采购单号
* @property string $warehouse 仓库
* @property string $supplier 供应商(页面表头第4列)
* @property string $transactionNo 交易号
* @property string $distributionNo 配送号
* @property string $expressNo 快递单号(页面表头第18列)
* @property string $sku 货号
* @property string $barCode 条码
* @property string $skuName 规格名称
* @property string $distributionDate 配送日期
* @property string $shipDate 发货日期
* @property string $expressName 快递名称(页面表头第17列)
*/
class UnOrderDetails extends JianSunModel
{
protected $table = 'pm_unOrderDetails';
protected $primaryKey = 'Id';
protected $fillable = [
'date', 'purchase', 'warehouse', 'supplier', 'transactionNo',
'distributionNo', 'expressNo', 'sku', 'barCode', 'skuName',
'distributionDate', 'shipDate', 'expressName',
];
protected $casts = [
'date' => 'datetime',
'distributionDate' => 'datetime',
'shipDate' => 'datetime',
];
}

View File

@ -0,0 +1,370 @@
<?php
namespace Modules\EcommerceFinance\Repositories;
use Modules\EcommerceFinance\Services\JianSunConnection;
use PDO;
/**
* 货款对账数据仓库
*
* 数据来源:
* - 入库记录:fys_supplierGoods_sttlement_purchase(~228K)+ pm_purchaseIn + pm_averageUnitPrice
* - 代发记录:fys_supplierGoods_sttlement_sale(~2.8M)+ pm_averageUnitPrice
*
* 性能策略:入库/代发分开查询,各自执行分页,避免全量 UNION ALL。
*/
class GoodsReconciliationRepository
{
private PDO $pdo;
public function __construct()
{
$this->pdo = JianSunConnection::get();
}
// ===================================================================
// 汇总统计(轻量:只用 COUNT + SUM,不做复杂 JOIN)
// ===================================================================
public function summary(array $filters = []): array
{
$purchase = $this->purchaseSummary($filters);
$sale = $this->saleSummary($filters);
return [
'totalAmount' => round($purchase['totalAmount'] + $sale['totalAmount'], 2),
'totalGuessAmount' => round($purchase['totalGuessAmount'] + $sale['totalGuessAmount'], 2),
'sentCount' => $purchase['sentCount'] + $sale['sentCount'],
'unsentCount' => $purchase['unsentCount'] + $sale['unsentCount'],
'totalCount' => $purchase['totalCount'] + $sale['totalCount'],
];
}
private function purchaseSummary(array $filters): array
{
$where = $this->buildPurchaseWhere($filters);
$params = $this->buildPurchaseParams($filters);
// 按需 JOIN,减少不必要的全表扫描
$joinA = $joinU = '';
if (strpos($where, 'a.') !== false) {
$joinA = ' LEFT JOIN pm_averageUnitPrice a ON s.procode = a.number AND s.scmname = a.supplier';
}
if (strpos($where, 'u.') !== false) {
$joinU = ' LEFT JOIN pm_unOrderDetails u ON s.spo = u.purchase AND s.skuoid = u.sku';
}
$sql = "SELECT
COUNT(*) as totalCount,
ISNULL(SUM(s.settlementPrice), 0) as totalAmount,
0 as totalGuessAmount,
COUNT(*) as sentCount,
0 as unsentCount
FROM fys_supplierGoods_sttlement_purchase s
{$joinA}{$joinU}
WHERE 1=1 {$where}";
$stmt = $this->pdo->prepare($sql);
$stmt->execute($params);
$row = $stmt->fetch();
return $row ?: ['totalCount' => 0, 'totalAmount' => 0, 'totalGuessAmount' => 0, 'sentCount' => 0, 'unsentCount' => 0];
}
private function saleSummary(array $filters): array
{
$where = $this->buildSaleWhere($filters);
$params = $this->buildSaleParams($filters);
$joinA = '';
if (strpos($where, 'a.') !== false) {
$joinA = ' LEFT JOIN pm_averageUnitPrice a ON s.procode = a.number AND s.supplier = a.supplier';
}
$sql = "SELECT
COUNT(*) as totalCount,
ISNULL(SUM(s.settlementPrice), 0) as totalAmount,
0 as totalGuessAmount,
ISNULL(SUM(CASE WHEN s.expressNo IS NOT NULL AND s.expressNo != '' THEN 1 ELSE 0 END), 0) as sentCount,
ISNULL(SUM(CASE WHEN s.expressNo IS NULL OR s.expressNo = '' THEN 1 ELSE 0 END), 0) as unsentCount
FROM fys_supplierGoods_sttlement_sale s
{$joinA}
WHERE 1=1 {$where}";
$stmt = $this->pdo->prepare($sql);
$stmt->execute($params);
$row = $stmt->fetch();
return $row ?: ['totalCount' => 0, 'totalAmount' => 0, 'totalGuessAmount' => 0, 'sentCount' => 0, 'unsentCount' => 0];
}
// ===================================================================
// 列表查询(核心 — 分别查入库/代发再合并)
// ===================================================================
public function list(array $filters = [], int $page = 1, int $perPage = 25): array
{
$type = $filters['record_type'] ?? 'all';
// 分别查询(同一分页参数,各自内部分页)
$purchaseResult = null;
$saleResult = null;
$totalPurchase = 0;
$totalSale = 0;
if ($type === 'all' || $type === 'purchase') {
$where = $this->buildPurchaseWhere($filters);
$params = $this->buildPurchaseParams($filters);
$totalPurchase = $this->countPurchase($where, $params);
$purchaseResult = $this->fetchPurchase($where, $params, $page, $perPage);
}
if ($type === 'all' || $type === 'sale') {
$where = $this->buildSaleWhere($filters);
$params = $this->buildSaleParams($filters);
$totalSale = $this->countSale($where, $params);
$saleResult = $this->fetchSale($where, $params, $page, $perPage);
}
// 合并并按 createTime 排序,取前 perPage 条
$merged = array_merge($purchaseResult ?? [], $saleResult ?? []);
// 按 createTime DESC 排序
usort($merged, function ($a, $b) {
return strcmp($b['createTime'] ?? '', $a['createTime'] ?? '');
});
// 截取当前页
$merged = array_slice($merged, 0, $perPage);
// 格式化金额
foreach ($merged as &$row) {
$row['amount'] = round((float) $row['amount'], 2);
$row['guessAmount'] = round((float) $row['guessAmount'], 2);
$row['unitPrice'] = round((float) $row['unitPrice'], 2);
$row['guessUnitPrice'] = round((float) ($row['guessUnitPrice'] ?? 0), 2);
}
return [
'data' => $merged,
'total' => $totalPurchase + $totalSale,
];
}
// ===================================================================
// 筛选器选项
// ===================================================================
public function filterOptions(): array
{
$sql = "SELECT DISTINCT scmname FROM fys_supplierGoods_sttlement_purchase WHERE scmname IS NOT NULL AND scmname != ''
UNION
SELECT DISTINCT supplier FROM fys_supplierGoods_sttlement_sale WHERE supplier IS NOT NULL AND supplier != ''
ORDER BY scmname";
$stmt = $this->pdo->query($sql);
$suppliers = $stmt->fetchAll(PDO::FETCH_COLUMN);
$sql = "SELECT DISTINCT whname FROM fys_supplierGoods_sttlement_purchase WHERE whname IS NOT NULL AND whname != ''
UNION
SELECT DISTINCT whName FROM fys_supplierGoods_sttlement_sale WHERE whName IS NOT NULL AND whName != ''
ORDER BY whname";
$stmt = $this->pdo->query($sql);
$warehouses = $stmt->fetchAll(PDO::FETCH_COLUMN);
return [
'suppliers' => $suppliers,
'warehouses' => $warehouses,
];
}
// ===================================================================
// 入库查询
// ===================================================================
private function countPurchase(string $where, array $params): int
{
// 仅当筛选条件引用关联表字段时才 JOIN,否则纯主表 COUNT
$joins = '';
if (strpos($where, 'p.') !== false) {
$joins .= ' LEFT JOIN pm_purchaseIn p ON s.spo = p.sourceno';
}
if (strpos($where, 'a.') !== false) {
$joins .= ' LEFT JOIN pm_averageUnitPrice a ON s.procode = a.number AND s.scmname = a.supplier';
}
if (strpos($where, 'u.') !== false) {
$joins .= ' LEFT JOIN pm_unOrderDetails u ON s.spo = u.purchase AND s.skuoid = u.sku';
}
$sql = "SELECT COUNT(*) as cnt FROM fys_supplierGoods_sttlement_purchase s{$joins} WHERE 1=1 {$where}";
$stmt = $this->pdo->prepare($sql);
$stmt->execute($params);
return (int) $stmt->fetch()['cnt'];
}
private function fetchPurchase(string $where, array $params, int $page, int $perPage): array
{
$offset = ($page - 1) * $perPage;
$sql = "SELECT
'purchase' AS recordType,
ISNULL(p.hno, '') AS stockNo,
ISNULL(s.spo, '') AS purchaseNo,
FORMAT(ISNULL(s.created, s.createTime), 'yyyy-MM-dd HH:mm:ss') AS sendTime,
ISNULL(s.scmname, '') AS supplier,
ISNULL(s.skuoid, '') AS goodsNo,
ISNULL(s.procode, '') AS productCode,
ISNULL(s.skuname, '') AS specName,
ISNULL(s.settlementStock, 0) AS qty,
ISNULL(s.singlePrice, 0) AS unitPrice,
ISNULL(s.settlementPrice, 0) AS amount,
ISNULL(a.price, 0) AS guessUnitPrice,
ISNULL(s.settlementStock * ISNULL(a.price, 0), 0) AS guessAmount,
'' AS supplierGoodsCode,
ISNULL(s.remark, '') AS remark,
ISNULL(s.whname, '') AS warehouseName,
FORMAT(s.createTime, 'yyyy-MM-dd HH:mm:ss') AS createTime,
'' AS expressName,
'' AS expressNo,
CASE WHEN u.Id IS NOT NULL THEN 1 ELSE 0 END AS isUnsent,
s.id AS sourceId
FROM fys_supplierGoods_sttlement_purchase s
LEFT JOIN pm_purchaseIn p ON s.spo = p.sourceno
LEFT JOIN pm_averageUnitPrice a ON s.procode = a.number AND s.scmname = a.supplier
LEFT JOIN pm_unOrderDetails u ON s.spo = u.purchase AND s.skuoid = u.sku
WHERE 1=1 {$where}
ORDER BY s.createTime DESC
OFFSET {$offset} ROWS FETCH NEXT {$perPage} ROWS ONLY";
$stmt = $this->pdo->prepare($sql);
$stmt->execute($params);
return $stmt->fetchAll();
}
// ===================================================================
// 代发查询
// ===================================================================
private function countSale(string $where, array $params): int
{
$sql = "SELECT COUNT(*) as cnt FROM fys_supplierGoods_sttlement_sale s
LEFT JOIN pm_averageUnitPrice a ON s.procode = a.number AND s.supplier = a.supplier
WHERE 1=1 {$where}";
$stmt = $this->pdo->prepare($sql);
$stmt->execute($params);
return (int) $stmt->fetch()['cnt'];
}
private function fetchSale(string $where, array $params, int $page, int $perPage): array
{
$offset = ($page - 1) * $perPage;
$sql = "SELECT
'sale' AS recordType,
'' AS stockNo,
'' AS purchaseNo,
FORMAT(s.sendTime, 'yyyy-MM-dd HH:mm:ss') AS sendTime,
ISNULL(s.supplier, '') AS supplier,
ISNULL(s.skucode, '') AS goodsNo,
ISNULL(s.procode, '') AS productCode,
ISNULL(s.skuname, '') AS specName,
ISNULL(s.settlementStock, 0) AS qty,
ISNULL(s.singlePrice, 0) AS unitPrice,
ISNULL(s.settlementPrice, 0) AS amount,
ISNULL(a.price, 0) AS guessUnitPrice,
ISNULL(s.settlementStock * ISNULL(a.price, 0), 0) AS guessAmount,
'' AS supplierGoodsCode,
ISNULL(s.remark, '') AS remark,
ISNULL(s.whName, '') AS warehouseName,
FORMAT(s.createTime, 'yyyy-MM-dd HH:mm:ss') AS createTime,
ISNULL(s.expressName, '') AS expressName,
ISNULL(s.expressNo, '') AS expressNo,
CASE WHEN s.expressNo IS NULL OR s.expressNo = '' THEN 1 ELSE 0 END AS isUnsent,
s.Id AS sourceId
FROM fys_supplierGoods_sttlement_sale s
LEFT JOIN pm_averageUnitPrice a ON s.procode = a.number AND s.supplier = a.supplier
WHERE 1=1 {$where}
ORDER BY s.createTime DESC
OFFSET {$offset} ROWS FETCH NEXT {$perPage} ROWS ONLY";
$stmt = $this->pdo->prepare($sql);
$stmt->execute($params);
return $stmt->fetchAll();
}
// ===================================================================
// WHERE 与参数构建
// ===================================================================
private function like(string $v): string
{
return '%' . $v . '%';
}
// --- 入库 WHERE ---
private function buildPurchaseWhere(array $filters): string
{
$c = [];
if (!empty($filters['supplier'])) $c[] = 'AND s.scmname LIKE :supplier';
if (!empty($filters['stock_no'])) $c[] = 'AND p.hno LIKE :stock_no';
if (!empty($filters['purchase_no'])) $c[] = 'AND s.spo LIKE :purchase_no';
if (!empty($filters['product_code'])) $c[] = 'AND s.procode LIKE :product_code';
if (!empty($filters['warehouse'])) $c[] = 'AND s.whname LIKE :warehouse';
if (!empty($filters['send_time_from'])) $c[] = 'AND s.created >= :send_time_from';
if (!empty($filters['send_time_to'])) $c[] = 'AND s.created <= :send_time_to';
if (!empty($filters['create_time_from'])) $c[] = 'AND s.createTime >= :create_time_from';
if (!empty($filters['create_time_to'])) $c[] = 'AND s.createTime <= :create_time_to';
if (!empty($filters['sent_status'])) {
if ($filters['sent_status'] === 'sent') $c[] = 'AND u.Id IS NULL';
if ($filters['sent_status'] === 'unsent') $c[] = 'AND u.Id IS NOT NULL';
}
if (!empty($filters['has_diff'])) {
$c[] = 'AND ABS(ISNULL(s.settlementPrice, 0) - ISNULL(s.settlementStock * ISNULL(a.price, 0), 0)) > 0.01';
}
return $c ? ' ' . implode(' ', $c) : '';
}
private function buildPurchaseParams(array $filters): array
{
$p = [];
if (!empty($filters['supplier'])) $p[':supplier'] = $this->like($filters['supplier']);
if (!empty($filters['stock_no'])) $p[':stock_no'] = $this->like($filters['stock_no']);
if (!empty($filters['purchase_no'])) $p[':purchase_no'] = $this->like($filters['purchase_no']);
if (!empty($filters['product_code'])) $p[':product_code'] = $this->like($filters['product_code']);
if (!empty($filters['warehouse'])) $p[':warehouse'] = $this->like($filters['warehouse']);
if (!empty($filters['send_time_from'])) $p[':send_time_from'] = $filters['send_time_from'];
if (!empty($filters['send_time_to'])) $p[':send_time_to'] = $filters['send_time_to'];
if (!empty($filters['create_time_from'])) $p[':create_time_from'] = $filters['create_time_from'];
if (!empty($filters['create_time_to'])) $p[':create_time_to'] = $filters['create_time_to'];
return $p;
}
// --- 代发 WHERE ---
private function buildSaleWhere(array $filters): string
{
$c = [];
if (!empty($filters['supplier'])) $c[] = 'AND s.supplier LIKE :supplier_s';
if (!empty($filters['product_code'])) $c[] = 'AND s.procode LIKE :product_code_s';
if (!empty($filters['express_no'])) $c[] = 'AND s.expressNo LIKE :express_no';
if (!empty($filters['warehouse'])) $c[] = 'AND s.whName LIKE :warehouse_s';
if (!empty($filters['send_time_from'])) $c[] = 'AND s.sendTime >= :send_time_from_s';
if (!empty($filters['send_time_to'])) $c[] = 'AND s.sendTime <= :send_time_to_s';
if (!empty($filters['create_time_from'])) $c[] = 'AND s.createTime >= :create_time_from_s';
if (!empty($filters['create_time_to'])) $c[] = 'AND s.createTime <= :create_time_to_s';
if (!empty($filters['sent_status'])) {
if ($filters['sent_status'] === 'sent') $c[] = "AND s.expressNo IS NOT NULL AND s.expressNo != ''";
if ($filters['sent_status'] === 'unsent') $c[] = "AND (s.expressNo IS NULL OR s.expressNo = '')";
}
if (!empty($filters['has_diff'])) {
$c[] = 'AND ABS(ISNULL(s.settlementPrice, 0) - ISNULL(s.settlementStock * ISNULL(a.price, 0), 0)) > 0.01';
}
return $c ? ' ' . implode(' ', $c) : '';
}
private function buildSaleParams(array $filters): array
{
$p = [];
if (!empty($filters['supplier'])) $p[':supplier_s'] = $this->like($filters['supplier']);
if (!empty($filters['product_code'])) $p[':product_code_s'] = $this->like($filters['product_code']);
if (!empty($filters['express_no'])) $p[':express_no'] = $this->like($filters['express_no']);
if (!empty($filters['warehouse'])) $p[':warehouse_s'] = $this->like($filters['warehouse']);
if (!empty($filters['send_time_from'])) $p[':send_time_from_s'] = $filters['send_time_from'];
if (!empty($filters['send_time_to'])) $p[':send_time_to_s'] = $filters['send_time_to'];
if (!empty($filters['create_time_from'])) $p[':create_time_from_s'] = $filters['create_time_from'];
if (!empty($filters['create_time_to'])) $p[':create_time_to_s'] = $filters['create_time_to'];
return $p;
}
}

View File

@ -0,0 +1,540 @@
<?php
namespace Modules\EcommerceFinance\Repositories;
use Modules\EcommerceFinance\Services\JianSunConnection;
use PDO;
/**
* 供应商应付 - 子功能页面数据仓库
*
* 封装 5 个子页面的数据访问逻辑:
* - returnReconciliations() 退货对账
* - invoiceDeductions() 发票扣款
* - compensationDetails() 赔偿明细(UNION 纸板 + 压板)
* - advanceDetails() 借支明细
* - sampleDeductions() 样品扣款
*
* 所有方法返回统一结构: ['data' => array<int,array>, 'total' => int]
*/
class SupplierPayableRepository
{
private PDO $db;
public function __construct()
{
$this->db = JianSunConnection::get();
}
/**
* 通用工具:带筛选 + 分页的查询
*/
private function runQuery(string $sql, array $params, int $page, int $pageSize, int $countThreshold = 5000): array
{
$offset = ($page - 1) * $pageSize;
// 1. 总数(先查总数,超阈值再用近似)
$countSql = 'SELECT COUNT(*) AS cnt FROM (' . $sql . ') AS sub';
$total = 0;
try {
$stmt = $this->db->prepare($countSql);
$stmt->execute($params);
$total = (int) $stmt->fetchColumn();
} catch (\Throwable $e) {
// 极端情况:表太大查不到总数时退回到 -1
$total = -1;
}
// 2. 分页数据(用 TOP + OFFSET/FETCH 不可行,统一使用 ROW_NUMBER() OVER 包装)
$pagedSql = 'SELECT * FROM (SELECT *, ROW_NUMBER() OVER (ORDER BY (SELECT 0)) AS rn FROM (' . $sql . ') AS src) AS t WHERE rn BETWEEN ? AND ?';
$paramsWithRange = array_merge($params, [$offset + 1, $offset + $pageSize]);
$stmt = $this->db->prepare($pagedSql);
$stmt->execute($paramsWithRange);
return [
'data' => $stmt->fetchAll(),
'total' => $total,
];
}
/**
* 计算分页的 SELECT TOP N(无 COUNT),加速大表查询
*/
private function runQueryFast(string $sql, array $params, int $page, int $pageSize, int $total = -1): array
{
$offset = ($page - 1) * $pageSize;
$pagedSql = 'SELECT * FROM (SELECT *, ROW_NUMBER() OVER (ORDER BY (SELECT 0)) AS rn FROM (' . $sql . ') AS src) AS t WHERE rn BETWEEN ? AND ?';
$paramsWithRange = array_merge($params, [$offset + 1, $offset + $pageSize]);
$stmt = $this->db->prepare($pagedSql);
$stmt->execute($paramsWithRange);
return [
'data' => $stmt->fetchAll(),
'total' => $total,
];
}
// ============================================================
// 1. 退货对账 — finance_purchaseReturn (36791行)
// ============================================================
public function returnReconciliations(array $filters, int $page, int $pageSize): array
{
$where = [];
$params = [];
if (!empty($filters['supplier'])) {
$where[] = 'supplierName LIKE ?';
$params[] = '%' . $filters['supplier'] . '%';
}
if (!empty($filters['warehouse'])) {
$where[] = 'warehouseName LIKE ?';
$params[] = '%' . $filters['warehouse'] . '%';
}
if (!empty($filters['sku_id'])) {
$where[] = 'sku_id LIKE ?';
$params[] = '%' . $filters['sku_id'] . '%';
}
if (!empty($filters['express_no'])) {
$where[] = 'expressNo LIKE ?';
$params[] = '%' . $filters['express_no'] . '%';
}
if (!empty($filters['date_from'])) {
$where[] = 'date >= ?';
$params[] = $filters['date_from'];
}
if (!empty($filters['date_to'])) {
$where[] = 'date <= ?';
$params[] = $filters['date_to'];
}
$whereSql = $where ? 'WHERE ' . implode(' AND ', $where) : '';
$sql = "SELECT
CONVERT(varchar(10), date, 120) AS send_time,
supplierName AS supplier,
sku_id AS barcode,
name AS sku_name,
CAST(qty AS decimal(18,2)) AS qty,
CAST(price AS decimal(18,2)) AS unit_price,
CAST(price * qty AS decimal(18,2)) AS amount,
CAST(priceB AS decimal(18,2)) AS guess_unit_price,
CAST(priceB * qty AS decimal(18,2)) AS guess_amount,
remark,
io_id AS wms_pick_no,
warehouseName AS warehouse,
expressNo AS express_no,
expressName AS express_name,
CONVERT(varchar(10), io_date, 120) AS io_date,
labels,
creator_name AS creator,
'' AS title,
'' AS supplier_barcode,
'' AS original_barcode,
'' AS reconciler
FROM finance_purchaseReturn
$whereSql";
$result = $this->runQuery($sql, $params, $page, $pageSize);
return [
'data' => $result['data'],
'total' => $result['total'],
];
}
public function returnReconciliationSummary(array $filters): array
{
$where = [];
$params = [];
if (!empty($filters['supplier'])) {
$where[] = 'supplierName LIKE ?';
$params[] = '%' . $filters['supplier'] . '%';
}
if (!empty($filters['date_from'])) {
$where[] = 'date >= ?';
$params[] = $filters['date_from'];
}
if (!empty($filters['date_to'])) {
$where[] = 'date <= ?';
$params[] = $filters['date_to'];
}
$whereSql = $where ? 'WHERE ' . implode(' AND ', $where) : '';
$sql = "SELECT
ISNULL(SUM(price * qty), 0) AS total_amount,
ISNULL(SUM(priceB * qty), 0) AS total_guess_amount,
COUNT(*) AS total_count
FROM finance_purchaseReturn $whereSql";
$stmt = $this->db->prepare($sql);
$stmt->execute($params);
$row = $stmt->fetch();
return [
'totalAmount' => (float) $row['total_amount'],
'totalGuessAmount' => (float) $row['total_guess_amount'],
'totalCount' => (int) $row['total_count'],
];
}
// ============================================================
// 2. 发票扣款 — finance_invoiceDeduction (195行)
// ============================================================
public function invoiceDeductions(array $filters, int $page, int $pageSize): array
{
$where = [];
$params = [];
if (!empty($filters['supplier'])) {
$where[] = 'supplier LIKE ?';
$params[] = '%' . $filters['supplier'] . '%';
}
if (!empty($filters['month_from'])) {
$where[] = 'month >= ?';
$params[] = $filters['month_from'];
}
if (!empty($filters['month_to'])) {
$where[] = 'month <= ?';
$params[] = $filters['month_to'];
}
$whereSql = $where ? 'WHERE ' . implode(' AND ', $where) : '';
$sql = "SELECT
CONVERT(varchar(7), month, 120) AS month_str,
'' AS category,
supplier AS payee,
supplier AS receiver,
'' AS board_factory,
CAST(0 AS decimal(18,2)) AS subsidy_fee,
remark,
CAST(price AS decimal(18,2)) AS price,
remark AS detail,
'' AS note,
personCharge AS person_charge,
CONVERT(varchar(19), createTime, 120) AS create_time,
creator
FROM finance_invoiceDeduction
$whereSql";
return $this->runQuery($sql, $params, $page, $pageSize);
}
public function invoiceDeductionSummary(array $filters): array
{
$where = [];
$params = [];
if (!empty($filters['supplier'])) {
$where[] = 'supplier LIKE ?';
$params[] = '%' . $filters['supplier'] . '%';
}
$whereSql = $where ? 'WHERE ' . implode(' AND ', $where) : '';
$sql = "SELECT ISNULL(SUM(price), 0) AS total_price, COUNT(*) AS total_count FROM finance_invoiceDeduction $whereSql";
$stmt = $this->db->prepare($sql);
$stmt->execute($params);
$row = $stmt->fetch();
return [
'totalPrice' => (float) $row['total_price'],
'totalCount' => (int) $row['total_count'],
];
}
// ============================================================
// 3. 赔偿明细 — fys_paper_factory + fys_pressing_factory UNION
// ============================================================
public function compensationDetails(array $filters, int $page, int $pageSize): array
{
$where = [];
$params = [];
if (!empty($filters['supplier'])) {
$where[] = 'supplier LIKE ?';
$params[] = '%' . $filters['supplier'] . '%';
}
if (!empty($filters['month_from'])) {
$where[] = 'month >= ?';
$params[] = $filters['month_from'];
}
if (!empty($filters['month_to'])) {
$where[] = 'month <= ?';
$params[] = $filters['month_to'];
}
$whereSql = $where ? 'WHERE ' . implode(' AND ', $where) : '';
$sql = "SELECT
month,
supplier,
CAST(compensation_amount AS decimal(18,2)) AS amount,
reason,
'' AS remark,
creator,
CONVERT(varchar(19), create_time, 120) AS create_time,
'纸板厂' AS factory_type
FROM (
SELECT
month,
factory_subject_name AS supplier,
compensation_amount,
reason,
creator,
create_time
FROM fys_paper_factory_compensation_detail
UNION ALL
SELECT
month,
factory_subject_name AS supplier,
compensation_amount,
reason,
creator,
create_time
FROM fys_pressing_factory_compensation_detail
) AS t
$whereSql
ORDER BY month DESC, create_time DESC";
return $this->runQuery($sql, $params, $page, $pageSize);
}
public function compensationDetailSummary(array $filters): array
{
$where = [];
$params = [];
if (!empty($filters['supplier'])) {
$where[] = 'supplier LIKE ?';
$params[] = '%' . $filters['supplier'] . '%';
}
$whereSql = $where ? 'WHERE ' . implode(' AND ', $where) : '';
$sql = "SELECT
ISNULL(SUM(compensation_amount), 0) AS total_amount,
COUNT(*) AS total_count
FROM (
SELECT compensation_amount, factory_subject_name AS supplier FROM fys_paper_factory_compensation_detail
UNION ALL
SELECT compensation_amount, factory_subject_name AS supplier FROM fys_pressing_factory_compensation_detail
) AS t
$whereSql";
$stmt = $this->db->prepare($sql);
$stmt->execute($params);
$row = $stmt->fetch();
return [
'totalAmount' => (float) $row['total_amount'],
'totalCount' => (int) $row['total_count'],
];
}
// ============================================================
// 4. 借支明细 — finance_advanceLoans (4行, 字段不足,部分列显示-)
// ============================================================
public function advanceDetails(array $filters, int $page, int $pageSize): array
{
$where = [];
$params = [];
if (!empty($filters['supplier'])) {
$where[] = 'supplier LIKE ?';
$params[] = '%' . $filters['supplier'] . '%';
}
if (!empty($filters['month_from'])) {
$where[] = 'month >= ?';
$params[] = $filters['month_from'];
}
if (!empty($filters['month_to'])) {
$where[] = 'month <= ?';
$params[] = $filters['month_to'];
}
$whereSql = $where ? 'WHERE ' . implode(' AND ', $where) : '';
$sql = "SELECT
supplier,
'' AS contract_name,
CAST(price AS decimal(18,2)) AS advance_amount,
CAST(0 AS decimal(18,2)) AS actual_pay,
CAST(0 AS decimal(18,2)) AS month_deduct,
CAST(0 AS decimal(18,2)) AS should_deduct,
'' AS deduct_start_month,
'' AS deduct_end_month,
0 AS remain_periods,
CONVERT(varchar(7), month, 120) AS month_str,
'' AS loan_term,
remark,
'' AS is_paid,
CONVERT(varchar(7), month, 120) AS pay_month,
'' AS serial_no,
'' AS loan_agreement,
personCharge AS person_charge,
'' AS receipt_0,
'' AS receipt_1,
'' AS receipt_2,
CONVERT(varchar(19), createTime, 120) AS create_time,
creator
FROM finance_advanceLoans
$whereSql
ORDER BY month DESC, createTime DESC";
return $this->runQuery($sql, $params, $page, $pageSize);
}
public function advanceDetailSummary(array $filters): array
{
$where = [];
$params = [];
if (!empty($filters['supplier'])) {
$where[] = 'supplier LIKE ?';
$params[] = '%' . $filters['supplier'] . '%';
}
$whereSql = $where ? 'WHERE ' . implode(' AND ', $where) : '';
$sql = "SELECT ISNULL(SUM(price), 0) AS total_amount, COUNT(*) AS total_count FROM finance_advanceLoans $whereSql";
$stmt = $this->db->prepare($sql);
$stmt->execute($params);
$row = $stmt->fetch();
return [
'totalAmount' => (float) $row['total_amount'],
'totalCount' => (int) $row['total_count'],
];
}
// ============================================================
// 5. 样品扣款 — finance_sampleDeduction (12行)
// ============================================================
public function sampleDeductions(array $filters, int $page, int $pageSize): array
{
$where = [];
$params = [];
if (!empty($filters['supplier'])) {
$where[] = 'supplier LIKE ?';
$params[] = '%' . $filters['supplier'] . '%';
}
if (!empty($filters['department'])) {
$where[] = 'department LIKE ?';
$params[] = '%' . $filters['department'] . '%';
}
if (!empty($filters['creator'])) {
$where[] = 'creator LIKE ?';
$params[] = '%' . $filters['creator'] . '%';
}
if (!empty($filters['sku_id'])) {
$where[] = 'sku_id LIKE ?';
$params[] = '%' . $filters['sku_id'] . '%';
}
if (!empty($filters['date_from'])) {
$where[] = 'finishTime >= ?';
$params[] = $filters['date_from'];
}
if (!empty($filters['date_to'])) {
$where[] = 'finishTime <= ?';
$params[] = $filters['date_to'];
}
$whereSql = $where ? 'WHERE ' . implode(' AND ', $where) : '';
$sql = "SELECT
CONVERT(varchar(10), finishTime, 120) AS finish_time,
department,
creator,
sku_id,
skuName,
CAST(num AS decimal(18,2)) AS num,
supplier,
purchase,
productCode,
CAST(shippingFee AS decimal(18,2)) AS shipping_fee,
CAST(costPrice AS decimal(18,2)) AS cost_price,
CAST(totalAmount AS decimal(18,2)) AS total_amount,
CAST(estimatedPrice AS decimal(18,2)) AS estimated_price,
CAST(estimatedPriceAll AS decimal(18,2)) AS estimated_price_all,
'' AS remark,
businessId,
CONVERT(varchar(19), updateAt, 120) AS update_time
FROM finance_sampleDeduction
$whereSql
ORDER BY finishTime DESC";
return $this->runQuery($sql, $params, $page, $pageSize);
}
public function sampleDeductionSummary(array $filters): array
{
$where = [];
$params = [];
if (!empty($filters['supplier'])) {
$where[] = 'supplier LIKE ?';
$params[] = '%' . $filters['supplier'] . '%';
}
$whereSql = $where ? 'WHERE ' . implode(' AND ', $where) : '';
$sql = "SELECT
ISNULL(SUM(totalAmount), 0) AS total_amount,
ISNULL(SUM(estimatedPriceAll), 0) AS total_estimate,
COUNT(*) AS total_count
FROM finance_sampleDeduction $whereSql";
$stmt = $this->db->prepare($sql);
$stmt->execute($params);
$row = $stmt->fetch();
return [
'totalAmount' => (float) $row['total_amount'],
'totalEstimate' => (float) $row['total_estimate'],
'totalCount' => (int) $row['total_count'],
];
}
// ============================================================
// 筛选器选项
// ============================================================
public function filterOptionsForReturn(): array
{
$suppliers = $this->fetchDistinct('SELECT DISTINCT supplierName FROM finance_purchaseReturn WHERE supplierName IS NOT NULL ORDER BY supplierName');
$warehouses = $this->fetchDistinct('SELECT DISTINCT warehouseName FROM finance_purchaseReturn WHERE warehouseName IS NOT NULL ORDER BY warehouseName');
return compact('suppliers', 'warehouses');
}
public function filterOptionsForInvoice(): array
{
$suppliers = $this->fetchDistinct('SELECT DISTINCT supplier FROM finance_invoiceDeduction WHERE supplier IS NOT NULL ORDER BY supplier');
return ['suppliers' => $suppliers];
}
public function filterOptionsForCompensation(): array
{
$suppliers = $this->fetchDistinct("SELECT DISTINCT supplier FROM (
SELECT factory_subject_name AS supplier FROM fys_paper_factory_compensation_detail WHERE factory_subject_name IS NOT NULL
UNION
SELECT factory_subject_name AS supplier FROM fys_pressing_factory_compensation_detail WHERE factory_subject_name IS NOT NULL
) AS t ORDER BY supplier");
return ['suppliers' => $suppliers];
}
public function filterOptionsForAdvance(): array
{
$suppliers = $this->fetchDistinct('SELECT DISTINCT supplier FROM finance_advanceLoans WHERE supplier IS NOT NULL ORDER BY supplier');
return ['suppliers' => $suppliers];
}
public function filterOptionsForSample(): array
{
$suppliers = $this->fetchDistinct('SELECT DISTINCT supplier FROM finance_sampleDeduction WHERE supplier IS NOT NULL ORDER BY supplier');
$departments = $this->fetchDistinct('SELECT DISTINCT department FROM finance_sampleDeduction WHERE department IS NOT NULL ORDER BY department');
$creators = $this->fetchDistinct('SELECT DISTINCT creator FROM finance_sampleDeduction WHERE creator IS NOT NULL ORDER BY creator');
return compact('suppliers', 'departments', 'creators');
}
private function fetchDistinct(string $sql): array
{
$rows = $this->db->query($sql)->fetchAll(PDO::FETCH_COLUMN);
return array_values(array_filter($rows, fn ($v) => $v !== null && $v !== ''));
}
}

View File

@ -0,0 +1,70 @@
<?php
namespace Modules\EcommerceFinance\Services;
use PDO;
use PDOException;
/**
* JianSun SQL Server 数据库连接管理
* 使用 PDO_ODBC 直连 SQL Server(无需 sqlsrv 扩展)
*/
class JianSunConnection
{
private static ?PDO $instance = null;
/**
* 获取 PDO 连接实例(单例模式)
*/
public static function get(): PDO
{
if (self::$instance === null) {
self::$instance = self::create();
}
return self::$instance;
}
/**
* 创建新的 PDO 连接
*/
public static function create(): PDO
{
$host = env('DB_JIANSUN_HOST', '218.93.191.21');
$port = env('DB_JIANSUN_PORT', '1433');
$db = env('DB_JIANSUN_DATABASE', 'JianSun');
$user = env('DB_JIANSUN_USERNAME', 'financeReader');
$pass = env('DB_JIANSUN_PASSWORD', '');
$dsn = sprintf(
'odbc:Driver={SQL Server};Server=%s,%s;Database=%s',
$host,
$port,
$db
);
try {
$pdo = new PDO($dsn, $user, $pass, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]);
return $pdo;
} catch (PDOException $e) {
throw new \RuntimeException(
'JianSun 数据库连接失败: ' . $e->getMessage(),
500,
$e
);
}
}
/**
* 重置连接(用于长时间运行或测试场景)
*/
public static function reset(): void
{
self::$instance = null;
}
}

View File

@ -0,0 +1,84 @@
require('./../../bootstrap');
import Vue from 'vue';
import DashboardPlugin from './../../plugins/dashboard-plugin';
import Global from './../../mixins/global';
import {
Table, TableColumn, Pagination, DatePicker, Tag, Button, Input, Message
} from 'element-ui';
Vue.use(DashboardPlugin);
Vue.component(Table.name, Table);
Vue.component(TableColumn.name, TableColumn);
Vue.component(Pagination.name, Pagination);
Vue.component(DatePicker.name, DatePicker);
Vue.component(Tag.name, Tag);
Vue.component(Input.name, Input);
Vue.component(Button.name, Button);
Vue.prototype.$message = Message;
const app = new Vue({
el: '#advance-detail-app',
mixins: [Global],
data() {
return {
baseUrl: url + '/api/ecommerce-finance/supplier-payables/advance-details',
loading: false,
tableData: [],
total: 0,
currentPage: 1,
pageSize: 25,
summary: { totalAmount: 0, totalCount: 0 },
filters: { supplier: '', month_range: null }
};
},
mounted() { this.fetchData(); },
methods: {
fetchData() {
this.loading = true;
const params = this.buildParams();
Promise.all([
axios.get(this.baseUrl, { params }),
axios.get(this.baseUrl + '/summary', { params })
]).then(([listRes, sumRes]) => {
this.tableData = listRes.data.data || [];
this.total = listRes.data.total || 0;
this.currentPage = listRes.data.page || 1;
this.pageSize = listRes.data.per_page || 25;
this.summary = sumRes.data;
}).catch(() => this.$message.error('获取数据失败'))
.finally(() => { this.loading = false; });
},
buildParams() {
const params = { page: this.currentPage, limit: this.pageSize };
if (this.filters.supplier) params.supplier = this.filters.supplier;
if (this.filters.month_range && this.filters.month_range.length === 2) {
params.month_from = this.filters.month_range[0];
params.month_to = this.filters.month_range[1];
}
return params;
},
onFilterChange() { this.currentPage = 1; this.fetchData(); },
onPageChange(p) { this.currentPage = p; this.fetchData(); },
onPageSizeChange(s) { this.pageSize = s; this.currentPage = 1; this.fetchData(); },
onReset() {
this.filters = { supplier: '', month_range: null };
this.currentPage = 1;
this.fetchData();
},
formatMoney(v) {
if (v == null || isNaN(v)) return '0.00';
return '¥' + Number(v).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}
}
});
export default app;

View File

@ -0,0 +1,84 @@
require('./../../bootstrap');
import Vue from 'vue';
import DashboardPlugin from './../../plugins/dashboard-plugin';
import Global from './../../mixins/global';
import {
Table, TableColumn, Pagination, DatePicker, Tag, Button, Input, Message
} from 'element-ui';
Vue.use(DashboardPlugin);
Vue.component(Table.name, Table);
Vue.component(TableColumn.name, TableColumn);
Vue.component(Pagination.name, Pagination);
Vue.component(DatePicker.name, DatePicker);
Vue.component(Tag.name, Tag);
Vue.component(Input.name, Input);
Vue.component(Button.name, Button);
Vue.prototype.$message = Message;
const app = new Vue({
el: '#compensation-detail-app',
mixins: [Global],
data() {
return {
baseUrl: url + '/api/ecommerce-finance/supplier-payables/compensation-details',
loading: false,
tableData: [],
total: 0,
currentPage: 1,
pageSize: 25,
summary: { totalAmount: 0, totalCount: 0 },
filters: { supplier: '', month_range: null }
};
},
mounted() { this.fetchData(); },
methods: {
fetchData() {
this.loading = true;
const params = this.buildParams();
Promise.all([
axios.get(this.baseUrl, { params }),
axios.get(this.baseUrl + '/summary', { params })
]).then(([listRes, sumRes]) => {
this.tableData = listRes.data.data || [];
this.total = listRes.data.total || 0;
this.currentPage = listRes.data.page || 1;
this.pageSize = listRes.data.per_page || 25;
this.summary = sumRes.data;
}).catch(() => this.$message.error('获取数据失败'))
.finally(() => { this.loading = false; });
},
buildParams() {
const params = { page: this.currentPage, limit: this.pageSize };
if (this.filters.supplier) params.supplier = this.filters.supplier;
if (this.filters.month_range && this.filters.month_range.length === 2) {
params.month_from = this.filters.month_range[0];
params.month_to = this.filters.month_range[1];
}
return params;
},
onFilterChange() { this.currentPage = 1; this.fetchData(); },
onPageChange(p) { this.currentPage = p; this.fetchData(); },
onPageSizeChange(s) { this.pageSize = s; this.currentPage = 1; this.fetchData(); },
onReset() {
this.filters = { supplier: '', month_range: null };
this.currentPage = 1;
this.fetchData();
},
formatMoney(v) {
if (v == null || isNaN(v)) return '0.00';
return '¥' + Number(v).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}
}
});
export default app;

View File

@ -0,0 +1,277 @@
require('./../../bootstrap');
import Vue from 'vue';
import DashboardPlugin from './../../plugins/dashboard-plugin';
import Global from './../../mixins/global';
import {
Table,
TableColumn,
Pagination,
DatePicker,
Tag,
Message
} from 'element-ui';
Vue.use(DashboardPlugin);
Vue.component(Table.name, Table);
Vue.component(TableColumn.name, TableColumn);
Vue.component(Pagination.name, Pagination);
Vue.component(DatePicker.name, DatePicker);
Vue.component(Tag.name, Tag);
Vue.prototype.$message = Message;
const app = new Vue({
el: '#goods-reconciliation-app',
mixins: [Global],
data() {
return {
baseUrl: url + '/api/ecommerce-finance/goods-reconciliations',
loading: false,
tableData: [],
total: 0,
currentPage: 1,
pageSize: 25,
activeTab: 'all',
summary: {
totalAmount: 0,
totalGuessAmount: 0,
totalCount: 0,
sentCount: 0,
unsentCount: 0
},
options: {
suppliers: [],
warehouses: []
},
filters: {
record_type: 'all',
supplier: '',
warehouse: '',
stock_no: '',
purchase_no: '',
product_code: '',
express_no: '',
send_time_range: null,
create_time_range: null,
sent_status: 'all',
has_diff: false
},
tabs: [
{ key: 'all', label: '全部记录' },
{ key: 'purchase', label: '入库对账' },
{ key: 'sale', label: '代发对账' },
{ key: 'unsent', label: '未发出' },
{ key: 'diff', label: '有差异' }
],
sortProp: 'createTime',
sortOrder: 'descending'
};
},
mounted() {
this.fetchOptions();
this.fetchData();
},
methods: {
/**
* Fetch filter options (suppliers, warehouses)
*/
fetchOptions() {
axios.get(this.baseUrl + '/options')
.then(response => {
this.options = response.data;
})
.catch(() => {
this.$message.error('获取筛选选项失败');
});
},
/**
* Fetch data list and summary
*/
fetchData() {
this.loading = true;
const params = this.buildParams();
const summaryParams = this.buildSummaryParams();
Promise.all([
axios.get(this.baseUrl, { params }),
axios.get(this.baseUrl + '/summary', { params: summaryParams })
])
.then(([listRes, summaryRes]) => {
this.tableData = listRes.data.data || [];
this.total = listRes.data.total || 0;
this.currentPage = listRes.data.page || 1;
this.pageSize = listRes.data.per_page || 25;
this.summary = summaryRes.data;
})
.catch(() => {
this.$message.error('获取数据失败,请稍后重试');
})
.finally(() => {
this.loading = false;
});
},
/**
* Build list query params
*/
buildParams() {
const params = {
page: this.currentPage,
limit: this.pageSize,
record_type: this.filters.record_type
};
// text filters
const textFields = ['supplier', 'warehouse', 'stock_no', 'purchase_no', 'product_code', 'express_no'];
textFields.forEach(f => {
if (this.filters[f]) {
params[f] = this.filters[f];
}
});
// date range filters
if (this.filters.send_time_range && this.filters.send_time_range.length === 2) {
params.send_time_from = this.filters.send_time_range[0];
params.send_time_to = this.filters.send_time_range[1];
}
if (this.filters.create_time_range && this.filters.create_time_range.length === 2) {
params.create_time_from = this.filters.create_time_range[0];
params.create_time_to = this.filters.create_time_range[1];
}
// sent status
if (this.filters.sent_status && this.filters.sent_status !== 'all') {
params.sent_status = this.filters.sent_status;
}
// has diff
if (this.filters.has_diff) {
params.has_diff = 1;
}
return params;
},
/**
* Summary params always use 'all' for overall counts
*/
buildSummaryParams() {
const params = {};
const textFields = ['supplier', 'warehouse', 'stock_no', 'purchase_no', 'product_code', 'express_no'];
textFields.forEach(f => {
if (this.filters[f]) {
params[f] = this.filters[f];
}
});
if (this.filters.send_time_range && this.filters.send_time_range.length === 2) {
params.send_time_from = this.filters.send_time_range[0];
params.send_time_to = this.filters.send_time_range[1];
}
if (this.filters.create_time_range && this.filters.create_time_range.length === 2) {
params.create_time_from = this.filters.create_time_range[0];
params.create_time_to = this.filters.create_time_range[1];
}
params.record_type = this.filters.record_type;
if (this.filters.sent_status && this.filters.sent_status !== 'all') {
params.sent_status = this.filters.sent_status;
}
return params;
},
/**
* Switch tab
*/
switchTab(key) {
this.activeTab = key;
this.currentPage = 1;
switch (key) {
case 'all':
this.filters.record_type = 'all';
this.filters.sent_status = 'all';
this.filters.has_diff = false;
break;
case 'purchase':
this.filters.record_type = 'purchase';
this.filters.sent_status = 'all';
this.filters.has_diff = false;
break;
case 'sale':
this.filters.record_type = 'sale';
this.filters.sent_status = 'all';
this.filters.has_diff = false;
break;
case 'unsent':
this.filters.sent_status = 'unsent';
this.filters.has_diff = false;
break;
case 'diff':
this.filters.has_diff = true;
this.filters.sent_status = 'all';
break;
}
this.fetchData();
},
/**
* On filter change, reset to page 1
*/
onFilterChange() {
this.currentPage = 1;
this.fetchData();
},
/**
* Pagination page change
*/
onPageChange(page) {
this.currentPage = page;
this.fetchData();
},
/**
* Pagination page size change
*/
onPageSizeChange(size) {
this.pageSize = size;
this.currentPage = 1;
this.fetchData();
},
/**
* Sort change
*/
onSortChange({ prop, order }) {
this.sortProp = prop;
this.sortOrder = order;
// Sorting is handled server-side for now; re-fetch
this.fetchData();
},
/**
* Format money (CNY)
*/
formatMoney(value) {
if (value == null || isNaN(value)) return '0.00';
return '¥' + Number(value).toLocaleString('zh-CN', {
minimumFractionDigits: 2,
maximumFractionDigits: 2
});
}
}
});
export default app;

View File

@ -0,0 +1,84 @@
require('./../../bootstrap');
import Vue from 'vue';
import DashboardPlugin from './../../plugins/dashboard-plugin';
import Global from './../../mixins/global';
import {
Table, TableColumn, Pagination, DatePicker, Tag, Button, Input, Message
} from 'element-ui';
Vue.use(DashboardPlugin);
Vue.component(Table.name, Table);
Vue.component(TableColumn.name, TableColumn);
Vue.component(Pagination.name, Pagination);
Vue.component(DatePicker.name, DatePicker);
Vue.component(Tag.name, Tag);
Vue.component(Input.name, Input);
Vue.component(Button.name, Button);
Vue.prototype.$message = Message;
const app = new Vue({
el: '#invoice-deduction-app',
mixins: [Global],
data() {
return {
baseUrl: url + '/api/ecommerce-finance/supplier-payables/invoice-deductions',
loading: false,
tableData: [],
total: 0,
currentPage: 1,
pageSize: 25,
summary: { totalPrice: 0, totalCount: 0 },
filters: { supplier: '', month_range: null }
};
},
mounted() { this.fetchData(); },
methods: {
fetchData() {
this.loading = true;
const params = this.buildParams();
Promise.all([
axios.get(this.baseUrl, { params }),
axios.get(this.baseUrl + '/summary', { params })
]).then(([listRes, sumRes]) => {
this.tableData = listRes.data.data || [];
this.total = listRes.data.total || 0;
this.currentPage = listRes.data.page || 1;
this.pageSize = listRes.data.per_page || 25;
this.summary = sumRes.data;
}).catch(() => this.$message.error('获取数据失败'))
.finally(() => { this.loading = false; });
},
buildParams() {
const params = { page: this.currentPage, limit: this.pageSize };
if (this.filters.supplier) params.supplier = this.filters.supplier;
if (this.filters.month_range && this.filters.month_range.length === 2) {
params.month_from = this.filters.month_range[0];
params.month_to = this.filters.month_range[1];
}
return params;
},
onFilterChange() { this.currentPage = 1; this.fetchData(); },
onPageChange(p) { this.currentPage = p; this.fetchData(); },
onPageSizeChange(s) { this.pageSize = s; this.currentPage = 1; this.fetchData(); },
onReset() {
this.filters = { supplier: '', month_range: null };
this.currentPage = 1;
this.fetchData();
},
formatMoney(v) {
if (v == null || isNaN(v)) return '0.00';
return '¥' + Number(v).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}
}
});
export default app;

View File

@ -0,0 +1,113 @@
require('./../../bootstrap');
import Vue from 'vue';
import DashboardPlugin from './../../plugins/dashboard-plugin';
import Global from './../../mixins/global';
import {
Table,
TableColumn,
Pagination,
DatePicker,
Tag,
Button,
Input,
Select,
Option,
Message
} from 'element-ui';
Vue.use(DashboardPlugin);
Vue.component(Table.name, Table);
Vue.component(TableColumn.name, TableColumn);
Vue.component(Pagination.name, Pagination);
Vue.component(DatePicker.name, DatePicker);
Vue.component(Tag.name, Tag);
Vue.component(Input.name, Input);
Vue.component(Button.name, Button);
Vue.component(Select.name, Select);
Vue.component(Option.name, Option);
Vue.prototype.$message = Message;
const app = new Vue({
el: '#return-reconciliation-app',
mixins: [Global],
data() {
return {
baseUrl: url + '/api/ecommerce-finance/supplier-payables/return-reconciliations',
loading: false,
tableData: [],
total: 0,
currentPage: 1,
pageSize: 25,
summary: { totalAmount: 0, totalGuessAmount: 0, totalCount: 0 },
options: { suppliers: [], warehouses: [] },
filters: {
supplier: '',
warehouse: '',
sku_id: '',
express_no: '',
date_range: null
}
};
},
mounted() {
this.fetchOptions();
this.fetchData();
},
methods: {
fetchOptions() {
axios.get(this.baseUrl + '/options')
.then(r => { this.options = r.data; })
.catch(() => this.$message.error('获取筛选选项失败'));
},
fetchData() {
this.loading = true;
const params = this.buildParams();
Promise.all([
axios.get(this.baseUrl, { params }),
axios.get(this.baseUrl + '/summary', { params })
]).then(([listRes, sumRes]) => {
this.tableData = listRes.data.data || [];
this.total = listRes.data.total || 0;
this.currentPage = listRes.data.page || 1;
this.pageSize = listRes.data.per_page || 25;
this.summary = sumRes.data;
}).catch(() => this.$message.error('获取数据失败'))
.finally(() => { this.loading = false; });
},
buildParams() {
const params = { page: this.currentPage, limit: this.pageSize };
['supplier', 'warehouse', 'sku_id', 'express_no'].forEach(f => {
if (this.filters[f]) params[f] = this.filters[f];
});
if (this.filters.date_range && this.filters.date_range.length === 2) {
params.date_from = this.filters.date_range[0];
params.date_to = this.filters.date_range[1];
}
return params;
},
onFilterChange() { this.currentPage = 1; this.fetchData(); },
onPageChange(p) { this.currentPage = p; this.fetchData(); },
onPageSizeChange(s) { this.pageSize = s; this.currentPage = 1; this.fetchData(); },
onReset() {
this.filters = { supplier: '', warehouse: '', sku_id: '', express_no: '', date_range: null };
this.currentPage = 1;
this.fetchData();
},
formatMoney(v) {
if (v == null || isNaN(v)) return '0.00';
return '¥' + Number(v).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}
}
});
export default app;

View File

@ -0,0 +1,92 @@
require('./../../bootstrap');
import Vue from 'vue';
import DashboardPlugin from './../../plugins/dashboard-plugin';
import Global from './../../mixins/global';
import {
Table, TableColumn, Pagination, DatePicker, Tag, Button, Input, Message
} from 'element-ui';
Vue.use(DashboardPlugin);
Vue.component(Table.name, Table);
Vue.component(TableColumn.name, TableColumn);
Vue.component(Pagination.name, Pagination);
Vue.component(DatePicker.name, DatePicker);
Vue.component(Tag.name, Tag);
Vue.component(Input.name, Input);
Vue.component(Button.name, Button);
Vue.prototype.$message = Message;
const app = new Vue({
el: '#sample-deduction-app',
mixins: [Global],
data() {
return {
baseUrl: url + '/api/ecommerce-finance/supplier-payables/sample-deductions',
loading: false,
tableData: [],
total: 0,
currentPage: 1,
pageSize: 25,
summary: { totalAmount: 0, totalEstimate: 0, totalCount: 0 },
filters: {
supplier: '',
department: '',
creator: '',
sku_id: '',
date_range: null
}
};
},
mounted() { this.fetchData(); },
methods: {
fetchData() {
this.loading = true;
const params = this.buildParams();
Promise.all([
axios.get(this.baseUrl, { params }),
axios.get(this.baseUrl + '/summary', { params })
]).then(([listRes, sumRes]) => {
this.tableData = listRes.data.data || [];
this.total = listRes.data.total || 0;
this.currentPage = listRes.data.page || 1;
this.pageSize = listRes.data.per_page || 25;
this.summary = sumRes.data;
}).catch(() => this.$message.error('获取数据失败'))
.finally(() => { this.loading = false; });
},
buildParams() {
const params = { page: this.currentPage, limit: this.pageSize };
['supplier', 'department', 'creator', 'sku_id'].forEach(f => {
if (this.filters[f]) params[f] = this.filters[f];
});
if (this.filters.date_range && this.filters.date_range.length === 2) {
params.date_from = this.filters.date_range[0];
params.date_to = this.filters.date_range[1];
}
return params;
},
onFilterChange() { this.currentPage = 1; this.fetchData(); },
onPageChange(p) { this.currentPage = p; this.fetchData(); },
onPageSizeChange(s) { this.pageSize = s; this.currentPage = 1; this.fetchData(); },
onReset() {
this.filters = { supplier: '', department: '', creator: '', sku_id: '', date_range: null };
this.currentPage = 1;
this.fetchData();
},
formatMoney(v) {
if (v == null || isNaN(v)) return '0.00';
return '¥' + Number(v).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}
}
});
export default app;

View File

@ -0,0 +1,56 @@
<?php
return [
'title' => '店铺营销账户账单管理',
'douyin' => '抖音账户',
'taobao-alipay' => '淘系支付宝账户',
'taobao-wechat' => '淘系微信账户',
'pinduoduo' => '拼多多账户',
'jd-wallet' => '京东网银钱包账户',
'dewu' => '得物账户',
'vipshop' => '唯品会账户',
// 表格列标题(对应Excel表1)
'store_name' => '店铺名称',
'bill_date' => '账单日期',
'total_deposit' => '总存入金额(元)',
'total_transfer_in' => '总转入金额(元)',
'total_transfer_out' => '总转出金额(元)',
'non_grant_consumption' => '非赠款消耗金额(元)',
'grant_consumption' => '赠款消耗金额(元)',
'rebate_redpack_consumption' => '消返红包消耗金额(元)',
'discount_redpack_consumption' => '立减红包消耗金额(元)',
'shared_wallet_consumption' => '共享钱包消耗金额(元)',
'shared_grant_consumption' => '共享赠款消耗金额(元)',
'daily_total_amount' => '当日资金总金额(元)',
'period_end_balance' => '期末总余额(元)',
'grant_balance' => '赠款余额(元)',
'non_grant_balance' => '非赠款余额(元)',
'notes' => '备注',
// 操作
'add_bill' => '新增账单',
'edit_bill' => '编辑账单',
'bulk_upload' => '批量导入',
'bulk_download' => '批量导出',
'download_template' => '下载导入模板',
'choose_file' => '选择文件',
'import_success' => '成功导入 :count 条账单记录',
'import_required' => '请选择要导入的文件',
'import_file_type' => '仅支持 .xlsx、.xls、.csv 文件',
'import_file_size' => '文件大小不能超过 5MB',
'select_all' => '全选',
'select_bill' => '选择账单',
'no_records' => '暂无账单数据',
// 平台标识
'platform_douyin' => '抖音',
'platform_taobao-alipay' => '淘系支付宝',
'platform_taobao-wechat' => '淘系微信',
'platform_pinduoduo' => '拼多多',
'platform_jd-wallet' => '京东网银钱包',
'platform_dewu' => '得物',
'platform_vipshop' => '唯品会',
];

View File

@ -0,0 +1,31 @@
<?php
return [
'title' => '拼多多账户账单',
// 表格列标题(对应Excel表1)
'store_name' => '店铺名称',
'transaction_time' => '时间',
'fund_type' => '资金类型',
'flow_type' => '流水类型',
'transaction_amount' => '交易金额',
'balance' => '余额',
'transaction_summary' => '交易摘要',
// 操作
'add_bill' => '新增账单',
'edit_bill' => '编辑账单',
'bulk_upload' => '批量导入',
'bulk_download' => '批量导出',
'download_template' => '下载导入模板',
'choose_file' => '选择文件',
'import_success' => '成功导入 :count 条账单记录',
'import_required' => '请选择要导入的文件',
'import_file_type' => '仅支持 .xlsx、.xls、.csv 文件',
'import_file_size' => '文件大小不能超过 5MB',
'select_all' => '全选',
'select_bill' => '选择账单',
'no_records' => '暂无账单数据',
];

View File

@ -0,0 +1,36 @@
<?php
return [
'title' => '淘系支付宝账户账单',
// 表格列标题(对应Excel表1)
'store_name' => '店铺名称',
'transaction_date' => '交易日期',
'bookkeeping_time' => '记账时间',
'income_expense_type' => '收支类型',
'transaction_type' => '交易类型',
'operation_amount' => '操作金额(元)',
'balance_after' => '操作后余额(元)',
'notes' => '备注',
// 操作
'add_bill' => '新增账单',
'edit_bill' => '编辑账单',
'bulk_upload' => '批量导入',
'bulk_download' => '批量导出',
'download_template' => '下载导入模板',
'choose_file' => '选择文件',
'import_success' => '成功导入 :count 条账单记录',
'import_required' => '请选择要导入的文件',
'import_file_type' => '仅支持 .xlsx、.xls、.csv 文件',
'import_file_size' => '文件大小不能超过 5MB',
'select_all' => '全选',
'select_bill' => '选择账单',
'no_records' => '暂无账单数据',
// 收支类型
'income' => '收入',
'expense' => '支出',
];

View File

@ -0,0 +1,188 @@
@php
$isEdit = $account->exists;
@endphp
<x-layouts.admin>
<x-slot name="title">
{{ $isEdit ? trans('marketing_account_bills.edit_bill') : trans('marketing_account_bills.add_bill') }}
- {{ trans('marketing_account_bills.' . $platform) }}
</x-slot>
<x-slot name="content">
<x-form.container>
<form method="POST"
action="{{ $isEdit ? route('marketing-account-bills.update', [$platform, $account]) : route('marketing-account-bills.store', $platform) }}"
class="space-y-6">
@csrf
@if ($isEdit)
@method('PATCH')
@endif
{{-- 基本信息 --}}
<div class="rounded-lg border border-gray-200 bg-gray-50 px-5 py-4">
<h3 class="mb-4 text-sm font-semibold text-gray-700">基本信息</h3>
<div class="grid gap-5 sm:grid-cols-3">
<div>
<label class="mb-1 block text-xs font-medium text-gray-600">
{{ trans('marketing_account_bills.store_name') }} <span class="text-red-500">*</span>
</label>
<input name="store_name" value="{{ old('store_name', $account->store_name) }}" required
class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-green focus:ring-green" />
</div>
<div>
<label class="mb-1 block text-xs font-medium text-gray-600">
{{ trans('marketing_account_bills.bill_date') }} <span class="text-red-500">*</span>
</label>
<input type="date" name="bill_date"
value="{{ old('bill_date', $account->bill_date ? $account->bill_date->toDateString() : now()->toDateString()) }}" required
class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-green focus:ring-green" />
</div>
<div>
<label class="mb-1 block text-xs font-medium text-gray-600">
{{ trans('marketing_account_bills.notes') }}
</label>
<input name="notes" value="{{ old('notes', $account->notes) }}"
class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-green focus:ring-green" />
</div>
</div>
</div>
{{-- 收支流水 --}}
<div class="rounded-lg border border-gray-200 bg-white px-5 py-4">
<h3 class="mb-4 text-sm font-semibold text-gray-700">收支流水(元)</h3>
<div class="grid gap-5 sm:grid-cols-3">
<div>
<label class="mb-1 block text-xs font-medium text-gray-600">
{{ trans('marketing_account_bills.total_deposit') }}
</label>
<input type="number" step="0.01" min="0" name="total_deposit"
value="{{ old('total_deposit', $account->total_deposit) }}"
class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm text-right focus:border-green focus:ring-green" />
</div>
<div>
<label class="mb-1 block text-xs font-medium text-gray-600">
{{ trans('marketing_account_bills.total_transfer_in') }}
</label>
<input type="number" step="0.01" min="0" name="total_transfer_in"
value="{{ old('total_transfer_in', $account->total_transfer_in) }}"
class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm text-right focus:border-green focus:ring-green" />
</div>
<div>
<label class="mb-1 block text-xs font-medium text-gray-600">
{{ trans('marketing_account_bills.total_transfer_out') }}
</label>
<input type="number" step="0.01" min="0" name="total_transfer_out"
value="{{ old('total_transfer_out', $account->total_transfer_out) }}"
class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm text-right focus:border-green focus:ring-green" />
</div>
</div>
</div>
{{-- 消耗明细 --}}
<div class="rounded-lg border border-gray-200 bg-white px-5 py-4">
<h3 class="mb-4 text-sm font-semibold text-gray-700">消耗明细(元)</h3>
<div class="grid gap-5 sm:grid-cols-3">
<div>
<label class="mb-1 block text-xs font-medium text-gray-600">
{{ trans('marketing_account_bills.non_grant_consumption') }}
</label>
<input type="number" step="0.01" min="0" name="non_grant_consumption"
value="{{ old('non_grant_consumption', $account->non_grant_consumption) }}"
class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm text-right focus:border-green focus:ring-green" />
</div>
<div>
<label class="mb-1 block text-xs font-medium text-gray-600">
{{ trans('marketing_account_bills.grant_consumption') }}
</label>
<input type="number" step="0.01" min="0" name="grant_consumption"
value="{{ old('grant_consumption', $account->grant_consumption) }}"
class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm text-right focus:border-green focus:ring-green" />
</div>
<div>
<label class="mb-1 block text-xs font-medium text-gray-600">
{{ trans('marketing_account_bills.rebate_redpack_consumption') }}
</label>
<input type="number" step="0.01" min="0" name="rebate_redpack_consumption"
value="{{ old('rebate_redpack_consumption', $account->rebate_redpack_consumption) }}"
class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm text-right focus:border-green focus:ring-green" />
</div>
<div>
<label class="mb-1 block text-xs font-medium text-gray-600">
{{ trans('marketing_account_bills.discount_redpack_consumption') }}
</label>
<input type="number" step="0.01" min="0" name="discount_redpack_consumption"
value="{{ old('discount_redpack_consumption', $account->discount_redpack_consumption) }}"
class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm text-right focus:border-green focus:ring-green" />
</div>
<div>
<label class="mb-1 block text-xs font-medium text-gray-600">
{{ trans('marketing_account_bills.shared_wallet_consumption') }}
</label>
<input type="number" step="0.01" min="0" name="shared_wallet_consumption"
value="{{ old('shared_wallet_consumption', $account->shared_wallet_consumption) }}"
class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm text-right focus:border-green focus:ring-green" />
</div>
<div>
<label class="mb-1 block text-xs font-medium text-gray-600">
{{ trans('marketing_account_bills.shared_grant_consumption') }}
</label>
<input type="number" step="0.01" min="0" name="shared_grant_consumption"
value="{{ old('shared_grant_consumption', $account->shared_grant_consumption) }}"
class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm text-right focus:border-green focus:ring-green" />
</div>
</div>
</div>
{{-- 资金汇总与余额 --}}
<div class="rounded-lg border border-gray-200 bg-white px-5 py-4">
<h3 class="mb-4 text-sm font-semibold text-gray-700">资金汇总与余额(元)</h3>
<div class="grid gap-5 sm:grid-cols-2 lg:grid-cols-4">
<div>
<label class="mb-1 block text-xs font-medium text-gray-600">
{{ trans('marketing_account_bills.daily_total_amount') }}
</label>
<input type="number" step="0.01" min="0" name="daily_total_amount"
value="{{ old('daily_total_amount', $account->daily_total_amount) }}"
class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm text-right focus:border-green focus:ring-green" />
</div>
<div>
<label class="mb-1 block text-xs font-medium text-gray-600">
{{ trans('marketing_account_bills.period_end_balance') }}
</label>
<input type="number" step="0.01" min="0" name="period_end_balance"
value="{{ old('period_end_balance', $account->period_end_balance) }}"
class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm text-right focus:border-green focus:ring-green" />
</div>
<div>
<label class="mb-1 block text-xs font-medium text-gray-600">
{{ trans('marketing_account_bills.grant_balance') }}
</label>
<input type="number" step="0.01" min="0" name="grant_balance"
value="{{ old('grant_balance', $account->grant_balance) }}"
class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm text-right focus:border-green focus:ring-green" />
</div>
<div>
<label class="mb-1 block text-xs font-medium text-gray-600">
{{ trans('marketing_account_bills.non_grant_balance') }}
</label>
<input type="number" step="0.01" min="0" name="non_grant_balance"
value="{{ old('non_grant_balance', $account->non_grant_balance) }}"
class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm text-right focus:border-green focus:ring-green" />
</div>
</div>
</div>
<div class="flex justify-end gap-4">
<a href="{{ route('marketing-account-bills.platform.index', $platform) }}"
class="px-5 py-2 text-gray-600 text-sm">
{{ trans('general.cancel') }}
</a>
<button type="submit"
class="rounded-lg bg-green px-6 py-2 text-sm text-white hover:bg-green-700">
{{ trans('general.save') }}
</button>
</div>
</form>
</x-form.container>
</x-slot>
</x-layouts.admin>

View File

@ -0,0 +1,186 @@
<x-layouts.admin>
<x-slot name="title">
{{ trans('marketing_account_bills.' . $platform) }}
</x-slot>
<x-slot name="buttons">
@can('create-banking-accounts')
<a href="{{ route('marketing-account-bills.create', $platform) }}"
class="inline-flex items-center bg-green hover:bg-green-700 text-white px-5 py-2 rounded-lg text-sm">
{{ trans('marketing_account_bills.add_bill') }}
</a>
@endcan
</x-slot>
<x-slot name="content">
<style>
#main-body > .container {
max-width: none;
width: auto;
margin-left: 1rem;
margin-right: 1rem;
}
@media (min-width: 1280px) {
#main-body > .container {
margin-left: 3rem;
margin-right: 1.5rem;
}
}
.bill-table-wrapper { overflow-x: auto; border-radius: 0.5rem; border: 1px solid #e5e7eb; }
.bill-table { min-width: 100%; white-space: nowrap; font-size: 0.875rem; }
.bill-table thead th { position: sticky; top: 0; background: #f9fafb; text-align: left; color: #6b7280; padding: 0.75rem 1rem; border-bottom: 1px solid #e5e7eb; }
.bill-table tbody td { padding: 0.75rem 1rem; border-bottom: 1px solid #f3f4f6; }
.bill-table tbody tr:hover { background: #f9fafb; }
.amount-cell { text-align: right; font-variant-numeric: tabular-nums; }
.amount-negative { color: #ef4444; }
</style>
@php $pageAccountIds = $accounts->pluck('id')->map(fn($id) => (string) $id)->values(); @endphp
<div class="my-5 w-full" x-data="{ selected: [] }">
<form id="marketing-account-bills-export-form" method="GET"
action="{{ route('marketing-account-bills.export', $platform) }}"></form>
{{-- 操作工具栏 --}}
<div class="mb-4 flex flex-wrap items-end justify-between gap-4 border-b border-gray-200 pb-4">
@can('create-banking-accounts')
<form method="POST" action="{{ route('marketing-account-bills.import', $platform) }}"
enctype="multipart/form-data" class="flex flex-wrap items-end gap-3">
@csrf
<div>
<label for="bills-import" class="mb-1 block text-xs font-medium text-gray-600">
{{ trans('marketing_account_bills.choose_file') }}
</label>
<input id="bills-import" type="file" name="import"
accept=".xlsx,.xls,.csv" required
class="block max-w-xs text-sm text-gray-600" />
</div>
<button type="submit"
class="inline-flex h-10 items-center gap-2 rounded-lg bg-green px-4 text-sm text-white hover:bg-green-700">
<span class="material-icons text-base" aria-hidden="true">upload_file</span>
{{ trans('marketing_account_bills.bulk_upload') }}
</button>
</form>
@endcan
<div class="flex flex-wrap items-center gap-3">
@can('create-banking-accounts')
<a href="{{ route('marketing-account-bills.template', $platform) }}"
class="inline-flex h-10 items-center gap-2 rounded-lg border border-gray-300 bg-white px-4 text-sm text-gray-700 hover:bg-gray-50">
<span class="material-icons text-base" aria-hidden="true">file_download</span>
{{ trans('marketing_account_bills.download_template') }}
</a>
@endcan
@can('update-banking-accounts')
<button type="submit" form="marketing-account-bills-export-form"
class="inline-flex h-10 items-center gap-2 rounded-lg border border-gray-300 bg-white px-4 text-sm text-gray-700 hover:bg-gray-50">
<span class="material-icons text-base" aria-hidden="true">download</span>
{{ trans('marketing_account_bills.bulk_download') }}
</button>
@endcan
</div>
</div>
@if ($errors->has('import'))
<div class="mb-4 border-l-4 border-red-500 bg-red-50 px-4 py-3 text-sm text-red-700">
@foreach ($errors->get('import') as $message)
<p>{{ $message }}</p>
@endforeach
</div>
@endif
{{-- 15列数据表格 --}}
<div class="bill-table-wrapper">
<table class="bill-table divide-y divide-gray-200">
<thead>
<tr>
<th class="w-12 text-center">
<input type="checkbox"
aria-label="{{ trans('marketing_account_bills.select_all') }}"
@if ($pageAccountIds->isEmpty()) disabled @endif
x-on:change="selected = $event.target.checked ? @js($pageAccountIds) : []"
x-bind:checked="selected.length === {{ $pageAccountIds->count() }} && selected.length > 0"
class="h-4 w-4 rounded border-gray-300 text-green" />
</th>
<th>{{ trans('marketing_account_bills.store_name') }}</th>
<th>{{ trans('marketing_account_bills.bill_date') }}</th>
<th class="text-right">{{ trans('marketing_account_bills.total_deposit') }}</th>
<th class="text-right">{{ trans('marketing_account_bills.total_transfer_in') }}</th>
<th class="text-right">{{ trans('marketing_account_bills.total_transfer_out') }}</th>
<th class="text-right">{{ trans('marketing_account_bills.non_grant_consumption') }}</th>
<th class="text-right">{{ trans('marketing_account_bills.grant_consumption') }}</th>
<th class="text-right">{{ trans('marketing_account_bills.rebate_redpack_consumption') }}</th>
<th class="text-right">{{ trans('marketing_account_bills.discount_redpack_consumption') }}</th>
<th class="text-right">{{ trans('marketing_account_bills.shared_wallet_consumption') }}</th>
<th class="text-right">{{ trans('marketing_account_bills.shared_grant_consumption') }}</th>
<th class="text-right">{{ trans('marketing_account_bills.daily_total_amount') }}</th>
<th class="text-right">{{ trans('marketing_account_bills.period_end_balance') }}</th>
<th class="text-right">{{ trans('marketing_account_bills.grant_balance') }}</th>
<th class="text-right">{{ trans('marketing_account_bills.non_grant_balance') }}</th>
<th>{{ trans('marketing_account_bills.notes') }}</th>
<th class="text-right">{{ trans('general.actions') }}</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100 bg-white">
@if ($accounts->isEmpty())
<tr>
<td colspan="18" class="px-5 py-10 text-center text-gray-500">
{{ trans('marketing_account_bills.no_records') }}
</td>
</tr>
@else
@foreach ($accounts as $account)
<tr>
<td class="text-center">
<input type="checkbox" name="ids[]" value="{{ $account->id }}"
form="marketing-account-bills-export-form"
x-model="selected"
aria-label="{{ trans('marketing_account_bills.select_bill') }}"
class="h-4 w-4 rounded border-gray-300 text-green" />
</td>
<td>{{ $account->store_name }}</td>
<td>{{ $account->bill_date->format('Y-m-d') }}</td>
<td class="amount-cell">{{ number_format($account->total_deposit, 2) }}</td>
<td class="amount-cell">{{ number_format($account->total_transfer_in, 2) }}</td>
<td class="amount-cell">{{ number_format($account->total_transfer_out, 2) }}</td>
<td class="amount-cell">{{ number_format($account->non_grant_consumption, 2) }}</td>
<td class="amount-cell">{{ number_format($account->grant_consumption, 2) }}</td>
<td class="amount-cell">{{ number_format($account->rebate_redpack_consumption, 2) }}</td>
<td class="amount-cell">{{ number_format($account->discount_redpack_consumption, 2) }}</td>
<td class="amount-cell">{{ number_format($account->shared_wallet_consumption, 2) }}</td>
<td class="amount-cell">{{ number_format($account->shared_grant_consumption, 2) }}</td>
<td class="amount-cell">{{ number_format($account->daily_total_amount, 2) }}</td>
<td class="amount-cell font-semibold">{{ number_format($account->period_end_balance, 2) }}</td>
<td class="amount-cell">{{ number_format($account->grant_balance, 2) }}</td>
<td class="amount-cell">{{ number_format($account->non_grant_balance, 2) }}</td>
<td class="max-w-xs truncate" title="{{ $account->notes }}">
{{ \Illuminate\Support\Str::limit($account->notes, 20) }}
</td>
<td class="text-right whitespace-nowrap">
@can('update-banking-accounts')
<a href="{{ route('marketing-account-bills.edit', [$platform, $account]) }}"
class="text-purple hover:underline mr-3">{{ trans('general.edit') }}</a>
@endcan
@can('delete-banking-accounts')
<form action="{{ route('marketing-account-bills.destroy', [$platform, $account]) }}"
method="POST" class="inline"
onsubmit="return confirm('{{ trans('general.delete_confirm') }}')">
@csrf
@method('DELETE')
<button type="submit" class="text-red-600 hover:underline">{{ trans('general.delete') }}</button>
</form>
@endcan
</td>
</tr>
@endforeach
@endif
</tbody>
</table>
</div>
@unless ($accounts->isEmpty())
<div class="mt-5">{{ $accounts->links() }}</div>
@endunless
</div>
</x-slot>
</x-layouts.admin>

View File

@ -0,0 +1,161 @@
<!-- 嵌入在业务页面中的表格视图(无完整布局包裹) -->
<style>
.bill-table-wrapper { overflow-x: auto; border-radius: 0.5rem; border: 1px solid #e5e7eb; }
.bill-table { min-width: 100%; white-space: nowrap; font-size: 0.875rem; }
.bill-table thead th { position: sticky; top: 0; background: #f9fafb; text-align: left; color: #6b7280; padding: 0.65rem 0.75rem; border-bottom: 1px solid #e5e7eb; font-weight: 600; }
.bill-table tbody td { padding: 0.65rem 0.75rem; border-bottom: 1px solid #f3f4f6; }
.bill-table tbody tr:hover { background: #f9fafb; }
.amount-cell { text-align: right; font-variant-numeric: tabular-nums; }
</style>
@php $pageAccountIds = $accounts->pluck('id')->map(fn($id) => (string) $id)->values(); @endphp
<div class="w-full" x-data="{ selected: [] }">
<form id="marketing-account-bills-export-form" method="GET"
action="{{ route('marketing-account-bills.export', $platform) }}"></form>
{{-- 操作工具栏 --}}
<div class="mb-4 flex flex-wrap items-end justify-between gap-3 border-b border-gray-200 pb-4">
@can('create-banking-accounts')
<div class="flex flex-wrap items-end gap-3">
<a href="{{ route('marketing-account-bills.create', $platform) }}"
class="inline-flex h-10 items-center gap-2 rounded-lg bg-green px-4 text-sm text-white hover:bg-green-700">
<span class="material-icons text-base" aria-hidden="true">add</span>
{{ trans('marketing_account_bills.add_bill') }}
</a>
<form method="POST" action="{{ route('marketing-account-bills.import', $platform) }}"
enctype="multipart/form-data" class="flex flex-wrap items-end gap-3">
@csrf
<div>
<label for="bills-import-embedded" class="mb-1 block text-xs font-medium text-gray-600">
{{ trans('marketing_account_bills.choose_file') }}
</label>
<input id="bills-import-embedded" type="file" name="import"
accept=".xlsx,.xls,.csv" required
class="block max-w-xs text-sm text-gray-600" />
</div>
<button type="submit"
class="inline-flex h-10 items-center gap-2 rounded-lg bg-green px-4 text-sm text-white hover:bg-green-700">
<span class="material-icons text-base" aria-hidden="true">upload_file</span>
{{ trans('marketing_account_bills.bulk_upload') }}
</button>
</form>
<a href="{{ route('marketing-account-bills.template', $platform) }}"
class="inline-flex h-10 items-center gap-2 rounded-lg border border-gray-300 bg-white px-4 text-sm text-gray-700 hover:bg-gray-50">
<span class="material-icons text-base" aria-hidden="true">file_download</span>
{{ trans('marketing_account_bills.download_template') }}
</a>
</div>
@endcan
<div class="flex items-center gap-3">
@can('update-banking-accounts')
<button type="submit" form="marketing-account-bills-export-form"
class="inline-flex h-10 items-center gap-2 rounded-lg border border-gray-300 bg-white px-4 text-sm text-gray-700 hover:bg-gray-50">
<span class="material-icons text-base" aria-hidden="true">download</span>
{{ trans('marketing_account_bills.bulk_download') }}
</button>
@endcan
</div>
</div>
@if ($errors->has('import'))
<div class="mb-4 border-l-4 border-red-500 bg-red-50 px-4 py-3 text-sm text-red-700">
@foreach ($errors->get('import') as $message)
<p>{{ $message }}</p>
@endforeach
</div>
@endif
{{-- 15列数据表格 --}}
<div class="bill-table-wrapper">
<table class="bill-table divide-y divide-gray-200">
<thead>
<tr>
<th class="w-12 text-center">
<input type="checkbox"
aria-label="{{ trans('marketing_account_bills.select_all') }}"
@if ($pageAccountIds->isEmpty()) disabled @endif
x-on:change="selected = $event.target.checked ? @js($pageAccountIds) : []"
x-bind:checked="selected.length === {{ $pageAccountIds->count() }} && selected.length > 0"
class="h-4 w-4 rounded border-gray-300 text-green" />
</th>
<th>{{ trans('marketing_account_bills.store_name') }}</th>
<th>{{ trans('marketing_account_bills.bill_date') }}</th>
<th class="text-right">{{ trans('marketing_account_bills.total_deposit') }}</th>
<th class="text-right">{{ trans('marketing_account_bills.total_transfer_in') }}</th>
<th class="text-right">{{ trans('marketing_account_bills.total_transfer_out') }}</th>
<th class="text-right">{{ trans('marketing_account_bills.non_grant_consumption') }}</th>
<th class="text-right">{{ trans('marketing_account_bills.grant_consumption') }}</th>
<th class="text-right">{{ trans('marketing_account_bills.rebate_redpack_consumption') }}</th>
<th class="text-right">{{ trans('marketing_account_bills.discount_redpack_consumption') }}</th>
<th class="text-right">{{ trans('marketing_account_bills.shared_wallet_consumption') }}</th>
<th class="text-right">{{ trans('marketing_account_bills.shared_grant_consumption') }}</th>
<th class="text-right">{{ trans('marketing_account_bills.daily_total_amount') }}</th>
<th class="text-right">{{ trans('marketing_account_bills.period_end_balance') }}</th>
<th class="text-right">{{ trans('marketing_account_bills.grant_balance') }}</th>
<th class="text-right">{{ trans('marketing_account_bills.non_grant_balance') }}</th>
<th>{{ trans('marketing_account_bills.notes') }}</th>
<th class="text-right">{{ trans('general.actions') }}</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100 bg-white">
@if ($accounts->isEmpty())
<tr>
<td colspan="18" class="px-5 py-10 text-center text-gray-400">
{{ trans('marketing_account_bills.no_records') }}
</td>
</tr>
@else
@foreach ($accounts as $account)
<tr>
<td class="text-center">
<input type="checkbox" name="ids[]" value="{{ $account->id }}"
form="marketing-account-bills-export-form"
x-model="selected"
aria-label="{{ trans('marketing_account_bills.select_bill') }}"
class="h-4 w-4 rounded border-gray-300 text-green" />
</td>
<td>{{ $account->store_name }}</td>
<td>{{ $account->bill_date->format('Y-m-d') }}</td>
<td class="amount-cell">{{ number_format($account->total_deposit, 2) }}</td>
<td class="amount-cell">{{ number_format($account->total_transfer_in, 2) }}</td>
<td class="amount-cell">{{ number_format($account->total_transfer_out, 2) }}</td>
<td class="amount-cell">{{ number_format($account->non_grant_consumption, 2) }}</td>
<td class="amount-cell">{{ number_format($account->grant_consumption, 2) }}</td>
<td class="amount-cell">{{ number_format($account->rebate_redpack_consumption, 2) }}</td>
<td class="amount-cell">{{ number_format($account->discount_redpack_consumption, 2) }}</td>
<td class="amount-cell">{{ number_format($account->shared_wallet_consumption, 2) }}</td>
<td class="amount-cell">{{ number_format($account->shared_grant_consumption, 2) }}</td>
<td class="amount-cell">{{ number_format($account->daily_total_amount, 2) }}</td>
<td class="amount-cell font-semibold">{{ number_format($account->period_end_balance, 2) }}</td>
<td class="amount-cell">{{ number_format($account->grant_balance, 2) }}</td>
<td class="amount-cell">{{ number_format($account->non_grant_balance, 2) }}</td>
<td class="max-w-xs truncate" title="{{ $account->notes }}">
{{ \Illuminate\Support\Str::limit($account->notes, 20) }}
</td>
<td class="text-right whitespace-nowrap">
@can('update-banking-accounts')
<a href="{{ route('marketing-account-bills.edit', [$platform, $account]) }}"
class="text-purple hover:underline mr-3">{{ trans('general.edit') }}</a>
@endcan
@can('delete-banking-accounts')
<form action="{{ route('marketing-account-bills.destroy', [$platform, $account]) }}"
method="POST" class="inline"
onsubmit="return confirm('{{ trans('general.delete_confirm') }}')">
@csrf
@method('DELETE')
<button type="submit" class="text-red-600 hover:underline">{{ trans('general.delete') }}</button>
</form>
@endcan
</td>
</tr>
@endforeach
@endif
</tbody>
</table>
</div>
@unless ($accounts->isEmpty())
<div class="mt-5">{{ $accounts->links() }}</div>
@endunless
</div>

View File

@ -0,0 +1,50 @@
<x-layouts.admin>
<x-slot name="title">
{{ trans('marketing_account_bills.title') }}
</x-slot>
<x-slot name="content">
<style>
#main-body > .container {
max-width: none;
width: auto;
margin-left: 1rem;
margin-right: 1rem;
}
@media (min-width: 1280px) {
#main-body > .container {
margin-left: 3rem;
margin-right: 1.5rem;
}
}
</style>
<div class="my-5">
<h2 class="mb-6 text-xl font-semibold text-gray-800">{{ trans('marketing_account_bills.title') }}</h2>
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
@foreach ($platforms as $slug => $labelKey)
@php
$count = \App\Models\Banking\MarketingAccountBill::platform($slug)->count();
@endphp
<a href="{{ route('marketing-account-bills.platform.index', $slug) }}"
class="group block rounded-xl border border-gray-200 bg-white p-6 shadow-sm transition hover:border-green-400 hover:shadow-md">
<div class="flex items-center justify-between">
<div>
<h3 class="text-base font-semibold text-gray-800 group-hover:text-green-700">
{{ trans('marketing_account_bills.' . $labelKey) }}
</h3>
<p class="mt-1 text-sm text-gray-500">
{{ $count }} {{ trans_choice('general.records', $count) }}
</p>
</div>
<span class="material-icons-outlined text-3xl text-gray-300 group-hover:text-green-500" aria-hidden="true">
account_balance_wallet
</span>
</div>
</a>
@endforeach
</div>
</div>
</x-slot>
</x-layouts.admin>

View File

@ -0,0 +1,113 @@
@php
$isEdit = $bill->exists;
@endphp
<x-layouts.admin>
<x-slot name="title">
{{ $isEdit ? trans('marketing_pinduoduo_bills.edit_bill') : trans('marketing_pinduoduo_bills.add_bill') }}
- {{ trans('marketing_pinduoduo_bills.title') }}
</x-slot>
<x-slot name="content">
<x-form.container>
<form method="POST"
action="{{ $isEdit ? route('marketing-pinduoduo-bills.update', $bill) : route('marketing-pinduoduo-bills.store') }}"
class="space-y-6">
@csrf
@if ($isEdit)
@method('PATCH')
@endif
{{-- 交易信息 --}}
<div class="rounded-lg border border-gray-200 bg-gray-50 px-5 py-4">
<h3 class="mb-4 text-sm font-semibold text-gray-700">交易信息</h3>
<div class="grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
<div>
<label class="mb-1 block text-xs font-medium text-gray-600">
{{ trans('marketing_pinduoduo_bills.store_name') }} <span class="text-red-500">*</span>
</label>
<input name="store_name" value="{{ old('store_name', $bill->store_name) }}" required
class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-green focus:ring-green" />
</div>
<div>
<label class="mb-1 block text-xs font-medium text-gray-600">
{{ trans('marketing_pinduoduo_bills.transaction_time') }} <span class="text-red-500">*</span>
</label>
<input type="datetime-local" name="transaction_time"
value="{{ old('transaction_time', $bill->transaction_time ? $bill->transaction_time->format('Y-m-d\TH:i') : now()->format('Y-m-d\TH:i')) }}" required
class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-green focus:ring-green" />
</div>
</div>
</div>
{{-- 资金与流水类型 --}}
<div class="rounded-lg border border-gray-200 bg-white px-5 py-4">
<h3 class="mb-4 text-sm font-semibold text-gray-700">资金与流水类型</h3>
<div class="grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
<div>
<label class="mb-1 block text-xs font-medium text-gray-600">
{{ trans('marketing_pinduoduo_bills.fund_type') }} <span class="text-red-500">*</span>
</label>
<input name="fund_type" value="{{ old('fund_type', $bill->fund_type) }}" required
placeholder="如:可用余额、保证金等"
class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-green focus:ring-green" />
</div>
<div>
<label class="mb-1 block text-xs font-medium text-gray-600">
{{ trans('marketing_pinduoduo_bills.flow_type') }}
</label>
<input name="flow_type" value="{{ old('flow_type', $bill->flow_type) }}"
placeholder="如:充值、提现、扣款等"
class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-green focus:ring-green" />
</div>
</div>
</div>
{{-- 金额信息 --}}
<div class="rounded-lg border border-gray-200 bg-white px-5 py-4">
<h3 class="mb-4 text-sm font-semibold text-gray-700">金额信息(元)</h3>
<div class="grid gap-5 sm:grid-cols-2">
<div>
<label class="mb-1 block text-xs font-medium text-gray-600">
{{ trans('marketing_pinduoduo_bills.transaction_amount') }} <span class="text-red-500">*</span>
</label>
<input type="number" step="0.01" name="transaction_amount"
value="{{ old('transaction_amount', $bill->transaction_amount) }}" required
class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm text-right focus:border-green focus:ring-green" />
</div>
<div>
<label class="mb-1 block text-xs font-medium text-gray-600">
{{ trans('marketing_pinduoduo_bills.balance') }}
</label>
<input type="number" step="0.01" name="balance"
value="{{ old('balance', $bill->balance) }}"
class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm text-right focus:border-green focus:ring-green" />
</div>
</div>
</div>
{{-- 交易摘要 --}}
<div class="rounded-lg border border-gray-200 bg-white px-5 py-4">
<div>
<label class="mb-1 block text-xs font-medium text-gray-600">
{{ trans('marketing_pinduoduo_bills.transaction_summary') }}
</label>
<textarea name="transaction_summary" rows="3"
class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-green focus:ring-green">{{ old('transaction_summary', $bill->transaction_summary) }}</textarea>
</div>
</div>
<div class="flex justify-end gap-4">
<a href="{{ route('marketing-pinduoduo-bills.index') }}"
class="px-5 py-2 text-gray-600 text-sm">
{{ trans('general.cancel') }}
</a>
<button type="submit"
class="rounded-lg bg-green px-6 py-2 text-sm text-white hover:bg-green-700">
{{ trans('general.save') }}
</button>
</div>
</form>
</x-form.container>
</x-slot>
</x-layouts.admin>

View File

@ -0,0 +1,9 @@
<x-layouts.admin>
<x-slot name="title">
{{ trans('marketing_pinduoduo_bills.title') }}
</x-slot>
<x-slot name="content">
@include('banking.marketing_pinduoduo_bills.partials.embedded-table')
</x-slot>
</x-layouts.admin>

View File

@ -0,0 +1,143 @@
<!-- 拼多多账户 - 嵌入在业务页面中的表格视图 -->
<style>
.bill-table-wrapper { overflow-x: auto; border-radius: 0.5rem; border: 1px solid #e5e7eb; }
.bill-table { min-width: 100%; white-space: nowrap; font-size: 0.875rem; }
.bill-table thead th { position: sticky; top: 0; background: #f9fafb; text-align: left; color: #6b7280; padding: 0.65rem 0.75rem; border-bottom: 1px solid #e5e7eb; font-weight: 600; }
.bill-table tbody td { padding: 0.65rem 0.75rem; border-bottom: 1px solid #f3f4f6; }
.bill-table tbody tr:hover { background: #f9fafb; }
.amount-cell { text-align: right; font-variant-numeric: tabular-nums; }
</style>
@php $pageBillIds = $bills->pluck('id')->map(fn($id) => (string) $id)->values(); @endphp
<div class="w-full" x-data="{ selected: [] }">
<form id="marketing-pinduoduo-bills-export-form" method="GET"
action="{{ route('marketing-pinduoduo-bills.export') }}"></form>
{{-- 操作工具栏 --}}
<div class="mb-4 flex flex-wrap items-end justify-between gap-3 border-b border-gray-200 pb-4">
@can('create-banking-accounts')
<div class="flex flex-wrap items-end gap-3">
<a href="{{ route('marketing-pinduoduo-bills.create') }}"
class="inline-flex h-10 items-center gap-2 rounded-lg bg-green px-4 text-sm text-white hover:bg-green-700">
<span class="material-icons text-base" aria-hidden="true">add</span>
{{ trans('marketing_pinduoduo_bills.add_bill') }}
</a>
<form method="POST" action="{{ route('marketing-pinduoduo-bills.import') }}"
enctype="multipart/form-data" class="flex flex-wrap items-end gap-3">
@csrf
<div>
<label for="pdd-import-embedded" class="mb-1 block text-xs font-medium text-gray-600">
{{ trans('marketing_pinduoduo_bills.choose_file') }}
</label>
<input id="pdd-import-embedded" type="file" name="import"
accept=".xlsx,.xls,.csv" required
class="block max-w-xs text-sm text-gray-600" />
</div>
<button type="submit"
class="inline-flex h-10 items-center gap-2 rounded-lg bg-green px-4 text-sm text-white hover:bg-green-700">
<span class="material-icons text-base" aria-hidden="true">upload_file</span>
{{ trans('marketing_pinduoduo_bills.bulk_upload') }}
</button>
</form>
<a href="{{ route('marketing-pinduoduo-bills.template') }}"
class="inline-flex h-10 items-center gap-2 rounded-lg border border-gray-300 bg-white px-4 text-sm text-gray-700 hover:bg-gray-50">
<span class="material-icons text-base" aria-hidden="true">file_download</span>
{{ trans('marketing_pinduoduo_bills.download_template') }}
</a>
</div>
@endcan
<div class="flex items-center gap-3">
@can('update-banking-accounts')
<button type="submit" form="marketing-pinduoduo-bills-export-form"
class="inline-flex h-10 items-center gap-2 rounded-lg border border-gray-300 bg-white px-4 text-sm text-gray-700 hover:bg-gray-50">
<span class="material-icons text-base" aria-hidden="true">download</span>
{{ trans('marketing_pinduoduo_bills.bulk_download') }}
</button>
@endcan
</div>
</div>
@if ($errors->has('import'))
<div class="mb-4 border-l-4 border-red-500 bg-red-50 px-4 py-3 text-sm text-red-700">
@foreach ($errors->get('import') as $message)
<p>{{ $message }}</p>
@endforeach
</div>
@endif
{{-- 7列数据表格 --}}
<div class="bill-table-wrapper">
<table class="bill-table divide-y divide-gray-200">
<thead>
<tr>
<th class="w-12 text-center">
<input type="checkbox"
aria-label="{{ trans('marketing_pinduoduo_bills.select_all') }}"
@if ($pageBillIds->isEmpty()) disabled @endif
x-on:change="selected = $event.target.checked ? @js($pageBillIds) : []"
x-bind:checked="selected.length === {{ $pageBillIds->count() }} && selected.length > 0"
class="h-4 w-4 rounded border-gray-300 text-green" />
</th>
<th>{{ trans('marketing_pinduoduo_bills.store_name') }}</th>
<th>{{ trans('marketing_pinduoduo_bills.transaction_time') }}</th>
<th>{{ trans('marketing_pinduoduo_bills.fund_type') }}</th>
<th>{{ trans('marketing_pinduoduo_bills.flow_type') }}</th>
<th class="text-right">{{ trans('marketing_pinduoduo_bills.transaction_amount') }}</th>
<th class="text-right">{{ trans('marketing_pinduoduo_bills.balance') }}</th>
<th>{{ trans('marketing_pinduoduo_bills.transaction_summary') }}</th>
<th class="text-right">{{ trans('general.actions') }}</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100 bg-white">
@if ($bills->isEmpty())
<tr>
<td colspan="9" class="px-5 py-10 text-center text-gray-400">
{{ trans('marketing_pinduoduo_bills.no_records') }}
</td>
</tr>
@else
@foreach ($bills as $bill)
<tr>
<td class="text-center">
<input type="checkbox" name="ids[]" value="{{ $bill->id }}"
form="marketing-pinduoduo-bills-export-form"
x-model="selected"
aria-label="{{ trans('marketing_pinduoduo_bills.select_bill') }}"
class="h-4 w-4 rounded border-gray-300 text-green" />
</td>
<td>{{ $bill->store_name }}</td>
<td>{{ $bill->transaction_time?->format('Y-m-d H:i:s') }}</td>
<td>{{ $bill->fund_type }}</td>
<td>{{ $bill->flow_type }}</td>
<td class="amount-cell font-semibold">{{ number_format($bill->transaction_amount, 2) }}</td>
<td class="amount-cell">{{ number_format($bill->balance, 2) }}</td>
<td class="max-w-xs truncate" title="{{ $bill->transaction_summary }}">
{{ \Illuminate\Support\Str::limit($bill->transaction_summary, 20) }}
</td>
<td class="text-right whitespace-nowrap">
@can('update-banking-accounts')
<a href="{{ route('marketing-pinduoduo-bills.edit', $bill) }}"
class="text-purple hover:underline mr-3">{{ trans('general.edit') }}</a>
@endcan
@can('delete-banking-accounts')
<form action="{{ route('marketing-pinduoduo-bills.destroy', $bill) }}"
method="POST" class="inline"
onsubmit="return confirm('{{ trans('general.delete_confirm') }}')">
@csrf
@method('DELETE')
<button type="submit" class="text-red-600 hover:underline">{{ trans('general.delete') }}</button>
</form>
@endcan
</td>
</tr>
@endforeach
@endif
</tbody>
</table>
</div>
@unless ($bills->isEmpty())
<div class="mt-5">{{ $bills->links() }}</div>
@endunless
</div>

View File

@ -0,0 +1,126 @@
@php
$isEdit = $bill->exists;
@endphp
<x-layouts.admin>
<x-slot name="title">
{{ $isEdit ? trans('marketing_taobao_alipay_bills.edit_bill') : trans('marketing_taobao_alipay_bills.add_bill') }}
- {{ trans('marketing_taobao_alipay_bills.title') }}
</x-slot>
<x-slot name="content">
<x-form.container>
<form method="POST"
action="{{ $isEdit ? route('marketing-taobao-alipay-bills.update', $bill) : route('marketing-taobao-alipay-bills.store') }}"
class="space-y-6">
@csrf
@if ($isEdit)
@method('PATCH')
@endif
{{-- 交易信息 --}}
<div class="rounded-lg border border-gray-200 bg-gray-50 px-5 py-4">
<h3 class="mb-4 text-sm font-semibold text-gray-700">交易信息</h3>
<div class="grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
<div>
<label class="mb-1 block text-xs font-medium text-gray-600">
{{ trans('marketing_taobao_alipay_bills.store_name') }} <span class="text-red-500">*</span>
</label>
<input name="store_name" value="{{ old('store_name', $bill->store_name) }}" required
class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-green focus:ring-green" />
</div>
<div>
<label class="mb-1 block text-xs font-medium text-gray-600">
{{ trans('marketing_taobao_alipay_bills.transaction_date') }} <span class="text-red-500">*</span>
</label>
<input type="date" name="transaction_date"
value="{{ old('transaction_date', $bill->transaction_date ? $bill->transaction_date->toDateString() : now()->toDateString()) }}" required
class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-green focus:ring-green" />
</div>
<div>
<label class="mb-1 block text-xs font-medium text-gray-600">
{{ trans('marketing_taobao_alipay_bills.bookkeeping_time') }}
</label>
<input type="datetime-local" name="bookkeeping_time"
value="{{ old('bookkeeping_time', $bill->bookkeeping_time ? $bill->bookkeeping_time->format('Y-m-d\TH:i') : now()->format('Y-m-d\TH:i')) }}"
class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-green focus:ring-green" />
</div>
</div>
</div>
{{-- 收支明细 --}}
<div class="rounded-lg border border-gray-200 bg-white px-5 py-4">
<h3 class="mb-4 text-sm font-semibold text-gray-700">收支明细</h3>
<div class="grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
<div>
<label class="mb-1 block text-xs font-medium text-gray-600">
{{ trans('marketing_taobao_alipay_bills.income_expense_type') }} <span class="text-red-500">*</span>
</label>
<select name="income_expense_type" required
class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-green focus:ring-green">
<option value="收入" @if(old('income_expense_type', $bill->income_expense_type) === '收入') selected @endif>
{{ trans('marketing_taobao_alipay_bills.income') }}
</option>
<option value="支出" @if(old('income_expense_type', $bill->income_expense_type) === '支出') selected @endif>
{{ trans('marketing_taobao_alipay_bills.expense') }}
</option>
</select>
</div>
<div>
<label class="mb-1 block text-xs font-medium text-gray-600">
{{ trans('marketing_taobao_alipay_bills.transaction_type') }}
</label>
<input name="transaction_type" value="{{ old('transaction_type', $bill->transaction_type) }}"
class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-green focus:ring-green" />
</div>
</div>
</div>
{{-- 金额信息 --}}
<div class="rounded-lg border border-gray-200 bg-white px-5 py-4">
<h3 class="mb-4 text-sm font-semibold text-gray-700">金额信息(元)</h3>
<div class="grid gap-5 sm:grid-cols-2">
<div>
<label class="mb-1 block text-xs font-medium text-gray-600">
{{ trans('marketing_taobao_alipay_bills.operation_amount') }} <span class="text-red-500">*</span>
</label>
<input type="number" step="0.01" name="operation_amount"
value="{{ old('operation_amount', $bill->operation_amount) }}" required
class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm text-right focus:border-green focus:ring-green" />
</div>
<div>
<label class="mb-1 block text-xs font-medium text-gray-600">
{{ trans('marketing_taobao_alipay_bills.balance_after') }}
</label>
<input type="number" step="0.01" name="balance_after"
value="{{ old('balance_after', $bill->balance_after) }}"
class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm text-right focus:border-green focus:ring-green" />
</div>
</div>
</div>
{{-- 备注 --}}
<div class="rounded-lg border border-gray-200 bg-white px-5 py-4">
<div>
<label class="mb-1 block text-xs font-medium text-gray-600">
{{ trans('marketing_taobao_alipay_bills.notes') }}
</label>
<textarea name="notes" rows="3"
class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-green focus:ring-green">{{ old('notes', $bill->notes) }}</textarea>
</div>
</div>
<div class="flex justify-end gap-4">
<a href="{{ route('marketing-taobao-alipay-bills.index') }}"
class="px-5 py-2 text-gray-600 text-sm">
{{ trans('general.cancel') }}
</a>
<button type="submit"
class="rounded-lg bg-green px-6 py-2 text-sm text-white hover:bg-green-700">
{{ trans('general.save') }}
</button>
</div>
</form>
</x-form.container>
</x-slot>
</x-layouts.admin>

View File

@ -0,0 +1,9 @@
<x-layouts.admin>
<x-slot name="title">
{{ trans('marketing_taobao_alipay_bills.title') }}
</x-slot>
<x-slot name="content">
@include('banking.marketing_taobao_alipay_bills.partials.embedded-table')
</x-slot>
</x-layouts.admin>

View File

@ -0,0 +1,153 @@
<!-- 淘系支付宝账户 - 嵌入在业务页面中的表格视图 -->
<style>
.bill-table-wrapper { overflow-x: auto; border-radius: 0.5rem; border: 1px solid #e5e7eb; }
.bill-table { min-width: 100%; white-space: nowrap; font-size: 0.875rem; }
.bill-table thead th { position: sticky; top: 0; background: #f9fafb; text-align: left; color: #6b7280; padding: 0.65rem 0.75rem; border-bottom: 1px solid #e5e7eb; font-weight: 600; }
.bill-table tbody td { padding: 0.65rem 0.75rem; border-bottom: 1px solid #f3f4f6; }
.bill-table tbody tr:hover { background: #f9fafb; }
.amount-cell { text-align: right; font-variant-numeric: tabular-nums; }
.type-income { color: #16a34a; font-weight: 600; }
.type-expense { color: #dc2626; font-weight: 600; }
</style>
@php $pageBillIds = $bills->pluck('id')->map(fn($id) => (string) $id)->values(); @endphp
<div class="w-full" x-data="{ selected: [] }">
<form id="marketing-taobao-alipay-bills-export-form" method="GET"
action="{{ route('marketing-taobao-alipay-bills.export') }}"></form>
{{-- 操作工具栏 --}}
<div class="mb-4 flex flex-wrap items-end justify-between gap-3 border-b border-gray-200 pb-4">
@can('create-banking-accounts')
<div class="flex flex-wrap items-end gap-3">
<a href="{{ route('marketing-taobao-alipay-bills.create') }}"
class="inline-flex h-10 items-center gap-2 rounded-lg bg-green px-4 text-sm text-white hover:bg-green-700">
<span class="material-icons text-base" aria-hidden="true">add</span>
{{ trans('marketing_taobao_alipay_bills.add_bill') }}
</a>
<form method="POST" action="{{ route('marketing-taobao-alipay-bills.import') }}"
enctype="multipart/form-data" class="flex flex-wrap items-end gap-3">
@csrf
<div>
<label for="bills-import-embedded" class="mb-1 block text-xs font-medium text-gray-600">
{{ trans('marketing_taobao_alipay_bills.choose_file') }}
</label>
<input id="bills-import-embedded" type="file" name="import"
accept=".xlsx,.xls,.csv" required
class="block max-w-xs text-sm text-gray-600" />
</div>
<button type="submit"
class="inline-flex h-10 items-center gap-2 rounded-lg bg-green px-4 text-sm text-white hover:bg-green-700">
<span class="material-icons text-base" aria-hidden="true">upload_file</span>
{{ trans('marketing_taobao_alipay_bills.bulk_upload') }}
</button>
</form>
<a href="{{ route('marketing-taobao-alipay-bills.template') }}"
class="inline-flex h-10 items-center gap-2 rounded-lg border border-gray-300 bg-white px-4 text-sm text-gray-700 hover:bg-gray-50">
<span class="material-icons text-base" aria-hidden="true">file_download</span>
{{ trans('marketing_taobao_alipay_bills.download_template') }}
</a>
</div>
@endcan
<div class="flex items-center gap-3">
@can('update-banking-accounts')
<button type="submit" form="marketing-taobao-alipay-bills-export-form"
class="inline-flex h-10 items-center gap-2 rounded-lg border border-gray-300 bg-white px-4 text-sm text-gray-700 hover:bg-gray-50">
<span class="material-icons text-base" aria-hidden="true">download</span>
{{ trans('marketing_taobao_alipay_bills.bulk_download') }}
</button>
@endcan
</div>
</div>
@if ($errors->has('import'))
<div class="mb-4 border-l-4 border-red-500 bg-red-50 px-4 py-3 text-sm text-red-700">
@foreach ($errors->get('import') as $message)
<p>{{ $message }}</p>
@endforeach
</div>
@endif
{{-- 8列数据表格 --}}
<div class="bill-table-wrapper">
<table class="bill-table divide-y divide-gray-200">
<thead>
<tr>
<th class="w-12 text-center">
<input type="checkbox"
aria-label="{{ trans('marketing_taobao_alipay_bills.select_all') }}"
@if ($pageBillIds->isEmpty()) disabled @endif
x-on:change="selected = $event.target.checked ? @js($pageBillIds) : []"
x-bind:checked="selected.length === {{ $pageBillIds->count() }} && selected.length > 0"
class="h-4 w-4 rounded border-gray-300 text-green" />
</th>
<th>{{ trans('marketing_taobao_alipay_bills.store_name') }}</th>
<th>{{ trans('marketing_taobao_alipay_bills.transaction_date') }}</th>
<th>{{ trans('marketing_taobao_alipay_bills.bookkeeping_time') }}</th>
<th>{{ trans('marketing_taobao_alipay_bills.income_expense_type') }}</th>
<th>{{ trans('marketing_taobao_alipay_bills.transaction_type') }}</th>
<th class="text-right">{{ trans('marketing_taobao_alipay_bills.operation_amount') }}</th>
<th class="text-right">{{ trans('marketing_taobao_alipay_bills.balance_after') }}</th>
<th>{{ trans('marketing_taobao_alipay_bills.notes') }}</th>
<th class="text-right">{{ trans('general.actions') }}</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100 bg-white">
@if ($bills->isEmpty())
<tr>
<td colspan="10" class="px-5 py-10 text-center text-gray-400">
{{ trans('marketing_taobao_alipay_bills.no_records') }}
</td>
</tr>
@else
@foreach ($bills as $bill)
<tr>
<td class="text-center">
<input type="checkbox" name="ids[]" value="{{ $bill->id }}"
form="marketing-taobao-alipay-bills-export-form"
x-model="selected"
aria-label="{{ trans('marketing_taobao_alipay_bills.select_bill') }}"
class="h-4 w-4 rounded border-gray-300 text-green" />
</td>
<td>{{ $bill->store_name }}</td>
<td>{{ $bill->transaction_date->format('Y-m-d') }}</td>
<td>{{ $bill->bookkeeping_time?->format('Y-m-d H:i:s') }}</td>
<td>
@if ($bill->income_expense_type === '收入' || $bill->income_expense_type === 'income')
<span class="type-income">{{ $bill->income_expense_type }}</span>
@else
<span class="type-expense">{{ $bill->income_expense_type }}</span>
@endif
</td>
<td>{{ $bill->transaction_type }}</td>
<td class="amount-cell font-semibold">{{ number_format($bill->operation_amount, 2) }}</td>
<td class="amount-cell">{{ number_format($bill->balance_after, 2) }}</td>
<td class="max-w-xs truncate" title="{{ $bill->notes }}">
{{ \Illuminate\Support\Str::limit($bill->notes, 20) }}
</td>
<td class="text-right whitespace-nowrap">
@can('update-banking-accounts')
<a href="{{ route('marketing-taobao-alipay-bills.edit', $bill) }}"
class="text-purple hover:underline mr-3">{{ trans('general.edit') }}</a>
@endcan
@can('delete-banking-accounts')
<form action="{{ route('marketing-taobao-alipay-bills.destroy', $bill) }}"
method="POST" class="inline"
onsubmit="return confirm('{{ trans('general.delete_confirm') }}')">
@csrf
@method('DELETE')
<button type="submit" class="text-red-600 hover:underline">{{ trans('general.delete') }}</button>
</form>
@endcan
</td>
</tr>
@endforeach
@endif
</tbody>
</table>
</div>
@unless ($bills->isEmpty())
<div class="mt-5">{{ $bills->links() }}</div>
@endunless
</div>

View File

@ -63,6 +63,14 @@
background: #ffffff;
}
.business-content-section {
margin-top: 1.5rem;
padding: 1.5rem;
border: 1px solid #eef0f4;
border-radius: 1rem;
background: #ffffff;
}
@media (min-width: 1280px) {
#main-body > .container {
margin-left: 2.5rem;
@ -96,7 +104,13 @@
</nav>
@endif
@if ($page && !empty($content) && !empty($content['view']))
<section class="business-content-section" aria-label="{{ $activeTab['title'] ?? ($page['title'] ?? $section['title']) }}">
@include($content['view'], $content)
</section>
@else
<section class="business-blank-page" aria-label="{{ $activeTab['title'] ?? ($page['title'] ?? $section['title']) }}"></section>
@endif
</div>
</x-slot>
</x-layouts.admin>

View File

@ -0,0 +1,111 @@
<div id="advance-detail-app" class="advance-detail-page">
<div v-cloak>
<div class="bg-yellow-50 border border-yellow-200 rounded-lg p-3 mb-4 text-sm text-yellow-800">
<i class="el-icon-warning mr-1"></i>
借支明细数据源 finance_advanceLoans 当前仅 4 行数据;表头 21 列中部分字段(如实际打款金额、分期扣款、票据等)在数据库中无对应列,已显示为占位符。
</div>
{{-- 统计卡片 --}}
<div class="grid grid-cols-2 gap-4 mb-6">
<div class="bg-white rounded-xl border border-gray-100 p-5 shadow-sm">
<div class="text-sm text-gray-500 mb-1">借支总金额</div>
<div class="text-2xl font-bold text-gray-800">@{{ formatMoney(summary.totalAmount) }}</div>
</div>
<div class="bg-white rounded-xl border border-gray-100 p-5 shadow-sm">
<div class="text-sm text-gray-500 mb-1">总记录数</div>
<div class="text-2xl font-bold text-indigo-600">@{{ (summary.totalCount || 0).toLocaleString() }}</div>
</div>
</div>
{{-- 筛选栏 --}}
<div class="bg-white rounded-xl border border-gray-100 p-4 mb-4 shadow-sm">
<div class="flex flex-wrap items-center gap-3">
<el-input v-model="filters.supplier" placeholder="供应商" size="small" style="width: 200px" clearable @clear="onFilterChange" @keyup.enter="onFilterChange"></el-input>
<el-date-picker v-model="filters.month_range" type="monthrange"
start-placeholder="月份开始" end-placeholder="月份结束" size="small"
style="width: 240px" value-format="yyyy-MM" @change="onFilterChange">
</el-date-picker>
<el-button size="small" type="primary" @click="onFilterChange">查询</el-button>
<el-button size="small" @click="onReset">重置</el-button>
</div>
</div>
{{-- 数据表格 --}}
<div class="bg-white rounded-xl border border-gray-100 shadow-sm overflow-hidden">
<div v-if="loading" class="flex items-center justify-center py-20">
<span class="text-gray-400">加载中...</span>
</div>
<el-table v-else :data="tableData" stripe border style="width: 100%" size="small" height="600">
<el-table-column type="index" label="#" width="50" fixed="left"></el-table-column>
<el-table-column prop="supplier" label="供应商" width="140" fixed="left" show-overflow-tooltip></el-table-column>
<el-table-column label="协议签订名称" width="140">
<template slot-scope="scope"><span class="text-gray-400">@{{ scope.row.contract_name || '-' }}</span></template>
</el-table-column>
<el-table-column prop="advance_amount" label="借支金额-借款协议" width="140" align="right">
<template slot-scope="scope">
<span :class="scope.row.advance_amount < 0 ? 'text-red-600' : 'text-gray-800'">@{{ formatMoney(scope.row.advance_amount) }}</span>
</template>
</el-table-column>
<el-table-column label="实际打款金额" width="120" align="right">
<template slot-scope="scope"><span class="text-gray-400">@{{ formatMoney(scope.row.actual_pay) }}</span></template>
</el-table-column>
<el-table-column label="本月扣款金额" width="120" align="right">
<template slot-scope="scope"><span class="text-gray-400">@{{ formatMoney(scope.row.month_deduct) }}</span></template>
</el-table-column>
<el-table-column label="应扣金额" width="120" align="right">
<template slot-scope="scope"><span class="text-gray-400">@{{ formatMoney(scope.row.should_deduct) }}</span></template>
</el-table-column>
<el-table-column label="分期起始扣款月份" width="130">
<template slot-scope="scope"><span class="text-gray-400">@{{ scope.row.deduct_start_month || '-' }}</span></template>
</el-table-column>
<el-table-column label="分期最后次扣款月份" width="130">
<template slot-scope="scope"><span class="text-gray-400">@{{ scope.row.deduct_end_month || '-' }}</span></template>
</el-table-column>
<el-table-column prop="remain_periods" label="分期剩余期数" width="110" align="center"></el-table-column>
<el-table-column prop="month_str" label="借支日期" width="110"></el-table-column>
<el-table-column label="借款期限" width="100">
<template slot-scope="scope"><span class="text-gray-400">@{{ scope.row.loan_term || '-' }}</span></template>
</el-table-column>
<el-table-column prop="remark" label="备注" width="180" show-overflow-tooltip></el-table-column>
<el-table-column label="是否打款" width="100">
<template slot-scope="scope"><span class="text-gray-400">@{{ scope.row.is_paid || '-' }}</span></template>
</el-table-column>
<el-table-column prop="pay_month" label="打款月份" width="110"></el-table-column>
<el-table-column label="流水号" width="120">
<template slot-scope="scope"><span class="text-gray-400">@{{ scope.row.serial_no || '-' }}</span></template>
</el-table-column>
<el-table-column label="借支协议" width="120">
<template slot-scope="scope"><span class="text-gray-400">@{{ scope.row.loan_agreement || '-' }}</span></template>
</el-table-column>
<el-table-column prop="person_charge" label="负责人" width="100"></el-table-column>
<el-table-column label="票据" width="80">
<template slot-scope="scope"><span class="text-gray-400">@{{ scope.row.receipt_0 || '-' }}</span></template>
</el-table-column>
<el-table-column label="票据1" width="80">
<template slot-scope="scope"><span class="text-gray-400">@{{ scope.row.receipt_1 || '-' }}</span></template>
</el-table-column>
<el-table-column label="票据2" width="80">
<template slot-scope="scope"><span class="text-gray-400">@{{ scope.row.receipt_2 || '-' }}</span></template>
</el-table-column>
</el-table>
<div class="flex items-center justify-between px-4 py-3 border-t border-gray-100" v-if="!loading">
<div class="text-sm text-gray-500">共 @{{ total }} 条记录</div>
<el-pagination
background
layout="prev, pager, next, sizes, jumper"
:total="total"
:page-size="pageSize"
:current-page.sync="currentPage"
:page-sizes="[25, 50, 100]"
@size-change="onPageSizeChange"
@current-change="onPageChange">
</el-pagination>
</div>
</div>
</div>
</div>
@push('body_end')
<script src="{{ asset('public/js/ecommerce-finance/advance-details.min.js') }}"></script>
@endpush

View File

@ -0,0 +1,73 @@
<div id="compensation-detail-app" class="compensation-detail-page">
<div v-cloak>
{{-- 统计卡片 --}}
<div class="grid grid-cols-2 gap-4 mb-6">
<div class="bg-white rounded-xl border border-gray-100 p-5 shadow-sm">
<div class="text-sm text-gray-500 mb-1">赔付总金额</div>
<div class="text-2xl font-bold text-red-600">@{{ formatMoney(summary.totalAmount) }}</div>
</div>
<div class="bg-white rounded-xl border border-gray-100 p-5 shadow-sm">
<div class="text-sm text-gray-500 mb-1">总记录数</div>
<div class="text-2xl font-bold text-indigo-600">@{{ (summary.totalCount || 0).toLocaleString() }}</div>
</div>
</div>
{{-- 筛选栏 --}}
<div class="bg-white rounded-xl border border-gray-100 p-4 mb-4 shadow-sm">
<div class="flex flex-wrap items-center gap-3">
<el-input v-model="filters.supplier" placeholder="供应商" size="small" style="width: 220px" clearable @clear="onFilterChange" @keyup.enter="onFilterChange"></el-input>
<el-date-picker v-model="filters.month_range" type="monthrange"
start-placeholder="月份开始" end-placeholder="月份结束" size="small"
style="width: 240px" value-format="yyyy-MM" @change="onFilterChange">
</el-date-picker>
<el-button size="small" type="primary" @click="onFilterChange">查询</el-button>
<el-button size="small" @click="onReset">重置</el-button>
</div>
</div>
{{-- 数据表格 --}}
<div class="bg-white rounded-xl border border-gray-100 shadow-sm overflow-hidden">
<div v-if="loading" class="flex items-center justify-center py-20">
<span class="text-gray-400">加载中...</span>
</div>
<el-table v-else :data="tableData" stripe border style="width: 100%" size="small" height="600">
<el-table-column type="index" label="#" width="50" fixed="left"></el-table-column>
<el-table-column prop="month" label="日期" width="100" fixed="left"></el-table-column>
<el-table-column prop="supplier" label="供应商" width="260" show-overflow-tooltip></el-table-column>
<el-table-column prop="amount" label="赔付金额" width="140" align="right">
<template slot-scope="scope">
<span :class="scope.row.amount < 0 ? 'text-red-600' : 'text-gray-800'">@{{ formatMoney(scope.row.amount) }}</span>
</template>
</el-table-column>
<el-table-column prop="reason" label="赔付原因" width="240" show-overflow-tooltip></el-table-column>
<el-table-column prop="remark" label="备注" width="180" show-overflow-tooltip>
<template slot-scope="scope"><span class="text-gray-400">@{{ scope.row.remark || '-' }}</span></template>
</el-table-column>
<el-table-column prop="factory_type" label="类型" width="100">
<template slot-scope="scope">
<el-tag size="mini" :type="scope.row.factory_type === '纸板厂' ? 'primary' : 'success'">@{{ scope.row.factory_type }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="creator" label="创建人" width="100"></el-table-column>
</el-table>
<div class="flex items-center justify-between px-4 py-3 border-t border-gray-100" v-if="!loading">
<div class="text-sm text-gray-500">共 @{{ total }} 条记录</div>
<el-pagination
background
layout="prev, pager, next, sizes, jumper"
:total="total"
:page-size="pageSize"
:current-page.sync="currentPage"
:page-sizes="[25, 50, 100]"
@size-change="onPageSizeChange"
@current-change="onPageChange">
</el-pagination>
</div>
</div>
</div>
</div>
@push('body_end')
<script src="{{ asset('public/js/ecommerce-finance/compensation-details.min.js') }}"></script>
@endpush

View File

@ -0,0 +1,79 @@
<div id="invoice-deduction-app" class="invoice-deduction-page">
<div v-cloak>
{{-- 统计卡片 --}}
<div class="grid grid-cols-2 gap-4 mb-6">
<div class="bg-white rounded-xl border border-gray-100 p-5 shadow-sm">
<div class="text-sm text-gray-500 mb-1">扣款总金额</div>
<div class="text-2xl font-bold text-red-600">@{{ formatMoney(summary.totalPrice) }}</div>
</div>
<div class="bg-white rounded-xl border border-gray-100 p-5 shadow-sm">
<div class="text-sm text-gray-500 mb-1">总记录数</div>
<div class="text-2xl font-bold text-indigo-600">@{{ (summary.totalCount || 0).toLocaleString() }}</div>
</div>
</div>
{{-- 筛选栏 --}}
<div class="bg-white rounded-xl border border-gray-100 p-4 mb-4 shadow-sm">
<div class="flex flex-wrap items-center gap-3">
<el-input v-model="filters.supplier" placeholder="供应商/打款抬头/收款方" size="small" style="width: 220px" clearable @clear="onFilterChange" @keyup.enter="onFilterChange"></el-input>
<el-date-picker v-model="filters.month_range" type="monthrange"
start-placeholder="月份开始" end-placeholder="月份结束" size="small"
style="width: 240px" value-format="yyyy-MM" @change="onFilterChange">
</el-date-picker>
<el-button size="small" type="primary" @click="onFilterChange">查询</el-button>
<el-button size="small" @click="onReset">重置</el-button>
</div>
</div>
{{-- 数据表格 --}}
<div class="bg-white rounded-xl border border-gray-100 shadow-sm overflow-hidden">
<div v-if="loading" class="flex items-center justify-center py-20">
<span class="text-gray-400">加载中...</span>
</div>
<el-table v-else :data="tableData" stripe border style="width: 100%" size="small" height="600">
<el-table-column type="index" label="#" width="50" fixed="left"></el-table-column>
<el-table-column prop="month_str" label="日期" width="100" fixed="left"></el-table-column>
<el-table-column label="类别" width="100">
<template slot-scope="scope"><span class="text-gray-400">@{{ scope.row.category || '-' }}</span></template>
</el-table-column>
<el-table-column prop="payee" label="打款抬头" width="160" show-overflow-tooltip></el-table-column>
<el-table-column prop="receiver" label="收款方" width="160" show-overflow-tooltip></el-table-column>
<el-table-column label="供板材厂家/代扣厂家" width="180">
<template slot-scope="scope"><span class="text-gray-400">@{{ scope.row.board_factory || '-' }}</span></template>
</el-table-column>
<el-table-column label="补贴费用" width="120" align="right">
<template slot-scope="scope"><span class="text-gray-400">@{{ formatMoney(scope.row.subsidy_fee) }}</span></template>
</el-table-column>
<el-table-column prop="remark" label="备注" width="200" show-overflow-tooltip></el-table-column>
<el-table-column prop="price" label="对私打款金额" width="140" align="right">
<template slot-scope="scope">
<span :class="scope.row.price < 0 ? 'text-red-600' : 'text-gray-800'">@{{ formatMoney(scope.row.price) }}</span>
</template>
</el-table-column>
<el-table-column prop="detail" label="明细" width="200" show-overflow-tooltip></el-table-column>
<el-table-column label="注明" width="100">
<template slot-scope="scope"><span class="text-gray-400">@{{ scope.row.note || '-' }}</span></template>
</el-table-column>
<el-table-column prop="person_charge" label="负责人" width="100" show-overflow-tooltip></el-table-column>
</el-table>
<div class="flex items-center justify-between px-4 py-3 border-t border-gray-100" v-if="!loading">
<div class="text-sm text-gray-500">共 @{{ total }} 条记录</div>
<el-pagination
background
layout="prev, pager, next, sizes, jumper"
:total="total"
:page-size="pageSize"
:current-page.sync="currentPage"
:page-sizes="[25, 50, 100]"
@size-change="onPageSizeChange"
@current-change="onPageChange">
</el-pagination>
</div>
</div>
</div>
</div>
@push('body_end')
<script src="{{ asset('public/js/ecommerce-finance/invoice-deductions.min.js') }}"></script>
@endpush

View File

@ -0,0 +1,139 @@
<div id="goods-reconciliation-app" class="goods-reconciliation-page">
<div v-cloak>
{{-- 统计卡片 --}}
<div class="grid grid-cols-4 gap-4 mb-6">
<div class="bg-white rounded-xl border border-gray-100 p-5 shadow-sm">
<div class="text-sm text-gray-500 mb-1">对账总金额</div>
<div class="text-2xl font-bold text-gray-800">@{{ formatMoney(summary.totalAmount) }}</div>
</div>
<div class="bg-white rounded-xl border border-gray-100 p-5 shadow-sm">
<div class="text-sm text-gray-500 mb-1">预估总金额</div>
<div class="text-2xl font-bold text-gray-800">@{{ formatMoney(summary.totalGuessAmount) }}</div>
</div>
<div class="bg-white rounded-xl border border-gray-100 p-5 shadow-sm">
<div class="text-sm text-gray-500 mb-1">已发出</div>
<div class="text-2xl font-bold text-green-600">@{{ summary.sentCount.toLocaleString() }}</div>
</div>
<div class="bg-white rounded-xl border border-gray-100 p-5 shadow-sm">
<div class="text-sm text-gray-500 mb-1">未发出</div>
<div class="text-2xl font-bold text-orange-500">@{{ summary.unsentCount.toLocaleString() }}</div>
</div>
</div>
{{-- 筛选栏 --}}
<div class="bg-white rounded-xl border border-gray-100 p-4 mb-4 shadow-sm">
<div class="flex flex-wrap items-center gap-3 mb-3">
<el-select v-model="filters.record_type" placeholder="记录类型" size="small" style="width: 140px" @change="onFilterChange">
<el-option label="全部记录" value="all"></el-option>
<el-option label="入库对账" value="purchase"></el-option>
<el-option label="代发对账" value="sale"></el-option>
</el-select>
<el-select v-model="filters.supplier" placeholder="供应商" size="small" style="width: 160px" filterable clearable @change="onFilterChange">
<el-option v-for="s in options.suppliers" :key="s" :label="s" :value="s"></el-option>
</el-select>
<el-select v-model="filters.warehouse" placeholder="仓库" size="small" style="width: 140px" filterable clearable @change="onFilterChange">
<el-option v-for="w in options.warehouses" :key="w" :label="w" :value="w"></el-option>
</el-select>
<el-input v-model="filters.stock_no" placeholder="入库单号" size="small" style="width: 140px" clearable @clear="onFilterChange" @keyup.enter="onFilterChange"></el-input>
<el-input v-model="filters.purchase_no" placeholder="采购单号" size="small" style="width: 140px" clearable @clear="onFilterChange" @keyup.enter="onFilterChange"></el-input>
</div>
<div class="flex flex-wrap items-center gap-3">
<el-input v-model="filters.product_code" placeholder="商品编码" size="small" style="width: 140px" clearable @clear="onFilterChange" @keyup.enter="onFilterChange"></el-input>
<el-input v-model="filters.express_no" placeholder="快递单号" size="small" style="width: 140px" clearable @clear="onFilterChange" @keyup.enter="onFilterChange"></el-input>
<el-date-picker v-model="filters.send_time_range" type="daterange"
start-placeholder="发货开始" end-placeholder="发货结束" size="small"
style="width: 260px" value-format="yyyy-MM-dd" @change="onFilterChange">
</el-date-picker>
<el-date-picker v-model="filters.create_time_range" type="daterange"
start-placeholder="创建开始" end-placeholder="创建结束" size="small"
style="width: 260px" value-format="yyyy-MM-dd" @change="onFilterChange">
</el-date-picker>
<el-select v-model="filters.sent_status" placeholder="发出状态" size="small" style="width: 120px" clearable @change="onFilterChange">
<el-option label="全部" value="all"></el-option>
<el-option label="已发出" value="sent"></el-option>
<el-option label="未发出" value="unsent"></el-option>
</el-select>
</div>
</div>
{{-- Tab 切换 --}}
<div class="flex gap-1 mb-4">
<button v-for="tab in tabs" :key="tab.key"
@click="switchTab(tab.key)"
:class="['px-4 py-2 rounded-lg text-sm font-medium transition-colors',
activeTab === tab.key ? 'bg-indigo-600 text-white shadow-sm' : 'bg-white text-gray-600 border border-gray-200 hover:bg-gray-50']">
@{{ tab.label }}
</button>
</div>
{{-- 数据表格 --}}
<div class="bg-white rounded-xl border border-gray-100 shadow-sm overflow-hidden">
<div v-if="loading" class="flex items-center justify-center py-20">
<span class="text-gray-400">加载中...</span>
</div>
<el-table v-else :data="tableData" stripe border style="width: 100%" size="small"
:default-sort="{prop: 'createTime', order: 'descending'}"
@sort-change="onSortChange">
<el-table-column type="index" label="#" width="55" fixed="left"></el-table-column>
<el-table-column prop="recordType" label="类型" width="80">
<template slot-scope="scope">
<el-tag size="mini" :type="scope.row.recordType === 'purchase' ? 'primary' : 'success'">
@{{ scope.row.recordType === 'purchase' ? '入库' : '代发' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="stockNo" label="入库单号" width="120" show-overflow-tooltip></el-table-column>
<el-table-column prop="purchaseNo" label="采购单号" width="110" show-overflow-tooltip></el-table-column>
<el-table-column prop="sendTime" label="发货时间" width="145" sortable="custom"></el-table-column>
<el-table-column prop="supplier" label="供应商" width="110" show-overflow-tooltip></el-table-column>
<el-table-column prop="skuCode" label="货号" width="130" show-overflow-tooltip></el-table-column>
<el-table-column prop="productCode" label="商品编码" width="120" show-overflow-tooltip></el-table-column>
<el-table-column prop="skuName" label="规格名称" width="180" show-overflow-tooltip></el-table-column>
<el-table-column prop="qty" label="数量" width="80" align="right"></el-table-column>
<el-table-column label="单价" width="100" align="right">
<template slot-scope="scope">@{{ formatMoney(scope.row.unitPrice) }}</template>
</el-table-column>
<el-table-column label="金额" width="120" align="right">
<template slot-scope="scope">@{{ formatMoney(scope.row.amount) }}</template>
</el-table-column>
<el-table-column label="预估单价" width="100" align="right">
<template slot-scope="scope">@{{ formatMoney(scope.row.guessUnitPrice) }}</template>
</el-table-column>
<el-table-column label="预估金额" width="120" align="right">
<template slot-scope="scope">@{{ formatMoney(scope.row.guessAmount) }}</template>
</el-table-column>
<el-table-column prop="supplierGoodsCode" label="供应商商品编码" width="140" show-overflow-tooltip></el-table-column>
<el-table-column prop="remark" label="备注" width="150" show-overflow-tooltip></el-table-column>
<el-table-column prop="warehouse" label="仓库名称" width="100" show-overflow-tooltip></el-table-column>
<el-table-column prop="createTime" label="创建时间" width="145" sortable="custom"></el-table-column>
<el-table-column prop="expressName" label="快递名称" width="100" show-overflow-tooltip></el-table-column>
<el-table-column prop="expressNo" label="快递单号" width="130" show-overflow-tooltip></el-table-column>
<el-table-column label="未发出" width="90" align="center">
<template slot-scope="scope">
<el-tag v-if="scope.row.isUnsent" size="mini" type="warning">未发出</el-tag>
<span v-else class="text-gray-400">-</span>
</template>
</el-table-column>
</el-table>
{{-- 分页 --}}
<div class="flex items-center justify-between px-4 py-3 border-t border-gray-100" v-if="!loading">
<div class="text-sm text-gray-500">共 @{{ total }} 条记录</div>
<el-pagination
background
layout="prev, pager, next, sizes, jumper"
:total="total"
:page-size="pageSize"
:current-page.sync="currentPage"
:page-sizes="[25, 50, 100]"
@size-change="onPageSizeChange"
@current-change="onPageChange">
</el-pagination>
</div>
</div>
</div>
</div>
@push('body_end')
<script src="{{ asset('public/js/ecommerce-finance/goods-reconciliations.min.js') }}"></script>
@endpush

View File

@ -0,0 +1,107 @@
<div id="return-reconciliation-app" class="return-reconciliation-page">
<div v-cloak>
{{-- 统计卡片 --}}
<div class="grid grid-cols-3 gap-4 mb-6">
<div class="bg-white rounded-xl border border-gray-100 p-5 shadow-sm">
<div class="text-sm text-gray-500 mb-1">对账总金额</div>
<div class="text-2xl font-bold text-gray-800">@{{ formatMoney(summary.totalAmount) }}</div>
</div>
<div class="bg-white rounded-xl border border-gray-100 p-5 shadow-sm">
<div class="text-sm text-gray-500 mb-1">预估总金额</div>
<div class="text-2xl font-bold text-gray-800">@{{ formatMoney(summary.totalGuessAmount) }}</div>
</div>
<div class="bg-white rounded-xl border border-gray-100 p-5 shadow-sm">
<div class="text-sm text-gray-500 mb-1">总记录数</div>
<div class="text-2xl font-bold text-indigo-600">@{{ (summary.totalCount || 0).toLocaleString() }}</div>
</div>
</div>
{{-- 筛选栏 --}}
<div class="bg-white rounded-xl border border-gray-100 p-4 mb-4 shadow-sm">
<div class="flex flex-wrap items-center gap-3">
<el-input v-model="filters.supplier" placeholder="明细供应商" size="small" style="width: 160px" clearable @clear="onFilterChange" @keyup.enter="onFilterChange"></el-input>
<el-select v-model="filters.warehouse" placeholder="仓库" size="small" style="width: 140px" filterable clearable @change="onFilterChange">
<el-option v-for="w in options.warehouses" :key="w" :label="w" :value="w"></el-option>
</el-select>
<el-input v-model="filters.sku_id" placeholder="条形码" size="small" style="width: 160px" clearable @clear="onFilterChange" @keyup.enter="onFilterChange"></el-input>
<el-input v-model="filters.express_no" placeholder="快递单号" size="small" style="width: 160px" clearable @clear="onFilterChange" @keyup.enter="onFilterChange"></el-input>
<el-date-picker v-model="filters.date_range" type="daterange"
start-placeholder="发货开始" end-placeholder="发货结束" size="small"
style="width: 260px" value-format="yyyy-MM-dd" @change="onFilterChange">
</el-date-picker>
<el-button size="small" type="primary" @click="onFilterChange">查询</el-button>
<el-button size="small" @click="onReset">重置</el-button>
</div>
</div>
{{-- 数据表格 --}}
<div class="bg-white rounded-xl border border-gray-100 shadow-sm overflow-hidden">
<div v-if="loading" class="flex items-center justify-center py-20">
<span class="text-gray-400">加载中...</span>
</div>
<el-table v-else :data="tableData" stripe border style="width: 100%" size="small" height="600">
<el-table-column type="index" label="#" width="50" fixed="left"></el-table-column>
<el-table-column prop="send_time" label="发货时间" width="110" fixed="left" show-overflow-tooltip></el-table-column>
<el-table-column prop="supplier" label="明细供应商" width="110" show-overflow-tooltip></el-table-column>
<el-table-column prop="barcode" label="条形码" width="140" show-overflow-tooltip></el-table-column>
<el-table-column prop="sku_name" label="商品名称" width="220" show-overflow-tooltip></el-table-column>
<el-table-column prop="qty" label="发货量" width="80" align="right"></el-table-column>
<el-table-column label="单价" width="100" align="right">
<template slot-scope="scope">@{{ formatMoney(scope.row.unit_price) }}</template>
</el-table-column>
<el-table-column label="金额" width="110" align="right">
<template slot-scope="scope">@{{ formatMoney(scope.row.amount) }}</template>
</el-table-column>
<el-table-column label="预估单价" width="100" align="right">
<template slot-scope="scope">@{{ formatMoney(scope.row.guess_unit_price) }}</template>
</el-table-column>
<el-table-column label="预估金额" width="110" align="right">
<template slot-scope="scope">@{{ formatMoney(scope.row.guess_amount) }}</template>
</el-table-column>
<el-table-column prop="supplier_barcode" label="供应商条码" width="120" show-overflow-tooltip>
<template slot-scope="scope"><span class="text-gray-400">@{{ scope.row.supplier_barcode || '-' }}</span></template>
</el-table-column>
<el-table-column prop="remark" label="备注" width="140" show-overflow-tooltip></el-table-column>
<el-table-column prop="title" label="标题" width="100" show-overflow-tooltip>
<template slot-scope="scope"><span class="text-gray-400">@{{ scope.row.title || '-' }}</span></template>
</el-table-column>
<el-table-column prop="wms_pick_no" label="wms配货单号" width="120" show-overflow-tooltip></el-table-column>
<el-table-column prop="warehouse" label="仓库名称" width="120" show-overflow-tooltip></el-table-column>
<el-table-column prop="express_no" label="快递单号" width="130" show-overflow-tooltip></el-table-column>
<el-table-column prop="express_name" label="快递名称" width="100" show-overflow-tooltip></el-table-column>
<el-table-column prop="io_date" label="发货时间" width="110" show-overflow-tooltip></el-table-column>
<el-table-column prop="original_barcode" label="原条码" width="130" show-overflow-tooltip>
<template slot-scope="scope"><span class="text-gray-400">@{{ scope.row.original_barcode || '-' }}</span></template>
</el-table-column>
<el-table-column prop="labels" label="标记多标签" width="120" show-overflow-tooltip>
<template slot-scope="scope">
<el-tag v-if="scope.row.labels" size="mini" type="warning">@{{ scope.row.labels }}</el-tag>
<span v-else class="text-gray-400">-</span>
</template>
</el-table-column>
<el-table-column prop="creator" label="创建人" width="100" show-overflow-tooltip></el-table-column>
<el-table-column prop="reconciler" label="对账人" width="100" show-overflow-tooltip>
<template slot-scope="scope"><span class="text-gray-400">@{{ scope.row.reconciler || '-' }}</span></template>
</el-table-column>
</el-table>
<div class="flex items-center justify-between px-4 py-3 border-t border-gray-100" v-if="!loading">
<div class="text-sm text-gray-500">共 @{{ total }} 条记录</div>
<el-pagination
background
layout="prev, pager, next, sizes, jumper"
:total="total"
:page-size="pageSize"
:current-page.sync="currentPage"
:page-sizes="[25, 50, 100]"
@size-change="onPageSizeChange"
@current-change="onPageChange">
</el-pagination>
</div>
</div>
</div>
</div>
@push('body_end')
<script src="{{ asset('public/js/ecommerce-finance/return-reconciliations.min.js') }}"></script>
@endpush

View File

@ -0,0 +1,93 @@
<div id="sample-deduction-app" class="sample-deduction-page">
<div v-cloak>
{{-- 统计卡片 --}}
<div class="grid grid-cols-3 gap-4 mb-6">
<div class="bg-white rounded-xl border border-gray-100 p-5 shadow-sm">
<div class="text-sm text-gray-500 mb-1">合计扣款金额</div>
<div class="text-2xl font-bold text-red-600">@{{ formatMoney(summary.totalAmount) }}</div>
</div>
<div class="bg-white rounded-xl border border-gray-100 p-5 shadow-sm">
<div class="text-sm text-gray-500 mb-1">预估总金额</div>
<div class="text-2xl font-bold text-gray-800">@{{ formatMoney(summary.totalEstimate) }}</div>
</div>
<div class="bg-white rounded-xl border border-gray-100 p-5 shadow-sm">
<div class="text-sm text-gray-500 mb-1">总记录数</div>
<div class="text-2xl font-bold text-indigo-600">@{{ (summary.totalCount || 0).toLocaleString() }}</div>
</div>
</div>
{{-- 筛选栏 --}}
<div class="bg-white rounded-xl border border-gray-100 p-4 mb-4 shadow-sm">
<div class="flex flex-wrap items-center gap-3">
<el-input v-model="filters.supplier" placeholder="厂家" size="small" style="width: 160px" clearable @clear="onFilterChange" @keyup.enter="onFilterChange"></el-input>
<el-input v-model="filters.department" placeholder="部门" size="small" style="width: 160px" clearable @clear="onFilterChange" @keyup.enter="onFilterChange"></el-input>
<el-input v-model="filters.creator" placeholder="申请人" size="small" style="width: 120px" clearable @clear="onFilterChange" @keyup.enter="onFilterChange"></el-input>
<el-input v-model="filters.sku_id" placeholder="商品代码" size="small" style="width: 160px" clearable @clear="onFilterChange" @keyup.enter="onFilterChange"></el-input>
<el-date-picker v-model="filters.date_range" type="daterange"
start-placeholder="开始" end-placeholder="结束" size="small"
style="width: 240px" value-format="yyyy-MM-dd" @change="onFilterChange">
</el-date-picker>
<el-button size="small" type="primary" @click="onFilterChange">查询</el-button>
<el-button size="small" @click="onReset">重置</el-button>
</div>
</div>
{{-- 数据表格 --}}
<div class="bg-white rounded-xl border border-gray-100 shadow-sm overflow-hidden">
<div v-if="loading" class="flex items-center justify-center py-20">
<span class="text-gray-400">加载中...</span>
</div>
<el-table v-else :data="tableData" stripe border style="width: 100%" size="small" height="600">
<el-table-column type="index" label="#" width="50" fixed="left"></el-table-column>
<el-table-column prop="finish_time" label="日期" width="110" fixed="left"></el-table-column>
<el-table-column prop="department" label="部门" width="200" show-overflow-tooltip></el-table-column>
<el-table-column prop="creator" label="申请人" width="100"></el-table-column>
<el-table-column prop="sku_id" label="商品代码" width="160" show-overflow-tooltip></el-table-column>
<el-table-column prop="skuName" label="商品名称" width="220" show-overflow-tooltip></el-table-column>
<el-table-column prop="num" label="数量" width="70" align="right"></el-table-column>
<el-table-column prop="supplier" label="厂家" width="120" show-overflow-tooltip></el-table-column>
<el-table-column prop="purchase" label="对应采购" width="200" show-overflow-tooltip></el-table-column>
<el-table-column prop="productCode" label="扣款厂家商品代码" width="160" show-overflow-tooltip></el-table-column>
<el-table-column prop="shipping_fee" label="运费" width="100" align="right">
<template slot-scope="scope">@{{ formatMoney(scope.row.shipping_fee) }}</template>
</el-table-column>
<el-table-column prop="cost_price" label="成本价" width="110" align="right">
<template slot-scope="scope">@{{ formatMoney(scope.row.cost_price) }}</template>
</el-table-column>
<el-table-column prop="total_amount" label="合计扣款金额" width="130" align="right">
<template slot-scope="scope">
<span :class="scope.row.total_amount < 0 ? 'text-red-600' : 'text-gray-800'">@{{ formatMoney(scope.row.total_amount) }}</span>
</template>
</el-table-column>
<el-table-column prop="estimated_price" label="预估价" width="100" align="right">
<template slot-scope="scope">@{{ formatMoney(scope.row.estimated_price) }}</template>
</el-table-column>
<el-table-column prop="estimated_price_all" label="预估金额" width="110" align="right">
<template slot-scope="scope">@{{ formatMoney(scope.row.estimated_price_all) }}</template>
</el-table-column>
<el-table-column prop="remark" label="备注" width="150" show-overflow-tooltip>
<template slot-scope="scope"><span class="text-gray-400">@{{ scope.row.remark || '-' }}</span></template>
</el-table-column>
<el-table-column prop="businessId" label="业务单号" width="200" show-overflow-tooltip></el-table-column>
</el-table>
<div class="flex items-center justify-between px-4 py-3 border-t border-gray-100" v-if="!loading">
<div class="text-sm text-gray-500">共 @{{ total }} 条记录</div>
<el-pagination
background
layout="prev, pager, next, sizes, jumper"
:total="total"
:page-size="pageSize"
:current-page.sync="currentPage"
:page-sizes="[25, 50, 100]"
@size-change="onPageSizeChange"
@current-change="onPageChange">
</el-pagination>
</div>
</div>
</div>
</div>
@push('body_end')
<script src="{{ asset('public/js/ecommerce-finance/sample-deductions.min.js') }}"></script>
@endpush

View File

@ -142,6 +142,43 @@ Route::group(['prefix' => 'purchases'], function () {
});
Route::group(['prefix' => 'banking'], function () {
Route::group(['as' => 'marketing-account-bills.', 'prefix' => 'marketing-account-bills'], function () {
Route::get('stores', 'Banking\MarketingAccountBills@storeIndex')->name('stores.index');
Route::get('{platform}/template', 'Banking\MarketingAccountBills@template')->name('template');
Route::get('{platform}/export', 'Banking\MarketingAccountBills@export')->name('export');
Route::post('{platform}/import', 'Banking\MarketingAccountBills@import')->name('import');
Route::get('{platform}/create', 'Banking\MarketingAccountBills@create')->name('create');
Route::post('{platform}', 'Banking\MarketingAccountBills@store')->name('store');
Route::get('{platform}/{marketingAccountBill}/edit', 'Banking\MarketingAccountBills@edit')->name('edit');
Route::patch('{platform}/{marketingAccountBill}', 'Banking\MarketingAccountBills@update')->name('update');
Route::delete('{platform}/{marketingAccountBill}', 'Banking\MarketingAccountBills@destroy')->name('destroy');
Route::get('{platform}', 'Banking\MarketingAccountBills@platformIndex')->name('platform.index');
});
Route::group(['as' => 'marketing-taobao-alipay-bills.', 'prefix' => 'marketing-taobao-alipay-bills'], function () {
Route::get('template', 'Banking\MarketingTaobaoAlipayBills@template')->name('template');
Route::get('export', 'Banking\MarketingTaobaoAlipayBills@export')->name('export');
Route::post('import', 'Banking\MarketingTaobaoAlipayBills@import')->name('import');
Route::get('create', 'Banking\MarketingTaobaoAlipayBills@create')->name('create');
Route::post('/', 'Banking\MarketingTaobaoAlipayBills@store')->name('store');
Route::get('{marketingTaobaoAlipayBill}/edit', 'Banking\MarketingTaobaoAlipayBills@edit')->name('edit');
Route::patch('{marketingTaobaoAlipayBill}', 'Banking\MarketingTaobaoAlipayBills@update')->name('update');
Route::delete('{marketingTaobaoAlipayBill}', 'Banking\MarketingTaobaoAlipayBills@destroy')->name('destroy');
Route::get('/', 'Banking\MarketingTaobaoAlipayBills@index')->name('index');
});
Route::group(['as' => 'marketing-pinduoduo-bills.', 'prefix' => 'marketing-pinduoduo-bills'], function () {
Route::get('template', 'Banking\MarketingPinduoduoBills@template')->name('template');
Route::get('export', 'Banking\MarketingPinduoduoBills@export')->name('export');
Route::post('import', 'Banking\MarketingPinduoduoBills@import')->name('import');
Route::get('create', 'Banking\MarketingPinduoduoBills@create')->name('create');
Route::post('/', 'Banking\MarketingPinduoduoBills@store')->name('store');
Route::get('{marketingPinduoduoBill}/edit', 'Banking\MarketingPinduoduoBills@edit')->name('edit');
Route::patch('{marketingPinduoduoBill}', 'Banking\MarketingPinduoduoBills@update')->name('update');
Route::delete('{marketingPinduoduoBill}', 'Banking\MarketingPinduoduoBills@destroy')->name('destroy');
Route::get('/', 'Banking\MarketingPinduoduoBills@index')->name('index');
});
Route::group(['as' => 'fund-accounts.', 'prefix' => 'fund-accounts'], function () {
Route::get('bank', 'Banking\FundAccounts@bankIndex')->name('bank.index');
Route::get('stores', 'Banking\FundAccounts@storeIndex')->name('stores.index');

Binary file not shown.