Files
xin-procurement/app/Http/Controllers/Mini/ReportController.php
T
2026-08-21 12:05:31 +08:00

143 lines
6.6 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\Models\StoreOrderItemModel;
use App\Models\StoreOrderModel;
use Carbon\Carbon;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Modules\AnnoRoute\Attribute\GetRoute;
use Modules\AnnoRoute\Attribute\RequestAttribute;
/**
* 小程序运营报表(门店视角:采购金额汇总与单品占比)
*/
#[RequestAttribute('/mini', 'mini', authGuard: 'users')]
class ReportController extends BaseMiniController
{
/**
* 采购金额报表:选定周期内采购总金额 + 每个单品累计采购金额/占比
*
* ?preset=week|last_week|month|last_month 周期预设(默认 month 本月);
* ?start_date=&end_date= 自定义区间(Y-m-d,需成对出现,优先于 preset)。
* 统计口径:本店订货单(排除已取消与后台已删除订单),金额=订货明细快照金额之和,
* 单品按 product_id 聚合(品名/规格/单位取明细快照),按金额降序,占比保留 1 位小数(四舍五入)
*/
#[GetRoute('/report/purchase', authorize: true)]
public function purchase(Request $request): JsonResponse
{
$params = $request->validate([
'preset' => 'nullable|string|in:week,last_week,month,last_month',
'start_date' => 'required_with:end_date|nullable|date_format:Y-m-d',
'end_date' => 'required_with:start_date|nullable|date_format:Y-m-d|after_or_equal:start_date',
], [
'preset.in' => 'preset 参数只能是 week/last_week/month/last_month',
'start_date.required_with' => '自定义区间需同时提供开始与结束日期',
'end_date.required_with' => '自定义区间需同时提供开始与结束日期',
'start_date.date_format' => '开始日期格式为 Y-m-d',
'end_date.date_format' => '结束日期格式为 Y-m-d',
'end_date.after_or_equal' => '结束日期不能早于开始日期',
]);
$store = $this->currentStore($request);
[$startDate, $endDate, $preset] = $this->resolveDateRange($params);
// 明细联订货单:按订单日期/状态过滤(已取消、已删除订单不计入)
$baseQuery = StoreOrderItemModel::query()
->join('store_order', static function ($join): void {
$join->on('store_order.id', '=', 'store_order_item.order_id')
->whereNull('store_order.deleted_at');
})
->where('store_order_item.store_id', $store->id)
->where('store_order.status', '<>', StoreOrderModel::STATUS_CANCELLED)
->whereDate('store_order.order_date', '>=', $startDate)
->whereDate('store_order.order_date', '<=', $endDate);
$totals = (clone $baseQuery)
->selectRaw('COALESCE(SUM(store_order_item.amount), 0) as total_amount')
->selectRaw('COALESCE(SUM(store_order_item.quantity), 0) as total_quantity')
->selectRaw('COUNT(DISTINCT store_order_item.order_id) as order_count')
->first();
$totalAmount = bcadd((string) $totals->total_amount, '0', 2);
$rows = (clone $baseQuery)
->select('store_order_item.product_id')
->selectRaw('MAX(store_order_item.product_name) as product_name')
->selectRaw('MAX(store_order_item.product_spec) as product_spec')
->selectRaw('MAX(store_order_item.unit) as unit')
->selectRaw('COALESCE(SUM(store_order_item.quantity), 0) as quantity')
->selectRaw('COALESCE(SUM(store_order_item.weight), 0) as weight')
->selectRaw('COALESCE(SUM(store_order_item.amount), 0) as amount')
->groupBy('store_order_item.product_id')
->orderByDesc('amount')
->orderBy('store_order_item.product_id')
->get();
$items = $rows->map(static function (StoreOrderItemModel $row) use ($totalAmount): array {
// 占比 %(1 位小数,四舍五入):tenths = round(amount × 1000 ÷ total),再 ÷10
$percent = 0.0;
if (bccomp($totalAmount, '0', 2) > 0) {
$tenths = bcdiv(
bcadd(bcmul((string) $row->amount, '1000', 2), bcdiv($totalAmount, '2', 0), 2),
$totalAmount,
0
);
$percent = (float) bcdiv($tenths, '10', 1);
}
return [
'product_id' => (int) $row->product_id,
'product_name' => (string) $row->product_name,
'product_spec' => (string) $row->product_spec,
'unit' => (string) $row->unit,
'quantity' => (int) $row->quantity,
'weight' => bcadd((string) $row->weight, '0', 3),
'amount' => bcadd((string) $row->amount, '0', 2),
'percent' => $percent,
];
})->values()->all();
return $this->success([
'preset' => $preset,
'start_date' => $startDate,
'end_date' => $endDate,
'total_amount' => $totalAmount,
'total_quantity' => (int) $totals->total_quantity,
'order_count' => (int) $totals->order_count,
'item_count' => count($items),
'items' => $items,
]);
}
/**
* 解析统计区间:自定义日期优先;否则按 preset 推导(进行中的周期封顶到今天)
*
* @param array{preset?: ?string, start_date?: ?string, end_date?: ?string} $params
* @return array{0: string, 1: string, 2: string} [开始日期, 结束日期, 实际 preset]
*/
private function resolveDateRange(array $params): array
{
if (! empty($params['start_date']) && ! empty($params['end_date'])) {
return [$params['start_date'], $params['end_date'], 'custom'];
}
$preset = (string) ($params['preset'] ?? 'month');
$today = Carbon::today();
[$start, $end] = match ($preset) {
'week' => [$today->copy()->startOfWeek(), $today->copy()->endOfWeek()],
'last_week' => [$today->copy()->subWeek()->startOfWeek(), $today->copy()->subWeek()->endOfWeek()],
'last_month' => [$today->copy()->subMonthNoOverflow()->startOfMonth(), $today->copy()->subMonthNoOverflow()->endOfMonth()],
default => [$today->copy()->startOfMonth(), $today->copy()->endOfMonth()],
};
// 未来日期本无数据,封顶到今天避免误解
if ($end->greaterThan($today)) {
$end = $today->copy();
}
return [$start->toDateString(), $end->toDateString(), $preset];
}
}