删除了文档
This commit is contained in:
-524
@@ -1,524 +0,0 @@
|
||||
# 学生板块:5 个必做接口完整实现
|
||||
|
||||
本文按当前项目的 FastAPI、Pydantic 2、SQLAlchemy 同步 Session 编写。代码按文件整理,可分别替换对应文件内容;本文仅提供实现文档,没有改动现有 Python 文件。
|
||||
|
||||
## 一、接口与约定
|
||||
|
||||
| 方法 | 路径 | 功能 |
|
||||
| --- | --- | --- |
|
||||
| GET | `/students` | 分页查询,按学号、姓名、班级筛选 |
|
||||
| POST | `/students` | 新增学生,未提供学号时自动生成 |
|
||||
| GET | `/students/{id}` | 查询未删除学生详情 |
|
||||
| PUT | `/students/{id}` | 修改学生,只更新本次提交的字段 |
|
||||
| DELETE | `/students/{id}` | 将 `is_deleted` 改为 1 |
|
||||
|
||||
- `id` 是数据库自增主键,`student_no` 是业务学号,二者不同。
|
||||
- 学号精确匹配,姓名按包含关系匹配,班级按 `class_id` 精确匹配;多个条件同时生效。
|
||||
- 列表默认第 1 页、每页 20 条,每页最多 100 条;无结果返回空数组。
|
||||
- PUT 在这里约定为部分更新:未传字段保持原值;可选字段传 `null` 表示清空。
|
||||
- 已删除学生不出现在列表、详情中,也不能再修改;重复删除返回 404。
|
||||
- 删除学生不自动删除成绩、就业记录,其他模块查询时需要自行处理学生的删除状态。
|
||||
- 性别暂不规定枚举,仅要求非空且不超过数据库长度,避免擅自改变团队约定。
|
||||
- 年龄暂设为 0~150;毕业日期不能早于入学日期。
|
||||
|
||||
## 二、请求和响应模型:`schemas/student.py`
|
||||
|
||||
作用:在进入接口业务代码前检查字段类型、长度和取值;定义返回给前端的数据结构。
|
||||
|
||||
```python
|
||||
from datetime import date, datetime
|
||||
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
||||
|
||||
|
||||
class StudentCreate(BaseModel):
|
||||
# 1. 自动去除字符串首尾空格;禁止多传 id、is_deleted 等未声明字段。
|
||||
model_config = ConfigDict(str_strip_whitespace=True, extra="forbid")
|
||||
|
||||
# 2. 学号可省略或传 null,此时由后端生成;空字符串不合法。
|
||||
student_no: str | None = Field(default=None, min_length=1, max_length=50)
|
||||
student_name: str = Field(min_length=1, max_length=50)
|
||||
class_id: int = Field(gt=0)
|
||||
consultant_id: int | None = Field(default=None, gt=0)
|
||||
native_place: str | None = Field(default=None, max_length=100)
|
||||
graduation_school: str | None = Field(default=None, max_length=100)
|
||||
major: str | None = Field(default=None, max_length=100)
|
||||
enrollment_date: date
|
||||
graduation_date: date | None = None
|
||||
education: str | None = Field(default=None, max_length=50)
|
||||
age: int = Field(ge=0, le=150)
|
||||
gender: str = Field(min_length=1, max_length=10)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def check_dates(self):
|
||||
# 3. 新增时两个日期都已确定,可以直接校验先后顺序。
|
||||
if self.graduation_date and self.graduation_date < self.enrollment_date:
|
||||
raise ValueError("毕业日期不能早于入学日期")
|
||||
return self
|
||||
|
||||
|
||||
class StudentUpdate(BaseModel):
|
||||
model_config = ConfigDict(str_strip_whitespace=True, extra="forbid")
|
||||
|
||||
# 1. 默认值为 None 是为了允许省略字段,不代表每个字段都能清空。
|
||||
student_no: str | None = Field(default=None, min_length=1, max_length=50)
|
||||
student_name: str | None = Field(default=None, min_length=1, max_length=50)
|
||||
class_id: int | None = Field(default=None, gt=0)
|
||||
consultant_id: int | None = Field(default=None, gt=0)
|
||||
native_place: str | None = Field(default=None, max_length=100)
|
||||
graduation_school: str | None = Field(default=None, max_length=100)
|
||||
major: str | None = Field(default=None, max_length=100)
|
||||
enrollment_date: date | None = None
|
||||
graduation_date: date | None = None
|
||||
education: str | None = Field(default=None, max_length=50)
|
||||
age: int | None = Field(default=None, ge=0, le=150)
|
||||
gender: str | None = Field(default=None, min_length=1, max_length=10)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def check_explicit_null(self):
|
||||
# 2. model_fields_set 记录客户端实际提交过的字段。
|
||||
# 没传姓名可以;明确传 student_name: null 不可以。
|
||||
required = {
|
||||
"student_no", "student_name", "class_id",
|
||||
"enrollment_date", "age", "gender",
|
||||
}
|
||||
for name in required & self.model_fields_set:
|
||||
if getattr(self, name) is None:
|
||||
raise ValueError(f"{name} 不能为 null")
|
||||
return self
|
||||
|
||||
|
||||
class StudentOut(BaseModel):
|
||||
# 允许 Pydantic 从 SQLAlchemy 对象的属性读取数据。
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int
|
||||
student_no: str | None
|
||||
student_name: str | None
|
||||
class_id: int | None
|
||||
consultant_id: int | None
|
||||
native_place: str | None
|
||||
graduation_school: str | None
|
||||
major: str | None
|
||||
enrollment_date: date
|
||||
graduation_date: date | None
|
||||
education: str | None
|
||||
age: int
|
||||
gender: str
|
||||
# 当前 Student 模型的实际字段名是 create_at 和 update_at。
|
||||
create_at: datetime | None
|
||||
update_at: datetime | None
|
||||
|
||||
|
||||
class StudentResponse(BaseModel):
|
||||
code: int = 200
|
||||
detail: str = "操作成功"
|
||||
data: StudentOut
|
||||
|
||||
|
||||
class StudentListResponse(BaseModel):
|
||||
code: int = 200
|
||||
detail: str = "查询成功"
|
||||
total: int
|
||||
page: int
|
||||
page_size: int
|
||||
data: list[StudentOut]
|
||||
|
||||
|
||||
class StudentDeleteResponse(BaseModel):
|
||||
code: int = 200
|
||||
detail: str = "删除学生成功"
|
||||
id: int
|
||||
```
|
||||
|
||||
`StudentOut` 的姓名、班级允许为空,是为了兼容当前数据库模型允许为空的历史记录;新增请求仍要求这些字段必填。
|
||||
|
||||
## 三、数据库操作:`dao/student.py`
|
||||
|
||||
为方便在现有项目直接使用,下列 DAO 使用 `HTTPException` 返回明确业务错误。项目扩大后,可改成自定义业务异常,再由 API 统一转换。
|
||||
|
||||
```python
|
||||
import logging
|
||||
from contextlib import contextmanager
|
||||
|
||||
from fastapi import HTTPException
|
||||
from sqlalchemy.exc import IntegrityError, SQLAlchemyError
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from models.student import Student
|
||||
from models.class_model import Classes
|
||||
from models.consultant import Consultant
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def database_errors(db: Session):
|
||||
# 统一处理数据库异常,避免每个接口重复写 try/except。
|
||||
try:
|
||||
yield
|
||||
except HTTPException:
|
||||
# 业务检查失败:撤销本次事务,保留原来的 404/409/422 等状态。
|
||||
db.rollback()
|
||||
raise
|
||||
except IntegrityError:
|
||||
# 外键、唯一约束等冲突,不能当作成功返回。
|
||||
db.rollback()
|
||||
logger.exception("学生数据约束冲突")
|
||||
raise HTTPException(409, "数据冲突,请检查学号、班级和顾问信息") from None
|
||||
except SQLAlchemyError:
|
||||
db.rollback()
|
||||
logger.exception("学生数据库操作失败")
|
||||
raise HTTPException(500, "数据库操作失败,请稍后重试") from None
|
||||
|
||||
|
||||
def require_student(student_id: int, db: Session):
|
||||
# 所有详情、更新和删除操作都复用这一步,排除逻辑删除记录。
|
||||
student = db.query(Student).filter(
|
||||
Student.id == student_id,
|
||||
Student.is_deleted == 0,
|
||||
).first()
|
||||
if student is None:
|
||||
raise HTTPException(404, "学生不存在或已删除")
|
||||
return student
|
||||
|
||||
|
||||
def check_references(data: dict, db: Session):
|
||||
# 1. 仅检查本次涉及的关联字段;更新其他字段时不用重复检查。
|
||||
if "class_id" in data:
|
||||
classroom = db.query(Classes).filter(
|
||||
Classes.id == data["class_id"],
|
||||
Classes.is_deleted == 0,
|
||||
).first()
|
||||
if classroom is None:
|
||||
raise HTTPException(422, "班级不存在或已删除")
|
||||
|
||||
# 2. 顾问允许为 null,此时代表不关联顾问,无需查询。
|
||||
if data.get("consultant_id") is not None:
|
||||
consultant = db.query(Consultant).filter(
|
||||
Consultant.id == data["consultant_id"],
|
||||
Consultant.is_deleted == 0,
|
||||
).first()
|
||||
if consultant is None:
|
||||
raise HTTPException(422, "顾问不存在或已删除")
|
||||
|
||||
|
||||
def check_student_no(student_no: str, db: Session, exclude_id=None):
|
||||
# 学号占用检查包含已删除记录,避免旧学号被其他学生重复使用。
|
||||
query = db.query(Student).filter(Student.student_no == student_no)
|
||||
if exclude_id is not None:
|
||||
# 修改时必须排除自己,否则原学号也会被判定为重复。
|
||||
query = query.filter(Student.id != exclude_id)
|
||||
if query.first() is not None:
|
||||
raise HTTPException(409, "学号已存在")
|
||||
|
||||
|
||||
def get_student_list(db, page, page_size, student_no=None,
|
||||
student_name=None, class_id=None):
|
||||
with database_errors(db):
|
||||
# 1. 先建立基础查询,只查询未删除学生。
|
||||
query = db.query(Student).filter(Student.is_deleted == 0)
|
||||
# 2. 参数有值时才增加筛选条件,多个 filter 之间为 AND。
|
||||
if student_no is not None:
|
||||
query = query.filter(Student.student_no == student_no)
|
||||
if student_name is not None:
|
||||
# 转义 % 和 _,让它们作为姓名的普通字符参与匹配。
|
||||
query = query.filter(Student.student_name.contains(
|
||||
student_name, autoescape=True,
|
||||
))
|
||||
if class_id is not None:
|
||||
query = query.filter(Student.class_id == class_id)
|
||||
# 3. 分页前统计符合条件的总数。
|
||||
total = query.count()
|
||||
# 4. 固定按主键倒序,最新记录优先,再跳过前面页的数据。
|
||||
rows = query.order_by(Student.id.desc()).offset(
|
||||
(page - 1) * page_size
|
||||
).limit(page_size).all()
|
||||
return total, rows
|
||||
|
||||
|
||||
def get_student_detail(student_id: int, db: Session):
|
||||
with database_errors(db):
|
||||
return require_student(student_id, db)
|
||||
|
||||
|
||||
def create_students(data: dict, db: Session):
|
||||
with database_errors(db):
|
||||
# 1. 校验班级、顾问是否存在且未删除。
|
||||
check_references(data, db)
|
||||
# 2. 用户指定学号时,先检查是否被占用。
|
||||
if data.get("student_no") is not None:
|
||||
check_student_no(data["student_no"], db)
|
||||
# 3. 创建 ORM 对象并加入当前事务。
|
||||
student = Student(**data)
|
||||
db.add(student)
|
||||
# 4. flush 执行 INSERT 并获取自增 id,但此时尚未提交事务。
|
||||
db.flush()
|
||||
# 5. 未提供学号时生成 STU0001 格式;四位是最小宽度。
|
||||
if student.student_no is None:
|
||||
generated_no = f"STU{student.id:04d}"
|
||||
check_student_no(generated_no, db, exclude_id=student.id)
|
||||
student.student_no = generated_no
|
||||
# 6. 在提交前生成响应快照,避免提交后再查询失败而误报新增失败。
|
||||
db.flush()
|
||||
db.refresh(student)
|
||||
from schemas.student import StudentOut
|
||||
result = StudentOut.model_validate(student)
|
||||
# 7. 所有步骤成功后提交;失败由 database_errors 回滚。
|
||||
db.commit()
|
||||
return result
|
||||
|
||||
|
||||
def update_student(student_id: int, updates: dict, db: Session):
|
||||
with database_errors(db):
|
||||
# 1. 先确认学生存在,再检查更新内容。
|
||||
student = require_student(student_id, db)
|
||||
if not updates:
|
||||
raise HTTPException(422, "请至少提供一个需要修改的字段")
|
||||
# 2. 若修改班级或顾问,校验新的关联对象。
|
||||
check_references(updates, db)
|
||||
if "student_no" in updates:
|
||||
check_student_no(updates["student_no"], db, exclude_id=student.id)
|
||||
# 3. 合并新旧日期再检查,防止只修改一个日期时绕过校验。
|
||||
enrollment = updates.get("enrollment_date", student.enrollment_date)
|
||||
graduation = updates.get("graduation_date", student.graduation_date)
|
||||
if graduation is not None and graduation < enrollment:
|
||||
raise HTTPException(422, "毕业日期不能早于入学日期")
|
||||
# 4. 仅更新实际提交的字段;可选字段传 None 会清空数据库值。
|
||||
for name, value in updates.items():
|
||||
setattr(student, name, value)
|
||||
# 5. 执行更新并读取模型自动维护的 update_at,构造响应后提交。
|
||||
db.flush()
|
||||
db.refresh(student)
|
||||
from schemas.student import StudentOut
|
||||
result = StudentOut.model_validate(student)
|
||||
db.commit()
|
||||
return result
|
||||
|
||||
|
||||
def delete_student(student_id: int, db: Session):
|
||||
with database_errors(db):
|
||||
# 1. 找到正常学生;不存在或已删除时返回 404。
|
||||
student = require_student(student_id, db)
|
||||
# 2. 只改删除标记,不调用 db.delete,不执行物理删除。
|
||||
student.is_deleted = 1
|
||||
# 3. 提交后,所有带 is_deleted == 0 的查询都会排除此学生。
|
||||
db.commit()
|
||||
return student_id
|
||||
```
|
||||
|
||||
注意:自动生成的学号若已被手动指定的学号占用,会返回 409 并回滚。自增 ID 在回滚后可能留下空号,这是正常现象。
|
||||
|
||||
## 四、接口层:`api/student.py`
|
||||
|
||||
```python
|
||||
from fastapi import APIRouter, Depends, Path, Query
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from core.database import get_db
|
||||
from dao import student as student_dao
|
||||
from schemas.student import (
|
||||
StudentCreate, StudentUpdate, StudentResponse,
|
||||
StudentListResponse, StudentDeleteResponse,
|
||||
)
|
||||
|
||||
student_router = APIRouter()
|
||||
|
||||
|
||||
@student_router.get("", response_model=StudentListResponse, summary="查询学生列表")
|
||||
def list_students(
|
||||
page: int = Query(default=1, ge=1, description="页码,从 1 开始"),
|
||||
page_size: int = Query(default=20, ge=1, le=100, description="每页条数"),
|
||||
student_no: str | None = Query(default=None, min_length=1, max_length=50),
|
||||
student_name: str | None = Query(default=None, min_length=1, max_length=50),
|
||||
class_id: int | None = Query(default=None, gt=0),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
# 1. FastAPI 校验查询参数,并通过 get_db 注入本次请求的数据库会话。
|
||||
# 2. DAO 按条件查询,返回总数和本页数据。
|
||||
total, rows = student_dao.get_student_list(
|
||||
db, page, page_size, student_no, student_name, class_id,
|
||||
)
|
||||
# 3. 返回分页信息,便于前端计算总页数。
|
||||
return StudentListResponse(
|
||||
total=total, page=page, page_size=page_size, data=rows,
|
||||
)
|
||||
|
||||
|
||||
@student_router.post("", response_model=StudentResponse, summary="新增学生")
|
||||
def create_student(request: StudentCreate, db: Session = Depends(get_db)):
|
||||
# 1. 请求体通过 StudentCreate 校验后转为字典。
|
||||
# 2. DAO 完成关联检查、学号生成和事务提交。
|
||||
result = student_dao.create_students(request.model_dump(), db)
|
||||
# 3. 返回完整数据,前端可以立即取得 id 和生成的 student_no。
|
||||
return StudentResponse(detail="添加学生成功", data=result)
|
||||
|
||||
|
||||
@student_router.get("/{id}", response_model=StudentResponse, summary="查询学生详情")
|
||||
def read_student(
|
||||
id: int = Path(gt=0),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
# 路径 id 必须是正整数;DAO 对不存在或已删除的记录返回 404。
|
||||
result = student_dao.get_student_detail(id, db)
|
||||
return StudentResponse(detail="查询成功", data=result)
|
||||
|
||||
|
||||
@student_router.put("/{id}", response_model=StudentResponse, summary="修改学生")
|
||||
def update_student(
|
||||
request: StudentUpdate,
|
||||
id: int = Path(gt=0),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
# 1. exclude_unset=True 只保留客户端实际传入的字段。
|
||||
# 不要换成 exclude_none=True,否则无法通过 null 清空顾问等字段。
|
||||
updates = request.model_dump(exclude_unset=True)
|
||||
# 2. DAO 校验目标学生、关联字段、学号和合并后的日期,再提交修改。
|
||||
result = student_dao.update_student(id, updates, db)
|
||||
return StudentResponse(detail="修改学生成功", data=result)
|
||||
|
||||
|
||||
@student_router.delete("/{id}", response_model=StudentDeleteResponse,
|
||||
summary="逻辑删除学生")
|
||||
def delete_student(
|
||||
id: int = Path(gt=0),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
# DAO 只修改删除标记,成功提交后返回被删除学生的主键。
|
||||
deleted_id = student_dao.delete_student(id, db)
|
||||
return StudentDeleteResponse(id=deleted_id)
|
||||
```
|
||||
|
||||
## 五、路由和数据库衔接
|
||||
|
||||
当前 `main.py` 已经有以下代码,保留即可,不要重复注册:
|
||||
|
||||
```python
|
||||
from api.student import student_router
|
||||
app.include_router(student_router, tags=["学生接口"], prefix="/students")
|
||||
```
|
||||
|
||||
路由文件内部使用空路径或 `/{id}`,是因为 `/students` 前缀由 `main.py` 统一添加。
|
||||
|
||||
`core.database.get_db()` 负责创建会话并在请求结束后关闭。写入操作由 DAO 提交或回滚,读取操作无需提交。模型继续使用 `core.database.Base`,不要重新创建 Base。
|
||||
|
||||
### 学号唯一性:当前模型需要补充的约束
|
||||
|
||||
当前 `models/student.py` 的 `student_no` 没有唯一约束。DAO 的预检查可以处理一般重复输入,但两个并发请求可能同时通过检查;要可靠防重,需配合数据库唯一约束。
|
||||
|
||||
模型中的字段可改为:
|
||||
|
||||
```python
|
||||
student_no = Column(VARCHAR(50), nullable=True, unique=True)
|
||||
```
|
||||
|
||||
保持允许 NULL,是因为自动编号流程先插入并获取 ID,再写入学号。
|
||||
|
||||
对于已经存在的表,修改模型或执行 `create_all()` 不会自动添加约束。先检查历史重复数据:
|
||||
|
||||
```sql
|
||||
SELECT student_no, COUNT(*) AS duplicate_count
|
||||
FROM student_info_detail
|
||||
WHERE student_no IS NOT NULL
|
||||
GROUP BY student_no
|
||||
HAVING COUNT(*) > 1;
|
||||
```
|
||||
|
||||
确认无重复且该唯一约束尚不存在后,再在项目使用的数据库中执行一次:
|
||||
|
||||
```sql
|
||||
ALTER TABLE student_info_detail
|
||||
ADD CONSTRAINT uk_student_no UNIQUE (student_no);
|
||||
```
|
||||
|
||||
本文没有执行以上 SQL。该约束包含已逻辑删除的数据,与 DAO 的学号占用规则一致。
|
||||
|
||||
## 六、调用示例
|
||||
|
||||
按项目当前入口启动后,可在 `http://127.0.0.1:23333/docs` 调试。需要先配置 MySQL、创建表,并准备一个真实存在且未删除的班级。以下 `class_id: 1`、路径中的学生 ID 均为示例,应替换为实际值。
|
||||
|
||||
### 1. 新增学生
|
||||
|
||||
`POST /students`
|
||||
|
||||
```json
|
||||
{
|
||||
"student_name": "张三",
|
||||
"class_id": 1,
|
||||
"enrollment_date": "2026-09-01",
|
||||
"age": 20,
|
||||
"gender": "男",
|
||||
"major": "软件技术"
|
||||
}
|
||||
```
|
||||
|
||||
成功 HTTP 状态为 200,响应包含 `code`、`detail` 和 `data`,其中 `data.id`、`data.student_no` 是后续操作使用的主键和学号。这里保留原接口成功返回 200 的约定。
|
||||
|
||||
### 2. 查询列表
|
||||
|
||||
```text
|
||||
GET /students?page=1&page_size=20
|
||||
GET /students?student_no=STU0001
|
||||
GET /students?student_name=张&class_id=1
|
||||
```
|
||||
|
||||
响应包含 `total`、`page`、`page_size` 和 `data` 数组。超出实际页数时,`total` 仍为匹配总数,`data` 为空。
|
||||
|
||||
### 3. 查询详情
|
||||
|
||||
```text
|
||||
GET /students/1
|
||||
```
|
||||
|
||||
### 4. 修改学生
|
||||
|
||||
`PUT /students/1`
|
||||
|
||||
```json
|
||||
{
|
||||
"student_name": "张三丰",
|
||||
"age": 21,
|
||||
"consultant_id": null
|
||||
}
|
||||
```
|
||||
|
||||
这次只更新姓名、年龄和顾问;顾问关联被清空,班级、日期等未传字段保持原值。
|
||||
|
||||
### 5. 逻辑删除
|
||||
|
||||
```text
|
||||
DELETE /students/1
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"detail": "删除学生成功",
|
||||
"id": 1
|
||||
}
|
||||
```
|
||||
|
||||
删除后数据库行仍存在,`is_deleted` 为 1;后续详情、更新、再次删除均返回 404。
|
||||
|
||||
## 七、验证清单与错误说明
|
||||
|
||||
| 场景 | 预期 |
|
||||
| --- | --- |
|
||||
| 不传学号新增 | 自动生成学号,响应带主键与学生信息 |
|
||||
| 指定已占用学号新增或修改 | 409,事务回滚 |
|
||||
| 班级不存在或已删除 | 422 |
|
||||
| 顾问不存在或已删除 | 422 |
|
||||
| 姓名为空白、年龄越界、日期格式错误 | 422 |
|
||||
| 毕业日期早于入学日期 | 422,新增与部分更新都检查 |
|
||||
| 查询参数 page=0 或 page_size=101 | 422 |
|
||||
| 同时按学号、姓名和班级筛选 | 返回满足全部条件的未删除学生 |
|
||||
| 查询无匹配记录 | 200,total=0,data=[] |
|
||||
| 更新仅传 age | 只更新年龄,其他字段保留 |
|
||||
| 更新 consultant_id=null | 清空顾问关联 |
|
||||
| 更新 student_name=null 或空请求体 | 422 |
|
||||
| 查询、修改或删除不存在的学生 | 404 |
|
||||
| 删除已有学生后重新查询 | 列表不可见,详情为 404 |
|
||||
| 数据库操作失败 | 500,写事务回滚,服务端记录日志 |
|
||||
|
||||
主动抛出的业务错误通常返回 `{"detail": "错误说明"}`;FastAPI 参数校验错误的 `detail` 是包含字段位置的数组。这两种结构应由前端分别处理。
|
||||
|
||||
以上是供接入项目的实现代码和验证步骤;文档生成时未连接真实 MySQL,实际数据库约束、历史数据和接口联调仍需按清单验证。
|
||||
Reference in New Issue
Block a user