vr-shopxo-uniapp/docs/city-based-diy-template-pla...

21 KiB
Raw Permalink Blame History

基于地理位置的城市化 DIY 装修模板自动加载方案

文档版本v1.0.0-draft
创建日期2026-06-10
作者sileya-ai
状态:草稿 - 待评审 末次更新2026-06-28部分已实现详见末尾"更新历史"


一、背景与目标

1.1 项目背景

随着业务扩展到多个城市,不同城市的用户对首页内容有不同的展示需求:

  • 厦门用户希望看到本地热门票务活动和场馆推荐
  • 上海用户希望看到上海场次的演出信息
  • 以此类推,每个城市可能有其专属的营销活动和商品推荐

现有系统仅支持单一默认首页模板,无法满足城市化的精细运营需求。

1.2 项目目标

# 目标 说明
1 城市化首页 根据用户地理位置自动加载对应城市的 DIY 装修模板
2 零数据库改动 利用现有 DIY 模板表的 name 字段实现命名约定
3 自动兜底 当特定城市模板不存在时,自动 fallback 到默认模板
4 前端零改动 复用现有位置选择和坐标传递机制,无需修改 UniApp 前端代码
5 最小后端改动 仅修改 index/index 控制器逻辑,不新增数据表

1.3 预期效果

用户定位厦门lng:118.11, lat:24.58
    ↓
自动加载 home_350200 模板(厦门专属首页)
    ↓
展示厦门热门票务、场馆推荐等本地化内容

用户定位上海
    ↓
自动加载 home_310000 模板(上海专属首页)
    ↓
展示上海场次演出信息

用户定位未知/坐标缺失
    ↓
自动加载 home_default 模板(默认兜底)
    ↓
展示通用内容

二、需求分析

2.1 功能需求

需求ID 需求描述 优先级
F01 后端根据用户 GPS 坐标自动匹配对应城市的 DIY 模板 必须
F02 DIY 模板通过命名约定name 字段)区分不同城市 必须
F03 当特定城市模板不存在时,自动加载默认模板作为兜底 必须
F04 前端自动传递用户坐标到后端 API 必须
F05 前端位置选择器切换城市后,触发首页模板刷新 必须

2.2 非功能需求

需求ID 需求描述 优先级
N01 模板匹配逻辑应在 100ms 内完成 必须
N02 默认模板必须存在,确保兜底可用 必须
N03 城市坐标数据必须完整Region 表 lng/lat 字段) 必须
N04 模板切换应有良好的用户体验(骨架屏过渡) 建议

三、技术约束

3.1 现有系统架构

┌─────────────────────────────────────────────────────────────┐
│                      UniApp 前端                              │
├─────────────────────────────────────────────────────────────┤
│  1. App.vue - 全局数据配置                                   │
│     - choice_user_location_init() 获取缓存的位置             │
│     - request_params_handle() 自动附加 user_lng/user_lat     │
│                                                             │
│  2. choice-location 组件 - 位置选择器                        │
│     - @onBack 事件返回 { name, lat, lng, status }            │
│     - user_back_choice_location() 处理回调,触发 init()      │
│                                                             │
│  3. index/index.vue - 首页                                    │
│     - init() 调用 /api.php?s=index/index                     │
│     - data_mode == 3 时渲染 DIY 组件                          │
│     - component-diy 接收 { config, id } 渲染模板              │
└─────────────────────────────────────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────┐
│                      后端 API                                 │
├─────────────────────────────────────────────────────────────┤
│  1. index/index 控制器                                       │
│     - 返回首页数据,含 data_list含 DIY 模板)               │
│     - 接收参数user_lng, user_lat, city_id                  │
│                                                             │
│  2. GeoCityService::FindNearestCityId(lng, lat)             │
│     - Haversine 公式计算最近城市                             │
│     - 依赖 Region 表的 lng/lat 坐标数据                      │
│     - 已被 VR Ticket 插件使用(成熟方案)                    │
│                                                             │
│  3. DIY 模板表 (shopxo_diy)                                  │
│     - id: 主键                                              │
│     - name: 模板名称(用于命名约定)                         │
│     - describe: 描述                                         │
│     - config: DIY 配置 JSON                                 │
└─────────────────────────────────────────────────────────────┘

