feat: add note module and route fixes
This commit is contained in:
@@ -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,
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user