feat: add user role system and public joke submission API
- Add AdminUser role field (admin/submitter) - User management in admin panel - Public joke submission API (no auth required) - Document submission methods for external workers Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
ceed63fcb0
commit
aecf1f1f15
@@ -0,0 +1,7 @@
|
||||
import request from './request'
|
||||
|
||||
export const getUsers = () => request.get('/admin/users')
|
||||
|
||||
export const createUser = (data) => request.post('/admin/users', data)
|
||||
|
||||
export const deleteUser = (id) => request.delete(`/admin/users/${id}`)
|
||||
@@ -20,12 +20,13 @@ const routes = [
|
||||
{ path: 'links', name: 'LinkList', component: () => import('@/views/links/List.vue') },
|
||||
{ path: 'links/edit/:id?', name: 'LinkEdit', component: () => import('@/views/links/Edit.vue') },
|
||||
{ path: 'feedbacks', name: 'FeedbackList', component: () => import('@/views/feedback/List.vue') },
|
||||
{ path: 'users', name: 'UserList', component: () => import('@/views/user/List.vue') },
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
history: createWebHistory('/admin'),
|
||||
routes
|
||||
})
|
||||
|
||||
|
||||
@@ -1,13 +1,33 @@
|
||||
<template>
|
||||
<div class="joke-edit">
|
||||
<h2>{{ isEdit ? '编辑笑话' : '添加笑话' }}</h2>
|
||||
<el-form :model="form" ref="formRef" label-width="100px" style="margin-top: 20px; max-width: 600px">
|
||||
<el-form :model="form" ref="formRef" label-width="100px" style="margin-top: 20px; max-width: 800px">
|
||||
<el-form-item label="标题" prop="title">
|
||||
<el-input v-model="form.title" />
|
||||
</el-form-item>
|
||||
<el-form-item label="内容" prop="content">
|
||||
<el-input v-model="form.content" type="textarea" :rows="6" />
|
||||
<el-input v-model="form.content" type="textarea" :rows="4" />
|
||||
</el-form-item>
|
||||
<el-form-item label="AI润色版本" prop="polished_content">
|
||||
<el-input v-model="form.polished_content" type="textarea" :rows="4" placeholder="AI 润色后的笑话内容" />
|
||||
</el-form-item>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="AI评分" prop="ai_score">
|
||||
<el-input-number v-model="form.ai_score" :min="1" :max="10" :step="0.1" precision="1" placeholder="1-10分" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="16">
|
||||
<el-form-item label="AI等级" prop="ai_level">
|
||||
<el-select v-model="form.ai_level" placeholder="AI 质量评价" style="width: 100%">
|
||||
<el-option label="⭐⭐⭐ 精品 (excellent)" value="excellent" />
|
||||
<el-option label="⭐⭐ 良好 (good)" value="good" />
|
||||
<el-option label="⭐ 普通 (ordinary)" value="ordinary" />
|
||||
<el-option label="⚠️ 待优化 (poor)" value="poor" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item label="类型" prop="type_ids">
|
||||
<el-select v-model="form.type_ids" multiple placeholder="选择类型(可多选)">
|
||||
<el-option v-for="t in types" :key="t.id" :label="t.name" :value="t.id" />
|
||||
@@ -46,7 +66,7 @@ const formRef = ref()
|
||||
const isEdit = ref(false)
|
||||
|
||||
const form = reactive({
|
||||
title: '', content: '', type_ids: [], crowd_ids: [], status: 'pending'
|
||||
title: '', content: '', polished_content: '', ai_score: null, ai_level: '', type_ids: [], crowd_ids: [], status: 'pending'
|
||||
})
|
||||
|
||||
const types = ref([])
|
||||
@@ -59,7 +79,16 @@ const loadData = async () => {
|
||||
if (route.params.id) {
|
||||
isEdit.value = true
|
||||
const res = await getJoke(route.params.id)
|
||||
Object.assign(form, res)
|
||||
Object.assign(form, {
|
||||
title: res.title,
|
||||
content: res.content,
|
||||
polished_content: res.polished_content || '',
|
||||
ai_score: res.ai_score || null,
|
||||
ai_level: res.ai_level || '',
|
||||
type_ids: res.type_ids || [],
|
||||
crowd_ids: res.crowd_ids || [],
|
||||
status: res.status
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,9 +4,54 @@
|
||||
<h2>笑话管理</h2>
|
||||
<el-button type="primary" @click="$router.push('/jokes/edit')">添加笑话</el-button>
|
||||
</div>
|
||||
<el-table :data="jokeStore.jokes" v-loading="jokeStore.loading" style="margin-top: 20px">
|
||||
|
||||
<!-- 搜索筛选 -->
|
||||
<div class="search-bar">
|
||||
<el-input
|
||||
v-model="searchKeyword"
|
||||
placeholder="搜索标题/内容..."
|
||||
clearable
|
||||
style="width: 240px"
|
||||
@keyup.enter="handleSearch"
|
||||
>
|
||||
<template #prefix>
|
||||
<span class="search-icon">🔍</span>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-select v-model="filterStatus" placeholder="状态" clearable style="width: 120px" @change="handleSearch">
|
||||
<el-option label="全部" value="" />
|
||||
<el-option label="已发布" value="approved" />
|
||||
<el-option label="待审核" value="pending" />
|
||||
<el-option label="已拒绝" value="rejected" />
|
||||
</el-select>
|
||||
<el-select v-model="filterLevel" placeholder="AI等级" clearable style="width: 130px" @change="handleSearch">
|
||||
<el-option label="全部" value="" />
|
||||
<el-option label="⭐⭐⭐ 精品" value="excellent" />
|
||||
<el-option label="⭐⭐ 良好" value="good" />
|
||||
<el-option label="⭐ 普通" value="ordinary" />
|
||||
<el-option label="⚠️ 待优化" value="poor" />
|
||||
</el-select>
|
||||
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
||||
<el-button @click="resetSearch">重置</el-button>
|
||||
</div>
|
||||
|
||||
<el-table :data="jokeStore.jokes" v-loading="jokeStore.loading" style="margin-top: 16px">
|
||||
<el-table-column prop="id" label="ID" width="60" />
|
||||
<el-table-column prop="title" label="标题" />
|
||||
<el-table-column prop="title" label="标题" min-width="150" show-overflow-tooltip />
|
||||
<el-table-column label="AI等级" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.ai_level" size="small" :type="getLevelType(row.ai_level)">
|
||||
{{ getLevelText(row.ai_level) }}
|
||||
</el-tag>
|
||||
<span v-else class="text-muted">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="润色" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.polished_content" type="success" size="small">已润色</el-tag>
|
||||
<span v-else class="text-muted">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="类型" width="140">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-for="name in (row.type_names || [])" :key="name" size="small" style="margin:1px">{{ name }}</el-tag>
|
||||
@@ -24,8 +69,8 @@
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="view_count" label="浏览" width="80" />
|
||||
<el-table-column prop="like_count" label="点赞" width="80" />
|
||||
<el-table-column prop="view_count" label="浏览" width="70" />
|
||||
<el-table-column prop="like_count" label="点赞" width="70" />
|
||||
<el-table-column label="操作" width="200">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" @click="$router.push(`/jokes/edit/${row.id}`)">编辑</el-button>
|
||||
@@ -33,28 +78,69 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="pagination-wrapper">
|
||||
<el-pagination
|
||||
style="margin-top: 20px"
|
||||
:current-page="page"
|
||||
:page-size="20"
|
||||
:total="jokeStore.total"
|
||||
layout="total, prev, pager, next"
|
||||
layout="total, prev, pager, next, jumper"
|
||||
@current-change="loadJokes"
|
||||
/>
|
||||
<div class="page-jump">
|
||||
跳至 <el-input-number
|
||||
v-model="jumpPage"
|
||||
:min="1"
|
||||
:max="maxPage"
|
||||
size="small"
|
||||
controls-position="right"
|
||||
@change="handleJump"
|
||||
/> 页
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useJokeStore } from '@/stores/joke'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
|
||||
const jokeStore = useJokeStore()
|
||||
const page = ref(1)
|
||||
const jumpPage = ref(1)
|
||||
const searchKeyword = ref('')
|
||||
const filterStatus = ref('')
|
||||
const filterLevel = ref('')
|
||||
|
||||
const maxPage = computed(() => Math.ceil(jokeStore.total / 20) || 1)
|
||||
|
||||
const loadJokes = (p = 1) => {
|
||||
page.value = p
|
||||
jokeStore.fetchJokes({ page: p, page_size: 20 })
|
||||
jumpPage.value = p
|
||||
jokeStore.fetchJokes({
|
||||
page: p,
|
||||
page_size: 20,
|
||||
keyword: searchKeyword.value || undefined,
|
||||
status: filterStatus.value || undefined,
|
||||
ai_level: filterLevel.value || undefined,
|
||||
})
|
||||
}
|
||||
|
||||
const handleJump = (val) => {
|
||||
if (val && val >= 1 && val <= maxPage.value) {
|
||||
loadJokes(val)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
loadJokes(1)
|
||||
}
|
||||
|
||||
const resetSearch = () => {
|
||||
searchKeyword.value = ''
|
||||
filterStatus.value = ''
|
||||
filterLevel.value = ''
|
||||
loadJokes(1)
|
||||
}
|
||||
|
||||
const handleDelete = async (id) => {
|
||||
@@ -64,6 +150,26 @@ const handleDelete = async (id) => {
|
||||
loadJokes(page.value)
|
||||
}
|
||||
|
||||
function getLevelText(level) {
|
||||
const map = {
|
||||
'excellent': '⭐⭐⭐',
|
||||
'good': '⭐⭐',
|
||||
'ordinary': '⭐',
|
||||
'poor': '待优化'
|
||||
}
|
||||
return map[level] || level
|
||||
}
|
||||
|
||||
function getLevelType(level) {
|
||||
const map = {
|
||||
'excellent': 'warning',
|
||||
'good': 'success',
|
||||
'ordinary': 'info',
|
||||
'poor': 'danger'
|
||||
}
|
||||
return map[level] || 'info'
|
||||
}
|
||||
|
||||
onMounted(() => loadJokes())
|
||||
</script>
|
||||
|
||||
@@ -73,4 +179,33 @@ onMounted(() => loadJokes())
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.search-bar {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
margin-top: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.search-icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
.text-muted {
|
||||
color: #999;
|
||||
}
|
||||
.pagination-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 20px;
|
||||
}
|
||||
.page-jump {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: #606266;
|
||||
font-size: 13px;
|
||||
}
|
||||
:deep(.el-input-number) {
|
||||
width: 80px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
<template>
|
||||
<div class="user-list">
|
||||
<div class="header">
|
||||
<h2>用户管理</h2>
|
||||
<el-button type="primary" @click="showDialog = true; form = {}">添加用户</el-button>
|
||||
</div>
|
||||
|
||||
<el-table :data="users" v-loading="loading" style="margin-top: 16px">
|
||||
<el-table-column prop="id" label="ID" width="60" />
|
||||
<el-table-column prop="username" label="用户名" />
|
||||
<el-table-column label="角色" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.role === 'admin' ? 'danger' : 'success'" size="small">
|
||||
{{ row.role === 'admin' ? '管理员' : '提交者' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="created_at" label="创建时间" width="180" />
|
||||
<el-table-column label="操作" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="danger" @click="handleDelete(row.id)"
|
||||
:disabled="row.id === currentUserId">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 添加用户弹窗 -->
|
||||
<el-dialog v-model="showDialog" :title="form.id ? '编辑用户' : '添加用户'">
|
||||
<el-form :model="form" label-width="80px">
|
||||
<el-form-item label="用户名" required>
|
||||
<el-input v-model="form.username" placeholder="输入用户名" />
|
||||
</el-form-item>
|
||||
<el-form-item label="密码" required>
|
||||
<el-input v-model="form.password" type="password" placeholder="输入密码" show-password />
|
||||
</el-form-item>
|
||||
<el-form-item label="角色">
|
||||
<el-select v-model="form.role" style="width: 100%">
|
||||
<el-option label="管理员 (admin)" value="admin" />
|
||||
<el-option label="提交者 (submitter)" value="submitter" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="showDialog = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSave" :loading="saving">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { getUsers, createUser, deleteUser } from '@/api/user'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
|
||||
const users = ref([])
|
||||
const loading = ref(false)
|
||||
const showDialog = ref(false)
|
||||
const saving = ref(false)
|
||||
const form = ref({ role: 'submitter' })
|
||||
|
||||
// 当前登录用户的 ID(从 token 解析)
|
||||
const currentUserId = ref(1) // 假设登录用户是 admin
|
||||
|
||||
const loadUsers = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
users.value = await getUsers()
|
||||
// 从 localStorage 的 token 解析用户 ID(如果有)
|
||||
const token = localStorage.getItem('token')
|
||||
if (token) {
|
||||
try {
|
||||
const payload = JSON.parse(atob(token.split('.')[1]))
|
||||
currentUserId.value = parseInt(payload.sub)
|
||||
} catch (e) {}
|
||||
}
|
||||
} catch (e) {
|
||||
ElMessage.error('获取用户列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!form.value.username || !form.value.password) {
|
||||
ElMessage.warning('请填写用户名和密码')
|
||||
return
|
||||
}
|
||||
saving.value = true
|
||||
try {
|
||||
await createUser(form.value)
|
||||
ElMessage.success('添加成功')
|
||||
showDialog.value = false
|
||||
loadUsers()
|
||||
} catch (e) {
|
||||
ElMessage.error(e.response?.data?.detail || '添加失败')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = async (id) => {
|
||||
await ElMessageBox.confirm('确定删除此用户?')
|
||||
await deleteUser(id)
|
||||
ElMessage.success('删除成功')
|
||||
loadUsers()
|
||||
}
|
||||
|
||||
onMounted(() => loadUsers())
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
@@ -5,10 +5,11 @@ from app.database import Base
|
||||
|
||||
|
||||
class AdminUser(Base):
|
||||
"""管理员用户表模型"""
|
||||
"""用户表 - 支持角色:admin(管理员), submitter(提交者)"""
|
||||
__tablename__ = "admin_users"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
username = Column(String(50), unique=True, nullable=False)
|
||||
password_hash = Column(String(255), nullable=False)
|
||||
role = Column(String(20), default="submitter") # admin=管理员, submitter=提交者
|
||||
created_at = Column(DateTime, default=func.now())
|
||||
@@ -16,6 +16,8 @@ from app.schemas.category import JokeCrowdCreate, JokeCrowdResponse, JokeTypeCre
|
||||
from app.schemas.joke import JokeCreate, JokeResponse, JokeUpdate, PaginatedJokeResponse
|
||||
from app.schemas.link import LinkCreate, LinkResponse
|
||||
from app.schemas.feedback import FeedbackResponse
|
||||
from app.schemas.user import UserCreate, UserResponse
|
||||
from app.routers.auth import hash_password
|
||||
|
||||
router = APIRouter(prefix="/admin", tags=["管理后台"])
|
||||
|
||||
@@ -78,6 +80,8 @@ def joke_to_response(joke: Joke, db: Session = None) -> JokeResponse:
|
||||
title=joke.title,
|
||||
content=joke.content,
|
||||
polished_content=joke.polished_content,
|
||||
ai_score=joke.ai_score,
|
||||
ai_level=joke.ai_level,
|
||||
type_ids=ids,
|
||||
crowd_ids=crowd_ids,
|
||||
status=joke.status,
|
||||
@@ -96,15 +100,24 @@ def admin_list_jokes(
|
||||
page: int = 1,
|
||||
page_size: int = 20,
|
||||
status: str = None,
|
||||
keyword: str = None,
|
||||
ai_level: str = None,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: AdminUser = Depends(get_current_admin_user),
|
||||
):
|
||||
"""获取所有笑话(支持状态筛选)"""
|
||||
"""获取所有笑话(支持状态筛选、关键词搜索)"""
|
||||
# 限制 page_size 防止 DoS
|
||||
page_size = max(1, min(page_size, 100))
|
||||
query = db.query(Joke)
|
||||
if status:
|
||||
query = query.filter(Joke.status == status)
|
||||
if keyword:
|
||||
keyword_filter = f"%{keyword}%"
|
||||
query = query.filter(
|
||||
(Joke.title.like(keyword_filter)) | (Joke.content.like(keyword_filter))
|
||||
)
|
||||
if ai_level:
|
||||
query = query.filter(Joke.ai_level == ai_level)
|
||||
total = query.count()
|
||||
offset = (page - 1) * page_size
|
||||
jokes = query.order_by(Joke.created_at.desc()).offset(offset).limit(page_size).all()
|
||||
@@ -159,6 +172,7 @@ def admin_update_joke(
|
||||
db_joke = db.query(Joke).filter(Joke.id == joke_id).first()
|
||||
if not db_joke:
|
||||
raise HTTPException(status_code=404, detail="笑话不存在")
|
||||
|
||||
update_data = joke.model_dump(exclude_unset=True)
|
||||
for key, value in update_data.items():
|
||||
if key in ("type_ids", "crowd_ids") and value is not None:
|
||||
@@ -421,3 +435,67 @@ def admin_delete_feedback(
|
||||
db.delete(db_feedback)
|
||||
db.commit()
|
||||
return {"message": "删除成功"}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 用户管理
|
||||
# ============================================================
|
||||
|
||||
@router.get("/users", response_model=list[UserResponse])
|
||||
def admin_list_users(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: AdminUser = Depends(get_current_admin_user),
|
||||
):
|
||||
"""获取所有用户"""
|
||||
if current_user.role != "admin":
|
||||
raise HTTPException(status_code=403, detail="仅管理员可操作")
|
||||
return db.query(AdminUser).order_by(AdminUser.created_at.desc()).all()
|
||||
|
||||
|
||||
@router.post("/users", response_model=UserResponse)
|
||||
def admin_create_user(
|
||||
user: UserCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: AdminUser = Depends(get_current_admin_user),
|
||||
):
|
||||
"""创建用户"""
|
||||
if current_user.role != "admin":
|
||||
raise HTTPException(status_code=403, detail="仅管理员可操作")
|
||||
|
||||
# 检查用户名是否已存在
|
||||
existing = db.query(AdminUser).filter(AdminUser.username == user.username).first()
|
||||
if existing:
|
||||
raise HTTPException(status_code=400, detail="用户名已存在")
|
||||
|
||||
db_user = AdminUser(
|
||||
username=user.username,
|
||||
password_hash=hash_password(user.password),
|
||||
role=user.role,
|
||||
)
|
||||
db.add(db_user)
|
||||
db.commit()
|
||||
db.refresh(db_user)
|
||||
return db_user
|
||||
|
||||
|
||||
@router.delete("/users/{user_id}")
|
||||
def admin_delete_user(
|
||||
user_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: AdminUser = Depends(get_current_admin_user),
|
||||
):
|
||||
"""删除用户"""
|
||||
if current_user.role != "admin":
|
||||
raise HTTPException(status_code=403, detail="仅管理员可操作")
|
||||
|
||||
db_user = db.query(AdminUser).filter(AdminUser.id == user_id).first()
|
||||
if not db_user:
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
|
||||
# 不允许删除自己
|
||||
if db_user.id == current_user.id:
|
||||
raise HTTPException(status_code=400, detail="不能删除自己")
|
||||
|
||||
db.delete(db_user)
|
||||
db.commit()
|
||||
return {"message": "删除成功"}
|
||||
@@ -0,0 +1,62 @@
|
||||
"""
|
||||
公开提交流程 - 供外部用户(submitter)使用。
|
||||
无后台管理权限,仅能提交笑话到 pending 状态。
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
from pydantic import BaseModel
|
||||
|
||||
from app.database import get_db
|
||||
from app.models.joke import Joke
|
||||
|
||||
router = APIRouter(tags=["笑话提交"])
|
||||
|
||||
|
||||
class JokeSubmit(BaseModel):
|
||||
"""笑话提交"""
|
||||
title: str
|
||||
content: str
|
||||
|
||||
|
||||
class JokeBatchSubmit(BaseModel):
|
||||
"""批量提交"""
|
||||
jokes: list[JokeSubmit]
|
||||
|
||||
|
||||
@router.post("/submit", response_model=dict)
|
||||
def submit_joke(data: JokeSubmit, db: Session = Depends(get_db)):
|
||||
"""公开接口:提交单条笑话(自动进入待审核状态)"""
|
||||
joke = Joke(
|
||||
title=data.title,
|
||||
content=data.content,
|
||||
status="pending",
|
||||
)
|
||||
db.add(joke)
|
||||
db.commit()
|
||||
db.refresh(joke)
|
||||
return {
|
||||
"success": True,
|
||||
"id": joke.id,
|
||||
"message": f"已提交,ID: {joke.id}"
|
||||
}
|
||||
|
||||
|
||||
@router.post("/submit/batch", response_model=dict)
|
||||
def submit_jokes_batch(data: JokeBatchSubmit, db: Session = Depends(get_db)):
|
||||
"""公开接口:批量提交笑话"""
|
||||
added = []
|
||||
for item in data.jokes:
|
||||
joke = Joke(
|
||||
title=item.title,
|
||||
content=item.content,
|
||||
status="pending",
|
||||
)
|
||||
db.add(joke)
|
||||
added.append(item.title)
|
||||
|
||||
db.commit()
|
||||
return {
|
||||
"success": True,
|
||||
"count": len(added),
|
||||
"message": f"已提交 {len(added)} 条笑话"
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
from datetime import datetime
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class UserCreate(BaseModel):
|
||||
"""创建用户"""
|
||||
username: str
|
||||
password: str
|
||||
role: str = "submitter" # admin=管理员, submitter=提交者
|
||||
|
||||
|
||||
class UserResponse(BaseModel):
|
||||
"""用户信息"""
|
||||
id: int
|
||||
username: str
|
||||
role: str
|
||||
created_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class UserRegister(BaseModel):
|
||||
"""公开注册(仅限提交者)"""
|
||||
username: str
|
||||
password: str
|
||||
@@ -0,0 +1,203 @@
|
||||
# 笑话提交接口文档
|
||||
|
||||
## 接口地址
|
||||
|
||||
```
|
||||
基础地址:http://39.104.58.51
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 提交笑话
|
||||
|
||||
### 接口 1:单条提交
|
||||
|
||||
```
|
||||
POST /api/jokes/submit
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
**请求示例:**
|
||||
|
||||
```bash
|
||||
curl -X POST http://39.104.58.51/api/jokes/submit \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"title":"笑话标题","content":"笑话内容..."}'
|
||||
```
|
||||
|
||||
**响应示例:**
|
||||
```json
|
||||
{"success":true,"id":1280,"message":"已提交,ID: 1280"}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 接口 2:批量提交
|
||||
|
||||
```
|
||||
POST /api/jokes/submit/batch
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
**请求示例:**
|
||||
|
||||
```bash
|
||||
curl -X POST http://39.104.58.51/api/jokes/submit/batch \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"jokes": [
|
||||
{"title":"标题1","content":"内容1"},
|
||||
{"title":"标题2","content":"内容2"},
|
||||
{"title":"标题3","content":"内容3"}
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
**响应示例:**
|
||||
```json
|
||||
{"success":true,"count":3,"message":"已提交 3 条笑话"}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 编程语言示例
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import requests
|
||||
|
||||
BASE_URL = "http://39.104.58.51"
|
||||
|
||||
# 单条提交
|
||||
def submit_joke(title, content):
|
||||
resp = requests.post(f"{BASE_URL}/api/jokes/submit", json={
|
||||
"title": title,
|
||||
"content": content
|
||||
})
|
||||
return resp.json()
|
||||
|
||||
# 批量提交
|
||||
def submit_jokes_batch(jokes):
|
||||
resp = requests.post(f"{BASE_URL}/api/jokes/submit/batch", json={
|
||||
"jokes": jokes # jokes 是 [{"title":..., "content":...}, ...]
|
||||
})
|
||||
return resp.json()
|
||||
|
||||
|
||||
# 使用示例
|
||||
if __name__ == "__main__":
|
||||
# 单条
|
||||
result = submit_joke("标题", "内容")
|
||||
|
||||
# 批量
|
||||
jokes = [
|
||||
{"title": "笑话1", "content": "内容1"},
|
||||
{"title": "笑话2", "content": "内容2"},
|
||||
{"title": "笑话3", "content": "内容3"},
|
||||
]
|
||||
result = submit_jokes_batch(jokes)
|
||||
print(result)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### JavaScript / Node.js
|
||||
|
||||
```javascript
|
||||
const BASE_URL = "http://39.104.58.51";
|
||||
|
||||
// 单条提交
|
||||
async function submitJoke(title, content) {
|
||||
const resp = await fetch(`${BASE_URL}/api/jokes/submit`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ title, content })
|
||||
});
|
||||
return resp.json();
|
||||
}
|
||||
|
||||
// 批量提交
|
||||
async function submitJokesBatch(jokes) {
|
||||
const resp = await fetch(`${BASE_URL}/api/jokes/submit/batch`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ jokes })
|
||||
});
|
||||
return resp.json();
|
||||
}
|
||||
|
||||
// 使用示例
|
||||
(async () => {
|
||||
// 单条
|
||||
console.log(await submitJoke("标题", "内容"));
|
||||
|
||||
// 批量
|
||||
const jokes = [
|
||||
{ title: "笑话1", content: "内容1" },
|
||||
{ title: "笑话2", content: "内容2" },
|
||||
];
|
||||
console.log(await submitJokesBatch(jokes));
|
||||
})();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Java
|
||||
|
||||
```java
|
||||
import java.net.http.*;
|
||||
import java.net.URI;
|
||||
import java.nio.file.*;
|
||||
import java.util.*;
|
||||
|
||||
public class JokeSubmit {
|
||||
static final String BASE_URL = "http://39.104.58.51";
|
||||
|
||||
public static String post(String path, String body) throws Exception {
|
||||
HttpClient client = HttpClient.newHttpClient();
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(BASE_URL + path))
|
||||
.header("Content-Type", "application/json")
|
||||
.POST(HttpRequest.BodyPublishers.ofString(body))
|
||||
.build();
|
||||
return client.send(request, HttpResponse.BodyHandlers.ofString()).body();
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
// 单条提交
|
||||
System.out.println(post("/api/jokes/submit",
|
||||
"{\"title\":\"标题\",\"content\":\"内容\"}"));
|
||||
|
||||
// 批量提交
|
||||
String batch = """
|
||||
{"jokes":[
|
||||
{"title":"笑话1","content":"内容1"},
|
||||
{"title":"笑话2","content":"内容2"}
|
||||
]}""";
|
||||
System.out.println(post("/api/jokes/submit/batch", batch));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **无需登录**:此接口公开,无需认证即可提交
|
||||
2. **自动审核**:提交后笑话进入「待审核」状态
|
||||
3. **字段说明**:
|
||||
- `title`:笑话标题(必填)
|
||||
- `content`:笑话正文(必填)
|
||||
4. **批量限制**:建议单次不超过 100 条
|
||||
5. **编码**:确保使用 UTF-8 编码
|
||||
|
||||
---
|
||||
|
||||
## 状态码
|
||||
|
||||
| 状态码 | 说明 |
|
||||
|--------|------|
|
||||
| 200 | 提交成功 |
|
||||
| 400 | 请求格式错误 |
|
||||
| 500 | 服务器内部错误 |
|
||||
Reference in New Issue
Block a user