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 */ 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(), ]; } }