Bladeren bron

chore: document wx-qr phone login and refresh graphify

Co-authored-by: Cursor <cursoragent@cursor.com>
zlwu 1 maand geleden
bovenliggende
commit
66e442e1e8

File diff suppressed because it is too large
+ 259 - 263
backend/graphify-out/.graphify_analysis.json


File diff suppressed because it is too large
+ 151 - 144
backend/graphify-out/.graphify_extract.json


+ 80 - 82
backend/graphify-out/GRAPH_REPORT.md

@@ -1,11 +1,11 @@
-# Graph Report - app  (2026-09-16)
+# Graph Report - app  (2026-09-18)
 
 ## Corpus Check
-- 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.
+- Large corpus: 265 files · ~319,283 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
-- Extraction: 31% EXTRACTED · 69% INFERRED · 0% AMBIGUOUS · INFERRED: 20619 edges (avg confidence: 0.6)
+- 5508 nodes · 30303 edges · 40 communities detected
+- Extraction: 31% EXTRACTED · 69% INFERRED · 0% AMBIGUOUS · INFERRED: 20768 edges (avg confidence: 0.6)
 - Token cost: 0 input · 0 output
 
 ## Community Hubs (Navigation)
@@ -51,22 +51,22 @@
 - [[_COMMUNITY_Community 41|Community 41]]
 
 ## God Nodes (most connected - your core abstractions)
-1. `success_response()` - 762 edges
-2. `error_response()` - 632 edges
-3. `SysUser` - 606 edges
-4. `BizQcTaskResult` - 352 edges
-5. `BizProductionMaterial` - 336 edges
-6. `BizProductionMaterialUsage` - 329 edges
-7. `BizQcMaterialUsage` - 327 edges
-8. `BizQcMaterial` - 317 edges
-9. `BizProductionMaterialStockBatch` - 316 edges
-10. `BizMaterialUsage` - 313 edges
+1. `success_response()` - 770 edges
+2. `error_response()` - 639 edges
+3. `SysUser` - 612 edges
+4. `BizQcTaskResult` - 354 edges
+5. `BizProductionMaterial` - 338 edges
+6. `BizProductionMaterialUsage` - 331 edges
+7. `BizQcMaterialUsage` - 329 edges
+8. `BizQcMaterial` - 319 edges
+9. `BizProductionMaterialStockBatch` - 318 edges
+10. `BizMaterialUsage` - 315 edges
 
 ## Surprising Connections (you probably didn't know these)
 - `统一记录 API 请求/响应出入参的中间件` --uses--> `SysApiRequestLog`  [INFERRED]
   core/api_request_log_middleware.py → models/models.py
-- `get_menu_list()` --calls--> `success_response()`  [INFERRED]
-  api/menu.py → core/response.py
+- `sync_lab_storage_seq_after_use()` --calls--> `generate_uuid()`  [INFERRED]
+  crud/sample_reception_lab_code.py → core/utils.py
 - `微信小程序登录业务:openid + 手机号一致性校验。` --uses--> `SysUser`  [INFERRED]
   services/wechat_login_service.py → models/models.py
 - `已绑定 openid 时静默换票(无需手机号授权)。` --uses--> `SysUser`  [INFERRED]
@@ -78,124 +78,124 @@
 
 ### Community 0 - "Community 0"
 Cohesion: 0.01
-Nodes (1075): attachment_download_url(), create_dept(), create_dict_data(), create_dict_type(), create_express_company(), create_item_unit(), create_menu(), create_post() (+1067 more)
+Nodes (1019): attachment_download_url(), create_dept(), create_dict_data(), create_dict_type(), create_express_company(), create_item_unit(), create_menu(), create_post() (+1011 more)
 
 ### Community 1 - "Community 1"
 Cohesion: 0.01
-Nodes (734): generate_uuid(), allocate_next_biz_no(), _format_biz_no(), _prefix_for(), 业务协议号自动生成:XBCC / JZCC + YYYYMMDD + 6 位序号, 原子分配下一个业务协议号。     须在 create 事务内调用;使用 MySQL LAST_INSERT_ID 保证并发安全。, create_cell_sample_type(), get_cell_sample_type() (+726 more)
+Nodes (821): _ensure_customer_for_mother(), generate_uuid(), create_cell_sample_type(), get_cell_sample_type(), list_cell_sample_types(), 细胞样本类型 CRUD(biz_cell_sample_type), _row_to_dict(), soft_delete_cell_sample_type() (+813 more)
 
 ### Community 2 - "Community 2"
 Cohesion: 0.01
-Nodes (655): AttachmentDownloadUrlBody, DeptCreateBody, DeptUpdateBody, DictDataCreateBody, DictDataUpdateBody, DictTypeCreateBody, DictTypeUpdateBody, ExpressCompanyCreateBody (+647 more)
+Nodes (621): AttachmentDownloadUrlBody, DeptCreateBody, DeptUpdateBody, DictDataCreateBody, DictDataUpdateBody, DictTypeCreateBody, DictTypeUpdateBody, ExpressCompanyCreateBody (+613 more)
 
 ### Community 3 - "Community 3"
-Cohesion: 0.01
-Nodes (420): _normalize_date_bound(), _persist_protocol_sub_resources(), list_ethnics_for_select(), apply_data_scope(), _get_child_dept_ids(), get_user_dept_id(), _resolve_user_role_scopes(), format_date() (+412 more)
+Cohesion: 0.07
+Nodes (451): 保险公司及险种价格管理 API(保险公司 + 开票信息 + 险种价格), 审批意见:前端传 commentText,兼容 comment。, TaskActionBody, Base, 采集协议单 / 审核 / 缴费明细 CRUD(细胞存储 2.2.6~2.2.10), 返回已在采集协议单或科研捐赠单样本表中占用的样本编码集合(同租户、未删除)。, 创建/更新样本行前校验:样本编码不得已被任一业务单占用(转移配对单豁免)。, 缴费明细行 → 前端字典(含协议、客户、转账附件)。 (+443 more)
 
 ### Community 4 - "Community 4"
-Cohesion: 0.08
-Nodes (388): 环境检测 — 检测项维护 + 登记管理 API, 保险公司及险种价格管理 API(保险公司 + 开票信息 + 险种价格), 小程序菜单:当前用户在「移动端菜单」树下有权限的入口。, 宫格图标底色/图形色(可按入口自定义,不强制品牌绿)。, 按路由匹配移动端宫格图标(与 mobile-h5 van-icon 字形名对齐)。, ProcessDefCreateBody, ProcessDefUpdateBody, PublishVersionBody (+380 more)
+Cohesion: 0.01
+Nodes (302): _normalize_date_bound(), _persist_protocol_sub_resources(), _body_to_payload(), _get_current_user_id(), get_production_reception_by_sample_code(), get_production_reception_detail(), _get_tenant_no(), post_production_reception() (+294 more)
 
 ### Community 5 - "Community 5"
-Cohesion: 0.06
-Nodes (322): CellSampleTypeCreateBody, CellSampleTypeUpdateBody, CollectionRegionCreateBody, CollectionRegionUpdateBody, CustomerCreateBody, CustomerUpdateBody, HospitalCreateBody, HospitalUpdateBody (+314 more)
+Cohesion: 0.07
+Nodes (323): 细胞存储 — 采集协议单 / 审核 / 财务确认 / 分期预警 API, 持久化协议子资源。      缴费(含明细校验)须在样本之前:明细校验失败时不得已占用样本编码。     各子表 CRUD 内部仍可能分段 commit,创建接, 与列表接口一致:按租户 + 角色数据权限判断是否可访问该协议单。, If mother info exists but no customerId, create a customer record and link it., BatchStockInBody, BatchStockInItem, BatchStockOutBody, BatchStockOutItem (+315 more)
 
 ### Community 6 - "Community 6"
 Cohesion: 0.02
-Nodes (265): eq_unicode(), like_unicode(), MySQL 字符串比较/模糊匹配时统一排序规则,避免 Illegal mix of collations。, 跨表字符串等值比较(JOIN / WHERE)时统一为 utf8mb4_unicode_ci。, 将列显式设为 utf8mb4_unicode_ci。, unicode_ci(), build_void_type_label_map(), format_protocol_status_label() (+257 more)
+Nodes (296): list_ethnics_for_select(), AccessLogCreateBody, AccessLogUpdateBody, CleanRoomCreateBody, CleanRoomUpdateBody, create_access_log(), create_uv_disinfect_log(), delete_access_log() (+288 more)
 
 ### Community 7 - "Community 7"
-Cohesion: 0.03
-Nodes (247): 截止日期:仅日期时扩到当天 23:59:59。, 发起前预览审批链(条件齐后解析最终路径与审批人)。, 兼容旧 GET:无 formData,含条件分支时多为待确定占位。, _app_to_dict(), _copied_app_name(), copy_app(), create_app(), create_group() (+239 more)
+Cohesion: 0.02
+Nodes (249): eq_unicode(), like_unicode(), MySQL 字符串比较/模糊匹配时统一排序规则,避免 Illegal mix of collations。, 跨表字符串等值比较(JOIN / WHERE)时统一为 utf8mb4_unicode_ci。, 将列显式设为 utf8mb4_unicode_ci。, unicode_ci(), build_void_type_label_map(), format_protocol_status_label() (+241 more)
 
 ### Community 8 - "Community 8"
-Cohesion: 0.01
-Nodes (175): 服务号网页授权 code 绑定当前用户(需 Bearer)。, 微信登录:openid 已绑定时可静默换票;否则须授权手机号且与账号 phone_no 一致。, 密码登录后绑定当前微信 openid(需 Bearer)。, wechat_login(), get_designer_data(), _error_json(), lifespan(), 未捕获异常:统一 JSON 500,确保跨域场景下仍有 Access-Control-Allow-Origin (+167 more)
+Cohesion: 0.02
+Nodes (235): 截止日期:仅日期时扩到当天 23:59:59。, 发起前预览审批链(条件齐后解析最终路径与审批人)。, 兼容旧 GET:无 formData,含条件分支时多为待确定占位。, _app_to_dict(), _copied_app_name(), copy_app(), create_app(), _create_version() (+227 more)
 
 ### Community 9 - "Community 9"
-Cohesion: 0.03
-Nodes (148): api_save_application(), map_payments_by_schedule_ids(), get_protocol_nos_used_in_protocols(), _fmt_dt(), get_handover_by_schedule(), get_outbound_handover(), get_outbound_handover_view(), _handover_row_to_dict() (+140 more)
+Cohesion: 0.02
+Nodes (140): get_designer_data(), _error_json(), lifespan(), 未捕获异常:统一 JSON 500,确保跨域场景下仍有 Access-Control-Allow-Origin, 数据库异常:返回 JSON 并保留 CORS 头,避免浏览器只报跨域错误, sqlalchemy_exception_handler(), unhandled_exception_handler(), BaseHTTPMiddleware (+132 more)
 
 ### Community 10 - "Community 10"
-Cohesion: 0.04
-Nodes (114): _deduct_step_usage_legacy_batch(), register_step_usage_from_requisition(), revert_step_material_usages(), add_task_step(), _aggregate_production_tab_step_field(), assert_step_assignee_for_write(), assert_task_assignee(), assert_task_step_assignee_or_legacy() (+106 more)
+Cohesion: 0.02
+Nodes (142): mp_bind_status(), 小程序鉴权:微信登录 / 绑定 openid。, 当前用户是否已绑定微信服务号 openid。, 服务号网页授权 code 绑定当前用户(需 Bearer)。, 微信登录:openid 已绑定时可静默换票;否则须授权手机号且与账号 phone_no 一致。, 密码登录后绑定当前微信 openid(需 Bearer)。, _resolve_user_id(), web_qr_confirm() (+134 more)
 
 ### Community 11 - "Community 11"
-Cohesion: 0.05
-Nodes (114): AccessLogCreateBody, AccessLogUpdateBody, CleanRoomCreateBody, CleanRoomUpdateBody, ExhibitionAppointmentCreateBody, ExhibitionAppointmentUpdateBody, list_storage_slot_stats(), OccupancyCreateBody (+106 more)
+Cohesion: 0.03
+Nodes (152): _ancestor_link_from_cell_product_input(), _ancestor_sample_chain_link(), _apply_cell_product_status_change(), _build_ancestor_chain(), _build_descendant_chain(), _build_facility_path_label(), build_split_product_codes(), create_cell_product() (+144 more)
 
 ### Community 12 - "Community 12"
 Cohesion: 0.04
-Nodes (91): _collect_descendants(), _current_user(), _find_mobile_root(), list_my_mobile_menus(), _mobile_menu_icon(), _mobile_menu_icon_colors(), _user_menu_ids(), _jwt_user_payload() (+83 more)
+Nodes (102): _fmt_dt(), get_handover_by_schedule(), get_outbound_handover(), get_outbound_handover_view(), _handover_row_to_dict(), _handover_view_dict(), _parse_handover_time(), 供输注计划 detail 聚合的交接只读信息。 (+94 more)
 
 ### Community 13 - "Community 13"
-Cohesion: 0.05
-Nodes (75): get_next_cell_product_code(), _ancestor_link_from_cell_product_input(), _ancestor_sample_chain_link(), _apply_cell_product_status_change(), _build_ancestor_chain(), _build_descendant_chain(), _build_facility_path_label(), build_split_product_codes() (+67 more)
-
-### Community 14 - "Community 14"
 Cohesion: 0.13
 Nodes (29): _assert_routes_exist(), collect_missing_route_ids(), create_set(), create_set_from_work_order(), create_tasks_from_set(), delete_set(), get_set(), _item_count_map() (+21 more)
 
-### Community 15 - "Community 15"
+### Community 14 - "Community 14"
 Cohesion: 0.12
 Nodes (11): get_wechat_pay_client(), get_wechat_pay_v2(), 微信支付工具模块  支持微信支付 V3 API 文档参考: https://pay.weixin.qq.com/docs/v3/apis/index.ht, 统一下单接口 (V3)          Args:             out_trade_no: 商户订单号             total, 申请退款          Args:             out_trade_no: 商户订单号             total_amount, 小程序调起支付的二次签名参数(与公众号 JSAPI 规则一致), 统一下单 (V2 API)          Args:             out_trade_no: 商户订单号             tot, 生成签名 V3 (SHA256 with RSA) (+3 more)
 
+### Community 15 - "Community 15"
+Cohesion: 0.14
+Nodes (11): apply_formulas_and_linkages(), eval_formula(), _extract_rules(), preprocess_formula(), OA 表单公式与数据联动(禁止任意 eval), 按 schema 中字段 props.formula / props.linkage 回填 form_data。     linkage 示例: { "when, 将 {field} 转为合法标识符引用 field。, _SafeEval (+3 more)
+
 ### Community 16 - "Community 16"
-Cohesion: 0.19
-Nodes (21): apply_overtime_kit_hours(), _build_rest_day_segments(), _build_workday_windows(), calc_overtime_hours(), _day_at_minutes(), _day_start_ms(), _extract_rules(), _format_ymd() (+13 more)
+Cohesion: 0.14
+Nodes (22): 验证密码(哈希非 bcrypt 或损坏时返回 False,避免抛错), verify_password(), _attach_user_relations(), change_password(), create_user(), _empty_user_relation_fields(), enrich_user_dicts_with_relations(), get_user() (+14 more)
 
 ### Community 17 - "Community 17"
 Cohesion: 0.21
 Nodes (19): _consumable_row_to_dict(), create_process(), _create_sub_consumable(), _create_sub_data_collection(), _create_sub_output(), _create_sub_reagent(), _data_collection_row_to_dict(), get_process() (+11 more)
 
 ### Community 18 - "Community 18"
-Cohesion: 0.15
-Nodes (19): _field_label(), _is_empty(), _is_required(), _parse_money(), OA 表单设计器:金额控件 form_data 校验, 校验 form_data 中金额控件的值格式,返回中文错误列表(空表示通过)。, 解析金额值。成功返回 (number, None);失败返回 (None, error_key)。, _rule_props() (+11 more)
-
-### Community 19 - "Community 19"
 Cohesion: 0.2
 Nodes (19): _code_exists(), create_material(), _dec(), _fmt_dt(), get_material(), get_material_entity(), get_my_usages(), _is_enabled_status() (+11 more)
 
+### Community 19 - "Community 19"
+Cohesion: 0.25
+Nodes (15): _build_rest_day_segments(), _build_workday_windows(), calc_overtime_hours(), _day_at_minutes(), _day_start_ms(), _format_ymd(), _intersect_interval(), _interval_minutes() (+7 more)
+
 ### Community 20 - "Community 20"
-Cohesion: 0.22
-Nodes (15): apply_trip_kit_days(), calc_trip_leg_days(), _day_index(), _end_offset(), _extract_rules(), is_half_day_multiple(), 出差套件:半日制行程时长(与前端 oa-trip-days.ts 对齐), 遍历 tripKit:行程未手改则覆盖 duration;出差天数未手改则覆盖为各行程时长之和。 (+7 more)
+Cohesion: 0.24
+Nodes (11): deduct_for_step(), _display_remark(), list_stock_logs(), _parse_receiver_purpose_from_remark(), 试剂变动记录 CRUD(biz_reagent_stock_log), 撤销步骤此前 step_save 扣减,便于重新填报。, 扣减试剂库存并生成变动记录。调用方控制事务,本函数不 commit。, _resolve_receiver_purpose() (+3 more)
 
 ### Community 21 - "Community 21"
-Cohesion: 0.23
-Nodes (12): Config, ProductBase, ProductCategoryBase, ProductCategoryCreate, ProductCategoryResponse, ProductCreate, ProductResponse, ProductUnitBase (+4 more)
+Cohesion: 0.24
+Nodes (11): deduct_for_step(), _display_remark(), list_stock_logs(), _parse_receiver_purpose_from_remark(), 耗材变动记录 CRUD(biz_consumable_stock_log), 撤销步骤此前 step_save 扣减,便于重新填报。, 扣减耗材库存并生成变动记录。调用方控制事务,本函数不 commit。, _resolve_receiver_purpose() (+3 more)
 
 ### Community 22 - "Community 22"
-Cohesion: 0.24
-Nodes (11): deduct_for_step(), _display_remark(), list_stock_logs(), _parse_receiver_purpose_from_remark(), 试剂变动记录 CRUD(biz_reagent_stock_log), 撤销步骤此前 step_save 扣减,便于重新填报。, 扣减试剂库存并生成变动记录。调用方控制事务,本函数不 commit。, _resolve_receiver_purpose() (+3 more)
+Cohesion: 0.31
+Nodes (10): create_account(), _filtered_query(), get_account(), list_accounts(), list_options(), OA 收款账户 CRUD(oa_receive_account), row_to_dict(), soft_delete_account() (+2 more)
 
 ### Community 23 - "Community 23"
 Cohesion: 0.31
-Nodes (10): create_account(), _filtered_query(), get_account(), list_accounts(), list_options(), OA 收款账户 CRUD(oa_receive_account), row_to_dict(), soft_delete_account() (+2 more)
+Nodes (10): _build_tree(), _cascade_soft_delete(), _collect_with_ancestors(), get_dept(), list_depts(), list_depts_tree(), 保留匹配节点及其全部上级,供关键词检索后仍能正确组树。, _row_to_dict() (+2 more)
 
 ### Community 24 - "Community 24"
+Cohesion: 0.33
+Nodes (8): _build_tree(), _cascade_soft_delete(), get_menu(), list_menus(), list_menus_tree(), _row_to_dict(), soft_delete_menu(), update_menu()
+
+### Community 25 - "Community 25"
 Cohesion: 0.29
 Nodes (2): BaseModel, SoftDeleteModel
 
-### Community 25 - "Community 25"
+### Community 26 - "Community 26"
 Cohesion: 0.38
 Nodes (6): derive_legacy_status_from_three(), derive_three_status_from_legacy(), 阶段A兼容:从旧 status 推导三字段。     约束:旧 status 本身混合了支付/配送/订单生命周期,因此只能尽力推断;     若业务侧已有更精确, 阶段A兼容:从三字段反推旧 status(用于旧前端/旧筛选参数兼容)。, ThreeStatus, _to_decimal()
 
-### Community 26 - "Community 26"
+### Community 27 - "Community 27"
 Cohesion: 0.4
 Nodes (5): batch_matches_stock_status(), normalize_batch_stock_status(), 质检物料入库批次:按剩余数量过滤的语义辅助。, 纯函数判定(供单测);list_batches 用 SQL 等价条件。, 返回 'available' | 'exhausted';全部 / 空 / 非法 → None(不加剩余数量条件)。
 
-### Community 27 - "Community 27"
+### Community 28 - "Community 28"
 Cohesion: 0.5
 Nodes (3): BaseSettings, Config, Settings
 
-### Community 28 - "Community 28"
+### Community 29 - "Community 29"
 Cohesion: 0.5
 Nodes (2): _auto_fill_create_dept_id(), ORM before_insert 钩子:当 create_dept_id 为空时,自动从请求上下文填充当前用户部门 ID
 
-### Community 29 - "Community 29"
-Cohesion: 0.67
-Nodes (2): get_menu_list(), 运营端菜单接口(与 web-admin 前端静态路由 sqxp.ts 对齐,5 个一级菜单分组)
-
 ### Community 30 - "Community 30"
 Cohesion: 0.67
 Nodes (1): 转正套件:入职日 + 试用期月数 → 建议转正日(YYYY-MM-DD)。
@@ -237,13 +237,11 @@ Cohesion: 1.0
 Nodes (1): 发送催单通知给配送员                  Args:             deliver_openid: 配送员openid
 
 ## Knowledge Gaps
-- **181 isolated node(s):** `刷新会话 TTL(refreshToken 续期;已登录用户每次 API 校验通过后滑动续期)`, `写入 refreshToken(用于 one-time 使用/吊销)`, `根据 token 获取当前用户(包含 Redis 会话校验;会话滑动续期)`, `登录凭据 RSA 解密(与前端 JSEncrypt PKCS#1 v1.5 兼容)。`, `加载 RSA 密钥对:优先环境变量,否则使用内置固定默认密钥。` (+176 more)
+- **184 isolated node(s):** `刷新会话 TTL(refreshToken 续期;已登录用户每次 API 校验通过后滑动续期)`, `写入 refreshToken(用于 one-time 使用/吊销)`, `根据 token 获取当前用户(包含 Redis 会话校验;会话滑动续期)`, `登录凭据 RSA 解密(与前端 JSEncrypt PKCS#1 v1.5 兼容)。`, `加载 RSA 密钥对:优先环境变量,否则使用内置固定默认密钥。` (+179 more)
   These have ≤1 connection - possible missing edges or undocumented components.
-- **Thin community `Community 24`** (8 nodes): `BaseModel`, `.to_dict()`, `get_active()`, `base.py`, `SoftDeleteModel`, `.restore()`, `.soft_delete()`, `__tablename__()`
-  Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 28`** (4 nodes): `_auto_fill_create_dept_id()`, `get_db()`, `database.py`, `ORM before_insert 钩子:当 create_dept_id 为空时,自动从请求上下文填充当前用户部门 ID`
+- **Thin community `Community 25`** (8 nodes): `BaseModel`, `.to_dict()`, `get_active()`, `base.py`, `SoftDeleteModel`, `.restore()`, `.soft_delete()`, `__tablename__()`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 29`** (3 nodes): `get_menu_list()`, `menu.py`, `运营端菜单接口(与 web-admin 前端静态路由 sqxp.ts 对齐,5 个一级菜单分组)`
+- **Thin community `Community 29`** (4 nodes): `_auto_fill_create_dept_id()`, `get_db()`, `database.py`, `ORM before_insert 钩子:当 create_dept_id 为空时,自动从请求上下文填充当前用户部门 ID`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
 - **Thin community `Community 30`** (3 nodes): `add_months_ymd()`, `oa_confirmation_date.py`, `转正套件:入职日 + 试用期月数 → 建议转正日(YYYY-MM-DD)。`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
@@ -269,17 +267,17 @@ Nodes (1): 发送催单通知给配送员                  Args:             del
 ## Suggested Questions
 _Questions this graph is uniquely positioned to answer:_
 
-- **Why does `SysUser` connect `Community 2` to `Community 0`, `Community 1`, `Community 3`, `Community 4`, `Community 5`, `Community 6`, `Community 7`, `Community 8`, `Community 9`, `Community 11`, `Community 12`, `Community 22`?**
-  _High betweenness centrality (0.114) - this node is a cross-community bridge._
-- **Why does `success_response()` connect `Community 0` to `Community 2`, `Community 3`, `Community 8`, `Community 9`, `Community 11`, `Community 12`, `Community 13`, `Community 29`?**
-  _High betweenness centrality (0.038) - this node is a cross-community bridge._
-- **Why does `generate_uuid()` connect `Community 1` to `Community 0`, `Community 3`, `Community 5`, `Community 6`, `Community 7`, `Community 8`, `Community 9`, `Community 10`, `Community 11`, `Community 12`, `Community 13`, `Community 14`, `Community 17`, `Community 19`, `Community 22`, `Community 23`?**
-  _High betweenness centrality (0.035) - this node is a cross-community bridge._
-- **Are the 923 inferred relationships involving `str` (e.g. with `unhandled_exception_handler()` and `get_current_user_by_token()`) actually correct?**
-  _`str` has 923 INFERRED edges - model-reasoned connections that need verification._
-- **Are the 761 inferred relationships involving `success_response()` (e.g. with `list_storage_release_reviews()` and `get_storage_release_review_detail()`) actually correct?**
-  _`success_response()` has 761 INFERRED edges - model-reasoned connections that need verification._
-- **Are the 631 inferred relationships involving `error_response()` (e.g. with `_json_error_response()` and `list_storage_release_reviews()`) actually correct?**
-  _`error_response()` has 631 INFERRED edges - model-reasoned connections that need verification._
-- **Are the 604 inferred relationships involving `SysUser` (e.g. with `数据权限过滤(V0.3:基于角色 data_scope 的查询级行级过滤)  data_scope 值:   1 = 全部数据   2 = 本部门数据   3` and `从 SysUser 读取用户的 dept_id。`) actually correct?**
-  _`SysUser` has 604 INFERRED edges - model-reasoned connections that need verification._
+- **Why does `SysUser` connect `Community 2` to `Community 0`, `Community 1`, `Community 3`, `Community 5`, `Community 6`, `Community 8`, `Community 9`, `Community 10`, `Community 12`, `Community 16`, `Community 20`, `Community 21`?**
+  _High betweenness centrality (0.109) - this node is a cross-community bridge._
+- **Why does `success_response()` connect `Community 0` to `Community 2`, `Community 4`, `Community 6`, `Community 9`, `Community 10`?**
+  _High betweenness centrality (0.039) - this node is a cross-community bridge._
+- **Why does `OaEngineError` connect `Community 8` to `Community 1`, `Community 2`, `Community 3`?**
+  _High betweenness centrality (0.033) - this node is a cross-community bridge._
+- **Are the 930 inferred relationships involving `str` (e.g. with `unhandled_exception_handler()` and `get_current_user_by_token()`) actually correct?**
+  _`str` has 930 INFERRED edges - model-reasoned connections that need verification._
+- **Are the 769 inferred relationships involving `success_response()` (e.g. with `list_storage_release_reviews()` and `get_storage_release_review_detail()`) actually correct?**
+  _`success_response()` has 769 INFERRED edges - model-reasoned connections that need verification._
+- **Are the 638 inferred relationships involving `error_response()` (e.g. with `_json_error_response()` and `list_storage_release_reviews()`) actually correct?**
+  _`error_response()` has 638 INFERRED edges - model-reasoned connections that need verification._
+- **Are the 610 inferred relationships involving `SysUser` (e.g. with `数据权限过滤(V0.3:基于角色 data_scope 的查询级行级过滤)  data_scope 值:   1 = 全部数据   2 = 本部门数据   3` and `从 SysUser 读取用户的 dept_id。`) actually correct?**
+  _`SysUser` has 610 INFERRED edges - model-reasoned connections that need verification._

File diff suppressed because it is too large
+ 112 - 112
backend/graphify-out/graph.json


+ 9 - 8
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": 1789545254.772123,
+  "/Users/wuzilong/work/00code/zkxw/backend/app/core/config.py": 1789709850.5506392,
   "/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,7 +33,7 @@
   "/Users/wuzilong/work/00code/zkxw/backend/app/models/material.py": 1782894693.6389399,
   "/Users/wuzilong/work/00code/zkxw/backend/app/models/production_material.py": 1782404882.9262497,
   "/Users/wuzilong/work/00code/zkxw/backend/app/models/qc_param.py": 1782379593.174925,
-  "/Users/wuzilong/work/00code/zkxw/backend/app/models/models.py": 1788231725.9250517,
+  "/Users/wuzilong/work/00code/zkxw/backend/app/models/models.py": 1789701693.5477362,
   "/Users/wuzilong/work/00code/zkxw/backend/app/models/cleanroom_garment.py": 1781347080.6165943,
   "/Users/wuzilong/work/00code/zkxw/backend/app/models/cn_holiday.py": 1786373798.242758,
   "/Users/wuzilong/work/00code/zkxw/backend/app/models/__init__.py": 1781344070.5899634,
@@ -143,7 +143,7 @@
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/oa_approval.py": 1786414220.5298004,
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/cell_product.py": 1785289892.8170507,
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/menu.py": 1779265729.0381103,
-  "/Users/wuzilong/work/00code/zkxw/backend/app/crud/collection_region.py": 1782181887.2821388,
+  "/Users/wuzilong/work/00code/zkxw/backend/app/crud/collection_region.py": 1789701832.2690132,
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/pilot_delivery.py": 1779935673.6808827,
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/sample_reception_check_item.py": 1779007909.2933013,
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/site.py": 1781401290.6845422,
@@ -192,7 +192,7 @@
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/clinical_payment.py": 1782713876.5518267,
   "/Users/wuzilong/work/00code/zkxw/backend/app/api/storage_release_review.py": 1787110947.8175044,
   "/Users/wuzilong/work/00code/zkxw/backend/app/api/outbound_release_review.py": 1785303263.2759058,
-  "/Users/wuzilong/work/00code/zkxw/backend/app/api/public.py": 1786374022.5691514,
+  "/Users/wuzilong/work/00code/zkxw/backend/app/api/public.py": 1789710686.7963963,
   "/Users/wuzilong/work/00code/zkxw/backend/app/api/material.py": 1782894693.6582327,
   "/Users/wuzilong/work/00code/zkxw/backend/app/api/production_material.py": 1782404982.2474263,
   "/Users/wuzilong/work/00code/zkxw/backend/app/api/mini_menu.py": 1786680837.227191,
@@ -201,7 +201,7 @@
   "/Users/wuzilong/work/00code/zkxw/backend/app/api/sample_production_reception.py": 1788159565.164049,
   "/Users/wuzilong/work/00code/zkxw/backend/app/api/collection_protocol.py": 1785145536.3204737,
   "/Users/wuzilong/work/00code/zkxw/backend/app/api/cleanroom_garment.py": 1781347256.2326367,
-  "/Users/wuzilong/work/00code/zkxw/backend/app/api/basic_info.py": 1782709076.3968658,
+  "/Users/wuzilong/work/00code/zkxw/backend/app/api/basic_info.py": 1789702130.708455,
   "/Users/wuzilong/work/00code/zkxw/backend/app/api/pilot_infusion_confirm.py": 1779657043.1389203,
   "/Users/wuzilong/work/00code/zkxw/backend/app/api/pilot_infusion_schedule_mgmt.py": 1779940938.18027,
   "/Users/wuzilong/work/00code/zkxw/backend/app/api/__init__.py": 1786426755.6367717,
@@ -212,14 +212,14 @@
   "/Users/wuzilong/work/00code/zkxw/backend/app/api/research_donation_sample_collection.py": 1780457788.2998126,
   "/Users/wuzilong/work/00code/zkxw/backend/app/api/oa_approval.py": 1787892843.1222966,
   "/Users/wuzilong/work/00code/zkxw/backend/app/api/operations.py": 1785213144.2881503,
-  "/Users/wuzilong/work/00code/zkxw/backend/app/api/menu.py": 1787887339.1254666,
+  "/Users/wuzilong/work/00code/zkxw/backend/app/api/menu.py": 1789696372.4245293,
   "/Users/wuzilong/work/00code/zkxw/backend/app/api/pilot_delivery.py": 1779655584.820777,
   "/Users/wuzilong/work/00code/zkxw/backend/app/api/admin.py": 1786010386.7044516,
   "/Users/wuzilong/work/00code/zkxw/backend/app/api/env_detect.py": 1786945487.513935,
   "/Users/wuzilong/work/00code/zkxw/backend/app/api/research_donation.py": 1783920522.841086,
   "/Users/wuzilong/work/00code/zkxw/backend/app/api/ethnic.py": 1779266243.9511197,
   "/Users/wuzilong/work/00code/zkxw/backend/app/api/pilot_checkup.py": 1779935751.2658868,
-  "/Users/wuzilong/work/00code/zkxw/backend/app/api/mini_auth.py": 1786616100.716625,
+  "/Users/wuzilong/work/00code/zkxw/backend/app/api/mini_auth.py": 1789710909.065258,
   "/Users/wuzilong/work/00code/zkxw/backend/app/api/pilot_service.py": 1781599314.0221174,
   "/Users/wuzilong/work/00code/zkxw/backend/app/api/workflow.py": 1779266232.4568832,
   "/Users/wuzilong/work/00code/zkxw/backend/app/api/sample_sms_notification.py": 1780325524.907156,
@@ -237,10 +237,11 @@
   "/Users/wuzilong/work/00code/zkxw/backend/app/services/oa_person_select.py": 1785401156.2563787,
   "/Users/wuzilong/work/00code/zkxw/backend/app/services/qc_work_order_conclusion.py": 1785206750.2532814,
   "/Users/wuzilong/work/00code/zkxw/backend/app/services/oa_dept_select.py": 1786000695.5043027,
-  "/Users/wuzilong/work/00code/zkxw/backend/app/services/wechat_mini_auth.py": 1786353961.1533935,
+  "/Users/wuzilong/work/00code/zkxw/backend/app/services/wechat_mini_auth.py": 1789710434.6386433,
   "/Users/wuzilong/work/00code/zkxw/backend/app/services/oa_publish.py": 1786001976.196836,
   "/Users/wuzilong/work/00code/zkxw/backend/app/services/cn_holiday_sync.py": 1786412469.104896,
   "/Users/wuzilong/work/00code/zkxw/backend/app/services/workflow_engine.py": 1785909957.8243756,
+  "/Users/wuzilong/work/00code/zkxw/backend/app/services/wx_qr_session.py": 1789710006.0382662,
   "/Users/wuzilong/work/00code/zkxw/backend/app/services/cn_holiday.py": 1786373910.8580065,
   "/Users/wuzilong/work/00code/zkxw/backend/app/services/oa_confirmation_date.py": 1785403063.141215,
   "/Users/wuzilong/work/00code/zkxw/backend/app/services/web_auth_roles.py": 1785909942.9479194,

+ 910 - 0
docs/superpowers/plans/2026-09-18-web-admin-wx-qr-phone-login.md

@@ -0,0 +1,910 @@
+# web-admin 微信扫码登录(手机号匹配)Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** web-admin 登录页以小程序码扫码为主:授权手机号匹配 `sys_user.phone_no` 后电脑自动登录;账号密码为辅。
+
+**Architecture:** 后端 Redis(或内存)保存短时扫码会话;`create` 生成小程序码;手机打开 `pages/login/web-qr` 授权后 `confirm` 复用 `resolve_wechat_login_user` + `issue_tokens_for_user`;web-admin 轮询 `status` 一次性取走 JWT。
+
+**Tech Stack:** FastAPI、Redis、微信小程序码 API、Vue 3(web-admin)、uni-app(mobile-h5)
+
+**Spec:** `docs/superpowers/specs/2026-09-18-web-admin-wx-qr-phone-login-design.md`
+
+---
+
+## File map
+
+| 路径 | 职责 |
+|------|------|
+| `backend/app/core/config.py` | `WX_QR_*` 配置;`API_ENCRYPT_EXCLUDE_PATHS` 追加路径 |
+| `backend/app/services/wx_qr_session.py` | 扫码会话 CRUD、状态机、一次性消费 |
+| `backend/app/services/wechat_mini_auth.py` | 新增 `get_unlimited_wxacode` |
+| `backend/app/api/public.py` | `POST/GET /web/auth/wx-qr/create|status` |
+| `backend/app/api/mini_auth.py` | `web-qr-scanned` / `web-qr-confirm` |
+| `backend/tests/test_wx_qr_session.py` | 会话状态机单测 |
+| `backend/tests/test_wx_qr_api.py` | create/status/confirm 接口测(mock 微信) |
+| `web-admin/src/api/auth.ts` | `fetchWxQrCreate` / `fetchWxQrStatus` |
+| `web-admin/src/views/auth/login/index.vue` | 扫码主 UI + 密码折叠 + 轮询 |
+| `mobile-h5/pages/login/web-qr/index.vue` | 授权落地页 |
+| `mobile-h5/pages.json` | 注册新页 |
+| `mobile-h5/utils/auth.js` | `webQrScanned` / `webQrConfirm` |
+| `mobile-h5/utils/request.js` | 未登录路径白名单(若需要) |
+
+**不修改:** 支付逻辑、开放平台网站应用、自动注册用户。
+
+---
+
+### Task 1: 配置项与加密排除路径
+
+**Files:**
+- Modify: `backend/app/core/config.py`
+
+- [ ] **Step 1: 增加扫码会话配置并扩展排除路径**
+
+在 `Settings` 中、`API_ENCRYPT_EXCLUDE_PATHS` 附近增加:
+
+```python
+    WX_QR_SESSION_TTL_SEC: int = 180
+    WX_QR_ENV_VERSION: str = "release"  # release | trial | develop
+    WX_QR_PAGE: str = "pages/login/web-qr/index"
+```
+
+将 `API_ENCRYPT_EXCLUDE_PATHS` 字符串追加(注意逗号分隔、无多余空格导致匹配失败;与现有风格一致):
+
+```text
+/api/web/auth/wx-qr/create,
+/api/web/auth/wx-qr/status,
+/api/mini/auth/web-qr-scanned,
+/api/mini/auth/web-qr-confirm,
+```
+
+- [ ] **Step 2: Commit**
+
+```bash
+git add backend/app/core/config.py
+git commit -m "$(cat <<'EOF'
+chore: add wx-qr login config and encrypt exclude paths
+
+EOF
+)"
+```
+
+---
+
+### Task 2: 扫码会话服务(TDD)
+
+**Files:**
+- Create: `backend/app/services/wx_qr_session.py`
+- Create: `backend/tests/test_wx_qr_session.py`
+
+- [ ] **Step 1: 写失败单测**
+
+```python
+"""wx_qr_session 状态机与一次性消费。"""
+import time
+from unittest.mock import patch
+
+from app.services import wx_qr_session as s
+
+
+def setup_function():
+    s._memory_clear_for_tests()
+
+
+def test_create_pending_and_get():
+    sid, short = s.create_session(ttl_sec=60)
+    assert sid and short and len(short) <= 32
+    data = s.get_session(sid)
+    assert data["status"] == "pending"
+    assert s.resolve_session_id(short) == sid
+
+
+def test_mark_scanned():
+    sid, _ = s.create_session(ttl_sec=60)
+    assert s.mark_scanned(sid) is True
+    assert s.get_session(sid)["status"] == "scanned"
+
+
+def test_mark_success_consume_once():
+    sid, _ = s.create_session(ttl_sec=60)
+    payload = {"token": "t", "refreshToken": "r", "expiresIn": 1, "userInfo": {}}
+    assert s.mark_success(sid, payload) is True
+    first = s.consume_success(sid)
+    assert first["token"] == "t"
+    assert s.consume_success(sid) is None
+    assert s.get_session(sid)["status"] == "consumed"
+
+
+def test_mark_failed():
+    sid, _ = s.create_session(ttl_sec=60)
+    assert s.mark_failed(sid, "未找到员工") is True
+    assert s.get_session(sid)["status"] == "failed"
+    assert s.get_session(sid)["message"] == "未找到员工"
+
+
+def test_expire_old_session():
+    sid, _ = s.create_session(ttl_sec=1)
+    time.sleep(1.1)
+    assert s.get_session(sid) is None or s.get_session(sid)["status"] == "expired"
+
+
+def test_invalidate():
+    sid, _ = s.create_session(ttl_sec=60)
+    s.invalidate(sid)
+    data = s.get_session(sid)
+    assert data is None or data["status"] == "expired"
+```
+
+- [ ] **Step 2: 跑测确认失败**
+
+Run: `cd backend && source venv/bin/activate && pytest tests/test_wx_qr_session.py -v`  
+Expected: FAIL(模块不存在)
+
+- [ ] **Step 3: 实现 `wx_qr_session.py`**
+
+```python
+"""web-admin 扫码登录会话:短码映射 + 状态机;优先 Redis,否则进程内存。"""
+from __future__ import annotations
+
+import json
+import secrets
+import time
+import uuid
+from typing import Any, Dict, Optional
+
+from app.core.config import settings
+
+_PREFIX = "wxqr:sess:"
+_SHORT_PREFIX = "wxqr:short:"
+_memory: Dict[str, Dict[str, Any]] = {}
+_short_map: Dict[str, str] = {}
+
+
+def _memory_clear_for_tests() -> None:
+    _memory.clear()
+    _short_map.clear()
+
+
+def _ttl() -> int:
+    return int(getattr(settings, "WX_QR_SESSION_TTL_SEC", 180) or 180)
+
+
+def _redis():
+    """复用 auth 侧 Redis 客户端模式;无 REDIS_HOST 返回 None。"""
+    host = (settings.REDIS_HOST or "").strip()
+    if not host:
+        return None
+    try:
+        import redis
+
+        return redis.Redis(
+            host=host,
+            port=int(settings.REDIS_PORT or 6379),
+            db=int(settings.REDIS_DB or 0),
+            password=settings.REDIS_PASSWORD or None,
+            decode_responses=True,
+            socket_timeout=2,
+        )
+    except Exception:
+        return None
+
+
+def create_session(*, ttl_sec: Optional[int] = None) -> tuple:
+    ttl = int(ttl_sec if ttl_sec is not None else _ttl())
+    session_id = str(uuid.uuid4())
+    short = secrets.token_urlsafe(16)[:32]
+    now = int(time.time())
+    data = {
+        "sessionId": session_id,
+        "shortCode": short,
+        "status": "pending",
+        "message": "",
+        "createdAt": now,
+        "expiresAt": now + ttl,
+        "tokens": None,
+    }
+    r = _redis()
+    if r:
+        pipe = r.pipeline()
+        pipe.setex(_PREFIX + session_id, ttl, json.dumps(data, ensure_ascii=False))
+        pipe.setex(_SHORT_PREFIX + short, ttl, session_id)
+        pipe.execute()
+    else:
+        _memory[session_id] = data
+        _short_map[short] = session_id
+    return session_id, short
+
+
+def resolve_session_id(short_or_id: str) -> Optional[str]:
+    key = (short_or_id or "").strip()
+    if not key:
+        return None
+    r = _redis()
+    if r:
+        sid = r.get(_SHORT_PREFIX + key)
+        if sid:
+            return sid
+        if r.exists(_PREFIX + key):
+            return key
+        return None
+    if key in _short_map:
+        return _short_map[key]
+    if key in _memory:
+        return key
+    return None
+
+
+def get_session(session_id: str) -> Optional[Dict[str, Any]]:
+    sid = (session_id or "").strip()
+    if not sid:
+        return None
+    r = _redis()
+    raw = None
+    if r:
+        raw = r.get(_PREFIX + sid)
+        if not raw:
+            return None
+        data = json.loads(raw)
+    else:
+        data = _memory.get(sid)
+        if not data:
+            return None
+        data = dict(data)
+    if int(time.time()) >= int(data.get("expiresAt") or 0):
+        data["status"] = "expired"
+        return data
+    return data
+
+
+def _save(data: Dict[str, Any]) -> None:
+    sid = data["sessionId"]
+    ttl = max(1, int(data["expiresAt"]) - int(time.time()))
+    r = _redis()
+    if r:
+        r.setex(_PREFIX + sid, ttl, json.dumps(data, ensure_ascii=False))
+    else:
+        _memory[sid] = data
+
+
+def mark_scanned(session_id: str) -> bool:
+    data = get_session(session_id)
+    if not data or data["status"] in ("expired", "success", "consumed", "failed"):
+        return False
+    if data["status"] == "pending":
+        data["status"] = "scanned"
+        _save(data)
+    return True
+
+
+def mark_success(session_id: str, tokens: Dict[str, Any]) -> bool:
+    data = get_session(session_id)
+    if not data or data["status"] in ("expired", "consumed", "failed"):
+        return False
+    data["status"] = "success"
+    data["tokens"] = tokens
+    data["message"] = ""
+    _save(data)
+    return True
+
+
+def mark_failed(session_id: str, message: str) -> bool:
+    data = get_session(session_id)
+    if not data or data["status"] in ("expired", "consumed", "success"):
+        return False
+    data["status"] = "failed"
+    data["message"] = message or "登录失败"
+    data["tokens"] = None
+    _save(data)
+    return True
+
+
+def consume_success(session_id: str) -> Optional[Dict[str, Any]]:
+    data = get_session(session_id)
+    if not data or data["status"] != "success":
+        return None
+    tokens = data.get("tokens")
+    data["status"] = "consumed"
+    data["tokens"] = None
+    _save(data)
+    return tokens
+
+
+def invalidate(session_id: str) -> None:
+    data = get_session(session_id)
+    if not data:
+        return
+    data["status"] = "expired"
+    data["tokens"] = None
+    _save(data)
+```
+
+注意:内存模式下 `expiresAt` 到期时 `get_session` 返回 `status=expired`;单测 `test_expire_old_session` 接受 `None` 或 `expired`。
+
+- [ ] **Step 4: 跑测通过**
+
+Run: `cd backend && source venv/bin/activate && pytest tests/test_wx_qr_session.py -v`  
+Expected: PASS
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add backend/app/services/wx_qr_session.py backend/tests/test_wx_qr_session.py
+git commit -m "$(cat <<'EOF'
+feat: add wx-qr login session store with one-time token consume
+
+EOF
+)"
+```
+
+---
+
+### Task 3: 微信小程序码生成
+
+**Files:**
+- Modify: `backend/app/services/wechat_mini_auth.py`
+- Create: `backend/tests/test_wxacode.py`
+
+- [ ] **Step 1: 写失败单测(mock httpx)**
+
+```python
+from unittest.mock import MagicMock, patch
+
+from app.services.wechat_mini_auth import get_unlimited_wxacode, WechatMiniAuthError
+
+
+def test_get_unlimited_wxacode_returns_png_bytes():
+    token_resp = MagicMock()
+    token_resp.raise_for_status = MagicMock()
+    token_resp.json.return_value = {"access_token": "tok"}
+
+    code_resp = MagicMock()
+    code_resp.raise_for_status = MagicMock()
+    code_resp.headers = {"Content-Type": "image/jpeg"}
+    code_resp.content = b"\xff\xd8fake"
+
+    with patch("app.services.wechat_mini_auth._require_mini_credentials", return_value=("app", "sec")):
+        with patch("app.services.wechat_mini_auth.httpx.Client") as Client:
+            client = MagicMock()
+            Client.return_value.__enter__.return_value = client
+            client.get.return_value = token_resp
+            client.post.return_value = code_resp
+            raw = get_unlimited_wxacode(scene="abc", page="pages/login/web-qr/index")
+    assert raw.startswith(b"\xff\xd8")
+
+
+def test_get_unlimited_wxacode_json_error():
+    token_resp = MagicMock()
+    token_resp.raise_for_status = MagicMock()
+    token_resp.json.return_value = {"access_token": "tok"}
+
+    code_resp = MagicMock()
+    code_resp.raise_for_status = MagicMock()
+    code_resp.headers = {"Content-Type": "application/json"}
+    code_resp.content = b'{"errcode":40001,"errmsg":"fail"}'
+    code_resp.json.return_value = {"errcode": 40001, "errmsg": "fail"}
+
+    with patch("app.services.wechat_mini_auth._require_mini_credentials", return_value=("app", "sec")):
+        with patch("app.services.wechat_mini_auth.httpx.Client") as Client:
+            client = MagicMock()
+            Client.return_value.__enter__.return_value = client
+            client.get.return_value = token_resp
+            client.post.return_value = code_resp
+            try:
+                get_unlimited_wxacode(scene="abc", page="pages/login/web-qr/index")
+                assert False, "expected error"
+            except WechatMiniAuthError:
+                pass
+```
+
+- [ ] **Step 2: 跑测确认失败**
+
+Run: `cd backend && source venv/bin/activate && pytest tests/test_wxacode.py -v`  
+Expected: FAIL(函数未定义)
+
+- [ ] **Step 3: 在 `wechat_mini_auth.py` 增加函数**
+
+在文件末尾追加(复用 `_get_access_token`):
+
+```python
+_WX_WXACODE_UNLIMITED = "https://api.weixin.qq.com/wxa/getwxacodeunlimit"
+
+
+def get_unlimited_wxacode(
+    *,
+    scene: str,
+    page: str,
+    env_version: Optional[str] = None,
+) -> bytes:
+    """获取不限制小程序码 PNG/JPEG 字节;失败抛 WechatMiniAuthError。"""
+    sc = (scene or "").strip()
+    if not sc or len(sc) > 32:
+        raise WechatMiniAuthError("scene 无效或超过 32 字符")
+    pg = (page or "").strip()
+    if not pg:
+        raise WechatMiniAuthError("page 不能为空")
+    ver = (env_version or getattr(settings, "WX_QR_ENV_VERSION", None) or "release").strip()
+    if ver not in ("release", "trial", "develop"):
+        ver = "release"
+    token = _get_access_token()
+    with httpx.Client(timeout=20.0) as client:
+        r = client.post(
+            f"{_WX_WXACODE_UNLIMITED}?access_token={token}",
+            json={
+                "scene": sc,
+                "page": pg,
+                "check_path": False,
+                "env_version": ver,
+                "width": 280,
+            },
+        )
+        r.raise_for_status()
+        ctype = (r.headers.get("Content-Type") or "").lower()
+        if "application/json" in ctype:
+            data = r.json()
+            logger.warning("getwxacodeunlimit failed: %s", data)
+            raise WechatMiniAuthError(
+                "生成小程序码失败",
+                wx_errcode=data.get("errcode"),
+            )
+        if not r.content:
+            raise WechatMiniAuthError("生成小程序码失败:空响应")
+        return r.content
+```
+
+确保文件顶部已有 `Optional` 导入。
+
+- [ ] **Step 4: 跑测通过**
+
+Run: `cd backend && source venv/bin/activate && pytest tests/test_wxacode.py -v`  
+Expected: PASS
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add backend/app/services/wechat_mini_auth.py backend/tests/test_wxacode.py
+git commit -m "$(cat <<'EOF'
+feat: add WeChat unlimited mini program code generator
+
+EOF
+)"
+```
+
+---
+
+### Task 4: Web 端 create / status 接口
+
+**Files:**
+- Modify: `backend/app/api/public.py`
+- Create: `backend/tests/test_wx_qr_api.py`
+
+- [ ] **Step 1: 写接口测(mock 会话与小程序码)**
+
+```python
+"""web wx-qr create/status API。"""
+from unittest.mock import patch
+
+from fastapi.testclient import TestClient
+
+from app.main import app
+
+client = TestClient(app)
+
+
+def test_wx_qr_create_and_poll_pending():
+    with patch(
+        "app.api.public.get_unlimited_wxacode",
+        return_value=b"\x89PNG\r\n\x1a\n",
+    ):
+        r = client.post("/api/web/auth/wx-qr/create")
+    assert r.status_code == 200
+    body = r.json()
+    assert body.get("code") == 200 or body.get("success") is True or "data" in body
+    data = body.get("data") or body
+    sid = data["sessionId"]
+    assert data["qrImageBase64"].startswith("data:image/")
+    st = client.get("/api/web/auth/wx-qr/status", params={"sessionId": sid})
+    assert st.status_code == 200
+    st_data = (st.json().get("data") or st.json())
+    assert st_data["status"] == "pending"
+```
+
+(若项目 `success_response` 结构为 `{code, msg, data}`,断言以实际为准;与现有 `tests/` 中 TestClient 用法对齐。若无 TestClient 先例,可改为直接调用路由函数 + Depends override。)
+
+先查看现有 API 测试写法;若无 TestClient,本 Task 改为纯函数测:抽 `create_wx_qr_payload()` / `status_payload()` 再测。
+
+推荐最小可测路径:在 `public.py` 内调用 service,单测 mock `get_unlimited_wxacode` 与 `wx_qr_session`。
+
+- [ ] **Step 2: 在 `public.py` 增加路由**
+
+```python
+import base64
+from app.services import wx_qr_session
+from app.services.wechat_mini_auth import get_unlimited_wxacode, WechatMiniAuthError
+
+@router.post("/web/auth/wx-qr/create", response_model=dict)
+async def wx_qr_create():
+    session_id, short = wx_qr_session.create_session()
+    page = (settings.WX_QR_PAGE or "pages/login/web-qr/index").strip()
+    try:
+        raw = get_unlimited_wxacode(
+            scene=short,
+            page=page,
+            env_version=settings.WX_QR_ENV_VERSION,
+        )
+    except WechatMiniAuthError as exc:
+        wx_qr_session.invalidate(session_id)
+        return error_response(400, str(exc))
+    b64 = base64.b64encode(raw).decode("ascii")
+    # 粗略按魔数选 mime
+    mime = "image/jpeg" if raw[:2] == b"\xff\xd8" else "image/png"
+    return success_response(
+        {
+            "sessionId": session_id,
+            "expiresIn": settings.WX_QR_SESSION_TTL_SEC,
+            "qrImageBase64": f"data:{mime};base64,{b64}",
+        }
+    )
+
+
+@router.get("/web/auth/wx-qr/status", response_model=dict)
+async def wx_qr_status(sessionId: str = ""):
+    sid = (sessionId or "").strip()
+    data = wx_qr_session.get_session(sid)
+    if not data:
+        return success_response({"status": "expired", "message": "二维码已过期,请刷新"})
+    status = data.get("status") or "expired"
+    if status == "success":
+        tokens = wx_qr_session.consume_success(sid)
+        if not tokens:
+            return success_response({"status": "expired", "message": "登录凭证已失效,请刷新二维码"})
+        return success_response({"status": "success", **tokens})
+    if status == "failed":
+        return success_response({"status": "failed", "message": data.get("message") or "登录失败"})
+    if status in ("expired", "consumed"):
+        return success_response({"status": "expired", "message": "二维码已过期,请刷新"})
+    return success_response({"status": status})
+```
+
+导入 `settings` 若尚未从 config 引入则补上。
+
+- [ ] **Step 3: 跑测**
+
+Run: `cd backend && source venv/bin/activate && pytest tests/test_wx_qr_api.py tests/test_wx_qr_session.py -v`  
+Expected: PASS
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add backend/app/api/public.py backend/tests/test_wx_qr_api.py
+git commit -m "$(cat <<'EOF'
+feat: add web auth wx-qr create and status endpoints
+
+EOF
+)"
+```
+
+---
+
+### Task 5: 小程序 confirm / scanned 接口
+
+**Files:**
+- Modify: `backend/app/api/mini_auth.py`
+- Modify: `backend/tests/test_wx_qr_api.py`(追加用例)
+
+- [ ] **Step 1: 追加单测(mock 微信与发票)**
+
+```python
+def test_web_qr_confirm_success_sets_session():
+    from app.services import wx_qr_session
+
+    wx_qr_session._memory_clear_for_tests()
+    sid, short = wx_qr_session.create_session(ttl_sec=60)
+    fake_user = SimpleNamespace(id="u1", phone_no="13800138000", wx_openid=None, wx_unionid=None, status="1")
+    tokens = {
+        "token": "access",
+        "refreshToken": "refresh",
+        "expiresIn": 3600,
+        "userInfo": {"id": "u1"},
+    }
+    with patch("app.api.mini_auth.code2session", return_value={"openid": "oid1", "unionid": ""}), \
+         patch("app.api.mini_auth.get_phone_number", return_value="13800138000"), \
+         patch("app.api.mini_auth.resolve_wechat_login_user", return_value=(fake_user, None, None)), \
+         patch("app.api.mini_auth.issue_tokens_for_user", return_value=tokens):
+        r = client.post(
+            "/api/mini/auth/web-qr-confirm",
+            json={"sessionId": sid, "loginCode": "lc", "phoneCode": "pc"},
+        )
+    assert r.status_code == 200
+    consumed = wx_qr_session.consume_success(sid)
+    assert consumed and consumed["token"] == "access"
+```
+
+(`SimpleNamespace` 从 `types` 导入;`client` 同 Task 4。若路由尚未注册导致 404,先实现再跑。)
+
+- [ ] **Step 2: 在 `mini_auth.py` 增加路由**
+
+```python
+from app.services import wx_qr_session
+
+
+@router.post("/web-qr-scanned", response_model=dict)
+async def web_qr_scanned(body: dict):
+    raw = (body.get("sessionId") or body.get("scene") or "").strip()
+    sid = wx_qr_session.resolve_session_id(raw) or raw
+    data = wx_qr_session.get_session(sid)
+    if not data or data.get("status") in ("expired", "consumed"):
+        return error_response(400, "二维码已过期,请在电脑上刷新后重扫")
+    wx_qr_session.mark_scanned(sid)
+    return success_response({"ok": True})
+
+
+@router.post("/web-qr-confirm", response_model=dict)
+async def web_qr_confirm(body: dict, db: Session = Depends(get_db)):
+    raw = (body.get("sessionId") or body.get("scene") or "").strip()
+    sid = wx_qr_session.resolve_session_id(raw) or raw
+    data = wx_qr_session.get_session(sid)
+    if not data or data.get("status") in ("expired", "consumed", "failed"):
+        return error_response(400, "二维码已过期,请在电脑上刷新后重扫")
+    if data.get("status") == "success":
+        return success_response({"ok": True, "message": "电脑端即将登录"})
+
+    wx_qr_session.mark_scanned(sid)
+
+    login_code = (body.get("loginCode") or "").strip()
+    phone_code = (body.get("phoneCode") or "").strip()
+    if not login_code or not phone_code:
+        wx_qr_session.mark_failed(sid, "需要授权手机号才能完成扫码登录")
+        return error_response(400, "需要授权手机号才能完成扫码登录")
+
+    try:
+        sess = code2session(login_code)
+        phone = get_phone_number(phone_code)
+    except WechatMiniAuthError as exc:
+        wx_qr_session.mark_failed(sid, str(exc))
+        return error_response(400, str(exc))
+
+    user, err_msg, err_code = resolve_wechat_login_user(
+        db, openid=sess["openid"], phone=phone
+    )
+    if err_msg:
+        wx_qr_session.mark_failed(sid, err_msg)
+        return error_response(err_code or 400, err_msg)
+
+    unionid = (sess.get("unionid") or "").strip()
+    if unionid and not (getattr(user, "wx_unionid", None) or "").strip():
+        user.wx_unionid = unionid
+        user.update_time = datetime.now()
+        db.add(user)
+        db.commit()
+        db.refresh(user)
+
+    try:
+        tokens = await issue_tokens_for_user(db, user)
+    except ValueError as exc:
+        wx_qr_session.mark_failed(sid, str(exc))
+        return error_response(403, str(exc))
+
+    if not wx_qr_session.mark_success(sid, tokens):
+        return error_response(400, "二维码已过期,请在电脑上刷新后重扫")
+    return success_response({"ok": True, "message": "电脑端即将登录,可关闭本页"})
+```
+
+- [ ] **Step 3: 跑测**
+
+Run: `cd backend && source venv/bin/activate && pytest tests/test_wx_qr_api.py tests/test_wx_qr_session.py -v`  
+Expected: PASS
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add backend/app/api/mini_auth.py backend/tests/test_wx_qr_api.py
+git commit -m "$(cat <<'EOF'
+feat: add mini auth web-qr scanned and confirm endpoints
+
+EOF
+)"
+```
+
+---
+
+### Task 6: web-admin API 与登录页
+
+**Files:**
+- Modify: `web-admin/src/api/auth.ts`
+- Modify: `web-admin/src/views/auth/login/index.vue`
+
+- [ ] **Step 1: 在 `auth.ts` 增加接口**
+
+```typescript
+export function fetchWxQrCreate() {
+  return request.post<{
+    sessionId: string
+    expiresIn: number
+    qrImageBase64: string
+  }>({
+    url: '/web/auth/wx-qr/create',
+    data: {}
+  })
+}
+
+export function fetchWxQrStatus(sessionId: string) {
+  return request.get<{
+    status: string
+    message?: string
+    token?: string
+    refreshToken?: string
+    expiresIn?: number
+    userInfo?: {
+      id: string
+      username: string
+      realName: string
+      phone: string
+      role: string
+      roleNames?: string[]
+      regionId: string
+      regionName: string
+      avatarUrl?: string | null
+    }
+  }>({
+    url: '/web/auth/wx-qr/status',
+    params: { sessionId }
+  })
+}
+```
+
+- [ ] **Step 2: 改版登录页**
+
+要点(在现有 `index.vue` 上改,保留 RSA 密码登录逻辑):
+
+1. 右侧主区:标题「欢迎登录」+ 副文案「微信扫一扫,授权手机号后自动登录」。
+2. `<img :src="qrImage" />` + 状态文案 +「刷新二维码」按钮(带 `Refresh` 图标)。
+3. 分割线「或使用账号密码登录」;`showPasswordLogin` 默认 `false`,点击展开现有表单。
+4. `onMounted` → `refreshQr()`;`onUnmounted` → `clearInterval`。
+5. `refreshQr`:调 `fetchWxQrCreate`,设 `sessionId`/`qrImage`,每 1500ms 调 `fetchWxQrStatus`。
+6. `status===success'`:停轮询,`userStore.setToken` / `setUserInfo` / 通知 / `router.push`(复用现有成功分支)。
+7. `failed`/`expired`:展示 message;`expired` 可自动 `refreshQr`(注意防抖,避免死循环——仅自动一次或提示手动刷新)。
+
+样式:扫码区居中;沿用现有 `.login-right` Fresh Mint 色;密码区折叠用简单 `v-show`。
+
+- [ ] **Step 3: 本地冒烟**
+
+Run: `cd web-admin && pnpm dev`  
+手动打开登录页:应见到二维码请求(后端未起或微信未配时 create 可能失败——页面应 ElMessage 报错且可点刷新;密码登录仍可用)。
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add web-admin/src/api/auth.ts web-admin/src/views/auth/login/index.vue
+git commit -m "$(cat <<'EOF'
+feat: make web-admin login QR-primary with password fallback
+
+EOF
+)"
+```
+
+---
+
+### Task 7: mobile-h5 网页登录授权页
+
+**Files:**
+- Create: `mobile-h5/pages/login/web-qr/index.vue`
+- Modify: `mobile-h5/pages.json`
+- Modify: `mobile-h5/utils/auth.js`
+- Modify: `mobile-h5/utils/request.js`(若有路径白名单)
+
+- [ ] **Step 1: `auth.js` 增加方法**
+
+```javascript
+export async function webQrScanned(sessionId) {
+  return post('/mini/auth/web-qr-scanned', { sessionId })
+}
+
+export async function webQrConfirm({ sessionId, loginCode, phoneCode }) {
+  return post('/mini/auth/web-qr-confirm', {
+    sessionId,
+    loginCode,
+    phoneCode
+  })
+}
+```
+
+在 `request.js` 的未加密/白名单列表中加入 `/mini/auth/web-qr-scanned`、`/mini/auth/web-qr-confirm`(与 `wechat-login` 同级)。
+
+- [ ] **Step 2: 注册页面**
+
+在 `pages.json` 的 `pages/login/index` 后插入:
+
+```json
+{
+  "path": "pages/login/web-qr/index",
+  "style": {
+    "navigationBarTitleText": "网页登录授权",
+    "backgroundColor": "#ffffff"
+  }
+}
+```
+
+- [ ] **Step 3: 实现 `pages/login/web-qr/index.vue`**
+
+最小结构:
+
+- `onLoad(options)`:`sessionId = options.scene || options.sessionId`(微信码进入时多为 `scene`)。
+- 无 sessionId:展示错误「请从电脑登录页重新扫码」。
+- 有则调用 `webQrScanned(sessionId)`(失败可忽略文案)。
+- 主按钮 `open-type="getPhoneNumber"`;`@getphonenumber` 内 `uni.login` 取 `loginCode`,再 `webQrConfirm`。
+- 成功:`uni.showToast` / 页内成功态「电脑端即将登录,可关闭本页」——**不要** `saveSession`、**不要** `switchTab` 首页。
+- 失败:展示后端 message。
+- 协议:若现登录页强制同意,复用 `ConfirmDialog` + `getPhoneNumber` 同次点击模式(对齐 `pages/login/index.vue`)。
+
+- [ ] **Step 4: 开发者工具冒烟**
+
+用开发者工具编译;手动 `navigateTo` `/pages/login/web-qr/index?sessionId=测试` 看缺会话/错误态;真机扫码联调放在 Task 8。
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add mobile-h5/pages/login/web-qr/index.vue mobile-h5/pages.json mobile-h5/utils/auth.js mobile-h5/utils/request.js
+git commit -m "$(cat <<'EOF'
+feat: add mini program page for web-admin QR phone login
+
+EOF
+)"
+```
+
+---
+
+### Task 8: 联调验收与图谱更新
+
+**Files:**
+- 无必须代码;按需微调文案/TTL
+
+- [ ] **Step 1: 环境检查清单**
+
+- `WXPAY_MINI_APPID` / `WXPAY_SECRET` 已配  
+- `REDIS_HOST` 建议生产开启  
+- `WX_QR_ENV_VERSION`:开发用 `develop`/`trial`,生产 `release`  
+- 小程序已包含并上传 `pages/login/web-qr/index`  
+- 后台用户 `phone_no` 与微信绑定手机一致  
+
+- [ ] **Step 2: 对照 spec §7 验收**
+
+逐条勾选设计文档验收标准 1–7。
+
+- [ ] **Step 3: 更新知识图谱**
+
+```bash
+./scripts/update-graph.sh all
+```
+
+- [ ] **Step 4: Commit(若有图谱或小修)**
+
+```bash
+git add backend/graphify-out web-admin/graphify-out
+git commit -m "$(cat <<'EOF'
+chore: update graphify after wx-qr login changes
+
+EOF
+)"
+```
+
+---
+
+## Spec coverage checklist
+
+| Spec 项 | Task |
+|---------|------|
+| 扫码会话状态机 + 一次性消费 | Task 2 |
+| 小程序码生成 | Task 3 |
+| `/web/auth/wx-qr/create|status` | Task 4 |
+| `/mini/auth/web-qr-*` + 复用手机号匹配 | Task 5 |
+| web-admin 主扫码辅密码 | Task 6 |
+| mobile-h5 授权页不强制登录工作台 | Task 7 |
+| 配置 / 排除路径 / 发版注意 | Task 1、8 |
+| 不自动注册、非经典网页扫码 | 全任务未引入 |
+
+## Self-review notes
+
+- 无 TBD/TODO 占位。  
+- `sessionId`(完整 UUID)供网页轮询;`short`(≤32)写入小程序码 `scene`;confirm 支持二者经 `resolve_session_id`。  
+- success 只经网页 `status` 消费一次;小程序 confirm 不返回 JWT。  
+- 密码登录路径保持 RSA,不删除。

