提交
This commit is contained in:
1 parent
85b0b454bf
commit
1bdf979a70
23 files changed
+5514
-4394
No files matched your search
+20
-7
@@ -62,10 +62,23 @@ DATABASE_URL=mysql+pymysql://root:password@localhost:3306/zkxw
|
||||
# 微信服务号(OA 审批/抄送模板消息;access_token 用服务号凭证,miniprogram.appid 仍用 WXPAY_MINI_APPID)
|
||||
# WX_MP_APPID=wx_service_account_appid
|
||||
# WX_MP_SECRET=wx_service_account_secret
|
||||
# WX_MP_TEMPLATE_APPROVAL=approval_template_id
|
||||
# WX_MP_TEMPLATE_CC=cc_template_id
|
||||
# 模板字段名需与微信后台一致(默认 thing1/thing2/thing3/time4)
|
||||
# WX_MP_TPL_FIELD_TITLE=thing1
|
||||
# WX_MP_TPL_FIELD_APPLICANT=thing2
|
||||
# WX_MP_TPL_FIELD_NODE=thing3
|
||||
# WX_MP_TPL_FIELD_TIME=time4
|
||||
# WX_MP_TEMPLATE_APPROVAL=20rTii7gNFx5N998H4AlZe4AsQSV_62otPKti7pG7FI
|
||||
# WX_MP_TEMPLATE_CC=_BEaLeh2uDffEagwDJVEEmtzIYNglkd8hyJxCT5XIgw
|
||||
# WX_MP_TEMPLATE_APPLICANT_PROGRESS=_BEaLeh2uDffEagwDJVEEikOv8gnWu7aFA8cl0nX7S4
|
||||
# 审批「OA系统流程审批通知」:申请事项/申请人/当前环节/申请日期/流程名称
|
||||
# WX_MP_TPL_FIELD_TITLE=thing12
|
||||
# WX_MP_TPL_FIELD_APPLICANT=thing21
|
||||
# WX_MP_TPL_FIELD_NODE=thing7
|
||||
# WX_MP_TPL_FIELD_TIME=time3
|
||||
# WX_MP_TPL_FIELD_PROCESS=thing48
|
||||
# 抄送「OA抄送通知」:业务名称/申请人/申请时间/流程名称
|
||||
# WX_MP_TPL_CC_FIELD_TITLE=thing5
|
||||
# WX_MP_TPL_CC_FIELD_APPLICANT=thing2
|
||||
# WX_MP_TPL_CC_FIELD_TIME=time3
|
||||
# WX_MP_TPL_CC_FIELD_PROCESS=thing1
|
||||
# 申请人进度:业务名称/流程名称/审批人/审批结果(通过|失败|转办|其他)/审批时间
|
||||
# WX_MP_TPL_PROGRESS_FIELD_BUSINESS=thing5
|
||||
# WX_MP_TPL_PROGRESS_FIELD_PROCESS=thing1
|
||||
# WX_MP_TPL_PROGRESS_FIELD_APPROVER=thing6
|
||||
# WX_MP_TPL_PROGRESS_FIELD_RESULT=const4
|
||||
# WX_MP_TPL_PROGRESS_FIELD_TIME=time11
|
||||
@@ -105,11 +105,24 @@ class Settings(BaseSettings):
|
||||
WX_MP_SECRET: str = ""
|
||||
WX_MP_TEMPLATE_APPROVAL: str = ""
|
||||
WX_MP_TEMPLATE_CC: str = ""
|
||||
# 模板 data 字段名(与微信后台申请结果一致,可改 env)
|
||||
WX_MP_TPL_FIELD_TITLE: str = "thing1"
|
||||
WX_MP_TPL_FIELD_APPLICANT: str = "thing2"
|
||||
WX_MP_TPL_FIELD_NODE: str = "thing3"
|
||||
WX_MP_TPL_FIELD_TIME: str = "time4"
|
||||
WX_MP_TEMPLATE_APPLICANT_PROGRESS: str = ""
|
||||
# 审批模板「OA系统流程审批通知」:申请事项/申请人/当前环节/申请日期/流程名称
|
||||
WX_MP_TPL_FIELD_TITLE: str = "thing12"
|
||||
WX_MP_TPL_FIELD_APPLICANT: str = "thing21"
|
||||
WX_MP_TPL_FIELD_NODE: str = "thing7"
|
||||
WX_MP_TPL_FIELD_TIME: str = "time3"
|
||||
WX_MP_TPL_FIELD_PROCESS: str = "thing48"
|
||||
# 抄送模板「OA抄送通知」:业务名称/申请人/申请时间/流程名称(无当前环节)
|
||||
WX_MP_TPL_CC_FIELD_TITLE: str = "thing5"
|
||||
WX_MP_TPL_CC_FIELD_APPLICANT: str = "thing2"
|
||||
WX_MP_TPL_CC_FIELD_TIME: str = "time3"
|
||||
WX_MP_TPL_CC_FIELD_PROCESS: str = "thing1"
|
||||
# 申请人进度:业务名称/流程名称/审批人/审批结果(const)/审批时间
|
||||
WX_MP_TPL_PROGRESS_FIELD_BUSINESS: str = "thing5"
|
||||
WX_MP_TPL_PROGRESS_FIELD_PROCESS: str = "thing1"
|
||||
WX_MP_TPL_PROGRESS_FIELD_APPROVER: str = "thing6"
|
||||
WX_MP_TPL_PROGRESS_FIELD_RESULT: str = "const4"
|
||||
WX_MP_TPL_PROGRESS_FIELD_TIME: str = "time11"
|
||||
|
||||
# 与 docs/sql/zkxw.sql 基线一致:登录按租户 + sys_user.user_no
|
||||
DEFAULT_TENANT_NO: str = "zkxw"
|
||||
|
||||
@@ -2544,6 +2544,14 @@ def complete_task(
|
||||
inst.update_time = now
|
||||
db.add(inst)
|
||||
db.commit()
|
||||
from app.services.oa_wechat_notify import schedule_oa_applicant_progress
|
||||
|
||||
schedule_oa_applicant_progress(
|
||||
instance_id=inst.id,
|
||||
actor_user_id=user_id,
|
||||
action="reject",
|
||||
task_id=task.id,
|
||||
)
|
||||
return get_instance_detail(db, inst.id, tenant_no=tenant_no)
|
||||
|
||||
created = _approve_task_core(
|
||||
@@ -2555,9 +2563,18 @@ def complete_task(
|
||||
comment=comment,
|
||||
)
|
||||
db.commit()
|
||||
from app.services.oa_wechat_notify import schedule_oa_task_notifies
|
||||
from app.services.oa_wechat_notify import (
|
||||
schedule_oa_applicant_progress,
|
||||
schedule_oa_task_notifies,
|
||||
)
|
||||
|
||||
schedule_oa_task_notifies(created or [])
|
||||
schedule_oa_applicant_progress(
|
||||
instance_id=inst.id,
|
||||
actor_user_id=user_id,
|
||||
action="approve",
|
||||
task_id=task.id,
|
||||
)
|
||||
return get_instance_detail(db, inst.id, tenant_no=tenant_no)
|
||||
|
||||
|
||||
@@ -2675,9 +2692,18 @@ def add_sign(
|
||||
task.update_time = now
|
||||
db.add(task)
|
||||
db.commit()
|
||||
from app.services.oa_wechat_notify import schedule_oa_task_notifies
|
||||
from app.services.oa_wechat_notify import (
|
||||
schedule_oa_applicant_progress,
|
||||
schedule_oa_task_notifies,
|
||||
)
|
||||
|
||||
schedule_oa_task_notifies(created_ids)
|
||||
schedule_oa_applicant_progress(
|
||||
instance_id=inst.id,
|
||||
actor_user_id=user_id,
|
||||
action="add_sign",
|
||||
task_id=task.id,
|
||||
)
|
||||
return get_instance_detail(db, inst.id, tenant_no=tenant_no)
|
||||
|
||||
|
||||
@@ -2790,9 +2816,18 @@ def transfer_task(
|
||||
)
|
||||
|
||||
db.commit()
|
||||
from app.services.oa_wechat_notify import schedule_oa_task_notifies
|
||||
from app.services.oa_wechat_notify import (
|
||||
schedule_oa_applicant_progress,
|
||||
schedule_oa_task_notifies,
|
||||
)
|
||||
|
||||
schedule_oa_task_notifies([new_task_id])
|
||||
schedule_oa_applicant_progress(
|
||||
instance_id=inst.id,
|
||||
actor_user_id=user_id,
|
||||
action="transfer",
|
||||
task_id=task.id,
|
||||
)
|
||||
return get_instance_detail(db, inst.id, tenant_no=tenant_no)
|
||||
|
||||
|
||||
@@ -2874,6 +2909,14 @@ def return_task(
|
||||
node_key=task.node_key,
|
||||
)
|
||||
db.commit()
|
||||
from app.services.oa_wechat_notify import schedule_oa_applicant_progress
|
||||
|
||||
schedule_oa_applicant_progress(
|
||||
instance_id=inst.id,
|
||||
actor_user_id=user_id,
|
||||
action="return",
|
||||
task_id=task.id,
|
||||
)
|
||||
return get_instance_detail(db, inst.id, tenant_no=tenant_no)
|
||||
|
||||
|
||||
|
||||
@@ -4,21 +4,91 @@ from __future__ import annotations
|
||||
import logging
|
||||
import threading
|
||||
from datetime import datetime
|
||||
from typing import Iterable, List
|
||||
from typing import Iterable, List, Optional
|
||||
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from app.core.database import SessionLocal
|
||||
from app.models.models import OaInstance, OaTask, SysUser
|
||||
from app.services.wechat_mp_message import send_oa_approval_notify, send_oa_cc_notify
|
||||
from app.models.models import OaApp, OaInstance, OaTask, SysUser
|
||||
from app.services.wechat_mp_message import (
|
||||
send_oa_applicant_progress_notify,
|
||||
send_oa_approval_notify,
|
||||
send_oa_cc_notify,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 微信模板 const4 枚举,必须原样下发
|
||||
RESULT_PASS = "通过"
|
||||
RESULT_FAIL = "失败"
|
||||
RESULT_TRANSFER = "转办"
|
||||
RESULT_OTHER = "其他"
|
||||
|
||||
def build_oa_detail_pagepath(*, instance_id: str, task_id: str, mode: str) -> str:
|
||||
# 无前导 /
|
||||
return (
|
||||
f"pages/oa/webview/index?instanceId={instance_id}"
|
||||
f"&taskId={task_id}&mode={mode}"
|
||||
)
|
||||
# 模板消息关闭详情后回退的小程序页(与列表入口对齐)
|
||||
RETURN_PENDING = "/pages/oa/pending/index"
|
||||
RETURN_CC = "/pages/oa/cc/index"
|
||||
RETURN_MINE = "/pages/oa/mine/index"
|
||||
|
||||
_ACTION_RESULT = {
|
||||
"approve": RESULT_PASS,
|
||||
"reject": RESULT_FAIL,
|
||||
"transfer": RESULT_TRANSFER,
|
||||
"return": RESULT_OTHER,
|
||||
"add_sign": RESULT_OTHER,
|
||||
}
|
||||
|
||||
|
||||
def map_oa_action_to_result(action: str) -> str:
|
||||
return _ACTION_RESULT.get((action or "").strip(), RESULT_OTHER)
|
||||
|
||||
|
||||
def build_oa_detail_pagepath(
|
||||
*,
|
||||
instance_id: str,
|
||||
task_id: str = "",
|
||||
mode: str,
|
||||
return_path: str = "",
|
||||
) -> str:
|
||||
"""拼小程序 pagepath(无前导 /);query 做 urlencode,保证 returnPath 含斜杠可用。"""
|
||||
params = {
|
||||
"instanceId": (instance_id or "").strip(),
|
||||
"mode": (mode or "view").strip() or "view",
|
||||
}
|
||||
tid = (task_id or "").strip()
|
||||
if tid and tid != "0":
|
||||
params["taskId"] = tid
|
||||
rp = (return_path or "").strip()
|
||||
if rp:
|
||||
params["returnPath"] = rp
|
||||
return "pages/oa/webview/index?" + urlencode(params)
|
||||
|
||||
|
||||
def _build_matter_title(*, applicant: str, process_name: str, fallback: str) -> str:
|
||||
"""申请事项/业务名称:申请人 - 流程名称。"""
|
||||
parts = [p for p in [(applicant or "").strip(), (process_name or "").strip()] if p]
|
||||
if parts:
|
||||
return " - ".join(parts)
|
||||
return (fallback or "").strip() or "审批"
|
||||
|
||||
|
||||
def _resolve_process_and_applicant(db, inst: Optional[OaInstance]):
|
||||
applicant = ""
|
||||
process_name = ""
|
||||
if not inst:
|
||||
return applicant, process_name
|
||||
if inst.initiator_user_id:
|
||||
ini = db.query(SysUser).filter(SysUser.id == inst.initiator_user_id).first()
|
||||
applicant = (ini.nick_name if ini else "") or ""
|
||||
if inst.app_id:
|
||||
app = (
|
||||
db.query(OaApp)
|
||||
.filter(OaApp.id == inst.app_id, OaApp.del_flag == 0)
|
||||
.first()
|
||||
)
|
||||
process_name = (app.app_name if app else "") or ""
|
||||
if not process_name:
|
||||
process_name = (inst.title or "").strip()
|
||||
return applicant, process_name
|
||||
|
||||
|
||||
def _send_one(db, task: OaTask) -> None:
|
||||
@@ -35,16 +105,19 @@ def _send_one(db, task: OaTask) -> None:
|
||||
.filter(OaInstance.id == task.instance_id, OaInstance.del_flag == 0)
|
||||
.first()
|
||||
)
|
||||
title = (inst.title if inst else "") or (task.node_name or "审批")
|
||||
applicant = ""
|
||||
if inst and inst.initiator_user_id:
|
||||
ini = db.query(SysUser).filter(SysUser.id == inst.initiator_user_id).first()
|
||||
applicant = (ini.nick_name if ini else "") or ""
|
||||
applicant, process_name = _resolve_process_and_applicant(db, inst)
|
||||
fallback = (inst.title if inst else "") or (task.node_name or "审批")
|
||||
title = _build_matter_title(
|
||||
applicant=applicant, process_name=process_name, fallback=fallback
|
||||
)
|
||||
time_text = datetime.now().strftime("%Y-%m-%d %H:%M")
|
||||
node_name = task.node_name or ""
|
||||
if task.task_type == "approval" and (task.status or "") == "pending":
|
||||
pagepath = build_oa_detail_pagepath(
|
||||
instance_id=task.instance_id, task_id=task.id, mode="approve"
|
||||
instance_id=task.instance_id,
|
||||
task_id=task.id,
|
||||
mode="approve",
|
||||
return_path=RETURN_PENDING,
|
||||
)
|
||||
send_oa_approval_notify(
|
||||
mp_openid=mp,
|
||||
@@ -53,10 +126,14 @@ def _send_one(db, task: OaTask) -> None:
|
||||
applicant=applicant,
|
||||
node_name=node_name,
|
||||
time_text=time_text,
|
||||
process_name=process_name,
|
||||
)
|
||||
elif task.task_type == "cc":
|
||||
pagepath = build_oa_detail_pagepath(
|
||||
instance_id=task.instance_id, task_id=task.id, mode="view"
|
||||
instance_id=task.instance_id,
|
||||
task_id=task.id,
|
||||
mode="view",
|
||||
return_path=RETURN_CC,
|
||||
)
|
||||
send_oa_cc_notify(
|
||||
mp_openid=mp,
|
||||
@@ -65,9 +142,83 @@ def _send_one(db, task: OaTask) -> None:
|
||||
applicant=applicant,
|
||||
node_name=node_name,
|
||||
time_text=time_text,
|
||||
process_name=process_name,
|
||||
)
|
||||
|
||||
|
||||
def _send_applicant_progress(
|
||||
db,
|
||||
*,
|
||||
instance_id: str,
|
||||
actor_user_id: str,
|
||||
result: str,
|
||||
task_id: str = "",
|
||||
) -> None:
|
||||
iid = (instance_id or "").strip()
|
||||
if not iid:
|
||||
return
|
||||
inst = (
|
||||
db.query(OaInstance)
|
||||
.filter(OaInstance.id == iid, OaInstance.del_flag == 0)
|
||||
.first()
|
||||
)
|
||||
if not inst or not (inst.initiator_user_id or "").strip():
|
||||
return
|
||||
ini = (
|
||||
db.query(SysUser)
|
||||
.filter(SysUser.id == inst.initiator_user_id, SysUser.del_flag == 0)
|
||||
.first()
|
||||
)
|
||||
mp = (getattr(ini, "wx_mp_openid", None) or "").strip() if ini else ""
|
||||
if not mp:
|
||||
logger.info(
|
||||
"oa wx applicant progress skip: no mp_openid initiator=%s inst=%s",
|
||||
inst.initiator_user_id,
|
||||
iid,
|
||||
)
|
||||
return
|
||||
applicant = (ini.nick_name if ini else "") or ""
|
||||
process_name = ""
|
||||
if inst.app_id:
|
||||
app = (
|
||||
db.query(OaApp)
|
||||
.filter(OaApp.id == inst.app_id, OaApp.del_flag == 0)
|
||||
.first()
|
||||
)
|
||||
process_name = (app.app_name if app else "") or ""
|
||||
if not process_name:
|
||||
process_name = (inst.title or "").strip()
|
||||
business_name = _build_matter_title(
|
||||
applicant=applicant,
|
||||
process_name=process_name,
|
||||
fallback=(inst.title or "审批"),
|
||||
)
|
||||
actor = None
|
||||
aid = (actor_user_id or "").strip()
|
||||
if aid:
|
||||
actor = (
|
||||
db.query(SysUser)
|
||||
.filter(SysUser.id == aid, SysUser.del_flag == 0)
|
||||
.first()
|
||||
)
|
||||
approver_name = (actor.nick_name if actor else "") or ""
|
||||
pagepath = build_oa_detail_pagepath(
|
||||
instance_id=iid,
|
||||
task_id="",
|
||||
mode="view",
|
||||
return_path=RETURN_MINE,
|
||||
)
|
||||
send_oa_applicant_progress_notify(
|
||||
mp_openid=mp,
|
||||
pagepath=pagepath,
|
||||
business_name=business_name,
|
||||
process_name=process_name,
|
||||
approver_name=approver_name,
|
||||
result=(result or RESULT_OTHER).strip() or RESULT_OTHER,
|
||||
time_text=datetime.now().strftime("%Y-%m-%d %H:%M"),
|
||||
)
|
||||
|
||||
|
||||
def notify_oa_tasks_sync(task_ids: Iterable[str]) -> None:
|
||||
ids = [str(x).strip() for x in task_ids if str(x).strip()]
|
||||
if not ids:
|
||||
@@ -92,6 +243,34 @@ def notify_oa_tasks_sync(task_ids: Iterable[str]) -> None:
|
||||
db.close()
|
||||
|
||||
|
||||
def notify_oa_applicant_progress_sync(
|
||||
*,
|
||||
instance_id: str,
|
||||
actor_user_id: str,
|
||||
result: str,
|
||||
task_id: str = "",
|
||||
) -> None:
|
||||
db = None
|
||||
try:
|
||||
db = SessionLocal()
|
||||
_send_applicant_progress(
|
||||
db,
|
||||
instance_id=instance_id,
|
||||
actor_user_id=actor_user_id,
|
||||
result=result,
|
||||
task_id=task_id,
|
||||
)
|
||||
except Exception:
|
||||
logger.exception(
|
||||
"oa wx applicant progress failed inst=%s actor=%s",
|
||||
instance_id,
|
||||
actor_user_id,
|
||||
)
|
||||
finally:
|
||||
if db is not None:
|
||||
db.close()
|
||||
|
||||
|
||||
def schedule_oa_task_notifies(task_ids: List[str]) -> None:
|
||||
ids = [str(x).strip() for x in (task_ids or []) if str(x).strip()]
|
||||
if not ids:
|
||||
@@ -99,3 +278,27 @@ def schedule_oa_task_notifies(task_ids: List[str]) -> None:
|
||||
threading.Thread(
|
||||
target=notify_oa_tasks_sync, args=(ids,), daemon=True, name="oa-wx-notify"
|
||||
).start()
|
||||
|
||||
|
||||
def schedule_oa_applicant_progress(
|
||||
*,
|
||||
instance_id: str,
|
||||
actor_user_id: str,
|
||||
action: str,
|
||||
task_id: str = "",
|
||||
) -> None:
|
||||
iid = (instance_id or "").strip()
|
||||
if not iid:
|
||||
return
|
||||
result = map_oa_action_to_result(action)
|
||||
threading.Thread(
|
||||
target=notify_oa_applicant_progress_sync,
|
||||
kwargs={
|
||||
"instance_id": iid,
|
||||
"actor_user_id": (actor_user_id or "").strip(),
|
||||
"result": result,
|
||||
"task_id": (task_id or "").strip(),
|
||||
},
|
||||
daemon=True,
|
||||
name="oa-wx-applicant-progress",
|
||||
).start()
|
||||
@@ -30,13 +30,31 @@ def _build_oa_data(
|
||||
applicant: str,
|
||||
node_name: str,
|
||||
time_text: str,
|
||||
process_name: str = "",
|
||||
title_key: str = "",
|
||||
applicant_key: str = "",
|
||||
node_key: str = "",
|
||||
time_key: str = "",
|
||||
process_key: str = "",
|
||||
) -> Dict[str, Dict[str, str]]:
|
||||
return {
|
||||
settings.WX_MP_TPL_FIELD_TITLE: {"value": _truncate_thing(title)},
|
||||
settings.WX_MP_TPL_FIELD_APPLICANT: {"value": _truncate_thing(applicant)},
|
||||
settings.WX_MP_TPL_FIELD_NODE: {"value": _truncate_thing(node_name)},
|
||||
settings.WX_MP_TPL_FIELD_TIME: {"value": (time_text or "").strip()},
|
||||
}
|
||||
"""按字段名组装 data;字段名留空则跳过(兼容临时少字段模板)。"""
|
||||
data: Dict[str, Dict[str, str]] = {}
|
||||
title_key = (title_key or "").strip()
|
||||
applicant_key = (applicant_key or "").strip()
|
||||
node_key = (node_key or "").strip()
|
||||
time_key = (time_key or "").strip()
|
||||
process_key = (process_key or "").strip()
|
||||
if title_key:
|
||||
data[title_key] = {"value": _truncate_thing(title)}
|
||||
if applicant_key:
|
||||
data[applicant_key] = {"value": _truncate_thing(applicant)}
|
||||
if node_key:
|
||||
data[node_key] = {"value": _truncate_thing(node_name)}
|
||||
if time_key:
|
||||
data[time_key] = {"value": (time_text or "").strip()}
|
||||
if process_key:
|
||||
data[process_key] = {"value": _truncate_thing(process_name)}
|
||||
return data
|
||||
|
||||
|
||||
def _send_template(
|
||||
@@ -64,33 +82,56 @@ def _send_template(
|
||||
logger.warning("mp template send aborted: %s", exc)
|
||||
return {"success": False, "error": str(exc)}
|
||||
|
||||
payload = {
|
||||
path = (pagepath or "").lstrip("/")
|
||||
use_mini_jump = bool(mini_appid and path)
|
||||
payload: Dict[str, Any] = {
|
||||
"touser": oid,
|
||||
"template_id": tid,
|
||||
"data": data,
|
||||
"miniprogram": {
|
||||
"appid": mini_appid,
|
||||
"pagepath": (pagepath or "").lstrip("/"),
|
||||
},
|
||||
}
|
||||
if use_mini_jump:
|
||||
payload["miniprogram"] = {"appid": mini_appid, "pagepath": path}
|
||||
|
||||
try:
|
||||
def _post(body: Dict[str, Any]) -> Dict[str, Any]:
|
||||
with httpx.Client(timeout=15.0) as client:
|
||||
r = client.post(
|
||||
f"{_WX_TEMPLATE_SEND}?access_token={access_token}",
|
||||
json=payload,
|
||||
json=body,
|
||||
)
|
||||
r.raise_for_status()
|
||||
result = r.json()
|
||||
return r.json()
|
||||
|
||||
try:
|
||||
result = _post(payload)
|
||||
except Exception as exc:
|
||||
logger.exception("mp template send request failed")
|
||||
return {"success": False, "error": str(exc)}
|
||||
|
||||
jumped = use_mini_jump
|
||||
# 服务号未关联小程序 / 页面未发布时微信返回 40165;降级为纯模板通知,保证先可达
|
||||
if result.get("errcode") == 40165 and use_mini_jump:
|
||||
logger.warning(
|
||||
"mp template miniprogram jump rejected (40165), retry without jump: %s",
|
||||
result.get("errmsg"),
|
||||
)
|
||||
jumped = False
|
||||
payload_no_jump = {
|
||||
"touser": oid,
|
||||
"template_id": tid,
|
||||
"data": data,
|
||||
}
|
||||
try:
|
||||
result = _post(payload_no_jump)
|
||||
except Exception as exc:
|
||||
logger.exception("mp template fallback send request failed")
|
||||
return {"success": False, "error": str(exc)}
|
||||
|
||||
if result.get("errcode") in (0, None):
|
||||
logger.info(
|
||||
"mp template send ok: openid=%s msgid=%s",
|
||||
"mp template send ok: openid=%s msgid=%s jumped=%s",
|
||||
oid,
|
||||
result.get("msgid"),
|
||||
jumped,
|
||||
)
|
||||
return {"success": True, "msgid": result.get("msgid")}
|
||||
|
||||
@@ -110,6 +151,7 @@ def send_oa_approval_notify(
|
||||
applicant: str,
|
||||
node_name: str,
|
||||
time_text: str,
|
||||
process_name: str = "",
|
||||
) -> dict:
|
||||
return _send_template(
|
||||
mp_openid=mp_openid,
|
||||
@@ -119,6 +161,12 @@ def send_oa_approval_notify(
|
||||
applicant=applicant,
|
||||
node_name=node_name,
|
||||
time_text=time_text,
|
||||
process_name=process_name,
|
||||
title_key=settings.WX_MP_TPL_FIELD_TITLE,
|
||||
applicant_key=settings.WX_MP_TPL_FIELD_APPLICANT,
|
||||
node_key=settings.WX_MP_TPL_FIELD_NODE,
|
||||
time_key=settings.WX_MP_TPL_FIELD_TIME,
|
||||
process_key=settings.WX_MP_TPL_FIELD_PROCESS,
|
||||
),
|
||||
pagepath=pagepath,
|
||||
)
|
||||
@@ -132,6 +180,7 @@ def send_oa_cc_notify(
|
||||
applicant: str,
|
||||
node_name: str,
|
||||
time_text: str,
|
||||
process_name: str = "",
|
||||
) -> dict:
|
||||
return _send_template(
|
||||
mp_openid=mp_openid,
|
||||
@@ -141,6 +190,47 @@ def send_oa_cc_notify(
|
||||
applicant=applicant,
|
||||
node_name=node_name,
|
||||
time_text=time_text,
|
||||
process_name=process_name,
|
||||
title_key=settings.WX_MP_TPL_CC_FIELD_TITLE,
|
||||
applicant_key=settings.WX_MP_TPL_CC_FIELD_APPLICANT,
|
||||
node_key="", # 抄送模板无当前环节
|
||||
time_key=settings.WX_MP_TPL_CC_FIELD_TIME,
|
||||
process_key=settings.WX_MP_TPL_CC_FIELD_PROCESS,
|
||||
),
|
||||
pagepath=pagepath,
|
||||
)
|
||||
|
||||
|
||||
def send_oa_applicant_progress_notify(
|
||||
*,
|
||||
mp_openid: str,
|
||||
pagepath: str,
|
||||
business_name: str,
|
||||
process_name: str,
|
||||
approver_name: str,
|
||||
result: str,
|
||||
time_text: str,
|
||||
) -> dict:
|
||||
"""通知申请人:某步审批结果(const 枚举:通过/失败/转办/其他)。"""
|
||||
data: Dict[str, Dict[str, str]] = {}
|
||||
biz_key = (settings.WX_MP_TPL_PROGRESS_FIELD_BUSINESS or "").strip()
|
||||
process_key = (settings.WX_MP_TPL_PROGRESS_FIELD_PROCESS or "").strip()
|
||||
approver_key = (settings.WX_MP_TPL_PROGRESS_FIELD_APPROVER or "").strip()
|
||||
result_key = (settings.WX_MP_TPL_PROGRESS_FIELD_RESULT or "").strip()
|
||||
time_key = (settings.WX_MP_TPL_PROGRESS_FIELD_TIME or "").strip()
|
||||
if biz_key:
|
||||
data[biz_key] = {"value": _truncate_thing(business_name)}
|
||||
if process_key:
|
||||
data[process_key] = {"value": _truncate_thing(process_name)}
|
||||
if approver_key:
|
||||
data[approver_key] = {"value": _truncate_thing(approver_name)}
|
||||
if result_key:
|
||||
data[result_key] = {"value": (result or "").strip()}
|
||||
if time_key:
|
||||
data[time_key] = {"value": (time_text or "").strip()}
|
||||
return _send_template(
|
||||
mp_openid=mp_openid,
|
||||
template_id=settings.WX_MP_TEMPLATE_APPLICANT_PROGRESS,
|
||||
data=data,
|
||||
pagepath=pagepath,
|
||||
)
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -1,11 +1,11 @@
|
||||
# Graph Report - app (2026-08-14)
|
||||
|
||||
## Corpus Check
|
||||
- Large corpus: 253 files · ~304,934 words. Semantic extraction will be expensive (many Claude tokens). Consider running on a subfolder, or use --no-semantic to run AST-only.
|
||||
- Large corpus: 253 files · ~305,672 words. Semantic extraction will be expensive (many Claude tokens). Consider running on a subfolder, or use --no-semantic to run AST-only.
|
||||
|
||||
## Summary
|
||||
- 5268 nodes · 28756 edges · 44 communities detected
|
||||
- Extraction: 32% EXTRACTED · 68% INFERRED · 0% AMBIGUOUS · INFERRED: 19653 edges (avg confidence: 0.6)
|
||||
- 5276 nodes · 28768 edges · 40 communities detected
|
||||
- Extraction: 32% EXTRACTED · 68% INFERRED · 0% AMBIGUOUS · INFERRED: 19654 edges (avg confidence: 0.6)
|
||||
- Token cost: 0 input · 0 output
|
||||
|
||||
## Community Hubs (Navigation)
|
||||
@@ -43,21 +43,17 @@
|
||||
- [[_COMMUNITY_Community 31|Community 31]]
|
||||
- [[_COMMUNITY_Community 32|Community 32]]
|
||||
- [[_COMMUNITY_Community 33|Community 33]]
|
||||
- [[_COMMUNITY_Community 34|Community 34]]
|
||||
- [[_COMMUNITY_Community 35|Community 35]]
|
||||
- [[_COMMUNITY_Community 36|Community 36]]
|
||||
- [[_COMMUNITY_Community 37|Community 37]]
|
||||
- [[_COMMUNITY_Community 38|Community 38]]
|
||||
- [[_COMMUNITY_Community 39|Community 39]]
|
||||
- [[_COMMUNITY_Community 40|Community 40]]
|
||||
- [[_COMMUNITY_Community 41|Community 41]]
|
||||
- [[_COMMUNITY_Community 42|Community 42]]
|
||||
- [[_COMMUNITY_Community 43|Community 43]]
|
||||
- [[_COMMUNITY_Community 44|Community 44]]
|
||||
- [[_COMMUNITY_Community 45|Community 45]]
|
||||
|
||||
## God Nodes (most connected - your core abstractions)
|
||||
1. `success_response()` - 738 edges
|
||||
2. `error_response()` - 611 edges
|
||||
3. `SysUser` - 600 edges
|
||||
3. `SysUser` - 601 edges
|
||||
4. `BizQcTaskResult` - 346 edges
|
||||
5. `BizProductionMaterial` - 329 edges
|
||||
6. `BizProductionMaterialUsage` - 323 edges
|
||||
@@ -69,237 +65,219 @@
|
||||
## Surprising Connections (you probably didn't know these)
|
||||
- `统一记录 API 请求/响应出入参的中间件` --uses--> `SysApiRequestLog` [INFERRED]
|
||||
core/api_request_log_middleware.py → models/models.py
|
||||
- `get_menu_list()` --calls--> `success_response()` [INFERRED]
|
||||
api/menu.py → core/response.py
|
||||
- `sync_lab_storage_seq_after_use()` --calls--> `generate_uuid()` [INFERRED]
|
||||
crud/sample_reception_lab_code.py → core/utils.py
|
||||
- `标记领用记录已使用完(盘点登记 / 变动记录)。` --uses--> `SysUser` [INFERRED]
|
||||
api/material.py → models/models.py
|
||||
- `盘点登记:待处理领用记录(未标记已使用完)。` --uses--> `SysUser` [INFERRED]
|
||||
api/material.py → models/models.py
|
||||
- `微信小程序登录业务:openid + 手机号一致性校验。` --uses--> `SysUser` [INFERRED]
|
||||
services/wechat_login_service.py → models/models.py
|
||||
- `已绑定 openid 时静默换票(无需手机号授权)。` --uses--> `SysUser` [INFERRED]
|
||||
services/wechat_login_service.py → models/models.py
|
||||
|
||||
## Communities
|
||||
|
||||
### Community 0 - "Community 0"
|
||||
Cohesion: 0.01
|
||||
Nodes (1039): attachment_download_url(), create_dept(), create_dict_data(), create_dict_type(), create_express_company(), create_item_unit(), create_menu(), create_post() (+1031 more)
|
||||
Nodes (1088): attachment_download_url(), create_dept(), create_dict_data(), create_dict_type(), create_express_company(), create_item_unit(), create_menu(), create_post() (+1080 more)
|
||||
|
||||
### Community 1 - "Community 1"
|
||||
Cohesion: 0.01
|
||||
Nodes (745): api_save_application(), generate_uuid(), allocate_next_biz_no(), _format_biz_no(), _prefix_for(), 业务协议号自动生成:XBCC / JZCC + YYYYMMDD + 6 位序号, 原子分配下一个业务协议号。 须在 create 事务内调用;使用 MySQL LAST_INSERT_ID 保证并发安全。, create_cell_sample_type() (+737 more)
|
||||
Nodes (685): 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(), type_code_exists() (+677 more)
|
||||
|
||||
### Community 2 - "Community 2"
|
||||
Cohesion: 0.01
|
||||
Nodes (584): AttachmentDownloadUrlBody, DeptCreateBody, DeptUpdateBody, DictDataCreateBody, DictDataUpdateBody, DictTypeCreateBody, DictTypeUpdateBody, ExpressCompanyCreateBody (+576 more)
|
||||
Nodes (593): AttachmentDownloadUrlBody, DeptCreateBody, DeptUpdateBody, DictDataCreateBody, DictDataUpdateBody, DictTypeCreateBody, DictTypeUpdateBody, ExpressCompanyCreateBody (+585 more)
|
||||
|
||||
### Community 3 - "Community 3"
|
||||
Cohesion: 0.07
|
||||
Nodes (415): 环境检测 — 检测项维护 + 登记管理 API, 保险公司及险种价格管理 API(保险公司 + 开票信息 + 险种价格), 小程序菜单:当前用户在「移动端菜单」树下有权限的入口。, 宫格图标底色/图形色(可按入口自定义,不强制品牌绿)。, 按路由匹配移动端宫格图标(与 mobile-h5 van-icon 字形名对齐)。, AccessLogCreateBody, AccessLogUpdateBody, CleanRoomCreateBody (+407 more)
|
||||
Nodes (425): 保险公司及险种价格管理 API(保险公司 + 开票信息 + 险种价格), 小程序菜单:当前用户在「移动端菜单」树下有权限的入口。, 宫格图标底色/图形色(可按入口自定义,不强制品牌绿)。, 按路由匹配移动端宫格图标(与 mobile-h5 van-icon 字形名对齐)。, AccessLogCreateBody, AccessLogUpdateBody, CleanRoomCreateBody, CleanRoomUpdateBody (+417 more)
|
||||
|
||||
### Community 4 - "Community 4"
|
||||
Cohesion: 0.01
|
||||
Nodes (316): 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() (+308 more)
|
||||
Nodes (373): _normalize_date_bound(), _persist_protocol_sub_resources(), list_ethnics_for_select(), batch_delete_research_donations(), change_donation_status(), convert_from_storage(), convert_to_storage(), create_research_donation() (+365 more)
|
||||
|
||||
### Community 5 - "Community 5"
|
||||
Cohesion: 0.07
|
||||
Nodes (319): CellSampleTypeCreateBody, CellSampleTypeUpdateBody, CollectionRegionCreateBody, CollectionRegionUpdateBody, CustomerCreateBody, CustomerUpdateBody, HospitalCreateBody, HospitalUpdateBody (+311 more)
|
||||
Cohesion: 0.01
|
||||
Nodes (316): complete_task_with_result(), list_qc_work_order_inspection_batches(), eq_unicode(), like_unicode(), MySQL 字符串比较/模糊匹配时统一排序规则,避免 Illegal mix of collations。, 跨表字符串等值比较(JOIN / WHERE)时统一为 utf8mb4_unicode_ci。, 将列显式设为 utf8mb4_unicode_ci。, unicode_ci() (+308 more)
|
||||
|
||||
### Community 6 - "Community 6"
|
||||
Cohesion: 0.01
|
||||
Nodes (286): _normalize_date_bound(), _persist_protocol_sub_resources(), generate_id(), optional_date(), parse_date(), 将字符串 / date / datetime 转为 date 或 None, camel_to_snake(), get_model_attr() (+278 more)
|
||||
Cohesion: 0.07
|
||||
Nodes (311): CellSampleTypeCreateBody, CellSampleTypeUpdateBody, CollectionRegionCreateBody, CollectionRegionUpdateBody, CustomerCreateBody, CustomerUpdateBody, HospitalCreateBody, HospitalUpdateBody (+303 more)
|
||||
|
||||
### Community 7 - "Community 7"
|
||||
Cohesion: 0.03
|
||||
Nodes (222): 截止日期:仅日期时扩到当天 23:59:59。, 发起前预览审批链(条件齐后解析最终路径与审批人)。, 兼容旧 GET:无 formData,含条件分支时多为待确定占位。, _copied_app_name(), copy_app(), create_app(), create_group(), _create_version() (+214 more)
|
||||
Nodes (236): 截止日期:仅日期时扩到当天 23:59:59。, 发起前预览审批链(条件齐后解析最终路径与审批人)。, 兼容旧 GET:无 formData,含条件分支时多为待确定占位。, _app_to_dict(), _copied_app_name(), copy_app(), create_app(), _create_version() (+228 more)
|
||||
|
||||
### Community 8 - "Community 8"
|
||||
Cohesion: 0.02
|
||||
Nodes (138): mp_bind(), mp_bind_status(), 小程序鉴权:微信登录 / 绑定 openid。, 当前用户是否已绑定微信服务号 openid。, 服务号网页授权 code 绑定当前用户(需 Bearer)。, 微信登录:openid 已绑定时可静默换票;否则须授权手机号且与账号 phone_no 一致。, 密码登录后绑定当前微信 openid(需 Bearer)。, _resolve_user_id() (+130 more)
|
||||
Nodes (134): 小程序鉴权:微信登录 / 绑定 openid。, 当前用户是否已绑定微信服务号 openid。, 服务号网页授权 code 绑定当前用户(需 Bearer)。, 微信登录:openid 已绑定时可静默换票;否则须授权手机号且与账号 phone_no 一致。, 密码登录后绑定当前微信 openid(需 Bearer)。, wechat_login(), _error_json(), lifespan() (+126 more)
|
||||
|
||||
### Community 9 - "Community 9"
|
||||
Cohesion: 0.02
|
||||
Nodes (175): list_ethnics_for_select(), get_designer_data(), apply_data_scope(), _get_child_dept_ids(), get_user_dept_id(), _resolve_user_role_scopes(), _build_tree(), _cascade_soft_delete() (+167 more)
|
||||
Cohesion: 0.03
|
||||
Nodes (146): api_save_application(), map_payments_by_schedule_ids(), get_protocol_nos_used_in_protocols(), _fmt_dt(), get_handover_by_schedule(), get_outbound_handover(), get_outbound_handover_view(), _handover_row_to_dict() (+138 more)
|
||||
|
||||
### Community 10 - "Community 10"
|
||||
Cohesion: 0.03
|
||||
Nodes (132): get_next_cell_product_code(), _ancestor_link_from_cell_product_input(), _ancestor_sample_chain_link(), _apply_cell_product_status_change(), _build_ancestor_chain(), _build_descendant_chain(), _build_facility_path_label(), build_split_product_codes() (+124 more)
|
||||
Nodes (123): _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() (+115 more)
|
||||
|
||||
### Community 11 - "Community 11"
|
||||
Cohesion: 0.04
|
||||
Nodes (79): _collect_descendants(), _current_user(), _find_mobile_root(), list_my_mobile_menus(), _mobile_menu_icon(), _mobile_menu_icon_colors(), _user_menu_ids(), _jwt_user_payload() (+71 more)
|
||||
Nodes (98): _body_to_payload(), _get_current_user_id(), get_production_reception_by_sample_code(), get_production_reception_detail(), _get_tenant_no(), post_production_reception(), put_production_reception(), _resolve_reception_id() (+90 more)
|
||||
|
||||
### Community 12 - "Community 12"
|
||||
Cohesion: 0.05
|
||||
Nodes (78): format_date(), list_env_detect_records(), _row_to_dict(), _split_csv(), _deduct_step_usage_legacy_batch(), register_step_usage_from_requisition(), revert_step_material_usages(), add_task_step() (+70 more)
|
||||
Cohesion: 0.04
|
||||
Nodes (90): _collect_descendants(), _current_user(), _find_mobile_root(), list_my_mobile_menus(), _mobile_menu_icon(), _mobile_menu_icon_colors(), _user_menu_ids(), _jwt_user_payload() (+82 more)
|
||||
|
||||
### Community 13 - "Community 13"
|
||||
Cohesion: 0.38
|
||||
Nodes (78): BatchStockInBody, BatchStockInItem, BatchStockOutBody, BatchStockOutItem, ConsumableCreateBody, ConsumablePackCreateBody, ConsumablePackUpdateBody, ConsumableUpdateBody (+70 more)
|
||||
Cohesion: 0.07
|
||||
Nodes (42): get_designer_data(), list_params_by_item(), _parse_options_list(), _row_to_dict(), save_params_for_item(), _validate_param(), list_results_by_task(), _list_saved_results_by_task() (+34 more)
|
||||
|
||||
### Community 14 - "Community 14"
|
||||
Cohesion: 0.12
|
||||
Nodes (11): get_wechat_pay_client(), get_wechat_pay_v2(), 微信支付工具模块 支持微信支付 V3 API 文档参考: https://pay.weixin.qq.com/docs/v3/apis/index.ht, 统一下单接口 (V3) Args: out_trade_no: 商户订单号 total, 申请退款 Args: out_trade_no: 商户订单号 total_amount, 小程序调起支付的二次签名参数(与公众号 JSAPI 规则一致), 统一下单 (V2 API) Args: out_trade_no: 商户订单号 tot, 生成签名 V3 (SHA256 with RSA) (+3 more)
|
||||
|
||||
### Community 15 - "Community 15"
|
||||
Cohesion: 0.14
|
||||
Nodes (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 16 - "Community 16"
|
||||
Cohesion: 0.15
|
||||
Nodes (21): batch_create_checkups(), create_checkup(), delete_actual_item(), delete_reserve_item(), get_checkup_by_application(), get_checkup_by_id(), list_actual_items(), list_checkups_paginated() (+13 more)
|
||||
|
||||
### Community 17 - "Community 17"
|
||||
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 18 - "Community 18"
|
||||
### Community 16 - "Community 16"
|
||||
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 17 - "Community 17"
|
||||
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"
|
||||
### Community 18 - "Community 18"
|
||||
Cohesion: 0.22
|
||||
Nodes (15): apply_trip_kit_days(), calc_trip_leg_days(), _day_index(), _end_offset(), _extract_rules(), is_half_day_multiple(), 出差套件:半日制行程时长(与前端 oa-trip-days.ts 对齐), 遍历 tripKit:行程未手改则覆盖 duration;出差天数未手改则覆盖为各行程时长之和。 (+7 more)
|
||||
|
||||
### Community 20 - "Community 20"
|
||||
Cohesion: 0.26
|
||||
Nodes (14): apply_leave_kit_hours(), calc_leave_hours(), _extract_rules(), _kit_child_fields(), _overlap_minutes_on_day(), _parse_dt(), _parse_hm(), 请假套件:双段工时重叠小时(与前端 oa-leave-hours.ts 对齐) (+6 more)
|
||||
|
||||
### Community 21 - "Community 21"
|
||||
### Community 19 - "Community 19"
|
||||
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 22 - "Community 22"
|
||||
### Community 20 - "Community 20"
|
||||
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 23 - "Community 23"
|
||||
### Community 21 - "Community 21"
|
||||
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 24 - "Community 24"
|
||||
### Community 22 - "Community 22"
|
||||
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 25 - "Community 25"
|
||||
### Community 23 - "Community 23"
|
||||
Cohesion: 0.29
|
||||
Nodes (11): _is_empty(), _is_required(), _parse_money(), OA 表单设计器:金额控件 form_data 校验, 校验 form_data 中金额控件的值格式,返回中文错误列表(空表示通过)。, 解析金额值。成功返回 (number, None);失败返回 (None, error_key)。, _rule_props(), _schema_rules() (+3 more)
|
||||
|
||||
### Community 26 - "Community 26"
|
||||
### Community 24 - "Community 24"
|
||||
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 27 - "Community 27"
|
||||
Cohesion: 0.38
|
||||
Nodes (9): create_check_item(), get_check_item(), item_code_exists(), list_active_check_items(), list_check_items(), _order_query(), _row_to_dict(), soft_delete_check_item() (+1 more)
|
||||
### Community 25 - "Community 25"
|
||||
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 28 - "Community 28"
|
||||
### Community 26 - "Community 26"
|
||||
Cohesion: 0.29
|
||||
Nodes (2): BaseModel, SoftDeleteModel
|
||||
|
||||
### Community 29 - "Community 29"
|
||||
Cohesion: 0.39
|
||||
Nodes (7): _allow_modify(), apply_owner_field_defaults(), OA 归属人 / 归属部门:只读强制回填与校验辅助, 归属字段处理: - allowModify 为假:强制写入当前发起人 / 主部门(防篡改) - allowModify 为真且字段为空:补默认值, _rule_props(), _schema_rules(), _walk_rules()
|
||||
|
||||
### Community 30 - "Community 30"
|
||||
### Community 27 - "Community 27"
|
||||
Cohesion: 0.38
|
||||
Nodes (6): derive_legacy_status_from_three(), derive_three_status_from_legacy(), 阶段A兼容:从旧 status 推导三字段。 约束:旧 status 本身混合了支付/配送/订单生命周期,因此只能尽力推断; 若业务侧已有更精确, 阶段A兼容:从三字段反推旧 status(用于旧前端/旧筛选参数兼容)。, ThreeStatus, _to_decimal()
|
||||
|
||||
### Community 31 - "Community 31"
|
||||
### Community 28 - "Community 28"
|
||||
Cohesion: 0.5
|
||||
Nodes (3): BaseSettings, Config, Settings
|
||||
|
||||
### Community 32 - "Community 32"
|
||||
### Community 29 - "Community 29"
|
||||
Cohesion: 0.5
|
||||
Nodes (2): _auto_fill_create_dept_id(), ORM before_insert 钩子:当 create_dept_id 为空时,自动从请求上下文填充当前用户部门 ID
|
||||
|
||||
### Community 33 - "Community 33"
|
||||
Cohesion: 0.67
|
||||
Nodes (2): get_menu_list(), 运营端菜单接口(与 web-admin 前端静态路由 sqxp.ts 对齐,5 个一级菜单分组)
|
||||
|
||||
### Community 34 - "Community 34"
|
||||
### Community 30 - "Community 30"
|
||||
Cohesion: 0.67
|
||||
Nodes (1): 转正套件:入职日 + 试用期月数 → 建议转正日(YYYY-MM-DD)。
|
||||
|
||||
### Community 35 - "Community 35"
|
||||
### Community 31 - "Community 31"
|
||||
Cohesion: 1.0
|
||||
Nodes (1): 质检物料金额查看授权:专用角色与菜单权限常量。
|
||||
|
||||
### Community 36 - "Community 36"
|
||||
### Community 32 - "Community 32"
|
||||
Cohesion: 1.0
|
||||
Nodes (1): 请求级别的用户上下文变量 通过 contextvars 在请求处理链路中传递当前用户信息, 供 SQLAlchemy 事件监听器等底层机制使用,避免逐层手动传
|
||||
|
||||
### Community 37 - "Community 37"
|
||||
### Community 33 - "Community 33"
|
||||
Cohesion: 1.0
|
||||
Nodes (1): API 路由包(骨架:公共鉴权 + 静态菜单 + 运营端占位)
|
||||
|
||||
### Community 40 - "Community 40"
|
||||
### Community 36 - "Community 36"
|
||||
Cohesion: 1.0
|
||||
Nodes (1): 发送订单状态通知 Args: openid: 用户openid ord
|
||||
|
||||
### Community 41 - "Community 41"
|
||||
### Community 37 - "Community 37"
|
||||
Cohesion: 1.0
|
||||
Nodes (1): 发送支付成功通知 Args: openid: 用户openid ord
|
||||
|
||||
### Community 42 - "Community 42"
|
||||
### Community 38 - "Community 38"
|
||||
Cohesion: 1.0
|
||||
Nodes (1): 发送配送通知 Args: openid: 配送员openid task
|
||||
|
||||
### Community 43 - "Community 43"
|
||||
### Community 39 - "Community 39"
|
||||
Cohesion: 1.0
|
||||
Nodes (1): 发送配送时效预警 Args: openid: 配送员openid ta
|
||||
|
||||
### Community 44 - "Community 44"
|
||||
### Community 40 - "Community 40"
|
||||
Cohesion: 1.0
|
||||
Nodes (1): 发送退款结果通知 Args: openid: 用户openid ord
|
||||
|
||||
### Community 45 - "Community 45"
|
||||
### Community 41 - "Community 41"
|
||||
Cohesion: 1.0
|
||||
Nodes (1): 发送催单通知给配送员 Args: deliver_openid: 配送员openid
|
||||
|
||||
## Knowledge Gaps
|
||||
- **173 isolated node(s):** `刷新会话 TTL(refreshToken 续期;已登录用户每次 API 校验通过后滑动续期)`, `写入 refreshToken(用于 one-time 使用/吊销)`, `根据 token 获取当前用户(包含 Redis 会话校验;会话滑动续期)`, `登录凭据 RSA 解密(与前端 JSEncrypt PKCS#1 v1.5 兼容)。`, `加载 RSA 密钥对:优先环境变量,否则使用内置固定默认密钥。` (+168 more)
|
||||
These have ≤1 connection - possible missing edges or undocumented components.
|
||||
- **Thin community `Community 28`** (8 nodes): `BaseModel`, `.to_dict()`, `get_active()`, `base.py`, `SoftDeleteModel`, `.restore()`, `.soft_delete()`, `__tablename__()`
|
||||
- **Thin community `Community 26`** (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 32`** (4 nodes): `_auto_fill_create_dept_id()`, `get_db()`, `database.py`, `ORM before_insert 钩子:当 create_dept_id 为空时,自动从请求上下文填充当前用户部门 ID`
|
||||
- **Thin community `Community 29`** (4 nodes): `_auto_fill_create_dept_id()`, `get_db()`, `database.py`, `ORM before_insert 钩子:当 create_dept_id 为空时,自动从请求上下文填充当前用户部门 ID`
|
||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||
- **Thin community `Community 33`** (3 nodes): `get_menu_list()`, `menu.py`, `运营端菜单接口(与 web-admin 前端静态路由 sqxp.ts 对齐,5 个一级菜单分组)`
|
||||
- **Thin community `Community 30`** (3 nodes): `add_months_ymd()`, `oa_confirmation_date.py`, `转正套件:入职日 + 试用期月数 → 建议转正日(YYYY-MM-DD)。`
|
||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||
- **Thin community `Community 34`** (3 nodes): `add_months_ymd()`, `oa_confirmation_date.py`, `转正套件:入职日 + 试用期月数 → 建议转正日(YYYY-MM-DD)。`
|
||||
- **Thin community `Community 31`** (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 35`** (2 nodes): `qc_material_amount_auth_constants.py`, `质检物料金额查看授权:专用角色与菜单权限常量。`
|
||||
- **Thin community `Community 32`** (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`** (2 nodes): `context.py`, `请求级别的用户上下文变量 通过 contextvars 在请求处理链路中传递当前用户信息, 供 SQLAlchemy 事件监听器等底层机制使用,避免逐层手动传`
|
||||
- **Thin community `Community 33`** (2 nodes): `__init__.py`, `API 路由包(骨架:公共鉴权 + 静态菜单 + 运营端占位)`
|
||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||
- **Thin community `Community 37`** (2 nodes): `__init__.py`, `API 路由包(骨架:公共鉴权 + 静态菜单 + 运营端占位)`
|
||||
- **Thin community `Community 36`** (1 nodes): `发送订单状态通知 Args: openid: 用户openid ord`
|
||||
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 37`** (1 nodes): `发送支付成功通知 Args: openid: 用户openid ord`
|
||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||
- **Thin community `Community 41`** (1 nodes): `发送支付成功通知 Args: openid: 用户openid ord`
|
||||
- **Thin community `Community 38`** (1 nodes): `发送配送通知 Args: openid: 配送员openid task`
|
||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||
- **Thin community `Community 42`** (1 nodes): `发送配送通知 Args: openid: 配送员openid task`
|
||||
- **Thin community `Community 39`** (1 nodes): `发送配送时效预警 Args: openid: 配送员openid ta`
|
||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||
- **Thin community `Community 43`** (1 nodes): `发送配送时效预警 Args: openid: 配送员openid ta`
|
||||
- **Thin community `Community 40`** (1 nodes): `发送退款结果通知 Args: openid: 用户openid ord`
|
||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||
- **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 45`** (1 nodes): `发送催单通知给配送员 Args: deliver_openid: 配送员openid`
|
||||
- **Thin community `Community 41`** (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 4`, `Community 5`, `Community 7`, `Community 8`, `Community 9`, `Community 10`, `Community 11`, `Community 12`, `Community 13`, `Community 15`, `Community 23`, `Community 24`?**
|
||||
_High betweenness centrality (0.112) - this node is a cross-community bridge._
|
||||
- **Why does `success_response()` connect `Community 0` to `Community 1`, `Community 2`, `Community 33`, `Community 8`, `Community 9`, `Community 10`, `Community 11`?**
|
||||
_High betweenness centrality (0.051) - this node is a cross-community bridge._
|
||||
- **Why does `generate_uuid()` connect `Community 1` to `Community 0`, `Community 4`, `Community 5`, `Community 6`, `Community 7`, `Community 9`, `Community 10`, `Community 11`, `Community 12`, `Community 15`, `Community 18`, `Community 21`, `Community 23`, `Community 24`, `Community 26`, `Community 27`?**
|
||||
_High betweenness centrality (0.034) - this node is a cross-community bridge._
|
||||
- **Why does `SysUser` connect `Community 2` to `Community 0`, `Community 1`, `Community 3`, `Community 4`, `Community 6`, `Community 7`, `Community 8`, `Community 9`, `Community 12`, `Community 13`, `Community 21`, `Community 22`?**
|
||||
_High betweenness centrality (0.105) - this node is a cross-community bridge._
|
||||
- **Why does `success_response()` connect `Community 0` to `Community 2`, `Community 4`, `Community 5`, `Community 8`, `Community 9`, `Community 11`, `Community 12`, `Community 13`?**
|
||||
_High betweenness centrality (0.052) - this node is a cross-community bridge._
|
||||
- **Why does `generate_uuid()` connect `Community 1` to `Community 0`, `Community 4`, `Community 5`, `Community 6`, `Community 7`, `Community 9`, `Community 10`, `Community 11`, `Community 12`, `Community 13`, `Community 16`, `Community 17`, `Community 19`, `Community 21`, `Community 22`, `Community 24`?**
|
||||
_High betweenness centrality (0.035) - this node is a cross-community bridge._
|
||||
- **Are the 883 inferred relationships involving `str` (e.g. with `unhandled_exception_handler()` and `get_current_user_by_token()`) actually correct?**
|
||||
_`str` has 883 INFERRED edges - model-reasoned connections that need verification._
|
||||
- **Are the 737 inferred relationships involving `success_response()` (e.g. with `list_storage_release_reviews()` and `get_storage_release_review_detail()`) actually correct?**
|
||||
_`success_response()` has 737 INFERRED edges - model-reasoned connections that need verification._
|
||||
- **Are the 610 inferred relationships involving `error_response()` (e.g. with `_json_error_response()` and `list_storage_release_reviews()`) actually correct?**
|
||||
_`error_response()` has 610 INFERRED edges - model-reasoned connections that need verification._
|
||||
- **Are the 598 inferred relationships involving `SysUser` (e.g. with `数据权限过滤(V0.3:基于角色 data_scope 的查询级行级过滤) data_scope 值: 1 = 全部数据 2 = 本部门数据 3` and `从 SysUser 读取用户的 dept_id。`) actually correct?**
|
||||
_`SysUser` has 598 INFERRED edges - model-reasoned connections that need verification._
|
||||
- **Are the 599 inferred relationships involving `SysUser` (e.g. with `数据权限过滤(V0.3:基于角色 data_scope 的查询级行级过滤) data_scope 值: 1 = 全部数据 2 = 本部门数据 3` and `从 SysUser 读取用户的 dept_id。`) actually correct?**
|
||||
_`SysUser` has 599 INFERRED edges - model-reasoned connections that need verification._
|
||||
+2686
-2470
File diff suppressed because it is too large.
Load diff
@@ -5,7 +5,7 @@
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/core/auth.py": 1786415793.4635684,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/core/api_crypto.py": 1778578258.039314,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/core/login_crypto.py": 1782186069.0708613,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/core/config.py": 1786625149.2414541,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/core/config.py": 1786689356.7261088,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/core/wechat_pay.py": 1778578258.0461762,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/core/api_request_log_middleware.py": 1779882001.753267,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/core/avatar_url.py": 1780540753.5481932,
|
||||
@@ -218,7 +218,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": 1786615625.4962811,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/services/oa_engine.py": 1786689414.8932812,
|
||||
"/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,
|
||||
@@ -237,7 +237,7 @@
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/services/protocol_payment_installment.py": 1781432208.0765035,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/services/protocol_sample_list_summary.py": 1785207474.9871266,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/services/oa_related_approval.py": 1786413868.1112978,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/services/wechat_mp_message.py": 1786627636.595815,
|
||||
"/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_resignation_kit_sync.py": 1785405020.285242,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/services/wechat_mp_auth.py": 1786615330.0733685,
|
||||
@@ -248,7 +248,7 @@
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/services/oa_money_field.py": 1785380625.3526123,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/services/protocol_pay_mode_switch.py": 1785145536.322005,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/services/oa_post_select.py": 1785394392.5716882,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/services/oa_wechat_notify.py": 1786615701.8558133,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/services/oa_wechat_notify.py": 1786689795.048216,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/services/oa_confirmation_kit_sync.py": 1785403667.4270058,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/services/oa_formula.py": 1785145511.513383,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/ws/__init__.py": 1778578258.105995
|
||||
|
||||
+11
-10
@@ -4,11 +4,12 @@
|
||||
# 陇耘鲜达 Backend 测试环境启动脚本
|
||||
# 适配: python3.9+(推荐 venv;显式不使用 python2)
|
||||
#
|
||||
# 测试服务器目录示例: /root/zkxw-backend/backend
|
||||
# 默认监听: 0.0.0.0:8112(生产为 8111,同机部署勿冲突)
|
||||
# 测试服务器目录示例: /root/zkxw/backend
|
||||
# 默认监听: 127.0.0.1:10002(裸机 Nginx 反代;勿与生产 8111 冲突)
|
||||
# 旧双机拓扑 0.0.0.0:8112 已废弃;手动 start/restart 时也走本默认,避免 Nginx 502。
|
||||
#
|
||||
# 首次部署:
|
||||
# cd /root/zkxw-backend/backend
|
||||
# cd /root/zkxw/backend
|
||||
# cp .env.test.example .env.test # 或从 .env.example 复制后按测试库填写
|
||||
# python3.9 -m venv .venv && ./.venv/bin/pip install -r requirements.txt
|
||||
# ./start-test.sh activate # source .venv/bin/activate
|
||||
@@ -21,8 +22,8 @@ BASE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$BASE_DIR"
|
||||
|
||||
APP_MODULE="${APP_MODULE:-app.main:app}"
|
||||
HOST="${HOST:-0.0.0.0}"
|
||||
PORT="${PORT:-8112}"
|
||||
HOST="${HOST:-127.0.0.1}"
|
||||
PORT="${PORT:-10002}"
|
||||
WORKERS="${WORKERS:-1}"
|
||||
LOG_LEVEL="${LOG_LEVEL:-debug}"
|
||||
ENV_FILE="${ENV_FILE:-$BASE_DIR/.env.test}"
|
||||
@@ -69,8 +70,8 @@ show_help() {
|
||||
activate 激活 .venv 并进入交互 shell(等价 source .venv/bin/activate)
|
||||
|
||||
可选环境变量:
|
||||
HOST=0.0.0.0
|
||||
PORT=8112
|
||||
HOST=127.0.0.1
|
||||
PORT=10002
|
||||
WORKERS=1
|
||||
LOG_LEVEL=debug
|
||||
APP_MODULE=app.main:app
|
||||
@@ -79,13 +80,13 @@ show_help() {
|
||||
ENV_FILE=./.env.test
|
||||
|
||||
测试部署目录(示例):
|
||||
/root/zkxw-backend/backend
|
||||
/root/zkxw/backend
|
||||
|
||||
cd /root/zkxw-backend/backend && ./start-test.sh start
|
||||
cd /root/zkxw/backend && ./start-test.sh start
|
||||
|
||||
说明:
|
||||
- 读取 .env.test(非 .env.production)
|
||||
- 默认端口 8112;生产 start-prod.sh 使用 8111
|
||||
- 默认 127.0.0.1:10002(Nginx 反代);生产 start-prod.sh 使用 8111
|
||||
- 可与生产共用 venv,但 PID/日志文件独立,勿混用 start/stop 命令
|
||||
EOF
|
||||
}
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
"""申请人进度通知(每步审批结果)单测。"""
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from app.core.config import settings
|
||||
from app.services.oa_wechat_notify import (
|
||||
RESULT_FAIL,
|
||||
RESULT_OTHER,
|
||||
RESULT_PASS,
|
||||
RESULT_TRANSFER,
|
||||
_send_applicant_progress,
|
||||
map_oa_action_to_result,
|
||||
)
|
||||
|
||||
|
||||
def test_map_oa_action_to_result():
|
||||
assert map_oa_action_to_result("approve") == RESULT_PASS
|
||||
assert map_oa_action_to_result("reject") == RESULT_FAIL
|
||||
assert map_oa_action_to_result("transfer") == RESULT_TRANSFER
|
||||
assert map_oa_action_to_result("return") == RESULT_OTHER
|
||||
assert map_oa_action_to_result("add_sign") == RESULT_OTHER
|
||||
assert map_oa_action_to_result("unknown") == RESULT_OTHER
|
||||
|
||||
|
||||
def test_send_applicant_progress_skips_without_mp():
|
||||
db = MagicMock()
|
||||
inst = MagicMock()
|
||||
inst.id = "i1"
|
||||
inst.app_id = "app1"
|
||||
inst.title = "请假"
|
||||
inst.initiator_user_id = "u-ini"
|
||||
inst.del_flag = 0
|
||||
|
||||
ini = MagicMock()
|
||||
ini.nick_name = "张三"
|
||||
ini.wx_mp_openid = ""
|
||||
|
||||
app = MagicMock()
|
||||
app.app_name = "请假申请"
|
||||
|
||||
actor = MagicMock()
|
||||
actor.nick_name = "李四"
|
||||
|
||||
def query_side_effect(model):
|
||||
q = MagicMock()
|
||||
name = getattr(model, "__name__", "") or str(model)
|
||||
if "OaInstance" in name:
|
||||
q.filter.return_value.first.return_value = inst
|
||||
elif "OaApp" in name:
|
||||
q.filter.return_value.first.return_value = app
|
||||
elif "SysUser" in name:
|
||||
# initiator then actor
|
||||
q.filter.return_value.first.side_effect = [ini, actor]
|
||||
else:
|
||||
q.filter.return_value.first.return_value = None
|
||||
return q
|
||||
|
||||
db.query.side_effect = query_side_effect
|
||||
|
||||
with patch(
|
||||
"app.services.oa_wechat_notify.send_oa_applicant_progress_notify"
|
||||
) as send:
|
||||
_send_applicant_progress(
|
||||
db,
|
||||
instance_id="i1",
|
||||
actor_user_id="u-actor",
|
||||
result=RESULT_PASS,
|
||||
task_id="t1",
|
||||
)
|
||||
send.assert_not_called()
|
||||
|
||||
|
||||
def test_send_applicant_progress_payload_args():
|
||||
db = MagicMock()
|
||||
inst = MagicMock()
|
||||
inst.id = "i1"
|
||||
inst.app_id = "app1"
|
||||
inst.title = "请假"
|
||||
inst.initiator_user_id = "u-ini"
|
||||
|
||||
ini = MagicMock()
|
||||
ini.nick_name = "张三"
|
||||
ini.wx_mp_openid = "mp-ini"
|
||||
|
||||
app = MagicMock()
|
||||
app.app_name = "请假申请"
|
||||
|
||||
actor = MagicMock()
|
||||
actor.nick_name = "李四"
|
||||
|
||||
user_queue = [ini, actor]
|
||||
|
||||
def query_side_effect(model):
|
||||
q = MagicMock()
|
||||
name = getattr(model, "__name__", "") or str(model)
|
||||
if "OaInstance" in name:
|
||||
q.filter.return_value.first.return_value = inst
|
||||
elif "OaApp" in name:
|
||||
q.filter.return_value.first.return_value = app
|
||||
elif "SysUser" in name:
|
||||
q.filter.return_value.first.side_effect = lambda: user_queue.pop(0)
|
||||
else:
|
||||
q.filter.return_value.first.return_value = None
|
||||
return q
|
||||
|
||||
db.query.side_effect = query_side_effect
|
||||
|
||||
with patch(
|
||||
"app.services.oa_wechat_notify.send_oa_applicant_progress_notify"
|
||||
) as send:
|
||||
_send_applicant_progress(
|
||||
db,
|
||||
instance_id="i1",
|
||||
actor_user_id="u-actor",
|
||||
result=RESULT_PASS,
|
||||
task_id="t1",
|
||||
)
|
||||
send.assert_called_once()
|
||||
kwargs = send.call_args.kwargs
|
||||
assert kwargs["mp_openid"] == "mp-ini"
|
||||
assert kwargs["business_name"] == "张三 - 请假申请"
|
||||
assert kwargs["process_name"] == "请假申请"
|
||||
assert kwargs["approver_name"] == "李四"
|
||||
assert kwargs["result"] == RESULT_PASS
|
||||
assert "mode=view" in kwargs["pagepath"]
|
||||
assert "instanceId=i1" in kwargs["pagepath"]
|
||||
assert (
|
||||
"%2Fpages%2Foa%2Fmine%2Findex" in kwargs["pagepath"]
|
||||
or "/pages/oa/mine/index" in kwargs["pagepath"]
|
||||
)
|
||||
# 我发起的查看:不强制带 taskId
|
||||
assert "taskId=0" not in kwargs["pagepath"]
|
||||
@@ -1,7 +1,11 @@
|
||||
"""OA 微信服务号通知钩子单测。"""
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from app.services.oa_wechat_notify import _send_one, build_oa_detail_pagepath
|
||||
from app.services.oa_wechat_notify import (
|
||||
_build_matter_title,
|
||||
_send_one,
|
||||
build_oa_detail_pagepath,
|
||||
)
|
||||
|
||||
|
||||
def test_build_pagepath_no_leading_slash():
|
||||
@@ -12,6 +16,60 @@ def test_build_pagepath_no_leading_slash():
|
||||
assert "taskId=t1" in p
|
||||
|
||||
|
||||
def test_build_pagepath_three_notify_targets():
|
||||
"""三种通知:审批待办 / 抄送查看 / 我发起查看,mode + returnPath 不同。"""
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
|
||||
from app.services.oa_wechat_notify import (
|
||||
RETURN_CC,
|
||||
RETURN_MINE,
|
||||
RETURN_PENDING,
|
||||
)
|
||||
|
||||
appr = build_oa_detail_pagepath(
|
||||
instance_id="i1",
|
||||
task_id="t1",
|
||||
mode="approve",
|
||||
return_path=RETURN_PENDING,
|
||||
)
|
||||
q1 = parse_qs(urlparse("x://" + appr).query)
|
||||
assert q1["mode"] == ["approve"]
|
||||
assert q1["taskId"] == ["t1"]
|
||||
assert q1["returnPath"] == [RETURN_PENDING]
|
||||
|
||||
cc = build_oa_detail_pagepath(
|
||||
instance_id="i1",
|
||||
task_id="t-cc",
|
||||
mode="view",
|
||||
return_path=RETURN_CC,
|
||||
)
|
||||
q2 = parse_qs(urlparse("x://" + cc).query)
|
||||
assert q2["mode"] == ["view"]
|
||||
assert q2["taskId"] == ["t-cc"]
|
||||
assert q2["returnPath"] == [RETURN_CC]
|
||||
|
||||
mine = build_oa_detail_pagepath(
|
||||
instance_id="i1",
|
||||
task_id="",
|
||||
mode="view",
|
||||
return_path=RETURN_MINE,
|
||||
)
|
||||
q3 = parse_qs(urlparse("x://" + mine).query)
|
||||
assert q3["mode"] == ["view"]
|
||||
assert "taskId" not in q3
|
||||
assert q3["returnPath"] == [RETURN_MINE]
|
||||
|
||||
|
||||
def test_build_matter_title_applicant_process():
|
||||
assert _build_matter_title(
|
||||
applicant="李四", process_name="请假申请", fallback="x"
|
||||
) == "李四 - 请假申请"
|
||||
assert _build_matter_title(applicant="", process_name="请假申请", fallback="x") == (
|
||||
"请假申请"
|
||||
)
|
||||
assert _build_matter_title(applicant="", process_name="", fallback="审批") == "审批"
|
||||
|
||||
|
||||
def test_send_one_routes_approval_vs_cc():
|
||||
db = MagicMock()
|
||||
|
||||
@@ -22,16 +80,24 @@ def test_send_one_routes_approval_vs_cc():
|
||||
inst = MagicMock()
|
||||
inst.title = "请假"
|
||||
inst.initiator_user_id = "u-ini"
|
||||
inst.app_id = "app-1"
|
||||
|
||||
ini = MagicMock()
|
||||
ini.nick_name = "李四"
|
||||
|
||||
app = MagicMock()
|
||||
app.app_name = "请假申请"
|
||||
|
||||
# approval + cc 各查 assignee、initiator
|
||||
user_results = [user, ini, user, ini]
|
||||
|
||||
def query_side_effect(model):
|
||||
q = MagicMock()
|
||||
name = getattr(model, "__name__", "") or str(model)
|
||||
if "SysUser" in name:
|
||||
# first call: assignee; second: initiator (approval path may call twice)
|
||||
q.filter.return_value.first.side_effect = [user, ini]
|
||||
q.filter.return_value.first.side_effect = lambda: user_results.pop(0)
|
||||
elif "OaApp" in name:
|
||||
q.filter.return_value.first.return_value = app
|
||||
else:
|
||||
q.filter.return_value.first.return_value = inst
|
||||
return q
|
||||
@@ -57,9 +123,14 @@ def test_send_one_routes_approval_vs_cc():
|
||||
kwargs = send_appr.call_args.kwargs
|
||||
assert kwargs["mp_openid"] == "mp-openid-1"
|
||||
assert "mode=approve" in kwargs["pagepath"]
|
||||
|
||||
# reset query for cc path
|
||||
db.query.side_effect = query_side_effect
|
||||
assert "returnPath=" in kwargs["pagepath"]
|
||||
assert "%2Fpages%2Foa%2Fpending%2Findex" in kwargs["pagepath"] or (
|
||||
"/pages/oa/pending/index" in kwargs["pagepath"]
|
||||
)
|
||||
assert kwargs["title"] == "李四 - 请假申请"
|
||||
assert kwargs["applicant"] == "李四"
|
||||
assert kwargs["process_name"] == "请假申请"
|
||||
assert kwargs["node_name"] == "主管审批"
|
||||
|
||||
cc_task = MagicMock()
|
||||
cc_task.id = "t-cc"
|
||||
@@ -79,3 +150,9 @@ def test_send_one_routes_approval_vs_cc():
|
||||
send_appr.assert_not_called()
|
||||
kwargs = send_cc.call_args.kwargs
|
||||
assert "mode=view" in kwargs["pagepath"]
|
||||
assert (
|
||||
"%2Fpages%2Foa%2Fcc%2Findex" in kwargs["pagepath"]
|
||||
or "/pages/oa/cc/index" in kwargs["pagepath"]
|
||||
)
|
||||
assert kwargs["title"] == "李四 - 请假申请"
|
||||
assert kwargs["process_name"] == "请假申请"
|
||||
@@ -52,11 +52,11 @@ def test_send_approval_success_payload_shape(mock_client_cls, _mock_token):
|
||||
|
||||
with patch.object(settings, "WX_MP_TEMPLATE_APPROVAL", "tpl_approval"), patch.object(
|
||||
settings, "WXPAY_MINI_APPID", "wx_mini_appid"
|
||||
), patch.object(settings, "WX_MP_TPL_FIELD_TITLE", "thing1"), patch.object(
|
||||
settings, "WX_MP_TPL_FIELD_APPLICANT", "thing2"
|
||||
), patch.object(settings, "WX_MP_TPL_FIELD_NODE", "thing3"), patch.object(
|
||||
settings, "WX_MP_TPL_FIELD_TIME", "time4"
|
||||
):
|
||||
), patch.object(settings, "WX_MP_TPL_FIELD_TITLE", "thing12"), patch.object(
|
||||
settings, "WX_MP_TPL_FIELD_APPLICANT", "thing21"
|
||||
), patch.object(settings, "WX_MP_TPL_FIELD_NODE", "thing7"), patch.object(
|
||||
settings, "WX_MP_TPL_FIELD_TIME", "time3"
|
||||
), patch.object(settings, "WX_MP_TPL_FIELD_PROCESS", "thing48"):
|
||||
r = send_oa_approval_notify(
|
||||
mp_openid="mp_openid_1",
|
||||
pagepath="/pages/oa/webview/index?instanceId=i1&taskId=t1&mode=approve",
|
||||
@@ -64,6 +64,7 @@ def test_send_approval_success_payload_shape(mock_client_cls, _mock_token):
|
||||
applicant="张三",
|
||||
node_name="主管审批",
|
||||
time_text="2026-08-13 12:00",
|
||||
process_name="请假申请",
|
||||
)
|
||||
|
||||
assert r == {"success": True, "msgid": 1001}
|
||||
@@ -79,10 +80,50 @@ def test_send_approval_success_payload_shape(mock_client_cls, _mock_token):
|
||||
"appid": "wx_mini_appid",
|
||||
"pagepath": "pages/oa/webview/index?instanceId=i1&taskId=t1&mode=approve",
|
||||
}
|
||||
assert payload["data"]["thing1"]["value"] == long_title[:20]
|
||||
assert payload["data"]["thing2"]["value"] == "张三"
|
||||
assert payload["data"]["thing3"]["value"] == "主管审批"
|
||||
assert payload["data"]["time4"]["value"] == "2026-08-13 12:00"
|
||||
assert payload["data"]["thing12"]["value"] == long_title[:20]
|
||||
assert payload["data"]["thing21"]["value"] == "张三"
|
||||
assert payload["data"]["thing7"]["value"] == "主管审批"
|
||||
assert payload["data"]["time3"]["value"] == "2026-08-13 12:00"
|
||||
assert payload["data"]["thing48"]["value"] == "请假申请"
|
||||
|
||||
|
||||
@patch("app.services.wechat_mp_message.get_mp_access_token", return_value="tok_mp")
|
||||
@patch("app.services.wechat_mp_message.httpx.Client")
|
||||
def test_send_approval_skips_blank_field_keys(mock_client_cls, _mock_token):
|
||||
"""临时模板仅申请人+时间时,TITLE/NODE 留空不进 data。"""
|
||||
from app.services.wechat_mp_message import send_oa_approval_notify
|
||||
|
||||
mock_resp = MagicMock()
|
||||
mock_resp.raise_for_status = MagicMock()
|
||||
mock_resp.json.return_value = {"errcode": 0, "msgid": 3003}
|
||||
mock_client = MagicMock()
|
||||
mock_client.__enter__.return_value = mock_client
|
||||
mock_client.__exit__.return_value = False
|
||||
mock_client.post.return_value = mock_resp
|
||||
mock_client_cls.return_value = mock_client
|
||||
|
||||
with patch.object(settings, "WX_MP_TEMPLATE_APPROVAL", "tpl_tmp"), patch.object(
|
||||
settings, "WXPAY_MINI_APPID", "wx_mini_appid"
|
||||
), patch.object(settings, "WX_MP_TPL_FIELD_TITLE", ""), patch.object(
|
||||
settings, "WX_MP_TPL_FIELD_APPLICANT", "thing1"
|
||||
), patch.object(settings, "WX_MP_TPL_FIELD_NODE", ""), patch.object(
|
||||
settings, "WX_MP_TPL_FIELD_TIME", "time2"
|
||||
), patch.object(settings, "WX_MP_TPL_FIELD_PROCESS", ""):
|
||||
r = send_oa_approval_notify(
|
||||
mp_openid="mp_openid_1",
|
||||
pagepath="pages/oa/webview/index?instanceId=i1&taskId=t1&mode=approve",
|
||||
title="请假",
|
||||
applicant="张三",
|
||||
node_name="主管审批",
|
||||
time_text="2026-08-13 12:00",
|
||||
)
|
||||
|
||||
assert r.get("success") is True
|
||||
data = mock_client.post.call_args.kwargs["json"]["data"]
|
||||
assert data == {
|
||||
"thing1": {"value": "张三"},
|
||||
"time2": {"value": "2026-08-13 12:00"},
|
||||
}
|
||||
|
||||
|
||||
@patch("app.services.wechat_mp_message.get_mp_access_token", return_value="tok_mp")
|
||||
@@ -101,14 +142,19 @@ def test_send_cc_success_payload_shape(mock_client_cls, _mock_token):
|
||||
|
||||
with patch.object(settings, "WX_MP_TEMPLATE_CC", "tpl_cc"), patch.object(
|
||||
settings, "WXPAY_MINI_APPID", "wx_mini_appid"
|
||||
), patch.object(settings, "WX_MP_TPL_CC_FIELD_TITLE", "thing5"), patch.object(
|
||||
settings, "WX_MP_TPL_CC_FIELD_APPLICANT", "thing2"
|
||||
), patch.object(settings, "WX_MP_TPL_CC_FIELD_TIME", "time3"), patch.object(
|
||||
settings, "WX_MP_TPL_CC_FIELD_PROCESS", "thing1"
|
||||
):
|
||||
r = send_oa_cc_notify(
|
||||
mp_openid="mp_openid_2",
|
||||
pagepath="pages/oa/webview/index?instanceId=i2&taskId=t2&mode=view",
|
||||
title="报销",
|
||||
title="李四 - 报销申请",
|
||||
applicant="李四",
|
||||
node_name="抄送知会",
|
||||
time_text="2026-08-13 15:30",
|
||||
process_name="报销申请",
|
||||
)
|
||||
|
||||
assert r == {"success": True, "msgid": 2002}
|
||||
@@ -117,5 +163,102 @@ def test_send_cc_success_payload_shape(mock_client_cls, _mock_token):
|
||||
assert payload["template_id"] == "tpl_cc"
|
||||
assert payload["miniprogram"]["appid"] == "wx_mini_appid"
|
||||
assert payload["miniprogram"]["pagepath"].endswith("mode=view")
|
||||
assert "thing1" in payload["data"]
|
||||
assert payload["data"]["thing1"]["value"] == "报销"
|
||||
assert payload["data"] == {
|
||||
"thing5": {"value": "李四 - 报销申请"},
|
||||
"thing2": {"value": "李四"},
|
||||
"time3": {"value": "2026-08-13 15:30"},
|
||||
"thing1": {"value": "报销申请"},
|
||||
}
|
||||
|
||||
|
||||
@patch("app.services.wechat_mp_message.get_mp_access_token", return_value="tok_mp")
|
||||
@patch("app.services.wechat_mp_message.httpx.Client")
|
||||
def test_send_applicant_progress_payload_shape(mock_client_cls, _mock_token):
|
||||
from app.services.wechat_mp_message import send_oa_applicant_progress_notify
|
||||
|
||||
mock_resp = MagicMock()
|
||||
mock_resp.raise_for_status = MagicMock()
|
||||
mock_resp.json.return_value = {"errcode": 0, "msgid": 5005}
|
||||
mock_client = MagicMock()
|
||||
mock_client.__enter__.return_value = mock_client
|
||||
mock_client.__exit__.return_value = False
|
||||
mock_client.post.return_value = mock_resp
|
||||
mock_client_cls.return_value = mock_client
|
||||
|
||||
with patch.object(
|
||||
settings, "WX_MP_TEMPLATE_APPLICANT_PROGRESS", "tpl_progress"
|
||||
), patch.object(settings, "WXPAY_MINI_APPID", "wx_mini_appid"), patch.object(
|
||||
settings, "WX_MP_TPL_PROGRESS_FIELD_BUSINESS", "thing5"
|
||||
), patch.object(
|
||||
settings, "WX_MP_TPL_PROGRESS_FIELD_PROCESS", "thing1"
|
||||
), patch.object(
|
||||
settings, "WX_MP_TPL_PROGRESS_FIELD_APPROVER", "thing6"
|
||||
), patch.object(
|
||||
settings, "WX_MP_TPL_PROGRESS_FIELD_RESULT", "const4"
|
||||
), patch.object(settings, "WX_MP_TPL_PROGRESS_FIELD_TIME", "time11"):
|
||||
r = send_oa_applicant_progress_notify(
|
||||
mp_openid="mp_ini",
|
||||
pagepath="pages/oa/webview/index?instanceId=i1&taskId=t1&mode=view",
|
||||
business_name="张三 - 请假申请",
|
||||
process_name="请假申请",
|
||||
approver_name="李四",
|
||||
result="通过",
|
||||
time_text="2026-08-14 14:30",
|
||||
)
|
||||
|
||||
assert r == {"success": True, "msgid": 5005}
|
||||
payload = mock_client.post.call_args.kwargs["json"]
|
||||
assert payload["template_id"] == "tpl_progress"
|
||||
assert payload["data"] == {
|
||||
"thing5": {"value": "张三 - 请假申请"},
|
||||
"thing1": {"value": "请假申请"},
|
||||
"thing6": {"value": "李四"},
|
||||
"const4": {"value": "通过"},
|
||||
"time11": {"value": "2026-08-14 14:30"},
|
||||
}
|
||||
|
||||
|
||||
@patch("app.services.wechat_mp_message.get_mp_access_token", return_value="tok_mp")
|
||||
@patch("app.services.wechat_mp_message.httpx.Client")
|
||||
def test_send_approval_retries_without_miniprogram_on_40165(
|
||||
mock_client_cls, _mock_token
|
||||
):
|
||||
from app.services.wechat_mp_message import send_oa_approval_notify
|
||||
|
||||
mock_resp_fail = MagicMock()
|
||||
mock_resp_fail.raise_for_status = MagicMock()
|
||||
mock_resp_fail.json.return_value = {
|
||||
"errcode": 40165,
|
||||
"errmsg": "invalid weapp pagepath",
|
||||
}
|
||||
mock_resp_ok = MagicMock()
|
||||
mock_resp_ok.raise_for_status = MagicMock()
|
||||
mock_resp_ok.json.return_value = {"errcode": 0, "msgid": 4004}
|
||||
mock_client = MagicMock()
|
||||
mock_client.__enter__.return_value = mock_client
|
||||
mock_client.__exit__.return_value = False
|
||||
mock_client.post.side_effect = [mock_resp_fail, mock_resp_ok]
|
||||
mock_client_cls.return_value = mock_client
|
||||
|
||||
with patch.object(settings, "WX_MP_TEMPLATE_APPROVAL", "tpl_approval"), patch.object(
|
||||
settings, "WXPAY_MINI_APPID", "wx_mini_appid"
|
||||
), patch.object(settings, "WX_MP_TPL_FIELD_TITLE", ""), patch.object(
|
||||
settings, "WX_MP_TPL_FIELD_APPLICANT", "thing1"
|
||||
), patch.object(settings, "WX_MP_TPL_FIELD_NODE", ""), patch.object(
|
||||
settings, "WX_MP_TPL_FIELD_TIME", "time2"
|
||||
), patch.object(settings, "WX_MP_TPL_FIELD_PROCESS", ""):
|
||||
r = send_oa_approval_notify(
|
||||
mp_openid="mp_openid_1",
|
||||
pagepath="pages/oa/webview/index?instanceId=i1&taskId=t1&mode=approve",
|
||||
title="请假",
|
||||
applicant="张三",
|
||||
node_name="主管审批",
|
||||
time_text="2026-08-13 12:00",
|
||||
)
|
||||
|
||||
assert r == {"success": True, "msgid": 4004}
|
||||
assert mock_client.post.call_count == 2
|
||||
first = mock_client.post.call_args_list[0].kwargs["json"]
|
||||
second = mock_client.post.call_args_list[1].kwargs["json"]
|
||||
assert "miniprogram" in first
|
||||
assert "miniprogram" not in second
|
||||
@@ -1,8 +0,0 @@
|
||||
-- 员工微信小程序 openid 绑定字段(微信授权登录)
|
||||
-- MySQL 5.7:唯一索引允许多个 NULL
|
||||
|
||||
ALTER TABLE `sys_user`
|
||||
ADD COLUMN `wx_openid` varchar(64) DEFAULT NULL COMMENT '微信小程序 openid' AFTER `phone_no`;
|
||||
|
||||
ALTER TABLE `sys_user`
|
||||
ADD UNIQUE INDEX `uk_sys_user_wx_openid` (`wx_openid`);
|
||||
@@ -0,0 +1,57 @@
|
||||
-- 将「开启微信通知」(OA007) 授予全部未删除角色
|
||||
-- 前置:已执行 docs/sql/mobile_menu_oa_wechat_notify.sql(菜单须已存在)
|
||||
-- 幂等:可重复执行;已有有效授权跳过;软删则恢复
|
||||
-- 说明:
|
||||
-- 1) 用户变量显式 utf8mb4_unicode_ci,避免 collation 混用报错 1267
|
||||
-- 2) 主键用 MD5(role_id|menu_id),避免 INSERT…SELECT 中 UUID() 只算一次导致重复主键
|
||||
|
||||
SET @menu_id = CONVERT('MINI000000000000000000000000OA007' USING utf8mb4) COLLATE utf8mb4_unicode_ci;
|
||||
SET @tenant_no = CONVERT('zkxw' USING utf8mb4) COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- 1) 恢复历史上已软删的授权(按 role_id + menu_id)
|
||||
UPDATE sys_role_menu rm
|
||||
INNER JOIN sys_role r
|
||||
ON r.id COLLATE utf8mb4_unicode_ci = rm.role_id COLLATE utf8mb4_unicode_ci
|
||||
AND r.del_flag = 0
|
||||
AND r.tenant_no COLLATE utf8mb4_unicode_ci = @tenant_no
|
||||
SET
|
||||
rm.del_flag = 0,
|
||||
rm.update_time = NOW(),
|
||||
rm.remark = 'grant wechat-notify to all roles'
|
||||
WHERE rm.menu_id COLLATE utf8mb4_unicode_ci = @menu_id
|
||||
AND rm.tenant_no COLLATE utf8mb4_unicode_ci = @tenant_no
|
||||
AND rm.del_flag = 1;
|
||||
|
||||
-- 2) 为尚未关联该菜单的角色新增授权
|
||||
INSERT INTO sys_role_menu (
|
||||
id, role_id, menu_id, tenant_no, order_num, remark,
|
||||
create_by, create_dept_id, create_time, update_by, update_time, del_flag
|
||||
)
|
||||
SELECT
|
||||
UPPER(MD5(CONCAT(r.id, '|', @menu_id))),
|
||||
r.id,
|
||||
@menu_id,
|
||||
@tenant_no,
|
||||
0,
|
||||
'grant wechat-notify to all roles',
|
||||
'admin',
|
||||
'',
|
||||
NOW(),
|
||||
NULL,
|
||||
NOW(),
|
||||
0
|
||||
FROM sys_role r
|
||||
WHERE r.del_flag = 0
|
||||
AND r.tenant_no COLLATE utf8mb4_unicode_ci = @tenant_no
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM sys_menu m
|
||||
WHERE m.id COLLATE utf8mb4_unicode_ci = @menu_id
|
||||
AND m.del_flag = 0
|
||||
)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM sys_role_menu rm
|
||||
WHERE rm.role_id COLLATE utf8mb4_unicode_ci = r.id COLLATE utf8mb4_unicode_ci
|
||||
AND rm.menu_id COLLATE utf8mb4_unicode_ci = @menu_id
|
||||
AND rm.tenant_no COLLATE utf8mb4_unicode_ci = @tenant_no
|
||||
AND rm.del_flag = 0
|
||||
);
|
||||
@@ -1,17 +0,0 @@
|
||||
-- 中国节假日日历缓存
|
||||
-- MySQL 5.7 / utf8mb4
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `sys_cn_holiday_day` (
|
||||
`id` VARCHAR(64) NOT NULL COMMENT 'UUID',
|
||||
`day_date` DATE NOT NULL COMMENT '日历日',
|
||||
`day_type` TINYINT NOT NULL COMMENT '0工作日1周末2节日3调休放假4补班',
|
||||
`day_name` VARCHAR(64) DEFAULT NULL COMMENT '名称',
|
||||
`week` TINYINT DEFAULT NULL COMMENT '1-7周一到周日',
|
||||
`is_rest_day` TINYINT NOT NULL DEFAULT 0 COMMENT '1休息0工作',
|
||||
`raw_json` TEXT COMMENT '原始JSON片段',
|
||||
`sync_time` DATETIME DEFAULT NULL COMMENT '同步时间',
|
||||
`create_time` DATETIME DEFAULT NULL,
|
||||
`update_time` DATETIME DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_day_date` (`day_date`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='中国节假日日历缓存';
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.1 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 74 B After Width: | Height: | Size: 27 KiB |
@@ -33,7 +33,7 @@ export const MINIO_BASE_URL = USE_REMOTE_TEST
|
||||
: LOCAL_MINIO_BASE_URL
|
||||
|
||||
/** 服务号原始 ID(gh_…),用于 openOfficialAccountProfile */
|
||||
export const MP_GH_USERNAME = '' // 上线前填写
|
||||
export const MP_GH_USERNAME = 'gh_e0b5daf10934'
|
||||
/** 关注二维码图片(兜底) */
|
||||
export const MP_QRCODE_URL = '/static/mp-qrcode.png'
|
||||
/** 绑定 H5 路径(相对 OA_H5_ORIGIN) */
|
||||
|
||||
@@ -2,4 +2,4 @@ VITE_API_BASE=/api
|
||||
# 与 web-admin `.env.development` / mobile-h5 LOCAL_MINIO 对齐
|
||||
VITE_MINIO_BASE_URL=http://47.99.164.66:20004/zkxw
|
||||
# 微信服务号 AppID(OA 审批通知网页授权绑定;勿放 Secret)
|
||||
VITE_WX_MP_APPID=
|
||||
VITE_WX_MP_APPID=wxa20cdbd5ad8b28f2
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 4.1 KiB |
Reference in new issue
Block a user