用户登录
请求参数:
{
"username": "string", // 用户名/手机号
"password": "string", // 密码
"captcha": "string" // 验证码(可选)
}响应:
{
"token": "eyJhbGciOiJIUzI1NiIs...",
"user": {
"id": 1,
"username": "admin",
"name": "管理员",
"roles": ["system_admin"]
}
}用户登出
请求头:
Authorization: Bearer {token}
响应:
{
"message": "登出成功"
}刷新 Token
请求头:
Authorization: Bearer {token}
响应:
{
"token": "eyJhbGciOiJIUzI1NiIs..."
}获取用户列表
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| page | int | 否 | 页码,默认1 |
| pageSize | int | 否 | 每页数量,默认20 |
| keyword | string | 否 | 搜索关键词 |
| role | string | 否 | 角色筛选 |
响应:
{
"list": [
{
"id": 1,
"username": "student001",
"name": "张三",
"phone": "138****5678",
"roles": ["student"],
"status": "active",
"createdAt": "2024-01-15T08:00:00Z"
}
],
"total": 100
}获取用户详情
响应:
{
"id": 1,
"username": "student001",
"name": "张三",
"phone": "13812345678",
"email": "zhangsan@example.com",
"roles": ["student"],
"student": {
"studentId": "2024001",
"major": "计算机科学",
"grade": "2024"
},
"createdAt": "2024-01-15T08:00:00Z"
}创建用户
请求参数:
{
"username": "string",
"password": "string",
"name": "string",
"phone": "string",
"email": "string",
"roles": ["string"],
"studentId": "string", // 学生必填
"major": "string", // 学生必填
"grade": "string" // 学生必填
}更新用户
请求参数:
{
"name": "string",
"phone": "string",
"email": "string",
"roles": ["string"],
"status": "active"
}删除用户
获取学生列表
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| page | int | 否 | 页码 |
| pageSize | int | 否 | 每页数量 |
| keyword | string | 否 | 搜索关键词 |
| buildingId | int | 否 | 楼栋筛选 |
| roomId | int | 否 | 房间筛选 |
响应:
{
"list": [
{
"id": 1,
"studentId": "2024001",
"name": "张三",
"gender": "male",
"major": "计算机科学",
"grade": "2024",
"room": {
"id": 101,
"building": "A栋",
"roomNumber": "101"
},
"phone": "138****5678",
"checkInDate": "2024-02-01"
}
],
"total": 500
}获取学生详情
录入学生信息
请求参数:
{
"studentId": "2024001",
"name": "张三",
"gender": "male",
"major": "计算机科学",
"grade": "2024",
"phone": "13812345678",
"idCard": "110101200001011234",
"roomId": 101
}调整宿舍
请求参数:
{
"roomId": 102,
"reason": "宿舍调整"
}获取房间列表
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| buildingId | int | 否 | 楼栋ID |
| floor | int | 否 | 楼层 |
| status | string | 否 | 状态: available/occupied/full |
响应:
{
"list": [
{
"id": 101,
"buildingId": 1,
"buildingName": "A栋",
"roomNumber": "101",
"floor": 1,
"capacity": 4,
"occupied": 3,
"status": "available",
"students": [
{ "id": 1, "name": "张三" }
]
}
],
"total": 200
}获取房间详情
创建房间
请求参数:
{
"buildingId": 1,
"roomNumber": "101",
"floor": 1,
"capacity": 4,
"type": "standard" // standard/suite/single
}获取楼栋列表
响应:
{
"list": [
{
"id": 1,
"name": "A栋",
"type": "male", // male/female/mixed
"floors": 6,
"roomsCount": 120,
"occupiedCount": 350,
"capacity": 480,
"managerId": 10,
"managerName": "李宿管"
}
]
}获取楼栋详情
创建楼栋
请求参数:
{
"name": "A栋",
"type": "male",
"floors": 6,
"roomsPerFloor": 20,
"managerId": 10
}更新楼栋信息
删除楼栋
获取报修列表
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| status | string | 否 | 状态筛选 |
| type | string | 否 | 类型筛选 |
| studentId | int | 否 | 学生ID |
| page | int | 否 | 页码 |
响应:
{
"list": [
{
"id": 1,
"title": "水龙头漏水",
"type": "plumbing",
"description": "卫生间水龙头滴水",
"images": ["url1", "url2"],
"status": "Pending",
"studentId": 1,
"studentName": "张三",
"roomNumber": "A-101",
"createdAt": "2024-03-16T10:00:00Z",
"updatedAt": "2024-03-16T10:00:00Z"
}
],
"total": 50
}获取报修详情
提交报修
请求参数:
{
"title": "水龙头漏水",
"type": "plumbing",
"description": "详细描述",
"images": ["url1", "url2"],
"urgency": "normal" // low/normal/high/urgent
}分配维修人员
请求参数:
{
"staffId": 5
}更新报修状态
请求参数:
{
"status": "Completed",
"comment": "已修复",
"images": ["url3"]
}获取公告列表
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| type | string | 否 | 类型 |
| page | int | 否 | 页码 |
响应:
{
"list": [
{
"id": 1,
"title": "关于宿舍安全检查的通知",
"content": "详细内容...",
"type": "system",
"author": "管理员",
"isTop": true,
"viewCount": 150,
"createdAt": "2024-03-15T08:00:00Z"
}
],
"total": 20
}获取公告详情
发布公告
请求参数:
{
"title": "公告标题",
"content": "公告内容",
"type": "system",
"isTop": false,
"targetRoles": ["student", "dorm_manager"]
}获取查寝记录
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| roomId | int | 否 | 房间ID |
| startDate | string | 否 | 开始日期 |
| endDate | string | 否 | 结束日期 |
响应:
{
"list": [
{
"id": 1,
"roomId": 101,
"roomNumber": "A-101",
"score": 95,
"items": {
"cleanliness": 20,
"order": 20,
"safety": 20,
"decoration": 15,
"atmosphere": 20
},
"issues": ["地面有水渍"],
"inspector": "李宿管",
"inspectionDate": "2024-03-16",
"createdAt": "2024-03-16T14:00:00Z"
}
]
}录入查寝评分
请求参数:
{
"roomId": 101,
"score": 95,
"items": {
"cleanliness": 20,
"order": 20,
"safety": 20,
"decoration": 15,
"atmosphere": 20
},
"issues": ["地面有水渍"],
"photos": ["url1"],
"inspectionDate": "2024-03-16"
}查寝排行榜
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| period | string | 否 | 周期: week/month/semester |
响应:
{
"rankings": [
{
"rank": 1,
"roomId": 101,
"roomNumber": "A-101",
"averageScore": 98.5,
"inspectionCount": 4
}
]
}获取换寝申请列表
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| status | string | 否 | 状态筛选 |
| studentId | int | 否 | 学生ID |
响应:
{
"list": [
{
"id": 1,
"studentId": 1,
"studentName": "张三",
"currentRoomId": 101,
"currentRoomNumber": "A-101",
"targetRoomId": 102,
"targetRoomNumber": "A-102",
"reason": "想和同学住一起",
"status": "Pending",
"approver": null,
"approvalComment": null,
"createdAt": "2024-03-15T10:00:00Z"
}
]
}提交换寝申请
请求参数:
{
"targetRoomId": 102,
"reason": "想和同学住一起",
"expectedDate": "2024-03-20"
}审批换寝申请
请求参数:
{
"approved": true,
"comment": "同意换寝"
}获取晚归记录
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| status | string | 否 | 状态 |
| date | string | 否 | 日期 |
| studentId | int | 否 | 学生ID |
响应:
{
"list": [
{
"id": 1,
"studentId": 1,
"studentName": "张三",
"roomNumber": "A-101",
"alertDate": "2024-03-16",
"lastEntry": "23:45",
"status": "Pending",
"handler": null,
"comment": null,
"createdAt": "2024-03-17T00:00:00Z"
}
]
}处理晚归记录
请求参数:
{
"status": "Handled",
"comment": "已联系学生确认"
}晚归统计
响应:
{
"total": 25,
"pending": 3,
"handled": 20,
"ignored": 2,
"trend": [5, 3, 8, 4, 5] // 最近5天
}获取门禁记录
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| studentId | int | 否 | 学生ID |
| direction | string | 否 | 方向: In/Out |
| status | string | 否 | 状态: Normal/Late/Absent |
| date | string | 否 | 日期 |
响应:
{
"list": [
{
"id": 1,
"studentId": 1,
"studentName": "张三",
"roomNumber": "A-101",
"timestamp": "2024-03-16T22:30:00Z",
"direction": "In",
"status": "Late",
"deviceId": "GATE_01"
}
],
"total": 1000
}门禁统计
响应:
{
"totalEntries": 5000,
"totalExits": 4800,
"lateReturns": 25,
"absentCount": 5,
"peakHours": [
{ "hour": 7, "count": 300 },
{ "hour": 12, "count": 450 },
{ "hour": 18, "count": 500 },
{ "hour": 22, "count": 200 }
]
}获取消息列表
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| type | string | 否 | 类型 |
| isRead | bool | 否 | 是否已读 |
| page | int | 否 | 页码 |
响应:
{
"list": [
{
"id": 1,
"type": "repair",
"title": "报修状态更新",
"content": "您的报修已处理完成",
"isRead": false,
"link": "/pages/repairs/detail?id=1",
"createdAt": "2024-03-16T14:00:00Z"
}
],
"total": 50
}获取未读消息数
响应:
{
"count": 5
}标记消息已读
标记全部已读
入住统计
响应:
{
"totalRooms": 1200,
"occupied": 1080,
"available": 120,
"occupancyRate": 90,
"byBuilding": [
{ "building": "A栋", "occupancyRate": 95 },
{ "building": "B栋", "occupancyRate": 88 }
]
}报修统计
响应:
{
"total": 86,
"pending": 2,
"processing": 6,
"completed": 78,
"byType": {
"plumbing": 25,
"electrical": 18,
"furniture": 15
},
"avgProcessTime": 24.5
}生成报表
请求参数:
{
"type": "occupancy", // occupancy/repair/inspection/late/access/roomswap
"timeRange": "month", // today/week/month/quarter/year/custom
"startDate": "2024-03-01",
"endDate": "2024-03-31",
"format": "excel" // excel/pdf/csv
}以下接口是 v0.2.0 引入的。如果你的部署还在 v0.1.0,这些接口不会存在。
通用文件上传(需登录,任意角色)。
- Content-Type:
multipart/form-data - 字段名:
file - 限制: 单文件 ≤ 5 MiB,MIME 白名单
image/{jpeg,png,gif,webp}+application/pdf(嗅探前 512 字节,不信前端 Content-Type) - 落盘路径:
./uploads/<YYYY-MM-DD>/<32 hex>.<ext> - 静态服务: 返回的 url 路径直接
GET /<url>即可下载
请求:
curl -X POST http://localhost:8080/api/upload \
-H "Authorization: Bearer <token>" \
-F "file=@/path/to/image.png"响应 (200):
{
"url": "/uploads/2026-05-28/4e0d3b7b08c12f763eb6501fb072a58c.png",
"filename": "4e0d3b7b08c12f763eb6501fb072a58c.png",
"size": 220382,
"mime": "image/png"
}错误:
| Status | error | 场景 |
|---|---|---|
| 400 | invalid_request | 缺 file 字段或读取失败 |
| 413 | file_too_large | 文件超 5 MiB |
| 415 | unsupported_type | MIME 不在白名单(响应 message 含真实嗅探类型) |
报修按日聚合,适合前端 LineChart 画 30 天趋势。需 dashboard:read 权限。
参数:
| Query | 类型 | 默认 | 范围 | 说明 |
|---|---|---|---|---|
| days | int | 30 | 1..180 | 取过去 N 天(含今天),用 generate_series 补齐空日 |
响应 (200):
{
"days": 7,
"data": [
{"day": "2026-05-22", "total": 0, "completed": 0, "pending": 0},
{"day": "2026-05-23", "total": 0, "completed": 0, "pending": 0},
{"day": "2026-05-24", "total": 0, "completed": 0, "pending": 0},
{"day": "2026-05-25", "total": 0, "completed": 0, "pending": 0},
{"day": "2026-05-26", "total": 0, "completed": 0, "pending": 0},
{"day": "2026-05-27", "total": 0, "completed": 0, "pending": 0},
{"day": "2026-05-28", "total": 0, "completed": 0, "pending": 0}
]
}审计日志查询(任何登录用户对 POST/PUT/PATCH/DELETE 的访问都会被记录)。需 users:read 权限。
参数:
| Query | 类型 | 默认 | 范围 | 说明 |
|---|---|---|---|---|
| page | int | 1 | ≥1 | 分页页码 |
| pageSize | int | 50 | 1..200 | 每页大小 |
| userId | string | - | - | 可选,只看某个用户的操作 |
响应 (200):
{
"total": 124,
"page": 1,
"pageSize": 50,
"data": [
{
"id": "8c0973d6-...",
"userId": "user-admin-1",
"username": "admin",
"method": "POST",
"path": "/api/buildings",
"status": 201,
"ip": "127.0.0.1",
"userAgent": "curl/8.4.0",
"createdAt": "2026-05-28 17:13:35"
}
]
}审计事件 SSE 实时流。客户端断开自动 unsubscribe;无新事件时连接保持。需 users:read 权限。
请求:
curl -N -H "Authorization: Bearer <token>" \
http://localhost:8080/api/audit-logs/stream响应 (200,持续):
event: audit
data: {"id":"...","userId":"user-admin-1","username":"admin","method":"POST","path":"/api/buildings","status":201,"ip":"127.0.0.1","userAgent":"curl/8.4.0","createdAt":"2026-05-28 17:13:35"}
event: audit
data: {"id":"...","method":"DELETE","path":"/api/buildings/...",...}
前端示例(浏览器 fetch + ReadableStream,适配带 Authorization 头):
const res = await fetch(`${API}/audit-logs/stream`, {
headers: { Authorization: `Bearer ${token}` },
signal: ctrl.signal,
});
const reader = res.body.getReader();
const dec = new TextDecoder();
let buf = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buf += dec.decode(value, { stream: true });
const blocks = buf.split('\n\n');
buf = blocks.pop() || '';
for (const block of blocks) {
const data = block.split('\n').find(l => l.startsWith('data:'));
if (data) handle(JSON.parse(data.slice(5).trim()));
}
}实现:in-memory broker,无持久化(完整记录在 audit_logs 表里)。慢消费者(buffer 32 满)直接丢事件,不阻塞 Publish。
查看当前注册的周期任务(需 users:read 权限)。
响应 (200):
{
"jobs": [
{"id": 1, "schedule": "0 0 3 * * *", "name": "cleanup-expired-tokens"},
{"id": 2, "schedule": "0 0 2 * * *", "name": "scan-late-returns"}
]
}任务说明:
- cleanup-expired-tokens — 每日 03:00,删
token_blacklist中expires_at < NOW()的行 - scan-late-returns — 每日 02:00,扫过去 24h 最后一次
access_logs.direction='Out'的学生,写late_return_alerts(同日不重复)
观测:每次跑完会 +1 到 Prometheus counter scheduler_job_runs_total{name, result}。
{
"code": 200,
"message": "success",
"data": { }
}{
"code": 400,
"message": "参数错误",
"error": {
"field": "username",
"message": "用户名不能为空"
}
}| 错误码 | 说明 |
|---|---|
| 200 | 成功 |
| 400 | 请求参数错误 |
| 401 | 未授权/Token过期 |
| 403 | 禁止访问 |
| 404 | 资源不存在 |
| 429 | 请求过于频繁 |
| 500 | 服务器内部错误 |
所有 API 请求(除登录外)需要在请求头中携带 Token:
Authorization: Bearer {access_token}
列表接口支持统一分页参数:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| page | int | 1 | 页码 |
| pageSize | int | 20 | 每页数量,最大100 |
分页响应格式:
{
"list": [],
"total": 100,
"page": 1,
"pageSize": 20,
"totalPages": 5
}