860 lines
29 KiB
Python
860 lines
29 KiB
Python
#!/usr/bin/env python3
|
||
import argparse
|
||
import datetime as dt
|
||
import json
|
||
import os
|
||
import pathlib
|
||
import re
|
||
import sys
|
||
from typing import Dict, List, Sequence, Tuple
|
||
|
||
import pytds
|
||
import pymysql
|
||
|
||
SQL_ENV_FILE = "/root/.openclaw/credentials/company-sql.env"
|
||
MYSQL_ENV_FILE = "/root/.openclaw/credentials/install-media-mysql.env"
|
||
DEFAULT_MAPPING_FILE = "/root/.openclaw/workspace/outputs/install-media-mysql-mapping.json"
|
||
|
||
|
||
def load_env_file(path: str) -> None:
|
||
p = pathlib.Path(path)
|
||
if not p.exists():
|
||
return
|
||
for raw in p.read_text(encoding="utf-8").splitlines():
|
||
line = raw.strip()
|
||
if not line or line.startswith("#") or "=" not in line:
|
||
continue
|
||
key, val = line.split("=", 1)
|
||
key = key.strip()
|
||
val = val.strip().strip('"').strip("'")
|
||
if key and key not in os.environ:
|
||
os.environ[key] = val
|
||
|
||
|
||
def sql_conn_params() -> Dict[str, object]:
|
||
load_env_file(SQL_ENV_FILE)
|
||
return {
|
||
"server": os.environ.get("COMPANY_DB_HOST", "218.93.191.16"),
|
||
"port": int(os.environ.get("COMPANY_DB_PORT", "1433")),
|
||
"database": os.environ.get("COMPANY_DB_NAME", "JSDataMiddlePlatform"),
|
||
"user": os.environ.get("COMPANY_DB_USER", "nanting"),
|
||
"password": os.environ.get("COMPANY_DB_PASSWORD", ""),
|
||
"login_timeout": 15,
|
||
"timeout": 120,
|
||
}
|
||
|
||
|
||
def mysql_conn_params() -> Dict[str, object]:
|
||
load_env_file(MYSQL_ENV_FILE)
|
||
return {
|
||
"host": os.environ.get("INSTALL_MEDIA_MYSQL_HOST", "218.93.191.20"),
|
||
"port": int(os.environ.get("INSTALL_MEDIA_MYSQL_PORT", "3306")),
|
||
"user": os.environ.get("INSTALL_MEDIA_MYSQL_USER", "read_only_jiansun"),
|
||
"password": os.environ.get("INSTALL_MEDIA_MYSQL_PASSWORD", ""),
|
||
"charset": "utf8mb4",
|
||
"connect_timeout": int(os.environ.get("INSTALL_MEDIA_MYSQL_CONNECT_TIMEOUT", "8")),
|
||
"read_timeout": int(os.environ.get("INSTALL_MEDIA_MYSQL_READ_TIMEOUT", "15")),
|
||
"write_timeout": int(os.environ.get("INSTALL_MEDIA_MYSQL_WRITE_TIMEOUT", "15")),
|
||
"autocommit": True,
|
||
}
|
||
|
||
|
||
def sql_escape(value: str) -> str:
|
||
return value.replace("'", "''")
|
||
|
||
|
||
def qi(identifier: str) -> str:
|
||
return "`" + identifier.replace("`", "``") + "`"
|
||
|
||
|
||
def fmt_value(value):
|
||
if isinstance(value, (dt.datetime, dt.date, dt.time)):
|
||
return value.isoformat(sep=" ")
|
||
return value
|
||
|
||
|
||
def print_rows(columns: Sequence[str], rows: Sequence[Dict[str, object]]) -> None:
|
||
print("COLUMNS:", ", ".join(columns))
|
||
if not rows:
|
||
print("ROWS: 0")
|
||
return
|
||
print(f"ROWS: {len(rows)}")
|
||
for row in rows:
|
||
obj = {c: fmt_value(row.get(c)) for c in columns}
|
||
print(json.dumps(obj, ensure_ascii=False, default=str))
|
||
|
||
|
||
def query_order_product_mappings(order_id: str, limit: int) -> List[Dict[str, object]]:
|
||
if not order_id:
|
||
return []
|
||
|
||
params = sql_conn_params()
|
||
if not params["password"]:
|
||
raise RuntimeError("COMPANY_DB_PASSWORD is empty")
|
||
|
||
oid = sql_escape(order_id.strip())
|
||
lim = max(1, min(limit, 2000))
|
||
|
||
sql = f"""
|
||
USE [JSDataMiddlePlatform];
|
||
SET NOCOUNT ON;
|
||
|
||
DECLARE @order_id nvarchar(120) = N'{oid}';
|
||
|
||
WITH order_map AS (
|
||
SELECT
|
||
NULLIF(a.undefined2, '') AS order_id,
|
||
NULLIF(a.undefined10, '') AS sku_id,
|
||
COALESCE(NULLIF(s.other_7, ''), NULLIF(a.undefined96, '')) AS product_no,
|
||
COALESCE(NULLIF(s.name, ''), NULLIF(a.undefined36, '')) AS sku_name,
|
||
COALESCE(NULLIF(a.undefined8, ''), NULLIF(e.shop_name, '')) AS shop_name,
|
||
COALESCE(
|
||
TRY_CONVERT(datetime2, NULLIF(a.undefined6, ''), 120),
|
||
TRY_CONVERT(datetime2, REPLACE(NULLIF(a.undefined6, ''), '/', '-'), 120),
|
||
TRY_CONVERT(datetime2, NULLIF(e.send_time, ''), 120),
|
||
TRY_CONVERT(datetime2, a.createdAt)
|
||
) AS send_time,
|
||
ROW_NUMBER() OVER (
|
||
PARTITION BY NULLIF(a.undefined2, ''), COALESCE(NULLIF(s.other_7, ''), NULLIF(a.undefined96, ''), NULLIF(a.undefined10, ''))
|
||
ORDER BY COALESCE(
|
||
TRY_CONVERT(datetime2, NULLIF(a.undefined6, ''), 120),
|
||
TRY_CONVERT(datetime2, REPLACE(NULLIF(a.undefined6, ''), '/', '-'), 120),
|
||
TRY_CONVERT(datetime2, NULLIF(e.send_time, ''), 120),
|
||
TRY_CONVERT(datetime2, a.createdAt)
|
||
) DESC,
|
||
a.createdAt DESC
|
||
) AS rn
|
||
FROM dbo.jst_goods_profit_kpi_items_afs AS a WITH (NOLOCK)
|
||
LEFT JOIN dbo.jst_sku AS s WITH (NOLOCK)
|
||
ON s.sku_id = NULLIF(a.undefined10, '')
|
||
LEFT JOIN dbo.jst_order_express_details AS e WITH (NOLOCK)
|
||
ON NULLIF(e.so_id, '') = NULLIF(a.undefined2, '')
|
||
WHERE (
|
||
NULLIF(a.undefined2, '') = @order_id
|
||
OR NULLIF(a.undefined3, '') = @order_id
|
||
OR NULLIF(a.undefined3, '') LIKE @order_id + '%'
|
||
)
|
||
AND ISNULL(NULLIF(a.undefined62, ''), '') NOT LIKE '%配件%'
|
||
AND ISNULL(NULLIF(a.undefined62, ''), '') NOT LIKE '%特殊单%'
|
||
)
|
||
SELECT TOP ({lim})
|
||
order_id,
|
||
sku_id,
|
||
product_no,
|
||
sku_name,
|
||
shop_name,
|
||
send_time
|
||
FROM order_map
|
||
WHERE rn = 1
|
||
AND ISNULL(NULLIF(order_id, ''), '') <> ''
|
||
AND ISNULL(NULLIF(product_no, ''), '') <> ''
|
||
ORDER BY send_time DESC, order_id ASC;
|
||
"""
|
||
|
||
with pytds.connect(**params) as conn:
|
||
cur = conn.cursor()
|
||
cur.execute(sql)
|
||
cols = [c[0] for c in cur.description]
|
||
data = [dict(zip(cols, row)) for row in cur.fetchall()]
|
||
cur.close()
|
||
return data
|
||
|
||
|
||
def _normalize_text(s: str) -> str:
|
||
return (s or "").strip().lower()
|
||
|
||
|
||
PRODUCT_COL_KEYS = [
|
||
"productno",
|
||
"product_no",
|
||
"goodsno",
|
||
"goods_no",
|
||
"itemno",
|
||
"item_no",
|
||
"stylecode",
|
||
"style_code",
|
||
"spu",
|
||
"sku",
|
||
"barcode",
|
||
"procode",
|
||
"pro_code",
|
||
"货号",
|
||
"商品编码",
|
||
"产品编码",
|
||
]
|
||
|
||
DRAWING_COL_KEYS = [
|
||
"instruction",
|
||
"drawing",
|
||
"manual",
|
||
"install",
|
||
"tuzhi",
|
||
"图纸",
|
||
"安装图",
|
||
"说明书",
|
||
]
|
||
|
||
VIDEO_COL_KEYS = [
|
||
"video",
|
||
"mp4",
|
||
"movie",
|
||
"教程",
|
||
"视频",
|
||
"安装视频",
|
||
]
|
||
|
||
PATH_COL_KEYS = [
|
||
"path",
|
||
"url",
|
||
"file",
|
||
"img",
|
||
"image",
|
||
"地址",
|
||
"链接",
|
||
"文件",
|
||
"路径",
|
||
]
|
||
|
||
SYSTEM_SCHEMAS = {"information_schema", "mysql", "performance_schema", "sys"}
|
||
|
||
|
||
def _contains_any(text: str, keys: Sequence[str]) -> bool:
|
||
low = _normalize_text(text)
|
||
return any(k in low for k in keys)
|
||
|
||
|
||
def discover_table_specs(
|
||
conn,
|
||
explicit_schema: str,
|
||
explicit_table: str,
|
||
max_tables: int,
|
||
) -> List[Dict[str, object]]:
|
||
where = ["TABLE_SCHEMA NOT IN ('information_schema','mysql','performance_schema','sys')"]
|
||
params: List[object] = []
|
||
if explicit_schema:
|
||
where.append("TABLE_SCHEMA = %s")
|
||
params.append(explicit_schema)
|
||
if explicit_table:
|
||
where.append("TABLE_NAME = %s")
|
||
params.append(explicit_table)
|
||
|
||
sql = f"""
|
||
SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME, DATA_TYPE, COALESCE(COLUMN_COMMENT, '')
|
||
FROM information_schema.COLUMNS
|
||
WHERE {' AND '.join(where)}
|
||
ORDER BY TABLE_SCHEMA, TABLE_NAME, ORDINAL_POSITION
|
||
"""
|
||
|
||
with conn.cursor() as cur:
|
||
cur.execute(sql, params)
|
||
rows = cur.fetchall()
|
||
|
||
grouped: Dict[Tuple[str, str], List[Tuple[str, str, str]]] = {}
|
||
for schema, table, col, dtype, comment in rows:
|
||
key = (schema, table)
|
||
grouped.setdefault(key, []).append((str(col), str(dtype), str(comment or "")))
|
||
|
||
specs: List[Dict[str, object]] = []
|
||
for (schema, table), cols in grouped.items():
|
||
product_cols: List[str] = []
|
||
drawing_cols: List[str] = []
|
||
video_cols: List[str] = []
|
||
generic_media_cols: List[str] = []
|
||
|
||
for col_name, _dtype, col_comment in cols:
|
||
text = f"{col_name} {col_comment}"
|
||
has_path = _contains_any(text, PATH_COL_KEYS)
|
||
has_drawing = _contains_any(text, DRAWING_COL_KEYS)
|
||
has_video = _contains_any(text, VIDEO_COL_KEYS)
|
||
has_product = _contains_any(text, PRODUCT_COL_KEYS)
|
||
|
||
if has_product:
|
||
product_cols.append(col_name)
|
||
|
||
if has_drawing and (has_path or has_drawing):
|
||
drawing_cols.append(col_name)
|
||
elif has_video and (has_path or has_video):
|
||
video_cols.append(col_name)
|
||
elif has_path and (has_drawing or has_video):
|
||
generic_media_cols.append(col_name)
|
||
elif has_path and _contains_any(text, ["install", "instruction", "图纸", "视频"]):
|
||
generic_media_cols.append(col_name)
|
||
|
||
media_cols = sorted(set(drawing_cols + video_cols + generic_media_cols))
|
||
if not product_cols or not media_cols:
|
||
continue
|
||
|
||
specs.append(
|
||
{
|
||
"schema": schema,
|
||
"table": table,
|
||
"product_cols": sorted(set(product_cols)),
|
||
"drawing_cols": sorted(set(drawing_cols)),
|
||
"video_cols": sorted(set(video_cols)),
|
||
"media_cols": media_cols,
|
||
}
|
||
)
|
||
|
||
specs.sort(key=lambda x: (x["schema"], x["table"]))
|
||
if max_tables > 0:
|
||
specs = specs[:max_tables]
|
||
return specs
|
||
|
||
|
||
def load_mapping_specs(mapping_file: str) -> List[Dict[str, object]]:
|
||
p = pathlib.Path(mapping_file)
|
||
if not p.exists():
|
||
return []
|
||
try:
|
||
obj = json.loads(p.read_text(encoding="utf-8"))
|
||
except Exception:
|
||
return []
|
||
|
||
rows = obj.get("tables") if isinstance(obj, dict) else None
|
||
if not isinstance(rows, list):
|
||
return []
|
||
|
||
specs: List[Dict[str, object]] = []
|
||
for row in rows:
|
||
if not isinstance(row, dict):
|
||
continue
|
||
schema = str(row.get("schema", "")).strip()
|
||
table = str(row.get("table", "")).strip()
|
||
product_cols = [str(x).strip() for x in row.get("product_cols", []) if str(x).strip()]
|
||
drawing_cols = [str(x).strip() for x in row.get("drawing_cols", []) if str(x).strip()]
|
||
video_cols = [str(x).strip() for x in row.get("video_cols", []) if str(x).strip()]
|
||
media_cols = [str(x).strip() for x in row.get("media_cols", []) if str(x).strip()]
|
||
if not schema or not table or not product_cols:
|
||
continue
|
||
merged_media = sorted(set(media_cols + drawing_cols + video_cols))
|
||
if not merged_media:
|
||
continue
|
||
specs.append(
|
||
{
|
||
"schema": schema,
|
||
"table": table,
|
||
"product_cols": sorted(set(product_cols)),
|
||
"drawing_cols": sorted(set(drawing_cols)),
|
||
"video_cols": sorted(set(video_cols)),
|
||
"media_cols": merged_media,
|
||
}
|
||
)
|
||
return specs
|
||
|
||
|
||
def classify_media_type(col_name: str, value: str) -> str:
|
||
text = _normalize_text(f"{col_name} {value}")
|
||
if _contains_any(text, VIDEO_COL_KEYS) or re.search(r"\.(mp4|avi|mov|mkv|wmv)(\?|$)", text):
|
||
return "video"
|
||
if _contains_any(text, DRAWING_COL_KEYS) or re.search(r"\.(png|jpg|jpeg|webp|gif|bmp)(\?|$)", text):
|
||
return "drawing"
|
||
return "unknown"
|
||
|
||
|
||
def normalize_media_url(raw_path: str, local_prefix: str, cdn_prefix: str) -> str:
|
||
s = (raw_path or "").strip()
|
||
if not s:
|
||
return ""
|
||
|
||
if s.startswith("http://") or s.startswith("https://"):
|
||
return s
|
||
|
||
s_norm = s.replace("\\", "/")
|
||
local_norm = (local_prefix or "D:/JianSunTuZhiFile").replace("\\", "/").rstrip("/")
|
||
cdn_norm = (cdn_prefix or "https://img.jiansuntuzhi.cn/").rstrip("/") + "/"
|
||
|
||
if s_norm.lower().startswith(local_norm.lower() + "/"):
|
||
rest = s_norm[len(local_norm) + 1 :]
|
||
return cdn_norm + rest
|
||
if s_norm.lower() == local_norm.lower():
|
||
return cdn_norm.rstrip("/")
|
||
|
||
marker = "jiansuntuzhifile/"
|
||
idx = s_norm.lower().find(marker)
|
||
if idx >= 0:
|
||
rest = s_norm[idx + len(marker) :]
|
||
return cdn_norm + rest
|
||
|
||
if re.match(r"^[a-zA-Z]:/", s_norm):
|
||
return s_norm
|
||
|
||
if s_norm.startswith("/"):
|
||
return cdn_norm + s_norm.lstrip("/")
|
||
|
||
return cdn_norm + s_norm
|
||
|
||
|
||
def _query_table_rows(conn, sql: str, params: Sequence[object]) -> List[Dict[str, object]]:
|
||
with conn.cursor() as cur:
|
||
cur.execute(sql, params)
|
||
cols = [d[0] for d in cur.description]
|
||
data = [dict(zip(cols, row)) for row in cur.fetchall()]
|
||
return data
|
||
|
||
|
||
def query_jiansun_special_records(
|
||
conn,
|
||
schema: str,
|
||
product_no: str,
|
||
row_limit: int,
|
||
local_prefix: str,
|
||
cdn_prefix: str,
|
||
) -> Tuple[List[Dict[str, object]], List[Dict[str, object]]]:
|
||
"""
|
||
Deterministic path for jiansunblueprint:
|
||
product_no -> sheet1 -> missiontask -> missiontask_instructions/missiontask_video
|
||
"""
|
||
table_rows: List[Dict[str, object]] = []
|
||
detail_rows: List[Dict[str, object]] = []
|
||
|
||
if not schema:
|
||
return table_rows, detail_rows
|
||
|
||
with conn.cursor() as cur:
|
||
cur.execute(f"SHOW TABLES FROM {qi(schema)}")
|
||
table_names = {str(r[0]).lower() for r in cur.fetchall()}
|
||
|
||
required = {"sheet1", "missiontask", "missiontask_instructions", "missiontask_video"}
|
||
if not required.issubset(table_names):
|
||
return table_rows, detail_rows
|
||
|
||
lim = max(1, min(row_limit, 2000))
|
||
pno = product_no.strip()
|
||
|
||
# 1) sheet1: map product_no -> barcode/pictureId/sku model
|
||
sheet_sql = (
|
||
f"SELECT {qi('sku(货号)')} AS product_no, {qi('工厂条形码')} AS barcode, "
|
||
f"{qi('图纸ID')} AS picture_id, {qi('SKU型号')} AS sku_model "
|
||
f"FROM {qi(schema)}.{qi('sheet1')} "
|
||
f"WHERE {qi('sku(货号)')} = %s OR {qi('sku(货号)')} LIKE %s "
|
||
f"LIMIT %s"
|
||
)
|
||
sheet_rows = _query_table_rows(conn, sheet_sql, (pno, f"{pno}%", lim))
|
||
table_rows.append(
|
||
{
|
||
"product_no": pno,
|
||
"source_schema": schema,
|
||
"source_table": "sheet1",
|
||
"matched_rows": len(sheet_rows),
|
||
"matched_media_items": 0,
|
||
}
|
||
)
|
||
|
||
barcodes = sorted({str(r.get("barcode", "")).strip() for r in sheet_rows if str(r.get("barcode", "")).strip()})
|
||
picture_ids = sorted({str(r.get("picture_id", "")).strip() for r in sheet_rows if str(r.get("picture_id", "")).strip()})
|
||
sku_models = sorted({str(r.get("sku_model", "")).strip() for r in sheet_rows if str(r.get("sku_model", "")).strip()})
|
||
|
||
# 2) missiontask: find task ids
|
||
match_parts: List[str] = []
|
||
params: List[object] = []
|
||
|
||
match_parts.append(f"{qi('skucodes')} LIKE %s")
|
||
params.append(f"%{pno}%")
|
||
|
||
for b in barcodes[:60]:
|
||
match_parts.append(f"{qi('skucodes')} LIKE %s")
|
||
params.append(f"%{b}%")
|
||
for pid in picture_ids[:60]:
|
||
match_parts.append(f"{qi('pictureId')} = %s")
|
||
params.append(pid)
|
||
for sm in sku_models[:40]:
|
||
match_parts.append(f"{qi('skuModel')} = %s")
|
||
params.append(sm)
|
||
|
||
where_clause = (
|
||
f"({qi('isDelete')} = '0' OR {qi('isDelete')} = 0 OR {qi('isDelete')} IS NULL)"
|
||
f" AND ({' OR '.join(match_parts)})"
|
||
)
|
||
|
||
mission_sql = (
|
||
f"SELECT {qi('id')} AS task_id, {qi('taskId')} AS task_no, {qi('SPU')} AS spu, "
|
||
f"{qi('picture')} AS picture_path, {qi('pictureId')} AS picture_id, {qi('skucodes')} AS skucodes, "
|
||
f"{qi('withInstructions')} AS with_instructions, {qi('withVideo')} AS with_video, "
|
||
f"{qi('createdTime')} AS created_time "
|
||
f"FROM {qi(schema)}.{qi('missiontask')} "
|
||
f"WHERE {where_clause} "
|
||
f"ORDER BY {qi('createdTime')} DESC "
|
||
f"LIMIT %s"
|
||
)
|
||
params.append(max(20, min(lim * 3, 500)))
|
||
mission_rows = _query_table_rows(conn, mission_sql, params)
|
||
table_rows.append(
|
||
{
|
||
"product_no": pno,
|
||
"source_schema": schema,
|
||
"source_table": "missiontask",
|
||
"matched_rows": len(mission_rows),
|
||
"matched_media_items": 0,
|
||
}
|
||
)
|
||
|
||
task_ids = [str(r.get("task_id", "")).strip() for r in mission_rows if str(r.get("task_id", "")).strip()]
|
||
if not task_ids:
|
||
return table_rows, detail_rows
|
||
|
||
# 3) instructions
|
||
placeholders = ",".join(["%s"] * len(task_ids))
|
||
ins_sql = (
|
||
f"SELECT {qi('id')} AS id, {qi('hid')} AS task_id, {qi('url')} AS raw_path, {qi('orderNum')} AS order_num "
|
||
f"FROM {qi(schema)}.{qi('missiontask_instructions')} "
|
||
f"WHERE {qi('hid')} IN ({placeholders}) AND {qi('isDelete')} = 0 "
|
||
f"ORDER BY {qi('hid')} ASC, {qi('orderNum')} ASC"
|
||
)
|
||
ins_rows = _query_table_rows(conn, ins_sql, task_ids)
|
||
for r in ins_rows:
|
||
raw_path = str(r.get("raw_path", "")).strip()
|
||
if not raw_path:
|
||
continue
|
||
detail_rows.append(
|
||
{
|
||
"product_no": pno,
|
||
"media_type": "drawing",
|
||
"media_url": normalize_media_url(raw_path, local_prefix, cdn_prefix),
|
||
"raw_path": raw_path,
|
||
"source_schema": schema,
|
||
"source_table": "missiontask_instructions",
|
||
"source_column": "url",
|
||
}
|
||
)
|
||
|
||
# 4) video
|
||
video_sql = (
|
||
f"SELECT {qi('id')} AS id, {qi('hid')} AS task_id, {qi('url')} AS raw_path, "
|
||
f"{qi('videoId')} AS video_id, {qi('orderNum')} AS order_num "
|
||
f"FROM {qi(schema)}.{qi('missiontask_video')} "
|
||
f"WHERE {qi('hid')} IN ({placeholders}) AND {qi('isDelete')} = 0 "
|
||
f"ORDER BY {qi('hid')} ASC, {qi('orderNum')} ASC"
|
||
)
|
||
video_rows = _query_table_rows(conn, video_sql, task_ids)
|
||
for r in video_rows:
|
||
raw_path = str(r.get("raw_path", "")).strip()
|
||
video_id = str(r.get("video_id", "")).strip()
|
||
media_url = ""
|
||
source_col = "url"
|
||
if raw_path:
|
||
media_url = normalize_media_url(raw_path, local_prefix, cdn_prefix)
|
||
elif video_id:
|
||
# youku id format: XNjUyMTkzNjkxMg==
|
||
media_url = f"https://v.youku.com/v_show/id_{video_id}.html"
|
||
raw_path = video_id
|
||
source_col = "videoId"
|
||
else:
|
||
continue
|
||
|
||
detail_rows.append(
|
||
{
|
||
"product_no": pno,
|
||
"media_type": "video",
|
||
"media_url": media_url,
|
||
"raw_path": raw_path,
|
||
"source_schema": schema,
|
||
"source_table": "missiontask_video",
|
||
"source_column": source_col,
|
||
}
|
||
)
|
||
|
||
table_rows.append(
|
||
{
|
||
"product_no": pno,
|
||
"source_schema": schema,
|
||
"source_table": "missiontask_instructions",
|
||
"matched_rows": len(ins_rows),
|
||
"matched_media_items": sum(1 for x in detail_rows if x.get("source_table") == "missiontask_instructions"),
|
||
}
|
||
)
|
||
table_rows.append(
|
||
{
|
||
"product_no": pno,
|
||
"source_schema": schema,
|
||
"source_table": "missiontask_video",
|
||
"matched_rows": len(video_rows),
|
||
"matched_media_items": sum(1 for x in detail_rows if x.get("source_table") == "missiontask_video"),
|
||
}
|
||
)
|
||
return table_rows, detail_rows
|
||
|
||
|
||
def query_media_records(
|
||
conn,
|
||
specs: Sequence[Dict[str, object]],
|
||
product_no: str,
|
||
row_limit: int,
|
||
local_prefix: str,
|
||
cdn_prefix: str,
|
||
) -> Tuple[List[Dict[str, object]], List[Dict[str, object]]]:
|
||
detail_rows: List[Dict[str, object]] = []
|
||
table_rows: List[Dict[str, object]] = []
|
||
|
||
for spec in specs:
|
||
schema = str(spec["schema"])
|
||
table = str(spec["table"])
|
||
product_cols = list(spec["product_cols"])
|
||
media_cols = list(spec["media_cols"])
|
||
drawing_cols = set(spec.get("drawing_cols", []))
|
||
video_cols = set(spec.get("video_cols", []))
|
||
|
||
select_cols = sorted(set(product_cols + media_cols))
|
||
where_parts: List[str] = []
|
||
params: List[object] = []
|
||
for col in product_cols:
|
||
qcol = qi(col)
|
||
where_parts.append(f"({qcol} = %s OR {qcol} LIKE %s)")
|
||
params.extend([product_no, f"{product_no}%"])
|
||
|
||
if not where_parts:
|
||
continue
|
||
|
||
sql = (
|
||
f"SELECT {', '.join(qi(c) for c in select_cols)} "
|
||
f"FROM {qi(schema)}.{qi(table)} "
|
||
f"WHERE {' OR '.join(where_parts)} "
|
||
f"LIMIT %s"
|
||
)
|
||
params.append(max(1, min(row_limit, 2000)))
|
||
|
||
with conn.cursor() as cur:
|
||
cur.execute(sql, params)
|
||
data = cur.fetchall()
|
||
|
||
if not data:
|
||
continue
|
||
|
||
col_index = {name: i for i, name in enumerate(select_cols)}
|
||
matched_media = 0
|
||
|
||
for row in data:
|
||
product_val = ""
|
||
for pcol in product_cols:
|
||
value = row[col_index[pcol]]
|
||
if value is not None and str(value).strip():
|
||
product_val = str(value).strip()
|
||
break
|
||
|
||
for mcol in media_cols:
|
||
value = row[col_index[mcol]]
|
||
if value is None:
|
||
continue
|
||
raw_path = str(value).strip()
|
||
if not raw_path:
|
||
continue
|
||
|
||
media_type = classify_media_type(mcol, raw_path)
|
||
if mcol in drawing_cols:
|
||
media_type = "drawing"
|
||
elif mcol in video_cols:
|
||
media_type = "video"
|
||
|
||
normalized = normalize_media_url(raw_path, local_prefix, cdn_prefix)
|
||
detail_rows.append(
|
||
{
|
||
"product_no": product_val or product_no,
|
||
"media_type": media_type,
|
||
"media_url": normalized,
|
||
"raw_path": raw_path,
|
||
"source_schema": schema,
|
||
"source_table": table,
|
||
"source_column": mcol,
|
||
}
|
||
)
|
||
matched_media += 1
|
||
|
||
table_rows.append(
|
||
{
|
||
"product_no": product_no,
|
||
"source_schema": schema,
|
||
"source_table": table,
|
||
"matched_rows": len(data),
|
||
"matched_media_items": matched_media,
|
||
}
|
||
)
|
||
|
||
return table_rows, detail_rows
|
||
|
||
|
||
def dedupe_media_rows(rows: Sequence[Dict[str, object]]) -> List[Dict[str, object]]:
|
||
seen = set()
|
||
out: List[Dict[str, object]] = []
|
||
for row in rows:
|
||
key = (
|
||
row.get("product_no", ""),
|
||
row.get("media_type", ""),
|
||
row.get("media_url", ""),
|
||
row.get("raw_path", ""),
|
||
row.get("source_schema", ""),
|
||
row.get("source_table", ""),
|
||
row.get("source_column", ""),
|
||
)
|
||
if key in seen:
|
||
continue
|
||
seen.add(key)
|
||
out.append(dict(row))
|
||
return out
|
||
|
||
|
||
def parse_args() -> argparse.Namespace:
|
||
parser = argparse.ArgumentParser(
|
||
description="Lookup install drawing/video links by order-id -> product-no -> MySQL media DB"
|
||
)
|
||
parser.add_argument("--order-id", default="", help="商品订单号")
|
||
parser.add_argument("--product-no", default="", help="产品货号(可跳过订单映射直接查)")
|
||
parser.add_argument("--sku-id", default="", help="SKU编码(等价于 --product-no,直接在安装库中匹配)")
|
||
parser.add_argument("--limit", type=int, default=200, help="每个来源表最多查询行数")
|
||
parser.add_argument("--max-tables", type=int, default=120, help="自动发现时最多探测表数")
|
||
parser.add_argument("--mysql-schema", default="", help="指定MySQL schema,减少扫描范围")
|
||
parser.add_argument("--mysql-table", default="", help="指定MySQL table,减少扫描范围")
|
||
parser.add_argument("--mapping-file", default=DEFAULT_MAPPING_FILE, help="固定映射配置JSON")
|
||
parser.add_argument("--local-prefix", default=os.environ.get("INSTALL_MEDIA_LOCAL_PREFIX", "D:\\JianSunTuZhiFile"), help="本地路径前缀")
|
||
parser.add_argument("--cdn-prefix", default=os.environ.get("INSTALL_MEDIA_CDN_PREFIX", "https://img.jiansuntuzhi.cn/"), help="图纸/视频CDN前缀")
|
||
return parser.parse_args()
|
||
|
||
|
||
def main() -> int:
|
||
args = parse_args()
|
||
order_id = args.order_id.strip()
|
||
direct_product = args.product_no.strip() or args.sku_id.strip()
|
||
|
||
if not order_id and not direct_product:
|
||
print("STATUS: failed")
|
||
print("ERROR: missing order-id or product-no")
|
||
return 2
|
||
|
||
warnings: List[str] = []
|
||
mapping_rows: List[Dict[str, object]] = []
|
||
|
||
if order_id:
|
||
try:
|
||
mapping_rows.extend(query_order_product_mappings(order_id, limit=300))
|
||
except Exception as exc:
|
||
warnings.append(f"SQLSERVER_ORDER_MAPPING_FAILED: {exc}")
|
||
|
||
if direct_product:
|
||
mapping_rows.append(
|
||
{
|
||
"order_id": order_id or "(direct)",
|
||
"sku_id": "",
|
||
"product_no": direct_product,
|
||
"sku_name": "",
|
||
"shop_name": "",
|
||
"send_time": None,
|
||
}
|
||
)
|
||
|
||
# dedupe mapping by (order_id, product_no, sku_id)
|
||
dedup = {}
|
||
for row in mapping_rows:
|
||
k = (str(row.get("order_id", "")).strip(), str(row.get("product_no", "")).strip(), str(row.get("sku_id", "")).strip())
|
||
if not k[1]:
|
||
continue
|
||
dedup[k] = row
|
||
mapping_rows = list(dedup.values())
|
||
|
||
product_nos = sorted({str(r.get("product_no", "")).strip() for r in mapping_rows if str(r.get("product_no", "")).strip()})
|
||
|
||
table_summary_rows: List[Dict[str, object]] = []
|
||
media_rows: List[Dict[str, object]] = []
|
||
|
||
if product_nos:
|
||
mysql_params = mysql_conn_params()
|
||
if not mysql_params["password"]:
|
||
warnings.append("MYSQL_PASSWORD_EMPTY: please set INSTALL_MEDIA_MYSQL_PASSWORD")
|
||
else:
|
||
try:
|
||
with pymysql.connect(**mysql_params) as conn:
|
||
schema_hint = args.mysql_schema.strip() or "jiansunblueprint"
|
||
source_mode = "special_jiansun"
|
||
discovered_count = 0
|
||
|
||
for pno in product_nos:
|
||
ts, ms = query_jiansun_special_records(
|
||
conn=conn,
|
||
schema=schema_hint,
|
||
product_no=pno,
|
||
row_limit=max(1, args.limit),
|
||
local_prefix=args.local_prefix,
|
||
cdn_prefix=args.cdn_prefix,
|
||
)
|
||
table_summary_rows.extend(ts)
|
||
media_rows.extend(ms)
|
||
|
||
if not ms:
|
||
specs = load_mapping_specs(args.mapping_file)
|
||
if specs:
|
||
source_mode = "mapping_file_fallback"
|
||
else:
|
||
specs = discover_table_specs(
|
||
conn,
|
||
args.mysql_schema.strip(),
|
||
args.mysql_table.strip(),
|
||
max(1, args.max_tables),
|
||
)
|
||
source_mode = "auto_discovery_fallback"
|
||
discovered_count = len(specs)
|
||
ts2, ms2 = query_media_records(
|
||
conn=conn,
|
||
specs=specs,
|
||
product_no=pno,
|
||
row_limit=max(1, args.limit),
|
||
local_prefix=args.local_prefix,
|
||
cdn_prefix=args.cdn_prefix,
|
||
)
|
||
table_summary_rows.extend(ts2)
|
||
media_rows.extend(ms2)
|
||
|
||
print(f"DISCOVERY_MODE: {source_mode}")
|
||
print(f"DISCOVERED_TABLES: {discovered_count}")
|
||
except Exception as exc:
|
||
warnings.append(f"MYSQL_LOOKUP_FAILED: {exc}")
|
||
else:
|
||
warnings.append("NO_PRODUCT_NO_FROM_ORDER: no product_no mapped from order-id")
|
||
|
||
media_rows = dedupe_media_rows(media_rows)
|
||
|
||
status = "ok"
|
||
if not media_rows and warnings:
|
||
status = "partial"
|
||
if not media_rows and not product_nos:
|
||
status = "failed"
|
||
|
||
print(f"STATUS: {status}")
|
||
print(f"ORDER_ID: {order_id or '(none)'}")
|
||
print(f"PRODUCT_NO_COUNT: {len(product_nos)}")
|
||
|
||
print("RESULT_SET 1")
|
||
print("NAME: order_to_product_mapping")
|
||
print_rows(
|
||
["order_id", "sku_id", "product_no", "sku_name", "shop_name", "send_time"],
|
||
mapping_rows,
|
||
)
|
||
|
||
print("RESULT_SET 2")
|
||
print("NAME: mysql_table_match_summary")
|
||
print_rows(
|
||
["product_no", "source_schema", "source_table", "matched_rows", "matched_media_items"],
|
||
table_summary_rows,
|
||
)
|
||
|
||
print("RESULT_SET 3")
|
||
print("NAME: install_media_links")
|
||
print_rows(
|
||
["product_no", "media_type", "media_url", "raw_path", "source_schema", "source_table", "source_column"],
|
||
media_rows,
|
||
)
|
||
|
||
if warnings:
|
||
print("WARNINGS:")
|
||
for w in warnings:
|
||
print(f"- {w}")
|
||
|
||
if not media_rows:
|
||
print("NEXT_STEP:")
|
||
if warnings:
|
||
print("- verify MySQL network reachability and fixed table mapping file")
|
||
else:
|
||
print("- matched sku/product but no install drawing/video rows in current tables")
|
||
|
||
return 0 if status in {"ok", "partial"} else 2
|
||
|
||
|
||
if __name__ == "__main__":
|
||
raise SystemExit(main())
|