zlwu hace 6 meses
padre
commit
edec4fde17
Se han modificado 54 ficheros con 824 adiciones y 420 borrados
  1. 15 37
      backend/app/api/admin.py
  2. 34 20
      backend/app/api/mini_client.py
  3. 22 12
      backend/app/api/mini_client_extra.py
  4. 14 6
      backend/app/api/mini_deliverer.py
  5. 84 12
      backend/app/api/mini_supplier.py
  6. 11 9
      backend/app/api/public.py
  7. 49 0
      backend/app/core/request_payload.py
  8. 2 4
      backend/app/crud/region.py
  9. 126 156
      graphify-out/GRAPH_REPORT.md
  10. 0 0
      graphify-out/cache/ast/0832211eef295d28a145810152f27ab5c2f4dbc9e92f69cc6f759bbf73edc010.json
  11. 0 0
      graphify-out/cache/ast/159025ec193f5036237d259bebade59296e24993469b834f8506d2bbdc0ced61.json
  12. 1 0
      graphify-out/cache/ast/17bf88c2db63c348a3f973da816a9c5ae0c3980a7a90bcc7b6f05feb8f97684e.json
  13. 0 0
      graphify-out/cache/ast/1c8ef12329ad83084ee5462100ff3e0057b9051f96a46ef1fb53be34cd889f41.json
  14. 0 0
      graphify-out/cache/ast/1fabf20f5f6b663addcc528b7d96036e554ac183ccc78e27d3c6dc75826c80ec.json
  15. 0 0
      graphify-out/cache/ast/2c135a5531363e8d8cffe0d516620b2e3caca691a7dd8bc707a2cd29f02afca4.json
  16. 0 0
      graphify-out/cache/ast/2f2f2266e72936173ba1a33f851a90459b22976873c0de704a14476b14ec4a7d.json
  17. 0 0
      graphify-out/cache/ast/35eb51bd789c34c701b2868f4c1a8e280a90b44375c08968332920d2ca042696.json
  18. 0 0
      graphify-out/cache/ast/3b1989a9573a644ac30113a366c735de801c1e21a2c137d291d938dfc48b20ff.json
  19. 0 0
      graphify-out/cache/ast/49a5dcbd47400d3072654649bf0c8bb92ac735cd71bdad9aaec185b5bce8f302.json
  20. 1 0
      graphify-out/cache/ast/579be1b1069b242728e9a7aa02182bfaf2e815e724aeef12ddf20ab80f48e676.json
  21. 0 0
      graphify-out/cache/ast/58a72eda6fb034b86b31cb1667436ac2246cb276bea5e5eae306c159d4f61122.json
  22. 0 0
      graphify-out/cache/ast/67deb3afb12011020f8ae835c6e611eccfb0279c2b06048954ef7ecc8cd7f2ee.json
  23. 0 0
      graphify-out/cache/ast/6f51dc265c6e1c914db5d66f21a8f9f361ca17fa1b6c61515fdba852a237b002.json
  24. 0 0
      graphify-out/cache/ast/7228d2d93a9c75f1dca876bacad12dc67140daae54d984fb9d45f30b6ba373f9.json
  25. 0 0
      graphify-out/cache/ast/777e9fc785e8cec58454cc7ff27820f2cc3f1226bc165a44d1f376b0d3e9683b.json
  26. 0 0
      graphify-out/cache/ast/77b808f4ee3a98bacfff0e5381f71f75960804d68114863f98800fabe19f93af.json
  27. 0 0
      graphify-out/cache/ast/77eac6b4d0214325a32c6ac68ad638277acb30d46e8c3a9ad64ee4b22ae55183.json
  28. 0 0
      graphify-out/cache/ast/7c79f0b04460dcbbc077edb20514b0f9f44fe2257dd93911097ca47178d97254.json
  29. 1 0
      graphify-out/cache/ast/8d97a77c8ce11081915607f3fb742ecaba14fca99f98d1157f0a517972c67dbd.json
  30. 0 0
      graphify-out/cache/ast/9aa9a41fc342ae174cf320ca89df1e3fb57d43c92bc97aae76dc4dd72ae52341.json
  31. 0 0
      graphify-out/cache/ast/a0c95d984a50bb1e69f89618b156beb8d8b3d2d6b12406f4728d4b2e30073764.json
  32. 1 0
      graphify-out/cache/ast/a1d144ebb9f002ea61899496509a372eb68e8dfe8bcec6fd9c76a140817eab78.json
  33. 1 0
      graphify-out/cache/ast/a2ba2f3806ff22be4ff6462d01fd1ebd4ec5bd57b81b2a976d20fa0a831f9fc1.json
  34. 1 0
      graphify-out/cache/ast/b723ca84c33b1e6b2782c6bbdbc5ebce5be48cf673ef85fa50ed1ac77371d6d8.json
  35. 1 0
      graphify-out/cache/ast/b73f283426dd27759b73abc75d5e5710181bda5f5b8b15d98d323bd4c840b908.json
  36. 0 0
      graphify-out/cache/ast/c91fa7f09f279794e3b94dc423802fe3c4bc86c733f47f92d7ba119e3e41e707.json
  37. 0 0
      graphify-out/cache/ast/d82eaee94a0d0475fc751ae570af4ff6946ec382ec470f47643933105ac167ec.json
  38. 0 0
      graphify-out/cache/ast/e6d5bddf2c0bc167a48f0e9d363c21ee497bc5e191670d634763ab4e9a4160e9.json
  39. 0 0
      graphify-out/cache/ast/f04dc1fb9605c18fbff3576f2d9b2fcc76d6ef5739f9959b05ef7b859a686631.json
  40. 1 0
      graphify-out/cache/ast/f2fe8d080b0f3398d51e4e2cd45c1185e7f8152b96e4ae3dac951079c148c6ef.json
  41. 0 0
      graphify-out/cache/ast/f7670c300842864a87c299ab1430a44d706cf44c9afbe676bfde6a6f4d2bfcec.json
  42. 0 0
      graphify-out/cache/ast/f9d85eaaf2ef87530cbb55fe113c17e63c5b5487407b961ac2ece7a865f1dd41.json
  43. 0 0
      graphify-out/cache/ast/fb6cac92e2064e36d364f20736d11dc6298b4972302ac406a120e7b69b0d77d8.json
  44. 1 0
      graphify-out/cache/ast/fd2897605aa8e09922fa40fc5fff41d870e53ff9de78cd1a6e66cd8da0a18c7b.json
  45. 1 1
      graphify-out/graph.html
  46. 143 107
      graphify-out/graph.json
  47. 3 3
      sqxp-wx/pages/client/address/address.vue
  48. 3 3
      sqxp-wx/pages/client/address/select.vue
  49. 3 3
      sqxp-wx/pages/supplier/address/address.vue
  50. 277 32
      sqxp-wx/pages/supplier/inquiry/inquiry.vue
  51. 14 0
      sqxp-wx/pages/supplier/product/detail.vue
  52. 3 1
      sqxp-wx/pages/supplier/settings/password.vue
  53. 1 1
      web-admin/src/api/business.ts
  54. 10 13
      web-admin/src/views/system/inquiry-quotation/index.vue

+ 15 - 37
backend/app/api/admin.py

@@ -34,6 +34,7 @@ from app.crud.password_history import password_history_crud
 from app.crud.product import product_crud
 from app.crud.address import address_crud
 from app.core.utils import generate_id
+from app.core.request_payload import resolve_request_payload
 from app.services.china_region_tree import get_china_region_tree_cached
 
 router = APIRouter(prefix="/web/admin", tags=["运营端管理接口"])
@@ -248,40 +249,11 @@ def _apply_region_minutes_to_data(request: dict, data: dict):
 
 def _resolve_admin_payload_with_crypto_fallback(request: dict, current_user: dict):
     """兼容明文/密文请求,返回业务 payload。失败时返回 error_response。"""
-    req_dict = request if isinstance(request, dict) else {}
-    # 若是密文信封,优先走兜底解密(避免把 enc/ts/nonce/edata 误当成业务 payload)。
-    if (
-        isinstance(req_dict, dict)
-        and req_dict.get("enc") == 1
-        and req_dict.get("edata")
-    ):
-        try:
-            from app.core import api_crypto
-            from app.core.config import settings
-
-            ts = req_dict.get("ts")
-            nonce = req_dict.get("nonce")
-            edata = req_dict.get("edata")
-            operator_user_id = current_user.get("user_id") if isinstance(current_user, dict) else None
-            fixed_secret = getattr(settings, "API_ENCRYPT_FIXED_SECRET", "") or ""
-            if not (fixed_secret and operator_user_id and ts is not None and nonce):
-                return {}, error_response(400, "密文解析配置缺失")
-
-            key = api_crypto.derive_key(
-                fixed_secret=fixed_secret,
-                user_id=str(operator_user_id),
-                ts=int(ts),
-                nonce=str(nonce),
-            )
-            plain = api_crypto.decrypt_json(str(edata), key)
-            resolved = plain.get("data", plain) if isinstance(plain, dict) else {}
-            if isinstance(resolved, dict):
-                return resolved, None
-            return {}, error_response(400, "密文解密后数据格式错误")
-        except Exception as exc:
-            return {}, error_response(400, f"密文解密失败: {exc}")
-
-    payload = req_dict.get("data", req_dict) if isinstance(req_dict, dict) else {}
+    payload = resolve_request_payload(request, current_user, source="admin")
+    if not payload:
+        req_dict = request if isinstance(request, dict) else {}
+        if req_dict.get("enc") == 1:
+            return {}, error_response(400, "密文解密失败")
     return payload if isinstance(payload, dict) else {}, None
 
 
