feat(voice): 大神语音介绍真实落地(录制/上传/试听)
- DB: companions.voice_intro_url;仓储 updateCompanionVoice;详情与 /api/expert/me 返回 voiceIntroUrl/voiceDuration - 后端: POST /api/companions/:id/voice(音频白名单 mp3/m4a/aac/wav/ogg、5MB、≤60s、限流、仅本人或管理员) - 小程序: api.uploadVoiceIntro(uni.uploadFile + 用户令牌);工作台新增「语音介绍」卡片(录制/重录/试听,60s mp3);详情页改为 uni.createInnerAudioContext 真实播放,卸载销毁 - 修复 uploadImage 未携带用户令牌(生产 fail-closed 后会导致上传失败) - 新增 voice-intro.spec.ts(104 套件/701 用例全绿);冒烟 7/7
这个提交包含在:
@@ -872,6 +872,18 @@ export async function applyPostParty(postId, applyData = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户会话 token 请求头(上传类接口需要)
|
||||
*/
|
||||
function authHeader() {
|
||||
try {
|
||||
const t = uni.getStorageSync('userToken');
|
||||
return t ? { Authorization: `Bearer ${t}` } : {};
|
||||
} catch (_) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 真实上传本地图片至服务端持久化静态存储
|
||||
* @param {string} tempFilePath 本地图片临时路径 (如 wxfile://tmp_xxx 或 blob:xxx)
|
||||
@@ -917,6 +929,7 @@ export function uploadImage(tempFilePath) {
|
||||
url: uploadUrl,
|
||||
filePath: tempFilePath,
|
||||
name: 'file',
|
||||
header: authHeader(),
|
||||
success: (uploadRes) => {
|
||||
try {
|
||||
const data = JSON.parse(uploadRes.data);
|
||||
@@ -1097,6 +1110,51 @@ export async function deleteExpertService(expertId, serviceId) {
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传大神语音介绍(录音文件),返回可直接播放的公网地址
|
||||
* @param {number} expertId 大神ID
|
||||
* @param {string} tempFilePath 录音临时文件路径
|
||||
* @param {number} duration 时长(秒,≤60)
|
||||
*/
|
||||
export function uploadVoiceIntro(expertId, tempFilePath, duration) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!tempFilePath) return reject(new Error('语音文件路径为空'));
|
||||
// #ifdef H5
|
||||
const uploadUrl = `/api/companions/${Number(expertId)}/voice`;
|
||||
// #endif
|
||||
// #ifndef H5
|
||||
const uploadUrl = `https://peiww.gynhc.com/api/companions/${Number(expertId)}/voice`;
|
||||
// #endif
|
||||
uni.uploadFile({
|
||||
url: uploadUrl,
|
||||
filePath: tempFilePath,
|
||||
name: 'file',
|
||||
header: authHeader(),
|
||||
formData: {
|
||||
duration: String(Math.min(60, Math.max(1, Math.round(Number(duration) || 15)))),
|
||||
userId: String(normalizeUserId()),
|
||||
},
|
||||
success: (uploadRes) => {
|
||||
try {
|
||||
const data = JSON.parse(uploadRes.data);
|
||||
if (data && data.code === 20000 && data.data && data.data.voiceIntroUrl) {
|
||||
let finalUrl = data.data.voiceIntroUrl;
|
||||
// #ifndef H5
|
||||
if (String(finalUrl).startsWith('/uploads/')) finalUrl = `https://peiww.gynhc.com${finalUrl}`;
|
||||
// #endif
|
||||
resolve({ url: finalUrl, duration: data.data.voiceDuration });
|
||||
} else {
|
||||
reject(new Error((data && data.message) || '语音上传失败'));
|
||||
}
|
||||
} catch (e) {
|
||||
reject(new Error('语音上传返回格式错误'));
|
||||
}
|
||||
},
|
||||
fail: (err) => reject(new Error(err.errMsg || '语音上传网络异常')),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户收藏的陪玩列表
|
||||
*/
|
||||
|
||||
在新工单中引用
屏蔽一个用户