Commit db1f4347 by luoqi

feat:接口标准

parent d91e197b
Pipeline #3222 passed with stage
in 9 seconds
......@@ -248,6 +248,58 @@ python migrate.py --rollback 001
## 📚 API文档
### 🔗 前后端接口标准约定
#### 📋 统一响应格式
**标准响应格式(所有接口):**
```json
{
"success": true,
"message": "操作成功",
"data": [实际数据内容]
}
```
**特殊响应格式(认证相关接口):**
```json
{
"success": true,
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
"refresh_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
"token_type": "bearer",
"expires_in": 86400,
"user": {...},
"message": "登录成功"
}
```
#### 🔧 前端自动解包机制
前端API客户端自动处理响应格式:
- **自动检测**:识别 `BaseResponse` 格式(包含 `success` 字段)
- **自动解包**:提取 `data` 字段内容,透明返回给业务代码
- **错误处理**:检查 `success` 字段,失败时自动抛出异常
- **兼容性**:对特殊格式接口(如login)保持原样返回
#### 📊 状态码约定
| 状态码 | 含义 | 使用场景 |
|--------|------|----------|
| 200 | 成功 | 正常请求成功 |
| 400 | 请求错误 | 参数验证失败、业务逻辑错误 |
| 401 | 未授权 | Token无效、未登录 |
| 403 | 禁止访问 | 权限不足 |
| 404 | 资源不存在 | 数据不存在 |
| 500 | 服务器错误 | 系统内部错误 |
#### 🔐 认证机制
- **认证方式**:JWT Bearer Token
- **Token类型**:Access Token(短期)+ Refresh Token(长期)
- **自动刷新**:前端自动检测401错误并尝试刷新Token
- **安全登出**:服务端Token黑名单机制
### 接口概览
系统提供完整的RESTful API,支持前后端分离架构。
......@@ -260,30 +312,110 @@ python migrate.py --rollback 001
### 主要接口
| 模块 | 方法 | 路径 | 描述 |
|------|------|------|------|
| 认证 | POST | `/users/login` | 用户登录 |
| 认证 | POST | `/users/refresh` | 刷新Token |
| 用户 | GET | `/users` | 获取用户列表 |
| 用户 | POST | `/users` | 创建用户 |
| 用户 | PUT | `/users/{id}` | 更新用户 |
| 用户 | DELETE | `/users/{id}` | 删除用户 |
| 机构 | GET | `/institutions` | 获取机构列表 |
| 机构 | POST | `/institutions` | 创建机构 |
| 历史 | GET | `/history` | 获取历史数据 |
| 配置 | GET | `/config` | 获取系统配置 |
### 认证示例
| 模块 | 方法 | 路径 | 描述 | 响应格式 |
|------|------|------|------|----------|
| 认证 | POST | `/users/login` | 用户登录 | 特殊格式 |
| 认证 | POST | `/users/logout` | 用户登出 | 标准格式 |
| 认证 | POST | `/users/refresh` | 刷新Token | 特殊格式 |
| 用户 | GET | `/users` | 获取用户列表 | 标准格式 |
| 用户 | GET | `/users/{id}` | 获取用户信息 | 标准格式 |
| 用户 | POST | `/users` | 创建用户 | 标准格式 |
| 用户 | PUT | `/users/{id}` | 更新用户 | 标准格式 |
| 用户 | DELETE | `/users/{id}` | 删除用户 | 标准格式 |
| 机构 | GET | `/institutions` | 获取机构列表 | 标准格式 |
| 机构 | GET | `/institutions/{id}` | 获取机构信息 | 标准格式 |
| 机构 | POST | `/institutions` | 创建机构 | 标准格式 |
| 机构 | PUT | `/institutions/{id}` | 更新机构 | 标准格式 |
| 机构 | DELETE | `/institutions/{id}` | 删除机构 | 标准格式 |
| 历史 | GET | `/history` | 获取历史数据列表 | 标准格式 |
| 历史 | GET | `/history/{month}` | 获取指定月份历史 | 标准格式 |
| 历史 | POST | `/history` | 保存月度历史 | 标准格式 |
| 历史 | DELETE | `/history` | 清空历史数据 | 标准格式 |
| 配置 | GET | `/config` | 获取系统配置 | 标准格式 |
| 配置 | GET | `/config/{key}` | 获取指定配置项 | 标准格式 |
### 🔧 API使用示例
#### 认证流程
```bash
# 登录获取Token
# 1. 用户登录(特殊格式响应)
curl -X POST "http://localhost:8000/api/users/login" \
-H "Content-Type: application/json" \
-d '{"phone":"admin","password":"admin123"}'
# 使用Token访问API
# 响应示例:
{
"success": true,
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
"refresh_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
"token_type": "bearer",
"expires_in": 86400,
"user": {
"id": "admin",
"phone": "admin",
"name": "系统管理员",
"role": "admin"
},
"message": "登录成功"
}
# 2. 使用Token访问API(标准格式响应)
curl -X GET "http://localhost:8000/api/users" \
-H "Authorization: Bearer YOUR_TOKEN_HERE"
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
# 响应示例:
{
"success": true,
"message": "获取用户列表成功",
"data": [
{
"id": "admin",
"phone": "admin",
"name": "系统管理员",
"role": "admin",
"institutions": [],
"created_at": "2025-08-30T10:00:00",
"updated_at": "2025-08-30T10:00:00"
}
]
}
# 3. 刷新Token
curl -X POST "http://localhost:8000/api/users/refresh" \
-H "Content-Type: application/json" \
-d '{"refresh_token":"YOUR_REFRESH_TOKEN"}'
# 4. 用户登出
curl -X POST "http://localhost:8000/api/users/logout" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
```
#### 业务接口示例
```bash
# 获取机构列表
curl -X GET "http://localhost:8000/api/institutions" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
# 创建机构
curl -X POST "http://localhost:8000/api/institutions" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"id": "inst_001",
"name": "测试机构",
"institution_id": "TEST001",
"owner_id": "user_001"
}'
# 获取历史数据
curl -X GET "http://localhost:8000/api/history" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
# 获取系统配置
curl -X GET "http://localhost:8000/api/config" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
```
## 🔧 环境配置
......
......@@ -16,7 +16,7 @@ from dependencies import get_current_active_user, require_admin
router = APIRouter()
@router.get("", response_model=List[MonthlyHistoryResponse], summary="获取所有历史记录")
@router.get("", response_model=BaseResponse, summary="获取所有历史记录")
async def get_all_history(
db: DatabaseManager = Depends(get_database),
current_user: UserResponse = Depends(get_current_active_user)
......@@ -54,18 +54,26 @@ async def get_all_history(
# 跳过有问题的记录,继续处理其他记录
continue
return result
return BaseResponse(
success=True,
message="获取历史记录成功",
data=result
)
except Exception as e:
logger.error(f"获取历史记录失败: {e}")
# 如果是表不存在或字段不存在的错误,返回空列表而不是抛出异常
if "does not exist" in str(e).lower():
logger.info("历史记录表或字段不存在,返回空列表")
return []
return BaseResponse(
success=True,
message="获取历史记录成功",
data=[]
)
raise HTTPException(status_code=500, detail="获取历史记录失败")
@router.get("/{month}", response_model=MonthlyHistoryResponse, summary="获取指定月份历史记录")
@router.get("/{month}", response_model=BaseResponse, summary="获取指定月份历史记录")
async def get_history_by_month(
month: str,
db: DatabaseManager = Depends(get_database),
......@@ -76,19 +84,19 @@ async def get_history_by_month(
# 验证月份格式
if not month or len(month) != 7 or month[4] != '-':
raise HTTPException(status_code=400, detail="月份格式错误,应为 YYYY-MM")
query = monthly_history_table.select().where(monthly_history_table.c.month == month)
history = await db.fetch_one(query)
if not history:
raise HTTPException(status_code=404, detail="指定月份的历史记录不存在")
# 安全地获取 institutions_data,确保兼容性
institutions_data = history["institutions_data"] if "institutions_data" in history._mapping else []
if institutions_data is None:
institutions_data = []
return MonthlyHistoryResponse(
history_data = MonthlyHistoryResponse(
id=history["id"],
month=history["month"],
save_time=history["save_time"],
......@@ -99,7 +107,13 @@ async def get_history_by_month(
institutions_data=institutions_data,
created_at=history["created_at"]
)
return BaseResponse(
success=True,
message="获取历史记录成功",
data=history_data
)
except HTTPException:
raise
except Exception as e:
......
......@@ -59,7 +59,7 @@ async def get_institution_with_images(institution_id: str, db: DatabaseManager):
)
@router.get("", response_model=List[InstitutionResponse], summary="获取机构列表")
@router.get("", response_model=BaseResponse, summary="获取机构列表")
async def get_all_institutions(
db: DatabaseManager = Depends(get_database),
current_user: UserResponse = Depends(get_current_active_user)
......@@ -85,14 +85,19 @@ async def get_all_institutions(
result.append(inst_with_images)
logger.info(f"用户 {current_user.name}({current_user.role}) 获取到 {len(result)} 个机构")
return result
return BaseResponse(
success=True,
message="获取机构列表成功",
data=result
)
except Exception as e:
logger.error(f"获取机构列表失败: {e}")
raise HTTPException(status_code=500, detail="获取机构列表失败")
@router.get("/{institution_id}", response_model=InstitutionResponse, summary="根据ID获取机构")
@router.get("/{institution_id}", response_model=BaseResponse, summary="根据ID获取机构")
async def get_institution_by_id(
institution_id: str,
db: DatabaseManager = Depends(get_database),
......@@ -101,12 +106,16 @@ async def get_institution_by_id(
"""根据机构ID获取机构信息"""
try:
institution = await get_institution_with_images(institution_id, db)
if not institution:
raise HTTPException(status_code=404, detail="机构不存在")
return institution
return BaseResponse(
success=True,
message="获取机构信息成功",
data=institution
)
except HTTPException:
raise
except Exception as e:
......@@ -114,7 +123,7 @@ async def get_institution_by_id(
raise HTTPException(status_code=500, detail="获取机构失败")
@router.get("/owner/{owner_id}", response_model=List[InstitutionResponse], summary="根据负责人ID获取机构")
@router.get("/owner/{owner_id}", response_model=BaseResponse, summary="根据负责人ID获取机构")
async def get_institutions_by_owner(
owner_id: str,
db: DatabaseManager = Depends(get_database),
......@@ -125,23 +134,27 @@ async def get_institutions_by_owner(
query = institutions_table.select().where(
institutions_table.c.owner_id == owner_id
).order_by(institutions_table.c.created_at)
institutions = await db.fetch_all(query)
result = []
for institution in institutions:
inst_with_images = await get_institution_with_images(institution["id"], db)
if inst_with_images:
result.append(inst_with_images)
return result
return BaseResponse(
success=True,
message="获取机构列表成功",
data=result
)
except Exception as e:
logger.error(f"根据负责人获取机构失败: {e}")
raise HTTPException(status_code=500, detail="获取机构失败")
@router.post("", response_model=InstitutionResponse, summary="创建机构")
@router.post("", response_model=BaseResponse, summary="创建机构")
async def create_institution(
institution_data: InstitutionCreate,
db: DatabaseManager = Depends(get_database),
......@@ -155,7 +168,7 @@ async def create_institution(
)
if existing_inst:
raise HTTPException(status_code=400, detail="机构ID已存在")
# 检查机构编号是否已存在(如果提供了编号)
if institution_data.institution_id:
existing_inst_id = await db.fetch_one(
......@@ -165,7 +178,7 @@ async def create_institution(
)
if existing_inst_id:
raise HTTPException(status_code=400, detail="机构编号已存在")
# 插入新机构
query = institutions_table.insert().values(
id=institution_data.id,
......@@ -173,12 +186,18 @@ async def create_institution(
institution_id=institution_data.institution_id,
owner_id=institution_data.owner_id
)
await db.execute(query)
# 返回创建的机构信息
return await get_institution_by_id(institution_data.id, db)
# 获取创建的机构信息
created_institution = await get_institution_with_images(institution_data.id, db)
return BaseResponse(
success=True,
message="创建机构成功",
data=created_institution
)
except HTTPException:
raise
except Exception as e:
......@@ -186,7 +205,7 @@ async def create_institution(
raise HTTPException(status_code=500, detail="创建机构失败")
@router.put("/{institution_id}", response_model=InstitutionResponse, summary="更新机构")
@router.put("/{institution_id}", response_model=BaseResponse, summary="更新机构")
async def update_institution(
institution_id: str,
institution_data: InstitutionUpdate,
......@@ -201,7 +220,7 @@ async def update_institution(
)
if not existing_inst:
raise HTTPException(status_code=404, detail="机构不存在")
# 构建更新数据
update_data = {}
if institution_data.name is not None:
......@@ -219,19 +238,25 @@ async def update_institution(
update_data["institution_id"] = institution_data.institution_id
if institution_data.owner_id is not None:
update_data["owner_id"] = institution_data.owner_id
if not update_data:
raise HTTPException(status_code=400, detail="没有提供更新数据")
# 执行更新
query = institutions_table.update().where(
institutions_table.c.id == institution_id
).values(**update_data)
await db.execute(query)
# 返回更新后的机构信息
return await get_institution_by_id(institution_id, db)
# 获取更新后的机构信息
updated_institution = await get_institution_with_images(institution_id, db)
return BaseResponse(
success=True,
message="更新机构成功",
data=updated_institution
)
except HTTPException:
raise
except Exception as e:
......@@ -359,7 +384,7 @@ async def delete_institution_image(
raise HTTPException(status_code=500, detail="删除图片失败")
@router.get("/institution-id/{inst_id}", response_model=InstitutionResponse, summary="根据机构编号获取机构")
@router.get("/institution-id/{inst_id}", response_model=BaseResponse, summary="根据机构编号获取机构")
async def get_institution_by_institution_id(
inst_id: str,
db: DatabaseManager = Depends(get_database),
......@@ -373,7 +398,13 @@ async def get_institution_by_institution_id(
if not institution:
raise HTTPException(status_code=404, detail="机构不存在")
return await get_institution_with_images(institution["id"], db)
institution_data = await get_institution_with_images(institution["id"], db)
return BaseResponse(
success=True,
message="获取机构信息成功",
data=institution_data
)
except HTTPException:
raise
......
......@@ -14,7 +14,7 @@ from dependencies import get_current_active_user, require_admin
router = APIRouter()
@router.get("", response_model=Dict[str, Any], summary="获取所有系统配置")
@router.get("", response_model=BaseResponse, summary="获取所有系统配置")
async def get_all_config(
db: DatabaseManager = Depends(get_database),
current_user: UserResponse = Depends(get_current_active_user)
......@@ -23,27 +23,31 @@ async def get_all_config(
try:
query = system_config_table.select()
configs = await db.fetch_all(query)
# 转换为键值对格式,与前端localStorage格式保持一致
result = {}
for config in configs:
result[config["config_key"]] = config["config_value"]
return result
return BaseResponse(
success=True,
message="获取系统配置成功",
data=result
)
except Exception as e:
logger.error(f"获取系统配置失败: {e}")
raise HTTPException(status_code=500, detail="获取系统配置失败")
@router.get("/list", response_model=List[SystemConfigResponse], summary="获取系统配置列表")
@router.get("/list", response_model=BaseResponse, summary="获取系统配置列表")
async def get_config_list(db: DatabaseManager = Depends(get_database)):
"""获取系统配置列表(详细格式)"""
try:
query = system_config_table.select().order_by(system_config_table.c.config_key)
configs = await db.fetch_all(query)
return [
config_list = [
SystemConfigResponse(
id=config["id"],
config_key=config["config_key"],
......@@ -54,24 +58,34 @@ async def get_config_list(db: DatabaseManager = Depends(get_database)):
)
for config in configs
]
return BaseResponse(
success=True,
message="获取系统配置列表成功",
data=config_list
)
except Exception as e:
logger.error(f"获取系统配置列表失败: {e}")
raise HTTPException(status_code=500, detail="获取系统配置列表失败")
@router.get("/{config_key}", response_model=Any, summary="获取指定配置项")
@router.get("/{config_key}", response_model=BaseResponse, summary="获取指定配置项")
async def get_config_by_key(config_key: str, db: DatabaseManager = Depends(get_database)):
"""根据配置键名获取配置值"""
try:
query = system_config_table.select().where(system_config_table.c.config_key == config_key)
config = await db.fetch_one(query)
if not config:
raise HTTPException(status_code=404, detail="配置项不存在")
return config["config_value"]
return BaseResponse(
success=True,
message="获取配置项成功",
data=config["config_value"]
)
except HTTPException:
raise
except Exception as e:
......
......@@ -22,7 +22,7 @@ from config import settings
router = APIRouter()
@router.get("", response_model=List[UserResponse], summary="获取所有用户")
@router.get("", response_model=BaseResponse, summary="获取所有用户")
async def get_all_users(
db: DatabaseManager = Depends(get_database),
current_user: UserResponse = Depends(require_admin)
......@@ -31,8 +31,8 @@ async def get_all_users(
try:
query = users_table.select()
users = await db.fetch_all(query)
return [
user_list = [
UserResponse(
id=user["id"],
phone=user["phone"],
......@@ -44,6 +44,12 @@ async def get_all_users(
)
for user in users
]
return BaseResponse(
success=True,
message="获取用户列表成功",
data=user_list
)
except Exception as e:
logger.error(f"获取用户列表失败: {e}")
raise HTTPException(status_code=500, detail="获取用户列表失败")
......@@ -55,7 +61,7 @@ async def get_current_user_info(current_user: UserResponse = Depends(get_current
return current_user
@router.get("/{user_id}", response_model=UserResponse, summary="根据ID获取用户")
@router.get("/{user_id}", response_model=BaseResponse, summary="根据ID获取用户")
async def get_user_by_id(
user_id: str,
db: DatabaseManager = Depends(get_database),
......@@ -69,7 +75,7 @@ async def get_user_by_id(
if not user:
raise HTTPException(status_code=404, detail="用户不存在")
return UserResponse(
user_data = UserResponse(
id=user["id"],
phone=user["phone"],
name=user["name"],
......@@ -78,6 +84,12 @@ async def get_user_by_id(
created_at=user["created_at"],
updated_at=user["updated_at"]
)
return BaseResponse(
success=True,
message="获取用户信息成功",
data=user_data
)
except HTTPException:
raise
except Exception as e:
......@@ -85,7 +97,7 @@ async def get_user_by_id(
raise HTTPException(status_code=500, detail="获取用户失败")
@router.post("", response_model=UserResponse, summary="创建用户")
@router.post("", response_model=BaseResponse, summary="创建用户")
async def create_user(
user_data: UserCreate,
db: DatabaseManager = Depends(get_database),
......@@ -99,7 +111,7 @@ async def create_user(
)
if existing_user:
raise HTTPException(status_code=400, detail="用户ID已存在")
# 检查手机号是否已存在
existing_phone = await db.fetch_one(
users_table.select().where(users_table.c.phone == user_data.phone)
......@@ -121,10 +133,16 @@ async def create_user(
)
await db.execute(query)
# 返回创建的用户信息
return await get_user_by_id(user_data.id, db)
# 获取创建的用户信息
created_user_response = await get_user_by_id(user_data.id, db, current_user)
return BaseResponse(
success=True,
message="创建用户成功",
data=created_user_response.data
)
except HTTPException:
raise
except Exception as e:
......@@ -132,7 +150,7 @@ async def create_user(
raise HTTPException(status_code=500, detail="创建用户失败")
@router.put("/{user_id}", response_model=UserResponse, summary="更新用户")
@router.put("/{user_id}", response_model=BaseResponse, summary="更新用户")
async def update_user(
user_id: str,
user_data: UserUpdate,
......@@ -147,21 +165,21 @@ async def update_user(
)
if not existing_user:
raise HTTPException(status_code=404, detail="用户不存在")
# 构建更新数据
update_data = {}
if user_data.phone is not None:
# 检查手机号是否被其他用户使用
phone_check = await db.fetch_one(
users_table.select().where(
(users_table.c.phone == user_data.phone) &
(users_table.c.phone == user_data.phone) &
(users_table.c.id != user_id)
)
)
if phone_check:
raise HTTPException(status_code=400, detail="手机号已被其他用户使用")
update_data["phone"] = user_data.phone
if user_data.name is not None:
update_data["name"] = user_data.name
if user_data.password is not None:
......@@ -171,17 +189,23 @@ async def update_user(
update_data["role"] = user_data.role
if user_data.institutions is not None:
update_data["institutions"] = user_data.institutions
if not update_data:
raise HTTPException(status_code=400, detail="没有提供更新数据")
# 执行更新
query = users_table.update().where(users_table.c.id == user_id).values(**update_data)
await db.execute(query)
# 返回更新后的用户信息
return await get_user_by_id(user_id, db)
# 获取更新后的用户信息
updated_user_response = await get_user_by_id(user_id, db, current_user)
return BaseResponse(
success=True,
message="更新用户成功",
data=updated_user_response.data
)
except HTTPException:
raise
except Exception as e:
......@@ -344,7 +368,7 @@ async def logout(
@router.get("/phone/{phone}", response_model=UserResponse, summary="根据手机号获取用户")
@router.get("/phone/{phone}", response_model=BaseResponse, summary="根据手机号获取用户")
async def get_user_by_phone(
phone: str,
db: DatabaseManager = Depends(get_database),
......@@ -354,11 +378,11 @@ async def get_user_by_phone(
try:
query = users_table.select().where(users_table.c.phone == phone)
user = await db.fetch_one(query)
if not user:
raise HTTPException(status_code=404, detail="用户不存在")
return UserResponse(
user_data = UserResponse(
id=user["id"],
phone=user["phone"],
name=user["name"],
......@@ -367,6 +391,12 @@ async def get_user_by_phone(
created_at=user["created_at"],
updated_at=user["updated_at"]
)
return BaseResponse(
success=True,
message="获取用户信息成功",
data=user_data
)
except HTTPException:
raise
except Exception as e:
......
......@@ -131,7 +131,8 @@ class ApiClient {
})
if (retryResponse.ok) {
return await retryResponse.json()
const retryData = await retryResponse.json()
return this.unwrapResponse(retryData)
}
}
......@@ -146,7 +147,9 @@ class ApiClient {
throw new Error(errorData.detail || `HTTP ${response.status}: ${response.statusText}`)
}
return await response.json()
const responseData = await response.json()
// 自动解包BaseResponse格式的响应
return this.unwrapResponse(responseData)
} catch (error) {
if (error.name === 'AbortError') {
throw new Error('请求超时,请检查网络连接')
......@@ -157,6 +160,23 @@ class ApiClient {
}
/**
* 自动解包BaseResponse格式的响应
*/
unwrapResponse(responseData) {
// 如果响应包含success字段,说明是BaseResponse格式
if (responseData && typeof responseData === 'object' && 'success' in responseData) {
// 检查请求是否成功
if (!responseData.success) {
throw new Error(responseData.message || '请求失败')
}
// 返回data字段的内容,如果没有data字段则返回整个响应
return responseData.data !== undefined ? responseData.data : responseData
}
// 如果不是BaseResponse格式,直接返回原始数据(如login/logout等特殊接口)
return responseData
}
/**
* 刷新访问token
*/
async refreshAccessToken() {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment