Files
zyt/server/app/adminapi/logic/tcm/FollowupAudioLogic.php
T

164 lines
7.4 KiB
PHP

<?php
declare(strict_types=1);
namespace app\adminapi\logic\tcm;
use app\common\service\followupaudio\FollowupAudioAccess as Access;
use app\common\service\followupaudio\FollowupAudioApply as Apply;
use app\common\service\followupaudio\FollowupAudioFields as Fields;
use app\common\service\followupaudio\FollowupAudioGate as Gate;
use app\common\service\followupaudio\FollowupAudioProviderConfig as ProviderConfig;
use app\common\service\followupaudio\FollowupAudioStore as Store;
use app\common\service\followupaudio\FollowupAudioUpload as Upload;
use DomainException;
final class FollowupAudioLogic
{
public static function capabilities(int $diagnosisId, int $actor, array $info): array
{
$diagnosis = Access::diagnosis($diagnosisId, $actor, $info, false, false);
$scope = Gate::scopeAllowed($diagnosisId, $actor);
$preview = Gate::previewOnly();
$ready = Store::ready();
$enabled = Store::enabled() && $scope;
$verified = Store::verified();
$daily = Access::canDaily($actor, $info);
return [
'enabled' => $enabled, 'audio_verified' => $verified,
'preview_only' => $preview, 'preview_ready' => $preview && $ready, 'test_scope_allowed' => $scope,
'can_upload' => $enabled && $ready, 'can_review' => $enabled && $ready, 'can_apply' => $enabled && $verified && !$preview,
'can_daily' => $daily, 'limits' => Upload::limits(),
'models' => $enabled ? ProviderConfig::readyModels() : [],
// No migration/dictionary dependency is introduced when the feature is OFF.
'fields' => $enabled ? self::catalogForActor($diagnosis, $actor, $info) : [],
];
}
public static function requireEnabled(bool $verified = false): void
{
if (!Store::enabled()) {
throw new DomainException('回访录音功能尚未启用');
}
if ($verified && !Store::ready()) {
throw new DomainException('当前服务与模型的音频能力尚未验证,暂不接受真实录音或写入');
}
}
public static function create(array $p, int $actor, array $info): array
{
self::requireEnabled(true);
Access::diagnosis($p['diagnosis_id'], $actor, $info);
$upload = Upload::owned($p['upload_id'], $actor, $info);
if ((int) $upload['diagnosis_id'] !== $p['diagnosis_id'] || $upload['status'] !== 'complete') {
throw new DomainException('录音与当前诊单不匹配或尚未上传完成');
}
Upload::path($upload);
$date = \DateTimeImmutable::createFromFormat('!Y-m-d H:i:s', $p['recorded_at'], new \DateTimeZone('Asia/Shanghai'));
if (!$date || $date->format('Y-m-d H:i:s') !== $p['recorded_at'] || $date->getTimestamp() > time() + 300) {
throw new DomainException('请填写正确的实际通话时间(北京时间),不能晚于当前时间');
}
if (!in_array($p['model_key'], ['qwen', 'openai'], true) || !Store::ready($p['model_key'])) {
throw new DomainException('所选模型音频能力尚未验证');
}
return Store::create($upload, $p['recorded_at'], $p['model_key'], $actor, $info);
}
public static function lists(int $diagnosisId, int $actor, array $info): array
{
Access::diagnosis($diagnosisId, $actor, $info);
return ['items' => Store::enabled() ? Store::lists($diagnosisId) : []];
}
public static function detail(int $id, int $actor, array $info): array
{
Access::task($id, $actor, $info);
return self::protectDetail(Store::detail($id), $actor, $info);
}
public static function saveDraft(int $id, int $version, array $items, int $actor, array $info, ?string $channelRoles = null): array
{
self::requireEnabled();
$task = Access::task($id, $actor, $info);
self::checkDailyItems($task, $items, $actor, $info);
return self::protectDetail(Store::saveDraft($id, $version, $items, $actor, $channelRoles), $actor, $info);
}
public static function apply(int $id, int $version, array $items, int $actor, array $info): array
{
Gate::assertMayApply();
self::requireEnabled(true);
$task = Access::task($id, $actor, $info);
self::checkDailyItems($task, $items, $actor, $info);
return Apply::apply($id, $version, $items, $actor, $info);
}
public static function retry(int $id, int $actor, array $info): array
{
self::requireEnabled(true);
Access::task($id, $actor, $info);
return Store::retry($id);
}
private static function checkDailyItems(array $task, array $items, int $actor, array $info): void
{
$diagnosis = Access::diagnosis((int) $task['diagnosis_id'], $actor, $info);
$plain = Access::allowed($actor, Access::actor($actor), 'tcm.diagnosis/phonePlain');
foreach ($items as $item) {
if (!is_array($item)) {
throw new DomainException('审阅内容无效');
}
if (!empty($item['selected']) && ($item['kind'] ?? '') === 'diagnosis' && !$plain) {
foreach (['phone', 'id_card'] as $key) {
if ($key === 'id_card' && trim((string) ($diagnosis[$key] ?? '')) === '') {
continue;
}
if (array_key_exists($key, (array) ($item['values'] ?? []))
&& (string) $item['values'][$key] !== (string) ($diagnosis[$key] ?? '')) {
throw new DomainException('无权通过录音修改已有手机号或身份证');
}
}
}
if (!empty($item['selected']) && ($item['kind'] ?? '') !== 'diagnosis') {
Access::diagnosis((int) $task['diagnosis_id'], $actor, $info, true);
// Continue to validate identity suggestions later in this same batch.
}
}
}
private static function catalogForActor(array $diagnosis, int $actor, array $info): array
{
$info = Access::actor($actor);
$catalog = Fields::catalog();
$basic = DiagnosisLogic::canEditPatientBasicInfo((int) $diagnosis['id'], $info);
$plain = Access::allowed($actor, $info, 'tcm.diagnosis/phonePlain');
foreach ($catalog['diagnosis'] as &$field) {
$key = $field['key'];
if ((!$basic && in_array($key, ['patient_name', 'id_card', 'phone', 'gender', 'age'], true))
|| (!$plain && ($key === 'phone' || ($key === 'id_card' && !empty($diagnosis['id_card']))))) {
$field['readonly'] = true;
}
}
unset($field);
return $catalog;
}
private static function protectDetail(array $detail, int $actor, array $info): array
{
$diagnosis = Access::diagnosis((int) $detail['diagnosis_id'], $actor, $info);
$detail['fields'] = self::catalogForActor($diagnosis, $actor, $info);
if (!Access::allowed($actor, Access::actor($actor), 'tcm.diagnosis/phonePlain')) {
foreach ($detail['items'] as &$item) {
foreach (['phone', 'id_card'] as $key) {
$value = (string) ($item['current_values'][$key] ?? '');
if ($value !== '') {
$item['current_values'][$key] = mb_substr($value, 0, 3) . '****' . mb_substr($value, -4);
}
}
}
unset($item);
}
return $detail;
}
}