从0到1:多服务厅预约小程序开发笔记(上)

基于腾讯云开发的通用预约小程序实战项目 随着移动互联网的深入发展,线上预约服务已成为医疗、政务、教育、体育、金融及生活服务等行业提升服务效率、优化用户体验的核心手段。通用预约小程序基于微信生态体系,依托腾讯小程序云开发平台,实现了无需独立服务器和域名的轻量化部署模式,极大降低了开发门槛与运维成本。该类小程序通过标准化接口设计与模块化功能架构,支持跨行业灵活配置,可快速适配医院挂号、场馆预订、课程约课等多种场景。 阅读详情

需求调研

多服务厅预约小程序:随着信息技术的快速发展和移动互联网的普及,越来越多的服务行业开始向线上转型, 传统的预约方式往往效率低下,用户需耗费大量时间进行电话预约或现场排队,服务厅预约小程序集多种服务于一体,可以提高服务效率、提升用户体验。 用户可以浏览不同服务厅的位置、服务项目和服务时间,用户可以选择日期、时间段和人数,然后进行预约。

功能设计

公告通知,用户预约,签到核销, 管理者可以自定义预约要填写的内容,比如姓名、性别、年龄、身份证、手机号等,后台预约管理,后台预约名单管理和导出Excel,后台设置预约时段和人数上线,后台管理最新通知公告,后台用户管理,后台服务厅管理,服务厅管理员设定等功能

概要设计

在这里插入图片描述

数据库设计


MeetModel.DB_STRUCTURE = {
	_pid: 'string|true',
	MEET_ID: 'string|true',
	MEET_ADMIN_ID: 'string|true|comment=添加的管理员',
	MEET_TITLE: 'string|true|comment=标题',

	MEET_UNIT_ID: 'string|false',
	MEET_UNIT_NAME: 'string|false',
 
	MEET_JOIN_FORMS: 'array|true|default=[]|comment=表单字段设置',
	MEET_DAYS: 'array|true|default=[]|comment=最近一次修改保存的可用日期',

	MEET_CATE_ID: 'string|true|comment=分类编号',
	MEET_CATE_NAME: 'string|true|comment=分类冗余', 

	MEET_FORMS: 'array|true|default=[]',
	MEET_OBJ: 'object|true|default={}',  

	MEET_CANCEL_SET: 'int|true|default=1|comment=取消设置 0=不允,1=允许,2=仅开始前可取消',

	MEET_STATUS: 'int|true|default=1|comment=状态 0=未启用,1=使用中,9=停止预约,10=已关闭',
	MEET_ORDER: 'int|true|default=9999',
	MEET_VOUCH: 'int|true|default=0',

	MEET_QR: 'string|false', 


	MEET_ADD_TIME: 'int|true',
	MEET_EDIT_TIME: 'int|true',
	MEET_ADD_IP: 'string|false',
	MEET_EDIT_IP: 'string|false',
};
DayModel.DB_STRUCTURE = {
	_pid: 'string|true',
	DAY_ID: 'string|true',
	DAY_MEET_ID: 'string|true',

	DAY_UNIT_ID: 'string|false',
	DAY_UNIT_NAME: 'string|false',

	day: 'string|true|comment=日期 yyyy-mm-dd',
	dayDesc: 'string|true|comment=描述',
	times: 'array|true|comment=具体时间段',
	/*
		{
			1. mark=唯一性标识,
			2. start=开始时间点hh:mm ~,  
			3. end=结束时间点hh:mm, 
			4. isLimit=是否人数限制, 
			5. limit=报名上限,  
			6. status=状态 0/1
			7. stat:{ //统计数据 
				succCnt=1预约成功*, 
				cancelCnt=10已取消, 
				adminCancelCnt=99后台取消
			}
		}', 
	*/

	DAY_ADD_TIME: 'int|true',
	DAY_EDIT_TIME: 'int|true',
	DAY_ADD_IP: 'string|false',
	DAY_EDIT_IP: 'string|false',
};

核心实现

class MeetService extends BaseProjectService {

	constructor() {
		super();
		this._log = new LogUtil(projectConfig.MEET_LOG_LEVEL);
	}

	/**
	 * 抛出异常
	 * @param {*} msg 
	 * @param {*} code 
	 */
	AppError(msg) {
		this._log.error(msg);
		super.AppError(msg);
	}

	_meetLog(meet, func = '', msg = '') {
		let str = '';
		str = `[MEET=${meet.MEET_TITLE}][${func}] ${msg}`;
		this._log.debug(str);
	}

	/** 统一获取Meet(某天) */
	async getMeetOneDay(meetId, day, where, fields = '*') {

		let meet = await MeetModel.getOne(where, fields);
		if (!meet) return meet;

		meet.MEET_DAYS_SET = await this.getDaysSet(meetId, day, day);
		return meet;
	}

	/** 获取日期设置 */
	async getDaysSet(meetId, startDay, endDay = null) {
		let where = {
			DAY_MEET_ID: meetId
		}
		if (startDay && endDay && endDay == startDay)
			where.day = startDay;
		else if (startDay && endDay)
			where.day = ['between', startDay, endDay];
		else if (!startDay && endDay)
			where.day = ['<=', endDay];
		else if (startDay && !endDay)
			where.day = ['>=', startDay];

		let orderBy = {
			'day': 'asc'
		}
		let list = await DayModel.getAllBig(where, 'day,dayDesc,times', orderBy, 1000);

		for (let k = 0; k < list.length; k++) {
			delete list[k]._id;
		}

		return list;
	}

