zlwu 1 месяц назад
Родитель
Сommit
679285dcc6

+ 1 - 0
.gitignore

@@ -60,6 +60,7 @@ secrets/
 生产环境信息/*_nginx/
 生产环境信息/微信生产.env.snippet
 生产环境信息/微信配置-测试号备份.txt
+生产环境信息/zkxw-v110-backend.env*
 
 # 日志与临时文件
 *.log

+ 10 - 10
backend/.env.example

@@ -60,20 +60,20 @@ DATABASE_URL=mysql+pymysql://root:password@localhost:3306/zkxw
 # WXPAY_SANDBOX=false
 
 # 微信服务号(OA 审批/抄送模板消息;access_token 用服务号凭证,miniprogram.appid 仍用 WXPAY_MINI_APPID)
-# 总开关:模板未就绪时保持 false;配置好三套模板 ID 后再改为 true
-# WX_MP_NOTIFY_ENABLED=false
+# 总开关:配好三套模板 ID 后改为 true
+# WX_MP_NOTIFY_ENABLED=true
 # WX_MP_APPID=wx_service_account_appid
 # WX_MP_SECRET=wx_service_account_secret
-# WX_MP_TEMPLATE_APPROVAL=20rTii7gNFx5N998H4AlZe4AsQSV_62otPKti7pG7FI
-# WX_MP_TEMPLATE_CC=_BEaLeh2uDffEagwDJVEEmtzIYNglkd8hyJxCT5XIgw
-# WX_MP_TEMPLATE_APPLICANT_PROGRESS=_BEaLeh2uDffEagwDJVEEikOv8gnWu7aFA8cl0nX7S4
-# 审批「OA系统流程审批通知」:申请事项/申请人/当前环节/申请日期/流程名称
+# WX_MP_TEMPLATE_APPROVAL=ZKKnm3PrH97iKt4SDrONiw0TvpDyRbQXdmbF6wz-eak
+# WX_MP_TEMPLATE_CC=rHu7intrAqOqSlxmjqq7FfyC1GKbRi3XaP1-ZEcnvqs
+# WX_MP_TEMPLATE_APPLICANT_PROGRESS=rHu7intrAqOqSlxmjqq7Fb6ylcB3u3quKSBYUcR6NU4
+# 审批:申请事项/操作人/操作时间/当前环节/申请类型
 # WX_MP_TPL_FIELD_TITLE=thing12
-# WX_MP_TPL_FIELD_APPLICANT=thing21
+# WX_MP_TPL_FIELD_APPLICANT=thing18
 # WX_MP_TPL_FIELD_NODE=thing7
-# WX_MP_TPL_FIELD_TIME=time3
-# WX_MP_TPL_FIELD_PROCESS=thing48
-# 抄送「OA抄送通知」:业务名称/申请人/申请时间/流程名称
+# WX_MP_TPL_FIELD_TIME=time17
+# WX_MP_TPL_FIELD_PROCESS=thing8
+# 抄送:业务名称/申请人/申请时间/流程名称
 # WX_MP_TPL_CC_FIELD_TITLE=thing5
 # WX_MP_TPL_CC_FIELD_APPLICANT=thing2
 # WX_MP_TPL_CC_FIELD_TIME=time3

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

@@ -108,12 +108,13 @@ class Settings(BaseSettings):
     WX_MP_TEMPLATE_APPROVAL: str = ""
     WX_MP_TEMPLATE_CC: str = ""
     WX_MP_TEMPLATE_APPLICANT_PROGRESS: str = ""
-    # 审批模板「OA系统流程审批通知」:申请事项/申请人/当前环节/申请日期/流程名称
+    # 审批模板:申请事项/操作人(申请人)/当前环节/操作时间/申请类型(流程名)
+    # 生产字段:thing12 / thing18 / thing7 / time17 / thing8
     WX_MP_TPL_FIELD_TITLE: str = "thing12"
-    WX_MP_TPL_FIELD_APPLICANT: str = "thing21"
+    WX_MP_TPL_FIELD_APPLICANT: str = "thing18"
     WX_MP_TPL_FIELD_NODE: str = "thing7"
-    WX_MP_TPL_FIELD_TIME: str = "time3"
-    WX_MP_TPL_FIELD_PROCESS: str = "thing48"
+    WX_MP_TPL_FIELD_TIME: str = "time17"
+    WX_MP_TPL_FIELD_PROCESS: str = "thing8"
     # 抄送模板「OA抄送通知」:业务名称/申请人/申请时间/流程名称(无当前环节)
     WX_MP_TPL_CC_FIELD_TITLE: str = "thing5"
     WX_MP_TPL_CC_FIELD_APPLICANT: str = "thing2"

+ 2 - 2
backend/graphify-out/.graphify_extract.json

@@ -432,7 +432,7 @@
       "label": "Config",
       "file_type": "code",
       "source_file": "core/config.py",
-      "source_location": "L177"
+      "source_location": "L178"
     },
     {
       "id": "core_wechat_pay_py",
@@ -40070,7 +40070,7 @@
       "relation": "contains",
       "confidence": "EXTRACTED",
       "source_file": "core/config.py",
-      "source_location": "L177",
+      "source_location": "L178",
       "weight": 1.0
     },
     {

+ 2 - 2
backend/graphify-out/GRAPH_REPORT.md

@@ -1,7 +1,7 @@
-# Graph Report - app  (2026-09-14)
+# Graph Report - app  (2026-09-16)
 
 ## Corpus Check
-- Large corpus: 264 files · ~317,132 words. Semantic extraction will be expensive (many Claude tokens). Consider running on a subfolder, or use --no-semantic to run AST-only.
+- Large corpus: 264 files · ~317,142 words. Semantic extraction will be expensive (many Claude tokens). Consider running on a subfolder, or use --no-semantic to run AST-only.
 
 ## Summary
 - 5468 nodes · 30082 edges · 40 communities detected

+ 2 - 2
backend/graphify-out/graph.json

@@ -556,7 +556,7 @@
       "label": "Config",
       "file_type": "code",
       "source_file": "core/config.py",
-      "source_location": "L177",
+      "source_location": "L178",
       "id": "core_config_config",
       "community": 27,
       "norm_label": "config"
@@ -51129,7 +51129,7 @@
       "relation": "contains",
       "confidence": "EXTRACTED",
       "source_file": "core/config.py",
-      "source_location": "L177",
+      "source_location": "L178",
       "weight": 1.0,
       "_src": "core_config_py",
       "_tgt": "core_config_config",

+ 1 - 1
backend/graphify-out/manifest.json

@@ -5,7 +5,7 @@
   "/Users/wuzilong/work/00code/zkxw/backend/app/core/auth.py": 1786415793.4635684,
   "/Users/wuzilong/work/00code/zkxw/backend/app/core/api_crypto.py": 1778578258.039314,
   "/Users/wuzilong/work/00code/zkxw/backend/app/core/login_crypto.py": 1782186069.0708613,
-  "/Users/wuzilong/work/00code/zkxw/backend/app/core/config.py": 1789108015.3185847,
+  "/Users/wuzilong/work/00code/zkxw/backend/app/core/config.py": 1789545254.772123,
   "/Users/wuzilong/work/00code/zkxw/backend/app/core/wechat_pay.py": 1778578258.0461762,
   "/Users/wuzilong/work/00code/zkxw/backend/app/core/api_request_log_middleware.py": 1779882001.753267,
   "/Users/wuzilong/work/00code/zkxw/backend/app/core/avatar_url.py": 1780540753.5481932,

+ 33 - 11
deploy/docker/.env.example

@@ -73,17 +73,39 @@ API_ENCRYPT_FAIL_OPEN=false
 # 全量 API 请求日志(需 sys_api_request_log 表,否则可关闭)
 API_REQUEST_LOG_ENABLED=false
 
-# ---------- 微信服务号 OA 模板消息(模板未就绪时保持 false)----------
-WX_MP_NOTIFY_ENABLED=false
-# WX_MP_APPID=wxe576b24f873f47cb
-# WX_MP_SECRET=
-# WX_MP_TEMPLATE_APPROVAL=
-# WX_MP_TEMPLATE_CC=
-# WX_MP_TEMPLATE_APPLICANT_PROGRESS=
-
-# ---------- 微信支付 / 小程序凭证(登录、模板消息跳转;Secret 勿提交真实值)----------
+# ---------- 微信服务号 OA 模板消息 ----------
+# AppID / 模板 ID / 字段可写在本示例;Secret 勿提交 git(留空)
+# 发版打包:scripts/package-docker-deploy.sh 会从本机 deploy/docker/.env 注入
+# WX_MP_SECRET / WXPAY_SECRET / LOGIN_RSA 等到交付包 .env(有值才覆盖)
+WX_MP_NOTIFY_ENABLED=true
+WX_MP_APPID=wxe576b24f873f47cb
+# 必填:微信公众平台 → 服务号 → 开发 → 基本配置 → AppSecret(仅写在本机 .env)
+WX_MP_SECRET=
+WX_MP_TEMPLATE_APPROVAL=ZKKnm3PrH97iKt4SDrONiw0TvpDyRbQXdmbF6wz-eak
+WX_MP_TEMPLATE_CC=rHu7intrAqOqSlxmjqq7FfyC1GKbRi3XaP1-ZEcnvqs
+WX_MP_TEMPLATE_APPLICANT_PROGRESS=rHu7intrAqOqSlxmjqq7Fb6ylcB3u3quKSBYUcR6NU4
+# 审批字段(生产)
+WX_MP_TPL_FIELD_TITLE=thing12
+WX_MP_TPL_FIELD_APPLICANT=thing18
+WX_MP_TPL_FIELD_NODE=thing7
+WX_MP_TPL_FIELD_TIME=time17
+WX_MP_TPL_FIELD_PROCESS=thing8
+# 抄送字段
+WX_MP_TPL_CC_FIELD_TITLE=thing5
+WX_MP_TPL_CC_FIELD_APPLICANT=thing2
+WX_MP_TPL_CC_FIELD_TIME=time3
+WX_MP_TPL_CC_FIELD_PROCESS=thing1
+# 申请人进度字段
+WX_MP_TPL_PROGRESS_FIELD_BUSINESS=thing5
+WX_MP_TPL_PROGRESS_FIELD_PROCESS=thing1
+WX_MP_TPL_PROGRESS_FIELD_APPROVER=thing6
+WX_MP_TPL_PROGRESS_FIELD_RESULT=const4
+WX_MP_TPL_PROGRESS_FIELD_TIME=time11
+
+# ---------- 微信支付 / 小程序凭证(登录、模板消息跳转)----------
 # WXPAY_MCHID=
-# WXPAY_MINI_APPID=wx2b139581f4c855e4
-# WXPAY_SECRET=
+WXPAY_MINI_APPID=wx2b139581f4c855e4
+# 必填:小程序 AppSecret(仅写在本机 .env,打包时注入交付包)
+WXPAY_SECRET=
 # WXPAY_API_KEY=
 # WXPAY_NOTIFY_URL=https://your-domain.com/api/payment/notify

+ 6 - 6
deploy/docker/README.md

@@ -182,15 +182,15 @@ cd /opt/zkxw
 tar -xzf zkxw-docker-deploy-v1.1.0.tar.gz
 docker load -i zkxw-docker-deploy-v1.1.0/images/zkxw-app-images.tar
 
-# 2) 改现网 .env(保留原密码;追加/修改下列项)
+# 2) 改现网 .env
 cd /opt/zkxw/zkxw-docker-deploy-v1.0.0/deploy/docker
+# 只改镜像 tag(示例):
 # ZKXW_BACKEND_IMAGE=zkxw-backend:v1.1.0
 # ZKXW_WEB_IMAGE=zkxw-web:v1.1.0
-# WXPAY_MINI_APPID=...
-# WXPAY_SECRET=...
-# WX_MP_APPID=...
-# WX_MP_SECRET=...
-# WX_MP_NOTIFY_ENABLED=false
+#
+# 微信/小程序:新交付包 .env.example 打包时会从发版机注入;
+# - 推荐:保留现网 MYSQL/SECRET/MINIO 密码,把包内 WX_MP_* / WXPAY_* 段合并进来
+# - 切勿用「仅含 change-me 库口令」的全新 .env 整文件覆盖现网,否则连不上原数据库
 
 # 3) 只滚动应用(数据卷不动)
 docker compose up -d backend web

+ 11 - 1
mobile-oa-h5/src/oa/close-to-host.js

@@ -14,6 +14,7 @@
  *   showToast?: (msg: string) => void,
  *   fallbackMessage?: string,
  *   fallbackMiniPath?: string,
+ *   preferReLaunch?: boolean,
  *   schedule?: (fn: () => void, ms?: number) => void,
  *   silentFailMs?: number,
  *   toastDelayMs?: number,
@@ -55,6 +56,7 @@ export function closeToHost(payload, deps = {}) {
   const fallbackMiniPath =
     String(deps.fallbackMiniPath || '/pages/index/index').trim() ||
     '/pages/index/index'
+  const preferReLaunch = !!deps.preferReLaunch
   const schedule =
     deps.schedule ||
     ((fn, ms) => {
@@ -74,6 +76,7 @@ export function closeToHost(payload, deps = {}) {
     action: data && data.action,
     message: data && data.message,
     fallbackMiniPath,
+    preferReLaunch,
     toastDelayMs,
     silentFailMs,
     env
@@ -88,7 +91,8 @@ export function closeToHost(payload, deps = {}) {
       hasPostMessage: typeof mp.postMessage === 'function',
       hasReLaunch: typeof mp.reLaunch === 'function',
       hasRedirectTo: typeof mp.redirectTo === 'function',
-      hasNavigateTo: typeof mp.navigateTo === 'function'
+      hasNavigateTo: typeof mp.navigateTo === 'function',
+      preferReLaunch
     })
     if (data && data.message) toast(String(data.message))
     try {
@@ -164,6 +168,12 @@ export function closeToHost(payload, deps = {}) {
     }
 
     const doBack = () => {
+      // 服务号模板直达时栈深常为 1,navigateBack 无效;审批完成应直接 reLaunch 回列表
+      if (preferReLaunch) {
+        log('doBack preferReLaunch → failFallback')
+        failFallback('prefer-relaunch')
+        return
+      }
       log('doBack call navigateBack')
       try {
         mp.navigateBack({

+ 34 - 0
mobile-oa-h5/src/oa/close-to-host.test.js

@@ -72,6 +72,40 @@ import { closeToHost } from './close-to-host.js'
   assert.deepEqual(calls[3], ['reLaunch', '/pages/oa/pending/index'])
 }
 
+// preferReLaunch:跳过 navigateBack,直接 reLaunch(服务号模板入口栈深=1)
+{
+  const calls = []
+  const result = closeToHost(
+    { action: 'oa-detail-done', message: '已同意' },
+    {
+      wx: {
+        miniProgram: {
+          postMessage: () => calls.push('post'),
+          navigateBack: () => calls.push('back'),
+          reLaunch: (opts) => {
+            calls.push(['reLaunch', opts && opts.url])
+            if (opts && typeof opts.success === 'function') opts.success()
+          }
+        }
+      },
+      window: {
+        __wxjs_environment: 'miniprogram',
+        parent: {},
+        WeixinJSBridge: { invoke() {} }
+      },
+      showToast: (m) => calls.push(['toast', m]),
+      fallbackMiniPath: '/pages/oa/pending/index',
+      preferReLaunch: true,
+      schedule: (fn) => fn()
+    }
+  )
+  assert.equal(result, 'wx')
+  assert.equal(calls[0][0], 'toast')
+  assert.equal(calls[1], 'post')
+  assert.ok(!calls.includes('back'))
+  assert.deepEqual(calls[2], ['reLaunch', '/pages/oa/pending/index'])
+}
+
 // navigateBack 静默无回调 → 超时后 reLaunch(对齐 sqxp:reLaunch + redirectTo)
 {
   const calls = []

+ 52 - 9
mobile-oa-h5/src/views/OaDetail.vue

@@ -373,18 +373,58 @@ function resolveFallbackMiniPath() {
 function closeToMini(payload) {
   const data = payload || { action: 'oa-detail-close' }
   const fallbackMiniPath = resolveFallbackMiniPath()
+  const action = String(data.action || '')
+  // 审批完成/撤销:模板消息入口栈深常为 1,须 reLaunch;关闭按钮仍优先 navigateBack
+  const preferReLaunch =
+    action === 'oa-detail-done' || action === 'oa-detail-withdraw'
   console.log('[oa-detail] closeToMini', {
     action: data.action,
     message: data.message,
     fallbackMiniPath,
+    preferReLaunch,
     returnPath: route.query.returnPath || ''
   })
-  const mode = closeToHost(data, {
+  const leaveMode = closeToHost(data, {
     showToast: (msg) => showToast(msg),
     fallbackMessage: '已关闭(非小程序环境)',
-    fallbackMiniPath
+    fallbackMiniPath,
+    preferReLaunch
   })
-  console.log('[oa-detail] closeToHost →', mode)
+  console.log('[oa-detail] closeToHost →', leaveMode)
+}
+
+/**
+ * 操作成功后:静默刷新详情(关闭失败时仍能看到最新进度/隐藏同意按钮),再通知宿主离开。
+ * 服务号模板进小程序时尤为常见:webview 为栈底页,仅 toast「已同意」却停在旧界面。
+ */
+async function finishActionAndLeave(payload) {
+  const id = instanceId.value
+  if (id) {
+    try {
+      const d = await fetchOaInstanceDetail(id)
+      detail.value = d
+      try {
+        document.title = d?.title || d?.appName || '审批详情'
+      } catch (_) {
+        /* ignore */
+      }
+      const prepared = prepareMobileFormSchema(d?.formSchemaJson)
+      formRule.value = prepared.rule
+      formOption.value = {
+        ...prepared.option,
+        form: {
+          ...(prepared.option?.form || {}),
+          disabled: true
+        },
+        submitBtn: false,
+        resetBtn: false
+      }
+      formData.value = parseFormData(d)
+    } catch (_) {
+      /* ignore;离开逻辑仍继续 */
+    }
+  }
+  closeToMini(payload)
 }
 
 function onClose() {
@@ -436,7 +476,10 @@ async function withdraw() {
   acting.value = true
   try {
     await fetchWithdrawOaInstance(id)
-    closeToMini({ action: 'oa-detail-withdraw', message: '已撤销' })
+    await finishActionAndLeave({
+      action: 'oa-detail-withdraw',
+      message: '已撤销'
+    })
   } catch (e) {
     showToast(e?.message || '撤销失败')
   } finally {
@@ -520,15 +563,15 @@ async function submitComment() {
     if (kind === 'approve') {
       await fetchApproveOaTask(task.id, comment || undefined)
       commentVisible.value = false
-      closeToMini({ action: 'oa-detail-done', message: '已同意' })
+      await finishActionAndLeave({ action: 'oa-detail-done', message: '已同意' })
     } else if (kind === 'reject') {
       await fetchRejectOaTask(task.id, comment || undefined)
       commentVisible.value = false
-      closeToMini({ action: 'oa-detail-done', message: '已拒绝' })
+      await finishActionAndLeave({ action: 'oa-detail-done', message: '已拒绝' })
     } else if (kind === 'return') {
       await fetchReturnOaTask(task.id, comment)
       commentVisible.value = false
-      closeToMini({ action: 'oa-detail-done', message: '已打回' })
+      await finishActionAndLeave({ action: 'oa-detail-done', message: '已打回' })
     }
   } catch (e) {
     showToast(e?.message || '操作失败')
@@ -562,7 +605,7 @@ async function submitAddSign() {
       comment: c || undefined
     })
     addSignVisible.value = false
-    closeToMini({ action: 'oa-detail-done', message: '已加签' })
+    await finishActionAndLeave({ action: 'oa-detail-done', message: '已加签' })
   } catch (e) {
     showToast(e?.message || '加签失败')
   } finally {
@@ -593,7 +636,7 @@ async function submitTransfer() {
       comment: c || undefined
     })
     transferVisible.value = false
-    closeToMini({ action: 'oa-detail-done', message: '已转交' })
+    await finishActionAndLeave({ action: 'oa-detail-done', message: '已转交' })
   } catch (e) {
     showToast(e?.message || '转交失败')
   } finally {

+ 119 - 22
scripts/package-docker-deploy.sh

@@ -35,46 +35,140 @@ sed -e "s|^ZKXW_BACKEND_IMAGE=.*|ZKXW_BACKEND_IMAGE=${BACKEND_IMAGE}|" \
     -e "s|^ZKXW_WEB_IMAGE=.*|ZKXW_WEB_IMAGE=${WEB_IMAGE}|" \
     "${ROOT}/deploy/docker/.env.example" > "${STAGING}/deploy/docker/.env.example"
 
-# 若本地 deploy/docker/.env 已配置登录 RSA 私钥,注入到交付包 .env.example(避免多 worker 登录解密失败)
+# 从本地 deploy/docker/.env 注入运行时密钥与微信配置到交付包 .env.example
+# (不注入 MYSQL/SECRET_KEY/MINIO 密码,避免覆盖甲方现网库口令)
 LOCAL_ENV="${ROOT}/deploy/docker/.env"
+STAGING_ENV="${STAGING}/deploy/docker/.env.example"
 if [[ -f "${LOCAL_ENV}" ]]; then
-  STAGING_ENV="${STAGING}/deploy/docker/.env.example"
   if python3 - "${LOCAL_ENV}" "${STAGING_ENV}" <<'PY'
 import sys
 from pathlib import Path
 
 local_env, staging_env = map(Path, sys.argv[1:3])
-rsa_line = next(
-    (line for line in local_env.read_text().splitlines() if line.startswith("LOGIN_RSA_PRIVATE_KEY_PEM=")),
-    "",
+
+# 打包时注入:登录 RSA + 微信/小程序(业务通知依赖);不注入库口令
+INJECT_KEYS = (
+    "LOGIN_RSA_PRIVATE_KEY_PEM",
+    "WXPAY_MINI_APPID",
+    "WXPAY_SECRET",
+    "WX_MP_NOTIFY_ENABLED",
+    "WX_MP_APPID",
+    "WX_MP_SECRET",
+    "WX_MP_TEMPLATE_APPROVAL",
+    "WX_MP_TEMPLATE_CC",
+    "WX_MP_TEMPLATE_APPLICANT_PROGRESS",
+    "WX_MP_TPL_FIELD_TITLE",
+    "WX_MP_TPL_FIELD_APPLICANT",
+    "WX_MP_TPL_FIELD_NODE",
+    "WX_MP_TPL_FIELD_TIME",
+    "WX_MP_TPL_FIELD_PROCESS",
+    "WX_MP_TPL_CC_FIELD_TITLE",
+    "WX_MP_TPL_CC_FIELD_APPLICANT",
+    "WX_MP_TPL_CC_FIELD_TIME",
+    "WX_MP_TPL_CC_FIELD_PROCESS",
+    "WX_MP_TPL_PROGRESS_FIELD_BUSINESS",
+    "WX_MP_TPL_PROGRESS_FIELD_PROCESS",
+    "WX_MP_TPL_PROGRESS_FIELD_APPROVER",
+    "WX_MP_TPL_PROGRESS_FIELD_RESULT",
+    "WX_MP_TPL_PROGRESS_FIELD_TIME",
 )
-if not rsa_line:
-    sys.exit(2)
 
-lines: list[str] = []
+def parse_env(text: str) -> dict[str, str]:
+    out: dict[str, str] = {}
+    for line in text.splitlines():
+        s = line.strip()
+        if not s or s.startswith("#") or "=" not in line:
+            continue
+        # 保留行首 key=(允许值内含 =)
+        if line.lstrip().startswith("#"):
+            continue
+        key, _, val = line.partition("=")
+        key = key.strip()
+        if key:
+            out[key] = line  # 整行,保留原样(含引号/换行转义)
+    return out
+
+local_map = parse_env(local_env.read_text())
+injected: list[str] = []
+missing: list[str] = []
+for k in INJECT_KEYS:
+    if k in local_map and local_map[k].partition("=")[2].strip():
+        injected.append(k)
+    else:
+        missing.append(k)
+
+lines_out: list[str] = []
+seen: set[str] = set()
 for line in staging_env.read_text().splitlines():
-    if line.startswith("# LOGIN_RSA_PRIVATE_KEY_PEM=") or line.startswith("LOGIN_RSA_PRIVATE_KEY_PEM="):
+    raw = line.lstrip()
+    if raw.startswith("#"):
+        # 更新注释:标明微信/RSA 可能已由打包注入
+        if "登录 RSA" in line:
+            lines_out.append(
+                "# 登录 RSA(打包时若本地 deploy/docker/.env 有值则已注入)"
+            )
+            continue
+        if "微信服务号" in line or "微信支付 / 小程序" in line:
+            lines_out.append(line)
+            if "微信服务号" in line:
+                lines_out.append(
+                    "# 下列 WX_MP_* / 模板字段:打包时从本地 deploy/docker/.env 注入(有则覆盖)"
+                )
+            continue
+        lines_out.append(line)
         continue
-    if line.startswith("# 登录 RSA"):
-        lines.append(
-            "# 登录 RSA(多 worker/多实例须配置同一私钥;打包时已从 deploy/docker/.env 注入)"
-        )
+
+    key = line.partition("=")[0].strip() if "=" in line else ""
+    # 去掉注释形态的同名键,稍后写注入行
+    if key.startswith("#"):
+        maybe = key.lstrip("#").strip()
+        if maybe in INJECT_KEYS:
+            continue
+        lines_out.append(line)
         continue
-    if line.startswith("API_REQUEST_LOG_ENABLED="):
-        lines.append(rsa_line)
-    lines.append(line)
 
-staging_env.write_text("\n".join(lines) + "\n")
+    if key in INJECT_KEYS:
+        if key in local_map and local_map[key].partition("=")[2].strip():
+            lines_out.append(local_map[key])
+            seen.add(key)
+        else:
+            lines_out.append(line)
+            seen.add(key)
+        continue
+
+    lines_out.append(line)
+
+# 本地有、example 里没有的键:追加到文件末尾
+extra = [local_map[k] for k in INJECT_KEYS if k not in seen and k in local_map and local_map[k].partition("=")[2].strip()]
+if extra:
+    lines_out.append("")
+    lines_out.append("# ---- 打包自 deploy/docker/.env 追加 ----")
+    lines_out.extend(extra)
+
+staging_env.write_text("\n".join(lines_out) + "\n")
+
+# 同时生成可直接 cp 的 .env(与 .env.example 相同;甲方改库密码即可)
+staging_env.with_name(".env").write_text(staging_env.read_text())
+
+print("injected:" + ",".join(injected) if injected else "injected:(none)")
+if missing:
+    print("missing_or_empty:" + ",".join(missing))
+sys.exit(0 if injected else 2)
 PY
   then
-    echo "    已从 deploy/docker/.env 注入 LOGIN_RSA_PRIVATE_KEY_PEM"
+    echo "    已从 deploy/docker/.env 注入微信/小程序/RSA 等到交付包 .env.example 与 .env"
   elif [[ $? -eq 2 ]]; then
-    echo "    警告: deploy/docker/.env 未配置 LOGIN_RSA_PRIVATE_KEY_PEM,交付包内仍为占位;多 worker 下登录可能失败"
+    echo "    警告: deploy/docker/.env 中微信/RSA 等注入项为空,交付包仍用 .env.example 占位" >&2
+    cp "${STAGING}/deploy/docker/.env.example" "${STAGING}/deploy/docker/.env"
   else
-    echo "    错误: 注入 LOGIN_RSA_PRIVATE_KEY_PEM 失败" >&2
+    echo "    错误: 注入交付包环境变量失败" >&2
     exit 1
   fi
+else
+  echo "    警告: 无 deploy/docker/.env,跳过注入;仅提供 .env.example"
+  cp "${STAGING}/deploy/docker/.env.example" "${STAGING}/deploy/docker/.env"
 fi
+
 cp "${ROOT}/deploy/docker/README.md" "${STAGING}/deploy/docker/"
 cp -R "${ROOT}/deploy/docker/mysql" "${STAGING}/deploy/docker/"
 cp "${ROOT}/docs/sql/zkxw.sql" "${STAGING}/docs/sql/zkxw.sql"
@@ -90,6 +184,7 @@ platform=${DOCKER_PLATFORM}
 backend_image=${BACKEND_IMAGE}
 web_image=${WEB_IMAGE}
 web_includes=web-admin+mobile-oa-h5(/oa-h5/)
+env_note=微信/小程序/RSA 已从发版机 deploy/docker/.env 注入(若有);库口令仍须按甲方现网填写
 built_at=$(date -Iseconds)
 git_commit=$(git -C "${ROOT}" rev-parse --short HEAD 2>/dev/null || echo unknown)
 deploy_dir=deploy/docker
@@ -106,7 +201,7 @@ echo "总包大小: $(du -h "${ARCHIVE}" | cut -f1)"
 echo ""
 echo "包内不含前后端源码,包含:"
 echo "  - images/zkxw-app-images.tar(zkxw-backend + zkxw-web,web 含 /oa-h5/)"
-echo "  - deploy/docker/ (compose + .env.example + mysql init)"
+echo "  - deploy/docker/ (compose + .env.example + .env + mysql init)"
 echo "  - docs/sql/zkxw.sql"
 echo "  - 部署指导手册.html(客户安装手册,浏览器打开)"
 echo ""
@@ -114,5 +209,7 @@ echo "甲方服务器执行:"
 echo "  tar -xzf ${PKG_NAME}.tar.gz"
 echo "  cd ${PKG_NAME}"
 echo "  docker load -i images/zkxw-app-images.tar"
-echo "  cd deploy/docker && cp .env.example .env && vim .env"
+echo "  cd deploy/docker"
+echo "  # 新装: cp .env.example .env 后改 MYSQL/SECRET/MINIO 密码"
+echo "  # 升级: 保留现网库密码;可用包内已注入的微信配置,或合并 .env.example 中 WX_*/WXPAY_* 段"
 echo "  docker compose up -d"

Разница между файлами не показана из-за своего большого размера
+ 298 - 616
web-admin/graphify-out/.graphify_analysis.json


+ 0 - 16
web-admin/graphify-out/.graphify_extract.json

@@ -18179,13 +18179,6 @@
       "source_file": "views/system/user/index.vue",
       "source_location": "L474"
     },
-    {
-      "id": "user_index_return",
-      "label": "return()",
-      "file_type": "code",
-      "source_file": "views/system/user/index.vue",
-      "source_location": "L504"
-    },
     {
       "id": "views_system_user_detail_vue",
       "label": "detail.vue",
@@ -57769,15 +57762,6 @@
       "source_location": "L474",
       "weight": 1.0
     },
-    {
-      "source": "views_system_user_index_vue",
-      "target": "user_index_return",
-      "relation": "contains",
-      "confidence": "EXTRACTED",
-      "source_file": "views/system/user/index.vue",
-      "source_location": "L504",
-      "weight": 1.0
-    },
     {
       "source": "views_system_user_modules_user_dialog_vue",
       "target": "users_wuzilong_work_00code_zkxw_web_admin_src_api_admin",

+ 144 - 138
web-admin/graphify-out/GRAPH_REPORT.md

@@ -1,10 +1,10 @@
-# Graph Report - src  (2026-09-14)
+# Graph Report - src  (2026-09-17)
 
 ## Corpus Check
-- Large corpus: 1000 files · ~660,217 words. Semantic extraction will be expensive (many Claude tokens). Consider running on a subfolder, or use --no-semantic to run AST-only.
+- Large corpus: 1000 files · ~660,260 words. Semantic extraction will be expensive (many Claude tokens). Consider running on a subfolder, or use --no-semantic to run AST-only.
 
 ## Summary
-- 3602 nodes · 5137 edges · 69 communities detected
+- 3601 nodes · 5136 edges · 71 communities detected
 - Extraction: 78% EXTRACTED · 22% INFERRED · 0% AMBIGUOUS · INFERRED: 1134 edges (avg confidence: 0.8)
 - Token cost: 0 input · 0 output
 
@@ -51,33 +51,35 @@
 - [[_COMMUNITY_Community 39|Community 39]]
 - [[_COMMUNITY_Community 40|Community 40]]
 - [[_COMMUNITY_Community 41|Community 41]]
+- [[_COMMUNITY_Community 42|Community 42]]
 - [[_COMMUNITY_Community 43|Community 43]]
-- [[_COMMUNITY_Community 44|Community 44]]
 - [[_COMMUNITY_Community 45|Community 45]]
 - [[_COMMUNITY_Community 46|Community 46]]
+- [[_COMMUNITY_Community 47|Community 47]]
 - [[_COMMUNITY_Community 48|Community 48]]
+- [[_COMMUNITY_Community 49|Community 49]]
 - [[_COMMUNITY_Community 51|Community 51]]
 - [[_COMMUNITY_Community 52|Community 52]]
-- [[_COMMUNITY_Community 54|Community 54]]
 - [[_COMMUNITY_Community 55|Community 55]]
 - [[_COMMUNITY_Community 56|Community 56]]
-- [[_COMMUNITY_Community 57|Community 57]]
 - [[_COMMUNITY_Community 58|Community 58]]
 - [[_COMMUNITY_Community 59|Community 59]]
 - [[_COMMUNITY_Community 60|Community 60]]
+- [[_COMMUNITY_Community 61|Community 61]]
 - [[_COMMUNITY_Community 62|Community 62]]
+- [[_COMMUNITY_Community 63|Community 63]]
 - [[_COMMUNITY_Community 65|Community 65]]
-- [[_COMMUNITY_Community 67|Community 67]]
-- [[_COMMUNITY_Community 72|Community 72]]
+- [[_COMMUNITY_Community 68|Community 68]]
 - [[_COMMUNITY_Community 74|Community 74]]
-- [[_COMMUNITY_Community 75|Community 75]]
+- [[_COMMUNITY_Community 76|Community 76]]
 - [[_COMMUNITY_Community 77|Community 77]]
-- [[_COMMUNITY_Community 78|Community 78]]
 - [[_COMMUNITY_Community 79|Community 79]]
-- [[_COMMUNITY_Community 83|Community 83]]
-- [[_COMMUNITY_Community 96|Community 96]]
-- [[_COMMUNITY_Community 99|Community 99]]
-- [[_COMMUNITY_Community 100|Community 100]]
+- [[_COMMUNITY_Community 80|Community 80]]
+- [[_COMMUNITY_Community 81|Community 81]]
+- [[_COMMUNITY_Community 85|Community 85]]
+- [[_COMMUNITY_Community 98|Community 98]]
+- [[_COMMUNITY_Community 101|Community 101]]
+- [[_COMMUNITY_Community 102|Community 102]]
 
 ## God Nodes (most connected - your core abstractions)
 1. `get()` - 317 edges
@@ -98,80 +100,80 @@
   utils/oa-receive-account-value.ts → views/operations/storage-facility/index.vue
 - `dash()` --calls--> `formatDisplayOrDash()`  [INFERRED]
   components/business/infusion-schedule-outbound-chain-view/index.vue → utils/datetime-display.ts
-- `dash()` --calls--> `formatDisplayOrDash()`  [INFERRED]
-  components/business/clinical-schedule-payment-detail/index.vue → utils/datetime-display.ts
 - `displayCell()` --calls--> `formatDisplayOrDash()`  [INFERRED]
   views/production/waste-output/index.vue → utils/datetime-display.ts
+- `fmtMoney()` --calls--> `formatMoneyDisplay()`  [INFERRED]
+  components/business/clinical-schedule-payment-detail/index.vue → utils/money-format.ts
 
 ## Communities
 
 ### Community 0 - "Community 0"
-Cohesion: 0.0
-Nodes (205): fetchAccessLogList(), fetchActiveRegions(), fetchCategoryList(), fetchCategoryParameters(), fetchCellProductPriceList(), fetchCellProductSpecList(), fetchCellProductTypeList(), fetchChannelAccount() (+197 more)
+Cohesion: 0.01
+Nodes (183): fetchAccessLogList(), fetchActiveRegions(), fetchCategoryList(), fetchCategoryParameters(), fetchCellProductPriceList(), fetchCellProductSpecList(), fetchCellProductTypeList(), fetchChannelAccount() (+175 more)
 
 ### Community 1 - "Community 1"
-Cohesion: 0.03
-Nodes (90): fetchUploadAttachment(), handleReportUpload(), handleReportUpload(), walk(), if(), async(), addDispatchItem(), createDispatchItem() (+82 more)
+Cohesion: 0.02
+Nodes (86): fetchUploadAttachment(), handleReportUpload(), handleReportUpload(), walk(), if(), async(), addDispatchItem(), createDispatchItem() (+78 more)
 
 ### Community 2 - "Community 2"
 Cohesion: 0.03
-Nodes (87): batchSavePilotCheckup(), deletePilotCheckupActualItem(), fetchCheckupInstitutionList(), fetchCheckupInstitutionPackageDetail(), fetchCheckupInstitutionPackages(), fetchPilotCheckupDetail(), fetchPilotCheckupList(), markPilotCheckupChecked() (+79 more)
+Nodes (86): batchSavePilotCheckup(), deletePilotCheckupActualItem(), fetchCheckupInstitutionList(), fetchCheckupInstitutionPackageDetail(), fetchCheckupInstitutionPackages(), fetchPilotCheckupActualItems(), fetchPilotCheckupDetail(), fetchPilotCheckupList() (+78 more)
 
 ### Community 3 - "Community 3"
-Cohesion: 0.02
-Nodes (73): fetchCheckupInstitutionInvoices(), fetchCheckupInstitutionProjects(), fetchCreateCheckupInstitution(), fetchCreateCheckupInstitutionInvoice(), fetchCreateCheckupInstitutionProject(), fetchUpdateCheckupInstitution(), fetchUpdateCheckupInstitutionInvoice(), fetchUpdateCheckupInstitutionProject() (+65 more)
+Cohesion: 0.03
+Nodes (79): fetchEthnicList(), formatNowDatetime(), applyDonationBundleToForm(), createEmptyTransport(), ensureEthnicOptionInList(), handleOpenForAdd(), if(), loadEthnicOptions() (+71 more)
 
 ### Community 4 - "Community 4"
 Cohesion: 0.02
-Nodes (60): fetchProductionTaskList(), fetchProductionWasteOutputList(), fetchProductionReceptionBySampleCode(), fetchProductionReceptionSubmit(), fetchProductionReceptionUpdate(), handleDialogClose(), nodeSummary(), onAdd() (+52 more)
+Nodes (64): fetchBatchDeleteOaReceiveAccounts(), fetchBatchMarkOaCcTasksRead(), fetchCreateOaReceiveAccount(), fetchDeleteOaReceiveAccount(), fetchMarkOaCcTaskRead(), fetchOaAppDraft(), fetchOaCcUnreadCount(), fetchOaInstanceDetail() (+56 more)
 
 ### Community 5 - "Community 5"
-Cohesion: 0.02
-Nodes (64): fetchBatchDeleteOaReceiveAccounts(), fetchBatchMarkOaCcTasksRead(), fetchCreateOaReceiveAccount(), fetchDeleteOaReceiveAccount(), fetchMarkOaCcTaskRead(), fetchOaAppDraft(), fetchOaCcUnreadCount(), fetchOaInstanceDetail() (+56 more)
+Cohesion: 0.03
+Nodes (34): fetchGetMenuList(), ComponentLoader, IframeRouteManager, MenuProcessor, RoutePermissionValidator, RouteRegistry, RouteTransformer, useAppMode() (+26 more)
 
 ### Community 6 - "Community 6"
 Cohesion: 0.03
-Nodes (79): fetchEthnicList(), formatNowDatetime(), applyDonationBundleToForm(), createEmptyTransport(), ensureEthnicOptionInList(), handleOpenForAdd(), if(), loadEthnicOptions() (+71 more)
+Nodes (65): fetchCellSampleTypeList(), fetchCheckupInstitutionInvoices(), fetchCheckupInstitutionProjects(), fetchCreateCellSampleType(), fetchCreateCheckupInstitution(), fetchCreateCheckupInstitutionInvoice(), fetchCreateCheckupInstitutionProject(), fetchDeleteCellSampleType() (+57 more)
 
 ### Community 7 - "Community 7"
 Cohesion: 0.03
-Nodes (41): fetchGetMenuList(), useSettingsHandlers(), useSettingsPanel(), useSettingsState(), ComponentLoader, IframeRouteManager, MenuProcessor, RouteRegistry (+33 more)
+Nodes (62): useSettingsHandlers(), useSettingsPanel(), useSettingsState(), useCeremony(), useTheme(), if(), nodeSummary(), createDefaultFlow() (+54 more)
 
 ### Community 8 - "Community 8"
 Cohesion: 0.03
-Nodes (39): handleBlur(), handleFocus(), handleInput(), fetchQcMaterialBatchDetail(), fetchQcMaterialBatchList(), fetchQcMaterialBatchOptions(), fetchQcMaterialList(), fetchQcMaterialOptions() (+31 more)
+Nodes (56): fetchCreateCustomer(), fetchCustomerList(), fetchDeleteCustomer(), fetchUpdateCustomer(), updateInfusionScheduleStatus(), fetchProductionWasteOutputList(), handleDialogClose(), handleAdd() (+48 more)
 
 ### Community 9 - "Community 9"
 Cohesion: 0.03
-Nodes (58): fetchUpdatePostBoundRoles(), fetchOaRelatedSummaries(), RoutePermissionValidator, if(), branchConditionLines(), collectConditionReferencedFields(), collectFormConditionReferencedFields(), flattenFormRules() (+50 more)
+Nodes (69): fetchDeptTree(), fetchUserList(), fetchOaFlowPreview(), fetchOaTemplateDetail(), remoteSearchHandoverUsers(), chipTitle(), confirmPicker(), memberOptionLabel() (+61 more)
 
 ### Community 10 - "Community 10"
-Cohesion: 0.04
-Nodes (47): fetchCreateMenu(), fetchDeleteMenu(), fetchMenuTree(), fetchUpdateMenu(), collectIds(), buildInboundBasicSection(), buildInboundCellStorageViewSections(), buildInboundResearchDonationViewSections() (+39 more)
+Cohesion: 0.02
+Nodes (29): fetchAllConsumablePacks(), fetchAllConsumables(), fetchAllConsumableStockLogs(), fetchAllEquipments(), fetchAllOutputTypes(), fetchAllProcessCategories(), fetchAllProcesses(), fetchAllReagents() (+21 more)
 
 ### Community 11 - "Community 11"
-Cohesion: 0.04
-Nodes (46): buildBaseFields(), buildInboundBasicSection(), buildInboundCellStorageProductionViewSections(), buildInboundResearchDonationProductionViewSections(), buildOutboundBasicExperimentProductionViewSections(), buildOutboundClinicalProductionViewSections(), buildOutboundDetail(), buildOutboundPilotProductionViewSections() (+38 more)
+Cohesion: 0.03
+Nodes (56): fetchMyProductionTasks(), fetchMyQcTaskList(), fetchQcTaskDetail(), fetchQcTaskList(), fetchMineTasksForWorkOrders(), useInboundSiblingNavigation(), shouldApplyTriggerSourceFilter(), useQcWorkOrderTabPage() (+48 more)
 
 ### Community 12 - "Community 12"
-Cohesion: 0.03
-Nodes (49): fetchAttachmentDownloadUrl(), qcAttachmentFullUrl(), qcDownloadAttachment(), qcPreviewAttachment(), formatHandoverUserLabel(), formatNowDatetime(), fullFileUrl(), handleDownloadAttachment() (+41 more)
+Cohesion: 0.04
+Nodes (53): buildTemporaryViewSections(), buildInsertedSiblingCard(), resolveShowProtocolPaymentAtGroup(), appendTaskResultSections(), batchTasksForPanelSlot(), buildConfiguredOnlySection(), for(), set() (+45 more)
 
 ### Community 13 - "Community 13"
 Cohesion: 0.04
-Nodes (53): fetchMyProductionTasks(), fetchMyQcTaskList(), fetchQcTaskDetail(), fetchQcTaskList(), fetchMineTasksForWorkOrders(), useInboundSiblingNavigation(), shouldApplyTriggerSourceFilter(), useQcWorkOrderTabPage() (+45 more)
+Nodes (49): buildInboundBasicSection(), buildInboundCellStorageViewSections(), buildInboundResearchDonationViewSections(), buildReceptionSection(), buildTransportSection(), resolveInboundProtocolStatus(), buildBaseFields(), buildInboundBasicSection() (+41 more)
 
 ### Community 14 - "Community 14"
-Cohesion: 0.04
-Nodes (51): buildTemporaryViewSections(), buildInsertedSiblingCard(), resolveShowProtocolPaymentAtGroup(), batchTasksForPanelSlot(), for(), set(), gateWorkOrder(), sampleTabStatusLabel() (+43 more)
+Cohesion: 0.03
+Nodes (49): fetchAttachmentDownloadUrl(), qcAttachmentFullUrl(), qcDownloadAttachment(), qcPreviewAttachment(), formatHandoverUserLabel(), formatNowDatetime(), fullFileUrl(), handleDownloadAttachment() (+41 more)
 
 ### Community 15 - "Community 15"
-Cohesion: 0.05
-Nodes (60): fetchDeptTree(), fetchUserList(), fetchOaFlowPreview(), fetchOaTemplateDetail(), remoteSearchHandoverUsers(), flowNeedsInitiatorSelect(), loadFlowPreview(), loadUsers() (+52 more)
+Cohesion: 0.04
+Nodes (46): fetchProductionWorkOrderQcInfo(), dash(), fmtMoney(), auditorDisplay(), dash(), ensureUnitsInOptions(), expressName(), formatYm() (+38 more)
 
 ### Community 16 - "Community 16"
-Cohesion: 0.05
-Nodes (53): fetchGetMenuCodes(), fetchGetUserInfo(), fetchLogin(), fetchRefreshToken(), fetchUpdateProfile(), fetchUploadProfileAvatar(), toLogin(), fetchUserInfo() (+45 more)
+Cohesion: 0.04
+Nodes (39): fetchCreateMenu(), fetchDeleteMenu(), fetchMenuTree(), fetchUpdateMenu(), fetchUpdatePostBoundRoles(), fetchCreateEnvDetectItem(), fetchDeleteEnvDetectItem(), fetchEnvDetectItemDetail() (+31 more)
 
 ### Community 17 - "Community 17"
 Cohesion: 0.04
@@ -179,263 +181,267 @@ Nodes (44): fetchFullmoonTodoList(), fetchUpdateFullmoonTodoInsurance(), fetchCo
 
 ### Community 18 - "Community 18"
 Cohesion: 0.05
-Nodes (34): fetchClinicalPaymentDetail(), fetchClinicalPayments(), fetchClinicalReceiptConfirmed(), fetchClinicalReceiptPending(), fetchClinicalRefundConfirmed(), fetchClinicalRefundPending(), fetchOrderAdjusts(), fetchOrderDetail() (+26 more)
+Nodes (39): fetchCreateQcInspectionItemTemplate(), fetchDeleteQcInspectionItemTemplate(), fetchQcInspectionItemTemplateDetail(), fetchQcInspectionItemTemplateList(), fetchQcInspectionItemTemplatesActive(), fetchUpdateQcInspectionItemTemplate(), fetchQcLabTestItemList(), fetchQcLabTestItemsActive() (+31 more)
 
 ### Community 19 - "Community 19"
-Cohesion: 0.05
-Nodes (33): fetchMineProductionWorkOrderTabStepCounts(), fetchMineProductionWorkOrderTabStepCountsDeduped(), fetchProductionWorkOrderCellProductOptions(), fetchProductionWorkOrderDetail(), fetchProductionWorkOrderList(), fetchProductionWorkOrderQcDispatchOptions(), fetchProductionWorkOrderQcInfo(), fetchProductionWorkOrderTabStepCounts() (+25 more)
+Cohesion: 0.07
+Nodes (43): fetchOaRelatedSummaries(), buildFormRows(), buildOaPrintFilename(), buildOaPrintModel(), canvasToPdf(), ensurePlainObject(), exportOaApprovalPdf(), extractUrls() (+35 more)
 
 ### Community 20 - "Community 20"
 Cohesion: 0.05
-Nodes (39): fetchCreateQcInspectionItemTemplate(), fetchDeleteQcInspectionItemTemplate(), fetchQcInspectionItemTemplateDetail(), fetchQcInspectionItemTemplateList(), fetchQcInspectionItemTemplatesActive(), fetchUpdateQcInspectionItemTemplate(), fetchQcLabTestItemList(), fetchQcLabTestItemsActive() (+31 more)
+Nodes (24): handleBlur(), handleFocus(), handleInput(), fetchClinicalReceiptConfirmed(), buildConfirmedParams(), buildPendingParams(), formatAmount(), handleConfirmedReset() (+16 more)
 
 ### Community 21 - "Community 21"
 Cohesion: 0.06
-Nodes (39): fetchCreateCustomer(), fetchCustomerList(), fetchDeleteCustomer(), fetchUpdateCustomer(), updateInfusionScheduleStatus(), handleAdd(), handleDelete(), handleDropdownCommand() (+31 more)
+Nodes (40): completePilotApplication(), deletePilotApplication(), fetchPilotApplicationList(), startPilotApplication(), voidPilotApplication(), fetchClinicalRefundConfirmed(), buildConfirmedParams(), buildPendingParams() (+32 more)
 
 ### Community 22 - "Community 22"
 Cohesion: 0.08
 Nodes (37): formatDatetimeToMinute(), handleChineseDatetimePaste(), parseChineseDatetimeText(), parseBirthOrder(), parseChineseNumeral(), addSampleHint(), earliestCollectTime(), isMaternalBloodEmpty() (+29 more)
 
 ### Community 23 - "Community 23"
-Cohesion: 0.08
-Nodes (34): buildFormRows(), buildOaPrintFilename(), buildOaPrintModel(), canvasToPdf(), ensurePlainObject(), exportOaApprovalPdf(), extractUrls(), formatFieldValue() (+26 more)
+Cohesion: 0.06
+Nodes (22): fetchQcMaterialBatchDetail(), fetchQcMaterialBatchList(), fetchQcMaterialBatchOptions(), fetchQcMaterialList(), fetchQcMaterialOptions(), fetchUpdateQcMaterialBatch(), fetchMarkQcMaterialUsageFullyUsed(), financeAttachmentsToUrlString() (+14 more)
 
 ### Community 24 - "Community 24"
 Cohesion: 0.1
 Nodes (2): WebSocketClient, TableCache
 
 ### Community 25 - "Community 25"
-Cohesion: 0.09
-Nodes (8): getDefaultLanguage(), checkStorageCompatibility(), StorageConfig, getSystemStorage(), getSysVersion(), StorageKeyManager, StorageCompatibilityManager, validateStorageData()
+Cohesion: 0.08
+Nodes (33): fetchRefreshToken(), toLogin(), base64ToBytes(), bytesToBase64(), decryptJson(), deriveKey(), encryptJson(), fromBase64Url() (+25 more)
 
 ### Community 26 - "Community 26"
 Cohesion: 0.09
-Nodes (29): fetchAddTaskStep(), fetchCopyTaskStep(), fetchDeleteTaskStep(), fetchDispatchProductionTaskSteps(), fetchForwardProductionTask(), fetchProductionTaskDetail(), fetchProductionTaskEditorInit(), fetchReassignProductionTask() (+21 more)
+Nodes (8): getDefaultLanguage(), checkStorageCompatibility(), StorageConfig, getSystemStorage(), getSysVersion(), StorageKeyManager, StorageCompatibilityManager, validateStorageData()
 
 ### Community 27 - "Community 27"
-Cohesion: 0.07
-Nodes (19): fetchDeleteMaterialBatch(), fetchMaterialBatchList(), fetchMaterialBatchOptions(), fetchMaterialList(), fetchMaterialOptions(), fetchUpdateMaterialBatch(), fetchMaterialUsageList(), fetchMyMaterialUsageList() (+11 more)
+Cohesion: 0.09
+Nodes (29): fetchAddTaskStep(), fetchCopyTaskStep(), fetchDeleteTaskStep(), fetchDispatchProductionTaskSteps(), fetchForwardProductionTask(), fetchProductionTaskDetail(), fetchProductionTaskEditorInit(), fetchReassignProductionTask() (+21 more)
 
 ### Community 28 - "Community 28"
 Cohesion: 0.08
 Nodes (20): createEmptySnapshots(), normalizeConfiguredReagents(), normalizeConsumables(), normalizeDataCollections(), normalizeReagents(), normalizeStepOutputs(), serializeAttachments(), serializeEquipmentIds() (+12 more)
 
 ### Community 29 - "Community 29"
-Cohesion: 0.09
-Nodes (27): completePilotApplication(), deletePilotApplication(), fetchPilotApplicationList(), startPilotApplication(), voidPilotApplication(), handleBatchComplete(), handleBatchStart(), handleBatchVoid() (+19 more)
+Cohesion: 0.07
+Nodes (19): fetchDeleteMaterialBatch(), fetchMaterialBatchList(), fetchMaterialBatchOptions(), fetchMaterialList(), fetchMaterialOptions(), fetchUpdateMaterialBatch(), fetchMaterialUsageList(), fetchMyMaterialUsageList() (+11 more)
 
 ### Community 30 - "Community 30"
+Cohesion: 0.08
+Nodes (22): fetchMineProductionWorkOrderTabStepCounts(), fetchMineProductionWorkOrderTabStepCountsDeduped(), fetchProductionWorkOrderCellProductOptions(), fetchProductionWorkOrderDetail(), fetchProductionWorkOrderList(), fetchProductionWorkOrderQcDispatchOptions(), fetchProductionWorkOrderTabStepCounts(), fetchProductionWorkOrderTabStepCountsDeduped() (+14 more)
+
+### Community 31 - "Community 31"
+Cohesion: 0.09
+Nodes (23): fetchProductionTaskList(), fetchProductionReceptionBySampleCode(), fetchProductionReceptionSubmit(), fetchProductionReceptionUpdate(), onUserSelectVisible(), remoteSearchUsers(), buildPayload(), ensureDefaultHandler() (+15 more)
+
+### Community 32 - "Community 32"
 Cohesion: 0.09
 Nodes (22): fetchOaAppList(), fetchOaGroupList(), fetchPublishOaApp(), loadData(), loadGroups(), resetForm(), onSuccess(), goBack() (+14 more)
 
-### Community 31 - "Community 31"
+### Community 33 - "Community 33"
 Cohesion: 0.1
 Nodes (21): displayPeople(), nodeStatusText(), personLines(), personStatusHint(), personTitle(), showNodeComment(), signBadge(), signBadgeText() (+13 more)
 
-### Community 32 - "Community 32"
+### Community 34 - "Community 34"
+Cohesion: 0.12
+Nodes (20): fetchGetMenuCodes(), fetchGetUserInfo(), fetchLogin(), fetchUpdateProfile(), fetchUploadProfileAvatar(), fetchUserInfo(), buildWebEncryptedLoginBody(), defaultAvatarPathByRole() (+12 more)
+
+### Community 35 - "Community 35"
 Cohesion: 0.09
 Nodes (12): fetchMineQcWorkOrderTabUnfinishedCounts(), fetchOutboundQcDetail(), fetchQcWorkOrderDetail(), fetchQcWorkOrderList(), fetchQcWorkOrdersBySample(), fetchQcWorkOrderTabUnfinishedCounts(), reload(), reloadTabCounts() (+4 more)
 
-### Community 33 - "Community 33"
+### Community 36 - "Community 36"
+Cohesion: 0.1
+Nodes (12): fetchOutboundReleaseMaterial(), fetchOutboundReleaseReviewDetail(), fetchOutboundReleaseReviewList(), fetchReleaseReviewList(), handleReset(), handleSearch(), handleTabChange(), handleToggleSelect() (+4 more)
+
+### Community 37 - "Community 37"
 Cohesion: 0.12
 Nodes (15): addCopyButton(), highlightCode(), insertLineNumbers(), isBlockProcessed(), markBlockAsProcessed(), processBlock(), setupHighlightDirective(), setupRippleDirective() (+7 more)
 
-### Community 34 - "Community 34"
-Cohesion: 0.13
-Nodes (18): fetchCellSampleTypeList(), fetchCreateCellSampleType(), fetchDeleteCellSampleType(), fetchUpdateCellSampleType(), handleAdd(), handleDelete(), handleDropdownCommand(), handleEdit() (+10 more)
-
-### Community 35 - "Community 35"
+### Community 38 - "Community 38"
 Cohesion: 0.2
 Nodes (14): applyFlagToRule(), applySchema(), countRules(), getActiveRule(), getDesigner(), getSetupState(), onMessage(), readFlag() (+6 more)
 
-### Community 36 - "Community 36"
+### Community 39 - "Community 39"
 Cohesion: 0.12
 Nodes (6): setupOaConfirmationKit(), ensureOaKitMenu(), setupOaLeaveKit(), setupOaOvertimeKit(), setupOaResignationKit(), setupOaTripKit()
 
-### Community 37 - "Community 37"
+### Community 40 - "Community 40"
 Cohesion: 0.16
 Nodes (10): useTable(), useTableImpl(), getColumnChecks(), useTableColumns(), createErrorHandler(), createSmartDebounce(), defaultResponseAdapter(), extractPagination() (+2 more)
 
-### Community 38 - "Community 38"
+### Community 41 - "Community 41"
 Cohesion: 0.27
 Nodes (14): useChart(), useChartComponent(), useChartOps(), colourBlend(), getCssVar(), getDarkColor(), getLightColor(), handleElementThemeColor() (+6 more)
 
-### Community 39 - "Community 39"
-Cohesion: 0.33
-Nodes (13): buildProtocolPaymentFlowSteps(), dash(), financeReceiptTime(), formatAmount(), hasFinanceReceipt(), hasFinanceRefund(), hasReceiptSubmit(), hasRefundSubmit() (+5 more)
-
-### Community 40 - "Community 40"
+### Community 42 - "Community 42"
 Cohesion: 0.19
 Nodes (6): fetchServiceOrderTxnList(), handleReset(), handleSearch(), handleTabChange(), loadData(), switchTab()
 
-### Community 41 - "Community 41"
+### Community 43 - "Community 43"
 Cohesion: 0.24
 Nodes (10): buildPrintDocumentHtml(), cleanupPrintIframe(), embedSopImagesAsDataUrls(), getSopA4ContentHeightPx(), getSopA4EditorHeightPx(), normalizeSopCheckboxes(), prepareSopHtmlForPrint(), printSopHtml() (+2 more)
 
-### Community 43 - "Community 43"
+### Community 45 - "Community 45"
+Cohesion: 0.15
+Nodes (4): fetchNextDonationSampleCode(), fetchResearchDonationAuditList(), fetchResearchDonationDetail(), fetchResearchDonationList()
+
+### Community 46 - "Community 46"
+Cohesion: 0.33
+Nodes (7): buildSheetColumns(), downloadOaHistoryExcel(), exportTimestamp(), pad(), sanitizeFilename(), styleHeaderCell(), writeHeaderRows()
+
+### Community 47 - "Community 47"
 Cohesion: 0.31
 Nodes (4): isSlotAvailable(), resolveStorageSlotRangeFill(), slotFacilityNodeId(), sortBoxSlots()
 
-### Community 44 - "Community 44"
-Cohesion: 0.25
-Nodes (3): companyProgressTagType(), companyProgressText(), companyGroupProgress()
+### Community 48 - "Community 48"
+Cohesion: 0.28
+Nodes (1): FireworkSystem
 
-### Community 45 - "Community 45"
+### Community 49 - "Community 49"
 Cohesion: 0.25
 Nodes (2): handleTouchEnd(), setTransition()
 
-### Community 46 - "Community 46"
+### Community 51 - "Community 51"
 Cohesion: 0.28
-Nodes (1): FireworkSystem
+Nodes (4): fetchMyProductionMaterialConsumptions(), handleReset(), handleSearch(), loadData()
 
-### Community 48 - "Community 48"
+### Community 52 - "Community 52"
 Cohesion: 0.28
 Nodes (4): isActiveHistoryInstance(), isUploadPreviewLabel(), previewColumnMinWidth(), syncWorktabTitle()
 
-### Community 51 - "Community 51"
+### Community 55 - "Community 55"
 Cohesion: 0.39
 Nodes (6): isIgnorableScriptError(), normalizeErrorMessage(), registerPromiseErrorHandler(), registerResourceErrorHandler(), scriptErrorHandler(), setupErrorHandle()
 
-### Community 52 - "Community 52"
+### Community 56 - "Community 56"
 Cohesion: 0.36
 Nodes (1): TableHeightCalculator
 
-### Community 54 - "Community 54"
+### Community 58 - "Community 58"
 Cohesion: 0.33
 Nodes (2): logApiCryptoBootInfo(), maskSecret()
 
-### Community 55 - "Community 55"
+### Community 59 - "Community 59"
 Cohesion: 0.38
 Nodes (4): handleFocus(), handleInput(), parseDecimalToNumber(), sanitizeDecimalInput()
 
-### Community 56 - "Community 56"
-Cohesion: 0.29
-Nodes (2): fetchStorageReleaseReviewDetail(), fetchStorageReleaseReviewList()
-
-### Community 57 - "Community 57"
+### Community 60 - "Community 60"
 Cohesion: 0.4
 Nodes (2): isUnfinishedQcWorkOrder(), resolveQcWorkOrderUnfinishedBadgeCount()
 
-### Community 58 - "Community 58"
+### Community 61 - "Community 61"
 Cohesion: 0.47
 Nodes (3): isPaymentDueWithinWarnWindow(), parsePaymentDueYmd(), startOfLocalDay()
 
-### Community 59 - "Community 59"
+### Community 62 - "Community 62"
 Cohesion: 0.5
 Nodes (2): formatProtocolDocDeleteConfirm(), formatProtocolDocSubject()
 
-### Community 60 - "Community 60"
+### Community 63 - "Community 63"
 Cohesion: 0.5
 Nodes (2): parseEquipmentIds(), syncEquipmentNamesFromOptions()
 
-### Community 62 - "Community 62"
+### Community 65 - "Community 65"
 Cohesion: 0.5
 Nodes (2): focusInput(), handleKeydown()
 
-### Community 65 - "Community 65"
+### Community 68 - "Community 68"
 Cohesion: 0.67
 Nodes (2): if(), logSidebarMenu()
 
-### Community 67 - "Community 67"
-Cohesion: 0.5
-Nodes (1): fetchQcSmsList()
-
-### Community 72 - "Community 72"
+### Community 74 - "Community 74"
 Cohesion: 1.0
 Nodes (2): getSettingDefaults(), resetToDefaults()
 
-### Community 74 - "Community 74"
+### Community 76 - "Community 76"
 Cohesion: 1.0
 Nodes (2): buildOvertimeKitChildren(), uniqueId()
 
-### Community 75 - "Community 75"
+### Community 77 - "Community 77"
 Cohesion: 1.0
 Nodes (2): buildLeaveKitChildren(), uniqueId()
 
-### Community 77 - "Community 77"
+### Community 79 - "Community 79"
 Cohesion: 1.0
 Nodes (2): buildConfirmationKitChildren(), uniqueId()
 
-### Community 78 - "Community 78"
+### Community 80 - "Community 80"
 Cohesion: 1.0
 Nodes (2): buildResignationKitChildren(), uniqueId()
 
-### Community 79 - "Community 79"
+### Community 81 - "Community 81"
 Cohesion: 1.0
 Nodes (2): copyTextToClipboard(), copyTextWithFallback()
 
-### Community 83 - "Community 83"
+### Community 85 - "Community 85"
 Cohesion: 1.0
 Nodes (2): getSourceProtocolAttachment(), normalizeAttachmentFiles()
 
-### Community 96 - "Community 96"
+### Community 98 - "Community 98"
 Cohesion: 1.0
 Nodes (2): emptyTabCounts(), useProductionWorkOrderTabStepCounts()
 
-### Community 99 - "Community 99"
+### Community 101 - "Community 101"
 Cohesion: 1.0
 Nodes (2): initialProgress(), useQcWorkOrderAttachmentExport()
 
-### Community 100 - "Community 100"
+### Community 102 - "Community 102"
 Cohesion: 1.0
 Nodes (2): emptyTabCounts(), useQcWorkOrderTabUnfinishedCounts()
 
 ## Knowledge Gaps
 - **Thin community `Community 24`** (42 nodes): `WebSocketClient`, `.calculateReconnectDelay()`, `.clearAllTimers()`, `.clearConnectionTimers()`, `.clearTimer()`, `.close()`, `.closeCurrentSocketForReconnect()`, `.connect()`, `.connectionStatusText()`, `.constructor()`, `.destroyInstance()`, `.flushMessageQueue()`, `.getInstance()`, `.getReadyStateText()`, `.handleClose()`, `.handleConnectionTimeout()`, `.handleError()`, `.handleMessage()`, `.handleOpen()`, `.init()`, `.isWebSocketConnected()`, `.reconnect()`, `.resetHeartbeat()`, `.send()`, `.startHeartbeat()`, `.startPing()`, `TableCache`, `.cleanupExpired()`, `.clear()`, `.clearByTags()`, `.clearCurrentSearch()`, `.clearPagination()`, `.constructor()`, `.evictLRU()`, `.generateKey()`, `.generateTags()`, `.get()`, `.getStats()`, `.log()`, `.set()`, `index.ts`, `tableCache.ts`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 45`** (9 nodes): `cleanupEventListeners()`, `handleTouchEnd()`, `handleTouchMove()`, `handleTouchStart()`, `handleWheelScroll()`, `if()`, `setTransition()`, `setupEventListeners()`, `index.vue`
-  Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 46`** (9 nodes): `FireworkSystem`, `.constructor()`, `.createFirework()`, `.createParticle()`, `.getAvailableParticle()`, `.initializePool()`, `.preloadAllImages()`, `.preloadImage()`, `index.vue`
-  Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 52`** (8 nodes): `TableHeightCalculator`, `.calculate()`, `.calculateOffset()`, `.calculatePaginationOffset()`, `.constructor()`, `.getHeaderHeight()`, `useTableHeight()`, `useTableHeight.ts`
+- **Thin community `Community 48`** (9 nodes): `FireworkSystem`, `.constructor()`, `.createFirework()`, `.createParticle()`, `.getAvailableParticle()`, `.initializePool()`, `.preloadAllImages()`, `.preloadImage()`, `index.vue`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 54`** (7 nodes): `App.vue`, `main.ts`, `refreshButtonAutoIcons()`, `refreshPanelTitleIcons()`, `logApiCryptoBootInfo()`, `maskSecret()`, `console.ts`
+- **Thin community `Community 49`** (9 nodes): `cleanupEventListeners()`, `handleTouchEnd()`, `handleTouchMove()`, `handleTouchStart()`, `handleWheelScroll()`, `if()`, `setTransition()`, `setupEventListeners()`, `index.vue`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 56`** (7 nodes): `batchReviewStorageRelease()`, `batchStoreSample()`, `fetchStorageReleaseReviewDetail()`, `fetchStorageReleaseReviewList()`, `storeSample()`, `submitStorageReleaseReview()`, `storage-release-review.ts`
+- **Thin community `Community 56`** (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 57`** (6 nodes): `formatQcWorkOrderUnfinishedBadgeCount()`, `isUnfinishedQcWorkOrder()`, `qcWorkOrderUnfinishedBadgeTitle()`, `resolveQcWorkOrderUnfinishedBadgeCount()`, `sumQcWorkOrderGroupUnfinishedBadgeCount()`, `qc-work-order-unfinished-badge.ts`
+- **Thin community `Community 58`** (7 nodes): `App.vue`, `main.ts`, `refreshButtonAutoIcons()`, `refreshPanelTitleIcons()`, `logApiCryptoBootInfo()`, `maskSecret()`, `console.ts`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 59`** (5 nodes): `formatProtocolDocBatchDeleteConfirm()`, `formatProtocolDocDeleteConfirm()`, `formatProtocolDocSubject()`, `getProtocolDocTypeLabel()`, `protocol-doc-label.ts`
+- **Thin community `Community 60`** (6 nodes): `formatQcWorkOrderUnfinishedBadgeCount()`, `isUnfinishedQcWorkOrder()`, `qcWorkOrderUnfinishedBadgeTitle()`, `resolveQcWorkOrderUnfinishedBadgeCount()`, `sumQcWorkOrderGroupUnfinishedBadgeCount()`, `qc-work-order-unfinished-badge.ts`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 60`** (5 nodes): `equipmentOptionLabel()`, `joinEquipmentIds()`, `parseEquipmentIds()`, `syncEquipmentNamesFromOptions()`, `equipment-select.ts`
+- **Thin community `Community 62`** (5 nodes): `formatProtocolDocBatchDeleteConfirm()`, `formatProtocolDocDeleteConfirm()`, `formatProtocolDocSubject()`, `getProtocolDocTypeLabel()`, `protocol-doc-label.ts`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 62`** (5 nodes): `focusInput()`, `handleKeydown()`, `if()`, `search()`, `index.vue`
+- **Thin community `Community 63`** (5 nodes): `equipmentOptionLabel()`, `joinEquipmentIds()`, `parseEquipmentIds()`, `syncEquipmentNamesFromOptions()`, `equipment-select.ts`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 65`** (4 nodes): `if()`, `logSidebarMenu()`, `index.vue`, `SidebarSubmenu.vue`
+- **Thin community `Community 65`** (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 67`** (4 nodes): `fetchGenerateQcSms()`, `fetchQcSmsList()`, `fetchToggleQcSmsNotified()`, `qc-sms-notification.ts`
+- **Thin community `Community 68`** (4 nodes): `if()`, `logSidebarMenu()`, `index.vue`, `SidebarSubmenu.vue`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 72`** (3 nodes): `getSettingDefaults()`, `resetToDefaults()`, `setting.ts`
+- **Thin community `Community 74`** (3 nodes): `getSettingDefaults()`, `resetToDefaults()`, `setting.ts`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 74`** (3 nodes): `buildOvertimeKitChildren()`, `oa-overtime-kit-rule.ts`, `uniqueId()`
+- **Thin community `Community 76`** (3 nodes): `buildOvertimeKitChildren()`, `oa-overtime-kit-rule.ts`, `uniqueId()`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 75`** (3 nodes): `buildLeaveKitChildren()`, `oa-leave-kit-rule.ts`, `uniqueId()`
+- **Thin community `Community 77`** (3 nodes): `buildLeaveKitChildren()`, `oa-leave-kit-rule.ts`, `uniqueId()`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 77`** (3 nodes): `buildConfirmationKitChildren()`, `oa-confirmation-kit-rule.ts`, `uniqueId()`
+- **Thin community `Community 79`** (3 nodes): `buildConfirmationKitChildren()`, `oa-confirmation-kit-rule.ts`, `uniqueId()`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 78`** (3 nodes): `buildResignationKitChildren()`, `oa-resignation-kit-rule.ts`, `uniqueId()`
+- **Thin community `Community 80`** (3 nodes): `buildResignationKitChildren()`, `oa-resignation-kit-rule.ts`, `uniqueId()`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 79`** (3 nodes): `copyTextToClipboard()`, `copyTextWithFallback()`, `copy-text.ts`
+- **Thin community `Community 81`** (3 nodes): `copyTextToClipboard()`, `copyTextWithFallback()`, `copy-text.ts`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 83`** (3 nodes): `getSourceProtocolAttachment()`, `normalizeAttachmentFiles()`, `protocol-source-attachment.ts`
+- **Thin community `Community 85`** (3 nodes): `getSourceProtocolAttachment()`, `normalizeAttachmentFiles()`, `protocol-source-attachment.ts`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 96`** (3 nodes): `emptyTabCounts()`, `useProductionWorkOrderTabStepCounts()`, `useProductionWorkOrderTabStepCounts.ts`
+- **Thin community `Community 98`** (3 nodes): `emptyTabCounts()`, `useProductionWorkOrderTabStepCounts()`, `useProductionWorkOrderTabStepCounts.ts`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 99`** (3 nodes): `initialProgress()`, `useQcWorkOrderAttachmentExport()`, `useQcWorkOrderAttachmentExport.ts`
+- **Thin community `Community 101`** (3 nodes): `initialProgress()`, `useQcWorkOrderAttachmentExport()`, `useQcWorkOrderAttachmentExport.ts`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 100`** (3 nodes): `emptyTabCounts()`, `useQcWorkOrderTabUnfinishedCounts()`, `useQcWorkOrderTabUnfinishedCounts.ts`
+- **Thin community `Community 102`** (3 nodes): `emptyTabCounts()`, `useQcWorkOrderTabUnfinishedCounts()`, `useQcWorkOrderTabUnfinishedCounts.ts`
   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 1` to `Community 0`, `Community 2`, `Community 3`, `Community 4`, `Community 5`, `Community 6`, `Community 9`, `Community 10`, `Community 11`, `Community 12`, `Community 13`, `Community 14`, `Community 15`, `Community 16`, `Community 17`, `Community 19`, `Community 20`, `Community 22`, `Community 23`, `Community 24`, `Community 26`, `Community 28`, `Community 29`, `Community 30`, `Community 31`, `Community 32`, `Community 46`?**
-  _High betweenness centrality (0.339) - this node is a cross-community bridge._
-- **Why does `get()` connect `Community 0` to `Community 1`, `Community 2`, `Community 3`, `Community 4`, `Community 5`, `Community 6`, `Community 8`, `Community 9`, `Community 10`, `Community 13`, `Community 14`, `Community 15`, `Community 16`, `Community 17`, `Community 18`, `Community 19`, `Community 20`, `Community 21`, `Community 26`, `Community 27`, `Community 29`, `Community 30`, `Community 32`, `Community 34`, `Community 40`, `Community 43`, `Community 56`, `Community 67`?**
+- **Why does `push()` connect `Community 1` to `Community 0`, `Community 2`, `Community 3`, `Community 4`, `Community 6`, `Community 7`, `Community 9`, `Community 11`, `Community 12`, `Community 13`, `Community 14`, `Community 15`, `Community 16`, `Community 17`, `Community 18`, `Community 19`, `Community 21`, `Community 22`, `Community 24`, `Community 25`, `Community 27`, `Community 28`, `Community 30`, `Community 32`, `Community 33`, `Community 35`, `Community 46`, `Community 48`?**
+  _High betweenness centrality (0.324) - this node is a cross-community bridge._
+- **Why does `get()` connect `Community 0` to `Community 1`, `Community 2`, `Community 3`, `Community 4`, `Community 6`, `Community 7`, `Community 8`, `Community 9`, `Community 10`, `Community 11`, `Community 12`, `Community 15`, `Community 16`, `Community 17`, `Community 18`, `Community 19`, `Community 20`, `Community 21`, `Community 23`, `Community 27`, `Community 29`, `Community 30`, `Community 31`, `Community 32`, `Community 34`, `Community 35`, `Community 36`, `Community 42`, `Community 45`, `Community 47`, `Community 51`?**
   _High betweenness centrality (0.239) - this node is a cross-community bridge._
-- **Why does `handleDynamicRoutes()` connect `Community 7` to `Community 32`, `Community 5`, `Community 9`, `Community 11`, `Community 16`, `Community 19`, `Community 23`?**
-  _High betweenness centrality (0.039) - this node is a cross-community bridge._
+- **Why does `handleDynamicRoutes()` connect `Community 5` to `Community 34`, `Community 35`, `Community 4`, `Community 13`, `Community 19`, `Community 30`?**
+  _High betweenness centrality (0.033) - this node is a cross-community bridge._
 - **Are the 316 inferred relationships involving `get()` (e.g. with `resolveStorageSlotRangeFill()` and `buildNodeCodePathFromFlat()`) actually correct?**
   _`get()` has 316 INFERRED edges - model-reasoned connections that need verification._
 - **Are the 127 inferred relationships involving `push()` (e.g. with `refreshQcMenuBadges()` and `collectLeafFacilityNodesInTreeOrder()`) actually correct?**

Разница между файлами не показана из-за своего большого размера
+ 1 - 1
web-admin/graphify-out/graph.html


+ 3 - 3
web-admin/graphify-out/manifest.json

@@ -17,8 +17,8 @@
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/types/common/index.ts": 1781272556.3597305,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/types/directive/directive.d.ts": 1781272556.3601494,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/types/api/api.d.ts": 1781272556.3606477,
-  "/Users/wuzilong/work/00code/zkxw/web-admin/src/types/import/components.d.ts": 1789362706.3178787,
-  "/Users/wuzilong/work/00code/zkxw/web-admin/src/types/import/auto-imports.d.ts": 1789362686.8080792,
+  "/Users/wuzilong/work/00code/zkxw/web-admin/src/types/import/components.d.ts": 1789611740.8245428,
+  "/Users/wuzilong/work/00code/zkxw/web-admin/src/types/import/auto-imports.d.ts": 1789611699.652026,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/types/store/index.ts": 1787887312.735774,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/types/router/index.ts": 1787887320.8486114,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/composables/useAttachmentActions.ts": 1788162977.6691713,
@@ -487,7 +487,7 @@
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/system/post/index.vue": 1785912180.839225,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/system/category/index.vue": 1783326448.9218316,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/system/express/index.vue": 1783326448.9225786,
-  "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/system/user/index.vue": 1785912511.2949293,
+  "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/system/user/index.vue": 1789612396.872853,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/system/user/detail.vue": 1782893853.853974,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/system/user/modules/user-dialog.vue": 1782893853.9540384,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/system/user/modules/user-search.vue": 1782893853.8827589,

+ 21 - 8
web-admin/src/views/system/user/index.vue

@@ -612,6 +612,26 @@
     }
   }
 
+  /** 编辑回填:按用户 ID 拉取主管详情写入选项(keyword 检索不含 id,不能用 UUID 搜) */
+  async function hydrateSupervisorOption(userId: string) {
+    const uid = (userId || '').trim()
+    if (!uid) {
+      supervisorUserOptions.value = []
+      return
+    }
+    if (supervisorUserOptions.value.some((u) => u.id === uid)) return
+    supervisorUserLoading.value = true
+    try {
+      const detail = await fetchUserDetail(uid)
+      supervisorUserOptions.value = [detail]
+    } catch (e) {
+      console.error('加载直属主管失败:', e)
+      supervisorUserOptions.value = []
+    } finally {
+      supervisorUserLoading.value = false
+    }
+  }
+
   const formRules = {
     userNo: [{ required: true, message: '请输入用户账号', trigger: 'blur' }],
     nickName: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
@@ -731,14 +751,7 @@
       formData.orderNum = detail.orderNum ?? 0
       formData.remark = detail.remark || ''
       formData.postIds = detail.postIds ? [...detail.postIds] : []
-      if (formData.supervisorUserId) {
-        await remoteSearchSupervisorUsers(detail.nickName || '')
-        if (!supervisorUserOptions.value.some((u) => u.id === formData.supervisorUserId)) {
-          await remoteSearchSupervisorUsers(formData.supervisorUserId)
-        }
-      } else {
-        supervisorUserOptions.value = []
-      }
+      await hydrateSupervisorOption(formData.supervisorUserId)
       dialogVisible.value = true
       formRef.value?.clearValidate?.()
     } catch (e) {

Некоторые файлы не были показаны из-за большого количества измененных файлов