diff --git a/.gitignore b/.gitignore index b61190c..cd673e8 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,7 @@ client/node_modules .trae .idea docs -client/dist \ No newline at end of file +client/dist +/client/.cursor/ +/client/.idea/ +/server/uploads/ diff --git a/client/scripts/migrate-api-response.mjs b/client/scripts/migrate-api-response.mjs new file mode 100644 index 0000000..e6b9dd4 --- /dev/null +++ b/client/scripts/migrate-api-response.mjs @@ -0,0 +1,87 @@ +import fs from 'fs' +import path from 'path' +import { fileURLToPath } from 'url' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const apiPath = path.join(__dirname, '../src/services/api.ts') +let content = fs.readFileSync(apiPath, 'utf8') + +// Remove errorData blocks after authFetch/fetch +content = content.replace( + /\s*if \(!response\.ok\) \{\s*const errorData = await response\.json\(\)\s*throw new Error\(errorData\.message \|\| '[^']*'\)\s*\}/g, + '' +) + +// Remove simple throw patterns +content = content.replace(/\s*if \(!response\.ok\) throw new Error\('[^']*'\)/g, '') + +// return data.result -> parseApiResponse +content = content.replace( + /const data = await response\.json\(\)\s*return data\.result/g, + 'return await parseApiResponse(response)' +) + +// return (data.result?.list || []) patterns +content = content.replace( + /const data = await response\.json\(\)\s*\/\/[^\n]*\n\s*return \(data\.result\?\.([^)]+)\)/g, + 'const data = await parseApiResponse(response)\n return (data?.$1)' +) + +content = content.replace( + /const data = await response\.json\(\)\s*return \(data\.result\?\.([^)]+)\)/g, + 'const data = await parseApiResponse(response)\n return (data?.$1)' +) + +// return data.result || [] +content = content.replace( + /const data = await response\.json\(\)\s*return data\.result \|\| \[\]/g, + 'return (await parseApiResponse(response)) || []' +) + +// const result = data.result as X patterns - keep data line, fix next lines +content = content.replace( + /const data = await response\.json\(\)\s*const result = data\.result as/g, + 'const result = await parseApiResponse' +) + +// login block +content = content.replace( + /export const login = async \(credentials: LoginRequest\): Promise => \{[\s\S]*?\n\}/, + `export const login = async (credentials: LoginRequest): Promise => { + try { + const response = await fetch(\`\${API_BASE}/admin/login\`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(credentials), + }) + const result = await parseApiResponse<{ token: string; user: User; expire: number }>(response, { + skipSessionExpired: true, + }) + return { + token: result.token, + user: result.user, + expire: result.expire, + } + } catch (error) { + console.error('Login error:', error) + throw error + } +}` +) + +// void returns that only had authFetch + ok check - add parseApiResponse +content = content.replace( + /(const response = await authFetch\([^)]+\)[\s\S]*?\))\s*\n(\s*\} catch)/g, + (match, fetchPart, catchPart) => { + if (match.includes('parseApiResponse') || match.includes('return await')) { + return match + } + if (match.includes('method:')) { + return `${fetchPart}\n await parseApiResponse(response)\n${catchPart}` + } + return match + } +) + +fs.writeFileSync(apiPath, content) +console.log('Migration complete') diff --git a/client/src/components/SearchSuggestDropdown.vue b/client/src/components/SearchSuggestDropdown.vue new file mode 100644 index 0000000..57c4226 --- /dev/null +++ b/client/src/components/SearchSuggestDropdown.vue @@ -0,0 +1,65 @@ + + + diff --git a/client/src/components/admin/AdminLayout.vue b/client/src/components/admin/AdminLayout.vue index c5eb9db..7366ab8 100644 --- a/client/src/components/admin/AdminLayout.vue +++ b/client/src/components/admin/AdminLayout.vue @@ -149,6 +149,7 @@ + @@ -156,12 +157,15 @@ import { ref, onMounted, onUnmounted, computed } from 'vue' import { useRouter, useRoute } from 'vue-router' import { useToast } from '../../composables/useToast' +import { useAuth } from '../../composables/useAuth' +import SessionExpiredModal from './SessionExpiredModal.vue' const router = useRouter() const route = useRoute() const toast = useToast() +const { logout, scheduleExpiryCheck, getUser, isAuthenticated } = useAuth() const isSidebarOpen = ref(true) -const currentUser = ref(JSON.parse(localStorage.getItem('user') || '{}')) +const currentUser = ref(getUser()) // Dropdown logic const isUserDropdownOpen = ref(false) @@ -286,10 +290,8 @@ const toggleSidebar = () => { } const handleLogout = () => { - localStorage.removeItem('token') - localStorage.removeItem('user') + logout() toast.showToast('退出登录成功', 'success') - router.push('/login') } // Helper to determine active state @@ -320,10 +322,11 @@ const currentRouteTitle = computed(() => { // Check if user is authenticated onMounted(() => { - const token = localStorage.getItem('token') - if (!token) { + if (!isAuthenticated()) { router.push('/login') + return } + scheduleExpiryCheck() document.addEventListener('click', closeDropdown) // Auto open menu based on current route diff --git a/client/src/components/admin/AttachmentDetailModal.vue b/client/src/components/admin/AttachmentDetailModal.vue index 6525e47..16c9b42 100644 --- a/client/src/components/admin/AttachmentDetailModal.vue +++ b/client/src/components/admin/AttachmentDetailModal.vue @@ -1,86 +1,107 @@ @@ -132,7 +152,6 @@ const emit = defineEmits<{ const localCategoryId = ref(0) const saving = ref(false) -// 计算属性:分类选项 const categoryOptions = computed(() => { return [ { value: 0, label: '无分类' }, @@ -140,7 +159,16 @@ const categoryOptions = computed(() => { ] }) -// 监听attachment变化,更新本地分类ID +const typeBadgeClass = computed(() => { + const map: Record = { + image: 'bg-emerald-500/10 text-emerald-300 border-emerald-500/30', + video: 'bg-violet-500/10 text-violet-300 border-violet-500/30', + document: 'bg-blue-500/10 text-blue-300 border-blue-500/30', + other: 'bg-white/5 text-art-muted border-white/10' + } + return map[props.attachment?.fileType || 'other'] || map.other +}) + watch(() => props.attachment, (newAttachment) => { if (newAttachment) { localCategoryId.value = newAttachment.categoryId || 0 @@ -155,7 +183,6 @@ const handleSave = () => { saving.value = true const categoryId = localCategoryId.value === 0 ? null : localCategoryId.value emit('save', { categoryId }) - // saving状态由父组件控制 } const handleCopyUrl = () => { @@ -183,8 +210,8 @@ const getFileTypeLabel = (fileType: string): string => { const getStorageTypeLabel = (storageType?: string): string => { const labels: Record = { local: '本地存储', - qcloud: '腾讯云COS', - aliyun: '阿里云OSS', + qcloud: '腾讯云 COS', + aliyun: '阿里云 OSS', qiniu: '七牛云' } return labels[storageType || 'local'] || storageType || '本地存储' @@ -195,12 +222,15 @@ const formatDate = (timestamp: string | number): string => { return date.toLocaleString('zh-CN') } -// 暴露saving状态给父组件 -defineExpose({ - saving -}) +defineExpose({ saving }) diff --git a/client/src/components/admin/ImageUpload.vue b/client/src/components/admin/ImageUpload.vue index 78df269..eb1eae5 100644 --- a/client/src/components/admin/ImageUpload.vue +++ b/client/src/components/admin/ImageUpload.vue @@ -187,7 +187,7 @@ + + diff --git a/client/src/components/admin/SessionExpiredModal.vue b/client/src/components/admin/SessionExpiredModal.vue new file mode 100644 index 0000000..acd5318 --- /dev/null +++ b/client/src/components/admin/SessionExpiredModal.vue @@ -0,0 +1,26 @@ + + + diff --git a/client/src/composables/useAuth.ts b/client/src/composables/useAuth.ts new file mode 100644 index 0000000..1dfba57 --- /dev/null +++ b/client/src/composables/useAuth.ts @@ -0,0 +1,111 @@ +import { ref } from 'vue' +import router from '../router' + +const TOKEN_KEY = 'token' +const USER_KEY = 'user' +const EXPIRE_KEY = 'tokenExpireAt' + +const sessionExpired = ref(false) +let sessionExpiredHandled = false +let expiryTimer: ReturnType | null = null + +export function useAuth() { + const getToken = () => localStorage.getItem(TOKEN_KEY) + + const getTokenExpireAt = (): number => { + const raw = localStorage.getItem(EXPIRE_KEY) + return raw ? Number(raw) : 0 + } + + const isAuthenticated = (): boolean => { + const token = getToken() + if (!token) return false + + const expireAt = getTokenExpireAt() + if (expireAt > 0 && Date.now() >= expireAt) { + return false + } + + return true + } + + const login = (token: string, user: unknown, expireUnixSeconds: number) => { + localStorage.setItem(TOKEN_KEY, token) + localStorage.setItem(USER_KEY, JSON.stringify(user)) + localStorage.setItem(EXPIRE_KEY, String(expireUnixSeconds * 1000)) + sessionExpiredHandled = false + sessionExpired.value = false + scheduleExpiryCheck() + } + + const logout = (showToast?: (msg: string, type: 'success' | 'error') => void) => { + clearExpiryTimer() + localStorage.removeItem(TOKEN_KEY) + localStorage.removeItem(USER_KEY) + localStorage.removeItem(EXPIRE_KEY) + sessionExpiredHandled = false + sessionExpired.value = false + + if (router.currentRoute.value.path !== '/login') { + router.push('/login') + } + + if (showToast) { + // noop by default for expiry flow + } + } + + const clearExpiryTimer = () => { + if (expiryTimer) { + clearTimeout(expiryTimer) + expiryTimer = null + } + } + + const scheduleExpiryCheck = () => { + clearExpiryTimer() + const expireAt = getTokenExpireAt() + if (!expireAt) return + + const delay = expireAt - Date.now() + if (delay <= 0) { + handleSessionExpired() + return + } + + expiryTimer = setTimeout(() => { + handleSessionExpired() + }, delay) + } + + const handleSessionExpired = () => { + if (sessionExpiredHandled) return + sessionExpiredHandled = true + sessionExpired.value = true + } + + const confirmSessionExpired = () => { + logout() + } + + const getUser = () => { + try { + return JSON.parse(localStorage.getItem(USER_KEY) || '{}') + } catch { + return {} + } + } + + return { + sessionExpired, + getToken, + getTokenExpireAt, + isAuthenticated, + login, + logout, + handleSessionExpired, + confirmSessionExpired, + scheduleExpiryCheck, + getUser, + } +} diff --git a/client/src/composables/useSearchHistory.ts b/client/src/composables/useSearchHistory.ts new file mode 100644 index 0000000..e8909f2 --- /dev/null +++ b/client/src/composables/useSearchHistory.ts @@ -0,0 +1,43 @@ +export const SEARCH_HISTORY_KEY = 'blog_search_history' +const MAX_HISTORY = 10 + +export function useSearchHistory() { + const readHistory = (): string[] => { + try { + const raw = localStorage.getItem(SEARCH_HISTORY_KEY) + if (!raw) return [] + const parsed = JSON.parse(raw) + return Array.isArray(parsed) ? parsed.filter((item) => typeof item === 'string') : [] + } catch { + return [] + } + } + + const writeHistory = (items: string[]) => { + localStorage.setItem(SEARCH_HISTORY_KEY, JSON.stringify(items.slice(0, MAX_HISTORY))) + } + + const getHistory = () => readHistory() + + const addHistory = (keyword: string) => { + const trimmed = keyword.trim() + if (!trimmed) return + const next = [trimmed, ...readHistory().filter((item) => item !== trimmed)].slice(0, MAX_HISTORY) + writeHistory(next) + } + + const removeHistory = (keyword: string) => { + writeHistory(readHistory().filter((item) => item !== keyword)) + } + + const clearHistory = () => { + localStorage.removeItem(SEARCH_HISTORY_KEY) + } + + return { + getHistory, + addHistory, + removeHistory, + clearHistory, + } +} diff --git a/client/src/pages/Blog.vue b/client/src/pages/Blog.vue index 699ffd6..89b08e3 100644 --- a/client/src/pages/Blog.vue +++ b/client/src/pages/Blog.vue @@ -10,10 +10,21 @@ +
+
@@ -251,6 +263,13 @@ + >({}) // Access log modal state const isAccessLogModalOpen = ref(false) +const isHistoryModalOpen = ref(false) const openAccessLogModal = () => { isAccessLogModalOpen.value = true } +const openHistoryModal = () => { + isHistoryModalOpen.value = true +} // Data sources const categories = ref<{value: number, label: string}[]>([]) @@ -517,67 +543,96 @@ const handleCancel = () => { router.push('/admin/posts') } -// Handle paste event for image upload -const handlePaste = async (e: ClipboardEvent) => { +const editorRef = ref() + +const uploadMediaFile = async (file: File): Promise => { + const formData = new FormData() + formData.append('file', file) + formData.append('categoryId', '1') + formData.append('storageType', 'local') + + const response = await authFetch('/api/admin/attachments/upload', { + method: 'POST', + headers: {}, + body: formData + }) + + const result = await parseApiResponse<{ fileUrl: string }>(response) + return result?.fileUrl ?? null +} + +const insertAtCursor = (text: string) => { + editorRef.value?.insert?.(() => ({ targetValue: text })) +} + +const uploadAndInsertVideo = async (file: File) => { + try { + const url = await uploadMediaFile(file) + if (!url) return + insertAtCursor(`\n\n`) + toast.showToast('视频上传成功', 'success') + } catch (error: any) { + console.error('上传视频失败:', error) + toast.showToast(error.message || '上传视频失败', 'error') + } +} + +const handleUploadImg = async (files: File[], callback: UploadImgCallBack) => { + try { + const uploadResults = await Promise.all( + files.map(async (file) => { + const url = await uploadMediaFile(file) + return url ? { url, alt: file.name, title: file.name } : null + }) + ) + const urls = uploadResults.filter((item): item is { url: string; alt: string; title: string } => item !== null) + if (urls.length > 0) { + callback(urls) + toast.showToast('图片上传成功', 'success') + } + } catch (error: any) { + console.error('上传图片失败:', error) + toast.showToast(error.message || '上传图片失败', 'error') + } +} + +const handleEditorDrop = async (e: DragEvent) => { + const files = Array.from(e.dataTransfer?.files || []) + const imageFiles = files.filter((f) => f.type.startsWith('image/')) + const videoFiles = files.filter((f) => f.type.startsWith('video/')) + if (imageFiles.length === 0 && videoFiles.length === 0) return + + e.preventDefault() + + if (imageFiles.length > 0) { + await handleUploadImg(imageFiles, (urls) => { + if (!Array.isArray(urls) || urls.length === 0) return + urls.forEach((item) => { + if (typeof item === 'string') { + insertAtCursor(`![image](${item})`) + } else { + insertAtCursor(`![${item.alt || 'image'}](${item.url})`) + } + }) + }) + } + + for (const file of videoFiles) { + await uploadAndInsertVideo(file) + } +} + +const handleVideoPaste = async (e: ClipboardEvent) => { const items = e.clipboardData?.items if (!items) return - // Check if any item is an image - let hasImage = false - for (let i = 0; i < items.length; i++) { - if (items[i].type.startsWith('image/')) { - hasImage = true - break - } - } - - if (!hasImage) return - - // Prevent default paste behavior for images - e.preventDefault() - for (let i = 0; i < items.length; i++) { const item = items[i] - if (item.type.startsWith('image/')) { + if (item.type.startsWith('video/')) { + e.preventDefault() const file = item.getAsFile() - if (!file) continue - - try { - // Upload image - const formData = new FormData() - formData.append('file', file) - formData.append('categoryId', '1') // 默认上传到分类1 - formData.append('storageType', 'local') - - const token = localStorage.getItem('token') - const response = await fetch('/api/admin/attachments/upload', { - method: 'POST', - headers: { - ...(token ? { Authorization: `Bearer ${token}` } : {}) - }, - body: formData - }) - - if (!response.ok) { - const errorData = await response.json() - throw new Error(errorData.message || '上传失败') - } - - const data = await response.json() - const imageUrl = data.result.fileUrl - - // Insert markdown image syntax - const imageMarkdown = `![${file.name}](${imageUrl})` - const currentContent = form.content - // Insert at the end of content (md-editor-v3 will handle cursor position) - form.content = currentContent + (currentContent ? '\n\n' : '') + imageMarkdown + '\n' - - toast.showToast('图片上传成功', 'success') - } catch (error: any) { - console.error('上传图片失败:', error) - toast.showToast(error.message || '上传图片失败', 'error') - } - break // Only handle first image + if (file) await uploadAndInsertVideo(file) + break } } } diff --git a/client/src/pages/admin/UserForm.vue b/client/src/pages/admin/UserForm.vue index b9197fd..293aebd 100644 --- a/client/src/pages/admin/UserForm.vue +++ b/client/src/pages/admin/UserForm.vue @@ -78,7 +78,7 @@ import { ref, reactive, onMounted, computed } from 'vue' import { useRouter, useRoute } from 'vue-router' import { useToast } from '../../composables/useToast' -import { createUser, updateUser, API_BASE, getAuthHeaders } from '../../services/api' +import { createUser, updateUser, fetchUser } from '../../services/api' import CustomSelect from '../../components/CustomSelect.vue' const router = useRouter() @@ -180,49 +180,13 @@ onMounted(async () => { if (isEditing.value) { try { const userId = parseInt(route.params.id as string) - console.log(`Fetching user data for ID: ${userId}`) - - // Direct fetch to debug - const response = await fetch(`${API_BASE}/admin/users/${userId}`, { - headers: getAuthHeaders() - }) - - console.log(`Response status: ${response.status}`) - - // Check response headers - const contentType = response.headers.get('content-type') - console.log(`Response content-type: ${contentType}`) - - // Read response as text first to debug - const responseText = await response.text() - console.log(`Response text: ${responseText}`) - - // Then try to parse as JSON - if (!response.ok) { - // If response is not ok, still try to parse as JSON - let errorData - try { - errorData = JSON.parse(responseText) - throw new Error(errorData.message || '获取用户详情失败') - } catch (parseError) { - const errorMessage = parseError instanceof Error ? parseError.message : String(parseError) - throw new Error(`获取用户详情失败,响应格式错误: ${errorMessage}`) - } - } - - // Parse successful response - const data = JSON.parse(responseText) - const user = data.result // 从统一响应格式中提取 result - console.log('Parsed user data:', user) - - // Populate form with user data + const user = await fetchUser(userId) form.username = user.username form.email = user.email form.role = user.role form.isActive = user.isActive } catch (error: any) { console.error('Failed to fetch user data:', error) - console.error('Error stack:', error.stack) toast.error('加载用户数据失败: ' + (error.message || '未知错误')) } } diff --git a/client/src/router.ts b/client/src/router.ts index cfd7cd8..9fec54d 100644 --- a/client/src/router.ts +++ b/client/src/router.ts @@ -1,4 +1,5 @@ import { createRouter, createWebHistory } from 'vue-router' +import { useAuth } from './composables/useAuth' import { getPublicSettings } from './services/api' // 缓存网站配置,避免重复获取 @@ -166,10 +167,8 @@ const router = createRouter({ router.beforeEach(async (to, _from, next) => { // 检查路由是否需要认证 if (to.matched.some(record => record.meta.requiresAuth)) { - // 检查本地存储中是否有token - const token = localStorage.getItem('token') - if (!token) { - // 没有token,重定向到登录页 + const { isAuthenticated } = useAuth() + if (!isAuthenticated()) { next({ name: 'login' }) return } diff --git a/client/src/services/api.ts b/client/src/services/api.ts index 0f2fa82..ba85f62 100644 --- a/client/src/services/api.ts +++ b/client/src/services/api.ts @@ -1,5 +1,69 @@ +import { useAuth } from '../composables/useAuth' + export const API_BASE = '/api' +export interface ApiResponse { + code: number + message: string + result: T +} + +export interface ParseApiOptions { + skipSessionExpired?: boolean +} + +export async function parseApiResponse(response: Response, options?: ParseApiOptions): Promise { + let data: ApiResponse + try { + data = await response.json() + } catch { + throw new Error('响应解析失败') + } + + if (data.code === 401 && !options?.skipSessionExpired) { + const { handleSessionExpired } = useAuth() + handleSessionExpired() + throw new Error(data.message || '登录已过期,请重新登录') + } + + if (data.code !== 200) { + throw new Error(data.message || '请求失败') + } + + return data.result +} + +// 带认证的 fetch,仅附加 Authorization,业务状态由 parseApiResponse 解析 +export async function authFetch(input: RequestInfo | URL, init: RequestInit = {}): Promise { + const { getToken } = useAuth() + const headers = new Headers(init.headers) + + const token = getToken() + if (token && !headers.has('Authorization')) { + headers.set('Authorization', `Bearer ${token}`) + } + + return fetch(input, { ...init, headers }) +} + +export async function authFetchJson( + input: RequestInfo | URL, + init: RequestInit = {}, + options?: ParseApiOptions +): Promise { + const response = await authFetch(input, init) + return parseApiResponse(response, options) +} + +export async function fetchJson( + input: RequestInfo | URL, + init: RequestInit = {}, + options?: ParseApiOptions +): Promise { + const response = await fetch(input, init) + return parseApiResponse(response, options) +} + // 通用请求头配置 export const getAuthHeaders = () => { const token = localStorage.getItem('token') @@ -110,14 +174,34 @@ export interface PostHistory { version: number title: string categoryId: number + columnId?: number + tagIds?: number[] excerpt?: string - content: string + content?: string isPublished: number modifiedBy: number modifiedAt: string createdAt: string } +export interface PostHistoryFieldDiff { + from: string + to: string + changed: boolean + diff?: string +} + +export interface PostHistoryDiff { + fromVersion: number + toVersion: number + fields: Record +} + +export interface HotSearchKeyword { + keyword: string + count: number +} + // 代码片段相关类型 export interface Snippet { id: string @@ -167,21 +251,16 @@ export const login = async (credentials: LoginRequest): Promise = try { const response = await fetch(`${API_BASE}/admin/login`, { method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, + headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(credentials), }) - if (!response.ok) { - const errorData = await response.json() - throw new Error(errorData.message || '登录失败') - } - const data = await response.json() - const result = data.result as { token: string; user: User } + const result = await parseApiResponse<{ token: string; user: User; expire: number }>(response, { + skipSessionExpired: true, + }) return { token: result.token, user: result.user, - expire: 24 * 60 * 60 * 1000 // 24小时 + expire: result.expire, } } catch (error) { console.error('Login error:', error) @@ -192,16 +271,11 @@ export const login = async (credentials: LoginRequest): Promise = // 用户管理API export const getUsers = async (): Promise => { try { - const response = await fetch(`${API_BASE}/admin/users`, { + const response = await authFetch(`${API_BASE}/admin/users`, { headers: getAuthHeaders() }) - if (!response.ok) { - const errorData = await response.json() - throw new Error(errorData.message || '获取用户列表失败') - } - const data = await response.json() - // 后端返回的是分页格式 {list, total, page, size},需要从 result.list 中取值 - return (data.result?.list || []) as User[] + const data = await parseApiResponse(response) + return (data?.list || []) as User[] } catch (error) { console.error('Get users error:', error) throw error @@ -210,15 +284,10 @@ export const getUsers = async (): Promise => { export const fetchUser = async (id: number): Promise => { try { - const response = await fetch(`${API_BASE}/admin/users/${id}`, { + const response = await authFetch(`${API_BASE}/admin/users/${id}`, { headers: getAuthHeaders() }) - if (!response.ok) { - const errorData = await response.json() - throw new Error(errorData.message || '获取用户详情失败') - } - const data = await response.json() - return data.result + return await parseApiResponse(response) } catch (error) { console.error(`Error fetching user ${id}:`, error) throw error @@ -227,15 +296,12 @@ export const fetchUser = async (id: number): Promise => { export const createUser = async (userData: Omit): Promise => { try { - const response = await fetch(`${API_BASE}/admin/users`, { + const response = await authFetch(`${API_BASE}/admin/users`, { method: 'POST', headers: getAuthHeaders(), body: JSON.stringify(userData) }) - if (!response.ok) { - const errorData = await response.json() - throw new Error(errorData.message || '创建用户失败') - } + await parseApiResponse(response) } catch (error) { console.error('Create user error:', error) throw error @@ -244,15 +310,12 @@ export const createUser = async (userData: Omit): Promise => { try { - const response = await fetch(`${API_BASE}/admin/users/${id}`, { + const response = await authFetch(`${API_BASE}/admin/users/${id}`, { method: 'PUT', headers: getAuthHeaders(), body: JSON.stringify(userData) }) - if (!response.ok) { - const errorData = await response.json() - throw new Error(errorData.message || '更新用户失败') - } + await parseApiResponse(response) } catch (error) { console.error('Update user error:', error) throw error @@ -261,14 +324,11 @@ export const updateUser = async (id: number, userData: Omit => { try { - const response = await fetch(`${API_BASE}/admin/users/${id}`, { + const response = await authFetch(`${API_BASE}/admin/users/${id}`, { method: 'DELETE', headers: getAuthHeaders() }) - if (!response.ok) { - const errorData = await response.json() - throw new Error(errorData.message || '删除用户失败') - } + await parseApiResponse(response) } catch (error) { console.error('Delete user error:', error) throw error @@ -278,15 +338,10 @@ export const deleteUser = async (id: number): Promise => { // 角色管理API export const getRoles = async (): Promise => { try { - const response = await fetch(`${API_BASE}/admin/roles`, { + const response = await authFetch(`${API_BASE}/admin/roles`, { headers: getAuthHeaders() }) - if (!response.ok) { - const errorData = await response.json() - throw new Error(errorData.message || '获取角色列表失败') - } - const data = await response.json() - return data.result || [] + return await parseApiResponse(response) || [] } catch (error) { console.error('Get roles error:', error) throw error @@ -295,15 +350,12 @@ export const getRoles = async (): Promise => { export const createRole = async (roleData: Omit): Promise => { try { - const response = await fetch(`${API_BASE}/admin/roles`, { + const response = await authFetch(`${API_BASE}/admin/roles`, { method: 'POST', headers: getAuthHeaders(), body: JSON.stringify(roleData) }) - if (!response.ok) { - const errorData = await response.json() - throw new Error(errorData.message || '创建角色失败') - } + await parseApiResponse(response) } catch (error) { console.error('Create role error:', error) throw error @@ -312,15 +364,12 @@ export const createRole = async (roleData: Omit): Promise => { try { - const response = await fetch(`${API_BASE}/admin/roles/${id}`, { + const response = await authFetch(`${API_BASE}/admin/roles/${id}`, { method: 'PUT', headers: getAuthHeaders(), body: JSON.stringify(roleData) }) - if (!response.ok) { - const errorData = await response.json() - throw new Error(errorData.message || '更新角色失败') - } + await parseApiResponse(response) } catch (error) { console.error('Update role error:', error) throw error @@ -329,14 +378,11 @@ export const updateRole = async (id: number, roleData: Omit => { try { - const response = await fetch(`${API_BASE}/admin/roles/${id}`, { + const response = await authFetch(`${API_BASE}/admin/roles/${id}`, { method: 'DELETE', headers: getAuthHeaders() }) - if (!response.ok) { - const errorData = await response.json() - throw new Error(errorData.message || '删除角色失败') - } + await parseApiResponse(response) } catch (error) { console.error('Delete role error:', error) throw error @@ -345,15 +391,10 @@ export const deleteRole = async (id: number): Promise => { export const fetchRole = async (id: number): Promise => { try { - const response = await fetch(`${API_BASE}/admin/roles/${id}`, { + const response = await authFetch(`${API_BASE}/admin/roles/${id}`, { headers: getAuthHeaders() }) - if (!response.ok) { - const errorData = await response.json() - throw new Error(errorData.message || '获取角色详情失败') - } - const data = await response.json() - return data.result + return await parseApiResponse(response) } catch (error) { console.error(`Error fetching role ${id}:`, error) throw error @@ -363,17 +404,11 @@ export const fetchRole = async (id: number): Promise => { // 作品管理API export const getAdminWorks = async (): Promise => { try { - const response = await fetch(`${API_BASE}/admin/works`, { + const response = await authFetch(`${API_BASE}/admin/works`, { headers: getAuthHeaders() }) - if (!response.ok) { - const errorData = await response.json() - throw new Error(errorData.message || '获取作品列表失败') - } - const data = await response.json() - // 后端返回的是分页格式 {list, total, page, size},需要从 result.list 中取值 - // 确保始终返回数组,即使 result 或 result.list 为 null/undefined - const list = data.result?.list + const data = await parseApiResponse>(response) + const list = data?.list return (list && Array.isArray(list)) ? list : [] } catch (error) { console.error('Get admin works error:', error) @@ -383,14 +418,7 @@ export const getAdminWorks = async (): Promise => { export const fetchWorks = async (): Promise => { try { - const response = await fetch(`${API_BASE}/works`) - if (!response.ok) { - const errorData = await response.json() - throw new Error(errorData.message || 'Failed to fetch works') - } - const data = await response.json() - // 确保从 result 字段取值,并确保始终返回数组 - const result = data.result + const result = await fetchJson(`${API_BASE}/works`) if (!result) { return [] } @@ -418,11 +446,7 @@ export const fetchWorks = async (): Promise => { export const fetchWork = async (id: string): Promise => { try { - const response = await fetch(`${API_BASE}/works/${id}`) - if (!response.ok) throw new Error('Failed to fetch work') - const data = await response.json() - // 确保从 result 字段取值 - const result = data.result as Work + const result = await fetchJson(`${API_BASE}/works/${id}`) if (!result) { throw new Error('Work not found') } @@ -453,15 +477,12 @@ export const fetchWork = async (id: string): Promise => { export const createWork = async (workData: Omit): Promise => { try { - const response = await fetch(`${API_BASE}/admin/works`, { + const response = await authFetch(`${API_BASE}/admin/works`, { method: 'POST', headers: getAuthHeaders(), body: JSON.stringify(workData) }) - if (!response.ok) { - const errorData = await response.json() - throw new Error(errorData.message || '创建作品失败') - } + await parseApiResponse(response) } catch (error) { console.error('Create work error:', error) throw error @@ -470,15 +491,12 @@ export const createWork = async (workData: Omit): Promise): Promise => { try { - const response = await fetch(`${API_BASE}/admin/works/${id}`, { + const response = await authFetch(`${API_BASE}/admin/works/${id}`, { method: 'PUT', headers: getAuthHeaders(), body: JSON.stringify(workData) }) - if (!response.ok) { - const errorData = await response.json() - throw new Error(errorData.message || '更新作品失败') - } + await parseApiResponse(response) } catch (error) { console.error('Update work error:', error) throw error @@ -487,14 +505,11 @@ export const updateWork = async (id: string, workData: Omit export const deleteWork = async (id: string): Promise => { try { - const response = await fetch(`${API_BASE}/admin/works/${id}`, { + const response = await authFetch(`${API_BASE}/admin/works/${id}`, { method: 'DELETE', headers: getAuthHeaders() }) - if (!response.ok) { - const errorData = await response.json() - throw new Error(errorData.message || '删除作品失败') - } + await parseApiResponse(response) } catch (error) { console.error('Delete work error:', error) throw error @@ -515,15 +530,10 @@ export const getAdminPosts = async ( params.append('keyword', keyword) } - const response = await fetch(`${API_BASE}/admin/posts?${params.toString()}`, { + const response = await authFetch(`${API_BASE}/admin/posts?${params.toString()}`, { headers: getAuthHeaders() }) - if (!response.ok) { - const errorData = await response.json() - throw new Error(errorData.message || '获取文章列表失败') - } - const data = await response.json() - return data.result + return await parseApiResponse(response) } catch (error) { console.error('Get admin posts error:', error) throw error @@ -552,17 +562,15 @@ export const fetchPosts = async ( url += `?${params.toString()}` } - const response = await fetch(url) - if (!response.ok) throw new Error('Failed to fetch posts') - const data = await response.json() + const data = await fetchJson | Post[]>(url) // 返回分页格式的响应 - if (data.result && typeof data.result === 'object' && 'list' in data.result) { - return data.result as PaginationResponse + if (data && typeof data === 'object' && 'list' in data) { + return data as PaginationResponse } // 兼容旧格式(直接返回数组) - const list = Array.isArray(data.result) ? data.result : [] + const list = Array.isArray(data) ? data : [] return { list, total: list.length, @@ -583,9 +591,7 @@ export const fetchPosts = async ( export const fetchPost = async (id: number | string): Promise => { try { const response = await fetch(`${API_BASE}/posts/${id}`) - if (!response.ok) throw new Error('Failed to fetch post') - const data = await response.json() - return data.result + return await parseApiResponse(response) } catch (error) { console.error(`Error fetching post ${id}:`, error) // Return empty fallback @@ -602,9 +608,7 @@ export const fetchPost = async (id: number | string): Promise => { export const getRecommendedPosts = async (postId: number | string, limit: number = 3): Promise => { try { const response = await fetch(`${API_BASE}/posts/${postId}/recommendations?limit=${limit}`) - if (!response.ok) throw new Error('Failed to fetch recommended posts') - const data = await response.json() - return data.result || [] + return await parseApiResponse(response) || [] } catch (error) { console.error(`Error fetching recommended posts for ${postId}:`, error) return [] @@ -613,15 +617,12 @@ export const getRecommendedPosts = async (postId: number | string, limit: number export const createPost = async (postData: Omit): Promise => { try { - const response = await fetch(`${API_BASE}/admin/posts`, { + const response = await authFetch(`${API_BASE}/admin/posts`, { method: 'POST', headers: getAuthHeaders(), body: JSON.stringify(postData) }) - if (!response.ok) { - const errorData = await response.json() - throw new Error(errorData.message || '创建文章失败') - } + await parseApiResponse(response) } catch (error) { console.error('Create post error:', error) throw error @@ -630,15 +631,12 @@ export const createPost = async (postData: Omit): Promise => { export const updatePost = async (id: number | string, postData: Omit): Promise => { try { - const response = await fetch(`${API_BASE}/admin/posts/${id}`, { + const response = await authFetch(`${API_BASE}/admin/posts/${id}`, { method: 'PUT', headers: getAuthHeaders(), body: JSON.stringify(postData) }) - if (!response.ok) { - const errorData = await response.json() - throw new Error(errorData.message || '更新文章失败') - } + await parseApiResponse(response) } catch (error) { console.error('Update post error:', error) throw error @@ -662,15 +660,12 @@ export const updatePostRelations = async ( payload.columnId = null } - const response = await fetch(`${API_BASE}/admin/posts/${id}/relations`, { + const response = await authFetch(`${API_BASE}/admin/posts/${id}/relations`, { method: 'PATCH', headers: getAuthHeaders(), body: JSON.stringify(payload) }) - if (!response.ok) { - const errorData = await response.json() - throw new Error(errorData.message || '更新文章关联失败') - } + await parseApiResponse(response) } catch (error) { console.error('Update post relations error:', error) throw error @@ -679,15 +674,12 @@ export const updatePostRelations = async ( export const togglePostStatus = async (id: number, isPublished: number): Promise => { try { - const response = await fetch(`${API_BASE}/admin/posts/${id}/status`, { + const response = await authFetch(`${API_BASE}/admin/posts/${id}/status`, { method: 'PATCH', headers: getAuthHeaders(), body: JSON.stringify({ isPublished }) }) - if (!response.ok) { - const errorData = await response.json() - throw new Error(errorData.message || '更新状态失败') - } + await parseApiResponse(response) } catch (error) { console.error('Toggle post status error:', error) throw error @@ -696,14 +688,11 @@ export const togglePostStatus = async (id: number, isPublished: number): Promise export const deletePost = async (id: number | string): Promise => { try { - const response = await fetch(`${API_BASE}/admin/posts/${id}`, { + const response = await authFetch(`${API_BASE}/admin/posts/${id}`, { method: 'DELETE', headers: getAuthHeaders() }) - if (!response.ok) { - const errorData = await response.json() - throw new Error(errorData.message || '删除文章失败') - } + await parseApiResponse(response) } catch (error) { console.error('Delete post error:', error) throw error @@ -714,9 +703,7 @@ export const deletePost = async (id: number | string): Promise => { export const fetchCategories = async (): Promise => { try { const response = await fetch(`${API_BASE}/categories`) - if (!response.ok) throw new Error('Failed to fetch categories') - const data = await response.json() - return data.result || [] + return await parseApiResponse(response) || [] } catch (error) { console.error('Fetch categories error:', error) return [] @@ -725,12 +712,10 @@ export const fetchCategories = async (): Promise => { export const getAdminCategories = async (): Promise => { try { - const response = await fetch(`${API_BASE}/admin/categories`, { + const response = await authFetch(`${API_BASE}/admin/categories`, { headers: getAuthHeaders() }) - if (!response.ok) throw new Error('Failed to fetch admin categories') - const data = await response.json() - return data.result || [] + return await parseApiResponse(response) || [] } catch (error) { console.error('Get admin categories error:', error) throw error @@ -739,12 +724,12 @@ export const getAdminCategories = async (): Promise => { export const createCategory = async (data: Omit): Promise => { try { - const response = await fetch(`${API_BASE}/admin/categories`, { + const response = await authFetch(`${API_BASE}/admin/categories`, { method: 'POST', headers: getAuthHeaders(), body: JSON.stringify(data) }) - if (!response.ok) throw new Error('Create category failed') + await parseApiResponse(response) } catch (error) { throw error } @@ -752,12 +737,12 @@ export const createCategory = async (data: Omit): Promise => { try { - const response = await fetch(`${API_BASE}/admin/categories/${id}`, { + const response = await authFetch(`${API_BASE}/admin/categories/${id}`, { method: 'PUT', headers: getAuthHeaders(), body: JSON.stringify(data) }) - if (!response.ok) throw new Error('Update category failed') + await parseApiResponse(response) } catch (error) { throw error } @@ -765,11 +750,11 @@ export const updateCategory = async (id: number, data: Omit => { try { - const response = await fetch(`${API_BASE}/admin/categories/${id}`, { + const response = await authFetch(`${API_BASE}/admin/categories/${id}`, { method: 'DELETE', headers: getAuthHeaders() }) - if (!response.ok) throw new Error('Delete category failed') + await parseApiResponse(response) } catch (error) { throw error } @@ -779,9 +764,7 @@ export const deleteCategory = async (id: number): Promise => { export const fetchColumns = async (): Promise => { try { const response = await fetch(`${API_BASE}/columns`) - if (!response.ok) throw new Error('Failed to fetch columns') - const data = await response.json() - return data.result || [] + return await parseApiResponse(response) || [] } catch (error) { return [] } @@ -790,9 +773,7 @@ export const fetchColumns = async (): Promise => { export const fetchColumn = async (id: number): Promise => { try { const response = await fetch(`${API_BASE}/columns/${id}`) - if (!response.ok) throw new Error('Failed to fetch column') - const data = await response.json() - return data.result + return await parseApiResponse(response) } catch (error) { throw error } @@ -800,12 +781,10 @@ export const fetchColumn = async (id: number): Promise => { export const getAdminColumns = async (): Promise => { try { - const response = await fetch(`${API_BASE}/admin/columns`, { + const response = await authFetch(`${API_BASE}/admin/columns`, { headers: getAuthHeaders() }) - if (!response.ok) throw new Error('Failed to fetch admin columns') - const data = await response.json() - return data.result || [] + return await parseApiResponse(response) || [] } catch (error) { throw error } @@ -813,12 +792,12 @@ export const getAdminColumns = async (): Promise => { export const createColumn = async (data: Omit): Promise => { try { - const response = await fetch(`${API_BASE}/admin/columns`, { + const response = await authFetch(`${API_BASE}/admin/columns`, { method: 'POST', headers: getAuthHeaders(), body: JSON.stringify(data) }) - if (!response.ok) throw new Error('Create column failed') + await parseApiResponse(response) } catch (error) { throw error } @@ -826,12 +805,12 @@ export const createColumn = async (data: Omit): Promise => { try { - const response = await fetch(`${API_BASE}/admin/columns/${id}`, { + const response = await authFetch(`${API_BASE}/admin/columns/${id}`, { method: 'PUT', headers: getAuthHeaders(), body: JSON.stringify(data) }) - if (!response.ok) throw new Error('Update column failed') + await parseApiResponse(response) } catch (error) { throw error } @@ -839,11 +818,11 @@ export const updateColumn = async (id: number, data: Omit => { try { - const response = await fetch(`${API_BASE}/admin/columns/${id}`, { + const response = await authFetch(`${API_BASE}/admin/columns/${id}`, { method: 'DELETE', headers: getAuthHeaders() }) - if (!response.ok) throw new Error('Delete column failed') + await parseApiResponse(response) } catch (error) { throw error } @@ -852,9 +831,7 @@ export const deleteColumn = async (id: number): Promise => { export const fetchColumnPosts = async (id: number): Promise => { try { const response = await fetch(`${API_BASE}/columns/${id}/posts`) - if (!response.ok) throw new Error('Failed to fetch column posts') - const data = await response.json() - return data.result || [] + return await parseApiResponse(response) || [] } catch (error) { console.error('Fetch column posts error:', error) return [] @@ -863,12 +840,12 @@ export const fetchColumnPosts = async (id: number): Promise => { export const addPostToColumn = async (columnId: number, postId: number): Promise => { try { - const response = await fetch(`${API_BASE}/admin/columns/${columnId}/posts`, { + const response = await authFetch(`${API_BASE}/admin/columns/${columnId}/posts`, { method: 'POST', headers: getAuthHeaders(), body: JSON.stringify({ postId }) }) - if (!response.ok) throw new Error('Add post to column failed') + await parseApiResponse(response) } catch (error) { throw error } @@ -876,11 +853,11 @@ export const addPostToColumn = async (columnId: number, postId: number): Promise export const removePostFromColumn = async (columnId: number, postId: number): Promise => { try { - const response = await fetch(`${API_BASE}/admin/columns/${columnId}/posts/${postId}`, { + const response = await authFetch(`${API_BASE}/admin/columns/${columnId}/posts/${postId}`, { method: 'DELETE', headers: getAuthHeaders() }) - if (!response.ok) throw new Error('Remove post from column failed') + await parseApiResponse(response) } catch (error) { throw error } @@ -890,9 +867,7 @@ export const removePostFromColumn = async (columnId: number, postId: number): Pr export const fetchTags = async (): Promise => { try { const response = await fetch(`${API_BASE}/tags`) - if (!response.ok) throw new Error('Failed to fetch tags') - const data = await response.json() - return data.result || [] + return await parseApiResponse(response) || [] } catch (error) { return [] } @@ -900,12 +875,10 @@ export const fetchTags = async (): Promise => { export const adminGetTags = async (): Promise => { try { - const response = await fetch(`${API_BASE}/admin/tags`, { + const response = await authFetch(`${API_BASE}/admin/tags`, { headers: getAuthHeaders() }) - if (!response.ok) throw new Error('Failed to fetch admin tags') - const data = await response.json() - return data.result || [] + return await parseApiResponse(response) || [] } catch (error) { throw error } @@ -913,12 +886,10 @@ export const adminGetTags = async (): Promise => { export const adminGetTag = async (id: number): Promise => { try { - const response = await fetch(`${API_BASE}/admin/tags/${id}`, { + const response = await authFetch(`${API_BASE}/admin/tags/${id}`, { headers: getAuthHeaders() }) - if (!response.ok) throw new Error('Failed to fetch admin tag') - const data = await response.json() - return data.result // Adapt to standard response + return await parseApiResponse(response) // Adapt to standard response } catch (error) { throw error } @@ -926,12 +897,12 @@ export const adminGetTag = async (id: number): Promise => { export const createTag = async (data: { name: string; slug: string }): Promise => { try { - const response = await fetch(`${API_BASE}/admin/tags`, { + const response = await authFetch(`${API_BASE}/admin/tags`, { method: 'POST', headers: getAuthHeaders(), body: JSON.stringify(data) }) - if (!response.ok) throw new Error('Create tag failed') + await parseApiResponse(response) } catch (error) { throw error } @@ -939,12 +910,12 @@ export const createTag = async (data: { name: string; slug: string }): Promise => { try { - const response = await fetch(`${API_BASE}/admin/tags/${id}`, { + const response = await authFetch(`${API_BASE}/admin/tags/${id}`, { method: 'PUT', headers: getAuthHeaders(), body: JSON.stringify(data) }) - if (!response.ok) throw new Error('Update tag failed') + await parseApiResponse(response) } catch (error) { throw error } @@ -952,11 +923,11 @@ export const updateTag = async (id: number, data: { name: string; slug: string } export const deleteTag = async (id: number): Promise => { try { - const response = await fetch(`${API_BASE}/admin/tags/${id}`, { + const response = await authFetch(`${API_BASE}/admin/tags/${id}`, { method: 'DELETE', headers: getAuthHeaders() }) - if (!response.ok) throw new Error('Delete tag failed') + await parseApiResponse(response) } catch (error) { throw error } @@ -965,15 +936,10 @@ export const deleteTag = async (id: number): Promise => { // 文章历史记录API export const getPostHistory = async (postId: number | string): Promise => { try { - const response = await fetch(`${API_BASE}/admin/posts/${postId}/history`, { + const response = await authFetch(`${API_BASE}/admin/posts/${postId}/history`, { headers: getAuthHeaders() }) - if (!response.ok) { - const errorData = await response.json() - throw new Error(errorData.message || '获取文章历史失败') - } - const data = await response.json() - return data.result || [] + return await parseApiResponse(response) || [] } catch (error) { console.error('Get post history error:', error) throw error @@ -982,34 +948,48 @@ export const getPostHistory = async (postId: number | string): Promise => { try { - const response = await fetch(`${API_BASE}/admin/posts/${postId}/history/${version}`, { + const response = await authFetch(`${API_BASE}/admin/posts/${postId}/history/${version}`, { headers: getAuthHeaders() }) - if (!response.ok) { - const errorData = await response.json() - throw new Error(errorData.message || '获取指定版本文章历史失败') - } - const data = await response.json() - return data.result + return await parseApiResponse(response) } catch (error) { console.error('Get post history by version error:', error) throw error } } +export const getPostHistoryDiff = async ( + postId: number | string, + fromVersion: number, + toVersion: number +): Promise => { + const response = await authFetch( + `${API_BASE}/admin/posts/${postId}/history/diff?from=${fromVersion}&to=${toVersion}`, + { headers: getAuthHeaders() } + ) + return await parseApiResponse(response) +} + +export const restorePostHistory = async (postId: number | string, version: number): Promise => { + const response = await authFetch(`${API_BASE}/admin/posts/${postId}/history/${version}/restore`, { + method: 'POST', + headers: getAuthHeaders() + }) + return await parseApiResponse(response) +} + +export const getHotSearches = async (limit = 10, days = 30): Promise => { + return (await fetchJson(`${API_BASE}/search/hot?limit=${limit}&days=${days}`)) || [] +} + // 代码片段管理API export const getAdminSnippets = async (): Promise => { try { - const response = await fetch(`${API_BASE}/admin/snippets`, { + const response = await authFetch(`${API_BASE}/admin/snippets`, { headers: getAuthHeaders() }) - if (!response.ok) { - const errorData = await response.json() - throw new Error(errorData.message || '获取代码片段列表失败') - } - const data = await response.json() - // 后端返回的是分页格式 {list, total, page, size},需要从 result.list 中取值 - return (data.result?.list || []) as Snippet[] + const data = await parseApiResponse(response) + return (data?.list || []) as Snippet[] } catch (error) { console.error('Get admin snippets error:', error) throw error @@ -1019,9 +999,7 @@ export const getAdminSnippets = async (): Promise => { export const fetchSnippets = async (): Promise => { try { const response = await fetch(`${API_BASE}/snippets`) - if (!response.ok) throw new Error('Failed to fetch snippets') - const data = await response.json() - return data.result || [] + return await parseApiResponse(response) || [] } catch (error) { console.error('Error fetching snippets:', error) return [] @@ -1031,9 +1009,7 @@ export const fetchSnippets = async (): Promise => { export const fetchSnippet = async (id: string): Promise => { try { const response = await fetch(`${API_BASE}/snippets/${id}`) - if (!response.ok) throw new Error('Failed to fetch snippet') - const data = await response.json() - return data.result + return await parseApiResponse(response) } catch (error) { console.error(`Error fetching snippet ${id}:`, error) return { @@ -1047,15 +1023,12 @@ export const fetchSnippet = async (id: string): Promise => { export const createSnippet = async (snippetData: Omit): Promise => { try { - const response = await fetch(`${API_BASE}/admin/snippets`, { + const response = await authFetch(`${API_BASE}/admin/snippets`, { method: 'POST', headers: getAuthHeaders(), body: JSON.stringify(snippetData) }) - if (!response.ok) { - const errorData = await response.json() - throw new Error(errorData.message || '创建代码片段失败') - } + await parseApiResponse(response) } catch (error) { console.error('Create snippet error:', error) throw error @@ -1064,15 +1037,12 @@ export const createSnippet = async (snippetData: Omit): Promise => { try { - const response = await fetch(`${API_BASE}/admin/snippets/${id}`, { + const response = await authFetch(`${API_BASE}/admin/snippets/${id}`, { method: 'PUT', headers: getAuthHeaders(), body: JSON.stringify(snippetData) }) - if (!response.ok) { - const errorData = await response.json() - throw new Error(errorData.message || '更新代码片段失败') - } + await parseApiResponse(response) } catch (error) { console.error('Update snippet error:', error) throw error @@ -1081,14 +1051,11 @@ export const updateSnippet = async (id: string, snippetData: Omit => { try { - const response = await fetch(`${API_BASE}/admin/snippets/${id}`, { + const response = await authFetch(`${API_BASE}/admin/snippets/${id}`, { method: 'DELETE', headers: getAuthHeaders() }) - if (!response.ok) { - const errorData = await response.json() - throw new Error(errorData.message || '删除代码片段失败') - } + await parseApiResponse(response) } catch (error) { console.error('Delete snippet error:', error) throw error @@ -1098,19 +1065,14 @@ export const deleteSnippet = async (id: string): Promise => { // 系统配置API export const getSettings = async (): Promise => { try { - const response = await fetch(`${API_BASE}/admin/settings`, { + const response = await authFetch(`${API_BASE}/admin/settings`, { headers: getAuthHeaders() }) - if (!response.ok) { - const errorData = await response.json() - throw new Error(errorData.message || '获取系统配置失败') - } - const data = await response.json() - // 确保从 result 字段取值,并确保始终返回数组 - if (!data.result) { + const data = await parseApiResponse(response) + if (!data) { return [] } - return Array.isArray(data.result) ? data.result : [] + return Array.isArray(data) ? data : [] } catch (error) { console.error('Get settings error:', error) throw error @@ -1119,15 +1081,12 @@ export const getSettings = async (): Promise => { export const createSetting = async (settingData: Omit): Promise => { try { - const response = await fetch(`${API_BASE}/admin/settings`, { + const response = await authFetch(`${API_BASE}/admin/settings`, { method: 'POST', headers: getAuthHeaders(), body: JSON.stringify(settingData) }) - if (!response.ok) { - const errorData = await response.json() - throw new Error(errorData.message || '创建系统配置失败') - } + await parseApiResponse(response) } catch (error) { console.error('Create setting error:', error) throw error @@ -1136,15 +1095,12 @@ export const createSetting = async (settingData: Omit): Promise => { try { - const response = await fetch(`${API_BASE}/admin/settings`, { + const response = await authFetch(`${API_BASE}/admin/settings`, { method: 'PUT', headers: getAuthHeaders(), body: JSON.stringify(settingData) }) - if (!response.ok) { - const errorData = await response.json() - throw new Error(errorData.message || '更新系统配置失败') - } + await parseApiResponse(response) } catch (error) { console.error('Update setting error:', error) throw error @@ -1153,14 +1109,11 @@ export const updateSetting = async (settingData: Omit => { try { - const response = await fetch(`${API_BASE}/admin/settings/${keyName}`, { + const response = await authFetch(`${API_BASE}/admin/settings/${keyName}`, { method: 'DELETE', headers: getAuthHeaders() }) - if (!response.ok) { - const errorData = await response.json() - throw new Error(errorData.message || '删除系统配置失败') - } + await parseApiResponse(response) } catch (error) { console.error('Delete setting error:', error) throw error @@ -1180,12 +1133,7 @@ export interface PublicSettings { export const getPublicSettings = async (): Promise => { try { const response = await fetch(`${API_BASE}/settings`) - if (!response.ok) { - const errorData = await response.json() - throw new Error(errorData.message || '获取网站配置失败') - } - const data = await response.json() - return data.result || {} + return await parseApiResponse(response) || {} } catch (error) { console.error('Get public settings error:', error) // 返回空对象,前端使用默认值 @@ -1196,15 +1144,10 @@ export const getPublicSettings = async (): Promise => { // 操作日志API export const getOperationLogs = async (page: number = 1, pageSize: number = 10): Promise> => { try { - const response = await fetch(`${API_BASE}/admin/operation-logs?page=${page}&pageSize=${pageSize}`, { + const response = await authFetch(`${API_BASE}/admin/operation-logs?page=${page}&pageSize=${pageSize}`, { headers: getAuthHeaders() }) - if (!response.ok) { - const errorData = await response.json() - throw new Error(errorData.message || '获取操作日志失败') - } - const data = await response.json() - return data.result + return await parseApiResponse(response) } catch (error) { console.error('Get operation logs error:', error) throw error @@ -1227,15 +1170,10 @@ export interface AccessLog { // 访问日志API export const getAccessLogs = async (page: number = 1, pageSize: number = 10): Promise> => { try { - const response = await fetch(`${API_BASE}/admin/access-logs?page=${page}&pageSize=${pageSize}`, { + const response = await authFetch(`${API_BASE}/admin/access-logs?page=${page}&pageSize=${pageSize}`, { headers: getAuthHeaders() }) - if (!response.ok) { - const errorData = await response.json() - throw new Error(errorData.message || '获取访问日志失败') - } - const data = await response.json() - return data.result + return await parseApiResponse(response) } catch (error) { console.error('Get access logs error:', error) throw error @@ -1255,15 +1193,10 @@ export interface PostAccessLog { // 获取指定文章的访问记录 export const getPostAccessLogs = async (postId: number, page: number = 1, pageSize: number = 20): Promise> => { try { - const response = await fetch(`${API_BASE}/admin/posts/${postId}/access-logs?page=${page}&pageSize=${pageSize}`, { + const response = await authFetch(`${API_BASE}/admin/posts/${postId}/access-logs?page=${page}&pageSize=${pageSize}`, { headers: getAuthHeaders() }) - if (!response.ok) { - const errorData = await response.json() - throw new Error(errorData.message || '获取文章访问记录失败') - } - const data = await response.json() - return data.result + return await parseApiResponse(response) } catch (error) { console.error('Get post access logs error:', error) throw error @@ -1295,15 +1228,9 @@ export const getDashboardStats = async (startDate?: string, endDate?: string): P if (endDate) params.append('endDate', endDate) if (params.toString()) url += `?${params.toString()}` - const response = await fetch(url, { + return await authFetchJson(url, { headers: getAuthHeaders() }) - if (!response.ok) { - const errorData = await response.json() - throw new Error(errorData.message || '获取仪表盘统计数据失败') - } - const data = await response.json() - return data.result } catch (error) { console.error('Get dashboard stats error:', error) throw error @@ -1313,15 +1240,10 @@ export const getDashboardStats = async (startDate?: string, endDate?: string): P // 获取最近活动 export const getRecentActivities = async (): Promise => { try { - const response = await fetch(`${API_BASE}/admin/dashboard/activities`, { + const response = await authFetch(`${API_BASE}/admin/dashboard/activities`, { headers: getAuthHeaders() }) - if (!response.ok) { - const errorData = await response.json() - throw new Error(errorData.message || '获取最近活动失败') - } - const data = await response.json() - return data.result || [] + return await parseApiResponse(response) || [] } catch (error) { console.error('Get recent activities error:', error) throw error @@ -1353,17 +1275,10 @@ export interface AboutProfile { // 关于页面API export const fetchAboutProfile = async (): Promise => { try { - const response = await fetch(`${API_BASE}/about`) - if (!response.ok) { - const errorData = await response.json() - throw new Error(errorData.message || '获取个人资料失败') - } - const data = await response.json() - // 确保从 result 字段取值 - if (!data.result) { + const result = await fetchJson(`${API_BASE}/about`) + if (!result) { throw new Error('个人资料数据为空') } - const result = data.result as AboutProfile // 确保 techStack 和 experiences 始终是数组 if (!Array.isArray(result.techStack)) { result.techStack = [] @@ -1380,15 +1295,10 @@ export const fetchAboutProfile = async (): Promise => { export const getAdminAboutProfiles = async (): Promise => { try { - const response = await fetch(`${API_BASE}/admin/about`, { + const response = await authFetch(`${API_BASE}/admin/about`, { headers: getAuthHeaders() }) - if (!response.ok) { - const errorData = await response.json() - throw new Error(errorData.message || '获取个人资料列表失败') - } - const data = await response.json() - return data.result || [] + return await parseApiResponse(response) || [] } catch (error) { console.error('Get admin about profiles error:', error) throw error @@ -1397,15 +1307,12 @@ export const getAdminAboutProfiles = async (): Promise => { export const createAboutProfile = async (profileData: Omit): Promise => { try { - const response = await fetch(`${API_BASE}/admin/about`, { + const response = await authFetch(`${API_BASE}/admin/about`, { method: 'POST', headers: getAuthHeaders(), body: JSON.stringify(profileData) }) - if (!response.ok) { - const errorData = await response.json() - throw new Error(errorData.message || '创建个人资料失败') - } + await parseApiResponse(response) } catch (error) { console.error('Create about profile error:', error) throw error @@ -1414,15 +1321,12 @@ export const createAboutProfile = async (profileData: Omit): Promise => { try { - const response = await fetch(`${API_BASE}/admin/about/${id}`, { + const response = await authFetch(`${API_BASE}/admin/about/${id}`, { method: 'PUT', headers: getAuthHeaders(), body: JSON.stringify(profileData) }) - if (!response.ok) { - const errorData = await response.json() - throw new Error(errorData.message || '更新个人资料失败') - } + await parseApiResponse(response) } catch (error) { console.error('Update about profile error:', error) throw error @@ -1431,14 +1335,11 @@ export const updateAboutProfile = async (id: number, profileData: Omit => { try { - const response = await fetch(`${API_BASE}/admin/about/${id}`, { + const response = await authFetch(`${API_BASE}/admin/about/${id}`, { method: 'DELETE', headers: getAuthHeaders() }) - if (!response.ok) { - const errorData = await response.json() - throw new Error(errorData.message || '删除个人资料失败') - } + await parseApiResponse(response) } catch (error) { console.error('Delete about profile error:', error) throw error @@ -1472,12 +1373,7 @@ export interface Partner { export const fetchTestimonials = async (): Promise => { try { const response = await fetch(`${API_BASE}/testimonials`) - if (!response.ok) { - const errorData = await response.json() - throw new Error(errorData.message || '获取客户评价失败') - } - const data = await response.json() - return data.result || [] + return await parseApiResponse(response) || [] } catch (error) { console.error('Fetch testimonials error:', error) throw error @@ -1486,15 +1382,12 @@ export const fetchTestimonials = async (): Promise => { export const createTestimonial = async (data: Omit): Promise => { try { - const response = await fetch(`${API_BASE}/admin/testimonials`, { + const response = await authFetch(`${API_BASE}/admin/testimonials`, { method: 'POST', headers: getAuthHeaders(), body: JSON.stringify(data) }) - if (!response.ok) { - const errorData = await response.json() - throw new Error(errorData.message || '创建客户评价失败') - } + await parseApiResponse(response) } catch (error) { console.error('Create testimonial error:', error) throw error @@ -1503,15 +1396,12 @@ export const createTestimonial = async (data: Omit): Promise => { try { - const response = await fetch(`${API_BASE}/admin/testimonials/${id}`, { + const response = await authFetch(`${API_BASE}/admin/testimonials/${id}`, { method: 'PUT', headers: getAuthHeaders(), body: JSON.stringify(data) }) - if (!response.ok) { - const errorData = await response.json() - throw new Error(errorData.message || '更新客户评价失败') - } + await parseApiResponse(response) } catch (error) { console.error('Update testimonial error:', error) throw error @@ -1520,14 +1410,11 @@ export const updateTestimonial = async (id: number, data: Omit => { try { - const response = await fetch(`${API_BASE}/admin/testimonials/${id}`, { + const response = await authFetch(`${API_BASE}/admin/testimonials/${id}`, { method: 'DELETE', headers: getAuthHeaders() }) - if (!response.ok) { - const errorData = await response.json() - throw new Error(errorData.message || '删除客户评价失败') - } + await parseApiResponse(response) } catch (error) { console.error('Delete testimonial error:', error) throw error @@ -1538,12 +1425,7 @@ export const deleteTestimonial = async (id: number): Promise => { export const fetchPartners = async (): Promise => { try { const response = await fetch(`${API_BASE}/partners`) - if (!response.ok) { - const errorData = await response.json() - throw new Error(errorData.message || '获取合作伙伴失败') - } - const data = await response.json() - return data.result || [] + return await parseApiResponse(response) || [] } catch (error) { console.error('Fetch partners error:', error) throw error @@ -1552,15 +1434,12 @@ export const fetchPartners = async (): Promise => { export const createPartner = async (data: Omit): Promise => { try { - const response = await fetch(`${API_BASE}/admin/partners`, { + const response = await authFetch(`${API_BASE}/admin/partners`, { method: 'POST', headers: getAuthHeaders(), body: JSON.stringify(data) }) - if (!response.ok) { - const errorData = await response.json() - throw new Error(errorData.message || '创建合作伙伴失败') - } + await parseApiResponse(response) } catch (error) { console.error('Create partner error:', error) throw error @@ -1569,15 +1448,12 @@ export const createPartner = async (data: Omit): Promise => { try { - const response = await fetch(`${API_BASE}/admin/partners/${id}`, { + const response = await authFetch(`${API_BASE}/admin/partners/${id}`, { method: 'PUT', headers: getAuthHeaders(), body: JSON.stringify(data) }) - if (!response.ok) { - const errorData = await response.json() - throw new Error(errorData.message || '更新合作伙伴失败') - } + await parseApiResponse(response) } catch (error) { console.error('Update partner error:', error) throw error @@ -1586,14 +1462,11 @@ export const updatePartner = async (id: number, data: Omit => { try { - const response = await fetch(`${API_BASE}/admin/partners/${id}`, { + const response = await authFetch(`${API_BASE}/admin/partners/${id}`, { method: 'DELETE', headers: getAuthHeaders() }) - if (!response.ok) { - const errorData = await response.json() - throw new Error(errorData.message || '删除合作伙伴失败') - } + await parseApiResponse(response) } catch (error) { console.error('Delete partner error:', error) throw error @@ -1674,17 +1547,11 @@ export interface EmailSuffix { // 咨询相关API export const submitInquiry = async (data: Inquiry): Promise => { try { - const response = await fetch(`${API_BASE}/inquiries`, { + await fetchJson(`${API_BASE}/inquiries`, { method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, + headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }) - if (!response.ok) { - const errorData = await response.json() - throw new Error(errorData.message || '提交咨询失败') - } } catch (error) { console.error('Submit inquiry error:', error) throw error @@ -1694,12 +1561,7 @@ export const submitInquiry = async (data: Inquiry): Promise => { export const fetchEmailSuffixes = async (): Promise => { try { const response = await fetch(`${API_BASE}/email-suffixes`) - if (!response.ok) { - const errorData = await response.json() - throw new Error(errorData.message || '获取邮箱后缀失败') - } - const data = await response.json() - return data.result || [] + return await parseApiResponse(response) || [] } catch (error) { console.error('Fetch email suffixes error:', error) throw error @@ -1708,15 +1570,10 @@ export const fetchEmailSuffixes = async (): Promise => { export const fetchInquiries = async (): Promise => { try { - const response = await fetch(`${API_BASE}/admin/inquiries`, { + const response = await authFetch(`${API_BASE}/admin/inquiries`, { headers: getAuthHeaders() }) - if (!response.ok) { - const errorData = await response.json() - throw new Error(errorData.message || '获取咨询列表失败') - } - const data = await response.json() - return data.result || [] + return await parseApiResponse(response) || [] } catch (error) { console.error('Fetch inquiries error:', error) throw error @@ -1726,15 +1583,10 @@ export const fetchInquiries = async (): Promise => { // OSS配置相关API export const getOSSConfigs = async (): Promise => { try { - const response = await fetch(`${API_BASE}/admin/oss-configs`, { + const response = await authFetch(`${API_BASE}/admin/oss-configs`, { headers: getAuthHeaders() }) - if (!response.ok) { - const errorData = await response.json() - throw new Error(errorData.message || '获取OSS配置失败') - } - const data = await response.json() - return data.result || [] + return await parseApiResponse(response) || [] } catch (error) { console.error('Get OSS configs error:', error) throw error @@ -1743,17 +1595,12 @@ export const getOSSConfigs = async (): Promise => { export const createOSSConfig = async (configData: Omit): Promise => { try { - const response = await fetch(`${API_BASE}/admin/oss-configs`, { + const response = await authFetch(`${API_BASE}/admin/oss-configs`, { method: 'POST', headers: getAuthHeaders(), body: JSON.stringify(configData) }) - if (!response.ok) { - const errorData = await response.json() - throw new Error(errorData.message || '创建OSS配置失败') - } - const data = await response.json() - return data.result + return await parseApiResponse(response) } catch (error) { console.error('Create OSS config error:', error) throw error @@ -1762,17 +1609,12 @@ export const createOSSConfig = async (configData: Omit>): Promise => { try { - const response = await fetch(`${API_BASE}/admin/oss-configs/${id}`, { + const response = await authFetch(`${API_BASE}/admin/oss-configs/${id}`, { method: 'PUT', headers: getAuthHeaders(), body: JSON.stringify(configData) }) - if (!response.ok) { - const errorData = await response.json() - throw new Error(errorData.message || '更新OSS配置失败') - } - const data = await response.json() - return data.result + return await parseApiResponse(response) } catch (error) { console.error('Update OSS config error:', error) throw error @@ -1781,14 +1623,11 @@ export const updateOSSConfig = async (id: number, configData: Partial => { try { - const response = await fetch(`${API_BASE}/admin/oss-configs/${id}`, { + const response = await authFetch(`${API_BASE}/admin/oss-configs/${id}`, { method: 'DELETE', headers: getAuthHeaders() }) - if (!response.ok) { - const errorData = await response.json() - throw new Error(errorData.message || '删除OSS配置失败') - } + await parseApiResponse(response) } catch (error) { console.error('Delete OSS config error:', error) throw error @@ -1843,19 +1682,14 @@ export const getAdminAttachments = async (params?: { if (params?.keyword) queryParams.append('keyword', params.keyword) const url = `${API_BASE}/admin/attachments${queryParams.toString() ? '?' + queryParams.toString() : ''}` - const response = await fetch(url, { + const data = await authFetchJson>(url, { headers: getAuthHeaders() }) - if (!response.ok) { - const errorData = await response.json() - throw new Error(errorData.message || '获取附件列表失败') - } - const data = await response.json() return { - list: data.result?.list || [], - total: data.result?.total || 0, - page: data.result?.page || 1, - size: data.result?.size || 20 + list: data?.list || [], + total: data?.total || 0, + page: data?.page || 1, + size: data?.size || 20 } } catch (error) { console.error('Get admin attachments error:', error) @@ -1865,17 +1699,11 @@ export const getAdminAttachments = async (params?: { export const updateAttachment = async (id: number, data: { categoryId?: number | null }): Promise => { try { - const response = await fetch(`${API_BASE}/admin/attachments/${id}`, { + return await authFetchJson(`${API_BASE}/admin/attachments/${id}`, { method: 'PUT', headers: getAuthHeaders(), body: JSON.stringify(data) }) - if (!response.ok) { - const errorData = await response.json() - throw new Error(errorData.message || '更新附件失败') - } - const responseData = await response.json() - return responseData.result } catch (error) { console.error('Update attachment error:', error) throw error @@ -1885,15 +1713,10 @@ export const updateAttachment = async (id: number, data: { categoryId?: number | // 附件分类管理API export const getAttachmentCategories = async (): Promise => { try { - const response = await fetch(`${API_BASE}/admin/attachment-categories`, { + const response = await authFetch(`${API_BASE}/admin/attachment-categories`, { headers: getAuthHeaders() }) - if (!response.ok) { - const errorData = await response.json() - throw new Error(errorData.message || '获取附件分类列表失败') - } - const data = await response.json() - return data.result || [] + return await parseApiResponse(response) || [] } catch (error) { console.error('Get attachment categories error:', error) throw error @@ -1902,15 +1725,12 @@ export const getAttachmentCategories = async (): Promise = export const createAttachmentCategory = async (categoryData: Omit): Promise => { try { - const response = await fetch(`${API_BASE}/admin/attachment-categories`, { + const response = await authFetch(`${API_BASE}/admin/attachment-categories`, { method: 'POST', headers: getAuthHeaders(), body: JSON.stringify(categoryData) }) - if (!response.ok) { - const errorData = await response.json() - throw new Error(errorData.message || '创建附件分类失败') - } + await parseApiResponse(response) } catch (error) { console.error('Create attachment category error:', error) throw error @@ -1919,15 +1739,12 @@ export const createAttachmentCategory = async (categoryData: Omit>): Promise => { try { - const response = await fetch(`${API_BASE}/admin/attachment-categories/${id}`, { + const response = await authFetch(`${API_BASE}/admin/attachment-categories/${id}`, { method: 'PUT', headers: getAuthHeaders(), body: JSON.stringify(categoryData) }) - if (!response.ok) { - const errorData = await response.json() - throw new Error(errorData.message || '更新附件分类失败') - } + await parseApiResponse(response) } catch (error) { console.error('Update attachment category error:', error) throw error @@ -1936,14 +1753,11 @@ export const updateAttachmentCategory = async (id: number, categoryData: Partial export const deleteAttachmentCategory = async (id: number): Promise => { try { - const response = await fetch(`${API_BASE}/admin/attachment-categories/${id}`, { + const response = await authFetch(`${API_BASE}/admin/attachment-categories/${id}`, { method: 'DELETE', headers: getAuthHeaders() }) - if (!response.ok) { - const errorData = await response.json() - throw new Error(errorData.message || '删除附件分类失败') - } + await parseApiResponse(response) } catch (error) { console.error('Delete attachment category error:', error) throw error diff --git a/server/about_page.sql b/server/about_page.sql deleted file mode 100644 index 5a9f559..0000000 --- a/server/about_page.sql +++ /dev/null @@ -1,16 +0,0 @@ -CREATE TABLE IF NOT EXISTS `about_profiles` ( - `id` INT AUTO_INCREMENT PRIMARY KEY, - `name` VARCHAR(255) NOT NULL, - `avatar` VARCHAR(255) DEFAULT '', - `location` VARCHAR(255) DEFAULT '', - `bio` TEXT, - `email` VARCHAR(255) DEFAULT '', - `wechat` VARCHAR(255) DEFAULT '', - `tech_stack` TEXT COMMENT 'JSON string or comma separated list', - `is_primary` BOOLEAN DEFAULT FALSE, - `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; - -INSERT INTO `about_profiles` (`name`, `avatar`, `location`, `bio`, `email`, `wechat`, `tech_stack`, `is_primary`) VALUES -('年糕崽崽', 'https://api.dicebear.com/7.x/avataaars/svg?seed=NianGao&backgroundColor=b6e3f4', '中国 · 浙江杭州', '嗨!我是年糕崽崽,一名热衷于探索数字边界的前端架构师。我的职业生涯始于对像素级完美的执念。从早期的 jQuery 插件开发到如今的 WebGL 3D 场景构建,我始终认为:技术是骨架,艺术是灵魂。