	// 按时段统计某时段报名情况
	async statJoinCnt(meetId, timeMark) {
		let whereDay = {
			DAY_MEET_ID: meetId,
			day: this.getDayByTimeMark(timeMark)
		};
		let day = await DayModel.getOne(whereDay, 'times');
		if (!day) return;

		let whereJoin = {
			JOIN_MEET_TIME_MARK: timeMark,
			JOIN_MEET_ID: meetId
		};
		let ret = await JoinModel.groupCount(whereJoin, 'JOIN_STATUS');

		let stat = { //统计数据
			succCnt: ret['JOIN_STATUS_1'] || 0, //1=预约成功,
			cancelCnt: ret['JOIN_STATUS_10'] || 0, //10=已取消, 
			adminCancelCnt: ret['JOIN_STATUS_99'] || 0, //99=后台取消
		};

		let times = day.times;
		for (let j in times) {
			if (times[j].mark === timeMark) {
				let data = {
					['times.' + j + '.stat']: stat
				}
				await DayModel.edit(whereDay, data);
				return;
			}
		}

	}


	// 预约前检测
	async beforeJoin(userId, meetId, timeMark) {
		await this.checkMeetRules(userId, meetId, timeMark);
	}

	 

	// 根据日期获取其所在天设置
	getDaySetByDay(meet, day) {
		for (let k = 0; k < meet.MEET_DAYS_SET.length; k++) {
			if (meet.MEET_DAYS_SET[k].day == day)
				return dataUtil.deepClone(meet.MEET_DAYS_SET[k]);
		}
		return null;
	}

	// 根据时段标识获取其所在天 
	getDayByTimeMark(timeMark) {
		return timeMark.substr(1, 4) + '-' + timeMark.substr(5, 2) + '-' + timeMark.substr(7, 2);
	}

	// 根据时段标识获取其所在天设置
	getDaySetByTimeMark(meet, timeMark) {
		let day = this.getDayByTimeMark(timeMark);

		for (let k = 0; k < meet.MEET_DAYS_SET.length; k++) {
			if (meet.MEET_DAYS_SET[k].day == day)
				return dataUtil.deepClone(meet.MEET_DAYS_SET[k]);
		}
		return null;
	}

	// 根据时段标识获取其所在时段设置
	getTimeSetByTimeMark(meet, timeMark) {
		let day = this.getDayByTimeMark(timeMark);

		for (let k = 0; k < meet.MEET_DAYS_SET.length; k++) {
			if (meet.MEET_DAYS_SET[k].day != day) continue;

			for (let j in meet.MEET_DAYS_SET[k].times) {
				if (meet.MEET_DAYS_SET[k].times[j].mark == timeMark)
					return dataUtil.deepClone(meet.MEET_DAYS_SET[k].times[j]);
			}
		}
		return null;
	}

	// 预约时段人数和状态控制校验
	async checkMeetTimeControll(meet, timeMark, meetPeopleCnt = 1) {
		if (!meet) this.AppError('预约时段设置错误, 预约项目不存在');

		let daySet = this.getDaySetByTimeMark(meet, timeMark); // 当天设置
		let timeSet = this.getTimeSetByTimeMark(meet, timeMark); // 预约时段设置

		if (!daySet || !timeSet) this.AppError('预约时段设置错误day&time');

		let statusDesc = timeSet.status == 1 ? '开启' : '关闭';
		let limitDesc = '';
		if (timeSet.isLimit) {
			limitDesc = '人数上限MAX=' + timeSet.limit;
		} else
			limitDesc = '人数不限制NO';

		this._meetLog(meet, `------------------------------`);
		this._meetLog(meet, `#预约时段控制,预约日期=<${daySet.day}>`, `预约时段=[${timeSet.start}-${timeSet.end}],状态=${statusDesc}, ${limitDesc} 当前预约成功人数=${timeSet.stat.succCnt}`);

		if (timeSet.status == 0) this.AppError('该时段预约已经关闭,请选择其他');

		// 时段总人数限制
		if (timeSet.isLimit) {
			if (timeSet.stat.succCnt >= timeSet.limit) {
				this.AppError('该时段预约人员已满,请选择其他');
			}

			let maxCnt = timeSet.limit - timeSet.stat.succCnt;

			if (maxCnt < meetPeopleCnt) {
				this.AppError('本时段最多还可以预约' + (maxCnt) + '人,您当前提交了' + meetPeopleCnt + '人,请调整后再提交');
			}
		}

	}


	/** 报名规则校验 */
	async checkMeetRules(userId, meetId, timeMark, formsList = null) {

		// 预约时段是否存在
		let meetWhere = {
			_id: meetId
		};
		let day = this.getDayByTimeMark(timeMark);
		let meet = await this.getMeetOneDay(meetId, day, meetWhere);
		if (!meet) {
			this.AppError('预约时段选择错误,请重新选择');
		}

		// 预约时段人数和状态控制校验
		let meetPeopleCnt = formsList ? formsList.length : 1;

		await this.checkMeetTimeControll(meet, timeMark, meetPeopleCnt);

		// 截止规则  
		await this.checkMeetEndSet(meet, timeMark);


		// 针对用户的次数限制
		await this.checkMeetLimitSet(userId, meet, timeMark, meetPeopleCnt);

	}


