SaaS型小程序自动化发布解决方案
本文档旨在为多租户(SaaS)平台提供一套完整的小程序自动化发布解决方案。通过本方案,平台可以为旗下成百上千的商户提供“一键发布”小程序的能力,极大提升效率和降低人工操作风险。
1. 核心理念与通用架构
1.1. 核心理念
代码模板化 (Code Templating): 维护一套核心的小程序代码库。这套代码是通用的、不包含任何商户特定信息的。所有商户的业务逻辑、UI组件等都在这个模板中。
配置中心化 (Centralized Configuration): 建立一个配置管理系统(可以是数据库表、独立的微服务或配置中心如Nacos/Apollo),用来存储每个商户的个性化信息。
CI/CD 自动化 (Automation via CI/CD): 利用持续集成/持续部署(CI/CD)工具(如 Jenkins, GitLab CI, GitHub Actions)来驱动整个编译、配置注入、上传的流程。
1.2. 架构图
2. 核心技术:miniprogram-ci
这是微信官方提供的关键工具,让小程序工程化成为可能。它是一个NPM包,可以在服务器命令行环境中使用。
主要功能:
上传 (Upload): 将代码包上传到微信后台,成为体验版。
预览 (Preview): 生成一个预览二维码,可以扫码体验。
构建 NPM: 运行 npm install 并构建。
其他: 支持代理、获取最近上传信息等。
关键准备工作:
小程序代码上传密钥: 在微信公众平台 -> 开发管理 -> 开发设置 -> “小程序代码上传”中,生成并下载密钥文件(private.appid.key)。这个密钥非常重要,需要妥善保管在服务器上。
IP白名单: 将CI/CD服务器的公网IP地址添加到微信公众平台的IP白名单中。
3. 实施方案
以下介绍两种主流的自动化实施方案。
3.1. 方案一:通用CI/CD流程 (以GitLab CI为例)
此方案适用于使用标准CI/CD工具(如GitLab, Jenkins, GitHub Actions)的团队。
3.1.1. 准备工作:小程序模板代码改造
将所有商户相关的信息都用占位符或者变量代替。
project.config.json: appid 必须是动态的。
{ "appid": "%APP_ID%", "projectname": "%PROJECT_NAME%", ... }
app.json: 页面标题、tabBar等可能也需要定制。
创建一个配置文件 (如 src/config.js): 用于存放API地址、租户ID、主题色等。
// src/config.js export default { apiBaseUrl: '%API_BASE_URL%', tenantId: '%TENANT_ID%', themeColor: '%THEME_COLOR%' };
在代码中引用这个配置文件。
3.1.2. 准备工作:建立商户配置中心
在你的数据库中创建一个
tenants_mp_config
表。字段包括:
tenant_id
,mp_appid
,mp_app_name
,api_base_url
,theme_color
,version
(当前发布版本),status
(发布状态:未发布、上传中、上传成功、发布失败)等。商户在后台填写自己的小程序AppID和AppSecret,并授权给你。
3.1.3. 搭建CI/CD自动化流程 (GitLab CI示例)
在CI/CD服务器上安装环境:
Node.js
npm install -g miniprogram-ci
编写自动化脚本 (
.gitlab-ci.yml
)stages: - build_and_upload # 定义一个模板,所有商户的发布都用这个模板 .build_template: &build_template stage: build_and_upload image: node:16 # 使用一个包含Node.js的Docker镜像 before_script: - npm install -g miniprogram-ci # 安装工具 script: - echo "开始为租户 ${TENANT_ID} 构建小程序..." # 1. 从API获取租户配置 (假设你有一个内部API) # - apt-get install -y curl jq # - CONFIG_JSON=$(curl -s "https://your-saas-api.com/tenants/${TENANT_ID}/mp-config" -H "Authorization: Bearer ${API_TOKEN}") # - APP_ID=$(echo $CONFIG_JSON | jq -r '.appid') # - PROJECT_NAME=$(echo $CONFIG_JSON | jq -r '.project_name') # - API_URL=$(echo $CONFIG_JSON | jq -r '.api_url') # 为了演示,这里直接使用CI/CD变量 - echo "AppID: ${MP_APPID}" - echo "Project Name: ${MP_PROJECT_NAME}" # 2. 注入配置 # - 使用 sed 或一个简单的node脚本来替换文件中的占位符 - sed -i "s/%APP_ID%/${MP_APPID}/g" project.config.json - sed -i "s/%PROJECT_NAME%/${MP_PROJECT_NAME}/g" project.config.json - sed -i "s|%API_BASE_URL%|${API_URL}|g" src/config.js # 注意URL中斜杠的处理 # 3. 安装依赖并构建 - npm install - npm run build # 如果有编译步骤(如Taro/Uni-app) # 4. 调用 miniprogram-ci 上传 # - 将私钥文件(private.key)作为安全的CI/CD变量存储,运行时写入文件 - echo "${MP_PRIVATE_KEY}" > ./private.${MP_APPID}.key - > miniprogram-ci upload --pp ./dist # 小程序代码目录 --appid ${MP_APPID} --pkp ./private.${MP_APPID}.key # 私钥路径 --ver 1.0.0-${CI_PIPELINE_IID} # 版本号,建议带上构建ID --desc "自动构建于 ${CI_COMMIT_SHORT_SHA}" --enable-es6 # 按需添加编译选项 -r 1 # 机器人1号(1-30) after_script: # 清理私钥文件 - rm ./private.${MP_APPID}.key # 更新商户后台状态 - echo "上传成功,通知SaaS平台后台..." # curl -X POST "https://your-saas-api.com/tenants/${TENANT_ID}/mp-status" -d '{"status":"uploaded", "version":"1.0.0-${CI_PIPELINE_IID}"}' # 当有商户点击发布时,通过API触发这个job publish_tenant_mp: <<: *build_template rules: - if: '$CI_PIPELINE_SOURCE == "trigger"' # 仅通过API触发器运行时执行 variables: # 这些变量由触发CI/CD的API请求动态传入 # TENANT_ID: "由API触发时传入" # MP_APPID: "由API触发时传入" # MP_PROJECT_NAME: "由API触发时传入" # API_URL: "由API触发时传入" # MP_PRIVATE_KEY: "从安全的变量库中获取"
3.1.4. 联通商户后台与 CI/CD
在商户后台创建“一键发布”按钮。
当商户点击此按钮时,你的后端服务执行以下操作: a. 验证权限: 检查该商户是否已配置AppID等信息。 b. 从配置中心读取配置: 获取商户的appid, project_name, api_url等。 c. 从安全存储中获取私钥: 获取你为这个appid保管的private.key内容。 d. 触发CI/CD Pipeline: 调用CI/CD工具的API(如GitLab Trigger API),将商户的配置信息作为变量传递给Pipeline。 e. 更新状态: 将商户小程序的状态更新为“发布中...”。
接收CI/CD回调(可选但推荐): CI/CD流程结束后(无论成功或失败),可以配置Webhook通知你的后端服务,以便及时更新最终状态,并通知商户。
3.2. 方案二:SpringBoot + Shell脚本
此方案适用于以Java为主要技术栈的团队,将CI/CD的能力内聚到自己的SpringBoot应用中,不依赖外部CI/CD工具。
3.2.1. 整体架构与流程
3.2.2. 服务器环境准备
在你的云服务器上,需要预先安装和配置好以下环境:
Node.js 和 npm:
# 以Ubuntu为例 curl -sL https://deb.nodesource.com/setup_16.x | sudo -E bash - sudo apt-get install -y nodejs
miniprogram-ci 工具:
npm install -g miniprogram-ci
小程序模板代码:
方案A (推荐): 在服务器上某个固定目录(如
/opt/mp-template
)存放你的小程序模板代码。你可以通过git clone
你的代码库,后续更新只需git pull
。方案B: 将模板代码打包在SpringBoot项目资源中,每次构建时解压。这种方式更新模板代码需要重新部署SpringBoot应用。
安全目录: 创建一个用于存放所有商户小程序上传密钥的安全目录。
sudo mkdir /opt/mp-keys sudo chown your_app_user:your_app_group /opt/mp-keys # 确保SpringBoot应用有权限读取
3.2.3. SpringBoot 实现步骤
数据库设计: 创建任务表
mp_publish_task
来跟踪每个发布任务的状态。id
(PK),tenant_id
,appid
,version
,status
(PENDING, PROCESSING, SUCCESS, FAILED),log
(TEXT),create_time
,update_time
Controller - 接收发布请求和查询状态:
@RestController @RequestMapping("/api/mp") public class MpPublishController { @Autowired private MpPublishService mpPublishService; // 1. 触发发布 @PostMapping("/publish") public ResponseEntity<String> publish(@RequestBody PublishRequest request) { // ...参数校验, 获取商户信息... mpPublishService.publish(tenantId, appid, ...); return ResponseEntity.ok("发布任务已创建,正在后台处理中..."); } // 2. 查询状态 @GetMapping("/status") public ResponseEntity<TaskStatus> getStatus(@RequestParam String tenantId) { TaskStatus status = mpPublishService.getLatestTaskStatus(tenantId); return ResponseEntity.ok(status); } }
Service - 核心异步处理逻辑: 在启动类上开启
@EnableAsync
。@Service public class MpPublishService { @Autowired private MpTaskRepository taskRepository; @Async // 标记为异步方法 public void publish(String tenantId, String appid, String appName) { // 1. 创建任务记录 MpPublishTask task = new MpPublishTask(tenantId, appid, "PROCESSING"); taskRepository.save(task); try { // 2. 构建ProcessBuilder来执行外部脚本 ProcessBuilder processBuilder = new ProcessBuilder( "bash", "/opt/scripts/build.sh", // 你的脚本路径 appid, appName, ... // 传递参数 ); processBuilder.redirectErrorStream(true); Process process = processBuilder.start(); // 3. 读取脚本输出日志 StringBuilder logOutput = new StringBuilder(); try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { String line; while ((line = reader.readLine()) != null) { logOutput.append(line).append("\n"); } } // 4. 等待脚本执行完成并根据退出码更新任务状态 int exitCode = process.waitFor(); if (exitCode == 0) { task.setStatus("SUCCESS"); } else { task.setStatus("FAILED"); } task.setLog(logOutput.toString()); taskRepository.save(task); } catch (IOException | InterruptedException e) { // 异常处理 task.setStatus("FAILED"); task.setLog("执行构建时发生内部错误: " + e.getMessage()); taskRepository.save(task); Thread.currentThread().interrupt(); } } }
Shell 脚本 - 实际的构建者 (
/opt/scripts/build.sh
)#!/bin/bash # 接收Java程序传来的参数 APP_ID=$1 PROJECT_NAME=$2 VERSION=$3 PRIVATE_KEY_PATH=$4 BUILD_ID=$5 TENANT_ID=$6 # 定义目录 TEMPLATE_DIR="/opt/mp-template" BUILD_BASE_DIR="/tmp/mp-builds" BUILD_DIR="${BUILD_BASE_DIR}/${BUILD_ID}" API_URL="https://api.your-saas.com/v1/${TENANT_ID}" function log_and_exit { echo "ERROR: $1" >&2 exit 1 } echo "--- 开始构建小程序 ---" # 1. 创建临时的、隔离的构建目录 mkdir -p "${BUILD_DIR}" || log_and_exit "无法创建构建目录 ${BUILD_DIR}" cd "${BUILD_DIR}" || log_and_exit "无法进入构建目录" # 2. 复制模板代码到构建目录 cp -r "${TEMPLATE_DIR}/." . || log_and_exit "复制模板代码失败" # 3. 注入配置 (使用sed命令替换占位符) sed -i "s/%APP_ID%/${APP_ID}/g" project.config.json || log_and_exit "注入AppID失败" sed -i "s/%PROJECT_NAME%/${PROJECT_NAME}/g" project.config.json || log_and_exit "注入项目名称失败" sed -i "s|%API_BASE_URL%|${API_URL}|g" src/config.js || log_and_exit "注入API地址失败" # 4. 安装依赖 (如果需要) if [ -f "package.json" ]; then npm install || log_and_exit "NPM install 失败" fi # 5. 调用 miniprogram-ci 上传 miniprogram-ci upload \ --pp ./dist `# 你小程序编译后的代码目录,如果是原生则为 ./` \ --appid "${APP_ID}" \ --pkp "${PRIVATE_KEY_PATH}" \ --ver "${VERSION}" \ --desc "自动构建于 ${BUILD_ID}" \ -r 1 || log_and_exit "miniprogram-ci upload 失败" # 6. 清理工作 rm -rf "${BUILD_DIR}" echo "--- 构建并上传成功 ---" exit 0
请确保这个脚本有执行权限:
chmod +x /opt/scripts/build.sh
3.2.4. 前端交互
触发: 用户点击“发布”按钮,调用后端的
/api/mp/publish
接口。轮询: 前端启动一个定时器(例如每5秒一次),调用
/api/mp/status
接口查询最新状态。结果: 根据返回的状态(PROCESSING, SUCCESS, FAILED)在UI上给与用户相应的反馈。
4. 进阶与最佳实践
4.1. 版本管理
基础代码版本: 小程序模板代码应该有自己的版本号(如v2.1.0)。
商户发布版本: 商户的发布版本号可以是
[基础代码版本]-[构建ID]
,例如2.1.0-build123
。这样既能追溯到是基于哪个模板版本构建的,又能区分每次发布。
4.2. 安全
private.key 的安全存储: 绝对不要将私钥硬编码或提交到Git仓库。应使用CI/CD系统提供的安全变量/Secrets功能,或在服务器上使用严格权限控制的目录进行存储。
API Token安全: CI/CD脚本中用于调用你内部API的Token也需要安全存储。
4.3. 灰度发布
对于所有商户的模板代码升级,可以先选择一部分“白名单”商户进行发布,验证无误后再全量开放给所有商户升级。
4.4. 日志与监控
CI/CD的每一次构建都应有详细的日志,方便排查问题。
监控每次发布的成功率、耗时等指标。
5. 完整的发布生命周期管理
miniprogram-ci
只能将代码上传为 体验版。后续的审核与发布流程,可以通过调用微信服务端API实现更高阶的自动化。
5.1. 小程序发布状态流转
上传代码 -> 体验版 (Experience Version)
提交审核 -> 审核中 (Under Review)
审核完成 -> 审核通过 (Review Passed) / 审核失败 (Review Failed)
发布 -> 线上版 (Live/Release Version)
5.2. 推荐的自动化方案
这是最常用、最平衡的方案,兼顾了效率和商户的控制权。
步骤 | 自动化工具/API | 触发方式 | 推荐方案 |
---|---|---|---|
1. 上传代码 | miniprogram-ci upload |
商户点击"构建"按钮 | 完全自动化 |
2. 提交审核 | wxa/submit_audit API |
商户体验后点击"提交审核" | 半自动化(推荐) |
3. 查询状态 | wxa/get_auditstatus API |
后台定时任务 | 完全自动化 |
4. 最终发布 | wxa/release API / 手动 |
商户收到通过通知后 | 手动发布(最推荐) 或 半自动化 |
流程建议:
自动化上传: 用CI/CD完成代码上传,生成体验版二维码。
半自动化审核: 在商户后台展示体验版二维码,并提供一个“提交审核”按钮。点击后,你的后端调用微信的
wxa/submit_audit
API。自动化查询审核状态: 使用定时任务调用
wxa/get_auditstatus
API,并将结果通知商户。手动发布: 审核通过后,建议由商户手动登录微信公众平台进行最后一步的“全量发布”。这能给商户一个最终确认的机会,避免误操作。
6. 附件:微信官方文档
6.1. miniprogram-ci 工具文档
官方文档主页: 概述 | 微信开放文档
内容重点: 安装使用、上传(upload)、预览(preview)、密钥获取、IP白名单设置。
6.2. 小程序发布相关服务端API文档
内容重点(关键API):
wxa/submit_audit
: 提交代码审核wxa/get_auditstatus
: 查询指定版本的审核状态wxa/get_latest_auditstatus
: 查询最新一次提交的审核状态wxa/release
: 发布已通过审核的小程序 (高危操作,谨慎调用)wxa/undocodeaudit
: 撤回审核
6.3. 使用建议
先跑通 miniprogram-ci:从最简单的
upload
功能开始,在您的服务器上手动执行命令,确保能成功上传一个体验版。再集成到后端服务:当手动命令跑通后,再将其集成到您的 SpringBoot + Shell 脚本流程中。
最后实现高级功能:在基础上传流程稳定运行后,再根据业务需求,逐步引入“提交审核”和“查询状态”等服务端API的调用。