Files
medical-mall/pages/mall/delivery/orders/checkin.uvue

1066 lines
32 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>