	// 预约次数限制校验
	async checkMeetLimitSet(userId, meet, timeMark, nowCnt) {
		if (!meet) this.AppError('预约次数规则错误, 预约项目不存在');
		let meetId = meet._id;

		let daySet = this.getDaySetByTimeMark(meet, timeMark); // 当天设置
		let timeSet = this.getTimeSetByTimeMark(meet, timeMark); // 预约时段设置

		this._meetLog(meet, `------------------------------`);
		this._meetLog(meet, `#预约次数规则,预约日期=<${daySet.day}>`, `预约时段=[${timeSet.start}~${timeSet.end}]`);

		let where = {
			JOIN_MEET_ID: meetId,
			JOIN_MEET_TIME_MARK: timeMark,
			JOIN_USER_ID: userId,
			JOIN_STATUS: JoinModel.STATUS.SUCC
		}
		let cnt = await JoinModel.count(where);
		let maxCnt = projectConfig.MEET_MAX_JOIN_CNT;
		this._meetLog(meet, `预约次数规则,mode=本时段可预约${maxCnt}次`, `当前已预约=${cnt}次`);

		if (cnt >= maxCnt)
			this.AppError(`您本时段已经预约,不能继续预约`);

	}



	// 预约截止设置校验
	async checkMeetEndSet(meet, timeMark) {
		if (!meet) this.AppError('预约截止规则错误, 预约项目不存在');


		this._meetLog(meet, `------------------------------`);
		let daySet = this.getDaySetByTimeMark(meet, timeMark); // 当天设置
		let timeSet = this.getTimeSetByTimeMark(meet, timeMark); // 预约时段设置

		this._meetLog(meet, `#预约截止规则,预约日期=<${daySet.day}>`, `预约时段=[${timeSet.start}-${timeSet.end}]`);

		let nowTime = timeUtil.time('Y-M-D h:m:s');

		/*
		let startTime = daySet.day + ' ' + timeSet.start + ':00';
		this._meetLog(meet, `预约开始规则,mode=<时段过期判定>`, `预约开始时段=${startTime},当前时段=${nowTime}`);
		if (nowTime > startTime) {
			this.AppError('该时段已开始,无法预约,请选择其他');
		}*/

		let endTime = daySet.day + ' ' + timeSet.end + ':59';
		this._meetLog(meet, `预约开始规则,mode=<时段过期判定>`, `预约结束时段=${endTime},当前时段=${nowTime}`);
		if (nowTime > endTime) {
			this.AppError('该时段已结束,无法预约,请选择其他');
		}

	}


	/**  预约详情 */
	async viewMeet(meetId) {

		let fields = '*';

		let where = {
			_id: meetId,
			MEET_STATUS: ['in', [MeetModel.STATUS.COMM, MeetModel.STATUS.OVER]]
		}
		let meet = await MeetModel.getOne(where, fields);
		if (!meet) return null;


		let getDaysSet = [];
		meet.MEET_DAYS_SET = await this.getDaysSet(meetId, timeUtil.time('Y-M-D')); //今天及以后
		let daysSet = meet.MEET_DAYS_SET;

		let now = timeUtil.time('Y-M-D');
		for (let k = 0; k < daysSet.length; k++) {
			let dayNode = daysSet[k];

			if (dayNode.day < now) continue; // 排除过期

			let getTimes = [];

			for (let j in dayNode.times) {
				let timeNode = dayNode.times[j];

				// 排除状态关闭的时段
				if (timeNode.status != 1) continue;

				// 判断数量是否已满
				if (timeNode.isLimit && timeNode.stat.succCnt >= timeNode.limit)
					timeNode.error = '预约已满';

				// 截止规则
				if (!timeNode.error) {
					try {
						await this.checkMeetEndSet(meet, timeNode.mark);
					} catch (ex) {
						if (ex.name == 'AppError')
							timeNode.error = '预约结束';
						else
							throw ex;
					}
				}

				getTimes.push(timeNode);
			}
			dayNode.times = getTimes;

			getDaysSet.push(dayNode);
		}

		// 只返回需要的字段
		let ret = {};
		ret.MEET_DAYS_SET = getDaysSet;

		ret.MEET_QR = meet.MEET_QR;
		ret.MEET_TITLE = meet.MEET_TITLE;
		ret.MEET_CATE_NAME = meet.MEET_CATE_NAME;
		ret.MEET_OBJ = meet.MEET_OBJ;
		ret.MEET_UNIT_NAME = meet.MEET_UNIT_NAME;

		return ret;
	}