3.2 约束条件

# 约束 说明
C01 不能新增数据表 必须使用现有 DIY 模板表
C02 不能修改数据库结构 现有字段不变,仅改变使用方式
C03 前端零改动 UniApp 代码保持不变
C04 复用现有服务 使用已有的 GeoCityService::FindNearestCityId()
C05 必须保留默认模板 home_default 必须存在作为兜底

3.3 依赖关系

依赖 说明 状态
Region 表坐标数据 用于 Haversine 计算最近城市 需要确认数据完整性
GeoCityService 坐标转城市 ID 服务 已存在VR Ticket 使用)
DIY 模板 name 字段 用于模板匹配 已存在

四、设计方案

4.1 命名规则设计

4.1.1 命名格式

类型 格式 示例 说明
默认模板 home_default home_default 全国通用兜底模板,必须存在
城市模板 home_{城市ID} home_350200 城市 ID来自 Region 表的 id 字段)
拼音别名(可选) home_{拼音} home_xiamen 便于运营记忆,可选扩展

4.1.2 城市 ID 参考

城市 城市ID 模板名称
全国默认 - home_default
厦门 350200 home_350200
上海 310000 home_310000
北京 110000 home_110000
广州 440100 home_440100
深圳 440300 home_440300

4.2 后端逻辑设计

4.2.1 修改范围

文件 修改内容 风险等级
app/index/controller/Index.php 修改 index() 方法,增加模板匹配逻辑
app/service/DiyService.php 新增 GetTemplateByName() 方法(如不存在)

4.2.2 流程图

┌─────────────────────────────────────────────────────────────┐
│                    index/index 控制器                        │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  1. 接收请求参数                                             │
│     - user_lng (经度)                                        │
│     - user_lat (纬度)                                       │
│     - city_id (可选显式城市ID)                             │
│                                                              │
│  2. 确定模板名称                                             │
│     │                                                       │
│     ├─── 优先city_id 显式传入 ──→ template_name = 'home_{city_id}'
│     │                                                       │
│     └─── 其次user_lng + user_lat ──→                       │
│              │                                              │
│              ↓                                              │
│         GeoCityService::FindNearestCityId(lng, lat)         │
│              │                                              │
│              ↓                                              │
│         city_id = 350200                                    │
│              │                                              │
│              ↓                                              │
│         template_name = 'home_350200'                       │
│                                                              │
│  3. 查询模板                                                 │
│     │                                                       │
│     ├─── 找到模板 ──→ 返回模板数据                           │
│     │                                                       │
│     └─── 未找到 ──→ template_name = 'home_default'          │
│                        │                                    │
│                        ↓                                    │
│                   查询默认模板                               │
│                        │                                    │
│                        ↓                                    │
│                   返回默认模板数据                           │
│                                                              │
│  4. 返回响应                                                 │
│     - data_list: { id, name, describe, config }              │
│     - data_mode: 3 (DIY 模式)                                 │
└─────────────────────────────────────────────────────────────┘

4.2.3 伪代码实现

<?php
// app/index/controller/Index.php

namespace app\index\controller;

use app\common\controller\BaseIndex;
use app\service\GeoCityService;
use app\service\DiyService;

class Index extends BaseIndex
{
    /**
     * 首页数据
     */
    public function index()
    {
        // 1. 确定模板名称
        $template_name = 'home_default';  // 默认值
        
        // 2. 尝试使用显式 city_id
        $city_id = $this->data['city_id'] ?? 0;
        
        // 3. 如果没有 city_id尝试坐标反推
        if (empty($city_id) && !empty($this->data['user_lng']) && !empty($this->data['user_lat'])) {
            $city_id = GeoCityService::FindNearestCityId(
                $this->data['user_lng'],
                $this->data['user_lat']
            );
        }
        
        // 4. 拼接模板名称
        if (!empty($city_id)) {
            $template_name = 'home_' . $city_id;
        }
        
        // 5. 查询模板
        $template = DiyService::GetTemplateByName($template_name);
        
        // 6. 未找到则使用默认模板兜底
        if (empty($template)) {
            $template = DiyService::GetTemplateByName('home_default');
        }
        
        // 7. 组装返回数据
        $result = [
            'data_mode' => 3,  // DIY 模式
            'data_list' => $template,
            // ... 其他首页数据
        ];
        
        return $this->success($result);
    }
}
<?php
// app/service/DiyService.php

