| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304 |
- #!/usr/bin/env python
- # -*- coding: utf-8 -*-
- """
- 毛利分析数据同步任务
- 功能:将MySQL中的fee_incomedetail数据同步到Doris DWD层宽表
- """
- import datetime
- from db.pool import get_mysql_conn, get_doris_conn
- from logger.handler import get_logger
- logger = get_logger("job.grossprofit")
- def run(start_date=None, end_date=None, group_id=None):
- if start_date is None or end_date is None:
- yesterday = datetime.date.today() - datetime.timedelta(days=1)
- start_date = end_date = yesterday.strftime("%Y-%m-%d")
- logger.info("=" * 50)
- logger.info(f"[毛利分析] 开始同步: {start_date} ~ {end_date}")
- logger.info("=" * 50)
- mysql_conn = get_mysql_conn()
- doris_conn = get_doris_conn()
- mysql_cursor = mysql_conn.cursor()
- doris_cursor = doris_conn.cursor()
- if group_id:
- mysql_cursor.execute("SELECT id, name FROM org_group WHERE id = %s", (int(group_id),))
- else:
- mysql_cursor.execute("SELECT id, name FROM org_group")
- groups = mysql_cursor.fetchall()
- logger.info(f"发现 {len(groups)} 个集团")
- if not groups:
- logger.info(f"未找到集团: {group_id}")
- mysql_cursor.close()
- doris_cursor.close()
- mysql_conn.close()
- doris_conn.close()
- return 0
- total_count = 0
- sdate = start_date + " 00:00:00"
- # 结束时间用下一天0点,SQL用<,刚好覆盖整天
- edate_dt = datetime.datetime.strptime(end_date, "%Y-%m-%d") + datetime.timedelta(days=1)
- edate = edate_dt.strftime("%Y-%m-%d") + " 00:00:00"
- logger.info(f"删除 {start_date} ~ {end_date} 的旧数据...")
- if group_id:
- doris_cursor.execute(
- "DELETE FROM fact_fee_grossprofit WHERE pay_time >= %s AND pay_time < %s AND group_id = %s",
- (sdate, edate, int(group_id))
- )
- else:
- doris_cursor.execute(
- "DELETE FROM fact_fee_grossprofit WHERE pay_time >= %s AND pay_time < %s",
- (sdate, edate)
- )
- doris_conn.commit()
- logger.info(f"旧数据已删除")
- for group in groups:
- group_id = group["id"]
- group_name = group["name"]
- logger.info(f"开始同步集团: {group_name} (id={group_id})")
- count = sync_group_data(mysql_cursor, doris_cursor, doris_conn, group_id, group_name, sdate, edate)
- total_count += count
- logger.info(f"集团 {group_name} 同步完成: {count} 条")
- logger.info(f"[毛利分析] 全部同步完成: 共 {total_count} 条")
- mysql_cursor.close()
- doris_cursor.close()
- mysql_conn.close()
- doris_conn.close()
- def sync_group_data(mysql_cursor, doris_cursor, doris_conn, group_id, group_name, sdate, edate):
- try:
- params = {"group_id": group_id, "sdate": sdate, "edate": edate}
- sql = """
- SELECT
- x.id, x.created, x.modified, x.patient_name, x.patient_identity,
- x.patient_mobile, x.patientId, x.org_name, x.order_no, x.fee_class,
- x.class_name, x.combo_name, x.per_id, x.name, x.unit,
- x.price, x.num, x.sum_price, x.should_amt, x.discount,
- x.discount_memo, x.coupon_name, x.flag, x.type_flag, x.order_status,
- x.created AS pay_time, x.exec_time, x.exec_status, x.cost_price, x.mcost_price,
- x.jcost_price, x.cost_flag, x.cost_flag2, x.cost_update_time, x.cost_update_time2,
- x.profit_amt, x.profit_rate, x.mprofit_amt, x.mprofit_rate,
- x.jprofit_amt, x.jprofit_rate, x.blq_amt, x.swap_card_amt, x.eye_card_amt,
- x.hx_amt, x.hx_cost_price, x.hx_sum_price, x.hx_num,
- x.pay_amt, x.pay_cost_price, x.pay_sum_price, x.pay_num,
- x.pay_profit_amt, x.pay_profit_rate, x.by_user_name, x.doctor_name,
- x.acp_doctor_name, x.acp_opt_name, x.acp_assistant_name,
- x.clinic_id, x.org_id, x.order_id,
- x.pk_code, x.uuid_no, x.birthday, x.first_accept_date,
- x.first_distance_day, x.age, x.source1, x.source2,
- x.inviter_name, x.inviter_phone, x.line_name, x.flag_memo,
- x.brands, x.factory, x.variety, x.visual, x.exclude_flag, x.pay_status,
- c.come_time, x.pay_time AS sell_time,
- CASE WHEN x.flag IN (9, 11) THEN f.pay_time ELSE NULL END AS refund_pay_time,
- 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
- FROM fee_incomedetail x
- JOIN clinic_clinic c ON c.id = x.clinic_id
- LEFT JOIN finance_bill f ON x.type_flag = 2 AND x.flag IN (9, 11) AND f.order_no = x.order_no
- LEFT JOIN fee_order fo ON x.type_flag = 2 AND x.flag NOT IN (9, 11) AND fo.id = x.order_id
- WHERE x.created >= %(sdate)s AND x.created < %(edate)s
- AND x.group_id = %(group_id)s
- AND x.visual = 1 AND x.org_id != 16
- AND (x.flag IN (4, 5) OR (x.flag IN (1, 2, 6, 7, 8, 9, 10, 11, 12) AND x.should_amt != 0))
- ORDER BY x.id
- """
- logger.info(f"查询MySQL数据... (created: {sdate} ~ {edate})")
- mysql_cursor.execute(sql, params)
- rows = mysql_cursor.fetchall()
- logger.info(f"获取到 {len(rows)} 条数据")
- if not rows:
- logger.info("没有数据需要同步")
- return 0
- doris_insert_sql = """
- INSERT INTO fact_fee_grossprofit (
- fee_incomedetail_id, created, modified, month, org_name, group_id, group_name,
- pay_time, fee_time, clinic_id, come_time, patientId,
- patient_name, patient_mobile, patient_identity, birthday, age,
- source1, source2, inviter_name, inviter_phone,
- first_accept_date, first_distance_day, by_user_name,
- doctor_name, opt_name, assistant_name,
- pay_status, order_no, pk_code, uuid_no,
- class_name, line_name, fee_class, name, combo_name, flag_memo,
- price, num, unit, sum_price, should_amt,
- cost_amt, profit_amt, profit_rate, discount,
- discount_memo, coupon_name, blq_amt,
- hx_sum_price, hx_num, hx_amt, hx_cost_price,
- pay_sum_price, pay_num, pay_amt, pay_cost_price,
- pay_profit_amt, pay_profit_rate,
- factory_name, brands_name, variety_name,
- exec_time, exec_status_name, order_status_name,
- cost_flag, update_time, cost_flag2, update_time2,
- exec_status, flag, per_id, order_id, type_flag, order_status, sell_time, sync_date
- ) VALUES (
- %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s,
- %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s,
- %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s,
- %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s
- )
- """
- def format_datetime(v):
- if isinstance(v, datetime.datetime):
- return v.strftime("%Y-%m-%d %H:%M:%S") if v else None
- elif isinstance(v, datetime.date):
- return v.strftime("%Y-%m-%d") if v else None
- return v
- def format_profit_rate(v):
- if v is None or v == 0:
- return ""
- return f"{float(v) * 100:.2f}%"
- def format_discount(v):
- if v is None or v == 0:
- return ""
- return f"{float(v) * 100:.2f}%"
- batch_size = 1000
- for i in range(0, len(rows), batch_size):
- batch = rows[i:i + batch_size]
- values = []
- for row in batch:
- r = row # DictCursor 已经返回字典
- created = format_datetime(r.get("created"))
- month = created[:7] if created else ""
- pay_time = format_datetime(r.get("pay_time")) # 即 x.created
- sell_time = format_datetime(r.get("sell_time")) # 即 x.pay_time
- # fee_time 计算逻辑完全对齐 API
- if r.get("name") != "系统逾期项目":
- fee_time = created
- else:
- fee_time = sell_time
- type_flag = r.get("type_flag", 1)
- if type_flag == 2:
- if r.get("flag") in [9, 11]:
- fee_time = format_datetime(r.get("refund_pay_time"))
- else:
- if r.get("name") != "系统逾期项目":
- fee_time = format_datetime(r.get("refund_order_pay_time"))
- else:
- fee_time = sell_time
- exec_status = r.get("exec_status", 0)
- exec_status_name = "已交付" if exec_status == 1 else "未交付"
- order_status = r.get("order_status", 1)
- order_status_name = "正常"
- # 完全对齐 API 中的 Bug: 若 order_status == 2,把 exec_status_name 设为 "作废"
- if order_status == 2:
- exec_status_name = "作废"
- pay_status = "已退费" if type_flag == 2 else "已收费"
- cost_amt = r.get("mcost_price", 0) or 0
- profit_amt = r.get("mprofit_amt", 0) or 0
-
- sum_price = float(r.get("sum_price", 0) or 0)
- should_amt = float(r.get("should_amt", 0) or 0)
- num = r.get("num", 0) or 0
-
- if type_flag == 2:
- cost_amt = -(cost_amt)
- profit_amt = -(profit_amt)
- sum_price = -sum_price
- should_amt = -should_amt
- num = -num
- profit_rate = format_profit_rate(r.get("mprofit_rate"))
- discount = format_discount(r.get("discount"))
- come_time = format_datetime(r.get("come_time"))
- exec_time = format_datetime(r.get("exec_time"))
- update_time = format_datetime(r.get("cost_update_time"))
- update_time2 = format_datetime(r.get("cost_update_time2"))
- sync_date = pay_time.split(" ")[0] if pay_time else None
- 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)
- hx_sum_price = float(r.get("hx_sum_price", 0) or 0)
- hx_num = r.get("hx_num", 0) or 0
- hx_amt = float(r.get("hx_amt", 0) or 0)
- hx_cost_price = float(r.get("hx_cost_price", 0) or 0)
- pay_sum_price = float(r.get("pay_sum_price", 0) or 0)
- pay_num = r.get("pay_num", 0) or 0
- pay_amt = float(r.get("pay_amt", 0) or 0)
- pay_cost_price = float(r.get("pay_cost_price", 0) or 0)
- pay_profit_amt = float(r.get("pay_profit_amt", 0) or 0)
- pay_profit_rate = r.get("pay_profit_rate", 0) or 0
- if type_flag == 2:
- hx_sum_price = -hx_sum_price
- hx_num = -hx_num
- hx_amt = -hx_amt
- hx_cost_price = -hx_cost_price
- pay_sum_price = -pay_sum_price
- pay_num = -pay_num
- pay_amt = -pay_amt
- pay_cost_price = -pay_cost_price
- pay_profit_amt = -pay_profit_amt
- pay_profit_rate = -pay_profit_rate
- values.append((
- r.get("id"), created, format_datetime(r.get("modified")), month,
- r.get("org_name"), group_id, group_name,
- pay_time, fee_time, r.get("clinic_id"), come_time, r.get("patientId"),
- r.get("patient_name"), r.get("patient_mobile"), r.get("patient_identity"),
- format_datetime(r.get("birthday")), r.get("age", 0),
- r.get("source1"), r.get("source2"),
- r.get("inviter_name"), r.get("inviter_phone"),
- format_datetime(r.get("first_accept_date")), r.get("first_distance_day", 0),
- r.get("by_user_name"),
- r.get("doctor_name"), r.get("acp_opt_name"), r.get("acp_assistant_name"),
- pay_status, r.get("order_no"), r.get("pk_code"), r.get("uuid_no"),
- r.get("class_name"), r.get("line_name"), r.get("fee_class"),
- r.get("name"), r.get("combo_name"), r.get("flag_memo"),
- r.get("price"), num, r.get("unit"), sum_price, should_amt,
- cost_amt, profit_amt, profit_rate, discount,
- r.get("discount_memo"), r.get("coupon_name"), blq_amt,
- hx_sum_price, hx_num, hx_amt, hx_cost_price,
- pay_sum_price, pay_num, pay_amt, pay_cost_price,
- pay_profit_amt, pay_profit_rate,
- r.get("factory"), r.get("brands"), r.get("variety"),
- exec_time, exec_status_name, order_status_name,
- r.get("cost_flag", 0), update_time, r.get("cost_flag2", 0), update_time2,
- exec_status, r.get("flag"), r.get("per_id"), r.get("order_id"),
- type_flag, order_status, sell_time, sync_date
- ))
- doris_cursor.executemany(doris_insert_sql, values)
- doris_conn.commit()
- logger.info(f"已插入 {i + len(batch)} / {len(rows)} 条")
- logger.info(f"[毛利分析] 同步完成: {len(rows)} 条")
- return len(rows)
- except Exception as e:
- logger.error(f"同步失败: {e}")
- doris_conn.rollback()
- raise
- if __name__ == "__main__":
- import argparse
- parser = argparse.ArgumentParser()
- parser.add_argument("--start", type=str, help="开始日期 YYYY-MM-DD")
- parser.add_argument("--end", type=str, help="结束日期 YYYY-MM-DD")
- parser.add_argument("--group-id", type=str, help="集团ID,不传则同步全部集团")
- args = parser.parse_args()
- run(args.start, args.end, args.group_id)
|