	/**  预约前获取关键信息 */
	async detailForJoin(userId, meetId, timeMark) {

		let fields = 'MEET_DAYS_SET,MEET_JOIN_FORMS, MEET_TITLE';

		let where = {
			_id: meetId,
			MEET_STATUS: ['in', [MeetModel.STATUS.COMM, MeetModel.STATUS.OVER]]
		}
		let day = this.getDayByTimeMark(timeMark);
		let meet = await this.getMeetOneDay(meetId, day, where, fields);
		if (!meet) return null;

		let dayDesc = timeUtil.fmtDateCHN(this.getDaySetByTimeMark(meet, timeMark).day);

		let timeSet = this.getTimeSetByTimeMark(meet, timeMark);
		let timeDesc = timeSet.start + '~' + timeSet.end;
		meet.dayDesc = dayDesc + ' ' + timeDesc;

		// 取出本人最近一次本时段填写表单
		let whereMy = {
			JOIN_USER_ID: userId,
		}
		let orderByMy = {
			JOIN_ADD_TIME: 'desc'
		}
		let joinMy = await JoinModel.getOne(whereMy, 'JOIN_FORMS', orderByMy);


		if (joinMy)
			meet.myForms = joinMy.JOIN_FORMS;
		else
			meet.myForms = [];

		return meet;
	}

	/** 按天获取预约项目 */
	async getMeetListByDay(unit, day) {
		let where = {
			'meet.MEET_STATUS': ['in', [MeetModel.STATUS.COMM, MeetModel.STATUS.OVER]],
			'day': day,
			DAY_UNIT_NAME: unit,
		};

		let orderBy = {
			'MEET_ORDER': 'asc',
			'MEET_ADD_TIME': 'desc'
		};

		let fields = 'meet.MEET_UNIT_NAME,meet.MEET_ORDER,meet.MEET_ADD_TIME,meet.MEET_TITLE,meet.MEET_DAYS_SET,meet.MEET_OBJ.cover, DAY_MEET_ID, day, times';

		let joinParams = {
			from: MeetModel.CL,
			localField: 'DAY_MEET_ID',
			foreignField: '_id',
			as: 'meet',
		};

		let list = await DayModel.getListJoin(joinParams, where, fields, orderBy, 1, 100, false);
		list = list.list;
		let retList = [];

		for (let k = 0; k < list.length; k++) {

			let usefulTimes = [];

			for (let j in list[k].times) {
				if (list[k].times[j].status != 1) continue;
				usefulTimes.push(list[k].times[j]);
			}

			if (usefulTimes.length == 0) continue;

			let node = {};
			node.timeDesc = usefulTimes.length > 1 ? usefulTimes.length + '个时段' : usefulTimes[0].start;
			node.title = list[k].meet.MEET_TITLE;
			node.pic = list[k].meet.MEET_OBJ.cover;
			node._id = list[k].DAY_MEET_ID;
			retList.push(node);

		}
		return retList;
	}

	/** 获取从某天开始可预约的日期 */
	async getHasDaysFromDay(unit, day) {
		console.log(unit)
		let where = {
			DAY_UNIT_NAME: unit,
			day: ['>=', day],
		};

		let fields = 'DAY_UNIT_NAME,times,day';
		let list = await DayModel.getAllBig(where, fields);

		let retList = [];
		for (let k = 0; k < list.length; k++) {
			for (let n in list[k].times) {
				if (list[k].times[n].status == 1) {
					retList.push(list[k].day);
					break;
				}
			}
		} console.log(retList)
		return retList;
	}

	/** 取得预约分页列表 */
	async getMeetList({
		unit,
		search, // 搜索条件
		sortType, // 搜索菜单
		sortVal, // 搜索菜单
		orderBy, // 排序 
		cateId, //分类查询条件
		page,
		size,
		isTotal = true,
		oldTotal
	}) {

		orderBy = orderBy || {
			'MEET_ORDER': 'asc',
			'MEET_ADD_TIME': 'desc'
		};
		let fields = 'MEET_UNIT_NAME,MEET_TITLE,MEET_OBJ,MEET_DAYS,MEET_CATE_NAME,MEET_CATE_ID';

		let where = {};
		where.and = {
			MEET_UNIT_NAME: unit,
			_pid: this.getProjectId() //复杂的查询在此处标注PID
		};

		if (cateId && cateId !== '0') where.and.MEET_CATE_ID = cateId;

		where.and.MEET_STATUS = ['in', [MeetModel.STATUS.COMM, MeetModel.STATUS.OVER]]; // 状态  

		if (util.isDefined(search) && search) {
			where.or = [
				{ MEET_TITLE: ['like', search] },
			];

		} else if (sortType && util.isDefined(sortVal)) {
			// 搜索菜单
			switch (sortType) {
				case 'sort': {
					orderBy = this.fmtOrderBySort(sortVal, 'NEWS_ADD_TIME');
					break;
				}
				case 'cateId': {
					if (sortVal) where.and.MEET_CATE_ID = String(sortVal);
					break;
				}
			}
		}
		let result = await MeetModel.getList(where, fields, orderBy, page, size, isTotal, oldTotal);

		return result;
	}



