This commit is contained in:
Your Name
2026-03-13 14:08:52 +08:00
parent 08dd9cd307
commit eb283f008b
667 changed files with 8425 additions and 27053 deletions
@@ -24,7 +24,7 @@ use app\adminapi\validate\LoginValidate;
*/
class LoginController extends BaseAdminController
{
public array $notNeedLogin = ['account'];
public array $notNeedLogin = ['account', 'workWechatConfig', 'workWechatLogin', 'checkDbColumn'];
/**
* @notes 账号登录
@@ -41,6 +41,66 @@ class LoginController extends BaseAdminController
return $this->data((new LoginLogic())->login($params));
}
/**
* @notes 获取企业微信登录配置(前端构造OAuth URL/扫码用)
*/
public function workWechatConfig()
{
$corpId = env('work_wechat.corp_id', '');
$agentId = env('work_wechat.agent_id', '');
if (empty($corpId) || empty($agentId)) {
return $this->data(['enabled' => false]);
}
return $this->data([
'enabled' => true,
'corp_id' => $corpId,
'agent_id' => $agentId,
]);
}
/**
* @notes 企业微信授权登录(用 code 换 token
*/
public function workWechatLogin()
{
$code = $this->request->post('code', '');
if (empty($code)) {
return $this->fail('缺少授权code');
}
$terminal = $this->request->post('terminal', 1);
$result = (new LoginLogic())->workWechatLogin([
'code' => $code,
'terminal' => $terminal,
]);
if ($result === false) {
return $this->fail(LoginLogic::getError());
}
return $this->data($result);
}
/**
* @notes 临时调试:检查 admin 表是否有 work_wechat_userid 列(确认后请删除此方法)
*/
public function checkDbColumn()
{
$dbName = env('database.database', '');
$prefix = env('database.prefix', '');
$tableName = $prefix . 'admin';
$columns = \think\facade\Db::query("SHOW COLUMNS FROM `{$tableName}` LIKE 'work_wechat_userid'");
return $this->data([
'database' => $dbName,
'table' => $tableName,
'column_exists' => !empty($columns),
'columns_result' => $columns,
]);
}
/**
* @notes 退出登录
* @return \think\response\Json
@@ -18,7 +18,9 @@ use app\adminapi\controller\BaseAdminController;
use app\adminapi\lists\auth\AdminLists;
use app\adminapi\validate\auth\AdminValidate;
use app\adminapi\logic\auth\AdminLogic;
use app\adminapi\logic\LoginLogic;
use app\adminapi\validate\auth\editSelfValidate;
use app\common\model\auth\Admin;
/**
* 管理员控制器
@@ -131,4 +133,74 @@ class AdminController extends BaseAdminController
return $this->success('操作成功', [], 1, 1);
}
/**
* @notes 企业微信扫码绑定(用 code 换取 userid 并保存到当前管理员)
*/
public function bindWorkWechat()
{
$code = $this->request->post('code', '');
if (empty($code)) {
return $this->fail('缺少授权code');
}
$corpId = env('work_wechat.corp_id', '');
$secret = env('work_wechat.secret', '');
if (empty($corpId) || empty($secret)) {
return $this->fail('企业微信未配置');
}
$accessToken = LoginLogic::getWorkWechatAccessTokenStatic($corpId, $secret);
if (!$accessToken) {
return $this->fail('获取企业微信凭证失败');
}
$url = "https://qyapi.weixin.qq.com/cgi-bin/auth/getuserinfo?access_token={$accessToken}&code={$code}";
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
$result = curl_exec($ch);
curl_close($ch);
$response = json_decode($result, true);
if (!$response || ($response['errcode'] ?? -1) != 0) {
return $this->fail('企业微信授权失败: ' . ($response['errmsg'] ?? '未知错误'));
}
$wxUserId = $response['userid'] ?? '';
if (empty($wxUserId)) {
return $this->fail('未获取到企业微信用户身份');
}
// 检查是否已被其他管理员绑定
$exists = Admin::where('work_wechat_userid', $wxUserId)
->where('id', '<>', $this->adminId)
->find();
if ($exists) {
return $this->fail('该企业微信账号已被其他管理员绑定');
}
Admin::update([
'id' => $this->adminId,
'work_wechat_userid' => $wxUserId,
]);
return $this->success('绑定成功', ['work_wechat_userid' => $wxUserId]);
}
/**
* @notes 解绑企业微信
*/
public function unbindWorkWechat()
{
Admin::update([
'id' => $this->adminId,
'work_wechat_userid' => '',
]);
return $this->success('解绑成功');
}
}
@@ -102,6 +102,29 @@ class DiagnosisController extends BaseAdminController
return $this->data($result);
}
/**
* @notes 诊单详情(患者端)
* @return \think\response\Json
*/
public function diagnosisDetail()
{
$params = $this->request->get();
if (empty($params['id'])) {
return $this->fail('诊单ID不能为空');
}
if (empty($params['user_id'])) {
return $this->fail('用户ID不能为空');
}
$result = DiagnosisLogic::diagnosisDetail($params);
if ($result) {
return $this->data($result);
}
return $this->fail(DiagnosisLogic::getError());
}
/**
* @notes 检查手机号是否重复
* @return \think\response\Json
@@ -58,6 +58,7 @@ class AppointmentLists extends BaseAdminDataLists implements ListsSearchInterfac
// 构建查询
$query = Appointment::alias('a')
->with('diagnosis')
->leftJoin('tcm_diagnosis u', 'a.patient_id = u.id')
->leftJoin('admin ad', 'a.doctor_id = ad.id')
->field('a.*, u.patient_name as patient_name, u.phone as patient_phone, ad.name as doctor_name, u.id as diagnosis_id')
+130
View File
@@ -19,6 +19,8 @@ use app\common\model\auth\Admin;
use app\adminapi\service\AdminTokenService;
use app\common\service\FileService;
use think\facade\Config;
use think\facade\Cache;
use think\facade\Log;
/**
* 登录逻辑
@@ -62,6 +64,134 @@ class LoginLogic extends BaseLogic
}
/**
* @notes 企业微信授权登录
*/
public function workWechatLogin($params)
{
$code = $params['code'];
$terminal = $params['terminal'] ?? 1;
$corpId = env('work_wechat.corp_id', '');
$secret = env('work_wechat.secret', '');
if (empty($corpId) || empty($secret)) {
self::setError('企业微信未配置');
return false;
}
// 获取 access_token
$accessToken = self::getWorkWechatAccessToken($corpId, $secret);
if (!$accessToken) {
return false;
}
// 用 code 换取用户身份
$url = "https://qyapi.weixin.qq.com/cgi-bin/auth/getuserinfo?access_token={$accessToken}&code={$code}";
$response = self::httpGet($url);
if (!$response || $response['errcode'] != 0) {
$errMsg = $response['errmsg'] ?? '未知错误';
Log::error('企业微信获取用户身份失败: ' . $errMsg);
self::setError('企业微信授权失败: ' . $errMsg);
return false;
}
$wxUserId = $response['userid'] ?? '';
if (empty($wxUserId)) {
self::setError('未获取到企业微信用户身份,可能是外部联系人');
return false;
}
// 通过 work_wechat_userid 匹配管理员(SoftDelete trait 自动排除已删除记录)
$admin = Admin::where('work_wechat_userid', '=', $wxUserId)->find();
if (!$admin) {
self::setError('该企业微信账号未绑定管理员,请联系管理员绑定(UserId: ' . $wxUserId . '');
return false;
}
if ($admin->disable === 1) {
self::setError('账号已被禁用');
return false;
}
// 更新登录信息
$time = time();
$admin->login_time = $time;
$admin->login_ip = request()->ip();
$admin->save();
// 设置 token
$adminInfo = AdminTokenService::setToken($admin->id, $terminal, $admin->multipoint_login);
$avatar = $admin->avatar ?: Config::get('project.default_image.admin_avatar');
$avatar = FileService::getFileUrl($avatar);
return [
'name' => $adminInfo['name'],
'avatar' => $avatar,
'role_name' => $adminInfo['role_name'],
'token' => $adminInfo['token'],
];
}
/**
* @notes 获取企业微信 access_token(带缓存),供外部(如绑定流程)调用
*/
public static function getWorkWechatAccessTokenStatic(string $corpId, string $secret)
{
return self::getWorkWechatAccessToken($corpId, $secret);
}
/**
* @notes 获取企业微信 access_token(带缓存)
*/
private static function getWorkWechatAccessToken(string $corpId, string $secret)
{
$cacheKey = 'work_wechat_access_token_' . md5($corpId . $secret);
$cached = Cache::get($cacheKey);
if ($cached) {
return $cached;
}
$url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid={$corpId}&corpsecret={$secret}";
$response = self::httpGet($url);
if (!$response || $response['errcode'] != 0) {
$errMsg = $response['errmsg'] ?? '未知错误';
Log::error('获取企业微信access_token失败: ' . $errMsg);
self::setError('获取企业微信凭证失败: ' . $errMsg);
return false;
}
$accessToken = $response['access_token'];
Cache::set($cacheKey, $accessToken, $response['expires_in'] - 200);
return $accessToken;
}
/**
* @notes HTTP GET 请求
*/
private static function httpGet(string $url)
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
$result = curl_exec($ch);
curl_close($ch);
if ($result === false) {
return null;
}
return json_decode($result, true);
}
/**
* @notes 退出登录
* @param $adminInfo
@@ -253,7 +253,8 @@ class AdminLogic extends BaseLogic
'id', 'account', 'name', 'disable', 'root',
'multipoint_login', 'avatar',
'gender', 'age', 'phone', 'title', 'department',
'specialty', 'education', 'experience', 'honors'
'specialty', 'education', 'experience', 'honors',
'work_wechat_userid'
])->findOrEmpty($params['id'])->toArray();
if ($action == 'detail') {
+142 -30
View File
@@ -66,7 +66,7 @@ class DiagnosisLogic extends BaseLogic
$multiSelectFields = [
'diet_condition', 'body_feeling', 'sleep_condition', 'eye_condition',
'head_feeling', 'sweat_condition', 'skin_condition', 'urine_condition',
'stool_condition', 'kidney_condition'
'stool_condition', 'kidney_condition', 'local_hospital_diagnosis'
];
foreach ($multiSelectFields as $field) {
@@ -156,7 +156,7 @@ class DiagnosisLogic extends BaseLogic
$multiSelectFields = [
'diet_condition', 'body_feeling', 'sleep_condition', 'eye_condition',
'head_feeling', 'sweat_condition', 'skin_condition', 'urine_condition',
'stool_condition', 'kidney_condition'
'stool_condition', 'kidney_condition', 'local_hospital_diagnosis'
];
foreach ($multiSelectFields as $field) {
@@ -231,7 +231,7 @@ class DiagnosisLogic extends BaseLogic
$multiSelectFields = [
'diet_condition', 'body_feeling', 'sleep_condition', 'eye_condition',
'head_feeling', 'sweat_condition', 'skin_condition', 'urine_condition',
'stool_condition', 'kidney_condition'
'stool_condition', 'kidney_condition', 'local_hospital_diagnosis'
];
foreach ($multiSelectFields as $field) {
@@ -242,18 +242,105 @@ class DiagnosisLogic extends BaseLogic
}
}
// 处理舌苔照片(JSON转数组)
if (!empty($diagnosis['tongue_images'])) {
$diagnosis['tongue_images'] = json_decode($diagnosis['tongue_images'], true) ?: [];
} else {
$diagnosis['tongue_images'] = [];
}
// 先尝试 JSON 解析(兼容旧数据)
$files = json_decode($diagnosis['tongue_images'], true);
if (is_array($files)) {
$diagnosis['tongue_images'] = $files;
} else {
// JSON 解析失败则按逗号分割
$diagnosis['tongue_images'] = array_filter(explode(',', $diagnosis['tongue_images'])) ?: [];
}
} else {
$diagnosis['tongue_images'] = [];
}
// 处理检查报告(JSON转数组)
if (!empty($diagnosis['report_files'])) {
$diagnosis['report_files'] = json_decode($diagnosis['report_files'], true) ?: [];
if (!empty($diagnosis['report_files'])) {
// 先尝试 JSON 解析(兼容旧数据)
$files = json_decode($diagnosis['report_files'], true);
if (is_array($files)) {
$diagnosis['report_files'] = $files;
} else {
// JSON 解析失败则按逗号分割
$diagnosis['report_files'] = array_filter(explode(',', $diagnosis['report_files'])) ?: [];
}
} else {
$diagnosis['report_files'] = [];
}
// 处理诊断日期格式
if (!empty($diagnosis['diagnosis_date'])) {
$diagnosis['diagnosis_date'] = date('Y-m-d', $diagnosis['diagnosis_date']);
}
// 如果local_hospital_visit_date为空但diagnosis_date有值,则使用diagnosis_date
if (empty($diagnosis['local_hospital_visit_date']) && !empty($diagnosis['diagnosis_date'])) {
$diagnosis['local_hospital_visit_date'] = $diagnosis['diagnosis_date'];
}
return $diagnosis;
}
/**
* @notes 诊单详情(患者端)
* @param array $params
* @return array
*/
public static function diagnosisDetail(array $params): array
{
$diagnosisId = $params['id'] ?? 0;
$userId = $params['user_id'] ?? 0;
if (!$diagnosisId || !$userId) {
self::setError('参数不完整');
return [];
}
$diagnosis = Diagnosis::where('id', $diagnosisId)
->where('patient_id', $userId)
->findOrEmpty()
->toArray();
if (empty($diagnosis)) {
self::setError('诊单不存在或无权限访问');
return [];
}
// 处理既往史为数组
if (!empty($diagnosis['past_history'])) {
$diagnosis['past_history'] = explode(',', $diagnosis['past_history']);
} else {
$diagnosis['report_files'] = [];
$diagnosis['past_history'] = [];
}
// 处理现病史多选字段为数组
$multiSelectFields = [
'diet_condition', 'body_feeling', 'sleep_condition', 'eye_condition',
'head_feeling', 'sweat_condition', 'skin_condition', 'urine_condition',
'stool_condition', 'kidney_condition', 'local_hospital_diagnosis'
];
foreach ($multiSelectFields as $field) {
if (!empty($diagnosis[$field])) {
$diagnosis[$field] = explode(',', $diagnosis[$field]);
} else {
$diagnosis[$field] = [];
}
}
// 处理检查报告为数组
if (!empty($diagnosis['examination_report'])) {
$diagnosis['examination_report'] = explode(',', $diagnosis['examination_report']);
} else {
$diagnosis['examination_report'] = [];
}
// 处理舌苔照片为数组
if (!empty($diagnosis['tongue_photo'])) {
$diagnosis['tongue_photo'] = explode(',', $diagnosis['tongue_photo']);
} else {
$diagnosis['tongue_photo'] = [];
}
// 处理诊断日期格式
@@ -261,6 +348,11 @@ class DiagnosisLogic extends BaseLogic
$diagnosis['diagnosis_date'] = date('Y-m-d', $diagnosis['diagnosis_date']);
}
// 如果local_hospital_visit_date为空但diagnosis_date有值,则使用diagnosis_date
if (empty($diagnosis['local_hospital_visit_date']) && !empty($diagnosis['diagnosis_date'])) {
$diagnosis['local_hospital_visit_date'] = $diagnosis['diagnosis_date'];
}
return $diagnosis;
}
@@ -360,7 +452,10 @@ class DiagnosisLogic extends BaseLogic
// 医生userId
$doctorUserId = 'doctor_' . $adminId;
$query = Diagnosis::where('id', $patientId)
->where('delete_time', null)->find();
// 患者userId(必须与小程序端一致)
$patientUserId = 'patient_' . $patientId;
@@ -382,6 +477,7 @@ class DiagnosisLogic extends BaseLogic
'sdkAppId' => (int)$config['sdkAppId'], // 确保返回整数
'userId' => $doctorUserId, // 医生的userId
'userSig' => $userSig,
'assistant_id'=>$query->assistant_id?'doctor_'.$query->assistant_id:'',
'patientUserId' => $patientUserId, // 患者的userId(用于发起通话)
'expireTime' => 86400 // 24小时
];
@@ -720,14 +816,14 @@ class DiagnosisLogic extends BaseLogic
return false;
}
// 患者userId
$patientUserId = 'doctor_' . $patientId;
// 医助userId(使用 doctor_ 前缀)
$doctorUserId = 'doctor_' . $patientId;
// 生成患者的 UserSig
// 生成医助的 UserSig
$userSig = self::generateUserSig(
$config['sdkAppId'],
$config['secretKey'],
$patientUserId
$doctorUserId
);
if (!$userSig) {
@@ -735,19 +831,19 @@ class DiagnosisLogic extends BaseLogic
return false;
}
// 确保患者账号已导入IM
self::ensurePatientImAccount($patientId, $patientUserId);
// 确保医助账号已导入IM(使用正确的方法)
self::importDoctorAccountToIm($patientId, $doctorUserId);
\think\facade\Log::info('小程序获取患者签名成功 - doctor_id: ' . $patientId . ', user_id: ' . $patientUserId);
\think\facade\Log::info('获取医助签名成功 - admin_id: ' . $patientId . ', user_id: ' . $doctorUserId);
return [
'sdkAppId' => (int)$config['sdkAppId'],
'userId' => $patientUserId,
'userId' => $doctorUserId,
'userSig' => $userSig,
'expireTime' => 86400
];
} catch (\Exception $e) {
\think\facade\Log::error('获取患者签名失败 - patient_id: ' . $patientId . ', error: ' . $e->getMessage());
\think\facade\Log::error('获取医助签名失败 - admin_id: ' . $patientId . ', error: ' . $e->getMessage());
self::setError($e->getMessage());
return false;
}
@@ -868,8 +964,8 @@ class DiagnosisLogic extends BaseLogic
throw new \Exception('小程序配置未设置');
}
// 构建小程序路径和参数
$page = 'pages/order/monad/monad';
// 构建小程序路径和参数(前端可传 mini_program_path 覆盖默认路径)
$page = !empty($params['mini_program_path']) ? $params['mini_program_path'] : 'pages/order/monad/monad';
$scene = "id={$diagnosisId}&share_user={$shareUserId}";
// 调用微信接口生成小程序码
@@ -1346,7 +1442,17 @@ class DiagnosisLogic extends BaseLogic
$updateData = [
'patient_name' => $params['patient_name'] ?? '',
'gender' => $params['gender'] ?? 1,
'age' => $params['age'] ?? 0,
'age' => !empty($params['age']) ? intval($params['age']) : 0,
'id_card' => $params['id_card'] ?? '',
'height' => !empty($params['height']) ? floatval($params['height']) : null,
'weight' => !empty($params['weight']) ? floatval($params['weight']) : null,
'systolic_pressure' => !empty($params['systolic_pressure']) ? intval($params['systolic_pressure']) : null,
'diastolic_pressure' => !empty($params['diastolic_pressure']) ? intval($params['diastolic_pressure']) : null,
'fasting_blood_sugar' => !empty($params['fasting_blood_sugar']) ? floatval($params['fasting_blood_sugar']) : null,
'diabetes_discovery_year' => !empty($params['diabetes_discovery_year']) ? intval($params['diabetes_discovery_year']) : null,
'local_hospital_diagnosis' => $params['local_hospital_diagnosis'] ?? '',
'local_hospital_name' => $params['local_hospital_name'] ?? '',
'local_hospital_visit_date' => $params['local_hospital_visit_date'] ?? '',
'diagnosis_type' => $params['diagnosis_type'] ?? '',
'syndrome_type' => $params['syndrome_type'] ?? '',
'diabetes_type' => $params['diabetes_type'] ?? '',
@@ -1367,14 +1473,16 @@ class DiagnosisLogic extends BaseLogic
'fatty_liver_degree' => $params['fatty_liver_degree'] ?? '',
// 既往史字段
'past_history' => $params['past_history'] ?? '',
'trauma_history' => $params['trauma_history'] ?? 0,
'surgery_history' => $params['surgery_history'] ?? 0,
'allergy_history' => $params['allergy_history'] ?? 0,
'family_history' => $params['family_history'] ?? 0,
'pregnancy_history' => $params['pregnancy_history'] ?? 0,
'trauma_history' => isset($params['trauma_history']) ? intval($params['trauma_history']) : 0,
'surgery_history' => isset($params['surgery_history']) ? intval($params['surgery_history']) : 0,
'allergy_history' => isset($params['allergy_history']) ? intval($params['allergy_history']) : 0,
'family_history' => isset($params['family_history']) ? intval($params['family_history']) : 0,
'pregnancy_history' => isset($params['pregnancy_history']) ? intval($params['pregnancy_history']) : 0,
// 临床信息
'symptoms' => $params['symptoms'] ?? '',
'tongue_coating' => $params['tongue_coating'] ?? '',
'tongue_images' => $params['tongue_images'] ?? '',
'report_files' => $params['report_files'] ?? '',
'pulse' => $params['pulse'] ?? '',
'treatment_principle' => $params['treatment_principle'] ?? '',
'prescription' => $params['prescription'] ?? '',
@@ -1386,8 +1494,12 @@ class DiagnosisLogic extends BaseLogic
// 处理诊断日期
if (!empty($params['diagnosis_date'])) {
$updateData['diagnosis_date'] = strtotime($params['diagnosis_date']);
} elseif (!empty($params['local_hospital_visit_date'])) {
// 如果没有diagnosis_date但有local_hospital_visit_date,则使用local_hospital_visit_date
$updateData['diagnosis_date'] = strtotime($params['local_hospital_visit_date']);
}
// 执行更新
$diagnosis->save($updateData);
@@ -62,7 +62,7 @@ class DiagnosisValidate extends BaseValidate
public function sceneEdit()
{
return $this->only(['id', 'patient_name', 'id_card', 'phone', 'gender', 'age', 'diagnosis_date', 'diagnosis_type', 'syndrome_type', 'past_history', 'symptoms', 'tongue_coating', 'pulse', 'treatment_principle', 'prescription', 'doctor_advice', 'remark', 'status']);
return $this->only(['id', 'patient_name', 'id_card', 'phone', 'gender', 'age', 'diagnosis_date', 'diagnosis_type', 'syndrome_type', 'marital_status', 'height', 'weight', 'region', 'systolic_pressure', 'diastolic_pressure', 'fasting_blood_sugar', 'diabetes_discovery_year', 'local_hospital_diagnosis', 'local_hospital_name', 'past_history', 'symptoms', 'tongue_coating', 'pulse', 'treatment_principle', 'prescription', 'doctor_advice', 'remark', 'status']);
}
public function sceneId()