grossprofit.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. """
  4. 毛利分析数据同步任务
  5. 功能:将MySQL中的fee_incomedetail数据同步到Doris DWD层宽表
  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.grossprofit")
  11. def run(start_date=None, end_date=None, group_id=None):
  12. if start_date is None or end_date is None:
  13. yesterday = datetime.date.today() - datetime.timedelta(days=1)
  14. start_date = end_date = yesterday.strftime("%Y-%m-%d")
  15. logger.info("=" * 50)
  16. logger.info(f"[毛利分析] 开始同步: {start_date} ~ {end_date}")
  17. logger.info("=" * 50)
  18. mysql_conn = get_mysql_conn()
  19. doris_conn = get_doris_conn()
  20. mysql_cursor = mysql_conn.cursor()
  21. doris_cursor = doris_conn.cursor()
  22. if group_id:
  23. mysql_cursor.execute("SELECT id, name FROM org_group WHERE id = %s", (int(group_id),))
  24. else:
  25. mysql_cursor.execute("SELECT id, name FROM org_group")
  26. groups = mysql_cursor.fetchall()
  27. logger.info(f"发现 {len(groups)} 个集团")
  28. if not groups:
  29. logger.info(f"未找到集团: {group_id}")
  30. mysql_cursor.close()
  31. doris_cursor.close()
  32. mysql_conn.close()
  33. doris_conn.close()
  34. return 0
  35. total_count = 0
  36. sdate = start_date + " 00:00:00"
  37. # 结束时间用下一天0点,SQL用<,刚好覆盖整天
  38. edate_dt = datetime.datetime.strptime(end_date, "%Y-%m-%d") + datetime.timedelta(days=1)
  39. edate = edate_dt.strftime("%Y-%m-%d") + " 00:00:00"
  40. logger.info(f"删除 {start_date} ~ {end_date} 的旧数据...")
  41. if group_id:
  42. doris_cursor.execute(
  43. "DELETE FROM fact_fee_grossprofit WHERE pay_time >= %s AND pay_time < %s AND group_id = %s",
  44. (sdate, edate, int(group_id))
  45. )
  46. else:
  47. doris_cursor.execute(
  48. "DELETE FROM fact_fee_grossprofit WHERE pay_time >= %s AND pay_time < %s",
  49. (sdate, edate)
  50. )
  51. doris_conn.commit()
  52. logger.info(f"旧数据已删除")
  53. for group in groups:
  54. group_id = group["id"]
  55. group_name = group["name"]
  56. logger.info(f"开始同步集团: {group_name} (id={group_id})")
  57. count = sync_group_data(mysql_cursor, doris_cursor, doris_conn, group_id, group_name, sdate, edate)
  58. total_count += count
  59. logger.info(f"集团 {group_name} 同步完成: {count} 条")
  60. logger.info(f"[毛利分析] 全部同步完成: 共 {total_count} 条")
  61. mysql_cursor.close()
  62. doris_cursor.close()
  63. mysql_conn.close()
  64. doris_conn.close()
  65. def sync_group_data(mysql_cursor, doris_cursor, doris_conn, group_id, group_name, sdate, edate):
  66. try:
  67. params = {"group_id": group_id, "sdate": sdate, "edate": edate}
  68. sql = """
  69. SELECT
  70. x.id, x.created, x.modified, x.patient_name, x.patient_identity,
  71. x.patient_mobile, x.patientId, x.org_name, x.order_no, x.fee_class,
  72. x.class_name, x.combo_name, x.per_id, x.name, x.unit,
  73. x.price, x.num, x.sum_price, x.should_amt, x.discount,
  74. x.discount_memo, x.coupon_name, x.flag, x.type_flag, x.order_status,
  75. x.created AS pay_time, x.exec_time, x.exec_status, x.cost_price, x.mcost_price,
  76. x.jcost_price, x.cost_flag, x.cost_flag2, x.cost_update_time, x.cost_update_time2,
  77. x.profit_amt, x.profit_rate, x.mprofit_amt, x.mprofit_rate,
  78. x.jprofit_amt, x.jprofit_rate, x.blq_amt, x.swap_card_amt, x.eye_card_amt,
  79. x.hx_amt, x.hx_cost_price, x.hx_sum_price, x.hx_num,
  80. x.pay_amt, x.pay_cost_price, x.pay_sum_price, x.pay_num,
  81. x.pay_profit_amt, x.pay_profit_rate, x.by_user_name, x.doctor_name,
  82. x.acp_doctor_name, x.acp_opt_name, x.acp_assistant_name,
  83. x.clinic_id, x.org_id, x.order_id,
  84. x.pk_code, x.uuid_no, x.birthday, x.first_accept_date,
  85. x.first_distance_day, x.age, x.source1, x.source2,
  86. x.inviter_name, x.inviter_phone, x.line_name, x.flag_memo,
  87. x.brands, x.factory, x.variety, x.visual, x.exclude_flag, x.pay_status,
  88. c.come_time, x.pay_time AS sell_time,
  89. CASE WHEN x.flag IN (9, 11) THEN f.pay_time ELSE NULL END AS refund_pay_time,
  90. CASE WHEN x.type_flag = 2 AND x.flag NOT IN (9, 11) THEN fo.pay_time ELSE NULL END AS refund_order_pay_time
  91. FROM fee_incomedetail x
  92. JOIN clinic_clinic c ON c.id = x.clinic_id
  93. LEFT JOIN finance_bill f ON x.type_flag = 2 AND x.flag IN (9, 11) AND f.order_no = x.order_no
  94. LEFT JOIN fee_order fo ON x.type_flag = 2 AND x.flag NOT IN (9, 11) AND fo.id = x.order_id
  95. WHERE x.created >= %(sdate)s AND x.created < %(edate)s
  96. AND x.group_id = %(group_id)s
  97. AND x.visual = 1 AND x.org_id != 16
  98. AND (x.flag IN (4, 5) OR (x.flag IN (1, 2, 6, 7, 8, 9, 10, 11, 12) AND x.should_amt != 0))
  99. ORDER BY x.id
  100. """
  101. logger.info(f"查询MySQL数据... (created: {sdate} ~ {edate})")
  102. mysql_cursor.execute(sql, params)
  103. rows = mysql_cursor.fetchall()
  104. logger.info(f"获取到 {len(rows)} 条数据")
  105. if not rows:
  106. logger.info("没有数据需要同步")
  107. return 0
  108. doris_insert_sql = """
  109. INSERT INTO fact_fee_grossprofit (
  110. fee_incomedetail_id, created, modified, month, org_name, group_id, group_name,
  111. pay_time, fee_time, clinic_id, come_time, patientId,
  112. patient_name, patient_mobile, patient_identity, birthday, age,
  113. source1, source2, inviter_name, inviter_phone,
  114. first_accept_date, first_distance_day, by_user_name,
  115. doctor_name, opt_name, assistant_name,
  116. pay_status, order_no, pk_code, uuid_no,
  117. class_name, line_name, fee_class, name, combo_name, flag_memo,
  118. price, num, unit, sum_price, should_amt,
  119. cost_amt, profit_amt, profit_rate, discount,
  120. discount_memo, coupon_name, blq_amt,
  121. hx_sum_price, hx_num, hx_amt, hx_cost_price,
  122. pay_sum_price, pay_num, pay_amt, pay_cost_price,
  123. pay_profit_amt, pay_profit_rate,
  124. factory_name, brands_name, variety_name,
  125. exec_time, exec_status_name, order_status_name,
  126. cost_flag, update_time, cost_flag2, update_time2,
  127. exec_status, flag, per_id, order_id, type_flag, order_status, sell_time, sync_date
  128. ) VALUES (
  129. %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s,
  130. %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s,
  131. %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s,
  132. %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s
  133. )
  134. """
  135. def format_datetime(v):
  136. if isinstance(v, datetime.datetime):
  137. return v.strftime("%Y-%m-%d %H:%M:%S") if v else None
  138. elif isinstance(v, datetime.date):
  139. return v.strftime("%Y-%m-%d") if v else None
  140. return v
  141. def format_profit_rate(v):
  142. if v is None or v == 0:
  143. return ""
  144. return f"{float(v) * 100:.2f}%"
  145. def format_discount(v):
  146. if v is None or v == 0:
  147. return ""
  148. return f"{float(v) * 100:.2f}%"
  149. batch_size = 1000
  150. for i in range(0, len(rows), batch_size):
  151. batch = rows[i:i + batch_size]
  152. values = []
  153. for row in batch:
  154. r = row # DictCursor 已经返回字典
  155. created = format_datetime(r.get("created"))
  156. month = created[:7] if created else ""
  157. pay_time = format_datetime(r.get("pay_time")) # 即 x.created
  158. sell_time = format_datetime(r.get("sell_time")) # 即 x.pay_time
  159. # fee_time 计算逻辑完全对齐 API
  160. if r.get("name") != "系统逾期项目":
  161. fee_time = created
  162. else:
  163. fee_time = sell_time
  164. type_flag = r.get("type_flag", 1)
  165. if type_flag == 2:
  166. if r.get("flag") in [9, 11]:
  167. fee_time = format_datetime(r.get("refund_pay_time"))
  168. else:
  169. if r.get("name") != "系统逾期项目":
  170. fee_time = format_datetime(r.get("refund_order_pay_time"))
  171. else:
  172. fee_time = sell_time
  173. exec_status = r.get("exec_status", 0)
  174. exec_status_name = "已交付" if exec_status == 1 else "未交付"
  175. order_status = r.get("order_status", 1)
  176. order_status_name = "正常"
  177. # 完全对齐 API 中的 Bug: 若 order_status == 2,把 exec_status_name 设为 "作废"
  178. if order_status == 2:
  179. exec_status_name = "作废"
  180. pay_status = "已退费" if type_flag == 2 else "已收费"
  181. cost_amt = r.get("mcost_price", 0) or 0
  182. profit_amt = r.get("mprofit_amt", 0) or 0
  183. sum_price = float(r.get("sum_price", 0) or 0)
  184. should_amt = float(r.get("should_amt", 0) or 0)
  185. num = r.get("num", 0) or 0
  186. if type_flag == 2:
  187. cost_amt = -(cost_amt)
  188. profit_amt = -(profit_amt)
  189. sum_price = -sum_price
  190. should_amt = -should_amt
  191. num = -num
  192. profit_rate = format_profit_rate(r.get("mprofit_rate"))
  193. discount = format_discount(r.get("discount"))
  194. come_time = format_datetime(r.get("come_time"))
  195. exec_time = format_datetime(r.get("exec_time"))
  196. update_time = format_datetime(r.get("cost_update_time"))
  197. update_time2 = format_datetime(r.get("cost_update_time2"))
  198. sync_date = pay_time.split(" ")[0] if pay_time else None
  199. blq_amt = float(r.get("blq_amt", 0) or 0) + float(r.get("swap_card_amt", 0) or 0) + float(r.get("eye_card_amt", 0) or 0)
  200. hx_sum_price = float(r.get("hx_sum_price", 0) or 0)
  201. hx_num = r.get("hx_num", 0) or 0
  202. hx_amt = float(r.get("hx_amt", 0) or 0)
  203. hx_cost_price = float(r.get("hx_cost_price", 0) or 0)
  204. pay_sum_price = float(r.get("pay_sum_price", 0) or 0)
  205. pay_num = r.get("pay_num", 0) or 0
  206. pay_amt = float(r.get("pay_amt", 0) or 0)
  207. pay_cost_price = float(r.get("pay_cost_price", 0) or 0)
  208. pay_profit_amt = float(r.get("pay_profit_amt", 0) or 0)
  209. pay_profit_rate = r.get("pay_profit_rate", 0) or 0
  210. if type_flag == 2:
  211. hx_sum_price = -hx_sum_price
  212. hx_num = -hx_num
  213. hx_amt = -hx_amt
  214. hx_cost_price = -hx_cost_price
  215. pay_sum_price = -pay_sum_price
  216. pay_num = -pay_num
  217. pay_amt = -pay_amt
  218. pay_cost_price = -pay_cost_price
  219. pay_profit_amt = -pay_profit_amt
  220. pay_profit_rate = -pay_profit_rate
  221. values.append((
  222. r.get("id"), created, format_datetime(r.get("modified")), month,
  223. r.get("org_name"), group_id, group_name,
  224. pay_time, fee_time, r.get("clinic_id"), come_time, r.get("patientId"),
  225. r.get("patient_name"), r.get("patient_mobile"), r.get("patient_identity"),
  226. format_datetime(r.get("birthday")), r.get("age", 0),
  227. r.get("source1"), r.get("source2"),
  228. r.get("inviter_name"), r.get("inviter_phone"),
  229. format_datetime(r.get("first_accept_date")), r.get("first_distance_day", 0),
  230. r.get("by_user_name"),
  231. r.get("doctor_name"), r.get("acp_opt_name"), r.get("acp_assistant_name"),
  232. pay_status, r.get("order_no"), r.get("pk_code"), r.get("uuid_no"),
  233. r.get("class_name"), r.get("line_name"), r.get("fee_class"),
  234. r.get("name"), r.get("combo_name"), r.get("flag_memo"),
  235. r.get("price"), num, r.get("unit"), sum_price, should_amt,
  236. cost_amt, profit_amt, profit_rate, discount,
  237. r.get("discount_memo"), r.get("coupon_name"), blq_amt,
  238. hx_sum_price, hx_num, hx_amt, hx_cost_price,
  239. pay_sum_price, pay_num, pay_amt, pay_cost_price,
  240. pay_profit_amt, pay_profit_rate,
  241. r.get("factory"), r.get("brands"), r.get("variety"),
  242. exec_time, exec_status_name, order_status_name,
  243. r.get("cost_flag", 0), update_time, r.get("cost_flag2", 0), update_time2,
  244. exec_status, r.get("flag"), r.get("per_id"), r.get("order_id"),
  245. type_flag, order_status, sell_time, sync_date
  246. ))
  247. doris_cursor.executemany(doris_insert_sql, values)
  248. doris_conn.commit()
  249. logger.info(f"已插入 {i + len(batch)} / {len(rows)} 条")
  250. logger.info(f"[毛利分析] 同步完成: {len(rows)} 条")
  251. return len(rows)
  252. except Exception as e:
  253. logger.error(f"同步失败: {e}")
  254. doris_conn.rollback()
  255. raise
  256. if __name__ == "__main__":
  257. import argparse
  258. parser = argparse.ArgumentParser()
  259. parser.add_argument("--start", type=str, help="开始日期 YYYY-MM-DD")
  260. parser.add_argument("--end", type=str, help="结束日期 YYYY-MM-DD")
  261. parser.add_argument("--group-id", type=str, help="集团ID,不传则同步全部集团")
  262. args = parser.parse_args()
  263. run(args.start, args.end, args.group_id)