#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""飞书多维表格实时监控服务器"""
import json, http.server, socketserver, urllib.request, urllib.parse, urllib.error
import time, threading, os, traceback, re, subprocess, sys
from concurrent.futures import ThreadPoolExecutor, as_completed
import cloudscraper

PORT = 8080
APP_TOKEN = "GzfQbb5bcabdPvs4taxc9mNpnDe"
DEVICE_TABLES = {}
feishu_token = {"access_token": None, "expires_at": 0, "lock": threading.Lock()}
data_cache = {}
CACHE_TTL = 10

# 提取店铺相关表格
TABLE_EXTRACTABLE = "tblJsz99omsxQGxG"  # 可提取店铺
TABLE_EXTRACTED = "tblWbQ5JArDJI99v"    # 已提取店铺

def log(msg):
    print("[" + time.strftime("%H:%M:%S") + "] " + str(msg), flush=True)

def load_feishu_config():
    for p in [
        os.path.join(os.path.dirname(os.path.abspath(__file__)), "feishu_config.json"),
        os.path.join(os.getcwd(), "feishu_config.json"),
        "feishu_config.json",
    ]:
        if os.path.exists(p):
            with open(p, "r", encoding="utf-8") as f:
                cfg = json.loads(f.read())
            log("飞书配置已加载: " + p)
            return cfg.get("app_id"), cfg.get("app_secret")
    return None, None

def refresh_token():
    app_id, app_secret = load_feishu_config()
    if not app_id or not app_secret:
        log("错误: feishu_config.json 未找到")
        return False
    for attempt in range(3):
        if _do_refresh_token(app_id, app_secret):
            return True
        if attempt < 2:
            time.sleep(2)
            log("Token 刷新重试 " + str(attempt + 2) + "/3...")
    return False

def _do_refresh_token(app_id, app_secret):
    url = "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal"
    body = json.dumps({"app_id": app_id, "app_secret": app_secret}).encode("utf-8")
    req = urllib.request.Request(url, data=body, method="POST")
    req.add_header("Content-Type", "application/json")
    try:
        with urllib.request.urlopen(req, timeout=10) as resp:
            result = json.loads(resp.read().decode("utf-8"))
        if result.get("code") == 0:
            tok = result["tenant_access_token"]
            exp = time.time() + result.get("expire", 7200) - 300
            with feishu_token["lock"]:
                feishu_token["access_token"] = tok
                feishu_token["expires_at"] = exp
            log("Token 已刷新")
            return True
        else:
            log("获取 Token 失败: " + str(result.get("msg")))
            return False
    except Exception as e:
        log("请求 Token API 失败: " + str(e))
        return False

def get_token():
    with feishu_token["lock"]:
        if feishu_token["access_token"] and time.time() < feishu_token["expires_at"]:
            return feishu_token["access_token"]
    if refresh_token():
        return feishu_token["access_token"]
    return None

def discover_tables():
    token = get_token()
    if not token:
        return
    url = "https://open.feishu.cn/open-apis/bitable/v1/apps/" + APP_TOKEN + "/tables"
    try:
        req = urllib.request.Request(url)
        req.add_header("Authorization", "Bearer " + token)
        with urllib.request.urlopen(req, timeout=15) as resp:
            result = json.loads(resp.read().decode("utf-8"))
        if result.get("code") == 0:
            for t in result["data"].get("items", []):
                name = t.get("name", "")
                tid = t.get("table_id", "")
                if "实时监控" in name:
                    # 提取设备名：从"注册实时监控U4"中提取"U4"
                    device_name = None
                    m = re.search(r'(U\d+)', name, re.IGNORECASE)
                    if m:
                        device_name = m.group(1).upper()
                    else:
                        # 如果没有U编号，用表格名去掉"注册实时监控"前缀
                        device_name = name.replace("注册实时监控", "").strip() or name
                    DEVICE_TABLES[device_name] = tid
                    log("发现设备 " + device_name + " 表: " + name)
    except Exception as e:
        log("发现表失败: " + str(e))

def fetch_table_data(table_id):
    """从飞书获取表格数据（完整分页）"""
    token = get_token()
    if not token or not table_id:
        return None
    url = ("https://open.feishu.cn/open-apis/bitable/v1/apps/"
            + APP_TOKEN + "/tables/" + table_id + "/records?page_size=500")
    try:
        req = urllib.request.Request(url)
        req.add_header("Authorization", "Bearer " + token)
        with urllib.request.urlopen(req, timeout=15) as resp:
            result = json.loads(resp.read().decode("utf-8"))
        if result.get("code") != 0:
            log("飞书 API 错误: " + str(result.get("msg")))
            return None
        records = result["data"].get("items", [])
        pt = result["data"].get("page_token", "")
        hm = result["data"].get("has_more", False)
        while hm and pt:
            url2 = ("https://open.feishu.cn/open-apis/bitable/v1/apps/"
                     + APP_TOKEN + "/tables/" + table_id
                     + "/records?page_size=500&page_token=" + pt)
            req2 = urllib.request.Request(url2)
            req2.add_header("Authorization", "Bearer " + token)
            with urllib.request.urlopen(req2, timeout=15) as r2:
                r2j = json.loads(r2.read().decode("utf-8"))
            if r2j.get("code") == 0:
                records.extend(r2j["data"].get("items", []))
                hm = r2j["data"].get("has_more", False)
                pt = r2j["data"].get("page_token", "")

        all_fields = []
        for rec in records:
            for k in rec.get("fields", {}):
                if k not in all_fields:
                    all_fields.append(k)
        status_field = None
        for f in all_fields:
            if "运行日志" in f or "当前运行" in f or f == "状态":
                status_field = f
                break
        if not status_field and all_fields:
            for f in all_fields:
                if "总数" not in f:
                    status_field = f
                    break

        log("all_fields: " + str(all_fields) + "  status_field: " + str(status_field))

        data_list = []
        for rec in records:
            fields = rec.get("fields", {})
            rec_total = None
            rec_status = "(空)"
            for k, v in fields.items():
                if "总数" in k:
                    rec_total = v
                    break
            if status_field:
                rec_status = fields.get(status_field, "(空)")
            data_list.append({
                "record_id": rec["record_id"],
                "fields": fields,
                "total": rec_total,
                "status": rec_status,
            })

        log("获取 " + str(len(data_list)) + " 条 (table=" + table_id + ")")
        return {
            "total": len(data_list),
            "records": data_list,
            "all_fields": all_fields,
            "status_field": status_field,
            "update_time": time.strftime("%Y-%m-%d %H:%M:%S"),
        }
    except Exception as e:
        log("请求飞书 API 失败: " + str(e))
        traceback.print_exc()
        return None

def fetch_table_data_quick(table_id):
    """快速获取表格数据（只取第一页，最多500条），适用于首次展示"""
    token = get_token()
    if not token or not table_id:
        return None
    url = ("https://open.feishu.cn/open-apis/bitable/v1/apps/"
            + APP_TOKEN + "/tables/" + table_id + "/records?page_size=500")
    try:
        req = urllib.request.Request(url)
        req.add_header("Authorization", "Bearer " + token)
        with urllib.request.urlopen(req, timeout=15) as resp:
            result = json.loads(resp.read().decode("utf-8"))
        if result.get("code") != 0:
            log("飞书 API 错误: " + str(result.get("msg")))
            return None

        records = result["data"].get("items", [])
        feishu_total = result["data"].get("total", len(records))

        # 只提取字段名（从当前批次就够了）
        all_fields = []
        for rec in records:
            for k in rec.get("fields", {}):
                if k not in all_fields:
                    all_fields.append(k)

        data_list = []
        for rec in records:
            data_list.append({
                "record_id": rec["record_id"],
                "fields": rec.get("fields", {}),
                "total": None,
                "status": "",
            })

        log("快速获取 " + str(len(data_list)) + " 条 (table=" + table_id + ", 共 " + str(feishu_total) + " 条)")
        return {
            "total": feishu_total,
            "records": data_list,
            "all_fields": all_fields,
            "status_field": "",
            "update_time": time.strftime("%Y-%m-%d %H:%M:%S"),
            "has_more": len(data_list) < feishu_total  # 标记是否还有更多数据
        }
    except Exception as e:
        log("快速获取失败: " + str(e))
        return None

# 提取店铺缓存
extract_cache = {"data": {}, "full_data": {}, "lock": threading.Lock()}
EXTRACT_CACHE_TTL = 10
extract_full_fetch_needed = {}  # 标记哪些表需要后台补全

def get_cached_table_all_data(table_id, force_refresh=False):
    """获取指定表格的全量数据（带缓存，首次使用快速模式）"""
    now = time.time()
    with extract_cache["lock"]:
        entry = extract_cache["data"].get(table_id)
        if entry and not force_refresh and (now - entry["time"]) < EXTRACT_CACHE_TTL:
            return entry["data"]

    # 缓存未命中
    # 先尝试快速获取（第一页），立即返回让前端展示
    log("快速获取 " + table_id + "...")
    quick_data = fetch_table_data_quick(table_id)
    if quick_data:
        with extract_cache["lock"]:
            extract_cache["data"][table_id] = {"data": quick_data, "time": now}
        # 标记需要在后台补全
        if quick_data.get("has_more"):
            extract_full_fetch_needed[table_id] = True
            log("标记 " + table_id + " 需要后台补全数据")
        # 如果已经有全量数据，合并之
        with extract_cache["lock"]:
            full = extract_cache["full_data"].get(table_id)
            if full and full.get("data") and full["data"].get("records"):
                full_data = full["data"]
                if len(full_data["records"]) > len(quick_data["records"]):
                    extract_cache["data"][table_id] = {"data": full_data, "time": now}
                    return full_data
        return quick_data

    # 快速失败，尝试全量
    log("快速获取失败，尝试全量 " + table_id + "...")
    data = fetch_table_data(table_id)
    if data:
        with extract_cache["lock"]:
            extract_cache["data"][table_id] = {"data": data, "time": now}
            extract_cache["full_data"][table_id] = {"data": data, "time": now}
        log("缓存更新: " + table_id + " (" + str(len(data.get("records", []))) + " 条)")
    return data

def fetch_full_extract_data(table_id):
    """后台补全提取店铺的全量数据"""
    log("后台补全 " + table_id + "...")
    data = fetch_table_data(table_id)
    if data:
        now = time.time()
        with extract_cache["lock"]:
            extract_cache["full_data"][table_id] = {"data": data, "time": now}
            # 也更新到活跃缓存中
            extract_cache["data"][table_id] = {"data": data, "time": now}
        extract_full_fetch_needed.pop(table_id, None)
        log("后台补全完成: " + table_id + " (" + str(len(data.get("records", []))) + " 条)")
    return data

def get_table_data(device, force_refresh=False):
    table_id = DEVICE_TABLES.get(device)
    if not table_id:
        return None
    now = time.time()
    cache = data_cache.get(device)
    if cache and not force_refresh and (now - cache["last_update"]) < CACHE_TTL:
        return cache["data"]
    new_data = fetch_table_data(table_id)
    if new_data:
        data_cache[device] = {"data": new_data, "last_update": now}
        return new_data
    return cache["data"] if cache else None

# ===== 飞书写入 API 封装 =====

def feishu_api_request(url, body_dict=None, method="POST"):
    """通用飞书 API 请求（POST/PUT/DELETE）"""
    token = get_token()
    if not token:
        log("飞书 API 请求失败: 无 token")
        return None
    data = json.dumps(body_dict).encode("utf-8") if body_dict else None
    req = urllib.request.Request(url, data=data, method=method)
    req.add_header("Authorization", "Bearer " + token)
    req.add_header("Content-Type", "application/json; charset=utf-8")
    try:
        with urllib.request.urlopen(req, timeout=30) as resp:
            result = json.loads(resp.read().decode("utf-8"))
        if result.get("code") != 0:
            log("飞书 API 错误(" + method + "): " + str(result.get("code")) + " " + str(result.get("msg")))
        return result
    except Exception as e:
        log("飞书 API 请求异常(" + method + "): " + str(e))
        return None

def batch_delete_records(table_id, record_ids):
    """批量删除记录（飞书 API 使用 POST 方法）"""
    url = ("https://open.feishu.cn/open-apis/bitable/v1/apps/"
           + APP_TOKEN + "/tables/" + table_id + "/records/batch_delete")
    body = {"records": record_ids}
    result = feishu_api_request(url, body, method="POST")
    if result and result.get("code") == 0:
        log("批量删除成功: " + str(len(record_ids)) + " 条 (table=" + table_id + ")")
        return True
    log("批量删除失败 (table=" + table_id + ")")
    return False

def batch_create_records(table_id, records_data):
    """批量创建记录。records_data = [{"fields": {...}}, ...]"""
    url = ("https://open.feishu.cn/open-apis/bitable/v1/apps/"
           + APP_TOKEN + "/tables/" + table_id + "/records/batch_create")
    body = {"records": records_data}
    result = feishu_api_request(url, body, method="POST")
    if result and result.get("code") == 0:
        log("批量创建成功: " + str(len(records_data)) + " 条 (table=" + table_id + ")")
        return True
    log("批量创建失败 (table=" + table_id + ")")
    return False

def get_machine_id():
    """跨平台获取机器唯一标识"""
    try:
        if sys.platform == "win32":
            output = subprocess.check_output(
                "wmic diskdrive get serialnumber", shell=True, timeout=5
            ).decode("utf-8", errors="ignore")
            lines = output.strip().split("\n")
            for line in lines[1:]:
                line = line.strip()
                if line and len(line) > 10:
                    # 格式化为 0000_0006_2403_0039_3A5A_2703_2000_0157 风格
                    return line.replace(".", "").replace("-", "").replace(" ", "")
        else:
            # Linux
            paths = ["/sys/class/dmi/id/product_serial",
                     "/sys/class/dmi/id/board_serial"]
            for p in paths:
                if os.path.exists(p):
                    with open(p) as f:
                        sid = f.read().strip()
                        if sid and "not" not in sid.lower():
                            return sid
            output = subprocess.check_output(
                ["dmidecode", "-s", "system-serial-number"],
                timeout=5
            ).decode("utf-8", errors="ignore").strip()
            if output and "not" not in output.lower():
                return output
    except Exception:
        pass
    # 兜底：MAC 地址
    import uuid
    return "_".join(
        hex((uuid.getnode() >> (8 * i)) & 0xFF)[2:].zfill(2)
        for i in reversed(range(6))
    ).upper()

