Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Building a Fishing Venue Reservation Mini-program: Backend Architecture and Core Logic

Tech May 12 2

Background

A platform designed for fishing venue owners and enthusiasts to facilitate information sharing and service access.

Feature Planning

  • Venue Booking: Users can browse locations, facilities, and availability of different venues, selecting date, time slot, and number of participants for booking.
  • Fishing Events: Users can view event details and register for competitions. Organizers manage registrations, including check-ins, records, photos, and feedback.
  • Event Analytics: The system tracks participation numbers, ratings, and user feedback.

Sysstem Overview

Database Schema

ActivityModel.DB_STRUCTURE = {
  _pid: 'string|true',
  ACTIVITY_ID: 'string|true',
  ACTIVITY_TITLE: 'string|true|comment=Title',
  ACTIVITY_STATUS: 'int|true|default=1|comment=Status 0=Inactive,1=Active',
  ACTIVITY_CATE_ID: 'string|true|default=0|comment=Category ID',
  ACTIVITY_CATE_NAME: 'string|false|comment=Category Name',
  ACTIVITY_CANCEL_SET: 'int|true|default=1|comment=Cancel Policy 0=Not Allowed,1=Allowed,2=Only Before Start',
  ACTIVITY_CHECK_SET: 'int|true|default=0|comment=Review 0=None,1=Required',
  ACTIVITY_IS_MENU: 'int|true|default=1|comment=Public List',
  ACTIVITY_MAX_CNT: 'int|true|default=20|comment=Max Participants 0=Unlimited',
  ACTIVITY_START: 'int|false|comment=Start Time',
  ACTIVITY_END: 'int|false|comment=End Time',
  ACTIVITY_STOP: 'int|true|default=0|comment=Registration Deadline 0=Never',
  ACTIVITY_ORDER: 'int|true|default=9999',
  ACTIVITY_VOUCH: 'int|true|default=0',
  ACTIVITY_FORMS: 'array|true|default=[]',
  ACTIVITY_OBJ: 'object|true|default={}',
  ACTIVITY_JOIN_FORMS: 'array|true|default=[]',
  ACTIVITY_ADDRESS: 'string|false|comment=Address',
  ACTIVITY_ADDRESS_GEO: 'object|false|comment=Coordinates',
  ACTIVITY_QR: 'string|false',
  ACTIVITY_VIEW_CNT: 'int|true|default=0',
  ACTIVITY_JOIN_CNT: 'int|true|default=0',
  ACTIVITY_COMMENT_CNT: 'int|true|default=0',
  ACTIVITY_USER_LIST: 'array|true|default=[]|comment={name,id,pic}',
  ACTIVITY_ADD_TIME: 'int|true',
  ACTIVITY_EDIT_TIME: 'int|true',
  ACTIVITY_ADD_IP: 'string|false',
  ACTIVITY_EDIT_IP: 'string|false',
};

MeetModel.DB_STRUCTURE = {
  _pid: 'string|true',
  MEET_ID: 'string|true',
  MEET_ADMIN_ID: 'string|true|comment=Admin ID',
  MEET_TITLE: 'string|true|comment=Title',
  MEET_JOIN_FORMS: 'array|true|default=[]|comment=Registration Fields',
  MEET_DAYS: 'array|true|default=[]|comment=Available Dates',
  MEET_CATE_ID: 'string|true|comment=Category ID',
  MEET_CATE_NAME: 'string|true|comment=Category Name',
  MEET_ADDRESS: 'string|false|comment=Address',
  MEET_ADDRESS_GEO: 'object|false|comment=Coordinates',
  MEET_FORMS: 'array|true|default=[]',
  MEET_OBJ: 'object|true|default={}',
  MEET_CANCEL_SET: 'int|true|default=1|comment=Cancel Policy 0=Not Allowed,1=Allowed,2=Before Start Only',
  MEET_STATUS: 'int|true|default=1|comment=Status 0=Inactive,1=Active,9=Booking Closed,10=Closed',
  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',
};

Core Service Implementation

