yeg
2026-09-18 70ea0911ce7519ca802266078624e9fed3559a2c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
import { getApiBaseUrl, getApiEndpoint, isApiConfigured } from '../config/api.uts'
import { getAccessToken } from './session.uts'
import { request, HttpError } from './http.uts'
 
export const createIdempotencyKey = (scope: string): string => scope + '-' + Date.now() + '-' + Math.floor(Math.random() * 1000000000)
 
export const sanitizeRichText = (html: string): string => {
    if (html == null) return ''
    const base = getApiBaseUrl().replace(/\/+$/, '')
    const content = ('' + html)
        .replace(/<script[\s\S]*?<\/script>/gi, '')
        .replace(/<style[\s\S]*?<\/style>/gi, '')
        .replace(/\son[a-z]+\s*=\s*(['"]).*?\1/gi, '')
        // 危险协议(javascript: 等)直接去掉属性。
        .replace(/\s(href|src)\s*=\s*(['"])\s*javascript:[^'"]*\2/gi, '')
    if (base == '') return content
    // 后台富文本里的图片/链接存的是相对路径(/uploads/xxx.png)。小程序加载不了相对路径,
    // 而原来这里会把所有非 https 的 src 整条删掉,图片就变成破图(只显示 alt 文字)。
    // 这里统一补成接口域名下的绝对地址,http(s) 绝对地址原样保留。
    return content
        .replace(/(src|href)=(["'])\/\//gi, '$1=$2https://')
        .replace(/(\s(?:src|href)\s*=\s*(['"]))\/+/gi, '$1' + base + '/')
        .replace(/(\s(?:src|href)\s*=\s*(['"]))(?!https?:|\/\/|\/|data:|javascript:)/gi, '$1' + base + '/')
        // 小程序 rich-text 不继承页面样式:后台图片带的是原图尺寸,会撑出卡片。
        // 在图片标签上补内联样式,按容器宽度等比缩放并居中。
        .replace(/(<img\b[^>]*?\sstyle=(["']))([^"']*)(\2)/gi, '$1$3max-width:100%;height:auto;display:block;margin:10px auto;$4')
        .replace(/<img\b(?![^>]*\sstyle=)/gi, '<img style="max-width:100%;height:auto;display:block;margin:10px auto;"')
}
 
export const requestDynamicCode = (codeType: string, businessId: string, onSuccess: (data: any) => void, onError: (error: HttpError) => void) => {
    request<any>({ path: getApiEndpoint('dynamicCode'), method: 'POST', data: { codeType: codeType, businessId: businessId } }, onSuccess, onError)
}
 
export const claimCoupon = (couponId: string, onSuccess: (data: any) => void, onError: (error: HttpError) => void) => {
    request<any>({ path: getApiEndpoint('couponClaim'), method: 'POST', data: { couponId: couponId }, idempotencyKey: createIdempotencyKey('coupon') }, onSuccess, onError)
}
 
export const getCouponDetail = (userCouponId: string, onSuccess: (data: any) => void, onError: (error: HttpError) => void) => {
    request<any>({ path: getApiEndpoint('couponDetail'), method: 'GET', data: { userCouponId: userCouponId } }, onSuccess, onError)
}
 
export const getRecordDetail = (recordId: string, onSuccess: (data: any) => void, onError: (error: HttpError) => void) => {
    request<any>({ path: getApiEndpoint('recordDetail'), method: 'GET', data: { recordId: recordId } }, onSuccess, onError)
}
 
export const submitBusiness = (businessType: string, payload: any, onSuccess: (data: any) => void, onError: (error: HttpError) => void) => {
    request<any>({ path: getApiEndpoint('businessSubmit'), method: 'POST', data: { businessType: businessType, payload: payload }, idempotencyKey: createIdempotencyKey(businessType) }, onSuccess, onError)
}
 
export const payOrder = (orderId: string, onSuccess: (data: any) => void, onError: (error: HttpError) => void) => {
    request<any>({ path: getApiEndpoint('orderPayment'), method: 'POST', data: { orderId: orderId }, idempotencyKey: createIdempotencyKey('payment') }, (payment: any) => {
        if (payment == null || payment.timeStamp == null || payment.nonceStr == null || payment.package == null || payment.paySign == null) {
            onError({ code: 'INVALID_PAYMENT_RESPONSE', message: '支付参数不完整', statusCode: 200, traceId: '' })
            return
        }
        uni.requestPayment({
            provider: 'wxpay',
            timeStamp: payment.timeStamp,
            nonceStr: payment.nonceStr,
            package: payment.package,
            signType: payment.signType != null ? payment.signType : 'RSA',
            paySign: payment.paySign,
            success: () => request<any>({ path: getApiEndpoint('orderPaymentResult'), method: 'GET', data: { orderId: orderId } }, onSuccess, onError),
            fail: () => onError({ code: 'PAYMENT_CANCELLED', message: '支付未完成', statusCode: 0, traceId: '' })
        })
    }, onError)
}
 
export const uploadProofFile = (filePath: string, businessType: string, onSuccess: (data: any) => void, onError: (error: HttpError) => void, onProgress?: (progress: number) => void): (() => void) => {
    const endpoint = getApiEndpoint('fileUpload')
    if (!isApiConfigured() || endpoint.length == 0) {
        onError({ code: 'API_NOT_CONFIGURED', message: '上传服务尚未配置', statusCode: 0, traceId: '' })
        return () => {}
    }
    const lowerPath = filePath.toLowerCase()
    if (!(/\.(jpg|jpeg|png|webp|pdf)$/).test(lowerPath)) {
        onError({ code: 'UNSUPPORTED_FILE', message: '仅支持 JPG、PNG、WEBP 或 PDF 文件', statusCode: 0, traceId: '' })
        return () => {}
    }
    let task: any = null
    let cancelled = false
    uni.getFileInfo({
        filePath: filePath,
        success: (file: any) => {
            if (cancelled) return
            if (file.size != null && file.size > 10 * 1024 * 1024) {
                onError({ code: 'FILE_TOO_LARGE', message: '文件不能超过 10MB', statusCode: 0, traceId: '' })
                return
            }
            task = uni.uploadFile({
                url: getApiBaseUrl().replace(/\/$/, '') + '/' + endpoint.replace(/^\//, ''),
                filePath: filePath,
                name: 'file',
                formData: { businessType: businessType },
                header: { Authorization: 'Bearer ' + getAccessToken(), 'X-Idempotency-Key': createIdempotencyKey('upload') },
                success: (response: any) => {
                    try {
                        const payload: any = JSON.parse(response.data as string)
                        if (response.statusCode >= 200 && response.statusCode < 300 && (payload.code == null || payload.code == 0 || payload.code == 200)) onSuccess(payload.data != null ? payload.data : payload)
                        else onError({ code: payload.code != null ? '' + payload.code : 'UPLOAD_FAILED', message: payload.message != null ? payload.message as string : '上传失败', statusCode: response.statusCode, traceId: payload.traceId != null ? payload.traceId as string : '' })
                    } catch (_) {
                        onError({ code: 'INVALID_UPLOAD_RESPONSE', message: '上传响应格式错误', statusCode: response.statusCode, traceId: '' })
                    }
                },
                fail: () => onError({ code: cancelled ? 'UPLOAD_CANCELLED' : 'UPLOAD_NETWORK_ERROR', message: cancelled ? '已取消上传' : '上传失败,请检查网络后重试', statusCode: 0, traceId: '' })
            })
            if (onProgress != null) task.onProgressUpdate((progress: any) => onProgress(progress.progress as number))
        },
        fail: () => onError({ code: 'FILE_READ_FAILED', message: '无法读取待上传文件', statusCode: 0, traceId: '' })
    })
    return () => { cancelled = true; if (task != null) task.abort() }
}