+ 273 - 0
docs/superpowers/specs/2026-09-18-web-admin-wx-qr-phone-login-design.md

@@ -0,0 +1,273 @@
+# web-admin 微信扫码登录(手机号匹配)设计
+
+> 日期:2026-09-18  
+> 适用范围:`web-admin` 登录页 + `backend` 扫码会话 + `mobile-h5` 网页登录授权页  
+> 关联:`docs/superpowers/specs/2026-08-10-mobile-h5-wechat-login-design.md`  
+> 决策:方案 B(按手机号匹配)+ 实现方案 1(小程序码 + 网页轮询)
+
+---
+
+## 1. 目标与已确认决策
+
+### 1.1 目标
+
+1. web-admin 登录以**微信扫码**为主:用户扫码 → 小程序授权手机号 → 匹配 `sys_user.phone_no` → 电脑端自动登录。
+2. **账号密码**为辅入口(默认折叠,可展开),现有 RSA 登录逻辑保持可用。
+3. 不要求小程序事先已登录;授权成功后**只登录电脑**,手机不强制进入工作台。
+
+### 1.2 决策摘要
+
+| 项 | 约定 |
+|----|------|
+| 身份策略 | 授权手机号匹配 `phone_no`(复用小程序微信登录逻辑) |
+| 扫码载体 | 微信小程序码(非开放平台网站应用经典扫码) |
+| 电脑取票 | 网页轮询会话状态,success 时下发与密码登录同结构的 JWT |
+| 未匹配 / 失败 | 明确提示,引导电脑端账号密码登录;**不自动注册** |
+| 手机会话 | 授权成功后小程序不强制写 JWT、不跳转工作台 |
+| 会话存储 | Redis(TTL 默认 180s);无 Redis 时仅开发环境内存兜底 |
+
+### 1.3 非目标
+
+- 微信开放平台「网站应用」嵌入式二维码登录(无手机号)
+- 扫码自动开户 / 注册
+- 改动密码登录签发结构(须与现有 `token` / `refreshToken` / `userInfo` 对齐)
+- 替换 `mobile-h5` 日常登录主流程(仅新增网页授权落地页)
+
+### 1.4 为何不走经典网页扫码
+
+PC 网页微信扫码(开放平台网站应用)一般只返回 openid/unionid,**不能授权手机号**。产品要求按手机号自动匹配,故必须经小程序 `getPhoneNumber`,再由网页轮询取票。
+
+---
+
+## 2. 主流程
+
+```text
+web-admin                         backend                         mobile-h5
+   |                                 |                                |
+   |-- POST /web/auth/wx-qr/create ->|                                |
+   |<- sessionId + 小程序码 ---------|                                |
+   |  展示二维码,开始轮询            |                                |
+   |                                 |   用户微信扫一扫                 |
+   |                                 |------------------------------->|
+   |                                 |         打开 web-qr 页          |
+   |                                 |<- (可选) web-qr-scanned --------|
+   |<- status=scanned ---------------|                                |
+   |                                 |<- web-qr-confirm (phone) -------|
+   |                                 |  匹配 phone_no,issue tokens    |
+   |<- status=success + JWT ---------|                                |
+   |  写入 store,进入系统            |         提示成功,可关闭         |
+```
+
+---
+
+## 3. 会话状态机
+
+```text
+pending  --扫码打开页-->  scanned  --授权成功-->  success
+   |                       |
+   |                       +--授权/匹配失败-->  failed(带 reason)
+   |
+   +--超时 / 刷新换新码-->  expired(旧 session 作废)
+```
+
+| 状态 | 含义 | 网页表现 |
+|------|------|----------|
+| `pending` | 已生成码,等待扫 | 「请使用微信扫一扫」 |
+| `scanned` | 已打开小程序页,待授权 | 「已扫码,请在手机上确认授权」 |
+| `success` | 已匹配并发票 | 取 token,进入系统 |
+| `failed` | 匹配或授权失败 | 展示 `message`,可刷新二维码 |
+| `expired` | 超时或已作废 | 自动或手动刷新 |
+
+规则:
+
+- TTL 默认 **180 秒**。
+- `success` 后 token 载荷**一次性消费**:网页轮询取走即从会话删除,防重放。
+- 刷新二维码 = 创建新会话,旧 `sessionId` 标记 `expired`。
+- `sessionId` 使用不可猜测值(UUID);因小程序码 `scene` 最长 32 字符,对外 scene 可用短码,服务端映射到完整 sessionId。
+
+---
+
+## 4. 后端 API
+
+路径加入 `API_ENCRYPT_EXCLUDE_PATHS`(与现有 login 一致)。未登录可调。
+
+### 4.1 `POST /api/web/auth/wx-qr/create`
+
+生成扫码会话并返回小程序码图片。
+
+**响应 `data`:**
+
+```json
+{
+  "sessionId": "<完整会话 id,供轮询>",
+  "expiresIn": 180,
+  "qrImageBase64": "data:image/png;base64,..."
+}
+```
+
+实现要点:
+
+- 写入 Redis:`status=pending`、过期时间。
+- 调用微信获取不限制小程序码(或等价 API),`page` 为小程序网页登录页路径,`scene` 为短码。
+- 同 IP / 同客户端创建频率限流,避免耗尽小程序码配额。
+
+### 4.2 `GET /api/web/auth/wx-qr/status?sessionId=`
+
+网页轮询(建议间隔约 1.5s)。
+
+| status | 额外字段 |
+|--------|----------|
+| pending / scanned / expired | — |
+| failed | `message` |
+| success(首次消费) | `token`、`refreshToken`、`expiresIn`、`userInfo`(与 `/web/auth/login` 一致) |
+| success(已消费) | 视为过期或返回明确错误,不再下发票 |
+
+### 4.3 `POST /api/mini/auth/web-qr-scanned`(可选)
+
+Body:`{ "sessionId": "..." }`  
+将 `pending` → `scanned`,改善网页文案。非法/过期会话返回错误。
+
+### 4.4 `POST /api/mini/auth/web-qr-confirm`
+
+Body:
+
+```json
+{
+  "sessionId": "...",
+  "loginCode": "<wx.login code>",
+  "phoneCode": "<getPhoneNumber code>"
+}
+```
+
+处理顺序:
+
+1. 校验会话存在且为 `pending`/`scanned`、未过期。
+2. `code2session(loginCode)` → openid(及可选 unionid)。
+3. `get_phone_number(phoneCode)` → 手机号。
+4. 调用现有 `resolve_wechat_login_user`(与 `POST /mini/auth/wechat-login` 同源)。
+5. `issue_tokens_for_user` → 写入会话 `success` + token 载荷。
+6. 失败 → 会话 `failed` + `message`;响应错误文案与小程序展示一致。
+
+成功时小程序响应仅需业务成功提示(**可不向手机返回完整 web JWT**),电脑端通过轮询取票。
+
+### 4.5 复用与签发
+
+- 匹配 / 绑定:`app/services/wechat_login_service.py` 的 `resolve_wechat_login_user`。
+- 发票:`issue_tokens_for_user`(与密码登录、小程序微信登录一致)。
+- 微信凭证:复用 `WXPAY_MINI_APPID`、`WXPAY_SECRET`;新增服务端 `access_token` + 小程序码生成封装(仓库内尚无现成 getwxacode 实现)。
+
+---
+
+## 5. 前端
+
+### 5.1 web-admin 登录页
+
+文件:`web-admin/src/views/auth/login/index.vue`(及 `src/api/auth.ts`)。
+
+布局:保留左右分栏;右侧主区为扫码,账号密码折叠为辅。
+
+```text
+欢迎登录
+微信扫一扫,授权手机号后自动登录
+
+      [ 小程序码 ]
+   状态文案(随轮询变化)
+   [ 刷新二维码 ]
+
+────── 或使用账号密码登录 ──────
+(点击展开)
+用户名 / 密码 / 登录(现有 RSA 逻辑)
+```
+
+交互:
+
+| 时机 | 行为 |
+|------|------|
+| 进入页 / 点刷新 | `create` → 展示码 → 开始轮询 |
+| pending | 「请使用微信扫一扫」 |
+| scanned | 「已扫码,请在手机上确认授权」 |
+| success | 停轮询 → `setToken` / `setUserInfo` → 通知 → 安全跳转(同现逻辑) |
+| failed | 展示 `message`,可刷新 |
+| expired | 提示过期,自动或手动刷新 |
+| 离开页面 | 停止轮询 |
+
+视觉:Fresh Mint(主色 `#10b981` 链);二维码区简洁。
+
+### 5.2 mobile-h5 网页登录授权页
+
+路径建议:`pages/login/web-qr/index`(写入 `pages.json`;小程序码 `page` 与此一致)。
+
+- 入参:从 `scene` / query 解析短码或 `sessionId`。
+- Header 标题:网页登录授权。
+- 说明:正在登录电脑端管理后台,请授权手机号完成验证。
+- 主按钮:`open-type="getPhoneNumber"`(须用户点击;协议同意可复用现登录页模式)。
+- 成功:提示「电脑端即将登录,可关闭本页」;不强制写本地 JWT、不跳转工作台。
+- 失败 / 缺 session:页内明确原因 + 引导电脑账号密码登录。
+- `onLoad` 可选调用 `web-qr-scanned`。
+
+### 5.3 统一失败文案
+
+| 场景 | 文案方向 |
+|------|----------|
+| 未匹配手机号 | 未找到与该手机号匹配的员工账号,请使用电脑账号密码登录或联系管理员 |
+| 一号多人 | 手机号对应多个账号,请使用账号密码登录 |
+| 账号停用 | 账号已停用 |
+| 会话过期 | 二维码已过期,请在电脑上刷新后重扫 |
+| 拒绝授权 | 需要授权手机号才能完成扫码登录 |
+
+---
+
+## 6. 配置与改动清单
+
+### 6.1 配置
+
+| 项 | 说明 |
+|----|------|
+| `WXPAY_MINI_APPID` / `WXPAY_SECRET` | 已有;小程序码、换票、手机号 |
+| `REDIS_*` | 扫码会话;未配置时仅开发内存兜底 |
+| `API_ENCRYPT_EXCLUDE_PATHS` | 加入 wx-qr / web-qr 相关路径 |
+| 可选 `WX_QR_SESSION_TTL_SEC` | 默认 180 |
+| 可选 `WX_QR_ENV_VERSION` | `release` / `trial` / `develop`,生成码环境 |
+| 小程序发版 | 必须包含 `pages/login/web-qr/index` |
+| 微信后台 | request 合法域名含 API |
+
+无新业务表。
+
+### 6.2 代码改动面
+
+| 区域 | 内容 |
+|------|------|
+| backend | 会话服务;小程序码生成;`/web/auth/wx-qr/*`;`/mini/auth/web-qr-*` |
+| web-admin | 登录页改版;auth API;轮询 |
+| mobile-h5 | web-qr 页 + `pages.json` |
+| docs | 本设计;实现计划另开 |
+
+---
+
+## 7. 验收标准
+
+1. 登录页默认展示二维码;账号密码可展开且仍可登录。
+2. 微信扫码 → 授权手机号 → 电脑在数秒内进入系统;token 结构与密码登录一致。
+3. 匹配成功时按现逻辑绑定/校验 `wx_openid`。
+4. 无匹配、一号多人、停用、拒授权、过期:两端提示正确;可刷新或改用密码。
+5. success 的 token 仅网页轮询成功取一次;重复请求不再返回完整票。
+6. 离开登录页停止轮询;刷新二维码使旧会话失效。
+7. 小程序未事先登录仍可完成授权。
+
+---
+
+## 8. 风险与缓解
+
+| 风险 | 缓解 |
+|------|------|
+| 正式小程序未发布新页面 | 上线清单含发版;开发/体验用 `env_version` |
+| `phone_no` 空或重复 | 主数据治理 + 明确失败文案 |
+| 小程序码配额被刷 | 创建限流 + 短 TTL |
+| scene 32 字符限制 | 短码 + 服务端映射完整 sessionId |
+
+---
+
+## 9. 测试要点
+
+- 单元/接口:会话状态迁移;success 一次性消费;过期;匹配失败各分支(可 mock 微信)。
+- 手工:真机微信扫正式/体验码;电脑轮询成功进系统;密码辅入口回归;离页停轮询。

