代办任务
This commit is contained in:
@@ -0,0 +1,207 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\command;
|
||||
|
||||
use app\common\model\auth\Admin;
|
||||
use app\common\model\tcm\Diagnosis;
|
||||
use app\common\model\tcm\DiagnosisTodo;
|
||||
use app\common\service\wechat\WechatWorkAppMessageService;
|
||||
use think\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\Output;
|
||||
use think\facade\Db;
|
||||
use think\facade\Log;
|
||||
|
||||
/**
|
||||
* 诊单待办事项 - 企业微信定时推送
|
||||
*
|
||||
* 使用方法:
|
||||
* php think tcm:diagnosis-todo-notify
|
||||
*
|
||||
* 推荐 cron(每分钟执行):
|
||||
* * * * * * cd /path/to/server && php think tcm:diagnosis-todo-notify >> /var/log/zyt-todo.log 2>&1
|
||||
*
|
||||
* 行为:
|
||||
* 1. 取出 status=0 且 remind_time<=now 的待办,按 remind_time asc,限 200 条
|
||||
* 2. 每条用乐观锁 (update set status=1 where id=? and status=0) 抢占,避免并发重复推送
|
||||
* 3. 找创建人 admin.work_wechat_userid,未绑定 → status=3 + error
|
||||
* 4. 调 WechatWorkAppMessageService::sendTextToUser,失败 → status=3 + error(终态,不重试)
|
||||
* 5. 整批不中断;输出 处理 N / 成功 X / 失败 Y / 跳过未绑定 Z
|
||||
*/
|
||||
class DiagnosisTodoNotify extends Command
|
||||
{
|
||||
/** 单次扫描最大条数 */
|
||||
private const BATCH_LIMIT = 200;
|
||||
|
||||
protected function configure()
|
||||
{
|
||||
$this->setName('tcm:diagnosis-todo-notify')
|
||||
->setDescription('诊单待办事项:扫描到点的待执行项并向创建人发送企业微信消息');
|
||||
}
|
||||
|
||||
protected function execute(Input $input, Output $output): int
|
||||
{
|
||||
$startTs = microtime(true);
|
||||
$now = time();
|
||||
|
||||
$output->writeln('[' . date('Y-m-d H:i:s', $now) . '] 开始扫描诊单待办事项...');
|
||||
|
||||
$totalProcessed = 0;
|
||||
$totalSent = 0;
|
||||
$totalFailed = 0;
|
||||
$totalSkipped = 0; // 被并发抢占
|
||||
$totalUnbound = 0; // 创建人未绑定企微(计入 failed)
|
||||
|
||||
try {
|
||||
$todoTable = (new DiagnosisTodo())->getTable();
|
||||
|
||||
$rows = Db::name(self::stripTablePrefix($todoTable))
|
||||
->where('status', DiagnosisTodo::STATUS_PENDING)
|
||||
->where('remind_time', '<=', $now)
|
||||
->whereNull('delete_time')
|
||||
->order('remind_time', 'asc')
|
||||
->limit(self::BATCH_LIMIT)
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
$output->writeln('待处理数量:' . count($rows));
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$totalProcessed++;
|
||||
$todoId = (int) ($row['id'] ?? 0);
|
||||
if ($todoId <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
// 乐观锁:抢占成功时影响 1 行;并发场景下另一个进程已抢走则影响 0 行 → 跳过
|
||||
$affected = Db::name(self::stripTablePrefix($todoTable))
|
||||
->where('id', $todoId)
|
||||
->where('status', DiagnosisTodo::STATUS_PENDING)
|
||||
->update([
|
||||
'status' => DiagnosisTodo::STATUS_SENT,
|
||||
'notified_at' => $now,
|
||||
'update_time' => $now,
|
||||
]);
|
||||
|
||||
if ($affected <= 0) {
|
||||
$totalSkipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 取创建人 work_wechat_userid
|
||||
$creatorId = (int) ($row['creator_id'] ?? 0);
|
||||
$wxId = '';
|
||||
if ($creatorId > 0) {
|
||||
$wxId = (string) Admin::where('id', $creatorId)->value('work_wechat_userid');
|
||||
}
|
||||
|
||||
if ($wxId === '') {
|
||||
$this->markFailed($todoTable, $todoId, '创建人未绑定企业微信 userid');
|
||||
$totalUnbound++;
|
||||
$totalFailed++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 拼推送文本
|
||||
$text = $this->buildText($row);
|
||||
|
||||
$res = WechatWorkAppMessageService::sendTextToUser($wxId, $text);
|
||||
if (!($res['ok'] ?? false)) {
|
||||
$errMsg = (string) ($res['message'] ?? '企业微信推送失败');
|
||||
$this->markFailed($todoTable, $todoId, $errMsg);
|
||||
$totalFailed++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$totalSent++;
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('诊单待办推送异常 todo_id=' . $todoId . ' msg=' . $e->getMessage());
|
||||
try {
|
||||
$this->markFailed($todoTable, $todoId, '系统异常: ' . $e->getMessage());
|
||||
} catch (\Throwable $ee) {
|
||||
Log::error('回写失败状态出错 todo_id=' . $todoId . ' msg=' . $ee->getMessage());
|
||||
}
|
||||
$totalFailed++;
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('诊单待办批处理异常: ' . $e->getMessage());
|
||||
$output->error('批处理异常: ' . $e->getMessage());
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
$duration = round(microtime(true) - $startTs, 3);
|
||||
$output->writeln(sprintf(
|
||||
'处理完成。总数: %d, 成功: %d, 失败: %d (其中未绑定企微: %d), 并发跳过: %d, 耗时: %ss',
|
||||
$totalProcessed,
|
||||
$totalSent,
|
||||
$totalFailed,
|
||||
$totalUnbound,
|
||||
$totalSkipped,
|
||||
$duration
|
||||
));
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 拼接推送文本
|
||||
*
|
||||
* @param array<string,mixed> $row 待办记录原始行
|
||||
*/
|
||||
private function buildText(array $row): string
|
||||
{
|
||||
$patientName = '';
|
||||
$diagnosisId = (int) ($row['diagnosis_id'] ?? 0);
|
||||
if ($diagnosisId > 0) {
|
||||
$patientName = (string) Diagnosis::where('id', $diagnosisId)->value('patient_name');
|
||||
}
|
||||
|
||||
$remindAt = (int) ($row['remind_time'] ?? 0);
|
||||
$content = trim((string) ($row['content'] ?? ''));
|
||||
|
||||
$lines = [
|
||||
'【患者跟踪提醒】',
|
||||
'患者:' . ($patientName !== '' ? $patientName : '-'),
|
||||
'时间:' . ($remindAt > 0 ? date('Y-m-d H:i', $remindAt) : '-'),
|
||||
'内容:' . ($content !== '' ? $content : '-'),
|
||||
'— 二中心跟踪系统',
|
||||
];
|
||||
|
||||
return implode("\n", $lines);
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记一条待办为「发送失败」,写入 error 字段
|
||||
*/
|
||||
private function markFailed(string $todoTable, int $todoId, string $errMsg): void
|
||||
{
|
||||
$errMsg = mb_substr($errMsg, 0, 500);
|
||||
Db::name(self::stripTablePrefix($todoTable))
|
||||
->where('id', $todoId)
|
||||
->update([
|
||||
'status' => DiagnosisTodo::STATUS_FAILED,
|
||||
'error' => $errMsg,
|
||||
'update_time' => time(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Db::name() 接收的是不带前缀的表名,BloodRecord 等 Model::getTable() 返回的是带前缀的全名。
|
||||
* 这里去掉首段 `<prefix>_`。注意:项目实际前缀是 `zyt_`,但 think-orm 的 Db::name()
|
||||
* 内部会用 config('database.prefix') 自动拼回,所以这里只需剥离一次即可。
|
||||
*/
|
||||
private static function stripTablePrefix(string $tableWithPrefix): string
|
||||
{
|
||||
$prefix = (string) config('database.connections.mysql.prefix', '');
|
||||
if ($prefix !== '' && str_starts_with($tableWithPrefix, $prefix)) {
|
||||
return substr($tableWithPrefix, strlen($prefix));
|
||||
}
|
||||
|
||||
return $tableWithPrefix;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user