目前,我专注于高性能 B 端应用的体验升级,以及探索生成式艺术(Generative Art)在网页中的应用。闲暇之余,我喜欢在杭州的西湖边散步,或者捣腾我的客制化机械键盘。', 'hello@niangao.dev', 'Niangao_Dev', '["Vue 3", "React", "TypeScript", "Three.js", "Golang", "Tailwind CSS", "Rust", "Wails"]', TRUE); diff --git a/server/cleanup_tables.sql b/server/cleanup_tables.sql deleted file mode 100644 index 34a5198..0000000 --- a/server/cleanup_tables.sql +++ /dev/null @@ -1,15 +0,0 @@ --- 清理脚本:删除可能有问题的表 --- 在执行 nl_blog.sql 之前先执行此脚本 - -SET FOREIGN_KEY_CHECKS = 0; - --- 删除 operation_logs 表(如果存在) -DROP TABLE IF EXISTS `operation_logs`; - --- 删除 tags 表(如果存在) -DROP TABLE IF EXISTS `tags`; - --- 删除 post_tags 关联表(如果存在,因为它可能引用 tags) -DROP TABLE IF EXISTS `post_tags`; - -SET FOREIGN_KEY_CHECKS = 1; diff --git a/server/go.mod b/server/go.mod index 06117b2..17f4f47 100644 --- a/server/go.mod +++ b/server/go.mod @@ -48,6 +48,7 @@ require ( github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/quic-go/qpack v0.5.1 // indirect github.com/quic-go/quic-go v0.54.0 // indirect + github.com/sergi/go-diff v1.4.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.3.0 // indirect go.uber.org/mock v0.5.0 // indirect diff --git a/server/go.sum b/server/go.sum index 64a659a..1647d3a 100644 --- a/server/go.sum +++ b/server/go.sum @@ -116,6 +116,8 @@ github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTE github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= github.com/rs/dnscache v0.0.0-20230804202142-fc85eb664529/go.mod h1:qe5TWALJ8/a1Lqznoc5BDHpYX/8HU60Hm2AwRmqzxqA= +github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= +github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -173,10 +175,12 @@ google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7I google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/server/handlers/post.go b/server/handlers/post.go index 9189b1b..03d028f 100644 --- a/server/handlers/post.go +++ b/server/handlers/post.go @@ -139,57 +139,63 @@ func GetPost(c *gin.Context) { // 构建响应,包含内容 response := repositories.BuildPostResponse(post, true) - // 记录用户访问日志 (异步执行,不阻塞响应) - go func() { - // 添加 panic recover 保护 - defer func() { - if r := recover(); r != nil { - log.Printf("Panic in user access log goroutine for post %d: %v", post.ID, r) - } - }() + visitorKey := utils.GetOrSetVisitorID(c) - // 获取客户端IP - ip := c.ClientIP() + var userID uint = 0 + if uid, exists := c.Get("userID"); exists { + userID = uid.(uint) + } - // 获取归属地,使用 recover 保护 - var location string - func() { + alreadyVisited, err := repositories.HasVisitedThisHour(post.ID, userID, visitorKey) + if err != nil { + log.Printf("Failed to check visit dedup for post %d: %v", post.ID, err) + } + + if !alreadyVisited { + if err := repositories.IncrementReadCount(post.ID); err != nil { + log.Printf("Failed to increment read count for post %d: %v", post.ID, err) + } else { + response.ReadCount++ + } + + // 记录用户访问日志 (异步执行,不阻塞响应) + go func() { defer func() { if r := recover(); r != nil { - log.Printf("Panic in GetRegion for IP %s (post %d): %v", ip, post.ID, r) - location = "Unknown" + log.Printf("Panic in user access log goroutine for post %d: %v", post.ID, r) } }() - location = utils.GetRegion(ip) + + ip := c.ClientIP() + + var location string + func() { + defer func() { + if r := recover(); r != nil { + log.Printf("Panic in GetRegion for IP %s (post %d): %v", ip, post.ID, r) + location = "Unknown" + } + }() + location = utils.GetRegion(ip) + }() + + if location == "" { + location = "Unknown" + } + + logEntry := &models.UserAccessLog{ + UserID: userID, + UserIP: ip, + UserLocation: location, + ArticleID: post.ID, + VisitorKey: visitorKey, + } + + if err := repositories.CreateUserAccessLog(logEntry); err != nil { + log.Printf("Failed to create user access log for PostID=%d, IP=%s: %v", post.ID, ip, err) + } }() - - // 确保 location 不为空,如果为空则设置为 "Unknown" - if location == "" { - location = "Unknown" - } - - // 记录调试信息 - log.Printf("Creating user access log: PostID=%d, IP=%s, Location=%s", post.ID, ip, location) - - // 获取用户ID (如果已登录) - var userID uint = 0 - if uid, exists := c.Get("userID"); exists { - userID = uid.(uint) - } - - logEntry := &models.UserAccessLog{ - UserID: userID, - UserIP: ip, - UserLocation: location, - ArticleID: post.ID, - } - - if err := repositories.CreateUserAccessLog(logEntry); err != nil { - log.Printf("Failed to create user access log for PostID=%d, IP=%s, Location=%s: %v", post.ID, ip, location, err) - } else { - log.Printf("Successfully created user access log: PostID=%d, IP=%s, Location=%s", post.ID, ip, location) - } - }() + } utils.Success(c, response) } @@ -412,7 +418,62 @@ func AdminGetPostHistoryByVersion(c *gin.Context) { return } - utils.Success(c, repositories.BuildPostHistoryResponse(history)) + utils.Success(c, repositories.BuildPostHistoryResponse(history, true)) +} + +// AdminGetPostHistoryDiff 对比两个历史版本 +func AdminGetPostHistoryDiff(c *gin.Context) { + postIDStr := c.Param("id") + var postID uint + if _, err := fmt.Sscanf(postIDStr, "%d", &postID); err != nil { + utils.Error(c, 400, "Invalid post ID") + return + } + + fromVersion, err := strconv.ParseUint(c.Query("from"), 10, 32) + if err != nil { + utils.Error(c, 400, "Invalid from version") + return + } + toVersion, err := strconv.ParseUint(c.Query("to"), 10, 32) + if err != nil { + utils.Error(c, 400, "Invalid to version") + return + } + + diff, err := repositories.GetPostHistoryDiff(postID, uint(fromVersion), uint(toVersion)) + if err != nil { + utils.Error(c, 404, err.Error()) + return + } + + utils.Success(c, diff) +} + +// AdminRestorePostHistory 恢复指定历史版本 +func AdminRestorePostHistory(c *gin.Context) { + postIDStr := c.Param("id") + var postID uint + if _, err := fmt.Sscanf(postIDStr, "%d", &postID); err != nil { + utils.Error(c, 400, "Invalid post ID") + return + } + + versionStr := c.Param("version") + versionUint, err := strconv.ParseUint(versionStr, 10, 32) + if err != nil { + utils.Error(c, 400, "Invalid version") + return + } + + userID, _ := c.Get("userID") + post, err := repositories.RestorePostFromHistory(postID, uint(versionUint), userID.(uint)) + if err != nil { + utils.ServerError(c, err) + return + } + + utils.Success(c, repositories.BuildPostResponse(post, true)) } // GetPostsByTagID 根据标签ID获取文章 diff --git a/server/handlers/search_log.go b/server/handlers/search_log.go index 6608fb1..229f439 100644 --- a/server/handlers/search_log.go +++ b/server/handlers/search_log.go @@ -47,6 +47,23 @@ func AdminDeleteSearchLog(c *gin.Context) { utils.SuccessWithMsg(c, "Search log deleted successfully", nil) } +// GetHotSearches 获取热门搜索关键词(公开) +func GetHotSearches(c *gin.Context) { + limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10")) + days, _ := strconv.Atoi(c.DefaultQuery("days", "30")) + + keywords, err := repositories.GetHotKeywords(limit, days) + if err != nil { + utils.ServerError(c, err) + return + } + if keywords == nil { + keywords = []repositories.HotKeyword{} + } + + utils.Success(c, keywords) +} + // LogSearch 记录搜索(异步,不阻塞) func LogSearch(keyword, searchType, userIP, userLocation string) { go func() { diff --git a/server/main.go b/server/main.go index 6ab7479..4e60188 100644 --- a/server/main.go +++ b/server/main.go @@ -56,6 +56,7 @@ func main() { // 博客路由 api.GET("/posts", handlers.GetPosts) api.GET("/posts/:id", handlers.GetPost) + api.GET("/search/hot", handlers.GetHotSearches) api.GET("/posts/:id/recommendations", handlers.GetRecommendedPosts) // 分类路由 @@ -168,6 +169,8 @@ func main() { // 文章历史记录 authAdmin.GET("/posts/:id/history", middleware.PermissionMiddleware("posts", "read"), handlers.AdminGetPostHistory) + authAdmin.GET("/posts/:id/history/diff", middleware.PermissionMiddleware("posts", "read"), handlers.AdminGetPostHistoryDiff) + authAdmin.POST("/posts/:id/history/:version/restore", middleware.PermissionMiddleware("posts", "update"), handlers.AdminRestorePostHistory) authAdmin.GET("/posts/:id/history/:version", middleware.PermissionMiddleware("posts", "read"), handlers.AdminGetPostHistoryByVersion) // 操作日志管理 diff --git a/server/middleware/auth.go b/server/middleware/auth.go index 5e4ca06..da958b0 100644 --- a/server/middleware/auth.go +++ b/server/middleware/auth.go @@ -2,7 +2,6 @@ package middleware import ( "errors" - "net/http" "os" "strings" "time" @@ -10,6 +9,7 @@ import ( "github.com/gin-gonic/gin" "github.com/golang-jwt/jwt/v5" "github.com/niangaodev/art-code/repositories" + "github.com/niangaodev/art-code/utils" ) // JWT密钥 @@ -96,7 +96,7 @@ func AuthMiddleware() gin.HandlerFunc { // 从请求头中获取令牌 authHeader := c.GetHeader("Authorization") if authHeader == "" { - c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header is required"}) + utils.Error(c, 401, "登录已过期,请重新登录") c.Abort() return } @@ -104,7 +104,7 @@ func AuthMiddleware() gin.HandlerFunc { // 检查令牌格式 parts := strings.SplitN(authHeader, " ", 2) if !(len(parts) == 2 && parts[0] == "Bearer") { - c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header format must be Bearer {token}"}) + utils.Error(c, 401, "登录已过期,请重新登录") c.Abort() return } @@ -112,7 +112,7 @@ func AuthMiddleware() gin.HandlerFunc { // 解析令牌 claims, err := ParseToken(parts[1]) if err != nil { - c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid or expired token"}) + utils.Error(c, 401, "登录已过期,请重新登录") c.Abort() return } @@ -133,7 +133,7 @@ func RoleMiddleware(roles ...string) gin.HandlerFunc { // 从上下文中获取用户角色 role, exists := c.Get("role") if !exists { - c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) + utils.Error(c, 401, "未授权,请重新登录") c.Abort() return } @@ -148,7 +148,7 @@ func RoleMiddleware(roles ...string) gin.HandlerFunc { } if !allowed { - c.JSON(http.StatusForbidden, gin.H{"error": "Insufficient permissions"}) + utils.Error(c, 403, "权限不足") c.Abort() return } @@ -166,13 +166,13 @@ func PermissionMiddleware(resource, action string) gin.HandlerFunc { // 尝试从role name获取 roleName, exists := c.Get("role") if !exists { - c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) + utils.Error(c, 401, "未授权,请重新登录") c.Abort() return } role, err := repositories.GetRoleByName(roleName.(string)) if err != nil || role == nil { - c.JSON(http.StatusForbidden, gin.H{"error": "Role not found"}) + utils.Error(c, 403, "角色不存在") c.Abort() return } @@ -184,7 +184,7 @@ func PermissionMiddleware(resource, action string) gin.HandlerFunc { // 获取该角色的所有权限 permissions, err := repositories.GetPermissionsByRoleID(roleID) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to check permissions"}) + utils.Error(c, 500, "权限校验失败") c.Abort() return } @@ -199,7 +199,7 @@ func PermissionMiddleware(resource, action string) gin.HandlerFunc { } if !allowed { - c.JSON(http.StatusForbidden, gin.H{"error": "Insufficient permissions"}) + utils.Error(c, 403, "权限不足") c.Abort() return } diff --git a/server/models/post.go b/server/models/post.go index ac9a2f0..920402f 100644 --- a/server/models/post.go +++ b/server/models/post.go @@ -1,6 +1,7 @@ package models import ( + "encoding/json" "time" "gorm.io/gorm" @@ -76,6 +77,8 @@ type PostHistory struct { Version int `json:"version" gorm:"column:version"` Title string `json:"title" gorm:"column:title"` CategoryID uint `json:"categoryId" gorm:"column:category_id"` + ColumnID *uint `json:"columnId,omitempty" gorm:"column:column_id"` + TagIDs string `json:"tagIds,omitempty" gorm:"column:tag_ids;type:json"` Excerpt string `json:"excerpt" gorm:"column:excerpt"` Content string `json:"content" gorm:"column:content;type:text"` IsPublished int `json:"isPublished" gorm:"column:is_published"` @@ -84,6 +87,18 @@ type PostHistory struct { CreatedAt int64 `json:"createdAt" gorm:"column:created_at"` } +// GetTagIDList parses stored tag_ids JSON into a slice of uint IDs. +func (ph *PostHistory) GetTagIDList() []uint { + if ph.TagIDs == "" { + return nil + } + var ids []uint + if err := json.Unmarshal([]byte(ph.TagIDs), &ids); err != nil { + return nil + } + return ids +} + // TableName 指定表名 func (PostHistory) TableName() string { return "post_history" @@ -108,10 +123,29 @@ type PostHistoryResponse struct { Version int `json:"version"` Title string `json:"title"` CategoryID uint `json:"categoryId"` - CategoryName string `json:"categoryName"` + CategoryName string `json:"categoryName,omitempty"` + ColumnID *uint `json:"columnId,omitempty"` + TagIDs []uint `json:"tagIds,omitempty"` + Excerpt string `json:"excerpt,omitempty"` + Content string `json:"content,omitempty"` Date string `json:"date"` IsPublished int `json:"isPublished"` ModifiedBy uint `json:"modifiedBy"` ModifiedAt string `json:"modifiedAt"` CreatedAt string `json:"createdAt"` } + +// PostHistoryFieldDiff 字段对比结果 +type PostHistoryFieldDiff struct { + From string `json:"from"` + To string `json:"to"` + Changed bool `json:"changed"` + Diff string `json:"diff,omitempty"` +} + +// PostHistoryDiffResponse 版本对比响应 +type PostHistoryDiffResponse struct { + FromVersion int `json:"fromVersion"` + ToVersion int `json:"toVersion"` + Fields map[string]PostHistoryFieldDiff `json:"fields"` +} diff --git a/server/models/user_access_log.go b/server/models/user_access_log.go index 0634849..d3304ce 100644 --- a/server/models/user_access_log.go +++ b/server/models/user_access_log.go @@ -9,11 +9,12 @@ import ( // UserAccessLog 用户访问日志模型 type UserAccessLog struct { ID uint `json:"id" gorm:"primaryKey;column:id"` - UserID uint `json:"user_id" gorm:"column:user_id;index"` // 用户ID(未登录用户为0) - UserIP string `json:"user_ip" gorm:"column:user_ip;index"` // 用户IP地址 - UserLocation string `json:"user_location" gorm:"column:user_location"` // 用户归属地 - ArticleID uint `json:"article_id" gorm:"column:article_id;index"` // 访问的文章ID - AccessTime int64 `json:"access_time" gorm:"column:access_time"` // 访问时间 + UserID uint `json:"user_id" gorm:"column:user_id;index"` // 用户ID(未登录用户为0) + UserIP string `json:"user_ip" gorm:"column:user_ip;index"` // 用户IP地址 + UserLocation string `json:"user_location" gorm:"column:user_location"` // 用户归属地 + ArticleID uint `json:"article_id" gorm:"column:article_id;index"` // 访问的文章ID + VisitorKey string `json:"visitor_key" gorm:"column:visitor_key;size:64"` // 匿名访客标识 + AccessTime int64 `json:"access_time" gorm:"column:access_time"` // 访问时间 DeletedAt int64 `json:"deleted_at" gorm:"column:deleted_at;default:0"` } diff --git a/server/nl_blog.sql b/server/nl_blog.sql index 2c219fd..6437352 100644 --- a/server/nl_blog.sql +++ b/server/nl_blog.sql @@ -1,17 +1,17 @@ /* Navicat Premium Dump SQL - Source Server : 开发环境-本地 + Source Server : 我的mysql8 Source Server Type : MySQL Source Server Version : 80407 (8.4.7) - Source Host : localhost:3306 + Source Host : 101.43.12.11:3306 Source Schema : nl_blog Target Server Type : MySQL Target Server Version : 80407 (8.4.7) File Encoding : 65001 - Date: 20/01/2026 13:33:04 + Date: 24/06/2026 16:07:16 */ SET NAMES utf8mb4; @@ -36,12 +36,7 @@ CREATE TABLE `about_profiles` ( `updated_at` bigint NOT NULL DEFAULT 0, `deleted_at` bigint NOT NULL DEFAULT 0, PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = DYNAMIC; - --- ---------------------------- --- Records of about_profiles --- ---------------------------- -INSERT INTO `about_profiles` VALUES (3, '年糕崽崽', 'https://api.dicebear.com/7.x/avataaars/svg?seed=NianGao&backgroundColor=b6e3f4', '中国 · 浙江杭州', '嗨!我是年糕崽崽,一名热衷于探索数字边界的前端架构师。我的职业生涯始于对像素级完美的执念。从早期的 jQuery 插件开发到如今的 WebGL 3D 场景构建,我始终认为:技术是骨架,艺术是灵魂。