File diff suppressed because it is too large
+ 944 - 857
web-admin/graphify-out/.graphify_analysis.json


File diff suppressed because it is too large
+ 244 - 216
web-admin/graphify-out/.graphify_extract.json


+ 143 - 136
web-admin/graphify-out/GRAPH_REPORT.md

@@ -1,11 +1,11 @@
-# Graph Report - src  (2026-09-17)
+# Graph Report - src  (2026-09-18)
 
 ## Corpus Check
-- 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.
+- Large corpus: 1000 files · ~661,825 words. Semantic extraction will be expensive (many Claude tokens). Consider running on a subfolder, or use --no-semantic to run AST-only.
 
 ## Summary
-- 3601 nodes · 5136 edges · 71 communities detected
-- Extraction: 78% EXTRACTED · 22% INFERRED · 0% AMBIGUOUS · INFERRED: 1134 edges (avg confidence: 0.8)
+- 3607 nodes · 5144 edges · 72 communities detected
+- Extraction: 78% EXTRACTED · 22% INFERRED · 0% AMBIGUOUS · INFERRED: 1136 edges (avg confidence: 0.8)
 - Token cost: 0 input · 0 output
 
 ## Community Hubs (Navigation)
@@ -52,14 +52,14 @@
 - [[_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 58|Community 58]]