	/** 取消我的预约 只有成功可以取消 */
	async cancelMyJoin(userId, joinId) {
		let where = {
			JOIN_USER_ID: userId,
			_id: joinId,
			JOIN_IS_CHECKIN: 0, // 核销不能取消
			JOIN_STATUS: JoinModel.STATUS.SUCC
		};
		let join = await JoinModel.getOne(where);

		if (!join) {
			this.AppError('未找到可取消的预约记录');
		}

		// 取消规则判定
		let whereMeet = {
			_id: join.JOIN_MEET_ID,
			MEET_STATUS: ['in', [MeetModel.STATUS.COMM, MeetModel.STATUS.OVER]]
		}
		let meet = await this.getMeetOneDay(join.JOIN_MEET_ID, join.JOIN_MEET_DAY, whereMeet);
		if (!meet) this.AppError('预约项目不存在或者已关闭');

		let daySet = this.getDaySetByTimeMark(meet, join.JOIN_MEET_TIME_MARK);
		let timeSet = this.getTimeSetByTimeMark(meet, join.JOIN_MEET_TIME_MARK);
		if (!timeSet) this.AppError('被取消的时段不存在');


		if (meet.MEET_CANCEL_SET == 0)
			this.AppError('该预约不能取消');


		let startT = daySet.day + ' ' + timeSet.start + ':00';
		let startTime = timeUtil.time2Timestamp(startT);
		let now = timeUtil.time();
		if (meet.MEET_CANCEL_SET == 2 && now > startTime)
			this.AppError('该预约时段已经开始,无法取消');

		// TODO 已过期不能取消


		await JoinModel.del(where);


		// 统计
		this.statJoinCnt(join.JOIN_MEET_ID, join.JOIN_MEET_TIME_MARK);

	}

	/** 取得我的预约详情 */
	async getMyJoinDetail(userId, joinId) {

		let fields = 'JOIN_UNIT_NAME,JOIN_COMPLETE_END_TIME,JOIN_IS_CHECKIN,JOIN_CHECKIN_TIME,JOIN_REASON,JOIN_MEET_ID,JOIN_MEET_TITLE,JOIN_MEET_DAY,JOIN_MEET_TIME_START,JOIN_MEET_TIME_END,JOIN_STATUS,JOIN_ADD_TIME,JOIN_CODE,JOIN_FORMS';

		let where = {
			_id: joinId,
			JOIN_USER_ID: userId
		};
		return await JoinModel.getOne(where, fields);
	}

	/** 取得我的预约分页列表 */
	async getMyJoinList(userId, {
		search, // 搜索条件
		sortType, // 搜索菜单
		sortVal, // 搜索菜单
		orderBy, // 排序 
		page,
		size,
		isTotal = true,
		oldTotal
	}) {
		orderBy = orderBy || {
			//	'JOIN_MEET_DAY': 'desc',
			//	'JOIN_MEET_TIME_START': 'desc',
			'JOIN_ADD_TIME': 'desc'
		};
		let fields = 'JOIN_UNIT_NAME,JOIN_COMPLETE_END_TIME,JOIN_IS_CHECKIN,JOIN_REASON,JOIN_MEET_ID,JOIN_MEET_TITLE,JOIN_MEET_DAY,JOIN_MEET_TIME_START,JOIN_MEET_TIME_END,JOIN_STATUS,JOIN_ADD_TIME,JOIN_OBJ';

		let where = {
			JOIN_USER_ID: userId
		};
		//where.MEET_STATUS = ['in', [MeetModel.STATUS.COMM, MeetModel.STATUS.OVER]]; // 状态  

		if (util.isDefined(search) && search) {
			where['JOIN_MEET_TITLE'] = {
				$regex: '.*' + search,
				$options: 'i'
			};
		} else if (sortType) {
			// 搜索菜单
			switch (sortType) {

				case 'cateId': {
					if (sortVal) where.JOIN_MEET_CATE_ID = String(sortVal);
					break;
				}
				case 'all': { //所有 
					break;
				}
				case 'use': { //可用未过期
					where.JOIN_STATUS = JoinModel.STATUS.SUCC;
					where.JOIN_COMPLETE_END_TIME = ['>=', timeUtil.time('Y-M-D h:m')];
					break;
				}
				case 'check': { //已核销
					where.JOIN_STATUS = JoinModel.STATUS.SUCC;
					where.JOIN_IS_CHECKIN = 1;
					break;
				}
				case 'timeout': { //已过期未核销
					where.JOIN_STATUS = JoinModel.STATUS.SUCC;
					where.JOIN_IS_CHECKIN = 0;
					where.JOIN_COMPLETE_END_TIME = ['<', timeUtil.time('Y-M-D h:m')];
					break;
				}
				case 'succ': { //预约成功
					where.JOIN_STATUS = JoinModel.STATUS.SUCC;
					//where.JOIN_MEET_DAY = ['>=', timeUtil.time('Y-M-D h:m')];
					//where.JOIN_MEET_TIME_START = ['>=', timeUtil.time('h:m')];
					break;
				}
				case 'cancel': { //已取消
					where.JOIN_STATUS = ['in', [JoinModel.STATUS.CANCEL, JoinModel.STATUS.ADMIN_CANCEL]];
					break;
				}
			}
		}
		let result = await JoinModel.getList(where, fields, orderBy, page, size, isTotal, oldTotal);

		return result;
	}

	/** 取得我的某日预约列表 */
	async getMyJoinSomeday(userId, day) {

		let fields = 'JOIN_IS_CHECKIN,JOIN_MEET_ID,JOIN_MEET_TITLE,JOIN_MEET_DAY,JOIN_MEET_TIME_START,JOIN_MEET_TIME_END,JOIN_STATUS,JOIN_ADD_TIME';

		let where = {
			JOIN_USER_ID: userId,
			JOIN_MEET_DAY: day
		};
		//where.MEET_STATUS = ['in', [MeetModel.STATUS.COMM, MeetModel.STATUS.OVER]]; // 状态  

		let orderBy = {
			'JOIN_MEET_TIME_START': 'asc',
			'JOIN_ADD_TIME': 'desc'
		}

		return await JoinModel.getAll(where, fields, orderBy);


	}
}

