从 0 到 1:多服务厅预约小程序开发笔记(上)
 作者:CC同学
- 2024-10-09  广东
 本文字数:10098 字
阅读完需:约 33 分钟
需求调研
多服务厅预约小程序:随着信息技术的快速发展和移动互联网的普及,越来越多的服务行业开始向线上转型, 传统的预约方式往往效率低下,用户需耗费大量时间进行电话预约或现场排队,服务厅预约小程序集多种服务于一体,可以提高服务效率、提升用户体验。 用户可以浏览不同服务厅的位置、服务项目和服务时间,用户可以选择日期、时间段和人数,然后进行预约。
功能设计
公告通知,用户预约,签到核销, 管理者可以自定义预约要填写的内容,比如姓名、性别、年龄、身份证、手机号等,后台预约管理,后台预约名单管理和导出 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 代码下载
划线
评论
复制
发布于: 刚刚阅读数: 4
CC同学
关注
CC同学的小程序开发笔记 2021-06-13 加入
大鹅厂的小小程序媛,vx: cclinux0730







    


评论