@@ -68,21 +68,22 @@
 - [[_COMMUNITY_Community 61|Community 61]]
 - [[_COMMUNITY_Community 62|Community 62]]
 - [[_COMMUNITY_Community 63|Community 63]]
-- [[_COMMUNITY_Community 65|Community 65]]
-- [[_COMMUNITY_Community 68|Community 68]]
-- [[_COMMUNITY_Community 74|Community 74]]
-- [[_COMMUNITY_Community 76|Community 76]]
+- [[_COMMUNITY_Community 64|Community 64]]
+- [[_COMMUNITY_Community 66|Community 66]]
+- [[_COMMUNITY_Community 69|Community 69]]
+- [[_COMMUNITY_Community 75|Community 75]]
 - [[_COMMUNITY_Community 77|Community 77]]
-- [[_COMMUNITY_Community 79|Community 79]]
+- [[_COMMUNITY_Community 78|Community 78]]
 - [[_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 82|Community 82]]
+- [[_COMMUNITY_Community 87|Community 87]]
+- [[_COMMUNITY_Community 99|Community 99]]
 - [[_COMMUNITY_Community 102|Community 102]]
+- [[_COMMUNITY_Community 103|Community 103]]
 
 ## God Nodes (most connected - your core abstractions)
-1. `get()` - 317 edges
+1. `get()` - 319 edges
 2. `push()` - 128 edges
 3. `emit()` - 29 edges
 4. `WebSocketClient` - 26 edges
