143 lines
6.6 KiB
PHP
143 lines
6.6 KiB
PHP
<?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];
|
||
}
|
||
}
|