UI设计

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

admin UI

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

git代码下载

点击下载

微信小程序开发实战:大学自习室预约系统 微信小程序自2017年推出以来,已经成为移动互联网生态中不可或缺的一部分。微信小程序以其无需下载安装,即点即用的便捷性深受用户喜爱。开发者们可以利用微信提供的开发工具包(SDK)和丰富的API接口,快速构建出功能丰富的应用。通过它,开发者能够实现与用户之间的无缝连接,为用户提供各种便捷服务。本章将从总体上概述微信小程序的开发背景、特点和开发环境,为后续章节深入探讨各模块开发和优化打下基础。为了适应不同场景的预约需求,小程序的预约管理模块需要具备灵活的预约信息填写项功能。 阅读详情

相关推荐

Isaac Sim仿真平台学习(2)系统架构

isaac sim系统架构

weixin_75067193的博客 2698

从0到1:基于微信小程序的瑜伽馆预约平台的开发笔记

背景分析 随着国民健身意识越来越强,各式各样的健身模式不断出现。瑜伽也受到了大众的喜爱,瑜伽行业发展越来越快,作为馆主,你还在微信群里让你的会员使用接龙的方式进行约课吗?你还在用传统的Excel进行排课吗?如果有一款小程序会员点一下就能约课,会不会让你惊喜、意外、激动——没错,瑜伽预约小程序就是为了解决馆主会员约课的痛点应运而生。功能包括瑜伽馆动态,瑜伽常识,瑜伽老师预约,瑜伽课程预约等模块。 该系统基于MVC架构,采用基于微信小程序平台开发,校园用户操作轻松快捷:无需下载安装APP。   功能抽象

CC同学呀 1693

Mac新手必看:用Homebrew安装wget的保姆级教程,附常见报错一键修复

本文为Mac新手提供使用Homebrew安装wget的详细教程,涵盖从Homebrew安装到wget基础使用的全流程,并附常见报错一键修复方案。通过本指南,用户可轻松掌握命令行下载工具wget的安装与使用技巧,解决Mac环境下资源下载的各种问题。

weixin_30635053的博客 366

从0到1:理发店预约剪发小程序开发笔记(上)

理发师可以在小程序上设置自己的可预约时间,价格,自我介绍,顾客可以根据理发师的日程安排选择合适的时间进行预约和支付。这样可以提高预约的效率,减少沟通成本,方便双方的安排。

CC同学呀 1196

MIMO技术入门

MIMO:多天线技术,发射端和接收端有多个天线(注意:并不是指多个天线板,而是多套振子,每一套是一个独立的天线。) 4G:2、8天线 5G:4、64天线(64T64R) 手机也是多天线 MIMO实现效果的分类: 1)空间分集 。2)空间复用。3)波束赋形。 空间分集(space diversity) 作用:提升接收质量 利用多个天线发射或者接收一个数据流,避免单个信道衰落对整个链路的影响。(比如你的耳机坏了一只并不影响另一只) 空间复用(space multiplexing) 作用:提升速率 利

About1129的博客 2873

【71页PPT】工业40及智能制造解决方案(附下载方式)

2013年,工业4.0平台发布白皮书,正式确立其发展战略。工业4.0不仅是技术的升级,更是制造业思维的重构。工业4.0的核心是通过信息物理系统(CPS)实现工厂内外的互联,构建高度灵活的个性化生产模式。它不仅是技术革新,更是一种生产方式的变革,使传统行业界限模糊化,催生新的商业模式和合作形式。德国作为制造业强国,其GDP的25%和出口总额的60%依赖制造业,因此工业4.0成为其维持全球领先地位的重要战略。CPS是工业4.0的基石,通过计算、通信与控制技术(3C)实现物理世界与信息世界的深度融合。

2501_92808811的博客 950

基于微信小程序的运动场馆预约小程序开发笔记

背景和需求分析 随着社会的在新时代下的飞跃发展,尤其是奥运会和冬奥会的成功举办,全民健身的热潮兴起,在平时紧张的工作,学习之余,重视身体,重视身材,重视 健康的需求越来越强烈,那么设计一款针对广大运动爱好者的小程序,使运动爱好者和体育场馆之间能有效的衔接,合理的规划时间和场地,节省双方来往成本的 需求痛点是成立的。 好的场馆预约系统包括场馆动态,运动常识,羽毛球场地预约,足球场地预约,篮球场地预约,健身房预约,乒乓球场地预约,游泳场地预约,网球场地预约等,可以按照业者的实际情况进行取舍或者增添场馆类型,既适

CC同学呀 5953

ESP32-S3射频测试与全球认证全栈实践指南

功率谱密度(PSD)和发射功率等级是无线通信设备射频合规的核心基础概念,其物理层原理直接决定自适应测试、阻塞容限及多国认证的通过路径。PSD ≥10 dBm/MHz触发强制性自适应测试,需验证LBT机制与跳频鲁棒性;而16级可编程发射功率等级(-24 dBm至+20 dBm)则为功耗、覆盖与法规平衡提供工程调控维度。该能力广泛应用于低功耗蓝牙IoT终端、智能家居网关及工业无线传感器等场景,尤其在ESP32-S3平台中,需结合eFuse校准、Sigma工具链与QuickTrack认证策略实现从实验室调试到量产