# ===== 导出文本生成 =====

# 导出字段定义: [排序位置, 字段标识, [匹配关键词列表]]
# 导出字段定义: (排序, 匹配关键词列表, 排除词列表)
# 排除词：字段名包含这些词的排除（如"邮箱"排除含"api"的字段，防止误配"邮箱api"）
EXPORT_FIELDS = [
    (0, "邮箱账号", ["邮箱账号", "邮箱帐号", "账号"], []),
    (1, "邮箱密码", ["邮箱密码", "密码"], ["api"]),
    (2, "IP地理位置", ["ip地理位置", "ip", "地理位置"], []),
    (3, "邮箱", ["邮箱", "email", "mail"], ["api", "密码", "密码", "账号"]),
    (4, "邮箱api", ["邮箱api", "api"], []),
]
EXPORT_COOKIE_KEYS = ["cookies", "cookie"]

# 排除字段（不导出）：国家、时间、日期等
EXPORT_EXCLUDE_KEYS = [
    "国家", "country", "地区", "region",
    "时间", "日期", "time", "date", "创建", "更新", "修改",
    "created", "updated", "modified",
]

def find_field_by_keywords(all_fields, keywords, exclude_kws=None):
    for f in all_fields:
        fl = f.lower()
        # 先检查排除词
        if exclude_kws:
            excluded = False
            for ek in exclude_kws:
                if ek.lower() in fl:
                    excluded = True
                    break
            if excluded:
                continue
        # 再检查匹配词
        for kw in keywords:
            if kw.lower() in fl:
                return f
    return None

def generate_export_text(records, all_fields):
    """生成导出文本，动态字段自动插入 cookies 前面"""
    # 分出已知字段和未知字段
    known_fields = []   # [(export_order, field_name), ...]
    cookie_field = find_field_by_keywords(all_fields, EXPORT_COOKIE_KEYS)
    matched_set = set()
    for order, name, keywords, exclude_kws in EXPORT_FIELDS:
        f = find_field_by_keywords(all_fields, keywords, exclude_kws)
        if f:
            known_fields.append((order, f))
            matched_set.add(f)

    # 未知字段 = 不在已知列表、不是 cookies、不在排除名单
    def is_excluded(fname):
        fl = fname.lower()
        for kw in EXPORT_EXCLUDE_KEYS:
            if kw.lower() in fl:
                return True
        return False
    unknown_fields = [f for f in all_fields
                      if f not in matched_set and f != cookie_field and not is_excluded(f)]

    # 构建输出字段顺序: 已知(按order) + 未知 + cookies
    known_fields.sort(key=lambda x: x[0])
    ordered_fields = [f for _, f in known_fields] + unknown_fields
    if cookie_field:
        ordered_fields.append(cookie_field)

    lines = []
    for rec in records:
        fields = rec.get("fields", {})
        parts = []
        for fname in ordered_fields:
            val = fields.get(fname, "")
            if val is None:
                val = ""
            elif isinstance(val, list):
                val = ", ".join(
                    v.get("text", v.get("link", str(v)))
                    if isinstance(v, dict) else str(v)
                    for v in val
                )
            elif isinstance(val, dict):
                val = val.get("text", val.get("link", str(val)))
            parts.append(str(val))
        lines.append("----".join(parts))
    return "\n".join(lines)

# ===== 一键扫号 =====

VINTED_OAUTH_URL = "https://www.vinted.fr/oauth/token"
VINTED_OAUTH_BODY = "scope=public&client_id=android&grant_type=password"
VINTED_USER_AGENT = (
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
    "AppleWebKit/537.36 (KHTML, like Gecko) "
    "Chrome/148.0.0.0 Safari/537.36"
)

_scan_state = {"running": False, "progress": 0, "total": 0, "current": "", "status": "",
               "country": "", "log": [], "result": None, "lock": threading.Lock()}

COUNTRY_TO_VINTED = {
    "uk": "co.uk", "英国": "co.uk", "united kingdom": "co.uk", "gb": "co.uk",
    "fr": "fr", "法国": "fr", "france": "fr",
    "de": "de", "德国": "de", "germany": "de",
    "it": "it", "意大利": "it", "italy": "it",
    "es": "es", "西班牙": "es", "spain": "es",
    "nl": "nl", "荷兰": "nl", "netherlands": "nl",
    "us": "com", "美国": "com", "usa": "com", "united states": "com",
    "be": "be", "比利时": "be", "belgium": "be",
    "at": "at", "奥地利": "at", "austria": "at",
    "pl": "pl", "波兰": "pl", "poland": "pl",
}

def clean_email_prefix(email):
    email = str(email or "")
    at = email.find("@")
    if at > 0:
        return re.sub(r"[^a-zA-Z0-9]", "", email[:at])
    return ""

def get_vinted_domain(country_name):
    key = str(country_name or "").strip().lower()
    return COUNTRY_TO_VINTED.get(key, None)

def check_vinted_user(session, token, username, domain):
    """用 cloudscraper session + OAuth token 查用户。返回 {status, message, http_code}"""
    try:
        url = "https://www.vinted." + domain + "/api/v2/users/" + username
        resp = session.get(url, headers={"Authorization": "Bearer " + token}, timeout=15)
        code = resp.status_code

        if code == 404:
            return {"status": "BANNED", "message": "404未找到", "http_code": 404}
        if code == 401:
            return {"status": "ERROR", "message": "401认证失败", "http_code": 401}
        if code == 429:
            time.sleep(1.5)
            resp = session.get(url, headers={"Authorization": "Bearer " + token}, timeout=15)
            code = resp.status_code
        if code != 200:
            return {"status": "ERROR", "message": "HTTP" + str(code), "http_code": code}

        data = resp.json()
        user = data.get("user", data)
        banned = user.get("is_account_banned", False)
        return {
            "status": "BANNED" if banned else "ALIVE",
            "user_id": user.get("id"),
            "is_banned": banned,
            "account_status": user.get("account_status"),
            "ban_date": user.get("account_ban_date"),
            "message": ("封禁" if banned else "存活"),
            "http_code": 200,
        }
    except Exception as e:
        return {"status": "ERROR", "message": str(e)[:80], "http_code": 0}

# 线程安全的计数器和线程本地 session
_scan_counter = 0
_scan_counter_lock = threading.Lock()
_thread_local = threading.local()

def _get_thread_session():
    """每个线程复用自己的 cloudscraper session"""
    if not hasattr(_thread_local, "session"):
        _thread_local.session = cloudscraper.create_scraper(
            browser={"custom": VINTED_USER_AGENT})
    return _thread_local.session

