diff --git a/application/api/controller/Map.php b/application/api/controller/Map.php new file mode 100644 index 0000000..f2f61f3 --- /dev/null +++ b/application/api/controller/Map.php @@ -0,0 +1,87 @@ +where('name', 'amap')->value('value'); + if ($raw) { + $cfg = json_decode($raw, true); + if (!empty($cfg['appkey'])) { + return $cfg['appkey']; + } + } + $platform = Db::name('shopro_config')->where('name', 'platform')->value('value'); + if ($platform) { + $cfg = json_decode($platform, true); + if (!empty($cfg['amap']['appkey'])) { + return $cfg['amap']['appkey']; + } + } + return '24eb092b2163da8fa5087bf92844a5bd'; + } + + protected function amapGet($path, $query) + { + $query['key'] = $this->amapKey(); + $url = 'https://restapi.amap.com/v3/' . $path . '?' . http_build_query($query); + $ctx = stream_context_create(['http' => ['timeout' => 8]]); + $body = @file_get_contents($url, false, $ctx); + if ($body === false) { + $this->error('地图服务请求失败'); + } + $data = json_decode($body, true); + if (!$data || (isset($data['status']) && $data['status'] !== '1')) { + $info = isset($data['info']) ? $data['info'] : '地理编码失败'; + $this->error($info); + } + return $data; + } + + /** + * 地址 → 坐标 + * @ApiParams (name="address", type="string", required=true) + */ + public function geocode() + { + $address = trim($this->request->get('address', '')); + if ($address === '') { + $this->error('地址不能为空'); + } + $data = $this->amapGet('geocode/geo', ['address' => $address]); + if (empty($data['geocodes'][0]['location'])) { + $this->error('无法解析该地址,请尽量填写门牌号'); + } + list($lng, $lat) = explode(',', $data['geocodes'][0]['location']); + $this->success('ok', [ + 'latitude' => $lat, + 'longitude' => $lng, + 'formatted' => $data['geocodes'][0]['formatted_address'] ?? '', + ]); + } + + /** + * 坐标 → 地址 + * @ApiParams (name="location", type="string", required=true, description="lng,lat") + */ + public function regeo() + { + $location = trim($this->request->get('location', '')); + if ($location === '') { + $this->error('坐标不能为空'); + } + $data = $this->amapGet('geocode/regeo', ['location' => $location]); + $this->success('ok', $data['regeocode'] ?? []); + } +} diff --git a/deploy/supervisor/install.sh b/deploy/supervisor/install.sh new file mode 100644 index 0000000..246f593 --- /dev/null +++ b/deploy/supervisor/install.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +# 将 Shopro 队列 Supervisor 配置安装到服务器 +# 用法(root): +# DEPLOY_PATH=/www/wwwroot/chufang.gynhc.com PHP_BIN=/www/server/php/74/bin/php bash deploy/supervisor/install.sh +set -euo pipefail + +DEPLOY_PATH="${DEPLOY_PATH:-/www/wwwroot/chufang.gynhc.com}" +PHP_BIN="${PHP_BIN:-$(command -v php || true)}" +CONF_SRC="${DEPLOY_PATH}/deploy/supervisor/shopro-queue.conf" +CONF_DST="${SUPERVISOR_CONF_DIR:-/etc/supervisor/conf.d}/shopro-queue.conf" + +if [[ -z "$PHP_BIN" || ! -x "$PHP_BIN" ]]; then + echo "ERROR: 找不到可执行的 PHP,请设置 PHP_BIN,例如:" + echo " PHP_BIN=/www/server/php/74/bin/php" + exit 1 +fi + +if [[ ! -f "$CONF_SRC" ]]; then + echo "ERROR: 配置文件不存在: $CONF_SRC" + exit 1 +fi + +if [[ ! -f "${DEPLOY_PATH}/think" ]]; then + echo "ERROR: 项目根目录不正确,未找到 think: ${DEPLOY_PATH}/think" + exit 1 +fi + +mkdir -p "${DEPLOY_PATH}/runtime/log" + +sed \ + -e "s|PHP_BIN_PLACEHOLDER|${PHP_BIN}|g" \ + -e "s|DEPLOY_PATH_PLACEHOLDER|${DEPLOY_PATH}|g" \ + "$CONF_SRC" > "$CONF_DST" + +echo "已写入: $CONF_DST" +echo "PHP: $PHP_BIN" +echo "项目: $DEPLOY_PATH" + +if command -v supervisorctl >/dev/null 2>&1; then + supervisorctl reread + supervisorctl update + supervisorctl status shopro-queue:* +else + echo "WARN: 未找到 supervisorctl,请手动执行:" + echo " supervisorctl reread && supervisorctl update && supervisorctl status" +fi + +echo "完成。" diff --git a/deploy/supervisor/shopro-queue.conf b/deploy/supervisor/shopro-queue.conf new file mode 100644 index 0000000..8a8de97 --- /dev/null +++ b/deploy/supervisor/shopro-queue.conf @@ -0,0 +1,60 @@ +; Shopro 队列守护(Supervisor) +; +; 安装步骤见下方注释,或阅读 deploy/supervisor/install.sh +; +; 队列说明: +; shopro-high 支付后处理(返积分、减库存、拼团、打印小票等,需优先消费) +; shopro 订单自动关闭/确认/评价、消息通知、分销升级、活动等 +; +; 使用前请修改: +; 1. PHP_BIN → 服务器 php 绝对路径(宝塔常见:/www/server/php/74/bin/php) +; 2. DEPLOY_PATH → 站点根目录(含 think 文件,非 public) + +; --------------------------------------------------------------------------- +; 高优先级队列(积分返还、打印等) +; --------------------------------------------------------------------------- +[program:shopro-queue-high] +process_name=%(program_name)s_%(process_num)02d +command=PHP_BIN_PLACEHOLDER DEPLOY_PATH_PLACEHOLDER/think queue:listen --queue shopro-high --sleep 1 --timeout 120 --memory 256 +directory=DEPLOY_PATH_PLACEHOLDER +autostart=true +autorestart=true +startsecs=3 +startretries=10 +stopasgroup=true +killasgroup=true +stopwaitsecs=120 +user=www +numprocs=1 +redirect_stderr=true +stdout_logfile=DEPLOY_PATH_PLACEHOLDER/runtime/log/supervisor-shopro-high.log +stdout_logfile_maxbytes=20MB +stdout_logfile_backups=10 + +; --------------------------------------------------------------------------- +; 普通队列(自动关单、通知、分销等) +; --------------------------------------------------------------------------- +[program:shopro-queue] +process_name=%(program_name)s_%(process_num)02d +command=PHP_BIN_PLACEHOLDER DEPLOY_PATH_PLACEHOLDER/think queue:listen --queue shopro --sleep 3 --timeout 120 --memory 256 +directory=DEPLOY_PATH_PLACEHOLDER +autostart=true +autorestart=true +startsecs=3 +startretries=10 +stopasgroup=true +killasgroup=true +stopwaitsecs=120 +user=www +numprocs=1 +redirect_stderr=true +stdout_logfile=DEPLOY_PATH_PLACEHOLDER/runtime/log/supervisor-shopro.log +stdout_logfile_maxbytes=20MB +stdout_logfile_backups=10 + +; --------------------------------------------------------------------------- +; 分组(可选,便于 supervisorctl 批量管理) +; --------------------------------------------------------------------------- +[group:shopro-queue] +programs=shopro-queue-high,shopro-queue +priority=999 diff --git a/scripts/gitea-deploy.sh b/scripts/gitea-deploy.sh index 2b8d6b9..fda528f 100644 --- a/scripts/gitea-deploy.sh +++ b/scripts/gitea-deploy.sh @@ -23,6 +23,14 @@ if [[ ! -d .git ]]; then exit 1 fi +# Webhook 由 PHP(www) 触发,项目目录必须对当前用户可写 +RUN_USER="$(id -un 2>/dev/null || echo unknown)" +if [[ ! -w "$DEPLOY_PATH" ]]; then + log "ERROR: 当前用户 ${RUN_USER} 对 ${DEPLOY_PATH} 无写权限" + log "修复(root): chown -R www:www ${DEPLOY_PATH} && chmod -R u+rwX ${DEPLOY_PATH}" + exit 1 +fi + git config --global --add safe.directory "$DEPLOY_PATH" 2>/dev/null || true git fetch origin "$DEPLOY_BRANCH" diff --git a/scripts/remove-live-for-audit.php b/scripts/remove-live-for-audit.php new file mode 100644 index 0000000..51d6713 --- /dev/null +++ b/scripts/remove-live-for-audit.php @@ -0,0 +1,272 @@ + 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 已去掉直播页路由)。'); diff --git a/scripts/remove-live-for-audit.sh b/scripts/remove-live-for-audit.sh new file mode 100644 index 0000000..60777ae --- /dev/null +++ b/scripts/remove-live-for-audit.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +# 在服务器后端项目根目录执行(与 public、application 同级) +set -euo pipefail +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +cd "$ROOT" +echo "项目目录: $ROOT" +php scripts/remove-live-for-audit.php "$@" +if [[ "${1:-}" == "--apply" ]]; then + echo "" + echo "下一步:" + echo " 1. 后台 Shopro → 装修 → 打开「使用中」的模板,随便保存并发布" + echo " 2. 小程序 chufang-goods 重新编译上传(pages.json 已去掉直播页)" +fi