Files
xin-procurement/app/Http/Controllers/Mini/BillController.php
T
2026-08-14 20:28:07 +08:00

195 lines
7.7 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace App\Http\Controllers\Mini;
use App\Exceptions\RepositoryException;
use App\Exports\BillExport;
use App\Models\BillModel;
use App\Services\BillDetailService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Maatwebsite\Excel\Facades\Excel;
use Modules\AnnoRoute\Attribute\GetRoute;
use Modules\AnnoRoute\Attribute\RequestAttribute;
use Symfony\Component\HttpFoundation\Response;
/**
* 小程序门店账单(采购单完成后由后台生成,门店端只读)
*/
#[RequestAttribute('/mini', 'mini', authGuard: 'users')]
class BillController extends BaseMiniController
{
/**
* 账单列表:当前门店强制过滤
* ?status= 支付状态(0未支付 1已支付);?payable=1 仅可发起付款(未支付且未在审核中,供合并付款选择页);
* ?start_date=&end_date= 账单日期区间;?page=&pageSize= 分页(pageSize 上限 50
* 响应附加 summary:待支付笔数/金额(仅按门店口径,不受列表筛选影响)
*/
#[GetRoute('/bill', authorize: true)]
public function index(Request $request): JsonResponse
{
$params = $request->validate([
'status' => 'nullable|integer|in:0,1',
'payable' => 'nullable|boolean',
'start_date' => 'nullable|date_format:Y-m-d',
'end_date' => 'nullable|date_format:Y-m-d|after_or_equal:start_date',
'page' => 'nullable|integer|min:1',
'pageSize' => 'nullable|integer|min:1|max:50',
], [
'status.in' => '支付状态不正确',
'start_date.date_format' => '开始日期格式为 Y-m-d',
'end_date.date_format' => '结束日期格式为 Y-m-d',
'end_date.after_or_equal' => '结束日期不能早于开始日期',
'pageSize.max' => '每页数量最大 50',
]);
$user = $this->currentUser($request);
$store = $this->ensureStoreBound($user);
$query = BillModel::query()
->where('store_id', $store->id)
->with('purchase:id,purchase_no,purchase_date');
if (isset($params['status'])) {
$query->where('status', (int) $params['status']);
}
if ($request->boolean('payable')) {
$query->where('status', BillModel::STATUS_UNPAID)->where('payment_id', 0);
}
if (! empty($params['start_date'])) {
$query->whereDate('bill_date', '>=', $params['start_date']);
}
if (! empty($params['end_date'])) {
$query->whereDate('bill_date', '<=', $params['end_date']);
}
$paginator = $query->orderBy('bill_date', 'desc')
->orderBy('id', 'desc')
->paginate((int) ($params['pageSize'] ?? 10));
$cycleDays = (int) $store->payment_cycle_days;
$paginator->getCollection()->transform(
fn (BillModel $bill): array => $this->formatBill($bill, $cycleDays)
);
// 待支付汇总(合并付款入口的头部统计)
$unpaid = BillModel::query()
->where('store_id', $store->id)
->where('status', BillModel::STATUS_UNPAID)
->selectRaw('COUNT(*) as aggregate_count, COALESCE(SUM(total_amount), 0) as aggregate_amount')
->first();
$data = $paginator->toArray();
$data['summary'] = [
'unpaid_count' => (int) $unpaid->aggregate_count,
'unpaid_amount' => bcadd((string) $unpaid->aggregate_amount, '0', 2),
];
return $this->success($data);
}
/**
* 合并导出账单(静态路由放在 /bill/{id} 之前声明)
* 仅可导出本店账单(强制 store_id 过滤,张数不匹配说明混入无效账单整批拒绝);
* ?ids=1,2,3 必选(1~100 张);?category_id= 一级分类过滤(0/缺省=全部)
*/
#[GetRoute('/bill/export', authorize: true)]
public function export(Request $request): Response
{
$data = $request->validate([
'ids' => 'required|string',
'category_id' => 'nullable|integer|min:0',
]);
$user = $this->currentUser($request);
$store = $this->ensureStoreBound($user);
$ids = array_values(array_unique(array_filter(array_map('intval', explode(',', (string) $data['ids'])))));
if ($ids === [] || count($ids) > 100) {
throw new RepositoryException('请选择 1~100 张账单');
}
$bills = BillModel::with('store:id,name')
->where('store_id', $store->id)
->whereIn('id', $ids)
->orderBy('bill_date')
->orderBy('id')
->get();
if ($bills->isEmpty()) {
throw new RepositoryException('账单不存在');
}
if ($bills->count() !== count($ids)) {
throw new RepositoryException('存在无效账单,请刷新后重试');
}
return Excel::download(
new BillExport($bills, (int) ($data['category_id'] ?? 0)),
'门店账单_' . now()->format('Ymd_His') . '.xlsx'
);
}
/** 账单详情(校验归属:仅能查看本店账单;含合并后的商品明细与关联订单) */
#[GetRoute('/bill/{id}', authorize: true, where: ['id' => '[0-9]+'])]
public function detail(int $id, Request $request): JsonResponse
{
$user = $this->currentUser($request);
$store = $this->ensureStoreBound($user);
$bill = BillModel::with('purchase:id,purchase_no,purchase_date')
->where('store_id', $store->id)
->find($id);
if ($bill === null) {
throw new RepositoryException('账单不存在');
}
$orders = $bill->orders()
->orderBy('id')
->get(['id', 'order_no', 'order_date', 'total_quantity', 'total_weight', 'total_amount', 'status'])
->toArray();
return $this->success([
'bill' => $this->formatBill($bill, (int) $store->payment_cycle_days),
'items' => app(BillDetailService::class)->mergedItems($bill),
'orders' => $orders,
]);
}
/**
* 账单输出格式化(列表行与详情共用)
* 推导支付进度(待支付/审核中/已支付),按门店回款周期计算应结算日期;
* 剔除收款操作人等后台字段
*
* @return array<string, mixed>
*/
private function formatBill(BillModel $bill, int $cycleDays): array
{
$payState = $bill->payState();
return [
'id' => $bill->id,
'bill_no' => $bill->bill_no,
'bill_date' => $bill->bill_date->toDateString(),
'purchase_id' => $bill->purchase_id,
'purchase' => $bill->purchase?->toArray(),
'product_amount' => $bill->product_amount,
'delivery_fee' => $bill->delivery_fee,
'box_num' => $bill->box_num,
'tray_num' => $bill->tray_num,
'box_price' => $bill->box_price,
'tray_price' => $bill->tray_price,
'added_amount' => $bill->added_amount,
'total_amount' => $bill->total_amount,
'status' => $bill->status,
'status_name' => BillModel::STATUS_NAMES[$bill->status] ?? '',
'pay_state' => $payState,
'pay_state_name' => BillModel::PAY_STATE_NAMES[$payState],
'can_pay' => $payState === BillModel::PAY_STATE_UNPAID,
'payment_id' => $bill->payment_id,
'settlement_date' => $bill->bill_date->copy()->addDays($cycleDays)->toDateString(),
'paid_at' => $bill->paid_at?->toDateTimeString(),
'pay_remark' => $bill->pay_remark,
'remark' => $bill->remark,
'created_at' => $bill->created_at?->toDateTimeString(),
];
}
}