从0到100:旅拍小程序开发笔记(上)

news2024/9/17 8:18:19

背景调研

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

功能规划

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

概要设计

在这里插入图片描述

数据库设计

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设计

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

后台设计

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

git代码下载

点击下载

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1961024.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

定制化Windows系统

定制化Windows系统 1、定制化需求介绍 预定义安装常用软件的原版操作系统&#xff0c;节省花费的时间。定制前提&#xff0c;你需要1块硬盘、1台&#xff08;2台&#xff09;测试电脑、一个PE盘&#xff08;wepe最好&#xff09;&#xff0c;原版系统镜像&#xff0c;虚拟光驱…

鸿蒙应用框架开发【选择并查看文档与媒体文件】 本地数据与文件

选择并查看文档与媒体文件 介绍 应用使用ohos.file.picker、ohos.file.fs等接口&#xff0c;实现了picker拉起文档编辑保存、拉起系统相册图片查看、拉起视频并播放的功能。 效果预览 使用说明&#xff1a; 在首页&#xff0c;应用显示查看最近打开文件功能的跳转按钮&…

GD32手把手教你移植FlashDB(片外Flash) -- 3.FlashDB使用

GD32手把手教你移植FlashDB(片外Flash) – 1.FlashDB-sfud移植 GD32手把手教你移植FlashDB(片外Flash) – 2.FlashDB移植 GD32手把手教你移植FlashDB(片外Flash) – 3.FlashDB使用 示例代码: https://gitee.com/ljmRD/GD32F427_FlashDB 3.FlashDB使用 main() /*************…

浅谈取样器之SSH Command

浅谈取样器之SSH Command JMeter的SSH Command取样器是一个强大的功能&#xff0c;允许用户在JMeter测试计划中执行远程SSH命令。这对于需要与Linux/Unix服务器交互以执行系统命令、脚本或者进行性能测试验证的场景尤为有用。通过这个取样器&#xff0c;您可以集成服务器端操作…

Linux中进程通信之信号

信号 信号通信&#xff0c;其实就是内核向用户空间进程发送信号&#xff0c;只有内核才能发信号&#xff0c;用户空间进程不能发送信号。 关于信号指令的查看&#xff1a;kill -l 例如我们之前使用的kill -9 pid用于杀死一个进程 使用一个死循环 成功发送kill -9指令&#x…

对于AI大模型发展态势的几点认识

本期内容从AI大模型产业的视角出发&#xff0c;全面审视该产业的发展现状&#xff0c;深入剖析其成长轨迹和未来趋势&#xff0c;旨在为人工智能产业的参与者提供一个全面的视角&#xff0c;更好地理解AI大模型产业的复杂性、动态性和潜力&#xff0c;以及如何在这个快速变化的…

Ruoyi 快速开发平台

Ruoyi 快速开发平台 一、官网二、准备工作2.1 环境要求2.2 必要配置 三、运行系统3.1 后端运行3.2 前端安装及运行 四、自定义开发4.1 新增业务模块4.2 代码生成4.2.1 创建菜单4.2.2 后端代码4.2.3 前端代码 一、官网 链接: 前后端分离版本 回到目录 二、准备工作 2.1 环境要…

UDP服务器端bind失败问题

本人使用microchip芯片开发&#xff0c;使用UDP虚拟机通讯&#xff0c;经常提示bind失败&#xff0c;返回-1&#xff0c;尝试了以前UDP作为客户端使用时正常&#xff0c;故硬件链路没问题。 一、可能有几个原因&#xff1a; 端口实际上被占用&#xff1a;最明显的原因是端口真…

基于入侵野草算法的KNN分类优化matlab仿真

目录 1.程序功能描述 2.测试软件版本以及运行结果展示 3.核心程序 4.本算法原理 4.1 入侵野草算法 4.2 K近邻分类器&#xff08;KNN&#xff09; 4.3 基于IWO的KNN分类优化 5.完整程序 1.程序功能描述 基于入侵野草算法的KNN分类优化。其中&#xff0c;入侵野草算法是一…

