zlwu пре 6 месеци
родитељ
комит
9c1d40f720
91 измењених фајлова са 6191 додато и 400 уклоњено
  1. 48 0
      .history/backend/.env_20260430114436.production
  2. 48 0
      .history/backend/.env_20260430115928.production
  3. 48 0
      .history/backend/.env_20260430121929.production
  4. 48 0
      .history/backend/.env_20260430121951.production
  5. 48 0
      .history/backend/.env_20260430122155.production
  6. 48 0
      .history/backend/.env_20260430152855.production
  7. 48 0
      .history/backend/.env_20260430153706.production
  8. 48 0
      .history/backend/.env_20260430153753.production
  9. 48 0
      .history/backend/.env_20260430153844.production
  10. 48 0
      .history/backend/.env_20260430154017.production
  11. 48 0
      .history/backend/.env_20260430154606.production
  12. 48 0
      .history/backend/.env_20260430154637.production
  13. 48 0
      .history/backend/.env_20260430170636.production
  14. 21 0
      .history/sqxp-wx/config/static-assets-defaults_20260430152324.js
  15. 21 0
      .history/sqxp-wx/config/static-assets-defaults_20260430152420.js
  16. 16 0
      .history/web-admin/.env_20260430120505.production
  17. 16 0
      .history/web-admin/.env_20260430121810.production
  18. 16 0
      .history/web-admin/.env_20260430121911.production
  19. 46 1
      backend/app/api/admin.py
  20. 4 2
      backend/app/core/config.py
  21. 3 0
      backend/app/core/login_crypto.py
  22. 32 1
      backend/app/ws/price_updates.py
  23. 34 0
      backend/start-local.sh
  24. 9 1
      backend/start-prod.sh
  25. 184 145
      graphify-out/GRAPH_REPORT.md
  26. 0 0
      graphify-out/cache/ast/015281feb18fc5d0b27ce36f2aa099ed464a4dad7302b3013e53d27c23c9c352.json
  27. 1 0
      graphify-out/cache/ast/046c18b54ddde3cc1c6d660d9e84327c083bc2d5d3ee1bd1060ab3d4e3dee7fc.json
  28. 0 0
      graphify-out/cache/ast/0d8aeab6acb06ce9f869984ca58c7545ed84a63870b787fbd0decbf64b365b9b.json
  29. 0 0
      graphify-out/cache/ast/16f862e2be2297f175164a33caf0d307e46c9311186dd936c3bd9e25e59d7600.json
  30. 1 0
      graphify-out/cache/ast/1bdc6c8c8a545016ba8349ae247bd01bfb76402df30e9e8bc57b56e8567e0bad.json
  31. 1 0
      graphify-out/cache/ast/1dfe5f0e9e7a07293e30e2ff72dc850bd0d98caf5af8f82e2e634afd6d4fae6e.json
  32. 0 0
      graphify-out/cache/ast/1fb799468885857a4425518f41a1c5e26527a4c08b03fcd1a4adc65eff9d69c6.json
  33. 0 0
      graphify-out/cache/ast/2039fcc3acb2b2f921dd492b1fadca598cb3f9e920647bad4f52dc040045e6d4.json
  34. 1 0
      graphify-out/cache/ast/253b9a44e45ce4e3033080bca5cb4b7ae933798384942c7293a53f74af1a11f9.json
  35. 1 0
      graphify-out/cache/ast/2b78e8a54234a92d2ef23b3444c7416c72e5dff091ff80f813fe2cc2f2da38b4.json
  36. 1 0
      graphify-out/cache/ast/31654249c44ae8974b7358630fe2a2c064a0a1874ce2ea4c2a81e56aa8585f62.json
  37. 1 0
      graphify-out/cache/ast/364fc1c4cd7772793642451622dee215d8ee7383fbbd3ccbe71cb59e2a655e0e.json
  38. 1 0
      graphify-out/cache/ast/464ade254197992ff588b2d82a6d8192f14c1e9f1b1d151b2844525ab1225325.json
  39. 1 0
      graphify-out/cache/ast/493b5b6154ccdd5a2482cb58e2d19946503059e4403e4886fcfe75c42034d80d.json
  40. 1 0
      graphify-out/cache/ast/504fdd6b5b11c3e14dc47e9a02be9c8f108aad5801cb4599bfddf8d6595319d2.json
  41. 1 0
      graphify-out/cache/ast/55deb4e00b2ca134bff000f33164a014384cafcd7501d88a40805b74dfa54ce4.json
  42. 0 0
      graphify-out/cache/ast/5f8edef24a1d596ffb3dc6de6660cf4f762f254cc1cd3cd82f51910aff81b5e1.json
  43. 1 0
      graphify-out/cache/ast/6419bc8200cb291635cf79ae3db068f7a6f8f73a76c247a7e63fd2e6e7207767.json
  44. 0 0
      graphify-out/cache/ast/68116a0da638286dc50bf49b087aaf8fe199c2e9136096607269b0cba4694568.json
  45. 1 0
      graphify-out/cache/ast/69ba673707506aa7c709d1529ebd926ae127400849353c009cf4c3507e952e04.json
  46. 1 0
      graphify-out/cache/ast/6f81bae306c3cdfe0e20a2fbabc398486613dd3c13ff7cf315cc8712ae342e79.json
  47. 0 0
      graphify-out/cache/ast/77e3d8f368c6a491eb9ae825fee0bf5d774c746c14eae238a071efeb3d499b11.json
  48. 0 0
      graphify-out/cache/ast/845c2391fa01b9246b13bf4b39916355a6ffe85fff61fd4f80107e8236be7b6c.json
  49. 0 0
      graphify-out/cache/ast/87ef4b6f2f48306cf294a2e1e07ee19e1869d00435f6cac882b716833a04aa51.json
  50. 0 0
      graphify-out/cache/ast/88f207dcbb0cd5fc06e794886cb5c7f720f039ce550678c12355eaa8baee4d8a.json
  51. 1 0
      graphify-out/cache/ast/8b4d5d9118a73b4fe309ab632ad81981919a3c915f88b123991a54bfe241cfab.json
  52. 1 0
      graphify-out/cache/ast/97085145a08163397c613210538cbc8d597d293817ab09d90879ac73cd5223fe.json
  53. 0 0
      graphify-out/cache/ast/a9d6badf4cc74a06b02cae34e79525fdf09a735d96e82ebce0edb608e5f26120.json
  54. 1 0
      graphify-out/cache/ast/b2c0261fceb9483d61887f82d8f8ea81a49b6e70f4893763e92845c9d2053307.json
  55. 1 0
      graphify-out/cache/ast/b65ab898ecf9875c825d92a69c32ae6038906e1234ae206555b5c9e6b4e6f90c.json
  56. 0 0
      graphify-out/cache/ast/c0d3397ba83a03e2a7c84ec68b7b36f79439658df3d403c34d8cd7bd68102997.json
  57. 1 0
      graphify-out/cache/ast/c5793b6e6d73d39c45f4a27ab8c0090e5da842d2efc5e7e732cb8c70953e16e0.json
  58. 1 0
      graphify-out/cache/ast/c659ab044f0d78da59f5c58d527b580f90452517cc3dc14caafc509506bd18e4.json
  59. 0 0
      graphify-out/cache/ast/c99eea80b2e98bbb1c8c4bb5cdcd62414a62adf8e16a62a4a09071692fd555c5.json
  60. 0 0
      graphify-out/cache/ast/cc0271c71eab6cbcddb06620de10d6d1236221daed64d323ab0c1f2179314e0a.json
  61. 1 0
      graphify-out/cache/ast/d4249133f46d564c0a8e12987d1a355ea04892b1b068831dbb5dd695b2279131.json
  62. 1 0
      graphify-out/cache/ast/d831b7e737e6ccb5b5ed31d530e6e05caf4e07393e06966f53ba127c15ee8b97.json
  63. 0 0
      graphify-out/cache/ast/dcc76b74cb3c528a47cbc59bf081a8c6bb35a893f758d4a1675174af556ff329.json
  64. 0 0
      graphify-out/cache/ast/ed11a07427a78814e65893c9f78174d148f2b941eb2c2448154abb28018b613f.json
  65. 1 0
      graphify-out/cache/ast/f2776505175c83eb733af2eca5cc06a5565f4e58ada0ec7e8ee92c7caead1fa0.json
  66. 0 0
      graphify-out/cache/ast/f505b309c40e87071c0665e2989e434985f49c6138973b4b04b5d30c2f6dd209.json
  67. 0 0
      graphify-out/cache/ast/f69aa7aecc81d171fd59514ea319ce9d49af6e638e45176f75d2965bccc77c4b.json
  68. 0 0
      graphify-out/cache/ast/f6b6cb234bfb591be9925c2989dbbce3bcbbdb3d664ff674a92cd2698ccca832.json
  69. 0 0
      graphify-out/cache/ast/fb1b64782835b03af4a0a76fac022d8f344a2aa289e3702fb652dff08ce8c876.json
  70. 1 1
      graphify-out/graph.html
  71. 115 115
      graphify-out/graph.json
  72. 3787 0
      sanqiang.sql
  73. 11 0
      sqxp-wx/App.vue
  74. 9 7
      sqxp-wx/config/api.js
  75. 8 8
      sqxp-wx/config/static-assets-defaults.js
  76. 2 2
      sqxp-wx/manifest.json
  77. 1 1
      sqxp-wx/pages/client/cart/cart.vue
  78. 11 1
      sqxp-wx/pages/client/order/create/create.vue
  79. 10 8
      sqxp-wx/pages/client/order/pay/pay.vue
  80. 145 1
      sqxp-wx/pages/login/login.vue
  81. 11 1
      sqxp-wx/pages/supplier/delivery/delivery.vue
  82. 17 4
      sqxp-wx/pages/supplier/inquiry/inquiry.vue
  83. 5 1
      sqxp-wx/pages/supplier/quotation/quotation.vue
  84. 5 23
      sqxp-wx/utils/ossCache.js
  85. 101 20
      sqxp-wx/utils/price-realtime.js
  86. 19 24
      sqxp-wx/utils/request.js
  87. 3 33
      sqxp-wx/utils/staticAsset.js
  88. 5 0
      www.gslyzn.com_0430061311/IIS/README.txt
  89. 8 0
      www.gslyzn.com_0430061311/Tomcat8_4/README.txt
  90. 8 0
      www.gslyzn.com_0430061311/Tomcat8_5+/README.txt
  91. 860 0
      生产环境部署/Ubuntu24.04-生产部署手册.md

+ 48 - 0
.history/backend/.env_20260430114436.production

@@ -0,0 +1,48 @@
+# =========================================
+# 生产环境变量(部署服务器使用)
+# =========================================
+APP_NAME=叁强鲜配
+DEBUG=false
+API_V1_PREFIX=/api
+
+# MySQL(宿主机运行 backend,连接 docker mysql 映射端口)
+DATABASE_URL=mysql+pymysql://root:RootSqxP123!234@127.0.0.1:13306/sanqiang
+
+# JWT(生产必须替换为强随机字符串)
+SECRET_KEY=PLEASE_CHANGE_TO_A_LONG_RANDOM_SECRET_FOR_PRODUCTION
+ALGORITHM=HS256
+ACCESS_TOKEN_EXPIRE_MINUTES=43200
+
+# 存储
+STORAGE_TYPE=minio
+MINIO_ENDPOINT=127.0.0.1:20003
+MINIO_ACCESS_KEY=minioadmin
+MINIO_SECRET_KEY=RootSqxP123!234
+MINIO_BUCKET=sanqiang
+MINIO_SECURE=false
+MINIO_BASE_URL=http://127.0.0.1:20003/sanqiang
+
+# Redis
+REDIS_HOST=127.0.0.1
+REDIS_PORT=16379
+REDIS_DB=8
+REDIS_PASSWORD=RootSqxP123!234
+REDIS_TIMEOUT_SEC=30
+REDIS_SSL_ENABLED=false
+
+# 微信支付(按生产商户实际值填写)
+WXPAY_MCHID=
+WXPAY_MINI_APPID=
+WXPAY_SECRET=
+WXPAY_API_KEY=
+WXPAY_SANDBOX=false
+WXPAY_NOTIFY_URL=https://gslyzn.com/api/payment/notify
+WXPAY_REFUND_NOTIFY_URL=https://gslyzn.com/api/payment/refund-notify
+
+# API 加密(与前端保持一致)
+API_ENCRYPT_ENABLED=true
+API_ENCRYPT_FAIL_OPEN=false
+API_ENCRYPT_FIXED_SECRET=sqxp-web-api-crypto-fixed-secret
+API_ENCRYPT_TS_SKEW_SEC=300
+API_ENCRYPT_NONCE_TTL_SEC=300
+API_ENCRYPT_EXCLUDE_PATHS=/,/health,/docs,/openapi.json,/api/web/auth/login-rsa-public-key,/api/web/auth/login,/api/mini/auth/login-rsa-public-key,/api/mini/auth/login,/api/mini/auth/sms-login,/api/mini/common/china-region-tree,/api/web/admin/attachment/upload,/api/web/admin/products/upload-image,/api/web/admin/categories/upload-image,/api/web/admin/users/upload-avatar,/api/invoice/download

+ 48 - 0
.history/backend/.env_20260430115928.production

@@ -0,0 +1,48 @@
+# =========================================
+# 生产环境变量(部署服务器使用)
+# =========================================
+APP_NAME=叁强鲜配
+DEBUG=false
+API_V1_PREFIX=/api
+
+# MySQL(宿主机运行 backend,连接 docker mysql 映射端口)
+DATABASE_URL=mysql+pymysql://root:RootSqxP123!234@127.0.0.1:13306/sanqiang
+
+# JWT(生产必须替换为强随机字符串)
+SECRET_KEY=PLEASE_CHANGE_TO_A_LONG_RANDOM_SECRET_FOR_PRODUCTION
+ALGORITHM=HS256
+ACCESS_TOKEN_EXPIRE_MINUTES=43200
+
+# 存储
+STORAGE_TYPE=minio
+MINIO_ENDPOINT=127.0.0.1:20003
+MINIO_ACCESS_KEY=minioadmin
+MINIO_SECRET_KEY=RootSqxP123!234
+MINIO_BUCKET=sanqiang
+MINIO_SECURE=false
+MINIO_BASE_URL=http://gslyzn.com/minio-static
+
+# Redis
+REDIS_HOST=127.0.0.1
+REDIS_PORT=16379
+REDIS_DB=8
+REDIS_PASSWORD=RootSqxP123!234
+REDIS_TIMEOUT_SEC=30
+REDIS_SSL_ENABLED=false
+
+# 微信支付(按生产商户实际值填写)
+WXPAY_MCHID=
+WXPAY_MINI_APPID=
+WXPAY_SECRET=
+WXPAY_API_KEY=
+WXPAY_SANDBOX=false
+WXPAY_NOTIFY_URL=https://gslyzn.com/api/payment/notify
+WXPAY_REFUND_NOTIFY_URL=https://gslyzn.com/api/payment/refund-notify
+
+# API 加密(与前端保持一致)
+API_ENCRYPT_ENABLED=true
+API_ENCRYPT_FAIL_OPEN=false
+API_ENCRYPT_FIXED_SECRET=sqxp-web-api-crypto-fixed-secret
+API_ENCRYPT_TS_SKEW_SEC=300
+API_ENCRYPT_NONCE_TTL_SEC=300
+API_ENCRYPT_EXCLUDE_PATHS=/,/health,/docs,/openapi.json,/api/web/auth/login-rsa-public-key,/api/web/auth/login,/api/mini/auth/login-rsa-public-key,/api/mini/auth/login,/api/mini/auth/sms-login,/api/mini/common/china-region-tree,/api/web/admin/attachment/upload,/api/web/admin/products/upload-image,/api/web/admin/categories/upload-image,/api/web/admin/users/upload-avatar,/api/invoice/download

+ 48 - 0
.history/backend/.env_20260430121929.production

@@ -0,0 +1,48 @@
+# =========================================
+# 生产环境变量(部署服务器使用)
+# =========================================
+APP_NAME=叁强鲜配
+DEBUG=false
+API_V1_PREFIX=/api
+
+# MySQL(宿主机运行 backend,连接 docker mysql 映射端口)
+DATABASE_URL=mysql+pymysql://root:RootSqxP123!234@127.0.0.1:13306/sanqiang
+
+# JWT(生产必须替换为强随机字符串)
+SECRET_KEY=PLEASE_CHANGE_TO_A_LONG_RANDOM_SECRET_FOR_PRODUCTION
+ALGORITHM=HS256
+ACCESS_TOKEN_EXPIRE_MINUTES=43200
+
+# 存储
+STORAGE_TYPE=minio
+MINIO_ENDPOINT=127.0.0.1:20003
+MINIO_ACCESS_KEY=minioadmin
+MINIO_SECRET_KEY=RootSqxP123!234
+MINIO_BUCKET=sanqiang
+MINIO_SECURE=false
+MINIO_BASE_URL=https://gslyzn.com/minio-static
+
+# Redis
+REDIS_HOST=127.0.0.1
+REDIS_PORT=16379
+REDIS_DB=8
+REDIS_PASSWORD=RootSqxP123!234
+REDIS_TIMEOUT_SEC=30
+REDIS_SSL_ENABLED=false
+
+# 微信支付(按生产商户实际值填写)
+WXPAY_MCHID=
+WXPAY_MINI_APPID=
+WXPAY_SECRET=
+WXPAY_API_KEY=
+WXPAY_SANDBOX=false
+WXPAY_NOTIFY_URL=http://gslyzn.com/api/payment/notify
+WXPAY_REFUND_NOTIFY_URL=http://gslyzn.com/api/payment/refund-notify
+
+# API 加密(与前端保持一致)
+API_ENCRYPT_ENABLED=true
+API_ENCRYPT_FAIL_OPEN=false
+API_ENCRYPT_FIXED_SECRET=sqxp-web-api-crypto-fixed-secret
+API_ENCRYPT_TS_SKEW_SEC=300
+API_ENCRYPT_NONCE_TTL_SEC=300
+API_ENCRYPT_EXCLUDE_PATHS=/,/health,/docs,/openapi.json,/api/web/auth/login-rsa-public-key,/api/web/auth/login,/api/mini/auth/login-rsa-public-key,/api/mini/auth/login,/api/mini/auth/sms-login,/api/mini/common/china-region-tree,/api/web/admin/attachment/upload,/api/web/admin/products/upload-image,/api/web/admin/categories/upload-image,/api/web/admin/users/upload-avatar,/api/invoice/download

+ 48 - 0
.history/backend/.env_20260430121951.production

@@ -0,0 +1,48 @@
+# =========================================
+# 生产环境变量(部署服务器使用)
+# =========================================
+APP_NAME=叁强鲜配
+DEBUG=false
+API_V1_PREFIX=/api
+
+# MySQL(宿主机运行 backend,连接 docker mysql 映射端口)
+DATABASE_URL=mysql+pymysql://root:RootSqxP123!234@127.0.0.1:13306/sanqiang
+
+# JWT(生产必须替换为强随机字符串)
+SECRET_KEY=PLEASE_CHANGE_TO_A_LONG_RANDOM_SECRET_FOR_PRODUCTION
+ALGORITHM=HS256
+ACCESS_TOKEN_EXPIRE_MINUTES=43200
+
+# 存储
+STORAGE_TYPE=minio
+MINIO_ENDPOINT=127.0.0.1:20003
+MINIO_ACCESS_KEY=minioadmin
+MINIO_SECRET_KEY=RootSqxP123!234
+MINIO_BUCKET=sanqiang
+MINIO_SECURE=false
+MINIO_BASE_URL=https://gslyzn.com/minio-static
+
+# Redis
+REDIS_HOST=127.0.0.1
+REDIS_PORT=16379
+REDIS_DB=8
+REDIS_PASSWORD=RootSqxP123!234
+REDIS_TIMEOUT_SEC=30
+REDIS_SSL_ENABLED=false
+
+# 微信支付(按生产商户实际值填写)
+WXPAY_MCHID=
+WXPAY_MINI_APPID=
+WXPAY_SECRET=
+WXPAY_API_KEY=
+WXPAY_SANDBOX=false
+WXPAY_NOTIFY_URL=https://gslyzn.com/api/payment/notify
+WXPAY_REFUND_NOTIFY_URL=https://gslyzn.com/api/payment/refund-notify
+
+# API 加密(与前端保持一致)
+API_ENCRYPT_ENABLED=true
+API_ENCRYPT_FAIL_OPEN=false
+API_ENCRYPT_FIXED_SECRET=sqxp-web-api-crypto-fixed-secret
+API_ENCRYPT_TS_SKEW_SEC=300
+API_ENCRYPT_NONCE_TTL_SEC=300
+API_ENCRYPT_EXCLUDE_PATHS=/,/health,/docs,/openapi.json,/api/web/auth/login-rsa-public-key,/api/web/auth/login,/api/mini/auth/login-rsa-public-key,/api/mini/auth/login,/api/mini/auth/sms-login,/api/mini/common/china-region-tree,/api/web/admin/attachment/upload,/api/web/admin/products/upload-image,/api/web/admin/categories/upload-image,/api/web/admin/users/upload-avatar,/api/invoice/download

+ 48 - 0
.history/backend/.env_20260430122155.production