namespace app\service;

use think\facade\Db;

class DiyService
{
    /**
     * 根据模板名称获取模板详情
     * 
     * @param string $name 模板名称
     * @return array|null
     */
    public static function GetTemplateByName($name)
    {
        return Db::name('diy')
            ->where('name', $name)
            ->where('is_enable', 1)
            ->find();
    }
    
    /**
     * 根据模板ID获取模板详情已有方法
     * 
     * @param int $id 模板ID
     * @return array|null
     */
    public static function GetDetail($id)
    {
        return Db::name('diy')
            ->where('id', $id)
            ->where('is_enable', 1)
            ->find();
    }
}

4.3 前端逻辑(现有代码,无需修改)

4.3.1 坐标自动传递

// App.vue - request_params_handle() 中的现有逻辑
let user_location = this.choice_user_location_init();
if(user_location != null && user_location.status == 1) {
    query_str += '&user_lng=' + (user_location.lng || '');
    query_str += '&user_lat=' + (user_location.lat || '');
}

4.3.2 位置选择回调

// index.vue - 现有回调处理
user_back_choice_location(e) {
    // 重新加载数据,自动带上新坐标
    this.init();
}

4.3.3 DIY 渲染

<!-- index.vue - 现有渲染逻辑 -->
<component-diy 
    :propValue="data_list.config" 
    :propDataId="data_list.id" 
    :propKey="random_value"
    @onLocationBack="user_back_choice_location">
</component-diy>

五、数据要求

5.1 DIY 模板数据

字段 类型 约束 说明
id INT 主键 模板ID
name VARCHAR(100) 唯一索引 模板名称命名约定home_{城市ID} 或 home_default
describe VARCHAR(255) - 模板描述
config JSON - DIY 配置 JSON
is_enable TINYINT 默认1 是否启用
is_home TINYINT - 是否首页模板

5.2 地区坐标数据Region 表)

字段 类型 约束 说明
id INT 主键 地区ID用于模板命名
name VARCHAR(50) - 地区名称
level TINYINT - 级别2=城市)
pid INT - 上级ID
lng DECIMAL(10,6) > 0 经度
lat DECIMAL(10,6) > 0 纬度

5.3 模板配置示例

// home_default默认模板
{
    "id": 1,
    "name": "home_default",
    "describe": "全国默认首页模板",
    "config": {
        "header": { ... },
        "diy_data": [ ... ],
        "footer": { ... }
    }
}

// home_350200厦门模板
{
    "id": 6,
    "name": "home_350200",
    "describe": "厦门城市首页模板",
    "config": {
        "header": { ... },
        "diy_data": [ ... ],
        "footer": { ... }
    }
}

六、运营指南

