截单时间
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -34,6 +34,10 @@ class OrderController extends BaseMiniController
|
||||
public function store(MiniOrderRequest $request): JsonResponse
|
||||
{
|
||||
$store = $this->currentStore($request);
|
||||
|
||||
// 截单时间校验:业务配置 services.order_time_start / order_time_end,均未配置时不限制
|
||||
$this->assertWithinOrderTimeWindow();
|
||||
|
||||
$level = $store->level_id > 0 ? $store->level : null;
|
||||
if ($level === null) {
|
||||
throw new RepositoryException('门店未设置客户等级,无法下单,请联系客服');
|
||||
@@ -321,4 +325,44 @@ class OrderController extends BaseMiniController
|
||||
|
||||
return $this->success([], '订单已取消');
|
||||
}
|
||||
|
||||
/**
|
||||
* 截单时间校验:业务配置 services.order_time_start / order_time_end(HH:mm)
|
||||
* 均留空不限制;只配一端按单边限制;开始时间晚于截单时间表示跨天时段(如 20:00-次日06:00);
|
||||
* 格式非法的配置按未配置处理,避免误配置导致全天无法下单
|
||||
*/
|
||||
private function assertWithinOrderTimeWindow(): void
|
||||
{
|
||||
$parse = static function (mixed $value): ?int {
|
||||
$value = trim((string) $value);
|
||||
if (! preg_match('/^([01]?\d|2[0-3]):([0-5]\d)$/', $value, $matches)) {
|
||||
return null;
|
||||
}
|
||||
return (int) $matches[1] * 60 + (int) $matches[2];
|
||||
};
|
||||
|
||||
$startText = trim((string) site_config('services.order_time_start', ''));
|
||||
$endText = trim((string) site_config('services.order_time_end', ''));
|
||||
$start = $parse($startText);
|
||||
$end = $parse($endText);
|
||||
if ($start === null && $end === null) {
|
||||
return;
|
||||
}
|
||||
if ($start !== null && $start === $end) {
|
||||
return;
|
||||
}
|
||||
|
||||
$now = (int) now()->format('H') * 60 + (int) now()->format('i');
|
||||
$allowed = match (true) {
|
||||
// 跨天时段:当晚开始时间之后 或 次日截单时间之前
|
||||
$start !== null && $end !== null && $start > $end => $now >= $start || $now <= $end,
|
||||
$start !== null && $end !== null => $now >= $start && $now <= $end,
|
||||
$start !== null => $now >= $start,
|
||||
default => $now <= $end,
|
||||
};
|
||||
if (! $allowed) {
|
||||
$window = ($startText !== '' ? $startText : '00:00') . ' - ' . ($endText !== '' ? $endText : '24:00');
|
||||
throw new RepositoryException('当前不在下单时段内(下单时间 ' . $window . '),请在规定时间内下单');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,8 @@ class SysDataSeeder extends Seeder
|
||||
['id' => 4, 'group_id' => 1, 'key' => 'describe', 'title' => '网站描述', 'describe' => '网站的基本描述', 'values' => '没有描述', 'type' => 'TextArea','options' => "", 'sort' => 2, 'created_at' => $date, 'updated_at' => $date],
|
||||
['id' => 7, 'group_id' => 3, 'key' => 'box_amount', 'title' => '周转筐金额', 'describe' => '周转筐的金额,用于附加业务金额的计算', 'values' => '', 'type' => 'InputNumber','props' => "min=0\nmax=10000\nstep=0.01", 'sort' => 0, 'created_at' => $date, 'updated_at' => $date],
|
||||
['id' => 8, 'group_id' => 3, 'key' => 'tray_amount', 'title' => '周转托盘金额', 'describe' => '周转托盘的金额,用于附加金额的计算', 'values' => '', 'type' => 'InputNumber','props' => "min=0\nmax=10000\nstep=0.01", 'sort' => 1, 'created_at' => $date, 'updated_at' => $date],
|
||||
['id' => 36, 'group_id' => 3, 'key' => 'order_time_start', 'title' => '下单开始时间', 'describe' => '允许下单的开始时间(24小时制 HH:mm,如 08:00),留空不限制;开始时间晚于截单时间表示跨天时段(如 20:00 至次日 06:00)', 'values' => '', 'type' => 'Input','options' => "", 'sort' => 2, 'created_at' => $date, 'updated_at' => $date],
|
||||
['id' => 37, 'group_id' => 3, 'key' => 'order_time_end', 'title' => '截单时间', 'describe' => '下单截止时间(24小时制 HH:mm,如 18:00),不在下单时段内门店不能下单,留空不限制', 'values' => '', 'type' => 'Input','options' => "", 'sort' => 3, 'created_at' => $date, 'updated_at' => $date],
|
||||
// 支付配置(pay):收款方式与在线支付渠道
|
||||
['id' => 9, 'group_id' => 4, 'key' => 'wechat_qrcode', 'title' => '微信收款码', 'describe' => '微信收款码图片地址(在文件管理中上传收款码后复制图片链接,或直接填写文件ID),小程序付款页展示', 'values' => '', 'type' => 'Input','options' => "", 'sort' => 0, 'created_at' => $date, 'updated_at' => $date],
|
||||
['id' => 10, 'group_id' => 4, 'key' => 'alipay_qrcode', 'title' => '支付宝收款码', 'describe' => '支付宝收款码图片地址(在文件管理中上传收款码后复制图片链接,或直接填写文件ID),小程序付款页展示', 'values' => '', 'type' => 'Input','options' => "", 'sort' => 1, 'created_at' => $date, 'updated_at' => $date],
|
||||
|
||||
@@ -106,7 +106,7 @@ class MiniProductTest extends ProcurementTestCase
|
||||
$this->assertNull($row['price']);
|
||||
}
|
||||
|
||||
/** 商品列表:先按分类树展示顺序(深度优先),同分类内按商品 sort 降序、id 升序,未入树分类排最后 */
|
||||
/** 商品列表:先按分类树展示顺序(深度优先),同分类内按商品 sort 升序、id 升序,未入树分类排最后 */
|
||||
public function test_product_list_ordered_by_category_tree(): void
|
||||
{
|
||||
$rootA = ProductCategoryModel::create(['name' => '蔬菜', 'parent_id' => 0, 'sort' => 0, 'status' => 1]);
|
||||
@@ -123,8 +123,8 @@ class MiniProductTest extends ProcurementTestCase
|
||||
|
||||
$ids = array_column($this->getJson('/mini/product/list')->assertOk()->json('data.data'), 'id');
|
||||
|
||||
// 树序:rootA → leafA2 → leafA1 → rootB;leafA2 内 sort 降序;无分类排最后
|
||||
$this->assertSame([$pA2High->id, $pA2Low->id, $pA1->id, $pB->id, $pNone->id], $ids);
|
||||
// 树序:rootA → leafA2 → leafA1 → rootB;leafA2 内 sort 升序;无分类排最后
|
||||
$this->assertSame([$pA2Low->id, $pA2High->id, $pA1->id, $pB->id, $pNone->id], $ids);
|
||||
}
|
||||
|
||||
/** 商品列表:登录门店附加购物车行ID与数量,响应附悬浮球汇总 */
|
||||
|
||||
@@ -9,6 +9,9 @@ use App\Models\PurchaseOrderModel;
|
||||
use App\Models\StoreModel;
|
||||
use App\Models\StoreOrderModel;
|
||||
use Modules\SystemTool\Models\SysFileModel;
|
||||
use Modules\SystemTool\Models\SysSiteConfigGroupModel;
|
||||
use Modules\SystemTool\Models\SysSiteConfigItemsModel;
|
||||
use Modules\SystemTool\Services\SysSiteConfigService;
|
||||
|
||||
/**
|
||||
* 小程序下单:等级上浮价快照、服务端重算总价、取消限制、门店数据隔离
|
||||
@@ -426,6 +429,96 @@ class StoreOrderTest extends ProcurementTestCase
|
||||
$this->assertSame(StoreOrderModel::STATUS_SUMMARIZED, $pending1->refresh()->status);
|
||||
}
|
||||
|
||||
/** 写入下单时段业务配置并刷新缓存(value 传 null 表示该项不配置) */
|
||||
private function setOrderTimeWindow(?string $start, ?string $end): void
|
||||
{
|
||||
$group = SysSiteConfigGroupModel::create(['title' => '业务配置', 'key' => 'services', 'remark' => '']);
|
||||
foreach (['order_time_start' => $start, 'order_time_end' => $end] as $key => $value) {
|
||||
if ($value === null) {
|
||||
continue;
|
||||
}
|
||||
SysSiteConfigItemsModel::create([
|
||||
'group_id' => $group->id,
|
||||
'key' => $key,
|
||||
'title' => $key,
|
||||
'describe' => '',
|
||||
'values' => $value,
|
||||
'type' => 'Input',
|
||||
'options' => '',
|
||||
'sort' => 0,
|
||||
]);
|
||||
}
|
||||
SysSiteConfigService::refreshSiteConfig();
|
||||
}
|
||||
|
||||
/** 截单时间:下单时段外禁止下单,时段内正常下单 */
|
||||
public function test_place_order_blocked_outside_order_time_window(): void
|
||||
{
|
||||
[$store, $product] = $this->makeStoreWithProduct('5.00');
|
||||
$this->setOrderTimeWindow('08:00', '18:00');
|
||||
$this->actingAsMiniStore($store);
|
||||
|
||||
// 20:00 已截单 → 拒绝且不生成订单
|
||||
$this->travelTo('2026-09-05 20:00:00');
|
||||
$response = $this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 1]]]);
|
||||
$response->assertOk()->assertJsonPath('success', false);
|
||||
$this->assertStringContainsString('不在下单时段内', (string) $response->json('msg'));
|
||||
$this->assertStringContainsString('08:00 - 18:00', (string) $response->json('msg'));
|
||||
$this->assertSame(0, StoreOrderModel::count());
|
||||
|
||||
// 边界:08:00 开始时间与 18:00 截单时间均可下单
|
||||
$this->travelTo('2026-09-05 08:00:00');
|
||||
$this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 1]]])
|
||||
->assertJsonPath('success', true);
|
||||
$this->travelTo('2026-09-05 18:00:00');
|
||||
$this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 1]]])
|
||||
->assertJsonPath('success', true);
|
||||
$this->assertSame(2, StoreOrderModel::count());
|
||||
}
|
||||
|
||||
/** 截单时间:跨天时段(开始时间晚于截单时间,如 20:00-次日06:00) */
|
||||
public function test_place_order_cross_midnight_time_window(): void
|
||||
{
|
||||
[$store, $product] = $this->makeStoreWithProduct('5.00');
|
||||
$this->setOrderTimeWindow('20:00', '06:00');
|
||||
$this->actingAsMiniStore($store);
|
||||
|
||||
// 当晚 22:00 与 次日 05:00 均在时段内
|
||||
$this->travelTo('2026-09-05 22:00:00');
|
||||
$this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 1]]])
|
||||
->assertJsonPath('success', true);
|
||||
$this->travelTo('2026-09-06 05:00:00');
|
||||
$this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 1]]])
|
||||
->assertJsonPath('success', true);
|
||||
|
||||
// 中午 12:00 不在时段内 → 拒绝
|
||||
$this->travelTo('2026-09-06 12:00:00');
|
||||
$this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 1]]])
|
||||
->assertJsonPath('success', false);
|
||||
$this->assertSame(2, StoreOrderModel::count());
|
||||
}
|
||||
|
||||
/** 截单时间:只配置截单时间时按单边限制(截止后不能下单);格式非法按未配置处理 */
|
||||
public function test_place_order_only_cutoff_time_and_invalid_config(): void
|
||||
{
|
||||
[$store, $product] = $this->makeStoreWithProduct('5.00');
|
||||
$this->setOrderTimeWindow(null, '18:00');
|
||||
$this->actingAsMiniStore($store);
|
||||
|
||||
$this->travelTo('2026-09-05 17:59:00');
|
||||
$this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 1]]])
|
||||
->assertJsonPath('success', true);
|
||||
$this->travelTo('2026-09-05 18:01:00');
|
||||
$this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 1]]])
|
||||
->assertJsonPath('success', false);
|
||||
|
||||
// 非法格式不拦截(避免误配置导致全天无法下单)
|
||||
SysSiteConfigItemsModel::query()->update(['values' => 'abc']);
|
||||
SysSiteConfigService::refreshSiteConfig();
|
||||
$this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 1]]])
|
||||
->assertJsonPath('success', true);
|
||||
}
|
||||
|
||||
/** 订单列表商品预览:附带首图/规格/单位供小程序端展示 */
|
||||
public function test_order_list_preview_includes_image_spec_and_unit(): void
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user