dootask/app/Tasks/TodoRemindTask.php
kuaifan 6b54b7b1c5 feat(todo): 聊天待办支持提醒时间(到点引用原消息+@提及)
给消息待办增加可选「提醒时间」,到点由 todo-alert 机器人对原消息发起
reply、正文 @ 仍在群内的被指派成员,完全复用原生回复/提及链路(定向未读、
红点、绕过会话免打扰、App 推送);被指派人全部退群则跳过发送并标记已提醒。
设/改/取消提醒的权限沿用 todo_set_permission 开关与 checkTodoOwnerPermission。

后端:
- 迁移:web_socket_dialog_msg_todos 增加 remind_at/reminded_at 及索引,
  注册为日期字段
- WebSocketDialogMsgTodo::dueReminders() 选取到点(未提醒/未完成)待办(limit 500)
- WebSocketDialogMsg::setTodoRemind() 纯数据写入(改时间重置 reminded_at),
  接入 toggleTodoMsg($remindAt) 与 msg__todo 透传
- 接口 msg__todoremind 设置/修改/取消提醒(权限闸门、消息类型校验、
  pushMsg 同步 todo_done)
- TodoRemindTask 到点按消息发提醒(reminded_at 防重复、迟发补发、原消息/
  会话删除兜底),buildRemindText 生成 <span class="mention user"> 文本,
  接入 crontab;登记 todo-alert 机器人
- msgJoinGroup 从提醒文本中提取被 @ 成员

前端:
- 设待办弹窗新增「提醒时间」(预设 + 自定义 DatePicker)
- 待办详情浮层每条待办可查看/修改/取消提醒:DatePicker on-clear「清空」
  二次确认后取消,无时间时仅关闭面板不发请求
- 待办浮层窄屏(≤500px)改为 待办/完成 tab 切换,宽屏维持双列;列表为空
  展示空状态占位;提醒时间用 Icon 替换 emoji
- 时间读写对齐项目任务时间的时区约定

测试:tests/Feature/TodoRemindTest(数据/选取/写入/权限决策/buildRemindText/
text mention 提取),TodoSetPermissionTest 无回归。

任务 #124 后续增强。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 12:08:34 +00:00

87 lines
3.3 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace App\Tasks;
use App\Models\User;
use App\Models\WebSocketDialog;
use App\Models\WebSocketDialogMsg;
use App\Models\WebSocketDialogMsgTodo;
use App\Module\Doo;
use Carbon\Carbon;
/**
* 待办提醒:到点由 todo-alert 机器人在原会话发一条「引用原消息 + @被指派成员」的普通文本
* (同一消息同批到点的成员合并一条)。
*/
class TodoRemindTask extends AbstractTask
{
public function __construct()
{
parent::__construct();
}
/**
* 构造提醒文本:每个被提醒成员一个 @ span + 提示语。
* 直接拼 <span class="mention user" data-id> 是因为 sendMsg 不会调用 formatMsg
* 文本会原样入库msgJoinGroup 据此 span 正则提取 @。
*/
public static function buildRemindText(array $mentionUserids): string
{
$nicknames = User::whereIn('userid', $mentionUserids)->pluck('nickname', 'userid');
$mentionText = '';
foreach ($mentionUserids as $uid) {
$name = $nicknames[$uid] ?? $uid;
$mentionText .= "<span class=\"mention user\" data-id=\"{$uid}\">@{$name}</span> ";
}
return $mentionText . Doo::translate('你有一条待办到提醒时间啦');
}
public function start()
{
$rows = WebSocketDialogMsgTodo::dueReminders();
if ($rows->isEmpty()) {
return;
}
$botUser = User::botGetOrCreate('todo-alert');
if (empty($botUser)) {
return;
}
foreach ($rows->groupBy('msg_id') as $msgId => $group) {
$rowIds = $group->pluck('id')->toArray();
$userids = $group->pluck('userid')->map('intval')->values()->toArray();
//
$msg = WebSocketDialogMsg::find($msgId);
$dialog = $msg ? WebSocketDialog::find($msg->dialog_id) : null;
if (empty($msg) || empty($dialog)) {
// 原消息/会话已不存在:标记已提醒,避免空转重复扫描
WebSocketDialogMsgTodo::whereIn('id', $rowIds)->update(['reminded_at' => Carbon::now()]);
continue;
}
//
$memberIds = $dialog->dialogUser->pluck('userid')->map('intval')->values()->toArray();
$mentionUserids = array_values(array_intersect($userids, $memberIds));
if (empty($mentionUserids)) {
// 被指派人都已退群:没人可 @,标记已提醒避免空转重复扫描
WebSocketDialogMsgTodo::whereIn('id', $rowIds)->update(['reminded_at' => Carbon::now()]);
continue;
}
$res = WebSocketDialogMsg::sendMsg(
"reply-{$msg->id}", // 引用原消息 → reply_data 自动填充
$dialog->id,
'text', // 普通文本
['text' => self::buildRemindText($mentionUserids)],
$botUser->userid,
false, false, false // push_self / push_retry / push_silence
);
//
if (\App\Module\Base::isSuccess($res)) {
WebSocketDialogMsgTodo::whereIn('id', $rowIds)->update(['reminded_at' => Carbon::now()]);
}
}
}
public function end()
{
}
}