1066 lines
32 KiB
Plaintext
1066 lines
32 KiB
Plaintext
<template>
|
||
<ServicePageScaffold title="到达签到" fallback-url="/pages/mall/delivery/orders/route">
|
||
<ServicePanel title="居家服务认证" subtitle="使用居家服务系统账号登录">
|
||
<view v-if="!isHomecareLoggedIn">
|
||
<text class="info-text">状态:未登录</text>
|
||
<text class="info-text warning-text">居家服务认证服务暂不可用,请跳过此步骤继续操作</text>
|
||
</view>
|
||
<view v-else>
|
||
<text class="info-text">已登录:{{ homecareUserEmail }}</text>
|
||
<text class="info-text success-text">居家服务认证通过</text>
|
||
</view>
|
||
</ServicePanel>
|
||
|
||
<ServicePanel title="签到要求" subtitle="定位签到或扫码签到,要求在 50 米范围内并上传现场图片。">
|
||
<text v-if="order != null" class="info-text">服务地址:{{ order.fullAddress }}</text>
|
||
<text class="info-text">当前位置:{{ locationText }}</text>
|
||
<text class="info-text">定位精度:{{ accuracyText }}</text>
|
||
<text class="info-text">现场图片:{{ photos.length }} 张</text>
|
||
</ServicePanel>
|
||
|
||
<ServicePanel title="距离预校验" subtitle="获取定位后校验是否在允许签到范围内。">
|
||
<text class="info-text">距离服务点:{{ distanceText }}</text>
|
||
<text class="info-text">允许半径:{{ allowedRadiusText }}</text>
|
||
<text class="info-text">校验状态:{{ precheckStatusText }}</text>
|
||
<text v-if="reasonText !== ''" class="info-text warning-text">{{ reasonText }}</text>
|
||
<view class="button-stack">
|
||
<button class="secondary-btn" :disabled="prechecking" @click="handlePrecheck">{{ prechecking ? '预校验中...' : '距离预校验' }}</button>
|
||
</view>
|
||
</ServicePanel>
|
||
|
||
<ServicePanel title="签到操作" subtitle="定位失败时要给出清晰提示。">
|
||
<view class="button-stack">
|
||
<button class="secondary-btn" @click="getCurrentLocation">获取 GPS 定位</button>
|
||
<button class="secondary-btn" @click="choosePhoto">拍照上传现场图片</button>
|
||
<input class="note-input" v-model="note" placeholder="补充签到说明,例如入户前核验联系人" />
|
||
<button class="primary-btn" :disabled="submitting" @click="submitCheckin">{{ submitting ? '签到中...' : '完成签到' }}</button>
|
||
</view>
|
||
</ServicePanel>
|
||
</ServicePageScaffold>
|
||
</template>
|
||
|
||
<script setup lang="uts">
|
||
import { ref } from 'vue'
|
||
import { onLoad } from '@dcloudio/uni-app'
|
||
import ServicePageScaffold from '@/components/homeService/ServicePageScaffold.uvue'
|
||
import ServicePanel from '@/components/homeService/ServicePanel.uvue'
|
||
import type { DeliveryLocationType, DeliveryOrderType } from '@/types/delivery.uts'
|
||
import supa from '@/components/supadb/aksupainstance.uts'
|
||
import { getDeliveryOrderDetail } from '@/services/deliveryService.uts'
|
||
import { requireDeliveryAuth } from '@/utils/deliveryAuth.uts'
|
||
import { getCurrentUserId } from '@/utils/store.uts'
|
||
import { getDeliveryRouteParam } from '@/utils/deliveryRoute.uts'
|
||
import { getCurrentAkUserId as readCurrentAkUserId } from '@/utils/akUserMapping.uts'
|
||
import {
|
||
checkinPrecheckCompat,
|
||
createCheckinEvidenceCompat,
|
||
submitHomecareCheckinCompat
|
||
} from '@/api/delivery.uts'
|
||
|
||
const checkinTraceId = ref('')
|
||
const CHECKIN_DEBUG_PREFIX = '[CHECKIN_TRACE]'
|
||
|
||
function nowLogTime(): string {
|
||
return new Date().toISOString()
|
||
}
|
||
|
||
function maskPhoneText(v: string): string {
|
||
if (v == null || v.length < 7) return v
|
||
return v.substring(0, 3) + '****' + v.substring(v.length - 4)
|
||
}
|
||
|
||
function safeLogValue(v: any): string {
|
||
try {
|
||
if (v == null) return 'null'
|
||
const s = JSON.stringify(v)
|
||
if (s == null) return String(v)
|
||
return s
|
||
} catch (e) {
|
||
return String(v)
|
||
}
|
||
}
|
||
|
||
function logStep(step: string, message: string, data: any | null = null) {
|
||
const tid = checkinTraceId.value
|
||
if (data == null) {
|
||
console.log(`${CHECKIN_DEBUG_PREFIX}[${step}][traceId=${tid}][${nowLogTime()}] ${message}`)
|
||
} else {
|
||
console.log(`${CHECKIN_DEBUG_PREFIX}[${step}][traceId=${tid}][${nowLogTime()}] ${message}`, data)
|
||
}
|
||
}
|
||
|
||
function warnStep(step: string, message: string, data: any | null = null) {
|
||
const tid = checkinTraceId.value
|
||
if (data == null) {
|
||
console.warn(`${CHECKIN_DEBUG_PREFIX}[${step}][traceId=${tid}][${nowLogTime()}] ${message}`)
|
||
} else {
|
||
console.warn(`${CHECKIN_DEBUG_PREFIX}[${step}][traceId=${tid}][${nowLogTime()}] ${message}`, data)
|
||
}
|
||
}
|
||
|
||
function errorStep(step: string, message: string, error: any, extra: any | null = null) {
|
||
const tid = checkinTraceId.value
|
||
console.error(`${CHECKIN_DEBUG_PREFIX}[${step}][traceId=${tid}][${nowLogTime()}] ${message}`, {
|
||
errorMessage: error != null ? String(error.message ?? error) : '',
|
||
errorStack: error != null ? String(error.stack ?? '') : '',
|
||
extra: extra
|
||
})
|
||
}
|
||
|
||
function readBoolCompat(obj: any, key: string, fallback: boolean = false): boolean {
|
||
try {
|
||
if (obj == null) return fallback
|
||
|
||
if (typeof obj.getBoolean == 'function') {
|
||
const v = obj.getBoolean(key)
|
||
if (v != null) return v
|
||
}
|
||
|
||
if (typeof obj.getBool == 'function') {
|
||
const v2 = obj.getBool(key)
|
||
if (v2 != null) return v2
|
||
}
|
||
|
||
const raw = obj[key]
|
||
if (raw == true || raw == 'true' || raw == 1 || raw == '1') return true
|
||
if (raw == false || raw == 'false' || raw == 0 || raw == '0') return false
|
||
|
||
return fallback
|
||
} catch (e) {
|
||
warnStep('S00_READ_BOOL', `readBoolCompat failed key=${key}`, e)
|
||
return fallback
|
||
}
|
||
}
|
||
|
||
function readStringCompat(obj: any, key: string, fallback: string = ''): string {
|
||
try {
|
||
if (obj == null) return fallback
|
||
|
||
if (typeof obj.getString == 'function') {
|
||
const v = obj.getString(key)
|
||
if (v != null) return v
|
||
}
|
||
|
||
const raw = obj[key]
|
||
if (raw == null) return fallback
|
||
return String(raw)
|
||
} catch (e) {
|
||
warnStep('S00_READ_STRING', `readStringCompat failed key=${key}`, e)
|
||
return fallback
|
||
}
|
||
}
|
||
|
||
function readNumberCompat(obj: any, key: string, fallback: number = 0): number {
|
||
try {
|
||
if (obj == null) return fallback
|
||
|
||
if (typeof obj.getNumber == 'function') {
|
||
const v = obj.getNumber(key)
|
||
if (v != null) return v
|
||
}
|
||
|
||
const raw = obj[key]
|
||
if (raw == null || raw == '') return fallback
|
||
const n = Number(raw)
|
||
if (isNaN(n)) return fallback
|
||
return n
|
||
} catch (e) {
|
||
warnStep('S00_READ_NUMBER', `readNumberCompat failed key=${key}`, e)
|
||
return fallback
|
||
}
|
||
}
|
||
|
||
const orderId = ref('')
|
||
const order = ref<DeliveryOrderType | null>(null)
|
||
const currentLocation = ref<DeliveryLocationType | null>(null)
|
||
const locationText = ref('未获取')
|
||
const accuracyText = ref('未知')
|
||
const photos = ref([] as Array<string>)
|
||
const evidenceFileIds = ref([] as Array<string>)
|
||
const note = ref('')
|
||
const submitting = ref(false)
|
||
|
||
// 居家服务认证状态
|
||
const isHomecareLoggedIn = ref(false)
|
||
const homecareUserEmail = ref('')
|
||
|
||
// 距离预校验状态
|
||
const prechecking = ref(false)
|
||
const canCheckin = ref(false)
|
||
const distanceText = ref('未校验')
|
||
const allowedRadiusText = ref('未校验')
|
||
const precheckStatusText = ref('未校验')
|
||
const reasonText = ref('')
|
||
const resolvedWorkOrderId = ref('')
|
||
|
||
async function updateHomecareLoginStatus(): Promise<void> {
|
||
const akUserId = await readCurrentAkUserId()
|
||
const storageAkUserId = uni.getStorageSync('ak_user_id')
|
||
const storageUserId = uni.getStorageSync('user_id')
|
||
const hasCurrentAkUser = uni.getStorageSync('current_ak_user') != null
|
||
const hasUserInfo = uni.getStorageSync('user_info') != null
|
||
|
||
logStep('S02_AUTH', 'current identity resolved', {
|
||
akUserId: akUserId,
|
||
storageAkUserId: storageAkUserId,
|
||
storageUserId: storageUserId,
|
||
hasCurrentAkUser: hasCurrentAkUser,
|
||
hasUserInfo: hasUserInfo
|
||
})
|
||
|
||
if (akUserId == '') {
|
||
isHomecareLoggedIn.value = false
|
||
homecareUserEmail.value = ''
|
||
warnStep('S02_AUTH', 'akUserId missing, cannot continue', {
|
||
orderId: orderId.value
|
||
})
|
||
return
|
||
}
|
||
|
||
isHomecareLoggedIn.value = true
|
||
homecareUserEmail.value = '已登录(uid: ' + akUserId.substring(0, 8) + '...)'
|
||
logStep('S02_AUTH', 'homecare login status updated', {
|
||
akUserIdPrefix: akUserId.substring(0, 8)
|
||
})
|
||
}
|
||
|
||
async function loginHomecare(): Promise<void> {
|
||
const token = getHomecareToken()
|
||
if (token !== '') {
|
||
uni.showToast({ title: '已登录,无需重复登录', icon: 'success' })
|
||
return
|
||
}
|
||
uni.showToast({ title: '居家服务认证服务暂不可用', icon: 'none' })
|
||
}
|
||
|
||
function toRadians(value: number): number {
|
||
return value * 3.1415926 / 180
|
||
}
|
||
|
||
function calculateDistance(lat1: number, lng1: number, lat2: number, lng2: number): number {
|
||
const earthRadius = 6378137
|
||
const deltaLat = toRadians(lat2 - lat1)
|
||
const deltaLng = toRadians(lng2 - lng1)
|
||
const a = Math.sin(deltaLat / 2) * Math.sin(deltaLat / 2) + Math.cos(toRadians(lat1)) * Math.cos(toRadians(lat2)) * Math.sin(deltaLng / 2) * Math.sin(deltaLng / 2)
|
||
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a))
|
||
return earthRadius * c
|
||
}
|
||
|
||
function wrapLocation(): Promise<DeliveryLocationType> {
|
||
logStep('S04_LOCATION', 'wrapLocation: starting')
|
||
return new Promise((resolve, reject) => {
|
||
uni.getLocation({
|
||
type: 'gcj02',
|
||
isHighAccuracy: true,
|
||
enableHighAccuracy: true,
|
||
success: (res) => {
|
||
logStep('S04_LOCATION', 'wrapLocation success', {
|
||
latitude: res.latitude,
|
||
longitude: res.longitude,
|
||
accuracy: res.accuracy,
|
||
speed: res.speed,
|
||
altitude: res.altitude,
|
||
altitudeAccuracy: res.altitudeAccuracy,
|
||
heading: res.heading,
|
||
timestamp: res.timestamp
|
||
})
|
||
resolve({
|
||
latitude: res.latitude,
|
||
longitude: res.longitude,
|
||
address: '当前位置',
|
||
time: new Date().toISOString().replace('T', ' ').substring(0, 19)
|
||
})
|
||
},
|
||
fail: (err) => {
|
||
errorStep('S04_LOCATION', 'wrapLocation fail', err, {
|
||
orderId: orderId.value
|
||
})
|
||
reject(new Error('定位失败'))
|
||
}
|
||
})
|
||
})
|
||
}
|
||
|
||
type LocationFullResult = {
|
||
location: DeliveryLocationType
|
||
accuracy: number
|
||
}
|
||
|
||
function wrapLocationFull(): Promise<LocationFullResult> {
|
||
logStep('S04_LOCATION', 'wrapLocationFull: starting')
|
||
return new Promise((resolve, reject) => {
|
||
uni.getLocation({
|
||
type: 'gcj02',
|
||
isHighAccuracy: true,
|
||
enableHighAccuracy: true,
|
||
success: (res) => {
|
||
logStep('S04_LOCATION', 'wrapLocationFull success', {
|
||
latitude: res.latitude,
|
||
longitude: res.longitude,
|
||
accuracy: res.accuracy,
|
||
speed: res.speed,
|
||
altitude: res.altitude,
|
||
altitudeAccuracy: res.altitudeAccuracy,
|
||
heading: res.heading,
|
||
timestamp: res.timestamp
|
||
})
|
||
const loc: DeliveryLocationType = {
|
||
latitude: res.latitude,
|
||
longitude: res.longitude,
|
||
address: '当前位置',
|
||
time: new Date().toISOString().replace('T', ' ').substring(0, 19)
|
||
}
|
||
const acc = res.accuracy != null ? res.accuracy : 0
|
||
logStep('S04_LOCATION', 'wrapLocationFull resolved', {
|
||
latitude: loc.latitude,
|
||
longitude: loc.longitude,
|
||
accuracy: acc
|
||
})
|
||
resolve({ location: loc, accuracy: acc })
|
||
},
|
||
fail: (err) => {
|
||
errorStep('S04_LOCATION', 'wrapLocationFull fail', err, {
|
||
orderId: orderId.value
|
||
})
|
||
reject(new Error('定位失败'))
|
||
}
|
||
})
|
||
})
|
||
}
|
||
|
||
function wrapChooseImage(): Promise<Array<string>> {
|
||
logStep('S07_PHOTO', 'uni.chooseImage request', {
|
||
count: 3,
|
||
sourceType: ['camera']
|
||
})
|
||
return new Promise((resolve, reject) => {
|
||
uni.chooseImage({
|
||
count: 3,
|
||
sourceType: ['camera'],
|
||
success: (res) => {
|
||
logStep('S07_PHOTO', 'uni.chooseImage success', {
|
||
tempFilePathsCount: res.tempFilePaths.length,
|
||
tempFilePaths: res.tempFilePaths
|
||
})
|
||
resolve(res.tempFilePaths)
|
||
},
|
||
fail: (err) => {
|
||
errorStep('S07_PHOTO', 'uni.chooseImage fail', err, {
|
||
orderId: orderId.value
|
||
})
|
||
reject(new Error('拍照失败'))
|
||
}
|
||
})
|
||
})
|
||
}
|
||
|
||
async function loadData() {
|
||
logStep('S03_ORDER_DETAIL', 'loadData start', {
|
||
orderId: orderId.value
|
||
})
|
||
|
||
try {
|
||
logStep('S02_AUTH', 'requireDeliveryAuth start')
|
||
const authResult = await requireDeliveryAuth({ redirectOnFail: true, toastOnFail: true })
|
||
if (!authResult.ok) {
|
||
warnStep('S02_AUTH', 'requireDeliveryAuth failed, stop loading', {
|
||
orderId: orderId.value,
|
||
authOk: authResult.ok
|
||
})
|
||
return
|
||
}
|
||
logStep('S02_AUTH', 'requireDeliveryAuth success')
|
||
} catch (e) {
|
||
errorStep('S02_AUTH', 'requireDeliveryAuth failed', e, {
|
||
orderId: orderId.value
|
||
})
|
||
return
|
||
}
|
||
|
||
if (orderId.value == '') {
|
||
warnStep('S03_ORDER_DETAIL', 'blocked: orderId is empty', {
|
||
orderId: orderId.value
|
||
})
|
||
return
|
||
}
|
||
|
||
try {
|
||
logStep('S03_ORDER_DETAIL', 'getDeliveryOrderDetail request', {
|
||
orderId: orderId.value
|
||
})
|
||
order.value = await getDeliveryOrderDetail(orderId.value)
|
||
const detail = order.value
|
||
if (detail != null) {
|
||
logStep('S03_ORDER_DETAIL', 'getDeliveryOrderDetail success', {
|
||
id: detail.id,
|
||
orderNo: detail.orderNo,
|
||
status: detail.status,
|
||
statusText: detail.statusText,
|
||
deliveryStaffId: detail.deliveryStaffId,
|
||
currentAssignmentId: (detail as any).currentAssignmentId,
|
||
latitude: detail.latitude,
|
||
longitude: detail.longitude,
|
||
address: detail.address,
|
||
allowCheckinRadiusMeters: detail.allowCheckinRadiusMeters
|
||
})
|
||
} else {
|
||
warnStep('S03_ORDER_DETAIL', 'getDeliveryOrderDetail returned null', {
|
||
orderId: orderId.value
|
||
})
|
||
}
|
||
} catch (e) {
|
||
errorStep('S03_ORDER_DETAIL', 'getDeliveryOrderDetail failed', e, {
|
||
orderId: orderId.value
|
||
})
|
||
return
|
||
}
|
||
|
||
updateHomecareLoginStatus()
|
||
}
|
||
|
||
async function getCurrentLocation() {
|
||
logStep('S04_LOCATION', 'getCurrentLocation start', {
|
||
orderId: orderId.value
|
||
})
|
||
try {
|
||
currentLocation.value = await wrapLocation()
|
||
locationText.value = '纬度 ' + String(currentLocation.value.latitude) + ' / 经度 ' + String(currentLocation.value.longitude)
|
||
logStep('S04_LOCATION', 'getCurrentLocation success', {
|
||
latitude: currentLocation.value.latitude,
|
||
longitude: currentLocation.value.longitude,
|
||
address: currentLocation.value.address,
|
||
time: currentLocation.value.time
|
||
})
|
||
} catch (error) {
|
||
errorStep('S04_LOCATION', 'getCurrentLocation error', error, {
|
||
orderId: orderId.value
|
||
})
|
||
uni.showToast({ title: '签到定位失败,请检查定位权限', icon: 'none' })
|
||
}
|
||
}
|
||
|
||
async function choosePhoto() {
|
||
logStep('S07_PHOTO', 'choosePhoto start', {
|
||
orderId: orderId.value,
|
||
canCheckin: canCheckin.value,
|
||
currentLocation: currentLocation.value,
|
||
evidenceFileIdsLength: evidenceFileIds.value.length
|
||
})
|
||
|
||
try {
|
||
// wrapChooseImage 已经返回 tempFilePaths (Array<string>)
|
||
const paths = await wrapChooseImage()
|
||
|
||
if (paths.length > 0) {
|
||
photos.value = paths
|
||
}
|
||
|
||
// 上传图片并创建 compat evidence 记录
|
||
if (paths.length > 0 && orderId.value != '') {
|
||
const akUserId = await readCurrentAkUserId()
|
||
logStep('S02_AUTH', 'current identity resolved', {
|
||
akUserId: akUserId,
|
||
storageAkUserId: uni.getStorageSync('ak_user_id'),
|
||
storageUserId: uni.getStorageSync('user_id'),
|
||
hasCurrentAkUser: uni.getStorageSync('current_ak_user') != null,
|
||
hasUserInfo: uni.getStorageSync('user_info') != null
|
||
})
|
||
|
||
if (akUserId == '') {
|
||
warnStep('S02_AUTH', 'akUserId missing, cannot continue', {
|
||
orderId: orderId.value
|
||
})
|
||
uni.showToast({ title: '未获取到业务用户 ID', icon: 'none' })
|
||
return
|
||
}
|
||
|
||
// 上传每张照片
|
||
for (let i = 0; i < paths.length; i++) {
|
||
const filePath = paths[i]
|
||
const storagePath = 'checkin/' + orderId.value + '/' + Date.now() + '_' + i + '.jpg'
|
||
|
||
// 确保 filePath 是字符串
|
||
if (typeof filePath != 'string' || filePath == '') {
|
||
warnStep('S07_PHOTO', `filePath invalid at index=${i}, skip`, {
|
||
filePath: filePath
|
||
})
|
||
continue
|
||
}
|
||
|
||
let fileUrl = ''
|
||
|
||
logStep('S07_PHOTO', 'upload evidence file start', {
|
||
index: i,
|
||
filePath: filePath,
|
||
storagePath: storagePath
|
||
})
|
||
|
||
try {
|
||
// 尝试上传到 Supabase Storage
|
||
const { data: uploadData, error: uploadError } = await supa.storage
|
||
.from('evidence')
|
||
.upload(storagePath, {
|
||
base64: filePath,
|
||
contentType: 'image/jpeg'
|
||
})
|
||
|
||
if (uploadError == null && uploadData != null) {
|
||
// 获取公开 URL
|
||
const { data: urlData } = supa.storage
|
||
.from('evidence')
|
||
.getPublicUrl(uploadData.path)
|
||
|
||
if (urlData != null) {
|
||
fileUrl = urlData.publicUrl
|
||
}
|
||
}
|
||
} catch (uploadErr) {
|
||
warnStep('S07_PHOTO', `upload evidence file exception at index=${i}`, uploadErr, {
|
||
filePath: filePath,
|
||
storagePath: storagePath
|
||
})
|
||
}
|
||
|
||
// 如果上传失败,使用测试 fileUrl
|
||
if (fileUrl == '') {
|
||
fileUrl = 'https://example.com/test-checkin-' + String(i) + '.jpg'
|
||
warnStep('S07_PHOTO', `upload failed, using test fileUrl at index=${i}`, {
|
||
fileUrl: fileUrl
|
||
})
|
||
}
|
||
|
||
logStep('S07_PHOTO', 'upload evidence file success', {
|
||
index: i,
|
||
publicUrl: fileUrl
|
||
})
|
||
|
||
// 调用 compat RPC 创建 evidence 记录
|
||
logStep('S08_EVIDENCE_RPC', 'createCheckinEvidenceCompat request', {
|
||
orderId: orderId.value,
|
||
akUserId: akUserId,
|
||
publicUrl: fileUrl,
|
||
note: note.value
|
||
})
|
||
|
||
const evidenceResult = await createCheckinEvidenceCompat(
|
||
orderId.value,
|
||
akUserId,
|
||
fileUrl,
|
||
note.value
|
||
)
|
||
|
||
logStep('S08_EVIDENCE_RPC', 'createCheckinEvidenceCompat raw result', evidenceResult)
|
||
|
||
const evidenceOk = readBoolCompat(evidenceResult, 'ok', false)
|
||
const evidenceReasonCode = readStringCompat(evidenceResult, 'reasonCode', '')
|
||
const evidenceId = readStringCompat(evidenceResult, 'id', '')
|
||
|
||
logStep('S08_EVIDENCE_RPC', 'createCheckinEvidenceCompat parsed result', {
|
||
ok: evidenceOk,
|
||
evidenceId: evidenceId,
|
||
evidenceFileIdsBefore: evidenceFileIds.value
|
||
})
|
||
|
||
if (evidenceReasonCode == 'OK' || evidenceOk) {
|
||
if (evidenceId != '') {
|
||
evidenceFileIds.value.push(evidenceId)
|
||
logStep('S08_EVIDENCE_RPC', 'evidenceFileIds updated', {
|
||
evidenceFileIdsAfter: evidenceFileIds.value,
|
||
evidenceFileIdsLength: evidenceFileIds.value.length
|
||
})
|
||
} else {
|
||
warnStep('S08_EVIDENCE_RPC', 'evidenceId missing, photo cannot be used for submit', {
|
||
evidenceResult: evidenceResult
|
||
})
|
||
}
|
||
} else {
|
||
warnStep('S08_EVIDENCE_RPC', 'evidence record creation rejected', {
|
||
reasonCode: evidenceReasonCode,
|
||
message: readStringCompat(evidenceResult, 'message', ''),
|
||
evidenceResult: evidenceResult
|
||
})
|
||
}
|
||
}
|
||
}
|
||
} catch (error) {
|
||
errorStep('S07_PHOTO', 'choosePhoto exception', error, {
|
||
orderId: orderId.value
|
||
})
|
||
uni.showToast({ title: '拍照失败,请重试', icon: 'none' })
|
||
}
|
||
}
|
||
|
||
async function handlePrecheck(): Promise<void> {
|
||
logStep('S05_PRECHECK_RPC', 'handlePrecheck start', {
|
||
orderId: orderId.value,
|
||
prechecking: prechecking.value
|
||
})
|
||
|
||
if (prechecking.value) {
|
||
warnStep('S05_PRECHECK_RPC', 'blocked: precheck already in progress')
|
||
return
|
||
}
|
||
|
||
if (orderId.value === '') {
|
||
warnStep('S05_PRECHECK_RPC', 'blocked: orderId is empty', {
|
||
rawOrderId: orderId.value
|
||
})
|
||
uni.showToast({ title: '工单信息缺失', icon: 'none' })
|
||
return
|
||
}
|
||
|
||
// 获取 akUserId
|
||
const akUserId = await readCurrentAkUserId()
|
||
logStep('S02_AUTH', 'current identity resolved', {
|
||
akUserId: akUserId,
|
||
storageAkUserId: uni.getStorageSync('ak_user_id'),
|
||
storageUserId: uni.getStorageSync('user_id'),
|
||
hasCurrentAkUser: uni.getStorageSync('current_ak_user') != null,
|
||
hasUserInfo: uni.getStorageSync('user_info') != null
|
||
})
|
||
if (akUserId == '' || akUserId == null) {
|
||
warnStep('S02_AUTH', 'akUserId missing, cannot continue', {
|
||
orderId: orderId.value
|
||
})
|
||
uni.showToast({ title: '业务账号未初始化,请重新登录', icon: 'none' })
|
||
return
|
||
}
|
||
|
||
prechecking.value = true
|
||
canCheckin.value = false
|
||
distanceText.value = '定位中...'
|
||
allowedRadiusText.value = '定位中...'
|
||
precheckStatusText.value = '定位中...'
|
||
reasonText.value = ''
|
||
resolvedWorkOrderId.value = ''
|
||
|
||
try {
|
||
logStep('S04_LOCATION', 'getLocation start', {
|
||
orderId: orderId.value
|
||
})
|
||
const fullResult = await wrapLocationFull()
|
||
const location = fullResult.location
|
||
const accuracy = fullResult.accuracy
|
||
|
||
if (location == null || location.latitude == 0 || location.longitude == 0 || accuracy == null || accuracy < 0 || accuracy > 5000) {
|
||
warnStep('S04_LOCATION', 'location suspicious', {
|
||
latitude: location != null ? location.latitude : 0,
|
||
longitude: location != null ? location.longitude : 0,
|
||
accuracy: accuracy
|
||
})
|
||
}
|
||
|
||
logStep('S04_LOCATION', 'getLocation success', {
|
||
latitude: location.latitude,
|
||
longitude: location.longitude,
|
||
accuracy: accuracy,
|
||
coordinateType: 'gcj02'
|
||
})
|
||
|
||
currentLocation.value = location
|
||
locationText.value = '纬度 ' + String(location.latitude) + ' / 经度 ' + String(location.longitude)
|
||
accuracyText.value = accuracy > 0 ? String(accuracy) + ' 米' : '未知'
|
||
|
||
// 调用 compat RPC 预校验
|
||
const precheckPayload = {
|
||
orderId: orderId.value,
|
||
akUserId: akUserId,
|
||
latitude: location.latitude,
|
||
longitude: location.longitude,
|
||
accuracy: accuracy,
|
||
coordinateType: 'gcj02'
|
||
}
|
||
logStep('S05_PRECHECK_RPC', 'checkinPrecheckCompat request', precheckPayload)
|
||
|
||
const result = await checkinPrecheckCompat(
|
||
orderId.value,
|
||
akUserId,
|
||
location.latitude,
|
||
location.longitude,
|
||
accuracy
|
||
)
|
||
|
||
logStep('S06_PRECHECK_RESULT', 'checkinPrecheckCompat raw result', result)
|
||
|
||
const canCheckinResult = readBoolCompat(result, 'canCheckin', false)
|
||
const reasonCode = readStringCompat(result, 'reasonCode', '')
|
||
const message = readStringCompat(result, 'message', '')
|
||
const distanceMeters = readNumberCompat(result, 'distanceMeters', 0)
|
||
const allowedRadiusMeters = readNumberCompat(result, 'allowedRadiusMeters', 0)
|
||
const serviceLocationReady = readBoolCompat(result, 'serviceLocationReady', false)
|
||
const workerLocationAccepted = readBoolCompat(result, 'workerLocationAccepted', false)
|
||
const resolvedWoId = readStringCompat(result, 'resolvedWorkOrderId', '')
|
||
const taskStatus = readStringCompat(result, 'taskStatus', '')
|
||
|
||
logStep('S06_PRECHECK_RESULT', 'checkinPrecheckCompat parsed result', {
|
||
canCheckin: canCheckinResult,
|
||
reasonCode: reasonCode,
|
||
message: message,
|
||
distanceMeters: distanceMeters,
|
||
allowedRadiusMeters: allowedRadiusMeters,
|
||
serviceLocationReady: serviceLocationReady,
|
||
workerLocationAccepted: workerLocationAccepted,
|
||
resolvedWorkOrderId: resolvedWoId,
|
||
taskStatus: taskStatus
|
||
})
|
||
|
||
if (result.distanceMeters != null || (typeof result.getNumber == 'function' && result.getNumber('distanceMeters') != null)) {
|
||
const dist = readNumberCompat(result, 'distanceMeters', 0)
|
||
distanceText.value = String(dist) + ' 米'
|
||
} else {
|
||
distanceText.value = '未知'
|
||
}
|
||
const radius = result.allowedRadiusMeters != null ? result.allowedRadiusMeters : readNumberCompat(result, 'allowedRadiusMeters', 100)
|
||
allowedRadiusText.value = String(radius) + ' 米'
|
||
|
||
if (canCheckinResult || reasonCode == 'OK') {
|
||
canCheckin.value = true
|
||
precheckStatusText.value = '可以签到 / 下一步拍照'
|
||
reasonText.value = ''
|
||
resolvedWorkOrderId.value = resolvedWoId
|
||
logStep('S06_PRECHECK_RESULT', 'precheck allowed by backend', {
|
||
serviceLocationReady: serviceLocationReady,
|
||
workerLocationAccepted: workerLocationAccepted,
|
||
resolvedWorkOrderId: resolvedWoId,
|
||
taskStatus: taskStatus
|
||
})
|
||
} else {
|
||
canCheckin.value = false
|
||
precheckStatusText.value = '不可签到'
|
||
warnStep('S06_PRECHECK_RESULT', 'precheck rejected by backend', {
|
||
reasonCode: reasonCode,
|
||
message: message,
|
||
distanceMeters: distanceMeters,
|
||
allowedRadiusMeters: allowedRadiusMeters,
|
||
serviceLocationReady: serviceLocationReady,
|
||
workerLocationAccepted: workerLocationAccepted,
|
||
resolvedWorkOrderId: resolvedWoId,
|
||
taskStatus: taskStatus
|
||
})
|
||
// 根据 reasonCode 显示具体原因
|
||
let displayMessage = message
|
||
if (displayMessage == '') {
|
||
switch (reasonCode) {
|
||
case 'WORK_ORDER_ID_NOT_RESOLVED':
|
||
displayMessage = '当前订单没有对应工单,后端桥接失败'
|
||
break
|
||
case 'SERVICE_LOCATION_MISSING':
|
||
displayMessage = '当前工单缺少服务地址坐标'
|
||
break
|
||
case 'WORKER_NOT_MATCHED':
|
||
displayMessage = '当前账号不是该工单服务人员'
|
||
break
|
||
case 'OUT_OF_RADIUS':
|
||
displayMessage = '当前位置距离服务地点较远,请到达服务地址附近后签到'
|
||
break
|
||
case 'RPC_ERROR':
|
||
case 'RPC_EXCEPTION':
|
||
displayMessage = '签到接口异常,请查看控制台日志'
|
||
break
|
||
default:
|
||
displayMessage = '不可签到,原因 ' + (reasonCode != '' ? reasonCode : '未知原因')
|
||
break
|
||
}
|
||
}
|
||
reasonText.value = displayMessage
|
||
}
|
||
|
||
if (canCheckinResult && !canCheckin.value) {
|
||
warnStep('S06_PRECHECK_RESULT', 'backend allowed checkin but frontend state not updated', {
|
||
backendCanCheckin: canCheckinResult,
|
||
frontendCanCheckin: canCheckin.value
|
||
})
|
||
}
|
||
} catch (error) {
|
||
errorStep('S99_ERROR', 'handlePrecheck exception', error, {
|
||
orderId: orderId.value
|
||
})
|
||
uni.showToast({ title: '预校验失败,请重试', icon: 'none' })
|
||
precheckStatusText.value = '预校验失败'
|
||
} finally {
|
||
prechecking.value = false
|
||
logStep('S05_PRECHECK_RPC', 'handlePrecheck finished', {
|
||
canCheckin: canCheckin.value,
|
||
precheckStatusText: precheckStatusText.value
|
||
})
|
||
}
|
||
}
|
||
|
||
async function submitCheckin() {
|
||
logStep('S09_SUBMIT_GUARD', 'submitCheckin start', {
|
||
orderId: orderId.value,
|
||
submitting: submitting.value,
|
||
canCheckin: canCheckin.value,
|
||
hasOrder: order.value != null,
|
||
hasCurrentLocation: currentLocation.value != null,
|
||
evidenceFileIds: evidenceFileIds.value,
|
||
evidenceFileIdsLength: evidenceFileIds.value.length
|
||
})
|
||
|
||
if (submitting.value) {
|
||
warnStep('S09_SUBMIT_GUARD', 'blocked: submitting already true')
|
||
return
|
||
}
|
||
|
||
if (order.value == null) {
|
||
warnStep('S09_SUBMIT_GUARD', 'blocked: order is null', {
|
||
orderId: orderId.value
|
||
})
|
||
uni.showToast({ title: '订单信息缺失', icon: 'none' })
|
||
return
|
||
}
|
||
|
||
// 必须通过预校验才能提交
|
||
if (!canCheckin.value) {
|
||
warnStep('S09_SUBMIT_GUARD', 'blocked: canCheckin is false, need precheck first', {
|
||
orderId: orderId.value
|
||
})
|
||
uni.showToast({ title: '请先完成距离预校验', icon: 'none' })
|
||
return
|
||
}
|
||
|
||
if (currentLocation.value == null) {
|
||
warnStep('S09_SUBMIT_GUARD', 'currentLocation is null, will try getCurrentLocation', {
|
||
orderId: orderId.value
|
||
})
|
||
await getCurrentLocation()
|
||
if (currentLocation.value == null) {
|
||
warnStep('S09_SUBMIT_GUARD', 'blocked: currentLocation still null after getCurrentLocation', {
|
||
orderId: orderId.value
|
||
})
|
||
return
|
||
}
|
||
}
|
||
|
||
if (evidenceFileIds.value.length == 0) {
|
||
warnStep('S09_SUBMIT_GUARD', 'blocked: no evidenceFileIds', {
|
||
orderId: orderId.value,
|
||
evidenceFileIds: evidenceFileIds.value
|
||
})
|
||
uni.showToast({ title: '现场图片未上传成功,请重新拍照上传', icon: 'none' })
|
||
return
|
||
}
|
||
|
||
logStep('S09_SUBMIT_GUARD', 'all submit guards passed, proceeding to doCheckin', {
|
||
orderId: orderId.value,
|
||
evidenceFileIdsLength: evidenceFileIds.value.length
|
||
})
|
||
doCheckin()
|
||
}
|
||
|
||
async function doCheckin() {
|
||
submitting.value = true
|
||
try {
|
||
// 获取 akUserId
|
||
const akUserId = await readCurrentAkUserId()
|
||
logStep('S02_AUTH', 'current identity resolved', {
|
||
akUserId: akUserId,
|
||
storageAkUserId: uni.getStorageSync('ak_user_id'),
|
||
storageUserId: uni.getStorageSync('user_id'),
|
||
hasCurrentAkUser: uni.getStorageSync('current_ak_user') != null,
|
||
hasUserInfo: uni.getStorageSync('user_info') != null
|
||
})
|
||
|
||
logStep('S10_SUBMIT_RPC', 'doCheckin start', {
|
||
orderId: orderId.value,
|
||
canCheckin: canCheckin.value,
|
||
currentLocation: currentLocation.value,
|
||
evidenceFileIds: evidenceFileIds.value
|
||
})
|
||
|
||
if (akUserId == '') {
|
||
warnStep('S02_AUTH', 'akUserId missing, cannot continue', {
|
||
orderId: orderId.value
|
||
})
|
||
uni.showToast({ title: '未获取到业务用户 ID,请重新登录', icon: 'none' })
|
||
return
|
||
}
|
||
|
||
logStep('S10_SUBMIT_RPC', 'submit identity resolved', {
|
||
akUserId: akUserId,
|
||
orderId: orderId.value
|
||
})
|
||
|
||
// 验证 evidenceFileIds
|
||
if (evidenceFileIds.value.length < 1) {
|
||
warnStep('S10_SUBMIT_RPC', 'blocked: evidenceFileIds empty', {
|
||
orderId: orderId.value,
|
||
evidenceFileIds: evidenceFileIds.value
|
||
})
|
||
uni.showToast({ title: '现场图片未上传成功,请重新拍照上传', icon: 'none' })
|
||
return
|
||
}
|
||
|
||
const accuracyValue = parseFloat(accuracyText.value) || 0
|
||
|
||
const submitPayload = {
|
||
orderId: orderId.value,
|
||
akUserId: akUserId,
|
||
latitude: currentLocation.value!.latitude,
|
||
longitude: currentLocation.value!.longitude,
|
||
accuracy: accuracyValue,
|
||
evidenceFileIds: evidenceFileIds.value,
|
||
note: null,
|
||
eventType: 'worker_arrived'
|
||
}
|
||
logStep('S10_SUBMIT_RPC', 'submitHomecareCheckinCompat request', submitPayload)
|
||
|
||
// 调用 compat RPC 提交签到
|
||
const submitResult = await submitHomecareCheckinCompat(
|
||
orderId.value,
|
||
akUserId,
|
||
currentLocation.value!.latitude,
|
||
currentLocation.value!.longitude,
|
||
accuracyValue,
|
||
evidenceFileIds.value.slice(),
|
||
null,
|
||
'worker_arrived'
|
||
)
|
||
|
||
logStep('S11_SUBMIT_RESULT', 'submitHomecareCheckinCompat raw result', submitResult)
|
||
|
||
const reasonCode = readStringCompat(submitResult, 'reasonCode', '')
|
||
const message = readStringCompat(submitResult, 'message', '')
|
||
const confirmationInserted = readBoolCompat(submitResult, 'confirmationInserted', false)
|
||
const nextStatus = readStringCompat(submitResult, 'nextStatus', '')
|
||
const resolvedWoId = readStringCompat(submitResult, 'resolvedWorkOrderId', '')
|
||
|
||
logStep('S11_SUBMIT_RESULT', 'submitHomecareCheckinCompat parsed result', {
|
||
reasonCode: reasonCode,
|
||
message: message,
|
||
confirmationInserted: confirmationInserted,
|
||
nextStatus: nextStatus,
|
||
resolvedWorkOrderId: resolvedWoId
|
||
})
|
||
|
||
// 成功条件: reasonCode=='OK' || confirmationInserted==true || nextStatus=='ARRIVAL_PENDING'
|
||
if (reasonCode == 'OK' || confirmationInserted == true || nextStatus == 'ARRIVAL_PENDING') {
|
||
logStep('S11_SUBMIT_RESULT', 'checkin submit success, frontend state will update', {
|
||
reasonCode: reasonCode,
|
||
confirmationInserted: confirmationInserted,
|
||
nextStatus: nextStatus
|
||
})
|
||
// 签到成功后:本地状态设置为 ARRIVAL_PENDING(等待消费者确认到达)
|
||
if (order.value != null) {
|
||
order.value.status = 'arrival_pending' as any
|
||
order.value.statusText = '等待消费者确认到达'
|
||
order.value.statusTone = 'warning'
|
||
}
|
||
|
||
uni.showModal({
|
||
title: '签到成功',
|
||
content: '已提交到达签到,等待消费者确认',
|
||
showCancel: false,
|
||
success: () => {
|
||
uni.navigateBack()
|
||
}
|
||
})
|
||
} else {
|
||
warnStep('S11_SUBMIT_RESULT', 'checkin submit rejected', {
|
||
reasonCode: reasonCode,
|
||
message: message,
|
||
confirmationInserted: confirmationInserted,
|
||
nextStatus: nextStatus,
|
||
resolvedWorkOrderId: resolvedWoId
|
||
})
|
||
// 失败:显示 message 或 reasonCode
|
||
const displayMsg = message != '' ? message : ('签到失败: ' + reasonCode)
|
||
uni.showToast({ title: displayMsg, icon: 'none' })
|
||
}
|
||
} catch (error) {
|
||
errorStep('S99_ERROR', 'doCheckin exception', error, {
|
||
orderId: orderId.value
|
||
})
|
||
uni.showToast({ title: '签到失败,请重试', icon: 'none' })
|
||
} finally {
|
||
submitting.value = false
|
||
logStep('S10_SUBMIT_RPC', 'doCheckin finished', {
|
||
orderId: orderId.value,
|
||
submitting: submitting.value
|
||
})
|
||
}
|
||
}
|
||
|
||
onLoad((options) => {
|
||
checkinTraceId.value = Date.now().toString()
|
||
|
||
logStep('S01_ROUTE', 'checkin page onLoad start', {
|
||
rawOptions: options,
|
||
extractedOrderId: getDeliveryRouteParam(options as UTSJSONObject, 'id')
|
||
})
|
||
|
||
if (options != null) {
|
||
orderId.value = getDeliveryRouteParam(options as UTSJSONObject, 'id')
|
||
logStep('S01_ROUTE', 'orderId resolved', {
|
||
orderId: orderId.value
|
||
})
|
||
}
|
||
|
||
if (orderId.value == '') {
|
||
warnStep('S01_ROUTE', 'missing orderId, stop loading', {
|
||
rawOptions: options
|
||
})
|
||
return
|
||
}
|
||
|
||
logStep('S01_ROUTE', 'calling loadData', {
|
||
orderId: orderId.value
|
||
})
|
||
loadData()
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.info-text {
|
||
display: block;
|
||
margin-bottom: 14rpx;
|
||
font-size: 26rpx;
|
||
line-height: 38rpx;
|
||
color: #16324f;
|
||
}
|
||
|
||
.success-text {
|
||
color: #0f766e;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.warning-text {
|
||
color: #dc2626;
|
||
}
|
||
|
||
.button-stack {
|
||
flex-direction: column;
|
||
}
|
||
|
||
.primary-btn,
|
||
.secondary-btn {
|
||
margin-bottom: 18rpx;
|
||
border-radius: 18rpx;
|
||
font-size: 28rpx;
|
||
}
|
||
|
||
.primary-btn {
|
||
background: #0f766e;
|
||
color: #ffffff;
|
||
}
|
||
|
||
.primary-btn[disabled] {
|
||
background: #9ca3af;
|
||
}
|
||
|
||
.secondary-btn {
|
||
background: #eaf2f0;
|
||
color: #0f766e;
|
||
}
|
||
|
||
.note-input {
|
||
height: 84rpx;
|
||
padding: 0 24rpx;
|
||
margin-bottom: 18rpx;
|
||
border-radius: 18rpx;
|
||
background: #f2f7f6;
|
||
font-size: 28rpx;
|
||
}
|
||
</style>
|
||
|