@@ -100,193 +101,193 @@
   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.01
-Nodes (183): fetchAccessLogList(), fetchActiveRegions(), fetchCategoryList(), fetchCategoryParameters(), fetchCellProductPriceList(), fetchCellProductSpecList(), fetchCellProductTypeList(), fetchChannelAccount() (+175 more)
+Cohesion: 0.0
+Nodes (204): fetchAccessLogList(), fetchActiveRegions(), fetchCategoryList(), fetchCategoryParameters(), fetchCellProductPriceList(), fetchCellProductSpecList(), fetchCellProductTypeList(), fetchChannelAccount() (+196 more)
 
 ### Community 1 - "Community 1"
 Cohesion: 0.02
-Nodes (86): fetchUploadAttachment(), handleReportUpload(), handleReportUpload(), walk(), if(), async(), addDispatchItem(), createDispatchItem() (+78 more)
+Nodes (139): fetchDeptTree(), fetchUploadAttachment(), fetchOaFlowPreview(), fetchOaTemplateDetail(), handleReportUpload(), handleReportUpload(), walk(), collectConditionReferencedFields() (+131 more)
 
 ### Community 2 - "Community 2"
-Cohesion: 0.03
-Nodes (86): batchSavePilotCheckup(), deletePilotCheckupActualItem(), fetchCheckupInstitutionList(), fetchCheckupInstitutionPackageDetail(), fetchCheckupInstitutionPackages(), fetchPilotCheckupActualItems(), fetchPilotCheckupDetail(), fetchPilotCheckupList() (+78 more)
+Cohesion: 0.02
+Nodes (92): batchSavePilotCheckup(), deletePilotCheckupActualItem(), fetchCheckupInstitutionList(), fetchCheckupInstitutionPackageDetail(), fetchCheckupInstitutionPackages(), fetchPilotCheckupDetail(), fetchPilotCheckupList(), fetchUpdatePostBoundRoles() (+84 more)
 
 ### Community 3 - "Community 3"
 Cohesion: 0.03
-Nodes (79): fetchEthnicList(), formatNowDatetime(), applyDonationBundleToForm(), createEmptyTransport(), ensureEthnicOptionInList(), handleOpenForAdd(), if(), loadEthnicOptions() (+71 more)
+Nodes (72): buildInboundBasicSection(), buildInboundCellStorageViewSections(), buildInboundResearchDonationViewSections(), buildReceptionSection(), buildTransportSection(), resolveInboundProtocolStatus(), buildBaseFields(), buildFeeSection() (+64 more)
 
 ### Community 4 - "Community 4"
 Cohesion: 0.02
-Nodes (64): fetchBatchDeleteOaReceiveAccounts(), fetchBatchMarkOaCcTasksRead(), fetchCreateOaReceiveAccount(), fetchDeleteOaReceiveAccount(), fetchMarkOaCcTaskRead(), fetchOaAppDraft(), fetchOaCcUnreadCount(), fetchOaInstanceDetail() (+56 more)
+Nodes (42): fetchGetMenuList(), useSettingsHandlers(), useSettingsPanel(), useSettingsState(), ComponentLoader, IframeRouteManager, MenuProcessor, RoutePermissionValidator (+34 more)
 
 ### Community 5 - "Community 5"
-Cohesion: 0.03
-Nodes (34): fetchGetMenuList(), ComponentLoader, IframeRouteManager, MenuProcessor, RoutePermissionValidator, RouteRegistry, RouteTransformer, useAppMode() (+26 more)
+Cohesion: 0.02
+Nodes (65): fetchBatchDeleteOaReceiveAccounts(), fetchBatchMarkOaCcTasksRead(), fetchCreateOaReceiveAccount(), fetchDeleteOaReceiveAccount(), fetchMarkOaCcTaskRead(), fetchOaAppDraft(), fetchOaCcUnreadCount(), fetchOaInstanceDetail() (+57 more)
 
 ### Community 6 - "Community 6"
 Cohesion: 0.03
-Nodes (65): fetchCellSampleTypeList(), fetchCheckupInstitutionInvoices(), fetchCheckupInstitutionProjects(), fetchCreateCellSampleType(), fetchCreateCheckupInstitution(), fetchCreateCheckupInstitutionInvoice(), fetchCreateCheckupInstitutionProject(), fetchDeleteCellSampleType() (+57 more)
+Nodes (79): fetchEthnicList(), formatNowDatetime(), applyDonationBundleToForm(), createEmptyTransport(), ensureEthnicOptionInList(), handleOpenForAdd(), if(), loadEthnicOptions() (+71 more)
 
 ### Community 7 - "Community 7"
 Cohesion: 0.03
-Nodes (62): useSettingsHandlers(), useSettingsPanel(), useSettingsState(), useCeremony(), useTheme(), if(), nodeSummary(), createDefaultFlow() (+54 more)
+Nodes (66): fetchFullmoonTodoList(), fetchUpdateFullmoonTodoInsurance(), fetchCollectionProtocolAuditList(), fetchCollectionProtocolDetail(), fetchCollectionProtocolList(), fetchCreateProtocolInvoiceRequest(), fetchInvoiceEligiblePayments(), fetchPaymentDetailList() (+58 more)
 
 ### Community 8 - "Community 8"
 Cohesion: 0.03
-Nodes (56): fetchCreateCustomer(), fetchCustomerList(), fetchDeleteCustomer(), fetchUpdateCustomer(), updateInfusionScheduleStatus(), fetchProductionWasteOutputList(), handleDialogClose(), handleAdd() (+48 more)
+Nodes (47): handleBlur(), handleFocus(), handleInput(), fetchClinicalPaymentDetail(), fetchClinicalPayments(), fetchClinicalReceiptConfirmed(), fetchClinicalReceiptPending(), fetchClinicalRefundConfirmed() (+39 more)
 
 ### Community 9 - "Community 9"
 Cohesion: 0.03
-Nodes (69): fetchDeptTree(), fetchUserList(), fetchOaFlowPreview(), fetchOaTemplateDetail(), remoteSearchHandoverUsers(), chipTitle(), confirmPicker(), memberOptionLabel() (+61 more)
+Nodes (61): fetchCheckupInstitutionInvoices(), fetchCheckupInstitutionProjects(), fetchCreateCheckupInstitution(), fetchCreateCheckupInstitutionInvoice(), fetchCreateCheckupInstitutionProject(), fetchUpdateCheckupInstitution(), fetchUpdateCheckupInstitutionInvoice(), fetchUpdateCheckupInstitutionProject() (+53 more)
 
 ### Community 10 - "Community 10"
-Cohesion: 0.02
-Nodes (29): fetchAllConsumablePacks(), fetchAllConsumables(), fetchAllConsumableStockLogs(), fetchAllEquipments(), fetchAllOutputTypes(), fetchAllProcessCategories(), fetchAllProcesses(), fetchAllReagents() (+21 more)
+Cohesion: 0.03
+Nodes (47): fetchProductionWasteOutputList(), fetchProductionReceptionBySampleCode(), fetchProductionReceptionSubmit(), fetchProductionReceptionUpdate(), handleDialogClose(), handleView(), branchConditionLines(), onAdd() (+39 more)
 
 ### Community 11 - "Community 11"
 Cohesion: 0.03
-Nodes (56): fetchMyProductionTasks(), fetchMyQcTaskList(), fetchQcTaskDetail(), fetchQcTaskList(), fetchMineTasksForWorkOrders(), useInboundSiblingNavigation(), shouldApplyTriggerSourceFilter(), useQcWorkOrderTabPage() (+48 more)
+Nodes (56): buildTemporaryViewSections(), buildInsertedSiblingCard(), resolveShowProtocolPaymentAtGroup(), useInboundSiblingNavigation(), shouldApplyTriggerSourceFilter(), useQcWorkOrderTabPage(), appendTaskResultSections(), batchTasksForPanelSlot() (+48 more)
 
 ### Community 12 - "Community 12"
