This commit is contained in:
Your Name
2026-08-13 14:22:19 +08:00
parent 72a5300b08
commit cbe71905c0
16 changed files with 347 additions and 375 deletions
@@ -145,7 +145,7 @@
<div class="panel-heading panel-heading--table">
<div>
<h2>明细数据列表</h2>
<p>展开部门可查看人员明细加粉=总进线=区间有效加粉按员工+客户去重须会话同意剔除已删客户继承客户及区间前已加过的重加挂号=已支付且实收低于 10 元的订单预约=有效预约记录开口率=开口/加粉挂号率=挂号/加粉面诊率=面诊/挂号看挂号后流失预约率=面诊/预约看预约后未面诊面诊接诊率=接诊诊单/面诊接诊率=接诊诊单/总进线</p>
<p>展开部门可查看人员明细加粉=总进线=区间有效加粉按员工+客户去重须会话同意剔除已删客户继承客户扫一扫/搜手机号/名片分享添加及区间前已加过的重加挂号=已支付且实收低于 10 元的订单预约=有效预约记录开口率=开口/加粉挂号率=挂号/加粉面诊率=面诊/挂号看挂号后流失预约率=面诊/预约看预约后未面诊面诊接诊率=接诊诊单/面诊接诊率=接诊诊单/总进线</p>
</div>
<span>{{ dashboard.rows.length }} 个顶层节点</span>
</div>
@@ -330,7 +330,7 @@ const timeOptions = [
{ label: '自定义', value: 'custom' }
]
const metricCards: Array<{ key: string; label: string; type: MetricType; hint: string }> = [
{ key: 'add_fans_count', label: '加粉数', type: 'count', hint: '区间有效加粉:去重,须会话同意,剔除已删客户、继承客户及区间前已加过的重加' },
{ key: 'add_fans_count', label: '加粉数', type: 'count', hint: '区间有效加粉:去重,须会话同意,剔除已删客户、继承客户、扫一扫/搜手机号/名片分享添加及区间前已加过的重加' },
{ key: 'total_open_count', label: '开口数', type: 'count', hint: '来源于个人业绩录入' },
{ key: 'interview_count', label: '面诊', type: 'count', hint: '已完成预约' },
{ key: 'completed_order_count', label: '接诊诊单', type: 'count', hint: '业务订单,按创建人归属并过滤无效单' },
-204
View File
@@ -1,204 +0,0 @@
# -*- coding: utf-8 -*-
import json
import sys
from collections import defaultdict
from datetime import datetime
import pymysql
sys.stdout.reconfigure(encoding="utf-8")
conn = pymysql.connect(
host="127.0.0.1", user="root", password="root", database="zyt", charset="utf8mb4"
)
cur = conn.cursor(pymysql.cursors.DictCursor)
START, END = 1786464000, 1786550399 # 2026-08-12
def q(sql, args=None):
cur.execute(sql, args or ())
return cur.fetchall()
def parse_follow(follow):
if isinstance(follow, str) and follow:
try:
return json.loads(follow)
except Exception:
return []
return follow if isinstance(follow, list) else []
def contact_info(ext, user_id):
rows = q(
"SELECT id, name, follow_users, delete_time FROM zyt_qywx_external_contact WHERE external_userid=%s ORDER BY id DESC",
(ext,),
)
name = None
remark = None
add_way = None
still = False
createtime = None
for c in rows:
if name is None:
name = c.get("name")
for fu in parse_follow(c.get("follow_users")):
if not isinstance(fu, dict):
continue
uid = str(fu.get("userid") or fu.get("UserId") or "")
if uid == user_id:
still = True
remark = fu.get("remark") or fu.get("Remark") or remark
add_way = fu.get("add_way", fu.get("AddWay"))
createtime = fu.get("createtime") or fu.get("create_time") or createtime
if remark:
break
return {
"name": name,
"remark": remark,
"add_way": add_way,
"still_following": still,
"follow_createtime": createtime,
"follow_createtime_at": datetime.fromtimestamp(int(createtime)).strftime("%Y-%m-%d %H:%M:%S")
if createtime
else None,
"contact_rows": len(rows),
}
def events_for(user_id, ext):
return q(
"""
SELECT change_type, FROM_UNIXTIME(event_time) AS event_at
FROM zyt_qywx_external_contact_event
WHERE user_id=%s AND external_userid=%s
AND change_type IN ('add_external_contact','del_follow_user','del_external_contact','msg_audit_approved')
ORDER BY event_time
""",
(user_id, ext),
)
def analyze(label, user_id):
print(f"\n========== {label} / {user_id} ==========")
admins = q(
"SELECT id, name, work_wechat_userid, disable, delete_time FROM zyt_admin WHERE work_wechat_userid=%s OR name LIKE %s",
(user_id, f"%{label}%"),
)
print("admin", json.dumps(admins, ensure_ascii=False, default=str))
raw = q(
"""
SELECT e.external_userid, COUNT(*) AS add_events,
FROM_UNIXTIME(MIN(e.event_time)) AS first_in_day,
FROM_UNIXTIME(MAX(e.event_time)) AS last_in_day
FROM zyt_qywx_external_contact_event e
WHERE e.change_type='add_external_contact'
AND e.user_id=%s
AND e.event_time BETWEEN %s AND %s
AND e.external_userid <> ''
GROUP BY e.external_userid
ORDER BY MIN(e.event_time)
""",
(user_id, START, END),
)
print("raw unique adds", len(raw))
def filtered(extra_prev=False):
sql = """
SELECT e.external_userid, MIN(e.event_time) AS first_add,
FROM_UNIXTIME(MIN(e.event_time)) AS first_add_at
FROM zyt_qywx_external_contact_event e
WHERE e.change_type='add_external_contact'
AND e.user_id=%s
AND e.event_time BETWEEN %s AND %s
AND e.external_userid <> ''
AND EXISTS (
SELECT 1 FROM zyt_qywx_external_contact_event audit_e
WHERE audit_e.user_id=e.user_id
AND audit_e.external_userid=e.external_userid
AND audit_e.change_type='msg_audit_approved'
AND audit_e.event_time >= e.event_time
AND audit_e.event_time <= %s
)
AND NOT EXISTS (
SELECT 1 FROM zyt_qywx_external_contact_event del_e
WHERE del_e.user_id=e.user_id
AND del_e.external_userid=e.external_userid
AND del_e.change_type='del_external_contact'
AND del_e.event_time >= e.event_time
AND del_e.event_time <= %s
)
"""
args = [user_id, START, END, END, END]
if extra_prev:
sql += """
AND NOT EXISTS (
SELECT 1 FROM zyt_qywx_external_contact_event prev_e
WHERE prev_e.user_id=e.user_id
AND prev_e.external_userid=e.external_userid
AND prev_e.change_type='add_external_contact'
AND prev_e.event_time < %s
)
"""
args.append(START)
sql += " GROUP BY e.external_userid ORDER BY first_add"
return q(sql, args)
old_pairs = filtered(False)
new_pairs = filtered(True)
print("system OLD (before readd fix)", len(old_pairs))
print("system NEW (after readd fix)", len(new_pairs))
old_ids = {p["external_userid"] for p in old_pairs}
new_ids = {p["external_userid"] for p in new_pairs}
raw_ids = {p["external_userid"] for p in raw}
dropped_no_audit_or_del = raw_ids - old_ids
dropped_readd = old_ids - new_ids
print("\n--- dropped vs raw (no audit / customer-deleted) ---")
for ext in sorted(dropped_no_audit_or_del, key=lambda x: next(r["first_in_day"] for r in raw if r["external_userid"] == x)):
info = contact_info(ext, user_id)
ev = events_for(user_id, ext)
print(json.dumps({"ext": ext, **info, "events": ev, "raw": next(r for r in raw if r["external_userid"] == ext)}, ensure_ascii=False, default=str))
print("\n--- dropped as re-add (old counted, new excluded) ---")
for ext in sorted(dropped_readd):
info = contact_info(ext, user_id)
ev = events_for(user_id, ext)
print(json.dumps({"ext": ext, **info, "events": ev}, ensure_ascii=False, default=str))
print("\n--- inheritance add_way 201/202 among NEW pairs ---")
for p in new_pairs:
info = contact_info(p["external_userid"], user_id)
if info["add_way"] in (201, 202):
print(json.dumps({"ext": p["external_userid"], **info, "first_add_at": p["first_add_at"]}, ensure_ascii=False, default=str))
print("\n--- NEW counted list ---")
for p in new_pairs:
info = contact_info(p["external_userid"], user_id)
types = [e["change_type"] for e in events_for(user_id, p["external_userid"])]
print(json.dumps({
"first_add_at": p["first_add_at"],
"name": info["name"],
"remark": info["remark"],
"add_way": info["add_way"],
"follow_createtime_at": info["follow_createtime_at"],
"still_following": info["still_following"],
"event_types": sorted(set(types)),
}, ensure_ascii=False, default=str))
# people in old but check missing audit on the in-day add specifically
print("\n--- raw adds missing audit after THAT add or del_external ---")
for r in raw:
ext = r["external_userid"]
if ext in old_ids:
continue
ev = events_for(user_id, ext)
print(json.dumps({"ext": ext, "raw": r, "events": ev, **contact_info(ext, user_id)}, ensure_ascii=False, default=str))
analyze("李晓焕", "LiXiaoHuan")
analyze("彭世博", "GeiWoNaHaoDeA")
-87
View File
@@ -1,87 +0,0 @@
# -*- coding: utf-8 -*-
import json
import sys
import pymysql
sys.stdout.reconfigure(encoding="utf-8")
conn = pymysql.connect(host="127.0.0.1", user="root", password="root", database="zyt", charset="utf8mb4")
cur = conn.cursor(pymysql.cursors.DictCursor)
def q(sql, args=None):
cur.execute(sql, args or ())
return cur.fetchall()
print("=== dept bindings ===")
print(json.dumps(q("""
SELECT a.id, a.name, a.work_wechat_userid, d.id AS dept_id, d.name AS dept_name
FROM zyt_admin a
LEFT JOIN zyt_admin_dept ad ON ad.admin_id=a.id
LEFT JOIN zyt_dept d ON d.id=ad.dept_id AND d.delete_time IS NULL
WHERE a.work_wechat_userid IN ('LiXiaoHuan','GeiWoNaHaoDeA','mu')
ORDER BY a.id, d.id
"""), ensure_ascii=False, default=str, indent=2))
print("=== 员工@ contact type ===")
print(json.dumps(q("""
SELECT c.id, c.external_userid, c.name, c.type, c.corp_name, c.gender
FROM zyt_qywx_external_contact_event e
JOIN zyt_qywx_external_contact c ON c.external_userid=e.external_userid
WHERE e.user_id='LiXiaoHuan' AND e.change_type='add_external_contact'
AND e.event_time BETWEEN 1786464000 AND 1786550399
AND (c.name LIKE '%@%' OR c.name LIKE '%员工%')
GROUP BY c.id
"""), ensure_ascii=False, default=str, indent=2))
print("=== today Aug 13 counts ===")
start, end = 1786550400, 1786636799
for uid, name in [("LiXiaoHuan","李晓焕"),("GeiWoNaHaoDeA","彭世博")]:
cur.execute("""
SELECT COUNT(*) AS c FROM (
SELECT e.external_userid
FROM zyt_qywx_external_contact_event e
WHERE e.change_type='add_external_contact' AND e.user_id=%s
AND e.event_time BETWEEN %s AND %s AND e.external_userid<>''
AND EXISTS (
SELECT 1 FROM zyt_qywx_external_contact_event a
WHERE a.user_id=e.user_id AND a.external_userid=e.external_userid
AND a.change_type='msg_audit_approved' AND a.event_time>=e.event_time AND a.event_time<=%s
)
AND NOT EXISTS (
SELECT 1 FROM zyt_qywx_external_contact_event d
WHERE d.user_id=e.user_id AND d.external_userid=e.external_userid
AND d.change_type='del_external_contact' AND d.event_time>=e.event_time AND d.event_time<=%s
)
AND NOT EXISTS (
SELECT 1 FROM zyt_qywx_external_contact_event p
WHERE p.user_id=e.user_id AND p.external_userid=e.external_userid
AND p.change_type='add_external_contact' AND p.event_time<%s
)
GROUP BY e.external_userid
) t
""", (uid, start, end, end, end, start))
print(name, "today unique", cur.fetchone())
cur.execute("""
SELECT COUNT(*) AS raw FROM (
SELECT external_userid FROM zyt_qywx_external_contact_event
WHERE change_type='add_external_contact' AND user_id=%s
AND event_time BETWEEN %s AND %s GROUP BY external_userid
) t
""", (uid, start, end))
print(name, "today raw unique", cur.fetchone())
print("=== personal yeji 8/12 open/fans ===")
print(json.dumps(q("""
SELECT a.name, y.yeji_date, y.add_fans_count, y.total_open_count, y.media_source, y.creator_id
FROM zyt_personal_yeji y
JOIN zyt_admin a ON a.id=y.creator_id
WHERE a.work_wechat_userid IN ('LiXiaoHuan','GeiWoNaHaoDeA')
AND y.yeji_date BETWEEN '2026-08-12' AND '2026-08-13'
ORDER BY a.name, y.yeji_date
"""), ensure_ascii=False, default=str, indent=2))
print("=== 李晓焕 add_way mix ===")
print(json.dumps(q("""
SELECT change_type, COUNT(*) c FROM zyt_qywx_external_contact_event
WHERE user_id='LiXiaoHuan' AND event_time BETWEEN 1786464000 AND 1786550399
GROUP BY change_type
"""), ensure_ascii=False, default=str))
@@ -756,7 +756,7 @@ class HerbRowWidget(QFrame):
self.setObjectName("PrescriptionHerbCard" if card_mode else "SubtleCard")
self.setProperty("duplicate", False)
self._locked = locked
self.formula_combo = QComboBox()
self.formula_combo = QComboBox(self)
self.formula_combo.addItem("主方", "主方")
self.formula_combo.addItem("辅方", "辅方")
_set_combo_data(
@@ -769,9 +769,10 @@ class HerbRowWidget(QFrame):
repository,
medicine_id=first_value(herb, "medicine_id", "id", default=None),
name=str(first_value(herb, "name", "medicine_name", default="")),
parent=self,
)
self.medicine.setEnabled(not locked)
self.dosage = QDoubleSpinBox()
self.dosage = QDoubleSpinBox(self)
self.dosage.setRange(0, 99999)
self.dosage.setDecimals(1)
self.dosage.setSingleStep(0.5)
@@ -779,7 +780,7 @@ class HerbRowWidget(QFrame):
self.dosage.setValue(_float(first_value(herb, "dosage", "amount", default=0)))
self.dosage.setEnabled(not locked)
self.dosage.setMaximumWidth(130)
self.remove_button = QPushButton("删除")
self.remove_button = QPushButton("删除", self)
self.remove_button.setProperty("variant", "ghost")
self.remove_button.setVisible(not locked)
self.remove_button.clicked.connect(lambda: self.remove_requested.emit(self))
@@ -871,14 +872,14 @@ class HerbEditor(QWidget):
root = QVBoxLayout(self)
root.setContentsMargins(0, 0, 0, 0)
root.setSpacing(8)
self.actions_host = QWidget()
self.actions_host = QWidget(self)
actions = QHBoxLayout(self.actions_host)
actions.setContentsMargins(0, 0, 0, 0)
self.main_button = QPushButton(" 添加主方药材")
self.main_button = QPushButton(" 添加主方药材", self.actions_host)
self.main_button.setProperty("variant", "secondary")
self.main_button.clicked.connect(lambda: self.add_row(formula_type="主方"))
actions.addWidget(self.main_button)
self.aux_button = QPushButton(" 添加辅方药材")
self.aux_button = QPushButton(" 添加辅方药材", self.actions_host)
self.aux_button.setProperty("variant", "secondary")
self.aux_button.clicked.connect(lambda: self.add_row(formula_type="辅方"))
self.aux_button.setVisible(show_formula)
@@ -1786,7 +1787,7 @@ class PrescriptionEditorDialog(QDialog):
copy.addWidget(subtitle)
layout.addLayout(copy, 1)
diagnosis_id = _int(self._source.get("diagnosis_id"), 0)
self.diagnosis_button = QPushButton("查看患者诊单详情")
self.diagnosis_button = QPushButton("查看患者诊单详情", header)
self.diagnosis_button.setObjectName("PrescriptionDrawerDiagnosisButton")
self.diagnosis_button.setVisible(diagnosis_id > 0)
self.diagnosis_button.clicked.connect(
@@ -1961,7 +1962,7 @@ class PrescriptionEditorDialog(QDialog):
self.add_aux_button = QPushButton("添加辅方")
self.add_aux_button.clicked.connect(lambda: self.herbs.add_row(formula_type="辅方"))
actions.addWidget(self.add_aux_button)
self.import_library_button = QPushButton("从处方库导入")
self.import_library_button = QPushButton("从处方库导入", actions_widget)
self.import_library_button.clicked.connect(self._import_library)
library_default = self.permissions is None
self.import_library_button.setVisible(
@@ -2714,7 +2715,7 @@ def _status_text(value: Any) -> str:
return {0: "待审核", 1: "已通过", 2: "已驳回"}.get(status, display_text(status))
def render_case_record_html(prescription: Any) -> str:
def render_case_record_html(prescription: Any, *, print_layout: bool = False) -> str:
"""Render the immutable diagnosis snapshot as the admin A3 case sheet.
``QTextDocument`` only implements a deliberately small HTML/CSS subset. In
@@ -2918,13 +2919,17 @@ def render_case_record_html(prescription: Any) -> str:
),
),
]
body_padding = "0" if print_layout else "12px"
body_background = "#ffffff" if print_layout else "#eef1f5"
paper_border = "0" if print_layout else "1px solid #dcdfe6"
content_padding = "36px 42px 44px" if print_layout else "26px 30px 34px"
return f"""
<!doctype html><html><head><meta charset="utf-8"><style>
body {{ margin:0; padding:12px; background:#eef1f5; color:#303133;
body {{ margin:0; padding:{body_padding}; background:{body_background}; color:#303133;
font-family:"Microsoft YaHei","PingFang SC","Segoe UI",sans-serif;
font-size:13px; }}
.paper {{ width:100%; margin:0 auto; background:#ffffff; border:1px solid #dcdfe6; }}
.paper-content {{ padding:26px 30px 34px; vertical-align:top; }}
.paper {{ width:100%; margin:0 auto; background:#ffffff; border:{paper_border}; }}
.paper-content {{ padding:{content_padding}; vertical-align:top; }}
h1 {{ text-align:center; font-size:20px; font-weight:600; margin:0;
padding:0 0 14px; border-bottom:2px solid #303133; }}
.cr-section {{ margin-top:14px; border-collapse:collapse; }}
@@ -3510,6 +3515,40 @@ def render_prescription_slip_image(
return image
def render_case_record_image(
prescription: Any,
*,
width: int = 1123,
scale: float = 2.0,
) -> QImage:
"""Rasterize the immutable detailed case record for A3 print/PDF output."""
document = QTextDocument()
document.setDocumentMargin(0)
family = _ensure_slip_fonts()
default_font = QFont(family)
default_font.setPixelSize(13)
default_font.setStyleHint(QFont.StyleHint.SansSerif)
document.setDefaultFont(default_font)
document.setTextWidth(float(width))
document.setHtml(render_case_record_html(prescription, print_layout=True))
size = document.size()
pixel_w = max(1, int(math.ceil(max(size.width(), float(width)) * scale)))
pixel_h = max(1, int(math.ceil(max(size.height(), 300.0) * scale)))
image = QImage(pixel_w, pixel_h, QImage.Format.Format_RGB32)
image.setDevicePixelRatio(1.0)
image.fill(QColor("#ffffff"))
painter = QPainter(image)
try:
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
painter.setRenderHint(QPainter.RenderHint.TextAntialiasing)
painter.scale(scale, scale)
document.drawContents(painter)
finally:
painter.end()
return image
def count_slip_ink_pixels(image: QImage, *, step: int = 3, ceiling: int = 245) -> int:
"""Count sampled pixels that are not paper-white. Used to catch blank exports."""
@@ -3652,12 +3691,12 @@ class PrescriptionDetailDialog(QDialog):
self.resize(920, 780)
root = QVBoxLayout(self)
actions = QHBoxLayout()
self.diagnosis_button = QPushButton("查看诊单详情")
self.diagnosis_button = QPushButton("查看诊单详情", self)
diagnosis_id = _int(first_value(prescription, "diagnosis_id"), 0)
self.diagnosis_button.setVisible(can_open_diagnosis and diagnosis_id > 0)
self.diagnosis_button.clicked.connect(lambda: self.diagnosis_requested.emit(diagnosis_id))
actions.addWidget(self.diagnosis_button)
self.orders_button = QPushButton("查看关联订单")
self.orders_button = QPushButton("查看关联订单", self)
prescription_id = _int(first_value(prescription, "id", "prescription_id"), 0)
self.orders_button.setVisible(can_open_orders and prescription_id > 0)
self.orders_button.clicked.connect(lambda: self.orders_requested.emit(prescription_id))
@@ -3671,7 +3710,7 @@ class PrescriptionDetailDialog(QDialog):
pdf_button.clicked.connect(self.choose_pdf_path)
actions.addWidget(pdf_button)
root.addLayout(actions)
self.status_banner = MessageBanner()
self.status_banner = MessageBanner(parent=self)
self.status_banner.setObjectName("PrescriptionDetailStatusBanner")
status_lines: list[str] = []
if _bool(first_value(prescription, "void_status", "is_void", default=False)):
@@ -3746,15 +3785,30 @@ class PrescriptionDetailDialog(QDialog):
def _current_variant(self) -> str:
return "user" if self.tabs.tabText(self.tabs.currentIndex()) == "处方联" else "internal"
def _is_case_record_selected(self) -> bool:
return self.tabs.tabText(self.tabs.currentIndex()) == "详细病历"
def _current_output_image(self) -> QImage:
if self._is_case_record_selected():
return render_case_record_image(self.prescription)
return render_prescription_slip_image(
self.prescription,
variant=self._current_variant(),
)
def _current_page_size(self) -> QPageSize:
page_id = (
QPageSize.PageSizeId.A3 if self._is_case_record_selected() else QPageSize.PageSizeId.A4
)
return QPageSize(page_id)
def print_slip(self) -> None:
printer = QPrinter(QPrinter.PrinterMode.ScreenResolution)
printer.setPageSize(QPageSize(QPageSize.PageSizeId.A4))
printer.setPageSize(self._current_page_size())
printer.setPageMargins(QMarginsF(8, 8, 8, 8), QPageLayout.Unit.Millimeter)
dialog = QPrintDialog(printer, self)
if dialog.exec() == QDialog.DialogCode.Accepted:
image = render_prescription_slip_image(
self.prescription, variant=self._current_variant()
)
image = self._current_output_image()
painter = QPainter(printer)
try:
_draw_slip_image_on_page(painter, image)
@@ -3762,12 +3816,18 @@ class PrescriptionDetailDialog(QDialog):
painter.end()
def choose_pdf_path(self) -> None:
patient = str(first_value(self.prescription, "patient_name", default="处方"))
prescription_id = first_value(self.prescription, "id", default="")
suggested = f"处方-{patient}-{prescription_id}.pdf"
patient = str(first_value(self.prescription, "patient_name", default="患者"))
if self._is_case_record_selected():
diagnosis_id = first_value(self.prescription, "diagnosis_id", default="")
suggested = f"病历-{patient}-{diagnosis_id}.pdf"
caption = "导出病历 PDF"
else:
prescription_id = first_value(self.prescription, "id", default="")
suggested = f"处方-{patient}-{prescription_id}.pdf"
caption = "导出处方 PDF"
path, _selected = QFileDialog.getSaveFileName(
self,
"导出处方 PDF",
caption,
suggested,
"PDF 文件 (*.pdf)",
)
@@ -3778,23 +3838,30 @@ class PrescriptionDetailDialog(QDialog):
output = str(path)
if not output.lower().endswith(".pdf"):
output += ".pdf"
image = render_prescription_slip_image(self.prescription, variant=self._current_variant())
is_case_record = self._is_case_record_selected()
output_label = "详细病历" if is_case_record else "处方笺"
image = self._current_output_image()
if count_slip_ink_pixels(image) <= 0:
QMessageBox.warning(self, "导出失败", "处方笺未能生成可见内容,请重试。")
QMessageBox.warning(self, "导出失败", f"{output_label}未能生成可见内容,请重试。")
return
page_size = QPageSize(QPageSize.PageSizeId.A4)
page_size = self._current_page_size()
page_label = "A3" if is_case_record else "A4"
if not page_size.isValid():
QMessageBox.warning(self, "导出失败", "无法创建有效的 A4 页面,请重试。")
QMessageBox.warning(self, "导出失败", f"无法创建有效的 {page_label} 页面,请重试。")
return
writer = QPdfWriter(output)
if not writer.setPageSize(page_size):
QMessageBox.warning(self, "导出失败", "无法创建有效的 A4 页面,请重试。")
QMessageBox.warning(self, "导出失败", f"无法创建有效的 {page_label} 页面,请重试。")
return
if (
not writer.setPageMargins(QMarginsF(8, 8, 8, 8), QPageLayout.Unit.Millimeter)
or not writer.pageLayout().isValid()
):
QMessageBox.warning(self, "导出失败", "无法设置 A4 页面边距,请重试。")
QMessageBox.warning(
self,
"导出失败",
f"无法设置 {page_label} 页面边距,请重试。",
)
return
writer.setResolution(300)
painter = QPainter()
@@ -4074,18 +4141,18 @@ class PrescriptionOrderDialog(QDialog):
self.amount.setDecimals(2)
self.amount.setPrefix("¥ ")
form.addRow("订单金额 *", self.amount)
self.internal_cost = QDoubleSpinBox()
self.internal_cost = QDoubleSpinBox(tab)
self.internal_cost.setRange(0, 10_000_000)
self.internal_cost.setDecimals(2)
self.internal_cost.setPrefix("¥ ")
self.internal_cost.setVisible(self.can_view_internal_cost)
self.internal_cost_label = QLabel("内部成本")
self.internal_cost_label = QLabel("内部成本", tab)
self.internal_cost_label.setVisible(self.can_view_internal_cost)
form.addRow(self.internal_cost_label, self.internal_cost)
self.remark_extra = QTextEdit()
self.remark_extra = QTextEdit(tab)
self.remark_extra.setMaximumHeight(80)
self.remark_extra.setVisible(self.can_edit_pharmacy_remark)
self.remark_extra_label = QLabel("药房备注")
self.remark_extra_label = QLabel("药房备注", tab)
self.remark_extra_label.setVisible(self.can_edit_pharmacy_remark)
form.addRow(self.remark_extra_label, self.remark_extra)
self.remark_assistant = QTextEdit()
@@ -427,7 +427,7 @@ class AppointmentsPage(QWidget):
self.patient_input.returnPressed.connect(self._search)
layout.addWidget(self.patient_input)
self.doctor_input = QLineEdit()
self.doctor_input = QLineEdit(frame)
self.doctor_input.setPlaceholderText("医生")
self.doctor_input.setClearButtonEnabled(True)
self.doctor_input.setMaximumWidth(120)
@@ -572,7 +572,10 @@ class AppointmentsPage(QWidget):
*,
danger: bool = False,
) -> QPushButton:
button = QPushButton(label)
# Permission visibility is evaluated before the toolbar layout adopts
# the button. Give it a real owner up front so setVisible(True) cannot
# create a transient top-level HWND on Windows.
button = QPushButton(label, self)
button.setProperty("variant", "danger" if danger else "secondary")
button.setVisible(_canonical_allowed(self.permissions, permission, default=False))
button.setEnabled(False)
@@ -965,7 +965,7 @@ class ConsultationsPage(QWidget):
self.completed_button = DiagnosisChip("已完成", semantic="success")
self.completed_button.clicked.connect(self._choose_completed)
self.main_chip_flow.flow.addWidget(self.completed_button)
self.pending_assign_wrap = QWidget()
self.pending_assign_wrap = QWidget(self)
pending_assign_wrap_layout = QHBoxLayout(self.pending_assign_wrap)
pending_assign_wrap_layout.setContentsMargins(8, 0, 0, 0)
pending_assign_wrap_layout.setSpacing(8)
@@ -1135,17 +1135,17 @@ class ConsultationsPage(QWidget):
toolbar_layout = QHBoxLayout(toolbar)
toolbar_layout.setContentsMargins(16, 10, 16, 10)
toolbar_layout.setSpacing(8)
self.add_button = QPushButton("新增诊单")
self.add_button = QPushButton("新增诊单", toolbar)
self.add_button.setProperty("variant", "primary")
self.add_button.setVisible(_canonical_allowed(permissions, "tcm.diagnosis/add"))
self.add_button.clicked.connect(self._add_diagnosis)
toolbar_layout.addWidget(self.add_button)
self.batch_assign_button = QPushButton("批量指派医助")
self.batch_assign_button = QPushButton("批量指派医助", toolbar)
self.batch_assign_button.setProperty("variant", "success")
self.batch_assign_button.setVisible(_canonical_allowed(permissions, "tcm.diagnosis/assign"))
self.batch_assign_button.clicked.connect(self._batch_assign)
toolbar_layout.addWidget(self.batch_assign_button)
self.batch_cancel_assign_button = QPushButton("批量取消指派")
self.batch_cancel_assign_button = QPushButton("批量取消指派", toolbar)
self.batch_cancel_assign_button.setProperty("variant", "warning")
self.batch_cancel_assign_button.setVisible(
_canonical_allowed(permissions, "tcm.diagnosis/assign")
@@ -352,7 +352,7 @@ class _LegacyAppointmentDialog(QDialog):
self.remark.setPlaceholderText("预约备注(可选)")
form.addRow("备注", self.remark)
root.addLayout(form)
self.banner = MessageBanner()
self.banner = MessageBanner(parent=self)
self.banner.show_message("正在加载医生、渠道与今日挂号状态…", "info")
root.addWidget(self.banner)
buttons = QDialogButtonBox(
@@ -1297,25 +1297,25 @@ class PatientListWorkspace(QWidget):
def _build_actions(self) -> QHBoxLayout:
layout = QHBoxLayout()
layout.setSpacing(6)
self.diagnosis_button = QPushButton("诊单")
self.diagnosis_button = QPushButton("诊单", self)
self.diagnosis_button.setProperty("variant", "primary")
self.diagnosis_button.clicked.connect(self._open_selected_diagnosis)
layout.addWidget(self.diagnosis_button)
self.appointment_button = QPushButton("预约")
self.appointment_button = QPushButton("预约", self)
self.appointment_button.clicked.connect(
lambda: self._emit_selected(self.appointment_requested)
)
layout.addWidget(self.appointment_button)
self.assign_button = QPushButton("指派医助")
self.assign_button = QPushButton("指派医助", self)
self.assign_button.clicked.connect(lambda: self._emit_selected(self.assign_requested))
layout.addWidget(self.assign_button)
self.fill_id_button = QPushButton("补全身份证")
self.fill_id_button = QPushButton("补全身份证", self)
self.fill_id_button.clicked.connect(lambda: self._emit_selected(self.fill_id_requested))
layout.addWidget(self.fill_id_button)
self.orders_button = QPushButton("关联订单")
self.orders_button = QPushButton("关联订单", self)
self.orders_button.clicked.connect(lambda: self._emit_selected(self.orders_requested))
layout.addWidget(self.orders_button)
self.cancel_button = QPushButton("取消挂号")
self.cancel_button = QPushButton("取消挂号", self)
self.cancel_button.setProperty("variant", "danger")
self.cancel_button.clicked.connect(lambda: self._emit_selected(self.cancel_requested))
layout.addWidget(self.cancel_button)
@@ -97,7 +97,7 @@ class PrescriptionLibraryPage(QWidget):
"我的处方库",
"管理可复用药材组合;公开模板可被其他医生导入,禁用修改仅作用于导入后的处方。",
)
self.new_button = QPushButton(" 新增处方")
self.new_button = QPushButton(" 新增处方", header)
self.new_button.setProperty("variant", "primary")
self.new_button.setVisible(has_permission(permissions, "wcf.prescription/add"))
self.new_button.clicked.connect(self._new_template)
@@ -147,17 +147,17 @@ class PrescriptionLibraryPage(QWidget):
title.setProperty("role", "sectionTitle")
toolbar.addWidget(title)
toolbar.addStretch(1)
self.view_button = QPushButton("查看")
self.view_button = QPushButton("查看", card)
self.view_button.setVisible(has_permission(permissions, "wcf.prescription/read"))
self.view_button.setEnabled(False)
self.view_button.clicked.connect(self._view_selected)
toolbar.addWidget(self.view_button)
self.edit_button = QPushButton("编辑")
self.edit_button = QPushButton("编辑", card)
self.edit_button.setVisible(has_permission(permissions, "wcf.prescription/edit"))
self.edit_button.setEnabled(False)
self.edit_button.clicked.connect(self._edit_selected)
toolbar.addWidget(self.edit_button)
self.delete_button = QPushButton("删除")
self.delete_button = QPushButton("删除", card)
self.delete_button.setProperty("variant", "danger")
self.delete_button.setVisible(has_permission(permissions, "wcf.prescription/delete"))
self.delete_button.setEnabled(False)
@@ -280,11 +280,11 @@ class PrescriptionsPage(QWidget):
"已开处方",
"管理处方审核、患者修正与履约订单;已通过且未作废的处方只允许查看。",
)
self.orders_button = QPushButton("业务订单")
self.orders_button = QPushButton("业务订单", header)
self.orders_button.setVisible(has_permission(permissions, "tcm.prescriptionOrder/lists"))
self.orders_button.clicked.connect(lambda: self._open_orders())
header.add_action(self.orders_button)
self.add_button = QPushButton(" 新增处方")
self.add_button = QPushButton(" 新增处方", header)
self.add_button.setProperty("variant", "primary")
self.add_button.setVisible(has_permission(permissions, "cf.prescription/add"))
self.add_button.clicked.connect(self._add_prescription)
@@ -436,7 +436,9 @@ class PrescriptionsPage(QWidget):
*,
danger: bool = False,
) -> QPushButton:
button = QPushButton(text)
# Keep permission-driven initial visibility inside the page hierarchy;
# otherwise Windows briefly exposes the button as its own window.
button = QPushButton(text, self)
if danger:
button.setProperty("variant", "danger")
button.setVisible(has_permission(self.permissions, permission))
@@ -434,13 +434,13 @@ class ReceptionPage(QWidget):
self.video_button.setProperty("variant", "secondary")
self.video_button.clicked.connect(self._request_video)
action_row.addWidget(self.video_button)
self.edit_button = QPushButton("编辑病历")
self.edit_button = QPushButton("编辑病历", content)
self.edit_button.setProperty("variant", "secondary")
self.edit_button.clicked.connect(self._edit_diagnosis)
self.edit_button.setVisible(self._can_edit)
action_row.addWidget(self.edit_button)
action_row.addStretch(1)
self.complete_button = QPushButton("完成接诊")
self.complete_button = QPushButton("完成接诊", content)
self.complete_button.setProperty("variant", "primary")
self.complete_button.clicked.connect(self._complete_appointment)
self.complete_button.setVisible(self._can_complete)
@@ -520,32 +520,32 @@ class ReceptionPage(QWidget):
self.notes_layout.setSpacing(8)
detail_layout.addWidget(self.notes_container)
self.note_edit = QTextEdit()
self.note_edit = QTextEdit(content)
self.note_edit.setPlaceholderText("记录本次沟通要点(最多 500 字,不会自动提交)")
self.note_edit.setMaximumHeight(105)
self.note_edit.textChanged.connect(self._limit_note_text)
self.note_edit.setVisible(self._can_note)
detail_layout.addWidget(self.note_edit)
self.note_counter = QLabel(f"0 / {NOTE_LIMIT}")
self.note_counter = QLabel(f"0 / {NOTE_LIMIT}", content)
self.note_counter.setProperty("role", "muted")
self.note_counter.setAlignment(Qt.AlignmentFlag.AlignRight)
self.note_counter.setVisible(self._can_note)
detail_layout.addWidget(self.note_counter)
attachment_actions = QHBoxLayout()
self.add_tongue_button = QPushButton("添加舌苔图")
self.add_tongue_button = QPushButton("添加舌苔图", content)
self.add_tongue_button.setProperty("variant", "secondary")
self.add_tongue_button.clicked.connect(self._choose_tongue_images)
self.add_tongue_button.setVisible(self._can_note)
attachment_actions.addWidget(self.add_tongue_button)
self.add_report_button = QPushButton("添加检查报告")
self.add_report_button = QPushButton("添加检查报告", content)
self.add_report_button.setProperty("variant", "secondary")
self.add_report_button.clicked.connect(self._choose_report_files)
self.add_report_button.setVisible(self._can_note)
attachment_actions.addWidget(self.add_report_button)
attachment_actions.addStretch(1)
detail_layout.addLayout(attachment_actions)
self.pending_attachments = QWidget()
self.pending_attachments = QWidget(content)
self.pending_attachments_layout = QVBoxLayout(self.pending_attachments)
self.pending_attachments_layout.setContentsMargins(0, 0, 0, 0)
self.pending_attachments_layout.setSpacing(4)
@@ -554,7 +554,7 @@ class ReceptionPage(QWidget):
note_action = QHBoxLayout()
note_action.addStretch(1)
self.save_note_button = QPushButton("保存备注")
self.save_note_button = QPushButton("保存备注", content)
self.save_note_button.setProperty("variant", "secondary")
self.save_note_button.clicked.connect(self._save_note)
self.save_note_button.setVisible(self._can_note)
+8 -8
View File
@@ -392,10 +392,10 @@ class PageHeader(QWidget):
layout.setSpacing(16)
text_layout = QVBoxLayout()
text_layout.setSpacing(3)
self.title_label = QLabel(title)
self.title_label = QLabel(title, self)
self.title_label.setProperty("role", "pageTitle")
text_layout.addWidget(self.title_label)
self.subtitle_label = QLabel(subtitle)
self.subtitle_label = QLabel(subtitle, self)
self.subtitle_label.setProperty("role", "muted")
self.subtitle_label.setWordWrap(True)
self.subtitle_label.setVisible(bool(subtitle))
@@ -454,21 +454,21 @@ class EmptyState(QWidget):
layout.setContentsMargins(24, 44, 24, 44)
layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
layout.setSpacing(8)
glyph = QLabel("")
glyph = QLabel("", self)
glyph.setObjectName("EmptyStateGlyph")
glyph.setAlignment(Qt.AlignmentFlag.AlignCenter)
glyph.setFixedSize(44, 44)
layout.addWidget(glyph)
title_label = QLabel(title)
title_label = QLabel(title, self)
title_label.setProperty("role", "sectionTitle")
title_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
layout.addWidget(title_label)
description_label = QLabel(description)
description_label = QLabel(description, self)
description_label.setProperty("role", "muted")
description_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
description_label.setWordWrap(True)
layout.addWidget(description_label)
self.action_button = QPushButton(action_text)
self.action_button = QPushButton(action_text, self)
self.action_button.setProperty("variant", "secondary")
self.action_button.setVisible(bool(action_text))
self.action_button.clicked.connect(self.action_requested)
@@ -483,11 +483,11 @@ class MessageBanner(QFrame):
layout = QHBoxLayout(self)
layout.setContentsMargins(12, 9, 12, 9)
layout.setSpacing(9)
self.icon = QLabel("i")
self.icon = QLabel("i", self)
self.icon.setObjectName("MessageBannerIcon")
self.icon.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.icon.setFixedSize(20, 20)
self.label = QLabel(text)
self.label = QLabel(text, self)
self.label.setObjectName("MessageBannerText")
self.label.setWordWrap(True)
layout.addWidget(self.icon)
+41 -1
View File
@@ -7,10 +7,12 @@ from typing import Any
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
import pytest
from PySide6.QtCore import QSettings
from PySide6.QtCore import QEvent, QObject, QSettings
from PySide6.QtWidgets import QApplication, QWidget
from doctor_workstation.services import DemoDoctorRepository
from doctor_workstation.ui.login import LoginWindow
from doctor_workstation.ui.shell import ShellWindow
from doctor_workstation.ui.widgets import BusyOverlay
@@ -87,3 +89,41 @@ def test_login_loading_does_not_spawn_extra_windows(
window.close()
application.processEvents()
def test_shell_construction_never_shows_orphan_business_controls(
application: QApplication,
) -> None:
shown: list[tuple[str, str]] = []
class OrphanShowRecorder(QObject):
def eventFilter(self, watched: QObject, event: QEvent) -> bool: # noqa: N802
if (
event.type() == QEvent.Type.Show
and isinstance(watched, QWidget)
and watched.parentWidget() is None
):
text = getattr(watched, "text", lambda: "")()
shown.append((type(watched).__name__, str(text)))
return False
repository = DemoDoctorRepository()
session = repository.login(repository.DEMO_ACCOUNT, repository.DEMO_PASSWORD)
recorder = OrphanShowRecorder(application)
application.installEventFilter(recorder)
try:
shell = ShellWindow(
repository,
{
"session": session,
"user": session.user,
"demo_mode": True,
},
permissions=session.permissions,
)
finally:
application.removeEventFilter(recorder)
assert shown == []
shell.close()
application.processEvents()
+148 -2
View File
@@ -7,10 +7,10 @@ from typing import Any
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
import pytest
from PySide6.QtCore import QSize, Qt
from PySide6.QtCore import QEvent, QObject, QSize, Qt
from PySide6.QtGui import QColor, QImage, QPainter
from PySide6.QtPdf import QPdfDocument
from PySide6.QtWidgets import QApplication
from PySide6.QtWidgets import QApplication, QDialog, QWidget
from doctor_workstation import app as app_module
from doctor_workstation.core import PermissionSet
@@ -354,6 +354,85 @@ def test_editor_builds_complete_add_payload(
application.processEvents()
def test_prescription_workflows_never_show_orphan_child_controls(
application: QApplication,
immediate_async: None,
) -> None:
shown: list[tuple[str, str]] = []
class OrphanShowRecorder(QObject):
def eventFilter(self, watched: QObject, event: QEvent) -> bool: # noqa: N802
if (
event.type() == QEvent.Type.Show
and isinstance(watched, QWidget)
and not isinstance(watched, QDialog)
and watched.parentWidget() is None
):
text = getattr(watched, "text", lambda: "")()
shown.append((type(watched).__name__, str(text)))
return False
repository = SimpleNamespace(
list_medicines=lambda **_kwargs: {
"lists": [{"id": 31, "name": "黄芪"}],
"count": 1,
}
)
prescription = {
"id": 12,
"diagnosis_id": 6,
"patient_name": "林晓岚",
"phone": "13800000000",
"audit_status": 1,
"herbs": [{"medicine_id": 31, "name": "黄芪", "dosage": 15}],
}
recorder = OrphanShowRecorder(application)
application.installEventFilter(recorder)
widgets: list[QWidget] = []
try:
widgets.extend(
[
dialog_module.HerbRowWidget(
repository,
prescription["herbs"][0],
show_formula=True,
locked=False,
),
dialog_module.HerbEditor(
repository,
show_formula=True,
show_actions=True,
),
PrescriptionEditorDialog(
repository,
prescription,
mode="edit",
permissions=PermissionSet(
["cf.prescription/edit", "tcm.prescriptionLibrary/lists"]
),
),
PrescriptionDetailDialog(
prescription,
can_open_diagnosis=True,
can_open_orders=True,
),
PrescriptionOrderDialog(
repository,
prescription,
can_view_internal_cost=True,
can_edit_pharmacy_remark=True,
),
]
)
finally:
application.removeEventFilter(recorder)
assert shown == []
for widget in widgets:
widget.close()
application.processEvents()
def test_editor_matches_admin_four_observation_fields_and_edit_context(
application: QApplication,
immediate_async: None,
@@ -667,6 +746,73 @@ def test_prescription_detail_can_open_immutable_case_record_tab(
application.processEvents()
def test_case_record_tab_exports_case_record_as_a3_pdf(
application: QApplication,
tmp_path: Any,
monkeypatch: pytest.MonkeyPatch,
) -> None:
prescription = {
"id": 12,
"diagnosis_id": 8169,
"patient_name": "何福萍",
"case_record": {
"diagnosis_id": 8169,
"patient_name": "何福萍",
"phone": "13800138000",
"diagnosis_type": "follow_up",
"local_hospital_name": "市中医院",
"symptoms": "口渴、乏力、睡眠欠佳",
"tongue_coating": "舌红少苔",
"pulse": "脉细数",
"doctor_advice": "规律复诊",
},
}
case_render_calls: list[str] = []
original_case_renderer = dialog_module.render_case_record_image
def render_case(*args: Any, **kwargs: Any) -> QImage:
case_render_calls.append("case")
return original_case_renderer(*args, **kwargs)
def reject_prescription_render(*_args: Any, **_kwargs: Any) -> QImage:
pytest.fail("详细病历页签不应调用处方笺渲染器")
monkeypatch.setattr(dialog_module, "render_case_record_image", render_case)
monkeypatch.setattr(
dialog_module,
"render_prescription_slip_image",
reject_prescription_render,
)
app_module._install_chinese_translations(application)
viewer = PrescriptionDetailDialog(prescription)
case_index = next(
index for index in range(viewer.tabs.count()) if viewer.tabs.tabText(index) == "详细病历"
)
viewer.tabs.setCurrentIndex(case_index)
output = tmp_path / "case-record.pdf"
viewer.export_pdf(output)
pdf = QPdfDocument()
assert pdf.load(str(output)) == QPdfDocument.Error.None_
assert pdf.pageCount() == 1
page_size = pdf.pagePointSize(0)
rendered_page = pdf.render(0, QSize(1400, 1980))
visible_ink = sum(
1
for y in range(0, rendered_page.height(), 4)
for x in range(0, rendered_page.width(), 4)
if (color := rendered_page.pixelColor(x, y)).alpha() > 0 and color.lightness() < 245
)
viewer.close()
application.processEvents()
assert case_render_calls == ["case"]
assert page_size.width() == pytest.approx(842.0, abs=2.0)
assert page_size.height() == pytest.approx(1191.0, abs=2.0)
assert visible_ink > 1_000
def test_demo_repository_pages_render_offscreen(
application: QApplication,
immediate_async: None,
Binary file not shown.
Binary file not shown.
@@ -966,6 +966,7 @@ class ConversionLogic
* - 加粉之后、统计结束前须有 msg_audit_approved(排除未完成链路的幽灵事件);
* - 加粉之后、统计结束前若出现 del_external_contact(客户从企微侧删除)则不计。
* 不扣减 del_follow_user:企微客户列表中改名带「删」的客户往往仍在列表中;
* - 剔除非投放加粉:跟进人 add_way∈{1 扫一扫, 2 搜索手机号, 3 名片分享}
* - 剔除继承客户:跟进人 add_way∈{201 内部成员共享, 202 管理员/负责人分配}(含在职/离职继承)。
*
* @param array<string, mixed>|null $mediaChannel
@@ -989,7 +990,7 @@ class ConversionLogic
}
}
$baseKey = self::requestRowsCacheKey('fans-effective-v4', [
$baseKey = self::requestRowsCacheKey('fans-effective-v6', [
$startTimestamp,
$endTimestamp,
self::mediaChannelCacheKey($mediaChannel),
@@ -1070,7 +1071,7 @@ class ConversionLogic
}
$pairs = $query->select()->toArray();
$pairs = self::excludeInheritedFanPairs($pairs);
$pairs = self::excludeUncountedFanPairs($pairs);
$countsByUser = [];
foreach ($pairs as $pair) {
@@ -1095,13 +1096,17 @@ class ConversionLogic
}
/**
* 剔除企微「继承/分配」客户:跟进人 add_way 201(内部成员共享)或 202(管理员/负责人分配,含在职/离职继承)。
* 剔除不应计入加粉的跟进来源:
* - add_way=1 扫一扫(客户通过扫一扫添加);
* - add_way=2 搜索手机号(成员通过搜索手机号添加);
* - add_way=3 名片分享(客户通过名片分享添加);
* - add_way=201/202 继承/分配(内部成员共享、管理员/负责人分配,含在职/离职继承)。
* 无本地客户档案或跟进信息不含该员工时保守保留(无法判定则仍计加粉)。
*
* @param array<int, array<string, mixed>> $pairs
* @return array<int, array<string, mixed>>
*/
private static function excludeInheritedFanPairs(array $pairs): array
private static function excludeUncountedFanPairs(array $pairs): array
{
if ($pairs === []) {
return [];
@@ -1119,8 +1124,8 @@ class ConversionLogic
return $pairs;
}
/** @var array<string, true> $inheritedKeys user_id\0external_userid */
$inheritedKeys = [];
/** @var array<string, true> $excludedKeys user_id\0external_userid */
$excludedKeys = [];
foreach (array_chunk($externalIdList, 500) as $chunk) {
$contactRows = Db::name('qywx_external_contact')
->whereIn('external_userid', $chunk)
@@ -1145,19 +1150,19 @@ class ConversionLogic
continue;
}
$addWay = (int) ($fu['add_way'] ?? $fu['AddWay'] ?? 0);
if ($addWay !== 201 && $addWay !== 202) {
if (!in_array($addWay, [1, 2, 3, 201, 202], true)) {
continue;
}
$followUserId = trim((string) ($fu['userid'] ?? $fu['UserId'] ?? ''));
if ($followUserId === '') {
continue;
}
$inheritedKeys[$followUserId . "\0" . $extId] = true;
$excludedKeys[$followUserId . "\0" . $extId] = true;
}
}
}
if ($inheritedKeys === []) {
if ($excludedKeys === []) {
return $pairs;
}
@@ -1168,7 +1173,7 @@ class ConversionLogic
if ($userId === '' || $extId === '') {
continue;
}
if (isset($inheritedKeys[$userId . "\0" . $extId])) {
if (isset($excludedKeys[$userId . "\0" . $extId])) {
continue;
}
$kept[] = $pair;