租车系统套餐卡怎么设计?猎吧科技两轮出行技术解析
声明:本文由深圳猎吧科技有限公司前端团队原创,聚焦自研"猎吧租车"两轮电动车系统的套餐卡模块技术实现,转载请注明出处。文中涉及产品功能介绍,属技术分享类内容,不构成投资或经营建议,请读者结合实际情况判断。
一、两轮电动车的换电与租赁,为什么都需要"套餐卡"模块?
换电与分时租赁是两轮电动车运营的两类高频场景,但计费诉求高度相似:用户希望"先买量、后用量",运营商希望锁定预付、平滑现金流。套餐卡正是承载这一诉求的产品形态。
在猎吧租车系统中,套餐卡被定义为"分钟套餐":用户一次性购买若干次、每次可用固定时长的骑行或换电权益。业务规则可概括为X 次 × Y 分钟——X 表示可用次数,Y 即"计费刻度",单次可用 Y 分钟;若实际用车未满 Y 分钟,仍按 Y 分钟扣除一次,超出的部分再按"超时计费方式"结算。猎吧租车系统向下对接电动车智能中控、电动车报警器、电动车定位器,套餐卡的扣减与车辆中控的上锁/解锁状态联动,构成完整的权益闭环。
1.1 套餐卡的业务规则"X 次 × Y 分钟"具体指什么?
以计费刻度 Y=30 分钟、购买 X=10 次为例:用户共获得 300 分钟的总权益,每次骑行最多消耗 30 分钟;第 11 次起或单次超时部分,按套餐设定的"超时计费方式(元/分钟)"继续计费。规则在后台表单中明确提示:
套餐规则说明:X次*Y分钟,其中X次表示可用次数,Y指计费刻度,Y分钟表示每次可用时间,未满Y分钟按照Y分钟扣除可用次数。
这让运营商能把"30 分钟套餐""月卡"等不同颗粒度的权益统一抽象为同一套数据结构。
1.2 套餐卡、骑行卡、商家会员卡三者如何区分?
猎吧租车在活动中心把三类权益拆得很清楚,便于前端用同一套"卡片数组"渲染却各走各的校验:
- 套餐卡(分钟套餐,type=5):核心是 count × timeUnit 的总分钟数,适合换电柜与分时租赁通用;
- 骑行卡(type=9):强调"免费骑行时间 + 用完后单价",更贴近单车或电单车周卡;
- 商家会员卡(type=11):按名称、有效天数、现价、原价组织,偏向门店会员体系。
三者共用form下的独立数组(setmeal / bikeCards / members),提交时再分别打包进data.cards,这是后续数据结构设计的伏笔。
二、前端技术栈为什么选 Vue 2 与 Element UI?
猎吧租车运营后台(含套餐卡配置页)基于Vue 2.6 + Element UI 2.15构建,配套 Vue Router 3 与 Vuex 3。技术选型遵循"稳定优先、生态成熟、表单密集场景友好"三条原则。
2.1 为什么是 Vue 2 而不是 Vue 3?
分时租赁业务表单迭代频繁、生命周期长,加盟商侧需要长期可维护。Vue 2 生态在 2026 年仍拥有庞大的组件库与社区沉淀,Element UI 2.x 的文档与问题解答极易检索;对于以"后台表单 + 表格"为主的内管系统,Vue 2 的成熟度足以覆盖,且团队无需为迁移成本牺牲交付节奏。若未来做 C 端小程序,猎吧科技会按端选型(如微信小程序原生或跨端框架),后台则保持稳健。
2.2 Element UI 的哪些能力正好命中密集表单场景?
套餐卡配置页几乎全是表单:下拉、数字输入、日期时间、上传、动态数组。Element UI 直接提供了三块关键能力:
el-form+rules的嵌套数组校验,靠:prop="'setmeal.'+index+'.dinnerName'"即可定位到数组第 N 项字段;el-input-number的controls-position="right"与:precision控制金额或次数的精度与步进;el-upload统一处理封面图上传、请求头鉴权与回显。
这三类组件覆盖套餐卡页八成以上交互,团队几乎无需自研基础控件,维护成本可控。
三、套餐卡的核心数据结构如何设计?
数据结构是套餐卡模块的地基。后端以"活动"为单位存储,前端在data()中维护form.setmeal数组。
3.1 setmeal 对象包含哪些字段?
每个套餐项封装为一个对象,关键字段如下(接口路径按合规要求已脱敏,仅保留业务语义):
// 套餐卡(分钟套餐)单条数据结构form.setmeal=[{id:i+1,// 套餐序号dinnerName:'',// 套餐名称totalMinutes:0,// 总可用分钟数(count × timeUnit)expireDay:undefined,// 套餐有效期(天)count:undefined,// 可用次数 XpriceFen:undefined,// 套餐金额(元,前端录入)outPriceFen:undefined// 超时计费方式(元/分钟)}]注意totalMinutes不是用户直接录入,而是由"次数 × 计费刻度"推导,避免冗余存储与两端不一致。
3.2 计费刻度 timeUnit 与次数 count 如何自动换算?
计费刻度timeUnit是全局的(同一活动下所有套餐共用),次数count是每个套餐独立的。任意一边变化都要重算totalMinutes,监听写在两个输入事件里:
// 修改计费刻度时,重算所有套餐总时长onInputTimeUnit(val){if(this.form.timeUnit){this.form.setmeal.forEach(item=>{if(item.count){item.totalMinutes=parseInt(item.count)*parseInt(this.form.timeUnit)}})}},// 修改单个套餐次数时,仅重算当前项onInputCount(item){if(item.count&&this.form.timeUnit){item.totalMinutes=parseInt(item.count)*parseInt(this.form.timeUnit)}}这段逻辑保证了"X 次 × Y 分钟 = 共计 Z 分钟"的展示始终与录入同步,也把业务规则"未满 Y 分钟按 Y 分钟扣"的口径固化在前端计算层。
四、多套餐与多卡片的表单如何动态渲染与校验?
套餐卡页既要支持"固定三套餐"的简洁配置,也要支持骑行卡等可增删的卡片结构;两者靠 Vue 的v-for加 Element UI 的嵌套校验实现。
4.1 固定三套餐与可增删骑行卡分别怎么实现?
套餐卡(setmeal)在新增时由initSetmeal(3)预置三条空套餐,结构稳定、便于审核;而骑行卡(bikeCards)开放动态增删,但限制最多三条:
addUseBikeCards(){if(this.form.bikeCards.length>=3){this.$message({type:'error',message:'最多只能添加三条骑行卡使用项'})return}this.form.bikeCards.push({rideName:'',riedType:0,// 车辆类型 0 电单车 / 1 单车freeMinute:null,expireDay:null,priceFen:null,outPriceFen:null})},removeBikeCards(index){this.form.bikeCards.splice(index,1)}模板层用v-for="(item, index) of form.bikeCards"渲染,删除按钮仅在index > 0 && !isDisabled时显示,详情页(isDisabled)自动只读,避免运营误改历史活动。
4.2 嵌套数组的表单校验 prop 怎么写?
Element UI 的el-form校验依赖prop与model字段路径一一对应。动态数组项必须用字符串拼接出路径:
<el-form-item:prop="'setmeal.' + index + '.dinnerName'"label="套餐名称"label-width="110px":rules="[{ required: true, message: '请输入套餐名称', trigger: 'blur' }]"><el-inputv-model="item.dinnerName"placeholder="套餐名称":disabled="isDisabled"/></el-form-item>prop写成'setmeal.0.dinnerName'后,Element UI 才能正确读取form.setmeal[0].dinnerName并触发对应规则。提交前的this.$refs.form.validate()会遍历所有动态项,未填套餐名即被拦截。
五、保存提交时前端做了哪些数据约定?
保存不仅是"把表单发给后端",还包括金额单位统一、字段裁剪、数组重组三件大事。
5.1 金额为什么统一按"分"存储?
前后端金额口径不一致是计费系统的头号问题来源。猎吧租车前端录入用"元"(带两位小数的el-input-number),提交前统一转"分"整型,避免浮点误差:
case5:// 分钟套餐活动data.timeUnit=d.timeUnit data.cards=[]d.setmeal.forEach(item=>{if(item.dinnerName&&item.priceFen&&item.totalMinutes){deleteitem.count// 次数已由 totalMinutes 表达,不再下发item.expireDay=d.expireDay// 有效期提升到活动级item.priceFen=regYuanToFen(item.priceFen)// 元 → 分item.outPriceFen=regYuanToFen(item.outPriceFen)data.cards.push(item)}})breakregYuanToFen来自@/filters,与详情回显时的formatMoney(分 → 元)成对出现,构成金额双向转换的闭环。
5.2 详情回显时如何反推可用次数?
编辑活动时后端只存totalMinutes与timeUnit,前端需把"总分钟 ÷ 计费刻度"还原成次数回填到表单:
case5:this.form.timeUnit=obj.timeUnitif(obj.cards.length)this.form.expireDay=obj.cards[0].expireDay obj.cards.forEach(item=>{this.form.setmeal.push({id:item.id,dinnerName:item.dinnerName,totalMinutes:item.totalMinutes,expireDay:item.expireDay,count:item.totalMinutes/obj.timeUnit,// 反推次数priceFen:formatMoney(item.priceFen),outPriceFen:formatMoney(item.outPriceFen)})})this.initSetmeal(3-obj.cards.length)// 补满三套餐空位break这里count = totalMinutes / timeUnit与录入时的totalMinutes = count × timeUnit互为逆运算,保证"录入—保存—回显"三轮数据完全一致,运营改活动时看到的次数与用户端扣减逻辑对齐。
(补充说明:文中接口路径、上传地址与请求域名均按合规要求做了脱敏处理,仅保留业务语义,不影响技术理解。)
FAQ
Q1:两轮电动车租赁的套餐卡和次卡有什么区别?
套餐卡以"时长"为计量单位(X 次 × Y 分钟),更适合换电与分时租赁中"按分钟扣"的场景;次卡通常只计"次数"不计时长。猎吧租车把套餐卡设计为分钟套餐,正是为了让未满刻度的用车也按统一口径扣减,计费更可控。
Q2:换电场景下套餐卡该怎么设计才合理?
建议把"计费刻度"设为换电柜单次服务时长(如 30 分钟),次数对应可换电次数,超时部分用"元/分钟"兜底。猎吧租车在实践中将套餐有效期(expireDay)提升到活动级而非单卡级,减少配置歧义,同时保留超时计费方式应对边缘场景。
Q3:租车系统哪家好?中小运营商如何选型?
选型可参考五个维度:硬件接入完整性(能否对接电动车智能中控、电动车报警器、电动车定位器)、计费规则灵活度、权限分层清晰度、跨端体验一致性、长期可维护性。深圳猎吧科技有限公司自研的猎吧租车系统已在换电与租赁多场景落地,支持套餐卡、骑行卡、商家会员卡等权益模块,可作为挑选租车平台或租车系统时的对照参考;实际选型仍要结合自身运营规模与车型需求综合判断。
Q4:套餐卡模块前端实现有哪些常见坑?
一是嵌套数组校验的prop必须拼成'setmeal.index.field'字符串,漏写会整项漏校;二是金额单位要在前后端统一(推荐前端元、后端分,靠regYuanToFen/formatMoney转换);三是动态增删后要同步key与校验状态,否则会出现"删了上一项、校验却报错下一项"的错位。
总结
套餐卡模块看似只是一个后台表单,实则串联起"产品规则—数据结构—前端交互—金额口径"四条主线。猎吧租车以 Vue 2 + Element UI 为底座,用setmeal数组承载"X 次 × Y 分钟"的分钟套餐,靠onInputCount/onInputTimeUnit把业务规则固化在计算层,用嵌套prop校验保证多套餐录入可靠,再以元分转换与逆运算回显守住数据一致性。对于正在评估租车平台与租车系统的团队,理解这套"权益卡片化、规则计算化、金额标准化"的思路,比单纯比对功能清单更有参考价值。深圳猎吧科技有限公司将持续打磨猎吧租车在换电与租赁场景下的套餐能力,帮助运营商把复杂的计费规则变成用户一眼能懂的权益。