-Cohesion: 0.04
-Nodes (53): buildTemporaryViewSections(), buildInsertedSiblingCard(), resolveShowProtocolPaymentAtGroup(), appendTaskResultSections(), batchTasksForPanelSlot(), buildConfiguredOnlySection(), for(), set() (+45 more)
+Cohesion: 0.03
+Nodes (50): if(), chipTitle(), confirmPicker(), memberOptionLabel(), removeUser(), resolveUser(), signBadge(), signBadgeText() (+42 more)
 
 ### Community 13 - "Community 13"
 Cohesion: 0.04
-Nodes (49): buildInboundBasicSection(), buildInboundCellStorageViewSections(), buildInboundResearchDonationViewSections(), buildReceptionSection(), buildTransportSection(), resolveInboundProtocolStatus(), buildBaseFields(), buildInboundBasicSection() (+41 more)
+Nodes (50): fetchMyProductionTasks(), fetchMyQcTaskList(), fetchQcTaskDetail(), fetchQcTaskList(), fetchMineTasksForWorkOrders(), buildMineTableRows(), buildPresetTasksMap(), buildWorkOrderFromTasks() (+42 more)
 
 ### Community 14 - "Community 14"
-Cohesion: 0.03
-Nodes (49): fetchAttachmentDownloadUrl(), qcAttachmentFullUrl(), qcDownloadAttachment(), qcPreviewAttachment(), formatHandoverUserLabel(), formatNowDatetime(), fullFileUrl(), handleDownloadAttachment() (+41 more)
+Cohesion: 0.04
+Nodes (38): fetchCellSampleTypeList(), fetchCreateCellSampleType(), fetchCreateMenu(), fetchDeleteCellSampleType(), fetchDeleteMenu(), fetchMenuTree(), fetchUpdateCellSampleType(), fetchUpdateMenu() (+30 more)
 
 ### Community 15 - "Community 15"
-Cohesion: 0.04
-Nodes (46): fetchProductionWorkOrderQcInfo(), dash(), fmtMoney(), auditorDisplay(), dash(), ensureUnitsInOptions(), expressName(), formatYm() (+38 more)
+Cohesion: 0.05
+Nodes (39): fetchCreateQcInspectionItemTemplate(), fetchDeleteQcInspectionItemTemplate(), fetchQcInspectionItemTemplateDetail(), fetchQcInspectionItemTemplateList(), fetchQcInspectionItemTemplatesActive(), fetchUpdateQcInspectionItemTemplate(), fetchQcLabTestItemList(), fetchQcLabTestItemsActive() (+31 more)
 
 ### Community 16 - "Community 16"
-Cohesion: 0.04
-Nodes (39): fetchCreateMenu(), fetchDeleteMenu(), fetchMenuTree(), fetchUpdateMenu(), fetchUpdatePostBoundRoles(), fetchCreateEnvDetectItem(), fetchDeleteEnvDetectItem(), fetchEnvDetectItemDetail() (+31 more)
+Cohesion: 0.07
+Nodes (43): fetchOaRelatedSummaries(), buildFormRows(), buildOaPrintFilename(), buildOaPrintModel(), canvasToPdf(), ensurePlainObject(), exportOaApprovalPdf(), extractUrls() (+35 more)
 
 ### Community 17 - "Community 17"
-Cohesion: 0.04
-Nodes (44): fetchFullmoonTodoList(), fetchUpdateFullmoonTodoInsurance(), fetchCollectionProtocolAuditList(), fetchCollectionProtocolDetail(), fetchCollectionProtocolList(), fetchCreateProtocolInvoiceRequest(), fetchInvoiceEligiblePayments(), fetchPaymentDetailList() (+36 more)
+Cohesion: 0.06
+Nodes (37): fetchUserList(), fetchAddTaskStep(), fetchCopyTaskStep(), fetchDeleteTaskStep(), fetchDispatchProductionTaskSteps(), fetchForwardProductionTask(), fetchProductionTaskDetail(), fetchProductionTaskEditorInit() (+29 more)
 
 ### Community 18 - "Community 18"
-Cohesion: 0.05
-Nodes (39): fetchCreateQcInspectionItemTemplate(), fetchDeleteQcInspectionItemTemplate(), fetchQcInspectionItemTemplateDetail(), fetchQcInspectionItemTemplateList(), fetchQcInspectionItemTemplatesActive(), fetchUpdateQcInspectionItemTemplate(), fetchQcLabTestItemList(), fetchQcLabTestItemsActive() (+31 more)
+Cohesion: 0.08
+Nodes (37): formatDatetimeToMinute(), handleChineseDatetimePaste(), parseChineseDatetimeText(), parseBirthOrder(), parseChineseNumeral(), addSampleHint(), earliestCollectTime(), isMaternalBloodEmpty() (+29 more)
 
 ### Community 19 - "Community 19"
-Cohesion: 0.07
-Nodes (43): fetchOaRelatedSummaries(), buildFormRows(), buildOaPrintFilename(), buildOaPrintModel(), canvasToPdf(), ensurePlainObject(), exportOaApprovalPdf(), extractUrls() (+35 more)
+Cohesion: 0.06
+Nodes (22): fetchQcMaterialBatchDetail(), fetchQcMaterialBatchList(), fetchQcMaterialBatchOptions(), fetchQcMaterialList(), fetchQcMaterialOptions(), fetchUpdateQcMaterialBatch(), fetchMarkQcMaterialUsageFullyUsed(), financeAttachmentsToUrlString() (+14 more)
 
 ### Community 20 - "Community 20"
-Cohesion: 0.05
-Nodes (24): handleBlur(), handleFocus(), handleInput(), fetchClinicalReceiptConfirmed(), buildConfirmedParams(), buildPendingParams(), formatAmount(), handleConfirmedReset() (+16 more)
+Cohesion: 0.06
+Nodes (26): fetchProductionTaskList(), createEmptySnapshots(), normalizeConfiguredReagents(), normalizeConsumables(), normalizeDataCollections(), normalizeReagents(), normalizeStepOutputs(), serializeAttachments() (+18 more)
 
 ### Community 21 - "Community 21"
-Cohesion: 0.06
-Nodes (40): completePilotApplication(), deletePilotApplication(), fetchPilotApplicationList(), startPilotApplication(), voidPilotApplication(), fetchClinicalRefundConfirmed(), buildConfirmedParams(), buildPendingParams() (+32 more)
+Cohesion: 0.1
+Nodes (2): WebSocketClient, TableCache
 
 ### Community 22 - "Community 22"
 Cohesion: 0.08
-Nodes (37): formatDatetimeToMinute(), handleChineseDatetimePaste(), parseChineseDatetimeText(), parseBirthOrder(), parseChineseNumeral(), addSampleHint(), earliestCollectTime(), isMaternalBloodEmpty() (+29 more)
+Nodes (33): fetchRefreshToken(), toLogin(), base64ToBytes(), bytesToBase64(), decryptJson(), deriveKey(), encryptJson(), fromBase64Url() (+25 more)
 
 ### Community 23 - "Community 23"
-Cohesion: 0.06
-Nodes (22): fetchQcMaterialBatchDetail(), fetchQcMaterialBatchList(), fetchQcMaterialBatchOptions(), fetchQcMaterialList(), fetchQcMaterialOptions(), fetchUpdateQcMaterialBatch(), fetchMarkQcMaterialUsageFullyUsed(), financeAttachmentsToUrlString() (+14 more)
+Cohesion: 0.07
+Nodes (13): fetchProductionWorkOrderQcInfo(), dash(), fmtMoney(), auditorDisplay(), dash(), ensureUnitsInOptions(), expressName(), formatYm() (+5 more)
 
 ### Community 24 - "Community 24"
-Cohesion: 0.1
-Nodes (2): WebSocketClient, TableCache
-
-### Community 25 - "Community 25"
 Cohesion: 0.08
-Nodes (33): fetchRefreshToken(), toLogin(), base64ToBytes(), bytesToBase64(), decryptJson(), deriveKey(), encryptJson(), fromBase64Url() (+25 more)
+Nodes (30): fetchCreateCustomer(), fetchCustomerList(), fetchDeleteCustomer(), fetchUpdateCustomer(), updateInfusionScheduleStatus(), handleAdd(), handleDelete(), handleDropdownCommand() (+22 more)
 
-### Community 26 - "Community 26"
+### Community 25 - "Community 25"
 Cohesion: 0.09
 Nodes (8): getDefaultLanguage(), checkStorageCompatibility(), StorageConfig, getSystemStorage(), getSysVersion(), StorageKeyManager, StorageCompatibilityManager, validateStorageData()
 
-### Community 27 - "Community 27"
-Cohesion: 0.09
-Nodes (29): fetchAddTaskStep(), fetchCopyTaskStep(), fetchDeleteTaskStep(), fetchDispatchProductionTaskSteps(), fetchForwardProductionTask(), fetchProductionTaskDetail(), fetchProductionTaskEditorInit(), fetchReassignProductionTask() (+21 more)
+### Community 26 - "Community 26"
+Cohesion: 0.08
+Nodes (20): fetchAttachmentDownloadUrl(), attachmentFullUrl(), dash(), handleAttachmentDownload(), handleAttachmentPreview(), ensureUniqueZipEntryName(), fetchAttachmentBlob(), getAttachmentExtension() (+12 more)
 
-### Community 28 - "Community 28"
+### Community 27 - "Community 27"
 Cohesion: 0.08
-Nodes (20): createEmptySnapshots(), normalizeConfiguredReagents(), normalizeConsumables(), normalizeDataCollections(), normalizeReagents(), normalizeStepOutputs(), serializeAttachments(), serializeEquipmentIds() (+12 more)
+Nodes (22): fetchMineProductionWorkOrderTabStepCounts(), fetchMineProductionWorkOrderTabStepCountsDeduped(), fetchProductionWorkOrderCellProductOptions(), fetchProductionWorkOrderDetail(), fetchProductionWorkOrderList(), fetchProductionWorkOrderQcDispatchOptions(), fetchProductionWorkOrderTabStepCounts(), fetchProductionWorkOrderTabStepCountsDeduped() (+14 more)
 
-### Community 29 - "Community 29"
+### Community 28 - "Community 28"
 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"
+### Community 29 - "Community 29"
 Cohesion: 0.09
-Nodes (23): fetchProductionTaskList(), fetchProductionReceptionBySampleCode(), fetchProductionReceptionSubmit(), fetchProductionReceptionUpdate(), onUserSelectVisible(), remoteSearchUsers(), buildPayload(), ensureDefaultHandler() (+15 more)
+Nodes (28): completePilotApplication(), deletePilotApplication(), fetchPilotApplicationList(), startPilotApplication(), voidPilotApplication(), clearTableSelection(), handleBatchComplete(), handleBatchStart() (+20 more)
 
-### Community 32 - "Community 32"
+### Community 30 - "Community 30"
 Cohesion: 0.09
 Nodes (22): fetchOaAppList(), fetchOaGroupList(), fetchPublishOaApp(), loadData(), loadGroups(), resetForm(), onSuccess(), goBack() (+14 more)
 
-### Community 33 - "Community 33"
+### Community 31 - "Community 31"
 Cohesion: 0.1
 Nodes (21): displayPeople(), nodeStatusText(), personLines(), personStatusHint(), personTitle(), showNodeComment(), signBadge(), signBadgeText() (+13 more)
 
-### Community 34 - "Community 34"
-Cohesion: 0.12
-Nodes (20): fetchGetMenuCodes(), fetchGetUserInfo(), fetchLogin(), fetchUpdateProfile(), fetchUploadProfileAvatar(), fetchUserInfo(), buildWebEncryptedLoginBody(), defaultAvatarPathByRole() (+12 more)
+### Community 32 - "Community 32"
+Cohesion: 0.11
+Nodes (21): fetchGetMenuCodes(), fetchGetUserInfo(), fetchLogin(), fetchUpdateProfile(), fetchUploadProfileAvatar(), fetchWxQrStatus(), fetchUserInfo(), buildWebEncryptedLoginBody() (+13 more)
 
-### Community 35 - "Community 35"
+### Community 33 - "Community 33"
 Cohesion: 0.09
 Nodes (12): fetchMineQcWorkOrderTabUnfinishedCounts(), fetchOutboundQcDetail(), fetchQcWorkOrderDetail(), fetchQcWorkOrderList(), fetchQcWorkOrdersBySample(), fetchQcWorkOrderTabUnfinishedCounts(), reload(), reloadTabCounts() (+4 more)
 
-### Community 36 - "Community 36"
+### Community 34 - "Community 34"
 Cohesion: 0.1
 Nodes (12): fetchOutboundReleaseMaterial(), fetchOutboundReleaseReviewDetail(), fetchOutboundReleaseReviewList(), fetchReleaseReviewList(), handleReset(), handleSearch(), handleTabChange(), handleToggleSelect() (+4 more)
 
-### Community 37 - "Community 37"
+### Community 35 - "Community 35"
 Cohesion: 0.12
 Nodes (15): addCopyButton(), highlightCode(), insertLineNumbers(), isBlockProcessed(), markBlockAsProcessed(), processBlock(), setupHighlightDirective(), setupRippleDirective() (+7 more)
 
-### Community 38 - "Community 38"
+### Community 36 - "Community 36"
 Cohesion: 0.2
 Nodes (14): applyFlagToRule(), applySchema(), countRules(), getActiveRule(), getDesigner(), getSetupState(), onMessage(), readFlag() (+6 more)
 
-### Community 39 - "Community 39"
+### Community 37 - "Community 37"
 Cohesion: 0.12
 Nodes (6): setupOaConfirmationKit(), ensureOaKitMenu(), setupOaLeaveKit(), setupOaOvertimeKit(), setupOaResignationKit(), setupOaTripKit()
 
-### Community 40 - "Community 40"
+### Community 38 - "Community 38"
 Cohesion: 0.16
 Nodes (10): useTable(), useTableImpl(), getColumnChecks(), useTableColumns(), createErrorHandler(), createSmartDebounce(), defaultResponseAdapter(), extractPagination() (+2 more)
 
-### Community 41 - "Community 41"
+### Community 39 - "Community 39"
 Cohesion: 0.27
 Nodes (14): useChart(), useChartComponent(), useChartOps(), colourBlend(), getCssVar(), getDarkColor(), getLightColor(), handleElementThemeColor() (+6 more)
 
-### Community 42 - "Community 42"
+### Community 40 - "Community 40"
+Cohesion: 0.33
+Nodes (13): buildProtocolPaymentFlowSteps(), dash(), financeReceiptTime(), formatAmount(), hasFinanceReceipt(), hasFinanceRefund(), hasReceiptSubmit(), hasRefundSubmit() (+5 more)
+
+### Community 41 - "Community 41"
 Cohesion: 0.19
 Nodes (6): fetchServiceOrderTxnList(), handleReset(), handleSearch(), handleTabChange(), loadData(), switchTab()
 
-### Community 43 - "Community 43"
+### Community 42 - "Community 42"
 Cohesion: 0.24
 Nodes (10): buildPrintDocumentHtml(), cleanupPrintIframe(), embedSopImagesAsDataUrls(), getSopA4ContentHeightPx(), getSopA4EditorHeightPx(), normalizeSopCheckboxes(), prepareSopHtmlForPrint(), printSopHtml() (+2 more)
 
-### Community 45 - "Community 45"
+### Community 44 - "Community 44"
 Cohesion: 0.15
 Nodes (4): fetchNextDonationSampleCode(), fetchResearchDonationAuditList(), fetchResearchDonationDetail(), fetchResearchDonationList()
 
