概要設計
醫院陪診預約小程序:隨著移動互聯網的普及,越來越多的醫院陪診服務開始向線上轉型, 傳統的預約方式往往效率低下,用戶需耗費大量時間進行電話預約或現場排隊,陪診服務預約小程序集多種服務于一體,可以提高服務效率、提升用戶體驗。 用戶可以瀏覽不同醫院的位置、陪診項目和陪診時間,用戶可以選擇日期、時間段和人數,然后進行預約。
功能規劃
本項目前后端完整代碼包括公告通知,用戶預約,簽到核銷, 管理者可以自定義預約要填寫的內容,比如姓名、性別、年齡、診療科室、手機號等,后臺預約管理,后臺預約名單管理和導出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',
};JoinModel.DB_STRUCTURE = {_pid: 'string|true',JOIN_ID: 'string|true',JOIN_UNIT_ID: 'string|false',JOIN_UNIT_NAME: 'string|false',JOIN_EDIT_ADMIN_ID: 'string|false|comment=最近修改的管理員ID',JOIN_EDIT_ADMIN_NAME: 'string|false|comment=最近修改的管理員名',JOIN_EDIT_ADMIN_TIME: 'int|true|default=0|comment=管理員最近修改的時間',JOIN_EDIT_ADMIN_STATUS: 'int|false|comment=最近管理員修改為的狀態 ',JOIN_IS_ADMIN: 'int|true|default=0|comment=是否管理員添加 0/1',JOIN_CODE: 'string|true|comment=核驗碼15位',JOIN_IS_CHECKIN: 'int|true|default=0|comment=是否核銷 0/1 ',JOIN_CHECKIN_TIME: 'int|true|default=0',JOIN_USER_ID: 'string|true|comment=用戶ID',JOIN_MEET_ID: 'string|true|comment=預約PK',JOIN_MEET_CATE_ID: 'string|true',JOIN_MEET_CATE_NAME: 'string|true',JOIN_MEET_TITLE: 'string|true|comment=項目',JOIN_MEET_DAY: 'string|true|comment=日期',JOIN_MEET_TIME_START: 'string|true|comment=時段開始',JOIN_MEET_TIME_END: 'string|true|comment=時段結束',JOIN_MEET_TIME_MARK: 'string|true|comment=時段標識',JOIN_COMPLETE_END_TIME: 'string|false|comment=完整結束時間',JOIN_START_TIME: 'int|true|comment=開始時間戳',JOIN_FORMS: 'array|true|default=[]|comment=表單',/* title:mark:type:val:*/JOIN_OBJ: 'object|true|default={}',JOIN_STATUS: 'int|true|default=1|comment=狀態 1=預約成功,10=已取消, 99=系統取消',JOIN_REASON: 'string|false|comment=審核拒絕或者取消理由',JOIN_ADD_TIME: 'int|true',JOIN_EDIT_TIME: 'int|true',JOIN_ADD_IP: 'string|false',JOIN_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;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;elsemeet.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設計
后臺管理系統設計
git下載
github下載