feat: 上线前安全加固与发布工程化;appmall 由插件迁移为独立应用
安全(P0) - 发布包签名:新增 scripts/package_keygen.php、scripts/package_verify.php、 ywxapp\service\PackageVerifier;FrameworkService::apply() 解压前强制验签 (ywxapp.framework_package_verify,默认开启) - JWT 收紧:config/jwt.php 移除公开默认密钥,JwtService::assertStrongSecret() 校验非空 / 长度≥32 / 已知弱值;.env.production.example 键名由 JWT_SECRET_KEY 修正为 JWT_SECRET - /api/v1/Article:免登录白名单收窄为只读接口,写操作字段白名单 + assertOwner() 越权校验 - 开发者令牌:查找接口不再回显明文、统一失败文案 + 限流 - TemplateInstaller:文件名白名单 + realpath 归一化 + 目录内断言,修复 Zip Slip;补 use ZipArchive - 后台登录:接入验证码、失败锁定与登录限流(config/ratelimit.php) - 缓存 / Session 驱动可由 .env 覆盖(CACHE_DRIVER / SESSION_TYPE),集群部署改 redis - paycenter 支付日志开关改为 PAY_LOG,不再跟随 APP_DEBUG 工程化 / 交付 - 新增 CHANGELOG.md 与 .env.production.example;composer.lock 入库锁定依赖版本 - 插件离线安装 CLI 增加 --uid,后台上传安装透传 uid,付费插件授权校验可通过 - 授权拒绝文案区分「未携带 uid」与「未找到有效授权」,便于排障 - .gitignore 放行 .env.production.example / .env.addon.example / .gitattributes / public/.htaccess 结构 - appmall 由 addon/appmall 迁移为独立应用 app/appmall(市场 / 开发者中心 / 审核 / 分润)
This commit is contained in:
1 parent
1d49e6f5ee
commit
18101cf11a
636 files changed
+58517
-9519
No files matched your search
@@ -0,0 +1,195 @@
|
||||
<?php
|
||||
/**
|
||||
* 一次性脚本:把本地模板皮肤打包成「非 Web 目录」下的市场 zip,并生成测试记录 SQL。
|
||||
* 产物(zip + sql)统一输出到 build/templates/ 与 build/sql/(非 Web 可访问目录)。
|
||||
*
|
||||
* 模板包结构(与运行时约定保持一致,详见 ywxapp/library/TemplateInstaller.php):
|
||||
* templates/<name>/
|
||||
* ├── template.json # 元数据
|
||||
* ├── view/<插件>/<区>/*.html # 覆盖视图(与插件 view 同构)
|
||||
* ├── static/ # 皮肤专属静态资源(安装时部署到 public/static/templates/<name>/)
|
||||
* └── preview.png # 商店预览图
|
||||
*
|
||||
* zip 内部两段前缀:
|
||||
* - templates/<name>/ 放 template.json / view/ / preview.png
|
||||
* - static/ 放皮肤静态资源 → 安装时落到 public 侧
|
||||
* 客户端 TemplateInstaller::install() 按此前缀还原。
|
||||
*
|
||||
* 用法:
|
||||
* php scripts/package_template.php <name> [version | major|minor|patch]
|
||||
* - version 缺省时按 patch 自动递增 templates/<name>/template.json 的版本号并写回
|
||||
*
|
||||
* 例:
|
||||
* php scripts/package_template.php myblog
|
||||
* php scripts/package_template.php myblog minor
|
||||
* php scripts/package_template.php myblog 1.2.0
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
$root = dirname(__DIR__) . DIRECTORY_SEPARATOR; // d:/wwwroot/ywxapp_dev/
|
||||
|
||||
$name = $argv[1] ?? '';
|
||||
if ($name === '' || !preg_match('/^[a-zA-Z][a-zA-Z0-9_]*$/', $name)) {
|
||||
fwrite(STDERR, "用法: php scripts/package_template.php <name> [version|major|minor|patch]\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$tplDir = $root . 'templates' . DIRECTORY_SEPARATOR . $name . DIRECTORY_SEPARATOR;
|
||||
if (!is_dir($tplDir)) {
|
||||
fwrite(STDERR, "模板目录不存在: {$tplDir}\n");
|
||||
exit(1);
|
||||
}
|
||||
$jsonFile = $tplDir . 'template.json';
|
||||
if (!is_file($jsonFile)) {
|
||||
fwrite(STDERR, "缺少 template.json: {$jsonFile}\n");
|
||||
exit(1);
|
||||
}
|
||||
$meta = json_decode(file_get_contents($jsonFile), true);
|
||||
if (!is_array($meta)) {
|
||||
fwrite(STDERR, "template.json 解析失败\n");
|
||||
exit(1);
|
||||
}
|
||||
$curVersion = $meta['version'] ?? '';
|
||||
if (!$curVersion) {
|
||||
fwrite(STDERR, "template.json 缺少 version\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// ===================== 确定新版本号(支持自动递增) =====================
|
||||
$verArg = $argv[2] ?? '';
|
||||
if ($verArg === '') {
|
||||
$newVersion = bumpVersion($curVersion, 'patch');
|
||||
echo "[版本] 未指定版本号,按 patch 自动递增: {$curVersion} -> {$newVersion}\n";
|
||||
} elseif (preg_match('/^\d+\.\d+\.\d+$/', $verArg)) {
|
||||
$newVersion = $verArg;
|
||||
} elseif (in_array($verArg, ['major', 'minor', 'patch'], true)) {
|
||||
$newVersion = bumpVersion($curVersion, $verArg);
|
||||
echo "[版本] 按 {$verArg} 递增: {$curVersion} -> {$newVersion}\n";
|
||||
} else {
|
||||
fwrite(STDERR, "无效的版本参数: {$verArg}\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// 写回 template.json(保持元数据与产出一致)
|
||||
$meta['version'] = $newVersion;
|
||||
file_put_contents($jsonFile, json_encode($meta, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . "\n");
|
||||
echo "[版本] 已写回 templates/{$name}/template.json: {$newVersion}\n";
|
||||
|
||||
// ===================== 打包 =====================
|
||||
$outDir = $root . 'build' . DIRECTORY_SEPARATOR . 'templates' . DIRECTORY_SEPARATOR;
|
||||
if (!is_dir($outDir)) {
|
||||
mkdir($outDir, 0755, true);
|
||||
}
|
||||
$zipFile = $outDir . $name . '-' . $newVersion . '.zip';
|
||||
|
||||
if (!class_exists('ZipArchive')) {
|
||||
fwrite(STDERR, "ZipArchive 扩展不可用\n");
|
||||
exit(1);
|
||||
}
|
||||
$zip = new ZipArchive();
|
||||
if ($zip->open($zipFile, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
|
||||
fwrite(STDERR, "无法创建 zip: {$zipFile}\n");
|
||||
exit(1);
|
||||
}
|
||||
// 模板代码(排除 static/,static 单独以 static/ 前缀打包)
|
||||
addDirToZip($zip, $tplDir, 'templates/' . $name . '/', ['static']);
|
||||
// 皮肤静态资源 → 安装时部署到 public/static/templates/<name>/
|
||||
$staticDir = $tplDir . 'static' . DIRECTORY_SEPARATOR;
|
||||
if (is_dir($staticDir)) {
|
||||
addDirToZip($zip, $staticDir, 'static/');
|
||||
}
|
||||
$zip->close();
|
||||
|
||||
$hash = hash_file('sha256', $zipFile);
|
||||
$size = filesize($zipFile);
|
||||
$absPath = realpath($zipFile);
|
||||
|
||||
// ===================== 测试记录 SQL(Phase 3 接入 appmall 时 type='template') =====================
|
||||
$esc = static function (string $v): string {
|
||||
return str_replace(["\\", "'"], ["\\\\", "\\'"], $v);
|
||||
};
|
||||
$title = $meta['title'] ?? $name;
|
||||
$author = $meta['author'] ?? '';
|
||||
$description = $meta['description'] ?? '';
|
||||
$price = 0.00;
|
||||
$sql = "INSERT INTO `wxapp_appmall_addon_list` "
|
||||
. "(`name`,`type`,`title`,`version`,`price`,`author`,`description`,`logo`,`file_path`,`file_hash`,`status`,`create_at`,`update_at`) VALUES ("
|
||||
. "'" . $esc($name) . "',"
|
||||
. "'template',"
|
||||
. "'" . $esc($title) . "',"
|
||||
. "'" . $esc($newVersion) . "',"
|
||||
. floatval($price) . ","
|
||||
. "'" . $esc($author) . "',"
|
||||
. "'" . $esc($description) . "',"
|
||||
. "'',"
|
||||
. "'" . $esc($absPath) . "',"
|
||||
. "'" . $hash . "',"
|
||||
. "1,UNIX_TIMESTAMP(),UNIX_TIMESTAMP());";
|
||||
|
||||
$sqlDir = $root . 'build' . DIRECTORY_SEPARATOR . 'sql' . DIRECTORY_SEPARATOR;
|
||||
if (!is_dir($sqlDir)) {
|
||||
mkdir($sqlDir, 0755, true);
|
||||
}
|
||||
$sqlFile = $sqlDir . $name . '-' . $newVersion . '-template.sql';
|
||||
file_put_contents($sqlFile,
|
||||
"-- 模板市场测试记录(由 scripts/package_template.php 生成,type='template')\n"
|
||||
. "-- zip: {$absPath}\n-- sha256: {$hash}\n" . $sql . "\n");
|
||||
|
||||
echo "=== 打包完成 ===\n";
|
||||
echo "name : {$name}\n";
|
||||
echo "title : {$title}\n";
|
||||
echo "zip : {$absPath}\n";
|
||||
echo "size : {$size} bytes\n";
|
||||
echo "sha256: {$hash}\n";
|
||||
echo "目标插件: " . implode(',', $meta['target_addons'] ?? []) . "\n\n";
|
||||
echo "=== 测试记录 SQL(已写入 {$sqlFile})===\n";
|
||||
echo $sql . "\n";
|
||||
|
||||
/**
|
||||
* 在 x.y.z 基础上递增指定段
|
||||
*/
|
||||
function bumpVersion(string $ver, string $level = 'patch'): string
|
||||
{
|
||||
if (!preg_match('/^(\d+)\.(\d+)\.(\d+)$/', $ver, $m)) {
|
||||
return $ver;
|
||||
}
|
||||
$major = (int) $m[1];
|
||||
$minor = (int) $m[2];
|
||||
$patch = (int) $m[3];
|
||||
switch ($level) {
|
||||
case 'major':
|
||||
$major++; $minor = 0; $patch = 0; break;
|
||||
case 'minor':
|
||||
$minor++; $patch = 0; break;
|
||||
case 'patch':
|
||||
default:
|
||||
$patch++; break;
|
||||
}
|
||||
return $major . '.' . $minor . '.' . $patch;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将目录递归加入 zip(结构对齐 AddonService::addDirectoryToZip)
|
||||
* @param string[] $exclude 需排除的子目录名(相对 $dir 的一级目录)
|
||||
*/
|
||||
function addDirToZip(ZipArchive $zip, string $dir, string $prefix, array $exclude = []): void
|
||||
{
|
||||
$files = new RecursiveIteratorIterator(
|
||||
new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS),
|
||||
RecursiveIteratorIterator::SELF_FIRST
|
||||
);
|
||||
foreach ($files as $file) {
|
||||
$filePath = $file->getRealPath();
|
||||
$rel = ltrim(str_replace(DIRECTORY_SEPARATOR, '/', substr($filePath, strlen($dir))), '/');
|
||||
// 排除指定一级子目录(如 static)
|
||||
$top = explode('/', $rel)[0] ?? '';
|
||||
if ($top !== '' && in_array($top, $exclude, true)) {
|
||||
continue;
|
||||
}
|
||||
if ($file->isDir()) {
|
||||
$zip->addEmptyDir($prefix . $rel);
|
||||
} else {
|
||||
$zip->addFile($filePath, $prefix . $rel);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user