Files
zyt/server/app/adminapi/logic/tcm/MobileRecordingLogic.php
2026-11-09 11:56:28 +08:00

244 lines
11 KiB
PHP

<?php
declare(strict_types=1);
namespace app\adminapi\logic\tcm;
use app\adminapi\logic\firstvisit\MyPatientLogic;
use app\common\model\tcm\Diagnosis;
use app\common\service\mobilerecording\MobileRecordingStorage;
use DomainException;
use think\db\Query;
use think\facade\Db;
/** Backward-compatible metadata API; media lifecycle follows the binding transaction. */
final class MobileRecordingLogic
{
public const PAGE_PERMISSION = 'firstvisit.myPatient/lists';
/** Re-read the actor and grants; token role/root snapshots are not authorization. */
public static function actor(int $adminId, bool $lock = false): array
{
$row = Db::name('admin')->where('id', $adminId)->whereNull('delete_time')
->where('disable', 0)->lock($lock)->find();
if ($adminId <= 0 || !$row) {
throw new DomainException('账号已停用或登录状态失效');
}
$roles = Db::name('admin_role')->where('admin_id', $adminId)->column('role_id');
$info = ['admin_id' => $adminId, 'id' => $adminId, 'root' => (int) $row['root'], 'role_id' => $roles];
if ($info['root'] !== 1) {
$menuIds = $roles ? Db::name('system_role_menu')->whereIn('role_id', $roles)->column('menu_id') : [];
$allowed = $menuIds && Db::name('system_menu')->whereIn('id', $menuIds)
->where('is_disable', 0)->where('perms', self::PAGE_PERMISSION)->count() > 0;
if (!$allowed) {
throw new DomainException('权限不足,无法访问我的患者录音关联');
}
}
return $info;
}
public static function bind(array $params, int $adminId): array
{
$p = self::bindingInput($params);
$garbage = [];
$result = Db::transaction(static function () use ($p, $adminId, &$garbage): array {
// Serialize mutations for this actor, including the first INSERT for an absent key.
$info = self::actor($adminId, true);
$existing = self::ownedQuery($adminId, $p)->lock(true)->find();
$ids = array_unique([$p['diagnosis_id'], (int) ($existing['diagnosis_id'] ?? $p['diagnosis_id'])]);
sort($ids);
$patients = [];
foreach ($ids as $id) {
$patients[$id] = self::diagnosis($id, $adminId, $info, true);
}
$now = time();
if ($existing) {
$id = (int) $existing['id'];
if ((int) $existing['diagnosis_id'] !== $p['diagnosis_id']) {
$garbage = MobileRecordingMediaLogic::bindingMoved($id, $p['diagnosis_id']);
}
self::ownedQuery($adminId, $p)->where('id', $id)->update($p + ['updated_at' => $now]);
$row = array_merge($existing, $p, ['updated_at' => $now]);
} else {
$row = $p + ['admin_id' => $adminId, 'created_at' => $now, 'updated_at' => $now];
$row['id'] = (int) Db::name('mobile_recording_binding')->insertGetId($row);
}
return ['binding' => self::deviceRow($row, $patients[$p['diagnosis_id']])];
});
MobileRecordingStorage::cleanup($garbage);
return $result;
}
public static function unbind(array $params, int $adminId): array
{
self::only($params, ['installation_id', 'recording_key']);
$identity = self::identity($params);
$garbage = [];
$result = Db::transaction(static function () use ($identity, $adminId, &$garbage): array {
$info = self::actor($adminId, true);
$row = self::ownedQuery($adminId, $identity)->lock(true)->find();
if ($row) {
self::diagnosis((int) $row['diagnosis_id'], $adminId, $info, true);
$garbage = MobileRecordingMediaLogic::bindingRemoved((int) $row['id']);
self::ownedQuery($adminId, $identity)->where('id', (int) $row['id'])->delete();
}
return ['unbound' => true];
});
MobileRecordingStorage::cleanup($garbage);
return $result;
}
public static function deviceLists(array $params, int $adminId): array
{
self::only($params, ['installation_id', 'page_no', 'page_size']);
$installationId = self::installation($params['installation_id'] ?? null);
[$page, $size] = self::pagination($params, 50);
$info = self::actor($adminId);
$query = Db::name('mobile_recording_binding')->alias('b')
->join([(new Diagnosis())->getTable() => 'd'], 'd.id = b.diagnosis_id')
->where('b.admin_id', $adminId)->where('b.installation_id', $installationId)
->where('d.status', 1)->whereNull('d.delete_time');
MyPatientLogic::applyScope($query, $adminId, $info);
$count = (int) (clone $query)->count('b.id');
$rows = $query->field('b.*,d.patient_id AS source_patient_id,d.patient_name,d.phone AS patient_phone')
->order('b.id', 'desc')->page($page, $size)->select()->toArray();
$items = array_map(static fn (array $row): array => self::deviceRow($row, [
'id' => $row['diagnosis_id'], 'patient_id' => $row['source_patient_id'],
'patient_name' => $row['patient_name'], 'phone' => $row['patient_phone'],
]), $rows);
return ['lists' => $items, 'count' => $count, 'page_no' => $page, 'page_size' => $size];
}
public static function patientLists(array $params, int $adminId): array
{
self::only($params, ['diagnosis_id', 'page_no', 'page_size']);
$diagnosisId = self::integer($params['diagnosis_id'] ?? null, 1, 4294967295, '诊单标识');
[$page, $size] = self::pagination($params, 20);
$info = self::actor($adminId);
self::diagnosis($diagnosisId, $adminId, $info);
$query = Db::name('mobile_recording_binding')->alias('b')->where('b.diagnosis_id', $diagnosisId);
$count = (int) (clone $query)->count();
$adminTable = Db::name('admin')->getTable();
$rows = $query->leftJoin([$adminTable => 'a'], 'a.id = b.admin_id')
->field('b.id,b.diagnosis_id,b.source_platform,b.recorded_at,b.duration_ms,b.created_at,b.updated_at,a.name AS bound_by')
->order('b.recorded_at', 'desc')->order('b.id', 'desc')->page($page, $size)->select()->toArray();
$media = MobileRecordingMediaLogic::summaries(array_column($rows, 'id'));
$items = array_map(static function (array $row) use ($media): array {
$dto = self::metadata($row);
$dto['bound_by'] = trim((string) ($row['bound_by'] ?? '')) ?: '已停用账号';
$dto += $media[$row['id']];
$dto['playback_location'] = $dto['audio_available'] ? 'server' : 'original_device';
return $dto;
}, $rows);
return ['lists' => $items, 'count' => $count, 'page_no' => $page, 'page_size' => $size];
}
/** Public for the controller and executable input-contract tests. */
public static function bindingInput(array $params): array
{
self::only($params, ['installation_id', 'recording_key', 'diagnosis_id', 'source_platform', 'recorded_at', 'duration_ms']);
if (($params['source_platform'] ?? null) !== 'android_bcr') {
throw new DomainException('录音来源无效');
}
return self::identity($params) + [
'diagnosis_id' => self::integer($params['diagnosis_id'] ?? null, 1, 4294967295, '诊单标识'),
'source_platform' => 'android_bcr',
'recorded_at' => self::integer($params['recorded_at'] ?? null, 0, (time() + 300) * 1000, '录音时间'),
'duration_ms' => self::integer($params['duration_ms'] ?? null, 0, 604800000, '录音时长'),
];
}
private static function ownedQuery(int $adminId, array $identity): Query
{
return Db::name('mobile_recording_binding')->where('admin_id', $adminId)
->where('installation_id', $identity['installation_id'])->where('recording_key', $identity['recording_key']);
}
public static function diagnosis(int $id, int $adminId, array $info, bool $lock = false): array
{
$row = Db::name('tcm_diagnosis')->where('id', $id)->whereNull('delete_time')->where('status', 1)
->lock($lock)->find();
if (!$row || !MyPatientLogic::canAccessDiagnosis($id, $adminId, $info)) {
throw new DomainException('患者不存在或无权操作');
}
return $row;
}
private static function deviceRow(array $row, array $patient): array
{
return self::metadata($row) + [
'installation_id' => (string) $row['installation_id'],
'recording_key' => (string) $row['recording_key'],
'patient' => [
'diagnosis_id' => (int) $patient['id'],
'source_patient_id' => (int) ($patient['patient_id'] ?? 0),
'patient_name' => (string) ($patient['patient_name'] ?? ''),
'phone_masked' => self::maskPhone((string) ($patient['phone'] ?? '')),
],
];
}
private static function metadata(array $row): array
{
$result = ['source_platform' => (string) $row['source_platform']];
foreach (['id', 'diagnosis_id', 'recorded_at', 'duration_ms', 'created_at', 'updated_at'] as $field) {
$result[$field] = (int) $row[$field];
}
return $result;
}
private static function maskPhone(string $phone): string
{
$digits = preg_replace('/\D+/', '', $phone) ?? '';
$length = strlen($digits);
if ($length <= 4) {
return str_repeat('*', $length);
}
if ($length < 8) {
return substr($digits, 0, 1) . str_repeat('*', $length - 2) . substr($digits, -1);
}
return substr($digits, 0, 3) . str_repeat('*', $length - 7) . substr($digits, -4);
}
public static function identity(array $params): array
{
$key = $params['recording_key'] ?? null;
if (!is_string($key) || !preg_match('/^[0-9a-f]{64}$/D', $key)) {
throw new DomainException('录音标识无效');
}
return ['installation_id' => self::installation($params['installation_id'] ?? null), 'recording_key' => $key];
}
private static function installation($value): string
{
if (!is_string($value) || !preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/D', $value)) {
throw new DomainException('设备标识无效');
}
return $value;
}
private static function pagination(array $params, int $defaultSize): array
{
return [
self::integer($params['page_no'] ?? 1, 1, 1000000, '页码'),
self::integer($params['page_size'] ?? $defaultSize, 1, 100, '每页数量'),
];
}
public static function integer($value, int $min, int $max, string $label): int
{
if (!(is_int($value) || is_string($value)) || !preg_match('/^(0|[1-9][0-9]{0,14})$/D', (string) $value)
|| (int) $value < $min || (int) $value > $max) {
throw new DomainException($label . '无效');
}
return (int) $value;
}
public static function only(array $params, array $allowed): void
{
if (array_diff(array_keys($params), $allowed) !== []) {
throw new DomainException('请求包含不支持的字段');
}
}
}