HTTP API for clinic-backend. The server listens on :8080 and speaks JSON. All
request and response bodies are UTF-8 JSON. Routes are registered in
internal/app/routes.go; this document is the source of truth for their
externally visible behavior.
In deployment the admin frontend proxies /api, /login, and /logout to the
backend, so browser traffic is same-origin and no CORS handling exists.
BASE=http://127.0.0.1:8080
# Admin/staff examples assume cookies.txt holds the cookies set by GET /login.
CSRF='<value of the csrf_token cookie>'
# Client examples use the API-key signature described below.
DATE=$(date -u +"%a, %d %b %Y %H:%M:%S GMT")
USERNAME=20230001
KEY=$(printf '%s' "${CLINIC_API_KEY}${USERNAME}${DATE}" | md5sum | cut -d' ' -f1)There are two independent authentication schemes. Every endpoint below states
which one it uses; /healthz, /login, /logout, and
GET /api/announcement/toc are public.
Admin/staff endpoints accept either:
- The
sessionidcookie (name configurable) created byGET /login. This is the browser flow through university CAS. Authorization: Bearer <keycloak-jwt>when Keycloak is configured.
Cookie-authenticated requests that mutate data (POST, PUT, PATCH,
DELETE) must also send:
X-CSRF-Token: <value of the csrf_token cookie>Missing or mismatched CSRF tokens receive 403 Forbidden. Bearer-token requests
do not need a CSRF token.
Staff and admin are separate authorization levels:
- Staff endpoints (
RequireStaff) accept rolestafforadmin. - Admin endpoints (
RequireAdmin) accept roleadminonly.
A session whose staff version differs from the configured STAFF_VERSION is
rejected with 401 and must log in again.
Sessions are created by GET /login, which redirects through CAS and sets both
cookies. GET /logout destroys the local session and redirects through CAS
logout.
Customer-facing endpoints under /api/ and /api/wechat/ (except the public
/api/announcement/toc) use a signature shared with the DingTalk proxy:
Date: <RFC1123 timestamp, e.g. Mon, 02 Nov 2026 10:00:00 GMT>
X-API-KEY: md5(CLINIC_API_KEY + username + Date)
?username=<job number of the logged-in customer>- All three values are required; missing ones receive
401. - The
Dateheader must be within 5 minutes of server time. - The signature is MD5 hex of the concatenation, compared in constant time.
usernameis a query parameter on every request, so it must be preserved when adding other query parameters.
The verified username becomes the record owner; it is never taken from a JSON
body.
Most errors share one shape:
{ "error": "human-readable message" }Customer ticket endpoints instead use {"msg": "..."} for booking validation
failures on 400, matching the old mini-program protocol (messages are
Chinese, e.g. 该日期诊所停止营业). Request-body binding failures use
{"error": "..."} even on ticket endpoints.
| Status | Meaning |
|---|---|
400 Bad Request |
Malformed JSON, missing/invalid field, invalid id/date range, or invalid record state transition |
401 Unauthorized |
Missing/invalid session, bearer token, or API-key signature |
403 Forbidden |
Authenticated but not allowed (wrong role, missing CSRF token, ticket owned by someone else) |
404 Not Found |
Referenced resource does not exist (or a staff user cannot see a disabled schedule) |
409 Conflict |
Uniqueness/in-use conflict, second enabled schedule, duplicate TOS, revert unavailable/expired/superseded |
500 Internal Server Error |
Unexpected server or database error |
502 Bad Gateway |
CAS ticket validation could not reach the CAS server |
503 Service Unavailable |
CAS is not configured, or GET /healthz cannot reach Redis |
-
Pagination uses
page(default1) andpageSize(default20). Admin record listings cappageSizeat100. Invalid or non-positive values fall back to defaults. -
Paginated responses look like:
{ "items": [], "total": 0, "page": 1, "pageSize": 20 }Staff lists and work-schedule assignment lists return only
{ "items": [...] }. -
IDs are positive integers. Invalid ids return
400. -
Dates and times:
- Appointment dates in record/ticket views are
YYYY-MM-DD. - Request timestamps for service dates are RFC3339
(
2026-11-01T09:00:00Z). Announcements and work schedules also accept date-onlyYYYY-MM-DDwhere noted. - Announcement
createdTime,lastEditedTime,expireDate, service datedate/startTime/endTime, work schedulestart_date/end_dateand weekday times are RFC3339 in responses. - Day boundaries use
CLINIC_TIMEZONE(defaultUTC+8). Legacy/api/date/*responses are always formatted in UTC+8.
- Appointment dates in record/ticket views are
-
Change counters: successful data-changing requests bump a per-group Redis counter that clients poll (see Sync). A counter failure never fails the write.
Description: Liveness/readiness check. Public. Pings Redis on every call.
Returns 503 when Redis is unreachable.
Request: No body, no parameters.
Response 200 OK
{ "status": "ok", "redis": "up" }Response 503 Service Unavailable
{ "status": "unavailable", "redis": "down" }curl
curl "$BASE/healthz"Description: Starts or completes CAS login. Without a ticket it redirects
(302) to the CAS login URL. With a ticket it validates the ticket, maps CAS
groups to a staff/admin role, upserts the staff row and work years, creates a
session, sets the sessionid (HttpOnly) and csrf_token cookies, then
redirects to next. Login also bumps the staff change counter.
Query parameters
| Field | Type | Required | Description |
|---|---|---|---|
next |
string | no | Same-origin path to return to after login. Must start with / and not //; otherwise CAS_DEFAULT_REDIRECT is used. |
ticket |
string | no | CAS service ticket; provided by the CAS redirect, not by callers. |
Response 302 Found with a Location header (CAS login page, or next).
Error responses: 403 (invalid ticket, no qualifying groups), 500
(failed to resolve staff or create session), 502 (CAS unreachable), 503
(CAS not configured).
curl
curl -i "$BASE/login?next=/"Description: Deletes the local session if present, clears both cookies, and
redirects (302) through CAS logout (or straight to next when CAS is not
configured). Public.
Query parameters
| Field | Type | Required | Description |
|---|---|---|---|
next |
string | no | Same-origin path to return to. Validated like /login. |
Response 302 Found
curl
curl -i "$BASE/logout?next=/"Description: Returns the currently authenticated staff member. Accepts a session cookie or a Keycloak bearer token.
Request: No body, no parameters.
Response 200 OK
| Field | Type | Description |
|---|---|---|
id |
integer | Staff id. |
account_id |
string | CAS/Keycloak account id. |
realname |
string | Display name. |
email |
string | Email address. |
role |
string | staff or admin ("" if unknown). |
{
"id": 1,
"account_id": "20230001",
"realname": "张三",
"email": "zhangsan@example.com",
"role": "staff"
}Error responses: 401 (not authenticated), 500 (session error).
curl
curl "$BASE/api/user" -b cookies.txtDescription: Like /api/user, but authentication is optional and the
endpoint always returns 200. Anonymous callers receive a zero-valued user
(id is 0), which is how the frontend detects "not logged in".
Response 200 OK — same shape as /api/user; anonymous:
{ "id": 0, "account_id": "", "realname": "", "email": "", "role": "" }Error responses: none expected (500 only on unexpected errors).
curl
curl "$BASE/api/users/me" -b cookies.txtAnnouncement object:
| Field | Type | Description |
|---|---|---|
id |
integer | Announcement id. |
title |
string | Title, max 20 characters. |
content |
string | Body text. |
tag |
string | normal, pinned, or tos. |
enabled |
boolean | Staff-facing on/off switch. |
brief |
string | Short summary, max 64 characters. |
createdTime |
string | RFC3339 creation time. |
lastEditedTime |
string | RFC3339 last-edit time. |
expireDate |
string | RFC3339 expiration date. tos announcements are never filtered out by expiry. |
priority |
integer | Higher sorts first. |
Listing order is priority DESC, created_time DESC. Mutations bump the
announcement change counter.
Description: Lists announcements. Staff and admin.
Query parameters
| Field | Type | Required | Description |
|---|---|---|---|
tag |
string | no | Exact tag filter (normal, pinned, tos). |
active |
string | no | true returns only enabled announcements that are tos or not expired. |
page |
integer | no | Default 1. |
pageSize |
integer | no | Default 20. |
Response 200 OK — paginated announcement objects.
Error responses: 401 (not authenticated), 500.
curl
curl "$BASE/api/admin/announcements?page=1&pageSize=20" -b cookies.txtDescription: Returns one announcement. Staff and admin.
Request: No body. id is a positive integer.
Response 200 OK — announcement object.
Error responses: 400 (invalid id), 401, 404.
curl
curl "$BASE/api/admin/announcements/1" -b cookies.txtDescription: Creates an announcement. Admin only. Bumps the announcement
counter.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
title |
string | yes | Max 20 characters. |
content |
string | yes | Body text. |
tag |
string | no | normal (default), pinned, or tos. Only one tos may exist. |
enabled |
boolean | no | Defaults to true. |
brief |
string | yes | Max 64 characters. |
expireDate |
string | yes | YYYY-MM-DD or RFC3339; must not be in the past. |
priority |
integer | no | Defaults to 0. |
Response 201 Created — created announcement object.
Error responses: 400 (bad body/date), 401, 403 (admin only / CSRF),
409 (a tos announcement already exists).
curl
curl -X POST "$BASE/api/admin/announcements" \
-b cookies.txt -H "X-CSRF-Token: $CSRF" \
-H 'Content-Type: application/json' \
-d '{"title":"国庆值班","content":"10 月 1 日暂停服务。","tag":"pinned","enabled":true,"brief":"假期安排","expireDate":"2026-12-31","priority":10}'Description: Updates an announcement. Admin only. Omitted fields are left
unchanged; bumps the announcement counter.
Request body — all fields optional, same types and constraints as create.
expireDate may be YYYY-MM-DD or RFC3339 and must not be in the past.
Response 200 OK — updated announcement object.
Error responses: 400, 401, 403, 404, 409 (second tos).
curl
curl -X PUT "$BASE/api/admin/announcements/1" \
-b cookies.txt -H "X-CSRF-Token: $CSRF" \
-H 'Content-Type: application/json' \
-d '{"enabled":false,"priority":0}'Description: Deletes an announcement. Admin only. Bumps the announcement
counter.
Response 204 No Content
Error responses: 400 (invalid id), 401, 403, 404.
curl
curl -X DELETE "$BASE/api/admin/announcements/1" \
-b cookies.txt -H "X-CSRF-Token: $CSRF"Service date object:
| Field | Type | Description |
|---|---|---|
id |
integer | Service date id. |
capacity |
integer | Maximum ticketed appointments. |
room_id |
integer | null | Owning room. |
date |
string | RFC3339 date. |
startTime |
string | RFC3339 service window start. |
endTime |
string | RFC3339 service window end. |
title |
string | Short label, max 20 characters. |
count |
integer | Ticketed bookings currently counted against capacity. Populated by read endpoints; create/update responses return 0. |
Mutations bump the service_date counter.
Description: Lists upcoming service dates. Staff and admin. When neither
from nor to is provided the listing starts at today; an explicit range takes
precedence so past dates can be selected.
Query parameters
| Field | Type | Required | Description |
|---|---|---|---|
room_id |
integer | no | Exact room filter. |
room_ids |
string | no | Comma-separated room ids; takes precedence over room_id. |
from |
string | no | RFC3339 inclusive lower bound. |
to |
string | no | RFC3339 inclusive upper bound; must not precede from. |
available |
string | no | true returns only dates with count < capacity. |
page |
integer | no | Default 1. |
pageSize |
integer | no | Default 20. |
Response 200 OK — paginated service date objects, ordered by date and
start time ascending.
Error responses: 400 (invalid room id or timestamp, to before from),
401, 403, 500.
curl
curl "$BASE/api/admin/service-dates?room_id=1&page=1" -b cookies.txtDescription: Lists all service dates with no "today onward" restriction. Staff and admin. Supports the same filters as the upcoming listing.
Response 200 OK — paginated service date objects.
Error responses: 400, 401, 403, 500.
curl
curl "$BASE/api/admin/service-dates/all?available=true" -b cookies.txtDescription: Returns one service date with its booked count. Staff and
admin.
Response 200 OK — service date object.
Error responses: 400 (invalid id), 401, 404.
curl
curl "$BASE/api/admin/service-dates/1" -b cookies.txtDescription: Creates a service date. Admin only. Bumps the service_date
counter.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
capacity |
integer | yes | Must be greater than zero. |
room_id |
integer | yes | Must reference an existing room. |
date |
string | yes | RFC3339 timestamp; the calendar day is used. Must not be in the past. |
startTime |
string | yes | RFC3339; must be before endTime. |
endTime |
string | yes | RFC3339; must fall on the same day as startTime. |
title |
string | yes | Max 20 characters. |
{
"capacity": 20,
"room_id": 1,
"date": "2026-11-02T00:00:00Z",
"startTime": "2026-11-02T18:30:00Z",
"endTime": "2026-11-02T21:00:00Z",
"title": "周一晚"
}Response 201 Created — service date object.
Error responses: 400 (bad body, past date, inverted window, room missing),
401, 403, 500.
curl
curl -X POST "$BASE/api/admin/service-dates" \
-b cookies.txt -H "X-CSRF-Token: $CSRF" \
-H 'Content-Type: application/json' \
-d '{"capacity":20,"room_id":1,"date":"2026-11-02T00:00:00Z","startTime":"2026-11-02T18:30:00Z","endTime":"2026-11-02T21:00:00Z","title":"周一晚"}'Description: Updates a service date. Admin only. Omitted fields stay
unchanged. The date/time window is validated only when date, startTime, and
endTime are all present. Bumps the service_date counter.
Request body — all fields optional (same types/constraints as create).
Response 200 OK — updated service date object.
Error responses: 400 (invalid window or room), 401, 403, 404,
409 (room/date changed while records exist, or capacity below current booked
count).
curl
curl -X PUT "$BASE/api/admin/service-dates/1" \
-b cookies.txt -H "X-CSRF-Token: $CSRF" \
-H 'Content-Type: application/json' \
-d '{"capacity":25}'Description: Deletes a service date. Admin only. Bumps the service_date
counter.
Response 204 No Content
Error responses: 400, 401, 403, 404, 409 (records exist for that
room and date).
curl
curl -X DELETE "$BASE/api/admin/service-dates/1" \
-b cookies.txt -H "X-CSRF-Token: $CSRF"Room object:
| Field | Type | Description |
|---|---|---|
id |
integer | Room id. |
name |
string | Unique room/campus name, max 64 characters. |
address |
string | Address text, max 256 characters. |
enabled |
boolean | Disabled rooms are not bookable. |
Mutations bump the room counter.
Description: Lists rooms. Staff and admin.
Query parameters
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | no | Case-sensitive substring match. |
enabled |
string | no | true returns only enabled rooms. |
page |
integer | no | Default 1. |
pageSize |
integer | no | Default 20. |
Response 200 OK — paginated room objects, ordered by id ascending.
Error responses: 401, 500.
curl
curl "$BASE/api/admin/rooms?enabled=true" -b cookies.txtDescription: Returns one room. Staff and admin.
Response 200 OK — room object.
Error responses: 400, 401, 404.
curl
curl "$BASE/api/admin/rooms/1" -b cookies.txtDescription: Creates a room. Admin only. Bumps the room counter.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | yes | Max 64, unique. |
address |
string | yes | Max 256. |
enabled |
boolean | no | Defaults to true. |
Response 201 Created — room object.
Error responses: 400, 401, 403, 409 (name taken).
curl
curl -X POST "$BASE/api/admin/rooms" \
-b cookies.txt -H "X-CSRF-Token: $CSRF" \
-H 'Content-Type: application/json' \
-d '{"name":"北区医务室","address":"北区 3 号楼 101","enabled":true}'Description: Updates a room. Admin only. Disabling is refused while the room
has unfinished tickets. Bumps the room counter.
Request body — name, address, enabled, all optional.
Response 200 OK — updated room object.
Error responses: 400, 401, 403, 404, 409 (name taken, or
unfinished tickets when disabling).
curl
curl -X PUT "$BASE/api/admin/rooms/1" \
-b cookies.txt -H "X-CSRF-Token: $CSRF" \
-H 'Content-Type: application/json' \
-d '{"address":"北区 3 号楼 102"}'Description: Deletes a room. Admin only. Refused while unfinished tickets,
service dates, or records reference it. Bumps the room counter.
Response 204 No Content
Error responses: 400, 401, 403, 404, 409 (unfinished tickets or
room in use).
curl
curl -X DELETE "$BASE/api/admin/rooms/1" \
-b cookies.txt -H "X-CSRF-Token: $CSRF"Record lifecycle states:
| Value | Meaning |
|---|---|
pending |
Submitted, awaiting approval |
confirmed |
Approved |
arrived |
Customer checked in |
in_progress |
Repair in progress |
completed |
Repair finished |
rejected |
Rejected |
referred |
Referred elsewhere |
no_show |
Customer did not arrive |
Admin record view:
| Field | Type | Description |
|---|---|---|
id |
integer | Record id. |
user |
string | Customer username (job number). |
realname |
string | Customer name. |
phone_num |
string | Phone number. |
status |
string | One of the lifecycle states above. |
appointment_time |
string | YYYY-MM-DD. |
description |
string | Customer problem description. |
campus |
string | Room name. |
worker_desc |
string | Staff repair notes. |
photo_count |
integer | Number of ordered photos attached to the worker notes. |
reject_reason |
string | Rejection reason, if rejected. |
referral_reason |
string | Referral reason, if referred. |
model |
string | Laptop model. |
password |
string | Device password supplied by the customer. |
tag |
string | Record tag title (e.g. no_pending for untagged). |
liquid_metal_status |
string | unknown, liquid_metal, to_verify, or non_liquid_metal. |
arrive_time |
string | RFC3339; omitted until arrival. |
finish_time |
string | RFC3339; omitted until completion. |
worker_id |
integer | Staff id of the assigned worker; omitted when unset. |
worker_realname |
string | Staff realname of the assigned worker; omitted when unset or unknown. |
approver_id |
integer | Staff id of the approver; omitted when unset. |
approver_realname |
string | Staff realname of the approver; omitted when unset or unknown. |
revertible |
boolean | Whether the caller can undo their latest action now. |
revertible_until |
string | RFC3339 deadline for that revert; omitted when not revertible. |
revertible_count |
integer | Number of the caller's in-window actions on this record. |
last_action_at |
string | RFC3339 time of the caller's latest action; omitted when none. |
All record lifecycle and worker_desc mutations below bump the record
counter and return the updated admin record view. Photo mutations also bump the
counter but return photo objects.
Description: Lists records for staff/admin. Newest ids first.
Query parameters
| Field | Type | Required | Description |
|---|---|---|---|
status |
string | no | Exact lifecycle state. Unknown values yield an empty page. |
realname |
string | no | Case-insensitive substring match. |
room_id |
integer | no | Exact room filter; non-numeric values are ignored. |
from_date |
string | no | YYYY-MM-DD inclusive lower bound on appointment date. |
to_date |
string | no | YYYY-MM-DD inclusive upper bound. |
page |
integer | no | Default 1. |
pageSize |
integer | no | Default 20, capped at 100. |
Response 200 OK — paginated admin record views, each with the caller's
revert metadata.
Error responses: 401, 403, 500.
curl
curl "$BASE/api/admin/records?status=pending&page=1&pageSize=50" -b cookies.txtDescription: Returns one record including revert metadata for the caller.
Response 200 OK — admin record view.
Error responses: 400, 401, 404.
curl
curl "$BASE/api/admin/records/1" -b cookies.txtDescription: Updates staff-maintained record fields. Currently only
worker_desc. Providing it creates/updates the worker side row and records a
revertible update action. A JSON object body is required.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
worker_desc |
string | no | Staff repair notes. |
Response 200 OK — admin record view.
Error responses: 400, 401, 403, 404.
curl
curl -X PUT "$BASE/api/admin/records/1" \
-b cookies.txt -H "X-CSRF-Token: $CSRF" \
-H 'Content-Type: application/json' \
-d '{"worker_desc":"已更换风扇并清灰。"}'Worker-note photos are ordered. position is 1-based and contiguous:
- A new photo appended with
POSTgetsposition = count + 1. - Replacing a photo with
PUTkeeps its position. - Deleting a photo shifts every later photo down by one.
GETalways returns photos in ascending position order.
The admin record view reports the current number of photos as photo_count.
Photo object:
| Field | Type | Description |
|---|---|---|
id |
integer | Stable photo id. |
position |
integer | 1-based position in the record's ordered photo list. |
content_type |
string | image/jpeg, image/png, or image/webp. |
size |
integer | Object size in bytes. |
created_at |
string | RFC3339 creation time. |
url |
string | Backend URL for the image bytes. |
Description: Lists a record's photos in position order, together with the active limits. Staff or admin only.
Response 200 OK
{
"items": [
{
"id": 12,
"position": 1,
"content_type": "image/jpeg",
"size": 204800,
"created_at": "2026-10-27T12:00:00Z",
"url": "/api/admin/records/7/photos/1"
}
],
"count": 1,
"max_count": 9,
"max_size_bytes": 10485760
}A record with no photos returns 200 with items: [] and count: 0.
Error responses: 400 (invalid id), 401, 403, 404.
curl
curl "$BASE/api/admin/records/7/photos" -b cookies.txtDescription: Streams one photo's bytes through the backend. Staff or admin only. The S3 bucket remains private.
Response 200 OK — the image body, not JSON. Headers include:
Content-Type: image/jpeg
Content-Length: 204800
ETag: "storage-etag"
Cache-Control: private, no-cache
X-Content-Type-Options: nosniffError responses: 400 (invalid id/position), 401, 403, 404, 500.
curl
curl "$BASE/api/admin/records/7/photos/1" -b cookies.txt -o photo.jpgDescription: Inserts one photo at the end of the record's ordered photo
list. Staff or admin only. Bumps the record counter.
Request body — multipart/form-data
| Field | Type | Required | Description |
|---|---|---|---|
photo |
file | yes | One JPEG, PNG, or WebP image. |
The server detects the image type from the bytes, so the part's declared
Content-Type is ignored. The request and file are limited by
RECORD_PHOTO_MAX_SIZE_BYTES; the record is limited by
RECORD_PHOTO_MAX_COUNT.
Response 201 Created — the new photo object. A Location header points at
its position URL.
Error responses: 400 (invalid id, missing/empty file, more than one file,
unsupported type), 401, 403, 404 (record missing), 409 (photo limit
reached), 413 (file or request body too large), 500.
curl
curl -X POST "$BASE/api/admin/records/7/photos" \
-b cookies.txt -H "X-CSRF-Token: $CSRF" \
-F 'photo=@repair.jpg'Description: Reorders a record's existing photos without changing their
image content, object keys, or stable ids. Staff or admin only. Bumps the
record synchronization counter.
Every photo currently attached to the record must appear exactly once in both
lists. expected_photo_ids is the client's snapshot of the current order; if
another request changed the order, the backend returns 409 and changes
nothing. Updates are atomic.
Request body — application/json
| Field | Type | Required | Description |
|---|---|---|---|
expected_photo_ids |
integer[] | yes | Current ordered photo ids, as last seen by the client. |
photo_ids |
integer[] | yes | Complete desired order of the same photo ids. |
{
"expected_photo_ids": [11, 12, 13],
"photo_ids": [13, 11, 12]
}Response 200 OK — the authoritative ordered photo list, in the same shape
as GET /api/admin/records/{id}/photos.
Error responses: 400 (invalid id or duplicate/missing/foreign photo ids),
401, 403, 404 (record missing), 409 (the current order differs from
expected_photo_ids), 500.
curl
curl -X PUT "$BASE/api/admin/records/7/photos/order" \
-b cookies.txt -H "X-CSRF-Token: $CSRF" \
-H 'Content-Type: application/json' \
-d '{"expected_photo_ids":[11,12,13],"photo_ids":[13,11,12]}'Description: Replaces the photo at an existing position without changing
its position. Staff or admin only. Bumps the record counter.
The admin frontend no longer uses this endpoint. Photo replacement was removed in favor of deletion plus ordering. It remains available for API compatibility.
Request body — same multipart/form-data photo field as POST.
Response 200 OK — the updated photo object.
Error responses: 400 (invalid id/position or invalid file), 401, 403,
404 (record or position missing), 413 (file too large), 500.
curl
curl -X PUT "$BASE/api/admin/records/7/photos/1" \
-b cookies.txt -H "X-CSRF-Token: $CSRF" \
-F 'photo=@repair-updated.jpg'Description: Removes the photo at a position and shifts every later photo
down by one. Staff or admin only. Bumps the record counter.
Response 204 No Content
Error responses: 400 (invalid id/position), 401, 403, 404 (record or
position missing), 500.
curl
curl -X DELETE "$BASE/api/admin/records/7/photos/1" \
-b cookies.txt -H "X-CSRF-Token: $CSRF"Description: Approves a pending record (pending → confirmed). Admin only.
Sets approver_id.
Request: No body.
Response 200 OK — admin record view.
Error responses: 400 (invalid id or invalid transition), 401, 403,
404.
curl
curl -X POST "$BASE/api/admin/records/1/confirm" \
-b cookies.txt -H "X-CSRF-Token: $CSRF"Description: Rejects a pending record (pending → rejected). Admin only.
Stores the rejection reason.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
reason |
string | yes | Non-empty rejection reason. |
Response 200 OK — admin record view.
Error responses: 400 (missing reason / invalid transition), 401, 403,
404.
curl
curl -X POST "$BASE/api/admin/records/1/reject" \
-b cookies.txt -H "X-CSRF-Token: $CSRF" \
-H 'Content-Type: application/json' \
-d '{"reason":"预约时间不在服务时段"}'Description: Marks a confirmed record as arrived (confirmed → arrived).
Staff or admin. Records the arrival time.
Request: No body.
Response 200 OK — admin record view.
Error responses: 400 (invalid id/transition), 401, 403, 404.
curl
curl -X POST "$BASE/api/admin/records/1/arrive" \
-b cookies.txt -H "X-CSRF-Token: $CSRF"Description: Starts repair (confirmed or arrived → in_progress). Staff
or admin. Assigns the acting staff member as the worker.
Request: No body.
Response 200 OK — admin record view.
Error responses: 400, 401, 403, 404.
curl
curl -X POST "$BASE/api/admin/records/1/in-progress" \
-b cookies.txt -H "X-CSRF-Token: $CSRF"Description: Completes repair (in_progress → completed). Staff or admin.
Sets finish_time.
Request: No body.
Response 200 OK — admin record view.
Error responses: 400, 401, 403, 404.
curl
curl -X POST "$BASE/api/admin/records/1/complete" \
-b cookies.txt -H "X-CSRF-Token: $CSRF"Description: Refers a record elsewhere. Allowed for any non-final state
(pending, confirmed, arrived, in_progress) → referred. Staff or admin.
Stores the referral reason. A JSON object body is required; reason may be
omitted or empty.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
reason |
string | no | Referral reason. |
Response 200 OK — admin record view.
Error responses: 400 (invalid id/transition), 401, 403, 404.
curl
curl -X POST "$BASE/api/admin/records/1/refer" \
-b cookies.txt -H "X-CSRF-Token: $CSRF" \
-H 'Content-Type: application/json' \
-d '{"reason":"需要主板级维修,建议返厂"}'Description: Marks a confirmed record as a no-show (confirmed →
no_show). Staff or admin.
Request: No body.
Response 200 OK — admin record view.
Error responses: 400, 401, 403, 404.
curl
curl -X POST "$BASE/api/admin/records/1/no-show" \
-b cookies.txt -H "X-CSRF-Token: $CSRF"Description: Undoes the caller's most recent reversible action on a record,
restoring the previous snapshot. Staff or admin; each staff member can only
revert their own actions. Reverts are strict LIFO and only valid inside
RECORD_ACTION_WINDOW (default 5m). Reverting is also refused when the record
has changed since the action.
Response 200 OK — admin record view after the restore.
Error responses: 400 (invalid id), 401, 403 (wrong role), 404,
409 (no revertible action, revert window expired, or
action has been superseded).
curl
curl -X POST "$BASE/api/admin/records/1/revert" \
-b cookies.txt -H "X-CSRF-Token: $CSRF"Staff object:
| Field | Type | Description |
|---|---|---|
id |
integer | Staff id. |
account_id |
string | CAS/Keycloak account id, unique. |
realname |
string | Display name. |
email |
string | Email address. |
role |
string | staff, admin, or "". |
version |
integer | Login version; compared with STAFF_VERSION. |
Staff list item adds:
| Field | Type | Description |
|---|---|---|
handled_count |
integer | Number of records this staff member has worked on. |
work_years |
integer[] | Work years recorded at login, newest first. |
All staff endpoints are admin only. Mutations bump the staff counter.
Description: Lists staff, ordered by id ascending.
Response 200 OK
{
"items": [
{
"id": 1,
"account_id": "20230001",
"realname": "张三",
"email": "zhangsan@example.com",
"role": "staff",
"handled_count": 12,
"work_years": [2026, 2025]
}
]
}Error responses: 401, 403, 500.
curl
curl "$BASE/api/admin/staff" -b cookies.txtDescription: Returns one staff row (no handled_count/work_years).
Response 200 OK — staff object.
Error responses: 400, 401, 403, 404.
curl
curl "$BASE/api/admin/staff/1" -b cookies.txtDescription: Creates a staff row, or returns the existing one for
account_id (updating real name/email when supplied).
Request body
| Field | Type | Required | Description |
|---|---|---|---|
account_id |
string | yes | Account id. |
realname |
string | no | Display name. |
email |
string | no | Email address; when non-empty it is written. |
Response 201 Created — staff object.
Error responses: 400, 401, 403, 500.
curl
curl -X POST "$BASE/api/admin/staff" \
-b cookies.txt -H "X-CSRF-Token: $CSRF" \
-H 'Content-Type: application/json' \
-d '{"account_id":"20230002","realname":"李四","email":"lisi@example.com"}'Description: Updates real name/email.
Request body — realname and email, both optional.
Response 200 OK — staff object.
Error responses: 400, 401, 403, 404.
curl
curl -X PUT "$BASE/api/admin/staff/1" \
-b cookies.txt -H "X-CSRF-Token: $CSRF" \
-H 'Content-Type: application/json' \
-d '{"realname":"张三(新)"}'Description: Deletes a staff row.
Response 204 No Content
Error responses: 400, 401, 403, 404.
curl
curl -X DELETE "$BASE/api/admin/staff/1" \
-b cookies.txt -H "X-CSRF-Token: $CSRF"Work schedule object:
| Field | Type | Description |
|---|---|---|
id |
integer | Schedule id. |
name |
string | Unique name, max 128 characters. |
start_date |
string | RFC3339 first day. |
end_date |
string | RFC3339 last day. |
enabled |
boolean | At most one schedule may be enabled. |
weekdays |
array | Weekly slots; omitted from list and create responses, present on get/update. |
Weekday slot:
| Field | Type | Description |
|---|---|---|
id |
integer | Slot id. |
work_schedule_id |
integer | Owning schedule. |
weekday |
integer | 0 = Sunday … 6 = Saturday (Go weekday numbering). |
start_time |
string | RFC3339 time on 1970-01-01. |
end_time |
string | RFC3339 time on 1970-01-01. |
room_id |
integer | Room for the slot. |
room |
object | Embedded room; omitted when not preloaded. |
staff |
array | Staff assignments; omitted when empty. |
Assignment object:
| Field | Type | Description |
|---|---|---|
id |
integer | Assignment id. |
weekday_id |
integer | Slot id. |
staff_id |
integer | Staff id. |
schedule_id |
integer | Schedule id. |
staff |
object | Embedded staff; omitted when not preloaded. |
Mutations bump the work_schedule counter. Only GET /api/admin/work-schedules
and GET /api/admin/work-schedules/{id} are readable by staff; every other
endpoint in this section (including /all, /service-availability, staff
assignments, and weekday edits) is admin only.
Description: Lists enabled schedules. Staff and admin. Newest data is
shared by all staff through the work_schedule counter.
Query parameters: page (default 1), pageSize (default 20).
Response 200 OK — paginated schedules (without weekdays), ordered by id
ascending.
Error responses: 401, 403, 500.
curl
curl "$BASE/api/admin/work-schedules" -b cookies.txtDescription: Lists all schedules, enabled or not. Admin only.
Query parameters
| Field | Type | Required | Description |
|---|---|---|---|
enabled |
boolean | no | Filter by enabled state. |
page |
integer | no | Default 1. |
pageSize |
integer | no | Default 20. |
Response 200 OK — paginated schedules (without weekdays).
Error responses: 401, 403, 500.
curl
curl "$BASE/api/admin/work-schedules/all?enabled=false" -b cookies.txtDescription: Reports, for every date in a range, whether the enabled schedule has staff assigned in each requested room. Admin only.
Query parameters
| Field | Type | Required | Description |
|---|---|---|---|
from |
string | yes | RFC3339 start date (inclusive). |
to |
string | yes | RFC3339 end date (inclusive); must not precede from. |
room_ids |
string | yes | Comma-separated room ids. |
Response 200 OK
| Field | Type | Description |
|---|---|---|
items[].room_id |
integer | Room id. |
items[].date |
string | YYYY-MM-DD. |
items[].available |
boolean | Date is inside the schedule period and the weekday has staff for the room. |
{
"items": [
{ "room_id": 1, "date": "2026-11-02", "available": true },
{ "room_id": 1, "date": "2026-11-03", "available": false }
]
}Error responses: 400 (missing/invalid parameters), 401, 403, 404
(no enabled schedule).
curl
curl "$BASE/api/admin/work-schedules/service-availability?from=2026-11-02T00:00:00Z&to=2026-11-08T00:00:00Z&room_ids=1,2" \
-b cookies.txtDescription: Returns one schedule with weekday slots, rooms, and assigned
staff. Staff and admin; staff receive 404 for a disabled schedule.
Response 200 OK — work schedule object including weekdays.
Error responses: 400, 401, 403, 404.
curl
curl "$BASE/api/admin/work-schedules/1" -b cookies.txtDescription: Creates a schedule with optional weekly slots. Admin only. Refuses to enable a second schedule.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | yes | Max 128, unique. |
start_date |
string | yes | YYYY-MM-DD or RFC3339; must not be after end_date. |
end_date |
string | yes | YYYY-MM-DD or RFC3339. |
enabled |
boolean | no | Defaults to false. |
weekdays |
array | no | Weekly slots. |
weekdays[].weekday |
integer | yes | 0–6 (Sunday–Saturday). |
weekdays[].start_time |
string | yes | HH:MM or HH:MM:SS; must be before end_time. |
weekdays[].end_time |
string | yes | HH:MM or HH:MM:SS. |
weekdays[].room_id |
integer | yes | Must exist. |
weekdays[].staff_ids |
integer[] | no | Staff ids; each must exist. |
Response 201 Created — work schedule object. weekdays is omitted from
this response; fetch the schedule afterwards to read the created slots.
Error responses: 400 (invalid date range/weekday/time), 401, 403,
404 (room or staff missing), 409 (name taken or another schedule already
enabled).
curl
curl -X POST "$BASE/api/admin/work-schedules" \
-b cookies.txt -H "X-CSRF-Token: $CSRF" \
-H 'Content-Type: application/json' \
-d '{"name":"2026 秋季","start_date":"2026-09-01","end_date":"2027-01-15","enabled":true,"weekdays":[{"weekday":1,"start_time":"18:30","end_time":"21:00","room_id":1,"staff_ids":[1,2]}]}'Description: Updates a schedule. Admin only. When weekdays is present it
replaces all existing slots and assignments.
Request body — all fields optional, same constraints as create.
Response 200 OK — updated schedule with weekdays.
Error responses: 400, 401, 403, 404, 409 (name taken / duplicate
enabled schedule).
curl
curl -X PUT "$BASE/api/admin/work-schedules/1" \
-b cookies.txt -H "X-CSRF-Token: $CSRF" \
-H 'Content-Type: application/json' \
-d '{"enabled":false}'Description: Deletes a schedule and its slots/assignments. Admin only.
Response 204 No Content
Error responses: 400, 401, 403, 404.
curl
curl -X DELETE "$BASE/api/admin/work-schedules/1" \
-b cookies.txt -H "X-CSRF-Token: $CSRF"Description: Lists all staff assignments of a schedule. Admin only.
Response 200 OK
{ "items": [ { "id": 1, "weekday_id": 3, "staff_id": 2, "schedule_id": 1, "staff": { "id": 2, "realname": "李四" } } ] }Error responses: 400, 401, 403, 404.
curl
curl "$BASE/api/admin/work-schedules/1/staff" -b cookies.txtDescription: Assigns a staff member to a weekly slot. Admin only. Identify
the slot by weekday_id, or by room_id + weekday; if that slot does not
exist it is created with default hours 18:30–21:00. The staff member must belong
to the schedule's start year. Assigning twice returns the existing assignment.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
staff_id |
integer | yes | Staff to assign. |
weekday_id |
integer | conditional | Existing slot id. Required unless room_id + weekday are given. |
room_id |
integer | conditional | Used with weekday to resolve/create a slot. |
weekday |
integer | conditional | 0–6. |
Response 201 Created — assignment object.
Error responses: 400 (missing slot identifiers, invalid weekday, staff not
in the schedule's work year), 401, 403, 404 (schedule/staff/slot missing).
curl
curl -X POST "$BASE/api/admin/work-schedules/1/staff" \
-b cookies.txt -H "X-CSRF-Token: $CSRF" \
-H 'Content-Type: application/json' \
-d '{"weekday_id":3,"staff_id":2}'Description: Removes a staff assignment. Admin only. A JSON object body is required.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
weekday_id |
integer | yes | Slot id. |
staff_id |
integer | yes | Staff id. |
Response 204 No Content
Error responses: 400, 401, 403, 404 (slot or assignment missing).
curl
curl -X DELETE "$BASE/api/admin/work-schedules/1/staff" \
-b cookies.txt -H "X-CSRF-Token: $CSRF" \
-H 'Content-Type: application/json' \
-d '{"weekday_id":3,"staff_id":2}'Description: Lists staff whose recorded work years include the schedule's start year. Admin only.
Response 200 OK — { "items": [<staff list item>] }.
Error responses: 400, 401, 403, 404.
curl
curl "$BASE/api/admin/work-schedules/1/valid-staff" -b cookies.txtDescription: Creates or updates one weekday slot's hours. Admin only. Existing assignments are preserved.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
room_id |
integer | yes | Room id. |
weekday |
integer | yes | 0–6. |
start_time |
string | yes | HH:MM or HH:MM:SS; before end_time. |
end_time |
string | yes | HH:MM or HH:MM:SS. |
Response 200 OK — weekday slot object.
Error responses: 400 (invalid weekday/time), 401, 403, 404 (schedule
or room missing).
curl
curl -X PUT "$BASE/api/admin/work-schedules/1/weekdays" \
-b cookies.txt -H "X-CSRF-Token: $CSRF" \
-H 'Content-Type: application/json' \
-d '{"room_id":1,"weekday":1,"start_time":"19:00","end_time":"21:30"}'The backend keeps one monotonically increasing counter per data group in Redis. Clients store the values they last saw and refetch a group when its value changes. Counters are best-effort: a Redis failure is logged and never fails the underlying write.
Groups: record, room, work_schedule, service_date, staff,
announcement.
Description: Returns the current counter for every group. Staff and admin.
Response 200 OK
{
"record": 42,
"room": 3,
"work_schedule": 5,
"service_date": 9,
"staff": 2,
"announcement": 7
}Error responses: 401, 403, 500 (Redis read failure).
curl
curl "$BASE/api/admin/sync" -b cookies.txtDescription: Reports the recommended polling interval. interval_ms is 0
when periodic polling is disabled.
Response 200 OK
{ "interval_ms": 10000 }Error responses: 401, 403.
curl
curl "$BASE/api/admin/sync/config" -b cookies.txtThese endpoints use API-key authentication. Announcement shape is the same as the admin announcement object.
Description: Lists announcements visible to customers. Pass active=true
to receive only enabled, non-expired announcements (plus the TOS entry).
Query parameters: tag, active, page, pageSize — same semantics as the
admin listing.
Response 200 OK — paginated announcement objects.
Error responses: 401, 500.
curl
curl "$BASE/api/announcements?username=$USERNAME&active=true" \
-H "Date: $DATE" -H "X-API-KEY: $KEY"Description: Returns one announcement.
Response 200 OK — announcement object.
Error responses: 400, 401, 404.
curl
curl "$BASE/api/announcements/1?username=$USERNAME" \
-H "Date: $DATE" -H "X-API-KEY: $KEY"Service date shape is the same as the admin service date object.
Description: Lists service dates. Add active=true to restrict the listing
to today onward; from/to select an explicit range.
Query parameters: active, room_id, room_ids, from, to,
available, page, pageSize — same semantics as the admin listing.
Response 200 OK — paginated service date objects, ordered by date/start
time ascending.
Error responses: 400 (invalid room/timestamp/range), 401, 500.
curl
curl "$BASE/api/service-dates?username=$USERNAME&active=true&available=true" \
-H "Date: $DATE" -H "X-API-KEY: $KEY"Description: Returns one service date with booked count.
Response 200 OK — service date object.
Error responses: 400, 401, 404.
curl
curl "$BASE/api/service-dates/1?username=$USERNAME" \
-H "Date: $DATE" -H "X-API-KEY: $KEY"Room shape is the same as the admin room object.
Description: Lists rooms (including disabled ones unless enabled=true).
Query parameters: name, enabled, page, pageSize.
Response 200 OK — paginated room objects.
Error responses: 401, 500.
curl
curl "$BASE/api/rooms?username=$USERNAME&enabled=true" \
-H "Date: $DATE" -H "X-API-KEY: $KEY"Description: Returns one room.
Response 200 OK — room object.
Error responses: 400, 401, 404.
curl
curl "$BASE/api/rooms/1?username=$USERNAME" \
-H "Date: $DATE" -H "X-API-KEY: $KEY"Ticket (customer-facing record) view:
| Field | Type | Description |
|---|---|---|
url |
string | /api/tickets/{id}. |
user |
string | Owner username from the API key. |
status |
string | pending, confirmed, arrived, in_progress, completed, rejected, referred, or no_show. |
realname |
string | Customer name. |
phone_num |
string | Phone number. |
campus |
string | Room name. |
appointment_time |
string | YYYY-MM-DD. |
description |
string | Problem description. |
worker_description |
string | Staff notes. |
model |
string | Laptop model. |
reject_reason |
string | Rejection reason. |
password |
string | Device password. |
liquid_metal_status |
string | unknown, liquid_metal, to_verify, or non_liquid_metal. |
Booking rules (enforced on create and when room/date changes):
campusmust name an existing room.- A service date must exist for that room and day.
- Ticketed bookings must be below the service date capacity.
- A customer may have at most one unfinished booking with an appointment today or later.
- The appointment date must not be in the past.
Description: Lists the caller's tickets, newest first. Active bookings whose appointment is in the past are hidden.
Query parameters: username (required for auth), page, pageSize.
Response 200 OK — paginated ticket views.
Error responses: 401, 500.
curl
curl "$BASE/api/tickets?username=$USERNAME&page=1" \
-H "Date: $DATE" -H "X-API-KEY: $KEY"Description: Creates a booking for the authenticated customer and (when the
tagger service is configured) starts asynchronous record tagging. Bumps the
record counter.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
realname |
string | yes | Customer name. |
phone_num |
string | yes | Phone number. |
campus |
string | yes | Room name. |
appointment_time |
string | yes | YYYY-MM-DD. |
description |
string | yes | Problem description. |
model |
string | no | Laptop model. |
password |
string | no | Device password. |
Response 201 Created — ticket view.
Error responses: 400 with {"msg": "..."} for booking violations
(campus 不存在, date closed, no capacity, already one unfinished booking, past
date) or invalid appointment_time; 400 with {"error": "..."} for malformed
bodies; 401, 500.
curl
curl -X POST "$BASE/api/tickets?username=$USERNAME" \
-H "Date: $DATE" -H "X-API-KEY: $KEY" \
-H 'Content-Type: application/json' \
-d '{"realname":"张三","phone_num":"13800000000","campus":"北区医务室","appointment_time":"2026-11-02","description":"风扇异响","model":"ThinkPad X1","password":"123456"}'Description: Returns the caller's single unfinished booking, if any.
Response 200 OK — { "count": 0 } when none, otherwise
{ "count": 1, "data": <ticket view> }.
Error responses: 401, 500.
curl
curl "$BASE/api/tickets/working?username=$USERNAME" \
-H "Date: $DATE" -H "X-API-KEY: $KEY"Description: Lists the caller's closed tickets (rejected, completed,
referred, no_show), newest first.
Query parameters: username, page, pageSize.
Response 200 OK — paginated ticket views.
Error responses: 401, 500.
curl
curl "$BASE/api/tickets/finished?username=$USERNAME" \
-H "Date: $DATE" -H "X-API-KEY: $KEY"Description: Returns one of the caller's tickets.
Response 200 OK — ticket view.
Error responses: 400, 401, 403 (owned by another user), 404.
curl
curl "$BASE/api/tickets/1?username=$USERNAME" \
-H "Date: $DATE" -H "X-API-KEY: $KEY"Description: Updates the caller's ticket. PUT and PATCH behave
identically. Changing campus or appointment_time re-runs the open-date,
capacity, and past-date checks. Bumps the record counter.
Request body — all fields optional:
| Field | Type | Description |
|---|---|---|
realname |
string | Customer name. |
phone_num |
string | Phone number. |
campus |
string | Room name; empty string is ignored. |
appointment_time |
string | YYYY-MM-DD. |
description |
string | Problem description. |
model |
string | Laptop model. |
password |
string | Device password. |
Response 200 OK — updated ticket view.
Error responses: 400 ({"msg": ...} for booking violations, {"error": ...} for malformed bodies), 401, 403, 404, 500.
curl
curl -X PUT "$BASE/api/tickets/1?username=$USERNAME" \
-H "Date: $DATE" -H "X-API-KEY: $KEY" \
-H 'Content-Type: application/json' \
-d '{"appointment_time":"2026-11-03"}'Description: Deletes the caller's ticket and its side rows. Refused when the
appointment's service date no longer exists. Bumps the record counter.
Response 204 No Content
Error responses: 400 ({"msg": "该日期诊所停止营业"}), 401, 403,
404.
curl
curl -X DELETE "$BASE/api/tickets/1?username=$USERNAME" \
-H "Date: $DATE" -H "X-API-KEY: $KEY"Description: Same counters as /api/admin/sync, for API-key clients.
Response 200 OK — object mapping each group to its counter.
Error responses: 401, 500.
curl
curl "$BASE/api/sync?username=$USERNAME" \
-H "Date: $DATE" -H "X-API-KEY: $KEY"Description: Same polling configuration as /api/admin/sync/config.
Response 200 OK
{ "interval_ms": 10000 }Error responses: 401.
curl
curl "$BASE/api/sync/config?username=$USERNAME" \
-H "Date: $DATE" -H "X-API-KEY: $KEY"These preserve the old Django wire format for the light-app. They use API-key
authentication and return plain arrays or DRF envelopes instead of the standard
paginated shape. Successful writes bump the record counter.
Legacy record object:
| Field | Type | Description |
|---|---|---|
url |
string | /api/wechat/{id}/. |
user |
string | Owner username. |
status |
integer | Legacy status code (see table below). |
realname |
string | Customer name. |
phone_num |
string | Phone number. |
campus |
string | Room name. |
appointment_time |
string | YYYY-MM-DD in CLINIC_TIMEZONE. |
description |
string | Problem description. |
worker_description |
string | Staff notes. |
model |
string | Laptop model. |
reject_reason |
string | Rejection reason. |
password |
string | Device password. |
Legacy status codes:
| Code | Status |
|---|---|
1 |
pending |
2 |
confirmed |
3 |
rejected |
4 |
arrived |
5 |
in_progress |
6 |
completed |
7 |
referred |
9 |
no_show |
DRF pagination envelope (fixed page size 10):
{ "count": 12, "next": "?page=2", "previous": null, "results": [] }next/previous are relative query strings, or null at the boundaries.
Description: Lists the caller's tickets with legacy records and DRF pagination.
Query parameters: username, page.
Response 200 OK — DRF page of legacy records.
Error responses: 401, 500.
curl
curl "$BASE/api/wechat/?username=$USERNAME&page=1" \
-H "Date: $DATE" -H "X-API-KEY: $KEY"Description: Creates a booking using the same body and validation rules as
POST /api/tickets.
Response 201 Created — legacy record.
Error responses: 400 ({"msg": ...} for booking violations), 401,
500.
curl
curl -X POST "$BASE/api/wechat/?username=$USERNAME" \
-H "Date: $DATE" -H "X-API-KEY: $KEY" \
-H 'Content-Type: application/json' \
-d '{"realname":"张三","phone_num":"13800000000","campus":"北区医务室","appointment_time":"2026-11-02","description":"风扇异响"}'Description: Returns the caller's unfinished booking in the legacy shape.
Response 200 OK — { "count": 0 } or
{ "count": 1, "data": <legacy record> }.
Error responses: 401, 500.
curl
curl "$BASE/api/wechat/working/?username=$USERNAME" \
-H "Date: $DATE" -H "X-API-KEY: $KEY"GET /api/wechat/finished, GET /api/wechat/finished/, GET /api/wechat/finish, GET /api/wechat/finish/
Description: Lists the caller's closed tickets in the legacy shape. All four paths are equivalent.
Query parameters: username, page.
Response 200 OK — DRF page of legacy records.
Error responses: 401, 500.
curl
curl "$BASE/api/wechat/finished/?username=$USERNAME" \
-H "Date: $DATE" -H "X-API-KEY: $KEY"Description: Returns one of the caller's tickets.
Response 200 OK — legacy record.
Error responses: 400, 401, 403, 404.
curl
curl "$BASE/api/wechat/1/?username=$USERNAME" \
-H "Date: $DATE" -H "X-API-KEY: $KEY"Description: Updates the caller's ticket using the same body and validation
rules as PUT /api/tickets/{id}.
Response 200 OK — updated legacy record.
Error responses: 400, 401, 403, 404, 500.
curl
curl -X PATCH "$BASE/api/wechat/1/?username=$USERNAME" \
-H "Date: $DATE" -H "X-API-KEY: $KEY" \
-H 'Content-Type: application/json' \
-d '{"description":"风扇异响,偶发停转"}'Description: Deletes the caller's ticket, with the same rules as
DELETE /api/tickets/{id}.
Response 204 No Content
Error responses: 400, 401, 403, 404.
curl
curl -X DELETE "$BASE/api/wechat/1/?username=$USERNAME" \
-H "Date: $DATE" -H "X-API-KEY: $KEY"Description: Lists enabled rooms as {name, address} items.
Response 200 OK
[
{ "name": "北区医务室", "address": "北区 3 号楼 101" }
]Error responses: 401, 500.
curl
curl "$BASE/api/campus/?username=$USERNAME" \
-H "Date: $DATE" -H "X-API-KEY: $KEY"Description: Lists service dates from today onward in the legacy shape, formatted in UTC+8.
Response 200 OK
| Field | Type | Description |
|---|---|---|
id |
integer | Service date id. |
title |
string | Service date title. |
date |
string | YYYY-MM-DD. |
capacity |
integer | Capacity. |
campus |
string | Room name. |
startTime |
string | HH:MM:SS. |
endTime |
string | HH:MM:SS. |
count |
integer | Ticketed bookings. |
finish |
integer | Always 0 (kept for shape compatibility). |
working |
integer | Always 0 (kept for shape compatibility). |
[
{
"id": 1,
"title": "周一晚",
"date": "2026-11-02",
"capacity": 20,
"campus": "北区医务室",
"startTime": "18:30:00",
"endTime": "21:00:00",
"count": 5,
"finish": 0,
"working": 0
}
]Error responses: 401, 500.
curl
curl "$BASE/api/date/?username=$USERNAME" \
-H "Date: $DATE" -H "X-API-KEY: $KEY"Description: Like /api/date/ but includes past service dates.
Response 200 OK — array of legacy date items.
Error responses: 401, 500.
curl
curl "$BASE/api/date/all/?username=$USERNAME" \
-H "Date: $DATE" -H "X-API-KEY: $KEY"Description: Lists active announcements in the legacy shape (enabled, not expired, plus TOS).
Response 200 OK
| Field | Type | Description |
|---|---|---|
id |
integer | Announcement id. |
title |
string | Title. |
content |
string | Body text. |
brief |
string | Short summary. |
tag |
string | normal, pinned, or tos. |
createdTime |
string | RFC3339 in CLINIC_TIMEZONE. |
lastEditedTime |
string | RFC3339 in CLINIC_TIMEZONE. |
expireDate |
string | YYYY-MM-DD in CLINIC_TIMEZONE. |
priority |
integer | Display priority. |
Error responses: 401, 500.
curl
curl "$BASE/api/announcement/?username=$USERNAME" \
-H "Date: $DATE" -H "X-API-KEY: $KEY"Description: Returns the terms-of-service announcement content. Public (no
authentication). When no TOS announcement exists, returns the fallback text
暂无公告.
Response 200 OK
{ "content": "..." }Error responses: 500.
curl
curl "$BASE/api/announcement/toc/"See CONFIGURATION.md for every environment variable and
deployment setting. Settings that affect API behavior are noted inline above:
CLINIC_API_KEY, CLINIC_TIMEZONE, STAFF_VERSION, SYNC_POLL_INTERVAL,
SESSION_*, CAS_*, and KEYCLOAK_*.