def _do_check_one(idx, rec, email_field, country_field, token, total):
    """单条检测（在线程池中执行）"""
    fields = rec.get("fields", {})
    raw_email = str(fields.get(email_field, ""))
    username = clean_email_prefix(raw_email)
    if not username:
        return None

    raw_country = str(fields.get(country_field, "")) if country_field else ""
    domain = get_vinted_domain(raw_country)
    if not domain:
        domain = "co.uk"

    session = _get_thread_session()
    result = check_vinted_user(session, token, username, domain)
    result["index"] = idx + 1
    result["username"] = username
    result["raw_email"] = raw_email
    result["country"] = raw_country
    result["domain"] = domain

    # 更新进度
    global _scan_counter
    with _scan_counter_lock:
        _scan_counter += 1
        cnt = _scan_counter

    status = result.get("status", "ERROR")
    msg = result.get("message", "")
    log_entry = "[" + str(cnt) + "/" + str(total) + "] " + username + " → " + msg

    with _scan_state["lock"]:
        _scan_state["progress"] = cnt
        _scan_state["current"] = username
        _scan_state["country"] = raw_country
        _scan_state["status"] = status
        _scan_state["log"].append(log_entry)

    log("扫号 " + str(cnt) + "/" + str(total) + ": " + username + " → " + msg)
    return result

def run_scan():
    """多线程并发扫描可提取店铺所有账号"""
    global _scan_counter
    _scan_counter = 0

    try:
        log("一键扫号: 获取 Token...")
        session0 = cloudscraper.create_scraper(browser={"custom": VINTED_USER_AGENT})
        resp = session0.post(VINTED_OAUTH_URL, data=VINTED_OAUTH_BODY,
                             headers={"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8"},
                             timeout=15)
        if resp.status_code != 200:
            raise RuntimeError("Token 获取失败 HTTP" + str(resp.status_code))
        token = resp.json().get("access_token", "")
        if not token:
            raise RuntimeError("Token 为空")
        log("Token 已获取")

        data = get_cached_table_all_data(TABLE_EXTRACTABLE, force_refresh=True)
        if not data or not data.get("records"):
            raise RuntimeError("没有可提取店铺数据")

        records = data["records"]
        all_fields = data.get("all_fields", [])
        email_field = find_field_by_keywords(all_fields, ["邮箱账号", "邮箱帐号"], [])
        if not email_field:
            email_field = find_field_by_keywords(all_fields, ["账号", "email"], ["api"])
        country_field = find_field_by_keywords(all_fields, ["国家", "country", "地区"])
        if not email_field:
            raise RuntimeError("未找到邮箱字段")

        total = len(records)
        with _scan_state["lock"]:
            _scan_state["total"] = total

        # 过滤有效记录
        tasks = []
        for i, rec in enumerate(records):
            fields = rec.get("fields", {})
            raw_email = str(fields.get(email_field, ""))
            username = clean_email_prefix(raw_email)
            if username:
                tasks.append((i, rec))

        country_stats = {}
        details = []

        log("一键扫号: " + str(len(tasks)) + " 条, 3线程并发")
        with _scan_state["lock"]:
            _scan_state["log"].append("3线程并发启动, 创建连接中...")

        with ThreadPoolExecutor(max_workers=3) as executor:
            futures = {}
            for idx, rec in tasks:
                f = executor.submit(_do_check_one, idx, rec, email_field, country_field, token, total)
                futures[f] = idx

            for f in as_completed(futures):
                result = f.result()
                if result is None:
                    continue
                details.append(result)
                status = result.get("status", "ERROR")
                raw_country = result.get("country", "")
                country_key = raw_country.strip() or "未知"
                if country_key not in country_stats:
                    country_stats[country_key] = {"alive": 0, "banned": 0, "error": 0}
                if status == "ALIVE":
                    country_stats[country_key]["alive"] += 1
                elif status == "BANNED":
                    country_stats[country_key]["banned"] += 1
                else:
                    country_stats[country_key]["error"] += 1

        # 按原始顺序排序
        details.sort(key=lambda x: x.get("index", 0))

        # 删除封禁记录，同步飞书
        banned_ids = [r.get("record_id") for r in records
                      if r.get("record_id") and clean_email_prefix(
                          str(r.get("fields", {}).get(email_field, "")))]
        # 从details中筛选出BANNED的记录对应的record_id
        banned_set = set()
        for d in details:
            if d.get("status") == "BANNED":
                # 从records中找到对应记录
                for rec in records:
                    fields = rec.get("fields", {})
                    if clean_email_prefix(str(fields.get(email_field, ""))) == d.get("username", ""):
                        rid = rec.get("record_id", "")
                        if rid:
                            banned_set.add(rid)
                        break

        banned_deleted = 0
        if banned_set:
            log("删除封禁记录: " + str(len(banned_set)) + " 条")
            if batch_delete_records(TABLE_EXTRACTABLE, list(banned_set)):
                banned_deleted = len(banned_set)
                # 清除缓存
                with extract_cache["lock"]:
                    extract_cache["data"].pop(TABLE_EXTRACTABLE, None)
                    extract_cache["full_data"].pop(TABLE_EXTRACTABLE, None)

        final_result = {
            "success": True, "total": total, "scanned": len(details),
            "countries": country_stats, "details": details,
            "banned_deleted": banned_deleted,
        }
        log("一键扫号完成: " + str(len(details)) + " 条, 删除封禁 " + str(banned_deleted) + " 条")
    except Exception as e:
        log("一键扫号异常: " + str(e))
        final_result = {"success": False, "error": str(e)[:200]}
    finally:
        with _scan_state["lock"]:
            _scan_state["running"] = False
            _scan_state["result"] = final_result
            _scan_state["progress"] = _scan_state.get("total", 0)

# ===== 授权码历史存储 =====
LICENSE_HISTORY_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "license_history.json")
LICENSE_REVOKE_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "license_revoke.txt")

def _load_license_history():
    try:
        with open(LICENSE_HISTORY_FILE, "r", encoding="utf-8") as f:
            return json.loads(f.read())
    except Exception:
        return []

def _save_license_history(entry):
    history = _load_license_history()
    history.insert(0, entry)
    _write_license_history(history)

def _write_license_history(history):
    with open(LICENSE_HISTORY_FILE, "w", encoding="utf-8") as f:
        f.write(json.dumps(history, ensure_ascii=False))

def _load_revoke_list():
    try:
        with open(LICENSE_REVOKE_FILE, "r", encoding="utf-8") as f:
            return [l.strip() for l in f.readlines() if l.strip()]
    except Exception:
        return []

def _add_to_revoke(code):
    revoked = _load_revoke_list()
    if code not in revoked:
        revoked.append(code)
        with open(LICENSE_REVOKE_FILE, "w", encoding="utf-8") as f:
            f.write("\n".join(revoked))

# ===== 用户认证模块 =====
import hashlib, base64, hmac as hmac_mod, secrets as sec_mod

AUTH_SECRET = b"VintedGo@2026!AuthSecret#X9"
SESSION_HOURS = 24
USERS_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "users.json")

# 权限列表 —— 后续加功能模块只需在这里加一行
PERM_LIST = [
    ("monitor", "注册实时监控"),
    ("shop", "店铺管理"),
    ("license", "授权码管理"),
    ("users", "用户管理"),
]

def _hash_password(password, salt=None):
    if salt is None:
        salt = sec_mod.token_hex(16)
    h = hashlib.sha256((salt + password).encode("utf-8")).hexdigest()
    return salt + ":" + h

