更新小程序隐私保护机制,新增手机号一键登录功能,用户需同意隐私协议后方可获取手机号。优化多个页面的登录交互,提升用户体验。调整相关配置以支持新功能。

This commit is contained in:
Alex-larget
2026-03-20 13:40:13 +08:00
parent 0bc32deb94
commit 385e47bc55
60 changed files with 2954 additions and 1669 deletions

View File

@@ -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
- binaryGo 二进制 + 宝塔 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: 不打进 tardocker: 不上传服务器目录 .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)