闪递校园:基于uni-app的校园综合服务平台开发实战
作为一名全栈开发者,我用6个月时间开发了这款校园综合服务平台——闪递校园。本文将详细分享项目从0到1的开发经验,包括技术选型、核心功能实现、踩坑记录以及性能优化等方面的干货内容。
📝 项目背景
为什么要做这个项目?
在大学期间,我发现校园里存在诸多痛点:
- 🏃♂️ 跑腿需求旺盛:取快递、买外卖、代购等需求量大,但缺乏规范化平台
- 💬 社交圈子固化:同学之间交流局限,缺乏破冰工具
- 🎫 信息不对称:演出票务、二手交易信息分散
- 💰 支付体验差:校园服务多采用现金支付,体验落后
于是我决定开发一款集跑腿服务、社交娱乐、校园电商于一体的综合性平台。
技术选型思考
技术栈 | 选择 | 理由 |
---|---|---|
前端框架 | uni-app + Vue 2.x | 一套代码多端运行,降低开发成本 |
UI组件库 | TuniaoUI | 组件丰富,校园风格契合 |
状态管理 | Vuex | 用户信息、订单状态等需要全局管理 |
地图服务 | 腾讯地图API | 国内定位精准,校园场景适配好 |
支付系统 | 微信支付 + 余额支付 | 覆盖主流支付场景 |
实时通讯 | WebSocket | 订单状态、聊天消息实时性要求高 |
🏗️ 项目架构设计
整体架构图
目录结构设计
reWxSchool/
├── pages/ # 主包页面
│ ├── index/ # 首页模块
│ ├── hall/ # 接单大厅
│ ├── circle/ # 校园圈子
│ └── user/ # 用户中心
├── pagesA/ # 分包A - 核心业务
│ ├── order/ # 订单管理
│ ├── chat/ # 聊天系统
│ ├── makeFrend/ # 盲盒交友
│ └── withdraw/ # 钱包系统
├── pagesB/ # 分包B - 扩展功能
│ ├── movie/ # 电影票务
│ ├── secondhand/ # 二手市场
│ └── mbti/ # 性格测试
├── components/ # 公共组件
├── store/ # Vuex状态管理
└── util/ # 工具函数
💡 核心功能实现
1. 复杂支付系统的设计与实现
支付系统是整个平台的核心,我设计了一套完整的支付流程:
1.1 支付组件封装
// components/payBox/payBox.vue
export default {data() {return {paymentInProgress: false, // 支付进行中标志isProcessing: false, // 处理中标志currentOrderId: null, // 当前订单IDpaymentSessionId: null, // 支付会话IDretryCount: 0, // 重试次数paymentTimeout: null // 支付超时定时器}},methods: {// 核心支付流程async processPayment() {if (this.paymentInProgress) {this.showToast('支付进行中,请稍候');return;}try {this.paymentInProgress = true;this.isProcessing = true;this.paymentStartTime = Date.now();this.retryCount++;// 设置支付超时(5分钟)this.setPaymentTimeout();// 保存订单(如果还没有订单ID)let orderResult;if (!this.currentOrderId) {orderResult = await this.saveOrder();this.currentOrderId = orderResult.id || orderResult.payOrderId;} else {// 使用现有订单ID,避免重复创建订单orderResult = { payOrderId: this.currentOrderId, id: this.currentOrderId };}// 发起支付await this.initiatePayment(orderResult.payOrderId, orderResult.id);} catch (error) {this.handlePaymentError(error);} finally {this.clearPaymentTimeout();// 延迟重置状态,防止快速重复点击setTimeout(() => {this.isProcessing = false;this.paymentInProgress = false;}, 1000);}},// 微信支付处理async wechatPayment(payOrderId, orderId) {try {const response = await this.$apiHttp.transactions({ payOrderId });if (response.code !== 0) {throw new Error('交易初始化失败');}await this.requestWechatPayment(response.data, orderId);} catch (error) {console.log('支付失败', orderId);}},// 余额支付处理async balancePayment(payOrderId, orderId) {try {const response = await this.$apiHttp.balancePayment({ payOrderId });if (response.code === 0) {this.handlePaymentSuccess(orderId);} else {this.handlePaymentFailure(response.msg, orderId);}} catch (error) {this.handlePaymentFailure('余额支付失败', orderId);}}}
}
1.2 防重复支付机制
为了防止用户快速点击导致重复扣费,我设计了多重保护机制:
// 多重锁定机制
confirmBalancePayment() {// 防止快速点击确认框if (this.isProcessing || this.paymentInProgress) {return;}uni.showModal({title: '确认支付',content: '是否使用余额免密支付?',success: (res) => {if (res.confirm) {// 再次检查状态,防止确认框期间状态变化if (!this.isProcessing && !this.paymentInProgress) {this.processPayment();}}},});
}
2. 实时通讯系统实现
2.1 WebSocket连接管理
// pagesA/chatUser/chat.vue
export default {data() {return {socket: null,wsUrl: "wss://school.bitsai.top/school-api/websocket",connectionStatus: 'disconnected',token: "",reconnectAttempts: 0,maxReconnectAttempts: 5}},methods: {// WebSocket连接connectWebSocket() {// 检查token是否存在if (!this.token) {console.error('Token不存在,无法建立WebSocket连接');this.showErrorMessage('登录信息无效,请重新登录');return;}this.socket = uni.connectSocket({url: this.wsUrl,header: {'token': this.token},success: () => console.log('WebSocket connection established'),fail: (error) => {console.error('WebSocket connection failed', error);this.showErrorMessage('WebSocket连接失败,请稍后重试');}});// 处理连接打开this.socket.onOpen(() => {console.log('WebSocket connection opened successfully');this.connectionStatus = 'connected';this.reconnectAttempts = 0; // 重置重连次数});// 处理接收消息this.socket.onMessage((message) => {let data = JSON.parse(message.data);if (data.type === 0) {this.handleReceivedMessage(data.msg);}});// 处理连接关闭,自动重连(指数退避)this.socket.onClose((event) => {if (event.code !== 1000) {this.connectionStatus = 'reconnecting';// 确保token存在时才重连if (this.token && this.reconnectAttempts < this.maxReconnectAttempts) {setTimeout(() => {this.reconnectAttempts++;this.connectWebSocket();}, this.getReconnectDelay());} else {this.connectionStatus = 'disconnected';}} else {this.connectionStatus = 'disconnected';}});},// 指数退避重连延迟getReconnectDelay() {return Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);},// 消息发送handleSend() {if (this.chatMsg && !/^\s+$/.test(this.chatMsg)) {let messageObj = {botContent: "",userContent: this.chatMsg,isload: true,iserr: false,sendTime: new Date().toISOString()};this.msgList.push(messageObj);this.shouldScrollToBottom = true;if (this.socket && this.socket.readyState === 1) {const message = {type: 0,msg: this.chatMsg,data: "Some data",userId: this.receiverId};this.socket.send({data: JSON.stringify(message),success: () => {console.log('Message sent successfully');this.updateMessageStatus('success');},fail: (error) => {console.error('Failed to send message', error);this.updateMessageStatus('error');}});} else {console.error('WebSocket is not open');this.updateMessageStatus('error');this.showErrorMessage('无法发送消息,请稍后重试');}this.chatMsg = ''; // 清空输入框}}}
}
3. 地理位置服务集成
3.1 腾讯地图API封装
// pagesB/utils/power.js
export default {methods: {// 获取用户定位信息Monitor() {var QQMapWX = require('@/comne/qqmap-wx-jssdk.js');let qqmapsdk = new QQMapWX({key: ''});var _this = this;uni.getLocation({type: 'gcj02',success(res) {var latitude = res.latitude;var longitude = res.longitude;// 小程序环境使用腾讯地图SDK// #ifdef MP-WEIXINqqmapsdk.reverseGeocoder({location: {latitude: latitude,longitude: longitude},success: function(res) {console.log(res.result);uni.setStorageSync('LocalHost', res.result);_this.city = res.result.ad_info.city;}});// #endif// H5环境使用HTTP API// #ifdef H5_this.getH5Loca(latitude, longitude);// #endif},fail(err) {console.log(err);uni.showModal({title: '提示',content: '请先授权位置信息',success: function(res) {if (res.cancel == false && res.confirm) {uni.openSetting({success: function(data) {_this.Monitor();}});} else {_this.Monitor();}}});}});},// H5环境地理编码getH5Loca(latitude, longitude) {let prm = {location: {latitude: latitude,longitude: longitude},key: '',output: 'jsonp',get_poi: 0,coord_type: 5};let url = `https://apis.map.qq.com/ws/geocoder/v1/?location=${latitude},${longitude}`;this.$jsonp(url, prm).then(res => {uni.setStorageSync('LocalHost', res.result);this.city = res.result.ad_info.city;});}}
}
4. AI智能客服集成
// util/request/api.js
coQuanziApi: (data) => {return new Promise((resolve, reject) => {uni.request({url: 'https://api.coze.cn/open_api/v2/chat',method: 'POST',header: {'Authorization': '','Connection': 'keep-alive','Host': 'api.coze.cn','Accept': '*/*','Content-Type': 'application/json',},data: {"conversation_id": data.userId,"bot_id": "","user": data.userId,"stream": false,"query": data.content,},success: (res) => {resolve(res);},fail: (err) => {console.error(err);reject(err);}});});
}
🎯 创新功能设计
1. 盲盒交友系统
这是我最引以为豪的创新功能,通过匿名投递和随机抽取的方式,为校园社交带来新的玩法:
核心逻辑:
- 投放纸条:用户可以匿名投放个人信息或心情到盲盒中
- 随机抽取:其他用户可以随机抽取纸条,如果感兴趣可以发起聊天
- 地理限制:仅限同校学生参与,确保社交的真实性
- 隐私保护:初期完全匿名,建立信任后才可以选择公开身份
// 盲盒相关API
// 投放纸条
luckyNoteSave: (data) => {return postRequest('luckyNote/save', data)
},// 抽取纸条
luckydrawNote: (data) => {return postRequest('luckyNote/drawNote', data)
},// 查看历史
noteHistoryPage: (data) => {return getRequest('luckyNote/noteHistoryPage', data)
}
2. 智能跑腿订单系统
订单状态流转:
核心API设计:
// 订单相关API
orderSave: (data) => postRequest('order/save', data), // 创建订单
orderTaking: (data) => postRequest('order/taking', data), // 骑手接单
orderDelivery: (data) => postRequest('order/delivery', data), // 骑手取货
orderArrive: (data) => postRequest('order/arrive', data), // 骑手送达
orderConfirmArrive: (data) => postRequest('order/confirmArrive', data), // 用户确认
🚀 性能优化实践
1. 分包加载策略
通过合理的分包设计,将首屏加载时间从3.2s优化到1.8s:
// pages.json
{"pages": [// 主包 - 核心页面{"path": "pages/index/index"},{"path": "pages/hall/index"},{"path": "pages/circle/circle"},{"path": "pages/user/index"}],"subPackages": [{"root": "pagesA", // 分包A - 核心业务功能"pages": [{"path": "order/index"},{"path": "chat/chat"},{"path": "makeFrend/makeFrend"}]},{"root": "pagesB", // 分包B - 扩展功能"pages": [{"path": "movie/index"},{"path": "secondhand/index"},{"path": "mbti/mbti"}]}]
}
2. 请求优化
API管理器设计:
// util/request/api.js
const apiManager = {// 用户相关login: (data) => postRequest('api/login', data),getUserData: () => getRequest('user/data'),// 订单相关orderList: (data) => getRequest('order/pageList', data),orderSave: (data) => postRequest('order/save', data),// 支付相关transactions: (data) => getRequest('pay/transactions', data),balancePayment: (data) => getRequest('pay/balancePayment', data),// 聊天相关getChatList: (data) => getRequest('chat/getChatList', data),chatPageList: (data) => getRequest('chat/message/pageList', data)
}
3. 状态管理优化
// store/index.js
const store = new Vuex.Store({state: {// 持久化用户信息vuex_user: lifeData.vuex_user ? lifeData.vuex_user : {name: '图鸟'},// 导航栏配置vuex_custom_nav_bar: true,vuex_status_bar_height: 0,vuex_custom_bar_height: 0,},mutations: {// 通用状态更新$tStore(state, payload) {let nameArr = payload.name.split('.');let saveKey = '';let len = nameArr.length;if (len >= 2) {// 支持多层级状态更新let obj = state[nameArr[0]];for (let i = 1; i < len - 1; i++) {obj = obj[nameArr[i]];}obj[nameArr[len - 1]] = payload.value;saveKey = nameArr[0];} else {state[payload.name] = payload.value;saveKey = payload.name;}// 自动持久化到本地存储saveLifeData(saveKey, state[saveKey]);}}
})
🐛 踩坑记录与解决方案
1. 微信支付回调问题
问题: 微信支付成功后,有时候回调不及时,导致订单状态更新延迟。
解决方案:
// 增加支付状态轮询机制
requestWechatPayment(paymentData, orderId) {return new Promise((resolve, reject) => {uni.requestPayment({...paymentData,success: () => {// 支付成功后启动状态轮询this.startPaymentPolling(orderId);this.handlePaymentSuccess(orderId);},fail: (error) => {this.handlePaymentFailure('微信支付失败', orderId);}});});
},// 支付状态轮询
startPaymentPolling(orderId) {const pollInterval = setInterval(async () => {try {const result = await this.$apiHttp.orderDetail(orderId);if (result.data.payStatus === 'PAID') {clearInterval(pollInterval);this.updateOrderStatus('paid');}} catch (error) {console.error('轮询支付状态失败', error);}}, 2000);// 30秒后停止轮询setTimeout(() => clearInterval(pollInterval), 30000);
}
2. WebSocket断线重连问题
问题: 用户切换应用或网络波动时,WebSocket连接容易断开,重连策略不够完善。
解决方案:
// 改进的重连机制
connectWebSocket() {// ... 连接逻辑this.socket.onClose((event) => {if (event.code !== 1000 && this.reconnectAttempts < this.maxReconnectAttempts) {this.connectionStatus = 'reconnecting';// 指数退避策略const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);setTimeout(() => {this.reconnectAttempts++;console.log(`第${this.reconnectAttempts}次重连尝试`);this.connectWebSocket();}, delay);} else {this.connectionStatus = 'disconnected';if (this.reconnectAttempts >= this.maxReconnectAttempts) {this.showErrorMessage('连接失败次数过多,请检查网络后手动重试');}}});
}
3. 地理位置权限问题
问题: 用户首次使用时拒绝地理位置权限,导致功能异常。
解决方案:
Monitor() {uni.getLocation({type: 'gcj02',success(res) {// 定位成功处理},fail(err) {console.log(err);// 优雅的权限引导uni.showModal({title: '定位权限',content: '为了给您提供更好的校园服务,需要获取您的位置信息',confirmText: '去设置',cancelText: '手动选择',success: function(res) {if (res.confirm) {// 引导用户去设置页面uni.openSetting({success: function(data) {if (data.authSetting['scope.userLocation']) {_this.Monitor(); // 重新获取定位}}});} else {// 提供手动选择城市的备选方案uni.navigateTo({url: '/pagesA/school/school'});}}});}});
}
🎉 结语
闪递校园这个项目让我对全栈开发有了更深入的理解,也让我意识到技术服务于业务、业务服务于用户的重要性。虽然项目还有很多可以优化的地方,但它已经成为我技术成长路上的一个重要里程碑。
希望这篇文章能够对正在学习 uni-app 开发或者想要做校园服务类项目的同学有所帮助。如果你有任何问题或建议,欢迎在评论区交流讨论!