@@ -0,0 +1,48 @@
+# =========================================
+# 生产环境变量(部署服务器使用)
+# =========================================
+APP_NAME=叁强鲜配
+DEBUG=false
+API_V1_PREFIX=/api
+
+# MySQL(宿主机运行 backend,连接 docker mysql 映射端口)
+DATABASE_URL=mysql+pymysql://root:RootSqxP123!234@127.0.0.1:13306/sanqiang
+
+# JWT(生产必须替换为强随机字符串)
+SECRET_KEY=PLEASE_CHANGE_TO_A_LONG_RANDOM_SECRET_FOR_PRODUCTION
+ALGORITHM=HS256
+ACCESS_TOKEN_EXPIRE_MINUTES=43200
+
+# 存储
+STORAGE_TYPE=minio
+MINIO_ENDPOINT=127.0.0.1:20003
+MINIO_ACCESS_KEY=minioadmin
+MINIO_SECRET_KEY=RootSqxP123!234
+MINIO_BUCKET=sanqiang
+MINIO_SECURE=false
+MINIO_BASE_URL=http://gslyzn.com/minio-static
+
+# Redis
+REDIS_HOST=127.0.0.1
+REDIS_PORT=16379
+REDIS_DB=8
+REDIS_PASSWORD=RootSqxP123!234
+REDIS_TIMEOUT_SEC=30
+REDIS_SSL_ENABLED=false
+
+# 微信支付(按生产商户实际值填写)
+WXPAY_MCHID=
+WXPAY_MINI_APPID=
+WXPAY_SECRET=
+WXPAY_API_KEY=
+WXPAY_SANDBOX=false
+WXPAY_NOTIFY_URL=https://gslyzn.com/api/payment/notify
+WXPAY_REFUND_NOTIFY_URL=https://gslyzn.com/api/payment/refund-notify
+
+# API 加密(与前端保持一致)
+API_ENCRYPT_ENABLED=true
+API_ENCRYPT_FAIL_OPEN=false
+API_ENCRYPT_FIXED_SECRET=sqxp-web-api-crypto-fixed-secret
+API_ENCRYPT_TS_SKEW_SEC=300
+API_ENCRYPT_NONCE_TTL_SEC=300
+API_ENCRYPT_EXCLUDE_PATHS=/,/health,/docs,/openapi.json,/api/web/auth/login-rsa-public-key,/api/web/auth/login,/api/mini/auth/login-rsa-public-key,/api/mini/auth/login,/api/mini/auth/sms-login,/api/mini/common/china-region-tree,/api/web/admin/attachment/upload,/api/web/admin/products/upload-image,/api/web/admin/categories/upload-image,/api/web/admin/users/upload-avatar,/api/invoice/download

+ 48 - 0
.history/backend/.env_20260430152855.production

@@ -0,0 +1,48 @@
+# =========================================
+# 生产环境变量(部署服务器使用)
+# =========================================
+APP_NAME=叁强鲜配
+DEBUG=false
+API_V1_PREFIX=/api
+
+# MySQL(宿主机运行 backend,连接 docker mysql 映射端口)
+DATABASE_URL=mysql+pymysql://root:RootSqxP123!234@127.0.0.1:13306/sanqiang
+
+# JWT(生产必须替换为强随机字符串)
+SECRET_KEY=PLEASE_CHANGE_TO_A_LONG_RANDOM_SECRET_FOR_PRODUCTION
+ALGORITHM=HS256
+ACCESS_TOKEN_EXPIRE_MINUTES=43200
+
+# 存储
+STORAGE_TYPE=minio
+MINIO_ENDPOINT=127.0.0.1:20003
+MINIO_ACCESS_KEY=minioadmin
+MINIO_SECRET_KEY=RootSqxP123!234
+MINIO_BUCKET=sanqiang
+MINIO_SECURE=false
+MINIO_BASE_URL=https://gslyzn.com/minio-static
+
+# Redis
+REDIS_HOST=127.0.0.1
+REDIS_PORT=16379
+REDIS_DB=8
+REDIS_PASSWORD=RootSqxP123!234
+REDIS_TIMEOUT_SEC=30
+REDIS_SSL_ENABLED=false
+
+# 微信支付(按生产商户实际值填写)
+WXPAY_MCHID=
+WXPAY_MINI_APPID=wx19655f52f540c22e
+WXPAY_SECRET=
+WXPAY_API_KEY=
+WXPAY_SANDBOX=false
+WXPAY_NOTIFY_URL=https://gslyzn.com/api/payment/notify
+WXPAY_REFUND_NOTIFY_URL=https://gslyzn.com/api/payment/refund-notify
+
+# API 加密(与前端保持一致)
+API_ENCRYPT_ENABLED=true
+API_ENCRYPT_FAIL_OPEN=false
+API_ENCRYPT_FIXED_SECRET=sqxp-web-api-crypto-fixed-secret
+API_ENCRYPT_TS_SKEW_SEC=300
+API_ENCRYPT_NONCE_TTL_SEC=300
+API_ENCRYPT_EXCLUDE_PATHS=/,/health,/docs,/openapi.json,/api/web/auth/login-rsa-public-key,/api/web/auth/login,/api/mini/auth/login-rsa-public-key,/api/mini/auth/login,/api/mini/auth/sms-login,/api/mini/common/china-region-tree,/api/web/admin/attachment/upload,/api/web/admin/products/upload-image,/api/web/admin/categories/upload-image,/api/web/admin/users/upload-avatar,/api/invoice/download

+ 48 - 0
.history/backend/.env_20260430153706.production

@@ -0,0 +1,48 @@
+# =========================================
+# 生产环境变量(部署服务器使用)
+# =========================================
+APP_NAME=叁强鲜配
+DEBUG=false
+API_V1_PREFIX=/api
+
+# MySQL(宿主机运行 backend,连接 docker mysql 映射端口)
+DATABASE_URL=mysql+pymysql://root:RootSqxP123!234@127.0.0.1:13306/sanqiang
+
+# JWT(生产必须替换为强随机字符串)
+SECRET_KEY=PLEASE_CHANGE_TO_A_LONG_RANDOM_SECRET_FOR_PRODUCTION
+ALGORITHM=HS256
+ACCESS_TOKEN_EXPIRE_MINUTES=43200
+
+# 存储
+STORAGE_TYPE=minio
+MINIO_ENDPOINT=127.0.0.1:20003
+MINIO_ACCESS_KEY=minioadmin
+MINIO_SECRET_KEY=RootSqxP123!234
+MINIO_BUCKET=sanqiang
+MINIO_SECURE=false
+MINIO_BASE_URL=https://gslyzn.com/minio-static
+
+# Redis
+REDIS_HOST=127.0.0.1
+REDIS_PORT=16379
+REDIS_DB=8
+REDIS_PASSWORD=RootSqxP123!234
+REDIS_TIMEOUT_SEC=30
+REDIS_SSL_ENABLED=false
+
+# 微信支付(按生产商户实际值填写)
+WXPAY_MCHID=
+WXPAY_MINI_APPID=wx19655f52f540c22e
+WXPAY_SECRET=
+WXPAY_API_KEY=
+WXPAY_SANDBOX=false
+WXPAY_NOTIFY_URL=https://gslyzn.com/api/payment/notify
+WXPAY_REFUND_NOTIFY_URL=https://gslyzn.com/api/payment/refund-notify
+
+# API 加密(与前端保持一致)
+API_ENCRYPT_ENABLED=true
+API_ENCRYPT_FAIL_OPEN=false
+API_ENCRYPT_FIXED_SECRET=sqxp-web-api-crypto-fixed-secret
+API_ENCRYPT_TS_SKEW_SEC=300
+API_ENCRYPT_NONCE_TTL_SEC=300
+API_ENCRYPT_EXCLUDE_PATHS=/,/health,/docs,/openapi.json,/api/web/auth/login-rsa-public-key,/api/web/auth/login,/api/mini/auth/login-rsa-public-key,/api/mini/auth/login,/api/mini/auth/sms-login,/api/mini/common/china-region-tree,/api/web/admin/attachment/upload,/api/web/admin/products/upload-image,/api/web/admin/categories/upload-image,/api/web/admin/users/upload-avatar,/api/invoice/download

+ 48 - 0
.history/backend/.env_20260430153753.production

@@ -0,0 +1,48 @@
+# =========================================
+# 生产环境变量(部署服务器使用)
+# =========================================
+APP_NAME=叁强鲜配
+DEBUG=false
+API_V1_PREFIX=/api
+
+# MySQL(宿主机运行 backend,连接 docker mysql 映射端口)
+DATABASE_URL=mysql+pymysql://root:RootSqxP123!234@127.0.0.1:13306/sanqiang
+
+# JWT(生产必须替换为强随机字符串)
+SECRET_KEY=PLEASE_CHANGE_TO_A_LONG_RANDOM_SECRET_FOR_PRODUCTION
+ALGORITHM=HS256
+ACCESS_TOKEN_EXPIRE_MINUTES=43200
+
+# 存储
+STORAGE_TYPE=minio
+MINIO_ENDPOINT=127.0.0.1:20003
+MINIO_ACCESS_KEY=minioadmin
+MINIO_SECRET_KEY=RootSqxP123!234
+MINIO_BUCKET=sanqiang
+MINIO_SECURE=false
+MINIO_BASE_URL=https://gslyzn.com/minio-static
+
+# Redis
+REDIS_HOST=127.0.0.1
+REDIS_PORT=16379
+REDIS_DB=8
+REDIS_PASSWORD=RootSqxP123!234
+REDIS_TIMEOUT_SEC=30
+REDIS_SSL_ENABLED=false
+
+# 微信支付(按生产商户实际值填写)
+WXPAY_MCHID=
+WXPAY_MINI_APPID=wx19655f52f540c22e
+WXPAY_SECRET=c1c134ccfe7ad30605430fd6dd4b5310
+WXPAY_API_KEY=
+WXPAY_SANDBOX=false
+WXPAY_NOTIFY_URL=https://gslyzn.com/api/payment/notify
+WXPAY_REFUND_NOTIFY_URL=https://gslyzn.com/api/payment/refund-notify
+
+# API 加密(与前端保持一致)
+API_ENCRYPT_ENABLED=true
+API_ENCRYPT_FAIL_OPEN=false
+API_ENCRYPT_FIXED_SECRET=sqxp-web-api-crypto-fixed-secret
+API_ENCRYPT_TS_SKEW_SEC=300
+API_ENCRYPT_NONCE_TTL_SEC=300
+API_ENCRYPT_EXCLUDE_PATHS=/,/health,/docs,/openapi.json,/api/web/auth/login-rsa-public-key,/api/web/auth/login,/api/mini/auth/login-rsa-public-key,/api/mini/auth/login,/api/mini/auth/sms-login,/api/mini/common/china-region-tree,/api/web/admin/attachment/upload,/api/web/admin/products/upload-image,/api/web/admin/categories/upload-image,/api/web/admin/users/upload-avatar,/api/invoice/download

+ 48 - 0
.history/backend/.env_20260430153844.production

@@ -0,0 +1,48 @@
+# =========================================
+# 生产环境变量(部署服务器使用)
+# =========================================
+APP_NAME=叁强鲜配
+DEBUG=false
+API_V1_PREFIX=/api
+
+# MySQL(宿主机运行 backend,连接 docker mysql 映射端口)
+DATABASE_URL=mysql+pymysql://root:RootSqxP123!234@127.0.0.1:13306/sanqiang
+
+# JWT(生产必须替换为强随机字符串)
+SECRET_KEY=PLEASE_CHANGE_TO_A_LONG_RANDOM_SECRET_FOR_PRODUCTION
+ALGORITHM=HS256
+ACCESS_TOKEN_EXPIRE_MINUTES=43200
+
+# 存储
+STORAGE_TYPE=minio
+MINIO_ENDPOINT=127.0.0.1:20003
+MINIO_ACCESS_KEY=minioadmin
+MINIO_SECRET_KEY=RootSqxP123!234
+MINIO_BUCKET=sanqiang
+MINIO_SECURE=false
+MINIO_BASE_URL=https://gslyzn.com/minio-static
+
+# Redis
+REDIS_HOST=127.0.0.1
+REDIS_PORT=16379
+REDIS_DB=8
+REDIS_PASSWORD=RootSqxP123!234
+REDIS_TIMEOUT_SEC=30
+REDIS_SSL_ENABLED=false
+
+# 微信支付(按生产商户实际值填写)
+WXPAY_MCHID=1111643684
+WXPAY_MINI_APPID=wx19655f52f540c22e
+WXPAY_SECRET=c1c134ccfe7ad30605430fd6dd4b5310
+WXPAY_API_KEY=
+WXPAY_SANDBOX=false
+WXPAY_NOTIFY_URL=https://gslyzn.com/api/payment/notify
+WXPAY_REFUND_NOTIFY_URL=https://gslyzn.com/api/payment/refund-notify
+
+# API 加密(与前端保持一致)
+API_ENCRYPT_ENABLED=true
+API_ENCRYPT_FAIL_OPEN=false
+API_ENCRYPT_FIXED_SECRET=sqxp-web-api-crypto-fixed-secret
+API_ENCRYPT_TS_SKEW_SEC=300
+API_ENCRYPT_NONCE_TTL_SEC=300
+API_ENCRYPT_EXCLUDE_PATHS=/,/health,/docs,/openapi.json,/api/web/auth/login-rsa-public-key,/api/web/auth/login,/api/mini/auth/login-rsa-public-key,/api/mini/auth/login,/api/mini/auth/sms-login,/api/mini/common/china-region-tree,/api/web/admin/attachment/upload,/api/web/admin/products/upload-image,/api/web/admin/categories/upload-image,/api/web/admin/users/upload-avatar,/api/invoice/download

+ 48 - 0
.history/backend/.env_20260430154017.production

@@ -0,0 +1,48 @@
+# =========================================
+# 生产环境变量(部署服务器使用)
+# =========================================
+APP_NAME=叁强鲜配
+DEBUG=false
+API_V1_PREFIX=/api
+
+# MySQL(宿主机运行 backend,连接 docker mysql 映射端口)
+DATABASE_URL=mysql+pymysql://root:RootSqxP123!234@127.0.0.1:13306/sanqiang
+
+# JWT(生产必须替换为强随机字符串)
+SECRET_KEY=PLEASE_CHANGE_TO_A_LONG_RANDOM_SECRET_FOR_PRODUCTION
+ALGORITHM=HS256
+ACCESS_TOKEN_EXPIRE_MINUTES=43200
+
+# 存储
+STORAGE_TYPE=minio
+MINIO_ENDPOINT=127.0.0.1:20003
+MINIO_ACCESS_KEY=minioadmin
+MINIO_SECRET_KEY=RootSqxP123!234
+MINIO_BUCKET=sanqiang
+MINIO_SECURE=false
+MINIO_BASE_URL=https://gslyzn.com/minio-static
+
+# Redis
+REDIS_HOST=127.0.0.1
+REDIS_PORT=16379
+REDIS_DB=8
+REDIS_PASSWORD=RootSqxP123!234
+REDIS_TIMEOUT_SEC=30
+REDIS_SSL_ENABLED=false
+
+# 微信支付(按生产商户实际值填写)
+WXPAY_MCHID=1111643684
+WXPAY_MINI_APPID=wx19655f52f540c22e
+WXPAY_SECRET=c1c134ccfe7ad30605430fd6dd4b5310
+WXPAY_API_KEY=7mK9pQ2xL4vR8tN6bH3cF1yD5uJ0sAeZ
+WXPAY_SANDBOX=false
+WXPAY_NOTIFY_URL=https://gslyzn.com/api/payment/notify
+WXPAY_REFUND_NOTIFY_URL=https://gslyzn.com/api/payment/refund-notify
+
+# API 加密(与前端保持一致)
+API_ENCRYPT_ENABLED=true
+API_ENCRYPT_FAIL_OPEN=false
+API_ENCRYPT_FIXED_SECRET=sqxp-web-api-crypto-fixed-secret
+API_ENCRYPT_TS_SKEW_SEC=300
+API_ENCRYPT_NONCE_TTL_SEC=300
+API_ENCRYPT_EXCLUDE_PATHS=/,/health,/docs,/openapi.json,/api/web/auth/login-rsa-public-key,/api/web/auth/login,/api/mini/auth/login-rsa-public-key,/api/mini/auth/login,/api/mini/auth/sms-login,/api/mini/common/china-region-tree,/api/web/admin/attachment/upload,/api/web/admin/products/upload-image,/api/web/admin/categories/upload-image,/api/web/admin/users/upload-avatar,/api/invoice/download

+ 48 - 0
.history/backend/.env_20260430154606.production

@@ -0,0 +1,48 @@
+# =========================================
+# 生产环境变量(部署服务器使用)
+# =========================================
+APP_NAME=叁强鲜配
+DEBUG=false
+API_V1_PREFIX=/api
+
+# MySQL(宿主机运行 backend,连接 docker mysql 映射端口)
+DATABASE_URL=mysql+pymysql://root:RootSqxP123!234@127.0.0.1:13306/sanqiang
+
+# JWT(生产必须替换为强随机字符串)
+SECRET_KEY=PLEASE_CHANGE_TO_A_LONG_RANDOM_SECRET_FOR_PRODUCTION
+ALGORITHM=HS256
+ACCESS_TOKEN_EXPIRE_MINUTES=43200
+
+# 存储
+STORAGE_TYPE=minio
+MINIO_ENDPOINT=127.0.0.1:20003
+MINIO_ACCESS_KEY=minioadmin
+MINIO_SECRET_KEY=RootSqxP123!234
+MINIO_BUCKET=sanqiang
+MINIO_SECURE=false
+MINIO_BASE_URL=https://gslyzn.com/minio-static
+
+# Redis
+REDIS_HOST=127.0.0.1
+REDIS_PORT=16379
+REDIS_DB=8
+REDIS_PASSWORD=RootSqxP123!234
+REDIS_TIMEOUT_SEC=30
+REDIS_SSL_ENABLED=false
+
+# 微信支付(按生产商户实际值填写)
+WXPAY_MCHID=1111643684
+WXPAY_MINI_APPID=wx19655f52f540c22e
+WXPAY_SECRET=c1c134ccfe7ad30605430fd6dd4b5310
+WXPAY_API_KEY=7mK9pQ2xL4vR8tN6bH3cF1yD5uJ0sAeZ
+WXPAY_SANDBOX=false
+WXPAY_NOTIFY_URL=https://gslyzn.com/api/payment/notify
+WXPAY_REFUND_NOTIFY_URL=https://gslyzn.com/api/payment/refund-notify
+
+# API 加密(与前端保持一致)
+API_ENCRYPT_ENABLED=true
+API_ENCRYPT_FAIL_OPEN=false
+API_ENCRYPT_FIXED_SECRET=sqxp-web-api-crypto-fixed-secret
+API_ENCRYPT_TS_SKEW_SEC=300
+API_ENCRYPT_NONCE_TTL_SEC=300
+API_ENCRYPT_EXCLUDE_PATHS=/,/health,/docs,/openapi.json,/api/web/auth/login-rsa-public-key,/api/web/auth/login,/api/mini/auth/login-rsa-public-key,/api/mini/auth/login,/api/mini/auth/sms-login,/api/mini/common/china-region-tree,/api/web/admin/attachment/upload,/api/web/admin/products/upload-image,/api/web/admin/categories/upload-image,/api/web/admin/users/upload-avatar,/api/invoice/download

+ 48 - 0
.history/backend/.env_20260430154637.production

@@ -0,0 +1,48 @@
+# =========================================
+# 生产环境变量(部署服务器使用)
+# =========================================
+APP_NAME=叁强鲜配
+DEBUG=false
+API_V1_PREFIX=/api
+
+# MySQL(宿主机运行 backend,连接 docker mysql 映射端口)
+DATABASE_URL=mysql+pymysql://root:RootSqxP123!234@127.0.0.1:13306/sanqiang
+
+# JWT(生产必须替换为强随机字符串)
+SECRET_KEY=PLEASE_CHANGE_TO_A_LONG_RANDOM_SECRET_FOR_PRODUCTION
+ALGORITHM=HS256
+ACCESS_TOKEN_EXPIRE_MINUTES=43200
+
+# 存储
+STORAGE_TYPE=minio
+MINIO_ENDPOINT=127.0.0.1:20003
+MINIO_ACCESS_KEY=minioadmin
+MINIO_SECRET_KEY=RootSqxP123!234
+MINIO_BUCKET=sanqiang
+MINIO_SECURE=false
+MINIO_BASE_URL=https://gslyzn.com/minio-static
+
+# Redis
+REDIS_HOST=127.0.0.1
+REDIS_PORT=16379
+REDIS_DB=8
+REDIS_PASSWORD=RootSqxP123!234
+REDIS_TIMEOUT_SEC=30
+REDIS_SSL_ENABLED=false
+
+# 微信支付(按生产商户实际值填写)
+WXPAY_MCHID=1111643684
+WXPAY_MINI_APPID=wx19655f52f540c22e
+WXPAY_SECRET=c1c134ccfe7ad30605430fd6dd4b5310
+WXPAY_API_KEY=7mK9pQ2xL4vR8tN6bH3cF1yD5uJ0sAeZ
+WXPAY_SANDBOX=false
+WXPAY_NOTIFY_URL=https://gslyzn.com/api/payment/notify
+WXPAY_REFUND_NOTIFY_URL=https://gslyzn.com/api/payment/refund-notify
+
+# API 加密(与前端保持一致)
+API_ENCRYPT_ENABLED=true
+API_ENCRYPT_FAIL_OPEN=false
+API_ENCRYPT_FIXED_SECRET=sqxp-web-api-crypto-fixed-secret
+API_ENCRYPT_TS_SKEW_SEC=300
+API_ENCRYPT_NONCE_TTL_SEC=300
+API_ENCRYPT_EXCLUDE_PATHS=/,/health,/docs,/openapi.json,/api/web/auth/login-rsa-public-key,/api/web/auth/login,/api/mini/auth/login-rsa-public-key,/api/mini/auth/login,/api/mini/auth/sms-login,/api/mini/common/china-region-tree,/api/web/admin/attachment/upload,/api/web/admin/products/upload-image,/api/web/admin/categories/upload-image,/api/web/admin/users/upload-avatar,/api/invoice/download

+ 48 - 0
.history/backend/.env_20260430170636.production

