clinic_log.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. """
  4. 诊所日志宽表同步任务
  5. 功能:将MySQL中的诊所日志数据同步到Doris宽表
  6. """
  7. import datetime
  8. from db.pool import get_mysql_conn, get_doris_conn
  9. from logger.handler import get_logger
  10. logger = get_logger("job.clinic_log")
  11. def run(start_date=None, end_date=None, group_id=1):
  12. """执行任务"""
  13. if start_date is None or end_date is None:
  14. yesterday = datetime.date.today() - datetime.timedelta(days=1)
  15. start_date = end_date = yesterday.strftime("%Y-%m-%d")
  16. logger.info("=" * 50)
  17. logger.info(f"[诊所日志] 开始同步: {start_date} ~ {end_date}, group_id={group_id}")
  18. logger.info("=" * 50)
  19. conn = get_mysql_conn()
  20. cursor = conn.cursor()
  21. # 1. 获取来源字典
  22. cursor.execute("SELECT id, name FROM orgdict_origin")
  23. source_dict = {s["id"]: s["name"] for s in cursor.fetchall()}
  24. # 2. 查询主数据
  25. params = {
  26. "start_date": start_date + " 00:00:00",
  27. "end_date": end_date + " 23:59:59",
  28. "group_id": int(group_id),
  29. }
  30. sql = """
  31. SELECT
  32. c.id as clinic_id,
  33. p.patientId,
  34. p.name as patient_name,
  35. p.sex,
  36. p.birthday,
  37. p.identity,
  38. p.mobile,
  39. p.source,
  40. p.inviter,
  41. p.inviter_phone,
  42. c.type,
  43. c.channel,
  44. c.add_user,
  45. c.appoint_date,
  46. c.AppointmentTime_start,
  47. c.matter,
  48. c.accept_doctor_id,
  49. c.accept_optometrist_id,
  50. c.accept_assistant_id,
  51. c.customer_manager_staff_id,
  52. c.customer_optometrist_staff_id,
  53. c.come_time,
  54. c.patient_id,
  55. o.id as org_id,
  56. o.name as org_name,
  57. o.group_id,
  58. g.name as group_name
  59. FROM clinic_clinic c
  60. JOIN patient_custome p ON c.patient_id = p.id
  61. JOIN org_org o ON c.org_id = o.id
  62. LEFT JOIN org_group g ON o.group_id = g.id
  63. WHERE c.reg_status IN (2, 3, 4)
  64. AND c.pno = ''
  65. AND o.is_effective = 1
  66. AND o.group_id = %(group_id)s
  67. AND c.come_time >= %(start_date)s
  68. AND c.come_time <= %(end_date)s
  69. GROUP BY c.id
  70. """
  71. logger.info("正在查询主数据...")
  72. cursor.execute(sql, params)
  73. data_list = cursor.fetchall() # DictCursor 直接返回字典列表
  74. logger.info(f"查询到 {len(data_list)} 条记录")
  75. if not data_list:
  76. cursor.close()
  77. conn.close()
  78. logger.info("[诊所日志] 无数据")
  79. return 0
  80. # 3. 收集ID列表
  81. clinic_id_list = [d["clinic_id"] for d in data_list]
  82. patient_id_list = [d["patient_id"] for d in data_list]
  83. staff_id_list = []
  84. for d in data_list:
  85. if d.get("accept_doctor_id"):
  86. staff_id_list.append(d["accept_doctor_id"])
  87. if d.get("accept_optometrist_id"):
  88. staff_id_list.append(d["accept_optometrist_id"])
  89. if d.get("accept_assistant_id"):
  90. staff_id_list.append(d["accept_assistant_id"])
  91. if d.get("customer_manager_staff_id"):
  92. staff_id_list.append(d["customer_manager_staff_id"])
  93. if d.get("customer_optometrist_staff_id"):
  94. staff_id_list.append(d["customer_optometrist_staff_id"])
  95. staff_id_list = list(set(staff_id_list))
  96. # 4. 查询员工信息
  97. staff_dict = {}
  98. if staff_id_list:
  99. sql = """
  100. SELECT s.id, u.name, s.qywx_userid, u.mobile
  101. FROM staff_staff s
  102. JOIN users_userprofile u ON s.user_id = u.id
  103. WHERE s.id IN %(ids)s
  104. """
  105. cursor.execute(sql, {"ids": staff_id_list})
  106. staff_dict = {s["id"]: s for s in cursor.fetchall()}
  107. # 5. 查询咨询信息
  108. recode_dict = {}
  109. user_dict = {}
  110. if clinic_id_list:
  111. sql = """
  112. SELECT r.clinic_id, r.collect_date, r.activity_location,
  113. r.extension_user_id, r.created_user_id
  114. FROM returnvisit_customerconsultationrecord r
  115. WHERE r.clinic_id IN %(ids)s
  116. """
  117. cursor.execute(sql, {"ids": clinic_id_list})
  118. recode_list = cursor.fetchall()
  119. for r in recode_list:
  120. recode_dict[r["clinic_id"]] = dict(r)
  121. # 获取用户ID列表查询用户信息
  122. user_id_list = []
  123. for r in recode_list:
  124. if r.get("extension_user_id"):
  125. user_id_list.append(r["extension_user_id"])
  126. if r.get("created_user_id"):
  127. user_id_list.append(r["created_user_id"])
  128. if user_id_list:
  129. sql = "SELECT id, name, mobile FROM users_userprofile WHERE id IN %(ids)s"
  130. cursor.execute(sql, {"ids": list(set(user_id_list))})
  131. user_list = cursor.fetchall()
  132. user_dict = {u["id"]: u for u in user_list}
  133. # 6. 查询视力信息
  134. exam_dict = {}
  135. if clinic_id_list:
  136. sql = """
  137. SELECT clinic_id, result
  138. FROM exam_examresult
  139. WHERE clinic_id IN %(ids)s
  140. AND visual = 1
  141. AND (tab = 'yj_dnyg' OR tab = 'tj_zhygy')
  142. """
  143. cursor.execute(sql, {"ids": clinic_id_list})
  144. exam_list = cursor.fetchall()
  145. for e in exam_list:
  146. clinic_id = e["clinic_id"]
  147. if clinic_id not in exam_dict:
  148. exam_dict[clinic_id] = {"r_sph": "", "l_sph": "", "r_nakesight": "", "l_nakesight": ""}
  149. try:
  150. res_dict = eval(e["result"]) if e.get("result") else {}
  151. if res_dict.get("r_sph"):
  152. exam_dict[clinic_id]["r_sph"] = res_dict.get("r_sph")
  153. if res_dict.get("l_sph"):
  154. exam_dict[clinic_id]["l_sph"] = res_dict.get("l_sph")
  155. if res_dict.get("r_nakesight"):
  156. exam_dict[clinic_id]["r_nakesight"] = res_dict.get("r_nakesight")
  157. if res_dict.get("l_nakesight"):
  158. exam_dict[clinic_id]["l_nakesight"] = res_dict.get("l_nakesight")
  159. except:
  160. pass
  161. # 7. 查询标签
  162. patient_tag_dict = {}
  163. if patient_id_list:
  164. sql = """
  165. SELECT t.patient_id, u.name
  166. FROM patient_uspatienttotag t
  167. JOIN patient_ustag u ON t.tag_id = u.id
  168. JOIN patient_ustagparent p ON u.parent_id = p.id
  169. WHERE t.patient_id IN %(ids)s AND p.enabled = 1
  170. """
  171. cursor.execute(sql, {"ids": patient_id_list})
  172. tag_list = cursor.fetchall()
  173. for t in tag_list:
  174. patient_tag_dict.setdefault(t["patient_id"], []).append(t["name"])
  175. # 7.1 按原系统逻辑判断新老客户和当前就诊次数
  176. clinic_new_old_dict = {}
  177. if patient_id_list:
  178. sql = """
  179. SELECT patient_id, id
  180. FROM clinic_clinic
  181. WHERE patient_id IN %(ids)s
  182. AND reg_status IN (2, 3, 4)
  183. AND (pno IS NULL OR pno = '')
  184. ORDER BY come_time, id
  185. """
  186. cursor.execute(sql, {"ids": list(set(patient_id_list))})
  187. all_clinic_list = cursor.fetchall()
  188. patient_clinic_dict = {}
  189. for clinic in all_clinic_list:
  190. patient_clinic_dict.setdefault(clinic["patient_id"], []).append(str(clinic["id"]))
  191. for clinic_id, patient_id in zip(clinic_id_list, patient_id_list):
  192. clinic_ids = patient_clinic_dict.get(patient_id, [])
  193. clinic_new_old_dict[clinic_id] = {
  194. "patient_type": "新客户",
  195. "this_come_number": 0,
  196. }
  197. if clinic_ids:
  198. if str(clinic_id) != clinic_ids[0]:
  199. clinic_new_old_dict[clinic_id]["patient_type"] = "老客户"
  200. try:
  201. clinic_new_old_dict[clinic_id]["this_come_number"] = clinic_ids.index(str(clinic_id)) + 1
  202. except ValueError:
  203. clinic_new_old_dict[clinic_id]["this_come_number"] = 0
  204. cursor.close()
  205. conn.close()
  206. # 8. 字段列表
  207. fields = [
  208. "clinic_id", "patientId", "patient_name", "sex", "birthday", "identity", "mobile",
  209. "source1", "source2", "tag_list", "patient_type", "visit_type", "r_sph", "l_sph",
  210. "r_nakesight", "l_nakesight", "org_id", "org_name", "appoint_time", "add_user",
  211. "group_id", "group_name", "matter", "accept_doctor_id", "accept_doctor_name", "accept_optometrist_id",
  212. "accept_optometrist_name", "accept_assistant_id", "accept_assistant_name",
  213. "manager_id", "manager_name", "manager_qywx_id", "manager_mobile",
  214. "optometrist_id", "optometrist_name", "optometrist_qywx_id", "optometrist_mobile",
  215. "come_time", "this_come_number", "inviter", "inviter_phone", "channel",
  216. "collect_date", "activity_location", "extension_user", "extension_user_mobile",
  217. ]
  218. # 9. 处理数据
  219. result = []
  220. for data in data_list:
  221. clinic_id = data["clinic_id"]
  222. patient_id = data["patient_id"]
  223. # 处理来源
  224. source = data.get("source", "")
  225. pat_source = source.split(",") if source else []
  226. source1 = source_dict.get(int(pat_source[0])) if len(pat_source) > 0 else ""
  227. source2 = source_dict.get(int(pat_source[1])) if len(pat_source) > 1 else ""
  228. # 处理渠道
  229. channel = data.get("channel", "")
  230. if channel == "wechat":
  231. channel = "微信小程序"
  232. elif channel == "crm":
  233. channel = "客服"
  234. elif channel == "doctor_wechat":
  235. channel = "渠道医生"
  236. elif channel == "eye_wechat":
  237. channel = "互联网医院"
  238. elif channel == "ileads":
  239. channel = "ileads"
  240. else:
  241. channel = "门店"
  242. # 处理性别
  243. sex = data.get("sex")
  244. if sex == "男":
  245. sex = 1
  246. elif sex == "女":
  247. sex = 2
  248. # 处理接诊类型
  249. visit_type = data.get("type")
  250. if visit_type == "初诊":
  251. visit_type = 1
  252. elif visit_type == "复诊":
  253. visit_type = 2
  254. elif visit_type == "预约":
  255. visit_type = 3
  256. elif visit_type == "复查":
  257. visit_type = 4
  258. # 处理日期
  259. birthday = data.get("birthday")
  260. if birthday:
  261. if isinstance(birthday, str):
  262. birthday = birthday.split(" ")[0]
  263. else:
  264. birthday = str(birthday).split(" ")[0]
  265. else:
  266. birthday = None
  267. # 辅助函数:处理日期时间格式
  268. def format_datetime(dt):
  269. """将日期时间转换为 Doris 兼容的格式"""
  270. if dt is None:
  271. return None
  272. if isinstance(dt, str):
  273. # 如果是字符串,检查是否有时分秒
  274. if " " in dt:
  275. return dt # 已包含时分秒
  276. else:
  277. return f"{dt} 00:00:00" # 只有日期,添加默认时间
  278. else:
  279. # datetime 对象
  280. return dt.strftime("%Y-%m-%d %H:%M:%S")
  281. come_time = format_datetime(data.get("come_time"))
  282. appoint_time = format_datetime(data.get("AppointmentTime_start"))
  283. # 获取其他信息
  284. exam_info = exam_dict.get(clinic_id, {})
  285. recode_info = recode_dict.get(clinic_id, {})
  286. new_old_info = clinic_new_old_dict.get(clinic_id, {})
  287. # 处理收集日期
  288. collect_date = recode_info.get("collect_date")
  289. if collect_date:
  290. if isinstance(collect_date, str):
  291. collect_date = collect_date.split(" ")[0]
  292. elif hasattr(collect_date, 'strftime'):
  293. collect_date = collect_date.strftime("%Y-%m-%d")
  294. else:
  295. collect_date = str(collect_date).split(" ")[0]
  296. # 确保整数字段类型正确
  297. def safe_int(v):
  298. if v is None or v == "":
  299. return None
  300. try:
  301. return int(v)
  302. except (TypeError, ValueError):
  303. return None
  304. # 确保字符串字段去除两边空格
  305. def safe_str(v):
  306. if v is None:
  307. return ""
  308. return str(v).strip()
  309. record = (
  310. safe_int(clinic_id),
  311. safe_str(data.get("patientId")),
  312. safe_str(data.get("patient_name")),
  313. safe_int(sex) if sex else None,
  314. birthday,
  315. safe_str(data.get("identity")),
  316. safe_str(data.get("mobile")),
  317. safe_str(source1),
  318. safe_str(source2),
  319. safe_str(",".join(patient_tag_dict.get(patient_id, []))),
  320. safe_str(new_old_info.get("patient_type", "")),
  321. safe_int(visit_type) if visit_type else None,
  322. safe_str(exam_info.get("r_sph")),
  323. safe_str(exam_info.get("l_sph")),
  324. safe_str(exam_info.get("r_nakesight")),
  325. safe_str(exam_info.get("l_nakesight")),
  326. safe_int(data.get("org_id")),
  327. safe_str(data.get("org_name")),
  328. appoint_time,
  329. safe_str(data.get("add_user")),
  330. safe_int(data.get("group_id")),
  331. safe_str(data.get("group_name")),
  332. safe_str(data.get("matter")),
  333. safe_int(data.get("accept_doctor_id")),
  334. safe_str(staff_dict.get(data.get("accept_doctor_id"), {}).get("name", "")),
  335. safe_int(data.get("accept_optometrist_id")),
  336. safe_str(staff_dict.get(data.get("accept_optometrist_id"), {}).get("name", "")),
  337. safe_int(data.get("accept_assistant_id")),
  338. safe_str(staff_dict.get(data.get("accept_assistant_id"), {}).get("name", "")),
  339. safe_int(data.get("customer_manager_staff_id")),
  340. safe_str(staff_dict.get(data.get("customer_manager_staff_id"), {}).get("name", "")),
  341. safe_str(staff_dict.get(data.get("customer_manager_staff_id"), {}).get("qywx_userid", "")),
  342. safe_str(staff_dict.get(data.get("customer_manager_staff_id"), {}).get("mobile", "")),
  343. safe_int(data.get("customer_optometrist_staff_id")),
  344. safe_str(staff_dict.get(data.get("customer_optometrist_staff_id"), {}).get("name", "")),
  345. safe_str(staff_dict.get(data.get("customer_optometrist_staff_id"), {}).get("qywx_userid", "")),
  346. safe_str(staff_dict.get(data.get("customer_optometrist_staff_id"), {}).get("mobile", "")),
  347. come_time,
  348. safe_int(new_old_info.get("this_come_number")) if new_old_info.get("this_come_number") else 1,
  349. safe_str(data.get("inviter")),
  350. safe_str(data.get("inviter_phone")),
  351. safe_str(channel),
  352. collect_date,
  353. safe_str(recode_info.get("activity_location")),
  354. safe_str(user_dict.get(recode_info.get("extension_user_id", 0), {}).get("name", "")),
  355. safe_str(user_dict.get(recode_info.get("extension_user_id", 0), {}).get("mobile", "")),
  356. )
  357. result.append(record)
  358. # 10. 写入Doris
  359. logger.info(f"数据处理完成,共 {len(result)} 条记录")
  360. conn = get_doris_conn()
  361. cursor = conn.cursor()
  362. # 10.1 先删除该时间段的数据(确保幂等性)
  363. logger.info(f"清理该时间段的历史数据: {start_date} ~ {end_date}")
  364. delete_sql = """
  365. DELETE FROM fact_cliniclog
  366. WHERE come_time >= %s AND come_time <= %s
  367. AND group_id = %s
  368. """
  369. cursor.execute(delete_sql, (f"{start_date} 00:00:00", f"{end_date} 23:59:59", int(group_id)))
  370. deleted_count = cursor.rowcount
  371. logger.info(f"已删除 {deleted_count} 条历史数据")
  372. conn.commit()
  373. # 10.2 插入新数据
  374. sql = f"INSERT INTO fact_cliniclog ({', '.join(fields)}) VALUES ({', '.join(['%s'] * len(fields))})"
  375. total = len(result)
  376. batch_size = 100
  377. success_count = 0
  378. for i in range(0, total, batch_size):
  379. batch = result[i:i + batch_size]
  380. try:
  381. cursor.executemany(sql, batch)
  382. conn.commit()
  383. success_count += len(batch)
  384. logger.info(f"[诊所日志] 已写入 {min(i + batch_size, total)}/{total}")
  385. except Exception as e:
  386. logger.error(f"写入失败: {e}")
  387. conn.rollback()
  388. raise
  389. cursor.close()
  390. conn.close()
  391. logger.info("=" * 50)
  392. logger.info(f"[诊所日志] 同步完成,共 {success_count} 条记录")
  393. logger.info("=" * 50)
  394. return success_count