weixin_34945060的博客 285

医院预约挂号系统微信小程序项目分享

不知不觉已经毕业一年多了,分享一个大学毕业设计完成的作品,这个是一个医院预约挂号的小程序系统,这个项目比较简易理解,很适合做计算机期末作业或者毕业设计。 医院预约挂号系统微信小程序项目主要的功能模块分析如下: 对该系统进行分析与设计,有以下几个主要的功能模块: (1)添加科室:通过后台管理系统新增对应的科室,可以对科室数据进行增删改查。 (2)添加医生:在后台管理系统录入医生相关信息,绑定...

qq_2422941992的博客 2万+

Windows11上pip安装IsaacSim5.1.0与IsaacLab

本文详细介绍了CUDA Toolkit 12.8.0、IsaacSim和IsaacLab的安装过程。首先从NVIDIA官网下载并安装CUDA Toolkit 12.8.0,然后通过miniforge创建conda环境(env_isaacsim)安装IsaacSim相关Python包,解决了Windows长路径问题。成功运行IsaacSim后,克隆IsaacLab仓库并完成安装验证,最后通过创建空场景测试环境配置。整个安装过程包含详细的命令行操作和问题解决方法,为机器人仿真开发环境搭建提供了完整指导。

拥抱具身智能,昇腾Ascend C 377

TC8工作原理

OPEN组织由全世界主要整车厂和供应商组成,旨在推广以太网技术在车辆中的应用。OPEN下属多个技术委员会(Tech Committee,简称TC),对不同方面的技术细节进行量化定义,并发布相应规范。其中TC8规范包括车载以太网ECU从物理层到应用层的各层互操作性以及常规基础功能服务,目的在于提高不同ECU之间的兼容性 UpperTester UpperTester作为Tester与DUT的被测核心功能模块之间的中介,辅助完成测试。UpperTester的主要功能包括: 与Tester就测试模式切换/请.

jasonj333 5324

从0到1:培训老师预约小程序开发笔记二

培训老师预约小程序: 教师和学生可以更便捷地安排课程,并提升教学质量和学习效果,使之成为管理和提升教学效果的强大工具。培训老师可以在小程序上设置自己的可预约时间,学员可以根据老师的日程安排选择合适的时间进行预约。这样可以提高预约的效率,减少沟通成本,方便双方的安排。

CC同学呀 865

第10集:AIOps 平台架构设计!画出面试官最想看的系统全景图

本文介绍了如何将AIOps平台的六大功能模块整合为四层架构设计,并生成专业系统全景图。文章首先指出面试中架构设计能力的重要性,强调一张清晰的架构图能直观展现全局观。随后指导读者检查项目结构,准备绘图环境。核心部分详细拆解了四层架构:数据采集层(Prometheus、ELK等)、AI分析层(巡检、告警、预测等)、决策执行层(工作流引擎、自愈Agent)和展示交互层(API、控制台)。通过Python代码自动生成模块依赖关系图,并提供了面试讲解技巧。全文旨在帮助开发者在面试中系统性地展示项目架构设计能力。

qq_2937655951的博客 92

关于微信小程序中时间预约的简单实现

关于微信小程序中时间预约的简单实现一级目录二级目录三级目录 在平时小程序的学习中遇到一个需求,就是要预约未来一星期内的某段时间,并自动返回是否可以预约的状态。 那么本人尝试过的解决的思路可以是如下的几个步骤:(在下初出茅庐、技拙请多多海涵) 在小程序对应Page的js文件中定义获取未来某天的方法 项目2 项目3 一级目录 计划任务 完成任务 二级目录 三级目录 ...

qq_46015945的博客 1万+

怀旧版电脑GBC模拟器

再现原版GBC掌机的界面,电脑运行连游戏界面都一样大小,很怀旧,很真实,(英文版)强烈推荐,很小的文件,很难才找到的。

Data-Structures-and-Algorithm-Analysis-in-Java.pdf

作者: Weiss, Mark A. 出版社: Addison Wesley Data Structures and Algorithm Analysis in Java is an “advanced algorithms” book that fits between traditional CS2 and Algorithms Analysis courses. In the old ACM Curriculum Guidelines, this course was known as CS7. This text is for readers who want to learn good programming and algorithm analysis skills simultaneously so that they can develop such programs with the maximum amo...

预约小程序多选修改——思路分享

微信小程序开发

Kirito_UGO的博客 1478

C# WinForm 基于DirectShow.dll编写的程序,可边预览边保存视频,音频和图片

基于微软的DirectShow.dll的二次开发1.自动识别连接的摄像头跟麦克风且可以对其进行选择(默认选中第一个)2.可录制wmv和avi两种格式的视频并且都带有音频,avi可以录制的分辨率和帧数进行选择,wmv默认进行了淸晰度优化如不喜欢可去除代码中有相关提示3.可在预览点开之后进行拍照,也可边录制视频边拍照4.添加了视频和音频的压缩功能(酌情使用)这次二次开发发现C#对DirectShow的相关信息太少,这次做完也不敢藏私共享出来,还望大家指正

从0-1:健身房私教预约小程序开发笔记

在当代“瘦身热”的带动下,到健身房运动和瘦身的人员也在不断增加,一定程度上加大了健身房的管理难度,因此,开发一款属于健身房预约的小程序这时候正好可以派上用场,包含用户端,教练端,管理端三方

