feat: 客服

This commit is contained in:
2026-07-20 15:31:52 +08:00
parent 0099ccaa60
commit 82a8043578
23 changed files with 5071 additions and 360 deletions
@@ -0,0 +1,222 @@
import request from '@/utils/request.js'
import { BASE_URL, KEFU_BASE_URL, Authorization } from '@/utils/config.js'
import { getStorageFun, TOKEN_NAME } from '@/utils/auth.js'
export const CONTENT_TYPE = Object.freeze({
TEXT: 1,
IMAGE: 2,
FILE: 3,
AUDIO: 4,
PRODUCT: 10,
ORDER: 11,
QUOTE: 12,
RECALLED: 99
})
function kefuRequest({ url, method = 'GET', data = {} }) {
const token = getStorageFun(TOKEN_NAME)
return new Promise((resolve, reject) => {
uni.request({
url: `${KEFU_BASE_URL}${url}`,
method,
data,
header: {
Authorization,
'HSM-AUTH': token || '',
'content-type': 'application/json'
},
success: response => {
const body = response.data || {}
if (response.statusCode >= 200 && response.statusCode < 300 && body.bizcode === 100) {
resolve(body)
return
}
const error = new Error(body.msg || `客服接口请求失败(${response.statusCode})`)
error.statusCode = response.statusCode
error.response = body
reject(error)
},
fail: error => reject(new Error(error.errMsg || '客服接口连接失败'))
})
})
}
export function getCurrentMember() {
return request({ url: '/user/getUserInfo', method: 'get', isShowLoading: false }).then(result => result.data)
}
export function getMessages({ userId, conversationId, page = 1, pageSize = 80 }) {
return kefuRequest({
url: '/kefu/user/messages',
method: 'get',
data: { userId, conversationId, page, pageSize }
}).then(result => result.data || {})
}
export function sendMessage(data) {
return kefuRequest({
url: '/kefu/user/send',
method: 'post',
data
}).then(result => result.data)
}
export function transferToHuman({ userId, conversationId, reason = '用户点击转人工' }) {
return kefuRequest({
url: '/kefu/user/transfer-human',
method: 'post',
data: { userId, conversationId, reason }
}).then(result => result.data)
}
export function recallMessage({ userId, messageId }) {
const data = { userId, messageId }
return kefuRequest({
url: '/kefu/user/message/recall',
method: 'post',
data
}).catch(error => {
if (!error || error.statusCode !== 404) throw error
return kefuRequest({ url: '/kefu/user/recall', method: 'post', data })
})
.then(result => result.data)
}
export function deleteMessageForMember({ userId, messageId }) {
return kefuRequest({
url: '/kefu/user/message/delete',
method: 'post',
data: { userId, messageId }
}).then(result => result.data)
}
export function transcribeMessage({ userId, messageId }) {
return kefuRequest({
url: '/kefu/user/message/transcribe',
method: 'post',
data: { userId, messageId }
}).then(result => result.data)
}
export function getUnreadCount(userId) {
return kefuRequest({
url: '/kefu/user/unread-count',
method: 'get',
data: { userId }
}).then(result => Number(result.data || 0))
}
export function markMemberMessagesRead({ userId, conversationId }) {
return kefuRequest({
url: '/kefu/user/read',
method: 'post',
data: { userId, conversationId }
}).then(result => result.data)
}
export function getBrowseHistory({ userId, page = 1, pageSize = 10 }) {
return kefuRequest({
url: '/kefu/user/browse-history',
method: 'get',
data: { userId, page, pageSize }
}).then(result => result.data)
}
export function getOrders({ userId, page = 1, pageSize = 10 }) {
return kefuRequest({
url: '/kefu/user/orders',
method: 'get',
data: { userId, page, pageSize }
}).then(result => result.data)
}
export function uploadMessageImage(filePath) {
const token = getStorageFun(TOKEN_NAME)
return new Promise((resolve, reject) => {
uni.uploadFile({
url: `${BASE_URL}/common/upload/image`,
filePath,
name: 'file',
formData: { type: 'COMMON' },
header: {
Authorization,
'HSM-AUTH': token || ''
},
success: response => {
try {
const body = typeof response.data === 'string' ? JSON.parse(response.data) : response.data
if (!body || body.bizcode !== 100) throw new Error((body && body.msg) || '图片上传失败')
const data = body.data || {}
const url = data.accessUrl || data.dbUrl || data.url || data.fileUrl || data.path
if (!url) throw new Error('图片上传后未返回访问地址')
resolve(url)
} catch (error) {
reject(error)
}
},
fail: error => reject(new Error(error.errMsg || '图片上传失败'))
})
})
}
function resolveUploadResult(response, fallbackMessage) {
const body = typeof response.data === 'string' ? JSON.parse(response.data) : response.data
if (!body || body.bizcode !== 100) throw new Error((body && body.msg) || fallbackMessage)
const data = body.data || {}
const url = data.accessUrl || data.dbUrl || data.url || data.fileUrl || data.path
if (!url) throw new Error('上传成功但未返回访问地址')
return url
}
export function uploadMessageAsset(source, kind = 'file') {
const token = getStorageFun(TOKEN_NAME)
const endpoints = [...new Set([
`${String(KEFU_BASE_URL).replace(/\/$/, '')}/kefu/user/upload/${kind}`,
`${String(BASE_URL).replace(/\/$/, '')}/common/upload/${kind}`
])]
// #ifdef H5
if (typeof Blob !== 'undefined' && source instanceof Blob) {
const formData = new FormData()
formData.append('file', source, source.name || `${kind}-${Date.now()}`)
return (async () => {
let lastError = null
for (const endpoint of endpoints) {
const response = await fetch(endpoint, {
method: 'POST',
headers: { Authorization, 'HSM-AUTH': token || '' },
body: formData
})
if (response.status === 404) {
lastError = new Error(`${kind === 'audio' ? '语音' : '文件'}上传接口不存在`)
continue
}
return resolveUploadResult({ data: await response.text() }, `${kind === 'audio' ? '语音' : '文件'}上传失败`)
}
throw lastError || new Error(`${kind === 'audio' ? '语音' : '文件'}上传失败`)
})()
}
// #endif
const filePath = typeof source === 'string' ? source : (source.tempFilePath || source.path)
return new Promise((resolve, reject) => {
const uploadAt = index => {
uni.uploadFile({
url: endpoints[index],
filePath,
name: 'file',
header: { Authorization, 'HSM-AUTH': token || '' },
success: response => {
if (Number(response.statusCode) === 404 && index + 1 < endpoints.length) {
uploadAt(index + 1)
return
}
try { resolve(resolveUploadResult(response, `${kind === 'audio' ? '语音' : '文件'}上传失败`)) } catch (error) { reject(error) }
},
fail: error => reject(new Error(error.errMsg || `${kind === 'audio' ? '语音' : '文件'}上传失败`))
})
}
uploadAt(0)
})
}
export const uploadMessageFile = source => uploadMessageAsset(source, 'file')
export const uploadMessageAudio = source => uploadMessageAsset(source, 'audio')