开发背景
随着全民健身的普及,为进一步贯彻落实《“健康中国2030”规划纲要》和《全民健身条例》,更好地满足广大青少年学生和人民群众就近、便利参加体育健身活动的需求,持续扩大公共服务配套资源供给, 中小学校园的操场,运动设施周也开始对市民开放,那么为了合理时间,方便各项设施的有效利用, 减少不必要的人员聚集, 预订小程序提供了线上预约的便捷性
功能概要设计
技术选型
- 本项目使用微信小程序平台进行开发。
- 使用腾讯专门的小程序云开发技术,云资源包含云函数,数据库,带宽,存储空间,定时器等,资源配额价格低廉,无需域名和服务器即可搭建。
- 小程序本身的即用即走,适合小工具的使用场景,也适合快速开发迭代。
- 云开发技术采用腾讯内部链路,没有被黑客攻击的风险,不会 DDOS攻击,节省防火墙费用,安全性高且免维护。
- 资源承载力可根据业务发展需要随时弹性扩展。
数据库设计
EnrollJoinModel.DB_STRUCTURE = {
_pid: 'string|true',
ENROLL_JOIN_ID: 'string|true',
ENROLL_JOIN_PRICE: 'int|true|default=0',
ENROLL_JOIN_IS_ADMIN: 'int|true|default=0|comment=是否管理员添加 0/1',
ENROLL_JOIN_ENROLL_ID: 'string|true|comment=报名PK',
ENROLL_JOIN_ENROLL_TITLE: 'string|false',
ENROLL_JOIN_CATE_ID: 'string|false|default=0|comment=分类',
ENROLL_JOIN_CATE_NAME: 'string|false|comment=分类冗余',
ENROLL_JOIN_CODE: 'string|true|comment=核验码15位',
ENROLL_JOIN_IS_CHECKIN: 'int|true|default=0|comment=是否核销 0/1 ',
ENROLL_JOIN_CHECKIN_TIME: 'int|true|default=0',
ENROLL_JOIN_DAY: 'string|false|comment=日期',
ENROLL_JOIN_START: 'string|false|comment=开始时间',
ENROLL_JOIN_END: 'string|false|comment=结束时间',
ENROLL_JOIN_END_POINT: 'string|false|comment=结束时间末尾',
ENROLL_JOIN_END_FULL: 'string|false|comment=完整的结束时间 YYYY-MM-DD hh:mm',
ENROLL_JOIN_START_FULL: 'string|false|comment=完整的开始时间 YYYY-MM-DD hh:mm',
ENROLL_JOIN_USER_ID: 'string|true|comment=用户ID',
ENROLL_JOIN_FORMS: 'array|true|default=[]|comment=表单',
ENROLL_JOIN_OBJ: 'object|true|default={}',
ENROLL_JOIN_STATUS: 'int|true|default=1|comment=状态 0=待审核 1=成功, 9=用户取消, 99=系统取消',
ENROLL_JOIN_LAST_TIME: 'int|true|default=0',
ENROLL_JOIN_ADD_TIME: 'int|true',
ENROLL_JOIN_EDIT_TIME: 'int|true',
ENROLL_JOIN_ADD_IP: 'string|false',
ENROLL_JOIN_EDIT_IP: 'string|false',
};
// 字段前缀
EnrollJoinModel.FIELD_PREFIX = "ENROLL_JOIN_";
/**
* 状态 0=待审核 1=成功,9=用户取消, 99=审核未过
*/
EnrollJoinModel.STATUS = {
WAIT: 0,
SUCC: 1,
CANCEL: 9,
ADMIN_CANCEL: 99
};
EnrollJoinModel.STATUS_DESC = {
WAIT: '待审核',
SUCC: '成功',
CANCEL: '用户取消',
ADMIN_CANCEL: '系统取消'
};
EnrollModel.DB_STRUCTURE = {
_pid: 'string|true',
ENROLL_ID: 'string|true',
ENROLL_TITLE: 'string|true|comment=标题',
ENROLL_STATUS: 'int|true|default=1|comment=状态 0=未启用,1=使用中',
ENROLL_CATE_ID: 'string|true|default=0|comment=分类',
ENROLL_CATE_NAME: 'string|false|comment=分类冗余',
ENROLL_CANCEL_SET: 'int|true|default=1|comment=取消设置 0=不允,1=允许,2=开始前可取消,3=结束前可取消',
ENROLL_EDIT_SET: 'int|true|default=1|comment=修改 0=不允,1=允许,2=开始前可修改,3=结束前可修改',
ENROLL_ORDER: 'int|true|default=9999',
ENROLL_VOUCH: 'int|true|default=0',
ENROLL_FORMS: 'array|true|default=[]',
ENROLL_OBJ: 'object|true|default={}',
ENROLL_JOIN_FORMS: 'array|true|default=[]',
ENROLL_DAYS: 'array|true|default=[]|comment=最近一次修改保存的可用日期',
ENROLL_DAY_CNT: 'int|true|default=0',
ENROLL_QR: 'string|false',
ENROLL_VIEW_CNT: 'int|true|default=0',
ENROLL_JOIN_CNT: 'int|true|default=0',
ENROLL_ADD_TIME: 'int|true',
ENROLL_EDIT_TIME: 'int|true',
ENROLL_ADD_IP: 'string|false',
ENROLL_EDIT_IP: 'string|false',
};
关键难点
// 获取某天某个场所下的可约时间点
async getOneDayTimePoint(day, enrollId) {
let where = {
DAY_ENROLL_ID: enrollId,
day
};
let fields = 'times';
let data = await DayModel.getOne(where, fields);
if (!data)
data = [];
else
data = data.times;
return data;
}
// 取得某天内所有场地信息
async getAllEnroll(cateId, day) {
let where = {
ENROLL_CATE_ID: String(cateId),
ENROLL_STATUS: EnrollModel.STATUS.COMM
}
let orderBy = {
ENROLL_ORDER: 'asc',
ENROLL_ADD_TIME: 'asc'
}
let list = await EnrollModel.getAll(where, '*', orderBy);
let arr = [];
let startTime = 23;
let endTime = 0;
for (let k = 0; k < list.length; k++) {
let times = await this.getOneDayTimePoint(day, list[k]._id);
// 分解小时
let t = [];
for (let j = 0; j < times.length; j++) {
if (times[j].start < startTime) startTime = times[j].start;
if (times[j].end > endTime) endTime = times[j].end;
for (let i = times[j].start; i <= times[j].end; i++) {
let node = {
t: i, //时间点
price: times[j].price, //价格
};
t.push(node);
}
}
if (t.length > 0)
arr.push({
enrollId: list[k]._id,
label: list[k].ENROLL_TITLE,
timePrice: t
})
}
// 取得可预订的最大日期
let maxDay = await DayModel.max({ day: ['>=', day], DAY_CATE_ID: cateId }, 'day');
if (maxDay == 0) maxDay = '';
return {
maxDay,
startTime,
endTime,
list: arr
};
}
// 获取某天预订情况
async getUsedByDay(cateId, day) {
let where = {
ENROLL_JOIN_CATE_ID: String(cateId),
ENROLL_JOIN_DAY: day,
ENROLL_JOIN_STATUS: ['in', [EnrollJoinModel.STATUS.WAIT, EnrollJoinModel.STATUS.SUCC]],
};
return EnrollJoinModel.getAll(where);
}
/** 取得我的登记分页列表 */
async getMyEnrollJoinList(userId, {
search, // 搜索条件
sortType, // 搜索菜单
sortVal, // 搜索菜单
orderBy, // 排序
page,
size,
isTotal = true,
oldTotal
}) {
orderBy = orderBy || {
'ENROLL_JOIN_ADD_TIME': 'desc'
};
let fields = 'ENROLL_JOIN_IS_CHECKIN,ENROLL_JOIN_CATE_NAME,ENROLL_JOIN_ENROL_TITLE,ENROLL_JOIN_PRICE,ENROLL_JOIN_END_FULL,ENROLL_JOIN_OBJ,ENROLL_JOIN_DAY,ENROLL_JOIN_START,ENROLL_JOIN_END,ENROLL_JOIN_END_POINT,ENROLL_JOIN_LAST_TIME,ENROLL_JOIN_ENROLL_ID,ENROLL_JOIN_STATUS,ENROLL_JOIN_ADD_TIME,enroll.ENROLL_TITLE,enroll.ENROLL_EDIT_SET,enroll.ENROLL_CANCEL_SET';
let where = {
ENROLL_JOIN_USER_ID: userId
};
if (util.isDefined(search) && search) {
where['ENROLL_JOIN_OBJ.name'] = {
$regex: '.*' + search,
$options: 'i'
};
} else if (sortType) {
// 搜索菜单
switch (sortType) {
case 'timedesc': { //按时间倒序
orderBy = {
'ENROLL_JOIN_START_FULL': 'desc'
};
break;
}
case 'timeasc': { //按时间正序
orderBy = {
'ENROLL_JOIN_START_FULL': 'asc'
};
break;
}
case 'status': {
break;
}
case 'today': {
where.ENROLL_JOIN_DAY = timeUtil.time('Y-M-D');
where.ENROLL_JOIN_STATUS = EnrollJoinModel.STATUS.SUCC;
break;
}
case 'run': {
where.ENROLL_JOIN_END_FULL = ['>', timeUtil.time('Y-M-D h:m')];
where.ENROLL_JOIN_STATUS = EnrollJoinModel.STATUS.SUCC;
where.ENROLL_JOIN_IS_CHECKIN = 0;
break;
}
case 'check': {
where.ENROLL_JOIN_END_FULL = ['>', timeUtil.time('Y-M-D h:m')];
where.ENROLL_JOIN_STATUS = EnrollJoinModel.STATUS.SUCC;
where.ENROLL_JOIN_IS_CHECKIN = 1;
break;
}
case 'out': {
where.ENROLL_JOIN_END_FULL = ['<=', timeUtil.time('Y-M-D h:m')];
where.ENROLL_JOIN_STATUS = EnrollJoinModel.STATUS.SUCC;
break;
}
case 'cancel': {
where.ENROLL_JOIN_STATUS = EnrollJoinModel.STATUS.CANCEL;
break;
}
case 'syscancel': {
where.ENROLL_JOIN_STATUS = EnrollJoinModel.STATUS.ADMIN_CANCEL;
break;
}
}
}
let joinParams = {
from: EnrollModel.CL,
localField: 'ENROLL_JOIN_ENROLL_ID',
foreignField: '_id',
as: 'enroll',
};
let result = await EnrollJoinModel.getListJoin(joinParams, where, fields, orderBy, page, size, isTotal, oldTotal);
return result;
}
/** 取得我的登记详情 */
async getMyEnrollJoinDetail(enrollJoinId) {
let fields = '*';
let where = {
_id: enrollJoinId
};
let enrollJoin = await EnrollJoinModel.getOne(where, fields);
if (enrollJoin) {
enrollJoin.enroll = await EnrollModel.getOne(enrollJoin.ENROLL_JOIN_ENROLL_ID, 'ENROLL_TITLE');
}
return enrollJoin;
}
//################## 登记
// 把时间格式'hh:mm'转为数组[1,2,3,4]
getTimeArr(start, end) {
start = start.replace(':00', '').trim();
start = start.replace(':30', '').trim();
start = Number(start);
end = end.replace(':00', '');
end = end.replace(':30', '').trim();
end = Number(end);
let ret = [];
for (let k = start; k <= end; k++) {
ret.push(k);
}
return ret;
}
// 登记
async enrollJoin(userId, {
enrollId,
price,
start,
end,
endPoint,
day,
forms
}) {
// 登记是否结束
let whereEnroll = {
_id: enrollId,
ENROLL_STATUS: EnrollModel.STATUS.COMM
}
let enroll = await EnrollModel.getOne(whereEnroll);
if (!enroll)
this.AppError('该' + ENROLL_NAME + '不存在或者已经停止');
// 判断是否已经被约(数组交集)
let nowTimeArr = this.getTimeArr(start, end);
let joinWhere = {
ENROLL_JOIN_ENROLL_ID: enrollId,
ENROLL_JOIN_DAY: day,
ENROLL_JOIN_STATUS: ['in', [EnrollJoinModel.STATUS.WAIT, EnrollJoinModel.STATUS.SUCC]],
}
let joinList = await EnrollJoinModel.getAll(joinWhere, 'ENROLL_JOIN_START,ENROLL_JOIN_END', { 'ENROLL_JOIN_START': 'asc' });
for (let k = 0; k < joinList.length; k++) {
let listTimeArr = this.getTimeArr(joinList[k].ENROLL_JOIN_START, joinList[k].ENROLL_JOIN_END);
for (let j = 0; j < nowTimeArr.length; j++) {
if (listTimeArr.includes(nowTimeArr[j])) {
this.AppError(nowTimeArr[j] + '点已经被预订,请重新选择');
}
}
}
// 入库
let data = {
ENROLL_JOIN_USER_ID: userId,
ENROLL_JOIN_ENROLL_ID: enrollId,
ENROLL_JOIN_CATE_ID: enroll.ENROLL_CATE_ID,
ENROLL_JOIN_CATE_NAME: enroll.ENROLL_CATE_NAME,
ENROLL_JOIN_CODE: dataUtil.genRandomIntString(15),
ENROLL_JOIN_PRICE: price,
ENROLL_JOIN_START: start,
ENROLL_JOIN_END: end,
ENROLL_JOIN_END_POINT: endPoint,
ENROLL_JOIN_DAY: day,
ENROLL_JOIN_ENROLL_TITLE: enroll.ENROLL_TITLE,
ENROLL_JOIN_END_FULL: day + ' ' + endPoint,
ENROLL_JOIN_START_FULL: day + ' ' + start,
ENROLL_JOIN_FORMS: forms,
ENROLL_JOIN_OBJ: dataUtil.dbForms2Obj(forms),
}
let enrollJoinId = await EnrollJoinModel.insert(data);
// 统计数量
this.statEnrollJoin(enrollId);
return { enrollJoinId }
}
// 修改登记
async enrollJoinEdit(userId, enrollId, enrollJoinId, forms) {
let whereJoin = {
_id: enrollJoinId,
ENROLL_JOIN_USER_ID: userId,
ENROLL_JOIN_ENROLL_ID: enrollId,
ENROLL_JOIN_STATUS: ['in', [EnrollJoinModel.STATUS.WAIT, EnrollJoinModel.STATUS.SUCC]],
}
let enrollJoin = await EnrollJoinModel.getOne(whereJoin);
if (!enrollJoin)
this.AppError('该' + ENROLL_NAME + '记录不存在或者已经被系统取消');
// 登记是否结束
let whereEnroll = {
_id: enrollId,
ENROLL_STATUS: EnrollModel.STATUS.COMM
}
let enroll = await EnrollModel.getOne(whereEnroll);
if (!enroll)
this.AppError('该' + ENROLL_NAME + '不存在或者已经停止');
if (enrollJoin.ENROLL_JOIN_IS_CHECKIN == 1)
this.AppError('该预订已核销,不能修改');
if (enroll.ENROLL_EDIT_SET == 0)
this.AppError('该' + ENROLL_NAME + '不允许修改资料');
if (enroll.ENROLL_EDIT_SET == 2 && enrollJoin.ENROLL_JOIN_START_FULL <= timeUtil.time('Y-M-D h:m'))
this.AppError('该' + ENROLL_NAME + '已经开始,不能修改资料');
if (enroll.ENROLL_EDIT_SET == 3 && enrollJoin.ENROLL_JOIN_END_FULL <= timeUtil.time('Y-M-D h:m'))
this.AppError('该' + ENROLL_NAME + '已经结束,不能修改资料');
let data = {
ENROLL_JOIN_FORMS: forms,
ENROLL_JOIN_OBJ: dataUtil.dbForms2Obj(forms),
ENROLL_JOIN_LAST_TIME: this._timestamp,
}
await EnrollJoinModel.edit(whereJoin, data);
}
async statEnrollJoin(id) {
let where = {
ENROLL_JOIN_ENROLL_ID: id,
ENROLL_JOIN_STATUS: ['in', [EnrollJoinModel.STATUS.WAIT, EnrollJoinModel.STATUS.SUCC]]
}
let cnt = await EnrollJoinModel.count(where);
await EnrollModel.edit(id, { ENROLL_JOIN_CNT: cnt });
}
/** 登记前获取关键信息 */
async detailForEnrollJoin(userId, enrollId, enrollJoinId = '') {
let fields = 'ENROLL_JOIN_FORMS, ENROLL_TITLE, ENROLL_CATE_NAME';
let where = {
_id: enrollId,
ENROLL_STATUS: EnrollModel.STATUS.COMM
}
let enroll = await EnrollModel.getOne(where, fields);
if (!enroll)
this.AppError('该' + ENROLL_NAME + '不存在');
let joinMy = null;
if (enrollJoinId) {
// 编辑
let whereMy = {
ENROLL_JOIN_USER_ID: userId,
_id: enrollJoinId
}
joinMy = await EnrollJoinModel.getOne(whereMy);
enroll.join = {
start: joinMy.ENROLL_JOIN_START,
end: joinMy.ENROLL_JOIN_END,
endPoint: joinMy.ENROLL_JOIN_END_POINT,
day: joinMy.ENROLL_JOIN_DAY,
}
}
else {
// 取出本人最近一次的填写表单
let whereMy = {
ENROLL_JOIN_USER_ID: userId,
}
let orderByMy = {
ENROLL_JOIN_ADD_TIME: 'desc'
}
joinMy = await EnrollJoinModel.getOne(whereMy, 'ENROLL_JOIN_FORMS', orderByMy);
}
let myForms = joinMy ? joinMy.ENROLL_JOIN_FORMS : [];
enroll.myForms = myForms;
return enroll;
}
/** 取消我的登记 */
async cancelMyEnrollJoin(userId, enrollJoinId) {
let where = {
ENROLL_JOIN_USER_ID: userId,
_id: enrollJoinId,
ENROLL_JOIN_STATUS: ['in', [EnrollJoinModel.STATUS.WAIT, EnrollJoinModel.STATUS.SUCC]]
};
let enrollJoin = await EnrollJoinModel.getOne(where);
if (!enrollJoin) {
this.AppError('未找到可取消的记录');
}
if (enrollJoin.ENROLL_JOIN_IS_CHECKIN == 1)
this.AppError('该预订已核销,不能取消');
let enroll = await EnrollModel.getOne(enrollJoin.ENROLL_JOIN_ENROLL_ID);
if (!enroll)
this.AppError('该' + ENROLL_NAME + '不存在');
if (enroll.ENROLL_CANCEL_SET == 0)
this.AppError('该' + ENROLL_NAME + '不能取消');
if (enroll.ENROLL_CANCEL_SET == 2 && enrollJoin.ENROLL_JOIN_START_FULL <= timeUtil.time('Y-M-D h:m'))
this.AppError('该' + ENROLL_NAME + '已经开始,不能取消');
if (enroll.ENROLL_CANCEL_SET == 3 && enrollJoin.ENROLL_JOIN_END_FULL <= timeUtil.time('Y-M-D h:m'))
this.AppError('该' + ENROLL_NAME + '已经结束,不能取消');
if (enroll.ENROLL_CANCEL_SET > 20) {
let step = enroll.ENROLL_CANCEL_SET - 20;
let day = timeUtil.time2Timestamp(enrollJoin.ENROLL_JOIN_END_FULL + ':00') - step * 86400 * 1000;
day = timeUtil.timestamp2Time(day, 'Y-M-D');
let now = timeUtil.time('Y-M-D');
if (now > day) this.AppError('仅开始前' + step + '天可取消');
}
await EnrollJoinModel.edit(where,
{
ENROLL_JOIN_STATUS: EnrollJoinModel.STATUS.CANCEL,
ENROLL_JOIN_IS_CHECKIN: 0
});
await this.statEnrollJoin(enrollJoin.ENROLL_JOIN_ENROLL_ID);
}
前端UI设计
后台管理系统UI设计
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。