家长端支持语音留言发送
This commit is contained in:
@@ -178,11 +178,18 @@ class ImDAO(BaseDAO):
|
||||
participant_b_id = str(high_id)
|
||||
return participant_a_id, participant_b_id, f"{participant_a_id}:{participant_b_id}"
|
||||
|
||||
async def _build_preview(self, content_type: int, content_text: str | None) -> str:
|
||||
async def _build_preview(
|
||||
self,
|
||||
content_type: int,
|
||||
content_text: str | None,
|
||||
ext_json: dict[str, Any] | None = None,
|
||||
) -> str:
|
||||
if content_type == 1:
|
||||
return (content_text or "").strip()[:255]
|
||||
if content_type == 2:
|
||||
return "[audio]"
|
||||
if (ext_json or {}).get("message_kind") == "leave_message":
|
||||
return "[留言]"
|
||||
return "[语音]"
|
||||
if content_type == 3:
|
||||
return "[image]"
|
||||
return "[json]"
|
||||
@@ -239,7 +246,11 @@ class ImDAO(BaseDAO):
|
||||
return conversation_id, True
|
||||
|
||||
now_sql = "CURRENT_TIMESTAMP(3)"
|
||||
preview = await self._build_preview(payload.content_type, payload.content_text)
|
||||
preview = await self._build_preview(
|
||||
payload.content_type,
|
||||
payload.content_text,
|
||||
payload.ext_json,
|
||||
)
|
||||
|
||||
try:
|
||||
conversation_row = await self._get_conversation_by_id(conversation_id=conversation_id, lock=True)
|
||||
|
||||
@@ -3,7 +3,7 @@ import logging
|
||||
from collections.abc import Mapping
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Request, Response, status
|
||||
from fastapi import APIRouter, Depends, File, Form, HTTPException, Query, Request, Response, UploadFile, status
|
||||
from sqlalchemy import text
|
||||
|
||||
try:
|
||||
@@ -442,6 +442,64 @@ async def create_child_message_for_parent(
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/{child_id}/voice-message",
|
||||
response_model=ConversationMessageCreateResponse,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
)
|
||||
async def create_child_voice_message_for_parent(
|
||||
child_id: int,
|
||||
request: Request,
|
||||
response: Response,
|
||||
file: UploadFile = File(...),
|
||||
duration_ms: int = Form(..., ge=0),
|
||||
client_msg_id: str = Form(..., min_length=1, max_length=64),
|
||||
transcript_text: str | None = Form(default=None, max_length=1000),
|
||||
current_user_id: int = Depends(get_current_user_id),
|
||||
) -> ConversationMessageCreateResponse:
|
||||
try:
|
||||
content = await file.read()
|
||||
result = await im_service.create_parent_child_voice_message(
|
||||
parent_user_id=current_user_id,
|
||||
child_id=child_id,
|
||||
filename=file.filename,
|
||||
content_type=file.content_type,
|
||||
content=content,
|
||||
media_duration_ms=duration_ms,
|
||||
media_transcript_text=transcript_text,
|
||||
client_msg_id=client_msg_id,
|
||||
ext_json={
|
||||
"message_kind": "leave_message",
|
||||
"source": "parent_weapp_voice",
|
||||
},
|
||||
)
|
||||
finally:
|
||||
await file.close()
|
||||
|
||||
if result.idempotent:
|
||||
response.status_code = status.HTTP_200_OK
|
||||
|
||||
logger.info(
|
||||
"parent child voice message created",
|
||||
extra={
|
||||
"event": "parent_child_voice_message_create",
|
||||
"request_id": getattr(request.state, "request_id", None),
|
||||
"user_id": current_user_id,
|
||||
"child_id": child_id,
|
||||
"conversation_id": result.conversation_id,
|
||||
"idempotent": result.idempotent,
|
||||
},
|
||||
)
|
||||
|
||||
return ConversationMessageCreateResponse(
|
||||
idempotent=result.idempotent,
|
||||
conversation_id=result.conversation_id,
|
||||
conversation_type=result.conversation_type,
|
||||
conversation_type_name=result.conversation_type_name,
|
||||
message=result.message,
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/{child_id}/conversations/{conversation_id}/messages",
|
||||
response_model=ChildConversationMessageListResponse,
|
||||
|
||||
@@ -2,6 +2,7 @@ from dataclasses import dataclass
|
||||
import hashlib
|
||||
import json
|
||||
from collections.abc import Mapping
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from fastapi import HTTPException
|
||||
@@ -36,6 +37,25 @@ PARTICIPANT_TYPE_NAMES = {
|
||||
CHILD_PARTICIPANT_TYPE: "child",
|
||||
}
|
||||
|
||||
_AUDIO_CONTENT_TYPE_TO_EXT = {
|
||||
"audio/mpeg": "mp3",
|
||||
"audio/mp3": "mp3",
|
||||
"audio/aac": "aac",
|
||||
"audio/wav": "wav",
|
||||
"audio/x-wav": "wav",
|
||||
"audio/x-m4a": "m4a",
|
||||
"audio/mp4": "m4a",
|
||||
"audio/webm": "webm",
|
||||
"application/octet-stream": "mp3",
|
||||
}
|
||||
_AUDIO_EXTENSION_ALIASES = {
|
||||
".mp3": "mp3",
|
||||
".aac": "aac",
|
||||
".m4a": "m4a",
|
||||
".wav": "wav",
|
||||
".webm": "webm",
|
||||
}
|
||||
|
||||
|
||||
def conversation_type_name(conversation_type: int) -> str:
|
||||
return CONVERSATION_TYPE_NAMES.get(conversation_type, f"unknown_{conversation_type}")
|
||||
@@ -66,6 +86,20 @@ def normalize_content_json(value: Any) -> dict[str, Any] | None:
|
||||
return None
|
||||
|
||||
|
||||
def normalize_audio_extension(*, filename: str | None, content_type: str | None) -> tuple[str, str]:
|
||||
normalized_content_type = (content_type or "").strip().lower() or "audio/mpeg"
|
||||
if normalized_content_type in _AUDIO_CONTENT_TYPE_TO_EXT:
|
||||
return _AUDIO_CONTENT_TYPE_TO_EXT[normalized_content_type], normalized_content_type
|
||||
|
||||
suffix = Path(filename or "").suffix.lower()
|
||||
if suffix in _AUDIO_EXTENSION_ALIASES:
|
||||
extension = _AUDIO_EXTENSION_ALIASES[suffix]
|
||||
fallback_content_type = "audio/m4a" if extension == "m4a" else f"audio/{extension}"
|
||||
return extension, fallback_content_type
|
||||
|
||||
raise HTTPException(status_code=415, detail="unsupported audio file type")
|
||||
|
||||
|
||||
def row_to_message_item(row: Mapping[str, Any]) -> ChildConversationMessageItem:
|
||||
return ChildConversationMessageItem(
|
||||
id=int(row["id"]),
|
||||
@@ -201,6 +235,58 @@ class ImService(DatabaseServiceBase):
|
||||
finally:
|
||||
await db_session.close()
|
||||
|
||||
async def create_parent_child_voice_message(
|
||||
self,
|
||||
*,
|
||||
parent_user_id: int,
|
||||
child_id: int,
|
||||
filename: str | None,
|
||||
content_type: str | None,
|
||||
content: bytes,
|
||||
media_duration_ms: int | None,
|
||||
media_transcript_text: str | None,
|
||||
client_msg_id: str,
|
||||
ext_json: dict[str, Any] | None = None,
|
||||
) -> ConversationMessageCreateResult:
|
||||
if not content:
|
||||
raise HTTPException(status_code=400, detail="audio file is empty")
|
||||
|
||||
extension, normalized_content_type = normalize_audio_extension(
|
||||
filename=filename,
|
||||
content_type=content_type,
|
||||
)
|
||||
stored_audio = await self.audio_storage.upload_audio(
|
||||
device_id=f"parent-{parent_user_id}",
|
||||
content=content,
|
||||
content_type=normalized_content_type,
|
||||
extension=extension,
|
||||
)
|
||||
payload = ParentChildMessageCreateRequest(
|
||||
content_type=2,
|
||||
media_file_key=stored_audio.file_key,
|
||||
media_duration_ms=media_duration_ms,
|
||||
media_mime_type=normalized_content_type,
|
||||
media_size_bytes=len(content),
|
||||
media_transcript_text=(media_transcript_text or "").strip() or None,
|
||||
client_msg_id=client_msg_id,
|
||||
ext_json=ext_json,
|
||||
)
|
||||
|
||||
try:
|
||||
result = await self.create_parent_child_message(
|
||||
parent_user_id=parent_user_id,
|
||||
child_id=child_id,
|
||||
payload=payload,
|
||||
)
|
||||
except Exception:
|
||||
try:
|
||||
await self.audio_storage.delete_audio(stored_audio.file_key)
|
||||
except MessageAudioStorageError:
|
||||
pass
|
||||
raise
|
||||
|
||||
return result
|
||||
|
||||
async def _create_device_message_with_payload(
|
||||
self,
|
||||
*,
|
||||
|
||||
@@ -113,6 +113,15 @@ class MessageAudioStorageService:
|
||||
Expired=settings.cos_avatar_url_expire_seconds,
|
||||
)
|
||||
|
||||
async def delete_audio(self, file_key_or_url: str) -> None:
|
||||
self._assert_ready()
|
||||
normalized_key = self._normalize_file_key(file_key_or_url)
|
||||
await asyncio.to_thread(
|
||||
self._get_client().delete_object,
|
||||
Bucket=settings.cos_bucket_message,
|
||||
Key=normalized_key,
|
||||
)
|
||||
|
||||
def _upload_audio_sync(
|
||||
self,
|
||||
*,
|
||||
|
||||
Reference in New Issue
Block a user