Files
hms/docs/qa/miniprogram-contract-verification-report.md
iven d623f8b2ff fix: V1 测试版本端到端验证修复 — 6 CRITICAL + 3 HIGH 问题全量修复
修复项:
- fix(db): 迁移 149 — 修复 Admin 角色权限绑定被迁移链破坏 (FE-C1)
- fix(health): 4 个 handler 添加空名称验证 — Doctor/Article/AlertRule/Tag (API-C1~C4)
- fix(health): Stats 仪表盘 new_this_week 查询修复 — SeaORM date_trunc bug (FE-C2)
- fix(server): 添加安全响应头 — X-Frame-Options/CSP/XSS-Protection/Referrer-Policy (SEC-H1)
- fix(mp): 预约创建契约修复 — notes/reason 字段映射 + 移除 schedule_id (MP-H1)
- fix(mp): 咨询会话 subject/last_message 字段改为可选 (MP-H3)
- fix(ai): AiConfig Default derive 替代手写 impl (clippy)

测试报告:
- 8 维度端到端测试全部完成 (后端 87 用例 / 前端 30 页面 / 小程序 80+ API / 安全 20 项 / 性能 20 端点)
- 多角色 7 角色 49 检查 100% 通过
- 综合测试报告 + 专家评估报告
2026-05-18 10:24:40 +08:00

358 lines
22 KiB
Markdown

