更新小程序隐私保护机制,新增手机号一键登录功能,用户需同意隐私协议后方可获取手机号。优化多个页面的登录交互,提升用户体验。调整相关配置以支持新功能。
This commit is contained in:
0
.cursor/agent/安全工程师/evolution/.gitkeep
Normal file
0
.cursor/agent/安全工程师/evolution/.gitkeep
Normal file
32
.cursor/agent/安全工程师/evolution/2026-03-20-挖矿与服务器Skills.md
Normal file
32
.cursor/agent/安全工程师/evolution/2026-03-20-挖矿与服务器Skills.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# 挖矿病毒排查与服务器操作 Skills 创建
|
||||
|
||||
**日期**:2026-03-20
|
||||
|
||||
## 背景
|
||||
|
||||
基于 agent 记录(3b9e0fa0、bc781e1b、1c1a81c3 等)中挖矿病毒排查经验,以及本地部署脚本(devloy.py、master.py、soul-admin/deploy.py、Cunkebao/miner_guard_install.py 等),将经验吸收转化为 Skills。
|
||||
|
||||
## 新增 Skills
|
||||
|
||||
### 1. security-miner-guard
|
||||
|
||||
- **路径**:`.cursor/skills/security-miner-guard/SKILL.md`
|
||||
- **触发词**:挖矿病毒、xmrig、服务器被入侵、miner_guard、安全排查、杀挖矿
|
||||
- **内容**:挖矿病毒特征、入侵链路、排查脚本、加固建议、miner_guard 安装与检查
|
||||
|
||||
### 2. security-server-ops
|
||||
|
||||
- **路径**:`.cursor/skills/security-server-ops/SKILL.md`
|
||||
- **触发词**:部署、服务器操作、SSH、宝塔、devloy、master、Cunkebao 部署
|
||||
- **内容**:服务器索引、部署脚本索引、环境变量一览、常用操作(不含明文密码)
|
||||
|
||||
## 配置更新
|
||||
|
||||
- `paths.py`:新增 `AGENT_SECURITY`、`ROLE_TO_AGENT["安全工程师"]`
|
||||
- `老板分身-索引.mdc`:经验自动收集推断增加「挖矿/安全/服务器操作→安全工程师」
|
||||
- `soul-project-boundary.mdc`:按语义触发词增加安全工程师及对应 Skills
|
||||
|
||||
## 安全提醒
|
||||
|
||||
- Skills 中**不写入明文密码**,仅说明配置来源(环境变量、脚本 get_cfg())
|
||||
- 建议将 master.py、devloy.py 等中的默认密码迁移到环境变量
|
||||
24
.cursor/agent/安全工程师/evolution/2026-03-20-部署管理端触发词.md
Normal file
24
.cursor/agent/安全工程师/evolution/2026-03-20-部署管理端触发词.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# 部署管理端触发词约定
|
||||
|
||||
**日期**:2026-03-20
|
||||
|
||||
## 场景
|
||||
|
||||
用户说「帮我部署管理端到线上」时,安全工程师应直接执行部署脚本,无需再询问或选择。
|
||||
|
||||
## 解决方案
|
||||
|
||||
- **触发词**:帮我部署管理端到线上
|
||||
- **动作**:直接执行 `cd soul-api && python master.py`
|
||||
- **脚本**:`soul-api/master.py`(soul-api 正式环境部署)
|
||||
|
||||
## 已升级 Skills
|
||||
|
||||
1. **security-server-ops**:何时使用表新增该触发词,明确直接执行命令
|
||||
2. **soul-project-boundary**:按场景触发词表新增,加载 security-server-ops 后执行
|
||||
|
||||
## 说明
|
||||
|
||||
- `master.py` 部署的是 soul-api(Go 后端),非 soul-admin 前端
|
||||
- 若需部署 soul-admin 前端,应使用 `soul-admin/deploy.py`
|
||||
- 用户明确指定「管理端」对应 master.py,按约定执行
|
||||
8
.cursor/agent/安全工程师/evolution/索引.md
Normal file
8
.cursor/agent/安全工程师/evolution/索引.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# 安全工程师 经验索引
|
||||
|
||||
> 挖矿病毒排查、服务器加固、部署与运维相关经验。
|
||||
|
||||
| 日期 | 摘要 | 文件 |
|
||||
|------|------|------|
|
||||
| 2026-03-20 | 挖矿病毒排查经验转化为 Skills;服务器操作 Skill 创建 | 2026-03-20-挖矿与服务器Skills.md |
|
||||
| 2026-03-20 | 「帮我部署管理端到线上」→ 执行 soul-api/master.py | 2026-03-20-部署管理端触发词.md |
|
||||
@@ -54,6 +54,7 @@
|
||||
| 2026-03-18 | 小程序、团队 | 业务规则/最佳实践 | - | 分享链路兼容好友/朋友圈 singlePage:单页模式能力降级(不支付/不自动领取),引导点击底部“前往小程序”进入完整版 |
|
||||
| 2026-03-18 | 产品、后端、管理端、测试 | 文档归档/需求口径 | - | 文档归档整理:以《以界面定需求》为基准,各角色重整“功能需求+验收口径+风险点”并写入各自经验库;补齐《项目落地推进表》 |
|
||||
| 2026-03-19 | 小程序 | 最佳实践 | miniprogram-dev SKILL §11 | 原生按钮覆盖定位:chooseAvatar 等用绝对定位 overlay 覆盖,禁止 button 包裹,避免原生样式影响(灰色矩形等) |
|
||||
| 2026-03-20 | 安全工程师 | 触发词约定 | security-server-ops、soul-project-boundary | 「帮我部署管理端到线上」→ 直接执行 soul-api/master.py |
|
||||
|
||||
---
|
||||
|
||||
@@ -64,4 +65,4 @@
|
||||
|
||||
---
|
||||
|
||||
**最后更新**:2026-03-18
|
||||
**最后更新**:2026-03-20
|
||||
|
||||
@@ -31,9 +31,10 @@ Soul 创业派对全项目架构与约定:路由隔离(miniprogram/admin/db
|
||||
| 2026-03-17 | 性能优化与 Redis 缓存方案落地:Redis 容灾回退 DB、OSS 上传容灾;/health 返回 database/redis 状态 | 已完成 |
|
||||
| 2026-03-18 | 吸收经验:分享进入链路需兼容朋友圈 singlePage;单页模式不执行支付/自动领取等强动作并引导“前往小程序” | 已完成 |
|
||||
| 2026-03-18 | 会议:超级个体开通后自动创建@人统一走 Person;幂等键绑定 userId;默认资料 flags 后端输出 | 已完成 |
|
||||
| 2026-03-20 | 「帮我部署管理端到线上」→ 安全工程师执行 soul-api/master.py;security-server-ops、soul-project-boundary 触发词升级 | 已完成 |
|
||||
|
||||
> **格式说明**:每次架构级讨论后在此追加一行,日期格式 YYYY-MM-DD
|
||||
|
||||
---
|
||||
|
||||
**最后更新**:2026-03-18
|
||||
**最后更新**:2026-03-20
|
||||
|
||||
@@ -53,6 +53,7 @@ AGENT_BACKEND = AGENT / "后端工程师"
|
||||
AGENT_PRODUCT = AGENT / "产品经理"
|
||||
AGENT_TEST = AGENT / "软件测试"
|
||||
AGENT_TEAM = AGENT / "团队"
|
||||
AGENT_SECURITY = AGENT / "安全工程师"
|
||||
|
||||
# ========== 常用文件 ==========
|
||||
RULE_MAIN = RULES / "老板分身-索引.mdc"
|
||||
@@ -80,6 +81,9 @@ ROLE_TO_AGENT = {
|
||||
"软件测试": "软件测试",
|
||||
"测试": "软件测试",
|
||||
"测试人员": "软件测试",
|
||||
# 安全
|
||||
"安全工程师": "安全工程师",
|
||||
"安全": "安全工程师",
|
||||
# 通用
|
||||
"团队": "团队",
|
||||
}
|
||||
|
||||
@@ -50,6 +50,7 @@ alwaysApply: true
|
||||
| 小程序、miniprogram、C 端、微信小程序 | 小程序开发工程师 | `e:\Gongsi\Mycontent\.cursor\skills\miniprogram-dev\SKILL.md` |
|
||||
| 产品、需求、验收、排期、需求文档 | 产品经理 | `e:\Gongsi\Mycontent\.cursor\skills\product-manager\SKILL.md` |
|
||||
| 测试、测试用例、回归测试、功能测试、QA | 测试人员 | `e:\Gongsi\Mycontent\.cursor\skills\testing\SKILL.md` |
|
||||
| 挖矿、安全、服务器操作、部署、miner_guard、xmrig、入侵排查 | 安全工程师 | `e:\Gongsi\Mycontent\.cursor\skills\security-miner-guard\SKILL.md`、`e:\Gongsi\Mycontent\.cursor\skills\security-server-ops\SKILL.md` |
|
||||
|
||||
### 按场景触发词
|
||||
|
||||
@@ -63,5 +64,6 @@ alwaysApply: true
|
||||
| 会议结束、散会、会开完了 | `e:\Gongsi\Mycontent\.cursor\skills\assistant-doc-sync\SKILL.md`(会议收尾) |
|
||||
| **加个需求**、加个需求:xxx | `e:\Gongsi\Mycontent\.cursor\skills\product-manager\SKILL.md`(产品经理三端分析 → 功能规划 → 指派) |
|
||||
| **新版分析**、版本对比、迁移分析、甲方代码分析、快速分析新版、抽取需求 | `e:\Gongsi\Mycontent\.cursor\skills\new-version-analyze\SKILL.md`(新版快速分析 → 差异清单 → 接口冲突 → 迁移迭代) |
|
||||
| **帮我部署管理端到线上** | `e:\Gongsi\Mycontent\.cursor\skills\security-server-ops\SKILL.md`(安全工程师 → 执行 soul-api/master.py) |
|
||||
|
||||
**注意**:「必须 Read」= 使用 Read 工具读取**绝对路径**的完整文件内容后执行,不可跳过或仅凭记忆。
|
||||
|
||||
@@ -39,6 +39,7 @@ alwaysApply: true
|
||||
- 产品/需求/config→**产品经理**
|
||||
- 测试/自检/QA→**软件测试**
|
||||
- 架构/选型/路由约定/三端协同→**团队**
|
||||
- 挖矿/安全/服务器操作/部署/入侵排查→**安全工程师**
|
||||
- 无法判断→**通用**(写入开发助理)
|
||||
3. **若可写文件**:
|
||||
- **有明确目标角色**:写入 `.cursor/agent/{角色}/evolution/YYYY-MM-DD-简短描述.md`,并更新该目录下的 `索引.md`
|
||||
|
||||
@@ -83,6 +83,7 @@ description: 开发团队文档同步与经验升级。小橙、橙子、讨论
|
||||
| 后端开发 | `agent/后端工程师/evolution/` | `agent/开发助理/项目索引/后端.md` |
|
||||
| 产品经理 | `agent/产品经理/evolution/` | `agent/开发助理/项目索引/产品.md` |
|
||||
| 测试人员 | `agent/软件测试/evolution/` | `agent/开发助理/项目索引/测试.md` |
|
||||
| 安全工程师 | `agent/安全工程师/evolution/` | `agent/开发助理/项目索引/团队.md` |
|
||||
| 助理橙子 | `agent/开发助理/evolution/` | `agent/开发助理/项目索引/助理橙子.md` |
|
||||
| 跨角色/团队 | `agent/团队/evolution/` | `agent/开发助理/项目索引/团队.md` |
|
||||
|
||||
|
||||
112
.cursor/skills/security-miner-guard/SKILL.md
Normal file
112
.cursor/skills/security-miner-guard/SKILL.md
Normal file
@@ -0,0 +1,112 @@
|
||||
---
|
||||
description: Soul 挖矿病毒排查与防护。xmrig、kdevtmpfsi、kinsing、minerd、miner_guard。Use when 挖矿病毒、xmrig、服务器被入侵、miner_guard、安全排查、杀挖矿.
|
||||
---
|
||||
# SKILL - 挖矿病毒排查与防护(安全工程师)
|
||||
|
||||
> 基于 Soul 项目历史排查经验沉淀,用于快速识别、清理挖矿病毒并加固服务器。
|
||||
|
||||
## 何时使用
|
||||
|
||||
| 触发词 | 动作 |
|
||||
|--------|------|
|
||||
| 挖矿病毒、xmrig、服务器被入侵 | 执行排查与清理流程 |
|
||||
| miner_guard、安装挖矿守护 | 安装/检查 miner_guard |
|
||||
| 安全排查、杀挖矿 | 按本 Skill 执行 |
|
||||
|
||||
---
|
||||
|
||||
## 一、挖矿病毒特征(Soul 项目实测)
|
||||
|
||||
### 1.1 进程/路径关键词
|
||||
|
||||
| 关键词 | 说明 |
|
||||
|--------|------|
|
||||
| xmrig | 门罗币挖矿程序,最常见 |
|
||||
| xmr-stak, minerd, cpuminer | 其他 CPU 挖矿 |
|
||||
| kdevtmpfsi, kinsing | Linux 常见挖矿木马 |
|
||||
| stratum | 矿池协议 |
|
||||
| libprocesshider, watchbog, ddgs, trace | 隐藏/持久化相关 |
|
||||
|
||||
### 1.2 常见路径
|
||||
|
||||
- `/tmp/xmrig`、`/tmp/config.json`、`/tmp/.x`
|
||||
- `/tmp/kdevtmpfsi`、`/tmp/kinsing`
|
||||
- `/www/wwwroot/**/xmrig*`(宝塔站点目录下残留)
|
||||
- `/www/wwwroot/self/wanzhi/tongzhi/xmrig-*`(历史发现)
|
||||
|
||||
### 1.3 入侵链路(Soul 项目根因分析)
|
||||
|
||||
```
|
||||
公网访问宝塔 9988 → 弱口令/漏洞 → 进入面板 → www 用户执行命令
|
||||
→ 下载 xmrig 到 /tmp → 运行挖矿
|
||||
```
|
||||
|
||||
**最可能入口**:宝塔面板 9988 对公网开放 + 弱口令/历史漏洞。
|
||||
|
||||
---
|
||||
|
||||
## 二、排查与清理脚本(soul-api 目录)
|
||||
|
||||
| 脚本 | 用途 |
|
||||
|------|------|
|
||||
| `miner_guard_check.py` | 检查 miner_guard 安装状态,手动执行一次脚本,查看日志 |
|
||||
| `miner_guard_install.py` | 安装挖矿守护到服务器(上传 miner_guard.sh + 配置 cron/systemd) |
|
||||
| `miner_guard.sh` | 守护脚本本体(杀进程、删文件、扫 /tmp、/www/wwwroot) |
|
||||
| `remove_xmrig_self.py` | 删除固定路径 `/www/wwwroot/self/wanzhi/tongzhi/xmrig-6.24.0` |
|
||||
|
||||
### 2.1 快速检查(本地执行)
|
||||
|
||||
```powershell
|
||||
cd e:\Gongsi\Mycontent\soul-api
|
||||
python miner_guard_check.py
|
||||
```
|
||||
|
||||
依赖:`pip install paramiko`。配置来源:`master.py` 的 `get_cfg()` 或环境变量 `DEPLOY_HOST`、`DEPLOY_PASSWORD` 等。
|
||||
|
||||
### 2.2 安装挖矿守护
|
||||
|
||||
```powershell
|
||||
cd e:\Gongsi\Mycontent\soul-api
|
||||
python miner_guard_install.py --yes
|
||||
```
|
||||
|
||||
- 上传 `miner_guard.sh` 到 `/root/miner_guard.sh`
|
||||
- 优先写入 `/etc/cron.d/miner_guard`(每 30 分钟),失败则尝试 crontab 或 systemd timer
|
||||
- 日志:`/var/log/miner_guard.log`
|
||||
|
||||
### 2.3 Cunkebao 服务器
|
||||
|
||||
```powershell
|
||||
cd e:\Gongsi\Mycontent\Cunkebao
|
||||
python miner_guard_install.py --yes
|
||||
```
|
||||
|
||||
使用 Cunkebao 内嵌配置(非 root 用户,日志在 `~/miner_guard.log`)。
|
||||
|
||||
---
|
||||
|
||||
## 三、加固建议(防止再次入侵)
|
||||
|
||||
| 优先级 | 措施 |
|
||||
|--------|------|
|
||||
| 1 | 宝塔面板:9988 仅允许指定 IP 访问;改非常规端口(如 29988);强密码 |
|
||||
| 2 | 修改所有密码:root、宝塔、宝塔 API、MySQL、Redis |
|
||||
| 3 | Redis:`bind 127.0.0.1`,设置 `requirepass` |
|
||||
| 4 | 敏感信息:密码/API Key 用环境变量,不提交到 Git |
|
||||
|
||||
---
|
||||
|
||||
## 四、miner_guard.sh 行为摘要
|
||||
|
||||
1. **杀进程**:`pgrep -f xmrig|kdevtmpfsi|kinsing|...` → `kill -9`
|
||||
2. **删已知路径**:`/tmp/xmrig`、`/tmp/config.json` 等
|
||||
3. **扫 /tmp、/var/tmp、/dev/shm**:含挖矿关键词的可执行文件 → `rm -f`
|
||||
4. **扫 /www/wwwroot**:含 xmrig 的目录/文件 → `rm -rf` / `rm -f`
|
||||
5. **检查 www 用户 crontab**:可疑项仅提示,不自动删
|
||||
|
||||
---
|
||||
|
||||
## 五、注意事项
|
||||
|
||||
- 脚本会按关键词删除,可能与业务目录重叠,部署前确认扫描范围
|
||||
- `master.py`、`devloy.py` 等含默认密码,应改为环境变量并确保 `.env` 不入库
|
||||
137
.cursor/skills/security-server-ops/SKILL.md
Normal file
137
.cursor/skills/security-server-ops/SKILL.md
Normal file
@@ -0,0 +1,137 @@
|
||||
---
|
||||
description: Soul 服务器操作与部署。部署脚本、SSH、宝塔、环境变量。Use when 部署、服务器操作、SSH、宝塔、devloy、master、Cunkebao 部署.
|
||||
---
|
||||
# SKILL - 服务器操作与部署(安全工程师)
|
||||
|
||||
> Soul 项目部署脚本索引与服务器操作规范。**密码等敏感信息仅通过环境变量或脚本内 get_cfg() 读取,不在此文档明文列出。**
|
||||
|
||||
## 何时使用
|
||||
|
||||
| 触发词 | 动作 |
|
||||
|--------|------|
|
||||
| **帮我部署管理端到线上** | **直接执行 `cd soul-api && python master.py`** |
|
||||
| 部署、服务器操作、SSH | 按本 Skill 选择对应脚本 |
|
||||
| 宝塔、devloy、master | 查阅部署配置与命令 |
|
||||
| Cunkebao 部署 | 使用 Cunkebao 专用脚本 |
|
||||
|
||||
---
|
||||
|
||||
## 一、服务器与配置来源
|
||||
|
||||
### 1.1 配置读取优先级
|
||||
|
||||
各部署脚本统一约定:
|
||||
|
||||
1. **环境变量**(推荐):`DEPLOY_HOST`、`DEPLOY_USER`、`DEPLOY_PASSWORD`、`DEPLOY_SSH_PORT`、`BT_API_KEY` 等
|
||||
2. **脚本内 get_cfg()**:无环境变量时使用脚本默认值
|
||||
3. **master.py**:soul-api 的 miner_guard_check、remove_xmrig_self 等从 `soul-api/master.py` 的 `get_cfg()` 读取
|
||||
|
||||
### 1.2 服务器索引(配置来源,不含明文密码)
|
||||
|
||||
| 项目 | 主机 | 端口 | 用户 | 配置来源 |
|
||||
|------|------|------|------|----------|
|
||||
| soul-api 正式 | 43.139.27.93 | 22022 | root | master.py / 环境变量 |
|
||||
| soul-api 测试 | 43.139.27.93 | 22022 | root | devloy.py / 环境变量 |
|
||||
| soul-admin | 43.139.27.93 | 22022 | root | soul-admin/deploy.py / 环境变量 |
|
||||
| Cunkebao | 42.194.245.239 | 6523 | yongpxu | Cunkebao/miner_guard_install.py 内嵌 |
|
||||
|
||||
**密码**:从 `DEPLOY_PASSWORD` 或各脚本 `get_cfg()` 默认值读取,**不在此文档记录**。
|
||||
|
||||
---
|
||||
|
||||
## 二、部署脚本索引
|
||||
|
||||
### 2.1 soul-api
|
||||
|
||||
| 脚本 | 用途 | 命令示例 |
|
||||
|------|------|----------|
|
||||
| `soul-api/devloy.py` | 测试环境部署(binary/docker/runner) | `python devloy.py --mode runner` |
|
||||
| `soul-api/master.py` | 正式环境部署 | `python master.py` |
|
||||
| `soul-api/deploy/runner-init.sh` | Runner 容器首次初始化 | `bash deploy/runner-init.sh` |
|
||||
| `soul-api/deploy/docker-deploy-remote.sh` | 服务器上执行蓝绿切换 | 由 devloy 自动调用 |
|
||||
| `soul-api/deploy/deploy-runner-remote.sh` | Runner 模式部署包拷贝 | 由 devloy 自动调用 |
|
||||
|
||||
**devloy 模式**:
|
||||
|
||||
- `--mode runner`:容器内红蓝切换,宝塔固定 9001
|
||||
- `--mode docker`:宿主机蓝绿,需 Nginx 切换
|
||||
- `--mode binary`:Go 二进制 + 宝塔 soulDev
|
||||
|
||||
### 2.2 soul-admin
|
||||
|
||||
| 脚本 | 用途 | 命令示例 |
|
||||
|------|------|----------|
|
||||
| `soul-admin/deploy.py` | 静态站点部署(dist → dist2 互换) | `python deploy.py` |
|
||||
|
||||
### 2.3 挖矿防护
|
||||
|
||||
| 脚本 | 用途 | 命令示例 |
|
||||
|------|------|----------|
|
||||
| `soul-api/miner_guard_install.py` | 安装挖矿守护(soul 服务器) | `python miner_guard_install.py --yes` |
|
||||
| `Cunkebao/miner_guard_install.py` | 安装挖矿守护(Cunkebao) | `cd Cunkebao && python miner_guard_install.py --yes` |
|
||||
|
||||
---
|
||||
|
||||
## 三、环境变量一览
|
||||
|
||||
| 变量 | 说明 | 默认来源 |
|
||||
|------|------|----------|
|
||||
| DEPLOY_HOST | SSH 主机 | 各脚本 get_cfg() |
|
||||
| DEPLOY_USER | SSH 用户 | root |
|
||||
| DEPLOY_PASSWORD | SSH 密码 | 脚本默认 / 需设置 |
|
||||
| DEPLOY_SSH_KEY | SSH 私钥路径 | 空 |
|
||||
| DEPLOY_SSH_PORT | SSH 端口 | 22022 |
|
||||
| BT_PANEL_URL | 宝塔面板 URL | https://{host}:9988 |
|
||||
| BT_API_KEY | 宝塔 API 密钥 | 脚本内默认 |
|
||||
| BT_GO_PROJECT_NAME | 宝塔 Go 项目名 | soulDev / soulApi |
|
||||
| DEPLOY_DOCKER_PATH | 部署目录 | /www/wwwroot/self/soul-dev |
|
||||
| DEPLOY_NGINX_CONF | Nginx 配置路径 | 空(可自动探测) |
|
||||
|
||||
---
|
||||
|
||||
## 四、常用操作
|
||||
|
||||
### 4.1 SSH 连接(示例,密码从环境变量读取)
|
||||
|
||||
```powershell
|
||||
# 设置环境变量后
|
||||
$env:DEPLOY_HOST="43.139.27.93"
|
||||
$env:DEPLOY_PASSWORD="<从安全存储读取>"
|
||||
ssh -p 22022 root@43.139.27.93
|
||||
```
|
||||
|
||||
### 4.2 部署 soul-api 测试环境(Runner 模式)
|
||||
|
||||
```powershell
|
||||
cd e:\Gongsi\Mycontent\soul-api
|
||||
python devloy.py --mode runner
|
||||
```
|
||||
|
||||
### 4.3 部署 soul-api 正式环境
|
||||
|
||||
```powershell
|
||||
cd e:\Gongsi\Mycontent\soul-api
|
||||
python master.py
|
||||
```
|
||||
|
||||
### 4.4 部署 soul-admin
|
||||
|
||||
```powershell
|
||||
cd e:\Gongsi\Mycontent\soul-admin
|
||||
python deploy.py
|
||||
```
|
||||
|
||||
### 4.5 检查挖矿守护
|
||||
|
||||
```powershell
|
||||
cd e:\Gongsi\Mycontent\soul-api
|
||||
python miner_guard_check.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、安全提醒
|
||||
|
||||
- **不要将密码提交到 Git**:master.py、devloy.py 等中的默认密码应迁移到环境变量
|
||||
- **宝塔 API 密钥**:BT_API_KEY 若泄露需在宝塔面板重新生成
|
||||
- **敏感文件**:`.env`、`master.py` 等应加入 `.gitignore` 或使用 `.env.example` 模板
|
||||
@@ -95,7 +95,10 @@ App({
|
||||
this._privacyResolve = resolve
|
||||
const pages = getCurrentPages()
|
||||
const cur = pages[pages.length - 1]
|
||||
if (cur && typeof cur.setData === 'function' && cur.route && (cur.route.includes('avatar-nickname') || cur.route.includes('profile-edit'))) {
|
||||
const route = (cur && cur.route) || ''
|
||||
const needPrivacyPages = ['avatar-nickname', 'profile-edit', 'read', 'my', 'gift-pay/detail', 'index', 'settings']
|
||||
const needShow = needPrivacyPages.some(p => route.includes(p))
|
||||
if (cur && typeof cur.setData === 'function' && needShow) {
|
||||
cur.setData({ showPrivacyModal: true })
|
||||
} else {
|
||||
resolve({ event: 'disagree' })
|
||||
@@ -413,12 +416,57 @@ App({
|
||||
this.globalData.hasFullBook = userInfo.hasFullBook || false
|
||||
this.globalData.isVip = userInfo.isVip || false
|
||||
this.globalData.vipExpireDate = userInfo.vipExpireDate || ''
|
||||
// 若手机号为空,后台静默刷新用户资料以同步最新手机号(可能在其他设备/页面已绑定)
|
||||
if (!(userInfo.phone || '').trim()) {
|
||||
this._refreshUserInfoIfPhoneEmpty()
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('检查登录状态失败:', e)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 手机号登录后:若响应中 user.phone 为空,从 profile 拉取最新资料并更新本地(后端已写入 DB)
|
||||
*/
|
||||
async _syncPhoneFromProfileAfterLogin(userId) {
|
||||
try {
|
||||
if (!userId) return
|
||||
const res = await this.request({ url: `/api/miniprogram/user/profile?userId=${userId}`, silent: true })
|
||||
const profile = res?.data
|
||||
if (!profile) return
|
||||
const phone = (profile.phone || '').trim()
|
||||
if (!phone) return
|
||||
const updated = { ...this.globalData.userInfo, phone }
|
||||
if (profile.wechatId != null) updated.wechatId = profile.wechatId
|
||||
this.globalData.userInfo = updated
|
||||
wx.setStorageSync('userInfo', updated)
|
||||
wx.setStorageSync('user_phone', phone)
|
||||
} catch (_) {}
|
||||
},
|
||||
|
||||
/**
|
||||
* 当本地 userInfo.phone 为空时,静默拉取 profile 并更新(用户可能在设置页或其他入口已绑定手机号)
|
||||
*/
|
||||
async _refreshUserInfoIfPhoneEmpty() {
|
||||
try {
|
||||
const userId = this.globalData.userInfo?.id
|
||||
if (!userId) return
|
||||
const res = await this.request({ url: `/api/miniprogram/user/profile?userId=${userId}`, silent: true })
|
||||
const profile = res?.data
|
||||
if (!profile) return
|
||||
const phone = (profile.phone || '').trim()
|
||||
if (!phone) return
|
||||
const updated = { ...this.globalData.userInfo, phone }
|
||||
if (profile.wechatId != null) updated.wechatId = profile.wechatId
|
||||
this.globalData.userInfo = updated
|
||||
wx.setStorageSync('userInfo', updated)
|
||||
if (phone) wx.setStorageSync('user_phone', phone)
|
||||
} catch (_) {
|
||||
// 静默失败,不影响主流程
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* WSS 在线心跳(占位):登录后连接 ws,发送 auth + 心跳,供管理端统计在线人数
|
||||
* 容错:任意异常均不向外抛出,不影响登录、API 请求等核心功能
|
||||
@@ -1026,6 +1074,13 @@ App({
|
||||
|
||||
wx.setStorageSync('userInfo', user)
|
||||
wx.setStorageSync('token', res.data.token)
|
||||
// 手机号登录后:若用户资料中手机号为空,从 profile 刷新并更新(后端已写入 DB,可能响应中未带回)
|
||||
const phone = (user.phone || '').trim()
|
||||
if (!phone) {
|
||||
this._syncPhoneFromProfileAfterLogin(user.id)
|
||||
} else {
|
||||
wx.setStorageSync('user_phone', phone)
|
||||
}
|
||||
|
||||
// 登录成功后绑定推荐码
|
||||
const pendingRef = wx.getStorageSync('pendingReferralCode') || this.globalData.pendingReferralCode
|
||||
|
||||
@@ -28,7 +28,8 @@
|
||||
"pages/avatar-nickname/avatar-nickname",
|
||||
"pages/gift-pay/detail",
|
||||
"pages/gift-pay/list",
|
||||
"pages/gift-pay/redemption-detail"
|
||||
"pages/gift-pay/redemption-detail",
|
||||
"pages/dev-login/dev-login"
|
||||
],
|
||||
"window": {
|
||||
"backgroundTextStyle": "light",
|
||||
|
||||
80
miniprogram/pages/dev-login/dev-login.js
Normal file
80
miniprogram/pages/dev-login/dev-login.js
Normal file
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* 卡若创业派对 - 开发登录页
|
||||
* 临时:账户=手机号,密码可空,用于切换为对方账号调试
|
||||
*/
|
||||
const app = getApp()
|
||||
const { checkAndExecute } = require('../../utils/ruleEngine.js')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
statusBarHeight: 44,
|
||||
account: '',
|
||||
password: '',
|
||||
loading: false
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.setData({
|
||||
statusBarHeight: app.globalData.statusBarHeight || 44
|
||||
})
|
||||
},
|
||||
|
||||
onAccountInput(e) {
|
||||
this.setData({ account: (e.detail.value || '').trim() })
|
||||
},
|
||||
|
||||
onPasswordInput(e) {
|
||||
this.setData({ password: e.detail.value || '' })
|
||||
},
|
||||
|
||||
goBack() {
|
||||
app.goBackOrToHome()
|
||||
},
|
||||
|
||||
async handleLogin() {
|
||||
const { account, password, loading } = this.data
|
||||
if (!account || loading) return
|
||||
|
||||
const phone = account.replace(/\s/g, '')
|
||||
if (phone.length < 11) {
|
||||
wx.showToast({ title: '请输入11位手机号', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
this.setData({ loading: true })
|
||||
try {
|
||||
const res = await app.request('/api/miniprogram/dev/login-by-phone', {
|
||||
method: 'POST',
|
||||
data: { phone, password: password || '' }
|
||||
})
|
||||
|
||||
if (res.success && res.data) {
|
||||
const user = res.data.user
|
||||
app.globalData.userInfo = user
|
||||
app.globalData.isLoggedIn = true
|
||||
app.globalData.purchasedSections = user.purchasedSections || []
|
||||
app.globalData.hasFullBook = user.hasFullBook || false
|
||||
app.globalData.isVip = user.isVip || false
|
||||
app.globalData.vipExpireDate = user.vipExpireDate || ''
|
||||
|
||||
wx.setStorageSync('userInfo', user)
|
||||
wx.setStorageSync('token', res.data.token)
|
||||
|
||||
const pendingRef = wx.getStorageSync('pendingReferralCode') || app.globalData.pendingReferralCode
|
||||
if (pendingRef) {
|
||||
app.bindReferralCode(pendingRef)
|
||||
}
|
||||
|
||||
checkAndExecute('after_login', null)
|
||||
setTimeout(() => app.checkVipContactRequiredAndGuide(), 1200)
|
||||
|
||||
wx.showToast({ title: '登录成功', icon: 'success' })
|
||||
setTimeout(() => wx.switchTab({ url: '/pages/index/index' }), 800)
|
||||
}
|
||||
} catch (e) {
|
||||
wx.showToast({ title: e.message || '登录失败', icon: 'none' })
|
||||
} finally {
|
||||
this.setData({ loading: false })
|
||||
}
|
||||
}
|
||||
})
|
||||
7
miniprogram/pages/dev-login/dev-login.json
Normal file
7
miniprogram/pages/dev-login/dev-login.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"usingComponents": {
|
||||
"icon": "/components/icon/icon"
|
||||
},
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": "账户密码登录"
|
||||
}
|
||||
51
miniprogram/pages/dev-login/dev-login.wxml
Normal file
51
miniprogram/pages/dev-login/dev-login.wxml
Normal file
@@ -0,0 +1,51 @@
|
||||
<!--开发登录页 - 临时:账户=手机号,密码可空-->
|
||||
<view class="page">
|
||||
<view class="nav-bar" style="padding-top: {{statusBarHeight}}px;">
|
||||
<view class="nav-back" bindtap="goBack">
|
||||
<icon name="chevron-left" size="44" color="rgba(255,255,255,0.6)" customClass="back-icon"></icon>
|
||||
</view>
|
||||
<text class="nav-title">账户密码登录</text>
|
||||
<view class="nav-placeholder"></view>
|
||||
</view>
|
||||
<view style="height: {{statusBarHeight + 44}}px;"></view>
|
||||
|
||||
<view class="content">
|
||||
<view class="tip-banner">
|
||||
<text class="tip-text">开发专用:输入对方手机号登录,密码可留空。仅开发环境可用。</text>
|
||||
</view>
|
||||
|
||||
<view class="form-card">
|
||||
<view class="form-item">
|
||||
<text class="form-label">账户(手机号)</text>
|
||||
<view class="form-input-wrap">
|
||||
<input
|
||||
class="form-input-inner"
|
||||
type="number"
|
||||
placeholder="请输入对方手机号"
|
||||
placeholder-class="input-placeholder"
|
||||
value="{{account}}"
|
||||
bindinput="onAccountInput"
|
||||
maxlength="11"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="form-label">密码(可留空)</text>
|
||||
<view class="form-input-wrap">
|
||||
<input
|
||||
class="form-input-inner"
|
||||
type="text"
|
||||
password="{{true}}"
|
||||
placeholder="可选,留空即可"
|
||||
placeholder-class="input-placeholder"
|
||||
value="{{password}}"
|
||||
bindinput="onPasswordInput"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
<view class="btn-primary {{!account || loading ? 'btn-disabled' : ''}}" bindtap="handleLogin">
|
||||
{{loading ? '登录中...' : '登录'}}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
23
miniprogram/pages/dev-login/dev-login.wxss
Normal file
23
miniprogram/pages/dev-login/dev-login.wxss
Normal file
@@ -0,0 +1,23 @@
|
||||
/* 开发登录页 */
|
||||
.page { min-height: 100vh; background: #000; padding-bottom: 64rpx; }
|
||||
|
||||
.nav-bar { position: fixed; top: 0; left: 0; right: 0; z-index: 100; background: rgba(0,0,0,0.9); backdrop-filter: blur(40rpx); display: flex; align-items: center; justify-content: space-between; padding: 0 32rpx; height: 88rpx; }
|
||||
.nav-back { width: 64rpx; height: 64rpx; background: #1c1c1e; border-radius: 50%; display: flex; align-items: center; justify-content: center; }
|
||||
.back-icon { font-size: 40rpx; color: rgba(255,255,255,0.6); font-weight: 300; }
|
||||
.nav-title { font-size: 34rpx; font-weight: 600; color: #fff; }
|
||||
.nav-placeholder { width: 64rpx; }
|
||||
|
||||
.content { padding: 24rpx 16rpx; }
|
||||
|
||||
.tip-banner { background: rgba(255,165,0,0.1); border: 2rpx solid rgba(255,165,0,0.3); border-radius: 20rpx; padding: 20rpx 24rpx; margin-bottom: 24rpx; }
|
||||
.tip-text { font-size: 24rpx; color: #FFA500; line-height: 1.5; }
|
||||
|
||||
.form-card { background: #1c1c1e; border-radius: 32rpx; padding: 32rpx; border: 2rpx solid rgba(0,206,209,0.2); }
|
||||
.form-item { margin-bottom: 32rpx; }
|
||||
.form-item:last-of-type { margin-bottom: 48rpx; }
|
||||
.form-label { font-size: 28rpx; color: rgba(255,255,255,0.8); display: block; margin-bottom: 16rpx; }
|
||||
.form-input-wrap { padding: 16rpx 24rpx; background: #1F2937; border: 2rpx solid rgba(255,255,255,0.1); border-radius: 24rpx; }
|
||||
.form-input-inner { width: 100%; font-size: 28rpx; background: transparent; color: #fff; }
|
||||
.input-placeholder { color: rgba(255,255,255,0.25); }
|
||||
.btn-primary { padding: 32rpx; background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%); color: #000; font-size: 32rpx; font-weight: 600; text-align: center; border-radius: 28rpx; }
|
||||
.btn-disabled { opacity: 0.5; }
|
||||
@@ -19,6 +19,7 @@ Page({
|
||||
isSinglePageMode: false,
|
||||
showLoginModal: false,
|
||||
agreeProtocol: false,
|
||||
showPrivacyModal: false,
|
||||
// 创建态
|
||||
isCreateMode: false,
|
||||
giftQuantity: 1,
|
||||
@@ -265,7 +266,7 @@ Page({
|
||||
},
|
||||
|
||||
closeLoginModal() {
|
||||
this.setData({ showLoginModal: false })
|
||||
this.setData({ showLoginModal: false, showPrivacyModal: false })
|
||||
},
|
||||
toggleAgree() {
|
||||
this.setData({ agreeProtocol: !this.data.agreeProtocol })
|
||||
@@ -284,6 +285,13 @@ Page({
|
||||
wx.showToast({ title: '登录失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
handleAgreePrivacyForPhone() {
|
||||
if (app._privacyResolve) {
|
||||
app._privacyResolve({ buttonId: 'agree-privacy-btn', event: 'agree' })
|
||||
app._privacyResolve = null
|
||||
}
|
||||
this.setData({ showPrivacyModal: false })
|
||||
},
|
||||
async handlePhoneLogin(e) {
|
||||
if (!e.detail.code) return this.handleWechatLogin()
|
||||
try {
|
||||
|
||||
@@ -152,10 +152,14 @@
|
||||
<view class="login-icon"><icon name="lock" size="80" color="#00CED1"></icon></view>
|
||||
<text class="login-title">登录 卡若创业派对</text>
|
||||
<text class="login-desc">登录后可免费领取并阅读</text>
|
||||
<button class="btn-wechat {{agreeProtocol ? '' : 'btn-wechat-disabled'}}" bindtap="handleWechatLogin" disabled="{{!agreeProtocol}}">
|
||||
<button id="agree-phone-btn" class="btn-wechat {{agreeProtocol ? '' : 'btn-wechat-disabled'}}" open-type="getPhoneNumber|agreePrivacyAuthorization" bindgetphonenumber="handlePhoneLogin" bindagreeprivacyauthorization="handleAgreePrivacyForPhone" disabled="{{!agreeProtocol}}">
|
||||
<text class="btn-wechat-icon">微</text>
|
||||
<text>微信快捷登录</text>
|
||||
<text>手机号一键登录</text>
|
||||
</button>
|
||||
<view class="privacy-wechat-row" wx:if="{{showPrivacyModal}}">
|
||||
<text class="privacy-wechat-desc">为获取手机号,请先同意《用户隐私保护指引》</text>
|
||||
<button id="agree-privacy-btn" class="privacy-agree-btn" open-type="agreePrivacyAuthorization" bindagreeprivacyauthorization="handleAgreePrivacyForPhone">同意</button>
|
||||
</view>
|
||||
<view class="login-agree-row" catchtap="toggleAgree">
|
||||
<view class="agree-checkbox {{agreeProtocol ? 'agree-checked' : ''}}"><icon wx:if="{{agreeProtocol}}" name="check" size="24" color="#34C759"></icon></view>
|
||||
<text class="agree-text">我已阅读并同意</text>
|
||||
|
||||
@@ -637,6 +637,10 @@
|
||||
}
|
||||
.btn-wechat-disabled { opacity: 0.5; }
|
||||
.btn-wechat-icon { font-weight: 700; margin-right: 8rpx; }
|
||||
.privacy-wechat-row { margin: 24rpx 0; padding: 24rpx; background: rgba(0,206,209,0.1); border-radius: 16rpx; }
|
||||
.privacy-wechat-desc { display: block; font-size: 26rpx; color: rgba(255,255,255,0.8); margin-bottom: 16rpx; }
|
||||
.privacy-agree-btn { width: 100%; padding: 20rpx; background: #07C160; color: #fff; font-size: 28rpx; border-radius: 16rpx; border: none; }
|
||||
.privacy-agree-btn::after { border: none; }
|
||||
.login-agree-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@@ -61,6 +61,7 @@ Page({
|
||||
// 链接卡若 - 留资弹窗
|
||||
showLeadModal: false,
|
||||
leadPhone: '',
|
||||
showPrivacyModal: false,
|
||||
|
||||
// 展开状态(首页精选/最新)
|
||||
featuredExpanded: false,
|
||||
@@ -380,7 +381,7 @@ Page({
|
||||
},
|
||||
|
||||
closeLeadModal() {
|
||||
this.setData({ showLeadModal: false, leadPhone: '' })
|
||||
this.setData({ showLeadModal: false, leadPhone: '', showPrivacyModal: false })
|
||||
},
|
||||
|
||||
// 阻止弹窗内部点击事件冒泡到遮罩层
|
||||
@@ -390,6 +391,15 @@ Page({
|
||||
this.setData({ leadPhone: (e.detail.value || '').trim() })
|
||||
},
|
||||
|
||||
// 微信隐私协议同意(getPhoneNumber 需先同意)
|
||||
onAgreePrivacyForLead() {
|
||||
if (app._privacyResolve) {
|
||||
app._privacyResolve({ buttonId: 'agree-privacy-btn', event: 'agree' })
|
||||
app._privacyResolve = null
|
||||
}
|
||||
this.setData({ showPrivacyModal: false })
|
||||
},
|
||||
|
||||
// 一键获取手机号(微信能力),成功后直接提交链接卡若
|
||||
async onGetPhoneNumberForLead(e) {
|
||||
if (e.detail.errMsg !== 'getPhoneNumber:ok') {
|
||||
|
||||
@@ -161,7 +161,11 @@
|
||||
<view class="lead-box" catchtap="stopPropagation">
|
||||
<text class="lead-title">留下联系方式</text>
|
||||
<text class="lead-desc">方便卡若与您联系</text>
|
||||
<button class="lead-get-phone-btn" open-type="getPhoneNumber" bindgetphonenumber="onGetPhoneNumberForLead">一键获取手机号</button>
|
||||
<button id="agree-lead-phone-btn" class="lead-get-phone-btn" open-type="getPhoneNumber|agreePrivacyAuthorization" bindgetphonenumber="onGetPhoneNumberForLead" bindagreeprivacyauthorization="onAgreePrivacyForLead">一键获取手机号</button>
|
||||
<view class="privacy-wechat-row" wx:if="{{showPrivacyModal}}">
|
||||
<text class="privacy-wechat-desc">为获取手机号,请先同意《用户隐私保护指引》</text>
|
||||
<button id="agree-privacy-btn" class="privacy-agree-btn" open-type="agreePrivacyAuthorization" bindagreeprivacyauthorization="onAgreePrivacyForLead">同意</button>
|
||||
</view>
|
||||
<text class="lead-divider">或手动输入</text>
|
||||
<view class="lead-input-wrap">
|
||||
<input class="lead-input" placeholder="请输入手机号" type="number" maxlength="11" value="{{leadPhone}}" bindinput="onLeadPhoneInput"/>
|
||||
|
||||
@@ -900,6 +900,10 @@
|
||||
line-height: normal;
|
||||
}
|
||||
.lead-get-phone-btn::after { border: none; }
|
||||
.privacy-wechat-row { margin: 24rpx 0; padding: 24rpx; background: rgba(0,206,209,0.1); border-radius: 16rpx; }
|
||||
.privacy-wechat-desc { display: block; font-size: 26rpx; color: rgba(255,255,255,0.8); margin-bottom: 16rpx; }
|
||||
.privacy-agree-btn { width: 100%; padding: 20rpx; background: #07C160; color: #fff; font-size: 28rpx; border-radius: 16rpx; border: none; }
|
||||
.privacy-agree-btn::after { border: none; }
|
||||
.lead-divider {
|
||||
display: block;
|
||||
font-size: 24rpx;
|
||||
|
||||
@@ -63,6 +63,7 @@ Page({
|
||||
isLoggingIn: false,
|
||||
// 用户须主动勾选同意协议(审核要求:不得默认同意)
|
||||
agreeProtocol: false,
|
||||
showPrivacyModal: false,
|
||||
|
||||
// 修改昵称弹窗
|
||||
showNicknameModal: false,
|
||||
@@ -715,7 +716,7 @@ Page({
|
||||
// 关闭登录弹窗
|
||||
closeLoginModal() {
|
||||
if (this.data.isLoggingIn) return
|
||||
this.setData({ showLoginModal: false })
|
||||
this.setData({ showLoginModal: false, showPrivacyModal: false })
|
||||
},
|
||||
|
||||
// 微信登录(须已勾选同意协议,且做好错误处理避免审核报错)
|
||||
@@ -742,6 +743,15 @@ Page({
|
||||
}
|
||||
},
|
||||
|
||||
// 微信隐私协议同意(getPhoneNumber 需先同意)
|
||||
handleAgreePrivacyForPhone() {
|
||||
if (app._privacyResolve) {
|
||||
app._privacyResolve({ buttonId: 'agree-privacy-btn', event: 'agree' })
|
||||
app._privacyResolve = null
|
||||
}
|
||||
this.setData({ showPrivacyModal: false })
|
||||
},
|
||||
|
||||
// 手机号登录(需要用户授权)
|
||||
async handlePhoneLogin(e) {
|
||||
// 检查是否有授权code
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<text wx:else class="guest-avatar-text">{{guestNickname[0] || '游'}}</text>
|
||||
</view>
|
||||
<text class="guest-name">{{guestNickname}}</text>
|
||||
<view class="guest-login-btn" bindtap="showLogin">点击登录</view>
|
||||
<view class="guest-login-btn" bindtap="showLogin">手机号一键登录</view>
|
||||
</view>
|
||||
|
||||
<!-- 已登录:用户卡片(设计稿布局) -->
|
||||
@@ -57,7 +57,7 @@
|
||||
<text class="profile-stat-label">推荐好友</text>
|
||||
</view>
|
||||
<view class="profile-stat" wx:if="{{referralEnabled}}" bindtap="goToReferral">
|
||||
<text class="profile-stat-val">{{earnings === '-' ? '--' : earnings}}</text>
|
||||
<text class="profile-stat-val">{{pendingEarnings === '-' ? '--' : pendingEarnings}}</text>
|
||||
<text class="profile-stat-label">我的收益</text>
|
||||
</view>
|
||||
<view class="profile-stat" wx:if="{{!auditMode}}" bindtap="handleMenuTap" data-id="wallet">
|
||||
@@ -178,10 +178,14 @@
|
||||
<view class="login-icon"><icon name="lock" size="80" color="#00CED1"></icon></view>
|
||||
<text class="login-title">登录 卡若创业派对</text>
|
||||
<text class="login-desc">登录后可购买章节、解锁更多内容</text>
|
||||
<button class="btn-wechat {{agreeProtocol ? '' : 'btn-wechat-disabled'}}" bindtap="handleWechatLogin" disabled="{{isLoggingIn || !agreeProtocol}}">
|
||||
<button id="agree-phone-btn" class="btn-wechat {{agreeProtocol ? '' : 'btn-wechat-disabled'}}" open-type="getPhoneNumber|agreePrivacyAuthorization" bindgetphonenumber="handlePhoneLogin" bindagreeprivacyauthorization="handleAgreePrivacyForPhone" disabled="{{isLoggingIn || !agreeProtocol}}">
|
||||
<text class="btn-wechat-icon">微</text>
|
||||
<text>{{isLoggingIn ? '登录中...' : '微信快捷登录'}}</text>
|
||||
<text>{{isLoggingIn ? '登录中...' : '手机号一键登录'}}</text>
|
||||
</button>
|
||||
<view class="privacy-wechat-row" wx:if="{{showPrivacyModal}}">
|
||||
<text class="privacy-wechat-desc">为获取手机号,请先同意《用户隐私保护指引》</text>
|
||||
<button id="agree-privacy-btn" class="privacy-agree-btn" open-type="agreePrivacyAuthorization" bindagreeprivacyauthorization="handleAgreePrivacyForPhone">同意</button>
|
||||
</view>
|
||||
<view class="login-modal-cancel" bindtap="closeLoginModal">取消</view>
|
||||
<view class="login-agree-row" catchtap="toggleAgree">
|
||||
<view class="agree-checkbox {{agreeProtocol ? 'agree-checked' : ''}}"><icon wx:if="{{agreeProtocol}}" name="check" size="24" color="#34C759"></icon></view>
|
||||
|
||||
@@ -225,6 +225,10 @@
|
||||
.agree-text { color: rgba(255,255,255,0.6); }
|
||||
.agree-link { color: #4FD1C5; text-decoration: underline; padding: 0 4rpx; }
|
||||
.btn-wechat-disabled { opacity: 0.6; }
|
||||
.privacy-wechat-row { margin: 24rpx 0; padding: 24rpx; background: rgba(0,206,209,0.1); border-radius: 16rpx; }
|
||||
.privacy-wechat-desc { display: block; font-size: 26rpx; color: rgba(255,255,255,0.8); margin-bottom: 16rpx; }
|
||||
.privacy-agree-btn { width: 100%; padding: 20rpx; background: #07C160; color: #fff; font-size: 28rpx; border-radius: 16rpx; border: none; }
|
||||
.privacy-agree-btn::after { border: none; }
|
||||
|
||||
/* 头像弹窗 */
|
||||
.avatar-modal .avatar-modal-title { display: block; font-size: 36rpx; font-weight: bold; color: #fff; text-align: center; margin-bottom: 16rpx; }
|
||||
|
||||
@@ -75,6 +75,7 @@ Page({
|
||||
giftRequestSn: '',
|
||||
showLoginModal: false,
|
||||
agreeProtocol: false,
|
||||
showPrivacyModal: false,
|
||||
showPosterModal: false,
|
||||
isPaying: false,
|
||||
isGeneratingPoster: false,
|
||||
@@ -982,7 +983,7 @@ Page({
|
||||
},
|
||||
|
||||
closeLoginModal() {
|
||||
this.setData({ showLoginModal: false })
|
||||
this.setData({ showLoginModal: false, showPrivacyModal: false })
|
||||
},
|
||||
|
||||
toggleAgree() {
|
||||
@@ -1018,6 +1019,16 @@ Page({
|
||||
}
|
||||
},
|
||||
|
||||
// 微信隐私协议同意(getPhoneNumber 需先同意)
|
||||
handleAgreePrivacyForPhone() {
|
||||
const app = getApp()
|
||||
if (app._privacyResolve) {
|
||||
app._privacyResolve({ buttonId: 'agree-privacy-btn', event: 'agree' })
|
||||
app._privacyResolve = null
|
||||
}
|
||||
this.setData({ showPrivacyModal: false })
|
||||
},
|
||||
|
||||
// 【重构】手机号登录(标准流程)
|
||||
async handlePhoneLogin(e) {
|
||||
if (!e.detail.code) {
|
||||
|
||||
@@ -131,7 +131,7 @@
|
||||
<text class="paywall-desc">已阅读50%,登录后查看完整内容</text>
|
||||
|
||||
<view class="login-btn" bindtap="showLoginModal">
|
||||
<text class="login-btn-text">立即登录</text>
|
||||
<text class="login-btn-text">手机号一键登录</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -341,11 +341,15 @@
|
||||
<text class="login-title">登录 卡若创业派对</text>
|
||||
<text class="login-desc">登录后可购买章节、解锁更多内容</text>
|
||||
|
||||
<button class="btn-wechat {{agreeProtocol ? '' : 'btn-wechat-disabled'}}" bindtap="handleWechatLogin" disabled="{{!agreeProtocol}}">
|
||||
<button id="agree-phone-btn" class="btn-wechat {{agreeProtocol ? '' : 'btn-wechat-disabled'}}" open-type="getPhoneNumber|agreePrivacyAuthorization" bindgetphonenumber="handlePhoneLogin" bindagreeprivacyauthorization="handleAgreePrivacyForPhone" disabled="{{!agreeProtocol}}">
|
||||
<text class="btn-wechat-icon">微</text>
|
||||
<text>微信快捷登录</text>
|
||||
<text>手机号一键登录</text>
|
||||
</button>
|
||||
|
||||
<view class="privacy-wechat-row" wx:if="{{showPrivacyModal}}">
|
||||
<text class="privacy-wechat-desc">为获取手机号,请先同意《用户隐私保护指引》</text>
|
||||
<button id="agree-privacy-btn" class="privacy-agree-btn" open-type="agreePrivacyAuthorization" bindagreeprivacyauthorization="handleAgreePrivacyForPhone">同意</button>
|
||||
</view>
|
||||
<view class="login-agree-row" catchtap="toggleAgree">
|
||||
<view class="agree-checkbox {{agreeProtocol ? 'agree-checked' : ''}}"><icon wx:if="{{agreeProtocol}}" name="check" size="24" color="#34C759"></icon></view>
|
||||
<text class="agree-text">我已阅读并同意</text>
|
||||
|
||||
@@ -1164,6 +1164,10 @@
|
||||
padding: 0 4rpx;
|
||||
}
|
||||
.btn-wechat-disabled { opacity: 0.6; }
|
||||
.privacy-wechat-row { margin: 24rpx 0; padding: 24rpx; background: rgba(0,206,209,0.1); border-radius: 16rpx; }
|
||||
.privacy-wechat-desc { display: block; font-size: 26rpx; color: rgba(255,255,255,0.8); margin-bottom: 16rpx; }
|
||||
.privacy-agree-btn { width: 100%; padding: 20rpx; background: #07C160; color: #fff; font-size: 28rpx; border-radius: 16rpx; border: none; }
|
||||
.privacy-agree-btn::after { border: none; }
|
||||
|
||||
/* ===== 支付中加载 ===== */
|
||||
.loading-box {
|
||||
|
||||
@@ -30,7 +30,8 @@ Page({
|
||||
// 绑定弹窗
|
||||
showBindModal: false,
|
||||
bindType: '', // phone | wechat | alipay
|
||||
bindValue: ''
|
||||
bindValue: '',
|
||||
showPrivacyModal: false
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
@@ -332,6 +333,15 @@ Page({
|
||||
}
|
||||
},
|
||||
|
||||
// 微信隐私协议同意(getPhoneNumber 需先同意)
|
||||
onAgreePrivacyForPhone() {
|
||||
if (app._privacyResolve) {
|
||||
app._privacyResolve({ buttonId: 'agree-privacy-btn', event: 'agree' })
|
||||
app._privacyResolve = null
|
||||
}
|
||||
this.setData({ showPrivacyModal: false })
|
||||
},
|
||||
|
||||
// 一键获取微信手机号(button组件回调)
|
||||
async onGetPhoneNumber(e) {
|
||||
console.log('[Settings] 获取手机号回调:', e.detail)
|
||||
@@ -392,6 +402,11 @@ Page({
|
||||
this.setData({ showBindModal: false })
|
||||
},
|
||||
|
||||
// 跳转账户密码登录页(开发)
|
||||
goToDevLogin() {
|
||||
wx.navigateTo({ url: '/pages/dev-login/dev-login' })
|
||||
},
|
||||
|
||||
// 打开切换账号弹窗(开发)
|
||||
openSwitchAccountModal() {
|
||||
this.setData({
|
||||
|
||||
@@ -32,11 +32,15 @@
|
||||
</view>
|
||||
<view class="bind-right">
|
||||
<icon wx:if="{{phoneNumber}}" name="check" size="36" color="#34C759" customClass="bind-check"></icon>
|
||||
<button wx:else class="get-phone-btn" open-type="getPhoneNumber" bindgetphonenumber="onGetPhoneNumber">
|
||||
<button wx:else id="agree-settings-phone-btn" class="get-phone-btn" open-type="getPhoneNumber|agreePrivacyAuthorization" bindgetphonenumber="onGetPhoneNumber" bindagreeprivacyauthorization="onAgreePrivacyForPhone">
|
||||
一键获取
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
<view class="privacy-wechat-row" wx:if="{{showPrivacyModal}}">
|
||||
<text class="privacy-wechat-desc">为获取手机号,请先同意《用户隐私保护指引》</text>
|
||||
<button id="agree-privacy-btn" class="privacy-agree-btn" open-type="agreePrivacyAuthorization" bindagreeprivacyauthorization="onAgreePrivacyForPhone">同意</button>
|
||||
</view>
|
||||
|
||||
<!-- 微信号 - 简化输入 -->
|
||||
<view class="bind-item">
|
||||
@@ -117,6 +121,13 @@
|
||||
<text class="dev-switch-desc">输入 userId 切换为其他账号调试</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="dev-switch-card" wx:if="{{isDevMode}}" bindtap="goToDevLogin">
|
||||
<view class="dev-switch-inner">
|
||||
<icon name="smartphone" size="40" color="#8e8e93" customClass="dev-switch-icon"></icon>
|
||||
<text class="dev-switch-text">账户密码登录</text>
|
||||
<text class="dev-switch-desc">输入对方手机号登录,密码可留空</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="logout-btn" wx:if="{{isLoggedIn}}" bindtap="handleLogout">退出登录</view>
|
||||
</view>
|
||||
|
||||
@@ -49,6 +49,10 @@
|
||||
line-height: normal;
|
||||
}
|
||||
.get-phone-btn::after { border: none; }
|
||||
.privacy-wechat-row { margin: 24rpx 0; padding: 24rpx; background: rgba(0,206,209,0.1); border-radius: 16rpx; }
|
||||
.privacy-wechat-desc { display: block; font-size: 26rpx; color: rgba(255,255,255,0.8); margin-bottom: 16rpx; }
|
||||
.privacy-agree-btn { width: 100%; padding: 20rpx; background: #07C160; color: #fff; font-size: 28rpx; border-radius: 16rpx; border: none; }
|
||||
.privacy-agree-btn::after { border: none; }
|
||||
|
||||
/* 自动提现卡片 */
|
||||
.auto-withdraw-card { margin-top: 24rpx; }
|
||||
|
||||
@@ -23,12 +23,19 @@
|
||||
"condition": {
|
||||
"miniprogram": {
|
||||
"list": [
|
||||
{
|
||||
"name": "开发登录",
|
||||
"pathName": "pages/dev-login/dev-login",
|
||||
"query": "",
|
||||
"scene": null,
|
||||
"launchMode": "default"
|
||||
},
|
||||
{
|
||||
"name": "pages/member-detail/member-detail",
|
||||
"pathName": "pages/member-detail/member-detail",
|
||||
"query": "id=ogpTW5cVMxd5afBBtXdvmeMO8aho",
|
||||
"scene": null,
|
||||
"launchMode": "default"
|
||||
"launchMode": "default",
|
||||
"scene": null
|
||||
},
|
||||
{
|
||||
"name": "pages/my/my",
|
||||
|
||||
166
scripts/test/web/admin_routes_smoke.py
Normal file
166
scripts/test/web/admin_routes_smoke.py
Normal file
@@ -0,0 +1,166 @@
|
||||
import json
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
import requests
|
||||
|
||||
|
||||
ROUTER_GO = r"e:\\Gongsi\\Mycontent\\soul-api\\internal\\router\\router.go"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Route:
|
||||
group: str # "admin" | "db" | "root"
|
||||
method: str
|
||||
path: str # path within the group, e.g. "/chapters" or "/admin"
|
||||
full_path: str # full path appended to API_BASE_URL
|
||||
|
||||
|
||||
def _read_text(path: str) -> str:
|
||||
with open(path, "r", encoding="utf-8", errors="ignore") as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
def extract_admin_and_db_routes() -> list[tuple[str, str]]:
|
||||
"""
|
||||
返回 [(method, full_path_template), ...]
|
||||
full_path_template 已包含 /api/admin 或 /api/db 前缀,保留 :id 占位符。
|
||||
"""
|
||||
text = _read_text(ROUTER_GO)
|
||||
|
||||
routes: list[tuple[str, str]] = []
|
||||
|
||||
# 1) /api/admin 登录/鉴权/登出(不是 admin group 内)
|
||||
# api.GET("/admin", ...) / api.POST("/admin", ...) / api.POST("/admin/logout", ...)
|
||||
for m in re.finditer(r'api\.(GET|POST|PUT|DELETE)\("(/admin(?:/[^"]*)?)",\s*handler\.[A-Za-z0-9_]+', text):
|
||||
routes.append((m.group(1), f"/api{m.group(2)}"))
|
||||
|
||||
# 2) admin group:api.Group("/admin") + admin.(GET|POST|PUT|DELETE)("/xxx", ...)
|
||||
for m in re.finditer(r'admin\.(GET|POST|PUT|DELETE)\("(/[^"]*)",\s*handler\.[A-Za-z0-9_]+', text):
|
||||
routes.append((m.group(1), f"/api/admin{m.group(2)}"))
|
||||
|
||||
# 3) db group:api.Group("/db") + db.(GET|POST|PUT|DELETE)("/xxx", ...)
|
||||
for m in re.finditer(r'db\.(GET|POST|PUT|DELETE)\("(/[^"]*)",\s*handler\.[A-Za-z0-9_]+', text):
|
||||
routes.append((m.group(1), f"/api/db{m.group(2)}"))
|
||||
|
||||
# 去重(同一 handler 可能存在重复注册)
|
||||
seen: set[tuple[str, str]] = set()
|
||||
out: list[tuple[str, str]] = []
|
||||
for method, p in routes:
|
||||
k = (method, p)
|
||||
if k in seen:
|
||||
continue
|
||||
seen.add(k)
|
||||
out.append((method, p))
|
||||
return out
|
||||
|
||||
|
||||
def replace_path_params(path: str) -> str:
|
||||
# 仅用于 smoke:把 :id 替换成一个固定占位
|
||||
return path.replace(":id", "1")
|
||||
|
||||
|
||||
def request_json(
|
||||
session: requests.Session,
|
||||
method: str,
|
||||
url: str,
|
||||
headers: dict[str, str],
|
||||
payload: Any | None = None,
|
||||
raw_body: str | None = None,
|
||||
) -> tuple[int, dict[str, Any] | None, str]:
|
||||
try:
|
||||
if raw_body is not None:
|
||||
resp = session.request(method, url, headers=headers, data=raw_body, timeout=10)
|
||||
elif payload is None:
|
||||
resp = session.request(method, url, headers=headers, timeout=10)
|
||||
else:
|
||||
resp = session.request(method, url, headers=headers, data=json.dumps(payload), timeout=10)
|
||||
text = resp.text or ""
|
||||
try:
|
||||
data = resp.json()
|
||||
except Exception:
|
||||
data = None
|
||||
return resp.status_code, data, text[:300]
|
||||
except Exception as e:
|
||||
return 0, None, f"EXC: {e}"
|
||||
|
||||
|
||||
def main() -> None:
|
||||
api_base = None
|
||||
# 优先使用本地默认;需要对接测试环境时在 PowerShell 设置 SOUL_API_BASE
|
||||
import os
|
||||
|
||||
api_base = (os.environ.get("SOUL_API_BASE") or "").rstrip("/")
|
||||
if not api_base:
|
||||
# 默认本机
|
||||
api_base = "http://localhost:8080"
|
||||
|
||||
admin_username = os.environ.get("SOUL_ADMIN_USERNAME", "admin")
|
||||
admin_password = os.environ.get("SOUL_ADMIN_PASSWORD", "admin123")
|
||||
|
||||
session = requests.Session()
|
||||
|
||||
# 本 smoke 默认不验证 TLS(如果你用的是 https 且是自签证书,能跑通测试)
|
||||
session.verify = False
|
||||
|
||||
# 登录拿 token
|
||||
login_url = f"{api_base}/api/admin"
|
||||
r = session.post(login_url, json={"username": admin_username, "password": admin_password}, timeout=10)
|
||||
try:
|
||||
login_data = r.json()
|
||||
except Exception:
|
||||
login_data = None
|
||||
if r.status_code != 200 or not (login_data and login_data.get("success") is True and login_data.get("token")):
|
||||
print("LOGIN_FAILED", r.status_code, r.text[:200])
|
||||
return
|
||||
|
||||
token = login_data["token"]
|
||||
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
|
||||
|
||||
routes = extract_admin_and_db_routes()
|
||||
print(f"Found routes: {len(routes)}")
|
||||
|
||||
failures: list[dict[str, Any]] = []
|
||||
unexpected_success: list[dict[str, Any]] = []
|
||||
|
||||
for method, path_template in routes:
|
||||
path = replace_path_params(path_template)
|
||||
url = f"{api_base}{path}"
|
||||
|
||||
payload = None
|
||||
raw_body = None
|
||||
if method in ("POST", "PUT", "DELETE"):
|
||||
# 安全模式:发送明显非法 JSON,尽量触发 ShouldBindJSON 失败,避免真实写入。
|
||||
payload = None
|
||||
raw_body = "{invalid_json"
|
||||
|
||||
status, data, preview = request_json(
|
||||
session, method, url, headers, payload=payload, raw_body=raw_body
|
||||
)
|
||||
|
||||
ok = status not in (404, 500) and status != 0
|
||||
# POST/PUT/DELETE 在安全模式下不应返回 success=true
|
||||
if method in ("POST", "PUT", "DELETE") and data and data.get("success") is True:
|
||||
unexpected_success.append(
|
||||
{"method": method, "path": path, "status": status, "data": data, "preview": preview}
|
||||
)
|
||||
|
||||
if not ok:
|
||||
failures.append({"method": method, "path": path, "status": status, "data": data, "preview": preview})
|
||||
|
||||
print("\n=== SMOKE_RESULT ===")
|
||||
print("Failures(404/500/EXC):", len(failures))
|
||||
if failures:
|
||||
for it in failures:
|
||||
print(f"- {it['method']} {it['path']} -> {it['status']}, preview={it.get('preview')}")
|
||||
|
||||
print("\nUnexpected success on write calls:", len(unexpected_success))
|
||||
if unexpected_success:
|
||||
for it in unexpected_success:
|
||||
print(f"- {it['method']} {it['path']} -> success=true (status {it['status']}, preview={it.get('preview')})")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
132
scripts/test/web/admin_routes_smoke_authless.py
Normal file
132
scripts/test/web/admin_routes_smoke_authless.py
Normal file
@@ -0,0 +1,132 @@
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
|
||||
import requests
|
||||
|
||||
|
||||
ROUTER_GO = r"e:\\Gongsi\\Mycontent\\soul-api\\internal\\router\\router.go"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Check:
|
||||
method: str
|
||||
path: str
|
||||
status: int
|
||||
preview: str
|
||||
|
||||
|
||||
def _read_text(path: str) -> str:
|
||||
with open(path, "r", encoding="utf-8", errors="ignore") as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
def extract_routes() -> list[tuple[str, str]]:
|
||||
"""
|
||||
返回 [(method, full_path_template), ...]
|
||||
full_path_template 保留 :id 占位符。
|
||||
"""
|
||||
text = _read_text(ROUTER_GO)
|
||||
routes: list[tuple[str, str]] = []
|
||||
|
||||
# /api/admin 登录/鉴权/登出
|
||||
for m in re.finditer(r'api\.(GET|POST|PUT|DELETE)\("(/admin(?:/[^"]*)?)",\s*handler\.[A-Za-z0-9_]+', text):
|
||||
routes.append((m.group(1), f"/api{m.group(2)}"))
|
||||
|
||||
# /api/admin 组
|
||||
for m in re.finditer(r'admin\.(GET|POST|PUT|DELETE)\("(/[^"]*)",\s*handler\.[A-Za-z0-9_]+', text):
|
||||
routes.append((m.group(1), f"/api/admin{m.group(2)}"))
|
||||
|
||||
# /api/db 组
|
||||
for m in re.finditer(r'db\.(GET|POST|PUT|DELETE)\("(/[^"]*)",\s*handler\.[A-Za-z0-9_]+', text):
|
||||
routes.append((m.group(1), f"/api/db{m.group(2)}"))
|
||||
|
||||
# 去重
|
||||
seen = set()
|
||||
out = []
|
||||
for method, p in routes:
|
||||
if (method, p) in seen:
|
||||
continue
|
||||
seen.add((method, p))
|
||||
out.append((method, p))
|
||||
return out
|
||||
|
||||
|
||||
def replace_path_params(path: str) -> str:
|
||||
return path.replace(":id", "1")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
import os
|
||||
|
||||
api_base = (os.environ.get("SOUL_API_BASE") or "http://localhost:8080").rstrip("/")
|
||||
session = requests.Session()
|
||||
session.verify = False # 如为 https 自签证书也可探测
|
||||
|
||||
routes = extract_routes()
|
||||
print(f"Found routes: {len(routes)}")
|
||||
|
||||
failures: list[Check] = []
|
||||
unexpected: list[Check] = []
|
||||
|
||||
headers = {"Content-Type": "application/json"}
|
||||
|
||||
# 先验证登录接口是否通(只对 /api/admin POST 登录做一次带凭证的检查)
|
||||
admin_username = os.environ.get("SOUL_ADMIN_USERNAME", "admin")
|
||||
admin_password = os.environ.get("SOUL_ADMIN_PASSWORD", "admin123")
|
||||
login_url = f"{api_base}/api/admin"
|
||||
r_login = session.post(
|
||||
login_url,
|
||||
json={"username": admin_username, "password": admin_password},
|
||||
headers=headers,
|
||||
timeout=10,
|
||||
)
|
||||
try:
|
||||
login_data = r_login.json()
|
||||
except Exception:
|
||||
login_data = None
|
||||
if r_login.status_code != 200 or not (login_data and login_data.get("success") is True and login_data.get("token")):
|
||||
failures.append(Check("POST", "/api/admin", r_login.status_code, (r_login.text or "")[:200]))
|
||||
print("LOGIN_CHECK_FAILED,后续路由鉴权探测可能不准确。")
|
||||
|
||||
for method, path_template in routes:
|
||||
path = replace_path_params(path_template)
|
||||
url = f"{api_base}{path}"
|
||||
|
||||
# 仅对登录接口放行;其他都不带 token,避免触发写操作
|
||||
json_payload = None
|
||||
if path == "/api/admin" and method == "POST":
|
||||
# 已在上面验证登录;这里跳过
|
||||
continue
|
||||
|
||||
if method in ("POST", "PUT"):
|
||||
# 发空 body,通常也会被 AdminAuth 在更早阶段拦截
|
||||
json_payload = {}
|
||||
|
||||
try:
|
||||
resp = session.request(method, url, headers=headers, json=json_payload, timeout=10)
|
||||
status = resp.status_code
|
||||
preview = (resp.text or "")[:200].replace("\n", " ")
|
||||
except Exception as e:
|
||||
failures.append(Check(method, path, 0, f"EXC: {e}"))
|
||||
continue
|
||||
|
||||
# 非登录接口:预期 AdminAuth 拦截 => 401 或 403
|
||||
if status not in (401, 403):
|
||||
unexpected.append(Check(method, path, status, preview))
|
||||
|
||||
print("\n=== AUTHLESS_SMOKE_RESULT ===")
|
||||
print("Failures(0/404/500 等异常/网络异常):", len(failures))
|
||||
for it in failures[:30]:
|
||||
print(f"- {it.method} {it.path} -> {it.status}, preview={it.preview}")
|
||||
|
||||
print("Unexpected (非 401/403):", len(unexpected))
|
||||
for it in unexpected[:30]:
|
||||
print(f"- {it.method} {it.path} -> {it.status}, preview={it.preview}")
|
||||
|
||||
if len(unexpected) > 30:
|
||||
print("... truncated")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
1
soul-admin/dist/assets/index-BHvHUv2T.css
vendored
Normal file
1
soul-admin/dist/assets/index-BHvHUv2T.css
vendored
Normal file
File diff suppressed because one or more lines are too long
914
soul-admin/dist/assets/index-B_-tFdMt.js
vendored
914
soul-admin/dist/assets/index-B_-tFdMt.js
vendored
File diff suppressed because one or more lines are too long
1
soul-admin/dist/assets/index-CCZxs-BW.css
vendored
1
soul-admin/dist/assets/index-CCZxs-BW.css
vendored
File diff suppressed because one or more lines are too long
914
soul-admin/dist/assets/index-uFsIOb55.js
vendored
Normal file
914
soul-admin/dist/assets/index-uFsIOb55.js
vendored
Normal file
File diff suppressed because one or more lines are too long
4
soul-admin/dist/index.html
vendored
4
soul-admin/dist/index.html
vendored
@@ -4,8 +4,8 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>管理后台 - Soul创业派对</title>
|
||||
<script type="module" crossorigin src="/assets/index-B_-tFdMt.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-CCZxs-BW.css">
|
||||
<script type="module" crossorigin src="/assets/index-uFsIOb55.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-BHvHUv2T.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* 章节树 - 仿照 catalog 设计,支持篇、章、节拖拽排序
|
||||
* 整行可拖拽;节和章可跨篇
|
||||
*/
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { ChevronRight, ChevronDown, BookOpen, Edit3, Trash2, GripVertical, Plus, Star } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
Eye,
|
||||
Undo2,
|
||||
Settings,
|
||||
Zap,
|
||||
} from 'lucide-react'
|
||||
import { ReferralSettingsPage } from '@/pages/referral-settings/ReferralSettingsPage'
|
||||
import { Pagination } from '@/components/ui/Pagination'
|
||||
@@ -23,6 +24,7 @@ import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -86,6 +88,7 @@ interface Withdrawal {
|
||||
account?: string
|
||||
name?: string
|
||||
status: string
|
||||
remark?: string
|
||||
createdAt?: string
|
||||
processedAt?: string
|
||||
}
|
||||
@@ -146,6 +149,8 @@ export function DistributionPage() {
|
||||
const [rejectWithdrawalId, setRejectWithdrawalId] = useState<string | null>(null)
|
||||
const [rejectReason, setRejectReason] = useState('')
|
||||
const [rejectLoading, setRejectLoading] = useState(false)
|
||||
const [enableAutoApprove, setEnableAutoApprove] = useState(false)
|
||||
const [autoApproveLoading, setAutoApproveLoading] = useState(false)
|
||||
const [giftPayRequests, setGiftPayRequests] = useState<Array<{
|
||||
id: string
|
||||
requestSn: string
|
||||
@@ -189,6 +194,10 @@ export function DistributionPage() {
|
||||
}
|
||||
}, [page, pageSize, statusFilter, searchTerm, giftPayPage, giftPayStatusFilter])
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTab === 'withdrawals') loadAutoApprove()
|
||||
}, [activeTab])
|
||||
|
||||
async function loadInitialData() {
|
||||
setError(null)
|
||||
try {
|
||||
@@ -408,6 +417,39 @@ export function DistributionPage() {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadAutoApprove() {
|
||||
try {
|
||||
const data = await get<{ success?: boolean; enableAutoApprove?: boolean }>(
|
||||
'/api/admin/withdrawals/auto-approve'
|
||||
)
|
||||
if (data?.success && typeof data.enableAutoApprove === 'boolean') {
|
||||
setEnableAutoApprove(data.enableAutoApprove)
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleAutoApprove(checked: boolean) {
|
||||
setAutoApproveLoading(true)
|
||||
try {
|
||||
const data = await put<{ success?: boolean; error?: string }>(
|
||||
'/api/admin/withdrawals/auto-approve',
|
||||
{ enableAutoApprove: checked }
|
||||
)
|
||||
if (data?.success) {
|
||||
setEnableAutoApprove(checked)
|
||||
toast.success(checked ? '已开启自动审批,新提现将自动打款' : '已关闭自动审批')
|
||||
} else {
|
||||
toast.error('更新失败: ' + (data?.error ?? ''))
|
||||
}
|
||||
} catch {
|
||||
toast.error('更新失败')
|
||||
} finally {
|
||||
setAutoApproveLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
function closeRejectDialog() {
|
||||
if (rejectWithdrawalId) toast.info('已取消操作')
|
||||
setRejectWithdrawalId(null)
|
||||
@@ -1187,6 +1229,16 @@ export function DistributionPage() {
|
||||
<option value="completed">已完成</option>
|
||||
<option value="rejected">已拒绝</option>
|
||||
</select>
|
||||
<div className="flex items-center gap-2 px-4 py-2 rounded-lg bg-[#0f2137] border border-gray-700/50 shrink-0">
|
||||
<Zap className="w-4 h-4 text-[#38bdac]" />
|
||||
<span className="text-sm text-gray-300">自动审批</span>
|
||||
<Switch
|
||||
checked={enableAutoApprove}
|
||||
onCheckedChange={toggleAutoApprove}
|
||||
disabled={autoApproveLoading}
|
||||
className="data-[state=checked]:bg-[#38bdac]"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
@@ -1213,6 +1265,7 @@ export function DistributionPage() {
|
||||
<th className="p-4 text-left font-medium">收款账号</th>
|
||||
<th className="p-4 text-left font-medium">申请时间</th>
|
||||
<th className="p-4 text-left font-medium">状态</th>
|
||||
<th className="p-4 text-left font-medium">备注</th>
|
||||
<th className="p-4 text-right font-medium">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -1267,6 +1320,18 @@ export function DistributionPage() {
|
||||
: '-'}
|
||||
</td>
|
||||
<td className="p-4">{getStatusBadge(withdrawal.status)}</td>
|
||||
<td className="p-4 max-w-[160px]">
|
||||
<span
|
||||
className={`text-xs ${
|
||||
withdrawal.status === 'rejected' || withdrawal.status === 'failed'
|
||||
? 'text-red-400'
|
||||
: 'text-gray-400'
|
||||
}`}
|
||||
title={withdrawal.remark}
|
||||
>
|
||||
{withdrawal.remark || '-'}
|
||||
</span>
|
||||
</td>
|
||||
<td className="p-4 text-right">
|
||||
{withdrawal.status === 'pending' && (
|
||||
<div className="flex gap-2 justify-end">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import toast from '@/utils/toast'
|
||||
import toast from '@/utils/toast'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Label } from '@/components/ui/label'
|
||||
@@ -15,6 +15,7 @@ interface ReferralConfig {
|
||||
minWithdrawAmount: number
|
||||
bindingDays: number
|
||||
userDiscount: number
|
||||
withdrawFee: number
|
||||
enableAutoWithdraw: boolean
|
||||
vipOrderShareVip: number
|
||||
vipOrderShareNonVip: number
|
||||
@@ -25,6 +26,7 @@ const DEFAULT: ReferralConfig = {
|
||||
minWithdrawAmount: 10,
|
||||
bindingDays: 30,
|
||||
userDiscount: 5,
|
||||
withdrawFee: 5,
|
||||
enableAutoWithdraw: false,
|
||||
vipOrderShareVip: 20,
|
||||
vipOrderShareNonVip: 10,
|
||||
@@ -49,6 +51,7 @@ export function ReferralSettingsPage(_props?: ReferralSettingsPageProps & { embe
|
||||
minWithdrawAmount: c.minWithdrawAmount ?? 10,
|
||||
bindingDays: c.bindingDays ?? 30,
|
||||
userDiscount: c.userDiscount ?? 5,
|
||||
withdrawFee: c.withdrawFee ?? 5,
|
||||
enableAutoWithdraw: c.enableAutoWithdraw ?? false,
|
||||
vipOrderShareVip: c.vipOrderShareVip ?? 20,
|
||||
vipOrderShareNonVip: c.vipOrderShareNonVip ?? 10,
|
||||
@@ -67,6 +70,7 @@ export function ReferralSettingsPage(_props?: ReferralSettingsPageProps & { embe
|
||||
minWithdrawAmount: Number(config.minWithdrawAmount) || 0,
|
||||
bindingDays: Number(config.bindingDays) || 0,
|
||||
userDiscount: Number(config.userDiscount) || 0,
|
||||
withdrawFee: Number(config.withdrawFee) ?? 5,
|
||||
enableAutoWithdraw: Boolean(config.enableAutoWithdraw),
|
||||
vipOrderShareVip: Number(config.vipOrderShareVip) || 20,
|
||||
vipOrderShareNonVip: Number(config.vipOrderShareNonVip) || 10,
|
||||
@@ -265,6 +269,22 @@ export function ReferralSettingsPage(_props?: ReferralSettingsPageProps & { embe
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="text-gray-300">提现手续费(%)</Label>
|
||||
<Input
|
||||
type="number"
|
||||
min={0}
|
||||
max={100}
|
||||
step={0.5}
|
||||
className="bg-[#0a1628] border-gray-700 text-white"
|
||||
value={config.withdrawFee}
|
||||
onChange={handleNumberChange('withdrawFee')}
|
||||
/>
|
||||
<p className="text-xs text-gray-500">
|
||||
批准提现时按此比例扣除后打款,如 5 表示申请 100 元实际到账 95 元。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="text-gray-300 flex items-center gap-2">
|
||||
自动提现开关
|
||||
|
||||
@@ -44,7 +44,6 @@ import {
|
||||
ChevronUp,
|
||||
Crown,
|
||||
Tag,
|
||||
CircleDot,
|
||||
UserPlus as LeadIcon,
|
||||
} from 'lucide-react'
|
||||
import { UserDetailModal } from '@/components/modules/user/UserDetailModal'
|
||||
|
||||
@@ -33,6 +33,7 @@ interface Withdrawal {
|
||||
account?: string
|
||||
name?: string
|
||||
userConfirmedAt?: string | null
|
||||
remark?: string
|
||||
userCommissionInfo?: {
|
||||
totalCommission: number
|
||||
withdrawnEarnings: number
|
||||
@@ -247,14 +248,14 @@ export function WithdrawalsPage() {
|
||||
<p className="text-gray-400 mt-1">管理用户分销收益的提现申请</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={loadWithdrawals}
|
||||
disabled={loading}
|
||||
className="border-gray-600 text-gray-300 hover:bg-gray-700/50 bg-transparent"
|
||||
>
|
||||
<RefreshCw className={`w-4 h-4 mr-2 ${loading ? 'animate-spin' : ''}`} />
|
||||
刷新
|
||||
</Button>
|
||||
variant="outline"
|
||||
onClick={loadWithdrawals}
|
||||
disabled={loading}
|
||||
className="border-gray-600 text-gray-300 hover:bg-gray-700/50 bg-transparent"
|
||||
>
|
||||
<RefreshCw className={`w-4 h-4 mr-2 ${loading ? 'animate-spin' : ''}`} />
|
||||
刷新
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 分账规则说明 */}
|
||||
@@ -277,7 +278,7 @@ export function WithdrawalsPage() {
|
||||
</p>
|
||||
<p>
|
||||
• <span className="text-[#38bdac]">审批流程</span>
|
||||
:待处理的提现需管理员手动确认打款后批准
|
||||
:待处理的提现需管理员手动确认打款后批准(自动审批开关在推广中心-提现审核)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -365,6 +366,7 @@ export function WithdrawalsPage() {
|
||||
<th className="p-4 text-left font-medium">提现金额</th>
|
||||
<th className="p-4 text-left font-medium">用户佣金信息</th>
|
||||
<th className="p-4 text-left font-medium">状态</th>
|
||||
<th className="p-4 text-left font-medium">备注</th>
|
||||
<th className="p-4 text-left font-medium">处理时间</th>
|
||||
<th className="p-4 text-left font-medium">确认收款</th>
|
||||
<th className="p-4 text-right font-medium">操作</th>
|
||||
@@ -444,9 +446,18 @@ export function WithdrawalsPage() {
|
||||
</td>
|
||||
<td className="p-4">
|
||||
{getStatusBadge(w.status)}
|
||||
{w.errorMessage && (
|
||||
<p className="text-xs text-red-400 mt-1">{w.errorMessage}</p>
|
||||
)}
|
||||
</td>
|
||||
<td className="p-4 max-w-[180px]">
|
||||
<span
|
||||
className={`text-xs ${
|
||||
w.status === 'rejected' || w.status === 'failed'
|
||||
? 'text-red-400'
|
||||
: 'text-gray-400'
|
||||
}`}
|
||||
title={w.remark}
|
||||
>
|
||||
{w.remark || '-'}
|
||||
</span>
|
||||
</td>
|
||||
<td className="p-4 text-gray-400">
|
||||
{w.processedAt ? new Date(w.processedAt).toLocaleString() : '-'}
|
||||
|
||||
@@ -7,16 +7,19 @@ soul-api 一键部署到宝塔【测试环境】
|
||||
- 默认:本地 go build → Dockerfile.local 打镜像(--pull=false 不拉 base 镜像)
|
||||
- 首次部署前请本地先拉好:alpine:3.19、redis:7-alpine,后续一律用本地缓存
|
||||
|
||||
两种模式均部署到测试环境 /www/wwwroot/self/soul-dev:
|
||||
三种模式:
|
||||
|
||||
- runner(推荐):容器内红蓝切换,宝塔固定 proxy_pass 到 9001,无需改配置
|
||||
- 使用 network_mode: host,避免 iptables 端口映射问题
|
||||
- 首次:服务器执行 deploy/runner-init.sh 构建并启动容器
|
||||
- 部署:python devloy.py --mode runner
|
||||
|
||||
- docker(默认):本地 go build → Dockerfile.local 打镜像 → 宿主机蓝绿切换
|
||||
- 需宿主机改 Nginx proxy_pass 或宝塔 API
|
||||
|
||||
- binary:Go 二进制 + 宝塔 soulDev 项目,用 .env.development
|
||||
- docker(默认):本地 go build → Dockerfile.local 打镜像 → 蓝绿无缝切换
|
||||
- 镜像内包含:二进制、soul-api/certs/ → /app/certs/、选定环境文件 → /app/.env
|
||||
- 环境文件:--env-file 或 DOCKER_ENV_FILE,否则自动 .env.development > .env.production > .env(.dockerignore 已放行)
|
||||
- 不加 --docker-in-go 时:本地 Go 编译 + 本地 base 镜像,不联网
|
||||
- 加 --docker-in-go 时:在 Docker 内用 golang 镜像编译(需本地已有 golang:1.25)
|
||||
|
||||
环境变量:DEPLOY_DOCKER_PATH、DEPLOY_NGINX_CONF、DEPLOY_HOST 等
|
||||
环境变量:DEPLOY_DOCKER_PATH、DEPLOY_NGINX_CONF、DEPLOY_HOST、DEPLOY_RUNNER_CONTAINER 等
|
||||
"""
|
||||
|
||||
from __future__ import print_function
|
||||
@@ -65,12 +68,14 @@ def get_cfg():
|
||||
bt_url = (os.environ.get("BT_PANEL_URL") or "").strip().rstrip("/")
|
||||
if not bt_url:
|
||||
bt_url = "https://%s:9988" % host
|
||||
deploy_path = os.environ.get("DEPLOY_DOCKER_PATH", DEPLOY_DOCKER_PATH).rstrip("/")
|
||||
return {
|
||||
"host": host,
|
||||
"user": os.environ.get("DEPLOY_USER", "root"),
|
||||
"password": os.environ.get("DEPLOY_PASSWORD", "Zhiqun1984"),
|
||||
"ssh_key": os.environ.get("DEPLOY_SSH_KEY", ""),
|
||||
"project_path": os.environ.get("DEPLOY_PROJECT_PATH", DEPLOY_PROJECT_PATH),
|
||||
"project_path": os.environ.get("DEPLOY_PROJECT_PATH", DEPLOY_PROJECT_PATH).rstrip("/"),
|
||||
"deploy_path": deploy_path,
|
||||
"bt_panel_url": bt_url,
|
||||
"bt_api_key": os.environ.get("BT_API_KEY", BT_API_KEY_DEFAULT),
|
||||
"bt_go_project_name": os.environ.get("BT_GO_PROJECT_NAME", "soulDev"),
|
||||
@@ -171,6 +176,49 @@ def set_env_mini_program_state(env_path, state):
|
||||
f.writelines(new_lines)
|
||||
|
||||
|
||||
def set_env_key(env_path, key, value):
|
||||
"""将 .env 中指定 key 设为 value"""
|
||||
if not os.path.isfile(env_path):
|
||||
return
|
||||
with open(env_path, "r", encoding="utf-8", errors="replace") as f:
|
||||
lines = f.readlines()
|
||||
found = False
|
||||
new_lines = []
|
||||
for line in lines:
|
||||
s = line.strip()
|
||||
if "=" in s and s.split("=", 1)[0].strip() == key:
|
||||
new_lines.append("%s=%s\n" % (key, value))
|
||||
found = True
|
||||
else:
|
||||
new_lines.append(line)
|
||||
if not found:
|
||||
new_lines.append("%s=%s\n" % (key, value))
|
||||
with open(env_path, "w", encoding="utf-8", newline="\n") as f:
|
||||
f.writelines(new_lines)
|
||||
|
||||
|
||||
def set_env_redis_url(env_path, url):
|
||||
"""将 .env 中的 REDIS_URL 设为指定值"""
|
||||
if not os.path.isfile(env_path):
|
||||
return
|
||||
key = "REDIS_URL"
|
||||
with open(env_path, "r", encoding="utf-8", errors="replace") as f:
|
||||
lines = f.readlines()
|
||||
found = False
|
||||
new_lines = []
|
||||
for line in lines:
|
||||
s = line.strip()
|
||||
if "=" in s and s.split("=", 1)[0].strip() == key:
|
||||
new_lines.append("%s=%s\n" % (key, url))
|
||||
found = True
|
||||
else:
|
||||
new_lines.append(line)
|
||||
if not found:
|
||||
new_lines.append("%s=%s\n" % (key, url))
|
||||
with open(env_path, "w", encoding="utf-8", newline="\n") as f:
|
||||
f.writelines(new_lines)
|
||||
|
||||
|
||||
def resolve_binary_pack_env_src(root):
|
||||
"""binary 模式 tar 包内 .env 的来源,与 Docker 自动优先级一致。"""
|
||||
for name in (".env.development", ".env.production", ".env"):
|
||||
@@ -180,6 +228,52 @@ def resolve_binary_pack_env_src(root):
|
||||
return None, None
|
||||
|
||||
|
||||
def pack_runner_deploy(root, binary_path, include_env=True):
|
||||
"""打包 Runner 部署包:二进制 + .env + certs,供容器内红蓝切换"""
|
||||
print("[2/4] 打包 Runner 部署包 ...")
|
||||
staging = tempfile.mkdtemp(prefix="soul_api_runner_deploy_")
|
||||
try:
|
||||
shutil.copy2(binary_path, os.path.join(staging, "soul-api"))
|
||||
staging_env = os.path.join(staging, ".env")
|
||||
if include_env:
|
||||
env_src, env_label = resolve_binary_pack_env_src(root)
|
||||
if env_src:
|
||||
shutil.copy2(env_src, staging_env)
|
||||
print(" [已包含] %s -> .env" % env_label)
|
||||
else:
|
||||
env_example = os.path.join(root, ".env.example")
|
||||
if os.path.isfile(env_example):
|
||||
shutil.copy2(env_example, staging_env)
|
||||
print(" [已包含] .env.example -> .env")
|
||||
if os.path.isfile(staging_env):
|
||||
set_env_port(staging_env, 18081)
|
||||
set_env_redis_url(staging_env, "redis://:soul-docker-redis@127.0.0.1:6379/0")
|
||||
set_env_mini_program_state(staging_env, "developer")
|
||||
set_env_key(staging_env, "UPLOAD_DIR", "/app/uploads")
|
||||
print(" [已设置] PORT=18081, REDIS_URL, UPLOAD_DIR=/app/uploads, WECHAT_MINI_PROGRAM_STATE=developer")
|
||||
certs_src = os.path.join(root, "certs")
|
||||
if os.path.isdir(certs_src):
|
||||
certs_dst = os.path.join(staging, "certs")
|
||||
os.makedirs(certs_dst, exist_ok=True)
|
||||
for f in os.listdir(certs_src):
|
||||
src = os.path.join(certs_src, f)
|
||||
if os.path.isfile(src):
|
||||
shutil.copy2(src, os.path.join(certs_dst, f))
|
||||
print(" [已包含] certs/")
|
||||
tarball = os.path.join(tempfile.gettempdir(), "soul_api_deploy.tar.gz")
|
||||
with tarfile.open(tarball, "w:gz") as tf:
|
||||
for name in os.listdir(staging):
|
||||
p = os.path.join(staging, name)
|
||||
tf.add(p, arcname=name)
|
||||
print(" [成功] 打包完成: %s (%.2f MB)" % (tarball, os.path.getsize(tarball) / 1024 / 1024))
|
||||
return tarball
|
||||
except Exception as e:
|
||||
print(" [失败] 打包异常:", str(e))
|
||||
return None
|
||||
finally:
|
||||
shutil.rmtree(staging, ignore_errors=True)
|
||||
|
||||
|
||||
def pack_deploy(root, binary_path, include_env=True):
|
||||
"""打包二进制和 .env 为 tar.gz"""
|
||||
print("[2/4] 打包部署文件 ...")
|
||||
@@ -277,8 +371,8 @@ def upload_and_extract(cfg, tarball_path, no_restart=False, restart_method="auto
|
||||
else:
|
||||
client.connect(cfg["host"], port=DEFAULT_SSH_PORT, username=cfg["user"], password=cfg["password"], timeout=15)
|
||||
sftp = client.open_sftp()
|
||||
remote_tar = "/tmp/soul_api_deploy.tar.gz"
|
||||
project_path = cfg["project_path"]
|
||||
remote_tar = project_path + "/soul_api_deploy.tar.gz"
|
||||
sftp.put(tarball_path, remote_tar)
|
||||
sftp.close()
|
||||
|
||||
@@ -524,7 +618,8 @@ def pack_docker_image(root):
|
||||
|
||||
def upload_and_deploy_docker(cfg, image_tar_path, include_env=True, deploy_method="ssh"):
|
||||
"""上传镜像与配置到服务器,执行蓝绿部署。deploy_method: ssh=脚本内 Nginx 切换, btapi=宝塔 API 更新 Nginx"""
|
||||
deploy_path = os.environ.get("DEPLOY_DOCKER_PATH", DEPLOY_DOCKER_PATH)
|
||||
deploy_path = cfg.get("deploy_path") or os.environ.get("DEPLOY_DOCKER_PATH", DEPLOY_DOCKER_PATH)
|
||||
deploy_path = deploy_path.rstrip("/")
|
||||
nginx_conf = os.environ.get("DEPLOY_NGINX_CONF", DEPLOY_NGINX_CONF)
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
@@ -542,7 +637,7 @@ def upload_and_deploy_docker(cfg, image_tar_path, include_env=True, deploy_metho
|
||||
client.connect(cfg["host"], port=DEFAULT_SSH_PORT, username=cfg["user"], password=cfg["password"], timeout=30)
|
||||
sftp = client.open_sftp()
|
||||
|
||||
remote_tar = "/tmp/soul_api_image.tar.gz"
|
||||
remote_tar = deploy_path + "/soul_api_image.tar.gz"
|
||||
sftp.put(image_tar_path, remote_tar)
|
||||
print(" [已上传] 镜像 tar.gz")
|
||||
|
||||
@@ -611,13 +706,157 @@ def upload_and_deploy_docker(cfg, image_tar_path, include_env=True, deploy_metho
|
||||
client.close()
|
||||
|
||||
|
||||
# ==================== Runner 部署(容器内红蓝切换) ====================
|
||||
|
||||
RUNNER_CONTAINER = os.environ.get("DEPLOY_RUNNER_CONTAINER", "soul-api-runner")
|
||||
CHUNK_SIZE = 65536
|
||||
|
||||
|
||||
def _ssh_connect(cfg, timeout=30):
|
||||
"""建立 SSH 连接"""
|
||||
client = paramiko.SSHClient()
|
||||
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
if cfg.get("ssh_key") and os.path.isfile(cfg["ssh_key"]):
|
||||
client.connect(cfg["host"], port=DEFAULT_SSH_PORT, username=cfg["user"], key_filename=cfg["ssh_key"], timeout=timeout)
|
||||
else:
|
||||
client.connect(cfg["host"], port=DEFAULT_SSH_PORT, username=cfg["user"], password=cfg["password"], timeout=timeout)
|
||||
return client
|
||||
|
||||
|
||||
def deploy_runner_container(cfg):
|
||||
"""推送 Runner 容器到服务器:本地构建镜像 → 上传 → docker load → compose up"""
|
||||
print("=" * 60)
|
||||
print(" soul-api Runner 容器推送(首次或更新容器)")
|
||||
print("=" * 60)
|
||||
deploy_path = cfg.get("deploy_path") or os.environ.get("DEPLOY_DOCKER_PATH", DEPLOY_DOCKER_PATH)
|
||||
deploy_path = deploy_path.rstrip("/")
|
||||
root = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
print("[1/4] 本地构建 Runner 镜像 ...")
|
||||
try:
|
||||
r = subprocess.run(
|
||||
["docker", "build", "-f", "deploy/Dockerfile.runner", "-t", "soul-api-runner:latest", "."],
|
||||
cwd=root, shell=False, timeout=120, capture_output=True, text=True, encoding="utf-8", errors="replace"
|
||||
)
|
||||
if r.returncode != 0:
|
||||
print(" [失败] docker build 失败:", (r.stderr or "")[-500:])
|
||||
return False
|
||||
except FileNotFoundError:
|
||||
print(" [失败] 未找到 docker 命令")
|
||||
return False
|
||||
|
||||
print("[2/4] 导出镜像为 tar.gz ...")
|
||||
import gzip
|
||||
img_tar = os.path.join(tempfile.gettempdir(), "soul_runner_image.tar.gz")
|
||||
try:
|
||||
r = subprocess.run(["docker", "save", "soul-api-runner:latest"], capture_output=True, timeout=180, cwd=root)
|
||||
if r.returncode != 0:
|
||||
print(" [失败] docker save 失败")
|
||||
return False
|
||||
with gzip.open(img_tar, "wb") as f:
|
||||
f.write(r.stdout)
|
||||
except Exception as e:
|
||||
print(" [失败] 导出异常:", str(e))
|
||||
return False
|
||||
|
||||
print("[3/4] SSH 上传镜像并加载 ...")
|
||||
client = None
|
||||
try:
|
||||
client = _ssh_connect(cfg, timeout=60)
|
||||
sftp = client.open_sftp()
|
||||
remote_img = deploy_path + "/soul_runner_image.tar.gz"
|
||||
sftp.put(img_tar, remote_img)
|
||||
sftp.close()
|
||||
os.remove(img_tar)
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
compose_standalone = os.path.join(script_dir, "deploy", "docker-compose.runner.standalone.yml")
|
||||
sftp = client.open_sftp()
|
||||
sftp.put(compose_standalone, deploy_path + "/docker-compose.runner.standalone.yml")
|
||||
sftp.close()
|
||||
cmd = (
|
||||
"mkdir -p %s && cd %s && gunzip -c soul_runner_image.tar.gz | docker load && "
|
||||
"docker compose -f docker-compose.runner.standalone.yml up -d && "
|
||||
"rm -f soul_runner_image.tar.gz && echo OK"
|
||||
) % (deploy_path, deploy_path)
|
||||
stdin, stdout, stderr = client.exec_command(cmd, timeout=300)
|
||||
out = stdout.read().decode("utf-8", errors="replace")
|
||||
err = stderr.read().decode("utf-8", errors="replace")
|
||||
exit_status = stdout.channel.recv_exit_status()
|
||||
print(out)
|
||||
if err:
|
||||
print(err[:500])
|
||||
if exit_status != 0 or "OK" not in out:
|
||||
print(" [失败] 远程执行退出码:", exit_status)
|
||||
return False
|
||||
except Exception as e:
|
||||
print(" [失败] SSH 错误:", str(e))
|
||||
return False
|
||||
finally:
|
||||
if client:
|
||||
client.close()
|
||||
|
||||
print("[4/4] Runner 容器已启动")
|
||||
print(" 宝塔 proxy_pass 保持 127.0.0.1:9001")
|
||||
return True
|
||||
|
||||
|
||||
def upload_and_deploy_runner(cfg, tarball_path):
|
||||
"""将部署包通过 SSH 管道直接传入容器,宿主机不落盘,防止机密泄露"""
|
||||
print("[3/4] 管道直传容器(宿主机不落盘)...")
|
||||
if not cfg.get("password") and not cfg.get("ssh_key"):
|
||||
print(" [失败] 请设置 DEPLOY_PASSWORD 或 DEPLOY_SSH_KEY")
|
||||
return False
|
||||
client = None
|
||||
try:
|
||||
client = _ssh_connect(cfg, timeout=60)
|
||||
file_size = os.path.getsize(tarball_path)
|
||||
print(" 传输 %.2f MB 到容器 /tmp/incoming.tar.gz ..." % (file_size / 1024 / 1024))
|
||||
stdin, stdout, stderr = client.exec_command(
|
||||
"docker exec -i %s sh -c 'cat > /tmp/incoming.tar.gz'" % RUNNER_CONTAINER,
|
||||
timeout=300,
|
||||
)
|
||||
with open(tarball_path, "rb") as f:
|
||||
while True:
|
||||
chunk = f.read(CHUNK_SIZE)
|
||||
if not chunk:
|
||||
break
|
||||
stdin.write(chunk)
|
||||
stdin.channel.shutdown_write()
|
||||
stdout.channel.recv_exit_status()
|
||||
err = stderr.read().decode("utf-8", errors="replace")
|
||||
if err and "Error" in err:
|
||||
print(" [失败] 管道写入异常:", err[:300])
|
||||
return False
|
||||
print(" [已传入容器] 执行红蓝切换 ...")
|
||||
stdin, stdout, stderr = client.exec_command(
|
||||
"docker exec %s /app/deploy.sh /tmp/incoming.tar.gz" % RUNNER_CONTAINER,
|
||||
timeout=180,
|
||||
)
|
||||
out = stdout.read().decode("utf-8", errors="replace")
|
||||
err = stderr.read().decode("utf-8", errors="replace")
|
||||
exit_status = stdout.channel.recv_exit_status()
|
||||
print(out)
|
||||
if err:
|
||||
print(err[:800])
|
||||
if exit_status != 0:
|
||||
print(" [失败] 远程部署退出码:", exit_status)
|
||||
return False
|
||||
return True
|
||||
except Exception as e:
|
||||
print(" [失败] SSH 错误: %s" % str(e))
|
||||
return False
|
||||
finally:
|
||||
if client:
|
||||
client.close()
|
||||
|
||||
|
||||
# ==================== 主函数 ====================
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="soul-api 测试环境一键部署到宝塔")
|
||||
parser.add_argument("--mode", choices=("binary", "docker"), default="docker",
|
||||
help="docker=Docker 蓝绿部署 (默认), binary=Go 二进制")
|
||||
parser.add_argument("--mode", choices=("binary", "docker", "runner", "start"), default="runner",
|
||||
help="runner=仅上传代码(默认), start=容器+代码, docker=Docker蓝绿, binary=Go二进制")
|
||||
parser.add_argument("--no-build", action="store_true", help="跳过本地编译/构建")
|
||||
parser.add_argument("--no-env", action="store_true",
|
||||
help="binary: 不打进 tar;docker: 不上传服务器目录 .env.production(镜像内配置不变)")
|
||||
@@ -630,12 +869,52 @@ def main():
|
||||
help="[docker] 部署方式: ssh=脚本内 Nginx 切换, btapi=宝塔 API 更新 Nginx 配置并重载 (默认 ssh)")
|
||||
parser.add_argument("--env-file", default=None, metavar="NAME",
|
||||
help="[docker] 打入镜像的环境文件名(默认自动:.env.development > .env.production > .env)")
|
||||
parser.add_argument("--init-runner", action="store_true",
|
||||
help="[runner] 等同于 --mode start,先推送容器再部署代码")
|
||||
args = parser.parse_args()
|
||||
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
root = script_dir
|
||||
cfg = get_cfg()
|
||||
|
||||
# start = 容器+代码;runner = 仅代码
|
||||
init_runner = args.init_runner or (args.mode == "start")
|
||||
if args.mode == "start":
|
||||
args.mode = "runner"
|
||||
|
||||
if args.mode == "runner":
|
||||
print("=" * 60)
|
||||
print(" soul-api Runner 模式(容器内红蓝切换,宝塔固定 9001)")
|
||||
print("=" * 60)
|
||||
print(" 服务器: %s@%s:%s" % (cfg["user"], cfg["host"], DEFAULT_SSH_PORT))
|
||||
print(" 容器: %s" % RUNNER_CONTAINER)
|
||||
print("=" * 60)
|
||||
if init_runner:
|
||||
if not deploy_runner_container(cfg):
|
||||
return 1
|
||||
binary_path = os.path.join(root, "soul-api")
|
||||
if not args.no_build:
|
||||
p = run_build(root)
|
||||
if not p:
|
||||
return 1
|
||||
else:
|
||||
if not os.path.isfile(binary_path):
|
||||
print("[错误] 未找到 soul-api 二进制")
|
||||
return 1
|
||||
print("[1/4] 跳过编译")
|
||||
tarball = pack_runner_deploy(root, binary_path, include_env=not args.no_env)
|
||||
if not tarball:
|
||||
return 1
|
||||
if not upload_and_deploy_runner(cfg, tarball):
|
||||
return 1
|
||||
try:
|
||||
os.remove(tarball)
|
||||
except Exception:
|
||||
pass
|
||||
print("")
|
||||
print(" 部署完成!宝塔代理 9001 无需修改")
|
||||
return 0
|
||||
|
||||
if args.mode == "docker":
|
||||
docker_path = os.environ.get("DEPLOY_DOCKER_PATH", DEPLOY_DOCKER_PATH)
|
||||
print("=" * 60)
|
||||
|
||||
@@ -8,13 +8,66 @@ import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"soul-api/internal/cache"
|
||||
"soul-api/internal/database"
|
||||
"soul-api/internal/model"
|
||||
"soul-api/internal/wechat"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// AdminWithdrawalsAutoApproveGet GET /api/admin/withdrawals/auto-approve 获取自动审批开关状态
|
||||
func AdminWithdrawalsAutoApproveGet(c *gin.Context) {
|
||||
db := database.DB()
|
||||
enabled := false
|
||||
var refCfg model.SystemConfig
|
||||
if err := db.Where("config_key = ?", "referral_config").First(&refCfg).Error; err == nil {
|
||||
var val map[string]interface{}
|
||||
if err := json.Unmarshal(refCfg.ConfigValue, &val); err == nil {
|
||||
if v, ok := val["enableAutoWithdraw"].(bool); ok {
|
||||
enabled = v
|
||||
}
|
||||
}
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "enableAutoApprove": enabled})
|
||||
}
|
||||
|
||||
// AdminWithdrawalsAutoApprovePut PUT /api/admin/withdrawals/auto-approve 设置自动审批开关
|
||||
func AdminWithdrawalsAutoApprovePut(c *gin.Context) {
|
||||
var body struct {
|
||||
EnableAutoApprove bool `json:"enableAutoApprove"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "请求体无效"})
|
||||
return
|
||||
}
|
||||
db := database.DB()
|
||||
var refCfg model.SystemConfig
|
||||
val := map[string]interface{}{
|
||||
"distributorShare": float64(90), "minWithdrawAmount": float64(10), "bindingDays": float64(30),
|
||||
"userDiscount": float64(5), "withdrawFee": float64(5), "enableAutoWithdraw": body.EnableAutoApprove,
|
||||
"vipOrderShareVip": float64(20), "vipOrderShareNonVip": float64(10),
|
||||
}
|
||||
if err := db.Where("config_key = ?", "referral_config").First(&refCfg).Error; err == nil {
|
||||
if err := json.Unmarshal(refCfg.ConfigValue, &val); err == nil {
|
||||
val["enableAutoWithdraw"] = body.EnableAutoApprove
|
||||
}
|
||||
}
|
||||
valBytes, _ := json.Marshal(val)
|
||||
desc := "分销 / 推广规则配置"
|
||||
if err := db.Where("config_key = ?", "referral_config").First(&refCfg).Error; err != nil {
|
||||
refCfg = model.SystemConfig{ConfigKey: "referral_config", ConfigValue: valBytes, Description: &desc}
|
||||
_ = db.Create(&refCfg)
|
||||
} else {
|
||||
refCfg.ConfigValue = valBytes
|
||||
refCfg.Description = &desc
|
||||
_ = db.Save(&refCfg)
|
||||
}
|
||||
cache.InvalidateConfig()
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "enableAutoApprove": body.EnableAutoApprove, "message": "已更新"})
|
||||
}
|
||||
|
||||
// AdminWithdrawalsList GET /api/admin/withdrawals(支持分页 page、pageSize,筛选 status)
|
||||
func AdminWithdrawalsList(c *gin.Context) {
|
||||
statusFilter := c.Query("status")
|
||||
@@ -97,11 +150,24 @@ func AdminWithdrawalsList(c *gin.Context) {
|
||||
if userAvatar != nil {
|
||||
avStr = resolveAvatarURL(*userAvatar)
|
||||
}
|
||||
// 备注:失败时显示 failReason/errorMessage,否则显示用户 remark
|
||||
remark := ""
|
||||
if st == "rejected" || st == "failed" {
|
||||
if w.FailReason != nil && *w.FailReason != "" {
|
||||
remark = *w.FailReason
|
||||
} else if w.ErrorMessage != nil && *w.ErrorMessage != "" {
|
||||
remark = *w.ErrorMessage
|
||||
}
|
||||
}
|
||||
if remark == "" && w.Remark != nil && *w.Remark != "" {
|
||||
remark = *w.Remark
|
||||
}
|
||||
withdrawals = append(withdrawals, gin.H{
|
||||
"id": w.ID, "userId": w.UserID, "userName": userName, "userAvatar": avStr,
|
||||
"amount": w.Amount, "status": st, "createdAt": w.CreatedAt,
|
||||
"method": "wechat", "account": account,
|
||||
"userConfirmedAt": userConfirmedAt,
|
||||
"remark": remark,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -127,6 +193,109 @@ func AdminWithdrawalsList(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// doApproveWithdrawal 执行提现审批逻辑(打款),供 AdminWithdrawalsAction 与自动审批共用
|
||||
// 返回 (successMessage, error),成功时 err 为 nil
|
||||
func doApproveWithdrawal(db *gorm.DB, id string) (string, error) {
|
||||
now := time.Now()
|
||||
var w model.Withdrawal
|
||||
if err := db.Where("id = ?", id).First(&w).Error; err != nil {
|
||||
return "", fmt.Errorf("提现记录不存在")
|
||||
}
|
||||
st := ""
|
||||
if w.Status != nil {
|
||||
st = *w.Status
|
||||
}
|
||||
if st != "pending" && st != "processing" && st != "pending_confirm" {
|
||||
return "", fmt.Errorf("当前状态不允许批准")
|
||||
}
|
||||
openID := ""
|
||||
if w.WechatOpenid != nil && *w.WechatOpenid != "" {
|
||||
openID = *w.WechatOpenid
|
||||
}
|
||||
if openID == "" {
|
||||
var u model.User
|
||||
if err := db.Where("id = ?", w.UserID).First(&u).Error; err == nil && u.OpenID != nil {
|
||||
openID = *u.OpenID
|
||||
}
|
||||
}
|
||||
if openID == "" {
|
||||
return "", fmt.Errorf("用户未绑定微信 openid,无法打款")
|
||||
}
|
||||
_, totalCommission, withdrawn, pending, _ := computeAvailableWithdraw(db, w.UserID)
|
||||
availableRaw := totalCommission - withdrawn - pending
|
||||
if availableRaw < -0.01 {
|
||||
return "", fmt.Errorf("用户当前可提现不足,无法批准")
|
||||
}
|
||||
remark := "提现"
|
||||
if w.Remark != nil && *w.Remark != "" {
|
||||
remark = *w.Remark
|
||||
}
|
||||
withdrawFee := 0.0
|
||||
var refCfg model.SystemConfig
|
||||
if err := db.Where("config_key = ?", "referral_config").First(&refCfg).Error; err == nil {
|
||||
var refVal map[string]interface{}
|
||||
if err := json.Unmarshal(refCfg.ConfigValue, &refVal); err == nil {
|
||||
if v, ok := refVal["withdrawFee"].(float64); ok {
|
||||
withdrawFee = v / 100
|
||||
}
|
||||
}
|
||||
}
|
||||
actualAmount := w.Amount * (1 - withdrawFee)
|
||||
if actualAmount < 0.01 {
|
||||
actualAmount = 0.01
|
||||
}
|
||||
amountFen := int(actualAmount * 100)
|
||||
if amountFen < 1 {
|
||||
return "", fmt.Errorf("提现金额异常")
|
||||
}
|
||||
params := wechat.FundAppTransferParams{
|
||||
OutBillNo: w.ID, OpenID: openID, Amount: amountFen, Remark: remark,
|
||||
NotifyURL: "", TransferSceneId: "1005",
|
||||
}
|
||||
result, err := wechat.InitiateTransferByFundApp(params)
|
||||
if err != nil {
|
||||
errMsg := err.Error()
|
||||
if errMsg == "支付/转账未初始化,请先调用 wechat.Init" || errMsg == "转账客户端未初始化" {
|
||||
_ = db.Model(&w).Updates(map[string]interface{}{"status": "success", "processed_at": now}).Error
|
||||
return "已标记为已打款。当前未接入微信转账,请线下打款。", nil
|
||||
}
|
||||
_ = db.Model(&w).Updates(map[string]interface{}{
|
||||
"status": "failed", "fail_reason": errMsg, "error_message": errMsg, "processed_at": now,
|
||||
}).Error
|
||||
return "", fmt.Errorf("%s", errMsg)
|
||||
}
|
||||
if result.OutBillNo == "" {
|
||||
failMsg := "微信未返回商户单号,请检查商户平台(如 IP 白名单)或查看服务端日志"
|
||||
_ = db.Model(&w).Updates(map[string]interface{}{
|
||||
"status": "failed", "fail_reason": failMsg, "error_message": failMsg, "processed_at": now,
|
||||
}).Error
|
||||
return "", fmt.Errorf("%s", failMsg)
|
||||
}
|
||||
rowStatus := "processing"
|
||||
if result.State == "WAIT_USER_CONFIRM" {
|
||||
rowStatus = "pending_confirm"
|
||||
}
|
||||
upd := map[string]interface{}{
|
||||
"status": rowStatus, "detail_no": result.OutBillNo, "batch_no": result.OutBillNo,
|
||||
"batch_id": result.TransferBillNo, "processed_at": now,
|
||||
}
|
||||
if result.PackageInfo != "" {
|
||||
upd["package_info"] = result.PackageInfo
|
||||
}
|
||||
if err := db.Model(&w).Updates(upd).Error; err != nil {
|
||||
return "", fmt.Errorf("更新状态失败: %w", err)
|
||||
}
|
||||
if openID != "" {
|
||||
go func() {
|
||||
ctx := context.Background()
|
||||
if e := wechat.SendWithdrawSubscribeMessage(ctx, openID, w.Amount, true); e != nil {
|
||||
fmt.Printf("[AdminWithdrawals] 订阅消息发送失败 id=%s: %v\n", id, e)
|
||||
}
|
||||
}()
|
||||
}
|
||||
return "已发起打款,微信处理中", nil
|
||||
}
|
||||
|
||||
// AdminWithdrawalsAction PUT /api/admin/withdrawals 审核/打款
|
||||
// approve:先调微信转账接口打款,成功则标为 processing,失败则标为 failed 并返回错误。
|
||||
// 若未初始化微信转账客户端,则仅将状态标为 success(线下打款后批准)。
|
||||
@@ -169,167 +338,12 @@ func AdminWithdrawalsAction(c *gin.Context) {
|
||||
return
|
||||
|
||||
case "approve":
|
||||
var w model.Withdrawal
|
||||
if err := db.Where("id = ?", body.ID).First(&w).Error; err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "提现记录不存在"})
|
||||
return
|
||||
}
|
||||
st := ""
|
||||
if w.Status != nil {
|
||||
st = *w.Status
|
||||
}
|
||||
if st != "pending" && st != "processing" && st != "pending_confirm" {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "当前状态不允许批准"})
|
||||
return
|
||||
}
|
||||
|
||||
openID := ""
|
||||
if w.WechatOpenid != nil && *w.WechatOpenid != "" {
|
||||
openID = *w.WechatOpenid
|
||||
}
|
||||
if openID == "" {
|
||||
var u model.User
|
||||
if err := db.Where("id = ?", w.UserID).First(&u).Error; err == nil && u.OpenID != nil {
|
||||
openID = *u.OpenID
|
||||
}
|
||||
}
|
||||
if openID == "" {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "用户未绑定微信 openid,无法打款"})
|
||||
return
|
||||
}
|
||||
|
||||
// 批准前二次校验可提现金额,与申请时口径一致,防止退款/冲正后超额打款
|
||||
available, _, _, _, _ := computeAvailableWithdraw(db, w.UserID)
|
||||
if w.Amount > available {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"error": "用户当前可提现不足,无法批准",
|
||||
"message": fmt.Sprintf("用户当前可提现 ¥%.2f,本笔申请 ¥%.2f,可能因退款/冲正导致。请核对后再批或联系用户。", available, w.Amount),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 调用微信转账接口:按提现手续费扣除后打款,例如申请100元、手续费5%则实际打款95元
|
||||
remark := "提现"
|
||||
if w.Remark != nil && *w.Remark != "" {
|
||||
remark = *w.Remark
|
||||
}
|
||||
withdrawFee := 0.0
|
||||
var refCfg model.SystemConfig
|
||||
if err := db.Where("config_key = ?", "referral_config").First(&refCfg).Error; err == nil {
|
||||
var refVal map[string]interface{}
|
||||
if err := json.Unmarshal(refCfg.ConfigValue, &refVal); err == nil {
|
||||
if v, ok := refVal["withdrawFee"].(float64); ok {
|
||||
withdrawFee = v / 100
|
||||
}
|
||||
}
|
||||
}
|
||||
actualAmount := w.Amount * (1 - withdrawFee)
|
||||
if actualAmount < 0.01 {
|
||||
actualAmount = 0.01
|
||||
}
|
||||
amountFen := int(actualAmount * 100)
|
||||
if amountFen < 1 {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "提现金额异常"})
|
||||
return
|
||||
}
|
||||
outBillNo := w.ID // 商户单号,回调时 out_bill_no 即此值,用于更新该条提现
|
||||
params := wechat.FundAppTransferParams{
|
||||
OutBillNo: outBillNo,
|
||||
OpenID: openID,
|
||||
Amount: amountFen,
|
||||
Remark: remark,
|
||||
NotifyURL: "", // 由 wechat 包从配置读取 WechatTransferURL
|
||||
TransferSceneId: "1005",
|
||||
}
|
||||
|
||||
result, err := wechat.InitiateTransferByFundApp(params)
|
||||
msg, err := doApproveWithdrawal(db, body.ID)
|
||||
if err != nil {
|
||||
errMsg := err.Error()
|
||||
fmt.Printf("[AdminWithdrawals] 发起转账失败 id=%s: %s\n", body.ID, errMsg)
|
||||
// 未初始化或未配置转账:仅标记为已打款并提示线下处理
|
||||
if errMsg == "支付/转账未初始化,请先调用 wechat.Init" || errMsg == "转账客户端未初始化" {
|
||||
_ = db.Model(&w).Updates(map[string]interface{}{
|
||||
"status": "success",
|
||||
"processed_at": now,
|
||||
}).Error
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "已标记为已打款。当前未接入微信转账,请线下打款。",
|
||||
})
|
||||
return
|
||||
}
|
||||
// 微信接口报错或其它失败:把微信/具体原因返回给管理端展示,不返回「微信处理中」
|
||||
failMsg := errMsg
|
||||
_ = db.Model(&w).Updates(map[string]interface{}{
|
||||
"status": "failed",
|
||||
"fail_reason": failMsg,
|
||||
"error_message": failMsg,
|
||||
"processed_at": now,
|
||||
}).Error
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"error": "发起打款失败",
|
||||
"message": failMsg, // 管理端直接展示微信报错信息(如 IP 白名单、参数错误等)
|
||||
})
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error(), "message": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 防护:微信未返回商户单号时也按失败返回,避免管理端显示「已发起打款」却无单号
|
||||
if result.OutBillNo == "" {
|
||||
failMsg := "微信未返回商户单号,请检查商户平台(如 IP 白名单)或查看服务端日志"
|
||||
_ = db.Model(&w).Updates(map[string]interface{}{
|
||||
"status": "failed",
|
||||
"fail_reason": failMsg,
|
||||
"error_message": failMsg,
|
||||
"processed_at": now,
|
||||
}).Error
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"error": "发起打款失败",
|
||||
"message": failMsg,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 打款已受理(微信同步返回),立即落库:商户单号、微信单号、package_info、按 state 设 status(不依赖回调)
|
||||
fmt.Printf("[AdminWithdrawals] 微信已受理 id=%s out_bill_no=%s transfer_bill_no=%s state=%s\n", body.ID, result.OutBillNo, result.TransferBillNo, result.State)
|
||||
rowStatus := "processing"
|
||||
if result.State == "WAIT_USER_CONFIRM" {
|
||||
rowStatus = "pending_confirm" // 待用户在小程序点击确认收款,回调在用户确认后才触发
|
||||
}
|
||||
upd := map[string]interface{}{
|
||||
"status": rowStatus,
|
||||
"detail_no": result.OutBillNo,
|
||||
"batch_no": result.OutBillNo,
|
||||
"batch_id": result.TransferBillNo,
|
||||
"processed_at": now,
|
||||
}
|
||||
if result.PackageInfo != "" {
|
||||
upd["package_info"] = result.PackageInfo
|
||||
}
|
||||
if err := db.Model(&w).Updates(upd).Error; err != nil {
|
||||
fmt.Printf("[AdminWithdrawals] 更新提现状态失败 id=%s: %v\n", body.ID, err)
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "更新状态失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
// 发起转账成功后发订阅消息(异步,失败不影响接口返回)
|
||||
if openID != "" {
|
||||
go func() {
|
||||
ctx := context.Background()
|
||||
if err := wechat.SendWithdrawSubscribeMessage(ctx, openID, w.Amount, true); err != nil {
|
||||
fmt.Printf("[AdminWithdrawals] 订阅消息发送失败 id=%s: %v\n", body.ID, err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "已发起打款,微信处理中",
|
||||
"data": gin.H{
|
||||
"out_bill_no": result.OutBillNo,
|
||||
"transfer_bill_no": result.TransferBillNo,
|
||||
},
|
||||
})
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": msg})
|
||||
return
|
||||
|
||||
default:
|
||||
|
||||
@@ -240,6 +240,88 @@ func MiniprogramDevLoginAs(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// MiniprogramDevLoginByPhone POST /api/miniprogram/dev/login-by-phone 开发专用:按手机号登录(仅 APP_ENV=development 可用,密码可空)
|
||||
func MiniprogramDevLoginByPhone(c *gin.Context) {
|
||||
if strings.ToLower(strings.TrimSpace(os.Getenv("APP_ENV"))) != "development" {
|
||||
c.JSON(http.StatusForbidden, gin.H{"success": false, "error": "仅开发环境可用"})
|
||||
return
|
||||
}
|
||||
var req struct {
|
||||
Phone string `json:"phone" binding:"required"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "缺少手机号"})
|
||||
return
|
||||
}
|
||||
phone := strings.TrimSpace(strings.ReplaceAll(req.Phone, " ", ""))
|
||||
if phone == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "手机号不能为空"})
|
||||
return
|
||||
}
|
||||
db := database.DB()
|
||||
var user model.User
|
||||
// 支持纯数字或带 +86 前缀
|
||||
if err := db.Where("phone = ? OR phone = ? OR phone = ?", phone, "+86"+phone, "+86 "+phone).First(&user).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"success": false, "error": "该手机号未注册"})
|
||||
return
|
||||
}
|
||||
openID := getStringValue(user.OpenID)
|
||||
if openID == "" {
|
||||
openID = user.ID
|
||||
}
|
||||
tokenSuffix := openID
|
||||
if len(openID) >= 8 {
|
||||
tokenSuffix = openID[len(openID)-8:]
|
||||
}
|
||||
token := fmt.Sprintf("tk_%s_%d", tokenSuffix, time.Now().Unix())
|
||||
|
||||
var purchasedSections []string
|
||||
var orderRows []struct {
|
||||
ProductID string `gorm:"column:product_id"`
|
||||
}
|
||||
db.Raw(`SELECT DISTINCT product_id FROM orders WHERE user_id = ? AND status = 'paid' AND product_type = 'section'`, user.ID).Scan(&orderRows)
|
||||
for _, row := range orderRows {
|
||||
if row.ProductID != "" {
|
||||
purchasedSections = append(purchasedSections, row.ProductID)
|
||||
}
|
||||
}
|
||||
if purchasedSections == nil {
|
||||
purchasedSections = []string{}
|
||||
}
|
||||
|
||||
responseUser := map[string]interface{}{
|
||||
"id": user.ID,
|
||||
"openId": openID,
|
||||
"nickname": getStringValue(user.Nickname),
|
||||
"avatar": resolveAvatarURL(getStringValue(user.Avatar)),
|
||||
"phone": getStringValue(user.Phone),
|
||||
"wechatId": getStringValue(user.WechatID),
|
||||
"referralCode": getStringValue(user.ReferralCode),
|
||||
"hasFullBook": getBoolValue(user.HasFullBook),
|
||||
"purchasedSections": purchasedSections,
|
||||
"earnings": getFloatValue(user.Earnings),
|
||||
"pendingEarnings": getFloatValue(user.PendingEarnings),
|
||||
"referralCount": getIntValue(user.ReferralCount),
|
||||
"createdAt": user.CreatedAt,
|
||||
}
|
||||
if user.IsVip != nil {
|
||||
responseUser["isVip"] = *user.IsVip
|
||||
}
|
||||
if user.VipExpireDate != nil {
|
||||
responseUser["vipExpireDate"] = user.VipExpireDate.Format("2006-01-02")
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"data": map[string]interface{}{
|
||||
"openId": openID,
|
||||
"user": responseUser,
|
||||
"token": token,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 辅助函数
|
||||
func getStringValue(ptr *string) string {
|
||||
if ptr == nil {
|
||||
|
||||
@@ -51,7 +51,7 @@ func generateWithdrawID() string {
|
||||
}
|
||||
|
||||
// WithdrawPost POST /api/withdraw 创建提现申请(仅落库待审核,不调用微信打款接口)
|
||||
// 可提现逻辑与小程序 referral 页一致;二次查库校验防止超额。打款由管理端审核后手动/后续接入官方接口再处理。
|
||||
// 余额不足时也允许落库,用户侧显示「申请已提交」而非「提现失败」;管理端批准时再校验可提现,不足则拒绝。
|
||||
func WithdrawPost(c *gin.Context) {
|
||||
var req struct {
|
||||
UserID string `json:"userId" binding:"required"`
|
||||
@@ -69,14 +69,8 @@ func WithdrawPost(c *gin.Context) {
|
||||
}
|
||||
|
||||
db := database.DB()
|
||||
available, _, _, _, minWithdrawAmount := computeAvailableWithdraw(db, req.UserID)
|
||||
if req.Amount > available {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"success": false,
|
||||
"message": fmt.Sprintf("可提现金额不足(当前可提现:%.2f元)", available),
|
||||
})
|
||||
return
|
||||
}
|
||||
_, _, _, _, minWithdrawAmount := computeAvailableWithdraw(db, req.UserID)
|
||||
// 不再在此处校验余额:余额不足也落库,由管理端批准时校验并拒绝,避免用户侧直接报「提现失败」
|
||||
if req.Amount < minWithdrawAmount {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"success": false,
|
||||
@@ -119,6 +113,23 @@ func WithdrawPost(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 自动审批:若 referral_config.enableAutoWithdraw 为 true,异步执行审批打款
|
||||
var refCfg model.SystemConfig
|
||||
if err := db.Where("config_key = ?", "referral_config").First(&refCfg).Error; err == nil {
|
||||
var config map[string]interface{}
|
||||
if _ = json.Unmarshal(refCfg.ConfigValue, &config); config != nil {
|
||||
if enabled, ok := config["enableAutoWithdraw"].(bool); ok && enabled {
|
||||
go func(id string) {
|
||||
if _, e := doApproveWithdrawal(db, id); e != nil {
|
||||
fmt.Printf("[WithdrawPost] 自动审批失败 id=%s: %v\n", id, e)
|
||||
} else {
|
||||
fmt.Printf("[WithdrawPost] 自动审批成功 id=%s\n", id)
|
||||
}
|
||||
}(withdrawal.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "提现申请已提交,审核通过后将打款至您的微信零钱",
|
||||
|
||||
@@ -14,8 +14,9 @@ type Withdrawal struct {
|
||||
DetailNo *string `gorm:"column:detail_no;size:100" json:"detailNo,omitempty"` // 商家明细单号
|
||||
BatchID *string `gorm:"column:batch_id;size:100" json:"batchId,omitempty"` // 微信批次单号
|
||||
PackageInfo *string `gorm:"column:package_info;size:500" json:"packageInfo,omitempty"` // 微信返回的 package_info,供小程序 wx.requestMerchantTransfer
|
||||
Remark *string `gorm:"column:remark;size:200" json:"remark,omitempty"` // 提现备注
|
||||
FailReason *string `gorm:"column:fail_reason;size:500" json:"failReason,omitempty"` // 失败原因
|
||||
Remark *string `gorm:"column:remark;size:200" json:"remark,omitempty"` // 提现备注(用户填写)
|
||||
FailReason *string `gorm:"column:fail_reason;size:500" json:"failReason,omitempty"` // 失败原因(打款失败/拒绝时记录)
|
||||
ErrorMessage *string `gorm:"column:error_message;size:500" json:"errorMessage,omitempty"` // 错误信息(与 fail_reason 同步)
|
||||
UserConfirmedAt *time.Time `gorm:"column:user_confirmed_at" json:"userConfirmedAt,omitempty"` // 用户点击「确认收款」时间
|
||||
CreatedAt time.Time `gorm:"column:created_at" json:"createdAt"`
|
||||
ProcessedAt *time.Time `gorm:"column:processed_at" json:"processedAt"`
|
||||
|
||||
@@ -78,6 +78,8 @@ func Setup(cfg *config.Config) *gin.Engine {
|
||||
admin.GET("/withdrawals", handler.AdminWithdrawalsList)
|
||||
admin.PUT("/withdrawals", handler.AdminWithdrawalsAction)
|
||||
admin.POST("/withdrawals/sync", handler.AdminWithdrawalsSync)
|
||||
admin.GET("/withdrawals/auto-approve", handler.AdminWithdrawalsAutoApproveGet)
|
||||
admin.PUT("/withdrawals/auto-approve", handler.AdminWithdrawalsAutoApprovePut)
|
||||
admin.GET("/withdraw-test", handler.AdminWithdrawTest)
|
||||
admin.POST("/withdraw-test", handler.AdminWithdrawTest)
|
||||
admin.GET("/settings", handler.AdminSettingsGet)
|
||||
@@ -295,7 +297,8 @@ func Setup(cfg *config.Config) *gin.Engine {
|
||||
miniprogram.GET("/config", handler.GetPublicDBConfig)
|
||||
miniprogram.POST("/login", handler.MiniprogramLogin)
|
||||
miniprogram.POST("/phone-login", handler.WechatPhoneLogin)
|
||||
miniprogram.POST("/dev/login-as", handler.MiniprogramDevLoginAs) // 开发专用:按 userId 切换账号
|
||||
miniprogram.POST("/dev/login-as", handler.MiniprogramDevLoginAs) // 开发专用:按 userId 切换账号
|
||||
miniprogram.POST("/dev/login-by-phone", handler.MiniprogramDevLoginByPhone) // 开发专用:按手机号登录(密码可空)
|
||||
miniprogram.POST("/phone", handler.MiniprogramPhone)
|
||||
miniprogram.GET("/pay", handler.MiniprogramPay)
|
||||
miniprogram.POST("/pay", handler.MiniprogramPay)
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
#!/bin/bash
|
||||
# 挖矿病毒守护脚本 - 每 30 分钟扫描并清理
|
||||
# 特征: xmrig, kdevtmpfsi, kinsing, minerd, stratum, libprocesshider, watchbog 等
|
||||
# 用法: chmod +x miner_guard.sh && ./miner_guard.sh
|
||||
# 或由 cron 每 30 分钟调用: */30 * * * * /path/to/miner_guard.sh >> /var/log/miner_guard.log 2>&1
|
||||
|
||||
LOG="/var/log/miner_guard.log"
|
||||
LOCK="/var/run/miner_guard.lock"
|
||||
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
|
||||
|
||||
# 挖矿病毒特征(进程名/路径关键词)
|
||||
MINER_KEYWORDS="xmrig|xmr-stak|minerd|cpuminer|stratum|kdevtmpfsi|kinsing|libprocesshider|ddgs|watchbog|trace"
|
||||
# 可疑路径
|
||||
MINER_PATHS="/tmp/xmrig /tmp/config.json /tmp/.x /tmp/kdevtmpfsi /tmp/kinsing"
|
||||
|
||||
run_lock() {
|
||||
exec 200>"$LOCK"
|
||||
flock -n 200 || { echo "[$TIMESTAMP] 已有实例在运行,跳过" >> "$LOG"; exit 0; }
|
||||
}
|
||||
|
||||
log() { echo "[$TIMESTAMP] $1" | tee -a "$LOG"; }
|
||||
|
||||
run_lock
|
||||
|
||||
# 1. 杀进程
|
||||
for kw in xmrig kdevtmpfsi kinsing minerd xmr cpuminer; do
|
||||
pids=$(pgrep -f "$kw" 2>/dev/null)
|
||||
if [ -n "$pids" ]; then
|
||||
log "发现挖矿进程: $kw (PID: $pids),执行 kill -9"
|
||||
echo "$pids" | xargs -r kill -9 2>/dev/null
|
||||
fi
|
||||
done
|
||||
|
||||
# 2. 删除已知挖矿文件
|
||||
deleted=0
|
||||
for p in $MINER_PATHS; do
|
||||
if [ -e "$p" ]; then
|
||||
log "删除: $p"
|
||||
rm -rf "$p" 2>/dev/null && deleted=1
|
||||
fi
|
||||
done
|
||||
|
||||
# 3. 扫描 /tmp 下含挖矿关键词的可执行文件
|
||||
find /tmp /var/tmp /dev/shm -maxdepth 2 -type f -executable 2>/dev/null | while read f; do
|
||||
if echo "$f" | grep -qiE "xmrig|miner|xmr|stratum|kinsing|kdevtmpfsi"; then
|
||||
log "删除可疑可执行文件: $f"
|
||||
rm -f "$f" 2>/dev/null
|
||||
fi
|
||||
done
|
||||
|
||||
# 4. 扫描 /www/wwwroot 下的 xmrig 目录
|
||||
find /www/wwwroot -maxdepth 6 -type d -name "*xmrig*" 2>/dev/null | while read d; do
|
||||
log "删除挖矿目录: $d"
|
||||
rm -rf "$d" 2>/dev/null
|
||||
done
|
||||
find /www/wwwroot -maxdepth 6 -type f -name "xmrig" 2>/dev/null | while read f; do
|
||||
log "删除挖矿文件: $f"
|
||||
rm -f "$f" 2>/dev/null
|
||||
done
|
||||
|
||||
# 5. 清理 www 用户 crontab 中的挖矿相关项
|
||||
if crontab -u www -l 2>/dev/null | grep -qiE "xmrig|miner|curl.*tmp|wget.*tmp"; then
|
||||
log "发现 www 用户 crontab 可疑项,建议人工检查: crontab -u www -l"
|
||||
fi
|
||||
|
||||
# 6. 若本次有清理动作,记录
|
||||
if [ "$deleted" = "1" ]; then
|
||||
log "本次已清理挖矿病毒"
|
||||
fi
|
||||
|
||||
exit 0
|
||||
@@ -1,40 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""检查 miner_guard 安装状态"""
|
||||
import os, sys
|
||||
try:
|
||||
import paramiko
|
||||
except ImportError:
|
||||
print("pip install paramiko"); sys.exit(1)
|
||||
|
||||
def get_cfg():
|
||||
try:
|
||||
import importlib.util
|
||||
spec = importlib.util.spec_from_file_location("m", os.path.join(os.path.dirname(__file__), "master.py"))
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
c = mod.get_cfg()
|
||||
return {"host": c["host"], "user": c.get("user","root"), "password": c.get("password",""), "port": 22022}
|
||||
except: return {}
|
||||
|
||||
cfg = get_cfg()
|
||||
if not cfg.get("host"): print("配置失败"); sys.exit(1)
|
||||
|
||||
c = paramiko.SSHClient()
|
||||
c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
c.connect(cfg["host"], port=cfg["port"], username=cfg["user"], password=cfg["password"], timeout=15)
|
||||
|
||||
def run(cmd, to=10):
|
||||
i, o, e = c.exec_command(cmd, timeout=to)
|
||||
return o.read().decode("utf-8","replace").strip()
|
||||
|
||||
print("=== crontab -l ===")
|
||||
print(run("crontab -l 2>/dev/null"))
|
||||
print("\n=== /root/miner_guard.sh 存在? ===")
|
||||
print(run("ls -la /root/miner_guard.sh 2>/dev/null"))
|
||||
print("\n=== 手动执行一次脚本 ===")
|
||||
print(run("/bin/bash /root/miner_guard.sh 2>&1"))
|
||||
print("\n=== 日志最后5行 ===")
|
||||
print(run("tail -5 /var/log/miner_guard.log 2>/dev/null"))
|
||||
|
||||
c.close()
|
||||
@@ -1,155 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
挖矿病毒守护 - 安装脚本
|
||||
将 miner_guard.sh 上传到服务器,并配置每 30 分钟执行一次。
|
||||
使用: python miner_guard_install.py [--yes]
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import io
|
||||
|
||||
try:
|
||||
import paramiko
|
||||
except ImportError:
|
||||
print("错误: pip install paramiko")
|
||||
sys.exit(1)
|
||||
|
||||
DEFAULT_SSH_PORT = int(os.environ.get("DEPLOY_SSH_PORT", "22022"))
|
||||
|
||||
|
||||
def get_cfg():
|
||||
host = os.environ.get("DEPLOY_HOST")
|
||||
if not host:
|
||||
try:
|
||||
import importlib.util
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"master", os.path.join(os.path.dirname(__file__), "master.py")
|
||||
)
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
c = mod.get_cfg()
|
||||
return {
|
||||
"host": c["host"], "user": c.get("user", "root"),
|
||||
"password": c.get("password", ""), "ssh_key": c.get("ssh_key", ""),
|
||||
"port": int(os.environ.get("DEPLOY_SSH_PORT", str(DEFAULT_SSH_PORT))),
|
||||
}
|
||||
except Exception:
|
||||
pass
|
||||
return {
|
||||
"host": host or "", "user": os.environ.get("DEPLOY_USER", "root"),
|
||||
"password": os.environ.get("DEPLOY_PASSWORD", ""),
|
||||
"ssh_key": os.environ.get("DEPLOY_SSH_KEY", ""),
|
||||
"port": int(os.environ.get("DEPLOY_SSH_PORT", str(DEFAULT_SSH_PORT))),
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
import argparse
|
||||
p = argparse.ArgumentParser(description="安装挖矿守护到服务器")
|
||||
p.add_argument("--yes", "-y", action="store_true", help="跳过确认")
|
||||
args = p.parse_args()
|
||||
|
||||
cfg = get_cfg()
|
||||
if not cfg.get("host") or (not cfg.get("password") and not cfg.get("ssh_key")):
|
||||
print("[错误] 需配置 DEPLOY_HOST 和 DEPLOY_PASSWORD")
|
||||
sys.exit(1)
|
||||
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
local_sh = os.path.join(script_dir, "miner_guard.sh")
|
||||
if not os.path.isfile(local_sh):
|
||||
print("[错误] 未找到 miner_guard.sh")
|
||||
sys.exit(1)
|
||||
|
||||
remote_path = "/root/miner_guard.sh"
|
||||
cron_line = "*/30 * * * * /bin/bash %s >> /var/log/miner_guard.log 2>&1" % remote_path
|
||||
|
||||
print("=" * 60)
|
||||
print(" 挖矿病毒守护 - 安装")
|
||||
print("=" * 60)
|
||||
print(" 服务器: %s" % cfg["host"])
|
||||
print(" 脚本路径: %s" % remote_path)
|
||||
print(" Cron: 每 30 分钟执行")
|
||||
print("=" * 60)
|
||||
if not args.yes:
|
||||
print("\n确认安装? 输入 yes 继续: ", end="")
|
||||
if input().strip().lower() != "yes":
|
||||
print("已取消")
|
||||
return
|
||||
|
||||
client = paramiko.SSHClient()
|
||||
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
try:
|
||||
if cfg.get("ssh_key") and os.path.isfile(cfg["ssh_key"]):
|
||||
client.connect(cfg["host"], port=cfg["port"], username=cfg["user"],
|
||||
key_filename=cfg["ssh_key"], timeout=15)
|
||||
else:
|
||||
client.connect(cfg["host"], port=cfg["port"], username=cfg["user"],
|
||||
password=cfg["password"], timeout=15)
|
||||
except Exception as e:
|
||||
print("[连接失败]", str(e))
|
||||
sys.exit(1)
|
||||
|
||||
sftp = client.open_sftp()
|
||||
with open(local_sh, "rb") as f:
|
||||
sftp.putfo(f, remote_path)
|
||||
sftp.chmod(remote_path, 0o755)
|
||||
sftp.close()
|
||||
|
||||
# 通过 SSH 执行写入 /etc/cron.d/(root 有权限)
|
||||
cron_line = "*/30 * * * * root /bin/bash %s >> /var/log/miner_guard.log 2>&1" % remote_path
|
||||
cron_body = "SHELL=/bin/bash\nPATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin\n%s\n" % cron_line
|
||||
cron_d_file = "/etc/cron.d/miner_guard"
|
||||
# 用 base64 避免 shell 转义问题
|
||||
import base64
|
||||
b64 = base64.b64encode(cron_body.encode()).decode()
|
||||
stdin, stdout, stderr = client.exec_command(
|
||||
"echo %s | base64 -d | tee %s > /dev/null && chmod 644 %s" % (b64, cron_d_file, cron_d_file),
|
||||
timeout=10
|
||||
)
|
||||
err = stderr.read().decode("utf-8", errors="replace").strip()
|
||||
if err:
|
||||
print("\n[备选] /etc/cron.d 失败")
|
||||
# 尝试 crontab
|
||||
crontab_line = "*/30 * * * * /bin/bash %s >> /var/log/miner_guard.log 2>&1\n" % remote_path
|
||||
tmp_cron = "/tmp/miner_guard_cron_%s" % os.getpid()
|
||||
sftp = client.open_sftp()
|
||||
try:
|
||||
with sftp.file(tmp_cron, "w") as f:
|
||||
f.write(crontab_line)
|
||||
finally:
|
||||
sftp.close()
|
||||
sin, sout, serr = client.exec_command("crontab %s 2>&1; rm -f %s; crontab -l 2>/dev/null" % (tmp_cron, tmp_cron), timeout=10)
|
||||
out = sout.read().decode("utf-8", errors="replace")
|
||||
err_msg = serr.read().decode("utf-8", errors="replace").strip()
|
||||
if "miner_guard" in out:
|
||||
print("[成功] crontab 已添加")
|
||||
else:
|
||||
# 尝试 systemd timer
|
||||
print(" crontab 不可用,尝试 systemd timer...")
|
||||
svc = "[Unit]\nDescription=Miner Guard\nAfter=network.target\n[Service]\nType=oneshot\nExecStart=/bin/bash %s\n[Install]\nWantedBy=multi-user.target\n" % remote_path
|
||||
tmr = "[Unit]\nDescription=Miner Guard 30min\n[Timer]\nOnBootSec=1min\nOnUnitActiveSec=30min\nPersistent=true\n[Install]\nWantedBy=timers.target\n"
|
||||
try:
|
||||
sftp = client.open_sftp()
|
||||
sftp.putfo(io.BytesIO(svc.encode()), "/etc/systemd/system/miner-guard.service")
|
||||
sftp.putfo(io.BytesIO((tmr.encode())), "/etc/systemd/system/miner-guard.timer")
|
||||
sftp.close()
|
||||
sin, sout, serr = client.exec_command("systemctl daemon-reload && systemctl enable miner-guard.timer && systemctl start miner-guard.timer 2>&1", timeout=15)
|
||||
r = sout.read().decode("utf-8", errors="replace") + serr.read().decode("utf-8", errors="replace")
|
||||
if "Failed" not in r and "denied" not in r:
|
||||
print("[成功] systemd timer 已启用,每 30 分钟执行")
|
||||
else:
|
||||
raise IOError(r)
|
||||
except Exception as ex:
|
||||
print(" systemd 失败: %s" % ex)
|
||||
print(" 请 SSH 登录后手动执行: (crontab -l 2>/dev/null; echo '%s') | crontab -" % cron_line.strip())
|
||||
else:
|
||||
print("\n[成功] 已写入 %s,每 30 分钟执行" % cron_d_file)
|
||||
|
||||
client.close()
|
||||
print("\n日志: /var/log/miner_guard.log")
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,46 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""删除 /www/wwwroot/self 下发现的 xmrig 挖矿病毒"""
|
||||
import os
|
||||
import sys
|
||||
try:
|
||||
import paramiko
|
||||
except ImportError:
|
||||
print("pip install paramiko"); sys.exit(1)
|
||||
|
||||
def get_cfg():
|
||||
try:
|
||||
import importlib.util
|
||||
spec = importlib.util.spec_from_file_location("m", os.path.join(os.path.dirname(__file__), "master.py"))
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
c = mod.get_cfg()
|
||||
return {"host": c["host"], "user": c.get("user","root"), "password": c.get("password",""), "port": 22022}
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
cfg = get_cfg()
|
||||
if not cfg.get("host"):
|
||||
print("配置失败"); sys.exit(1)
|
||||
|
||||
c = paramiko.SSHClient()
|
||||
c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
c.connect(cfg["host"], port=cfg["port"], username=cfg["user"], password=cfg["password"], timeout=15)
|
||||
|
||||
target = "/www/wwwroot/self/wanzhi/tongzhi/xmrig-6.24.0"
|
||||
print("删除挖矿病毒: %s" % target)
|
||||
stdin, stdout, stderr = c.exec_command("rm -rf %s 2>&1" % target, timeout=30)
|
||||
out = stdout.read().decode("utf-8","replace").strip()
|
||||
err = stderr.read().decode("utf-8","replace").strip()
|
||||
if out: print(out)
|
||||
if err: print("stderr:", err)
|
||||
|
||||
# 验证已删除
|
||||
stdin, stdout, stderr = c.exec_command("ls -la %s 2>&1" % target, timeout=5)
|
||||
check = stdout.read().decode("utf-8","replace").strip()
|
||||
if "No such file" in check or "cannot access" in check:
|
||||
print("\n[成功] xmrig 病毒目录已删除")
|
||||
else:
|
||||
print("\n[警告] 删除可能失败:", check[:100])
|
||||
|
||||
c.close()
|
||||
@@ -26,3 +26,51 @@
|
||||
{"level":"debug","timestamp":"2026-03-19T16:41:36+08:00","caller":"kernel/baseClient.go:459","content":"------------------response content:HTTP/1.1 200 OK\r\nContent-Length: 82\r\nConnection: keep-alive\r\nContent-Type: text/plain\r\nDate: Thu, 19 Mar 2026 08:41:35 GMT\r\n\r\n{\"session_key\":\"WgxBUmbDt4ijGryl7tI4jQ==\",\"openid\":\"ogpTW5a9exdEmEwqZsYywvgSpSQg\"}"}
|
||||
{"level":"debug","timestamp":"2026-03-19T16:45:15+08:00","caller":"kernel/baseClient.go:457","content":"GET https://api.weixin.qq.com/sns/jscode2session?access_token=102_1s_JNaLepGIJ-yC3eHp8MhR-MLZVGr3Nvcun0XtSe1bHYVhgwXDfdLI3RbOFcf2sG0-17AXUpJNje2tiFvKr4q26kQBa-2OqN2_499sTEFm0QDwY4EuHDS_133ANIBeADAHCV&appid=wxb8bbb2b10dec74aa&grant_type=authorization_code&js_code=0a1ZdL000Viw2W1nc3300clDfj2ZdL0h&secret=3c1fb1f63e6e052222bbcead9d07fe0c request header: { Accept:*/*} "}
|
||||
{"level":"debug","timestamp":"2026-03-19T16:45:15+08:00","caller":"kernel/baseClient.go:459","content":"------------------response content:HTTP/1.1 200 OK\r\nContent-Length: 82\r\nConnection: keep-alive\r\nContent-Type: text/plain\r\nDate: Thu, 19 Mar 2026 08:45:14 GMT\r\n\r\n{\"session_key\":\"T4ENAqokN4b3TNGiD+7DLA==\",\"openid\":\"ogpTW5a9exdEmEwqZsYywvgSpSQg\"}"}
|
||||
{"level":"debug","timestamp":"2026-03-20T12:44:07+08:00","caller":"kernel/accessToken.go:381","content":"GET https://api.weixin.qq.com/cgi-bin/token?appid=wxb8bbb2b10dec74aa&grant_type=client_credential&neededText=&secret=3c1fb1f63e6e052222bbcead9d07fe0c request header: { Accept:*/*} "}
|
||||
{"level":"debug","timestamp":"2026-03-20T12:44:07+08:00","caller":"kernel/accessToken.go:383","content":"------------------response content:HTTP/1.1 200 OK\r\nContent-Length: 174\r\nConnection: keep-alive\r\nContent-Type: application/json; encoding=utf-8\r\nDate: Fri, 20 Mar 2026 04:44:06 GMT\r\n\r\n{\"access_token\":\"102_DPOg4ZBZnQeOr42BvOi0KNAdmD0bF9Xs5CfUzLawTllFy-lgjjcAOxvM1MWziLZQp_rGji8dJR0wWaSf9b5YhqyqUU423evyfBanz29Is-3B_uMCeQMGDjhWRvAXTIdAIAGCR\",\"expires_in\":7200}"}
|
||||
{"level":"debug","timestamp":"2026-03-20T12:44:07+08:00","caller":"kernel/baseClient.go:457","content":"GET https://api.weixin.qq.com/sns/jscode2session?access_token=102_DPOg4ZBZnQeOr42BvOi0KNAdmD0bF9Xs5CfUzLawTllFy-lgjjcAOxvM1MWziLZQp_rGji8dJR0wWaSf9b5YhqyqUU423evyfBanz29Is-3B_uMCeQMGDjhWRvAXTIdAIAGCR&appid=wxb8bbb2b10dec74aa&grant_type=authorization_code&js_code=0f3uCfHa1MgSnL007TFa1mP01b4uCfHj&secret=3c1fb1f63e6e052222bbcead9d07fe0c request header: { Accept:*/*} "}
|
||||
{"level":"debug","timestamp":"2026-03-20T12:44:07+08:00","caller":"kernel/baseClient.go:459","content":"------------------response content:HTTP/1.1 200 OK\r\nContent-Length: 82\r\nConnection: keep-alive\r\nContent-Type: text/plain\r\nDate: Fri, 20 Mar 2026 04:44:07 GMT\r\n\r\n{\"session_key\":\"d/a9oOU7MpOG90wRgBIEHA==\",\"openid\":\"ogpTW5fmXRGNpoUbXB3UEqnVe5Tg\"}"}
|
||||
{"level":"debug","timestamp":"2026-03-20T12:47:38+08:00","caller":"kernel/baseClient.go:457","content":"POST https://api.mch.weixin.qq.com/v3/fund-app/mch-transfer/transfer-bills request header: { Content-Type:application/jsonAuthorization:WECHATPAY2-SHA256-RSA2048 mchid=\"1318592501\",nonce_str=\"VhORiGFxNYTRRUJ5zb4dxXl8oijs63rz\",timestamp=\"1773982057\",serial_no=\"4A1DB62CD5C9BE0B6FC51C30621D6F99686E75C5\",signature=\"Kczv0Ok0lw289nmaaUjjVJgDDXq4bBOZrlYKQYllkPYXq0NgKcEDGphohbouxYFYEjCy6odEHi9UDWytJVvrVV3RLF3zdtp8Nf8SpLbgeSHaq3iRNlylyAHdhsSeEvakmUFw5QR0k/0xCY1fWYVfvlC+MqVYjPckw2jdAI1AHYEyxtaosM5FIK6zWJGfQGgKs49TlF1dCvHCyfbKp9HYi18RHwSgb+vpknWcsL35OHB6309RoGQWupN57yjj0mjeYGjmaJwTBE1YJAzogU6PfaRkTB29ZqbF92RQRaZl9epwnideYlc7O9KdPz8cn8mOHE9fZUF0jrShVaZTW1pFXg==\"Accept:*/*} request body:"}
|
||||
{"level":"debug","timestamp":"2026-03-20T12:47:38+08:00","caller":"kernel/baseClient.go:459","content":"------------------response content:HTTP/1.1 400 Bad Request\r\nContent-Length: 98\r\nCache-Control: no-cache, must-revalidate\r\nConnection: keep-alive\r\nContent-Language: zh-CN\r\nContent-Type: application/json; charset=utf-8\r\nDate: Fri, 20 Mar 2026 04:47:37 GMT\r\nKeep-Alive: timeout=8\r\nRequest-Id: 08E9A2F3CD06102618AFE6F8AF0120EEC60128D48101-268491067\r\nServer: nginx\r\nWechatpay-Nonce: 5051256999ad05409d4300cc4f300122\r\nWechatpay-Serial: 5F2543BF58239A4EB68FA4433DF1438A88B34B16\r\nWechatpay-Signature: P1E9gp4sU0vOErMTyNiwgLBr4coeub0g5UFO8EmLVIkzUOdrasmItGezBmg7xDv1Uo/ENTU4AdX4fS7/hIakahR0awjiQgBy2nbUCVO8tgTwk7l3eINGntHd+CU/G53B1bsr/W+enk4PDrAbAWqVlBgLH2vvkGBqTRAXvm6QrDx4yirY1HgcqJOtCyVUsSgGN6dNGw/D39Xr5i3yDp+Lfmn212lm/lY1Dx4idZi+ePNW4dIgmv836JwqJRXrzDOD8z1b4d8eEapf5MfRRsGo8NHFJMYwFJlSdapmwVxLHmWVaGls25rI3kmPLb8YOgE5YIj2cx47Z4wVB8ZBGNKETw==\r\nWechatpay-Signature-Type: WECHATPAY2-SHA256-RSA2048\r\nWechatpay-Timestamp: 1773982057\r\nX-Content-Type-Options: nosniff\r\n\r\n{\"code\":\"INVALID_REQUEST\",\"message\":\"此IP地址不允许调用接口,请按开发指引设置\"}"}
|
||||
{"level":"debug","timestamp":"2026-03-20T13:00:32+08:00","caller":"kernel/accessToken.go:381","content":"GET https://api.weixin.qq.com/cgi-bin/token?appid=wxb8bbb2b10dec74aa&grant_type=client_credential&neededText=&secret=3c1fb1f63e6e052222bbcead9d07fe0c request header: { Accept:*/*} "}
|
||||
{"level":"debug","timestamp":"2026-03-20T13:00:32+08:00","caller":"kernel/accessToken.go:383","content":"------------------response content:HTTP/1.1 200 OK\r\nContent-Length: 174\r\nConnection: keep-alive\r\nContent-Type: application/json; encoding=utf-8\r\nDate: Fri, 20 Mar 2026 05:00:31 GMT\r\n\r\n{\"access_token\":\"102_hGbH_8pS5ttLodC6gMVsF0b_c9IPXlucKff_guY7vt2DG2jaR_YObm6FvDmQ3PozLg6Tp3ihoOQgeGsxV3BZnFo8EkHRN9qBkWeaKmfMn7ptwjqtw-mrKRM57gEQNZaABAEBN\",\"expires_in\":7200}"}
|
||||
{"level":"debug","timestamp":"2026-03-20T13:00:32+08:00","caller":"kernel/baseClient.go:457","content":"GET https://api.weixin.qq.com/sns/jscode2session?access_token=102_hGbH_8pS5ttLodC6gMVsF0b_c9IPXlucKff_guY7vt2DG2jaR_YObm6FvDmQ3PozLg6Tp3ihoOQgeGsxV3BZnFo8EkHRN9qBkWeaKmfMn7ptwjqtw-mrKRM57gEQNZaABAEBN&appid=wxb8bbb2b10dec74aa&grant_type=authorization_code&js_code=0c3ihlFa1YX7oL0lXCIa1W46FX1ihlFK&secret=3c1fb1f63e6e052222bbcead9d07fe0c request header: { Accept:*/*} "}
|
||||
{"level":"debug","timestamp":"2026-03-20T13:00:32+08:00","caller":"kernel/baseClient.go:459","content":"------------------response content:HTTP/1.1 200 OK\r\nContent-Length: 82\r\nConnection: keep-alive\r\nContent-Type: text/plain\r\nDate: Fri, 20 Mar 2026 05:00:31 GMT\r\n\r\n{\"session_key\":\"K5ZPJ6/4q9e/1SvQuEKsHg==\",\"openid\":\"ogpTW5fmXRGNpoUbXB3UEqnVe5Tg\"}"}
|
||||
{"level":"debug","timestamp":"2026-03-20T13:07:28+08:00","caller":"kernel/baseClient.go:457","content":"GET https://api.weixin.qq.com/sns/jscode2session?access_token=102_hGbH_8pS5ttLodC6gMVsF0b_c9IPXlucKff_guY7vt2DG2jaR_YObm6FvDmQ3PozLg6Tp3ihoOQgeGsxV3BZnFo8EkHRN9qBkWeaKmfMn7ptwjqtw-mrKRM57gEQNZaABAEBN&appid=wxb8bbb2b10dec74aa&grant_type=authorization_code&js_code=0d3BNTFa1kKIoL083wGa1TJPyY3BNTF1&secret=3c1fb1f63e6e052222bbcead9d07fe0c request header: { Accept:*/*} "}
|
||||
{"level":"debug","timestamp":"2026-03-20T13:07:28+08:00","caller":"kernel/baseClient.go:459","content":"------------------response content:HTTP/1.1 200 OK\r\nContent-Length: 82\r\nConnection: keep-alive\r\nContent-Type: text/plain\r\nDate: Fri, 20 Mar 2026 05:07:27 GMT\r\n\r\n{\"session_key\":\"JNkHBiUKvcrB3af9Tf7fLQ==\",\"openid\":\"ogpTW5fmXRGNpoUbXB3UEqnVe5Tg\"}"}
|
||||
{"level":"debug","timestamp":"2026-03-20T13:07:29+08:00","caller":"kernel/baseClient.go:457","content":"POST https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi request header: { Content-Type:application/jsonAuthorization:WECHATPAY2-SHA256-RSA2048 mchid=\"1318592501\",nonce_str=\"Uh6ZLYqxAOzJx3DlhgYXtB5cZZfDZk1O\",timestamp=\"1773983249\",serial_no=\"4A1DB62CD5C9BE0B6FC51C30621D6F99686E75C5\",signature=\"HNqmQAl10cXL6AY94+LJucsRX+0FVOukP88YCCUD/ex1z2y0Ig8BF+MNkYafaIa5nb24wHmnNFiWRM+L5EI25Qk+51HMMWsQ4AHmu6A1+iHXPk43FMXzI5/EcGYg+FB9LDDaO5EB7jRjr4wyG35n80yZ6c78qrMgsi+Aue98+iItpRzOdrVlkafmJ8y1OeUGK3owkQrdobqSQJA11JaOsc6VzXcRUhxXDrQqwCy1pVcrQELtGos/0207eqRXun42ZDoDjvGTkuP5OPyP62UZvnlLuF1Tv8naUUVyTj3XS9kAQk0CaBduxO2J7D4B2po5sb/hjWKsQtYgVwUJNfAkaw==\"Accept:*/*} request body:"}
|
||||
{"level":"debug","timestamp":"2026-03-20T13:07:29+08:00","caller":"kernel/baseClient.go:459","content":"------------------response content:HTTP/1.1 200 OK\r\nContent-Length: 52\r\nCache-Control: no-cache, must-revalidate\r\nConnection: keep-alive\r\nContent-Language: zh-CN\r\nContent-Type: application/json; charset=utf-8\r\nDate: Fri, 20 Mar 2026 05:07:28 GMT\r\nKeep-Alive: timeout=8\r\nRequest-Id: 0890ACF3CD0610EE02189281F8AF0120A8D51828F09004-0\r\nServer: nginx\r\nWechatpay-Nonce: c398601a140d9814c4c4b7ad3909a18b\r\nWechatpay-Serial: 5F2543BF58239A4EB68FA4433DF1438A88B34B16\r\nWechatpay-Signature: vS+u6Jk0SQrs4t85GRJVKgUM5VKBc3G7TFOQSgujrXmNModtw2QCtSKMz10rerjUx6hE1LlzAA41uxi2cQGC5h7UoCwGbcbODtFdHtRfJOrdMHwQrfpDUsJPUhEfHCQ0XPrf36hQeCsu5nwXnrTIZbl8pif29wGQXFjZSQgzC41KelYjqAhHjUZLM5jTJvT4cLOa2+fT6w/LTKnxvMRz5OGhWFjm9IcB/0CWGYg9v061G1790JpQwsvNW+f5eCU5RSg4wgpQ+kaWveEED5TIo8Iil43YhFpeRQqS+ARoZ2ibHQgfG/MoC72otqh6Ic7ORUH9oggK4VYjhQmAFMR4iw==\r\nWechatpay-Signature-Type: WECHATPAY2-SHA256-RSA2048\r\nWechatpay-Timestamp: 1773983248\r\nX-Content-Type-Options: nosniff\r\n\r\n{\"prepay_id\":\"wx20130728575617ac637c82a36b9c3d0001\"}"}
|
||||
{"level":"debug","timestamp":"2026-03-20T13:07:34+08:00","caller":"kernel/baseClient.go:457","content":"POST https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi request header: { Content-Type:application/jsonAuthorization:WECHATPAY2-SHA256-RSA2048 mchid=\"1318592501\",nonce_str=\"r8RlY41EXwM4xPXdOCoBobcLSrHqD7pX\",timestamp=\"1773983254\",serial_no=\"4A1DB62CD5C9BE0B6FC51C30621D6F99686E75C5\",signature=\"kpi/tTZ5+nx+DjL222lNGS1NwMLlHApWmo0B30SUSdoMBQ1jCh7GEgr6JJygVSMQy7lX9uEXeB78ixQ456ZnKekBGXbtL2MPTbZEzTwSUpWKlZ4f5wAlPDF3JwGbzEWnViNHltoA1pJIXjXSFEbWeFekQzvyWrDBX1ea0Q+e1D3+wXq60nAQSIA2mCw69ANsgcjPstB1vl9pNDqDXyz72/dRp/vqI3tE4pjrdDhG/okxKG/JGWIdy5h7Z90ojqZi/L2lG/fiN4IePza3GTGXSQ8OtCNagnli2+sy2MBVapelmTbCU0oNGghcNLZJHJ+UMQdbQCdHR+zztf9IvlmE/w==\"Accept:*/*} request body:"}
|
||||
{"level":"debug","timestamp":"2026-03-20T13:07:34+08:00","caller":"kernel/baseClient.go:459","content":"------------------response content:HTTP/1.1 200 OK\r\nContent-Length: 52\r\nCache-Control: no-cache, must-revalidate\r\nConnection: keep-alive\r\nContent-Language: zh-CN\r\nContent-Type: application/json; charset=utf-8\r\nDate: Fri, 20 Mar 2026 05:07:33 GMT\r\nKeep-Alive: timeout=8\r\nRequest-Id: 0895ACF3CD0610C305188BDA8C5820CEEE1C28A30B-0\r\nServer: nginx\r\nWechatpay-Nonce: 52be8c275f44fdbc7381bc4f8127dfb2\r\nWechatpay-Serial: 5F2543BF58239A4EB68FA4433DF1438A88B34B16\r\nWechatpay-Signature: Ci3uWJjea2Hw8xppEyuQyqMGAleliOglETHf8UBR3B2rFhny91OkZEmyCpAh32bLrfRyr7MaNxtsnDF49h0KCxf8u7WCehHJbK9M6c2YmtJZ40QrvnCEpgQdyyOTTeBORPpgszdYizycXKhmhMqO7SLWOzEyI0lvkv11ehCS2dsFfZXH7QNTLdFi4/qA1YV5IWfdEqzpj9ZOhcdE2zwyQM8ChTyPL2qi9hnX6xFsTXhIYDnxlcae8yRarvlzwYXUhRZkf3pBd/kzwdvz7ae+6R0DU1hs5hwwILUnMfF0QK7p+VX9CAYJGAqW0kjWXvsY1O60yel3IY9H/92mBDVU+Q==\r\nWechatpay-Signature-Type: WECHATPAY2-SHA256-RSA2048\r\nWechatpay-Timestamp: 1773983253\r\nX-Content-Type-Options: nosniff\r\n\r\n{\"prepay_id\":\"wx20130733888002cc7db0f278ae9cd60001\"}"}
|
||||
{"level":"debug","timestamp":"2026-03-20T13:11:18+08:00","caller":"kernel/baseClient.go:457","content":"GET https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/MP20260320130728781800?mchid=1318592501 request header: { Accept:*/*Content-Type:application/jsonAuthorization:WECHATPAY2-SHA256-RSA2048 mchid=\"1318592501\",nonce_str=\"gFe13wRPeq6iJSh0I6h2dIJ6QUj7M0Cx\",timestamp=\"1773983478\",serial_no=\"4A1DB62CD5C9BE0B6FC51C30621D6F99686E75C5\",signature=\"TROPreERjOH5arAw0Lr/KGNfNnRE82ti/7wLix63nEdzTuxnfWGVjM/AucI0PZ0tOGoE0KU8UjR5dfx5S4tthnsn8sAO+4PKkxoXwJp4QWrIX7ra6ZigpxAtUjoDQSzCRsGmB7ePvka/MitScIBxsoo2gT4/50D/8MmsmDgp4YBo9not1bdLTCRqTk9zkC8OQfHu+U2d4V4fcdjPgeYVtQV+9QvM988G9TdiqT4WLObVUDodk9P6FgYrgQz84BnpEsrnrTcSCkpErU7DdX/V/Y3NKQjrr8nF2LPvybWxAPp+73xbsB5Ebo0UHdgJ/epYeT8kzPUyZ86y0w+eWmo9Vw==\"} request body:"}
|
||||
{"level":"debug","timestamp":"2026-03-20T13:11:18+08:00","caller":"kernel/baseClient.go:459","content":"------------------response content:HTTP/1.1 200 OK\r\nContent-Length: 250\r\nCache-Control: no-cache, must-revalidate\r\nConnection: keep-alive\r\nContent-Language: zh-CN\r\nContent-Type: application/json; charset=utf-8\r\nDate: Fri, 20 Mar 2026 05:11:18 GMT\r\nKeep-Alive: timeout=8\r\nRequest-Id: 08F5ADF3CD0610CB0718F3B5F8AF0120F8A2082899CE01-0\r\nServer: nginx\r\nWechatpay-Nonce: 1328d38594debffbf2fa32729539ab39\r\nWechatpay-Serial: 5F2543BF58239A4EB68FA4433DF1438A88B34B16\r\nWechatpay-Signature: Nt2U3ril8JbqOc3FSTwCMEWoFOgqHHNExmqxjXyn/lrxCcWlva3nshvPlxtWNWoaDvg6MUY3PnHMvwa5+V7FH5ZwKeommlWTGoi4s/pAKsedczGPOXbdMKIcYeyle61SEg9RQic0i8VHVj6aOx/dRE2qYHebx4VZtsNwlyDFluzAFVvzArKPW5s8Mtqna4HvZgIC5p1zflzXeW717TEM4ALnNI1BYNSkcWZx8XfzMs/ihv8nMDhdKEzizyHV9RojKWueEOkfR9lbRbc08atBQMO2qarSS5QVA/ol3e3YrXqFZFqJWuVGA11a8FgrLP4RjB/CJZJV9kNPK0qdcDsXTA==\r\nWechatpay-Signature-Type: WECHATPAY2-SHA256-RSA2048\r\nWechatpay-Timestamp: 1773983478\r\nX-Content-Type-Options: nosniff\r\n\r\n{\"amount\":{\"payer_currency\":\"CNY\",\"total\":1000},\"appid\":\"wxb8bbb2b10dec74aa\",\"mchid\":\"1318592501\",\"out_trade_no\":\"MP20260320130728781800\",\"promotion_detail\":[],\"scene_info\":{\"device_id\":\"\"},\"trade_state\":\"NOTPAY\",\"trade_state_desc\":\"订单未支付\"}"}
|
||||
{"level":"debug","timestamp":"2026-03-20T13:11:19+08:00","caller":"kernel/baseClient.go:457","content":"GET https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/MP20260320130734906900?mchid=1318592501 request header: { Content-Type:application/jsonAuthorization:WECHATPAY2-SHA256-RSA2048 mchid=\"1318592501\",nonce_str=\"UphkApbjx4HuNQ0hDxrJyfELdcbG19Je\",timestamp=\"1773983478\",serial_no=\"4A1DB62CD5C9BE0B6FC51C30621D6F99686E75C5\",signature=\"0PQ/3CItyMrRFjaU7GR1xEsLsm2k13+eLNuKr1w/GMhBxHGtPgQvrFZ3Axldn3WsvfTRI4uKyw882lnZ2k5Ljiu12JUsmSQJmC+Tl3kN04p1FLUEz0vqXUDRjZ+E4XoTcFpVixcwvEvkZm0M8D3scKj1882OhnnKIv6Y0jHGy3mVGSusW2+DHfJFWsdnbpm2kbEjYHc5YxTLZ1iLii8mX0ZTGWxfczhSue3mFSs79Xg6P3hf4oQVITYdb0esTqdIncLOiSJO23wnKd14FfPrrxyNBj3mW1127FojOLnvvmHcEoUNuJ4wlAkEBHBEvDnDOpcb3bN7MqLinWAPZHuY1A==\"Accept:*/*} request body:"}
|
||||
{"level":"debug","timestamp":"2026-03-20T13:11:19+08:00","caller":"kernel/baseClient.go:459","content":"------------------response content:HTTP/1.1 200 OK\r\nContent-Length: 250\r\nCache-Control: no-cache, must-revalidate\r\nConnection: keep-alive\r\nContent-Language: zh-CN\r\nContent-Type: application/json; charset=utf-8\r\nDate: Fri, 20 Mar 2026 05:11:18 GMT\r\nKeep-Alive: timeout=8\r\nRequest-Id: 08F6ADF3CD0610E50118B39DECF50120CC9B1528E5D601-0\r\nServer: nginx\r\nWechatpay-Nonce: 399e3c310dbc8fa5e47114c95afd4db7\r\nWechatpay-Serial: 5F2543BF58239A4EB68FA4433DF1438A88B34B16\r\nWechatpay-Signature: Vj8HAAxXynUyO00gixkU9feTfOqf7VCRMNZF/vDCkNKUvwtFPu5N0EKKcTzWtRp4frlaa8JprOWvoB5yc3NaDPkwnNTof+QxJ5JR2rrvRIoDWRx9R8ZxIK6p8WKS5lnaZ+xKI87bZFX3tomlx/Cy2ESD4F3OTw7TcTVqh864WRCbMaBPqjYdShYj9xDunqgDIe8iYKmkHeL7q6xmnVSW8qO6yvXH/pLFDkxOuHe8T/ydKmHaKo9fCHxscvd4RRu2LLATMtL0zuodO914IUPQ0th8NSytr3iIKRo+jX3s7gVECEredk0l4ZZieOcmM+n7hiJr1LfkiJ9oZY01QC55gQ==\r\nWechatpay-Signature-Type: WECHATPAY2-SHA256-RSA2048\r\nWechatpay-Timestamp: 1773983478\r\nX-Content-Type-Options: nosniff\r\n\r\n{\"amount\":{\"payer_currency\":\"CNY\",\"total\":1000},\"appid\":\"wxb8bbb2b10dec74aa\",\"mchid\":\"1318592501\",\"out_trade_no\":\"MP20260320130734906900\",\"promotion_detail\":[],\"scene_info\":{\"device_id\":\"\"},\"trade_state\":\"NOTPAY\",\"trade_state_desc\":\"订单未支付\"}"}
|
||||
{"level":"debug","timestamp":"2026-03-20T13:15:25+08:00","caller":"kernel/baseClient.go:457","content":"GET https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/MP20260320130728781800?mchid=1318592501 request header: { Accept:*/*Content-Type:application/jsonAuthorization:WECHATPAY2-SHA256-RSA2048 mchid=\"1318592501\",nonce_str=\"xhkeCroakqZFpKfFZCGpTENfZit5qX4v\",timestamp=\"1773983725\",serial_no=\"4A1DB62CD5C9BE0B6FC51C30621D6F99686E75C5\",signature=\"iEBERNeyYSBpiqJwroP1JHVLq99utY/rbi6ZAX3INvPhYrkYwvvIOXn+PCbIOMO8/qrDdhZLPJi3HD8g2cfZX28BVN9vejNShwcUzSt/YO62FWNE5SYk1bKEd7xKHL2YHkaCyE0GnMDT+DRcirCUpPAlQGs8aT7GZ8FCpiw1yNAQWMvNcqVSaTfuUTIF0FZfwp0hG5C0sZfdfQBxax8FNwW22T350k+1/KcUZx+0dj80LyzDZJVjWsxWqd9mhs+U0sxqDaWKbd3tvS/QFtp+mUGNLR83DOCN4V9/yREPOp9U9nf4EZFNWksehjAwPUW8zuf/HmNbVc3MtSdJu4tiqQ==\"} request body:"}
|
||||
{"level":"debug","timestamp":"2026-03-20T13:15:25+08:00","caller":"kernel/baseClient.go:459","content":"------------------response content:HTTP/1.1 200 OK\r\nContent-Length: 250\r\nCache-Control: no-cache, must-revalidate\r\nConnection: keep-alive\r\nContent-Language: zh-CN\r\nContent-Type: application/json; charset=utf-8\r\nDate: Fri, 20 Mar 2026 05:15:25 GMT\r\nKeep-Alive: timeout=8\r\nRequest-Id: 08ECAFF3CD0610EB0618B5A7F8AF012080EE2A28EDEC02-0\r\nServer: nginx\r\nWechatpay-Nonce: 9172717ca8b2e47c8b797a5e212e1ab4\r\nWechatpay-Serial: 5F2543BF58239A4EB68FA4433DF1438A88B34B16\r\nWechatpay-Signature: IPq7BUIpOWUF6KlQhVJ/esAzaqupDq3bpoCnVrPfPQNyum4B9PD4WFf223IbZ+54UGO3mqyDJf6V9bo7YYnE3UbM49RRxLO5zPRCgUARwvuZzkNJu/q7NZsAfsnaaWloCmS5N+QR34ePnzBFe5yPpRNvcAlsoRt14JNrOumNl7Ib8YHjArPWT2SkpbN2x6aturEZ0rgMB4zdnv5z2yjjdsUBUerg9nHNUz+Qhxlye+mJk10RWYjOFXP1rWvAURTf4N1a3vmQBvqF8zMoxl9ad9EVEKZi9yLezqN1DVVkrR2SUuu/LMIpJXnPtdS83doYC/rA98f4N/BwZfLRFUgGEw==\r\nWechatpay-Signature-Type: WECHATPAY2-SHA256-RSA2048\r\nWechatpay-Timestamp: 1773983724\r\nX-Content-Type-Options: nosniff\r\n\r\n{\"amount\":{\"payer_currency\":\"CNY\",\"total\":1000},\"appid\":\"wxb8bbb2b10dec74aa\",\"mchid\":\"1318592501\",\"out_trade_no\":\"MP20260320130728781800\",\"promotion_detail\":[],\"scene_info\":{\"device_id\":\"\"},\"trade_state\":\"NOTPAY\",\"trade_state_desc\":\"订单未支付\"}"}
|
||||
{"level":"debug","timestamp":"2026-03-20T13:15:26+08:00","caller":"kernel/baseClient.go:457","content":"GET https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/MP20260320130734906900?mchid=1318592501 request header: { Authorization:WECHATPAY2-SHA256-RSA2048 mchid=\"1318592501\",nonce_str=\"owbeqXG9qWFk8i8Oc4HYCjr0e8xHH0ve\",timestamp=\"1773983725\",serial_no=\"4A1DB62CD5C9BE0B6FC51C30621D6F99686E75C5\",signature=\"up/07Vq5dlG3HogzK8hbEeapdSC6mJnkBarr5b+C6XPDmPGGMif5BPJS7MHgUFB4CdlwgCfYZ392z0A9SL97ebAKFCQ2qRj27YcjSue2Eo/m5DpzRyzOlrJDszzvgNvP0du0no9GD3AfYtnaCe5i3r//qZV7nRWui2HBPa3kBZB0fKLJPeHEKc3ueoLdkwVsfI38miKE05zOSJeBmAQZIOLPFdD5CboAQsDF06lgxLVcR7YnLz+4/Hw1Jbs+HImSewPQt058Xtn3TMF9h/gW4gwSYw1Tzc3wDowxpEcZ7XqqRT1dKJlOiXP4DcmsQTFtlgF7707DAf1HtCGHMW5R/g==\"Accept:*/*Content-Type:application/json} request body:"}
|
||||
{"level":"debug","timestamp":"2026-03-20T13:15:26+08:00","caller":"kernel/baseClient.go:459","content":"------------------response content:HTTP/1.1 200 OK\r\nContent-Length: 250\r\nCache-Control: no-cache, must-revalidate\r\nConnection: keep-alive\r\nContent-Language: zh-CN\r\nContent-Type: application/json; charset=utf-8\r\nDate: Fri, 20 Mar 2026 05:15:25 GMT\r\nKeep-Alive: timeout=8\r\nRequest-Id: 08EDAFF3CD06105B18B5A7F8AF0120D2800B28B8F405-0\r\nServer: nginx\r\nWechatpay-Nonce: 0ef81b6e156dd0e8e601b777f8df1bc8\r\nWechatpay-Serial: 5F2543BF58239A4EB68FA4433DF1438A88B34B16\r\nWechatpay-Signature: iNwW2dKzUJm4gpn1Z3G0NJhdanyyBJZjj/KREF6EKw18hRdY0WUQlwYEvEPmWM3y3tMnm5OCeWtDIyTn2mDgpW+SKkVuXWelDFwW703CgBShfqwwRBFCoAgr9X4N+s/cby6ALhy7Iw6cfYDFLzRK/yLGRbfo+ZNqem8PYWM/sZ6SoHcMJJrHXBakCFtwf2+fObK30CgE65xKnMVZNU5Pvs606nXuae4Ws/zAaUBtcMWiGyiqgGg3eicGySR10na1BXO+/1SJUeSpeYYYc3Ji5FWiVSqOgpXooloxRsJ6LUheDK2pgSL4fVn3efWxcUYm2jEp3hvde01BSC2DwiJczA==\r\nWechatpay-Signature-Type: WECHATPAY2-SHA256-RSA2048\r\nWechatpay-Timestamp: 1773983725\r\nX-Content-Type-Options: nosniff\r\n\r\n{\"amount\":{\"payer_currency\":\"CNY\",\"total\":1000},\"appid\":\"wxb8bbb2b10dec74aa\",\"mchid\":\"1318592501\",\"out_trade_no\":\"MP20260320130734906900\",\"promotion_detail\":[],\"scene_info\":{\"device_id\":\"\"},\"trade_state\":\"NOTPAY\",\"trade_state_desc\":\"订单未支付\"}"}
|
||||
{"level":"debug","timestamp":"2026-03-20T13:20:25+08:00","caller":"kernel/baseClient.go:457","content":"GET https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/MP20260320130728781800?mchid=1318592501 request header: { Content-Type:application/jsonAuthorization:WECHATPAY2-SHA256-RSA2048 mchid=\"1318592501\",nonce_str=\"EsubDL8LOn17psdMACQIuAvy0u0ZOpFe\",timestamp=\"1773984025\",serial_no=\"4A1DB62CD5C9BE0B6FC51C30621D6F99686E75C5\",signature=\"eQcyMJ/SmzoxaYBeSvaq3vCUwM58zwLqR+V1Nr2JWReAIU5qt2qd2qXBrvy57vMv/jaOxNsOA4do6eKpQ88ubUQMcWpiJXxjtsi1pqfzAiQdvEKScFBCNyK53gtVUDXz7rNHbQJZbSPJh8Puxg0PcTk5f3U0FdmHd8MF9yinZnyhgtCEDWT/sGQ9bkfbvFmiXfOMnLOJtX1vAQIUVkhLHqrCkFzsUcbs7OVqBBnUjnCql6sCRmnWMIyKtP/bOjsvb0Tk7XaDqObzXQkE/RFmNRhJajutdd7xkbZ65/WDSRkO3AcoagpTjpqs3GG5BsD7ODhaBCLsoy/pI7ZtUGTfUw==\"Accept:*/*} request body:"}
|
||||
{"level":"debug","timestamp":"2026-03-20T13:20:25+08:00","caller":"kernel/baseClient.go:459","content":"------------------response content:HTTP/1.1 200 OK\r\nContent-Length: 250\r\nCache-Control: no-cache, must-revalidate\r\nConnection: keep-alive\r\nContent-Language: zh-CN\r\nContent-Type: application/json; charset=utf-8\r\nDate: Fri, 20 Mar 2026 05:20:24 GMT\r\nKeep-Alive: timeout=8\r\nRequest-Id: 0898B2F3CD0610AE0618BCB2F8AF0120EEFF3028C1F703-0\r\nServer: nginx\r\nWechatpay-Nonce: 354c2c44a5980845a63d52ee874905b7\r\nWechatpay-Serial: 5F2543BF58239A4EB68FA4433DF1438A88B34B16\r\nWechatpay-Signature: DnjICyEVs9ZaKSjX68EutIMK1XRSXRk8rqpXYazRqgzABIyiAbZeRQ6eB9srWmBCgkpThBnp/iN6fXMsQme/91inoukFp2CLVBBWZIkyoEEeUNHMrFaWBn64rlUdOBUz8dtmYC0gGscqlZv3zzducXNvbJx1REqaIHamrUzNq4iv3cC7kRkWBzwM721REOTVoA8piXdogyn6OtIvpMMLEZWr13RTH2wKpAbkgK+NJonFzT6jDzPYH6JFDdC0VPUjkxge8y9k1JeajhiFEby5lSBkk/ZeGA3Kie+oiLNoLy2L7uwc7HEbz2HB1HEv7ERwhFNLJf1fAu6GBlw/S9k1bA==\r\nWechatpay-Signature-Type: WECHATPAY2-SHA256-RSA2048\r\nWechatpay-Timestamp: 1773984024\r\nX-Content-Type-Options: nosniff\r\n\r\n{\"amount\":{\"payer_currency\":\"CNY\",\"total\":1000},\"appid\":\"wxb8bbb2b10dec74aa\",\"mchid\":\"1318592501\",\"out_trade_no\":\"MP20260320130728781800\",\"promotion_detail\":[],\"scene_info\":{\"device_id\":\"\"},\"trade_state\":\"NOTPAY\",\"trade_state_desc\":\"订单未支付\"}"}
|
||||
{"level":"debug","timestamp":"2026-03-20T13:20:26+08:00","caller":"kernel/baseClient.go:457","content":"GET https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/MP20260320130734906900?mchid=1318592501 request header: { Content-Type:application/jsonAuthorization:WECHATPAY2-SHA256-RSA2048 mchid=\"1318592501\",nonce_str=\"KgnH4npnqT8HCnFNxEfFmTmLACu39eeH\",timestamp=\"1773984025\",serial_no=\"4A1DB62CD5C9BE0B6FC51C30621D6F99686E75C5\",signature=\"TCT35xm0PGkEltYMfaMaYE90N6jVkogNdtevJI3ofePvMVKKcCqobyUgaMW2HnXWprlc0IsikIYsP7Tuz3VCFKiLFWqfe9s/TZOmxpeQqRpaEK/E+31WfnIOZWJg8WuYD1iJlb6/NwV80J6pfJDIZQnrBirFU3yX2gsBm0hmi9cI+0mmJntPUMD1/harMgEWrWP1glMHyU4sI90ppVC/4W89bUUQR9PDJLrl1cV17iaB8GBQ8WEfszVLfe/Y8gmAWQ789KWTbHrbTJUXyn3BCKprGFyne3lRWvH/zxizFmxvbENvLDR7HzfPlxMFN2lKkoQgphY/BaVmawzufCoXoQ==\"Accept:*/*} request body:"}
|
||||
{"level":"debug","timestamp":"2026-03-20T13:20:26+08:00","caller":"kernel/baseClient.go:459","content":"------------------response content:HTTP/1.1 200 OK\r\nContent-Length: 250\r\nCache-Control: no-cache, must-revalidate\r\nConnection: keep-alive\r\nContent-Language: zh-CN\r\nContent-Type: application/json; charset=utf-8\r\nDate: Fri, 20 Mar 2026 05:20:25 GMT\r\nKeep-Alive: timeout=8\r\nRequest-Id: 0899B2F3CD06103618A294F5AF012086EB3628A559-0\r\nServer: nginx\r\nWechatpay-Nonce: 966e33621df834c72f86754e5d983adb\r\nWechatpay-Serial: 5F2543BF58239A4EB68FA4433DF1438A88B34B16\r\nWechatpay-Signature: WG+7KqWCytTIe8ysCo+rc0DDIjBHgQ35GNLUW88DZ38EU1jr460wg2GGX0QaAet+nat7qtstau3wlVhWaVNTmk1Ij7zwIawmUah/AN4UWHoIHoQlmo8W/Ymht82eulCgL1EPKbwsY5bHdYjPN2siKg3/5z5pqBzldMHciaEp7HTDpshsEcAlnsjhQZkZDDFDDaKBcRte7+pHUdA9K49raF4BEaIta+SNwuJ8VpYVheWxuhwXDv1s2qw8Um0cyqLEJ/UcRFk6kouzZbZoNYX4GmiddWdNiDjA0DlQN+x2XCQoK888cSPqivEC1vn7VbmJyfTqLXgAIifAe6V4i2AdqA==\r\nWechatpay-Signature-Type: WECHATPAY2-SHA256-RSA2048\r\nWechatpay-Timestamp: 1773984025\r\nX-Content-Type-Options: nosniff\r\n\r\n{\"amount\":{\"payer_currency\":\"CNY\",\"total\":1000},\"appid\":\"wxb8bbb2b10dec74aa\",\"mchid\":\"1318592501\",\"out_trade_no\":\"MP20260320130734906900\",\"promotion_detail\":[],\"scene_info\":{\"device_id\":\"\"},\"trade_state\":\"NOTPAY\",\"trade_state_desc\":\"订单未支付\"}"}
|
||||
{"level":"debug","timestamp":"2026-03-20T13:22:29+08:00","caller":"kernel/accessToken.go:381","content":"GET https://api.weixin.qq.com/cgi-bin/token?appid=wxb8bbb2b10dec74aa&grant_type=client_credential&neededText=&secret=3c1fb1f63e6e052222bbcead9d07fe0c request header: { Accept:*/*} "}
|
||||
{"level":"debug","timestamp":"2026-03-20T13:22:29+08:00","caller":"kernel/accessToken.go:383","content":"------------------response content:HTTP/1.1 200 OK\r\nContent-Length: 174\r\nConnection: keep-alive\r\nContent-Type: application/json; encoding=utf-8\r\nDate: Fri, 20 Mar 2026 05:22:28 GMT\r\n\r\n{\"access_token\":\"102_LYzQoAxwGVSLcEAbTpcQbFxakY42Hg9jT-2zNq3-D5xTP_e-JyXHpNI2K3vJZOI08NpfU8vYdmihs1HF8wmEaladetKA5XrYl66-2CwwrCreSCE7WiVehaR26hcWODiACASKZ\",\"expires_in\":7200}"}
|
||||
{"level":"debug","timestamp":"2026-03-20T13:22:29+08:00","caller":"kernel/baseClient.go:457","content":"GET https://api.weixin.qq.com/sns/jscode2session?access_token=102_LYzQoAxwGVSLcEAbTpcQbFxakY42Hg9jT-2zNq3-D5xTP_e-JyXHpNI2K3vJZOI08NpfU8vYdmihs1HF8wmEaladetKA5XrYl66-2CwwrCreSCE7WiVehaR26hcWODiACASKZ&appid=wxb8bbb2b10dec74aa&grant_type=authorization_code&js_code=0d3iFDFa1L2roL0AXTIa166MGF4iFDFH&secret=3c1fb1f63e6e052222bbcead9d07fe0c request header: { Accept:*/*} "}
|
||||
{"level":"debug","timestamp":"2026-03-20T13:22:29+08:00","caller":"kernel/baseClient.go:459","content":"------------------response content:HTTP/1.1 200 OK\r\nContent-Length: 82\r\nConnection: keep-alive\r\nContent-Type: text/plain\r\nDate: Fri, 20 Mar 2026 05:22:28 GMT\r\n\r\n{\"session_key\":\"+t0l1lw5IRJ3snNlVhHVjA==\",\"openid\":\"ogpTW5fmXRGNpoUbXB3UEqnVe5Tg\"}"}
|
||||
{"level":"debug","timestamp":"2026-03-20T13:23:00+08:00","caller":"kernel/baseClient.go:457","content":"GET https://api.weixin.qq.com/sns/jscode2session?access_token=102_LYzQoAxwGVSLcEAbTpcQbFxakY42Hg9jT-2zNq3-D5xTP_e-JyXHpNI2K3vJZOI08NpfU8vYdmihs1HF8wmEaladetKA5XrYl66-2CwwrCreSCE7WiVehaR26hcWODiACASKZ&appid=wxb8bbb2b10dec74aa&grant_type=authorization_code&js_code=0e3S5vll2hhVoh45Xunl2aYh7i2S5vlU&secret=3c1fb1f63e6e052222bbcead9d07fe0c request header: { Accept:*/*} "}
|
||||
{"level":"debug","timestamp":"2026-03-20T13:23:00+08:00","caller":"kernel/baseClient.go:459","content":"------------------response content:HTTP/1.1 200 OK\r\nContent-Length: 82\r\nConnection: keep-alive\r\nContent-Type: text/plain\r\nDate: Fri, 20 Mar 2026 05:22:59 GMT\r\n\r\n{\"session_key\":\"DgQe8zI8iPAd7BHkKF7NLw==\",\"openid\":\"ogpTW5fmXRGNpoUbXB3UEqnVe5Tg\"}"}
|
||||
{"level":"debug","timestamp":"2026-03-20T13:25:25+08:00","caller":"kernel/baseClient.go:457","content":"GET https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/MP20260320130728781800?mchid=1318592501 request header: { Authorization:WECHATPAY2-SHA256-RSA2048 mchid=\"1318592501\",nonce_str=\"khcmZC8SnR9F97GNc62MHsh6rg9lQ4m4\",timestamp=\"1773984325\",serial_no=\"4A1DB62CD5C9BE0B6FC51C30621D6F99686E75C5\",signature=\"raVbKp7FQWDlwpdfPhFJP3Aocw1ZQ2nJBI+oYNheqvQqyOb0HyTUAX24ICEaGa8wXk55REV+xg2h558PVDdiH1BI/buBPRCAy+NCrpDdYAJe8xVA+kikyAaXn0Wa91YTuivpeJn+wc95ucdqpPeJMOU2p2jGfPBdaieXhWkRHmoXdMJc3jZCInHjpGuXPvMPfznIhvPYc1IXh0wwkL/j5WzXek8VuQAJnVHa0Bnx/kcYRwiNuOnTt8GslzSia0mqoBFaLmSotV7d4oV95/B7tlsKkmDtDFoysxBkv66pfZkh7DAFT/xySs3c4ZhzBWOCDCj46DgPkR4pLI2hnNePnA==\"Accept:*/*Content-Type:application/json} request body:"}
|
||||
{"level":"debug","timestamp":"2026-03-20T13:25:25+08:00","caller":"kernel/baseClient.go:459","content":"------------------response content:HTTP/1.1 200 OK\r\nContent-Length: 250\r\nCache-Control: no-cache, must-revalidate\r\nConnection: keep-alive\r\nContent-Language: zh-CN\r\nContent-Type: application/json; charset=utf-8\r\nDate: Fri, 20 Mar 2026 05:25:24 GMT\r\nKeep-Alive: timeout=8\r\nRequest-Id: 08C4B4F3CD0610A20618A6B1F8AF0120C4D31228D89405-0\r\nServer: nginx\r\nWechatpay-Nonce: e0a27ed24ef90e75d4f3299b7d6af71e\r\nWechatpay-Serial: 5F2543BF58239A4EB68FA4433DF1438A88B34B16\r\nWechatpay-Signature: ibvxwB7gmzZg8iAOEMiSi+8muG9DjgW4gEuCd4FrAajTQohSpFde4O0DNUZExgRL6/XWucs29xhnqMCOyqJN1EW8fIUcm0DLryvLcGKvVLFbimHXNK/3OGjAOE1m9Fx+51ZYXer1dgfQD/vKLPwnIudVkk6us7Ip7cjl78dwDHN2i3lEaNqlBEH+2qME+OltwkNvH+nLS2fDm7J0I78gl5bKUzBEhK/9s230jXs8vO8u1txnrto8HmsV5I1UQWoalAQITPt8Dm8l3ogdDi4QFTA9zmVlPvVSSmrxYVB6M1D5GdnTPJL6NY9SGNt3jIP2w3X944U9zN372uOrnt5u6g==\r\nWechatpay-Signature-Type: WECHATPAY2-SHA256-RSA2048\r\nWechatpay-Timestamp: 1773984324\r\nX-Content-Type-Options: nosniff\r\n\r\n{\"amount\":{\"payer_currency\":\"CNY\",\"total\":1000},\"appid\":\"wxb8bbb2b10dec74aa\",\"mchid\":\"1318592501\",\"out_trade_no\":\"MP20260320130728781800\",\"promotion_detail\":[],\"scene_info\":{\"device_id\":\"\"},\"trade_state\":\"NOTPAY\",\"trade_state_desc\":\"订单未支付\"}"}
|
||||
{"level":"debug","timestamp":"2026-03-20T13:25:26+08:00","caller":"kernel/baseClient.go:457","content":"GET https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/MP20260320130734906900?mchid=1318592501 request header: { Content-Type:application/jsonAuthorization:WECHATPAY2-SHA256-RSA2048 mchid=\"1318592501\",nonce_str=\"v9QvVQXMAA5bC3WIevcYEq5XqTLoAH0M\",timestamp=\"1773984325\",serial_no=\"4A1DB62CD5C9BE0B6FC51C30621D6F99686E75C5\",signature=\"KE/+IRw58u0kxhZy/8xMleaEF97dqfLDAp3RJZib5R1cglNahJp/CK8v8uEDqp8C/S9SOojOEeRwdFhcJRGtCSi3ck9CefGxLTPJQdLk5Y+n9YlaYXa1ksJvs7O/HdVNZTMZSLDHOoJ1shQCEuCgP1i+Vj8lfbFecsqrefnla67YneBYVl88rMRM7XUF0xKkBsonxYBcY9tN8tCCcxCDcpBROyOQLxFHOomglErAlx9eWBNTpd1NDZopFR9uVpS9nWFcI0ISj0JO0yS5uhGxcrb9PveRzNZ+YBLzlYIpUF3KurzDlexnW6DXK8u+jGfNNV6kJwLTdJhEO/EXHdyFvA==\"Accept:*/*} request body:"}
|
||||
{"level":"debug","timestamp":"2026-03-20T13:25:26+08:00","caller":"kernel/baseClient.go:459","content":"------------------response content:HTTP/1.1 200 OK\r\nContent-Length: 250\r\nCache-Control: no-cache, must-revalidate\r\nConnection: keep-alive\r\nContent-Language: zh-CN\r\nContent-Type: application/json; charset=utf-8\r\nDate: Fri, 20 Mar 2026 05:25:25 GMT\r\nKeep-Alive: timeout=8\r\nRequest-Id: 08C5B4F3CD0610191890C5C05520E2FC2B28E89E02-0\r\nServer: nginx\r\nWechatpay-Nonce: 594dbae1c45eca2b8573de992640d642\r\nWechatpay-Serial: 5F2543BF58239A4EB68FA4433DF1438A88B34B16\r\nWechatpay-Signature: L8uag53oL6xKCKgPnauht0RuVdlnHcrwdiz3pRdtko60xCUEyyqFwLimoCmezjcIgmDz+U8WXgaWsU0Ab4uoVI/QvsyPhx2ekCRiX8y5ZHl6Tg4r1D0s0zRyhG5/E5h6re+HWNg/p79j4izwt7E+t/nQouVLP0S/pYX/M3EDu5nmt7usFk4blsQ/qGi1yygF1CQxzzBiIPRuTBrv6cOIhctOXciUX4GIeOEwHZyj+3f8H0D0qp39CJMF1J63PmVjLFOWuF4qpHRKsh5smiae2Wa1N+6NYfA+dSKRaoe7prCy5Su1JwJ4btjlVSmxx9P4Jzwtf0HUm7alE6I/p8w8cA==\r\nWechatpay-Signature-Type: WECHATPAY2-SHA256-RSA2048\r\nWechatpay-Timestamp: 1773984325\r\nX-Content-Type-Options: nosniff\r\n\r\n{\"amount\":{\"payer_currency\":\"CNY\",\"total\":1000},\"appid\":\"wxb8bbb2b10dec74aa\",\"mchid\":\"1318592501\",\"out_trade_no\":\"MP20260320130734906900\",\"promotion_detail\":[],\"scene_info\":{\"device_id\":\"\"},\"trade_state\":\"NOTPAY\",\"trade_state_desc\":\"订单未支付\"}"}
|
||||
{"level":"debug","timestamp":"2026-03-20T13:25:42+08:00","caller":"kernel/baseClient.go:457","content":"GET https://api.weixin.qq.com/sns/jscode2session?access_token=102_LYzQoAxwGVSLcEAbTpcQbFxakY42Hg9jT-2zNq3-D5xTP_e-JyXHpNI2K3vJZOI08NpfU8vYdmihs1HF8wmEaladetKA5XrYl66-2CwwrCreSCE7WiVehaR26hcWODiACASKZ&appid=wxb8bbb2b10dec74aa&grant_type=authorization_code&js_code=0b3rOmFa1JpaoL0tUhJa1vNtd22rOmFU&secret=3c1fb1f63e6e052222bbcead9d07fe0c request header: { Accept:*/*} "}
|
||||
{"level":"debug","timestamp":"2026-03-20T13:25:42+08:00","caller":"kernel/baseClient.go:459","content":"------------------response content:HTTP/1.1 200 OK\r\nContent-Length: 82\r\nConnection: keep-alive\r\nContent-Type: text/plain\r\nDate: Fri, 20 Mar 2026 05:25:41 GMT\r\n\r\n{\"session_key\":\"S/U/KzIRlBE9biInVk8t5Q==\",\"openid\":\"ogpTW5fmXRGNpoUbXB3UEqnVe5Tg\"}"}
|
||||
{"level":"debug","timestamp":"2026-03-20T13:30:25+08:00","caller":"kernel/baseClient.go:457","content":"GET https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/MP20260320130728781800?mchid=1318592501 request header: { Content-Type:application/jsonAuthorization:WECHATPAY2-SHA256-RSA2048 mchid=\"1318592501\",nonce_str=\"9YlIEXeLbdbxUx7CziwTkTrjDgMtXxcx\",timestamp=\"1773984625\",serial_no=\"4A1DB62CD5C9BE0B6FC51C30621D6F99686E75C5\",signature=\"YZ6fkW6Czn6Jv/VYpi0F1/GIVfG7FsvtNRclkgpQT6KCWhdMMLYNG8Wnx6WikcApvh5flgqObBfJ7nI6fj3bsVo1tlGI3uBvl9ppW1jNlKQNmUbxd/AI01/XLZvGrhE9JpCNxEoIxZzrtLql1niqlLTxwuIG0useg8fp98aiDzYNUu8WpWOaNZGADA7sD5Ro/4WJVFT7QLKkaNk0HGflDOVZZ1EsiPPUq2OfupDEpx4B+tzLge7yuhVqAVvX3O6StSCX1u2clr6K/0qIwqEfL5MaPZdLvqP/r1dhYpAFG7pm7Lf7g2S0gbiBDN3Z62VLYbf9jyyAPalJy5x9oFCLMQ==\"Accept:*/*} request body:"}
|
||||
{"level":"debug","timestamp":"2026-03-20T13:30:25+08:00","caller":"kernel/baseClient.go:459","content":"------------------response content:HTTP/1.1 200 OK\r\nContent-Length: 250\r\nCache-Control: no-cache, must-revalidate\r\nConnection: keep-alive\r\nContent-Language: zh-CN\r\nContent-Type: application/json; charset=utf-8\r\nDate: Fri, 20 Mar 2026 05:30:24 GMT\r\nKeep-Alive: timeout=8\r\nRequest-Id: 08F0B6F3CD0610B00618C38CA85C2082F11D28AA9303-0\r\nServer: nginx\r\nWechatpay-Nonce: 4bc7e1f9444cdf9ef9ef7c8934a726ee\r\nWechatpay-Serial: 5F2543BF58239A4EB68FA4433DF1438A88B34B16\r\nWechatpay-Signature: UnjMAU3z2e6s+t9Zx2LCiiL/0QMsB5CdMMphgyCIm9ONWhr48gZ5i9kDLNDAwNj/z8fZWbIMIzQ/UEGUdmKKL6a08NnRoq5ZqqWOUrgwsQi59RwL/KthYeXB/nzTwoldo7ALqgLvk93fJpxMZrdb8BdewVzHr1+eoK2Kj+U11HaAlma1Bknc4y3XC0per7xt6ygSdPaxQqNNhvWa+gOxDD9k56jOYS43ewh94n67vjW4w75LUBQwqxRugpmCMNvH6AQyF0uj8ffjljb1/VllYynpCUWHLXuXYt+A0KUWlCuKqIk0jsoiGJ8M0Mg0SkkfixHQWrO6EFl4zo61WuSCIg==\r\nWechatpay-Signature-Type: WECHATPAY2-SHA256-RSA2048\r\nWechatpay-Timestamp: 1773984624\r\nX-Content-Type-Options: nosniff\r\n\r\n{\"amount\":{\"payer_currency\":\"CNY\",\"total\":1000},\"appid\":\"wxb8bbb2b10dec74aa\",\"mchid\":\"1318592501\",\"out_trade_no\":\"MP20260320130728781800\",\"promotion_detail\":[],\"scene_info\":{\"device_id\":\"\"},\"trade_state\":\"NOTPAY\",\"trade_state_desc\":\"订单未支付\"}"}
|
||||
{"level":"debug","timestamp":"2026-03-20T13:30:26+08:00","caller":"kernel/baseClient.go:457","content":"GET https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/MP20260320130734906900?mchid=1318592501 request header: { Content-Type:application/jsonAuthorization:WECHATPAY2-SHA256-RSA2048 mchid=\"1318592501\",nonce_str=\"P0B7crMg1vkPlxmoxU9qB6P0JwIIUVzC\",timestamp=\"1773984625\",serial_no=\"4A1DB62CD5C9BE0B6FC51C30621D6F99686E75C5\",signature=\"N32Mlc3Xlr1ok3Dt04wLOGZyrTr2yQb74EPuJKGbbx2urCjVY0ThjLi+/qKV89rhTCnhmS4TJnDa/dgshi2fRMO5leLhgx/mYCeOUM0cQinFDEVwE9ynFLpLDrw3i4DN25tXD5Ume3ohxre55IzI5s687hgmaigYPdU4FMqk8/6QtQrTIxg3/7pHD8kyZbCP4aBzwDq8Vej+lMGenkTRcdCKmCCmqDCI5w7sfQfz1YWT+XbpEmkP8iQDcqZb6iyr5QyqIBl6pcO17KizIzpmtihrDUKqg8D7zcRfyyFX7Dxb0AvNQRPnLZEDOYljFFOl3I2VSNaHb22zdeLrdM7V0Q==\"Accept:*/*} request body:"}
|
||||
{"level":"debug","timestamp":"2026-03-20T13:30:26+08:00","caller":"kernel/baseClient.go:459","content":"------------------response content:HTTP/1.1 200 OK\r\nContent-Length: 250\r\nCache-Control: no-cache, must-revalidate\r\nConnection: keep-alive\r\nContent-Language: zh-CN\r\nContent-Type: application/json; charset=utf-8\r\nDate: Fri, 20 Mar 2026 05:30:25 GMT\r\nKeep-Alive: timeout=8\r\nRequest-Id: 08F1B6F3CD06101D18C2E7F8AF012096B21428F5DB02-0\r\nServer: nginx\r\nWechatpay-Nonce: 420a870ca7ce6a7c37f97d0cb912800c\r\nWechatpay-Serial: 5F2543BF58239A4EB68FA4433DF1438A88B34B16\r\nWechatpay-Signature: GD2GUJ1wGhAzIA94eS2CURqZ0HM3zVM35G8KXQT8aiGp3sNouQoxOo/+Z1p/u/AkUJHmEAbRqeVlW7+4bD7+y1ZxehL0bEfBhDdHibRvGHe963ueJXJVVq5QblIsEG0W8TKJTsHvStrN6Avg0ZkVYocDFfzAqNix9f7YaFITW0s9TVX8xWC5K8LdV5RO+N2IsbLNzZ40FwuHxTZVqCxgupNcTQ3EPXIu7QqqegxgH/I7iYV3bhnDi7k4dTQBkOUXur7ETvDXLq8Uo9Dj/dax05ccCWIM1kPam8LsNSkugDYLAnfls01zm3pIiLM2zU4yS6YerALTuzRA4qOdxt95VA==\r\nWechatpay-Signature-Type: WECHATPAY2-SHA256-RSA2048\r\nWechatpay-Timestamp: 1773984625\r\nX-Content-Type-Options: nosniff\r\n\r\n{\"amount\":{\"payer_currency\":\"CNY\",\"total\":1000},\"appid\":\"wxb8bbb2b10dec74aa\",\"mchid\":\"1318592501\",\"out_trade_no\":\"MP20260320130734906900\",\"promotion_detail\":[],\"scene_info\":{\"device_id\":\"\"},\"trade_state\":\"NOTPAY\",\"trade_state_desc\":\"订单未支付\"}"}
|
||||
{"level":"debug","timestamp":"2026-03-20T13:35:34+08:00","caller":"kernel/baseClient.go:457","content":"GET https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/MP20260320130728781800?mchid=1318592501 request header: { Accept:*/*Content-Type:application/jsonAuthorization:WECHATPAY2-SHA256-RSA2048 mchid=\"1318592501\",nonce_str=\"mxtOkbMKqdPqcP0dZWKieInMIZudN8Se\",timestamp=\"1773984933\",serial_no=\"4A1DB62CD5C9BE0B6FC51C30621D6F99686E75C5\",signature=\"aCb8nEK4nqAXARLScUWoGyX0ye38DgFYMmz/jXbA9iFqo7gwS3SA4RkWyrpJtqlLCmLWgjkTenUk1IzXddm9zVFZiid7PjGsZ/aXKnjtYiYVnQgyymxK3qqrDAy+Hsme1DPQ/ghp4pC4zfHIUBk2JnXWwplf4zVyOtjTWNlQV9ZyOHU1ndbGKiJsLO28E7ynTuFg7EFCe/uudXWhHL/nUamV2v7Dd9zyOVgUJKHO0Nqgw7hxQ9Y89DBiB6y7IV36YkofQUTz+wSr0LycwSsEe94G7N1pKE+sJsNODme6eeaS/jEHsu58A2GXikZgmf4IQlCtWX/DQdHXen+9+45o2Q==\"} request body:"}
|
||||
{"level":"debug","timestamp":"2026-03-20T13:35:34+08:00","caller":"kernel/baseClient.go:459","content":"------------------response content:HTTP/1.1 200 OK\r\nContent-Length: 250\r\nCache-Control: no-cache, must-revalidate\r\nConnection: keep-alive\r\nContent-Language: zh-CN\r\nContent-Type: application/json; charset=utf-8\r\nDate: Fri, 20 Mar 2026 05:35:33 GMT\r\nKeep-Alive: timeout=8\r\nRequest-Id: 08A5B9F3CD0610D00118C3B9F8AF012098C81228AAF801-0\r\nServer: nginx\r\nWechatpay-Nonce: 3ac56a1aba0043f64f699ffe371ebabe\r\nWechatpay-Serial: 5F2543BF58239A4EB68FA4433DF1438A88B34B16\r\nWechatpay-Signature: tJsbXVjsXPLgAzH1ETyBrZ+xsrwmgOnT+sH8JmcqlyxUkFGCFkiqm+5DiKhE07f60dHXGeOSS4qrBzR37HzZitTeqodR7E2JB0Ut5xFbBKQL39nMDvB314u3CmklcjRrrUuSayXNUHrINYYwizfaj3GQMWlytWiS/IuC8rKhOSD1LxKQRUZvs9YD/v2EtjJrU8rXqIJdtdpAggMdIxl88oGR9/cPqBE+fSrngtqJtS+8HXfClPhrDOka1ah/faeHsQxYnb9Izglix057mv7W6vUBIjlbPsTxL1dREPQyyZ9UumcbPgnXXHkJ4iufXD61KPDPHtJjMQRaoLyZMCKR7A==\r\nWechatpay-Signature-Type: WECHATPAY2-SHA256-RSA2048\r\nWechatpay-Timestamp: 1773984933\r\nX-Content-Type-Options: nosniff\r\n\r\n{\"amount\":{\"payer_currency\":\"CNY\",\"total\":1000},\"appid\":\"wxb8bbb2b10dec74aa\",\"mchid\":\"1318592501\",\"out_trade_no\":\"MP20260320130728781800\",\"promotion_detail\":[],\"scene_info\":{\"device_id\":\"\"},\"trade_state\":\"NOTPAY\",\"trade_state_desc\":\"订单未支付\"}"}
|
||||
{"level":"debug","timestamp":"2026-03-20T13:35:34+08:00","caller":"kernel/baseClient.go:457","content":"GET https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/MP20260320130734906900?mchid=1318592501 request header: { Authorization:WECHATPAY2-SHA256-RSA2048 mchid=\"1318592501\",nonce_str=\"F4jAe1o38Lshn82q3fXtEKVPM1DONLof\",timestamp=\"1773984934\",serial_no=\"4A1DB62CD5C9BE0B6FC51C30621D6F99686E75C5\",signature=\"TIlP8QQWNG3f+RHfZefrC7CIaovaFw6JW0V8O5r6bbPu37/aKwItYZcWKlfIkv/oxQDc1hYEx/u3EU+Wwj51DemXo4ct2phaBjLH7bc2KVM5lJEOFSODme9AKlrWiu8etlYESCjSZ01TXwrD1VFoIO3Ga16TNa7xV9Y4V9MXnkcvd2v1OUHpIwB1VjjrKvpoC31959mQfXOKvJ8aLTdru+IjYuCYlMIlz7hIy6tNTpzfHA0biVsHSU8Y4ap301GeuyRp4GQdS0px8pJR0wN7BXuuXuUA3GO6vNqVCREveYcEAaFOgz0GDyCAqYiVk5MMXNNueLqGmUXBz9mvj9d/Mw==\"Accept:*/*Content-Type:application/json} request body:"}
|
||||
{"level":"debug","timestamp":"2026-03-20T13:35:34+08:00","caller":"kernel/baseClient.go:459","content":"------------------response content:HTTP/1.1 200 OK\r\nContent-Length: 250\r\nCache-Control: no-cache, must-revalidate\r\nConnection: keep-alive\r\nContent-Language: zh-CN\r\nContent-Type: application/json; charset=utf-8\r\nDate: Fri, 20 Mar 2026 05:35:33 GMT\r\nKeep-Alive: timeout=8\r\nRequest-Id: 08A5B9F3CD0610A60318FACDC0552094913028E68402-0\r\nServer: nginx\r\nWechatpay-Nonce: c8cd238aa151bc5b3562d983525c9a10\r\nWechatpay-Serial: 5F2543BF58239A4EB68FA4433DF1438A88B34B16\r\nWechatpay-Signature: cU4LnoqphDCn40rM/L3JSxkg6E3H9TZXBZ2xFed9nrEt885jzrXhZACABPs+o9Pw6yyIoPDbzTukJlAZNQTZT/6zliHHuP4Wp+XF1HnWm/AxbDQwK6t23GtzY8L60ZgCJQN7xbnjqHmwKL2cX86ZB9lWGvoAcMXRhLKN3McoicwTC4268Z7xwSvUGBbIv3zmawbXZkqiYKckowym8kEHP9KXzV/7SEwwvW3CIGmloTukwcpaYOtJSRuit6/A3uz25ryxrgHT2gIudNRpW/SRKV/+M5uCd8gsJgQX27IE+KW58BnUu7oP0JC0PZXtvBHheSXo/9PmfBQVRt4Bgsbqtg==\r\nWechatpay-Signature-Type: WECHATPAY2-SHA256-RSA2048\r\nWechatpay-Timestamp: 1773984933\r\nX-Content-Type-Options: nosniff\r\n\r\n{\"amount\":{\"payer_currency\":\"CNY\",\"total\":1000},\"appid\":\"wxb8bbb2b10dec74aa\",\"mchid\":\"1318592501\",\"out_trade_no\":\"MP20260320130734906900\",\"promotion_detail\":[],\"scene_info\":{\"device_id\":\"\"},\"trade_state\":\"NOTPAY\",\"trade_state_desc\":\"订单未支付\"}"}
|
||||
|
||||
226
开发文档/1、需求/链接人与事-置顶功能-技术分析.md
Normal file
226
开发文档/1、需求/链接人与事-置顶功能-技术分析.md
Normal file
@@ -0,0 +1,226 @@
|
||||
# 链接人与事 — 置顶功能 技术分析
|
||||
|
||||
> 参与角色:管理端开发工程师、后端工程师、小程序开发工程师
|
||||
> 创建日期:2026-03-20
|
||||
> 需求:管理端「链接人与事」列表增加「置顶」能力,置顶人昵称+头像显示在小程序首页右上角;置顶唯一(只能一人)
|
||||
|
||||
---
|
||||
|
||||
## 一、现状梳理
|
||||
|
||||
| 模块 | 现状 |
|
||||
|------|------|
|
||||
| **管理端** | ContentPage「链接人与事」tab,列表展示 token、@的人、获客数、planId、apiKey、操作(编辑/查看新客户/编辑计划/删除) |
|
||||
| **Person 模型** | person_id、token、name、label、user_id、ckb_api_key、ckb_plan_id 等;**无 avatar 字段** |
|
||||
| **小程序首页** | 右上角固定展示 `/assets/images/author-avatar.png` + 文案「点击链接卡若」 |
|
||||
| **index-lead** | `POST /api/miniprogram/ckb/index-lead` 使用全局 `getCkbLeadApiKey()` 推送到存客宝,文案硬编码「卡若会尽快联系您」 |
|
||||
| **置顶存储** | 无,需新增 |
|
||||
|
||||
---
|
||||
|
||||
## 二、改造方案总览
|
||||
|
||||
### 2.1 数据层
|
||||
|
||||
| 改造项 | 方案 | 说明 |
|
||||
|--------|------|------|
|
||||
| 置顶存储 | `system_config` 新增 `pinned_person_token` | config_value 存 `{"token":"xxx"}`,token 为 persons.token |
|
||||
| Person 头像 | 方案 A:Person 表加 `avatar` 字段 | 管理端可编辑,小程序展示 |
|
||||
| Person 头像 | 方案 B:有 user_id 时 JOIN users 取 avatar | 无 user_id 时用默认占位图 |
|
||||
|
||||
**推荐**:Person 表加 `avatar` 字段(VARCHAR 255),管理端 PersonAddEditModal 可编辑;有 user_id 时可选从 users 同步(后续扩展)。
|
||||
|
||||
### 2.2 接口层
|
||||
|
||||
| 接口 | 使用方 | 说明 |
|
||||
|------|--------|------|
|
||||
| `PUT /api/db/persons/pin` | 管理端 | body: `{ token }`,置顶该人;先清空其他置顶再设置 |
|
||||
| `GET /api/miniprogram/ckb/pinned-person` | 小程序 | 返回 `{ nickname, avatar, token }` 或空(无置顶时) |
|
||||
| `POST /api/miniprogram/ckb/index-lead` | 小程序 | 改造:根据 pinned_person_token 查 persons 得 ckb_api_key,推送到该人计划;无置顶时 fallback 全局密钥 |
|
||||
|
||||
### 2.3 管理端
|
||||
|
||||
- 操作列新增「置顶」图标(Pin 或 Star)
|
||||
- 点击置顶:调用 `PUT /api/db/persons/pin`,成功后刷新列表,已置顶行显示「已置顶」标识
|
||||
- 置顶互斥:新置顶时,后端自动取消其他人置顶
|
||||
|
||||
### 2.4 小程序
|
||||
|
||||
- 首页 onLoad/onShow 调用 `GET /api/miniprogram/ckb/pinned-person` 获取置顶人
|
||||
- 有数据:展示 `avatar`(网络图需配置 downloadFile 域名)+ `nickname` + 文案「点击链接{nickname}」
|
||||
- 无数据:展示默认头像 + 「点击链接卡若」(兼容旧版)
|
||||
- 点击留资:仍调 `index-lead`,后端按置顶人 token 推送到对应存客宝计划
|
||||
|
||||
---
|
||||
|
||||
## 三、流程图
|
||||
|
||||
### 3.1 置顶设置流程(管理端)
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph 管理端
|
||||
A[管理员点击「置顶」] --> B[PUT /api/db/persons/pin]
|
||||
B --> C{后端处理}
|
||||
C --> D[读取 system_config.pinned_person_token]
|
||||
D --> E[更新 config_value = 新 token]
|
||||
E --> F[返回 success]
|
||||
F --> G[管理端刷新列表]
|
||||
G --> H[置顶行显示「已置顶」]
|
||||
end
|
||||
|
||||
subgraph 后端
|
||||
C --> I[清空原 pinned_person_token]
|
||||
I --> J[写入新 token]
|
||||
J --> F
|
||||
end
|
||||
```
|
||||
|
||||
### 3.2 小程序首页展示与留资流程
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph 小程序首页
|
||||
A1[onLoad / onShow] --> A2[GET /api/miniprogram/ckb/pinned-person]
|
||||
A2 --> A3{有置顶人?}
|
||||
A3 -->|是| A4[展示 nickname + avatar]
|
||||
A3 -->|否| A5[展示默认「点击链接卡若」]
|
||||
A4 --> A6[用户点击]
|
||||
A5 --> A6
|
||||
A6 --> A7[POST /api/miniprogram/ckb/index-lead]
|
||||
end
|
||||
|
||||
subgraph 后端 index-lead
|
||||
A7 --> B1[读取 pinned_person_token]
|
||||
B1 --> B2{有置顶?}
|
||||
B2 -->|是| B3[查 persons 得 ckb_api_key]
|
||||
B3 --> B4[用该 key 推存客宝]
|
||||
B2 -->|否| B5[用全局 getCkbLeadApiKey]
|
||||
B5 --> B4
|
||||
B4 --> B6[返回 success]
|
||||
end
|
||||
```
|
||||
|
||||
### 3.3 三端协同时序图
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant 管理端
|
||||
participant 后端
|
||||
participant 小程序
|
||||
|
||||
Note over 管理端: 阶段 1:置顶设置
|
||||
管理端->>后端: PUT /api/db/persons/pin { token }
|
||||
后端->>后端: 更新 system_config.pinned_person_token
|
||||
后端-->>管理端: success
|
||||
|
||||
Note over 小程序: 阶段 2:首页展示
|
||||
小程序->>后端: GET /api/miniprogram/ckb/pinned-person
|
||||
后端->>后端: 读 pinned_person_token → 查 persons
|
||||
后端-->>小程序: { nickname, avatar, token }
|
||||
小程序->>小程序: 渲染右上角
|
||||
|
||||
Note over 小程序: 阶段 3:用户点击留资
|
||||
小程序->>后端: POST /api/miniprogram/ckb/index-lead
|
||||
后端->>后端: 按 pinned token 取 ckb_api_key → 推存客宝
|
||||
后端-->>小程序: success
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、实施清单(按角色)
|
||||
|
||||
### 4.1 后端工程师
|
||||
|
||||
| 序号 | 任务 | 说明 |
|
||||
|------|------|------|
|
||||
| 1 | Person 模型加 `avatar` | `Avatar string gorm:"column:avatar;size:255" json:"avatar"` |
|
||||
| 2 | 迁移脚本 | `ALTER TABLE persons ADD COLUMN avatar VARCHAR(255) DEFAULT ''` |
|
||||
| 3 | `PUT /api/db/persons/pin` | body: `{ token }`,更新 system_config.pinned_person_token |
|
||||
| 4 | `GET /api/miniprogram/ckb/pinned-person` | 读 pinned token → 查 persons → 返回 nickname、avatar、token |
|
||||
| 5 | 改造 CKBIndexLead | 有 pinned 时用 persons.ckb_api_key;无则 fallback 全局 |
|
||||
| 6 | PersonAddEditModal 支持 avatar | 管理端提交时传 avatar(可选) |
|
||||
|
||||
### 4.2 管理端开发工程师
|
||||
|
||||
| 序号 | 任务 | 说明 |
|
||||
|------|------|------|
|
||||
| 1 | 操作列加「置顶」按钮 | Pin 图标,title「设为置顶」 |
|
||||
| 2 | 调用 PUT /api/db/persons/pin | 成功后 toast + 刷新 loadPersons |
|
||||
| 3 | 置顶状态展示 | 列表加载后需知「当前置顶是谁」→ 需 GET pinned 接口或列表返回 isPinned |
|
||||
| 4 | PersonAddEditModal 加 avatar 输入 | 可选,URL 输入框 |
|
||||
|
||||
**置顶状态获取**:后端 `GET /api/db/persons` 可扩展返回 `pinnedToken`(或单独 `GET /api/db/config?key=pinned_person_token`),管理端据此标亮已置顶行。
|
||||
|
||||
### 4.3 小程序开发工程师
|
||||
|
||||
| 序号 | 任务 | 说明 |
|
||||
|------|------|------|
|
||||
| 1 | 首页 initData 加 loadPinnedPerson | 调用 GET /api/miniprogram/ckb/pinned-person |
|
||||
| 2 | data 增加 pinnedPerson | `{ nickname, avatar, token }` 或 null |
|
||||
| 3 | WXML 动态渲染 | `wx:if` 有 pinnedPerson 时用其 avatar + nickname,否则默认 |
|
||||
| 4 | 文案 | 有置顶:「点击链接{{pinnedPerson.nickname}}」;无:「点击链接卡若」 |
|
||||
| 5 | 头像 | 网络图需配置 downloadFile 合法域名;失败时用占位 |
|
||||
|
||||
---
|
||||
|
||||
## 五、接口契约
|
||||
|
||||
### 5.1 PUT /api/db/persons/pin(管理端)
|
||||
|
||||
**请求**:
|
||||
```json
|
||||
{ "token": "32位persons.token" }
|
||||
```
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{ "success": true }
|
||||
```
|
||||
或
|
||||
```json
|
||||
{ "success": false, "error": "该人物不存在" }
|
||||
```
|
||||
|
||||
### 5.2 GET /api/miniprogram/ckb/pinned-person(小程序)
|
||||
|
||||
**响应(有置顶)**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"nickname": "卡若",
|
||||
"avatar": "https://xxx/avatar.png",
|
||||
"token": "xxx"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**响应(无置顶)**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 管理端获取当前置顶
|
||||
|
||||
- 方案 A:`GET /api/db/config?key=pinned_person_token` 返回 `{ "token": "xxx" }`
|
||||
- 方案 B:`GET /api/db/persons` 响应增加 `pinnedToken` 字段
|
||||
|
||||
推荐方案 A,与现有 config 接口一致。
|
||||
|
||||
---
|
||||
|
||||
## 六、注意事项
|
||||
|
||||
1. **缓存**:pinned-person 可加入 config 缓存或短 TTL(如 1 分钟),置顶变更后需失效。
|
||||
2. **兼容**:无置顶时,index-lead 保持现有逻辑(全局密钥),文案可继续「卡若会尽快联系您」或改为通用「提交成功」。
|
||||
3. **Person 删除**:删除已置顶的 Person 时,后端应同时清空 pinned_person_token。
|
||||
4. **头像域名**:小程序展示网络头像需在微信后台配置 downloadFile 合法域名。
|
||||
|
||||
---
|
||||
|
||||
**创建时间**:2026-03-20
|
||||
**适用**:链接人与事置顶、小程序首页动态展示
|
||||
112
开发文档/提现流程图.md
Normal file
112
开发文档/提现流程图.md
Normal file
@@ -0,0 +1,112 @@
|
||||
# Soul 提现流程图
|
||||
|
||||
## 一、整体流程(Mermaid)
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph 小程序["小程序端"]
|
||||
A1[用户进入推广中心/我的]
|
||||
A2[输入提现金额]
|
||||
A3[POST /api/miniprogram/withdraw]
|
||||
end
|
||||
|
||||
subgraph 后端校验["后端校验"]
|
||||
B1{参数校验}
|
||||
B2{金额 > 0?}
|
||||
B3{金额 ≤ 可提现?}
|
||||
B4{金额 ≥ 最低门槛?}
|
||||
B5{用户存在且已绑定微信?}
|
||||
end
|
||||
|
||||
subgraph 落库["落库"]
|
||||
C1[写入 withdrawals 表<br/>status = pending]
|
||||
end
|
||||
|
||||
subgraph 管理端["管理端"]
|
||||
D1[管理员查看提现列表<br/>GET /api/admin/withdrawals]
|
||||
D2{审核决策}
|
||||
D3[拒绝 → status = failed]
|
||||
D4[批准 → 调用微信转账]
|
||||
end
|
||||
|
||||
subgraph 微信["微信支付"]
|
||||
E1[商家转账到零钱]
|
||||
E2{微信返回 state}
|
||||
E3[processing<br/>处理中]
|
||||
E4[pending_confirm<br/>待用户确认收款]
|
||||
end
|
||||
|
||||
subgraph 用户确认["用户确认"]
|
||||
F1[获取待确认列表<br/>GET /api/miniprogram/withdraw/pending-confirm]
|
||||
F2[领取零钱<br/>wx.requestMerchantTransfer]
|
||||
F3[确认收款<br/>POST /api/miniprogram/withdraw/confirm-received]
|
||||
end
|
||||
|
||||
subgraph 终态["终态"]
|
||||
G1[success<br/>已到账]
|
||||
G2[failed<br/>已拒绝/打款失败]
|
||||
end
|
||||
|
||||
A1 --> A2 --> A3 --> B1
|
||||
B1 --> B2 --> B3 --> B4 --> B5
|
||||
B5 -->|通过| C1
|
||||
C1 --> D1 --> D2
|
||||
D2 -->|拒绝| D3 --> G2
|
||||
D2 -->|批准| D4 --> E1 --> E2
|
||||
E2 --> E3
|
||||
E2 --> E4 --> F1 --> F2 --> F3 --> G1
|
||||
E3 -->|回调/同步| G1
|
||||
E3 -->|失败| G2
|
||||
```
|
||||
|
||||
## 二、可提现金额计算逻辑
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph 公式["可提现 = 累计佣金 - 已提现 - 待审核"]
|
||||
A[累计佣金<br/>paid 订单佣金求和]
|
||||
B[已提现<br/>status=success 的 sum]
|
||||
C[待审核<br/>pending+processing+pending_confirm 的 sum]
|
||||
end
|
||||
A --> D[可提现]
|
||||
B --> D
|
||||
C --> D
|
||||
```
|
||||
|
||||
## 三、状态流转
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> pending: 用户申请
|
||||
pending --> failed: 管理员拒绝
|
||||
pending --> processing: 批准且微信直接处理
|
||||
pending --> pending_confirm: 批准且需用户确认收款
|
||||
processing --> success: 微信回调/主动同步
|
||||
processing --> failed: 打款失败
|
||||
pending_confirm --> success: 用户点击确认收款
|
||||
success --> [*]
|
||||
failed --> [*]
|
||||
```
|
||||
|
||||
## 四、关键接口一览
|
||||
|
||||
| 端 | 接口 | 用途 |
|
||||
|---|---|---|
|
||||
| 小程序 | POST /api/miniprogram/withdraw | 申请提现 |
|
||||
| 小程序 | GET /api/miniprogram/withdraw/records | 提现记录 |
|
||||
| 小程序 | GET /api/miniprogram/withdraw/pending-confirm | 待确认收款列表 |
|
||||
| 小程序 | GET /api/miniprogram/withdraw/confirm-info | 领取零钱参数 |
|
||||
| 小程序 | POST /api/miniprogram/withdraw/confirm-received | 确认收款 |
|
||||
| 管理端 | GET /api/admin/withdrawals | 提现列表 |
|
||||
| 管理端 | PUT /api/admin/withdrawals | 审核/打款(approve/reject) |
|
||||
| 管理端 | POST /api/admin/withdrawals/sync | 主动同步微信转账结果 |
|
||||
|
||||
## 五、校验规则(申请时)
|
||||
|
||||
1. **金额 > 0**
|
||||
2. **金额 ≤ 可提现余额**(累计佣金 - 已提现 - 待审核)
|
||||
3. **金额 ≥ 最低提现门槛**(默认 10 元,来自 referral_config.minWithdrawAmount)
|
||||
4. **用户存在**
|
||||
5. **用户已绑定微信**(openid 不为空)
|
||||
|
||||
批准时二次校验可提现金额,防止退款/冲正后超额打款。
|
||||
@@ -1,220 +0,0 @@
|
||||
# Windows 上通过 WSL2 + QEMU/KVM 运行 macOS 虚拟机(安装方案)
|
||||
|
||||
> 目标:在 **Windows 10/11** 上,以 **WSL2(Ubuntu)+ QEMU/KVM + OpenCore 引导**的方式运行 macOS 虚拟机,用于演示/测试。
|
||||
>
|
||||
> 重要说明:
|
||||
> - **macOS 不能用 Docker 容器运行**(Docker 只运行 Linux 容器)。本方案使用的是 **虚拟机**。
|
||||
> - 本方案 **不依赖 Docker Desktop**,也不会要求你卸载/改动 Docker Desktop。
|
||||
|
||||
---
|
||||
|
||||
## 1. 前置条件
|
||||
|
||||
- **Windows 10/11**(建议较新的版本)
|
||||
- CPU 支持虚拟化(Intel VT-x / AMD-V),并在 BIOS/UEFI 中开启
|
||||
- 管理员权限(启用 Windows 功能、安装 WSL、创建防火墙规则时可能需要)
|
||||
- 具备稳定网络(下载恢复镜像与依赖)
|
||||
- 一个 VNC 客户端(例如 TightVNC/RealVNC/TigerVNC 等)
|
||||
|
||||
---
|
||||
|
||||
## 2. 启用 WSL2(管理员 PowerShell)
|
||||
|
||||
> 如果你已经能 `wsl -l -v` 正常看到发行版,可跳过本节。
|
||||
|
||||
```powershell
|
||||
wsl --install
|
||||
```
|
||||
|
||||
重启后,如需安装指定发行版:
|
||||
|
||||
```powershell
|
||||
wsl --install -d Ubuntu-24.04 --web-download
|
||||
```
|
||||
|
||||
验证:
|
||||
|
||||
```powershell
|
||||
wsl -l -v
|
||||
```
|
||||
|
||||
期望看到 `Ubuntu-24.04` 且 `VERSION` 为 `2`。
|
||||
|
||||
---
|
||||
|
||||
## 3. 目录约定(建议)
|
||||
|
||||
建议把虚拟机相关文件统一放到一个目录,便于迁移与清理,例如:
|
||||
|
||||
- Windows:`%USERPROFILE%\Mycontent\macos-vm`
|
||||
- WSL:`/mnt/c/Users/<你的用户名>/Mycontent/macos-vm`
|
||||
|
||||
在 Windows 侧先创建目录:
|
||||
|
||||
```powershell
|
||||
mkdir "$env:USERPROFILE\Mycontent\macos-vm" -Force | Out-Null
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 在 Ubuntu 中安装 QEMU / KVM 依赖
|
||||
|
||||
进入 Ubuntu:
|
||||
|
||||
```powershell
|
||||
wsl -d Ubuntu-24.04
|
||||
```
|
||||
|
||||
在 Ubuntu 里执行:
|
||||
|
||||
```bash
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y qemu-system qemu-utils python3 python3-pip cpu-checker git curl unzip
|
||||
```
|
||||
|
||||
验证 KVM:
|
||||
|
||||
```bash
|
||||
kvm-ok
|
||||
```
|
||||
|
||||
期望输出包含:
|
||||
|
||||
- `INFO: /dev/kvm exists`
|
||||
- `KVM acceleration can be used`
|
||||
|
||||
若没有 `/dev/kvm`,通常是 BIOS 虚拟化未开、或系统虚拟化相关功能未启用导致。
|
||||
|
||||
---
|
||||
|
||||
## 5. 获取启动脚本工程(推荐 ZIP 方式)
|
||||
|
||||
下面以 OneClick-macOS-Simple-KVM 为例(它主要负责:下载恢复镜像、准备虚拟盘、拼 QEMU 启动参数)。
|
||||
|
||||
在 Ubuntu 中进入工作目录:
|
||||
|
||||
```bash
|
||||
cd /mnt/c/Users/<你的用户名>/Mycontent/macos-vm
|
||||
```
|
||||
|
||||
下载并解压(ZIP 方式更抗网络波动):
|
||||
|
||||
```bash
|
||||
rm -rf OneClick-macOS-Simple-KVM OneClick.zip
|
||||
curl -L --retry 8 --retry-delay 2 --connect-timeout 20 --max-time 600 \
|
||||
-o OneClick.zip \
|
||||
https://codeload.github.com/notAperson535/OneClick-macOS-Simple-KVM/zip/refs/heads/master
|
||||
unzip -q OneClick.zip
|
||||
mv OneClick-macOS-Simple-KVM-master OneClick-macOS-Simple-KVM
|
||||
```
|
||||
|
||||
进入工程目录并授权:
|
||||
|
||||
```bash
|
||||
cd OneClick-macOS-Simple-KVM
|
||||
chmod +x *.sh *.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 准备虚拟硬盘与恢复镜像(以 Ventura 为例)
|
||||
|
||||
创建系统盘(容量可按需调整):
|
||||
|
||||
```bash
|
||||
[ -f macOS.qcow2 ] || qemu-img create -f qcow2 macOS.qcow2 64G
|
||||
```
|
||||
|
||||
下载恢复镜像(脚本会拉取官方 Recovery/Restore 资源并生成/落盘):
|
||||
|
||||
```bash
|
||||
python3 fetch-macOS-v2.py -s ventura
|
||||
```
|
||||
|
||||
常见做法是把下载到的 dmg 统一命名为 `BaseSystem.dmg`,并转 raw:
|
||||
|
||||
```bash
|
||||
[ -f RecoveryImage.dmg ] && mv RecoveryImage.dmg BaseSystem.dmg
|
||||
qemu-img convert BaseSystem.dmg -O raw BaseSystem.img
|
||||
ls -lah BaseSystem.* macOS.qcow2
|
||||
```
|
||||
|
||||
经验上你会看到:
|
||||
|
||||
- `BaseSystem.dmg` 数百 MB
|
||||
- `BaseSystem.img` 数 GB
|
||||
- `macOS.qcow2` 初始很小,安装完成后会逐渐变大
|
||||
|
||||
---
|
||||
|
||||
## 7. 启动虚拟机(Headless + VNC)
|
||||
|
||||
在 OneClick 目录启动(Headless 表示不弹 GUI 窗口,改走 VNC):
|
||||
|
||||
```bash
|
||||
sudo HEADLESS=1 ./basic.sh
|
||||
```
|
||||
|
||||
在 Windows 侧确认 VNC 端口监听(通常是 `5900`):
|
||||
|
||||
```powershell
|
||||
Get-NetTCPConnection -LocalPort 5900 -State Listen
|
||||
```
|
||||
|
||||
然后用 VNC 客户端连接:
|
||||
|
||||
- `localhost:5900`
|
||||
|
||||
进入图形界面后,常规安装流程是:
|
||||
|
||||
- 先打开“磁盘工具”抹掉/格式化虚拟盘(APFS)
|
||||
- 退出磁盘工具
|
||||
- 选择“安装 macOS”
|
||||
|
||||
安装过程中会多次重启属正常。
|
||||
|
||||
---
|
||||
|
||||
## 8. 常见问题
|
||||
|
||||
### 8.1 `kvm-ok` 不通过 / 没有 `/dev/kvm`
|
||||
|
||||
- 检查 BIOS/UEFI 是否开启虚拟化
|
||||
- 确保启用了 Windows 虚拟化相关功能(WSL2、VirtualMachinePlatform 等)
|
||||
- 若你已经在用 WSL2,但仍没有 `/dev/kvm`,通常是环境限制或虚拟化被占用/策略限制
|
||||
|
||||
### 8.2 VNC 连不上
|
||||
|
||||
- Windows 侧确认 `5900` 端口确实在 Listen
|
||||
- 检查防火墙策略(本机连 `localhost` 一般不需要额外放行)
|
||||
- 确认启动脚本没有退出(WSL 窗口关闭会导致 VM 退出)
|
||||
|
||||
---
|
||||
|
||||
## 9. 卸载/清理(不动 Docker Desktop)
|
||||
|
||||
### 9.1 只删除 macOS VM 文件(保留 Ubuntu)
|
||||
|
||||
直接删目录即可(示例路径):
|
||||
|
||||
```powershell
|
||||
Remove-Item -Recurse -Force "$env:USERPROFILE\Mycontent\macos-vm"
|
||||
```
|
||||
|
||||
### 9.2 卸载 Ubuntu(保留 Docker Desktop)
|
||||
|
||||
先确认 WSL 中有哪些发行版:
|
||||
|
||||
```powershell
|
||||
wsl -l -v
|
||||
```
|
||||
|
||||
只卸载 Ubuntu(示例 `Ubuntu-24.04`):
|
||||
|
||||
```powershell
|
||||
wsl --terminate Ubuntu-24.04
|
||||
wsl --unregister Ubuntu-24.04
|
||||
```
|
||||
|
||||
> 注意:以上会删除 Ubuntu 发行版的全部数据,但不会卸载 Docker Desktop,也不会动 `docker-desktop` 发行版。
|
||||
|
||||
Reference in New Issue
Block a user