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
这个提交包含在:
@@ -95,6 +95,7 @@ export async function initDatabase(): Promise<void> {
|
||||
hourly_rate INT NOT NULL,
|
||||
voice_duration INT DEFAULT 15,
|
||||
voice_title VARCHAR(128),
|
||||
voice_intro_url VARCHAR(255) DEFAULT '',
|
||||
rating DECIMAL(3,2) DEFAULT 4.90,
|
||||
order_count INT DEFAULT 0,
|
||||
status VARCHAR(32) DEFAULT '接单中',
|
||||
@@ -115,6 +116,9 @@ export async function initDatabase(): Promise<void> {
|
||||
try {
|
||||
await conn.query("ALTER TABLE companions ADD COLUMN reject_reason VARCHAR(255) DEFAULT '' AFTER audit_status");
|
||||
} catch (_) {}
|
||||
try {
|
||||
await conn.query("ALTER TABLE companions ADD COLUMN voice_intro_url VARCHAR(255) NOT NULL DEFAULT '' AFTER voice_title");
|
||||
} catch (_) {}
|
||||
|
||||
// 4. 订单表
|
||||
await conn.query(`
|
||||
|
||||
@@ -750,7 +750,7 @@ export class MysqlRepository {
|
||||
SELECT
|
||||
id, user_id as userId, name, avatar, gender, age, track, rank_name as \`rank\`,
|
||||
hourly_rate as hourlyRate, voice_duration as voiceDuration,
|
||||
voice_title as voiceTitle, rating, order_count as orderCount,
|
||||
voice_title as voiceTitle, voice_intro_url as voiceIntroUrl, rating, order_count as orderCount,
|
||||
status, audit_status as auditStatus, COALESCE(reject_reason, '') as rejectReason,
|
||||
id_card_masked as idCardMasked, tags, apply_time as applyTime
|
||||
FROM companions
|
||||
@@ -936,6 +936,7 @@ export class MysqlRepository {
|
||||
voiceDuration: synthetic ? 0 : (Number(c.voice_duration) || 15),
|
||||
voiceType: 'voice',
|
||||
voiceTitle: synthetic ? '' : (c.voice_title || '语音招呼'),
|
||||
voiceIntroUrl: synthetic ? '' : (c.voice_intro_url || ''),
|
||||
bio: c.custom_bio || (synthetic ? '' : '专业同城生活陪伴,热情礼貌,守时诚信。'),
|
||||
tags: parsedTags,
|
||||
photos: c.custom_photos || (synthetic
|
||||
@@ -949,6 +950,18 @@ export class MysqlRepository {
|
||||
};
|
||||
}
|
||||
|
||||
/** 更新大神语音介绍(录音文件 URL + 时长秒) */
|
||||
async updateCompanionVoice(companionId: number | string, voiceIntroUrl: string, voiceDuration: number): Promise<any> {
|
||||
const comp = await this.resolveCompanionRow(companionId);
|
||||
if (!comp) return null;
|
||||
const dur = Math.min(60, Math.max(1, Math.round(Number(voiceDuration) || 0)));
|
||||
await pool.query(
|
||||
"UPDATE companions SET voice_intro_url = ?, voice_duration = ?, voice_title = COALESCE(NULLIF(voice_title, ''), '语音介绍') WHERE id = ?",
|
||||
[String(voiceIntroUrl), dur, comp.id]
|
||||
);
|
||||
return this.getCompanionById(comp.id);
|
||||
}
|
||||
|
||||
async updateCompanionProfile(id: number | string, data: { hourlyRate?: number; tags?: string[]; rankName?: string; status?: string }): Promise<any> {
|
||||
const comp = await this.resolveCompanionRow(id);
|
||||
if (!comp) return null;
|
||||
|
||||
在新工单中引用
屏蔽一个用户