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

559 lines
18 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 -*-
"""
微信小程序管理命令行工具
使用方法:
python mp_manager.py status # 查看小程序状态
python mp_manager.py audit # 查看审核状态
python mp_manager.py release # 发布上线
python mp_manager.py qrcode # 生成小程序码
python mp_manager.py domain # 查看/配置域名
python mp_manager.py privacy # 配置隐私协议
python mp_manager.py data # 查看数据分析
"""
import os
import sys
import argparse
from datetime import datetime, timedelta
from pathlib import Path
# 添加当前目录到路径
sys.path.insert(0, str(Path(__file__).parent))
from mp_api import MiniProgramAPI, APIError, create_api_from_env
def print_header(title: str):
"""打印标题"""
print("\n" + "=" * 50)
print(f" {title}")
print("=" * 50)
def print_success(message: str):
"""打印成功信息"""
print(f"{message}")
def print_error(message: str):
"""打印错误信息"""
print(f"{message}")
def print_info(message: str):
"""打印信息"""
print(f" {message}")
def cmd_status(api: MiniProgramAPI, args):
"""查看小程序状态"""
print_header("小程序基础信息")
try:
info = api.get_basic_info()
print(f"\n📱 AppID: {info.appid}")
print(f"📝 名称: {info.nickname}")
print(f"📄 简介: {info.signature}")
print(f"🏢 主体: {info.principal_name}")
print(f"✓ 认证状态: {'已认证' if info.realname_status == 1 else '未认证'}")
if info.head_image_url:
print(f"🖼️ 头像: {info.head_image_url}")
# 获取类目
print("\n📂 已设置类目:")
categories = api.get_category()
if categories:
for cat in categories:
print(f" - {cat.get('first_class', '')} > {cat.get('second_class', '')}")
else:
print(" (未设置类目)")
except APIError as e:
print_error(f"获取信息失败: {e}")
def cmd_audit(api: MiniProgramAPI, args):
"""查看审核状态"""
print_header("审核状态")
try:
status = api.get_latest_audit_status()
print(f"\n🔢 审核单ID: {status.auditid}")
print(f"📊 状态: {status.status_text}")
if status.reason:
print(f"\n❗ 拒绝原因:")
print(f" {status.reason}")
if status.screenshot:
print(f"\n📸 问题截图: {status.screenshot}")
if status.status == 0:
print("\n👉 下一步: 运行 'python mp_manager.py release' 发布上线")
elif status.status == 1:
print("\n👉 请根据拒绝原因修改后重新提交审核")
elif status.status == 2:
print("\n👉 审核中请耐心等待通常1-3个工作日")
print(" 可运行 'python mp_manager.py audit' 再次查询")
except APIError as e:
print_error(f"获取审核状态失败: {e}")
def cmd_submit(api: MiniProgramAPI, args):
"""提交审核"""
print_header("提交审核")
version_desc = args.desc or input("请输入版本说明: ").strip()
if not version_desc:
print_error("版本说明不能为空")
return
try:
# 获取页面列表
pages = api.get_page()
if not pages:
print_error("未找到页面,请先上传代码")
return
print(f"\n📄 检测到 {len(pages)} 个页面:")
for p in pages[:5]:
print(f" - {p}")
if len(pages) > 5:
print(f" ... 还有 {len(pages) - 5}")
# 确认提交
confirm = input("\n确认提交审核? (y/n): ").strip().lower()
if confirm != 'y':
print_info("已取消")
return
auditid = api.submit_audit(version_desc=version_desc)
print_success(f"审核已提交审核单ID: {auditid}")
print("\n👉 运行 'python mp_manager.py audit' 查询审核状态")
except APIError as e:
print_error(f"提交审核失败: {e}")
def cmd_release(api: MiniProgramAPI, args):
"""发布上线"""
print_header("发布上线")
try:
# 先检查审核状态
status = api.get_latest_audit_status()
if status.status != 0:
print_error(f"当前审核状态: {status.status_text}")
print_info("只有审核通过的版本才能发布")
return
print(f"📊 审核状态: {status.status_text}")
# 确认发布
confirm = input("\n确认发布上线? (y/n): ").strip().lower()
if confirm != 'y':
print_info("已取消")
return
api.release()
print_success("🎉 发布成功!小程序已上线")
except APIError as e:
print_error(f"发布失败: {e}")
def cmd_revert(api: MiniProgramAPI, args):
"""版本回退"""
print_header("版本回退")
try:
# 获取可回退版本
history = api.get_revert_history()
if not history:
print_info("没有可回退的版本")
return
print("\n📜 可回退版本:")
for v in history:
print(f" - {v.get('user_version', '?')}: {v.get('user_desc', '')}")
# 确认回退
confirm = input("\n确认回退到上一版本? (y/n): ").strip().lower()
if confirm != 'y':
print_info("已取消")
return
api.revert_code_release()
print_success("版本回退成功")
except APIError as e:
print_error(f"版本回退失败: {e}")
def cmd_qrcode(api: MiniProgramAPI, args):
"""生成小程序码"""
print_header("生成小程序码")
# 场景选择
print("\n选择类型:")
print(" 1. 体验版二维码")
print(" 2. 小程序码有限制每个path最多10万个")
print(" 3. 无限小程序码(推荐)")
choice = args.type or input("\n请选择 (1/2/3): ").strip()
output_file = args.output or f"qrcode_{datetime.now().strftime('%Y%m%d_%H%M%S')}.png"
try:
if choice == "1":
# 体验版二维码
path = args.path or input("页面路径 (默认首页): ").strip() or None
data = api.get_qrcode(path)
elif choice == "2":
# 小程序码
path = args.path or input("页面路径: ").strip()
if not path:
print_error("页面路径不能为空")
return
data = api.get_wxacode(path)
elif choice == "3":
# 无限小程序码
scene = args.scene or input("场景值 (最长32字符): ").strip()
if not scene:
print_error("场景值不能为空")
return
page = args.path or input("页面路径 (需已发布): ").strip() or None
data = api.get_wxacode_unlimit(scene, page)
else:
print_error("无效选择")
return
# 保存文件
with open(output_file, "wb") as f:
f.write(data)
print_success(f"小程序码已保存: {output_file}")
# 尝试打开
if sys.platform == "darwin":
os.system(f'open "{output_file}"')
except APIError as e:
print_error(f"生成小程序码失败: {e}")
def cmd_domain(api: MiniProgramAPI, args):
"""查看/配置域名"""
print_header("域名配置")
try:
# 获取当前配置
domains = api.get_domain()
webview_domains = api.get_webview_domain()
print("\n🌐 服务器域名:")
print(f" request: {', '.join(domains.get('requestdomain', [])) or '(无)'}")
print(f" wsrequest: {', '.join(domains.get('wsrequestdomain', [])) or '(无)'}")
print(f" upload: {', '.join(domains.get('uploaddomain', [])) or '(无)'}")
print(f" download: {', '.join(domains.get('downloaddomain', [])) or '(无)'}")
print(f"\n🔗 业务域名:")
print(f" webview: {', '.join(webview_domains) or '(无)'}")
# 是否要配置
if args.set_request:
print(f"\n配置 request 域名: {args.set_request}")
api.set_domain(requestdomain=[args.set_request])
print_success("域名配置成功")
except APIError as e:
print_error(f"域名配置失败: {e}")
def cmd_privacy(api: MiniProgramAPI, args):
"""配置隐私协议"""
print_header("隐私协议配置")
try:
# 获取当前配置
settings = api.get_privacy_setting()
print("\n📋 当前隐私设置:")
setting_list = settings.get("setting_list", [])
if setting_list:
for s in setting_list:
print(f" - {s.get('privacy_key', '?')}: {s.get('privacy_text', '')}")
else:
print(" (未配置)")
owner = settings.get("owner_setting", {})
if owner:
print(f"\n📧 联系方式:")
if owner.get("contact_email"):
print(f" 邮箱: {owner['contact_email']}")
if owner.get("contact_phone"):
print(f" 电话: {owner['contact_phone']}")
# 快速配置
if args.quick:
print("\n⚡ 快速配置常用隐私项...")
default_settings = [
{"privacy_key": "UserInfo", "privacy_text": "用于展示您的头像和昵称"},
{"privacy_key": "Location", "privacy_text": "用于获取您的位置信息以推荐附近服务"},
{"privacy_key": "PhoneNumber", "privacy_text": "用于登录验证和订单通知"},
]
api.set_privacy_setting(
setting_list=default_settings,
contact_email=args.email or "contact@example.com",
contact_phone=args.phone or "15880802661"
)
print_success("隐私协议配置成功")
except APIError as e:
print_error(f"隐私协议配置失败: {e}")
def cmd_data(api: MiniProgramAPI, args):
"""查看数据分析"""
print_header("数据分析")
# 默认查询最近7天
end_date = datetime.now().strftime("%Y%m%d")
begin_date = (datetime.now() - timedelta(days=7)).strftime("%Y%m%d")
if args.begin:
begin_date = args.begin
if args.end:
end_date = args.end
try:
print(f"\n📊 访问趋势 ({begin_date} ~ {end_date}):")
data = api.get_daily_visit_trend(begin_date, end_date)
if not data:
print(" (暂无数据)")
return
# 统计汇总
total_pv = sum(d.get("visit_pv", 0) for d in data)
total_uv = sum(d.get("visit_uv", 0) for d in data)
total_new = sum(d.get("visit_uv_new", 0) for d in data)
print(f"\n📈 汇总数据:")
print(f" 总访问次数: {total_pv:,}")
print(f" 总访问人数: {total_uv:,}")
print(f" 新用户数: {total_new:,}")
print(f"\n📅 每日明细:")
for d in data[-7:]: # 只显示最近7天
date = d.get("ref_date", "?")
pv = d.get("visit_pv", 0)
uv = d.get("visit_uv", 0)
stay = d.get("stay_time_uv", 0)
print(f" {date}: PV={pv}, UV={uv}, 人均停留={stay:.1f}")
except APIError as e:
print_error(f"获取数据失败: {e}")
def cmd_quota(api: MiniProgramAPI, args):
"""查看API配额"""
print_header("API配额")
common_apis = [
"/wxa/getwxacode",
"/wxa/getwxacodeunlimit",
"/wxa/genwxashortlink",
"/wxa/submit_audit",
"/cgi-bin/message/subscribe/send"
]
try:
for cgi_path in common_apis:
try:
quota = api.get_api_quota(cgi_path)
daily_limit = quota.get("daily_limit", 0)
used = quota.get("used", 0)
remain = quota.get("remain", 0)
print(f"\n📌 {cgi_path}")
print(f" 每日限额: {daily_limit:,}")
print(f" 已使用: {used:,}")
print(f" 剩余: {remain:,}")
except APIError:
pass
except APIError as e:
print_error(f"获取配额失败: {e}")
def cmd_cli(api: MiniProgramAPI, args):
"""使用微信开发者工具CLI"""
print_header("微信开发者工具CLI")
cli_path = "/Applications/wechatwebdevtools.app/Contents/MacOS/cli"
project_path = args.project or os.getenv("MINIPROGRAM_PATH", "")
if not project_path:
project_path = input("请输入小程序项目路径: ").strip()
if not os.path.exists(project_path):
print_error(f"项目路径不存在: {project_path}")
return
if not os.path.exists(cli_path):
print_error("未找到微信开发者工具,请先安装")
return
print(f"\n📂 项目路径: {project_path}")
print("\n选择操作:")
print(" 1. 打开项目")
print(" 2. 预览(生成二维码)")
print(" 3. 上传代码")
print(" 4. 编译")
choice = input("\n请选择: ").strip()
if choice == "1":
os.system(f'"{cli_path}" -o "{project_path}"')
print_success("项目已打开")
elif choice == "2":
output = f"{project_path}/preview.png"
os.system(f'"{cli_path}" preview --project "{project_path}" --qr-format image --qr-output "{output}"')
if os.path.exists(output):
print_success(f"预览二维码已生成: {output}")
os.system(f'open "{output}"')
else:
print_error("生成失败,请检查开发者工具是否已登录")
elif choice == "3":
version = input("版本号 (如 1.0.0): ").strip()
desc = input("版本说明: ").strip()
os.system(f'"{cli_path}" upload --project "{project_path}" --version "{version}" --desc "{desc}"')
print_success("代码上传完成")
elif choice == "4":
os.system(f'"{cli_path}" build-npm --project "{project_path}"')
print_success("编译完成")
else:
print_error("无效选择")
def main():
parser = argparse.ArgumentParser(
description="微信小程序管理工具",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
示例:
python mp_manager.py status 查看小程序状态
python mp_manager.py audit 查看审核状态
python mp_manager.py submit -d "修复xxx问题" 提交审核
python mp_manager.py release 发布上线
python mp_manager.py qrcode -t 3 -s "id=123" 生成无限小程序码
python mp_manager.py domain 查看域名配置
python mp_manager.py privacy --quick 快速配置隐私协议
python mp_manager.py data 查看数据分析
python mp_manager.py cli 使用开发者工具CLI
"""
)
subparsers = parser.add_subparsers(dest="command", help="子命令")
# status
subparsers.add_parser("status", help="查看小程序状态")
# audit
subparsers.add_parser("audit", help="查看审核状态")
# submit
submit_parser = subparsers.add_parser("submit", help="提交审核")
submit_parser.add_argument("-d", "--desc", help="版本说明")
# release
subparsers.add_parser("release", help="发布上线")
# revert
subparsers.add_parser("revert", help="版本回退")
# qrcode
qr_parser = subparsers.add_parser("qrcode", help="生成小程序码")
qr_parser.add_argument("-t", "--type", choices=["1", "2", "3"], help="类型1=体验版2=小程序码3=无限小程序码")
qr_parser.add_argument("-p", "--path", help="页面路径")
qr_parser.add_argument("-s", "--scene", help="场景值类型3时使用")
qr_parser.add_argument("-o", "--output", help="输出文件名")
# domain
domain_parser = subparsers.add_parser("domain", help="查看/配置域名")
domain_parser.add_argument("--set-request", help="设置request域名")
# privacy
privacy_parser = subparsers.add_parser("privacy", help="配置隐私协议")
privacy_parser.add_argument("--quick", action="store_true", help="快速配置常用隐私项")
privacy_parser.add_argument("--email", help="联系邮箱")
privacy_parser.add_argument("--phone", help="联系电话")
# data
data_parser = subparsers.add_parser("data", help="查看数据分析")
data_parser.add_argument("--begin", help="开始日期 YYYYMMDD")
data_parser.add_argument("--end", help="结束日期 YYYYMMDD")
# quota
subparsers.add_parser("quota", help="查看API配额")
# cli
cli_parser = subparsers.add_parser("cli", help="使用微信开发者工具CLI")
cli_parser.add_argument("-p", "--project", help="小程序项目路径")
args = parser.parse_args()
if not args.command:
parser.print_help()
return
# 创建API实例
try:
api = create_api_from_env()
except Exception as e:
print_error(f"初始化API失败: {e}")
print_info("请检查 .env 文件中的配置")
return
# 执行命令
commands = {
"status": cmd_status,
"audit": cmd_audit,
"submit": cmd_submit,
"release": cmd_release,
"revert": cmd_revert,
"qrcode": cmd_qrcode,
"domain": cmd_domain,
"privacy": cmd_privacy,
"data": cmd_data,
"quota": cmd_quota,
"cli": cmd_cli,
}
cmd_func = commands.get(args.command)
if cmd_func:
try:
cmd_func(api, args)
finally:
api.close()
else:
parser.print_help()
if __name__ == "__main__":
main()