diff --git a/.phpunit.result.cache b/.phpunit.result.cache index aa5436d..7a0531b 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":7,"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":8,"Tests\\Feature\\MiniAuthTest::test_phone_no_match_stays_pending":8,"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,"Tests\\Feature\\ProductCostPriceTest::test_create_product_with_cost_and_percent_pricing":7,"Tests\\Feature\\ProductCostPriceTest::test_create_percent_row_without_percent_fails":7,"Tests\\Feature\\ProductCostPriceTest::test_batch_price_cost_only_notifies_percent_level_stores":7,"Tests\\Feature\\ProductCostPriceTest::test_batch_price_empty_row_rejected":7,"Tests\\Feature\\MiniAuthTest::test_disabled_user_cannot_login":8,"Tests\\Feature\\MiniAuthTest::test_mini_token_cannot_access_admin_api":8,"Tests\\Feature\\ProductCostPriceTest::test_batch_price_percent_row_update":7,"Tests\\Feature\\ProductCostPriceTest::test_batch_price_mixed_row_updates_cost_and_level":7,"Tests\\Feature\\ReconciliationTest::test_build_creates_reconciliation_items":8,"Tests\\Feature\\ReconciliationTest::test_build_filters_by_supplier":8,"Tests\\Feature\\ReconciliationTest::test_update_item_recalculates_diff_and_header":8,"Tests\\Feature\\ReconciliationTest::test_toggle_reconciled_flag":8,"Tests\\Feature\\ReconciliationTest::test_settle_creates_settlements_per_store":8,"Tests\\Feature\\ProductCostPriceTest::test_mini_list_returns_actual_price_without_cost_price":8,"Tests\\Feature\\ProductCostPriceTest::test_mini_order_uses_actual_price_for_percent_row":8,"Tests\\Feature\\ProductCostPriceTest::test_mini_cart_uses_actual_price_for_percent_row":8},"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.007,"Tests\\Feature\\ProductPriceTest::test_batch_price_updates_and_notifies_affected_stores":0.01,"Tests\\Feature\\ProductPriceTest::test_price_matrix_structure":0.007,"Tests\\Feature\\StoreOrderTest::test_place_order_snapshots_level_price_and_recalculates":0.007,"Tests\\Feature\\StoreOrderTest::test_client_supplied_amount_is_ignored":0.008,"Tests\\Feature\\StoreOrderTest::test_product_without_level_price_rejected":0.007,"Tests\\Feature\\StoreOrderTest::test_cancel_only_pending_orders":0.01,"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.02,"Tests\\Feature\\PurchaseGenerateTest::test_generate_without_pending_orders_fails":0.006,"Tests\\Feature\\PurchaseGenerateTest::test_generate_is_idempotent":0.021,"Tests\\Feature\\AllocationTest::test_allocation_conserves_amount_with_tail_correction":0.074,"Tests\\Feature\\AllocationTest::test_allocation_follows_order_ratio":0.022,"Tests\\Feature\\AllocationTest::test_allocation_is_idempotent":0.026,"Tests\\Feature\\AllocationTest::test_allocation_rejected_without_actual_amount":0.017,"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.03,"Tests\\Feature\\ReconciliationTest::test_update_item_recalculates_diff_and_header":0.029,"Tests\\Feature\\ReconciliationTest::test_toggle_reconciled_flag":0.029,"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.016,"Tests\\Feature\\StatementTest::test_generate_excludes_cancelled_and_used_items":0.016,"Tests\\Feature\\MiniAuthTest::test_login_creates_user_and_issues_token":0.056,"Tests\\Feature\\MiniAuthTest::test_disabled_user_cannot_login":0.003,"Tests\\Feature\\MiniAuthTest::test_phone_binding_matches_store":0.005,"Tests\\Feature\\MiniAuthTest::test_phone_no_match_stays_pending":0.001,"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.078,"Tests\\Feature\\ExportTest::test_export_category_filters_to_vegetables":0.018,"Tests\\Feature\\ExportTest::test_export_pdf":1.307,"Tests\\Feature\\ExportTest::test_export_invalid_format_rejected":0.037,"Tests\\Feature\\ExportTest::test_export_requires_export_permission":0.017,"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.009,"Tests\\Feature\\CartTest::test_duplicate_add_merges_quantity":0.009,"Tests\\Feature\\CartTest::test_off_shelf_product_rejected":0.007,"Tests\\Feature\\CartTest::test_soft_deleted_product_rejected":0.006,"Tests\\Feature\\CartTest::test_product_without_level_price_rejected":0.006,"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.011,"Tests\\Feature\\CartTest::test_empty_cart_returns_empty_items":0.005,"Tests\\Feature\\CartTest::test_update_quantity":0.008,"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.007,"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.013,"Tests\\Feature\\CartTest::test_unauthenticated_returns_401":0.004,"Tests\\Feature\\CartTest::test_cart_isolated_between_users":0.009,"Tests\\Feature\\DebugCartListTest::test_dump_cart_list_response":0.057,"Tests\\Feature\\ProductCostPriceTest::test_calc_actual_price_fixed_and_percent":0.001,"Tests\\Feature\\ProductCostPriceTest::test_calc_actual_price_edge_cases":0.001,"Tests\\Feature\\ProductCostPriceTest::test_admin_list_contains_cost_price_and_actual_price":0.009,"Tests\\Feature\\ProductCostPriceTest::test_mini_list_returns_actual_price_without_cost_price":0.007,"Tests\\Feature\\ProductCostPriceTest::test_create_product_with_cost_and_percent_pricing":0.009,"Tests\\Feature\\ProductCostPriceTest::test_create_percent_row_without_percent_fails":0.007,"Tests\\Feature\\ProductCostPriceTest::test_update_switches_fixed_to_percent":0.009,"Tests\\Feature\\ProductCostPriceTest::test_batch_price_cost_only_notifies_percent_level_stores":0.01,"Tests\\Feature\\ProductCostPriceTest::test_batch_price_percent_row_update":0.008,"Tests\\Feature\\ProductCostPriceTest::test_batch_price_mixed_row_updates_cost_and_level":0.009,"Tests\\Feature\\ProductCostPriceTest::test_batch_price_empty_row_rejected":0.006,"Tests\\Feature\\ProductCostPriceTest::test_price_matrix_includes_cost_and_percent_columns":0.007,"Tests\\Feature\\ProductCostPriceTest::test_mini_order_uses_actual_price_for_percent_row":0.008,"Tests\\Feature\\ProductCostPriceTest::test_mini_cart_uses_actual_price_for_percent_row":0.008,"Tests\\Feature\\PurchaseGenerateTest::test_generate_uses_min_actual_price_across_types":0.013,"Tests\\Feature\\ProductPriceTest::test_store_without_level_sees_products_without_price":0.007,"Tests\\Feature\\ProductPriceTest::test_unbound_user_can_browse_products_without_price_but_cannot_cart":0.009,"Tests\\Feature\\MiniAuthTest::test_login_issues_token_for_registered_user":0.012,"Tests\\Feature\\MiniAuthTest::test_login_rejects_unregistered_openid":0.002,"Tests\\Feature\\MiniAuthTest::test_register_binds_store_and_issues_token":0.028,"Tests\\Feature\\MiniAuthTest::test_register_rejects_unknown_store":0.002,"Tests\\Feature\\MiniAuthTest::test_register_rejects_duplicate_openid":0.003,"Tests\\Feature\\MiniStoreTest::test_info_returns_bound_store":0.004,"Tests\\Feature\\MiniStoreTest::test_update_info":0.005,"Tests\\Feature\\MiniStoreTest::test_update_info_ignores_readonly_fields":0.005,"Tests\\Feature\\MiniStoreTest::test_requires_bound_store":0.007,"Tests\\Feature\\MiniStoreTest::test_update_info_validates_length":0.005}} \ 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":7,"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":8,"Tests\\Feature\\MiniAuthTest::test_phone_no_match_stays_pending":8,"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,"Tests\\Feature\\ProductCostPriceTest::test_create_product_with_cost_and_percent_pricing":7,"Tests\\Feature\\ProductCostPriceTest::test_create_percent_row_without_percent_fails":7,"Tests\\Feature\\ProductCostPriceTest::test_batch_price_cost_only_notifies_percent_level_stores":7,"Tests\\Feature\\ProductCostPriceTest::test_batch_price_empty_row_rejected":7,"Tests\\Feature\\MiniAuthTest::test_disabled_user_cannot_login":8,"Tests\\Feature\\MiniAuthTest::test_mini_token_cannot_access_admin_api":8,"Tests\\Feature\\ProductCostPriceTest::test_batch_price_percent_row_update":7,"Tests\\Feature\\ProductCostPriceTest::test_batch_price_mixed_row_updates_cost_and_level":7,"Tests\\Feature\\ReconciliationTest::test_build_creates_reconciliation_items":8,"Tests\\Feature\\ReconciliationTest::test_build_filters_by_supplier":8,"Tests\\Feature\\ReconciliationTest::test_update_item_recalculates_diff_and_header":8,"Tests\\Feature\\ReconciliationTest::test_toggle_reconciled_flag":8,"Tests\\Feature\\ReconciliationTest::test_settle_creates_settlements_per_store":8,"Tests\\Feature\\ProductCostPriceTest::test_mini_list_returns_actual_price_without_cost_price":8,"Tests\\Feature\\ProductCostPriceTest::test_mini_order_uses_actual_price_for_percent_row":8,"Tests\\Feature\\ProductCostPriceTest::test_mini_cart_uses_actual_price_for_percent_row":8,"Tests\\Feature\\StoreOrderTest::test_container_update_recalculates_amounts":8,"Tests\\Feature\\StoreOrderTest::test_container_update_allowed_in_editable_statuses":8,"Tests\\Feature\\StoreOrderTest::test_container_update_rejected_in_other_statuses":8,"Tests\\Feature\\StoreOrderTest::test_container_update_heals_legacy_product_amount":8,"Tests\\Feature\\StoreOrderTest::test_container_update_validates_negative_numbers":8},"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.007,"Tests\\Feature\\ProductPriceTest::test_batch_price_updates_and_notifies_affected_stores":0.009,"Tests\\Feature\\ProductPriceTest::test_price_matrix_structure":0.006,"Tests\\Feature\\StoreOrderTest::test_place_order_snapshots_level_price_and_recalculates":0.007,"Tests\\Feature\\StoreOrderTest::test_client_supplied_amount_is_ignored":0.007,"Tests\\Feature\\StoreOrderTest::test_product_without_level_price_rejected":0.008,"Tests\\Feature\\StoreOrderTest::test_cancel_only_pending_orders":0.01,"Tests\\Feature\\StoreOrderTest::test_summarized_order_cannot_be_cancelled":0.009,"Tests\\Feature\\StoreOrderTest::test_order_data_isolated_between_stores":0.012,"Tests\\Feature\\PurchaseGenerateTest::test_generate_aggregates_orders_by_product":0.025,"Tests\\Feature\\PurchaseGenerateTest::test_generate_writes_back_order_status":0.018,"Tests\\Feature\\PurchaseGenerateTest::test_generate_without_pending_orders_fails":0.006,"Tests\\Feature\\PurchaseGenerateTest::test_generate_is_idempotent":0.02,"Tests\\Feature\\AllocationTest::test_allocation_conserves_amount_with_tail_correction":0.083,"Tests\\Feature\\AllocationTest::test_allocation_follows_order_ratio":0.02,"Tests\\Feature\\AllocationTest::test_allocation_is_idempotent":0.021,"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.026,"Tests\\Feature\\ReconciliationTest::test_build_filters_by_supplier":0.028,"Tests\\Feature\\ReconciliationTest::test_update_item_recalculates_diff_and_header":0.029,"Tests\\Feature\\ReconciliationTest::test_toggle_reconciled_flag":0.027,"Tests\\Feature\\ReconciliationTest::test_settle_creates_settlements_per_store":0.028,"Tests\\Feature\\StatementTest::test_generate_snapshots_payment_cycle":0.013,"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.056,"Tests\\Feature\\MiniAuthTest::test_disabled_user_cannot_login":0.002,"Tests\\Feature\\MiniAuthTest::test_phone_binding_matches_store":0.005,"Tests\\Feature\\MiniAuthTest::test_phone_no_match_stays_pending":0.001,"Tests\\Feature\\MiniAuthTest::test_sys_token_cannot_access_mini":0.004,"Tests\\Feature\\MiniAuthTest::test_mini_token_cannot_access_admin_api":0.003,"Tests\\Feature\\ExportTest::test_export_xlsx_with_encoded_chinese_filename":0.119,"Tests\\Feature\\ExportTest::test_export_category_filters_to_vegetables":0.018,"Tests\\Feature\\ExportTest::test_export_pdf":1.336,"Tests\\Feature\\ExportTest::test_export_invalid_format_rejected":0.022,"Tests\\Feature\\ExportTest::test_export_requires_export_permission":0.03,"Tests\\Unit\\ExampleTest::test_that_true_is_true":0.001,"Tests\\Feature\\MiniAuthTest::test_login_fails_when_wechat_rejects_code":0.005,"Tests\\Feature\\CartTest::test_add_to_cart_succeeds":0.008,"Tests\\Feature\\CartTest::test_duplicate_add_merges_quantity":0.008,"Tests\\Feature\\CartTest::test_off_shelf_product_rejected":0.006,"Tests\\Feature\\CartTest::test_soft_deleted_product_rejected":0.007,"Tests\\Feature\\CartTest::test_product_without_level_price_rejected":0.006,"Tests\\Feature\\CartTest::test_unbound_user_rejected":0.006,"Tests\\Feature\\CartTest::test_store_without_level_rejected":0.006,"Tests\\Feature\\CartTest::test_list_with_prices_amounts_and_totals":0.01,"Tests\\Feature\\CartTest::test_list_marks_off_shelf_item_unbuyable":0.011,"Tests\\Feature\\CartTest::test_empty_cart_returns_empty_items":0.005,"Tests\\Feature\\CartTest::test_update_quantity":0.008,"Tests\\Feature\\CartTest::test_update_other_users_item_rejected":0.009,"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.013,"Tests\\Feature\\CartTest::test_unauthenticated_returns_401":0.004,"Tests\\Feature\\CartTest::test_cart_isolated_between_users":0.008,"Tests\\Feature\\DebugCartListTest::test_dump_cart_list_response":0.057,"Tests\\Feature\\ProductCostPriceTest::test_calc_actual_price_fixed_and_percent":0.001,"Tests\\Feature\\ProductCostPriceTest::test_calc_actual_price_edge_cases":0.001,"Tests\\Feature\\ProductCostPriceTest::test_admin_list_contains_cost_price_and_actual_price":0.009,"Tests\\Feature\\ProductCostPriceTest::test_mini_list_returns_actual_price_without_cost_price":0.006,"Tests\\Feature\\ProductCostPriceTest::test_create_product_with_cost_and_percent_pricing":0.022,"Tests\\Feature\\ProductCostPriceTest::test_create_percent_row_without_percent_fails":0.006,"Tests\\Feature\\ProductCostPriceTest::test_update_switches_fixed_to_percent":0.008,"Tests\\Feature\\ProductCostPriceTest::test_batch_price_cost_only_notifies_percent_level_stores":0.01,"Tests\\Feature\\ProductCostPriceTest::test_batch_price_percent_row_update":0.01,"Tests\\Feature\\ProductCostPriceTest::test_batch_price_mixed_row_updates_cost_and_level":0.008,"Tests\\Feature\\ProductCostPriceTest::test_batch_price_empty_row_rejected":0.006,"Tests\\Feature\\ProductCostPriceTest::test_price_matrix_includes_cost_and_percent_columns":0.006,"Tests\\Feature\\ProductCostPriceTest::test_mini_order_uses_actual_price_for_percent_row":0.007,"Tests\\Feature\\ProductCostPriceTest::test_mini_cart_uses_actual_price_for_percent_row":0.008,"Tests\\Feature\\PurchaseGenerateTest::test_generate_uses_min_actual_price_across_types":0.013,"Tests\\Feature\\ProductPriceTest::test_store_without_level_sees_products_without_price":0.006,"Tests\\Feature\\ProductPriceTest::test_unbound_user_can_browse_products_without_price_but_cannot_cart":0.008,"Tests\\Feature\\MiniAuthTest::test_login_issues_token_for_registered_user":0.021,"Tests\\Feature\\MiniAuthTest::test_login_rejects_unregistered_openid":0.002,"Tests\\Feature\\MiniAuthTest::test_register_binds_store_and_issues_token":0.035,"Tests\\Feature\\MiniAuthTest::test_register_rejects_unknown_store":0.002,"Tests\\Feature\\MiniAuthTest::test_register_rejects_duplicate_openid":0.002,"Tests\\Feature\\MiniStoreTest::test_info_returns_bound_store":0.004,"Tests\\Feature\\MiniStoreTest::test_update_info":0.005,"Tests\\Feature\\MiniStoreTest::test_update_info_ignores_readonly_fields":0.005,"Tests\\Feature\\MiniStoreTest::test_requires_bound_store":0.006,"Tests\\Feature\\MiniStoreTest::test_update_info_validates_length":0.004,"Tests\\Feature\\StoreOrderTest::test_container_update_recalculates_amounts":0.012,"Tests\\Feature\\StoreOrderTest::test_container_update_allowed_in_editable_statuses":0.015,"Tests\\Feature\\StoreOrderTest::test_container_update_rejected_in_other_statuses":0.014,"Tests\\Feature\\StoreOrderTest::test_container_update_heals_legacy_product_amount":0.012,"Tests\\Feature\\StoreOrderTest::test_container_update_validates_negative_numbers":0.011}} \ No newline at end of file diff --git a/app/Http/Controllers/Mini/OrderController.php b/app/Http/Controllers/Mini/OrderController.php index 24abd02..ca863c0 100644 --- a/app/Http/Controllers/Mini/OrderController.php +++ b/app/Http/Controllers/Mini/OrderController.php @@ -101,6 +101,7 @@ class OrderController extends BaseMiniController 'order_date' => $now->toDateString(), 'total_quantity' => $totalQuantity, 'total_weight' => 0, + 'product_amount' => $totalAmount, 'total_amount' => $totalAmount, 'status' => StoreOrderModel::STATUS_PENDING, 'remark' => $remark, diff --git a/app/Http/Controllers/Order/StoreOrderController.php b/app/Http/Controllers/Order/StoreOrderController.php index 626f7fb..af26112 100644 --- a/app/Http/Controllers/Order/StoreOrderController.php +++ b/app/Http/Controllers/Order/StoreOrderController.php @@ -57,6 +57,8 @@ class StoreOrderController extends BaseController $box_amount = site_config('services.box_amount'); $tray_amount = site_config('services.tray_amount'); foreach ($data['data'] as &$item) { + $item['box_price'] = number_format($box_amount, 2); + $item['tray_price'] = number_format($tray_amount, 2); $item['box_amount'] = number_format($box_amount * $item['box_num'], 2); $item['tray_amount'] = number_format($tray_amount * $item['tray_num'], 2); if(count($item['items']) > 0) { @@ -160,6 +162,61 @@ class StoreOrderController extends BaseController return $this->success(); } + /** + * 修改周转框/周转托盘数量(仅已接单、采购中、配送中可改), + * 自动重算附加金额与订单总金额 + */ + #[PutRoute(route: '/{id}/container', authorize: 'update', where: ['id' => '[0-9]+'])] + public function container(int $id, Request $request): JsonResponse + { + $data = $request->validate([ + 'box_num' => 'required|integer|min:0', + 'tray_num' => 'required|integer|min:0', + ], [ + 'box_num.required' => '周转框数量不能为空', + 'box_num.integer' => '周转框数量必须为整数', + 'box_num.min' => '周转框数量不能小于 0', + 'tray_num.required' => '周转托盘数量不能为空', + 'tray_num.integer' => '周转托盘数量必须为整数', + 'tray_num.min' => '周转托盘数量不能小于 0', + ]); + + $order = StoreOrderModel::find($id); + if (empty($order)) { + throw new RepositoryException('订单不存在'); + } + + $editable = [ + StoreOrderModel::STATUS_SUMMARIZED, + StoreOrderModel::STATUS_DELIVERING, + StoreOrderModel::STATUS_DISTRIBUTION, + ]; + if (! in_array($order->status, $editable, true)) { + throw new RepositoryException( + '订单当前状态为「' . (self::STATUS_NAMES[$order->status] ?? $order->status) . '」,不允许修改周转框/托盘数量' + ); + } + + $boxPrice = (float) site_config('services.box_amount', 0); + $trayPrice = (float) site_config('services.tray_amount', 0); + $addedAmount = round($data['box_num'] * $boxPrice + $data['tray_num'] * $trayPrice, 2); + + // 历史订单未写商品金额:按「总额 - 附加」反推并回写,保证 总额 = 商品 + 附加 恒成立 + $productAmount = (float) $order->product_amount; + if ($productAmount <= 0 && (float) $order->total_amount > 0) { + $productAmount = round((float) $order->total_amount - (float) $order->added_amount, 2); + } + + $order->box_num = (int) $data['box_num']; + $order->tray_num = (int) $data['tray_num']; + $order->product_amount = $productAmount; + $order->added_amount = $addedAmount; + $order->total_amount = round($productAmount + $addedAmount, 2); + $order->save(); + + return $this->success(); + } + /** * 状态流转后通知门店用户 */ diff --git a/database/migrations/2026_07_23_030610_create_store_order_table.php b/database/migrations/2026_07_23_030610_create_store_order_table.php index abac1fe..494c450 100644 --- a/database/migrations/2026_07_23_030610_create_store_order_table.php +++ b/database/migrations/2026_07_23_030610_create_store_order_table.php @@ -21,7 +21,7 @@ return new class extends Migration $table->integer('purchase_id')->nullable()->comment('关联采购单ID'); $table->integer('statement_id')->nullable()->comment('关联账单ID'); $table->date('order_date')->comment('订货日期'); - $table->integer('total_quantity', 10)->default(0)->comment('订货总量'); + $table->integer('total_quantity')->default(0)->comment('订货总量'); $table->decimal('total_weight', 10, 3)->default(0)->comment('总重量'); $table->decimal('total_amount', 10, 2)->default(0)->comment('订单总金额'); $table->decimal('product_amount', 10, 2)->default(0)->comment('商品总金额'); @@ -47,7 +47,7 @@ return new class extends Migration $table->string('product_name', 100)->comment('品名(快照)'); $table->string('product_spec', 100)->default('')->comment('规格/包规(快照)'); $table->decimal('price', 10, 2)->default(0)->comment('单价(下单时客户等级价快照)'); - $table->integer('quantity', 10)->default(0)->comment('订货量'); + $table->integer('quantity')->default(0)->comment('订货量'); $table->decimal('weight', 10, 3)->default(0)->comment('重量'); $table->decimal('amount', 10, 2)->default(0)->comment('单品金额'); $table->string('remark', 255)->default('')->comment('门店下单备注'); diff --git a/tests/Feature/StoreOrderTest.php b/tests/Feature/StoreOrderTest.php index f3b1dfd..5e8f3ef 100644 --- a/tests/Feature/StoreOrderTest.php +++ b/tests/Feature/StoreOrderTest.php @@ -8,6 +8,9 @@ use App\Models\ProductPriceModel; use App\Models\StoreModel; use App\Models\StoreOrderModel; use App\Models\UserModel; +use Illuminate\Support\Facades\Cache; +use Modules\SystemTool\Models\SysSiteConfigGroupModel; +use Modules\SystemTool\Models\SysSiteConfigItemsModel; /** * 小程序下单:等级价快照、服务端重算总价、取消限制、门店数据隔离 @@ -129,4 +132,122 @@ class StoreOrderTest extends ProcurementTestCase $this->getJson("/mini/order/{$orderOfA->id}")->assertJsonPath('success', false); $this->getJson('/mini/order')->assertJsonPath('data.total', 0); } + + /** 种子化周转框/托盘单价配置(框 2.00、托盘 10.00) */ + private function seedContainerConfig(): void + { + $group = SysSiteConfigGroupModel::create(['title' => '业务配置', 'key' => 'services']); + foreach ([['box_amount', '周转框单价', '2.00'], ['tray_amount', '周转托盘单价', '10.00']] as [$key, $title, $value]) { + SysSiteConfigItemsModel::create([ + 'group_id' => $group->id, + 'key' => $key, + 'title' => $title, + 'type' => 'InputNumber', + 'values' => $value, + 'sort' => 0, + ]); + } + Cache::forget('site_config'); + } + + /** 造一笔指定状态的订单(商品 5.00 × 2 = 10.00) */ + private function makeOrderWithStatus(int $status): StoreOrderModel + { + [$store, $product, $user] = $this->makeStoreWithProduct('5.00'); + $this->actingAsMiniUser($user); + $this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 2]]]) + ->assertJsonPath('success', true); + $order = StoreOrderModel::where('store_id', $store->id)->first(); + $order->update(['status' => $status]); + + return $order; + } + + /** 修改周转框/托盘:自动重算附加金额与订单总金额 */ + public function test_container_update_recalculates_amounts(): void + { + $this->seedContainerConfig(); + $order = $this->makeOrderWithStatus(StoreOrderModel::STATUS_SUMMARIZED); + $this->assertSame('10.00', (string) $order->product_amount, '下单时应写入商品金额'); + + $this->actingAsSysUser(); + $this->putJson("/order/store/{$order->id}/container", ['box_num' => 3, 'tray_num' => 1]) + ->assertOk()->assertJsonPath('success', true); + + $order->refresh(); + $this->assertSame(3, $order->box_num); + $this->assertSame(1, $order->tray_num); + $this->assertSame('16.00', (string) $order->added_amount, '3×2.00 + 1×10.00'); + $this->assertSame('10.00', (string) $order->product_amount); + $this->assertSame('26.00', (string) $order->total_amount, '10.00 + 16.00'); + } + + /** 已接单、采购中、配送中均可修改 */ + public function test_container_update_allowed_in_editable_statuses(): void + { + $this->seedContainerConfig(); + $order = $this->makeOrderWithStatus(StoreOrderModel::STATUS_SUMMARIZED); + $this->actingAsSysUser(); + + foreach ([ + StoreOrderModel::STATUS_SUMMARIZED, + StoreOrderModel::STATUS_DELIVERING, + StoreOrderModel::STATUS_DISTRIBUTION, + ] as $status) { + $order->update(['status' => $status]); + $this->putJson("/order/store/{$order->id}/container", ['box_num' => 1, 'tray_num' => 0]) + ->assertOk()->assertJsonPath('success', true); + $this->assertSame('2.00', (string) $order->fresh()->added_amount); + } + } + + /** 待接单、已完成、已取消不允许修改 */ + public function test_container_update_rejected_in_other_statuses(): void + { + $this->seedContainerConfig(); + $order = $this->makeOrderWithStatus(StoreOrderModel::STATUS_PENDING); + $this->actingAsSysUser(); + + foreach ([ + StoreOrderModel::STATUS_PENDING, + StoreOrderModel::STATUS_COMPLETED, + StoreOrderModel::STATUS_CANCELLED, + ] as $status) { + $order->update(['status' => $status]); + $this->putJson("/order/store/{$order->id}/container", ['box_num' => 5, 'tray_num' => 5]) + ->assertOk()->assertJsonPath('success', false); + } + $order->refresh(); + $this->assertSame(0, $order->box_num, '被拒绝后数量不变'); + $this->assertSame('10.00', (string) $order->total_amount, '被拒绝后总金额不变'); + } + + /** 历史订单未写商品金额时按「总额 - 附加」反推回写 */ + public function test_container_update_heals_legacy_product_amount(): void + { + $this->seedContainerConfig(); + $order = $this->makeOrderWithStatus(StoreOrderModel::STATUS_SUMMARIZED); + $order->update(['product_amount' => 0]); // 模拟历史数据 + + $this->actingAsSysUser(); + $this->putJson("/order/store/{$order->id}/container", ['box_num' => 2, 'tray_num' => 0]) + ->assertOk()->assertJsonPath('success', true); + + $order->refresh(); + $this->assertSame('10.00', (string) $order->product_amount, '反推回写商品金额'); + $this->assertSame('4.00', (string) $order->added_amount, '2×2.00'); + $this->assertSame('14.00', (string) $order->total_amount); + } + + /** 数量为负时校验失败 */ + public function test_container_update_validates_negative_numbers(): void + { + $this->seedContainerConfig(); + $order = $this->makeOrderWithStatus(StoreOrderModel::STATUS_SUMMARIZED); + + $this->actingAsSysUser(); + $this->putJson("/order/store/{$order->id}/container", ['box_num' => -1, 'tray_num' => 0]) + ->assertJsonPath('success', false) + ->assertJsonPath('msg', '周转框数量不能小于 0'); + } } diff --git a/web/api/order/store.ts b/web/api/order/store.ts index 822a1d5..d59157a 100644 --- a/web/api/order/store.ts +++ b/web/api/order/store.ts @@ -19,6 +19,15 @@ export async function updateOrderStatus(id: number, status: number) { }); } +/** 修改周转框/周转托盘数量(自动重算附加金额与订单总金额,仅已接单/采购中/配送中可改) */ +export async function updateOrderContainer(id: number, data: { box_num: number; tray_num: number }) { + return createAxios({ + url: `/order/store/${id}/container`, + method: 'put', + data, + }); +} + /** 待汇总预览(按商品聚合) */ export async function getOrderSummary() { return createAxios({ diff --git a/web/domain/iStoreOrder.ts b/web/domain/iStoreOrder.ts index 385c612..f559bc2 100644 --- a/web/domain/iStoreOrder.ts +++ b/web/domain/iStoreOrder.ts @@ -36,6 +36,14 @@ export default interface IStoreOrder { added_amount: string; box_num: number; tray_num: number; + /** 周转框单价(列表接口附加) */ + box_price?: string; + /** 周转托盘单价(列表接口附加) */ + tray_price?: string; + /** 周转框金额(列表接口附加) */ + box_amount?: string; + /** 周转托盘金额(列表接口附加) */ + tray_amount?: string; /** 0待接单 1已接单 2采购中 3配送中 4已完成 9已取消 */ status?: number; remark?: string; diff --git a/web/pages/order/store.tsx b/web/pages/order/store.tsx index 707a534..abe5bbe 100644 --- a/web/pages/order/store.tsx +++ b/web/pages/order/store.tsx @@ -3,7 +3,10 @@ import { Button, Descriptions, Drawer, + Form, + InputNumber, message, + Modal, Popconfirm, Space, Table, @@ -21,13 +24,17 @@ import type { import type IStoreOrder from '@/domain/iStoreOrder.ts'; import type { IStoreOrderItem } from '@/domain/iStoreOrder.ts'; import { STORE_ORDER_STATUS_MAP } from '@/domain/iStoreOrder.ts'; -import { getStoreOrder, updateOrderStatus } from '@/api/order/store.ts'; +import { getStoreOrder, updateOrderContainer, updateOrderStatus } from '@/api/order/store.ts'; import { getStoreOptions } from '@/api/customer/store.ts'; import type IStore from '@/domain/iStore.ts'; import AuthButton from '@/components/AuthButton'; +import {UnorderedListOutlined} from "@ant-design/icons"; const { Title, Text } = Typography; +/** 允许修改周转框/托盘数量的订单状态:已接单、采购中、配送中 */ +const CONTAINER_EDITABLE_STATUS = [1, 2, 3]; + /** * 状态流转合法路径:待汇总→配送中/取消;已汇总→配送中;配送中→已完成 */ @@ -51,6 +58,11 @@ const StoreOrderPage: React.FC = () => { const [detail, setDetail] = useState(null); const [detailLoading, setDetailLoading] = useState(false); + const [containerOpen, setContainerOpen] = useState(false); + const [containerOrder, setContainerOrder] = useState(null); + const [containerSaving, setContainerSaving] = useState(false); + const [containerForm] = Form.useForm<{ box_num: number; tray_num: number }>(); + useEffect(() => { getStoreOptions().then((res) => setStores(res.data.data ?? [])); }, []); @@ -73,6 +85,32 @@ const StoreOrderPage: React.FC = () => { await tableRef.current?.reload(); }; + const openContainer = (record: IStoreOrder) => { + setContainerOrder(record); + containerForm.setFieldsValue({ box_num: record.box_num, tray_num: record.tray_num }); + setContainerOpen(true); + }; + + const handleContainerSave = async (values: { box_num: number; tray_num: number }) => { + if (!containerOrder?.id) return; + setContainerSaving(true); + try { + await updateOrderContainer(containerOrder.id, values); + message.success('周转框/托盘数量已更新'); + setContainerOpen(false); + await tableRef.current?.reload(); + } finally { + setContainerSaving(false); + } + }; + + // 弹窗内实时预览:附加金额 = 框×单价 + 托盘×单价;订单总金额 = 商品金额 + 附加金额 + const watchBoxNum = Number(Form.useWatch('box_num', containerForm) ?? 0); + const watchTrayNum = Number(Form.useWatch('tray_num', containerForm) ?? 0); + const previewAdded = watchBoxNum * Number(containerOrder?.box_price ?? 0) + + watchTrayNum * Number(containerOrder?.tray_price ?? 0); + const previewTotal = Number(containerOrder?.product_amount ?? 0) + previewAdded; + const itemColumns: TableProps['columns'] = [ { title: '品名', dataIndex: 'product_name' }, { title: '规格', dataIndex: 'product_spec', render: (v) => v || '-' }, @@ -245,12 +283,29 @@ const StoreOrderPage: React.FC = () => { valueType: 'digit', hideInForm: true, }, - ]; - - const operateRender: XinTableProps['operateRender'] = (record) => [ - , + { + title: '操作栏', + dataIndex: 'operate', + align: 'center', + hideInForm: true, + hideInSearch: true, + render: (_, record) => ( + + + { + CONTAINER_EDITABLE_STATUS.includes(record.status ?? -1) ? ( + + + + ) : null + } + + ) + } ]; const tableProps: XinTableProps = { @@ -259,7 +314,7 @@ const StoreOrderPage: React.FC = () => { rowKey: 'id', accessName: 'order.store', tableRef, - operateRender, + operateShow: false, formProps: false, }; @@ -341,6 +396,50 @@ const StoreOrderPage: React.FC = () => { ) : null} + + {/* 修改周转框/托盘数量 */} + setContainerOpen(false)} + onOk={() => containerForm.submit()} + confirmLoading={containerSaving} + okText="保存" + destroyOnHidden + > +
+ 订单 {containerOrder?.order_no},保存后将按单价自动重算附加金额与订单总金额。 +
+
+ + + + + + +
+ +
+ 商品总金额:¥{containerOrder?.product_amount ?? '0.00'} +
+
+ 附加总金额: + ¥{previewAdded.toFixed(2)} +
+
+ 订单总金额: + ¥{previewTotal.toFixed(2)} +
+
+
); }; diff --git a/小程序端API文档.md b/小程序端API文档.md deleted file mode 100644 index 85d824d..0000000 --- a/小程序端API文档.md +++ /dev/null @@ -1,631 +0,0 @@ -# 订货采购系统 · 小程序端 API 文档 - -> 版本:V1.1 更新日期:2026-08-07 -> 适用:微信小程序门店端 / 供应商端;接口由后端 `app/Http/Controllers/Mini/` 提供(Laravel 12 + Sanctum)。 - -## 1. 通用说明 - -### 1.1 基础信息 - -| 项目 | 说明 | -|------|------| -| Base URL | `http://localhost:8000`(生产域名待定,通常为 HTTPS) | -| 数据格式 | JSON(请求/响应均 `Content-Type: application/json`) | -| 金额字段 | 后端统一 `decimal` 字符串返回(如 `"13.00"`),下单/购物车金额**一律服务端重算**,前端传的金额字段会被忽略 | - -### 1.2 认证方式 - -除「登录」接口外,全部接口需携带 `Authorization: Bearer `(登录接口返回的 token,Sanctum plainTextToken)。 - -```http -Authorization: Bearer 1|xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx -``` - -token 附带 `abilities: ["mini"]` 仅作来源标记;后端按 `users` guard 解析用户。 - -### 1.3 统一响应格式 - -成功: - -```json -{ "success": true, "data": { ... } } -``` - -成功带提示: - -```json -{ "success": true, "data": { ... }, "msg": "下单成功" } -``` - -失败(业务错误/验证错误均返回 HTTP 200,`success=false`): - -```json -{ "success": false, "msg": "尚未绑定门店,请联系客服处理", "showType": 1 } -``` - -分页数据统一结构(`data` 字段内): - -```json -{ - "success": true, - "data": { - "data": [ ... ], - "total": 35, - "pageSize": 10, - "current": 1 - } -} -``` - -### 1.4 角色前置校验 - -| 接口域 | 前置要求 | 未满足时提示 | -|--------|----------|--------------| -| 商品浏览(分类树/列表) | 仅需登录 | 无(未绑定门店也可浏览,`price` 为 null) | -| 购物车/订单/对账单/门店设置 | 用户 `type=门店(1)` 且已绑定正常门店 | 「尚未绑定门店,请联系客服处理」 | -| 供应商采购单 | 用户 `type=供应商(2)` 且已绑定正常供应商 | 「尚未绑定供应商,请联系客服处理」 | -| 商品价格展示 | 门店已设置客户等级(`store.level_id > 0`) | 未满足时 `price` 为 null,不影响浏览商品 | -| 加购/下单计价 | 门店已设置客户等级 | 「门店未设置客户等级,无法展示价格,请联系客服」 | - -> 登录后未绑定身份的用户 `type=0`(待绑定):**可浏览商品分类与列表(价格不可见)**,但加购、下单等购买行为仍要求绑定门店;注册流程按门店编码(storeCode)直接绑定门店,或由后台人工绑定。 - -### 1.5 价格体系 - -- 商品价格按「门店客户等级」展示,同一商品不同等级价格不同 -- 等级价格支持两种计价类型: - - **固定价**(`price_type=0`):直接存储实际单价 - - **成本百分比**(`price_type=1`):实际价 = 成本价 × (100 + 上浮百分点) / 100 -- 小程序端接口返回的 `price` 均为**换算后的实际价**;成本价为商业敏感数据,**不会**下发到小程序端 - ---- - -## 2. 认证 - -> 登录/注册均以 `wx.login()` 的 code 换取 openid 鉴权;注册流程同步绑定门店(storeCode)与手机号(phoneCode)。 - -### 2.1 微信登录 - -`POST /mini/auth/login` - -用 `wx.login()` 获取的 code 换 openid。**仅已注册用户可登录**,未注册用户须先走 2.2 注册。 - -请求参数: - -| 参数 | 类型 | 必填 | 说明 | -|------|------|------|------| -| code | string | 是 | `wx.login` 的临时凭证 | - -响应(`data`): - -| 字段 | 类型 | 说明 | -|------|------|------| -| token | string | Bearer 令牌(后续请求头携带) | -| user | object | 用户信息(字段见下表) | - -`user` 字段(user 表实际返回字段): - -| 字段 | 类型 | 说明 | -|------|------|------| -| id | int | 用户ID | -| username | string | 用户名(注册时生成 `wx_xxxx`) | -| nickname | string | 昵称(注册默认「微信用户」) | -| avatar | string | 头像 | -| phone | string | 手机号(未绑定为空) | -| store_id | int | 绑定门店ID(0 未绑定) | -| status | int | 1 正常 / 0 停用 | -| openid / unionid / email | string | 微信标识 / 邮箱 | -| last_login_at / created_at / updated_at | string\|null | 时间 | - -响应示例: - -```json -{ - "success": true, - "data": { - "token": "1|abc...", - "user": { - "id": 5, "username": "wx_66f1a2b3c4d5", "nickname": "微信用户", "avatar": "", - "phone": "", "store_id": 2, "status": 1, - "openid": "oXXXX", "unionid": "", "email": "", - "last_login_at": null, "created_at": "...", "updated_at": "..." - } - }, - "msg": "登录成功" -} -``` - -错误: -- `code` 缺失 → 「登录参数格式错误」 -- openid 未注册 → 「用户不存在!」 -- code 无效 / 微信未配置 → 「微信登录失败:...」 - -> 注意:登录接口**不校验用户状态**,被停用用户仍可登录(其余接口由 `currentUser()` 前置校验拦截)。 - -### 2.2 微信注册 - -`POST /mini/auth/register` - -新用户注册流程:code 换 openid → 校验未注册 → 按 storeCode 查门店 → phoneCode 换手机号 → 创建用户(绑定该门店)并返回 token。 - -请求参数: - -| 参数 | 类型 | 必填 | 说明 | -|------|------|------|------| -| code | string | 是 | `wx.login` 的临时凭证 | -| phoneCode | string | 是 | `wx.getPhoneNumber` 授权得到的 code | -| storeCode | string | 是 | 门店编码(后台门店管理维护) | - -响应(`data`):`token` + `user`,结构同 2.1,`msg` 为「登录成功」。 - -错误: -- `code`/`phoneCode` 缺失 → 「注册参数格式错误」;`storeCode` 缺失 → 「门店编码必须填写」 -- 该微信已注册 → 「你的微信已经注册,请直接登录!」 -- 门店编码不存在 → 「门店不存在!」 -- 手机号换取失败 → 「获取手机号失败:...」(会中断注册流程) - -> ⚠️ 现状说明:换取到的手机号写入 `mobile` 字段,但 user 表列为 `phone` 且不在模型 `$fillable` 白名单,**实际不会保存**(注册返回的 `user.phone` 为空)。注册即绑定门店(`store_id`),不存在「待绑定」用户状态。 - -### 2.3 当前用户信息 - -`GET /mini/auth/info`(需登录) - -响应(`data`):`user` 对象(字段同 2.1)+ 关联门店(**仅本接口**加载 `store`,登录/注册响应不含): - -| 字段 | 类型 | 说明 | -|------|------|------| -| user.store | object\|null | 绑定门店(未绑定为 null) | - -`user.store` 字段(store 表): - -| 字段 | 类型 | 说明 | -|------|------|------| -| id / name / code | - | 门店ID / 名称 / 编码 | -| level_id | int | 客户等级ID(0 未设置) | -| level | object\|null | 客户等级 `{ id, name }`(等级价展示依据) | -| contact / phone / address | string | 联系人 / 电话 / 地址 | -| payment_cycle_days | int | 回款周期天数 | -| status | int | 1 正常 / 0 停用 | - -错误:账号不存在(如已被删除)→ 「账号不存在」。 - -### 2.4 更新用户信息 - -`PUT miniauth/info`(需登录) - -⚠️ **路由注意**:代码中 `#[PutRoute('auth/info')]` 缺少前导 `/`,实际注册路由为 `miniauth/info`(未正确拼接为 `/mini/auth/info`)。 - -请求参数(`UserUpdateInfoRequest` 校验,全部必填): - -| 参数 | 类型 | 必填 | 说明 | -|------|------|------|------| -| username | string | 是 | 用户名 4-20 位 | -| nickname | string | 是 | 昵称 4-20 位 | -| gender | string | 是 | 性别 | -| email | string | 是 | 邮箱格式 | -| avatar_id | int | 是 | 头像ID | -| mobile | string | 是 | 手机号(`1[34578]` 开头 11 位) | - -> ⚠️ 现状说明:该接口当前实现存在多处问题,实际调用**无法正常使用**—— -> 1. 更新条件 `where('user_id', ...)`:user 表不存在 `user_id` 列(主键为 `id`); -> 2. `auth('user')` guard 未定义(`config/auth.php` 仅有 `sys_users` / `users`),会抛「Auth guard [user] is not defined」; -> 3. 更新成功返回 `success=false` 与提示「更新成功」(使用了 `$this->error()`)。 -> 建议后端修复后再联调。 - ---- - -## 3. 商品 - -### 3.1 商品分类树 - -`GET /mini/product/categories`(需登录) - -返回分类树,**仅包含有上架商品的分类及其全部祖先**(保证树结构完整)。未绑定门店也可调用。 - -响应(`data`):分类树数组,节点字段: - -| 字段 | 类型 | 说明 | -|------|------|------| -| id | int | 分类ID | -| parent_id | int | 父级分类ID(0 为顶级) | -| name | string | 分类名称 | -| children | array | 子分类(递归) | - -### 3.2 商品列表 - -`GET /mini/product/list?category_id=&keyword=&page=&pageSize=`(需登录) - -请求参数: - -| 参数 | 类型 | 必填 | 默认 | 说明 | -|------|------|------|------|------| -| category_id | int | 否 | - | 分类ID过滤 | -| keyword | string | 否 | - | 搜索品名/规格(模糊) | -| page | int | 否 | 1 | 页码 | -| pageSize | int | 否 | 10 | 每页条数 | - -响应(`data` 为分页结构),每项字段: - -| 字段 | 类型 | 说明 | -|------|------|------| -| id | int | 商品ID | -| category_id | int | 分类ID | -| supplier_id | int | 默认供应商ID | -| name | string | 品名 | -| spec | string | 规格/包规 | -| unit | string | 计价单位 | -| content | string | 商品图文详情(HTML) | -| **price** | string\|null | **当前门店等级的实际销售价**(未绑定门店或门店未设等级价为 null,此时仅可浏览不可购买) | -| images_arr | array | 商品图片数组(`{id, file_url, ...}`) | -| sort / shelf_life / stock / status | - | 排序 / 保质期 / 库存 / 状态(仅返回上架商品) | - -> 注意:只返回上架商品;`cost_price`、计价类型、上浮百分点等成本信息不会下发。 -> 未绑定门店(`type=0`)或门店未设客户等级时,列表正常返回、`price` 为 null——价格不可见,加购/下单仍由前置校验拦截。 - ---- - -## 4. 购物车 - -> 购物车为下单前的编辑容器,同商品重复加购自动合并数量;提交订单复用「5.1 下单」接口。 - -### 4.1 加购 - -`POST /mini/cart`(需登录 + 门店 + 客户等级) - -请求参数: - -| 参数 | 类型 | 必填 | 说明 | -|------|------|------|------| -| product_id | int | 是 | 商品ID(须上架且已设本等级价格) | -| quantity | number | 是 | 数量(>0,最多 99999999.99) | - -响应(`data`): - -```json -{ "id": 12, "quantity": "2.50" } -``` - -提示:`已加入购物车`。错误:商品未设本等级价格 → 「商品「xx」未设置您所在等级的价格,无法加购」;超上限 → 「该商品在购物车中的数量已达上限」。 - -### 4.2 购物车列表 - -`GET /mini/cart`(需登录 + 门店) - -响应(`data`): - -| 字段 | 类型 | 说明 | -|------|------|------| -| items | array | 购物车项(倒序) | -| total_count | int | 总项数 | -| total_quantity | string | 可购项总数量 | -| total_amount | string | 可购项总金额 | - -items 每项: - -| 字段 | 类型 | 说明 | -|------|------|------| -| id | int | 购物车项ID | -| product_id | int | 商品ID | -| name / spec / unit | string | 商品快照 | -| image | string | 商品首图 URL | -| **price** | string\|null | **当前等级实际价**(商品下架或未设等级价为 null) | -| quantity | string | 数量 | -| amount | string\|null | 金额 = price × quantity(不可购为 null) | -| status | int | 1 可购 / 0 商品下架、缺失或未设等级价 | - -### 4.3 修改数量 - -`PUT /mini/cart/{id}`(需登录) - -请求参数:`quantity`(number,必填,>0)。 - -响应:`{ id, quantity }`,提示「已修改数量」。 - -### 4.4 删除单项 - -`DELETE /mini/cart/{id}`(需登录) - -响应:`success=true`,提示「已删除」;不存在 → 「购物车项不存在」。 - -### 4.5 清空购物车 - -`DELETE /mini/cart`(需登录) - -仅清空当前用户;响应:`success=true`,提示「购物车已清空」。 - ---- - -## 5. 门店订单 - -### 5.1 下单 - -`POST /mini/order`(需登录 + 门店 + 客户等级) - -请求参数: - -| 参数 | 类型 | 必填 | 说明 | -|------|------|------|------| -| items | array | 是 | 订单明细(至少 1 行) | -| items[].product_id | int | 是 | 商品ID(须上架) | -| items[].quantity | number | 是 | 数量(>0) | -| remark | string | 否 | 订单备注(≤255 字符) | - -> 金额不接受前端传入:服务端按商品当前等级**实际价**逐行快照并重算 `amount` 与 `total_amount`。 - -响应(`data`): - -```json -{ "id": 23, "order_no": "SO202608060001", "total_amount": "39.00" } -``` - -提示:「下单成功」。错误示例:存在已下架商品 → 「存在已下架或不存在的商品,请刷新后重试」;未设等级价 → 「商品「xx」未设置您所在等级的价格,无法下单」。 - -### 5.2 历史订单 - -`GET /mini/order?status=&page=&pageSize=`(需登录 + 门店,强制本店隔离) - -请求参数: - -| 参数 | 类型 | 必填 | 默认 | 说明 | -|------|------|------|------|------| -| status | int | 否 | - | 0 待汇总 / 1 已汇总 / 2 配送中 / 3 已完成 / 9 已取消 | -| page / pageSize | - | 否 | 1 / 10 | 分页 | - -响应(`data` 分页结构),订单字段: - -| 字段 | 类型 | 说明 | -|------|------|------| -| id | int | 订单ID | -| order_no | string | 单号(SO + 日期 + 序列) | -| order_date | string | 订货日期(Y-m-d) | -| total_quantity / total_amount | string | 总数量 / 总金额 | -| status | int | 状态(见上) | -| remark | string | 备注 | - -### 5.3 周期汇总 - -`GET /mini/order/summary?period=day|week|month`(需登录 + 门店) - -请求参数:`period`(day/week/month,默认 month)。 - -响应(`data`): - -```json -{ - "period": "month", - "groups": [ - { "period_label": "2026-07", "total_amount": "1280.50", "total_quantity": "86.00", "order_count": 12 } - ] -} -``` - -> `period_label` 格式:day=`Y-m-d`、week=`Y-W+周数`、month=`Y-m`;不含已取消订单;最多返回 50 组。 - -### 5.4 订单详情 - -`GET /mini/order/{id}`(需登录 + 门店,校验本店归属) - -响应(`data`):订单对象 + `items` 数组(明细字段见下表)。 - -明细字段: - -| 字段 | 类型 | 说明 | -|------|------|------| -| id / order_id | int | 明细ID / 订单ID | -| product_id / product_name / product_spec | - | 商品快照 | -| price | string | 下单时等级实际价快照 | -| quantity / weight | string | 数量 / 称重(默认 0) | -| amount | string | 金额 = price × quantity | -| remark | string | 行备注 | - -### 5.5 取消订单 - -`PUT /mini/order/{id}/cancel`(需登录 + 门店) - -仅「待汇总(0)」可取消;响应提示「订单已取消」;非待汇总 → 「仅待汇总的订单可以取消」。 - ---- - -## 6. 对账单(门店自助) - -### 6.1 对账单列表 - -`GET /mini/statement?page=&pageSize=`(需登录 + 门店,仅本店) - -响应(`data` 分页结构),对账单字段: - -| 字段 | 类型 | 说明 | -|------|------|------| -| id | int | 对账单ID | -| statement_no | string | 单号(ST + 日期 + 序列) | -| period_start / period_end | string | 对账周期 | -| total_amount | string | 总金额 | -| payment_cycle_days | int | 生成时快照的回款周期 | -| settlement_date | string\|null | 应结算日期 = 周期结束 + 回款周期天 | -| status | int | 0 待对账 / 1 已对账 / 2 已结算 | -| reconciled_at / settled_at | string\|null | 对账 / 结算时间 | -| remark | string | 备注 | - -### 6.2 生成对账单 - -`POST /mini/statement/generate`(需登录 + 门店) - -请求参数: - -| 参数 | 类型 | 必填 | 说明 | -|------|------|------|------| -| period_start | string | 是 | 周期开始(Y-m-d) | -| period_end | string | 是 | 周期结束(Y-m-d,不早于开始) | - -响应(`data`):`{ id, statement_no, total_amount, settlement_date }`,提示「对账单已生成」。 - -> 快照当前回款周期计算结算日期。业务约束:周期内本店无订单 → 「周期内本店无订单数据,无法生成对账单」;周期内订单均已生成过对账单 → 「周期内的订单明细均已生成过对账单」。 - -### 6.3 对账单详情 - -`GET /mini/statement/{id}`(需登录 + 门店,校验归属) - -响应(`data`):对账单对象 + `items` 数组,明细字段: - -| 字段 | 类型 | 说明 | -|------|------|------| -| order_id / order_item_id | int | 源订单 / 源明细ID | -| product_id / product_name | - | 商品快照 | -| price | string | 单价 | -| quantity / weight / amount | string | 数量 / 称重 / 金额 | -| is_reconciled | int | 0 未对账 / 1 已对账 | -| store_remark | string | 门店备注 | - -### 6.4 导出对账单 - -`GET /mini/statement/{id}/export?format=xlsx|pdf`(需登录 + 门店,校验归属) - -- `format` 默认 `xlsx`(支持 `xlsx` / `pdf`) -- 返回文件流(附件下载,含中文文件名),非 JSON - ---- - -## 7. 门店设置 - -### 7.1 门店详情 - -`GET /mini/store/info`(需登录 + 门店) - -用于门店信息编辑页回显。响应(`data`): - -| 字段 | 类型 | 说明 | -|------|------|------| -| id | int | 门店 ID | -| name | string | 门店名称(只读,后台维护) | -| code | string | 门店编码(只读) | -| contact | string | 联系人 | -| phone | string | 联系电话 | -| address | string | 地址 | -| payment_cycle_days | int | 回款周期天数(只读,后台维护) | - -### 7.2 修改门店信息 - -`PUT /mini/store/info`(需登录 + 门店) - -请求参数: - -| 参数 | 类型 | 必填 | 说明 | -|------|------|------|------| -| contact | string | 否 | 联系人(≤50 字符) | -| phone | string | 否 | 联系电话(≤20 字符) | -| address | string | 否 | 地址(≤255 字符) | - -响应(`data`):`{ "contact": "张三", "phone": "13800138000", "address": "幸福路 1 号" }`,提示「门店信息已更新」。 - -> 仅支持修改联系人 / 电话 / 地址(白名单更新,提交其他字段将被忽略);门店名称、编码、回款周期由后台维护。回款周期影响后续生成对账单的 `settlement_date`(周期结束 + 回款周期天)。 - ---- - -## 8. 通知 - -### 8.1 通知列表 - -`GET /mini/notice?page=&pageSize=`(需登录) - -返回本人通知 + 全员广播(本人已读的广播自动隐藏)。响应(`data` 分页结构 + 附加字段): - -| 字段 | 类型 | 说明 | -|------|------|------| -| unread_count | int | 未读总数 | -| 分页内字段 | - | 标准分页结构 | - -通知字段: - -| 字段 | 类型 | 说明 | -|------|------|------| -| id | int | 通知ID | -| type | string | `order` 订单 / `price` 价格变更 / `system` 系统 | -| title / content | string | 标题 / 内容 | -| data | object | 附加数据(价格变更通知含 `product_ids`、`level_ids`) | -| is_read | int | 0 未读 / 1 已读 | -| read_at | string\|null | 已读时间 | - -### 8.2 标记已读 - -`PUT /mini/notice/{id}/read`(需登录) - -- 个人通知:直接标记已读 -- 全员广播:复制一条本人专属已读记录(原广播对他人仍为未读) - -响应:`success=true`;通知不存在 → 「通知不存在」。 - ---- - -## 9. 供应商端 - -### 9.1 收到的采购单 - -`GET /mini/supplier/purchases?page=&pageSize=`(需登录 + 供应商) - -返回**含本供应商已发送明细**(`is_sent=1`)的采购单(去重,按日期倒序)。 - -响应(`data` 分页结构),采购单字段: - -| 字段 | 类型 | 说明 | -|------|------|------| -| id | int | 采购单ID | -| purchase_no | string | 单号(PO + 日期 + 序列) | -| purchase_date | string | 采购日期 | -| status | int | 0 待发送 / 1 部分发送 / 2 全部发送 / 3 已完成 | -| total_quantity / estimate_amount / actual_amount | string | 总数量 / 估算金额 / 实际金额 | -| remark | string | 备注 | - -### 9.2 采购单明细 - -`GET /mini/supplier/purchases/{id}`(需登录 + 供应商) - -仅返回**本供应商且已发送**的明细行。响应(`data`): - -```json -{ - "id": 3, "purchase_no": "PO202608060001", "purchase_date": "2026-08-06", "remark": "", - "items": [ - { "id": 11, "product_id": 2, "product_name": "大白菜", "product_spec": "10斤/箱", - "price": "4.00", "quantity": "10.00", "weight": "0.000", "amount": "40.00", - "sort": 1, "is_sent": 1, "sent_at": "...", "supplier_confirmed_at": null } - ] -} -``` - -无本供应商明细 → 「该采购单无贵司的采购明细」。 - -### 9.3 确认接单 - -`PUT /mini/supplier/purchases/{id}/confirm`(需登录 + 供应商) - -批量记录本供应商全部已发送明细的 `supplier_confirmed_at`(幂等,已确认的跳过)。 - -响应(`data`):`{ "confirmed": 2 }`(本次新确认条数),提示「已确认接单」。 - ---- - -## 10. 状态字典汇总 - -| 枚举 | 值 | 含义 | -|------|-----|------| -| 用户类型 user.type | 0 / 1 / 2 | 待绑定 / 门店 / 供应商 | -| 门店订单 status | 0 / 1 / 2 / 3 / 9 | 待汇总 / 已汇总 / 配送中 / 已完成 / 已取消 | -| 采购单 status | 0 / 1 / 2 / 3 | 待发送 / 部分发送 / 全部发送 / 已完成 | -| 采购明细 is_sent | 0 / 1 | 未发送 / 已发送 | -| 对账单 status | 0 / 1 / 2 | 待对账 / 已对账 / 已结算 | -| 对账明细 is_reconciled | 0 / 1 | 未对账 / 已对账 | -| 通知 type | order / price / system | 订单 / 价格变更 / 系统 | -| 通知 is_read | 0 / 1 | 未读 / 已读 | -| 商品 status | 0 / 1 | 下架 / 上架 | -| 门店/供应商 status | 0 / 1 | 停用 / 正常 | - -## 11. 常见错误提示 - -| 提示语 | 触发场景 | -|--------|----------| -| 尚未绑定门店,请联系客服处理 | 购物车/订单/对账单等接口但用户未绑定门店(商品浏览不受限) | -| 尚未绑定供应商,请联系客服处理 | 供应商端接口但用户未绑定供应商 | -| 门店未设置客户等级,无法展示价格,请联系客服 | 门店 `level_id=0`(购物车/下单)——商品列表不报错,返回 `price=null` | -| 商品「xx」未设置您所在等级的价格,无法下单 | 下单商品缺本等级价格 | -| 存在已下架或不存在的商品,请刷新后重试 | 下单商品已下架 | -| 账号不存在或已被停用 | token 用户被停用 | -| 账号已被停用,请联系客服 | 登录时账号被停用 | diff --git a/项目需求规划书.md b/项目需求规划书.md index 4b44989..8b00d43 100644 --- a/项目需求规划书.md +++ b/项目需求规划书.md @@ -104,12 +104,3 @@ F3 页面:前端UI页面整体搭建 3、对账管理模块(含结算表生成、下载存档) 4、权限管理系统 5、导出功能,支持Excel格式导出 - -阶段四:测试与上线(5天) -1、功能测试、兼容性测试、性能测试 -2、小程序提交微信审核 -3、部署上线 -六、风险与注意事项 -1、特殊业务处理:周转柜、周转托盘、调货、售后、物流是否涉及重大变动,需业务方明确规则,否则影响系统设计和与开发进度 -2、价格实时性:后台调价后需确保门店端及时同步 -3、数据安全:对账单涉及金额数据,需做好权限隔离,确保门店仅可见自身数据