diff --git a/TUICallKit-Vue3/main.js b/TUICallKit-Vue3/main.js
index 5fd1d831..f1d13fa0 100644
--- a/TUICallKit-Vue3/main.js
+++ b/TUICallKit-Vue3/main.js
@@ -1,5 +1,5 @@
import App from './App'
-var baseUrl ='https://admin.zhenyangtang.com.cn/';
+var baseUrl ='https://css.zhenyangtang.com.cn/';
// #ifndef VUE3
import Vue from 'vue'
import './uni.promisify.adaptor'
diff --git a/TUICallKit-Vue3/manifest.json b/TUICallKit-Vue3/manifest.json
index 5389584a..2d9c65d6 100644
--- a/TUICallKit-Vue3/manifest.json
+++ b/TUICallKit-Vue3/manifest.json
@@ -57,7 +57,13 @@
"optimization" : {
"subPackages" : true
},
- "usingComponents" : true
+ "usingComponents" : true,
+ "plugins" : {
+ "WechatSI" : {
+ "version" : "0.3.5",
+ "provider" : "wx069ba97219f66d99"
+ }
+ }
},
"mp-alipay" : {
"usingComponents" : true
diff --git a/TUICallKit-Vue3/pages.json b/TUICallKit-Vue3/pages.json
index 8fe89074..f574c992 100644
--- a/TUICallKit-Vue3/pages.json
+++ b/TUICallKit-Vue3/pages.json
@@ -126,6 +126,21 @@
}
}
]
+ },
+ {
+ "root": "tongji",
+ "pages": [
+ {
+ "path": "pages/index",
+ "style": {
+ "navigationBarTitleText": "日常记录",
+ "navigationBarBackgroundColor": "#0ea5a4",
+ "navigationBarTextStyle": "white",
+ "backgroundColor": "#f1f5f9",
+ "enablePullDownRefresh": true
+ }
+ }
+ ]
}
],
"tabBar": {
diff --git a/TUICallKit-Vue3/pages/Card/Card.vue b/TUICallKit-Vue3/pages/Card/Card.vue
index 0b800434..8393e98c 100644
--- a/TUICallKit-Vue3/pages/Card/Card.vue
+++ b/TUICallKit-Vue3/pages/Card/Card.vue
@@ -53,6 +53,18 @@
{{ card.create_time || '-' }}
+
+
+
+
+ 📈
+ 日常记录
+
+
+ 📝
+ 查看 / 编辑
+
+
@@ -138,6 +150,16 @@ const viewCardDetail = (card) => {
})
}
+// 查看日常记录(血糖血压 / 饮食 / 运动 / 跟踪备注 + 波浪图)
+const viewDailyRecord = (card) => {
+ const url = `/tongji/pages/index?diagnosis_id=${card.id}` +
+ `&patient_id=${card.patient_id || ''}` +
+ `&patient_name=${encodeURIComponent(card.patient_name || '')}` +
+ `&age=${card.age || ''}` +
+ `&gender=${card.gender || ''}`
+ uni.navigateTo({ url })
+}
+
// 新建就诊卡(patient_id 创建后自动生成,仅需登录)
const createCard = () => {
const token = uni.getStorageSync('token')
@@ -310,6 +332,54 @@ const formatDate = (timestamp) => {
border-top: 4rpx solid #e8eaed;
}
+.card-actions {
+ display: flex;
+ gap: 20rpx;
+ margin-top: 28rpx;
+ padding-top: 28rpx;
+ border-top: 4rpx solid #e8eaed;
+}
+
+.card-action-btn {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 12rpx;
+ padding: 24rpx 12rpx;
+ background: #f6faff;
+ border-radius: 20rpx;
+ border: 2rpx solid #d6e6fb;
+ min-height: 80rpx;
+ transition: all 0.2s;
+
+ &:active {
+ transform: scale(0.98);
+ opacity: 0.85;
+ }
+
+ &.primary {
+ background: linear-gradient(135deg, #1890ff, #0ea5a4);
+ border-color: transparent;
+
+ .card-action-icon,
+ .card-action-text {
+ color: #ffffff;
+ }
+ }
+}
+
+.card-action-icon {
+ font-size: 36rpx;
+ color: #1890ff;
+}
+
+.card-action-text {
+ font-size: 30rpx;
+ color: #1890ff;
+ font-weight: 500;
+}
+
.time-info {
display: flex;
align-items: center;
diff --git a/TUICallKit-Vue3/tongji/pages/index.vue b/TUICallKit-Vue3/tongji/pages/index.vue
new file mode 100644
index 00000000..a4627447
--- /dev/null
+++ b/TUICallKit-Vue3/tongji/pages/index.vue
@@ -0,0 +1,4220 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ timeGreetingEmoji }}
+ {{ timeGreeting }}
+
+ {{ patientName || '健康档案' }}
+
+
+ {{ ageText }} · {{ genderText }}
+
+
+ {{ todayChecked ? '✓' : '!' }}
+ {{ todayChecked ? '今日已记录' : '今日待记录' }}
+
+
+
+
+
+ +
+ 录入今日
+
+
+ {{ ttsSpeaking ? '⏸' : '🔊' }}
+ {{ ttsSpeaking ? '停止' : '朗读' }}
+
+
+ ↻
+ 刷新
+
+
+
+
+
+
+
+
+
+
+
+
+ 空腹血糖
+
+
+ ▲
+ 偏高
+
+
+
+
+ {{ latestStat.fasting.value }}
+
+ mmol/L
+
+
+
+
+
+
+
+
+ 餐后血糖
+
+
+ ▲
+ 偏高
+
+
+
+
+ {{ latestStat.postprandial.value }}
+
+ mmol/L
+
+
+
+
+
+
+
+
+ 其他血糖
+
+
+ ▲
+ 偏高
+
+
+
+
+ {{ latestStat.other.value }}
+
+ mmol/L
+
+
+
+
+
+
+
+
+
+ {{ streakDays.days === 0 ? '✨' : '🔥' }}
+
+
+
+ {{ streakDays.days }}
+ 天
+ 截至昨天
+
+ {{ streakDays.days === 0 ? '记录身体每一天的变化' : '连续记录,坚持就是胜利' }}
+
+
+
+
+
+ {{ dot.label }}
+
+
+
+ 本周 {{ last7CheckedCount }} / 7
+
+
+
+
+
+
+ 就诊卡
+ {{ cards.length }} 张
+
+
+
+
+ {{ card.patient_name || '未填' }}
+ {{ card.age }}岁
+
+
+
+
+
+
+
+
+ {{ opt.label }}
+
+
+
+
+
+
+ 🔊
+
+ 语音播报
+ 为长辈准备 · 大字号 · 慢语速
+
+
+
+
+ 自动
+
+
+
+
+
+ {{ ttsEnabled ? '🔊' : '🔇' }}
+
+
+
+
+
+
+
+
+ 血糖趋势
+ {{ rangeText }} · mmol/L
+
+
+
+
+ 空腹
+
+
+
+ 餐后
+
+
+
+
+
+ {{ bloodHighDayCount }}
+ 天偏高
+
+
+ {{ bloodNormalDayCount }}
+ 天正常
+
+
+ {{ bloodRecordDayCount }}
+ 天有记录
+
+
+
+
+
+
+
+ 图表已暂停显示
+
+
+ 📈
+ 当前时间范围暂无血糖数据
+
+
+ {{ hoverInfo.date }}
+
+
+ 空腹 {{ hoverInfo.fasting }}
+
+
+
+ 餐后 {{ hoverInfo.postprandial }}
+
+
+
+
+
+
+
+
+
+ 健康日历
+ {{ heatmapMonthLabel }} · 已记录 {{ heatmapTotalRecords }} / {{ heatmapInRangeDays }} 天
+
+
+ {{ heatmapTotalRecords }}
+ 天有记录
+
+
+
+
+
+ 一
+ 二
+ 三
+ 四
+ 五
+ 六
+ 日
+
+
+
+
+
+ 0 ? jumpToDayRecord(cell.date) : null"
+ >
+ {{ cell.day }}
+
+
+ 今天
+
+
+
+
+
+
+
+
+
+
+ 少量
+
+
+
+ 较多
+
+
+
+ 完整
+
+
+
+ 今日
+
+
+
+
+
+
+
+
+
+
+ 日常记录
+ 共 {{ recordDays.length }} 天
+
+
+
+ 加载中...
+
+
+ 📭
+ 该时段暂无任何记录
+
+
+
+
+
+ {{ day.dateLabel }}
+ 今天
+
+
+ {{ day.weekday }}
+
+ 🔊
+ 朗读
+
+
+
+
+
+
+
+ 血糖血压
+ {{ day.blood.record_time }}
+
+
+
+
+ 空腹
+
+ ▲
+ 偏高
+
+
+
+
+ {{ formatValue(day.blood.fasting_blood_sugar) }}
+
+ mmol/L
+
+
+
+ {{ getDayTrend(day.date, 'fasting_blood_sugar').dir === 'up' ? '↑' : getDayTrend(day.date, 'fasting_blood_sugar').dir === 'down' ? '↓' : '→' }}
+
+ {{ getDayTrend(day.date, 'fasting_blood_sugar').deltaText }}
+
+
+
+
+ 餐后
+
+ ▲
+ 偏高
+
+
+
+
+ {{ formatValue(day.blood.postprandial_blood_sugar) }}
+
+ mmol/L
+
+
+
+ {{ getDayTrend(day.date, 'postprandial_blood_sugar').dir === 'up' ? '↑' : getDayTrend(day.date, 'postprandial_blood_sugar').dir === 'down' ? '↓' : '→' }}
+
+ {{ getDayTrend(day.date, 'postprandial_blood_sugar').deltaText }}
+
+
+
+
+ 血压
+
+ ▲
+ 偏高
+
+
+
+
+ {{ day.blood.bpText }}
+
+ mmHg
+
+
+
+ {{ getDayBpTrend(day.date).dir === 'up' ? '↑' : getDayBpTrend(day.date).dir === 'down' ? '↓' : '→' }}
+
+ 收缩压 {{ getDayBpTrend(day.date).deltaText }}
+
+
+
+
+ 其他血糖
+
+ ▲
+ 偏高
+
+
+
+
+ {{ formatValue(day.blood.other_blood_sugar) }}
+
+ mmol/L
+
+
+
+ {{ getDayTrend(day.date, 'other_blood_sugar').dir === 'up' ? '↑' : getDayTrend(day.date, 'other_blood_sugar').dir === 'down' ? '↓' : '→' }}
+
+ {{ getDayTrend(day.date, 'other_blood_sugar').deltaText }}
+
+
+
+
+ 💊 {{ day.blood.western_medicine }}
+ 💉 {{ day.blood.insulin }}
+
+
+
+
+
+
+
+ 饮食
+
+
+
+ 🌞 早餐
+ {{ day.diet.breakfast_foods }}
+
+
+ 🍱 午餐
+ {{ day.diet.lunch_foods }}
+
+
+ 🌙 晚餐
+ {{ day.diet.dinner_foods }}
+
+
+
+
+
+
+
+
+ 运动
+
+
+ {{ day.exercise.exercise_type || '运动' }}
+ {{ day.exercise.duration }} 分钟
+ {{ day.exercise.intensityText }}
+
+
+
+
+
+
+
+ 备注
+
+
+
+ {{ line }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ inputForm.id ? '修改今日数据' : '录入今日数据' }}
+ {{ todayText }} · {{ patientName || '本人' }}
+
+
+ ×
+
+
+
+
+ 加载今日数据中…
+
+
+
+ 请如实填写,留空表示该项不上传。当天可多次修改。
+
+
+ 血糖(mmol/L)
+
+
+
+ 空腹血糖
+
+
+
+
+
+
+
+
+ 餐后血糖
+
+
+
+
+
+
+
+
+ 其他血糖
+
+
+
+
+
+
+ 血压(mmHg)
+
+
+
+ 高压
+
+
+
+
+
+
+
+ 低压
+
+
+
+
+
+
+
+ 备注
+
+
+
+
+
+
+
+ 删除
+
+
+ {{ inputForm.submitting ? '提交中…' : (inputForm.id ? '保存修改' : '提交录入') }}
+
+
+
+
+
+
+
+
+
+
diff --git a/admin/src/views/tcm/diagnosis/components/DailyMatrix.vue b/admin/src/views/tcm/diagnosis/components/DailyMatrix.vue
index 7ba95c4c..ded7fbba 100644
--- a/admin/src/views/tcm/diagnosis/components/DailyMatrix.vue
+++ b/admin/src/views/tcm/diagnosis/components/DailyMatrix.vue
@@ -23,6 +23,10 @@
+
+ 自录
+ = 患者在小程序自录
+
+血糖
+饮食
+运动
@@ -30,6 +34,10 @@
刷新
+
+ 自录
+ = 患者在小程序自录
+
刷新
@@ -57,7 +65,8 @@
:class="{
'is-clickable': isCellClickable(row.key, date),
'is-empty': !getCell(row.key, date).hasRecord,
- 'is-high': getCell(row.key, date).isHigh
+ 'is-high': getCell(row.key, date).isHigh,
+ 'is-patient-self': getCell(row.key, date).isPatientSelf && getCell(row.key, date).hasRecord
}"
@click="handleCellClick(row.key, date)"
>
@@ -82,6 +91,14 @@
{{ getCell(row.key, date).value }}
↑
+
+ 自录
+
—
@@ -345,6 +362,10 @@ interface BloodRecord {
western_medicine?: string
insulin?: string
remark?: string
+ /** 来源:0-医生录入 1-患者自录(小程序日常记录页) */
+ source?: number
+ /** 合并后该日是否含有「患者自录」记录 */
+ has_patient_self?: boolean
[key: string]: any
}
@@ -558,6 +579,10 @@ const visibleDates = computed(() => {
return dates
})
+const hasPatientSelfRecord = computed(() => {
+ return bloodRecords.value.some((r) => Number((r as any).source) === 1)
+})
+
const bloodByDate = computed(() => {
const map = new Map()
for (const record of bloodRecords.value) {
@@ -570,7 +595,8 @@ const bloodByDate = computed(() => {
patient_id: record.patient_id,
record_date: date,
record_time: record.record_time || '',
- record_date_ts: record.record_date_ts
+ record_date_ts: record.record_date_ts,
+ has_patient_self: false
})
}
const merged = map.get(date)!
@@ -594,6 +620,10 @@ const bloodByDate = computed(() => {
;(merged as any)[field] = record[field]
}
}
+ // 当天有任意 source=1 的记录则标记为患者自录
+ if (Number(record.source) === 1) {
+ merged.has_patient_self = true
+ }
}
return map
})
@@ -723,35 +753,40 @@ function parseTrendNumber(value: unknown): number | null {
return parsed
}
-function getCell(metric: string, date: string): { value: string; isHigh: boolean; hasRecord: boolean } {
+function getCell(metric: string, date: string): { value: string; isHigh: boolean; hasRecord: boolean; isPatientSelf?: boolean } {
const blood = bloodByDate.value.get(date)
const diet = dietByDate.value.get(date)
const exercise = exerciseByDate.value.get(date)
const tracking = trackingByDate.value.get(date)
+ const isPatientSelf = !!blood?.has_patient_self
switch (metric) {
case 'fasting':
return {
value: blood?.fasting_blood_sugar != null && blood.fasting_blood_sugar !== '' ? String(blood.fasting_blood_sugar) : '',
isHigh: isHighFastingBloodSugar(blood?.fasting_blood_sugar, props.age),
- hasRecord: !!blood && blood.fasting_blood_sugar != null && blood.fasting_blood_sugar !== ''
+ hasRecord: !!blood && blood.fasting_blood_sugar != null && blood.fasting_blood_sugar !== '',
+ isPatientSelf
}
case 'postprandial':
return {
value: blood?.postprandial_blood_sugar != null && blood.postprandial_blood_sugar !== '' ? String(blood.postprandial_blood_sugar) : '',
isHigh: isHighPostprandialBloodSugar(blood?.postprandial_blood_sugar, props.age),
- hasRecord: !!blood && blood.postprandial_blood_sugar != null && blood.postprandial_blood_sugar !== ''
+ hasRecord: !!blood && blood.postprandial_blood_sugar != null && blood.postprandial_blood_sugar !== '',
+ isPatientSelf
}
case 'other':
return {
value: blood?.other_blood_sugar != null && blood.other_blood_sugar !== '' ? String(blood.other_blood_sugar) : '',
isHigh: isHighOtherBloodSugar(blood?.other_blood_sugar, props.age),
- hasRecord: !!blood && blood.other_blood_sugar != null && blood.other_blood_sugar !== ''
+ hasRecord: !!blood && blood.other_blood_sugar != null && blood.other_blood_sugar !== '',
+ isPatientSelf
}
case 'bp':
return {
value: blood && (blood.systolic_pressure || blood.diastolic_pressure) ? formatBp(blood) : '',
isHigh: isHighBloodPressure(blood),
- hasRecord: !!blood && !!(blood.systolic_pressure || blood.diastolic_pressure)
+ hasRecord: !!blood && !!(blood.systolic_pressure || blood.diastolic_pressure),
+ isPatientSelf
}
case 'western':
return {
@@ -1115,6 +1150,12 @@ defineExpose({ refresh: fetchTracking })
color: #dc2626;
font-weight: 700;
}
+
+ &.is-patient-self {
+ position: relative;
+ background: linear-gradient(180deg, rgba(139, 92, 246, 0.04) 0%, rgba(139, 92, 246, 0.10) 100%);
+ border-radius: 4px;
+ }
}
&__cell-up {
@@ -1122,6 +1163,38 @@ defineExpose({ refresh: fetchTracking })
font-size: 13px;
}
+ &__cell-patient {
+ display: inline-block;
+ margin-left: 4px;
+ padding: 1px 6px;
+ font-size: 11px;
+ font-weight: 600;
+ color: #6d28d9;
+ background: #ede9fe;
+ border: 1px solid #ddd6fe;
+ border-radius: 999px;
+ line-height: 1.2;
+ letter-spacing: 0.5px;
+ white-space: nowrap;
+ }
+
+ &__legend {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ margin-right: 12px;
+ padding: 2px 10px 2px 6px;
+ background: #f8f6ff;
+ border: 1px dashed #ddd6fe;
+ border-radius: 999px;
+ }
+
+ &__legend-text {
+ font-size: 12px;
+ color: #6d28d9;
+ font-weight: 500;
+ }
+
&__todo {
margin-top: 16px;
}
diff --git a/server/app/api/controller/TcmController.php b/server/app/api/controller/TcmController.php
index 52f858cf..bed3e404 100755
--- a/server/app/api/controller/TcmController.php
+++ b/server/app/api/controller/TcmController.php
@@ -15,7 +15,10 @@
namespace app\api\controller;
use app\adminapi\logic\tcm\DiagnosisLogic;
+use app\adminapi\logic\tcm\TrackingNoteLogic;
use app\adminapi\logic\ConfigLogic;
+use app\common\model\tcm\Diagnosis;
+use app\common\model\tcm\BloodRecord;
/**
* 中医诊断控制器(供小程序调用)
@@ -28,7 +31,7 @@ class TcmController extends BaseApiController
* @notes 不需要登录的方法
* @var array
*/
- public array $notNeedLogin = ['getPatientSignature', 'diagnosisDetail', 'getDict', 'confirmDiagnosis', 'getCardList', 'getOrderByNo', 'patientHangupVideo'];
+ public array $notNeedLogin = ['getPatientSignature', 'diagnosisDetail', 'getDict', 'confirmDiagnosis', 'getCardList', 'getOrderByNo', 'patientHangupVideo', 'dailyTrackingWindow', 'dailyTrackingNotes'];
/**
* @notes 获取患者签名(供小程序调用)
@@ -332,6 +335,301 @@ class TcmController extends BaseApiController
}
}
+ /**
+ * @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', '');
+
+ if ($diagnosisId <= 0) {
+ return $this->fail('诊单ID不能为空');
+ }
+
+ try {
+ // 患者端越权校验:诊单必须存在;如传入 patient_id 则须与诊单匹配
+ $diagnosis = Diagnosis::where('id', $diagnosisId)->find();
+ if (!$diagnosis) {
+ return $this->fail('诊单不存在');
+ }
+ 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);
+
+ if ($diagnosisId <= 0) {
+ return $this->fail('诊单ID不能为空');
+ }
+
+ try {
+ $diagnosis = Diagnosis::where('id', $diagnosisId)->find();
+ if (!$diagnosis) {
+ return $this->fail('诊单不存在');
+ }
+ 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());
+ }
+ }
+
+ /**
+ * @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 日常记录-获取今日已录入的血糖血压记录(患者端)
+ *
+ * 路由: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) {
+ $payload['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 根据订单号获取订单详情(供小程序支付页调用)
* @return \think\response\Json
diff --git a/server/sql/1.9.20260526/add_blood_record_source.sql b/server/sql/1.9.20260526/add_blood_record_source.sql
new file mode 100644
index 00000000..6c6f731f
--- /dev/null
+++ b/server/sql/1.9.20260526/add_blood_record_source.sql
@@ -0,0 +1,11 @@
+-- 为「血糖血压记录表」增加来源字段
+-- 0 = 医生 / 助理在管理后台录入(历史数据默认 0)
+-- 1 = 患者在小程序「日常记录」页自录
+-- 该字段用于:1) 后台 DailyMatrix 上展示「患者自录」徽章;2) 限制患者只能编辑/删除自己当天的记录
+
+ALTER TABLE `zyt_tcm_blood_record`
+ADD COLUMN `source` tinyint(1) NOT NULL DEFAULT 0 COMMENT '来源:0-医生录入 1-患者自录' AFTER `remark`;
+
+-- 加索引方便按来源过滤
+ALTER TABLE `zyt_tcm_blood_record`
+ADD KEY `idx_source` (`source`);