def _check_password(password, stored):
    parts = stored.split(":", 1)
    if len(parts) != 2:
        return False
    salt, expected = parts
    return _hash_password(password, salt).split(":", 1)[1] == expected

_users_cache = None
_users_cache_time = 0

def _load_users():
    global _users_cache, _users_cache_time
    now = time.time()
    if _users_cache is not None and (now - _users_cache_time) < 5:
        return _users_cache
    try:
        with open(USERS_FILE, "r", encoding="utf-8") as f:
            _users_cache = json.loads(f.read())
    except Exception:
        _users_cache = []
    _users_cache_time = now
    return _users_cache

def _save_users(users):
    global _users_cache, _users_cache_time
    _users_cache = users
    _users_cache_time = time.time()
    with open(USERS_FILE, "w", encoding="utf-8") as f:
        f.write(json.dumps(users, ensure_ascii=False, indent=2))

def _init_default_admin():
    users = _load_users()
    if not users:
        default_pwd = _hash_password("Zaq35169286?")
        users.append({
            "username": "yujunqin888",
            "password": default_pwd,
            "role": "admin",
            "permissions": {k: True for k, v in PERM_LIST},
            "created_at": time.strftime("%Y-%m-%d %H:%M:%S"),
            "created_by": "system"
        })
        _save_users(users)
        log("默认管理员已创建: yujunqin888")

def _gen_token(username):
    expiry = int(time.time()) + SESSION_HOURS * 3600
    payload = username + ":" + str(expiry)
    sig = hmac_mod.new(AUTH_SECRET, payload.encode(), hashlib.sha256).hexdigest()[:16]
    token = base64.b64encode((payload + ":" + sig).encode()).decode()
    return token

def _verify_token(token):
    try:
        data = base64.b64decode(token.encode()).decode()
        parts = data.rsplit(":", 1)
        if len(parts) != 2:
            return None
        payload, sig = parts
        expected = hmac_mod.new(AUTH_SECRET, payload.encode(), hashlib.sha256).hexdigest()[:16]
        if not hmac_mod.compare_digest(sig, expected):
            return None
        user_parts = payload.split(":", 1)
        if len(user_parts) != 2:
            return None
        username, expiry_str = user_parts
        expiry = int(expiry_str)
        if time.time() > expiry:
            return None
        users = _load_users()
        for u in users:
            if u["username"] == username:
                u["_token"] = _gen_token(username)
                return u
        return None
    except Exception:
        return None

def _get_user_from_request(handler):
    token = handler.headers.get("X-Auth-Token", "")
    if not token:
        return None
    return _verify_token(token)

def _check_perm(handler, perm):
    user = _get_user_from_request(handler)
    if not user:
        return None
    if user["role"] == "admin":
        return user
    if user.get("permissions", {}).get(perm):
        return user
    return None

def _require_auth(handler):
    user = _get_user_from_request(handler)
    if not user:
        handler.send_response(401)
        handler.send_header("Content-Type", "application/json; charset=utf-8")
        handler.send_header("Access-Control-Allow-Origin", "*")
        handler.end_headers()
        handler.wfile.write(json.dumps({"error": "未登录"}, ensure_ascii=False).encode("utf-8"))
    return user

def _require_perm(handler, perm):
    user = _check_perm(handler, perm)
    if not user:
        handler.send_response(403)
        handler.send_header("Content-Type", "application/json; charset=utf-8")
        handler.send_header("Access-Control-Allow-Origin", "*")
        handler.end_headers()
        handler.wfile.write(json.dumps({"error": "无权限"}, ensure_ascii=False).encode("utf-8"))
    return user

