背景調研
培訓老師預約小程序: 教師和學生可以更便捷地安排課程,并提升教學質量和學習效果,使之成為管理和提升教學效果的強大工具。培訓老師可以在小程序上設置自己的可預約時間,學員可以根據老師的日程安排選擇合適的時間進行預約。這樣可以提高預約的效率,減少溝通成本,方便雙方的安排
功能規劃
-
- 首頁展示:展示最新的公告通知、老師推薦等內容,吸引用戶關注。
-
- 老師列表:列出所有開課的老師信息,包括老師的個人簡介、星級等,方便用戶選擇合適的老師。
-
- 預約功能:用戶可以根據自己的需求選擇老師,并進行預約。
-
- 預約管理:用戶可以查看自己的預約記錄,包括已完成的預約和待核銷的預約,也可以取消預約。
-
后臺端:可以添加和設定老師的基本信息,賬號,登陸密碼等。
-
老師端:可以編輯自己的個人資料(頭像,簡介,標簽集等),設定預約時段排期(可預約時段,各時段人數限定), 在現場核銷用戶的預約碼。
-
顧客端:選擇自己需要的老師和時段,下單預約,預約成功后出示預約碼給老師或者工作人員核銷。
概要設計
數據庫設計
MeetModel.DB_STRUCTURE = {_pid: 'string|true',MEET_ID: 'string|true',MEET_ADMIN_ID: 'string|true|comment=添加的管理員',MEET_TITLE: 'string|true|comment=標題',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_PHONE: 'string|false|comment=登錄手機',MEET_PASSWORD: 'string|false|comment=登錄密碼',MEET_TOKEN: 'string|false|comment=當前登錄token',MEET_TOKEN_TIME: 'int|true|default=0|comment=當前登錄token time',MEET_MINI_OPENID: 'string|false|comment=小程序openid',MEET_LOGIN_CNT: 'int|true|default=0|comment=登錄次數',MEET_LOGIN_TIME: 'int|false|comment=最近登錄時間',MEET_ADD_TIME: 'int|true',MEET_EDIT_TIME: 'int|true',MEET_ADD_IP: 'string|false',MEET_EDIT_IP: 'string|false',
};UserModel.DB_STRUCTURE = {_pid: 'string|true',USER_ID: 'string|true',USER_MINI_OPENID: 'string|true|comment=小程序openid',USER_STATUS: 'int|true|default=1|comment=狀態 0=待審核,1=正常,8=審核未過,9=禁用',USER_CHECK_REASON: 'string|false|comment=審核未過的理由',USER_NAME: 'string|false|comment=用戶昵稱',USER_MOBILE: 'string|false|comment=聯系電話',USER_FORMS: 'array|true|default=[]',USER_OBJ: 'object|true|default={}',USER_LOGIN_CNT: 'int|true|default=0|comment=登錄次數',USER_LOGIN_TIME: 'int|false|comment=最近登錄時間',USER_ADD_TIME: 'int|true',USER_ADD_IP: 'string|false',USER_EDIT_TIME: 'int|true',USER_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;} elselimitDesc = '人數不限制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 = '預約結束';elsethrow 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;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;elsemeet.myForms = [];return meet;}/** 按天獲取預約項目 */async getMeetListByDay(day) {let where = {'meet.MEET_STATUS': ['in', [MeetModel.STATUS.COMM, MeetModel.STATUS.OVER]],'day': day,};let orderBy = {'MEET_ORDER': 'asc','MEET_ADD_TIME': 'desc'};let fields = '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(day) {let where = {day: ['>=', day],};let fields = '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;}}}return retList;}/** 取得預約分頁列表 */async getMeetList({search, // 搜索條件sortType, // 搜索菜單sortVal, // 搜索菜單orderBy, // 排序 cateId, //分類查詢條件page,size,isTotal = true,oldTotal}) {orderBy = orderBy || {'MEET_ORDER': 'asc','MEET_ADD_TIME': 'desc'};let fields = 'MEET_TITLE,MEET_OBJ,MEET_DAYS,MEET_CATE_NAME,MEET_CATE_ID';let where = {};where.and = {_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_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_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設計
老師端設計
管理端
git代碼下載
git下載