闪递校园:基于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, // 当前订单ID
paymentSessionId: null, // 支付会话ID
retryCount: 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-WEIXIN
qqmapsdk.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 开发或者想要做校园服务类项目的同学有所帮助。如果你有任何问题或建议,欢迎在评论区交流讨论!