@@ -0,0 +1,48 @@
+# =========================================
+# 生产环境变量(部署服务器使用)
+# =========================================
+APP_NAME=叁强鲜配
+DEBUG=false
+API_V1_PREFIX=/api
+
+# MySQL(宿主机运行 backend,连接 docker mysql 映射端口)
+DATABASE_URL=mysql+pymysql://root:RootSqxP123!234@127.0.0.1:13306/sanqiang
+
+# JWT(生产必须替换为强随机字符串)
+SECRET_KEY=F9vK2sQx7mLp4Tn8cR1yW6hDz3Jb0UaE5GqN9tXv2Mf7Pk4Sr8Yw1Hd6Lc3Zj0Bn
+ALGORITHM=HS256
+ACCESS_TOKEN_EXPIRE_MINUTES=43200
+
+# 存储
+STORAGE_TYPE=minio
+MINIO_ENDPOINT=127.0.0.1:20003
+MINIO_ACCESS_KEY=minioadmin
+MINIO_SECRET_KEY=RootSqxP123!234
+MINIO_BUCKET=sanqiang
+MINIO_SECURE=false
+MINIO_BASE_URL=https://gslyzn.com/minio-static
+
+# Redis
+REDIS_HOST=127.0.0.1
+REDIS_PORT=16379
+REDIS_DB=8
+REDIS_PASSWORD=RootSqxP123!234
+REDIS_TIMEOUT_SEC=30
+REDIS_SSL_ENABLED=false
+
+# 微信支付(按生产商户实际值填写)
+WXPAY_MCHID=1111643684
+WXPAY_MINI_APPID=wx19655f52f540c22e
+WXPAY_SECRET=c1c134ccfe7ad30605430fd6dd4b5310
+WXPAY_API_KEY=7mK9pQ2xL4vR8tN6bH3cF1yD5uJ0sAeZ
+WXPAY_SANDBOX=false
+WXPAY_NOTIFY_URL=https://gslyzn.com/api/payment/notify
+WXPAY_REFUND_NOTIFY_URL=https://gslyzn.com/api/payment/refund-notify
+
+# API 加密(与前端保持一致)
+API_ENCRYPT_ENABLED=true
+API_ENCRYPT_FAIL_OPEN=false
+API_ENCRYPT_FIXED_SECRET=sqxp-web-api-crypto-fixed-secret
+API_ENCRYPT_TS_SKEW_SEC=300
+API_ENCRYPT_NONCE_TTL_SEC=300
+API_ENCRYPT_EXCLUDE_PATHS=/,/health,/docs,/openapi.json,/api/web/auth/login-rsa-public-key,/api/web/auth/login,/api/mini/auth/login-rsa-public-key,/api/mini/auth/login,/api/mini/auth/sms-login,/api/mini/common/china-region-tree,/api/web/admin/attachment/upload,/api/web/admin/products/upload-image,/api/web/admin/categories/upload-image,/api/web/admin/users/upload-avatar,/api/invoice/download

+ 21 - 0
.history/sqxp-wx/config/static-assets-defaults_20260430152324.js

@@ -0,0 +1,21 @@
+/**
+ * 远程静态资源根(须以 / 结尾),与部署端 nginx 静态目录一致。
+ *
+ * **测试 / 生产**
+ * - `STATIC_ASSETS_BASE_URL_TEST`:测试(预发)环境。
+ * - `STATIC_ASSETS_BASE_URL_PRODUCTION`:正式环境(上线前替换占位或通过构建注入 VITE_STATIC_ASSETS_BASE_URL)。
+ * - 运行时优先使用 `VITE_STATIC_ASSETS_BASE_URL`(若构建工具注入),否则按 `import.meta.env.MODE === 'production'` 选用生产/测试常量。
+ *
+ * 小程序 tabBar 等若在 pages.json 写完整 URL,请与当前环境基址一致,或运行 `npm run sync-static-public-urls`。
+ */
+/**
+ * 远程静态资源根(须以 / 结尾)。
+ *
+ * 说明:
+ * - 在部分小程序构建产物中,运行时无法读取 `import.meta.env` / `process.env`,因此必须提供可用的兜底基址。
+ * - 生产环境请务必在 `.env.production` 中注入 `VITE_STATIC_ASSETS_BASE_URL`(或 `VUE_APP_STATIC_ASSETS_BASE_URL`)覆盖此兜底。
+ */
+export const STATIC_ASSETS_BASE_URL_TEST = 'https://www.chengyantek.cn/sqxp-static/'
+export const STATIC_ASSETS_BASE_URL_PRODUCTION = 'https://gslyzn.com/minio-static/'
+/** @deprecated 同 TEST,占位兜底 */
+export const STATIC_ASSETS_BASE_URL_DEFAULT = STATIC_ASSETS_BASE_URL_TEST

+ 21 - 0
.history/sqxp-wx/config/static-assets-defaults_20260430152420.js

@@ -0,0 +1,21 @@
+/**
+ * 远程静态资源根(须以 / 结尾),与部署端 nginx 静态目录一致。
+ *
+ * **测试 / 生产**
+ * - `STATIC_ASSETS_BASE_URL_TEST`:测试(预发)环境。
+ * - `STATIC_ASSETS_BASE_URL_PRODUCTION`:正式环境(上线前替换占位或通过构建注入 VITE_STATIC_ASSETS_BASE_URL)。
+ * - 运行时优先使用 `VITE_STATIC_ASSETS_BASE_URL`(若构建工具注入),否则按 `import.meta.env.MODE === 'production'` 选用生产/测试常量。
+ *
+ * 小程序 tabBar 等若在 pages.json 写完整 URL,请与当前环境基址一致,或运行 `npm run sync-static-public-urls`。
+ */
+/**
+ * 远程静态资源根(须以 / 结尾)。
+ *
+ * 说明:
+ * - 在部分小程序构建产物中,运行时无法读取 `import.meta.env` / `process.env`,因此必须提供可用的兜底基址。
+ * - 生产环境请务必在 `.env.production` 中注入 `VITE_STATIC_ASSETS_BASE_URL`(或 `VUE_APP_STATIC_ASSETS_BASE_URL`)覆盖此兜底。
+ */
+export const STATIC_ASSETS_BASE_URL_TEST = 'https://gslyzn.com/minio-static/'
+export const STATIC_ASSETS_BASE_URL_PRODUCTION = 'https://gslyzn.com/minio-static/'
+/** @deprecated 同 TEST,占位兜底 */
+export const STATIC_ASSETS_BASE_URL_DEFAULT = STATIC_ASSETS_BASE_URL_TEST

+ 16 - 0
.history/web-admin/.env_20260430120505.production

@@ -0,0 +1,16 @@
+# Web Admin 生产环境
+VITE_WITH_CREDENTIALS=false
+VITE_API_ENCRYPT_ENABLED=true
+VITE_API_ENCRYPT_FAIL_OPEN=false
+VITE_API_ENCRYPT_FIXED_SECRET=sqxp-web-api-crypto-fixed-secret
+VITE_API_ENCRYPT_EXCLUDE_PATHS=/web/auth/login-rsa-public-key,/web/auth/login,/web/admin/attachment/upload,/web/admin/products/upload-image,/web/admin/categories/upload-image,/web/admin/users/upload-avatar,/api/invoice/download
+# API 地址前缀
+#VITE_API_URL = https://m1.apifoxmock.com/m1/6400575-6097373-default
+# Delete console
+VITE_DROP_CONSOLE = true
+
+VITE_BASE_URL=/
+
+VITE_API_URL=http://gslyzn.com/api
+
+VITE_ACCESS_MODE=frontend

+ 16 - 0
.history/web-admin/.env_20260430121810.production

@@ -0,0 +1,16 @@
+# Web Admin 生产环境
+VITE_WITH_CREDENTIALS=false
+VITE_API_ENCRYPT_ENABLED=true
+VITE_API_ENCRYPT_FAIL_OPEN=false
+VITE_API_ENCRYPT_FIXED_SECRET=sqxp-web-api-crypto-fixed-secret
+VITE_API_ENCRYPT_EXCLUDE_PATHS=/web/auth/login-rsa-public-key,/web/auth/login,/web/admin/attachment/upload,/web/admin/products/upload-image,/web/admin/categories/upload-image,/web/admin/users/upload-avatar,/api/invoice/download
+# API 地址前缀
+#VITE_API_URL = https://m1.apifoxmock.com/m1/6400575-6097373-default
+# Delete console
+VITE_DROP_CONSOLE = true
+
+VITE_BASE_URL=/
+
+VITE_API_URL=https://gslyzn.com/api
+
+VITE_ACCESS_MODE=frontend

+ 16 - 0
.history/web-admin/.env_20260430121911.production

@@ -0,0 +1,16 @@
+# Web Admin 生产环境
+VITE_WITH_CREDENTIALS=false
+VITE_API_ENCRYPT_ENABLED=true
+VITE_API_ENCRYPT_FAIL_OPEN=false
+VITE_API_ENCRYPT_FIXED_SECRET=sqxp-web-api-crypto-fixed-secret
+VITE_API_ENCRYPT_EXCLUDE_PATHS=/web/auth/login-rsa-public-key,/web/auth/login,/web/admin/attachment/upload,/web/admin/products/upload-image,/web/admin/categories/upload-image,/web/admin/users/upload-avatar,/api/invoice/download
+# API 地址前缀
+#VITE_API_URL = https://m1.apifoxmock.com/m1/6400575-6097373-default
+# Delete console
+VITE_DROP_CONSOLE = true
+
+VITE_BASE_URL=/
+
+VITE_API_URL=https://gslyzn.com/api
+
+VITE_ACCESS_MODE=frontend

+ 46 - 1
backend/app/api/admin.py

@@ -1,5 +1,6 @@
 """运营端接口"""
 import json
+import logging
 import random
 import string
 import uuid
@@ -38,6 +39,7 @@ 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=["运营端管理接口"])
+logger = logging.getLogger(__name__)
 
 
 def _numeric_to_float_optional(v) -> Optional[float]:
@@ -3340,6 +3342,23 @@ def publish_price_list(
         effective_type=effective_type
     )
 
