小程序用户改用门店登录

This commit is contained in:
liu
2026-08-20 13:46:04 +08:00
parent 6d9f02d831
commit c1d490134e
251 changed files with 689 additions and 2727 deletions
+2 -14
View File
@@ -9,7 +9,6 @@ use App\Models\ProductModel;
use App\Models\PurchaseOrderModel;
use App\Models\StoreModel;
use App\Models\StoreOrderItemModel;
use App\Models\UserModel;
use Maatwebsite\Excel\Facades\Excel;
/**
@@ -219,7 +218,7 @@ class BillExportTest extends ProcurementTestCase
}
/** 小程序端导出:强制本店过滤,混入他店账单整批拒绝 */
public function test_mini_export_scoped_to_bound_store(): void
public function test_mini_export_scoped_to_own_store(): void
{
$this->freezeTime();
@@ -233,8 +232,7 @@ class BillExportTest extends ProcurementTestCase
$this->makeItem($billA, $product, 10, '2.00');
$this->makeItem($billB, $product, 5, '20.00');
$user = UserModel::factory()->forStore($storeA->id)->create();
$this->actingAsMiniUser($user);
$this->actingAsMiniStore($storeA);
// 本店账单正常导出
Excel::fake();
@@ -256,14 +254,4 @@ class BillExportTest extends ProcurementTestCase
$this->get("/mini/bill/export?ids={$billB->id}")
->assertJsonPath('success', false);
}
/** 小程序端导出:未绑定门店 → 拒绝 */
public function test_mini_export_requires_store_binding(): void
{
$user = UserModel::factory()->create();
$this->actingAsMiniUser($user);
$this->get('/mini/bill/export?ids=1')
->assertJsonPath('success', false);
}
}
+2 -11
View File
@@ -9,7 +9,6 @@ use App\Models\ProductModel;
use App\Models\PurchaseOrderModel;
use App\Models\StoreModel;
use App\Models\StoreOrderModel;
use App\Models\UserModel;
use Modules\SystemTool\Models\SysFileModel;
/**
@@ -29,9 +28,8 @@ class BillPaymentTest extends ProcurementTestCase
$level = CustomerLevelModel::factory()->create();
$store = StoreModel::factory()->create(['level_id' => $level->id]);
$product = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'cost_price' => '5.00']);
$user = UserModel::factory()->forStore($store->id)->create();
$this->actingAsMiniUser($user);
$this->actingAsMiniStore($store);
$this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 4]]])
->assertJsonPath('success', true);
$order = StoreOrderModel::where('store_id', $store->id)->first();
@@ -80,9 +78,8 @@ class BillPaymentTest extends ProcurementTestCase
$level = CustomerLevelModel::factory()->create();
$store = StoreModel::factory()->create(['level_id' => $level->id]);
$product = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'cost_price' => '5.00']);
$user = UserModel::factory()->forStore($store->id)->create();
$this->actingAsMiniUser($user);
$this->actingAsMiniStore($store);
$this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 4]]])
->assertJsonPath('success', true);
$order = StoreOrderModel::where('store_id', $store->id)->first();
@@ -144,12 +141,10 @@ class BillPaymentTest extends ProcurementTestCase
public function test_payment_audit_pass_accumulates_store_total_purchase_amount(): void
{
$store = StoreModel::factory()->create();
$user = UserModel::factory()->forStore($store->id)->create();
$payment = PaymentModel::create([
'payment_no' => 'ZF202608140001',
'store_id' => $store->id,
'user_id' => $user->id,
'amount' => '150.00',
'pay_method' => PaymentModel::METHOD_BANK,
'voucher_ids' => '',
@@ -176,12 +171,10 @@ class BillPaymentTest extends ProcurementTestCase
public function test_payment_audit_reject_does_not_accumulate(): void
{
$store = StoreModel::factory()->create();
$user = UserModel::factory()->forStore($store->id)->create();
$payment = PaymentModel::create([
'payment_no' => 'ZF202608140002',
'store_id' => $store->id,
'user_id' => $user->id,
'amount' => '100.00',
'pay_method' => PaymentModel::METHOD_WECHAT,
'voucher_ids' => '',
@@ -234,7 +227,6 @@ class BillPaymentTest extends ProcurementTestCase
public function test_payment_detail_returns_voucher_urls(): void
{
$store = StoreModel::factory()->create();
$user = UserModel::factory()->forStore($store->id)->create();
$makeFile = static function (string $path): int {
return (int) SysFileModel::create([
@@ -255,7 +247,6 @@ class BillPaymentTest extends ProcurementTestCase
$payment = PaymentModel::create([
'payment_no' => 'ZF202608140003',
'store_id' => $store->id,
'user_id' => $user->id,
'amount' => '100.00',
'pay_method' => PaymentModel::METHOD_BANK,
'voucher_ids' => [$fileA, $fileB],
+52 -90
View File
@@ -6,17 +6,17 @@ use App\Models\CartModel;
use App\Models\CustomerLevelModel;
use App\Models\ProductModel;
use App\Models\StoreModel;
use App\Models\UserModel;
/**
* 小程序购物车:加购合并、等级上浮价/金额服务端计算、归属校验、数据隔离、清空
* (门店即用户:购物车按 store_id 归属)
*/
class CartTest extends ProcurementTestCase
{
/**
* 造一家门店 + 一个上架商品(成本价即售价,等级上浮 0%)+ 该店用户
* 造一家门店(含等级)+ 一个上架商品(成本价即售价,等级上浮 0%)
*
* @return array{0: StoreModel, 1: ProductModel, 2: UserModel}
* @return array{0: StoreModel, 1: ProductModel}
*/
private function makeStoreWithProduct(string $price = '5.00'): array
{
@@ -27,14 +27,14 @@ class CartTest extends ProcurementTestCase
'cost_price' => $price,
]);
return [$store, $product, UserModel::factory()->forStore($store->id)->create()];
return [$store, $product];
}
/** 加购成功:数量入库、返回购物车项 */
public function test_add_to_cart_succeeds(): void
{
[, $product, $user] = $this->makeStoreWithProduct('5.50');
$this->actingAsMiniUser($user);
[$store, $product] = $this->makeStoreWithProduct('5.50');
$this->actingAsMiniStore($store);
$this->postJson('/mini/cart', [
'product_id' => $product->id,
@@ -46,15 +46,15 @@ class CartTest extends ProcurementTestCase
$cart = CartModel::first();
$this->assertNotNull($cart);
$this->assertSame($user->id, $cart->user_id);
$this->assertSame($store->id, $cart->store_id);
$this->assertSame('2.50', (string) $cart->quantity);
}
/** 重复加购同一商品合并累加(同商品唯一行) */
public function test_duplicate_add_merges_quantity(): void
{
[, $product, $user] = $this->makeStoreWithProduct();
$this->actingAsMiniUser($user);
[$store, $product] = $this->makeStoreWithProduct();
$this->actingAsMiniStore($store);
$this->postJson('/mini/cart', ['product_id' => $product->id, 'quantity' => 2]);
$this->postJson('/mini/cart', ['product_id' => $product->id, 'quantity' => 3])
@@ -67,9 +67,9 @@ class CartTest extends ProcurementTestCase
/** 已下架商品拒绝加购 */
public function test_off_shelf_product_rejected(): void
{
[, $product, $user] = $this->makeStoreWithProduct();
[$store, $product] = $this->makeStoreWithProduct();
$product->update(['status' => ProductModel::STATUS_OFF]);
$this->actingAsMiniUser($user);
$this->actingAsMiniStore($store);
$this->postJson('/mini/cart', ['product_id' => $product->id, 'quantity' => 1])
->assertOk()
@@ -78,25 +78,11 @@ class CartTest extends ProcurementTestCase
$this->assertSame(0, CartModel::count());
}
/** 已软删除商品拒绝加购 */
public function test_soft_deleted_product_rejected(): void
/** 门店未设置客户等级时拒绝加购 */
public function test_store_without_level_rejected(): void
{
[, $product, $user] = $this->makeStoreWithProduct();
$product->delete();
$this->actingAsMiniUser($user);
$this->postJson('/mini/cart', ['product_id' => $product->id, 'quantity' => 1])
->assertJsonPath('success', false);
$this->assertSame(0, CartModel::count());
}
/** 门店绑定的客户等级被删除时拒绝加购 */
public function test_store_level_missing_rejected(): void
{
[$store, $product, $user] = $this->makeStoreWithProduct();
CustomerLevelModel::whereKey($store->level_id)->delete();
$this->actingAsMiniUser($user);
[, $product] = $this->makeStoreWithProduct();
$this->actingAsMiniStore(StoreModel::factory()->create(['level_id' => 0]));
$this->postJson('/mini/cart', ['product_id' => $product->id, 'quantity' => 1])
->assertJsonPath('success', false)
@@ -105,28 +91,6 @@ class CartTest extends ProcurementTestCase
$this->assertSame(0, CartModel::count());
}
/** 未绑定门店的用户拒绝加购 */
public function test_unbound_user_rejected(): void
{
[, $product] = $this->makeStoreWithProduct();
$this->actingAsMiniUser(UserModel::factory()->create());
$this->postJson('/mini/cart', ['product_id' => $product->id, 'quantity' => 1])
->assertJsonPath('success', false)
->assertJsonPath('msg', '尚未绑定门店,请联系客服处理');
}
/** 门店未设置客户等级时拒绝加购 */
public function test_store_without_level_rejected(): void
{
[, $product] = $this->makeStoreWithProduct();
$store = StoreModel::factory()->create(['level_id' => 0]);
$this->actingAsMiniUser(UserModel::factory()->forStore($store->id)->create());
$this->postJson('/mini/cart', ['product_id' => $product->id, 'quantity' => 1])
->assertJsonPath('success', false);
}
/** 列表:实时等级上浮价、服务端金额、汇总(同店两商品,新加入在前) */
public function test_list_with_prices_amounts_and_totals(): void
{
@@ -134,7 +98,7 @@ class CartTest extends ProcurementTestCase
$store = StoreModel::factory()->create(['level_id' => $level->id]);
$p1 = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'cost_price' => '5.50']);
$p2 = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'cost_price' => '3.00']);
$this->actingAsMiniUser(UserModel::factory()->forStore($store->id)->create());
$this->actingAsMiniStore($store);
$this->postJson('/mini/cart', ['product_id' => $p1->id, 'quantity' => 3]);
$this->postJson('/mini/cart', ['product_id' => $p2->id, 'quantity' => 2]);
@@ -163,7 +127,7 @@ class CartTest extends ProcurementTestCase
$store = StoreModel::factory()->create(['level_id' => $level->id]);
$p1 = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'cost_price' => '5.50']);
$p2 = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'cost_price' => '3.00']);
$this->actingAsMiniUser(UserModel::factory()->forStore($store->id)->create());
$this->actingAsMiniStore($store);
$this->postJson('/mini/cart', ['product_id' => $p1->id, 'quantity' => 3]);
$this->postJson('/mini/cart', ['product_id' => $p2->id, 'quantity' => 2]);
@@ -181,7 +145,7 @@ class CartTest extends ProcurementTestCase
public function test_empty_cart_returns_empty_items(): void
{
$store = StoreModel::factory()->create(['level_id' => CustomerLevelModel::factory()->create()->id]);
$this->actingAsMiniUser(UserModel::factory()->forStore($store->id)->create());
$this->actingAsMiniStore($store);
$this->getJson('/mini/cart')->assertOk()
->assertJsonPath('data.items', [])
@@ -192,8 +156,8 @@ class CartTest extends ProcurementTestCase
/** 修改数量 */
public function test_update_quantity(): void
{
[, $product, $user] = $this->makeStoreWithProduct();
$this->actingAsMiniUser($user);
[$store, $product] = $this->makeStoreWithProduct();
$this->actingAsMiniStore($store);
$this->postJson('/mini/cart', ['product_id' => $product->id, 'quantity' => 1]);
$cart = CartModel::first();
@@ -201,30 +165,30 @@ class CartTest extends ProcurementTestCase
->assertJsonPath('success', true)
->assertJsonPath('data.quantity', '7.00');
$this->assertSame('7.00', (string) $cart->fresh()->quantity);
$this->assertSame('7.00', (string) $cart->refresh()->quantity);
}
/** 修改他购物车项拒绝(同店另一用户) */
public function test_update_other_users_item_rejected(): void
/** 修改他购物车项拒绝 */
public function test_update_other_stores_item_rejected(): void
{
[$store, $product, $userA] = $this->makeStoreWithProduct();
$this->actingAsMiniUser($userA);
[$store, $product] = $this->makeStoreWithProduct();
$this->actingAsMiniStore($store);
$this->postJson('/mini/cart', ['product_id' => $product->id, 'quantity' => 1]);
$cart = CartModel::first();
$this->actingAsMiniUser(UserModel::factory()->forStore($store->id)->create());
$this->actingAsMiniStore(StoreModel::factory()->create());
$this->putJson("/mini/cart/{$cart->id}", ['quantity' => 9])
->assertJsonPath('success', false)
->assertJsonPath('msg', '购物车项不存在');
$this->assertSame('1.00', (string) $cart->fresh()->quantity, '他改数量不应生效');
$this->assertSame('1.00', (string) $cart->refresh()->quantity, '他改数量不应生效');
}
/** 修改不存在的购物车项拒绝 */
public function test_update_missing_item_rejected(): void
{
[$store, $product, $user] = $this->makeStoreWithProduct();
$this->actingAsMiniUser($user);
[$store, $product] = $this->makeStoreWithProduct();
$this->actingAsMiniStore($store);
$this->postJson('/mini/cart', ['product_id' => $product->id, 'quantity' => 1]);
$this->putJson('/mini/cart/999999', ['quantity' => 1])->assertJsonPath('success', false);
@@ -233,8 +197,8 @@ class CartTest extends ProcurementTestCase
/** 数量校验:0 与负值拒绝 */
public function test_invalid_quantity_rejected(): void
{
[, $product, $user] = $this->makeStoreWithProduct();
$this->actingAsMiniUser($user);
[$store, $product] = $this->makeStoreWithProduct();
$this->actingAsMiniStore($store);
$this->postJson('/mini/cart', ['product_id' => $product->id, 'quantity' => 0])
->assertOk()
@@ -247,8 +211,8 @@ class CartTest extends ProcurementTestCase
/** 删除单项 */
public function test_delete_item(): void
{
[, $product, $user] = $this->makeStoreWithProduct();
$this->actingAsMiniUser($user);
[$store, $product] = $this->makeStoreWithProduct();
$this->actingAsMiniStore($store);
$this->postJson('/mini/cart', ['product_id' => $product->id, 'quantity' => 1]);
$cart = CartModel::first();
@@ -256,15 +220,15 @@ class CartTest extends ProcurementTestCase
$this->assertSame(0, CartModel::count());
}
/** 删除他购物车项拒绝 */
public function test_delete_other_users_item_rejected(): void
/** 删除他购物车项拒绝 */
public function test_delete_other_stores_item_rejected(): void
{
[$store, $product, $userA] = $this->makeStoreWithProduct();
$this->actingAsMiniUser($userA);
[$store, $product] = $this->makeStoreWithProduct();
$this->actingAsMiniStore($store);
$this->postJson('/mini/cart', ['product_id' => $product->id, 'quantity' => 1]);
$cart = CartModel::first();
$this->actingAsMiniUser(UserModel::factory()->forStore($store->id)->create());
$this->actingAsMiniStore(StoreModel::factory()->create());
$this->deleteJson("/mini/cart/{$cart->id}")
->assertJsonPath('success', false)
->assertJsonPath('msg', '购物车项不存在');
@@ -272,28 +236,27 @@ class CartTest extends ProcurementTestCase
$this->assertSame(1, CartModel::count());
}
/** 清空购物车仅影响当前用户 */
/** 清空购物车仅影响本店 */
public function test_clear_only_own_cart(): void
{
$level = CustomerLevelModel::factory()->create();
$store = StoreModel::factory()->create(['level_id' => $level->id]);
$storeA = StoreModel::factory()->create(['level_id' => $level->id]);
$storeB = StoreModel::factory()->create(['level_id' => $level->id]);
$p1 = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'cost_price' => '5.00']);
$p2 = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'cost_price' => '5.00']);
$userA = UserModel::factory()->forStore($store->id)->create();
$userB = UserModel::factory()->forStore($store->id)->create();
$this->actingAsMiniUser($userA);
$this->actingAsMiniStore($storeA);
$this->postJson('/mini/cart', ['product_id' => $p1->id, 'quantity' => 1]);
$this->postJson('/mini/cart', ['product_id' => $p2->id, 'quantity' => 1]);
$this->actingAsMiniUser($userB);
$this->actingAsMiniStore($storeB);
$this->postJson('/mini/cart', ['product_id' => $p1->id, 'quantity' => 1]);
$this->actingAsMiniUser($userA);
$this->actingAsMiniStore($storeA);
$this->deleteJson('/mini/cart')->assertJsonPath('success', true);
$this->assertSame(0, CartModel::where('user_id', $userA->id)->count());
$this->assertSame(1, CartModel::where('user_id', $userB->id)->count(), '他购物车不受影响');
$this->assertSame(0, CartModel::where('store_id', $storeA->id)->count());
$this->assertSame(1, CartModel::where('store_id', $storeB->id)->count(), '他购物车不受影响');
}
/** 未登录访问购物车 → 401 */
@@ -302,19 +265,18 @@ class CartTest extends ProcurementTestCase
$this->getJson('/mini/cart')->assertStatus(401);
}
/** 跨用户列表隔离:B 看不到 A 的购物车 */
public function test_cart_isolated_between_users(): void
/** 跨门店列表隔离:B 看不到 A 的购物车 */
public function test_cart_isolated_between_stores(): void
{
$level = CustomerLevelModel::factory()->create();
$store = StoreModel::factory()->create(['level_id' => $level->id]);
$storeA = StoreModel::factory()->create(['level_id' => $level->id]);
$storeB = StoreModel::factory()->create(['level_id' => $level->id]);
$product = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'cost_price' => '5.00']);
$userA = UserModel::factory()->forStore($store->id)->create();
$userB = UserModel::factory()->forStore($store->id)->create();
$this->actingAsMiniUser($userA);
$this->actingAsMiniStore($storeA);
$this->postJson('/mini/cart', ['product_id' => $product->id, 'quantity' => 1]);
$this->actingAsMiniUser($userB);
$this->actingAsMiniStore($storeB);
$this->getJson('/mini/cart')->assertOk()
->assertJsonPath('data.items', [])
->assertJsonPath('data.total_count', 0);
+1 -3
View File
@@ -10,7 +10,6 @@ use App\Models\ProductModel;
use App\Models\PurchaseOrderModel;
use App\Models\StoreModel;
use App\Models\StoreOrderModel;
use App\Models\UserModel;
use Illuminate\Support\Facades\DB;
use Maatwebsite\Excel\Facades\Excel;
use Modules\SystemTool\Services\SysSiteConfigService;
@@ -58,9 +57,8 @@ class ContainerReturnTest extends ProcurementTestCase
$level = CustomerLevelModel::factory()->create();
$store = StoreModel::factory()->create(['level_id' => $level->id]);
$product = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'cost_price' => '5.00']);
$user = UserModel::factory()->forStore($store->id)->create();
$this->actingAsMiniUser($user);
$this->actingAsMiniStore($store);
$this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 4]]])
->assertJsonPath('success', true);
$order = StoreOrderModel::where('store_id', $store->id)->first();
+5 -8
View File
@@ -11,7 +11,6 @@ use App\Models\StoreModel;
use App\Models\StoreOrderItemModel;
use App\Models\StoreOrderModel;
use App\Models\SupplierModel;
use App\Models\UserModel;
/**
* 仪表盘分析页聚合接口:权限校验、指标口径(排除已取消)、趋势补日、排行与对账概览
@@ -262,7 +261,6 @@ class DashboardTest extends ProcurementTestCase
PaymentModel::create([
'payment_no' => 'ZF' . random_int(100000000000, 999999999999),
'store_id' => $store->id,
'user_id' => 0,
'amount' => '88.00',
'pay_method' => PaymentModel::METHOD_WECHAT,
'status' => PaymentModel::STATUS_PENDING,
@@ -302,7 +300,7 @@ class DashboardTest extends ProcurementTestCase
$this->assertSame('已完成', $latest[0]['status_name']);
}
/** 基础档案仅统计启用中的门店/商品/供应商/用户 */
/** 基础档案仅统计启用中的门店/商品/供应商 */
public function test_archives_count_active_records_only(): void
{
StoreModel::factory()->count(2)->create(['status' => StoreModel::STATUS_NORMAL]);
@@ -311,15 +309,14 @@ class DashboardTest extends ProcurementTestCase
ProductModel::factory()->create(['status' => ProductModel::STATUS_OFF]);
SupplierModel::factory()->create(['status' => SupplierModel::STATUS_NORMAL]);
SupplierModel::factory()->create(['status' => SupplierModel::STATUS_DISABLED]);
UserModel::factory()->count(2)->create(['status' => UserModel::STATUS_NORMAL]);
UserModel::factory()->create(['status' => UserModel::STATUS_DISABLED]);
$this->actingAsSysUser(['dashboard.analysis']);
$this->getJson(self::ROUTE)
$response = $this->getJson(self::ROUTE)
->assertOk()
->assertJsonPath('data.archives.stores', 2)
->assertJsonPath('data.archives.products', 3)
->assertJsonPath('data.archives.suppliers', 1)
->assertJsonPath('data.archives.users', 2);
->assertJsonPath('data.archives.suppliers', 1);
$this->assertArrayNotHasKey('users', $response->json('data.archives'), '档案计数不再返回 users 键(与门店数重复)');
}
}
+3 -4
View File
@@ -12,7 +12,6 @@ use App\Models\PurchaseOrderModel;
use App\Models\StoreModel;
use App\Models\StoreOrderModel;
use App\Models\SupplierModel;
use App\Models\UserModel;
use Maatwebsite\Excel\Facades\Excel;
/**
@@ -34,7 +33,7 @@ class ExportTest extends ProcurementTestCase
$veg = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'category_id' => $vegRoot->id, 'cost_price' => '5.00']);
$meat = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'category_id' => $meatRoot->id, 'cost_price' => '20.00']);
$this->actingAsMiniUser(UserModel::factory()->forStore($store->id)->create());
$this->actingAsMiniStore($store);
$this->postJson('/mini/order', ['items' => [
['product_id' => $veg->id, 'quantity' => 2],
['product_id' => $meat->id, 'quantity' => 1],
@@ -117,12 +116,12 @@ class ExportTest extends ProcurementTestCase
'supplier_id' => $supplierB->id,
]);
$this->actingAsMiniUser(UserModel::factory()->forStore($storeA->id)->create());
$this->actingAsMiniStore($storeA);
$this->postJson('/mini/order', ['items' => [
['product_id' => $veg->id, 'quantity' => 2],
['product_id' => $meat->id, 'quantity' => 1],
]])->assertJsonPath('success', true);
$this->actingAsMiniUser(UserModel::factory()->forStore($storeB->id)->create());
$this->actingAsMiniStore($storeB);
$this->postJson('/mini/order', ['items' => [
['product_id' => $veg->id, 'quantity' => 3],
]])->assertJsonPath('success', true);
+120 -173
View File
@@ -3,195 +3,142 @@
namespace Tests\Feature;
use App\Models\StoreModel;
use App\Models\UserModel;
use App\Services\WechatService;
use Illuminate\Support\Facades\DB;
use Modules\SystemTool\Services\SysSiteConfigService;
use Symfony\Component\HttpClient\MockHttpClient;
use Symfony\Component\HttpClient\Response\MockResponse;
/**
* 小程序认证:EasyWeChat MockHttpClient 拦截微信调用
* 注册绑定门店、登录签发 token、停用拒绝、双端 token 隔离
* 小程序认证:门店 账号 + 密码 登录(不再使用微信能力)
* 停用拒绝、修改密码、双端 token 隔离
*/
class MiniAuthTest extends ProcurementTestCase
{
protected function setUp(): void
/** 正确账号密码 → 签发 token,回传门店信息(含等级),刷新最后登录时间 */
public function test_login_issues_token_for_valid_credentials(): void
{
parent::setUp();
// 微信配置为 DB 驱动(后台「小程序设置」→ sys_site_configWechatService 经
// site_config('wechatMini') 读取):测试落库并刷新配置缓存
DB::table('sys_site_config_group')->insert([
'id' => 100,
'title' => '小程序设置',
'key' => 'wechatMini',
'remark' => '',
'created_at' => now(),
'updated_at' => now(),
$level = \App\Models\CustomerLevelModel::factory()->create();
$store = StoreModel::factory()->withPassword('abc12345')->create([
'username' => 'store_login_1',
'level_id' => $level->id,
]);
DB::table('sys_site_config_items')->insert([
[
'group_id' => 100, 'key' => 'appid', 'title' => 'APPID', 'describe' => '',
'values' => 'test_appid', 'type' => 'Input', 'options' => null, 'props' => null, 'sort' => 0,
'created_at' => now(), 'updated_at' => now(),
],
[
'group_id' => 100, 'key' => 'secret', 'title' => 'SecretKey', 'describe' => '',
'values' => 'test_secret', 'type' => 'Input', 'options' => null, 'props' => null, 'sort' => 1,
'created_at' => now(), 'updated_at' => now(),
],
$response = $this->postJson('/mini/auth/login', [
'username' => 'store_login_1',
'password' => 'abc12345',
]);
SysSiteConfigService::refreshSiteConfig();
$response->assertOk()
->assertJsonPath('success', true)
->assertJsonStructure(['data' => ['token', 'user' => ['id', 'name', 'code', 'level']]]);
$this->assertNotEmpty($response->json('data.token'));
$this->assertSame($store->id, $response->json('data.user.id'));
// 密码永不回显
$this->assertArrayNotHasKey('password', $response->json('data.user'));
$this->assertNotNull($store->refresh()->last_login_at);
}
/**
* 向 WechatService 注入 MockHttpClient,按 URL 分发微信接口 mock 响应
*
* @param callable(string, string): MockResponse $responder
*/
private function mockWechat(callable $responder): void
/** 密码错误 → 拒绝 */
public function test_login_rejects_wrong_password(): void
{
app(WechatService::class)->setHttpClient(
new MockHttpClient($responder, 'https://api.weixin.qq.com')
);
StoreModel::factory()->withPassword('abc12345')->create(['username' => 'store_login_2']);
$this->postJson('/mini/auth/login', [
'username' => 'store_login_2',
'password' => 'wrong_pwd',
])->assertOk()
->assertJsonPath('success', false);
}
/** wx.login code → 已注册用户签发 token */
public function test_login_issues_token_for_registered_user(): void
/** 账号不存在 → 拒绝 */
public function test_login_rejects_unknown_username(): void
{
$this->postJson('/mini/auth/login', [
'username' => 'not_exist',
'password' => 'abc12345',
])->assertOk()
->assertJsonPath('success', false);
}
/** 停用门店拒绝登录 */
public function test_disabled_store_cannot_login(): void
{
StoreModel::factory()->disabled()->withPassword('abc12345')->create(['username' => 'store_login_3']);
$this->postJson('/mini/auth/login', [
'username' => 'store_login_3',
'password' => 'abc12345',
])->assertOk()
->assertJsonPath('success', false);
}
/** 登录参数缺失 → 校验失败 */
public function test_login_validates_required_params(): void
{
$this->postJson('/mini/auth/login', [])
->assertOk()
->assertJsonPath('success', false);
}
/** 登录签发的 token 可访问受保护接口,auth/info 返回当前门店 */
public function test_login_token_accesses_protected_routes(): void
{
$store = StoreModel::factory()->withPassword('abc12345')->create(['username' => 'store_login_4']);
$login = $this->postJson('/mini/auth/login', [
'username' => 'store_login_4',
'password' => 'abc12345',
]);
$token = $login->json('data.token');
$this->withToken($token)->getJson('/mini/auth/info')
->assertOk()
->assertJsonPath('success', true)
->assertJsonPath('data.id', $store->id);
}
/** 修改密码:原密码正确 → 新密码可登录 */
public function test_set_password_succeeds(): void
{
$store = StoreModel::factory()->withPassword('old_pwd_1')->create();
$this->actingAsMiniStore($store);
$this->putJson('/mini/auth/password', [
'oldPassword' => 'old_pwd_1',
'newPassword' => 'new_pwd_1',
'rePassword' => 'new_pwd_1',
])->assertOk()
->assertJsonPath('success', true);
$this->postJson('/mini/auth/login', [
'username' => $store->username,
'password' => 'new_pwd_1',
])->assertOk()
->assertJsonPath('success', true);
}
/** 修改密码:原密码错误 → 拒绝 */
public function test_set_password_rejects_wrong_old_password(): void
{
$store = StoreModel::factory()->withPassword('old_pwd_2')->create();
$this->actingAsMiniStore($store);
$this->putJson('/mini/auth/password', [
'oldPassword' => 'bad_old',
'newPassword' => 'new_pwd_2',
'rePassword' => 'new_pwd_2',
])->assertOk()
->assertJsonPath('success', false);
}
/** 修改密码:两次输入不一致 → 校验失败 */
public function test_set_password_validates_confirmation(): void
{
$store = StoreModel::factory()->create();
$user = UserModel::factory()->forStore($store->id)->create(['openid' => 'openid_test_001']);
$this->mockWechat(fn () => new MockResponse((string) json_encode([
'openid' => 'openid_test_001',
'session_key' => 'session_key_x',
])));
$this->actingAsMiniStore($store);
$response = $this->postJson('/mini/auth/login', ['code' => 'wx_code']);
$response->assertOk()
->assertJsonPath('success', true)
->assertJsonStructure(['data' => ['token', 'user' => ['id']]]);
$this->assertNotEmpty($response->json('data.token'));
$this->assertNotNull($user->fresh()->last_login_at);
}
/** openid 未注册 → 拒绝登录 */
public function test_login_rejects_unregistered_openid(): void
{
$this->mockWechat(fn () => new MockResponse((string) json_encode([
'openid' => 'openid_unknown',
'session_key' => 'sk',
])));
$this->postJson('/mini/auth/login', ['code' => 'wx_code'])
->assertOk()
$this->putJson('/mini/auth/password', [
'oldPassword' => '123456',
'newPassword' => 'new_pwd_3',
'rePassword' => 'different',
])->assertOk()
->assertJsonPath('success', false);
$this->assertSame(0, UserModel::count());
}
/** 注册:门店编码绑定门店并签发 token */
public function test_register_binds_store_and_issues_token(): void
{
$store = StoreModel::factory()->create(['code' => 'ST001', 'phone' => '13800138000']);
$this->mockWechat(function (string $method, string $url): MockResponse {
if (str_contains($url, 'jscode2session')) {
return new MockResponse((string) json_encode([
'openid' => 'openid_reg_001',
'session_key' => 'sk',
]));
}
if (str_contains($url, 'cgi-bin/token')) {
return new MockResponse((string) json_encode([
'access_token' => 'mock_access_token',
'expires_in' => 7200,
]));
}
return new MockResponse((string) json_encode([
'errcode' => 0,
'phone_info' => ['phoneNumber' => '13800138000'],
]));
});
$response = $this->postJson('/mini/auth/register', [
'code' => 'wx_code',
'phoneCode' => 'phone_code',
'storeCode' => 'ST001',
]);
$response->assertOk()
->assertJsonPath('success', true)
->assertJsonStructure(['data' => ['token', 'user' => ['id', 'store_id']]]);
$this->assertNotEmpty($response->json('data.token'));
$user = UserModel::where('openid', 'openid_reg_001')->first();
$this->assertNotNull($user, '应按 openid 注册用户');
$this->assertSame($store->id, $user->store_id);
$this->assertSame('13800138000', $user->phone);
}
/** 注册:门店编码不存在 → 拒绝 */
public function test_register_rejects_unknown_store(): void
{
$this->mockWechat(fn () => new MockResponse((string) json_encode([
'openid' => 'openid_reg_002',
'session_key' => 'sk',
])));
$this->postJson('/mini/auth/register', [
'code' => 'wx_code',
'phoneCode' => 'phone_code',
'storeCode' => 'NOT_EXIST',
])
->assertOk()
->assertJsonPath('success', false);
$this->assertSame(0, UserModel::count());
}
/** 注册:openid 已注册 → 拒绝重复注册 */
public function test_register_rejects_duplicate_openid(): void
{
UserModel::factory()->create(['openid' => 'openid_dup']);
$this->mockWechat(fn () => new MockResponse((string) json_encode([
'openid' => 'openid_dup',
'session_key' => 'sk',
])));
$this->postJson('/mini/auth/register', [
'code' => 'wx_code',
'phoneCode' => 'phone_code',
'storeCode' => 'ST001',
])
->assertOk()
->assertJsonPath('success', false);
$this->assertSame(1, UserModel::count());
}
/** 停用账号拒绝登录 */
public function test_disabled_user_cannot_login(): void
{
UserModel::factory()->disabled()->create(['openid' => 'openid_disabled']);
$this->mockWechat(fn () => new MockResponse((string) json_encode([
'openid' => 'openid_disabled',
'session_key' => 'sk',
])));
$this->postJson('/mini/auth/login', ['code' => 'c'])
->assertOk()
->assertJsonPath('success', false);
}
/** 微信接口报错时登录失败(错误被转译为业务异常,不泄露原始报文结构) */
public function test_login_fails_when_wechat_rejects_code(): void
{
$this->mockWechat(fn () => new MockResponse((string) json_encode([
'errcode' => 40029,
'errmsg' => 'invalid code',
])));
$this->postJson('/mini/auth/login', ['code' => 'bad_code'])
->assertOk()
->assertJsonPath('success', false);
$this->assertSame(0, UserModel::count());
}
/** 跨端隔离:后台 token 访问小程序接口 → 401 */
@@ -205,7 +152,7 @@ class MiniAuthTest extends ProcurementTestCase
/** 跨端隔离:小程序 token 访问后台接口 → 401 */
public function test_mini_token_cannot_access_admin_api(): void
{
$this->actingAsMiniUser(UserModel::factory()->create());
$this->actingAsMiniStore(StoreModel::factory()->create());
$this->getJson('/customer/level')->assertStatus(401);
}
+17 -23
View File
@@ -8,27 +8,21 @@ use App\Models\PurchaseOrderModel;
use App\Models\StoreModel;
use App\Models\StoreOrderItemModel;
use App\Models\StoreOrderModel;
use App\Models\UserModel;
use Modules\SystemTool\Models\SysFileModel;
/**
* 小程序账单:支付进度推导(待支付/审核中/已支付)、应结算日期(回款周期)、
* 待支付汇总、可付款筛选、门店数据隔离、详情归属校验
* (门店即用户:账单按 store_id 归属)
*/
class MiniBillTest extends ProcurementTestCase
{
private static int $billSeq = 0;
/**
* 造一个绑定正常门店的小程序用户
*
* @return array{0: StoreModel, 1: UserModel}
*/
private function makeStoreWithUser(int $cycleDays = 3): array
/** 造一家门店(指定回款周期) */
private function makeStore(int $cycleDays = 3): StoreModel
{
$store = StoreModel::factory()->paymentCycle($cycleDays)->create();
return [$store, UserModel::factory()->forStore($store->id)->create()];
return StoreModel::factory()->paymentCycle($cycleDays)->create();
}
/** 造一张账单(默认 2026-08-10 出账,总额 140.00,未支付) */
@@ -56,8 +50,8 @@ class MiniBillTest extends ProcurementTestCase
/** 支付进度推导与应结算日期(账单日期 + 门店回款周期) */
public function test_bill_list_derives_pay_state_and_settlement_date(): void
{
[$store, $user] = $this->makeStoreWithUser(3);
$this->actingAsMiniUser($user);
$store = $this->makeStore(3);
$this->actingAsMiniStore($store);
$unpaid = $this->makeBill($store);
$reviewing = $this->makeBill($store, ['payment_id' => 99]);
@@ -88,8 +82,8 @@ class MiniBillTest extends ProcurementTestCase
/** 待支付汇总:未支付笔数/金额(含审核中),不受列表筛选影响 */
public function test_bill_list_summary_aggregates_unpaid(): void
{
[$store, $user] = $this->makeStoreWithUser();
$this->actingAsMiniUser($user);
$store = $this->makeStore();
$this->actingAsMiniStore($store);
$this->makeBill($store, ['total_amount' => '100.00']);
$this->makeBill($store, ['total_amount' => '50.50', 'payment_id' => 99]);
@@ -104,8 +98,8 @@ class MiniBillTest extends ProcurementTestCase
/** 可付款筛选:仅未支付且未锁定到支付记录的账单(合并付款选择页) */
public function test_bill_list_payable_filter(): void
{
[$store, $user] = $this->makeStoreWithUser();
$this->actingAsMiniUser($user);
$store = $this->makeStore();
$this->actingAsMiniStore($store);
$payable = $this->makeBill($store);
$this->makeBill($store, ['payment_id' => 99]);
@@ -119,8 +113,8 @@ class MiniBillTest extends ProcurementTestCase
/** 账单日期区间筛选 */
public function test_bill_list_filters_by_bill_date_range(): void
{
[$store, $user] = $this->makeStoreWithUser();
$this->actingAsMiniUser($user);
$store = $this->makeStore();
$this->actingAsMiniStore($store);
$this->makeBill($store, ['bill_date' => '2026-08-01']);
$this->makeBill($store, ['bill_date' => '2026-08-05']);
@@ -134,11 +128,11 @@ class MiniBillTest extends ProcurementTestCase
/** 门店数据隔离:只能查看本店账单 */
public function test_bill_data_isolated_between_stores(): void
{
[$storeA] = $this->makeStoreWithUser();
$storeA = $this->makeStore();
$billOfA = $this->makeBill($storeA);
[, $userB] = $this->makeStoreWithUser();
$this->actingAsMiniUser($userB);
$storeB = $this->makeStore();
$this->actingAsMiniStore($storeB);
$this->getJson('/mini/bill')->assertJsonPath('data.total', 0);
$this->getJson("/mini/bill/{$billOfA->id}")->assertJsonPath('success', false);
@@ -147,8 +141,8 @@ class MiniBillTest extends ProcurementTestCase
/** 详情:格式化账单 + 合并商品明细 + 关联订单 */
public function test_bill_detail_outputs_bill_items_and_orders(): void
{
[$store, $user] = $this->makeStoreWithUser(0);
$this->actingAsMiniUser($user);
$store = $this->makeStore(0);
$this->actingAsMiniStore($store);
$file = SysFileModel::create([
'group_id' => 1,
+31 -37
View File
@@ -4,42 +4,36 @@ namespace Tests\Feature;
use App\Models\NoticeModel;
use App\Models\StoreModel;
use App\Models\UserModel;
/**
* 小程序消息通知:本通知 + 全员广播;广播已读复制本副本(data.broadcast_from 记来源),
* 小程序消息通知:本通知 + 全员广播;广播已读复制本副本(data.broadcast_from 记来源),
* 列表排除已读广播。回归:列表曾报 Undefined property: stdClass::$data->broadcast_from
* pluck JSON 路径被 Query Builder 当作结果列名读取 stdClass 属性)
* (门店即用户:通知按 store_id 归属,store_id=0 为全员广播)
*/
class MiniNoticeTest extends ProcurementTestCase
{
/**
* 造一个绑定门店的小程序用户
*
* @return array{0: StoreModel, 1: UserModel}
*/
private function makeStoreWithUser(): array
/** 造一家门店 */
private function makeStore(): StoreModel
{
$store = StoreModel::factory()->create();
return [$store, UserModel::factory()->forStore($store->id)->create()];
return StoreModel::factory()->create();
}
/** 通知列表:本通知 + 未读广播,unread_count 正确 */
/** 通知列表:本通知 + 未读广播,unread_count 正确 */
public function test_notice_list_includes_personal_and_broadcast(): void
{
[, $user] = $this->makeStoreWithUser();
$this->actingAsMiniUser($user);
$store = $this->makeStore();
$this->actingAsMiniStore($store);
NoticeModel::create([
'user_id' => NoticeModel::BROADCAST_USER_ID,
'store_id' => NoticeModel::BROADCAST_STORE_ID,
'type' => NoticeModel::TYPE_SYSTEM,
'title' => '系统公告',
'content' => '全员可见',
'is_read' => NoticeModel::UNREAD,
]);
NoticeModel::create([
'user_id' => $user->id,
'store_id' => $store->id,
'type' => NoticeModel::TYPE_ORDER,
'title' => '订单状态更新',
'content' => '您的订单已接单',
@@ -52,14 +46,14 @@ class MiniNoticeTest extends ProcurementTestCase
$this->assertSame(2, $data['unread_count']);
}
/** 广播标记已读:复制本已读副本,列表排除原广播(回归:列表不再报错) */
/** 广播标记已读:复制本已读副本,列表排除原广播(回归:列表不再报错) */
public function test_broadcast_read_creates_personal_copy(): void
{
[, $user] = $this->makeStoreWithUser();
$this->actingAsMiniUser($user);
$store = $this->makeStore();
$this->actingAsMiniStore($store);
$broadcast = NoticeModel::create([
'user_id' => NoticeModel::BROADCAST_USER_ID,
'store_id' => NoticeModel::BROADCAST_STORE_ID,
'type' => NoticeModel::TYPE_SYSTEM,
'title' => '系统公告',
'content' => '全员可见',
@@ -69,8 +63,8 @@ class MiniNoticeTest extends ProcurementTestCase
$this->putJson("/mini/notice/{$broadcast->id}/read")->assertJsonPath('success', true);
// 已读副本:本记录、已读、broadcast_from 记来源且保留原 data
$copy = NoticeModel::where('user_id', $user->id)->first();
// 已读副本:本记录、已读、broadcast_from 记来源且保留原 data
$copy = NoticeModel::where('store_id', $store->id)->first();
$this->assertNotNull($copy);
$this->assertSame(NoticeModel::READ, $copy->is_read);
$this->assertSame($broadcast->id, $copy->data['broadcast_from']);
@@ -84,17 +78,17 @@ class MiniNoticeTest extends ProcurementTestCase
// 重复标记不重复复制
$this->putJson("/mini/notice/{$broadcast->id}/read")->assertJsonPath('success', true);
$this->assertSame(1, NoticeModel::where('user_id', $user->id)->count());
$this->assertSame(1, NoticeModel::where('store_id', $store->id)->count());
}
/** 个人通知标记已读:直接更新原记录,不产生副本 */
/** 定向通知标记已读:直接更新原记录,不产生副本 */
public function test_personal_notice_read_marks_directly(): void
{
[, $user] = $this->makeStoreWithUser();
$this->actingAsMiniUser($user);
$store = $this->makeStore();
$this->actingAsMiniStore($store);
$notice = NoticeModel::create([
'user_id' => $user->id,
'store_id' => $store->id,
'type' => NoticeModel::TYPE_ORDER,
'title' => '订单状态更新',
'content' => '您的订单已接单',
@@ -102,26 +96,26 @@ class MiniNoticeTest extends ProcurementTestCase
]);
$this->putJson("/mini/notice/{$notice->id}/read")->assertJsonPath('success', true);
$this->assertSame(NoticeModel::READ, $notice->fresh()->is_read);
$this->assertSame(1, NoticeModel::count(), '个人通知直接更新,不产生副本');
$this->assertSame(NoticeModel::READ, $notice->refresh()->is_read);
$this->assertSame(1, NoticeModel::count(), '定向通知直接更新,不产生副本');
}
/** 他通知不可标记(仅本通知与广播可读) */
public function test_other_users_notice_not_found(): void
/** 他通知不可标记(仅本通知与广播可读) */
public function test_other_stores_notice_not_found(): void
{
[, $user] = $this->makeStoreWithUser();
$this->actingAsMiniUser($user);
$store = $this->makeStore();
$this->actingAsMiniStore($store);
[, $other] = $this->makeStoreWithUser();
$other = $this->makeStore();
$notice = NoticeModel::create([
'user_id' => $other->id,
'store_id' => $other->id,
'type' => NoticeModel::TYPE_ORDER,
'title' => '订单状态更新',
'content' => '他通知',
'content' => '他通知',
'is_read' => NoticeModel::UNREAD,
]);
$this->putJson("/mini/notice/{$notice->id}/read")->assertJsonPath('success', false);
$this->assertSame(NoticeModel::UNREAD, $notice->fresh()->is_read);
$this->assertSame(NoticeModel::UNREAD, $notice->refresh()->is_read);
}
}
+11 -11
View File
@@ -5,18 +5,18 @@ namespace Tests\Feature;
use App\Models\CustomerLevelModel;
use App\Models\ProductModel;
use App\Models\StoreModel;
use App\Models\UserModel;
/**
* 小程序商品:商品详情接口(免登录浏览,登录门店按等级上浮比例显示换算价);
* 商品列表回归:登录门店场景曾数组误用对象访问 + 价格缺失导致 500
* (门店即用户:登录主体为 StoreModel
*/
class MiniProductTest extends ProcurementTestCase
{
/**
* 造门店 + 上架商品(成本价即售价,等级上浮 0%)
*
* @return array{0: StoreModel, 1: ProductModel, 2: UserModel, 3: CustomerLevelModel}
* @return array{0: StoreModel, 1: ProductModel, 2: CustomerLevelModel}
*/
private function makeStoreWithProduct(string $price = '5.00'): array
{
@@ -27,7 +27,7 @@ class MiniProductTest extends ProcurementTestCase
'cost_price' => $price,
]);
return [$store, $product, UserModel::factory()->forStore($store->id)->create(), $level];
return [$store, $product, $level];
}
/** 商品详情:未登录浏览 price=null,基础字段完整 */
@@ -45,10 +45,10 @@ class MiniProductTest extends ProcurementTestCase
}
/** 商品详情:登录门店按等级显示固定价 */
public function test_product_detail_store_user_sees_level_price(): void
public function test_product_detail_store_sees_level_price(): void
{
[, $product, $user] = $this->makeStoreWithProduct('6.50');
$this->actingAsMiniUser($user);
[$store, $product] = $this->makeStoreWithProduct('6.50');
$this->actingAsMiniStore($store);
$data = $this->getJson("/mini/product/{$product->id}")->assertOk()->json('data');
@@ -58,10 +58,10 @@ class MiniProductTest extends ProcurementTestCase
/** 商品详情:按门店等级上浮比例换算(成本价上浮 30%) */
public function test_product_detail_percent_price_calculated_from_cost(): void
{
[, $product, $user, $level] = $this->makeStoreWithProduct();
[$store, $product, $level] = $this->makeStoreWithProduct();
$level->update(['percent' => 30]);
$product->update(['cost_price' => '20.00']);
$this->actingAsMiniUser($user);
$this->actingAsMiniStore($store);
$data = $this->getJson("/mini/product/{$product->id}")->assertOk()->json('data');
@@ -79,10 +79,10 @@ class MiniProductTest extends ProcurementTestCase
}
/** 商品列表:登录门店显示等级价(回归:曾数组误用对象访问导致 500) */
public function test_product_list_store_user_sees_level_price(): void
public function test_product_list_store_sees_level_price(): void
{
[, $product, $user] = $this->makeStoreWithProduct('7.00');
$this->actingAsMiniUser($user);
[$store, $product] = $this->makeStoreWithProduct('7.00');
$this->actingAsMiniStore($store);
$rows = $this->getJson('/mini/product/list')->assertOk()->json('data.data');
$row = collect($rows)->firstWhere('id', $product->id);
+22 -21
View File
@@ -3,23 +3,23 @@
namespace Tests\Feature;
use App\Models\StoreModel;
use App\Models\UserModel;
/**
* 小程序门店设置:详情回显、联系人/电话/地址维护、
* 名称/编码/回款周期等字段不可通过小程序端修改
* (门店即用户:登录主体就是门店,操作对象即当前登录门店)
*/
class MiniStoreTest extends ProcurementTestCase
{
/** 门店详情:返回当前绑定门店信息(含只读回款周期) */
public function test_info_returns_bound_store(): void
/** 门店详情:返回当前门店信息(含只读回款周期) */
public function test_info_returns_current_store(): void
{
$store = StoreModel::factory()->paymentCycle(7)->create([
'contact' => '张三',
'phone' => '13800138000',
'address' => '幸福路 1 号',
]);
$this->actingAsMiniUser(UserModel::factory()->forStore($store->id)->create());
$this->actingAsMiniStore($store);
$this->getJson('/mini/store/info')
->assertOk()
@@ -35,7 +35,7 @@ class MiniStoreTest extends ProcurementTestCase
public function test_update_info(): void
{
$store = StoreModel::factory()->create();
$this->actingAsMiniUser(UserModel::factory()->forStore($store->id)->create());
$this->actingAsMiniStore($store);
$this->putJson('/mini/store/info', [
'contact' => '李四',
@@ -48,10 +48,10 @@ class MiniStoreTest extends ProcurementTestCase
->assertJsonPath('data.phone', '13900139000')
->assertJsonPath('data.address', '建设路 88 号');
$fresh = $store->fresh();
$this->assertSame('李四', $fresh->contact);
$this->assertSame('13900139000', $fresh->phone);
$this->assertSame('建设路 88 号', $fresh->address);
$store->refresh();
$this->assertSame('李四', $store->contact);
$this->assertSame('13900139000', $store->phone);
$this->assertSame('建设路 88 号', $store->address);
}
/** 白名单更新:名称 / 编码 / 回款周期等字段提交后被忽略 */
@@ -61,7 +61,7 @@ class MiniStoreTest extends ProcurementTestCase
'name' => '原门店',
'code' => 'ST100',
]);
$this->actingAsMiniUser(UserModel::factory()->forStore($store->id)->create());
$this->actingAsMiniStore($store);
$this->putJson('/mini/store/info', [
'contact' => '王五',
@@ -72,22 +72,23 @@ class MiniStoreTest extends ProcurementTestCase
->assertOk()
->assertJsonPath('success', true);
$fresh = $store->fresh();
$this->assertSame('王五', $fresh->contact);
$this->assertSame('原门店', $fresh->name);
$this->assertSame('ST100', $fresh->code);
$this->assertSame(5, $fresh->payment_cycle_days);
$store->refresh();
$this->assertSame('王五', $store->contact);
$this->assertSame('原门店', $store->name);
$this->assertSame('ST100', $store->code);
$this->assertSame(5, $store->payment_cycle_days);
}
/** 未绑定门店 → 拒绝访问 */
public function test_requires_bound_store(): void
/** 停用门店 → 拒绝访问 */
public function test_disabled_store_rejected(): void
{
$this->actingAsMiniUser(UserModel::factory()->create());
$store = StoreModel::factory()->disabled()->create();
$this->actingAsMiniStore($store);
$this->getJson('/mini/store/info')
->assertOk()
->assertJsonPath('success', false)
->assertJsonPath('msg', '尚未绑定门店,请联系客服处理');
->assertJsonPath('msg', '账号不存在或已被停用');
$this->putJson('/mini/store/info', ['contact' => '路人'])
->assertOk()
@@ -98,13 +99,13 @@ class MiniStoreTest extends ProcurementTestCase
public function test_update_info_validates_length(): void
{
$store = StoreModel::factory()->create();
$this->actingAsMiniUser(UserModel::factory()->forStore($store->id)->create());
$this->actingAsMiniStore($store);
$this->putJson('/mini/store/info', ['contact' => str_repeat('长', 51)])
->assertOk()
->assertJsonPath('success', false)
->assertJsonPath('msg', '联系人最长 50 个字符');
$this->assertSame($store->contact, $store->fresh()->contact, '校验失败不应落库');
$this->assertSame($store->contact, $store->refresh()->contact, '校验失败不应落库');
}
}
+6 -5
View File
@@ -2,7 +2,7 @@
namespace Tests\Feature;
use App\Models\UserModel;
use App\Models\StoreModel;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Modules\SystemUser\Models\SysUserModel;
use Tests\TestCase;
@@ -13,6 +13,7 @@ use Tests\TestCase;
* - SQLite :memory: + RefreshDatabase,每个测试方法独立迁移
* - 双端认证辅助:createToken 签发 Sanctum tokenwithToken 注入 Authorization 头,
* 走真实链路(auth:sanctum → AuthGuardMiddleware 双端隔离 → abilities 校验)
* - 门店即用户:小程序端登录主体为 StoreModelstore 表)
*/
abstract class ProcurementTestCase extends TestCase
{
@@ -52,16 +53,16 @@ abstract class ProcurementTestCase extends TestCase
}
/**
* 以小程序用户身份发起后续请求(token abilities 默认 ['mini']
* 以门店身份(小程序端)发起后续请求(token abilities 默认 ['mini']
*
* @param array<int, string> $abilities
*/
protected function actingAsMiniUser(UserModel $user, array $abilities = ['mini']): UserModel
protected function actingAsMiniStore(StoreModel $store, array $abilities = ['mini']): StoreModel
{
$this->app['auth']->forgetGuards();
$this->withToken($user->createToken('mini', $abilities)->plainTextToken);
$this->withToken($store->createToken('mini', $abilities)->plainTextToken);
return $user;
return $store;
}
}
+2 -2
View File
@@ -3,7 +3,7 @@
namespace Tests\Feature;
use App\Models\ProductCategoryModel;
use App\Models\UserModel;
use App\Models\StoreModel;
/**
* 商品分类两级限制 + 商品只能挂末级分类:
@@ -169,7 +169,7 @@ class ProductCategoryTest extends ProcurementTestCase
'status' => ProductCategoryModel::STATUS_DISABLED,
]);
$this->actingAsMiniUser(UserModel::factory()->create());
$this->actingAsMiniStore(StoreModel::factory()->create());
$response = $this->getJson('/mini/product/categories');
$response->assertOk()->assertJsonPath('success', true);
+12 -14
View File
@@ -7,7 +7,6 @@ use App\Models\NoticeModel;
use App\Models\ProductCategoryModel;
use App\Models\ProductModel;
use App\Models\StoreModel;
use App\Models\UserModel;
/**
* 商品成本价 + 等级上浮计价:
@@ -26,7 +25,7 @@ class ProductCostPriceTest extends ProcurementTestCase
]);
}
/** 造门店(等级上浮 percent)+ 上架商品(成本价 cost) + 该店用户 */
/** 造门店(等级上浮 percent)+ 上架商品(成本价 cost */
private function makeStoreWithPercentProduct(float $cost = 10, float $percent = 30): array
{
$level = CustomerLevelModel::factory()->create(['percent' => $percent]);
@@ -36,7 +35,7 @@ class ProductCostPriceTest extends ProcurementTestCase
'cost_price' => $cost,
]);
return [$store, $product, UserModel::factory()->forStore($store->id)->create()];
return [$store, $product];
}
/** 售价换算:成本价 × (100 + percent) / 100 */
@@ -78,8 +77,8 @@ class ProductCostPriceTest extends ProcurementTestCase
/** 小程序列表:返回换算后实际价,且成本价不泄漏 */
public function test_mini_list_returns_actual_price_without_cost_price(): void
{
[$store, $product, $user] = $this->makeStoreWithPercentProduct();
$this->actingAsMiniUser($user);
[$store, $product] = $this->makeStoreWithPercentProduct();
$this->actingAsMiniStore($store);
$response = $this->getJson('/mini/product/list');
$response->assertOk()->assertJsonPath('success', true);
@@ -116,7 +115,6 @@ class ProductCostPriceTest extends ProcurementTestCase
$level = CustomerLevelModel::factory()->create(['percent' => 30]);
$store = StoreModel::factory()->create(['level_id' => $level->id]);
$user = UserModel::factory()->forStore($store->id)->create();
$product = ProductModel::factory()->create(['cost_price' => 10]);
$this->putJson('/product/goods/batchPrice', [
@@ -125,15 +123,15 @@ class ProductCostPriceTest extends ProcurementTestCase
],
])->assertOk()->assertJsonPath('success', true);
$this->assertSame('20.00', (string) $product->fresh()->cost_price, '成本价应已更新');
$this->assertSame('20.00', (string) $product->refresh()->cost_price, '成本价应已更新');
$this->assertSame(
1,
NoticeModel::where('user_id', $user->id)->where('type', NoticeModel::TYPE_PRICE)->count(),
'门店用户应收到价格变更通知'
NoticeModel::where('store_id', $store->id)->where('type', NoticeModel::TYPE_PRICE)->count(),
'门店应收到价格变更通知'
);
// 等级售价联动:20 元上浮 30% = 26.00
$this->actingAsMiniUser($user);
$this->actingAsMiniStore($store);
$row = collect($this->getJson('/mini/product/list')->json('data.data'))->firstWhere('id', $product->id);
$this->assertSame('26.00', (string) $row['price'], '成本变更后小程序端售价应按上浮比例联动');
}
@@ -178,8 +176,8 @@ class ProductCostPriceTest extends ProcurementTestCase
/** 小程序下单:按等级上浮换算价重算金额并快照 */
public function test_mini_order_uses_level_price(): void
{
[$store, $product, $user] = $this->makeStoreWithPercentProduct(10, 30);
$this->actingAsMiniUser($user);
[$store, $product] = $this->makeStoreWithPercentProduct(10, 30);
$this->actingAsMiniStore($store);
$this->postJson('/mini/order', [
'items' => [['product_id' => $product->id, 'quantity' => 3]],
@@ -195,8 +193,8 @@ class ProductCostPriceTest extends ProcurementTestCase
/** 小程序购物车:列表金额按等级上浮换算价计算 */
public function test_mini_cart_uses_level_price(): void
{
[$store, $product, $user] = $this->makeStoreWithPercentProduct(10, 30);
$this->actingAsMiniUser($user);
[$store, $product] = $this->makeStoreWithPercentProduct(10, 30);
$this->actingAsMiniStore($store);
$this->postJson('/mini/cart', ['product_id' => $product->id, 'quantity' => 2.5])
->assertOk()->assertJsonPath('success', true);
+20 -51
View File
@@ -6,11 +6,10 @@ use App\Models\CustomerLevelModel;
use App\Models\NoticeModel;
use App\Models\ProductModel;
use App\Models\StoreModel;
use App\Models\UserModel;
/**
* A2 等级价格体系(等级上浮比例):列表按等级换算价、批量调价(成本价)事务与调价通知、
* 等级上浮比例变更通知
* 等级上浮比例变更通知(门店即用户:通知按门店下发,一店一条)
*/
class ProductPriceTest extends ProcurementTestCase
{
@@ -23,13 +22,13 @@ class ProductPriceTest extends ProcurementTestCase
$storeB = StoreModel::factory()->create(['level_id' => $levelB->id]);
$product = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'cost_price' => 10.00]);
$this->actingAsMiniUser(UserModel::factory()->forStore($storeA->id)->create());
$this->actingAsMiniStore($storeA);
$rowA = collect($this->getJson('/mini/product/list')->assertOk()->json('data.data'))
->firstWhere('id', $product->id);
$this->assertNotNull($rowA, '商品应出现在列表中');
$this->assertSame(13.00, (float) $rowA['price'], 'A 等级:10 元上浮 30% = 13.00');
$this->actingAsMiniUser(UserModel::factory()->forStore($storeB->id)->create());
$this->actingAsMiniStore($storeB);
$rowB = collect($this->getJson('/mini/product/list')->assertOk()->json('data.data'))
->firstWhere('id', $product->id);
$this->assertSame(11.00, (float) $rowB['price'], 'B 等级:10 元上浮 10% = 11.00');
@@ -40,7 +39,7 @@ class ProductPriceTest extends ProcurementTestCase
{
$store = StoreModel::factory()->create(['level_id' => 0]);
$product = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON]);
$this->actingAsMiniUser(UserModel::factory()->forStore($store->id)->create());
$this->actingAsMiniStore($store);
$response = $this->getJson('/mini/product/list');
$response->assertOk()->assertJsonPath('success', true);
@@ -50,37 +49,13 @@ class ProductPriceTest extends ProcurementTestCase
$this->assertNull($row['price'], '未设等级时价格应为 null');
}
/** 未绑定门店:可浏览商品与分类,但价格不可见;加购仍被拦截(购买前置校验不变 */
public function test_unbound_user_can_browse_products_without_price_but_cannot_cart(): void
{
$product = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON]);
$this->actingAsMiniUser(UserModel::factory()->create()); // type=0 待绑定
$list = $this->getJson('/mini/product/list');
$list->assertOk()->assertJsonPath('success', true);
$row = collect($list->json('data.data'))->firstWhere('id', $product->id);
$this->assertNotNull($row, '未绑定门店应能看到商品');
$this->assertNull($row['price'], '未绑定门店不得看到价格');
$this->getJson('/mini/product/categories')
->assertOk()
->assertJsonPath('success', true);
$this->postJson('/mini/cart', ['product_id' => $product->id, 'quantity' => 1])
->assertJsonPath('success', false)
->assertJsonPath('msg', '尚未绑定门店,请联系客服处理');
}
/** 批量调价(成本价):事务写入 + 通知全部正常门店用户(成本变更影响所有等级售价) */
/** 批量调价(成本价):事务写入 + 通知全部正常门店(成本变更影响所有等级售价,一店一条 */
public function test_batch_price_updates_cost_and_notifies_all_stores(): void
{
$this->actingAsSysUser();
$storeA = StoreModel::factory()->create(['level_id' => CustomerLevelModel::factory()->create()->id]);
$storeB = StoreModel::factory()->create(['level_id' => CustomerLevelModel::factory()->create()->id]);
$userA = UserModel::factory()->forStore($storeA->id)->create();
$userB = UserModel::factory()->forStore($storeB->id)->create();
$product = ProductModel::factory()->create(['cost_price' => 10]);
@@ -90,17 +65,17 @@ class ProductPriceTest extends ProcurementTestCase
],
])->assertOk()->assertJsonPath('success', true);
$this->assertSame('12.00', (string) $product->fresh()->cost_price, '成本价应已更新');
$this->assertSame('12.00', (string) $product->refresh()->cost_price, '成本价应已更新');
$this->assertSame(
1,
NoticeModel::where('user_id', $userA->id)->where('type', NoticeModel::TYPE_PRICE)->count(),
'成本价变更影响所有等级售价,门店用户应收到价格变更通知'
NoticeModel::where('store_id', $storeA->id)->where('type', NoticeModel::TYPE_PRICE)->count(),
'成本价变更影响所有等级售价,门店应收到价格变更通知'
);
$this->assertSame(
1,
NoticeModel::where('user_id', $userB->id)->where('type', NoticeModel::TYPE_PRICE)->count(),
'成本价变更影响所有等级售价,门店用户应收到价格变更通知'
NoticeModel::where('store_id', $storeB->id)->where('type', NoticeModel::TYPE_PRICE)->count(),
'成本价变更影响所有等级售价,门店应收到价格变更通知'
);
}
@@ -125,35 +100,31 @@ class ProductPriceTest extends ProcurementTestCase
$this->assertSame(12.0, (float) $row['price_' . $level->id], '矩阵等级格应为换算价:10 元上浮 20% = 12.00');
}
/** 等级上浮比例变更:通知该等级下正常门店用户(不影响其他等级门店) */
/** 等级上浮比例变更:通知该等级下正常门店(不影响其他等级门店) */
public function test_level_percent_update_notifies_level_stores(): void
{
$this->actingAsSysUser();
$level = CustomerLevelModel::factory()->create(['percent' => 10]);
$otherLevel = CustomerLevelModel::factory()->create(['percent' => 20]);
$user = UserModel::factory()->forStore(
StoreModel::factory()->create(['level_id' => $level->id])->id
)->create();
$otherUser = UserModel::factory()->forStore(
StoreModel::factory()->create(['level_id' => $otherLevel->id])->id
)->create();
$store = StoreModel::factory()->create(['level_id' => $level->id]);
$otherStore = StoreModel::factory()->create(['level_id' => $otherLevel->id]);
$this->putJson('/customer/level/' . $level->id, [
'name' => $level->name,
'percent' => 30,
])->assertOk()->assertJsonPath('success', true);
$this->assertSame('30.00', (string) $level->fresh()->percent, '上浮比例应已更新');
$this->assertSame('30.00', (string) $level->refresh()->percent, '上浮比例应已更新');
$this->assertSame(
1,
NoticeModel::where('user_id', $user->id)->where('type', NoticeModel::TYPE_PRICE)->count(),
'该等级门店用户应收到价格变更通知'
NoticeModel::where('store_id', $store->id)->where('type', NoticeModel::TYPE_PRICE)->count(),
'该等级门店应收到价格变更通知'
);
$this->assertSame(
0,
NoticeModel::where('user_id', $otherUser->id)->count(),
'其他等级门店用户不应收到通知'
NoticeModel::where('store_id', $otherStore->id)->count(),
'其他等级门店不应收到通知'
);
}
@@ -163,16 +134,14 @@ class ProductPriceTest extends ProcurementTestCase
$this->actingAsSysUser();
$level = CustomerLevelModel::factory()->create(['percent' => 10]);
$user = UserModel::factory()->forStore(
StoreModel::factory()->create(['level_id' => $level->id])->id
)->create();
$store = StoreModel::factory()->create(['level_id' => $level->id]);
$this->putJson('/customer/level/' . $level->id, [
'name' => $level->name . '(改)',
'percent' => 10,
])->assertOk()->assertJsonPath('success', true);
$this->assertSame(0, NoticeModel::where('user_id', $user->id)->count(), '上浮比例未变不应发通知');
$this->assertSame(0, NoticeModel::where('store_id', $store->id)->count(), '上浮比例未变不应发通知');
}
/** 等级表单:上浮比例校验(负数/超限拒绝) */
+4 -6
View File
@@ -9,7 +9,6 @@ use App\Models\StoreModel;
use App\Models\StoreOrderItemModel;
use App\Models\StoreOrderModel;
use App\Models\SupplierModel;
use App\Models\UserModel;
/**
* C4 采购单数据修改:详情矩阵(商品行 × 门店列)、门店单元格下钻编辑、行级修改同步商品档案、
@@ -37,7 +36,7 @@ class PurchaseEditTest extends ProcurementTestCase
]);
foreach ([[$storeA, 2], [$storeB, 3]] as [$store, $qty]) {
$this->actingAsMiniUser(UserModel::factory()->forStore($store->id)->create());
$this->actingAsMiniStore($store);
$this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => $qty]]])
->assertJsonPath('success', true);
}
@@ -298,7 +297,7 @@ class PurchaseEditTest extends ProcurementTestCase
$product = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'cost_price' => 10]);
foreach ([[$storeA, 2], [$storeB, 3]] as [$store, $qty]) {
$this->actingAsMiniUser(UserModel::factory()->forStore($store->id)->create());
$this->actingAsMiniStore($store);
$this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => $qty]]])
->assertJsonPath('success', true);
}
@@ -411,8 +410,7 @@ class PurchaseEditTest extends ProcurementTestCase
$product = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'cost_price' => 10]);
// 同一门店两笔订单各订 2 件(同商品)→ 同一采购单
$user = UserModel::factory()->forStore($store->id)->create();
$this->actingAsMiniUser($user);
$this->actingAsMiniStore($store);
foreach ([2, 2] as $qty) {
$this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => $qty]]])
->assertJsonPath('success', true);
@@ -515,7 +513,7 @@ class PurchaseEditTest extends ProcurementTestCase
'unit' => '斤',
]);
$this->actingAsMiniUser(UserModel::factory()->forStore($store->id)->create());
$this->actingAsMiniStore($store);
foreach ([2, 3] as $qty) {
$this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => $qty]]])
->assertJsonPath('success', true);
+2 -3
View File
@@ -8,7 +8,6 @@ use App\Models\PurchaseOrderModel;
use App\Models\StoreModel;
use App\Models\StoreOrderItemModel;
use App\Models\StoreOrderModel;
use App\Models\UserModel;
/**
* C1 订单汇总生成采购单:订单/明细归集回写、头部汇总、无订单/重复生成防护
@@ -28,7 +27,7 @@ class PurchaseGenerateTest extends ProcurementTestCase
$product = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'cost_price' => '6.00']);
foreach ([[$storeA, 3], [$storeA, 3], [$storeB, 4]] as [$store, $qty]) {
$this->actingAsMiniUser(UserModel::factory()->forStore($store->id)->create());
$this->actingAsMiniStore($store);
$this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => $qty]]])
->assertJsonPath('success', true);
}
@@ -89,7 +88,7 @@ class PurchaseGenerateTest extends ProcurementTestCase
$store = StoreModel::factory()->create(['level_id' => $level->id]);
$product = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'cost_price' => '5.00']);
$this->actingAsMiniUser(UserModel::factory()->forStore($store->id)->create());
$this->actingAsMiniStore($store);
$this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 1]]])
->assertJsonPath('success', true);
+26 -30
View File
@@ -9,19 +9,19 @@ use App\Models\StoreModel;
use App\Models\StoreOrderItemModel;
use App\Models\StoreOrderModel;
use App\Models\SupplierModel;
use App\Models\UserModel;
/**
* 门店订单明细(商品快照):
* 下单快照完整商品档案、成本价防泄漏、采购单内门店单品增删改重算汇总、按商品名称搜索订单
* (门店即用户:小程序端登录主体为 StoreModel
*/
class StoreOrderItemTest extends ProcurementTestCase
{
/**
* 造门店 + 上架商品(指定成本价)+ 门店用户
* 造门店 + 上架商品(指定成本价);
* 等级上浮比例按 price/costPrice 反算(售价 = 成本价 × (100 + percent) / 100
*
* @return array{0: StoreModel, 1: ProductModel, 2: UserModel}
* @return array{0: StoreModel, 1: ProductModel}
*/
private function makeStoreWithProduct(string $price = '5.00', string $costPrice = '4.00'): array
{
@@ -33,17 +33,17 @@ class StoreOrderItemTest extends ProcurementTestCase
'cost_price' => $costPrice,
]);
return [$store, $product, UserModel::factory()->forStore($store->id)->create()];
return [$store, $product];
}
/** 以小程序身份下一单并返回订单(可指定初始状态) */
private function placeOrder(ProductModel $product, UserModel $user, int $quantity = 2, int $status = StoreOrderModel::STATUS_PENDING): StoreOrderModel
private function placeOrder(ProductModel $product, StoreModel $store, int $quantity = 2, int $status = StoreOrderModel::STATUS_PENDING): StoreOrderModel
{
$this->actingAsMiniUser($user);
$this->actingAsMiniStore($store);
$this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => $quantity]]])
->assertOk()->assertJsonPath('success', true);
$order = StoreOrderModel::where('store_id', $user->store_id)->latest('id')->first();
$order = StoreOrderModel::where('store_id', $store->id)->latest('id')->first();
$this->assertNotNull($order);
if ($status !== StoreOrderModel::STATUS_PENDING) {
$order->update(['status' => $status]);
@@ -80,9 +80,8 @@ class StoreOrderItemTest extends ProcurementTestCase
'shelf_life' => 30,
'cost_price' => '4.00',
]);
$user = UserModel::factory()->forStore($store->id)->create();
$this->actingAsMiniUser($user);
$this->actingAsMiniStore($store);
$this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 3]]])
->assertOk()->assertJsonPath('success', true);
@@ -100,10 +99,10 @@ class StoreOrderItemTest extends ProcurementTestCase
/** 小程序订单详情不泄漏成本价 */
public function test_mini_detail_hides_cost_price(): void
{
[, $product, $user] = $this->makeStoreWithProduct('5.00', '4.00');
$order = $this->placeOrder($product, $user);
[$store, $product] = $this->makeStoreWithProduct('5.00', '4.00');
$order = $this->placeOrder($product, $store);
$this->actingAsMiniUser($user);
$this->actingAsMiniStore($store);
$this->getJson("/mini/order/{$order->id}")
->assertOk()
->assertJsonPath('success', true)
@@ -114,9 +113,9 @@ class StoreOrderItemTest extends ProcurementTestCase
public function test_admin_detail_shows_cost_price_and_supplier(): void
{
$supplier = SupplierModel::factory()->create();
[, $product, $user] = $this->makeStoreWithProduct('5.00', '4.00');
[$store, $product] = $this->makeStoreWithProduct('5.00', '4.00');
$product->update(['supplier_id' => $supplier->id]);
$order = $this->placeOrder($product, $user);
$order = $this->placeOrder($product, $store);
$this->actingAsSysUser();
$this->getJson("/order/store/{$order->id}")
@@ -130,8 +129,8 @@ class StoreOrderItemTest extends ProcurementTestCase
/** 采购单内修改门店单品:重算单品金额与订单/采购单汇总 */
public function test_update_store_item_recalculates_totals(): void
{
[$store, $product, $user] = $this->makeStoreWithProduct('5.00');
$order = $this->placeOrder($product, $user, 2, StoreOrderModel::STATUS_SUMMARIZED);
[$store, $product] = $this->makeStoreWithProduct('5.00');
$order = $this->placeOrder($product, $store, 2, StoreOrderModel::STATUS_SUMMARIZED);
$item = $order->items->first();
$purchase = $this->generatePurchase();
@@ -162,10 +161,9 @@ class StoreOrderItemTest extends ProcurementTestCase
/** 采购单已完成:门店单品增删改均拒绝,明细不变 */
public function test_store_item_edits_rejected_when_purchase_completed(): void
{
[, $product, $user] = $this->makeStoreWithProduct('5.00');
$order = $this->placeOrder($product, $user, 1, StoreOrderModel::STATUS_SUMMARIZED);
[$store, $product] = $this->makeStoreWithProduct('5.00');
$order = $this->placeOrder($product, $store, 1, StoreOrderModel::STATUS_SUMMARIZED);
$purchase = $this->generatePurchase();
$store = StoreModel::find($order->store_id);
$purchase->update(['status' => PurchaseOrderModel::STATUS_COMPLETED]);
$this->actingAsSysUser();
@@ -182,15 +180,15 @@ class StoreOrderItemTest extends ProcurementTestCase
->assertJsonPath('success', false);
$item = $order->items->first();
$this->assertSame(1, $item->fresh()->quantity, '被拒绝后明细不变');
$this->assertSame('5.00', (string) $order->fresh()->total_amount, '被拒绝后总金额不变');
$this->assertSame(1, $item->refresh()->quantity, '被拒绝后明细不变');
$this->assertSame('5.00', (string) $order->refresh()->total_amount, '被拒绝后总金额不变');
}
/** 修改门店单品参数校验:负单价/负数量被拦截 */
public function test_update_store_item_validates_negative_values(): void
{
[$store, $product, $user] = $this->makeStoreWithProduct('5.00');
$this->placeOrder($product, $user, 1, StoreOrderModel::STATUS_SUMMARIZED);
[$store, $product] = $this->makeStoreWithProduct('5.00');
$this->placeOrder($product, $store, 1, StoreOrderModel::STATUS_SUMMARIZED);
$purchase = $this->generatePurchase();
$this->actingAsSysUser();
@@ -215,9 +213,8 @@ class StoreOrderItemTest extends ProcurementTestCase
$store = StoreModel::factory()->create(['level_id' => $level->id]);
$product = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'cost_price' => '10.00']);
$extra = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'cost_price' => '20.00']);
$user = UserModel::factory()->forStore($store->id)->create();
$this->placeOrder($product, $user, 2, StoreOrderModel::STATUS_SUMMARIZED);
$this->placeOrder($product, $store, 2, StoreOrderModel::STATUS_SUMMARIZED);
$purchase = $this->generatePurchase();
$this->actingAsSysUser();
@@ -237,8 +234,8 @@ class StoreOrderItemTest extends ProcurementTestCase
/** 新增单品:商品已删除时拒绝 */
public function test_add_store_item_rejected_when_product_deleted(): void
{
[$store, $product, $user] = $this->makeStoreWithProduct('5.00');
$this->placeOrder($product, $user, 1, StoreOrderModel::STATUS_SUMMARIZED);
[$store, $product] = $this->makeStoreWithProduct('5.00');
$this->placeOrder($product, $store, 1, StoreOrderModel::STATUS_SUMMARIZED);
$purchase = $this->generatePurchase();
$extra = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'cost_price' => '9.00']);
@@ -258,12 +255,11 @@ class StoreOrderItemTest extends ProcurementTestCase
{
$level = CustomerLevelModel::factory()->create();
$store = StoreModel::factory()->create(['level_id' => $level->id]);
$user = UserModel::factory()->forStore($store->id)->create();
$cabbage = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'name' => '大白菜A', 'cost_price' => '5.00']);
$potato = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'name' => '土豆B', 'cost_price' => '5.00']);
$this->placeOrder($cabbage, $user);
$this->placeOrder($potato, $user);
$this->placeOrder($cabbage, $store);
$this->placeOrder($potato, $store);
$this->actingAsSysUser();
$response = $this->getJson('/order/store?product_name=' . urlencode('白菜'));
+36 -37
View File
@@ -6,16 +6,16 @@ use App\Models\CustomerLevelModel;
use App\Models\ProductModel;
use App\Models\StoreModel;
use App\Models\StoreOrderModel;
use App\Models\UserModel;
use Modules\SystemTool\Models\SysFileModel;
/**
* 小程序下单:等级上浮价快照、服务端重算总价、取消限制、门店数据隔离
* (门店即用户:小程序端登录主体为 StoreModel
*/
class StoreOrderTest extends ProcurementTestCase
{
/**
* @return array{0: StoreModel, 1: ProductModel, 2: UserModel}
* @return array{0: StoreModel, 1: ProductModel}
*/
private function makeStoreWithProduct(string $price = '5.00'): array
{
@@ -26,14 +26,14 @@ class StoreOrderTest extends ProcurementTestCase
'cost_price' => $price,
]);
return [$store, $product, UserModel::factory()->forStore($store->id)->create()];
return [$store, $product];
}
/** 下单快照等级价,服务端重算单品金额与订单总价 */
public function test_place_order_snapshots_level_price_and_recalculates(): void
{
[$store, $product, $user] = $this->makeStoreWithProduct('5.50');
$this->actingAsMiniUser($user);
[$store, $product] = $this->makeStoreWithProduct('5.50');
$this->actingAsMiniStore($store);
$this->postJson('/mini/order', [
'items' => [['product_id' => $product->id, 'quantity' => 3]],
@@ -54,8 +54,8 @@ class StoreOrderTest extends ProcurementTestCase
/** 前端传入的金额字段一律被忽略 */
public function test_client_supplied_amount_is_ignored(): void
{
[$store, $product, $user] = $this->makeStoreWithProduct('5.00');
$this->actingAsMiniUser($user);
[$store, $product] = $this->makeStoreWithProduct('5.00');
$this->actingAsMiniStore($store);
$this->postJson('/mini/order', [
'items' => [[
@@ -73,9 +73,9 @@ class StoreOrderTest extends ProcurementTestCase
/** 门店绑定的客户等级被删除时拒绝下单 */
public function test_store_level_missing_rejected(): void
{
[$store, $product, $user] = $this->makeStoreWithProduct('5.00');
[$store, $product] = $this->makeStoreWithProduct('5.00');
CustomerLevelModel::whereKey($store->level_id)->delete();
$this->actingAsMiniUser($user);
$this->actingAsMiniStore($store);
$this->postJson('/mini/order', [
'items' => [['product_id' => $product->id, 'quantity' => 1]],
@@ -87,13 +87,13 @@ class StoreOrderTest extends ProcurementTestCase
/** 取消:仅待汇总订单可取消 */
public function test_cancel_only_pending_orders(): void
{
[$store, $product, $user] = $this->makeStoreWithProduct();
$this->actingAsMiniUser($user);
[$store, $product] = $this->makeStoreWithProduct();
$this->actingAsMiniStore($store);
$this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 1]]]);
$order = StoreOrderModel::where('store_id', $store->id)->first();
$this->putJson("/mini/order/{$order->id}/cancel")->assertJsonPath('success', true);
$this->assertSame(StoreOrderModel::STATUS_CANCELLED, $order->fresh()->status);
$this->assertSame(StoreOrderModel::STATUS_CANCELLED, $order->refresh()->status);
// 已取消不可重复取消
$this->putJson("/mini/order/{$order->id}/cancel")->assertJsonPath('success', false);
@@ -102,27 +102,27 @@ class StoreOrderTest extends ProcurementTestCase
/** 已汇总订单不可取消 */
public function test_summarized_order_cannot_be_cancelled(): void
{
[$store, $product, $user] = $this->makeStoreWithProduct();
$this->actingAsMiniUser($user);
[$store, $product] = $this->makeStoreWithProduct();
$this->actingAsMiniStore($store);
$this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 1]]]);
$order = StoreOrderModel::where('store_id', $store->id)->first();
$order->update(['status' => StoreOrderModel::STATUS_SUMMARIZED]);
$this->putJson("/mini/order/{$order->id}/cancel")->assertJsonPath('success', false);
$this->assertSame(StoreOrderModel::STATUS_SUMMARIZED, $order->fresh()->status);
$this->assertSame(StoreOrderModel::STATUS_SUMMARIZED, $order->refresh()->status);
}
/** 门店数据隔离:只能查看本店订单 */
public function test_order_data_isolated_between_stores(): void
{
[$storeA, $product, $userA] = $this->makeStoreWithProduct();
$this->actingAsMiniUser($userA);
[$storeA, $product] = $this->makeStoreWithProduct();
$this->actingAsMiniStore($storeA);
$this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 1]]]);
$orderOfA = StoreOrderModel::where('store_id', $storeA->id)->first();
// 门店 B 用户访问门店 A 的订单
// 门店 B 访问门店 A 的订单
$storeB = StoreModel::factory()->create(['level_id' => $storeA->level_id]);
$this->actingAsMiniUser(UserModel::factory()->forStore($storeB->id)->create());
$this->actingAsMiniStore($storeB);
$this->getJson("/mini/order/{$orderOfA->id}")->assertJsonPath('success', false);
$this->getJson('/mini/order')->assertJsonPath('data.total', 0);
@@ -133,7 +133,7 @@ class StoreOrderTest extends ProcurementTestCase
{
$level = CustomerLevelModel::factory()->create();
$store = StoreModel::factory()->create(['level_id' => $level->id]);
$this->actingAsMiniUser(UserModel::factory()->forStore($store->id)->create());
$this->actingAsMiniStore($store);
$items = [];
foreach (['白菜', '土豆', '番茄', '黄瓜'] as $name) {
@@ -161,8 +161,8 @@ class StoreOrderTest extends ProcurementTestCase
/** 订货日期区间筛选(配合 /order/summary 周期下钻) */
public function test_order_list_filters_by_order_date_range(): void
{
[$store, $product, $user] = $this->makeStoreWithProduct();
$this->actingAsMiniUser($user);
[$store, $product] = $this->makeStoreWithProduct();
$this->actingAsMiniStore($store);
foreach ([1, 1, 1] as $qty) {
$this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => $qty]]])
@@ -181,8 +181,8 @@ class StoreOrderTest extends ProcurementTestCase
/** 非法筛选参数被拦截(状态枚举 / 分页上限 / 日期区间倒置) */
public function test_order_list_rejects_invalid_filters(): void
{
[, , $user] = $this->makeStoreWithProduct();
$this->actingAsMiniUser($user);
[$store] = $this->makeStoreWithProduct();
$this->actingAsMiniStore($store);
$this->getJson('/mini/order?status=7')->assertJsonPath('success', false);
$this->getJson('/mini/order?pageSize=100')->assertJsonPath('success', false);
@@ -192,8 +192,8 @@ class StoreOrderTest extends ProcurementTestCase
/** 造一笔指定状态的订单(商品 5.00 × 2 = 10.00 */
private function makeOrderWithStatus(int $status): StoreOrderModel
{
[$store, $product, $user] = $this->makeStoreWithProduct('5.00');
$this->actingAsMiniUser($user);
[$store, $product] = $this->makeStoreWithProduct('5.00');
$this->actingAsMiniStore($store);
$this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 2]]])
->assertJsonPath('success', true);
$order = StoreOrderModel::where('store_id', $store->id)->first();
@@ -213,7 +213,7 @@ class StoreOrderTest extends ProcurementTestCase
$this->actingAsSysUser();
$this->deleteJson("/order/store/{$order->id}")->assertOk()->assertJsonPath('success', true);
$this->assertNotNull($order->fresh()->deleted_at, '软删除应写入 deleted_at');
$this->assertNotNull($order->refresh()->deleted_at, '软删除应写入 deleted_at');
$this->assertNull(StoreOrderModel::find($order->id), '默认查询不可见软删除订单');
$this->assertNotNull(StoreOrderModel::withTrashed()->find($order->id), '数据仍保留在库中');
@@ -222,7 +222,7 @@ class StoreOrderTest extends ProcurementTestCase
$this->getJson("/order/store/{$order->id}")->assertJsonPath('success', false);
// 小程序端历史订单同样不可见
$this->actingAsMiniUser(UserModel::where('store_id', $order->store_id)->first());
$this->actingAsMiniStore(StoreModel::find($order->store_id));
$this->getJson('/mini/order')->assertOk()->assertJsonPath('data.total', 0);
$this->getJson("/mini/order/{$order->id}")->assertJsonPath('success', false);
}
@@ -241,7 +241,7 @@ class StoreOrderTest extends ProcurementTestCase
$this->actingAsSysUser();
$this->deleteJson("/order/store/{$order->id}")
->assertOk()->assertJsonPath('success', false);
$this->assertNull($order->fresh()->deleted_at, '非已取消订单不得删除');
$this->assertNull($order->refresh()->deleted_at, '非已取消订单不得删除');
}
}
@@ -263,13 +263,13 @@ class StoreOrderTest extends ProcurementTestCase
$this->putJson("/order/store/{$order->id}/status", ['status' => StoreOrderModel::STATUS_SUMMARIZED])
->assertJsonPath('success', true);
$this->assertSame(StoreOrderModel::STATUS_SUMMARIZED, $order->fresh()->status);
$this->assertSame(StoreOrderModel::STATUS_SUMMARIZED, $order->refresh()->status);
$order2 = $this->makeOrderWithStatus(StoreOrderModel::STATUS_PENDING);
$this->actingAsSysUser();
$this->putJson("/order/store/{$order2->id}/status", ['status' => StoreOrderModel::STATUS_CANCELLED])
->assertJsonPath('success', true);
$this->assertSame(StoreOrderModel::STATUS_CANCELLED, $order2->fresh()->status);
$this->assertSame(StoreOrderModel::STATUS_CANCELLED, $order2->refresh()->status);
}
/** 手动流转:配送中/已完成等目标状态被拒绝(由采购/账单链路自动推进) */
@@ -280,7 +280,7 @@ class StoreOrderTest extends ProcurementTestCase
$this->actingAsSysUser();
$this->putJson("/order/store/{$order->id}/status", ['status' => $target])
->assertJsonPath('success', false);
$this->assertSame(StoreOrderModel::STATUS_DELIVERING, $order->fresh()->status);
$this->assertSame(StoreOrderModel::STATUS_DELIVERING, $order->refresh()->status);
}
// 已接单不能手动转采购中(须经生成采购单)
@@ -302,8 +302,8 @@ class StoreOrderTest extends ProcurementTestCase
'ids' => [$pending1->id, $pending2->id],
'status' => StoreOrderModel::STATUS_SUMMARIZED,
])->assertJsonPath('success', true);
$this->assertSame(StoreOrderModel::STATUS_SUMMARIZED, $pending1->fresh()->status);
$this->assertSame(StoreOrderModel::STATUS_SUMMARIZED, $pending2->fresh()->status);
$this->assertSame(StoreOrderModel::STATUS_SUMMARIZED, $pending1->refresh()->status);
$this->assertSame(StoreOrderModel::STATUS_SUMMARIZED, $pending2->refresh()->status);
// 批量完成被拒绝(目标状态校验拦截)
$this->putJson('/order/store/batchStatus', [
@@ -316,7 +316,7 @@ class StoreOrderTest extends ProcurementTestCase
'ids' => [$pending1->id],
'status' => StoreOrderModel::STATUS_CANCELLED,
])->assertJsonPath('success', false);
$this->assertSame(StoreOrderModel::STATUS_SUMMARIZED, $pending1->fresh()->status);
$this->assertSame(StoreOrderModel::STATUS_SUMMARIZED, $pending1->refresh()->status);
}
/** 订单列表商品预览:附带首图/规格/单位供小程序端展示 */
@@ -343,9 +343,8 @@ class StoreOrderTest extends ProcurementTestCase
'image_ids' => (string) $file->id,
'cost_price' => '5.00',
]);
$user = UserModel::factory()->forStore($store->id)->create();
$this->actingAsMiniUser($user);
$this->actingAsMiniStore($store);
$this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 2]]])
->assertJsonPath('success', true);