Files
medical-mall/services/serviceOrderService.uts
2026-06-12 10:16:27 +08:00

1079 lines
41 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.
import supa from '@/components/supadb/aksupainstance.uts'
import { getCurrentUserId } from '@/utils/store.uts'
import { getCurrentUser } from '@/utils/store.uts'
import type { UserAddress } from '@/utils/supabaseService.uts'
import type { HomeServiceCatalogType, HomeServicePackageType } from '@/types/home-service.uts'
import type { DeliveryServiceRecordType } from '@/types/delivery.uts'
import {
getServiceOrderStatusText,
normalizeServiceOrderStatus,
type ServiceOrderAddressSnapshotType,
type ServiceEvidenceFileType,
type ServiceExecutionRecordType,
type ServiceOrderStatus,
type ServiceOrderTimelineItemType,
type ServiceOrderType,
type ServiceReviewType
} from '@/types/service-order.uts'
export type HomecareDispatchResult = {
success: boolean
code: string
message: string
display_type: string
retryable: boolean
dispatch_status?: string
order_id?: string
assignment_id?: string
staff_id?: string
station_id?: string
dispatch_distance_km?: number
}
export const HOMECARE_DISPATCH_STATUS_PENDING = 'pending'
export const HOMECARE_DISPATCH_STATUS_DISPATCHING = 'dispatching'
export const HOMECARE_DISPATCH_STATUS_ASSIGNED = 'assigned'
export const HOMECARE_DISPATCH_STATUS_FAILED = 'failed'
export type CreateServiceOrderParams = {
service: HomeServiceCatalogType
packageInfo: HomeServicePackageType
address: ServiceOrderAddressSnapshotType
recipientName: string
recipientPhone: string
recipientAge: number
recipientGender: string
contactName: string
contactPhone: string
appointmentTime: string
remark: string
}
function buildServiceSnapshot(params: CreateServiceOrderParams): any {
return {
serviceId: params.service.id,
serviceName: params.service.name,
category: params.service.category,
price: params.packageInfo.price,
durationText: params.packageInfo.durationText != '' ? params.packageInfo.durationText : params.service.durationText,
summary: params.packageInfo.packageDesc != '' ? params.packageInfo.packageDesc : params.service.summary,
tags: params.service.tags,
suitableFor: params.service.suitableFor,
packageId: params.packageInfo.id,
packageName: params.packageInfo.packageName,
packagePrice: params.packageInfo.price,
packageListPrice: params.packageInfo.listPrice,
packageDataSource: params.packageInfo.dataSource,
packageSeedBatchNo: params.packageInfo.seedBatchNo
} as any
}
function buildPricingSnapshot(params: CreateServiceOrderParams): any {
return {
service_id: params.service.id,
service_name: params.service.name,
package_id: params.packageInfo.id,
package_name: params.packageInfo.packageName,
package_desc: params.packageInfo.packageDesc,
duration_minutes: params.packageInfo.durationMinutes,
duration_text: params.packageInfo.durationText,
price: params.packageInfo.price,
list_price: params.packageInfo.listPrice,
data_source: params.packageInfo.dataSource,
seed_batch_no: params.packageInfo.seedBatchNo,
remark: params.packageInfo.remark
} as any
}
const HOMECARE_DISPATCH_CANDIDATE_RPC = 'rpc_homecare_dispatch_candidate'
function nowText(): string {
return new Date().toISOString().replace('T', ' ').substring(0, 19)
}
function nowIso(): string {
return new Date().toISOString()
}
function buildId(prefix: string): string {
return prefix + '-' + String(Date.now()) + '-' + String(Math.floor(Math.random() * 100000)).padStart(5, '0')
}
function randomHex(len: number): string {
let result = ''
for (let i = 0; i < len; i++) {
result += Math.floor(Math.random() * 16).toString(16)
}
return result
}
function generateUuid(): string {
return randomHex(8) + '-' + randomHex(4) + '-4' + randomHex(3) + '-' + String(Math.floor(Math.random() * 4 + 8).toString(16)) + randomHex(3) + '-' + randomHex(12)
}
function buildOrderNo(): string {
const now = new Date()
const year = String(now.getFullYear())
const month = String(now.getMonth() + 1).padStart(2, '0')
const day = String(now.getDate()).padStart(2, '0')
const random = String(Math.floor(Math.random() * 1000000)).padStart(6, '0')
return 'SO' + year + month + day + random
}
function normalizeAppointmentTime(time: string): string {
if (time == '') {
return new Date().toISOString()
}
const parsed = Date.parse(time)
if (!isNaN(parsed)) {
return new Date(parsed).toISOString()
}
return new Date().toISOString()
}
function normalizeUuidOrNull(id: string): string | null {
if (id == '') {
return null
}
if (id.indexOf('-') >= 0 && id.length == 36) {
return id
}
return null
}
function plainObject(source: any): any {
if (source instanceof UTSJSONObject) {
return source
}
return JSON.parse(JSON.stringify(source)) as any
}
function readString(source: any, key: string): string {
if (source instanceof UTSJSONObject) {
const value = (source as UTSJSONObject).getString(key)
return value != null ? value : ''
}
const value = plainObject(source)[key]
if (value == null) {
return ''
}
return typeof value == 'string' ? value : String(value)
}
function readNumber(source: any, key: string): number {
if (source instanceof UTSJSONObject) {
const value = (source as UTSJSONObject).getNumber(key)
return value != null ? value : 0
}
const value = plainObject(source)[key]
if (typeof value == 'number') {
return value
}
if (typeof value == 'string' && value != '') {
const parsed = Number(value)
return isNaN(parsed) ? 0 : parsed
}
return 0
}
function readFirstString(source: any, keys: Array<string>): string {
for (let i = 0; i < keys.length; i++) {
const value = readString(source, keys[i])
if (value != '') {
return value
}
}
return ''
}
function readJsonField(source: any, key: string): any {
if (source != null && typeof source.getJSONObj === 'function') {
const value = source.getJSONObj(key)
return value != null ? value : null
}
const value = plainObject(source)[key]
return value == null ? null : value
}
function hasMissingColumnError(error: any, columnName: string): boolean {
if (error == null || columnName == '') {
return false
}
const errorText = JSON.stringify(error).toLowerCase()
return errorText.indexOf('could not find the') >= 0 && errorText.indexOf(columnName.toLowerCase()) >= 0
}
let ecCareTaskCreateUnavailable = false
function shouldBypassEcCareTaskCreate(error: any): boolean {
if (error == null) {
return false
}
return hasMissingColumnError(error, 'address_snapshot_json')
|| hasMissingColumnError(error, 'service_snapshot_json')
|| hasMissingColumnError(error, 'scheduled_at')
|| hasMissingColumnError(error, 'appointment_time')
|| hasMissingColumnError(error, 'task_no')
|| hasMissingColumnError(error, 'service_catalog_id')
|| hasMissingColumnError(error, 'elder_name')
|| hasMissingColumnError(error, 'contact_name')
}
function readServiceItems(source: any): Array<DeliveryServiceItemType> {
const raw = readJsonField(source, 'service_items_json')
const result = [] as Array<DeliveryServiceItemType>
if (raw == null) {
return result
}
const items = Array.isArray(raw) ? raw : plainObject(raw)
if (!Array.isArray(items)) {
return result
}
for (let i = 0; i < items.length; i++) {
result.push({
id: readFirstString(items[i], ['id']) != '' ? readFirstString(items[i], ['id']) : buildId('svc-item'),
name: readFirstString(items[i], ['name', 'label']),
required: readString(items[i], 'required') != 'false',
completed: readString(items[i], 'completed') == 'true',
incompleteReason: readString(items[i], 'incompleteReason'),
remark: readString(items[i], 'remark'),
updatedAt: readString(items[i], 'updatedAt')
} as DeliveryServiceItemType)
}
return result
}
function buildDefaultServiceItems(order: DeliveryOrderType): Array<DeliveryServiceItemType> {
const names = [] as Array<string>
if (order.serviceName.indexOf('护理') >= 0) {
names.push('上门签到确认')
names.push('基础护理执行')
names.push('生命体征记录')
names.push('家属沟通反馈')
} else if (order.serviceName.indexOf('随访') >= 0) {
names.push('上门签到确认')
names.push('慢病指标采集')
names.push('用药情况核对')
names.push('随访结论反馈')
} else {
names.push('上门签到确认')
names.push('服务项目执行')
names.push('服务过程记录')
names.push('完成情况确认')
}
const result = [] as Array<DeliveryServiceItemType>
for (let i = 0; i < names.length; i++) {
result.push({
id: order.id + '-item-' + String(i + 1),
name: names[i],
required: true,
completed: false,
incompleteReason: '',
remark: '',
updatedAt: ''
} as DeliveryServiceItemType)
}
return result
}
async function getCurrentStaffId(): Promise<string> {
const userId = getCurrentUserId()
if (userId == '') {
return ''
}
const profile = await getDeliveryProfileByUserId(userId)
return profile != null ? profile.id : ''
}
function getCurrentWorkerUserId(): string {
return getCurrentUserId()
}
// =============================================================================
// 居家服务 delivery 动作 RPC 统一调用封装
// 说明:所有会改变履约状态的动作必须走后端 RPC禁止前端直接 update/insert。
// =============================================================================
function parseRpcOrderResult(data: any): DeliveryOrderType | null {
if (data == null) {
return null
}
const plain = plainObject(data)
if (Array.isArray(plain)) {
if (plain.length == 0) {
return null
}
return plain[0] as DeliveryOrderType
}
if (readString(plain, 'id') == '') {
return null
}
return plain as DeliveryOrderType
}
async function callDeliveryActionRpc(rpcName: string, params: any): Promise<DeliveryOrderType | null> {
try {
const { data, error } = await supa.rpc(rpcName, params as any)
if (error != null) {
console.error('[delivery-action] RPC failed:', rpcName, error)
return null
}
return parseRpcOrderResult(data)
} catch (e) {
console.error('[delivery-action] RPC exception:', rpcName, e)
return null
}
}
// LEGACY: 以下两个函数为旧链路直接写表逻辑,已停用。新链路由 rpc_delivery_* 统一处理。
// 保留函数体仅为了避免引用处编译报错,实际不再执行写操作。
async function insertLegacyStatusLog(orderId: string, fromStatus: string, toStatus: ServiceOrderStatus, remark: string): Promise<void> {
// LEGACY/TODO: 旧链路直接写 hss_service_order_status_logs 已停用。
// 状态事件由后端 rpc_delivery_* 统一写入 hc_work_order_events / hss_service_order_status_logs。
console.warn('[LEGACY] insertLegacyStatusLog skipped for', orderId, fromStatus, toStatus, remark)
}
async function insertWorkOrderEvent(taskId: string, fromStatus: string, toStatus: string, action: string, remark: string): Promise<void> {
// LEGACY/TODO: 前端直接 insert hc_work_order_events 已禁止。
// 状态事件必须由后端 rpc_delivery_* 统一写入。
console.warn('[LEGACY] insertWorkOrderEvent skipped for', taskId, fromStatus, toStatus, action, remark)
}
function parseExecutionRecord(item: any): ServiceExecutionRecordType {
return {
id: '',
orderNo: '',
serviceType: '',
serviceName: '',
serviceCategory: '',
serviceItems: [] as Array<any>,
elderId: '',
elderName: '',
elderNameMasked: '',
elderGender: '',
elderAge: 0,
elderPhone: '',
elderPhoneMasked: '',
fullElderName: '',
fullPhone: '',
contactRelation: '家属',
addressSummary: '',
address: '',
addressDetail: '',
fullAddress: '',
latitude: 0,
longitude: 0,
appointmentTime: '',
appointmentStartTime: '',
appointmentEndTime: '',
duration: 90,
estimatedDuration: 90,
price: 0,
staffIncome: 0,
distance: '',
actualStartTime: '',
actualEndTime: '',
status: 'pending_assignment' as any,
statusText: '',
statusTone: 'warning',
riskTags: [] as Array<string>,
healthTags: [] as Array<string>,
careLevel: '',
needFamilyPresent: false,
needMaterials: false,
remark: '',
merchantId: '',
merchantName: '',
deliveryStaffId: '',
deliveryStaffName: '',
acceptTime: '',
departTime: '',
arriveTime: '',
checkinTime: '',
finishTime: '',
cancelReason: '',
exceptionType: '',
exceptionDesc: '',
evidenceList: [] as Array<any>,
signatureUrl: '',
signatureName: '',
satisfactionStatus: '',
settlementStatus: '',
archiveStatus: '',
createdAt: '',
updatedAt: '',
contactName: '',
contactPhone: '',
notices: [] as Array<string>,
timeline: [] as Array<any>,
statusLog: [] as Array<any>,
serviceSummary: '',
progressNote: '',
distanceKm: '',
allowCheckinRadiusMeters: 100,
lastLocation: null,
trackPoints: [] as Array<any>,
serviceRecord: null,
abnormalReport: null
} as DeliveryOrderType
}
function deriveTaskStatus(item: any): string {
const rawStatus = readString(item, 'status')
if (readFirstString(item, ['accepted_by_family_at']) != '') return 'ACCEPTED'
if (readFirstString(item, ['acceptance_pending_at']) != '') return 'ACCEPTANCE_PENDING'
if (readFirstString(item, ['service_started_at']) != '') return 'ORDER_IN_SERVICE'
if (readFirstString(item, ['checked_in_at']) != '') return 'ORDER_CHECKED_IN'
if (readFirstString(item, ['departed_at']) != '') return 'departed'
if (readFirstString(item, ['accepted_at']) != '') return 'ORDER_ACCEPTED'
return rawStatus
}
function statusToDeliveryStatus(status: ServiceOrderStatus, item: any): string {
if (status == 'assigned') return 'pending_assignment'
if (status == 'accepted') {
return readFirstString(item, ['departed_at']) != '' ? 'departed' : 'accepted'
}
if (status == 'departed') return 'departed'
if (status == 'arrived') return 'arrived'
if (status == 'in_service') return 'in_service'
if (status == 'pending_acceptance') return 'pending_acceptance'
if (status == 'reviewed' || status == 'accepted_by_user' || status == 'settled') return 'completed'
if (status == 'rejected') return 'rejected'
if (status == 'cancelled') return 'cancelled'
if (status == 'exception') return 'abnormal'
return 'pending_assignment'
}
function statusTone(status: ServiceOrderStatus): string {
if (status == 'pending_acceptance' || status == 'assigned') return 'warning'
if (status == 'accepted' || status == 'departed' || status == 'arrived' || status == 'in_service') return 'primary'
if (status == 'accepted_by_user' || status == 'reviewed' || status == 'settled') return 'success'
if (status == 'rejected' || status == 'cancelled' || status == 'exception') return 'danger'
return 'warning'
}
function hasOrderCoreInfo(order: DeliveryOrderType | null): boolean {
if (order == null) {
return false
}
return order.serviceName != '' || order.elderName != '' || order.address != '' || order.contactName != ''
}
function mapCareTaskToLegacyShape(item: any): any {
return {
id: readString(item, 'id'),
order_no: readFirstString(item, ['task_no', 'order_no']),
user_id: readFirstString(item, ['user_id', 'requester_user_id']),
service_id: readFirstString(item, ['service_catalog_id', 'service_id']),
service_name: readString(item, 'service_name'),
service_snapshot_json: serviceSnapshot,
service_address_id: readFirstString(item, ['service_address_id', 'address_id']),
address_snapshot_json: addressSnapshotValue != null ? addressSnapshotValue : JSON.parse('{}'),
recipient_name: readFirstString(item, ['elder_name', 'recipient_name']),
recipient_phone: readFirstString(item, ['elder_phone', 'recipient_phone']),
recipient_age: readFirstNumber(item, ['elder_age', 'recipient_age']),
recipient_gender: readFirstString(item, ['elder_gender', 'recipient_gender']),
contact_name: readString(item, 'contact_name'),
contact_phone: readString(item, 'contact_phone'),
appointment_time: readFirstString(item, ['scheduled_at', 'appointment_time']),
remark: readString(item, 'remark'),
status: derivedStatus,
current_assignment_id: readFirstString(item, ['assignment_id', 'current_assignment_id']),
current_staff_id: readFirstString(item, ['assigned_to', 'current_staff_id']),
accepted_at: readString(item, 'accepted_at'),
departed_at: readString(item, 'departed_at'),
arrived_at: readFirstString(item, ['checked_in_at', 'arrived_at']),
service_started_at: readString(item, 'service_started_at'),
completed_at: readFirstString(item, ['service_completed_at', 'completed_at']),
pending_acceptance_at: readFirstString(item, ['acceptance_pending_at', 'pending_acceptance_at']),
accepted_by_user_at: readFirstString(item, ['accepted_by_family_at', 'accepted_by_user_at']),
reviewed_at: readString(item, 'reviewed_at'),
created_at: readString(item, 'created_at'),
updated_at: readString(item, 'updated_at')
}
}
function mapWorkOrderEventToLegacyLog(item: any): any {
return {
id: readString(item, 'id'),
order_id: readFirstString(item, ['task_id', 'order_id']),
from_status: readString(item, 'from_status'),
to_status: readFirstString(item, ['to_status', 'status']),
operator_id: readFirstString(item, ['actor_id', 'operator_id']),
operator_role: readFirstString(item, ['actor_role', 'operator_role']),
remark: readString(item, 'remark'),
created_at: readString(item, 'created_at')
}
}
function mapCareReviewRecordToLegacyReview(item: any): any {
return {
id: readString(item, 'id'),
order_id: readFirstString(item, ['task_id', 'order_id']),
user_id: readFirstString(item, ['created_by', 'user_id']),
rating: readFirstNumber(item, ['rating']),
tags_json: readJsonObjectField(item, ['tags_json']) != null ? readJsonObjectField(item, ['tags_json']) : [] as Array<string>,
content: readFirstString(item, ['content', 'summary', 'remark']),
created_at: readString(item, 'created_at')
}
}
function buildLegacyExecutionRecord(taskId: string, records: Array<any>): ServiceExecutionRecordType | null {
if (records.length == 0) {
return null
}
let checkinRecord: any = null
let serviceRecord: any = null
for (let i = 0; i < records.length; i++) {
const recordType = readFirstString(records[i], ['record_type', 'care_record_type'])
if (recordType == 'review') {
continue
}
if (recordType == 'checkin') {
checkinRecord = records[i]
continue
}
if (serviceRecord == null) {
serviceRecord = records[i]
}
}
const target = serviceRecord != null ? serviceRecord : (checkinRecord != null ? checkinRecord : records[0])
return parseExecutionRecord({
id: readString(target, 'id'),
order_id: taskId,
assignment_id: readFirstString(target, ['assignment_id']),
checkin_time: checkinRecord != null ? readFirstString(checkinRecord, ['checked_in_at', 'checkin_time', 'started_at']) : readFirstString(target, ['checked_in_at', 'checkin_time']),
checkin_latitude: checkinRecord != null ? readFirstNumber(checkinRecord, ['latitude', 'checkin_latitude']) : readFirstNumber(target, ['latitude', 'checkin_latitude']),
checkin_longitude: checkinRecord != null ? readFirstNumber(checkinRecord, ['longitude', 'checkin_longitude']) : readFirstNumber(target, ['longitude', 'checkin_longitude']),
checkin_address: checkinRecord != null ? readFirstString(checkinRecord, ['location_text', 'checkin_address', 'remark']) : readFirstString(target, ['location_text', 'checkin_address']),
service_started_at: readFirstString(target, ['started_at', 'service_started_at']),
service_finished_at: readFirstString(target, ['finished_at', 'service_finished_at']),
actual_duration_minutes: readFirstNumber(target, ['duration_minutes', 'actual_duration_minutes']),
service_items_json: readJsonObjectField(target, ['service_items_json']) != null ? readJsonObjectField(target, ['service_items_json']) : [] as Array<any>,
summary: readFirstString(target, ['summary', 'content']),
remark: readString(target, 'remark'),
track_points_json: readJsonObjectField(target, ['track_points_json']) != null ? readJsonObjectField(target, ['track_points_json']) : [] as Array<any>,
created_at: readString(target, 'created_at'),
updated_at: readString(target, 'updated_at')
})
}
async function getCareTaskDetail(taskId: string): Promise<ServiceOrderType | null> {
if (!isUuidLike(taskId)) {
return null
}
const taskResponse = await supa.from('ec_care_tasks').select('*').eq('id', taskId).limit(1).execute()
if (taskResponse.error != null || taskResponse.data == null) {
return null
}
const taskRows = taskResponse.data as Array<any>
if (taskRows.length == 0) {
return null
}
const eventsResponse = await supa.from('hc_work_order_events').select('*').eq('task_id', taskId).order('created_at', { ascending: false }).execute()
const recordsResponse = await supa.from('ec_care_records').select('*').eq('task_id', taskId).order('created_at', { ascending: false }).execute()
const evidenceResponse = await supa.from('hc_evidence_files').select('*').eq('task_id', taskId).order('created_at', { ascending: false }).execute()
const logs = [] as Array<ServiceOrderTimelineItemType>
if (eventsResponse.data != null) {
const rawEvents = eventsResponse.data as Array<any>
for (let i = 0; i < rawEvents.length; i++) {
logs.push(parseTimeline(mapWorkOrderEventToLegacyLog(rawEvents[i])))
}
}
let review: ServiceReviewType | null = null
const recordRows = recordsResponse.data != null ? recordsResponse.data as Array<any> : [] as Array<any>
for (let i = 0; i < recordRows.length; i++) {
const recordType = readFirstString(recordRows[i], ['record_type', 'care_record_type'])
if (recordType == 'review') {
review = parseReview(mapCareReviewRecordToLegacyReview(recordRows[i]))
break
}
}
const parsed = parseServiceOrder(mapCareTaskRowToLegacyOrderRow(taskRows[0]), logs, review)
parsed.executionRecord = buildLegacyExecutionRecord(taskId, recordRows)
if (evidenceResponse.data != null) {
const rawEvidence = evidenceResponse.data as Array<any>
const evidenceFiles = [] as Array<ServiceEvidenceFileType>
for (let i = 0; i < rawEvidence.length; i++) {
evidenceFiles.push(parseEvidenceFile({
id: readString(rawEvidence[i], 'id'),
order_id: taskId,
execution_record_id: readFirstString(rawEvidence[i], ['care_record_id', 'execution_record_id']),
phase: readString(rawEvidence[i], 'phase'),
file_type: readString(rawEvidence[i], 'file_type'),
storage_path: readString(rawEvidence[i], 'storage_path'),
file_url: readString(rawEvidence[i], 'file_url'),
latitude: readFirstNumber(rawEvidence[i], ['latitude']),
longitude: readFirstNumber(rawEvidence[i], ['longitude']),
captured_at: readString(rawEvidence[i], 'captured_at'),
created_at: readString(rawEvidence[i], 'created_at')
}))
}
parsed.evidenceFiles = evidenceFiles
}
if (parsed.currentStaffId != '') {
const staffResponse = await supa.from('ml_delivery_staff').select('nickname, phone').eq('uid', parsed.currentStaffId).limit(1).execute()
if (staffResponse.data != null) {
const rawStaffList = staffResponse.data as Array<any>
if (rawStaffList.length > 0) {
parsed.staffName = readString(rawStaffList[0], 'nickname')
parsed.staffPhone = readString(rawStaffList[0], 'phone')
}
}
}
parsed.paymentStatus = 2
parsed.dispatchStatus = parsed.currentStaffId != '' ? HOMECARE_DISPATCH_STATUS_ASSIGNED : HOMECARE_DISPATCH_STATUS_PENDING
parsed.dispatchErrorCode = ''
parsed.dispatchErrorMessage = ''
return parsed
}
async function tryCreateCareTask(params: CreateServiceOrderParams): Promise<ServiceOrderType | null> {
// LEGACY/TODO: 本函数为前端直接 INSERT ec_service_requests + ec_care_tasks 的过渡逻辑。
// 按新架构,履约工单应在支付完成后由后端 RPC 生成/激活,前端不得直接创建。
// 当前因缺少后端接口rpc_consumer_create_homecare_task 或支付回调自动创建)暂时保留,
// 但已移除前端直接派单(不再写入 assigned_to + ORDER_ASSIGNED
if (ecServiceRequestCreateUnavailable) {
return null
}
const userId = getCurrentUserId()
if (userId == '') {
return null
}
const requestId = buildUuidLike()
const taskId = buildUuidLike()
const taskNo = buildOrderNo()
const createdAt = nowIso()
const appointmentTime = normalizeAppointmentTime(params.appointmentTime)
let requestResponse = await supa.from('ec_service_requests').insert(
buildEcServiceRequestPayload(params, userId, requestId, createdAt, appointmentTime, true)
).execute()
if (shouldBypassEcServiceRequestCreate(requestResponse.error)) {
ecServiceRequestCreateUnavailable = true
return null
}
if (requestResponse.error != null) {
requestResponse = await supa.from('ec_service_requests').insert(
buildEcServiceRequestPayload(params, userId, requestId, createdAt, appointmentTime, false)
).execute()
}
if (shouldBypassEcServiceRequestCreate(requestResponse.error)) {
ecServiceRequestCreateUnavailable = true
return null
}
if (requestResponse.error != null) {
requestResponse = await supa.from('ec_service_requests').insert(
buildEcServiceRequestPayloadWithoutAddress(params, userId, requestId, createdAt, appointmentTime)
).execute()
}
if (shouldBypassEcServiceRequestCreate(requestResponse.error)) {
ecServiceRequestCreateUnavailable = true
return null
}
if (requestResponse.error != null) {
return null
}
let taskResponse = await supa.from('ec_care_tasks').insert(
buildEcCareTaskPayload(params, userId, requestId, taskId, taskNo, createdAt, appointmentTime, true)
).execute()
if (taskResponse.error != null) {
taskResponse = await supa.from('ec_care_tasks').insert(
buildEcCareTaskPayload(params, userId, requestId, taskId, taskNo, createdAt, appointmentTime, false)
).execute()
}
if (taskResponse.error != null) {
taskResponse = await supa.from('ec_care_tasks').insert(
buildEcCareTaskPayloadWithoutAddress(params, userId, requestId, taskId, taskNo, createdAt, appointmentTime)
).execute()
}
if (taskResponse.error != null) {
return null
}
// 已移除前端直接派单逻辑。派单应由后端调度系统完成。
await insertWorkOrderEvent(taskId, '', 'ORDER_CREATED', userId, 'consumer', 'create_task', '创建服务申请')
return await getCareTaskDetail(taskId)
}
async function getLegacyServiceOrderDetail(orderId: string): Promise<ServiceOrderType | null> {
const orderResponse = await supa.from('hss_service_orders').select('*').eq('id', orderId).limit(1).execute()
if (orderResponse.error != null || orderResponse.data == null) {
return null
}
const rawOrders = orderResponse.data as any[]
if (rawOrders.length == 0) {
return null
}
const logsResponse = await supa.from('hss_service_order_status_logs').select('*').eq('order_id', orderId).order('created_at', { ascending: false }).execute()
const reviewResponse = await supa.from('hss_service_reviews').select('*').eq('order_id', orderId).order('created_at', { ascending: false }).execute()
const executionResponse = await supa.from('hss_service_execution_records').select('*').eq('order_id', orderId).order('created_at', { ascending: false }).limit(1).execute()
const evidenceResponse = await supa.from('hss_service_evidence_files').select('*').eq('order_id', orderId).order('created_at', { ascending: false }).execute()
const logs = [] as Array<ServiceOrderTimelineItemType>
if (logsResponse.data != null) {
const rawLogs = logsResponse.data as any[]
for (let i = 0; i < rawLogs.length; i++) {
logs.push(parseTimeline(rawLogs[i]))
}
}
let review: ServiceReviewType | null = null
if (reviewResponse.data != null) {
const rawReviews = reviewResponse.data as any[]
if (rawReviews.length > 0) {
review = parseReview(rawReviews[0])
}
}
const parsed = parseServiceOrder(rawOrders[0], logs, review)
if (executionResponse.data != null) {
const rawExecutionList = executionResponse.data as any[]
if (rawExecutionList.length > 0) {
parsed.executionRecord = parseExecutionRecord(rawExecutionList[0])
}
}
if (evidenceResponse.data != null) {
const rawEvidenceList = evidenceResponse.data as any[]
const evidenceFiles = [] as Array<ServiceEvidenceFileType>
for (let i = 0; i < rawEvidenceList.length; i++) {
evidenceFiles.push(parseEvidenceFile(rawEvidenceList[i]))
}
parsed.evidenceFiles = evidenceFiles
}
if (parsed.currentStaffId != '') {
const staffResponse = await supa.from('ml_delivery_staff').select('nickname, phone').eq('id', parsed.currentStaffId).limit(1).execute()
if (staffResponse.data != null) {
const rawStaffList = staffResponse.data as any[]
if (rawStaffList.length > 0) {
const staffObj = plainObject(rawStaffList[0])
parsed.staffName = readString(staffObj, 'nickname')
parsed.staffPhone = readString(staffObj, 'phone')
}
}
}
return parsed
}
export function buildAddressSnapshot(address: UserAddress, latitude: number, longitude: number): ServiceOrderAddressSnapshotType {
return {
addressId: address.id,
contactName: address.recipient_name,
contactPhone: address.phone,
province: address.province,
city: address.city,
district: address.district,
detailAddress: address.detail_address,
fullAddress: address.province + address.city + address.district + ' ' + address.detail_address,
latitude,
longitude,
coordinateType: 'gcj02',
remark: address.label ?? ''
}
}
export async function createServiceOrder(params: CreateServiceOrderParams): Promise<ServiceOrderType | null> {
// 当前下单链路分为两层:
// 1) 交易支付层hss_service_orders旧表仍承担套餐价格快照与支付状态
// 2) 履约工单层ec_care_tasks新表应由后端在支付成功后生成/激活)。
// 非套餐单当前走 tryCreateCareTaskLEGACY前端直接写 ec_service_requests + ec_care_tasks
// 待后端提供 rpc_consumer_create_homecare_task 后应统一切到后端创建。
if (params.packageInfo.id == '') {
const newTask = await tryCreateCareTask(params)
if (newTask != null) {
return newTask
}
}
const userId = getCurrentUserId()
if (userId == '') {
return null
}
const orderId = buildId('so')
const orderNo = buildOrderNo()
const now = new Date().toISOString()
const appointmentTime = normalizeAppointmentTime(params.appointmentTime)
const pricingSnapshot = buildPricingSnapshot(params)
const serviceSnapshot = buildServiceSnapshot(params)
const payableAmount = params.packageInfo.price
const response = await supa.from('hss_service_orders').insert({
id: orderId,
order_no: orderNo,
user_id: userId,
service_id: params.service.id,
service_name: params.service.name,
service_snapshot_json: serviceSnapshot,
service_package_id: params.packageInfo.id,
pricing_snapshot_json: pricingSnapshot,
original_amount: params.packageInfo.listPrice > 0 ? params.packageInfo.listPrice : payableAmount,
payable_amount: payableAmount,
total_amount: payableAmount,
service_address_id: normalizeUuidOrNull(params.address.addressId),
address_snapshot_json: params.address as any,
recipient_name: params.recipientName,
recipient_phone: params.recipientPhone,
recipient_age: params.recipientAge,
recipient_gender: params.recipientGender,
contact_name: params.contactName,
contact_phone: params.contactPhone,
appointment_time: appointmentTime,
remark: params.remark,
status: 'created',
payment_status: 1,
created_at: now,
updated_at: now
}).execute()
if (response.error != null) {
console.error('createServiceOrder failed', response.error)
return null
}
await insertLegacyStatusLog(orderId, '', 'created', '创建服务订单')
const staffObj = await getAutoAssignableStaff()
if (staffObj != null) {
const plainStaff = plainObject(staffObj)
const assignmentId = buildId('sa')
await supa.from('hss_service_assignments').insert({
id: assignmentId,
order_id: orderId,
staff_id: readString(plainStaff, 'id'),
station_id: readString(plainStaff, 'station_id') == '' ? null : readString(plainStaff, 'station_id'),
status: 'assigned',
assigned_at: now,
created_at: now,
updated_at: now
}).execute()
await supa.from('hss_service_orders').update({
status: 'assigned',
current_assignment_id: assignmentId,
current_staff_id: readString(plainStaff, 'id'),
updated_at: now
}).eq('id', orderId).execute()
await insertLegacyStatusLog(orderId, 'created', 'assigned', '系统已自动派单')
}
return await getLegacyServiceOrderDetail(orderId)
}
export async function getOrdersByTab(tab: string): Promise<Array<DeliveryOrderType>> {
const staffId = await getCurrentStaffId()
if (staffId == '') {
return [] as Array<DeliveryOrderType>
}
// FIX: assigned_to / current_staff_id 存的是 ml_delivery_staff.id不是 auth.uid
const careTaskResponse = await supa.from('ec_care_tasks').select('*').eq('assigned_to', staffId).order('created_at', { ascending: false }).execute()
if (careTaskResponse.error == null && careTaskResponse.data != null) {
const rawTasks = careTaskResponse.data as Array<any>
const result = [] as Array<DeliveryOrderType>
for (let i = 0; i < rawTasks.length; i++) {
const parsed = await parseCareTaskOrder(readString(rawTasks[i], 'id'), rawTasks[i])
let matched = true
if (tab == 'pending') {
matched = parsed.status == 'pending_assignment'
} else if (tab == 'today') {
matched = parsed.status == 'pending_assignment' || parsed.status == 'accepted' || parsed.status == 'departed' || parsed.status == 'arrived' || parsed.status == 'in_service' || parsed.status == 'pending_acceptance'
} else if (tab == 'history') {
matched = parsed.status == 'pending_acceptance' || parsed.status == 'completed' || parsed.status == 'abnormal' || parsed.status == 'cancelled' || parsed.status == 'rejected'
}
if (tab == 'all' || matched) {
result.push(parsed)
}
}
const validResult = [] as Array<DeliveryOrderType>
for (let i = 0; i < result.length; i++) {
if (hasOrderCoreInfo(result[i])) {
validResult.push(result[i])
}
}
if (validResult.length > 0) {
return validResult
}
}
const response = await supa.from('hss_service_orders').select('*').eq('current_staff_id', staffId).order('created_at', { ascending: false }).execute()
if (response.error != null || response.data == null) {
return [] as Array<DeliveryOrderType>
}
const list = response.data as any[]
const result = [] as Array<ServiceOrderType>
for (let i = 0; i < list.length; i++) {
const parsed = await getLegacyServiceOrderDetail(JSON.parse(JSON.stringify(list[i]))['id'] as string)
if (parsed != null) {
result.push(parsed)
}
}
return result
}
export async function getOrderDetail(orderId: string): Promise<DeliveryOrderType | null> {
const careTaskId = normalizeUuidOrNull(orderId)
if (careTaskId != null) {
const careTaskResponse = await supa.from('ec_care_tasks').select('*').eq('id', careTaskId).limit(1).execute()
if (careTaskResponse.error == null && careTaskResponse.data != null) {
const rows = careTaskResponse.data as Array<any>
if (rows.length > 0) {
return await parseCareTaskOrder(orderId, rows[0])
}
}
}
const response = await supa.from('hss_service_orders').select('*').eq('id', orderId).single().execute()
if (response.error != null || response.data == null) {
return null
}
return await parseLegacyDeliveryOrder(orderId, response.data)
}
async function updateLegacyOrderStatus(orderId: string, nextStatus: ServiceOrderStatus, updateData: UTSJSONObject, remark: string): Promise<DeliveryOrderType | null> {
// LEGACY/TODO: 前端直接 update hss_service_orders.status 已禁止。
// 请使用 rpc_delivery_* 系列 RPC 完成状态流转。
console.warn('[LEGACY] updateLegacyOrderStatus skipped for', orderId, nextStatus, remark)
return await getOrderDetail(orderId)
}
async function updateCareTask(orderId: string, nextStatus: string, updateData: any, action: string, remark: string): Promise<DeliveryOrderType | null> {
// LEGACY/TODO: 前端直接 update ec_care_tasks.status + insert hc_work_order_events 已严格禁止。
// 所有状态流转必须调用后端 rpc_delivery_* RPC。
console.warn('[LEGACY] updateCareTask skipped for', orderId, nextStatus, action, remark)
return await getOrderDetail(orderId)
}
export async function acceptOrder(orderId: string): Promise<DeliveryOrderType | null> {
return await callDeliveryActionRpc('rpc_delivery_accept_order', {
p_order_id: orderId
})
}
export async function rejectOrder(orderId: string, reason: string): Promise<DeliveryOrderType | null> {
return await callDeliveryActionRpc('rpc_delivery_reject_order', {
p_order_id: orderId,
p_reason: reason
})
}
export async function departOrder(orderId: string, location: DeliveryLocationType | null): Promise<DeliveryOrderType | null> {
return await callDeliveryActionRpc('rpc_delivery_start_depart', {
p_order_id: orderId,
p_location: location != null ? {
latitude: location.latitude,
longitude: location.longitude,
address: location.address
} : {} as any
})
}
export async function arriveOrder(orderId: string, location: DeliveryLocationType | null): Promise<DeliveryOrderType | null> {
return await callDeliveryActionRpc('rpc_delivery_arrive_order', {
p_order_id: orderId,
p_location: location != null ? {
latitude: location.latitude,
longitude: location.longitude,
address: location.address
} : {} as any
})
}
export async function checkinOrder(orderId: string, payload: DeliveryCheckinPayloadType): Promise<DeliveryOrderType | null> {
return await callDeliveryActionRpc('rpc_delivery_checkin_order', {
p_order_id: orderId,
p_payload: {
location: payload.location != null ? {
latitude: payload.location.latitude,
longitude: payload.location.longitude,
address: payload.location.address
} : {} as any,
photos: payload.photos != null ? payload.photos : [] as Array<string>,
note: payload.note != null ? payload.note : ''
} as any
})
}
export async function startService(orderId: string): Promise<DeliveryOrderType | null> {
return await callDeliveryActionRpc('rpc_delivery_start_service', {
p_order_id: orderId
})
}
export async function saveServiceRecord(orderId: string, record: DeliveryServiceRecordType): Promise<DeliveryOrderType | null> {
// 先保存服务进度
const progressResult = await callDeliveryActionRpc('rpc_delivery_save_progress', {
p_order_id: orderId,
p_payload: {
items: record.serviceItems != null ? record.serviceItems as any : [] as any,
serviceSummary: record.processNote != null ? record.processNote : '',
progressNote: record.staffRemark != null ? record.staffRemark : ''
} as any
})
if (progressResult == null) {
return null
}
// 再逐张上传证据照片
if (record.photos != null && record.photos.length > 0) {
for (let i = 0; i < record.photos.length; i++) {
await callDeliveryActionRpc('rpc_delivery_upload_evidence', {
p_order_id: orderId,
p_phase: 'service',
p_file_url: record.photos[i],
p_latitude: 0,
p_longitude: 0
})
}
}
return await getOrderDetail(orderId)
}
export async function finishOrder(orderId: string): Promise<DeliveryOrderType | null> {
return await callDeliveryActionRpc('rpc_delivery_finish_service', {
p_order_id: orderId,
p_payload: {} as any
})
}
export async function submitException(orderId: string, payload: DeliveryExceptionPayloadType): Promise<DeliveryOrderType | null> {
return await callDeliveryActionRpc('rpc_delivery_submit_exception', {
p_order_id: orderId,
p_payload: {
exceptionType: payload.exceptionType != null ? payload.exceptionType : 'other',
description: payload.description != null ? payload.description : '',
occurredAt: payload.occurredAt != null && payload.occurredAt != '' ? payload.occurredAt : nowIso(),
locationText: payload.locationText != null ? payload.locationText : '',
images: payload.images != null ? payload.images as any : [] as any,
needPlatformIntervention: payload.needPlatformIntervention == true,
requestCancelOrder: payload.requestCancelOrder == true,
requestReschedule: payload.requestReschedule == true
} as any
})
}
export async function changeOrderStatus(orderId: string, nextStatus: DeliveryOrderStatus, extraRemark: string = ''): Promise<DeliveryOrderType | null> {
if (nextStatus == 'accepted') return await acceptOrder(orderId)
if (nextStatus == 'departed' || nextStatus == 'on_the_way') return await departOrder(orderId, null)
if (nextStatus == 'arrived' || nextStatus == 'checked_in') return await arriveOrder(orderId, null)
if (nextStatus == 'in_service' || nextStatus == 'serving') return await startService(orderId)
if (nextStatus == 'pending_acceptance' || nextStatus == 'pending_confirm' || nextStatus == 'pending_submit' || nextStatus == 'completed') return await finishOrder(orderId)
if (nextStatus == 'rejected') return await rejectOrder(orderId, extraRemark)
if (nextStatus == 'abnormal' || nextStatus == 'exception_pending') {
return await submitException(orderId, {
exceptionType: 'other',
description: extraRemark,
occurredAt: nowIso(),
locationText: '',
images: [] as Array<string>,
needPlatformIntervention: false,
requestCancelOrder: false,
requestReschedule: false
})
}
return await getOrderDetail(orderId)
}
export async function confirmServiceOrder(orderId: string, rating: number, feedback: string, tags: Array<string>): Promise<DeliveryOrderType | null> {
// TODO/GAP: consumer 确认验收暂无后端 RPCec_care_tasks 新链)。
// 当前禁止前端直接 update ec_care_tasks / insert ec_care_records / insert hc_work_order_events。
// 如需启用,请后端补充 rpc_consumer_confirm_acceptance(task_id, rating, feedback, tags)。
console.warn('[GAP] confirmServiceOrder 暂不可用:缺少 consumer 新链确认验收 RPC')
uni.showToast({ title: '验收功能正在升级,请稍后重试', icon: 'none' })
return await getOrderDetail(orderId)
}
export async function rejectServiceOrderAcceptance(orderId: string, feedback: string): Promise<DeliveryOrderType | null> {
// TODO/GAP: consumer 拒绝验收暂无后端 RPCec_care_tasks 新链)。
// 当前禁止前端直接 update ec_care_tasks.status。
// 如需启用,请后端补充 rpc_consumer_reject_acceptance(task_id, feedback)。
console.warn('[GAP] rejectServiceOrderAcceptance 暂不可用:缺少 consumer 新链拒绝验收 RPC')
uni.showToast({ title: '验收功能正在升级,请稍后重试', icon: 'none' })
return await getOrderDetail(orderId)
}