From c7f43e30a94ae4b3440403ba674fb997598aeb76 Mon Sep 17 00:00:00 2001 From: tianmo Date: Wed, 30 Sep 2026 11:02:58 +0800 Subject: [PATCH] =?UTF-8?q?=E8=AE=A2=E5=8D=95=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + deploy/README.md | 21 +- deploy/deploy.config.example.sh | 52 +++++ deploy/deploy.sh | 193 ++++++++++++++++++ docs/部署指南.md | 5 + yelaymaths_api/.env.example | 5 + yelaymaths_api/src/common/wechat-msg.test.ts | 38 ++++ yelaymaths_api/src/common/wechat-msg.ts | 42 ++++ yelaymaths_api/src/config/config.ts | 8 + .../modules/payments/payments.controller.ts | 69 ++++++- .../src/modules/payments/payments.service.ts | 77 +++++-- 11 files changed, 492 insertions(+), 19 deletions(-) create mode 100644 deploy/deploy.config.example.sh create mode 100755 deploy/deploy.sh create mode 100644 yelaymaths_api/src/common/wechat-msg.test.ts create mode 100644 yelaymaths_api/src/common/wechat-msg.ts diff --git a/.gitignore b/.gitignore index 0b76ce9..f4c5b59 100644 --- a/.gitignore +++ b/.gitignore @@ -35,6 +35,7 @@ pnpm-debug.log* # 部署脚本的本地配置(含服务器地址,禁止入库) deploy/deploy.config.ps1 +deploy/deploy.config.sh # 小程序构建产物 yelaymaths/miniprogram_npm/ diff --git a/deploy/README.md b/deploy/README.md index b48bfd5..af5865c 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -4,13 +4,32 @@ 服务器上**不需要安装任何东西**,它复用的就是你已经能用的 SSH。 +## 用哪个脚本 + +| 你的系统 | 脚本 | 配置文件 | +|---|---|---| +| Windows | `deploy.ps1` | `deploy.config.ps1`(照抄 `deploy.config.example.ps1`) | +| macOS / Linux | `deploy.sh` | `deploy.config.sh`(照抄 `deploy.config.example.sh`) | + +Mac 上先执行一次 `chmod +x deploy/deploy.sh`,之后用法和 Windows 版完全一样: + +```bash +./deploy/deploy.sh api +./deploy/deploy.sh admin +./deploy/deploy.sh +./deploy/deploy.sh api --dry-run # 只打包、只显示会上传哪些文件,不真传 +./deploy/deploy.sh api --with-install # 顺带装依赖(只有改了 package.json 才需要) +``` + +> 第一次用先跑 `--dry-run`,它会打印打包大小和文件清单,核对无误再去掉这个参数。 + --- ## 一次性配置 ### 1. 填配置文件 -编辑 `deploy/deploy.config.ps1`,需要改这几项: +编辑 `deploy/deploy.config.ps1`(Mac 上是 `deploy/deploy.config.sh`,字段名多一层下划线,含义完全一样),需要改这几项: | 配置项 | 填什么 | 怎么查 | |---|---|---| diff --git a/deploy/deploy.config.example.sh b/deploy/deploy.config.example.sh new file mode 100644 index 0000000..24595a8 --- /dev/null +++ b/deploy/deploy.config.example.sh @@ -0,0 +1,52 @@ +# 复制本文件为 deploy.config.sh 并填写自己的服务器信息。 +# deploy.config.sh 已 gitignore,服务器地址不会进仓库。 + +# ---- SSH ---- +DEPLOY_HOST='1.2.3.4' # 服务器 IP 或域名 +DEPLOY_PORT=22 +DEPLOY_USER='root' + +# 服务器上 Node 的 bin 目录(远端构建时会加进 PATH)。 +# 宝塔 → 软件商店 → Node 版本管理器里看,例如 /www/server/nodejs/v22.14.0/bin +NODE_BIN='/www/server/nodejs/v22/bin' + +# ---- Nginx 反代缓存 ---- +# 宝塔在 /www/server/nginx/conf/proxy.conf 里全局开了 proxy_cache cache_one, +# 而 Next.js 给页面的响应头是 Cache-Control: s-maxage=31536000(允许缓存一年)。 +# 两者一叠加,新构建传上去也访问不到,而旧 HTML 引用的分包早被删掉,页面会整块消失。 +# 部署脚本每次清一次这个目录并 reload。留空则跳过。 +NGINX_CACHE_DIR='/www/server/nginx/proxy_cache_dir' +NGINX_BIN='/www/server/nginx/sbin/nginx' + +# ---- yelaymaths_api ---- +API_LABEL='yelaymaths_api (server API)' +API_LOCAL='yelaymaths_api' +API_REMOTE='/www/wwwroot/yelaymaths/yelaymaths_api' +API_PATHS='src prisma tools package.json pnpm-lock.yaml pnpm-workspace.yaml tsconfig.json tsconfig.build.json nest-cli.json .env.example' +API_EXCLUDES='' +# 部署前先在服务器上删掉的旧目录(留空表示不删) +API_CLEAN='' +API_LOCAL_BUILD='' # 想在本机构建就填 'pnpm build',一般留空 +API_INSTALL='pnpm install --frozen-lockfile' +API_BUILD='pnpm build' +# 重启命令:杀掉旧进程。服务器上执行 pm2 list 看真实进程名。 +API_RESTART='pm2 restart yelaymaths-api' +# 拉起命令:宝塔不会自动把进程拉回来,所以自己 setsid + nohup 起。留空则只杀不拉。 +API_START='setsid nohup node dist/main.js >> /www/wwwlogs/yelaymaths-api.out.log 2>&1 < /dev/null &' +API_HEALTH='http://127.0.0.1:3002/healthz' + +# ---- yelaymaths_admin ---- +# 注意:NEXT_PUBLIC_* 是构建时写死的,.env.local 必须在服务器上先准备好(脚本不会上传它)。 +ADMIN_LABEL='yelaymaths_admin (admin console)' +ADMIN_LOCAL='yelaymaths_admin' +ADMIN_REMOTE='/www/wwwroot/yelaymaths/yelaymaths_admin' +ADMIN_PATHS='src package.json pnpm-lock.yaml tsconfig.json next.config.ts postcss.config.mjs' +ADMIN_EXCLUDES='' +# Next 的预渲染缓存在 .next/cache 里,命中就不读磁盘新文件,所以每次先删掉重建 +ADMIN_CLEAN='.next/cache' +ADMIN_LOCAL_BUILD='' +ADMIN_INSTALL='pnpm install --frozen-lockfile' +ADMIN_BUILD='pnpm build' +ADMIN_RESTART='pm2 restart yelaymaths-admin' +ADMIN_START='setsid nohup node node_modules/next/dist/bin/next start -p 3001 >> /www/wwwlogs/yelaymaths-admin.out.log 2>&1 < /dev/null &' +ADMIN_HEALTH='http://127.0.0.1:3001/' diff --git a/deploy/deploy.sh b/deploy/deploy.sh new file mode 100755 index 0000000..3334e6f --- /dev/null +++ b/deploy/deploy.sh @@ -0,0 +1,193 @@ +#!/usr/bin/env bash +# +# Mac / Linux 版一键部署,行为与 deploy.ps1 一致。 +# +# ./deploy/deploy.sh api 只部署服务端 +# ./deploy/deploy.sh admin 只部署管理端 +# ./deploy/deploy.sh 两个都部署 +# ./deploy/deploy.sh api --dry-run 只打包、只显示会上传什么,不真传 +# ./deploy/deploy.sh api --with-install 顺带装依赖(只有改了 package.json 才需要) +# +# 用白名单打包,.env / node_modules / dist / .next 结构上不可能被传上去。 + +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +repo_root="$(dirname "$script_dir")" +config_file="$script_dir/deploy.config.sh" + +target='all' +dry_run=0 +with_install=0 + +# bash 3.2(macOS 自带)在 set -u 下对无参数的 "$@" 会报 unbound variable,所以要加这层保护 +for arg in ${@+"$@"}; do + case "$arg" in + api|admin|all) target="$arg" ;; + -n|--dry-run) dry_run=1 ;; + --with-install) with_install=1 ;; + -h|--help) sed -n '2,13p' "$0"; exit 0 ;; + # ${arg} 必须带花括号:紧跟中文时 bash 会把多字节字符合进变量名 + *) echo "未知参数:${arg}(可用:api | admin | all | --dry-run | --with-install)" >&2; exit 2 ;; + esac +done + +if [ ! -f "$config_file" ]; then + echo 'ERROR: 找不到 deploy.config.sh' >&2 + echo '把 deploy/deploy.config.example.sh 复制成 deploy/deploy.config.sh,填好服务器信息再跑。' >&2 + exit 1 +fi + +# shellcheck disable=SC1090 +. "$config_file" + +for cmd in ssh scp tar; do + command -v "$cmd" >/dev/null 2>&1 || { echo "缺少命令:$cmd" >&2; exit 1; } +done + +ssh_target="$DEPLOY_USER@$DEPLOY_HOST" + +# 读配置里的某一项,如 val API_LABEL +val() { + local out + eval "out=\${$1:-}" + printf '%s' "$out" +} + +deploy_app() { + local key="$1" + local up label local_path remote_path paths excludes clean local_build + local install build restart start health nginx_cache nginx_bin + up="$(printf '%s' "$key" | tr '[:lower:]' '[:upper:]')" + + label="$(val "${up}_LABEL")" + local_path="$repo_root/$(val "${up}_LOCAL")" + remote_path="$(val "${up}_REMOTE")" + paths="$(val "${up}_PATHS")" + excludes="$(val "${up}_EXCLUDES")" + clean="$(val "${up}_CLEAN")" + local_build="$(val "${up}_LOCAL_BUILD")" + install="$(val "${up}_INSTALL")" + build="$(val "${up}_BUILD")" + restart="$(val "${up}_RESTART")" + start="$(val "${up}_START")" + health="$(val "${up}_HEALTH")" + + [ -d "$local_path" ] || { echo "本地目录不存在:$local_path" >&2; exit 1; } + + echo '' + echo "=== $label ===" + + # ---- 本地构建(默认关,配了 $X_LOCAL_BUILD 才跑)---- + if [ -n "$local_build" ]; then + echo "[1/5] Building locally: $local_build" + # 国内拉 Prisma 引擎很慢,指向镜像 + ( cd "$local_path" && PRISMA_ENGINES_MIRROR='https://registry.npmmirror.com/-/binary/prisma' bash -c "$local_build" ) + else + echo '[1/5] Skipping local build' + fi + + # ---- 打包:只收白名单里存在的路径 ---- + echo "[2/5] Packing $(val "${up}_LOCAL")" + local stamp tar_file remote_tar items + stamp="$(date +%Y%m%d-%H%M%S)" + tar_file="${TMPDIR:-/tmp}/yelay-${key}-${stamp}.tar.gz" + remote_tar="/tmp/$(basename "$tar_file")" + + local -a tar_args=() + for p in $excludes; do tar_args+=("--exclude=$p"); done + local -a file_args=() + for p in $paths; do + [ -e "$local_path/$p" ] && file_args+=("$p") + done + [ ${#file_args[@]} -gt 0 ] || { echo "白名单里没有任何存在的路径:$paths" >&2; exit 1; } + + ( cd "$local_path" && tar -czf "$tar_file" ${tar_args[@]+"${tar_args[@]}"} "${file_args[@]}" ) + + local size_kb + size_kb=$(( $(wc -c < "$tar_file") / 1024 )) + echo " ${size_kb} KB ($(printf '%s, ' "${file_args[@]}" | sed 's/, $//'))" + + if [ "$dry_run" = 1 ]; then + echo ' DryRun: nothing uploaded.' + rm -f "$tar_file" + return 0 + fi + + echo "[3/5] Uploading to $remote_path" + scp -P "$DEPLOY_PORT" -q "$tar_file" "$ssh_target:$remote_tar" + + # ---- 拼远端脚本 ---- + local clean_cmd='' + for p in $clean; do clean_cmd="$clean_cmd +rm -rf '$remote_path/$p'"; done + + local install_cmd='' + [ "$with_install" = 1 ] && install_cmd="$install" + + local start_cmd='' + # 先 sleep 2 让刚被杀的进程彻底放开端口,否则新进程会 EADDRINUSE + [ -n "$start" ] && start_cmd="sleep 2 +$start" + + # 探活:30 秒还没起来就让部署直接失败,免得「显示成功、线上 502」 + local health_cmd='' + if [ -n "$health" ]; then + health_cmd="for i in {1..30}; do curl -sf -o /dev/null '$health' && break; sleep 1; done +curl -sf -o /dev/null '$health' || { echo 'ERROR: service did not come up: $health'; exit 1; }" + fi + + nginx_cache="$(val NGINX_CACHE_DIR)" + nginx_bin="$(val NGINX_BIN)" + local nginx_cmd='' + if [ -n "$nginx_cache" ]; then + nginx_cmd="find '$nginx_cache' -mindepth 1 -delete 2>/dev/null || true" + [ -n "$nginx_bin" ] && nginx_cmd="$nginx_cmd +'$nginx_bin' -s reload 2>/dev/null || true" + fi + + # 远端脚本保持 ASCII 注释,避免 SSH 传输编码问题 + local remote_script + remote_script="$(cat <` | +| 消息推送 URL / Token | 开发 → 开发管理 → 消息推送(也是虚拟支付的发货推送地址) | URL 填 `https:///v1/payments/notify`;Token 随便生成一串,**要和 `.env` 里的 `WX_MSG_TOKEN` 一致** | | 发货推送 URL | 虚拟支付 → 基本配置 | `https:///v1/payments/notify` | | 小程序简称 | 虚拟支付 → 基础配置 | iOS 支付必需 | | 苹果 IAP 支付 | 虚拟支付 → 基础配置 | iOS 支付需开启 | | 支付道具 | 虚拟支付 → 道具管理 | 每门课建一个,记下道具 ID 与价格 | +> 消息推送的 URL 一保存,微信就会发一个 `GET` 来验签。服务端要是没配 `WX_MSG_TOKEN` +> 或两边不一致,页面会一直显示「服务器地址验证失败,请检查服务器配置」,发货推送也就配不上。 +> 加解密方式选**明文模式**只需 Token;选**安全模式**则 `WX_MSG_AES_KEY` 也要填上后台那串 EncodingAESKey。 + > `` 与 `