diff --git a/.gitignore b/.gitignore index c54b73e5..e5394e74 100644 --- a/.gitignore +++ b/.gitignore @@ -107,3 +107,5 @@ modules/* /artifacts/ /runtime/ /.caiwu-build/ +.workbuddy/ +runtime/.env diff --git a/LOCAL_DEPLOY.md b/LOCAL_DEPLOY.md new file mode 100644 index 00000000..29abd260 --- /dev/null +++ b/LOCAL_DEPLOY.md @@ -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_`(见上) diff --git a/app/Http/Controllers/Banking/MarketingAccountBills.php b/app/Http/Controllers/Banking/MarketingAccountBills.php new file mode 100644 index 00000000..0fe39409 --- /dev/null +++ b/app/Http/Controllers/Banking/MarketingAccountBills.php @@ -0,0 +1,313 @@ + '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); + } +} diff --git a/app/Http/Controllers/Banking/MarketingPinduoduoBills.php b/app/Http/Controllers/Banking/MarketingPinduoduoBills.php new file mode 100644 index 00000000..1aa1c8e8 --- /dev/null +++ b/app/Http/Controllers/Banking/MarketingPinduoduoBills.php @@ -0,0 +1,226 @@ +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'); + } +} diff --git a/app/Http/Controllers/Banking/MarketingTaobaoAlipayBills.php b/app/Http/Controllers/Banking/MarketingTaobaoAlipayBills.php new file mode 100644 index 00000000..d0ee4ec2 --- /dev/null +++ b/app/Http/Controllers/Banking/MarketingTaobaoAlipayBills.php @@ -0,0 +1,232 @@ +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'); + } +} diff --git a/app/Http/Controllers/Business/Pages.php b/app/Http/Controllers/Business/Pages.php index 0e16b4b7..69296f65 100644 --- a/app/Http/Controllers/Business/Pages.php +++ b/app/Http/Controllers/Business/Pages.php @@ -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 = [ diff --git a/app/Http/Requests/Banking/MarketingAccountBill.php b/app/Http/Requests/Banking/MarketingAccountBill.php new file mode 100644 index 00000000..b9e2cf1d --- /dev/null +++ b/app/Http/Requests/Banking/MarketingAccountBill.php @@ -0,0 +1,35 @@ + ['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'], + ]; + } +} diff --git a/app/Http/Requests/Banking/MarketingPinduoduoBill.php b/app/Http/Requests/Banking/MarketingPinduoduoBill.php new file mode 100644 index 00000000..509d6bce --- /dev/null +++ b/app/Http/Requests/Banking/MarketingPinduoduoBill.php @@ -0,0 +1,26 @@ + ['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'], + ]; + } +} diff --git a/app/Http/Requests/Banking/MarketingTaobaoAlipayBill.php b/app/Http/Requests/Banking/MarketingTaobaoAlipayBill.php new file mode 100644 index 00000000..2abaee2e --- /dev/null +++ b/app/Http/Requests/Banking/MarketingTaobaoAlipayBill.php @@ -0,0 +1,27 @@ + ['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'], + ]; + } +} diff --git a/app/Models/Banking/MarketingAccountBill.php b/app/Models/Banking/MarketingAccountBill.php new file mode 100644 index 00000000..5ed86a93 --- /dev/null +++ b/app/Models/Banking/MarketingAccountBill.php @@ -0,0 +1,56 @@ + '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); + } +} diff --git a/app/Models/Banking/MarketingPinduoduoBill.php b/app/Models/Banking/MarketingPinduoduoBill.php new file mode 100644 index 00000000..7a9d3c37 --- /dev/null +++ b/app/Models/Banking/MarketingPinduoduoBill.php @@ -0,0 +1,30 @@ + 'datetime', + 'transaction_amount' => 'double', + 'balance' => 'double', + 'deleted_at' => 'datetime', + ]; +} diff --git a/app/Models/Banking/MarketingTaobaoAlipayBill.php b/app/Models/Banking/MarketingTaobaoAlipayBill.php new file mode 100644 index 00000000..cfe55a53 --- /dev/null +++ b/app/Models/Banking/MarketingTaobaoAlipayBill.php @@ -0,0 +1,32 @@ + 'date', + 'bookkeeping_time' => 'datetime', + 'operation_amount' => 'double', + 'balance_after' => 'double', + 'deleted_at' => 'datetime', + ]; +} diff --git a/app/Utilities/Console.php b/app/Utilities/Console.php index 5dabeaf3..b98392f7 100644 --- a/app/Utilities/Console.php +++ b/app/Utilities/Console.php @@ -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); diff --git a/app/Utilities/Installer.php b/app/Utilities/Installer.php index 20de3d76..85331462 100644 --- a/app/Utilities/Installer.php +++ b/app/Utilities/Installer.php @@ -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', ])); } diff --git a/config/database.php b/config/database.php index bd4cb8b0..6914f878 100644 --- a/config/database.php +++ b/config/database.php @@ -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'), ]) : [], ], diff --git a/database/migrations/2026_07_21_000000_create_marketing_account_bills_table.php b/database/migrations/2026_07_21_000000_create_marketing_account_bills_table.php new file mode 100644 index 00000000..23f9c55e --- /dev/null +++ b/database/migrations/2026_07_21_000000_create_marketing_account_bills_table.php @@ -0,0 +1,45 @@ +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'); + } +}; diff --git a/database/migrations/2026_07_21_100000_create_marketing_taobao_alipay_bills_table.php b/database/migrations/2026_07_21_100000_create_marketing_taobao_alipay_bills_table.php new file mode 100644 index 00000000..55f4a2f7 --- /dev/null +++ b/database/migrations/2026_07_21_100000_create_marketing_taobao_alipay_bills_table.php @@ -0,0 +1,37 @@ +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'); + } +}; diff --git a/database/migrations/2026_07_21_110000_create_marketing_pinduoduo_bills_table.php b/database/migrations/2026_07_21_110000_create_marketing_pinduoduo_bills_table.php new file mode 100644 index 00000000..c8564d0a --- /dev/null +++ b/database/migrations/2026_07_21_110000_create_marketing_pinduoduo_bills_table.php @@ -0,0 +1,35 @@ +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'); + } +}; diff --git a/database/seeds/Permissions.php b/database/seeds/Permissions.php index 3dbcfebf..5b3c1f1b 100644 --- a/database/seeds/Permissions.php +++ b/database/seeds/Permissions.php @@ -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', diff --git a/deploy/Dockerfile.local b/deploy/Dockerfile.local new file mode 100644 index 00000000..7d481813 --- /dev/null +++ b/deploy/Dockerfile.local @@ -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"] diff --git a/deploy/local-init.sh b/deploy/local-init.sh new file mode 100644 index 00000000..f1b8fe3d --- /dev/null +++ b/deploy/local-init.sh @@ -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 "=====================================================" diff --git a/docker-compose.local.yml b/docker-compose.local.yml new file mode 100644 index 00000000..fc3801de --- /dev/null +++ b/docker-compose.local.yml @@ -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 diff --git a/modules/EcommerceFinance/Http/Controllers/Api/GoodsReconciliations.php b/modules/EcommerceFinance/Http/Controllers/Api/GoodsReconciliations.php new file mode 100644 index 00000000..a8c5b570 --- /dev/null +++ b/modules/EcommerceFinance/Http/Controllers/Api/GoodsReconciliations.php @@ -0,0 +1,115 @@ +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); + } +} diff --git a/modules/EcommerceFinance/Http/Controllers/Api/SupplierPayables.php b/modules/EcommerceFinance/Http/Controllers/Api/SupplierPayables.php new file mode 100644 index 00000000..90fa5159 --- /dev/null +++ b/modules/EcommerceFinance/Http/Controllers/Api/SupplierPayables.php @@ -0,0 +1,196 @@ +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); + } +} diff --git a/modules/EcommerceFinance/Models/JianSun/AverageUnitPrice.php b/modules/EcommerceFinance/Models/JianSun/AverageUnitPrice.php new file mode 100644 index 00000000..34afacc2 --- /dev/null +++ b/modules/EcommerceFinance/Models/JianSun/AverageUnitPrice.php @@ -0,0 +1,27 @@ + 'decimal:4', + ]; +} diff --git a/modules/EcommerceFinance/Models/JianSun/JianSunModel.php b/modules/EcommerceFinance/Models/JianSun/JianSunModel.php new file mode 100644 index 00000000..19dfbf02 --- /dev/null +++ b/modules/EcommerceFinance/Models/JianSun/JianSunModel.php @@ -0,0 +1,27 @@ + 'datetime', + 'sign_time' => 'datetime', + 'pay_time' => 'datetime', + 'fetch_time' => 'datetime', + 'weight' => 'decimal:4', + 'f_weight' => 'decimal:4', + ]; +} diff --git a/modules/EcommerceFinance/Models/JianSun/JstSupplier.php b/modules/EcommerceFinance/Models/JianSun/JstSupplier.php new file mode 100644 index 00000000..bf90299f --- /dev/null +++ b/modules/EcommerceFinance/Models/JianSun/JstSupplier.php @@ -0,0 +1,31 @@ + 'integer', + 'scmid' => 'integer', + 'postfee' => 'decimal:2', + 'created' => 'datetime', + 'inwhdate' => 'datetime', + 'finishdate' => 'datetime', + ]; +} diff --git a/modules/EcommerceFinance/Models/JianSun/PurchaseInItem.php b/modules/EcommerceFinance/Models/JianSun/PurchaseInItem.php new file mode 100644 index 00000000..8ca00d30 --- /dev/null +++ b/modules/EcommerceFinance/Models/JianSun/PurchaseInItem.php @@ -0,0 +1,39 @@ + 'integer', + 'incount' => 'integer', + 'skucount' => 'integer', + 'payment' => 'decimal:2', + 'created' => 'datetime', + ]; +} diff --git a/modules/EcommerceFinance/Models/JianSun/SkuInfoManage.php b/modules/EcommerceFinance/Models/JianSun/SkuInfoManage.php new file mode 100644 index 00000000..2888db8c --- /dev/null +++ b/modules/EcommerceFinance/Models/JianSun/SkuInfoManage.php @@ -0,0 +1,29 @@ + 'integer', + 'settlementStock' => 'integer', + 'singlePrice' => 'decimal:2', + 'settlementPrice' => 'decimal:2', + 'created' => 'datetime', + 'createTime' => 'datetime', + ]; +} diff --git a/modules/EcommerceFinance/Models/JianSun/SupplierPaymentDetail.php b/modules/EcommerceFinance/Models/JianSun/SupplierPaymentDetail.php new file mode 100644 index 00000000..b7d4d1cd --- /dev/null +++ b/modules/EcommerceFinance/Models/JianSun/SupplierPaymentDetail.php @@ -0,0 +1,36 @@ + 'decimal:2', + 'created' => 'datetime', + 'date' => 'date', + ]; +} diff --git a/modules/EcommerceFinance/Models/JianSun/SupplierPaymentSummary.php b/modules/EcommerceFinance/Models/JianSun/SupplierPaymentSummary.php new file mode 100644 index 00000000..40d9bb39 --- /dev/null +++ b/modules/EcommerceFinance/Models/JianSun/SupplierPaymentSummary.php @@ -0,0 +1,85 @@ + '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', + ]; +} diff --git a/modules/EcommerceFinance/Models/JianSun/UnOrderDetails.php b/modules/EcommerceFinance/Models/JianSun/UnOrderDetails.php new file mode 100644 index 00000000..5fb2e330 --- /dev/null +++ b/modules/EcommerceFinance/Models/JianSun/UnOrderDetails.php @@ -0,0 +1,43 @@ + 'datetime', + 'distributionDate' => 'datetime', + 'shipDate' => 'datetime', + ]; +} diff --git a/modules/EcommerceFinance/Repositories/GoodsReconciliationRepository.php b/modules/EcommerceFinance/Repositories/GoodsReconciliationRepository.php new file mode 100644 index 00000000..ae0d1879 --- /dev/null +++ b/modules/EcommerceFinance/Repositories/GoodsReconciliationRepository.php @@ -0,0 +1,370 @@ +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; + } +} diff --git a/modules/EcommerceFinance/Repositories/SupplierPayableRepository.php b/modules/EcommerceFinance/Repositories/SupplierPayableRepository.php new file mode 100644 index 00000000..85d9c1e6 --- /dev/null +++ b/modules/EcommerceFinance/Repositories/SupplierPayableRepository.php @@ -0,0 +1,540 @@ + 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 !== '')); + } +} diff --git a/modules/EcommerceFinance/Services/JianSunConnection.php b/modules/EcommerceFinance/Services/JianSunConnection.php new file mode 100644 index 00000000..f3289843 --- /dev/null +++ b/modules/EcommerceFinance/Services/JianSunConnection.php @@ -0,0 +1,70 @@ + 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; + } +} diff --git a/resources/assets/js/views/ecommerce-finance/advance-details.js b/resources/assets/js/views/ecommerce-finance/advance-details.js new file mode 100644 index 00000000..0d70936a --- /dev/null +++ b/resources/assets/js/views/ecommerce-finance/advance-details.js @@ -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; diff --git a/resources/assets/js/views/ecommerce-finance/compensation-details.js b/resources/assets/js/views/ecommerce-finance/compensation-details.js new file mode 100644 index 00000000..61e3bd2e --- /dev/null +++ b/resources/assets/js/views/ecommerce-finance/compensation-details.js @@ -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; diff --git a/resources/assets/js/views/ecommerce-finance/goods-reconciliations.js b/resources/assets/js/views/ecommerce-finance/goods-reconciliations.js new file mode 100644 index 00000000..41770d95 --- /dev/null +++ b/resources/assets/js/views/ecommerce-finance/goods-reconciliations.js @@ -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; diff --git a/resources/assets/js/views/ecommerce-finance/invoice-deductions.js b/resources/assets/js/views/ecommerce-finance/invoice-deductions.js new file mode 100644 index 00000000..5866abd0 --- /dev/null +++ b/resources/assets/js/views/ecommerce-finance/invoice-deductions.js @@ -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; diff --git a/resources/assets/js/views/ecommerce-finance/return-reconciliations.js b/resources/assets/js/views/ecommerce-finance/return-reconciliations.js new file mode 100644 index 00000000..29ae55c5 --- /dev/null +++ b/resources/assets/js/views/ecommerce-finance/return-reconciliations.js @@ -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; diff --git a/resources/assets/js/views/ecommerce-finance/sample-deductions.js b/resources/assets/js/views/ecommerce-finance/sample-deductions.js new file mode 100644 index 00000000..2436ea89 --- /dev/null +++ b/resources/assets/js/views/ecommerce-finance/sample-deductions.js @@ -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; diff --git a/resources/lang/zh-CN/marketing_account_bills.php b/resources/lang/zh-CN/marketing_account_bills.php new file mode 100644 index 00000000..f4aecef6 --- /dev/null +++ b/resources/lang/zh-CN/marketing_account_bills.php @@ -0,0 +1,56 @@ + '店铺营销账户账单管理', + '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' => '唯品会', + +]; diff --git a/resources/lang/zh-CN/marketing_pinduoduo_bills.php b/resources/lang/zh-CN/marketing_pinduoduo_bills.php new file mode 100644 index 00000000..eed29a6d --- /dev/null +++ b/resources/lang/zh-CN/marketing_pinduoduo_bills.php @@ -0,0 +1,31 @@ + '拼多多账户账单', + + // 表格列标题(对应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' => '暂无账单数据', + +]; diff --git a/resources/lang/zh-CN/marketing_taobao_alipay_bills.php b/resources/lang/zh-CN/marketing_taobao_alipay_bills.php new file mode 100644 index 00000000..7175f8eb --- /dev/null +++ b/resources/lang/zh-CN/marketing_taobao_alipay_bills.php @@ -0,0 +1,36 @@ + '淘系支付宝账户账单', + + // 表格列标题(对应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' => '支出', + +]; diff --git a/resources/views/banking/marketing_account_bills/form.blade.php b/resources/views/banking/marketing_account_bills/form.blade.php new file mode 100644 index 00000000..4c37305f --- /dev/null +++ b/resources/views/banking/marketing_account_bills/form.blade.php @@ -0,0 +1,188 @@ +@php + $isEdit = $account->exists; +@endphp + + + + {{ $isEdit ? trans('marketing_account_bills.edit_bill') : trans('marketing_account_bills.add_bill') }} + - {{ trans('marketing_account_bills.' . $platform) }} + + + + +
+ @csrf + @if ($isEdit) + @method('PATCH') + @endif + + {{-- 基本信息 --}} +
+

基本信息

+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + {{-- 收支流水 --}} +
+

收支流水(元)

+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + {{-- 消耗明细 --}} +
+

消耗明细(元)

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + {{-- 资金汇总与余额 --}} +
+

资金汇总与余额(元)

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+ + {{ trans('general.cancel') }} + + +
+
+
+
+
diff --git a/resources/views/banking/marketing_account_bills/index.blade.php b/resources/views/banking/marketing_account_bills/index.blade.php new file mode 100644 index 00000000..261c6a1b --- /dev/null +++ b/resources/views/banking/marketing_account_bills/index.blade.php @@ -0,0 +1,186 @@ + + + {{ trans('marketing_account_bills.' . $platform) }} + + + + @can('create-banking-accounts') + + {{ trans('marketing_account_bills.add_bill') }} + + @endcan + + + + + + @php $pageAccountIds = $accounts->pluck('id')->map(fn($id) => (string) $id)->values(); @endphp + +
+
+ + {{-- 操作工具栏 --}} +
+ @can('create-banking-accounts') +
+ @csrf +
+ + +
+ +
+ @endcan + +
+ @can('create-banking-accounts') + + + {{ trans('marketing_account_bills.download_template') }} + + @endcan + @can('update-banking-accounts') + + @endcan +
+
+ + @if ($errors->has('import')) +
+ @foreach ($errors->get('import') as $message) +

{{ $message }}

+ @endforeach +
+ @endif + + {{-- 15列数据表格 --}} +
+ + + + + + + + + + + + + + + + + + + + + + + + + @if ($accounts->isEmpty()) + + + + @else + @foreach ($accounts as $account) + + + + + + + + + + + + + + + + + + + + + @endforeach + @endif + +
+ 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" /> + {{ trans('marketing_account_bills.store_name') }}{{ trans('marketing_account_bills.bill_date') }}{{ trans('marketing_account_bills.total_deposit') }}{{ trans('marketing_account_bills.total_transfer_in') }}{{ trans('marketing_account_bills.total_transfer_out') }}{{ trans('marketing_account_bills.non_grant_consumption') }}{{ trans('marketing_account_bills.grant_consumption') }}{{ trans('marketing_account_bills.rebate_redpack_consumption') }}{{ trans('marketing_account_bills.discount_redpack_consumption') }}{{ trans('marketing_account_bills.shared_wallet_consumption') }}{{ trans('marketing_account_bills.shared_grant_consumption') }}{{ trans('marketing_account_bills.daily_total_amount') }}{{ trans('marketing_account_bills.period_end_balance') }}{{ trans('marketing_account_bills.grant_balance') }}{{ trans('marketing_account_bills.non_grant_balance') }}{{ trans('marketing_account_bills.notes') }}{{ trans('general.actions') }}
+ {{ trans('marketing_account_bills.no_records') }} +
+ + {{ $account->store_name }}{{ $account->bill_date->format('Y-m-d') }}{{ number_format($account->total_deposit, 2) }}{{ number_format($account->total_transfer_in, 2) }}{{ number_format($account->total_transfer_out, 2) }}{{ number_format($account->non_grant_consumption, 2) }}{{ number_format($account->grant_consumption, 2) }}{{ number_format($account->rebate_redpack_consumption, 2) }}{{ number_format($account->discount_redpack_consumption, 2) }}{{ number_format($account->shared_wallet_consumption, 2) }}{{ number_format($account->shared_grant_consumption, 2) }}{{ number_format($account->daily_total_amount, 2) }}{{ number_format($account->period_end_balance, 2) }}{{ number_format($account->grant_balance, 2) }}{{ number_format($account->non_grant_balance, 2) }} + {{ \Illuminate\Support\Str::limit($account->notes, 20) }} + + @can('update-banking-accounts') + {{ trans('general.edit') }} + @endcan + @can('delete-banking-accounts') +
+ @csrf + @method('DELETE') + +
+ @endcan +
+
+ + @unless ($accounts->isEmpty()) +
{{ $accounts->links() }}
+ @endunless +
+
+
diff --git a/resources/views/banking/marketing_account_bills/partials/embedded-table.blade.php b/resources/views/banking/marketing_account_bills/partials/embedded-table.blade.php new file mode 100644 index 00000000..b90fc786 --- /dev/null +++ b/resources/views/banking/marketing_account_bills/partials/embedded-table.blade.php @@ -0,0 +1,161 @@ + + + +@php $pageAccountIds = $accounts->pluck('id')->map(fn($id) => (string) $id)->values(); @endphp + +
+
+ + {{-- 操作工具栏 --}} +
+ @can('create-banking-accounts') +
+ + + {{ trans('marketing_account_bills.add_bill') }} + +
+ @csrf +
+ + +
+ +
+ + + {{ trans('marketing_account_bills.download_template') }} + +
+ @endcan +
+ @can('update-banking-accounts') + + @endcan +
+
+ + @if ($errors->has('import')) +
+ @foreach ($errors->get('import') as $message) +

{{ $message }}

+ @endforeach +
+ @endif + + {{-- 15列数据表格 --}} +
+ + + + + + + + + + + + + + + + + + + + + + + + + @if ($accounts->isEmpty()) + + + + @else + @foreach ($accounts as $account) + + + + + + + + + + + + + + + + + + + + + @endforeach + @endif + +
+ 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" /> + {{ trans('marketing_account_bills.store_name') }}{{ trans('marketing_account_bills.bill_date') }}{{ trans('marketing_account_bills.total_deposit') }}{{ trans('marketing_account_bills.total_transfer_in') }}{{ trans('marketing_account_bills.total_transfer_out') }}{{ trans('marketing_account_bills.non_grant_consumption') }}{{ trans('marketing_account_bills.grant_consumption') }}{{ trans('marketing_account_bills.rebate_redpack_consumption') }}{{ trans('marketing_account_bills.discount_redpack_consumption') }}{{ trans('marketing_account_bills.shared_wallet_consumption') }}{{ trans('marketing_account_bills.shared_grant_consumption') }}{{ trans('marketing_account_bills.daily_total_amount') }}{{ trans('marketing_account_bills.period_end_balance') }}{{ trans('marketing_account_bills.grant_balance') }}{{ trans('marketing_account_bills.non_grant_balance') }}{{ trans('marketing_account_bills.notes') }}{{ trans('general.actions') }}
+ {{ trans('marketing_account_bills.no_records') }} +
+ + {{ $account->store_name }}{{ $account->bill_date->format('Y-m-d') }}{{ number_format($account->total_deposit, 2) }}{{ number_format($account->total_transfer_in, 2) }}{{ number_format($account->total_transfer_out, 2) }}{{ number_format($account->non_grant_consumption, 2) }}{{ number_format($account->grant_consumption, 2) }}{{ number_format($account->rebate_redpack_consumption, 2) }}{{ number_format($account->discount_redpack_consumption, 2) }}{{ number_format($account->shared_wallet_consumption, 2) }}{{ number_format($account->shared_grant_consumption, 2) }}{{ number_format($account->daily_total_amount, 2) }}{{ number_format($account->period_end_balance, 2) }}{{ number_format($account->grant_balance, 2) }}{{ number_format($account->non_grant_balance, 2) }} + {{ \Illuminate\Support\Str::limit($account->notes, 20) }} + + @can('update-banking-accounts') + {{ trans('general.edit') }} + @endcan + @can('delete-banking-accounts') +
+ @csrf + @method('DELETE') + +
+ @endcan +
+
+ + @unless ($accounts->isEmpty()) +
{{ $accounts->links() }}
+ @endunless +
diff --git a/resources/views/banking/marketing_account_bills/store-index.blade.php b/resources/views/banking/marketing_account_bills/store-index.blade.php new file mode 100644 index 00000000..f33a66ef --- /dev/null +++ b/resources/views/banking/marketing_account_bills/store-index.blade.php @@ -0,0 +1,50 @@ + + + {{ trans('marketing_account_bills.title') }} + + + + + +
+

{{ trans('marketing_account_bills.title') }}

+ +
+ @foreach ($platforms as $slug => $labelKey) + @php + $count = \App\Models\Banking\MarketingAccountBill::platform($slug)->count(); + @endphp + +
+
+

+ {{ trans('marketing_account_bills.' . $labelKey) }} +

+

+ {{ $count }} {{ trans_choice('general.records', $count) }} +

+
+ +
+
+ @endforeach +
+
+
+
diff --git a/resources/views/banking/marketing_pinduoduo_bills/form.blade.php b/resources/views/banking/marketing_pinduoduo_bills/form.blade.php new file mode 100644 index 00000000..615b39d0 --- /dev/null +++ b/resources/views/banking/marketing_pinduoduo_bills/form.blade.php @@ -0,0 +1,113 @@ +@php + $isEdit = $bill->exists; +@endphp + + + + {{ $isEdit ? trans('marketing_pinduoduo_bills.edit_bill') : trans('marketing_pinduoduo_bills.add_bill') }} + - {{ trans('marketing_pinduoduo_bills.title') }} + + + + +
+ @csrf + @if ($isEdit) + @method('PATCH') + @endif + + {{-- 交易信息 --}} +
+

交易信息

+
+
+ + +
+
+ + +
+
+
+ + {{-- 资金与流水类型 --}} +
+

资金与流水类型

+
+
+ + +
+
+ + +
+
+
+ + {{-- 金额信息 --}} +
+

金额信息(元)

+
+
+ + +
+
+ + +
+
+
+ + {{-- 交易摘要 --}} +
+
+ + +
+
+ +
+ + {{ trans('general.cancel') }} + + +
+
+
+
+
diff --git a/resources/views/banking/marketing_pinduoduo_bills/index.blade.php b/resources/views/banking/marketing_pinduoduo_bills/index.blade.php new file mode 100644 index 00000000..2f48b6ec --- /dev/null +++ b/resources/views/banking/marketing_pinduoduo_bills/index.blade.php @@ -0,0 +1,9 @@ + + + {{ trans('marketing_pinduoduo_bills.title') }} + + + + @include('banking.marketing_pinduoduo_bills.partials.embedded-table') + + diff --git a/resources/views/banking/marketing_pinduoduo_bills/partials/embedded-table.blade.php b/resources/views/banking/marketing_pinduoduo_bills/partials/embedded-table.blade.php new file mode 100644 index 00000000..151f1639 --- /dev/null +++ b/resources/views/banking/marketing_pinduoduo_bills/partials/embedded-table.blade.php @@ -0,0 +1,143 @@ + + + +@php $pageBillIds = $bills->pluck('id')->map(fn($id) => (string) $id)->values(); @endphp + +
+
+ + {{-- 操作工具栏 --}} +
+ @can('create-banking-accounts') +
+ + + {{ trans('marketing_pinduoduo_bills.add_bill') }} + +
+ @csrf +
+ + +
+ +
+ + + {{ trans('marketing_pinduoduo_bills.download_template') }} + +
+ @endcan +
+ @can('update-banking-accounts') + + @endcan +
+
+ + @if ($errors->has('import')) +
+ @foreach ($errors->get('import') as $message) +

{{ $message }}

+ @endforeach +
+ @endif + + {{-- 7列数据表格 --}} +
+ + + + + + + + + + + + + + + + @if ($bills->isEmpty()) + + + + @else + @foreach ($bills as $bill) + + + + + + + + + + + + @endforeach + @endif + +
+ 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" /> + {{ trans('marketing_pinduoduo_bills.store_name') }}{{ trans('marketing_pinduoduo_bills.transaction_time') }}{{ trans('marketing_pinduoduo_bills.fund_type') }}{{ trans('marketing_pinduoduo_bills.flow_type') }}{{ trans('marketing_pinduoduo_bills.transaction_amount') }}{{ trans('marketing_pinduoduo_bills.balance') }}{{ trans('marketing_pinduoduo_bills.transaction_summary') }}{{ trans('general.actions') }}
+ {{ trans('marketing_pinduoduo_bills.no_records') }} +
+ + {{ $bill->store_name }}{{ $bill->transaction_time?->format('Y-m-d H:i:s') }}{{ $bill->fund_type }}{{ $bill->flow_type }}{{ number_format($bill->transaction_amount, 2) }}{{ number_format($bill->balance, 2) }} + {{ \Illuminate\Support\Str::limit($bill->transaction_summary, 20) }} + + @can('update-banking-accounts') + {{ trans('general.edit') }} + @endcan + @can('delete-banking-accounts') +
+ @csrf + @method('DELETE') + +
+ @endcan +
+
+ + @unless ($bills->isEmpty()) +
{{ $bills->links() }}
+ @endunless +
diff --git a/resources/views/banking/marketing_taobao_alipay_bills/form.blade.php b/resources/views/banking/marketing_taobao_alipay_bills/form.blade.php new file mode 100644 index 00000000..19808e45 --- /dev/null +++ b/resources/views/banking/marketing_taobao_alipay_bills/form.blade.php @@ -0,0 +1,126 @@ +@php + $isEdit = $bill->exists; +@endphp + + + + {{ $isEdit ? trans('marketing_taobao_alipay_bills.edit_bill') : trans('marketing_taobao_alipay_bills.add_bill') }} + - {{ trans('marketing_taobao_alipay_bills.title') }} + + + + +
+ @csrf + @if ($isEdit) + @method('PATCH') + @endif + + {{-- 交易信息 --}} +
+

交易信息

+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + {{-- 收支明细 --}} +
+

收支明细

+
+
+ + +
+
+ + +
+
+
+ + {{-- 金额信息 --}} +
+

金额信息(元)

+
+
+ + +
+
+ + +
+
+
+ + {{-- 备注 --}} +
+
+ + +
+
+ +
+ + {{ trans('general.cancel') }} + + +
+
+
+
+
diff --git a/resources/views/banking/marketing_taobao_alipay_bills/index.blade.php b/resources/views/banking/marketing_taobao_alipay_bills/index.blade.php new file mode 100644 index 00000000..b601b2cc --- /dev/null +++ b/resources/views/banking/marketing_taobao_alipay_bills/index.blade.php @@ -0,0 +1,9 @@ + + + {{ trans('marketing_taobao_alipay_bills.title') }} + + + + @include('banking.marketing_taobao_alipay_bills.partials.embedded-table') + + diff --git a/resources/views/banking/marketing_taobao_alipay_bills/partials/embedded-table.blade.php b/resources/views/banking/marketing_taobao_alipay_bills/partials/embedded-table.blade.php new file mode 100644 index 00000000..14f9d2e4 --- /dev/null +++ b/resources/views/banking/marketing_taobao_alipay_bills/partials/embedded-table.blade.php @@ -0,0 +1,153 @@ + + + +@php $pageBillIds = $bills->pluck('id')->map(fn($id) => (string) $id)->values(); @endphp + +
+
+ + {{-- 操作工具栏 --}} +
+ @can('create-banking-accounts') +
+ + + {{ trans('marketing_taobao_alipay_bills.add_bill') }} + +
+ @csrf +
+ + +
+ +
+ + + {{ trans('marketing_taobao_alipay_bills.download_template') }} + +
+ @endcan +
+ @can('update-banking-accounts') + + @endcan +
+
+ + @if ($errors->has('import')) +
+ @foreach ($errors->get('import') as $message) +

{{ $message }}

+ @endforeach +
+ @endif + + {{-- 8列数据表格 --}} +
+ + + + + + + + + + + + + + + + + @if ($bills->isEmpty()) + + + + @else + @foreach ($bills as $bill) + + + + + + + + + + + + + @endforeach + @endif + +
+ 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" /> + {{ trans('marketing_taobao_alipay_bills.store_name') }}{{ trans('marketing_taobao_alipay_bills.transaction_date') }}{{ trans('marketing_taobao_alipay_bills.bookkeeping_time') }}{{ trans('marketing_taobao_alipay_bills.income_expense_type') }}{{ trans('marketing_taobao_alipay_bills.transaction_type') }}{{ trans('marketing_taobao_alipay_bills.operation_amount') }}{{ trans('marketing_taobao_alipay_bills.balance_after') }}{{ trans('marketing_taobao_alipay_bills.notes') }}{{ trans('general.actions') }}
+ {{ trans('marketing_taobao_alipay_bills.no_records') }} +
+ + {{ $bill->store_name }}{{ $bill->transaction_date->format('Y-m-d') }}{{ $bill->bookkeeping_time?->format('Y-m-d H:i:s') }} + @if ($bill->income_expense_type === '收入' || $bill->income_expense_type === 'income') + {{ $bill->income_expense_type }} + @else + {{ $bill->income_expense_type }} + @endif + {{ $bill->transaction_type }}{{ number_format($bill->operation_amount, 2) }}{{ number_format($bill->balance_after, 2) }} + {{ \Illuminate\Support\Str::limit($bill->notes, 20) }} + + @can('update-banking-accounts') + {{ trans('general.edit') }} + @endcan + @can('delete-banking-accounts') +
+ @csrf + @method('DELETE') + +
+ @endcan +
+
+ + @unless ($bills->isEmpty()) +
{{ $bills->links() }}
+ @endunless +
diff --git a/resources/views/business/pages/show.blade.php b/resources/views/business/pages/show.blade.php index 356de8f2..11de90bf 100644 --- a/resources/views/business/pages/show.blade.php +++ b/resources/views/business/pages/show.blade.php @@ -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 @@ @endif -
+ @if ($page && !empty($content) && !empty($content['view'])) +
+ @include($content['view'], $content) +
+ @else +
+ @endif diff --git a/resources/views/business/pages/supplier-payables/advance-details.blade.php b/resources/views/business/pages/supplier-payables/advance-details.blade.php new file mode 100644 index 00000000..917dd7f4 --- /dev/null +++ b/resources/views/business/pages/supplier-payables/advance-details.blade.php @@ -0,0 +1,111 @@ +
+
+
+ + 借支明细数据源 finance_advanceLoans 当前仅 4 行数据;表头 21 列中部分字段(如实际打款金额、分期扣款、票据等)在数据库中无对应列,已显示为占位符。 +
+ + {{-- 统计卡片 --}} +
+
+
借支总金额
+
@{{ formatMoney(summary.totalAmount) }}
+
+
+
总记录数
+
@{{ (summary.totalCount || 0).toLocaleString() }}
+
+
+ + {{-- 筛选栏 --}} +
+
+ + + + 查询 + 重置 +
+
+ + {{-- 数据表格 --}} +
+
+ 加载中... +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
共 @{{ total }} 条记录
+ + +
+
+
+
+ +@push('body_end') + +@endpush diff --git a/resources/views/business/pages/supplier-payables/compensation-details.blade.php b/resources/views/business/pages/supplier-payables/compensation-details.blade.php new file mode 100644 index 00000000..869e836e --- /dev/null +++ b/resources/views/business/pages/supplier-payables/compensation-details.blade.php @@ -0,0 +1,73 @@ +
+
+ {{-- 统计卡片 --}} +
+
+
赔付总金额
+
@{{ formatMoney(summary.totalAmount) }}
+
+
+
总记录数
+
@{{ (summary.totalCount || 0).toLocaleString() }}
+
+
+ + {{-- 筛选栏 --}} +
+
+ + + + 查询 + 重置 +
+
+ + {{-- 数据表格 --}} +
+
+ 加载中... +
+ + + + + + + + + + + + + + + + + +
+
共 @{{ total }} 条记录
+ + +
+
+
+
+ +@push('body_end') + +@endpush diff --git a/resources/views/business/pages/supplier-payables/invoice-deductions.blade.php b/resources/views/business/pages/supplier-payables/invoice-deductions.blade.php new file mode 100644 index 00000000..3934e82e --- /dev/null +++ b/resources/views/business/pages/supplier-payables/invoice-deductions.blade.php @@ -0,0 +1,79 @@ +
+
+ {{-- 统计卡片 --}} +
+
+
扣款总金额
+
@{{ formatMoney(summary.totalPrice) }}
+
+
+
总记录数
+
@{{ (summary.totalCount || 0).toLocaleString() }}
+
+
+ + {{-- 筛选栏 --}} +
+
+ + + + 查询 + 重置 +
+
+ + {{-- 数据表格 --}} +
+
+ 加载中... +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+
共 @{{ total }} 条记录
+ + +
+
+
+
+ +@push('body_end') + +@endpush diff --git a/resources/views/business/pages/supplier-payables/payment-reconciliation.blade.php b/resources/views/business/pages/supplier-payables/payment-reconciliation.blade.php new file mode 100644 index 00000000..f73f0176 --- /dev/null +++ b/resources/views/business/pages/supplier-payables/payment-reconciliation.blade.php @@ -0,0 +1,139 @@ +
+
+ {{-- 统计卡片 --}} +
+
+
对账总金额
+
@{{ formatMoney(summary.totalAmount) }}
+
+
+
预估总金额
+
@{{ formatMoney(summary.totalGuessAmount) }}
+
+
+
已发出
+
@{{ summary.sentCount.toLocaleString() }}
+
+
+
未发出
+
@{{ summary.unsentCount.toLocaleString() }}
+
+
+ + {{-- 筛选栏 --}} +
+
+ + + + + + + + + + + + + +
+
+ + + + + + + + + + + +
+
+ + {{-- Tab 切换 --}} +
+ +
+ + {{-- 数据表格 --}} +
+
+ 加载中... +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{-- 分页 --}} +
+
共 @{{ total }} 条记录
+ + +
+
+
+
+ +@push('body_end') + +@endpush diff --git a/resources/views/business/pages/supplier-payables/return-reconciliation.blade.php b/resources/views/business/pages/supplier-payables/return-reconciliation.blade.php new file mode 100644 index 00000000..ef9cbc11 --- /dev/null +++ b/resources/views/business/pages/supplier-payables/return-reconciliation.blade.php @@ -0,0 +1,107 @@ +
+
+ {{-- 统计卡片 --}} +
+
+
对账总金额
+
@{{ formatMoney(summary.totalAmount) }}
+
+
+
预估总金额
+
@{{ formatMoney(summary.totalGuessAmount) }}
+
+
+
总记录数
+
@{{ (summary.totalCount || 0).toLocaleString() }}
+
+
+ + {{-- 筛选栏 --}} +
+
+ + + + + + + + + 查询 + 重置 +
+
+ + {{-- 数据表格 --}} +
+
+ 加载中... +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
共 @{{ total }} 条记录
+ + +
+
+
+
+ +@push('body_end') + +@endpush diff --git a/resources/views/business/pages/supplier-payables/sample-deductions.blade.php b/resources/views/business/pages/supplier-payables/sample-deductions.blade.php new file mode 100644 index 00000000..8594621e --- /dev/null +++ b/resources/views/business/pages/supplier-payables/sample-deductions.blade.php @@ -0,0 +1,93 @@ +
+
+ {{-- 统计卡片 --}} +
+
+
合计扣款金额
+
@{{ formatMoney(summary.totalAmount) }}
+
+
+
预估总金额
+
@{{ formatMoney(summary.totalEstimate) }}
+
+
+
总记录数
+
@{{ (summary.totalCount || 0).toLocaleString() }}
+
+
+ + {{-- 筛选栏 --}} +
+
+ + + + + + + 查询 + 重置 +
+
+ + {{-- 数据表格 --}} +
+
+ 加载中... +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
共 @{{ total }} 条记录
+ + +
+
+
+
+ +@push('body_end') + +@endpush diff --git a/routes/admin.php b/routes/admin.php index 11cecef6..ada7aafc 100644 --- a/routes/admin.php +++ b/routes/admin.php @@ -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'); diff --git a/test-data/database.sqlite b/test-data/database.sqlite index 9a1ba698..4edff84e 100644 Binary files a/test-data/database.sqlite and b/test-data/database.sqlite differ