From 07b08c89158e9fb2047b035d05a991ddc4789d06 Mon Sep 17 00:00:00 2001 From: liu <2302563948@qq.com> Date: Thu, 6 Aug 2026 10:49:51 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B4=AD=E7=89=A9=E8=BD=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .phpunit.result.cache | 2 +- app/Http/Controllers/Mini/CartController.php | 234 +++++++++++++ app/Http/Requests/Mini/MiniCartRequest.php | 39 +++ app/Models/CartModel.php | 46 +++ app/Services/WechatService.php | 4 +- database/factories/CartModelFactory.php | 25 ++ .../factories/CustomerLevelModelFactory.php | 29 ++ database/factories/ProductModelFactory.php | 50 +++ .../factories/ProductPriceModelFactory.php | 29 ++ .../factories/PurchaseOrderModelFactory.php | 43 +++ database/factories/StoreModelFactory.php | 51 +++ .../factories/StoreOrderItemModelFactory.php | 42 +++ database/factories/StoreOrderModelFactory.php | 50 +++ database/factories/SupplierModelFactory.php | 43 +++ database/factories/UserModelFactory.php | 69 ++++ .../2026_08_06_000001_create_cart_table.php | 36 ++ tests/Feature/CartTest.php | 331 ++++++++++++++++++ tests/Feature/MiniAuthTest.php | 27 +- 18 files changed, 1145 insertions(+), 5 deletions(-) create mode 100644 app/Http/Controllers/Mini/CartController.php create mode 100644 app/Http/Requests/Mini/MiniCartRequest.php create mode 100644 app/Models/CartModel.php create mode 100644 database/factories/CartModelFactory.php create mode 100644 database/factories/CustomerLevelModelFactory.php create mode 100644 database/factories/ProductModelFactory.php create mode 100644 database/factories/ProductPriceModelFactory.php create mode 100644 database/factories/PurchaseOrderModelFactory.php create mode 100644 database/factories/StoreModelFactory.php create mode 100644 database/factories/StoreOrderItemModelFactory.php create mode 100644 database/factories/StoreOrderModelFactory.php create mode 100644 database/factories/SupplierModelFactory.php create mode 100644 database/factories/UserModelFactory.php create mode 100644 database/migrations/2026_08_06_000001_create_cart_table.php create mode 100644 tests/Feature/CartTest.php diff --git a/.phpunit.result.cache b/.phpunit.result.cache index 5f94a10..e8892ad 100644 --- a/.phpunit.result.cache +++ b/.phpunit.result.cache @@ -1 +1 @@ -{"version":2,"defects":{"Tests\\Feature\\ProductPriceTest::test_product_list_returns_price_for_store_level":8,"Tests\\Feature\\ProductPriceTest::test_store_without_level_is_rejected":8,"Tests\\Feature\\ProductPriceTest::test_batch_price_updates_and_notifies_affected_stores":8,"Tests\\Feature\\ProductPriceTest::test_price_matrix_structure":8,"Tests\\Feature\\StoreOrderTest::test_place_order_snapshots_level_price_and_recalculates":8,"Tests\\Feature\\StoreOrderTest::test_client_supplied_amount_is_ignored":8,"Tests\\Feature\\StoreOrderTest::test_product_without_level_price_rejected":8,"Tests\\Feature\\StoreOrderTest::test_cancel_only_pending_orders":8,"Tests\\Feature\\StoreOrderTest::test_summarized_order_cannot_be_cancelled":8,"Tests\\Feature\\StoreOrderTest::test_order_data_isolated_between_stores":7,"Tests\\Feature\\PurchaseGenerateTest::test_generate_aggregates_orders_by_product":7,"Tests\\Feature\\PurchaseGenerateTest::test_generate_writes_back_order_status":7,"Tests\\Feature\\PurchaseGenerateTest::test_generate_without_pending_orders_fails":8,"Tests\\Feature\\PurchaseGenerateTest::test_generate_is_idempotent":7,"Tests\\Feature\\AllocationTest::test_allocation_conserves_amount_with_tail_correction":7,"Tests\\Feature\\AllocationTest::test_allocation_follows_order_ratio":7,"Tests\\Feature\\AllocationTest::test_allocation_is_idempotent":7,"Tests\\Feature\\AllocationTest::test_allocation_rejected_without_actual_amount":8,"Tests\\Feature\\MiniAuthTest::test_login_creates_user_and_issues_token":7,"Tests\\Feature\\MiniAuthTest::test_phone_binding_matches_store":7,"Tests\\Feature\\MiniAuthTest::test_phone_no_match_stays_pending":7,"Tests\\Feature\\ExportTest::test_export_xlsx_with_encoded_chinese_filename":7},"times":{"Tests\\Feature\\ExampleTest::test_the_application_returns_a_successful_response":0.002,"Tests\\Feature\\ProductPriceTest::test_product_list_returns_price_for_store_level":0.021,"Tests\\Feature\\ProductPriceTest::test_store_without_level_is_rejected":0.001,"Tests\\Feature\\ProductPriceTest::test_batch_price_updates_and_notifies_affected_stores":0.008,"Tests\\Feature\\ProductPriceTest::test_price_matrix_structure":0.002,"Tests\\Feature\\StoreOrderTest::test_place_order_snapshots_level_price_and_recalculates":0.008,"Tests\\Feature\\StoreOrderTest::test_client_supplied_amount_is_ignored":0.009,"Tests\\Feature\\StoreOrderTest::test_product_without_level_price_rejected":0.009,"Tests\\Feature\\StoreOrderTest::test_cancel_only_pending_orders":0.013,"Tests\\Feature\\StoreOrderTest::test_summarized_order_cannot_be_cancelled":0.01,"Tests\\Feature\\StoreOrderTest::test_order_data_isolated_between_stores":0.011,"Tests\\Feature\\PurchaseGenerateTest::test_generate_aggregates_orders_by_product":0.03,"Tests\\Feature\\PurchaseGenerateTest::test_generate_writes_back_order_status":0.019,"Tests\\Feature\\PurchaseGenerateTest::test_generate_without_pending_orders_fails":0.006,"Tests\\Feature\\PurchaseGenerateTest::test_generate_is_idempotent":0.024,"Tests\\Feature\\AllocationTest::test_allocation_conserves_amount_with_tail_correction":0.071,"Tests\\Feature\\AllocationTest::test_allocation_follows_order_ratio":0.02,"Tests\\Feature\\AllocationTest::test_allocation_is_idempotent":0.023,"Tests\\Feature\\AllocationTest::test_allocation_rejected_without_actual_amount":0.016,"Tests\\Feature\\DebugGenerateTest::test_debug_generate":0.047,"Tests\\Feature\\ReconciliationTest::test_build_creates_reconciliation_items":0.027,"Tests\\Feature\\ReconciliationTest::test_build_filters_by_supplier":0.032,"Tests\\Feature\\ReconciliationTest::test_update_item_recalculates_diff_and_header":0.028,"Tests\\Feature\\ReconciliationTest::test_toggle_reconciled_flag":0.027,"Tests\\Feature\\ReconciliationTest::test_settle_creates_settlements_per_store":0.03,"Tests\\Feature\\StatementTest::test_generate_snapshots_payment_cycle":0.011,"Tests\\Feature\\StatementTest::test_statement_isolated_between_stores":0.013,"Tests\\Feature\\StatementTest::test_generate_excludes_cancelled_and_used_items":0.015,"Tests\\Feature\\MiniAuthTest::test_login_creates_user_and_issues_token":0.008,"Tests\\Feature\\MiniAuthTest::test_disabled_user_cannot_login":0.013,"Tests\\Feature\\MiniAuthTest::test_phone_binding_matches_store":0.012,"Tests\\Feature\\MiniAuthTest::test_phone_no_match_stays_pending":0.006,"Tests\\Feature\\MiniAuthTest::test_sys_token_cannot_access_mini":0.005,"Tests\\Feature\\MiniAuthTest::test_mini_token_cannot_access_admin_api":0.005,"Tests\\Feature\\ExportTest::test_export_xlsx_with_encoded_chinese_filename":0.059,"Tests\\Feature\\ExportTest::test_export_category_filters_to_vegetables":0.017,"Tests\\Feature\\ExportTest::test_export_pdf":1.324,"Tests\\Feature\\ExportTest::test_export_invalid_format_rejected":0.016,"Tests\\Feature\\ExportTest::test_export_requires_export_permission":0.016,"Tests\\Unit\\ExampleTest::test_that_true_is_true":0.001,"Tests\\Feature\\MiniAuthTest::test_login_fails_when_wechat_rejects_code":0.004}} \ No newline at end of file +{"version":2,"defects":{"Tests\\Feature\\ProductPriceTest::test_product_list_returns_price_for_store_level":8,"Tests\\Feature\\ProductPriceTest::test_store_without_level_is_rejected":8,"Tests\\Feature\\ProductPriceTest::test_batch_price_updates_and_notifies_affected_stores":8,"Tests\\Feature\\ProductPriceTest::test_price_matrix_structure":8,"Tests\\Feature\\StoreOrderTest::test_place_order_snapshots_level_price_and_recalculates":8,"Tests\\Feature\\StoreOrderTest::test_client_supplied_amount_is_ignored":8,"Tests\\Feature\\StoreOrderTest::test_product_without_level_price_rejected":8,"Tests\\Feature\\StoreOrderTest::test_cancel_only_pending_orders":8,"Tests\\Feature\\StoreOrderTest::test_summarized_order_cannot_be_cancelled":8,"Tests\\Feature\\StoreOrderTest::test_order_data_isolated_between_stores":8,"Tests\\Feature\\PurchaseGenerateTest::test_generate_aggregates_orders_by_product":7,"Tests\\Feature\\PurchaseGenerateTest::test_generate_writes_back_order_status":7,"Tests\\Feature\\PurchaseGenerateTest::test_generate_without_pending_orders_fails":8,"Tests\\Feature\\PurchaseGenerateTest::test_generate_is_idempotent":7,"Tests\\Feature\\AllocationTest::test_allocation_conserves_amount_with_tail_correction":7,"Tests\\Feature\\AllocationTest::test_allocation_follows_order_ratio":7,"Tests\\Feature\\AllocationTest::test_allocation_is_idempotent":7,"Tests\\Feature\\AllocationTest::test_allocation_rejected_without_actual_amount":8,"Tests\\Feature\\MiniAuthTest::test_login_creates_user_and_issues_token":7,"Tests\\Feature\\MiniAuthTest::test_phone_binding_matches_store":7,"Tests\\Feature\\MiniAuthTest::test_phone_no_match_stays_pending":7,"Tests\\Feature\\ExportTest::test_export_xlsx_with_encoded_chinese_filename":7,"Tests\\Feature\\CartTest::test_list_with_prices_amounts_and_totals":7,"Tests\\Feature\\CartTest::test_list_marks_off_shelf_item_unbuyable":7,"Tests\\Feature\\DebugCartListTest::test_dump_cart_list_response":5},"times":{"Tests\\Feature\\ExampleTest::test_the_application_returns_a_successful_response":0.003,"Tests\\Feature\\ProductPriceTest::test_product_list_returns_price_for_store_level":0.007,"Tests\\Feature\\ProductPriceTest::test_store_without_level_is_rejected":0.005,"Tests\\Feature\\ProductPriceTest::test_batch_price_updates_and_notifies_affected_stores":0.009,"Tests\\Feature\\ProductPriceTest::test_price_matrix_structure":0.007,"Tests\\Feature\\StoreOrderTest::test_place_order_snapshots_level_price_and_recalculates":0.008,"Tests\\Feature\\StoreOrderTest::test_client_supplied_amount_is_ignored":0.007,"Tests\\Feature\\StoreOrderTest::test_product_without_level_price_rejected":0.006,"Tests\\Feature\\StoreOrderTest::test_cancel_only_pending_orders":0.012,"Tests\\Feature\\StoreOrderTest::test_summarized_order_cannot_be_cancelled":0.009,"Tests\\Feature\\StoreOrderTest::test_order_data_isolated_between_stores":0.011,"Tests\\Feature\\PurchaseGenerateTest::test_generate_aggregates_orders_by_product":0.019,"Tests\\Feature\\PurchaseGenerateTest::test_generate_writes_back_order_status":0.019,"Tests\\Feature\\PurchaseGenerateTest::test_generate_without_pending_orders_fails":0.008,"Tests\\Feature\\PurchaseGenerateTest::test_generate_is_idempotent":0.02,"Tests\\Feature\\AllocationTest::test_allocation_conserves_amount_with_tail_correction":0.073,"Tests\\Feature\\AllocationTest::test_allocation_follows_order_ratio":0.02,"Tests\\Feature\\AllocationTest::test_allocation_is_idempotent":0.023,"Tests\\Feature\\AllocationTest::test_allocation_rejected_without_actual_amount":0.016,"Tests\\Feature\\DebugGenerateTest::test_debug_generate":0.047,"Tests\\Feature\\ReconciliationTest::test_build_creates_reconciliation_items":0.028,"Tests\\Feature\\ReconciliationTest::test_build_filters_by_supplier":0.029,"Tests\\Feature\\ReconciliationTest::test_update_item_recalculates_diff_and_header":0.028,"Tests\\Feature\\ReconciliationTest::test_toggle_reconciled_flag":0.034,"Tests\\Feature\\ReconciliationTest::test_settle_creates_settlements_per_store":0.037,"Tests\\Feature\\StatementTest::test_generate_snapshots_payment_cycle":0.013,"Tests\\Feature\\StatementTest::test_statement_isolated_between_stores":0.021,"Tests\\Feature\\StatementTest::test_generate_excludes_cancelled_and_used_items":0.021,"Tests\\Feature\\MiniAuthTest::test_login_creates_user_and_issues_token":0.008,"Tests\\Feature\\MiniAuthTest::test_disabled_user_cannot_login":0.003,"Tests\\Feature\\MiniAuthTest::test_phone_binding_matches_store":0.011,"Tests\\Feature\\MiniAuthTest::test_phone_no_match_stays_pending":0.005,"Tests\\Feature\\MiniAuthTest::test_sys_token_cannot_access_mini":0.005,"Tests\\Feature\\MiniAuthTest::test_mini_token_cannot_access_admin_api":0.003,"Tests\\Feature\\ExportTest::test_export_xlsx_with_encoded_chinese_filename":0.085,"Tests\\Feature\\ExportTest::test_export_category_filters_to_vegetables":0.019,"Tests\\Feature\\ExportTest::test_export_pdf":1.275,"Tests\\Feature\\ExportTest::test_export_invalid_format_rejected":0.022,"Tests\\Feature\\ExportTest::test_export_requires_export_permission":0.016,"Tests\\Unit\\ExampleTest::test_that_true_is_true":0.001,"Tests\\Feature\\MiniAuthTest::test_login_fails_when_wechat_rejects_code":0.003,"Tests\\Feature\\CartTest::test_add_to_cart_succeeds":0.007,"Tests\\Feature\\CartTest::test_duplicate_add_merges_quantity":0.008,"Tests\\Feature\\CartTest::test_off_shelf_product_rejected":0.007,"Tests\\Feature\\CartTest::test_soft_deleted_product_rejected":0.007,"Tests\\Feature\\CartTest::test_product_without_level_price_rejected":0.007,"Tests\\Feature\\CartTest::test_unbound_user_rejected":0.007,"Tests\\Feature\\CartTest::test_store_without_level_rejected":0.006,"Tests\\Feature\\CartTest::test_list_with_prices_amounts_and_totals":0.011,"Tests\\Feature\\CartTest::test_list_marks_off_shelf_item_unbuyable":0.01,"Tests\\Feature\\CartTest::test_empty_cart_returns_empty_items":0.006,"Tests\\Feature\\CartTest::test_update_quantity":0.009,"Tests\\Feature\\CartTest::test_update_other_users_item_rejected":0.01,"Tests\\Feature\\CartTest::test_update_missing_item_rejected":0.008,"Tests\\Feature\\CartTest::test_invalid_quantity_rejected":0.006,"Tests\\Feature\\CartTest::test_delete_item":0.008,"Tests\\Feature\\CartTest::test_delete_other_users_item_rejected":0.009,"Tests\\Feature\\CartTest::test_clear_only_own_cart":0.024,"Tests\\Feature\\CartTest::test_unauthenticated_returns_401":0.002,"Tests\\Feature\\CartTest::test_cart_isolated_between_users":0.01,"Tests\\Feature\\DebugCartListTest::test_dump_cart_list_response":0.057}} \ No newline at end of file diff --git a/app/Http/Controllers/Mini/CartController.php b/app/Http/Controllers/Mini/CartController.php new file mode 100644 index 0000000..ce024ba --- /dev/null +++ b/app/Http/Controllers/Mini/CartController.php @@ -0,0 +1,234 @@ +currentUser($request); + $store = $this->ensureStoreBound($user); + if ($store->level_id <= 0) { + throw new RepositoryException('门店未设置客户等级,无法加购,请联系客服'); + } + + $productId = (int) $request->validated('product_id'); + // Eloquent 查询自带 SoftDeletes 全局作用域:软删除/下架一并在内 + $product = ProductModel::where('status', ProductModel::STATUS_ON)->find($productId); + if ($product === null) { + throw new RepositoryException('商品不存在或已下架,请刷新后重试'); + } + $price = ProductPriceModel::query() + ->where('product_id', $productId) + ->where('level_id', $store->level_id) + ->value('price'); + if ($price === null) { + throw new RepositoryException('商品「' . $product->name . '」未设置您所在等级的价格,无法加购'); + } + + $quantity = (string) $request->validated('quantity'); + + $cart = DB::transaction(function () use ($user, $productId, $quantity) { + $row = CartModel::query() + ->where('user_id', $user->id) + ->where('product_id', $productId) + ->lockForUpdate() + ->first(); + + if ($row !== null) { + $merged = bcadd((string) $row->quantity, $quantity, 2); + if (bccomp($merged, self::MAX_QUANTITY, 2) > 0) { + throw new RepositoryException('该商品在购物车中的数量已达上限'); + } + $row->quantity = $merged; + $row->save(); + + return $row; + } + + return CartModel::create([ + 'user_id' => $user->id, + 'product_id' => $productId, + 'quantity' => $quantity, + ]); + }); + + return $this->success([ + 'id' => $cart->id, + 'quantity' => $cart->quantity, + ], '已加入购物车'); + } + + /** + * 购物车列表:当前用户全部项 + 实时等级价,逐项服务端 bcmul 算金额; + * status=1 可购 / 0 商品下架、缺失或未设等级价;汇总只统计可购项 + */ + #[GetRoute('/cart', authorize: true)] + public function index(Request $request): JsonResponse + { + $user = $this->currentUser($request); + $store = $this->ensureStoreBound($user); + + $rows = CartModel::query() + ->where('user_id', $user->id) + ->orderBy('id', 'desc') + ->get(); + + if ($rows->isEmpty()) { + return $this->success([ + 'items' => [], + 'total_count' => 0, + 'total_quantity' => '0.00', + 'total_amount' => '0.00', + ]); + } + + $productIds = $rows->pluck('product_id') + ->map(static fn ($id) => (int) $id) + ->unique()->values()->all(); + + $products = ProductModel::withTrashed()->whereIn('id', $productIds)->get()->keyBy('id'); + $prices = ProductPriceModel::query() + ->where('level_id', $store->level_id) + ->whereIn('product_id', $productIds) + ->pluck('price', 'product_id'); + + // 图片一次查回(避免 ProductModel::$appends images_arr 的 N+1)。 + // 注意:image_ids 有 imageIds Attribute 访问器(get 返回数组),需取原始值 + $allFileIds = []; + foreach ($products as $product) { + foreach (explode(',', (string) $product->getRawOriginal('image_ids')) as $fileId) { + if ($fileId !== '') { + $allFileIds[] = (int) $fileId; + } + } + } + $fileMap = SysFileModel::query() + ->whereIn('id', $allFileIds) + ->get()->keyBy('id'); + + $items = []; + $totalQuantity = '0.00'; + $totalAmount = '0.00'; + foreach ($rows as $row) { + $product = $products->get($row->product_id); + $productOn = $product !== null && $product->status === ProductModel::STATUS_ON; + $price = $prices->get($row->product_id); // string|null + $buyable = $productOn && $price !== null; + $quantity = (string) $row->quantity; + + $firstFileId = (int) (explode(',', (string) $product->getRawOriginal('image_ids'))[0] ?? 0); + $firstFile = $firstFileId > 0 ? $fileMap->get($firstFileId) : null; + + $item = [ + 'id' => $row->id, + 'product_id' => $row->product_id, + 'name' => $product->name ?? '', + 'spec' => $product->spec ?? '', + 'unit' => $product->unit ?? '', + 'image' => $firstFile?->file_url ?? '', + 'price' => $price, + 'quantity' => $quantity, + 'amount' => $buyable ? bcmul($price, $quantity, 2) : null, + 'status' => $buyable ? 1 : 0, + ]; + $items[] = $item; + + if ($buyable) { + $totalQuantity = bcadd($totalQuantity, $quantity, 2); + $totalAmount = bcadd($totalAmount, $item['amount'], 2); + } + } + + return $this->success([ + 'items' => $items, + 'total_count' => count($items), + 'total_quantity' => $totalQuantity, + 'total_amount' => $totalAmount, + ]); + } + + /** + * 修改数量(校验归属) + */ + #[PutRoute('/cart/{id}', authorize: true, where: ['id' => '[0-9]+'])] + public function update(int $id, MiniCartRequest $request): JsonResponse + { + $user = $this->currentUser($request); + $this->ensureStoreBound($user); + + $row = CartModel::query() + ->where('id', $id) + ->where('user_id', $user->id) + ->first(); + if ($row === null) { + throw new RepositoryException('购物车项不存在'); + } + + $row->quantity = (string) $request->validated('quantity'); + $row->save(); + + return $this->success(['id' => $row->id, 'quantity' => $row->quantity], '已修改数量'); + } + + /** + * 删除单项(校验归属) + */ + #[DeleteRoute('/cart/{id}', authorize: true, where: ['id' => '[0-9]+'])] + public function destroy(int $id, Request $request): JsonResponse + { + $user = $this->currentUser($request); + $this->ensureStoreBound($user); + + $deleted = CartModel::query() + ->where('id', $id) + ->where('user_id', $user->id) + ->delete(); + if ($deleted === 0) { + throw new RepositoryException('购物车项不存在'); + } + + return $this->success([], '已删除'); + } + + /** + * 清空购物车(仅当前用户) + */ + #[DeleteRoute('/cart', authorize: true)] + public function clear(Request $request): JsonResponse + { + $user = $this->currentUser($request); + $this->ensureStoreBound($user); + + CartModel::query()->where('user_id', $user->id)->delete(); + + return $this->success([], '购物车已清空'); + } +} diff --git a/app/Http/Requests/Mini/MiniCartRequest.php b/app/Http/Requests/Mini/MiniCartRequest.php new file mode 100644 index 0000000..6b8a3db --- /dev/null +++ b/app/Http/Requests/Mini/MiniCartRequest.php @@ -0,0 +1,39 @@ + 'required|numeric|min:0.01|max:99999999.99', + ]; + + if (! $this->isUpdate()) { + $rules['product_id'] = 'required|integer|exists:product,id'; + } + + return $rules; + } + + public function messages(): array + { + return [ + 'product_id.required' => '请选择商品', + 'product_id.integer' => '商品参数错误', + 'product_id.exists' => '商品不存在', + 'quantity.required' => '订货数量不能为空', + 'quantity.numeric' => '订货数量必须为数字', + 'quantity.min' => '订货数量必须大于 0', + 'quantity.max' => '订货数量超出上限', + ]; + } +} diff --git a/app/Models/CartModel.php b/app/Models/CartModel.php new file mode 100644 index 0000000..f106061 --- /dev/null +++ b/app/Models/CartModel.php @@ -0,0 +1,46 @@ + 'integer', + 'product_id' => 'integer', + 'quantity' => 'decimal:2', + ]; + + /** + * 归属用户 + */ + public function user(): BelongsTo + { + return $this->belongsTo(UserModel::class, 'user_id', 'id'); + } + + /** + * 购物车商品(软删除后为 null;列表接口需 withTrashed 自行判断状态) + */ + public function product(): BelongsTo + { + return $this->belongsTo(ProductModel::class, 'product_id', 'id'); + } +} diff --git a/app/Services/WechatService.php b/app/Services/WechatService.php index 8c09478..541f72d 100644 --- a/app/Services/WechatService.php +++ b/app/Services/WechatService.php @@ -11,8 +11,8 @@ use Symfony\Contracts\HttpClient\HttpClientInterface; /** * 微信小程序服务(基于 EasyWeChat 6.x) * - * 封装 code2Session / 手机号解密;配置读取 config('services.wechat.mini') - * (env:WECHAT_MINI_APPID / WECHAT_MINI_SECRET,需业务方提供)。 + * 封装 code2Session / 手机号解密;配置读取 site_config('wechatMini') + * (后台「小程序设置」面板维护,存 sys_site_config 表)。 * * 测试策略:通过 setHttpClient() 注入 Symfony MockHttpClient 拦截微信 HTTP 调用。 */ diff --git a/database/factories/CartModelFactory.php b/database/factories/CartModelFactory.php new file mode 100644 index 0000000..7766b2c --- /dev/null +++ b/database/factories/CartModelFactory.php @@ -0,0 +1,25 @@ + + */ +class CartModelFactory extends Factory +{ + protected $model = CartModel::class; + + public function definition(): array + { + return [ + 'user_id' => 0, + 'product_id' => 0, + 'quantity' => number_format(random_int(100, 10000) / 100, 2, '.', ''), + ]; + } +} diff --git a/database/factories/CustomerLevelModelFactory.php b/database/factories/CustomerLevelModelFactory.php new file mode 100644 index 0000000..c6a6cda --- /dev/null +++ b/database/factories/CustomerLevelModelFactory.php @@ -0,0 +1,29 @@ + + */ +class CustomerLevelModelFactory extends Factory +{ + protected $model = CustomerLevelModel::class; + + private static int $sequence = 0; + + public function definition(): array + { + $seq = ++self::$sequence; + + return [ + 'name' => '客户等级' . $seq, + 'sort' => $seq, + 'status' => CustomerLevelModel::STATUS_NORMAL, + ]; + } +} diff --git a/database/factories/ProductModelFactory.php b/database/factories/ProductModelFactory.php new file mode 100644 index 0000000..9197e81 --- /dev/null +++ b/database/factories/ProductModelFactory.php @@ -0,0 +1,50 @@ + + */ +class ProductModelFactory extends Factory +{ + protected $model = ProductModel::class; + + private const NAMES = ['大白菜', '土豆', '西红柿', '黄瓜', '苹果', '香蕉']; + private const SPECS = ['500g/袋', '10斤/箱', '散装', '25斤/袋']; + private const UNITS = ['斤', '箱', '袋']; + + private static int $sequence = 0; + + public function definition(): array + { + $seq = ++self::$sequence; + + return [ + 'category_id' => 0, + 'supplier_id' => 0, + 'name' => self::NAMES[$seq % count(self::NAMES)] . $seq, + 'spec' => self::SPECS[$seq % count(self::SPECS)], + 'unit' => self::UNITS[$seq % count(self::UNITS)], + 'image_ids' => '', + 'content' => '', + 'sort' => $seq, + 'shelf_life' => 0, + 'stock' => 0, + 'status' => ProductModel::STATUS_ON, + 'remark' => '', + ]; + } + + /** + * 下架商品 + */ + public function off(): static + { + return $this->state(fn () => ['status' => ProductModel::STATUS_OFF]); + } +} diff --git a/database/factories/ProductPriceModelFactory.php b/database/factories/ProductPriceModelFactory.php new file mode 100644 index 0000000..a6f4fcf --- /dev/null +++ b/database/factories/ProductPriceModelFactory.php @@ -0,0 +1,29 @@ + + */ +class ProductPriceModelFactory extends Factory +{ + protected $model = ProductPriceModel::class; + + private static int $sequence = 0; + + public function definition(): array + { + $seq = ++self::$sequence; + + return [ + 'product_id' => 0, + 'level_id' => 0, + 'price' => number_format(random_int(100, 10000) / 100 + $seq * 0.01, 2, '.', ''), + ]; + } +} diff --git a/database/factories/PurchaseOrderModelFactory.php b/database/factories/PurchaseOrderModelFactory.php new file mode 100644 index 0000000..b2a4899 --- /dev/null +++ b/database/factories/PurchaseOrderModelFactory.php @@ -0,0 +1,43 @@ + + */ +class PurchaseOrderModelFactory extends Factory +{ + protected $model = PurchaseOrderModel::class; + + private static int $sequence = 0; + + public function definition(): array + { + $seq = ++self::$sequence; + + return [ + 'purchase_no' => 'PO' . str_pad((string) $seq, 12, '0', STR_PAD_LEFT), + 'purchase_date' => now()->toDateString(), + 'status' => PurchaseOrderModel::STATUS_PENDING, + 'total_quantity' => 0, + 'total_weight' => 0, + 'estimate_amount' => 0, + 'actual_amount' => 0, + 'operator_id' => 0, + 'remark' => '', + ]; + } + + /** + * 指定采购日期 + */ + public function onDate(string $date): static + { + return $this->state(fn () => ['purchase_date' => $date]); + } +} diff --git a/database/factories/StoreModelFactory.php b/database/factories/StoreModelFactory.php new file mode 100644 index 0000000..d579fd1 --- /dev/null +++ b/database/factories/StoreModelFactory.php @@ -0,0 +1,51 @@ + + */ +class StoreModelFactory extends Factory +{ + protected $model = StoreModel::class; + + private static int $sequence = 0; + + public function definition(): array + { + $seq = ++self::$sequence; + + return [ + 'name' => '测试门店' . $seq, + 'code' => 'S' . str_pad((string) $seq, 6, '0', STR_PAD_LEFT), + 'level_id' => 0, + 'contact' => '联系人' . $seq, + 'phone' => '138' . str_pad((string) $seq, 8, '0', STR_PAD_LEFT), + 'address' => '测试地址' . $seq . '号', + 'payment_cycle_days' => $seq % 8, + 'status' => StoreModel::STATUS_NORMAL, + 'remark' => '', + ]; + } + + /** + * 停用门店 + */ + public function disabled(): static + { + return $this->state(fn () => ['status' => StoreModel::STATUS_DISABLED]); + } + + /** + * 指定回款周期(天) + */ + public function paymentCycle(int $days): static + { + return $this->state(fn () => ['payment_cycle_days' => $days]); + } +} diff --git a/database/factories/StoreOrderItemModelFactory.php b/database/factories/StoreOrderItemModelFactory.php new file mode 100644 index 0000000..75bc2f2 --- /dev/null +++ b/database/factories/StoreOrderItemModelFactory.php @@ -0,0 +1,42 @@ + + */ +class StoreOrderItemModelFactory extends Factory +{ + protected $model = StoreOrderItemModel::class; + + public function definition(): array + { + return [ + 'order_id' => 0, + 'store_id' => 0, + 'product_id' => 0, + 'product_name' => '测试商品', + 'product_spec' => '500g/袋', + 'price' => number_format(random_int(100, 5000) / 100, 2, '.', ''), + 'quantity' => number_format(random_int(100, 10000) / 100, 2, '.', ''), + 'weight' => 0, + 'amount' => 0, + 'remark' => '', + ]; + } + + public function configure(): static + { + return $this->afterMaking(function (StoreOrderItemModel $item): void { + if ((float) $item->amount === 0.0 && (float) $item->price > 0 && (float) $item->quantity > 0) { + $item->amount = bcmul((string) $item->price, (string) $item->quantity, 2); + } + }); + } +} diff --git a/database/factories/StoreOrderModelFactory.php b/database/factories/StoreOrderModelFactory.php new file mode 100644 index 0000000..68a32e5 --- /dev/null +++ b/database/factories/StoreOrderModelFactory.php @@ -0,0 +1,50 @@ + + */ +class StoreOrderModelFactory extends Factory +{ + protected $model = StoreOrderModel::class; + + private static int $sequence = 0; + + public function definition(): array + { + $seq = ++self::$sequence; + + return [ + 'order_no' => 'SO' . str_pad((string) $seq, 12, '0', STR_PAD_LEFT), + 'store_id' => 0, + 'order_date' => now()->toDateString(), + 'total_quantity' => 0, + 'total_weight' => 0, + 'total_amount' => 0, + 'status' => StoreOrderModel::STATUS_PENDING, + 'remark' => '', + ]; + } + + /** + * 指定订货日期 + */ + public function onDate(string $date): static + { + return $this->state(fn () => ['order_date' => $date]); + } + + /** + * 已汇总(已被采购单归集) + */ + public function summarized(): static + { + return $this->state(fn () => ['status' => StoreOrderModel::STATUS_SUMMARIZED]); + } +} diff --git a/database/factories/SupplierModelFactory.php b/database/factories/SupplierModelFactory.php new file mode 100644 index 0000000..07a6309 --- /dev/null +++ b/database/factories/SupplierModelFactory.php @@ -0,0 +1,43 @@ + + */ +class SupplierModelFactory extends Factory +{ + protected $model = SupplierModel::class; + + private const MAIN_PRODUCTS = ['蔬菜', '水果', '蔬菜/水果', '肉禽蛋']; + + private static int $sequence = 0; + + public function definition(): array + { + $seq = ++self::$sequence; + + return [ + 'name' => '测试供应商' . $seq, + 'contact' => '联系人' . $seq, + 'phone' => '139' . str_pad((string) $seq, 8, '0', STR_PAD_LEFT), + 'address' => '供应商地址' . $seq . '号', + 'main_products' => self::MAIN_PRODUCTS[$seq % count(self::MAIN_PRODUCTS)], + 'status' => SupplierModel::STATUS_NORMAL, + 'remark' => '', + ]; + } + + /** + * 停用供应商 + */ + public function disabled(): static + { + return $this->state(fn () => ['status' => SupplierModel::STATUS_DISABLED]); + } +} diff --git a/database/factories/UserModelFactory.php b/database/factories/UserModelFactory.php new file mode 100644 index 0000000..b3d5115 --- /dev/null +++ b/database/factories/UserModelFactory.php @@ -0,0 +1,69 @@ + + */ +class UserModelFactory extends Factory +{ + protected $model = UserModel::class; + + private static int $sequence = 0; + + public function definition(): array + { + $seq = ++self::$sequence; + + return [ + 'username' => null, + 'password' => null, + 'nickname' => '微信用户' . $seq, + 'email' => '', + 'openid' => 'openid_' . str_pad((string) $seq, 16, '0', STR_PAD_LEFT), + 'unionid' => '', + 'phone' => '', + 'avatar' => '', + 'type' => UserModel::TYPE_PENDING, + 'store_id' => 0, + 'supplier_id' => 0, + 'status' => UserModel::STATUS_NORMAL, + 'last_login_at' => null, + ]; + } + + /** + * 已绑定门店的门店用户 + */ + public function forStore(int $storeId): static + { + return $this->state(fn () => [ + 'type' => UserModel::TYPE_STORE, + 'store_id' => $storeId, + ]); + } + + /** + * 已绑定供应商的供应商用户 + */ + public function forSupplier(int $supplierId): static + { + return $this->state(fn () => [ + 'type' => UserModel::TYPE_SUPPLIER, + 'supplier_id' => $supplierId, + ]); + } + + /** + * 停用账号 + */ + public function disabled(): static + { + return $this->state(fn () => ['status' => UserModel::STATUS_DISABLED]); + } +} diff --git a/database/migrations/2026_08_06_000001_create_cart_table.php b/database/migrations/2026_08_06_000001_create_cart_table.php new file mode 100644 index 0000000..cba0470 --- /dev/null +++ b/database/migrations/2026_08_06_000001_create_cart_table.php @@ -0,0 +1,36 @@ +increments('id')->comment('购物车项ID'); + $table->integer('user_id')->comment('用户ID(购物车归属者)'); + $table->integer('product_id')->comment('商品ID'); + $table->decimal('quantity', 10, 2)->default(0)->comment('订货量'); + $table->timestamps(); + $table->unique(['user_id', 'product_id'], 'cart_user_product_unique'); + $table->index(['user_id', 'created_at'], 'cart_user_created_index'); + $table->comment('小程序购物车表(门店订货车)'); + }); + } + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('cart'); + } +}; diff --git a/tests/Feature/CartTest.php b/tests/Feature/CartTest.php new file mode 100644 index 0000000..a3ad22d --- /dev/null +++ b/tests/Feature/CartTest.php @@ -0,0 +1,331 @@ +create(); + $store = StoreModel::factory()->create(['level_id' => $level->id]); + $product = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON]); + ProductPriceModel::factory()->create([ + 'product_id' => $product->id, + 'level_id' => $level->id, + 'price' => $price, + ]); + + return [$store, $product, UserModel::factory()->forStore($store->id)->create()]; + } + + /** 加购成功:数量入库、返回购物车项 */ + public function test_add_to_cart_succeeds(): void + { + [, $product, $user] = $this->makeStoreWithProduct('5.50'); + $this->actingAsMiniUser($user); + + $this->postJson('/mini/cart', [ + 'product_id' => $product->id, + 'quantity' => 2.5, + ])->assertOk() + ->assertJsonPath('success', true) + ->assertJsonPath('data.quantity', '2.50') + ->assertJsonStructure(['data' => ['id', 'quantity']]); + + $cart = CartModel::first(); + $this->assertNotNull($cart); + $this->assertSame($user->id, $cart->user_id); + $this->assertSame('2.50', (string) $cart->quantity); + } + + /** 重复加购同一商品合并累加(同商品唯一行) */ + public function test_duplicate_add_merges_quantity(): void + { + [, $product, $user] = $this->makeStoreWithProduct(); + $this->actingAsMiniUser($user); + + $this->postJson('/mini/cart', ['product_id' => $product->id, 'quantity' => 2]); + $this->postJson('/mini/cart', ['product_id' => $product->id, 'quantity' => 3]) + ->assertJsonPath('data.quantity', '5.00'); + + $this->assertSame(1, CartModel::count()); + $this->assertSame('5.00', (string) CartModel::first()->quantity); + } + + /** 已下架商品拒绝加购 */ + public function test_off_shelf_product_rejected(): void + { + [, $product, $user] = $this->makeStoreWithProduct(); + $product->update(['status' => ProductModel::STATUS_OFF]); + $this->actingAsMiniUser($user); + + $this->postJson('/mini/cart', ['product_id' => $product->id, 'quantity' => 1]) + ->assertOk() + ->assertJsonPath('success', false); + + $this->assertSame(0, CartModel::count()); + } + + /** 已软删除商品拒绝加购 */ + public function test_soft_deleted_product_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_product_without_level_price_rejected(): void + { + [, $product, $user] = $this->makeStoreWithProduct(); + ProductPriceModel::where('product_id', $product->id)->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_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 + { + $level = CustomerLevelModel::factory()->create(); + $store = StoreModel::factory()->create(['level_id' => $level->id]); + $p1 = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON]); + $p2 = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON]); + ProductPriceModel::factory()->create(['product_id' => $p1->id, 'level_id' => $level->id, 'price' => '5.50']); + ProductPriceModel::factory()->create(['product_id' => $p2->id, 'level_id' => $level->id, 'price' => '3.00']); + $this->actingAsMiniUser(UserModel::factory()->forStore($store->id)->create()); + + $this->postJson('/mini/cart', ['product_id' => $p1->id, 'quantity' => 3]); + $this->postJson('/mini/cart', ['product_id' => $p2->id, 'quantity' => 2]); + + $this->getJson('/mini/cart')->assertOk() + ->assertJsonPath('success', true) + // items 按 id 倒序:后加入的 p2 在前 + ->assertJsonPath('data.items.0.product_id', $p2->id) + ->assertJsonPath('data.items.0.price', '3.00') + ->assertJsonPath('data.items.0.quantity', '2.00') + ->assertJsonPath('data.items.0.amount', '6.00') + ->assertJsonPath('data.items.0.status', 1) + ->assertJsonPath('data.items.1.product_id', $p1->id) + ->assertJsonPath('data.items.1.price', '5.50') + ->assertJsonPath('data.items.1.amount', '16.50') + ->assertJsonPath('data.items.1.status', 1) + ->assertJsonPath('data.total_count', 2) + ->assertJsonPath('data.total_quantity', '5.00') + ->assertJsonPath('data.total_amount', '22.50'); + } + + /** 列表:下架商品标记不可购,不计入汇总 */ + public function test_list_marks_off_shelf_item_unbuyable(): void + { + $level = CustomerLevelModel::factory()->create(); + $store = StoreModel::factory()->create(['level_id' => $level->id]); + $p1 = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON]); + $p2 = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON]); + ProductPriceModel::factory()->create(['product_id' => $p1->id, 'level_id' => $level->id, 'price' => '5.50']); + ProductPriceModel::factory()->create(['product_id' => $p2->id, 'level_id' => $level->id, 'price' => '3.00']); + $this->actingAsMiniUser(UserModel::factory()->forStore($store->id)->create()); + + $this->postJson('/mini/cart', ['product_id' => $p1->id, 'quantity' => 3]); + $this->postJson('/mini/cart', ['product_id' => $p2->id, 'quantity' => 2]); + $p1->update(['status' => ProductModel::STATUS_OFF]); + + $this->getJson('/mini/cart')->assertOk() + ->assertJsonPath('data.items.1.status', 0) + ->assertJsonPath('data.items.1.amount', null) + ->assertJsonPath('data.total_count', 2) + ->assertJsonPath('data.total_quantity', '2.00') + ->assertJsonPath('data.total_amount', '6.00'); + } + + /** 空购物车返回空列表 */ + 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->getJson('/mini/cart')->assertOk() + ->assertJsonPath('data.items', []) + ->assertJsonPath('data.total_count', 0) + ->assertJsonPath('data.total_amount', '0.00'); + } + + /** 修改数量 */ + public function test_update_quantity(): void + { + [, $product, $user] = $this->makeStoreWithProduct(); + $this->actingAsMiniUser($user); + $this->postJson('/mini/cart', ['product_id' => $product->id, 'quantity' => 1]); + $cart = CartModel::first(); + + $this->putJson("/mini/cart/{$cart->id}", ['quantity' => 7]) + ->assertJsonPath('success', true) + ->assertJsonPath('data.quantity', '7.00'); + + $this->assertSame('7.00', (string) $cart->fresh()->quantity); + } + + /** 修改他人购物车项拒绝(同店另一用户) */ + public function test_update_other_users_item_rejected(): void + { + [$store, $product, $userA] = $this->makeStoreWithProduct(); + $this->actingAsMiniUser($userA); + $this->postJson('/mini/cart', ['product_id' => $product->id, 'quantity' => 1]); + $cart = CartModel::first(); + + $this->actingAsMiniUser(UserModel::factory()->forStore($store->id)->create()); + $this->putJson("/mini/cart/{$cart->id}", ['quantity' => 9]) + ->assertJsonPath('success', false) + ->assertJsonPath('msg', '购物车项不存在'); + + $this->assertSame('1.00', (string) $cart->fresh()->quantity, '他人改数量不应生效'); + } + + /** 修改不存在的购物车项拒绝 */ + public function test_update_missing_item_rejected(): void + { + [$store, $product, $user] = $this->makeStoreWithProduct(); + $this->actingAsMiniUser($user); + $this->postJson('/mini/cart', ['product_id' => $product->id, 'quantity' => 1]); + + $this->putJson('/mini/cart/999999', ['quantity' => 1])->assertJsonPath('success', false); + } + + /** 数量校验:0 与负值拒绝 */ + public function test_invalid_quantity_rejected(): void + { + [, $product, $user] = $this->makeStoreWithProduct(); + $this->actingAsMiniUser($user); + + $this->postJson('/mini/cart', ['product_id' => $product->id, 'quantity' => 0]) + ->assertOk() + ->assertJsonPath('success', false) + ->assertJsonPath('msg', '订货数量必须大于 0'); + + $this->assertSame(0, CartModel::count()); + } + + /** 删除单项 */ + public function test_delete_item(): void + { + [, $product, $user] = $this->makeStoreWithProduct(); + $this->actingAsMiniUser($user); + $this->postJson('/mini/cart', ['product_id' => $product->id, 'quantity' => 1]); + $cart = CartModel::first(); + + $this->deleteJson("/mini/cart/{$cart->id}")->assertJsonPath('success', true); + $this->assertSame(0, CartModel::count()); + } + + /** 删除他人购物车项拒绝 */ + public function test_delete_other_users_item_rejected(): void + { + [$store, $product, $userA] = $this->makeStoreWithProduct(); + $this->actingAsMiniUser($userA); + $this->postJson('/mini/cart', ['product_id' => $product->id, 'quantity' => 1]); + $cart = CartModel::first(); + + $this->actingAsMiniUser(UserModel::factory()->forStore($store->id)->create()); + $this->deleteJson("/mini/cart/{$cart->id}") + ->assertJsonPath('success', false) + ->assertJsonPath('msg', '购物车项不存在'); + + $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]); + $p1 = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON]); + $p2 = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON]); + ProductPriceModel::factory()->create(['product_id' => $p1->id, 'level_id' => $level->id, 'price' => '5.00']); + ProductPriceModel::factory()->create(['product_id' => $p2->id, 'level_id' => $level->id, 'price' => '5.00']); + $userA = UserModel::factory()->forStore($store->id)->create(); + $userB = UserModel::factory()->forStore($store->id)->create(); + + $this->actingAsMiniUser($userA); + $this->postJson('/mini/cart', ['product_id' => $p1->id, 'quantity' => 1]); + $this->postJson('/mini/cart', ['product_id' => $p2->id, 'quantity' => 1]); + + $this->actingAsMiniUser($userB); + $this->postJson('/mini/cart', ['product_id' => $p1->id, 'quantity' => 1]); + + $this->actingAsMiniUser($userA); + $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(), '他人购物车不受影响'); + } + + /** 未登录访问购物车 → 401 */ + public function test_unauthenticated_returns_401(): void + { + $this->getJson('/mini/cart')->assertStatus(401); + } + + /** 跨用户列表隔离:B 看不到 A 的购物车 */ + public function test_cart_isolated_between_users(): void + { + $level = CustomerLevelModel::factory()->create(); + $store = StoreModel::factory()->create(['level_id' => $level->id]); + $product = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON]); + ProductPriceModel::factory()->create(['product_id' => $product->id, 'level_id' => $level->id, 'price' => '5.00']); + $userA = UserModel::factory()->forStore($store->id)->create(); + $userB = UserModel::factory()->forStore($store->id)->create(); + + $this->actingAsMiniUser($userA); + $this->postJson('/mini/cart', ['product_id' => $product->id, 'quantity' => 1]); + + $this->actingAsMiniUser($userB); + $this->getJson('/mini/cart')->assertOk() + ->assertJsonPath('data.items', []) + ->assertJsonPath('data.total_count', 0); + } +} diff --git a/tests/Feature/MiniAuthTest.php b/tests/Feature/MiniAuthTest.php index a1bb1db..9a54d86 100644 --- a/tests/Feature/MiniAuthTest.php +++ b/tests/Feature/MiniAuthTest.php @@ -5,6 +5,8 @@ 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; @@ -17,8 +19,29 @@ class MiniAuthTest extends ProcurementTestCase protected function setUp(): void { parent::setUp(); - // 测试环境注入微信配置(生产由 WECHAT_MINI_APPID/SECRET 提供) - config(['services.wechat.mini' => ['appid' => 'test_appid', 'secret' => 'test_secret']]); + // 微信配置为 DB 驱动(后台「小程序设置」→ sys_site_config,WechatService 经 + // site_config('wechatMini') 读取):测试落库并刷新配置缓存 + DB::table('sys_site_config_group')->insert([ + 'id' => 100, + 'title' => '小程序设置', + 'key' => 'wechatMini', + 'remark' => '', + 'created_at' => now(), + 'updated_at' => now(), + ]); + 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(), + ], + ]); + SysSiteConfigService::refreshSiteConfig(); } /**