193 lines
7.6 KiB
PHP
193 lines
7.6 KiB
PHP
<?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',
|
||
]);
|
||
|
||
$store = $this->currentStore($request);
|
||
|
||
$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',
|
||
]);
|
||
|
||
$store = $this->currentStore($request);
|
||
|
||
$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
|
||
{
|
||
$store = $this->currentStore($request);
|
||
|
||
$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,
|
||
'after_sale' => $bill->after_sale,
|
||
'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(),
|
||
];
|
||
}
|
||
}
|