| | |
| | | int unpaidCancelMinutes = scenicSpot == null || scenicSpot.getUnpaidCancelMinutes() == null |
| | | || scenicSpot.getUnpaidCancelMinutes() < 1 |
| | | ? DEFAULT_UNPAID_CANCEL_MINUTES : scenicSpot.getUnpaidCancelMinutes(); |
| | | cancelExpiredUnpaidIfNeeded(order, unpaidCancelMinutes); |
| | | return TourOrderResponse.from(order) |
| | | .enrich(scenicSpot, meetingPoint, pricing, guide) |
| | | .withUnpaidCancelMinutes(unpaidCancelMinutes) |
| | |
| | | platformSettingsService.getSettings().getUnacceptedAutoRefundMinutes()); |
| | | } |
| | | |
| | | /** 读取订单时惰性取消已超时的未支付订单,避免等待定时任务扫描延迟。 */ |
| | | private void cancelExpiredUnpaidIfNeeded(TourOrder order, int unpaidCancelMinutes) { |
| | | if (!OrderStatus.PENDING_PAYMENT.matches(order.getStatus())) return; |
| | | LocalDateTime baseTime = order.getPlacedAt() == null ? order.getCreatedAt() : order.getPlacedAt(); |
| | | if (baseTime == null || baseTime.plusMinutes(unpaidCancelMinutes).isAfter(LocalDateTime.now())) return; |
| | | if (orderMapper.cancelUnpaid(order.getId()) > 0) { |
| | | order.setStatus(OrderStatus.CANCELLED.getCode()); |
| | | order.setCancelledAt(LocalDateTime.now()); |
| | | } |
| | | } |
| | | |
| | | @Transactional |
| | | public TourOrderResponse cancel(Long visitorId, Long orderId) { |
| | | TourOrder order = orderMapper.selectForUpdate(orderId); |
| | |
| | | package com.tourguide.modules.user.service; |
| | | |
| | | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
| | | import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; |
| | | import com.tourguide.common.exception.BusinessException; |
| | | import com.tourguide.modules.order.domain.OrderStatus; |
| | | import com.tourguide.modules.order.entity.TourOrder; |
| | |
| | | if (idCardNo != null && !idCardNo.trim().isEmpty()) { |
| | | guideApplicationMapper.clearIdentityByIdCard(idCardNo); |
| | | } |
| | | user.setUsername("deleted_" + UUID.randomUUID().toString().replace("-", "").substring(0, 24)); |
| | | user.setMobile(null); |
| | | user.setCertificateNo(null); |
| | | user.setIdCardNo(null); |
| | | user.setIdCardFrontUrl(null); |
| | | user.setIdCardBackUrl(null); |
| | | user.setStatus(GuideUserService.STATUS_CANCELLED); |
| | | user.setAutoDisableWhitelisted(0); |
| | | user.setAutoDisabled(0); |
| | | guideUserMapper.updateById(user); |
| | | guideUserMapper.update(null, new LambdaUpdateWrapper<GuideUser>() |
| | | .eq(GuideUser::getId, guideId) |
| | | .set(GuideUser::getUsername, "deleted_" + UUID.randomUUID().toString().replace("-", "").substring(0, 24)) |
| | | .set(GuideUser::getMobile, null) |
| | | .set(GuideUser::getCertificateNo, null) |
| | | .set(GuideUser::getIdCardNo, null) |
| | | .set(GuideUser::getIdCardFrontUrl, null) |
| | | .set(GuideUser::getIdCardBackUrl, null) |
| | | .set(GuideUser::getStatus, GuideUserService.STATUS_CANCELLED) |
| | | .set(GuideUser::getAutoDisableWhitelisted, 0) |
| | | .set(GuideUser::getAutoDisabled, 0)); |
| | | tokenSessionService.remove(payload); |
| | | } |
| | | } |
| | |
| | | package com.tourguide.modules.user.service; |
| | | |
| | | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
| | | import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; |
| | | import com.baomidou.mybatisplus.extension.plugins.pagination.Page; |
| | | import com.tourguide.common.api.PageResponse; |
| | | import com.tourguide.common.exception.BusinessException; |
| | |
| | | } |
| | | |
| | | private void anonymizeCancelledGuide(GuideUser user) { |
| | | user.setUsername("deleted_" + UUID.randomUUID().toString().replace("-", "").substring(0, 24)); |
| | | user.setMobile(null); |
| | | user.setCertificateNo(null); |
| | | user.setIdCardNo(null); |
| | | userMapper.updateById(user); |
| | | userMapper.update(null, new LambdaUpdateWrapper<GuideUser>() |
| | | .eq(GuideUser::getId, user.getId()) |
| | | .set(GuideUser::getUsername, "deleted_" + UUID.randomUUID().toString().replace("-", "").substring(0, 24)) |
| | | .set(GuideUser::getMobile, null) |
| | | .set(GuideUser::getCertificateNo, null) |
| | | .set(GuideUser::getIdCardNo, null)); |
| | | } |
| | | |
| | | private void ensureUniqueGuideIdentity(GuideApplication application, GuideUser existingUser) { |
| | |
| | | "<uses-permission android:name=\"android.permission.CAMERA\"/>", |
| | | "<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>", |
| | | "<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>", |
| | | "<uses-permission android:name=\"android.permission.CALL_PHONE\"/>", |
| | | "<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>", |
| | | "<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>", |
| | | "<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>", |
| | |
| | | |
| | | <script> |
| | | import GuideDialog from '../../components/guide-dialog/guide-dialog.vue' |
| | | import { getLoginState, isLoggedIn } from '../../services/auth' |
| | | import { clearGuideLocalData, getLoginState, isLoggedIn } from '../../services/auth' |
| | | import { |
| | | getCurrentGuideReceivingSession, |
| | | getGuideProfile, |
| | |
| | | } |
| | | |
| | | function dialPhone(phoneNumber) { |
| | | // App 端(5+):先申请拨号权限,再调用系统拨号界面 |
| | | if (typeof plus !== 'undefined' && plus.device && typeof plus.device.dial === 'function') { |
| | | plus.device.dial(phoneNumber, true) |
| | | const doDial = () => plus.device.dial(phoneNumber, true) |
| | | // Android 6.0+ 需运行时申请 CALL_PHONE 权限,iOS 直接拨号 |
| | | if (typeof plus.android !== 'undefined' && typeof plus.android.requestPermissions === 'function') { |
| | | plus.android.requestPermissions(['android.permission.CALL_PHONE'], (result) => { |
| | | const denied = [ |
| | | ...(result && result.deniedAlways ? result.deniedAlways : []), |
| | | ...(result && result.denied ? result.denied : []) |
| | | ] |
| | | if (denied.length > 0) { |
| | | showPhoneCallError() |
| | | return |
| | | } |
| | | doDial() |
| | | }, () => { |
| | | showPhoneCallError() |
| | | }) |
| | | } else { |
| | | doDial() |
| | | } |
| | | return true |
| | | } |
| | | |
| | | if (typeof uni === 'undefined' || typeof uni.makePhoneCall !== 'function') { |
| | | if (typeof window !== 'undefined' && window.location) { |
| | | window.location.href = `tel:${phoneNumber}` |
| | | // H5 端:用 <a href="tel:"> 触发,兼容微信 WebView |
| | | if (typeof window !== 'undefined' && typeof document !== 'undefined' && typeof document.createElement === 'function') { |
| | | const link = document.createElement('a') |
| | | link.setAttribute('href', `tel:${phoneNumber}`) |
| | | link.style.display = 'none' |
| | | document.body.appendChild(link) |
| | | link.click() |
| | | document.body.removeChild(link) |
| | | return true |
| | | } |
| | | return false |
| | | } |
| | | |
| | | // 小程序端:uni.makePhoneCall |
| | | if (typeof uni !== 'undefined' && typeof uni.makePhoneCall === 'function') { |
| | | const result = uni.makePhoneCall({ |
| | | phoneNumber, |
| | | fail: handlePhoneCallFailure |
| | |
| | | result.catch(handlePhoneCallFailure) |
| | | } |
| | | return true |
| | | } |
| | | |
| | | return false |
| | | } |
| | | |
| | | function buildReceivingConfig(session, scenics = [], fallback = {}) { |
| | |
| | | } |
| | | this.countdownSeconds = Math.max( |
| | | 0, |
| | | Math.min( |
| | | leadMinutes * 60, |
| | | Math.ceil((this.countdownFallbackEndAt - Date.now()) / 1000) |
| | | ) |
| | | ) |
| | | return |
| | | } |
| | | this.countdownFallbackEndAt = null |
| | | const endAt = serving ? startAt + durationMinutes * 60 * 1000 : startAt |
| | | this.countdownSeconds = Math.max(0, Math.ceil((endAt - Date.now()) / 1000)) |
| | | const maxSeconds = (serving ? durationMinutes : leadMinutes) * 60 |
| | | this.countdownSeconds = Math.max( |
| | | 0, |
| | | Math.min(maxSeconds, Math.ceil((endAt - Date.now()) / 1000)) |
| | | ) |
| | | }, |
| | | clearCountdown() { |
| | | if (this.countdownTimer) { |
| | |
| | | }) |
| | | }, |
| | | resubmit() { |
| | | routeTo('/pages/register-phone/index') |
| | | clearGuideLocalData() |
| | | routeTo('/pages/login/index', 'reLaunch') |
| | | }, |
| | | openPolicy() { |
| | | routeTo('/pages/agreements/index') |
| | |
| | | }) |
| | | } |
| | | |
| | | function clearGuideLocalData() { |
| | | export function clearGuideLocalData() { |
| | | [ |
| | | GUIDE_LOGIN_KEY, |
| | | GUIDE_ORDERS_KEY, |
| | |
| | | |
| | | function updateCountdown() { |
| | | if (!paymentDeadline.value) return |
| | | const previous = remainingSeconds.value |
| | | remainingSeconds.value = Math.max( |
| | | 0, |
| | | Math.min( |
| | |
| | | Math.ceil((paymentDeadline.value - Date.now()) / 1000) |
| | | ) |
| | | ) |
| | | if (previous > 0 && remainingSeconds.value === 0) { |
| | | onCountdownExpired() |
| | | } |
| | | } |
| | | |
| | | function onCountdownExpired() { |
| | | if (pageActive && order.value.id && !redirecting) { |
| | | refreshOrderStatus() |
| | | } |
| | | } |
| | | |
| | | function parseServerTime(value) { |