Files
zyt/server/app/api/controller/TcmController.php
T
2026-06-01 09:34:27 +08:00

1209 lines
46 KiB
PHP
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
// +----------------------------------------------------------------------
// | likeadmin快速开发前后端分离管理后台(PHP版)
// +----------------------------------------------------------------------
// | 欢迎阅读学习系统程序代码,建议反馈是我们前进的动力
// | 开源版本可自由商用,可去除界面版权logo
// | gitee下载:https://gitee.com/likeshop_gitee/likeadmin
// | github下载:https://github.com/likeshop-github/likeadmin
// | 访问官网:https://www.likeadmin.cn
// | likeadmin团队 版权所有 拥有最终解释权
// +----------------------------------------------------------------------
// | author: likeadminTeam
// +----------------------------------------------------------------------
namespace app\api\controller;
use app\adminapi\logic\tcm\DiagnosisLogic;
use app\adminapi\logic\tcm\TrackingNoteLogic;
use app\api\logic\tcm\DailyGamifyLogic;
use app\api\logic\tcm\DailyFamilyLikeLogic;
use app\api\logic\tcm\DailyShareLogic;
use app\api\logic\tcm\DailyPhoneLogic;
use app\api\logic\tcm\DailyDietAiLogic;
use app\adminapi\logic\ConfigLogic;
use app\common\model\tcm\Diagnosis;
use app\common\model\tcm\BloodRecord;
use app\common\model\tcm\DietRecord;
use app\common\model\tcm\ExerciseRecord;
/**
* 中医诊断控制器(供小程序调用)
* Class TcmController
* @package app\api\controller
*/
class TcmController extends BaseApiController
{
/**
* @notes 不需要登录的方法
* @var array
*/
public array $notNeedLogin = ['getPatientSignature', 'diagnosisDetail', 'getDict', 'confirmDiagnosis', 'getCardList', 'getOrderByNo', 'patientHangupVideo', 'dailySharePreview', 'dailyFamilyLike'];
/**
* @notes 获取患者签名(供小程序调用)
* @return \think\response\Json
*/
public function getPatientSignature()
{
$patientId = $this->request->get('patient_id');
if (!$patientId) {
return $this->fail('患者ID不能为空');
}
// 调用逻辑层
$result = DiagnosisLogic::getPatientSignature($patientId);
if ($result === false) {
return $this->fail(DiagnosisLogic::getError());
}
return $this->data($result);
}
/**
* @notes 患者挂断视频:后端 DeleteCloudRecording + 结束通话记录(与诊单 patient_id 校验)
* @return \think\response\Json
*/
public function patientHangupVideo()
{
$params = [
'diagnosis_id' => (int)$this->request->post('diagnosis_id', 0),
'patient_id' => (int)$this->request->post('patient_id', 0),
'room_id' => trim((string)$this->request->post('room_id', '')),
];
if ($params['patient_id'] <= 0) {
return $this->fail('患者ID不能为空');
}
if ($params['diagnosis_id'] <= 0 && $params['room_id'] === '') {
return $this->fail('诊单ID或房间号须至少传一项');
}
$result = DiagnosisLogic::patientHangupVideoCall($params);
if ($result === false) {
return $this->fail(DiagnosisLogic::getError());
}
return $this->success('已结束通话');
}
/**
* @notes 获取诊单详情(供小程序调用)
* @return \think\response\Json
*/
public function diagnosisDetail()
{
$params = [
'id' => $this->request->get('id'),
'user_id' => $this->userId,
'share_user_id' => $this->request->get('share_user_id', 0)
];
if (!$params['id']) {
return $this->fail('诊单ID不能为空');
}
try {
$result = DiagnosisLogic::diagnosisDetailWithRecord($params);
if ($result === false) {
return $this->fail(DiagnosisLogic::getError());
}
return $this->data($result);
} catch (\Exception $e) {
return $this->fail($e->getMessage());
}
}
/**
* @notes 确认诊单(供小程序调用)
* @return \think\response\Json
*/
public function confirmDiagnosis()
{
$params = [
'id' => $this->request->post('id'),
'user_id' => $this->userId,
'share_user_id' => $this->request->post('share_user', 0)
];
if (!$params['id']) {
return $this->fail('诊单ID不能为空');
}
try {
$result = DiagnosisLogic::confirmDiagnosisRecord($params);
if ($result === false) {
return $this->fail(DiagnosisLogic::getError());
}
return $this->success('确认成功');
} catch (\Exception $e) {
return $this->fail($e->getMessage());
}
}
/**
* @notes 获取字典数据(供小程序调用)
* @return \think\response\Json
*/
public function getDict()
{
$type = $this->request->get('type', '');
if (!$type) {
return $this->fail('字典类型不能为空');
}
try {
$data = ConfigLogic::getDictByType($type);
return $this->data($data);
} catch (\Exception $e) {
return $this->fail($e->getMessage());
}
}
/**
* @notes 获取就诊卡列表(供小程序调用)
* @return \think\response\Json
*/
public function getCardList()
{
$user_id =$this->userId;
if (!$user_id) {
return $this->fail('患者ID不能为空');
}
try {
$result = DiagnosisLogic::getCardList($user_id);
if ($result === false) {
return $this->fail(DiagnosisLogic::getError());
}
return $this->data($result);
} catch (\Exception $e) {
return $this->fail($e->getMessage());
}
}
/**
* @notes 新建就诊卡(供小程序调用)
* @return \think\response\Json
*/
public function addCard()
{
$params = [
'user_id' => $this->userId,
'patient_id' => $this->request->post('patient_id'),
'patient_name' => $this->request->post('patient_name'),
'gender' => $this->request->post('gender'),
'age' => $this->request->post('age'),
'id_card' => $this->request->post('id_card'),
'height' => $this->request->post('height'),
'weight' => $this->request->post('weight'),
'systolic_pressure' => $this->request->post('systolic_pressure'),
'diastolic_pressure' => $this->request->post('diastolic_pressure'),
'fasting_blood_sugar' => $this->request->post('fasting_blood_sugar'),
'diabetes_discovery_year' => $this->request->post('diabetes_discovery_year'),
'local_hospital_diagnosis' => $this->request->post('local_hospital_diagnosis'),
'local_hospital_name' => $this->request->post('local_hospital_name'),
'local_hospital_visit_date' => $this->request->post('local_hospital_visit_date'),
'diagnosis_date' => $this->request->post('diagnosis_date'),
'diagnosis_type' => $this->request->post('diagnosis_type'),
'syndrome_type' => $this->request->post('syndrome_type'),
'diabetes_type' => $this->request->post('diabetes_type'),
'appetite' => $this->request->post('appetite'),
'water_intake' => $this->request->post('water_intake'),
'diet_condition' => $this->request->post('diet_condition'),
'weight_change' => $this->request->post('weight_change'),
'body_feeling' => $this->request->post('body_feeling'),
'sleep_condition' => $this->request->post('sleep_condition'),
'eye_condition' => $this->request->post('eye_condition'),
'head_feeling' => $this->request->post('head_feeling'),
'sweat_condition' => $this->request->post('sweat_condition'),
'skin_condition' => $this->request->post('skin_condition'),
'urine_condition' => $this->request->post('urine_condition'),
'stool_condition' => $this->request->post('stool_condition'),
'kidney_condition' => $this->request->post('kidney_condition'),
'fatty_liver_degree' => $this->request->post('fatty_liver_degree'),
'past_history' => $this->request->post('past_history'),
'trauma_history' => $this->request->post('trauma_history'),
'surgery_history' => $this->request->post('surgery_history'),
'allergy_history' => $this->request->post('allergy_history'),
'family_history' => $this->request->post('family_history'),
'pregnancy_history' => $this->request->post('pregnancy_history'),
'symptoms' => $this->request->post('symptoms'),
'tongue_coating' => $this->request->post('tongue_coating'),
'tongue_images' => $this->request->post('tongue_images'),
'report_files' => $this->request->post('report_files'),
'pulse' => $this->request->post('pulse'),
'treatment_principle' => $this->request->post('treatment_principle'),
'prescription' => $this->request->post('prescription'),
'doctor_advice' => $this->request->post('doctor_advice'),
'remark' => $this->request->post('remark'),
'create_source' => DailyPhoneLogic::CREATE_SOURCE_MNP,
];
// patient_id 可选:不传则创建时自动生成(新患者首张就诊卡)
try {
$result = DiagnosisLogic::addCard($params);
if ($result === false) {
return $this->fail(DiagnosisLogic::getError());
}
return $this->success('创建成功', $result);
} catch (\Exception $e) {
return $this->fail($e->getMessage());
}
}
/**
* @notes 更新就诊卡(供小程序调用)
* @return \think\response\Json
*/
public function updateCard()
{
$params = [
'id' => $this->request->post('id'),
'patient_id' => $this->request->post('patient_id'),
'patient_name' => $this->request->post('patient_name'),
'gender' => $this->request->post('gender'),
'age' => $this->request->post('age'),
'id_card' => $this->request->post('id_card'),
// 生命体征
'height' => $this->request->post('height'),
'weight' => $this->request->post('weight'),
'systolic_pressure' => $this->request->post('systolic_pressure'),
'diastolic_pressure' => $this->request->post('diastolic_pressure'),
'fasting_blood_sugar' => $this->request->post('fasting_blood_sugar'),
// 主诉
'diabetes_discovery_year' => $this->request->post('diabetes_discovery_year'),
'local_hospital_diagnosis' => $this->request->post('local_hospital_diagnosis'),
'local_hospital_name' => $this->request->post('local_hospital_name'),
'local_hospital_visit_date' => $this->request->post('local_hospital_visit_date'),
// 诊断信息
'diagnosis_date' => $this->request->post('diagnosis_date'),
'diagnosis_type' => $this->request->post('diagnosis_type'),
'syndrome_type' => $this->request->post('syndrome_type'),
'diabetes_type' => $this->request->post('diabetes_type'),
// 现病史字段
'appetite' => $this->request->post('appetite'),
'water_intake' => $this->request->post('water_intake'),
'diet_condition' => $this->request->post('diet_condition'),
'weight_change' => $this->request->post('weight_change'),
'body_feeling' => $this->request->post('body_feeling'),
'sleep_condition' => $this->request->post('sleep_condition'),
'eye_condition' => $this->request->post('eye_condition'),
'head_feeling' => $this->request->post('head_feeling'),
'sweat_condition' => $this->request->post('sweat_condition'),
'skin_condition' => $this->request->post('skin_condition'),
'urine_condition' => $this->request->post('urine_condition'),
'stool_condition' => $this->request->post('stool_condition'),
'kidney_condition' => $this->request->post('kidney_condition'),
'fatty_liver_degree' => $this->request->post('fatty_liver_degree'),
// 既往史字段
'past_history' => $this->request->post('past_history'),
'trauma_history' => $this->request->post('trauma_history'),
'surgery_history' => $this->request->post('surgery_history'),
'allergy_history' => $this->request->post('allergy_history'),
'family_history' => $this->request->post('family_history'),
'pregnancy_history' => $this->request->post('pregnancy_history'),
// 临床信息
'symptoms' => $this->request->post('symptoms'),
'tongue_coating' => $this->request->post('tongue_coating'),
'tongue_images' => $this->request->post('tongue_images'),
'report_files' => $this->request->post('report_files'),
'pulse' => $this->request->post('pulse'),
'treatment_principle' => $this->request->post('treatment_principle'),
'prescription' => $this->request->post('prescription'),
'doctor_advice' => $this->request->post('doctor_advice'),
'remark' => $this->request->post('remark')
];
if (!$params['id']) {
return $this->fail('诊单ID不能为空');
}
if (!$params['patient_id']) {
return $this->fail('患者ID不能为空');
}
try {
$result = DiagnosisLogic::updateCard($params);
if ($result === false) {
return $this->fail(DiagnosisLogic::getError());
}
return $this->success('更新成功');
} catch (\Exception $e) {
return $this->fail($e->getMessage());
}
}
/**
* @notes 日常记录窗口数据(血糖血压 / 饮食 / 运动)—— 患者端
*
* 路由:GET /api/tcm/dailyTrackingWindow?diagnosis_id=:id&patient_id=:pid&start_date=&end_date=
* 说明:供小程序「日常记录」页面拉取一段时间内的跟踪数据,复用后台 fetchTrackingWindow。
*
* @return \think\response\Json
*/
public function dailyTrackingWindow()
{
$diagnosisId = (int) $this->request->get('diagnosis_id', 0);
$patientId = (int) $this->request->get('patient_id', 0);
$startDate = (string) $this->request->get('start_date', '');
$endDate = (string) $this->request->get('end_date', '');
$check = $this->ensurePatientOwnsDiagnosis($diagnosisId);
if (!$check['ok']) {
return $this->fail($check['error']);
}
$diagnosis = $check['diagnosis'];
try {
if ($patientId > 0 && (int) $diagnosis['patient_id'] !== $patientId) {
return $this->fail('无权查看该诊单的日常记录');
}
$data = DiagnosisLogic::fetchTrackingWindow($diagnosisId, $startDate, $endDate);
// 把诊断的基础信息一并返回,便于小程序展示头部摘要
$data['diagnosis'] = [
'id' => (int) $diagnosis['id'],
'patient_id' => (int) $diagnosis['patient_id'],
'patient_name' => (string) ($diagnosis['patient_name'] ?? ''),
'age' => (int) ($diagnosis['age'] ?? 0),
'gender' => (int) ($diagnosis['gender'] ?? 0),
];
return $this->data($data);
} catch (\Exception $e) {
return $this->fail($e->getMessage());
}
}
/**
* @notes 日常记录-跟踪备注列表(患者端)
*
* 路由:GET /api/tcm/dailyTrackingNotes?diagnosis_id=:id&patient_id=:pid
*
* @return \think\response\Json
*/
public function dailyTrackingNotes()
{
$diagnosisId = (int) $this->request->get('diagnosis_id', 0);
$patientId = (int) $this->request->get('patient_id', 0);
$check = $this->ensurePatientOwnsDiagnosis($diagnosisId);
if (!$check['ok']) {
return $this->fail($check['error']);
}
$diagnosis = $check['diagnosis'];
try {
if ($patientId > 0 && (int) $diagnosis['patient_id'] !== $patientId) {
return $this->fail('无权查看该诊单的跟踪备注');
}
$notes = TrackingNoteLogic::getByDiagnosis($diagnosisId);
return $this->data($notes);
} catch (\Exception $e) {
return $this->fail($e->getMessage());
}
}
/**
* 解析小程序 form-urlencoded 提交的 JSON 字段
*
* @param mixed $raw
* @param string $arrayKey ThinkPHP 数组字段名,如 badges/a
* @return array
*/
private function decodeJsonPostField($raw, string $arrayKey): array
{
if (is_string($raw) && $raw !== '') {
$decoded = json_decode($raw, true);
if (is_array($decoded)) {
return $decoded;
}
}
if (is_array($raw)) {
return $raw;
}
$arr = $this->request->post($arrayKey, []);
return is_array($arr) ? $arr : [];
}
/**
* @notes 校验当前登录患者是否拥有指定诊单(通过 diagnosis_view_records 关联)
* @param int $diagnosisId
* @return array{ok:bool,diagnosis?:\app\common\model\tcm\Diagnosis,error?:string}
*/
private function ensurePatientOwnsDiagnosis(int $diagnosisId): array
{
if (!$this->userId) {
return ['ok' => false, 'error' => '请先登录'];
}
if ($diagnosisId <= 0) {
return ['ok' => false, 'error' => '诊单ID不能为空'];
}
$diagnosis = Diagnosis::where('id', $diagnosisId)->find();
if (!$diagnosis) {
return ['ok' => false, 'error' => '诊单不存在'];
}
// 通过 diagnosis_view_records 校验当前小程序用户与该诊单的归属关系
// 该表是「就诊卡列表」的来源(参考 DiagnosisLogic::getCardList
$owned = \think\facade\Db::name('diagnosis_view_records')
->where('user_id', $this->userId)
->where('diagnosis_id', $diagnosisId)
->where('delete_time', null)
->find();
if (!$owned) {
return ['ok' => false, 'error' => '无权操作该诊单'];
}
return ['ok' => true, 'diagnosis' => $diagnosis];
}
/**
* @notes 日常记录-手机号快捷建档(无就诊卡时,已绑定手机号即可录入血糖)
*
* 路由:POST /api/tcm/dailyEnsurePhoneContext
*
* @return \think\response\Json
*/
public function dailyEnsurePhoneContext()
{
$sex = $this->request->post('sex', null);
$options = [];
if ($sex !== null && $sex !== '') {
$options['sex'] = (int) $sex;
}
$result = DailyPhoneLogic::ensureDailyContext((int) $this->userId, $options);
if (empty($result['ok'])) {
$data = [];
if (!empty($result['need_mobile'])) {
$data['need_mobile'] = 1;
}
return $this->fail($result['error'] ?? '无法开始录入', $data);
}
unset($result['ok'], $result['error'], $result['need_mobile']);
return $this->data($result);
}
/**
* @notes 日常记录-获取今日已录入的血糖血压记录(患者端)
*
* 路由:GET /api/tcm/dailyTodayBloodRecord?diagnosis_id=:id
*
* 用于「日常记录」页打开「录入今日」表单时预填数据;只返回 source=1 的当天记录。
*
* @return \think\response\Json
*/
public function dailyTodayBloodRecord()
{
$diagnosisId = (int) $this->request->get('diagnosis_id', 0);
$check = $this->ensurePatientOwnsDiagnosis($diagnosisId);
if (!$check['ok']) {
return $this->fail($check['error']);
}
try {
$todayStart = strtotime(date('Y-m-d 00:00:00'));
$todayEnd = strtotime(date('Y-m-d 23:59:59'));
$record = BloodRecord::where('diagnosis_id', $diagnosisId)
->where('source', 1)
->where('record_date', '>=', $todayStart)
->where('record_date', '<=', $todayEnd)
->order('id', 'desc')
->find();
if (!$record) {
return $this->data(['record' => null, 'today' => date('Y-m-d')]);
}
$data = $record->toArray();
$data['record_date'] = date('Y-m-d', (int) $data['record_date']);
return $this->data(['record' => $data, 'today' => date('Y-m-d')]);
} catch (\Exception $e) {
return $this->fail($e->getMessage());
}
}
/**
* @notes 日常记录-保存今日的血糖血压记录(患者端)
*
* 路由:POST /api/tcm/dailySaveBloodRecord
* 必填:diagnosis_id
* 可选数值:fasting_blood_sugar, postprandial_blood_sugar, other_blood_sugar,
* systolic_pressure, diastolic_pressure
* 可选字符串:remark
*
* 语义:同一天同一诊单只有一条 source=1 的记录;存在则 update,不存在则 create。
* 日期强制为「今天」;任何 record_date 入参被忽略,避免补录历史。
*
* @return \think\response\Json
*/
public function dailySaveBloodRecord()
{
$diagnosisId = (int) $this->request->post('diagnosis_id', 0);
$check = $this->ensurePatientOwnsDiagnosis($diagnosisId);
if (!$check['ok']) {
return $this->fail($check['error']);
}
$diagnosis = $check['diagnosis'];
// 解析并校验数值字段;空字符串当 null 处理
$parseDecimal = function ($v) {
if ($v === null || $v === '' || $v === '0' || $v === 0) return null;
if (!is_numeric($v)) return null;
$f = (float) $v;
return $f > 0 ? $f : null;
};
$parseInt = function ($v) {
if ($v === null || $v === '') return null;
if (!is_numeric($v)) return null;
$i = (int) $v;
return $i > 0 ? $i : null;
};
$fasting = $parseDecimal($this->request->post('fasting_blood_sugar'));
$postprandial = $parseDecimal($this->request->post('postprandial_blood_sugar'));
$other = $parseDecimal($this->request->post('other_blood_sugar'));
$systolic = $parseInt($this->request->post('systolic_pressure'));
$diastolic = $parseInt($this->request->post('diastolic_pressure'));
$remark = trim((string) $this->request->post('remark', ''));
// 至少要填一个有效字段
if ($fasting === null && $postprandial === null && $other === null &&
$systolic === null && $diastolic === null && $remark === '') {
return $this->fail('请至少填写一项血糖或血压数据');
}
// 边界校验,避免误录天文数字
if ($fasting !== null && ($fasting < 0.1 || $fasting > 50)) return $this->fail('空腹血糖数值异常');
if ($postprandial !== null && ($postprandial < 0.1 || $postprandial > 50)) return $this->fail('餐后血糖数值异常');
if ($other !== null && ($other < 0.1 || $other > 50)) return $this->fail('其他血糖数值异常');
if ($systolic !== null && ($systolic < 40 || $systolic > 300)) return $this->fail('收缩压数值异常');
if ($diastolic !== null && ($diastolic < 20 || $diastolic > 200)) return $this->fail('舒张压数值异常');
try {
$todayStart = strtotime(date('Y-m-d 00:00:00'));
$todayEnd = strtotime(date('Y-m-d 23:59:59'));
$existing = BloodRecord::where('diagnosis_id', $diagnosisId)
->where('source', 1)
->where('record_date', '>=', $todayStart)
->where('record_date', '<=', $todayEnd)
->order('id', 'desc')
->find();
$payload = [
'diagnosis_id' => $diagnosisId,
'patient_id' => (int) $diagnosis['patient_id'],
'record_date' => $todayStart,
'record_time' => date('H:i'),
'fasting_blood_sugar' => $fasting,
'postprandial_blood_sugar' => $postprandial,
'other_blood_sugar' => $other,
'systolic_pressure' => $systolic,
'diastolic_pressure' => $diastolic,
'remark' => $remark,
'source' => 1,
];
if ($existing) {
// 分表单项更新时保留未提交字段,避免血糖/血压互相覆盖
$keepDecimal = function ($new, string $oldKey) use ($parseDecimal, $existing) {
if ($new !== null) {
return $new;
}
return $parseDecimal($existing[$oldKey] ?? null);
};
$keepInt = function ($new, string $oldKey) use ($parseInt, $existing) {
if ($new !== null) {
return $new;
}
return $parseInt($existing[$oldKey] ?? null);
};
$fasting = $keepDecimal($fasting, 'fasting_blood_sugar');
$postprandial = $keepDecimal($postprandial, 'postprandial_blood_sugar');
$other = $keepDecimal($other, 'other_blood_sugar');
$systolic = $keepInt($systolic, 'systolic_pressure');
$diastolic = $keepInt($diastolic, 'diastolic_pressure');
if ($remark === '') {
$remark = trim((string) ($existing['remark'] ?? ''));
}
$payload = [
'diagnosis_id' => $diagnosisId,
'patient_id' => (int) $diagnosis['patient_id'],
'record_date' => $todayStart,
'record_time' => date('H:i'),
'fasting_blood_sugar' => $fasting,
'postprandial_blood_sugar' => $postprandial,
'other_blood_sugar' => $other,
'systolic_pressure' => $systolic,
'diastolic_pressure' => $diastolic,
'remark' => $remark,
'source' => 1,
'id' => $existing['id'],
];
BloodRecord::update($payload);
return $this->success('已更新今日记录', ['id' => $existing['id']]);
}
$created = BloodRecord::create($payload);
return $this->success('录入成功', ['id' => $created->id]);
} catch (\Exception $e) {
return $this->fail($e->getMessage());
}
}
/**
* @notes 日常记录-删除今日的血糖血压记录(患者端)
*
* 路由:POST /api/tcm/dailyDeleteBloodRecord
* 必填:diagnosis_id, id
*
* 只允许删除当前用户拥有的诊单 + 当天 + source=1 的记录;其他情况一律拒绝。
*
* @return \think\response\Json
*/
public function dailyDeleteBloodRecord()
{
$diagnosisId = (int) $this->request->post('diagnosis_id', 0);
$recordId = (int) $this->request->post('id', 0);
$check = $this->ensurePatientOwnsDiagnosis($diagnosisId);
if (!$check['ok']) {
return $this->fail($check['error']);
}
if ($recordId <= 0) {
return $this->fail('记录ID不能为空');
}
try {
$todayStart = strtotime(date('Y-m-d 00:00:00'));
$todayEnd = strtotime(date('Y-m-d 23:59:59'));
$record = BloodRecord::where('id', $recordId)
->where('diagnosis_id', $diagnosisId)
->where('source', 1)
->find();
if (!$record) {
return $this->fail('记录不存在或无权删除');
}
$ts = (int) $record['record_date'];
if ($ts < $todayStart || $ts > $todayEnd) {
return $this->fail('只能删除当天的记录');
}
BloodRecord::destroy($recordId);
return $this->success('已删除');
} catch (\Exception $e) {
return $this->fail($e->getMessage());
}
}
/**
* @notes 获取稳糖乐园状态(积分、今日任务、可领取积分)
*
* 路由:GET /api/tcm/dailyGetGamify?diagnosis_id=:id
*
* @return \think\response\Json
*/
public function dailyGetGamify()
{
$diagnosisId = (int) $this->request->get('diagnosis_id', 0);
$result = DailyGamifyLogic::getState((int) $this->userId, $diagnosisId);
if ($result === false) {
return $this->fail(DailyGamifyLogic::getError());
}
return $this->data($result);
}
/**
* @notes 给小树浇水(领取今日已完成任务的积分,服务端校验)
*
* 路由:POST /api/tcm/dailyWaterTree
* 必填:diagnosis_id
*
* @return \think\response\Json
*/
public function dailyWaterTree()
{
$diagnosisId = (int) $this->request->post('diagnosis_id', 0);
$result = DailyGamifyLogic::waterTree((int) $this->userId, $diagnosisId);
if ($result === false) {
return $this->fail(DailyGamifyLogic::getError());
}
// 统一走 success,保证 msg 与 data 同时返回,前端可提示且刷新状态
return $this->success($result['message'] ?? '操作成功', $result, 1, 1);
}
/**
* @notes 日常记录-获取今日饮食记录
*
* 路由:GET /api/tcm/dailyTodayDietRecord?diagnosis_id=:id
*/
public function dailyTodayDietRecord()
{
$diagnosisId = (int) $this->request->get('diagnosis_id', 0);
$check = $this->ensurePatientOwnsDiagnosis($diagnosisId);
if (!$check['ok']) {
return $this->fail($check['error']);
}
try {
$todayStart = strtotime(date('Y-m-d 00:00:00'));
$todayEnd = strtotime(date('Y-m-d 23:59:59'));
$record = DietRecord::where('diagnosis_id', $diagnosisId)
->where('record_date', '>=', $todayStart)
->where('record_date', '<=', $todayEnd)
->order('id', 'desc')
->find();
if (!$record) {
return $this->data(['record' => null, 'today' => date('Y-m-d')]);
}
$data = $record->toArray();
$data['record_date'] = date('Y-m-d', (int) $data['record_date']);
return $this->data(['record' => $data, 'today' => date('Y-m-d')]);
} catch (\Exception $e) {
return $this->fail($e->getMessage());
}
}
/**
* @notes 日常记录-保存今日饮食
*
* 路由:POST /api/tcm/dailySaveDietRecord
*/
public function dailySaveDietRecord()
{
$diagnosisId = (int) $this->request->post('diagnosis_id', 0);
$check = $this->ensurePatientOwnsDiagnosis($diagnosisId);
if (!$check['ok']) {
return $this->fail($check['error']);
}
$diagnosis = $check['diagnosis'];
$breakfast = trim((string) $this->request->post('breakfast_foods', ''));
$lunch = trim((string) $this->request->post('lunch_foods', ''));
$dinner = trim((string) $this->request->post('dinner_foods', ''));
$note = trim((string) $this->request->post('note', ''));
if ($breakfast === '' && $lunch === '' && $dinner === '') {
return $this->fail('请至少填写一餐饮食');
}
try {
$todayStart = strtotime(date('Y-m-d 00:00:00'));
$todayEnd = strtotime(date('Y-m-d 23:59:59'));
$existing = DietRecord::where('diagnosis_id', $diagnosisId)
->where('record_date', '>=', $todayStart)
->where('record_date', '<=', $todayEnd)
->order('id', 'desc')
->find();
$payload = [
'diagnosis_id' => $diagnosisId,
'patient_id' => (int) $diagnosis['patient_id'],
'record_date' => $todayStart,
'breakfast_foods' => $breakfast,
'lunch_foods' => $lunch,
'dinner_foods' => $dinner,
'note' => $note,
];
if ($existing) {
$payload['id'] = $existing['id'];
DietRecord::update($payload);
return $this->success('已更新今日饮食', ['id' => $existing['id']]);
}
$created = DietRecord::create($payload);
return $this->success('饮食记录已保存', ['id' => $created->id]);
} catch (\Exception $e) {
return $this->fail($e->getMessage());
}
}
/**
* @notes 日常记录-AI 今日饮食推荐(霍大夫升糖指数)
*
* 路由:GET /api/tcm/dailyDietAiRecommend?diagnosis_id=:id&refresh=0|1
*/
public function dailyDietAiRecommend()
{
$diagnosisId = (int) $this->request->get('diagnosis_id', 0);
$check = $this->ensurePatientOwnsDiagnosis($diagnosisId);
if (!$check['ok']) {
return $this->fail($check['error']);
}
$refresh = (int) $this->request->get('refresh', 0) === 1;
$result = DailyDietAiLogic::getDailyRecommend($diagnosisId, $refresh);
if (empty($result['ok'])) {
return $this->fail($result['error'] ?? '获取推荐失败');
}
return $this->data($result['data']);
}
/**
* @notes 日常记录-AI 饮食问答(能不能吃某食物)
*
* 路由:POST /api/tcm/dailyDietAiAsk body: diagnosis_id, question
*/
public function dailyDietAiAsk()
{
$diagnosisId = (int) $this->request->post('diagnosis_id', 0);
$check = $this->ensurePatientOwnsDiagnosis($diagnosisId);
if (!$check['ok']) {
return $this->fail($check['error']);
}
$question = trim((string) $this->request->post('question', ''));
$result = DailyDietAiLogic::askFood($diagnosisId, $question);
if (empty($result['ok'])) {
return $this->fail($result['error'] ?? '咨询失败');
}
return $this->data($result['data']);
}
/**
* @notes 日常记录-AI 今日饮食推荐(SSE 流式)
*
* 路由:GET /api/tcm/dailyDietAiRecommendStream?diagnosis_id=:id&refresh=0|1
*/
public function dailyDietAiRecommendStream()
{
$diagnosisId = (int) $this->request->get('diagnosis_id', 0);
$check = $this->ensurePatientOwnsDiagnosis($diagnosisId);
if (!$check['ok']) {
return $this->fail($check['error']);
}
$refresh = (int) $this->request->get('refresh', 0) === 1;
$this->runDailyDietSse(static function (callable $emit) use ($diagnosisId, $refresh): void {
DailyDietAiLogic::streamDailyRecommend($diagnosisId, $refresh, $emit);
});
}
/**
* @notes 日常记录-AI 饮食问答(SSE 流式)
*
* 路由:POST /api/tcm/dailyDietAiAskStream body: diagnosis_id, question
*/
public function dailyDietAiAskStream()
{
$diagnosisId = (int) $this->request->post('diagnosis_id', 0);
$check = $this->ensurePatientOwnsDiagnosis($diagnosisId);
if (!$check['ok']) {
return $this->fail($check['error']);
}
$question = trim((string) $this->request->post('question', ''));
$this->runDailyDietSse(static function (callable $emit) use ($diagnosisId, $question): void {
DailyDietAiLogic::streamAskFood($diagnosisId, $question, $emit);
});
}
/**
* @param callable(callable(string, array<string, mixed>):void):void $runner
*/
private function runDailyDietSse(callable $runner): void
{
while (ob_get_level() > 0) {
ob_end_clean();
}
@ini_set('output_buffering', 'off');
@ini_set('zlib.output_compression', '0');
if (function_exists('apache_setenv')) {
@apache_setenv('no-gzip', '1');
}
header('Content-Type: text/event-stream; charset=utf-8');
header('Cache-Control: no-cache, no-transform');
header('Connection: keep-alive');
header('X-Accel-Buffering: no');
header('Content-Encoding: none');
// 撑开 Nginx/FastCGI 缓冲,让后续 chunk 能立即下发
echo ':' . str_repeat(' ', 2048) . "\n\n";
if (function_exists('ob_flush')) {
@ob_flush();
}
flush();
$emit = static function (string $event, array $payload): void {
echo 'event: ' . $event . "\n";
echo 'data: ' . json_encode($payload, JSON_UNESCAPED_UNICODE) . "\n\n";
if (function_exists('ob_flush')) {
@ob_flush();
}
flush();
};
$emit('start', ['message' => '已连接,正在生成…']);
try {
$runner($emit);
} catch (\Throwable $e) {
$emit('error', ['message' => '服务异常,请稍后再试']);
}
exit;
}
/**
* @notes 日常记录-获取今日运动记录
*
* 路由:GET /api/tcm/dailyTodayExerciseRecord?diagnosis_id=:id
*/
public function dailyTodayExerciseRecord()
{
$diagnosisId = (int) $this->request->get('diagnosis_id', 0);
$check = $this->ensurePatientOwnsDiagnosis($diagnosisId);
if (!$check['ok']) {
return $this->fail($check['error']);
}
try {
$todayStart = strtotime(date('Y-m-d 00:00:00'));
$todayEnd = strtotime(date('Y-m-d 23:59:59'));
$record = ExerciseRecord::where('diagnosis_id', $diagnosisId)
->where('record_date', '>=', $todayStart)
->where('record_date', '<=', $todayEnd)
->order('id', 'desc')
->find();
if (!$record) {
return $this->data(['record' => null, 'today' => date('Y-m-d')]);
}
$data = $record->toArray();
$data['record_date'] = date('Y-m-d', (int) $data['record_date']);
return $this->data(['record' => $data, 'today' => date('Y-m-d')]);
} catch (\Exception $e) {
return $this->fail($e->getMessage());
}
}
/**
* @notes 日常记录-保存今日运动
*
* 路由:POST /api/tcm/dailySaveExerciseRecord
*/
public function dailySaveExerciseRecord()
{
$diagnosisId = (int) $this->request->post('diagnosis_id', 0);
$check = $this->ensurePatientOwnsDiagnosis($diagnosisId);
if (!$check['ok']) {
return $this->fail($check['error']);
}
$diagnosis = $check['diagnosis'];
$exerciseType = trim((string) $this->request->post('exercise_type', ''));
$durationRaw = $this->request->post('duration', '');
$intensity = (int) $this->request->post('intensity', 2);
$note = trim((string) $this->request->post('note', ''));
$duration = null;
if ($durationRaw !== '' && $durationRaw !== null && is_numeric($durationRaw)) {
$d = (int) $durationRaw;
if ($d > 0) {
$duration = $d;
}
}
if ($exerciseType === '' && $duration === null) {
return $this->fail('请填写运动类型或时长');
}
if ($intensity < 1 || $intensity > 3) {
$intensity = 2;
}
try {
$todayStart = strtotime(date('Y-m-d 00:00:00'));
$todayEnd = strtotime(date('Y-m-d 23:59:59'));
$existing = ExerciseRecord::where('diagnosis_id', $diagnosisId)
->where('record_date', '>=', $todayStart)
->where('record_date', '<=', $todayEnd)
->order('id', 'desc')
->find();
$payload = [
'diagnosis_id' => $diagnosisId,
'patient_id' => (int) $diagnosis['patient_id'],
'record_date' => $todayStart,
'exercise_type' => $exerciseType,
'duration' => $duration,
'intensity' => $intensity,
'note' => $note,
];
if ($existing) {
$payload['id'] = $existing['id'];
ExerciseRecord::update($payload);
return $this->success('已更新今日运动', ['id' => $existing['id']]);
}
$created = ExerciseRecord::create($payload);
return $this->success('运动记录已保存', ['id' => $created->id]);
} catch (\Exception $e) {
return $this->fail($e->getMessage());
}
}
/**
* @notes 保存稳糖分 / 勋章 / 任务领奖状态
*
* 路由:POST /api/tcm/dailySaveGamify
* 参数:diagnosis_id, points, badges(数组), task_awards(对象)
*
* @return \think\response\Json
*/
public function dailySaveGamify()
{
$diagnosisId = (int) $this->request->post('diagnosis_id', 0);
$points = (int) $this->request->post('points', 0);
$badges = $this->decodeJsonPostField($this->request->post('badges', ''), 'badges/a');
$taskAwards = $this->decodeJsonPostField($this->request->post('task_awards', ''), 'task_awards/a');
$result = DailyGamifyLogic::saveState((int) $this->userId, $diagnosisId, $points, $badges, $taskAwards);
if ($result === false) {
return $this->fail(DailyGamifyLogic::getError());
}
return $this->data($result);
}
/**
* @notes 生成日常记录分享邀请码(仅当天有效)
*
* 路由:POST /api/tcm/dailyCreateShareInvite
* 必填:diagnosis_id
*
* @return \think\response\Json
*/
public function dailyCreateShareInvite()
{
$diagnosisId = (int) $this->request->post('diagnosis_id', 0);
$result = DailyShareLogic::createInvite((int) $this->userId, $diagnosisId);
if ($result === false) {
return $this->fail(DailyShareLogic::getError());
}
return $this->data($result);
}
/**
* @notes 凭邀请码查看分享战报(无需登录,含近7日血糖/血压记录)
*
* 路由:GET /api/tcm/dailySharePreview?invite_code=XXXXXXXX
*
* @return \think\response\Json
*/
public function dailySharePreview()
{
$inviteCode = (string) $this->request->get('invite_code', '');
$viewerKey = (string) $this->request->get('viewer_key', '');
$result = DailyShareLogic::previewByInviteCode($inviteCode, $viewerKey);
if ($result === false) {
return $this->fail(DailyShareLogic::getError());
}
return $this->data($result);
}
/**
* @notes 家人点赞(邀请观看页,无需登录)
*
* 路由:POST /api/tcm/dailyFamilyLike
* 必填:invite_code, viewer_key
* 可选:nickname(家人/亲友/老伴 等)
*
* @return \think\response\Json
*/
public function dailyFamilyLike()
{
$inviteCode = (string) $this->request->post('invite_code', '');
$viewerKey = (string) $this->request->post('viewer_key', '');
$nickname = (string) $this->request->post('nickname', '');
$result = DailyFamilyLikeLogic::addLike($inviteCode, $viewerKey, $nickname);
if ($result === false) {
return $this->fail(DailyFamilyLikeLogic::getError());
}
return $this->data($result);
}
/**
* @notes 患者查看今日家人点赞汇总
*
* 路由:GET /api/tcm/dailyFamilyLikeSummary?diagnosis_id=
*
* @return \think\response\Json
*/
public function dailyFamilyLikeSummary()
{
$diagnosisId = (int) $this->request->get('diagnosis_id', 0);
$result = DailyFamilyLikeLogic::summaryForPatient((int) $this->userId, $diagnosisId);
if ($result === false) {
return $this->fail(DailyFamilyLikeLogic::getError());
}
return $this->data($result);
}
/**
* @notes 根据订单号获取订单详情(供小程序支付页调用)
* @return \think\response\Json
*/
public function getOrderByNo()
{
$orderNo = $this->request->get('order_no', '');
if (empty($orderNo)) {
return $this->fail('订单号不能为空');
}
try {
$order = \app\common\model\Order::where('order_no', $orderNo)->find();
if (!$order) {
return $this->fail('订单不存在');
}
$typeMap = [1 => '挂号费', 2 => '问诊费', 3 => '药品费用'];
$statusMap = [1 => '待支付', 2 => '已支付', 3 => '已取消', 4 => '已退款'];
return $this->data([
'id' => $order['id'],
'order_no' => $order['order_no'],
'order_type' => $order['order_type'],
'order_type_desc' => $typeMap[$order['order_type']] ?? '未知',
'amount' => $order['amount'],
'status' => $order['status'],
'status_desc' => $statusMap[$order['status']] ?? '未知',
'remark' => $order['remark'] ?? '',
'create_time' => $order['create_time'],
]);
} catch (\Exception $e) {
return $this->fail($e->getMessage());
}
}
}