feat: add note module and route fixes
This commit is contained in:
@@ -8,4 +8,14 @@ DB_PASS = password
|
|||||||
DB_PORT = 3306
|
DB_PORT = 3306
|
||||||
DB_CHARSET = utf8
|
DB_CHARSET = utf8
|
||||||
|
|
||||||
|
DB_NOTE_HOSTNAME = 127.0.0.1
|
||||||
|
DB_NOTE_DATABASE = tp
|
||||||
|
DB_NOTE_USERNAME = tp
|
||||||
|
DB_NOTE_PASSWORD = your_note_password
|
||||||
|
DB_NOTE_HOSTPORT = 3306
|
||||||
|
DB_NOTE_CHARSET = utf8mb4
|
||||||
|
|
||||||
|
WECHAT_MINI_APPID = your_wechat_mini_appid
|
||||||
|
WECHAT_MINI_SECRET = your_wechat_mini_secret
|
||||||
|
|
||||||
DEFAULT_LANG = zh-cn
|
DEFAULT_LANG = zh-cn
|
||||||
|
|||||||
@@ -42,6 +42,36 @@ abstract class BaseController
|
|||||||
protected function initialize()
|
protected function initialize()
|
||||||
{}
|
{}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前登录用户载荷。
|
||||||
|
*
|
||||||
|
* 说明:
|
||||||
|
* - 登录态由路由中间件 `\app\api\middleware\Auth` 统一校验
|
||||||
|
* - 控制器只负责读取中间件已经注入的用户信息
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
protected function getLoginPayload(): array
|
||||||
|
{
|
||||||
|
$payload = $this->request->middleware('payload', []);
|
||||||
|
|
||||||
|
if (empty($payload['userid'])) {
|
||||||
|
throw new \RuntimeException('未登录', 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前登录用户 ID。
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
protected function getLoginUserId(): int
|
||||||
|
{
|
||||||
|
return (int) $this->getLoginPayload()['userid'];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 成功响应
|
* 成功响应
|
||||||
* @param mixed $data 返回数据
|
* @param mixed $data 返回数据
|
||||||
|
|||||||
@@ -117,12 +117,9 @@ class Auth extends BaseController
|
|||||||
public function me()
|
public function me()
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$payload = $this->request->payload ?? null;
|
$userid = $this->getLoginUserId();
|
||||||
if (!$payload) {
|
|
||||||
return Response::error('未登录', 401);
|
|
||||||
}
|
|
||||||
|
|
||||||
$result = $this->authService->getUserInfo($payload['userid']);
|
$result = $this->authService->getUserInfo($userid);
|
||||||
|
|
||||||
return Response::success($result);
|
return Response::success($result);
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
@@ -146,10 +143,7 @@ class Auth extends BaseController
|
|||||||
public function password()
|
public function password()
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$payload = $this->request->payload ?? null;
|
$userid = $this->getLoginUserId();
|
||||||
if (!$payload) {
|
|
||||||
return Response::error('未登录', 401);
|
|
||||||
}
|
|
||||||
|
|
||||||
$data = $this->request->post();
|
$data = $this->request->post();
|
||||||
|
|
||||||
@@ -164,7 +158,7 @@ class Auth extends BaseController
|
|||||||
])->check($data);
|
])->check($data);
|
||||||
|
|
||||||
$this->authService->changePassword(
|
$this->authService->changePassword(
|
||||||
$payload['userid'],
|
$userid,
|
||||||
$data['old_password'],
|
$data['old_password'],
|
||||||
$data['new_password']
|
$data['new_password']
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -40,10 +40,7 @@ class Platform extends BaseController
|
|||||||
public function accounts()
|
public function accounts()
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$payload = $this->request->payload ?? null;
|
$userid = $this->getLoginUserId();
|
||||||
if (!$payload || empty($payload['userid'])) {
|
|
||||||
return Response::error('未登录', 401);
|
|
||||||
}
|
|
||||||
|
|
||||||
$platformInput = $this->request->get('platform');
|
$platformInput = $this->request->get('platform');
|
||||||
$platform = null;
|
$platform = null;
|
||||||
@@ -56,7 +53,7 @@ class Platform extends BaseController
|
|||||||
$platform = (int) $platformInput;
|
$platform = (int) $platformInput;
|
||||||
}
|
}
|
||||||
|
|
||||||
$result = $this->platformService->getAccountList((int) $payload['userid'], $platform);
|
$result = $this->platformService->getAccountList($userid, $platform);
|
||||||
|
|
||||||
return Response::success($result);
|
return Response::success($result);
|
||||||
} catch (\Exception $exception) {
|
} catch (\Exception $exception) {
|
||||||
|
|||||||
@@ -45,12 +45,9 @@ class PublishPlan extends BaseController
|
|||||||
public function index()
|
public function index()
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$payload = $this->request->payload ?? null;
|
$userid = $this->getLoginUserId();
|
||||||
if (!$payload || empty($payload['userid'])) {
|
|
||||||
return Response::error('未登录', 401);
|
|
||||||
}
|
|
||||||
|
|
||||||
$result = $this->publishPlanService->getPlanList((int) $payload['userid'], [
|
$result = $this->publishPlanService->getPlanList($userid, [
|
||||||
'status' => (string) $this->request->get('status', 'all'),
|
'status' => (string) $this->request->get('status', 'all'),
|
||||||
'page' => (int) $this->request->get('page', 1),
|
'page' => (int) $this->request->get('page', 1),
|
||||||
'page_size' => (int) $this->request->get('page_size', 20),
|
'page_size' => (int) $this->request->get('page_size', 20),
|
||||||
@@ -74,12 +71,9 @@ class PublishPlan extends BaseController
|
|||||||
public function start(int $id)
|
public function start(int $id)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$payload = $this->request->payload ?? null;
|
$userid = $this->getLoginUserId();
|
||||||
if (!$payload || empty($payload['userid'])) {
|
|
||||||
return Response::error('未登录', 401);
|
|
||||||
}
|
|
||||||
|
|
||||||
$result = $this->publishPlanService->startPlan((int) $payload['userid'], $id);
|
$result = $this->publishPlanService->startPlan($userid, $id);
|
||||||
|
|
||||||
return Response::success($result, '设置成功');
|
return Response::success($result, '设置成功');
|
||||||
} catch (\Exception $exception) {
|
} catch (\Exception $exception) {
|
||||||
@@ -100,12 +94,9 @@ class PublishPlan extends BaseController
|
|||||||
public function stop(int $id)
|
public function stop(int $id)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$payload = $this->request->payload ?? null;
|
$userid = $this->getLoginUserId();
|
||||||
if (!$payload || empty($payload['userid'])) {
|
|
||||||
return Response::error('未登录', 401);
|
|
||||||
}
|
|
||||||
|
|
||||||
$result = $this->publishPlanService->stopPlan((int) $payload['userid'], $id);
|
$result = $this->publishPlanService->stopPlan($userid, $id);
|
||||||
|
|
||||||
return Response::success($result, '设置成功');
|
return Response::success($result, '设置成功');
|
||||||
} catch (\Exception $exception) {
|
} catch (\Exception $exception) {
|
||||||
|
|||||||
@@ -46,12 +46,9 @@ class VideoWork extends BaseController
|
|||||||
public function index()
|
public function index()
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$payload = $this->request->payload ?? null;
|
$userid = $this->getLoginUserId();
|
||||||
if (!$payload || empty($payload['userid'])) {
|
|
||||||
return Response::error('未登录', 401);
|
|
||||||
}
|
|
||||||
|
|
||||||
$result = $this->videoWorkService->getVideoList((int) $payload['userid'], [
|
$result = $this->videoWorkService->getVideoList($userid, [
|
||||||
'platform' => $this->request->get('platform', 'all'),
|
'platform' => $this->request->get('platform', 'all'),
|
||||||
'vuid' => $this->request->get('vuid', 'all'),
|
'vuid' => $this->request->get('vuid', 'all'),
|
||||||
'page' => (int) $this->request->get('page', 1),
|
'page' => (int) $this->request->get('page', 1),
|
||||||
|
|||||||
@@ -33,9 +33,11 @@ class Auth
|
|||||||
return Response::error('令牌无效或已过期', 401);
|
return Response::error('令牌无效或已过期', 401);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 将用户信息注入请求
|
// 将用户信息写入请求中间件上下文,供控制器统一读取
|
||||||
$request->payload = $payload;
|
$request->withMiddleware([
|
||||||
$request->userid = $payload['userid'] ?? null;
|
'payload' => $payload,
|
||||||
|
'userid' => (int) ($payload['userid'] ?? 0),
|
||||||
|
]);
|
||||||
|
|
||||||
return $next($request);
|
return $next($request);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace app\note\controller;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* note 模块基础控制器
|
||||||
|
*/
|
||||||
|
abstract class BaseController extends \app\api\controller\BaseController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 获取当前 note 模块登录用户 ID。
|
||||||
|
*
|
||||||
|
* 说明:
|
||||||
|
* - note 模块复用全局 JWT 中间件
|
||||||
|
* - 但要求 token 载荷中必须带 `guard=note`
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
protected function getCurrentNoteUserId(): int
|
||||||
|
{
|
||||||
|
$payload = $this->getLoginPayload();
|
||||||
|
|
||||||
|
if (($payload['guard'] ?? '') !== 'note') {
|
||||||
|
throw new \RuntimeException('note 模块登录态无效', 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int) ($payload['userid'] ?? 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace app\note\controller\v1;
|
||||||
|
|
||||||
|
use app\api\common\Response;
|
||||||
|
use app\note\controller\BaseController;
|
||||||
|
use app\note\service\AiService;
|
||||||
|
use app\note\service\NoteService;
|
||||||
|
use think\App;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 笔记 AI 能力控制器
|
||||||
|
*/
|
||||||
|
class Ai extends BaseController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var PlanningService
|
||||||
|
*/
|
||||||
|
protected $aiService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var NoteService
|
||||||
|
*/
|
||||||
|
protected $noteService;
|
||||||
|
|
||||||
|
public function __construct(App $app)
|
||||||
|
{
|
||||||
|
parent::__construct($app);
|
||||||
|
$this->aiService = new AiService();
|
||||||
|
$this->noteService = new NoteService();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发起 AI 总结
|
||||||
|
* POST /note/v1/ai/summary/:id
|
||||||
|
*/
|
||||||
|
public function summary(int $id)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$noteUserId = $this->getCurrentNoteUserId();
|
||||||
|
|
||||||
|
if ($id <= 0) {
|
||||||
|
return Response::error('笔记 ID 不正确', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$note = $this->noteService->getOwnedNote($noteUserId, $id);
|
||||||
|
$data = $this->request->post();
|
||||||
|
$result = $this->aiService->createSummary(
|
||||||
|
$note,
|
||||||
|
(string) ($data['summary_type'] ?? 'brief'),
|
||||||
|
!empty($data['force_refresh'])
|
||||||
|
);
|
||||||
|
|
||||||
|
return Response::success($result, '总结生成成功');
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return Response::error($e->getMessage(), $e->getCode() ?: 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查看 AI 总结结果
|
||||||
|
* GET /note/v1/ai/summary/:id
|
||||||
|
*/
|
||||||
|
public function readSummary(int $id)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$noteUserId = $this->getCurrentNoteUserId();
|
||||||
|
|
||||||
|
if ($id <= 0) {
|
||||||
|
return Response::error('笔记 ID 不正确', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->noteService->getOwnedNote($noteUserId, $id);
|
||||||
|
$result = $this->aiService->getSummary($id);
|
||||||
|
return Response::success($result);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return Response::error($e->getMessage(), $e->getCode() ?: 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace app\note\controller\v1;
|
||||||
|
|
||||||
|
use app\api\common\Response;
|
||||||
|
use app\note\controller\BaseController;
|
||||||
|
use app\note\service\AuthService;
|
||||||
|
use think\App;
|
||||||
|
use think\exception\ValidateException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 笔记小程序认证控制器
|
||||||
|
*/
|
||||||
|
class Auth extends BaseController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var PlanningService
|
||||||
|
*/
|
||||||
|
protected $authService;
|
||||||
|
|
||||||
|
public function __construct(App $app)
|
||||||
|
{
|
||||||
|
parent::__construct($app);
|
||||||
|
$this->authService = new AuthService();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信小程序登录
|
||||||
|
* POST /note/v1/auth/wechat-login
|
||||||
|
*/
|
||||||
|
public function wechatLogin()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$data = $this->request->post();
|
||||||
|
|
||||||
|
validate([
|
||||||
|
'code' => 'require',
|
||||||
|
], [
|
||||||
|
'code.require' => '微信登录 code 不能为空',
|
||||||
|
])->check($data);
|
||||||
|
|
||||||
|
$result = $this->authService->wechatLogin(
|
||||||
|
(string) $data['code'],
|
||||||
|
isset($data['nickname']) ? (string) $data['nickname'] : null,
|
||||||
|
isset($data['avatar_url']) ? (string) $data['avatar_url'] : null
|
||||||
|
);
|
||||||
|
|
||||||
|
return Response::success($result, '登录成功');
|
||||||
|
} catch (ValidateException $e) {
|
||||||
|
return Response::error($e->getMessage(), 400);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return Response::error($e->getMessage(), $e->getCode() ?: 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前小程序用户信息
|
||||||
|
* GET /note/v1/auth/me
|
||||||
|
*/
|
||||||
|
public function me()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$result = $this->authService->getUserInfo($this->getCurrentNoteUserId());
|
||||||
|
return Response::success($result);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return Response::error($e->getMessage(), $e->getCode() ?: 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace app\note\controller\v1;
|
||||||
|
|
||||||
|
use app\api\common\Response;
|
||||||
|
use app\note\controller\BaseController;
|
||||||
|
use app\note\service\PlanningService;
|
||||||
|
use think\App;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 笔记模块元信息控制器
|
||||||
|
*/
|
||||||
|
class Meta extends BaseController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var PlanningService
|
||||||
|
*/
|
||||||
|
protected $planningService;
|
||||||
|
|
||||||
|
public function __construct(App $app)
|
||||||
|
{
|
||||||
|
parent::__construct($app);
|
||||||
|
$this->planningService = new PlanningService();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 note 模块接口规划概览
|
||||||
|
* GET /note/v1/meta/interfaces
|
||||||
|
*/
|
||||||
|
public function interfaces()
|
||||||
|
{
|
||||||
|
return Response::success($this->planningService->getModuleOverview());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
<?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;
|
||||||
|
use think\exception\ValidateException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 笔记控制器
|
||||||
|
*/
|
||||||
|
class Note extends BaseController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var PlanningService
|
||||||
|
*/
|
||||||
|
protected $noteService;
|
||||||
|
|
||||||
|
public function __construct(App $app)
|
||||||
|
{
|
||||||
|
parent::__construct($app);
|
||||||
|
$this->noteService = new NoteService();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建笔记
|
||||||
|
* POST /note/v1/item/create
|
||||||
|
*/
|
||||||
|
public function create()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$noteUserId = $this->getCurrentNoteUserId();
|
||||||
|
$data = $this->request->post();
|
||||||
|
|
||||||
|
validate([
|
||||||
|
'source_type' => 'require|in:text,audio,mix',
|
||||||
|
], [
|
||||||
|
'source_type.require' => '笔记来源类型不能为空',
|
||||||
|
'source_type.in' => '笔记来源类型不正确',
|
||||||
|
])->check($data);
|
||||||
|
|
||||||
|
$result = $this->noteService->create($noteUserId, $data);
|
||||||
|
return Response::success($result, '创建成功');
|
||||||
|
} catch (ValidateException $e) {
|
||||||
|
return Response::error($e->getMessage(), 400);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return Response::error($e->getMessage(), $e->getCode() ?: 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 笔记列表
|
||||||
|
* GET /note/v1/item/list
|
||||||
|
*/
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$result = $this->noteService->getList($this->getCurrentNoteUserId(), $this->request->get());
|
||||||
|
return Response::success($result);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return Response::error($e->getMessage(), $e->getCode() ?: 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 笔记详情
|
||||||
|
* GET /note/v1/item/:id
|
||||||
|
*/
|
||||||
|
public function read(int $id)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$noteUserId = $this->getCurrentNoteUserId();
|
||||||
|
|
||||||
|
if ($id <= 0) {
|
||||||
|
return Response::error('笔记 ID 不正确', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $this->noteService->getDetail($noteUserId, $id);
|
||||||
|
return Response::success($result);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return Response::error($e->getMessage(), $e->getCode() ?: 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新笔记
|
||||||
|
* POST /note/v1/item/update/:id
|
||||||
|
*/
|
||||||
|
public function update(int $id)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$noteUserId = $this->getCurrentNoteUserId();
|
||||||
|
|
||||||
|
if ($id <= 0) {
|
||||||
|
return Response::error('笔记 ID 不正确', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $this->noteService->update($noteUserId, $id, $this->request->post());
|
||||||
|
return Response::success($result, '更新成功');
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return Response::error($e->getMessage(), $e->getCode() ?: 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除笔记
|
||||||
|
* POST /note/v1/item/delete/:id
|
||||||
|
*/
|
||||||
|
public function delete(int $id)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$noteUserId = $this->getCurrentNoteUserId();
|
||||||
|
|
||||||
|
if ($id <= 0) {
|
||||||
|
return Response::error('笔记 ID 不正确', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $this->noteService->delete($noteUserId, $id);
|
||||||
|
return Response::success($result, '删除成功');
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return Response::error($e->getMessage(), $e->getCode() ?: 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存实时转写内容
|
||||||
|
* POST /note/v1/item/transcript/:id
|
||||||
|
*/
|
||||||
|
public function transcript(int $id)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$noteUserId = $this->getCurrentNoteUserId();
|
||||||
|
$data = $this->request->post();
|
||||||
|
|
||||||
|
if ($id <= 0) {
|
||||||
|
return Response::error('笔记 ID 不正确', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
validate([
|
||||||
|
'full_text' => 'require',
|
||||||
|
], [
|
||||||
|
'full_text.require' => '转写文本不能为空',
|
||||||
|
])->check($data);
|
||||||
|
|
||||||
|
$result = $this->noteService->saveTranscript($noteUserId, $id, $data);
|
||||||
|
return Response::success($result, '转写保存成功');
|
||||||
|
} catch (ValidateException $e) {
|
||||||
|
return Response::error($e->getMessage(), 400);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return Response::error($e->getMessage(), $e->getCode() ?: 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace app\note\model;
|
||||||
|
|
||||||
|
use think\Model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* note 模块 AI 总结模型
|
||||||
|
*/
|
||||||
|
class NoteAiSummary extends Model
|
||||||
|
{
|
||||||
|
protected $connection = 'dbnote';
|
||||||
|
|
||||||
|
protected $name = 'note_ai_summary';
|
||||||
|
|
||||||
|
protected $pk = 'id';
|
||||||
|
|
||||||
|
protected $autoWriteTimestamp = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取笔记最新总结
|
||||||
|
*
|
||||||
|
* @param int $noteId
|
||||||
|
* @return self|null
|
||||||
|
*/
|
||||||
|
public static function findLatestByNoteId(int $noteId): ?self
|
||||||
|
{
|
||||||
|
return self::where('note_id', $noteId)
|
||||||
|
->order('id', 'desc')
|
||||||
|
->find();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace app\note\model;
|
||||||
|
|
||||||
|
use think\Model;
|
||||||
|
use think\db\Query;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* note 模块笔记主表模型
|
||||||
|
*/
|
||||||
|
class NoteItem extends Model
|
||||||
|
{
|
||||||
|
protected $connection = 'dbnote';
|
||||||
|
|
||||||
|
protected $name = 'note_item';
|
||||||
|
|
||||||
|
protected $pk = 'id';
|
||||||
|
|
||||||
|
protected $autoWriteTimestamp = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建当前用户的基础查询
|
||||||
|
*
|
||||||
|
* @param int $noteUserId
|
||||||
|
* @return Query
|
||||||
|
*/
|
||||||
|
public static function buildUserQuery(int $noteUserId): Query
|
||||||
|
{
|
||||||
|
return self::where('note_user_id', $noteUserId)
|
||||||
|
->where('deleted_at', 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询当前用户拥有的笔记
|
||||||
|
*
|
||||||
|
* @param int $noteUserId
|
||||||
|
* @param int $id
|
||||||
|
* @return self|null
|
||||||
|
*/
|
||||||
|
public static function findOwnedNote(int $noteUserId, int $id): ?self
|
||||||
|
{
|
||||||
|
return self::buildUserQuery($noteUserId)
|
||||||
|
->where('id', $id)
|
||||||
|
->find();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace app\note\model;
|
||||||
|
|
||||||
|
use think\Model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* note 模块实时转写模型
|
||||||
|
*/
|
||||||
|
class NoteTranscript extends Model
|
||||||
|
{
|
||||||
|
protected $connection = 'dbnote';
|
||||||
|
|
||||||
|
protected $name = 'note_transcript';
|
||||||
|
|
||||||
|
protected $pk = 'id';
|
||||||
|
|
||||||
|
protected $autoWriteTimestamp = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查找某条笔记的指定分片
|
||||||
|
*
|
||||||
|
* @param int $noteId
|
||||||
|
* @param int $segmentNo
|
||||||
|
* @return self|null
|
||||||
|
*/
|
||||||
|
public static function findByNoteAndSegment(int $noteId, int $segmentNo): ?self
|
||||||
|
{
|
||||||
|
return self::where('note_id', $noteId)
|
||||||
|
->where('segment_no', $segmentNo)
|
||||||
|
->find();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace app\note\model;
|
||||||
|
|
||||||
|
use think\Model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* note 模块小程序用户模型
|
||||||
|
*/
|
||||||
|
class NoteUser extends Model
|
||||||
|
{
|
||||||
|
protected $connection = 'dbnote';
|
||||||
|
|
||||||
|
protected $name = 'note_user';
|
||||||
|
|
||||||
|
protected $pk = 'id';
|
||||||
|
|
||||||
|
protected $autoWriteTimestamp = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据 openid 查找用户
|
||||||
|
*
|
||||||
|
* @param string $openid
|
||||||
|
* @return self|null
|
||||||
|
*/
|
||||||
|
public static function findByOpenid(string $openid): ?self
|
||||||
|
{
|
||||||
|
return self::where('openid', $openid)
|
||||||
|
->where('deleted_at', 0)
|
||||||
|
->find();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据主键查找用户
|
||||||
|
*
|
||||||
|
* @param int $id
|
||||||
|
* @return self|null
|
||||||
|
*/
|
||||||
|
public static function findActiveById(int $id): ?self
|
||||||
|
{
|
||||||
|
return self::where('id', $id)
|
||||||
|
->where('deleted_at', 0)
|
||||||
|
->find();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use think\facade\Route;
|
||||||
|
use app\note\controller\v1\Ai;
|
||||||
|
use app\note\controller\v1\Auth;
|
||||||
|
use app\note\controller\v1\Meta;
|
||||||
|
use app\note\controller\v1\Note;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* note 应用路由
|
||||||
|
*
|
||||||
|
* 由于项目启用了 think-multi-app,
|
||||||
|
* `/note/...` 会优先进入 note 应用,
|
||||||
|
* 因此 note 模块自己的接口必须定义在 `app/note/route/app.php` 下,
|
||||||
|
* 路由前缀应从 `v1/...` 开始,而不是再重复写 `note/...`。
|
||||||
|
*/
|
||||||
|
|
||||||
|
// v1 笔记模块接口规划(公开)
|
||||||
|
Route::get('v1/meta/interfaces', [Meta::class, 'interfaces']);
|
||||||
|
Route::post('v1/auth/wechat-login', [Auth::class, 'wechatLogin']);
|
||||||
|
|
||||||
|
// v1 笔记模块接口(需登录)
|
||||||
|
Route::group('v1', function () {
|
||||||
|
Route::get('auth/me', [Auth::class, 'me']);
|
||||||
|
|
||||||
|
Route::post('item/create', [Note::class, 'create']);
|
||||||
|
Route::get('item/list', [Note::class, 'index']);
|
||||||
|
Route::get('item/:id', [Note::class, 'read']);
|
||||||
|
Route::post('item/update/:id', [Note::class, 'update']);
|
||||||
|
Route::post('item/delete/:id', [Note::class, 'delete']);
|
||||||
|
Route::post('item/transcript/:id', [Note::class, 'transcript']);
|
||||||
|
|
||||||
|
Route::post('ai/summary/:id', [Ai::class, 'summary']);
|
||||||
|
Route::get('ai/summary/:id', [Ai::class, 'readSummary']);
|
||||||
|
})->middleware(\app\api\middleware\Auth::class);
|
||||||
@@ -0,0 +1,228 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace app\note\service;
|
||||||
|
|
||||||
|
use app\note\model\NoteAiSummary;
|
||||||
|
use app\note\model\NoteItem;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* note 模块 AI 总结服务
|
||||||
|
*
|
||||||
|
* 当前先提供规则版总结,保证接口可用。
|
||||||
|
* 后续如需接入大模型,可在本服务内部替换实现,不影响控制器接口。
|
||||||
|
*/
|
||||||
|
class AiService
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 生成或刷新总结
|
||||||
|
*
|
||||||
|
* @param NoteItem $note
|
||||||
|
* @param string $summaryType
|
||||||
|
* @param bool $forceRefresh
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function createSummary(NoteItem $note, string $summaryType = 'brief', bool $forceRefresh = false): array
|
||||||
|
{
|
||||||
|
$summaryType = in_array($summaryType, ['brief', 'outline', 'todo'], true) ? $summaryType : 'brief';
|
||||||
|
$existing = NoteAiSummary::findLatestByNoteId((int) $note->id);
|
||||||
|
|
||||||
|
if ($existing && !$forceRefresh && $existing->status === 'success') {
|
||||||
|
return $this->formatSummary($existing);
|
||||||
|
}
|
||||||
|
|
||||||
|
$sourceText = trim((string) $note->content);
|
||||||
|
if ($sourceText === '') {
|
||||||
|
$sourceText = trim((string) $note->transcript_text);
|
||||||
|
}
|
||||||
|
if ($sourceText === '') {
|
||||||
|
throw new \Exception('笔记内容为空,无法生成总结', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$generated = $this->generateSummaryPayload($sourceText, $summaryType);
|
||||||
|
$now = time();
|
||||||
|
|
||||||
|
$summary = $existing ?: new NoteAiSummary();
|
||||||
|
if (!$existing) {
|
||||||
|
$summary->note_id = (int) $note->id;
|
||||||
|
$summary->created_at = $now;
|
||||||
|
}
|
||||||
|
|
||||||
|
$summary->summary_type = $summaryType;
|
||||||
|
$summary->summary_text = $generated['summary_text'];
|
||||||
|
$summary->todo_list = json_encode($generated['todo_list'], JSON_UNESCAPED_UNICODE);
|
||||||
|
$summary->keywords = json_encode($generated['keywords'], JSON_UNESCAPED_UNICODE);
|
||||||
|
$summary->status = 'success';
|
||||||
|
$summary->error_message = '';
|
||||||
|
$summary->updated_at = $now;
|
||||||
|
$summary->save();
|
||||||
|
|
||||||
|
$note->summary_status = 'success';
|
||||||
|
$note->updated_at = $now;
|
||||||
|
$note->save();
|
||||||
|
|
||||||
|
return $this->formatSummary($summary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取总结
|
||||||
|
*
|
||||||
|
* @param int $noteId
|
||||||
|
* @return array
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
public function getSummary(int $noteId): array
|
||||||
|
{
|
||||||
|
$summary = NoteAiSummary::findLatestByNoteId($noteId);
|
||||||
|
if (!$summary) {
|
||||||
|
throw new \Exception('该笔记暂无总结结果', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->formatSummary($summary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成规则版总结
|
||||||
|
*
|
||||||
|
* @param string $text
|
||||||
|
* @param string $summaryType
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private function generateSummaryPayload(string $text, string $summaryType): array
|
||||||
|
{
|
||||||
|
$sentences = $this->splitSentences($text);
|
||||||
|
$summarySentences = array_slice($sentences, 0, $summaryType === 'outline' ? 5 : 3);
|
||||||
|
$summaryText = implode("\n", array_map(function ($item, $index) use ($summaryType) {
|
||||||
|
if ($summaryType === 'outline') {
|
||||||
|
return sprintf('%d. %s', $index + 1, $item);
|
||||||
|
}
|
||||||
|
return $item;
|
||||||
|
}, $summarySentences, array_keys($summarySentences)));
|
||||||
|
|
||||||
|
$todoList = $this->extractTodoList($text);
|
||||||
|
$keywords = $this->extractKeywords($text);
|
||||||
|
|
||||||
|
if ($summaryType === 'todo' && !empty($todoList)) {
|
||||||
|
$summaryText = "待办事项:\n" . implode("\n", array_map(function ($item, $index) {
|
||||||
|
return sprintf('%d. %s', $index + 1, $item);
|
||||||
|
}, $todoList, array_keys($todoList)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'summary_text' => $summaryText !== '' ? $summaryText : mb_substr($text, 0, 200),
|
||||||
|
'todo_list' => $todoList,
|
||||||
|
'keywords' => $keywords,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切分句子
|
||||||
|
*
|
||||||
|
* @param string $text
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private function splitSentences(string $text): array
|
||||||
|
{
|
||||||
|
$normalized = preg_replace('/\s+/', ' ', trim($text));
|
||||||
|
$parts = preg_split('/[。!?!?;;\n\r]+/u', (string) $normalized);
|
||||||
|
$parts = array_values(array_filter(array_map('trim', $parts), function ($item) {
|
||||||
|
return $item !== '';
|
||||||
|
}));
|
||||||
|
|
||||||
|
return empty($parts) ? [mb_substr($normalized, 0, 200)] : $parts;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提取待办
|
||||||
|
*
|
||||||
|
* @param string $text
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private function extractTodoList(string $text): array
|
||||||
|
{
|
||||||
|
$lines = preg_split('/[\n\r]+/u', $text);
|
||||||
|
$todoList = [];
|
||||||
|
|
||||||
|
foreach ($lines as $line) {
|
||||||
|
$line = trim($line);
|
||||||
|
if ($line === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (preg_match('/^(待办|todo|todo:|TODO|TODO:|需要|安排|跟进|完成|处理)/u', $line)) {
|
||||||
|
$todoList[] = $line;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($todoList)) {
|
||||||
|
return array_slice(array_values(array_unique($todoList)), 0, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
$sentences = $this->splitSentences($text);
|
||||||
|
$fallback = [];
|
||||||
|
foreach ($sentences as $sentence) {
|
||||||
|
if (preg_match('/(需要|安排|跟进|完成|处理|确认|整理|联系)/u', $sentence)) {
|
||||||
|
$fallback[] = $sentence;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_slice(array_values(array_unique($fallback)), 0, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提取关键词
|
||||||
|
*
|
||||||
|
* @param string $text
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private function extractKeywords(string $text): array
|
||||||
|
{
|
||||||
|
preg_match_all('/[\x{4e00}-\x{9fa5}A-Za-z0-9]{2,20}/u', $text, $matches);
|
||||||
|
$words = $matches[0] ?? [];
|
||||||
|
$stopWords = ['我们', '你们', '他们', '这个', '那个', '然后', '以及', '因为', '所以', '可以', '进行', '一个', '没有', '已经', '需要', '今天', '目前', '如果', '就是'];
|
||||||
|
$countMap = [];
|
||||||
|
|
||||||
|
foreach ($words as $word) {
|
||||||
|
if (in_array($word, $stopWords, true)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$countMap[$word] = ($countMap[$word] ?? 0) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
arsort($countMap);
|
||||||
|
return array_slice(array_keys($countMap), 0, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化总结结果
|
||||||
|
*
|
||||||
|
* @param NoteAiSummary $summary
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private function formatSummary(NoteAiSummary $summary): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'summary_id' => (int) $summary->id,
|
||||||
|
'note_id' => (int) $summary->note_id,
|
||||||
|
'summary_type' => (string) $summary->summary_type,
|
||||||
|
'summary_text' => (string) $summary->summary_text,
|
||||||
|
'todo_list' => $this->decodeJsonList((string) $summary->todo_list),
|
||||||
|
'keywords' => $this->decodeJsonList((string) $summary->keywords),
|
||||||
|
'status' => (string) $summary->status,
|
||||||
|
'error_message' => (string) $summary->error_message,
|
||||||
|
'created_at' => (int) $summary->created_at,
|
||||||
|
'updated_at' => (int) $summary->updated_at,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析 JSON 列表
|
||||||
|
*
|
||||||
|
* @param string $value
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private function decodeJsonList(string $value): array
|
||||||
|
{
|
||||||
|
$decoded = json_decode($value, true);
|
||||||
|
return is_array($decoded) ? $decoded : [];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace app\note\service;
|
||||||
|
|
||||||
|
use app\api\common\Jwt;
|
||||||
|
use app\note\model\NoteUser;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* note 模块认证服务
|
||||||
|
*/
|
||||||
|
class AuthService
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 微信小程序登录
|
||||||
|
*
|
||||||
|
* @param string $code
|
||||||
|
* @param string|null $nickname
|
||||||
|
* @param string|null $avatarUrl
|
||||||
|
* @return array
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
public function wechatLogin(string $code, ?string $nickname = null, ?string $avatarUrl = null): array
|
||||||
|
{
|
||||||
|
$session = $this->fetchWechatSession($code);
|
||||||
|
$openid = trim((string) ($session['openid'] ?? ''));
|
||||||
|
|
||||||
|
if ($openid === '') {
|
||||||
|
throw new \Exception('微信登录失败,未获取到 openid', 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
$now = time();
|
||||||
|
$user = NoteUser::findByOpenid($openid);
|
||||||
|
$isNewUser = false;
|
||||||
|
|
||||||
|
if (!$user) {
|
||||||
|
$user = new NoteUser();
|
||||||
|
$user->openid = $openid;
|
||||||
|
$user->created_at = $now;
|
||||||
|
$isNewUser = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$isNewUser && (int) $user->status === 0) {
|
||||||
|
throw new \Exception('小程序账号已被禁用', 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
$user->unionid = (string) ($session['unionid'] ?? '');
|
||||||
|
$user->session_key = (string) ($session['session_key'] ?? '');
|
||||||
|
$user->nickname = $nickname !== null ? trim($nickname) : (string) $user->nickname;
|
||||||
|
$user->avatar_url = $avatarUrl !== null ? trim($avatarUrl) : (string) $user->avatar_url;
|
||||||
|
$user->status = 1;
|
||||||
|
$user->last_login_ip = (string) request()->ip();
|
||||||
|
$user->last_login_time = $now;
|
||||||
|
$user->updated_at = $now;
|
||||||
|
$user->deleted_at = 0;
|
||||||
|
|
||||||
|
if (!$user->save()) {
|
||||||
|
throw new \Exception('小程序用户登录保存失败', 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
$token = Jwt::encode([
|
||||||
|
'userid' => (int) $user->id,
|
||||||
|
'guard' => 'note',
|
||||||
|
'openid' => $user->openid,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$refreshToken = Jwt::encode([
|
||||||
|
'userid' => (int) $user->id,
|
||||||
|
'guard' => 'note',
|
||||||
|
'type' => 'refresh',
|
||||||
|
'openid' => $user->openid,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'token' => $token,
|
||||||
|
'refresh_token' => $refreshToken,
|
||||||
|
'expires_in' => config('jwt.expire', 604800),
|
||||||
|
'user' => [
|
||||||
|
'id' => (int) $user->id,
|
||||||
|
'member_id' => (int) ($user->member_id ?? 0),
|
||||||
|
'openid' => (string) $user->openid,
|
||||||
|
'nickname' => (string) $user->nickname,
|
||||||
|
'avatar_url' => (string) $user->avatar_url,
|
||||||
|
'mobile' => (string) $user->mobile,
|
||||||
|
'is_new_user' => $isNewUser,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前小程序用户信息
|
||||||
|
*
|
||||||
|
* @param int $noteUserId
|
||||||
|
* @return array
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
public function getUserInfo(int $noteUserId): array
|
||||||
|
{
|
||||||
|
$user = NoteUser::findActiveById($noteUserId);
|
||||||
|
if (!$user) {
|
||||||
|
throw new \Exception('小程序用户不存在', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'id' => (int) $user->id,
|
||||||
|
'member_id' => (int) ($user->member_id ?? 0),
|
||||||
|
'openid' => (string) $user->openid,
|
||||||
|
'nickname' => (string) $user->nickname,
|
||||||
|
'avatar_url' => (string) $user->avatar_url,
|
||||||
|
'mobile' => (string) $user->mobile,
|
||||||
|
'status' => (int) $user->status,
|
||||||
|
'last_login_time' => (int) $user->last_login_time,
|
||||||
|
'created_at' => (int) $user->created_at,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 请求微信 code2Session
|
||||||
|
*
|
||||||
|
* @param string $code
|
||||||
|
* @return array
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
private function fetchWechatSession(string $code): array
|
||||||
|
{
|
||||||
|
$appId = (string) env('WECHAT_MINI_APPID', '');
|
||||||
|
$appSecret = (string) env('WECHAT_MINI_SECRET', '');
|
||||||
|
|
||||||
|
if ($appId === '' || $appSecret === '') {
|
||||||
|
throw new \Exception('缺少微信小程序配置 WECHAT_MINI_APPID / WECHAT_MINI_SECRET', 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
$url = sprintf(
|
||||||
|
'https://api.weixin.qq.com/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code',
|
||||||
|
urlencode($appId),
|
||||||
|
urlencode($appSecret),
|
||||||
|
urlencode($code)
|
||||||
|
);
|
||||||
|
|
||||||
|
$ch = curl_init($url);
|
||||||
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
|
||||||
|
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
|
||||||
|
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
|
||||||
|
$response = curl_exec($ch);
|
||||||
|
$error = curl_error($ch);
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
if ($response === false || $response === '' || $error) {
|
||||||
|
throw new \Exception('微信登录请求失败', 502);
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = json_decode($response, true);
|
||||||
|
if (!is_array($data)) {
|
||||||
|
throw new \Exception('微信登录响应解析失败', 502);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($data['errcode'])) {
|
||||||
|
throw new \Exception(sprintf('微信登录失败:%s', (string) ($data['errmsg'] ?? 'unknown error')), 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,294 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace app\note\service;
|
||||||
|
|
||||||
|
use app\note\model\NoteAiSummary;
|
||||||
|
use app\note\model\NoteItem;
|
||||||
|
use app\note\model\NoteTranscript;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* note 模块笔记服务
|
||||||
|
*/
|
||||||
|
class NoteService
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 创建笔记
|
||||||
|
*
|
||||||
|
* @param int $noteUserId
|
||||||
|
* @param array $data
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function create(int $noteUserId, array $data): array
|
||||||
|
{
|
||||||
|
$now = time();
|
||||||
|
$note = new NoteItem();
|
||||||
|
$note->note_user_id = $noteUserId;
|
||||||
|
$note->title = $this->normalizeTitle(
|
||||||
|
(string) ($data['title'] ?? ''),
|
||||||
|
(string) ($data['content'] ?? '')
|
||||||
|
);
|
||||||
|
$note->content = (string) ($data['content'] ?? '');
|
||||||
|
$note->transcript_text = '';
|
||||||
|
$note->source_type = (string) ($data['source_type'] ?? 'text');
|
||||||
|
$note->status = (string) ($data['status'] ?? 'draft');
|
||||||
|
$note->audio_duration_ms = (int) ($data['audio_duration_ms'] ?? 0);
|
||||||
|
$note->summary_status = 'none';
|
||||||
|
$note->last_transcript_time = 0;
|
||||||
|
$note->created_at = $now;
|
||||||
|
$note->updated_at = $now;
|
||||||
|
$note->deleted_at = 0;
|
||||||
|
$note->save();
|
||||||
|
|
||||||
|
return $this->formatNoteItem($note);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取笔记列表
|
||||||
|
*
|
||||||
|
* @param int $noteUserId
|
||||||
|
* @param array $params
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getList(int $noteUserId, array $params): array
|
||||||
|
{
|
||||||
|
$page = max(1, (int) ($params['page'] ?? 1));
|
||||||
|
$pageSize = max(1, min(100, (int) ($params['page_size'] ?? 10)));
|
||||||
|
$keyword = trim((string) ($params['keyword'] ?? ''));
|
||||||
|
$status = trim((string) ($params['status'] ?? ''));
|
||||||
|
|
||||||
|
$query = NoteItem::buildUserQuery($noteUserId);
|
||||||
|
|
||||||
|
if ($status !== '') {
|
||||||
|
$query->where('status', $status);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($keyword !== '') {
|
||||||
|
$query->where(function ($subQuery) use ($keyword) {
|
||||||
|
$subQuery->whereLike('title', '%' . $keyword . '%')
|
||||||
|
->whereOrLike('content', '%' . $keyword . '%')
|
||||||
|
->whereOrLike('transcript_text', '%' . $keyword . '%');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$total = (int) $query->count();
|
||||||
|
$list = $query->order('id', 'desc')
|
||||||
|
->page($page, $pageSize)
|
||||||
|
->select();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'list' => array_map(function ($item) {
|
||||||
|
return $this->formatNoteItem($item);
|
||||||
|
}, $list->all()),
|
||||||
|
'total' => $total,
|
||||||
|
'page' => $page,
|
||||||
|
'page_size' => $pageSize,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取笔记详情
|
||||||
|
*
|
||||||
|
* @param int $noteUserId
|
||||||
|
* @param int $id
|
||||||
|
* @return array
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
public function getDetail(int $noteUserId, int $id): array
|
||||||
|
{
|
||||||
|
$note = $this->getOwnedNote($noteUserId, $id);
|
||||||
|
$summary = NoteAiSummary::findLatestByNoteId($id);
|
||||||
|
|
||||||
|
$result = $this->formatNoteItem($note);
|
||||||
|
$result['summary'] = $summary ? [
|
||||||
|
'summary_id' => (int) $summary->id,
|
||||||
|
'summary_type' => (string) $summary->summary_type,
|
||||||
|
'summary_text' => (string) $summary->summary_text,
|
||||||
|
'todo_list' => $this->decodeJsonList((string) $summary->todo_list),
|
||||||
|
'keywords' => $this->decodeJsonList((string) $summary->keywords),
|
||||||
|
'status' => (string) $summary->status,
|
||||||
|
] : null;
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新笔记
|
||||||
|
*
|
||||||
|
* @param int $noteUserId
|
||||||
|
* @param int $id
|
||||||
|
* @param array $data
|
||||||
|
* @return array
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
public function update(int $noteUserId, int $id, array $data): array
|
||||||
|
{
|
||||||
|
$note = $this->getOwnedNote($noteUserId, $id);
|
||||||
|
|
||||||
|
if (array_key_exists('title', $data)) {
|
||||||
|
$note->title = $this->normalizeTitle((string) $data['title'], (string) ($data['content'] ?? $note->content));
|
||||||
|
}
|
||||||
|
if (array_key_exists('content', $data)) {
|
||||||
|
$note->content = (string) $data['content'];
|
||||||
|
if (trim((string) $note->title) === '') {
|
||||||
|
$note->title = $this->normalizeTitle('', (string) $note->content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (array_key_exists('status', $data) && $data['status'] !== '') {
|
||||||
|
$note->status = (string) $data['status'];
|
||||||
|
}
|
||||||
|
if (array_key_exists('audio_duration_ms', $data)) {
|
||||||
|
$note->audio_duration_ms = max(0, (int) $data['audio_duration_ms']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$note->updated_at = time();
|
||||||
|
$note->save();
|
||||||
|
|
||||||
|
return $this->formatNoteItem($note);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除笔记
|
||||||
|
*
|
||||||
|
* @param int $noteUserId
|
||||||
|
* @param int $id
|
||||||
|
* @return array
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
public function delete(int $noteUserId, int $id): array
|
||||||
|
{
|
||||||
|
$note = $this->getOwnedNote($noteUserId, $id);
|
||||||
|
$note->deleted_at = time();
|
||||||
|
$note->updated_at = time();
|
||||||
|
$note->save();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'deleted' => true,
|
||||||
|
'id' => $id,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存转写内容
|
||||||
|
*
|
||||||
|
* @param int $noteUserId
|
||||||
|
* @param int $id
|
||||||
|
* @param array $data
|
||||||
|
* @return array
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
public function saveTranscript(int $noteUserId, int $id, array $data): array
|
||||||
|
{
|
||||||
|
$note = $this->getOwnedNote($noteUserId, $id);
|
||||||
|
$segmentNo = max(0, (int) ($data['segment_no'] ?? 0));
|
||||||
|
$now = time();
|
||||||
|
|
||||||
|
$transcript = NoteTranscript::findByNoteAndSegment($id, $segmentNo);
|
||||||
|
if (!$transcript) {
|
||||||
|
$transcript = new NoteTranscript();
|
||||||
|
$transcript->note_id = $id;
|
||||||
|
$transcript->segment_no = $segmentNo;
|
||||||
|
$transcript->created_at = $now;
|
||||||
|
}
|
||||||
|
|
||||||
|
$transcript->segment_text = (string) ($data['segment_text'] ?? '');
|
||||||
|
$transcript->full_text = (string) ($data['full_text'] ?? '');
|
||||||
|
$transcript->is_final = empty($data['is_final']) ? 0 : 1;
|
||||||
|
$transcript->audio_duration_ms = max(0, (int) ($data['audio_duration_ms'] ?? 0));
|
||||||
|
$transcript->save();
|
||||||
|
|
||||||
|
$note->transcript_text = $transcript->full_text;
|
||||||
|
$note->audio_duration_ms = max($note->audio_duration_ms, (int) $transcript->audio_duration_ms);
|
||||||
|
$note->last_transcript_time = $now;
|
||||||
|
$note->updated_at = $now;
|
||||||
|
if ($note->title === '') {
|
||||||
|
$note->title = $this->normalizeTitle('', $note->transcript_text);
|
||||||
|
}
|
||||||
|
$note->save();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'note_id' => $id,
|
||||||
|
'segment_no' => $segmentNo,
|
||||||
|
'is_final' => (int) $transcript->is_final,
|
||||||
|
'transcript_text' => (string) $note->transcript_text,
|
||||||
|
'audio_duration_ms' => (int) $note->audio_duration_ms,
|
||||||
|
'updated_at' => (int) $note->updated_at,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前用户拥有的笔记
|
||||||
|
*
|
||||||
|
* @param int $noteUserId
|
||||||
|
* @param int $id
|
||||||
|
* @return NoteItem
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
public function getOwnedNote(int $noteUserId, int $id): NoteItem
|
||||||
|
{
|
||||||
|
$note = NoteItem::findOwnedNote($noteUserId, $id);
|
||||||
|
if (!$note) {
|
||||||
|
throw new \Exception('笔记不存在', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $note;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化笔记返回
|
||||||
|
*
|
||||||
|
* @param NoteItem $note
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private function formatNoteItem(NoteItem $note): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => (int) $note->id,
|
||||||
|
'note_user_id' => (int) $note->note_user_id,
|
||||||
|
'title' => (string) $note->title,
|
||||||
|
'content' => (string) $note->content,
|
||||||
|
'transcript_text' => (string) $note->transcript_text,
|
||||||
|
'source_type' => (string) $note->source_type,
|
||||||
|
'status' => (string) $note->status,
|
||||||
|
'audio_duration_ms' => (int) $note->audio_duration_ms,
|
||||||
|
'summary_status' => (string) $note->summary_status,
|
||||||
|
'last_transcript_time' => (int) $note->last_transcript_time,
|
||||||
|
'created_at' => (int) $note->created_at,
|
||||||
|
'updated_at' => (int) $note->updated_at,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 规范化标题
|
||||||
|
*
|
||||||
|
* @param string $title
|
||||||
|
* @param string $fallback
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private function normalizeTitle(string $title, string $fallback): string
|
||||||
|
{
|
||||||
|
$title = trim($title);
|
||||||
|
if ($title !== '') {
|
||||||
|
return mb_substr($title, 0, 255);
|
||||||
|
}
|
||||||
|
|
||||||
|
$fallback = trim(preg_replace('/\s+/', ' ', $fallback));
|
||||||
|
if ($fallback === '') {
|
||||||
|
return '未命名笔记';
|
||||||
|
}
|
||||||
|
|
||||||
|
return mb_substr($fallback, 0, 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析 JSON 列表
|
||||||
|
*
|
||||||
|
* @param string $value
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private function decodeJsonList(string $value): array
|
||||||
|
{
|
||||||
|
$decoded = json_decode($value, true);
|
||||||
|
return is_array($decoded) ? $decoded : [];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,206 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace app\note\service;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 笔记小程序接口规划服务
|
||||||
|
*
|
||||||
|
* 说明:
|
||||||
|
* - 当前阶段先在独立 `note` 模块沉淀接口规划与模块骨架
|
||||||
|
* - 暂未落真实业务逻辑,接口先返回规划结构与字段建议
|
||||||
|
* - 后续确定表结构后,可直接在 `app/note` 模块内继续补 Model / Service / Controller
|
||||||
|
*/
|
||||||
|
class PlanningService
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 获取整个 note 模块接口概览。
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getModuleOverview(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'module' => 'note',
|
||||||
|
'version' => 'v1',
|
||||||
|
'description' => '笔记小程序独立模块,负责小程序登录、笔记、转写、AI 总结能力',
|
||||||
|
'business_blocks' => [
|
||||||
|
[
|
||||||
|
'name' => '小程序用户登录',
|
||||||
|
'goal' => '基于微信 code 登录,换取 openid / session_key,并建立本地用户映射与 JWT 登录态',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => '笔记创建与实时录音转文字',
|
||||||
|
'goal' => '支持纯文本笔记、语音笔记、实时转写文本落地、后续编辑与查看',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'AI 总结',
|
||||||
|
'goal' => '对单条笔记或一次录音转写结果生成结构化摘要、待办、标签等内容',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'routes' => [
|
||||||
|
['method' => 'GET', 'path' => '/note/v1/meta/interfaces', 'desc' => '获取 note 模块接口规划'],
|
||||||
|
['method' => 'POST', 'path' => '/note/v1/auth/wechat-login', 'desc' => '微信小程序登录'],
|
||||||
|
['method' => 'GET', 'path' => '/note/v1/auth/me', 'desc' => '获取当前小程序用户信息'],
|
||||||
|
['method' => 'POST', 'path' => '/note/v1/item/create', 'desc' => '创建笔记'],
|
||||||
|
['method' => 'GET', 'path' => '/note/v1/item/list', 'desc' => '笔记列表'],
|
||||||
|
['method' => 'GET', 'path' => '/note/v1/item/: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/transcript/:id', 'desc' => '保存实时转写内容'],
|
||||||
|
['method' => 'POST', 'path' => '/note/v1/ai/summary/:id', 'desc' => '发起 AI 总结'],
|
||||||
|
['method' => 'GET', 'path' => '/note/v1/ai/summary/:id', 'desc' => '查看 AI 总结结果'],
|
||||||
|
],
|
||||||
|
'suggested_tables' => [
|
||||||
|
'note_user',
|
||||||
|
'note_item',
|
||||||
|
'note_transcript',
|
||||||
|
'note_ai_summary',
|
||||||
|
],
|
||||||
|
'development_priority' => [
|
||||||
|
'1. 先落小程序登录,打通微信 openid 与 JWT',
|
||||||
|
'2. 再落笔记主表与笔记 CRUD',
|
||||||
|
'3. 再补实时转写保存接口',
|
||||||
|
'4. 最后接入 AI 总结任务与结果存储',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 小程序登录接口规划。
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getWechatLoginSpec(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'name' => '微信小程序登录',
|
||||||
|
'route' => 'POST /note/v1/auth/wechat-login',
|
||||||
|
'request' => [
|
||||||
|
'code' => '微信 wx.login 获取的临时 code,必填',
|
||||||
|
'nickname' => '用户昵称,可选',
|
||||||
|
'avatar_url' => '头像地址,可选',
|
||||||
|
'device' => '设备信息,可选',
|
||||||
|
],
|
||||||
|
'response' => [
|
||||||
|
'token' => '沿用 tp 现有 JWT 登录态',
|
||||||
|
'refresh_token' => '刷新令牌',
|
||||||
|
'expires_in' => '过期时间',
|
||||||
|
'user' => [
|
||||||
|
'id' => 'note_user 主键 ID',
|
||||||
|
'member_id' => '关联 tp 现有 member 用户 ID,可为空',
|
||||||
|
'openid' => '微信 openid',
|
||||||
|
'nickname' => '昵称',
|
||||||
|
'avatar_url' => '头像',
|
||||||
|
'is_new_user' => '是否首次登录',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'depends_on' => [
|
||||||
|
'微信 code2Session',
|
||||||
|
'note_user 表',
|
||||||
|
'是否与现有 member 表映射的业务策略',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 笔记接口规划。
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getNoteSpecs(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'create' => [
|
||||||
|
'route' => 'POST /note/v1/item/create',
|
||||||
|
'request' => [
|
||||||
|
'title' => '标题,可选',
|
||||||
|
'content' => '正文,可选',
|
||||||
|
'source_type' => '来源类型:text / audio / mix',
|
||||||
|
'audio_duration_ms' => '录音时长,可选',
|
||||||
|
'status' => '状态:draft / completed,默认 draft',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'list' => [
|
||||||
|
'route' => 'GET /note/v1/item/list',
|
||||||
|
'request' => [
|
||||||
|
'page' => '页码,默认 1',
|
||||||
|
'page_size' => '每页数量,默认 10',
|
||||||
|
'keyword' => '标题/正文搜索,可选',
|
||||||
|
'status' => 'draft / completed,可选',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'detail' => [
|
||||||
|
'route' => 'GET /note/v1/item/:id',
|
||||||
|
'response' => [
|
||||||
|
'id' => '笔记 ID',
|
||||||
|
'title' => '标题',
|
||||||
|
'content' => '正文',
|
||||||
|
'transcript_text' => '转写文本',
|
||||||
|
'audio_duration_ms' => '录音时长',
|
||||||
|
'summary' => 'AI 总结结果,可选',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'update' => [
|
||||||
|
'route' => 'POST /note/v1/item/update/:id',
|
||||||
|
],
|
||||||
|
'delete' => [
|
||||||
|
'route' => 'POST /note/v1/item/delete/:id',
|
||||||
|
],
|
||||||
|
'transcript' => [
|
||||||
|
'route' => 'POST /note/v1/item/transcript/:id',
|
||||||
|
'request' => [
|
||||||
|
'segment_no' => '分片序号',
|
||||||
|
'segment_text' => '本次实时转写文本,可选',
|
||||||
|
'full_text' => '当前整段累计文本,必填',
|
||||||
|
'is_final' => '是否最终片段',
|
||||||
|
'audio_duration_ms' => '当前累计录音时长,可选',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI 总结接口规划。
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getAiSpecs(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'create_summary' => [
|
||||||
|
'route' => 'POST /note/v1/ai/summary/:id',
|
||||||
|
'request' => [
|
||||||
|
'summary_type' => '摘要类型:brief / outline / todo',
|
||||||
|
'force_refresh' => '是否强制重新生成,可选',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'read_summary' => [
|
||||||
|
'route' => 'GET /note/v1/ai/summary/:id',
|
||||||
|
'response' => [
|
||||||
|
'summary_id' => '总结记录 ID',
|
||||||
|
'summary_type' => '摘要类型',
|
||||||
|
'summary_text' => '总结内容',
|
||||||
|
'todo_list' => '待办列表,可选',
|
||||||
|
'keywords' => '关键词列表,可选',
|
||||||
|
'status' => '状态',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 返回接口尚未落库的统一说明。
|
||||||
|
*
|
||||||
|
* @param array $spec
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function buildPendingImplementationPayload(array $spec): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'implemented' => false,
|
||||||
|
'reason' => '当前阶段仅完成 note 独立模块接口规划骨架,数据库表已设计,真实业务逻辑待继续实现',
|
||||||
|
'spec' => $spec,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -62,6 +62,27 @@ return [
|
|||||||
'fields_cache' => false,
|
'fields_cache' => false,
|
||||||
],
|
],
|
||||||
|
|
||||||
|
// 笔记数据库(独立 note 模块)
|
||||||
|
'dbnote' => [
|
||||||
|
'type' => 'mysql',
|
||||||
|
'hostname' => env('DB_NOTE_HOSTNAME', '127.0.0.1'),
|
||||||
|
'database' => env('DB_NOTE_DATABASE', 'tp'),
|
||||||
|
'username' => env('DB_NOTE_USERNAME', 'tp'),
|
||||||
|
'password' => env('DB_NOTE_PASSWORD', ''),
|
||||||
|
'hostport' => env('DB_NOTE_HOSTPORT', '3306'),
|
||||||
|
'params' => [],
|
||||||
|
'charset' => env('DB_NOTE_CHARSET', 'utf8mb4'),
|
||||||
|
'prefix' => '',
|
||||||
|
'deploy' => 0,
|
||||||
|
'rw_separate' => false,
|
||||||
|
'master_num' => 1,
|
||||||
|
'slave_no' => '',
|
||||||
|
'fields_strict' => false,
|
||||||
|
'break_reconnect' => false,
|
||||||
|
'trigger_sql' => env('APP_DEBUG', true),
|
||||||
|
'fields_cache' => false,
|
||||||
|
],
|
||||||
|
|
||||||
// 抖音业务数据库(用于首页发布作品统计分表)
|
// 抖音业务数据库(用于首页发布作品统计分表)
|
||||||
'dbdouying' => [
|
'dbdouying' => [
|
||||||
'type' => 'mysql',
|
'type' => 'mysql',
|
||||||
|
|||||||
@@ -21,3 +21,85 @@ CREATE TABLE IF NOT EXISTS `user` (
|
|||||||
INSERT INTO `user` (`username`, `password`, `nickname`, `email`, `status`) VALUES
|
INSERT INTO `user` (`username`, `password`, `nickname`, `email`, `status`) VALUES
|
||||||
('demo', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', '演示用户', 'demo@example.com', 1);
|
('demo', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', '演示用户', 'demo@example.com', 1);
|
||||||
-- 密码为: password
|
-- 密码为: password
|
||||||
|
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- note 独立模块:笔记小程序表结构
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
-- 小程序用户表
|
||||||
|
CREATE TABLE IF NOT EXISTS `note_user` (
|
||||||
|
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||||
|
`member_id` int(11) unsigned DEFAULT NULL COMMENT '关联 tp 现有 member.userid,可为空',
|
||||||
|
`openid` varchar(64) NOT NULL DEFAULT '' COMMENT '微信小程序 openid',
|
||||||
|
`unionid` varchar(64) NOT NULL DEFAULT '' COMMENT '微信 unionid',
|
||||||
|
`session_key` varchar(128) NOT NULL DEFAULT '' COMMENT '微信 session_key',
|
||||||
|
`nickname` varchar(100) NOT NULL DEFAULT '' COMMENT '用户昵称',
|
||||||
|
`avatar_url` varchar(255) NOT NULL DEFAULT '' COMMENT '头像地址',
|
||||||
|
`mobile` varchar(20) NOT NULL DEFAULT '' COMMENT '手机号',
|
||||||
|
`status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '状态:0禁用 1正常',
|
||||||
|
`last_login_ip` varchar(64) NOT NULL DEFAULT '' COMMENT '最后登录IP',
|
||||||
|
`last_login_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 '更新时间',
|
||||||
|
`deleted_at` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '删除时间,0表示未删除',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uniq_openid` (`openid`),
|
||||||
|
KEY `idx_member_id` (`member_id`),
|
||||||
|
KEY `idx_status` (`status`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='note模块-小程序用户表';
|
||||||
|
|
||||||
|
-- 笔记主表
|
||||||
|
CREATE TABLE IF NOT EXISTS `note_item` (
|
||||||
|
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '笔记ID',
|
||||||
|
`note_user_id` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT 'note_user.id',
|
||||||
|
`title` varchar(255) NOT NULL DEFAULT '' COMMENT '标题',
|
||||||
|
`content` longtext COMMENT '用户整理后的正文',
|
||||||
|
`transcript_text` longtext COMMENT '录音转写累计文本',
|
||||||
|
`source_type` varchar(20) NOT NULL DEFAULT 'text' COMMENT '来源类型:text/audio/mix',
|
||||||
|
`status` varchar(20) NOT NULL DEFAULT 'draft' COMMENT '状态:draft/completed/archived',
|
||||||
|
`audio_duration_ms` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '录音时长(毫秒)',
|
||||||
|
`summary_status` varchar(20) NOT NULL DEFAULT 'none' COMMENT 'AI总结状态:none/pending/success/failed',
|
||||||
|
`last_transcript_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 '更新时间',
|
||||||
|
`deleted_at` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '删除时间,0表示未删除',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_note_user_id` (`note_user_id`),
|
||||||
|
KEY `idx_status` (`status`),
|
||||||
|
KEY `idx_summary_status` (`summary_status`),
|
||||||
|
KEY `idx_created_at` (`created_at`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='note模块-笔记主表';
|
||||||
|
|
||||||
|
-- 笔记实时转写表
|
||||||
|
CREATE TABLE IF NOT EXISTS `note_transcript` (
|
||||||
|
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||||
|
`note_id` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT 'note_item.id',
|
||||||
|
`segment_no` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '分片序号',
|
||||||
|
`segment_text` text COMMENT '本次分片转写文本',
|
||||||
|
`full_text` longtext COMMENT '当前累计完整转写文本',
|
||||||
|
`is_final` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否最终片段:0否 1是',
|
||||||
|
`audio_duration_ms` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '当前累计录音时长',
|
||||||
|
`created_at` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建时间',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_note_id` (`note_id`),
|
||||||
|
KEY `idx_note_segment` (`note_id`, `segment_no`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='note模块-实时转写记录表';
|
||||||
|
|
||||||
|
-- AI 总结结果表
|
||||||
|
CREATE TABLE IF NOT EXISTS `note_ai_summary` (
|
||||||
|
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||||
|
`note_id` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT 'note_item.id',
|
||||||
|
`summary_type` varchar(20) NOT NULL DEFAULT 'brief' COMMENT '总结类型:brief/outline/todo',
|
||||||
|
`summary_text` longtext COMMENT 'AI总结正文',
|
||||||
|
`todo_list` longtext COMMENT '待办列表,建议JSON字符串',
|
||||||
|
`keywords` longtext COMMENT '关键词列表,建议JSON字符串',
|
||||||
|
`status` varchar(20) NOT NULL DEFAULT 'pending' COMMENT '状态:pending/success/failed',
|
||||||
|
`error_message` varchar(500) NOT NULL DEFAULT '' 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`),
|
||||||
|
KEY `idx_status` (`status`),
|
||||||
|
KEY `idx_summary_type` (`summary_type`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='note模块-AI总结结果表';
|
||||||
|
|||||||
@@ -0,0 +1,549 @@
|
|||||||
|
# Note 模块接口文档
|
||||||
|
|
||||||
|
## 1. 模块说明
|
||||||
|
|
||||||
|
`note` 是独立于现有 `app/api` 业务的笔记小程序模块,代码位置如下:
|
||||||
|
|
||||||
|
- [app/note/controller](/root/work/tp/app/note/controller)
|
||||||
|
- [app/note/service](/root/work/tp/app/note/service)
|
||||||
|
- [app/note/model](/root/work/tp/app/note/model)
|
||||||
|
|
||||||
|
当前已实现能力:
|
||||||
|
|
||||||
|
1. 微信小程序登录
|
||||||
|
2. 笔记创建、列表、详情、更新、删除
|
||||||
|
3. 实时转写文本保存
|
||||||
|
4. AI 总结生成与查询
|
||||||
|
|
||||||
|
路由注册位置:
|
||||||
|
|
||||||
|
- [route/app.php](/root/work/tp/route/app.php)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 鉴权说明
|
||||||
|
|
||||||
|
除登录接口外,其余 `note` 接口都需要携带 JWT:
|
||||||
|
|
||||||
|
- Header: `Authorization: Bearer {token}`
|
||||||
|
|
||||||
|
`note` 模块复用现有 JWT 机制,但要求 token 载荷中必须包含:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"userid": 123,
|
||||||
|
"guard": "note"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
说明:
|
||||||
|
|
||||||
|
- `userid` 对应 `note_user.id`
|
||||||
|
- `guard=note` 用于和旧 `api` 模块登录态隔离
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 环境变量
|
||||||
|
|
||||||
|
微信登录依赖以下配置:
|
||||||
|
|
||||||
|
- `WECHAT_MINI_APPID`
|
||||||
|
- `WECHAT_MINI_SECRET`
|
||||||
|
- `DB_NOTE_HOSTNAME`
|
||||||
|
- `DB_NOTE_DATABASE`
|
||||||
|
- `DB_NOTE_USERNAME`
|
||||||
|
- `DB_NOTE_PASSWORD`
|
||||||
|
- `DB_NOTE_HOSTPORT`
|
||||||
|
|
||||||
|
示例位置:
|
||||||
|
|
||||||
|
- [\.example.env](/root/work/tp/.example.env)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 数据表
|
||||||
|
|
||||||
|
表结构定义位置:
|
||||||
|
|
||||||
|
- [database.sql](/root/work/tp/database.sql#L26)
|
||||||
|
|
||||||
|
当前 `note` 模块使用以下 4 张表:
|
||||||
|
|
||||||
|
1. `note_user`
|
||||||
|
用于小程序用户登录和用户资料
|
||||||
|
|
||||||
|
2. `note_item`
|
||||||
|
笔记主表,包含正文、转写累计文本、状态、录音时长等
|
||||||
|
|
||||||
|
3. `note_transcript`
|
||||||
|
实时转写分片记录表
|
||||||
|
|
||||||
|
4. `note_ai_summary`
|
||||||
|
AI 总结结果表
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 接口列表
|
||||||
|
|
||||||
|
### 5.1 获取模块概览
|
||||||
|
|
||||||
|
- 方法:`GET`
|
||||||
|
- 路径:`/note/v1/meta/interfaces`
|
||||||
|
- 是否鉴权:否
|
||||||
|
|
||||||
|
用途:
|
||||||
|
|
||||||
|
- 返回 note 模块接口规划概览
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5.2 微信小程序登录
|
||||||
|
|
||||||
|
- 方法:`POST`
|
||||||
|
- 路径:`/note/v1/auth/wechat-login`
|
||||||
|
- 是否鉴权:否
|
||||||
|
|
||||||
|
请求参数:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
| :--- | :--- | :--- | :--- |
|
||||||
|
| `code` | string | 是 | `wx.login` 获取的临时 code |
|
||||||
|
| `nickname` | string | 否 | 用户昵称 |
|
||||||
|
| `avatar_url` | string | 否 | 用户头像地址 |
|
||||||
|
|
||||||
|
请求示例:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": "021xxx",
|
||||||
|
"nickname": "张三",
|
||||||
|
"avatar_url": "https://example.com/avatar.png"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
成功响应示例:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "登录成功",
|
||||||
|
"data": {
|
||||||
|
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.xxx",
|
||||||
|
"refresh_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.xxx",
|
||||||
|
"expires_in": 604800,
|
||||||
|
"user": {
|
||||||
|
"id": 1,
|
||||||
|
"member_id": 0,
|
||||||
|
"openid": "oxxx",
|
||||||
|
"nickname": "张三",
|
||||||
|
"avatar_url": "https://example.com/avatar.png",
|
||||||
|
"mobile": "",
|
||||||
|
"is_new_user": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"time": 1710000000
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
说明:
|
||||||
|
|
||||||
|
1. 首次登录时自动创建 `note_user`
|
||||||
|
2. 后续登录时按 `openid` 更新资料与最后登录时间
|
||||||
|
3. 若用户被禁用,返回 403
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5.3 获取当前用户信息
|
||||||
|
|
||||||
|
- 方法:`GET`
|
||||||
|
- 路径:`/note/v1/auth/me`
|
||||||
|
- 是否鉴权:是
|
||||||
|
|
||||||
|
成功响应示例:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "success",
|
||||||
|
"data": {
|
||||||
|
"id": 1,
|
||||||
|
"member_id": 0,
|
||||||
|
"openid": "oxxx",
|
||||||
|
"nickname": "张三",
|
||||||
|
"avatar_url": "https://example.com/avatar.png",
|
||||||
|
"mobile": "",
|
||||||
|
"status": 1,
|
||||||
|
"last_login_time": 1710000000,
|
||||||
|
"created_at": 1710000000
|
||||||
|
},
|
||||||
|
"time": 1710000001
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5.4 创建笔记
|
||||||
|
|
||||||
|
- 方法:`POST`
|
||||||
|
- 路径:`/note/v1/item/create`
|
||||||
|
- 是否鉴权:是
|
||||||
|
|
||||||
|
请求参数:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
| :--- | :--- | :--- | :--- |
|
||||||
|
| `source_type` | string | 是 | `text` / `audio` / `mix` |
|
||||||
|
| `title` | string | 否 | 标题 |
|
||||||
|
| `content` | string | 否 | 正文 |
|
||||||
|
| `audio_duration_ms` | int | 否 | 录音时长 |
|
||||||
|
| `status` | string | 否 | 默认 `draft` |
|
||||||
|
|
||||||
|
请求示例:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"source_type": "text",
|
||||||
|
"title": "会议纪要",
|
||||||
|
"content": "今天确认了下周需求排期",
|
||||||
|
"status": "draft"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
成功响应示例:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "创建成功",
|
||||||
|
"data": {
|
||||||
|
"id": 10,
|
||||||
|
"note_user_id": 1,
|
||||||
|
"title": "会议纪要",
|
||||||
|
"content": "今天确认了下周需求排期",
|
||||||
|
"transcript_text": "",
|
||||||
|
"source_type": "text",
|
||||||
|
"status": "draft",
|
||||||
|
"audio_duration_ms": 0,
|
||||||
|
"summary_status": "none",
|
||||||
|
"last_transcript_time": 0,
|
||||||
|
"created_at": 1710000000,
|
||||||
|
"updated_at": 1710000000
|
||||||
|
},
|
||||||
|
"time": 1710000000
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
说明:
|
||||||
|
|
||||||
|
- 如果不传 `title`,后端会根据 `content` 自动生成标题
|
||||||
|
- 如果 `title` 和 `content` 都为空,标题会默认为 `未命名笔记`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5.5 笔记列表
|
||||||
|
|
||||||
|
- 方法:`GET`
|
||||||
|
- 路径:`/note/v1/item/list`
|
||||||
|
- 是否鉴权:是
|
||||||
|
|
||||||
|
查询参数:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
| :--- | :--- | :--- | :--- |
|
||||||
|
| `page` | int | 否 | 默认 1 |
|
||||||
|
| `page_size` | int | 否 | 默认 10,最大 100 |
|
||||||
|
| `keyword` | string | 否 | 搜索标题、正文、转写文本 |
|
||||||
|
| `status` | string | 否 | 按状态筛选 |
|
||||||
|
|
||||||
|
响应示例:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "success",
|
||||||
|
"data": {
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"id": 10,
|
||||||
|
"note_user_id": 1,
|
||||||
|
"title": "会议纪要",
|
||||||
|
"content": "今天确认了下周需求排期",
|
||||||
|
"transcript_text": "",
|
||||||
|
"source_type": "text",
|
||||||
|
"status": "draft",
|
||||||
|
"audio_duration_ms": 0,
|
||||||
|
"summary_status": "none",
|
||||||
|
"last_transcript_time": 0,
|
||||||
|
"created_at": 1710000000,
|
||||||
|
"updated_at": 1710000000
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"total": 1,
|
||||||
|
"page": 1,
|
||||||
|
"page_size": 10
|
||||||
|
},
|
||||||
|
"time": 1710000001
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5.6 笔记详情
|
||||||
|
|
||||||
|
- 方法:`GET`
|
||||||
|
- 路径:`/note/v1/item/:id`
|
||||||
|
- 是否鉴权:是
|
||||||
|
|
||||||
|
响应示例:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "success",
|
||||||
|
"data": {
|
||||||
|
"id": 10,
|
||||||
|
"note_user_id": 1,
|
||||||
|
"title": "会议纪要",
|
||||||
|
"content": "今天确认了下周需求排期",
|
||||||
|
"transcript_text": "需要跟进接口联调",
|
||||||
|
"source_type": "mix",
|
||||||
|
"status": "draft",
|
||||||
|
"audio_duration_ms": 120000,
|
||||||
|
"summary_status": "success",
|
||||||
|
"last_transcript_time": 1710000100,
|
||||||
|
"created_at": 1710000000,
|
||||||
|
"updated_at": 1710000200,
|
||||||
|
"summary": {
|
||||||
|
"summary_id": 3,
|
||||||
|
"summary_type": "brief",
|
||||||
|
"summary_text": "今天确认了下周需求排期\n需要跟进接口联调",
|
||||||
|
"todo_list": [
|
||||||
|
"需要跟进接口联调"
|
||||||
|
],
|
||||||
|
"keywords": [
|
||||||
|
"需求",
|
||||||
|
"排期",
|
||||||
|
"接口联调"
|
||||||
|
],
|
||||||
|
"status": "success"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"time": 1710000201
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5.7 更新笔记
|
||||||
|
|
||||||
|
- 方法:`POST`
|
||||||
|
- 路径:`/note/v1/item/update/:id`
|
||||||
|
- 是否鉴权:是
|
||||||
|
|
||||||
|
请求参数:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
| :--- | :--- | :--- | :--- |
|
||||||
|
| `title` | string | 否 | 标题 |
|
||||||
|
| `content` | string | 否 | 正文 |
|
||||||
|
| `status` | string | 否 | 状态 |
|
||||||
|
| `audio_duration_ms` | int | 否 | 录音时长 |
|
||||||
|
|
||||||
|
说明:
|
||||||
|
|
||||||
|
- 只更新传入字段
|
||||||
|
- 只能更新自己的笔记
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5.8 删除笔记
|
||||||
|
|
||||||
|
- 方法:`POST`
|
||||||
|
- 路径:`/note/v1/item/delete/:id`
|
||||||
|
- 是否鉴权:是
|
||||||
|
|
||||||
|
说明:
|
||||||
|
|
||||||
|
- 当前为软删除,写入 `deleted_at`
|
||||||
|
|
||||||
|
成功响应示例:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "删除成功",
|
||||||
|
"data": {
|
||||||
|
"deleted": true,
|
||||||
|
"id": 10
|
||||||
|
},
|
||||||
|
"time": 1710000300
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5.9 保存实时转写内容
|
||||||
|
|
||||||
|
- 方法:`POST`
|
||||||
|
- 路径:`/note/v1/item/transcript/:id`
|
||||||
|
- 是否鉴权:是
|
||||||
|
|
||||||
|
请求参数:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
| :--- | :--- | :--- | :--- |
|
||||||
|
| `full_text` | string | 是 | 当前累计完整转写文本 |
|
||||||
|
| `segment_no` | int | 否 | 分片序号,默认 0 |
|
||||||
|
| `segment_text` | string | 否 | 当前分片文本 |
|
||||||
|
| `is_final` | int/bool | 否 | 是否最终片段 |
|
||||||
|
| `audio_duration_ms` | int | 否 | 当前累计录音时长 |
|
||||||
|
|
||||||
|
请求示例:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"segment_no": 3,
|
||||||
|
"segment_text": "需要跟进接口联调",
|
||||||
|
"full_text": "今天确认了下周需求排期,需要跟进接口联调",
|
||||||
|
"is_final": 1,
|
||||||
|
"audio_duration_ms": 120000
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
成功响应示例:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "转写保存成功",
|
||||||
|
"data": {
|
||||||
|
"note_id": 10,
|
||||||
|
"segment_no": 3,
|
||||||
|
"is_final": 1,
|
||||||
|
"transcript_text": "今天确认了下周需求排期,需要跟进接口联调",
|
||||||
|
"audio_duration_ms": 120000,
|
||||||
|
"updated_at": 1710000400
|
||||||
|
},
|
||||||
|
"time": 1710000400
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
说明:
|
||||||
|
|
||||||
|
- 后端会同步更新 `note_item.transcript_text`
|
||||||
|
- 同一个 `note_id + segment_no` 会覆盖写入
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5.10 生成 AI 总结
|
||||||
|
|
||||||
|
- 方法:`POST`
|
||||||
|
- 路径:`/note/v1/ai/summary/:id`
|
||||||
|
- 是否鉴权:是
|
||||||
|
|
||||||
|
请求参数:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
| :--- | :--- | :--- | :--- |
|
||||||
|
| `summary_type` | string | 否 | `brief` / `outline` / `todo` |
|
||||||
|
| `force_refresh` | int/bool | 否 | 是否强制重新生成 |
|
||||||
|
|
||||||
|
请求示例:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"summary_type": "brief",
|
||||||
|
"force_refresh": 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
成功响应示例:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "总结生成成功",
|
||||||
|
"data": {
|
||||||
|
"summary_id": 3,
|
||||||
|
"note_id": 10,
|
||||||
|
"summary_type": "brief",
|
||||||
|
"summary_text": "今天确认了下周需求排期\n需要跟进接口联调",
|
||||||
|
"todo_list": [
|
||||||
|
"需要跟进接口联调"
|
||||||
|
],
|
||||||
|
"keywords": [
|
||||||
|
"需求",
|
||||||
|
"排期",
|
||||||
|
"接口联调"
|
||||||
|
],
|
||||||
|
"status": "success",
|
||||||
|
"error_message": "",
|
||||||
|
"created_at": 1710000500,
|
||||||
|
"updated_at": 1710000500
|
||||||
|
},
|
||||||
|
"time": 1710000500
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
说明:
|
||||||
|
|
||||||
|
- 当前实现为规则版总结,不依赖外部大模型
|
||||||
|
- 若已存在成功总结且 `force_refresh=0`,会直接返回已有结果
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5.11 查看 AI 总结
|
||||||
|
|
||||||
|
- 方法:`GET`
|
||||||
|
- 路径:`/note/v1/ai/summary/:id`
|
||||||
|
- 是否鉴权:是
|
||||||
|
|
||||||
|
说明:
|
||||||
|
|
||||||
|
- 返回指定笔记最新的一条总结记录
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 错误码约定
|
||||||
|
|
||||||
|
当前模块沿用项目统一返回结构:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 400,
|
||||||
|
"msg": "错误信息",
|
||||||
|
"data": [],
|
||||||
|
"time": 1710000000
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
常见情况:
|
||||||
|
|
||||||
|
- `400`:参数错误
|
||||||
|
- `401`:未登录或登录态无效
|
||||||
|
- `403`:用户被禁用
|
||||||
|
- `404`:资源不存在
|
||||||
|
- `500`:服务端错误
|
||||||
|
- `502`:调用微信接口失败
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 调用顺序建议
|
||||||
|
|
||||||
|
小程序端推荐按以下顺序接入:
|
||||||
|
|
||||||
|
1. 调 `wx.login`
|
||||||
|
2. 把 `code` 发给 `POST /note/v1/auth/wechat-login`
|
||||||
|
3. 保存返回的 `token`
|
||||||
|
4. 创建笔记 `POST /note/v1/item/create`
|
||||||
|
5. 录音转写过程中持续调用 `POST /note/v1/item/transcript/:id`
|
||||||
|
6. 需要生成总结时调用 `POST /note/v1/ai/summary/:id`
|
||||||
|
7. 打开详情页时调用 `GET /note/v1/item/:id`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 当前限制
|
||||||
|
|
||||||
|
1. AI 总结目前是规则版,不是大模型版
|
||||||
|
2. 微信登录依赖服务端已正确配置 `WECHAT_MINI_APPID` 和 `WECHAT_MINI_SECRET`
|
||||||
|
3. 当前未实现文件音频上传,只实现了“转写文本写回后端”的数据链路
|
||||||
@@ -6,6 +6,10 @@ use app\api\controller\v1\Auth;
|
|||||||
use app\api\controller\v1\Platform;
|
use app\api\controller\v1\Platform;
|
||||||
use app\api\controller\v1\PublishPlan;
|
use app\api\controller\v1\PublishPlan;
|
||||||
use app\api\controller\v1\VideoWork;
|
use app\api\controller\v1\VideoWork;
|
||||||
|
use app\note\controller\v1\Ai as NoteAi;
|
||||||
|
use app\note\controller\v1\Auth as NoteAuth;
|
||||||
|
use app\note\controller\v1\Meta as NoteMeta;
|
||||||
|
use app\note\controller\v1\Note as NoteItem;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 全局路由入口。
|
* 全局路由入口。
|
||||||
@@ -45,3 +49,24 @@ Route::group('api/v1/publish-plan', function () {
|
|||||||
Route::group('api/v1/video-work', function () {
|
Route::group('api/v1/video-work', function () {
|
||||||
Route::get('list', [VideoWork::class, 'index']);
|
Route::get('list', [VideoWork::class, 'index']);
|
||||||
})->middleware(\app\api\middleware\Auth::class);
|
})->middleware(\app\api\middleware\Auth::class);
|
||||||
|
|
||||||
|
// note v1 笔记小程序模块接口规划(公开)
|
||||||
|
Route::group('note/v1', function () {
|
||||||
|
Route::get('meta/interfaces', [NoteMeta::class, 'interfaces']);
|
||||||
|
Route::post('auth/wechat-login', [NoteAuth::class, 'wechatLogin']);
|
||||||
|
});
|
||||||
|
|
||||||
|
// note v1 笔记小程序模块接口(需登录)
|
||||||
|
Route::group('note/v1', function () {
|
||||||
|
Route::get('auth/me', [NoteAuth::class, 'me']);
|
||||||
|
|
||||||
|
Route::post('item/create', [NoteItem::class, 'create']);
|
||||||
|
Route::get('item/list', [NoteItem::class, 'index']);
|
||||||
|
Route::get('item/:id', [NoteItem::class, 'read']);
|
||||||
|
Route::post('item/update/:id', [NoteItem::class, 'update']);
|
||||||
|
Route::post('item/delete/:id', [NoteItem::class, 'delete']);
|
||||||
|
Route::post('item/transcript/:id', [NoteItem::class, 'transcript']);
|
||||||
|
|
||||||
|
Route::post('ai/summary/:id', [NoteAi::class, 'summary']);
|
||||||
|
Route::get('ai/summary/:id', [NoteAi::class, 'readSummary']);
|
||||||
|
})->middleware(\app\api\middleware\Auth::class);
|
||||||
|
|||||||
Reference in New Issue
Block a user