Files
soul/开发文档/小程序管理/scripts/mp_deploy.py

726 lines
25 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
小程序一键部署工具 v2.0
功能:
- 多小程序管理
- 一键部署上线
- 自动认证提交
- 认证状态检查
- 材料有效性验证
使用方法:
python mp_deploy.py list # 列出所有小程序
python mp_deploy.py add # 添加新小程序
python mp_deploy.py deploy <app_id> # 一键部署
python mp_deploy.py cert <app_id> # 提交认证
python mp_deploy.py cert-status <app_id> # 查询认证状态
python mp_deploy.py upload <app_id> # 上传代码
python mp_deploy.py release <app_id> # 发布上线
"""
import os
import sys
import json
import subprocess
import argparse
from datetime import datetime
from pathlib import Path
from typing import Optional, Dict, List, Any
from dataclasses import dataclass, asdict
# 配置文件路径
CONFIG_FILE = Path(__file__).parent / "apps_config.json"
@dataclass
class AppConfig:
"""小程序配置"""
id: str
name: str
appid: str
project_path: str
private_key_path: str = ""
api_domain: str = ""
description: str = ""
certification: Dict = None
def __post_init__(self):
if self.certification is None:
self.certification = {
"status": "unknown",
"enterprise_name": "",
"license_number": "",
"legal_persona_name": "",
"legal_persona_wechat": "",
"component_phone": ""
}
class ConfigManager:
"""配置管理器"""
def __init__(self, config_file: Path = CONFIG_FILE):
self.config_file = config_file
self.config = self._load_config()
def _load_config(self) -> Dict:
"""加载配置"""
if self.config_file.exists():
with open(self.config_file, 'r', encoding='utf-8') as f:
return json.load(f)
return {"apps": [], "certification_materials": {}, "third_party_platform": {}}
def _save_config(self):
"""保存配置"""
with open(self.config_file, 'w', encoding='utf-8') as f:
json.dump(self.config, f, ensure_ascii=False, indent=2)
def get_apps(self) -> List[AppConfig]:
"""获取所有小程序"""
return [AppConfig(**app) for app in self.config.get("apps", [])]
def get_app(self, app_id: str) -> Optional[AppConfig]:
"""获取指定小程序"""
for app in self.config.get("apps", []):
if app["id"] == app_id or app["appid"] == app_id:
return AppConfig(**app)
return None
def add_app(self, app: AppConfig):
"""添加小程序"""
apps = self.config.get("apps", [])
# 检查是否已存在
for i, existing in enumerate(apps):
if existing["id"] == app.id:
apps[i] = asdict(app)
self.config["apps"] = apps
self._save_config()
return
apps.append(asdict(app))
self.config["apps"] = apps
self._save_config()
def update_app(self, app_id: str, updates: Dict):
"""更新小程序配置"""
apps = self.config.get("apps", [])
for i, app in enumerate(apps):
if app["id"] == app_id:
apps[i].update(updates)
self.config["apps"] = apps
self._save_config()
return True
return False
def get_cert_materials(self) -> Dict:
"""获取通用认证材料"""
return self.config.get("certification_materials", {})
def update_cert_materials(self, materials: Dict):
"""更新认证材料"""
self.config["certification_materials"] = materials
self._save_config()
class MiniProgramDeployer:
"""小程序部署器"""
# 微信开发者工具CLI路径
WX_CLI = "/Applications/wechatwebdevtools.app/Contents/MacOS/cli"
def __init__(self):
self.config = ConfigManager()
def _check_wx_cli(self) -> bool:
"""检查微信开发者工具是否安装"""
return os.path.exists(self.WX_CLI)
def _run_cli(self, *args, project_path: str = None) -> tuple:
"""运行CLI命令"""
cmd = [self.WX_CLI] + list(args)
if project_path:
cmd.extend(["--project", project_path])
try:
result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
return result.returncode == 0, result.stdout + result.stderr
except subprocess.TimeoutExpired:
return False, "命令执行超时"
except Exception as e:
return False, str(e)
def list_apps(self):
"""列出所有小程序"""
apps = self.config.get_apps()
if not apps:
print("\n📭 暂无配置的小程序")
print(" 运行 'python mp_deploy.py add' 添加小程序")
return
print("\n" + "=" * 60)
print(" 📱 小程序列表")
print("=" * 60)
for i, app in enumerate(apps, 1):
cert_status = app.certification.get("status", "unknown")
status_icon = {
"verified": "",
"pending": "",
"rejected": "",
"expired": "⚠️",
"unknown": ""
}.get(cert_status, "")
print(f"\n [{i}] {app.name}")
print(f" ID: {app.id}")
print(f" AppID: {app.appid}")
print(f" 认证: {status_icon} {cert_status}")
print(f" 路径: {app.project_path}")
print("\n" + "-" * 60)
print(" 使用方法:")
print(" python mp_deploy.py deploy <id> 一键部署")
print(" python mp_deploy.py cert <id> 提交认证")
print("=" * 60 + "\n")
def add_app(self):
"""交互式添加小程序"""
print("\n" + "=" * 50)
print(" 添加新小程序")
print("=" * 50 + "\n")
# 收集信息
app_id = input("小程序ID用于标识如 my-app: ").strip()
if not app_id:
print("❌ ID不能为空")
return
name = input("小程序名称: ").strip()
appid = input("AppID如 wx1234567890: ").strip()
project_path = input("项目路径: ").strip()
if not os.path.exists(project_path):
print(f"⚠️ 警告:路径不存在 {project_path}")
api_domain = input("API域名可选: ").strip()
description = input("描述(可选): ").strip()
# 认证信息
print("\n📋 认证信息(可稍后配置):")
enterprise_name = input("企业名称: ").strip()
app = AppConfig(
id=app_id,
name=name,
appid=appid,
project_path=project_path,
api_domain=api_domain,
description=description,
certification={
"status": "unknown",
"enterprise_name": enterprise_name,
"license_number": "",
"legal_persona_name": "",
"legal_persona_wechat": "",
"component_phone": "15880802661"
}
)
self.config.add_app(app)
print(f"\n✅ 小程序 [{name}] 添加成功!")
def deploy(self, app_id: str, skip_cert_check: bool = False):
"""一键部署流程"""
app = self.config.get_app(app_id)
if not app:
print(f"❌ 未找到小程序: {app_id}")
print(" 运行 'python mp_deploy.py list' 查看所有小程序")
return False
print("\n" + "=" * 60)
print(f" 🚀 一键部署: {app.name}")
print("=" * 60)
steps = [
("检查环境", self._step_check_env),
("检查认证状态", lambda a: self._step_check_cert(a, skip_cert_check)),
("编译项目", self._step_build),
("上传代码", self._step_upload),
("提交审核", self._step_submit_audit),
]
for step_name, step_func in steps:
print(f"\n📍 步骤: {step_name}")
print("-" * 40)
success = step_func(app)
if not success:
print(f"\n❌ 部署中断于: {step_name}")
return False
print("\n" + "=" * 60)
print(" 🎉 部署完成!")
print("=" * 60)
print(f"\n 下一步操作:")
print(f" 1. 等待审核通常1-3个工作日")
print(f" 2. 审核通过后运行: python mp_deploy.py release {app_id}")
print(f" 3. 查看状态: python mp_deploy.py status {app_id}")
return True
def _step_check_env(self, app: AppConfig) -> bool:
"""检查环境"""
# 检查微信开发者工具
if not self._check_wx_cli():
print("❌ 未找到微信开发者工具")
print(" 请安装: https://developers.weixin.qq.com/miniprogram/dev/devtools/download.html")
return False
print("✅ 微信开发者工具已安装")
# 检查项目路径
if not os.path.exists(app.project_path):
print(f"❌ 项目路径不存在: {app.project_path}")
return False
print(f"✅ 项目路径存在")
# 检查project.config.json
config_file = os.path.join(app.project_path, "project.config.json")
if os.path.exists(config_file):
with open(config_file, 'r') as f:
config = json.load(f)
if config.get("appid") != app.appid:
print(f"⚠️ 警告: project.config.json中的AppID与配置不一致")
print(f" 配置: {app.appid}")
print(f" 文件: {config.get('appid')}")
print("✅ 环境检查通过")
return True
def _step_check_cert(self, app: AppConfig, skip: bool = False) -> bool:
"""检查认证状态"""
if skip:
print("⏭️ 跳过认证检查")
return True
cert_status = app.certification.get("status", "unknown")
if cert_status == "verified":
print("✅ 已完成微信认证")
return True
if cert_status == "pending":
print("⏳ 认证审核中")
print(" 可选择:")
print(" 1. 继续部署(未认证可上传,但无法发布)")
print(" 2. 等待认证完成")
choice = input("\n是否继续? (y/n): ").strip().lower()
return choice == 'y'
if cert_status == "expired":
print("⚠️ 认证已过期,需要重新认证")
print(" 运行: python mp_deploy.py cert " + app.id)
choice = input("\n是否继续部署? (y/n): ").strip().lower()
return choice == 'y'
# 未认证或未知状态
print("⚠️ 未完成微信认证")
print(" 未认证的小程序可以上传代码,但无法发布上线")
print(" 运行: python mp_deploy.py cert " + app.id + " 提交认证")
choice = input("\n是否继续? (y/n): ").strip().lower()
return choice == 'y'
def _step_build(self, app: AppConfig) -> bool:
"""编译项目"""
print("📦 编译项目...")
# 使用CLI编译
success, output = self._run_cli("build-npm", project_path=app.project_path)
if not success:
# build-npm可能失败如果没有npm依赖不算错误
print(" 编译完成无npm依赖或编译失败继续...")
else:
print("✅ 编译成功")
return True
def _step_upload(self, app: AppConfig) -> bool:
"""上传代码"""
# 获取版本号
version = datetime.now().strftime("%Y.%m.%d.%H%M")
desc = f"自动部署 - {datetime.now().strftime('%Y-%m-%d %H:%M')}"
print(f"📤 上传代码...")
print(f" 版本: {version}")
print(f" 描述: {desc}")
success, output = self._run_cli(
"upload",
"--version", version,
"--desc", desc,
project_path=app.project_path
)
if not success:
print(f"❌ 上传失败")
print(f" {output}")
# 常见错误处理
if "login" in output.lower():
print("\n💡 提示: 请在微信开发者工具中登录后重试")
return False
print("✅ 上传成功")
return True
def _step_submit_audit(self, app: AppConfig) -> bool:
"""提交审核"""
print("📝 提交审核...")
# 使用CLI提交审核
success, output = self._run_cli(
"submit-audit",
project_path=app.project_path
)
if not success:
if "未认证" in output or "认证" in output:
print("⚠️ 提交审核失败:未完成微信认证")
print(" 代码已上传,但需要完成认证后才能提交审核")
print(f" 运行: python mp_deploy.py cert {app.id}")
return True # 不算失败,只是需要认证
print(f"❌ 提交审核失败")
print(f" {output}")
return False
print("✅ 审核已提交")
return True
def submit_certification(self, app_id: str):
"""提交企业认证"""
app = self.config.get_app(app_id)
if not app:
print(f"❌ 未找到小程序: {app_id}")
return
print("\n" + "=" * 60)
print(f" 📋 提交认证: {app.name}")
print("=" * 60)
# 获取通用认证材料
materials = self.config.get_cert_materials()
cert = app.certification
# 合并材料(小程序配置优先)
enterprise_name = cert.get("enterprise_name") or materials.get("enterprise_name", "")
print(f"\n📌 认证信息:")
print(f" 小程序: {app.name} ({app.appid})")
print(f" 企业名称: {enterprise_name}")
# 检查必要材料
missing = []
if not enterprise_name:
missing.append("企业名称")
if not materials.get("license_number"):
missing.append("营业执照号")
if not materials.get("legal_persona_name"):
missing.append("法人姓名")
if missing:
print(f"\n⚠️ 缺少认证材料:")
for m in missing:
print(f" - {m}")
print(f"\n请先完善认证材料:")
print(f" 编辑: {self.config.config_file}")
print(f" 或运行: python mp_deploy.py cert-config")
return
print("\n" + "-" * 40)
print("📋 认证方式说明:")
print("-" * 40)
print("""
【方式一】微信后台手动认证(推荐)
1. 登录小程序后台: https://mp.weixin.qq.com/
2. 设置 → 基本设置 → 微信认证
3. 选择"企业"类型
4. 填写企业信息、上传营业执照
5. 法人微信扫码验证
6. 支付认证费用300元/年)
7. 等待审核1-5个工作日
【方式二】通过第三方平台代认证(需开发)
如果你有第三方平台资质可以通过API代认证
1. 配置第三方平台凭证
2. 获取授权
3. 调用认证API
API接口: POST /wxa/sec/wxaauth
""")
print("\n" + "-" * 40)
print("📝 认证材料清单:")
print("-" * 40)
print("""
必需材料:
☐ 企业营业执照(扫描件或照片)
☐ 法人身份证(正反面)
☐ 法人微信号(用于扫码验证)
☐ 联系人手机号
☐ 认证费用 300元
认证有效期: 1年
到期后需重新认证(年审)
""")
# 更新状态为待认证
self.config.update_app(app_id, {
"certification": {
**cert,
"status": "pending",
"submit_time": datetime.now().isoformat()
}
})
print("\n✅ 已标记为待认证状态")
print(" 完成认证后运行: python mp_deploy.py cert-done " + app_id)
def check_cert_status(self, app_id: str):
"""检查认证状态"""
app = self.config.get_app(app_id)
if not app:
print(f"❌ 未找到小程序: {app_id}")
return
print("\n" + "=" * 60)
print(f" 🔍 认证状态: {app.name}")
print("=" * 60)
cert = app.certification
status = cert.get("status", "unknown")
status_info = {
"verified": ("✅ 已认证", "认证有效"),
"pending": ("⏳ 审核中", "请等待审核结果"),
"rejected": ("❌ 被拒绝", "请查看拒绝原因并重新提交"),
"expired": ("⚠️ 已过期", "需要重新认证(年审)"),
"unknown": ("❓ 未知", "请在微信后台确认状态")
}
icon, desc = status_info.get(status, ("", "未知状态"))
print(f"\n📌 当前状态: {icon}")
print(f" 说明: {desc}")
print(f" 企业: {cert.get('enterprise_name', '未填写')}")
if cert.get("submit_time"):
print(f" 提交时间: {cert.get('submit_time')}")
if cert.get("verify_time"):
print(f" 认证时间: {cert.get('verify_time')}")
if cert.get("expire_time"):
print(f" 到期时间: {cert.get('expire_time')}")
# 提示下一步操作
print("\n" + "-" * 40)
if status == "unknown" or status == "rejected":
print("👉 下一步: python mp_deploy.py cert " + app_id)
elif status == "pending":
print("👉 等待审核通常1-5个工作日")
print(" 审核通过后运行: python mp_deploy.py cert-done " + app_id)
elif status == "verified":
print("👉 可以发布小程序: python mp_deploy.py deploy " + app_id)
elif status == "expired":
print("👉 需要重新认证: python mp_deploy.py cert " + app_id)
def mark_cert_done(self, app_id: str):
"""标记认证完成"""
app = self.config.get_app(app_id)
if not app:
print(f"❌ 未找到小程序: {app_id}")
return
cert = app.certification
self.config.update_app(app_id, {
"certification": {
**cert,
"status": "verified",
"verify_time": datetime.now().isoformat(),
"expire_time": datetime.now().replace(year=datetime.now().year + 1).isoformat()
}
})
print(f"✅ 已标记 [{app.name}] 认证完成")
print(f" 有效期至: {datetime.now().year + 1}")
def release(self, app_id: str):
"""发布上线"""
app = self.config.get_app(app_id)
if not app:
print(f"❌ 未找到小程序: {app_id}")
return
print("\n" + "=" * 60)
print(f" 🎉 发布上线: {app.name}")
print("=" * 60)
# 检查认证状态
if app.certification.get("status") != "verified":
print("\n⚠️ 警告: 小程序未完成认证")
print(" 未认证的小程序无法发布上线")
choice = input("\n是否继续尝试? (y/n): ").strip().lower()
if choice != 'y':
return
print("\n📦 正在发布...")
# 尝试使用CLI发布
success, output = self._run_cli("release", project_path=app.project_path)
if success:
print("\n🎉 发布成功!小程序已上线")
else:
print(f"\n发布结果: {output}")
if "认证" in output:
print("\n💡 提示: 请先完成微信认证")
print(f" 运行: python mp_deploy.py cert {app_id}")
else:
print("\n💡 提示: 请在微信后台手动发布")
print(" 1. 登录 https://mp.weixin.qq.com/")
print(" 2. 版本管理 → 审核版本 → 发布")
def quick_upload(self, app_id: str, version: str = None, desc: str = None):
"""快速上传代码"""
app = self.config.get_app(app_id)
if not app:
print(f"❌ 未找到小程序: {app_id}")
return
if not version:
version = datetime.now().strftime("%Y.%m.%d.%H%M")
if not desc:
desc = f"快速上传 - {datetime.now().strftime('%Y-%m-%d %H:%M')}"
print(f"\n📤 上传代码: {app.name}")
print(f" 版本: {version}")
print(f" 描述: {desc}")
success, output = self._run_cli(
"upload",
"--version", version,
"--desc", desc,
project_path=app.project_path
)
if success:
print("✅ 上传成功")
else:
print(f"❌ 上传失败: {output}")
def print_header(title: str):
print("\n" + "=" * 50)
print(f" {title}")
print("=" * 50)
def main():
parser = argparse.ArgumentParser(
description="小程序一键部署工具 v2.0",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
示例:
python mp_deploy.py list 列出所有小程序
python mp_deploy.py add 添加新小程序
python mp_deploy.py deploy soul-party 一键部署
python mp_deploy.py cert soul-party 提交认证
python mp_deploy.py cert-status soul-party 查询认证状态
python mp_deploy.py cert-done soul-party 标记认证完成
python mp_deploy.py upload soul-party 仅上传代码
python mp_deploy.py release soul-party 发布上线
部署流程:
1. add 添加小程序配置
2. cert 提交企业认证(首次)
3. cert-done 认证通过后标记
4. deploy 一键部署(编译+上传+提审)
5. release 审核通过后发布
"""
)
subparsers = parser.add_subparsers(dest="command", help="子命令")
# list
subparsers.add_parser("list", help="列出所有小程序")
# add
subparsers.add_parser("add", help="添加新小程序")
# deploy
deploy_parser = subparsers.add_parser("deploy", help="一键部署")
deploy_parser.add_argument("app_id", help="小程序ID")
deploy_parser.add_argument("--skip-cert", action="store_true", help="跳过认证检查")
# cert
cert_parser = subparsers.add_parser("cert", help="提交认证")
cert_parser.add_argument("app_id", help="小程序ID")
# cert-status
cert_status_parser = subparsers.add_parser("cert-status", help="查询认证状态")
cert_status_parser.add_argument("app_id", help="小程序ID")
# cert-done
cert_done_parser = subparsers.add_parser("cert-done", help="标记认证完成")
cert_done_parser.add_argument("app_id", help="小程序ID")
# upload
upload_parser = subparsers.add_parser("upload", help="上传代码")
upload_parser.add_argument("app_id", help="小程序ID")
upload_parser.add_argument("-v", "--version", help="版本号")
upload_parser.add_argument("-d", "--desc", help="版本描述")
# release
release_parser = subparsers.add_parser("release", help="发布上线")
release_parser.add_argument("app_id", help="小程序ID")
args = parser.parse_args()
if not args.command:
parser.print_help()
return
deployer = MiniProgramDeployer()
commands = {
"list": lambda: deployer.list_apps(),
"add": lambda: deployer.add_app(),
"deploy": lambda: deployer.deploy(args.app_id, args.skip_cert if hasattr(args, 'skip_cert') else False),
"cert": lambda: deployer.submit_certification(args.app_id),
"cert-status": lambda: deployer.check_cert_status(args.app_id),
"cert-done": lambda: deployer.mark_cert_done(args.app_id),
"upload": lambda: deployer.quick_upload(args.app_id, getattr(args, 'version', None), getattr(args, 'desc', None)),
"release": lambda: deployer.release(args.app_id),
}
cmd_func = commands.get(args.command)
if cmd_func:
cmd_func()
else:
parser.print_help()
if __name__ == "__main__":
main()