feat(deploy): 添加部署路径权限检查功能

Webhook 由 PHP(www) 触发,项目目录必须对当前用户可写。
添加了对 DEPLOY_PATH 的写权限检查,如果当前用户对部署路径没有写权限,
则显示错误信息并退出部署流程。同时提供修复建议:
chown -R www:www ${DEPLOY_PATH} && chmod -R u+rwX ${DEPLOY_PATH}
```
这个提交包含在:
可乐加冰
2026-10-26 15:55:43 +08:00
父节点 0e26119bcf
当前提交 2456ad6945
共有 6 个文件被更改,包括 488 次插入 和 0 次删除

查看文件

@@ -0,0 +1,272 @@
<?php
/**
* 方案 A:清除小程序「直播」相关后台数据(链接、装修组件、宫格入口等)
*
* 用法(在项目根目录 chufang.gynhc.com_rFTnT 下):
* php scripts/remove-live-for-audit.php # 仅预览,不写库
* php scripts/remove-live-for-audit.php --apply # 执行清理
*
* 说明:
* - 依赖 application/database.php / .env 数据库配置
* - 清理后请到后台「装修」各页面保存一次并发布;小程序需重新编译(已去掉 pages/live 路由)
*/
define('APP_PATH', dirname(__DIR__) . DIRECTORY_SEPARATOR . 'application' . DIRECTORY_SEPARATOR);
if (!is_file(APP_PATH . 'admin/command/Install/install.lock')) {
fwrite(STDERR, "未检测到 install.lock,请确认在 Shopro 后端项目根目录运行。\n");
exit(1);
}
require dirname(__DIR__) . DIRECTORY_SEPARATOR . 'thinkphp' . DIRECTORY_SEPARATOR . 'base.php';
\think\App::initCommon();
use think\Db;
$apply = in_array('--apply', $argv ?? [], true);
/** 是否直播相关路径/文案 */
function is_live_related_string($str)
{
if (!is_string($str) || $str === '') {
return false;
}
$lower = strtolower($str);
if (strpos($lower, 'live') !== false) {
return true;
}
if (strpos($str, '直播') !== false) {
return true;
}
if (strpos($str, 'live-player-plugin') !== false) {
return true;
}
return false;
}
/** 递归清理 JSON 结构中的直播链接与菜单项 */
function scrub_live_from_value(&$value)
{
$changed = false;
if (is_array($value)) {
// 带 path / path_name / name 的菜单项:整项删除
if (isset($value['path']) || isset($value['path_name']) || isset($value['name'])) {
$path = isset($value['path']) ? $value['path'] : '';
$pathName = isset($value['path_name']) ? $value['path_name'] : (isset($value['pathName']) ? $value['pathName'] : '');
$name = isset($value['name']) ? $value['name'] : '';
if (is_live_related_string($path) || is_live_related_string($pathName) || is_live_related_string($name)) {
return ['__delete__' => true, 'changed' => true];
}
}
$isList = array_keys($value) === range(0, count($value) - 1);
if ($isList) {
$newList = [];
foreach ($value as $item) {
$res = scrub_live_from_value($item);
if (is_array($res) && !empty($res['__delete__'])) {
$changed = true;
continue;
}
if (is_array($res) && isset($res['value'])) {
$newList[] = $res['value'];
if (!empty($res['changed'])) {
$changed = true;
}
} else {
$newList[] = $item;
}
}
$value = $newList;
return ['value' => $value, 'changed' => $changed];
}
foreach ($value as $k => &$child) {
if ($k === 'path' || $k === 'path_name' || $k === 'pathName' || $k === 'page') {
if (is_string($child) && is_live_related_string($child)) {
$child = '';
$changed = true;
}
}
$res = scrub_live_from_value($child);
if (is_array($res) && !empty($res['__delete__'])) {
unset($value[$k]);
$changed = true;
continue;
}
if (is_array($res) && array_key_exists('value', $res)) {
$value[$k] = $res['value'];
if (!empty($res['changed'])) {
$changed = true;
}
}
}
unset($child);
}
return ['value' => $value, 'changed' => $changed];
}
function log_line($msg)
{
echo $msg . PHP_EOL;
}
try {
Db::query('SELECT 1');
} catch (\Throwable $e) {
fwrite(STDERR, "数据库连接失败,请在已安装 Shopro 的服务器上运行(读取 .env / application/database.php)。\n");
fwrite(STDERR, $e->getMessage() . "\n");
exit(1);
}
$prefix = config('database.prefix') ?: 'fa_';
$linkTable = $prefix . 'shopro_link';
$contentTable = $prefix . 'shopro_decorate_content';
$configTable = $prefix . 'shopro_config';
$decorateTable = $prefix . 'shopro_decorate';
log_line('=== 清除直播入口(方案 A)===');
log_line('模式: ' . ($apply ? '【执行 --apply】' : '【预览,加 --apply 才写库】'));
log_line('表前缀: ' . $prefix);
log_line('');
// ---------- 1. shopro_link ----------
$links = Db::table($linkTable)
->whereNull('deletetime')
->where(function ($q) {
$q->where('path', 'like', '%live%')
->whereOr('path', 'like', '%直播%')
->whereOr('name', 'like', '%直播%');
})
->select();
log_line('[1] 系统链接 fa_shopro_link 命中 ' . count($links) . ' 条');
foreach ($links as $row) {
log_line(' - id=' . $row['id'] . ' group=' . $row['group'] . ' name=' . $row['name'] . ' path=' . $row['path']);
}
if ($apply && count($links) > 0) {
$ids = array_column($links, 'id');
Db::table($linkTable)->where('id', 'in', $ids)->update(['deletetime' => time()]);
log_line(' => 已软删除');
}
// 名称/路径含 live 的宽松匹配(path 字段)
$links2 = Db::table($linkTable)
->whereNull('deletetime')
->where('path', 'like', '%/pages/live%')
->select();
if (count($links2) > 0) {
log_line('[1b] 路径含 /pages/live 再命中 ' . count($links2) . ' 条');
foreach ($links2 as $row) {
log_line(' - id=' . $row['id'] . ' name=' . $row['name'] . ' path=' . $row['path']);
}
if ($apply) {
Db::table($linkTable)->where('id', 'in', array_column($links2, 'id'))->update(['deletetime' => time()]);
}
}
log_line('');
// ---------- 2. shopro_decorate_content ----------
$rows = Db::table($contentTable)->select();
$deletedComponents = 0;
$updatedComponents = 0;
log_line('[2] 装修组件 fa_shopro_decorate_content 共 ' . count($rows) . ' 条');
foreach ($rows as $row) {
$type = $row['type'] ?? '';
$id = $row['id'];
$decorateId = $row['decorate_id'] ?? '';
$name = $row['name'] ?? '';
if ($type === 'live') {
log_line(" - 删除直播组件 id={$id} decorate_id={$decorateId} name={$name}");
if ($apply) {
Db::table($contentTable)->where('id', $id)->delete();
}
$deletedComponents++;
continue;
}
$contentRaw = $row['content'] ?? '';
if ($contentRaw === '' || $contentRaw === null) {
continue;
}
$content = json_decode($contentRaw, true);
if (!is_array($content)) {
continue;
}
$res = scrub_live_from_value($content);
if (empty($res['changed'])) {
continue;
}
log_line(" - 清理组件 id={$id} type={$type} decorate_id={$decorateId} name={$name}");
$updatedComponents++;
if ($apply) {
Db::table($contentTable)->where('id', $id)->update([
'content' => json_encode($res['value'], JSON_UNESCAPED_UNICODE),
]);
}
}
log_line(" 小结: 删除直播组件 {$deletedComponents} 个,清理其它组件 {$updatedComponents} 个");
log_line('');
// ---------- 3. shopro_config(JSON 配置里可能藏宫格/菜单) ----------
$configRows = Db::table($configTable)->where(function ($q) {
$q->where('value', 'like', '%live%')->whereOr('value', 'like', '%直播%');
})->select();
log_line('[3] 配置 fa_shopro_config 命中 ' . count($configRows) . ' 条(value 含 live/直播)');
$configUpdated = 0;
foreach ($configRows as $row) {
$val = $row['value'];
$decoded = json_decode($val, true);
if (!is_array($decoded)) {
if (is_live_related_string($val)) {
log_line(' - 跳过非 JSON 配置 name=' . $row['name'] . '(请人工检查)');
}
continue;
}
$res = scrub_live_from_value($decoded);
if (empty($res['changed'])) {
continue;
}
log_line(' - 清理配置 name=' . $row['name']);
$configUpdated++;
if ($apply) {
Db::table($configTable)->where('id', $row['id'])->update([
'value' => json_encode($res['value'], JSON_UNESCAPED_UNICODE),
]);
}
}
log_line(" 小结: 更新配置 {$configUpdated} 条");
log_line('');
// ---------- 4. 列出仍使用中的装修模板(便于人工核对) ----------
try {
$decorates = Db::table($decorateTable)->field('id,name,status,type,platform')->select();
log_line('[4] 当前装修模板(清理后请到后台确认「使用中」的页面已发布)');
foreach ($decorates as $d) {
$status = isset($d['status']) ? $d['status'] : '';
log_line(' - id=' . $d['id'] . ' name=' . $d['name'] . ' status=' . $status . ' platform=' . ($d['platform'] ?? ''));
}
} catch (\Exception $e) {
log_line('[4] 读取装修模板列表跳过: ' . $e->getMessage());
}
log_line('');
log_line('=== 完成 ===');
if (!$apply) {
log_line('以上为预览。确认无误后执行: php scripts/remove-live-for-audit.php --apply');
}
log_line('小程序端请重新编译上传(pages.json 已去掉直播页路由)。');