CC同学呀 661

SIMATIC S7-1200, S7-1500 PID 控制[手册]

SIMATIC S7-1200, S7-1500 PID 控制[手册]

如何快速制作微信小程序预约功能--在这里只要十分钟

1 .概述 我们在学习微信小程序或者做项目时,应该会遇到需要时间预约功能情况,那么这个时间预约功能我们应该怎么编写呢?于是就做了一个类似电商的时间预约功能,觉得有用,就独立出来成了个小插件,今天我们就分享这样的小教程。希望对大家有所帮助。 不多说了,二当家要上图来啦! 更多自ji关注下载 2. wxml &amp;lt;!--pages/orderTime/index.wx...

极客小寨的博客 2万+

从0到1开发一个预约小程序,我踩过的坑

预约系统是很常见的需求:理发店要预约、诊所要看诊、教育机构要排课、健身房要约课...看似简单的功能,背后其实有不少弯弯绕绕。我最近做了一个预约小程序,从需求分析到上线跑通,整个过程踩了不少坑。这篇文章就把这些经验整理出来,供有类似需求的朋友参考。预约系统看似简单,实际上要考虑的场景很多:时间段管理、库存控制、并发安全、取消退款、提醒通知...做的时候多花点心思,上线后才能少踩坑。如果你正在做或者准备做预约系统,有什么问题可以找我聊聊。技术选型、方案设计、开发实现,都可以聊。你做过预约系统吗?

czy的博客 398

从0到100: 基于微信云开发的驾校预约学车小程序

随着机动车逐渐走进寻常百姓家中,学车也随之被人们提上了日程,这使得汽车驾驶培训行业得到迅猛发展。移动互联网技术的广泛应用,使手机约车成为可能,如何合理地分配已有资源,提高资源利用率,增强驾校服务水平,己成为驾校越来越迫切的需求。驾校学车预约小程序使预约者只需拿起手机,就可以线上约车,可以做到“足不出户选教练”,这样能够有效减少学习者的等待时间,从而为学员提供更优质的服务。

CC同学呀 1340

微信小程序前端开发(预约功能1)

定义items数组内容,属于数据的范畴,需要在页面.js文件中编辑data: {items: [{ id: 1, text: "可约时段1" ,count:3},{ id: 2, text: "可约时段2" ,count:3},{ id: 3, text: "可约时段3" ,count:3},{ id: 4, text: "可约时段4" ,count:3},{ id: 5, text: "可约时段5" ,count:3},{ id: 6, text: "可约时段6" ,count:3}],

songguiren的博客 975

计算机毕业设计springboot体检预约小程序 医疗体检预约小程序 健康体检预约平台开发

用户注册与登录:用户可以通过手机号码或微信账号进行注册和登录,方便快捷地进入小程序使用各项功能。体检机构查询:用户可以根据地理位置、机构类型、口碑评分等条件筛选并查询附近的体检机构,查看机构的详细信息,如地址、营业时间、特色项目等,以便选择最合适的体检机构。体检套餐浏览与选择:小程序提供多种体检套餐供用户选择,用户可以根据自己的年龄、性别、健康状况等因素挑选合适的套餐,并查看套餐包含的具体项目和价格。在线预约:用户选定体检套餐和体检机构后,可选择预约日期和时间,提交预约申请。

学姐计算机毕设程序 1202

从0到100:定制公交预约小程序开发笔记

可以让用户通过手机随时随地进行公交车的预约,无需排队等候,节省了用户的时间和精力。通过公交预约小程序,用户可以提前预约座位,避免了拥挤和等待时间,提高了公交出行的效率。可以根据用户的预约情况,合理安排车辆和座位,减少了公交车的拥挤和堵塞现象,提升了公交出行的舒适度。

CC同学呀 401

基于微信小程序的驾校预约小程序设计与实现

今天带来的是基于微信小程序的驾校预约小程序设计与实现,随着移动互联网的迅猛发展及人们生活水平的提高,传统驾校预约方式存在的效率低下、资源分配不均等问题日益凸显。微信小程序以其无需下载、即用即走的便捷特性,成为优化驾校预约管理服务的理想选择。该系统旨在通过微信平台庞大的用户基础和丰富的功能接口,为学员提供灵活便捷的预约体验,同时帮助驾校实现信息化管理,提升管理效率和服务质量,从而满足现代科技发展趋势和市场需求。

qq_15801219的博客 1265

从0到1:基于SpringBoot的体育场馆预约小程序开发笔记(一)

给用户提供便捷的预约平台,方便他们查询场馆信息、选择合适时间并进行预约;主要功能包括,展示不同类型和位置的场馆,选择日期和时间,完成场馆预约,用户查看历史订单和预约状态等。

CC同学呀 493

从0到1:大学校园参观预约小程序开发笔记(一)

校园参观预约小程序,包含参观者和管理人员,门卫角色,实现不同人群的操作需求。整个系统包括:预约模块、用户模块、公告模块、后台管理模块。

CC同学呀 851
上一篇: 从0到1:小区业主决策投票小程序开发笔记
下一篇: 从0到1:基于SpringBoot的体育场馆预约小程序开发笔记(一)
CC同学呀
码龄17年 331粉丝 139原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值