背景调研

旅拍店或者摄影师可以在小程序上设置自己的可预约时间,价格,拍摄介绍,并定义不同的套餐(服装套数,底片张数,精修数量,取景风格,套餐价格等),顾客可以根据套餐内容和日程安排选择合适的时间进行预约和在线支付。这样可以提高预约的效率,减少沟通成本,方便双方的安排

功能规划

  • 首页展示:展示最新的公告通知、拍摄项目介绍,摄影师推荐等内容,吸引用户关注。
  • 摄影师列表:列出所有在班的摄影师信息,包括个人简介、星级、套餐介绍及不同价格等,方便用户选择。
  • 拍摄项目列表:列出所有可拍摄项目,包括简介、星级、套餐介绍及不同价格等,方便用户选择。
  • 预约功能:用户可以根据自己的需求选择套餐,进行预约并支付费用。
  • 预约管理:用户可以查看自己的预约记录,包括已完成的预约和待核销的预约,也可以取消预约(所支付费用原路退回)。

概要设计

image.png

数据库设计

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_ITEM_LIST: 'array|true|default=[]',
    MEET_ITEM_MIN_PRICE: 'int|true|default=0|comment=最低价格',


    MEET_PAY_CNT: 'int|true|default=0|comment=支付数',
    MEET_PAY_FEE: 'int|true|default=0|comment=支付额',
    MEET_JOIN_CNT: 'int|true|default=0|comment=预约数',

    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_VIEW_CNT: 'int|true|default=0',

    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_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_CANCEL_TIME: 'int|false|defualt=0|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_FEE: 'int|true|default=0|comment=需支付费用 分',

    JOIN_PAY_TRADE_NO: 'string|false|comment=商家订单号 32位',
    JOIN_PAY_STATUS: 'int|true|default=0|comment=支付状态 0=未支付 1=已支付 8=已退款 99=无需支付',
    JOIN_PAY_FEE: 'int|true|default=0|comment=已支付费用 分',
    JOIN_PAY_TIME: 'int|true|default=0|comment=支付时间', 

    JOIN_ITEM: 'object|true|default={}|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);
    }

    async minuteJob() {
        console.log('### minuteJob >>>>>');


        // 未支付的成功订单取消  
        let time = this._timestamp - 6 * 60 * 1000;
        console.log('###### Begin>>> 未支付订单6分钟后取消, time<=' + time + ', ' + timeUtil.timestamp2Time(time));


        let where = {
            JOIN_STATUS: ['in', [JoinModel.STATUS.SUCC]],
            JOIN_PAY_STATUS: 0,
            JOIN_ADD_TIME: ['<=', time],
        }
        let rows = await JoinModel.getAll(where, '*', {}, 3000, false);
        console.log('未支付订单6分钟后取消, count=', rows.length);

        for (let k in rows) {
            let join = rows[k];

            let tradeNo = join.JOIN_PAY_TRADE_NO;

            if (!await this.fixMeetJoinPay(tradeNo, join.JOIN_MEET_ID, join.JOIN_MEET_TIME_MARK)) {
                console.log('该报名记录未支付,已取消并删除!', join);
            }

        }

        console.log('###### END. 未支付订单6分钟后取消');
    }


    /** 统一获取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);
    }

    // 修正某用户所有未支付的成功订单状态,无须支付的不用处理
    async fixUserMeetJoinPayRecord(userId) {
        let where = {
            JOIN_USER_ID: userId,
            JOIN_PAY_STATUS: 0,
            JOIN_STATUS: ['in', [JoinModel.STATUS.SUCC]],
        }
        let list = await JoinModel.getAll(where);

        for (let k = 0; k < list.length; k++) {
            await this.fixMeetJoinPay(list[k].JOIN_PAY_TRADE_NO, list[k].JOIN_MEET_ID, list[k].JOIN_MEET_TIME_MARK);
        }
    }

    // 修正某订单状态 (仅需支付订单)
    async fixMeetJoinPay(tradeNo, meetId, timeMark) {

        if (!tradeNo) {
            // 无支付号空单 删除
            await JoinModel.del({ JOIN_PAY_TRADE_NO: tradeNo });

            // 重新统计
            this.statJoinCnt(meetId, timeMark);

            return false;
        }

        let payService = new PayService();
        if (!await payService.fixPayResult(tradeNo)) {
            // 关闭未支付单
            payService.closePay(tradeNo);

            // 未支付 
            await JoinModel.del({ JOIN_PAY_TRADE_NO: tradeNo });

            // 重新统计
            this.statJoinCnt(meetId, timeMark);

            return false;
        }

        // 已支付
        let pay = await PayModel.getOne({ PAY_TRADE_NO: tradeNo });
        if (!pay) this.AppError('支付流水异常,请核查');

        // 更新支付信息
        let data = {
            JOIN_PAY_STATUS: 1,
            JOIN_PAY_TRADE_NO: tradeNo,
            JOIN_PAY_FEE: pay.PAY_TOTAL_FEE,
            JOIN_PAY_TIME: pay.PAY_END_TIME,
        }
        await JoinModel.edit({ JOIN_PAY_TRADE_NO: tradeNo }, data);


        // 重新统计
        this.statJoinCnt(meetId, timeMark);
        return true;
    }

    // 用户预约逻辑
    async prepay(userId, meetId, packageIdx, timeMark, formsList) {
     
    }



    // 根据日期获取其所在天设置
    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(userId, meetId) {
        await this.fixUserMeetJoinPayRecord(userId);

        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;

        MeetModel.inc(meetId, 'MEET_VIEW_CNT', 1);

        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 whereJoin = {
            JOIN_MEET_ID: meetId,
            JOIN_STATUS: JoinModel.STATUS.SUCC
        }
        let cntJoin = await JoinModel.count(whereJoin);

        // 收藏人数
        let FavModel = require('../model/fav_model.js');
        let whereFav = {
            FAV_OID: meetId,
        }
        let cntFav = await FavModel.count(whereFav);

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

        ret.cntJoin = cntJoin;
        ret.cntFav = cntFav;

        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_CATE_ID = meet.MEET_CATE_ID;
        ret.MEET_OBJ = meet.MEET_OBJ;
        ret.MEET_VIEW_CNT = meet.MEET_VIEW_CNT;
        ret.cover = await cloudUtil.getTempFileURLOne(meet.MEET_OBJ.cover[0]);
        ret.MEET_PHONE = meet.MEET_PHONE;
        ret.MEET_ITEM_LIST = meet.MEET_ITEM_LIST;

        return ret;
    }


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

        await this.fixUserMeetJoinPayRecord(userId);

        let fields = 'MEET_ITEM_LIST,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 getUsefulTimesByDaysSet(meetId, day) {
        let where = {
            DAY_MEET_ID: meetId,
            day
        }
        let daysSet = await DayModel.getAll(where, 'day,times');
        let usefulTimes = [];
        for (let k = 0; k < daysSet.length; k++) {
            if (daysSet[k].day != day)
                continue;

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

        }
        return usefulTimes;
    }

    /** 按天获取预约项目 */
    async getMeetListByDay(day) {
        let where = {
            MEET_STATUS: MeetModel.STATUS.COMM,
        };

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

        let fields = 'MEET_TITLE,MEET_DAYS_SET,MEET_OBJ.cover';

        let list = await MeetModel.getAll(where, fields, orderBy);

        let retList = [];

        for (let k = 0; k < list.length; k++) {
            let usefulTimes = await this.getUsefulTimesByDaysSet(list[k]._id, day);

            if (usefulTimes.length == 0) continue;

            let node = {};
            node.timeDesc = usefulTimes.length > 1 ? usefulTimes.length + '个时段' : usefulTimes[0].start;
            node.title = list[k].MEET_TITLE;
            node.pic = list[k].MEET_OBJ.cover;
            node._id = list[k]._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_ITEM_LIST,MEET_TITLE,MEET_OBJ,MEET_DAYS,MEET_CATE_NAME,MEET_CATE_ID,MEET_ITEM_MIN_PRICE';

        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 'leveldesc': {
                    orderBy = {
                        'MEET_OBJ.level': 'desc',
                        'MEET_ADD_TIME': 'desc'
                    };
                    break;
                }
                case 'levelasc': {
                    orderBy = {
                        'MEET_OBJ.level': 'asc',
                        'MEET_ADD_TIME': 'desc'
                    };
                    break;
                }
                case 'pricedesc': {
                    orderBy = {
                        'MEET_ITEM_MIN_PRICE': 'desc',
                        'MEET_ADD_TIME': 'desc'
                    };
                    break;
                }
                case 'priceasc': {
                    orderBy = {
                        'MEET_ITEM_MIN_PRICE': 'asc',
                        'MEET_ADD_TIME': 'desc'
                    };
                    break;
                }
                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 已过期不能取消

        if (join.JOIN_PAY_STATUS == 99) {
            // 无须支付
            // 更新记录 
            let data = {
                JOIN_STATUS: JoinModel.STATUS.CANCEL,
                JOIN_CANCEL_TIME: this._timestamp,
            }
            await JoinModel.edit(joinId, data);
        }
        else {
            let tradeNo = join.JOIN_PAY_TRADE_NO;
            if (!await this.fixMeetJoinPay(tradeNo, join.JOIN_MEET_ID, join.JOIN_MEET_TIME_MARK)) {
                this.AppError('该预约记录未支付,已取消并删除!');
            }
            let payService = new PayService();
            await payService.refundPay(tradeNo, '用户取消预约');

            // 更新记录 
            let data = {
                JOIN_STATUS: JoinModel.STATUS.CANCEL,
                JOIN_CANCEL_TIME: this._timestamp,
                JOIN_PAY_STATUS: 8
            }
            await JoinModel.edit(joinId, data);
        }

        // await JoinModel.del(where); 以前是删除,现在改为取消 


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

    }

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

        let fields = 'JOIN_ITEM,JOIN_PAY_STATUS,JOIN_PAY_FEE,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
    }) {

        await this.fixUserMeetJoinPayRecord(userId);

        orderBy = orderBy || {
            //    'JOIN_MEET_DAY': 'desc',
            //    'JOIN_MEET_TIME_START': 'desc',
            'JOIN_ADD_TIME': 'desc'
        };
        let fields = 'JOIN_ITEM.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.ADMIN_CANCEL]];
                    break;
                }
                case 'usercancel': { //用户已取消
                    where.JOIN_STATUS = ['in', [JoinModel.STATUS.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设计

image.png
image.png
image.png
image.png
image.png

后台设计

image.png
image.png
image.png
image.png
image.png
image.png
image.png

git代码下载

点击下载


CC同学呀
27 声望13 粉丝

鹅厂程序猿一枚,交流v: cclinux0730