+### Community 45 - "Community 45"
+Cohesion: 0.27
+Nodes (10): nodeSummary(), createDefaultFlow(), createEndNode(), createNode(), ensureFlowRoot(), newId(), normalizeCcAssigneeTypes(), removeChildNode() (+2 more)
+
 ### Community 46 - "Community 46"
 Cohesion: 0.33
 Nodes (7): buildSheetColumns(), downloadOaHistoryExcel(), exportTimestamp(), pad(), sanitizeFilename(), styleHeaderCell(), writeHeaderRows()
@@ -296,21 +297,21 @@ Cohesion: 0.31
 Nodes (4): isSlotAvailable(), resolveStorageSlotRangeFill(), slotFacilityNodeId(), sortBoxSlots()
 
 ### Community 48 - "Community 48"
-Cohesion: 0.28
-Nodes (1): FireworkSystem
-
-### Community 49 - "Community 49"
 Cohesion: 0.25
 Nodes (2): handleTouchEnd(), setTransition()
 
-### Community 51 - "Community 51"
+### Community 49 - "Community 49"
 Cohesion: 0.28
-Nodes (4): fetchMyProductionMaterialConsumptions(), handleReset(), handleSearch(), loadData()
+Nodes (1): FireworkSystem
 
-### Community 52 - "Community 52"
+### Community 51 - "Community 51"
 Cohesion: 0.28
 Nodes (4): isActiveHistoryInstance(), isUploadPreviewLabel(), previewColumnMinWidth(), syncWorktabTitle()
 
+### Community 54 - "Community 54"
+Cohesion: 0.29
+Nodes (4): if(), emptyTripLeg(), buildTripKitChildren(), uniqueId()
+
 ### Community 55 - "Community 55"
 Cohesion: 0.39
 Nodes (6): isIgnorableScriptError(), normalizeErrorMessage(), registerPromiseErrorHandler(), registerResourceErrorHandler(), scriptErrorHandler(), setupErrorHandle()
@@ -328,122 +329,128 @@ Cohesion: 0.38
 Nodes (4): handleFocus(), handleInput(), parseDecimalToNumber(), sanitizeDecimalInput()
 
 ### Community 60 - "Community 60"
+Cohesion: 0.29
+Nodes (1): fetchQcMonthTemplateList()
+
+### Community 61 - "Community 61"
 Cohesion: 0.4
 Nodes (2): isUnfinishedQcWorkOrder(), resolveQcWorkOrderUnfinishedBadgeCount()
 
-### Community 61 - "Community 61"
+### Community 62 - "Community 62"
 Cohesion: 0.47
 Nodes (3): isPaymentDueWithinWarnWindow(), parsePaymentDueYmd(), startOfLocalDay()
 
-### Community 62 - "Community 62"
+### Community 63 - "Community 63"
 Cohesion: 0.5
 Nodes (2): formatProtocolDocDeleteConfirm(), formatProtocolDocSubject()
 
-### Community 63 - "Community 63"
+### Community 64 - "Community 64"
 Cohesion: 0.5
 Nodes (2): parseEquipmentIds(), syncEquipmentNamesFromOptions()
 
-### Community 65 - "Community 65"
+### Community 66 - "Community 66"
 Cohesion: 0.5
 Nodes (2): focusInput(), handleKeydown()
 
-### Community 68 - "Community 68"
+### Community 69 - "Community 69"
 Cohesion: 0.67
 Nodes (2): if(), logSidebarMenu()
 
-### Community 74 - "Community 74"
+### Community 75 - "Community 75"
 Cohesion: 1.0
 Nodes (2): getSettingDefaults(), resetToDefaults()
 
-### Community 76 - "Community 76"
+### Community 77 - "Community 77"
 Cohesion: 1.0
 Nodes (2): buildOvertimeKitChildren(), uniqueId()
 
-### Community 77 - "Community 77"
+### Community 78 - "Community 78"
 Cohesion: 1.0
 Nodes (2): buildLeaveKitChildren(), uniqueId()
 
-### Community 79 - "Community 79"
+### Community 80 - "Community 80"
 Cohesion: 1.0
 Nodes (2): buildConfirmationKitChildren(), uniqueId()
 
-### Community 80 - "Community 80"
+### Community 81 - "Community 81"
 Cohesion: 1.0
 Nodes (2): buildResignationKitChildren(), uniqueId()
 
-### Community 81 - "Community 81"
+### Community 82 - "Community 82"
 Cohesion: 1.0
 Nodes (2): copyTextToClipboard(), copyTextWithFallback()
 
-### Community 85 - "Community 85"
+### Community 87 - "Community 87"
 Cohesion: 1.0
 Nodes (2): getSourceProtocolAttachment(), normalizeAttachmentFiles()
 
-### Community 98 - "Community 98"
+### Community 99 - "Community 99"
 Cohesion: 1.0
 Nodes (2): emptyTabCounts(), useProductionWorkOrderTabStepCounts()
 
-### Community 101 - "Community 101"
+### Community 102 - "Community 102"
 Cohesion: 1.0
 Nodes (2): initialProgress(), useQcWorkOrderAttachmentExport()
 
-### Community 102 - "Community 102"
+### Community 103 - "Community 103"
 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`
+- **Thin community `Community 21`** (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 48`** (9 nodes): `FireworkSystem`, `.constructor()`, `.createFirework()`, `.createParticle()`, `.getAvailableParticle()`, `.initializePool()`, `.preloadAllImages()`, `.preloadImage()`, `index.vue`
+- **Thin community `Community 48`** (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 49`** (9 nodes): `cleanupEventListeners()`, `handleTouchEnd()`, `handleTouchMove()`, `handleTouchStart()`, `handleWheelScroll()`, `if()`, `setTransition()`, `setupEventListeners()`, `index.vue`
+- **Thin community `Community 49`** (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 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 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 60`** (6 nodes): `formatQcWorkOrderUnfinishedBadgeCount()`, `isUnfinishedQcWorkOrder()`, `qcWorkOrderUnfinishedBadgeTitle()`, `resolveQcWorkOrderUnfinishedBadgeCount()`, `sumQcWorkOrderGroupUnfinishedBadgeCount()`, `qc-work-order-unfinished-badge.ts`
+- **Thin community `Community 60`** (7 nodes): `fetchConfirmQcMonthImport()`, `fetchCreateQcMonthTemplate()`, `fetchDeleteQcMonthTemplate()`, `fetchPreviewQcMonthImport()`, `fetchQcMonthTemplateList()`, `fetchUpdateQcMonthTemplate()`, `qc-material-month-template.ts`
+  Too small to be a meaningful cluster - may be noise or needs more connections extracted.
+- **Thin community `Community 61`** (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 62`** (5 nodes): `formatProtocolDocBatchDeleteConfirm()`, `formatProtocolDocDeleteConfirm()`, `formatProtocolDocSubject()`, `getProtocolDocTypeLabel()`, `protocol-doc-label.ts`
+- **Thin community `Community 63`** (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 63`** (5 nodes): `equipmentOptionLabel()`, `joinEquipmentIds()`, `parseEquipmentIds()`, `syncEquipmentNamesFromOptions()`, `equipment-select.ts`
+- **Thin community `Community 64`** (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`** (5 nodes): `focusInput()`, `handleKeydown()`, `if()`, `search()`, `index.vue`
+- **Thin community `Community 66`** (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 68`** (4 nodes): `if()`, `logSidebarMenu()`, `index.vue`, `SidebarSubmenu.vue`
+- **Thin community `Community 69`** (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 74`** (3 nodes): `getSettingDefaults()`, `resetToDefaults()`, `setting.ts`
+- **Thin community `Community 75`** (3 nodes): `getSettingDefaults()`, `resetToDefaults()`, `setting.ts`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 76`** (3 nodes): `buildOvertimeKitChildren()`, `oa-overtime-kit-rule.ts`, `uniqueId()`
+- **Thin community `Community 77`** (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 77`** (3 nodes): `buildLeaveKitChildren()`, `oa-leave-kit-rule.ts`, `uniqueId()`
+- **Thin community `Community 78`** (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 79`** (3 nodes): `buildConfirmationKitChildren()`, `oa-confirmation-kit-rule.ts`, `uniqueId()`
+- **Thin community `Community 80`** (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 80`** (3 nodes): `buildResignationKitChildren()`, `oa-resignation-kit-rule.ts`, `uniqueId()`
+- **Thin community `Community 81`** (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 81`** (3 nodes): `copyTextToClipboard()`, `copyTextWithFallback()`, `copy-text.ts`
+- **Thin community `Community 82`** (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 85`** (3 nodes): `getSourceProtocolAttachment()`, `normalizeAttachmentFiles()`, `protocol-source-attachment.ts`
+- **Thin community `Community 87`** (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 98`** (3 nodes): `emptyTabCounts()`, `useProductionWorkOrderTabStepCounts()`, `useProductionWorkOrderTabStepCounts.ts`
+- **Thin community `Community 99`** (3 nodes): `emptyTabCounts()`, `useProductionWorkOrderTabStepCounts()`, `useProductionWorkOrderTabStepCounts.ts`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 101`** (3 nodes): `initialProgress()`, `useQcWorkOrderAttachmentExport()`, `useQcWorkOrderAttachmentExport.ts`
+- **Thin community `Community 102`** (3 nodes): `initialProgress()`, `useQcWorkOrderAttachmentExport()`, `useQcWorkOrderAttachmentExport.ts`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 102`** (3 nodes): `emptyTabCounts()`, `useQcWorkOrderTabUnfinishedCounts()`, `useQcWorkOrderTabUnfinishedCounts.ts`
+- **Thin community `Community 103`** (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 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 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._
+- **Why does `push()` connect `Community 1` to `Community 0`, `Community 2`, `Community 3`, `Community 5`, `Community 6`, `Community 7`, `Community 9`, `Community 11`, `Community 12`, `Community 13`, `Community 15`, `Community 16`, `Community 17`, `Community 18`, `Community 20`, `Community 21`, `Community 22`, `Community 23`, `Community 26`, `Community 27`, `Community 29`, `Community 30`, `Community 31`, `Community 33`, `Community 45`, `Community 46`, `Community 49`?**
+  _High betweenness centrality (0.327) - this node is a cross-community bridge._
+- **Why does `get()` connect `Community 0` to `Community 1`, `Community 2`, `Community 5`, `Community 6`, `Community 7`, `Community 8`, `Community 9`, `Community 10`, `Community 11`, `Community 12`, `Community 13`, `Community 14`, `Community 15`, `Community 16`, `Community 17`, `Community 19`, `Community 20`, `Community 23`, `Community 24`, `Community 27`, `Community 28`, `Community 29`, `Community 30`, `Community 32`, `Community 33`, `Community 34`, `Community 41`, `Community 44`, `Community 47`, `Community 60`?**
+  _High betweenness centrality (0.260) - this node is a cross-community bridge._
+- **Why does `handleDynamicRoutes()` connect `Community 4` to `Community 32`, `Community 33`, `Community 3`, `Community 5`, `Community 16`, `Community 27`?**
+  _High betweenness centrality (0.035) - this node is a cross-community bridge._
+- **Are the 318 inferred relationships involving `get()` (e.g. with `resolveStorageSlotRangeFill()` and `buildNodeCodePathFromFlat()`) actually correct?**
+  _`get()` has 318 INFERRED edges - model-reasoned connections that need verification._
 - **Are the 127 inferred relationships involving `push()` (e.g. with `refreshQcMenuBadges()` and `collectLeafFacilityNodesInTreeOrder()`) actually correct?**
   _`push()` has 127 INFERRED edges - model-reasoned connections that need verification._
 - **Are the 28 inferred relationships involving `emit()` (e.g. with `handleSubmit()` and `handleDialogClose()`) actually correct?**

File diff suppressed because it is too large
+ 1 - 1
web-admin/graphify-out/graph.html


File diff suppressed because it is too large
+ 112 - 112
web-admin/graphify-out/graph.json


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

@@ -8,7 +8,7 @@
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/directives/core/auth.ts": 1781272556.3503833,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/directives/business/ripple.ts": 1781272556.3509357,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/directives/business/highlight.ts": 1781272556.3516848,
-  "/Users/wuzilong/work/00code/zkxw/web-admin/src/types/api.d.ts": 1785912860.2590215,
+  "/Users/wuzilong/work/00code/zkxw/web-admin/src/types/api.d.ts": 1789702283.3593416,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/types/index.ts": 1781272556.36091,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/types/config/index.ts": 1781272556.3566036,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/types/component/chart.ts": 1781272556.3575494,
@@ -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": 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/import/components.d.ts": 1789708405.9863644,
+  "/Users/wuzilong/work/00code/zkxw/web-admin/src/types/import/auto-imports.d.ts": 1789711366.3105218,
   "/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,
@@ -410,7 +410,7 @@
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/chinaRegion.ts": 1781272556.6606348,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/storage-entry-sms-notification.ts": 1781272556.660898,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/business.ts": 1781750847.0646322,
-  "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/admin.ts": 1785663707.7250297,
+  "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/admin.ts": 1789702188.9523969,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/inbound-sample.ts": 1782889889.452178,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/ethnic.ts": 1781272556.6658764,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/research-donation-sample-collection.ts": 1781272556.6660974,
@@ -433,9 +433,9 @@
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/system.ts": 1782802585.0325377,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/material.ts": 1782894693.658258,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/sample-reception.ts": 1782719389.581961,
-  "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/auth.ts": 1782122651.4628098,
+  "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/auth.ts": 1789711364.4677992,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/qc-work-order.ts": 1787193626.7833252,
-  "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/auth/login/index.vue": 1782893854.347133,
+  "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/auth/login/index.vue": 1789711416.2312732,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/refund-confirm/index.vue": 1782893854.3402505,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/my-material-usage/index.vue": 1782893854.311591,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/index.vue": 1788315818.1102333,
@@ -723,7 +723,7 @@
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/basic-info/insurance-company/index.vue": 1782893851.6212523,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/basic-info/collection-protocol-no/index.vue": 1782893851.573797,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/basic-info/customer/index.vue": 1783326449.0455372,
-  "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/basic-info/collection-region/index.vue": 1783326449.0480328,
+  "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/basic-info/collection-region/index.vue": 1789702500.075,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/basic-info/collection-region/CollectionProtocolNoTab.vue": 1783326449.0493598,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/assets/svg/loading.ts": 1781272558.486104,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/store/index.ts": 1781272559.227605,
@@ -745,7 +745,7 @@
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/router/core/RoutePermissionValidator.ts": 1781272559.4235668,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/router/core/index.ts": 1781272559.424284,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/router/core/IframeRouteManager.ts": 1781272559.425215,
-  "/Users/wuzilong/work/00code/zkxw/web-admin/src/router/modules/sqxp.ts": 1787887334.7914455,
+  "/Users/wuzilong/work/00code/zkxw/web-admin/src/router/modules/sqxp.ts": 1789696342.9507763,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/router/modules/index.ts": 1781272559.5419338,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/router/modules/exception.ts": 1781272559.549901,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/router/routes/staticRoutes.ts": 1781272559.596198,

Some files were not shown because too many files changed in this diff