feat(note): add audio upload and sharing functionality
- Introduced `note_audio` table for storing audio attachments related to notes. - Implemented audio upload endpoint in `Note` controller to handle audio file uploads. - Added sharing functionality with `note_share` table to manage share tokens and view counts. - Updated API routes to include endpoints for audio uploads and share creation. - Enhanced documentation to reflect new audio and sharing features.
This commit is contained in:
@@ -11,7 +11,7 @@ use think\App;
|
|||||||
/**
|
/**
|
||||||
* 笔记模块元信息控制器
|
* 笔记模块元信息控制器
|
||||||
*/
|
*/
|
||||||
class Meta extends BaseController
|
class c extends BaseController
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @var PlanningService
|
* @var PlanningService
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use app\note\controller\BaseController;
|
|||||||
use app\note\service\NoteService;
|
use app\note\service\NoteService;
|
||||||
use think\App;
|
use think\App;
|
||||||
use think\exception\ValidateException;
|
use think\exception\ValidateException;
|
||||||
|
use think\Request;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 笔记控制器
|
* 笔记控制器
|
||||||
@@ -153,4 +154,28 @@ class Note extends BaseController
|
|||||||
return Response::error($e->getMessage(), $e->getCode() ?: 500);
|
return Response::error($e->getMessage(), $e->getCode() ?: 500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传笔记录音
|
||||||
|
* POST /note/v1/item/audio/:id
|
||||||
|
*/
|
||||||
|
public function audio(Request $request, int $id)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
if ($id <= 0) {
|
||||||
|
return Response::error('笔记 ID 不正确', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$file = $request->file('audio');
|
||||||
|
if (!$file) {
|
||||||
|
return Response::error('录音文件不能为空', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$durationMs = (int) $request->post('audio_duration_ms', 0);
|
||||||
|
$result = $this->noteService->uploadAudio($this->getCurrentNoteUserId(), $id, $file, $durationMs);
|
||||||
|
return Response::success($result, '上传成功');
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return Response::error($e->getMessage(), $e->getCode() ?: 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace app\note\controller\v1;
|
||||||
|
|
||||||
|
use app\api\common\Response;
|
||||||
|
use app\note\controller\BaseController;
|
||||||
|
use app\note\service\NoteService;
|
||||||
|
use think\App;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 笔记分享控制器
|
||||||
|
*/
|
||||||
|
class Share extends BaseController
|
||||||
|
{
|
||||||
|
protected $noteService;
|
||||||
|
|
||||||
|
public function __construct(App $app)
|
||||||
|
{
|
||||||
|
parent::__construct($app);
|
||||||
|
$this->noteService = new NoteService();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create(int $id)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
if ($id <= 0) {
|
||||||
|
return Response::error('笔记 ID 不正确', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $this->noteService->createShare($this->getCurrentNoteUserId(), $id);
|
||||||
|
return Response::success($result, '分享已生成');
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return Response::error($e->getMessage(), $e->getCode() ?: 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function read(string $token)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
if (trim($token) === '') {
|
||||||
|
return Response::error('分享标识不能为空', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $this->noteService->getSharedDetail(trim($token));
|
||||||
|
return Response::success($result);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return Response::error($e->getMessage(), $e->getCode() ?: 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace app\note\model;
|
||||||
|
|
||||||
|
use think\Model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* note 模块录音附件模型
|
||||||
|
*/
|
||||||
|
class NoteAudio extends Model
|
||||||
|
{
|
||||||
|
protected $connection = 'dbnote';
|
||||||
|
|
||||||
|
protected $name = 'note_audio';
|
||||||
|
|
||||||
|
protected $pk = 'id';
|
||||||
|
|
||||||
|
protected $autoWriteTimestamp = false;
|
||||||
|
|
||||||
|
public static function findLatestByNoteId(int $noteId): ?self
|
||||||
|
{
|
||||||
|
return self::where('note_id', $noteId)
|
||||||
|
->order('id', 'desc')
|
||||||
|
->find();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace app\note\model;
|
||||||
|
|
||||||
|
use think\Model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* note 模块分享模型
|
||||||
|
*/
|
||||||
|
class NoteShare extends Model
|
||||||
|
{
|
||||||
|
protected $connection = 'dbnote';
|
||||||
|
|
||||||
|
protected $name = 'note_share';
|
||||||
|
|
||||||
|
protected $pk = 'id';
|
||||||
|
|
||||||
|
protected $autoWriteTimestamp = false;
|
||||||
|
|
||||||
|
public static function findActiveByNote(int $noteId, int $noteUserId): ?self
|
||||||
|
{
|
||||||
|
return self::where('note_id', $noteId)
|
||||||
|
->where('note_user_id', $noteUserId)
|
||||||
|
->where('status', 1)
|
||||||
|
->order('id', 'desc')
|
||||||
|
->find();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function findByToken(string $token): ?self
|
||||||
|
{
|
||||||
|
return self::where('share_token', $token)
|
||||||
|
->where('status', 1)
|
||||||
|
->find();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ use app\note\controller\v1\Ai;
|
|||||||
use app\note\controller\v1\Auth;
|
use app\note\controller\v1\Auth;
|
||||||
use app\note\controller\v1\Meta;
|
use app\note\controller\v1\Meta;
|
||||||
use app\note\controller\v1\Note;
|
use app\note\controller\v1\Note;
|
||||||
|
use app\note\controller\v1\Share;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* note 应用路由
|
* note 应用路由
|
||||||
@@ -19,6 +20,7 @@ use app\note\controller\v1\Note;
|
|||||||
// v1 笔记模块接口规划(公开)
|
// v1 笔记模块接口规划(公开)
|
||||||
Route::get('v1/meta/interfaces', [Meta::class, 'interfaces']);
|
Route::get('v1/meta/interfaces', [Meta::class, 'interfaces']);
|
||||||
Route::post('v1/auth/wechat-login', [Auth::class, 'wechatLogin']);
|
Route::post('v1/auth/wechat-login', [Auth::class, 'wechatLogin']);
|
||||||
|
Route::get('v1/share/read/:token', [Share::class, 'read']);
|
||||||
|
|
||||||
// v1 笔记模块接口(需登录)
|
// v1 笔记模块接口(需登录)
|
||||||
Route::group('v1', function () {
|
Route::group('v1', function () {
|
||||||
@@ -30,7 +32,9 @@ Route::group('v1', function () {
|
|||||||
Route::post('item/update/:id', [Note::class, 'update']);
|
Route::post('item/update/:id', [Note::class, 'update']);
|
||||||
Route::post('item/delete/:id', [Note::class, 'delete']);
|
Route::post('item/delete/:id', [Note::class, 'delete']);
|
||||||
Route::post('item/transcript/:id', [Note::class, 'transcript']);
|
Route::post('item/transcript/:id', [Note::class, 'transcript']);
|
||||||
|
Route::post('item/audio/:id', [Note::class, 'audio']);
|
||||||
|
|
||||||
Route::post('ai/summary/:id', [Ai::class, 'summary']);
|
Route::post('ai/summary/:id', [Ai::class, 'summary']);
|
||||||
Route::get('ai/summary/:id', [Ai::class, 'readSummary']);
|
Route::get('ai/summary/:id', [Ai::class, 'readSummary']);
|
||||||
|
Route::post('share/create/:id', [Share::class, 'create']);
|
||||||
})->middleware(\app\api\middleware\Auth::class);
|
})->middleware(\app\api\middleware\Auth::class);
|
||||||
|
|||||||
@@ -3,9 +3,13 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace app\note\service;
|
namespace app\note\service;
|
||||||
|
|
||||||
|
use app\note\model\NoteAudio;
|
||||||
use app\note\model\NoteAiSummary;
|
use app\note\model\NoteAiSummary;
|
||||||
use app\note\model\NoteItem;
|
use app\note\model\NoteItem;
|
||||||
|
use app\note\model\NoteShare;
|
||||||
use app\note\model\NoteTranscript;
|
use app\note\model\NoteTranscript;
|
||||||
|
use think\File;
|
||||||
|
use think\facade\Filesystem;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* note 模块笔记服务
|
* note 模块笔记服务
|
||||||
@@ -98,8 +102,10 @@ class NoteService
|
|||||||
{
|
{
|
||||||
$note = $this->getOwnedNote($noteUserId, $id);
|
$note = $this->getOwnedNote($noteUserId, $id);
|
||||||
$summary = NoteAiSummary::findLatestByNoteId($id);
|
$summary = NoteAiSummary::findLatestByNoteId($id);
|
||||||
|
$audio = NoteAudio::findLatestByNoteId($id);
|
||||||
|
|
||||||
$result = $this->formatNoteItem($note);
|
$result = $this->formatNoteItem($note);
|
||||||
|
$result['audio'] = $audio ? $this->formatAudio($audio) : null;
|
||||||
$result['summary'] = $summary ? [
|
$result['summary'] = $summary ? [
|
||||||
'summary_id' => (int) $summary->id,
|
'summary_id' => (int) $summary->id,
|
||||||
'summary_type' => (string) $summary->summary_type,
|
'summary_type' => (string) $summary->summary_type,
|
||||||
@@ -216,6 +222,131 @@ class NoteService
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传录音文件
|
||||||
|
*
|
||||||
|
* @param int $noteUserId
|
||||||
|
* @param int $id
|
||||||
|
* @param File $file
|
||||||
|
* @param int $durationMs
|
||||||
|
* @return array
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
public function uploadAudio(int $noteUserId, int $id, File $file, int $durationMs = 0): array
|
||||||
|
{
|
||||||
|
$note = $this->getOwnedNote($noteUserId, $id);
|
||||||
|
$savedPath = str_replace('\\', '/', Filesystem::disk('public')->putFile('note/audio', $file));
|
||||||
|
$now = time();
|
||||||
|
|
||||||
|
$audio = NoteAudio::findLatestByNoteId($id);
|
||||||
|
if (!$audio) {
|
||||||
|
$audio = new NoteAudio();
|
||||||
|
$audio->note_id = $id;
|
||||||
|
$audio->created_at = $now;
|
||||||
|
}
|
||||||
|
|
||||||
|
$audio->disk = 'public';
|
||||||
|
$audio->file_path = $savedPath;
|
||||||
|
$audio->file_url = $this->buildPublicFileUrl($savedPath);
|
||||||
|
$audio->file_size = (int) $file->getSize();
|
||||||
|
$audio->mime_type = (string) $file->getMime();
|
||||||
|
$audio->duration_ms = max(0, $durationMs);
|
||||||
|
$audio->updated_at = $now;
|
||||||
|
$audio->save();
|
||||||
|
|
||||||
|
$note->audio_duration_ms = max((int) $note->audio_duration_ms, (int) $audio->duration_ms);
|
||||||
|
if ($note->source_type === 'text') {
|
||||||
|
$note->source_type = trim((string) $note->content) !== '' ? 'mix' : 'audio';
|
||||||
|
}
|
||||||
|
$note->updated_at = $now;
|
||||||
|
$note->save();
|
||||||
|
|
||||||
|
return $this->formatAudio($audio);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建分享
|
||||||
|
*
|
||||||
|
* @param int $noteUserId
|
||||||
|
* @param int $id
|
||||||
|
* @return array
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
public function createShare(int $noteUserId, int $id): array
|
||||||
|
{
|
||||||
|
$note = $this->getOwnedNote($noteUserId, $id);
|
||||||
|
$share = NoteShare::findActiveByNote($id, $noteUserId);
|
||||||
|
$now = time();
|
||||||
|
|
||||||
|
if (!$share) {
|
||||||
|
$share = new NoteShare();
|
||||||
|
$share->note_id = $id;
|
||||||
|
$share->note_user_id = $noteUserId;
|
||||||
|
$share->share_token = bin2hex(random_bytes(16));
|
||||||
|
$share->view_count = 0;
|
||||||
|
$share->status = 1;
|
||||||
|
$share->created_at = $now;
|
||||||
|
}
|
||||||
|
|
||||||
|
$share->title = (string) $note->title;
|
||||||
|
$share->updated_at = $now;
|
||||||
|
$share->save();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'note_id' => $id,
|
||||||
|
'share_token' => (string) $share->share_token,
|
||||||
|
'share_path' => '/pages/note/edit?share_token=' . $share->share_token,
|
||||||
|
'title' => (string) $note->title,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取分享详情
|
||||||
|
*
|
||||||
|
* @param string $token
|
||||||
|
* @return array
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
public function getSharedDetail(string $token): array
|
||||||
|
{
|
||||||
|
$share = NoteShare::findByToken($token);
|
||||||
|
if (!$share) {
|
||||||
|
throw new \Exception('分享内容不存在或已失效', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((int) $share->expired_at > 0 && (int) $share->expired_at < time()) {
|
||||||
|
throw new \Exception('分享已过期', 410);
|
||||||
|
}
|
||||||
|
|
||||||
|
$note = NoteItem::where('id', (int) $share->note_id)
|
||||||
|
->where('deleted_at', 0)
|
||||||
|
->find();
|
||||||
|
if (!$note) {
|
||||||
|
throw new \Exception('分享内容不存在', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$summary = NoteAiSummary::findLatestByNoteId((int) $note->id);
|
||||||
|
$audio = NoteAudio::findLatestByNoteId((int) $note->id);
|
||||||
|
|
||||||
|
$share->view_count = (int) $share->view_count + 1;
|
||||||
|
$share->last_view_time = time();
|
||||||
|
$share->save();
|
||||||
|
|
||||||
|
$result = $this->formatNoteItem($note);
|
||||||
|
$result['audio'] = $audio ? $this->formatAudio($audio) : null;
|
||||||
|
$result['summary'] = $summary ? [
|
||||||
|
'summary_text' => (string) $summary->summary_text,
|
||||||
|
'status' => (string) $summary->status,
|
||||||
|
] : null;
|
||||||
|
$result['share'] = [
|
||||||
|
'share_token' => (string) $share->share_token,
|
||||||
|
'title' => (string) $share->title,
|
||||||
|
'view_count' => (int) $share->view_count,
|
||||||
|
];
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取当前用户拥有的笔记
|
* 获取当前用户拥有的笔记
|
||||||
*
|
*
|
||||||
@@ -258,6 +389,26 @@ class NoteService
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化音频附件
|
||||||
|
*
|
||||||
|
* @param NoteAudio $audio
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private function formatAudio(NoteAudio $audio): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'audio_id' => (int) $audio->id,
|
||||||
|
'disk' => (string) $audio->disk,
|
||||||
|
'file_path' => (string) $audio->file_path,
|
||||||
|
'audio_url' => (string) $audio->file_url,
|
||||||
|
'file_size' => (int) $audio->file_size,
|
||||||
|
'mime_type' => (string) $audio->mime_type,
|
||||||
|
'duration_ms' => (int) $audio->duration_ms,
|
||||||
|
'updated_at' => (int) $audio->updated_at,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 规范化标题
|
* 规范化标题
|
||||||
*
|
*
|
||||||
@@ -291,4 +442,15 @@ class NoteService
|
|||||||
$decoded = json_decode($value, true);
|
$decoded = json_decode($value, true);
|
||||||
return is_array($decoded) ? $decoded : [];
|
return is_array($decoded) ? $decoded : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 拼接公开文件 URL
|
||||||
|
*
|
||||||
|
* @param string $savedPath
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private function buildPublicFileUrl(string $savedPath): string
|
||||||
|
{
|
||||||
|
return rtrim((string) request()->domain(), '/') . '/storage/' . ltrim($savedPath, '/');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,14 +48,19 @@ class PlanningService
|
|||||||
['method' => 'POST', 'path' => '/note/v1/item/update/:id', 'desc' => '更新笔记'],
|
['method' => 'POST', 'path' => '/note/v1/item/update/:id', 'desc' => '更新笔记'],
|
||||||
['method' => 'POST', 'path' => '/note/v1/item/delete/:id', 'desc' => '删除笔记'],
|
['method' => 'POST', 'path' => '/note/v1/item/delete/:id', 'desc' => '删除笔记'],
|
||||||
['method' => 'POST', 'path' => '/note/v1/item/transcript/:id', 'desc' => '保存实时转写内容'],
|
['method' => 'POST', 'path' => '/note/v1/item/transcript/:id', 'desc' => '保存实时转写内容'],
|
||||||
|
['method' => 'POST', 'path' => '/note/v1/item/audio/:id', 'desc' => '上传录音附件'],
|
||||||
['method' => 'POST', 'path' => '/note/v1/ai/summary/:id', 'desc' => '发起 AI 总结'],
|
['method' => 'POST', 'path' => '/note/v1/ai/summary/:id', 'desc' => '发起 AI 总结'],
|
||||||
['method' => 'GET', 'path' => '/note/v1/ai/summary/:id', 'desc' => '查看 AI 总结结果'],
|
['method' => 'GET', 'path' => '/note/v1/ai/summary/:id', 'desc' => '查看 AI 总结结果'],
|
||||||
|
['method' => 'POST', 'path' => '/note/v1/share/create/:id', 'desc' => '生成分享标识'],
|
||||||
|
['method' => 'GET', 'path' => '/note/v1/share/read/:token', 'desc' => '读取分享内容'],
|
||||||
],
|
],
|
||||||
'suggested_tables' => [
|
'suggested_tables' => [
|
||||||
'note_user',
|
'note_user',
|
||||||
'note_item',
|
'note_item',
|
||||||
'note_transcript',
|
'note_transcript',
|
||||||
'note_ai_summary',
|
'note_ai_summary',
|
||||||
|
'note_audio',
|
||||||
|
'note_share',
|
||||||
],
|
],
|
||||||
'development_priority' => [
|
'development_priority' => [
|
||||||
'1. 先落小程序登录,打通微信 openid 与 JWT',
|
'1. 先落小程序登录,打通微信 openid 与 JWT',
|
||||||
@@ -157,6 +162,20 @@ class PlanningService
|
|||||||
'audio_duration_ms' => '当前累计录音时长,可选',
|
'audio_duration_ms' => '当前累计录音时长,可选',
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
'audio' => [
|
||||||
|
'route' => 'POST /note/v1/item/audio/:id',
|
||||||
|
'request' => [
|
||||||
|
'audio' => '录音文件 multipart 字段,必填',
|
||||||
|
'audio_duration_ms' => '录音时长,可选',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'share' => [
|
||||||
|
'route' => 'POST /note/v1/share/create/:id',
|
||||||
|
'response' => [
|
||||||
|
'share_token' => '分享 token',
|
||||||
|
'share_path' => '小程序分享路径',
|
||||||
|
],
|
||||||
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -103,3 +103,38 @@ CREATE TABLE IF NOT EXISTS `note_ai_summary` (
|
|||||||
KEY `idx_status` (`status`),
|
KEY `idx_status` (`status`),
|
||||||
KEY `idx_summary_type` (`summary_type`)
|
KEY `idx_summary_type` (`summary_type`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='note模块-AI总结结果表';
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='note模块-AI总结结果表';
|
||||||
|
|
||||||
|
-- 笔记录音附件表
|
||||||
|
CREATE TABLE IF NOT EXISTS `note_audio` (
|
||||||
|
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||||
|
`note_id` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT 'note_item.id',
|
||||||
|
`disk` varchar(30) NOT NULL DEFAULT 'public' COMMENT '存储磁盘',
|
||||||
|
`file_path` varchar(255) NOT NULL DEFAULT '' COMMENT '磁盘相对路径',
|
||||||
|
`file_url` varchar(500) NOT NULL DEFAULT '' COMMENT '公开访问地址',
|
||||||
|
`file_size` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '文件大小',
|
||||||
|
`mime_type` varchar(100) NOT NULL DEFAULT '' COMMENT '文件类型',
|
||||||
|
`duration_ms` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '录音时长',
|
||||||
|
`created_at` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建时间',
|
||||||
|
`updated_at` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '更新时间',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_note_id` (`note_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='note模块-录音附件表';
|
||||||
|
|
||||||
|
-- 笔记分享表
|
||||||
|
CREATE TABLE IF NOT EXISTS `note_share` (
|
||||||
|
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||||
|
`note_id` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT 'note_item.id',
|
||||||
|
`note_user_id` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT 'note_user.id',
|
||||||
|
`share_token` varchar(64) NOT NULL DEFAULT '' COMMENT '分享 token',
|
||||||
|
`title` varchar(255) NOT NULL DEFAULT '' COMMENT '分享标题',
|
||||||
|
`view_count` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '查看次数',
|
||||||
|
`status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '状态:0失效 1有效',
|
||||||
|
`expired_at` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '过期时间,0不过期',
|
||||||
|
`last_view_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '最后查看时间',
|
||||||
|
`created_at` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建时间',
|
||||||
|
`updated_at` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '更新时间',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uniq_share_token` (`share_token`),
|
||||||
|
KEY `idx_note_id` (`note_id`),
|
||||||
|
KEY `idx_note_user_id` (`note_user_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='note模块-分享记录表';
|
||||||
|
|||||||
+142
-7
@@ -13,7 +13,9 @@
|
|||||||
1. 微信小程序登录
|
1. 微信小程序登录
|
||||||
2. 笔记创建、列表、详情、更新、删除
|
2. 笔记创建、列表、详情、更新、删除
|
||||||
3. 实时转写文本保存
|
3. 实时转写文本保存
|
||||||
4. AI 总结生成与查询
|
4. 录音文件上传与播放
|
||||||
|
5. AI 总结生成与查询
|
||||||
|
6. 笔记分享与分享只读访问
|
||||||
|
|
||||||
路由注册位置:
|
路由注册位置:
|
||||||
|
|
||||||
@@ -67,7 +69,7 @@
|
|||||||
|
|
||||||
- [database.sql](/root/work/tp/database.sql#L26)
|
- [database.sql](/root/work/tp/database.sql#L26)
|
||||||
|
|
||||||
当前 `note` 模块使用以下 4 张表:
|
当前 `note` 模块使用以下 6 张表:
|
||||||
|
|
||||||
1. `note_user`
|
1. `note_user`
|
||||||
用于小程序用户登录和用户资料
|
用于小程序用户登录和用户资料
|
||||||
@@ -81,6 +83,12 @@
|
|||||||
4. `note_ai_summary`
|
4. `note_ai_summary`
|
||||||
AI 总结结果表
|
AI 总结结果表
|
||||||
|
|
||||||
|
5. `note_audio`
|
||||||
|
录音附件表,保存上传后的音频文件地址与时长
|
||||||
|
|
||||||
|
6. `note_share`
|
||||||
|
分享记录表,保存分享 token 与查看次数
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 5. 接口列表
|
## 5. 接口列表
|
||||||
@@ -435,7 +443,48 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 5.10 生成 AI 总结
|
### 5.10 上传录音文件
|
||||||
|
|
||||||
|
- 方法:`POST`
|
||||||
|
- 路径:`/note/v1/item/audio/:id`
|
||||||
|
- 是否鉴权:是
|
||||||
|
- Content-Type:`multipart/form-data`
|
||||||
|
|
||||||
|
表单参数:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
| :--- | :--- | :--- | :--- |
|
||||||
|
| `audio` | file | 是 | 录音文件 |
|
||||||
|
| `audio_duration_ms` | int | 否 | 录音时长 |
|
||||||
|
|
||||||
|
说明:
|
||||||
|
|
||||||
|
- 上传成功后会返回可直接播放的 `audio_url`
|
||||||
|
- 后端会同步写入 `note_audio`,并更新笔记的录音时长
|
||||||
|
|
||||||
|
成功响应示例:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "上传成功",
|
||||||
|
"data": {
|
||||||
|
"audio_id": 2,
|
||||||
|
"disk": "public",
|
||||||
|
"file_path": "note/audio/20260417/test.m4a",
|
||||||
|
"audio_url": "http://127.0.0.1:8000/storage/note/audio/20260417/test.m4a",
|
||||||
|
"file_size": 20480,
|
||||||
|
"mime_type": "audio/mp4",
|
||||||
|
"duration_ms": 18500,
|
||||||
|
"updated_at": 1710000400
|
||||||
|
},
|
||||||
|
"time": 1710000400
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5.11 生成 AI 总结
|
||||||
|
|
||||||
- 方法:`POST`
|
- 方法:`POST`
|
||||||
- 路径:`/note/v1/ai/summary/:id`
|
- 路径:`/note/v1/ai/summary/:id`
|
||||||
@@ -492,7 +541,7 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 5.11 查看 AI 总结
|
### 5.12 查看 AI 总结
|
||||||
|
|
||||||
- 方法:`GET`
|
- 方法:`GET`
|
||||||
- 路径:`/note/v1/ai/summary/:id`
|
- 路径:`/note/v1/ai/summary/:id`
|
||||||
@@ -504,6 +553,90 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### 5.13 创建分享
|
||||||
|
|
||||||
|
- 方法:`POST`
|
||||||
|
- 路径:`/note/v1/share/create/:id`
|
||||||
|
- 是否鉴权:是
|
||||||
|
|
||||||
|
成功响应示例:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "分享已生成",
|
||||||
|
"data": {
|
||||||
|
"note_id": 10,
|
||||||
|
"share_token": "7a4d2f2d4a5f1b20f6f9670bb1f4d123",
|
||||||
|
"share_path": "/pages/note/edit?share_token=7a4d2f2d4a5f1b20f6f9670bb1f4d123",
|
||||||
|
"title": "会议纪要"
|
||||||
|
},
|
||||||
|
"time": 1710000600
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
说明:
|
||||||
|
|
||||||
|
- 返回 `share_token` 和小程序页面路径
|
||||||
|
- 小程序可用该路径做转发
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5.14 读取分享内容
|
||||||
|
|
||||||
|
- 方法:`GET`
|
||||||
|
- 路径:`/note/v1/share/read/:token`
|
||||||
|
- 是否鉴权:否
|
||||||
|
|
||||||
|
成功响应示例:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "success",
|
||||||
|
"data": {
|
||||||
|
"id": 10,
|
||||||
|
"note_user_id": 1,
|
||||||
|
"title": "会议纪要",
|
||||||
|
"content": "今天确认了下周需求排期",
|
||||||
|
"transcript_text": "今天确认了下周需求排期",
|
||||||
|
"source_type": "mix",
|
||||||
|
"status": "draft",
|
||||||
|
"audio_duration_ms": 18500,
|
||||||
|
"summary_status": "success",
|
||||||
|
"last_transcript_time": 1710000300,
|
||||||
|
"created_at": 1710000000,
|
||||||
|
"updated_at": 1710000600,
|
||||||
|
"audio": {
|
||||||
|
"audio_id": 2,
|
||||||
|
"disk": "public",
|
||||||
|
"file_path": "note/audio/20260417/test.m4a",
|
||||||
|
"audio_url": "http://127.0.0.1:8000/storage/note/audio/20260417/test.m4a",
|
||||||
|
"file_size": 20480,
|
||||||
|
"mime_type": "audio/mp4",
|
||||||
|
"duration_ms": 18500,
|
||||||
|
"updated_at": 1710000400
|
||||||
|
},
|
||||||
|
"summary": {
|
||||||
|
"summary_text": "今天确认了下周需求排期,需要继续跟进接口联调。",
|
||||||
|
"status": "success"
|
||||||
|
},
|
||||||
|
"share": {
|
||||||
|
"share_token": "7a4d2f2d4a5f1b20f6f9670bb1f4d123",
|
||||||
|
"title": "会议纪要",
|
||||||
|
"view_count": 3
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"time": 1710000601
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
说明:
|
||||||
|
|
||||||
|
- 用于收件人直接查看分享的笔记内容、录音和 AI 摘要
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 6. 错误码约定
|
## 6. 错误码约定
|
||||||
|
|
||||||
当前模块沿用项目统一返回结构:
|
当前模块沿用项目统一返回结构:
|
||||||
@@ -537,8 +670,10 @@
|
|||||||
3. 保存返回的 `token`
|
3. 保存返回的 `token`
|
||||||
4. 创建笔记 `POST /note/v1/item/create`
|
4. 创建笔记 `POST /note/v1/item/create`
|
||||||
5. 录音转写过程中持续调用 `POST /note/v1/item/transcript/:id`
|
5. 录音转写过程中持续调用 `POST /note/v1/item/transcript/:id`
|
||||||
6. 需要生成总结时调用 `POST /note/v1/ai/summary/:id`
|
6. 录音停止后上传录音文件 `POST /note/v1/item/audio/:id`
|
||||||
7. 打开详情页时调用 `GET /note/v1/item/:id`
|
7. 需要生成总结时调用 `POST /note/v1/ai/summary/:id`
|
||||||
|
8. 分享前调用 `POST /note/v1/share/create/:id`
|
||||||
|
9. 打开详情页时调用 `GET /note/v1/item/:id`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -546,4 +681,4 @@
|
|||||||
|
|
||||||
1. AI 总结目前是规则版,不是大模型版
|
1. AI 总结目前是规则版,不是大模型版
|
||||||
2. 微信登录依赖服务端已正确配置 `WECHAT_MINI_APPID` 和 `WECHAT_MINI_SECRET`
|
2. 微信登录依赖服务端已正确配置 `WECHAT_MINI_APPID` 和 `WECHAT_MINI_SECRET`
|
||||||
3. 当前未实现文件音频上传,只实现了“转写文本写回后端”的数据链路
|
3. 分享读取接口为公开接口,建议前端仅用于只读展示
|
||||||
|
|||||||
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
u�Z
|
||||||
Binary file not shown.
Binary file not shown.
@@ -10,6 +10,7 @@ use app\note\controller\v1\Ai as NoteAi;
|
|||||||
use app\note\controller\v1\Auth as NoteAuth;
|
use app\note\controller\v1\Auth as NoteAuth;
|
||||||
use app\note\controller\v1\Meta as NoteMeta;
|
use app\note\controller\v1\Meta as NoteMeta;
|
||||||
use app\note\controller\v1\Note as NoteItem;
|
use app\note\controller\v1\Note as NoteItem;
|
||||||
|
use app\note\controller\v1\Share as NoteShare;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 全局路由入口。
|
* 全局路由入口。
|
||||||
@@ -54,6 +55,7 @@ Route::group('api/v1/video-work', function () {
|
|||||||
Route::group('note/v1', function () {
|
Route::group('note/v1', function () {
|
||||||
Route::get('meta/interfaces', [NoteMeta::class, 'interfaces']);
|
Route::get('meta/interfaces', [NoteMeta::class, 'interfaces']);
|
||||||
Route::post('auth/wechat-login', [NoteAuth::class, 'wechatLogin']);
|
Route::post('auth/wechat-login', [NoteAuth::class, 'wechatLogin']);
|
||||||
|
Route::get('share/read/:token', [NoteShare::class, 'read']);
|
||||||
});
|
});
|
||||||
|
|
||||||
// note v1 笔记小程序模块接口(需登录)
|
// note v1 笔记小程序模块接口(需登录)
|
||||||
@@ -66,7 +68,9 @@ Route::group('note/v1', function () {
|
|||||||
Route::post('item/update/:id', [NoteItem::class, 'update']);
|
Route::post('item/update/:id', [NoteItem::class, 'update']);
|
||||||
Route::post('item/delete/:id', [NoteItem::class, 'delete']);
|
Route::post('item/delete/:id', [NoteItem::class, 'delete']);
|
||||||
Route::post('item/transcript/:id', [NoteItem::class, 'transcript']);
|
Route::post('item/transcript/:id', [NoteItem::class, 'transcript']);
|
||||||
|
Route::post('item/audio/:id', [NoteItem::class, 'audio']);
|
||||||
|
|
||||||
Route::post('ai/summary/:id', [NoteAi::class, 'summary']);
|
Route::post('ai/summary/:id', [NoteAi::class, 'summary']);
|
||||||
Route::get('ai/summary/:id', [NoteAi::class, 'readSummary']);
|
Route::get('ai/summary/:id', [NoteAi::class, 'readSummary']);
|
||||||
|
Route::post('share/create/:id', [NoteShare::class, 'create']);
|
||||||
})->middleware(\app\api\middleware\Auth::class);
|
})->middleware(\app\api\middleware\Auth::class);
|
||||||
|
|||||||
Reference in New Issue
Block a user