class MyHandler(http.server.SimpleHTTPRequestHandler):
    def do_GET(self):
        if self.path.startswith("/api/data"):
            if not _require_perm(self, "monitor"): return
            self.send_response(200)
            self.send_header("Content-Type", "application/json; charset=utf-8")
            self.send_header("Cache-Control", "no-cache, no-store, must-revalidate")
            self.send_header("Access-Control-Allow-Origin", "*")
            self.end_headers()
            device = None
            if "?" in self.path:
                for p in self.path.split("?")[1].split("&"):
                    if p.startswith("device="):
                        device = urllib.parse.unquote(p.split("=")[1])
            if not device or device not in DEVICE_TABLES:
                self.send_response(404)
                self.send_header("Content-Type", "application/json; charset=utf-8")
                self.send_header("Access-Control-Allow-Origin", "*")
                self.end_headers()
                self.wfile.write(json.dumps({"error": "未知设备，可用: " + ", ".join(DEVICE_TABLES.keys())},
                                           ensure_ascii=False).encode("utf-8"))
                return
            data = get_table_data(device)
            if data:
                self.wfile.write(json.dumps(data, ensure_ascii=False).encode("utf-8"))
                log("/api/data " + str(data["total"]) + " 条 (device=" + device + ")")
            else:
                self.send_response(502)
                self.send_header("Content-Type", "application/json; charset=utf-8")
                self.send_header("Access-Control-Allow-Origin", "*")
                self.end_headers()
                self.wfile.write(json.dumps({"error": "无法获取数据"},
                                           ensure_ascii=False).encode("utf-8"))
            return
        elif self.path.startswith("/api/devices"):
            if not _require_perm(self, "monitor"): return
            self.send_response(200)
            self.send_header("Content-Type", "application/json; charset=utf-8")
            self.send_header("Cache-Control", "no-cache, no-store, must-revalidate")
            self.send_header("Access-Control-Allow-Origin", "*")
            self.end_headers()
            self.wfile.write(json.dumps({"devices": sorted(DEVICE_TABLES.keys())},
                                        ensure_ascii=False).encode("utf-8"))
            return

        # API: 获取可提取店铺数据
        if self.path == "/api/extractable":
            if not _require_perm(self, "shop"): return
            self.send_response(200)
            self.send_header("Content-Type", "application/json; charset=utf-8")
            self.send_header("Cache-Control", "no-cache, no-store, must-revalidate")
            self.send_header("Access-Control-Allow-Origin", "*")
            self.end_headers()
            data = get_cached_table_all_data("tblJsz99omsxQGxG")
            if data:
                self.wfile.write(json.dumps(data, ensure_ascii=False).encode("utf-8"))
            else:
                self.wfile.write(json.dumps({"error": "无法获取可提取店铺数据"}, ensure_ascii=False).encode("utf-8"))
            return

        # API: 获取已提取店铺数据
        if self.path == "/api/extracted":
            if not _require_perm(self, "shop"): return
            self.send_response(200)
            self.send_header("Content-Type", "application/json; charset=utf-8")
            self.send_header("Cache-Control", "no-cache, no-store, must-revalidate")
            self.send_header("Access-Control-Allow-Origin", "*")
            self.end_headers()
            data = get_cached_table_all_data("tblWbQ5JArDJI99v")
            if data:
                self.wfile.write(json.dumps(data, ensure_ascii=False).encode("utf-8"))
            else:
                self.wfile.write(json.dumps({"error": "无法获取已提取店铺数据"}, ensure_ascii=False).encode("utf-8"))
            return
        elif self.path == "/api/scan/progress":
            self.send_response(200)
            self.send_header("Content-Type", "application/json; charset=utf-8")
            self.send_header("Cache-Control", "no-cache, no-store, must-revalidate")
            self.send_header("Access-Control-Allow-Origin", "*")
            self.end_headers()
            with _scan_state["lock"]:
                prog = {
                    "running": _scan_state["running"],
                    "progress": _scan_state["progress"],
                    "total": _scan_state["total"],
                    "current": _scan_state["current"],
                    "status": _scan_state["status"],
                    "country": _scan_state["country"],
                    "log": list(_scan_state["log"]),
                    "result": _scan_state["result"],
                }
            self.wfile.write(json.dumps(prog, ensure_ascii=False).encode("utf-8"))
            return

        elif self.path.startswith("/api/license/check"):
            code = ""
            if "?" in self.path:
                for p in self.path.split("?")[1].split("&"):
                    if p.startswith("code="):
                        code = urllib.parse.unquote(p[5:])
            history = _load_license_history()
            entry = None
            for h in history:
                if h.get("code") == code:
                    entry = h
                    break
            self.send_response(200)
            self.send_header("Content-Type", "application/json; charset=utf-8")
            self.send_header("Cache-Control", "no-cache")
            self.send_header("Access-Control-Allow-Origin", "*")
            self.end_headers()
            if entry:
                expiry_ts = entry.get("expiry_ts", 0)
                remaining = max(0, expiry_ts - int(time.time()))
                self.wfile.write(json.dumps({
                    "valid": True, "expiry_ts": expiry_ts,
                    "remaining_seconds": remaining,
                    "remaining_days": remaining // 86400,
                    "remark": entry.get("remark", ""),
                }, ensure_ascii=False).encode("utf-8"))
            else:
                # 检查是否在撤销列表
                revoked = _load_revoke_list()
                if code in revoked:
                    self.wfile.write(json.dumps({"valid": False, "reason": "已撤销"},
                                                 ensure_ascii=False).encode("utf-8"))
                else:
                    self.wfile.write(json.dumps({"valid": False, "reason": "未找到"},
                                                 ensure_ascii=False).encode("utf-8"))
            return

        elif self.path == "/api/license/revoke":
            self.send_response(200)
            self.send_header("Content-Type", "text/plain; charset=utf-8")
            self.send_header("Cache-Control", "no-cache")
            self.send_header("Access-Control-Allow-Origin", "*")
            self.end_headers()
            revoked = _load_revoke_list()
            self.wfile.write("\n".join(revoked).encode("utf-8"))
            return

        elif self.path == "/api/license/history":
            self.send_response(200)
            self.send_header("Content-Type", "application/json; charset=utf-8")
            self.send_header("Cache-Control", "no-cache")
            self.send_header("Access-Control-Allow-Origin", "*")
            self.end_headers()
            history = _load_license_history()
            self.wfile.write(json.dumps({"success": True, "history": history},
                                         ensure_ascii=False).encode("utf-8"))
            return

        elif self.path == "/api/download":
            with extract_cache["lock"]:
                text = extract_cache.get("last_export_text", "")
                filename = extract_cache.get("last_export_filename", "export.txt")
            if text:
                content = text.encode("utf-8")
                self.send_response(200)
                self.send_header("Content-Type", "text/plain; charset=utf-8")
                self.send_header("Content-Disposition",
                                 "attachment; filename*=UTF-8''" + urllib.parse.quote(filename))
                self.send_header("Content-Length", str(len(content)))
                self.send_header("Cache-Control", "no-cache")
                self.send_header("Access-Control-Allow-Origin", "*")
                self.end_headers()
                self.wfile.write(content)
            else:
                self.send_response(404)
                self.send_header("Content-Type", "application/json; charset=utf-8")
                self.send_header("Access-Control-Allow-Origin", "*")
                self.end_headers()
                self.wfile.write(json.dumps({"error": "没有可下载的文件"},
                                             ensure_ascii=False).encode("utf-8"))
            return
        super().do_GET()
    def do_POST(self):
        if self.path == "/api/extract":
            if not _require_perm(self, "shop"): return
            try:
                content_len = int(self.headers.get("Content-Length", 0))
                body = json.loads(self.rfile.read(content_len).decode("utf-8"))
            except Exception as e:
                self.send_response(400)
                self.send_header("Content-Type", "application/json; charset=utf-8")
                self.send_header("Access-Control-Allow-Origin", "*")
                self.end_headers()
                self.wfile.write(json.dumps({"success": False, "error": "请求解析失败: " + str(e)},
                                             ensure_ascii=False).encode("utf-8"))
                return

            country = body.get("country", "").strip()
            try:
                count = int(body.get("count", 0))
            except (ValueError, TypeError):
                count = 0
            remark = body.get("remark", "").strip()

            if not country or count <= 0:
                self.send_response(400)
                self.send_header("Content-Type", "application/json; charset=utf-8")
                self.send_header("Access-Control-Allow-Origin", "*")
                self.end_headers()
                self.wfile.write(json.dumps({"success": False, "error": "参数无效: 国家和条数必填"},
                                             ensure_ascii=False).encode("utf-8"))
                return

            log("提取请求: 国家=" + country + ", 条数=" + str(count) + ", 备注=" + remark)

            # 1. 获取可提取店铺全量数据
            source_data = get_cached_table_all_data(TABLE_EXTRACTABLE, force_refresh=True)
            if not source_data or not source_data.get("records"):
                self._send_json({"success": False, "error": "无法获取可提取店铺数据"})
                return

            all_records = source_data["records"]
            all_fields = source_data.get("all_fields", [])

            # 2. 找到国家字段
            country_field = find_field_by_keywords(all_fields, ["国家", "country", "地区"])

            # 3. 按国家筛选并取前 count 条
            matched = []
            for rec in all_records:
                if country_field:
                    rec_country = str(rec.get("fields", {}).get(country_field, "")).strip()
                else:
                    rec_country = ""
                if rec_country and rec_country.lower() == country.lower():
                    matched.append(rec)
                if len(matched) >= count:
                    break

            if len(matched) == 0:
                self._send_json({"success": False, "error": "没有找到国家『" + country + "』的记录"})
                return

            actual_count = len(matched)
            log("实际匹配: " + str(actual_count) + " 条")

            # 4. 获取 MAC 地址
            machine_id = get_machine_id()
            log("机器标识: " + machine_id)

            # 5. 构造已提取表格的 5 个字段
            email_field = find_field_by_keywords(all_fields, ["邮箱账号", "邮箱帐号", "账号", "email"])
            pwd_field = find_field_by_keywords(all_fields, ["邮箱密码", "密码", "password"])

            new_records = []
            for rec in matched:
                fields = rec.get("fields", {})
                new_fields = {
                    "提取姓名": remark,
                    "国家": country,
                    "邮箱账号": str(fields.get(email_field, "")) if email_field else "",
                    "邮箱密码": str(fields.get(pwd_field, "")) if pwd_field else "",
                    "MAC地址": machine_id,
                }
                new_records.append({"fields": new_fields})

            # 6. 先生成导出文本（数据未动，生成失败不影响表格）
            export_text = generate_export_text(matched, all_fields)
            ts = time.strftime("%Y%m%d_%H%M%S")
            export_filename = "提取_" + country + "_" + ts + ".txt"
            with extract_cache["lock"]:
                extract_cache["last_export_text"] = export_text
                extract_cache["last_export_filename"] = export_filename
            log("导出文本已生成: " + export_filename)

            # 7. 写入已提取表
            if not batch_create_records(TABLE_EXTRACTED, new_records):
                # 写入失败，数据完整，导出文本保留
                self._send_json({"success": False, "error": "写入已提取表失败",
                                 "export_text": export_text})
                return

            # 8. 删除可提取表中的记录
            record_ids = [r["record_id"] for r in matched]
            if not batch_delete_records(TABLE_EXTRACTABLE, record_ids):
                # 已写入但删除失败，返回导出文本
                with extract_cache["lock"]:
                    extract_cache["data"].pop(TABLE_EXTRACTABLE, None)
                    extract_cache["full_data"].pop(TABLE_EXTRACTABLE, None)
                    extract_cache["data"].pop(TABLE_EXTRACTED, None)
                    extract_cache["full_data"].pop(TABLE_EXTRACTED, None)
                self._send_json({"success": True,
                                 "extracted_count": actual_count,
                                 "export_text": export_text,
                                 "warning": "已提取但原记录删除失败，请手动清理"})
                return

            # 9. 清除缓存
            with extract_cache["lock"]:
                extract_cache["data"].pop(TABLE_EXTRACTABLE, None)
                extract_cache["full_data"].pop(TABLE_EXTRACTABLE, None)
                extract_cache["data"].pop(TABLE_EXTRACTED, None)
                extract_cache["full_data"].pop(TABLE_EXTRACTED, None)

            self._send_json({
                "success": True,
                "extracted_count": actual_count,
                "export_text": export_text,
            })

        elif self.path == "/api/scan":
            if not _require_perm(self, "shop"): return
            log("开始一键扫号...")
            with _scan_state["lock"]:
                if _scan_state["running"]:
                    self._send_json({"success": False, "error": "扫描已在运行中"})
                    return
                # 先清空旧结果，再启动线程
                _scan_state["running"] = True
                _scan_state["progress"] = 0
                _scan_state["total"] = 0
                _scan_state["current"] = ""
                _scan_state["status"] = ""
                _scan_state["country"] = ""
                _scan_state["log"] = ["扫描启动中..."]
                _scan_state["result"] = None
            threading.Thread(target=run_scan, daemon=True).start()
            self._send_json({"success": True, "message": "扫描已启动"})

        elif self.path == "/api/license/generate":
            if not _require_perm(self, "license"): return
            try:
                import license as lic_module
            except ImportError:
                self._send_json({"success": False, "error": "license 模块未安装"})
                return
            try:
                content_len = int(self.headers.get("Content-Length", 0))
                body = json.loads(self.rfile.read(content_len).decode("utf-8"))
            except Exception:
                self._send_json({"success": False, "error": "请求解析失败"})
                return
            try:
                days = int(body.get("days", 0))
            except (ValueError, TypeError):
                days = 0
            remark = body.get("remark", "").strip()
            if days <= 0:
                self._send_json({"success": False, "error": "有效天数必须大于0"})
                return
            now_ts = time.time()
            expiry_ts = now_ts + days * 86400
            expiry_date = time.strftime("%Y-%m-%d",
                          time.localtime(expiry_ts))
            try:
                import secrets
                code = lic_module.generate_license(expiry_date)
                code = code + "-" + secrets.token_hex(4).upper()
                valid, lic_days, err = lic_module.validate_license(code)
                if not valid:
                    self._send_json({"success": False, "error": err})
                    return
                ts = time.strftime("%Y-%m-%d %H:%M:%S")
                expiry_full = time.strftime("%Y-%m-%d %H:%M:%S",
                                              time.localtime(expiry_ts))
                entry = {"code": code, "expiry": expiry_full, "days": lic_days,
                         "remark": remark, "created": ts,
                         "expiry_ts": int(expiry_ts), "input_days": days}
                _save_license_history(entry)
                self._send_json({"success": True, "code": code, "days": lic_days,
                                 "expiry": expiry_full, "created": ts,
                                 "expiry_ts": int(expiry_ts)})
            except Exception as e:
                log("授权码生成失败: " + str(e))
                self._send_json({"success": False, "error": str(e)[:200]})

        elif self.path == "/api/login":
            try:
                content_len = int(self.headers.get("Content-Length", 0))
                body = json.loads(self.rfile.read(content_len).decode("utf-8"))
                username = body.get("username", "").strip()
                password = body.get("password", "")
            except Exception:
                self._send_json({"success": False, "error": "请求解析失败"})
                return
            users = _load_users()
            found = None
            for u in users:
                if u["username"] == username and _check_password(password, u["password"]):
                    found = u
                    break
            if not found:
                self._send_json({"success": False, "error": "用户名或密码错误"})
                return
            token = _gen_token(username)
            self._send_json({"success": True, "token": token, "username": username,
                             "role": found["role"], "permissions": found.get("permissions", {})})

        elif self.path == "/api/perms":
            self.send_response(200)
            self.send_header("Content-Type","application/json; charset=utf-8")
            self.send_header("Access-Control-Allow-Origin","*"); self.end_headers()
            self.wfile.write(json.dumps({"perms": [{"key":k,"name":v} for k,v in PERM_LIST]},
                                         ensure_ascii=False).encode("utf-8")); return

        elif self.path == "/api/users/list":
            user = _require_auth(self)
            if not user: return
            if user["role"] != "admin":
                self.send_response(403); self.send_header("Content-Type","application/json; charset=utf-8")
                self.send_header("Access-Control-Allow-Origin","*"); self.end_headers()
                self.wfile.write(json.dumps({"error":"无权限"},ensure_ascii=False).encode("utf-8")); return
            users = _load_users()
            safe = []
            for u in users:
                safe.append({"username":u["username"],"role":u["role"],
                            "permissions":u.get("permissions",{}),
                            "created_at":u.get("created_at","")})
            self._send_json({"success":True,"users":safe})

        elif self.path == "/api/users/create":
            user = _require_auth(self)
            if not user: return
            if user["role"] != "admin":
                self.send_response(403); self.send_header("Content-Type","application/json; charset=utf-8")
                self.send_header("Access-Control-Allow-Origin","*"); self.end_headers()
                self.wfile.write(json.dumps({"error":"无权限"},ensure_ascii=False).encode("utf-8")); return
            try:
                content_len = int(self.headers.get("Content-Length", 0))
                body = json.loads(self.rfile.read(content_len).decode("utf-8"))
                new_username = body.get("username","").strip()
                new_password = body.get("password","")
                if not new_username or not new_password:
                    self._send_json({"success":False,"error":"用户名和密码必填"}); return
            except Exception:
                self._send_json({"success":False,"error":"请求解析失败"}); return
            users = _load_users()
            for u in users:
                if u["username"] == new_username:
                    self._send_json({"success":False,"error":"用户名已存在"}); return
            default_perms = {k: True for k, v in PERM_LIST if k != "users"}
            users.append({"username":new_username,"password":_hash_password(new_password),
                         "role":"user","permissions":default_perms,
                         "created_at":time.strftime("%Y-%m-%d %H:%M:%S"),"created_by":user["username"]})
            _save_users(users); log("用户已创建: "+new_username)
            self._send_json({"success":True})

        elif self.path == "/api/users/update":
            user = _require_auth(self)
            if not user: return
            if user["role"] != "admin":
                self.send_response(403); self.send_header("Content-Type","application/json; charset=utf-8")
                self.send_header("Access-Control-Allow-Origin","*"); self.end_headers()
                self.wfile.write(json.dumps({"error":"无权限"},ensure_ascii=False).encode("utf-8")); return
            try:
                content_len = int(self.headers.get("Content-Length", 0))
                body = json.loads(self.rfile.read(content_len).decode("utf-8"))
                target = body.get("username","").strip()
                perms = body.get("permissions",None)
            except Exception:
                self._send_json({"success":False,"error":"请求解析失败"}); return
            users = _load_users(); found = False
            for u in users:
                if u["username"] == target:
                    if perms is not None: u["permissions"] = perms
                    found = True; break
            if not found: self._send_json({"success":False,"error":"用户不存在"}); return
            _save_users(users); log("用户权限更新: "+target)
            self._send_json({"success":True})

        elif self.path == "/api/users/delete":
            user = _require_auth(self)
            if not user: return
            if user["role"] != "admin":
                self.send_response(403); self.send_header("Content-Type","application/json; charset=utf-8")
                self.send_header("Access-Control-Allow-Origin","*"); self.end_headers()
                self.wfile.write(json.dumps({"error":"无权限"},ensure_ascii=False).encode("utf-8")); return
            try:
                content_len = int(self.headers.get("Content-Length", 0))
                body = json.loads(self.rfile.read(content_len).decode("utf-8"))
                target = body.get("username","").strip()
            except Exception:
                self._send_json({"success":False,"error":"请求解析失败"}); return
            if target == user["username"]: self._send_json({"success":False,"error":"不能删除自己"}); return
            users = _load_users(); users = [u for u in users if u["username"] != target]
            _save_users(users); log("用户已删除: "+target)
            self._send_json({"success":True})

        elif self.path == "/api/license/extend":
            try:
                content_len = int(self.headers.get("Content-Length", 0))
                body = json.loads(self.rfile.read(content_len).decode("utf-8"))
            except Exception:
                self._send_json({"success": False, "error": "请求解析失败"})
                return
            code = body.get("code", "").strip()
            try:
                add_days = int(body.get("days", 0))
            except (ValueError, TypeError):
                add_days = 0
            if not code or add_days <= 0:
                self._send_json({"success": False, "error": "参数无效"})
                return
            history = _load_license_history()
            found = False
            for h in history:
                if h.get("code") == code:
                    old_ts = h.get("expiry_ts", 0)
                    new_ts = (max(time.time(), old_ts) if old_ts else time.time()) + add_days * 86400
                    h["expiry_ts"] = int(new_ts)
                    h["expiry"] = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(new_ts))
                    h["days"] = h.get("days", 0) + add_days
                    found = True
                    break
            if not found:
                self._send_json({"success": False, "error": "授权码不存在"})
                return
            _write_license_history(history)
            log("授权码已延期: " + code + " +" + str(add_days) + "天")
            self._send_json({"success": True})

        elif self.path == "/api/license/delete":
            try:
                content_len = int(self.headers.get("Content-Length", 0))
                body = json.loads(self.rfile.read(content_len).decode("utf-8"))
            except Exception:
                self._send_json({"success": False, "error": "请求解析失败"})
                return
            code = body.get("code", "").strip()
            if not code:
                self._send_json({"success": False, "error": "授权码必填"})
                return
            history = _load_license_history()
            history = _load_license_history()
            history = [h for h in history if h.get("code") != code]
            _write_license_history(history)
            _add_to_revoke(code)
            log("授权码已撤销: " + code)
            self._send_json({"success": True})

        else:
            self.send_response(404)
            self.send_header("Content-Type", "application/json; charset=utf-8")
            self.send_header("Access-Control-Allow-Origin", "*")
            self.end_headers()
            self.wfile.write(json.dumps({"success": False, "error": "未知接口"},
                                         ensure_ascii=False).encode("utf-8"))

    def _send_json(self, data):
        self.send_response(200)
        self.send_header("Content-Type", "application/json; charset=utf-8")
        self.send_header("Cache-Control", "no-cache, no-store, must-revalidate")
        self.send_header("Access-Control-Allow-Origin", "*")
        self.end_headers()
        self.wfile.write(json.dumps(data, ensure_ascii=False).encode("utf-8"))

    def log_message(self, format, *args):
        pass

