打好基版

This commit is contained in:
乘风
2026-02-09 15:09:29 +08:00
parent cdac67ebfd
commit 7b2123dfe5
132 changed files with 1511 additions and 504 deletions

775
devlop.py Normal file
View File

@@ -0,0 +1,775 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from __future__ import print_function
import os
import sys
import shutil
import tempfile
import argparse
import json
import zipfile
import tarfile
import subprocess
import time
import hashlib
try:
import paramiko
except ImportError:
print("错误: 请先安装 paramiko")
print(" pip install paramiko")
sys.exit(1)
try:
import requests
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
except ImportError:
print("错误: 请先安装 requests")
print(" pip install requests")
sys.exit(1)
# ==================== 配置 ====================
# 端口统一从环境变量 DEPLOY_PORT 读取,未设置时使用此默认值(需与 Nginx proxy_pass、ecosystem.config.cjs 一致)
DEPLOY_PM2_APP = "soul"
DEFAULT_DEPLOY_PORT = 3006
DEPLOY_PROJECT_PATH = "/www/wwwroot/自营/soul"
DEPLOY_SITE_URL = "https://soul.quwanzhi.com"
# SSH 端口(支持环境变量 DEPLOY_SSH_PORT未设置时默认为 22022
DEFAULT_SSH_PORT = int(os.environ.get("DEPLOY_SSH_PORT", "22022"))
def get_cfg():
"""获取基础部署配置deploy 模式与 devlop 共用 SSH/宝塔)"""
return {
"host": os.environ.get("DEPLOY_HOST", "43.139.27.93"),
"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),
"panel_url": os.environ.get("BAOTA_PANEL_URL", "https://43.139.27.93:9988"),
"api_key": os.environ.get("BAOTA_API_KEY", "hsAWqFSi0GOCrunhmYdkxy92tBXfqYjd"),
"pm2_name": os.environ.get("DEPLOY_PM2_APP", DEPLOY_PM2_APP),
"site_url": os.environ.get("DEPLOY_SITE_URL", DEPLOY_SITE_URL),
"port": int(os.environ.get("DEPLOY_PORT", str(DEFAULT_DEPLOY_PORT))),
"node_version": os.environ.get("DEPLOY_NODE_VERSION", "v22.14.0"),
"node_path": os.environ.get("DEPLOY_NODE_PATH", "/www/server/nodejs/v22.14.0/bin"),
}
def get_cfg_devlop():
"""devlop 模式配置:在基础配置上增加 base_path / dist / dist2。
实际运行目录为 dist_path切换后新版本在 dist宝塔 PM2 项目路径必须指向 dist_path
否则会从错误目录启动导致 .next/static 等静态资源 404。"""
cfg = get_cfg().copy()
cfg["base_path"] = os.environ.get("DEVOP_BASE_PATH", DEPLOY_PROJECT_PATH)
cfg["dist_path"] = cfg["base_path"] + "/dist"
cfg["dist2_path"] = cfg["base_path"] + "/dist2"
return cfg
# ==================== 宝塔 API ====================
def _get_sign(api_key):
now_time = int(time.time())
sign_str = str(now_time) + hashlib.md5(api_key.encode("utf-8")).hexdigest()
request_token = hashlib.md5(sign_str.encode("utf-8")).hexdigest()
return now_time, request_token
def _baota_request(panel_url, api_key, path, data=None):
req_time, req_token = _get_sign(api_key)
payload = {"request_time": req_time, "request_token": req_token}
if data:
payload.update(data)
url = panel_url.rstrip("/") + "/" + path.lstrip("/")
try:
r = requests.post(url, data=payload, verify=False, timeout=30)
return r.json() if r.text else {}
except Exception as e:
print(" API 请求失败: %s" % str(e))
return None
def get_node_project_list(panel_url, api_key):
for path in ["/project/nodejs/get_project_list", "/plugin?action=a&name=nodejs&s=get_project_list"]:
result = _baota_request(panel_url, api_key, path)
if result and (result.get("status") is True or "data" in result):
return result.get("data", [])
return None
def get_node_project_status(panel_url, api_key, pm2_name):
projects = get_node_project_list(panel_url, api_key)
if projects:
for p in projects:
if p.get("name") == pm2_name:
return p
return None
def start_node_project(panel_url, api_key, pm2_name):
for path in ["/project/nodejs/start_project", "/plugin?action=a&name=nodejs&s=start_project"]:
result = _baota_request(panel_url, api_key, path, {"project_name": pm2_name})
if result and (result.get("status") is True or result.get("msg") or "成功" in str(result)):
print(" [成功] 启动成功: %s" % pm2_name)
return True
return False
def stop_node_project(panel_url, api_key, pm2_name):
for path in ["/project/nodejs/stop_project", "/plugin?action=a&name=nodejs&s=stop_project"]:
result = _baota_request(panel_url, api_key, path, {"project_name": pm2_name})
if result and (result.get("status") is True or result.get("msg") or "成功" in str(result)):
print(" [成功] 停止成功: %s" % pm2_name)
return True
return False
def restart_node_project(panel_url, api_key, pm2_name):
project_status = get_node_project_status(panel_url, api_key, pm2_name)
if project_status:
print(" 项目状态: %s" % project_status.get("status", "未知"))
for path in ["/project/nodejs/restart_project", "/plugin?action=a&name=nodejs&s=restart_project"]:
result = _baota_request(panel_url, api_key, path, {"project_name": pm2_name})
if result and (result.get("status") is True or result.get("msg") or "成功" in str(result)):
print(" [成功] 重启成功: %s" % pm2_name)
return True
if result and "msg" in result:
print(" API 返回: %s" % result.get("msg"))
print(" [警告] 重启失败,请检查宝塔 Node 插件是否安装、API 密钥是否正确")
return False
def add_or_update_node_project(panel_url, api_key, pm2_name, project_path, port=None, node_path=None):
if port is None:
port = int(os.environ.get("DEPLOY_PORT", str(DEFAULT_DEPLOY_PORT)))
port_env = "PORT=%d " % port
run_cmd = port_env + ("%s/node server.js" % node_path if node_path else "node server.js")
payload = {"name": pm2_name, "path": project_path, "run_cmd": run_cmd, "port": str(port)}
for path in ["/project/nodejs/add_project", "/plugin?action=a&name=nodejs&s=add_project"]:
result = _baota_request(panel_url, api_key, path, payload)
if result and result.get("status") is True:
print(" [成功] 项目配置已更新: %s" % pm2_name)
return True
if result and "msg" in result:
print(" API 返回: %s" % result.get("msg"))
return False
# ==================== 本地构建 ====================
def run_build(root):
"""执行本地 pnpm build"""
use_shell = sys.platform == "win32"
standalone = os.path.join(root, ".next", "standalone")
server_js = os.path.join(standalone, "server.js")
try:
r = subprocess.run(
["pnpm", "build"],
cwd=root,
shell=use_shell,
timeout=600,
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
)
stdout_text = r.stdout or ""
stderr_text = r.stderr or ""
combined = stdout_text + stderr_text
is_windows_symlink_error = (
sys.platform == "win32"
and r.returncode != 0
and ("EPERM" in combined or "symlink" in combined.lower() or "operation not permitted" in combined.lower() or "errno: -4048" in combined)
)
if r.returncode != 0:
if is_windows_symlink_error:
print(" [警告] Windows 符号链接权限错误EPERM")
print(" 解决方案:开启开发者模式 / 以管理员运行 / 或使用 --no-build")
if os.path.isdir(standalone) and os.path.isfile(server_js):
print(" [成功] standalone 输出可用,继续部署")
return True
return False
print(" [失败] 构建失败,退出码:", r.returncode)
for line in (stdout_text.strip().split("\n") or [])[-10:]:
print(" " + line)
return False
except subprocess.TimeoutExpired:
print(" [失败] 构建超时超过10分钟")
return False
except FileNotFoundError:
print(" [失败] 未找到 pnpm请安装: npm install -g pnpm")
return False
except Exception as e:
print(" [失败] 构建异常:", str(e))
if os.path.isdir(standalone) and os.path.isfile(server_js):
print(" [提示] 可尝试使用 --no-build 跳过构建")
return False
if not os.path.isdir(standalone) or not os.path.isfile(server_js):
print(" [失败] 未找到 .next/standalone 或 server.js")
return False
print(" [成功] 构建完成")
return True
def clean_standalone_before_build(root, retries=3, delay=2):
"""构建前删除 .next/standalone避免 Windows EBUSY"""
standalone = os.path.join(root, ".next", "standalone")
if not os.path.isdir(standalone):
return True
for attempt in range(1, retries + 1):
try:
shutil.rmtree(standalone)
print(" [清理] 已删除 .next/standalone%d 次尝试)" % attempt)
return True
except (OSError, PermissionError):
if attempt < retries:
print(" [清理] 被占用,%ds 后重试 (%d/%d) ..." % (delay, attempt, retries))
time.sleep(delay)
else:
print(" [失败] 无法删除 .next/standalone可改用 --no-build")
return False
return False
# ==================== 打包deploy 模式tar.gz ====================
def _copy_with_dereference(src, dst):
if os.path.islink(src):
link_target = os.readlink(src)
real_path = link_target if os.path.isabs(link_target) else os.path.join(os.path.dirname(src), link_target)
if os.path.exists(real_path):
if os.path.isdir(real_path):
shutil.copytree(real_path, dst, symlinks=False, dirs_exist_ok=True)
else:
shutil.copy2(real_path, dst)
else:
shutil.copy2(src, dst, follow_symlinks=False)
elif os.path.isdir(src):
if os.path.exists(dst):
shutil.rmtree(dst)
shutil.copytree(src, dst, symlinks=False, dirs_exist_ok=True)
else:
shutil.copy2(src, dst)
def pack_standalone_tar(root):
"""打包 standalone 为 tar.gzdeploy 模式用)"""
print("[2/4] 打包 standalone ...")
standalone = os.path.join(root, ".next", "standalone")
static_src = os.path.join(root, ".next", "static")
public_src = os.path.join(root, "public")
ecosystem_src = os.path.join(root, "ecosystem.config.cjs")
if not os.path.isdir(standalone) or not os.path.isdir(static_src):
print(" [失败] 未找到 .next/standalone 或 .next/static")
return None
chunks_dir = os.path.join(static_src, "chunks")
if not os.path.isdir(chunks_dir):
print(" [失败] .next/static/chunks 不存在,请先完整执行 pnpm build本地 pnpm start 能正常打开页面后再部署)")
return None
staging = tempfile.mkdtemp(prefix="soul_deploy_")
try:
for name in os.listdir(standalone):
_copy_with_dereference(os.path.join(standalone, name), os.path.join(staging, name))
node_modules_dst = os.path.join(staging, "node_modules")
pnpm_dir = os.path.join(node_modules_dst, ".pnpm")
if os.path.isdir(pnpm_dir):
for dep in ["styled-jsx"]:
dep_in_root = os.path.join(node_modules_dst, dep)
if not os.path.exists(dep_in_root):
for pnpm_pkg in os.listdir(pnpm_dir):
if pnpm_pkg.startswith(dep + "@"):
src_dep = os.path.join(pnpm_dir, pnpm_pkg, "node_modules", dep)
if os.path.isdir(src_dep):
shutil.copytree(src_dep, dep_in_root, symlinks=False, dirs_exist_ok=True)
break
static_dst = os.path.join(staging, ".next", "static")
if os.path.exists(static_dst):
shutil.rmtree(static_dst)
os.makedirs(os.path.dirname(static_dst), exist_ok=True)
shutil.copytree(static_src, static_dst)
# 同步构建索引(与 start-standalone.js 一致),避免宝塔上 server 用错导致页面空白/404
next_root = os.path.join(root, ".next")
next_staging = os.path.join(staging, ".next")
index_files = [
"BUILD_ID",
"build-manifest.json",
"app-path-routes-manifest.json",
"routes-manifest.json",
"prerender-manifest.json",
"required-server-files.json",
"fallback-build-manifest.json",
]
for name in index_files:
src = os.path.join(next_root, name)
if os.path.isfile(src):
shutil.copy2(src, os.path.join(next_staging, name))
print(" [已同步] 构建索引: BUILD_ID, build-manifest, routes-manifest 等")
if os.path.isdir(public_src):
shutil.copytree(public_src, os.path.join(staging, "public"), dirs_exist_ok=True)
if os.path.isfile(ecosystem_src):
shutil.copy2(ecosystem_src, os.path.join(staging, "ecosystem.config.cjs"))
pkg_json = os.path.join(staging, "package.json")
if os.path.isfile(pkg_json):
try:
with open(pkg_json, "r", encoding="utf-8") as f:
data = json.load(f)
data.setdefault("scripts", {})["start"] = "node server.js"
with open(pkg_json, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
except Exception:
pass
tarball = os.path.join(tempfile.gettempdir(), "soul_deploy.tar.gz")
with tarfile.open(tarball, "w:gz") as tf:
for name in os.listdir(staging):
tf.add(os.path.join(staging, name), 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)
# ==================== Node 环境检查 & SSH 上传deploy 模式) ====================
def check_node_environments(cfg):
print("[检查] Node 环境 ...")
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
try:
if cfg.get("ssh_key"):
client.connect(cfg["host"], port=DEFAULT_SSH_PORT, username=cfg["user"], key_filename=cfg["ssh_key"], timeout=15)
else:
client.connect(cfg["host"], port=DEFAULT_SSH_PORT, username=cfg["user"], password=cfg["password"], timeout=15)
stdin, stdout, stderr = client.exec_command("which node && node -v", timeout=10)
print(" 默认 Node: %s" % (stdout.read().decode("utf-8", errors="replace").strip() or "未找到"))
node_path = cfg.get("node_path", "/www/server/nodejs/v22.14.0/bin")
stdin, stdout, stderr = client.exec_command("%s/node -v 2>/dev/null" % node_path, timeout=5)
print(" 配置 Node: %s" % (stdout.read().decode("utf-8", errors="replace").strip() or "不可用"))
return True
except Exception as e:
print(" [警告] %s" % str(e))
return False
finally:
client.close()
def upload_and_extract(cfg, tarball_path):
"""SSH 上传 tar.gz 并解压到 project_pathdeploy 模式)"""
print("[3/4] SSH 上传并解压 ...")
if not cfg.get("password") and not cfg.get("ssh_key"):
print(" [失败] 请设置 DEPLOY_PASSWORD 或 DEPLOY_SSH_KEY")
return False
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=DEFAULT_SSH_PORT, username=cfg["user"], key_filename=cfg["ssh_key"], timeout=15)
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_deploy.tar.gz"
remote_script = "/tmp/soul_deploy_extract.sh"
sftp.put(tarball_path, remote_tar)
node_path = cfg.get("node_path", "/www/server/nodejs/v22.14.0/bin")
project_path = cfg["project_path"]
script_content = """#!/bin/bash
export PATH=%s:$PATH
cd %s
rm -rf .next public ecosystem.config.cjs server.js package.json 2>/dev/null
tar -xzf %s
rm -f %s
echo OK
""" % (node_path, project_path, remote_tar, remote_tar)
with sftp.open(remote_script, "w") as f:
f.write(script_content)
sftp.close()
client.exec_command("chmod +x %s" % remote_script, timeout=10)
stdin, stdout, stderr = client.exec_command("bash %s" % remote_script, timeout=120)
out = stdout.read().decode("utf-8", errors="replace").strip()
exit_status = stdout.channel.recv_exit_status()
if exit_status != 0 or "OK" not in out:
print(" [失败] 解压失败,退出码:", exit_status)
return False
print(" [成功] 解压完成: %s" % project_path)
return True
except Exception as e:
print(" [失败] SSH 错误:", str(e))
return False
finally:
client.close()
def deploy_via_baota_api(cfg):
"""宝塔 API 重启 Node 项目deploy 模式)"""
print("[4/4] 宝塔 API 管理 Node 项目 ...")
panel_url, api_key, pm2_name = cfg["panel_url"], cfg["api_key"], cfg["pm2_name"]
project_path = cfg["project_path"]
node_path = cfg.get("node_path", "/www/server/nodejs/v22.14.0/bin")
port = cfg["port"]
if not get_node_project_status(panel_url, api_key, pm2_name):
add_or_update_node_project(panel_url, api_key, pm2_name, project_path, port, node_path)
stop_node_project(panel_url, api_key, pm2_name)
time.sleep(2)
ok = restart_node_project(panel_url, api_key, pm2_name)
if not ok:
ok = start_node_project(panel_url, api_key, pm2_name)
if not ok:
print(" 请到宝塔 Node 项目手动重启 %s,路径: %s" % (pm2_name, project_path))
return ok
# ==================== 打包devlop 模式zip ====================
ZIP_EXCLUDE_DIRS = {".cache", "__pycache__", ".git", "node_modules", "cache", "test", "tests", "coverage", ".nyc_output", ".turbo", "开发文档"}
ZIP_EXCLUDE_FILE_NAMES = {".DS_Store", "Thumbs.db"}
ZIP_EXCLUDE_FILE_SUFFIXES = (".log", ".map")
def _should_exclude_from_zip(arcname, is_file=True):
parts = arcname.replace("\\", "/").split("/")
for part in parts:
if part in ZIP_EXCLUDE_DIRS:
return True
if is_file and parts:
name = parts[-1]
if name in ZIP_EXCLUDE_FILE_NAMES or any(name.endswith(s) for s in ZIP_EXCLUDE_FILE_SUFFIXES):
return True
return False
def pack_standalone_zip(root):
"""打包 standalone 为 zipdevlop 模式用)"""
print("[2/7] 打包 standalone 为 zip ...")
standalone = os.path.join(root, ".next", "standalone")
static_src = os.path.join(root, ".next", "static")
public_src = os.path.join(root, "public")
ecosystem_src = os.path.join(root, "ecosystem.config.cjs")
if not os.path.isdir(standalone) or not os.path.isdir(static_src):
print(" [失败] 未找到 .next/standalone 或 .next/static")
return None
chunks_dir = os.path.join(static_src, "chunks")
if not os.path.isdir(chunks_dir):
print(" [失败] .next/static/chunks 不存在,请先完整执行 pnpm build本地 pnpm start 能正常打开页面后再部署)")
return None
staging = tempfile.mkdtemp(prefix="soul_devlop_")
try:
for name in os.listdir(standalone):
_copy_with_dereference(os.path.join(standalone, name), os.path.join(staging, name))
node_modules_dst = os.path.join(staging, "node_modules")
pnpm_dir = os.path.join(node_modules_dst, ".pnpm")
if os.path.isdir(pnpm_dir):
for dep in ["styled-jsx"]:
dep_in_root = os.path.join(node_modules_dst, dep)
if not os.path.exists(dep_in_root):
for pnpm_pkg in os.listdir(pnpm_dir):
if pnpm_pkg.startswith(dep + "@"):
src_dep = os.path.join(pnpm_dir, pnpm_pkg, "node_modules", dep)
if os.path.isdir(src_dep):
shutil.copytree(src_dep, dep_in_root, symlinks=False, dirs_exist_ok=True)
break
os.makedirs(os.path.join(staging, ".next"), exist_ok=True)
shutil.copytree(static_src, os.path.join(staging, ".next", "static"), dirs_exist_ok=True)
# 同步构建索引(与 start-standalone.js 一致),避免宝塔上 server 用错导致页面空白/404
next_root = os.path.join(root, ".next")
next_staging = os.path.join(staging, ".next")
index_files = [
"BUILD_ID",
"build-manifest.json",
"app-path-routes-manifest.json",
"routes-manifest.json",
"prerender-manifest.json",
"required-server-files.json",
"fallback-build-manifest.json",
]
for name in index_files:
src = os.path.join(next_root, name)
if os.path.isfile(src):
shutil.copy2(src, os.path.join(next_staging, name))
print(" [已同步] 构建索引: BUILD_ID, build-manifest, routes-manifest 等")
if os.path.isdir(public_src):
shutil.copytree(public_src, os.path.join(staging, "public"), dirs_exist_ok=True)
if os.path.isfile(ecosystem_src):
shutil.copy2(ecosystem_src, os.path.join(staging, "ecosystem.config.cjs"))
pkg_json = os.path.join(staging, "package.json")
if os.path.isfile(pkg_json):
try:
with open(pkg_json, "r", encoding="utf-8") as f:
data = json.load(f)
data.setdefault("scripts", {})["start"] = "node server.js"
with open(pkg_json, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
except Exception:
pass
server_js = os.path.join(staging, "server.js")
if os.path.isfile(server_js):
try:
deploy_port = int(os.environ.get("DEPLOY_PORT", str(DEFAULT_DEPLOY_PORT)))
with open(server_js, "r", encoding="utf-8") as f:
c = f.read()
if "|| 3000" in c:
with open(server_js, "w", encoding="utf-8") as f:
f.write(c.replace("|| 3000", "|| %d" % deploy_port))
except Exception:
pass
zip_path = os.path.join(tempfile.gettempdir(), "soul_devlop.zip")
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
for name in os.listdir(staging):
path = os.path.join(staging, name)
if os.path.isfile(path):
if not _should_exclude_from_zip(name):
zf.write(path, name)
else:
for dirpath, dirs, filenames in os.walk(path):
dirs[:] = [d for d in dirs if not _should_exclude_from_zip(os.path.join(name, os.path.relpath(os.path.join(dirpath, d), path)), is_file=False)]
for f in filenames:
full = os.path.join(dirpath, f)
arcname = os.path.join(name, os.path.relpath(full, path))
if not _should_exclude_from_zip(arcname):
zf.write(full, arcname)
print(" [成功] 打包完成: %s (%.2f MB)" % (zip_path, os.path.getsize(zip_path) / 1024 / 1024))
return zip_path
except Exception as e:
print(" [失败] 打包异常:", str(e))
return None
finally:
shutil.rmtree(staging, ignore_errors=True)
def upload_zip_and_extract_to_dist2(cfg, zip_path):
"""上传 zip 并解压到 dist2devlop 模式)"""
print("[3/7] SSH 上传 zip 并解压到 dist2 ...")
sys.stdout.flush()
if not cfg.get("password") and not cfg.get("ssh_key"):
print(" [失败] 请设置 DEPLOY_PASSWORD 或 DEPLOY_SSH_KEY")
return False
zip_size_mb = os.path.getsize(zip_path) / (1024 * 1024)
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
try:
print(" 正在连接 %s@%s:%s ..." % (cfg["user"], cfg["host"], DEFAULT_SSH_PORT))
sys.stdout.flush()
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=30, banner_timeout=30)
else:
client.connect(cfg["host"], port=DEFAULT_SSH_PORT, username=cfg["user"], password=cfg["password"], timeout=30, banner_timeout=30)
print(" [OK] SSH 已连接,正在上传 zip%.1f MB..." % zip_size_mb)
sys.stdout.flush()
remote_zip = cfg["base_path"].rstrip("/") + "/soul_devlop.zip"
sftp = client.open_sftp()
# 上传进度:每 5MB 打印一次
chunk_mb = 5.0
last_reported = [0]
def _progress(transferred, total):
if total and total > 0:
now_mb = transferred / (1024 * 1024)
if now_mb - last_reported[0] >= chunk_mb or transferred >= total:
last_reported[0] = now_mb
print("\r 上传进度: %.1f / %.1f MB" % (now_mb, total / (1024 * 1024)), end="")
sys.stdout.flush()
sftp.put(zip_path, remote_zip, callback=_progress)
if zip_size_mb >= chunk_mb:
print("")
print(" [OK] zip 已上传,正在服务器解压(约 13 分钟)...")
sys.stdout.flush()
sftp.close()
dist2 = cfg["dist2_path"]
cmd = "rm -rf %s && mkdir -p %s && unzip -o -q %s -d %s && rm -f %s && echo OK" % (dist2, dist2, remote_zip, dist2, remote_zip)
stdin, stdout, stderr = client.exec_command(cmd, timeout=300)
out = stdout.read().decode("utf-8", errors="replace").strip()
err = stderr.read().decode("utf-8", errors="replace").strip()
if err:
print(" 服务器 stderr: %s" % err[:500])
exit_status = stdout.channel.recv_exit_status()
if exit_status != 0 or "OK" not in out:
print(" [失败] 解压失败,退出码: %s" % exit_status)
if out:
print(" stdout: %s" % out[:300])
return False
print(" [成功] 已解压到: %s" % dist2)
return True
except Exception as e:
print(" [失败] SSH 错误: %s" % str(e))
import traceback
traceback.print_exc()
return False
finally:
client.close()
def run_pnpm_install_in_dist2(cfg):
"""服务器 dist2 内执行 pnpm install阻塞等待完成后再返回改目录前必须完成"""
print("[4/7] 服务器 dist2 内执行 pnpm install等待完成后再切换目录...")
sys.stdout.flush()
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=DEFAULT_SSH_PORT, username=cfg["user"], key_filename=cfg["ssh_key"], timeout=15)
else:
client.connect(cfg["host"], port=DEFAULT_SSH_PORT, username=cfg["user"], password=cfg["password"], timeout=15)
stdin, stdout, stderr = client.exec_command("bash -lc 'which pnpm'", timeout=10)
pnpm_path = stdout.read().decode("utf-8", errors="replace").strip()
if not pnpm_path:
return False, "未找到 pnpm请服务器安装: npm install -g pnpm"
cmd = "bash -lc 'cd %s && %s install'" % (cfg["dist2_path"], pnpm_path)
stdin, stdout, stderr = client.exec_command(cmd, timeout=300)
out = stdout.read().decode("utf-8", errors="replace").strip()
err = stderr.read().decode("utf-8", errors="replace").strip()
if stdout.channel.recv_exit_status() != 0:
return False, "pnpm install 失败\n" + (err or out)
print(" [成功] dist2 内 pnpm install 已执行完成,可安全切换目录")
return True, None
except Exception as e:
return False, str(e)
finally:
client.close()
def remote_swap_dist_and_restart(cfg):
"""暂停 → dist→dist1, dist2→dist → 删除 dist1 → 更新 PM2 项目路径 → 重启devlop 模式)"""
print("[5/7] 宝塔 API 暂停 Node 项目 ...")
stop_node_project(cfg["panel_url"], cfg["api_key"], cfg["pm2_name"])
time.sleep(2)
print("[6/7] 服务器切换目录: dist→dist1, dist2→dist ...")
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=DEFAULT_SSH_PORT, username=cfg["user"], key_filename=cfg["ssh_key"], timeout=15)
else:
client.connect(cfg["host"], port=DEFAULT_SSH_PORT, username=cfg["user"], password=cfg["password"], timeout=15)
cmd = "cd %s && mv dist dist1 2>/dev/null; mv dist2 dist && rm -rf dist1 && echo OK" % cfg["base_path"]
stdin, stdout, stderr = client.exec_command(cmd, timeout=60)
out = stdout.read().decode("utf-8", errors="replace").strip()
if stdout.channel.recv_exit_status() != 0 or "OK" not in out:
print(" [失败] 切换失败")
return False
print(" [成功] 新版本位于 %s" % cfg["dist_path"])
finally:
client.close()
# 关键devlop 实际运行目录是 dist_path必须让宝塔 PM2 从该目录启动,否则会从错误目录跑导致静态资源 404
print("[7/7] 更新宝塔 Node 项目路径并重启 ...")
add_or_update_node_project(
cfg["panel_url"], cfg["api_key"], cfg["pm2_name"],
cfg["dist_path"], # 使用 dist_path不是 project_path
port=cfg["port"],
node_path=cfg.get("node_path"),
)
if not start_node_project(cfg["panel_url"], cfg["api_key"], cfg["pm2_name"]):
print(" [警告] 请到宝塔手动启动 %s,并确认项目路径为: %s" % (cfg["pm2_name"], cfg["dist_path"]))
return False
return True
# ==================== 主函数 ====================
def main():
parser = argparse.ArgumentParser(description="Soul 创业派对 - 统一部署脚本", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=__doc__)
parser.add_argument("--mode", choices=["devlop", "deploy"], default="devlop", help="devlop=dist切换(默认), deploy=直接覆盖")
parser.add_argument("--no-build", action="store_true", help="跳过本地构建")
parser.add_argument("--no-upload", action="store_true", help="仅 deploy 模式:跳过 SSH 上传")
parser.add_argument("--no-api", action="store_true", help="仅 deploy 模式:上传后不调宝塔 API")
args = parser.parse_args()
script_dir = os.path.dirname(os.path.abspath(__file__))
# 支持 devlop.py 在项目根或 scripts/ 下:以含 package.json 的目录为 root
if os.path.isfile(os.path.join(script_dir, "package.json")):
root = script_dir
else:
root = os.path.dirname(script_dir)
if args.mode == "devlop":
cfg = get_cfg_devlop()
print("=" * 60)
print(" Soul 自动部署dist 切换)")
print("=" * 60)
print(" 服务器: %s@%s 目录: %s Node: %s" % (cfg["user"], cfg["host"], cfg["base_path"], cfg["pm2_name"]))
print("=" * 60)
if not args.no_build:
print("[1/7] 本地构建 pnpm build ...")
if sys.platform == "win32" and not clean_standalone_before_build(root):
return 1
if not run_build(root):
return 1
elif not os.path.isfile(os.path.join(root, ".next", "standalone", "server.js")):
print("[错误] 未找到 .next/standalone/server.js")
return 1
else:
print("[1/7] 跳过本地构建")
zip_path = pack_standalone_zip(root)
if not zip_path:
return 1
if not upload_zip_and_extract_to_dist2(cfg, zip_path):
return 1
try:
os.remove(zip_path)
except Exception:
pass
# 必须在 dist2 内 pnpm install 执行完成后再切换目录
ok, err = run_pnpm_install_in_dist2(cfg)
if not ok:
print(" [失败] %s" % (err or "pnpm install 失败"))
return 1
# install 已完成,再执行 dist→dist1、dist2→dist 切换
if not remote_swap_dist_and_restart(cfg):
return 1
print("")
print(" 部署完成!运行目录: %s" % cfg["dist_path"])
return 0
# deploy 模式
cfg = get_cfg()
print("=" * 60)
print(" Soul 一键部署(直接覆盖)")
print("=" * 60)
print(" 服务器: %s@%s 项目路径: %s PM2: %s" % (cfg["user"], cfg["host"], cfg["project_path"], cfg["pm2_name"]))
print("=" * 60)
if not args.no_upload:
check_node_environments(cfg)
if not args.no_build:
print("[1/4] 本地构建 ...")
if not run_build(root):
return 1
elif not os.path.isfile(os.path.join(root, ".next", "standalone", "server.js")):
print("[错误] 未找到 .next/standalone/server.js")
return 1
else:
print("[1/4] 跳过本地构建")
tarball = pack_standalone_tar(root)
if not tarball:
return 1
if not args.no_upload:
if not upload_and_extract(cfg, tarball):
return 1
try:
os.remove(tarball)
except Exception:
pass
else:
print(" 压缩包: %s" % tarball)
if not args.no_api and not args.no_upload:
deploy_via_baota_api(cfg)
print("")
print(" 部署完成!站点: %s" % cfg["site_url"])
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -1,89 +0,0 @@
# Mycontent-book 项目总览
**我是卡若。**
做这个项目,逻辑很简单:**把书卖出去,把私域做起来,把钱分下去。**
这就不是一个普通的博客网站,这是一个**内容变现系统**。所有的技术架构,都要围绕着“阅读体验”、“流量承接”和“变现转化”来做。
别整那些虚头巴脑的概念,咱们直接看这个盘子怎么搭。
## 1. 核心逻辑
这个项目的生意逻辑就是三层:
1. **流量层(前端)**:让用户看着爽,像刷抖音、看公众号一样流畅。必须移动端优先,模拟 iOS 的原生质感。
2. **内容层(数据)**`book/` 目录下的 Markdown 文件就是我们的资产。改个字,推送到 GitHub网站立马更新。
3. **变现层(后端/接口)**:谁看了?谁买了?谁推荐的?这些数据要跑通。
## 2. 为什么这么架构?
我选 Next.js不是因为流行是因为它**省事**。
- **SSR服务端渲染**SEO 友好,百度谷歌能搜到,自带流量。
- **API Routes**:不用单独起个 Java 或 Python 服务,省服务器钱。
- **Vercel/宝塔部署**:自动化流水线,我只管写文章,代码自动跑。
## 3. 当前项目结构Next 前端)
- **app/view/**移动端C 端)页面。根路径 `/` 重定向到 `/view`;路由为 `/view``/view/chapters``/view/read/[id]``/view/match``/view/my``/view/about``/view/login` 等。
- **app/admin/**:管理端页面,路由为 `/admin``/admin/*`
- **app/api/**:接口不变,仍为 `/api/*`
- **components/view/**:移动端布局与组件(如 layout-wrapper、bottom-nav、config**components/admin/**:管理端用 UI如 admin/ui
- 通用 UI 在 view 与 admin 各保留一份(见 `components/README.md`)。
## 4. 目录导航(别迷路)
- **[1、需求](1、需求/业务需求.md)**:我们要干啥,成本多少,技术要求;[TDD 需求方案](1、需求/TDD_创业派对项目方案_v1.0.md)。
- **[2、架构](2、架构/系统架构.md)**:整体怎么搭,前后端怎么分。
- [技术选型与全景图](2、架构/技术选型与全景图.md)、[前后端架构分离策略](2、架构/前后端架构分离策略.md)
- **[3、原型](3、原型/原型设计规范.md)**:原型设计规范。
- **[4、前端](4、前端/前端架构.md)**:前端架构、模块详解、开发规范;[当前小程序开发细则](4、前端/当前小程序开发细则.md)[ui 子目录](4、前端/ui/)项目概述、页面功能、组件清单、API/状态/分销/支付/管理后台/部署说明等。
- **[5、接口](5、接口/API接口.md)**:前后端怎么说话。
- **[6、后端](6、后端/后端架构.md)**:数据处理,后端开发规范。
- **[7、数据库](7、数据库/数据库设计.md)**:数据存哪,怎么存。
- **[8、部署](8、部署/部署总览.md)**:怎么上线、本地运行、宝塔部署、新分销部署、修复与优化记录等。
- **[9、手册](9、手册/写作与结构维护手册.md)**:怎么写书,怎么维护。
- **[10、项目管理](10、项目管理/项目落地推进表.md)**:项目推进与提示词。
## 5. 开发约束(重要)
> **2026-02-04 起生效**
### 5.1 前端开发策略
| 端 | 路径 | 开发状态 | 说明 |
|---|------|---------|-----|
| **微信小程序** | `miniprogram/` | ✅ 活跃开发 | 所有 C 端新功能在此开发 |
| **Next.js C端** | `app/view/` | 🔒 冻结维护 | 不再新增功能,仅修复严重 Bug |
| **Next.js 管理端** | `app/admin/` | ✅ 活跃开发 | 管理后台继续在此开发 |
| **API 接口** | `app/api/` | ✅ 活跃开发 | 小程序和管理端共用 |
### 5.2 核心原则
1. **小程序优先**:所有面向用户的新功能,只在小程序端开发
2. **Next.js C端冻结**`app/view/` 目录不再新增功能,保持现状作为 Web 备用入口
3. **管理端继续**`app/admin/` 管理后台功能继续在 Next.js 开发
4. **API 统一**:接口层保持统一,小程序和 Web 端共用同一套 API
### 5.3 登录体系差异
| 端 | 登录方式 | 说明 |
|---|---------|-----|
| 小程序 | 微信一键登录 / 手机号快速授权 | 保持原生体验,不复刻 Next.js 登录页 |
| Next.js | 手机号 + 密码 | 保持现状 |
| 账号统一 | 以手机号为唯一标识 | 两端数据互通 |
### 5.4 为什么这样做?
- **用户体量**90%+ 用户来自小程序,优先保障主要渠道体验
- **开发效率**:集中精力做好一端,避免两端同步维护的成本
- **原生体验**:小程序有更好的分享、支付、订阅消息等微信生态能力
## 6. 这里的规矩
- **行动至上**:文档是用来指导干活的,不是写来看的。
- **数据说话**:所有优化要有数据支撑,加载快了多少?转化高了多少?
- **保持简单**:能用现成的库就别自己造轮子。
---
**复盘:**
目前项目处于“文件数据库”阶段,适合我这种单人高频写作。等流量上来了,用户系统一接,立马切 MongoDB。这一步步来别贪多。

View File

@@ -1,70 +0,0 @@
# 前后端分离开发架构图
**我是卡若。**
为了让你(开发人员)能闭着眼睛把活干了,我把前后端分离的流程画得清清楚楚。
我们采用**接口契约驱动开发 (Contract-First Development)**。
## 1. 分离开发流程
\`\`\`mermaid
sequenceDiagram
participant PM as 卡若 (PM)
participant Doc as API 文档
participant FE as 前端开发
participant BE as 后端开发
PM->>Doc: 1. 定义需求与接口 (API接口.md)
Note over Doc: 确定 URL, Params, Response
par 并行开发
FE->>Doc: 2. 查阅接口定义
FE->>FE: 3. Mock 数据 (假数据)
FE->>FE: 4. 开发 UI 与交互
BE->>Doc: 2. 查阅接口定义
BE->>BE: 3. 实现业务逻辑
BE->>BE: 4. 单元测试 (Postman)
end
FE->>BE: 5. 联调 (对接真实接口)
BE-->>FE: 返回真实数据
FE->>PM: 6. 验收与上线
\`\`\`
## 2. 架构交互图 (Data Flow)
\`\`\`mermaid
graph LR
subgraph "前端域 (Browser)"
UI[页面组件] --> API_Client[API 请求封装层]
API_Client -- JSON 请求 --> API_Route
end
subgraph "后端域 (Server/Next.js)"
API_Route[API 路由入口] --> Controller[业务控制器]
Controller --> Service[逻辑服务层]
Service --> Content[内容解析器]
Service --> Config[配置加载器]
Content -- 读取 --> FS[文件系统 book/]
Config -- 读取 --> DB[(MongoDB/JSON)]
end
Service -- JSON 响应 --> Controller
Controller -- HTTP 响应 --> API_Client
API_Client -- 数据 --> UI
\`\`\`
## 3. 落地执行规范
1. **接口先行**: 没定义好接口文档,不许写代码。
2. **Mock 优先**: 前端别等后端,自己造个 JSON 数据先跑起来。
3. **统一封装**: 前端所有请求必须走 `lib/api.ts`,禁止在组件里直接写 `fetch('/api/...')`
---
**卡若说:**
按这个流程走,前后端吵架的概率降低 90%。效率就是这么抠出来的。

View File

@@ -1,61 +0,0 @@
# 系统架构
**我是卡若。**
架构不是为了画图好看,是为了**省事**和**赚钱**。
我们的架构设计,核心围绕两个字:**分离**。
- 内容和代码分离。
- 前端和后端分离。
- 静态和动态分离。
## 1. 架构全景图
\`\`\`mermaid
graph TD
subgraph "内容生产 (Content)"
Typora[本地写作] --> Git[Git 仓库]
Git --> AutoSync[自动同步脚本]
end
subgraph "应用层 (Next.js)"
AutoSync --> FileSys[文件系统 book/]
FileSys --> Backend[后端 API]
Backend --> Frontend[前端 UI]
end
subgraph "用户触达 (User)"
Frontend --> Wechat[微信环境]
Frontend --> Browser[手机浏览器]
end
\`\`\`
## 2. 核心设计理念
### 2.1 “内容即产品”
我们的核心资产不是代码,是 `book/` 目录下的文章。
- 代码丢了可以重写,文章丢了就完了。
- 所以,文章用 Markdown 存Git 管,最安全。
### 2.2 前后端分离 (即使在 Next.js 里)
为了以后能扩展(比如招人开发,或者把后端换成 Java我们在代码逻辑上做了强制隔离。
- 详见:**[前后端架构分离策略](file:///Users/karuo/Documents/个人/2、我写的书/《一场soul的创业实验》/开发文档/2、架构/前后端架构分离策略.md)**
### 2.3 极简部署
- 不要 Docker除非必要
- 不要 K8s。
- 既然是 Node.js 项目PM2 或者 Vercel 就够了。宝塔面板配个 Webhook 自动拉代码,是最适合个人开发者的方案。
## 3. 关键约束
1. **本地优先**: 写作在本地,代码开发也在本地。
2. **稀疏检出 (Sparse Checkout)**: 如果你只负责写作,就别拉取代码;如果你负责开发,就拉取全部。
3. **单向流动**: 数据流向是 `Markdown -> API -> UI`。除非是评论或订单,否则 UI 不反向修改 Markdown。
## 4. 详细技术栈
详见:**[技术选型与全景图](file:///Users/karuo/Documents/个人/2、我写的书/《一场soul的创业实验》/开发文档/2、架构/技术选型与全景图.md)**
---
**总结:**
保持架构的简单性,就是保持业务的灵活性。能在文件里解决的,就别上数据库;能在 Next.js 里解决的,就别拆微服务。

View File

@@ -1,56 +0,0 @@
# 前端开发规范 (Frontend Specs) - 智能自生长文档
> **提示词功能 (Prompt Function)**: 将本文件拖入 AI 对话框,即可激活“前端技术专家”角色,生成符合 iOS 风格的 React 代码。
## 1. 基础上下文 (The Two Basic Files)
### 1.1 角色档案:卡若 (Karuo)
- **视觉标准**:像素级复刻 iOS (San Francisco, 1:1 间距, 弥散阴影)。
- **体验标准**:无白屏 (Skeleton),丝滑转场 (Transition)。
### 1.2 技术栈
- **核心**React + Shadcn UI + Tailwind CSS。
- **辅助**Vant UI (移动端组件)。
- **构建**Vite / Next.js。
## 2. 开发规范核心 (Master Content)
### 2.1 视觉与风格 (iOS)
- **字体**San Francisco > PingFang SC。
- **色彩**
- 背景:`#F2F2F7` (Grouped Background)。
- 分割:`#C6C6C8`
- 交互:`#007AFF` (System Blue)。
- **细节**
- 圆角:统一 `rounded-lg``rounded-xl`
- 阴影:柔和弥散,非生硬投影。
### 2.2 交互与性能 (Mandatory)
- **骨架屏**:数据加载必须显示 Skeleton严禁 Spinner。
- **转场**:路由切换必须有动画。
- **图片**:懒加载 + 失败占位。
### 2.3 目录结构
- `/src/components`: 原子组件。
- `/scenarios/new`: 场景获客页。
- `/src/hooks`: 逻辑复用。
## 3. AI 协作指令 (Expanded Function)
**角色**:你是我(卡若)的前端主程。
**任务**
1. **代码生成**:生成 React 组件代码,**必须**包含 Tailwind 类名。
2. **样式检查**:确保所有 UI 元素符合 iOS 规范(检查圆角、阴影、字体)。
3. **结构分析**:用 Mermaid 展示组件依赖。
### 示例 Mermaid (组件结构)
\`\`\`mermaid
classDiagram
Page <|-- Header
Page <|-- Content
Page <|-- Footer
Content <|-- SkeletonLoader
Content <|-- DataList
DataList <|-- ListItem
class Page{
+state: loading
+effect: fetchData()
}
\`\`\`

View File

@@ -1,94 +0,0 @@
# 前端架构
**我是卡若。**
前端就是项目的脸。用户不管是通过朋友圈、抖音还是私域进来,第一眼看到的就是这个页面。如果加载慢、长得丑、滑动卡,人家转头就走,我的流量就浪费了。
所以,前端的核心目标只有一个:**极致的移动端阅读体验,像原生 App 一样丝滑。**
## 1. 技术底座
别跟我说什么技术先进,我要的是**稳**和**快**。
- **框架**: Next.js 14 (App Router) - 必须用最新的 App Router路由管理更清晰。
- **语言**: TypeScript - 必须用 TS类型安全少出低级 Bug。
- **样式**: Tailwind CSS - 写样式最快,没有之一。配合 `globals.css` 做全局控制。
- **UI 组件库**: Shadcn UI (基于 Radix UI) + Vant UI (风格参考)。
- *注意*:我们要像素级复刻 iOS 风格,字体用 San Francisco圆角、阴影都要对齐。
## 2. 目录结构(我的地盘)
前端代码主要集中在 `app/``components/`
\`\`\`
app/
├── (routes)/ # 路由组,逻辑隔离
│ ├── page.tsx # 首页:封面、简介、购买按钮
│ ├── chapters/ # 目录页:章节列表
│ ├── read/[id]/ # 阅读页:核心体验区
│ ├── my/ # 个人中心:购买记录、分销
│ ├── admin/ # 管理后台:给自己用的
│ └── documentation/ # 文档生成:内部工具
├── layout.tsx # 全局布局导航栏、SEO Meta
├── globals.css # 全局样式
└── error.tsx # 错误处理页面
components/
├── ui/ # 通用组件 (Button, Input, Skeleton)
├── modules/ # 业务模块组件 (新增)
│ ├── auth/ # 认证模块 (AuthModal)
│ ├── payment/ # 支付模块 (PaymentModal)
│ ├── marketing/ # 营销模块 (QRCodeModal)
│ └── referral/ # 分销模块 (ReferralShare)
├── book-cover.tsx # 书籍封面展示
├── chapter-content.tsx # 章节内容渲染器
├── bottom-nav.tsx # 底部导航栏 (手机端核心)
└── theme-provider.tsx # 主题管理 (深色/浅色模式)
\`\`\`
## 2.1 业务模块化 (Modularization)
为了支持“云阿米巴”模式的快速迭代,我们将核心业务逻辑封装为独立模块:
- **支付模块 (Payment)**: 统一管理微信、支付宝、USDT 等支付方式,支持整书/单章购买。
- **营销模块 (Marketing)**: 负责引流如二维码弹窗、倒计时Banner连接私域流量池。
- **分销模块 (Referral)**: 负责裂变传播(如分享按钮、返利计算),让用户帮我们卖书。
- **认证模块 (Auth)**: 统一的用户登录与权限校验。
这种设计允许我们在不修改页面核心逻辑的情况下,插拔不同的变现策略。
## 3. 核心交互设计
### 3.1 骨架屏 (Skeleton)
**规则**:凡是需要加载数据的地方,必须先展示骨架屏。
- 用户不能看白屏,哪怕等 0.5 秒,也要让他看到“东西正在来”的样子。
- 强制引入 `Skeleton` 组件。
### 3.2 路由动画 (Transition)
**规则**:页面切换不能生硬地跳。
- 使用 Framer Motion 或 CSS Transition。
- 模拟 iOS 的滑动切换或淡入淡出。
### 3.3 阅读体验
- **字体**:针对不同设备优化,保证字号适中,行间距舒服(建议 1.6-1.8)。
- **图片**:懒加载 (Lazy Load),点击可放大预览。
- **代码块**:虽然是书,但如果有代码,要有高亮和复制按钮。
## 4. 数据获取 (Fetching)
- **服务端组件 (Server Components)**
- `page.tsx`, `read/[id]/page.tsx` 默认都是服务端组件。
- 直接在组件内 `await` 获取数据(通过 `lib/book-data.ts`SEO 极佳。
- **客户端组件 (Client Components)**
- 需要交互的(点击、弹窗、状态变化),头部加 `'use client'`
- 比如 `auth-modal.tsx`, `purchase-section.tsx`
## 5. 待办事项 (Todo)
- [ ] 全局引入 Skeleton替换掉所有的 `Loading...` 文字。
- [ ] 检查所有页面的 Mobile 适配,在 Chrome 开发者工具里用 iPhone SE 和 iPhone 14 Pro Max 两个尺寸测。
- [ ] 优化字体栈,确保在安卓上也不难看。
---
**总结**
前端不仅是写代码,是**做产品**。每一个像素的偏移都影响用户的信任感。把细节抠好,转化率自然就高了。

View File

@@ -1,64 +0,0 @@
# 后端开发规范 (Backend Specs) - 智能自生长文档
> **提示词功能 (Prompt Function)**: 将本文件拖入 AI 对话框即可激活“Python 后端专家”角色,生成高效、规范的 FastAPI 代码。
## 1. 基础上下文 (The Two Basic Files)
### 1.1 角色档案:卡若 (Karuo)
- **核心**:开发快、性能好、支持 AI。
- **习惯**:优先使用异步 (`async/await`),强制类型提示 (`Type Hints`)。
### 1.2 技术栈
- **语言**Python 3.10+。
- **框架**FastAPI (Web), Pydantic (Validation), LangChain (AI)。
- **数据**Motor (Async Mongo), Redis。
## 2. 开发规范核心 (Master Content)
### 2.1 代码规范
- **风格**:遵循 PEP 8使用 Black 格式化。
- **类型****强制 Type Hints** (如 `def get_user(id: int) -> User:`)。
- **注释****强制中文注释**解释“业务逻辑”与“AI 处理流程”。
- **结构**
- `app/routers`: 路由
- `app/models`: Pydantic 模型
- `app/services`: 业务逻辑
- `app/core`: 配置与工具
### 2.2 AI 与安全规范
- **AI 调用**:所有 LLM 调用必须封装在 Service 层,并包含重试机制与超时控制。
- **安全**
- **命令执行**:严禁使用 `os.system`,必须使用 `subprocess` 并校验参数。
- **SQL/NoSQL**:使用 ORM 或参数化查询,防止注入。
### 2.3 异常与日志
- **异常**:使用 FastAPI `HTTPException` 或自定义 Exception Handler。
- **日志**:使用 `loguru` 或 Python 标准 `logging`,必须记录 Traceback。
### 2.4 依赖管理
- **工具**`pip``poetry`
- **原则**:提交代码前更新 `requirements.txt``pyproject.toml`
## 3. AI 协作指令 (Expanded Function)
**角色**:你是我(卡若)的 Python 架构师。
**任务**
1. **代码实现**:生成 FastAPI 的 Router/Model/Service 代码。
2. **AI 集成**:编写 LangChain 调用逻辑或向量检索代码。
3. **逻辑图解**:用 Mermaid 展示异步处理流程。
### 示例 Mermaid (类图)
\`\`\`mermaid
classDiagram
class UserRouter {
+get_user()
+create_user()
}
class UserService {
+verify_token()
+process_ai_request()
}
class VectorStore {
+search_similarity()
+add_documents()
}
UserRouter --> UserService
UserService --> VectorStore
\`\`\`

View File

@@ -1,68 +0,0 @@
# 后端架构与业务逻辑
**我是卡若。**
后端不仅仅是读写数据库,它是**业务逻辑的翻译官**。
我们要把“私域引流”、“内容分发”这些生意话术,翻译成代码逻辑。
## 1. 核心业务模块
### 1.1 内容服务 (Content Service)
这是最基础的。
- **逻辑**:
- 扫描 `book/` 目录,生成目录树 (Tree)。
- 解析 Markdown提取 Frontmatter (标题、日期、标签)。
- **缓存策略**: 既然是读文件IO 慢。要在内存里做一个 LRU 缓存,读取一次后由内存直接返回,直到文件发生变更。
### 1.2 配置服务 (Config Service)
我的微信号、群二维码、价格,这些东西会变,不能写死在代码里。
- **实现**:
- 一个 `config/settings.json` 文件(或者未来的 MongoDB `settings` 表)。
- 接口: `GET /api/config`
- 前端拿到配置,动态展示微信号。
### 1.3 引流服务 (Lead Service)
这是赚钱的关键。
- **埋点逻辑**:
- 记录 `UserView` (用户看了哪章)。
- 记录 `UserClick` (用户点了“加微信”)。
- 虽然不存库,但可以先打到日志文件里,或者调一个飞书的 Webhook实时通知我“有人对这章感兴趣”。
## 2. 接口设计原则
- **RESTful**: 资源导向。`GET /articles`, `GET /articles/:id`
- **统一响应体**:
\`\`\`typescript
interface ApiResponse<T> {
code: number; // 0 成功, >0 错误
data: T;
msg: string;
}
\`\`\`
## 3. 目录结构 (后端专用)
\`\`\`
app/api/
├── content/ # 内容相关
├── config/ # 全局配置
└── track/ # 埋点上报
lib/
├── content/
│ ├── parser.ts # Markdown 解析器
│ └── cache.ts # 内存缓存
├── config/
│ └── loader.ts # 配置加载器
└── db/ # 数据库连接 (预留)
\`\`\`
## 4. 扩展性预留
- **鉴权中间件**: 现在是裸奔,未来加 `middleware.ts` 拦截 `/admin` 开头的请求。
- **任务队列**: 未来如果生成文档太慢,就扔到 Redis 队列里异步处理。
---
**卡若说:**
后端代码要写得像瑞士军刀一样,功能明确,结实耐用。

View File

@@ -1,3 +1,5 @@
# 对接后端 base URL不改 API 路径,仅改此处即可切换 Next → Gin
# VITE_API_BASE_URL=http://localhost:3006
VITE_API_BASE_URL=http://localhost:8080
# VITE_API_BASE_URL=http://localhost:8080
VITE_API_BASE_URL=http://soulapi.quwanzhi.com

1
soul-admin/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
node_modules/

View File

@@ -6,7 +6,7 @@ import './index.css'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<BrowserRouter>
<BrowserRouter future={{ v7_startTransition: true, v7_relativeSplatPath: true }}>
<App />
</BrowserRouter>
</StrictMode>,

238
soul-api/devlop.py Normal file
View File

@@ -0,0 +1,238 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
soul-api Go 项目一键部署到宝塔
- 本地交叉编译 Linux 二进制
- 上传到 /www/wwwroot/自营/soul-api
- 重启服务nohup 或跳过)
"""
from __future__ import print_function
import os
import sys
import tempfile
import argparse
import subprocess
import shutil
import tarfile
try:
import paramiko
except ImportError:
print("错误: 请先安装 paramiko")
print(" pip install paramiko")
sys.exit(1)
# ==================== 配置 ====================
DEPLOY_PROJECT_PATH = "/www/wwwroot/自营/soul-api"
DEFAULT_SSH_PORT = int(os.environ.get("DEPLOY_SSH_PORT", "22022"))
def get_cfg():
return {
"host": os.environ.get("DEPLOY_HOST", "43.139.27.93"),
"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),
}
# ==================== 本地构建 ====================
def run_build(root):
"""交叉编译 Go 二进制Linux amd64"""
print("[1/4] 本地交叉编译 Go 二进制 ...")
env = os.environ.copy()
env["GOOS"] = "linux"
env["GOARCH"] = "amd64"
env["CGO_ENABLED"] = "0"
try:
r = subprocess.run(
["go", "build", "-o", "soul-api", "./cmd/server"],
cwd=root,
env=env,
shell=(sys.platform == "win32"),
timeout=120,
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
)
if r.returncode != 0:
print(" [失败] go build 失败,退出码:", r.returncode)
if r.stderr:
for line in (r.stderr or "").strip().split("\n")[-10:]:
print(" " + line)
return None
out_path = os.path.join(root, "soul-api")
if not os.path.isfile(out_path):
print(" [失败] 未找到编译产物 soul-api")
return None
print(" [成功] 编译完成: %s (%.2f MB)" % (out_path, os.path.getsize(out_path) / 1024 / 1024))
return out_path
except subprocess.TimeoutExpired:
print(" [失败] 编译超时")
return None
except FileNotFoundError:
print(" [失败] 未找到 go 命令,请安装 Go")
return None
except Exception as e:
print(" [失败] 编译异常:", str(e))
return None
# ==================== 打包 ====================
def pack_deploy(root, binary_path, include_env=True):
"""打包二进制和 .env 为 tar.gz"""
print("[2/4] 打包部署文件 ...")
staging = tempfile.mkdtemp(prefix="soul_api_deploy_")
try:
shutil.copy2(binary_path, os.path.join(staging, "soul-api"))
env_src = os.path.join(root, ".env")
if include_env and os.path.isfile(env_src):
shutil.copy2(env_src, os.path.join(staging, ".env"))
print(" [已包含] .env")
else:
env_example = os.path.join(root, ".env.example")
if os.path.isfile(env_example):
shutil.copy2(env_example, os.path.join(staging, ".env"))
print(" [已包含] .env.example -> .env (请服务器上检查配置)")
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):
tf.add(os.path.join(staging, name), 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)
# ==================== SSH 上传 ====================
def upload_and_extract(cfg, tarball_path, no_restart=False):
"""上传 tar.gz 到服务器并解压、重启"""
print("[3/4] SSH 上传并解压 ...")
if not cfg.get("password") and not cfg.get("ssh_key"):
print(" [失败] 请设置 DEPLOY_PASSWORD 或 DEPLOY_SSH_KEY")
return False
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=DEFAULT_SSH_PORT,
username=cfg["user"], key_filename=cfg["ssh_key"],
timeout=15,
)
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"]
sftp.put(tarball_path, remote_tar)
sftp.close()
cmd = (
"mkdir -p %s && cd %s && tar -xzf %s && "
"chmod +x soul-api && rm -f %s && echo OK"
) % (project_path, project_path, remote_tar, remote_tar)
stdin, stdout, stderr = client.exec_command(cmd, timeout=60)
out = stdout.read().decode("utf-8", errors="replace").strip()
exit_status = stdout.channel.recv_exit_status()
if exit_status != 0 or "OK" not in out:
print(" [失败] 解压失败,退出码:", exit_status)
return False
print(" [成功] 已解压到: %s" % project_path)
if not no_restart:
print("[4/4] 重启 soul-api 服务 ...")
restart_cmd = (
"cd %s && pkill -f 'soul-api' 2>/dev/null; sleep 2; "
"nohup ./soul-api >> soul-api.log 2>&1 & sleep 1; "
"pgrep -f soul-api >/dev/null && echo RESTART_OK || echo RESTART_FAIL"
) % project_path
stdin, stdout, stderr = client.exec_command(restart_cmd, timeout=15)
out = stdout.read().decode("utf-8", errors="replace").strip()
if "RESTART_OK" in out:
print(" [成功] soul-api 已重启")
else:
print(" [警告] 重启状态未知,请手动检查: cd %s && ./soul-api" % project_path)
else:
print("[4/4] 跳过重启 (--no-restart)")
return True
except Exception as e:
print(" [失败] SSH 错误:", str(e))
return False
finally:
client.close()
# ==================== 主函数 ====================
def main():
parser = argparse.ArgumentParser(
description="soul-api Go 项目一键部署到宝塔",
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument("--no-build", action="store_true", help="跳过本地编译(使用已有 soul-api 二进制)")
parser.add_argument("--no-env", action="store_true", help="不打包 .env保留服务器现有 .env")
parser.add_argument("--no-restart", action="store_true", help="上传后不重启服务")
args = parser.parse_args()
script_dir = os.path.dirname(os.path.abspath(__file__))
root = script_dir
cfg = get_cfg()
print("=" * 60)
print(" soul-api 一键部署到宝塔")
print("=" * 60)
print(" 服务器: %s@%s:%s" % (cfg["user"], cfg["host"], DEFAULT_SSH_PORT))
print(" 目标目录: %s" % cfg["project_path"])
print("=" * 60)
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 二进制,请先编译或去掉 --no-build")
return 1
print("[1/4] 跳过编译,使用现有 soul-api")
tarball = pack_deploy(root, binary_path, include_env=not args.no_env)
if not tarball:
return 1
if not upload_and_extract(cfg, tarball, no_restart=args.no_restart):
return 1
try:
os.remove(tarball)
except Exception:
pass
print("")
print(" 部署完成!目录: %s" % cfg["project_path"])
return 0
if __name__ == "__main__":
sys.exit(main())

BIN
soul-api/soul-api Normal file

Binary file not shown.

BIN
开发文档/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -0,0 +1,99 @@
# Mycontent-book 项目总览
**我是卡若。**
做这个项目,逻辑很简单:**把书卖出去,把私域做起来,把钱分下去。**
这就不是一个普通的博客网站,这是一个**内容变现系统**。所有的技术架构,都要围绕着“阅读体验”、“流量承接”和“变现转化”来做。
别整那些虚头巴脑的概念,咱们直接看这个盘子怎么搭。
## 1. 核心逻辑
这个项目的生意逻辑就是三层:
1. **流量层(前端)**:让用户看着爽,像刷抖音、看公众号一样流畅。必须移动端优先,模拟 iOS 的原生质感。
2. **内容层(数据)**`book/` 目录下的 Markdown 文件就是我们的资产。改个字,推送到 GitHub网站立马更新。
3. **变现层(后端/接口)**:谁看了?谁买了?谁推荐的?这些数据要跑通。
## 2. 为什么这么架构?
我选 Next.js不是因为流行是因为它**省事**。
- **SSR服务端渲染**SEO 友好,百度谷歌能搜到,自带流量。
- **API Routes**:不用单独起个 Java 或 Python 服务,省服务器钱。
- **Vercel/宝塔部署**:自动化流水线,我只管写文章,代码自动跑。
## 3. 当前项目结构(前后端已分离)
### 3.1 仓库根目录
| 目录/项目 | 技术栈 | 说明 |
|-----------|--------|------|
| **soul-api/** | Go 1.25 + Gin + GORM + MySQL | 独立后端 API 服务,路径与现网一致 `/api/*` |
| **soul-admin/** | React 18 + Vite 6 + TypeScript + Tailwind 4 + Radix UI | 管理后台 SPA请求通过 `VITE_API_BASE_URL` 对接 soul-api 或 Next |
| **miniprogram/** | 微信小程序原生 | C 端主阵地,用户阅读、购买、分销、提现等 |
| **next-project/** | Next.js可选保留 | 原单体:含 `app/view/` C 端、`app/admin/` 管理端、`app/api/`;可作备用或逐步下线 |
### 3.2 接口与前端对应关系
- **API 服务**:由 **soul-api**Go提供端口默认 8080路径与现网完全一致`/api/user/profile``/api/admin/withdrawals`)。
- **管理端****soul-admin** 独立部署,环境变量 `VITE_API_BASE_URL` 指向 soul-api 或 Next 的 API 基地址。
- **小程序**:通过 `app.request()` 等封装请求 APIbaseUrl 可配置,与 soul-api 对接。
- **API 字段规范**:对外请求/响应**统一使用小写开头驼峰camelCase**,如 `userId``referralCode``createdAt`;数据库列名仍为 snake_case仅在服务端内部使用。
## 4. 目录导航(别迷路)
- **[1、需求](1、需求/业务需求.md)**:我们要干啥,成本多少,技术要求;[TDD 需求方案](1、需求/TDD_创业派对项目方案_v1.0.md)。
- **[2、架构](2、架构/系统架构.md)**:整体怎么搭,前后端怎么分。
- [技术选型与全景图](2、架构/技术选型与全景图.md)、[前后端架构分离策略](2、架构/前后端架构分离策略.md)
- **[3、原型](3、原型/原型设计规范.md)**:原型设计规范。
- **[4、前端](4、前端/前端架构.md)**:前端架构(含 **soul-admin**、小程序)、模块详解、开发规范;[当前小程序开发细则](4、前端/当前小程序开发细则.md)[ui 子目录](4、前端/ui/)项目概述、页面功能、组件清单、API/状态/分销/支付/管理后台/部署说明等。
- **[5、接口](5、接口/API接口.md)**:前后端怎么说话。
- **[6、后端](6、后端/后端架构.md)****soul-api**Go + Gin + GORM架构与业务模块后端开发规范。
- **[7、数据库](7、数据库/数据库设计.md)**:数据存哪,怎么存。
- **[8、部署](8、部署/部署总览.md)**:怎么上线、本地运行、宝塔部署、新分销部署、修复与优化记录等。
- **[9、手册](9、手册/写作与结构维护手册.md)**:怎么写书,怎么维护。
- **[10、项目管理](10、项目管理/项目落地推进表.md)**:项目推进与提示词。
## 5. 开发约束(重要)
> **2026-02-07 更新前后端已分离soul-api + soul-admin 为主力**
### 5.1 项目与开发策略
| 项目/端 | 路径/端口 | 开发状态 | 说明 |
|---------|-----------|----------|------|
| **soul-api** | 端口 8080 | ✅ 主力后端 | Go + Gin提供全部 `/api/*` 接口MySQL + GORM |
| **soul-admin** | 独立 SPA | ✅ 主力管理端 | React + Vite通过 `VITE_API_BASE_URL` 对接 soul-api |
| **微信小程序** | `miniprogram/` | ✅ 主力 C 端 | 所有面向用户的新功能在此开发 |
| **Next.js** | `next-project/` 或原 app | 🔒 备用/冻结 | C 端 `app/view/` 冻结;`app/api/` 可作过渡或下线 |
### 5.2 核心原则
1. **后端统一走 soul-api**:新接口在 soul-api 实现,路径与现网一致,响应字段统一 **camelCase**
2. **管理端统一走 soul-admin**:新管理功能在 soul-admin 开发,请求体与展示字段统一 **camelCase**
3. **小程序优先**C 端新功能只在小程序开发,请求/响应已按 camelCase 对接。
4. **API 契约统一**:查询、新增、编辑、删除的请求/响应字段全部小写开头驼峰(如 `userId``createdAt``referralCode`)。
### 5.3 登录与鉴权
| 端 | 登录方式 | 说明 |
|---|----------|------|
| 小程序 | 微信一键登录 / 手机号快速授权 | 与 soul-api 或 Next 的 `/api/miniprogram/login` 等对接 |
| soul-admin | 手机号 + 密码 | POST `/api/admin` 登录GET 鉴权Cookie/Token |
| 账号统一 | 以手机号/用户 ID 为唯一标识 | 多端数据互通 |
### 5.4 为什么前后端分离?
- **独立部署与扩展**:后端 Go 可单独扩容,前端静态资源可 CDN。
- **多端复用 API**小程序、soul-admin、未来 App 共用同一套 soul-api。
- **开发效率**前后端并行、接口契约清晰camelCase 统一)。
## 6. 这里的规矩
- **行动至上**:文档是用来指导干活的,不是写来看的。
- **数据说话**:所有优化要有数据支撑,加载快了多少?转化高了多少?
- **保持简单**:能用现成的库就别自己造轮子。
---
**复盘:**
项目已完成前后端分离soul-apiGo负责接口与数据库soul-adminReact负责管理后台miniprogram 负责 C 端。API 与前端字段已统一为 camelCase便于多端对接与后续扩展。

BIN
开发文档/1、需求/.DS_Store vendored Normal file

Binary file not shown.

BIN
开发文档/1、需求/修改/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -0,0 +1,66 @@
# 前后端分离开发架构
**我是卡若。**
当前项目已完成**前后端物理分离**:后端 soul-apiGo管理端 soul-adminReactC 端小程序。开发流程仍采用**接口契约驱动**。
## 1. 当前架构(已落地)
\`\`\`mermaid
graph LR
subgraph "前端"
Admin[soul-admin<br/>React+Vite]
Mini[微信小程序]
end
subgraph "后端 soul-api (Go)"
Gin[Gin Router /api/*]
Handler[Handler 层]
GORM[GORM]
Gin --> Handler --> GORM
end
MySQL[(MySQL)]
GORM --> MySQL
Admin -->|VITE_API_BASE_URL + path| Gin
Mini -->|baseUrl + path| Gin
\`\`\`
## 2. 分离开发流程
\`\`\`mermaid
sequenceDiagram
participant Doc as API 文档
participant FE as 前端 (soul-admin / 小程序)
participant BE as 后端 (soul-api)
Doc->>FE: 1. 接口定义 (URL、camelCase 字段)
Doc->>BE: 1. 同上
par 并行开发
FE->>FE: 2. Mock 或对接已有 API
FE->>FE: 3. UI 与交互,字段用 camelCase
BE->>BE: 2. Handler + Modeljson 标签 camelCase
BE->>BE: 3. 单元测试 / 联调
end
FE->>BE: 4. 联调path 一致body/response camelCase
BE-->>FE: 返回 camelCase JSON
\`\`\`
## 3. 数据流与规范
- **请求**:前端只发 camelCase`userId``referralCode`。soul-api 的 Go 结构体用 `json:"userId"` 等接收。
- **响应**soul-api 通过 GORM 模型 `json:"userId"` 等输出 camelCase前端 TypeScript 类型与接口字段一致camelCase
- **数据库**:表名列名保持 snake_case`user_id``created_at`),仅在 soul-api 内部使用,不暴露给前端。
## 4. 落地执行规范
1. **接口先行**:新增/修改接口先在文档约定 URL、方法、请求/响应体(字段 camelCase
2. **统一封装**soul-admin 所有请求走 `src/api/client.ts`get/post/put/delpath 与现网一致;小程序走 `app.request()`,不写死域名。
3. **字段统一**:禁止在 API 响应或前端类型中使用 snake_case 对外字段;表单提交、列表展示一律 camelCase。
---
**卡若说:**
按这个流程走,前后端对接清晰。路径一致、字段 camelCase 统一,多端复用同一套 soul-api 即可。

View File

@@ -0,0 +1,78 @@
# 系统架构
**我是卡若。**
架构不是为了画图好看,是为了**省事**和**赚钱**。
我们的架构设计,核心围绕两个字:**分离**。
- 内容和代码分离。
- 前端和后端分离已落地soul-api + soul-admin + 小程序)。
- 静态和动态分离。
## 1. 架构全景图(当前)
\`\`\`mermaid
graph TD
subgraph "内容生产 (Content)"
Typora[本地写作] --> Git[Git 仓库]
Git --> AutoSync[自动同步]
end
subgraph "后端 (soul-api - Go)"
API[Gin API /api/*]
API --> GORM[GORM]
GORM --> MySQL[(MySQL)]
AutoSync --> FileSys[文件/配置]
FileSys --> API
end
subgraph "前端"
Admin[soul-admin React+Vite]
Mini[微信小程序]
Admin --> API
Mini --> API
end
subgraph "用户触达"
Mini --> Wechat[微信]
Admin --> Browser[浏览器]
end
\`\`\`
## 2. 当前项目分工
| 层级 | 项目 | 技术 | 说明 |
|------|------|------|------|
| **后端** | soul-api | Go 1.25 + Gin + GORM + MySQL | 提供全部 `/api/*`,路径与现网一致,响应 camelCase |
| **管理端** | soul-admin | React 18 + Vite 6 + TS + Tailwind + Radix UI | SPA`VITE_API_BASE_URL` 指向 soul-api |
| **C 端** | miniprogram | 微信小程序原生 | 阅读、购买、分销、提现,请求/响应 camelCase |
| **备用** | next-project | Next.js | 原单体,可保留 app/api 过渡或逐步下线 |
## 3. 核心设计理念
### 3.1 “内容即产品”
核心资产是内容(书籍章节、配置)。文章用 Markdown/数据库存Git 或脚本同步,安全可控。
### 3.2 前后端已分离
- **后端**soul-apiGo独立部署可单独扩容、多端复用。
- **管理端**soul-admin 纯静态 SPA部署到任意静态托管或 CDN。
- **接口契约**:路径不变,请求/响应字段统一 **camelCase**,详见 [前后端架构分离策略](前后端架构分离策略.md)。
### 3.3 极简部署
- 后端Go 二进制 + 环境变量,或 Docker 单容器。
- 管理端:`npm run build` 后部署到 Nginx/Vercel/OSS。
- 小程序:微信后台发布。宝塔/Webhook 可按需做自动拉代码。
## 4. 关键约束
1. **API 字段统一**:对外一律 camelCase`userId``createdAt`);数据库列名保持 snake_case 仅在服务端使用。
2. **本地优先**:写作与开发在本地,通过 soul-api 连接数据库。
3. **单向流动**:数据流向 `内容/配置 -> API -> 前端`;订单、用户等由 API 写库。
## 5. 详细技术栈
详见:**[技术选型与全景图](技术选型与全景图.md)**、**[前后端架构分离策略](前后端架构分离策略.md)**。
---
**总结:**
当前已实现前后端分离soul-api 负责接口与数据soul-admin 负责管理后台,小程序负责 C 端。保持接口路径与字段规范统一,便于多端对接与扩展。

View File

@@ -0,0 +1,52 @@
# 前端开发规范 (Frontend Specs) - 智能自生长文档
> **提示词功能 (Prompt Function)**: 将本文件拖入 AI 对话框,即可激活“前端技术专家”角色,生成符合项目规范的代码。
## 1. 基础上下文
### 1.1 角色档案:卡若 (Karuo)
- **管理端 (soul-admin)**:深色后台风格,稳、快、信息密度合理。
- **C 端 (小程序)**:移动端优先,阅读与转化体验顺畅。
### 1.2 技术栈(当前)
| 项目 | 技术栈 |
|------|--------|
| **soul-admin** | React 18 + Vite 6 + TypeScript + Tailwind 4 + Radix UI类 Shadcn+ React Router 6 |
| **miniprogram** | 微信小程序原生 (JS/WXML/WXSS) |
## 2. 开发规范核心
### 2.1 API 与字段规范(强制)
- **请求/响应字段**:一律**小写开头驼峰camelCase**。
- 正确:`userId``referralCode``createdAt``hasFullBook``pendingEarnings`
- 错误:`user_id``referral_code``created_at`(仅数据库内部使用,不暴露给前端)。
- **类型定义**TypeScript 接口与 API 约定一致,全部 camelCase。
- **表单提交**:提交 body 的字段名使用 camelCase`isAdmin``hasFullBook`)。
### 2.2 soul-admin 规范
- **请求**:所有接口请求通过 `src/api/client.ts` 的 get/post/put/delpath 与现网一致,不写死域名。
- **环境变量**`VITE_API_BASE_URL` 指向 soul-api 或现有 API 基地址(开发/生产分开配置)。
- **目录**:页面在 `src/pages/`,通用 UI 在 `src/components/ui/`,业务模块在 `src/components/modules/`
- **样式**Tailwind 为主,深色主题可用 `#0f2137``#38bdac` 等品牌色。
### 2.3 小程序规范
- **请求**:通过 `app.request()` 等封装baseUrl 可配置请求体与后端约定一致camelCase
- **存储**:需要与后端一致的标识(如 `referral_code` 存为业务值可保留 key 名)时,注意与接口字段区分;接口层面仍用 camelCase。
### 2.4 交互与性能
- **加载**:列表/详情需 loading 或骨架屏,避免白屏。
- **错误**接口失败需有明确提示Toast/Alert
- **图片**:懒加载 + 失败占位。
## 3. AI 协作指令
**角色**前端主程soul-admin + 小程序)。
**任务**
1. **代码生成**React 组件带 Tailwind 类名;类型与 API 字段 camelCase 一致。
2. **接口对接**:使用统一 clientget/post/put/del不直接写死 fetch 域名;请求体/响应类型为 camelCase。
3. **结构分析**:复杂页面可用 Mermaid 展示组件或数据流依赖。

View File

@@ -0,0 +1,75 @@
# 前端架构
**我是卡若。**
前端分两块:**管理端 soul-admin**(给运营/自己用)、**C 端 miniprogram**(给用户用)。目标都是稳、快、体验好。
## 1. 当前前端分工
| 项目 | 技术栈 | 说明 |
|------|--------|------|
| **soul-admin** | React 18 + Vite 6 + TypeScript + Tailwind 4 + Radix UI (Shadcn 风格) | 管理后台 SPA对接 soul-api路由与现网 /admin/* 对应 |
| **miniprogram** | 微信小程序原生 (JS/WXML/WXSS) | C 端主阵地:阅读、购买、分销、提现等 |
(原 Next.js 的 `app/view/``app/admin/` 可保留为备用或逐步下线。)
## 2. soul-admin 技术底座
- **框架**: React 18 + Vite 6构建快、热更新稳。
- **语言**: TypeScript类型与 API 字段一致camelCase
- **样式**: Tailwind CSS 4全局与组件级样式。
- **UI**: Radix UI 系Dialog、Tabs、Switch 等),类 Shadcn 用法,深色管理后台风格。
- **路由**: React Router 6路径与现网管理端一致`/dashboard``/users``/withdrawals`)。
- **请求**: 统一走 `src/api/client.ts`get/post/put/delbaseUrl 为 `VITE_API_BASE_URL`path 与 soul-api 一致;**请求体与响应字段一律 camelCase**。
## 3. soul-admin 目录结构
\`\`\`
soul-admin/src/
├── api/
│ └── client.ts # 统一请求封装baseUrl + path
├── components/
│ ├── ui/ # 通用 UI (Button, Card, Dialog, Table...)
│ └── modules/ # 业务模块 (如 UserDetailModal)
├── layouts/
│ └── AdminLayout.tsx # 管理端布局与侧栏
├── pages/ # 页面(与路由一一对应)
│ ├── login/
│ ├── dashboard/
│ ├── users/
│ ├── orders/
│ ├── distribution/
│ ├── withdrawals/
│ ├── content/
│ ├── chapters/
│ ├── settings/
│ ├── referral-settings/
│ ├── payment/
│ ├── match/
│ └── ...
├── lib/
│ └── utils.ts
├── App.tsx
└── main.tsx
\`\`\`
## 4. 小程序 (miniprogram)
- **技术**: 微信原生,`app.js` 全局、`app.request()` 封装请求baseUrl 可配置指向 soul-api。
- **页面**: 首页、阅读、个人中心、分销、提现等;与 soul-api 接口对接,**请求/响应字段已统一为 camelCase**(如 `userId``referralCode``createdAt`)。
- **登录**: 微信登录 + 手机号授权,与后端 `/api/miniprogram/login` 等对接。
## 5. API 与数据规范
- **路径**: 与现网完全一致(如 `/api/user/profile``/api/admin/withdrawals`),由 soul-api 提供。
- **字段**: 所有请求体、响应体、前端类型定义**统一小写开头驼峰camelCase**`userId``createdAt``referralCode``hasFullBook` 等。禁止对外使用 snake_case。
- **类型**: TypeScript 接口与 API 响应一一对应,便于联调与维护。
## 6. 交互与体验(通用)
- **加载**: 列表/详情加载时使用骨架屏或明确 loading 状态,避免白屏。
- **错误**: 接口失败要有提示Toast/Alert必要处可重试。
- **表单**: 提交字段与后端约定一致camelCase校验与错误信息清晰。
---
**总结**:管理端在 soul-adminReact+ViteC 端在小程序;两者都通过统一 API 封装对接 soul-api字段规范统一为 camelCase。

View File

Before

Width:  |  Height:  |  Size: 125 KiB

After

Width:  |  Height:  |  Size: 125 KiB

View File

@@ -0,0 +1,54 @@
# 后端开发规范 (Backend Specs) - 智能自生长文档
> **提示词功能 (Prompt Function)**: 将本文件拖入 AI 对话框即可激活“Go 后端专家”角色,生成符合项目规范的 soul-api 代码。
## 1. 基础上下文
### 1.1 角色档案:卡若 (Karuo)
- **核心**:接口稳、性能好、与现网路径和契约一致。
- **习惯**:请求/响应统一 camelCase数据库列名 snake_case 仅内部使用。
### 1.2 技术栈(当前)
- **语言**Go 1.25+。
- **框架**Gin (HTTP)GORM (ORM)。
- **数据**MySQL。
- **配置**环境变量godotenv.env 不提交。
## 2. 开发规范核心
### 2.1 项目结构 (soul-api)
- **handler**:按业务拆分文件(如 `user.go``order.go``admin_withdrawals.go`),每个 handler 对应现网 API 路径与行为。
- **model**GORM 模型,表名与列名 snake_case**JSON 标签必须 camelCase**(如 `json:"userId"``json:"createdAt"`)。
- **router**:在 `router.go` 中集中注册,路径与现网 `/api/*` 一致。
- **middleware**CORS、AdminAuth、限流、安全头等。
### 2.2 接口与字段规范(强制)
- **路径**:与现网完全一致,例如 `GET /api/user/profile``PUT /api/admin/withdrawals`
- **请求体**Go 结构体 `json` 标签使用 camelCase例如
- `UserId string \`json:"userId"\``
- `ReferralCode string \`json:"referralCode"\``
- `CreatedAt time.Time \`json:"createdAt"\``
- **响应**:通过 GORM 模型或 `gin.H` 返回时,键名一律 camelCase禁止对外返回 `user_id``created_at` 等 snake_case。
- **数据库**:表/列名保持 snake_case仅在 GORM 与 SQL 中使用。
### 2.3 安全与错误
- **SQL**:一律使用 GORM 或参数化查询,禁止拼接 SQL。
- **鉴权**:管理端接口使用 `middleware.AdminAuth()`,未登录返回 401 或统一错误体。
- **错误响应**:统一格式如 `gin.H{"success": false, "error": "错误说明"}`
### 2.4 配置与依赖
- **配置**:从环境变量读取(如 `DB_HOST``PORT``CORS_ORIGINS`),参考 `.env.example`
- **依赖**`go mod tidy`,提交前确保 go.mod/go.sum 已更新。
## 3. AI 协作指令
**角色**Go 后端架构师soul-api
**任务**
1. **代码实现**:新增或修改 handler/model/router路径与现网一致请求/响应字段 camelCase。
2. **模型定义**GORM 的 `gorm` 标签用 snake_case`json` 标签用 camelCase。
3. **逻辑图解**:复杂流程可用 Mermaid 展示调用关系或数据流。

View File

@@ -0,0 +1,69 @@
# 后端架构与业务逻辑
**我是卡若。**
当前后端为独立项目 **soul-api**Go + Gin + GORM提供全部 `/api/*` 接口,与 soul-admin、小程序对接。
## 1. 技术栈(当前)
| 项目 | 技术 | 说明 |
|------|------|------|
| **soul-api** | Go 1.25 + Gin + GORM + MySQL | 独立 API 服务,路径与现网一致,响应 camelCase |
## 2. soul-api 目录结构
\`\`\`
soul-api/
├── cmd/
│ └── server/
│ └── main.go # 入口
├── internal/
│ ├── config/ # 配置(环境变量)
│ ├── database/ # 数据库连接 (GORM)
│ ├── handler/ # API 处理函数(按业务拆分)
│ │ ├── admin.go, admin_chapters.go, admin_withdrawals.go ...
│ │ ├── auth.go, user.go, book.go, orders.go
│ │ ├── withdraw.go, referral.go, distribution.go
│ │ ├── config.go, db.go, miniprogram.go ...
│ ├── middleware/ # 中间件CORS、鉴权、限流、安全头
│ ├── model/ # 数据模型GORM + json 标签 camelCase
│ │ ├── user.go, order.go, chapter.go, withdrawal.go ...
│ └── router/
│ └── router.go # 路由注册,路径与现网 /api/* 一致
├── .env, .env.example
├── go.mod, go.sum
└── Makefile
\`\`\`
## 3. 核心业务模块(在 handler 中实现)
- **鉴权**`/api/admin` 登录与鉴权,管理端 Cookie/Token小程序走 `/api/miniprogram/login` 等。
- **用户**`/api/user/profile``/api/user/update``/api/user/track``/api/user/purchase-status` 等。
- **书籍/章节**`/api/book/*`(目录、章节、搜索、统计)。
- **订单与支付**`/api/orders``/api/miniprogram/pay`、支付回调等。
- **分销与提现**`/api/referral/*``/api/distribution/*``/api/withdraw/*``/api/admin/withdrawals`
- **配置与内容**`/api/config``/api/content``/api/db/config``/api/db/book` 等。
- **管理端**`/api/admin/*`(章节、分销概览、提现审核等)、`/api/db/*`(用户、配置、初始化等)。
## 4. 接口与字段规范
- **路径**:与现网完全一致(如 `GET /api/user/profile``PUT /api/admin/withdrawals`),便于前端只改 baseUrl 切换后端。
- **请求/响应字段**:一律**小写开头驼峰camelCase**。
- Go 结构体:`json:"userId"``json:"createdAt"``json:"referralCode"`
- 数据库列名:保持 snake_case`user_id``created_at`),仅在 GORM 与 SQL 中使用,不暴露给前端。
- **统一响应**:成功可返回 `gin.H{"success": true, "data": ...}`;失败 `gin.H{"success": false, "error": "..."}`
## 5. 数据库与配置
- **数据库**MySQLGORM 连接;表结构可与原 Next/Prisma 保持一致,便于迁移。
- **配置**:环境变量(.env`DB_*``PORT``CORS_ORIGINS``JWT_SECRET` 等;敏感信息不提交仓库。
## 6. 扩展性
- **鉴权**:管理端已用 `middleware.AdminAuth()` 保护 `/api/admin/*``/api/db/*`
- **CORS**:已配置 AllowOrigins/AllowCredentials支持 soul-admin 与小程序跨域。
- **限流**:可按需在 middleware 中增加 rate limit。
---
**卡若说:**
soul-api 是唯一对外后端,功能明确、路径与字段规范统一,便于多端复用与维护。

View File

Before

Width:  |  Height:  |  Size: 183 KiB

After

Width:  |  Height:  |  Size: 183 KiB

Some files were not shown because too many files have changed in this diff Show More