PUGE 3 өдөр өмнө
parent
commit
8d6545215f

+ 5 - 0
cdk_config.json

@@ -0,0 +1,5 @@
+{
+  "cdk": "asdsad",
+  "verified_time": 1786464546.4044068,
+  "verified_date": "2026-08-12 00:09:06"
+}

+ 1 - 0
config.json

@@ -1,5 +1,6 @@
 {
     "username": "深金川",
+    "password": "123456",
     "folder_path": "C:/Users/mail/Documents/GitHub/pythonTest",
     "last_load_client": "",
     "last_unload_client": "",

+ 152 - 29
test.py

@@ -1,35 +1,158 @@
 import requests
-import time
+import json
+import datetime
+import hashlib
+import hmac
+from urllib.parse import quote
 
-url = "https://cppaidan.com/paidanserv/system/user/verifySensitivePassword"
+# ==================== 配置区域 ====================
+CLIENT_ID = "amzn1.application-oa2-client.9f775d35077a4c7598116e5a89b60222"
+CLIENT_SECRET = "amzn1.oa2-cs.v1.6909777baa82b796555a431cd23709342e8e01434abcad1f8b59cadf048b51c3"
+REFRESH_TOKEN = "Atzr|IwEBICh_fojh7qAfrePzSjOZhQxTnHRZCYOuWNvSAHGaBs1WfkjR4qjC1Dmv0uK3oyq_L-R9SRKK4cYmPwF1MQXQdqKlpzOAGItWeXnNzh-SyhHHISv0_vCt3D6hluVmIEmF2kA7ro_XF-PslD41g3Eq6bv0Qq4y4pQbkDj-9dudDuz7nku8UL-nc4EWgJCrHCJTAs8TK48Yvrfi3E35uv97tYhs6qb8D9d4dR9C7daaC49eoDqP88qyUHThQV1Kqon8xukx4AT2_wLqWNcabpADzuK-5w7D-vvFFtvzfkbmtfzoPeEdHPulG6scc59BPFTyBlGU7a3d1JSFg2ROHIlrljRm"
 
-headers = {
-    "accept": "application/json, text/plain, */*",
-    "accept-language": "zh-CN,zh;q=0.9,en;q=0.8,zh-TW;q=0.7",
-    "authorization": "Bearer eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6ImM0MjQ0ZWFhLWM0MDktNGRmYS04NTk5LTk5NGQ5N2FmODQ1NyJ9.uPEKT90Q2QGQeWqz5Y3-TUCapP4-UChKqkcxVS6FgkIdGSa8PYLadEOMiC0OSL2hVRTx54ZL-6SjfVrpoiUWxA",
-    "cookie": "Admin-Paidan-Token=eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6Ijk3OWE1YWVkLWZjYTctNDY1Zi1iMmU2LTE1ZjdhZDkyY2NmNCJ9.9RwvxQdkbCWs4K_4CXuyO3OW6Uh42NR03zkz7r-ugc3lvyxvUzdtToug2BqqtdvO0lh6t4hHKgUn3S8QbY21uw",
-    "referer": "https://cppaidan.com/paidan/system/user"
-}
+AWS_ACCESS_KEY = "amzn1.application-oa2-client.9f775d35077a4c7598116e5a89b60222"       # ← 填你的
+AWS_SECRET_KEY = "amzn1.oa2-cs.v1.6909777baa82b796555a431cd23709342e8e01434abcad1f8b59cadf048b51c3"             # ← 填你的
+AWS_REGION = "us-east-1"
 
-password = 67745
-while password < 1000000:
+SELLER_ID = "A1H50Z0WYYLEGQ"
+SKU = "bar grey 03"
+MARKETPLACE_ID = "ATVPDKIKX0DER"
+
+# ==================== 工具函数 ====================
+def sha256_hash(data): 
+    return hashlib.sha256(data.encode("utf-8")).hexdigest()
+
+def hmac_sha256(key, msg): 
+    return hmac.new(key, msg.encode("utf-8"), hashlib.sha256).digest()
+
+def get_signature_key(secret_key, date_stamp, region, service):
+    k_date = hmac_sha256(("AWS4" + secret_key).encode("utf-8"), date_stamp)
+    k_region = hmac_sha256(k_date, region)
+    k_service = hmac_sha256(k_region, service)
+    return hmac_sha256(k_service, "aws4_request")
+
+def get_access_token():
+    url = "https://api.amazon.com/auth/o2/token"
+    resp = requests.post(url, data={
+        "grant_type": "refresh_token",
+        "refresh_token": REFRESH_TOKEN,
+        "client_id": CLIENT_ID,
+        "client_secret": CLIENT_SECRET,
+    })
+    resp.raise_for_status()
+    return resp.json()["access_token"]
+
+# ==================== 带 AWS 签名的请求 ====================
+def spapi_request(method, path, query, body=None, access_token=None):
+    service = "execute-api"
+    host = "sellingpartnerapi-na.amazon.com"
+    
+    # path 已经包含编码后的 SKU,直接用
+    uri = path
+    
+    payload = json.dumps(body, separators=(",", ":")) if body else ""
+    
+    t = datetime.datetime.now(datetime.timezone.utc)
+    amz_date = t.strftime("%Y%m%dT%H%M%SZ")
+    date_stamp = t.strftime("%Y%m%d")
+    
+    canonical_headers = (
+        f"host:{host}\n"
+        f"x-amz-access-token:{access_token}\n"
+        f"x-amz-date:{amz_date}\n"
+    )
+    signed_headers = "host;x-amz-access-token;x-amz-date"
+    payload_hash = sha256_hash(payload)
+    
+    canonical_request = (
+        f"{method}\n{uri}\n{query}\n"
+        f"{canonical_headers}\n{signed_headers}\n{payload_hash}"
+    )
+    
+    algorithm = "AWS4-HMAC-SHA256"
+    credential_scope = f"{date_stamp}/{AWS_REGION}/{service}/aws4_request"
+    string_to_sign = (
+        f"{algorithm}\n{amz_date}\n{credential_scope}\n"
+        f"{sha256_hash(canonical_request)}"
+    )
+    
+    signing_key = get_signature_key(AWS_SECRET_KEY, date_stamp, AWS_REGION, service)
+    signature = hmac.new(signing_key, string_to_sign.encode("utf-8"), hashlib.sha256).hexdigest()
+    
+    auth_header = (
+        f"{algorithm} Credential={AWS_ACCESS_KEY}/{credential_scope}, "
+        f"SignedHeaders={signed_headers}, Signature={signature}"
+    )
+    
+    headers = {
+        "Host": host,
+        "x-amz-access-token": access_token,
+        "x-amz-date": amz_date,
+        "Authorization": auth_header,
+    }
+    if body:
+        headers["Content-Type"] = "application/json"
+    
+    url = f"https://{host}{uri}?{query}"
+    return requests.request(method, url, headers=headers, data=payload or None)
+
+# ==================== 查询商品 ====================
+def get_listings_item(access_token):
+    # 对 SKU 进行 URL 编码:空格 → %20
+    encoded_sku = quote(SKU, safe="")
+    path = f"/listings/2021-08-01/items/{SELLER_ID}/{encoded_sku}"
+    query = f"marketplaceIds={MARKETPLACE_ID}&includedData=summaries,attributes,issues,offers"
+    return spapi_request("GET", path, query, access_token=access_token)
+
+# ==================== 修改主图 ====================
+def patch_main_image(access_token, image_url):
+    encoded_sku = quote(SKU, safe="")
+    path = f"/listings/2021-08-01/items/{SELLER_ID}/{encoded_sku}"
+    query = f"marketplaceIds={MARKETPLACE_ID}"
+    
+    body = {
+        "productType": "PRODUCT",
+        "patches": [
+            {
+                "op": "replace",
+                "path": "/attributes/main_product_image_locator",
+                "value": [
+                    {
+                        "marketplace_id": MARKETPLACE_ID,
+                        "media_location": image_url
+                    }
+                ]
+            }
+        ]
+    }
+    return spapi_request("PATCH", path, query, body=body, access_token=access_token)
+
+# ==================== 主程序 ====================
+if __name__ == "__main__":
+    print("🔑 获取 Access Token...")
+    token = get_access_token()
+    print("✅ 令牌获取成功")
+    
+    print(f"\n📤 查询商品信息(SKU: {SKU})...")
+    resp = get_listings_item(token)
+    print(f"状态码: {resp.status_code}")
+    
     try:
-        response = requests.post(
-            url,
-            headers=headers,
-            json={"userId": 251, "sensitivePassword": str(password)},
-            timeout=3
-        )
-        
-        data = response.json()
-        print(password)
-        
-        if not (data.get("code") == 500 and data.get("msg") == "密码错误"):
-            print(data)
-            break
+        data = resp.json()
+        print(json.dumps(data, indent=2, ensure_ascii=False))
+    except Exception:
+        print(resp.text)
+    
+    if resp.status_code == 200:
+        print("\n📋 关键信息:")
+        print(f"  productType: {data.get('productType', 'N/A')}")
+        summaries = data.get('summaries', [])
+        if summaries:
+            print(f"  标题: {summaries[0].get('itemName', 'N/A')}")
         
-        password += 1
-            
-    except Exception as e:
-        print(f"错误: {e},重试密码: {password}")
-        time.sleep(1)  # 错误后等待1秒再重试
+        attrs = data.get('attributes', {})
+        main_img = attrs.get('main_product_image_locator', [])
+        if main_img:
+            print(f"  当前主图: {main_img[0].get('value', {}).get('link', 'N/A')}")
+        else:
+            print("  当前主图: 未设置")

+ 677 - 0
天龙八部拍卖行.py

@@ -0,0 +1,677 @@
+import ctypes
+import time
+import os
+import sys
+import platform
+import win32gui
+import win32con
+import win32api
+import win32clipboard as clipboard
+from PIL import ImageGrab
+import requests
+import io
+import tkinter as tk
+from tkinter import ttk, scrolledtext, messagebox
+import threading
+import json
+
+
+lastKeyWord = ""
+# ========== 获取程序目录 ==========
+def get_app_dir():
+    if getattr(sys, 'frozen', False):
+        return os.path.dirname(sys.executable)
+    else:
+        return os.path.dirname(os.path.abspath(__file__))
+
+APP_DIR = get_app_dir()
+
+# ========== 全局变量 ==========
+is_running = False
+stop_flag = False
+config_file = os.path.join(APP_DIR, "config.json")
+
+# ========== 幽灵键鼠加载 ==========
+def load_ghost_key_mouse():
+    if platform.architecture()[0] == "64bit":
+        dll_path = os.path.join(APP_DIR, "gbild64.dll")
+    else:
+        dll_path = os.path.join(APP_DIR, "gbild32.dll")
+    
+    if not os.path.exists(dll_path):
+        return None
+    
+    try:
+        dll = ctypes.windll.LoadLibrary(dll_path)
+        return dll
+    except Exception as e:
+        return None
+
+ghost_dll = load_ghost_key_mouse()
+
+if ghost_dll:
+    ghost_dll.getmodel.restype = ctypes.c_char_p
+    ghost_dll.getserialnumber.restype = ctypes.c_char_p
+    ghost_dll.getproductiondate.restype = ctypes.c_char_p
+    ghost_dll.getfirmwareversion.restype = ctypes.c_char_p
+    ghost_dll.getclientscreenresolution.restype = ctypes.c_char_p
+    ghost_dll.readstring.restype = ctypes.c_char_p
+    ghost_dll.encryptstring.restype = ctypes.c_char_p
+    ghost_dll.decryptstring.restype = ctypes.c_char_p
+    ghost_dll.getproductname.restype = ctypes.c_char_p
+    ghost_dll.sdktype.restype = ctypes.c_char_p
+    ghost_dll.sdkversion.restype = ctypes.c_char_p
+
+    def opendevice(index=0):
+        return ghost_dll.opendevice(index)
+
+    def closedevice():
+        return ghost_dll.closedevice()
+
+    def pressandreleasemousebutton(mbtn):
+        return ghost_dll.pressandreleasemousebutton(mbtn)
+
+    def combinationkey(key_sequence):
+        return ghost_dll.combinationkey(key_sequence)
+
+    def pressandreleasekeybyname(key_name):
+        return ghost_dll.pressandreleasekeybyname(key_name)
+
+    device_id = opendevice(0)
+    if device_id == 0:
+        ghost_available = False
+    else:
+        ghost_available = True
+else:
+    ghost_available = False
+
+# ========== 核心功能类 ==========
+class AuctionBot:
+    def __init__(self, log_callback):
+        self.log_callback = log_callback
+        self.target_hwnd = None
+        self.left = 0
+        self.top = 0
+        self.is_running = False
+        
+    def log(self, message):
+        if self.log_callback:
+            self.log_callback(message)
+    
+    def get_hwnds_by_title_contains(self, title_contains):
+        hwnds = []
+        def enum_callback(hwnd, _):
+            if win32gui.IsWindowVisible(hwnd):
+                window_title = win32gui.GetWindowText(hwnd)
+                if title_contains in window_title:
+                    hwnds.append(hwnd)
+            return True
+        win32gui.EnumWindows(enum_callback, None)
+        return hwnds
+
+    def get_client_rect(self, hwnd):
+        rect = win32gui.GetClientRect(hwnd)
+        point = win32gui.ClientToScreen(hwnd, (rect[0], rect[1]))
+        return (point[0], point[1], point[0] + rect[2], point[1] + rect[3])
+
+    def activate_window(self, hwnd):
+        try:
+            if win32gui.IsIconic(hwnd):
+                win32gui.ShowWindow(hwnd, win32con.SW_RESTORE)
+            win32gui.SetForegroundWindow(hwnd)
+            win32gui.BringWindowToTop(hwnd)
+            time.sleep(0.2)
+            return True
+        except Exception as e:
+            self.log(f"激活窗口失败: {e}")
+            return False
+
+    def capture_window_area_to_bytes(self, hwnd, x1, y1, x2, y2):
+        try:
+            client_rect = self.get_client_rect(hwnd)
+            left, top, right, bottom = client_rect
+            
+            screen_x1 = left + x1
+            screen_y1 = top + y1
+            screen_x2 = left + x2
+            screen_y2 = top + y2
+            
+            screenshot = ImageGrab.grab(bbox=(screen_x1, screen_y1, screen_x2, screen_y2))
+            img_bytes = io.BytesIO()
+            screenshot.save(img_bytes, format='PNG')
+            img_bytes.seek(0)
+            return img_bytes.getvalue()
+        except Exception as e:
+            self.log(f"截图失败: {e}")
+            return None
+
+    def ocr_price(self, image_bytes):
+        try:
+            # url = "http://111.229.45.38:19100/ocr"
+            url = "http://192.168.1.101:10001/ocr"
+            files = {'image': ('screenshot.png', image_bytes, 'image/png')}
+            
+            self.log("正在识别价格...")
+            response = requests.post(url, files=files, timeout=30)
+            
+            if response.status_code != 200:
+                self.log(f"OCR请求失败: HTTP {response.status_code}")
+                return None
+            
+            result = response.json()
+            
+            if not result.get('success', False):
+                self.log(f"OCR识别失败: {result}")
+                return None
+            
+            texts = result.get('texts', [])
+            if not texts:
+                self.log("未识别到任何文字")
+                return None
+            
+            all_texts = []
+            for item in texts:
+                rec_texts = item.get('rec_texts', [])
+                all_texts.extend(rec_texts)
+            
+            for text in all_texts:
+                try:
+                    price = float(text)
+                    self.log(f"识别到价格: {price}")
+                    return price
+                except ValueError:
+                    continue
+            
+            self.log(f"未识别到价格,识别到的文字: {all_texts}")
+            return None
+        except Exception as e:
+            self.log(f"OCR识别异常: {e}")
+            return None
+
+    def mouse_click_at(self, x, y, button=0):
+        try:
+            # 1. 移动物理光标(部分游戏会校验系统光标位置)
+            win32api.SetCursorPos((x, y))
+            time.sleep(0.1)
+
+            # 2. 准备 SendInput 结构体(64/32位兼容)
+            ULONG_PTR = ctypes.c_uint64 if ctypes.sizeof(ctypes.c_void_p) == 8 else ctypes.c_uint32
+
+            class MOUSEINPUT(ctypes.Structure):
+                _fields_ = [
+                    ("dx", ctypes.c_long),
+                    ("dy", ctypes.c_long),
+                    ("mouseData", ctypes.c_ulong),
+                    ("dwFlags", ctypes.c_ulong),
+                    ("time", ctypes.c_ulong),
+                    ("dwExtraInfo", ULONG_PTR),
+                ]
+
+            class KEYBDINPUT(ctypes.Structure):
+                _fields_ = [
+                    ("wVk", ctypes.c_ushort),
+                    ("wScan", ctypes.c_ushort),
+                    ("dwFlags", ctypes.c_ulong),
+                    ("time", ctypes.c_ulong),
+                    ("dwExtraInfo", ULONG_PTR),
+                ]
+
+            class HARDWAREINPUT(ctypes.Structure):
+                _fields_ = [
+                    ("uMsg", ctypes.c_ulong),
+                    ("wParamL", ctypes.c_ushort),
+                    ("wParamH", ctypes.c_ushort),
+                ]
+
+            class DUMMYUNIONNAME(ctypes.Union):
+                _fields_ = [("mi", MOUSEINPUT), ("ki", KEYBDINPUT), ("hi", HARDWAREINPUT)]
+
+            class INPUT(ctypes.Structure):
+                _fields_ = [("type", ctypes.c_ulong), ("DUMMYUNIONNAME", DUMMYUNIONNAME)]
+
+            INPUT_MOUSE = 0
+            screen_w = win32api.GetSystemMetrics(win32con.SM_CXSCREEN)
+            screen_h = win32api.GetSystemMetrics(win32con.SM_CYSCREEN)
+
+            # 3. 转换为绝对坐标(0-65535)
+            abs_x = int(x * 65535 / (screen_w - 1)) if screen_w > 1 else 0
+            abs_y = int(y * 65535 / (screen_h - 1)) if screen_h > 1 else 0
+
+            # 4. 选择按键
+            if button == 0:
+                down_flag = win32con.MOUSEEVENTF_LEFTDOWN
+                up_flag = win32con.MOUSEEVENTF_LEFTUP
+            else:
+                down_flag = win32con.MOUSEEVENTF_RIGHTDOWN
+                up_flag = win32con.MOUSEEVENTF_RIGHTUP
+
+            move_abs = win32con.MOUSEEVENTF_ABSOLUTE | win32con.MOUSEEVENTF_MOVE
+
+            # 5. 按下(带绝对坐标移动)
+            inp_down = INPUT()
+            inp_down.type = INPUT_MOUSE
+            inp_down.DUMMYUNIONNAME.mi.dx = abs_x
+            inp_down.DUMMYUNIONNAME.mi.dy = abs_y
+            inp_down.DUMMYUNIONNAME.mi.mouseData = 0
+            inp_down.DUMMYUNIONNAME.mi.dwFlags = move_abs | down_flag
+            inp_down.DUMMYUNIONNAME.mi.time = 0
+            inp_down.DUMMYUNIONNAME.mi.dwExtraInfo = 0
+
+            ctypes.windll.user32.SendInput(1, ctypes.byref(inp_down), ctypes.sizeof(INPUT))
+            
+            # 关键:保持按下状态至少100ms,确保游戏能轮询到
+            time.sleep(0.1)
+
+            # 6. 释放
+            inp_up = INPUT()
+            inp_up.type = INPUT_MOUSE
+            inp_up.DUMMYUNIONNAME.mi.dx = abs_x
+            inp_up.DUMMYUNIONNAME.mi.dy = abs_y
+            inp_up.DUMMYUNIONNAME.mi.mouseData = 0
+            inp_up.DUMMYUNIONNAME.mi.dwFlags = move_abs | up_flag
+            inp_up.DUMMYUNIONNAME.mi.time = 0
+            inp_up.DUMMYUNIONNAME.mi.dwExtraInfo = 0
+
+            ctypes.windll.user32.SendInput(1, ctypes.byref(inp_up), ctypes.sizeof(INPUT))
+            time.sleep(0.1)
+
+            return True
+
+        except Exception as e:
+            self.log(f"鼠标点击失败: {e}")
+            return False
+
+    def keyboard_ctrl_v(self):
+        try:
+            if ghost_available:
+                return combinationkey(b"ctrl+v") == 1
+            else:
+                win32api.keybd_event(win32con.VK_CONTROL, 0, 0, 0)
+                time.sleep(0.05)
+                win32api.keybd_event(ord('V'), 0, 0, 0)
+                time.sleep(0.05)
+                win32api.keybd_event(ord('V'), 0, win32con.KEYEVENTF_KEYUP, 0)
+                time.sleep(0.05)
+                win32api.keybd_event(win32con.VK_CONTROL, 0, win32con.KEYEVENTF_KEYUP, 0)
+                return True
+        except Exception as e:
+            self.log(f"Ctrl+V失败: {e}")
+            return False
+
+    def clipboard_set_text(self, text):
+        try:
+            clipboard.OpenClipboard()
+            clipboard.EmptyClipboard()
+            clipboard.SetClipboardText(text, clipboard.CF_TEXT)
+            clipboard.CloseClipboard()
+            return True
+        except Exception as e:
+            self.log(f"设置剪贴板失败: {e}")
+            return False
+
+    def process_item(self, keyword, target_price):
+        global lastKeyWord
+        """处理单个关键词"""
+        if stop_flag:
+            return False
+        
+        self.log(f"========== 处理: {keyword} (目标价格: {target_price}) ==========")
+        if (lastKeyWord != keyword):
+            # 点击重置
+            click_x = self.left + 728
+            click_y = self.top + 152
+            self.log(f"点击重置: ({click_x}, {click_y})")
+            self.mouse_click_at(click_x, click_y, 0)
+            time.sleep(1)
+        
+            # 设置剪贴板
+            self.log(f"设置剪贴板: '{keyword}'")
+            self.clipboard_set_text(keyword)
+            
+            # 点击输入框粘贴
+            click_x = self.left + 568
+            click_y = self.top + 152
+            self.log(f"点击输入框: ({click_x}, {click_y})")
+            self.mouse_click_at(click_x, click_y, 0)
+            time.sleep(1)
+            
+            # Ctrl+V
+            self.log("执行 Ctrl+V")
+            self.keyboard_ctrl_v()
+            time.sleep(1)
+        else:
+            self.log(f"搜索关键词一样,自动忽略重置")    
+        lastKeyWord = keyword
+        # 点击搜索按钮
+        click_x = self.left + 664
+        click_y = self.top + 152
+        self.log(f"点击搜索按钮: ({click_x}, {click_y})")
+        self.mouse_click_at(click_x, click_y, 0)
+        time.sleep(2)
+        
+        # 截图识别价格
+        self.log("截图识别价格...")
+        img_bytes = self.capture_window_area_to_bytes(self.target_hwnd, 580, 190 - 30, 634, 211 - 30)
+        
+        if not img_bytes:
+            self.log("截图失败")
+            return False
+        
+        price = self.ocr_price(img_bytes)
+        
+        if price is None:
+            self.log("价格识别失败")
+            return False
+        
+        self.log(f"识别到价格: {price}, 目标价格: {target_price}")
+        
+        if price <= target_price and price > 1:
+            self.log(f"价格 {price} <= {target_price},执行购买!")
+            
+            # 选择购买项
+            click_x = self.left + 506
+            click_y = self.top + 202
+            self.log(f"选择购买项: ({click_x}, {click_y})")
+            self.mouse_click_at(click_x, click_y, 0)
+            time.sleep(0.5)
+            
+            # 购买
+            click_x = self.left + 729
+            click_y = self.top + 545
+            self.log(f"点击购买: ({click_x}, {click_y})")
+            self.mouse_click_at(click_x, click_y, 0)
+            time.sleep(1)
+            
+            # 确认购买
+            click_x = self.left + 516
+            click_y = self.top + 286
+            self.log(f"确认购买: ({click_x}, {click_y})")
+            self.mouse_click_at(click_x, click_y, 0)
+
+            
+            self.log(f"✅ {keyword} 购买成功! 价格: {price}")
+            return True
+        else:
+            self.log(f"价格 {price} > {target_price},不购买")
+            return False
+
+    def run(self, keywords_prices, interval):
+        """主循环"""
+        global stop_flag
+        stop_flag = False
+        
+        self.log("="*60)
+        self.log("开始自动拍卖操作")
+        self.log("="*60)
+        
+        # 查找窗口
+        self.log("查找新天龙八部窗口...")
+        window_list = self.get_hwnds_by_title_contains("新天龙八部")
+        
+        if not window_list:
+            self.log("未找到新天龙八部窗口!")
+            return
+        
+        self.target_hwnd = window_list[0]
+        window_title = win32gui.GetWindowText(self.target_hwnd)
+        self.log(f"找到窗口: {window_title}")
+        
+        # 获取窗口位置
+        client_rect = self.get_client_rect(self.target_hwnd)
+        self.left, self.top, right, bottom = client_rect
+        self.top = self.top - 30
+        self.log(f"窗口位置: left={self.left}, top={self.top}")
+        
+        # 激活窗口
+        self.log("激活窗口...")
+        self.activate_window(self.target_hwnd)
+        time.sleep(1)
+        
+        round_count = 0
+        
+        while not stop_flag:
+            round_count += 1
+            self.log(f"\n========== 第 {round_count} 轮 ==========")
+            
+            for keyword, target_price in keywords_prices:
+                if stop_flag:
+                    break
+                
+                try:
+                    self.process_item(keyword, target_price)
+                    time.sleep(1)
+                except Exception as e:
+                    self.log(f"处理 {keyword} 时出错: {e}")
+            
+            if not stop_flag:
+                self.log(f"等待 {interval} 秒后继续...")
+                # 分段等待,便于检测停止信号
+                for _ in range(interval):
+                    if stop_flag:
+                        break
+                    time.sleep(1)
+        
+        self.log("程序已停止")
+
+# ========== GUI程序 ==========
+class AuctionGUI:
+    def __init__(self, root):
+        self.root = root
+        self.root.title("天龙八部自动拍卖机器人")
+        self.root.geometry("800x700")
+        self.root.resizable(True, True)
+        
+        self.bot = AuctionBot(self.log_message)
+        self.keywords_list = []
+        self.running = False
+        
+        self.setup_ui()
+        self.load_config()
+    
+    def setup_ui(self):
+        # 主框架
+        main_frame = ttk.Frame(self.root, padding="10")
+        main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
+        
+        # 配置区域
+        config_frame = ttk.LabelFrame(main_frame, text="配置", padding="10")
+        config_frame.grid(row=0, column=0, sticky=(tk.W, tk.E), pady=(0, 10))
+        
+        # 关键词列表
+        ttk.Label(config_frame, text="关键词和价格:").grid(row=0, column=0, sticky=tk.W)
+        
+        # 关键词表格
+        columns = ('关键词', '目标价格')
+        self.tree = ttk.Treeview(config_frame, columns=columns, show='headings', height=6)
+        self.tree.heading('关键词', text='关键词')
+        self.tree.heading('目标价格', text='目标价格')
+        self.tree.column('关键词', width=150)
+        self.tree.column('目标价格', width=100)
+        self.tree.grid(row=1, column=0, columnspan=4, sticky=(tk.W, tk.E), pady=(5, 5))
+        
+        # 滚动条
+        scrollbar = ttk.Scrollbar(config_frame, orient=tk.VERTICAL, command=self.tree.yview)
+        scrollbar.grid(row=1, column=4, sticky=(tk.N, tk.S))
+        self.tree.configure(yscrollcommand=scrollbar.set)
+        
+        # 添加关键词
+        ttk.Label(config_frame, text="关键词:").grid(row=2, column=0, sticky=tk.W, pady=(5, 0))
+        self.keyword_entry = ttk.Entry(config_frame, width=15)
+        self.keyword_entry.grid(row=3, column=0, sticky=tk.W, pady=(0, 5))
+        
+        ttk.Label(config_frame, text="目标价格:").grid(row=2, column=1, sticky=tk.W, pady=(5, 0))
+        self.price_entry = ttk.Entry(config_frame, width=10)
+        self.price_entry.grid(row=3, column=1, sticky=tk.W, pady=(0, 5))
+        
+        ttk.Button(config_frame, text="添加", command=self.add_keyword).grid(row=3, column=2, padx=(5, 0))
+        ttk.Button(config_frame, text="删除选中", command=self.delete_keyword).grid(row=3, column=3, padx=(5, 0))
+        
+        # 循环间隔
+        ttk.Label(config_frame, text="循环间隔(秒):").grid(row=4, column=0, sticky=tk.W, pady=(5, 0))
+        self.interval_var = tk.StringVar(value="10")
+        self.interval_entry = ttk.Entry(config_frame, textvariable=self.interval_var, width=10)
+        self.interval_entry.grid(row=5, column=0, sticky=tk.W, pady=(0, 5))
+        
+        # 控制按钮
+        control_frame = ttk.Frame(config_frame)
+        control_frame.grid(row=5, column=1, columnspan=3, sticky=tk.E, pady=(0, 5))
+        
+        self.start_btn = ttk.Button(control_frame, text="开始", command=self.start_bot)
+        self.start_btn.grid(row=0, column=0, padx=(0, 5))
+        
+        self.stop_btn = ttk.Button(control_frame, text="停止", command=self.stop_bot, state=tk.DISABLED)
+        self.stop_btn.grid(row=0, column=1, padx=(0, 5))
+        
+        ttk.Button(control_frame, text="保存配置", command=self.save_config).grid(row=0, column=2, padx=(0, 5))
+        ttk.Button(control_frame, text="加载配置", command=self.load_config).grid(row=0, column=3)
+        
+        # 日志区域
+        log_frame = ttk.LabelFrame(main_frame, text="日志", padding="10")
+        log_frame.grid(row=1, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
+        
+        self.log_text = scrolledtext.ScrolledText(log_frame, height=20, width=80)
+        self.log_text.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
+        
+        # 配置网格权重
+        self.root.columnconfigure(0, weight=1)
+        self.root.rowconfigure(0, weight=1)
+        main_frame.columnconfigure(0, weight=1)
+        main_frame.rowconfigure(1, weight=1)
+        log_frame.columnconfigure(0, weight=1)
+        log_frame.rowconfigure(0, weight=1)
+    
+    def add_keyword(self):
+        keyword = self.keyword_entry.get().strip()
+        price_str = self.price_entry.get().strip()
+        
+        if not keyword:
+            messagebox.showwarning("警告", "请输入关键词")
+            return
+        
+        try:
+            price = float(price_str)
+        except ValueError:
+            messagebox.showwarning("警告", "请输入有效的价格")
+            return
+        
+        self.tree.insert('', 'end', values=(keyword, price))
+        self.keyword_entry.delete(0, tk.END)
+        self.price_entry.delete(0, tk.END)
+    
+    def delete_keyword(self):
+        selected = self.tree.selection()
+        if not selected:
+            messagebox.showwarning("警告", "请先选中要删除的项目")
+            return
+        for item in selected:
+            self.tree.delete(item)
+    
+    def get_keywords_list(self):
+        items = self.tree.get_children()
+        result = []
+        for item in items:
+            values = self.tree.item(item)['values']
+            if values:
+                result.append((values[0], float(values[1])))
+        return result
+    
+    def log_message(self, message):
+        self.log_text.insert(tk.END, f"{time.strftime('%H:%M:%S')} - {message}\n")
+        self.log_text.see(tk.END)
+        self.root.update_idletasks()
+    
+    def start_bot(self):
+        keywords = self.get_keywords_list()
+        if not keywords:
+            messagebox.showwarning("警告", "请至少添加一个关键词")
+            return
+        
+        try:
+            interval = int(self.interval_var.get())
+            if interval < 1:
+                raise ValueError
+        except ValueError:
+            messagebox.showwarning("警告", "请输入有效的间隔秒数(大于0)")
+            return
+        
+        if not ghost_available:
+            result = messagebox.askyesno("警告", "幽灵键鼠未连接,点击将使用系统API,是否继续?")
+            if not result:
+                return
+        
+        self.running = True
+        self.start_btn.config(state=tk.DISABLED)
+        self.stop_btn.config(state=tk.NORMAL)
+        
+        # 在新线程中运行
+        self.bot_thread = threading.Thread(
+            target=self.bot.run,
+            args=(keywords, interval)
+        )
+        self.bot_thread.daemon = True
+        self.bot_thread.start()
+    
+    def stop_bot(self):
+        global stop_flag
+        stop_flag = True
+        self.log_message("正在停止...")
+        self.start_btn.config(state=tk.NORMAL)
+        self.stop_btn.config(state=tk.DISABLED)
+        self.running = False
+    
+    def save_config(self):
+        keywords = self.get_keywords_list()
+        config = {
+            'keywords': keywords,
+            'interval': self.interval_var.get()
+        }
+        try:
+            with open(config_file, 'w', encoding='utf-8') as f:
+                json.dump(config, f, ensure_ascii=False, indent=2)
+            self.log_message(f"配置已保存到: {config_file}")
+            messagebox.showinfo("成功", "配置已保存")
+        except Exception as e:
+            messagebox.showerror("错误", f"保存配置失败: {e}")
+    
+    def load_config(self):
+        try:
+            if not os.path.exists(config_file):
+                return
+            
+            with open(config_file, 'r', encoding='utf-8') as f:
+                config = json.load(f)
+            
+            # 清空当前列表
+            for item in self.tree.get_children():
+                self.tree.delete(item)
+            
+            # 加载关键词
+            for keyword, price in config.get('keywords', []):
+                self.tree.insert('', 'end', values=(keyword, price))
+            
+            # 加载间隔
+            if 'interval' in config:
+                self.interval_var.set(config['interval'])
+            
+            self.log_message("配置已加载")
+        except Exception as e:
+            messagebox.showerror("错误", f"加载配置失败: {e}")
+
+def main():
+    root = tk.Tk()
+    app = AuctionGUI(root)
+    root.mainloop()
+
+if __name__ == "__main__":
+    try:
+        main()
+    except Exception as e:
+        print(f"程序出错: {e}")
+        import traceback
+        traceback.print_exc()
+    finally:
+        if ghost_available:
+            try:
+                closedevice()
+            except:
+                pass

BIN
幽灵键鼠/__pycache__/ghostbox.cpython-313.pyc


BIN
幽灵键鼠/gbild32.dll


BIN
幽灵键鼠/gbild64.dll


+ 260 - 0
幽灵键鼠/ghostbox.py

@@ -0,0 +1,260 @@
+# -*- coding: utf-8 -*-
+import platform
+from ctypes import *
+import os
+
+
+# 加载DLL
+script_dir = os.path.dirname(os.path.abspath(__file__))
+if platform.architecture()[0] == "64bit":
+    dll_path = os.path.join(script_dir, "gbild64.dll")
+else:
+    dll_path = os.path.join(script_dir, "gbild32.dll")
+dll = windll.LoadLibrary(dll_path)
+
+
+# 设置接口返回值类型
+dll.getmodel.restype = c_char_p
+dll.getserialnumber.restype = c_char_p
+dll.getproductiondate.restype = c_char_p
+dll.getfirmwareversion.restype = c_char_p
+dll.getclientscreenresolution.restype = c_char_p
+dll.readstring.restype = c_char_p
+dll.encryptstring.restype = c_char_p
+dll.decryptstring.restype = c_char_p
+dll.getproductname.restype = c_char_p
+dll.sdktype.restype = c_char_p
+dll.sdkversion.restype = c_char_p
+
+
+
+# ================ 设备操作
+# 打开设备(根据设备序号)
+def opendevice(index=0):
+    return dll.opendevice(index)
+# 打开设备(根据设备ID)
+def opendevicebyid(vid,pid):
+    return dll.opendevicebyid(vid,pid)
+# 打开设备(根据设备路径)
+def opendevicebypath(path):
+    return dll.opendevicebypath(bytes(path,"utf-8"))
+# 检查设备是否连接
+def isconnected():
+    return dll.isconnected()
+# 关闭设备
+def closedevice():
+    return dll.closedevice()
+# 复位设备
+def resetdevice():
+    return dll.resetdevice()
+
+
+# ================ 设备信息
+# 获取设备型号
+def getmodel():
+    return dll.getmodel().decode("utf-8")
+# 获取设备序列号
+def getserialnumber():
+    return dll.getserialnumber().decode("utf-8")
+# 获取设备生产日期
+def getproductiondate():
+    return dll.getproductiondate().decode("utf-8")
+# 获取设备固件版本号
+def getfirmwareversion():
+    return dll.getfirmwareversion().decode("utf-8")
+
+
+# ================ 键盘操作
+# 按下键
+def presskeybyname(keyn):
+    return dll.presskeybyname(bytes(keyn,"utf-8"))
+def presskeybyvalue(keyv):
+    return dll.presskeybyvalue(keyv)
+# 释放键
+def releasekeybyname(keyn):
+    return dll.releasekeybyname(bytes(keyn,"utf-8"))
+def releasekeybyvalue(keyv):
+    return dll.releasekeybyvalue(keyv)
+# 按下并释放键
+def pressandreleasekeybyname(keyn):
+    return dll.pressandreleasekeybyname(bytes(keyn,"utf-8"))
+def pressandreleasekeybyvalue(keyv):
+    return dll.pressandreleasekeybyvalue(keyv)
+# 判断键盘按键状态
+def iskeypressedbyname(keyn):
+    return dll.iskeypressedbyname(bytes(keyn,"utf-8"))
+def iskeypressedbyvalue(keyv):
+    return dll.iskeypressedbyvalue(keyv)
+# 释放所有键盘按键
+def releaseallkey():
+    return dll.releaseallkey()
+# 组合按键
+def combinationkey(keys):
+    return dll.combinationkey(bytes(keys,"utf-8"))
+# 输入字符串
+def inputstring(str):
+    return dll.inputstring(bytes(str,"utf-8"))
+# 获取大写锁定状态
+def getcapslock():
+    return dll.getcapslock()
+# 获取数字键盘锁定状态
+def getnumlock():
+    return dll.getnumlock()
+# 设置是否区分大小写
+def setcasesensitive(cs):
+    return dll.setcasesensitive(cs)
+# 设置按键延时
+def setpresskeydelay(maxd,mind):
+    return dll.setpresskeydelay(maxd,mind)
+# 设置输入字符串间隔时间
+def setinputstringintervaltime(maxd,mind):
+    return dll.setinputstringintervaltime(maxd,mind)
+# 设置中文输入模式
+def setchineseinputmode(mode):
+    return dll.setchineseinputmode(mode)
+
+
+# ================ 鼠标操作
+# 按下鼠标键
+def pressmousebutton(mbtn):
+    return dll.pressmousebutton(mbtn)
+# 释放鼠标键
+def releasemousebutton(mbtn):
+    return dll.releasemousebutton(mbtn)
+# 按下并释放鼠标键
+def pressandreleasemousebutton(mbtn):
+    return dll.pressandreleasemousebutton(mbtn)
+# 判断鼠标按键状态
+def ismousebuttonpressed(mbtn):
+    return dll.ismousebuttonpressed(mbtn)
+# 释放所有鼠标按键
+def releaseallmousebutton():
+    return dll.releaseallmousebutton()
+# 相对移动鼠标
+def movemouserelative(x,y):
+    return dll.movemouserelative(x,y)
+# 移动鼠标到指定坐标
+def movemouseto(x,y):
+    return dll.movemouseto(x,y)
+# 获取鼠标当前位置
+def getmousex():
+    return dll.getmousex()
+def getmousey():
+    return dll.getmousey()
+# 移动鼠标滚轮
+def movemousewheel(z):
+    return dll.movemousewheel(z)
+# 设置鼠标按键延时
+def setpressmousebuttondelay(maxd,mind):
+    return dll.setpressmousebuttondelay(maxd,mind)
+# 设置鼠标移动延时
+def setmousemovementdelay(maxd,mind):
+    return dll.setmousemovementdelay(maxd,mind)
+# 设置鼠标移动速度
+def setmousemovementspeed(speedvalue):
+    return dll.setmousemovementspeed(speedvalue)
+# 设置鼠标移动模式
+def setmousemovementmode(modevalue):
+    return dll.setmousemovementmode(modevalue)
+
+
+# ================ 双机设备接口
+# 设置鼠标当前位置(针对不支持绝对值的鼠标)
+def setmouseposition(x,y):
+    return dll.setmouseposition(x,y)
+# 设置鼠标绝对位置(针对支持绝对值的鼠标)
+def setmouseabsoluteposition(x,y):
+    return dll.setmouseabsoluteposition(x,y)
+# 设置鼠标逻辑位置(不移动鼠标,只更新内部当前坐标)
+def setmouselogicalposition(x,y):
+    return dll.setmouselogicalposition(x,y)
+# 相对移动鼠标(只移动鼠标,不更新内部当前坐标)
+def movemouserelative_ncc(x,y):
+    return dll.movemouserelative_ncc(x,y)
+# 设置被控端屏幕分辨率
+def setclientscreenresolution(width,height):
+    return dll.setclientscreenresolution(width,height)
+# 获取被控端屏幕分辨率
+def getclientscreenresolution():
+    return dll.getclientscreenresolution().decode("utf-8")
+# 选择被控端(对于APV系列的一控多模式,指定键盘鼠标操作对应的设备)
+def selectsubdevice(addr):
+    return dll.selectsubdevice(addr)
+# 修改被控端设备ID  devtype: 0 主控端,1 被控端键盘(U2),2 被控端鼠标(U2),3 被控端复合键鼠(U1)
+def setslavedeviceid(vid,pid,devtype):
+    return dll.setslavedeviceid(vid,pid,devtype)
+
+
+# ================ 加密狗操作
+# 初始化加密狗
+def initializedongle():
+    return dll.initializedongle()
+# 设置读密码
+def setreadpassword(writepwd,newpwd):
+    return dll.setreadpassword(bytes(writepwd,"utf-8"),bytes(newpwd,"utf-8"))
+# 设置写密码
+def setwritepassword(oldpwd,newpwd):
+    return dll.setwritepassword(bytes(oldpwd,"utf-8"),bytes(newpwd,"utf-8"))
+# 从设备读字符串
+def readstring(readpwd,addr,count):
+    return dll.readstring(bytes(readpwd,"utf-8"),addr,count).decode("utf-8", errors="ignore")
+# 将字符串写入设备
+def writestring(writepwd,str,addr):
+    return dll.writestring(bytes(writepwd,"utf-8"),bytes(str,"utf-8"),addr)
+# 设置密钥
+def setencryptionkey(writepwd,cipher):
+    return dll.setencryptionkey(bytes(writepwd,"utf-8"),bytes(cipher,"utf-8"))
+# 加密字符串
+def encryptstring(str):
+    return dll.encryptstring(bytes(str,"utf-8")).decode("utf-8")
+# 解密字符串
+def decryptstring(str):
+    return dll.decryptstring(bytes(str,"utf-8")).decode("utf-8")
+
+
+# ================ 电源控制接口
+# 按下电源按钮
+def presspowerbutton():
+    return dll.presspowerbutton()
+# 释放电源按钮
+def releasepowerbutton():
+    return dll.releasepowerbutton()
+# 按下并释放电源按钮
+def pressandreleasepowerbutton():
+    return dll.pressandreleasepowerbutton()
+# 获取电源工作状态
+def getpowerstatus():
+    return dll.getpowerstatus()
+
+
+# ================ 设备定义接口
+# 修改设备速度
+def setspeed(speed):
+    return dll.setspeed()
+# 修改设备ID
+def setdeviceid(vid,pid):
+    return dll.setdeviceid(vid,pid)
+# 恢复设备默认ID
+def restoredeviceid():
+    return dll.restoredeviceid()
+# 获取当前设备VID
+def getvid():
+    return dll.getvid()
+# 获取当前设备PID
+def getpid():
+    return dll.getpid()
+# 设置产品名称
+def setproductname(name):
+    return dll.setproductname(bytes(name,"gbk"))
+# 获取产品名称
+def getproductname():
+    return dll.getproductname().decode("gbk")
+
+
+# ================ 其他操作
+# 获取当前SDK库类型
+def sdktype():
+    return dll.sdktype().decode("utf-8")
+# 获取当前SDK库版本
+def sdkversion():
+    return dll.sdkversion().decode("utf-8")

+ 31 - 0
幽灵键鼠/test_ghostbox.py

@@ -0,0 +1,31 @@
+# -*- coding: utf-8 -*-
+import ghostbox as gb
+import time
+
+print(f"{gb.sdktype()}({gb.sdkversion()})")
+
+# 打开设备
+print(gb.opendevice())
+#print(gb.opendevicebyid(0x5188,0x1801))
+#print(gb.opendevicebypath("\\\\?\\hid#vid_5188&pid_1801&mi_02#7&28610698&0&0000#{4d1e55b2-f16f-11cf-88cb-001111000030}"))
+# 获取设备型号
+print(gb.getmodel())
+# 获取硬件版本号
+print(gb.getfirmwareversion())
+# 按win键
+# print(gb.pressandreleasekeybyname("win"))
+# 移动鼠标
+# gb.setmousemovementmode(3)
+# gb.setmousemovementdelay(3,5)
+# print(gb.movemouseto(100,100))
+
+gb.presskeybyname("right")
+gb.presskeybyname("up")
+time.sleep(3)
+gb.releaseallkey()
+
+# gb.combinationkey("win+r")
+
+# time.sleep(2)
+# print(gb.getcapslock())
+# gb.inputstring("xxxDDDff")

+ 1 - 1
条码处理/pdf_merge_config.json

@@ -4,5 +4,5 @@
   "output_dir": "C:/Users/mail/Downloads/速卖通 条码测试",
   "scale_percent": "100",
   "offset_x": "0",
-  "offset_y": "105"
+  "offset_y": "100"
 }

+ 24 - 14
条码处理/速卖通.py

@@ -3,6 +3,7 @@
 """
 PDF 条码合成工具(单文件输入,每页独立输出)
 模板为图片,嵌入到输入文件每一页的指定位置
+输出页面固定裁剪为 60mm × 50mm
 """
 
 import tkinter as tk
@@ -16,6 +17,11 @@ from PIL import Image
 
 CONFIG_FILE = "pdf_merge_config.json"
 
+# 输出页面固定尺寸:60mm × 50mm → 点(1pt = 25.4mm / 72)
+MM_TO_PT = 72.0 / 25.4
+TARGET_W = 60 * MM_TO_PT   # ≈ 170.08 pt
+TARGET_H = 50 * MM_TO_PT   # ≈ 141.73 pt
+
 
 class PDFMergeApp:
     def __init__(self, root):
@@ -32,7 +38,7 @@ class PDFMergeApp:
         # 缩放与偏移变量
         self.scale_var = tk.StringVar(value="100")
         self.offset_x_var = tk.StringVar(value="0")
-        self.offset_y_var = tk.StringVar(value="110")
+        self.offset_y_var = tk.StringVar(value="100")
 
         self.build_ui()
         self.load_config()
@@ -64,17 +70,17 @@ class PDFMergeApp:
         param_frame = ttk.LabelFrame(main_frame, text="合成参数(自动保存)", padding="10")
         param_frame.grid(row=3, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=10)
 
-        ttk.Label(param_frame, text="缩放比例(% 页宽):").grid(row=0, column=0, sticky=tk.W, padx=4)
+        ttk.Label(param_frame, text="缩放比例(% 输出页宽):").grid(row=0, column=0, sticky=tk.W, padx=4)
         ttk.Entry(param_frame, textvariable=self.scale_var, width=10).grid(row=0, column=1, sticky=tk.W, padx=4)
-        ttk.Label(param_frame, text="例:100 = 模板宽度填满页面").grid(row=0, column=2, sticky=tk.W, padx=4)
+        ttk.Label(param_frame, text="例:100 = 模板宽度填满 60mm").grid(row=0, column=2, sticky=tk.W, padx=4)
 
         ttk.Label(param_frame, text="X 偏移(点):").grid(row=1, column=0, sticky=tk.W, padx=4, pady=6)
         ttk.Entry(param_frame, textvariable=self.offset_x_var, width=10).grid(row=1, column=1, sticky=tk.W, padx=4, pady=6)
-        ttk.Label(param_frame, text="左上角水平偏移,0 表示贴左边").grid(row=1, column=2, sticky=tk.W, padx=4, pady=6)
+        ttk.Label(param_frame, text="相对于 60mm 页面左上角,0 表示贴左边").grid(row=1, column=2, sticky=tk.W, padx=4, pady=6)
 
         ttk.Label(param_frame, text="Y 偏移(点):").grid(row=2, column=0, sticky=tk.W, padx=4)
         ttk.Entry(param_frame, textvariable=self.offset_y_var, width=10).grid(row=2, column=1, sticky=tk.W, padx=4)
-        ttk.Label(param_frame, text="左上角垂直偏移,0 表示贴顶部").grid(row=2, column=2, sticky=tk.W, padx=4)
+        ttk.Label(param_frame, text="相对于 50mm 页面左上角,0 表示贴顶部").grid(row=2, column=2, sticky=tk.W, padx=4)
 
         # 开始按钮
         btn_frame = ttk.Frame(main_frame)
@@ -185,7 +191,7 @@ class PDFMergeApp:
         lines = [ln.strip() for ln in text.splitlines() if ln.strip()]
         if not lines:
             return ""
-        first = lines[0]
+        first = lines[1]
         fourth = lines[4] if len(lines) >= 5 else ""
         fourth = fourth.replace("¨", "0")
         fourth = fourth.replace("©", "1")
@@ -197,7 +203,7 @@ class PDFMergeApp:
         fourth = fourth.replace("°2", "7")
         fourth = fourth.replace("°", "8")
         fourth = fourth.replace("°2", "9")
-        name = f"{first}_{fourth}" if fourth else first
+        name = first
         return self.sanitize_filename(name)
 
     def sanitize_filename(self, text):
@@ -247,6 +253,7 @@ class PDFMergeApp:
 
         self.log(f"📄 输入文件:{os.path.basename(input_file)}")
         self.log(f"🖼 模板图片:{os.path.basename(template_file)}")
+        self.log(f"📐 输出页面尺寸:60mm × 50mm({TARGET_W:.1f}pt × {TARGET_H:.1f}pt)")
         self.log(f"⚙ 参数:缩放 {scale_percent}% 宽,偏移 ({offset_x}, {offset_y})\n")
 
         try:
@@ -276,16 +283,19 @@ class PDFMergeApp:
                     base = os.path.splitext(os.path.basename(input_file))[0]
                     safe_name = f"{base}_page{page_num}"
 
-                # 创建单页新 PDF
+                # 创建固定 60×50mm 的新页面
                 new_doc = fitz.open()
-                page_rect = page.rect
-                new_page = new_doc.new_page(width=page_rect.width, height=page_rect.height)
+                new_page = new_doc.new_page(width=TARGET_W, height=TARGET_H)
 
-                # 1) 输入页作为背景
-                new_page.show_pdf_page(page_rect, doc, idx, overlay=False)
+                # 1) 输入页作为背景(原尺寸放置,超出部分自然被裁剪)
+                src_rect = page.rect
+                new_page.show_pdf_page(
+                    fitz.Rect(0, 0, src_rect.width, src_rect.height),
+                    doc, idx, overlay=False
+                )
 
-                # 2) 计算模板目标矩形(缩放模板)
-                target_w = page_rect.width * (scale_percent / 100.0)
+                # 2) 计算模板目标矩形(缩放基准改为输出页面宽度
+                target_w = TARGET_W * (scale_percent / 100.0)
                 scale = target_w / template_w
                 target_h = template_h * scale
                 target_rect = fitz.Rect(offset_x, offset_y, offset_x + target_w, offset_y + target_h)

+ 274 - 0
自动匹配上架图片.py

@@ -0,0 +1,274 @@
+import tkinter as tk
+from tkinter import ttk, filedialog, messagebox, scrolledtext
+import pandas as pd
+import os
+import threading
+import requests
+from openpyxl import load_workbook
+from openpyxl.utils import get_column_letter
+import re
+
+
+class ImageMatcherApp:
+    def __init__(self, root):
+        self.root = root
+        self.root.title("图片匹配与上传工具")
+        self.root.geometry("850x650")
+        
+        # 存储文件路径
+        self.template_path = tk.StringVar()
+        self.image_dir = tk.StringVar()
+        
+        # 固定上传配置
+        self.upload_url = "https://cos.port.run/uploadFile"
+        self.upload_group = "soundSynthesis"
+        self.upload_user = "1"
+        
+        # 创建UI
+        self.create_widgets()
+        
+    def create_widgets(self):
+        # 主框架
+        main_frame = ttk.Frame(self.root, padding="10")
+        main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
+        
+        # 配置网格权重
+        self.root.columnconfigure(0, weight=1)
+        self.root.rowconfigure(0, weight=1)
+        main_frame.columnconfigure(1, weight=1)
+        main_frame.rowconfigure(4, weight=1)
+        
+        # 标题
+        title_label = ttk.Label(main_frame, text="图片匹配与上传工具", font=("Arial", 16, "bold"))
+        title_label.grid(row=0, column=0, columnspan=3, pady=10)
+        
+        # 模板文件选择
+        ttk.Label(main_frame, text="模板文件:").grid(row=1, column=0, sticky=tk.W, pady=5)
+        ttk.Entry(main_frame, textvariable=self.template_path, width=50).grid(row=1, column=1, sticky=(tk.W, tk.E), pady=5, padx=5)
+        ttk.Button(main_frame, text="选择模板", command=self.select_template).grid(row=1, column=2, pady=5, padx=5)
+        
+        # 图片目录选择
+        ttk.Label(main_frame, text="图片目录:").grid(row=2, column=0, sticky=tk.W, pady=5)
+        ttk.Entry(main_frame, textvariable=self.image_dir, width=50).grid(row=2, column=1, sticky=(tk.W, tk.E), pady=5, padx=5)
+        ttk.Button(main_frame, text="选择目录", command=self.select_image_dir).grid(row=2, column=2, pady=5, padx=5)
+        
+        # 按钮框架
+        button_frame = ttk.Frame(main_frame)
+        button_frame.grid(row=3, column=0, columnspan=3, pady=10)
+        
+        ttk.Button(button_frame, text="开始处理", command=self.start_matching, width=15).pack(side=tk.LEFT, padx=5)
+        ttk.Button(button_frame, text="清空日志", command=self.clear_log, width=15).pack(side=tk.LEFT, padx=5)
+        
+        # 日志框
+        log_frame = ttk.LabelFrame(main_frame, text="日志", padding="5")
+        log_frame.grid(row=4, column=0, columnspan=3, sticky=(tk.W, tk.E, tk.N, tk.S), pady=10)
+        log_frame.columnconfigure(0, weight=1)
+        log_frame.rowconfigure(0, weight=1)
+        
+        self.log_text = scrolledtext.ScrolledText(log_frame, height=15, width=80, wrap=tk.WORD)
+        self.log_text.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
+        
+        # 状态栏
+        self.status_var = tk.StringVar()
+        self.status_var.set("就绪")
+        status_bar = ttk.Label(main_frame, textvariable=self.status_var, relief=tk.SUNKEN, anchor=tk.W)
+        status_bar.grid(row=5, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=5)
+        
+        # 存储生成的文件路径
+        self.generated_file_path = None
+        
+    def select_template(self):
+        filename = filedialog.askopenfilename(
+            title="选择模板文件",
+            filetypes=[("Excel文件", "*.xlsx *.xls"), ("所有文件", "*.*")]
+        )
+        if filename:
+            self.template_path.set(filename)
+            self.log(f"已选择模板文件: {filename}")
+            
+    def select_image_dir(self):
+        directory = filedialog.askdirectory(title="选择图片目录")
+        if directory:
+            self.image_dir.set(directory)
+            self.log(f"已选择图片目录: {directory}")
+            
+    def log(self, message):
+        """在日志框中添加消息"""
+        self.log_text.insert(tk.END, f"{message}\n")
+        self.log_text.see(tk.END)
+        self.root.update_idletasks()
+        
+    def clear_log(self):
+        """清空日志"""
+        self.log_text.delete(1.0, tk.END)
+        
+    def start_matching(self):
+        """开始匹配处理"""
+        if not self.template_path.get():
+            messagebox.showerror("错误", "请先选择模板文件!")
+            return
+        if not self.image_dir.get():
+            messagebox.showerror("错误", "请先选择图片目录!")
+            return
+            
+        thread = threading.Thread(target=self.process_matching, daemon=True)
+        thread.start()
+        
+    def process_matching(self):
+        """执行匹配处理"""
+        try:
+            self.status_var.set("处理中...")
+            self.log("=" * 50)
+            self.log("开始处理...")
+            
+            template_path = self.template_path.get()
+            image_dir = self.image_dir.get()
+            
+            self.log(f"读取模板文件: {template_path}")
+            
+            # 使用openpyxl加载工作簿,保留所有样式
+            wb = load_workbook(template_path)
+            ws = wb.active
+            
+            # 获取表头(第二行)
+            headers = []
+            for col in range(1, ws.max_column + 1):
+                cell_value = ws.cell(row=2, column=col).value
+                headers.append(str(cell_value) if cell_value else '')
+            
+            self.log(f"找到 {len(headers)} 列")
+            
+            # 查找各列的列号
+            col_a_index = 1  # A列是第1列
+            product_main_col = None
+            image_cols = {}
+            
+            for idx, header in enumerate(headers, start=1):
+                if header == '产品主图':
+                    product_main_col = idx
+                elif '上架图片' in header:
+                    # 提取数字
+                    match = re.search(r'(\d+)', header)
+                    if match:
+                        num = int(match.group(1))
+                        image_cols[num] = idx
+            
+            if product_main_col is None:
+                self.log("警告: 未找到'产品主图'列")
+            else:
+                self.log(f"找到产品主图列: 第{product_main_col}列")
+                
+            self.log(f"找到上架图片列: {len(image_cols)} 列")
+            
+            # 处理每一行(从第三行开始)
+            processed_count = 0
+            for row_idx in range(3, ws.max_row + 1):
+                # 获取A列的值
+                a_cell = ws.cell(row=row_idx, column=col_a_index)
+                a_value = str(a_cell.value).strip() if a_cell.value else ''
+                
+                if not a_value or a_value == 'nan' or a_value == 'None':
+                    continue
+                    
+                self.log(f"\n处理第 {row_idx} 行, A列值: {a_value}")
+                
+                # 处理产品主图 (图片编号1)
+                if product_main_col is not None:
+                    img_url = self.find_and_upload_image(a_value, 1, image_dir)
+                    if img_url:
+                        ws.cell(row=row_idx, column=product_main_col, value=img_url)
+                        self.log(f"  产品主图上传成功: {img_url[:50]}...")
+                    else:
+                        self.log(f"  产品主图: 未找到或上传失败")
+                
+                # 处理上架图片1-6 (图片编号2-7)
+                for i in range(1, 7):
+                    if i in image_cols:
+                        img_url = self.find_and_upload_image(a_value, i+1, image_dir)
+                        if img_url:
+                            ws.cell(row=row_idx, column=image_cols[i], value=img_url)
+                            self.log(f"  上架图片{i}上传成功: {img_url[:50]}...")
+                        else:
+                            self.log(f"  上架图片{i}: 未找到或上传失败")
+                            
+                processed_count += 1
+                
+            # 保存结果(保留所有样式)
+            output_path = self.generate_output_path(template_path)
+            wb.save(output_path)
+            self.generated_file_path = output_path
+            self.log(f"\n处理完成!共处理 {processed_count} 行数据")
+            self.log(f"结果已保存到: {output_path}")
+            
+            self.status_var.set(f"处理完成!共处理 {processed_count} 行")
+            messagebox.showinfo("完成", f"处理完成!\n共处理 {processed_count} 行数据\n结果保存在:\n{output_path}")
+            
+        except Exception as e:
+            error_msg = f"处理出错: {str(e)}"
+            self.log(f"错误: {error_msg}")
+            self.status_var.set("处理出错")
+            messagebox.showerror("错误", error_msg)
+            import traceback
+            self.log(traceback.format_exc())
+    
+    def generate_output_path(self, template_path):
+        """生成输出文件路径"""
+        dir_path = os.path.dirname(template_path)
+        base_name = os.path.basename(template_path)
+        name_without_ext = os.path.splitext(base_name)[0]
+        new_filename = f"{name_without_ext}_结果.xlsx"
+        return os.path.join(dir_path, new_filename)
+            
+    def find_and_upload_image(self, a_value, num, image_dir):
+        """查找图片并上传,返回URL"""
+        # 查找本地图片
+        extensions = ['.jpg', '.jpeg', '.png']
+        local_file = None
+        
+        for ext in extensions:
+            filename = f"{a_value}_{num}{ext}"
+            filepath = os.path.join(image_dir, filename)
+            if os.path.exists(filepath):
+                local_file = filepath
+                break
+        
+        if not local_file:
+            return None
+            
+        # 上传图片到服务器
+        try:
+            full_url = f"{self.upload_url}?group={self.upload_group}&user={self.upload_user}"
+            
+            # 根据文件扩展名确定content-type
+            content_type = 'image/jpeg'
+            if local_file.lower().endswith('.png'):
+                content_type = 'image/png'
+            
+            with open(local_file, 'rb') as f:
+                files = {'file': (os.path.basename(local_file), f, content_type)}
+                response = requests.post(full_url, files=files, timeout=30)
+                
+                if response.status_code == 200:
+                    result = response.json()
+                    if result.get('err') == 0:
+                        return result.get('url')
+                    else:
+                        self.log(f"    上传失败: {result.get('msg', '未知错误')}")
+                        return None
+                else:
+                    self.log(f"    上传请求失败,状态码: {response.status_code}")
+                    return None
+                    
+        except Exception as e:
+            self.log(f"    上传出错: {str(e)}")
+            return None
+
+
+def main():
+    root = tk.Tk()
+    app = ImageMatcherApp(root)
+    root.mainloop()
+
+
+if __name__ == "__main__":
+    main()

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 1 - 1729
飞书提交数据.py


Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно