From fb8ae6878c0104f100fd98a5fda3f2e9a5e15a58 Mon Sep 17 00:00:00 2001 From: Guoxianpeng <744964089@qq.com> Date: Thu, 16 Apr 2026 10:31:54 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E7=BB=9F=E8=AE=A1=E4=B8=AD=E5=BF=83-?= =?UTF-8?q?=E3=80=8B=E6=B7=BB=E5=8A=A0=E9=83=A8=E9=97=A8=E7=BB=9F=E8=AE=A1?= =?UTF-8?q?=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- admin/src/api/doctor.ts | 12 + admin/src/views/doctor/dept-tongji.vue | 356 ++++++++++++++++++ .../doctor/StatisticsController.php | 9 + .../adminapi/lists/doctor/StatisticsLists.php | 77 ++++ 4 files changed, 454 insertions(+) create mode 100644 admin/src/views/doctor/dept-tongji.vue diff --git a/admin/src/api/doctor.ts b/admin/src/api/doctor.ts index a96b2cd1..c05892d0 100644 --- a/admin/src/api/doctor.ts +++ b/admin/src/api/doctor.ts @@ -82,3 +82,15 @@ export function completeAppointment(params: any) { export function getDoctorStatistics(params: any) { return request.get({ url: '/doctor.statistics/lists', params }) } + +// ========== 部门统计 ========== + +// 获取部门列表 +export function getDeptList() { + return request.get({ url: '/dept.dept/all' }) +} + +// 获取部门统计 +export function getDeptStatistics(params: any) { + return request.get({ url: '/doctor.statistics/deptLists', params }) +} diff --git a/admin/src/views/doctor/dept-tongji.vue b/admin/src/views/doctor/dept-tongji.vue new file mode 100644 index 00000000..dc4d87fe --- /dev/null +++ b/admin/src/views/doctor/dept-tongji.vue @@ -0,0 +1,356 @@ + + + + + + + 筛选条件 + + + + + + + + + + + + 今天 + 最近7天 + 最近30天 + 自定义 + + + + + + + + + 查询 + 重置 + + + + + + + + + 部门统计数据 + + + + + + + + + + + + + + 已挂号: {{ row.registered_count }} + + + 已完成: {{ row.completed_count }} + + + 已取消: {{ row.canceled_count }} + + + 已过号: {{ row.expired_count }} + + + 总数量: {{ row.total_count }} + + + 完成率: {{ row.completion_rate }}% + + + + + + + + + + + + + + + + + + + diff --git a/server/app/adminapi/controller/doctor/StatisticsController.php b/server/app/adminapi/controller/doctor/StatisticsController.php index 97b2b8a9..b312a3e5 100644 --- a/server/app/adminapi/controller/doctor/StatisticsController.php +++ b/server/app/adminapi/controller/doctor/StatisticsController.php @@ -20,4 +20,13 @@ class StatisticsController extends BaseAdminController { return $this->dataLists(new StatisticsLists()); } + + /** + * @notes 部门统计列表 + * @return \think\response\Json + */ + public function deptLists() + { + return $this->dataLists(new StatisticsLists('dept')); + } } diff --git a/server/app/adminapi/lists/doctor/StatisticsLists.php b/server/app/adminapi/lists/doctor/StatisticsLists.php index e5840337..9139014c 100644 --- a/server/app/adminapi/lists/doctor/StatisticsLists.php +++ b/server/app/adminapi/lists/doctor/StatisticsLists.php @@ -13,6 +13,17 @@ use app\common\model\auth\Admin; */ class StatisticsLists extends BaseAdminDataLists { + protected $type = 'doctor'; + + /** + * @param string $type 统计类型:doctor(医生统计) 或 dept(部门统计) + */ + public function __construct($type = 'doctor') + { + parent::__construct(); + $this->type = $type; + } + /** * @notes 搜索条件 * @return array @@ -28,6 +39,10 @@ class StatisticsLists extends BaseAdminDataLists */ public function lists(): array { + if ($this->type === 'dept') { + return $this->getDeptStatistics(); + } + $doctorId = $this->params['doctor_id'] ?? ''; $timeType = $this->params['time_type'] ?? 'today'; $startDate = $this->params['start_date'] ?? ''; @@ -377,6 +392,68 @@ class StatisticsLists extends BaseAdminDataLists return $result; } + /** + * @notes 获取部门统计数据 + * @return array + */ + private function getDeptStatistics() + { + $deptId = $this->params['dept_id'] ?? ''; + $timeType = $this->params['time_type'] ?? 'today'; + $startDate = $this->params['start_date'] ?? ''; + $endDate = $this->params['end_date'] ?? ''; + + // 计算时间范围 + list($startDate, $endDate, $timeRangeText) = $this->getTimeRange($timeType, $startDate, $endDate); + + // 查询部门统计数据 + $deptStatsQuery = \think\facade\Db::name('doctor_appointment') + ->alias('apt') + ->leftJoin('zyt_admin_dept ad', 'apt.assistant_id = ad.admin_id') + ->leftJoin('zyt_dept dept', 'ad.dept_id = dept.id') + ->field([ + 'dept.id as dept_id', + 'dept.name as dept_name', + 'COUNT(*) as total_count', + 'SUM(CASE WHEN apt.status = 1 THEN 1 ELSE 0 END) as registered_count', + 'SUM(CASE WHEN apt.status = 3 THEN 1 ELSE 0 END) as completed_count', + 'SUM(CASE WHEN apt.status = 2 THEN 1 ELSE 0 END) as canceled_count', + 'SUM(CASE WHEN apt.status = 4 THEN 1 ELSE 0 END) as expired_count' + ]) + ->where('apt.appointment_date', '>=', $startDate) + ->where('apt.appointment_date', '<=', $endDate) + ->whereNotNull('dept.id'); + + if ($deptId) { + $deptStatsQuery->where('dept.id', $deptId); + } + + $deptStats = $deptStatsQuery->group('dept.id')->select()->toArray(); + + // 组装结果 + $result = []; + foreach ($deptStats as $stat) { + // 计算完成率 + $completionRate = $stat['total_count'] > 0 + ? round(($stat['completed_count'] / $stat['total_count']) * 100, 2) + : 0; + + $result[] = [ + 'dept_id' => $stat['dept_id'], + 'dept_name' => $stat['dept_name'], + 'total_count' => (int)$stat['total_count'] ?? 0, + 'registered_count' => (int)$stat['registered_count'] ?? 0, + 'completed_count' => (int)$stat['completed_count'] ?? 0, + 'canceled_count' => (int)$stat['canceled_count'] ?? 0, + 'expired_count' => (int)$stat['expired_count'] ?? 0, + 'completion_rate' => $completionRate, + 'time_range' => $timeRangeText + ]; + } + + return $result; + } + /** * @notes 获取时间范围 * @param string $timeType From 0fff9953029dfc8a08eaf309346d7613bfdee623 Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 16 Apr 2026 11:32:03 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DIAGNOSIS_IMAGE_URL_PREFIX_FIX.md | 259 ++++++++++++++++++ .../app/adminapi/logic/tcm/DiagnosisLogic.php | 28 ++ 2 files changed, 287 insertions(+) create mode 100644 DIAGNOSIS_IMAGE_URL_PREFIX_FIX.md diff --git a/DIAGNOSIS_IMAGE_URL_PREFIX_FIX.md b/DIAGNOSIS_IMAGE_URL_PREFIX_FIX.md new file mode 100644 index 00000000..b17ee92f --- /dev/null +++ b/DIAGNOSIS_IMAGE_URL_PREFIX_FIX.md @@ -0,0 +1,259 @@ +# Diagnosis Image URL Prefix Fix (诊断图片URL前缀修复) + +## Issue +Image URLs in the diagnosis detail (tongue_images and report_files) were stored as relative paths without the domain prefix, causing issues when accessing them from external sources or different domains. + +## Solution +Added logic to automatically prepend the current domain to image URLs that don't already have an `http://` or `https://` prefix. + +## Changes Made + +### File: `server/app/adminapi/logic/tcm/DiagnosisLogic.php` + +#### 1. Updated tongue_images Processing +Added domain prefix logic after parsing the images array: + +```php +if (!empty($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'])) ?: []; + } + + // 为没有 http 前缀的图片添加域名 + $domain = request()->domain(); + $diagnosis['tongue_images'] = array_map(function($url) use ($domain) { + if (empty($url)) { + return $url; + } + // 如果已经包含 http:// 或 https://,则跳过 + if (stripos($url, 'http://') === 0 || stripos($url, 'https://') === 0) { + return $url; + } + // 添加域名前缀 + return $domain . $url; + }, $diagnosis['tongue_images']); +} else { + $diagnosis['tongue_images'] = []; +} +``` + +#### 2. Updated report_files Processing +Applied the same logic to report files: + +```php +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'])) ?: []; + } + + // 为没有 http 前缀的文件添加域名 + $domain = request()->domain(); + $diagnosis['report_files'] = array_map(function($url) use ($domain) { + if (empty($url)) { + return $url; + } + // 如果已经包含 http:// 或 https://,则跳过 + if (stripos($url, 'http://') === 0 || stripos($url, 'https://') === 0) { + return $url; + } + // 添加域名前缀 + return $domain . $url; + }, $diagnosis['report_files']); +} else { + $diagnosis['report_files'] = []; +} +``` + +## Logic Flow + +### URL Processing Logic: +1. Parse the images/files array (JSON or comma-separated) +2. Get current domain using `request()->domain()` +3. For each URL in the array: + - Check if URL is empty → skip + - Check if URL starts with `http://` or `https://` → skip (already has protocol) + - Otherwise → prepend domain to the URL + +### Examples: + +#### Relative Path (Needs Domain): +```php +Input: "/uploads/images/tongue/2024/01/image.jpg" +Domain: "https://admin.zhenyangtang.com.cn" +Output: "https://admin.zhenyangtang.com.cn/uploads/images/tongue/2024/01/image.jpg" +``` + +#### Absolute URL (Skip): +```php +Input: "https://cdn.example.com/images/tongue.jpg" +Output: "https://cdn.example.com/images/tongue.jpg" +``` + +#### HTTP URL (Skip): +```php +Input: "http://example.com/image.jpg" +Output: "http://example.com/image.jpg" +``` + +#### Empty URL (Skip): +```php +Input: "" +Output: "" +``` + +## Benefits + +### 1. Cross-Domain Compatibility +Images can now be accessed from different domains or external applications without path issues. + +### 2. API Response Consistency +API responses always return complete, accessible URLs regardless of how they were stored. + +### 3. Backward Compatibility +- Existing relative paths are automatically converted +- Existing absolute URLs are preserved +- No database migration required + +### 4. Flexible Storage +- Database can store either relative or absolute paths +- System handles both formats transparently +- Future-proof for CDN integration + +## Use Cases + +### 1. Mobile App Access +Mobile apps can directly use the returned URLs without needing to construct them. + +### 2. Third-Party Integration +External systems can access images using the complete URLs from API responses. + +### 3. CDN Migration +When migrating to CDN, images with absolute CDN URLs will work alongside local relative paths. + +### 4. Multi-Domain Setup +System works correctly across different domains (dev, staging, production). + +## Testing Steps + +### 1. Test Relative Path URLs +1. Create a diagnosis with tongue images stored as relative paths: + ``` + /uploads/images/tongue/2024/01/image.jpg + ``` +2. Fetch diagnosis detail via API +3. Verify response contains: + ```json + { + "tongue_images": [ + "https://admin.zhenyangtang.com.cn/uploads/images/tongue/2024/01/image.jpg" + ] + } + ``` + +### 2. Test Absolute URLs +1. Create a diagnosis with absolute URL: + ``` + https://cdn.example.com/images/tongue.jpg + ``` +2. Fetch diagnosis detail +3. Verify URL is unchanged: + ```json + { + "tongue_images": [ + "https://cdn.example.com/images/tongue.jpg" + ] + } + ``` + +### 3. Test Mixed URLs +1. Create a diagnosis with both relative and absolute URLs: + ``` + [ + "/uploads/local.jpg", + "https://cdn.example.com/remote.jpg" + ] + ``` +2. Fetch diagnosis detail +3. Verify correct processing: + ```json + { + "tongue_images": [ + "https://admin.zhenyangtang.com.cn/uploads/local.jpg", + "https://cdn.example.com/remote.jpg" + ] + } + ``` + +### 4. Test Empty/Null Values +1. Create a diagnosis with empty images +2. Fetch diagnosis detail +3. Verify empty array is returned: + ```json + { + "tongue_images": [] + } + ``` + +### 5. Test Report Files +Repeat all tests above for `report_files` field. + +## Technical Details + +### Domain Detection +Uses `request()->domain()` which returns the current request domain including protocol: +- Development: `http://localhost:8000` +- Production: `https://admin.zhenyangtang.com.cn` + +### Case-Insensitive Protocol Check +Uses `stripos()` for case-insensitive checking: +- Matches: `http://`, `HTTP://`, `Http://` +- Matches: `https://`, `HTTPS://`, `Https://` + +### Array Processing +Uses `array_map()` for efficient processing of all URLs in the array. + +## Related Files + +### Backend: +- `server/app/adminapi/logic/tcm/DiagnosisLogic.php` + - Updated `tongue_images` processing + - Updated `report_files` processing + +### Database: +- `tcm_diagnosis` table + - `tongue_images` column (stores JSON or comma-separated paths) + - `report_files` column (stores JSON or comma-separated paths) + +## Notes + +### Storage Format +The database continues to store paths as-is (relative or absolute). The conversion happens only when reading data for API responses. + +### Performance +The `array_map()` operation is efficient and adds minimal overhead. For typical diagnosis records with 1-5 images, the performance impact is negligible. + +### Future Enhancements +If needed, this logic could be extracted into a helper function for reuse across other file/image fields in the system. + +## Summary + +✅ Automatically adds domain prefix to relative image URLs +✅ Preserves absolute URLs (http/https) +✅ Handles both tongue_images and report_files +✅ Backward compatible with existing data +✅ Case-insensitive protocol detection +✅ Handles empty/null values gracefully +✅ No database migration required +✅ Works across different domains + +Image URLs in diagnosis details are now always complete and accessible, improving API usability and cross-domain compatibility! diff --git a/server/app/adminapi/logic/tcm/DiagnosisLogic.php b/server/app/adminapi/logic/tcm/DiagnosisLogic.php index b3c30ed1..a03b7238 100644 --- a/server/app/adminapi/logic/tcm/DiagnosisLogic.php +++ b/server/app/adminapi/logic/tcm/DiagnosisLogic.php @@ -255,6 +255,20 @@ class DiagnosisLogic extends BaseLogic // JSON 解析失败则按逗号分割 $diagnosis['tongue_images'] = array_filter(explode(',', $diagnosis['tongue_images'])) ?: []; } + + // 为没有 http 前缀的图片添加域名 + $domain = request()->domain(); + $diagnosis['tongue_images'] = array_map(function($url) use ($domain) { + if (empty($url)) { + return $url; + } + // 如果已经包含 http:// 或 https://,则跳过 + if (stripos($url, 'http://') === 0 || stripos($url, 'https://') === 0) { + return $url; + } + // 添加域名前缀 + return $domain .'/'. $url; + }, $diagnosis['tongue_images']); } else { $diagnosis['tongue_images'] = []; } @@ -269,6 +283,20 @@ class DiagnosisLogic extends BaseLogic // JSON 解析失败则按逗号分割 $diagnosis['report_files'] = array_filter(explode(',', $diagnosis['report_files'])) ?: []; } + + // 为没有 http 前缀的文件添加域名 + $domain = request()->domain(); + $diagnosis['report_files'] = array_map(function($url) use ($domain) { + if (empty($url)) { + return $url; + } + // 如果已经包含 http:// 或 https://,则跳过 + if (stripos($url, 'http://') === 0 || stripos($url, 'https://') === 0) { + return $url; + } + // 添加域名前缀 + return $domain . $url; + }, $diagnosis['report_files']); } else { $diagnosis['report_files'] = []; }