This commit is contained in:
zlwu committed 2026-09-24 09:59:26 +08:00
1 parent 8248e5ef15
commit b9b4cc2673
57 files changed
+42800 -30473

No files matched your search

+111 -90
View File
@@ -7,6 +7,7 @@ from app.core.response import success_response
router = APIRouter(prefix="/web/menu", tags=["菜单管理"])
# 与 web-admin/src/router/modules/sqxp.ts 一致:5 个一级菜单分组
# 市场二级顺序:细胞存储、科研捐赠、市场基础信息、中试服务、基础实验
STATIC_MENU_TREE = [
# ────────────────────────────────────────────
# 1. 市场 (menu:market)
@@ -76,6 +77,7 @@ STATIC_MENU_TREE = [
# },
],
},
# 科研捐赠
{
"id": "RSC00000000000000000000000000001",
@@ -112,6 +114,106 @@ STATIC_MENU_TREE = [
},
],
},
# 市场基础信息(前段:存储/捐赠相关;后段:中试/基础实验相关)
{
"id": "MKT00000000000000000000000000001",
"name": "MarketInfo",
"path": "market-info",
"component": "",
"meta": {
"title": "市场基础信息",
"icon": "ri:price-tag-3-line",
"keepAlive": False,
"roles": ["admin"],
},
"children": [
{
"id": "122",
"name": "BizCellSampleType",
"path": "cell-sample-type",
"component": "/basic-info/cell-sample-type/index",
"meta": {"title": "细胞样本基础信息维护", "keepAlive": True, "menuCode": "biz:cellSampleType:list"},
},
{
"id": "123",
"name": "BizCollectionRegion",
"path": "collection-region",
"component": "/basic-info/collection-region/index",
"meta": {"title": "采集地区及医院", "keepAlive": True, "menuCode": "biz:collectionRegion:list"},
},
{
"id": "40000000000000000000000000000007",
"name": "BasicInfoInsuranceCompany",
"path": "insurance-company",
"component": "/basic-info/insurance-company/index",
"meta": {"title": "保险公司及险种价格", "keepAlive": True, "menuCode": "basic-info:insurance-company:list"},
},
{
"id": "50000000000000000000000000000009",
"name": "MarketMaterial",
"path": "material",
"component": "/cell-storage/material/index",
"meta": {"title": "市场物料库", "keepAlive": True, "menuCode": "biz:material:list"},
},
{
"id": "95000000000000000000000000000003",
"name": "MarketMyMaterialUsage",
"path": "my-material-usage",
"component": "/cell-storage/my-material-usage/index",
"meta": {
"title": "我的物料领用记录",
"keepAlive": True,
"menuCode": "market:material-usage:list",
},
},
{
"id": "121",
"name": "BizCustomer",
"path": "customer",
"component": "/basic-info/customer/index",
"meta": {"title": "客户管理", "keepAlive": True, "menuCode": "biz:customer:list"},
},
{
"id": "80000000000000000000000000000002",
"name": "CellProductType",
"path": "cell-product-type",
"component": "/pilot-service/cell-product-type/index",
"meta": {"title": "细胞产品基础信息维护", "keepAlive": True, "menuCode": "pilot-service:cell-product-type:list"},
},
{
"id": "80000000000000000000000000000007",
"name": "CheckupInstitution",
"path": "checkup-institution",
"component": "/pilot-service/checkup-institution/index",
"meta": {"title": "体检机构及项目价格", "keepAlive": True, "menuCode": "pilot-service:checkup-institution:list"},
},
{
"id": "80000000000000000000000000000006",
"name": "Channel",
"path": "channel",
"component": "/pilot-service/channel/index",
"meta": {"title": "渠道信息管理", "keepAlive": True, "menuCode": "pilot-service:channel:list"},
},
{
"id": "80000000000000000000000000000009",
"name": "ChannelServiceOrderFinance",
"path": "channel-service-order-finance",
"component": "/pilot-service/channel-service-order-finance/index",
"meta": {"title": "渠道服务订单(财务确认)", "keepAlive": True, "menuCode": "pilot-service:channel-service-order-finance:list"},
}
],
},
# 中试服务
{
"id": "PIL00000000000000000000000000001",
@@ -141,6 +243,7 @@ STATIC_MENU_TREE = [
},
],
},
# 基础实验
{
"id": "BEX00000000000000000000000000001",
@@ -188,96 +291,7 @@ STATIC_MENU_TREE = [
},
},
],
},
# 市场基础信息
{
"id": "MKT00000000000000000000000000001",
"name": "MarketInfo",
"path": "market-info",
"component": "",
"meta": {
"title": "市场基础信息",
"icon": "ri:price-tag-3-line",
"keepAlive": False,
"roles": ["admin"],
},
"children": [
{
"id": "122",
"name": "BizCellSampleType",
"path": "cell-sample-type",
"component": "/basic-info/cell-sample-type/index",
"meta": {"title": "细胞样本基础信息维护", "keepAlive": True, "menuCode": "biz:cellSampleType:list"},
},
{
"id": "123",
"name": "BizCollectionRegion",
"path": "collection-region",
"component": "/basic-info/collection-region/index",
"meta": {"title": "细胞采集基础信息维护", "keepAlive": True, "menuCode": "biz:collectionRegion:list"},
},
{
"id": "80000000000000000000000000000002",
"name": "CellProductType",
"path": "cell-product-type",
"component": "/pilot-service/cell-product-type/index",
"meta": {"title": "细胞产品基础信息维护", "keepAlive": True, "menuCode": "pilot-service:cell-product-type:list"},
},
{
"id": "40000000000000000000000000000007",
"name": "BasicInfoInsuranceCompany",
"path": "insurance-company",
"component": "/basic-info/insurance-company/index",
"meta": {"title": "保险公司及险种价格", "keepAlive": True, "menuCode": "basic-info:insurance-company:list"},
},
{
"id": "80000000000000000000000000000007",
"name": "CheckupInstitution",
"path": "checkup-institution",
"component": "/pilot-service/checkup-institution/index",
"meta": {"title": "体检机构及项目价格", "keepAlive": True, "menuCode": "pilot-service:checkup-institution:list"},
},
{
"id": "50000000000000000000000000000009",
"name": "MarketMaterial",
"path": "material",
"component": "/cell-storage/material/index",
"meta": {"title": "市场物料库", "keepAlive": True, "menuCode": "biz:material:list"},
},
{
"id": "95000000000000000000000000000003",
"name": "MarketMyMaterialUsage",
"path": "my-material-usage",
"component": "/cell-storage/my-material-usage/index",
"meta": {
"title": "我的物料领用记录",
"keepAlive": True,
"menuCode": "market:material-usage:list",
},
},
{
"id": "121",
"name": "BizCustomer",
"path": "customer",
"component": "/basic-info/customer/index",
"meta": {"title": "客户管理", "keepAlive": True, "menuCode": "biz:customer:list"},
},
{
"id": "80000000000000000000000000000006",
"name": "Channel",
"path": "channel",
"component": "/pilot-service/channel/index",
"meta": {"title": "渠道信息管理", "keepAlive": True, "menuCode": "pilot-service:channel:list"},
},
{
"id": "80000000000000000000000000000009",
"name": "ChannelServiceOrderFinance",
"path": "channel-service-order-finance",
"component": "/pilot-service/channel-service-order-finance/index",
"meta": {"title": "渠道服务订单(财务确认)", "keepAlive": True, "menuCode": "pilot-service:channel-service-order-finance:list"},
},
],
},
}
],
},
@@ -944,6 +958,13 @@ STATIC_MENU_TREE = [
"component": "/oa-approval/cc/index",
"meta": {"title": "抄送我的", "keepAlive": True, "menuCode": "oa:cc"},
},
{
"id": "OA000000000000000000000000000110",
"name": "OaApprovalMention",
"path": "mentions",
"component": "/oa-approval/mentions/index",
"meta": {"title": "提到我的", "keepAlive": True, "menuCode": "oa:mention"},
},
{
"id": "OA000000000000000000000000000201",
"name": "OaApprovalDesigner",
+3
View File
@@ -103,6 +103,8 @@ def _mobile_menu_icon(path: str) -> str:
return "oa-mine"
if "oa/cc" in p:
return "oa-cc"
if "oa/mentions" in p:
return "oa-mentions"
if "oa/wechat-notify" in p:
return "oa-wechat-notify"
if "form-sink" in p:
@@ -123,6 +125,7 @@ def _mobile_menu_icon_colors(path: str) -> dict:
"oa/done",
"oa/mine",
"oa/cc",
"oa/mentions",
"oa/wechat-notify",
)
):
+3 -1
View File
@@ -766,10 +766,11 @@ class OaTransferBody(BaseModel):
class OaCommentCreateBody(BaseModel):
model_config = ConfigDict(populate_by_name=True)
content: str
content: Optional[str] = ""
attachment_refs: Optional[List[Dict[str, Any]]] = Field(
None, alias="attachmentRefs"
)
mention_user_ids: Optional[List[str]] = Field(None, alias="mentionUserIds")
class OaResubmitBody(BaseModel):
@@ -1069,6 +1070,7 @@ async def create_oa_instance_comment(
dept_id=user.get("dept_id") or "",
content=body.content,
attachment_refs=body.attachment_refs,
mention_user_ids=body.mention_user_ids,
)
return success_response(data)
except OaEngineError as e:
+23
View File
@@ -993,6 +993,7 @@ class OaInstanceComment(Base):
attachment_refs_json = Column(Text, nullable=True, comment="引用表单附件快照 JSON")
status = Column(String(16), nullable=False, default="normal", comment="normal/revoked")
revoked_time = Column(DateTime, nullable=True, comment="撤回时间")
mention_user_ids_json = Column(Text, nullable=True, comment="评论@用户ID列表 JSON")
tenant_no = Column(String(64), nullable=False, default="zkxw", comment="租户编号")
order_num = Column(Integer, nullable=True, comment="排序号")
remark = Column(String(512), nullable=True, comment="备注")
@@ -1004,6 +1005,28 @@ class OaInstanceComment(Base):
del_flag = Column(SmallInteger, nullable=False, default=0, comment="删除标记")
class OaInstanceViewGrant(Base):
"""OA审批实例查看授权(评论@产生,不写入抄送任务)"""
__tablename__ = "oa_instance_view_grant"
id = Column(String(64), primary_key=True, comment="主键ID")
instance_id = Column(String(64), nullable=False, comment="实例ID")
user_id = Column(String(64), nullable=False, comment="被授权查看人")
source = Column(String(32), nullable=False, default="comment_mention", comment="comment_mention")
comment_id = Column(String(64), nullable=True, comment="来源评论ID,撤回评论不删除授权")
granted_by = Column(String(64), nullable=True, comment="授权人")
tenant_no = Column(String(64), nullable=False, default="zkxw", comment="租户编号")
order_num = Column(Integer, nullable=True, comment="排序号")
remark = Column(String(512), nullable=True, comment="备注")
create_by = Column(String(64), nullable=True, comment="创建人id")
create_dept_id = Column(String(64), nullable=False, default="", comment="创建人部门ID")
create_time = Column(DateTime, nullable=False, default=datetime.now, comment="创建时间")
update_by = Column(String(64), nullable=True, comment="更新人id")
update_time = Column(DateTime, nullable=False, default=datetime.now, onupdate=datetime.now, comment="更新时间")
del_flag = Column(SmallInteger, nullable=False, default=0, comment="删除标记")
class OaRelation(Base):
"""OA关联审批 oa_relation"""
+31
View File
@@ -2866,6 +2866,37 @@ def list_tasks(
"total": int(total),
}
if kind == "mention":
from app.services.oa_instance_comment import list_mentioned_instance_ids
ids = list_mentioned_instance_ids(db, tenant_no=tenant_no, user_id=user_id)
if not ids:
return {"list": [], "total": 0}
q = db.query(OaInstance).filter(
OaInstance.tenant_no == tenant_no,
OaInstance.del_flag == 0,
OaInstance.id.in_(ids),
)
q, _, _ = _apply_instance_list_filters(
q,
app_name=app_name,
initiator_name=initiator_name,
keyword=keyword,
apply_time_from=apply_time_from,
apply_time_to=apply_time_to,
instance_status=instance_status,
)
insts = q.all()
rank = {iid: i for i, iid in enumerate(ids)}
insts.sort(key=lambda inst: rank.get(inst.id, len(ids)))
total = len(insts)
insts = insts[offset : offset + page_size]
rows = [_instance_to_dict(i) for i in insts]
return {
"list": _enrich_list_rows(db, tenant_no=tenant_no, rows=rows, instances=insts),
"total": int(total),
}
if kind == "pending":
q = db.query(OaTask).filter(
OaTask.tenant_no == tenant_no,
+239 -10
View File
@@ -8,7 +8,13 @@ from typing import Any, Dict, List, Optional
from sqlalchemy.orm import Session
from app.core.utils import generate_uuid
from app.models.models import OaAppVersion, OaInstance, OaInstanceComment
from app.models.models import (
OaAppVersion,
OaInstance,
OaInstanceComment,
OaInstanceViewGrant,
SysUser,
)
from app.services.oa_engine import OaEngineError, _get_instance, _loads, _user_name_map
from app.services.oa_related_approval import (
UPLOAD_TYPES,
@@ -18,6 +24,7 @@ from app.services.oa_related_approval import (
)
CONTENT_MAX = 1024
MENTION_MAX = 20
def enumerate_form_attachments(
@@ -77,10 +84,46 @@ def _allowed_url_index(
return index
def _attachment_url_candidates(url: str) -> List[str]:
raw = (url or "").strip()
path = raw.split("?", 1)[0].strip()
found: List[str] = []
for item in (raw, path, path.lstrip("/")):
if item and item not in found:
found.append(item)
try:
from app.core.config import settings
base = (getattr(settings, "MINIO_BASE_URL", None) or "").rstrip("/")
except Exception:
base = ""
if base and path.startswith(base + "/"):
rel = path[len(base) + 1 :]
if rel and rel not in found:
found.append(rel)
return found
def _find_uploaded_attachment(db: Session, tenant_no: str, url: str):
from app.models.models import BizAttachment
q = db.query(BizAttachment).filter(
BizAttachment.del_flag == 0,
BizAttachment.tenant_no == tenant_no,
)
for candidate in _attachment_url_candidates(url):
row = q.filter(BizAttachment.file_url == candidate).first()
if row is not None:
return row
return None
def normalize_attachment_refs(
schema: Any,
form_data: Optional[Dict[str, Any]],
refs: Optional[List[Dict[str, Any]]],
db: Optional[Session] = None,
tenant_no: str = "",
) -> List[Dict[str, str]]:
if not refs:
return []
@@ -93,14 +136,37 @@ def normalize_attachment_refs(
if not isinstance(raw, dict):
raise OaEngineError("引用的附件不存在或不属于本表单")
url = str(raw.get("url") or "").strip()
if not url or url not in index:
raise OaEngineError("引用的附件不存在或不属于本表单")
if url in seen:
source = str(raw.get("source") or "form").strip() or "form"
if not url or url in seen:
if not url:
raise OaEngineError("引用的附件不存在或不属于本表单")
continue
if source == "upload":
if db is None:
raise OaEngineError("上传的附件不存在")
row = _find_uploaded_attachment(db, tenant_no, url)
if row is None:
raise OaEngineError("上传的附件不存在")
seen.add(url)
out.append(
{
"source": "upload",
"fieldKey": "",
"fieldLabel": "",
"url": str(row.file_url or url),
"fileName": str(row.original_name or "").strip() or url,
}
)
continue
if source != "form":
raise OaEngineError("attachmentRefs 格式无效")
if url not in index:
raise OaEngineError("引用的附件不存在或不属于本表单")
seen.add(url)
meta = index[url]
out.append(
{
"source": "form",
"fieldKey": meta["fieldKey"],
"fieldLabel": meta["fieldLabel"],
"url": url,
@@ -114,6 +180,25 @@ def _fmt_time(dt: Optional[datetime]) -> str:
return dt.strftime("%Y-%m-%d %H:%M:%S") if dt else ""
def _mention_ids(row: OaInstanceComment) -> List[str]:
raw = _loads(getattr(row, "mention_user_ids_json", None), [])
if not isinstance(raw, list):
return []
out: List[str] = []
for item in raw:
uid = str(item or "").strip()
if uid and uid not in out:
out.append(uid)
return out
def _mentions_payload(row: OaInstanceComment, name_map: Dict[str, str]) -> List[Dict[str, str]]:
return [
{"userId": uid, "userName": name_map.get(uid, uid)}
for uid in _mention_ids(row)
]
def _comment_to_dict(
row: OaInstanceComment,
*,
@@ -141,6 +226,7 @@ def _comment_to_dict(
"createByName": name_map.get(row.create_by or "", row.create_by or ""),
"createTime": _fmt_time(row.create_time),
"canRevoke": can_revoke,
"mentions": _mentions_payload(row, name_map),
}
@@ -173,7 +259,10 @@ def list_comments(
.order_by(OaInstanceComment.create_time.asc())
.all()
)
name_map = _user_name_map(db, [r.create_by for r in rows if r.create_by])
name_ids = [r.create_by for r in rows if r.create_by]
for r in rows:
name_ids.extend(_mention_ids(r))
name_map = _user_name_map(db, name_ids)
return [
_comment_to_dict(
r,
@@ -196,6 +285,129 @@ def list_form_attachments(
return enumerate_form_attachments(schema, form)
def normalize_mention_user_ids(
db: Session,
*,
tenant_no: str,
actor_user_id: str,
user_ids: Optional[List[str]],
) -> List[str]:
if not user_ids:
return []
if not isinstance(user_ids, list):
raise OaEngineError("mentionUserIds 格式无效")
actor = (actor_user_id or "").strip()
seen: List[str] = []
for raw in user_ids:
uid = str(raw or "").strip()
if not uid or uid in seen or uid == actor:
continue
seen.append(uid)
if len(seen) > MENTION_MAX:
raise OaEngineError(f"一次最多@{MENTION_MAX}人")
if not seen:
return []
rows = (
db.query(SysUser.id)
.filter(
SysUser.id.in_(seen),
SysUser.tenant_no == tenant_no,
SysUser.del_flag == 0,
SysUser.status == "1",
)
.all()
)
found = {str(r[0]) for r in rows}
if any(uid not in found for uid in seen):
raise OaEngineError("提及的用户不存在或已停用")
return seen
def _ensure_view_grants(
db: Session,
*,
instance_id: str,
tenant_no: str,
user_ids: List[str],
comment_id: str,
actor_user_id: str,
dept_id: str,
) -> None:
if not user_ids:
return
existing = (
db.query(OaInstanceViewGrant.user_id)
.filter(
OaInstanceViewGrant.instance_id == instance_id,
OaInstanceViewGrant.tenant_no == tenant_no,
OaInstanceViewGrant.del_flag == 0,
OaInstanceViewGrant.user_id.in_(user_ids),
)
.all()
)
have = {str(r[0]) for r in existing}
now = datetime.now()
for uid in user_ids:
if uid in have:
continue
db.add(
OaInstanceViewGrant(
id=generate_uuid(),
instance_id=instance_id,
user_id=uid,
source="comment_mention",
comment_id=comment_id,
granted_by=actor_user_id,
tenant_no=tenant_no,
create_by=actor_user_id,
create_dept_id=dept_id or "",
create_time=now,
update_by=actor_user_id,
update_time=now,
del_flag=0,
)
)
def list_mentioned_instance_ids(
db: Session, *, tenant_no: str, user_id: str
) -> List[str]:
"""评论@授权的实例。被@即出现在「提到我的」,不因发起人/审批人/抄送身份而隐藏。"""
uid = (user_id or "").strip()
if not uid:
return []
grants = (
db.query(OaInstanceViewGrant)
.filter(
OaInstanceViewGrant.tenant_no == tenant_no,
OaInstanceViewGrant.user_id == uid,
OaInstanceViewGrant.del_flag == 0,
)
.order_by(OaInstanceViewGrant.create_time.desc(), OaInstanceViewGrant.id.desc())
.all()
)
inst_ids: List[str] = []
seen = set()
for g in grants:
iid = (g.instance_id or "").strip()
if iid and iid not in seen:
seen.add(iid)
inst_ids.append(iid)
if not inst_ids:
return []
allowed = {
str(r[0])
for r in db.query(OaInstance.id)
.filter(
OaInstance.tenant_no == tenant_no,
OaInstance.del_flag == 0,
OaInstance.id.in_(inst_ids),
)
.all()
}
return [iid for iid in inst_ids if iid in allowed]
def create_comment(
db: Session,
*,
@@ -205,26 +417,34 @@ def create_comment(
dept_id: str,
content: str,
attachment_refs: Optional[List[Dict[str, Any]]],
mention_user_ids: Optional[List[str]] = None,
) -> Dict[str, Any]:
inst = _get_instance(db, instance_id, tenant_no)
if (inst.status or "") != "running":
raise OaEngineError("仅审批进行中可评论")
text = (content or "").strip()
if not text:
raise OaEngineError("评论内容不能为空")
if len(text) > CONTENT_MAX:
raise OaEngineError(f"评论内容不能超过{CONTENT_MAX}字")
schema = _load_version_schema(db, inst)
form = _loads(inst.form_data_json, {})
if not isinstance(form, dict):
form = {}
refs = normalize_attachment_refs(schema, form, attachment_refs)
refs = normalize_attachment_refs(schema, form, attachment_refs, db=db, tenant_no=tenant_no)
mentions = normalize_mention_user_ids(
db,
tenant_no=tenant_no,
actor_user_id=user_id,
user_ids=mention_user_ids,
)
if not text and not mentions and not refs:
raise OaEngineError("请填写评论、@人员或添加附件")
now = datetime.now()
row = OaInstanceComment(
id=generate_uuid(),
instance_id=instance_id,
content=text,
attachment_refs_json=json.dumps(refs, ensure_ascii=False) if refs else None,
mention_user_ids_json=json.dumps(mentions, ensure_ascii=False) if mentions else None,
status="normal",
revoked_time=None,
tenant_no=tenant_no,
@@ -236,9 +456,18 @@ def create_comment(
del_flag=0,
)
db.add(row)
_ensure_view_grants(
db,
instance_id=instance_id,
tenant_no=tenant_no,
user_ids=mentions,
comment_id=row.id,
actor_user_id=user_id,
dept_id=dept_id or "",
)
db.commit()
db.refresh(row)
name_map = _user_name_map(db, [user_id])
name_map = _user_name_map(db, [user_id, *mentions])
return _comment_to_dict(
row,
name_map=name_map,
@@ -281,7 +510,7 @@ def revoke_comment(
db.add(row)
db.commit()
db.refresh(row)
name_map = _user_name_map(db, [row.create_by] if row.create_by else [])
name_map = _user_name_map(db, [row.create_by, *_mention_ids(row)] if row.create_by else _mention_ids(row))
return _comment_to_dict(
row,
name_map=name_map,
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+137 -95
View File
@@ -1,11 +1,11 @@
# Graph Report - app (2026-09-18)
# Graph Report - app (2026-09-21)
## Corpus Check
- 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.
- Large corpus: 266 files · ~321,223 words. Semantic extraction will be expensive (many Claude tokens). Consider running on a subfolder, or use --no-semantic to run AST-only.
## Summary
- 5508 nodes · 30303 edges · 40 communities detected
- Extraction: 31% EXTRACTED · 69% INFERRED · 0% AMBIGUOUS · INFERRED: 20768 edges (avg confidence: 0.6)
- 5538 nodes · 30525 edges · 48 communities detected
- Extraction: 31% EXTRACTED · 69% INFERRED · 0% AMBIGUOUS · INFERRED: 20936 edges (avg confidence: 0.6)
- Token cost: 0 input · 0 output
## Community Hubs (Navigation)
@@ -43,30 +43,38 @@
- [[_COMMUNITY_Community 31|Community 31]]
- [[_COMMUNITY_Community 32|Community 32]]
- [[_COMMUNITY_Community 33|Community 33]]
- [[_COMMUNITY_Community 34|Community 34]]
- [[_COMMUNITY_Community 35|Community 35]]
- [[_COMMUNITY_Community 36|Community 36]]
- [[_COMMUNITY_Community 37|Community 37]]
- [[_COMMUNITY_Community 38|Community 38]]
- [[_COMMUNITY_Community 39|Community 39]]
- [[_COMMUNITY_Community 40|Community 40]]
- [[_COMMUNITY_Community 41|Community 41]]
- [[_COMMUNITY_Community 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]]
## God Nodes (most connected - your core abstractions)
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
1. `success_response()` - 774 edges
2. `error_response()` - 643 edges
3. `SysUser` - 616 edges
4. `BizQcTaskResult` - 358 edges
5. `BizProductionMaterial` - 342 edges
6. `BizProductionMaterialUsage` - 335 edges
7. `BizQcMaterialUsage` - 333 edges
8. `BizQcMaterial` - 323 edges
9. `BizProductionMaterialStockBatch` - 322 edges
10. `BizMaterialUsage` - 319 edges
## Surprising Connections (you probably didn't know these)
- `统一记录 API 请求/响应出入参的中间件` --uses--> `SysApiRequestLog` [INFERRED]
core/api_request_log_middleware.py → models/models.py
- `sync_lab_storage_seq_after_use()` --calls--> `generate_uuid()` [INFERRED]
crud/sample_reception_lab_code.py → core/utils.py
- `get_menu_list()` --calls--> `success_response()` [INFERRED]
api/menu.py → core/response.py
- `微信小程序登录业务:openid + 手机号一致性校验。` --uses--> `SysUser` [INFERRED]
services/wechat_login_service.py → models/models.py
- `已绑定 openid 时静默换票(无需手机号授权)。` --uses--> `SysUser` [INFERRED]
@@ -78,206 +86,240 @@
### Community 0 - "Community 0"
Cohesion: 0.01
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)
Nodes (1054): attachment_download_url(), create_dept(), create_dict_data(), create_dict_type(), create_express_company(), create_item_unit(), create_menu(), create_post() (+1046 more)
### Community 1 - "Community 1"
Cohesion: 0.01
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)
Nodes (754): generate_uuid(), _apply_cell_product_status_change(), create_cell_product(), create_cell_products_batch(), _validate_batch_items(), create_cell_sample_type(), get_cell_sample_type(), list_cell_sample_types() (+746 more)
### Community 2 - "Community 2"
Cohesion: 0.01
Nodes (621): AttachmentDownloadUrlBody, DeptCreateBody, DeptUpdateBody, DictDataCreateBody, DictDataUpdateBody, DictTypeCreateBody, DictTypeUpdateBody, ExpressCompanyCreateBody (+613 more)
Nodes (649): AttachmentDownloadUrlBody, DeptCreateBody, DeptUpdateBody, DictDataCreateBody, DictDataUpdateBody, DictTypeCreateBody, DictTypeUpdateBody, ExpressCompanyCreateBody (+641 more)
### Community 3 - "Community 3"
Cohesion: 0.07
Nodes (451): 保险公司及险种价格管理 API(保险公司 + 开票信息 + 险种价格), 审批意见:前端传 commentText,兼容 comment。, TaskActionBody, Base, 采集协议单 / 审核 / 缴费明细 CRUD(细胞存储 2.2.6~2.2.10), 返回已在采集协议单或科研捐赠单样本表中占用的样本编码集合(同租户、未删除)。, 创建/更新样本行前校验:样本编码不得已被任一业务单占用(转移配对单豁免)。, 缴费明细行 → 前端字典(含协议、客户、转账附件)。 (+443 more)
Cohesion: 0.06
Nodes (567): 环境检测 — 检测项维护 + 登记管理 API, 保险公司及险种价格管理 API(保险公司 + 开票信息 + 险种价格), AccessLogCreateBody, AccessLogUpdateBody, CleanRoomCreateBody, CleanRoomUpdateBody, ExhibitionAppointmentCreateBody, ExhibitionAppointmentUpdateBody (+559 more)
### Community 4 - "Community 4"
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)
Nodes (320): _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(), ApiRequestLogCRUD (+312 more)
### Community 5 - "Community 5"
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)
Cohesion: 0.01
Nodes (323): get_production_reception_by_sample_code(), eq_unicode(), like_unicode(), MySQL 字符串比较/模糊匹配时统一排序规则,避免 Illegal mix of collations。, 跨表字符串等值比较(JOIN / WHERE)时统一为 utf8mb4_unicode_ci。, 将列显式设为 utf8mb4_unicode_ci。, unicode_ci(), build_void_type_label_map() (+315 more)
### Community 6 - "Community 6"
Cohesion: 0.02
Nodes (296): list_ethnics_for_select(), AccessLogCreateBody, AccessLogUpdateBody, CleanRoomCreateBody, CleanRoomUpdateBody, create_access_log(), create_uv_disinfect_log(), delete_access_log() (+288 more)
Nodes (236): 截止日期:仅日期时扩到当天 23:59:59。, 发起前预览审批链(条件齐后解析最终路径与审批人)。, 兼容旧 GET:无 formData,含条件分支时多为待确定占位。, _app_to_dict(), _copied_app_name(), copy_app(), create_app(), _create_version() (+228 more)
### Community 7 - "Community 7"
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)
Nodes (147): get_designer_data(), _error_json(), lifespan(), 未捕获异常:统一 JSON 500,确保跨域场景下仍有 Access-Control-Allow-Origin, 数据库异常:返回 JSON 并保留 CORS 头,避免浏览器只报跨域错误, sqlalchemy_exception_handler(), unhandled_exception_handler(), BaseHTTPMiddleware (+139 more)
### Community 8 - "Community 8"
Cohesion: 0.02
Nodes (235): 截止日期:仅日期时扩到当天 23:59:59。, 发起前预览审批链(条件齐后解析最终路径与审批人)。, 兼容旧 GET:无 formData,含条件分支时多为待确定占位。, _app_to_dict(), _copied_app_name(), copy_app(), create_app(), _create_version() (+227 more)
Cohesion: 0.07
Nodes (206): CellSampleTypeCreateBody, CellSampleTypeUpdateBody, CollectionRegionCreateBody, CollectionRegionUpdateBody, CustomerCreateBody, CustomerUpdateBody, HospitalCreateBody, HospitalUpdateBody (+198 more)
### Community 9 - "Community 9"
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)
Nodes (143): mp_bind_status(), 小程序鉴权:微信登录 / 绑定 openid。, 当前用户是否已绑定微信服务号 openid。, 服务号网页授权 code 绑定当前用户(需 Bearer)。, 微信登录:openid 已绑定时可静默换票;否则须授权手机号且与账号 phone_no 一致。, 密码登录后绑定当前微信 openid(需 Bearer)。, _resolve_user_id(), web_qr_confirm() (+135 more)
### Community 10 - "Community 10"
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)
Cohesion: 0.04
Nodes (83): batch_delete_research_donations(), change_donation_status(), convert_from_storage(), convert_to_storage(), create_research_donation(), delete_research_donation(), _get_current_user_id(), get_research_donation_detail() (+75 more)
### Community 11 - "Community 11"
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)
Cohesion: 0.04
Nodes (85): format_date(), join_equipment_ids(), parse_equipment_ids(), 工艺/步骤设备多选:逗号分隔 ID 与名称快照解析, 校验设备列表,返回 (equipment_ids 逗号串, equipment_name 逗号串快照)。, resolve_equipment_list_snapshot(), _resolve_course_display(), 登记步骤物料使用;关联领用记录时不扣减库存。调用方控制事务,本函数不 commit。 (+77 more)
### Community 12 - "Community 12"
Cohesion: 0.04
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)
Cohesion: 0.05
Nodes (68): map_payments_by_schedule_ids(), create_customer(), get_customer(), id_card_taken(), list_customers(), 客户 CRUD(biz_customer), _row_to_dict(), soft_delete_customer() (+60 more)
### Community 13 - "Community 13"
Cohesion: 0.05
Nodes (71): aggregate_slot_stats_by_facility(), _apply_slot_select_base_filters(), _apply_slot_select_query_filters(), batch_create_facilities(), batch_create_slots(), _build_node_path_cache(), _build_slot_keyword_sql_filters(), clear_slots_by_facility_node() (+63 more)
### Community 14 - "Community 14"
Cohesion: 0.06
Nodes (66): complete_application(), create_basic_experiment_sale_schedules(), create_infusion_plan(), create_schedule(), _create_schedule_products(), _format_yyyy_mm_dd(), _generate_application_no(), get_application() (+58 more)
### Community 15 - "Community 15"
Cohesion: 0.09
Nodes (44): get_next_cell_product_code(), _ancestor_link_from_cell_product_input(), _ancestor_sample_chain_link(), _build_ancestor_chain(), _build_descendant_chain(), _build_facility_path_label(), build_split_product_codes(), create_cell_products_by_quantity() (+36 more)
### Community 16 - "Community 16"
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 14 - "Community 14"
### Community 17 - "Community 17"
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)
Nodes (10): 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, 小程序调起支付的二次签名参数(与公众号 JSAPI 规则一致), 统一下单 (V2 API) Args: out_trade_no: 商户订单号 tot, 生成签名 V3 (SHA256 with RSA), wechat_pay() (+2 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 18 - "Community 18"
Cohesion: 0.16
Nodes (12): apply_formulas_and_linkages(), eval_formula(), _extract_rules(), FormulaError, preprocess_formula(), OA 表单公式与数据联动(禁止任意 eval), 按 schema 中字段 props.formula / props.linkage 回填 form_data。 linkage 示例: { "when, 将 {field} 转为合法标识符引用 field。 (+4 more)
### Community 16 - "Community 16"
### Community 19 - "Community 19"
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"
### Community 20 - "Community 20"
Cohesion: 0.16
Nodes (21): _build_tree(), _cascade_soft_delete(), get_menu(), list_menus(), list_menus_tree(), _row_to_dict(), soft_delete_menu(), update_menu() (+13 more)
### Community 21 - "Community 21"
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)
### Community 22 - "Community 22"
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"
### Community 23 - "Community 23"
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 24 - "Community 24"
Cohesion: 0.27
Nodes (12): create_equipment(), _dec(), equipment_code_exists(), equipment_is_referenced(), get_equipment(), list_equipments(), _norm_status(), _parse_date() (+4 more)
### Community 20 - "Community 20"
### Community 25 - "Community 25"
Cohesion: 0.24
Nodes (9): 微信消息通知服务 支持微信小程序模板消息推送 使用方式: from app.core.wechat_message import Wech, 发送模板消息 Args: openid: 用户openid templ, send_delivery_notice(), send_delivery_warning(), send_order_status(), send_payment_success(), send_refund_result(), send_urge_notification() (+1 more)
### Community 26 - "Community 26"
Cohesion: 0.23
Nodes (12): Config, ProductBase, ProductCategoryBase, ProductCategoryCreate, ProductCategoryResponse, ProductCreate, ProductResponse, ProductUnitBase (+4 more)
### Community 27 - "Community 27"
Cohesion: 0.27
Nodes (12): _field_label(), _is_empty(), _is_required(), _parse_money(), OA 表单设计器:金额控件 form_data 校验, 校验 form_data 中金额控件的值格式,返回中文错误列表(空表示通过)。, 解析金额值。成功返回 (number, None);失败返回 (None, error_key)。, _rule_props() (+4 more)
### Community 28 - "Community 28"
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"
### Community 29 - "Community 29"
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"
### Community 30 - "Community 30"
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"
### Community 31 - "Community 31"
Cohesion: 0.31
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"
### Community 32 - "Community 32"
Cohesion: 0.29
Nodes (2): BaseModel, SoftDeleteModel
### Community 26 - "Community 26"
### Community 33 - "Community 33"
Cohesion: 0.38
Nodes (6): derive_legacy_status_from_three(), derive_three_status_from_legacy(), 阶段A兼容:从旧 status 推导三字段。 约束:旧 status 本身混合了支付/配送/订单生命周期,因此只能尽力推断; 若业务侧已有更精确, 阶段A兼容:从三字段反推旧 status(用于旧前端/旧筛选参数兼容)。, ThreeStatus, _to_decimal()
### Community 27 - "Community 27"
### Community 34 - "Community 34"
Cohesion: 0.4
Nodes (5): batch_matches_stock_status(), normalize_batch_stock_status(), 质检物料入库批次:按剩余数量过滤的语义辅助。, 纯函数判定(供单测);list_batches 用 SQL 等价条件。, 返回 'available' | 'exhausted';全部 / 空 / 非法 → None(不加剩余数量条件)。
### Community 28 - "Community 28"
### Community 35 - "Community 35"
Cohesion: 0.5
Nodes (3): BaseSettings, Config, Settings
### Community 29 - "Community 29"
### Community 36 - "Community 36"
Cohesion: 0.5
Nodes (2): _auto_fill_create_dept_id(), ORM before_insert 钩子:当 create_dept_id 为空时,自动从请求上下文填充当前用户部门 ID
### Community 30 - "Community 30"
### Community 37 - "Community 37"
Cohesion: 0.67
Nodes (2): get_menu_list(), 运营端菜单接口(与 web-admin 前端静态路由 sqxp.ts 对齐,5 个一级菜单分组)
### Community 38 - "Community 38"
Cohesion: 0.67
Nodes (1): 转正套件:入职日 + 试用期月数 → 建议转正日(YYYY-MM-DD)。
### Community 31 - "Community 31"
### Community 39 - "Community 39"
Cohesion: 1.0
Nodes (1): 质检物料金额查看授权:专用角色与菜单权限常量。
### Community 32 - "Community 32"
### Community 40 - "Community 40"
Cohesion: 1.0
Nodes (1): 请求级别的用户上下文变量 通过 contextvars 在请求处理链路中传递当前用户信息, 供 SQLAlchemy 事件监听器等底层机制使用,避免逐层手动传
### Community 33 - "Community 33"
### Community 41 - "Community 41"
Cohesion: 1.0
Nodes (1): API 路由包(骨架:公共鉴权 + 静态菜单 + 运营端占位)
### Community 36 - "Community 36"
### Community 44 - "Community 44"
Cohesion: 1.0
Nodes (1): 发送订单状态通知 Args: openid: 用户openid ord
### Community 37 - "Community 37"
### Community 45 - "Community 45"
Cohesion: 1.0
Nodes (1): 发送支付成功通知 Args: openid: 用户openid ord
### Community 38 - "Community 38"
### Community 46 - "Community 46"
Cohesion: 1.0
Nodes (1): 发送配送通知 Args: openid: 配送员openid task
### Community 39 - "Community 39"
### Community 47 - "Community 47"
Cohesion: 1.0
Nodes (1): 发送配送时效预警 Args: openid: 配送员openid ta
### Community 40 - "Community 40"
### Community 48 - "Community 48"
Cohesion: 1.0
Nodes (1): 发送退款结果通知 Args: openid: 用户openid ord
### Community 41 - "Community 41"
### Community 49 - "Community 49"
Cohesion: 1.0
Nodes (1): 发送催单通知给配送员 Args: deliver_openid: 配送员openid
## Knowledge Gaps
- **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 25`** (8 nodes): `BaseModel`, `.to_dict()`, `get_active()`, `base.py`, `SoftDeleteModel`, `.restore()`, `.soft_delete()`, `__tablename__()`
- **Thin community `Community 32`** (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`** (4 nodes): `_auto_fill_create_dept_id()`, `get_db()`, `database.py`, `ORM before_insert 钩子:当 create_dept_id 为空时,自动从请求上下文填充当前用户部门 ID`
- **Thin community `Community 36`** (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)。`
- **Thin community `Community 37`** (3 nodes): `get_menu_list()`, `menu.py`, `运营端菜单接口(与 web-admin 前端静态路由 sqxp.ts 对齐,5 个一级菜单分组)`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 31`** (2 nodes): `qc_material_amount_auth_constants.py`, `质检物料金额查看授权:专用角色与菜单权限常量。`
- **Thin community `Community 38`** (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.
- **Thin community `Community 32`** (2 nodes): `context.py`, `请求级别的用户上下文变量 通过 contextvars 在请求处理链路中传递当前用户信息, 供 SQLAlchemy 事件监听器等底层机制使用,避免逐层手动传`
- **Thin community `Community 39`** (2 nodes): `qc_material_amount_auth_constants.py`, `质检物料金额查看授权:专用角色与菜单权限常量。`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 33`** (2 nodes): `__init__.py`, `API 路由包(骨架:公共鉴权 + 静态菜单 + 运营端占位)`
- **Thin community `Community 40`** (2 nodes): `context.py`, `请求级别的用户上下文变量 通过 contextvars 在请求处理链路中传递当前用户信息, 供 SQLAlchemy 事件监听器等底层机制使用,避免逐层手动传`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 36`** (1 nodes): `发送订单状态通知 Args: openid: 用户openid ord`
- **Thin community `Community 41`** (2 nodes): `__init__.py`, `API 路由包(骨架:公共鉴权 + 静态菜单 + 运营端占位)`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 37`** (1 nodes): `发送支付成功通知 Args: openid: 用户openid ord`
- **Thin community `Community 44`** (1 nodes): `发送订单状态通知 Args: openid: 用户openid ord`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 38`** (1 nodes): `发送配送通知 Args: openid: 配送员openid task`
- **Thin community `Community 45`** (1 nodes): `发送支付成功通知 Args: openid: 用户openid ord`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 39`** (1 nodes): `发送配送时效预警 Args: openid: 配送员openid ta`
- **Thin community `Community 46`** (1 nodes): `发送配送通知 Args: openid: 配送员openid task`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 40`** (1 nodes): `发送退款结果通知 Args: openid: 用户openid ord`
- **Thin community `Community 47`** (1 nodes): `发送配送时效预警 Args: openid: 配送员openid ta`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 41`** (1 nodes): `发送催单通知给配送员 Args: deliver_openid: 配送员openid`
- **Thin community `Community 48`** (1 nodes): `发送退款结果通知 Args: openid: 用户openid ord`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 49`** (1 nodes): `发送催单通知给配送员 Args: deliver_openid: 配送员openid`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
## Suggested Questions
_Questions this graph is uniquely positioned to answer:_
- **Why does `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._
- **Why does `SysUser` connect `Community 2` to `Community 1`, `Community 3`, `Community 4`, `Community 6`, `Community 7`, `Community 8`, `Community 9`, `Community 10`, `Community 11`, `Community 12`, `Community 19`, `Community 28`, `Community 29`?**
_High betweenness centrality (0.128) - this node is a cross-community bridge._
- **Why does `success_response()` connect `Community 0` to `Community 2`, `Community 4`, `Community 37`, `Community 5`, `Community 7`, `Community 9`, `Community 10`, `Community 15`?**
_High betweenness centrality (0.046) - this node is a cross-community bridge._
- **Why does `generate_uuid()` connect `Community 1` to `Community 0`, `Community 3`, `Community 4`, `Community 5`, `Community 6`, `Community 7`, `Community 8`, `Community 9`, `Community 10`, `Community 11`, `Community 12`, `Community 13`, `Community 16`, `Community 19`, `Community 20`, `Community 22`, `Community 23`, `Community 24`, `Community 28`, `Community 29`, `Community 30`?**
_High betweenness centrality (0.034) - this node is a cross-community bridge._
- **Are the 941 inferred relationships involving `str` (e.g. with `unhandled_exception_handler()` and `get_current_user_by_token()`) actually correct?**
_`str` has 941 INFERRED edges - model-reasoned connections that need verification._
- **Are the 773 inferred relationships involving `success_response()` (e.g. with `list_storage_release_reviews()` and `get_storage_release_review_detail()`) actually correct?**
_`success_response()` has 773 INFERRED edges - model-reasoned connections that need verification._
- **Are the 642 inferred relationships involving `error_response()` (e.g. with `_json_error_response()` and `list_storage_release_reviews()`) actually correct?**
_`error_response()` has 642 INFERRED edges - model-reasoned connections that need verification._
- **Are the 614 inferred relationships involving `SysUser` (e.g. with `数据权限过滤(V0.3:基于角色 data_scope 的查询级行级过滤) data_scope 值: 1 = 全部数据 2 = 本部门数据 3` and `从 SysUser 读取用户的 dept_id。`) actually correct?**
_`SysUser` has 614 INFERRED edges - model-reasoned connections that need verification._
+17491 -14557
View File
File diff suppressed because it is too large. Load diff
+6 -5
View File
@@ -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": 1789701693.5477362,
"/Users/wuzilong/work/00code/zkxw/backend/app/models/models.py": 1789971994.638983,
"/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,
@@ -195,7 +195,7 @@
"/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,
"/Users/wuzilong/work/00code/zkxw/backend/app/api/mini_menu.py": 1789979771.8476667,
"/Users/wuzilong/work/00code/zkxw/backend/app/api/qc.py": 1787193588.7157009,
"/Users/wuzilong/work/00code/zkxw/backend/app/api/market.py": 1781522283.6823432,
"/Users/wuzilong/work/00code/zkxw/backend/app/api/sample_production_reception.py": 1788159565.164049,
@@ -210,9 +210,9 @@
"/Users/wuzilong/work/00code/zkxw/backend/app/api/pre_transit_sample.py": 1786938943.6929982,
"/Users/wuzilong/work/00code/zkxw/backend/app/api/qc_sms_notification.py": 1780326875.7886882,
"/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/oa_approval.py": 1789977632.0231144,
"/Users/wuzilong/work/00code/zkxw/backend/app/api/operations.py": 1785213144.2881503,
"/Users/wuzilong/work/00code/zkxw/backend/app/api/menu.py": 1789696372.4245293,
"/Users/wuzilong/work/00code/zkxw/backend/app/api/menu.py": 1789972063.9200244,
"/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,
@@ -229,7 +229,7 @@
"/Users/wuzilong/work/00code/zkxw/backend/app/api/protocol_sample_collection.py": 1780457788.2093296,
"/Users/wuzilong/work/00code/zkxw/backend/app/api/leave.py": 1779266168.8693042,
"/Users/wuzilong/work/00code/zkxw/backend/app/api/clinical_payment.py": 1781750841.074568,
"/Users/wuzilong/work/00code/zkxw/backend/app/services/oa_engine.py": 1787892839.3046124,
"/Users/wuzilong/work/00code/zkxw/backend/app/services/oa_engine.py": 1789972028.300875,
"/Users/wuzilong/work/00code/zkxw/backend/app/services/oa_overtime_hours.py": 1786373766.5113294,
"/Users/wuzilong/work/00code/zkxw/backend/app/services/workflow_callback.py": 1778727966.7277644,
"/Users/wuzilong/work/00code/zkxw/backend/app/services/qc_work_order_fee_summary.py": 1783329884.6125586,
@@ -251,6 +251,7 @@
"/Users/wuzilong/work/00code/zkxw/backend/app/services/oa_related_approval.py": 1787892816.3154016,
"/Users/wuzilong/work/00code/zkxw/backend/app/services/wechat_mp_message.py": 1786689383.1677845,
"/Users/wuzilong/work/00code/zkxw/backend/app/services/oa_leave_hours.py": 1785397787.0493405,
"/Users/wuzilong/work/00code/zkxw/backend/app/services/oa_instance_comment.py": 1790001317.4419823,
"/Users/wuzilong/work/00code/zkxw/backend/app/services/oa_resignation_kit_sync.py": 1785405020.285242,
"/Users/wuzilong/work/00code/zkxw/backend/app/services/wechat_mp_auth.py": 1786615330.0733685,
"/Users/wuzilong/work/00code/zkxw/backend/app/services/oa_assignee.py": 1785835523.951382,
+327
View File
@@ -64,6 +64,83 @@ class TestNormalizeAttachmentRefs(unittest.TestCase):
[{"fieldKey": "files", "url": "https://evil/x.pdf"}],
)
def test_marks_form_ref_source(self):
schema = {"rule": [{"type": "upload", "field": "files", "title": "附件"}]}
form = {"files": [{"url": "https://x/a.pdf", "name": "a.pdf"}]}
out = normalize_attachment_refs(
schema,
form,
[{"url": "https://x/a.pdf"}],
)
self.assertEqual(out[0]["source"], "form")
def test_accepts_tenant_upload_and_uses_stored_name(self):
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from app.models.models import BizAttachment
engine = create_engine("sqlite:///:memory:")
BizAttachment.__table__.create(engine)
db = sessionmaker(bind=engine)()
db.add(
BizAttachment(
id="att1",
original_name="note.pdf",
file_url="oa-approval/note.pdf",
tenant_no="t1",
create_dept_id="",
)
)
db.commit()
out = normalize_attachment_refs(
None,
{},
[{"source": "upload", "url": "oa-approval/note.pdf", "fileName": "hack.pdf"}],
db=db,
tenant_no="t1",
)
self.assertEqual(out[0]["source"], "upload")
self.assertEqual(out[0]["fileName"], "note.pdf")
self.assertEqual(out[0]["fieldKey"], "")
db.close()
def test_rejects_upload_from_other_tenant_or_unknown_url(self):
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from app.models.models import BizAttachment
from app.services.oa_engine import OaEngineError
engine = create_engine("sqlite:///:memory:")
BizAttachment.__table__.create(engine)
db = sessionmaker(bind=engine)()
db.add(
BizAttachment(
id="att2",
original_name="note.pdf",
file_url="oa-approval/note.pdf",
tenant_no="other",
create_dept_id="",
)
)
db.commit()
with self.assertRaises(OaEngineError):
normalize_attachment_refs(
None,
{},
[{"source": "upload", "url": "oa-approval/note.pdf"}],
db=db,
tenant_no="t1",
)
with self.assertRaises(OaEngineError):
normalize_attachment_refs(
None,
{},
[{"source": "upload", "url": "https://evil.example/a.pdf"}],
db=db,
tenant_no="t1",
)
db.close()
class TestCommentCrud(unittest.TestCase):
def setUp(self):
@@ -198,3 +275,253 @@ class TestCommentCrud(unittest.TestCase):
content="x",
attachment_refs=None,
)
class TestCommentMentionGrant(unittest.TestCase):
def setUp(self):
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from app.models.models import (
OaAppVersion,
OaInstance,
OaInstanceComment,
OaInstanceViewGrant,
OaTask,
SysUser,
)
self.engine = create_engine("sqlite:///:memory:")
for table in (
OaAppVersion.__table__,
OaInstance.__table__,
OaInstanceComment.__table__,
OaInstanceViewGrant.__table__,
OaTask.__table__,
SysUser.__table__,
):
table.create(self.engine)
Session = sessionmaker(bind=self.engine, autoflush=False, autocommit=False)
self.db = Session()
now = datetime.now()
self.db.add(
OaAppVersion(
id="v1",
app_id="app1",
version_no=1,
status="published",
is_latest_published="1",
form_schema_json="{}",
flow_tree_json="{}",
advanced_settings_json="{}",
tenant_no="zkxw",
create_dept_id="",
create_time=now,
update_time=now,
del_flag=0,
)
)
self.db.add(
OaInstance(
id="i1",
app_id="app1",
app_version_id="v1",
title="t",
status="running",
initiator_user_id="u0",
initiator_dept_id="d1",
form_data_json="{}",
tenant_no="zkxw",
create_dept_id="d1",
create_time=now,
update_time=now,
del_flag=0,
current_round=1,
)
)
for uid, name, status in (("u1", "张三", "1"), ("u2", "李四", "1"), ("u3", "王五", "0")):
self.db.add(
SysUser(
id=uid,
tenant_no="zkxw",
user_no=uid,
nick_name=name,
email="",
phone_no="",
password="",
status=status,
create_dept_id="",
del_flag=0,
create_time=now,
update_time=now,
)
)
self.db.commit()
def test_mention_grants_view_and_is_idempotent(self):
from app.models.models import OaInstanceViewGrant
from app.services import oa_instance_comment as svc
row = svc.create_comment(
self.db,
instance_id="i1",
tenant_no="zkxw",
user_id="u1",
dept_id="d1",
content="",
attachment_refs=None,
mention_user_ids=["u2", "u2", "u1"],
)
self.assertEqual(row["content"], "")
self.assertEqual([m["userId"] for m in row["mentions"]], ["u2"])
self.assertEqual(row["mentions"][0]["userName"], "李四")
grants = (
self.db.query(OaInstanceViewGrant)
.filter(OaInstanceViewGrant.del_flag == 0)
.all()
)
self.assertEqual(len(grants), 1)
self.assertEqual(grants[0].user_id, "u2")
self.assertEqual(grants[0].source, "comment_mention")
svc.create_comment(
self.db,
instance_id="i1",
tenant_no="zkxw",
user_id="u1",
dept_id="d1",
content="再提一次",
attachment_refs=None,
mention_user_ids=["u2"],
)
grants = (
self.db.query(OaInstanceViewGrant)
.filter(OaInstanceViewGrant.del_flag == 0)
.all()
)
self.assertEqual(len(grants), 1)
def test_rejects_empty_without_mention(self):
from app.services import oa_instance_comment as svc
from app.services.oa_engine import OaEngineError
with self.assertRaises(OaEngineError):
svc.create_comment(
self.db,
instance_id="i1",
tenant_no="zkxw",
user_id="u1",
dept_id="d1",
content=" ",
attachment_refs=None,
mention_user_ids=None,
)
def test_allows_upload_only_comment(self):
from app.models.models import BizAttachment
from app.services import oa_instance_comment as svc
BizAttachment.__table__.create(self.engine)
self.db.add(
BizAttachment(
id="att-c1",
original_name="note.pdf",
file_url="oa-approval/note.pdf",
tenant_no="zkxw",
create_dept_id="",
)
)
self.db.commit()
row = svc.create_comment(
self.db,
instance_id="i1",
tenant_no="zkxw",
user_id="u1",
dept_id="d1",
content=" ",
attachment_refs=[{"source": "upload", "url": "oa-approval/note.pdf"}],
mention_user_ids=None,
)
self.assertEqual(row["content"], "")
self.assertEqual(row["attachmentRefs"][0]["source"], "upload")
self.assertEqual(row["attachmentRefs"][0]["fileName"], "note.pdf")
def test_rejects_disabled_user(self):
from app.services import oa_instance_comment as svc
from app.services.oa_engine import OaEngineError
with self.assertRaises(OaEngineError):
svc.create_comment(
self.db,
instance_id="i1",
tenant_no="zkxw",
user_id="u1",
dept_id="d1",
content="x",
attachment_refs=None,
mention_user_ids=["u3"],
)
def test_revoke_keeps_grant_and_list_skips_participants(self):
from app.models.models import OaInstance, OaInstanceViewGrant, OaTask
from app.services import oa_instance_comment as svc
row = svc.create_comment(
self.db,
instance_id="i1",
tenant_no="zkxw",
user_id="u1",
dept_id="d1",
content="请看",
attachment_refs=None,
mention_user_ids=["u2"],
)
svc.revoke_comment(
self.db,
instance_id="i1",
comment_id=row["id"],
tenant_no="zkxw",
user_id="u1",
)
self.assertEqual(
self.db.query(OaInstanceViewGrant)
.filter(OaInstanceViewGrant.del_flag == 0)
.count(),
1,
)
self.assertEqual(
svc.list_mentioned_instance_ids(self.db, tenant_no="zkxw", user_id="u2"),
["i1"],
)
now = datetime.now()
self.db.add(
OaTask(
id="t-cc",
instance_id="i1",
task_type="cc",
assignee_user_id="u2",
status="done",
tenant_no="zkxw",
create_dept_id="",
create_time=now,
update_time=now,
del_flag=0,
round_no=1,
is_read=0,
)
)
self.db.commit()
# 即使已是抄送人,被@仍应出现在「提到我的」
self.assertEqual(
svc.list_mentioned_instance_ids(self.db, tenant_no="zkxw", user_id="u2"),
["i1"],
)
inst = self.db.query(OaInstance).filter(OaInstance.id == "i1").one()
inst.initiator_user_id = "u2"
self.db.commit()
# 即使已是发起人,被@仍应出现在「提到我的」
self.assertEqual(
svc.list_mentioned_instance_ids(self.db, tenant_no="zkxw", user_id="u2"),
["i1"],
)