GEE APP:利用谷歌地球引擎实现更有效的草原管理:决策支持应用视角

简介 草原占地球表面和农田的很大一部分,对人类福祉和畜牧业至关重要。由于牧区基础设施不发达、通信不畅,牧民和草原管理部门在有效控制牧民放牧行为和草原利用方面面临挑战。要解决这一问题,促进草原的可持续利用并保护其生态系统服务,就需要基于云的放牧管理和决策支持…

C++初阶大总结

目录 一.命名空间 1.命名空间定义 2.命名空间使用 二.C++输入&输出 三.缺省参数 四. 函数重载 五.引用 1.常引用 2.传值、传引用效率比较 3.引用和指针的区别 4.引用和指针的不同点: 小知识点: 六.内联函数 七.auto关键字(C++11) 1.auto的使用细则 八.基于…

24暑假算法刷题 | Day23 | LeetCode 39. 组合总和,40. 组合总和 II,131. 分割回文串

目录 39. 组合总和题目描述题解 40. 组合总和 II题目描述题解 131. 分割回文串题目描述题解 39. 组合总和 点此跳转题目链接 题目描述 给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target &#xff0c;找出 candidates 中可以使数字和为目标数 target 的 所有…

Dolphinscheduler 3.2.1bug记录

问题1&#xff1a;分页只展示首页 解决方案&#xff1a; [Bug][API] list paging missing totalpage by Gallardot Pull Request #15619 apache/dolphinscheduler GitHub 问题2:Hive 数据源连接失败 解决方案&#xff1a;修改源码&#xff1a;HiveDataSourceProcessor.cla…

A Survey on Multimodal Large Language Models(from gpt-4o)

目录 A Survey on Multimodal Large Language Models1. INTRODUCTION2. ARCHITECTURE2.1 Modality encoder2.2 Pre-trained LLM2.3 Modality interface 3. TRAINING STRATEGY AND DATA3.1 Pre-training3.1.1 Training Detail3.1.2 Data 3.2 Instruction-tuning3.2.1 Introducti…

Linux下文件编译器-GCC/G++

前言 本文介绍了c/c的编译过程以及gcc/g的时使用 一.c/c翻译的本质&#xff1a;将高级语言翻译成二进制 1&#xff09;程序翻译过程&#xff1a; &#xff08;1&#xff09;预处理&#xff08;头文件展开、宏替换、去注释、条件编译&#xff09;还是C语言代码 ​ …

hash表如何形成,hash函数如何计算,什么是hash冲突 如何解决 ,Golang map的底层原理及扩容机制

散列表 散列表&#xff08;hash表&#xff09;:根据给定的关键字来计算出关键字在表中的地址的数据结构。也就是说&#xff0c;散列表建立了关键字和 存储地址之间的一种直接映射关系。 问题&#xff1a;如何建立映射管血 散列函数:一个把查找表中的关键字映射成该关键字对应…

平移、旋转、缩放和媒体

一、平移 1.1translate&#xff08;&#xff09;函数 做转换工作可以用translate()函数&#xff0c;这个函数可以改变坐标系。通过改变默认的坐标系&#xff0c;我们可以创建不同的转换方式&#xff0c;包括平移、旋转和缩放。 1.2平移位置案例 案例代码如图1 图1 保存运行如…

【大一公共课】C语言+python语言入门对比+思维导图

C 和 Python 入门教程对比 一、引言 C 语言和 Python 语言都是在编程领域中广泛使用的语言&#xff0c;但它们在语法、应用场景和学习难度上有很大的不同。本教程将对 C 和 Python 的入门知识进行对比&#xff0c;帮助您更好地理解和选择适合自己的编程语言。 二、C 语言入门 …

python爬取某财富网

过程&#xff1a; 点击底部的第3页&#xff0c;第5页&#xff0c;网页刷新了&#xff0c;但是顶部的url地址没有变。那么就是 动态加载&#xff0c; 就是 XHR. 直接请求api. 实验代码如下: import requestsheaders {"User-Agent": "Mozilla/5.0 (Windows NT…