Some checks failed
CD Pipeline / deploy (push) Failing after 59s
- 建立 Gitea Actions CD pipeline (.gitea/workflows/cd.yaml) - 部署模式: rsync Python 檔案至 188 → docker restart (volume mount) - Dockerfile/requirements 變動時自動重建 Docker image - 部署通知: Telegram (開始/成功/失敗) - 健康檢查: https://mo.wooo.work/health (最多 5 次重試) - 同步最新 CLAUDE.md / ADR-008 / memory (2026-04-19) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
175 lines
6.3 KiB
Python
175 lines
6.3 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
商品圖片 URL 批次更新工具
|
||
功能:將所有商品的圖片 URL 更新為 MOMO CDN 標準格式
|
||
格式:https://m.momoshop.com.tw/moscdn/goods/{i_code}_m.webp
|
||
"""
|
||
|
||
from database.manager import DatabaseManager
|
||
from database.models import Product
|
||
from logger_manager import SystemLogger
|
||
from datetime import datetime
|
||
|
||
log = SystemLogger("ImageURLUpdater").get_logger()
|
||
|
||
def get_image_path(i_code):
|
||
"""
|
||
將 i_code 轉換為圖片路徑
|
||
規則:從右往左取 3位/3位/剩餘,第一部分補0到4位
|
||
例如:12092813 → 813/092/12 → 0012/092/813
|
||
"""
|
||
i_code_str = str(i_code)
|
||
|
||
# 從右往左切分
|
||
part3 = i_code_str[-3:] if len(i_code_str) >= 3 else i_code_str.zfill(3) # 最後3位
|
||
part2 = i_code_str[-6:-3] if len(i_code_str) > 3 else '000' # 中間3位
|
||
part1 = i_code_str[:-6] if len(i_code_str) > 6 else '0' # 前面剩餘
|
||
|
||
part1 = part1.zfill(4) # 第一部分補0到4位
|
||
part2 = part2.zfill(3) # 第二部分補0到3位 (如果需要)
|
||
|
||
return f'{part1}/{part2}/{part3}'
|
||
|
||
def update_all_product_images():
|
||
"""批次更新所有商品的圖片 URL"""
|
||
|
||
db = DatabaseManager()
|
||
session = db.get_session()
|
||
|
||
try:
|
||
log.info("=" * 80)
|
||
log.info("🖼️ 開始批次更新商品圖片 URL")
|
||
log.info("=" * 80)
|
||
|
||
# 查詢所有商品
|
||
all_products = session.query(Product).all()
|
||
total_count = len(all_products)
|
||
|
||
log.info(f"📊 總商品數: {total_count}")
|
||
|
||
# 統計變數
|
||
updated_count = 0
|
||
no_change_count = 0
|
||
error_count = 0
|
||
|
||
log.info("\n🔄 開始處理商品...")
|
||
print() # 空行,讓輸出更清晰
|
||
|
||
for idx, product in enumerate(all_products, 1):
|
||
try:
|
||
# 構造標準圖片 URL (使用新格式)
|
||
# https://img.momoshop.com.tw/goodsimg/0012/092/813/12092813_OL_m.webp
|
||
path = get_image_path(product.i_code)
|
||
new_image_url = f"https://img.momoshop.com.tw/goodsimg/{path}/{product.i_code}_OL_m.webp"
|
||
|
||
# 判斷是否需要更新
|
||
needs_update = False
|
||
|
||
if product.image_url is None:
|
||
# 情況 1: 沒有圖片 URL
|
||
needs_update = True
|
||
reason = "無圖片 URL"
|
||
elif product.image_url != new_image_url:
|
||
# 情況 2: 圖片 URL 格式不正確
|
||
needs_update = True
|
||
reason = "格式需更新"
|
||
|
||
if needs_update:
|
||
old_url = product.image_url if product.image_url else "無"
|
||
product.image_url = new_image_url
|
||
product.updated_at = datetime.now()
|
||
updated_count += 1
|
||
|
||
if updated_count % 100 == 0:
|
||
log.info(f" ✅ 已更新 {updated_count}/{total_count} 件商品...")
|
||
|
||
# 詳細記錄(每 500 件顯示一次詳情)
|
||
if updated_count % 500 == 1 or updated_count <= 5:
|
||
log.debug(f" [{idx}/{total_count}] {reason}")
|
||
log.debug(f" 商品: {product.name[:40]}...")
|
||
log.debug(f" i_code: {product.i_code}")
|
||
log.debug(f" 舊 URL: {old_url[:60]}..." if len(str(old_url)) > 60 else f" 舊 URL: {old_url}")
|
||
log.debug(f" 新 URL: {new_image_url}")
|
||
else:
|
||
no_change_count += 1
|
||
|
||
except Exception as e:
|
||
error_count += 1
|
||
log.error(f" ❌ 處理失敗 | i_code: {product.i_code} | 商品: {product.name[:30]} | Error: {e}")
|
||
|
||
# 提交變更
|
||
log.info("\n💾 提交資料庫變更...")
|
||
session.commit()
|
||
|
||
# 輸出統計結果
|
||
log.info("\n" + "=" * 80)
|
||
log.info("📊 處理結果統計")
|
||
log.info("=" * 80)
|
||
log.info(f" 總商品數: {total_count}")
|
||
log.info(f" ✅ 已更新: {updated_count} ({updated_count/total_count*100:.1f}%)")
|
||
log.info(f" ⏭️ 無需更新: {no_change_count} ({no_change_count/total_count*100:.1f}%)")
|
||
log.info(f" ❌ 處理失敗: {error_count}")
|
||
log.info("=" * 80)
|
||
|
||
if updated_count > 0:
|
||
log.info(f"✅ 成功更新 {updated_count} 件商品的圖片 URL")
|
||
else:
|
||
log.info("✅ 所有商品圖片 URL 已是最新格式,無需更新")
|
||
|
||
return {
|
||
'total': total_count,
|
||
'updated': updated_count,
|
||
'no_change': no_change_count,
|
||
'error': error_count
|
||
}
|
||
|
||
except Exception as e:
|
||
session.rollback()
|
||
log.error(f"❌ 批次更新失敗: {e}")
|
||
raise
|
||
finally:
|
||
session.close()
|
||
|
||
if __name__ == "__main__":
|
||
try:
|
||
result = update_all_product_images()
|
||
|
||
# 驗證結果
|
||
print("\n" + "=" * 80)
|
||
print("🔍 驗證更新結果...")
|
||
print("=" * 80)
|
||
|
||
db = DatabaseManager()
|
||
session = db.get_session()
|
||
|
||
try:
|
||
# 檢查還有多少商品沒有圖片 URL
|
||
products_without_image = session.query(Product).filter(Product.image_url.is_(None)).count()
|
||
|
||
# 檢查使用標準格式的商品數
|
||
standard_pattern = "https://m.momoshop.com.tw/moscdn/goods/%"
|
||
products_with_standard_url = session.query(Product).filter(
|
||
Product.image_url.like(standard_pattern)
|
||
).count()
|
||
|
||
total_products = session.query(Product).count()
|
||
|
||
print(f"無圖片 URL 商品: {products_without_image} ({products_without_image/total_products*100:.1f}%)")
|
||
print(f"標準格式商品: {products_with_standard_url} ({products_with_standard_url/total_products*100:.1f}%)")
|
||
print("=" * 80)
|
||
|
||
if products_without_image == 0:
|
||
print("✅ 驗證成功:所有商品都有圖片 URL")
|
||
else:
|
||
print(f"⚠️ 仍有 {products_without_image} 件商品沒有圖片 URL")
|
||
|
||
finally:
|
||
session.close()
|
||
|
||
except KeyboardInterrupt:
|
||
print("\n⚠️ 使用者中斷執行")
|
||
except Exception as e:
|
||
print(f"\n❌ 執行失敗: {e}")
|
||
raise
|