完善报价单填表与币种回填,并补齐账号末次登录、角色按钮权限与陆运全线匹配。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
linmianxun
2026-09-19 22:03:45 +08:00
co-authored by Cursor
parent 1a2e1d6cb2
commit e4743541f7
25 changed files with 2262 additions and 163 deletions
+197 -6
View File
@@ -288,11 +288,14 @@ def assess_mapping_gaps(payload: dict[str, Any], mapping: dict[str, Any] | None)
def merge_mapping_prefer_existing(
old_mapping: dict[str, Any] | None,
new_mapping: dict[str, Any] | None,
*,
cell_inventory: list[str] | None = None,
) -> dict[str, Any]:
"""
补缺合并:旧 fields / 区域优先,新结果只补充旧里没有的 code/id。
重新解析合并:费用/线路区按 id 旧优先;fields 对照单元格清单智能取舍。
用于「未换文件重新解析」:避免已对上的字段被新一轮 AI 冲掉。
规则:旧无→用新;新无→留旧;相同→留旧;旧坐标仍贴标签→留旧;
旧坐标对不上(清单里能看到该标签)→改用新坐标。无清单时保守旧优先。
"""
new_mapping = dict(new_mapping or {})
old_mapping = old_mapping if isinstance(old_mapping, dict) else {}
@@ -300,11 +303,41 @@ def merge_mapping_prefer_existing(
new_fields = new_mapping.get("fields") if isinstance(new_mapping.get("fields"), dict) else {}
old_fields = old_mapping.get("fields") if isinstance(old_mapping.get("fields"), dict) else {}
fields: dict[str, Any] = dict(new_fields)
for code, cell in old_fields.items():
if cell is not None and str(cell).strip():
fields[str(code)] = cell
new_meta = new_mapping.get("fieldMeta") if isinstance(new_mapping.get("fieldMeta"), dict) else {}
old_meta = old_mapping.get("fieldMeta") if isinstance(old_mapping.get("fieldMeta"), dict) else {}
meta_out: dict[str, Any] = dict(new_meta)
codes = set(str(k) for k in old_fields.keys()) | set(str(k) for k in new_fields.keys())
fields: dict[str, Any] = {}
inv = list(cell_inventory or [])
for code in codes:
old_cell = str(old_fields.get(code) or "").strip()
new_cell = str(new_fields.get(code) or "").strip()
kept_old = False
if not old_cell:
chosen = new_cell
elif not new_cell:
chosen = old_cell
kept_old = True
elif old_cell.lower() == new_cell.lower():
chosen = old_cell
kept_old = True
elif inv and _field_mapping_still_aligned(code, old_cell, inv):
chosen = old_cell
kept_old = True
elif inv:
chosen = new_cell
logger.info("映射坐标纠偏 code=%s old=%s -> new=%s", code, old_cell, new_cell)
else:
chosen = old_cell
kept_old = True
if chosen:
fields[code] = chosen
if kept_old and isinstance(old_meta.get(code), dict):
meta_out[code] = dict(old_meta[code])
out["fields"] = fields
if meta_out:
out["fieldMeta"] = meta_out
out["feeRegions"] = _merge_regions_by_id(
old_mapping.get("feeRegions"), new_mapping.get("feeRegions")
@@ -315,6 +348,164 @@ def merge_mapping_prefer_existing(
return out
# 与主账 FILL_BY_LABEL 对齐的常用码→标签(智能合并校验用)
_FIELD_LABELS: dict[str, tuple[str, ...]] = {
"quote_no": ("RFQ No", "报价单编号", "报价编号"),
"quote_date": ("询价日期", "报价日期", "Inquiry Date"),
"pol": ("起运港", "POL"),
"pod": ("目的港", "POD"),
"commodity": ("品名", "货物品名", "Commodity"),
"quantity": ("货物数量", "货量"),
"fcl_or_lcl": ("整柜或拼柜",),
"container_type_qty": ("箱型箱量",),
"valid_until": ("报价有效期", "有效期至"),
"total_amount": ("费用总计", "报价合计"),
"origin": ("始发地", "始发站", "起运地"),
"destination": ("目的地",),
}
def _parse_inventory_line(line: str) -> tuple[str, str, str] | None:
"""地址 | 原文 | 合并 | 色 → (full_addr, text, merge)。"""
parts = [p.strip() for p in str(line or "").split("|")]
if len(parts) < 2:
return None
return parts[0], parts[1], parts[2] if len(parts) > 2 else "-"
def _col_index(letter: str) -> int:
n = 0
for ch in (letter or "").upper():
if "A" <= ch <= "Z":
n = n * 26 + (ord(ch) - ord("A") + 1)
return max(n - 1, 0)
def _split_addr(full: str) -> tuple[str, int, int] | None:
"""Sheet!B3 → (sheet, row0, col0)。"""
text = (full or "").strip()
if "!" not in text:
return None
sheet, addr = text.split("!", 1)
m = re.match(r"^\$?([A-Za-z]+)\$?(\d+)$", addr.strip())
if not m:
return None
return sheet, int(m.group(2)) - 1, _col_index(m.group(1))
def _merge_end_col(merge: str, fallback_col: int) -> int:
"""A3:C3 → 末列 index;无合并返回 fallback。"""
m = (merge or "").strip()
if not m or m == "-":
return fallback_col
# 可能是 A3:C3 或 Sheet!A3:C3
if "!" in m:
m = m.split("!", 1)[1]
if ":" not in m:
return fallback_col
end = m.split(":", 1)[1].strip()
mm = re.match(r"^\$?([A-Za-z]+)\$?(\d+)$", end)
if not mm:
return fallback_col
return _col_index(mm.group(1))
def _text_looks_like_label(text: str, labels: tuple[str, ...]) -> bool:
norm = (text or "").replace(":", ":").strip()
if not norm:
return False
primary = norm.split("/")[0].strip()
upper = norm.upper()
for lb in labels:
if not lb:
continue
if norm.lower().startswith(lb.lower()) or primary.lower().startswith(lb.lower()):
return True
if len(lb) >= 3 and lb.upper() in upper:
return True
return False
def _field_mapping_still_aligned(code: str, cell_ref: str, inventory: list[str]) -> bool:
"""
旧坐标是否仍可贴标签。
quote_no:贴着 RFQ 的 B3 常是 Port to Port / 工作天单选,不算对齐。
"""
labels = _FIELD_LABELS.get(str(code), ())
if not labels:
return True
parsed = _split_addr(cell_ref)
if not parsed:
return False
sheet, row, col = parsed
rows: list[tuple[str, int, int, str, str]] = []
mapped_text = ""
for line in inventory:
one = _parse_inventory_line(line)
if not one:
continue
addr, text, merge = one
sp = _split_addr(addr)
if not sp or sp[0] != sheet:
continue
rows.append((addr, sp[1], sp[2], text, merge))
if sp[1] == row and sp[2] == col:
mapped_text = text
if _text_looks_like_label(text, labels):
return True
if _is_unsuitable_value_cell(mapped_text):
return False
if str(code) == "quote_no" and mapped_text.strip() and not _looks_like_quote_no(mapped_text):
return False
saw = False
for _addr, r, c, text, merge in rows:
if not _text_looks_like_label(text, labels):
continue
saw = True
if r == row and c == col:
return True
end_col = _merge_end_col(merge, c)
if r == row:
gap = col - end_col
if 1 <= gap <= 3 and (not mapped_text.strip() or _looks_like_quote_no(mapped_text)):
return True
if str(code) == "quote_no" and gap > 3 and (
not mapped_text.strip() or _looks_like_quote_no(mapped_text)
):
return True
return not saw
def _is_unsuitable_value_cell(text: str) -> bool:
t = (text or "").replace(":", ":").strip()
if not t:
return False
lower = t.lower()
if any(x in t for x in ("○", "●", "⊙", "□", "☑")):
return True
if "port to port" in lower or "door to door" in lower:
return True
if "工作天" in t or "工作日" in t or "回复" in t:
return True
if "其它" in t and ("天" in t or "回复" in t or "Port" in t):
return True
return False
def _looks_like_quote_no(text: str) -> bool:
t = (text or "").strip()
if not t or len(t) > 40:
return False
import re as _re
if _re.match(r"(?i)WO\d{6,}", t) or _re.match(r"(?i)W0\d{6,}", t):
return True
if _re.match(r"(?i)RFQ[-_]?[A-Z0-9]+", t):
return True
return bool(_re.match(r"[A-Z]{1,5}[-_]?\d{8,}", t))
def _merge_regions_by_id(old_arr: Any, new_arr: Any) -> list[dict[str, Any]]:
"""区域按 id 合并:旧整段保留,新 id 追加。"""
out: list[dict[str, Any]] = []