_shutdown = threading.Event()

def start_background_update():
    while not _shutdown.is_set():
        _shutdown.wait(30)
        if _shutdown.is_set():
            break
        for d in list(DEVICE_TABLES.keys()):
            get_table_data(d, force_refresh=True)
        # 提取店铺：如果有需要补全的表，先补全
        for tid in list(extract_full_fetch_needed.keys()):
            fetch_full_extract_data(tid)
        # 每30秒刷新一次缓存（全量）
        for tid in [TABLE_EXTRACTABLE, TABLE_EXTRACTED]:
            get_cached_table_all_data(tid, force_refresh=True)

if __name__ == "__main__":
    print("=" * 60)
    print("  飞书多维表格实时监控服务器")
    print("=" * 60)
    _init_default_admin()
    if not refresh_token():
        print("[错误] 无法获取飞书 Token")
        input("按回车键退出...")
        exit(1)
    print("正在自动发现实时监控表...")
    discover_tables()
    print("已发现设备: " + ", ".join(DEVICE_TABLES.keys()))
    for d in list(DEVICE_TABLES.keys()):
        print("正在加载 " + d + " 数据...")
        get_table_data(d, force_refresh=True)
    bg = threading.Thread(target=start_background_update, daemon=True)
    bg.start()
    print("后台定时更新已启动（每 30 秒）")
    # 后台预取提取店铺数据（快速模式）
    for tid in [TABLE_EXTRACTABLE, TABLE_EXTRACTED]:
        threading.Thread(target=get_cached_table_all_data, args=(tid, True), daemon=True).start()
    script_dir = os.path.dirname(os.path.abspath(__file__))
    os.chdir(script_dir)
    print("工作目录: " + script_dir)
    try:
        socketserver.ThreadingTCPServer.allow_reuse_address = True
        httpd = socketserver.ThreadingTCPServer(("0.0.0.0", PORT), MyHandler)
        print("服务器已启动: http://localhost:" + str(PORT))
        print("查看页面: http://localhost:" + str(PORT) + "/admin.html")
        print("按 Ctrl+C 停止")
        httpd.serve_forever()
    except KeyboardInterrupt:
        print("\n正在停止服务器...")
        _shutdown.set()
        httpd.shutdown()
        print("服务器已停止")
    except Exception as e:
        print("[错误] 服务器启动失败: " + str(e))
        input("按回车键退出...")