feat: base go mkiso
This commit is contained in:
235
docs/patient-api-spec.md
Normal file
235
docs/patient-api-spec.md
Normal file
@@ -0,0 +1,235 @@
|
||||
# Patient Data API — External Specification
|
||||
|
||||
## Purpose
|
||||
|
||||
The Go mkiso server needs patient data (name, medical record ID, registration ID) to generate ISO filenames. Instead of connecting directly to the HIS/PACS database, it calls this API.
|
||||
|
||||
**This API must be implemented by the HIS/PACS team** (or provided as a thin proxy in front of the existing DB).
|
||||
|
||||
---
|
||||
|
||||
## Authentication
|
||||
|
||||
Use one of:
|
||||
- `Authorization: Bearer <jwt-token>` — if the HIS exposes standard JWT
|
||||
- `X-API-Key: <key>` — simpler for internal services
|
||||
- Configurable in Go server's `config.yaml`:`patient_api.auth_header`, `patient_api.auth_token`
|
||||
|
||||
---
|
||||
|
||||
## Endpoint: Get Patient by Accession Number
|
||||
|
||||
### `GET /patient/by-accession`
|
||||
|
||||
**Query Parameters:**
|
||||
|
||||
| Parameter | Required | Type | Description |
|
||||
|-----------|----------|------|-------------|
|
||||
| `accession_number` | Yes | string | DICOM Accession Number (e.g., `MR.2024.001`) |
|
||||
|
||||
**Request:**
|
||||
```
|
||||
GET /patient/by-accession?accession_number=MR.2024.001
|
||||
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
|
||||
```
|
||||
|
||||
**Response 200 — OK:**
|
||||
|
||||
```json
|
||||
{
|
||||
"accession_number": "MR.2024.001",
|
||||
"medrec_id": "00012345",
|
||||
"reg_id": "REG-2024-00123",
|
||||
"patient_name": "JOHN DOE",
|
||||
"modality": "MR",
|
||||
"study_description": "MRI Brain"
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `accession_number` | string | Echoed back for verification |
|
||||
| `medrec_id` | string | Medical Record ID |
|
||||
| `reg_id` | string | Registration ID |
|
||||
| `patient_name` | string | Patient full name (for ISO filename) |
|
||||
| `modality` | string | DICOM Modality code (MR, CT, CR, etc.) |
|
||||
| `study_description` | string | DICOM Study Description |
|
||||
|
||||
**Response 404 — Not Found:**
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "accession_number not found",
|
||||
"accession_number": "MR.9999.XXX"
|
||||
}
|
||||
```
|
||||
|
||||
**Response 502 — Upstream Failure:**
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "upstream database unavailable"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Implementation Guide (for the HIS/PACS team)
|
||||
|
||||
### Data Source
|
||||
|
||||
The existing `mkiso_multiple.php` queries these tables directly:
|
||||
|
||||
```sql
|
||||
-- Step 1: MEDRECID + RegID from pacs_result_series
|
||||
SELECT MEDRECID, RegID
|
||||
FROM pacs_result_series
|
||||
WHERE AccessionNumber = ?
|
||||
LIMIT 1;
|
||||
|
||||
-- Step 2: Patient name from medrec
|
||||
SELECT Nama
|
||||
FROM medrec
|
||||
WHERE MEDRECID = ?;
|
||||
```
|
||||
|
||||
**The API should wrap these queries** but the Go server never touches the DB directly.
|
||||
|
||||
### Minimal Reference Implementation (PHP)
|
||||
|
||||
```php
|
||||
<?php
|
||||
// patient-api.php
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// Auth check (simple API key)
|
||||
$valid_key = getenv('PATIENT_API_KEY');
|
||||
if ($_SERVER['HTTP_X_API_KEY'] !== $valid_key) {
|
||||
http_response_code(401);
|
||||
echo json_encode(['error' => 'unauthorized']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$acc = $_GET['accession_number'] ?? '';
|
||||
if (empty($acc)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'missing accession_number']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Connect to DB (PACS DB: 192.168.2.7, pacsdb_his)
|
||||
$db = new PDO('mysql:host=192.168.2.7;dbname=pacsdb_his', 'remote', '12Digit');
|
||||
|
||||
$stmt = $db->prepare(
|
||||
"SELECT prs.MEDRECID, prs.RegID, m.Nama
|
||||
FROM pacs_result_series prs
|
||||
LEFT JOIN medrec m ON m.MEDRECID = prs.MEDRECID
|
||||
WHERE prs.AccessionNumber = ?
|
||||
LIMIT 1"
|
||||
);
|
||||
$stmt->execute([$acc]);
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$row) {
|
||||
http_response_code(404);
|
||||
echo json_encode(['error' => 'accession_number not found', 'accession_number' => $acc]);
|
||||
exit;
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'accession_number' => $acc,
|
||||
'medrec_id' => $row['MEDRECID'],
|
||||
'reg_id' => $row['RegID'],
|
||||
'patient_name' => $row['Nama'],
|
||||
'modality' => '', // Optional, can be added from another query
|
||||
'study_description' => '' // Optional
|
||||
]);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Go Server Configuration for Patient API
|
||||
|
||||
```yaml
|
||||
# config.yaml
|
||||
patient_api:
|
||||
base_url: "http://192.168.2.7:8090" # Patient API server
|
||||
endpoint: "/patient/by-accession"
|
||||
auth_type: "api_key" # "jwt" or "api_key" or "none"
|
||||
auth_header: "X-API-Key" # Header name for auth
|
||||
auth_token: "changeme-secret-key" # The API key or JWT
|
||||
timeout: 10s
|
||||
retry: 3 # Retry on network failure
|
||||
retry_backoff: 500ms
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Go Client (internal/repo/patient.go)
|
||||
|
||||
```go
|
||||
package repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
type PatientData struct {
|
||||
AccessionNumber string `json:"accession_number"`
|
||||
MedrecID string `json:"medrec_id"`
|
||||
RegID string `json:"reg_id"`
|
||||
PatientName string `json:"patient_name"`
|
||||
Modality string `json:"modality"`
|
||||
StudyDescription string `json:"study_description"`
|
||||
}
|
||||
|
||||
type PatientRepo struct {
|
||||
baseURL string
|
||||
httpClient *http.Client
|
||||
authHeader string
|
||||
authToken string
|
||||
}
|
||||
|
||||
func NewPatientRepo(baseURL, authHeader, authToken string, timeout time.Duration) *PatientRepo {
|
||||
return &PatientRepo{
|
||||
baseURL: baseURL,
|
||||
httpClient: &http.Client{Timeout: timeout},
|
||||
authHeader: authHeader,
|
||||
authToken: authToken,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *PatientRepo) ByAccessionNumber(ctx context.Context, acc string) (*PatientData, error) {
|
||||
u, _ := url.Parse(r.baseURL + "/patient/by-accession")
|
||||
u.RawQuery = url.Values{"accession_number": {acc}}.Encode()
|
||||
|
||||
req, _ := http.NewRequestWithContext(ctx, "GET", u.String(), nil)
|
||||
if r.authHeader != "" {
|
||||
req.Header.Set(r.authHeader, r.authToken)
|
||||
}
|
||||
|
||||
resp, err := r.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("patient API request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == 404 {
|
||||
return nil, fmt.Errorf("accession_number %q not found", acc)
|
||||
}
|
||||
if resp.StatusCode != 200 {
|
||||
return nil, fmt.Errorf("patient API returned %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var data PatientData
|
||||
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
|
||||
return nil, fmt.Errorf("patient API response decode: %w", err)
|
||||
}
|
||||
return &data, nil
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user