493 lines
12 KiB
Markdown
493 lines
12 KiB
Markdown
# ExamService
|
||
|
||
题库小程序服务端 - FastAPI实现
|
||
|
||
## 项目结构
|
||
|
||
```
|
||
ExamService/
|
||
├── alembic/ # 数据库迁移相关
|
||
├── mooc/ # 主应用目录
|
||
│ ├── api/ # API路由
|
||
│ │ └── v1/ # API v1版本
|
||
│ ├── core/ # 核心配置
|
||
│ ├── crud/ # 数据库操作
|
||
│ ├── db/ # 数据库
|
||
│ ├── models/ # 数据库模型
|
||
│ ├── schemas/ # Pydantic模型
|
||
│ └── utils/ # 工具函数
|
||
├── tests/ # 测试目录
|
||
└── [配置文件]
|
||
```
|
||
|
||
|
||
## 安装
|
||
|
||
### 1. 安装 Miniconda
|
||
|
||
在 Ubuntu 上安装 Miniconda,执行以下步骤:
|
||
|
||
```bash
|
||
# 下载 Miniconda 安装脚本
|
||
wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh
|
||
|
||
# 给安装脚本添加执行权限
|
||
chmod +x Miniconda3-latest-Linux-x86_64.sh
|
||
|
||
# 运行安装脚本
|
||
./Miniconda3-latest-Linux-x86_64.sh
|
||
|
||
# 按照提示完成安装,选择安装目录并接受许可协议
|
||
|
||
# 激活 Miniconda
|
||
source ~/.bashrc
|
||
# 创建名为 'mooc' 的虚拟环境,Python 版本大于等于 3.13
|
||
conda create --name mooc python>=3.13
|
||
# 激活 'mooc' 环境
|
||
conda activate mooc
|
||
# 安装依赖
|
||
pip install -r requirements.txt
|
||
```
|
||
|
||
### 安装mysql开发库
|
||
|
||
```bash
|
||
# 更新包列表
|
||
apt-get update
|
||
|
||
# 安装 MySQL 开发包和其他必要的包
|
||
apt-get install -y python3-dev default-libmysqlclient-dev build-essential pkg-config
|
||
|
||
|
||
pip install mysqlclient pymysql cryptography
|
||
```
|
||
|
||
## 运行
|
||
|
||
```bash
|
||
uvicorn main:app --reload
|
||
```
|
||
|
||
## 初始化数据库
|
||
|
||
```bash
|
||
alembic init alembic
|
||
alembic revision --autogenerate -m "Initial migration"
|
||
alembic upgrade head
|
||
```
|
||
|
||
## 数据库管理
|
||
|
||
### 添加新的数据库表
|
||
|
||
1. 创建模型文件:
|
||
|
||
将同类型的表模型放在同一个文件中。文件命名规则:去除表名的 `ims_` 前缀,例如:
|
||
- `ims_account_wechats`、`ims_account_wxapp` 等表 → `account.py`
|
||
- `ims_uni_account`、`ims_uni_settings` 等表 → `uni_account.py`
|
||
|
||
```python
|
||
# filepath: mooc/models/account.py
|
||
from sqlalchemy import Column, Integer, String, SmallInteger
|
||
from mooc.db.database import Base
|
||
|
||
class AccountWechats(Base):
|
||
"""微信公众号账号表"""
|
||
__tablename__ = "ims_account_wechats"
|
||
|
||
acid = Column(Integer, primary_key=True)
|
||
uniacid = Column(Integer, nullable=False)
|
||
# ... 其他字段
|
||
```
|
||
|
||
2. 在 `mooc/models/__init__.py` 中注册表:
|
||
|
||
```python
|
||
# 添加到 expected_tables 集合中
|
||
expected_tables = {
|
||
# ... 现有表 ...
|
||
'ims_account_wechats', # 新添加的表
|
||
}
|
||
```
|
||
|
||
3. 导出模型类:
|
||
|
||
```python
|
||
# 在 mooc/models/__init__.py 中添加导出
|
||
from mooc.models.account import AccountWechats
|
||
```
|
||
|
||
4. 创建 Schema:
|
||
|
||
```python
|
||
# filepath: mooc/schemas/account.py
|
||
from pydantic import BaseModel
|
||
|
||
class AccountWechatsBase(BaseModel):
|
||
uniacid: int
|
||
# ... 其他字段
|
||
|
||
class AccountWechatsCreate(AccountWechatsBase):
|
||
pass
|
||
|
||
class AccountWechatsUpdate(BaseModel):
|
||
# 可选字段用于更新
|
||
uniacid: Optional[int] = None
|
||
|
||
class AccountWechatsRead(AccountWechatsBase):
|
||
acid: int
|
||
|
||
class Config:
|
||
from_attributes = True
|
||
```
|
||
|
||
5. 创建 CRUD 操作:
|
||
|
||
```python
|
||
# filepath: mooc/crud/crud_account.py
|
||
from mooc.crud.crud_base import CRUDBase
|
||
|
||
class CRUDAccountWechats(CRUDBase[AccountWechats, AccountWechatsCreate, AccountWechatsUpdate]):
|
||
def get_by_uniacid(self, db: Session, *, uniacid: int) -> Optional[AccountWechats]:
|
||
return self.get_by_field(db, "uniacid", uniacid)
|
||
|
||
# 创建实例
|
||
account_wechats = CRUDAccountWechats(AccountWechats)
|
||
```
|
||
|
||
6. 添加 API 路由:
|
||
|
||
```python
|
||
# filepath: mooc/api/v1/endpoints/account.py
|
||
@account_router.post("/wechats", response_model=AccountWechatsRead)
|
||
def create_wechat_account(
|
||
*,
|
||
db: Session = Depends(deps.get_db),
|
||
account_in: AccountWechatsCreate,
|
||
):
|
||
"""创建微信公众号账号"""
|
||
return account_wechats.create(db=db, obj_in=account_in)
|
||
```
|
||
|
||
### 数据库表自动创建
|
||
|
||
应用启动时会自动执行以下操作:
|
||
1. 导入并验证所有模型定义
|
||
2. 检查数据库中缺失的表
|
||
3. 自动创建缺失的表
|
||
|
||
你可以通过查看应用日志了解表创建的详细情况:
|
||
```bash
|
||
python main.py
|
||
```
|
||
|
||
### 注意事项
|
||
|
||
1. 模型类名采用帕斯卡命名法(PascalCase),去除 `ims_` 前缀
|
||
2. 表名(`__tablename__`)必须与数据库中的实际表名完全一致
|
||
3. 字段类型要与数据库定义严格匹配
|
||
4. 确保主键和索引的正确定义
|
||
5. 添加适当的文档字符串说明表的用途
|
||
|
||
### 调试帮助
|
||
|
||
如果遇到问题,可以:
|
||
1. 检查日志输出了解具体错误
|
||
2. 使用 `get_all_table_names()` 查看已注册的表
|
||
3. 验证模型定义是否完整
|
||
4. 确认数据库连接配置是否正确
|
||
|
||
## CRUDBase 使用指南
|
||
|
||
### 简介
|
||
|
||
CRUDBase是一个通用的CRUD基类,提供了基础的增删改查操作。通过继承这个基类,可以快速实现特定模型的CRUD操作。
|
||
|
||
### 使用方法
|
||
|
||
1. 创建CRUD类:
|
||
|
||
```python
|
||
from mooc.crud.base import CRUDBase
|
||
from mooc.models.your_model import YourModel
|
||
from mooc.schemas.your_schema import YourCreateSchema, YourUpdateSchema
|
||
|
||
class CRUDYourModel(CRUDBase[YourModel, YourCreateSchema, YourUpdateSchema]):
|
||
pass # 继承基础功能即可,如需自定义方法可以在这里添加
|
||
```
|
||
|
||
2. 实例化CRUD对象:
|
||
|
||
```python
|
||
crud_your_model = CRUDYourModel(YourModel)
|
||
```
|
||
|
||
### 基础操作示例
|
||
|
||
```python
|
||
# 创建记录
|
||
new_item = crud_your_model.create(db, obj_in=item_create_schema)
|
||
|
||
# 获取单条记录
|
||
item = crud_your_model.get(db, id=123)
|
||
|
||
# 获取多条记录
|
||
items = crud_your_model.get_multi(db, skip=0, limit=100)
|
||
|
||
# 更新记录
|
||
updated_item = crud_your_model.update(db, db_obj=existing_item, obj_in=item_update_schema)
|
||
|
||
# 删除记录
|
||
crud_your_model.remove(db, id=123)
|
||
```
|
||
|
||
### 自定义方法示例
|
||
|
||
```python
|
||
class CRUDYourModel(CRUDBase[YourModel, YourCreateSchema, YourUpdateSchema]):
|
||
def get_by_custom_field(self, db: Session, field_value: str) -> Optional[YourModel]:
|
||
return db.query(self.model).filter(self.model.custom_field == field_value).first()
|
||
```
|
||
|
||
### 注意事项
|
||
|
||
1. CRUDBase需要三个类型参数:
|
||
- ModelType: SQLAlchemy模型类
|
||
- CreateSchemaType: Pydantic创建模型
|
||
- UpdateSchemaType: Pydantic更新模型
|
||
|
||
2. 所有方法都需要传入数据库会话(db: Session)参数
|
||
|
||
3. 更新操作支持两种方式:
|
||
- 字典形式:`update(db, db_obj=item, obj_in={"field": "new_value"})`
|
||
- Pydantic模型:`update(db, db_obj=item, obj_in=ItemUpdate(field="new_value"))`
|
||
|
||
## API文档
|
||
|
||
启动服务后访问: http://localhost:2333/docs
|
||
|
||
### 使用 AI 生成模型代码
|
||
|
||
当你有一个新的数据库表需要添加时,可以使用以下方式让 AI 帮助生成代码:
|
||
|
||
1. 准备 SQL 建表语句,例如:
|
||
```sql
|
||
CREATE TABLE `ims_uni_settings` (
|
||
`uniacid` int(10) UNSIGNED NOT NULL,
|
||
`passport` varchar(200) NOT NULL,
|
||
`oauth` varchar(100) NOT NULL,
|
||
-- ... 其他字段
|
||
PRIMARY KEY (`uniacid`)
|
||
) ENGINE = MyISAM CHARACTER SET = utf8 COLLATE = utf8_general_ci;
|
||
```
|
||
|
||
2. 使用以下提示词模板:
|
||
```
|
||
请帮我为这个表生成 FastAPI + SQLAlchemy 的实现,包括:
|
||
1. SQLAlchemy 模型(models/xxx.py)
|
||
2. Pydantic Schema(schemas/xxx.py)
|
||
3. CRUD 操作(crud/crud_xxx.py)
|
||
4. API 路由(api/v1/endpoints/xxx.py)
|
||
|
||
要求:
|
||
- 遵循项目的命名规范(去除ims_前缀,使用帕斯卡命名法)
|
||
- 确保字段类型映射正确
|
||
- 包含必要的文档注释
|
||
- 实现基本的 CRUD 操作和常用查询方法
|
||
|
||
[SQL建表语句]
|
||
```
|
||
|
||
3. 对生成的代码进行以下检查:
|
||
- 表名是否与数据库一致
|
||
- 字段类型是否正确映射
|
||
- 主键和索引是否正确定义
|
||
- 是否符合项目的命名规范
|
||
- CRUD 方法是否满足业务需求
|
||
|
||
4. 常用的 SQL 类型映射参考:
|
||
```python
|
||
# MySQL 到 SQLAlchemy 的类型映射
|
||
int/bigint -> Integer
|
||
varchar/char -> String(length)
|
||
text -> Text
|
||
datetime -> DateTime
|
||
timestamp -> TIMESTAMP
|
||
tinyint -> SmallInteger
|
||
decimal -> DECIMAL(precision, scale)
|
||
```
|
||
|
||
5. 示例提示词:
|
||
```
|
||
我需要为以下数据表生成 FastAPI 实现:
|
||
- 表名:ims_uni_settings
|
||
- 主键:uniacid
|
||
- 特殊需求:
|
||
* 需要通过 uniacid 查询的方法
|
||
* 需要处理 Text 类型的 payment 字段
|
||
* 所有字符串字段都是必填的
|
||
|
||
[SQL语句]
|
||
...
|
||
|
||
请生成完整的代码实现。
|
||
```
|
||
|
||
6. 代码生成后的集成步骤:
|
||
- 将模型类添加到对应的 models 文件
|
||
- 在 models/__init__.py 中注册表名
|
||
- 在 api.py 中注册新的路由
|
||
- 检查并测试生成的 API 端点
|
||
|
||
这种方式可以:
|
||
- 快速生成符合项目规范的代码
|
||
- 确保类型映射的准确性
|
||
- 保持代码结构的一致性
|
||
- 减少手动编码的错误
|
||
|
||
|
||
|
||
|
||
## PHP 到 FastAPI 迁移指南
|
||
|
||
### 全局变量对应关系
|
||
|
||
1. `$_W` (WeEngine全局配置):
|
||
```php
|
||
$_W = array(
|
||
'config' => array(), // 系统配置信息
|
||
'timestamp' => time(), // 当前时间戳
|
||
'charset' => 'utf8', // 字符集
|
||
'clientip' => '', // 客户端IP
|
||
'uniacid' => 0, // 当前统一公众号ID
|
||
'acid' => 0, // 当前账号ID
|
||
'uid' => 0, // 当前用户ID
|
||
'isajax' => false, // 是否AJAX请求
|
||
'ispost' => false, // 是否POST请求
|
||
'siteroot' => '', // 站点根目录URL
|
||
'siteurl' => '', // 当前URL
|
||
'attachurl' => '', // 附件URL
|
||
'setting' => array(), // 站点设置
|
||
'module' => array() // 当前模块信息
|
||
);
|
||
```
|
||
|
||
2. `$_GPC` (全局请求参数):
|
||
```php
|
||
$_GPC = array_merge(
|
||
$_GET, // GET参数
|
||
$_POST, // POST参数
|
||
$_COOKIE // COOKIE数据
|
||
);
|
||
```
|
||
|
||
### 迁移方法步骤
|
||
|
||
以迁移 `dopageLogin` 为例:
|
||
|
||
1. 在 `wxapp.py` 中添加路由:
|
||
```python
|
||
@wxapp_router.post("/index")
|
||
async def handle_wxapp_request(
|
||
request: Request,
|
||
i: str, # uniacid
|
||
do: Optional[str] = None,
|
||
db: Session = Depends(get_db)
|
||
):
|
||
# 处理不同的 do 参数
|
||
if do == "Login":
|
||
return await handle_login(request, i, db)
|
||
```
|
||
|
||
2. 创建请求数据模型:
|
||
```python
|
||
class LoginRequest(BaseModel):
|
||
code: str
|
||
encryptedData: Optional[str] = None
|
||
iv: Optional[str] = None
|
||
```
|
||
|
||
3. 创建处理函数:
|
||
```python
|
||
async def handle_login(request: Request, uniacid: str, db: Session):
|
||
# 获取所有参数
|
||
params = await get_all_params(request)
|
||
|
||
# 验证必要参数
|
||
if not params.get("code"):
|
||
return {"code": 1, "msg": "code值获取失败"}
|
||
|
||
# 获取小程序配置
|
||
account = await get_account_info(db, uniacid)
|
||
|
||
# 业务逻辑处理
|
||
try:
|
||
result = await process_login(
|
||
db,
|
||
code=params["code"],
|
||
appid=account.key,
|
||
secret=account.secret,
|
||
encrypted_data=params.get("encryptedData"),
|
||
iv=params.get("iv")
|
||
)
|
||
return {"code": 0, "msg": "登录成功", "data": result}
|
||
except Exception as e:
|
||
return {"code": 1, "msg": str(e)}
|
||
```
|
||
|
||
4. 实现数据库操作:
|
||
```python
|
||
# 在 crud/crud_goouc_fullexam_user.py 中
|
||
class CRUDUser(CRUDBase[User, UserCreate, UserUpdate]):
|
||
async def get_by_openid(self, db: Session, *, openid: str) -> Optional[User]:
|
||
return db.query(self.model).filter(
|
||
self.model.openid == openid,
|
||
self.model.istatus == 1
|
||
).first()
|
||
```
|
||
|
||
5. 实现业务逻辑:
|
||
```python
|
||
# 在 services/user.py 中
|
||
async def process_login(
|
||
db: Session,
|
||
code: str,
|
||
appid: str,
|
||
secret: str,
|
||
encrypted_data: Optional[str] = None,
|
||
iv: Optional[str] = None
|
||
) -> Dict:
|
||
# 1. 调用微信API获取session_key和openid
|
||
# 2. 解密用户信息
|
||
# 3. 查找或创建用户
|
||
# 4. 更新登录时间
|
||
# 5. 返回结果
|
||
```
|
||
|
||
### 注意事项
|
||
|
||
1. 参数获取:
|
||
- PHP: `$_GPC["param"]`
|
||
- FastAPI: `params = await get_all_params(request)`
|
||
|
||
2. 数据库操作:
|
||
- PHP: `pdo_fetch("SELECT * FROM " . tablename($this->t_user))`
|
||
- FastAPI: `db.query(User).filter(User.id == uid).first()`
|
||
|
||
3. 返回格式:
|
||
- PHP: `$this->result(0, "success", $data)`
|
||
- FastAPI: `{"code": 0, "msg": "success", "data": data}`
|
||
|
||
4. 错误处理:
|
||
```python
|
||
try:
|
||
# 业务逻辑
|
||
return {"code": 0, "msg": "success", "data": result}
|
||
except Exception as e:
|
||
return {"code": 1, "msg": str(e)}
|
||
```
|
||
|
||
5. 表名处理:
|
||
- PHP: `tablename($this->t_user)`
|
||
- FastAPI: 在模型定义中指定 `__tablename__ = "ims_goouc_fullexam_user"`
|