6.1 创建城市模板步骤

  1. 登录管理后台
  2. 进入 DIY 装修 → 模板管理
  3. 点击「新增模板」
  4. 设置模板名称(如 home_350200
  5. 进行页面装修
  6. 保存模板

6.2 命名规范

模板用途 命名格式 必须存在
默认兜底 home_default 必须
厦门 home_350200 可选
上海 home_310000 可选
北京 home_110000 可选

警告home_default 模板必须存在,否则当城市模板不存在时将无法显示首页内容。

6.3 模板预览

管理后台应提供「按模板名称预览」功能方便运营确认不同城市的模板效果。目前采用后台管理界面的“DIY 装修模板”作为解决方案,用户手动创建、设置多个城市的模板并且进行预览。需要修改的时候手动修改管理对应城市的首页活动界面。未来如果需要方便管理,再考虑做一个集成界面用于快速切换。


七、错误处理与兜底机制

7.1 兜底场景

场景 处理方式
用户未授权位置权限 使用默认模板 home_default
坐标解析失败 使用默认模板 home_default
城市模板不存在 使用默认模板 home_default
默认模板也不存在 返回错误提示「请配置默认首页模板」
Region 表缺少坐标数据 使用默认模板 home_default

7.2 错误日志

后端应记录以下日志便于排查:

  • 模板匹配失败原因
  • 坐标转城市失败原因
  • 默认模板 fallback 记录

八、测试计划

8.1 单元测试

测试项 测试内容 预期结果
T01 传入厦门坐标 (118.11, 24.58) 返回 home_350200 模板
T02 传入上海坐标 (121.47, 31.23) 返回 home_310000 模板
T03 传入坐标但模板不存在 返回 home_default 模板
T04 不传坐标 返回 home_default 模板
T05 显式传入 city_id=350200 返回 home_350200 模板
T06 默认模板不存在 返回错误

8.2 集成测试

测试项 测试内容 预期结果
I01 UniApp 授权位置后打开首页 显示对应城市模板
I02 切换位置选择器到上海 首页刷新为上海模板
I03 未授权位置打开首页 显示默认模板
I04 清除位置缓存后打开首页 显示默认模板

8.3 边界测试

测试项 测试内容 预期结果
B01 传入无效坐标 (0, 0) 使用默认模板
B02 传入超出中国范围坐标 使用默认模板
B03 并发请求不同城市 各自返回正确模板
B04 模板 config 为空 显示空页面或错误提示

九、部署说明

9.1 后端部署

  1. 备份 app/index/controller/Index.php
  2. 修改 index() 方法,增加模板匹配逻辑
  3. 检查 DiyService 是否有 GetTemplateByName() 方法,如无则新增
  4. 测试 本地环境验证功能
  5. 部署 到生产环境

9.2 配置检查清单

# 检查项 状态
1 Region 表坐标数据完整
2 存在 home_default 模板
3 GeoCityService 正常工作
4 前端坐标传递正常

9.3 回滚方案

如出现问题,可通过以下方式回滚:

  1. 恢复 Index.php 备份文件
  2. 临时删除城市模板,仅保留 home_default

十、相关文档

文档 路径 说明
VR票务城市筛选API vr-ticket-city-filter-api.md 现有城市筛选方案参考
地图API配置 map-api-key-configuration.md 地图服务配置说明
VR票务集成规划 vr-ticket-integration-plan.md VR票务相关功能规划

十一、版本历史

版本 日期 作者 说明
v1.0.0-draft 2026-06-10 sileya-ai 初始版本

十二、待确认事项

  • 后端是否有 DiyService::GetTemplateByName() 方法?
  • index/index 控制器修改权限?
  • Region 表坐标数据完整性检查?
  • home_default 模板是否已存在?
  • 是否需要支持拼音别名(如 home_xiamen

十三、联系方式

  • 作者sileya-ai
  • 技术支持vr-shopxo 开发组

〇、更新历史

2026-06-28 部分修正

关于本方案"前端零改动"假设§1.2.4、§3.2 C03、§4.3

⚠️ 本假设已被打破。前端已做大量改动以支持位置自动获取 + 城市反查。详细实现见 diy-home-location-work-record-20260628.md

已实现的前端改动2026-06-28

  1. App.vue:get_user_location 重写5 条件守护 + fail 占位 + onLocationReady 事件)
  2. pages/index/index.vue 监听事件 + 重新请求首页(让后端按新坐标推荐 DIY 模板)
  3. components/choice-location/choice-location.vue 监听事件 + 城市反查
  4. 完整验证DIY 首页左上角从"当前位置"自动切换为"厦门市"

后端能力依赖(已由另一名同事提供):

  • vr_ticket/geo/city 距离匹配接口(>150km 报 code:-1
  • 业务后端按 URL 里的 user_lng / user_lat 自动推荐对应城市的 DIY 模板

未变的内容

  • 后端控制器逻辑(不在本前端任务范围)
  • 城市中心点数据Region 表)
  • DIY 模板命名约定(home_<city_id> / home_default

为什么打破"前端零改动"假设

  • 原方案假设"用户进入首页前已经选过城市"——但实际场景是"用户首次进首页就要按位置自动推荐"
  • 现有的 get_user_location 有一个长期隐藏 bug:已授权用户后续进首页不会再重新获取位置(守护条件 result == null 把第二次进入挡在外面)
  • 需要前端在拿到新坐标后主动重新请求首页,让后端按新坐标推荐对应 DIY 模板