
1. 旅游定制不是选套餐需求拆解先把人搞明白去年有个做旅行社的朋友找我说想做一个微信小程序。一开始需求写得很简单用户选个目的地我们推几条线路用户下单。听起来跟携程、马蜂窝上的标准产品没区别但聊到第二轮他就抛了个真问题他们社里大部分业务来自企业团建、家庭结伴游、小众深度游这类用户根本不满足于现成线路而是我要去川西但不想走传统环线最好能骑马进山住一晚藏寨吃一顿地道藏餐——这种需求没法靠固定SKU解决。所以这篇内容的核心价值就在这微信小程序不是重点定制才是难点。小程序只是载体真正要说清楚的是——怎么把一堆碎片化、口语化的旅行需求变成一条可执行、可报价、可售卖的线路再通过微信生态里的小程序把它落地。如果你正在做旅游类小程序、OTA类平台或者手里有个想做定制化业务的旅行社客户这篇从头到尾都是实操过程包括需求拆解、技术选型、核心模块实现、踩坑记录和优化思路。没有代码基础的人也能看懂大方向想动手的人可以直接抄部分设计。我先把需求拆解的结果列出来。当时我们把旅游线路定制拆分成了四层目的地偏好用户想去哪。这是最粗粒度决定了后续所有推荐的地理范围。行程节奏天数、出发时间、每天移动距离的容忍度。同样是云南5天和12天的玩法完全不是一回事。体验标签美食、徒步、摄影、亲子、人文、轻奢住宿等这里是最能体现定制价值的部分。预算区间与成团人数决定了交通方式包车还是拼车、住宿等级、领队配置。这四层不是简单堆在界面上让用户填而是每层之间有关联逻辑。比如用户选了摄影标签那目的地选项里就不该推城市商圈游选了亲子行程节奏就得自动降低每日车程住宿偏好偏亲子酒店。定制系统本质上是一套规则引擎不是一堆筛选框。这套拆解做完我和朋友都松了一口气。因为后面所有页面设计、接口设计、算法设计都围绕这四层数据模型展开。如果一开始就奔着功能多去做做出来的只会是一个普通订票工具。2. 技术选型我为什么从原生小程序切到uniapp接下来是技术选型。这个环节我踩了一个比较明显的坑值得单独拿出来说。2.1 原生开发到一半发现不对劲项目启动初期我图省事直接用微信小程序原生语法开发WXML WXSS JS 那一套。原因很简单团队里两个人对原生语法最熟而且小程序这种轻量场景原生上手快。做到第三个页面的时候问题开始暴露。我们这个项目除了小程序端后台管理端要用 Vue未来还可能要出 App 端或鸿蒙端。原生小程序代码没法复用到后台管理项目里等于两套代码库、两套接口联调、两套维护成本。更要命的是小程序的模板语法写多了之后状态管理很别扭复杂页面比如定制行程的多步骤表单来回 setData性能和代码可读性都掉得厉害。后来我们做了一个决定切到 uniapp 重新写。原因不是uniapp有多完美而是Vue对团队来说太熟了——后台管理是Vue小程序端也用Vue语法一套人才能通用。这个取舍在热词里正好有对应大到uniapp开发微信小程序 vs android/ios/鸿蒙小到vue项目如何发布微信小程序说明现在很多团队都在做这个方向的选型比较。2.2 uniapp的几项硬性优势针对本项目从实际落地效果来看有五点感受最直接代码复用率线路定制的核心逻辑、表单校验、接口请求封装全部写成公共模块。以后要做App端样式和业务代码大部分能迁过去。状态管理顺手uniapp里能用Vuex/Pinia管理跨页面状态比原生的全局变量和事件传参稳太多。UI库可用性uni-ui、uView等组件库帮了大忙单选框、日期选择器、步骤条直接拿来改省了造轮子的时间。条件编译碰到平台差异化需求用条件编译写平台分支不用整个项目分叉。社区流量遇到问题搜uniapp xxx基本都有答案比搜原生小程序踩坑经验容易得多。但注意uniapp不是银弹。如果你只做微信小程序、团队又完全不熟Vue那没必要硬上uniapp。我见过有人用uniapp跑原生项目结果在生命周期差异上卡了半个月。选型永远看团队和长期维护成本不看社区热度。2.3 开发工具链和真机调试项目跑起来之后有几个工具链问题是必然遇到的HBuilderX我们用的是HBuilderX进行编译和真机调试。这个工具对Vue语法的支持比微信开发者工具好但调试H5端时偶尔有白屏问题需要刷新几次。微信开发者工具作为最终预览和上传的工具基本上天天开着。记得把本地设置里的自动热重载勾上不然改一行代码要手动编译效率极差。热词里微信小程序能做热刷新吗就是这个问题的真实写照——原生小程序开发时热刷新支持不完美uniapp的HBuilderX热重载体验好很多。版本管理微信开发者工具有两套环境——测试号和自己注册的AppID。早点注册正式AppID很多接口比如支付、获取手机号测试号根本调不通。3. 线路定制引擎表单收集、规则匹配与推荐生成的完整链路这是整个项目的核心。我详细说明这个模块是怎么从零开始设计和实现的中间包括为什么这么设计的逻辑拆解。3.1 第一步多步骤表单设计——怎么把想旅游变成结构化数据用户不是来填写表格的是来描述愿望的。所以我们把定制页面做成了一个含有多个步骤的表单流程每一步对应需求拆解中的一层。具体来说一共五个步骤第一步选择目的地类型国内/国外、热门城市列表、地图选点第二步选择出行日期和天数日期范围组件 滑块选天数第三步选择同行人类型单人、情侣、亲子、朋友结伴、企业团建——这一步会影响后面的推荐策略第四步勾选体验标签美食、摄影、徒步、景点打卡、人文历史、购物、夜生活、亲子娱乐、轻奢住宿、温泉等第五步填写预算区间和特殊需求备注自由文本比如不想去网红点、需要轮椅通道这个顺序是刻意设计的。前几步让用户越做越轻松如果第一步就让用户填一堆标签很多人会在中途流失。一个生活化的类比你去餐厅点菜服务员先问几位、有没有忌口再递菜单没人一上来就问你对菜品的镬气有什么要求。3.2 第二步规则匹配——推荐算法不能只靠等于拿到表单数据后推荐逻辑分三层第一层硬性过滤。目的地类型、出行日期、预算区间这三个是硬条件。比如用户预算上限2000系统绝对不会给推单日就1800的定制方案。代码层面就是简单的filter但要注意边界值——预算区间用左闭右开区间存否则2000预算会把恰好2000的方案排除掉这是个很隐性但容易惹用户投诉的问题。第二层标签权重匹配。这里我没有用复杂的机器学习模型项目初创期没必要而是设计了一套可解释的加权评分机制。每条线路在后台录入时都打上了标签比如// 线路标签权重示意 { lineId: line_88912, title: 稻城亚丁摄影徒步6日, tags: [摄影, 徒步, 自然风景, 人文], tagWeights: { 摄影: 3, 徒步: 2, 自然风景: 2, 人文: 1, 美食: 0.5 } }用户的标签选择记为1未选记0计算线路得分function calculateScore(line, userTags) { let score 0; for (const tag of userTags) { if (line.tags.includes(tag)) { score line.tagWeights[tag] || 1; } } return score; }第三层协同过滤的轻量变体。当用户数量和下单数据积累到一定量后我们加了一个非常轻量的相似人群扩量逻辑找到历史订单中与当前用户标签重合度超过60%的订单把这些订单中用户选择的线路提高权重作为补充推荐。不需要维护太复杂的模型存一张用户-标签矩阵就行。3.3 第三步线路结果页与行程编辑推荐结果页也不是简单展示固定线路。用户点进某条推荐线路后能看到一个行程概览页包括每日行程时间轴Day 1 / Day 2 ... 每个节点有地点、交通方式、餐饮安排可编辑的时间轴节点用户可以拖拽调整顺序或者点击替换按钮换成同类型备选右下角浮动按钮更新报价点击后根据修改结果实时重新计算价格这里有个技术实现细节值得说一下时间轴节点的拖拽排序当初在uniapp里用movable-area做过体验不好后来改用简单的上下移按钮反而稳定。小屏幕上的复杂拖拽交互在微信生态里要尤其谨慎用户误触概率太高不如把操作拆成明确的上移/下移/删除/替换四个按钮。3.4 报价实时计算逻辑报价这块是旅行社老板最关心的。我们的做法是给每个线路定义基础单价 浮动因子结构基础价根据线路的名称、天数、标准住宿等级确定一个基准价浮动因子节假日、旅游旺季、成团人数、定制标签实时报价的伪代码function calculatePrice(line, params) { let base line.basePrice; let factor 1.0; if (params.isHoliday) factor * 1.3; // 节假日上浮30% if (params.groupSize 4) factor * 0.9; // 四人以上打九折 if (params.customTags.length 3) factor * 1.1; // 高端定制加分 return Math.round(base * factor); }算完之后页面上会用大字号展示预估价格并注明最终价格以客服确认单为准。旅行产品的实时报价不能直接锁定成订单价格因为机票、酒店的实时库存波动太大页面上的价格更多是建立用户心理预期所以文案里一定加上以客服确认单为准不然纠纷很多。4. 请求封装与接口设计一次封装管三年维护热词里有一条微信小程序 请求封装这几乎是每个小程序开发者必碰的活。在我们这个旅游定制项目里请求层做得好不好直接关系到后续迭代效率。4.1 为什么不能直接用wx.request小程序原生提供的wx.request能用但有几个硬伤没有统一的拦截器机制每个页面都要写loading和错误处理登录态过期、token刷新这些逻辑没法集中处理接口返回结构不规范的话出错信息没法统一展示没有请求取消机制页面跳走之后请求回调还在执行容易触发setData报错在uniapp里官方推荐用uni.request但同样存在上述问题。所以我们的做法是基于Promise封装一个request工具模块。4.2 封装设计思路与核心代码封装的核心目标就六个字统一入口集中处理。// utils/request.js const BASE_URL https://api.example.com/v1; function request(path, options {}) { const token uni.getStorageSync(token); return new Promise((resolve, reject) { uni.request({ url: BASE_URL path, method: options.method || GET, data: options.data || {}, header: { Content-Type: application/json, Authorization: token ? Bearer token : , ...options.header }, timeout: options.timeout || 15000, success: (res) { // 业务约定code0表示成功非0表示业务错误 if (res.statusCode 200 res.data.code 0) { resolve(res.data.data); } else if (res.statusCode 401) { // 登录过期处理 uni.removeStorageSync(token); uni.redirectTo({ url: /pages/login/login }); reject(new Error(登录已过期)); } else { uni.showToast({ title: res.data.msg || 请求失败, icon: none }); reject(new Error(res.data.msg || error)); } }, fail: (err) { uni.showToast({ title: 网络异常请稍后重试, icon: none }); reject(err); } }); }); } export const get (path, data) request(path, { method: GET, data }); export const post (path, data) request(path, { method: POST, data });这段封装有几个细节值得展开401处理旅游定制类接口涉及用户登录态登录过期是个高频场景。集中处理401统一跳转登录页避免每个页面各自判断、各自跳转代码会整洁很多。业务码与HTTP码分离很多团队前期图省事直接用HTTP状态码表达业务结果结果就是后端返回200但data里带着错误信息前端还得多处判断。我们约定code0为成功、非0为业务错误统一在封装层拦截页面代码里就能少写大量if-else。网络错误提示这个封装在fail回调里统一弹toast。这样页面里调用接口时基本只需要关心成功的数据处理错误分支都收敛了。4.3 缓存策略为什么只缓存四大类数据旅游类小程序的缓存设计比普通工具类复杂因为数据既要新鲜又要快。我们最终定了四条缓存规则目的地列表基本不变缓存7天线路标签库运营偶尔调整缓存24小时推荐线路列表依赖实时库存和季节活动不缓存每次请求定制表单草稿本地持久化用户在填写过程中随时退出下次进入自动恢复热词里微信小程序设置缓存时间这个关键词说明很多人搞不清缓存时长怎么设。我认为核心原则是数据变更频率决定缓存时间不是拍脑袋定7天或24小时。目的地列表可能一季都不变没必要每次启动都拉推荐线路包含实时价格缓存了反而会让用户看到过时报价干脆不缓存。实现上用uniapp的uni.setStorageSync和uni.getStorageSync附带一个时间戳校验function getCache(key, maxAge) { const cached uni.getStorageSync(key); if (!cached || !cached.timestamp) return null; if (Date.now() - cached.timestamp maxAge) { uni.removeStorageSync(key); return null; } return cached.data; } function setCache(key, data) { uni.setStorageSync(key, { data: data, timestamp: Date.now() }); }这样封装之后页面里调目的地接口时先读缓存没有再请求网络体验和服务器压力都兼顾了。4.4 接口层设计把定制流程拆成几个原子接口后端接口设计上我们没有做一个大而全的submitCustomTravelPlan接口而是拆成多个原子接口POST /api/custom/draft保存定制表单草稿POST /api/custom/recommend传入表单数据返回推荐线路列表GET /api/custom/line/{id}获取单条线路详情含每日行程POST /api/custom/plan提交定制方案用户编辑后的最终方案POST /api/custom/order对定制方案下单这样的好处是前端每个步骤都能独立调试、独立上报错误不会因为一步填错导致整个流程崩溃。而且草稿接口的存在让填写到一半退出的用户也能被保存下来对转化率帮助很大。5. 微信小程序独有的坑导航栏高度、缓存时序和组件怪癖做微信小程序一定绕不开一些平台特有的坑。热词列表里至少有五条直接对应这个主题——微信小程序顶部导航栏高度、微信小程序设置缓存时间、微信小程序单选框、微信小程序 list-builder、h5唤起微信小程序链接无法访问。5.1 自定义导航栏的适配问题旅游定制小程序的核心页面定制表单、线路详情都用了自定义导航栏这样视觉更好看、按钮位更灵活。但自定义导航栏有个经典问题顶部安全区域高度在不同的手机上不一样。iPhone X系列有底部小黑条、刘海屏有顶部传感器区域安卓各种厂商的全面屏手势区域又各不相同。如果导航栏高度写死到了真机上就是内容上下错位、按钮被状态栏挡住。最终方案使用uniapp提供的uni.getSystemInfoSync()获取状态栏高度动态计算导航栏高度。const systemInfo uni.getSystemInfoSync(); const statusBarHeight systemInfo.statusBarHeight || 20; // px const navBarHeight 44; // 默认导航栏内容高度页面最外层容器预留padding-top: statusBarHeight navBarHeight按钮定位按这个高度计算。这个代码在新机型和旧机型上都要测光在开发者工具里看是没用的。5.2 缓存时序getStorageSync拿到undefined的真相热词里微信小程序设置缓存时间我前文已经讲了策略但还有一个更隐蔽的时序坑。在uniapp里如果用uni.getStorageSync(key)去拿一个不存在的key返回的是空字符串而不是null。很多新人写if (uni.getStorageSync(token)) { ... }初看没问题但如果你要判断缓存是否真的设置了空字符串和值为空的语义会混淆。建议统一用封装函数判断function hasCache(key) { const value uni.getStorageSync(key); return value ! value ! null value ! undefined; }还有更坑的某些版本的uniapp在真机上对Storage的写入是异步的理论上会有写入后立即读取读不到的偶发情况。我们最终在新版本用uni.setStorage异步带回调处理关键数据写入后的后续逻辑避免竞态。这个比较小众但真遇到了会让人怀疑人生。5.3 单选/多选组件原生view模拟还是用UI库热词里有微信小程序单选框可见这是个高频踩到点。旅游定制的体验标签选择本质上是一个可以多选的标签云不是严格的单选框。我尝试过三种方案原生checkbox样式老气定制样式还得改组件的原生阴影非常麻烦uView的checkbox能用但标签多时布局不够灵活自己用view实现标签云成本最低动态渲染一个flex-wrap容器点击切换active状态最后选了第三种代码看着平平无奇view classtag-list view v-fortag in tags :keytag.id classtag-item :class{ active: selectedTags.includes(tag.id) } taptoggleTag(tag.id) {{ tag.name }} /view /view好处有两个一是样式完全可控想做成圆形、多行、带颜色过渡都行二是响应式好手机屏幕宽度不够时flex-wrap自动换行不用操心原生组件的适配问题。凡是看起来像标签云的选择场景我强烈建议不要用radio或checkbox直接view模拟。5.4 H5唤起小程序失败链接规范和场景值热词里h5唤起微信小程序链接无法访问也值得一说。旅游定制项目的分享功能很重用户把定制好的线路分享给同伴同伴如果从H5页面点击打开小程序需要走微信的URL Link或URL Scheme机制。这里最容易踩的坑有两处URL Link必须在微信公众平台后台配置好域名白名单而且域名不能带路径参数很多团队在这里卡半天结果发现是域名配置漏了唤起小程序时一定要带scene参数用来标记来源。我们所有从H5唤起小程序的链接都带上了sceneshare_h5小程序端通过App.onShow(options.query.scene)解析来源再做对应的路由跳转。如果省略scene用户从H5跳进来只会落在首页体验非常脱节。这个细节在技术上不难但非常影响转化。5.5 list-builder列表性能调优热词里有微信小程序 list-builder这大概是社区里一个列表构建库的话题。我们在定制结果页的推荐列表上也有过性能问题总结下来的经验是列表项不要一次性全部渲染用分页加载或虚拟列表图片懒加载必须开启lazy-load属性直接加在image上公共列表容器的滚动区域独立设置不要让页面整体滚动的同时列表内部还滚动会出现滚动冲突不要用太深的v-for嵌套时间轴里套评分、评分里套标签改动一次setData数据量就很大把这些规则落到项目里之后低端安卓机上列表页的卡顿明显改善了。6. 从能跑到好用支付、审核和上线后的迭代思路主体功能全部实现之后还有三个绕不开的坎支付接入、微信审核、上线后的数据迭代。每一个我都踩过坑把这些经验放出来比代码本身更值钱。6.1 支付微信支付如何接入以及支付宝渠道的问题热词里有一条微信小程序可以加入支付宝支付渠道吗如何设计我直接说结论微信小程序生态里只能使用微信支付不能直接在微信小程序内唤起支付宝支付。若确实需要支付宝只能引导用户跳转到支付宝App或者走H5支付 浏览器中转这类折中方案但微信官方对这类跳转的限制比较严格不建议依赖。微信支付的接入流程是这样的申请微信支付商户号小程序主体和商户号主体必须是同一个后端统一下单后端调微信支付接口获取prepay_id小程序端调起支付uni.requestPayment({ provider: wxpay, timeStamp: res.timeStamp, nonceStr: res.nonceStr, package: res.package, signType: MD5, paySign: res.paySign, success: (res) { ... }, fail: (err) { ... } });注意点timeStamp是字符串类型后端传过来是什么就传什么不要自己转数字再拼回去很容易出现签名错误。我们在这个坑上花了一个下午最后发现就是类型转换导致签名串和官方算法不一致。6.2 微信审核旅游类目需要额外资质旅游类小程序和个人开发者的普通工具不一样在微信公众平台提交审核时类目选择旅游会要求提供《旅行社业务经营许可证》等资质文件。这个如果没提前准备审核会在一个工作日左右被驳回而且驳回理由写得很笼统你搜半天才知道是资质问题。建议的流程是先注册社会组织主体或企业主体账号不要用个人主体提前准备营业执照、旅行社资质等扫描件在小程序后台先配置好类目和服务范围再提审测试账号准备一套完整的可用数据不能是写着测试的假数据审核人员点开页面时如果看到空数据或报错大概率被打回我们第一次提审就被打了原因是定制表单页面在未登录状态下无法使用审核人员要看到完整的流程必须是可直接演示的状态。后来我们在未登录状态下也能浏览线路和标签仅在下单时要求登录才过审。6.3 上线后的迭代用户行为告诉我AI推荐该怎么做上线后第一个月除了常规的bug修复我关注的三个核心指标是定制表单完成率用户从进入定制页到完成提交的比例推荐线路点击率推荐结果里被点进详情页的占比定制方案下单转化率从方案确认页到支付完成的比例数据出来之后有几个发现直接驱动了后续迭代第一完成率只有34%一半以上的用户卡在第三步同行人类型。后来把第三步和第二步合并了出行日期、天数和同行人放一个页面完成率提到了51%。这验证了前面说的步骤越少越好的设计原则。第二推荐线路点击率分布极不均匀前两条线路占了80%的点击。这不是用户的问题是我们评分算法把分数拉得太开了。后来给推荐数量做了限制最多展示5条并在推荐理由里说明为什么推荐这条线路用标签匹配的逻辑生成一段文案点击率反而涨了。第三下单转化率只有7%但客服系统的咨询量翻了倍。用户对定制方案心里没底要反复问清楚才敢付钱。后来在方案确认页加入了微信客服按钮引导用户先加客服微信再下单。这个改动不算技术活但对交易类产品来说极其重要。6.4 一个续篇的方向从定制走向邀约共创项目稳定运行后我们开始规划下一个版本让用户不只是一个表单的填写者而是可以和旅行规划师在站内直接沟通、共同编辑一份行程。技术上的核心是从静态方案确认升级为共享编辑状态预计会引入类似于协作文档里操作记录的能力。这部分工作还在进行中将来跑通后再专门写一篇。最后说点实在的我最大的感受是旅游线路定制小程序的难点从来不在某个单一技术上而在需求建模和流程设计。技术选型、请求封装、组件适配这些都是可以靠经验快速解决的真正的分水岭是你愿不愿意把用户当成人而不是流量去理解——他们不是要一个筛选器而是想要一个懂旅行的助手在耳边给建议。如果你现在正好在做一个类似的项目我建议第一步别急着写代码。找三个真实用户让他们用纸面原型走一遍定制流程你会收获比任何技术调研都多的信息量。我当时就是拿着便利贴在朋友公司楼下堵了五个准备去旅游的顾客聊了一下午才把表单从七步砍成五步。这个投入比后期改架构划算得多。