diff --git a/admin/src/views/first_visit/conversion/index.vue b/admin/src/views/first_visit/conversion/index.vue index 8c8a7c5f1..756c68411 100644 --- a/admin/src/views/first_visit/conversion/index.vue +++ b/admin/src/views/first_visit/conversion/index.vue @@ -145,7 +145,7 @@

明细数据列表

-

展开部门可查看人员明细;加粉=总进线=区间有效加粉(按员工+客户去重,须会话同意,剔除已删客户、继承客户及区间前已加过的重加);挂号=已支付且实收低于 10 元的订单,预约=有效预约记录;开口率=开口/加粉,挂号率=挂号/加粉,面诊率=面诊/挂号(看挂号后流失),预约率=面诊/预约(看预约后未面诊),面诊接诊率=接诊诊单/面诊,接诊率=接诊诊单/总进线

+

展开部门可查看人员明细;加粉=总进线=区间有效加粉(按员工+客户去重,须会话同意,剔除已删客户、继承客户、扫一扫/搜手机号/名片分享添加及区间前已加过的重加);挂号=已支付且实收低于 10 元的订单,预约=有效预约记录;开口率=开口/加粉,挂号率=挂号/加粉,面诊率=面诊/挂号(看挂号后流失),预约率=面诊/预约(看预约后未面诊),面诊接诊率=接诊诊单/面诊,接诊率=接诊诊单/总进线

{{ dashboard.rows.length }} 个顶层节点
@@ -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: '业务订单,按创建人归属并过滤无效单' }, diff --git a/app/artifacts/debug_lxh_psb.py b/app/artifacts/debug_lxh_psb.py deleted file mode 100644 index 29bb5685e..000000000 --- a/app/artifacts/debug_lxh_psb.py +++ /dev/null @@ -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") diff --git a/app/artifacts/debug_lxh_psb2.py b/app/artifacts/debug_lxh_psb2.py deleted file mode 100644 index 34a9e78c2..000000000 --- a/app/artifacts/debug_lxh_psb2.py +++ /dev/null @@ -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)) diff --git a/app/src/doctor_workstation/ui/dialogs/prescription.py b/app/src/doctor_workstation/ui/dialogs/prescription.py index b38a57457..fa7a19b9f 100644 --- a/app/src/doctor_workstation/ui/dialogs/prescription.py +++ b/app/src/doctor_workstation/ui/dialogs/prescription.py @@ -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"""