Compare commits
15
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0baac665da | ||
|
|
1a13075926 | ||
|
|
019cbadd1e | ||
|
|
e6fdda0b3f | ||
|
|
e4885803df | ||
|
|
2321e8ae9f | ||
|
|
fdcf107993 | ||
|
|
e5175a0504 | ||
|
|
489dacca49 | ||
|
|
5eacfefb6c | ||
|
|
63d30d8f25 | ||
|
|
fff5b707ac | ||
|
|
d07c67327e | ||
|
|
9c76772969 | ||
|
|
a83b89b08a |
@@ -0,0 +1 @@
|
|||||||
|
3.12
|
||||||
+3
-3
@@ -37,16 +37,16 @@ class OcrResult(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
# OCR 服务已有分类器模型的国家(版面见 api.md §4)
|
# OCR 服务已有分类器模型的国家(版面见 api.md §4)
|
||||||
SUPPORTED_COUNTRIES = {"US", "UK", "DE", "MX", "SA", "CA", "ES", "IT"}
|
SUPPORTED_COUNTRIES = {"US", "UK", "DE", "MX", "SA", "CA", "ES", "IT", "PL"}
|
||||||
|
|
||||||
# ISO 国家代码 / 中文国名 → OCR 国家代码
|
# ISO 国家代码 / 中文国名 → OCR 国家代码
|
||||||
_COUNTRY_MAP: dict[str, str] = {
|
_COUNTRY_MAP: dict[str, str] = {
|
||||||
# 代码
|
# 代码
|
||||||
"GB": "UK", "UK": "UK", "US": "US", "DE": "DE", "MX": "MX", "SA": "SA",
|
"GB": "UK", "UK": "UK", "US": "US", "DE": "DE", "MX": "MX", "SA": "SA",
|
||||||
"CA": "CA", "ES": "ES", "IT": "IT",
|
"CA": "CA", "ES": "ES", "IT": "IT", "PL": "PL",
|
||||||
# 中文名
|
# 中文名
|
||||||
"英国": "UK", "美国": "US", "德国": "DE", "墨西哥": "MX", "沙特": "SA",
|
"英国": "UK", "美国": "US", "德国": "DE", "墨西哥": "MX", "沙特": "SA",
|
||||||
"加拿大": "CA", "西班牙": "ES", "意大利": "IT",
|
"加拿大": "CA", "西班牙": "ES", "意大利": "IT", "波兰": "PL",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -142,7 +142,7 @@ def label_ocr(
|
|||||||
手动跑指定国家: label_ocr(country_regions=['德国'])
|
手动跑指定国家: label_ocr(country_regions=['德国'])
|
||||||
"""
|
"""
|
||||||
if country_regions is None:
|
if country_regions is None:
|
||||||
country_regions = ['英国', '美国', '墨西哥', '德国', '沙特', '加拿大', '西班牙', '意大利']
|
country_regions = ['英国', '美国', '墨西哥', '德国', '沙特', '加拿大', '西班牙', '意大利', '波兰']
|
||||||
if _has_running_peer():
|
if _has_running_peer():
|
||||||
print("已有 label-ocr 在运行,跳过本次")
|
print("已有 label-ocr 在运行,跳过本次")
|
||||||
return {"skipped": True}
|
return {"skipped": True}
|
||||||
|
|||||||
@@ -0,0 +1,144 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""shop_dim 同步 flow:teable(源) -> 本地 smt 库(目标)。
|
||||||
|
|
||||||
|
最小包装自 sync_shop_dim.py,对应 shop-dim-sync,每 5 分钟调度一次。
|
||||||
|
源: PostgreSQL @ table.inkreach.cc:10432 db=teable schema=bsehWCxpH6HoHUdkjUd
|
||||||
|
物理表 tblUF7OrBxeRKUyWBc2 (teable 里名为 "shop_dim" 的表)
|
||||||
|
目标: PostgreSQL @ 192.168.124.92:5432 db=smt public.shop_dim
|
||||||
|
|
||||||
|
本地直接跑: python flows/platform_order/sync_shop_dim.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
import time
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import psycopg2
|
||||||
|
from prefect import flow
|
||||||
|
|
||||||
|
SRC = dict(host="table.inkreach.cc", port=10432, dbname="teable",
|
||||||
|
user="read_only_role_bsehWCxpH6HoHUdkjUd",
|
||||||
|
password="ygBuh8Au5J-PjuNQASos0", connect_timeout=25)
|
||||||
|
SRC_TABLE = '"bsehWCxpH6HoHUdkjUd"."tblUF7OrBxeRKUyWBc2"'
|
||||||
|
|
||||||
|
DST = dict(host="192.168.124.92", port=5432, dbname="smt",
|
||||||
|
user="postgres", password="Inkreach123456.", connect_timeout=15)
|
||||||
|
|
||||||
|
COL_MAP = [
|
||||||
|
("Guo_Jia", "country"),
|
||||||
|
("Dian_Pu_Ming_Cheng", "shop_name"),
|
||||||
|
("DBDian_Pu_Ming", "db_store_name"),
|
||||||
|
("Ping_Tai", "platform"),
|
||||||
|
("Lei_Xing", "shop_type"),
|
||||||
|
("Deng_Ji", "tier"),
|
||||||
|
("Fu_Ze_Ren", "owner"),
|
||||||
|
("Pin_Lei", "category"),
|
||||||
|
("Xia_Dian_Shi_Jian", "opened_at"),
|
||||||
|
("Mu_Biao_Jian_Shu", "sep_target_qty"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def log_msg(msg):
|
||||||
|
print(msg)
|
||||||
|
|
||||||
|
|
||||||
|
def connect(cfg, tries=5):
|
||||||
|
last = None
|
||||||
|
for i in range(tries):
|
||||||
|
try:
|
||||||
|
return psycopg2.connect(**cfg)
|
||||||
|
except Exception as e:
|
||||||
|
last = e
|
||||||
|
log_msg(f" 连接重试 {i+1}/{tries}: {e}")
|
||||||
|
time.sleep(3)
|
||||||
|
raise ConnectionError(f"连接失败: {last}")
|
||||||
|
|
||||||
|
|
||||||
|
def norm(v, col):
|
||||||
|
if v is None:
|
||||||
|
return None
|
||||||
|
if col == "opened_at":
|
||||||
|
return v.date() if hasattr(v, "date") else v
|
||||||
|
if col == "sep_target_qty":
|
||||||
|
try:
|
||||||
|
return int(round(float(v)))
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
s = str(v).strip()
|
||||||
|
return s if s != "" else None
|
||||||
|
|
||||||
|
|
||||||
|
@flow(name="shop-dim-sync", log_prints=True)
|
||||||
|
def shop_dim_sync(dry_run: bool = False, prune: bool = True) -> dict[str, Any]:
|
||||||
|
log_msg(f"[1/4] 读取源表 {SRC_TABLE} ...")
|
||||||
|
sc = connect(SRC)
|
||||||
|
scur = sc.cursor()
|
||||||
|
sel = ", ".join('"%s"' % c for c, _ in COL_MAP)
|
||||||
|
scur.execute("SELECT %s FROM %s" % (sel, SRC_TABLE))
|
||||||
|
raw = scur.fetchall()
|
||||||
|
scur.close(); sc.close()
|
||||||
|
src_rows = [
|
||||||
|
dict(zip([d for _, d in COL_MAP], [norm(v, c) for v, (_, c) in zip(row, COL_MAP)]))
|
||||||
|
for row in raw
|
||||||
|
]
|
||||||
|
log_msg(f" 源记录: {len(src_rows)} 行")
|
||||||
|
|
||||||
|
log_msg("[2/4] 连接目标 smt.shop_dim ...")
|
||||||
|
dc = connect(DST)
|
||||||
|
dcur = dc.cursor()
|
||||||
|
dcur.execute("SELECT country, db_store_name FROM shop_dim WHERE db_store_name IS NOT NULL")
|
||||||
|
existing = {(r[0], r[1]) for r in dcur.fetchall()}
|
||||||
|
log_msg(f" 目标现有: {len(existing)} 个 (country, db_store_name)")
|
||||||
|
|
||||||
|
src_keys = {(r["country"], r["db_store_name"]) for r in src_rows}
|
||||||
|
to_update = src_keys & existing
|
||||||
|
to_insert = src_keys - existing
|
||||||
|
to_delete = existing - src_keys
|
||||||
|
log_msg(f"[3/4] 计划: 更新 {len(to_update)} | 新增 {len(to_insert)} | 待删 {len(to_delete)}")
|
||||||
|
|
||||||
|
if dry_run:
|
||||||
|
log_msg("[dry-run] 不执行写入。")
|
||||||
|
if to_delete:
|
||||||
|
log_msg(" 待删示例: " + str(list(to_delete)[:10]))
|
||||||
|
dcur.close(); dc.close()
|
||||||
|
return {"sourceRows": len(src_rows), "dryRun": True}
|
||||||
|
|
||||||
|
cols = [d for _, d in COL_MAP]
|
||||||
|
upd = 0
|
||||||
|
ins = 0
|
||||||
|
for r in src_rows:
|
||||||
|
vals = [r[c] for c in cols]
|
||||||
|
if (r["country"], r["db_store_name"]) in existing:
|
||||||
|
set_clause = ", ".join(f"{c}=%s" for c in cols)
|
||||||
|
dcur.execute(
|
||||||
|
f"UPDATE shop_dim SET {set_clause} WHERE country=%s AND db_store_name=%s",
|
||||||
|
vals + [r["country"], r["db_store_name"]])
|
||||||
|
upd += dcur.rowcount
|
||||||
|
else:
|
||||||
|
ph = ", ".join(["%s"] * len(cols))
|
||||||
|
dcur.execute(f"INSERT INTO shop_dim ({', '.join(cols)}) VALUES ({ph})", vals)
|
||||||
|
ins += dcur.rowcount
|
||||||
|
|
||||||
|
dele = 0
|
||||||
|
if prune and to_delete:
|
||||||
|
for country, dbn in to_delete:
|
||||||
|
dcur.execute("DELETE FROM shop_dim WHERE country=%s AND db_store_name=%s", (country, dbn))
|
||||||
|
dele += dcur.rowcount
|
||||||
|
|
||||||
|
dc.commit()
|
||||||
|
log_msg(f"[4/4] 完成: 更新 {upd} | 新增 {ins} | 删除 {dele}")
|
||||||
|
dcur.execute("SELECT COUNT(*) FROM shop_dim WHERE db_store_name IS NOT NULL")
|
||||||
|
total = dcur.fetchone()[0]
|
||||||
|
log_msg(f" 目标现有总数: {total}")
|
||||||
|
dcur.close(); dc.close()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"sourceRows": len(src_rows),
|
||||||
|
"updated": upd,
|
||||||
|
"inserted": ins,
|
||||||
|
"deleted": dele,
|
||||||
|
"total": total,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
shop_dim_sync.serve(name="shop-dim-sync", cron="*/5 * * * *")
|
||||||
+3
-2
@@ -25,8 +25,9 @@ LOOKUP_BATCH = 10000
|
|||||||
# 单锁无环,写事务之间从结构上不可能死锁。1768851058 = "inkr" 的 ASCII 整数表示。
|
# 单锁无环,写事务之间从结构上不可能死锁。1768851058 = "inkr" 的 ASCII 整数表示。
|
||||||
DB_WRITE_LOCK_KEY = 1768851058
|
DB_WRITE_LOCK_KEY = 1768851058
|
||||||
LOCK_TIMEOUT = "15s" # 等锁超时 → LockNotAvailable → 交给重试兜底
|
LOCK_TIMEOUT = "15s" # 等锁超时 → LockNotAvailable → 交给重试兜底
|
||||||
WRITE_RETRIES = 3 # 死锁/等锁超时的整事务重试次数
|
WRITE_RETRIES = 5 # 死锁/等锁超时的整事务重试次数(外部写者如 17track
|
||||||
WRITE_RETRY_BACKOFF = [1, 3, 7] # 重试退避(秒)
|
# 物流回写不走 advisory 锁,冲突窗口可达分钟级,重试需够深)
|
||||||
|
WRITE_RETRY_BACKOFF = [1, 3, 7, 15, 30] # 重试退避(秒;末位预留,最后一次失败直接抛出)
|
||||||
WRITE_RETRYABLE_ERRORS = (pg_errors.DeadlockDetected, pg_errors.LockNotAvailable)
|
WRITE_RETRYABLE_ERRORS = (pg_errors.DeadlockDetected, pg_errors.LockNotAvailable)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ RIIN_GOODS_STATUS_MAP: dict[str, str] = {
|
|||||||
"CANCELLED": "已取消", "REFUNDING": "退款中",
|
"CANCELLED": "已取消", "REFUNDING": "退款中",
|
||||||
# 商品级状态复用平台状态枚举,COMPLETE/PAUSE 缺失曾导致 1/3 商品行 status 为 NULL
|
# 商品级状态复用平台状态枚举,COMPLETE/PAUSE 缺失曾导致 1/3 商品行 status 为 NULL
|
||||||
"COMPLETE": "已完成", "PAUSE": "暂停",
|
"COMPLETE": "已完成", "PAUSE": "暂停",
|
||||||
|
# CLOSE 同为商品终态(Goods 约束无"已关闭",取"已取消");UNKNOWN 保留 NULL 并由告警暴露
|
||||||
|
"CLOSE": "已取消",
|
||||||
}
|
}
|
||||||
|
|
||||||
RIIN_PLATFORM_STATUS_MAP: dict[str, str] = {
|
RIIN_PLATFORM_STATUS_MAP: dict[str, str] = {
|
||||||
|
|||||||
@@ -108,6 +108,13 @@ deployments:
|
|||||||
timezone: Asia/Shanghai
|
timezone: Asia/Shanghai
|
||||||
work_pool:
|
work_pool:
|
||||||
name: inkreach-pool
|
name: inkreach-pool
|
||||||
|
- name: shop-dim-sync
|
||||||
|
entrypoint: flows/platform_order/sync_shop_dim.py:shop_dim_sync
|
||||||
|
schedules:
|
||||||
|
- cron: '*/5 * * * *'
|
||||||
|
timezone: Asia/Shanghai
|
||||||
|
work_pool:
|
||||||
|
name: inkreach-pool
|
||||||
- name: label-ocr
|
- name: label-ocr
|
||||||
entrypoint: flows/label_ocr.py:label_ocr
|
entrypoint: flows/label_ocr.py:label_ocr
|
||||||
schedules:
|
schedules:
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ class RiinGoodsStatusTest(unittest.TestCase):
|
|||||||
"REFUNDING": "退款中",
|
"REFUNDING": "退款中",
|
||||||
"COMPLETE": "已完成", # 本次补齐(曾导致 NULL)
|
"COMPLETE": "已完成", # 本次补齐(曾导致 NULL)
|
||||||
"PAUSE": "暂停", # 本次补齐(曾导致 NULL)
|
"PAUSE": "暂停", # 本次补齐(曾导致 NULL)
|
||||||
|
"CLOSE": "已取消", # 回填重拉时告警发现的新枚举
|
||||||
}
|
}
|
||||||
for raw, want in cases.items():
|
for raw, want in cases.items():
|
||||||
with self.subTest(raw=raw):
|
with self.subTest(raw=raw):
|
||||||
|
|||||||
Reference in New Issue
Block a user