+    # 广播:价格清单发布后,通知该区域客户端刷新价格
+    try:
+        from app.ws.price_updates import notify_price_list_updated, PriceListUpdatedEvent
+        db.refresh(price_list)
+        notify_price_list_updated(PriceListUpdatedEvent(
+            regionId=str(getattr(price_list, "region_id", "") or ""),
+            priceListId=str(getattr(price_list, "id", "") or ""),
+            priceVersion=int(getattr(price_list, "version", 0) or 0),
+            publishedTime=str(getattr(price_list, "published_time", None) or getattr(price_list, "publish_time", None) or ""),
+        ))
+    except Exception:
+        # 推送失败不影响主流程
+        logger.exception(
+            "[ws-mini] notify failed in publish_price_list price_list_id=%s",
+            price_list_id,
+        )
+
     return success_response({
         "id": price_list_id,
         "message": "发布成功"
@@ -8070,6 +8089,12 @@ def confirm_quotation_supplier(
     # 广播:该区域生效价格已更新(小程序端可实时刷新)
     try:
         from app.ws.price_updates import notify_price_list_updated, PriceListUpdatedEvent
+        logger.warning(
+            "[ws-mini] trigger confirm_supplier region=%s price_list_id=%s version=%s",
+            inquiry.region_id,
+            price_list_id,
+            next_version,
+        )
         notify_price_list_updated(PriceListUpdatedEvent(
             regionId=inquiry.region_id,
             priceListId=price_list_id,
@@ -8078,7 +8103,11 @@ def confirm_quotation_supplier(
         ))
     except Exception:
         # 推送失败不影响主流程
-        pass
+        logger.exception(
+            "[ws-mini] notify failed in confirm_supplier inquiry_id=%s quotation_id=%s",
+            inquiry_id,
+            quotation_id,
+        )
 
     return success_response({
         "message": "已选择供应商报价并生成生效价格",
@@ -8363,6 +8392,22 @@ def publish_price_version(
 
     db.commit()
 
+    # 广播:价格版本发布后,通知该区域客户端刷新价格
+    try:
+        from app.ws.price_updates import notify_price_list_updated, PriceListUpdatedEvent
+        notify_price_list_updated(PriceListUpdatedEvent(
+            regionId=str(getattr(version, "region_id", "") or ""),
+            priceListId=str(getattr(version, "id", "") or ""),
+            priceVersion=int(getattr(version, "version", 0) or 0),
+            publishedTime=str(getattr(version, "publish_time", None) or ""),
+        ))
+    except Exception:
+        # 推送失败不影响主流程
+        logger.exception(
+            "[ws-mini] notify failed in publish_price_version version_id=%s",
+            version_id,
+        )
+
     return success_response({
         "id": version.id,
         "status": version.status,

+ 4 - 2
backend/app/core/config.py

@@ -1,6 +1,7 @@
 from pydantic_settings import BaseSettings
 from typing import Optional
 from pathlib import Path
+import os
 
 
 class Settings(BaseSettings):
@@ -135,8 +136,9 @@ class Settings(BaseSettings):
     )
 
     class Config:
-        # 让配置文件从后端目录加载(不依赖你从仓库根目录还是 backend 目录启动服务)
-        env_file = str(Path(__file__).resolve().parents[2] / ".env")
+        # 支持通过 ENV_FILE 指定环境文件;未指定时回退到 backend/.env
+        _default_env_file = Path(__file__).resolve().parents[2] / ".env"
+        env_file = os.getenv("ENV_FILE", str(_default_env_file))
         case_sensitive = True
 
 

+ 3 - 0
backend/app/core/login_crypto.py

@@ -26,6 +26,9 @@ def ensure_initialized() -> None:
         return
 
     pem = (settings.LOGIN_RSA_PRIVATE_KEY_PEM or "").strip()
+    # 兼容 .env 中单行保存 PEM(以 "\n" 形式转义换行)的场景
+    if "\\n" in pem and "-----BEGIN" in pem:
+        pem = pem.replace("\\n", "\n")
     if pem:
         _private_key = serialization.load_pem_private_key(
             pem.encode("utf-8"), password=None, backend=default_backend()

+ 32 - 1
backend/app/ws/price_updates.py

@@ -3,6 +3,7 @@
 from __future__ import annotations
 
 import asyncio
+import logging
 from dataclasses import dataclass
 from typing import Dict, Set, Optional, Any
 
@@ -12,6 +13,7 @@ from fastapi import WebSocket, WebSocketDisconnect
 from app.core.database import SessionLocal
 from app.core.auth import get_current_user_by_token
 
+logger = logging.getLogger(__name__)
 
 @dataclass(frozen=True)
 class PriceListUpdatedEvent:
@@ -50,6 +52,7 @@ class RegionWSManager:
 
         region_id = (current_user.get("region_id") or "").strip()
         if not region_id:
+            logger.warning("[ws-mini] reject: empty region_id user=%s", current_user.get("id"))
             await websocket.close(code=1008)
             raise RuntimeError("user has no region_id")
 
@@ -57,6 +60,14 @@ class RegionWSManager:
 
         async with self._lock:
             self._rooms.setdefault(region_id, set()).add(websocket)
+            room_size = len(self._rooms.get(region_id, set()))
+        logger.warning(
+            "[ws-mini] connected user=%s role=%s region=%s room_size=%s",
+            current_user.get("id"),
+            current_user.get("role"),
+            region_id,
+            room_size,
+        )
 
         return current_user
 
@@ -67,8 +78,10 @@ class RegionWSManager:
                 return
             if websocket in s:
                 s.remove(websocket)
+            room_size = len(s)
             if not s:
                 self._rooms.pop(region_id, None)
+        logger.warning("[ws-mini] disconnected region=%s room_size=%s", region_id, room_size if "room_size" in locals() else 0)
 
     async def broadcast_region(self, region_id: str, message: dict) -> int:
         """向 region 房间广播消息。返回尝试发送的连接数。"""
@@ -76,8 +89,15 @@ class RegionWSManager:
             targets = list(self._rooms.get(region_id, set()))
 
         if not targets:
+            logger.warning("[ws-mini] broadcast skip: no targets region=%s event=%s", region_id, message.get("event"))
             return 0
 
+        logger.warning(
+            "[ws-mini] broadcasting region=%s event=%s target_count=%s",
+            region_id,
+            message.get("event"),
+            len(targets),
+        )
         dead: list[WebSocket] = []
         for ws in targets:
             try:
@@ -110,12 +130,14 @@ async def ws_mini_price_updates(websocket: WebSocket) -> None:
             token = auth[7:].strip()
 
     if not token:
+        logger.warning("[ws-mini] reject: missing token ip=%s", websocket.client.host if websocket.client else "")
         await websocket.close(code=1008)
         return
 
     try:
         current_user = await manager.connect(websocket, token=token)
     except Exception:
+        logger.exception("[ws-mini] connect failed")
         # connect 内部会 close;这里兜底
         try:
             await websocket.close(code=1008)
@@ -129,8 +151,10 @@ async def ws_mini_price_updates(websocket: WebSocket) -> None:
         while True:
             await websocket.receive_text()
     except WebSocketDisconnect:
+        logger.warning("[ws-mini] client disconnected user=%s region=%s", current_user.get("id"), region_id)
         pass
     except Exception:
+        logger.exception("[ws-mini] receive loop error user=%s region=%s", current_user.get("id"), region_id)
         pass
     finally:
         await manager.disconnect(websocket, region_id=region_id)
@@ -140,7 +164,14 @@ def notify_price_list_updated(event: PriceListUpdatedEvent) -> None:
     """线程/同步上下文安全:广播价格更新事件。"""
 
     async def _run():
-        await manager.broadcast_region(event.regionId, event.to_message())
+        sent = await manager.broadcast_region(event.regionId, event.to_message())
+        logger.warning(
+            "[ws-mini] notify price update region=%s price_list_id=%s version=%s sent=%s",
+            event.regionId,
+            event.priceListId,
+            event.priceVersion,
+            sent,
+        )
 
     try:
         loop = asyncio.get_running_loop()

+ 34 - 0
backend/start-local.sh

@@ -0,0 +1,34 @@
+#!/bin/bash
+
+set -euo pipefail
+
+BASE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+cd "$BASE_DIR"
+
+ENV_FILE="${ENV_FILE:-$BASE_DIR/.env.local}"
+HOST="${HOST:-127.0.0.1}"
+PORT="${PORT:-8000}"
+APP_MODULE="${APP_MODULE:-app.main:app}"
+VENV_DIR="${VENV_DIR:-$BASE_DIR/venv}"
+UVICORN_BIN="${UVICORN_BIN:-$VENV_DIR/bin/uvicorn}"
+
+if [ ! -f "$ENV_FILE" ]; then
+  echo "[ERROR] 本地环境文件不存在: $ENV_FILE"
+  echo "请先创建并填写 backend/.env.local"
+  exit 1
+fi
+
+if [ ! -x "$UVICORN_BIN" ]; then
+  echo "[ERROR] 未找到 uvicorn: $UVICORN_BIN"
+  echo "请先创建虚拟环境并安装依赖:"
+  echo "  python3 -m venv venv"
+  echo "  source venv/bin/activate"
+  echo "  pip install -r requirements.txt"
+  exit 1
+fi
+
+echo "[INFO] 启动本地开发服务"
+echo "[INFO] ENV_FILE=$ENV_FILE"
+echo "[INFO] URL=http://$HOST:$PORT"
+
+ENV_FILE="$ENV_FILE" "$UVICORN_BIN" "$APP_MODULE" --host "$HOST" --port "$PORT" --reload --log-level debug

+ 9 - 1
backend/start-prod.sh

@@ -15,6 +15,7 @@ HOST="${HOST:-0.0.0.0}"
 PORT="${PORT:-8000}"
 WORKERS="${WORKERS:-2}"
 LOG_LEVEL="${LOG_LEVEL:-info}"
+ENV_FILE="${ENV_FILE:-$BASE_DIR/.env.production}"
 
 VENV_DIR="${VENV_DIR:-$BASE_DIR/venv}"
 PYTHON_BIN="${PYTHON_BIN:-$VENV_DIR/bin/python}"
@@ -60,6 +61,7 @@ show_help() {
   LOG_LEVEL=info
   APP_MODULE=app.main:app
   VENV_DIR=./venv
+  ENV_FILE=./.env.production
 EOF
 }
 
@@ -150,8 +152,14 @@ start_server() {
   log_info "监听地址: $HOST:$PORT"
   log_info "工作进程: $WORKERS"
   log_info "日志文件: $LOG_FILE"
+  log_info "环境文件: $ENV_FILE"
 
-  nohup "$UVICORN_BIN" "$APP_MODULE" \
+  if [ ! -f "$ENV_FILE" ]; then
+    log_error "环境文件不存在: $ENV_FILE"
+    exit 1
+  fi
+
+  ENV_FILE="$ENV_FILE" nohup "$UVICORN_BIN" "$APP_MODULE" \
     --host "$HOST" \
     --port "$PORT" \
     --workers "$WORKERS" \

+ 184 - 145
graphify-out/GRAPH_REPORT.md

@@ -1,12 +1,12 @@
 # Graph Report - sqxp_副本2  (2026-04-30)
 
 ## Corpus Check
-- 701 files · ~637,582 words
+- 701 files · ~640,190 words
 - Verdict: corpus is large enough that graph structure adds value.
 
 ## Summary
-- 4853 nodes · 23872 edges · 60 communities detected
-- Extraction: 22% EXTRACTED · 78% INFERRED · 0% AMBIGUOUS · INFERRED: 18723 edges (avg confidence: 0.55)
+- 4916 nodes · 25358 edges · 65 communities detected
+- Extraction: 20% EXTRACTED · 80% INFERRED · 0% AMBIGUOUS · INFERRED: 20196 edges (avg confidence: 0.55)
 - Token cost: 0 input · 0 output
 
 ## Community Hubs (Navigation)
@@ -30,58 +30,63 @@
 - [[_COMMUNITY_Community 17|Community 17]]
 - [[_COMMUNITY_Community 18|Community 18]]
 - [[_COMMUNITY_Community 19|Community 19]]
-- [[_COMMUNITY_Community 20|Community 20]]
-- [[_COMMUNITY_Community 21|Community 21]]
 - [[_COMMUNITY_Community 22|Community 22]]
+- [[_COMMUNITY_Community 23|Community 23]]
+- [[_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 34|Community 34]]
-- [[_COMMUNITY_Community 35|Community 35]]
 - [[_COMMUNITY_Community 36|Community 36]]
+- [[_COMMUNITY_Community 37|Community 37]]
 - [[_COMMUNITY_Community 38|Community 38]]
 - [[_COMMUNITY_Community 39|Community 39]]
 - [[_COMMUNITY_Community 40|Community 40]]
-- [[_COMMUNITY_Community 41|Community 41]]
-- [[_COMMUNITY_Community 42|Community 42]]
+- [[_COMMUNITY_Community 45|Community 45]]
 - [[_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 56|Community 56]]
-- [[_COMMUNITY_Community 57|Community 57]]
-- [[_COMMUNITY_Community 61|Community 61]]
-- [[_COMMUNITY_Community 64|Community 64]]
+- [[_COMMUNITY_Community 55|Community 55]]
+- [[_COMMUNITY_Community 59|Community 59]]
+- [[_COMMUNITY_Community 62|Community 62]]
+- [[_COMMUNITY_Community 63|Community 63]]
 - [[_COMMUNITY_Community 65|Community 65]]
-- [[_COMMUNITY_Community 67|Community 67]]
-- [[_COMMUNITY_Community 80|Community 80]]
+- [[_COMMUNITY_Community 78|Community 78]]
+- [[_COMMUNITY_Community 88|Community 88]]
 - [[_COMMUNITY_Community 90|Community 90]]
-- [[_COMMUNITY_Community 92|Community 92]]
+- [[_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]]
 - [[_COMMUNITY_Community 148|Community 148]]
-- [[_COMMUNITY_Community 149|Community 149]]
-- [[_COMMUNITY_Community 150|Community 150]]
+- [[_COMMUNITY_Community 416|Community 416]]
+- [[_COMMUNITY_Community 417|Community 417]]
+- [[_COMMUNITY_Community 418|Community 418]]
+- [[_COMMUNITY_Community 419|Community 419]]
+- [[_COMMUNITY_Community 420|Community 420]]
+- [[_COMMUNITY_Community 421|Community 421]]
+- [[_COMMUNITY_Community 422|Community 422]]
 
 ## God Nodes (most connected - your core abstractions)
-1. `BizPriceListItem` - 699 edges
-2. `BizInquiry` - 628 edges
-3. `BizQuotation` - 626 edges
-4. `BizAttachment` - 616 edges
-5. `BasProduct` - 615 edges
-6. `SysUser` - 606 edges
-7. `BasProductCategory` - 605 edges
-8. `BasProductUnit` - 605 edges
-9. `BizDeliveryTask` - 598 edges
-10. `BizOrder` - 595 edges
+1. `BizPriceListItem` - 753 edges
+2. `BizInquiry` - 682 edges
+3. `BizQuotation` - 680 edges
+4. `BizAttachment` - 670 edges
+5. `BasProduct` - 669 edges
+6. `SysUser` - 660 edges
+7. `BasProductCategory` - 659 edges
+8. `BasProductUnit` - 659 edges
+9. `BizDeliveryTask` - 652 edges
+10. `BizOrder` - 649 edges
 
 ## Surprising Connections (you probably didn't know these)
 - `get_active()` --calls--> `query()`  [INFERRED]
@@ -92,315 +97,349 @@
   web-admin/src/views/system/inquiry-quotation/index.vue → sqxp-wx/uni_modules/uni-easyinput/components/uni-easyinput/uni-easyinput.vue
 - `number()` --calls--> `mounted()`  [INFERRED]
   web-admin/src/views/system/inquiry-quotation/index.vue → sqxp-wx/uni_modules/uni-table/components/uni-tr/uni-tr.vue
-- `showMenu()` --calls--> `nextTick()`  [INFERRED]
-  web-admin/src/views/widgets/context-menu/index.vue → sqxp-wx/wxcomponents/vant/common/utils.js
+- `number()` --calls--> `onPickerChange()`  [INFERRED]
+  web-admin/src/views/system/inquiry-quotation/index.vue → sqxp-wx/pages/client/invoice/invoice.vue
 
 ## Communities
 
 ### Community 0 - "Community 0"
 Cohesion: 0.01
-Nodes (647): 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() (+639 more)
+Nodes (815): 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() (+807 more)
 
 ### Community 1 - "Community 1"
-Cohesion: 0.08
-Nodes (618): 获取管理员的区域过滤条件      对于区域管理员,返回其所属区域ID(用于过滤数据)     对于超级管理员(无区域绑定),返回None(不过滤), 更新用户菜单授权(按 code 全量覆盖;兼容明文/密文载荷)。, 更新用户菜单授权(按 code 全量覆盖;兼容明文/密文载荷)。, 与 PUT 同逻辑:前端按 POST 模式调用时复用。, 兼容旧逻辑:返回单区域过滤ID。多区域场景返回排序后首个区域。, 更新用户菜单授权(按 code 全量覆盖;兼容明文/密文载荷)。, 兼容旧逻辑:返回单区域过滤ID。多区域场景返回排序后首个区域。, 上传用户头像图片,返回可写入 avatarUrl 的地址 (+610 more)
+Cohesion: 0.07
+Nodes (665): 获取管理员的区域过滤条件      对于区域管理员,返回其所属区域ID(用于过滤数据)     对于超级管理员(无区域绑定),返回None(不过滤), 更新用户菜单授权(按 code 全量覆盖;兼容明文/密文载荷)。, 更新用户菜单授权(按 code 全量覆盖;兼容明文/密文载荷)。, 与 PUT 同逻辑:前端按 POST 模式调用时复用。, 兼容旧逻辑:返回单区域过滤ID。多区域场景返回排序后首个区域。, 更新用户菜单授权(按 code 全量覆盖;兼容明文/密文载荷)。, 兼容旧逻辑:返回单区域过滤ID。多区域场景返回排序后首个区域。, 兼容旧逻辑:返回单区域过滤ID。多区域场景返回排序后首个区域。 (+657 more)
 
 ### Community 2 - "Community 2"
 Cohesion: 0.01
-Nodes (206): fullRegion(), if(), if(), FireworkSystem, mapKeys(), VantComponent(), HTMLParser(), parseAttrs() (+198 more)
+Nodes (196): fullRegion(), if(), if(), FireworkSystem, mapKeys(), VantComponent(), HTMLParser(), parseAttrs() (+188 more)
 
 ### Community 3 - "Community 3"
 Cohesion: 0.01
-Nodes (186): handleSubmit(), loadDetail(), onRegionMultiConfirm(), regionLabel(), syncMultiFromForm(), validate(), fetchActiveRegions(), fetchAssignableMenuCodes() (+178 more)
+Nodes (157): calendar(), memo(), record(), handleSelect(), onMenuShow(), showMenu(), onClear(), onInput() (+149 more)
 
 ### Community 4 - "Community 4"
 Cohesion: 0.01
-Nodes (146): calendar(), memo(), record(), handleSelect(), onMenuShow(), showMenu(), onClear(), onInput() (+138 more)
+Nodes (247): InvoiceCreateRequest, MessageCreateRequest, 获取商品的评价列表(仅显示已审核通过的评价), AdjustResponse, Config, CreateAdjustRequest, 兼容明文/密文请求,返回业务 payload。失败时返回 error_response。, BaseModel (+239 more)
 
 ### Community 5 - "Community 5"
 Cohesion: 0.01
-Nodes (249): InvoiceCreateRequest, MessageCreateRequest, 获取商品的评价列表(仅显示已审核通过的评价), AdjustResponse, Config, CreateAdjustRequest, 兼容明文/密文请求,返回业务 payload。失败时返回 error_response。, BaseModel (+241 more)
+Nodes (133): fetchActiveRegions(), fetchAssignableMenuCodes(), fetchCategoryList(), fetchCategoryParameters(), fetchCategoryTree(), fetchCreateUnit(), fetchNotDeletedRegions(), fetchProductBannerList() (+125 more)
 
 ### Community 6 - "Community 6"
-Cohesion: 0.02
-Nodes (172): # TODO: 发送微信模板消息提醒配送员, # TODO: 发送微信模板消息提醒配送员, 记录处理过程(不结案,结案由配送完成/取消自动触发), 获取我抢到的任务列表      返回当前配送员已抢但尚未开始配送的任务, 获取我抢到的任务列表      返回当前配送员已抢但尚未开始配送的任务, 取消抢单      配送员可以取消自己抢到但尚未开始配送的任务, 取消抢单      配送员可以取消自己抢到但尚未开始配送的任务, 取消抢单      配送员可以取消自己抢到但尚未开始配送的任务 (+164 more)
+Cohesion: 0.01
+Nodes (130): handleSubmit(), loadDetail(), onRegionMultiConfirm(), regionLabel(), syncMultiFromForm(), validate(), disableDevTools(), handleLock() (+122 more)
 
 ### Community 7 - "Community 7"
 Cohesion: 0.02
-Nodes (69): fetchGetMenuCodes(), fetchGetUserInfo(), fetchLogin(), fetchGetMenuList(), fetchGetRoleList(), fetchGetUserList(), reload(), addUnit() (+61 more)
+Nodes (64): fetchGetMenuCodes(), fetchGetUserInfo(), fetchLogin(), fetchGetMenuList(), fetchGetRoleList(), fetchGetUserList(), reload(), addUnit() (+56 more)
 
 ### Community 8 - "Community 8"
-Cohesion: 0.03
-Nodes (92): 小程序客户端扩展接口:购物车、商品分类、仪表盘、发票、下单、支付, 从叶子分类 id 向上解析(含缓存):返回 (根分类, 叶子分类)。, 可申请开票的已完成订单(简化:已完成且尚无发票), 可申请开票的已完成订单(简化:已完成且尚无发票), 可申请开票的已完成订单(简化:已完成且尚无发票), 商品对应的一级(根)分类,供购物车行展示与序列化。, 从叶子分类 id 向上解析(含缓存):返回 (根分类, 叶子分类)。, 购物车:先一级分类 sort_order,再商品名、商品 id(稳定)。 (+84 more)
+Cohesion: 0.02
+Nodes (64): askAbortDownload(), checkLocalStoragePackage(), closeUpdate(), compare(), deleteSavedFile(), downLoadComplete(), installPackage(), onLoad() (+56 more)
 
 ### Community 9 - "Community 9"
 Cohesion: 0.03
-Nodes (52): handleFormClose(), _Blur(), created(), init(), inputContentStyle(), inputMaxlength(), inputStyle(), obj2strStyle() (+44 more)
+Nodes (92): 小程序客户端扩展接口:购物车、商品分类、仪表盘、发票、下单、支付, 从叶子分类 id 向上解析(含缓存):返回 (根分类, 叶子分类)。, 可申请开票的已完成订单(简化:已完成且尚无发票), 可申请开票的已完成订单(简化:已完成且尚无发票), 可申请开票的已完成订单(简化:已完成且尚无发票), 商品对应的一级(根)分类,供购物车行展示与序列化。, 从叶子分类 id 向上解析(含缓存):返回 (根分类, 叶子分类)。, 购物车:先一级分类 sort_order,再商品名、商品 id(稳定)。 (+84 more)
 
 ### Community 10 - "Community 10"
 Cohesion: 0.03
-Nodes (17): toLogin(), if(), confirmLogout(), getDefaultLanguage(), emitUrls(), removeItem(), revokeLocalPreview(), checkStorageCompatibility() (+9 more)
+Nodes (52): handleFormClose(), _Blur(), created(), init(), inputContentStyle(), inputMaxlength(), inputStyle(), obj2strStyle() (+44 more)
 
 ### Community 11 - "Community 11"
-Cohesion: 0.04
-Nodes (40): BaseHTTPMiddleware, ApiCryptoError, _b64url_decode(), _b64url_encode(), encrypt_json(), gen_nonce_hex(), is_ts_in_window(), ApiCryptoMiddleware (+32 more)
-
-### Community 12 - "Community 12"
 Cohesion: 0.05
 Nodes (77): 批量获取供应商取货地址,避免列表接口逐条查询。, 一级/二级分类用 / 连接(与订单详情一致)。, 价格清单行 price 为面向客户的最终单价;pricing_coefficient 为调价系数(%),供货单价 = price / (1 + 系数/100)。, (price_list_id, product_id, unit_id) -> BizPriceListItem, 供应商默认取货地址;无默认时取第一条 pickup 类型地址。, 获取供应商配送任务详情(订单仅返回客户订单号;商品行含主图与规格;单价由价格清单行 price(客户最终单价)与 pricing_coefficient 反推供, 获取供应商配送任务详情(订单仅返回客户订单号;商品行含主图与规格;单价由价格清单行 price(客户最终单价)与 pricing_coefficient 反推供, 获取供应商配送任务详情(订单仅返回客户订单号;商品行含主图与规格;单价由价格清单行 price(客户最终单价)与 pricing_coefficient 反推供 (+69 more)
 
-### Community 13 - "Community 13"
-Cohesion: 0.03
-Nodes (43): askAbortDownload(), checkLocalStoragePackage(), closeUpdate(), compare(), deleteSavedFile(), downLoadComplete(), installPackage(), onLoad() (+35 more)
-
-### Community 14 - "Community 14"
+### Community 12 - "Community 12"
 Cohesion: 0.05
-Nodes (22): change(), created(), getDataList(), getSelectedValue(), handler(), loadData(), mixinDatacomResData(), modelValue() (+14 more)
-
-### Community 15 - "Community 15"
-Cohesion: 0.11
-Nodes (12): activated(), bottom(), center(), close(), deactivated(), destroyed(), left(), onTap() (+4 more)
+Nodes (15): fetchGetLatestUpgradeLog(), if(), getDefaultLanguage(), emitUrls(), removeItem(), revokeLocalPreview(), checkStorageCompatibility(), StorageConfig (+7 more)
 
-### Community 16 - "Community 16"
+### Community 13 - "Community 13"
 Cohesion: 0.18
 Nodes (21): destroyed(), getDirection(), getDom(), isPC(), mousedown(), mouseleave(), mousemove(), mouseup() (+13 more)
 
-### Community 17 - "Community 17"
+### Community 14 - "Community 14"
 Cohesion: 0.13
 Nodes (13): change(), clearVal(), created(), debounce(), emit(), formatItemName(), getCache(), handler() (+5 more)
 
-### Community 18 - "Community 18"
+### Community 15 - "Community 15"
 Cohesion: 0.12
 Nodes (15): addCopyButton(), highlightCode(), insertLineNumbers(), isBlockProcessed(), markBlockAsProcessed(), processBlock(), setupHighlightDirective(), setupRippleDirective() (+7 more)
 
-### Community 19 - "Community 19"
-Cohesion: 0.18
-Nodes (16): defaultAvatarPathByRole(), normalizeAvatarUrlForMini(), resolveUserAvatarUrl(), appendQueryParam(), fallbackVersionFromManifest(), getStaticAssetsVersion(), hasSignedQuery(), hasVersionParam() (+8 more)
+### Community 16 - "Community 16"
+Cohesion: 0.17
+Nodes (12): BusinessException, InvalidStatusTransitionException, log_delivery_operation(), log_operation(), log_order_operation(), log_settlement_operation(), log_user_operation(), OrderLockedException (+4 more)
 
-### Community 20 - "Community 20"
+### Community 17 - "Community 17"
 Cohesion: 0.16
 Nodes (10): useTable(), useTableImpl(), getColumnChecks(), useTableColumns(), createErrorHandler(), createSmartDebounce(), defaultResponseAdapter(), extractPagination() (+2 more)
 
-### Community 21 - "Community 21"
-Cohesion: 0.27
-Nodes (14): useChart(), useChartComponent(), useChartOps(), colourBlend(), getCssVar(), getDarkColor(), getLightColor(), handleElementThemeColor() (+6 more)
+### Community 18 - "Community 18"
+Cohesion: 0.2
+Nodes (14): defaultAvatarPathByRole(), normalizeAvatarUrlForMini(), resolveUserAvatarUrl(), appendQueryParam(), fallbackVersionFromManifest(), getStaticAssetsVersion(), hasSignedQuery(), hasVersionParam() (+6 more)
 
-### Community 22 - "Community 22"
+### Community 19 - "Community 19"
 Cohesion: 0.28
 Nodes (14): getDirection(), getDisabledType(), getDom(), move(), moveDirection(), openState(), range(), resetTouchStatus() (+6 more)
 
-### Community 25 - "Community 25"
+### Community 22 - "Community 22"
 Cohesion: 0.17
 Nodes (2): clean(), onInput()
 
-### Community 26 - "Community 26"
+### Community 23 - "Community 23"
 Cohesion: 0.33
 Nodes (11): changeFlag(), countDown(), day(), handler(), hour(), minute(), second(), startData() (+3 more)
 
-### Community 27 - "Community 27"
+### Community 24 - "Community 24"
 Cohesion: 0.24
 Nodes (4): getVal(), leftIconWidth(), navbarHeight(), rightIconWidth()
 
-### Community 28 - "Community 28"
+### Community 25 - "Community 25"
 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 29 - "Community 29"
+### Community 26 - "Community 26"
 Cohesion: 0.24
 Nodes (3): _calcValue(), _getDecimalScale(), _onBlur()
 
-### Community 31 - "Community 31"
+### Community 28 - "Community 28"
 Cohesion: 0.27
 Nodes (4): dataValue(), emit(), onChange(), setOpen()
 
-### Community 33 - "Community 33"
+### Community 30 - "Community 30"
 Cohesion: 0.25
 Nodes (2): borderStyle(), value2px()
 
-### Community 34 - "Community 34"
+### Community 31 - "Community 31"
+Cohesion: 0.29
+Nodes (3): BaseModel, get_active(), SoftDeleteModel
+
+### Community 32 - "Community 32"
 Cohesion: 0.39
 Nodes (6): isIgnorableScriptError(), normalizeErrorMessage(), registerPromiseErrorHandler(), registerResourceErrorHandler(), scriptErrorHandler(), setupErrorHandle()
 
-### Community 35 - "Community 35"
+### Community 33 - "Community 33"
 Cohesion: 0.29
 Nodes (2): handleTouchEnd(), setTransition()
 
-### Community 36 - "Community 36"
+### Community 34 - "Community 34"
 Cohesion: 0.36
 Nodes (1): TableHeightCalculator
 
-### Community 38 - "Community 38"
+### Community 36 - "Community 36"
 Cohesion: 0.36
 Nodes (1): RouteValidator
 
-### Community 39 - "Community 39"
+### Community 37 - "Community 37"
 Cohesion: 0.39
 Nodes (5): chooseAll(), chooseAndUploadFile(), chooseImage(), chooseVideo(), uploadFiles()
 
-### Community 40 - "Community 40"
+### Community 38 - "Community 38"
 Cohesion: 0.33
 Nodes (2): logApiCryptoBootInfo(), maskSecret()
 
-### Community 41 - "Community 41"
+### Community 39 - "Community 39"
 Cohesion: 0.38
 Nodes (4): fetchApiRequestLogList(), handleReset(), handleSearch(), loadData()
 
-### Community 42 - "Community 42"
+### Community 40 - "Community 40"
 Cohesion: 0.38
 Nodes (3): _change(), close(), open()
 
-### Community 47 - "Community 47"
+### Community 45 - "Community 45"
 Cohesion: 0.33
 Nodes (1): BridgeError
 
-### Community 49 - "Community 49"
+### Community 47 - "Community 47"
 Cohesion: 0.4
 Nodes (2): created(), getTable()
 
-### Community 50 - "Community 50"
+### Community 48 - "Community 48"
 Cohesion: 0.47
 Nodes (3): get_file_data(), get_file_ext(), get_file_info()
 
-### Community 51 - "Community 51"
+### Community 49 - "Community 49"
 Cohesion: 0.4
 Nodes (2): created(), getForm()
 
-### Community 52 - "Community 52"
+### Community 50 - "Community 50"
 Cohesion: 0.47
 Nodes (4): onDial(), onTextClick(), dialPhone(), normalizeDialNumber()
 
-### Community 53 - "Community 53"
+### Community 51 - "Community 51"
 Cohesion: 0.4
 Nodes (4): 运营端角色守卫:admin / super_admin 可访问。, 接口菜单权限守卫。super_admin 直接放行。, require_admin_or_super_admin(), require_menu_code()
 
-### Community 54 - "Community 54"
+### Community 52 - "Community 52"
 Cohesion: 0.5
 Nodes (2): focusInput(), handleKeydown()
 
-### Community 56 - "Community 56"
+### Community 54 - "Community 54"
 Cohesion: 0.5
 Nodes (3): BaseSettings, Config, Settings
 
-### Community 57 - "Community 57"
+### Community 55 - "Community 55"
 Cohesion: 0.5
 Nodes (1): collectIds()
 
-### Community 61 - "Community 61"
+### Community 59 - "Community 59"
 Cohesion: 0.83
 Nodes (3): classes(), isTrue(), onClick()
 
-### Community 64 - "Community 64"
+### Community 62 - "Community 62"
 Cohesion: 0.67
 Nodes (2): created(), getParent()
 
-### Community 65 - "Community 65"
+### Community 63 - "Community 63"
 Cohesion: 1.0
 Nodes (2): getSettingDefaults(), resetToDefaults()
 
-### Community 67 - "Community 67"
+### Community 65 - "Community 65"
 Cohesion: 1.0
 Nodes (2): defaultAvatarPathByRole(), resolveUserAvatarUrl()
 
-### Community 80 - "Community 80"
+### Community 78 - "Community 78"
 Cohesion: 0.67
 Nodes (1): updateUseModal()
 
-### Community 90 - "Community 90"
+### Community 88 - "Community 88"
 Cohesion: 1.0
 Nodes (2): buildMiniEncryptedLoginBody(), fetchLoginPublicKey()
 
-### Community 92 - "Community 92"
+### Community 90 - "Community 90"
 Cohesion: 0.67
 Nodes (1): return()
 
-### Community 145 - "Community 145"
+### Community 143 - "Community 143"
 Cohesion: 1.0
 Nodes (1): 发送订单状态通知                  Args:             openid: 用户openid             ord
 
-### Community 146 - "Community 146"
+### Community 144 - "Community 144"
 Cohesion: 1.0
 Nodes (1): 发送支付成功通知                  Args:             openid: 用户openid             ord
 
-### Community 147 - "Community 147"
+### Community 145 - "Community 145"
 Cohesion: 1.0
 Nodes (1): 发送配送通知                  Args:             openid: 配送员openid             task
 
-### Community 148 - "Community 148"
+### Community 146 - "Community 146"
 Cohesion: 1.0
 Nodes (1): 发送配送时效预警                  Args:             openid: 配送员openid             ta
 
-### Community 149 - "Community 149"
+### Community 147 - "Community 147"
 Cohesion: 1.0
 Nodes (1): 发送退款结果通知                  Args:             openid: 用户openid             ord
 
-### Community 150 - "Community 150"
+### Community 148 - "Community 148"
 Cohesion: 1.0
 Nodes (1): 发送催单通知给配送员                  Args:             deliver_openid: 配送员openid
 
+### Community 416 - "Community 416"
+Cohesion: 1.0
+Nodes (1): 按 region_id 管理 websocket 连接并广播。
+
+### Community 417 - "Community 417"
+Cohesion: 1.0
+Nodes (1): 鉴权并接入房间。返回 current_user dict。
+
+### Community 418 - "Community 418"
+Cohesion: 1.0
+Nodes (1): 向 region 房间广播消息。返回尝试发送的连接数。
+
+### Community 419 - "Community 419"
+Cohesion: 1.0
+Nodes (1): 小程序端 WebSocket 入口:/ws/mini
+
+### Community 420 - "Community 420"
+Cohesion: 1.0
+Nodes (1): 在 FastAPI app 上注册 websocket 路由。
+
+### Community 421 - "Community 421"
+Cohesion: 1.0
+Nodes (1): Base64 密文 -> JSON 对象(须含 username、password、ts 毫秒时间戳)。
+
+### Community 422 - "Community 422"
+Cohesion: 1.0
+Nodes (1): 从请求体解析用户名、密码。     返回 (username, password, error_message);error_message 非空时表示失败。
+
 ## Knowledge Gaps
-- **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)
+- **81 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 非空时表示失败。` (+76 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 22`** (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 30`** (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 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 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 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 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 45`** (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 47`** (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 33`** (9 nodes): `upload-file.vue`, `borderLineStyle()`, `borderStyle()`, `choose()`, `default()`, `delFile()`, `styles()`, `uploadFiles()`, `value2px()`
+- **Thin community `Community 49`** (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 35`** (8 nodes): `cleanupEventListeners()`, `handleTouchEnd()`, `handleTouchMove()`, `handleTouchStart()`, `handleWheelScroll()`, `setTransition()`, `setupEventListeners()`, `index.vue`
+- **Thin community `Community 52`** (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 36`** (8 nodes): `TableHeightCalculator`, `.calculate()`, `.calculateOffset()`, `.calculatePaginationOffset()`, `.constructor()`, `.getHeaderHeight()`, `useTableHeight()`, `useTableHeight.ts`
+- **Thin community `Community 55`** (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 38`** (8 nodes): `RouteValidator`, `.checkComponents()`, `.checkDuplicates()`, `.checkNestedIndexComponent()`, `.logLayoutError()`, `.resolvePath()`, `.validate()`, `RouteValidator.ts`
+- **Thin community `Community 62`** (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 40`** (7 nodes): `refreshButtonAutoIcons()`, `refreshPanelTitleIcons()`, `logApiCryptoBootInfo()`, `maskSecret()`, `App.vue`, `main.ts`, `console.ts`
+- **Thin community `Community 63`** (3 nodes): `getSettingDefaults()`, `resetToDefaults()`, `setting.ts`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 47`** (6 nodes): `bridge-error.js`, `BridgeError`, `.code()`, `.constructor()`, `.errCode()`, `.errMsg()`
+- **Thin community `Community 65`** (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 49`** (6 nodes): `uni-thead.vue`, `checkboxSelected()`, `created()`, `data()`, `getTable()`, `init()`
+- **Thin community `Community 78`** (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 51`** (6 nodes): `uni-group.vue`, `created()`, `data()`, `getForm()`, `onClick()`, `title()`
+- **Thin community `Community 88`** (3 nodes): `loginRsa.js`, `buildMiniEncryptedLoginBody()`, `fetchLoginPublicKey()`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 54`** (5 nodes): `focusInput()`, `handleKeydown()`, `if()`, `search()`, `index.vue`
+- **Thin community `Community 90`** (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 57`** (4 nodes): `catch()`, `collectIds()`, `handleCategoryTableSelectionChange()`, `index.vue`
+- **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 64`** (4 nodes): `popup.js`, `created()`, `data()`, `getParent()`
+- **Thin community `Community 144`** (1 nodes): `发送支付成功通知                  Args:             openid: 用户openid             ord`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 65`** (3 nodes): `getSettingDefaults()`, `resetToDefaults()`, `setting.ts`
+- **Thin community `Community 145`** (1 nodes): `发送配送通知                  Args:             openid: 配送员openid             task`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 67`** (3 nodes): `defaultAvatarPathByRole()`, `resolveUserAvatarUrl()`, `user-avatar.ts`
+- **Thin community `Community 146`** (1 nodes): `发送配送时效预警                  Args:             openid: 配送员openid             ta`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 80`** (3 nodes): `check-update.js`, `check-update.ts`, `updateUseModal()`
+- **Thin community `Community 147`** (1 nodes): `发送退款结果通知                  Args:             openid: 用户openid             ord`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 90`** (3 nodes): `loginRsa.js`, `buildMiniEncryptedLoginBody()`, `fetchLoginPublicKey()`
+- **Thin community `Community 148`** (1 nodes): `发送催单通知给配送员                  Args:             deliver_openid: 配送员openid`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 92`** (3 nodes): `return()`, `detail.vue`, `detail.vue`
+- **Thin community `Community 416`** (1 nodes): `按 region_id 管理 websocket 连接并广播。`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 145`** (1 nodes): `发送订单状态通知                  Args:             openid: 用户openid             ord`
+- **Thin community `Community 417`** (1 nodes): `鉴权并接入房间。返回 current_user dict。`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 146`** (1 nodes): `发送支付成功通知                  Args:             openid: 用户openid             ord`
+- **Thin community `Community 418`** (1 nodes): `向 region 房间广播消息。返回尝试发送的连接数。`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 147`** (1 nodes): `发送配送通知                  Args:             openid: 配送员openid             task`
+- **Thin community `Community 419`** (1 nodes): `小程序端 WebSocket 入口:/ws/mini`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 148`** (1 nodes): `发送配送时效预警                  Args:             openid: 配送员openid             ta`
+- **Thin community `Community 420`** (1 nodes): `在 FastAPI app 上注册 websocket 路由。`
   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 421`** (1 nodes): `Base64 密文 -> JSON 对象(须含 username、password、ts 毫秒时间戳)。`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 150`** (1 nodes): `发送催单通知给配送员                  Args:             deliver_openid: 配送员openid`
+- **Thin community `Community 422`** (1 nodes): `从请求体解析用户名、密码。     返回 (username, password, error_message);error_message 非空时表示失败。`
   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 10`, `Community 13`, `Community 14`?**
-  _High betweenness centrality (0.133) - this node is a cross-community bridge._
-- **Why does `query()` connect `Community 0` to `Community 17`, `Community 6`?**
+- **Why does `push()` connect `Community 2` to `Community 0`, `Community 3`, `Community 5`, `Community 8`, `Community 10`, `Community 12`?**
+  _High betweenness centrality (0.135) - this node is a cross-community bridge._
+- **Why does `query()` connect `Community 0` to `Community 14`, `Community 31`?**
   _High betweenness centrality (0.049) - this node is a cross-community bridge._
-- **Why does `number()` connect `Community 3` to `Community 0`, `Community 2`, `Community 9`, `Community 13`, `Community 14`, `Community 21`?**
-  _High betweenness centrality (0.044) - this node is a cross-community bridge._
-- **Are the 697 inferred relationships involving `BizPriceListItem` (e.g. with `PriceListCRUD` and `PriceListItemCRUD`) actually correct?**
-  _`BizPriceListItem` has 697 INFERRED edges - model-reasoned connections that need verification._
-- **Are the 626 inferred relationships involving `BizInquiry` (e.g. with `InquiryCRUD` and `InquiryItemCRUD`) actually correct?**
-  _`BizInquiry` has 626 INFERRED edges - model-reasoned connections that need verification._
-- **Are the 624 inferred relationships involving `BizQuotation` (e.g. with `QuotationCRUD` and `QuotationItemCRUD`) actually correct?**
-  _`BizQuotation` has 624 INFERRED edges - model-reasoned connections that need verification._
-- **Are the 614 inferred relationships involving `BizAttachment` (e.g. with `AttachmentCRUD` and `价格清单行 price 为面向客户的最终单价;pricing_coefficient 为调价系数(%),供货单价 = price / (1 + 系数/100)。`) actually correct?**
-  _`BizAttachment` has 614 INFERRED edges - model-reasoned connections that need verification._
+- **Why does `number()` connect `Community 5` to `Community 0`, `Community 2`, `Community 6`, `Community 8`, `Community 10`?**
+  _High betweenness centrality (0.041) - this node is a cross-community bridge._
+- **Are the 751 inferred relationships involving `BizPriceListItem` (e.g. with `PriceListCRUD` and `PriceListItemCRUD`) actually correct?**
+  _`BizPriceListItem` has 751 INFERRED edges - model-reasoned connections that need verification._
+- **Are the 680 inferred relationships involving `BizInquiry` (e.g. with `InquiryCRUD` and `InquiryItemCRUD`) actually correct?**
+  _`BizInquiry` has 680 INFERRED edges - model-reasoned connections that need verification._
+- **Are the 678 inferred relationships involving `BizQuotation` (e.g. with `QuotationCRUD` and `QuotationItemCRUD`) actually correct?**
+  _`BizQuotation` has 678 INFERRED edges - model-reasoned connections that need verification._
+- **Are the 668 inferred relationships involving `BizAttachment` (e.g. with `AttachmentCRUD` and `价格清单行 price 为面向客户的最终单价;pricing_coefficient 为调价系数(%),供货单价 = price / (1 + 系数/100)。`) actually correct?**
+  _`BizAttachment` has 668 INFERRED edges - model-reasoned connections that need verification._

Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
graphify-out/cache/ast/015281feb18fc5d0b27ce36f2aa099ed464a4dad7302b3013e53d27c23c9c352.json


+ 1 - 0
graphify-out/cache/ast/046c18b54ddde3cc1c6d660d9e84327c083bc2d5d3ee1bd1060ab3d4e3dee7fc.json

@@ -0,0 +1 @@
+{"nodes": [{"id": "users_wuzilong_work_00code_sqxp_2_sqxp_wx_pages_supplier_quotation_quotation_vue", "label": "quotation.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/sqxp-wx/pages/supplier/quotation/quotation.vue", "source_location": "L1"}, {"id": "quotation_quotation_if", "label": "if()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/sqxp-wx/pages/supplier/quotation/quotation.vue", "source_location": "L102"}], "edges": [{"source": "users_wuzilong_work_00code_sqxp_2_sqxp_wx_pages_supplier_quotation_quotation_vue", "target": "quotation_quotation_if", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/sqxp-wx/pages/supplier/quotation/quotation.vue", "source_location": "L102", "weight": 1.0}], "raw_calls": [{"caller_nid": "quotation_quotation_if", "callee": "push", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/sqxp-wx/pages/supplier/quotation/quotation.vue", "source_location": "L103"}, {"caller_nid": "quotation_quotation_if", "callee": "add", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/sqxp-wx/pages/supplier/quotation/quotation.vue", "source_location": "L104"}]}

Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
graphify-out/cache/ast/0d8aeab6acb06ce9f869984ca58c7545ed84a63870b787fbd0decbf64b365b9b.json


Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
graphify-out/cache/ast/16f862e2be2297f175164a33caf0d307e46c9311186dd936c3bd9e25e59d7600.json


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

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

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

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

Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
graphify-out/cache/ast/1fb799468885857a4425518f41a1c5e26527a4c08b03fcd1a4adc65eff9d69c6.json


Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
graphify-out/cache/ast/2039fcc3acb2b2f921dd492b1fadca598cb3f9e920647bad4f52dc040045e6d4.json


+ 1 - 0
graphify-out/cache/ast/253b9a44e45ce4e3033080bca5cb4b7ae933798384942c7293a53f74af1a11f9.json

@@ -0,0 +1 @@
+{"nodes": [{"id": "users_wuzilong_work_00code_sqxp_2_sqxp_wx_config_static_assets_defaults_js", "label": "static-assets-defaults.js", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/sqxp-wx/config/static-assets-defaults.js", "source_location": "L1"}], "edges": [], "raw_calls": []}

+ 1 - 0
graphify-out/cache/ast/2b78e8a54234a92d2ef23b3444c7416c72e5dff091ff80f813fe2cc2f2da38b4.json

@@ -0,0 +1 @@
+{"nodes": [{"id": "users_wuzilong_work_00code_sqxp_2_sqxp_wx_config_static_assets_defaults_js", "label": "static-assets-defaults.js", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/sqxp-wx/config/static-assets-defaults.js", "source_location": "L1"}], "edges": [], "raw_calls": []}

+ 1 - 0
graphify-out/cache/ast/31654249c44ae8974b7358630fe2a2c064a0a1874ce2ea4c2a81e56aa8585f62.json

@@ -0,0 +1 @@
+{"nodes": [{"id": "users_wuzilong_work_00code_sqxp_2_sqxp_wx_pages_supplier_delivery_delivery_vue", "label": "delivery.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/sqxp-wx/pages/supplier/delivery/delivery.vue", "source_location": "L1"}, {"id": "delivery_delivery_if", "label": "if()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/sqxp-wx/pages/supplier/delivery/delivery.vue", "source_location": "L196"}], "edges": [{"source": "users_wuzilong_work_00code_sqxp_2_sqxp_wx_pages_supplier_delivery_delivery_vue", "target": "delivery_delivery_if", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/sqxp-wx/pages/supplier/delivery/delivery.vue", "source_location": "L196", "weight": 1.0}], "raw_calls": [{"caller_nid": "delivery_delivery_if", "callee": "push", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/sqxp-wx/pages/supplier/delivery/delivery.vue", "source_location": "L197"}, {"caller_nid": "delivery_delivery_if", "callee": "add", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/sqxp-wx/pages/supplier/delivery/delivery.vue", "source_location": "L198"}]}

+ 1 - 0
graphify-out/cache/ast/364fc1c4cd7772793642451622dee215d8ee7383fbbd3ccbe71cb59e2a655e0e.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": []}

+ 1 - 0
graphify-out/cache/ast/464ade254197992ff588b2d82a6d8192f14c1e9f1b1d151b2844525ab1225325.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": "L124"}], "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": "L124", "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": "L204", "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": "L125"}, {"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": "L207"}]}

+ 1 - 0
graphify-out/cache/ast/493b5b6154ccdd5a2482cb58e2d19946503059e4403e4886fcfe75c42034d80d.json

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

+ 1 - 0
graphify-out/cache/ast/504fdd6b5b11c3e14dc47e9a02be9c8f108aad5801cb4599bfddf8d6595319d2.json

@@ -0,0 +1 @@
+{"nodes": [{"id": "users_wuzilong_work_00code_sqxp_2_sqxp_wx_config_static_assets_defaults_js", "label": "static-assets-defaults.js", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/sqxp-wx/config/static-assets-defaults.js", "source_location": "L1"}], "edges": [], "raw_calls": []}

+ 1 - 0
graphify-out/cache/ast/55deb4e00b2ca134bff000f33164a014384cafcd7501d88a40805b74dfa54ce4.json

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

Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
graphify-out/cache/ast/5f8edef24a1d596ffb3dc6de6660cf4f762f254cc1cd3cd82f51910aff81b5e1.json


+ 1 - 0
graphify-out/cache/ast/6419bc8200cb291635cf79ae3db068f7a6f8f73a76c247a7e63fd2e6e7207767.json

@@ -0,0 +1 @@
+{"nodes": [{"id": "users_wuzilong_work_00code_sqxp_2_sqxp_wx_pages_client_cart_cart_vue", "label": "cart.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/sqxp-wx/pages/client/cart/cart.vue", "source_location": "L1"}, {"id": "cart_cart_if", "label": "if()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/sqxp-wx/pages/client/cart/cart.vue", "source_location": "L197"}], "edges": [{"source": "users_wuzilong_work_00code_sqxp_2_sqxp_wx_pages_client_cart_cart_vue", "target": "cart_cart_if", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/sqxp-wx/pages/client/cart/cart.vue", "source_location": "L197", "weight": 1.0}], "raw_calls": []}

Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
graphify-out/cache/ast/68116a0da638286dc50bf49b087aaf8fe199c2e9136096607269b0cba4694568.json


+ 1 - 0
graphify-out/cache/ast/69ba673707506aa7c709d1529ebd926ae127400849353c009cf4c3507e952e04.json

@@ -0,0 +1 @@
+{"nodes": [{"id": "users_wuzilong_work_00code_sqxp_2_sqxp_wx_pages_client_order_pay_pay_vue", "label": "pay.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/sqxp-wx/pages/client/order/pay/pay.vue", "source_location": "L1"}, {"id": "pay_pay_onready", "label": "onReady()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/sqxp-wx/pages/client/order/pay/pay.vue", "source_location": "L165"}], "edges": [{"source": "users_wuzilong_work_00code_sqxp_2_sqxp_wx_pages_client_order_pay_pay_vue", "target": "pay_pay_onready", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/sqxp-wx/pages/client/order/pay/pay.vue", "source_location": "L165", "weight": 1.0}], "raw_calls": [{"caller_nid": "pay_pay_onready", "callee": "resolve", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/sqxp-wx/pages/client/order/pay/pay.vue", "source_location": "L165"}]}

+ 1 - 0
graphify-out/cache/ast/6f81bae306c3cdfe0e20a2fbabc398486613dd3c13ff7cf315cc8712ae342e79.json

@@ -0,0 +1 @@
+{"nodes": [{"id": "users_wuzilong_work_00code_sqxp_2_sqxp_wx_config_api_js", "label": "api.js", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/sqxp-wx/config/api.js", "source_location": "L1"}], "edges": [], "raw_calls": []}

Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
graphify-out/cache/ast/77e3d8f368c6a491eb9ae825fee0bf5d774c746c14eae238a071efeb3d499b11.json


Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
graphify-out/cache/ast/845c2391fa01b9246b13bf4b39916355a6ffe85fff61fd4f80107e8236be7b6c.json


Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
graphify-out/cache/ast/87ef4b6f2f48306cf294a2e1e07ee19e1869d00435f6cac882b716833a04aa51.json


Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
graphify-out/cache/ast/88f207dcbb0cd5fc06e794886cb5c7f720f039ce550678c12355eaa8baee4d8a.json


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

@@ -0,0 +1 @@
+{"nodes": [{"id": "users_wuzilong_work_00code_sqxp_2_sqxp_wx_config_api_js", "label": "api.js", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/sqxp-wx/config/api.js", "source_location": "L1"}], "edges": [], "raw_calls": []}

+ 1 - 0
graphify-out/cache/ast/97085145a08163397c613210538cbc8d597d293817ab09d90879ac73cd5223fe.json

@@ -0,0 +1 @@
+{"nodes": [{"id": "users_wuzilong_work_00code_sqxp_2_sqxp_wx_app_vue", "label": "App.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/sqxp-wx/App.vue", "source_location": "L1"}], "edges": [{"source": "users_wuzilong_work_00code_sqxp_2_sqxp_wx_app_vue", "target": "index_scss", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/sqxp-wx/App.vue", "source_location": "L67", "weight": 1.0}, {"source": "users_wuzilong_work_00code_sqxp_2_sqxp_wx_app_vue", "target": "users_wuzilong_work_00code_sqxp_2_sqxp_wx_common_uni_css", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/sqxp-wx/App.vue", "source_location": "L70", "weight": 1.0}], "raw_calls": []}

Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
graphify-out/cache/ast/a9d6badf4cc74a06b02cae34e79525fdf09a735d96e82ebce0edb608e5f26120.json


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

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

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

@@ -0,0 +1 @@
+{"nodes": [{"id": "users_wuzilong_work_00code_sqxp_2_sqxp_wx_config_static_assets_defaults_js", "label": "static-assets-defaults.js", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/sqxp-wx/config/static-assets-defaults.js", "source_location": "L1"}], "edges": [], "raw_calls": []}

Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
graphify-out/cache/ast/c0d3397ba83a03e2a7c84ec68b7b36f79439658df3d403c34d8cd7bd68102997.json


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

@@ -0,0 +1 @@
+{"nodes": [{"id": "users_wuzilong_work_00code_sqxp_2_sqxp_wx_config_api_js", "label": "api.js", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/sqxp-wx/config/api.js", "source_location": "L1"}], "edges": [], "raw_calls": []}

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

@@ -0,0 +1 @@
+{"nodes": [{"id": "users_wuzilong_work_00code_sqxp_2_sqxp_wx_config_api_js", "label": "api.js", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/sqxp-wx/config/api.js", "source_location": "L1"}, {"id": "config_api_resolvenodeenv", "label": "resolveNodeEnv()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/sqxp-wx/config/api.js", "source_location": "L10"}], "edges": [{"source": "users_wuzilong_work_00code_sqxp_2_sqxp_wx_config_api_js", "target": "config_api_resolvenodeenv", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/sqxp-wx/config/api.js", "source_location": "L10", "weight": 1.0}], "raw_calls": [{"caller_nid": "config_api_resolvenodeenv", "callee": "trim", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/sqxp-wx/config/api.js", "source_location": "L13"}, {"caller_nid": "config_api_resolvenodeenv", "callee": "String", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/sqxp-wx/config/api.js", "source_location": "L13"}, {"caller_nid": "config_api_resolvenodeenv", "callee": "trim", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/sqxp-wx/config/api.js", "source_location": "L20"}, {"caller_nid": "config_api_resolvenodeenv", "callee": "String", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/sqxp-wx/config/api.js", "source_location": "L20"}]}

Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
graphify-out/cache/ast/c99eea80b2e98bbb1c8c4bb5cdcd62414a62adf8e16a62a4a09071692fd555c5.json


Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
graphify-out/cache/ast/cc0271c71eab6cbcddb06620de10d6d1236221daed64d323ab0c1f2179314e0a.json


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

@@ -0,0 +1 @@
+{"nodes": [{"id": "users_wuzilong_work_00code_sqxp_2_sqxp_wx_config_static_assets_defaults_js", "label": "static-assets-defaults.js", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/sqxp-wx/config/static-assets-defaults.js", "source_location": "L1"}], "edges": [], "raw_calls": []}

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

@@ -0,0 +1 @@
+{"nodes": [{"id": "users_wuzilong_work_00code_sqxp_2_sqxp_wx_pages_supplier_delivery_delivery_vue", "label": "delivery.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/sqxp-wx/pages/supplier/delivery/delivery.vue", "source_location": "L1"}, {"id": "delivery_delivery_if", "label": "if()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/sqxp-wx/pages/supplier/delivery/delivery.vue", "source_location": "L195"}], "edges": [{"source": "users_wuzilong_work_00code_sqxp_2_sqxp_wx_pages_supplier_delivery_delivery_vue", "target": "delivery_delivery_if", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/sqxp-wx/pages/supplier/delivery/delivery.vue", "source_location": "L195", "weight": 1.0}], "raw_calls": [{"caller_nid": "delivery_delivery_if", "callee": "push", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/sqxp-wx/pages/supplier/delivery/delivery.vue", "source_location": "L196"}, {"caller_nid": "delivery_delivery_if", "callee": "add", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/sqxp-wx/pages/supplier/delivery/delivery.vue", "source_location": "L197"}]}

Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
graphify-out/cache/ast/dcc76b74cb3c528a47cbc59bf081a8c6bb35a893f758d4a1675174af556ff329.json


Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
graphify-out/cache/ast/ed11a07427a78814e65893c9f78174d148f2b941eb2c2448154abb28018b613f.json


+ 1 - 0
graphify-out/cache/ast/f2776505175c83eb733af2eca5cc06a5565f4e58ada0ec7e8ee92c7caead1fa0.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": "L124"}], "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": "L124", "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": "L204", "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": "L125"}, {"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": "L207"}]}

Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
graphify-out/cache/ast/f505b309c40e87071c0665e2989e434985f49c6138973b4b04b5d30c2f6dd209.json


Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
graphify-out/cache/ast/f69aa7aecc81d171fd59514ea319ce9d49af6e638e45176f75d2965bccc77c4b.json


Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
graphify-out/cache/ast/f6b6cb234bfb591be9925c2989dbbce3bcbbdb3d664ff674a92cd2698ccca832.json


Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
graphify-out/cache/ast/fb1b64782835b03af4a0a76fac022d8f344a2aa289e3702fb652dff08ce8c876.json


Разлика између датотеке није приказан због своје велике величине
+ 1 - 1
graphify-out/graph.html


Разлика између датотеке није приказан због своје велике величине
+ 115 - 115
graphify-out/graph.json


Разлика између датотеке није приказан због своје велике величине
+ 3787 - 0
sanqiang.sql


+ 11 - 0
sqxp-wx/App.vue

@@ -73,6 +73,17 @@ import { initGlobalSafeArea } from '@/utils/safeArea'
 		background-color: #ffffff;
 	}
 
+	/* 全局按钮兜底:避免微信/uni 默认 button-hover 覆盖导致点击发白 */
+	button.button-hover:not([disabled]) {
+		background-color: transparent;
+		opacity: 1;
+	}
+
+	/* 保留原按钮边框策略,避免点击后出现系统默认描边 */
+	button::after {
+		border: none;
+	}
+
 	/* #ifdef H5 */
 	html,
 	body {

+ 9 - 7
sqxp-wx/config/api.js

@@ -1,21 +1,23 @@
 // API 基础配置
-const DEV_BASE_URL = 'http://192.168.2.178:8000/api'
+// TODO(config-hardcoded): 当前为真机稳定性临时写死,后续恢复为可配置(构建期常量或安全配置中心)。
+const DEV_BASE_URL = 'https://gslyzn.com/api'
 let devBaseUrl = DEV_BASE_URL
 
 // 微信小程序真机调试时,localhost 指向手机本机,需使用电脑局域网 IP。
 // #ifdef MP-WEIXIN
-devBaseUrl = 'http://192.168.2.178:8000/api'
+devBaseUrl = 'https://gslyzn.com/api'
 // #endif
 
-export const BASE_URL = process.env.NODE_ENV === 'development'
-  ? devBaseUrl
-  : 'http://192.168.2.178:8000/api'
+// TODO(config-hardcoded): BASE_URL 目前写死,后续按环境分层配置并提供发布校验。
+// 统一采用写死配置:不依赖任何环境变量注入。
+export const BASE_URL = devBaseUrl
 
+// TODO(config-hardcoded): API_PREFIX 目前固定为空,若后续网关策略变更需改为可配置项。
 // 开发环境不需要 API 前缀 /api
-const API_PREFIX = process.env.NODE_ENV === 'development' ? '' : ''
+const API_PREFIX = ''
 
 export const API_BASE = `${BASE_URL}${API_PREFIX}`
-console.log('[ENV]', process.env.NODE_ENV, BASE_URL, API_BASE)
+console.log('[ENV]', 'hardcoded', BASE_URL, API_BASE)
 
 /** 与 miniprogram `config/api.ts` 中客户/评价相关路径一致,供多页与 `api/reviewApi.js` 使用 */
 export const API_URL = {

+ 8 - 8
sqxp-wx/config/static-assets-defaults.js

@@ -1,21 +1,21 @@
 /**
  * 远程静态资源根(须以 / 结尾),与部署端 nginx 静态目录一致。
  *
- * **测试 / 生产**
+ * TODO(config-hardcoded): 当前测试/生产同源写死,后续需拆分并由发布流程校验配置一致性。
+ *
+ * 写死模式:
  * - `STATIC_ASSETS_BASE_URL_TEST`:测试(预发)环境。
- * - `STATIC_ASSETS_BASE_URL_PRODUCTION`:正式环境(上线前替换占位或通过构建注入 VITE_STATIC_ASSETS_BASE_URL)。
- * - 运行时优先使用 `VITE_STATIC_ASSETS_BASE_URL`(若构建工具注入),否则按 `import.meta.env.MODE === 'production'` 选用生产/测试常量。
+ * - `STATIC_ASSETS_BASE_URL_PRODUCTION`:正式环境。
+ * - 运行时不读取环境变量,直接使用代码内常量。
  *
  * 小程序 tabBar 等若在 pages.json 写完整 URL,请与当前环境基址一致,或运行 `npm run sync-static-public-urls`。
  */
 /**
  * 远程静态资源根(须以 / 结尾)。
  *
- * 说明:
- * - 在部分小程序构建产物中,运行时无法读取 `import.meta.env` / `process.env`,因此必须提供可用的兜底基址。
- * - 生产环境请务必在 `.env.production` 中注入 `VITE_STATIC_ASSETS_BASE_URL`(或 `VUE_APP_STATIC_ASSETS_BASE_URL`)覆盖此兜底。
+ * 说明:小程序端统一采用固定基址,避免环境变量注入差异导致真机行为不一致。
  */
-export const STATIC_ASSETS_BASE_URL_TEST = 'https://www.chengyantek.cn/sqxp-static/'
-export const STATIC_ASSETS_BASE_URL_PRODUCTION = 'https://REPLACE_PROD_STATIC_HOST/sqxp-static/'
+export const STATIC_ASSETS_BASE_URL_TEST = 'https://gslyzn.com/sqxp-static/'
+export const STATIC_ASSETS_BASE_URL_PRODUCTION = 'https://gslyzn.com/sqxp-static/'
 /** @deprecated 同 TEST,占位兜底 */
 export const STATIC_ASSETS_BASE_URL_DEFAULT = STATIC_ASSETS_BASE_URL_TEST

+ 2 - 2
sqxp-wx/manifest.json

@@ -1,6 +1,6 @@
 {
     "name" : "sqxp-wx",
-    "appid" : "wxa261ad85f2c99b26",
+    "appid" : "wx19655f52f540c22e",
     "description" : "应用描述",
     "versionName" : "1.0.0",
     "versionCode" : "100",
@@ -95,7 +95,7 @@
         "versionCode" : 100
     },
     "mp-weixin" : {
-        "appid" : "wxa261ad85f2c99b26",
+        "appid" : "wx19655f52f540c22e",
         "setting" : {
             "urlCheck" : false
         },

+ 1 - 1
sqxp-wx/pages/client/cart/cart.vue

@@ -531,7 +531,7 @@ async function goCheckout() {
     return
   }
   await cartStore.fetchCart()
-  const cartIds = encodeURIComponent(Array.from(selectedIds.value).join(','))
+  const cartIds = Array.from(selectedIds.value).join(',')
   uni.navigateTo({ url: `/pages/client/order/create/create?cartIds=${cartIds}` })
 }
 

+ 11 - 1
sqxp-wx/pages/client/order/create/create.vue

@@ -401,8 +401,18 @@ onShow(() => {
 onLoad((options) => {
   const raw = options?.cartIds
   if (typeof raw === 'string') {
+    let decoded = raw.trim()
+    for (let i = 0; i < 2; i += 1) {
+      try {
+        const next = decodeURIComponent(decoded)
+        if (next === decoded) break
+        decoded = next
+      } catch {
+        break
+      }
+    }
     hasSelectedCartIdsParam.value = true
-    selectedCartIds.value = raw
+    selectedCartIds.value = decoded
       .split(',')
       .map(s => s.trim())
       .filter(Boolean)

+ 10 - 8
sqxp-wx/pages/client/order/pay/pay.vue

@@ -420,18 +420,20 @@ onLoad((query) => {
   color: #fff;
 }
 .btn.secondary {
-  background: #ffffff;
-  color: var(--c-brand-green);
-  border: 2rpx solid rgba(10, 159, 109, 0.45);
-  box-shadow: 0 2rpx 10rpx rgba(10, 159, 109, 0.1);
+  background: linear-gradient(145deg, var(--c-barley-yellow), #e4b458);
+  color: #fff;
+  border: none;
+  box-shadow: inset 0 -2rpx 0 rgba(0, 0, 0, 0.1);
+  font-weight: 700;
 }
 
-/* 微信小程序按钮点击态会挂 button-hover,需锁定文字色避免变白 */
+/* 支付按钮与首页“去结算”一致:点击态保持原样式不漂白 */
 .btn.secondary:active,
 .btn.secondary.button-hover {
-  color: var(--c-brand-green) !important;
-  background: #f3fbf7 !important;
-  border-color: rgba(10, 159, 109, 0.55) !important;
+  color: #fff !important;
+  background: linear-gradient(145deg, var(--c-barley-yellow), #e4b458) !important;
+  border: none !important;
+  opacity: 1 !important;
 }
 
 :deep(.btn.van-button) {

+ 145 - 1
sqxp-wx/pages/login/login.vue

@@ -16,6 +16,10 @@ const form = ref({
 })
 
 const isLoading = ref(false)
+const agreeProtocol = ref(false)
+const loginDisabled = computed(() => isLoading.value)
+const protocolDialogVisible = ref(false)
+let protocolDialogResolver = null
 
 onShow(() => {
   userStore.checkLoginStatus()
@@ -27,6 +31,14 @@ onShow(() => {
 const loginPageStyle = computed(() => ({ '--theme-color': '#07c160' }))
 
 const handleSubmit = async () => {
+  if (!agreeProtocol.value) {
+    const shouldContinue = await new Promise((resolve) => {
+      protocolDialogResolver = resolve
+      protocolDialogVisible.value = true
+    })
+    if (!shouldContinue) return
+    agreeProtocol.value = true
+  }
   if (!form.value.username) {
     uni.showToast({ title: '请输入用户名', icon: 'none' })
     return
@@ -58,6 +70,18 @@ const goPrivacyPolicy = () => {
   uni.navigateTo({ url: '/pages/login/privacy-policy' })
 }
 
+const toggleProtocolAgreement = () => {
+  agreeProtocol.value = !agreeProtocol.value
+}
+
+const resolveProtocolDialog = (confirmed) => {
+  protocolDialogVisible.value = false
+  if (protocolDialogResolver) {
+    protocolDialogResolver(confirmed)
+    protocolDialogResolver = null
+  }
+}
+
 /** 与 input placeholder 同色,用于 SVG mask 着色 */
 function iconMaskStyle(maskUrl) {
   return {
@@ -113,7 +137,7 @@ function iconMaskStyle(maskUrl) {
         </view>
 
         <view class="form-actions">
-          <button class="btn-login" :disabled="isLoading" @click="handleSubmit">
+          <button class="btn-login" :class="{ 'btn-login--disabled': loginDisabled }" :disabled="loginDisabled" @click="handleSubmit">
             <text v-if="isLoading" class="btn-login-text">登录中...</text>
             <text v-else class="btn-login-text">账号密码登录</text>
           </button>
@@ -122,6 +146,9 @@ function iconMaskStyle(maskUrl) {
 
       <view class="login-footer">
         <view class="footer-line">
+          <view class="agree-check" @tap="toggleProtocolAgreement">
+            <view class="agree-check__icon" :class="{ 'agree-check__icon--checked': agreeProtocol }">✓</view>
+          </view>
           <text class="footer-text">登录即表示同意</text>
           <text class="link" @tap="goUserAgreement">《用户服务协议》</text>
           <text class="footer-text">和</text>
@@ -129,6 +156,17 @@ function iconMaskStyle(maskUrl) {
         </view>
       </view>
     </view>
+
+    <view v-if="protocolDialogVisible" class="protocol-dialog-mask" @tap="resolveProtocolDialog(false)">
+      <view class="protocol-dialog" @tap.stop>
+        <text class="protocol-dialog__title">协议确认</text>
+        <text class="protocol-dialog__desc">登录前请先同意《用户服务协议》和《隐私政策》</text>
+        <view class="protocol-dialog__actions">
+          <view class="protocol-dialog__btn protocol-dialog__btn--cancel" @tap="resolveProtocolDialog(false)">否</view>
+          <view class="protocol-dialog__btn protocol-dialog__btn--confirm" @tap="resolveProtocolDialog(true)">是,继续登录</view>
+        </view>
+      </view>
+    </view>
   </view>
 </template>
 
@@ -298,10 +336,22 @@ function iconMaskStyle(maskUrl) {
   line-height: 1.2;
 }
 
+.btn-login--disabled {
+  background: #c8ccd3;
+  color: #ffffff;
+}
+
 .btn-login::after {
   border: none;
 }
 
+.btn-login.button-hover,
+.btn-login:active {
+  background: linear-gradient(135deg, var(--theme-color) 0%, #07c160 100%);
+  color: #fff;
+  opacity: 1;
+}
+
 .btn-login-text {
   line-height: 1.2;
   font-size: 30rpx;
@@ -321,6 +371,33 @@ function iconMaskStyle(maskUrl) {
   row-gap: 8rpx;
 }
 
+.agree-check {
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  min-width: 28rpx;
+  min-height: 28rpx;
+  margin-right: 4rpx;
+}
+
+.agree-check__icon {
+  width: 24rpx;
+  height: 24rpx;
+  border-radius: 50%;
+  border: 1px solid #c0c4cc;
+  color: transparent;
+  font-size: 16rpx;
+  line-height: 24rpx;
+  text-align: center;
+  background: #fff;
+}
+
+.agree-check__icon--checked {
+  border-color: var(--theme-color);
+  background: var(--theme-color);
+  color: #fff;
+}
+
 .footer-text,
 .link {
   font-size: 22rpx;
@@ -334,6 +411,73 @@ function iconMaskStyle(maskUrl) {
 .link {
   color: var(--theme-color);
 }
+
+.protocol-dialog-mask {
+  position: fixed;
+  inset: 0;
+  background: rgba(0, 0, 0, 0.42);
+  z-index: 99;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 40rpx;
+  box-sizing: border-box;
+}
+
+.protocol-dialog {
+  width: 100%;
+  max-width: 620rpx;
+  background: #fff;
+  border-radius: 24rpx;
+  padding: 36rpx 30rpx 28rpx;
+  box-sizing: border-box;
+  box-shadow: 0 14rpx 48rpx rgba(0, 0, 0, 0.16);
+}
+
+.protocol-dialog__title {
+  display: block;
+  text-align: center;
+  font-size: 34rpx;
+  font-weight: 700;
+  color: #303133;
+}
+
+.protocol-dialog__desc {
+  display: block;
+  margin-top: 16rpx;
+  text-align: center;
+  font-size: 26rpx;
+  line-height: 1.6;
+  color: #606266;
+}
+
+.protocol-dialog__actions {
+  margin-top: 30rpx;
+  display: flex;
+  gap: 16rpx;
+}
+
+.protocol-dialog__btn {
+  flex: 1;
+  min-height: 78rpx;
+  border-radius: 999rpx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 28rpx;
+  font-weight: 600;
+}
+
+.protocol-dialog__btn--cancel {
+  background: #f4f7f6;
+  color: #5b6b64;
+  border: 1px solid #ddebe5;
+}
+
+.protocol-dialog__btn--confirm {
+  background: linear-gradient(135deg, var(--theme-color) 0%, #07c160 100%);
+  color: #fff;
+}
 </style>
 
 <!-- placeholder-class 在部分真机需非 scoped 才稳定生效;颜色与 --login-input-placeholder 一致 -->

+ 11 - 1
sqxp-wx/pages/supplier/delivery/delivery.vue

@@ -8,6 +8,7 @@ import { formatYuan } from '@/utils/format';
 import PhoneDialInline from '@/components/phone-dial-inline.vue';
 import { DEFAULT_PLACEHOLDER_IMAGE_URL, IMG_EMPTY } from '@/utils/staticAsset';
 import ClientSkeleton from '@/components/client/client-skeleton.vue';
+import ClientImageLoader from '@/components/client/client-image-loader.vue';
 import SqCopyBtn from '@/components/sq-copy-btn.vue';
 const userStore = useUserStore();
 const PAGE_SIZE = 10;
@@ -767,10 +768,19 @@ page {
     flex: 0 0 auto;
   }
   .product-image {
+    display: block;
+    flex-shrink: 0;
     width: 100%;
     height: 168rpx;
     background: #ebeef5;
-    display: block;
+  }
+  .product-image :deep(.img-wrap) {
+    width: 100%;
+    height: 100%;
+  }
+  .product-image :deep(.img) {
+    width: 100%;
+    height: 100%;
   }
   .product-meta {
     display: flex;

+ 17 - 4
sqxp-wx/pages/supplier/inquiry/inquiry.vue

@@ -1,14 +1,13 @@
 <script setup>
 import { computed, ref, watch } from 'vue';
 import { onShow } from '@dcloudio/uni-app';
-import VanIcon from 'vant/es/icon';
-import 'vant/es/icon/style';
 import { useUserStore } from '@/stores/user';
 import { request, parseResponseJson } from '@/utils/request';
-import { DEFAULT_PLACEHOLDER_IMAGE_URL, IMG_EMPTY } from '@/utils/staticAsset';
+import { DEFAULT_PLACEHOLDER_IMAGE_URL, IMG_EMPTY, staticAssetUrl } from '@/utils/staticAsset';
 import ClientSkeleton from '@/components/client/client-skeleton.vue';
 import ClientImageLoader from '@/components/client/client-image-loader.vue';
 const userStore = useUserStore();
+const IMG_INQUIRY_SUBMIT = staticAssetUrl('commit-12.png');
 const isLoading = ref(false);
 const isSaving = ref(false);
 const currentInquiryId = ref('');
@@ -589,7 +588,13 @@ onShow(() => {
 
     <view v-if="currentInquiryId" class="footer-bar">
       <button class="save-btn" :disabled="!canSubmit" :loading="isSaving" @click="handleSave">
-        <VanIcon name="completed-o" size="24" :color="canSubmit ? '#ffffff' : '#e5e7eb'" />
+        <image
+          v-if="!isSaving"
+          class="save-btn-icon"
+          :src="IMG_INQUIRY_SUBMIT"
+          mode="aspectFit"
+          style="width: 72rpx; height: 72rpx;"
+        />
       </button>
     </view>
 
@@ -1093,6 +1098,14 @@ page {
   pointer-events: auto;
 }
 
+.save-btn-icon {
+  width: 72rpx !important;
+  height: 72rpx !important;
+  display: block;
+  transform: scale(1.9);
+  transform-origin: center;
+}
+
 .save-btn[disabled] {
   background: linear-gradient(145deg, #94a3b8, #cbd5e1);
   box-shadow: none;

+ 5 - 1
sqxp-wx/pages/supplier/quotation/quotation.vue

@@ -276,7 +276,10 @@ page {
 .quotation-root {
   position: relative;
   width: 100%;
+  height: 100%;
   min-height: 100%;
+  display: flex;
+  flex-direction: column;
 }
 
 /* #ifdef H5 */
@@ -303,6 +306,7 @@ page {
   height: 100%;
   max-height: 100%;
   min-height: 0;
+  flex: 1;
   display: flex;
   flex-direction: column;
   background: #f3f7f5;
@@ -350,7 +354,7 @@ page {
   flex: 1;
   flex-basis: 0;
   min-height: 0;
-  height: 0;
+  height: auto;
   width: 100%;
   box-sizing: border-box;
   overflow: hidden;

+ 5 - 23
sqxp-wx/utils/ossCache.js

@@ -2,29 +2,13 @@
 // 约束:不要用 Date.now() 常态化破缓存;默认用“可控版本号”,需要强刷时只改版本号即可。
 
 let _cachedVersion = null
+// TODO(config-hardcoded): 资源版本号当前写死,后续改为构建产物自动注入(如 commit hash / CI build number)。
+const STATIC_ASSETS_VERSION = '20260423'
 
 // 作为兜底版本号来源(构建期静态注入)
 // eslint-disable-next-line import/no-unresolved
 import manifest from '@/manifest.json'
 
-function pickEnv(name) {
-  try {
-    if (typeof import.meta !== 'undefined' && import.meta.env && import.meta.env[name]) {
-      return String(import.meta.env[name]).trim()
-    }
-  } catch {
-    // ignore
-  }
-  try {
-    if (typeof process !== 'undefined' && process.env && process.env[name]) {
-      return String(process.env[name]).trim()
-    }
-  } catch {
-    // ignore
-  }
-  return ''
-}
-
 function fallbackVersionFromManifest() {
   try {
     const vn = String(manifest?.versionName || '').trim()
@@ -38,16 +22,14 @@ function fallbackVersionFromManifest() {
 
 /**
  * 统一版本号:
- * - 优先:`VITE_STATIC_ASSETS_VERSION` / `VUE_APP_STATIC_ASSETS_VERSION` / `UNI_APP_STATIC_ASSETS_VERSION`
+ * - 写死常量:`STATIC_ASSETS_VERSION`
  * - 兜底:manifest.versionName/versionCode
- * - 最终兜底:固定日期(需要强刷时可直接改这个值或用环境变量覆盖)
+ * - 最终兜底:固定日期
  */
 export function getStaticAssetsVersion() {
   if (_cachedVersion != null) return _cachedVersion
   const v =
-    pickEnv('VITE_STATIC_ASSETS_VERSION') ||
-    pickEnv('VUE_APP_STATIC_ASSETS_VERSION') ||
-    pickEnv('UNI_APP_STATIC_ASSETS_VERSION') ||
+    STATIC_ASSETS_VERSION ||
     fallbackVersionFromManifest() ||
     '20260423'
   _cachedVersion = String(v || '').trim()

+ 101 - 20
sqxp-wx/utils/price-realtime.js

@@ -4,6 +4,15 @@ import { emit } from '@/utils/bus'
 
 const STORAGE_PRICE_VERSION_KEY = 'sq_active_price_version'
 const STORAGE_PRICE_LIST_ID_KEY = 'sq_active_price_list_id'
+const WS_LOG_PREFIX = '[price-ws]'
+
+function wsLog(...args) {
+  try {
+    console.log(WS_LOG_PREFIX, ...args)
+  } catch (e) {
+    // ignore
+  }
+}
 
 function getStoredVersion() {
   const v = uni.getStorageSync(STORAGE_PRICE_VERSION_KEY)
@@ -32,48 +41,83 @@ let socketTask = null
 let reconnectTimer = null
 let reconnectDelayMs = 1000
 let isConnecting = false
-let listenersBound = false
+let h5ListenersBound = false
+let shouldReconnect = true
+let socketOpened = false
+let pollTimer = null
+const FALLBACK_POLL_INTERVAL_MS = 5000
+const WS_ENABLED = true
 
-function bindSocketListeners() {
-  if (listenersBound) return
+function safeCloseSocket(task, reason = 'client-close') {
+  // 仅在连接已建立后主动关闭,避免小程序端在未连接状态 close 抛错
+  if (!socketOpened) {
+    wsLog('skip close: socket not opened yet', { reason })
+    return false
+  }
+  // 微信小程序仅允许 1000 或 3000-4999 的关闭码
+  const closeOptions = { code: 1000, reason }
+  if (task && typeof task.close === 'function') {
+    wsLog('closing socket', { reason, code: 1000 })
+    // #ifdef MP-WEIXIN
+    task.close({ code: 1000 })
+    // #endif
+    // #ifndef MP-WEIXIN
+    task.close(closeOptions)
+    // #endif
+    return true
+  }
+  // 无有效 task 时不调用 uni.closeSocket,避免出现“WebSocket is not connected / invalid code”噪音
+  return false
+}
 
+function bindSocketListeners() {
   const onOpen = () => {
+    wsLog('socket open')
+    socketOpened = true
+    clearFallbackPoll()
     reconnectDelayMs = 1000
     void checkActiveVersionAndEmitIfChanged()
   }
   const onMessage = (evt) => {
+    wsLog('socket message received')
     handleWsMessage(evt?.data)
   }
   const onClose = () => {
+    wsLog('socket close', { shouldReconnect })
+    socketOpened = false
     socketTask = null
-    scheduleReconnect()
+    ensureFallbackPollRunning()
+    if (shouldReconnect) scheduleReconnect()
   }
   const onError = () => {
+    wsLog('socket error', { shouldReconnect })
     try {
-      socketTask && socketTask.close && socketTask.close()
+      // 错误回调中不主动 close,交给 onClose,避免重复关闭触发小程序异常
     } catch (e) {
       // ignore
     }
+    socketOpened = false
     socketTask = null
-    scheduleReconnect()
+    ensureFallbackPollRunning()
+    if (shouldReconnect) scheduleReconnect()
   }
 
-  // App/小程序端:SocketTask 事件方法
+  // App/小程序端:SocketTask 事件方法(每次重连都会产生新的 SocketTask,需要重新绑定监听)
   if (socketTask && typeof socketTask.onOpen === 'function') {
     socketTask.onOpen(onOpen)
     socketTask.onMessage(onMessage)
     socketTask.onClose(onClose)
     socketTask.onError(onError)
-    listenersBound = true
     return
   }
 
-  // H5 端:uni 全局 socket 事件
+  // H5 端:uni 全局 socket 事件(全局监听只需注册一次)
+  if (h5ListenersBound) return
   if (typeof uni.onSocketOpen === 'function') uni.onSocketOpen(onOpen)
   if (typeof uni.onSocketMessage === 'function') uni.onSocketMessage(onMessage)
   if (typeof uni.onSocketClose === 'function') uni.onSocketClose(onClose)
   if (typeof uni.onSocketError === 'function') uni.onSocketError(onError)
-  listenersBound = true
+  h5ListenersBound = true
 }
 
 function clearReconnect() {
@@ -81,9 +125,23 @@ function clearReconnect() {
   reconnectTimer = null
 }
 
+function clearFallbackPoll() {
+  if (pollTimer) clearInterval(pollTimer)
+  pollTimer = null
+}
+
+function ensureFallbackPollRunning() {
+  if (pollTimer) return
+  wsLog('start fallback polling', { intervalMs: FALLBACK_POLL_INTERVAL_MS })
+  pollTimer = setInterval(() => {
+    void checkActiveVersionAndEmitIfChanged()
+  }, FALLBACK_POLL_INTERVAL_MS)
+}
+
 function scheduleReconnect() {
   if (reconnectTimer) return
   const delay = reconnectDelayMs
+  wsLog('schedule reconnect', { delay })
   reconnectDelayMs = Math.min(reconnectDelayMs * 2, 30000)
   reconnectTimer = setTimeout(() => {
     reconnectTimer = null
@@ -101,6 +159,7 @@ async function checkActiveVersionAndEmitIfChanged() {
     const data = body && body.code === 200 ? body.data : null
     const newVersion = Number(data?.priceVersion || 0) || 0
     const oldVersion = getStoredVersion()
+    wsLog('poll active-version', { oldVersion, newVersion, priceListId: data?.priceListId || '' })
     if (newVersion > 0 && newVersion !== oldVersion) {
       setStoredVersion(newVersion, data?.priceListId)
       emit('price_list_updated', data)
@@ -123,6 +182,12 @@ function handleWsMessage(raw) {
   const oldVersion = getStoredVersion()
   const oldPriceListId = getStoredPriceListId()
   const newPriceListId = String(data.priceListId || '').trim()
+  wsLog('price_list_updated event', {
+    oldVersion,
+    newVersion,
+    oldPriceListId,
+    newPriceListId
+  })
 
   // WebSocket 推送属于“已发生更新”的强信号:
   // 即便版本号缺失/相同,也触发页面刷新,避免界面停留旧价。
@@ -140,35 +205,51 @@ function handleWsMessage(raw) {
 
 export async function ensurePriceRealtimeConnected() {
   const token = String(uni.getStorageSync(TOKEN_KEY) || '').trim()
-  if (!token) return
-  if (isConnecting) return
-  if (socketTask) return
+  if (!token) {
+    wsLog('skip connect: empty token')
+    ensureFallbackPollRunning()
+    return
+  }
+  if (isConnecting) {
+    wsLog('skip connect: already connecting')
+    return
+  }
+  if (socketTask) {
+    wsLog('skip connect: socketTask exists')
+    return
+  }
 
   isConnecting = true
+  shouldReconnect = true
+  socketOpened = false
   try {
     clearReconnect()
+    ensureFallbackPollRunning()
     const url = buildWsUrl(token)
-    socketTask = uni.connectSocket({ url })
-    bindSocketListeners()
+    wsLog('connect socket', { url })
+    if (WS_ENABLED) {
+      socketTask = uni.connectSocket({ url })
+      bindSocketListeners()
+    }
   } finally {
     isConnecting = false
   }
 }
 
 export function stopPriceRealtime() {
+  wsLog('stop realtime')
   clearReconnect()
+  clearFallbackPoll()
   reconnectDelayMs = 1000
+  shouldReconnect = false
   if (socketTask) {
     try {
-      if (typeof socketTask.close === 'function') {
-        socketTask.close()
-      } else if (typeof uni.closeSocket === 'function') {
-        uni.closeSocket()
-      }
+      safeCloseSocket(socketTask, 'manual-stop')
     } catch (e) {
       // ignore
     }
   }
+  socketOpened = false
   socketTask = null
 }
 

+ 19 - 24
sqxp-wx/utils/request.js

@@ -1,34 +1,24 @@
 import { API_BASE, TOKEN_KEY, REFRESH_TOKEN_KEY, USER_INFO_KEY } from '@/config/api'
 
 let clearingAuth = false
-function readEnv(key, fallback = '') {
-  // HBuilderX / uni-app H5 有时不会注入自定义 env,这里做兜底
-  const v = process.env && Object.prototype.hasOwnProperty.call(process.env, key) ? process.env[key] : undefined
-  if (v == null || v === '') return fallback
-  return v
-}
-
-// HBuilderX/H5 构建时 env 注入不稳定:这里默认开启,避免发布到浏览器后退回明文。
-// 若确需关闭,可在环境变量中显式设置 VUE_APP_ENABLE_API_CRYPTO=false
-const API_ENCRYPT_ENABLED =
-  String(readEnv('VUE_APP_ENABLE_API_CRYPTO', readEnv('VITE_ENABLE_API_CRYPTO', 'true'))) === 'true'
-const API_ENCRYPT_FAIL_OPEN =
-  String(readEnv('VUE_APP_API_ENCRYPT_FAIL_OPEN', readEnv('VITE_API_ENCRYPT_FAIL_OPEN', 'false'))) ===
-  'true'
-const API_ENCRYPT_FIXED_SECRET = String(
-  readEnv(
-    'VUE_APP_API_ENCRYPT_FIXED_SECRET',
-    readEnv('VITE_API_ENCRYPT_FIXED_SECRET', 'sqxp-web-api-crypto-fixed-secret')
-  )
-).trim()
+// TODO(config-hardcoded): 以下加密参数为临时写死,后续应迁移至构建期注入或安全配置下发。
+// 写死配置:不从环境变量读取,避免小程序端注入差异导致行为不一致。
+let API_ENCRYPT_ENABLED = true
+const API_ENCRYPT_FAIL_OPEN = false
+const API_ENCRYPT_FIXED_SECRET = 'sqxp-web-api-crypto-fixed-secret'
+// TODO(config-hardcoded): 排除路径清单后续改为集中配置,避免多端重复维护。
 const API_ENCRYPT_EXCLUDE_PATHS = String(
-  readEnv('VUE_APP_API_ENCRYPT_EXCLUDE_PATHS', readEnv('VITE_API_ENCRYPT_EXCLUDE_PATHS', '')) ||
-    '/mini/auth/login-rsa-public-key,/mini/auth/login,/mini/auth/sms-login,/mini/auth/wechat-login,/mini/auth/code-openid,/mini/common/china-region-tree'
+  '/mini/auth/login-rsa-public-key,/mini/auth/login,/mini/auth/sms-login,/mini/auth/wechat-login,/mini/auth/code-openid,/mini/common/china-region-tree'
 )
   .split(',')
   .map((item) => item.trim())
   .filter(Boolean)
 
+// #ifdef MP-WEIXIN
+// 微信小程序运行时不保证 WebCrypto(getRandomValues/subtle),关闭前端加密避免全链路请求失败。
+API_ENCRYPT_ENABLED = false
+// #endif
+
 function appendTimestampQuery(url) {
   const base = String(url || '')
   const ts = Date.now()
@@ -216,11 +206,16 @@ function normalizeRequestData(data) {
   return typeof data === 'object' ? data : {}
 }
 
+function isNativeFormData(data) {
+  // 微信小程序等运行时可能不存在全局 FormData,直接 instanceof 会抛错
+  return typeof FormData !== 'undefined' && data instanceof FormData
+}
+
 function shouldEncryptRequest(rawUrl, userId, data) {
   if (!API_ENCRYPT_ENABLED || !API_ENCRYPT_FIXED_SECRET) return false
   if (!rawUrl || !String(rawUrl).includes('/mini/')) return false
   if (!userId) return false
-  if (data instanceof FormData) return false
+  if (isNativeFormData(data)) return false
   return !API_ENCRYPT_EXCLUDE_PATHS.some((prefix) => String(rawUrl).startsWith(prefix))
 }
 
@@ -229,7 +224,7 @@ function getEncryptSkipReason(rawUrl, userId, data) {
   if (!API_ENCRYPT_FIXED_SECRET) return 'missing-fixed-secret'
   if (!rawUrl || !String(rawUrl).includes('/mini/')) return 'non-mini-path'
   if (!userId) return 'missing-user-id'
-  if (data instanceof FormData) return 'form-data'
+  if (isNativeFormData(data)) return 'form-data'
   if (API_ENCRYPT_EXCLUDE_PATHS.some((prefix) => String(rawUrl).startsWith(prefix))) return 'exclude-path'
   return ''
 }

+ 3 - 33
sqxp-wx/utils/staticAsset.js

@@ -10,40 +10,10 @@ function normalizeBase(u) {
   return s.endsWith('/') ? s : `${s}/`
 }
 
-function envBaseExplicit() {
-  try {
-    if (typeof import.meta !== 'undefined' && import.meta.env && import.meta.env.VITE_STATIC_ASSETS_BASE_URL) {
-      return String(import.meta.env.VITE_STATIC_ASSETS_BASE_URL).trim()
-    }
-  } catch {
-    // ignore
-  }
-  // 兼容 uni-app webpack/HBuilderX:使用 process.env(通常要求 VUE_APP_ 前缀)
-  try {
-    if (typeof process !== 'undefined' && process.env) {
-      const v =
-        process.env.VITE_STATIC_ASSETS_BASE_URL ||
-        process.env.VUE_APP_STATIC_ASSETS_BASE_URL ||
-        process.env.UNI_APP_STATIC_ASSETS_BASE_URL
-      if (v) return String(v).trim()
-    }
-  } catch {
-    // ignore
-  }
-  return ''
-}
-
 function resolveBase() {
-  const explicit = envBaseExplicit()
-  if (explicit) return normalizeBase(explicit)
-  try {
-    if (import.meta.env && import.meta.env.MODE === 'production') {
-      return normalizeBase(STATIC_ASSETS_BASE_URL_PRODUCTION)
-    }
-  } catch {
-    // ignore
-  }
-  return normalizeBase(STATIC_ASSETS_BASE_URL_TEST)
+  // TODO(config-hardcoded): 静态资源基址当前写死,后续恢复为发布时可切换配置并增加联调检查。
+  // 写死模式:不读取环境变量,不依赖 MODE 判定。
+  return normalizeBase(STATIC_ASSETS_BASE_URL_PRODUCTION || STATIC_ASSETS_BASE_URL_TEST)
 }
 
 export function getStaticAssetsBaseUrl() {

+ 5 - 0
www.gslyzn.com_0430061311/IIS/README.txt

@@ -0,0 +1,5 @@
+注意:
+  密码为:Vc6v57if
+
+Note:
+ The password is:Vc6v57if

+ 8 - 0
www.gslyzn.com_0430061311/Tomcat8_4/README.txt

@@ -0,0 +1,8 @@
+注意:
+  密码为:Vc6v57if
+  alias:www.gslyzn.com
+如果 Tomcat 的版本大于 8.5 请将 *.jks 文件的名称更改为 tomcat.jks 
+Note:
+ The password is:Vc6v57if
+  alias:www.gslyzn.com
+If the version of Tomcat is greater than 8.5, please change the name of the *.jks ZipFile to tomcat.jks

+ 8 - 0
www.gslyzn.com_0430061311/Tomcat8_5+/README.txt

@@ -0,0 +1,8 @@
+注意:
+  密码为:Vc6v57if
+  alias:www.gslyzn.com
+如果 Tomcat 的版本大于 8.5 请将 *.jks 文件的名称更改为 tomcat.jks 
+Note:
+ The password is:Vc6v57if
+  alias:www.gslyzn.com
+If the version of Tomcat is greater than 8.5, please change the name of the *.jks ZipFile to tomcat.jks

+ 860 - 0
生产环境部署/Ubuntu24.04-生产部署手册.md

@@ -0,0 +1,860 @@
+# SQXP 生产环境部署手册(Ubuntu 24.04)
+
+本文档用于在全新 `Ubuntu 24.04` 服务器(`2C8G`,IP:`125.75.68.71`,域名:`gslyzn.com`)部署系统。
+
+部署策略:
+- 中间件使用 Docker:`nginx`、`mysql:5.7`、`redis`、`minio`
+- 业务工程非 Docker:`backend`、`web-admin`(打包产物)、静态资源目录
+- 中间件配置与数据统一落盘在 `/root/soft`
+
+---
+
+## 1. 目标目录规划
+
+```bash
+mkdir -p /root/soft/{nginx,mysql,redis,minio}
+mkdir -p /root/soft/nginx/{conf.d,html,logs,certs}
+mkdir -p /root/soft/mysql/{conf,data,logs}
+mkdir -p /root/soft/redis/{conf,data,logs}
+mkdir -p /root/soft/minio/{data,config}
+
+mkdir -p /root/sqxp/{backend,web,assets,logs}
+```
+
+目录说明:
+- `/root/soft/nginx`:Nginx 配置、证书、日志
+- `/root/soft/mysql`:MySQL 配置、数据、日志
+- `/root/soft/redis`:Redis 配置、数据、日志
+- `/root/soft/minio`:MinIO 数据、配置
+- `/root/sqxp/backend`:后端代码目录(你会上传)
+- `/root/sqxp/web`:`web-admin` 打包产物目录
+- `/root/sqxp/assets`:业务静态资源目录(Nginx 代理到 `/sqxp-static/`)
+
+---
+
+## 2. 安装系统基础组件(宿主机)
+
+```bash
+apt update
+apt install -y curl wget vim git unzip net-tools htop jq
+apt install -y ca-certificates gnupg lsb-release
+```
+
+### 2.1 安装 Docker 与 Compose
+
+```bash
+install -m 0755 -d /etc/apt/keyrings
+curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
+chmod a+r /etc/apt/keyrings/docker.asc
+
+echo \
+  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
+  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
+  tee /etc/apt/sources.list.d/docker.list > /dev/null
+
+apt update
+apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
+systemctl enable docker
+systemctl start docker
+docker --version
+docker compose version
+```
+
+---
+
+## 3. 中间件容器编排(Docker Compose)
+
+在 `/root/soft/docker-compose.yml` 写入以下内容:
+
+```yaml
+services:
+  mysql:
+    image: mysql:5.7
+    container_name: sqxp-mysql
+    restart: always
+    ports:
+      - "13306:3306"
+    environment:
+      MYSQL_ROOT_PASSWORD: "RootSqxP123!234"
+      TZ: "Asia/Shanghai"
+    command:
+      [
+        "mysqld",
+        "--defaults-file=/etc/mysql/conf.d/my.cnf"
+      ]
+    volumes:
+      - /root/soft/mysql/conf/my.cnf:/etc/mysql/conf.d/my.cnf:ro
+      - /root/soft/mysql/data:/var/lib/mysql
+      - /root/soft/mysql/logs:/var/log/mysql
+    networks:
+      - sqxp-net
+    ulimits:
+      nofile:
+        soft: 65535
+        hard: 65535
+
+  redis:
+    image: redis:7-alpine
+    container_name: sqxp-redis
+    restart: always
+    ports:
+      - "16379:6379"
+    command: ["redis-server", "/usr/local/etc/redis/redis.conf"]
+    volumes:
+      - /root/soft/redis/conf/redis.conf:/usr/local/etc/redis/redis.conf:ro
+      - /root/soft/redis/data:/data
+    networks:
+      - sqxp-net
+    ulimits:
+      nofile:
+        soft: 65535
+        hard: 65535
+
+  minio:
+    image: minio/minio:latest
+    container_name: sqxp-minio
+    restart: always
+    ports:
+      - "20003:20003"
+      - "20004:20004"
+    environment:
+      MINIO_ROOT_USER: "minioadmin"
+      MINIO_ROOT_PASSWORD: "RootSqxP123!234"
+      TZ: "Asia/Shanghai"
+    command: server /data --address ":20003" --console-address ":20004"
+    volumes:
+      - /root/soft/minio/data:/data
+      - /root/soft/minio/config:/root/.minio
+    networks:
+      - sqxp-net
+
+  nginx:
+    image: nginx:1.26-alpine
+    container_name: sqxp-nginx
+    restart: always
+    ports:
+      - "80:80"
+      - "443:443"
+    volumes:
+      - /root/soft/nginx/nginx.conf:/etc/nginx/nginx.conf:ro
+      - /root/soft/nginx/conf.d:/etc/nginx/conf.d:ro
+      - /root/soft/nginx/logs:/var/log/nginx
+      - /root/soft/nginx/certs:/etc/nginx/certs:ro
+      - /root/sqxp/web:/srv/web:ro
+      - /root/sqxp/assets:/srv/assets:ro
+    depends_on:
+      - mysql
+      - redis
+      - minio
+    extra_hosts:
+      - "host.docker.internal:host-gateway"
+    networks:
+      - sqxp-net
+    ulimits:
+      nofile:
+        soft: 65535
+        hard: 65535
+
+networks:
+  sqxp-net:
+    driver: bridge
+```
+
+---
+
+## 4. 中间件优化配置
+
+### 4.1 MySQL 配置(大小写不敏感)
+
+写入 `/root/soft/mysql/conf/my.cnf`:
+
+```cnf
+[mysqld]
+user=mysql
+port=3306
+character-set-server=utf8mb4
+collation-server=utf8mb4_general_ci
+skip-name-resolve=1
+max_connections=300
+max_connect_errors=1000
+table_open_cache=1024
+thread_cache_size=64
+tmp_table_size=64M
+max_heap_table_size=64M
+sort_buffer_size=2M
+join_buffer_size=2M
+read_buffer_size=1M
+read_rnd_buffer_size=4M
+innodb_buffer_pool_size=2G
+innodb_buffer_pool_instances=2
+innodb_log_file_size=256M
+innodb_log_buffer_size=16M
+innodb_flush_log_at_trx_commit=1
+innodb_flush_method=O_DIRECT
+innodb_file_per_table=1
+sql_mode=STRICT_TRANS_TABLES,NO_ENGINE_SUBSTITUTION
+lower_case_table_names=1
+default-time-zone=+08:00
+log_timestamps=SYSTEM
+slow_query_log=1
+long_query_time=1
+slow_query_log_file=/var/log/mysql/slow.log
+
+[client]
+default-character-set=utf8mb4
+```
+
+重点:
+- `lower_case_table_names=1` 已开启(满足你要求的大小写不敏感)
+- 建议在第一次初始化数据库时就固定该参数,不要中途改
+
+### 4.1.1 开启 MySQL root 远程登录(`host='%'`)
+
+中间件启动后执行以下命令,确保 `root@'%'` 可远程登录,同时保留本机 `root@'localhost'`:
+
+```bash
+docker exec -i sqxp-mysql mysql -uroot -p'RootSqxP123!234' <<'EOF'
+CREATE USER IF NOT EXISTS 'root'@'%' IDENTIFIED BY 'RootSqxP123!234';
+ALTER USER 'root'@'%' IDENTIFIED BY 'RootSqxP123!234';
+GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' WITH GRANT OPTION;
+
+CREATE USER IF NOT EXISTS 'root'@'localhost' IDENTIFIED BY 'RootSqxP123!234';
+ALTER USER 'root'@'localhost' IDENTIFIED BY 'RootSqxP123!234';
+GRANT ALL PRIVILEGES ON *.* TO 'root'@'localhost' WITH GRANT OPTION;
+
+FLUSH PRIVILEGES;
+SELECT user, host FROM mysql.user WHERE user='root';
+EOF
+```
+
+验证远程授权是否生效:
+
+```bash
+docker exec -i sqxp-mysql mysql -uroot -p'RootSqxP123!234' -e "SELECT user, host FROM mysql.user WHERE user='root';"
+```
+
+安全建议:
+- 仅在确有远程运维需求时开放 `root@'%'`
+- `13306` 端口务必做安全组白名单或 VPN 限制,不建议全网放开
+- 长期建议新增专用管理账号,避免日常使用 `root`
+
+一条补丁命令(用于你前面的“3 段命令超精简版”场景):
+
+```bash
+docker exec -i sqxp-mysql mysql -uroot -p'RootSqxP123!234' <<'EOF'
+CREATE USER IF NOT EXISTS 'root'@'%' IDENTIFIED BY 'RootSqxP123!234';
+ALTER USER 'root'@'%' IDENTIFIED BY 'RootSqxP123!234';
+GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' WITH GRANT OPTION;
+
+CREATE USER IF NOT EXISTS 'root'@'localhost' IDENTIFIED BY 'RootSqxP123!234';
+ALTER USER 'root'@'localhost' IDENTIFIED BY 'RootSqxP123!234';
+GRANT ALL PRIVILEGES ON *.* TO 'root'@'localhost' WITH GRANT OPTION;
+
+FLUSH PRIVILEGES;
+SELECT user, host FROM mysql.user WHERE user='root';
+EOF
+```
+
+### 4.1.2 创建 `sanqiang` 数据库(初始化 SQL 脚本)
+
+将以下内容保存为 `init-sanqiang.sql` 并执行:
+
+```sql
+-- 创建业务数据库(不存在则创建)
+CREATE DATABASE IF NOT EXISTS `sanqiang`
+  DEFAULT CHARACTER SET utf8mb4
+  DEFAULT COLLATE utf8mb4_general_ci;
+
+-- 切换到业务数据库
+USE `sanqiang`;
+
+-- 校验当前数据库
+SELECT DATABASE() AS current_db;
+```
+
+在当前部署环境可直接执行:
+
+```bash
+docker exec -i sqxp-mysql mysql -h127.0.0.1 -P3306 -uroot -p'RootSqxP123!234' < init-sanqiang.sql
+docker exec -i sqxp-mysql mysql -h127.0.0.1 -P3306 -uroot -p'RootSqxP123!234' -e "SHOW DATABASES LIKE 'sanqiang';"
+```
+
+### 4.2 Redis 配置
+
+写入 `/root/soft/redis/conf/redis.conf`:
+
+```conf
+bind 0.0.0.0
+port 6379
+requirepass RootSqxP123!234
+timeout 300
+tcp-keepalive 60
+databases 16
+appendonly yes
+appendfsync everysec
+auto-aof-rewrite-percentage 100
+auto-aof-rewrite-min-size 64mb
+save 900 1
+save 300 10
+save 60 10000
+maxmemory 512mb
+maxmemory-policy allkeys-lru
+loglevel notice
+logfile ""
+dir /data
+```
+
+说明(生产环境强烈建议):
+- 建议将 `logfile` 改为 `logfile ""`,让 Redis 日志走容器标准输出,统一通过 `docker logs sqxp-redis` 查看
+- 若继续使用文件日志(例如 `logfile /var/log/redis/redis.log`),需在 compose 中额外挂载 `/var/log/redis`,并确保目录可写,否则会触发 `Can't open the log file`
+
+### 4.3 MinIO 配置建议
+
+当前 compose 已指定:
+- API 端口:`20003`
+- Console 端口:`20004`
+- 账号:`minioadmin`
+- 密码:`RootSqxP123!234`
+
+建议:
+- 桶策略仅开放必要路径,上传/下载走后端签名 URL
+- 定期备份 `/root/soft/minio/data`
+
+### 4.4 Nginx 主配置
+
+写入 `/root/soft/nginx/nginx.conf`:
+
+```nginx
+user  nginx;
+worker_processes  auto;
+
+error_log  /var/log/nginx/error.log warn;
+pid        /var/run/nginx.pid;
+
+events {
+    worker_connections  4096;
+    use epoll;
+    multi_accept on;
+}
+
+http {
+    include       /etc/nginx/mime.types;
+    default_type  application/octet-stream;
+
+    log_format main '$remote_addr - $remote_user [$time_local] "$request" '
+                    '$status $body_bytes_sent "$http_referer" '
+                    '"$http_user_agent" "$http_x_forwarded_for"';
+
+    access_log  /var/log/nginx/access.log  main;
+
+    sendfile        on;
+    tcp_nopush      on;
+    tcp_nodelay     on;
+    keepalive_timeout  65;
+    types_hash_max_size 4096;
+    client_max_body_size 100m;
+    server_tokens off;
+
+    gzip on;
+    gzip_min_length 1k;
+    gzip_comp_level 5;
+    gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
+
+    include /etc/nginx/conf.d/*.conf;
+}
+```
+
+---
+
+## 5. Nginx 站点配置(域名 gslyzn.com)
+
+写入 `/root/soft/nginx/conf.d/sqxp.conf`:
+
+```nginx
+server {
+    listen 80;
+    server_name gslyzn.com www.gslyzn.com;
+
+    location /.well-known/acme-challenge/ {
+        root /srv/web;
+    }
+
+    location / {
+        return 301 https://$host$request_uri;
+    }
+}
+
+server {
+    listen 443 ssl http2;
+    server_name gslyzn.com www.gslyzn.com;
+
+    ssl_certificate     /etc/nginx/certs/fullchain.pem;
+    ssl_certificate_key /etc/nginx/certs/privkey.pem;
+    ssl_protocols TLSv1.2 TLSv1.3;
+    ssl_ciphers HIGH:!aNULL:!MD5;
+
+    root /srv/web;
+    index index.html;
+
+    location / {
+        try_files $uri $uri/ /index.html;
+    }
+
+    location /api/ {
+        proxy_pass http://host.docker.internal:8000/api/;
+        proxy_http_version 1.1;
+        proxy_set_header Host $host;
+        proxy_set_header X-Real-IP $remote_addr;
+        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+        proxy_set_header X-Forwarded-Proto $scheme;
+        proxy_read_timeout 120s;
+    }
+
+    location /sqxp-static/ {
+        alias /srv/assets/;
+        autoindex off;
+        expires 30d;
+        add_header Cache-Control "public, max-age=2592000";
+    }
+
+    # MinIO 桶 sanqiang 对外访问(模式 B)
+    location /minio-static/ {
+        proxy_pass http://host.docker.internal:20003/sanqiang/;
+        proxy_http_version 1.1;
+        proxy_set_header Host $host;
+        proxy_set_header X-Real-IP $remote_addr;
+        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+        proxy_set_header X-Forwarded-Proto $scheme;
+        proxy_read_timeout 120s;
+    }
+}
+```
+
+说明:
+- `web-admin` 通过根路径 `/` 提供
+- 后端通过 `/api/` 反向代理到宿主机 `8000`
+- 静态资源通过 `/sqxp-static/` 对外提供,对应目录 `/root/sqxp/assets`
+- MinIO 资源通过 `/minio-static/` 对外提供,对应 MinIO 桶 `sanqiang`
+- 若容器内 `host.docker.internal` 不可用,可改为宿主机实际内网 IP(如 `172.17.0.1`)
+
+---
+
+## 6. 启动中间件
+
+```bash
+cd /root/soft
+docker compose up -d
+docker compose ps
+```
+
+检查:
+
+```bash
+docker logs -f sqxp-mysql
+docker logs -f sqxp-redis
+docker logs -f sqxp-minio
+docker logs -f sqxp-nginx
+```
+
+---
+
+## 7. 业务工程部署(非 Docker)
+
+## 7.1 Backend(`/root/sqxp/backend`)
+
+你问到“是不是要安装 Python”:**需要安装**(后端是 Python 服务)。
+
+建议安装:
+
+```bash
+apt update
+apt install -y python3 python3-venv python3-pip
+python3 --version
+```
+
+部署步骤:
+1. 上传后端代码到 `/root/sqxp/backend`
+2. 在后端目录执行:
+
+```bash
+cd /root/sqxp/backend
+chmod +x start-prod.sh
+./start-prod.sh start
+./start-prod.sh status
+```
+
+默认按脚本监听 `8000`(符合你要求)。
+
+### 7.1.1 推荐 systemd 托管 backend
+
+创建 `/etc/systemd/system/sqxp-backend.service`:
+
+```ini
+[Unit]
+Description=SQXP Backend Service
+After=network.target
+
+[Service]
+Type=forking
+WorkingDirectory=/root/sqxp/backend
+ExecStart=/root/sqxp/backend/start-prod.sh start
+ExecStop=/root/sqxp/backend/start-prod.sh stop
+ExecReload=/root/sqxp/backend/start-prod.sh restart
+Restart=always
+RestartSec=5
+User=root
+Environment=PORT=8000
+
+[Install]
+WantedBy=multi-user.target
+```
+
+启用:
+
+```bash
+systemctl daemon-reload
+systemctl enable sqxp-backend
+systemctl start sqxp-backend
+systemctl status sqxp-backend
+```
+
+### 7.1.2 生产服务器实际启动留痕(2026-04-30)
+
+本节记录本次在生产机 `root@ecm-f782` 启动后端服务的完整过程,便于审计与复盘。
+
+#### A. 实际目录与启动口径
+
+- 实际部署目录:`/root/sqxp/backend/backend`
+- 启动脚本:`start-prod.sh`
+- 环境文件:`.env.production`
+- 监听端口:`8000`
+
+实际启动命令:
+
+```bash
+cd /root/sqxp/backend/backend
+ENV_FILE=./.env.production ./start-prod.sh start
+./start-prod.sh status
+```
+
+#### B. 首次启动遇到的问题与修复
+
+1) `sh start-prod.sh` 报错(`Illegal option -o pipefail`)  
+原因:`sh`(dash)不支持 `pipefail`。  
+修复:统一使用 `bash`/可执行方式启动:
+
+```bash
+chmod +x start-prod.sh
+./start-prod.sh start
+```
+
+2) 脚本行尾格式问题(`not found`)  
+原因:脚本含 CRLF 行尾。  
+修复:
+
+```bash
+sed -i 's/\r$//' start-prod.sh
+```
+
+3) `venv/bin/python` 不存在  
+原因:`venv` 目录残缺(历史失败痕迹)。  
+修复:
+
+```bash
+rm -rf venv
+python3 -m venv venv
+source venv/bin/activate
+pip install -r requirements.txt
+deactivate
+```
+
+4) 固定 RSA 私钥后启动失败(`Unable to load PEM file ... InvalidByte(0, 92)`)  
+原因:`.env.production` 中 `LOGIN_RSA_PRIVATE_KEY_PEM` 以单行 `\n` 形式写入,后端未展开换行。  
+修复结果:后端已兼容 `\n` 展开逻辑(`app/core/login_crypto.py`),并成功启动。
+
+#### C. 固定 RSA 私钥步骤(生产建议保留)
+
+```bash
+cd /root/sqxp/backend/backend
+openssl genrsa -out login_rsa_private.pem 2048
+chmod 600 login_rsa_private.pem
+
+RSA_ONE_LINE=$(awk '{printf "%s\\n",$0}' login_rsa_private.pem)
+sed -i '/^LOGIN_RSA_PRIVATE_KEY_PEM=/d' .env.production
+printf 'LOGIN_RSA_PRIVATE_KEY_PEM=%s\n' "$RSA_ONE_LINE" >> .env.production
+```
+
+重启并验证:
+
+```bash
+ENV_FILE=./.env.production ./start-prod.sh restart
+tail -100 /root/sqxp/backend/backend/logs/backend-prod.log
+```
+
+验收标准:
+- 出现 `Application startup complete`(每个 worker 都正常)
+- 不再出现 `Unable to load PEM file`
+- 不再出现“未配置 LOGIN_RSA_PRIVATE_KEY_PEM,已生成进程内临时密钥”
+
+#### D. 本次最终状态
+
+- `uvicorn` 多进程启动成功(`WORKERS=2`)
+- 后端监听 `0.0.0.0:8000`
+- API 加密启动日志正常(`API_CRYPTO_BOOT enabled=True ...`)
+
+## 7.2 Web Admin(`/root/sqxp/web`)
+
+本地打包后将 dist 全量上传到 `/root/sqxp/web`,Nginx 自动托管。
+
+如果前端有环境变量(例如 `VITE_BASE_URL` 或静态资源地址),生产打包前需改成生产域名:
+- API 基础地址:`https://gslyzn.com/api`
+- 静态资源基础地址:`https://gslyzn.com/sqxp-static/`
+
+### 7.2.1 本次生产打包与发布留痕(2026-04-30)
+
+生产环境变量(`web-admin/.env.production`)按本次部署口径配置为:
+
+```env
+VITE_BASE_URL=/
+VITE_API_URL=https://gslyzn.com/api
+```
+
+本地打包命令:
+
+```bash
+cd /Users/wuzilong/work/00code/sqxp_副本2/web-admin
+pnpm install
+pnpm build
+```
+
+产物目录:
+- `web-admin/dist`
+
+上传到生产服务器(覆盖式同步):
+
+```bash
+rsync -avz --delete /Users/wuzilong/work/00code/sqxp_副本2/web-admin/dist/ root@125.75.68.71:/root/sqxp/web/
+```
+
+发布后验收命令:
+
+```bash
+curl -I http://gslyzn.com/
+curl -I http://gslyzn.com/api/web/auth/login-rsa-public-key
+```
+
+验收说明(本次实测):
+- `http://gslyzn.com/api/web/auth/login-rsa-public-key` 返回 `200`,说明域名到 Nginx 到后端链路正常
+- 若根路径返回 `403`,优先检查 `/root/sqxp/web/index.html` 是否存在及目录权限
+
+## 7.3 静态资源目录(`/root/sqxp/assets`)
+
+将原测试资源地址:
+- `https://www.chengyantek.cn/sqxp-static/`
+
+切换为生产地址:
+- `https://gslyzn.com/sqxp-static/`
+
+---
+
+## 8. HTTPS 证书(建议)
+
+可用 `certbot` 申请证书(DNS 已解析到 `125.75.68.71`):
+
+```bash
+apt install -y certbot
+certbot certonly --standalone -d gslyzn.com -d www.gslyzn.com
+```
+
+将证书复制到 Nginx 挂载目录:
+
+```bash
+cp /etc/letsencrypt/live/gslyzn.com/fullchain.pem /root/soft/nginx/certs/
+cp /etc/letsencrypt/live/gslyzn.com/privkey.pem /root/soft/nginx/certs/
+```
+
+重启 Nginx:
+
+```bash
+cd /root/soft
+docker compose restart nginx
+```
+
+### 8.1 当前证书状态留痕(2026-04-30)
+
+当前服务器使用临时自签证书时,客户端验证会报错(符合预期):
+
+```bash
+curl -i https://gslyzn.com/api/web/auth/login-rsa-public-key
+```
+
+示例错误:
+
+```text
+curl: (60) SSL certificate problem: self signed certificate
+```
+
+临时验证可使用(仅测试):
+
+```bash
+curl -ik https://gslyzn.com/api/web/auth/login-rsa-public-key
+```
+
+正式上线前务必替换为受信任 CA 证书(`fullchain.pem`、`privkey.pem`),替换后重启 Nginx 即可生效。
+
+---
+
+## 9. 域名与 IP 配置说明(必做)
+
+生产信息:
+- 公网 IP:`125.75.68.71`
+- 域名:`gslyzn.com`
+- 建议同时启用:`www.gslyzn.com`
+
+### 9.1 DNS 解析配置(在域名服务商控制台)
+
+添加以下记录(若控制台支持 `@` 则可填写 `@`,若不支持则主机记录留空):
+
+| 主机记录 | 记录类型 | 记录值 | 说明 |
+|---|---|---|---|
+| `留空`(或 `@`) | `A` | `125.75.68.71` | 主域名 `gslyzn.com` |
+| `www` | `A` | `125.75.68.71` | 子域名 `www.gslyzn.com` |
+
+建议 TTL:`600`(10 分钟)或服务商默认值。
+
+### 9.1.1 天翼云平台配置步骤(域名与 IP 映射)
+
+以下步骤适用于在天翼云控制台完成 `gslyzn.com -> 125.75.68.71` 解析:
+
+1. 登录天翼云控制台,进入 **云解析 DNS**(不同账号可能显示为“域名解析”)。
+2. 找到你的域名 `gslyzn.com`,进入“解析记录管理”页面。
+3. 新增两条 `A` 记录:
+
+| 主机记录 | 记录类型 | 线路类型 | 记录值 | TTL |
+|---|---|---|---|---|
+| `留空`(不填写) | `A` | 默认 | `125.75.68.71` | `600` |
+| `www` | `A` | 默认 | `125.75.68.71` | `600` |
+
+4. 保存后等待生效(通常几分钟到十几分钟,受本地 DNS 缓存影响)。
+
+按天翼云弹窗的填写习惯,可直接按下面逐项填写:
+- 主机记录:`留空`(不要写任何字符),对应 `gslyzn.com`
+- 记录类型:`A`
+- 记录值:`125.75.68.71`
+- 线路类型:`默认`
+- TTL:`600`(或默认)
+
+再新增第二条:
+- 主机记录:`www`
+- 记录类型:`A`
+- 记录值:`125.75.68.71`
+- 线路类型:`默认`
+- TTL:`600`(或默认)
+
+注意事项:
+- 若域名不在天翼云托管,而在第三方注册商,需要先把 NS 改为天翼云分配的 DNS 服务器后再配置记录。
+- 若使用 CDN/WAF,请确认回源地址是该服务器 IP,且证书域名与回源策略一致。
+- 解析完成后还需在服务器放行 `80/443`,并确保 Nginx `server_name` 包含 `gslyzn.com www.gslyzn.com`。
+
+### 9.2 服务器侧 Nginx 绑定域名
+
+确保站点配置中 `server_name` 包含两个域名(文档第 5 节示例已配置):
+
+```nginx
+server_name gslyzn.com www.gslyzn.com;
+```
+
+更新后重载容器:
+
+```bash
+cd /root/soft
+docker compose restart nginx
+```
+
+### 9.3 生效检查命令
+
+在服务器或本地终端执行:
+
+```bash
+dig +short gslyzn.com
+dig +short www.gslyzn.com
+```
+
+期望都返回:
+
+```text
+125.75.68.71
+```
+
+再检查 HTTP/HTTPS:
+
+```bash
+curl -I http://gslyzn.com
+curl -I https://gslyzn.com
+curl -I https://www.gslyzn.com
+```
+
+### 9.4 本地临时测试(DNS 未生效时可选)
+
+如果 DNS 尚未生效,可在本机临时加 `hosts`:
+
+```bash
+sudo sh -c 'echo "125.75.68.71 gslyzn.com www.gslyzn.com" >> /etc/hosts'
+```
+
+测试完成后建议删除该行,避免后续误导。
+
+---
+
+## 10. 防火墙与端口放行
+
+建议仅公网开放:`80`、`443`、`20004`(若需要直连 MinIO 控制台)。
+`13306`、`16379`、`20003` 建议仅内网或安全组白名单开放。
+
+```bash
+ufw allow 22
+ufw allow 80
+ufw allow 443
+ufw allow 20004
+ufw enable
+ufw status
+```
+
+---
+
+## 11. 验收清单
+
+- `docker compose ps` 全部 `Up`
+- `mysql` 可通过 `13306` 登录,且 `lower_case_table_names=1`
+- `redis` 可通过 `16379` + 密码连接
+- `minio` 控制台可访问 `http://125.75.68.71:20004`(或通过 Nginx 二次代理后走域名)
+- 后端健康检查:`http://127.0.0.1:8000/health`
+- 前台页面可访问:`https://gslyzn.com/`
+- API 可访问:`https://gslyzn.com/api/...`
+- 静态资源可访问:`https://gslyzn.com/sqxp-static/...`
+
+---
+
+## 12. 常用运维命令
+
+```bash
+# 中间件
+cd /root/soft && docker compose ps
+cd /root/soft && docker compose restart
+cd /root/soft && docker compose logs -f nginx
+
+# backend
+cd /root/sqxp/backend && ./start-prod.sh status
+cd /root/sqxp/backend && ./start-prod.sh logs
+systemctl restart sqxp-backend
+```
+
+---
+
+## 13. 资源配置建议(2C8G)
+
+- MySQL InnoDB Buffer Pool:`2G`(已在配置中体现)
+- Redis Max Memory:`512MB`(已在配置中体现)
+- Uvicorn workers:`2`(与 CPU 核数一致)
+- 建议开启日志轮转(`logrotate`)防止磁盘被日志占满
+

Неке датотеке нису приказане због велике количине промена