@@ -7880,9 +7852,13 @@ def confirm_quotation_supplier(
     from sqlalchemy import and_
     from decimal import Decimal
 
-    inquiry_id = request.get("inquiryId")
-    quotation_id = request.get("quotationId")
-    final_price_items = request.get("finalPriceItems") or []
+    payload, payload_error = _resolve_admin_payload_with_crypto_fallback(request, current_user)
+    if payload_error:
+        return payload_error
+
+    inquiry_id = payload.get("inquiryId")
+    quotation_id = payload.get("quotationId")
+    final_price_items = payload.get("finalPriceItems") or []
 
     if not inquiry_id or not quotation_id:
         return error_response(400, "询价单ID和报价ID不能为空")
@@ -8016,6 +7992,8 @@ def confirm_quotation_supplier(
                 final_price = Decimal(str(qi.price)).quantize(Decimal("0.01"))
             else:
                 return error_response(400, f"商品 {pid_key} 缺少供应商报价,且未传入最终售价,无法生成价格清单")
+        if final_price <= Decimal("0"):
+            return error_response(400, f"商品 {pid_key} 的价格必须大于0")
 
         # 单位ID:优先报价单项;否则从商品表取
         unit_id = getattr(qi, "unit_id", None) if qi is not None else None

+ 34 - 20
backend/app/api/mini_client.py

@@ -8,6 +8,7 @@ from sqlalchemy import and_
 from app.core.database import get_db
 from app.core.auth import get_current_user, get_client_ip
 from app.core.response import success_response, error_response
+from app.core.request_payload import resolve_request_payload
 from app.models.models import (
     BasAddress,
     BasProduct,
@@ -39,6 +40,10 @@ def require_supplier_or_client(current_user: dict = Depends(get_current_user)):
     return current_user
 
 
+def _resolve_request_payload(request_body: dict, current_user: dict) -> dict:
+    return resolve_request_payload(request_body, current_user, source="mini_client")
+
+
 def _deliverer_contact_phone(du: SysUser) -> Optional[str]:
     """配送员联系电话:优先 sys_user.phone;为空则尝试 username 为 11 位手机号的情况。"""
     p = (getattr(du, "phone", None) or "").strip()
@@ -130,14 +135,17 @@ def create_address(
     current_user: dict = Depends(require_client)
 ):
     """创建地址"""
-    request["user_id"] = current_user.get("user_id")
+    payload = _resolve_request_payload(request, current_user)
+    if not payload.get("addressType") and not payload.get("address_type"):
+        return error_response(400, "地址类型不能为空")
+    payload["user_id"] = current_user.get("user_id")
     creator = {"user_id": current_user.get("user_id"), "real_name": current_user.get("real_name")}
     
     # 如果设置为默认,先取消其他默认
-    if request.get("isDefault"):
+    if payload.get("isDefault"):
         address_crud.set_default(db, current_user.get("user_id"), "")
     
-    address = address_crud.create(db, request, creator)
+    address = address_crud.create(db, payload, creator)
     
     return success_response({
         "id": address.id,
@@ -161,11 +169,12 @@ def update_address(
         return error_response(404, "地址不存在")
     
     # 如果设置为默认,先取消其他默认
-    if request.get("isDefault"):
+    payload = _resolve_request_payload(request, current_user)
+    if payload.get("isDefault"):
         address_crud.set_default(db, current_user.get("user_id"), address_id)
     
     updater = {"user_id": current_user.get("user_id"), "real_name": current_user.get("real_name")}
-    address = address_crud.update_with_history(db, address_id, request, updater)
+    address = address_crud.update_with_history(db, address_id, payload, updater)
     
     return success_response({
         "id": address.id,
@@ -267,8 +276,9 @@ def update_profile(
     if not user:
         return error_response(404, "用户不存在")
     
+    payload = _resolve_request_payload(request, current_user)
     updater = {"user_id": current_user.get("user_id"), "real_name": current_user.get("real_name")}
-    user = user_crud.update_with_history(db, current_user.get("user_id"), request, updater)
+    user = user_crud.update_with_history(db, current_user.get("user_id"), payload, updater)
     
     return success_response({
         "id": user.id,
@@ -469,9 +479,10 @@ def preview_order(
     from app.crud.unit import unit_crud
     from decimal import Decimal
 
-    price_list_id = request.get("priceListId")
-    items = request.get("items", [])
-    address_id = request.get("addressId")
+    payload = _resolve_request_payload(request, current_user)
+    price_list_id = payload.get("priceListId")
+    items = payload.get("items", [])
+    address_id = payload.get("addressId")
 
     if not price_list_id:
         return error_response(400, "请选择价格清单")
@@ -548,15 +559,16 @@ def create_order(
     from app.crud.order import order_crud
     from app.crud.price_list import price_list_crud, price_list_item_crud
 
-    price_list_id = request.get("priceListId")
-    items = request.get("items", [])
-    address_id = request.get("addressId")
-    payment_method_raw = request.get("paymentMethod", "online")
+    payload = _resolve_request_payload(request, current_user)
+    price_list_id = payload.get("priceListId")
+    items = payload.get("items", [])
+    address_id = payload.get("addressId")
+    payment_method_raw = payload.get("paymentMethod", "online")
     payment_method = order_crud.normalize_payment_method(payment_method_raw)
     if payment_method not in ["online", "offline"]:
         return error_response(400, "支付方式仅支持线上或线下")
 
-    remark = request.get("remark")
+    remark = payload.get("remark")
 
     if not price_list_id:
         return error_response(400, "请选择价格清单")
@@ -908,13 +920,14 @@ def review_deliverer(
     if exists:
         return error_response(400, "该订单已评价")
 
-    score = int(request.get("score", 0))
+    payload = _resolve_request_payload(request, current_user)
+    score = int(payload.get("score", 0))
     if score < 1 or score > 5:
         return error_response(400, "评分必须在1-5之间")
 
-    tags = request.get("tags") or []
-    content = request.get("content")
-    is_anonymous = bool(request.get("isAnonymous", False))
+    tags = payload.get("tags") or []
+    content = payload.get("content")
+    is_anonymous = bool(payload.get("isAnonymous", False))
     creator = {
         "user_id": current_user.get("user_id"),
         "real_name": current_user.get("real_name")
@@ -1386,8 +1399,9 @@ def apply_client_refund(
     if order.client_id != client_id:
         return error_response(403, "无权操作此订单")
 
-    refund_amount = request.get("refundAmount")
-    reason = request.get("reason")
+    payload = _resolve_request_payload(request, current_user)
+    refund_amount = payload.get("refundAmount")
+    reason = payload.get("reason")
 
     if not refund_amount or refund_amount <= 0:
         return error_response(400, "退款金额必须大于0")

+ 22 - 12
backend/app/api/mini_client_extra.py

@@ -8,7 +8,7 @@ from sqlalchemy import and_
 from app.core.database import get_db
 from app.core.auth import get_current_user
 from app.core.response import success_response, error_response
-from app.core.config import settings
+from app.core.request_payload import resolve_request_payload
 from app.core.utils import generate_id
 from datetime import datetime
 import random
@@ -31,6 +31,10 @@ from app.api.mini_client import require_client
 router = APIRouter(prefix="/api/mini/client", tags=["小程序端-客户端扩展"])
 
 
+def _resolve_request_payload(request_body: dict, current_user: dict) -> dict:
+    return resolve_request_payload(request_body, current_user, source="mini_client_extra")
+
+
 def _upsert_wechat_pay_pending(
     db: Session,
     *,
@@ -229,8 +233,9 @@ def add_cart(
     db: Session = Depends(get_db),
     current_user: dict = Depends(require_client),
 ):
-    product_id = request.get("productId")
-    quantity = Decimal(str(request.get("quantity", 1)))
+    payload = _resolve_request_payload(request, current_user)
+    product_id = payload.get("productId")
+    quantity = Decimal(str(payload.get("quantity", 1)))
     if not product_id or quantity <= 0:
         return error_response(400, "商品与数量无效")
 
@@ -289,7 +294,8 @@ def put_cart(
     db: Session = Depends(get_db),
     current_user: dict = Depends(require_client),
 ):
-    qty = Decimal(str(request.get("quantity", 0)))
+    payload = _resolve_request_payload(request, current_user)
+    qty = Decimal(str(payload.get("quantity", 0)))
     if qty <= 0:
         return error_response(400, "数量无效")
     uid = current_user.get("user_id")
@@ -336,7 +342,8 @@ def delete_cart_batch(
     db: Session = Depends(get_db),
     current_user: dict = Depends(require_client),
 ):
-    ids = request.get("cartIds") or []
+    payload = _resolve_request_payload(request, current_user)
+    ids = payload.get("cartIds") or []
     if not isinstance(ids, list):
         return error_response(400, "cartIds 格式错误")
     op = {"user_id": current_user.get("user_id"), "real_name": current_user.get("real_name")}
@@ -880,9 +887,10 @@ def create_order_from_cart(
     db: Session = Depends(get_db),
     current_user: dict = Depends(require_client),
 ):
-    address_id = request.get("addressId")
-    payment_method = order_crud.normalize_payment_method(request.get("paymentMethod", "online"))
-    remark = request.get("remark")
+    payload = _resolve_request_payload(request, current_user)
+    address_id = payload.get("addressId")
+    payment_method = order_crud.normalize_payment_method(payload.get("paymentMethod", "online"))
+    remark = payload.get("remark")
     if not address_id:
         return error_response(400, "请选择配送地址")
 
@@ -1073,9 +1081,10 @@ def apply_invoice(
     current_user: dict = Depends(require_client),
 ):
     """申请开票(简化)"""
-    order_id = request.get("orderId")
-    title = request.get("title") or ""
-    invoice_type = request.get("invoiceType") or "normal"
+    payload = _resolve_request_payload(request, current_user)
+    order_id = payload.get("orderId")
+    title = payload.get("title") or ""
+    invoice_type = payload.get("invoiceType") or "normal"
     if not order_id:
         return error_response(400, "缺少 orderId")
     order = order_crud.get(db, order_id)
@@ -1124,7 +1133,8 @@ async def mini_order_wechat_pay(
     """调起微信支付参数(需 openid)"""
     from app.core.wechat_pay import get_wechat_pay_v2
 
-    openid = request.get("openid")
+    payload = _resolve_request_payload(request, current_user)
+    openid = payload.get("openid")
     if not openid:
         return error_response(400, "缺少 openid")
 

+ 14 - 6
backend/app/api/mini_deliverer.py

@@ -7,6 +7,7 @@ from sqlalchemy.orm import Session
 from app.core.database import get_db
 from app.core.auth import get_current_user
 from app.core.response import success_response, error_response
+from app.core.request_payload import resolve_request_payload
 from app.crud.region import region_crud
 from app.crud.user import user_crud
 from app.crud.order import order_crud, order_item_crud
@@ -18,6 +19,10 @@ from app.crud.attachment import attachment_crud
 router = APIRouter(prefix="/api/mini/deliverer", tags=["小程序端-配送员接口"])
 
 
+def _resolve_request_payload(request_body: dict, current_user: dict) -> dict:
+    return resolve_request_payload(request_body, current_user, source="mini_deliverer")
+
+
 def _supplier_pickup_address_block(db: Session, supplier_id: Optional[str]) -> dict:
     """供应商默认取货地址;无默认时取第一条 pickup 类型地址。"""
     empty = {"fullAddress": None, "contactPerson": None, "contactPhone": None}
@@ -618,8 +623,9 @@ def confirm_pickup(
     if deliverer_status != "pending_pickup" or supplier_prepare_status != "prepared":
         return error_response(400, "当前状态不支持取货(需供应商完成备货)")
 
-    remark = request.get("remark")
-    image_urls = _normalize_image_urls(request.get("imageUrls", []))
+    payload = _resolve_request_payload(request, current_user)
+    remark = payload.get("remark")
+    image_urls = _normalize_image_urls(payload.get("imageUrls", []))
     if not image_urls:
         return error_response(400, "请上传取货凭证照片")
 
@@ -679,9 +685,10 @@ def confirm_deliver(
     if ds != "delivering" and not (ds == "pending_pickup" and ss == "picked_up"):
         return error_response(400, "当前状态不支持送达操作(请先确认取货)")
 
-    items_data = request.get("items", [])
-    remark = request.get("remark")
-    image_urls = _normalize_image_urls(request.get("imageUrls", []))
+    payload = _resolve_request_payload(request, current_user)
+    items_data = payload.get("items", [])
+    remark = payload.get("remark")
+    image_urls = _normalize_image_urls(payload.get("imageUrls", []))
     if not image_urls:
         return error_response(400, "请上传送达凭证照片")
 
@@ -858,8 +865,9 @@ def update_deliverer_profile(
     if not user:
         return error_response(404, "用户不存在")
 
+    payload = _resolve_request_payload(request, current_user)
     updater = {"user_id": current_user.get("user_id"), "real_name": current_user.get("real_name")}
-    user = user_crud.update_with_history(db, current_user.get("user_id"), request, updater)
+    user = user_crud.update_with_history(db, current_user.get("user_id"), payload, updater)
 
     return success_response({
         "id": user.id,

+ 84 - 12
backend/app/api/mini_supplier.py

@@ -2,12 +2,16 @@
 from decimal import Decimal
 from typing import Optional
 from datetime import datetime
+import logging
 from fastapi import APIRouter, Depends, Request, Query
 from sqlalchemy.orm import Session
 
 from app.core.database import get_db
 from app.core.auth import get_current_user, get_client_ip
 from app.core.response import success_response, error_response
+from app.core.config import settings
+from app.core import api_crypto
+from app.core.request_payload import resolve_request_payload
 from app.models.models import BasAddress
 from app.crud.address import address_crud
 from app.crud.user import user_crud
@@ -16,6 +20,7 @@ from app.crud.attachment import attachment_crud
 from app.models.models import BizAttachment
 
 router = APIRouter(prefix="/api/mini/supplier", tags=["小程序端-供应商接口"])
+logger = logging.getLogger("sqxp")
 
 
 def _supplier_quote_unit_yuan_from_pli(pli) -> Optional[float]:
@@ -45,6 +50,11 @@ def require_supplier(current_user: dict = Depends(get_current_user)):
     return current_user
 
 
+def _resolve_request_payload(request_body: dict, current_user: dict) -> dict:
+    """兼容明文与密文信封,返回业务层 payload。"""
+    return resolve_request_payload(request_body, current_user, source="mini_supplier")
+
+
 def _normalize_image_urls(raw) -> list[str]:
     out: list[str] = []
     if isinstance(raw, list):
@@ -173,14 +183,17 @@ def create_address(
     current_user: dict = Depends(require_supplier)
 ):
     """创建地址"""
-    request["user_id"] = current_user.get("user_id")
+    payload = _resolve_request_payload(request, current_user)
+    if not payload.get("addressType") and not payload.get("address_type"):
+        return error_response(400, "地址类型不能为空")
+    payload["user_id"] = current_user.get("user_id")
     creator = {"user_id": current_user.get("user_id"), "real_name": current_user.get("real_name")}
     
     # 如果设置为默认,先取消其他默认
-    if request.get("isDefault"):
+    if payload.get("isDefault"):
         address_crud.set_default(db, current_user.get("user_id"), "")
     
-    address = address_crud.create(db, request, creator)
+    address = address_crud.create(db, payload, creator)
     
     return success_response({
         "id": address.id,
@@ -203,12 +216,14 @@ def update_address(
     if not address or address.user_id != current_user.get("user_id"):
         return error_response(404, "地址不存在")
     
+    payload = _resolve_request_payload(request, current_user)
+
     # 如果设置为默认,先取消其他默认
-    if request.get("isDefault"):
+    if payload.get("isDefault"):
         address_crud.set_default(db, current_user.get("user_id"), address_id)
     
     updater = {"user_id": current_user.get("user_id"), "real_name": current_user.get("real_name")}
-    address = address_crud.update_with_history(db, address_id, request, updater)
+    address = address_crud.update_with_history(db, address_id, payload, updater)
     
     return success_response({
         "id": address.id,
@@ -310,8 +325,9 @@ def update_profile(
     if not user:
         return error_response(404, "用户不存在")
     
+    payload = _resolve_request_payload(request, current_user)
     updater = {"user_id": current_user.get("user_id"), "real_name": current_user.get("real_name")}
-    user = user_crud.update_with_history(db, current_user.get("user_id"), request, updater)
+    user = user_crud.update_with_history(db, current_user.get("user_id"), payload, updater)
     
     return success_response({
         "id": user.id,
@@ -503,6 +519,7 @@ def get_inquiry_detail(
 @router.post("/quotations", response_model=dict)
 def submit_quotation(
     request: dict,
+    raw_request: Request,
     db: Session = Depends(get_db),
     current_user: dict = Depends(require_supplier)
 ):
@@ -516,8 +533,61 @@ def submit_quotation(
     if not user or not user.region_id:
         return error_response(400, "用户未绑定区域")
 
-    inquiry_id = request.get("inquiryId")
-    items = request.get("items", [])
+    req_payload = request if isinstance(request, dict) else {}
+
+    # 兜底:若中间件未生效导致路由仍收到密文信封,则在路由内自行解密
+    if int(req_payload.get("enc", 0) or 0) == 1 and isinstance(req_payload.get("edata"), str):
+        try:
+            user_id_for_crypto = str(current_user.get("user_id") or "")
+            ts = int(req_payload.get("ts"))
+            nonce = str(req_payload.get("nonce") or "")
+            if not user_id_for_crypto or not nonce:
+                return error_response(400, "加密报文缺少必要字段")
+            if not settings.API_ENCRYPT_FIXED_SECRET:
+                return error_response(500, "服务端加密配置缺失")
+            key = api_crypto.derive_key(
+                fixed_secret=settings.API_ENCRYPT_FIXED_SECRET,
+                user_id=user_id_for_crypto,
+                ts=ts,
+                nonce=nonce,
+            )
+            plain = api_crypto.decrypt_json(str(req_payload.get("edata")), key)
+            req_payload = plain.get("data", plain) if isinstance(plain, dict) else {}
+            if not isinstance(req_payload, dict):
+                req_payload = {}
+        except Exception as exc:
+            logger.warning("submit_quotation decrypt fallback failed: err=%s", exc)
+            return error_response(400, "密文解密失败")
+
+    def _extract_payload(d: dict) -> dict:
+        """兼容不同层级的请求体结构:优先返回包含 inquiryId/items 的那层 dict。"""
+        if not isinstance(d, dict):
+            return {}
+        if "inquiryId" in d or "items" in d:
+            return d
+        for k in ("data", "payload", "body"):
+            v = d.get(k)
+            if isinstance(v, dict):
+                found = _extract_payload(v)
+                if found:
+                    return found
+        return d
+
+    payload = _extract_payload(req_payload)
+    inquiry_id = payload.get("inquiryId")
+    items = payload.get("items", [])
+
+    # 诊断:强制打印到 stdout(会落到 logs/app.log),仅输出 keys/长度/数量
+    try:
+        keys = list(payload.keys()) if isinstance(payload, dict) else []
+        iid_len = len(str(inquiry_id)) if inquiry_id is not None else None
+        items_len = len(items) if isinstance(items, list) else None
+        print(
+            f"[submit_quotation debug] keys={keys} inquiryId={repr(inquiry_id)} inquiryId_len={iid_len} items_len={items_len}",
+            flush=True,
+        )
+    except Exception:
+        pass
 
     if not inquiry_id:
         return error_response(400, "询价单ID不能为空")
@@ -1123,7 +1193,8 @@ def supplier_start_stock(
     if (getattr(task, "supplier_prepare_status", None) or "pending_prepare") != "pending_prepare":
         return error_response(400, "当前状态不可开始备货(需为待备货/已分配)")
 
-    remark = (request.get("remark") or "").strip() or None
+    payload = _resolve_request_payload(request, current_user)
+    remark = (payload.get("remark") or "").strip() or None
     task = delivery_task_crud.update_status(
         db,
         task_id=task_id,
@@ -1177,10 +1248,11 @@ def supplier_finish_stock(
         if ds != "pending_pickup":
             return error_response(400, "配送员已取货,备货凭证不可再修改")
 
-    remark = (request.get("remark") or request.get("preparedRemark") or "").strip() or None
-    image_urls = _normalize_image_urls(request.get("imageUrls"))
+    payload = _resolve_request_payload(request, current_user)
+    remark = (payload.get("remark") or payload.get("preparedRemark") or "").strip() or None
+    image_urls = _normalize_image_urls(payload.get("imageUrls"))
     if not image_urls:
-        prep_img = request.get("preparedImageUrl") or request.get("pickupImageUrl")
+        prep_img = payload.get("preparedImageUrl") or payload.get("pickupImageUrl")
         image_urls = _normalize_image_urls(prep_img)
     if not image_urls:
         return error_response(400, "请上传备货完成凭证照片")

+ 11 - 9
backend/app/api/public.py

@@ -34,6 +34,7 @@ from app.crud.region import region_crud
 from app.crud.password_history import password_history_crud
 from app.core.utils import generate_uuid
 from app.core import login_crypto
+from app.core.request_payload import resolve_request_payload
 
 router = APIRouter(prefix="", tags=["公共接口"])
 
@@ -586,15 +587,6 @@ async def mini_change_password(
     db: Session = Depends(get_db)
 ):
     """小程序端修改密码"""
-    old_password = body.get("oldPassword")
-    new_password = body.get("newPassword")
-    
-    if not old_password or not new_password:
-        return error_response(400, "旧密码和新密码都不能为空")
-    
-    if len(new_password) < 6:
-        return error_response(400, "新密码长度不能少于6位")
-    
     authorization = request.headers.get("Authorization", "")
     if not authorization.startswith("Bearer "):
         return error_response(401, "未授权")
@@ -602,6 +594,16 @@ async def mini_change_password(
     token = authorization.replace("Bearer ", "")
     try:
         current_user = await get_current_user_by_token(token=token, db=db)
+        payload = resolve_request_payload(body, current_user, source="mini_public_change_password")
+        old_password = payload.get("oldPassword")
+        new_password = payload.get("newPassword")
+
+        if not old_password or not new_password:
+            return error_response(400, "旧密码和新密码都不能为空")
+
+        if len(new_password) < 6:
+            return error_response(400, "新密码长度不能少于6位")
+
         user_id = current_user.get("user_id")
         user = user_crud.get(db, user_id)
         

+ 49 - 0
backend/app/core/request_payload.py

@@ -0,0 +1,49 @@
+"""统一解析接口请求载荷(明文 / 密文信封)。"""
+
+from __future__ import annotations
+
+import logging
+from typing import Any
+
+from app.core import api_crypto
+from app.core.config import settings
+
+
+def resolve_request_payload(
+    request_body: dict,
+    current_user: dict,
+    *,
+    source: str = "unknown",
+) -> dict:
+    """兼容明文与密文信封,返回业务层 payload。"""
+    logger = logging.getLogger("sqxp")
+    payload = request_body if isinstance(request_body, dict) else {}
+    if int(payload.get("enc", 0) or 0) == 1 and isinstance(payload.get("edata"), str):
+        logger.warning(
+            "%s received encrypted envelope in route handler; middleware may be bypassed",
+            source,
+        )
+        try:
+            user_id_for_crypto = str(current_user.get("user_id") or "")
+            ts = int(payload.get("ts"))
+            nonce = str(payload.get("nonce") or "")
+            if user_id_for_crypto and nonce and settings.API_ENCRYPT_FIXED_SECRET:
+                key = api_crypto.derive_key(
+                    fixed_secret=settings.API_ENCRYPT_FIXED_SECRET,
+                    user_id=user_id_for_crypto,
+                    ts=ts,
+                    nonce=nonce,
+                )
+                plain = api_crypto.decrypt_json(str(payload.get("edata")), key)
+                payload = plain if isinstance(plain, dict) else {}
+                logger.warning(
+                    "%s decrypted payload via route fallback; please verify ApiCryptoMiddleware",
+                    source,
+                )
+        except Exception as exc:
+            logger.warning("%s resolve_request_payload decrypt failed: err=%s", source, exc)
+            return {}
+    if isinstance(payload.get("data"), dict):
+        payload = payload.get("data")
+    return payload if isinstance(payload, dict) else {}
+

+ 2 - 4
backend/app/crud/region.py

@@ -11,10 +11,8 @@ class RegionCRUD(CRUDBase[SysRegion]):
     """区域CRUD操作"""
 
     def get_by_code(self, db: Session, code: str) -> Optional[SysRegion]:
-        """根据编码获取区域"""
-        return db.query(SysRegion).filter(
-            and_(SysRegion.code == code, SysRegion.is_del == 0)
-        ).first()
+        """根据编码获取区域(含已删除,匹配数据库唯一索引约束)"""
+        return db.query(SysRegion).filter(SysRegion.code == code).first()
 
     def get_active_all(self, db: Session) -> List[SysRegion]:
         """获取所有启用的区域"""

+ 126 - 156
graphify-out/GRAPH_REPORT.md

@@ -1,12 +1,12 @@
 # Graph Report - sqxp_副本2  (2026-04-29)
 
 ## Corpus Check
-- 700 files · ~635,193 words
+- 701 files · ~636,299 words
 - Verdict: corpus is large enough that graph structure adds value.
 
 ## Summary
-- 4662 nodes · 21544 edges · 63 communities detected
-- Extraction: 24% EXTRACTED · 76% INFERRED · 0% AMBIGUOUS · INFERRED: 16431 edges (avg confidence: 0.56)
+- 4824 nodes · 23137 edges · 57 communities detected
+- Extraction: 22% EXTRACTED · 78% INFERRED · 0% AMBIGUOUS · INFERRED: 17993 edges (avg confidence: 0.55)
 - Token cost: 0 input · 0 output
 
 ## Community Hubs (Navigation)
@@ -32,59 +32,53 @@
 - [[_COMMUNITY_Community 19|Community 19]]
 - [[_COMMUNITY_Community 20|Community 20]]
 - [[_COMMUNITY_Community 21|Community 21]]
-- [[_COMMUNITY_Community 22|Community 22]]
+- [[_COMMUNITY_Community 24|Community 24]]
 - [[_COMMUNITY_Community 25|Community 25]]
 - [[_COMMUNITY_Community 26|Community 26]]
 - [[_COMMUNITY_Community 27|Community 27]]
-- [[_COMMUNITY_Community 28|Community 28]]
 - [[_COMMUNITY_Community 29|Community 29]]
-- [[_COMMUNITY_Community 30|Community 30]]
 - [[_COMMUNITY_Community 31|Community 31]]
+- [[_COMMUNITY_Community 32|Community 32]]
 - [[_COMMUNITY_Community 33|Community 33]]
-- [[_COMMUNITY_Community 35|Community 35]]
+- [[_COMMUNITY_Community 34|Community 34]]
 - [[_COMMUNITY_Community 36|Community 36]]
 - [[_COMMUNITY_Community 37|Community 37]]
 - [[_COMMUNITY_Community 38|Community 38]]
-- [[_COMMUNITY_Community 40|Community 40]]
-- [[_COMMUNITY_Community 41|Community 41]]
-- [[_COMMUNITY_Community 42|Community 42]]
-- [[_COMMUNITY_Community 43|Community 43]]
+- [[_COMMUNITY_Community 39|Community 39]]
 - [[_COMMUNITY_Community 44|Community 44]]
+- [[_COMMUNITY_Community 46|Community 46]]
+- [[_COMMUNITY_Community 47|Community 47]]
+- [[_COMMUNITY_Community 48|Community 48]]
 - [[_COMMUNITY_Community 49|Community 49]]
+- [[_COMMUNITY_Community 50|Community 50]]
 - [[_COMMUNITY_Community 51|Community 51]]
-- [[_COMMUNITY_Community 52|Community 52]]
 - [[_COMMUNITY_Community 53|Community 53]]
 - [[_COMMUNITY_Community 54|Community 54]]
-- [[_COMMUNITY_Community 55|Community 55]]
-- [[_COMMUNITY_Community 56|Community 56]]
 - [[_COMMUNITY_Community 58|Community 58]]
-- [[_COMMUNITY_Community 59|Community 59]]
-- [[_COMMUNITY_Community 60|Community 60]]
+- [[_COMMUNITY_Community 61|Community 61]]
+- [[_COMMUNITY_Community 62|Community 62]]
 - [[_COMMUNITY_Community 64|Community 64]]
-- [[_COMMUNITY_Community 67|Community 67]]
-- [[_COMMUNITY_Community 68|Community 68]]
-- [[_COMMUNITY_Community 70|Community 70]]
-- [[_COMMUNITY_Community 83|Community 83]]
-- [[_COMMUNITY_Community 93|Community 93]]
-- [[_COMMUNITY_Community 95|Community 95]]
-- [[_COMMUNITY_Community 148|Community 148]]
-- [[_COMMUNITY_Community 149|Community 149]]
-- [[_COMMUNITY_Community 150|Community 150]]
-- [[_COMMUNITY_Community 151|Community 151]]
-- [[_COMMUNITY_Community 152|Community 152]]
-- [[_COMMUNITY_Community 153|Community 153]]
+- [[_COMMUNITY_Community 77|Community 77]]
+- [[_COMMUNITY_Community 87|Community 87]]
+- [[_COMMUNITY_Community 89|Community 89]]
+- [[_COMMUNITY_Community 142|Community 142]]
+- [[_COMMUNITY_Community 143|Community 143]]
+- [[_COMMUNITY_Community 144|Community 144]]
+- [[_COMMUNITY_Community 145|Community 145]]
+- [[_COMMUNITY_Community 146|Community 146]]
+- [[_COMMUNITY_Community 147|Community 147]]
 
 ## God Nodes (most connected - your core abstractions)
-1. `BizPriceListItem` - 541 edges
-2. `SysUser` - 534 edges
-3. `BizOrder` - 531 edges
-4. `BizDeliveryTask` - 520 edges
-5. `BasProduct` - 518 edges
-6. `SysRegion` - 511 edges
-7. `BizInquiry` - 509 edges
-8. `BizOrderItem` - 509 edges
-9. `BasProductCategory` - 508 edges
-10. `BasProductUnit` - 508 edges
+1. `BizPriceListItem` - 672 edges
+2. `BizInquiry` - 601 edges
+3. `BizQuotation` - 599 edges
+4. `BizAttachment` - 589 edges
+5. `BasProduct` - 588 edges
+6. `SysUser` - 579 edges
+7. `BasProductCategory` - 578 edges
+8. `BasProductUnit` - 578 edges
+9. `BizDeliveryTask` - 571 edges
+10. `BizOrder` - 568 edges
 
 ## Surprising Connections (you probably didn't know these)
 - `get_active()` --calls--> `query()`  [INFERRED]
@@ -102,47 +96,47 @@
 
 ### Community 0 - "Community 0"
 Cohesion: 0.01
-Nodes (621): adjust_order(), _aggregate_client_settlement_history(), _aggregate_supplier_settlement_history(), _apply_region_minutes_to_data(), approve_quotation(), assign_deliverer_to_order(), assign_delivery_alias(), batch_create_settlements() (+613 more)
+Nodes (623): adjust_order(), _aggregate_client_settlement_history(), _aggregate_supplier_settlement_history(), _apply_region_minutes_to_data(), approve_quotation(), assign_deliverer_to_order(), assign_delivery_alias(), batch_create_settlements() (+615 more)
 
 ### Community 1 - "Community 1"
-Cohesion: 0.09
-Nodes (537): 获取管理员的区域过滤条件      对于区域管理员,返回其所属区域ID(用于过滤数据)     对于超级管理员(无区域绑定),返回None(不过滤), 更新用户菜单授权(按 code 全量覆盖;兼容明文/密文载荷)。, 更新用户菜单授权(按 code 全量覆盖;兼容明文/密文载荷)。, 与 PUT 同逻辑:前端按 POST 模式调用时复用。, 兼容旧逻辑:返回单区域过滤ID。多区域场景返回排序后首个区域。, 更新用户菜单授权(按 code 全量覆盖;兼容明文/密文载荷)。, 兼容旧逻辑:返回单区域过滤ID。多区域场景返回排序后首个区域。, 上传用户头像图片,返回可写入 avatarUrl 的地址 (+529 more)
+Cohesion: 0.08
+Nodes (585): 获取管理员的区域过滤条件      对于区域管理员,返回其所属区域ID(用于过滤数据)     对于超级管理员(无区域绑定),返回None(不过滤), 更新用户菜单授权(按 code 全量覆盖;兼容明文/密文载荷)。, 更新用户菜单授权(按 code 全量覆盖;兼容明文/密文载荷)。, 与 PUT 同逻辑:前端按 POST 模式调用时复用。, 兼容旧逻辑:返回单区域过滤ID。多区域场景返回排序后首个区域。, 更新用户菜单授权(按 code 全量覆盖;兼容明文/密文载荷)。, 兼容旧逻辑:返回单区域过滤ID。多区域场景返回排序后首个区域。, 上传用户头像图片,返回可写入 avatarUrl 的地址 (+577 more)
 
 ### Community 2 - "Community 2"
 Cohesion: 0.01
-Nodes (178): fullRegion(), if(), if(), FireworkSystem, mapKeys(), VantComponent(), HTMLParser(), parseAttrs() (+170 more)
+Nodes (183): fullRegion(), if(), if(), FireworkSystem, mapKeys(), VantComponent(), HTMLParser(), parseAttrs() (+175 more)
 
 ### Community 3 - "Community 3"
 Cohesion: 0.01
-Nodes (145): calendar(), memo(), record(), handleSelect(), onMenuShow(), showMenu(), onClear(), onInput() (+137 more)
+Nodes (146): calendar(), memo(), record(), handleSelect(), onMenuShow(), showMenu(), onClear(), onInput() (+138 more)
 
 ### Community 4 - "Community 4"
 Cohesion: 0.01
-Nodes (144): handleSubmit(), loadDetail(), onRegionMultiConfirm(), regionLabel(), syncMultiFromForm(), validate(), fetchActiveRegions(), fetchAssignableMenuCodes() (+136 more)
+Nodes (160): fetchActiveRegions(), fetchAssignableMenuCodes(), fetchCategoryList(), fetchCategoryParameters(), fetchCategoryTree(), fetchCreateUnit(), fetchNotDeletedRegions(), fetchProductBannerList() (+152 more)
 
 ### Community 5 - "Community 5"
-Cohesion: 0.02
-Nodes (195): # TODO: 发送微信模板消息提醒配送员, # TODO: 发送微信模板消息提醒配送员, 记录处理过程(不结案,结案由配送完成/取消自动触发), 取消抢单      配送员可以取消自己抢到但尚未开始配送的任务, 配送员查看商品详情:轮播、基础信息、参数、富文本、详情媒体(与供应商端一致,不依赖客户价目表/区域绑定), 供应商默认取货地址;无默认时取第一条 pickup 类型地址。, 即将超时的配送任务(基于 effective_time), 获取待抢单的任务列表      返回所有未分配给特定配送员的任务,配送员可以抢单 (+187 more)
+Cohesion: 0.01
+Nodes (249): InvoiceCreateRequest, MessageCreateRequest, 获取商品的评价列表(仅显示已审核通过的评价), AdjustResponse, Config, CreateAdjustRequest, 兼容明文/密文请求,返回业务 payload。失败时返回 error_response。, BaseModel (+241 more)
 
 ### Community 6 - "Community 6"
-Cohesion: 0.01
-Nodes (236): InvoiceCreateRequest, MessageCreateRequest, 获取商品的评价列表(仅显示已审核通过的评价), AdjustResponse, Config, CreateAdjustRequest, BaseModel, Config (+228 more)
+Cohesion: 0.02
+Nodes (190): # TODO: 发送微信模板消息提醒配送员, # TODO: 发送微信模板消息提醒配送员, 记录处理过程(不结案,结案由配送完成/取消自动触发), 获取我抢到的任务列表      返回当前配送员已抢但尚未开始配送的任务, 获取我抢到的任务列表      返回当前配送员已抢但尚未开始配送的任务, 取消抢单      配送员可以取消自己抢到但尚未开始配送的任务, 取消抢单      配送员可以取消自己抢到但尚未开始配送的任务, 取消抢单      配送员可以取消自己抢到但尚未开始配送的任务 (+182 more)
 
 ### Community 7 - "Community 7"
-Cohesion: 0.02
-Nodes (107): 小程序客户端扩展接口:购物车、商品分类、仪表盘、发票、下单、支付, 可申请开票的已完成订单(简化:已完成且尚无发票), 商品对应的一级(根)分类,供购物车行展示与序列化。, 购物车:先一级分类 sort_order,再商品名、商品 id(稳定)。, 当前用户有效购物车行(含区域价目表同步、过滤已删)。, 将购物车 ORM 行序列化为与 GET /cart 一致的 list 项(仅未删除行)。, 购物车汇总(件数、行数、金额),供 Tab 角标等轻量刷新。, 与 /price-list/current 相同,便于小程序路径统一 (+99 more)
+Cohesion: 0.01
+Nodes (122): handleSubmit(), loadDetail(), onRegionMultiConfirm(), regionLabel(), syncMultiFromForm(), validate(), disableDevTools(), handleLock() (+114 more)
 
 ### Community 8 - "Community 8"
 Cohesion: 0.02
-Nodes (51): fetchGetMenuCodes(), fetchGetUserInfo(), fetchLogin(), fetchGetMenuList(), fetchGetRoleList(), fetchGetUserList(), reload(), addUnit() (+43 more)
+Nodes (75): fetchGetMenuCodes(), fetchGetUserInfo(), fetchLogin(), fetchGetMenuList(), fetchGetRoleList(), fetchGetUserList(), reload(), addUnit() (+67 more)
 
 ### Community 9 - "Community 9"
 Cohesion: 0.03
-Nodes (52): handleFormClose(), _Blur(), created(), init(), inputContentStyle(), inputMaxlength(), inputStyle(), obj2strStyle() (+44 more)
+Nodes (101): 小程序客户端扩展接口:购物车、商品分类、仪表盘、发票、下单、支付, 从叶子分类 id 向上解析(含缓存):返回 (根分类, 叶子分类)。, 可申请开票的已完成订单(简化:已完成且尚无发票), 可申请开票的已完成订单(简化:已完成且尚无发票), 可申请开票的已完成订单(简化:已完成且尚无发票), 商品对应的一级(根)分类,供购物车行展示与序列化。, 从叶子分类 id 向上解析(含缓存):返回 (根分类, 叶子分类)。, 购物车:先一级分类 sort_order,再商品名、商品 id(稳定)。 (+93 more)
 
 ### Community 10 - "Community 10"
 Cohesion: 0.03
-Nodes (52): BaseHTTPMiddleware, ApiCryptoError, _b64url_decode(), _b64url_encode(), decrypt_json(), derive_key(), encrypt_json(), gen_nonce_hex() (+44 more)
+Nodes (52): handleFormClose(), _Blur(), created(), init(), inputContentStyle(), inputMaxlength(), inputStyle(), obj2strStyle() (+44 more)
 
 ### Community 11 - "Community 11"
 Cohesion: 0.03
@@ -150,15 +144,15 @@ Nodes (43): askAbortDownload(), checkLocalStoragePackage(), closeUpdate(), compa
 
 ### Community 12 - "Community 12"
 Cohesion: 0.05
-Nodes (15): fetchGetLatestUpgradeLog(), if(), getDefaultLanguage(), emitUrls(), removeItem(), revokeLocalPreview(), checkStorageCompatibility(), StorageConfig (+7 more)
+Nodes (72): 获取供应商配送任务详情(订单仅返回客户订单号;商品行含主图与规格;单价由价格清单行 price(客户最终单价)与 pricing_coefficient 反推供, 获取供应商配送任务详情(订单仅返回客户订单号;商品行含主图与规格;单价由价格清单行 price(客户最终单价)与 pricing_coefficient 反推供, 获取供应商配送任务详情(订单仅返回客户订单号;商品行含主图与规格;单价由价格清单行 price(客户最终单价)与 pricing_coefficient 反推供, 获取供应商配送任务详情(订单仅返回客户订单号;商品行含主图与规格;单价由价格清单行 price(客户最终单价)与 pricing_coefficient 反推供, 备货完成后通知配送员(微信订阅消息,失败不阻断主流程), 备货完成后通知配送员(微信订阅消息,失败不阻断主流程), 备货完成后通知配送员(微信订阅消息,失败不阻断主流程), 供应商开始备货:assigned -> supplier_stocking,并写入订单状态日志(配送进度) (+64 more)
 
 ### Community 13 - "Community 13"
 Cohesion: 0.05
-Nodes (21): change(), created(), getDataList(), getSelectedValue(), handler(), loadData(), mixinDatacomResData(), modelValue() (+13 more)
+Nodes (14): if(), getDefaultLanguage(), emitUrls(), removeItem(), revokeLocalPreview(), checkStorageCompatibility(), StorageConfig, getSystemStorage() (+6 more)
 
 ### Community 14 - "Community 14"
-Cohesion: 0.09
-Nodes (27): base64ToBytes(), bytesToBase64(), decryptJson(), deriveKey(), encryptJson(), fromBase64Url(), hexToBytes(), sha256Hex() (+19 more)
+Cohesion: 0.06
+Nodes (21): change(), created(), getDataList(), getSelectedValue(), handler(), loadData(), mixinDatacomResData(), modelValue() (+13 more)
 
 ### Community 15 - "Community 15"
 Cohesion: 0.11
@@ -177,245 +171,221 @@ Cohesion: 0.12
 Nodes (15): addCopyButton(), highlightCode(), insertLineNumbers(), isBlockProcessed(), markBlockAsProcessed(), processBlock(), setupHighlightDirective(), setupRippleDirective() (+7 more)
 
 ### Community 19 - "Community 19"
-Cohesion: 0.17
-Nodes (16): animation(), bindTiming(), close(), getDom(), getEl(), getSelectorQuery(), init(), leftOptions() (+8 more)
-
-### Community 20 - "Community 20"
 Cohesion: 0.18
 Nodes (16): defaultAvatarPathByRole(), normalizeAvatarUrlForMini(), resolveUserAvatarUrl(), appendQueryParam(), fallbackVersionFromManifest(), getStaticAssetsVersion(), hasSignedQuery(), hasVersionParam() (+8 more)
 
-### Community 21 - "Community 21"
+### Community 20 - "Community 20"
 Cohesion: 0.16
 Nodes (10): useTable(), useTableImpl(), getColumnChecks(), useTableColumns(), createErrorHandler(), createSmartDebounce(), defaultResponseAdapter(), extractPagination() (+2 more)
 
-### Community 22 - "Community 22"
+### Community 21 - "Community 21"
 Cohesion: 0.28
 Nodes (14): getDirection(), getDisabledType(), getDom(), move(), moveDirection(), openState(), range(), resetTouchStatus() (+6 more)
 
-### Community 25 - "Community 25"
+### Community 24 - "Community 24"
 Cohesion: 0.17
 Nodes (2): clean(), onInput()
 
-### Community 26 - "Community 26"
-Cohesion: 0.29
-Nodes (11): 备货完成后通知配送员(微信订阅消息,失败不阻断主流程), 供应商开始备货:assigned -> supplier_stocking,并写入订单状态日志(配送进度), 供应商备货完成:supplier_stocking -> supplier_ready(待配送员取货),写入订单日志并可通知配送员, [兼容] 请改用 POST /tasks/{id}/start-stock, [兼容] 请改用 POST /tasks/{id}/finish-stock, 供应商端商品详情:轮播、基础信息、参数、富文本、详情媒体, 价格清单行 price 为面向客户的最终单价;pricing_coefficient 为调价系数(%),供货单价 = price / (1 + 系数/100)。, 获取供应商的配送任务列表(含商品缩略列表;供货合计按价格清单还原供货单价后汇总) (+3 more)
-
-### Community 27 - "Community 27"
-Cohesion: 0.33
-Nodes (11): changeFlag(), countDown(), day(), handler(), hour(), minute(), second(), startData() (+3 more)
-
-### Community 28 - "Community 28"
-Cohesion: 0.23
-Nodes (6): formatDate(), friendlyDate(), getDate(), dateShow(), handler(), setAutoRefresh()
-
-### Community 29 - "Community 29"
+### Community 25 - "Community 25"
 Cohesion: 0.24
 Nodes (4): getVal(), leftIconWidth(), navbarHeight(), rightIconWidth()
 
-### Community 30 - "Community 30"
+### Community 26 - "Community 26"
 Cohesion: 0.4
 Nodes (9): backup_table(), count_affected(), get_text_columns(), main(), make_backup_table_name(), parse_args(), quote_ident(), 生成备份表名,控制在 MySQL 64 字符限制内。 (+1 more)
 
-### Community 31 - "Community 31"
+### Community 27 - "Community 27"
 Cohesion: 0.24
 Nodes (3): _calcValue(), _getDecimalScale(), _onBlur()
 
-### Community 33 - "Community 33"
+### Community 29 - "Community 29"
 Cohesion: 0.27
 Nodes (4): dataValue(), emit(), onChange(), setOpen()
 
-### Community 35 - "Community 35"
+### Community 31 - "Community 31"
 Cohesion: 0.25
 Nodes (2): borderStyle(), value2px()
 
-### Community 36 - "Community 36"
+### Community 32 - "Community 32"
 Cohesion: 0.39
 Nodes (6): isIgnorableScriptError(), normalizeErrorMessage(), registerPromiseErrorHandler(), registerResourceErrorHandler(), scriptErrorHandler(), setupErrorHandle()
 
-### Community 37 - "Community 37"
+### Community 33 - "Community 33"
 Cohesion: 0.29
 Nodes (2): handleTouchEnd(), setTransition()
 
-### Community 38 - "Community 38"
+### Community 34 - "Community 34"
 Cohesion: 0.36
 Nodes (1): TableHeightCalculator
 
-### Community 40 - "Community 40"
+### Community 36 - "Community 36"
 Cohesion: 0.36
 Nodes (1): RouteValidator
 
-### Community 41 - "Community 41"
+### Community 37 - "Community 37"
 Cohesion: 0.39
 Nodes (5): chooseAll(), chooseAndUploadFile(), chooseImage(), chooseVideo(), uploadFiles()
 
-### Community 42 - "Community 42"
+### Community 38 - "Community 38"
 Cohesion: 0.33
 Nodes (2): logApiCryptoBootInfo(), maskSecret()
 
-### Community 43 - "Community 43"
-Cohesion: 0.38
-Nodes (4): fetchApiRequestLogList(), handleReset(), handleSearch(), loadData()
-
-### Community 44 - "Community 44"
+### Community 39 - "Community 39"
 Cohesion: 0.38
 Nodes (3): _change(), close(), open()
 
-### Community 49 - "Community 49"
+### Community 44 - "Community 44"
 Cohesion: 0.33
 Nodes (1): BridgeError
 
-### Community 51 - "Community 51"
+### Community 46 - "Community 46"
 Cohesion: 0.4
 Nodes (2): created(), getTable()
 
-### Community 52 - "Community 52"
+### Community 47 - "Community 47"
 Cohesion: 0.47
 Nodes (3): get_file_data(), get_file_ext(), get_file_info()
 
-### Community 53 - "Community 53"
+### Community 48 - "Community 48"
 Cohesion: 0.4
 Nodes (2): created(), getForm()
 
-### Community 54 - "Community 54"
+### Community 49 - "Community 49"
 Cohesion: 0.47
 Nodes (4): onDial(), onTextClick(), dialPhone(), normalizeDialNumber()
 
-### Community 55 - "Community 55"
+### Community 50 - "Community 50"
 Cohesion: 0.4
 Nodes (4): 运营端角色守卫:admin / super_admin 可访问。, 接口菜单权限守卫。super_admin 直接放行。, require_admin_or_super_admin(), require_menu_code()
 
-### Community 56 - "Community 56"
+### Community 51 - "Community 51"
 Cohesion: 0.5
 Nodes (2): focusInput(), handleKeydown()
 
-### Community 58 - "Community 58"
+### Community 53 - "Community 53"
 Cohesion: 0.5
 Nodes (3): BaseSettings, Config, Settings
 
-### Community 59 - "Community 59"
-Cohesion: 0.67
-Nodes (3): build_menu_tree(), get_menu_list(), 获取菜单列表(从 sys_menu 表读取,构建树形结构)
-
-### Community 60 - "Community 60"
+### Community 54 - "Community 54"
 Cohesion: 0.5
 Nodes (1): collectIds()
 
-### Community 64 - "Community 64"
+### Community 58 - "Community 58"
 Cohesion: 0.83
 Nodes (3): classes(), isTrue(), onClick()
 
-### Community 67 - "Community 67"
+### Community 61 - "Community 61"
 Cohesion: 0.67
 Nodes (2): created(), getParent()
 
-### Community 68 - "Community 68"
+### Community 62 - "Community 62"
 Cohesion: 1.0
 Nodes (2): getSettingDefaults(), resetToDefaults()
 
-### Community 70 - "Community 70"
+### Community 64 - "Community 64"
 Cohesion: 1.0
 Nodes (2): defaultAvatarPathByRole(), resolveUserAvatarUrl()
 
-### Community 83 - "Community 83"
+### Community 77 - "Community 77"
 Cohesion: 0.67
 Nodes (1): updateUseModal()
 
-### Community 93 - "Community 93"
+### Community 87 - "Community 87"
 Cohesion: 1.0
 Nodes (2): buildMiniEncryptedLoginBody(), fetchLoginPublicKey()
 
-### Community 95 - "Community 95"
+### Community 89 - "Community 89"
 Cohesion: 0.67
 Nodes (1): return()
 
-### Community 148 - "Community 148"
+### Community 142 - "Community 142"
 Cohesion: 1.0
 Nodes (1): 发送订单状态通知                  Args:             openid: 用户openid             ord
 
-### Community 149 - "Community 149"
+### Community 143 - "Community 143"
 Cohesion: 1.0
 Nodes (1): 发送支付成功通知                  Args:             openid: 用户openid             ord
 
-### Community 150 - "Community 150"
+### Community 144 - "Community 144"
 Cohesion: 1.0
 Nodes (1): 发送配送通知                  Args:             openid: 配送员openid             task
 
-### Community 151 - "Community 151"
+### Community 145 - "Community 145"
 Cohesion: 1.0
 Nodes (1): 发送配送时效预警                  Args:             openid: 配送员openid             ta
 
-### Community 152 - "Community 152"
+### Community 146 - "Community 146"
 Cohesion: 1.0
 Nodes (1): 发送退款结果通知                  Args:             openid: 用户openid             ord
 
-### Community 153 - "Community 153"
+### Community 147 - "Community 147"
 Cohesion: 1.0
 Nodes (1): 发送催单通知给配送员                  Args:             deliver_openid: 配送员openid
 
 ## Knowledge Gaps
-- **72 isolated node(s):** `关闭超时未支付的线上订单(超时分钟数来自系统设置 auto_cancel_minutes)。`, `初始化定时任务      Args:         get_db_session: 数据库会话获取函数(从 app.core.database 导入)`, `登录凭据 RSA 解密(与前端 JSEncrypt PKCS#1 v1.5 兼容)。`, `Base64 密文 -> JSON 对象(须含 username、password、ts 毫秒时间戳)。`, `从请求体解析用户名、密码。     返回 (username, password, error_message);error_message 非空时表示失败。` (+67 more)
+- **74 isolated node(s):** `关闭超时未支付的线上订单(超时分钟数来自系统设置 auto_cancel_minutes)。`, `初始化定时任务      Args:         get_db_session: 数据库会话获取函数(从 app.core.database 导入)`, `登录凭据 RSA 解密(与前端 JSEncrypt PKCS#1 v1.5 兼容)。`, `Base64 密文 -> JSON 对象(须含 username、password、ts 毫秒时间戳)。`, `从请求体解析用户名、密码。     返回 (username, password, error_message);error_message 非空时表示失败。` (+69 more)
   These have ≤1 connection - possible missing edges or undocumented components.
-- **Thin community `Community 25`** (13 nodes): `uni-combox.vue`, `clean()`, `data()`, `default()`, `filterCandidates()`, `filterCandidatesLength()`, `handler()`, `labelStyle()`, `onBlur()`, `onFocus()`, `onInput()`, `onSelectorClick()`, `toggleSelector()`
+- **Thin community `Community 24`** (13 nodes): `uni-combox.vue`, `clean()`, `data()`, `default()`, `filterCandidates()`, `filterCandidatesLength()`, `handler()`, `labelStyle()`, `onBlur()`, `onFocus()`, `onInput()`, `onSelectorClick()`, `toggleSelector()`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 35`** (9 nodes): `upload-file.vue`, `borderLineStyle()`, `borderStyle()`, `choose()`, `default()`, `delFile()`, `styles()`, `uploadFiles()`, `value2px()`
+- **Thin community `Community 31`** (9 nodes): `upload-file.vue`, `borderLineStyle()`, `borderStyle()`, `choose()`, `default()`, `delFile()`, `styles()`, `uploadFiles()`, `value2px()`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 37`** (8 nodes): `cleanupEventListeners()`, `handleTouchEnd()`, `handleTouchMove()`, `handleTouchStart()`, `handleWheelScroll()`, `setTransition()`, `setupEventListeners()`, `index.vue`
+- **Thin community `Community 33`** (8 nodes): `cleanupEventListeners()`, `handleTouchEnd()`, `handleTouchMove()`, `handleTouchStart()`, `handleWheelScroll()`, `setTransition()`, `setupEventListeners()`, `index.vue`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 38`** (8 nodes): `TableHeightCalculator`, `.calculate()`, `.calculateOffset()`, `.calculatePaginationOffset()`, `.constructor()`, `.getHeaderHeight()`, `useTableHeight()`, `useTableHeight.ts`
+- **Thin community `Community 34`** (8 nodes): `TableHeightCalculator`, `.calculate()`, `.calculateOffset()`, `.calculatePaginationOffset()`, `.constructor()`, `.getHeaderHeight()`, `useTableHeight()`, `useTableHeight.ts`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 40`** (8 nodes): `RouteValidator`, `.checkComponents()`, `.checkDuplicates()`, `.checkNestedIndexComponent()`, `.logLayoutError()`, `.resolvePath()`, `.validate()`, `RouteValidator.ts`
+- **Thin community `Community 36`** (8 nodes): `RouteValidator`, `.checkComponents()`, `.checkDuplicates()`, `.checkNestedIndexComponent()`, `.logLayoutError()`, `.resolvePath()`, `.validate()`, `RouteValidator.ts`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 42`** (7 nodes): `refreshButtonAutoIcons()`, `refreshPanelTitleIcons()`, `logApiCryptoBootInfo()`, `maskSecret()`, `App.vue`, `main.ts`, `console.ts`
+- **Thin community `Community 38`** (7 nodes): `refreshButtonAutoIcons()`, `refreshPanelTitleIcons()`, `logApiCryptoBootInfo()`, `maskSecret()`, `App.vue`, `main.ts`, `console.ts`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 49`** (6 nodes): `bridge-error.js`, `BridgeError`, `.code()`, `.constructor()`, `.errCode()`, `.errMsg()`
+- **Thin community `Community 44`** (6 nodes): `bridge-error.js`, `BridgeError`, `.code()`, `.constructor()`, `.errCode()`, `.errMsg()`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 51`** (6 nodes): `uni-thead.vue`, `checkboxSelected()`, `created()`, `data()`, `getTable()`, `init()`
+- **Thin community `Community 46`** (6 nodes): `uni-thead.vue`, `checkboxSelected()`, `created()`, `data()`, `getTable()`, `init()`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 53`** (6 nodes): `uni-group.vue`, `created()`, `data()`, `getForm()`, `onClick()`, `title()`
+- **Thin community `Community 48`** (6 nodes): `uni-group.vue`, `created()`, `data()`, `getForm()`, `onClick()`, `title()`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 56`** (5 nodes): `focusInput()`, `handleKeydown()`, `if()`, `search()`, `index.vue`
+- **Thin community `Community 51`** (5 nodes): `focusInput()`, `handleKeydown()`, `if()`, `search()`, `index.vue`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 60`** (4 nodes): `catch()`, `collectIds()`, `handleCategoryTableSelectionChange()`, `index.vue`
+- **Thin community `Community 54`** (4 nodes): `catch()`, `collectIds()`, `handleCategoryTableSelectionChange()`, `index.vue`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 67`** (4 nodes): `popup.js`, `created()`, `data()`, `getParent()`
+- **Thin community `Community 61`** (4 nodes): `popup.js`, `created()`, `data()`, `getParent()`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 68`** (3 nodes): `getSettingDefaults()`, `resetToDefaults()`, `setting.ts`
+- **Thin community `Community 62`** (3 nodes): `getSettingDefaults()`, `resetToDefaults()`, `setting.ts`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 70`** (3 nodes): `defaultAvatarPathByRole()`, `resolveUserAvatarUrl()`, `user-avatar.ts`
+- **Thin community `Community 64`** (3 nodes): `defaultAvatarPathByRole()`, `resolveUserAvatarUrl()`, `user-avatar.ts`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 83`** (3 nodes): `check-update.js`, `check-update.ts`, `updateUseModal()`
+- **Thin community `Community 77`** (3 nodes): `check-update.js`, `check-update.ts`, `updateUseModal()`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 93`** (3 nodes): `loginRsa.js`, `buildMiniEncryptedLoginBody()`, `fetchLoginPublicKey()`
+- **Thin community `Community 87`** (3 nodes): `loginRsa.js`, `buildMiniEncryptedLoginBody()`, `fetchLoginPublicKey()`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 95`** (3 nodes): `return()`, `detail.vue`, `detail.vue`
+- **Thin community `Community 89`** (3 nodes): `return()`, `detail.vue`, `detail.vue`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 148`** (1 nodes): `发送订单状态通知                  Args:             openid: 用户openid             ord`
+- **Thin community `Community 142`** (1 nodes): `发送订单状态通知                  Args:             openid: 用户openid             ord`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 149`** (1 nodes): `发送支付成功通知                  Args:             openid: 用户openid             ord`
+- **Thin community `Community 143`** (1 nodes): `发送支付成功通知                  Args:             openid: 用户openid             ord`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 150`** (1 nodes): `发送配送通知                  Args:             openid: 配送员openid             task`
+- **Thin community `Community 144`** (1 nodes): `发送配送通知                  Args:             openid: 配送员openid             task`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 151`** (1 nodes): `发送配送时效预警                  Args:             openid: 配送员openid             ta`
+- **Thin community `Community 145`** (1 nodes): `发送配送时效预警                  Args:             openid: 配送员openid             ta`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 152`** (1 nodes): `发送退款结果通知                  Args:             openid: 用户openid             ord`
+- **Thin community `Community 146`** (1 nodes): `发送退款结果通知                  Args:             openid: 用户openid             ord`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 153`** (1 nodes): `发送催单通知给配送员                  Args:             deliver_openid: 配送员openid`
+- **Thin community `Community 147`** (1 nodes): `发送催单通知给配送员                  Args:             deliver_openid: 配送员openid`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
 
 ## Suggested Questions
 _Questions this graph is uniquely positioned to answer:_
 
-- **Why does `push()` connect `Community 2` to `Community 0`, `Community 3`, `Community 4`, `Community 9`, `Community 11`, `Community 12`, `Community 13`?**
-  _High betweenness centrality (0.117) - this node is a cross-community bridge._
-- **Why does `number()` connect `Community 4` to `Community 0`, `Community 2`, `Community 7`, `Community 9`, `Community 11`, `Community 13`, `Community 14`?**
-  _High betweenness centrality (0.055) - this node is a cross-community bridge._
-- **Why does `query()` connect `Community 0` to `Community 17`, `Community 59`, `Community 5`?**
-  _High betweenness centrality (0.038) - this node is a cross-community bridge._
-- **Are the 539 inferred relationships involving `BizPriceListItem` (e.g. with `PriceListCRUD` and `PriceListItemCRUD`) actually correct?**
-  _`BizPriceListItem` has 539 INFERRED edges - model-reasoned connections that need verification._
-- **Are the 532 inferred relationships involving `SysUser` (e.g. with `刷新会话 TTL(用于 refreshToken 延长会话)` and `写入 refreshToken(用于 one-time 使用/吊销)`) actually correct?**
-  _`SysUser` has 532 INFERRED edges - model-reasoned connections that need verification._
-- **Are the 529 inferred relationships involving `BizOrder` (e.g. with `DeliveryTaskCRUD` and `DeliveryTaskItemCRUD`) actually correct?**
-  _`BizOrder` has 529 INFERRED edges - model-reasoned connections that need verification._
-- **Are the 518 inferred relationships involving `BizDeliveryTask` (e.g. with `DeliveryWarningTask` and `配送时效预警定时任务  依据订单分配时写入的配送截止时间(biz_delivery_task.effective_time)及区域表 sys_region.`) actually correct?**
-  _`BizDeliveryTask` has 518 INFERRED edges - model-reasoned connections that need verification._
+- **Why does `push()` connect `Community 2` to `Community 0`, `Community 3`, `Community 4`, `Community 10`, `Community 11`, `Community 13`, `Community 14`?**
+  _High betweenness centrality (0.133) - this node is a cross-community bridge._
+- **Why does `number()` connect `Community 4` to `Community 0`, `Community 2`, `Community 7`, `Community 10`, `Community 11`, `Community 14`?**
+  _High betweenness centrality (0.052) - this node is a cross-community bridge._
+- **Why does `query()` connect `Community 0` to `Community 17`, `Community 6`?**
+  _High betweenness centrality (0.049) - this node is a cross-community bridge._
+- **Are the 670 inferred relationships involving `BizPriceListItem` (e.g. with `PriceListCRUD` and `PriceListItemCRUD`) actually correct?**
+  _`BizPriceListItem` has 670 INFERRED edges - model-reasoned connections that need verification._
+- **Are the 599 inferred relationships involving `BizInquiry` (e.g. with `InquiryCRUD` and `InquiryItemCRUD`) actually correct?**
+  _`BizInquiry` has 599 INFERRED edges - model-reasoned connections that need verification._
+- **Are the 597 inferred relationships involving `BizQuotation` (e.g. with `QuotationCRUD` and `QuotationItemCRUD`) actually correct?**
+  _`BizQuotation` has 597 INFERRED edges - model-reasoned connections that need verification._
+- **Are the 587 inferred relationships involving `BizAttachment` (e.g. with `AttachmentCRUD` and `价格清单行 price 为面向客户的最终单价;pricing_coefficient 为调价系数(%),供货单价 = price / (1 + 系数/100)。`) actually correct?**
+  _`BizAttachment` has 587 INFERRED edges - model-reasoned connections that need verification._

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
graphify-out/cache/ast/0832211eef295d28a145810152f27ab5c2f4dbc9e92f69cc6f759bbf73edc010.json


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
graphify-out/cache/ast/159025ec193f5036237d259bebade59296e24993469b834f8506d2bbdc0ced61.json


+ 1 - 0
graphify-out/cache/ast/17bf88c2db63c348a3f973da816a9c5ae0c3980a7a90bcc7b6f05feb8f97684e.json

@@ -0,0 +1 @@
+{"nodes": [{"id": "users_wuzilong_work_00code_sqxp_2_sqxp_wx_pages_supplier_inquiry_inquiry_vue", "label": "inquiry.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/sqxp-wx/pages/supplier/inquiry/inquiry.vue", "source_location": "L1"}, {"id": "inquiry_inquiry_if", "label": "if()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/sqxp-wx/pages/supplier/inquiry/inquiry.vue", "source_location": "L101"}], "edges": [{"source": "users_wuzilong_work_00code_sqxp_2_sqxp_wx_pages_supplier_inquiry_inquiry_vue", "target": "inquiry_inquiry_if", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/sqxp-wx/pages/supplier/inquiry/inquiry.vue", "source_location": "L101", "weight": 1.0}, {"source": "users_wuzilong_work_00code_sqxp_2_sqxp_wx_pages_supplier_inquiry_inquiry_vue", "target": "inquiry_inquiry_if", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/sqxp-wx/pages/supplier/inquiry/inquiry.vue", "source_location": "L181", "weight": 1.0}], "raw_calls": [{"caller_nid": "inquiry_inquiry_if", "callee": "filter", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/sqxp-wx/pages/supplier/inquiry/inquiry.vue", "source_location": "L102"}, {"caller_nid": "inquiry_inquiry_if", "callee": "setStorageSync", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/sqxp-wx/pages/supplier/inquiry/inquiry.vue", "source_location": "L184"}]}

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
graphify-out/cache/ast/1c8ef12329ad83084ee5462100ff3e0057b9051f96a46ef1fb53be34cd889f41.json


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
graphify-out/cache/ast/1fabf20f5f6b663addcc528b7d96036e554ac183ccc78e27d3c6dc75826c80ec.json


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
graphify-out/cache/ast/2c135a5531363e8d8cffe0d516620b2e3caca691a7dd8bc707a2cd29f02afca4.json


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
graphify-out/cache/ast/2f2f2266e72936173ba1a33f851a90459b22976873c0de704a14476b14ec4a7d.json


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
graphify-out/cache/ast/35eb51bd789c34c701b2868f4c1a8e280a90b44375c08968332920d2ca042696.json


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
graphify-out/cache/ast/3b1989a9573a644ac30113a366c735de801c1e21a2c137d291d938dfc48b20ff.json


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
graphify-out/cache/ast/49a5dcbd47400d3072654649bf0c8bb92ac735cd71bdad9aaec185b5bce8f302.json


+ 1 - 0
graphify-out/cache/ast/579be1b1069b242728e9a7aa02182bfaf2e815e724aeef12ddf20ab80f48e676.json

@@ -0,0 +1 @@
+{"nodes": [{"id": "users_wuzilong_work_00code_sqxp_2_sqxp_wx_pages_supplier_inquiry_inquiry_vue", "label": "inquiry.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/sqxp-wx/pages/supplier/inquiry/inquiry.vue", "source_location": "L1"}, {"id": "inquiry_inquiry_if", "label": "if()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/sqxp-wx/pages/supplier/inquiry/inquiry.vue", "source_location": "L109"}], "edges": [{"source": "users_wuzilong_work_00code_sqxp_2_sqxp_wx_pages_supplier_inquiry_inquiry_vue", "target": "inquiry_inquiry_if", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/sqxp-wx/pages/supplier/inquiry/inquiry.vue", "source_location": "L109", "weight": 1.0}, {"source": "users_wuzilong_work_00code_sqxp_2_sqxp_wx_pages_supplier_inquiry_inquiry_vue", "target": "inquiry_inquiry_if", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/sqxp-wx/pages/supplier/inquiry/inquiry.vue", "source_location": "L189", "weight": 1.0}], "raw_calls": [{"caller_nid": "inquiry_inquiry_if", "callee": "filter", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/sqxp-wx/pages/supplier/inquiry/inquiry.vue", "source_location": "L110"}, {"caller_nid": "inquiry_inquiry_if", "callee": "setStorageSync", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/sqxp-wx/pages/supplier/inquiry/inquiry.vue", "source_location": "L192"}]}

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
graphify-out/cache/ast/58a72eda6fb034b86b31cb1667436ac2246cb276bea5e5eae306c159d4f61122.json


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
graphify-out/cache/ast/67deb3afb12011020f8ae835c6e611eccfb0279c2b06048954ef7ecc8cd7f2ee.json


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
graphify-out/cache/ast/6f51dc265c6e1c914db5d66f21a8f9f361ca17fa1b6c61515fdba852a237b002.json


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
graphify-out/cache/ast/7228d2d93a9c75f1dca876bacad12dc67140daae54d984fb9d45f30b6ba373f9.json


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
graphify-out/cache/ast/777e9fc785e8cec58454cc7ff27820f2cc3f1226bc165a44d1f376b0d3e9683b.json


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
graphify-out/cache/ast/77b808f4ee3a98bacfff0e5381f71f75960804d68114863f98800fabe19f93af.json


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
graphify-out/cache/ast/77eac6b4d0214325a32c6ac68ad638277acb30d46e8c3a9ad64ee4b22ae55183.json


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
graphify-out/cache/ast/7c79f0b04460dcbbc077edb20514b0f9f44fe2257dd93911097ca47178d97254.json


+ 1 - 0
graphify-out/cache/ast/8d97a77c8ce11081915607f3fb742ecaba14fca99f98d1157f0a517972c67dbd.json

@@ -0,0 +1 @@
+{"nodes": [{"id": "users_wuzilong_work_00code_sqxp_2_sqxp_wx_pages_supplier_inquiry_inquiry_vue", "label": "inquiry.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/sqxp-wx/pages/supplier/inquiry/inquiry.vue", "source_location": "L1"}, {"id": "inquiry_inquiry_if", "label": "if()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/sqxp-wx/pages/supplier/inquiry/inquiry.vue", "source_location": "L109"}], "edges": [{"source": "users_wuzilong_work_00code_sqxp_2_sqxp_wx_pages_supplier_inquiry_inquiry_vue", "target": "inquiry_inquiry_if", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/sqxp-wx/pages/supplier/inquiry/inquiry.vue", "source_location": "L109", "weight": 1.0}, {"source": "users_wuzilong_work_00code_sqxp_2_sqxp_wx_pages_supplier_inquiry_inquiry_vue", "target": "inquiry_inquiry_if", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/sqxp-wx/pages/supplier/inquiry/inquiry.vue", "source_location": "L189", "weight": 1.0}], "raw_calls": [{"caller_nid": "inquiry_inquiry_if", "callee": "filter", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/sqxp-wx/pages/supplier/inquiry/inquiry.vue", "source_location": "L110"}, {"caller_nid": "inquiry_inquiry_if", "callee": "setStorageSync", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/sqxp-wx/pages/supplier/inquiry/inquiry.vue", "source_location": "L192"}]}

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
graphify-out/cache/ast/9aa9a41fc342ae174cf320ca89df1e3fb57d43c92bc97aae76dc4dd72ae52341.json


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
graphify-out/cache/ast/a0c95d984a50bb1e69f89618b156beb8d8b3d2d6b12406f4728d4b2e30073764.json


+ 1 - 0
graphify-out/cache/ast/a1d144ebb9f002ea61899496509a372eb68e8dfe8bcec6fd9c76a140817eab78.json

@@ -0,0 +1 @@
+{"nodes": [{"id": "users_wuzilong_work_00code_sqxp_2_sqxp_wx_pages_supplier_product_detail_vue", "label": "detail.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/sqxp-wx/pages/supplier/product/detail.vue", "source_location": "L1"}], "edges": [], "raw_calls": []}

+ 1 - 0
graphify-out/cache/ast/a2ba2f3806ff22be4ff6462d01fd1ebd4ec5bd57b81b2a976d20fa0a831f9fc1.json

@@ -0,0 +1 @@
+{"nodes": [{"id": "users_wuzilong_work_00code_sqxp_2_sqxp_wx_pages_supplier_inquiry_inquiry_vue", "label": "inquiry.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/sqxp-wx/pages/supplier/inquiry/inquiry.vue", "source_location": "L1"}, {"id": "inquiry_inquiry_if", "label": "if()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/sqxp-wx/pages/supplier/inquiry/inquiry.vue", "source_location": "L109"}], "edges": [{"source": "users_wuzilong_work_00code_sqxp_2_sqxp_wx_pages_supplier_inquiry_inquiry_vue", "target": "inquiry_inquiry_if", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/sqxp-wx/pages/supplier/inquiry/inquiry.vue", "source_location": "L109", "weight": 1.0}, {"source": "users_wuzilong_work_00code_sqxp_2_sqxp_wx_pages_supplier_inquiry_inquiry_vue", "target": "inquiry_inquiry_if", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/sqxp-wx/pages/supplier/inquiry/inquiry.vue", "source_location": "L189", "weight": 1.0}], "raw_calls": [{"caller_nid": "inquiry_inquiry_if", "callee": "filter", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/sqxp-wx/pages/supplier/inquiry/inquiry.vue", "source_location": "L110"}, {"caller_nid": "inquiry_inquiry_if", "callee": "setStorageSync", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/sqxp-wx/pages/supplier/inquiry/inquiry.vue", "source_location": "L192"}]}

+ 1 - 0
graphify-out/cache/ast/b723ca84c33b1e6b2782c6bbdbc5ebce5be48cf673ef85fa50ed1ac77371d6d8.json

@@ -0,0 +1 @@
+{"nodes": [{"id": "users_wuzilong_work_00code_sqxp_2_sqxp_wx_pages_supplier_inquiry_inquiry_vue", "label": "inquiry.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/sqxp-wx/pages/supplier/inquiry/inquiry.vue", "source_location": "L1"}, {"id": "inquiry_inquiry_if", "label": "if()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/sqxp-wx/pages/supplier/inquiry/inquiry.vue", "source_location": "L118"}], "edges": [{"source": "users_wuzilong_work_00code_sqxp_2_sqxp_wx_pages_supplier_inquiry_inquiry_vue", "target": "inquiry_inquiry_if", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/sqxp-wx/pages/supplier/inquiry/inquiry.vue", "source_location": "L118", "weight": 1.0}, {"source": "users_wuzilong_work_00code_sqxp_2_sqxp_wx_pages_supplier_inquiry_inquiry_vue", "target": "inquiry_inquiry_if", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/sqxp-wx/pages/supplier/inquiry/inquiry.vue", "source_location": "L198", "weight": 1.0}], "raw_calls": [{"caller_nid": "inquiry_inquiry_if", "callee": "filter", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/sqxp-wx/pages/supplier/inquiry/inquiry.vue", "source_location": "L119"}, {"caller_nid": "inquiry_inquiry_if", "callee": "setStorageSync", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/sqxp-wx/pages/supplier/inquiry/inquiry.vue", "source_location": "L201"}]}

+ 1 - 0
graphify-out/cache/ast/b73f283426dd27759b73abc75d5e5710181bda5f5b8b15d98d323bd4c840b908.json

@@ -0,0 +1 @@
+{"nodes": [{"id": "users_wuzilong_work_00code_sqxp_2_web_admin_src_types_import_components_d_ts", "label": "components.d.ts", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/web-admin/src/types/import/components.d.ts", "source_location": "L1"}], "edges": [], "raw_calls": []}

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
graphify-out/cache/ast/c91fa7f09f279794e3b94dc423802fe3c4bc86c733f47f92d7ba119e3e41e707.json


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
graphify-out/cache/ast/d82eaee94a0d0475fc751ae570af4ff6946ec382ec470f47643933105ac167ec.json


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
graphify-out/cache/ast/e6d5bddf2c0bc167a48f0e9d363c21ee497bc5e191670d634763ab4e9a4160e9.json


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
graphify-out/cache/ast/f04dc1fb9605c18fbff3576f2d9b2fcc76d6ef5739f9959b05ef7b859a686631.json


+ 1 - 0
graphify-out/cache/ast/f2fe8d080b0f3398d51e4e2cd45c1185e7f8152b96e4ae3dac951079c148c6ef.json

@@ -0,0 +1 @@
+{"nodes": [{"id": "users_wuzilong_work_00code_sqxp_2_web_admin_src_types_import_components_d_ts", "label": "components.d.ts", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/web-admin/src/types/import/components.d.ts", "source_location": "L1"}], "edges": [], "raw_calls": []}

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
graphify-out/cache/ast/f7670c300842864a87c299ab1430a44d706cf44c9afbe676bfde6a6f4d2bfcec.json


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
graphify-out/cache/ast/f9d85eaaf2ef87530cbb55fe113c17e63c5b5487407b961ac2ece7a865f1dd41.json


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
graphify-out/cache/ast/fb6cac92e2064e36d364f20736d11dc6298b4972302ac406a120e7b69b0d77d8.json


+ 1 - 0
graphify-out/cache/ast/fd2897605aa8e09922fa40fc5fff41d870e53ff9de78cd1a6e66cd8da0a18c7b.json

@@ -0,0 +1 @@
+{"nodes": [{"id": "users_wuzilong_work_00code_sqxp_2_sqxp_wx_pages_supplier_inquiry_inquiry_vue", "label": "inquiry.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/sqxp-wx/pages/supplier/inquiry/inquiry.vue", "source_location": "L1"}, {"id": "inquiry_inquiry_if", "label": "if()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/sqxp-wx/pages/supplier/inquiry/inquiry.vue", "source_location": "L109"}], "edges": [{"source": "users_wuzilong_work_00code_sqxp_2_sqxp_wx_pages_supplier_inquiry_inquiry_vue", "target": "inquiry_inquiry_if", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/sqxp-wx/pages/supplier/inquiry/inquiry.vue", "source_location": "L109", "weight": 1.0}, {"source": "users_wuzilong_work_00code_sqxp_2_sqxp_wx_pages_supplier_inquiry_inquiry_vue", "target": "inquiry_inquiry_if", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/sqxp-wx/pages/supplier/inquiry/inquiry.vue", "source_location": "L189", "weight": 1.0}], "raw_calls": [{"caller_nid": "inquiry_inquiry_if", "callee": "filter", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/sqxp-wx/pages/supplier/inquiry/inquiry.vue", "source_location": "L110"}, {"caller_nid": "inquiry_inquiry_if", "callee": "setStorageSync", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/sqxp-wx/pages/supplier/inquiry/inquiry.vue", "source_location": "L192"}]}

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 1 - 1
graphify-out/graph.html


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 143 - 107
graphify-out/graph.json


+ 3 - 3
sqxp-wx/pages/client/address/address.vue

@@ -244,8 +244,8 @@ function handleDelete(item) {
   deleteDialogVisible.value = true
 }
 
-function closeDeleteDialog() {
-  if (deleting.value) return
+function closeDeleteDialog(force = false) {
+  if (deleting.value && !force) return
   deleteDialogVisible.value = false
   deletingItem.value = null
 }
@@ -262,7 +262,7 @@ async function confirmDelete() {
     const body = parseResponseJson(req)
     if (body.code === 200) {
       uni.showToast({ title: '已删除', icon: 'success' })
-      closeDeleteDialog()
+      closeDeleteDialog(true)
       await fetchList(true)
     } else {
       uni.showToast({ title: body.message || '删除失败', icon: 'none' })

+ 3 - 3
sqxp-wx/pages/client/address/select.vue

@@ -238,8 +238,8 @@ function handleDelete(item) {
   deleteDialogVisible.value = true
 }
 
-function closeDeleteDialog() {
-  if (deleting.value) return
+function closeDeleteDialog(force = false) {
+  if (deleting.value && !force) return
   deleteDialogVisible.value = false
   deletingItem.value = null
 }
@@ -253,7 +253,7 @@ async function confirmDelete() {
     const body = parseResponseJson(req)
     if (body.code === 200) {
       uni.showToast({ title: '已删除', icon: 'success' })
-      closeDeleteDialog()
+      closeDeleteDialog(true)
       await fetchAddresses(true)
     } else {
       uni.showToast({ title: body.message || '删除失败', icon: 'none' })

+ 3 - 3
sqxp-wx/pages/supplier/address/address.vue

@@ -125,8 +125,8 @@ function handleDelete(item) {
     deletingItem.value = item;
     deleteDialogVisible.value = true;
 }
-function closeDeleteDialog() {
-    if (deleting.value)
+function closeDeleteDialog(force = false) {
+    if (deleting.value && !force)
         return;
     deleteDialogVisible.value = false;
     deletingItem.value = null;
@@ -144,7 +144,7 @@ async function confirmDelete() {
         const body = parseResponseJson(req);
         if (body.code === 200) {
             uni.showToast({ title: '已删除', icon: 'success' });
-            closeDeleteDialog();
+            closeDeleteDialog(true);
             await fetchList(true);
         }
         else {

+ 277 - 32
sqxp-wx/pages/supplier/inquiry/inquiry.vue

@@ -20,6 +20,15 @@ const selectedLeftFilterId = ref('all');
 const priceMap = ref({});
 const keyword = ref('');
 const quickPriceFilter = ref('all');
+const editingProductId = ref('');
+const showConfirmDialog = ref(false);
+const confirmDialogVariant = ref('submit');
+const confirmDialogTitle = ref('提示');
+const confirmDialogDesc = ref('');
+const confirmDialogConfirmText = ref('确认');
+const confirmDialogCancelText = ref('取消');
+const confirmDialogLoading = ref(false);
+let confirmDialogOnConfirm = null;
 const TEMP_PRICE_STORAGE_KEY = 'supplier_inquiry_temp_prices';
 // H5 上 type="digit" 常无法输入小数点;小程序端保留 digit 以调出带小数点键盘
 const isH5Env = typeof window !== 'undefined';
@@ -65,6 +74,15 @@ const firstCategoryPages = computed(() => {
     }
     return pages;
 });
+function hasPositivePrice(productId) {
+    const raw = priceMap.value[productId];
+    if (raw === '' || raw == null)
+        return false;
+    const num = Number(raw);
+    if (Number.isNaN(num))
+        return false;
+    return num > 0;
+}
 const firstCategorySwiperIndex = ref(0);
 const leftCategoryOptions = computed(() => {
     if (selectedFirstCategoryId.value === 'all') {
@@ -91,26 +109,28 @@ const leftCategoryOptions = computed(() => {
 });
 const filteredItems = computed(() => {
     let list = items.value;
+    const currentEditingId = editingProductId.value;
+    const shouldKeepEditing = (item) => currentEditingId && item?.productId === currentEditingId;
     if (selectedFirstCategoryId.value !== 'all') {
-        list = list.filter((item) => (item.firstCategoryId || item.categoryId) === selectedFirstCategoryId.value);
+        list = list.filter((item) => shouldKeepEditing(item) || (item.firstCategoryId || item.categoryId) === selectedFirstCategoryId.value);
     }
     if (selectedLeftFilterId.value !== 'all') {
         if (selectedFirstCategoryId.value === 'all') {
-            list = list.filter((item) => (item.firstCategoryId || item.categoryId) === selectedLeftFilterId.value);
+            list = list.filter((item) => shouldKeepEditing(item) || (item.firstCategoryId || item.categoryId) === selectedLeftFilterId.value);
         }
         else {
-            list = list.filter((item) => getItemSecondCategoryId(item) === selectedLeftFilterId.value);
+            list = list.filter((item) => shouldKeepEditing(item) || getItemSecondCategoryId(item) === selectedLeftFilterId.value);
         }
     }
     const kw = keyword.value.trim().toLowerCase();
     if (kw) {
-        list = list.filter((item) => (item.productName || '').toLowerCase().includes(kw));
+        list = list.filter((item) => shouldKeepEditing(item) || (item.productName || '').toLowerCase().includes(kw));
     }
     if (quickPriceFilter.value === 'empty') {
-        list = list.filter((item) => !priceMap.value[item.productId]);
+        list = list.filter((item) => shouldKeepEditing(item) || !hasPositivePrice(item.productId));
     }
     else if (quickPriceFilter.value === 'filled') {
-        list = list.filter((item) => !!priceMap.value[item.productId]);
+        list = list.filter((item) => shouldKeepEditing(item) || hasPositivePrice(item.productId));
     }
     return list;
 });
@@ -164,6 +184,33 @@ function formatPriceToFixed2(value) {
         return '';
     return num.toFixed(2);
 }
+function saveTempPriceToStorage(productId) {
+    if (!productId)
+        return;
+    const inquiryKey = currentInquiryId.value || '_default';
+    const raw = priceMap.value[productId];
+    const tempPriceMap = uni.getStorageSync(TEMP_PRICE_STORAGE_KEY) || {};
+    const inquiryTempMap = (tempPriceMap[inquiryKey] && typeof tempPriceMap[inquiryKey] === 'object')
+        ? tempPriceMap[inquiryKey]
+        : {};
+    // 价格为空:移除暂存记录(回到未填状态)
+    if (!raw) {
+        if (inquiryTempMap && Object.prototype.hasOwnProperty.call(inquiryTempMap, productId)) {
+            delete inquiryTempMap[productId];
+            tempPriceMap[inquiryKey] = inquiryTempMap;
+            uni.setStorageSync(TEMP_PRICE_STORAGE_KEY, tempPriceMap);
+        }
+        return;
+    }
+    const num = Number(raw);
+    if (Number.isNaN(num) || num < 0)
+        return;
+    tempPriceMap[inquiryKey] = {
+        ...(inquiryTempMap || {}),
+        [productId]: Number(num.toFixed(2))
+    };
+    uni.setStorageSync(TEMP_PRICE_STORAGE_KEY, tempPriceMap);
+}
 async function fetchInquiryData() {
     isLoading.value = true;
     try {
@@ -226,13 +273,62 @@ function applyTempPriceFromStorage() {
 function onPriceInput(productId, value) {
     priceMap.value[productId] = normalizePriceInput(value);
 }
+function onPriceFocus(productId) {
+    editingProductId.value = productId || '';
+}
 function onPriceBlur(productId) {
     priceMap.value[productId] = formatPriceToFixed2(priceMap.value[productId]);
+    saveTempPriceToStorage(productId);
+    if (editingProductId.value === productId) {
+        editingProductId.value = '';
+    }
 }
 function onSearchClick() {
     // 由于列表是实时根据 keyword 过滤,这里主要用于收起键盘
     uni.hideKeyboard && uni.hideKeyboard();
 }
+function closeConfirmDialog() {
+    if (confirmDialogLoading.value)
+        return;
+    showConfirmDialog.value = false;
+    confirmDialogOnConfirm = null;
+}
+async function confirmDialogConfirm() {
+    if (confirmDialogLoading.value)
+        return;
+    const cb = confirmDialogOnConfirm;
+    if (!cb) {
+        closeConfirmDialog();
+        return;
+    }
+    confirmDialogLoading.value = true;
+    try {
+        await cb();
+        showConfirmDialog.value = false;
+        confirmDialogOnConfirm = null;
+    }
+    finally {
+        confirmDialogLoading.value = false;
+    }
+}
+function openSubmitConfirmDialog({ title, desc, confirmText, onConfirm }) {
+    confirmDialogVariant.value = 'submit';
+    confirmDialogTitle.value = title || '提示';
+    confirmDialogDesc.value = desc || '';
+    confirmDialogConfirmText.value = confirmText || '继续提交';
+    confirmDialogCancelText.value = '取消';
+    confirmDialogOnConfirm = onConfirm || null;
+    showConfirmDialog.value = true;
+}
+function openDeleteConfirmDialog({ title, desc, confirmText, onConfirm }) {
+    confirmDialogVariant.value = 'delete';
+    confirmDialogTitle.value = title || '删除';
+    confirmDialogDesc.value = desc || '确认删除吗?';
+    confirmDialogConfirmText.value = confirmText || '删除';
+    confirmDialogCancelText.value = '取消';
+    confirmDialogOnConfirm = onConfirm || null;
+    showConfirmDialog.value = true;
+}
 function goProductDetail(productId) {
     if (!productId)
         return;
@@ -252,33 +348,12 @@ function goProductDetail(productId) {
         }
     });
 }
-async function handleSave() {
-    if (!currentInquiryId.value || items.value.length === 0)
-        return;
-    const invalidItem = items.value.find((item) => {
-        const value = priceMap.value[item.productId];
-        if (!value)
-            return false;
-        const num = Number(value);
-        return Number.isNaN(num) || num < 0;
-    });
-    if (invalidItem) {
-        uni.showToast({ title: '请填写完整且有效的价格', icon: 'none' });
+async function doSubmitQuotation({ emptyCount, inquiryId }) {
+    const finalInquiryId = (inquiryId || currentInquiryId.value || '').trim();
+    if (!finalInquiryId) {
+        uni.showToast({ title: '询价单ID不能为空', icon: 'none' });
         return;
     }
-    const emptyCount = items.value.filter((item) => !priceMap.value[item.productId]).length;
-    if (emptyCount > 0) {
-        const confirmSave = await new Promise((resolve) => {
-            uni.showModal({
-                title: '提示',
-                content: `有 ${emptyCount} 个商品未填写价格,将按 0.00 元保存,是否继续?`,
-                success: (res) => resolve(!!res.confirm),
-                fail: () => resolve(false)
-            });
-        });
-        if (!confirmSave)
-            return;
-    }
     const payloadItems = items.value.map((item) => ({
         productId: item.productId,
         unitId: item.unitId,
@@ -290,7 +365,7 @@ async function handleSave() {
             url: '/mini/supplier/quotations',
             method: 'POST',
             data: {
-                inquiryId: currentInquiryId.value,
+                inquiryId: finalInquiryId,
                 items: payloadItems
             }
         });
@@ -309,6 +384,33 @@ async function handleSave() {
         isSaving.value = false;
     }
 }
+async function handleSave() {
+    if (!currentInquiryId.value || items.value.length === 0)
+        return;
+    const invalidItem = items.value.find((item) => {
+        const value = priceMap.value[item.productId];
+        if (!value)
+            return false;
+        const num = Number(value);
+        return Number.isNaN(num) || num < 0;
+    });
+    if (invalidItem) {
+        uni.showToast({ title: '请填写完整且有效的价格', icon: 'none' });
+        return;
+    }
+    const emptyCount = items.value.filter((item) => !priceMap.value[item.productId]).length;
+    if (emptyCount > 0) {
+        const inquiryIdSnapshot = currentInquiryId.value;
+        openSubmitConfirmDialog({
+            title: '提示',
+            desc: `有 ${emptyCount} 个商品未填写价格,将按 0.00 元保存,是否继续?`,
+            confirmText: '继续提交',
+            onConfirm: () => doSubmitQuotation({ emptyCount, inquiryId: inquiryIdSnapshot })
+        });
+        return;
+    }
+    await doSubmitQuotation({ emptyCount: 0, inquiryId: currentInquiryId.value });
+}
 onShow(() => {
     if (!userStore.isLoggedIn) {
         uni.reLaunch({ url: '/pages/login/login' });
@@ -467,6 +569,7 @@ onShow(() => {
                         :inputmode="priceInputMode"
                         :value="priceMap[item.productId]"
                         placeholder="请输入价格"
+                        @focus="onPriceFocus(item.productId)"
                         @input="onPriceInput(item.productId, $event.detail.value)"
                         @blur="onPriceBlur(item.productId)"
                         @click.stop
@@ -489,6 +592,30 @@ onShow(() => {
         <VanIcon name="completed-o" size="24" color="#ffffff" />
       </button>
     </view>
+
+    <view v-if="showConfirmDialog" class="sq-confirm-mask" @click="closeConfirmDialog">
+      <view class="sq-confirm" @click.stop>
+        <view class="sq-confirm__icon" :class="confirmDialogVariant">
+          {{ confirmDialogVariant === 'delete' ? '!' : '✓' }}
+        </view>
+        <text class="sq-confirm__title">{{ confirmDialogTitle }}</text>
+        <text class="sq-confirm__desc">{{ confirmDialogDesc }}</text>
+        <view class="sq-confirm__actions">
+          <view class="sq-confirm__action sq-confirm__action--cancel" @click="closeConfirmDialog">
+            {{ confirmDialogCancelText }}
+          </view>
+          <button
+            class="sq-confirm__action"
+            :class="confirmDialogVariant === 'delete' ? 'sq-confirm__action--danger' : 'sq-confirm__action--primary'"
+            :loading="confirmDialogLoading"
+            :disabled="confirmDialogLoading"
+            @click="confirmDialogConfirm"
+          >
+            {{ confirmDialogConfirmText }}
+          </button>
+        </view>
+      </view>
+    </view>
   </view>
 </template>
 
@@ -987,4 +1114,122 @@ page {
   transform: rotate(45deg);
 }
 
+.sq-confirm-mask {
+  position: fixed;
+  left: 0;
+  top: 0;
+  right: 0;
+  bottom: 0;
+  background: rgba(0, 0, 0, 0.45);
+  z-index: 100;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 48rpx;
+  box-sizing: border-box;
+}
+
+.sq-confirm {
+  width: min(640rpx, 92vw);
+  background: #ffffff;
+  border-radius: 24rpx;
+  padding: 34rpx 30rpx 28rpx;
+  box-sizing: border-box;
+  box-shadow: 0 22rpx 52rpx rgba(0, 0, 0, 0.18);
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+
+.sq-confirm__icon {
+  width: 72rpx;
+  height: 72rpx;
+  border-radius: 999rpx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 40rpx;
+  font-weight: 800;
+  color: #ffffff;
+  background: linear-gradient(145deg, #087f57, #0a9f6d);
+  margin-bottom: 18rpx;
+}
+
+.sq-confirm__icon.delete {
+  background: linear-gradient(145deg, #c24141, #dc2626);
+}
+
+.sq-confirm__title {
+  font-size: 30rpx;
+  font-weight: 700;
+  color: #1f2a26;
+  margin-bottom: 10rpx;
+  text-align: center;
+}
+
+.sq-confirm__desc {
+  font-size: 26rpx;
+  color: #6b7b74;
+  text-align: center;
+  line-height: 1.55;
+  margin-bottom: 26rpx;
+}
+
+.sq-confirm__actions {
+  width: 100%;
+  display: grid;
+  grid-template-columns: 1fr 1fr;
+  gap: 18rpx;
+}
+
+.sq-confirm__action {
+  height: 84rpx;
+  border-radius: 16rpx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 28rpx;
+  font-weight: 700;
+  line-height: 1;
+  padding: 0;
+  box-sizing: border-box;
+  border: none;
+  white-space: nowrap;
+  width: 100%;
+  min-width: 0;
+  justify-self: stretch;
+}
+
+.sq-confirm__action--cancel {
+  background: #f2f3f5;
+  color: #3c4a45;
+  border: 1rpx solid #edf5f1;
+}
+
+.sq-confirm__action::after {
+  border: none;
+}
+
+.sq-confirm__action--primary {
+  background: linear-gradient(145deg, #087f57, #0a9f6d);
+  color: #ffffff;
+  box-shadow: 0 10rpx 22rpx rgba(7, 193, 96, 0.28);
+}
+
+.sq-confirm__action--primary[disabled] {
+  color: #ffffff !important;
+  opacity: 0.92;
+}
+
+.sq-confirm__action--danger {
+  background: linear-gradient(145deg, #b91c1c, #dc2626);
+  color: #ffffff;
+  box-shadow: 0 10rpx 22rpx rgba(220, 38, 38, 0.22);
+}
+
+.sq-confirm__action--danger[disabled] {
+  color: #ffffff !important;
+  opacity: 0.92;
+}
+
 </style>

+ 14 - 0
sqxp-wx/pages/supplier/product/detail.vue

@@ -178,6 +178,19 @@ function onPriceBlur() {
   priceInput.value = formatPriceToFixed2(priceInput.value)
 }
 
+function applyTempPriceForCurrentProduct() {
+  if (!currentProductId.value) return
+  const inquiryKey = inquiryId.value || '_default'
+  const tempPriceMap = uni.getStorageSync(TEMP_PRICE_STORAGE_KEY) || {}
+  const inquiryTempMap = tempPriceMap[inquiryKey]
+  if (!inquiryTempMap || typeof inquiryTempMap !== 'object') return
+  const temp = inquiryTempMap[currentProductId.value]
+  if (temp == null || temp === '') return
+  const num = Number(temp)
+  if (Number.isNaN(num) || num < 0) return
+  priceInput.value = num.toFixed(2)
+}
+
 function onBannerChange(event: any) {
   bannerActiveIndex.value = Number(event?.detail?.current || 0)
 }
@@ -228,6 +241,7 @@ async function loadInquiryContext() {
   inquiryItems.value = Array.isArray(body.data.items) ? body.data.items : []
   const current = inquiryItems.value.find((row) => row.productId === currentProductId.value)
   priceInput.value = current?.lastPrice == null ? '' : Number(current.lastPrice).toFixed(2)
+  applyTempPriceForCurrentProduct()
 }
 
 async function loadQuotationContext() {

+ 3 - 1
sqxp-wx/pages/supplier/settings/password.vue

@@ -134,7 +134,7 @@ onMounted(() => {
         </view>
       </view>
       <button class="btn" type="default" :disabled="submitting" @click="handleSubmit">
-        {{ submitting ? '提交中…' : '确认修改' }}
+        确认修改
       </button>
     </view>
   </view>
@@ -213,6 +213,8 @@ onMounted(() => {
   line-height: 80rpx;
   &[disabled] {
     opacity: 0.68;
+    background: #10b981;
+    color: #ffffff;
   }
 }
 </style>

+ 1 - 1
web-admin/src/api/business.ts

@@ -375,7 +375,7 @@ export function fetchConfirmQuotationSupplier(data: {
 }) {
   return request.post({
     url: '/web/admin/quotations/confirm-supplier',
-    params: data
+    data
   })
 }
 

+ 10 - 13
web-admin/src/views/system/inquiry-quotation/index.vue

@@ -1459,7 +1459,7 @@
                     v-model="row.finalPrice"
                     :precision="2"
                     :step="0.1"
-                    :min="0"
+                    :min="0.01"
                     controls-position="right"
                     style="width: 110px"
                     :disabled="!row.inSale"
@@ -2951,20 +2951,17 @@
   }
 
   const submitConfirmPricing = async () => {
-    if (!selectedInquiryId.value || !confirmPricingContext.quotationId) return
+    const inquiryId = String(selectedInquiryId.value || '').trim()
+    const quotationId = String(confirmPricingContext.quotationId || '').trim()
+    if (!inquiryId || !quotationId) {
+      ElMessage.error('询价单或报价单信息缺失,请关闭弹窗后重试')
+      return
+    }
     const inSaleItems = confirmPricingItems.value.filter((x) => x.inSale)
     if (!inSaleItems.length) {
       ElMessage.warning('当前在售商品为空,无法选中')
       return
     }
-    // 最终售价以运营填写为准:只要在售且 finalPrice>0,就必须提交到后端作为最终结果
-    const finalizableItems = inSaleItems.filter(
-      (x) => Number.isFinite(Number(x.finalPrice)) && Number(x.finalPrice) > 0
-    )
-    if (!finalizableItems.length) {
-      ElMessage.error('请至少设置一个商品的最终售价(必须大于 0)')
-      return
-    }
 
     const invalid = inSaleItems.find(
       (x) => !Number.isFinite(Number(x.finalPrice)) || Number(x.finalPrice) <= 0
@@ -2975,14 +2972,14 @@
     }
     confirmPricingSubmitting.value = true
     try {
-      const finalPriceItems = finalizableItems.map((x) => ({
+      const finalPriceItems = inSaleItems.map((x) => ({
         productId: x.productId,
         price: Number(Number(x.finalPrice).toFixed(2)),
         pricingCoefficient: Number(Number(x.pricingCoefficient ?? 0).toFixed(2))
       }))
       const res: any = await fetchConfirmQuotationSupplier({
-        inquiryId: selectedInquiryId.value,
-        quotationId: confirmPricingContext.quotationId,
+        inquiryId,
+        quotationId,
         finalPriceItems
       })
       ElMessage.success(res?.message || '已生效')

Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio