运营报表
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -18,7 +18,7 @@ use Modules\AnnoRoute\Attribute\RequestAttribute;
|
|||||||
class ProductController extends BaseMiniController
|
class ProductController extends BaseMiniController
|
||||||
{
|
{
|
||||||
/** 分类树(全部启用分类,含暂无商品的分类) */
|
/** 分类树(全部启用分类,含暂无商品的分类) */
|
||||||
#[GetRoute('/product/categories', authorize: true)]
|
#[GetRoute('/product/categories', authorize: false)]
|
||||||
public function categories(): JsonResponse
|
public function categories(): JsonResponse
|
||||||
{
|
{
|
||||||
return $this->success(ProductCategoryModel::getTreeData(['*'], true));
|
return $this->success(ProductCategoryModel::getTreeData(['*'], true));
|
||||||
|
|||||||
@@ -0,0 +1,142 @@
|
|||||||
|
<?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];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
# 小程序接口文档:采购运营报表
|
||||||
|
|
||||||
|
> 门店端运营报表:拉取「一周 / 一月 / 任意选定时间段」内的采购总金额,以及每个单品的累计采购金额与金额占比。
|
||||||
|
>
|
||||||
|
> 示例场景:上个月我店一共采购了 10 万元,其中土豆 8764 元(占比 8.8%)、洋葱 3664 元(占比 3.7%)。
|
||||||
|
|
||||||
|
## 接口信息
|
||||||
|
|
||||||
|
| 项 | 值 |
|
||||||
|
|---|---|
|
||||||
|
| 请求方式 | `GET` |
|
||||||
|
| 路径 | `/mini/report/purchase` |
|
||||||
|
| 鉴权 | 门店 token(`Authorization: Bearer <token>`),与其他小程序接口一致 |
|
||||||
|
| 数据范围 | 仅当前登录门店自身的订货数据 |
|
||||||
|
|
||||||
|
## 请求参数
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `preset` | string | 否 | 周期预设:`week` 本周 / `last_week` 上周 / `month` 本月 / `last_month` 上月。**缺省为 `month`** |
|
||||||
|
| `start_date` | string | 否 | 自定义开始日期,格式 `Y-m-d`(如 `2026-07-01`)。需与 `end_date` 成对出现 |
|
||||||
|
| `end_date` | string | 否 | 自定义结束日期,格式 `Y-m-d`,不能早于 `start_date` |
|
||||||
|
|
||||||
|
规则:
|
||||||
|
|
||||||
|
- 同时传 `start_date` + `end_date` 时为**自定义区间,优先于 `preset`**(响应中 `preset` 返回 `custom`);
|
||||||
|
- 只传其中一个日期、日期格式错误、结束日期早于开始日期、preset 非枚举值,均返回参数错误;
|
||||||
|
- 进行中的周期(本周/本月)结束日期自动**封顶为今天**;周以**周一**为起点。
|
||||||
|
|
||||||
|
## 响应示例
|
||||||
|
|
||||||
|
`GET /mini/report/purchase?preset=last_month`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"msg": "ok",
|
||||||
|
"data": {
|
||||||
|
"preset": "last_month",
|
||||||
|
"start_date": "2026-07-01",
|
||||||
|
"end_date": "2026-07-31",
|
||||||
|
"total_amount": "100000.00",
|
||||||
|
"total_quantity": 1700,
|
||||||
|
"order_count": 20,
|
||||||
|
"item_count": 2,
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"product_id": 11,
|
||||||
|
"product_name": "土豆",
|
||||||
|
"product_spec": "50斤/袋",
|
||||||
|
"unit": "斤",
|
||||||
|
"quantity": 500,
|
||||||
|
"weight": "0.000",
|
||||||
|
"amount": "8764.00",
|
||||||
|
"percent": 8.8
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"product_id": 12,
|
||||||
|
"product_name": "洋葱",
|
||||||
|
"product_spec": "20斤/袋",
|
||||||
|
"unit": "斤",
|
||||||
|
"quantity": 200,
|
||||||
|
"weight": "0.000",
|
||||||
|
"amount": "3664.00",
|
||||||
|
"percent": 3.7
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 响应字段
|
||||||
|
|
||||||
|
### 顶层(`data`)
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| `preset` | string | 实际生效的周期预设(`week`/`last_week`/`month`/`last_month`/`custom`) |
|
||||||
|
| `start_date` | string | 实际统计开始日期 `Y-m-d` |
|
||||||
|
| `end_date` | string | 实际统计结束日期 `Y-m-d` |
|
||||||
|
| `total_amount` | string | 周期内采购总金额(元,2 位小数;金额一律以字符串返回防精度丢失) |
|
||||||
|
| `total_quantity` | number | 周期内订货总量(各单品数量之和) |
|
||||||
|
| `order_count` | number | 周期内有效订货单数 |
|
||||||
|
| `item_count` | number | 单品个数(= `items` 长度) |
|
||||||
|
| `items` | array | 单品累计列表,**按金额降序** |
|
||||||
|
|
||||||
|
### 单品行(`data.items[]`)
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| `product_id` | number | 商品 ID |
|
||||||
|
| `product_name` | string | 品名(下单时快照,商品档案改名/删除不影响历史统计) |
|
||||||
|
| `product_spec` | string | 规格/包规(快照) |
|
||||||
|
| `unit` | string | 计价单位(快照) |
|
||||||
|
| `quantity` | number | 周期内累计订货量 |
|
||||||
|
| `weight` | string | 周期内累计重量(3 位小数,未称重为 `0.000`) |
|
||||||
|
| `amount` | string | 周期内累计采购金额(元,2 位小数) |
|
||||||
|
| `percent` | number | 金额占比(%,1 位小数,四舍五入;如 `8.8` 表示 8.8%。周期内无数据时为 `0`) |
|
||||||
|
|
||||||
|
## 统计口径
|
||||||
|
|
||||||
|
- 数据源为**门店订货单明细**(下单时快照的等级单价 × 数量),即门店视角的应付采购金额;
|
||||||
|
- **已取消**订单与后台**已删除**订单不计入统计,其余状态(待接单/已接单/采购中/配送中/已完成)全部计入;
|
||||||
|
- 单品按 `product_id` 聚合,同一商品在周期内多次下单会累计为一行;
|
||||||
|
- 占比 = 单品金额 ÷ 总金额 × 100,保留 1 位小数(四舍五入),各行占比之和可能因舍入存在 ±0.1 的误差,属正常现象。
|
||||||
|
|
||||||
|
## 错误响应
|
||||||
|
|
||||||
|
与小程序其他接口一致:`success: false` + `msg` 描述原因。
|
||||||
|
|
||||||
|
| 场景 | msg 示例 |
|
||||||
|
|---|---|
|
||||||
|
| 未登录 / token 失效 | `Unauthenticated`(按全局鉴权约定) |
|
||||||
|
| 账号被停用 | `账号不存在或已被停用` |
|
||||||
|
| preset 非法 | `preset 参数只能是 week/last_week/month/last_month` |
|
||||||
|
| 日期格式错误 | `开始日期格式为 Y-m-d` |
|
||||||
|
| 结束早于开始 | `结束日期不能早于开始日期` |
|
||||||
|
| 自定义区间只传一侧 | `自定义区间需同时提供开始与结束日期` |
|
||||||
|
|
||||||
|
## 前端对接建议
|
||||||
|
|
||||||
|
- 顶部放周期切换 Tab(本周 / 上周 / 本月 / 上月 / 自定义),自定义时弹出日期选择器,选中后传 `start_date` + `end_date`;
|
||||||
|
- 头部卡片展示 `total_amount`(总金额)与 `order_count`(单数);
|
||||||
|
- 列表直接渲染 `items`(已按金额降序),每行展示品名、规格、累计金额与 `percent`%;如需饼图可直接用 `items[].percent`;
|
||||||
|
- 区间无数据时 `items` 为空数组、`total_amount` 为 `"0.00"`,前端展示空状态即可。
|
||||||
@@ -0,0 +1,257 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature;
|
||||||
|
|
||||||
|
use App\Models\StoreModel;
|
||||||
|
use App\Models\StoreOrderItemModel;
|
||||||
|
use App\Models\StoreOrderModel;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 小程序采购报表:周期区间推导(本周/上周/本月/上月/自定义)、采购总金额与单品累计金额/占比、
|
||||||
|
* 已取消与已删除订单剔除、门店数据隔离、参数校验
|
||||||
|
*/
|
||||||
|
class MiniReportTest extends ProcurementTestCase
|
||||||
|
{
|
||||||
|
/** 造一张指定日期/状态的订货单 */
|
||||||
|
private function makeOrder(StoreModel $store, string $date, int $status = StoreOrderModel::STATUS_COMPLETED): StoreOrderModel
|
||||||
|
{
|
||||||
|
return StoreOrderModel::factory()->create([
|
||||||
|
'store_id' => $store->id,
|
||||||
|
'order_date' => $date,
|
||||||
|
'status' => $status,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 造一条明细(金额默认按 price × quantity 计算) */
|
||||||
|
private function makeItem(StoreOrderModel $order, array $attributes = []): StoreOrderItemModel
|
||||||
|
{
|
||||||
|
return StoreOrderItemModel::factory()->create(array_merge([
|
||||||
|
'order_id' => $order->id,
|
||||||
|
'store_id' => $order->store_id,
|
||||||
|
], $attributes));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 汇总与占比:总金额=全部明细金额之和,单品按金额降序,占比 1 位小数四舍五入 */
|
||||||
|
public function test_report_aggregates_total_and_item_percent(): void
|
||||||
|
{
|
||||||
|
$store = StoreModel::factory()->create();
|
||||||
|
$this->actingAsMiniStore($store);
|
||||||
|
|
||||||
|
// 总额恰好 100000.00:土豆 8764.00 → 8.8%,洋葱 3664.00 → 3.7%,白菜 87572.00 → 87.6%
|
||||||
|
$orderA = $this->makeOrder($store, '2026-07-03');
|
||||||
|
$this->makeItem($orderA, ['product_id' => 11, 'product_name' => '土豆', 'quantity' => 500, 'price' => '17.53', 'amount' => '8764.00']);
|
||||||
|
$this->makeItem($orderA, ['product_id' => 12, 'product_name' => '洋葱', 'quantity' => 200, 'price' => '18.32', 'amount' => '3664.00']);
|
||||||
|
$orderB = $this->makeOrder($store, '2026-07-20');
|
||||||
|
$this->makeItem($orderB, ['product_id' => 13, 'product_name' => '白菜', 'quantity' => 1000, 'price' => '87.57', 'amount' => '87572.00']);
|
||||||
|
|
||||||
|
$data = $this->getJson('/mini/report/purchase?start_date=2026-07-01&end_date=2026-07-31')
|
||||||
|
->assertOk()
|
||||||
|
->json('data');
|
||||||
|
|
||||||
|
$this->assertSame('custom', $data['preset']);
|
||||||
|
$this->assertSame('2026-07-01', $data['start_date']);
|
||||||
|
$this->assertSame('2026-07-31', $data['end_date']);
|
||||||
|
$this->assertSame('100000.00', $data['total_amount']);
|
||||||
|
$this->assertSame(1700, $data['total_quantity']);
|
||||||
|
$this->assertSame(2, $data['order_count']);
|
||||||
|
$this->assertSame(3, $data['item_count']);
|
||||||
|
|
||||||
|
// 按金额降序
|
||||||
|
$this->assertSame(['白菜', '土豆', '洋葱'], array_column($data['items'], 'product_name'));
|
||||||
|
|
||||||
|
$potato = $data['items'][1];
|
||||||
|
$this->assertSame(11, $potato['product_id']);
|
||||||
|
$this->assertSame('8764.00', $potato['amount']);
|
||||||
|
$this->assertSame(500, $potato['quantity']);
|
||||||
|
$this->assertSame(8.8, $potato['percent']);
|
||||||
|
|
||||||
|
$onion = $data['items'][2];
|
||||||
|
$this->assertSame('3664.00', $onion['amount']);
|
||||||
|
$this->assertSame(3.7, $onion['percent']);
|
||||||
|
|
||||||
|
$cabbage = $data['items'][0];
|
||||||
|
$this->assertSame(87.6, $cabbage['percent']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 同一单品跨多订单累计:数量/金额求和,品名取快照 */
|
||||||
|
public function test_report_accumulates_same_product_across_orders(): void
|
||||||
|
{
|
||||||
|
$store = StoreModel::factory()->create();
|
||||||
|
$this->actingAsMiniStore($store);
|
||||||
|
|
||||||
|
$orderA = $this->makeOrder($store, '2026-08-01');
|
||||||
|
$this->makeItem($orderA, ['product_id' => 11, 'product_name' => '土豆', 'quantity' => 100, 'price' => '10.00', 'amount' => '1000.00']);
|
||||||
|
$orderB = $this->makeOrder($store, '2026-08-05');
|
||||||
|
$this->makeItem($orderB, ['product_id' => 11, 'product_name' => '土豆', 'quantity' => 300, 'price' => '10.00', 'amount' => '3000.00']);
|
||||||
|
|
||||||
|
$data = $this->getJson('/mini/report/purchase?start_date=2026-08-01&end_date=2026-08-31')
|
||||||
|
->assertOk()
|
||||||
|
->json('data');
|
||||||
|
|
||||||
|
$this->assertSame('4000.00', $data['total_amount']);
|
||||||
|
$this->assertSame(1, $data['item_count']);
|
||||||
|
$this->assertSame(400, $data['items'][0]['quantity']);
|
||||||
|
$this->assertSame('4000.00', $data['items'][0]['amount']);
|
||||||
|
$this->assertEquals(100.0, $data['items'][0]['percent'], '单一单品占比 100%(整数百分比 JSON 编码为 100)');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 自定义区间:区间外订单不计入 */
|
||||||
|
public function test_report_filters_by_custom_date_range(): void
|
||||||
|
{
|
||||||
|
$store = StoreModel::factory()->create();
|
||||||
|
$this->actingAsMiniStore($store);
|
||||||
|
|
||||||
|
$in = $this->makeOrder($store, '2026-08-10');
|
||||||
|
$this->makeItem($in, ['product_id' => 11, 'amount' => '100.00']);
|
||||||
|
$out = $this->makeOrder($store, '2026-08-20');
|
||||||
|
$this->makeItem($out, ['product_id' => 11, 'amount' => '900.00']);
|
||||||
|
|
||||||
|
$data = $this->getJson('/mini/report/purchase?start_date=2026-08-01&end_date=2026-08-15')
|
||||||
|
->assertOk()
|
||||||
|
->json('data');
|
||||||
|
|
||||||
|
$this->assertSame('100.00', $data['total_amount']);
|
||||||
|
$this->assertSame(1, $data['order_count']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 已取消与后台已删除订单不计入 */
|
||||||
|
public function test_report_excludes_cancelled_and_deleted_orders(): void
|
||||||
|
{
|
||||||
|
$store = StoreModel::factory()->create();
|
||||||
|
$this->actingAsMiniStore($store);
|
||||||
|
|
||||||
|
$normal = $this->makeOrder($store, '2026-08-10');
|
||||||
|
$this->makeItem($normal, ['product_id' => 11, 'amount' => '100.00']);
|
||||||
|
|
||||||
|
$cancelled = $this->makeOrder($store, '2026-08-10', StoreOrderModel::STATUS_CANCELLED);
|
||||||
|
$this->makeItem($cancelled, ['product_id' => 12, 'amount' => '500.00']);
|
||||||
|
|
||||||
|
$deleted = $this->makeOrder($store, '2026-08-10', StoreOrderModel::STATUS_CANCELLED);
|
||||||
|
$this->makeItem($deleted, ['product_id' => 13, 'amount' => '700.00']);
|
||||||
|
$deleted->delete();
|
||||||
|
|
||||||
|
$data = $this->getJson('/mini/report/purchase?start_date=2026-08-01&end_date=2026-08-31')
|
||||||
|
->assertOk()
|
||||||
|
->json('data');
|
||||||
|
|
||||||
|
$this->assertSame('100.00', $data['total_amount']);
|
||||||
|
$this->assertSame(1, $data['order_count']);
|
||||||
|
$this->assertSame(1, $data['item_count']);
|
||||||
|
$this->assertSame(11, $data['items'][0]['product_id']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** preset 推导:本月(结束日期封顶到今天)与上月 */
|
||||||
|
public function test_report_preset_month_and_last_month(): void
|
||||||
|
{
|
||||||
|
Carbon::setTestNow('2026-08-21 12:00:00');
|
||||||
|
|
||||||
|
$store = StoreModel::factory()->create();
|
||||||
|
$this->actingAsMiniStore($store);
|
||||||
|
|
||||||
|
$july = $this->makeOrder($store, '2026-07-15');
|
||||||
|
$this->makeItem($july, ['product_id' => 11, 'amount' => '200.00']);
|
||||||
|
$august = $this->makeOrder($store, '2026-08-15');
|
||||||
|
$this->makeItem($august, ['product_id' => 11, 'amount' => '300.00']);
|
||||||
|
|
||||||
|
$month = $this->getJson('/mini/report/purchase?preset=month')->assertOk()->json('data');
|
||||||
|
$this->assertSame('2026-08-01', $month['start_date']);
|
||||||
|
$this->assertSame('2026-08-21', $month['end_date'], '进行中的本月封顶到今天');
|
||||||
|
$this->assertSame('300.00', $month['total_amount']);
|
||||||
|
|
||||||
|
$lastMonth = $this->getJson('/mini/report/purchase?preset=last_month')->assertOk()->json('data');
|
||||||
|
$this->assertSame('2026-07-01', $lastMonth['start_date']);
|
||||||
|
$this->assertSame('2026-07-31', $lastMonth['end_date']);
|
||||||
|
$this->assertSame('200.00', $lastMonth['total_amount']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** preset 推导:本周(周一起算)与上周 */
|
||||||
|
public function test_report_preset_week_and_last_week(): void
|
||||||
|
{
|
||||||
|
Carbon::setTestNow('2026-08-21 12:00:00'); // 周五,本周一 = 2026-08-17
|
||||||
|
|
||||||
|
$store = StoreModel::factory()->create();
|
||||||
|
$this->actingAsMiniStore($store);
|
||||||
|
|
||||||
|
$thisWeek = $this->makeOrder($store, '2026-08-18');
|
||||||
|
$this->makeItem($thisWeek, ['product_id' => 11, 'amount' => '100.00']);
|
||||||
|
$lastWeek = $this->makeOrder($store, '2026-08-12');
|
||||||
|
$this->makeItem($lastWeek, ['product_id' => 11, 'amount' => '400.00']);
|
||||||
|
|
||||||
|
$week = $this->getJson('/mini/report/purchase?preset=week')->assertOk()->json('data');
|
||||||
|
$this->assertSame('2026-08-17', $week['start_date']);
|
||||||
|
$this->assertSame('2026-08-21', $week['end_date']);
|
||||||
|
$this->assertSame('100.00', $week['total_amount']);
|
||||||
|
|
||||||
|
$last = $this->getJson('/mini/report/purchase?preset=last_week')->assertOk()->json('data');
|
||||||
|
$this->assertSame('2026-08-10', $last['start_date']);
|
||||||
|
$this->assertSame('2026-08-16', $last['end_date']);
|
||||||
|
$this->assertSame('400.00', $last['total_amount']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 不传参数默认本月 */
|
||||||
|
public function test_report_defaults_to_current_month(): void
|
||||||
|
{
|
||||||
|
Carbon::setTestNow('2026-08-21 12:00:00');
|
||||||
|
|
||||||
|
$store = StoreModel::factory()->create();
|
||||||
|
$this->actingAsMiniStore($store);
|
||||||
|
|
||||||
|
$order = $this->makeOrder($store, '2026-08-02');
|
||||||
|
$this->makeItem($order, ['product_id' => 11, 'amount' => '66.00']);
|
||||||
|
|
||||||
|
$data = $this->getJson('/mini/report/purchase')->assertOk()->json('data');
|
||||||
|
$this->assertSame('month', $data['preset']);
|
||||||
|
$this->assertSame('2026-08-01', $data['start_date']);
|
||||||
|
$this->assertSame('66.00', $data['total_amount']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 门店数据隔离:仅统计本店订单 */
|
||||||
|
public function test_report_data_isolated_between_stores(): void
|
||||||
|
{
|
||||||
|
$storeA = StoreModel::factory()->create();
|
||||||
|
$orderA = $this->makeOrder($storeA, '2026-08-10');
|
||||||
|
$this->makeItem($orderA, ['product_id' => 11, 'amount' => '999.00']);
|
||||||
|
|
||||||
|
$storeB = StoreModel::factory()->create();
|
||||||
|
$this->actingAsMiniStore($storeB);
|
||||||
|
|
||||||
|
$data = $this->getJson('/mini/report/purchase?start_date=2026-08-01&end_date=2026-08-31')
|
||||||
|
->assertOk()
|
||||||
|
->json('data');
|
||||||
|
|
||||||
|
$this->assertSame('0.00', $data['total_amount']);
|
||||||
|
$this->assertSame(0, $data['order_count']);
|
||||||
|
$this->assertSame([], $data['items']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 空周期:总额为 0、明细为空,不出现除零 */
|
||||||
|
public function test_report_empty_period_returns_zero(): void
|
||||||
|
{
|
||||||
|
$store = StoreModel::factory()->create();
|
||||||
|
$this->actingAsMiniStore($store);
|
||||||
|
|
||||||
|
$data = $this->getJson('/mini/report/purchase?start_date=2026-01-01&end_date=2026-01-31')
|
||||||
|
->assertOk()
|
||||||
|
->json('data');
|
||||||
|
|
||||||
|
$this->assertSame('0.00', $data['total_amount']);
|
||||||
|
$this->assertSame(0, $data['total_quantity']);
|
||||||
|
$this->assertSame(0, $data['order_count']);
|
||||||
|
$this->assertSame(0, $data['item_count']);
|
||||||
|
$this->assertSame([], $data['items']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 参数校验:非法 preset / 日期格式 / 结束早于开始 / 区间缺一侧 */
|
||||||
|
public function test_report_validates_params(): void
|
||||||
|
{
|
||||||
|
$store = StoreModel::factory()->create();
|
||||||
|
$this->actingAsMiniStore($store);
|
||||||
|
|
||||||
|
$this->getJson('/mini/report/purchase?preset=year')->assertJsonPath('success', false);
|
||||||
|
$this->getJson('/mini/report/purchase?start_date=2026/08/01&end_date=2026-08-31')->assertJsonPath('success', false);
|
||||||
|
$this->getJson('/mini/report/purchase?start_date=2026-08-31&end_date=2026-08-01')->assertJsonPath('success', false);
|
||||||
|
$this->getJson('/mini/report/purchase?start_date=2026-08-01')->assertJsonPath('success', false);
|
||||||
|
$this->getJson('/mini/report/purchase?end_date=2026-08-31')->assertJsonPath('success', false);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user