diff --git a/.phpunit.result.cache b/.phpunit.result.cache index 9afafb7..dab5692 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,"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.005,"Tests\\Feature\\ProductPriceTest::test_product_list_returns_price_for_store_level":0.017,"Tests\\Feature\\ProductPriceTest::test_store_without_level_is_rejected":0.007,"Tests\\Feature\\ProductPriceTest::test_batch_price_updates_and_notifies_affected_stores":0.021,"Tests\\Feature\\ProductPriceTest::test_price_matrix_structure":0.014,"Tests\\Feature\\StoreOrderTest::test_place_order_snapshots_level_price_and_recalculates":0.013,"Tests\\Feature\\StoreOrderTest::test_client_supplied_amount_is_ignored":0.012,"Tests\\Feature\\StoreOrderTest::test_product_without_level_price_rejected":0.017,"Tests\\Feature\\StoreOrderTest::test_cancel_only_pending_orders":0.018,"Tests\\Feature\\StoreOrderTest::test_summarized_order_cannot_be_cancelled":0.02,"Tests\\Feature\\StoreOrderTest::test_order_data_isolated_between_stores":0.021,"Tests\\Feature\\PurchaseGenerateTest::test_generate_aggregates_orders_by_product":0.043,"Tests\\Feature\\PurchaseGenerateTest::test_generate_writes_back_order_status":0.037,"Tests\\Feature\\PurchaseGenerateTest::test_generate_without_pending_orders_fails":0.006,"Tests\\Feature\\PurchaseGenerateTest::test_generate_is_idempotent":0.035,"Tests\\Feature\\AllocationTest::test_allocation_conserves_amount_with_tail_correction":0.194,"Tests\\Feature\\AllocationTest::test_allocation_follows_order_ratio":0.032,"Tests\\Feature\\AllocationTest::test_allocation_is_idempotent":0.041,"Tests\\Feature\\AllocationTest::test_allocation_rejected_without_actual_amount":0.035,"Tests\\Feature\\DebugGenerateTest::test_debug_generate":0.047,"Tests\\Feature\\ReconciliationTest::test_build_creates_reconciliation_items":0.062,"Tests\\Feature\\ReconciliationTest::test_build_filters_by_supplier":0.062,"Tests\\Feature\\ReconciliationTest::test_update_item_recalculates_diff_and_header":0.056,"Tests\\Feature\\ReconciliationTest::test_toggle_reconciled_flag":0.064,"Tests\\Feature\\ReconciliationTest::test_settle_creates_settlements_per_store":0.05,"Tests\\Feature\\StatementTest::test_generate_snapshots_payment_cycle":0.019,"Tests\\Feature\\StatementTest::test_statement_isolated_between_stores":0.028,"Tests\\Feature\\StatementTest::test_generate_excludes_cancelled_and_used_items":0.033,"Tests\\Feature\\MiniAuthTest::test_login_creates_user_and_issues_token":0.056,"Tests\\Feature\\MiniAuthTest::test_disabled_user_cannot_login":0.006,"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.011,"Tests\\Feature\\MiniAuthTest::test_mini_token_cannot_access_admin_api":0.008,"Tests\\Feature\\ExportTest::test_export_xlsx_with_encoded_chinese_filename":0.163,"Tests\\Feature\\ExportTest::test_export_category_filters_to_vegetables":0.03,"Tests\\Feature\\ExportTest::test_export_pdf":1.831,"Tests\\Feature\\ExportTest::test_export_invalid_format_rejected":0.031,"Tests\\Feature\\ExportTest::test_export_requires_export_permission":0.032,"Tests\\Unit\\ExampleTest::test_that_true_is_true":0.001,"Tests\\Feature\\MiniAuthTest::test_login_fails_when_wechat_rejects_code":0.009,"Tests\\Feature\\CartTest::test_add_to_cart_succeeds":0.013,"Tests\\Feature\\CartTest::test_duplicate_add_merges_quantity":0.011,"Tests\\Feature\\CartTest::test_off_shelf_product_rejected":0.009,"Tests\\Feature\\CartTest::test_soft_deleted_product_rejected":0.009,"Tests\\Feature\\CartTest::test_product_without_level_price_rejected":0.009,"Tests\\Feature\\CartTest::test_unbound_user_rejected":0.012,"Tests\\Feature\\CartTest::test_store_without_level_rejected":0.01,"Tests\\Feature\\CartTest::test_list_with_prices_amounts_and_totals":0.013,"Tests\\Feature\\CartTest::test_list_marks_off_shelf_item_unbuyable":0.018,"Tests\\Feature\\CartTest::test_empty_cart_returns_empty_items":0.008,"Tests\\Feature\\CartTest::test_update_quantity":0.011,"Tests\\Feature\\CartTest::test_update_other_users_item_rejected":0.013,"Tests\\Feature\\CartTest::test_update_missing_item_rejected":0.011,"Tests\\Feature\\CartTest::test_invalid_quantity_rejected":0.012,"Tests\\Feature\\CartTest::test_delete_item":0.011,"Tests\\Feature\\CartTest::test_delete_other_users_item_rejected":0.012,"Tests\\Feature\\CartTest::test_clear_only_own_cart":0.022,"Tests\\Feature\\CartTest::test_unauthenticated_returns_401":0.005,"Tests\\Feature\\CartTest::test_cart_isolated_between_users":0.011,"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.002,"Tests\\Feature\\ProductCostPriceTest::test_admin_list_contains_cost_price_and_actual_price":0.02,"Tests\\Feature\\ProductCostPriceTest::test_mini_list_returns_actual_price_without_cost_price":0.015,"Tests\\Feature\\ProductCostPriceTest::test_create_product_with_cost_and_percent_pricing":0.021,"Tests\\Feature\\ProductCostPriceTest::test_create_percent_row_without_percent_fails":0.015,"Tests\\Feature\\ProductCostPriceTest::test_update_switches_fixed_to_percent":0.015,"Tests\\Feature\\ProductCostPriceTest::test_batch_price_cost_only_notifies_percent_level_stores":0.026,"Tests\\Feature\\ProductCostPriceTest::test_batch_price_percent_row_update":0.013,"Tests\\Feature\\ProductCostPriceTest::test_batch_price_mixed_row_updates_cost_and_level":0.015,"Tests\\Feature\\ProductCostPriceTest::test_batch_price_empty_row_rejected":0.013,"Tests\\Feature\\ProductCostPriceTest::test_price_matrix_includes_cost_and_percent_columns":0.013,"Tests\\Feature\\ProductCostPriceTest::test_mini_order_uses_actual_price_for_percent_row":0.016,"Tests\\Feature\\ProductCostPriceTest::test_mini_cart_uses_actual_price_for_percent_row":0.016,"Tests\\Feature\\PurchaseGenerateTest::test_generate_uses_min_actual_price_across_types":0.02,"Tests\\Feature\\ProductPriceTest::test_store_without_level_sees_products_without_price":0.012,"Tests\\Feature\\ProductPriceTest::test_unbound_user_can_browse_products_without_price_but_cannot_cart":0.016,"Tests\\Feature\\MiniAuthTest::test_login_issues_token_for_registered_user":0.038,"Tests\\Feature\\MiniAuthTest::test_login_rejects_unregistered_openid":0.003,"Tests\\Feature\\MiniAuthTest::test_register_binds_store_and_issues_token":0.04,"Tests\\Feature\\MiniAuthTest::test_register_rejects_unknown_store":0.004,"Tests\\Feature\\MiniAuthTest::test_register_rejects_duplicate_openid":0.004,"Tests\\Feature\\MiniStoreTest::test_info_returns_bound_store":0.011,"Tests\\Feature\\MiniStoreTest::test_update_info":0.011,"Tests\\Feature\\MiniStoreTest::test_update_info_ignores_readonly_fields":0.011,"Tests\\Feature\\MiniStoreTest::test_requires_bound_store":0.012,"Tests\\Feature\\MiniStoreTest::test_update_info_validates_length":0.019,"Tests\\Feature\\StoreOrderTest::test_container_update_recalculates_amounts":0.021,"Tests\\Feature\\StoreOrderTest::test_container_update_allowed_in_editable_statuses":0.035,"Tests\\Feature\\StoreOrderTest::test_container_update_rejected_in_other_statuses":0.034,"Tests\\Feature\\StoreOrderTest::test_container_update_heals_legacy_product_amount":0.028,"Tests\\Feature\\StoreOrderTest::test_container_update_validates_negative_numbers":0.024,"Tests\\Feature\\PurchaseGenerateTest::test_generate_without_accepted_orders_fails":0.024,"Tests\\Feature\\PurchaseGenerateTest::test_generate_with_selected_order_ids":0.04}} \ 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,"Tests\\Feature\\StoreOrderItemTest::test_sync_item_rejected_when_product_deleted":7,"Tests\\Feature\\StoreOrderItemTest::test_order_list_search_by_product_name":7},"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.015,"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.014,"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.019,"Tests\\Feature\\PurchaseGenerateTest::test_generate_without_pending_orders_fails":0.006,"Tests\\Feature\\PurchaseGenerateTest::test_generate_is_idempotent":0.022,"Tests\\Feature\\AllocationTest::test_allocation_conserves_amount_with_tail_correction":0.081,"Tests\\Feature\\AllocationTest::test_allocation_follows_order_ratio":0.021,"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.029,"Tests\\Feature\\ReconciliationTest::test_build_filters_by_supplier":0.032,"Tests\\Feature\\ReconciliationTest::test_update_item_recalculates_diff_and_header":0.043,"Tests\\Feature\\ReconciliationTest::test_toggle_reconciled_flag":0.039,"Tests\\Feature\\ReconciliationTest::test_settle_creates_settlements_per_store":0.031,"Tests\\Feature\\StatementTest::test_generate_snapshots_payment_cycle":0.012,"Tests\\Feature\\StatementTest::test_statement_isolated_between_stores":0.018,"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.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.005,"Tests\\Feature\\MiniAuthTest::test_mini_token_cannot_access_admin_api":0.004,"Tests\\Feature\\ExportTest::test_export_xlsx_with_encoded_chinese_filename":0.054,"Tests\\Feature\\ExportTest::test_export_category_filters_to_vegetables":0.018,"Tests\\Feature\\ExportTest::test_export_pdf":1.303,"Tests\\Feature\\ExportTest::test_export_invalid_format_rejected":0.018,"Tests\\Feature\\ExportTest::test_export_requires_export_permission":0.018,"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.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.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.01,"Tests\\Feature\\CartTest::test_list_marks_off_shelf_item_unbuyable":0.012,"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.015,"Tests\\Feature\\CartTest::test_unauthenticated_returns_401":0.002,"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.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.012,"Tests\\Feature\\ProductCostPriceTest::test_batch_price_percent_row_update":0.008,"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.009,"Tests\\Feature\\ProductCostPriceTest::test_mini_order_uses_actual_price_for_percent_row":0.011,"Tests\\Feature\\ProductCostPriceTest::test_mini_cart_uses_actual_price_for_percent_row":0.013,"Tests\\Feature\\PurchaseGenerateTest::test_generate_uses_min_actual_price_across_types":0.015,"Tests\\Feature\\ProductPriceTest::test_store_without_level_sees_products_without_price":0.01,"Tests\\Feature\\ProductPriceTest::test_unbound_user_can_browse_products_without_price_but_cannot_cart":0.012,"Tests\\Feature\\MiniAuthTest::test_login_issues_token_for_registered_user":0.009,"Tests\\Feature\\MiniAuthTest::test_login_rejects_unregistered_openid":0.002,"Tests\\Feature\\MiniAuthTest::test_register_binds_store_and_issues_token":0.01,"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.005,"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.013,"Tests\\Feature\\StoreOrderTest::test_container_update_recalculates_amounts":0.041,"Tests\\Feature\\StoreOrderTest::test_container_update_allowed_in_editable_statuses":0.019,"Tests\\Feature\\StoreOrderTest::test_container_update_rejected_in_other_statuses":0.02,"Tests\\Feature\\StoreOrderTest::test_container_update_heals_legacy_product_amount":0.015,"Tests\\Feature\\StoreOrderTest::test_container_update_validates_negative_numbers":0.018,"Tests\\Feature\\PurchaseGenerateTest::test_generate_without_accepted_orders_fails":0.014,"Tests\\Feature\\PurchaseGenerateTest::test_generate_with_selected_order_ids":0.025,"Tests\\Feature\\StoreOrderItemTest::test_place_order_snapshots_full_product_info":0.048,"Tests\\Feature\\StoreOrderItemTest::test_mini_detail_hides_cost_price":0.011,"Tests\\Feature\\StoreOrderItemTest::test_admin_detail_shows_cost_price_and_supplier":0.015,"Tests\\Feature\\StoreOrderItemTest::test_update_item_recalculates_order_totals":0.014,"Tests\\Feature\\StoreOrderItemTest::test_update_item_rejected_when_completed_or_cancelled":0.016,"Tests\\Feature\\StoreOrderItemTest::test_update_item_validates_negative_price":0.012,"Tests\\Feature\\StoreOrderItemTest::test_sync_item_updates_snapshot_from_product":0.013,"Tests\\Feature\\StoreOrderItemTest::test_sync_item_recalculates_percent_price_with_latest_cost":0.013,"Tests\\Feature\\StoreOrderItemTest::test_sync_item_rejected_when_product_deleted":0.012,"Tests\\Feature\\StoreOrderItemTest::test_order_list_search_by_product_name":0.017}} \ No newline at end of file diff --git a/app/Http/Controllers/Mini/OrderController.php b/app/Http/Controllers/Mini/OrderController.php index ca863c0..4acfa7d 100644 --- a/app/Http/Controllers/Mini/OrderController.php +++ b/app/Http/Controllers/Mini/OrderController.php @@ -83,12 +83,19 @@ class OrderController extends BaseMiniController $rows[] = [ 'store_id' => $store->id, 'product_id' => $productId, + 'category_id' => (int) $product->category_id, + 'supplier_id' => (int) $product->supplier_id, 'product_name' => $product->name, 'product_spec' => $product->spec, + 'unit' => (string) $product->unit, 'price' => $price, + 'image_ids' => implode(',', (array) $product->image_ids), + 'content' => (string) $product->content, + 'shelf_life' => (int) $product->shelf_life, 'quantity' => $quantity, 'weight' => 0, 'amount' => $amount, + 'cost_price' => (string) $product->cost_price, 'remark' => '', 'created_at' => $now, 'updated_at' => $now, diff --git a/app/Http/Controllers/Order/StoreOrderController.php b/app/Http/Controllers/Order/StoreOrderController.php index 0092b22..fae22ec 100644 --- a/app/Http/Controllers/Order/StoreOrderController.php +++ b/app/Http/Controllers/Order/StoreOrderController.php @@ -5,6 +5,7 @@ namespace App\Http\Controllers\Order; use App\Exceptions\RepositoryException; use App\Models\NoticeModel; use App\Models\ProductModel; +use App\Models\ProductPriceModel; use App\Models\StoreOrderItemModel; use App\Models\StoreOrderModel; use App\Models\UserModel; @@ -15,9 +16,10 @@ use Modules\AnnoRoute\Attribute\GetRoute; use Modules\AnnoRoute\Attribute\PutRoute; use Modules\AnnoRoute\Attribute\RequestAttribute; use Modules\Common\Http\Controllers\BaseController; +use Modules\SystemTool\Models\SysFileModel; /** - * 门店订单管理(订单只读 + 状态管理;创建/取消在小程序端) + * 门店订单管理(订单只读 + 状态管理 + 明细修改/同步;创建/取消在小程序端) */ #[RequestAttribute('/order/store', 'order.store')] class StoreOrderController extends BaseController @@ -32,6 +34,14 @@ class StoreOrderController extends BaseController StoreOrderModel::STATUS_CANCELLED => '已取消', ]; + /** 明细可编辑状态:待接单、已接单、采购中、配送中(已完成/已取消锁定) */ + private const array ITEM_EDITABLE_STATUS = [ + StoreOrderModel::STATUS_PENDING, + StoreOrderModel::STATUS_SUMMARIZED, + StoreOrderModel::STATUS_DELIVERING, + StoreOrderModel::STATUS_DISTRIBUTION, + ]; + protected array $searchField = [ 'store_id' => '=', 'status' => '=', @@ -39,7 +49,7 @@ class StoreOrderController extends BaseController 'order_date' => 'betweenDate', ]; - /** 订单列表 */ + /** 订单列表(支持按包含的商品名称搜索 ?product_name=) */ #[GetRoute(authorize: 'query')] public function query(Request $request): JsonResponse { @@ -47,14 +57,36 @@ class StoreOrderController extends BaseController $pageSize = $params['pageSize'] ?? 10; $query = StoreOrderModel::query()->with([ 'store:id,name,address,contact,phone', - 'items:id,order_id,product_id,product_name,product_spec,price,quantity,amount', - 'items.product' + 'items:id,order_id,product_id,product_name,product_spec,unit,price,quantity,amount,image_ids', ]); + + // 按包含的商品名称搜索:任一明细品名包含关键字即命中 + $productName = trim((string) ($params['product_name'] ?? '')); + if ($productName !== '') { + $keyword = '%' . str_replace('%', '\%', $productName) . '%'; + $query->whereHas('items', static function ($itemQuery) use ($keyword) { + $itemQuery->where('product_name', 'like', $keyword); + }); + } + $data = $this->buildSearch($params, $query) ->orderBy('order_date', 'desc') ->orderBy('id', 'desc') ->paginate($pageSize) ->toArray(); + + // Paginator::toArray() 的 data 仍为模型,先显式转数组(明细/关系一并转换) + $data['data'] = array_map( + static fn ($order) => is_object($order) ? $order->toArray() : $order, + $data['data'] + ); + + // 明细封面图:从快照 image_ids 批量解析(不再关联商品档案表) + foreach ($data['data'] as &$order) { + $this->resolveItemImages($order['items']); + } + unset($order); + $box_amount = site_config('services.box_amount'); $tray_amount = site_config('services.tray_amount'); foreach ($data['data'] as &$item) { @@ -62,21 +94,13 @@ class StoreOrderController extends BaseController $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) { - foreach ($item['items'] as &$product) { - if($product['product'] && $product['product']['images_arr'] > 0) { - $product['image'] = $product['product']['images_arr'][0]['preview_url'] ?? ''; - unset($product['product']); - } - } - } } return $this->success($data); } /** * 已接单预览:聚合所有已接单订单明细(按商品分组), - * 供生成采购单前确认(C1 前置) + * 供生成采购单前确认(C1 前置);单位取明细快照 */ #[GetRoute('/summary', 'query')] public function summary(): JsonResponse @@ -85,6 +109,7 @@ class StoreOrderController extends BaseController ->select('product_id') ->selectRaw('MAX(product_name) as product_name') ->selectRaw('MAX(product_spec) as product_spec') + ->selectRaw('MAX(unit) as unit') ->selectRaw('SUM(quantity) as total_quantity') ->selectRaw('COUNT(DISTINCT store_id) as store_count') ->whereHas('order', function ($query) { @@ -95,26 +120,205 @@ class StoreOrderController extends BaseController ->get() ->toArray(); - // 补充计价单位(商品档案,含已下架/软删除) - $units = ProductModel::withTrashed() - ->whereIn('id', array_column($rows, 'product_id')) - ->pluck('unit', 'id'); - foreach ($rows as &$row) { - $row['unit'] = $units[$row['product_id']] ?? ''; + // 快照无单位的历史明细兜底:从商品档案(含已下架/软删除)补齐 + $emptyUnitProductIds = array_column(array_filter( + $rows, + static fn (array $row) => ($row['unit'] ?? '') === '' + ), 'product_id'); + if ($emptyUnitProductIds !== []) { + $units = ProductModel::withTrashed() + ->whereIn('id', $emptyUnitProductIds) + ->pluck('unit', 'id'); + foreach ($rows as &$row) { + if (($row['unit'] ?? '') === '') { + $row['unit'] = $units[$row['product_id']] ?? ''; + } + } } return $this->success($rows); } - /** 订单详情:订单头 + 明细(含商品快照) */ + /** 订单详情:订单头 + 明细(含商品快照、首图;后台侧成本价可见、附供应商名) */ #[GetRoute(route: '/{id}', authorize: 'query', where: ['id' => '[0-9]+'])] public function detail(int $id): JsonResponse { - $order = StoreOrderModel::with(['store:id,name', 'items'])->find($id); + $order = StoreOrderModel::with([ + 'store:id,name,address,contact,phone', + 'items' => static fn ($query) => $query->with('supplier:id,name'), + ])->find($id); if (empty($order)) { throw new RepositoryException('订单不存在'); } - return $this->success($order->toArray()); + + // 成本价默认对序列化隐藏(防泄漏到小程序端),后台恢复可见 + $order->items->each->makeVisible('cost_price'); + + $data = $order->toArray(); + + // 明细首图 + 附加金额(与列表接口一致) + $this->resolveItemImages($data['items']); + $boxAmount = site_config('services.box_amount'); + $trayAmount = site_config('services.tray_amount'); + $data['box_price'] = number_format($boxAmount, 2); + $data['tray_price'] = number_format($trayAmount, 2); + $data['box_amount'] = number_format($boxAmount * $data['box_num'], 2); + $data['tray_amount'] = number_format($trayAmount * $data['tray_num'], 2); + + return $this->success($data); + } + + /** + * 明细首图解析:从快照 image_ids 批量解析文件 URL,未找到时置空字符串(列表/详情共用) + * + * @param array> $items 订单明细数组(引用修改) + */ + private function resolveItemImages(array &$items): void + { + $fileIds = []; + foreach ($items as $line) { + foreach ((array) ($line['image_ids'] ?? []) as $fileId) { + if ($fileId !== '' && $fileId !== null) { + $fileIds[] = (int) $fileId; + } + } + } + $fileUrls = []; + if ($fileIds !== []) { + foreach (SysFileModel::query()->whereIn('id', array_unique($fileIds))->get() as $file) { + $fileUrls[$file->id] = $file->preview_url; + } + } + foreach ($items as &$product) { + $product['image'] = ''; + foreach ((array) ($product['image_ids'] ?? []) as $fileId) { + if ($fileId !== '' && $fileId !== null && isset($fileUrls[(int) $fileId])) { + $product['image'] = $fileUrls[(int) $fileId]; + break; + } + } + unset($product['image_ids']); + } + } + + /** + * 修改订单明细(商品快照 + 订货量/重量),事务内重算单品金额与订单总价。 + * 可改字段:供应商、品名、规格、单位、单价、成本价、订货量、重量 + */ + #[PutRoute(route: '/item/{id}', authorize: 'update', where: ['id' => '[0-9]+'])] + public function updateItem(int $id, Request $request): JsonResponse + { + $data = $request->validate([ + 'supplier_id' => 'required|integer|min:0', + 'product_name' => 'required|string|max:100', + 'product_spec' => 'nullable|string|max:100', + 'unit' => 'required|string|max:20', + 'price' => 'required|numeric|min:0', + 'cost_price' => 'required|numeric|min:0', + 'quantity' => 'required|integer|min:1', + 'weight' => 'required|numeric|min:0', + 'remark' => 'nullable|string' + ], [ + 'supplier_id.required' => '供应商不能为空', + 'supplier_id.integer' => '供应商ID必须为整数', + 'supplier_id.min' => '供应商ID不正确', + 'product_name.required' => '品名不能为空', + 'product_name.max' => '品名最长 100 个字符', + 'product_spec.max' => '规格最长 100 个字符', + 'unit.required' => '计价单位不能为空', + 'unit.max' => '计价单位最长 20 个字符', + 'price.required' => '单价不能为空', + 'price.numeric' => '单价必须为数字', + 'price.min' => '单价不能小于 0', + 'cost_price.required' => '成本价不能为空', + 'cost_price.numeric' => '成本价必须为数字', + 'cost_price.min' => '成本价不能小于 0', + 'quantity.required' => '订货量不能为空', + 'quantity.integer' => '订货量必须为整数', + 'quantity.min' => '订货量必须大于 0', + 'weight.required' => '重量不能为空', + 'weight.numeric' => '重量必须为数字', + 'weight.min' => '重量不能小于 0', + ]); + + return DB::transaction(function () use ($id, $data) { + $item = StoreOrderItemModel::query()->lockForUpdate()->find($id); + if (empty($item)) { + throw new RepositoryException('订单明细不存在'); + } + $order = StoreOrderModel::query()->lockForUpdate()->find($item->order_id); + if (empty($order)) { + throw new RepositoryException('订单不存在'); + } + $this->assertItemEditable($order); + + $item->fill($data); + $item->product_spec = (string) ($data['product_spec'] ?? ''); + $item->amount = bcmul( + bcadd((string) $data['price'], '0', 2), + (string) $data['quantity'], + 2 + ); + $item->save(); + + $this->recalculateOrderTotals($order); + + return $this->success($item->load('supplier:id,name')->makeVisible('cost_price')->toArray()); + }); + } + + /** + * 一键同步明细商品快照:按商品ID同步最新商品档案的 + * 供应商、品名、规格、单位、成本价;单价按门店当前等级价重算 + * (未设置等级价时保留原单价),并重算订单总价 + */ + #[PutRoute(route: '/item/{id}/sync', authorize: 'update', where: ['id' => '[0-9]+'])] + public function syncItem(int $id): JsonResponse + { + return DB::transaction(function () use ($id) { + $item = StoreOrderItemModel::query()->lockForUpdate()->find($id); + if (empty($item)) { + throw new RepositoryException('订单明细不存在'); + } + $order = StoreOrderModel::query()->lockForUpdate()->find($item->order_id); + if (empty($order)) { + throw new RepositoryException('订单不存在'); + } + $this->assertItemEditable($order); + + // 软删除商品无法同步(下架商品仍可同步最新档案) + $product = ProductModel::find($item->product_id); + if (empty($product)) { + throw new RepositoryException('商品不存在或已被删除,无法同步'); + } + + $item->supplier_id = (int) $product->supplier_id; + $item->product_name = $product->name; + $item->product_spec = (string) $product->spec; + $item->unit = (string) $product->unit; + $item->cost_price = $product->cost_price; + + // 单价:按订货门店当前客户等级价重算(百分比计价按最新成本价换算) + $levelId = (int) ($order->store->level_id ?? 0); + $priceRow = ProductPriceModel::query() + ->forProductLevel((int) $item->product_id, $levelId) + ->first(['price', 'price_type', 'percent']); + if ($priceRow !== null) { + $item->price = ProductPriceModel::calcActualPrice( + (int) $priceRow->price_type, + $priceRow->price, + $priceRow->percent, + $product->cost_price, + ); + } + + $item->amount = bcmul((string) $item->price, (string) $item->quantity, 2); + $item->save(); + + $this->recalculateOrderTotals($order); + + return $this->success($item->load('supplier:id,name')->makeVisible('cost_price')->toArray()); + }); } /** @@ -274,6 +478,39 @@ class StoreOrderController extends BaseController return $this->success(); } + /** + * 明细编辑状态校验:已完成/已取消订单锁定 + */ + private function assertItemEditable(StoreOrderModel $order): void + { + if (! in_array($order->status, self::ITEM_EDITABLE_STATUS, true)) { + throw new RepositoryException( + '订单当前状态为「' . (self::STATUS_NAMES[$order->status] ?? $order->status) . '」,不允许修改商品明细' + ); + } + } + + /** + * 重算订单汇总:明细金额合计 → 商品总金额;订货量/重量合计 → 订货总量/总重量; + * 订单总金额 = 商品总金额 + 附加金额(恒成立) + */ + private function recalculateOrderTotals(StoreOrderModel $order): void + { + $totals = StoreOrderItemModel::query() + ->where('order_id', $order->id) + ->selectRaw('COALESCE(SUM(quantity), 0) as total_quantity') + ->selectRaw('COALESCE(SUM(weight), 0) as total_weight') + ->selectRaw('COALESCE(SUM(amount), 0) as product_amount') + ->first(); + + $productAmount = bcadd((string) $totals->product_amount, '0', 2); + $order->total_quantity = (int) $totals->total_quantity; + $order->total_weight = bcadd((string) $totals->total_weight, '0', 3); + $order->product_amount = $productAmount; + $order->total_amount = bcadd($productAmount, (string) $order->added_amount, 2); + $order->save(); + } + /** * 状态流转后通知门店用户 */ diff --git a/app/Models/StoreOrderItemModel.php b/app/Models/StoreOrderItemModel.php index 569b6b4..2fac920 100644 --- a/app/Models/StoreOrderItemModel.php +++ b/app/Models/StoreOrderItemModel.php @@ -2,12 +2,14 @@ namespace App\Models; +use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; /** - * 门店订单明细模型(快照下单时的商品名/规格/等级价,历史单据不受调价影响) + * 门店订单明细模型(下单时快照商品档案:品名/规格/单位/供应商/图文/成本价 + 等级单价, + * 商品调价或档案变更不影响历史单据) */ class StoreOrderItemModel extends Model { @@ -20,12 +22,19 @@ class StoreOrderItemModel extends Model 'order_id', 'store_id', 'product_id', + 'category_id', + 'supplier_id', 'product_name', 'product_spec', + 'unit', 'price', + 'image_ids', + 'content', + 'shelf_life', 'quantity', 'weight', 'amount', + 'cost_price', 'remark', ]; @@ -33,12 +42,33 @@ class StoreOrderItemModel extends Model 'order_id' => 'integer', 'store_id' => 'integer', 'product_id' => 'integer', + 'category_id' => 'integer', + 'supplier_id' => 'integer', 'price' => 'decimal:2', + 'shelf_life' => 'integer', 'quantity' => 'integer', 'weight' => 'decimal:3', 'amount' => 'decimal:2', + 'cost_price' => 'decimal:2', ]; + /** + * 成本价属商业敏感数据,默认不随 toArray 输出(防止泄漏到小程序端); + * 后台管理接口需在查询结果上调用 makeVisible('cost_price') 恢复。 + */ + protected $hidden = ['cost_price']; + + /** + * 商品图片ID(逗号分隔字符串 ↔ 数组) + */ + public function imageIds(): Attribute + { + return Attribute::make( + get: fn ($value) => $value === '' || $value === null ? [] : explode(',', (string) $value), + set: fn ($value) => is_array($value) ? implode(',', $value) : $value, + ); + } + /** * 所属订单 */ @@ -62,4 +92,20 @@ class StoreOrderItemModel extends Model { return $this->belongsTo(ProductModel::class, 'product_id', 'id'); } + + /** + * 快照供应商 + */ + public function supplier(): BelongsTo + { + return $this->belongsTo(SupplierModel::class, 'supplier_id', 'id'); + } + + /** + * 快照商品分类 + */ + public function category(): BelongsTo + { + return $this->belongsTo(ProductCategoryModel::class, 'category_id', 'id'); + } } diff --git a/database/factories/StoreOrderItemModelFactory.php b/database/factories/StoreOrderItemModelFactory.php index 75bc2f2..d199c55 100644 --- a/database/factories/StoreOrderItemModelFactory.php +++ b/database/factories/StoreOrderItemModelFactory.php @@ -21,12 +21,19 @@ class StoreOrderItemModelFactory extends Factory 'order_id' => 0, 'store_id' => 0, 'product_id' => 0, + 'category_id' => 0, + 'supplier_id' => 0, 'product_name' => '测试商品', 'product_spec' => '500g/袋', + 'unit' => '斤', 'price' => number_format(random_int(100, 5000) / 100, 2, '.', ''), + 'image_ids' => '', + 'content' => '', + 'shelf_life' => 0, 'quantity' => number_format(random_int(100, 10000) / 100, 2, '.', ''), 'weight' => 0, 'amount' => 0, + 'cost_price' => 0, 'remark' => '', ]; } 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 494c450..1749edf 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 @@ -42,14 +42,21 @@ return new class extends Migration Schema::create('store_order_item', function (Blueprint $table) { $table->increments('id')->comment('明细ID'); $table->integer('order_id')->comment('订单ID'); - $table->integer('store_id')->comment('门店ID(冗余,便于按门店筛选)'); + $table->integer('store_id')->comment('门店ID'); $table->integer('product_id')->comment('商品ID'); - $table->string('product_name', 100)->comment('品名(快照)'); - $table->string('product_spec', 100)->default('')->comment('规格/包规(快照)'); - $table->decimal('price', 10, 2)->default(0)->comment('单价(下单时客户等级价快照)'); + $table->integer('category_id')->default(0)->comment('分类ID'); + $table->integer('supplier_id')->default(0)->comment('供应商ID'); + $table->string('product_name', 100)->comment('品名'); + $table->string('product_spec', 100)->default('')->comment('规格/包规'); + $table->string('unit', 20)->default('斤')->comment('计价单位(斤/件/箱等)'); + $table->decimal('price', 10, 2)->default(0)->comment('单价'); + $table->string('image_ids', 255)->default('')->comment('商品图片'); + $table->text('content')->comment('商品图文详情'); + $table->integer('shelf_life')->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->decimal('cost_price', 10, 2)->default(0)->comment('成本价'); $table->string('remark', 255)->default('')->comment('门店下单备注'); $table->timestamps(); $table->index(['order_id'], 'store_order_item_order_index'); diff --git a/tests/Feature/StoreOrderItemTest.php b/tests/Feature/StoreOrderItemTest.php new file mode 100644 index 0000000..f0fe7bb --- /dev/null +++ b/tests/Feature/StoreOrderItemTest.php @@ -0,0 +1,330 @@ +create(); + $store = StoreModel::factory()->create(['level_id' => $level->id]); + $product = ProductModel::factory()->create([ + 'status' => ProductModel::STATUS_ON, + 'cost_price' => $costPrice, + ]); + ProductPriceModel::factory()->create([ + 'product_id' => $product->id, + 'level_id' => $level->id, + 'price' => $price, + ]); + + return [$store, $product, UserModel::factory()->forStore($store->id)->create()]; + } + + /** 以小程序身份下一单并返回订单(可指定初始状态) */ + private function placeOrder(ProductModel $product, UserModel $user, int $quantity = 2, int $status = StoreOrderModel::STATUS_PENDING): StoreOrderModel + { + $this->actingAsMiniUser($user); + $this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => $quantity]]]) + ->assertOk()->assertJsonPath('success', true); + + $order = StoreOrderModel::where('store_id', $user->store_id)->latest('id')->first(); + $this->assertNotNull($order); + if ($status !== StoreOrderModel::STATUS_PENDING) { + $order->update(['status' => $status]); + } + + return $order; + } + + /** 下单时明细快照完整商品档案(分类/供应商/单位/图片/图文/保质期/成本价) */ + public function test_place_order_snapshots_full_product_info(): void + { + $supplier = SupplierModel::factory()->create(); + $level = CustomerLevelModel::factory()->create(); + $store = StoreModel::factory()->create(['level_id' => $level->id]); + $product = ProductModel::factory()->create([ + 'status' => ProductModel::STATUS_ON, + 'supplier_id' => $supplier->id, + 'category_id' => 7, + 'unit' => '箱', + 'image_ids' => '1,2', + 'content' => '图文详情', + 'shelf_life' => 30, + 'cost_price' => '4.00', + ]); + ProductPriceModel::factory()->create([ + 'product_id' => $product->id, + 'level_id' => $level->id, + 'price' => '5.50', + ]); + $user = UserModel::factory()->forStore($store->id)->create(); + + $this->actingAsMiniUser($user); + $this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 3]]]) + ->assertOk()->assertJsonPath('success', true); + + $item = StoreOrderModel::where('store_id', $store->id)->first()->items->first(); + $this->assertSame($supplier->id, $item->supplier_id, '快照供应商'); + $this->assertSame(7, $item->category_id, '快照分类'); + $this->assertSame('箱', $item->unit, '快照单位'); + $this->assertSame('1,2', implode(',', $item->image_ids), '快照图片'); + $this->assertSame('图文详情', $item->content, '快照图文详情'); + $this->assertSame(30, $item->shelf_life, '快照保质期'); + $this->assertSame('4.00', (string) $item->cost_price, '快照成本价'); + $this->assertSame('5.50', (string) $item->price, '快照等级单价'); + } + + /** 小程序订单详情不泄漏成本价 */ + public function test_mini_detail_hides_cost_price(): void + { + [, $product, $user] = $this->makeStoreWithProduct('5.00', '4.00'); + $order = $this->placeOrder($product, $user); + + $this->actingAsMiniUser($user); + $this->getJson("/mini/order/{$order->id}") + ->assertOk() + ->assertJsonPath('success', true) + ->assertJsonMissingPath('data.items.0.cost_price'); + } + + /** 后台订单详情:成本价可见、附供应商名与单位 */ + public function test_admin_detail_shows_cost_price_and_supplier(): void + { + $supplier = SupplierModel::factory()->create(); + [, $product, $user] = $this->makeStoreWithProduct('5.00', '4.00'); + $product->update(['supplier_id' => $supplier->id]); + $order = $this->placeOrder($product, $user); + + $this->actingAsSysUser(); + $this->getJson("/order/store/{$order->id}") + ->assertOk() + ->assertJsonPath('success', true) + ->assertJsonPath('data.items.0.cost_price', '4.00') + ->assertJsonPath('data.items.0.unit', $product->unit) + ->assertJsonPath('data.items.0.supplier.name', $supplier->name); + } + + /** 修改明细:重算单品金额与订单总量/总额(总额 = 商品 + 附加) */ + public function test_update_item_recalculates_order_totals(): void + { + [$store, $product, $user] = $this->makeStoreWithProduct('5.00'); + $order = $this->placeOrder($product, $user, 2, StoreOrderModel::STATUS_SUMMARIZED); + $order->update(['added_amount' => '6.00']); // 已有附加金额 + $item = $order->items->first(); + $supplier = SupplierModel::factory()->create(); + + $this->actingAsSysUser(); + $this->putJson("/order/store/item/{$item->id}", [ + 'supplier_id' => $supplier->id, + 'product_name' => '改名后的商品', + 'product_spec' => '新规格', + 'unit' => '箱', + 'price' => '6.00', + 'cost_price' => '3.50', + 'quantity' => 5, + 'weight' => '2.5', + ])->assertOk()->assertJsonPath('success', true); + + $item->refresh(); + $this->assertSame($supplier->id, $item->supplier_id); + $this->assertSame('改名后的商品', $item->product_name); + $this->assertSame('新规格', $item->product_spec); + $this->assertSame('箱', $item->unit); + $this->assertSame('6.00', (string) $item->price); + $this->assertSame('3.50', (string) $item->cost_price); + $this->assertSame(5, $item->quantity); + $this->assertSame('2.500', (string) $item->weight); + $this->assertSame('30.00', (string) $item->amount, '6.00 × 5'); + + $order->refresh(); + $this->assertSame(5, $order->total_quantity, '订货总量 = 明细合计'); + $this->assertSame('2.500', (string) $order->total_weight, '总重量 = 明细合计'); + $this->assertSame('30.00', (string) $order->product_amount, '商品总金额 = 明细金额合计'); + $this->assertSame('36.00', (string) $order->total_amount, '订单总金额 = 商品 30 + 附加 6'); + } + + /** 已完成/已取消订单不允许修改明细 */ + public function test_update_item_rejected_when_completed_or_cancelled(): void + { + [, $product, $user] = $this->makeStoreWithProduct('5.00'); + $order = $this->placeOrder($product, $user, 1); + $item = $order->items->first(); + $payload = [ + 'supplier_id' => 0, + 'product_name' => '不应生效', + 'product_spec' => '', + 'unit' => '斤', + 'price' => '1.00', + 'cost_price' => '1.00', + 'quantity' => 1, + 'weight' => 0, + ]; + + $this->actingAsSysUser(); + foreach ([StoreOrderModel::STATUS_COMPLETED, StoreOrderModel::STATUS_CANCELLED] as $status) { + $order->update(['status' => $status]); + $this->putJson("/order/store/item/{$item->id}", $payload) + ->assertOk()->assertJsonPath('success', false); + } + + $this->assertNotSame('不应生效', $item->fresh()->product_name, '被拒绝后明细不变'); + $this->assertSame('5.00', (string) $order->fresh()->total_amount, '被拒绝后总金额不变'); + } + + /** 修改明细参数校验:负单价被拦截 */ + public function test_update_item_validates_negative_price(): void + { + [, $product, $user] = $this->makeStoreWithProduct('5.00'); + $order = $this->placeOrder($product, $user, 1, StoreOrderModel::STATUS_SUMMARIZED); + $item = $order->items->first(); + + $this->actingAsSysUser(); + $this->putJson("/order/store/item/{$item->id}", [ + 'supplier_id' => 0, + 'product_name' => $item->product_name, + 'unit' => '斤', + 'price' => -1, + 'cost_price' => 0, + 'quantity' => 1, + 'weight' => 0, + ])->assertOk() + ->assertJsonPath('success', false) + ->assertJsonPath('msg', '单价不能小于 0'); + } + + /** 一键同步:按商品ID同步最新档案(固定价等级保留原单价) */ + public function test_sync_item_updates_snapshot_from_product(): void + { + [$store, $product, $user] = $this->makeStoreWithProduct('5.50', '4.00'); + $order = $this->placeOrder($product, $user, 3, StoreOrderModel::STATUS_SUMMARIZED); + $item = $order->items->first(); + $this->assertSame('16.50', (string) $item->amount); + + // 下单后商品档案变更:改名/改规格/改单位/换供应商/调成本价 + $supplier = SupplierModel::factory()->create(); + $product->update([ + 'name' => '同步后的品名', + 'spec' => '同步后的规格', + 'unit' => '箱', + 'supplier_id' => $supplier->id, + 'cost_price' => '9.99', + ]); + + $this->actingAsSysUser(); + $this->putJson("/order/store/item/{$item->id}/sync") + ->assertOk()->assertJsonPath('success', true); + + $item->refresh(); + $this->assertSame('同步后的品名', $item->product_name); + $this->assertSame('同步后的规格', $item->product_spec); + $this->assertSame('箱', $item->unit); + $this->assertSame($supplier->id, $item->supplier_id); + $this->assertSame('9.99', (string) $item->cost_price); + $this->assertSame('5.50', (string) $item->price, '固定价等级单价不随档案变化'); + $this->assertSame('16.50', (string) $item->amount, '单价未变,金额不变'); + } + + /** 一键同步:百分比计价等级按最新成本价重算单价与订单总价 */ + public function test_sync_item_recalculates_percent_price_with_latest_cost(): void + { + $level = CustomerLevelModel::factory()->create(); + $store = StoreModel::factory()->create(['level_id' => $level->id]); + $product = ProductModel::factory()->create([ + 'status' => ProductModel::STATUS_ON, + 'cost_price' => '10.00', + ]); + ProductPriceModel::factory()->create([ + 'product_id' => $product->id, + 'level_id' => $level->id, + 'price_type' => ProductPriceModel::PRICE_TYPE_PERCENT, + 'percent' => 30, + ]); + $user = UserModel::factory()->forStore($store->id)->create(); + + $order = $this->placeOrder($product, $user, 2, StoreOrderModel::STATUS_SUMMARIZED); + $item = $order->items->first(); + $this->assertSame('13.00', (string) $item->price, '下单时 10 元上浮 30%'); + $this->assertSame('26.00', (string) $order->total_amount); + + // 成本价上调后同步:单价应按最新成本重算 + $product->update(['cost_price' => '20.00']); + + $this->actingAsSysUser(); + $this->putJson("/order/store/item/{$item->id}/sync") + ->assertOk()->assertJsonPath('success', true); + + $item->refresh(); + $this->assertSame('20.00', (string) $item->cost_price); + $this->assertSame('26.00', (string) $item->price, '20 元上浮 30% = 26.00'); + $this->assertSame('52.00', (string) $item->amount, '26.00 × 2'); + + $order->refresh(); + $this->assertSame('52.00', (string) $order->product_amount); + $this->assertSame('52.00', (string) $order->total_amount); + } + + /** 一键同步:商品已删除时拒绝同步 */ + public function test_sync_item_rejected_when_product_deleted(): void + { + [, $product, $user] = $this->makeStoreWithProduct('5.00'); + $order = $this->placeOrder($product, $user, 1, StoreOrderModel::STATUS_SUMMARIZED); + $item = $order->items->first(); + + $product->delete(); // 软删除 + + $this->actingAsSysUser(); + $this->putJson("/order/store/item/{$item->id}/sync") + ->assertOk() + ->assertJsonPath('success', false) + ->assertJsonPath('msg', '商品不存在或已被删除,无法同步'); + } + + /** 订单列表按包含的商品名称搜索 */ + public function test_order_list_search_by_product_name(): void + { + $level = CustomerLevelModel::factory()->create(); + $store = StoreModel::factory()->create(['level_id' => $level->id]); + $user = UserModel::factory()->forStore($store->id)->create(); + + $cabbage = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'name' => '大白菜A']); + $potato = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'name' => '土豆B']); + foreach ([$cabbage, $potato] as $p) { + ProductPriceModel::factory()->create([ + 'product_id' => $p->id, + 'level_id' => $level->id, + 'price' => '5.00', + ]); + } + + $this->placeOrder($cabbage, $user); + $this->placeOrder($potato, $user); + + $this->actingAsSysUser(); + $response = $this->getJson('/order/store?product_name=' . urlencode('白菜')); + $response->assertOk()->assertJsonPath('success', true); + $this->assertSame(1, $response->json('data.total'), '仅命中包含「白菜」的订单'); + $this->assertSame('大白菜A', $response->json('data.data.0.items.0.product_name')); + + // 无匹配关键字 → 空列表 + $this->getJson('/order/store?product_name=' . urlencode('不存在的商品')) + ->assertOk()->assertJsonPath('data.total', 0); + } +} diff --git a/web/api/order/store.ts b/web/api/order/store.ts index 8349878..dfec2ca 100644 --- a/web/api/order/store.ts +++ b/web/api/order/store.ts @@ -1,6 +1,6 @@ import createAxios from '@/utils/request'; import type IStoreOrder from '@/domain/iStoreOrder.ts'; -import type { IOrderSummaryRow } from '@/domain/iStoreOrder.ts'; +import type { IOrderSummaryRow, IStoreOrderItem, IStoreOrderItemUpdate } from '@/domain/iStoreOrder.ts'; /** 订单详情(头 + 明细) */ export async function getStoreOrder(id: number) { @@ -44,3 +44,20 @@ export async function getOrderSummary() { method: 'get', }); } + +/** 修改订单明细(供应商/品名/规格/单位/单价/成本价/订货量/重量,后端重算订单总价) */ +export async function updateOrderItem(itemId: number, data: IStoreOrderItemUpdate) { + return createAxios({ + url: `/order/store/item/${itemId}`, + method: 'put', + data, + }); +} + +/** 一键同步明细商品快照为最新商品档案信息(供应商/品名/规格/单位/单价/成本价) */ +export async function syncOrderItem(itemId: number) { + return createAxios({ + url: `/order/store/item/${itemId}/sync`, + method: 'put', + }); +} diff --git a/web/domain/iStoreOrder.ts b/web/domain/iStoreOrder.ts index f559bc2..78c68b1 100644 --- a/web/domain/iStoreOrder.ts +++ b/web/domain/iStoreOrder.ts @@ -1,19 +1,42 @@ -/** 门店订单明细(商品快照) */ +/** 门店订单明细(下单时商品档案快照) */ export interface IStoreOrderItem { id?: number; order_id?: number; store_id?: number; product_id?: number; + category_id?: number; + supplier_id?: number; + /** 详情接口附带的供应商信息 */ + supplier?: { id: number; name: string }; product_name?: string; product_spec?: string; + /** 计价单位(斤/件/箱等) */ + unit?: string; price?: string; + /** 列表接口附带的封面图 */ image?: string; + image_ids?: string; + shelf_life?: number; quantity?: number; weight?: string; amount?: string; + /** 成本价(仅后台接口可见) */ + cost_price?: string; remark?: string; } +/** 修改订单明细入参(保存后后端重算单品金额与订单总价) */ +export interface IStoreOrderItemUpdate { + supplier_id: number; + product_name: string; + product_spec?: string; + unit: string; + price: number; + cost_price: number; + quantity: number; + weight: number; +} + /** 门店订单 */ export default interface IStoreOrder { id?: number; diff --git a/web/pages/order/store.tsx b/web/pages/order/store.tsx index 151b740..2e3ef43 100644 --- a/web/pages/order/store.tsx +++ b/web/pages/order/store.tsx @@ -4,20 +4,21 @@ import { DatePicker, Descriptions, Drawer, + Empty, Form, + Input, InputNumber, message, Modal, Popconfirm, Radio, + Select, Space, - Table, Tag, Typography, Image, } from 'antd'; import dayjs from 'dayjs'; -import type { TableProps } from 'antd'; import XinTable from '@/components/XinTable'; import type { XinTableColumn, @@ -25,20 +26,33 @@ import type { XinTableProps, } from '@/components/XinTable/typings.ts'; import type IStoreOrder from '@/domain/iStoreOrder.ts'; -import type { IStoreOrderItem } from '@/domain/iStoreOrder.ts'; +import type { IStoreOrderItem, IStoreOrderItemUpdate } from '@/domain/iStoreOrder.ts'; import { STORE_ORDER_STATUS_MAP } from '@/domain/iStoreOrder.ts'; -import { getStoreOrder, updateOrderContainer, updateOrderStatus, batchUpdateOrderStatus } from '@/api/order/store.ts'; +import { + getStoreOrder, + updateOrderContainer, + updateOrderStatus, + batchUpdateOrderStatus, + updateOrderItem, + syncOrderItem, +} from '@/api/order/store.ts'; import { generatePurchase } from '@/api/purchase/order.ts'; import { getStoreOptions } from '@/api/customer/store.ts'; +import { getSupplierOptions } from '@/api/customer/supplier.ts'; import type IStore from '@/domain/iStore.ts'; +import type ISupplier from '@/domain/iSupplier.ts'; import AuthButton from '@/components/AuthButton'; -import {UnorderedListOutlined} from "@ant-design/icons"; +import { EditOutlined, SyncOutlined, UnorderedListOutlined } from '@ant-design/icons'; +import TextArea from "antd/es/input/TextArea"; const { Title, Text } = Typography; /** 允许修改周转框/托盘数量的订单状态:已接单、采购中、配送中 */ const CONTAINER_EDITABLE_STATUS = [1, 2, 3]; +/** 允许修改/同步商品明细的订单状态:待接单、已接单、采购中、配送中(已完成/已取消锁定) */ +const ITEM_EDITABLE_STATUS = [0, 1, 2, 3]; + /** * 状态流转合法路径:待接单→已接单/已取消;采购中→配送中/已完成;配送中→已完成 * (已接单→采购中 通过「生成采购单」完成,不在流转按钮内) @@ -90,8 +104,18 @@ const StoreOrderPage: React.FC = () => { const [generateLoading, setGenerateLoading] = useState(false); const [generateForm] = Form.useForm<{ purchase_date: dayjs.Dayjs }>(); + /** 供应商选项(明细编辑用) */ + const [suppliers, setSuppliers] = useState([]); + + /** 商品明细编辑(弹窗表单,保存后后端重算订单总价) */ + const [itemEditOpen, setItemEditOpen] = useState(false); + const [itemEditTarget, setItemEditTarget] = useState(null); + const [itemSaving, setItemSaving] = useState(false); + const [itemForm] = Form.useForm(); + useEffect(() => { getStoreOptions().then((res) => setStores(res.data.data ?? [])); + getSupplierOptions().then((res) => setSuppliers(res.data.data ?? [])); }, []); const openDetail = async (id: number) => { @@ -165,6 +189,46 @@ const StoreOrderPage: React.FC = () => { } }; + /** 打开明细编辑弹窗(商品快照 + 订货量/重量) */ + const openItemEdit = (item: IStoreOrderItem) => { + setItemEditTarget(item); + itemForm.setFieldsValue({ + supplier_id: item.supplier_id ?? 0, + product_name: item.product_name ?? '', + product_spec: item.product_spec ?? '', + unit: item.unit ?? '', + price: Number(item.price ?? 0), + cost_price: Number(item.cost_price ?? 0), + quantity: Number(item.quantity ?? 0), + weight: Number(item.weight ?? 0), + }); + setItemEditOpen(true); + }; + + /** 保存明细修改:后端重算单品金额与订单总价 */ + const handleItemSave = async (values: IStoreOrderItemUpdate) => { + if (!itemEditTarget?.id || !detail?.id) return; + setItemSaving(true); + try { + await updateOrderItem(itemEditTarget.id, values); + message.success('明细已更新,订单总价已重算'); + setItemEditOpen(false); + await openDetail(detail.id); + await tableRef.current?.reload(); + } finally { + setItemSaving(false); + } + }; + + /** 一键同步:按商品ID拉取最新商品档案(供应商/品名/规格/单位/单价/成本价) */ + const handleItemSync = async (item: IStoreOrderItem) => { + if (!item.id || !detail?.id) return; + await syncOrderItem(item.id); + message.success('已同步最新商品信息,订单总价已重算'); + await openDetail(detail.id); + await tableRef.current?.reload(); + }; + // 弹窗内实时预览:附加金额 = 框×单价 + 托盘×单价;订单总金额 = 商品金额 + 附加金额 const watchBoxNum = Number(Form.useWatch('box_num', containerForm) ?? 0); const watchTrayNum = Number(Form.useWatch('tray_num', containerForm) ?? 0); @@ -172,14 +236,8 @@ const StoreOrderPage: React.FC = () => { + 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 || '-' }, - { title: '单价', dataIndex: 'price', align: 'right', render: (v) => `¥${v}` }, - { title: '订货量', dataIndex: 'quantity', align: 'right' }, - { title: '金额', dataIndex: 'amount', align: 'right', render: (v) => `¥${v}` }, - { title: '备注', dataIndex: 'remark', render: (v) => v || '-' }, - ]; + /** 明细行编辑/同步按钮(仅未完成、未取消订单可见) */ + const itemEditable = ITEM_EDITABLE_STATUS.includes(detail?.status ?? -1); const columns: XinTableColumn[] = [ { @@ -189,6 +247,13 @@ const StoreOrderPage: React.FC = () => { valueType: 'text', hideInForm: true }, + { + title: '商品名称', + hideInTable: true, + dataIndex: 'product_name', + valueType: 'text', + hideInForm: true + }, { title: '商品信息', dataIndex: 'items', @@ -352,7 +417,7 @@ const StoreOrderPage: React.FC = () => { hideInSearch: true, render: (_, record) => ( - { @@ -362,7 +427,7 @@ const StoreOrderPage: React.FC = () => { title={`确认将订单状态更新为「${action.label}」?`} onConfirm={() => handleStatusChange(record.id!, action.status)} > - @@ -372,7 +437,7 @@ const StoreOrderPage: React.FC = () => { { CONTAINER_EDITABLE_STATUS.includes(record.status ?? -1) ? ( - @@ -429,7 +494,7 @@ const StoreOrderPage: React.FC = () => { title="订单详情" open={detailOpen} onClose={() => setDetailOpen(false)} - width={720} + size={1000} loading={detailLoading} footer={ detail && NEXT_STATUS[detail.status ?? -1] ? ( @@ -452,9 +517,21 @@ const StoreOrderPage: React.FC = () => { > {detail ? ( <> - + {/* 门店信息 */} + + + {detail.store?.name ?? `门店#${detail.store_id}`} + + {detail.store?.contact ?? '-'} + {detail.store?.phone ?? '-'} + + {detail.store?.address ?? '-'} + + + + {/* 订单信息 */} + {detail.order_no} - {detail.store?.name} {detail.order_date} @@ -462,34 +539,128 @@ const StoreOrderPage: React.FC = () => { {detail.total_quantity} - ¥{detail.total_amount} + {detail.box_num} + {detail.tray_num} {detail.remark ? ( - + {detail.remark} ) : null} - + + {/* 商品明细(商城模式:首图 + 单价 × 订货量 + 金额) */} + <Title level={5} className="mt-6! mb-3!"> 商品明细 - - rowKey="id" - size="small" - columns={itemColumns} - dataSource={detail.items ?? []} - pagination={false} - summary={() => ( - - - 合计 - - - ¥{detail.total_amount} - - - - )} - /> +
+
+
商品信息
+
供应商
+
单价
+
订货量
+
金额
+ {itemEditable ?
操作
: null} +
+ {(detail.items ?? []).map((item) => ( +
+
+ + {item.image ? ( + + ) : ( +
+ 暂无图片 +
+ )} +
+
+
{item.product_name}
+
+ {item.product_spec || '-'} / {item.unit || '-'} · 成本价 ¥ + {item.cost_price ?? '0.00'} + {Number(item.weight ?? 0) > 0 ? ` · 重量:${item.weight}` : ''} +
+ {item.remark ? ( +
备注:{item.remark}
+ ) : null} +
+
+
{item.supplier?.name ?? '-'}
+
¥{item.price}
+
{item.quantity}
+
+ ¥{item.amount} +
+ {itemEditable ? ( +
+ + + + + + handleItemSync(item)} + > + + + + +
+ ) : null} +
+ ))} + {(detail.items ?? []).length === 0 ? ( + + ) : null} +
+ + {/* 附加信息 */} +
+
+
+ + 周转框金额({detail.box_num} × ¥{detail.box_price ?? '0.00'}) + + ¥{detail.box_amount ?? '0.00'} +
+
+ + 周转托盘金额({detail.tray_num} × ¥{detail.tray_price ?? '0.00'}) + + ¥{detail.tray_amount ?? '0.00'} +
+
+ 商品总金额 + ¥{detail.product_amount} +
+
+ 订单总金额 + + ¥{detail.total_amount} + +
+
+
) : null} @@ -593,6 +764,87 @@ const StoreOrderPage: React.FC = () => {
+ + {/* 编辑商品明细(保存后后端重算单品金额与订单总价) */} + setItemEditOpen(false)} + onOk={() => itemForm.submit()} + confirmLoading={itemSaving} + okText="保存" + width={640} + destroyOnHidden + > +
+ 订单 {detail?.order_no};修改保存后,系统将自动重算单品金额与订单总价。 +
+
+
+ + + + + + + + + + + + + + + + + + + + + + +