mirror of
https://github.com/kuaifan/dootask.git
synced 2026-07-27 16:37:52 +00:00
feat(push): 移动端推送从友盟替换为 DooPush 通道
新移动端 (Expo App) 使用 doopush-react-native-sdk 完成设备注册;服务端
按 userid 标签 OR 并集定向推送,替代原友盟 alias 链路。
- 新增 app/Services/DooPushClient.php:DooPush 服务端 HTTP 客户端
POST /apps/{appId}/push(按 tags 定向)
POST /apps/{appId}/device-tags/{deviceToken}(绑定 userid 标签)
DELETE 同上(解绑)
X-API-Key 鉴权
- 新增 app/Module/Push/PushService.php:推送业务编排
按 badge(未读数)分组合并下发,减少 HTTP 次数
- 新增 app/Http/Controllers/Api/PushController.php:设备注册/解绑接口
api/push/register、api/push/unregister
- 修改 app/Models/UmengAlias.php:pushMsgToUserid 入口在
DooPushClient::enabled() 时委托新通道,否则保留旧友盟链路
- 修改 config/dootask.php:新增 doopush 段(appId/apiKey/baseURL/tag_name)
- 修改 routes/web.php:注册 api/push/{method} 路由
- 同步 routes/api-map.md(doc:api-map 自动生成)
旧 EEUI App 推送链路完全不动;新 App 通过 .env 的 DOOPUSH_ENABLED=yes 启用。
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a19822a617
commit
9151939ba5
71
app/Http/Controllers/Api/PushController.php
Normal file
71
app/Http/Controllers/Api/PushController.php
Normal file
@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Module\Base;
|
||||
use App\Module\Doo;
|
||||
use App\Services\DooPushClient;
|
||||
use Request;
|
||||
|
||||
/**
|
||||
* 推送相关接口(DooPush 通道)。
|
||||
*
|
||||
* 路由:api/push/{method}(见 routes/web.php)。
|
||||
* - register:App 端登录后上报 deviceToken + userid,后端调 DooPush 设 userid 标签
|
||||
* - unregister:App 端登出/换号时调用,后端调 DooPush 解除标签
|
||||
*/
|
||||
class PushController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* 绑定推送设备到当前会员(设置 DooPush userid 标签)。
|
||||
*
|
||||
* @apiParam {String} device_token DooPush SDK 注册返回的 deviceToken(不是平台 token)
|
||||
*/
|
||||
public function register__index()
|
||||
{
|
||||
$userid = Doo::userId();
|
||||
$token = trim(Request::input('device_token', ''));
|
||||
if ($token === '') {
|
||||
return Base::retError('device_token 不能为空');
|
||||
}
|
||||
if (!DooPushClient::enabled()) {
|
||||
return Base::retError('推送服务未启用');
|
||||
}
|
||||
try {
|
||||
$resp = DooPushClient::setUserTag($token, $userid);
|
||||
if (!$resp->successful()) {
|
||||
return Base::retError('设置标签失败: ' . $resp->status());
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
return Base::retError('设置标签异常: ' . $e->getMessage());
|
||||
}
|
||||
return Base::retSuccess('success');
|
||||
}
|
||||
|
||||
/**
|
||||
* 解绑推送设备(解除 DooPush userid 标签)。
|
||||
*
|
||||
* @apiParam {String} device_token DooPush SDK 注册返回的 deviceToken
|
||||
*/
|
||||
public function unregister__index()
|
||||
{
|
||||
$userid = Doo::userId();
|
||||
$token = trim(Request::input('device_token', ''));
|
||||
if ($token === '') {
|
||||
return Base::retError('device_token 不能为空');
|
||||
}
|
||||
if (!DooPushClient::enabled()) {
|
||||
return Base::retError('推送服务未启用');
|
||||
}
|
||||
try {
|
||||
$resp = DooPushClient::unsetUserTag($token);
|
||||
if (!$resp->successful()) {
|
||||
return Base::retError('解绑标签失败: ' . $resp->status());
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
return Base::retError('解绑标签异常: ' . $e->getMessage());
|
||||
}
|
||||
unset($userid); // 当前实现按 token 解绑;保留 userid 拉取以约束鉴权语义
|
||||
return Base::retSuccess('success');
|
||||
}
|
||||
}
|
||||
@ -3,6 +3,8 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App\Module\Base;
|
||||
use App\Module\Push\PushService;
|
||||
use App\Services\DooPushClient;
|
||||
use Carbon\Carbon;
|
||||
use Hedeqiang\UMeng\Android;
|
||||
use Hedeqiang\UMeng\IOS;
|
||||
@ -253,6 +255,11 @@ class UmengAlias extends AbstractModel
|
||||
*/
|
||||
public static function pushMsgToUserid($userid, $array)
|
||||
{
|
||||
// DooPush 启用时走新通道(按 userid 标签 OR 并集定向);否则保留旧 Umeng 链路。
|
||||
if (DooPushClient::enabled()) {
|
||||
PushService::pushToUsers($userid, $array);
|
||||
return;
|
||||
}
|
||||
$builder = self::select(['id', 'platform', 'alias', 'userid'])->where('updated_at', '>', Carbon::now()->subMonth());
|
||||
if (is_array($userid)) {
|
||||
$builder->whereIn('userid', $userid);
|
||||
|
||||
58
app/Module/Push/PushService.php
Normal file
58
app/Module/Push/PushService.php
Normal file
@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace App\Module\Push;
|
||||
|
||||
use App\Models\WebSocketDialogMsgRead;
|
||||
use App\Services\DooPushClient;
|
||||
|
||||
/**
|
||||
* 推送业务编排(DooPush 通道)。
|
||||
*
|
||||
* UmengAlias::pushMsgToUserid 在 DooPushClient::enabled() 时委托到此处;
|
||||
* 旧 Umeng 链路仍在 UmengAlias 内部保留,不动 private 方法的可见性。
|
||||
*/
|
||||
class PushService
|
||||
{
|
||||
/**
|
||||
* DooPush:按 userid 标签 OR 并集定向推送;badge 按每个 user 单独取未读数。
|
||||
*
|
||||
* DooPush /push 一次只能下发一个 badge,需按 badge 分组合并下发以减少 HTTP 次数。
|
||||
*
|
||||
* @param array<int>|int $userid
|
||||
* @param array{title?:string, body?:string, extra?:array} $array
|
||||
*/
|
||||
public static function pushToUsers($userid, array $array): void
|
||||
{
|
||||
if (empty($userid) || empty($array)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$userids = is_array($userid) ? array_values(array_filter($userid)) : [(int) $userid];
|
||||
if (empty($userids)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// userid -> badge 数;按 badge 分组合并下发
|
||||
$byBadge = [];
|
||||
foreach ($userids as $uid) {
|
||||
$badge = WebSocketDialogMsgRead::whereUserid($uid)
|
||||
->whereSilence(0)
|
||||
->whereReadAt(null)
|
||||
->count();
|
||||
$byBadge[$badge][] = (int) $uid;
|
||||
}
|
||||
|
||||
foreach ($byBadge as $badge => $batch) {
|
||||
try {
|
||||
$msg = $array;
|
||||
$msg['badge'] = (int) $badge;
|
||||
$resp = DooPushClient::pushToUserids($batch, $msg);
|
||||
if (!$resp->successful()) {
|
||||
info('[DooPush] non-2xx: ' . $resp->status() . ' ' . $resp->body());
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
info('[DooPush] exception: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
112
app/Services/DooPushClient.php
Normal file
112
app/Services/DooPushClient.php
Normal file
@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Illuminate\Http\Client\Response;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
/**
|
||||
* DooPush 服务端 HTTP 客户端封装:
|
||||
*
|
||||
* - 发推送:POST /apps/{appId}/push
|
||||
* - 设设备标签:POST /apps/{appId}/device-tags/{deviceToken}
|
||||
* - 删设备标签:DELETE /apps/{appId}/device-tags/{deviceToken}/{tagName}
|
||||
*
|
||||
* 鉴权使用 X-API-Key(DooPush 文档推荐 Header 方式)。
|
||||
* 配置来源:config('dootask.doopush')。未启用或未配置时由调用方判断;本类不做静默降级。
|
||||
*/
|
||||
class DooPushClient
|
||||
{
|
||||
public static function enabled(): bool
|
||||
{
|
||||
$cfg = (array) config('dootask.doopush', []);
|
||||
return ($cfg['enabled'] ?? false)
|
||||
&& !empty($cfg['app_id'])
|
||||
&& !empty($cfg['api_key']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 推送给若干 userid(按 userid 标签 OR 并集定向)。
|
||||
*
|
||||
* @param array<int> $userids
|
||||
* @param array{title:string, body?:string, badge?:int, extra?:array} $msg
|
||||
*/
|
||||
public static function pushToUserids(array $userids, array $msg, ?string $platform = null): Response
|
||||
{
|
||||
$cfg = (array) config('dootask.doopush', []);
|
||||
$tagName = (string) ($cfg['tag_name'] ?? 'userid');
|
||||
|
||||
$tags = [];
|
||||
foreach (array_unique($userids) as $uid) {
|
||||
$tags[] = ['tag_name' => $tagName, 'tag_value' => (string) $uid];
|
||||
}
|
||||
|
||||
$target = ['type' => 'tags', 'tags' => $tags];
|
||||
if ($platform !== null) {
|
||||
$target['platform'] = $platform;
|
||||
}
|
||||
|
||||
$payload = [
|
||||
'title' => (string) ($msg['title'] ?? ''),
|
||||
'content' => (string) ($msg['body'] ?? ''),
|
||||
'target' => $target,
|
||||
];
|
||||
|
||||
if (isset($msg['badge'])) {
|
||||
$payload['badge'] = (int) $msg['badge'];
|
||||
}
|
||||
|
||||
// 自定义载荷:保留 extra 中的 dialog_id / msg_id 等点击跳转字段
|
||||
$extra = (array) ($msg['extra'] ?? []);
|
||||
if (!empty($extra)) {
|
||||
$payload['payload'] = [
|
||||
'action' => 'open_dialog',
|
||||
'data' => json_encode($extra, JSON_UNESCAPED_UNICODE),
|
||||
];
|
||||
}
|
||||
|
||||
return self::http()->post(self::url('/push'), $payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* 给指定设备 token 绑定 userid 标签(App SDK 注册后由客户端→DooTask 后端→本接口完成)。
|
||||
*/
|
||||
public static function setUserTag(string $deviceToken, int $userid): Response
|
||||
{
|
||||
$cfg = (array) config('dootask.doopush', []);
|
||||
$tagName = (string) ($cfg['tag_name'] ?? 'userid');
|
||||
|
||||
return self::http()->post(self::url("/device-tags/{$deviceToken}"), [
|
||||
'tag_name' => $tagName,
|
||||
'tag_value' => (string) $userid,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解除设备的 userid 标签(用户登出/换号时调用)。
|
||||
*/
|
||||
public static function unsetUserTag(string $deviceToken): Response
|
||||
{
|
||||
$cfg = (array) config('dootask.doopush', []);
|
||||
$tagName = (string) ($cfg['tag_name'] ?? 'userid');
|
||||
|
||||
return self::http()->delete(self::url("/device-tags/{$deviceToken}/{$tagName}"));
|
||||
}
|
||||
|
||||
private static function http()
|
||||
{
|
||||
$cfg = (array) config('dootask.doopush', []);
|
||||
return Http::withHeaders([
|
||||
'X-API-Key' => (string) ($cfg['api_key'] ?? ''),
|
||||
'Content-Type' => 'application/json',
|
||||
])->timeout(10);
|
||||
}
|
||||
|
||||
private static function url(string $path): string
|
||||
{
|
||||
$cfg = (array) config('dootask.doopush', []);
|
||||
$baseUrl = rtrim((string) ($cfg['base_url'] ?? 'https://doopush.com/api/v1'), '/');
|
||||
$appId = (string) ($cfg['app_id'] ?? '');
|
||||
return $baseUrl . '/apps/' . $appId . $path;
|
||||
}
|
||||
}
|
||||
@ -32,4 +32,13 @@ return [
|
||||
// 临时文件自动清理天数(DeleteTmpTask)
|
||||
'auto_empty_temp_file' => env('AUTO_EMPTY_TEMP_FILE', 30),
|
||||
|
||||
// DooPush 推送服务配置(替代旧友盟 umeng;空值表示未启用,会回退到旧 umeng 链路)
|
||||
'doopush' => [
|
||||
'enabled' => env('DOOPUSH_ENABLED') === 'yes',
|
||||
'app_id' => env('DOOPUSH_APP_ID'),
|
||||
'api_key' => env('DOOPUSH_API_KEY'),
|
||||
'base_url' => env('DOOPUSH_BASE_URL', 'https://doopush.com/api/v1'),
|
||||
'tag_name' => env('DOOPUSH_USER_TAG', 'userid'),
|
||||
],
|
||||
|
||||
];
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
> 此文件由 `php artisan doc:api-map` 生成,勿手改。
|
||||
|
||||
接口总数:319
|
||||
接口总数:300
|
||||
|
||||
## 路由规则
|
||||
|
||||
@ -224,6 +224,7 @@ API 使用动态路由(见 `routes/web.php`),URL 段映射为控制器方
|
||||
| api/dialog/msg/sendtext | msg__sendtext() | post | 发送消息 |
|
||||
| api/dialog/msg/sendnotice | msg__sendnotice() | post | 发送通知 |
|
||||
| api/dialog/msg/sendtemplate | msg__sendtemplate() | post | 发送模板消息 |
|
||||
| api/dialog/msg/sendapprove | msg__sendapprove() | post | 发送审批通知卡片 |
|
||||
| api/dialog/msg/sendrecord | msg__sendrecord() | post | 发送语音 |
|
||||
| api/dialog/msg/convertrecord | msg__convertrecord() | post | 录音转文字 |
|
||||
| api/dialog/msg/sendfile | msg__sendfile() | post | 文件上传 |
|
||||
@ -326,33 +327,6 @@ API 使用动态路由(见 `routes/web.php`),URL 段映射为控制器方
|
||||
| api/public/checkin/install | checkin__install() | any | |
|
||||
| api/public/checkin/report | checkin__report() | any | |
|
||||
|
||||
## approve(ApproveController)
|
||||
|
||||
| URL | 方法名 | HTTP | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| api/approve/verifyToken | verifyToken() | get | 验证APi登录 |
|
||||
| api/approve/procdef/all | procdef__all() | post | 查询流程定义 |
|
||||
| api/approve/procdef/del | procdef__del() | get | 删除流程定义 |
|
||||
| api/approve/process/start | process__start() | post | 启动流程(审批中) |
|
||||
| api/approve/process/addGlobalComment | process__addGlobalComment() | post | 添加全局评论 |
|
||||
| api/approve/task/complete | task__complete() | post | 审批 |
|
||||
| api/approve/task/withdraw | task__withdraw() | post | 撤回 |
|
||||
| api/approve/process/delById | process__delById() | post | 删除审批(流程实例) |
|
||||
| api/approve/process/findTask | process__findTask() | post | 查询需要我审批的流程(审批中) |
|
||||
| api/approve/process/startByMyselfAll | process__startByMyselfAll() | post | 查询我启动的流程(全部) |
|
||||
| api/approve/process/startByMyself | process__startByMyself() | post | 查询我启动的流程(审批中) |
|
||||
| api/approve/process/findProcNotify | process__findProcNotify() | post | 查询抄送我的流程(审批中) |
|
||||
| api/approve/identitylink/findParticipant | identitylink__findParticipant() | get | 查询流程实例的参与者(审批中) |
|
||||
| api/approve/procHistory/findTask | procHistory__findTask() | post | 查询需要我审批的流程(已结束) |
|
||||
| api/approve/procHistory/startByMyself | procHistory__startByMyself() | post | 查询我启动的流程(已结束) |
|
||||
| api/approve/procHistory/findProcNotify | procHistory__findProcNotify() | post | 查询抄送我的流程(已结束) |
|
||||
| api/approve/identitylinkHistory/findParticipant | identitylinkHistory__findParticipant() | get | 查询流程实例的参与者(已结束) |
|
||||
| api/approve/process/detail | process__detail() | get | 根据流程ID查询流程详情 |
|
||||
| api/approve/export | export() | post | 导出数据 |
|
||||
| api/approve/down | down() | get | 下载导出的审批数据 |
|
||||
| api/approve/user/status | user__status() | get | 获取用户审批状态 |
|
||||
| api/approve/process/doto | process__doto() | get | 查询需要我审批的流程数量 |
|
||||
|
||||
## assistant(AssistantController)
|
||||
|
||||
| URL | 方法名 | HTTP | 说明 |
|
||||
@ -390,3 +364,10 @@ API 使用动态路由(见 `routes/web.php`),URL 段映射为控制器方
|
||||
|
||||
| URL | 方法名 | HTTP | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
|
||||
## push(PushController)
|
||||
|
||||
| URL | 方法名 | HTTP | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| api/push/register/index | register__index() | any | |
|
||||
| api/push/unregister/index | unregister__index() | any | |
|
||||
|
||||
@ -13,6 +13,7 @@ use App\Http\Controllers\Api\AssistantController;
|
||||
use App\Http\Controllers\Api\ProjectController;
|
||||
use App\Http\Controllers\Api\ComplaintController;
|
||||
use App\Http\Controllers\Api\SearchController;
|
||||
use App\Http\Controllers\Api\PushController;
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
@ -63,6 +64,9 @@ Route::prefix('api')->middleware(['webapi'])->group(function () {
|
||||
// 测试
|
||||
Route::any('test/{method}', TestController::class);
|
||||
Route::any('test/{method}/{action}', TestController::class);
|
||||
// 推送(DooPush 设备注册/解绑)
|
||||
Route::any('push/{method}', PushController::class);
|
||||
Route::any('push/{method}/{action}', PushController::class);
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user