feat: 创作中心改造 - 我的改写/续写Tab展示已发布作品

This commit is contained in:
wangwuww111
2026-03-11 23:17:18 +08:00
parent e101e8721b
commit 411110ce0c
8 changed files with 372 additions and 104 deletions

BIN
.gitignore vendored

Binary file not shown.

View File

@@ -1,7 +1,7 @@
/**
* 用户数据管理器
*/
import { get, post, del } from '../utils/http';
import { get, post, put, del } from '../utils/http';
export default class UserManager {
constructor() {
@@ -271,6 +271,53 @@ export default class UserManager {
}
}
/**
* 获取已发布到创作中心的草稿
* @param {string} draftType - 草稿类型: rewrite/continue
*/
async getPublishedDrafts(draftType) {
if (!this.isLoggedIn) return [];
try {
console.log('[UserManager] 获取已发布草稿, userId:', this.userId, 'draftType:', draftType);
const res = await get('/drafts/published', { userId: this.userId, draftType });
console.log('[UserManager] 已发布草稿响应:', res);
return res || [];
} catch (e) {
console.error('获取已发布草稿失败:', e);
return [];
}
}
/**
* 发布草稿到创作中心
* @param {number} draftId - 草稿ID
*/
async publishDraft(draftId) {
if (!this.isLoggedIn) return false;
try {
await put(`/drafts/${draftId}/publish`, null, { params: { userId: this.userId } });
return true;
} catch (e) {
console.error('发布草稿失败:', e);
return false;
}
}
/**
* 从创作中心取消发布
* @param {number} draftId - 草稿ID
*/
async unpublishDraft(draftId) {
if (!this.isLoggedIn) return false;
try {
await put(`/drafts/${draftId}/unpublish`, null, { params: { userId: this.userId } });
return true;
} catch (e) {
console.error('取消发布失败:', e);
return false;
}
}
// ========== 游玩记录相关 ==========
/**

View File

@@ -6,8 +6,8 @@ import BaseScene from './BaseScene';
export default class AICreateScene extends BaseScene {
constructor(main, params) {
super(main, params);
this.currentTab = 0; // 0:改写 1:续写 2:创作
this.tabs = ['AI改写', 'AI续写', 'AI创作'];
this.currentTab = 0; // 0:我的改写 1:我的续写 2:AI创作
this.tabs = ['我的改写', '我的续写', 'AI创作'];
// 滚动
this.scrollY = 0;
@@ -17,8 +17,8 @@ export default class AICreateScene extends BaseScene {
this.hasMoved = false;
// 用户数据
this.recentStories = [];
this.aiHistory = [];
this.publishedRewrites = []; // 已发布的改写作品
this.publishedContinues = []; // 已发布的续写作品
this.quota = { daily: 3, used: 0, purchased: 0 };
// 创作表单
@@ -29,12 +29,7 @@ export default class AICreateScene extends BaseScene {
conflict: ''
};
// 选中的故事(用于改写/续写)
this.selectedStory = null;
// 快捷标签
this.rewriteTags = ['主角逆袭', '甜蜜HE', '虐心BE', '反转剧情', '意外重逢', '身份揭秘'];
this.continueTags = ['增加悬念', '感情升温', '冲突加剧', '真相大白', '误会解除'];
this.genreTags = ['都市言情', '古风宫廷', '悬疑推理', '校园青春', '修仙玄幻', '职场商战'];
}
@@ -44,10 +39,17 @@ export default class AICreateScene extends BaseScene {
async loadData() {
try {
// 加载最近游玩的故事
this.recentStories = await this.main.userManager.getRecentPlayed() || [];
// 加载AI创作历史
this.aiHistory = await this.main.userManager.getAIHistory() || [];
const userId = this.main.userManager.userId;
if (!userId) return;
// 加载已发布的改写作品
const rewriteRes = await this.main.userManager.getPublishedDrafts('rewrite');
this.publishedRewrites = rewriteRes || [];
// 加载已发布的续写作品
const continueRes = await this.main.userManager.getPublishedDrafts('continue');
this.publishedContinues = continueRes || [];
// 加载配额
const quotaData = await this.main.userManager.getAIQuota();
if (quotaData) this.quota = quotaData;
@@ -59,8 +61,10 @@ export default class AICreateScene extends BaseScene {
calculateMaxScroll() {
let contentHeight = 400;
if (this.currentTab === 0 || this.currentTab === 1) {
contentHeight = 300 + this.recentStories.length * 80;
if (this.currentTab === 0) {
contentHeight = 300 + this.publishedRewrites.length * 90;
} else if (this.currentTab === 1) {
contentHeight = 300 + this.publishedContinues.length * 90;
} else {
contentHeight = 600;
}
@@ -210,23 +214,10 @@ export default class AICreateScene extends BaseScene {
ctx.fillStyle = 'rgba(255,255,255,0.6)';
ctx.font = '13px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('选择一个已玩过的故事AI帮你改写结局', this.screenWidth / 2, y + 25);
ctx.fillText('展示你从草稿箱发布的改写作品', this.screenWidth / 2, y + 25);
// 快捷标签
ctx.fillStyle = 'rgba(255,255,255,0.8)';
ctx.font = '12px sans-serif';
ctx.textAlign = 'left';
ctx.fillText('热门改写方向:', padding, y + 55);
const tagEndY = this.renderTags(ctx, this.rewriteTags, padding, y + 70, 'rewrite');
// 选择故事 - 位置根据标签高度动态调整
ctx.fillStyle = 'rgba(255,255,255,0.8)';
ctx.font = '13px sans-serif';
ctx.textAlign = 'left';
ctx.fillText('选择要改写的故事:', padding, tagEndY + 25);
this.renderStoryList(ctx, tagEndY + 40, 'rewrite');
// 作品列表
this.renderPublishedList(ctx, y + 50, this.publishedRewrites, 'rewrite');
}
renderContinueTab(ctx, startY) {
@@ -236,21 +227,10 @@ export default class AICreateScene extends BaseScene {
ctx.fillStyle = 'rgba(255,255,255,0.6)';
ctx.font = '13px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('选择一个进行中的故事AI帮你续写剧情', this.screenWidth / 2, y + 25);
ctx.fillText('展示你从草稿箱发布的续写作品', this.screenWidth / 2, y + 25);
ctx.fillStyle = 'rgba(255,255,255,0.8)';
ctx.font = '12px sans-serif';
ctx.textAlign = 'left';
ctx.fillText('续写方向:', padding, y + 55);
const tagEndY = this.renderTags(ctx, this.continueTags, padding, y + 70, 'continue');
ctx.fillStyle = 'rgba(255,255,255,0.8)';
ctx.font = '13px sans-serif';
ctx.textAlign = 'left';
ctx.fillText('选择要续写的故事:', padding, tagEndY + 25);
this.renderStoryList(ctx, tagEndY + 40, 'continue');
// 作品列表
this.renderPublishedList(ctx, y + 50, this.publishedContinues, 'continue');
}
renderCreateTab(ctx, startY) {
@@ -388,6 +368,84 @@ export default class AICreateScene extends BaseScene {
this.inputRects[field] = { x, y: y + this.scrollY, width, height, field };
}
renderPublishedList(ctx, startY, items, type) {
const padding = 15;
const cardHeight = 80;
const cardGap = 12;
if (!this.publishedRects) this.publishedRects = {};
this.publishedRects[type] = [];
if (!items || items.length === 0) {
ctx.fillStyle = 'rgba(255,255,255,0.4)';
ctx.font = '13px sans-serif';
ctx.textAlign = 'center';
const tipText = type === 'rewrite'
? '暂无改写作品,去草稿箱发布吧'
: '暂无续写作品,去草稿箱发布吧';
ctx.fillText(tipText, this.screenWidth / 2, startY + 40);
// 跳转草稿箱按钮
const btnY = startY + 70;
const btnWidth = 120;
const btnX = (this.screenWidth - btnWidth) / 2;
ctx.fillStyle = 'rgba(168, 85, 247, 0.3)';
this.roundRect(ctx, btnX, btnY, btnWidth, 36, 18);
ctx.fill();
ctx.fillStyle = '#a855f7';
ctx.font = '13px sans-serif';
ctx.fillText('前往草稿箱', this.screenWidth / 2, btnY + 24);
this.gotoDraftsBtnRect = { x: btnX, y: btnY + this.scrollY, width: btnWidth, height: 36 };
return;
}
items.forEach((item, index) => {
const y = startY + index * (cardHeight + cardGap);
// 卡片背景
ctx.fillStyle = 'rgba(255,255,255,0.06)';
this.roundRect(ctx, padding, y, this.screenWidth - padding * 2, cardHeight, 12);
ctx.fill();
// 标题
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 14px sans-serif';
ctx.textAlign = 'left';
const title = item.title?.length > 15 ? item.title.substring(0, 15) + '...' : (item.title || '未命名作品');
ctx.fillText(title, padding + 15, y + 25);
// 原故事
ctx.fillStyle = 'rgba(255,255,255,0.5)';
ctx.font = '11px sans-serif';
ctx.fillText(`原故事:${item.storyTitle || '未知'}`, padding + 15, y + 45);
// 创作时间
ctx.fillText(item.createdAt || '', padding + 15, y + 65);
// 阅读按钮
const btnX = this.screenWidth - padding - 70;
const btnGradient = ctx.createLinearGradient(btnX, y + 25, btnX + 60, y + 25);
btnGradient.addColorStop(0, '#a855f7');
btnGradient.addColorStop(1, '#ec4899');
ctx.fillStyle = btnGradient;
this.roundRect(ctx, btnX, y + 25, 60, 30, 15);
ctx.fill();
ctx.fillStyle = '#ffffff';
ctx.font = '12px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('阅读', btnX + 30, y + 45);
this.publishedRects[type].push({
x: padding,
y: y + this.scrollY,
width: this.screenWidth - padding * 2,
height: cardHeight,
item,
btnRect: { x: btnX, y: y + 25 + this.scrollY, width: 60, height: 30 }
});
});
}
renderStoryList(ctx, startY, type) {
const padding = 15;
const cardHeight = 70;
@@ -550,7 +608,6 @@ export default class AICreateScene extends BaseScene {
if (this.currentTab !== tab.index) {
this.currentTab = tab.index;
this.scrollY = 0;
this.selectedStory = null;
this.calculateMaxScroll();
}
return;
@@ -561,14 +618,34 @@ export default class AICreateScene extends BaseScene {
// 调整y坐标考虑滚动
const scrolledY = y + this.scrollY;
// 标签点击
if (this.tagRects) {
const tagType = this.currentTab === 0 ? 'rewrite' : this.currentTab === 1 ? 'continue' : 'genre';
const tags = this.tagRects[tagType];
// 前往草稿箱按钮
if (this.gotoDraftsBtnRect && this.isInRect(x, scrolledY, this.gotoDraftsBtnRect)) {
this.main.sceneManager.switchScene('drafts');
return;
}
// 已发布作品点击(改写/续写Tab
if (this.currentTab < 2 && this.publishedRects) {
const type = this.currentTab === 0 ? 'rewrite' : 'continue';
const items = this.publishedRects[type];
if (items) {
for (const rect of items) {
// 阅读按钮点击
if (this.isInRect(x, scrolledY, rect.btnRect)) {
this.handleReadPublished(rect.item);
return;
}
}
}
}
// 标签点击只有创作Tab有标签
if (this.currentTab === 2 && this.tagRects) {
const tags = this.tagRects['genre'];
if (tags) {
for (const tag of tags) {
if (this.isInRect(x, scrolledY, tag)) {
this.handleTagSelect(tagType, tag);
this.handleTagSelect('genre', tag);
return;
}
}
@@ -586,26 +663,6 @@ export default class AICreateScene extends BaseScene {
}
}
// 故事列表点击
if (this.currentTab < 2 && this.storyRects) {
const type = this.currentTab === 0 ? 'rewrite' : 'continue';
const stories = this.storyRects[type];
if (stories) {
for (const rect of stories) {
if (this.isInRect(x, scrolledY, rect)) {
this.selectedStory = rect.story;
return;
}
}
}
}
// 操作按钮
if (this.actionBtnRect && this.isInRect(x, scrolledY, this.actionBtnRect)) {
this.handleAction(this.actionBtnRect.type);
return;
}
// 创作按钮
if (this.currentTab === 2 && this.createBtnRect && this.isInRect(x, scrolledY, this.createBtnRect)) {
this.handleCreate();
@@ -613,6 +670,15 @@ export default class AICreateScene extends BaseScene {
}
}
handleReadPublished(item) {
// 跳转到故事场景播放AI改写/续写的内容
this.main.sceneManager.switchScene('story', {
storyId: item.storyId,
draftId: item.id,
fromDrafts: true
});
}
isInRect(x, y, rect) {
return x >= rect.x && x <= rect.x + rect.width && y >= rect.y && y <= rect.y + rect.height;
}
@@ -620,10 +686,6 @@ export default class AICreateScene extends BaseScene {
handleTagSelect(type, tag) {
if (type === 'genre') {
this.createForm.genre = tag.value;
} else if (type === 'rewrite') {
this.selectedRewriteTag = tag.index;
} else if (type === 'continue') {
this.selectedContinueTag = tag.index;
}
}

View File

@@ -636,10 +636,10 @@ export default class ProfileScene extends BaseScene {
const promptText = item.userPrompt ? `"「${item.userPrompt}」"` : '';
ctx.fillText(this.truncateText(ctx, promptText, w - 100), textX, y + 48);
// 时间
// 时间(放在左下角)
ctx.fillStyle = 'rgba(255,255,255,0.35)';
ctx.font = '10px sans-serif';
ctx.fillText(item.createdAt || '', textX, y + 68);
ctx.fillText(item.createdAt || '', textX, y + 72);
// 未读标记
if (!item.isRead && item.status === 'completed') {
@@ -649,33 +649,52 @@ export default class ProfileScene extends BaseScene {
ctx.fill();
}
// 按钮
const btnY = y + 62;
// 删除按钮(所有状态都显示)
ctx.fillStyle = 'rgba(239, 68, 68, 0.2)';
this.roundRect(ctx, x + w - 55, btnY, 45, 24, 12);
ctx.fill();
ctx.fillStyle = '#ef4444';
ctx.font = '11px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('删除', x + w - 32, btnY + 16);
// 按钮行(放在右下角)
const btnY = y + 60;
const btnStartX = x + w - 170; // 从右边开始排列按钮
// 播放按钮(仅已完成状态)
if (item.status === 'completed') {
const btnGradient = ctx.createLinearGradient(textX, btnY, textX + 65, btnY);
const btnGradient = ctx.createLinearGradient(btnStartX, btnY, btnStartX + 50, btnY);
btnGradient.addColorStop(0, '#a855f7');
btnGradient.addColorStop(1, '#ec4899');
ctx.fillStyle = btnGradient;
this.roundRect(ctx, textX + 120, btnY, 60, 24, 12);
this.roundRect(ctx, btnStartX, btnY, 50, 26, 13);
ctx.fill();
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 11px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('播放', textX + 150, btnY + 16);
ctx.fillText('播放', btnStartX + 25, btnY + 17);
// 发布按钮(仅已完成且未发布)
if (!item.publishedToCenter) {
ctx.fillStyle = 'rgba(34, 197, 94, 0.2)';
this.roundRect(ctx, btnStartX + 58, btnY, 50, 26, 13);
ctx.fill();
ctx.fillStyle = '#22c55e';
ctx.font = '11px sans-serif';
ctx.fillText('发布', btnStartX + 83, btnY + 17);
} else {
// 已发布标识
ctx.fillStyle = 'rgba(34, 197, 94, 0.15)';
this.roundRect(ctx, btnStartX + 58, btnY, 55, 26, 13);
ctx.fill();
ctx.fillStyle = '#22c55e';
ctx.font = '10px sans-serif';
ctx.fillText('已发布', btnStartX + 85, btnY + 17);
}
}
// 删除按钮(所有状态都显示,最右边)
ctx.fillStyle = 'rgba(239, 68, 68, 0.2)';
this.roundRect(ctx, x + w - 55, btnY, 45, 26, 13);
ctx.fill();
ctx.fillStyle = '#ef4444';
ctx.font = '11px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('删除', x + w - 32, btnY + 17);
}
renderSimpleCard(ctx, item, x, y, w, h, index) {
ctx.fillStyle = 'rgba(255,255,255,0.05)';
this.roundRect(ctx, x, y, w, h, 12);
@@ -896,23 +915,32 @@ export default class ProfileScene extends BaseScene {
// AI草稿 Tab 的按钮检测
if (this.currentTab === 1) {
const btnY = 62;
const btnH = 24;
const btnY = 60;
const btnH = 26;
const btnStartX = padding + cardW - 170;
// 检测删除按钮点击(右侧)
// 检测删除按钮点击(右侧)
const deleteBtnX = padding + cardW - 55;
if (x >= deleteBtnX && x <= deleteBtnX + 45 && relativeY >= btnY && relativeY <= btnY + btnH) {
this.confirmDeleteDraft(item, index);
return;
}
// 检测播放按钮点击(左侧,仅已完成状态)
// 检测播放按钮点击(仅已完成状态)
if (item.status === 'completed') {
const playBtnX = padding + 88 + 120;
if (x >= playBtnX && x <= playBtnX + 60 && relativeY >= btnY && relativeY <= btnY + btnH) {
if (x >= btnStartX && x <= btnStartX + 50 && relativeY >= btnY && relativeY <= btnY + btnH) {
this.main.sceneManager.switchScene('story', { storyId: item.storyId, draftId: item.id });
return;
}
// 检测发布按钮点击(仅未发布状态)
if (!item.publishedToCenter) {
const publishBtnX = btnStartX + 58;
if (x >= publishBtnX && x <= publishBtnX + 50 && relativeY >= btnY && relativeY <= btnY + btnH) {
this.confirmPublishDraft(item, index);
return;
}
}
}
// 点击卡片其他区域
@@ -1027,6 +1055,31 @@ export default class ProfileScene extends BaseScene {
});
}
// 确认发布草稿到创作中心
confirmPublishDraft(item, index) {
wx.showModal({
title: '发布到创作中心',
content: `确定要将「${item.title || 'AI改写'}」发布到创作中心吗?`,
confirmText: '发布',
confirmColor: '#22c55e',
cancelText: '取消',
success: async (res) => {
if (res.confirm) {
wx.showLoading({ title: '发布中...' });
const success = await this.main.userManager.publishDraft(item.id);
wx.hideLoading();
if (success) {
// 更新本地状态
this.drafts[index].publishedToCenter = true;
wx.showToast({ title: '发布成功', icon: 'success' });
} else {
wx.showToast({ title: '发布失败', icon: 'none' });
}
}
}
});
}
// 确认删除游玩记录
confirmDeleteRecord(item, index) {
wx.showModal({

View File

@@ -152,8 +152,8 @@ function requestCloud(options) {
/**
* GET请求
*/
export function get(url, data) {
return request({ url, method: 'GET', data });
export function get(url, params) {
return request({ url, method: 'GET', params });
}
/**
@@ -170,4 +170,11 @@ export function del(url, data) {
return request({ url, method: 'DELETE', data });
}
export default { request, get, post, del };
/**
* PUT请求
*/
export function put(url, data, options = {}) {
return request({ url, method: 'PUT', data, ...options });
}
export default { request, get, post, put, del };

View File

@@ -121,6 +121,8 @@ class StoryDraft(Base):
status = Column(Enum(DraftStatus), default=DraftStatus.pending)
error_message = Column(String(500), default="")
is_read = Column(Boolean, default=False) # 用户是否已查看
published_to_center = Column(Boolean, default=False) # 是否发布到创作中心
draft_type = Column(String(20), default="rewrite") # 草稿类型: rewrite/continue/create
created_at = Column(TIMESTAMP, server_default=func.now())
completed_at = Column(TIMESTAMP, default=None)

View File

@@ -374,7 +374,8 @@ async def create_draft(
current_node_key=request.currentNodeKey,
current_content=request.currentContent,
user_prompt=request.prompt,
status=DraftStatus.pending
status=DraftStatus.pending,
draft_type='rewrite'
)
db.add(draft)
@@ -419,7 +420,8 @@ async def create_ending_draft(
current_node_key=request.endingName, # 保存结局名称
current_content=request.endingContent, # 保存结局内容
user_prompt=request.prompt,
status=DraftStatus.pending
status=DraftStatus.pending,
draft_type='rewrite'
)
db.add(draft)
@@ -464,7 +466,8 @@ async def create_continue_ending_draft(
current_node_key=request.endingName, # 保存结局名称
current_content=request.endingContent, # 保存结局内容
user_prompt=request.prompt,
status=DraftStatus.pending
status=DraftStatus.pending,
draft_type='continue'
)
db.add(draft)
@@ -508,6 +511,8 @@ async def get_drafts(
"userPrompt": draft.user_prompt,
"status": draft.status.value if draft.status else "pending",
"isRead": draft.is_read,
"publishedToCenter": draft.published_to_center,
"draftType": draft.draft_type or "rewrite",
"createdAt": draft.created_at.strftime("%Y-%m-%d %H:%M") if draft.created_at else "",
"completedAt": draft.completed_at.strftime("%Y-%m-%d %H:%M") if draft.completed_at else None
})
@@ -549,6 +554,48 @@ async def check_new_drafts(
}
@router.get("/published")
async def get_published_drafts(
userId: int,
draftType: Optional[str] = None,
db: AsyncSession = Depends(get_db)
):
"""获取已发布到创作中心的草稿列表"""
query = select(StoryDraft, Story.title.label('story_title')).join(
Story, StoryDraft.story_id == Story.id
).where(
StoryDraft.user_id == userId,
StoryDraft.published_to_center == True,
StoryDraft.status == DraftStatus.completed
)
# 按类型筛选
if draftType:
query = query.where(StoryDraft.draft_type == draftType)
query = query.order_by(StoryDraft.created_at.desc())
result = await db.execute(query)
rows = result.all()
drafts = []
for draft, story_title in rows:
drafts.append({
"id": draft.id,
"storyId": draft.story_id,
"storyTitle": story_title or "未知故事",
"title": draft.title or "",
"userPrompt": draft.user_prompt,
"draftType": draft.draft_type or "rewrite",
"createdAt": draft.created_at.strftime("%Y-%m-%d %H:%M") if draft.created_at else ""
})
return {
"code": 0,
"data": drafts
}
@router.get("/{draft_id}")
async def get_draft_detail(
draft_id: int,
@@ -652,3 +699,51 @@ async def mark_all_drafts_read(
await db.commit()
return {"code": 0, "message": "已全部标记为已读"}
@router.put("/{draft_id}/publish")
async def publish_draft_to_center(
draft_id: int,
userId: int,
db: AsyncSession = Depends(get_db)
):
"""发布草稿到创作中心"""
# 验证草稿存在且属于该用户
result = await db.execute(
select(StoryDraft).where(
StoryDraft.id == draft_id,
StoryDraft.user_id == userId,
StoryDraft.status == DraftStatus.completed
)
)
draft = result.scalar_one_or_none()
if not draft:
raise HTTPException(status_code=404, detail="草稿不存在或未完成")
# 更新发布状态
draft.published_to_center = True
await db.commit()
return {"code": 0, "message": "已发布到创作中心"}
@router.put("/{draft_id}/unpublish")
async def unpublish_draft_from_center(
draft_id: int,
userId: int,
db: AsyncSession = Depends(get_db)
):
"""从创作中心取消发布"""
await db.execute(
update(StoryDraft)
.where(
StoryDraft.id == draft_id,
StoryDraft.user_id == userId
)
.values(published_to_center=False)
)
await db.commit()
return {"code": 0, "message": "已从创作中心移除"}

View File

@@ -146,6 +146,8 @@ CREATE TABLE IF NOT EXISTS `story_drafts` (
`status` ENUM('pending', 'processing', 'completed', 'failed') DEFAULT 'pending' COMMENT '状态',
`error_message` VARCHAR(500) DEFAULT '' COMMENT '失败原因',
`is_read` TINYINT(1) DEFAULT 0 COMMENT '用户是否已查看',
`published_to_center` TINYINT(1) DEFAULT 0 COMMENT '是否发布到创作中心',
`draft_type` VARCHAR(20) DEFAULT 'rewrite' COMMENT '草稿类型: rewrite/continue/create',
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`completed_at` TIMESTAMP NULL DEFAULT NULL COMMENT '完成时间',
PRIMARY KEY (`id`),