diff --git a/common/env.js b/common/env.js index 95811de..cea7457 100644 --- a/common/env.js +++ b/common/env.js @@ -7,10 +7,10 @@ const development = { // API基础URL // baseUrl: 'https://ydsapi.zpc-xy.com', - baseUrl: 'http://1.15.21.245:2401', - host: ['http://1.15.21.245:2401'], - // baseUrl: 'http://localhost:2015', - // host: ['http://localhost:2015'], + // baseUrl: 'http://1.15.21.245:2401', + // host: ['http://1.15.21.245:2401'], + baseUrl: 'http://localhost:2015', + host: ['http://localhost:2015'], imageUrl: 'https://guyu-1308826010.cos.ap-shanghai.myqcloud.com', }; diff --git a/common/server/config.js b/common/server/config.js index 0549a12..4e4101e 100644 --- a/common/server/config.js +++ b/common/server/config.js @@ -21,4 +21,38 @@ export const getConfigData = async () => { await preloadConfigData(); } return configData.value; +} + +/** + * 获取订阅消息模板ID列表 + * @param {Number} depositFee 押金费用 + * @returns {Array|null} 返回模板ID数组,失败时返回null + */ +export const getSubscribeMessage = async (depositFee) => { + try { + debugger + const config = await getConfigData(); + if (!config || !config.config || !config.config.subscribeMessage) { + return null; + } + + const templateIds = config.config.subscribeMessage + .filter(element => { + // 如果没有押金,过滤掉扣费通知和退款通知 + if (element.type == "reservation_success" || element.type == "reservation_change" || element.type == "reservation_reminder") { + return true; + } + return false; + // if (depositFee === 0) { + // return element.type !== "deduction_notice" && element.type !== "refund"; + // } + // return true; + }) + .map(element => element.templateId); + + return templateIds; + } catch (error) { + console.error('获取订阅消息配置失败:', error); + return null; + } } \ No newline at end of file diff --git a/common/server/interface/sq.js b/common/server/interface/sq.js index 95273dd..05a85d7 100644 --- a/common/server/interface/sq.js +++ b/common/server/interface/sq.js @@ -112,7 +112,75 @@ export const getReservationRoomList = async (startTime, endTime) => { return null; } - +/** + * 用户创建预约接口 + * @param {Object} reservationData 预约数据 + * @param {number} reservationData.room_id 房间ID + * @param {number} reservationData.start_time 开始时间 时间戳(秒) + * @param {number} reservationData.end_time 结束时间 时间戳(秒) + * @param {string} reservationData.game_type 游戏类型 + * @param {number} reservationData.deposit_fee 押金费用 + * @param {string} reservationData.important_data 重要数据 + * @returns {Promise} 返回预约结果 + */ +export const addSQReservation = async (reservationData) => { + const res = await request.post("sq/AddSQReservation", reservationData); + if (res.code == 0) { + return { + success: true, + data: res.data.reservation_id + }; + } + return { + success: false, + message: res.msg || '预约失败' + }; +} + +/** + * 用户加入预约接口 + * @param {Object} joinData 加入预约数据 + * @param {number} joinData.ReservationsId 预约ID + * @param {string} joinData.important_data 重要数据 + * @returns {Promise} 返回加入结果 + */ +export const joinReservation = async (joinData) => { + const res = await request.post("sq/JoinReservation", joinData); + if (res.code == 0) { + return { + success: true, + message: res.msg || '加入预约成功' + }; + } + return { + success: false, + message: res.msg || '加入预约失败' + }; +} + +/** + * 取消预约接口 + * @param {Object} cancelData 取消预约数据 + * @param {number} cancelData.reservation_id 预约ID + * @param {string} cancelData.cancel_reason 取消原因(可选) + * @returns {Promise} 返回取消结果 + */ +export const cancelReservation = async (reservation_id, cancel_reason) => { + console.log("cancelReservation", reservation_id, cancel_reason); + const res = await request.post("sq/CancelReservation", { reservation_id: reservation_id, cancel_reason: cancel_reason }); + if (res.code == 0) { + return { + success: true, + message: res.msg || '取消预约成功' + }; + } + return { + success: false, + message: res.msg || '取消预约失败' + }; +} + + export const sqInterface = { @@ -122,5 +190,8 @@ export const sqInterface = { getEvaluateServices, addEvaluateServices, getReputationByUser, - getReservationRoomList + getReservationRoomList, + addSQReservation, + joinReservation, + cancelReservation } \ No newline at end of file diff --git a/common/server/interface/user.js b/common/server/interface/user.js index 71e5b2e..2351be4 100644 --- a/common/server/interface/user.js +++ b/common/server/interface/user.js @@ -124,8 +124,20 @@ export const cancelUserBlack = async (userId) => { } return false; } - +/** + * 添加黑名单 + * @param {*} userId 要拉黑的用户ID + * @returns {Promise} + */ +export const usePay = async (money) => { + const res = await request.post("user/UsePay", { money: money }); + if (res.code == 0) { + return res.data; + } + return null; +} export const userInterface = { + getAnonymousLogin, ueWxPhoneNumberLogin, anonymousLogin, @@ -134,7 +146,8 @@ export const userInterface = { editUserInfo, getMyBlackList, addUserBlack, - cancelUserBlack + cancelUserBlack, + usePay } diff --git a/common/server/user.js b/common/server/user.js index 82e3f99..ef694bd 100644 --- a/common/server/user.js +++ b/common/server/user.js @@ -12,7 +12,7 @@ const throttledLoadUserInfo = throttle(_loadUserInfo, 2000, { leading: true, tra /** * 清除用户相关存储 */ -const clearUserStorage = () => { +export const clearUserStorage = () => { userInfo.value = null; uni.removeStorageSync('tokenInfo'); uni.removeStorageSync('userInfo'); diff --git a/common/system/request.js b/common/system/request.js index d7d9042..970d028 100644 --- a/common/system/request.js +++ b/common/system/request.js @@ -7,6 +7,7 @@ import config from '@/common/env.js'; import md5 from 'js-md5'; import { getLocalStorage, setLocalStorage } from './cacheService'; import qs from 'qs'; +import { clearUserStorage } from '@/common/server/user' class request { /** * 生成唯一的nonce值 @@ -139,6 +140,10 @@ class request { success: res => { const endDate = Date.now(); console.log(requestUrl, "请求消耗时间", endDate - startDate); + if (res.data.code == 14007) { + //登录失效 + clearUserStorage(); + } resolve(res.data); }, fail: e => { diff --git a/common/utils.js b/common/utils.js index 3d956bc..7b98616 100644 --- a/common/utils.js +++ b/common/utils.js @@ -1,3 +1,4 @@ +import { forEach } from "lodash"; /** * 延迟执行 @@ -139,6 +140,81 @@ export function hideLoading() { } +/** + * 请求订阅消息 + * @param {Array} tmplIds 模板ID数组 + * @returns {Promise} 返回Promise对象,resolve中返回订阅结果对象 + */ +export function requestSubscribeMessage(tmplIds) { + return new Promise((resolve) => { + uni.requestSubscribeMessage({ + tmplIds: tmplIds || [], + success(res) { + console.log('订阅消息授权结果:', res); + // if(res['']) + var data = {}; + for (let i = 0; i < tmplIds.length; i++) { + if (res[tmplIds[i]] != null && res[tmplIds[i]] == "accept") { + data[tmplIds[i]] = true; + } else { + data[tmplIds[i]] = false; + } + } + console.log("订阅消息授权结果:", data); + + resolve({ + success: true, + result: data + }); + }, + fail(err) { + console.error('订阅消息授权失败:', err); + resolve({ + success: false, + result: [], + error: err + }); + } + }); + }); +} + +/** + * 微信支付 + * @param {Object} orderInfo 支付订单信息对象 + * @param {String} orderInfo.appid 微信开放平台应用AppId + * @param {String} orderInfo.noncestr 随机字符串 + * @param {String} orderInfo.package 固定值 "Sign=WXPay" + * @param {String} orderInfo.partnerid 微信支付商户号 + * @param {String} orderInfo.prepayid 统一下单订单号 + * @param {Number} orderInfo.timestamp 时间戳(单位:秒) + * @param {String} orderInfo.sign 签名 + * @returns {Promise} 返回Promise对象,resolve中返回支付结果对象 + */ +export function requestPayment(orderInfo) { + return new Promise((resolve) => { + uni.requestPayment({ + provider: "weixin", + ...orderInfo, + success(res) { + console.log('微信支付成功:', res); + resolve({ + success: true, + result: res + }); + }, + fail(err) { + console.error('微信支付失败:', err); + resolve({ + success: false, + result: null, + error: err + }); + } + }); + }); +} + let os = ''; /** * diff --git a/components/com/index/ReservationPopup.vue b/components/com/index/ReservationPopup.vue index 4a9f364..5059072 100644 --- a/components/com/index/ReservationPopup.vue +++ b/components/com/index/ReservationPopup.vue @@ -209,19 +209,20 @@ const getGenderText = (genderLimit) => { // 处理加入组局 const handleJoin = () => { - // 触发父组件的加入事件 - emit('join', reservationData.value) + // 触发父组件的加入事件 reservationData.value + console.log("reservationData.value", reservationData.value); + } const cancelJoin = async () => { var res = await showModalConfirm('提示', '确定要取消组局吗?') if (res) { - emit('cancelJoin', reservationData.value) + console.log("reservationData.value", reservationData.value); } } const exitJoin = async () => { var res = await showModalConfirm('提示', '确定要退出组局吗?') if (res) { - emit('exitJoin', reservationData.value) + console.log("reservationData.value", reservationData.value); } } // 打开用户信息弹窗 @@ -231,7 +232,7 @@ const openUserPop = (user) => { } // 定义事件 -const emit = defineEmits(['join', 'openUserPop', 'cancelJoin', 'exitJoin']) +const emit = defineEmits(['openUserPop']) // 暴露方法给父组件 defineExpose({ diff --git a/components/index/MahjongCard.vue b/components/index/MahjongCard.vue index fec9612..8b6766c 100644 --- a/components/index/MahjongCard.vue +++ b/components/index/MahjongCard.vue @@ -7,7 +7,7 @@ {{ statusName }} - + @@ -17,21 +17,21 @@ - + - + - + @@ -40,7 +40,7 @@ - + @@ -68,7 +68,7 @@ const props = defineProps({ id: '', status: '', description: '', - dateStr:'', + dateStr: '', time: '', room: '', requirements: '', @@ -135,7 +135,7 @@ const playerPositions = computed(() => { const statusName = computed(() => { const { status, personCount, joinPerson } = props.item const count = joinPerson.length - + if (status === 0) { return personCount === count ? "待开始" : "组局中..." } else if (status === 1) { @@ -168,7 +168,7 @@ const getPlayerAtPosition = (position) => { */ const getJoinPlayerAtPosition = (index) => { const personCount = props.item.personCount - + if (personCount === 2) { // 2人局:第二行左边和右边 return [2, 3].includes(index) @@ -255,7 +255,7 @@ const handleJoin = () => { .status-tag { position: absolute; left: 16rpx; - top: 0; + top: 5px; font-family: PingFang SC, PingFang SC; font-weight: 500; font-size: 26rpx; diff --git a/pages/appointment/appointment-page.vue b/pages/appointment/appointment-page.vue index 5a7e859..0854163 100644 --- a/pages/appointment/appointment-page.vue +++ b/pages/appointment/appointment-page.vue @@ -10,11 +10,11 @@ - {{ getDayDescription(startTimeStr) }} + {{ getDayDescription(reservationInfo.start_time * 1000) }} - {{ getDayDescription(endTimeStr) }} + {{ getDayDescription(reservationInfo.end_time * 1000) }} @@ -97,7 +97,7 @@ 鸽子费(保证金),参与人需缴纳鸽子费。若有参与者在预约后没有赴约,其鸽子费由在场的所有人平分。组局成功或失败后鸽子费将全额返还。 - + 发起预约 @@ -121,12 +121,18 @@ import { watch } from 'vue'; import { - getConfigData + getConfigData, getSubscribeMessage } from '@/common/server/config' +import { usePay } from '@/common/server/interface/user' +import { + requestSubscribeMessage, + requestPayment +} from '@/common/utils' import { getDayDescription, ceilMinuteToNext5 } from '@/common/system/timeUtile'; +import { isLogin } from '@/common/server/user' import { forEach, union @@ -139,15 +145,13 @@ import TagSelect from '@/components/com/appointment/tag-select.vue' import ComAppointmentPickerData from '@/components/com/appointment/picker-data.vue' import ComAppointmentRadioSelect from '@/components/com/appointment/radio-select.vue' import { - getReservationRoomList + getReservationRoomList, addSQReservation, cancelReservation } from '@/common/server/interface/sq' const _containerBase = ref(null) // 年龄选择器状态 const agePickerVisible = ref(false) const agePickerColumns = ref([[], []]) const agePickerDefaultIndex = ref([0, 0]) -const startTimeStr = ref(0) -const endTimeStr = ref(0) const lineHeight = ref("15rpx") const timeRange = ref(["2小时", "3小时", "4小时", "自定义"]) @@ -157,17 +161,17 @@ const roomOptions = ref([]) const roomPickerRef = ref(null) //提交表单数据 const reservationInfo = ref({ - room_id: 0, //房间id - room_name: '请选择房间', //房间名称 - start_time: 0, //开始时间 时间戳 - end_time: 0, //结束时间 时间戳 - max_age: 0, //最大年龄 - min_age: 0, //最小年龄 - title: '', //组局名称 - extra_info: '', //其它说明 - game_rule: '', //具体规则 - game_type: '', //玩法类型 - gender_limit: 0, //性别限制 + room_id: 0, //房间id 非空 + room_name: '请选择房间', //房间名称 + start_time: 0, //开始时间 时间戳 非空 + end_time: 0, //结束时间 时间戳 非空 + max_age: 0, //最大年龄 默认0 + min_age: 0, //最小年龄 非空 + title: '', //组局名称 非空 + extra_info: '', //其它说明 可为空 + game_rule: '', //具体规则 非空 + game_type: '', //玩法类型 非空 + gender_limit: 0, //性别限制 is_smoking: 2, //是否禁烟 credit_limit: 0, //信誉限制 deposit_fee: 0, //鸽子费 @@ -179,14 +183,14 @@ const onTimeRangeChange = async (val) => { console.log('timeRange change:', val) if (val != "") { await openUpDatesTimePicker(false); - if (startTimeStr.value > 0) { + if (reservationInfo.value.start_time > 0) { var str = val; if (str == "2小时") { - endTimeStr.value = startTimeStr.value + 1000 * 60 * 60 * 2; + reservationInfo.value.end_time = reservationInfo.value.start_time + 2 * 60 * 60; } else if (str == "3小时") { - endTimeStr.value = startTimeStr.value + 1000 * 60 * 60 * 3; + reservationInfo.value.end_time = reservationInfo.value.start_time + 3 * 60 * 60; } else if (str == "4小时") { - endTimeStr.value = startTimeStr.value + 1000 * 60 * 60 * 4; + reservationInfo.value.end_time = reservationInfo.value.start_time + 4 * 60 * 60; } else { await openUpDatesTimePickerEnd(false); } @@ -198,43 +202,41 @@ const getRoomPickerName = () => { } const openUpDatesTimePicker = async (isManual = true) => { - var now = startTimeStr.value; + var now = reservationInfo.value.start_time * 1000; var min = Date.now(); min += 1000 * 60 * 30; min = ceilMinuteToNext5(min).valueOf(); - if (startTimeStr.value == 0) { + if (reservationInfo.value.start_time == 0) { now = Date.now(); now += 1000 * 60 * 30; now = ceilMinuteToNext5(now).valueOf(); } const startTime = await _containerBase.value.openUpDatesTimePicker(now, min, "预约开始时间") - startTimeStr.value = startTime - // 同步到表单对象,毫秒转秒 - reservationInfo.value.start_time = Math.floor(startTimeStr.value / 1000) - // 手动选择时间则切换为“自定义” + // 直接设置到表单对象,毫秒转秒 + reservationInfo.value.start_time = Math.floor(startTime / 1000) + // 手动选择时间则切换为"自定义" if (isManual) { timeRangeValue.value = "自定义" } } const openUpDatesTimePickerEnd = async (isManual = true) => { - if (startTimeStr.value == 0) { + if (reservationInfo.value.start_time == 0) { uni.showToast({ title: '请先选择开始时间', icon: 'none' }) return; } - var now = endTimeStr.value; - var min = (startTimeStr.value + 1000 * 60 * 30); + var now = reservationInfo.value.end_time * 1000; + var min = (reservationInfo.value.start_time * 1000 + 1000 * 60 * 30); if (now == 0) { - now = (startTimeStr.value + 1000 * 60 * 30); + now = (reservationInfo.value.start_time * 1000 + 1000 * 60 * 30); } //minDate+1000*60*30 最小间隔30分钟 const endTime = await _containerBase.value.openUpDatesTimePicker(now, min, "预约结束时间") - endTimeStr.value = endTime - // 同步到表单对象,毫秒转秒 - reservationInfo.value.end_time = Math.floor(endTimeStr.value / 1000) - // 手动选择时间则切换为“自定义” + // 直接设置到表单对象,毫秒转秒 + reservationInfo.value.end_time = Math.floor(endTime / 1000) + // 手动选择时间则切换为"自定义" if (isManual) { timeRangeValue.value = "自定义" } @@ -339,6 +341,8 @@ const increment = () => { if (currentValue.value > 5) { currentValue.value = 5 } + // 同步到表单对象 + reservationInfo.value.credit_limit = currentValue.value } } @@ -348,18 +352,227 @@ const decrement = () => { if (currentValue.value < 0) { currentValue.value = 0 } + // 同步到表单对象 + reservationInfo.value.credit_limit = currentValue.value } } const changeLog = (e) => { console.log('change事件:', e) - } -// 当开始或结束时间变化(通过其它方式)时也自动刷新房间 -watch([startTimeStr, endTimeStr], async ([s, e]) => { - reservationInfo.value.start_time = s ? Math.floor(s / 1000) : 0 - reservationInfo.value.end_time = e ? Math.floor(e / 1000) : 0 +// 表单验证方法 +const validateForm = () => { + const info = reservationInfo.value + + // 必填字段验证 + if (!info.room_id || info.room_id === 0) { + uni.showToast({ + title: '请选择房间', + icon: 'none' + }) + return false + } + + if (!info.start_time || info.start_time === 0) { + uni.showToast({ + title: '请选择开始时间', + icon: 'none' + }) + return false + } + + if (!info.end_time || info.end_time === 0) { + uni.showToast({ + title: '请选择结束时间', + icon: 'none' + }) + return false + } + + if (info.end_time <= info.start_time) { + uni.showToast({ + title: '结束时间需晚于开始时间', + icon: 'none' + }) + return false + } + + if (!info.title || info.title.trim() === '') { + uni.showToast({ + title: '请输入组局名称', + icon: 'none' + }) + return false + } + + if (!info.player_count || info.player_count === 0) { + uni.showToast({ + title: '请选择游玩人数', + icon: 'none' + }) + return false + } + + if (!info.game_type || info.game_type === 0) { + uni.showToast({ + title: '请选择玩法类型', + icon: 'none' + }) + return false + } + + if (!info.game_rule || info.game_rule === 0) { + uni.showToast({ + title: '请选择具体规则', + icon: 'none' + }) + return false + } + + // 年龄范围验证(如果设置了年龄限制) + if (info.min_age > 0 && info.max_age > 0 && info.min_age > info.max_age) { + uni.showToast({ + title: '最小年龄不能大于最大年龄', + icon: 'none' + }) + return false + } + + return true +} + +// 提交预约方法 +const submitReservation = async () => { + var isLoginSucces = await isLogin(); + if (!isLoginSucces) { + uni.navigateTo({ + url: '/pages/me/login' + }); + return; + } + // 验证表单 + if (!validateForm()) { + return + } + + + try { + var messageId = await getSubscribeMessage(reservationInfo.value.deposit_fee); + console.log("messageId", messageId); + var subscribeMessage = await requestSubscribeMessage(messageId); + console.log("message", subscribeMessage); + + // 显示加载状态 + uni.showLoading({ + title: '提交中...' + }) + + // 准备提交数据 + const submitData = { + ...reservationInfo.value, + // 确保信誉限制同步 + credit_limit: currentValue.value, + important_data: {} + } + + var resPay = null; + if (submitData.deposit_fee > 0) { + resPay = await usePay(submitData.deposit_fee); + if (resPay == null) { + uni.showToast({ + title: '鸽子费订单创建失败,请重新提交预约数据!', + icon: 'none' + }) + return; + } + subscribeMessage.result.paymentId = resPay.paymentId; + } + console.log('提交预约数据:', submitData) + var important_data = ""; + if (subscribeMessage.result != null) { + important_data = JSON.stringify(subscribeMessage.result); + } + submitData.important_data = important_data; + // TODO: 调用后端API提交预约 + // 调用后端API提交预约 + const result = await addSQReservation(submitData) + uni.hideLoading() + console.log("result", result); + if (!result.success) { + uni.showToast({ + title: result.message, + icon: 'none' + }) + return; + } + if (submitData.deposit_fee > 0 && resPay != null) { + var payRes = await requestPayment(resPay); + if (payRes.success) { + uni.showToast({ + title: '鸽子费支付成功', + icon: 'success' + }) + } else { + await cancelReservation(result.data, "鸽子费支付失败,请重新预约!"); + uni.showToast({ + title: '鸽子费支付失败,请重新预约!', + icon: 'none' + }) + return; + } + console.log("payRes", payRes); + } + // 提交成功 + uni.showToast({ + title: '预约提交成功', + icon: 'success' + }) + + // 可以跳转到其他页面或重置表单 + // uni.navigateBack() // 返回上一页 + // 或者重置表单 + resetForm() + + } catch (error) { + uni.hideLoading() + console.error('提交预约失败:', error) + uni.showToast({ + title: '提交失败,请重试', + icon: 'none' + }) + } +} + +// 重置表单方法(可选) +const resetForm = () => { + reservationInfo.value = { + room_id: 0, + room_name: '请选择房间', + start_time: 0, + end_time: 0, + max_age: 0, + min_age: 0, + title: '', + extra_info: '', + game_rule: '', + game_type: '', + gender_limit: 0, + is_smoking: 2, + credit_limit: 0, + deposit_fee: 0, + player_count: 0, + } + currentValue.value = 0 + timeRangeValue.value = "" + gameRuleRange.value = [] + peopleRange.value = [] + peopleText.value = "请先选择房间" + gameRuleText.value = "请先选择玩法类型" +} + +// 当开始或结束时间变化时自动刷新房间 +watch([() => reservationInfo.value.start_time, () => reservationInfo.value.end_time], async ([s, e]) => { await tryLoadRooms() }) @@ -412,6 +625,9 @@ onLoad(async () => { gameTypeRange.value = [...config.config.playingMethodOptions]; } }) +onShow(async () => { + resetForm(); +}) // 年龄列构建与显示/文案 const buildAgeColumns = () => { const minList = [{ value: 0, text: '不限' }] diff --git a/pages/index/index.vue b/pages/index/index.vue index 9c9ac5a..56fdba4 100644 --- a/pages/index/index.vue +++ b/pages/index/index.vue @@ -38,7 +38,7 @@ - +