提交
This commit is contained in:
1 parent
8248e5ef15
commit
b9b4cc2673
57 files changed
+42800
-30473
No files matched your search
+111
-90
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
):
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"""
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
+14040
-10721
File diff suppressed because it is too large.
Load diff
@@ -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
File diff suppressed because it is too large.
Load diff
@@ -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,
|
||||
|
||||
@@ -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"],
|
||||
)
|
||||
Reference in new issue
Block a user