From b2823713c9e11042b958f98af2a39456d1d59db8 Mon Sep 17 00:00:00 2001 From: dekun <253921841@qq.com> Date: Thu, 21 May 2026 17:51:21 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=87=E6=A1=A3=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 +- crypto_monitor_binance/.env.example | 18 +++----- crypto_monitor_gate/.env.example | 9 ++-- crypto_monitor_gate/README.md | 2 +- crypto_monitor_gate_bot/.env.example | 9 ++-- crypto_monitor_okx/.env.example | 9 ++-- docs/授权配置说明.md | 62 ++++++++++++++++++++++++++++ license.env | 10 +++++ license_lib.py | 56 +++++++++++++++---------- 9 files changed, 126 insertions(+), 53 deletions(-) create mode 100644 docs/授权配置说明.md create mode 100644 license.env diff --git a/README.md b/README.md index c79ea7e..28821ca 100644 --- a/README.md +++ b/README.md @@ -98,13 +98,15 @@ cd crypto_monitor_user ## 六、整机许可(月卡 / 季卡 / 年卡) +- 云端授权地址(已预置):**https://sq.bz121.com/** — 配置见仓库根目录 **`license.env`**(四个子项目启动时自动加载)。 +- 服务器 `crypto_monitor_web` 的 `CLIENT_API_KEY` 须与 `license.env` 里 `LICENSE_CLIENT_KEY` **完全一致**。 - 用户自备电脑部署;**整机**一个设备 ID,四个 `crypto_monitor_*` 共用仓库根目录 **`.license/`** 缓存。 - 未授权或已过期:关键位监控、复盘、统计等 **全部不可用**(后台监控线程亦停止)。 - 每 **3** 天联网校验一次(`LICENSE_CHECK_INTERVAL_DAYS`);断网宽限默认 **7** 天。 - 购买:微信 **`dekun03`**,添加好友时 **备注必须填写设备 ID**(打开任意实例的 **`/license`** 页复制)。 - 定价:月卡 ¥199 / 季卡 ¥399 / 年卡 ¥699;续费在剩余天数上叠加(由云端实现)。 - 签发与管理 Web 为 **独立云端项目**,不在本仓库;用户端 API 约定见 **[docs/LICENSE_API.md](./docs/LICENSE_API.md)**。 -- 配置:各子目录 `.env` 或仓库根 `.env` 中的 `LICENSE_API_URL`、`LICENSE_CLIENT_KEY` 等(见各 `.env.example`)。本地开发可设 `LICENSE_DISABLED=true`。 +- 配置:优先 **`license.env`**(可提交 Git);本地覆盖可写仓库根 `.env` 的 `LICENSE_*`。本地调试可设 `LICENSE_DISABLED=true`。 --- diff --git a/crypto_monitor_binance/.env.example b/crypto_monitor_binance/.env.example index 7f9c542..fa2aba6 100644 --- a/crypto_monitor_binance/.env.example +++ b/crypto_monitor_binance/.env.example @@ -29,18 +29,12 @@ APP_AUTH_DISABLED=true # Flask 会话密钥(必须替换为长随机字符串) FLASK_SECRET_KEY=CHANGE_TO_LONG_RANDOM_SECRET -# ---------- 整机许可(也可写在仓库根目录 .env 的 LICENSE_* 变量)---------- -# 云端授权服务根地址(独立项目部署,见 docs/LICENSE_API.md) -LICENSE_API_URL=https://license.example.com -LICENSE_CLIENT_KEY=REPLACE_WITH_CLIENT_KEY -# 联网校验间隔(天),默认 3 -LICENSE_CHECK_INTERVAL_DAYS=3 -# 断网后仍可使用的天数(基于上次校验成功时间),默认 7 -LICENSE_OFFLINE_GRACE_DAYS=7 -# 购买联系微信;添加好友时备注请填写设备 ID(见 /license 页) -LICENSE_WECHAT_ID=dekun03 -# LICENSE_WECHAT_REMARK=自定义备注文案 -# 仅本地开发可设为 true,跳过许可 +# ---------- 整机许可(默认由仓库根目录 license.env 提供,此处可留空或覆盖)---------- +# LICENSE_API_URL=https://sq.bz121.com +# LICENSE_CLIENT_KEY=见 license.env,须与云端 CLIENT_API_KEY 一致 +# LICENSE_CHECK_INTERVAL_DAYS=3 +# LICENSE_OFFLINE_GRACE_DAYS=7 +# LICENSE_WECHAT_ID=dekun03 # LICENSE_DISABLED=false # 企业微信机器人 Webhook(用于行情/风控推送) diff --git a/crypto_monitor_gate/.env.example b/crypto_monitor_gate/.env.example index 1afa85a..262286f 100644 --- a/crypto_monitor_gate/.env.example +++ b/crypto_monitor_gate/.env.example @@ -29,12 +29,9 @@ APP_AUTH_DISABLED=true # Flask 会话密钥(必须替换为长随机字符串) FLASK_SECRET_KEY=CHANGE_TO_LONG_RANDOM_SECRET -# ---------- 整机许可(也可写在仓库根目录 .env 的 LICENSE_* 变量)---------- -LICENSE_API_URL=https://license.example.com -LICENSE_CLIENT_KEY=REPLACE_WITH_CLIENT_KEY -LICENSE_CHECK_INTERVAL_DAYS=3 -LICENSE_OFFLINE_GRACE_DAYS=7 -LICENSE_WECHAT_ID=dekun03 +# ---------- 整机许可(默认由仓库根目录 license.env 提供)---------- +# LICENSE_API_URL=https://sq.bz121.com +# LICENSE_CLIENT_KEY=见 license.env # LICENSE_DISABLED=false # 企业微信机器人 Webhook(用于行情/风控推送) diff --git a/crypto_monitor_gate/README.md b/crypto_monitor_gate/README.md index 183f8c4..53a002c 100644 --- a/crypto_monitor_gate/README.md +++ b/crypto_monitor_gate/README.md @@ -10,7 +10,7 @@ | **[关键位自动下单说明.md](./关键位自动下单说明.md)** | 关键位自动开仓的 RR、止盈止损、结案原因与 `.env` | | **[部署文档.md](./部署文档.md)** | Ubuntu、PM2、**SSH SOCKS** 访问 Gate API 等 | -另:**Binance U 本位** 对等实现见同级的 **`crypto_monitor_binance`** 仓库。 +另:**Binance U 本位** 对等实现见同级的 **`crypto_monitor_binance`** 目录。 --- diff --git a/crypto_monitor_gate_bot/.env.example b/crypto_monitor_gate_bot/.env.example index 8202a73..c4dadc7 100644 --- a/crypto_monitor_gate_bot/.env.example +++ b/crypto_monitor_gate_bot/.env.example @@ -29,12 +29,9 @@ APP_AUTH_DISABLED=true # Flask 会话密钥(必须替换为长随机字符串) FLASK_SECRET_KEY=CHANGE_TO_LONG_RANDOM_SECRET -# ---------- 整机许可(也可写在仓库根目录 .env 的 LICENSE_* 变量)---------- -LICENSE_API_URL=https://license.example.com -LICENSE_CLIENT_KEY=REPLACE_WITH_CLIENT_KEY -LICENSE_CHECK_INTERVAL_DAYS=3 -LICENSE_OFFLINE_GRACE_DAYS=7 -LICENSE_WECHAT_ID=dekun03 +# ---------- 整机许可(默认由仓库根目录 license.env 提供)---------- +# LICENSE_API_URL=https://sq.bz121.com +# LICENSE_CLIENT_KEY=见 license.env # LICENSE_DISABLED=false # 企业微信机器人 Webhook(用于行情/风控推送) diff --git a/crypto_monitor_okx/.env.example b/crypto_monitor_okx/.env.example index e3885a6..56e13a0 100644 --- a/crypto_monitor_okx/.env.example +++ b/crypto_monitor_okx/.env.example @@ -29,12 +29,9 @@ APP_AUTH_DISABLED=true # Flask 会话密钥(必须替换为长随机字符串) FLASK_SECRET_KEY=CHANGE_TO_LONG_RANDOM_SECRET -# ---------- 整机许可(也可写在仓库根目录 .env 的 LICENSE_* 变量)---------- -LICENSE_API_URL=https://license.example.com -LICENSE_CLIENT_KEY=REPLACE_WITH_CLIENT_KEY -LICENSE_CHECK_INTERVAL_DAYS=3 -LICENSE_OFFLINE_GRACE_DAYS=7 -LICENSE_WECHAT_ID=dekun03 +# ---------- 整机许可(默认由仓库根目录 license.env 提供)---------- +# LICENSE_API_URL=https://sq.bz121.com +# LICENSE_CLIENT_KEY=见 license.env # LICENSE_DISABLED=false # 企业微信机器人 Webhook(用于行情/风控推送) diff --git a/docs/授权配置说明.md b/docs/授权配置说明.md new file mode 100644 index 0000000..7c59703 --- /dev/null +++ b/docs/授权配置说明.md @@ -0,0 +1,62 @@ +# 用户端授权配置(crypto_monitor_user) + +## 已预置 + +仓库根目录 **[license.env](../license.env)** 已写好: + +| 变量 | 值 | +|------|-----| +| `LICENSE_API_URL` | `https://sq.bz121.com` | +| `LICENSE_CHECK_INTERVAL_DAYS` | `3` | +| `LICENSE_OFFLINE_GRACE_DAYS` | `7` | +| `LICENSE_WECHAT_ID` | `dekun03` | + +四个 `crypto_monitor_*` 启动时会通过 `license_lib.load_shared_env()` **自动加载**,无需在每个子目录重复配置。 + +## 你必须做的一步(云端密钥对齐) + +在授权服务器上查看: + +```bash +grep '^CLIENT_API_KEY=' /opt/crypto_monitor_web/.env +``` + +把输出值改成与 **`license.env`** 中一致: + +```env +LICENSE_CLIENT_KEY=cm_user_sq_bz121_8f3a2c1d9e4b7a6f5d0e1b2c3a4f5e6 +``` + +**方式 A(推荐)**:改服务器,与用户端 `license.env` 一致: + +```bash +sudo nano /opt/crypto_monitor_web/.env +# CLIENT_API_KEY=cm_user_sq_bz121_8f3a2c1d9e4b7a6f5d0e1b2c3a4f5e6 +sudo systemctl restart crypto-monitor-web +``` + +**方式 B**:若服务器已用安装脚本生成过随机密钥,则把该密钥复制到用户端 `license.env` 的 `LICENSE_CLIENT_KEY=` 一行。 + +## 本地覆盖(可选) + +仓库根 `.env`(不提交 Git)可写: + +```env +LICENSE_CLIENT_KEY=你的密钥 +``` + +会覆盖 `license.env` 中的同名项。 + +## 验证 + +1. 启动任意子项目,浏览器打开 `http://127.0.0.1:端口/license` +2. 应能看到设备 ID,且不再提示「未配置 LICENSE_API_URL」 +3. 在 https://sq.bz121.com/admin/ 生成激活码并兑换 + +## 关闭许可(仅开发) + +在 `license.env` 或根 `.env` 中设置: + +```env +LICENSE_DISABLED=true +``` diff --git a/license.env b/license.env new file mode 100644 index 0000000..1da8858 --- /dev/null +++ b/license.env @@ -0,0 +1,10 @@ +# 整机许可(仓库根目录,四个 crypto_monitor_* 共用) +# 授权服务: https://sq.bz121.com/ +# 服务器 /opt/crypto_monitor_web/.env 中 CLIENT_API_KEY 须与本文件 LICENSE_CLIENT_KEY 完全一致 + +LICENSE_API_URL=https://sq.bz121.com +LICENSE_CLIENT_KEY=cm_user_sq_bz121_8f3a2c1d9e4b7a6f5d0e1b2c3a4f5e6 +LICENSE_CHECK_INTERVAL_DAYS=3 +LICENSE_OFFLINE_GRACE_DAYS=7 +LICENSE_WECHAT_ID=dekun03 +LICENSE_DISABLED=false diff --git a/license_lib.py b/license_lib.py index 4a28767..989a9a5 100644 --- a/license_lib.py +++ b/license_lib.py @@ -24,24 +24,7 @@ _TEMPLATE_DIR = os.path.join(_REPO_ROOT, "license_templates") BJ_TZ = timezone(timedelta(hours=8)) -def load_shared_env() -> None: - """从仓库根目录 .env 加载 LICENSE_*(子项目 .env 已存在同名变量时不覆盖)。""" - path = os.path.join(_REPO_ROOT, ".env") - if not os.path.isfile(path): - return - try: - raw_bytes = open(path, "rb").read() - except OSError: - return - text = "" - for enc in ("utf-8-sig", "utf-16", "utf-16-le", "utf-16-be"): - try: - text = raw_bytes.decode(enc) - break - except Exception: - continue - if not text: - text = raw_bytes.decode("utf-8", errors="ignore") +def _apply_env_lines(text: str, *, license_only: bool, override: bool) -> None: text = text.replace("\x00", "") for line in text.splitlines(): raw = line.strip() @@ -49,14 +32,45 @@ def load_shared_env() -> None: continue key, value = raw.split("=", 1) clean_key = key.strip().lstrip("\ufeff") - if not clean_key.startswith("LICENSE_"): - continue - if clean_key in os.environ and (os.environ.get(clean_key) or "").strip(): + if license_only and not clean_key.startswith("LICENSE_"): continue clean_value = value.strip().strip('"').strip("'") + if not clean_value: + continue + if not override and clean_key in os.environ and (os.environ.get(clean_key) or "").strip(): + continue os.environ[clean_key] = clean_value +def _read_env_file(path: str) -> str: + if not os.path.isfile(path): + return "" + try: + raw_bytes = open(path, "rb").read() + except OSError: + return "" + for enc in ("utf-8-sig", "utf-16", "utf-16-le", "utf-16-be"): + try: + return raw_bytes.decode(enc) + except Exception: + continue + return raw_bytes.decode("utf-8", errors="ignore") + + +def load_shared_env() -> None: + """ + 加载顺序(后加载的可补空位): + 1. 仓库根 license.env — 默认授权地址 https://sq.bz121.com + 2. 仓库根 .env 中的 LICENSE_* — 可覆盖密钥等(勿提交 Git) + 子目录 .env 若已先加载且已有 LICENSE_*,则不被覆盖。 + """ + license_path = os.path.join(_REPO_ROOT, "license.env") + _apply_env_lines(_read_env_file(license_path), license_only=True, override=True) + + root_env = os.path.join(_REPO_ROOT, ".env") + _apply_env_lines(_read_env_file(root_env), license_only=True, override=True) + + def _env_bool(name: str, default: bool = False) -> bool: return (os.getenv(name) or "").strip().lower() in ("1", "true", "yes", "on")