feat: 初始化考培练系统项目

- 从服务器拉取完整代码
- 按框架规范整理项目结构
- 配置 Drone CI 测试环境部署
- 包含后端(FastAPI)、前端(Vue3)、管理端

技术栈: Vue3 + TypeScript + FastAPI + MySQL
This commit is contained in:
111
2026-01-24 19:33:28 +08:00
commit 998211c483
1197 changed files with 228429 additions and 0 deletions

View File

@@ -0,0 +1,637 @@
"""
AI 提示词管理 API
"""
import os
import json
from typing import Optional, List
import pymysql
from fastapi import APIRouter, Depends, HTTPException, status, Query
from .auth import get_current_admin, require_superadmin, get_db_connection, AdminUserInfo
from .schemas import (
AIPromptCreate,
AIPromptUpdate,
AIPromptResponse,
AIPromptVersionResponse,
TenantPromptResponse,
TenantPromptUpdate,
ResponseModel,
)
router = APIRouter(prefix="/prompts", tags=["提示词管理"])
def log_operation(cursor, admin: AdminUserInfo, tenant_id: int, tenant_code: str,
operation_type: str, resource_type: str, resource_id: int,
resource_name: str, old_value: dict = None, new_value: dict = None):
"""记录操作日志"""
cursor.execute(
"""
INSERT INTO operation_logs
(admin_user_id, admin_username, tenant_id, tenant_code, operation_type,
resource_type, resource_id, resource_name, old_value, new_value)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
""",
(admin.id, admin.username, tenant_id, tenant_code, operation_type,
resource_type, resource_id, resource_name,
json.dumps(old_value, ensure_ascii=False) if old_value else None,
json.dumps(new_value, ensure_ascii=False) if new_value else None)
)
@router.get("", response_model=List[AIPromptResponse], summary="获取提示词列表")
async def list_prompts(
module: Optional[str] = Query(None, description="模块筛选"),
is_active: Optional[bool] = Query(None, description="是否启用"),
admin: AdminUserInfo = Depends(get_current_admin),
):
"""
获取所有 AI 提示词模板
- **module**: 模块筛选course, exam, practice, ability
- **is_active**: 是否启用
"""
conn = get_db_connection()
try:
with conn.cursor() as cursor:
conditions = []
params = []
if module:
conditions.append("module = %s")
params.append(module)
if is_active is not None:
conditions.append("is_active = %s")
params.append(is_active)
where_clause = " AND ".join(conditions) if conditions else "1=1"
cursor.execute(
f"""
SELECT * FROM ai_prompts
WHERE {where_clause}
ORDER BY module, id
""",
params
)
rows = cursor.fetchall()
result = []
for row in rows:
# 解析 JSON 字段
variables = None
if row.get("variables"):
try:
variables = json.loads(row["variables"])
except:
pass
output_schema = None
if row.get("output_schema"):
try:
output_schema = json.loads(row["output_schema"])
except:
pass
result.append(AIPromptResponse(
id=row["id"],
code=row["code"],
name=row["name"],
description=row["description"],
module=row["module"],
system_prompt=row["system_prompt"],
user_prompt_template=row["user_prompt_template"],
variables=variables,
output_schema=output_schema,
model_recommendation=row["model_recommendation"],
max_tokens=row["max_tokens"],
temperature=float(row["temperature"]) if row["temperature"] else 0.7,
is_system=row["is_system"],
is_active=row["is_active"],
version=row["version"],
created_at=row["created_at"],
updated_at=row["updated_at"],
))
return result
finally:
conn.close()
@router.get("/{prompt_id}", response_model=AIPromptResponse, summary="获取提示词详情")
async def get_prompt(
prompt_id: int,
admin: AdminUserInfo = Depends(get_current_admin),
):
"""获取提示词详情"""
conn = get_db_connection()
try:
with conn.cursor() as cursor:
cursor.execute("SELECT * FROM ai_prompts WHERE id = %s", (prompt_id,))
row = cursor.fetchone()
if not row:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="提示词不存在",
)
# 解析 JSON 字段
variables = None
if row.get("variables"):
try:
variables = json.loads(row["variables"])
except:
pass
output_schema = None
if row.get("output_schema"):
try:
output_schema = json.loads(row["output_schema"])
except:
pass
return AIPromptResponse(
id=row["id"],
code=row["code"],
name=row["name"],
description=row["description"],
module=row["module"],
system_prompt=row["system_prompt"],
user_prompt_template=row["user_prompt_template"],
variables=variables,
output_schema=output_schema,
model_recommendation=row["model_recommendation"],
max_tokens=row["max_tokens"],
temperature=float(row["temperature"]) if row["temperature"] else 0.7,
is_system=row["is_system"],
is_active=row["is_active"],
version=row["version"],
created_at=row["created_at"],
updated_at=row["updated_at"],
)
finally:
conn.close()
@router.post("", response_model=AIPromptResponse, summary="创建提示词")
async def create_prompt(
data: AIPromptCreate,
admin: AdminUserInfo = Depends(require_superadmin),
):
"""
创建新的提示词模板
需要超级管理员权限
"""
conn = get_db_connection()
try:
with conn.cursor() as cursor:
# 检查编码是否已存在
cursor.execute("SELECT id FROM ai_prompts WHERE code = %s", (data.code,))
if cursor.fetchone():
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="提示词编码已存在",
)
# 创建提示词
cursor.execute(
"""
INSERT INTO ai_prompts
(code, name, description, module, system_prompt, user_prompt_template,
variables, output_schema, model_recommendation, max_tokens, temperature,
is_system, created_by)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, FALSE, %s)
""",
(data.code, data.name, data.description, data.module,
data.system_prompt, data.user_prompt_template,
json.dumps(data.variables) if data.variables else None,
json.dumps(data.output_schema) if data.output_schema else None,
data.model_recommendation, data.max_tokens, data.temperature,
admin.id)
)
prompt_id = cursor.lastrowid
# 记录操作日志
log_operation(
cursor, admin, None, None,
"create", "prompt", prompt_id, data.name,
new_value=data.model_dump()
)
conn.commit()
return await get_prompt(prompt_id, admin)
finally:
conn.close()
@router.put("/{prompt_id}", response_model=AIPromptResponse, summary="更新提示词")
async def update_prompt(
prompt_id: int,
data: AIPromptUpdate,
admin: AdminUserInfo = Depends(get_current_admin),
):
"""
更新提示词模板
更新会自动保存版本历史
"""
conn = get_db_connection()
try:
with conn.cursor() as cursor:
# 获取原提示词
cursor.execute("SELECT * FROM ai_prompts WHERE id = %s", (prompt_id,))
old_prompt = cursor.fetchone()
if not old_prompt:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="提示词不存在",
)
# 保存版本历史(如果系统提示词或用户提示词有变化)
if data.system_prompt or data.user_prompt_template:
new_version = old_prompt["version"] + 1
cursor.execute(
"""
INSERT INTO ai_prompt_versions
(prompt_id, version, system_prompt, user_prompt_template, variables,
output_schema, change_summary, created_by)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
""",
(prompt_id, old_prompt["version"],
old_prompt["system_prompt"], old_prompt["user_prompt_template"],
old_prompt["variables"], old_prompt["output_schema"],
f"版本 {old_prompt['version']} 备份",
admin.id)
)
else:
new_version = old_prompt["version"]
# 构建更新语句
update_fields = []
update_values = []
if data.name is not None:
update_fields.append("name = %s")
update_values.append(data.name)
if data.description is not None:
update_fields.append("description = %s")
update_values.append(data.description)
if data.system_prompt is not None:
update_fields.append("system_prompt = %s")
update_values.append(data.system_prompt)
if data.user_prompt_template is not None:
update_fields.append("user_prompt_template = %s")
update_values.append(data.user_prompt_template)
if data.variables is not None:
update_fields.append("variables = %s")
update_values.append(json.dumps(data.variables))
if data.output_schema is not None:
update_fields.append("output_schema = %s")
update_values.append(json.dumps(data.output_schema))
if data.model_recommendation is not None:
update_fields.append("model_recommendation = %s")
update_values.append(data.model_recommendation)
if data.max_tokens is not None:
update_fields.append("max_tokens = %s")
update_values.append(data.max_tokens)
if data.temperature is not None:
update_fields.append("temperature = %s")
update_values.append(data.temperature)
if data.is_active is not None:
update_fields.append("is_active = %s")
update_values.append(data.is_active)
if not update_fields:
return await get_prompt(prompt_id, admin)
# 更新版本号
if data.system_prompt or data.user_prompt_template:
update_fields.append("version = %s")
update_values.append(new_version)
update_fields.append("updated_by = %s")
update_values.append(admin.id)
update_values.append(prompt_id)
cursor.execute(
f"UPDATE ai_prompts SET {', '.join(update_fields)} WHERE id = %s",
update_values
)
# 记录操作日志
log_operation(
cursor, admin, None, None,
"update", "prompt", prompt_id, old_prompt["name"],
old_value={"version": old_prompt["version"]},
new_value=data.model_dump(exclude_unset=True)
)
conn.commit()
return await get_prompt(prompt_id, admin)
finally:
conn.close()
@router.get("/{prompt_id}/versions", response_model=List[AIPromptVersionResponse], summary="获取提示词版本历史")
async def get_prompt_versions(
prompt_id: int,
admin: AdminUserInfo = Depends(get_current_admin),
):
"""获取提示词的版本历史"""
conn = get_db_connection()
try:
with conn.cursor() as cursor:
cursor.execute(
"""
SELECT * FROM ai_prompt_versions
WHERE prompt_id = %s
ORDER BY version DESC
""",
(prompt_id,)
)
rows = cursor.fetchall()
result = []
for row in rows:
variables = None
if row.get("variables"):
try:
variables = json.loads(row["variables"])
except:
pass
result.append(AIPromptVersionResponse(
id=row["id"],
prompt_id=row["prompt_id"],
version=row["version"],
system_prompt=row["system_prompt"],
user_prompt_template=row["user_prompt_template"],
variables=variables,
change_summary=row["change_summary"],
created_at=row["created_at"],
))
return result
finally:
conn.close()
@router.post("/{prompt_id}/rollback/{version}", response_model=AIPromptResponse, summary="回滚提示词版本")
async def rollback_prompt_version(
prompt_id: int,
version: int,
admin: AdminUserInfo = Depends(get_current_admin),
):
"""回滚到指定版本的提示词"""
conn = get_db_connection()
try:
with conn.cursor() as cursor:
# 获取指定版本
cursor.execute(
"""
SELECT * FROM ai_prompt_versions
WHERE prompt_id = %s AND version = %s
""",
(prompt_id, version)
)
version_row = cursor.fetchone()
if not version_row:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="版本不存在",
)
# 获取当前提示词
cursor.execute("SELECT * FROM ai_prompts WHERE id = %s", (prompt_id,))
current = cursor.fetchone()
if not current:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="提示词不存在",
)
# 保存当前版本到历史
new_version = current["version"] + 1
cursor.execute(
"""
INSERT INTO ai_prompt_versions
(prompt_id, version, system_prompt, user_prompt_template, variables,
output_schema, change_summary, created_by)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
""",
(prompt_id, current["version"],
current["system_prompt"], current["user_prompt_template"],
current["variables"], current["output_schema"],
f"回滚前备份(版本 {current['version']}",
admin.id)
)
# 回滚
cursor.execute(
"""
UPDATE ai_prompts
SET system_prompt = %s, user_prompt_template = %s, variables = %s,
output_schema = %s, version = %s, updated_by = %s
WHERE id = %s
""",
(version_row["system_prompt"], version_row["user_prompt_template"],
version_row["variables"], version_row["output_schema"],
new_version, admin.id, prompt_id)
)
# 记录操作日志
log_operation(
cursor, admin, None, None,
"rollback", "prompt", prompt_id, current["name"],
old_value={"version": current["version"]},
new_value={"version": new_version, "rollback_from": version}
)
conn.commit()
return await get_prompt(prompt_id, admin)
finally:
conn.close()
@router.get("/tenants/{tenant_id}", response_model=List[TenantPromptResponse], summary="获取租户自定义提示词")
async def get_tenant_prompts(
tenant_id: int,
admin: AdminUserInfo = Depends(get_current_admin),
):
"""获取租户的自定义提示词列表"""
conn = get_db_connection()
try:
with conn.cursor() as cursor:
cursor.execute(
"""
SELECT tp.*, ap.code as prompt_code, ap.name as prompt_name
FROM tenant_prompts tp
JOIN ai_prompts ap ON tp.prompt_id = ap.id
WHERE tp.tenant_id = %s
ORDER BY ap.module, ap.id
""",
(tenant_id,)
)
rows = cursor.fetchall()
return [
TenantPromptResponse(
id=row["id"],
tenant_id=row["tenant_id"],
prompt_id=row["prompt_id"],
prompt_code=row["prompt_code"],
prompt_name=row["prompt_name"],
system_prompt=row["system_prompt"],
user_prompt_template=row["user_prompt_template"],
is_active=row["is_active"],
created_at=row["created_at"],
updated_at=row["updated_at"],
)
for row in rows
]
finally:
conn.close()
@router.put("/tenants/{tenant_id}/{prompt_id}", response_model=ResponseModel, summary="更新租户自定义提示词")
async def update_tenant_prompt(
tenant_id: int,
prompt_id: int,
data: TenantPromptUpdate,
admin: AdminUserInfo = Depends(get_current_admin),
):
"""创建或更新租户的自定义提示词"""
conn = get_db_connection()
try:
with conn.cursor() as cursor:
# 验证租户存在
cursor.execute("SELECT code FROM tenants WHERE id = %s", (tenant_id,))
tenant = cursor.fetchone()
if not tenant:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="租户不存在",
)
# 验证提示词存在
cursor.execute("SELECT name FROM ai_prompts WHERE id = %s", (prompt_id,))
prompt = cursor.fetchone()
if not prompt:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="提示词不存在",
)
# 检查是否已有自定义
cursor.execute(
"""
SELECT id FROM tenant_prompts
WHERE tenant_id = %s AND prompt_id = %s
""",
(tenant_id, prompt_id)
)
existing = cursor.fetchone()
if existing:
# 更新
update_fields = []
update_values = []
if data.system_prompt is not None:
update_fields.append("system_prompt = %s")
update_values.append(data.system_prompt)
if data.user_prompt_template is not None:
update_fields.append("user_prompt_template = %s")
update_values.append(data.user_prompt_template)
if data.is_active is not None:
update_fields.append("is_active = %s")
update_values.append(data.is_active)
if update_fields:
update_fields.append("updated_by = %s")
update_values.append(admin.id)
update_values.append(existing["id"])
cursor.execute(
f"UPDATE tenant_prompts SET {', '.join(update_fields)} WHERE id = %s",
update_values
)
else:
# 创建
cursor.execute(
"""
INSERT INTO tenant_prompts
(tenant_id, prompt_id, system_prompt, user_prompt_template, is_active, created_by)
VALUES (%s, %s, %s, %s, %s, %s)
""",
(tenant_id, prompt_id, data.system_prompt, data.user_prompt_template,
data.is_active if data.is_active is not None else True, admin.id)
)
# 记录操作日志
log_operation(
cursor, admin, tenant_id, tenant["code"],
"update", "tenant_prompt", prompt_id, prompt["name"],
new_value=data.model_dump(exclude_unset=True)
)
conn.commit()
return ResponseModel(message="自定义提示词已保存")
finally:
conn.close()
@router.delete("/tenants/{tenant_id}/{prompt_id}", response_model=ResponseModel, summary="删除租户自定义提示词")
async def delete_tenant_prompt(
tenant_id: int,
prompt_id: int,
admin: AdminUserInfo = Depends(get_current_admin),
):
"""删除租户的自定义提示词(恢复使用默认)"""
conn = get_db_connection()
try:
with conn.cursor() as cursor:
cursor.execute(
"""
DELETE FROM tenant_prompts
WHERE tenant_id = %s AND prompt_id = %s
""",
(tenant_id, prompt_id)
)
if cursor.rowcount == 0:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="自定义提示词不存在",
)
conn.commit()
return ResponseModel(message="自定义提示词已删除,将使用默认模板")
finally:
conn.close()