目前,我专注于高性能 B 端应用的体验升级,以及探索生成式艺术(Generative Art)在网页中的应用。闲暇之余,我喜欢在杭州的西湖边散步,或者捣腾我的客制化机械键盘。', 'liqiworker@gmail.com', 'ngzz_0218', '[\\\"Vue 3\\\",\\\"React\\\",\\\"TypeScript\\\",\\\"Three.js\\\",\\\"Golang\\\",\\\"Tailwind CSS\\\",\\\"Rust\\\",\\\"Wails\\\"]', '[{\\\"year\\\":\\\"2024 - 至今\\\",\\\"role\\\":\\\"技术负责人\\\",\\\"company\\\":\\\"某医疗平台公司\\\"},{\\\"year\\\":\\\"2020 - 2024\\\",\\\"role\\\":\\\"PHP开发工程师\\\",\\\"company\\\":\\\"某电商公司\\\"}]', 0, 0, 0, 0); +) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for access_logs @@ -55,16 +50,13 @@ CREATE TABLE `access_logs` ( `method` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'HTTP方法', `status_code` int UNSIGNED NOT NULL COMMENT 'HTTP状态码', `response_time` int UNSIGNED NOT NULL COMMENT '响应时间(毫秒)', + `region` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT 'IP归属地', `deleted_at` bigint NOT NULL DEFAULT 0, `created_at` bigint NOT NULL DEFAULT 0, PRIMARY KEY (`id`) USING BTREE, INDEX `idx_path`(`path` ASC) USING BTREE COMMENT '按访问路径查询索引', INDEX `idx_status_code`(`status_code` ASC) USING BTREE COMMENT '按状态码查询索引' -) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '访问日志表' ROW_FORMAT = DYNAMIC; - --- ---------------------------- --- Records of access_logs --- ---------------------------- +) ENGINE = InnoDB AUTO_INCREMENT = 25100 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '访问日志表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for attachment_categories @@ -80,12 +72,7 @@ CREATE TABLE `attachment_categories` ( `updated_at` bigint NOT NULL DEFAULT 0, PRIMARY KEY (`id`) USING BTREE, INDEX `idx_sort_order`(`sort_order` ASC) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '附件分类表' ROW_FORMAT = DYNAMIC; - --- ---------------------------- --- Records of attachment_categories --- ---------------------------- -INSERT INTO `attachment_categories` VALUES (1, '文章附件', '在博客中上传的附件', 0, 0, 1768812104, 1768812104); +) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '附件分类表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for attachments @@ -111,23 +98,7 @@ CREATE TABLE `attachments` ( INDEX `idx_storage_type`(`storage_type` ASC) USING BTREE, INDEX `idx_file_type`(`file_type` ASC) USING BTREE, INDEX `idx_created_at`(`created_at` ASC) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 13 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '附件表' ROW_FORMAT = DYNAMIC; - --- ---------------------------- --- Records of attachments --- ---------------------------- -INSERT INTO `attachments` VALUES (1, NULL, 'cc_upload_xTUFrRBdm7zWyjQs693a39ea93969.jpg', 'cc_upload_xTUFrRBdm7zWyjQs693a39ea93969.jpg', 'uploads\\2026\\01\\19\\cc_upload_xTUFrRBdm7zWyjQs693a39ea93969_1768807852.jpg', '/uploads/2026/01/19/cc_upload_xTUFrRBdm7zWyjQs693a39ea93969_1768807852.jpg', 40648, 'image', 'image/jpeg', 'local', NULL, 0, 1768807852, 1768807852); -INSERT INTO `attachments` VALUES (2, 1, 'cc_upload_opYFPBlXkfyOSrH269152a21424c4.jpg', 'cc_upload_opYFPBlXkfyOSrH269152a21424c4.jpg', 'uploads\\2026\\01\\19\\cc_upload_opYFPBlXkfyOSrH269152a21424c4_1768811098.jpg', '/uploads/2026/01/19/cc_upload_opYFPBlXkfyOSrH269152a21424c4_1768811098.jpg', 12911, 'image', 'image/jpeg', 'local', NULL, 0, 1768811098, 1768811098); -INSERT INTO `attachments` VALUES (3, 1, 'cc_upload_xTUFrRBdm7zWyjQs693a39ea93969.jpg', 'cc_upload_xTUFrRBdm7zWyjQs693a39ea93969.jpg', 'uploads\\2026\\01\\19\\cc_upload_xTUFrRBdm7zWyjQs693a39ea93969_1768824015.jpg', '/uploads/2026/01/19/cc_upload_xTUFrRBdm7zWyjQs693a39ea93969_1768824015.jpg', 40648, 'image', 'image/jpeg', 'local', NULL, 0, 1768824015, 1768824015); -INSERT INTO `attachments` VALUES (4, 1, 'cc_upload_0NeLXeWx8AmBi7Mx693ce05fea320.jpg', 'cc_upload_0NeLXeWx8AmBi7Mx693ce05fea320.jpg', 'uploads\\2026\\01\\19\\cc_upload_0NeLXeWx8AmBi7Mx693ce05fea320_1768824028.jpg', '/uploads/2026/01/19/cc_upload_0NeLXeWx8AmBi7Mx693ce05fea320_1768824028.jpg', 19875, 'image', 'image/jpeg', 'local', NULL, 0, 1768824029, 1768824029); -INSERT INTO `attachments` VALUES (5, 1, 'cc_upload_62sXZ0HMvINRUYDO693d6c27c7045.jpg', 'cc_upload_62sXZ0HMvINRUYDO693d6c27c7045.jpg', 'uploads\\2026\\01\\19\\cc_upload_62sXZ0HMvINRUYDO693d6c27c7045_1768824028.jpg', '/uploads/2026/01/19/cc_upload_62sXZ0HMvINRUYDO693d6c27c7045_1768824028.jpg', 305101, 'image', 'image/jpeg', 'local', NULL, 0, 1768824029, 1768824029); -INSERT INTO `attachments` VALUES (6, 1, 'cc_upload_9AqkM4FiiRhzl1On693d6c27ce6eb.jpg', 'cc_upload_9AqkM4FiiRhzl1On693d6c27ce6eb.jpg', 'uploads\\2026\\01\\19\\cc_upload_9AqkM4FiiRhzl1On693d6c27ce6eb_1768824028.jpg', '/uploads/2026/01/19/cc_upload_9AqkM4FiiRhzl1On693d6c27ce6eb_1768824028.jpg', 237431, 'image', 'image/jpeg', 'local', NULL, 0, 1768824029, 1768824029); -INSERT INTO `attachments` VALUES (7, 1, 'cc_upload_gXTnJhQ2DSbBr3Ql693d6c27c0b62.jpg', 'cc_upload_gXTnJhQ2DSbBr3Ql693d6c27c0b62.jpg', 'uploads\\2026\\01\\19\\cc_upload_gXTnJhQ2DSbBr3Ql693d6c27c0b62_1768824028.jpg', '/uploads/2026/01/19/cc_upload_gXTnJhQ2DSbBr3Ql693d6c27c0b62_1768824028.jpg', 210572, 'image', 'image/jpeg', 'local', NULL, 0, 1768824029, 1768824029); -INSERT INTO `attachments` VALUES (8, 1, 'cc_upload_etVM7GrPwl33KYzn693cc3016abdf.png', 'cc_upload_etVM7GrPwl33KYzn693cc3016abdf.png', 'uploads\\2026\\01\\19\\cc_upload_etVM7GrPwl33KYzn693cc3016abdf_1768824028.png', '/uploads/2026/01/19/cc_upload_etVM7GrPwl33KYzn693cc3016abdf_1768824028.png', 1161285, 'image', 'image/png', 'local', NULL, 0, 1768824029, 1768824029); -INSERT INTO `attachments` VALUES (9, 1, 'cc_upload_HiPDFg6oL0Q5ijfo693ce1f5eb57c.jpg', 'cc_upload_HiPDFg6oL0Q5ijfo693ce1f5eb57c.jpg', 'uploads\\2026\\01\\19\\cc_upload_HiPDFg6oL0Q5ijfo693ce1f5eb57c_1768825204.jpg', '/uploads/2026/01/19/cc_upload_HiPDFg6oL0Q5ijfo693ce1f5eb57c_1768825204.jpg', 1674408, 'image', 'image/jpeg', 'local', NULL, 0, 1768825204, 1768825204); -INSERT INTO `attachments` VALUES (10, 1, 'cc_upload_7BNbjCmbBatx3S4t6958b017537b6.jpg', 'cc_upload_7BNbjCmbBatx3S4t6958b017537b6.jpg', 'uploads\\2026\\01\\19\\cc_upload_7BNbjCmbBatx3S4t6958b017537b6_1768828219.jpg', '/uploads/2026/01/19/cc_upload_7BNbjCmbBatx3S4t6958b017537b6_1768828219.jpg', 104725, 'image', 'image/jpeg', 'local', NULL, 0, 1768828220, 1768828220); -INSERT INTO `attachments` VALUES (11, 1, 'cc_upload_P17ls1spvgI0uDk56958b02daaf14.jpg', 'cc_upload_P17ls1spvgI0uDk56958b02daaf14.jpg', 'uploads\\2026\\01\\19\\cc_upload_P17ls1spvgI0uDk56958b02daaf14_1768828219.jpg', '/uploads/2026/01/19/cc_upload_P17ls1spvgI0uDk56958b02daaf14_1768828219.jpg', 305101, 'image', 'image/jpeg', 'local', NULL, 0, 1768828220, 1768828220); -INSERT INTO `attachments` VALUES (12, 1, 'cc_upload_7BNbjCmbBatx3S4t6958b017537b6.jpg', 'cc_upload_7BNbjCmbBatx3S4t6958b017537b6.jpg', 'uploads\\2026\\01\\19\\cc_upload_7BNbjCmbBatx3S4t6958b017537b6_1768828707.jpg', '/uploads/2026/01/19/cc_upload_7BNbjCmbBatx3S4t6958b017537b6_1768828707.jpg', 104725, 'image', 'image/jpeg', 'local', NULL, 0, 1768828707, 1768828707); +) ENGINE = InnoDB AUTO_INCREMENT = 74 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '附件表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for categories @@ -145,15 +116,7 @@ CREATE TABLE `categories` ( PRIMARY KEY (`id`) USING BTREE, UNIQUE INDEX `uk_slug`(`slug` ASC) USING BTREE, INDEX `idx_sort_order`(`sort_order` ASC) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '文章分类表' ROW_FORMAT = DYNAMIC; - --- ---------------------------- --- Records of categories --- ---------------------------- -INSERT INTO `categories` VALUES (1, '工程化', '工程化', NULL, 0, 0, 0, 0); -INSERT INTO `categories` VALUES (2, '图形渲染', '图形渲染', NULL, 0, 0, 0, 0); -INSERT INTO `categories` VALUES (3, '设计思维', '设计思维', NULL, 0, 0, 0, 0); -INSERT INTO `categories` VALUES (4, 'Go语言', 'Go语言', NULL, 0, 0, 0, 0); +) ENGINE = InnoDB AUTO_INCREMENT = 7 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '文章分类表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for column_posts @@ -166,16 +129,7 @@ CREATE TABLE `column_posts` ( `created_at` bigint NOT NULL DEFAULT 0, PRIMARY KEY (`column_id`, `post_id`) USING BTREE, INDEX `idx_post_id`(`post_id` ASC) USING BTREE -) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '专栏文章关联表' ROW_FORMAT = DYNAMIC; - --- ---------------------------- --- Records of column_posts --- ---------------------------- -INSERT INTO `column_posts` VALUES (0, 2, 0, 0); -INSERT INTO `column_posts` VALUES (0, 3, 0, 0); -INSERT INTO `column_posts` VALUES (1, 4, 0, 0); -INSERT INTO `column_posts` VALUES (1, 5, 0, 0); -INSERT INTO `column_posts` VALUES (1, 6, 0, 1768809352); +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '专栏文章关联表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for columns @@ -193,12 +147,7 @@ CREATE TABLE `columns` ( `updated_at` bigint NOT NULL DEFAULT 0, PRIMARY KEY (`id`) USING BTREE, INDEX `idx_sort_order`(`sort_order` ASC) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '专栏表' ROW_FORMAT = DYNAMIC; - --- ---------------------------- --- Records of columns --- ---------------------------- -INSERT INTO `columns` VALUES (1, 'Goravel', 'Goravel入门手册', 'https://www.goravel.dev/logo.png', 1, 0, 0, 1768552230, 1768552230); +) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '专栏表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for email_suffixes @@ -214,18 +163,7 @@ CREATE TABLE `email_suffixes` ( `updated_at` bigint NOT NULL DEFAULT 0, PRIMARY KEY (`id`) USING BTREE, UNIQUE INDEX `uk_suffix`(`suffix` ASC) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 15 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '邮箱后缀配置表' ROW_FORMAT = DYNAMIC; - --- ---------------------------- --- Records of email_suffixes --- ---------------------------- -INSERT INTO `email_suffixes` VALUES (1, '@gmail.com', 1, 1, 0, 1768523866, 1768538954); -INSERT INTO `email_suffixes` VALUES (2, '@163.com', 1, 2, 0, 1768523866, 1768538954); -INSERT INTO `email_suffixes` VALUES (3, '@qq.com', 1, 3, 0, 1768523866, 1768538954); -INSERT INTO `email_suffixes` VALUES (4, '@outlook.com', 1, 4, 0, 1768523866, 1768538954); -INSERT INTO `email_suffixes` VALUES (5, '@foxmail.com', 1, 5, 0, 1768523866, 1768538954); -INSERT INTO `email_suffixes` VALUES (6, '@sina.com', 1, 6, 0, 1768523866, 1768538954); -INSERT INTO `email_suffixes` VALUES (7, '@126.com', 1, 7, 0, 1768523866, 1768538954); +) ENGINE = InnoDB AUTO_INCREMENT = 15 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '邮箱后缀配置表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for inquiries @@ -244,11 +182,7 @@ CREATE TABLE `inquiries` ( `created_at` bigint NOT NULL DEFAULT 0, `updated_at` bigint NOT NULL DEFAULT 0, PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '合作咨询表' ROW_FORMAT = DYNAMIC; - --- ---------------------------- --- Records of inquiries --- ---------------------------- +) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '合作咨询表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for operation_logs @@ -269,235 +203,7 @@ CREATE TABLE `operation_logs` ( `created_at` bigint NOT NULL DEFAULT 0, PRIMARY KEY (`id`) USING BTREE, INDEX `idx_user_id`(`user_id` ASC) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 225 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '操作日志表' ROW_FORMAT = DYNAMIC; - --- ---------------------------- --- Records of operation_logs --- ---------------------------- -INSERT INTO `operation_logs` VALUES (1, 1, 'lq', '::1', NULL, '/api/admin/oss-configs', 'GET', '', 200, 97, 0, 1768828795); -INSERT INTO `operation_logs` VALUES (2, 1, 'lq', '::1', NULL, '/api/admin/columns', 'GET', '', 200, 51, 0, 1768828812); -INSERT INTO `operation_logs` VALUES (3, 1, 'lq', '::1', NULL, '/api/admin/columns', 'GET', '', 200, 45, 0, 1768828890); -INSERT INTO `operation_logs` VALUES (4, 1, 'lq', '::1', NULL, '/api/admin/settings', 'GET', '', 200, 44, 0, 1768828891); -INSERT INTO `operation_logs` VALUES (5, 1, 'lq', '::1', NULL, '/api/admin/operation-logs', 'GET', '', 200, 98, 0, 1768828901); -INSERT INTO `operation_logs` VALUES (6, 1, 'lq', '::1', NULL, '/api/admin/attachment-categories', 'GET', '', 200, 48, 0, 1768829023); -INSERT INTO `operation_logs` VALUES (7, 1, 'lq', '::1', NULL, '/api/admin/attachments', 'GET', '', 200, 144, 0, 1768829023); -INSERT INTO `operation_logs` VALUES (8, 1, 'lq', '::1', NULL, '/api/admin/works', 'GET', '', 200, 85, 0, 1768829039); -INSERT INTO `operation_logs` VALUES (9, 1, 'lq', '::1', NULL, '/api/admin/attachment-categories', 'GET', '', 200, 42, 0, 1768829041); -INSERT INTO `operation_logs` VALUES (10, 1, 'lq', '::1', NULL, '/api/admin/attachment-categories', 'GET', '', 200, 46, 0, 1768829041); -INSERT INTO `operation_logs` VALUES (11, 1, 'lq', '::1', NULL, '/api/admin/attachments', 'GET', '', 200, 133, 0, 1768829045); -INSERT INTO `operation_logs` VALUES (12, 1, 'lq', '::1', NULL, '/api/admin/attachment-categories', 'GET', '', 200, 44, 0, 1768829482); -INSERT INTO `operation_logs` VALUES (13, 1, 'lq', '::1', NULL, '/api/admin/attachment-categories', 'GET', '', 200, 44, 0, 1768829482); -INSERT INTO `operation_logs` VALUES (14, 1, 'lq', '::1', NULL, '/api/admin/posts', 'GET', '', 200, 265, 0, 1768829814); -INSERT INTO `operation_logs` VALUES (15, 1, 'lq', '::1', NULL, '/api/admin/columns', 'GET', '', 200, 49, 0, 1768869064); -INSERT INTO `operation_logs` VALUES (16, 1, 'lq', '::1', NULL, '/api/admin/attachment-categories', 'GET', '', 200, 44, 0, 1768869065); -INSERT INTO `operation_logs` VALUES (17, 1, 'lq', '::1', NULL, '/api/admin/attachments', 'GET', '', 200, 140, 0, 1768869068); -INSERT INTO `operation_logs` VALUES (18, 1, 'lq', '::1', NULL, '/api/admin/columns', 'GET', '', 200, 49, 0, 1768869074); -INSERT INTO `operation_logs` VALUES (19, 1, 'lq', '::1', NULL, '/api/admin/about', 'GET', '', 200, 47, 0, 1768869173); -INSERT INTO `operation_logs` VALUES (20, 1, 'lq', '::1', NULL, '/api/admin/attachment-categories', 'GET', '', 200, 51, 0, 1768869180); -INSERT INTO `operation_logs` VALUES (21, 1, 'lq', '::1', NULL, '/api/admin/attachments', 'GET', '', 200, 139, 0, 1768869189); -INSERT INTO `operation_logs` VALUES (22, 1, 'lq', '::1', NULL, '/api/admin/partners/2', 'PUT', '{\"name\":\"Supabase\",\"logo\":\"/uploads/2026/01/19/cc_upload_HiPDFg6oL0Q5ijfo693ce1f5eb57c_1768825204.jpg\",\"description\":\"开源 Firebase 替代方案\"}', 200, 100, 0, 1768869197); -INSERT INTO `operation_logs` VALUES (23, 1, 'lq', '::1', NULL, '/api/admin/snippets', 'GET', '', 200, 95, 0, 1768869231); -INSERT INTO `operation_logs` VALUES (24, 1, 'lq', '::1', NULL, '/api/admin/dashboard/stats', 'GET', '', 200, 379, 0, 1768869446); -INSERT INTO `operation_logs` VALUES (25, 1, 'lq', '::1', NULL, '/api/admin/dashboard/stats', 'GET', '', 200, 376, 0, 1768869486); -INSERT INTO `operation_logs` VALUES (26, 1, 'lq', '::1', NULL, '/api/admin/dashboard/stats', 'GET', '', 200, 327, 0, 1768869688); -INSERT INTO `operation_logs` VALUES (27, 1, 'lq', '::1', NULL, '/api/admin/dashboard/stats', 'GET', '', 200, 372, 0, 1768869729); -INSERT INTO `operation_logs` VALUES (28, 1, 'lq', '::1', NULL, '/api/admin/dashboard/stats', 'GET', '', 200, 325, 0, 1768869747); -INSERT INTO `operation_logs` VALUES (29, 1, 'lq', '::1', NULL, '/api/admin/dashboard/stats', 'GET', '', 200, 375, 0, 1768869756); -INSERT INTO `operation_logs` VALUES (30, 1, 'lq', '::1', NULL, '/api/admin/dashboard/stats', 'GET', '', 200, 375, 0, 1768869758); -INSERT INTO `operation_logs` VALUES (31, 1, 'lq', '::1', NULL, '/api/admin/dashboard/stats', 'GET', '', 200, 375, 0, 1768869760); -INSERT INTO `operation_logs` VALUES (32, 1, 'lq', '::1', NULL, '/api/admin/dashboard/stats', 'GET', '', 200, 358, 0, 1768869781); -INSERT INTO `operation_logs` VALUES (33, 1, 'lq', '::1', NULL, '/api/admin/dashboard/stats', 'GET', '', 200, 376, 0, 1768869786); -INSERT INTO `operation_logs` VALUES (34, 1, 'lq', '::1', NULL, '/api/admin/dashboard/stats', 'GET', '', 200, 386, 0, 1768869793); -INSERT INTO `operation_logs` VALUES (35, 1, 'lq', '::1', NULL, '/api/admin/dashboard/stats', 'GET', '', 200, 385, 0, 1768869795); -INSERT INTO `operation_logs` VALUES (36, 1, 'lq', '::1', NULL, '/api/admin/dashboard/stats', 'GET', '', 200, 388, 0, 1768869800); -INSERT INTO `operation_logs` VALUES (37, 1, 'lq', '::1', NULL, '/api/admin/dashboard/stats', 'GET', '', 200, 341, 0, 1768870363); -INSERT INTO `operation_logs` VALUES (38, 1, 'lq', '::1', NULL, '/api/admin/dashboard/stats', 'GET', '', 200, 340, 0, 1768870495); -INSERT INTO `operation_logs` VALUES (39, 1, 'lq', '::1', NULL, '/api/admin/dashboard/stats', 'GET', '', 200, 354, 0, 1768870522); -INSERT INTO `operation_logs` VALUES (40, 1, 'lq', '::1', NULL, '/api/admin/dashboard/stats', 'GET', '', 200, 335, 0, 1768870535); -INSERT INTO `operation_logs` VALUES (41, 1, 'lq', '::1', NULL, '/api/admin/dashboard/stats', 'GET', '', 200, 334, 0, 1768870539); -INSERT INTO `operation_logs` VALUES (42, 1, 'lq', '::1', NULL, '/api/admin/dashboard/stats', 'GET', '', 200, 338, 0, 1768870541); -INSERT INTO `operation_logs` VALUES (43, 1, 'lq', '::1', NULL, '/api/admin/dashboard/stats', 'GET', '', 200, 325, 0, 1768870546); -INSERT INTO `operation_logs` VALUES (44, 1, 'lq', '::1', NULL, '/api/admin/dashboard/stats', 'GET', '', 200, 322, 0, 1768870579); -INSERT INTO `operation_logs` VALUES (45, 1, 'lq', '::1', NULL, '/api/admin/dashboard/stats', 'GET', '', 200, 321, 0, 1768870581); -INSERT INTO `operation_logs` VALUES (46, 1, 'lq', '::1', NULL, '/api/admin/dashboard/stats', 'GET', '', 200, 321, 0, 1768870587); -INSERT INTO `operation_logs` VALUES (47, 1, 'lq', '::1', NULL, '/api/admin/operation-logs', 'GET', '', 200, 95, 0, 1768870656); -INSERT INTO `operation_logs` VALUES (48, 1, 'lq', '::1', NULL, '/api/admin/dashboard/stats', 'GET', '', 200, 280, 0, 1768870656); -INSERT INTO `operation_logs` VALUES (49, 1, 'lq', '::1', NULL, '/api/admin/dashboard/stats', 'GET', '', 200, 376, 0, 1768870657); -INSERT INTO `operation_logs` VALUES (50, 1, 'lq', '::1', NULL, '/api/admin/posts', 'GET', '', 200, 275, 0, 1768870658); -INSERT INTO `operation_logs` VALUES (51, 1, 'lq', '::1', NULL, '/api/admin/categories', 'GET', '', 200, 47, 0, 1768870659); -INSERT INTO `operation_logs` VALUES (52, 1, 'lq', '::1', NULL, '/api/admin/columns', 'GET', '', 200, 48, 0, 1768870660); -INSERT INTO `operation_logs` VALUES (53, 1, 'lq', '::1', NULL, '/api/admin/works', 'GET', '', 200, 92, 0, 1768870660); -INSERT INTO `operation_logs` VALUES (54, 1, 'lq', '::1', NULL, '/api/admin/snippets', 'GET', '', 200, 92, 0, 1768870661); -INSERT INTO `operation_logs` VALUES (55, 1, 'lq', '::1', NULL, '/api/admin/tags', 'GET', '', 200, 44, 0, 1768870661); -INSERT INTO `operation_logs` VALUES (56, 1, 'lq', '::1', NULL, '/api/admin/attachment-categories', 'GET', '', 200, 47, 0, 1768870663); -INSERT INTO `operation_logs` VALUES (57, 1, 'lq', '::1', NULL, '/api/admin/about', 'GET', '', 200, 47, 0, 1768870663); -INSERT INTO `operation_logs` VALUES (58, 1, 'lq', '::1', NULL, '/api/admin/attachment-categories', 'GET', '', 200, 47, 0, 1768870664); -INSERT INTO `operation_logs` VALUES (59, 1, 'lq', '::1', NULL, '/api/admin/about', 'GET', '', 200, 47, 0, 1768870664); -INSERT INTO `operation_logs` VALUES (60, 1, 'lq', '::1', NULL, '/api/admin/inquiries', 'GET', '', 200, 47, 0, 1768870668); -INSERT INTO `operation_logs` VALUES (61, 1, 'lq', '::1', NULL, '/api/admin/email-suffixes', 'GET', '', 200, 49, 0, 1768870669); -INSERT INTO `operation_logs` VALUES (62, 1, 'lq', '::1', NULL, '/api/admin/users', 'GET', '', 200, 93, 0, 1768870671); -INSERT INTO `operation_logs` VALUES (63, 1, 'lq', '::1', NULL, '/api/admin/roles', 'GET', '', 200, 138, 0, 1768870672); -INSERT INTO `operation_logs` VALUES (64, 1, 'lq', '::1', NULL, '/api/admin/attachment-categories', 'GET', '', 200, 49, 0, 1768870674); -INSERT INTO `operation_logs` VALUES (65, 1, 'lq', '::1', NULL, '/api/admin/attachments', 'GET', '', 200, 104, 0, 1768870674); -INSERT INTO `operation_logs` VALUES (66, 1, 'lq', '::1', NULL, '/api/admin/attachment-categories', 'GET', '', 200, 47, 0, 1768870675); -INSERT INTO `operation_logs` VALUES (67, 1, 'lq', '::1', NULL, '/api/admin/oss-configs', 'GET', '', 200, 97, 0, 1768870676); -INSERT INTO `operation_logs` VALUES (68, 1, 'lq', '::1', NULL, '/api/admin/settings', 'GET', '', 200, 49, 0, 1768870678); -INSERT INTO `operation_logs` VALUES (69, 1, 'lq', '::1', NULL, '/api/admin/operation-logs', 'GET', '', 200, 95, 0, 1768870678); -INSERT INTO `operation_logs` VALUES (70, 1, 'lq', '::1', NULL, '/api/admin/settings', 'GET', '', 200, 46, 0, 1768870679); -INSERT INTO `operation_logs` VALUES (71, 1, 'lq', '::1', NULL, '/api/admin/settings', 'PUT', '{\"id\":4,\"keyName\":\"site_keywords\",\"value\":\"前端, 设计, 技术博客\",\"description\":\"网站关键词\"}', 400, 5, 0, 1768870685); -INSERT INTO `operation_logs` VALUES (72, 1, 'lq', '::1', NULL, '/api/admin/settings', 'PUT', '{\"id\":5,\"keyName\":\"posts_per_page\",\"value\":\"12\",\"description\":\"每页显示的文章数量\"}', 400, 2, 0, 1768870685); -INSERT INTO `operation_logs` VALUES (73, 1, 'lq', '::1', NULL, '/api/admin/settings', 'PUT', '{\"id\":3,\"keyName\":\"site_author\",\"value\":\"年糕崽崽\",\"description\":\"网站作者\"}', 400, 4, 0, 1768870685); -INSERT INTO `operation_logs` VALUES (74, 1, 'lq', '::1', NULL, '/api/admin/settings', 'PUT', '{\"id\":2,\"keyName\":\"site_description\",\"value\":\"分享前端技术、交互设计以及数字艺术的深度思考\",\"description\":\"网站描述\"}', 400, 3, 0, 1768870685); -INSERT INTO `operation_logs` VALUES (75, 1, 'lq', '::1', NULL, '/api/admin/settings', 'PUT', '{\"id\":1,\"keyName\":\"site_title\",\"value\":\"年糕博客\",\"description\":\"网站标题\"}', 400, 6, 0, 1768870685); -INSERT INTO `operation_logs` VALUES (76, 1, 'lq', '::1', NULL, '/api/admin/settings', 'PUT', '{\"id\":7,\"keyName\":\"snippets_per_page\",\"value\":\"8\",\"description\":\"每页显示的代码片段数量\"}', 400, 3, 0, 1768870685); -INSERT INTO `operation_logs` VALUES (77, 1, 'lq', '::1', NULL, '/api/admin/settings', 'PUT', '{\"id\":6,\"keyName\":\"works_per_page\",\"value\":\"6\",\"description\":\"每页显示的作品数量\"}', 400, 4, 0, 1768870685); -INSERT INTO `operation_logs` VALUES (78, 1, 'lq', '::1', NULL, '/api/admin/settings', 'PUT', '{\"id\":5,\"keyName\":\"posts_per_page\",\"value\":\"12\",\"description\":\"每页显示的文章数量\"}', 200, 95, 0, 1768870798); -INSERT INTO `operation_logs` VALUES (79, 1, 'lq', '::1', NULL, '/api/admin/settings', 'PUT', '{\"id\":3,\"keyName\":\"site_author\",\"value\":\"年糕崽崽\",\"description\":\"网站作者\"}', 200, 97, 0, 1768870798); -INSERT INTO `operation_logs` VALUES (80, 1, 'lq', '::1', NULL, '/api/admin/settings', 'PUT', '{\"id\":2,\"keyName\":\"site_description\",\"value\":\"分享前端技术、交互设计以及数字艺术的深度思考\",\"description\":\"网站描述\"}', 200, 96, 0, 1768870799); -INSERT INTO `operation_logs` VALUES (81, 1, 'lq', '::1', NULL, '/api/admin/settings', 'PUT', '{\"id\":4,\"keyName\":\"site_keywords\",\"value\":\"前端, 设计, 技术博客\",\"description\":\"网站关键词\"}', 200, 96, 0, 1768870799); -INSERT INTO `operation_logs` VALUES (82, 1, 'lq', '::1', NULL, '/api/admin/settings', 'PUT', '{\"id\":1,\"keyName\":\"site_title\",\"value\":\"年糕博客\",\"description\":\"网站标题\"}', 200, 94, 0, 1768870799); -INSERT INTO `operation_logs` VALUES (83, 1, 'lq', '::1', NULL, '/api/admin/settings', 'PUT', '{\"id\":7,\"keyName\":\"snippets_per_page\",\"value\":\"8\",\"description\":\"每页显示的代码片段数量\"}', 200, 96, 0, 1768870799); -INSERT INTO `operation_logs` VALUES (84, 1, 'lq', '::1', NULL, '/api/admin/settings', 'PUT', '{\"id\":6,\"keyName\":\"works_per_page\",\"value\":\"6\",\"description\":\"每页显示的作品数量\"}', 200, 96, 0, 1768870799); -INSERT INTO `operation_logs` VALUES (85, 1, 'lq', '::1', NULL, '/api/admin/settings', 'GET', '', 200, 46, 0, 1768870799); -INSERT INTO `operation_logs` VALUES (86, 1, 'lq', '::1', NULL, '/api/admin/settings', 'GET', '', 200, 47, 0, 1768870807); -INSERT INTO `operation_logs` VALUES (87, 1, 'lq', '::1', NULL, '/api/admin/settings', 'GET', '', 200, 49, 0, 1768870895); -INSERT INTO `operation_logs` VALUES (88, 1, 'lq', '::1', NULL, '/api/admin/settings', 'GET', '', 200, 48, 0, 1768870907); -INSERT INTO `operation_logs` VALUES (89, 1, 'lq', '::1', NULL, '/api/admin/settings', 'GET', '', 200, 45, 0, 1768870951); -INSERT INTO `operation_logs` VALUES (90, 1, 'lq', '::1', NULL, '/api/admin/settings', 'GET', '', 200, 53, 0, 1768871123); -INSERT INTO `operation_logs` VALUES (91, 1, 'lq', '::1', NULL, '/api/admin/settings', 'GET', '', 200, 50, 0, 1768871142); -INSERT INTO `operation_logs` VALUES (92, 1, 'lq', '::1', NULL, '/api/admin/settings', 'GET', '', 200, 47, 0, 1768871185); -INSERT INTO `operation_logs` VALUES (93, 1, 'lq', '::1', NULL, '/api/admin/settings', 'GET', '', 200, 50, 0, 1768871213); -INSERT INTO `operation_logs` VALUES (94, 1, 'lq', '::1', NULL, '/api/admin/settings', 'GET', '', 200, 49, 0, 1768871273); -INSERT INTO `operation_logs` VALUES (95, 1, 'lq', '::1', NULL, '/api/admin/settings', 'GET', '', 200, 50, 0, 1768871314); -INSERT INTO `operation_logs` VALUES (96, 1, 'lq', '::1', NULL, '/api/admin/settings', 'GET', '', 200, 49, 0, 1768871359); -INSERT INTO `operation_logs` VALUES (97, 1, 'lq', '::1', NULL, '/api/admin/settings', 'GET', '', 200, 47, 0, 1768871360); -INSERT INTO `operation_logs` VALUES (98, 1, 'lq', '::1', NULL, '/api/admin/settings', 'GET', '', 200, 48, 0, 1768871381); -INSERT INTO `operation_logs` VALUES (99, 1, 'lq', '::1', NULL, '/api/admin/settings', 'GET', '', 200, 57, 0, 1768871394); -INSERT INTO `operation_logs` VALUES (100, 1, 'lq', '::1', NULL, '/api/admin/settings', 'GET', '', 200, 49, 0, 1768871427); -INSERT INTO `operation_logs` VALUES (101, 1, 'lq', '::1', NULL, '/api/admin/settings', 'GET', '', 200, 44, 0, 1768871860); -INSERT INTO `operation_logs` VALUES (102, 1, 'lq', '::1', NULL, '/api/admin/operation-logs', 'GET', '', 200, 97, 0, 1768872916); -INSERT INTO `operation_logs` VALUES (103, 1, 'lq', '::1', NULL, '/api/admin/dashboard/stats', 'GET', '', 200, 334, 0, 1768872916); -INSERT INTO `operation_logs` VALUES (104, 1, 'lq', '::1', NULL, '/api/admin/operation-logs', 'GET', '', 200, 98, 0, 1768872983); -INSERT INTO `operation_logs` VALUES (105, 1, 'lq', '::1', NULL, '/api/admin/dashboard/stats', 'GET', '', 200, 338, 0, 1768872983); -INSERT INTO `operation_logs` VALUES (106, 1, 'lq', '::1', NULL, '/api/admin/posts', 'GET', '', 200, 277, 0, 1768873803); -INSERT INTO `operation_logs` VALUES (107, 1, 'lq', '::1', NULL, '/api/admin/posts', 'GET', '', 200, 290, 0, 1768877345); -INSERT INTO `operation_logs` VALUES (108, 1, 'lq', '::1', NULL, '/api/admin/settings', 'GET', '', 200, 51, 0, 1768877347); -INSERT INTO `operation_logs` VALUES (109, 1, 'lq', '::1', NULL, '/api/admin/settings', 'PUT', '{\"id\":5,\"keyName\":\"posts_per_page\",\"value\":\"12\",\"description\":\"每页显示的文章数量\"}', 200, 98, 0, 1768877365); -INSERT INTO `operation_logs` VALUES (110, 1, 'lq', '::1', NULL, '/api/admin/settings', 'PUT', '{\"id\":3,\"keyName\":\"site_author\",\"value\":\"年糕崽崽1\",\"description\":\"网站作者\"}', 200, 93, 0, 1768877365); -INSERT INTO `operation_logs` VALUES (111, 1, 'lq', '::1', NULL, '/api/admin/settings', 'PUT', '{\"id\":2,\"keyName\":\"site_description\",\"value\":\"分享前端技术、交互设计以及数字艺术的深度思考\",\"description\":\"网站描述\"}', 200, 96, 0, 1768877365); -INSERT INTO `operation_logs` VALUES (112, 1, 'lq', '::1', NULL, '/api/admin/settings', 'PUT', '{\"id\":4,\"keyName\":\"site_keywords\",\"value\":\"前端, 设计, 技术博客\",\"description\":\"网站关键词\"}', 200, 98, 0, 1768877365); -INSERT INTO `operation_logs` VALUES (113, 1, 'lq', '::1', NULL, '/api/admin/settings', 'PUT', '{\"id\":1,\"keyName\":\"site_title\",\"value\":\"年糕博客\",\"description\":\"网站标题\"}', 200, 96, 0, 1768877365); -INSERT INTO `operation_logs` VALUES (114, 1, 'lq', '::1', NULL, '/api/admin/settings', 'PUT', '{\"id\":7,\"keyName\":\"snippets_per_page\",\"value\":\"8\",\"description\":\"每页显示的代码片段数量\"}', 200, 97, 0, 1768877365); -INSERT INTO `operation_logs` VALUES (115, 1, 'lq', '::1', NULL, '/api/admin/settings', 'PUT', '{\"id\":6,\"keyName\":\"works_per_page\",\"value\":\"6\",\"description\":\"每页显示的作品数量\"}', 200, 96, 0, 1768877365); -INSERT INTO `operation_logs` VALUES (116, 1, 'lq', '::1', NULL, '/api/admin/settings', 'GET', '', 200, 47, 0, 1768877365); -INSERT INTO `operation_logs` VALUES (117, 1, 'lq', '::1', NULL, '/api/admin/operation-logs', 'GET', '', 200, 101, 0, 1768877421); -INSERT INTO `operation_logs` VALUES (118, 1, 'lq', '::1', NULL, '/api/admin/dashboard/stats', 'GET', '', 200, 332, 0, 1768877421); -INSERT INTO `operation_logs` VALUES (119, 1, 'lq', '::1', NULL, '/api/admin/dashboard/stats', 'GET', '', 200, 373, 0, 1768877441); -INSERT INTO `operation_logs` VALUES (120, 1, 'lq', '::1', NULL, '/api/admin/dashboard/stats', 'GET', '', 200, 373, 0, 1768877446); -INSERT INTO `operation_logs` VALUES (121, 1, 'lq', '::1', 'Internal', '/api/admin/dashboard/stats', 'GET', '', 200, 332, 0, 1768877578); -INSERT INTO `operation_logs` VALUES (122, 1, 'lq', '::1', 'Internal', '/api/admin/dashboard/stats', 'GET', '', 200, 328, 0, 1768877614); -INSERT INTO `operation_logs` VALUES (123, 1, 'lq', '::1', 'Internal', '/api/admin/dashboard/stats', 'GET', '', 200, 328, 0, 1768877620); -INSERT INTO `operation_logs` VALUES (124, 1, 'lq', '::1', 'Internal', '/api/admin/dashboard/stats', 'GET', '', 200, 329, 0, 1768877627); -INSERT INTO `operation_logs` VALUES (125, 1, 'lq', '::1', 'Internal', '/api/admin/dashboard/stats', 'GET', '', 200, 330, 0, 1768877629); -INSERT INTO `operation_logs` VALUES (126, 1, 'lq', '::1', 'Internal', '/api/admin/access-logs', 'GET', '', 200, 95, 0, 1768877636); -INSERT INTO `operation_logs` VALUES (127, 1, 'lq', '::1', 'Internal', '/api/admin/operation-logs', 'GET', '', 200, 97, 0, 1768877637); -INSERT INTO `operation_logs` VALUES (128, 1, 'lq', '::1', 'Internal', '/api/admin/access-logs', 'GET', '', 200, 94, 0, 1768877641); -INSERT INTO `operation_logs` VALUES (129, 1, 'lq', '::1', 'Internal', '/api/admin/operation-logs', 'GET', '', 200, 94, 0, 1768877642); -INSERT INTO `operation_logs` VALUES (130, 1, 'lq', '::1', 'Internal', '/api/admin/settings', 'GET', '', 200, 48, 0, 1768877642); -INSERT INTO `operation_logs` VALUES (131, 1, 'lq', '::1', 'Internal', '/api/admin/dashboard/stats', 'GET', '', 200, 330, 0, 1768877655); -INSERT INTO `operation_logs` VALUES (132, 1, 'lq', '::1', 'Internal', '/api/admin/dashboard/stats', 'GET', '', 200, 328, 0, 1768877658); -INSERT INTO `operation_logs` VALUES (133, 1, 'lq', '::1', 'Internal', '/api/admin/dashboard/stats', 'GET', '', 200, 329, 0, 1768877716); -INSERT INTO `operation_logs` VALUES (134, 1, 'lq', '::1', 'Internal', '/api/admin/settings', 'GET', '', 200, 48, 0, 1768877809); -INSERT INTO `operation_logs` VALUES (135, 1, 'lq', '::1', 'Internal', '/api/admin/settings', 'GET', '', 200, 52, 0, 1768878153); -INSERT INTO `operation_logs` VALUES (136, 1, 'lq', '::1', 'Internal', '/api/admin/settings', 'GET', '', 200, 49, 0, 1768878166); -INSERT INTO `operation_logs` VALUES (137, 1, 'lq', '::1', 'Internal', '/api/admin/settings', 'GET', '', 200, 55, 0, 1768878174); -INSERT INTO `operation_logs` VALUES (138, 1, 'lq', '::1', 'Internal', '/api/admin/settings', 'GET', '', 200, 57, 0, 1768878177); -INSERT INTO `operation_logs` VALUES (139, 1, 'lq', '::1', 'Internal', '/api/admin/settings', 'GET', '', 200, 52, 0, 1768878188); -INSERT INTO `operation_logs` VALUES (140, 1, 'lq', '::1', 'Internal', '/api/admin/settings', 'GET', '', 200, 51, 0, 1768878472); -INSERT INTO `operation_logs` VALUES (141, 1, 'lq', '::1', 'Internal', '/api/admin/settings', 'GET', '', 200, 47, 0, 1768878512); -INSERT INTO `operation_logs` VALUES (142, 1, 'lq', '::1', 'Internal', '/api/admin/settings', 'GET', '', 200, 52, 0, 1768878616); -INSERT INTO `operation_logs` VALUES (143, 1, 'lq', '::1', 'Internal', '/api/admin/settings', 'PUT', '{\"id\":5,\"keyName\":\"posts_per_page\",\"value\":\"12\",\"description\":\"每页显示的文章数量\"}', 200, 96, 0, 1768878627); -INSERT INTO `operation_logs` VALUES (144, 1, 'lq', '::1', 'Internal', '/api/admin/settings', 'PUT', '{\"id\":3,\"keyName\":\"site_author\",\"value\":\"年糕崽崽1\",\"description\":\"网站作者\"}', 200, 98, 0, 1768878627); -INSERT INTO `operation_logs` VALUES (145, 1, 'lq', '::1', 'Internal', '/api/admin/settings', 'PUT', '{\"id\":2,\"keyName\":\"site_description\",\"value\":\"分享前端技术、交互设计以及数字艺术的深度思考\",\"description\":\"网站描述\"}', 200, 96, 0, 1768878627); -INSERT INTO `operation_logs` VALUES (146, 1, 'lq', '::1', 'Internal', '/api/admin/settings', 'PUT', '{\"id\":4,\"keyName\":\"site_keywords\",\"value\":\"前端, 设计, 技术博客\",\"description\":\"网站关键词\"}', 200, 98, 0, 1768878628); -INSERT INTO `operation_logs` VALUES (147, 1, 'lq', '::1', 'Internal', '/api/admin/settings', 'PUT', '{\"id\":1,\"keyName\":\"site_title\",\"value\":\"年糕崽崽\",\"description\":\"网站标题\"}', 200, 97, 0, 1768878628); -INSERT INTO `operation_logs` VALUES (148, 1, 'lq', '::1', 'Internal', '/api/admin/settings', 'PUT', '{\"id\":7,\"keyName\":\"snippets_per_page\",\"value\":\"8\",\"description\":\"每页显示的代码片段数量\"}', 200, 98, 0, 1768878628); -INSERT INTO `operation_logs` VALUES (149, 1, 'lq', '::1', 'Internal', '/api/admin/settings', 'PUT', '{\"id\":6,\"keyName\":\"works_per_page\",\"value\":\"6\",\"description\":\"每页显示的作品数量\"}', 200, 97, 0, 1768878628); -INSERT INTO `operation_logs` VALUES (150, 1, 'lq', '::1', 'Internal', '/api/admin/settings', 'GET', '', 200, 50, 0, 1768878628); -INSERT INTO `operation_logs` VALUES (151, 1, 'lq', '::1', 'Internal', '/api/admin/settings', 'GET', '', 200, 50, 0, 1768878630); -INSERT INTO `operation_logs` VALUES (152, 1, 'lq', '::1', 'Internal', '/api/admin/settings', 'GET', '', 200, 52, 0, 1768878632); -INSERT INTO `operation_logs` VALUES (153, 1, 'lq', '::1', 'Internal', '/api/admin/settings', 'GET', '', 200, 51, 0, 1768878651); -INSERT INTO `operation_logs` VALUES (154, 1, 'lq', '::1', 'Internal', '/api/admin/settings', 'GET', '', 200, 51, 0, 1768878667); -INSERT INTO `operation_logs` VALUES (155, 1, 'lq', '::1', 'Internal', '/api/admin/settings', 'PUT', '{\"id\":5,\"keyName\":\"posts_per_page\",\"value\":\"12\",\"description\":\"每页显示的文章数量\"}', 200, 98, 0, 1768878783); -INSERT INTO `operation_logs` VALUES (156, 1, 'lq', '::1', 'Internal', '/api/admin/settings', 'PUT', '{\"id\":3,\"keyName\":\"site_author\",\"value\":\"年糕崽崽1\",\"description\":\"网站作者\"}', 200, 96, 0, 1768878783); -INSERT INTO `operation_logs` VALUES (157, 1, 'lq', '::1', 'Internal', '/api/admin/settings', 'PUT', '{\"id\":2,\"keyName\":\"site_description\",\"value\":\"分享前端技术、交互设计以及数字艺术的深度思考\",\"description\":\"网站描述\"}', 200, 97, 0, 1768878783); -INSERT INTO `operation_logs` VALUES (158, 1, 'lq', '::1', 'Internal', '/api/admin/settings', 'PUT', '{\"id\":4,\"keyName\":\"site_keywords\",\"value\":\"前端, 设计, 技术博客\",\"description\":\"网站关键词\"}', 200, 98, 0, 1768878783); -INSERT INTO `operation_logs` VALUES (159, 1, 'lq', '::1', 'Internal', '/api/admin/settings', 'PUT', '{\"id\":1,\"keyName\":\"site_title\",\"value\":\"年糕崽崽\",\"description\":\"网站标题\"}', 200, 96, 0, 1768878783); -INSERT INTO `operation_logs` VALUES (160, 1, 'lq', '::1', 'Internal', '/api/admin/settings', 'PUT', '{\"id\":7,\"keyName\":\"snippets_per_page\",\"value\":\"8\",\"description\":\"每页显示的代码片段数量\"}', 200, 98, 0, 1768878783); -INSERT INTO `operation_logs` VALUES (161, 1, 'lq', '::1', 'Internal', '/api/admin/settings', 'PUT', '{\"id\":6,\"keyName\":\"works_per_page\",\"value\":\"6\",\"description\":\"每页显示的作品数量\"}', 200, 104, 0, 1768878783); -INSERT INTO `operation_logs` VALUES (162, 1, 'lq', '::1', 'Internal', '/api/admin/settings', 'GET', '', 200, 48, 0, 1768878783); -INSERT INTO `operation_logs` VALUES (163, 1, 'lq', '::1', 'Internal', '/api/admin/settings', 'GET', '', 200, 45, 0, 1768878798); -INSERT INTO `operation_logs` VALUES (164, 1, 'lq', '::1', 'Internal', '/api/admin/settings', 'GET', '', 200, 47, 0, 1768878805); -INSERT INTO `operation_logs` VALUES (165, 1, 'lq', '::1', 'Internal', '/api/admin/settings', 'GET', '', 200, 46, 0, 1768878844); -INSERT INTO `operation_logs` VALUES (166, 1, 'lq', '::1', 'Internal', '/api/admin/settings', 'GET', '', 200, 50, 0, 1768878871); -INSERT INTO `operation_logs` VALUES (167, 1, 'lq', '::1', 'Internal', '/api/admin/settings', 'GET', '', 200, 50, 0, 1768878985); -INSERT INTO `operation_logs` VALUES (168, 1, 'lq', '::1', 'Internal', '/api/admin/settings', 'PUT', '{\"id\":15,\"keyName\":\"visible_menus\",\"value\":\"[\\\"home\\\",\\\"blog\\\",\\\"columns\\\",\\\"works\\\",\\\"about\\\",\\\"services\\\"]\",\"description\":\"前台显示的菜单项(JSON数组格式)\"}', 200, 95, 0, 1768878990); -INSERT INTO `operation_logs` VALUES (169, 1, 'lq', '::1', 'Internal', '/api/admin/settings', 'GET', '', 200, 44, 0, 1768878990); -INSERT INTO `operation_logs` VALUES (170, 1, 'lq', '::1', 'Internal', '/api/admin/settings', 'GET', '', 200, 47, 0, 1768879150); -INSERT INTO `operation_logs` VALUES (171, 1, 'lq', '::1', 'Internal', '/api/admin/settings', 'GET', '', 200, 46, 0, 1768879154); -INSERT INTO `operation_logs` VALUES (172, 1, 'lq', '::1', 'Internal', '/api/admin/settings', 'GET', '', 200, 48, 0, 1768879158); -INSERT INTO `operation_logs` VALUES (173, 1, 'lq', '::1', 'Internal', '/api/admin/settings', 'GET', '', 200, 16, 0, 1768880799); -INSERT INTO `operation_logs` VALUES (174, 1, 'lq', '::1', 'Internal', '/api/admin/settings', 'GET', '', 200, 53, 0, 1768880809); -INSERT INTO `operation_logs` VALUES (175, 1, 'lq', '::1', 'Internal', '/api/admin/settings', 'PUT', '{\"id\":15,\"keyName\":\"visible_menus\",\"value\":\"[\\\"home\\\",\\\"blog\\\",\\\"columns\\\",\\\"works\\\",\\\"about\\\"]\",\"description\":\"前台显示的菜单项(JSON数组格式)\"}', 200, 99, 0, 1768881703); -INSERT INTO `operation_logs` VALUES (176, 1, 'lq', '::1', 'Internal', '/api/admin/settings', 'GET', '', 200, 50, 0, 1768881703); -INSERT INTO `operation_logs` VALUES (177, 1, 'lq', '::1', 'Internal', '/api/admin/dashboard/stats', 'GET', '', 200, 312, 0, 1768881885); -INSERT INTO `operation_logs` VALUES (178, 1, 'lq', '::1', 'Internal', '/api/admin/dashboard/stats', 'GET', '', 200, 289, 0, 1768881900); -INSERT INTO `operation_logs` VALUES (179, 1, 'lq', '::1', 'Internal', '/api/admin/dashboard/stats', 'GET', '', 200, 329, 0, 1768882129); -INSERT INTO `operation_logs` VALUES (180, 1, 'lq', '::1', 'Internal', '/api/admin/dashboard/stats', 'GET', '', 200, 342, 0, 1768882288); -INSERT INTO `operation_logs` VALUES (181, 1, 'lq', '::1', 'Internal', '/api/admin/dashboard/stats', 'GET', '', 200, 330, 0, 1768882702); -INSERT INTO `operation_logs` VALUES (182, 1, 'lq', '::1', 'Internal', '/api/admin/dashboard/stats', 'GET', '', 200, 406, 0, 1768882750); -INSERT INTO `operation_logs` VALUES (183, 1, 'lq', '::1', 'Internal', '/api/admin/dashboard/stats', 'GET', '', 200, 387, 0, 1768882851); -INSERT INTO `operation_logs` VALUES (184, 1, 'lq', '::1', 'Internal', '/api/admin/dashboard/stats', 'GET', '', 200, 390, 0, 1768882853); -INSERT INTO `operation_logs` VALUES (185, 1, 'lq', '::1', 'Internal', '/api/admin/dashboard/stats', 'GET', '', 200, 392, 0, 1768882899); -INSERT INTO `operation_logs` VALUES (186, 1, 'lq', '::1', 'Internal', '/api/admin/dashboard/stats', 'GET', '', 200, 390, 0, 1768882943); -INSERT INTO `operation_logs` VALUES (187, 1, 'lq', '::1', 'Internal', '/api/admin/dashboard/stats', 'GET', '', 200, 344, 0, 1768883044); -INSERT INTO `operation_logs` VALUES (188, 1, 'lq', '::1', 'Internal', '/api/admin/dashboard/stats', 'GET', '', 200, 393, 0, 1768883048); -INSERT INTO `operation_logs` VALUES (189, 1, 'lq', '::1', 'Internal', '/api/admin/dashboard/stats', 'GET', '', 200, 337, 0, 1768883061); -INSERT INTO `operation_logs` VALUES (190, 1, 'lq', '::1', 'Internal', '/api/admin/dashboard/stats', 'GET', '', 200, 403, 0, 1768883292); -INSERT INTO `operation_logs` VALUES (191, 1, 'lq', '::1', 'Internal', '/api/admin/dashboard/stats', 'GET', '', 200, 385, 0, 1768883350); -INSERT INTO `operation_logs` VALUES (192, 1, 'lq', '::1', 'Internal', '/api/admin/dashboard/stats', 'GET', '', 200, 390, 0, 1768883362); -INSERT INTO `operation_logs` VALUES (193, 1, 'lq', '::1', 'Internal', '/api/admin/dashboard/stats', 'GET', '', 200, 380, 0, 1768883369); -INSERT INTO `operation_logs` VALUES (194, 1, 'lq', '::1', 'Internal', '/api/admin/dashboard/stats', 'GET', '', 200, 386, 0, 1768883370); -INSERT INTO `operation_logs` VALUES (195, 1, 'lq', '::1', 'Internal', '/api/admin/dashboard/stats', 'GET', '', 200, 382, 0, 1768883398); -INSERT INTO `operation_logs` VALUES (196, 1, 'lq', '::1', 'Internal', '/api/admin/dashboard/stats', 'GET', '', 200, 380, 0, 1768883424); -INSERT INTO `operation_logs` VALUES (197, 1, 'lq', '::1', 'Internal', '/api/admin/dashboard/stats', 'GET', '', 200, 379, 0, 1768883445); -INSERT INTO `operation_logs` VALUES (198, 1, 'lq', '::1', 'Internal', '/api/admin/dashboard/stats', 'GET', '', 200, 383, 0, 1768883471); -INSERT INTO `operation_logs` VALUES (199, 1, 'lq', '::1', 'Internal', '/api/admin/attachment-categories', 'GET', '', 200, 49, 0, 1768885354); -INSERT INTO `operation_logs` VALUES (200, 1, 'lq', '::1', 'Internal', '/api/admin/attachments', 'GET', '', 200, 101, 0, 1768885354); -INSERT INTO `operation_logs` VALUES (201, 1, 'lq', '::1', 'Internal', '/api/admin/attachment-categories', 'GET', '', 200, 47, 0, 1768885377); -INSERT INTO `operation_logs` VALUES (202, 1, 'lq', '::1', 'Internal', '/api/admin/attachments', 'GET', '', 200, 143, 0, 1768885377); -INSERT INTO `operation_logs` VALUES (203, 1, 'lq', '::1', 'Internal', '/api/admin/attachment-categories', 'GET', '', 200, 51, 0, 1768885548); -INSERT INTO `operation_logs` VALUES (204, 1, 'lq', '::1', 'Internal', '/api/admin/attachments', 'GET', '', 200, 101, 0, 1768885548); -INSERT INTO `operation_logs` VALUES (205, 1, 'lq', '::1', 'Internal', '/api/admin/attachment-categories', 'GET', '', 200, 52, 0, 1768886215); -INSERT INTO `operation_logs` VALUES (206, 1, 'lq', '::1', 'Internal', '/api/admin/attachments', 'GET', '', 200, 98, 0, 1768886215); -INSERT INTO `operation_logs` VALUES (207, 1, 'lq', '::1', 'Internal', '/api/admin/attachment-categories', 'GET', '', 200, 55, 0, 1768886263); -INSERT INTO `operation_logs` VALUES (208, 1, 'lq', '::1', 'Internal', '/api/admin/attachments', 'GET', '', 200, 99, 0, 1768886263); -INSERT INTO `operation_logs` VALUES (209, 1, 'lq', '::1', 'Internal', '/api/admin/attachment-categories', 'GET', '', 200, 54, 0, 1768886265); -INSERT INTO `operation_logs` VALUES (210, 1, 'lq', '::1', 'Internal', '/api/admin/attachments', 'GET', '', 200, 102, 0, 1768886265); -INSERT INTO `operation_logs` VALUES (211, 1, 'lq', '::1', 'Internal', '/api/admin/attachment-categories', 'GET', '', 200, 52, 0, 1768886268); -INSERT INTO `operation_logs` VALUES (212, 1, 'lq', '::1', 'Internal', '/api/admin/attachments', 'GET', '', 200, 99, 0, 1768886268); -INSERT INTO `operation_logs` VALUES (213, 1, 'lq', '::1', 'Internal', '/api/admin/attachment-categories', 'GET', '', 200, 48, 0, 1768886279); -INSERT INTO `operation_logs` VALUES (214, 1, 'lq', '::1', 'Internal', '/api/admin/attachments', 'GET', '', 200, 98, 0, 1768886279); -INSERT INTO `operation_logs` VALUES (215, 1, 'lq', '::1', 'Internal', '/api/admin/attachment-categories', 'GET', '', 200, 45, 0, 1768886289); -INSERT INTO `operation_logs` VALUES (216, 1, 'lq', '::1', 'Internal', '/api/admin/attachments', 'GET', '', 200, 133, 0, 1768886289); -INSERT INTO `operation_logs` VALUES (217, 1, 'lq', '::1', 'Internal', '/api/admin/attachment-categories', 'GET', '', 200, 47, 0, 1768886433); -INSERT INTO `operation_logs` VALUES (218, 1, 'lq', '::1', 'Internal', '/api/admin/attachments', 'GET', '', 200, 144, 0, 1768886433); -INSERT INTO `operation_logs` VALUES (219, 1, 'lq', '::1', 'Internal', '/api/admin/attachment-categories', 'GET', '', 200, 55, 0, 1768886433); -INSERT INTO `operation_logs` VALUES (220, 1, 'lq', '::1', 'Internal', '/api/admin/attachments', 'GET', '', 200, 152, 0, 1768886433); -INSERT INTO `operation_logs` VALUES (221, 1, 'lq', '::1', 'Internal', '/api/admin/attachment-categories', 'GET', '', 200, 47, 0, 1768886790); -INSERT INTO `operation_logs` VALUES (222, 1, 'lq', '::1', 'Internal', '/api/admin/attachments', 'GET', '', 200, 147, 0, 1768886790); -INSERT INTO `operation_logs` VALUES (223, 1, 'lq', '::1', 'Internal', '/api/admin/attachment-categories', 'GET', '', 200, 49, 0, 1768887180); -INSERT INTO `operation_logs` VALUES (224, 1, 'lq', '::1', 'Internal', '/api/admin/attachments', 'GET', '', 200, 97, 0, 1768887180); +) ENGINE = InnoDB AUTO_INCREMENT = 487 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '操作日志表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for oss_configs @@ -519,15 +225,7 @@ CREATE TABLE `oss_configs` ( PRIMARY KEY (`id`) USING BTREE, INDEX `idx_storage_type`(`storage_type` ASC) USING BTREE, INDEX `idx_is_active`(`is_active` ASC) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'OSS配置表' ROW_FORMAT = DYNAMIC; - --- ---------------------------- --- Records of oss_configs --- ---------------------------- -INSERT INTO `oss_configs` VALUES (1, '本地存储', 'local', '', '', '', '', '', 1, 0, 1768820300, 1768820300); -INSERT INTO `oss_configs` VALUES (2, '阿里云OSS', 'aliyun', '', '', '', '', '', 0, 0, 1768820300, 1768820300); -INSERT INTO `oss_configs` VALUES (3, '腾讯云COS', 'qcloud', '', '', '', '', '', 0, 0, 1768820300, 1768820300); -INSERT INTO `oss_configs` VALUES (4, '七牛云', 'qiniu', '', '', '', '', '', 0, 0, 1768820300, 1768820300); +) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'OSS配置表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for partners @@ -544,16 +242,7 @@ CREATE TABLE `partners` ( `created_at` bigint NOT NULL DEFAULT 0, `updated_at` bigint NOT NULL DEFAULT 0, PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '合作伙伴表' ROW_FORMAT = DYNAMIC; - --- ---------------------------- --- Records of partners --- ---------------------------- -INSERT INTO `partners` VALUES (1, 'Vercel', 'https://upload.wikimedia.org/wikipedia/commons/5/5e/Vercel_logo_black.svg', '前端部署平台', 'https://vercel.com', 0, 0, 1768482993, 1768538953); -INSERT INTO `partners` VALUES (2, 'Supabase', '/uploads/2026/01/19/cc_upload_HiPDFg6oL0Q5ijfo693ce1f5eb57c_1768825204.jpg', '开源 Firebase 替代方案', '', 0, 0, 1768482993, 1768869197); -INSERT INTO `partners` VALUES (3, 'Stripe', 'https://upload.wikimedia.org/wikipedia/commons/b/ba/Stripe_Logo%2C_revised_2016.svg', '在线支付基础设施', 'https://stripe.com', 0, 0, 1768482993, 1768538953); -INSERT INTO `partners` VALUES (4, 'Algolia', 'https://upload.wikimedia.org/wikipedia/commons/6/69/Algolia-logo.svg', '搜索即服务 API', 'https://algolia.com', 0, 0, 1768482993, 1768538953); -INSERT INTO `partners` VALUES (5, 'Prisma', 'https://seeklogo.com/images/P/prisma-logo-3805665B69-seeklogo.com.png', '下一代 ORM', 'https://prisma.io', 0, 0, 1768482993, 1768538953); +) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '合作伙伴表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for permissions @@ -569,41 +258,7 @@ CREATE TABLE `permissions` ( `updated_at` bigint NOT NULL DEFAULT 0, PRIMARY KEY (`id`) USING BTREE, UNIQUE INDEX `unique_resource_action`(`resource` ASC, `action` ASC) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 31 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '权限表' ROW_FORMAT = DYNAMIC; - --- ---------------------------- --- Records of permissions --- ---------------------------- -INSERT INTO `permissions` VALUES (1, 'Create User', 'users', 'create', 0, 1768452892, 1768538956); -INSERT INTO `permissions` VALUES (2, 'Read User', 'users', 'read', 0, 1768452892, 1768538956); -INSERT INTO `permissions` VALUES (3, 'Update User', 'users', 'update', 0, 1768452892, 1768538956); -INSERT INTO `permissions` VALUES (4, 'Delete User', 'users', 'delete', 0, 1768452892, 1768538956); -INSERT INTO `permissions` VALUES (5, 'Create Role', 'roles', 'create', 0, 1768452892, 1768538956); -INSERT INTO `permissions` VALUES (6, 'Read Role', 'roles', 'read', 0, 1768452892, 1768538956); -INSERT INTO `permissions` VALUES (7, 'Update Role', 'roles', 'update', 0, 1768452892, 1768538956); -INSERT INTO `permissions` VALUES (8, 'Delete Role', 'roles', 'delete', 0, 1768452892, 1768538956); -INSERT INTO `permissions` VALUES (9, 'Create Post', 'posts', 'create', 0, 1768452892, 1768538956); -INSERT INTO `permissions` VALUES (10, 'Read Post', 'posts', 'read', 0, 1768452892, 1768538956); -INSERT INTO `permissions` VALUES (11, 'Update Post', 'posts', 'update', 0, 1768452892, 1768538956); -INSERT INTO `permissions` VALUES (12, 'Delete Post', 'posts', 'delete', 0, 1768452892, 1768538956); -INSERT INTO `permissions` VALUES (13, 'Create Work', 'works', 'create', 0, 1768452892, 1768538956); -INSERT INTO `permissions` VALUES (14, 'Read Work', 'works', 'read', 0, 1768452892, 1768538956); -INSERT INTO `permissions` VALUES (15, 'Update Work', 'works', 'update', 0, 1768452892, 1768538956); -INSERT INTO `permissions` VALUES (16, 'Delete Work', 'works', 'delete', 0, 1768452892, 1768538956); -INSERT INTO `permissions` VALUES (17, 'Create Snippet', 'snippets', 'create', 0, 1768452892, 1768538956); -INSERT INTO `permissions` VALUES (18, 'Read Snippet', 'snippets', 'read', 0, 1768452892, 1768538956); -INSERT INTO `permissions` VALUES (19, 'Update Snippet', 'snippets', 'update', 0, 1768452892, 1768538956); -INSERT INTO `permissions` VALUES (20, 'Delete Snippet', 'snippets', 'delete', 0, 1768452892, 1768538956); -INSERT INTO `permissions` VALUES (21, 'Create Setting', 'settings', 'create', 0, 1768452892, 1768538956); -INSERT INTO `permissions` VALUES (22, 'Read Setting', 'settings', 'read', 0, 1768452892, 1768538956); -INSERT INTO `permissions` VALUES (23, 'Update Setting', 'settings', 'update', 0, 1768452892, 1768538956); -INSERT INTO `permissions` VALUES (24, 'Delete Setting', 'settings', 'delete', 0, 1768452892, 1768538956); -INSERT INTO `permissions` VALUES (25, 'Create Tag', 'tags', 'create', 0, 1768452892, 1768538956); -INSERT INTO `permissions` VALUES (26, 'Read Tag', 'tags', 'read', 0, 1768452892, 1768538956); -INSERT INTO `permissions` VALUES (27, 'Update Tag', 'tags', 'update', 0, 1768452892, 1768538956); -INSERT INTO `permissions` VALUES (28, 'Delete Tag', 'tags', 'delete', 0, 1768452892, 1768538956); -INSERT INTO `permissions` VALUES (29, 'Read Operation Log', 'operation_logs', 'read', 0, 1768452892, 1768538956); -INSERT INTO `permissions` VALUES (30, 'Read Dashboard', 'dashboard', 'read', 0, 1768452892, 1768538956); +) ENGINE = InnoDB AUTO_INCREMENT = 31 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '权限表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for post_history @@ -615,6 +270,8 @@ CREATE TABLE `post_history` ( `version` int UNSIGNED NOT NULL DEFAULT 1 COMMENT '版本号', `title` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '文章标题', `category_id` int UNSIGNED NOT NULL DEFAULT 0 COMMENT '文章分类ID', + `column_id` int UNSIGNED NULL DEFAULT NULL COMMENT '专栏ID', + `tag_ids` json NULL COMMENT '标签ID快照', `excerpt` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '文章摘要', `content` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '文章内容', `is_published` tinyint(1) NULL DEFAULT 1 COMMENT '是否已发布', @@ -625,14 +282,7 @@ CREATE TABLE `post_history` ( PRIMARY KEY (`id`) USING BTREE, INDEX `idx_post_id`(`post_id` ASC) USING BTREE, INDEX `idx_version`(`version` ASC) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '文章历史记录表' ROW_FORMAT = DYNAMIC; - --- ---------------------------- --- Records of post_history --- ---------------------------- -INSERT INTO `post_history` VALUES (1, 6, 1, 'Goravel 入门指南 (三):ORM 数据库操作', 4, '掌握 Goravel 强大的 ORM 功能,从数据库配置、模型定义到执行增删改查(CRUD)操作。', '', 1, 1, 1768809133, 1768809133, 0); -INSERT INTO `post_history` VALUES (2, 6, 2, 'Goravel 入门指南 (三):ORM 数据库操作', 4, '掌握 Goravel 强大的 ORM 功能,从数据库配置、模型定义到执行增删改查(CRUD)操作。', '', 1, 1, 1768809352, 1768809352, 0); -INSERT INTO `post_history` VALUES (3, 1, 1, '重构的艺术:如何优雅地处理遗留代码', 4, '在本文中,我们将探讨重构的核心原则和实用技巧,帮助你优雅地处理遗留代码,提高代码质量和可维护性。', '

什么是代码重构?

代码重构是在不改变代码外部行为的前提下,优化代码内部结构的过程...

\n\n\n![cc_upload_opYFPBlXkfyOSrH269152a21424c4.jpg](/uploads/2026/01/19/cc_upload_opYFPBlXkfyOSrH269152a21424c4_1768811098.jpg)\n', 1, 1, 1768811113, 1768811113, 0); +) ENGINE = InnoDB AUTO_INCREMENT = 60 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '文章历史记录表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for post_tags @@ -644,15 +294,7 @@ CREATE TABLE `post_tags` ( `created_at` bigint NOT NULL DEFAULT 0, PRIMARY KEY (`post_id`, `tag_id`) USING BTREE, INDEX `idx_tag_id`(`tag_id` ASC) USING BTREE -) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '文章标签关联表' ROW_FORMAT = DYNAMIC; - --- ---------------------------- --- Records of post_tags --- ---------------------------- -INSERT INTO `post_tags` VALUES (1, 6, 0); -INSERT INTO `post_tags` VALUES (2, 6, 0); -INSERT INTO `post_tags` VALUES (3, 6, 0); -INSERT INTO `post_tags` VALUES (4, 6, 0); +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '文章标签关联表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for posts @@ -677,17 +319,7 @@ CREATE TABLE `posts` ( INDEX `idx_is_published`(`is_published` ASC) USING BTREE COMMENT '按发布状态查询索引', INDEX `idx_original_id`(`original_id` ASC) USING BTREE, FULLTEXT INDEX `idx_title_content`(`title`, `content`) COMMENT '标题和内容全文索引,用于搜索' -) ENGINE = InnoDB AUTO_INCREMENT = 8 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '博客文章表(新结构)' ROW_FORMAT = DYNAMIC; - --- ---------------------------- --- Records of posts --- ---------------------------- -INSERT INTO `posts` VALUES (1, 'refactor', '重构的艺术:如何优雅地处理遗留代码', 4, NULL, '在本文中,我们将探讨重构的核心原则和实用技巧,帮助你优雅地处理遗留代码,提高代码质量和可维护性。', '

什么是代码重构?

代码重构是在不改变代码外部行为的前提下,优化代码内部结构的过程...

\n\n\n![cc_upload_opYFPBlXkfyOSrH269152a21424c4.jpg](/uploads/2026/01/19/cc_upload_opYFPBlXkfyOSrH269152a21424c4_1768811098.jpg)\n', 10, 1, 0, 1768291814, 1768811113); -INSERT INTO `posts` VALUES (2, 'shader', '着色器魔法:从零开始写一个噪声生成器', 2, NULL, '深入了解WebGL着色器,学习如何从零开始实现一个高性能的噪声生成器,为你的3D作品增添独特的视觉效果。', '

GLSL (OpenGL Shading Language) 是一门让人生畏但也充满魅力的语言。它运行在 GPU 上,能够并行处理数百万个像素,创造出惊人的视觉效果。

\r\n

什么是柏林噪声?

\r\n

柏林噪声(Perlin Noise)是一种梯度噪声,它比普通的随机数生成的噪声看起来更自然、更平滑。它常被用来模拟云彩、地形、火焰等自然现象。

\r\n

Three.js 中的实现

\r\n

在 Three.js 中,我们可以通过 ShaderMaterial 直接编写 GLSL 代码。

\r\n
// 简单的顶点着色器\r\nvarying vec2 vUv;\r\nvoid main() {\r\n    vUv = uv;\r\n    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\r\n}
\r\n

通过调整噪声的频率和振幅,我们可以得到各种不同的纹理效果。在我的个人网站背景中,就使用了这种技术来生成流动的极光效果。

\r\n ', 1, 1, 0, 1768291815, 1768538949); -INSERT INTO `posts` VALUES (3, 'ux', '用户体验设计:从认知心理学到交互实践', 3, NULL, '探索用户体验设计的核心原理,结合认知心理学知识,学习如何设计出真正符合用户需求的交互界面。', '

认知心理学在UX设计中的应用

了解用户的认知过程是设计良好用户体验的基础...

', 0, 1, 0, 1768291816, 1768538949); -INSERT INTO `posts` VALUES (4, 'goravel-guide-1', 'Goravel 入门指南 (一):环境搭建与 Hello World', 4, 1, '本文将带你了解 Go 语言领域的 Laravel —— Goravel 框架,并演示如何快速搭建环境及运行第一个 Web 服务。', '## 什么是 Goravel?\n\nGoravel 是一个基于 Go 语言的 Web 开发框架,它的设计理念深受 PHP Laravel 框架的启发。如果你是一名从 PHP 转 Go 的开发者,或者你喜欢 Laravel 那种“开箱即用、优雅简洁”的开发体验,那么 Goravel 绝对是你的不二之选。\n\n它集成了丰富的功能模块,包括但不限于:\n- 强大的路由系统\n- ORM(基于 GORM 封装)\n- 依赖注入容器\n- 队列与任务调度\n- 缓存与文件存储\n\n## 环境搭建\n\nGoravel 提供了一个名为 `knit` 的命令行工具(类似 Laravel 的 artisan),可以帮助我们快速初始化项目。\n\n### 1. 安装 Knit CLI\n\n确保你已经安装了 Go (1.20+),然后运行以下命令:\n\n```bash\ngo install github.com/goravel/knit/cmd/knit@latest\n```\n\n### 2. 创建新项目\n\n使用 `knit new` 命令创建项目:\n\n```bash\nknit new my-goravel-app\ncd my-goravel-app\n```\n\n### 3. 安装依赖\n\n```bash\ngo mod tidy\n```\n\n## 目录结构\n\n打开项目,你会发现它的目录结构非常清晰,带有浓厚的 Laravel 风格:\n\n- **app/**: 核心业务代码(Http 控制器、模型、服务提供者等)\n- **config/**: 配置文件(应用配置、数据库配置等)\n- **routes/**: 路由定义文件\n- **database/**: 数据库迁移与填充\n- **public/**: 静态资源文件\n\n## 运行 Hello World\n\nGoravel 的入口文件是根目录下的 `main.go`。在运行之前,我们先看一眼路由定义。打开 `routes/web.go`:\n\n```go\npackage routes\n\nimport (\n \"github.com/goravel/framework/contracts/http\"\n \"github.com/goravel/framework/facades\"\n)\n\nfunc Web() {\n facades.Route().Get(\"/\", func(ctx http.Context) http.Response {\n return ctx.Response().Json(200, http.Json{\n \"Hello\": \"Goravel\",\n })\n })\n}\n```\n\n非常直观!现在让我们启动服务:\n\n```bash\ngo run .\n```\n\n默认情况下,服务会运行在 `http://localhost:3000`。打开浏览器访问,你应该能看到 JSON 响应:\n\n```json\n{\n \"Hello\": \"Goravel\"\n}\n```\n\n至此,你已经成功运行了你的第一个 Goravel 应用!在下一篇文章中,我们将深入探讨路由与控制器的使用。', 1223, 1, 0, 1768465932, 1768538949); -INSERT INTO `posts` VALUES (5, 'goravel-guide-2', 'Goravel 入门指南 (二):路由与控制器', 4, 1, '深入理解 Goravel 的 HTTP 层,学习如何定义 RESTful 路由、创建控制器以及处理 HTTP 请求与响应。', '## 路由系统\n\n在 Goravel 中,路由定义通常位于 `routes/` 目录下。`api.go` 用于定义 API 路由,`web.go` 用于定义网页路由。Goravel 使用 `facades.Route()` 来定义路由,这得益于其强大的依赖注入系统。\n\n### 基础路由\n\n```go\n// GET 请求\nfacades.Route().Get(\"/users\", func(ctx http.Context) http.Response {\n return ctx.Response().String(200, \"User List\")\n})\n\n// POST 请求\nfacades.Route().Post(\"/users\", func(ctx http.Context) http.Response {\n return ctx.Response().Success().Json(http.Json{\"id\": 1})\n})\n```\n\n### 路由参数\n\n获取 URL 中的动态参数非常简单:\n\n```go\nfacades.Route().Get(\"/users/{id}\", func(ctx http.Context) http.Response {\n id := ctx.Request().Input(\"id\")\n return ctx.Response().Success().Json(http.Json{\"user_id\": id})\n})\n```\n\n## 控制器 (Controllers)\n\n随着应用变大,我们不可能把所有逻辑都写在路由闭包里。这时候就需要控制器了。\n\n### 创建控制器\n\n使用 `knit` 工具可以快速生成控制器:\n\n```bash\nknit make:controller UserController\n```\n\n这会在 `app/http/controllers` 目录下生成 `user_controller.go`。让我们修改它来添加一个 `Show` 方法:\n\n```go\npackage controllers\n\nimport (\n \"github.com/goravel/framework/contracts/http\"\n)\n\ntype UserController struct {\n // 可以在这里注入服务\n}\n\nfunc NewUserController() *UserController {\n return &UserController{}\n}\n\nfunc (r *UserController) Show(ctx http.Context) http.Response {\n id := ctx.Request().Input(\"id\")\n // 模拟数据库查询\n return ctx.Response().Success().Json(http.Json{\n \"id\": id,\n \"name\": \"Goravel User\",\n })\n}\n```\n\n### 注册控制器路由\n\n回到 `routes/api.go`,我们需要先实例化控制器,然后绑定路由:\n\n```go\nimport \"my-goravel-app/app/http/controllers\"\n\nfunc Api() {\n userController := controllers.NewUserController()\n \n // 绑定到控制器方法\n facades.Route().Get(\"/users/{id}\", userController.Show)\n}\n```\n\n## 请求与响应\n\n在控制器方法中,`ctx` (http.Context) 是核心:\n\n- **获取输入**: `ctx.Request().Input(\"key\")`\n- **获取 JSON**: `ctx.Request().Bind(&user)`\n- **返回 JSON**: `ctx.Response().Json(200, data)`\n- **设置状态码**: `ctx.Response().Status(404)`\n\n通过这种方式,Goravel 让 HTTP 层的处理变得异常清晰和标准化。下一章,我们将学习如何通过 ORM 操作数据库。', 925, 1, 0, 1768465933, 1768538949); -INSERT INTO `posts` VALUES (6, 'goravel-guide-3', 'Goravel 入门指南 (三):ORM 数据库操作', 4, 1, '掌握 Goravel 强大的 ORM 功能,从数据库配置、模型定义到执行增删改查(CRUD)操作。', '## Goravel ORM 简介\r\nGoravel 的 ORM 基于著名的 GORM 库构建,但它进行了一层优雅的封装,使其调用方式更接近 Laravel 的 Eloquent ORM。这意味着你可以使用 Facade 模式轻松调用数据库,且支持自动迁移。\r\n## 配置数据库\r\n打开 `config/database.go` 或 `.env` 文件,配置你的 MySQL 连接信息:\r\n```env\r\nDB_CONNECTION=mysql\r\nDB_HOST=127.0.0.1\r\nDB_PORT=3306\r\nDB_DATABASE=goravel\r\nDB_USERNAME=root\r\nDB_PASSWORD=password\r\n```\r\n## 定义模型\r\n使用 `knit` 生成模型:\r\n```bash\r\nknit make:model Post\r\n```\r\n生成的模型文件位于 `app/models/post.go`。我们需要定义结构体字段:\r\n```go\r\npackage models\r\nimport (\r\n \\\"github.com/goravel/framework/database/orm\\\"\r\n )\r\ntype Post struct {\r\n orm.Model\r\n Title string `gorm:\\\"size:255;not null\\\"`\r\n Content string `gorm:\\\"type:text\\\"`\r\n UserID uint\r\n }\r\n ```\r\n## 数据库迁移\r\n虽然 GORM 支持 AutoMigrate,但 Goravel 推荐使用迁移文件来管理数据库变更。\r\n```bash\r\nknit make:migration create_posts_table\r\n```\r\n编辑生成的迁移文件 (`database/migrations/xxxx_create_posts_table.go`),定义表结构,然后运行:\r\n```bash\r\nknit migrate\r\n```\r\n## CRUD 操作\r\n有了模型,我们就可以愉快地操作数据库了。所有操作都通过 `facades.Orm()` 入口。\r\n### 创建 (Create)\r\n```go\r\npost := models.Post{\r\n Title: \\\"My First Post\\\",\r\n Content: \\\"Content goes here...\\\",\r\n }\r\n err := facades.Orm().Query().Create(&post)\r\n ```\r\n### 查询 (Read)\r\n```go\r\nvar post models.Post\r\n// 根据主键查询\r\nfacades.Orm().Query().Find(&post, 1)\r\n// 条件查询\r\nvar posts []models.Post\r\nfacades.Orm().Query().Where(\\\"title\\\", \\\"My First Post\\\").Get(&posts)\r\n```\r\n### 更新 (Update)\r\n```go\r\nvar post models.Post\r\nfacades.Orm().Query().Find(&post, 1)\r\npost.Title = \\\"Updated Title\\\"\r\nfacades.Orm().Query().Save(&post)\r\n```\r\n### 删除 (Delete)\r\n```go\r\nvar post models.Post\r\nfacades.Orm().Query().Delete(&post, 1)\r\n```\r\nGoravel 的 ORM 让 Go 语言的数据库操作不再繁琐,极大地提高了开发效率。配合之前的路由和控制器知识,你现在已经具备了开发完整 RESTful API 的能力!', 1602, 1, 0, 1768465934, 1768809351); +) ENGINE = InnoDB AUTO_INCREMENT = 36 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '博客文章表(新结构)' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for role_permissions @@ -698,54 +330,7 @@ CREATE TABLE `role_permissions` ( `permission_id` bigint UNSIGNED NOT NULL COMMENT '权限ID', PRIMARY KEY (`role_id`, `permission_id`) USING BTREE, INDEX `role_permissions_ibfk_2`(`permission_id` ASC) USING BTREE -) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '角色权限关联表' ROW_FORMAT = DYNAMIC; - --- ---------------------------- --- Records of role_permissions --- ---------------------------- -INSERT INTO `role_permissions` VALUES (1, 1); -INSERT INTO `role_permissions` VALUES (1, 2); -INSERT INTO `role_permissions` VALUES (3, 2); -INSERT INTO `role_permissions` VALUES (1, 3); -INSERT INTO `role_permissions` VALUES (1, 4); -INSERT INTO `role_permissions` VALUES (1, 5); -INSERT INTO `role_permissions` VALUES (1, 6); -INSERT INTO `role_permissions` VALUES (3, 6); -INSERT INTO `role_permissions` VALUES (1, 7); -INSERT INTO `role_permissions` VALUES (1, 8); -INSERT INTO `role_permissions` VALUES (1, 9); -INSERT INTO `role_permissions` VALUES (2, 9); -INSERT INTO `role_permissions` VALUES (1, 10); -INSERT INTO `role_permissions` VALUES (2, 10); -INSERT INTO `role_permissions` VALUES (3, 10); -INSERT INTO `role_permissions` VALUES (1, 11); -INSERT INTO `role_permissions` VALUES (2, 11); -INSERT INTO `role_permissions` VALUES (1, 12); -INSERT INTO `role_permissions` VALUES (2, 12); -INSERT INTO `role_permissions` VALUES (1, 13); -INSERT INTO `role_permissions` VALUES (1, 14); -INSERT INTO `role_permissions` VALUES (3, 14); -INSERT INTO `role_permissions` VALUES (1, 15); -INSERT INTO `role_permissions` VALUES (1, 16); -INSERT INTO `role_permissions` VALUES (1, 17); -INSERT INTO `role_permissions` VALUES (1, 18); -INSERT INTO `role_permissions` VALUES (3, 18); -INSERT INTO `role_permissions` VALUES (1, 19); -INSERT INTO `role_permissions` VALUES (1, 20); -INSERT INTO `role_permissions` VALUES (1, 21); -INSERT INTO `role_permissions` VALUES (1, 22); -INSERT INTO `role_permissions` VALUES (3, 22); -INSERT INTO `role_permissions` VALUES (1, 23); -INSERT INTO `role_permissions` VALUES (1, 24); -INSERT INTO `role_permissions` VALUES (1, 25); -INSERT INTO `role_permissions` VALUES (1, 26); -INSERT INTO `role_permissions` VALUES (3, 26); -INSERT INTO `role_permissions` VALUES (1, 27); -INSERT INTO `role_permissions` VALUES (1, 28); -INSERT INTO `role_permissions` VALUES (1, 29); -INSERT INTO `role_permissions` VALUES (3, 29); -INSERT INTO `role_permissions` VALUES (1, 30); -INSERT INTO `role_permissions` VALUES (3, 30); +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '角色权限关联表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for roles @@ -760,14 +345,7 @@ CREATE TABLE `roles` ( `updated_at` bigint NOT NULL DEFAULT 0, PRIMARY KEY (`id`) USING BTREE, UNIQUE INDEX `name`(`name` ASC) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '角色表' ROW_FORMAT = DYNAMIC; - --- ---------------------------- --- Records of roles --- ---------------------------- -INSERT INTO `roles` VALUES (1, 'admin', '系统管理员', 0, 1768452892, 1768538956); -INSERT INTO `roles` VALUES (2, 'editor', '内容编辑', 0, 1768452892, 1768538956); -INSERT INTO `roles` VALUES (3, 'viewer', '普通访客', 0, 1768452892, 1768538956); +) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '角色表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for search_logs @@ -784,11 +362,7 @@ CREATE TABLE `search_logs` ( PRIMARY KEY (`id`) USING BTREE, INDEX `idx_search_type`(`search_type` ASC) USING BTREE, INDEX `idx_created_at`(`created_at` ASC) USING BTREE -) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '搜索记录表' ROW_FORMAT = DYNAMIC; - --- ---------------------------- --- Records of search_logs --- ---------------------------- +) ENGINE = InnoDB AUTO_INCREMENT = 22 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '搜索记录表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for settings @@ -805,19 +379,7 @@ CREATE TABLE `settings` ( PRIMARY KEY (`id`) USING BTREE, UNIQUE INDEX `key_name`(`key_name` ASC) USING BTREE, INDEX `idx_key_name`(`key_name` ASC) USING BTREE COMMENT '按键名查询索引' -) ENGINE = InnoDB AUTO_INCREMENT = 16 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '网站配置表' ROW_FORMAT = DYNAMIC; - --- ---------------------------- --- Records of settings --- ---------------------------- -INSERT INTO `settings` VALUES (1, 'site_title', '年糕崽崽', '网站标题', 0, 1768291814, 1768878783); -INSERT INTO `settings` VALUES (2, 'site_description', '分享前端技术、交互设计以及数字艺术的深度思考', '网站描述', 0, 1768291814, 1768878783); -INSERT INTO `settings` VALUES (3, 'site_author', '年糕崽崽1', '网站作者', 0, 1768291814, 1768878783); -INSERT INTO `settings` VALUES (4, 'site_keywords', '前端, 设计, 技术博客', '网站关键词', 0, 1768291814, 1768878783); -INSERT INTO `settings` VALUES (5, 'posts_per_page', '12', '每页显示的文章数量', 0, 1768291814, 1768878783); -INSERT INTO `settings` VALUES (6, 'works_per_page', '6', '每页显示的作品数量', 0, 1768291814, 1768878783); -INSERT INTO `settings` VALUES (7, 'snippets_per_page', '8', '每页显示的代码片段数量', 0, 1768291814, 1768878783); -INSERT INTO `settings` VALUES (15, 'visible_menus', '[\"home\",\"blog\",\"columns\",\"works\",\"about\"]', '前台显示的菜单项(JSON数组格式)', 0, 1768878978, 1768881703); +) ENGINE = InnoDB AUTO_INCREMENT = 16 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '网站配置表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for snippets @@ -836,12 +398,7 @@ CREATE TABLE `snippets` ( PRIMARY KEY (`id`) USING BTREE, INDEX `idx_type`(`type` ASC) USING BTREE COMMENT '按代码类型查询索引', INDEX `idx_view_count`(`view_count` ASC) USING BTREE COMMENT '按查看次数查询索引' -) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '代码片段表' ROW_FORMAT = DYNAMIC; - --- ---------------------------- --- Records of snippets --- ---------------------------- -INSERT INTO `snippets` VALUES ('1', 'React 鼠标追踪 Hook', 'import { useState, useEffect } from \'react\';\r\n\r\nexport const useMousePosition = () => {\r\n const [pos, setPos] = useState({ x: 0, y: 0 });\r\n useEffect(() => {\r\n const update = (e) => setPos({ x: e.clientX, y: e.clientY });\r\n window.addEventListener(\'mousemove\', update);\r\n return () => window.removeEventListener(\'mousemove\', update);\r\n }, []);\r\n return pos;\r\n};', 'mouse', '这是一个鼠标追踪', 3, 0, 1768350739, 1768538952); +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '代码片段表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for tags @@ -858,15 +415,7 @@ CREATE TABLE `tags` ( UNIQUE INDEX `name`(`name` ASC) USING BTREE, UNIQUE INDEX `slug`(`slug` ASC) USING BTREE, INDEX `idx_slug`(`slug` ASC) USING BTREE COMMENT '按别名查询索引' -) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '标签表' ROW_FORMAT = DYNAMIC; - --- ---------------------------- --- Records of tags --- ---------------------------- -INSERT INTO `tags` VALUES (1, 'Goravel', '', 0, 1768550858, 1768550858); -INSERT INTO `tags` VALUES (2, '入门教程', '入门教程', 0, 1768809322, 1768809322); -INSERT INTO `tags` VALUES (3, 'Golang框架', 'Golang框架', 0, 1768809341, 1768809341); -INSERT INTO `tags` VALUES (4, '前后端分离', '前后端分离', 0, 1768809349, 1768809349); +) ENGINE = InnoDB AUTO_INCREMENT = 15 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '标签表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for testimonials @@ -884,17 +433,7 @@ CREATE TABLE `testimonials` ( `created_at` bigint NOT NULL DEFAULT 0, `updated_at` bigint NOT NULL DEFAULT 0, PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 7 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '客户评价表' ROW_FORMAT = DYNAMIC; - --- ---------------------------- --- Records of testimonials --- ---------------------------- -INSERT INTO `testimonials` VALUES (1, 'Alex Chen', 'Product Owner @ TechFlow', '年糕不仅技术过硬,对设计细节的把控更是令人惊叹。', 'https://api.dicebear.com/7.x/avataaars/svg?seed=Alex', 5, 0, 0, 1768482993, 1768538954); -INSERT INTO `testimonials` VALUES (2, 'Sarah Wu', 'Design Director @ ArtSpace', '很少见到能把代码写得像诗一样的工程师,合作非常愉快!', 'https://api.dicebear.com/7.x/avataaars/svg?seed=Sarah', 5, 0, 0, 1768482993, 1768538954); -INSERT INTO `testimonials` VALUES (3, 'Mike Zhang', 'CTO @ FutureWave', '交付质量远超预期,特别是在性能优化方面做得非常出色。', 'https://api.dicebear.com/7.x/avataaars/svg?seed=Mike', 5, 0, 0, 1768482993, 1768538954); -INSERT INTO `testimonials` VALUES (4, 'Jessica Li', 'Founder @ ZenMode', '从交互动效到整体架构,都体现了极高的专业水准。', 'https://api.dicebear.com/7.x/avataaars/svg?seed=Jessica', 5, 0, 0, 1768482993, 1768538954); -INSERT INTO `testimonials` VALUES (5, 'David Wang', 'Tech Lead @ Innovate', '代码结构清晰,注释完善,后续维护非常轻松。', 'https://api.dicebear.com/7.x/avataaars/svg?seed=David', 5, 0, 0, 1768482993, 1768538954); -INSERT INTO `testimonials` VALUES (6, '', 'CTO', '服务很贴心,技术够硬', 'https://api.dicebear.com/7.x/avataaars/svg?seed=David', 5, 0, 0, 20260116130552, 20260116130552); +) ENGINE = InnoDB AUTO_INCREMENT = 7 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '客户评价表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for user_access_logs @@ -906,61 +445,15 @@ CREATE TABLE `user_access_logs` ( `user_ip` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '用户IP地址', `user_location` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '用户归属地', `article_id` int NOT NULL COMMENT '访问的文章ID', + `visitor_key` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '匿名访客标识', `deleted_at` bigint NOT NULL DEFAULT 0, `access_time` bigint NOT NULL DEFAULT 0, PRIMARY KEY (`id`) USING BTREE, INDEX `idx_user_id`(`user_id` ASC) USING BTREE, - INDEX `idx_article_id`(`article_id` ASC) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 46 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '用户访问记录表' ROW_FORMAT = DYNAMIC; - --- ---------------------------- --- Records of user_access_logs --- ---------------------------- -INSERT INTO `user_access_logs` VALUES (1, 0, '::1', 'Internal', 1, 0, 1768829817); -INSERT INTO `user_access_logs` VALUES (2, 0, '::1', 'Internal', 4, 0, 1768829860); -INSERT INTO `user_access_logs` VALUES (3, 0, '::1', 'Internal', 4, 0, 1768869688); -INSERT INTO `user_access_logs` VALUES (4, 0, '::1', 'Internal', 4, 0, 1768869746); -INSERT INTO `user_access_logs` VALUES (5, 0, '::1', 'Internal', 4, 0, 1768870362); -INSERT INTO `user_access_logs` VALUES (6, 0, '::1', 'Internal', 4, 0, 1768870495); -INSERT INTO `user_access_logs` VALUES (7, 0, '::1', 'Internal', 4, 0, 1768870534); -INSERT INTO `user_access_logs` VALUES (8, 0, '::1', 'Internal', 4, 0, 1768870538); -INSERT INTO `user_access_logs` VALUES (9, 0, '::1', 'Internal', 4, 0, 1768870540); -INSERT INTO `user_access_logs` VALUES (10, 0, '::1', 'Internal', 4, 0, 1768870545); -INSERT INTO `user_access_logs` VALUES (11, 0, '::1', 'Internal', 4, 0, 1768870579); -INSERT INTO `user_access_logs` VALUES (12, 0, '::1', 'Internal', 4, 0, 1768870580); -INSERT INTO `user_access_logs` VALUES (13, 0, '::1', 'Internal', 4, 0, 1768870586); -INSERT INTO `user_access_logs` VALUES (14, 0, '::1', 'Internal', 4, 0, 1768870637); -INSERT INTO `user_access_logs` VALUES (15, 0, '::1', 'Internal', 5, 0, 1768870640); -INSERT INTO `user_access_logs` VALUES (16, 1, '::1', 'Internal', 0, 0, 1768872916); -INSERT INTO `user_access_logs` VALUES (17, 0, '::1', 'Internal', 6, 0, 1768878230); -INSERT INTO `user_access_logs` VALUES (18, 0, '::1', 'Internal', 6, 0, 1768878635); -INSERT INTO `user_access_logs` VALUES (19, 0, '::1', 'Internal', 6, 0, 1768878639); -INSERT INTO `user_access_logs` VALUES (20, 0, '::1', 'Internal', 6, 0, 1768878649); -INSERT INTO `user_access_logs` VALUES (21, 0, '::1', 'Internal', 6, 0, 1768878651); -INSERT INTO `user_access_logs` VALUES (22, 0, '::1', 'Internal', 6, 0, 1768878667); -INSERT INTO `user_access_logs` VALUES (23, 0, '::1', 'Internal', 6, 0, 1768878844); -INSERT INTO `user_access_logs` VALUES (24, 0, '::1', 'Internal', 6, 0, 1768878871); -INSERT INTO `user_access_logs` VALUES (25, 0, '::1', 'Internal', 6, 0, 1768878875); -INSERT INTO `user_access_logs` VALUES (26, 0, '::1', 'Internal', 5, 0, 1768879449); -INSERT INTO `user_access_logs` VALUES (27, 0, '::1', 'Internal', 5, 0, 1768879452); -INSERT INTO `user_access_logs` VALUES (28, 0, '::1', 'Internal', 5, 0, 1768879513); -INSERT INTO `user_access_logs` VALUES (29, 0, '::1', 'Internal', 5, 0, 1768880797); -INSERT INTO `user_access_logs` VALUES (30, 0, '::1', 'Internal', 5, 0, 1768880799); -INSERT INTO `user_access_logs` VALUES (31, 0, '::1', 'Internal', 5, 0, 1768880810); -INSERT INTO `user_access_logs` VALUES (32, 0, '::1', 'Internal', 5, 0, 1768881631); -INSERT INTO `user_access_logs` VALUES (33, 0, '::1', 'Internal', 5, 0, 1768881636); -INSERT INTO `user_access_logs` VALUES (34, 0, '::1', 'Internal', 5, 0, 1768881706); -INSERT INTO `user_access_logs` VALUES (35, 0, '::1', 'Internal', 5, 0, 1768883044); -INSERT INTO `user_access_logs` VALUES (36, 0, '::1', 'Internal', 5, 0, 1768883060); -INSERT INTO `user_access_logs` VALUES (37, 0, '::1', 'Internal', 5, 0, 1768883290); -INSERT INTO `user_access_logs` VALUES (38, 0, '::1', 'Internal', 5, 0, 1768883291); -INSERT INTO `user_access_logs` VALUES (39, 0, '::1', 'Internal', 5, 0, 1768886263); -INSERT INTO `user_access_logs` VALUES (40, 0, '::1', 'Internal', 5, 0, 1768886279); -INSERT INTO `user_access_logs` VALUES (41, 0, '::1', 'Internal', 5, 0, 1768886433); -INSERT INTO `user_access_logs` VALUES (42, 0, '::1', 'Internal', 5, 0, 1768886788); -INSERT INTO `user_access_logs` VALUES (43, 0, '::1', 'Internal', 5, 0, 1768886790); -INSERT INTO `user_access_logs` VALUES (44, 0, '::1', 'Internal', 5, 0, 1768887178); -INSERT INTO `user_access_logs` VALUES (45, 0, '::1', 'Internal', 5, 0, 1768887180); + INDEX `idx_article_id`(`article_id` ASC) USING BTREE, + INDEX `idx_dedup_hour`(`article_id` ASC, `visitor_key` ASC, `access_time` ASC) USING BTREE, + INDEX `idx_dedup_user_hour`(`article_id` ASC, `user_id` ASC, `access_time` ASC) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 298 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '用户访问记录表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for users @@ -984,14 +477,7 @@ CREATE TABLE `users` ( INDEX `idx_email`(`email` ASC) USING BTREE COMMENT '按邮箱查询索引', INDEX `idx_role`(`role` ASC) USING BTREE COMMENT '按角色查询索引', INDEX `users_ibfk_1`(`role_id` ASC) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户表' ROW_FORMAT = DYNAMIC; - --- ---------------------------- --- Records of users --- ---------------------------- -INSERT INTO `users` VALUES (1, 'lq', 'liqiworker@gmail.com', '$2a$10$b.KMnG261WjsvdKciKn01uRpuZs2eIiiIIXhpwIu6rgQ8AL5oxA8e', 1, 'admin', 1, 0, 1768291814, 1768538951); -INSERT INTO `users` VALUES (2, 'editor', 'editor@example.com', '$2a$14$OqZ7svgz3S68yuuXTJWz1.0CuqI2i3WvwC1jIGBy4l0mcJW9Kqvy2', 2, 'editor', 1, 0, 1768291814, 1768538951); -INSERT INTO `users` VALUES (5, 'cs', 'cs@nailaoyun.cn', '$2a$14$OqZ7svgz3S68yuuXTJWz1.0CuqI2i3WvwC1jIGBy4l0mcJW9Kqvy2', 3, 'viewer', 1, 0, 1768462115, 1768538951); +) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for work_gallery @@ -1008,40 +494,7 @@ CREATE TABLE `work_gallery` ( PRIMARY KEY (`id`) USING BTREE, INDEX `idx_work_id`(`work_id` ASC) USING BTREE, INDEX `idx_sort_order`(`sort_order` ASC) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 30 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '作品图库表' ROW_FORMAT = DYNAMIC; - --- ---------------------------- --- Records of work_gallery --- ---------------------------- -INSERT INTO `work_gallery` VALUES (1, 'nova', 'https://images.unsplash.com/photo-1642543492481-44e81e3914a7?q=80&w=2070', 1, 'Nova 交易平台首页', 1768826702, 1768291937); -INSERT INTO `work_gallery` VALUES (2, 'nova', 'https://images.unsplash.com/photo-1551288049-bebda4e38f71?q=80&w=2070', 2, 'Nova 交易平台交易界面', 1768826702, 1768291937); -INSERT INTO `work_gallery` VALUES (3, 'archdaily', 'https://images.unsplash.com/photo-1503387762-592deb58ef4e?q=80&w=2089', 1, 'ArchDaily 网站首页', 1768828228, 1768291937); -INSERT INTO `work_gallery` VALUES (4, 'archdaily', 'https://images.unsplash.com/photo-1518005020951-ecc859466abc?q=80&w=1920', 2, 'ArchDaily 文章详情页', 1768828228, 1768291937); -INSERT INTO `work_gallery` VALUES (5, 'nova', 'https://images.unsplash.com/photo-1642543492481-44e81e3914a7?q=80&w=2070', 1, '', 1768826702, 1768825221); -INSERT INTO `work_gallery` VALUES (6, 'nova', 'https://images.unsplash.com/photo-1551288049-bebda4e38f71?q=80&w=2070', 2, '', 1768826702, 1768825221); -INSERT INTO `work_gallery` VALUES (7, 'nova', '/uploads/2026/01/19/cc_upload_HiPDFg6oL0Q5ijfo693ce1f5eb57c_1768825204.jpg', 3, '', 1768826702, 1768825221); -INSERT INTO `work_gallery` VALUES (8, 'nova', 'https://images.unsplash.com/photo-1642543492481-44e81e3914a7?q=80&w=2070', 1, '', 0, 1768826702); -INSERT INTO `work_gallery` VALUES (9, 'nova', 'https://images.unsplash.com/photo-1551288049-bebda4e38f71?q=80&w=2070', 2, '', 0, 1768826702); -INSERT INTO `work_gallery` VALUES (10, 'nova', '/uploads/2026/01/19/cc_upload_HiPDFg6oL0Q5ijfo693ce1f5eb57c_1768825204.jpg', 3, '', 0, 1768826702); -INSERT INTO `work_gallery` VALUES (11, 'nova', 'https://images.unsplash.com/photo-1642543492481-44e81e3914a7?q=80&w=2070', 4, '', 0, 1768826702); -INSERT INTO `work_gallery` VALUES (12, 'nova', 'https://images.unsplash.com/photo-1551288049-bebda4e38f71?q=80&w=2070', 5, '', 0, 1768826702); -INSERT INTO `work_gallery` VALUES (13, 'nova', '/uploads/2026/01/19/cc_upload_HiPDFg6oL0Q5ijfo693ce1f5eb57c_1768825204.jpg', 6, '', 0, 1768826702); -INSERT INTO `work_gallery` VALUES (14, 'nova', '/uploads/2026/01/19/cc_upload_0NeLXeWx8AmBi7Mx693ce05fea320_1768824028.jpg', 7, '', 0, 1768826702); -INSERT INTO `work_gallery` VALUES (15, 'nova', '/uploads/2026/01/19/cc_upload_62sXZ0HMvINRUYDO693d6c27c7045_1768824028.jpg', 8, '', 0, 1768826702); -INSERT INTO `work_gallery` VALUES (16, 'nova', '/uploads/2026/01/19/cc_upload_9AqkM4FiiRhzl1On693d6c27ce6eb_1768824028.jpg', 9, '', 0, 1768826702); -INSERT INTO `work_gallery` VALUES (17, 'nova', '/uploads/2026/01/19/cc_upload_gXTnJhQ2DSbBr3Ql693d6c27c0b62_1768824028.jpg', 10, '', 0, 1768826702); -INSERT INTO `work_gallery` VALUES (18, 'nova', '/uploads/2026/01/19/cc_upload_etVM7GrPwl33KYzn693cc3016abdf_1768824028.png', 11, '', 0, 1768826702); -INSERT INTO `work_gallery` VALUES (19, 'nova', '/uploads/2026/01/19/cc_upload_xTUFrRBdm7zWyjQs693a39ea93969_1768824015.jpg', 12, '', 0, 1768826702); -INSERT INTO `work_gallery` VALUES (20, 'archdaily', 'https://images.unsplash.com/photo-1503387762-592deb58ef4e?q=80&w=2089', 1, '', 0, 1768828228); -INSERT INTO `work_gallery` VALUES (21, 'archdaily', '/uploads/2026/01/19/cc_upload_7BNbjCmbBatx3S4t6958b017537b6_1768828219.jpg', 2, '', 0, 1768828228); -INSERT INTO `work_gallery` VALUES (22, 'archdaily', '/uploads/2026/01/19/cc_upload_P17ls1spvgI0uDk56958b02daaf14_1768828219.jpg', 3, '', 0, 1768828228); -INSERT INTO `work_gallery` VALUES (23, 'work_1768828710998326100', '/uploads/2026/01/19/cc_upload_HiPDFg6oL0Q5ijfo693ce1f5eb57c_1768825204.jpg', 1, '', 0, 1768828711); -INSERT INTO `work_gallery` VALUES (24, 'work_1768828710998326100', '/uploads/2026/01/19/cc_upload_P17ls1spvgI0uDk56958b02daaf14_1768828219.jpg', 2, '', 0, 1768828711); -INSERT INTO `work_gallery` VALUES (25, 'work_1768828710998326100', '/uploads/2026/01/19/cc_upload_7BNbjCmbBatx3S4t6958b017537b6_1768828219.jpg', 3, '', 0, 1768828711); -INSERT INTO `work_gallery` VALUES (26, 'work_1768828710998326100', '/uploads/2026/01/19/cc_upload_gXTnJhQ2DSbBr3Ql693d6c27c0b62_1768824028.jpg', 4, '', 0, 1768828711); -INSERT INTO `work_gallery` VALUES (27, 'work_1768828710998326100', '/uploads/2026/01/19/cc_upload_etVM7GrPwl33KYzn693cc3016abdf_1768824028.png', 5, '', 0, 1768828711); -INSERT INTO `work_gallery` VALUES (28, 'work_1768828710998326100', '/uploads/2026/01/19/cc_upload_xTUFrRBdm7zWyjQs693a39ea93969_1768824015.jpg', 6, '', 0, 1768828711); -INSERT INTO `work_gallery` VALUES (29, 'work_1768828710998326100', '/uploads/2026/01/19/cc_upload_9AqkM4FiiRhzl1On693d6c27ce6eb_1768824028.jpg', 7, '', 0, 1768828711); +) ENGINE = InnoDB AUTO_INCREMENT = 30 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '作品图库表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for work_tech_stack @@ -1057,38 +510,7 @@ CREATE TABLE `work_tech_stack` ( PRIMARY KEY (`id`) USING BTREE, INDEX `idx_work_id`(`work_id` ASC) USING BTREE, INDEX `idx_category`(`category` ASC) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 28 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '作品技术栈表' ROW_FORMAT = DYNAMIC; - --- ---------------------------- --- Records of work_tech_stack --- ---------------------------- -INSERT INTO `work_tech_stack` VALUES (1, 'nova', '前端层', 'React 18', 1768826702, 1768291937); -INSERT INTO `work_tech_stack` VALUES (2, 'nova', '前端层', 'TypeScript', 1768826702, 1768291937); -INSERT INTO `work_tech_stack` VALUES (3, 'nova', '前端层', 'D3.js', 1768826702, 1768291937); -INSERT INTO `work_tech_stack` VALUES (4, 'nova', '后端服务', 'Golang', 1768826702, 1768291937); -INSERT INTO `work_tech_stack` VALUES (5, 'nova', '后端服务', 'gRPC', 1768826702, 1768291937); -INSERT INTO `work_tech_stack` VALUES (6, 'archdaily', '核心前端', 'Vue 3', 1768828228, 1768291937); -INSERT INTO `work_tech_stack` VALUES (7, 'archdaily', '核心前端', 'Nuxt.js', 1768828228, 1768291937); -INSERT INTO `work_tech_stack` VALUES (8, 'archdaily', '核心前端', 'GSAP', 1768828228, 1768291937); -INSERT INTO `work_tech_stack` VALUES (9, 'archdaily', 'CMS', 'Strapi', 1768828228, 1768291937); -INSERT INTO `work_tech_stack` VALUES (10, 'nova', '前端层', 'React 18', 1768826702, 1768825220); -INSERT INTO `work_tech_stack` VALUES (11, 'nova', '前端层', 'TypeScript', 1768826702, 1768825220); -INSERT INTO `work_tech_stack` VALUES (12, 'nova', '前端层', 'D3.js', 1768826702, 1768825220); -INSERT INTO `work_tech_stack` VALUES (13, 'nova', '后端服务', 'Golang', 1768826702, 1768825220); -INSERT INTO `work_tech_stack` VALUES (14, 'nova', '后端服务', 'gRPC', 1768826702, 1768825220); -INSERT INTO `work_tech_stack` VALUES (15, 'nova', '前端层', 'React 18', 0, 1768826702); -INSERT INTO `work_tech_stack` VALUES (16, 'nova', '前端层', 'TypeScript', 0, 1768826702); -INSERT INTO `work_tech_stack` VALUES (17, 'nova', '前端层', 'D3.js', 0, 1768826702); -INSERT INTO `work_tech_stack` VALUES (18, 'nova', '后端服务', 'Golang', 0, 1768826702); -INSERT INTO `work_tech_stack` VALUES (19, 'nova', '后端服务', 'gRPC', 0, 1768826702); -INSERT INTO `work_tech_stack` VALUES (20, 'archdaily', 'CMS', 'Strapi', 0, 1768828228); -INSERT INTO `work_tech_stack` VALUES (21, 'archdaily', '核心前端', 'Vue 3', 0, 1768828228); -INSERT INTO `work_tech_stack` VALUES (22, 'archdaily', '核心前端', 'Nuxt.js', 0, 1768828228); -INSERT INTO `work_tech_stack` VALUES (23, 'archdaily', '核心前端', 'GSAP', 0, 1768828228); -INSERT INTO `work_tech_stack` VALUES (24, 'work_1768828710998326100', '前端', 'Vue3', 0, 1768828711); -INSERT INTO `work_tech_stack` VALUES (25, 'work_1768828710998326100', '前端', 'Ts', 0, 1768828711); -INSERT INTO `work_tech_stack` VALUES (26, 'work_1768828710998326100', '前端', 'Tailwindcss', 0, 1768828711); -INSERT INTO `work_tech_stack` VALUES (27, 'work_1768828710998326100', '前端', 'IndexDB', 0, 1768828711); +) ENGINE = InnoDB AUTO_INCREMENT = 28 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '作品技术栈表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for works @@ -1110,13 +532,6 @@ CREATE TABLE `works` ( INDEX `idx_category`(`category` ASC) USING BTREE COMMENT '按分类查询索引', INDEX `idx_year`(`year` ASC) USING BTREE COMMENT '按年份查询索引', INDEX `idx_is_featured`(`is_featured` ASC) USING BTREE COMMENT '按精选状态查询索引' -) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '作品表' ROW_FORMAT = DYNAMIC; - --- ---------------------------- --- Records of works --- ---------------------------- -INSERT INTO `works` VALUES ('archdaily', 'ArchDaily 网站重构', '建筑设计', '2022', 'https://images.unsplash.com/photo-1487958449943-2429e8be8625?q=80&w=2070', 'ArchDaily 是全球最受欢迎的建筑网站之一。这次重构的目标是提升移动端体验。我们使用了 Nuxt 3 进行服务端渲染(SSR)。', '{\"demo\":\"http://localhost:3000/works\",\"github\":\"http://localhost:3000/works\",\"live\":\"http://localhost:3000/works\"}', 0, 0, 1768291814, 1768828228); -INSERT INTO `works` VALUES ('nova', 'Nova 交易平台', '金融科技', '2023', 'https://images.unsplash.com/photo-1487958449943-2429e8be8625?q=80&w=2070', 'Nova 是一个专为机构交易员设计的高频交易终端。我们面临的最大挑战是如何在处理毫秒级市场数据的同时,保持界面的流畅响应。我们采用 Web Worker 来处理繁重的数据计算,避免阻塞主线程。', '{\"demo\":\"http://localhost:3000/works/nova\",\"github\":\"http://localhost:3000/works/nova\",\"live\":\"http://localhost:3000/works/nova\"}', 0, 0, 1768291814, 1768826701); -INSERT INTO `works` VALUES ('work_1768828710998326100', 'Mac OS', '仿系统', '2025', '/uploads/2026/01/19/cc_upload_7BNbjCmbBatx3S4t6958b017537b6_1768828707.jpg', '这是一个基于Vue3的仿Mac风格系统前端项目,可以而开成企业网盘这些功能', '{\"demo\":\"\",\"github\":\"\",\"live\":\"\"}', 0, 0, 1768828711, 1768828711); +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '作品表' ROW_FORMAT = Dynamic; SET FOREIGN_KEY_CHECKS = 1; diff --git a/server/nl_blog1.sql b/server/nl_blog1.sql deleted file mode 100644 index afe3b28..0000000 --- a/server/nl_blog1.sql +++ /dev/null @@ -1,898 +0,0 @@ -/* - Navicat Premium Dump SQL - - Source Server : 开发环境-本地 - Source Server Type : MySQL - Source Server Version : 80407 (8.4.7) - Source Host : localhost:3306 - Source Schema : nl_blog - - Target Server Type : MySQL - Target Server Version : 80407 (8.4.7) - File Encoding : 65001 - - Date: 19/01/2026 16:13:35 -*/ - -SET NAMES utf8mb4; -SET FOREIGN_KEY_CHECKS = 0; - --- ---------------------------- --- Table structure for about_profiles --- ---------------------------- -DROP TABLE IF EXISTS `about_profiles`; -CREATE TABLE `about_profiles` ( - `id` int NOT NULL AUTO_INCREMENT, - `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `avatar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '', - `location` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '', - `bio` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL, - `email` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '', - `wechat` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '', - `tech_stack` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT 'JSON string or comma separated list', - `experiences` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT 'JSON string of experience list', - `is_primary` tinyint(1) NULL DEFAULT 0, - `created_at` bigint NOT NULL DEFAULT 0, - `updated_at` bigint NOT NULL DEFAULT 0, - `deleted_at` bigint NOT NULL DEFAULT 0, - PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = DYNAMIC; - --- ---------------------------- --- Records of about_profiles --- ---------------------------- -INSERT INTO `about_profiles` VALUES (3, '年糕崽崽', 'https://api.dicebear.com/7.x/avataaars/svg?seed=NianGao&backgroundColor=b6e3f4', '中国 · 浙江杭州', '嗨!我是年糕崽崽,一名热衷于探索数字边界的前端架构师。我的职业生涯始于对像素级完美的执念。从早期的 jQuery 插件开发到如今的 WebGL 3D 场景构建,我始终认为:技术是骨架,艺术是灵魂。

目前,我专注于高性能 B 端应用的体验升级,以及探索生成式艺术(Generative Art)在网页中的应用。闲暇之余,我喜欢在杭州的西湖边散步,或者捣腾我的客制化机械键盘。', 'liqiworker@gmail.com', 'ngzz_0218', '[\\\"Vue 3\\\",\\\"React\\\",\\\"TypeScript\\\",\\\"Three.js\\\",\\\"Golang\\\",\\\"Tailwind CSS\\\",\\\"Rust\\\",\\\"Wails\\\"]', '[{\\\"year\\\":\\\"2024 - 至今\\\",\\\"role\\\":\\\"技术负责人\\\",\\\"company\\\":\\\"某医疗平台公司\\\"},{\\\"year\\\":\\\"2020 - 2024\\\",\\\"role\\\":\\\"PHP开发工程师\\\",\\\"company\\\":\\\"某电商公司\\\"}]', 0, 0, 0, 0); - --- ---------------------------- --- Table structure for access_logs --- ---------------------------- -DROP TABLE IF EXISTS `access_logs`; -CREATE TABLE `access_logs` ( - `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', - `ip` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '访问者IP地址', - `user_agent` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '访问者浏览器信息', - `path` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '访问路径', - `method` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'HTTP方法', - `status_code` int UNSIGNED NOT NULL COMMENT 'HTTP状态码', - `response_time` int UNSIGNED NOT NULL COMMENT '响应时间(毫秒)', - `deleted_at` bigint NOT NULL DEFAULT 0, - `created_at` bigint NOT NULL DEFAULT 0, - PRIMARY KEY (`id`) USING BTREE, - INDEX `idx_path`(`path` ASC) USING BTREE COMMENT '按访问路径查询索引', - INDEX `idx_status_code`(`status_code` ASC) USING BTREE COMMENT '按状态码查询索引' -) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '访问日志表' ROW_FORMAT = DYNAMIC; - --- ---------------------------- --- Records of access_logs --- ---------------------------- - --- ---------------------------- --- Table structure for attachment_categories --- ---------------------------- -DROP TABLE IF EXISTS `attachment_categories`; -CREATE TABLE `attachment_categories` ( - `id` int UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', - `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '分类名称', - `description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '分类描述', - `sort_order` int UNSIGNED NULL DEFAULT 0 COMMENT '排序', - `deleted_at` bigint NOT NULL DEFAULT 0, - `created_at` bigint NOT NULL DEFAULT 0, - `updated_at` bigint NOT NULL DEFAULT 0, - PRIMARY KEY (`id`) USING BTREE, - INDEX `idx_sort_order`(`sort_order` ASC) USING BTREE -) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '附件分类表' ROW_FORMAT = DYNAMIC; - --- ---------------------------- --- Records of attachment_categories --- ---------------------------- - --- ---------------------------- --- Table structure for attachments --- ---------------------------- -DROP TABLE IF EXISTS `attachments`; -CREATE TABLE `attachments` ( - `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', - `category_id` int UNSIGNED NULL DEFAULT NULL COMMENT '附件分类ID', - `original_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '原始文件名', - `stored_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '存储文件名', - `file_path` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '文件路径', - `file_url` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '文件访问URL', - `file_size` bigint UNSIGNED NOT NULL DEFAULT 0 COMMENT '文件大小(字节)', - `file_type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '文件类型: image/video/document/other', - `mime_type` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT 'MIME类型', - `storage_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'local' COMMENT '存储类型: local/qcloud/aliyun/qiniu', - `oss_config_id` int UNSIGNED NULL DEFAULT NULL COMMENT 'OSS配置ID', - `deleted_at` bigint NOT NULL DEFAULT 0, - `created_at` bigint NOT NULL DEFAULT 0, - `updated_at` bigint NOT NULL DEFAULT 0, - PRIMARY KEY (`id`) USING BTREE, - INDEX `idx_category_id`(`category_id` ASC) USING BTREE, - INDEX `idx_storage_type`(`storage_type` ASC) USING BTREE, - INDEX `idx_file_type`(`file_type` ASC) USING BTREE, - INDEX `idx_created_at`(`created_at` ASC) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '附件表' ROW_FORMAT = DYNAMIC; - --- ---------------------------- --- Records of attachments --- ---------------------------- -INSERT INTO `attachments` VALUES (1, NULL, 'cc_upload_xTUFrRBdm7zWyjQs693a39ea93969.jpg', 'cc_upload_xTUFrRBdm7zWyjQs693a39ea93969.jpg', 'uploads\\2026\\01\\19\\cc_upload_xTUFrRBdm7zWyjQs693a39ea93969_1768807852.jpg', '/uploads/2026/01/19/cc_upload_xTUFrRBdm7zWyjQs693a39ea93969_1768807852.jpg', 40648, 'image', 'image/jpeg', 'local', NULL, 0, 1768807852, 1768807852); - --- ---------------------------- --- Table structure for categories --- ---------------------------- -DROP TABLE IF EXISTS `categories`; -CREATE TABLE `categories` ( - `id` int UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '分类ID', - `name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '分类名称', - `slug` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '分类别名', - `description` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '描述', - `sort_order` int UNSIGNED NULL DEFAULT 0 COMMENT '排序', - `deleted_at` bigint NOT NULL DEFAULT 0, - `created_at` bigint NOT NULL DEFAULT 0, - `updated_at` bigint NOT NULL DEFAULT 0, - PRIMARY KEY (`id`) USING BTREE, - UNIQUE INDEX `uk_slug`(`slug` ASC) USING BTREE, - INDEX `idx_sort_order`(`sort_order` ASC) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '文章分类表' ROW_FORMAT = DYNAMIC; - --- ---------------------------- --- Records of categories --- ---------------------------- -INSERT INTO `categories` VALUES (1, '工程化', '工程化', NULL, 0, 0, 0, 0); -INSERT INTO `categories` VALUES (2, '图形渲染', '图形渲染', NULL, 0, 0, 0, 0); -INSERT INTO `categories` VALUES (3, '设计思维', '设计思维', NULL, 0, 0, 0, 0); -INSERT INTO `categories` VALUES (4, 'Go语言', 'Go语言', NULL, 0, 0, 0, 0); - --- ---------------------------- --- Table structure for column_posts --- ---------------------------- -DROP TABLE IF EXISTS `column_posts`; -CREATE TABLE `column_posts` ( - `column_id` int UNSIGNED NOT NULL COMMENT '专栏ID', - `post_id` int UNSIGNED NOT NULL COMMENT '文章ID', - `sort_order` int UNSIGNED NULL DEFAULT 0 COMMENT '排序', - `created_at` bigint NOT NULL DEFAULT 0, - PRIMARY KEY (`column_id`, `post_id`) USING BTREE, - INDEX `idx_post_id`(`post_id` ASC) USING BTREE -) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '专栏文章关联表' ROW_FORMAT = DYNAMIC; - --- ---------------------------- --- Records of column_posts --- ---------------------------- -INSERT INTO `column_posts` VALUES (0, 1, 0, 0); -INSERT INTO `column_posts` VALUES (0, 2, 0, 0); -INSERT INTO `column_posts` VALUES (0, 3, 0, 0); -INSERT INTO `column_posts` VALUES (1, 4, 0, 0); -INSERT INTO `column_posts` VALUES (1, 5, 0, 0); -INSERT INTO `column_posts` VALUES (1, 6, 0, 1768809352); - --- ---------------------------- --- Table structure for columns --- ---------------------------- -DROP TABLE IF EXISTS `columns`; -CREATE TABLE `columns` ( - `id` int UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '专栏ID', - `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '专栏名称', - `description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '专栏描述', - `cover` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '专栏封面', - `is_active` tinyint(1) NOT NULL DEFAULT 1 COMMENT '是否启用', - `sort_order` int UNSIGNED NULL DEFAULT 0 COMMENT '排序', - `deleted_at` bigint NOT NULL DEFAULT 0, - `created_at` bigint NOT NULL DEFAULT 0, - `updated_at` bigint NOT NULL DEFAULT 0, - PRIMARY KEY (`id`) USING BTREE, - INDEX `idx_sort_order`(`sort_order` ASC) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '专栏表' ROW_FORMAT = DYNAMIC; - --- ---------------------------- --- Records of columns --- ---------------------------- -INSERT INTO `columns` VALUES (1, 'Goravel', 'Goravel入门手册', 'https://www.goravel.dev/logo.png', 1, 0, 0, 1768552230, 1768552230); - --- ---------------------------- --- Table structure for email_suffixes --- ---------------------------- -DROP TABLE IF EXISTS `email_suffixes`; -CREATE TABLE `email_suffixes` ( - `id` int NOT NULL AUTO_INCREMENT, - `suffix` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '邮箱后缀', - `is_active` tinyint(1) NOT NULL DEFAULT 1 COMMENT '是否启用', - `sort_order` int NOT NULL DEFAULT 0 COMMENT '排序', - `deleted_at` bigint NOT NULL DEFAULT 0, - `created_at` bigint NOT NULL DEFAULT 0, - `updated_at` bigint NOT NULL DEFAULT 0, - PRIMARY KEY (`id`) USING BTREE, - UNIQUE INDEX `uk_suffix`(`suffix` ASC) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 15 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '邮箱后缀配置表' ROW_FORMAT = DYNAMIC; - --- ---------------------------- --- Records of email_suffixes --- ---------------------------- -INSERT INTO `email_suffixes` VALUES (1, '@gmail.com', 1, 1, 0, 1768523866, 1768538954); -INSERT INTO `email_suffixes` VALUES (2, '@163.com', 1, 2, 0, 1768523866, 1768538954); -INSERT INTO `email_suffixes` VALUES (3, '@qq.com', 1, 3, 0, 1768523866, 1768538954); -INSERT INTO `email_suffixes` VALUES (4, '@outlook.com', 1, 4, 0, 1768523866, 1768538954); -INSERT INTO `email_suffixes` VALUES (5, '@foxmail.com', 1, 5, 0, 1768523866, 1768538954); -INSERT INTO `email_suffixes` VALUES (6, '@sina.com', 1, 6, 0, 1768523866, 1768538954); -INSERT INTO `email_suffixes` VALUES (7, '@126.com', 1, 7, 0, 1768523866, 1768538954); - --- ---------------------------- --- Table structure for inquiries --- ---------------------------- -DROP TABLE IF EXISTS `inquiries`; -CREATE TABLE `inquiries` ( - `id` int NOT NULL AUTO_INCREMENT, - `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '姓名', - `company` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '公司/组织', - `contact_method` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '联系方式类型(email/wechat/phone)', - `contact_value` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '联系方式值', - `budget` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '预算范围', - `description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '需求描述', - `status` tinyint NOT NULL DEFAULT 0 COMMENT '状态: 0-未读, 1-已读, 2-已联系', - `deleted_at` bigint NOT NULL DEFAULT 0, - `created_at` bigint NOT NULL DEFAULT 0, - `updated_at` bigint NOT NULL DEFAULT 0, - PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '合作咨询表' ROW_FORMAT = DYNAMIC; - --- ---------------------------- --- Records of inquiries --- ---------------------------- - --- ---------------------------- --- Table structure for operation_logs --- ---------------------------- -DROP TABLE IF EXISTS `operation_logs`; -CREATE TABLE `operation_logs` ( - `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', - `user_id` bigint UNSIGNED NOT NULL COMMENT '操作用户ID', - `username` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '操作用户名', - `ip` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '操作IP地址', - `path` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '操作路径', - `method` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'HTTP方法', - `params` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '请求参数', - `status` int NOT NULL COMMENT '响应状态码', - `duration` int NOT NULL COMMENT '响应时间(毫秒)', - `deleted_at` bigint NOT NULL DEFAULT 0, - `created_at` bigint NOT NULL DEFAULT 0, - PRIMARY KEY (`id`) USING BTREE, - INDEX `idx_user_id`(`user_id` ASC) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 720 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '操作日志表' ROW_FORMAT = DYNAMIC; - --- ---------------------------- --- Records of operation_logs --- ---------------------------- -INSERT INTO `operation_logs` VALUES (644, 1, 'lq', '::1', '/api/admin/operation-logs', 'GET', '', 200, 52, 0, 1768807488); -INSERT INTO `operation_logs` VALUES (645, 1, 'lq', '::1', '/api/admin/dashboard/stats', 'GET', '', 200, 330, 0, 1768807489); -INSERT INTO `operation_logs` VALUES (646, 1, 'lq', '::1', '/api/admin/posts', 'GET', '', 200, 224, 0, 1768807490); -INSERT INTO `operation_logs` VALUES (647, 1, 'lq', '::1', '/api/admin/settings', 'GET', '', 200, 45, 0, 1768807512); -INSERT INTO `operation_logs` VALUES (648, 1, 'lq', '::1', '/api/admin/attachment-categories', 'GET', '', 200, 47, 0, 1768807513); -INSERT INTO `operation_logs` VALUES (649, 1, 'lq', '::1', '/api/admin/attachments', 'GET', '', 200, 89, 0, 1768807513); -INSERT INTO `operation_logs` VALUES (650, 1, 'lq', '::1', '/api/admin/operation-logs', 'GET', '', 200, 90, 0, 1768807515); -INSERT INTO `operation_logs` VALUES (651, 1, 'lq', '::1', '/api/admin/settings', 'GET', '', 200, 45, 0, 1768807516); -INSERT INTO `operation_logs` VALUES (652, 1, 'lq', '::1', '/api/admin/attachment-categories', 'GET', '', 200, 45, 0, 1768807569); -INSERT INTO `operation_logs` VALUES (653, 1, 'lq', '::1', '/api/admin/attachments', 'GET', '', 200, 90, 0, 1768807569); -INSERT INTO `operation_logs` VALUES (654, 1, 'lq', '::1', '/api/admin/attachment-categories', 'GET', '', 200, 46, 0, 1768807801); -INSERT INTO `operation_logs` VALUES (655, 1, 'lq', '::1', '/api/admin/attachments', 'GET', '', 200, 91, 0, 1768807801); -INSERT INTO `operation_logs` VALUES (656, 1, 'lq', '::1', '/api/admin/attachment-categories', 'GET', '', 200, 46, 0, 1768807805); -INSERT INTO `operation_logs` VALUES (657, 1, 'lq', '::1', '/api/admin/attachments', 'GET', '', 200, 92, 0, 1768807805); -INSERT INTO `operation_logs` VALUES (658, 1, 'lq', '::1', '/api/admin/attachment-categories', 'GET', '', 200, 51, 0, 1768807839); -INSERT INTO `operation_logs` VALUES (659, 1, 'lq', '::1', '/api/admin/attachments', 'GET', '', 200, 98, 0, 1768807839); -INSERT INTO `operation_logs` VALUES (660, 1, 'lq', '::1', '/api/admin/attachments', 'GET', '', 200, 97, 0, 1768807843); -INSERT INTO `operation_logs` VALUES (661, 1, 'lq', '::1', '/api/admin/attachments', 'GET', '', 200, 95, 0, 1768807844); -INSERT INTO `operation_logs` VALUES (662, 1, 'lq', '::1', '/api/admin/attachments', 'GET', '', 200, 98, 0, 1768807845); -INSERT INTO `operation_logs` VALUES (663, 1, 'lq', '::1', '/api/admin/attachments', 'GET', '', 200, 95, 0, 1768807852); -INSERT INTO `operation_logs` VALUES (664, 1, 'lq', '::1', '/api/admin/attachment-categories', 'GET', '', 200, 44, 0, 1768807858); -INSERT INTO `operation_logs` VALUES (665, 1, 'lq', '::1', '/api/admin/attachments', 'GET', '', 200, 88, 0, 1768807858); -INSERT INTO `operation_logs` VALUES (666, 1, 'lq', '::1', '/api/admin/attachments', 'GET', '', 200, 96, 0, 1768807861); -INSERT INTO `operation_logs` VALUES (667, 1, 'lq', '::1', '/api/admin/attachments', 'GET', '', 200, 94, 0, 1768807862); -INSERT INTO `operation_logs` VALUES (668, 1, 'lq', '::1', '/api/admin/attachments', 'GET', '', 200, 96, 0, 1768807863); -INSERT INTO `operation_logs` VALUES (669, 1, 'lq', '::1', '/api/admin/attachments', 'GET', '', 200, 98, 0, 1768807863); -INSERT INTO `operation_logs` VALUES (670, 1, 'lq', '::1', '/api/admin/attachments/1', 'DELETE', '', 400, 45, 0, 1768807885); -INSERT INTO `operation_logs` VALUES (671, 1, 'lq', '::1', '/api/admin/attachment-categories', 'GET', '', 200, 47, 0, 1768807890); -INSERT INTO `operation_logs` VALUES (672, 1, 'lq', '::1', '/api/admin/attachments', 'GET', '', 200, 50, 0, 1768807890); -INSERT INTO `operation_logs` VALUES (673, 1, 'lq', '::1', '/api/admin/attachments/1', 'DELETE', '', 400, 47, 0, 1768807893); -INSERT INTO `operation_logs` VALUES (674, 1, 'lq', '::1', '/api/admin/attachment-categories', 'GET', '', 200, 43, 0, 1768807942); -INSERT INTO `operation_logs` VALUES (675, 1, 'lq', '::1', '/api/admin/attachments', 'GET', '', 200, 91, 0, 1768807942); -INSERT INTO `operation_logs` VALUES (676, 1, 'lq', '::1', '/api/admin/attachment-categories', 'GET', '', 200, 49, 0, 1768807943); -INSERT INTO `operation_logs` VALUES (677, 1, 'lq', '::1', '/api/admin/attachments', 'GET', '', 200, 97, 0, 1768807943); -INSERT INTO `operation_logs` VALUES (678, 1, 'lq', '::1', '/api/admin/attachment-categories', 'GET', '', 200, 46, 0, 1768807958); -INSERT INTO `operation_logs` VALUES (679, 1, 'lq', '::1', '/api/admin/attachments', 'GET', '', 200, 91, 0, 1768807958); -INSERT INTO `operation_logs` VALUES (680, 1, 'lq', '::1', '/api/admin/operation-logs', 'GET', '', 200, 93, 0, 1768807958); -INSERT INTO `operation_logs` VALUES (681, 1, 'lq', '::1', '/api/admin/dashboard/stats', 'GET', '', 200, 309, 0, 1768807958); -INSERT INTO `operation_logs` VALUES (682, 1, 'lq', '::1', '/api/admin/attachment-categories', 'GET', '', 200, 45, 0, 1768807962); -INSERT INTO `operation_logs` VALUES (683, 1, 'lq', '::1', '/api/admin/attachments', 'GET', '', 200, 95, 0, 1768807962); -INSERT INTO `operation_logs` VALUES (684, 1, 'lq', '::1', '/api/admin/attachment-categories', 'GET', '', 200, 46, 0, 1768807968); -INSERT INTO `operation_logs` VALUES (685, 1, 'lq', '::1', '/api/admin/attachments', 'GET', '', 200, 95, 0, 1768807968); -INSERT INTO `operation_logs` VALUES (686, 1, 'lq', '::1', '/api/admin/attachment-categories', 'GET', '', 200, 47, 0, 1768807994); -INSERT INTO `operation_logs` VALUES (687, 1, 'lq', '::1', '/api/admin/attachments', 'GET', '', 200, 94, 0, 1768807994); -INSERT INTO `operation_logs` VALUES (688, 1, 'lq', '::1', '/api/admin/attachment-categories', 'GET', '', 200, 47, 0, 1768808006); -INSERT INTO `operation_logs` VALUES (689, 1, 'lq', '::1', '/api/admin/attachments', 'GET', '', 200, 96, 0, 1768808006); -INSERT INTO `operation_logs` VALUES (690, 1, 'lq', '::1', '/api/admin/attachment-categories', 'GET', '', 200, 46, 0, 1768808082); -INSERT INTO `operation_logs` VALUES (691, 1, 'lq', '::1', '/api/admin/attachments', 'GET', '', 200, 95, 0, 1768808082); -INSERT INTO `operation_logs` VALUES (692, 1, 'lq', '::1', '/api/admin/attachments', 'GET', '', 200, 51, 0, 1768808127); -INSERT INTO `operation_logs` VALUES (693, 1, 'lq', '::1', '/api/admin/attachment-categories', 'GET', '', 200, 50, 0, 1768808127); -INSERT INTO `operation_logs` VALUES (694, 1, 'lq', '::1', '/api/admin/attachment-categories', 'GET', '', 200, 44, 0, 1768808155); -INSERT INTO `operation_logs` VALUES (695, 1, 'lq', '::1', '/api/admin/attachments', 'GET', '', 200, 94, 0, 1768808155); -INSERT INTO `operation_logs` VALUES (696, 1, 'lq', '::1', '/api/admin/settings', 'GET', '', 200, 47, 0, 1768808167); -INSERT INTO `operation_logs` VALUES (697, 1, 'lq', '::1', '/api/admin/oss-configs', 'GET', '', 200, 49, 0, 1768808167); -INSERT INTO `operation_logs` VALUES (698, 1, 'lq', '::1', '/api/admin/oss-configs', 'GET', '', 200, 49, 0, 1768808492); -INSERT INTO `operation_logs` VALUES (699, 1, 'lq', '::1', '/api/admin/settings', 'GET', '', 200, 49, 0, 1768808492); -INSERT INTO `operation_logs` VALUES (700, 1, 'lq', '::1', '/api/admin/oss-configs', 'POST', '{\"name\":\"默认-本地\",\"storageType\":\"local\",\"accessKey\":\"1\",\"secretKey\":\"1\",\"bucket\":\"/uploads/\",\"region\":\"locallhost\",\"domain\":\"http://locallhost:3001\",\"isActive\":1,\"createdAt\":\"\",\"updatedAt\":\"\"}', 500, 1, 0, 1768808546); -INSERT INTO `operation_logs` VALUES (701, 1, 'lq', '::1', '/api/admin/oss-configs', 'POST', '{\"name\":\"默认-本地\",\"storageType\":\"local\",\"accessKey\":\"1\",\"secretKey\":\"1\",\"bucket\":\"/uploads/\",\"region\":\"locallhost\",\"domain\":\"http://locallhost:3001\",\"isActive\":1,\"createdAt\":\"\",\"updatedAt\":\"\"}', 500, 1, 0, 1768808550); -INSERT INTO `operation_logs` VALUES (702, 1, 'lq', '::1', '/api/admin/oss-configs', 'POST', '{\"name\":\"默认-本地\",\"storageType\":\"local\",\"accessKey\":\"123123aaa\",\"secretKey\":\"123123aaa\",\"bucket\":\"/uploads/\",\"region\":\"locallhost\",\"domain\":\"http://locallhost:3001\",\"isActive\":1,\"createdAt\":\"\",\"updatedAt\":\"\"}', 500, 1, 0, 1768808572); -INSERT INTO `operation_logs` VALUES (703, 1, 'lq', '::1', '/api/admin/columns', 'GET', '', 200, 50, 0, 1768809000); -INSERT INTO `operation_logs` VALUES (704, 1, 'lq', '::1', '/api/admin/categories', 'GET', '', 200, 48, 0, 1768809017); -INSERT INTO `operation_logs` VALUES (705, 1, 'lq', '::1', '/api/admin/posts', 'GET', '', 200, 230, 0, 1768809020); -INSERT INTO `operation_logs` VALUES (706, 1, 'lq', '::1', '/api/admin/posts/6', 'PUT', '{\"title\":\"Goravel 入门指南 (三):ORM 数据库操作\",\"categoryId\":4,\"date\":\"2026-01-15\",\"excerpt\":\"掌握 Goravel 强大的 ORM 功能,从数据库配置、模型定义到执行增删改查(CRUD)操作。\",\"content\":\"\",\"isPublished\":1,\"tags\":[{\"id\":1}],\"columnId\":1}', 200, 509, 0, 1768809133); -INSERT INTO `operation_logs` VALUES (707, 1, 'lq', '::1', '/api/admin/posts', 'GET', '', 200, 265, 0, 1768809134); -INSERT INTO `operation_logs` VALUES (708, 1, 'lq', '::1', '/api/admin/posts', 'GET', '', 200, 266, 0, 1768809309); -INSERT INTO `operation_logs` VALUES (709, 1, 'lq', '::1', '/api/admin/tags', 'POST', '{\"name\":\"入门教程\",\"slug\":\"入门教程\"}', 200, 98, 0, 1768809322); -INSERT INTO `operation_logs` VALUES (710, 1, 'lq', '::1', '/api/admin/tags', 'POST', '{\"name\":\"Golang框架\",\"slug\":\"Golang框架\"}', 200, 89, 0, 1768809341); -INSERT INTO `operation_logs` VALUES (711, 1, 'lq', '::1', '/api/admin/tags', 'POST', '{\"name\":\"前后端分离\",\"slug\":\"前后端分离\"}', 200, 91, 0, 1768809349); -INSERT INTO `operation_logs` VALUES (712, 1, 'lq', '::1', '/api/admin/posts/6', 'PUT', '{\"title\":\"Goravel 入门指南 (三):ORM 数据库操作\",\"categoryId\":4,\"date\":\"2026-01-15\",\"excerpt\":\"掌握 Goravel 强大的 ORM 功能,从数据库配置、模型定义到执行增删改查(CRUD)操作。\",\"content\":\"\",\"isPublished\":1,\"tags\":[{\"id\":1},{\"id\":2},{\"id\":3},{\"id\":4}],\"columnId\":1}', 200, 521, 0, 1768809352); -INSERT INTO `operation_logs` VALUES (713, 1, 'lq', '::1', '/api/admin/posts', 'GET', '', 200, 289, 0, 1768809352); -INSERT INTO `operation_logs` VALUES (714, 1, 'lq', '::1', '/api/admin/categories', 'GET', '', 200, 46, 0, 1768809361); -INSERT INTO `operation_logs` VALUES (715, 1, 'lq', '::1', '/api/admin/columns', 'GET', '', 200, 49, 0, 1768809361); -INSERT INTO `operation_logs` VALUES (716, 1, 'lq', '::1', '/api/admin/posts', 'GET', '', 200, 302, 0, 1768810118); -INSERT INTO `operation_logs` VALUES (717, 1, 'lq', '::1', '/api/admin/posts', 'GET', '', 200, 286, 0, 1768810284); -INSERT INTO `operation_logs` VALUES (718, 1, 'lq', '::1', '/api/admin/attachment-categories', 'GET', '', 200, 45, 0, 1768810405); -INSERT INTO `operation_logs` VALUES (719, 1, 'lq', '::1', '/api/admin/attachments', 'GET', '', 200, 89, 0, 1768810405); - --- ---------------------------- --- Table structure for oss_configs --- ---------------------------- -DROP TABLE IF EXISTS `oss_configs`; -CREATE TABLE `oss_configs` ( - `id` int UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', - `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '配置名称', - `storage_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '存储类型: local/qcloud/aliyun/qiniu', - `access_key` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'Access Key (AES加密)', - `secret_key` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'Secret Key (AES加密)', - `bucket` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '存储桶名称', - `region` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '区域', - `domain` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '访问域名', - `is_active` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否启用', - `deleted_at` bigint NOT NULL DEFAULT 0, - `created_at` bigint NOT NULL DEFAULT 0, - `updated_at` bigint NOT NULL DEFAULT 0, - PRIMARY KEY (`id`) USING BTREE, - INDEX `idx_storage_type`(`storage_type` ASC) USING BTREE, - INDEX `idx_is_active`(`is_active` ASC) USING BTREE -) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'OSS配置表' ROW_FORMAT = DYNAMIC; - --- ---------------------------- --- Records of oss_configs --- ---------------------------- - --- ---------------------------- --- Table structure for partners --- ---------------------------- -DROP TABLE IF EXISTS `partners`; -CREATE TABLE `partners` ( - `id` int UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', - `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '合作伙伴名称', - `logo` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '合作伙伴Logo URL', - `description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '合作伙伴介绍', - `url` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '合作伙伴官网链接', - `sort_order` int UNSIGNED NULL DEFAULT 0 COMMENT '排序权重', - `deleted_at` bigint NOT NULL DEFAULT 0, - `created_at` bigint NOT NULL DEFAULT 0, - `updated_at` bigint NOT NULL DEFAULT 0, - PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '合作伙伴表' ROW_FORMAT = DYNAMIC; - --- ---------------------------- --- Records of partners --- ---------------------------- -INSERT INTO `partners` VALUES (1, 'Vercel', 'https://upload.wikimedia.org/wikipedia/commons/5/5e/Vercel_logo_black.svg', '前端部署平台', 'https://vercel.com', 0, 0, 1768482993, 1768538953); -INSERT INTO `partners` VALUES (2, 'Supabase', 'https://seeklogo.com/images/S/supabase-logo-DCC676FFE2-seeklogo.com.png', '开源 Firebase 替代方案', 'https://supabase.com', 0, 0, 1768482993, 1768538953); -INSERT INTO `partners` VALUES (3, 'Stripe', 'https://upload.wikimedia.org/wikipedia/commons/b/ba/Stripe_Logo%2C_revised_2016.svg', '在线支付基础设施', 'https://stripe.com', 0, 0, 1768482993, 1768538953); -INSERT INTO `partners` VALUES (4, 'Algolia', 'https://upload.wikimedia.org/wikipedia/commons/6/69/Algolia-logo.svg', '搜索即服务 API', 'https://algolia.com', 0, 0, 1768482993, 1768538953); -INSERT INTO `partners` VALUES (5, 'Prisma', 'https://seeklogo.com/images/P/prisma-logo-3805665B69-seeklogo.com.png', '下一代 ORM', 'https://prisma.io', 0, 0, 1768482993, 1768538953); - --- ---------------------------- --- Table structure for permissions --- ---------------------------- -DROP TABLE IF EXISTS `permissions`; -CREATE TABLE `permissions` ( - `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', - `name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '权限名称', - `resource` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '资源名称', - `action` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '操作名称', - `deleted_at` bigint NOT NULL DEFAULT 0, - `created_at` bigint NOT NULL DEFAULT 0, - `updated_at` bigint NOT NULL DEFAULT 0, - PRIMARY KEY (`id`) USING BTREE, - UNIQUE INDEX `unique_resource_action`(`resource` ASC, `action` ASC) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 31 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '权限表' ROW_FORMAT = DYNAMIC; - --- ---------------------------- --- Records of permissions --- ---------------------------- -INSERT INTO `permissions` VALUES (1, 'Create User', 'users', 'create', 0, 1768452892, 1768538956); -INSERT INTO `permissions` VALUES (2, 'Read User', 'users', 'read', 0, 1768452892, 1768538956); -INSERT INTO `permissions` VALUES (3, 'Update User', 'users', 'update', 0, 1768452892, 1768538956); -INSERT INTO `permissions` VALUES (4, 'Delete User', 'users', 'delete', 0, 1768452892, 1768538956); -INSERT INTO `permissions` VALUES (5, 'Create Role', 'roles', 'create', 0, 1768452892, 1768538956); -INSERT INTO `permissions` VALUES (6, 'Read Role', 'roles', 'read', 0, 1768452892, 1768538956); -INSERT INTO `permissions` VALUES (7, 'Update Role', 'roles', 'update', 0, 1768452892, 1768538956); -INSERT INTO `permissions` VALUES (8, 'Delete Role', 'roles', 'delete', 0, 1768452892, 1768538956); -INSERT INTO `permissions` VALUES (9, 'Create Post', 'posts', 'create', 0, 1768452892, 1768538956); -INSERT INTO `permissions` VALUES (10, 'Read Post', 'posts', 'read', 0, 1768452892, 1768538956); -INSERT INTO `permissions` VALUES (11, 'Update Post', 'posts', 'update', 0, 1768452892, 1768538956); -INSERT INTO `permissions` VALUES (12, 'Delete Post', 'posts', 'delete', 0, 1768452892, 1768538956); -INSERT INTO `permissions` VALUES (13, 'Create Work', 'works', 'create', 0, 1768452892, 1768538956); -INSERT INTO `permissions` VALUES (14, 'Read Work', 'works', 'read', 0, 1768452892, 1768538956); -INSERT INTO `permissions` VALUES (15, 'Update Work', 'works', 'update', 0, 1768452892, 1768538956); -INSERT INTO `permissions` VALUES (16, 'Delete Work', 'works', 'delete', 0, 1768452892, 1768538956); -INSERT INTO `permissions` VALUES (17, 'Create Snippet', 'snippets', 'create', 0, 1768452892, 1768538956); -INSERT INTO `permissions` VALUES (18, 'Read Snippet', 'snippets', 'read', 0, 1768452892, 1768538956); -INSERT INTO `permissions` VALUES (19, 'Update Snippet', 'snippets', 'update', 0, 1768452892, 1768538956); -INSERT INTO `permissions` VALUES (20, 'Delete Snippet', 'snippets', 'delete', 0, 1768452892, 1768538956); -INSERT INTO `permissions` VALUES (21, 'Create Setting', 'settings', 'create', 0, 1768452892, 1768538956); -INSERT INTO `permissions` VALUES (22, 'Read Setting', 'settings', 'read', 0, 1768452892, 1768538956); -INSERT INTO `permissions` VALUES (23, 'Update Setting', 'settings', 'update', 0, 1768452892, 1768538956); -INSERT INTO `permissions` VALUES (24, 'Delete Setting', 'settings', 'delete', 0, 1768452892, 1768538956); -INSERT INTO `permissions` VALUES (25, 'Create Tag', 'tags', 'create', 0, 1768452892, 1768538956); -INSERT INTO `permissions` VALUES (26, 'Read Tag', 'tags', 'read', 0, 1768452892, 1768538956); -INSERT INTO `permissions` VALUES (27, 'Update Tag', 'tags', 'update', 0, 1768452892, 1768538956); -INSERT INTO `permissions` VALUES (28, 'Delete Tag', 'tags', 'delete', 0, 1768452892, 1768538956); -INSERT INTO `permissions` VALUES (29, 'Read Operation Log', 'operation_logs', 'read', 0, 1768452892, 1768538956); -INSERT INTO `permissions` VALUES (30, 'Read Dashboard', 'dashboard', 'read', 0, 1768452892, 1768538956); - --- ---------------------------- --- Table structure for post_history --- ---------------------------- -DROP TABLE IF EXISTS `post_history`; -CREATE TABLE `post_history` ( - `id` int UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '历史记录ID', - `post_id` int UNSIGNED NOT NULL COMMENT '文章ID', - `version` int UNSIGNED NOT NULL DEFAULT 1 COMMENT '版本号', - `title` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '文章标题', - `category_id` int UNSIGNED NOT NULL DEFAULT 0 COMMENT '文章分类ID', - `excerpt` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '文章摘要', - `content` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '文章内容', - `is_published` tinyint(1) NULL DEFAULT 1 COMMENT '是否已发布', - `modified_by` int UNSIGNED NOT NULL DEFAULT 0 COMMENT '修改人ID', - `modified_at` bigint NOT NULL DEFAULT 0 COMMENT '修改时间', - `created_at` bigint NOT NULL DEFAULT 0, - `deleted_at` bigint NOT NULL DEFAULT 0, - PRIMARY KEY (`id`) USING BTREE, - INDEX `idx_post_id`(`post_id` ASC) USING BTREE, - INDEX `idx_version`(`version` ASC) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '文章历史记录表' ROW_FORMAT = DYNAMIC; - --- ---------------------------- --- Records of post_history --- ---------------------------- -INSERT INTO `post_history` VALUES (1, 6, 1, 'Goravel 入门指南 (三):ORM 数据库操作', 4, '掌握 Goravel 强大的 ORM 功能,从数据库配置、模型定义到执行增删改查(CRUD)操作。', '', 1, 1, 1768809133, 1768809133, 0); -INSERT INTO `post_history` VALUES (2, 6, 2, 'Goravel 入门指南 (三):ORM 数据库操作', 4, '掌握 Goravel 强大的 ORM 功能,从数据库配置、模型定义到执行增删改查(CRUD)操作。', '', 1, 1, 1768809352, 1768809352, 0); - --- ---------------------------- --- Table structure for post_tags --- ---------------------------- -DROP TABLE IF EXISTS `post_tags`; -CREATE TABLE `post_tags` ( - `tag_id` bigint UNSIGNED NOT NULL COMMENT '关联的标签ID', - `post_id` int UNSIGNED NOT NULL COMMENT '关联的文章ID', - `created_at` bigint NOT NULL DEFAULT 0, - PRIMARY KEY (`post_id`, `tag_id`) USING BTREE, - INDEX `idx_tag_id`(`tag_id` ASC) USING BTREE -) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '文章标签关联表' ROW_FORMAT = DYNAMIC; - --- ---------------------------- --- Records of post_tags --- ---------------------------- -INSERT INTO `post_tags` VALUES (1, 6, 0); -INSERT INTO `post_tags` VALUES (2, 6, 0); -INSERT INTO `post_tags` VALUES (3, 6, 0); -INSERT INTO `post_tags` VALUES (4, 6, 0); - --- ---------------------------- --- Table structure for posts --- ---------------------------- -DROP TABLE IF EXISTS `posts`; -CREATE TABLE `posts` ( - `id` int UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '文章唯一标识(自增ID)', - `original_id` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '原始字符串ID备份', - `title` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '文章标题', - `category_id` int UNSIGNED NOT NULL DEFAULT 0 COMMENT '文章分类ID', - `column_id` int UNSIGNED NULL DEFAULT NULL COMMENT '专栏ID(可选)', - `excerpt` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '文章摘要', - `content` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '文章内容', - `read_count` int UNSIGNED NULL DEFAULT 0 COMMENT '阅读量', - `is_published` tinyint(1) NULL DEFAULT 1 COMMENT '是否已发布(0:草稿,1:已发布)', - `deleted_at` bigint NOT NULL DEFAULT 0, - `created_at` bigint NOT NULL DEFAULT 0, - `updated_at` bigint NOT NULL DEFAULT 0, - PRIMARY KEY (`id`) USING BTREE, - INDEX `idx_category_id`(`category_id` ASC) USING BTREE COMMENT '按分类查询索引', - INDEX `idx_column_id`(`column_id` ASC) USING BTREE COMMENT '按专栏查询索引', - INDEX `idx_is_published`(`is_published` ASC) USING BTREE COMMENT '按发布状态查询索引', - INDEX `idx_original_id`(`original_id` ASC) USING BTREE, - FULLTEXT INDEX `idx_title_content`(`title`, `content`) COMMENT '标题和内容全文索引,用于搜索' -) ENGINE = InnoDB AUTO_INCREMENT = 8 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '博客文章表(新结构)' ROW_FORMAT = DYNAMIC; - --- ---------------------------- --- Records of posts --- ---------------------------- -INSERT INTO `posts` VALUES (1, 'refactor', '重构的艺术:如何优雅地处理遗留代码', 4, NULL, '在本文中,我们将探讨重构的核心原则和实用技巧,帮助你优雅地处理遗留代码,提高代码质量和可维护性。', '

什么是代码重构?

代码重构是在不改变代码外部行为的前提下,优化代码内部结构的过程...

', 5, 1, 0, 1768291814, 1768538949); -INSERT INTO `posts` VALUES (2, 'shader', '着色器魔法:从零开始写一个噪声生成器', 2, NULL, '深入了解WebGL着色器,学习如何从零开始实现一个高性能的噪声生成器,为你的3D作品增添独特的视觉效果。', '

GLSL (OpenGL Shading Language) 是一门让人生畏但也充满魅力的语言。它运行在 GPU 上,能够并行处理数百万个像素,创造出惊人的视觉效果。

\r\n

什么是柏林噪声?

\r\n

柏林噪声(Perlin Noise)是一种梯度噪声,它比普通的随机数生成的噪声看起来更自然、更平滑。它常被用来模拟云彩、地形、火焰等自然现象。

\r\n

Three.js 中的实现

\r\n

在 Three.js 中,我们可以通过 ShaderMaterial 直接编写 GLSL 代码。

\r\n
// 简单的顶点着色器\r\nvarying vec2 vUv;\r\nvoid main() {\r\n    vUv = uv;\r\n    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\r\n}
\r\n

通过调整噪声的频率和振幅,我们可以得到各种不同的纹理效果。在我的个人网站背景中,就使用了这种技术来生成流动的极光效果。

\r\n ', 1, 1, 0, 1768291815, 1768538949); -INSERT INTO `posts` VALUES (3, 'ux', '用户体验设计:从认知心理学到交互实践', 3, NULL, '探索用户体验设计的核心原理,结合认知心理学知识,学习如何设计出真正符合用户需求的交互界面。', '

认知心理学在UX设计中的应用

了解用户的认知过程是设计良好用户体验的基础...

', 0, 1, 0, 1768291816, 1768538949); -INSERT INTO `posts` VALUES (4, 'goravel-guide-1', 'Goravel 入门指南 (一):环境搭建与 Hello World', 4, 1, '本文将带你了解 Go 语言领域的 Laravel —— Goravel 框架,并演示如何快速搭建环境及运行第一个 Web 服务。', '## 什么是 Goravel?\n\nGoravel 是一个基于 Go 语言的 Web 开发框架,它的设计理念深受 PHP Laravel 框架的启发。如果你是一名从 PHP 转 Go 的开发者,或者你喜欢 Laravel 那种“开箱即用、优雅简洁”的开发体验,那么 Goravel 绝对是你的不二之选。\n\n它集成了丰富的功能模块,包括但不限于:\n- 强大的路由系统\n- ORM(基于 GORM 封装)\n- 依赖注入容器\n- 队列与任务调度\n- 缓存与文件存储\n\n## 环境搭建\n\nGoravel 提供了一个名为 `knit` 的命令行工具(类似 Laravel 的 artisan),可以帮助我们快速初始化项目。\n\n### 1. 安装 Knit CLI\n\n确保你已经安装了 Go (1.20+),然后运行以下命令:\n\n```bash\ngo install github.com/goravel/knit/cmd/knit@latest\n```\n\n### 2. 创建新项目\n\n使用 `knit new` 命令创建项目:\n\n```bash\nknit new my-goravel-app\ncd my-goravel-app\n```\n\n### 3. 安装依赖\n\n```bash\ngo mod tidy\n```\n\n## 目录结构\n\n打开项目,你会发现它的目录结构非常清晰,带有浓厚的 Laravel 风格:\n\n- **app/**: 核心业务代码(Http 控制器、模型、服务提供者等)\n- **config/**: 配置文件(应用配置、数据库配置等)\n- **routes/**: 路由定义文件\n- **database/**: 数据库迁移与填充\n- **public/**: 静态资源文件\n\n## 运行 Hello World\n\nGoravel 的入口文件是根目录下的 `main.go`。在运行之前,我们先看一眼路由定义。打开 `routes/web.go`:\n\n```go\npackage routes\n\nimport (\n \"github.com/goravel/framework/contracts/http\"\n \"github.com/goravel/framework/facades\"\n)\n\nfunc Web() {\n facades.Route().Get(\"/\", func(ctx http.Context) http.Response {\n return ctx.Response().Json(200, http.Json{\n \"Hello\": \"Goravel\",\n })\n })\n}\n```\n\n非常直观!现在让我们启动服务:\n\n```bash\ngo run .\n```\n\n默认情况下,服务会运行在 `http://localhost:3000`。打开浏览器访问,你应该能看到 JSON 响应:\n\n```json\n{\n \"Hello\": \"Goravel\"\n}\n```\n\n至此,你已经成功运行了你的第一个 Goravel 应用!在下一篇文章中,我们将深入探讨路由与控制器的使用。', 1210, 1, 0, 1768465932, 1768538949); -INSERT INTO `posts` VALUES (5, 'goravel-guide-2', 'Goravel 入门指南 (二):路由与控制器', 4, 1, '深入理解 Goravel 的 HTTP 层,学习如何定义 RESTful 路由、创建控制器以及处理 HTTP 请求与响应。', '## 路由系统\n\n在 Goravel 中,路由定义通常位于 `routes/` 目录下。`api.go` 用于定义 API 路由,`web.go` 用于定义网页路由。Goravel 使用 `facades.Route()` 来定义路由,这得益于其强大的依赖注入系统。\n\n### 基础路由\n\n```go\n// GET 请求\nfacades.Route().Get(\"/users\", func(ctx http.Context) http.Response {\n return ctx.Response().String(200, \"User List\")\n})\n\n// POST 请求\nfacades.Route().Post(\"/users\", func(ctx http.Context) http.Response {\n return ctx.Response().Success().Json(http.Json{\"id\": 1})\n})\n```\n\n### 路由参数\n\n获取 URL 中的动态参数非常简单:\n\n```go\nfacades.Route().Get(\"/users/{id}\", func(ctx http.Context) http.Response {\n id := ctx.Request().Input(\"id\")\n return ctx.Response().Success().Json(http.Json{\"user_id\": id})\n})\n```\n\n## 控制器 (Controllers)\n\n随着应用变大,我们不可能把所有逻辑都写在路由闭包里。这时候就需要控制器了。\n\n### 创建控制器\n\n使用 `knit` 工具可以快速生成控制器:\n\n```bash\nknit make:controller UserController\n```\n\n这会在 `app/http/controllers` 目录下生成 `user_controller.go`。让我们修改它来添加一个 `Show` 方法:\n\n```go\npackage controllers\n\nimport (\n \"github.com/goravel/framework/contracts/http\"\n)\n\ntype UserController struct {\n // 可以在这里注入服务\n}\n\nfunc NewUserController() *UserController {\n return &UserController{}\n}\n\nfunc (r *UserController) Show(ctx http.Context) http.Response {\n id := ctx.Request().Input(\"id\")\n // 模拟数据库查询\n return ctx.Response().Success().Json(http.Json{\n \"id\": id,\n \"name\": \"Goravel User\",\n })\n}\n```\n\n### 注册控制器路由\n\n回到 `routes/api.go`,我们需要先实例化控制器,然后绑定路由:\n\n```go\nimport \"my-goravel-app/app/http/controllers\"\n\nfunc Api() {\n userController := controllers.NewUserController()\n \n // 绑定到控制器方法\n facades.Route().Get(\"/users/{id}\", userController.Show)\n}\n```\n\n## 请求与响应\n\n在控制器方法中,`ctx` (http.Context) 是核心:\n\n- **获取输入**: `ctx.Request().Input(\"key\")`\n- **获取 JSON**: `ctx.Request().Bind(&user)`\n- **返回 JSON**: `ctx.Response().Json(200, data)`\n- **设置状态码**: `ctx.Response().Status(404)`\n\n通过这种方式,Goravel 让 HTTP 层的处理变得异常清晰和标准化。下一章,我们将学习如何通过 ORM 操作数据库。', 903, 1, 0, 1768465933, 1768538949); -INSERT INTO `posts` VALUES (6, 'goravel-guide-3', 'Goravel 入门指南 (三):ORM 数据库操作', 4, 1, '掌握 Goravel 强大的 ORM 功能,从数据库配置、模型定义到执行增删改查(CRUD)操作。', '## Goravel ORM 简介\r\nGoravel 的 ORM 基于著名的 GORM 库构建,但它进行了一层优雅的封装,使其调用方式更接近 Laravel 的 Eloquent ORM。这意味着你可以使用 Facade 模式轻松调用数据库,且支持自动迁移。\r\n## 配置数据库\r\n打开 `config/database.go` 或 `.env` 文件,配置你的 MySQL 连接信息:\r\n```env\r\nDB_CONNECTION=mysql\r\nDB_HOST=127.0.0.1\r\nDB_PORT=3306\r\nDB_DATABASE=goravel\r\nDB_USERNAME=root\r\nDB_PASSWORD=password\r\n```\r\n## 定义模型\r\n使用 `knit` 生成模型:\r\n```bash\r\nknit make:model Post\r\n```\r\n生成的模型文件位于 `app/models/post.go`。我们需要定义结构体字段:\r\n```go\r\npackage models\r\nimport (\r\n \\\"github.com/goravel/framework/database/orm\\\"\r\n )\r\ntype Post struct {\r\n orm.Model\r\n Title string `gorm:\\\"size:255;not null\\\"`\r\n Content string `gorm:\\\"type:text\\\"`\r\n UserID uint\r\n }\r\n ```\r\n## 数据库迁移\r\n虽然 GORM 支持 AutoMigrate,但 Goravel 推荐使用迁移文件来管理数据库变更。\r\n```bash\r\nknit make:migration create_posts_table\r\n```\r\n编辑生成的迁移文件 (`database/migrations/xxxx_create_posts_table.go`),定义表结构,然后运行:\r\n```bash\r\nknit migrate\r\n```\r\n## CRUD 操作\r\n有了模型,我们就可以愉快地操作数据库了。所有操作都通过 `facades.Orm()` 入口。\r\n### 创建 (Create)\r\n```go\r\npost := models.Post{\r\n Title: \\\"My First Post\\\",\r\n Content: \\\"Content goes here...\\\",\r\n }\r\n err := facades.Orm().Query().Create(&post)\r\n ```\r\n### 查询 (Read)\r\n```go\r\nvar post models.Post\r\n// 根据主键查询\r\nfacades.Orm().Query().Find(&post, 1)\r\n// 条件查询\r\nvar posts []models.Post\r\nfacades.Orm().Query().Where(\\\"title\\\", \\\"My First Post\\\").Get(&posts)\r\n```\r\n### 更新 (Update)\r\n```go\r\nvar post models.Post\r\nfacades.Orm().Query().Find(&post, 1)\r\npost.Title = \\\"Updated Title\\\"\r\nfacades.Orm().Query().Save(&post)\r\n```\r\n### 删除 (Delete)\r\n```go\r\nvar post models.Post\r\nfacades.Orm().Query().Delete(&post, 1)\r\n```\r\nGoravel 的 ORM 让 Go 语言的数据库操作不再繁琐,极大地提高了开发效率。配合之前的路由和控制器知识,你现在已经具备了开发完整 RESTful API 的能力!', 1586, 1, 0, 1768465934, 1768809351); - --- ---------------------------- --- Table structure for role_permissions --- ---------------------------- -DROP TABLE IF EXISTS `role_permissions`; -CREATE TABLE `role_permissions` ( - `role_id` bigint UNSIGNED NOT NULL COMMENT '角色ID', - `permission_id` bigint UNSIGNED NOT NULL COMMENT '权限ID', - PRIMARY KEY (`role_id`, `permission_id`) USING BTREE, - INDEX `role_permissions_ibfk_2`(`permission_id` ASC) USING BTREE -) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '角色权限关联表' ROW_FORMAT = DYNAMIC; - --- ---------------------------- --- Records of role_permissions --- ---------------------------- -INSERT INTO `role_permissions` VALUES (1, 1); -INSERT INTO `role_permissions` VALUES (1, 2); -INSERT INTO `role_permissions` VALUES (3, 2); -INSERT INTO `role_permissions` VALUES (1, 3); -INSERT INTO `role_permissions` VALUES (1, 4); -INSERT INTO `role_permissions` VALUES (1, 5); -INSERT INTO `role_permissions` VALUES (1, 6); -INSERT INTO `role_permissions` VALUES (3, 6); -INSERT INTO `role_permissions` VALUES (1, 7); -INSERT INTO `role_permissions` VALUES (1, 8); -INSERT INTO `role_permissions` VALUES (1, 9); -INSERT INTO `role_permissions` VALUES (2, 9); -INSERT INTO `role_permissions` VALUES (1, 10); -INSERT INTO `role_permissions` VALUES (2, 10); -INSERT INTO `role_permissions` VALUES (3, 10); -INSERT INTO `role_permissions` VALUES (1, 11); -INSERT INTO `role_permissions` VALUES (2, 11); -INSERT INTO `role_permissions` VALUES (1, 12); -INSERT INTO `role_permissions` VALUES (2, 12); -INSERT INTO `role_permissions` VALUES (1, 13); -INSERT INTO `role_permissions` VALUES (1, 14); -INSERT INTO `role_permissions` VALUES (3, 14); -INSERT INTO `role_permissions` VALUES (1, 15); -INSERT INTO `role_permissions` VALUES (1, 16); -INSERT INTO `role_permissions` VALUES (1, 17); -INSERT INTO `role_permissions` VALUES (1, 18); -INSERT INTO `role_permissions` VALUES (3, 18); -INSERT INTO `role_permissions` VALUES (1, 19); -INSERT INTO `role_permissions` VALUES (1, 20); -INSERT INTO `role_permissions` VALUES (1, 21); -INSERT INTO `role_permissions` VALUES (1, 22); -INSERT INTO `role_permissions` VALUES (3, 22); -INSERT INTO `role_permissions` VALUES (1, 23); -INSERT INTO `role_permissions` VALUES (1, 24); -INSERT INTO `role_permissions` VALUES (1, 25); -INSERT INTO `role_permissions` VALUES (1, 26); -INSERT INTO `role_permissions` VALUES (3, 26); -INSERT INTO `role_permissions` VALUES (1, 27); -INSERT INTO `role_permissions` VALUES (1, 28); -INSERT INTO `role_permissions` VALUES (1, 29); -INSERT INTO `role_permissions` VALUES (3, 29); -INSERT INTO `role_permissions` VALUES (1, 30); -INSERT INTO `role_permissions` VALUES (3, 30); - --- ---------------------------- --- Table structure for roles --- ---------------------------- -DROP TABLE IF EXISTS `roles`; -CREATE TABLE `roles` ( - `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', - `name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '角色名称', - `description` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '角色描述', - `deleted_at` bigint NOT NULL DEFAULT 0, - `created_at` bigint NOT NULL DEFAULT 0, - `updated_at` bigint NOT NULL DEFAULT 0, - PRIMARY KEY (`id`) USING BTREE, - UNIQUE INDEX `name`(`name` ASC) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '角色表' ROW_FORMAT = DYNAMIC; - --- ---------------------------- --- Records of roles --- ---------------------------- -INSERT INTO `roles` VALUES (1, 'admin', '系统管理员', 0, 1768452892, 1768538956); -INSERT INTO `roles` VALUES (2, 'editor', '内容编辑', 0, 1768452892, 1768538956); -INSERT INTO `roles` VALUES (3, 'viewer', '普通访客', 0, 1768452892, 1768538956); - --- ---------------------------- --- Table structure for search_logs --- ---------------------------- -DROP TABLE IF EXISTS `search_logs`; -CREATE TABLE `search_logs` ( - `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', - `keyword` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '搜索关键词', - `search_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '搜索类型: category/tag/column/keyword', - `user_ip` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '用户IP地址', - `user_location` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '用户归属地', - `deleted_at` bigint NOT NULL DEFAULT 0, - `created_at` bigint NOT NULL DEFAULT 0, - PRIMARY KEY (`id`) USING BTREE, - INDEX `idx_search_type`(`search_type` ASC) USING BTREE, - INDEX `idx_created_at`(`created_at` ASC) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '搜索记录表' ROW_FORMAT = DYNAMIC; - --- ---------------------------- --- Records of search_logs --- ---------------------------- -INSERT INTO `search_logs` VALUES (1, '4', 'category', '::1', 'Internal', 0, 1768808440); -INSERT INTO `search_logs` VALUES (2, '4', 'category', '::1', 'Internal', 0, 1768808446); -INSERT INTO `search_logs` VALUES (3, '啊', 'keyword', '::1', 'Internal', 0, 1768808446); -INSERT INTO `search_logs` VALUES (4, '4', 'category', '::1', 'Internal', 0, 1768808447); - --- ---------------------------- --- Table structure for settings --- ---------------------------- -DROP TABLE IF EXISTS `settings`; -CREATE TABLE `settings` ( - `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', - `key_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '配置项键名', - `value` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '配置项值', - `description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '配置项描述', - `deleted_at` bigint NOT NULL DEFAULT 0, - `created_at` bigint NOT NULL DEFAULT 0, - `updated_at` bigint NOT NULL DEFAULT 0, - PRIMARY KEY (`id`) USING BTREE, - UNIQUE INDEX `key_name`(`key_name` ASC) USING BTREE, - INDEX `idx_key_name`(`key_name` ASC) USING BTREE COMMENT '按键名查询索引' -) ENGINE = InnoDB AUTO_INCREMENT = 15 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '网站配置表' ROW_FORMAT = DYNAMIC; - --- ---------------------------- --- Records of settings --- ---------------------------- -INSERT INTO `settings` VALUES (1, 'site_title', '年糕博客', '网站标题', 0, 1768291814, 1768538952); -INSERT INTO `settings` VALUES (2, 'site_description', '分享前端技术、交互设计以及数字艺术的深度思考', '网站描述', 0, 1768291814, 1768538952); -INSERT INTO `settings` VALUES (3, 'site_author', '年糕崽崽', '网站作者', 0, 1768291814, 1768538952); -INSERT INTO `settings` VALUES (4, 'site_keywords', '前端, 设计, 技术博客', '网站关键词', 0, 1768291814, 1768538952); -INSERT INTO `settings` VALUES (5, 'posts_per_page', '10', '每页显示的文章数量', 0, 1768291814, 1768538952); -INSERT INTO `settings` VALUES (6, 'works_per_page', '6', '每页显示的作品数量', 0, 1768291814, 1768538952); -INSERT INTO `settings` VALUES (7, 'snippets_per_page', '8', '每页显示的代码片段数量', 0, 1768291814, 1768538952); - --- ---------------------------- --- Table structure for snippets --- ---------------------------- -DROP TABLE IF EXISTS `snippets`; -CREATE TABLE `snippets` ( - `id` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '代码片段唯一标识', - `title` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '代码片段标题', - `code` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '代码内容', - `type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '代码类型(如:javascript、css、html等)', - `description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '代码片段描述', - `view_count` int UNSIGNED NULL DEFAULT 0 COMMENT '查看次数', - `deleted_at` bigint NOT NULL DEFAULT 0, - `created_at` bigint NOT NULL DEFAULT 0, - `updated_at` bigint NOT NULL DEFAULT 0, - PRIMARY KEY (`id`) USING BTREE, - INDEX `idx_type`(`type` ASC) USING BTREE COMMENT '按代码类型查询索引', - INDEX `idx_view_count`(`view_count` ASC) USING BTREE COMMENT '按查看次数查询索引' -) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '代码片段表' ROW_FORMAT = DYNAMIC; - --- ---------------------------- --- Records of snippets --- ---------------------------- -INSERT INTO `snippets` VALUES ('1', 'React 鼠标追踪 Hook', 'import { useState, useEffect } from \'react\';\r\n\r\nexport const useMousePosition = () => {\r\n const [pos, setPos] = useState({ x: 0, y: 0 });\r\n useEffect(() => {\r\n const update = (e) => setPos({ x: e.clientX, y: e.clientY });\r\n window.addEventListener(\'mousemove\', update);\r\n return () => window.removeEventListener(\'mousemove\', update);\r\n }, []);\r\n return pos;\r\n};', 'mouse', '这是一个鼠标追踪', 2, 0, 1768350739, 1768538952); - --- ---------------------------- --- Table structure for tags --- ---------------------------- -DROP TABLE IF EXISTS `tags`; -CREATE TABLE `tags` ( - `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', - `name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '标签名称', - `slug` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '标签别名,用于URL', - `deleted_at` bigint NOT NULL DEFAULT 0, - `created_at` bigint NOT NULL DEFAULT 0, - `updated_at` bigint NOT NULL DEFAULT 0, - PRIMARY KEY (`id`) USING BTREE, - UNIQUE INDEX `name`(`name` ASC) USING BTREE, - UNIQUE INDEX `slug`(`slug` ASC) USING BTREE, - INDEX `idx_slug`(`slug` ASC) USING BTREE COMMENT '按别名查询索引' -) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '标签表' ROW_FORMAT = DYNAMIC; - --- ---------------------------- --- Records of tags --- ---------------------------- -INSERT INTO `tags` VALUES (1, 'Goravel', '', 0, 1768550858, 1768550858); -INSERT INTO `tags` VALUES (2, '入门教程', '入门教程', 0, 1768809322, 1768809322); -INSERT INTO `tags` VALUES (3, 'Golang框架', 'Golang框架', 0, 1768809341, 1768809341); -INSERT INTO `tags` VALUES (4, '前后端分离', '前后端分离', 0, 1768809349, 1768809349); - --- ---------------------------- --- Table structure for testimonials --- ---------------------------- -DROP TABLE IF EXISTS `testimonials`; -CREATE TABLE `testimonials` ( - `id` int UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', - `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '客户姓名', - `role` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '客户职位', - `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '评价内容', - `avatar` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '客户头像URL', - `rating` tinyint UNSIGNED NULL DEFAULT 5 COMMENT '评分(1-5)', - `sort_order` int UNSIGNED NULL DEFAULT 0 COMMENT '排序权重', - `deleted_at` bigint NOT NULL DEFAULT 0, - `created_at` bigint NOT NULL DEFAULT 0, - `updated_at` bigint NOT NULL DEFAULT 0, - PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 7 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '客户评价表' ROW_FORMAT = DYNAMIC; - --- ---------------------------- --- Records of testimonials --- ---------------------------- -INSERT INTO `testimonials` VALUES (1, 'Alex Chen', 'Product Owner @ TechFlow', '年糕不仅技术过硬,对设计细节的把控更是令人惊叹。', 'https://api.dicebear.com/7.x/avataaars/svg?seed=Alex', 5, 0, 0, 1768482993, 1768538954); -INSERT INTO `testimonials` VALUES (2, 'Sarah Wu', 'Design Director @ ArtSpace', '很少见到能把代码写得像诗一样的工程师,合作非常愉快!', 'https://api.dicebear.com/7.x/avataaars/svg?seed=Sarah', 5, 0, 0, 1768482993, 1768538954); -INSERT INTO `testimonials` VALUES (3, 'Mike Zhang', 'CTO @ FutureWave', '交付质量远超预期,特别是在性能优化方面做得非常出色。', 'https://api.dicebear.com/7.x/avataaars/svg?seed=Mike', 5, 0, 0, 1768482993, 1768538954); -INSERT INTO `testimonials` VALUES (4, 'Jessica Li', 'Founder @ ZenMode', '从交互动效到整体架构,都体现了极高的专业水准。', 'https://api.dicebear.com/7.x/avataaars/svg?seed=Jessica', 5, 0, 0, 1768482993, 1768538954); -INSERT INTO `testimonials` VALUES (5, 'David Wang', 'Tech Lead @ Innovate', '代码结构清晰,注释完善,后续维护非常轻松。', 'https://api.dicebear.com/7.x/avataaars/svg?seed=David', 5, 0, 0, 1768482993, 1768538954); -INSERT INTO `testimonials` VALUES (6, '', 'CTO', '服务很贴心,技术够硬', 'https://api.dicebear.com/7.x/avataaars/svg?seed=David', 5, 0, 0, 20260116130552, 20260116130552); - --- ---------------------------- --- Table structure for user_access_logs --- ---------------------------- -DROP TABLE IF EXISTS `user_access_logs`; -CREATE TABLE `user_access_logs` ( - `id` int NOT NULL AUTO_INCREMENT, - `user_id` int NULL DEFAULT 0 COMMENT '用户ID(未登录用户为0)', - `user_ip` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '用户IP地址', - `user_location` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '用户归属地', - `article_id` int NOT NULL COMMENT '访问的文章ID', - `deleted_at` bigint NOT NULL DEFAULT 0, - `access_time` bigint NOT NULL DEFAULT 0, - PRIMARY KEY (`id`) USING BTREE, - INDEX `idx_user_id`(`user_id` ASC) USING BTREE, - INDEX `idx_article_id`(`article_id` ASC) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 41 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '用户访问记录表' ROW_FORMAT = DYNAMIC; - --- ---------------------------- --- Records of user_access_logs --- ---------------------------- -INSERT INTO `user_access_logs` VALUES (13, 0, '::1', 'Unknown', 6, 0, 20260116160835); -INSERT INTO `user_access_logs` VALUES (14, 0, '::1', 'Unknown', 6, 0, 20260116165800); -INSERT INTO `user_access_logs` VALUES (15, 0, '::1', 'Unknown', 6, 0, 20260119133107); -INSERT INTO `user_access_logs` VALUES (16, 0, '::1', 'Unknown', 6, 0, 20260119133147); -INSERT INTO `user_access_logs` VALUES (17, 0, '::1', 'Unknown', 6, 0, 20260119133216); -INSERT INTO `user_access_logs` VALUES (18, 0, '::1', 'Internal', 6, 0, 1768801962); -INSERT INTO `user_access_logs` VALUES (19, 1, '::1', 'Internal', 0, 0, 1768807318); -INSERT INTO `user_access_logs` VALUES (20, 1, '::1', 'Internal', 0, 0, 1768807357); -INSERT INTO `user_access_logs` VALUES (21, 0, '::1', 'Internal', 6, 0, 1768807466); -INSERT INTO `user_access_logs` VALUES (22, 1, '::1', 'Internal', 0, 0, 1768807488); -INSERT INTO `user_access_logs` VALUES (23, 0, '::1', 'Internal', 6, 0, 1768807765); -INSERT INTO `user_access_logs` VALUES (24, 0, '::1', 'Internal', 6, 0, 1768807777); -INSERT INTO `user_access_logs` VALUES (25, 0, '::1', 'Internal', 6, 0, 1768807795); -INSERT INTO `user_access_logs` VALUES (26, 0, '::1', 'Internal', 6, 0, 1768809365); -INSERT INTO `user_access_logs` VALUES (27, 0, '::1', 'Internal', 6, 0, 1768809378); -INSERT INTO `user_access_logs` VALUES (28, 0, '::1', 'Internal', 6, 0, 1768809388); -INSERT INTO `user_access_logs` VALUES (29, 0, '::1', 'Internal', 6, 0, 1768809694); -INSERT INTO `user_access_logs` VALUES (30, 0, '::1', 'Internal', 6, 0, 1768809805); -INSERT INTO `user_access_logs` VALUES (31, 0, '::1', 'Internal', 5, 0, 1768809830); -INSERT INTO `user_access_logs` VALUES (32, 0, '::1', 'Internal', 6, 0, 1768809866); -INSERT INTO `user_access_logs` VALUES (33, 0, '::1', 'Internal', 6, 0, 1768809889); -INSERT INTO `user_access_logs` VALUES (34, 0, '::1', 'Internal', 6, 0, 1768809926); -INSERT INTO `user_access_logs` VALUES (35, 0, '::1', 'Internal', 5, 0, 1768809977); -INSERT INTO `user_access_logs` VALUES (36, 0, '::1', 'Internal', 6, 0, 1768810062); -INSERT INTO `user_access_logs` VALUES (37, 0, '::1', 'Internal', 6, 0, 1768810115); -INSERT INTO `user_access_logs` VALUES (38, 0, '::1', 'Internal', 6, 0, 1768810120); -INSERT INTO `user_access_logs` VALUES (39, 0, '::1', 'Internal', 6, 0, 1768810276); -INSERT INTO `user_access_logs` VALUES (40, 0, '::1', 'Internal', 6, 0, 1768810279); - --- ---------------------------- --- Table structure for users --- ---------------------------- -DROP TABLE IF EXISTS `users`; -CREATE TABLE `users` ( - `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', - `username` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '用户名', - `email` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '邮箱地址', - `password_hash` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '密码哈希值', - `role_id` bigint UNSIGNED NULL DEFAULT NULL COMMENT '角色ID', - `role` enum('admin','editor','viewer') CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT 'viewer' COMMENT '用户角色(兼容旧版)', - `is_active` tinyint(1) NULL DEFAULT 1 COMMENT '是否激活(0:禁用,1:激活)', - `deleted_at` bigint NOT NULL DEFAULT 0, - `created_at` bigint NOT NULL DEFAULT 0, - `updated_at` bigint NOT NULL DEFAULT 0, - PRIMARY KEY (`id`) USING BTREE, - UNIQUE INDEX `username`(`username` ASC) USING BTREE, - UNIQUE INDEX `email`(`email` ASC) USING BTREE, - INDEX `idx_username`(`username` ASC) USING BTREE COMMENT '按用户名查询索引', - INDEX `idx_email`(`email` ASC) USING BTREE COMMENT '按邮箱查询索引', - INDEX `idx_role`(`role` ASC) USING BTREE COMMENT '按角色查询索引', - INDEX `users_ibfk_1`(`role_id` ASC) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户表' ROW_FORMAT = DYNAMIC; - --- ---------------------------- --- Records of users --- ---------------------------- -INSERT INTO `users` VALUES (1, 'lq', 'liqiworker@gmail.com', '$2a$10$Bq6rv7714W3jGyXruYx4puXjflMTSkq2QM9kF54x9iZnk.0AD4B1G', 1, 'admin', 1, 0, 1768291814, 1768538951); -INSERT INTO `users` VALUES (2, 'editor', 'editor@example.com', '$2a$14$OqZ7svgz3S68yuuXTJWz1.0CuqI2i3WvwC1jIGBy4l0mcJW9Kqvy2', 2, 'editor', 1, 0, 1768291814, 1768538951); -INSERT INTO `users` VALUES (5, 'cs', 'cs@nailaoyun.cn', '$2a$14$OqZ7svgz3S68yuuXTJWz1.0CuqI2i3WvwC1jIGBy4l0mcJW9Kqvy2', 3, 'viewer', 1, 0, 1768462115, 1768538951); - --- ---------------------------- --- Table structure for work_gallery --- ---------------------------- -DROP TABLE IF EXISTS `work_gallery`; -CREATE TABLE `work_gallery` ( - `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', - `work_id` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '关联的作品ID', - `image_url` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '图片URL', - `sort_order` int UNSIGNED NULL DEFAULT 0 COMMENT '排序顺序,数值越小越靠前', - `description` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '图片描述', - `deleted_at` bigint NOT NULL DEFAULT 0, - `created_at` bigint NOT NULL DEFAULT 0, - PRIMARY KEY (`id`) USING BTREE, - INDEX `idx_work_id`(`work_id` ASC) USING BTREE, - INDEX `idx_sort_order`(`sort_order` ASC) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '作品图库表' ROW_FORMAT = DYNAMIC; - --- ---------------------------- --- Records of work_gallery --- ---------------------------- -INSERT INTO `work_gallery` VALUES (1, 'nova', 'https://images.unsplash.com/photo-1642543492481-44e81e3914a7?q=80&w=2070', 1, 'Nova 交易平台首页', 0, 1768291937); -INSERT INTO `work_gallery` VALUES (2, 'nova', 'https://images.unsplash.com/photo-1551288049-bebda4e38f71?q=80&w=2070', 2, 'Nova 交易平台交易界面', 0, 1768291937); -INSERT INTO `work_gallery` VALUES (3, 'archdaily', 'https://images.unsplash.com/photo-1503387762-592deb58ef4e?q=80&w=2089', 1, 'ArchDaily 网站首页', 0, 1768291937); -INSERT INTO `work_gallery` VALUES (4, 'archdaily', 'https://images.unsplash.com/photo-1518005020951-ecc859466abc?q=80&w=1920', 2, 'ArchDaily 文章详情页', 0, 1768291937); - --- ---------------------------- --- Table structure for work_tech_stack --- ---------------------------- -DROP TABLE IF EXISTS `work_tech_stack`; -CREATE TABLE `work_tech_stack` ( - `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', - `work_id` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '关联的作品ID', - `category` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '技术分类(如:前端、后端、数据库等)', - `item` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '具体技术项(如:Vue 3、Golang、MySQL等)', - `deleted_at` bigint NOT NULL DEFAULT 0, - `created_at` bigint NOT NULL DEFAULT 0, - PRIMARY KEY (`id`) USING BTREE, - INDEX `idx_work_id`(`work_id` ASC) USING BTREE, - INDEX `idx_category`(`category` ASC) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 10 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '作品技术栈表' ROW_FORMAT = DYNAMIC; - --- ---------------------------- --- Records of work_tech_stack --- ---------------------------- -INSERT INTO `work_tech_stack` VALUES (1, 'nova', '前端层', 'React 18', 0, 1768291937); -INSERT INTO `work_tech_stack` VALUES (2, 'nova', '前端层', 'TypeScript', 0, 1768291937); -INSERT INTO `work_tech_stack` VALUES (3, 'nova', '前端层', 'D3.js', 0, 1768291937); -INSERT INTO `work_tech_stack` VALUES (4, 'nova', '后端服务', 'Golang', 0, 1768291937); -INSERT INTO `work_tech_stack` VALUES (5, 'nova', '后端服务', 'gRPC', 0, 1768291937); -INSERT INTO `work_tech_stack` VALUES (6, 'archdaily', '核心前端', 'Vue 3', 0, 1768291937); -INSERT INTO `work_tech_stack` VALUES (7, 'archdaily', '核心前端', 'Nuxt.js', 0, 1768291937); -INSERT INTO `work_tech_stack` VALUES (8, 'archdaily', '核心前端', 'GSAP', 0, 1768291937); -INSERT INTO `work_tech_stack` VALUES (9, 'archdaily', 'CMS', 'Strapi', 0, 1768291937); - --- ---------------------------- --- Table structure for works --- ---------------------------- -DROP TABLE IF EXISTS `works`; -CREATE TABLE `works` ( - `id` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '作品唯一标识', - `title` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '作品标题', - `category` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '作品分类', - `year` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '创作年份', - `hero_img` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '作品主图URL', - `description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '作品详细描述', - `is_featured` tinyint(1) NULL DEFAULT 0 COMMENT '是否为精选作品(0:否,1:是)', - `deleted_at` bigint NOT NULL DEFAULT 0, - `created_at` bigint NOT NULL DEFAULT 0, - `updated_at` bigint NOT NULL DEFAULT 0, - PRIMARY KEY (`id`) USING BTREE, - INDEX `idx_category`(`category` ASC) USING BTREE COMMENT '按分类查询索引', - INDEX `idx_year`(`year` ASC) USING BTREE COMMENT '按年份查询索引', - INDEX `idx_is_featured`(`is_featured` ASC) USING BTREE COMMENT '按精选状态查询索引' -) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '作品表' ROW_FORMAT = DYNAMIC; - --- ---------------------------- --- Records of works --- ---------------------------- -INSERT INTO `works` VALUES ('archdaily', 'ArchDaily 网站重构', '建筑设计', '2022', 'https://images.unsplash.com/photo-1487958449943-2429e8be8625?q=80&w=2070', 'ArchDaily 是全球最受欢迎的建筑网站之一。这次重构的目标是提升移动端体验。我们使用了 Nuxt 3 进行服务端渲染(SSR)。', 1, 0, 1768291814, 1768538952); -INSERT INTO `works` VALUES ('nova', 'Nova 交易平台', '金融科技', '2023', 'https://images.unsplash.com/photo-1487958449943-2429e8be8625?q=80&w=2070', 'Nova 是一个专为机构交易员设计的高频交易终端。我们面临的最大挑战是如何在处理毫秒级市场数据的同时,保持界面的流畅响应。我们采用 Web Worker 来处理繁重的数据计算,避免阻塞主线程。', 1, 0, 1768291814, 1768538952); - -SET FOREIGN_KEY_CHECKS = 1; diff --git a/server/repositories/post_repository.go b/server/repositories/post_repository.go index e12e096..82a2926 100644 --- a/server/repositories/post_repository.go +++ b/server/repositories/post_repository.go @@ -1,11 +1,15 @@ package repositories import ( + "encoding/json" + "fmt" "log" + "strconv" "time" "github.com/niangaodev/art-code/config" "github.com/niangaodev/art-code/models" + "github.com/sergi/go-diff/diffmatchpatch" "gorm.io/gorm" ) @@ -91,14 +95,17 @@ func GetPostByID(id uint) (*models.Post, error) { return nil, err } - // 更新阅读量 - config.DB.Model(&models.Post{}). - Where("id = ?", id). - UpdateColumn("read_count", gorm.Expr("read_count + ?", 1)) - + // 不再在此处更新阅读量,由 handler 按小时去重后递增 return &post, nil } +// IncrementReadCount 递增文章阅读量 +func IncrementReadCount(id uint) error { + return config.DB.Model(&models.Post{}). + Where("id = ?", id). + UpdateColumn("read_count", gorm.Expr("read_count + ?", 1)).Error +} + // GetAllPosts 获取所有博客文章(包括未发布的,后台用,支持搜索) func GetAllPosts(page, pageSize int, keyword string) ([]models.Post, int64, error) { offset := (page - 1) * pageSize @@ -408,11 +415,19 @@ func SavePostHistory(post *models.Post, modifiedBy uint) error { return err } + tagIDs := make([]uint, 0, len(post.Tags)) + for _, tag := range post.Tags { + tagIDs = append(tagIDs, tag.ID) + } + tagJSON, _ := json.Marshal(tagIDs) + history := &models.PostHistory{ PostID: post.ID, Version: maxVersion + 1, Title: post.Title, CategoryID: post.CategoryID, + ColumnID: post.ColumnID, + TagIDs: string(tagJSON), Excerpt: post.Excerpt, Content: post.Content, IsPublished: post.IsPublished, @@ -541,30 +556,163 @@ func GetPostHistoryByVersion(postID uint, version uint) (*models.PostHistory, er } // BuildPostHistoryResponse 构建历史记录响应 -func BuildPostHistoryResponse(h *models.PostHistory) *models.PostHistoryResponse { - return &models.PostHistoryResponse{ +func BuildPostHistoryResponse(h *models.PostHistory, includeFull bool) *models.PostHistoryResponse { + resp := &models.PostHistoryResponse{ ID: h.ID, PostID: h.PostID, Version: h.Version, Title: h.Title, CategoryID: h.CategoryID, + ColumnID: h.ColumnID, + TagIDs: h.GetTagIDList(), Date: time.Unix(h.CreatedAt, 0).Format("2006-01-02"), IsPublished: h.IsPublished, ModifiedBy: h.ModifiedBy, ModifiedAt: time.Unix(h.ModifiedAt, 0).Format("2006-01-02 15:04:05"), CreatedAt: time.Unix(h.CreatedAt, 0).Format("2006-01-02 15:04:05"), } + + if includeFull { + resp.Excerpt = h.Excerpt + resp.Content = h.Content + } + + return resp } // BuildPostHistoryResponses 构建历史记录列表响应 func BuildPostHistoryResponses(history []models.PostHistory) []models.PostHistoryResponse { var responses []models.PostHistoryResponse for _, h := range history { - responses = append(responses, *BuildPostHistoryResponse(&h)) + responses = append(responses, *BuildPostHistoryResponse(&h, false)) } return responses } +func buildFieldDiff(from, to string, withLineDiff bool) models.PostHistoryFieldDiff { + diff := models.PostHistoryFieldDiff{ + From: from, + To: to, + Changed: from != to, + } + if withLineDiff && from != to { + dmp := diffmatchpatch.New() + patches := dmp.PatchMake(from, to) + diff.Diff = dmp.PatchToText(patches) + } + return diff +} + +// GetPostHistoryDiff compares two history versions. +func GetPostHistoryDiff(postID uint, fromVersion, toVersion uint) (*models.PostHistoryDiffResponse, error) { + fromHistory, err := GetPostHistoryByVersion(postID, fromVersion) + if err != nil { + return nil, err + } + if fromHistory == nil { + return nil, fmt.Errorf("from version not found") + } + + toHistory, err := GetPostHistoryByVersion(postID, toVersion) + if err != nil { + return nil, err + } + if toHistory == nil { + return nil, fmt.Errorf("to version not found") + } + + fromTags, _ := json.Marshal(fromHistory.GetTagIDList()) + toTags, _ := json.Marshal(toHistory.GetTagIDList()) + + fields := map[string]models.PostHistoryFieldDiff{ + "title": buildFieldDiff(fromHistory.Title, toHistory.Title, false), + "excerpt": buildFieldDiff(fromHistory.Excerpt, toHistory.Excerpt, false), + "content": buildFieldDiff(fromHistory.Content, toHistory.Content, true), + "categoryId": buildFieldDiff(strconv.FormatUint(uint64(fromHistory.CategoryID), 10), strconv.FormatUint(uint64(toHistory.CategoryID), 10), false), + "columnId": buildFieldDiff(formatOptionalUint(fromHistory.ColumnID), formatOptionalUint(toHistory.ColumnID), false), + "tagIds": buildFieldDiff(string(fromTags), string(toTags), false), + "isPublished": buildFieldDiff(strconv.Itoa(fromHistory.IsPublished), strconv.Itoa(toHistory.IsPublished), false), + } + + return &models.PostHistoryDiffResponse{ + FromVersion: int(fromVersion), + ToVersion: int(toVersion), + Fields: fields, + }, nil +} + +func formatOptionalUint(v *uint) string { + if v == nil { + return "" + } + return strconv.FormatUint(uint64(*v), 10) +} + +// RestorePostFromHistory restores a post from a history snapshot and saves a new history entry. +func RestorePostFromHistory(postID, version, modifiedBy uint) (*models.Post, error) { + history, err := GetPostHistoryByVersion(postID, version) + if err != nil { + return nil, err + } + if history == nil { + return nil, fmt.Errorf("history version not found") + } + + var post models.Post + if err := config.DB.Preload("Tags").Where("id = ? AND deleted_at = ?", postID, 0).First(&post).Error; err != nil { + return nil, err + } + + post.Title = history.Title + post.CategoryID = history.CategoryID + post.ColumnID = history.ColumnID + post.Excerpt = history.Excerpt + post.Content = history.Content + post.IsPublished = history.IsPublished + + if err := UpdatePost(&post); err != nil { + return nil, err + } + + tagIDs := history.GetTagIDList() + tags := make([]models.Tag, 0, len(tagIDs)) + for _, id := range tagIDs { + tags = append(tags, models.Tag{ID: id}) + } + if err := config.DB.Model(&post).Association("Tags").Replace(tags); err != nil { + return nil, err + } + post.Tags = tags + + if err := SavePostHistory(&post, modifiedBy); err != nil { + return nil, err + } + + reloaded, err := GetPostByIDAdmin(postID) + if err != nil { + return nil, err + } + return reloaded, nil +} + +// GetPostByIDAdmin loads a post for admin use without incrementing read count. +func GetPostByIDAdmin(id uint) (*models.Post, error) { + var post models.Post + err := config.DB.Model(&models.Post{}). + Preload("Category"). + Preload("Column"). + Preload("Tags"). + Where("id = ? AND deleted_at = ?", id, 0). + First(&post).Error + if err != nil { + if err == gorm.ErrRecordNotFound { + return nil, nil + } + return nil, err + } + return &post, nil +} + // GetRecommendedPostsByIP 基于IP的协同过滤推荐算法 // 查找相同IP访问过的其他文章,按访问频率排序返回 func GetRecommendedPostsByIP(currentPostID uint, userIP string, limit int) ([]models.Post, error) { diff --git a/server/repositories/search_log_repository.go b/server/repositories/search_log_repository.go index 74c1c6e..061c975 100644 --- a/server/repositories/search_log_repository.go +++ b/server/repositories/search_log_repository.go @@ -61,3 +61,39 @@ func DeleteSearchLog(id uint) error { } return nil } + +// HotKeyword 热门搜索关键词 +type HotKeyword struct { + Keyword string `json:"keyword"` + Count int `json:"count"` +} + +// GetHotKeywords 获取热门搜索关键词 +func GetHotKeywords(limit, days int) ([]HotKeyword, error) { + if limit <= 0 { + limit = 10 + } + if days <= 0 { + days = 30 + } + + since := time.Now().AddDate(0, 0, -days).Unix() + + var results []HotKeyword + err := config.DB.Model(&models.SearchLog{}). + Select("keyword, COUNT(*) as count"). + Where("deleted_at = ?", 0). + Where("search_type = ?", "keyword"). + Where("created_at >= ?", since). + Where("CHAR_LENGTH(keyword) >= ?", 2). + Group("keyword"). + Order("count DESC"). + Limit(limit). + Scan(&results).Error + if err != nil { + log.Printf("Error querying hot keywords: %v", err) + return nil, err + } + + return results, nil +} diff --git a/server/repositories/user_access_log_repository.go b/server/repositories/user_access_log_repository.go index e5da6fc..eb0177b 100644 --- a/server/repositories/user_access_log_repository.go +++ b/server/repositories/user_access_log_repository.go @@ -23,6 +23,30 @@ func CreateUserAccessLog(logEntry *models.UserAccessLog) error { return nil } +// HasVisitedThisHour checks if the visitor already has an access log for this article in the current hour. +func HasVisitedThisHour(articleID, userID uint, visitorKey string) (bool, error) { + hourStart := time.Now().Truncate(time.Hour).Unix() + hourEnd := hourStart + 3600 + + var count int64 + query := config.DB.Model(&models.UserAccessLog{}). + Where("deleted_at = ?", 0). + Where("article_id = ?", articleID). + Where("access_time >= ? AND access_time < ?", hourStart, hourEnd) + + if userID > 0 { + query = query.Where("user_id = ?", userID) + } else { + query = query.Where("visitor_key = ?", visitorKey) + } + + if err := query.Count(&count).Error; err != nil { + return false, err + } + + return count > 0, nil +} + // AccessStats 访问统计数据结构 type AccessStats struct { Date string `json:"date"` diff --git a/server/utils/response.go b/server/utils/response.go index 1f35cd0..4efc032 100644 --- a/server/utils/response.go +++ b/server/utils/response.go @@ -31,14 +31,9 @@ func SuccessWithMsg(c *gin.Context, msg string, result interface{}) { }) } -// Error 错误响应 (默认 Code 400) +// Error 错误响应,HTTP 状态恒为 200,通过 code 区分业务状态 func Error(c *gin.Context, code int, msg string) { - httpStatus := http.StatusBadRequest - if code == 500 { - httpStatus = http.StatusInternalServerError - } - - c.JSON(httpStatus, Response{ + c.JSON(http.StatusOK, Response{ Code: code, Message: msg, Result: nil, @@ -47,7 +42,7 @@ func Error(c *gin.Context, code int, msg string) { // ServerError 服务器错误 (Code 500) func ServerError(c *gin.Context, err error) { - c.JSON(http.StatusInternalServerError, Response{ + c.JSON(http.StatusOK, Response{ Code: 500, Message: err.Error(), Result: nil, diff --git a/server/utils/visitor.go b/server/utils/visitor.go new file mode 100644 index 0000000..565546e --- /dev/null +++ b/server/utils/visitor.go @@ -0,0 +1,17 @@ +package utils + +import ( + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +// GetOrSetVisitorID returns an anonymous visitor identifier from cookie, creating one if missing. +func GetOrSetVisitorID(c *gin.Context) string { + if cookie, err := c.Cookie("visitor_id"); err == nil && cookie != "" { + return cookie + } + + id := uuid.New().String() + c.SetCookie("visitor_id", id, 365*24*3600, "/", "", false, true) + return id +}