consumption.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414
  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.consumption")
  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. params = {
  23. "group_id": int(group_id),
  24. "fsdate": start_date + " 00:00:00",
  25. "fedate": end_date + " 23:59:59",
  26. }
  27. sql = """
  28. SELECT
  29. x.clinic_id,
  30. x.patientId,
  31. x.patient_name,
  32. x.inviter_name,
  33. x.sex,
  34. x.patient_mobile as mobile,
  35. x.patient_birthday as birthday,
  36. x.patient_identity as identity,
  37. x.come_number,
  38. x.apt_doctor_name,
  39. x.apt_opt_name as apt_optometrist_name,
  40. x.act_doctor_name,
  41. x.act_assistant_name,
  42. x.act_opt_name as act_optometrist_name,
  43. x.matter,
  44. x.type,
  45. x.come_time,
  46. x.org_name,
  47. x.source1,
  48. x.source2,
  49. x.manager_name,
  50. x.first_opt_name,
  51. x.first_accept_date,
  52. x.created as pay_time,
  53. x.type_flag,
  54. x.fee_class,
  55. x.by_user_name,
  56. x.coupon_name,
  57. x.sign_wxapp,
  58. x.add_manager,
  59. x.not_transaction_reason,
  60. c.org_id,
  61. c.customer_manager_staff_id,
  62. c.customer_optometrist_staff_id,
  63. SUM(x.pay_amt) as total_amt
  64. FROM fee_dealdetail x
  65. JOIN clinic_clinic c ON c.id = x.clinic_id
  66. WHERE x.group_id = %(group_id)s
  67. AND x.org_id != 16
  68. AND x.created >= %(fsdate)s
  69. AND x.created <= %(fedate)s
  70. GROUP BY x.clinic_id, x.patientId, x.patient_name, x.inviter_name, x.sex,
  71. x.patient_mobile, x.patient_birthday, x.patient_identity, x.come_number,
  72. x.apt_doctor_name, x.apt_opt_name, x.act_doctor_name, x.act_assistant_name,
  73. x.act_opt_name, x.matter, x.type, x.come_time, x.org_name, x.source1, x.source2,
  74. x.manager_name, x.first_opt_name, x.first_accept_date, x.created,
  75. x.type_flag, x.fee_class, x.by_user_name, x.coupon_name, x.sign_wxapp,
  76. x.add_manager, x.not_transaction_reason, c.org_id, c.customer_manager_staff_id, c.customer_optometrist_staff_id
  77. """
  78. logger.info("正在查询主数据...")
  79. cursor.execute(sql, params)
  80. data_list = cursor.fetchall() # DictCursor 直接返回字典列表
  81. logger.info(f"查询到 {len(data_list)} 条记录")
  82. if not data_list:
  83. cursor.close()
  84. conn.close()
  85. logger.info("[患者消费] 无数据")
  86. return 0
  87. # 2. 获取 clinic_id 列表
  88. clinic_ids = [data["clinic_id"] for data in data_list]
  89. # 3. 查询诊所和分组信息
  90. org_ids = list(set([d.get("org_id") for d in data_list if d.get("org_id")]))
  91. org_info_dict = {}
  92. if org_ids:
  93. sql = """
  94. SELECT o.id as org_id, o.name as org_name, o.group_id, g.name as group_name
  95. FROM org_org o
  96. LEFT JOIN org_group g ON o.group_id = g.id
  97. WHERE o.id IN %(ids)s
  98. """
  99. cursor.execute(sql, {"ids": org_ids})
  100. org_list = cursor.fetchall()
  101. org_info_dict = {o["org_id"]: o for o in org_list}
  102. # 4. 查询员工信息
  103. staff_ids = []
  104. for data in data_list:
  105. if data.get("customer_manager_staff_id"):
  106. staff_ids.append(data["customer_manager_staff_id"])
  107. if data.get("customer_optometrist_staff_id"):
  108. staff_ids.append(data["customer_optometrist_staff_id"])
  109. staff_ids = list(set(staff_ids))
  110. staff_dict = {}
  111. if staff_ids:
  112. sql = """
  113. SELECT s.id, u.name, s.qywx_userid, u.mobile
  114. FROM staff_staff s
  115. JOIN users_userprofile u ON s.user_id = u.id
  116. WHERE s.id IN %(ids)s
  117. """
  118. cursor.execute(sql, {"ids": staff_ids})
  119. staff_list = cursor.fetchall()
  120. staff_dict = {s["id"]: s for s in staff_list}
  121. # 5. 查询推广信息
  122. clinic_info_dict = {}
  123. if clinic_ids:
  124. sql = """
  125. SELECT cc.collect_date, cc.activity_location,
  126. u.name as extension_user_name, cc.clinic_id
  127. FROM returnvisit_customerconsultationrecord cc
  128. LEFT JOIN users_userprofile u ON cc.extension_user_id = u.id
  129. WHERE cc.clinic_id IN %(ids)s
  130. """
  131. cursor.execute(sql, {"ids": clinic_ids})
  132. clinic_info_list = cursor.fetchall()
  133. for c in clinic_info_list:
  134. clinic_info_dict[c["clinic_id"]] = c
  135. # 5.1 查询患者检查信息
  136. patient_ids = list(set([d.get("patientId") for d in data_list if d.get("patientId")]))
  137. patient_exam_dict = {}
  138. if patient_ids:
  139. sql = """
  140. SELECT patientId, r_sph, l_sph, r_nakeye, l_nakeye, al_od, al_os
  141. FROM patient_custome
  142. WHERE patientId IN %(ids)s
  143. """
  144. cursor.execute(sql, {"ids": patient_ids})
  145. exam_list = cursor.fetchall()
  146. patient_exam_dict = {e["patientId"]: e for e in exam_list}
  147. cursor.close()
  148. conn.close()
  149. # 6. 字段列表
  150. fields = [
  151. "clinic_id", "pay_date", "patientId", "patient_name", "mobile", "sex", "birthday",
  152. "identity", "come_number", "inviter_name", "come_time", "come_date", "type", "matter",
  153. "org_id", "org_name", "group_id", "group_name", "apt_doctor_id", "apt_doctor_name",
  154. "apt_optometrist_id", "apt_optometrist_name", "act_doctor_id", "act_doctor_name",
  155. "act_optometrist_id", "act_optometrist_name", "act_assistant_name",
  156. "source1", "source2", "manager_id", "manager_name", "manager_qywx_id", "manager_mobile",
  157. "optometrist_id", "optometrist_name", "optometrist_qywx_id", "optometrist_mobile",
  158. "first_opt_name", "first_accept_date",
  159. "r_sph", "l_sph", "r_nakeye", "l_nakeye", "al_od", "al_os", "not_transaction_reason",
  160. "total_amt", "pay_time", "by_user_id",
  161. "by_user_name", "coupon_name", "add_manager", "sign_wxapp", "collect_date",
  162. "activity_location", "extension_user_id", "extension_user_name", "create_time",
  163. ]
  164. # 7. 处理数据
  165. result_dict = {}
  166. for data in data_list:
  167. clinic_id = data["clinic_id"]
  168. # 处理金额(根据type_flag正负)
  169. total_amt = data.get("total_amt", 0)
  170. try:
  171. total_amt = float(total_amt) if total_amt is not None else 0.0
  172. except (TypeError, ValueError):
  173. total_amt = 0.0
  174. if str(data.get("type_flag")) != "1":
  175. total_amt = -total_amt if total_amt else 0
  176. # 处理日期
  177. pay_time = data.get("pay_time")
  178. if pay_time:
  179. if isinstance(pay_time, str):
  180. pay_date = pay_time.split(" ")[0]
  181. pay_time_str = pay_time if " " in pay_time else f"{pay_time} 00:00:00"
  182. else:
  183. pay_date = pay_time.strftime("%Y-%m-%d")
  184. pay_time_str = pay_time.strftime("%Y-%m-%d %H:%M:%S")
  185. else:
  186. pay_date = None
  187. pay_time_str = None
  188. if not pay_date:
  189. continue
  190. key = f"{clinic_id}_{pay_date}"
  191. if key in result_dict:
  192. result_dict[key]["total_amt"] += total_amt
  193. continue
  194. come_time = data.get("come_time")
  195. if come_time:
  196. if isinstance(come_time, str):
  197. come_date = come_time.split(" ")[0]
  198. come_time_str = come_time if " " in come_time else f"{come_time} 00:00:00"
  199. else:
  200. come_date = come_time.strftime("%Y-%m-%d")
  201. come_time_str = come_time.strftime("%Y-%m-%d %H:%M:%S")
  202. else:
  203. come_date = None
  204. come_time_str = None
  205. # 获取诊所信息
  206. org_id = data.get("org_id")
  207. org_info = org_info_dict.get(org_id, {})
  208. # 获取员工信息
  209. manager_staff = staff_dict.get(data.get("customer_manager_staff_id"), {})
  210. optometrist_staff = staff_dict.get(data.get("customer_optometrist_staff_id"), {})
  211. # 获取推广信息
  212. clinic_info = clinic_info_dict.get(clinic_id, {})
  213. # 获取患者检查信息
  214. patient_exam = patient_exam_dict.get(data.get("patientId"), {})
  215. # 处理性别
  216. sex_val = data.get("sex")
  217. if sex_val == "男":
  218. sex_val = 1
  219. elif sex_val == "女":
  220. sex_val = 2
  221. # 处理就诊类型
  222. type_val = data.get("type")
  223. if type_val == "初诊":
  224. type_val = 1
  225. elif type_val == "复诊":
  226. type_val = 2
  227. elif type_val == "预约":
  228. type_val = 3
  229. elif type_val == "复查":
  230. type_val = 4
  231. # 确保整数字段类型正确
  232. def safe_int(v):
  233. if v is None or v == "":
  234. return None
  235. try:
  236. return int(v)
  237. except (TypeError, ValueError):
  238. return None
  239. # 确保字符串字段去除两边空格
  240. def safe_str(v):
  241. if v is None:
  242. return ""
  243. return str(v).strip()
  244. record_dict = {
  245. "clinic_id": safe_int(clinic_id),
  246. "pay_date": pay_date,
  247. "patientId": safe_str(data.get("patientId")),
  248. "patient_name": safe_str(data.get("patient_name")),
  249. "mobile": safe_str(data.get("mobile")),
  250. "sex": safe_int(sex_val) if sex_val else None,
  251. "birthday": data.get("birthday"),
  252. "identity": safe_str(data.get("identity")),
  253. "come_number": safe_int(data.get("come_number")),
  254. "inviter_name": safe_str(data.get("inviter_name")),
  255. "come_time": come_time_str,
  256. "come_date": come_date,
  257. "type": safe_int(type_val) if type_val else None,
  258. "matter": safe_str(data.get("matter")),
  259. "org_id": safe_int(org_id),
  260. "org_name": safe_str(org_info.get("org_name")),
  261. "group_id": safe_int(org_info.get("group_id")),
  262. "group_name": safe_str(org_info.get("group_name")),
  263. "apt_doctor_id": None,
  264. "apt_doctor_name": safe_str(data.get("apt_doctor_name")),
  265. "apt_optometrist_id": None,
  266. "apt_optometrist_name": safe_str(data.get("apt_optometrist_name")),
  267. "act_doctor_id": None,
  268. "act_doctor_name": safe_str(data.get("act_doctor_name")),
  269. "act_optometrist_id": None,
  270. "act_optometrist_name": safe_str(data.get("act_optometrist_name")),
  271. "act_assistant_name": safe_str(data.get("act_assistant_name")),
  272. "source1": safe_str(data.get("source1")),
  273. "source2": safe_str(data.get("source2")),
  274. "manager_id": safe_int(data.get("customer_manager_staff_id")),
  275. "manager_name": safe_str(manager_staff.get("name")),
  276. "manager_qywx_id": safe_str(manager_staff.get("qywx_userid")),
  277. "manager_mobile": safe_str(manager_staff.get("mobile")),
  278. "optometrist_id": safe_int(data.get("customer_optometrist_staff_id")),
  279. "optometrist_name": safe_str(optometrist_staff.get("name")),
  280. "optometrist_qywx_id": safe_str(optometrist_staff.get("qywx_userid")),
  281. "optometrist_mobile": safe_str(optometrist_staff.get("mobile")),
  282. "first_opt_name": safe_str(data.get("first_opt_name")),
  283. "first_accept_date": data.get("first_accept_date"),
  284. "r_sph": safe_str(patient_exam.get("r_sph")),
  285. "l_sph": safe_str(patient_exam.get("l_sph")),
  286. "r_nakeye": safe_str(patient_exam.get("r_nakeye")),
  287. "l_nakeye": safe_str(patient_exam.get("l_nakeye")),
  288. "al_od": safe_str(patient_exam.get("al_od")),
  289. "al_os": safe_str(patient_exam.get("al_os")),
  290. "not_transaction_reason": safe_str(data.get("not_transaction_reason")),
  291. "total_amt": total_amt,
  292. "pay_time": pay_time_str,
  293. "by_user_id": None,
  294. "by_user_name": safe_str(data.get("by_user_name")),
  295. "coupon_name": safe_str(data.get("coupon_name")),
  296. "add_manager": "已添加" if data.get("add_manager") == 1 else "未添加",
  297. "sign_wxapp": "已注册" if data.get("sign_wxapp") == 1 else "未注册",
  298. "collect_date": clinic_info.get("collect_date"),
  299. "activity_location": safe_str(clinic_info.get("activity_location")),
  300. "extension_user_id": None,
  301. "extension_user_name": safe_str(clinic_info.get("extension_user_name")),
  302. "create_time": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
  303. }
  304. result_dict[key] = record_dict
  305. result = []
  306. for key, record_dict in result_dict.items():
  307. record = (
  308. record_dict["clinic_id"], record_dict["pay_date"], record_dict["patientId"],
  309. record_dict["patient_name"], record_dict["mobile"], record_dict["sex"],
  310. record_dict["birthday"], record_dict["identity"], record_dict["come_number"],
  311. record_dict["inviter_name"], record_dict["come_time"], record_dict["come_date"],
  312. record_dict["type"], record_dict["matter"], record_dict["org_id"],
  313. record_dict["org_name"], record_dict["group_id"], record_dict["group_name"],
  314. record_dict["apt_doctor_id"], record_dict["apt_doctor_name"],
  315. record_dict["apt_optometrist_id"], record_dict["apt_optometrist_name"],
  316. record_dict["act_doctor_id"], record_dict["act_doctor_name"],
  317. record_dict["act_optometrist_id"], record_dict["act_optometrist_name"],
  318. record_dict["act_assistant_name"], record_dict["source1"], record_dict["source2"],
  319. record_dict["manager_id"], record_dict["manager_name"],
  320. record_dict["manager_qywx_id"], record_dict["manager_mobile"],
  321. record_dict["optometrist_id"], record_dict["optometrist_name"],
  322. record_dict["optometrist_qywx_id"], record_dict["optometrist_mobile"],
  323. record_dict["first_opt_name"], record_dict["first_accept_date"],
  324. record_dict["r_sph"], record_dict["l_sph"], record_dict["r_nakeye"],
  325. record_dict["l_nakeye"], record_dict["al_od"], record_dict["al_os"],
  326. record_dict["not_transaction_reason"],
  327. float(record_dict["total_amt"]), record_dict["pay_time"],
  328. record_dict["by_user_id"], record_dict["by_user_name"], record_dict["coupon_name"],
  329. record_dict["add_manager"], record_dict["sign_wxapp"], record_dict["collect_date"],
  330. record_dict["activity_location"], record_dict["extension_user_id"],
  331. record_dict["extension_user_name"], record_dict["create_time"]
  332. )
  333. result.append(record)
  334. # 8. 写入Doris
  335. logger.info(f"数据处理完成,共 {len(result)} 条记录")
  336. conn = get_doris_conn()
  337. cursor = conn.cursor()
  338. # 8.1 先删除该时间段的数据(确保幂等性)
  339. logger.info(f"清理该时间段的历史数据: {start_date} ~ {end_date}")
  340. delete_sql = """
  341. DELETE FROM fact_patientconsumption
  342. WHERE pay_date >= %s AND pay_date <= %s
  343. AND group_id = %s
  344. """
  345. cursor.execute(delete_sql, (start_date, end_date, int(group_id)))
  346. deleted_count = cursor.rowcount
  347. logger.info(f"已删除 {deleted_count} 条历史数据")
  348. conn.commit()
  349. # 8.2 插入新数据
  350. sql = f"INSERT INTO fact_patientconsumption ({', '.join(fields)}) VALUES ({', '.join(['%s'] * len(fields))})"
  351. total = len(result)
  352. batch_size = 100
  353. success_count = 0
  354. for i in range(0, total, batch_size):
  355. batch = result[i:i + batch_size]
  356. try:
  357. cursor.executemany(sql, batch)
  358. conn.commit()
  359. success_count += len(batch)
  360. logger.info(f"[患者消费] 已写入 {min(i + batch_size, total)}/{total}")
  361. except Exception as e:
  362. logger.error(f"写入失败: {e}")
  363. conn.rollback()
  364. raise
  365. cursor.close()
  366. conn.close()
  367. logger.info("=" * 50)
  368. logger.info(f"[患者消费] 同步完成,共 {success_count} 条记录")
  369. logger.info("=" * 50)
  370. return success_count