完善服务模块缺少付款页的bug

This commit is contained in:
2026-06-02 11:35:31 +08:00
parent c3324d459a
commit 881262940c
35 changed files with 29069 additions and 557 deletions

View File

@@ -1,3 +1,15 @@
// =============================================================================
// LEGACY/TODO 全局声明:
// 本文件中所有直接 update ec_care_tasks / insert hc_work_order_events /
// insert hc_work_order_exceptions / insert ec_care_records 的逻辑均为旧链路。
// 按新架构,状态流转与事件留痕必须由后端 RPC 统一处理,前端不得直接写表。
// 已改造的函数(如 submitWorkerCheckIn / submitWorkerServiceRecord / submitWorkerException /
// completeWorkerTask已切换为 rpc_delivery_* 或标记为 GAP。
// 尚未改造的 admin 函数submitAdminAssessment / submitAdminServicePlan /
// submitAdminRectification / submitAdminSettlementArchive 等)仍保留旧逻辑,
// 但已增加 TODO 注释,待后端提供对应 RPC 后统一替换。
// =============================================================================
import {
HomeServiceAcceptanceType,
HomeServiceAdminApplicationType,
@@ -6,6 +18,7 @@ import {
HomeServiceCatalogType,
HomeServiceCaseType,
HomeServiceOverviewCardType,
HomeServicePackageType,
HomeServicePlanType,
HomeServiceRectificationType,
HomeServiceSettlementType,
@@ -15,7 +28,9 @@ import {
import {
confirmServiceOrder,
createServiceOrder,
getHomecareOrderDisplayStatus,
getServiceOrderDetail,
HOMECARE_DISPATCH_STATUS_FAILED,
listConsumerServiceOrders,
rejectServiceOrderAcceptance
} from '@/services/serviceOrderService.uts'
@@ -96,6 +111,24 @@ function parseCatalogItem(source: any): HomeServiceCatalogType {
}
}
function parsePackageItem(source: any): HomeServicePackageType {
return {
id: readString(source, 'id'),
serviceId: readString(source, 'service_id'),
packageName: readString(source, 'package_name'),
packageDesc: readString(source, 'package_desc'),
durationMinutes: readNumber(source, 'duration_minutes'),
durationText: readString(source, 'duration_text'),
price: readNumber(source, 'price'),
listPrice: readNumber(source, 'list_price'),
isDefault: readString(source, 'is_default') == 'true' || plainObject(source)['is_default'] === true,
sortNo: readNumber(source, 'sort_no'),
dataSource: readString(source, 'data_source'),
seedBatchNo: readString(source, 'seed_batch_no'),
remark: readString(source, 'remark')
}
}
function createTimeline(title1: string, title2: string, title3: string): Array<HomeServiceTimelineItemType> {
return [
{
@@ -410,6 +443,49 @@ export async function fetchHomeServiceCatalog(): Promise<Array<HomeServiceCatalo
return result
}
export async function fetchHomeServicePackages(serviceId: string): Promise<Array<HomeServicePackageType>> {
if (serviceId == '') {
return [] as Array<HomeServicePackageType>
}
const response = await supa
.from('hss_service_packages')
.select('id, service_id, package_name, package_desc, duration_minutes, duration_text, price, list_price, is_default, sort_no, data_source, seed_batch_no, remark, effective_at, expires_at')
.eq('service_id', serviceId)
.eq('status', 1)
.is('deleted_at', null)
.order('sort_no', { ascending: true })
.execute()
if (response.error != null || response.data == null || !Array.isArray(response.data)) {
return [] as Array<HomeServicePackageType>
}
const result = [] as Array<HomeServicePackageType>
for (let i = 0; i < response.data.length; i++) {
const item = response.data[i]
const effectiveAt = readString(item, 'effective_at')
const expiresAt = readString(item, 'expires_at')
const effectiveMs = effectiveAt != '' ? Date.parse(effectiveAt) : 0
const expiresMs = expiresAt != '' ? Date.parse(expiresAt) : 0
const nowMs = Date.now()
if (effectiveMs > 0 && effectiveMs > nowMs) {
continue
}
if (expiresMs > 0 && expiresMs <= nowMs) {
continue
}
result.push(parsePackageItem(item))
}
result.sort((left: HomeServicePackageType, right: HomeServicePackageType): number => {
if (left.isDefault && !right.isDefault) {
return -1
}
if (!left.isDefault && right.isDefault) {
return 1
}
return left.sortNo - right.sortNo
})
return result
}
export async function fetchConsumerHomeServiceCases(): Promise<Array<HomeServiceCaseType>> {
const orders = await listConsumerServiceOrders()
const result = [] as Array<HomeServiceCaseType>
@@ -436,9 +512,28 @@ export async function createHomeServiceApplication(draft: HomeServiceApplication
break
}
}
if (matchedService != null && draft.serviceAddressSnapshot != null) {
const packages = await fetchHomeServicePackages(draft.serviceId)
let matchedPackage: HomeServicePackageType | null = null
for (let i = 0; i < packages.length; i++) {
if (packages[i].id == draft.selectedPackageId) {
matchedPackage = packages[i]
break
}
}
if (matchedService != null && matchedPackage != null && draft.serviceAddressSnapshot != null) {
const serviceSnapshot: HomeServiceCatalogType = {
id: matchedService.id,
name: matchedService.name,
category: matchedService.category,
price: matchedPackage.price,
durationText: matchedPackage.durationText != '' ? matchedPackage.durationText : matchedService.durationText,
summary: matchedPackage.packageDesc != '' ? matchedPackage.packageDesc : matchedService.summary,
tags: matchedService.tags,
suitableFor: matchedService.suitableFor
}
const createdOrder = await createServiceOrder({
service: matchedService,
service: serviceSnapshot,
packageInfo: matchedPackage,
address: {
addressId: draft.serviceAddressSnapshot.addressId,
contactName: draft.serviceAddressSnapshot.contactName,
@@ -557,12 +652,18 @@ function mapLogsToTimeline(logs: Array<ServiceOrderTimelineItemType>): Array<Hom
}
function mapOrderToCase(order: ServiceOrderType): HomeServiceCaseType {
const displayStatus = getHomecareOrderDisplayStatus(order)
let statusText = displayStatus
let statusTone = getCaseTone(order.status)
if (order.dispatchStatus == HOMECARE_DISPATCH_STATUS_FAILED) {
statusTone = 'danger'
}
return {
id: order.id,
caseNo: order.orderNo,
status: order.status,
statusText: getServiceOrderStatusText(order.status),
statusTone: getCaseTone(order.status),
statusText: statusText,
statusTone: statusTone,
serviceName: order.serviceName,
serviceTime: formatServiceAppointmentText(order.appointmentTime),
applicantName: order.contactName,
@@ -576,6 +677,8 @@ function mapOrderToCase(order: ServiceOrderType): HomeServiceCaseType {
staffName: order.staffName == '' ? '待分配' : order.staffName,
staffPhone: order.staffPhone == '' ? '待分配' : order.staffPhone,
amount: order.serviceSnapshot.price,
paymentStatus: order.paymentStatus,
payExpireAt: order.payExpireAt,
checkinTime: order.executionRecord != null ? order.executionRecord.checkinTime : '',
checkinAddress: order.executionRecord != null ? order.executionRecord.checkinAddress : '',
serviceStartedAt: order.executionRecord != null ? order.executionRecord.serviceStartedAt : order.serviceStartedAt,
@@ -677,42 +780,13 @@ async function isCareTask(taskId: string): Promise<boolean> {
}
async function completeWorkerTask(taskId: string): Promise<HomeServiceTaskType | null> {
const completedAt = nowIso()
if (await isCareTask(taskId)) {
await supa.from('ec_care_tasks').update({
status: 'ACCEPTANCE_PENDING',
service_completed_at: completedAt,
acceptance_pending_at: completedAt,
updated_at: completedAt
}).eq('id', taskId).execute()
await supa.from('hc_work_order_events').insert({
id: buildId('hc-event'),
task_id: taskId,
from_status: 'ORDER_IN_SERVICE',
to_status: 'ACCEPTANCE_PENDING',
actor_id: getCurrentUserId(),
actor_role: 'merchant',
action: 'finish_service',
remark: '服务记录和凭证已经提交。',
created_at: completedAt
}).execute()
} else {
await supa.from('hss_service_orders').update({
status: 'pending_acceptance',
completed_at: completedAt,
pending_acceptance_at: completedAt,
updated_at: completedAt
}).eq('id', taskId).execute()
await supa.from('hss_service_order_status_logs').insert({
id: buildId('slog'),
order_id: taskId,
from_status: 'in_service',
to_status: 'pending_acceptance',
operator_id: getCurrentUserId(),
operator_role: 'merchant',
remark: '服务记录和凭证已经提交。',
created_at: completedAt
}).execute()
// LEGACY/TODO: 已切换为调用 rpc_delivery_finish_service。
const { data, error } = await supa.rpc('rpc_delivery_finish_service', {
p_order_id: taskId,
p_payload: {} as any
} as any)
if (error != null) {
console.error('[homeServiceService] finish_service rpc failed:', error)
}
return await fetchWorkerTaskDetail(taskId)
}
@@ -876,161 +950,60 @@ export async function advanceWorkerTask(taskId: string): Promise<HomeServiceTask
}
export async function submitWorkerCheckIn(taskId: string, note: string): Promise<HomeServiceTaskType | null> {
const checkedInAt = nowIso()
if (await isCareTask(taskId)) {
const recordId = buildId('care-checkin')
await supa.from('ec_care_records').insert({
id: recordId,
task_id: taskId,
record_type: 'checkin',
started_at: checkedInAt,
checked_in_at: checkedInAt,
location_text: note,
remark: note,
created_at: checkedInAt,
updated_at: checkedInAt
}).execute()
await supa.from('ec_care_tasks').update({
status: 'ORDER_IN_SERVICE',
checked_in_at: checkedInAt,
service_started_at: checkedInAt,
updated_at: checkedInAt
}).eq('id', taskId).execute()
await supa.from('hc_work_order_events').insert({
id: buildId('hc-event'),
task_id: taskId,
from_status: 'ORDER_ACCEPTED',
to_status: 'ORDER_IN_SERVICE',
actor_id: getCurrentUserId(),
actor_role: 'merchant',
action: 'checkin_task',
remark: note == '' ? '已完成签到,开始执行服务。' : note,
created_at: checkedInAt
}).execute()
} else {
const recordId = buildId('ser')
await supa.from('hss_service_execution_records').upsert({
id: recordId,
order_id: taskId,
assignment_id: '',
checkin_time: checkedInAt,
checkin_address: note,
service_started_at: checkedInAt,
remark: note,
created_at: checkedInAt,
updated_at: checkedInAt
}).execute()
await supa.from('hss_service_orders').update({
status: 'in_service',
arrived_at: checkedInAt,
service_started_at: checkedInAt,
updated_at: checkedInAt
}).eq('id', taskId).execute()
await supa.from('hss_service_order_status_logs').insert({
id: buildId('slog'),
order_id: taskId,
from_status: 'accepted',
to_status: 'in_service',
operator_id: getCurrentUserId(),
operator_role: 'merchant',
remark: note == '' ? '已完成签到,开始执行服务。' : note,
created_at: checkedInAt
}).execute()
// LEGACY/TODO: 已切换为调用 rpc_delivery_checkin_order + rpc_delivery_start_service。
const checkinResult = await supa.rpc('rpc_delivery_checkin_order', {
p_order_id: taskId,
p_payload: {
location: {} as any,
photos: [] as Array<string>,
note: note != null ? note : ''
} as any
} as any)
if (checkinResult.error != null) {
console.error('[homeServiceService] checkin rpc failed:', checkinResult.error)
}
const startResult = await supa.rpc('rpc_delivery_start_service', {
p_order_id: taskId
} as any)
if (startResult.error != null) {
console.error('[homeServiceService] start_service rpc failed:', startResult.error)
}
return await fetchWorkerTaskDetail(taskId)
}
export async function submitWorkerServiceRecord(taskId: string, summary: string): Promise<HomeServiceTaskType | null> {
const detail = await getServiceOrderDetail(taskId)
if (detail == null) {
return null
}
const now = nowIso()
const recordId = detail.executionRecord != null && detail.executionRecord.id != '' ? detail.executionRecord.id : buildId('worker-rec')
if (await isCareTask(taskId)) {
await supa.from('ec_care_records').upsert({
id: recordId,
task_id: taskId,
record_type: 'service',
started_at: detail.executionRecord != null ? detail.executionRecord.serviceStartedAt : detail.serviceStartedAt,
summary: summary,
remark: summary,
created_at: detail.executionRecord != null && detail.executionRecord.createdAt != '' ? detail.executionRecord.createdAt : now,
updated_at: now
}).execute()
await supa.from('ec_care_tasks').update({
status: 'ORDER_IN_SERVICE',
updated_at: now
}).eq('id', taskId).execute()
await supa.from('hc_work_order_events').insert({
id: buildId('hc-event'),
task_id: taskId,
from_status: 'ORDER_IN_SERVICE',
to_status: 'ORDER_IN_SERVICE',
actor_id: getCurrentUserId(),
actor_role: 'merchant',
action: 'save_record',
remark: summary == '' ? '已保存服务记录。' : summary,
created_at: now
}).execute()
} else {
await supa.from('hss_service_execution_records').upsert({
id: recordId,
order_id: taskId,
assignment_id: detail.currentAssignmentId,
service_started_at: detail.executionRecord != null ? detail.executionRecord.serviceStartedAt : detail.serviceStartedAt,
summary: summary,
remark: summary,
created_at: detail.executionRecord != null && detail.executionRecord.createdAt != '' ? detail.executionRecord.createdAt : now,
updated_at: now
}).execute()
// LEGACY/TODO: 已切换为调用 rpc_delivery_save_progress。
const { data, error } = await supa.rpc('rpc_delivery_save_progress', {
p_order_id: taskId,
p_payload: {
items: [] as any,
serviceSummary: summary != null ? summary : '',
progressNote: summary != null ? summary : ''
} as any
} as any)
if (error != null) {
console.error('[homeServiceService] save_progress rpc failed:', error)
}
return await fetchWorkerTaskDetail(taskId)
}
export async function submitWorkerException(taskId: string, exceptionType: string, description: string): Promise<HomeServiceTaskType | null> {
const now = nowIso()
if (await isCareTask(taskId)) {
await supa.from('hc_work_order_exceptions').insert({
id: buildId('hc-ex'),
task_id: taskId,
exception_type: exceptionType,
description: description,
occurred_at: now,
created_by: getCurrentUserId(),
created_at: now,
updated_at: now
}).execute()
await supa.from('ec_care_tasks').update({
status: 'ORDER_EXCEPTION',
updated_at: now
}).eq('id', taskId).execute()
await supa.from('hc_work_order_events').insert({
id: buildId('hc-event'),
task_id: taskId,
from_status: '',
to_status: 'ORDER_EXCEPTION',
actor_id: getCurrentUserId(),
actor_role: 'merchant',
action: 'report_exception',
remark: exceptionType + '' + description,
created_at: now
}).execute()
} else {
await supa.from('hss_service_orders').update({
status: 'exception',
updated_at: now
}).eq('id', taskId).execute()
await supa.from('hss_service_order_status_logs').insert({
id: buildId('slog'),
order_id: taskId,
from_status: 'in_service',
to_status: 'exception',
operator_id: getCurrentUserId(),
operator_role: 'merchant',
remark: exceptionType + '' + description,
created_at: now
}).execute()
// LEGACY/TODO: 已切换为调用 rpc_delivery_submit_exception。
const { data, error } = await supa.rpc('rpc_delivery_submit_exception', {
p_order_id: taskId,
p_payload: {
exceptionType: exceptionType != null ? exceptionType : 'other',
description: description != null ? description : '',
occurredAt: nowIso(),
locationText: '',
images: [] as any,
needPlatformIntervention: false,
requestCancelOrder: false,
requestReschedule: false
} as any
} as any)
if (error != null) {
console.error('[homeServiceService] submit_exception rpc failed:', error)
}
return await fetchWorkerTaskDetail(taskId)
}
@@ -1056,30 +1029,10 @@ export async function submitAdminAssessment(caseId: string, riskLevel: string, c
requirementTags: order.serviceSnapshot.tags,
updatedAt: nowIso()
}
if (await isCareTask(caseId)) {
await supa.from('hc_work_order_events').insert({
id: buildId('hc-event'),
task_id: caseId,
from_status: order.status,
to_status: order.status,
actor_id: getCurrentUserId(),
actor_role: 'admin',
action: 'submit_assessment',
remark: encodeAdminRemark(ADMIN_ASSESSMENT_PREFIX, payload),
created_at: nowIso()
}).execute()
} else {
await supa.from('hss_service_order_status_logs').insert({
id: buildId('slog'),
order_id: caseId,
from_status: order.status,
to_status: order.status,
operator_id: getCurrentUserId(),
operator_role: 'admin',
remark: encodeAdminRemark(ADMIN_ASSESSMENT_PREFIX, payload),
created_at: nowIso()
}).execute()
}
// TODO/GAP: admin 提交评估暂无后端 RPCec_care_tasks 新链)。
// 禁止前端直接 insert hc_work_order_events 作为评估留痕。
// 如需启用,请后端补充 rpc_admin_submit_assessment(case_id, payload)。
console.warn('[GAP] submitAdminAssessment 暂不可用(新链):缺少 admin 评估提交 RPC')
return buildAssessmentDetail(order, payload)
}
@@ -1112,30 +1065,10 @@ export async function submitAdminServicePlan(
planSummary,
updatedAt: nowIso()
}
if (await isCareTask(caseId)) {
await supa.from('hc_work_order_events').insert({
id: buildId('hc-event'),
task_id: caseId,
from_status: order.status,
to_status: order.status,
actor_id: getCurrentUserId(),
actor_role: 'admin',
action: 'submit_plan',
remark: encodeAdminRemark(ADMIN_PLAN_PREFIX, payload),
created_at: nowIso()
}).execute()
} else {
await supa.from('hss_service_order_status_logs').insert({
id: buildId('slog'),
order_id: caseId,
from_status: order.status,
to_status: order.status,
operator_id: getCurrentUserId(),
operator_role: 'admin',
remark: encodeAdminRemark(ADMIN_PLAN_PREFIX, payload),
created_at: nowIso()
}).execute()
}
// TODO/GAP: admin 提交服务计划暂无后端 RPCec_care_tasks 新链)。
// 禁止前端直接 insert hc_work_order_events 作为计划留痕。
// 如需启用,请后端补充 rpc_admin_submit_service_plan(case_id, payload)。
console.warn('[GAP] submitAdminServicePlan 暂不可用(新链):缺少 admin 服务计划提交 RPC')
return buildPlanDetail(order, payload)
}
@@ -1200,42 +1133,10 @@ export async function submitAdminRectification(caseId: string, issueSummary: str
status: 'closed',
updatedAt: nowIso()
}
if (await isCareTask(caseId)) {
const reopenedAt = nowIso()
await supa.from('ec_care_tasks').update({
status: 'ACCEPTANCE_PENDING',
acceptance_pending_at: reopenedAt,
updated_at: reopenedAt
}).eq('id', caseId).execute()
await supa.from('hc_work_order_events').insert({
id: buildId('hc-event'),
task_id: caseId,
from_status: 'ACCEPTANCE_REJECTED',
to_status: 'ACCEPTANCE_PENDING',
actor_id: getCurrentUserId(),
actor_role: 'admin',
action: 'submit_rectification',
remark: encodeAdminRemark(ADMIN_RECTIFICATION_PREFIX, payload),
created_at: reopenedAt
}).execute()
} else {
const reopenedAt = nowIso()
await supa.from('hss_service_orders').update({
status: 'pending_acceptance',
pending_acceptance_at: reopenedAt,
updated_at: reopenedAt
}).eq('id', caseId).execute()
await supa.from('hss_service_order_status_logs').insert({
id: buildId('slog'),
order_id: caseId,
from_status: order.status,
to_status: 'pending_acceptance',
operator_id: getCurrentUserId(),
operator_role: 'admin',
remark: encodeAdminRemark(ADMIN_RECTIFICATION_PREFIX, payload),
created_at: reopenedAt
}).execute()
}
// TODO/GAP: admin 提交整改暂无后端 RPCec_care_tasks 新链)。
// 禁止前端直接 update ec_care_tasks / insert hc_work_order_events。
// 如需启用,请后端补充 rpc_admin_submit_rectification(case_id, payload)。
console.warn('[GAP] submitAdminRectification 暂不可用(新链):缺少 admin 整改提交 RPC')
const latest = await getServiceOrderDetail(caseId)
if (latest == null) {
return buildRectificationDetail(order, payload, issueSummary)

View File

@@ -2,7 +2,7 @@ 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 } from '@/types/home-service.uts'
import type { HomeServiceCatalogType, HomeServicePackageType } from '@/types/home-service.uts'
import type { DeliveryServiceRecordType } from '@/types/delivery.uts'
import {
getServiceOrderStatusText,
@@ -16,8 +16,28 @@ import {
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
@@ -29,6 +49,42 @@ export type CreateServiceOrderParams = {
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 {
@@ -89,8 +145,15 @@ function normalizeAppointmentTime(value: string): string | null {
if (text == '') {
return null
}
if (/^\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}(\s*-\s*\d{2}:\d{2})?$/.test(text)) {
return text
const fullRangeMatch = text.match(/^(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2})(\s*-\s*\d{2}:\d{2})?$/)
if (fullRangeMatch != null) {
const datePart = fullRangeMatch[1] ?? ''
const startTime = fullRangeMatch[2] ?? ''
const parsed = Date.parse(datePart + 'T' + startTime + ':00')
if (!isNaN(parsed)) {
return new Date(parsed).toISOString()
}
return datePart + ' ' + startTime
}
if (/^\d{4}-\d{2}-\d{2}(\s+(上午|下午|晚上))$/.test(text)) {
return text
@@ -119,9 +182,12 @@ function normalizeAppointmentTime(value: string): string | null {
const rangeMatch = text.match(/(\d{2}:\d{2})(\s*-\s*\d{2}:\d{2})?/)
if (rangeMatch != null) {
const startTime = rangeMatch[1] ?? ''
const endRange = rangeMatch[2] ?? ''
if (startTime != '') {
return year + '-' + month + '-' + day + ' ' + startTime + endRange.replace(/\s+/g, '')
const parsed = Date.parse(year + '-' + month + '-' + day + 'T' + startTime + ':00')
if (!isNaN(parsed)) {
return new Date(parsed).toISOString()
}
return year + '-' + month + '-' + day + ' ' + startTime
}
}
const tailText = text.substring(text.indexOf(month + '/' + day) + 5).trim()
@@ -223,7 +289,7 @@ function shouldBypassEcServiceRequestCreate(error: any): boolean {
|| hasMissingColumnError(error, 'contact_phone')
}
function shouldUseCareTaskPath(orderId: string): boolean {
export function shouldUseCareTaskPath(orderId: string): boolean {
return isUuidLike(orderId)
}
@@ -327,7 +393,7 @@ function buildEcCareTaskPayload(params: CreateServiceOrderParams, userId: string
service_catalog_id: params.service.id,
service_name: params.service.name,
service_category: params.service.category,
service_snapshot_json: params.service as any,
service_snapshot_json: buildServiceSnapshot(params),
elder_name: params.recipientName,
elder_phone: params.recipientPhone,
elder_age: params.recipientAge,
@@ -357,7 +423,7 @@ function buildEcCareTaskPayloadWithoutAddress(params: CreateServiceOrderParams,
service_catalog_id: params.service.id,
service_name: params.service.name,
service_category: params.service.category,
service_snapshot_json: params.service as any,
service_snapshot_json: buildServiceSnapshot(params),
elder_name: params.recipientName,
elder_phone: params.recipientPhone,
elder_age: params.recipientAge,
@@ -479,6 +545,8 @@ function parseServiceOrder(item: any, logs: Array<ServiceOrderTimelineItemType>,
appointmentTime: readString(item, 'appointment_time'),
remark: readString(item, 'remark'),
status: normalizeServiceOrderStatus(readString(item, 'status')),
paymentStatus: readNumber(item, 'payment_status'),
payExpireAt: readString(item, 'pay_expire_at'),
currentAssignmentId: readString(item, 'current_assignment_id'),
currentStaffId: readString(item, 'current_staff_id'),
acceptedAt: readString(item, 'accepted_at'),
@@ -496,35 +564,23 @@ function parseServiceOrder(item: any, logs: Array<ServiceOrderTimelineItemType>,
logs,
executionRecord: null,
evidenceFiles: [] as Array<any>,
review
review,
dispatchStatus: readString(item, 'dispatch_status'),
dispatchErrorCode: readString(item, 'dispatch_error_code'),
dispatchErrorMessage: readString(item, 'dispatch_error_message')
}
}
async function insertLegacyStatusLog(orderId: string, fromStatus: string, toStatus: ServiceOrderStatus, operatorId: string, operatorRole: string, remark: string): Promise<void> {
await supa.from('hss_service_order_status_logs').insert({
id: buildId('slog'),
order_id: orderId,
from_status: fromStatus,
to_status: toStatus,
operator_id: operatorId == '' ? null : operatorId,
operator_role: operatorRole,
remark,
created_at: new Date().toISOString()
}).execute()
// LEGACY/TODO: 旧链路直接写 hss_service_order_status_logs 已停用。
// 状态事件应由后端 RPC 统一写入 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, actorId: string, actorRole: string, action: string, remark: string): Promise<void> {
await supa.from('hc_work_order_events').insert({
id: buildId('hc-event'),
task_id: taskId,
from_status: fromStatus == '' ? null : fromStatus,
to_status: toStatus,
actor_id: actorId == '' ? null : actorId,
actor_role: actorRole,
action,
remark,
created_at: nowIso()
}).execute()
// LEGACY/TODO: 前端直接 insert hc_work_order_events 已严格禁止。
// 状态事件必须由后端 RPC 统一写入。
console.warn('[LEGACY] insertWorkOrderEvent skipped for', taskId, fromStatus, toStatus, action, remark)
}
function readFirstString(source: any, keys: Array<string>): string {
@@ -748,10 +804,18 @@ async function getCareTaskDetail(taskId: string): Promise<ServiceOrderType | nul
}
}
}
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
}
@@ -808,19 +872,8 @@ async function tryCreateCareTask(params: CreateServiceOrderParams): Promise<Serv
if (taskResponse.error != null) {
return null
}
// 已移除前端直接派单逻辑。派单应由后端调度系统完成。
await insertWorkOrderEvent(taskId, '', 'ORDER_CREATED', userId, 'consumer', 'create_task', '创建服务申请')
const assignedStaff = await getAutoAssignableStaff()
if (assignedStaff != null) {
const assignedUserId = readString(assignedStaff, 'uid')
if (assignedUserId != '') {
await supa.from('ec_care_tasks').update({
status: 'ORDER_ASSIGNED',
assigned_to: assignedUserId,
updated_at: createdAt
}).eq('id', taskId).execute()
await insertWorkOrderEvent(taskId, 'ORDER_CREATED', 'ORDER_ASSIGNED', userId, 'system', 'assign_task', '系统已自动派单')
}
}
return await getCareTaskDetail(taskId)
}
@@ -897,10 +950,190 @@ export function buildAddressSnapshot(address: UserAddress, latitude: number, lon
}
}
export function getHomecareOrderDisplayStatus(order: ServiceOrderType): string {
if (order.paymentStatus == 1 && order.status == 'created') {
return '待付款'
}
if (order.paymentStatus == 2 && order.dispatchStatus == HOMECARE_DISPATCH_STATUS_PENDING) {
return '待派单'
}
if (order.paymentStatus == 2 && order.dispatchStatus == HOMECARE_DISPATCH_STATUS_DISPATCHING) {
return '正在派单'
}
if (order.paymentStatus == 2 && order.dispatchStatus == HOMECARE_DISPATCH_STATUS_FAILED) {
return '派单未成功'
}
if (order.paymentStatus == 2 && order.dispatchStatus == HOMECARE_DISPATCH_STATUS_ASSIGNED) {
return '已派单'
}
return getServiceOrderStatusText(order.status)
}
export async function dispatchPaidHomecareOrder(orderId: string): Promise<HomecareDispatchResult> {
if (orderId == null || orderId.trim() == '') {
return {
success: false,
code: 'ORDER_ID_REQUIRED',
message: '订单信息异常,请返回后重试',
display_type: 'modal',
retryable: false
}
}
// LEGACY/TODO: rpc_homecare_auto_dispatch 当前只操作 hss_service_orders旧交易链
// 对于 ec_care_tasks 新链UUID 格式订单 ID不要调用旧 RPC避免错误回写旧表。
if (shouldUseCareTaskPath(orderId)) {
return {
success: true,
code: 'SYNC_IN_PROGRESS',
message: '付款成功,服务安排信息正在同步中,请稍后在我的服务中查看。',
display_type: 'toast',
retryable: false,
dispatch_status: HOMECARE_DISPATCH_STATUS_PENDING,
order_id: orderId
}
}
const { data, error } = await supa.rpc('rpc_homecare_auto_dispatch', {
p_order_id: orderId
} as any)
if (error != null) {
console.error('[homecare-dispatch] rpc failed:', error)
return {
success: false,
code: 'RPC_EXECUTION_FAILED',
message: '派单服务暂时异常,请稍后重试',
display_type: 'modal',
retryable: true,
dispatch_status: 'failed',
order_id: orderId
}
}
if (data == null) {
return {
success: false,
code: 'RPC_EMPTY_RESULT',
message: '未获取到派单结果,请稍后重试',
display_type: 'modal',
retryable: true,
dispatch_status: 'failed',
order_id: orderId
}
}
const result = plainObject(data)
const successValue = result['success']
const isSuccess = successValue === true || successValue === 'true' || (typeof successValue === 'boolean' && successValue)
const code = readString(result, 'code')
const message = readString(result, 'message')
const displayType = readString(result, 'display_type')
const retryable = result['retryable'] === true || result['retryable'] === 'true'
const dispatchStatus = readString(result, 'dispatch_status')
if (isSuccess || code == 'DISPATCH_ASSIGNED' || code == 'ALREADY_ASSIGNED' || code == 'ALREADY_ASSIGNED_RECOVERED') {
return {
success: true,
code: code != '' ? code : 'DISPATCH_ASSIGNED',
message: message != '' ? message : '系统已为您匹配服务人员',
display_type: displayType != '' ? displayType : 'none',
retryable: false,
dispatch_status: dispatchStatus != '' ? dispatchStatus : 'assigned',
order_id: readString(result, 'order_id'),
assignment_id: readString(result, 'assignment_id'),
staff_id: readString(result, 'staff_id'),
station_id: readString(result, 'station_id'),
dispatch_distance_km: readNumber(result, 'dispatch_distance_km')
}
}
return {
success: false,
code: code != '' ? code : 'DISPATCH_FAILED',
message: message != '' ? message : '派单失败,请稍后重试',
display_type: displayType != '' ? displayType : 'modal',
retryable: retryable,
dispatch_status: dispatchStatus != '' ? dispatchStatus : 'failed',
order_id: readString(result, 'order_id')
}
}
export function showHomecareDispatchFailureModal(orderId: string, result: HomecareDispatchResult, retryCallback: (id: string) => void): void {
const code = result.code
let title = '操作失败'
let allowRetry = false
if (code == 'ORDER_ID_REQUIRED') {
title = '操作失败'
allowRetry = false
} else if (code == 'UNAUTHENTICATED') {
title = '请重新登录'
allowRetry = false
} else if (code == 'USER_PROFILE_NOT_FOUND') {
title = '账户异常'
allowRetry = false
} else if (code == 'ORDER_NOT_FOUND') {
title = '订单异常'
allowRetry = false
} else if (code == 'ORDER_ACCESS_DENIED') {
title = '无权操作'
allowRetry = false
} else if (code == 'ORDER_NOT_PAID') {
title = '暂不能派单'
allowRetry = false
} else if (code == 'ORDER_STATUS_NOT_DISPATCHABLE') {
title = '暂不能派单'
allowRetry = false
} else if (code == 'NO_ONLINE_STAFF' || code == 'NO_STAFF_IN_SERVICE_STATION' || code == 'NO_QUALIFIED_STAFF' || code == 'NO_NEARBY_STAFF' || code == 'ALL_ELIGIBLE_STAFF_BUSY') {
title = '暂未匹配成功'
allowRetry = true
} else if (code == 'DISPATCH_CONFLICT_RETRY') {
title = '请重新尝试'
allowRetry = true
} else if (code == 'RPC_EXECUTION_FAILED' || code == 'RPC_EMPTY_RESULT') {
title = '派单服务异常'
allowRetry = true
} else {
title = '操作失败'
allowRetry = result.retryable
}
if (!allowRetry) {
uni.showModal({
title: title,
content: result.message,
showCancel: false,
confirmText: '我知道了'
})
return
}
uni.showModal({
title: title,
content: result.message,
showCancel: true,
cancelText: '稍后再试',
confirmText: '重新派单',
success: (res) => {
if (res.confirm) {
retryCallback(orderId)
}
}
})
}
export async function createServiceOrder(params: CreateServiceOrderParams): Promise<ServiceOrderType | null> {
const newTask = await tryCreateCareTask(params)
if (newTask != null) {
return newTask
// 当前下单链路分为两层:
// 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 == '') {
@@ -910,13 +1143,21 @@ export async function createServiceOrder(params: CreateServiceOrderParams): Prom
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: params.service as any,
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,
@@ -928,6 +1169,7 @@ export async function createServiceOrder(params: CreateServiceOrderParams): Prom
appointment_time: appointmentTime,
remark: params.remark,
status: 'created',
payment_status: 1,
created_at: now,
updated_at: now
}).execute()
@@ -936,30 +1178,8 @@ export async function createServiceOrder(params: CreateServiceOrderParams): Prom
return null
}
await insertLegacyStatusLog(orderId, '', 'created', userId, 'consumer', '创建服务订单')
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', userId, 'system', '系统已自动派单')
}
return await getLegacyServiceOrderDetail(orderId)
}
}
export async function listConsumerServiceOrders(): Promise<Array<ServiceOrderType>> {
const userId = getCurrentUserId()
@@ -1010,40 +1230,11 @@ export async function saveServiceRecord(orderId: string, record: DeliveryService
}
const careTask = shouldUseCareTaskPath(orderId) ? await getCareTaskDetail(orderId) : null
if (careTask != null) {
const savedAt = nowIso()
const serviceContent = record.serviceContent.length > 0 ? record.serviceContent : record.serviceItems.map(item => item.name)
const serviceSummary = record.serviceSummary != '' ? record.serviceSummary : record.processNote
const healthMetrics = record.healthMetrics as any
const familyConfirmation = record.familyConfirmation as any
const insertResponse = await supa.from('ec_care_records').insert({
id: buildId('care-record'),
task_id: orderId,
record_type: 'service_record',
created_by: userId,
service_items_json: record.serviceItems as any,
service_content_json: serviceContent as any,
service_summary: serviceSummary,
process_note: record.processNote,
elder_status: record.elderStatus,
health_metrics_json: healthMetrics,
materials_used: record.materialsUsed,
abnormal_note: record.abnormalNote,
photos_json: record.photos as any,
staff_remark: record.staffRemark,
family_confirmation_json: familyConfirmation,
created_at: savedAt,
updated_at: savedAt
}).execute()
if (insertResponse.error != null) {
return null
}
const updateResponse = await supa.from('ec_care_tasks').update({
updated_at: savedAt
}).eq('id', orderId).execute()
if (updateResponse.error != null) {
return null
}
await insertWorkOrderEvent(orderId, careTask.status, careTask.status, userId, 'staff', 'save_service_record', serviceSummary == '' ? '服务记录已保存' : serviceSummary)
// TODO/GAP: consumer 端保存服务记录ec_care_tasks 新链)暂无后端 RPC。
// 禁止前端直接 insert ec_care_records / update ec_care_tasks。
// 如需启用,请后端补充 rpc_consumer_save_service_record(task_id, record)。
console.warn('[GAP] saveServiceRecord 暂不可用(新链):缺少 consumer 保存服务记录 RPC')
uni.showToast({ title: '服务记录功能正在升级,请稍后重试', icon: 'none' })
return await getCareTaskDetail(orderId)
}
const current = await getLegacyServiceOrderDetail(orderId)
@@ -1083,31 +1274,14 @@ export async function confirmServiceOrder(orderId: string, rating: number, conte
}
const careTask = shouldUseCareTaskPath(orderId) ? await getCareTaskDetail(orderId) : null
if (careTask != null) {
const acceptedAt = nowIso()
const updateResponse = await supa.from('ec_care_tasks').update({
status: 'ACCEPTED',
accepted_by_family_at: acceptedAt,
updated_at: acceptedAt
}).eq('id', orderId).execute()
if (updateResponse.error == null) {
await insertWorkOrderEvent(orderId, 'ACCEPTANCE_PENDING', 'ACCEPTED', userId, 'consumer', 'accept_task', '用户确认验收')
if (rating > 0 || content != '' || tags.length > 0) {
await supa.from('ec_care_records').insert({
id: buildId('care-review'),
task_id: orderId,
record_type: 'review',
created_by: userId,
rating,
tags_json: tags as any,
content,
created_at: acceptedAt,
updated_at: acceptedAt
}).execute()
await insertWorkOrderEvent(orderId, 'ACCEPTED', 'ACCEPTED', userId, 'consumer', 'submit_review', '用户提交评价')
}
return await getCareTaskDetail(orderId)
}
// TODO/GAP: consumer 确认验收ec_care_tasks 新链)暂无后端 RPC。
// 禁止前端直接 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 getCareTaskDetail(orderId)
}
// LEGACY: 以下仍为旧 hss_service_orders 链路,仅用于兼容已有交易订单。
const current = await getServiceOrderDetail(orderId)
if (current == null) {
return null
@@ -1147,31 +1321,14 @@ export async function rejectServiceOrderAcceptance(orderId: string, content: str
}
const careTask = shouldUseCareTaskPath(orderId) ? await getCareTaskDetail(orderId) : null
if (careTask != null) {
const rejectedAt = nowIso()
const updateResponse = await supa.from('ec_care_tasks').update({
status: 'ACCEPTANCE_REJECTED',
updated_at: rejectedAt
}).eq('id', orderId).execute()
if (updateResponse.error == null) {
await supa.from('hc_work_order_exceptions').insert({
id: buildId('hc-ex'),
task_id: orderId,
exception_type: 'acceptance_rejected',
description: content == '' ? '用户退回整改' : content,
occurred_at: rejectedAt,
location_text: '',
images_json: [] as Array<string>,
need_platform_intervention: false,
request_cancel_order: false,
request_reschedule: false,
created_by: userId,
created_at: rejectedAt,
updated_at: rejectedAt
}).execute()
await insertWorkOrderEvent(orderId, 'ACCEPTANCE_PENDING', 'ACCEPTANCE_REJECTED', userId, 'consumer', 'reject_acceptance', content == '' ? '用户退回整改' : content)
return await getCareTaskDetail(orderId)
}
// TODO/GAP: consumer 拒绝验收ec_care_tasks 新链)暂无后端 RPC。
// 禁止前端直接 update ec_care_tasks / insert hc_work_order_exceptions。
// 如需启用,请后端补充 rpc_consumer_reject_acceptance(task_id, feedback)。
console.warn('[GAP] rejectServiceOrderAcceptance 暂不可用(新链):缺少 consumer 拒绝验收 RPC')
uni.showToast({ title: '验收功能正在升级,请稍后重试', icon: 'none' })
return await getCareTaskDetail(orderId)
}
// LEGACY: 以下仍为旧 hss_service_orders 链路。
const current = await getServiceOrderDetail(orderId)
if (current == null) {
return null