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
|
// 小程序 rich-text 不认 <s>/<strike>/<u>,换成它支持的 <del>/<ins>,
|
// 否则后台设的删除线/下划线在人才端看不见(看着像"样式没生效")。
|
.replace(/<\s*(\/?)\s*s(?=[\s>/])([^>]*)>/gi, '<$1del$2>')
|
.replace(/<\s*(\/?)\s*strike(?=[\s>/])([^>]*)>/gi, '<$1del$2>')
|
.replace(/<\s*(\/?)\s*u(?=[\s>/])([^>]*)>/gi, '<$1ins$2>')
|
.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() }
|
}
|