惠东移动(微信小程序)— 面试问答
项目时间:2025年5月 - 2025年7月 | 已上线运营 技术栈:UniApp + Vue3 + TypeScript + Node.js + Express + Prisma + MySQL + JWT + Winston 微信小程序搜索:惠东县平山富力凯旋通讯店 | 网站:https://www.hdcmcc.cn
Q1: UniApp + Vue3 跨平台架构设计
面试官可能问:"说说这个微信小程序的整体架构?为什么选 UniApp?"
回答:
整个项目采用 UniApp + Vue3 Composition API + TypeScript 的前端架构,配合 Express + Prisma + MySQL 的后端。
前端用 UniApp 是因为它基于 Vue 语法,学习成本低,而且一套代码可以编译到微信小程序、H5、App 等多个平台。虽然目前只上线了微信小程序,但架构上已经为多端扩展留好了余地。
项目一共 30+ 个页面,通过 pages.json 统一配置路由和 TabBar(首页/商城/服务/我的四个标签页)。全局配置了统一的导航栏样式:蓝色主题 #1675FB、白色文字。所有页面组件采用 script setup 语法糖 + 独立的 CSS 文件组织,结构清晰。
后端用 Express + TypeScript 构建 RESTful API,按 Route-Controller-Service 三层分层。Prisma ORM 做类型安全的数据库操作,MySQL 持久化。JWT 做用户认证,Winston 做日志记录。
追问:"为什么不用原生的微信小程序框架?"
UniApp 的 Vue3 开发体验比原生小程序好太多——Composition API、TypeScript 支持、组件化开发、Vite 构建,开发效率高。而且项目后续如果有 H5 或 App 需求,UniApp 能快速编译过去,不用重写。
Q2: 微信 OAuth 登录与 JWT 认证体系
面试官可能问:"微信登录的流程是怎么设计的?JWT 怎么用的?"
回答:
整个认证流程分三步:
第一步:微信登录。 前端调 uni.login({ provider: 'weixin' }) 拿到临时 code → 发给后端 /auth/wxlogin → 后端拿着 code 去微信服务器换 openid 和 session_key → 查数据库判断是新用户还是老用户 → 签发 JWT Token 返回前端。
第二步:Token 管理。 前端收到 Token 后存到 uni.setStorageSync,同时算一个 24 小时后的过期时间也存进去。所有后续请求通过 request.ts 的拦截器自动在 Header 里带上 Authorization: Bearer <token>。Token 过期时 401 响应触发自动清除本地缓存并跳转登录页。
第三步:手机号绑定。 用户登录后如果没绑定手机号,可以调 uni.getPhoneNumber(微信的 <button open-type="getPhoneNumber">)获取加密的手机号数据,发给后端 /auth/bind-phone 解密并绑定。
另外还有个 全局登录状态检查 的设计:每个需要登录的页面操作前都会调 checkLoginStatus()——检查 user、token、tokenExpire 三个 Storage key 是否都存在且未过期。未登录就弹窗引导去登录,用户体验上不会出现"操作到一半才发现没登录"的尴尬。
追问:"JWT 过期了怎么处理?用户要重新走微信授权吗?"
不需要。JWT 过期只是后端拒绝请求,前端的微信登录态(openid)是持久的。Token 过期后前端清除本地缓存,用户再次进入小程序时自动静默调 uni.login 换新 code → 后端返回新 Token,用户无感知。只有微信自身的登录态过期(比如长时间未使用),才需要用户重新点授权按钮。
Q3: 网络请求层的统一封装
面试官可能问:"前端的网络请求是怎么封装的?有什么设计考量?"
回答:
在 src/utils/request.ts 里封装了一个统一的 request 函数,所有 API 调用都走它。
核心设计:
环境切换:通过
import.meta.env.DEV自动判断开发/生产环境,开发连localhost:3000,生产连api.hdcmcc.cn。Token 自动注入:每次请求从 Storage 里取 Token 拼到 Authorization Header,上层 API 代码不用关心认证细节。
错误分级处理:
statusCode >= 200 && < 300:正常 resolve401:Token 失效 → 清除本地缓存 → 弹 toast → 1.5s 后跳登录页- 其他 4xx/5xx:弹 toast 显示后端返回的 message
fail回调(网络断连等):统一提示"网络异常,请稍后再试"
Loading 统一管理:默认自动
uni.showLoading,可以通过showLoading: false关闭(比如静默刷新的请求)。请求结束自动hideLoading。URL 智能拼接:如果传的 URL 已经是
http(s)://开头就直接用,否则拼接BASE_URL。这样支持调第三方接口。快捷方法:包装了
get、post、put、del四个语义化方法,调用时不需要每次写 method。
追问:"URL 拼接为什么要判断 http(s)😕/ 开头?"
主要是给文件上传预留的——uni.uploadFile 不走这个 request 函数,直接拼 BASE_URL + '/upload'。如果以后需要调外部 CDN 或其他服务,也能复用这个判断逻辑。
Q4: 多业务类型统一订单系统
面试官可能问:"宽带、手机卡、流量包、设备回收……这么多业务类型,订单系统怎么统一管理的?"
回答:
这确实是这个项目最核心的设计挑战。7 种业务类型(宽带、手机卡、流量包、商品购买、设备回收、故障报修、快捷办理),每种的数据结构差异很大——宽带有关键的安装地址和预约时间,手机卡有选号信息,商品有 SKU 规格和收货地址……
解决方案是"统一订单表 + extraData JSON 字段":
订单表 Order 有通用字段:id、orderNo、status、source(区分业务类型)、totalAmount、receiverName/Phone/Address、remark、createdAt 等。
业务特有的数据全部存在 extraData 这个 JSON 字段里:
- 宽带:
{ packageName, period, installAddress, installationTime, installationFee } - 手机卡:
{ simNumber, planName, planType, contractPeriod } - 流量包:
{ phoneNumber, packageName, validity, expiryTime } - 设备回收:
{ deviceBrand, deviceModel, evaluatedPrice, deviceCondition } - 故障报修:
{ deviceType, deviceModel, issueDesc, repairType, appointmentTime } - 快捷办理:
{ applicationType, serviceName, appointmentTime }
好处:不用建 7 张订单表,订单列表和状态流转逻辑复用同一套代码。前端 order-detail 组件根据 source 字段动态渲染对应的详情子组件。
追问:"JSON 字段怎么查询?比如我想查某个手机号的流量包订单?"
MySQL 5.7+ 支持 JSON 函数,比如 WHERE JSON_EXTRACT(extraData, '$.phoneNumber') = '138xxxx'。但考虑到性能,经常查询的字段(比如手机号、订单状态)直接在订单表建了索引列,不走 JSON 查询。extraData 只存展示用的、不需要查询过滤的字段。
Q5: 订单详情组件的多态设计
面试官可能问:"不同订单类型的详情页是怎么组织的?"
回答:
订单详情页用了 BaseDetail + 业务 Detail 插槽 的组合模式:
src/components/order-detail/
base-detail.vue ← 所有订单共通的展示:状态卡片、订单信息、联系信息、底部操作按钮
broadband-detail.vue ← 宽带特有:套餐名、合约期、安装地址、安装时间、安装费
sim-detail.vue ← 手机卡特有:SIM 卡号、套餐名、激活时间
data-detail.vue ← 流量波特有:手机号、流量包名、有效期
product-detail.vue ← 商城商品:物流信息
repair-detail.vue ← 故障报修:设备型号、故障描述、预约时间、维修技师
trade-in-detail.vue ← 设备回收:品牌、型号、估价
quick-apply-detail.vue ← 快捷办理:服务类型、预约时间base-detail.vue 通过 <slot name="extra-details"> 提供插槽,业务 Detail 组件插入到这个插槽里展示各自的特殊字段。订单详情页面 order-detail.vue 根据 orderType 动态 v-if 加载对应的子组件。
底部操作按钮也通过 actions prop 动态配置——待处理状态显示"修改/取消",处理中显示"取消",已完成不显示操作按钮。修改和取消分别跳到 order/edit 和 order/cancel-reason 页面。
追问:"为什么不直接在 order-detail.vue 里用 v-if 判断字段?"
如果全写在一个文件里,7 种业务的字段叠加起来代码会非常长,维护困难。拆成独立组件后,每个业务只关心自己的字段,方便独立修改。而且 base-detail.vue 封装了通用的状态展示逻辑(pending/processing/completed 不同的颜色和图标),所有业务共享。
Q6: 自动填充与代码复用
面试官可能问:"用户在多个页面都要填手机号和姓名,有什么优化吗?"
回答:
封装了一个 autoFill.ts 工具模块,核心思路是"用户信息只填一次,全局复用"。
getUserInfo():从 uni.getStorageSync('user') 读取登录时保存的用户信息(昵称、手机号等)。
autoFillCommonFields(formData):定义了常见的字段映射表——phoneNumber → mobile、contactPhone → mobile、userName → nickname、contactName → nickname 等。遍历表单数据对象的 key,如果为空且用户信息里有对应值,就自动填入。
使用场景:宽带办理页、手机卡办理页、故障报修页、工单修改页都加了一个"自动填充"按钮,点击后一键填入。不用每个页面都写一遍"获取用户信息 → 判断是否为空 → 填入"的逻辑。
追问:"自动填充会不会覆盖用户已经填好的内容?"
不会。填充前会判断目标字段是否为空或只有空白字符,只有空字段才会被填充。用户手动修改过的字段不会被覆盖。
Q7: 购物车与库存管理
面试官可能问:"购物车这边有什么特别的设计?"
回答:
购物车有三个值得说的点:
1. 商品可用性动态检测。 购物车列表接口返回时,后端会检查每个商品是否还在架上(status === 'active')和库存是否充足。如果商品已下架或缺货,isAvailable 字段为 false,前端渲染时加灰色蒙层 + 失效原因 + 删除按钮。不可用商品不能勾选结算,只能在编辑模式下批量删除。
2. 库存双重校验。 用户在购物车增加数量时,不仅检查后端返回的 availableStock,还会实时调 getProductDetail 拿最新库存做二次确认。如果 SKU 级别的库存不足,会弹 toast 提示"库存仅剩 X 件",防止超卖。
3. 编辑模式 vs 浏览模式。 点"管理"进入编辑模式——隐藏数量加减按钮,显示复选框 + 批量删除。点"完成"切回浏览模式——显示数量控制,复选框只对可用商品生效。这个设计让购物车的日常使用和批量清理互不干扰。
4. 购物车 → 下单数据传递。 结算时把选中的商品列表 JSON.stringify 后存到 uni.setStorageSync('cartOrderData'),跳到确认订单页再读取。不用 URL 传参,避免数据过长导致小程序跳转失败。
Q8: 后端 MVC 三层架构
面试官可能问:"后端的架构是怎么设计的?"
回答:
后端采用 Node.js + Express + TypeScript,按照经典的 Route → Controller → Service 三层架构组织:
- Route 层:定义路由和中间件。比如
/orders/broadband/create绑定了 JWT 认证中间件 + 数据验证中间件,然后转到 Controller。 - Controller 层:处理请求参数解析、调用 Service、组装响应格式。不做业务逻辑。
- Service 层:核心业务逻辑。调用 Prisma Client 做数据库 CRUD,处理复杂的业务规则(比如订单状态流转校验、库存扣减)。
- Prisma ORM:
schema.prisma定义数据模型 →prisma migrate自动生成 SQL 迁移 → Prisma Client 提供类型安全的查询 API。
认证层通过 JWT 中间件统一鉴权——解析 Header 里的 Authorization: Bearer <token>,验证通过后把 userId 注入 req.user,后续 Controller 直接从 req.user 拿。
追问:"权限控制怎么做?管理员和普通用户怎么区分?"
JWT Payload 里存了 role 字段(user/admin)。需要管理员权限的路由加一个 adminAuth 中间件,检查 req.user.role === 'admin'。普通用户的接口只允许查自己的数据——Controller 里用 req.user.id 做过滤条件,确保用户不能跨权访问别人的订单。
Q9: 小程序路由与 TabBar 设计
面试官可能问:"30 多个页面怎么管理路由的?TabBar 和普通页面跳转怎么区分?"
回答:
所有页面路由在 pages.json 里集中配置。
TabBar 四个标签页:首页 (index)、商城 (store)、服务 (services)、我的 (my)。这四个页面用 uni.switchTab() 跳转,不需要传参。
非 TabBar 页面(20+ 个):用 uni.navigateTo() 跳转。首页里封装了一个 navigateTo(url) 函数,会自动判断目标 URL 是不是 TabBar 页,是就用 switchTab,不是就用 navigateTo。这样所有入口调同一个函数就行,不用关心跳转方式。
传参方式:
- 简单参数走 URL query:
/pages/my/orders?status=pending - 复杂数据(如购物车商品列表)用
uni.setStorageSync+ 页面读取,避免 URL 超长 - 事件通信:比如订单修改成功后回上一页,通过
uni.$emit通知刷新列表
页面返回后的数据刷新:比如取消订单页提交成功后 navigateBack,返回时通过 getCurrentPages() 拿到上一页的 vm 实例,调 prevPage.$vm.onRefresh() 触发刷新。用户看到的效果是返回后列表自动更新了。
追问:"为什么不用 Vue Router?"
UniApp 在小程序环境下不能用 Vue Router——小程序的页面栈由微信框架管理。pages.json 是 UniApp 的页面注册方式,编译后映射到小程序的原生页面配置。
行为面试常见题快速答
"为什么做这个项目?"
公司接了一个通信代理商的业务需求,客户需要一个小程序来替代传统的线下门店办理流程——用户能在线上办理宽带、买手机卡、买流量包、申请故障维修等。我从零搭建前端小程序 + 后端 API 的完整方案,独立完成了整个项目。
"遇到的最大难点?"
多业务类型的订单系统设计。7 种业务的订单数据结构差异很大,如果建 7 张不同的订单表,后续的订单列表、状态流转、详情展示都要写 7 套代码。最后用"统一订单表 + extraData JSON 字段"的方案解决了——通用字段放表列,业务特有数据放 JSON,前端根据 source 动态渲染详情组件。
"有什么遗憾?"
- 测试覆盖不够——前后端都没有自动化测试,业务逻辑全靠手动测试。
- 没有统一的错误码规范——后端有些地方返回中文 message,有些地方返回英文 error code,前端处理起来不统一。
- 图片资源依赖外部 CDN——所有图标都用
api.iconify.design的 SVG 外链,如果这个服务挂了或者用户网络不好,图标全都不显示。应该下载到本地 static 目录。
"怎么跟后端配合的?"
这个项目的后端也是我写的,但如果是团队协作的话,我会先用 Prisma schema 定义好数据模型,前后端基于同一个 schema 对齐接口结构。API 接口文档用 Swagger 或 Apifox 维护,前后端各看各的。
"小程序上线和审核有什么经验?"
微信小程序审核比较严格,需要提供 ICP 备案、营业执照等资质。项目做了粤ICP 备案号展示。另外小程序不能有诱导分享、不能跳转外部链接,这些都要注意。审核周期一般 1-3 个工作日。
上一篇:03-知乎摸鱼 zhihu-fisher 下一篇:05-EMI智慧SOP看板