# -*- coding: utf-8 -*- """小票详情加载器""" from ..base_loader import BaseLoader import json class TicketLoader(BaseLoader): """ Loader for parsing Ticket Detail JSON and populating DWD fact tables. Handles: - fact_order (Header) - fact_order_goods (Items) - fact_table_usage (Items) - fact_assistant_service (Items) """ def process_tickets(self, tickets: list, store_id: int) -> tuple: """ Process a batch of ticket JSONs. Returns (inserted_count, error_count) """ inserted_count = 0 error_count = 0 # Prepare batch lists orders = [] goods_list = [] table_usages = [] assistant_services = [] for ticket in tickets: try: # 1. Parse Header (fact_order) root_data = ticket.get("data", {}).get("data", {}) if not root_data: continue order_settle_id = root_data.get("orderSettleId") if not order_settle_id: continue orders.append({ "store_id": store_id, "order_settle_id": order_settle_id, "order_trade_no": 0, "order_no": str(root_data.get("orderSettleNumber", "")), "member_id": 0, "pay_time": root_data.get("payTime"), "total_amount": root_data.get("consumeMoney", 0), "pay_amount": root_data.get("actualPayment", 0), "discount_amount": root_data.get("memberOfferAmount", 0), "coupon_amount": root_data.get("couponAmount", 0), "status": "PAID", "cashier_name": root_data.get("cashierName", ""), "remark": root_data.get("orderRemark", ""), "raw_data": json.dumps(ticket, ensure_ascii=False) }) # 2. Parse Items (orderItem list) order_items = root_data.get("orderItem", []) for item in order_items: order_trade_no = item.get("siteOrderId") # 2.1 Table Ledger table_ledger = item.get("tableLedger") if table_ledger: table_usages.append({ "store_id": store_id, "order_ledger_id": table_ledger.get("orderTableLedgerId"), "order_settle_id": order_settle_id, "table_id": table_ledger.get("siteTableId"), "table_name": table_ledger.get("tableName"), "start_time": table_ledger.get("chargeStartTime"), "end_time": table_ledger.get("chargeEndTime"), "duration_minutes": table_ledger.get("useDuration", 0), "total_amount": table_ledger.get("consumptionAmount", 0), "pay_amount": table_ledger.get("consumptionAmount", 0) - table_ledger.get("memberDiscountAmount", 0) }) # 2.2 Goods Ledgers goods_ledgers = item.get("goodsLedgers", []) for g in goods_ledgers: goods_list.append({ "store_id": store_id, "order_goods_id": g.get("orderGoodsLedgerId"), "order_settle_id": order_settle_id, "order_trade_no": order_trade_no, "goods_id": g.get("siteGoodsId"), "goods_name": g.get("goodsName"), "quantity": g.get("goodsCount", 0), "unit_price": g.get("goodsPrice", 0), "total_amount": g.get("ledgerAmount", 0), "pay_amount": g.get("realGoodsMoney", 0) }) # 2.3 Assistant Services assistant_ledgers = item.get("assistantPlayWith", []) for a in assistant_ledgers: assistant_services.append({ "store_id": store_id, "ledger_id": a.get("orderAssistantLedgerId"), "order_settle_id": order_settle_id, "assistant_id": a.get("assistantId"), "assistant_name": a.get("ledgerName"), "service_type": a.get("skillName", "Play"), "start_time": a.get("ledgerStartTime"), "end_time": a.get("ledgerEndTime"), "duration_minutes": int(a.get("ledgerCount", 0) / 60) if a.get("ledgerCount") else 0, "total_amount": a.get("ledgerAmount", 0), "pay_amount": a.get("ledgerAmount", 0) }) inserted_count += 1 except Exception as e: self.logger.error(f"Error parsing ticket: {e}", exc_info=True) error_count += 1 # 3. Batch Insert/Upsert if orders: self._upsert_orders(orders) if goods_list: self._upsert_goods(goods_list) if table_usages: self._upsert_table_usages(table_usages) if assistant_services: self._upsert_assistant_services(assistant_services) return inserted_count, error_count def _upsert_orders(self, rows): sql = """ INSERT INTO billiards.fact_order ( store_id, order_settle_id, order_trade_no, order_no, member_id, pay_time, total_amount, pay_amount, discount_amount, coupon_amount, status, cashier_name, remark, raw_data ) VALUES ( %(store_id)s, %(order_settle_id)s, %(order_trade_no)s, %(order_no)s, %(member_id)s, %(pay_time)s, %(total_amount)s, %(pay_amount)s, %(discount_amount)s, %(coupon_amount)s, %(status)s, %(cashier_name)s, %(remark)s, %(raw_data)s ) ON CONFLICT (store_id, order_settle_id) DO UPDATE SET pay_time = EXCLUDED.pay_time, pay_amount = EXCLUDED.pay_amount, updated_at = now() """ self.db.batch_execute(sql, rows) def _upsert_goods(self, rows): sql = """ INSERT INTO billiards.fact_order_goods ( store_id, order_goods_id, order_settle_id, order_trade_no, goods_id, goods_name, quantity, unit_price, total_amount, pay_amount ) VALUES ( %(store_id)s, %(order_goods_id)s, %(order_settle_id)s, %(order_trade_no)s, %(goods_id)s, %(goods_name)s, %(quantity)s, %(unit_price)s, %(total_amount)s, %(pay_amount)s ) ON CONFLICT (store_id, order_goods_id) DO UPDATE SET pay_amount = EXCLUDED.pay_amount """ self.db.batch_execute(sql, rows) def _upsert_table_usages(self, rows): sql = """ INSERT INTO billiards.fact_table_usage ( store_id, order_ledger_id, order_settle_id, table_id, table_name, start_time, end_time, duration_minutes, total_amount, pay_amount ) VALUES ( %(store_id)s, %(order_ledger_id)s, %(order_settle_id)s, %(table_id)s, %(table_name)s, %(start_time)s, %(end_time)s, %(duration_minutes)s, %(total_amount)s, %(pay_amount)s ) ON CONFLICT (store_id, order_ledger_id) DO UPDATE SET pay_amount = EXCLUDED.pay_amount """ self.db.batch_execute(sql, rows) def _upsert_assistant_services(self, rows): sql = """ INSERT INTO billiards.fact_assistant_service ( store_id, ledger_id, order_settle_id, assistant_id, assistant_name, service_type, start_time, end_time, duration_minutes, total_amount, pay_amount ) VALUES ( %(store_id)s, %(ledger_id)s, %(order_settle_id)s, %(assistant_id)s, %(assistant_name)s, %(service_type)s, %(start_time)s, %(end_time)s, %(duration_minutes)s, %(total_amount)s, %(pay_amount)s ) ON CONFLICT (store_id, ledger_id) DO UPDATE SET pay_amount = EXCLUDED.pay_amount """ self.db.batch_execute(sql, rows)