This commit is contained in:
Your Name
2026-10-08 14:12:44 +08:00
parent 5543f45850
commit 1c9b093e08
7 changed files with 268 additions and 54 deletions
@@ -4,19 +4,19 @@ declare(strict_types=1);
namespace app\adminapi\controller\qywx;
use app\adminapi\controller\BaseAdminController;
use app\adminapi\lists\qywx\CustomerLists;
use app\adminapi\logic\auth\AuthLogic;
use app\adminapi\logic\qywx\CustomerLogic;
use app\adminapi\validate\qywx\CustomerValidate;
use app\adminapi\controller\BaseAdminController;
use app\adminapi\lists\qywx\CustomerLists;
use app\adminapi\logic\auth\AuthLogic;
use app\adminapi\logic\qywx\CustomerLogic;
use app\adminapi\validate\qywx\CustomerValidate;
/**
* 企业微信客户管理控制器
*/
class CustomerController extends BaseAdminController
{
private const DELETE_PERMISSION = 'qywx.customer/delete';
class CustomerController extends BaseAdminController
{
private const DELETE_PERMISSION = 'qywx.customer/delete';
/**
* @notes 客户列表
*/
@@ -28,34 +28,34 @@ class CustomerController extends BaseAdminController
/**
* @notes 同步企业微信客户
*/
public function sync()
{
public function sync()
{
$result = CustomerLogic::triggerBackgroundSync();
if ($result === false) {
return $this->fail(CustomerLogic::getError());
}
$msg = is_array($result) && isset($result['message']) ? (string) $result['message'] : '已提交同步';
return $this->success($msg, $result);
}
/**
* @notes 删除一条本地企业微信客户同步记录
*/
public function delete()
{
// 显式鉴权,避免权限菜单迁移漏执行时被通用中间件当成“未受控 URI”放行。
if (!$this->canDeleteCustomer()) {
return $this->fail('权限不足,无法删除企业微信客户');
}
$params = (new CustomerValidate())->post()->goCheck('delete');
if (!CustomerLogic::deleteCustomer((int) $params['id'])) {
return $this->fail(CustomerLogic::getError());
}
return $this->success('删除成功');
}
return $this->success($msg, $result);
}
/**
* @notes 删除一条本地企业微信客户同步记录
*/
public function delete()
{
// 显式鉴权,避免权限菜单迁移漏执行时被通用中间件当成“未受控 URI”放行。
if (!$this->canDeleteCustomer()) {
return $this->fail('权限不足,无法删除企业微信客户');
}
$params = (new CustomerValidate())->post()->goCheck('delete');
if (!CustomerLogic::deleteCustomer((int) $params['id'])) {
return $this->fail(CustomerLogic::getError());
}
return $this->success('删除成功');
}
/**
* @notes 获取统计信息
@@ -105,22 +105,22 @@ class CustomerController extends BaseAdminController
/**
* @notes 保存同步设置
*/
public function saveSyncSettings()
public function saveSyncSettings()
{
$params = (new CustomerValidate())->post()->goCheck('syncSettings');
$result = CustomerLogic::saveSyncSettings($params);
if ($result === false) {
return $this->fail(CustomerLogic::getError());
}
return $this->success('保存成功');
}
private function canDeleteCustomer(): bool
{
if ((int) ($this->adminInfo['root'] ?? 0) === 1) {
return true;
}
return in_array(self::DELETE_PERMISSION, AuthLogic::getAuthByAdminId($this->adminId), true);
}
}
return $this->success('保存成功');
}
private function canDeleteCustomer(): bool
{
if ((int) ($this->adminInfo['root'] ?? 0) === 1) {
return true;
}
return in_array(self::DELETE_PERMISSION, AuthLogic::getAuthByAdminId($this->adminId), true);
}
}
@@ -53,6 +53,8 @@ class DiagnosisAiLogic extends BaseLogic
private const PERMISSION_ASSISTANT = 'tcm.diagnosis/aiassistant';
private const PERMISSION_GENERATE_PRESCRIPTION = 'tcm.diagnosis/aigenerateprescription';
private const PATIENT_OPTIONS_DEFAULT_PAGE_SIZE = 20;
private const PATIENT_OPTIONS_MAX_PAGE_SIZE = 50;
@@ -358,6 +360,17 @@ class DiagnosisAiLogic extends BaseLogic
array $adminInfo
): ?array {
self::$assistantErrorCode = 'AI_ASSISTANT_FAILED';
$task = strtolower(trim($task));
if (!isset(self::ASSISTANT_TASKS[$task])) {
self::setError('不支持的 AI 助手任务');
return null;
}
if ($task === 'prescription_generate'
&& !self::hasPermission($adminId, $adminInfo, self::PERMISSION_GENERATE_PRESCRIPTION)) {
self::setError('权限不足,无法使用 AI 生成处方');
return null;
}
$diagnosis = self::loadAuthorizedDiagnosis(
$diagnosisId,
$adminId,
@@ -369,11 +382,6 @@ class DiagnosisAiLogic extends BaseLogic
return null;
}
$task = strtolower(trim($task));
if (!isset(self::ASSISTANT_TASKS[$task])) {
self::setError('不支持的 AI 助手任务');
return null;
}
$prompt = self::cleanText($prompt, self::MAX_ASSISTANT_PROMPT_LENGTH, true);
if ($task === 'custom' && $prompt === '') {
self::setError('请输入要咨询的问题');
@@ -0,0 +1,35 @@
-- AI 生成处方单独授权。默认不给现有角色,须在管理端角色权限中明确勾选。
-- 默认表前缀为 zyt_;与 AI 助手同级显示。
START TRANSACTION;
SET @diagnosis_ai_assistant_parent_id := (
SELECT `pid`
FROM `zyt_system_menu`
WHERE `perms` = 'tcm.diagnosis/aiAssistant'
ORDER BY `id`
LIMIT 1
);
SET @diagnosis_menu_id := (
SELECT `id`
FROM `zyt_system_menu`
WHERE `perms` = 'tcm.diagnosis/lists' AND `type` = 'C'
ORDER BY `id`
LIMIT 1
);
INSERT INTO `zyt_system_menu` (
`pid`, `type`, `name`, `icon`, `sort`, `perms`, `paths`, `component`,
`selected`, `params`, `is_cache`, `is_show`, `is_disable`, `create_time`, `update_time`
)
SELECT
COALESCE(@diagnosis_ai_assistant_parent_id, @diagnosis_menu_id, 0),
'A', 'AI生成处方', '', 70, 'tcm.diagnosis/aiGeneratePrescription', '', '',
'', '', 0, 1, 0, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()
FROM DUAL
WHERE NOT EXISTS (
SELECT 1 FROM `zyt_system_menu`
WHERE `perms` = 'tcm.diagnosis/aiGeneratePrescription'
);
COMMIT;
@@ -0,0 +1,102 @@
<?php
declare(strict_types=1);
require dirname(__DIR__) . '/vendor/autoload.php';
use app\adminapi\logic\tcm\DiagnosisAiLogic;
final class PrescriptionGenerateAuthCacheDouble
{
/** @var array<int,string> */
public static array $uris = [];
public function __construct(int $adminId = 0)
{
}
/** @return array<int,string> */
public function getAdminUri(): array
{
return self::$uris;
}
}
function prescriptionPermissionExpect(bool $condition, string $message): void
{
if (!$condition) {
fwrite(STDERR, "FAIL: {$message}\n");
exit(1);
}
}
prescriptionPermissionExpect(
class_alias(PrescriptionGenerateAuthCacheDouble::class, 'app\\common\\cache\\AdminAuthCache'),
'permission cache double is installed before logic autoload'
);
$reflection = new ReflectionClass(DiagnosisAiLogic::class);
prescriptionPermissionExpect(
$reflection->getConstant('PERMISSION_GENERATE_PRESCRIPTION') === 'tcm.diagnosis/aigenerateprescription',
'prescription generation has a dedicated permission'
);
PrescriptionGenerateAuthCacheDouble::$uris = ['tcm.diagnosis/aiAssistant'];
prescriptionPermissionExpect(
DiagnosisAiLogic::prepareAssistant(1, 'prescription_generate', '', 7, ['root' => 0]) === null,
'general assistant permission alone cannot generate a prescription'
);
prescriptionPermissionExpect(
DiagnosisAiLogic::getError() === '权限不足,无法使用 AI 生成处方',
'denial names the missing prescription permission'
);
prescriptionPermissionExpect(
DiagnosisAiLogic::assistant(1, 'prescription_generate', '', 7, ['root' => 0]) === null,
'blocking endpoint rejects the task before any upstream call'
);
prescriptionPermissionExpect(
DiagnosisAiLogic::prepareAssistant(1, 'PRESCRIPTION_GENERATE', '', 7, ['root' => 0]) === null
&& DiagnosisAiLogic::getError() === '权限不足,无法使用 AI 生成处方',
'task name normalization cannot bypass the dedicated permission'
);
prescriptionPermissionExpect(
DiagnosisAiLogic::prepareAssistant(0, 'summary', '', 7, ['root' => 0]) === null
&& DiagnosisAiLogic::getError() === '诊单ID必须大于0',
'ordinary assistant tasks continue past the prescription permission check'
);
PrescriptionGenerateAuthCacheDouble::$uris = ['tcm.diagnosis/aiGeneratePrescription'];
prescriptionPermissionExpect(
DiagnosisAiLogic::prepareAssistant(1, 'prescription_generate', '', 7, ['root' => 0]) === null,
'prescription permission alone does not bypass the general assistant permission'
);
prescriptionPermissionExpect(
DiagnosisAiLogic::getError() === '权限不足,无法使用诊单 AI 助手',
'general assistant permission remains required'
);
$hasPermission = $reflection->getMethod('hasPermission');
prescriptionPermissionExpect(
$hasPermission->invoke(null, 7, ['root' => 0], 'tcm.diagnosis/aigenerateprescription') === true,
'new permission is read from role permissions'
);
prescriptionPermissionExpect(
$hasPermission->invoke(null, 1, ['root' => 1], 'tcm.diagnosis/aigenerateprescription') === true,
'root keeps its existing permission bypass'
);
$migration = file_get_contents(
dirname(__DIR__) . '/database/migrations/2026_10_08_diagnosis_ai_generate_prescription_permission.sql'
);
prescriptionPermissionExpect(
is_string($migration)
&& str_contains($migration, "'tcm.diagnosis/aiGeneratePrescription'")
&& str_contains($migration, "'AI生成处方'"),
'management role tree registers the dedicated checkbox'
);
prescriptionPermissionExpect(
!str_contains($migration, 'zyt_system_role_menu'),
'migration does not silently grant the new permission to existing roles'
);
echo "Diagnosis AI prescription generation permission: OK\n";