小程序接口

This commit is contained in:
liu
2026-08-14 18:02:32 +08:00
parent 969eabcd11
commit 9a0999742f
7 changed files with 434 additions and 188 deletions
File diff suppressed because one or more lines are too long
+92 -7
View File
@@ -16,24 +16,70 @@ use Modules\AnnoRoute\Attribute\RequestAttribute;
#[RequestAttribute('/mini', 'mini', authGuard: 'users')]
class BillController extends BaseMiniController
{
/** 账单列表:当前门店强制过滤,?status= 按支付状态筛选(0未支付 1已支付) */
/**
* 账单列表:当前门店强制过滤
* ?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',
]);
$user = $this->currentUser($request);
$store = $this->ensureStoreBound($user);
$query = BillModel::query()
->where('store_id', $store->id)
->with('purchase:id,purchase_no,purchase_date');
if ($request->filled('status')) {
$query->where('status', (int) $request->input('status'));
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']);
}
$data = $query->orderBy('bill_date', 'desc')
$paginator = $query->orderBy('bill_date', 'desc')
->orderBy('id', 'desc')
->paginate((int) $request->input('pageSize', 10))
->toArray();
->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);
}
@@ -58,9 +104,48 @@ class BillController extends BaseMiniController
->toArray();
return $this->success([
'bill' => $bill->toArray(),
'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,
'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(),
];
}
}
+65 -8
View File
@@ -128,24 +128,81 @@ class OrderController extends BaseMiniController
], '下单成功');
}
/** 历史订单:当前门店强制过滤,?status=&page=&pageSize= */
/**
* 历史订单:当前门店强制过滤
* ?status= 状态筛选;?start_date=&end_date= 订货日期区间(配合 /order/summary 周期下钻);
* ?page=&pageSize= 分页(pageSize 上限 50
* 行数据附带 status_name / can_cancel / 商品预览(前 3 条明细)/ item_count,完整明细走详情接口
*/
#[GetRoute('/order', authorize: true)]
public function index(Request $request): JsonResponse
{
$params = $request->validate([
'status' => 'nullable|integer|in:0,1,2,3,4,9',
'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',
]);
$user = $this->currentUser($request);
$store = $this->ensureStoreBound($user);
$query = StoreOrderModel::query()->where('store_id', $store->id);
if ($request->filled('status')) {
$query->where('status', (int) $request->input('status'));
$query = StoreOrderModel::query()
->where('store_id', $store->id)
->select([
'id', 'order_no', 'order_date', 'total_quantity', 'total_weight',
'total_amount', 'status', 'remark', 'purchase_id', 'bill_id', 'created_at',
])
->with(['items' => static fn ($itemsQuery) => $itemsQuery
->select(['id', 'order_id', 'product_name', 'quantity', 'unit'])
->orderBy('id')]);
if (isset($params['status'])) {
$query->where('status', (int) $params['status']);
}
if (! empty($params['start_date'])) {
$query->whereDate('order_date', '>=', $params['start_date']);
}
if (! empty($params['end_date'])) {
$query->whereDate('order_date', '<=', $params['end_date']);
}
$data = $query->orderBy('order_date', 'desc')
$paginator = $query->orderBy('order_date', 'desc')
->orderBy('id', 'desc')
->paginate((int) $request->input('pageSize', 10))
->toArray();
->paginate((int) ($params['pageSize'] ?? 10));
return $this->success($data);
$paginator->getCollection()->transform(
static fn (StoreOrderModel $order): array => [
'id' => $order->id,
'order_no' => $order->order_no,
'order_date' => $order->order_date->toDateString(),
'status' => $order->status,
'status_name' => StoreOrderModel::STATUS_NAMES[$order->status] ?? '',
'can_cancel' => $order->status === StoreOrderModel::STATUS_PENDING,
'total_quantity' => $order->total_quantity,
'total_weight' => $order->total_weight,
'total_amount' => $order->total_amount,
'remark' => $order->remark,
'purchase_id' => $order->purchase_id,
'bill_id' => $order->bill_id,
'created_at' => $order->created_at?->toDateTimeString(),
'item_count' => $order->items->count(),
'items' => $order->items->take(3)
->map(static fn (StoreOrderItemModel $item): array => [
'product_name' => $item->product_name,
'quantity' => $item->quantity,
'unit' => $item->unit,
])->values()->all(),
]
);
return $this->success($paginator->toArray());
}
/**
+26
View File
@@ -26,6 +26,32 @@ class BillModel extends Model
self::STATUS_PAID => '已支付',
];
/** 支付进度(小程序端):待支付(可发起合并付款) */
public const int PAY_STATE_UNPAID = 0;
/** 支付进度:审核中(已提交合并付款凭证,待后台审核;审核拒绝后释放回待支付) */
public const int PAY_STATE_REVIEWING = 1;
/** 支付进度:已支付 */
public const int PAY_STATE_PAID = 2;
/** 支付进度中文名 */
public const array PAY_STATE_NAMES = [
self::PAY_STATE_UNPAID => '待支付',
self::PAY_STATE_REVIEWING => '审核中',
self::PAY_STATE_PAID => '已支付',
];
/**
* 支付进度推导:已支付 > 审核中(payment_id 锁定中)> 待支付
* (支付审核拒绝后 payment_id 释放为 0,回到待支付;线下收款直接置已支付)
*/
public function payState(): int
{
if ($this->status === self::STATUS_PAID) {
return self::PAY_STATE_PAID;
}
return $this->payment_id > 0 ? self::PAY_STATE_REVIEWING : self::PAY_STATE_UNPAID;
}
protected $table = 'bill';
protected $primaryKey = 'id';
-172
View File
@@ -1,172 +0,0 @@
# 小程序首页接口文档
> 适用端:微信小程序(h5 仓库,Taro)
> 后台配置入口:PC 后台「客户端配置」菜单(首页轮播图 / 宫格导航 / 促销推荐卡片)
> 更新日期:2026-08-14
## 通用约定
| 项 | 说明 |
|---|---|
| 根地址 | `BASE_URL`(见 `src/utils/request.ts`,如 `http://localhost:8000/index.php` |
| 认证 | 请求头 `Authorization: Bearer {token}`,token 来自登录接口,本地存储 key `auth_token` |
| 响应包络 | `{ success: boolean, data: T, msg?: string, showType?: number }` |
| 失败处理 | `success=false``msg` 为中文错误信息;HTTP 401 表示登录过期,需重新登录 |
## GET /mini/home
首页配置聚合接口:一次返回轮播图、宫格导航、促销推荐卡片三组数据,均为**启用状态(status=1)**且按 `sort` 升序(越小越靠前)。
- **权限**:需登录(Bearer token
- **请求参数**:无
### 响应示例
```json
{
"success": true,
"msg": "ok",
"data": {
"banners": [
{
"id": 1,
"title": "新鲜直采",
"image_id": 123,
"link": "/pages/goods/detail?id=1",
"sort": 0,
"image_url": "http://localhost:8000/storage/uploads/2026/08/14/xxx.jpg"
}
],
"navs": [
{
"id": 1,
"name": "蔬菜专区",
"image_id": 124,
"link": "/pages/category/index?id=1",
"sort": 0,
"image_url": "http://localhost:8000/storage/uploads/2026/08/14/yyy.png"
}
],
"promos": [
{
"id": 1,
"title": "限时特惠",
"sub_title": "全场 8 折起",
"image_id": 125,
"link": "/pages/promo/detail?id=1",
"sort": 0,
"image_url": "http://localhost:8000/storage/uploads/2026/08/14/zzz.jpg"
}
]
}
}
```
### 字段说明
**banners(轮播图)**
| 字段 | 类型 | 说明 |
|---|---|---|
| id | number | 轮播图 ID |
| title | string | 标题(后台维护,可用于无障碍/占位) |
| image_id | number | 图片文件 IDsys_file),一般无需使用 |
| **image_url** | string \| null | 轮播图片完整 URL,直接用于 `<Image src>`;未传图时为 null |
| link | string | 小程序页面跳转路径,**空字符串表示点击不跳转** |
| sort | number | 排序值(已按此升序返回,前端无需再排) |
**navs(宫格导航)**
| 字段 | 类型 | 说明 |
|---|---|---|
| id | number | 导航 ID |
| name | string | 导航名称(宫格文字) |
| **image_url** | string \| null | 导航图标完整 URL |
| link | string | 小程序页面跳转路径,空字符串不跳转 |
| sort | number | 排序值 |
**promos(促销推荐卡片)**
| 字段 | 类型 | 说明 |
|---|---|---|
| id | number | 卡片 ID |
| title | string | 卡片标题 |
| sub_title | string | 副标题/促销文案,可能为空字符串 |
| **image_url** | string \| null | 卡片图片完整 URL |
| link | string | 小程序页面跳转路径,空字符串不跳转 |
| sort | number | 排序值 |
### 前端接入示例
`src/services/home.ts`(新增):
```ts
import { get } from '@/utils/request'
/** 首页轮播图项 */
export interface HomeBanner {
id: number
title: string
image_id: number
image_url: string | null
link: string
sort: number
}
/** 首页宫格导航项 */
export interface HomeNav {
id: number
name: string
image_id: number
image_url: string | null
link: string
sort: number
}
/** 首页促销推荐卡片 */
export interface HomePromo {
id: number
title: string
sub_title: string
image_id: number
image_url: string | null
link: string
sort: number
}
/** 首页配置聚合数据 */
export interface HomeConfig {
banners: HomeBanner[]
navs: HomeNav[]
promos: HomePromo[]
}
/** 首页配置(轮播图 + 宫格导航 + 促销卡片):GET /mini/home */
export function getHomeConfigApi() {
return get<HomeConfig>('/mini/home')
}
```
页面中使用(跳转需兼容空链接):
```tsx
const [config, setConfig] = useState<HomeConfig>({ banners: [], navs: [], promos: [] })
useEffect(() => {
getHomeConfigApi().then(res => setConfig(res.data))
}, [])
/** 统一跳转:link 为空不跳转 */
function handleLink(link: string) {
if (!link) return
Taro.navigateTo({ url: link })
}
```
### 注意事项
1. **三组数据均可能为空数组**(后台未配置或全部停用),页面需做空态处理。
2. `image_url` 可能为 `null`(后台未上传图片),渲染前判空。
3. `link` 为小程序内部页面路径(以 `/` 开头),用 `Taro.navigateTo` 跳转;若目标为 tabBar 页面需改用 `Taro.switchTab`(建议后台配置时避免填 tabBar 路径)。
4. 数据实时生效:后台修改后,小程序下次进入首页请求即为最新内容,无缓存。
5. 接口需登录后调用;未登录(401)会由 request 封装自动跳登录页。
+188
View File
@@ -0,0 +1,188 @@
<?php
namespace Tests\Feature;
use App\Models\BillModel;
use App\Models\ProductModel;
use App\Models\PurchaseOrderModel;
use App\Models\StoreModel;
use App\Models\StoreOrderItemModel;
use App\Models\StoreOrderModel;
use App\Models\UserModel;
/**
* 小程序账单:支付进度推导(待支付/审核中/已支付)、应结算日期(回款周期)、
* 待支付汇总、可付款筛选、门店数据隔离、详情归属校验
*/
class MiniBillTest extends ProcurementTestCase
{
private static int $billSeq = 0;
/**
* 造一个绑定正常门店的小程序用户
*
* @return array{0: StoreModel, 1: UserModel}
*/
private function makeStoreWithUser(int $cycleDays = 3): array
{
$store = StoreModel::factory()->paymentCycle($cycleDays)->create();
return [$store, UserModel::factory()->forStore($store->id)->create()];
}
/** 造一张账单(默认 2026-08-10 出账,总额 140.00,未支付) */
private function makeBill(StoreModel $store, array $attributes = []): BillModel
{
$seq = ++self::$billSeq;
return BillModel::create(array_merge([
'bill_no' => 'ZD' . str_pad((string) $seq, 12, '0', STR_PAD_LEFT),
'purchase_id' => PurchaseOrderModel::factory()->create()->id,
'store_id' => $store->id,
'bill_date' => '2026-08-10',
'product_amount' => '100.00',
'delivery_fee' => '10.00',
'box_num' => 2,
'tray_num' => 1,
'box_price' => '5.00',
'tray_price' => '20.00',
'added_amount' => '30.00',
'total_amount' => '140.00',
'status' => BillModel::STATUS_UNPAID,
], $attributes));
}
/** 支付进度推导与应结算日期(账单日期 + 门店回款周期) */
public function test_bill_list_derives_pay_state_and_settlement_date(): void
{
[$store, $user] = $this->makeStoreWithUser(3);
$this->actingAsMiniUser($user);
$unpaid = $this->makeBill($store);
$reviewing = $this->makeBill($store, ['payment_id' => 99]);
$paid = $this->makeBill($store, ['status' => BillModel::STATUS_PAID, 'paid_at' => '2026-08-12 10:00:00']);
$rows = collect($this->getJson('/mini/bill')->assertOk()->json('data.data'))->keyBy('id');
$unpaidRow = $rows[$unpaid->id];
$this->assertSame(BillModel::PAY_STATE_UNPAID, $unpaidRow['pay_state']);
$this->assertSame('待支付', $unpaidRow['pay_state_name']);
$this->assertTrue($unpaidRow['can_pay']);
$this->assertSame('2026-08-13', $unpaidRow['settlement_date'], '账单日期 2026-08-10 + 回款周期 3 天');
$this->assertArrayNotHasKey('operator_id', $unpaidRow, '小程序端不输出后台操作人字段');
$this->assertArrayNotHasKey('paid_operator_id', $unpaidRow);
$reviewingRow = $rows[$reviewing->id];
$this->assertSame(BillModel::PAY_STATE_REVIEWING, $reviewingRow['pay_state']);
$this->assertSame('审核中', $reviewingRow['pay_state_name']);
$this->assertFalse($reviewingRow['can_pay']);
$paidRow = $rows[$paid->id];
$this->assertSame(BillModel::PAY_STATE_PAID, $paidRow['pay_state']);
$this->assertSame('已支付', $paidRow['pay_state_name']);
$this->assertFalse($paidRow['can_pay']);
$this->assertSame('2026-08-12 10:00:00', $paidRow['paid_at']);
}
/** 待支付汇总:未支付笔数/金额(含审核中),不受列表筛选影响 */
public function test_bill_list_summary_aggregates_unpaid(): void
{
[$store, $user] = $this->makeStoreWithUser();
$this->actingAsMiniUser($user);
$this->makeBill($store, ['total_amount' => '100.00']);
$this->makeBill($store, ['total_amount' => '50.50', 'payment_id' => 99]);
$this->makeBill($store, ['total_amount' => '200.00', 'status' => BillModel::STATUS_PAID]);
$response = $this->getJson('/mini/bill?status=1')->assertOk();
$this->assertSame(1, $response->json('data.total'), 'status 筛选只影响列表');
$this->assertSame(2, $response->json('data.summary.unpaid_count'));
$this->assertSame('150.50', $response->json('data.summary.unpaid_amount'));
}
/** 可付款筛选:仅未支付且未锁定到支付记录的账单(合并付款选择页) */
public function test_bill_list_payable_filter(): void
{
[$store, $user] = $this->makeStoreWithUser();
$this->actingAsMiniUser($user);
$payable = $this->makeBill($store);
$this->makeBill($store, ['payment_id' => 99]);
$this->makeBill($store, ['status' => BillModel::STATUS_PAID]);
$rows = $this->getJson('/mini/bill?payable=1')->assertOk()->json('data.data');
$this->assertCount(1, $rows);
$this->assertSame($payable->id, $rows[0]['id']);
}
/** 账单日期区间筛选 */
public function test_bill_list_filters_by_bill_date_range(): void
{
[$store, $user] = $this->makeStoreWithUser();
$this->actingAsMiniUser($user);
$this->makeBill($store, ['bill_date' => '2026-08-01']);
$this->makeBill($store, ['bill_date' => '2026-08-05']);
$this->makeBill($store, ['bill_date' => '2026-08-10']);
$this->getJson('/mini/bill?start_date=2026-08-03')->assertJsonPath('data.total', 2);
$this->getJson('/mini/bill?start_date=2026-08-03&end_date=2026-08-06')->assertJsonPath('data.total', 1);
$this->getJson('/mini/bill?end_date=2026-08-06')->assertJsonPath('data.total', 2);
}
/** 门店数据隔离:只能查看本店账单 */
public function test_bill_data_isolated_between_stores(): void
{
[$storeA] = $this->makeStoreWithUser();
$billOfA = $this->makeBill($storeA);
[, $userB] = $this->makeStoreWithUser();
$this->actingAsMiniUser($userB);
$this->getJson('/mini/bill')->assertJsonPath('data.total', 0);
$this->getJson("/mini/bill/{$billOfA->id}")->assertJsonPath('success', false);
}
/** 详情:格式化账单 + 合并商品明细 + 关联订单 */
public function test_bill_detail_outputs_bill_items_and_orders(): void
{
[$store, $user] = $this->makeStoreWithUser(0);
$this->actingAsMiniUser($user);
$bill = $this->makeBill($store);
$product = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON]);
$order = StoreOrderModel::factory()->create([
'store_id' => $store->id,
'purchase_id' => $bill->purchase_id,
'bill_id' => $bill->id,
'total_amount' => '100.00',
]);
StoreOrderItemModel::factory()->create([
'order_id' => $order->id,
'purchase_id' => $bill->purchase_id,
'bill_id' => $bill->id,
'store_id' => $store->id,
'product_id' => $product->id,
'product_name' => '白菜',
'price' => '5.00',
'quantity' => 20,
'amount' => '100.00',
]);
$data = $this->getJson("/mini/bill/{$bill->id}")->assertOk()->json('data');
$this->assertSame($bill->bill_no, $data['bill']['bill_no']);
$this->assertSame('待支付', $data['bill']['pay_state_name']);
$this->assertSame('2026-08-10', $data['bill']['settlement_date'], '回款周期 0 天,出账当天应结算');
$this->assertSame($bill->purchase->purchase_no, $data['bill']['purchase']['purchase_no']);
$this->assertCount(1, $data['items']);
$this->assertSame('白菜', $data['items'][0]['product_name']);
$this->assertSame('5.00', $data['items'][0]['price'], '合并明细单价 = Σ金额 ÷ Σ数量');
$this->assertSame(20, $data['items'][0]['quantity']);
$this->assertSame('100.00', $data['items'][0]['amount']);
$this->assertCount(1, $data['orders']);
$this->assertSame($order->order_no, $data['orders'][0]['order_no']);
}
}
+62
View File
@@ -130,6 +130,68 @@ class StoreOrderTest extends ProcurementTestCase
$this->getJson('/mini/order')->assertJsonPath('data.total', 0);
}
/** 列表输出:状态名 / 可取消标记 / 商品预览(前 3 条)与明细种数 */
public function test_order_list_outputs_status_name_and_item_preview(): void
{
$level = CustomerLevelModel::factory()->create();
$store = StoreModel::factory()->create(['level_id' => $level->id]);
$this->actingAsMiniUser(UserModel::factory()->forStore($store->id)->create());
$items = [];
foreach (['白菜', '土豆', '番茄', '黄瓜'] as $name) {
$product = ProductModel::factory()->create(['name' => $name, 'status' => ProductModel::STATUS_ON]);
ProductPriceModel::factory()->create(['product_id' => $product->id, 'level_id' => $level->id, 'price' => 5.00]);
$items[] = ['product_id' => $product->id, 'quantity' => 2];
}
$this->postJson('/mini/order', ['items' => $items])->assertJsonPath('success', true);
$order = StoreOrderModel::where('store_id', $store->id)->first();
$row = collect($this->getJson('/mini/order')->assertOk()->json('data.data'))->firstWhere('id', $order->id);
$this->assertSame('待接单', $row['status_name']);
$this->assertTrue($row['can_cancel']);
$this->assertSame(4, $row['item_count']);
$this->assertCount(3, $row['items'], '预览仅输出前 3 条明细,完整明细走详情接口');
$this->assertSame('白菜', $row['items'][0]['product_name']);
$this->assertSame(2, $row['items'][0]['quantity']);
// 已接单后状态名变化且不可取消
$order->update(['status' => StoreOrderModel::STATUS_SUMMARIZED]);
$row = collect($this->getJson('/mini/order')->json('data.data'))->firstWhere('id', $order->id);
$this->assertSame('已接单', $row['status_name']);
$this->assertFalse($row['can_cancel']);
}
/** 订货日期区间筛选(配合 /order/summary 周期下钻) */
public function test_order_list_filters_by_order_date_range(): void
{
[$store, $product, $user] = $this->makeStoreWithProduct();
$this->actingAsMiniUser($user);
foreach ([1, 1, 1] as $qty) {
$this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => $qty]]])
->assertJsonPath('success', true);
}
$orders = StoreOrderModel::where('store_id', $store->id)->orderBy('id')->get();
$orders[0]->update(['order_date' => '2026-08-01']);
$orders[1]->update(['order_date' => '2026-08-05']);
$orders[2]->update(['order_date' => '2026-08-10']);
$this->getJson('/mini/order?start_date=2026-08-03')->assertJsonPath('data.total', 2);
$this->getJson('/mini/order?start_date=2026-08-03&end_date=2026-08-06')->assertJsonPath('data.total', 1);
$this->getJson('/mini/order?end_date=2026-08-06')->assertJsonPath('data.total', 2);
}
/** 非法筛选参数被拦截(状态枚举 / 分页上限 / 日期区间倒置) */
public function test_order_list_rejects_invalid_filters(): void
{
[, , $user] = $this->makeStoreWithProduct();
$this->actingAsMiniUser($user);
$this->getJson('/mini/order?status=7')->assertJsonPath('success', false);
$this->getJson('/mini/order?pageSize=100')->assertJsonPath('success', false);
$this->getJson('/mini/order?start_date=2026-08-10&end_date=2026-08-01')->assertJsonPath('success', false);
}
/** 造一笔指定状态的订单(商品 5.00 × 2 = 10.00 */
private function makeOrderWithStatus(int $status): StoreOrderModel
{