|
|
@@ -113,8 +113,11 @@ class FlightPriceClient:
|
|
|
time.sleep(RETRY_INTERVAL)
|
|
|
resp = requests.post(url, headers=self.headers, json=payload, timeout=30)
|
|
|
resp.raise_for_status()
|
|
|
- print(resp.json())
|
|
|
- return resp.json()
|
|
|
+ body = resp.json()
|
|
|
+ print(json.dumps(body, ensure_ascii=False)[:200])
|
|
|
+ return body
|
|
|
+ # print(resp.json())
|
|
|
+ # return resp.json()
|
|
|
except requests.Timeout as e:
|
|
|
last_err = FlightPriceRequestError(f"请求超时: {url}", cause=e)
|
|
|
except requests.ConnectionError as e:
|
|
|
@@ -183,9 +186,10 @@ class ResultMatcher:
|
|
|
第 i 段需满足:seg[i].cabin == 第 i 个舱位、seg[i].baggage == 第 i 个行李、seg[i].flight_number == 第 i 个航班号。
|
|
|
返回匹配到的那条 result 项(含 data),未匹配到返回 None。
|
|
|
"""
|
|
|
- cabin_list = [s.strip() for s in (cabins or "").split(";")]
|
|
|
- baggage_list = [s.strip() for s in (baggages or "").split(";")]
|
|
|
- flight_list = [s.strip() for s in (flight_numbers or "").split(";")] if flight_numbers else []
|
|
|
+ separator = '|' # 更换分隔符由;为|
|
|
|
+ cabin_list = [s.strip() for s in (cabins or "").split(separator)]
|
|
|
+ baggage_list = [s.strip() for s in (baggages or "").split(separator)]
|
|
|
+ flight_list = [s.strip() for s in (flight_numbers or "").split(separator)] if flight_numbers else []
|
|
|
|
|
|
n = len(cabin_list)
|
|
|
if n == 0 or len(baggage_list) != n:
|
|
|
@@ -257,17 +261,19 @@ class FlightPriceTaskRunner:
|
|
|
self.client = client or FlightPriceClient()
|
|
|
self.matcher = ResultMatcher()
|
|
|
self.handler = VerifyResultHandler()
|
|
|
- # self.rate = fetch_rate("USD", "CNY")
|
|
|
+ self.rate = fetch_rate("USD", "CNY")
|
|
|
|
|
|
|
|
|
def run(
|
|
|
self,
|
|
|
task: dict,
|
|
|
+ do_verify: bool = True,
|
|
|
) -> dict:
|
|
|
"""
|
|
|
执行单条任务。task 需含: from_city_code, to_city_code, from_day, cabin, baggage, adult_total_price。
|
|
|
flight_number 可选,用于匹配。
|
|
|
- 返回: {"status": "ok"|"placeholder"|"no_match", "price_info": {...}, "raw_verify": ...}
|
|
|
+ do_verify=False 时仅执行询价+匹配并返回 matched(不做验价)
|
|
|
+ 返回: {"status": "ok"|"placeholder"|"no_match", "price_info": {...}, "raw_verify": ..., "raw_search": ..., "matched": ...}
|
|
|
"""
|
|
|
from_city_code = task["from_city_code"]
|
|
|
to_city_code = task["to_city_code"]
|
|
|
@@ -296,6 +302,38 @@ class FlightPriceTaskRunner:
|
|
|
data = matched.get("data")
|
|
|
if not data:
|
|
|
return {"status": "no_data", "msg": "匹配项无 data", "raw_search": search_resp}
|
|
|
+
|
|
|
+ # 只询价不验价走的流程
|
|
|
+ if not do_verify:
|
|
|
+ # return {"status": "ok", "raw_search": search_resp, "matched": matched, "data": data}
|
|
|
+ expected_in_currency, rate_err = self._expected_price_in_verify_currency(task, matched)
|
|
|
+ if rate_err:
|
|
|
+ return {
|
|
|
+ "status": "rate_error",
|
|
|
+ "msg": rate_err,
|
|
|
+ "raw_search": search_resp,
|
|
|
+ "matched": matched,
|
|
|
+ "data": data,
|
|
|
+ }
|
|
|
+ actual = matched.get("adult_total_price") # 询价和验价接口出来的币种已经是人民币, 不用再转换
|
|
|
+ if self._price_within_threshold(expected_in_currency, actual): # 对比
|
|
|
+ return {
|
|
|
+ "status": "ok",
|
|
|
+ "price_info": self._extract_price_info(matched),
|
|
|
+ "raw_search": search_resp,
|
|
|
+ "matched": matched,
|
|
|
+ "data": data,
|
|
|
+ }
|
|
|
+ return {
|
|
|
+ "status": "price_not_within_threshold",
|
|
|
+ "msg": "询价结果价格不在阈值内",
|
|
|
+ "expected": expected_in_currency,
|
|
|
+ "actual": actual,
|
|
|
+ "raw_search": search_resp,
|
|
|
+ "matched": matched,
|
|
|
+ "data": data,
|
|
|
+ }
|
|
|
+
|
|
|
# 2. 验价(先 not_verify=False)
|
|
|
try:
|
|
|
verify_resp = self.client.verify_price(
|
|
|
@@ -385,14 +423,16 @@ class FlightPriceTaskRunner:
|
|
|
expected_val = float(task.get("adult_total_price"))
|
|
|
except (TypeError, ValueError):
|
|
|
return None, "任务 adult_total_price 无效"
|
|
|
- task_currency = (task.get("currency") or "USD").strip().upper()
|
|
|
- verify_currency = (valid.get("currency") or "CNY").strip().upper()
|
|
|
- if task_currency == verify_currency:
|
|
|
- return expected_val, None
|
|
|
- rate = fetch_rate(task_currency, verify_currency)
|
|
|
- if rate is None:
|
|
|
- return None, "汇率获取失败"
|
|
|
- return expected_val * rate, None
|
|
|
+ if self.rate is None:
|
|
|
+ task_currency = (task.get("currency") or "USD").strip().upper()
|
|
|
+ verify_currency = (valid.get("currency") or "CNY").strip().upper()
|
|
|
+ if task_currency == verify_currency:
|
|
|
+ return expected_val, None
|
|
|
+ rate = fetch_rate(task_currency, verify_currency)
|
|
|
+ if rate is None:
|
|
|
+ return None, "汇率获取失败"
|
|
|
+ self.rate = rate
|
|
|
+ return expected_val * self.rate, None
|
|
|
|
|
|
@staticmethod
|
|
|
def _price_within_threshold(
|
|
|
@@ -428,7 +468,7 @@ class FlightPriceTaskRunner:
|
|
|
def _process_one_task(row, runner):
|
|
|
"""处理单条任务:构建 end_task、执行 run、解析结果。成功返回 flight_data 字典,失败返回 None。"""
|
|
|
task = row
|
|
|
-
|
|
|
+ separator = '|' # 分隔符由;更换为|
|
|
|
|
|
|
thread_name = threading.current_thread().name
|
|
|
# print(f"[thread_name: {thread_name}] 正在处理任务: {task}")
|
|
|
@@ -438,9 +478,15 @@ def _process_one_task(row, runner):
|
|
|
|
|
|
flight_numbers = task["flight_number_1"].strip()
|
|
|
if task["flight_number_2"].strip() != "VJ":
|
|
|
- flight_numbers += ";" + task["flight_number_2"].strip()
|
|
|
- cabins = ";".join(["Y"] * len(flight_numbers.split(";")))
|
|
|
- baggages = ";".join([f"1-{task['baggage']}"] * len(flight_numbers.split(";")))
|
|
|
+ flight_numbers += separator + task["flight_number_2"].strip()
|
|
|
+ cabins = separator.join(["Y"] * len(flight_numbers.split(separator)))
|
|
|
+
|
|
|
+ if str(task['baggage']) == '0':
|
|
|
+ baggage_str = "-;-;-;-"
|
|
|
+ else:
|
|
|
+ baggage_str = f"1-{task['baggage']}"
|
|
|
+
|
|
|
+ baggages = separator.join([baggage_str] * len(flight_numbers.split(separator)))
|
|
|
|
|
|
end_task = {
|
|
|
"from_city_code": from_city_code,
|
|
|
@@ -456,7 +502,8 @@ def _process_one_task(row, runner):
|
|
|
# print(end_task)
|
|
|
# print("--------------------------------")
|
|
|
|
|
|
- out = runner.run(end_task)
|
|
|
+ time.sleep(1)
|
|
|
+ out = runner.run(end_task, do_verify=False) # 不验价,仅询价
|
|
|
# print(json.dumps(out, ensure_ascii=False, indent=2))
|
|
|
if out.get("status") != "ok":
|
|
|
# print(f"[thread_name={thread_name}] 错误: {out.get('msg')}")
|
|
|
@@ -464,15 +511,35 @@ def _process_one_task(row, runner):
|
|
|
|
|
|
# print(f"价格: {out.get('price_info').get('adult_total_price')}")
|
|
|
raw_verify = out.get("raw_verify")
|
|
|
- results = raw_verify.get("result")
|
|
|
+ if raw_verify:
|
|
|
+ results = raw_verify.get("result") or []
|
|
|
+ else:
|
|
|
+ matched = out.get("matched") or {}
|
|
|
+ results = [matched] if matched else []
|
|
|
if not results:
|
|
|
return None
|
|
|
|
|
|
+ print("raw_verify pass")
|
|
|
+
|
|
|
+ # task 存放了 keep_info 的全部字段
|
|
|
+ drop_price_change_upper = float(task.get("drop_price_change_upper")) # 降价的最小幅度
|
|
|
+ drop_price_change_lower = float(task.get("drop_price_change_lower"))
|
|
|
+
|
|
|
+ max_threshold = round(drop_price_change_upper * runner.rate * 0.5) # 降价阈值要按汇率转人民币(四舍五入到整数)
|
|
|
+
|
|
|
result = results[0]
|
|
|
- segments = result.get("segments")
|
|
|
+ # adult_price = result.get("adult_price")
|
|
|
+ # adult_tax = result.get("adult_tax")
|
|
|
+ # adult_total_price = result.get("adult_total_price")
|
|
|
+ segments = result.get("segments") or []
|
|
|
+ if not segments:
|
|
|
+ return None
|
|
|
end_segments = []
|
|
|
baggage = segments[0].get("baggage")
|
|
|
- pc, kg = [int(i) for i in baggage.split("-")]
|
|
|
+ if baggage == "-;-;-;-":
|
|
|
+ pc, kg = 0, 0 # 无行李的设置?
|
|
|
+ else:
|
|
|
+ pc, kg = [int(i) for i in baggage.split("-")]
|
|
|
for seg in segments:
|
|
|
flight_number = seg.get("flight_number")
|
|
|
operating_flight_number = seg.get("operating_flight_number")
|
|
|
@@ -486,24 +553,27 @@ def _process_one_task(row, runner):
|
|
|
|
|
|
end_segment = {
|
|
|
"carrier": seg.get("carrier"),
|
|
|
- "dep_air_port": seg.get("dep_air_port"),
|
|
|
- "arr_air_port": seg.get("arr_air_port"),
|
|
|
+ "flight_number": flight_number,
|
|
|
+ # "dep_air_port": seg.get("dep_air_port"),
|
|
|
+ # "arr_air_port": seg.get("arr_air_port"),
|
|
|
"dep_city_code": seg.get("dep_city_code"),
|
|
|
"arr_city_code": seg.get("arr_city_code"),
|
|
|
- "flight_number": flight_number,
|
|
|
- "operating_flight_number": operating_flight_number,
|
|
|
+ # "operating_flight_number": operating_flight_number,
|
|
|
"cabin": seg.get("cabin"),
|
|
|
"dep_time": dep_time,
|
|
|
- "arr_time": arr_time,
|
|
|
+ # "arr_time": arr_time,
|
|
|
}
|
|
|
end_segments.append(end_segment)
|
|
|
|
|
|
return {
|
|
|
"trip_type": 1,
|
|
|
- "segments": end_segments,
|
|
|
- "price_add": 0,
|
|
|
+ # "cover_price": adult_price,
|
|
|
+ # "cover_tax": adult_tax,
|
|
|
"bag_amount": pc,
|
|
|
"bag_weight": kg,
|
|
|
+ "max_threshold": max_threshold,
|
|
|
+ "segments": end_segments,
|
|
|
+ "ret_segments": [],
|
|
|
"task": task
|
|
|
}
|
|
|
|
|
|
@@ -553,9 +623,12 @@ def main():
|
|
|
|
|
|
policy_list = []
|
|
|
keep_info_end = []
|
|
|
- max_workers = 3 # 并发线程数,可按需要调整
|
|
|
+ max_workers = 5 # 并发线程数,可按需要调整
|
|
|
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
|
|
futures = {executor.submit(_process_one_task, task, runner): task for task in task_list}
|
|
|
+ total = len(futures)
|
|
|
+ done = 0
|
|
|
+ failed = 0
|
|
|
for future in as_completed(futures):
|
|
|
try:
|
|
|
flight_data = future.result()
|
|
|
@@ -564,12 +637,20 @@ def main():
|
|
|
keep_info_end.append(task)
|
|
|
policy_list.append(flight_data)
|
|
|
except Exception as e:
|
|
|
+ failed += 1
|
|
|
task = futures[future]
|
|
|
- print(f"任务异常 {task}: {e}")
|
|
|
+ # print(f"任务异常 {task}: {e}")
|
|
|
+ logger.error(f"任务异常 {task}: {e}")
|
|
|
+ finally:
|
|
|
+ done += 1
|
|
|
+ logger.info(
|
|
|
+ f"进度: {done}/{total}, policy: {len(policy_list)}, keep: {len(keep_info_end)}, failed: {failed}"
|
|
|
+ )
|
|
|
|
|
|
# 3 批量一次性上传政策
|
|
|
logger.info(f"数据过滤后, 上传政策: {len(policy_list)}")
|
|
|
- logger.info(f"policy_list: {policy_list}")
|
|
|
+ # logger.info(f"policy_list: {policy_list}")
|
|
|
+ logger.info(f"policy_list: {json.dumps(policy_list, ensure_ascii=False, default=str)[:1000]}")
|
|
|
if len(policy_list) > 0:
|
|
|
# 这里批量一次性上传政策
|
|
|
payload = {"items": policy_list}
|