2026-05-22 10:42:31 +08:00
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
|
|
import re
|
|
|
|
|
|
from decimal import Decimal
|
|
|
|
|
|
|
2026-05-22 23:47:28 +08:00
|
|
|
|
from app.services.expense_type_keywords import iter_expense_keywords
|
|
|
|
|
|
|
2026-05-22 10:42:31 +08:00
|
|
|
|
EXPENSE_TYPE_LABELS = {
|
|
|
|
|
|
"travel": "差旅",
|
|
|
|
|
|
"train_ticket": "火车票",
|
|
|
|
|
|
"flight_ticket": "机票",
|
|
|
|
|
|
"hotel_ticket": "住宿票",
|
|
|
|
|
|
"ride_ticket": "乘车",
|
|
|
|
|
|
"travel_allowance": "出差补贴",
|
|
|
|
|
|
"hotel": "住宿",
|
|
|
|
|
|
"transport": "交通",
|
2026-05-22 23:47:28 +08:00
|
|
|
|
"meal": "业务招待",
|
2026-05-22 10:42:31 +08:00
|
|
|
|
"meeting": "会务",
|
|
|
|
|
|
"entertainment": "招待",
|
2026-05-26 12:16:20 +08:00
|
|
|
|
"marketing": "市场推广",
|
2026-05-22 23:47:28 +08:00
|
|
|
|
"office": "办公用品",
|
2026-05-22 10:42:31 +08:00
|
|
|
|
"training": "培训",
|
2026-05-26 12:16:20 +08:00
|
|
|
|
"software": "软件服务",
|
2026-05-22 10:42:31 +08:00
|
|
|
|
"communication": "通讯",
|
|
|
|
|
|
"welfare": "福利",
|
|
|
|
|
|
}
|
|
|
|
|
|
MAX_DRAFT_CLAIMS_PER_USER = 3
|
|
|
|
|
|
EDITABLE_CLAIM_STATUSES = ("draft", "supplement", "returned")
|
|
|
|
|
|
SYSTEM_GENERATED_ITEM_TYPES = {"travel_allowance"}
|
|
|
|
|
|
TRAVEL_DETAIL_ITEM_TYPES = {
|
|
|
|
|
|
"train_ticket",
|
|
|
|
|
|
"flight_ticket",
|
|
|
|
|
|
"hotel_ticket",
|
|
|
|
|
|
"ride_ticket",
|
|
|
|
|
|
"travel_allowance",
|
|
|
|
|
|
}
|
|
|
|
|
|
TRAVEL_ALLOWANCE_TRIGGER_ITEM_TYPES = {"train_ticket", "flight_ticket"}
|
|
|
|
|
|
DOCUMENT_TYPE_ITEM_TYPE_MAP = {
|
|
|
|
|
|
"train_ticket": "train_ticket",
|
|
|
|
|
|
"flight_itinerary": "flight_ticket",
|
|
|
|
|
|
"hotel_invoice": "hotel_ticket",
|
|
|
|
|
|
"taxi_receipt": "ride_ticket",
|
|
|
|
|
|
"transport_receipt": "ride_ticket",
|
|
|
|
|
|
}
|
|
|
|
|
|
DOCUMENT_TYPE_SCENE_MAP = {
|
|
|
|
|
|
"train_ticket": "travel",
|
|
|
|
|
|
"flight_itinerary": "travel",
|
|
|
|
|
|
"hotel_invoice": "hotel",
|
|
|
|
|
|
"taxi_receipt": "transport",
|
|
|
|
|
|
"transport_receipt": "transport",
|
|
|
|
|
|
"parking_toll_receipt": "transport",
|
|
|
|
|
|
"meal_receipt": "meal",
|
|
|
|
|
|
"office_invoice": "office",
|
|
|
|
|
|
"meeting_invoice": "meeting",
|
|
|
|
|
|
"training_invoice": "training",
|
|
|
|
|
|
}
|
2026-05-26 12:16:20 +08:00
|
|
|
|
DOCUMENT_FACT_ITEM_TYPES = {
|
|
|
|
|
|
"train_ticket",
|
|
|
|
|
|
"flight_ticket",
|
|
|
|
|
|
"hotel_ticket",
|
|
|
|
|
|
"ride_ticket",
|
|
|
|
|
|
"ship_ticket",
|
|
|
|
|
|
"ferry_ticket",
|
|
|
|
|
|
}
|
|
|
|
|
|
ROUTE_DESCRIPTION_ITEM_TYPES = {
|
|
|
|
|
|
"train_ticket",
|
|
|
|
|
|
"flight_ticket",
|
|
|
|
|
|
"ship_ticket",
|
|
|
|
|
|
"ferry_ticket",
|
|
|
|
|
|
"ride_ticket",
|
|
|
|
|
|
}
|
2026-05-22 10:42:31 +08:00
|
|
|
|
DOCUMENT_TRIP_DATE_LABELS = {
|
|
|
|
|
|
"train_ticket": "列车出发时间",
|
|
|
|
|
|
"flight_itinerary": "起飞日期",
|
|
|
|
|
|
"taxi_receipt": "乘车时间",
|
|
|
|
|
|
"transport_receipt": "乘车时间",
|
|
|
|
|
|
"parking_toll_receipt": "通行日期",
|
|
|
|
|
|
}
|
|
|
|
|
|
DOCUMENT_TRIP_DATE_REQUIREMENT_LABELS = {
|
|
|
|
|
|
"train_ticket": "列车出发时间或乘车日期",
|
|
|
|
|
|
"flight_itinerary": "起飞日期或航班日期",
|
|
|
|
|
|
"taxi_receipt": "乘车时间",
|
|
|
|
|
|
"transport_receipt": "乘车时间",
|
|
|
|
|
|
"parking_toll_receipt": "通行日期",
|
|
|
|
|
|
"hotel_invoice": "入住或离店日期",
|
|
|
|
|
|
}
|
|
|
|
|
|
DOCUMENT_TRIP_DATE_KEYS = {
|
|
|
|
|
|
"traveldate",
|
|
|
|
|
|
"tripdate",
|
|
|
|
|
|
"journeydate",
|
|
|
|
|
|
"departuredate",
|
|
|
|
|
|
"departuretime",
|
|
|
|
|
|
"departdate",
|
|
|
|
|
|
"departtime",
|
|
|
|
|
|
"boardingdate",
|
|
|
|
|
|
"boardingtime",
|
|
|
|
|
|
"traindate",
|
|
|
|
|
|
"traintime",
|
|
|
|
|
|
"traindeparturetime",
|
|
|
|
|
|
"scheduleddeparturetime",
|
|
|
|
|
|
"flightdate",
|
|
|
|
|
|
"flighttime",
|
|
|
|
|
|
"ridedate",
|
|
|
|
|
|
"ridetime",
|
|
|
|
|
|
"pickuptime",
|
|
|
|
|
|
"starttime",
|
|
|
|
|
|
}
|
|
|
|
|
|
DOCUMENT_GENERIC_DATE_KEYS = {"date", "time", "occurredat", "occurreddate", "businessdate"}
|
|
|
|
|
|
DOCUMENT_INVOICE_DATE_KEYS = {"issuedat", "issuedate", "invoicedate", "billingdate"}
|
|
|
|
|
|
DOCUMENT_TRIP_DATE_LABEL_TOKENS = (
|
|
|
|
|
|
"出发日期",
|
|
|
|
|
|
"出发时间",
|
|
|
|
|
|
"列车出发时间",
|
|
|
|
|
|
"发车日期",
|
|
|
|
|
|
"发车时间",
|
|
|
|
|
|
"开车时间",
|
|
|
|
|
|
"乘车日期",
|
|
|
|
|
|
"乘车时间",
|
|
|
|
|
|
"起飞日期",
|
|
|
|
|
|
"航班日期",
|
|
|
|
|
|
"行程日期",
|
|
|
|
|
|
"上车时间",
|
|
|
|
|
|
"用车时间",
|
|
|
|
|
|
"通行日期",
|
|
|
|
|
|
)
|
|
|
|
|
|
DOCUMENT_GENERIC_DATE_LABEL_TOKENS = ("日期", "时间", "发生时间", "业务发生日期")
|
|
|
|
|
|
DOCUMENT_INVOICE_DATE_LABEL_TOKENS = ("开票日期", "发票日期")
|
|
|
|
|
|
DOCUMENT_ROUTE_FORMAT_PATTERN = re.compile(
|
|
|
|
|
|
r"^[A-Za-z0-9\u4e00-\u9fa5()()·]{2,40}\s*-\s*"
|
|
|
|
|
|
r"[A-Za-z0-9\u4e00-\u9fa5()()·]{2,40}$"
|
|
|
|
|
|
)
|
|
|
|
|
|
DOCUMENT_ROUTE_TEXT_PATTERN = re.compile(
|
|
|
|
|
|
r"([A-Za-z0-9\u4e00-\u9fa5()()·]{2,40})\s*(?:至|到|→|->|—|–|-)\s*"
|
|
|
|
|
|
r"([A-Za-z0-9\u4e00-\u9fa5()()·]{2,40})"
|
|
|
|
|
|
)
|
2026-05-26 12:16:20 +08:00
|
|
|
|
DOCUMENT_ROUTE_ORIGIN_LABELS = {
|
|
|
|
|
|
"起点",
|
|
|
|
|
|
"上车",
|
|
|
|
|
|
"上车地点",
|
|
|
|
|
|
"上车地址",
|
|
|
|
|
|
"出发",
|
|
|
|
|
|
"出发地",
|
|
|
|
|
|
"出发站",
|
|
|
|
|
|
"始发站",
|
|
|
|
|
|
"乘车起点",
|
|
|
|
|
|
}
|
2026-05-22 10:42:31 +08:00
|
|
|
|
DOCUMENT_ROUTE_DESTINATION_LABELS = {
|
|
|
|
|
|
"终点",
|
|
|
|
|
|
"下车",
|
|
|
|
|
|
"下车地点",
|
|
|
|
|
|
"下车地址",
|
|
|
|
|
|
"到达",
|
|
|
|
|
|
"到达地",
|
|
|
|
|
|
"到达站",
|
|
|
|
|
|
"目的地",
|
|
|
|
|
|
"乘车终点",
|
|
|
|
|
|
}
|
|
|
|
|
|
GENERIC_ATTACHMENT_BACKFILL_ITEM_TYPES = {"", "other", "travel", "transport", "hotel"}
|
|
|
|
|
|
LOCATION_REQUIRED_EXPENSE_TYPES = {"travel", "meeting", "entertainment"}
|
|
|
|
|
|
EXPENSE_SCENE_KEYWORDS = {
|
2026-05-22 23:47:28 +08:00
|
|
|
|
code: tuple(iter_expense_keywords(code))
|
|
|
|
|
|
for code in (
|
|
|
|
|
|
"travel",
|
|
|
|
|
|
"hotel",
|
|
|
|
|
|
"transport",
|
|
|
|
|
|
"meal",
|
|
|
|
|
|
"entertainment",
|
2026-05-26 12:16:20 +08:00
|
|
|
|
"marketing",
|
2026-05-22 23:47:28 +08:00
|
|
|
|
"office",
|
|
|
|
|
|
"meeting",
|
|
|
|
|
|
"training",
|
2026-05-26 12:16:20 +08:00
|
|
|
|
"software",
|
2026-05-22 23:47:28 +08:00
|
|
|
|
"communication",
|
|
|
|
|
|
"welfare",
|
|
|
|
|
|
)
|
2026-05-22 10:42:31 +08:00
|
|
|
|
}
|
|
|
|
|
|
EXPENSE_TYPE_ALLOWED_DOCUMENT_SCENES = {
|
|
|
|
|
|
"travel": {"travel", "hotel", "transport", "meal"},
|
|
|
|
|
|
"train_ticket": {"travel"},
|
|
|
|
|
|
"flight_ticket": {"travel"},
|
|
|
|
|
|
"hotel_ticket": {"hotel"},
|
|
|
|
|
|
"ride_ticket": {"transport"},
|
|
|
|
|
|
"travel_allowance": set(),
|
|
|
|
|
|
"hotel": {"hotel"},
|
|
|
|
|
|
"transport": {"transport", "travel"},
|
|
|
|
|
|
"meal": {"meal", "entertainment"},
|
|
|
|
|
|
"entertainment": {"entertainment", "meal"},
|
2026-05-26 12:16:20 +08:00
|
|
|
|
"marketing": {"marketing"},
|
2026-05-22 10:42:31 +08:00
|
|
|
|
"office": {"office"},
|
|
|
|
|
|
"meeting": {"meeting"},
|
|
|
|
|
|
"training": {"training"},
|
2026-05-26 12:16:20 +08:00
|
|
|
|
"software": {"software"},
|
2026-05-22 10:42:31 +08:00
|
|
|
|
}
|
|
|
|
|
|
DOCUMENT_SCENE_LABELS = {
|
|
|
|
|
|
"travel": "差旅",
|
|
|
|
|
|
"hotel": "住宿",
|
|
|
|
|
|
"transport": "交通",
|
2026-05-22 23:47:28 +08:00
|
|
|
|
"meal": "业务招待",
|
2026-05-22 10:42:31 +08:00
|
|
|
|
"entertainment": "业务招待",
|
2026-05-26 12:16:20 +08:00
|
|
|
|
"marketing": "市场推广",
|
2026-05-22 10:42:31 +08:00
|
|
|
|
"office": "办公用品",
|
|
|
|
|
|
"meeting": "会务",
|
|
|
|
|
|
"training": "培训",
|
2026-05-26 12:16:20 +08:00
|
|
|
|
"software": "软件服务",
|
2026-05-22 10:42:31 +08:00
|
|
|
|
"other": "其他票据",
|
|
|
|
|
|
}
|
|
|
|
|
|
DOCUMENT_ASSOCIATION_REVIEW_ACTIONS = {
|
|
|
|
|
|
"link_to_existing_draft",
|
|
|
|
|
|
"create_new_claim_from_documents",
|
|
|
|
|
|
}
|
|
|
|
|
|
PERSISTENT_EXPENSE_REVIEW_ACTIONS = {
|
|
|
|
|
|
"save_draft",
|
|
|
|
|
|
"next_step",
|
|
|
|
|
|
*DOCUMENT_ASSOCIATION_REVIEW_ACTIONS,
|
|
|
|
|
|
}
|
|
|
|
|
|
RETURN_REASON_OPTIONS = {
|
|
|
|
|
|
"missing_attachment": "附件缺失或不清晰",
|
|
|
|
|
|
"invoice_mismatch": "票据类型/金额与明细不一致",
|
|
|
|
|
|
"over_policy": "超出制度标准或缺少超标说明",
|
|
|
|
|
|
"business_explanation": "业务事由/地点/人员信息不完整",
|
|
|
|
|
|
"duplicate_or_abnormal": "疑似重复或异常票据",
|
|
|
|
|
|
"approval_question": "审批人需要补充说明",
|
|
|
|
|
|
}
|
|
|
|
|
|
MAX_CLAIM_NO_RETRY_ATTEMPTS = 3
|
2026-05-26 12:16:20 +08:00
|
|
|
|
DOCUMENT_DATE_PATTERN = re.compile(
|
|
|
|
|
|
r"((?:20\d{2}|19\d{2})[-/年.](?:1[0-2]|0?[1-9])[-/月.]"
|
|
|
|
|
|
r"(?:3[01]|[12]\d|0?[1-9])日?)"
|
|
|
|
|
|
)
|
2026-05-22 10:42:31 +08:00
|
|
|
|
SYSTEM_GENERATED_REASON_PREFIXES = (
|
|
|
|
|
|
"我上传了",
|
|
|
|
|
|
"请按当前已识别信息",
|
|
|
|
|
|
"请把当前上传的票据",
|
|
|
|
|
|
"请基于当前上传的多张票据",
|
|
|
|
|
|
"我已核对右侧识别结果",
|
|
|
|
|
|
"请同步修正逐票据识别结果",
|
|
|
|
|
|
"我已修改识别信息",
|
|
|
|
|
|
"查看报销草稿",
|
|
|
|
|
|
"请解释一下当前这笔报销的合规风险和待补充项",
|
|
|
|
|
|
)
|
|
|
|
|
|
LEADING_REASON_TIME_PATTERNS = (
|
|
|
|
|
|
re.compile(
|
|
|
|
|
|
r"^\s*(?:识别事项(?:有)?[::]\s*)?"
|
|
|
|
|
|
r"(?:业务发生(?:时间|日期)|费用发生(?:时间|日期)|发生(?:时间|日期)|报销(?:时间|日期)|时间)[::]?\s*"
|
|
|
|
|
|
r"(?:19|20)\d{2}[-/年.]\d{1,2}[-/月.]\d{1,2}日?"
|
|
|
|
|
|
r"(?:\s*(?:至|到|~|~|—|-)\s*(?:19|20)\d{2}[-/年.]\d{1,2}[-/月.]\d{1,2}日?)?"
|
|
|
|
|
|
r"\s*[,,。;;、]?\s*"
|
|
|
|
|
|
),
|
|
|
|
|
|
re.compile(
|
|
|
|
|
|
r"^\s*(?:19|20)\d{2}[-/年.]\d{1,2}[-/月.]\d{1,2}日?"
|
|
|
|
|
|
r"(?:\s*(?:至|到|~|~|—|-)\s*(?:19|20)\d{2}[-/年.]\d{1,2}[-/月.]\d{1,2}日?)?"
|
|
|
|
|
|
r"\s*[,,。;;、]\s*"
|
|
|
|
|
|
),
|
|
|
|
|
|
)
|
|
|
|
|
|
AI_REVIEW_LOOKBACK_DAYS = 90
|
|
|
|
|
|
AI_REVIEW_REPEAT_RISK_WARNING_COUNT = 1
|
|
|
|
|
|
AI_REVIEW_REPEAT_RISK_BLOCK_COUNT = 2
|
|
|
|
|
|
TRAVEL_REVIEW_RELEVANT_EXPENSE_TYPES = {"travel", "hotel", "transport"}
|
|
|
|
|
|
TRAVEL_REVIEW_LONG_DISTANCE_DOCUMENT_TYPES = {"flight_itinerary", "train_ticket"}
|
|
|
|
|
|
TRAVEL_POLICY_CITY_TIERS = {
|
|
|
|
|
|
"北京": "tier_1",
|
|
|
|
|
|
"上海": "tier_1",
|
|
|
|
|
|
"广州": "tier_1",
|
|
|
|
|
|
"深圳": "tier_1",
|
|
|
|
|
|
"杭州": "tier_2",
|
|
|
|
|
|
"南京": "tier_2",
|
|
|
|
|
|
"苏州": "tier_2",
|
|
|
|
|
|
"武汉": "tier_2",
|
|
|
|
|
|
"成都": "tier_2",
|
|
|
|
|
|
"重庆": "tier_2",
|
|
|
|
|
|
"西安": "tier_2",
|
|
|
|
|
|
"天津": "tier_2",
|
|
|
|
|
|
"宁波": "tier_2",
|
|
|
|
|
|
"厦门": "tier_2",
|
|
|
|
|
|
"青岛": "tier_2",
|
|
|
|
|
|
"长沙": "tier_2",
|
|
|
|
|
|
"郑州": "tier_2",
|
|
|
|
|
|
"合肥": "tier_2",
|
|
|
|
|
|
"济南": "tier_2",
|
|
|
|
|
|
"沈阳": "tier_2",
|
|
|
|
|
|
"大连": "tier_2",
|
|
|
|
|
|
"福州": "tier_2",
|
|
|
|
|
|
"昆明": "tier_2",
|
|
|
|
|
|
"海口": "tier_2",
|
|
|
|
|
|
"三亚": "tier_2",
|
|
|
|
|
|
"无锡": "tier_2",
|
|
|
|
|
|
"东莞": "tier_2",
|
|
|
|
|
|
"佛山": "tier_2",
|
|
|
|
|
|
}
|
|
|
|
|
|
TRAVEL_POLICY_CITY_MATCH_ORDER = tuple(
|
|
|
|
|
|
sorted(TRAVEL_POLICY_CITY_TIERS.keys(), key=lambda item: len(item), reverse=True)
|
|
|
|
|
|
)
|
|
|
|
|
|
TRAVEL_POLICY_BAND_LABELS = {
|
|
|
|
|
|
"junior": "P1-P3",
|
|
|
|
|
|
"mid": "P4-P5",
|
|
|
|
|
|
"senior": "P6-P7",
|
|
|
|
|
|
"manager": "M1-M2",
|
|
|
|
|
|
"executive": "M3及以上 / D序列",
|
|
|
|
|
|
}
|
|
|
|
|
|
TRAVEL_POLICY_HOTEL_LIMITS = {
|
|
|
|
|
|
"junior": {
|
|
|
|
|
|
"tier_1": Decimal("450.00"),
|
|
|
|
|
|
"tier_2": Decimal("380.00"),
|
|
|
|
|
|
"tier_3": Decimal("320.00"),
|
|
|
|
|
|
},
|
|
|
|
|
|
"mid": {
|
|
|
|
|
|
"tier_1": Decimal("550.00"),
|
|
|
|
|
|
"tier_2": Decimal("480.00"),
|
|
|
|
|
|
"tier_3": Decimal("380.00"),
|
|
|
|
|
|
},
|
|
|
|
|
|
"senior": {
|
|
|
|
|
|
"tier_1": Decimal("700.00"),
|
|
|
|
|
|
"tier_2": Decimal("620.00"),
|
|
|
|
|
|
"tier_3": Decimal("520.00"),
|
|
|
|
|
|
},
|
|
|
|
|
|
"manager": {
|
|
|
|
|
|
"tier_1": Decimal("900.00"),
|
|
|
|
|
|
"tier_2": Decimal("820.00"),
|
|
|
|
|
|
"tier_3": Decimal("720.00"),
|
|
|
|
|
|
},
|
|
|
|
|
|
"executive": {
|
|
|
|
|
|
"tier_1": Decimal("1200.00"),
|
|
|
|
|
|
"tier_2": Decimal("1000.00"),
|
|
|
|
|
|
"tier_3": Decimal("900.00"),
|
|
|
|
|
|
},
|
|
|
|
|
|
}
|
|
|
|
|
|
TRAVEL_POLICY_ALLOWED_TRANSPORT_LEVELS = {
|
|
|
|
|
|
"junior": {"flight": 1, "train": 1},
|
|
|
|
|
|
"mid": {"flight": 1, "train": 1},
|
|
|
|
|
|
"senior": {"flight": 2, "train": 2},
|
|
|
|
|
|
"manager": {"flight": 3, "train": 3},
|
|
|
|
|
|
"executive": {"flight": 4, "train": 3},
|
|
|
|
|
|
}
|
|
|
|
|
|
TRAVEL_POLICY_ROUTE_EXCEPTION_KEYWORDS = (
|
|
|
|
|
|
"中转",
|
|
|
|
|
|
"转机",
|
|
|
|
|
|
"经停",
|
|
|
|
|
|
"改签",
|
|
|
|
|
|
"多地出差",
|
|
|
|
|
|
"多城市",
|
|
|
|
|
|
"多站",
|
|
|
|
|
|
"异地返程",
|
|
|
|
|
|
"异地结束",
|
|
|
|
|
|
"临时变更",
|
|
|
|
|
|
"继续前往",
|
|
|
|
|
|
"第二站",
|
|
|
|
|
|
)
|
|
|
|
|
|
TRAVEL_POLICY_STANDARD_EXCEPTION_KEYWORDS = (
|
|
|
|
|
|
"超标说明",
|
|
|
|
|
|
"无直达",
|
|
|
|
|
|
"展会高峰",
|
|
|
|
|
|
"会议高峰",
|
|
|
|
|
|
"协议酒店满房",
|
|
|
|
|
|
"客户指定",
|
|
|
|
|
|
"临时改签",
|
|
|
|
|
|
"行程变更",
|
|
|
|
|
|
"红眼航班",
|
|
|
|
|
|
"晚到店",
|
|
|
|
|
|
)
|
|
|
|
|
|
TRAVEL_POLICY_FLIGHT_CLASS_PATTERNS = (
|
|
|
|
|
|
("头等舱", 4),
|
|
|
|
|
|
("公务舱", 3),
|
|
|
|
|
|
("商务舱", 3),
|
|
|
|
|
|
("超级经济舱", 2),
|
|
|
|
|
|
("高端经济舱", 2),
|
|
|
|
|
|
("明珠经济舱", 2),
|
|
|
|
|
|
("经济舱", 1),
|
|
|
|
|
|
)
|
|
|
|
|
|
TRAVEL_POLICY_TRAIN_CLASS_PATTERNS = (
|
|
|
|
|
|
("商务座", 3),
|
|
|
|
|
|
("一等座", 2),
|
|
|
|
|
|
("软卧", 2),
|
|
|
|
|
|
("二等座", 1),
|
|
|
|
|
|
("二等卧", 1),
|
|
|
|
|
|
("硬卧", 1),
|
|
|
|
|
|
)
|
|
|
|
|
|
TRAVEL_POLICY_HOTEL_NIGHT_PATTERN = re.compile(r"(\d+)\s*(?:晚|间夜)")
|