class MeetService extends BaseProjectService {
  constructor() {
    super();
    this._log = new LogUtil(projectConfig.MEET_LOG_LEVEL);
  }

  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);
  }

  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,
      cancelCnt: ret['JOIN_STATUS_10'] || 0,
      adminCancelCnt: ret['JOIN_STATUS_99'] || 0
    };
    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);
  }

  async join() {
    // Implementation here
  }

  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('Invalid meeting time settings');
    let daySet = this.getDaySetByTimeMark(meet, timeMark);
    let timeSet = this.getTimeSetByTimeMark(meet, timeMark);
    if (!daySet || !timeSet) this.AppError('Invalid meeting time settings');
    let statusDesc = timeSet.status == 1 ? 'Open' : 'Closed';
    let limitDesc = '';
    if (timeSet.isLimit) {
      limitDesc = 'Max=' + timeSet.limit;
    } else {
      limitDesc = 'No Limit';
    }
    this._meetLog(meet, `------------------------------`);
    this._meetLog(meet, `#Time Control, Date=<${daySet.day}>`, `Time Slot=[${timeSet.start}-${timeSet.end}], Status=${statusDesc}, ${limitDesc}, Success Count=${timeSet.stat.succCnt}`);
    if (timeSet.status == 0) this.AppError('Time slot is closed');
    if (timeSet.isLimit) {
      if (timeSet.stat.succCnt >= timeSet.limit) {
        this.AppError('Time slot is full');
      }
      let maxCnt = timeSet.limit - timeSet.stat.succCnt;
      if (maxCnt < meetPeopleCnt) {
        this.AppError(`Only ${maxCnt} spots left, you requested ${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('Invalid time slot selection');
    }
    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('Invalid booking rule');
    let meetId = meet._id;
    let daySet = this.getDaySetByTimeMark(meet, timeMark);
    let timeSet = this.getTimeSetByTimeMark(meet, timeMark);
    this._meetLog(meet, `------------------------------`);
    this._meetLog(meet, `#Booking Limit, Date=<${daySet.day}>`, `Time Slot=[${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, `Booking Limit, Max=${maxCnt}`, `Current=${cnt}`);
    if (cnt >= maxCnt)
      this.AppError(`Already booked for this time slot`);
  }

  async checkMeetEndSet(meet, timeMark) {
    if (!meet) this.AppError('Invalid booking deadline');
    this._meetLog(meet, `------------------------------`);
    let daySet = this.getDaySetByTimeMark(meet, timeMark);
    let timeSet = this.getTimeSetByTimeMark(meet, timeMark);
    this._meetLog(meet, `#Booking Deadline, Date=<${daySet.day}>`, `Time Slot=[${timeSet.start}-${timeSet.end}]`);
    let nowTime = timeUtil.time('Y-M-D h:m:s');
    let endTime = daySet.day + ' ' + timeSet.end + ':59';
    this._meetLog(meet, `Deadline Check`, `End Time=${endTime}, Now=${nowTime}`);
    if (nowTime > endTime) {
      this.AppError('Time slot has ended');
    }
  }

  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 = 'Full';
        if (!timeNode.error) {
          try {
            await this.checkMeetEndSet(meet, timeNode.mark);
          } catch (ex) {
            if (ex.name == 'AppError')
              timeNode.error = 'Ended';
            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_ADDRESS = meet.MEET_ADDRESS;
    ret.MEET_ADDRESS_GEO = meet.MEET_ADDRESS_GEO;
    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(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 + ' slots' : 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_ADDRESS,MEET_TITLE,MEET_OBJ,MEET_DAYS,MEET_CATE_NAME,MEET_CATE_ID';
    let where = {};
    where.and = {
      _pid: this.getProjectId()
    };
    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('No cancellable booking found');
    }
    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('Venue not available');
    let daySet = this.getDaySetByTimeMark(meet, join.JOIN_MEET_TIME_MARK);
    let timeSet = this.getTimeSetByTimeMark(meet, join.JOIN_MEET_TIME_MARK);
    if (!timeSet) this.AppError('Time slot not found');
    if (meet.MEET_CANCEL_SET == 0)
      this.AppError('Cancellation not allowed');
    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('Too late to cancel');
    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_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
    };
    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;
          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
    };
    let orderBy = {
      'JOIN_MEET_TIME_START': 'asc',
      'JOIN_ADD_TIME': 'desc'
    };
    return await JoinModel.getAll(where, fields, orderBy);
  }
}

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.