# Mini-Program API Contract Verification Report
> Date: 2026-05-18 | Tester: API Tester (automated) | Branch: feat/media-library-banner
## 1. Summary
Comprehensive verification of API contract consistency between the WeChat mini-program service layer (TypeScript) and the backend Rust DTOs and live API responses. The mini-program source is at `apps/miniprogram/src/services/` and the backend DTOs are at `crates/erp-health/src/dto/`.
**Overall Status: PASS with 7 issues found (0 CRITICAL, 3 HIGH, 4 MEDIUM)**
## 2. Build Verification
| Check | Status |
|-------|--------|
| `pnpm build:weapp` | PASS (Sass deprecation warnings only, no errors) |
| Page count (app.config.ts) | 60 pages (12 main + 48 subpackage) |
| All page files exist | PASS (60/60 files verified) |
| Backend server startup | PASS (database connected, migrations applied) |
## 3. API Contract Comparison
### 3.1 Patient Service (`services/patient.ts` vs `dto/patient_dto.rs`)
| Endpoint | MP Path | Backend Path | Status |
|----------|---------|-------------|--------|
| List patients | `GET /health/patients` | `GET /health/patients` | PASS |
| Create patient | `POST /health/patients` | `POST /health/patients` | PASS |
| Update patient | `PUT /health/patients/{id}` | `PUT /health/patients/{id}` | PASS |
**Field comparison (MP `Patient` vs Backend `PatientResp`):**
| Field | MP Type | Backend Type | Match |
|-------|---------|-------------|-------|
| id | string | Uuid (string) | PASS |
| name | string | String | PASS |
| gender | string? | Option<String> | PASS |
| birth_date | string? | Option<NaiveDate> | PASS |
| blood_type | string? | Option<String> | PASS |
| id_number | string? | Option<String> | PASS |
| allergy_history | string? | Option<String> | PASS |
| medical_history_summary | string? | Option<String> | PASS |
| emergency_contact_name | string? | Option<String> | PASS |
| emergency_contact_phone | string? | Option<String> | PASS |
| phone | string? | **MISSING** | ISSUE-1 |
| relation | string? | **MISSING** | ISSUE-2 |
| status | string? | String | PASS |
| verification_status | string? | String | PASS |
| source | string? | Option<String> | PASS |
| notes | string? | Option<String> | PASS |
| version | number | i32 | PASS |
| user_id | **MISSING** | Option<Uuid> | PASS (internal) |
| created_at | **MISSING** | DateTime | PASS (not needed in MP) |
| updated_at | **MISSING** | DateTime | PASS (not needed in MP) |
**ISSUE-1 (MEDIUM):** MP `Patient.phone` does not exist in backend `PatientResp`. Backend has no phone field on patient entity. This field will always be `undefined` from the API.
**ISSUE-2 (MEDIUM):** MP `Patient.relation` does not exist in backend `PatientResp`. Relation is a family member concept, not on the patient entity itself. The `PatientUpdateInput` also includes `relation` which backend ignores.
### 3.2 Appointment Service (`services/appointment.ts` vs `dto/appointment_dto.rs`)
| Endpoint | MP Path | Backend Path | Status |
|----------|---------|-------------|--------|
| List appointments | `GET /health/appointments` | `GET /health/appointments` | PASS |
| Get appointment | `GET /health/appointments/{id}` | `GET /health/appointments/{id}` | PASS |
| Create appointment | `POST /health/appointments` | `POST /health/appointments` | ISSUE-3 |
| Cancel appointment | `PUT /health/appointments/{id}/status` | `PUT /health/appointments/{id}/status` | PASS |
| List doctors | `GET /health/doctors` | `GET /health/doctors` | PASS |
| Get doctor schedules | `GET /health/doctor-schedules` | `GET /health/doctor-schedules` | PASS |
| Calendar view | `GET /health/doctor-schedules/calendar` | `GET /health/doctor-schedules/calendar` | PASS |
**ISSUE-3 (HIGH):** MP `createAppointment` sends `schedule_id` and `reason` fields. Backend `CreateAppointmentReq` does not have `schedule_id` or `reason`. It does have `notes` (optional). The `reason` field will be silently dropped. The `schedule_id` is not used by backend at all.
**MP `Appointment` vs Backend `AppointmentResp`:**
| Field | MP Type | Backend Type | Match |
|-------|---------|-------------|-------|
| patient_name | string | Option<String> | PASS |
| doctor_name | string | Option<String> | ISSUE-4 |
| department | string? | **MISSING** | ISSUE-4 |
| appointment_date | string | NaiveDate | PASS |
| start_time | string | NaiveTime | PASS |
| end_time | string | NaiveTime | PASS |
| status | string | String | PASS |
| version | number | i32 | PASS |
| appointment_type | **MISSING** | String | ISSUE-5 |
| patient_id | **MISSING** | Uuid | PASS (not in MP DTO) |
| doctor_id | **MISSING** | Option<Uuid> | PASS (not in MP DTO) |
| cancel_reason | **MISSING** | Option<String> | ISSUE-5 |
| notes | **MISSING** | Option<String> | ISSUE-5 |
| created_at | **MISSING** | DateTime | PASS (not needed) |
| updated_at | **MISSING** | DateTime | PASS (not needed) |
**ISSUE-4 (HIGH):** MP `Appointment.department` does not exist in backend `AppointmentResp`. Appointment has no department field -- department belongs to Doctor. MP expects `department` on each appointment but backend never returns it.
**ISSUE-5 (MEDIUM):** MP `Appointment` DTO is missing `appointment_type`, `cancel_reason`, and `notes` fields that backend sends. These fields are lost when consuming the API response.
**MP `DoctorSchedule` vs Backend `ScheduleResp`:**
| Field | MP Type | Backend Type | Match |
|-------|---------|-------------|-------|
| date | string | **MISSING** (schedule_date) | ISSUE-6 |
| available_count | number | **MISSING** (computed) | ISSUE-6 |
**ISSUE-6 (MEDIUM):** MP `DoctorSchedule.date` -- backend uses `schedule_date`. MP also expects `available_count` which backend does not provide. MP compensates with client-side computation (`max_appointments - current_appointments`), so this is handled but relies on correct fallback logic.
### 3.3 Consultation Service (`services/consultation.ts` vs `dto/consultation_dto.rs`)
| Endpoint | MP Path | Backend Path | Status |
|----------|---------|-------------|--------|
| List sessions | `GET /health/consultation-sessions` | `GET /health/consultation-sessions` | PASS |
| Get session | `GET /health/consultation-sessions/{id}` | `GET /health/consultation-sessions/{id}` | PASS |
| List messages | `GET /health/consultation-sessions/{id}/messages` | `GET /health/consultation-sessions/{id}/messages` | PASS |
| Send message | `POST /health/consultation-messages` | `POST /health/consultation-messages` | PASS |
| Mark read | `PUT /health/consultation-sessions/{id}/read` | `PUT /health/consultation-sessions/{id}/read` | PASS |
| Poll messages | `GET /health/consultation-sessions/{id}/messages/poll` | `GET /.../poll` | PASS |
**MP `ConsultationSession` vs Backend `SessionResp`:**
| Field | MP Type | Backend Type | Match |
|-------|---------|-------------|-------|
| subject | string? | **MISSING** | ISSUE-7 |
| last_message | string? | **MISSING** | ISSUE-7 |
| updated_at | string? | DateTime | PASS |
| version | number? | i32 | PASS |
**ISSUE-7 (HIGH):** MP `ConsultationSession` expects `subject` and `last_message` fields that do not exist in backend `SessionResp`. Backend returns `last_message_at` (timestamp only, no content). These fields will always be `null/undefined`.
### 3.4 Health Data Service (`services/health.ts` vs `dto/health_data_dto.rs`)
| Endpoint | MP Path | Backend Path | Status |
|----------|---------|-------------|--------|
| Today summary | `GET /health/vital-signs/today` | `GET /health/vital-signs/today` | PASS |
| Input vital sign | `POST /health/patients/{id}/vital-signs` | `POST /health/patients/{id}/vital-signs` | PASS |
| Get trend | `GET /health/vital-signs/trend` | `GET /health/vital-signs/trend` | PASS |
| Daily monitoring list | `GET /health/patients/{id}/daily-monitoring` | `GET /.../daily-monitoring` | PASS |
| Create daily monitoring | `POST /health/daily-monitoring` | `POST /health/daily-monitoring` | PASS |
| Health thresholds | `GET /health/critical-value-thresholds/public` | `GET /.../public` | PASS |
**Today Summary:** MP `TodaySummary` matches backend `MiniTodayResp` -- both have `blood_pressure`, `heart_rate`, `blood_sugar`, `weight` with nested `IndicatorSummary` objects. PASS.
**Input Vital Sign:** MP performs indicator_type-to-structured-field mapping before sending (e.g., `blood_pressure` -> `systolic_bp_morning`/`diastolic_bp_morning`). This matches backend `CreateVitalSignsReq` structure. PASS.
**Daily Monitoring:** MP `DailyMonitoring` matches backend `DailyMonitoringResp` exactly. PASS.
### 3.5 Points Service (`services/points.ts` vs `dto/points_dto.rs`)
| Endpoint | MP Path | Backend Path | Status |
|----------|---------|-------------|--------|
| Get account | `GET /health/points/account` | `GET /health/points/account` | PASS |
| Daily checkin | `POST /health/points/checkin` | `POST /health/points/checkin` | PASS |
| Checkin status | `GET /health/points/checkin/status` | `GET /health/points/checkin/status` | PASS |
| List products | `GET /health/points/products` | `GET /health/points/products` | PASS |
| Get product | `GET /health/points/products/{id}` | `GET /health/points/products/{id}` | PASS |
| Exchange | `POST /health/points/exchange` | `POST /health/points/exchange` | PASS |
| List orders | `GET /health/points/orders` | `GET /health/points/orders` | PASS |
| List transactions | `GET /health/points/transactions` | `GET /health/points/transactions` | PASS |
| List offline events | `GET /health/offline-events` | `GET /health/offline-events` | PASS |
| Register event | `POST /health/offline-events/{id}/register` | `POST /.../register` | PASS |
**Field verification (live API responses):**
| DTO | Match |
|-----|-------|
| PointsAccount vs PointsAccountResp | PASS (all fields present: id, patient_id, balance, total_earned, total_spent, total_expired) |
| CheckinStatus vs CheckinStatusResp | PASS (checked_in_today, consecutive_days, next_streak_milestone) |
| PointsProduct vs PointsProductResp | PASS |
| PointsOrder vs PointsOrderResp | PASS (qr_code is UUID string, verified_by/verified_at nullable) |
| PointsTransaction vs PointsTransactionResp | PASS (transaction_type matches backend field name) |
| OfflineEvent vs OfflineEventResp | PASS (all fields including start_time/end_time which are optional) |
### 3.6 Alert Service (`services/alert.ts` vs `dto/alert_dto.rs`)
| Endpoint | MP Path | Backend Path | Status |
|----------|---------|-------------|--------|
| List patient alerts | `GET /health/alerts` | `GET /health/alerts` | PASS |
**MP `Alert` vs Backend `AlertResponse`:**
| Field | Match |
|-------|-------|
| id, patient_id, rule_id, severity, title, status, created_at | PASS |
| detail (Record<string,unknown>) | PASS (backend returns JSON) |
| message | **MISSING** in backend |
| acknowledged_at, resolved_at, version | PASS |
**Note:** MP has `message` field which does not exist in `AlertResponse`. Backend has `acknowledged_by`, `acknowledged_by_name` which MP does not consume. Minor gap, not causing errors.
### 3.7 Article Service (`services/article.ts` vs `dto/article_dto.rs`)
| Endpoint | MP Path | Backend Path | Status |
|----------|---------|-------------|--------|
| List articles | `GET /health/articles` | `GET /health/articles` | PASS |
| Get article detail | `GET /health/articles/{id}` | `GET /health/articles/{id}` | PASS |
| Public article detail | `GET /public/articles/{id}` | `GET /public/articles/{id}` | PASS |
| List categories | `GET /health/article-categories` | `GET /health/article-categories` | PASS |
**MP `Article` vs Backend `ArticleResp` / `ArticleListItem`:**
Backend returns `tags` as `Vec<String>` (tag names), but MP expects `tags?: { id: string; name: string }[]` (objects with id and name). The live API confirms tags are returned as `string[]`. MP's typed interface will not match -- tag objects would be undefined. This is handled gracefully by TypeScript (optional field), but category_name would be null while MP expects it.
### 3.8 Follow-Up Service (`services/followup.ts` vs `dto/follow_up_dto.rs`)
| Endpoint | MP Path | Backend Path | Status |
|----------|---------|-------------|--------|
| List tasks | `GET /health/follow-up-tasks` | `GET /health/follow-up-tasks` | PASS |
| Get task detail | `GET /health/follow-up-tasks/{id}` | `GET /health/follow-up-tasks/{id}` | PASS |
| Submit record | `POST /health/follow-up-tasks/{id}/records` | `POST /.../records` | PASS |
| List records | `GET /health/follow-up-records` | `GET /health/follow-up-records` | PASS |
**MP `FollowUpTask` vs Backend `FollowUpTaskResp`:** PASS -- all fields match.
### 3.9 Other Services Verified
| Service | Endpoints | Status |
|---------|-----------|--------|
| Dialysis | listDialysisRecords, getDialysisRecord, listDialysisPrescriptions, getDialysisPrescription | PASS |
| Health Record | listHealthRecords, listDiagnoses | PASS |
| Consent | listConsents, grantConsent, revokeConsent | PASS |
| Medication Reminder | listReminders, createReminder, updateReminder, deleteReminder | PASS |
| Device Sync | uploadReadings, queryDeviceReadings, queryHourlyReadings | PASS |
| Action Inbox | listActionItems, getActionThread | PASS |
| Notification | list, markRead, markAllRead, getUnreadCount | PASS |
| AI Chat | sendAiMessage | PASS |
| AI Analysis | listAiAnalysis, getAiAnalysisDetail | PASS |
| Doctor Dashboard | getDashboard | PASS |
| Doctor Patient | listPatients, getPatient, getHealthSummary, listPatientTags, getPatientStats | PASS |
| Doctor Consultation | listSessions, getSession, listMessages, sendMessage, markSessionRead, closeSession, pollMessages, getConsultationStats | PASS |
| Doctor Follow-up | listFollowUpTasks, getFollowUpTask, updateFollowUpTask, createFollowUpRecord, listFollowUpRecords, getFollowUpStats | PASS |
| Doctor Lab Report | listLabReports, getLabReport, reviewLabReport | PASS |
| Doctor Alerts | listAlerts, getAlert, acknowledgeAlert, dismissAlert, resolveAlert | PASS |
| Doctor Appointment | listAppointments | PASS |
| Doctor Dialysis | Full CRUD + stats | PASS |
| Analytics | trackEvent, flushEvents | PASS (fire-and-forget) |
| Auth | credentialLogin, wechatLogin, wechatBindPhone, getPatients | PASS |
## 4. Page Route Verification
All 60 page routes in `app.config.ts` have corresponding `.tsx` files:
- Main pages: 12/12 exist
- pkg-health subpackage: 5/5 exist
- pkg-doctor-core subpackage: 8/8 exist
- pkg-doctor-clinical subpackage: 10/10 exist
- pkg-mall subpackage: 4/4 exist
- pkg-profile subpackage: 18/18 exist
- ai-report subpackage: 2/2 exist
- article subpackage: 2/2 exist
- pkg-consultation subpackage: 1/1 exist
**Status: PASS**
## 5. Cross-Platform Data Flow Verification
### 5.1 Patient Created on Web -> Retrieved via API
Tested: `GET /health/patients` returns 83 patients including ones created through web admin. Response structure matches `PatientResp` DTO. All standard fields present.
**Status: PASS**
### 5.2 Appointment Created via API -> Has All Fields
Backend `AppointmentResp` includes `patient_name`, `doctor_name`, `appointment_type`, `cancel_reason`, `notes`, and all timing fields. MP receives the full response.
**Status: PASS** (MP just does not consume all fields in its DTO)
### 5.3 Health Data Input
MP's `inputVitalSign()` correctly maps indicator types to backend's structured format (`systolic_bp_morning`, etc.). The `CreateVitalSignsReq` DTO matches what MP sends.
**Status: PASS**
### 5.4 Consultation Messages
Both patient and doctor services use the same message endpoints. `ConsultationMessage` DTOs match backend `MessageResp` (id, session_id, sender_id, sender_role, content_type, content, is_read, created_at).
**Status: PASS**
### 5.5 Points/Balance Consistency
Points account, transactions, and orders all use the same backend data. Balance changes via checkin or exchange are reflected in the account immediately. No MP-specific caching issues.
**Status: PASS**
## 6. Issues Summary
### HIGH Priority
| ID | Service | Issue | Impact |
|----|---------|-------|--------|
| ISSUE-3 | Appointment | MP sends `schedule_id` and `reason` fields in create request; backend does not accept these. `CreateAppointmentReq` has `notes` instead of `reason`, and no `schedule_id` field. | Data silently dropped. Appointment may not link to schedule. |
| ISSUE-4 | Appointment | MP `Appointment.department` does not exist on backend. Backend returns department only on Doctor entity, not on Appointment. | UI always shows empty/undefined for department. |
| ISSUE-7 | Consultation | MP `ConsultationSession.subject` and `last_message` fields do not exist on backend `SessionResp`. | These fields always null/undefined in MP UI. |
### MEDIUM Priority
| ID | Service | Issue | Impact |
|----|---------|-------|--------|
| ISSUE-1 | Patient | MP `Patient.phone` does not exist in backend `PatientResp`. No phone field on patient entity. | Field always undefined. |
| ISSUE-2 | Patient | MP `Patient.relation` and `PatientUpdateInput.relation` do not exist on backend. | Field always undefined; update sends data backend ignores. |
| ISSUE-5 | Appointment | MP `Appointment` DTO missing `appointment_type`, `cancel_reason`, `notes` from backend response. | Data available from API but not consumed by MP. |
| ISSUE-6 | DoctorSchedule | MP expects `date` field but backend uses `schedule_date`. MP expects `available_count` but it is not returned. | Client-side workaround exists (field rename + computation), but fragile. |
### LOW Priority
| ID | Service | Issue | Impact |
|----|---------|-------|--------|
| - | Article | MP expects `tags` as `{id, name}[]` but backend returns `string[]`. | Tags display may be empty or incorrect. |
| - | Article | MP expects `category_name` but backend returns `category` (string) and `category_id` (UUID). | Minor naming mismatch, no data loss. |
| - | Alert | MP has `message` field not in backend. Backend has `acknowledged_by_name` not consumed by MP. | Extra/null fields, no functional impact. |
## 7. Recommendations
1. **ISSUE-3 (HIGH):** Align MP `createAppointment` with backend. Either add `schedule_id` to backend `CreateAppointmentReq` (to enable schedule-based booking with capacity tracking), or remove `schedule_id` from MP and use `notes` instead of `reason`.
2. **ISSUE-4 (HIGH):** Add `department` to backend `AppointmentResp` (join from doctor entity), or have MP fetch doctor details separately to get department.
3. **ISSUE-7 (HIGH):** Add `subject` and `last_message` (content preview) to backend `SessionResp`, or remove from MP DTO if not needed.
4. **ISSUE-1/2 (MEDIUM):** Either add `phone` and `relation` fields to backend `PatientResp` (via join or denormalization), or remove from MP interface and handle these at the family-member level.
5. **ISSUE-5 (MEDIUM):** Add missing fields (`appointment_type`, `cancel_reason`, `notes`) to MP `Appointment` DTO to consume available backend data.
6. **ISSUE-6 (MEDIUM):** Standardize field naming: rename backend `schedule_date` to `date`, or update MP to use `schedule_date`. Consider adding `available_count` as a computed field in backend response.
7. **Article tags:** Update MP `Article.tags` type to `string[]` to match backend, or update backend to return full tag objects.
## 8. Service File Inventory
| File | Endpoints | Functions | Interfaces |
|------|-----------|-----------|------------|
| request.ts | N/A (base) | api.get/post/put/delete, requestUnlimited | ApiResponse, CacheEntry |
| patient.ts | 3 | listPatients, createPatient, updatePatient | Patient, PatientUpdateInput |
| appointment.ts | 7 | listAppointments, getAppointment, createAppointment, cancelAppointment, getDoctorSchedules, listDoctors, calendarView | Appointment, Doctor, DoctorSchedule |
| consultation.ts | 6 | listConsultations, getSession, listMessages, sendMessage, markSessionRead, pollMessages | ConsultationSession, ConsultationMessage |
| health.ts | 6 | getTodaySummary, inputVitalSign, getTrend, createDailyMonitoring, listDailyMonitoring, getHealthThresholds | TodaySummary, DailyMonitoring, HealthThreshold |
| points.ts | 11 | getAccount, dailyCheckin, getCheckinStatus, listProducts, getProduct, exchangeProduct, listMyOrders, listMyTransactions, listOfflineEvents, registerEvent | PointsAccount, PointsProduct, etc. |
| alert.ts | 1 | listPatientAlerts | Alert |
| article.ts | 4 | listArticles, getArticleDetail, getPublicArticleDetail, listCategories | Article, ArticleCategory |
| followup.ts | 4 | listTasks, getTaskDetail, submitRecord, listRecords | FollowUpTask, FollowUpRecord |
| dialysis.ts | 4 | listDialysisRecords, getDialysisRecord, listDialysisPrescriptions, getDialysisPrescription | DialysisRecord, DialysisPrescription |
| health-record.ts | 2 | listHealthRecords, listDiagnoses | HealthRecord, Diagnosis |
| consent.ts | 3 | listConsents, grantConsent, revokeConsent | Consent |
| medication-reminder.ts | 4 | listReminders, createReminder, updateReminder, deleteReminder | MedicationReminder |
| device-sync.ts | 3 | uploadReadings, queryDeviceReadings, queryHourlyReadings | BatchReadingRequest, BatchResult |
| action-inbox.ts | 2 | listActionItems, getActionThread | ActionItem, ThreadResponse |
| notification.ts | 4 | list, markRead, markAllRead, getUnreadCount | (anonymous) |
| auth.ts | 4 | credentialLogin, wechatLogin, wechatBindPhone, getPatients | UserInfo, LoginResp, PatientInfo |
| ai-chat.ts | 2 | sendAiMessage, (getLocalHistory, saveLocalHistory) | AiChatMessage, AiChatResponse |
| ai-analysis.ts | 3 | listAiAnalysis, getAiAnalysisDetail, listPendingSuggestions | AiAnalysisItem, AiSuggestionItem |
| analytics.ts | 3 | trackEvent, trackPageView, flushEvents | (internal) |
| doctor.ts | re-export | 8 sub-modules | (delegated) |
**Total: 43 service files, ~80 API functions, ~45 TypeScript interfaces**
## 9. Conclusion
The mini-program API service layer is well-aligned with the backend, with most contracts matching correctly. The 7 identified issues are concentrated in three areas: Appointment (3 issues), Patient (2 issues), and Consultation (1 issue), plus one field naming issue in DoctorSchedule. No CRITICAL issues were found -- all API paths are valid, response envelopes are consistent (`{success, data, message}`), and field types align correctly where they exist. The 3 HIGH issues represent missing fields that cause silent data loss or always-null UI elements, and should be addressed in the next iteration.