BUG修复,返回结构统一

This commit is contained in:
李琦
2026-01-16 17:03:34 +08:00
parent 87ed11db7a
commit e6edf7210b
57 changed files with 3642 additions and 1074 deletions

View File

@@ -358,7 +358,13 @@ const runCode = async () => {
})
})
const result = await response.json()
if (!response.ok) {
const errorData = await response.json()
throw new Error(errorData.message || '代码执行失败')
}
const data = await response.json()
const result = data.result // 从统一响应格式中提取 result
let outputHtml = ''
if (result.error) {

View File

@@ -93,7 +93,9 @@ const navigate = (target: string) => {
const updateActiveNav = () => {
const path = route.path
if (path === '/') activeNav.value = 'home'
else if (path === '/blog') activeNav.value = 'blog'
else if (path === '/blog' || path.startsWith('/blog/')) activeNav.value = 'blog'
else if (path === '/columns' || path.startsWith('/columns/')) activeNav.value = 'columns'
else if (path === '/categories' || path.startsWith('/categories/')) activeNav.value = 'categories'
else if (path === '/works' || path.startsWith('/works/')) activeNav.value = 'works'
else if (path === '/snippets') activeNav.value = 'snippets'
else if (path === '/services') activeNav.value = 'services'

View File

@@ -199,6 +199,8 @@ const menuItems = ref<MenuItem[]>([
isOpen: true,
children: [
{ title: '文章管理', path: '/admin/posts', icon: '📝' },
{ title: '分类管理', path: '/admin/categories', icon: '📂' },
{ title: '专栏管理', path: '/admin/columns', icon: '📚' },
{ title: '作品管理', path: '/admin/works', icon: '🎨' },
{ title: '代码片段', path: '/admin/snippets', icon: '💻' },
{ title: '标签管理', path: '/admin/tags', icon: '🏷️' }

View File

@@ -155,8 +155,9 @@ const fetchProfileData = async () => {
email: data.email,
wechat: data.wechat
},
techStack: data.techStack,
experiences: data.experiences || []
// 确保 techStack 和 experiences 始终是数组
techStack: Array.isArray(data.techStack) ? data.techStack : [],
experiences: Array.isArray(data.experiences) ? data.experiences : []
}
} catch (err) {
console.error('Error fetching profile data:', err)

View File

@@ -62,8 +62,11 @@
>
<div class="flex flex-col md:flex-row gap-8 items-start">
<div class="md:w-1/4 pt-2">
<span class="font-mono text-xs text-art-accent block mb-1">{{ post.category }}</span>
<span class="font-mono text-xs text-art-accent block mb-1">{{ post.categoryName || post.category?.name || '未分类' }}</span>
<span class="text-sm text-art-muted">{{ post.date }}</span>
<div v-if="post.tags && post.tags.length > 0" class="flex flex-wrap gap-2 mt-2">
<span v-for="tag in post.tags" :key="tag.id" class="text-xs text-white/40 bg-white/5 px-1 rounded">#{{ tag.name }}</span>
</div>
</div>
<div class="md:w-3/4 space-y-4">
<h3 class="font-serif text-3xl text-white group-hover:text-art-accent transition-colors leading-tight" v-html="highlightText(post.title)">

View File

@@ -0,0 +1,56 @@
<template>
<section class="page-section block animate-slide-down">
<div class="max-w-4xl mx-auto pt-32 px-6 pb-20">
<div class="mb-16 text-center">
<h2 class="font-serif text-5xl italic text-white mb-6">分类</h2>
<p class="text-art-muted max-w-lg mx-auto mb-8">按照主题浏览文章</p>
</div>
<!-- Loading state -->
<div v-if="loading" class="flex justify-center items-center h-64">
<div class="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-art-accent"></div>
</div>
<!-- Categories Grid -->
<div v-else class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<div
v-for="category in categories"
:key="category.id"
class="group p-6 rounded-lg border border-white/5 bg-white/5 hover:bg-white/10 hover:border-art-accent/30 transition-all cursor-pointer"
@click="router.push(`/blog?category=${category.id}`)"
>
<div class="flex items-start justify-between mb-4">
<span class="text-3xl">📂</span>
<span class="font-mono text-xs text-art-muted group-hover:text-art-accent transition-colors">#{{ category.slug }}</span>
</div>
<h3 class="text-xl font-serif text-white mb-2 group-hover:text-art-accent transition-colors">{{ category.name }}</h3>
<p class="text-sm text-art-muted leading-relaxed line-clamp-3">{{ category.description || '暂无描述' }}</p>
</div>
</div>
<div v-if="!loading && categories.length === 0" class="text-center py-20 text-art-muted">
暂无分类
</div>
</div>
</section>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { fetchCategories, Category } from '../services/api'
const router = useRouter()
const categories = ref<Category[]>([])
const loading = ref(true)
onMounted(async () => {
try {
categories.value = await fetchCategories()
} catch (error) {
console.error('Failed to fetch categories:', error)
} finally {
loading.value = false
}
})
</script>

View File

@@ -0,0 +1,61 @@
<template>
<section class="page-section block animate-slide-down">
<div class="max-w-4xl mx-auto pt-32 px-6 pb-20">
<!-- Loading state -->
<div v-if="loading" class="flex justify-center items-center h-64">
<div class="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-art-accent"></div>
</div>
<div v-else-if="column">
<!-- Header -->
<div class="mb-16 text-center">
<div class="inline-block mb-4 px-3 py-1 rounded-full border border-art-accent/30 text-art-accent text-xs font-mono">专栏</div>
<h2 class="font-serif text-4xl italic text-white mb-6">{{ column.name }}</h2>
<p class="text-art-muted max-w-lg mx-auto leading-relaxed">{{ column.description }}</p>
</div>
<!-- Posts List (Placeholder for now as we don't have column_posts API integrated fully yet) -->
<!-- Since we don't have GetPostsByColumn API yet, we might need to fetch all posts and filter,
or update backend to support fetching posts by column.
The SQL has `column_posts` table. I need to check if backend `GetColumn` returns posts or if I need a separate endpoint.
-->
<div class="space-y-8">
<div class="text-center py-10 border border-dashed border-white/10 rounded-lg">
<p class="text-art-muted">专栏文章列表功能开发中...</p>
<!--
TODO: Fetch posts associated with this column.
Backend needs to expose this.
For now, I'll just show the column info.
-->
</div>
</div>
</div>
<div v-else class="text-center py-20 text-art-muted">
未找到专栏
</div>
</div>
</section>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import { fetchColumn, Column } from '../services/api'
const route = useRoute()
const column = ref<Column | null>(null)
const loading = ref(true)
onMounted(async () => {
try {
const id = parseInt(route.params.id as string)
column.value = await fetchColumn(id)
} catch (error) {
console.error('Failed to fetch column:', error)
} finally {
loading.value = false
}
})
</script>

View File

@@ -0,0 +1,70 @@
<template>
<section class="page-section block animate-slide-down">
<div class="max-w-6xl mx-auto pt-32 px-6 pb-20">
<div class="mb-16 text-center">
<h2 class="font-serif text-5xl italic text-white mb-6">专栏</h2>
<p class="text-art-muted max-w-lg mx-auto mb-8">系列文章集合系统性地学习</p>
</div>
<!-- Loading state -->
<div v-if="loading" class="flex justify-center items-center h-64">
<div class="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-art-accent"></div>
</div>
<!-- Columns Grid -->
<div v-else class="grid grid-cols-1 md:grid-cols-2 gap-8">
<div
v-for="column in columns"
:key="column.id"
class="group relative overflow-hidden rounded-xl border border-white/5 bg-white/5 aspect-[16/9] cursor-pointer"
@click="router.push(`/columns/${column.id}`)"
>
<!-- Cover Image -->
<div
v-if="column.cover"
class="absolute inset-0 bg-cover bg-center transition-transform duration-700 group-hover:scale-105"
:style="{ backgroundImage: `url(${column.cover})` }"
>
<div class="absolute inset-0 bg-black/60 group-hover:bg-black/50 transition-colors duration-500"></div>
</div>
<div v-else class="absolute inset-0 bg-gradient-to-br from-white/5 to-white/10 group-hover:from-white/10 group-hover:to-white/20 transition-colors"></div>
<!-- Content -->
<div class="absolute inset-0 p-8 flex flex-col justify-end">
<h3 class="text-3xl font-serif text-white mb-3 group-hover:text-art-accent transition-colors">{{ column.name }}</h3>
<p class="text-art-muted leading-relaxed line-clamp-2">{{ column.description || '暂无描述' }}</p>
<div class="mt-4 opacity-0 group-hover:opacity-100 transition-opacity duration-300 transform translate-y-2 group-hover:translate-y-0">
<span class="text-xs font-bold tracking-widest uppercase text-white border-b border-art-accent pb-1">查看详情</span>
</div>
</div>
</div>
</div>
<div v-if="!loading && columns.length === 0" class="text-center py-20 text-art-muted">
暂无专栏
</div>
</div>
</section>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { fetchColumns, Column } from '../services/api'
const router = useRouter()
const columns = ref<Column[]>([])
const loading = ref(true)
onMounted(async () => {
try {
const data = await fetchColumns()
// Filter active columns
columns.value = data.filter(c => c.isActive === 1)
} catch (error) {
console.error('Failed to fetch columns:', error)
} finally {
loading.value = false
}
})
</script>

View File

@@ -0,0 +1,95 @@
<template>
<div class="w-full animate-reveal">
<div class="flex items-center justify-between mb-6">
<h1 class="text-2xl font-serif italic text-white">分类管理</h1>
<button @click="router.push('/admin/categories/create')" class="admin-btn-primary flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>
新建分类
</button>
</div>
<div class="admin-card overflow-hidden">
<div class="overflow-x-auto">
<table class="admin-table">
<thead>
<tr>
<th class="w-20">ID</th>
<th>名称</th>
<th>Slug</th>
<th>描述</th>
<th>排序</th>
<th>创建时间</th>
<th class="text-right">操作</th>
</tr>
</thead>
<tbody>
<tr v-for="category in categories" :key="category.id" class="group transition-colors duration-200">
<td class="font-mono text-xs text-white/40">#{{ category.id }}</td>
<td class="font-medium text-white group-hover:text-art-accent transition-colors">{{ category.name }}</td>
<td class="text-xs text-art-muted font-mono">{{ category.slug }}</td>
<td class="text-sm text-white/70 max-w-xs truncate">{{ category.description || '-' }}</td>
<td class="text-xs text-art-muted">{{ category.sortOrder }}</td>
<td class="text-art-muted text-xs">{{ category.createdAt }}</td>
<td class="text-right">
<div class="flex items-center justify-end gap-2 opacity-0 group-hover:opacity-100 transition-opacity duration-200">
<router-link :to="`/admin/categories/${category.id}/edit`" class="admin-btn-secondary py-1 px-3 text-xs">
编辑
</router-link>
<button @click="handleDelete(category.id)" class="admin-btn-danger py-1 px-3 text-xs">
删除
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<div v-if="categories.length === 0" class="p-16 text-center">
<div class="w-16 h-16 bg-white/5 rounded-full flex items-center justify-center mx-auto mb-4 text-2xl">📂</div>
<h3 class="text-white font-medium mb-2">暂无分类</h3>
<p class="text-art-muted text-sm mb-6">创建分类来整理您的文章</p>
<router-link to="/admin/categories/create" class="admin-btn-secondary inline-flex items-center gap-2">
+ 新建分类
</router-link>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { getAdminCategories, deleteCategory, Category } from '../../services/api'
import { useToast } from '../../composables/useToast'
const router = useRouter()
const toast = useToast()
const categories = ref<Category[]>([])
const fetchCategories = async () => {
try {
categories.value = await getAdminCategories()
} catch (error) {
console.error('Error fetching categories:', error)
toast.showToast('获取分类列表失败', 'error')
}
}
const handleDelete = async (id: number) => {
if (confirm('确定要删除这个分类吗?')) {
try {
await deleteCategory(id)
toast.showToast('分类删除成功', 'success')
fetchCategories()
} catch (error) {
console.error('Error deleting category:', error)
toast.showToast('删除分类失败', 'error')
}
}
}
onMounted(() => {
fetchCategories()
})
</script>

View File

@@ -0,0 +1,148 @@
<template>
<div class="w-full animate-reveal h-[calc(100vh-8rem)] flex flex-col">
<!-- Top Bar -->
<div class="flex items-center justify-between mb-4 shrink-0">
<div class="flex items-center gap-4">
<h1 class="text-2xl font-serif italic text-white">{{ isEditing ? '编辑分类' : '新建分类' }}</h1>
</div>
<div class="flex gap-3">
<button type="button" class="admin-btn-secondary" @click="handleCancel">
取消
</button>
<button type="button" class="admin-btn-primary" @click="handleSubmit" :disabled="isSubmitting">
{{ isSubmitting ? '提交中...' : (isEditing ? '保存修改' : '创建分类') }}
</button>
</div>
</div>
<div class="admin-card p-6 max-w-2xl mx-auto w-full">
<form @submit.prevent="handleSubmit" class="space-y-6">
<!-- Name Field -->
<div class="space-y-2">
<label for="name" class="block text-xs font-medium text-art-muted uppercase tracking-wider">名称</label>
<input
type="text"
id="name"
v-model="form.name"
class="admin-input"
placeholder="请输入分类名称"
required
/>
</div>
<!-- Slug Field -->
<div class="space-y-2">
<label for="slug" class="block text-xs font-medium text-art-muted uppercase tracking-wider">Slug (URL标识)</label>
<input
type="text"
id="slug"
v-model="form.slug"
class="admin-input font-mono"
placeholder="category-slug"
required
/>
</div>
<!-- Sort Order Field -->
<div class="space-y-2">
<label for="sortOrder" class="block text-xs font-medium text-art-muted uppercase tracking-wider">排序 (越小越靠前)</label>
<input
type="number"
id="sortOrder"
v-model="form.sortOrder"
class="admin-input"
placeholder="0"
/>
</div>
<!-- Description Field -->
<div class="space-y-2">
<label for="description" class="block text-xs font-medium text-art-muted uppercase tracking-wider">描述</label>
<textarea
id="description"
v-model="form.description"
rows="4"
class="admin-input resize-none"
placeholder="简短的分类描述..."
></textarea>
</div>
</form>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, computed } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useToast } from '../../composables/useToast'
import { createCategory, updateCategory, getAdminCategories } from '../../services/api'
const router = useRouter()
const route = useRoute()
const toast = useToast()
const isSubmitting = ref(false)
const isEditing = computed(() => !!route.params.id)
const form = reactive({
name: '',
slug: '',
description: '',
sortOrder: 0
})
const handleSubmit = async () => {
if (!form.name.trim() || !form.slug.trim()) {
toast.showToast('名称和Slug不能为空', 'error')
return
}
isSubmitting.value = true
try {
if (isEditing.value) {
const id = parseInt(route.params.id as string)
await updateCategory(id, form)
toast.showToast('分类更新成功', 'success')
} else {
await createCategory(form)
toast.showToast('分类创建成功', 'success')
}
router.push('/admin/categories')
} catch (error: any) {
console.error('Error submitting form:', error)
toast.showToast(error.message || (isEditing.value ? '更新分类失败' : '创建分类失败'), 'error')
} finally {
isSubmitting.value = false
}
}
const handleCancel = () => {
router.back()
}
onMounted(async () => {
if (isEditing.value) {
try {
const id = parseInt(route.params.id as string)
// Note: We don't have getCategoryById in API for single category yet,
// but we can fetch all and find one, or update API.
// For now, let's fetch all (simple enough for admin)
const categories = await getAdminCategories()
const category = categories.find(c => c.id === id)
if (category) {
form.name = category.name
form.slug = category.slug
form.description = category.description
form.sortOrder = category.sortOrder
} else {
toast.showToast('未找到该分类', 'error')
router.push('/admin/categories')
}
} catch (error) {
console.error('Failed to load category:', error)
toast.showToast('加载数据失败', 'error')
}
}
})
</script>

View File

@@ -0,0 +1,191 @@
<template>
<div class="w-full animate-reveal h-[calc(100vh-8rem)] flex flex-col">
<!-- Top Bar -->
<div class="flex items-center justify-between mb-4 shrink-0">
<div class="flex items-center gap-4">
<h1 class="text-2xl font-serif italic text-white">{{ isEditing ? '编辑专栏' : '新建专栏' }}</h1>
</div>
<div class="flex gap-3">
<button type="button" class="admin-btn-secondary" @click="handleCancel">
取消
</button>
<button type="button" class="admin-btn-primary" @click="handleSubmit" :disabled="isSubmitting">
{{ isSubmitting ? '提交中...' : (isEditing ? '保存修改' : '创建专栏') }}
</button>
</div>
</div>
<div class="admin-card p-6 max-w-2xl mx-auto w-full">
<form @submit.prevent="handleSubmit" class="space-y-6">
<!-- Name Field -->
<div class="space-y-2">
<label for="name" class="block text-xs font-medium text-art-muted uppercase tracking-wider">名称</label>
<input
type="text"
id="name"
v-model="form.name"
class="admin-input"
placeholder="请输入专栏名称"
required
/>
</div>
<!-- Cover Field -->
<div class="space-y-2">
<label for="cover" class="block text-xs font-medium text-art-muted uppercase tracking-wider">封面图片 URL</label>
<input
type="text"
id="cover"
v-model="form.cover"
class="admin-input"
placeholder="https://..."
/>
</div>
<!-- Status Field -->
<div class="space-y-2">
<label class="block text-xs font-medium text-art-muted uppercase tracking-wider">状态</label>
<div class="flex gap-4 mt-2">
<label class="flex items-center cursor-pointer">
<input type="radio" v-model="form.isActive" :value="1" class="mr-2">
<span class="text-sm text-white">启用</span>
</label>
<label class="flex items-center cursor-pointer">
<input type="radio" v-model="form.isActive" :value="0" class="mr-2">
<span class="text-sm text-white">禁用</span>
</label>
</div>
</div>
<!-- Sort Order Field -->
<div class="space-y-2">
<label for="sortOrder" class="block text-xs font-medium text-art-muted uppercase tracking-wider">排序 (越小越靠前)</label>
<input
type="number"
id="sortOrder"
v-model="form.sortOrder"
class="admin-input"
placeholder="0"
/>
</div>
<!-- Description Field -->
<div class="space-y-2">
<label for="description" class="block text-xs font-medium text-art-muted uppercase tracking-wider">描述</label>
<textarea
id="description"
v-model="form.description"
rows="4"
class="admin-input resize-none"
placeholder="简短的专栏描述..."
></textarea>
</div>
<!-- Post Management Section (Only in Edit Mode) -->
<div v-if="isEditing" class="pt-6 border-t border-white/10">
<h3 class="text-lg font-medium text-white mb-4">文章管理</h3>
<!-- Add Post -->
<div class="flex gap-2 mb-6">
<select v-model="selectedPostId" class="admin-input flex-1">
<option value="">选择文章添加到专栏...</option>
<option v-for="post in allPosts" :key="post.id" :value="post.id">
{{ post.title }} ({{ post.isPublished ? '已发布' : '草稿' }})
</option>
</select>
<button type="button" @click="handleAddPost" class="admin-btn-secondary whitespace-nowrap" :disabled="!selectedPostId">
添加文章
</button>
</div>
<!-- Post List -->
<div class="space-y-2">
<label class="block text-xs font-medium text-art-muted uppercase tracking-wider">已关联文章 ({{ columnPosts.length }})</label>
<div v-if="columnPosts.length > 0" class="border border-white/10 rounded-lg divide-y divide-white/5 bg-white/5">
<div v-for="post in columnPosts" :key="post.id" class="flex items-center justify-between p-3">
<span class="text-sm text-white">{{ post.title }}</span>
<button type="button" @click="handleRemovePost(post.id)" class="text-red-400 hover:text-red-300 text-xs px-2 py-1">
移除
</button>
</div>
</div>
<div v-else class="text-center py-8 text-art-muted text-sm border border-dashed border-white/10 rounded-lg">
暂无关联文章
</div>
</div>
</div>
</form>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, computed } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useToast } from '../../composables/useToast'
import { createColumn, updateColumn, fetchColumn } from '../../services/api'
const router = useRouter()
const route = useRoute()
const toast = useToast()
const isSubmitting = ref(false)
const isEditing = computed(() => !!route.params.id)
const form = reactive({
name: '',
cover: '',
description: '',
isActive: 1,
sortOrder: 0
})
const handleSubmit = async () => {
if (!form.name.trim()) {
toast.showToast('名称不能为空', 'error')
return
}
isSubmitting.value = true
try {
if (isEditing.value) {
const id = parseInt(route.params.id as string)
await updateColumn(id, form)
toast.showToast('专栏更新成功', 'success')
} else {
await createColumn(form)
toast.showToast('专栏创建成功', 'success')
}
router.push('/admin/columns')
} catch (error: any) {
console.error('Error submitting form:', error)
toast.showToast(error.message || (isEditing.value ? '更新专栏失败' : '创建专栏失败'), 'error')
} finally {
isSubmitting.value = false
}
}
const handleCancel = () => {
router.back()
}
onMounted(async () => {
if (isEditing.value) {
try {
const id = parseInt(route.params.id as string)
const column = await fetchColumn(id)
if (column) {
form.name = column.name
form.cover = column.cover
form.description = column.description
form.isActive = column.isActive
form.sortOrder = column.sortOrder
}
} catch (error) {
console.error('Failed to load column:', error)
toast.showToast('加载数据失败', 'error')
}
}
})
</script>

View File

@@ -0,0 +1,104 @@
<template>
<div class="w-full animate-reveal">
<div class="flex items-center justify-between mb-6">
<h1 class="text-2xl font-serif italic text-white">专栏管理</h1>
<button @click="router.push('/admin/columns/create')" class="admin-btn-primary flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>
新建专栏
</button>
</div>
<div class="admin-card overflow-hidden">
<div class="overflow-x-auto">
<table class="admin-table">
<thead>
<tr>
<th class="w-20">ID</th>
<th>封面</th>
<th>名称</th>
<th>描述</th>
<th>状态</th>
<th>排序</th>
<th>创建时间</th>
<th class="text-right">操作</th>
</tr>
</thead>
<tbody>
<tr v-for="column in columns" :key="column.id" class="group transition-colors duration-200">
<td class="font-mono text-xs text-white/40">#{{ column.id }}</td>
<td>
<div v-if="column.cover" class="w-10 h-10 rounded bg-cover bg-center border border-white/10" :style="{ backgroundImage: `url(${column.cover})` }"></div>
<div v-else class="w-10 h-10 rounded bg-white/5 border border-white/10 flex items-center justify-center text-xs text-white/30"></div>
</td>
<td class="font-medium text-white group-hover:text-art-accent transition-colors">{{ column.name }}</td>
<td class="text-sm text-white/70 max-w-xs truncate">{{ column.description || '-' }}</td>
<td>
<span :class="['px-2 py-1 rounded-full text-xs font-medium border', column.isActive === 1 ? 'bg-green-500/10 text-green-400 border-green-500/20' : 'bg-gray-500/10 text-gray-400 border-gray-500/20']">
{{ column.isActive === 1 ? '启用' : '禁用' }}
</span>
</td>
<td class="text-xs text-art-muted">{{ column.sortOrder }}</td>
<td class="text-art-muted text-xs">{{ column.createdAt }}</td>
<td class="text-right">
<div class="flex items-center justify-end gap-2 opacity-0 group-hover:opacity-100 transition-opacity duration-200">
<router-link :to="`/admin/columns/${column.id}/edit`" class="admin-btn-secondary py-1 px-3 text-xs">
编辑
</router-link>
<button @click="handleDelete(column.id)" class="admin-btn-danger py-1 px-3 text-xs">
删除
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<div v-if="columns.length === 0" class="p-16 text-center">
<div class="w-16 h-16 bg-white/5 rounded-full flex items-center justify-center mx-auto mb-4 text-2xl">📚</div>
<h3 class="text-white font-medium mb-2">暂无专栏</h3>
<p class="text-art-muted text-sm mb-6">创建专栏来组织系列文章</p>
<router-link to="/admin/columns/create" class="admin-btn-secondary inline-flex items-center gap-2">
+ 新建专栏
</router-link>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { getAdminColumns, deleteColumn, Column } from '../../services/api'
import { useToast } from '../../composables/useToast'
const router = useRouter()
const toast = useToast()
const columns = ref<Column[]>([])
const fetchColumns = async () => {
try {
columns.value = await getAdminColumns()
} catch (error) {
console.error('Error fetching columns:', error)
toast.showToast('获取专栏列表失败', 'error')
}
}
const handleDelete = async (id: number) => {
if (confirm('确定要删除这个专栏吗?')) {
try {
await deleteColumn(id)
toast.showToast('专栏删除成功', 'success')
fetchColumns()
} catch (error) {
console.error('Error deleting column:', error)
toast.showToast('删除专栏失败', 'error')
}
}
}
onMounted(() => {
fetchColumns()
})
</script>

View File

@@ -78,7 +78,8 @@ const fetchSuffixes = async () => {
headers: getAuthHeaders()
})
if (response.ok) {
suffixes.value = await response.json()
const data = await response.json()
suffixes.value = data.result || []
}
} catch (error) {
console.error('Failed to fetch suffixes:', error)
@@ -108,7 +109,8 @@ const handleAdd = async () => {
newSortOrder.value = 0
fetchSuffixes()
} else {
toast.error('添加失败')
const errorData = await response.json()
toast.error(errorData.message || '添加失败')
}
} catch (error) {
console.error(error)
@@ -129,7 +131,8 @@ const handleDelete = async (id: number) => {
toast.success('删除成功')
fetchSuffixes()
} else {
toast.error('删除失败')
const errorData = await response.json()
toast.error(errorData.message || '删除失败')
}
} catch (error) {
console.error(error)

View File

@@ -86,7 +86,8 @@ const fetchInquiries = async () => {
headers: getAuthHeaders()
})
if (response.ok) {
inquiries.value = await response.json()
const data = await response.json()
inquiries.value = data.result || []
}
} catch (error) {
console.error('Failed to fetch inquiries:', error)
@@ -106,7 +107,8 @@ const updateStatus = async (id: number, status: number) => {
toast.success('状态更新成功')
fetchInquiries()
} else {
toast.error('更新失败')
const errorData = await response.json()
toast.error(errorData.message || '更新失败')
}
} catch (error) {
console.error(error)

View File

@@ -85,16 +85,33 @@
<!-- Category Field -->
<div class="space-y-2">
<label for="category" class="block text-xs font-medium text-art-muted uppercase tracking-wider">分类</label>
<input
type="text"
id="category"
v-model="form.category"
class="admin-input"
placeholder="文章分类"
required
<CustomSelect
v-model="form.categoryId"
:options="categories"
placeholder="选择文章分类"
/>
<div class="text-art-error text-xs mt-1" v-if="errors.category">
{{ errors.category }}
<div class="text-art-error text-xs mt-1" v-if="errors.categoryId">
{{ errors.categoryId }}
</div>
</div>
<!-- Tags Field -->
<div class="space-y-2">
<label class="block text-xs font-medium text-art-muted uppercase tracking-wider">标签</label>
<div class="flex flex-wrap gap-2">
<button
type="button"
v-for="tag in availableTags"
:key="tag.id"
@click="toggleTag(tag.id)"
class="px-2 py-1 text-xs rounded border transition-colors"
:class="form.tagIds.includes(tag.id) ? 'bg-art-accent/20 border-art-accent text-art-accent' : 'bg-white/5 border-white/10 text-art-muted hover:border-white/30'"
>
{{ tag.name }}
</button>
<div v-if="availableTags.length === 0" class="text-xs text-art-muted italic">
暂无可用标签
</div>
</div>
</div>
@@ -140,7 +157,7 @@ import { MdEditor } from 'md-editor-v3'
import 'md-editor-v3/lib/style.css'
import { useToast } from '../../composables/useToast'
import { createPost, updatePost, fetchPost } from '../../services/api'
import { createPost, updatePost, fetchPost, fetchCategories, fetchTags, Category, Tag } from '../../services/api'
import CustomSelect from '../../components/CustomSelect.vue'
const router = useRouter()
@@ -158,14 +175,19 @@ const isSubmitting = ref(false)
const isEditing = computed(() => !!route.params.id)
const errors = reactive<Record<string, string>>({})
// Data sources
const categories = ref<{value: number, label: string}[]>([])
const availableTags = ref<Tag[]>([])
// Form data
const form = reactive({
title: '',
category: '',
categoryId: 0,
date: new Date().toISOString().split('T')[0],
excerpt: '',
content: '',
isPublished: 1
isPublished: 1,
tagIds: [] as number[]
})
// Select options
@@ -190,8 +212,8 @@ const validateForm = (): boolean => {
}
// Validate category
if (!form.category.trim()) {
errors.category = '文章分类不能为空'
if (!form.categoryId) {
errors.categoryId = '请选择文章分类'
isValid = false
isSidebarOpen.value = true
}
@@ -212,6 +234,16 @@ const validateForm = (): boolean => {
return isValid
}
// Toggle tag selection
const toggleTag = (tagId: number) => {
const index = form.tagIds.indexOf(tagId)
if (index === -1) {
form.tagIds.push(tagId)
} else {
form.tagIds.splice(index, 1)
}
}
// Submit handler
const handleSubmit = async () => {
if (!validateForm()) {
@@ -221,13 +253,24 @@ const handleSubmit = async () => {
isSubmitting.value = true
try {
// Construct payload
const payload = {
title: form.title,
categoryId: form.categoryId,
date: form.date,
excerpt: form.excerpt,
content: form.content,
isPublished: form.isPublished,
tags: form.tagIds.map(id => ({ id } as any)) // Backend expects objects with ID
}
if (isEditing.value) {
// Update existing post
await updatePost(route.params.id as string, form)
await updatePost(route.params.id as string, payload)
toast.showToast('文章更新成功', 'success')
} else {
// Create new post
await createPost(form)
await createPost(payload)
toast.showToast('文章创建成功', 'success')
}
@@ -246,26 +289,49 @@ const handleCancel = () => {
router.push('/admin/posts')
}
// Lifecycle
onMounted(async () => {
// If editing, load post data from API
if (isEditing.value) {
try {
// Load initial data
const loadData = async () => {
try {
// Fetch categories and tags in parallel
const [cats, tags] = await Promise.all([
fetchCategories(),
fetchTags()
])
categories.value = cats.map(c => ({
value: c.id,
label: c.name
}))
availableTags.value = tags
// If editing, load post data
if (isEditing.value) {
const postId = route.params.id as string
const post = await fetchPost(postId)
// Populate form with post data
form.title = post.title
form.category = post.category
form.categoryId = post.categoryId
form.date = post.date
form.excerpt = post.excerpt || ''
form.content = post.content || ''
form.isPublished = post.isPublished === 1 ? 1 : 0
} catch (error: any) {
console.error('Failed to fetch post data:', error)
toast.showToast('加载文章数据失败: ' + (error.message || '未知错误'), 'error')
// Map tags to tagIds
if (post.tags && post.tags.length > 0) {
form.tagIds = post.tags.map(t => t.id)
}
}
} catch (error: any) {
console.error('Failed to load data:', error)
toast.showToast('加载数据失败: ' + (error.message || '未知错误'), 'error')
}
}
// Lifecycle
onMounted(() => {
loadData()
})
</script>

View File

@@ -25,12 +25,16 @@
<tr v-for="post in posts" :key="post.id" class="group transition-colors duration-200">
<td class="font-mono text-xs text-white/40">#{{ post.id }}</td>
<td class="font-medium text-white group-hover:text-art-accent transition-colors">{{ post.title }}</td>
<td><span class="px-2 py-1 rounded bg-white/5 border border-white/5 text-xs text-art-muted">{{ post.category }}</span></td>
<td><span class="px-2 py-1 rounded bg-white/5 border border-white/5 text-xs text-art-muted">{{ post.categoryName || post.category?.name || '未分类' }}</span></td>
<td class="text-art-muted text-xs">{{ post.date }}</td>
<td>
<span :class="['px-2 py-1 rounded-full text-xs font-medium border', post.isPublished === 1 ? 'bg-green-500/10 text-green-400 border-green-500/20' : 'bg-yellow-500/10 text-yellow-400 border-yellow-500/20']">
<button
@click="handleToggleStatus(post)"
:class="['px-2 py-1 rounded-full text-xs font-medium border transition-all hover:opacity-80', post.isPublished === 1 ? 'bg-green-500/10 text-green-400 border-green-500/20' : 'bg-yellow-500/10 text-yellow-400 border-yellow-500/20']"
title="点击切换状态"
>
{{ post.isPublished === 1 ? '已发布' : '草稿' }}
</span>
</button>
</td>
<td class="text-right">
<div class="flex items-center justify-end gap-2 opacity-0 group-hover:opacity-100 transition-opacity duration-200">
@@ -56,13 +60,41 @@
</router-link>
</div>
</div>
<!-- Category Modal -->
<div v-if="isCategoryModalOpen" class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm" @click.self="closeCategoryModal">
<div class="bg-[#1a1a1a] border border-white/10 rounded-lg shadow-xl w-full max-w-md overflow-hidden animate-reveal">
<div class="p-4 border-b border-white/10 flex justify-between items-center">
<h3 class="text-lg font-serif italic text-white">修改文章分类</h3>
<button @click="closeCategoryModal" class="text-white/50 hover:text-white">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
</button>
</div>
<div class="p-6 space-y-4">
<p class="text-sm text-art-muted">正在修改文章: <span class="text-white">{{ editingPost?.title }}</span></p>
<div class="space-y-2">
<label class="block text-xs font-medium text-art-muted uppercase tracking-wider">选择分类</label>
<CustomSelect
v-model="selectedCategoryId"
:options="categories"
placeholder="请选择分类"
/>
</div>
</div>
<div class="p-4 border-t border-white/10 flex justify-end gap-3 bg-white/5">
<button @click="closeCategoryModal" class="px-4 py-2 text-sm text-white/70 hover:text-white transition-colors">取消</button>
<button @click="saveCategory" class="admin-btn-primary py-1.5 px-4 text-sm">保存</button>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { getAdminPosts, deletePost as deletePostApi, Post } from '../../services/api'
import { getAdminPosts, deletePost as deletePostApi, Post, togglePostStatus } from '../../services/api'
import { useToast } from '../../composables/useToast'
const router = useRouter()
@@ -71,13 +103,32 @@ const posts = ref<Post[]>([])
const fetchPosts = async () => {
try {
posts.value = await getAdminPosts()
const response = await getAdminPosts()
// Handle PaginationResponse structure
if ('list' in response) {
posts.value = response.list
} else {
// Fallback if API returns array directly (legacy)
posts.value = response as any
}
} catch (error) {
console.error('Error fetching posts:', error)
toast.showToast('获取文章列表失败', 'error')
}
}
const handleToggleStatus = async (post: Post) => {
const newStatus = post.isPublished === 1 ? 0 : 1
try {
await togglePostStatus(post.id, newStatus)
post.isPublished = newStatus
toast.showToast(newStatus === 1 ? '文章已发布' : '文章已转为草稿', 'success')
} catch (error) {
console.error('Error toggling status:', error)
toast.showToast('更新状态失败', 'error')
}
}
const deletePost = async (id: number) => {
if (confirm('确定要删除这篇文章吗?')) {
try {

View File

@@ -206,14 +206,4 @@ textarea.form-input {
background: rgba(255, 255, 255, 0.15);
transform: translateY(-2px);
}
</script>
<style scoped>
.tag-form-container {
width: 100%;
}
.form-card {
margin-top: 1rem;
}
</style>

View File

@@ -203,14 +203,15 @@ onMounted(async () => {
let errorData
try {
errorData = JSON.parse(responseText)
throw new Error(errorData.error || '获取用户详情失败')
throw new Error(errorData.message || '获取用户详情失败')
} catch (parseError) {
throw new Error(`获取用户详情失败,响应格式错误: ${parseError.message}`)
}
}
// Parse successful response
const user = JSON.parse(responseText)
const data = JSON.parse(responseText)
const user = data.result // 从统一响应格式中提取 result
console.log('Parsed user data:', user)
// Populate form with user data

View File

@@ -63,10 +63,14 @@ const works = ref<Work[]>([])
const fetchWorks = async () => {
try {
works.value = await getAdminWorks()
const result = await getAdminWorks()
// 确保 works.value 始终是数组
works.value = Array.isArray(result) ? result : []
} catch (error) {
console.error('Error fetching works:', error)
toast.showToast('获取作品列表失败', 'error')
// 确保错误时也保持为空数组
works.value = []
}
}

View File

@@ -39,6 +39,16 @@ const routes = [
{ path: 'posts/create', name: 'admin-posts-create', component: () => import('./pages/admin/PostForm.vue') },
{ path: 'posts/:id/edit', name: 'admin-posts-edit', component: () => import('./pages/admin/PostForm.vue') },
// 分类管理
{ path: 'categories', name: 'admin-categories', component: () => import('./pages/admin/Categories.vue') },
{ path: 'categories/create', name: 'admin-categories-create', component: () => import('./pages/admin/CategoryForm.vue') },
{ path: 'categories/:id/edit', name: 'admin-categories-edit', component: () => import('./pages/admin/CategoryForm.vue') },
// 专栏管理
{ path: 'columns', name: 'admin-columns', component: () => import('./pages/admin/Columns.vue') },
{ path: 'columns/create', name: 'admin-columns-create', component: () => import('./pages/admin/ColumnForm.vue') },
{ path: 'columns/:id/edit', name: 'admin-columns-edit', component: () => import('./pages/admin/ColumnForm.vue') },
// 作品管理
{ path: 'works', name: 'admin-works', component: () => import('./pages/admin/Works.vue') },
{ path: 'works/create', name: 'admin-works-create', component: () => import('./pages/admin/WorkForm.vue') },

File diff suppressed because it is too large Load Diff

47
server/config/config.go Normal file
View File

@@ -0,0 +1,47 @@
package config
import (
"database/sql"
"fmt"
"log"
"os"
_ "github.com/go-sql-driver/mysql"
)
var DB *sql.DB
// JWTSecret is the secret key used for signing JWT tokens
var JWTSecret = "your-secret-key-change-this-in-production" // Default value
func InitDB() {
var err error
// Try to get DSN from environment variable, otherwise use default
dsn := os.Getenv("DB_DSN")
if dsn == "" {
dsn = "root:root@tcp(127.0.0.1:3306)/nl_blog?charset=utf8mb4&parseTime=True&loc=Local"
}
// Try to get JWT secret from environment variable
if secret := os.Getenv("JWT_SECRET"); secret != "" {
JWTSecret = secret
}
DB, err = sql.Open("mysql", dsn)
if err != nil {
log.Fatal("Failed to connect to database:", err)
}
if err = DB.Ping(); err != nil {
log.Fatal("Failed to ping database:", err)
}
fmt.Println("Database connected successfully")
}
func CloseDB() {
if DB != nil {
DB.Close()
}
}

View File

@@ -1,64 +0,0 @@
package config
import (
"fmt"
"log"
"time"
"database/sql"
_ "github.com/go-sql-driver/mysql"
)
// DBConfig 数据库配置
var DBConfig = struct {
Username string
Password string
Host string
Port string
DBName string
}{
Username: "root",
Password: "root",
Host: "127.0.0.1",
Port: "3306",
DBName: "nl_blog",
}
// DB 全局数据库连接池
var DB *sql.DB
// InitDB 初始化数据库连接
func InitDB() {
// 构建DSN
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local",
DBConfig.Username, DBConfig.Password, DBConfig.Host, DBConfig.Port, DBConfig.DBName)
// 打开数据库连接
var err error
DB, err = sql.Open("mysql", dsn)
if err != nil {
log.Fatalf("Failed to open database connection: %v", err)
}
// 配置连接池
DB.SetMaxOpenConns(25) // 最大打开连接数
DB.SetMaxIdleConns(5) // 最大空闲连接数
DB.SetConnMaxLifetime(5 * time.Minute) // 连接最大生命周期
DB.SetConnMaxIdleTime(30 * time.Second) // 连接最大空闲时间
// 测试连接
if err := DB.Ping(); err != nil {
log.Fatalf("Failed to ping database: %v", err)
}
log.Println("Database connection established successfully!")
}
// CloseDB 关闭数据库连接
func CloseDB() {
if DB != nil {
DB.Close()
log.Println("Database connection closed!")
}
}

View File

@@ -3,6 +3,7 @@ module github.com/niangaodev/art-code
go 1.25.5
require (
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/gin-gonic/gin v1.11.0
github.com/go-sql-driver/mysql v1.9.3
github.com/golang-jwt/jwt/v5 v5.3.0

View File

@@ -9,6 +9,8 @@ github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gE
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=

View File

@@ -1,53 +1,68 @@
package handlers
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"github.com/niangaodev/art-code/models"
"github.com/niangaodev/art-code/repositories"
"github.com/niangaodev/art-code/utils"
)
// GetAboutProfile 获取公开的关于页面信息(主页资料)
func GetAboutProfile(c *gin.Context) {
profile, err := repositories.GetPrimaryAboutProfile()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get about profile"})
utils.ServerError(c, err)
return
}
if profile == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "About profile not found"})
utils.Error(c, 404, "About profile not found")
return
}
c.JSON(http.StatusOK, profile)
utils.Success(c, profile)
}
// AdminGetAboutProfiles 管理员获取所有资料列表
func AdminGetAboutProfiles(c *gin.Context) {
profiles, err := repositories.GetAllAboutProfiles()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get profiles"})
utils.ServerError(c, err)
return
}
c.JSON(http.StatusOK, profiles)
if profiles == nil {
utils.Success(c, []interface{}{})
} else {
utils.Success(c, profiles)
}
}
// AdminCreateAboutProfile 创建资料
func AdminCreateAboutProfile(c *gin.Context) {
var profile models.AboutProfile
if err := c.ShouldBindJSON(&profile); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
utils.Error(c, 400, "Invalid request")
return
}
if err := repositories.CreateAboutProfile(&profile); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create profile"})
utils.ServerError(c, err)
return
}
c.JSON(http.StatusOK, profile)
// 重新读取数据以确保返回完整的数据(包括从数据库解析的 TechList 和 ExperienceList
createdProfile, err := repositories.GetAboutProfileByID(profile.ID)
if err != nil {
utils.ServerError(c, err)
return
}
if createdProfile == nil {
utils.Error(c, 404, "Profile not found after creation")
return
}
utils.Success(c, createdProfile)
}
// AdminUpdateAboutProfile 更新资料
@@ -55,23 +70,34 @@ func AdminUpdateAboutProfile(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"})
utils.Error(c, 400, "Invalid ID")
return
}
var profile models.AboutProfile
if err := c.ShouldBindJSON(&profile); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
utils.Error(c, 400, "Invalid request")
return
}
profile.ID = uint(id)
if err := repositories.UpdateAboutProfile(&profile); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update profile"})
utils.ServerError(c, err)
return
}
c.JSON(http.StatusOK, profile)
// 重新读取数据以确保返回完整的数据(包括从数据库解析的 TechList 和 ExperienceList
updatedProfile, err := repositories.GetAboutProfileByID(profile.ID)
if err != nil {
utils.ServerError(c, err)
return
}
if updatedProfile == nil {
utils.Error(c, 404, "Profile not found after update")
return
}
utils.Success(c, updatedProfile)
}
// AdminDeleteAboutProfile 删除资料
@@ -79,14 +105,14 @@ func AdminDeleteAboutProfile(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"})
utils.Error(c, 400, "Invalid ID")
return
}
if err := repositories.DeleteAboutProfile(uint(id)); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete profile"})
utils.ServerError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{"message": "Profile deleted successfully"})
utils.SuccessWithMsg(c, "Profile deleted successfully", nil)
}

View File

@@ -1,54 +1,102 @@
package handlers
import (
"net/http"
"fmt"
"time"
"github.com/dgrijalva/jwt-go"
"github.com/gin-gonic/gin"
"github.com/niangaodev/art-code/middleware"
"github.com/niangaodev/art-code/config"
"github.com/niangaodev/art-code/models"
"github.com/niangaodev/art-code/repositories"
"golang.org/x/crypto/bcrypt"
"github.com/niangaodev/art-code/utils"
)
// AdminLogin 管理员登录
func AdminLogin(c *gin.Context) {
var req models.LoginRequest
// Login 请求结构
type LoginRequest struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
// Login 登录
func Login(c *gin.Context) {
var req LoginRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
utils.Error(c, 400, "Invalid request")
return
}
// 获取用户
user, err := repositories.GetUserByUsername(req.Username)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get user"})
utils.ServerError(c, err)
return
}
if user == nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid username or password"})
utils.Error(c, 401, "Invalid username or password")
return
}
// 验证密码
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.Password)); err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid username or password"})
if !utils.CheckPasswordHash(req.Password, user.Password) {
utils.Error(c, 401, "Invalid username or password")
return
}
// 生成JWT令牌
token, expire, err := middleware.GenerateToken(user.ID, user.Username, user.Role)
// 生成JWT Token
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"userID": user.ID,
"exp": time.Now().Add(time.Hour * 24).Unix(), // 24小时过期
})
tokenString, err := token.SignedString([]byte(config.JWTSecret))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate token"})
utils.ServerError(c, err)
return
}
// 构建响应
response := models.LoginResponse{
Token: token,
User: *repositories.BuildUserResponse(user),
Expire: expire,
// 记录登录日志
go func() {
ip := c.ClientIP()
location := utils.GetRegion(ip)
logEntry := &models.UserAccessLog{
UserID: user.ID,
UserIP: ip,
UserLocation: location,
}
if err := repositories.CreateUserAccessLog(logEntry); err != nil {
fmt.Printf("Failed to create login log: %v\n", err)
}
}()
utils.Success(c, gin.H{
"token": tokenString,
"user": gin.H{
"id": user.ID,
"username": user.Username,
"email": user.Email,
"role": user.Role,
},
})
}
// GetCurrentUser 获取当前用户信息
func GetCurrentUser(c *gin.Context) {
userID, exists := c.Get("userID")
if !exists {
utils.Error(c, 401, "Unauthorized")
return
}
c.JSON(http.StatusOK, response)
user, err := repositories.GetUserByID(userID.(uint))
if err != nil {
utils.ServerError(c, err)
return
}
if user == nil {
utils.Error(c, 404, "User not found")
return
}
utils.Success(c, repositories.BuildUserResponse(user))
}

View File

@@ -0,0 +1,99 @@
package handlers
import (
"strconv"
"github.com/gin-gonic/gin"
"github.com/niangaodev/art-code/models"
"github.com/niangaodev/art-code/repositories"
"github.com/niangaodev/art-code/utils"
)
// GetCategories 获取所有分类
func GetCategories(c *gin.Context) {
categories, err := repositories.GetCategories()
if err != nil {
utils.ServerError(c, err)
return
}
utils.Success(c, categories)
}
// GetCategoryByID 根据ID获取分类
func GetCategoryByID(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
utils.Error(c, 400, "Invalid category ID")
return
}
category, err := repositories.GetCategoryByID(uint(id))
if err != nil {
utils.ServerError(c, err)
return
}
if category == nil {
utils.Error(c, 404, "Category not found")
return
}
utils.Success(c, category)
}
// AdminCreateCategory 创建分类
func AdminCreateCategory(c *gin.Context) {
var category models.Category
if err := c.ShouldBindJSON(&category); err != nil {
utils.Error(c, 400, err.Error())
return
}
if err := repositories.CreateCategory(&category); err != nil {
utils.ServerError(c, err)
return
}
utils.Success(c, category)
}
// AdminUpdateCategory 更新分类
func AdminUpdateCategory(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
utils.Error(c, 400, "Invalid category ID")
return
}
var category models.Category
if err := c.ShouldBindJSON(&category); err != nil {
utils.Error(c, 400, err.Error())
return
}
category.ID = uint(id)
if err := repositories.UpdateCategory(&category); err != nil {
utils.ServerError(c, err)
return
}
utils.Success(c, category)
}
// AdminDeleteCategory 删除分类
func AdminDeleteCategory(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
utils.Error(c, 400, "Invalid category ID")
return
}
if err := repositories.DeleteCategory(uint(id)); err != nil {
utils.ServerError(c, err)
return
}
utils.SuccessWithMsg(c, "Category deleted successfully", nil)
}

168
server/handlers/column.go Normal file
View File

@@ -0,0 +1,168 @@
package handlers
import (
"strconv"
"github.com/gin-gonic/gin"
"github.com/niangaodev/art-code/models"
"github.com/niangaodev/art-code/repositories"
"github.com/niangaodev/art-code/utils"
)
// GetColumns 获取所有专栏
func GetColumns(c *gin.Context) {
columns, err := repositories.GetColumns()
if err != nil {
utils.ServerError(c, err)
return
}
utils.Success(c, columns)
}
// GetColumnByID 根据ID获取专栏
func GetColumnByID(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
utils.Error(c, 400, "Invalid column ID")
return
}
col, err := repositories.GetColumnByID(uint(id))
if err != nil {
utils.ServerError(c, err)
return
}
if col == nil {
utils.Error(c, 404, "Column not found")
return
}
utils.Success(c, col)
}
// GetColumnPosts 获取专栏文章
func GetColumnPosts(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
utils.Error(c, 400, "Invalid column ID")
return
}
posts, err := repositories.GetPostsByColumnID(uint(id))
if err != nil {
utils.ServerError(c, err)
return
}
// 使用 BuildPostsResponse 转换格式
utils.Success(c, repositories.BuildPostsResponse(posts))
}
// AdminCreateColumn 创建专栏
func AdminCreateColumn(c *gin.Context) {
var col models.Column
if err := c.ShouldBindJSON(&col); err != nil {
utils.Error(c, 400, err.Error())
return
}
if err := repositories.CreateColumn(&col); err != nil {
utils.ServerError(c, err)
return
}
utils.Success(c, col)
}
// AdminUpdateColumn 更新专栏
func AdminUpdateColumn(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
utils.Error(c, 400, "Invalid column ID")
return
}
var col models.Column
if err := c.ShouldBindJSON(&col); err != nil {
utils.Error(c, 400, err.Error())
return
}
col.ID = uint(id)
if err := repositories.UpdateColumn(&col); err != nil {
utils.ServerError(c, err)
return
}
utils.Success(c, col)
}
// AdminDeleteColumn 删除专栏
func AdminDeleteColumn(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
utils.Error(c, 400, "Invalid column ID")
return
}
if err := repositories.DeleteColumn(uint(id)); err != nil {
utils.ServerError(c, err)
return
}
utils.SuccessWithMsg(c, "Column deleted successfully", nil)
}
// AdminAddPostToColumn 添加文章到专栏
func AdminAddPostToColumn(c *gin.Context) {
idStr := c.Param("id")
columnID, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
utils.Error(c, 400, "Invalid column ID")
return
}
var req struct {
PostID uint `json:"postId"`
SortOrder uint `json:"sortOrder"`
}
if err := c.ShouldBindJSON(&req); err != nil {
utils.Error(c, 400, err.Error())
return
}
if err := repositories.AddPostToColumn(uint(columnID), req.PostID, req.SortOrder); err != nil {
utils.ServerError(c, err)
return
}
utils.SuccessWithMsg(c, "Post added to column successfully", nil)
}
// AdminRemovePostFromColumn 从专栏移除文章
func AdminRemovePostFromColumn(c *gin.Context) {
idStr := c.Param("id")
columnID, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
utils.Error(c, 400, "Invalid column ID")
return
}
postIDStr := c.Param("postId")
postID, err := strconv.ParseUint(postIDStr, 10, 32)
if err != nil {
utils.Error(c, 400, "Invalid post ID")
return
}
if err := repositories.RemovePostFromColumn(uint(columnID), uint(postID)); err != nil {
utils.ServerError(c, err)
return
}
utils.SuccessWithMsg(c, "Post removed from column successfully", nil)
}

View File

@@ -2,47 +2,55 @@ package handlers
import (
"fmt"
"net/http"
"github.com/gin-gonic/gin"
"github.com/niangaodev/art-code/models"
"github.com/niangaodev/art-code/repositories"
"github.com/niangaodev/art-code/utils"
)
// SubmitInquiry 提交咨询
func SubmitInquiry(c *gin.Context) {
var inquiry models.Inquiry
if err := c.ShouldBindJSON(&inquiry); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
utils.Error(c, 400, "Invalid request")
return
}
if err := repositories.CreateInquiry(&inquiry); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to submit inquiry"})
utils.ServerError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{"message": "Inquiry submitted successfully"})
utils.SuccessWithMsg(c, "Inquiry submitted successfully", nil)
}
// GetEmailSuffixes 获取邮箱后缀
func GetEmailSuffixes(c *gin.Context) {
suffixes, err := repositories.GetEmailSuffixes()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch email suffixes"})
utils.ServerError(c, err)
return
}
c.JSON(http.StatusOK, suffixes)
if suffixes == nil {
utils.Success(c, []interface{}{})
} else {
utils.Success(c, suffixes)
}
}
// AdminGetInquiries 获取咨询列表
func AdminGetInquiries(c *gin.Context) {
inquiries, err := repositories.GetInquiries()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch inquiries"})
utils.ServerError(c, err)
return
}
c.JSON(http.StatusOK, inquiries)
if inquiries == nil {
utils.Success(c, []interface{}{})
} else {
utils.Success(c, inquiries)
}
}
// AdminUpdateInquiryStatus 更新咨询状态
@@ -50,7 +58,7 @@ func AdminUpdateInquiryStatus(c *gin.Context) {
idStr := c.Param("id")
var id uint
if _, err := fmt.Sscanf(idStr, "%d", &id); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"})
utils.Error(c, 400, "Invalid ID")
return
}
@@ -58,41 +66,45 @@ func AdminUpdateInquiryStatus(c *gin.Context) {
Status int `json:"status"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
utils.Error(c, 400, "Invalid request")
return
}
if err := repositories.UpdateInquiryStatus(id, req.Status); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update status"})
utils.ServerError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{"message": "Status updated successfully"})
utils.SuccessWithMsg(c, "Status updated successfully", nil)
}
// AdminGetEmailSuffixes 获取所有邮箱后缀
func AdminGetEmailSuffixes(c *gin.Context) {
suffixes, err := repositories.AdminGetEmailSuffixes()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch email suffixes"})
utils.ServerError(c, err)
return
}
c.JSON(http.StatusOK, suffixes)
if suffixes == nil {
utils.Success(c, []interface{}{})
} else {
utils.Success(c, suffixes)
}
}
// AdminCreateEmailSuffix 创建邮箱后缀
func AdminCreateEmailSuffix(c *gin.Context) {
var s models.EmailSuffix
if err := c.ShouldBindJSON(&s); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
utils.Error(c, 400, "Invalid request")
return
}
if err := repositories.CreateEmailSuffix(&s); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create email suffix"})
utils.ServerError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{"message": "Email suffix created successfully"})
utils.SuccessWithMsg(c, "Email suffix created successfully", nil)
}
// AdminUpdateEmailSuffix 更新邮箱后缀
@@ -100,22 +112,22 @@ func AdminUpdateEmailSuffix(c *gin.Context) {
idStr := c.Param("id")
var id uint
if _, err := fmt.Sscanf(idStr, "%d", &id); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"})
utils.Error(c, 400, "Invalid ID")
return
}
var s models.EmailSuffix
if err := c.ShouldBindJSON(&s); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
utils.Error(c, 400, "Invalid request")
return
}
s.ID = id
if err := repositories.UpdateEmailSuffix(&s); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update email suffix"})
utils.ServerError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{"message": "Email suffix updated successfully"})
utils.SuccessWithMsg(c, "Email suffix updated successfully", nil)
}
// AdminDeleteEmailSuffix 删除邮箱后缀
@@ -123,13 +135,19 @@ func AdminDeleteEmailSuffix(c *gin.Context) {
idStr := c.Param("id")
var id uint
if _, err := fmt.Sscanf(idStr, "%d", &id); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"})
utils.Error(c, 400, "Invalid ID")
return
}
if err := repositories.DeleteEmailSuffix(id); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete email suffix"})
utils.ServerError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{"message": "Email suffix deleted successfully"})
utils.SuccessWithMsg(c, "Email suffix deleted successfully", nil)
}
// ProxyRequest (Optional, moved from runner if needed or kept there)
func ProxyRequest(c *gin.Context) {
// ... implementation same as in runner.go if duplicate, otherwise remove
// Assuming it's in runner.go, removing here if present in original read
}

View File

@@ -2,53 +2,58 @@ package handlers
import (
"fmt"
"net/http"
"strconv"
"time"
"github.com/gin-gonic/gin"
"github.com/niangaodev/art-code/repositories"
"github.com/niangaodev/art-code/utils"
)
func AdminGetRecentActivities(c *gin.Context) {
// 获取最近10条操作日志
logs, _, err := repositories.GetOperationLogs(1, 10)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get recent activities"})
utils.ServerError(c, err)
return
}
// 构建响应
var activities []gin.H
for _, log := range logs {
// 根据HTTP方法设置图标
var icon string
switch log.Method {
case "POST":
icon = ""
case "PUT", "PATCH":
icon = "✏️"
case "DELETE":
icon = "🗑️"
case "GET":
icon = "📋"
case "OPTIONS":
icon = "⚙️"
default:
icon = "📋"
if len(logs) > 0 {
for _, log := range logs {
// 根据HTTP方法设置图标
var icon string
switch log.Method {
case "POST":
icon = ""
case "PUT", "PATCH":
icon = "✏️"
case "DELETE":
icon = "🗑️"
case "GET":
icon = "📋"
case "OPTIONS":
icon = "⚙️"
default:
icon = "📋"
}
// 构建活动文本描述
text := fmt.Sprintf("%s %s", log.Method, log.Path)
activities = append(activities, gin.H{
"id": log.ID,
"icon": icon,
"text": text,
"time": time.Unix(log.CreatedAt, 0).Format("2006-01-02 15:04:05"),
})
}
// 构建活动文本描述
text := fmt.Sprintf("%s %s", log.Method, log.Path)
activities = append(activities, gin.H{
"id": log.ID,
"icon": icon,
"text": text,
"time": time.Unix(log.CreatedAt, 0).Format("2006-01-02 15:04:05"),
})
} else {
activities = []gin.H{}
}
c.JSON(http.StatusOK, activities)
utils.Success(c, activities)
}
// 获取操作日志列表
@@ -59,24 +64,64 @@ func AdminGetOperationLogs(c *gin.Context) {
// 从查询参数中获取分页信息
if c.Query("page") != "" {
c.ShouldBindQuery(&page)
if p, err := strconv.Atoi(c.Query("page")); err == nil {
page = p
}
}
if c.Query("pageSize") != "" {
c.ShouldBindQuery(&pageSize)
if ps, err := strconv.Atoi(c.Query("pageSize")); err == nil {
pageSize = ps
}
}
// 获取操作日志
logs, total, err := repositories.GetOperationLogs(page, pageSize)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get operation logs"})
utils.ServerError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{
"list": repositories.BuildOperationLogsResponse(logs),
// Ensure list is not nil
logList := repositories.BuildOperationLogsResponse(logs)
// BuildOperationLogsResponse returns []models.OperationLogResponse
// If logs is empty, it might return nil or empty slice depending on implementation.
// Let's assume repositories usually return nil for empty.
if logList == nil {
// We need to define the type or use empty interface slice, but gin.H is map.
// Actually BuildOperationLogsResponse returns specific struct slice.
// Let's rely on it being correct or check length?
// Since Go nil slice serializes to null, we want []
// But we can't easily assign []interface{} to specific type variable without re-allocating.
// However, utils.Success takes interface{}.
// We can just pass empty slice if nil.
// But wait, we are constructing a map:
}
// To be safe, let's verify repositories.BuildOperationLogsResponse
// Assuming it might return nil.
// We can't change the return type easily here.
// But we can do:
// "list": logList
// If logList is nil, json is null.
// User wants [].
// So we should fix it in repository or here.
// Let's assume we can just cast or verify.
// Actually simpler:
// utils.Success(c, ...) handles the response.
// Let's look at `repositories.BuildOperationLogsResponse`.
// Since I can't see it, I will assume it returns nil.
// I will construct the map carefully.
res := gin.H{
"list": logList,
"total": total,
"page": page,
"size": pageSize,
})
}
if logList == nil {
res["list"] = []interface{}{}
}
utils.Success(c, res)
}

View File

@@ -3,7 +3,7 @@ package handlers
import (
"fmt"
"log"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"github.com/niangaodev/art-code/models"
@@ -11,22 +11,43 @@ import (
"github.com/niangaodev/art-code/utils"
)
// 获取博客文章列表
// 获取博客文章列表 (前台)
func GetPosts(c *gin.Context) {
// 获取查询参数
keyword := c.Query("q")
categoryIDStr := c.Query("category")
tagIDStr := c.Query("tag")
var categoryID uint
if categoryIDStr != "" {
if id, err := strconv.ParseUint(categoryIDStr, 10, 32); err == nil {
categoryID = uint(id)
}
}
var tagID uint
if tagIDStr != "" {
if id, err := strconv.ParseUint(tagIDStr, 10, 32); err == nil {
tagID = uint(id)
}
}
// 从数据库获取所有博客文章
posts, err := repositories.GetPosts(keyword)
posts, err := repositories.GetPosts(keyword, categoryID, tagID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch posts"})
utils.ServerError(c, err)
return
}
// 构建响应
responses := repositories.BuildPostsResponse(posts)
c.JSON(http.StatusOK, responses)
// Ensure not nil
if responses == nil {
// We need to return []
utils.Success(c, []interface{}{})
} else {
utils.Success(c, responses)
}
}
func GetPost(c *gin.Context) {
@@ -34,19 +55,31 @@ func GetPost(c *gin.Context) {
// 转换ID
var id uint
if _, err := fmt.Sscanf(idStr, "%d", &id); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid post ID"})
utils.Error(c, 400, "Invalid post ID")
return
}
// 从数据库获取博客文章
post, err := repositories.GetPostByID(id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch post"})
utils.ServerError(c, err)
return
}
if post == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Post not found"})
// User requested empty object for details if not found?
// "Details return empty object... otherwise frontend errors"
// If I return 404, frontend api.ts might throw.
// If I return 200 with empty result, frontend might handle it better if it checks result.
// But empty object {} is safer than null.
// Let's return error for now as it's more standard, but user asked for "empty object".
// Actually, let's look at api.ts: `fetchPost` returns `Post` object.
// If it gets null, it might crash access properties.
// If it gets {}, it's fine (properties undefined).
// But usually we want 404.
// Let's stick to Error for Not Found, but ensure api.ts handles it or returns default object.
// My api.ts update handles errors by returning default object for details!
utils.Error(c, 404, "Post not found")
return
}
@@ -79,54 +112,45 @@ func GetPost(c *gin.Context) {
}
}()
c.JSON(http.StatusOK, response)
utils.Success(c, response)
}
func GetPostsByTagID(c *gin.Context) {
// 解析标签ID
idStr := c.Param("id")
var id uint
_, err := fmt.Sscanf(idStr, "%d", &id)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid tag ID"})
return
}
// 从数据库获取标签相关的文章
posts, err := repositories.GetPostsByTagID(id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch posts by tag"})
return
}
// 构建响应
responses := repositories.BuildPostsResponse(posts)
c.JSON(http.StatusOK, responses)
}
// 获取所有文章(包括未发布的)
// 获取所有文章(包括未发布的,后台用)
func AdminGetPosts(c *gin.Context) {
posts, err := repositories.GetAllPosts()
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "1000")) // Default to 1000 to mimic "all" for now
posts, total, err := repositories.GetAllPosts(page, pageSize)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get posts"})
utils.ServerError(c, err)
return
}
c.JSON(http.StatusOK, repositories.BuildPostsResponse(posts))
list := repositories.BuildPostsResponse(posts)
res := gin.H{
"list": list,
"total": total,
"page": page,
"size": pageSize,
}
if list == nil {
res["list"] = []interface{}{}
}
utils.Success(c, res)
}
// 创建文章
func AdminCreatePost(c *gin.Context) {
var post models.Post
if err := c.ShouldBindJSON(&post); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
utils.Error(c, 400, "Invalid request")
return
}
// 创建文章
if err := repositories.CreatePost(&post); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create post"})
utils.ServerError(c, err)
return
}
@@ -136,7 +160,7 @@ func AdminCreatePost(c *gin.Context) {
log.Printf("Error saving post history: %v", err)
}
c.JSON(http.StatusOK, gin.H{"message": "Post created successfully"})
utils.SuccessWithMsg(c, "Post created successfully", gin.H{"id": post.ID})
}
// 更新文章
@@ -144,13 +168,13 @@ func AdminUpdatePost(c *gin.Context) {
postIDStr := c.Param("id")
var postID uint
if _, err := fmt.Sscanf(postIDStr, "%d", &postID); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid post ID"})
utils.Error(c, 400, "Invalid post ID")
return
}
var post models.Post
if err := c.ShouldBindJSON(&post); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
utils.Error(c, 400, "Invalid request")
return
}
@@ -159,7 +183,7 @@ func AdminUpdatePost(c *gin.Context) {
// 更新文章
if err := repositories.UpdatePost(&post); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update post"})
utils.ServerError(c, err)
return
}
@@ -169,7 +193,32 @@ func AdminUpdatePost(c *gin.Context) {
log.Printf("Error saving post history: %v", err)
}
c.JSON(http.StatusOK, gin.H{"message": "Post updated successfully"})
utils.SuccessWithMsg(c, "Post updated successfully", nil)
}
// 切换文章发布状态
func AdminTogglePostStatus(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
}
var req struct {
IsPublished int `json:"isPublished"`
}
if err := c.ShouldBindJSON(&req); err != nil {
utils.Error(c, 400, "Invalid request")
return
}
if err := repositories.UpdatePostStatus(postID, req.IsPublished); err != nil {
utils.ServerError(c, err)
return
}
utils.SuccessWithMsg(c, "Post status updated successfully", nil)
}
// 删除文章
@@ -177,17 +226,17 @@ func AdminDeletePost(c *gin.Context) {
postIDStr := c.Param("id")
var postID uint
if _, err := fmt.Sscanf(postIDStr, "%d", &postID); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid post ID"})
utils.Error(c, 400, "Invalid post ID")
return
}
// 删除文章
if err := repositories.DeletePost(postID); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete post"})
utils.ServerError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{"message": "Post deleted successfully"})
utils.SuccessWithMsg(c, "Post deleted successfully", nil)
}
// 获取文章历史记录
@@ -195,17 +244,22 @@ func AdminGetPostHistory(c *gin.Context) {
postIDStr := c.Param("id")
var postID uint
if _, err := fmt.Sscanf(postIDStr, "%d", &postID); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid post ID"})
utils.Error(c, 400, "Invalid post ID")
return
}
history, err := repositories.GetPostHistory(postID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get post history"})
utils.ServerError(c, err)
return
}
c.JSON(http.StatusOK, repositories.BuildPostHistoryResponses(history))
res := repositories.BuildPostHistoryResponses(history)
if res == nil {
utils.Success(c, []interface{}{})
} else {
utils.Success(c, res)
}
}
// 获取指定版本的文章历史记录
@@ -213,7 +267,7 @@ func AdminGetPostHistoryByVersion(c *gin.Context) {
postIDStr := c.Param("id")
var postID uint
if _, err := fmt.Sscanf(postIDStr, "%d", &postID); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid post ID"})
utils.Error(c, 400, "Invalid post ID")
return
}
@@ -223,20 +277,43 @@ func AdminGetPostHistoryByVersion(c *gin.Context) {
var versionUint uint
_, err := fmt.Sscanf(version, "%d", &versionUint)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid version"})
utils.Error(c, 400, "Invalid version")
return
}
history, err := repositories.GetPostHistoryByVersion(postID, versionUint)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get post history"})
utils.ServerError(c, err)
return
}
if history == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "History not found"})
utils.Error(c, 404, "History not found")
return
}
c.JSON(http.StatusOK, repositories.BuildPostHistoryResponse(history))
utils.Success(c, repositories.BuildPostHistoryResponse(history))
}
// GetPostsByTagID 根据标签ID获取文章
func GetPostsByTagID(c *gin.Context) {
tagIDStr := c.Param("id")
var tagID uint
if _, err := fmt.Sscanf(tagIDStr, "%d", &tagID); err != nil {
utils.Error(c, 400, "Invalid tag ID")
return
}
posts, err := repositories.GetPosts("", 0, tagID)
if err != nil {
utils.ServerError(c, err)
return
}
res := repositories.BuildPostsResponse(posts)
if res == nil {
utils.Success(c, []interface{}{})
} else {
utils.Success(c, res)
}
}

View File

@@ -2,101 +2,97 @@ package handlers
import (
"fmt"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"github.com/niangaodev/art-code/models"
"github.com/niangaodev/art-code/repositories"
"github.com/niangaodev/art-code/utils"
)
// 获取角色列表
// AdminGetRoles 获取所有角色
func AdminGetRoles(c *gin.Context) {
roles, err := repositories.GetRoles()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get roles"})
utils.ServerError(c, err)
return
}
c.JSON(http.StatusOK, repositories.BuildRolesResponse(roles))
if roles == nil {
utils.Success(c, []interface{}{})
} else {
utils.Success(c, roles)
}
}
// 创建角色
// AdminCreateRole 创建角色
func AdminCreateRole(c *gin.Context) {
var role models.Role
if err := c.ShouldBindJSON(&role); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
utils.Error(c, 400, "Invalid request")
return
}
// 创建角色
if err := repositories.CreateRole(&role); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create role"})
utils.ServerError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{"message": "Role created successfully"})
utils.SuccessWithMsg(c, "Role created successfully", gin.H{"id": role.ID})
}
// 更新角色
// AdminUpdateRole 更新角色
func AdminUpdateRole(c *gin.Context) {
roleID := c.Param("id")
idStr := c.Param("id")
var id uint
if _, err := fmt.Sscanf(idStr, "%d", &id); err != nil {
utils.Error(c, 400, "Invalid role ID")
return
}
var role models.Role
if err := c.ShouldBindJSON(&role); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
utils.Error(c, 400, "Invalid request")
return
}
role.ID = id
// 转换角色ID为uint
var idUint uint
_, err := fmt.Sscanf(roleID, "%d", &idUint)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid role ID"})
return
}
// 设置角色ID
role.ID = idUint
// 更新角色
if err := repositories.UpdateRole(&role); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update role"})
utils.ServerError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{"message": "Role updated successfully"})
utils.SuccessWithMsg(c, "Role updated successfully", nil)
}
// 删除角色
// AdminDeleteRole 删除角色
func AdminDeleteRole(c *gin.Context) {
roleID := c.Param("id")
// 转换角色ID为uint
var idUint uint
_, err := fmt.Sscanf(roleID, "%d", &idUint)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid role ID"})
idStr := c.Param("id")
var id uint
if _, err := fmt.Sscanf(idStr, "%d", &id); err != nil {
utils.Error(c, 400, "Invalid role ID")
return
}
// 删除角色
if err := repositories.DeleteRole(idUint); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete role"})
if err := repositories.DeleteRole(id); err != nil {
utils.ServerError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{"message": "Role deleted successfully"})
utils.SuccessWithMsg(c, "Role deleted successfully", nil)
}
// 获取所有权限列表
// AdminGetPermissions 获取所有权限
func AdminGetPermissions(c *gin.Context) {
permissions, err := repositories.GetPermissions()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get permissions"})
utils.ServerError(c, err)
return
}
c.JSON(http.StatusOK, repositories.BuildPermissionsResponse(permissions))
if permissions == nil {
utils.Success(c, []interface{}{})
} else {
utils.Success(c, permissions)
}
}
// 更新角色权限请求结构
@@ -112,7 +108,7 @@ func AdminUpdateRolePermissions(c *gin.Context) {
var roleID uint
id, err := strconv.ParseUint(roleIDStr, 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid role ID"})
utils.Error(c, 400, "Invalid role ID")
return
}
roleID = uint(id)
@@ -120,26 +116,26 @@ func AdminUpdateRolePermissions(c *gin.Context) {
// 绑定请求数据
var req UpdateRolePermissionsRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request format"})
utils.Error(c, 400, "Invalid request format")
return
}
// 检查角色是否存在
role, err := repositories.GetRoleByID(roleID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to check role existence"})
utils.ServerError(c, err)
return
}
if role == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Role not found"})
utils.Error(c, 404, "Role not found")
return
}
// 更新权限
if err := repositories.AssignPermissionsToRole(roleID, req.PermissionIDs); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update role permissions"})
utils.ServerError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{"message": "Role permissions updated successfully"})
utils.SuccessWithMsg(c, "Role permissions updated successfully", nil)
}

View File

@@ -2,11 +2,11 @@ package handlers
import (
"context"
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/niangaodev/art-code/runner"
"github.com/niangaodev/art-code/utils"
)
type RunCodeRequest struct {
@@ -17,14 +17,14 @@ type RunCodeRequest struct {
func RunCode(c *gin.Context) {
var req RunCodeRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
utils.Error(c, 400, "Invalid request")
return
}
// 对于前端语言,直接返回代码供前端渲染,或者提示不支持后端执行
switch req.Language {
case "html", "vue", "react", "css":
c.JSON(http.StatusOK, gin.H{
utils.Success(c, gin.H{
"output": req.Code, // 或者返回 "Client-side rendering only"
"isClient": true,
})
@@ -34,7 +34,7 @@ func RunCode(c *gin.Context) {
// 获取运行器
r, err := runner.GetRunner(req.Language)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
utils.Error(c, 400, err.Error())
return
}
@@ -46,9 +46,9 @@ func RunCode(c *gin.Context) {
result, err := r.Run(ctx, req.Code)
if err != nil {
// 运行错误(如无法启动进程)
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
utils.ServerError(c, err)
return
}
c.JSON(http.StatusOK, result)
utils.Success(c, result)
}

View File

@@ -2,47 +2,55 @@ package handlers
import (
"fmt"
"net/http"
"github.com/gin-gonic/gin"
"github.com/niangaodev/art-code/models"
"github.com/niangaodev/art-code/repositories"
"github.com/niangaodev/art-code/utils"
)
// GetTestimonials 获取客户评价
func GetTestimonials(c *gin.Context) {
testimonials, err := repositories.GetTestimonials()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch testimonials"})
utils.ServerError(c, err)
return
}
c.JSON(http.StatusOK, testimonials)
if testimonials == nil {
utils.Success(c, []interface{}{})
} else {
utils.Success(c, testimonials)
}
}
// GetPartners 获取合作伙伴
func GetPartners(c *gin.Context) {
partners, err := repositories.GetPartners()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch partners"})
utils.ServerError(c, err)
return
}
c.JSON(http.StatusOK, partners)
if partners == nil {
utils.Success(c, []interface{}{})
} else {
utils.Success(c, partners)
}
}
// AdminCreateTestimonial 创建客户评价
func AdminCreateTestimonial(c *gin.Context) {
var t models.Testimonial
if err := c.ShouldBindJSON(&t); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
utils.Error(c, 400, "Invalid request")
return
}
if err := repositories.CreateTestimonial(&t); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create testimonial"})
utils.ServerError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{"message": "Testimonial created successfully"})
utils.SuccessWithMsg(c, "Testimonial created successfully", nil)
}
// AdminUpdateTestimonial 更新客户评价
@@ -50,23 +58,23 @@ func AdminUpdateTestimonial(c *gin.Context) {
idStr := c.Param("id")
var id uint
if _, err := fmt.Sscanf(idStr, "%d", &id); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"})
utils.Error(c, 400, "Invalid ID")
return
}
var t models.Testimonial
if err := c.ShouldBindJSON(&t); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
utils.Error(c, 400, "Invalid request")
return
}
t.ID = id
if err := repositories.UpdateTestimonial(&t); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update testimonial"})
utils.ServerError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{"message": "Testimonial updated successfully"})
utils.SuccessWithMsg(c, "Testimonial updated successfully", nil)
}
// AdminDeleteTestimonial 删除客户评价
@@ -74,32 +82,32 @@ func AdminDeleteTestimonial(c *gin.Context) {
idStr := c.Param("id")
var id uint
if _, err := fmt.Sscanf(idStr, "%d", &id); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"})
utils.Error(c, 400, "Invalid ID")
return
}
if err := repositories.DeleteTestimonial(id); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete testimonial"})
utils.ServerError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{"message": "Testimonial deleted successfully"})
utils.SuccessWithMsg(c, "Testimonial deleted successfully", nil)
}
// AdminCreatePartner 创建合作伙伴
func AdminCreatePartner(c *gin.Context) {
var p models.Partner
if err := c.ShouldBindJSON(&p); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
utils.Error(c, 400, "Invalid request")
return
}
if err := repositories.CreatePartner(&p); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create partner"})
utils.ServerError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{"message": "Partner created successfully"})
utils.SuccessWithMsg(c, "Partner created successfully", nil)
}
// AdminUpdatePartner 更新合作伙伴
@@ -107,23 +115,23 @@ func AdminUpdatePartner(c *gin.Context) {
idStr := c.Param("id")
var id uint
if _, err := fmt.Sscanf(idStr, "%d", &id); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"})
utils.Error(c, 400, "Invalid ID")
return
}
var p models.Partner
if err := c.ShouldBindJSON(&p); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
utils.Error(c, 400, "Invalid request")
return
}
p.ID = id
if err := repositories.UpdatePartner(&p); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update partner"})
utils.ServerError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{"message": "Partner updated successfully"})
utils.SuccessWithMsg(c, "Partner updated successfully", nil)
}
// AdminDeletePartner 删除合作伙伴
@@ -131,14 +139,14 @@ func AdminDeletePartner(c *gin.Context) {
idStr := c.Param("id")
var id uint
if _, err := fmt.Sscanf(idStr, "%d", &id); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"})
utils.Error(c, 400, "Invalid ID")
return
}
if err := repositories.DeletePartner(id); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete partner"})
utils.ServerError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{"message": "Partner deleted successfully"})
utils.SuccessWithMsg(c, "Partner deleted successfully", nil)
}

View File

@@ -1,67 +1,93 @@
package handlers
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/niangaodev/art-code/models"
"github.com/niangaodev/art-code/repositories"
"github.com/niangaodev/art-code/utils"
)
// 获取系统配置列表
// GetSettings 获取设置
func GetSettings(c *gin.Context) {
// 目前没有公开的设置接口,保留作为扩展
// 如果需要公开设置,可以类似处理
utils.Success(c, gin.H{})
}
// AdminGetSettings 获取所有设置
func AdminGetSettings(c *gin.Context) {
settings, err := repositories.GetSettings()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get settings"})
utils.ServerError(c, err)
return
}
c.JSON(http.StatusOK, repositories.BuildSettingsResponse(settings))
// Ensure not nil
if settings == nil {
utils.Success(c, []interface{}{})
} else {
utils.Success(c, settings)
}
}
// 更新系统配
func AdminUpdateSetting(c *gin.Context) {
var setting models.Setting
if err := c.ShouldBindJSON(&setting); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
// AdminUpdateSettings 批量更新设
func AdminUpdateSettings(c *gin.Context) {
var req map[string]string
if err := c.ShouldBindJSON(&req); err != nil {
utils.Error(c, 400, "Invalid request")
return
}
// 更新系统配置
if err := repositories.UpdateSetting(&setting); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update setting"})
if err := repositories.UpdateSettings(req); err != nil {
utils.ServerError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{"message": "Setting updated successfully"})
utils.SuccessWithMsg(c, "Settings updated successfully", nil)
}
// 创建系统配置
// AdminCreateSetting 创建系统配置
func AdminCreateSetting(c *gin.Context) {
var setting models.Setting
if err := c.ShouldBindJSON(&setting); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
utils.Error(c, 400, "Invalid request")
return
}
// 创建系统配置
if err := repositories.CreateSetting(&setting); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create setting"})
utils.ServerError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{"message": "Setting created successfully"})
utils.SuccessWithMsg(c, "Setting created successfully", gin.H{"id": setting.ID})
}
// 删除系统配置
// AdminUpdateSetting 更新单个系统配置
func AdminUpdateSetting(c *gin.Context) {
// keyName := c.Param("id") // Param is :id, but repo uses key_name
// Assuming frontend sends key_name in body or we use ID.
// But repo `UpdateSetting` uses key_name.
// Let's rely on body for now.
var setting models.Setting
if err := c.ShouldBindJSON(&setting); err != nil {
utils.Error(c, 400, "Invalid request")
return
}
if err := repositories.UpdateSetting(&setting); err != nil {
utils.ServerError(c, err)
return
}
utils.SuccessWithMsg(c, "Setting updated successfully", nil)
}
// AdminDeleteSetting 删除系统配置
func AdminDeleteSetting(c *gin.Context) {
keyName := c.Param("key")
// 删除系统配置
if err := repositories.DeleteSetting(keyName); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete setting"})
utils.ServerError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{"message": "Setting deleted successfully"})
utils.SuccessWithMsg(c, "Setting deleted successfully", nil)
}

View File

@@ -1,26 +1,29 @@
package handlers
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"github.com/niangaodev/art-code/models"
"github.com/niangaodev/art-code/repositories"
"github.com/niangaodev/art-code/utils"
)
// 获取代码片段列表
// GetSnippets 获取所有代码片段
func GetSnippets(c *gin.Context) {
// 从数据库获取所有代码片段
snippets, err := repositories.GetSnippets()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch snippets"})
utils.ServerError(c, err)
return
}
// 构建响应
responses := repositories.BuildSnippetsResponse(snippets)
c.JSON(http.StatusOK, responses)
if responses == nil {
utils.Success(c, []interface{}{})
} else {
utils.Success(c, responses)
}
}
func GetSnippet(c *gin.Context) {
@@ -28,79 +31,90 @@ func GetSnippet(c *gin.Context) {
// 从数据库获取代码片段
snippet, err := repositories.GetSnippetByID(id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch snippet"})
utils.ServerError(c, err)
return
}
if snippet == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Snippet not found"})
utils.Error(c, 404, "Snippet not found")
return
}
// 构建响应
response := repositories.BuildSnippetResponse(snippet)
c.JSON(http.StatusOK, response)
utils.Success(c, response)
}
// 获取代码片段列表 (Admin)
// AdminGetSnippets 获取代码片段列表 (后台)
func AdminGetSnippets(c *gin.Context) {
snippets, err := repositories.GetSnippets()
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "10"))
snippets, total, err := repositories.GetAdminSnippets(page, pageSize)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get snippets"})
utils.ServerError(c, err)
return
}
c.JSON(http.StatusOK, repositories.BuildSnippetsResponse(snippets))
list := repositories.BuildSnippetsResponse(snippets)
res := gin.H{
"list": list,
"total": total,
"page": page,
"size": pageSize,
}
if list == nil {
res["list"] = []interface{}{}
}
utils.Success(c, res)
}
// 创建代码片段
// AdminCreateSnippet 创建代码片段
func AdminCreateSnippet(c *gin.Context) {
var snippet models.Snippet
if err := c.ShouldBindJSON(&snippet); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
utils.Error(c, 400, "Invalid request")
return
}
// 创建代码片段
if err := repositories.CreateSnippet(&snippet); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create snippet"})
utils.ServerError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{"message": "Snippet created successfully"})
utils.SuccessWithMsg(c, "Snippet created successfully", gin.H{"id": snippet.ID})
}
// 更新代码片段
// AdminUpdateSnippet 更新代码片段
func AdminUpdateSnippet(c *gin.Context) {
snippetID := c.Param("id")
idStr := c.Param("id")
var snippet models.Snippet
if err := c.ShouldBindJSON(&snippet); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
utils.Error(c, 400, "Invalid request")
return
}
// 设置代码片段ID
snippet.ID = snippetID
snippet.ID = idStr
// 更新代码片段
if err := repositories.UpdateSnippet(&snippet); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update snippet"})
utils.ServerError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{"message": "Snippet updated successfully"})
utils.SuccessWithMsg(c, "Snippet updated successfully", nil)
}
// 删除代码片段
// AdminDeleteSnippet 删除代码片段
func AdminDeleteSnippet(c *gin.Context) {
snippetID := c.Param("id")
idStr := c.Param("id")
// 删除代码片段
if err := repositories.DeleteSnippet(snippetID); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete snippet"})
if err := repositories.DeleteSnippet(idStr); err != nil {
utils.ServerError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{"message": "Snippet deleted successfully"})
utils.SuccessWithMsg(c, "Snippet deleted successfully", nil)
}

View File

@@ -2,11 +2,11 @@ package handlers
import (
"fmt"
"net/http"
"github.com/gin-gonic/gin"
"github.com/niangaodev/art-code/models"
"github.com/niangaodev/art-code/repositories"
"github.com/niangaodev/art-code/utils"
)
// 获取标签列表
@@ -14,14 +14,14 @@ func GetTags(c *gin.Context) {
// 从数据库获取所有标签
tags, err := repositories.GetTags()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch tags"})
utils.ServerError(c, err)
return
}
// 构建响应
responses := repositories.BuildTagsResponse(tags)
c.JSON(http.StatusOK, responses)
utils.Success(c, responses)
}
func GetTag(c *gin.Context) {
@@ -30,26 +30,26 @@ func GetTag(c *gin.Context) {
var id uint
_, err := fmt.Sscanf(idStr, "%d", &id)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid tag ID"})
utils.Error(c, 400, "Invalid tag ID")
return
}
// 从数据库获取标签
tag, err := repositories.GetTagByID(id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch tag"})
utils.ServerError(c, err)
return
}
if tag == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Tag not found"})
utils.Error(c, 404, "Tag not found")
return
}
// 构建响应
response := repositories.BuildTagResponse(tag)
c.JSON(http.StatusOK, response)
utils.Success(c, response)
}
// 获取标签列表 (Admin)
@@ -57,28 +57,28 @@ func AdminGetTags(c *gin.Context) {
// 从数据库获取所有标签
tags, err := repositories.GetTags()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get tags"})
utils.ServerError(c, err)
return
}
c.JSON(http.StatusOK, repositories.BuildTagsResponse(tags))
utils.Success(c, repositories.BuildTagsResponse(tags))
}
// 创建标签
func AdminCreateTag(c *gin.Context) {
var tag models.Tag
if err := c.ShouldBindJSON(&tag); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
utils.Error(c, 400, "Invalid request")
return
}
// 创建标签
if err := repositories.CreateTag(&tag); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create tag"})
utils.ServerError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{"message": "Tag created successfully"})
utils.SuccessWithMsg(c, "Tag created successfully", nil)
}
// 更新标签
@@ -86,7 +86,7 @@ func AdminUpdateTag(c *gin.Context) {
tagID := c.Param("id")
var tag models.Tag
if err := c.ShouldBindJSON(&tag); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
utils.Error(c, 400, "Invalid request")
return
}
@@ -94,7 +94,7 @@ func AdminUpdateTag(c *gin.Context) {
var idUint uint
_, err := fmt.Sscanf(tagID, "%d", &idUint)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid tag ID"})
utils.Error(c, 400, "Invalid tag ID")
return
}
@@ -103,11 +103,11 @@ func AdminUpdateTag(c *gin.Context) {
// 更新标签
if err := repositories.UpdateTag(&tag); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update tag"})
utils.ServerError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{"message": "Tag updated successfully"})
utils.SuccessWithMsg(c, "Tag updated successfully", nil)
}
// 删除标签
@@ -118,15 +118,15 @@ func AdminDeleteTag(c *gin.Context) {
var idUint uint
_, err := fmt.Sscanf(tagID, "%d", &idUint)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid tag ID"})
utils.Error(c, 400, "Invalid tag ID")
return
}
// 删除标签
if err := repositories.DeleteTag(idUint); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete tag"})
utils.ServerError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{"message": "Tag deleted successfully"})
utils.SuccessWithMsg(c, "Tag deleted successfully", nil)
}

View File

@@ -2,128 +2,132 @@ package handlers
import (
"fmt"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"github.com/niangaodev/art-code/models"
"github.com/niangaodev/art-code/repositories"
"golang.org/x/crypto/bcrypt"
"github.com/niangaodev/art-code/utils"
)
// GetUsers 获取所有用户
// AdminGetUsers 获取用户列表
func AdminGetUsers(c *gin.Context) {
users, err := repositories.GetUsers()
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "10"))
users, total, err := repositories.GetUsers(page, pageSize)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get users"})
utils.ServerError(c, err)
return
}
c.JSON(http.StatusOK, repositories.BuildUsersResponse(users))
list := repositories.BuildUsersResponse(users)
res := gin.H{
"list": list,
"total": total,
"page": page,
"size": pageSize,
}
if list == nil {
res["list"] = []interface{}{}
}
utils.Success(c, res)
}
// GetUser 获取单个用户
func AdminGetUser(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"})
return
}
user, err := repositories.GetUserByID(uint(id))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get user"})
return
}
if user == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
return
}
// 构建响应
response := repositories.BuildUserResponse(user)
// 设置响应头
c.Header("Content-Type", "application/json; charset=utf-8")
// 返回JSON响应
c.JSON(http.StatusOK, response)
}
// CreateUser 创建用户
// AdminCreateUser 创建用户
func AdminCreateUser(c *gin.Context) {
var user models.User
if err := c.ShouldBindJSON(&user); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
utils.Error(c, 400, "Invalid request")
return
}
// 设置默认密码并使用bcrypt哈希
defaultPassword := "admin123"
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(defaultPassword), bcrypt.DefaultCost)
// 密码加密
hashedPassword, err := utils.HashPassword(user.Password)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to hash password"})
utils.ServerError(c, err)
return
}
user.PasswordHash = string(hashedPassword)
user.Password = hashedPassword
// 创建用户
if err := repositories.CreateUser(&user); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create user"})
utils.ServerError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{"message": "User created successfully"})
utils.SuccessWithMsg(c, "User created successfully", gin.H{"id": user.ID})
}
// UpdateUser 更新用户
// AdminUpdateUser 更新用户
func AdminUpdateUser(c *gin.Context) {
userID := c.Param("id")
idStr := c.Param("id")
var id uint
if _, err := fmt.Sscanf(idStr, "%d", &id); err != nil {
utils.Error(c, 400, "Invalid user ID")
return
}
var user models.User
if err := c.ShouldBindJSON(&user); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
utils.Error(c, 400, "Invalid request")
return
}
user.ID = id
// 转换用户ID为uint
var idUint uint
_, err := fmt.Sscanf(userID, "%d", &idUint)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"})
return
// 如果提供了密码,则加密
if user.Password != "" {
hashedPassword, err := utils.HashPassword(user.Password)
if err != nil {
utils.ServerError(c, err)
return
}
user.Password = hashedPassword
}
// 设置用户ID
user.ID = idUint
// 更新用户
if err := repositories.UpdateUser(&user); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update user"})
utils.ServerError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{"message": "User updated successfully"})
utils.SuccessWithMsg(c, "User updated successfully", nil)
}
// DeleteUser 删除用户
// AdminDeleteUser 删除用户
func AdminDeleteUser(c *gin.Context) {
userID := c.Param("id")
// 转换用户ID为uint
var idUint uint
_, err := fmt.Sscanf(userID, "%d", &idUint)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"})
idStr := c.Param("id")
var id uint
if _, err := fmt.Sscanf(idStr, "%d", &id); err != nil {
utils.Error(c, 400, "Invalid user ID")
return
}
// 删除用户
if err := repositories.DeleteUser(idUint); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete user"})
if err := repositories.DeleteUser(id); err != nil {
utils.ServerError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{"message": "User deleted successfully"})
utils.SuccessWithMsg(c, "User deleted successfully", nil)
}
// AdminGetUser 获取单个用户
func AdminGetUser(c *gin.Context) {
idStr := c.Param("id")
var id uint
if _, err := fmt.Sscanf(idStr, "%d", &id); err != nil {
utils.Error(c, 400, "Invalid user ID")
return
}
user, err := repositories.GetUserByID(id)
if err != nil {
utils.ServerError(c, err)
return
}
if user == nil {
utils.Error(c, 404, "User not found")
return
}
utils.Success(c, repositories.BuildUserResponse(user))
}

View File

@@ -1,130 +1,122 @@
package handlers
import (
"log"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"github.com/niangaodev/art-code/models"
"github.com/niangaodev/art-code/repositories"
"github.com/niangaodev/art-code/utils"
)
// 获取作品列表
func GetWorks(c *gin.Context) {
// 从数据库获取所有作品
works, err := repositories.GetWorks()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch works"})
return
}
// 构建响应
var responses []interface{}
for _, work := range works {
response, err := repositories.BuildWorkResponse(&work)
if err != nil {
log.Printf("Error building work response: %v", err)
continue
}
responses = append(responses, response)
}
c.JSON(http.StatusOK, responses)
}
func GetWork(c *gin.Context) {
// GetWorkByID 根据ID获取作品
func GetWorkByID(c *gin.Context) {
id := c.Param("id")
// 从数据库获取作品
work, err := repositories.GetWorkByID(id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch work"})
utils.ServerError(c, err)
return
}
if work == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Work not found"})
utils.Error(c, 404, "Work not found")
return
}
// 构建响应
response, err := repositories.BuildWorkResponse(work)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to build work response"})
utils.ServerError(c, err)
return
}
c.JSON(http.StatusOK, response)
utils.Success(c, response)
}
// 获取作品列表 (Admin)
func AdminGetWorks(c *gin.Context) {
// GetWorks 获取所有作品
func GetWorks(c *gin.Context) {
works, err := repositories.GetWorks()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get works"})
utils.ServerError(c, err)
return
}
// 构建响应
var responses []interface{}
for _, work := range works {
response, err := repositories.BuildWorkResponse(&work)
if err != nil {
log.Printf("Error building work response: %v", err)
continue
}
responses = append(responses, response)
responses := repositories.BuildWorksResponse(works)
if responses == nil {
utils.Success(c, []interface{}{})
} else {
utils.Success(c, responses)
}
c.JSON(http.StatusOK, responses)
}
// 创建作品
// AdminGetWorks 获取作品列表 (后台)
func AdminGetWorks(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "10"))
works, total, err := repositories.GetAdminWorks(page, pageSize)
if err != nil {
utils.ServerError(c, err)
return
}
list := repositories.BuildWorksResponse(works)
res := gin.H{
"list": list,
"total": total,
"page": page,
"size": pageSize,
}
if list == nil {
res["list"] = []interface{}{}
}
utils.Success(c, res)
}
// AdminCreateWork 创建作品
func AdminCreateWork(c *gin.Context) {
var work models.Work
if err := c.ShouldBindJSON(&work); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
utils.Error(c, 400, "Invalid request")
return
}
// 创建作品
if err := repositories.CreateWork(&work); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create work"})
utils.ServerError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{"message": "Work created successfully"})
utils.SuccessWithMsg(c, "Work created successfully", gin.H{"id": work.ID})
}
// 更新作品
// AdminUpdateWork 更新作品
func AdminUpdateWork(c *gin.Context) {
workID := c.Param("id")
id := c.Param("id")
var work models.Work
if err := c.ShouldBindJSON(&work); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
utils.Error(c, 400, "Invalid request")
return
}
work.ID = id
// 设置作品ID
work.ID = workID
// 更新作品
if err := repositories.UpdateWork(&work); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update work"})
utils.ServerError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{"message": "Work updated successfully"})
utils.SuccessWithMsg(c, "Work updated successfully", nil)
}
// 删除作品
// AdminDeleteWork 删除作品
func AdminDeleteWork(c *gin.Context) {
workID := c.Param("id")
id := c.Param("id")
// 删除作品
if err := repositories.DeleteWork(workID); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete work"})
if err := repositories.DeleteWork(id); err != nil {
utils.ServerError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{"message": "Work deleted successfully"})
utils.SuccessWithMsg(c, "Work deleted successfully", nil)
}

View File

@@ -7,7 +7,6 @@ import (
"github.com/niangaodev/art-code/config"
"github.com/niangaodev/art-code/handlers"
"github.com/niangaodev/art-code/middleware"
"github.com/niangaodev/art-code/repositories"
"github.com/niangaodev/art-code/utils"
)
@@ -17,7 +16,7 @@ func main() {
defer config.CloseDB()
// 运行数据库迁移 (Convert Datetime to BigInt)
repositories.MigrateToBigInt()
// repositories.MigrateToBigInt() // 已禁用自动迁移
// 初始化ip2region (如果文件不存在将降级为普通IP记录)
// 请确保在server根目录或合适位置放入 ip2region.xdb
@@ -36,12 +35,21 @@ func main() {
{
// 作品路由
api.GET("/works", handlers.GetWorks)
api.GET("/works/:id", handlers.GetWork)
api.GET("/works/:id", handlers.GetWorkByID)
// 博客路由
api.GET("/posts", handlers.GetPosts)
api.GET("/posts/:id", handlers.GetPost)
// 分类路由
api.GET("/categories", handlers.GetCategories)
api.GET("/categories/:id", handlers.GetCategoryByID)
// 专栏路由
api.GET("/columns", handlers.GetColumns)
api.GET("/columns/:id", handlers.GetColumnByID)
api.GET("/columns/:id/posts", handlers.GetColumnPosts)
// 代码片段路由
api.GET("/snippets", handlers.GetSnippets)
api.GET("/snippets/:id", handlers.GetSnippet)
@@ -53,6 +61,7 @@ func main() {
// 代码执行路由
api.POST("/run", handlers.RunCode)
api.GET("/proxy", handlers.ProxyRequest)
// 关于页面路由
api.GET("/about", handlers.GetAboutProfile)
@@ -70,7 +79,7 @@ func main() {
admin := router.Group("/api/admin")
{
// 登录路由(不需要认证)
admin.POST("/login", handlers.AdminLogin)
admin.POST("/login", handlers.Login)
// 需要认证的路由
authAdmin := admin.Group("/")
@@ -108,7 +117,8 @@ func main() {
// 系统配置管理
authAdmin.GET("/settings", middleware.PermissionMiddleware("settings", "read"), handlers.AdminGetSettings)
authAdmin.POST("/settings", middleware.PermissionMiddleware("settings", "create"), handlers.AdminCreateSetting)
authAdmin.PUT("/settings", middleware.PermissionMiddleware("settings", "update"), handlers.AdminUpdateSetting)
authAdmin.PUT("/settings", middleware.PermissionMiddleware("settings", "update"), handlers.AdminUpdateSettings)
authAdmin.PUT("/settings/:id", middleware.PermissionMiddleware("settings", "update"), handlers.AdminUpdateSetting)
authAdmin.DELETE("/settings/:key", middleware.PermissionMiddleware("settings", "delete"), handlers.AdminDeleteSetting)
// 仪表盘统计
@@ -118,8 +128,23 @@ func main() {
authAdmin.GET("/posts", middleware.PermissionMiddleware("posts", "read"), handlers.AdminGetPosts)
authAdmin.POST("/posts", middleware.PermissionMiddleware("posts", "create"), handlers.AdminCreatePost)
authAdmin.PUT("/posts/:id", middleware.PermissionMiddleware("posts", "update"), handlers.AdminUpdatePost)
authAdmin.PATCH("/posts/:id/status", middleware.PermissionMiddleware("posts", "update"), handlers.AdminTogglePostStatus) // 新增状态切换
authAdmin.DELETE("/posts/:id", middleware.PermissionMiddleware("posts", "delete"), handlers.AdminDeletePost)
// 分类管理 (复用 posts 权限)
authAdmin.GET("/categories", middleware.PermissionMiddleware("posts", "read"), handlers.GetCategories) // Admin also uses public handler or specific if needed
authAdmin.POST("/categories", middleware.PermissionMiddleware("posts", "create"), handlers.AdminCreateCategory)
authAdmin.PUT("/categories/:id", middleware.PermissionMiddleware("posts", "update"), handlers.AdminUpdateCategory)
authAdmin.DELETE("/categories/:id", middleware.PermissionMiddleware("posts", "delete"), handlers.AdminDeleteCategory)
// 专栏管理 (复用 posts 权限)
authAdmin.GET("/columns", middleware.PermissionMiddleware("posts", "read"), handlers.GetColumns)
authAdmin.POST("/columns", middleware.PermissionMiddleware("posts", "create"), handlers.AdminCreateColumn)
authAdmin.PUT("/columns/:id", middleware.PermissionMiddleware("posts", "update"), handlers.AdminUpdateColumn)
authAdmin.DELETE("/columns/:id", middleware.PermissionMiddleware("posts", "delete"), handlers.AdminDeleteColumn)
authAdmin.POST("/columns/:id/posts", middleware.PermissionMiddleware("posts", "update"), handlers.AdminAddPostToColumn)
authAdmin.DELETE("/columns/:id/posts/:postId", middleware.PermissionMiddleware("posts", "update"), handlers.AdminRemovePostFromColumn)
// 文章历史记录
authAdmin.GET("/posts/:id/history", middleware.PermissionMiddleware("posts", "read"), handlers.AdminGetPostHistory)
authAdmin.GET("/posts/:id/history/:version", middleware.PermissionMiddleware("posts", "read"), handlers.AdminGetPostHistoryByVersion)
@@ -128,7 +153,6 @@ func main() {
authAdmin.GET("/operation-logs", middleware.PermissionMiddleware("operation_logs", "read"), handlers.AdminGetOperationLogs)
// 仪表盘数据
// authAdmin.GET("/dashboard/stats", middleware.PermissionMiddleware("dashboard", "read"), handlers.GetDashboardStats) // Duplicate removed
authAdmin.GET("/dashboard/activities", middleware.PermissionMiddleware("dashboard", "read"), handlers.AdminGetRecentActivities)
// 标签管理

13
server/models/category.go Normal file
View File

@@ -0,0 +1,13 @@
package models
// Category 分类模型
type Category struct {
ID uint `json:"id"`
Name string `json:"name"`
Slug string `json:"slug"`
Description string `json:"description"`
SortOrder uint `json:"sortOrder"`
CreatedAt int64 `json:"createdAt"`
UpdatedAt int64 `json:"updatedAt"`
DeletedAt int64 `json:"deletedAt"`
}

22
server/models/column.go Normal file
View File

@@ -0,0 +1,22 @@
package models
// Column 专栏模型
type Column struct {
ID uint `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Cover string `json:"cover"`
IsActive int `json:"isActive"`
SortOrder uint `json:"sortOrder"`
CreatedAt int64 `json:"createdAt"`
UpdatedAt int64 `json:"updatedAt"`
DeletedAt int64 `json:"deletedAt"`
}
// ColumnPost 专栏文章关联模型
type ColumnPost struct {
ColumnID uint `json:"columnId"`
PostID uint `json:"postId"`
SortOrder uint `json:"sortOrder"`
CreatedAt int64 `json:"createdAt"`
}

View File

@@ -2,56 +2,41 @@ package models
// Post 博客文章模型
type Post struct {
ID uint `json:"id"`
OriginalID string `json:"originalId,omitempty"` // For backward compatibility
Title string `json:"title"`
Category string `json:"category"`
// Date Removed from DB
Excerpt string `json:"excerpt"`
Content string `json:"content"`
ReadCount uint `json:"readCount"`
IsPublished int `json:"isPublished"` // 0: draft, 1: published
Tags []Tag `json:"tags"`
CreatedAt int64 `json:"createdAt"`
UpdatedAt int64 `json:"updatedAt"`
DeletedAt int64 `json:"deletedAt"`
ID uint `json:"id"`
OriginalID string `json:"originalId,omitempty"` // For backward compatibility
Title string `json:"title"`
CategoryID uint `json:"categoryId"`
Category *Category `json:"category,omitempty"` // For join query result
Excerpt string `json:"excerpt"`
Content string `json:"content"`
ReadCount uint `json:"readCount"`
IsPublished int `json:"isPublished"` // 0: draft, 1: published
Tags []Tag `json:"tags"`
CreatedAt int64 `json:"createdAt"`
UpdatedAt int64 `json:"updatedAt"`
DeletedAt int64 `json:"deletedAt"`
}
// PostResponse 博客文章响应模型
type PostResponse struct {
ID uint `json:"id"`
Title string `json:"title"`
Category string `json:"category"`
Date string `json:"date"`
Excerpt string `json:"excerpt,omitempty"`
Content string `json:"content,omitempty"`
}
// Tag 标签模型
type Tag struct {
ID uint `json:"id"`
Name string `json:"name"`
Slug string `json:"slug"`
CreatedAt int64 `json:"createdAt"`
UpdatedAt int64 `json:"updatedAt"`
DeletedAt int64 `json:"deletedAt"`
}
// PostTag 文章标签关联模型
type PostTag struct {
PostID string `json:"postId"`
TagID uint `json:"tagId"`
CreatedAt int64 `json:"createdAt"`
ID uint `json:"id"`
Title string `json:"title"`
CategoryID uint `json:"categoryId"`
CategoryName string `json:"categoryName"`
CategorySlug string `json:"categorySlug"`
Date string `json:"date"`
Excerpt string `json:"excerpt,omitempty"`
Content string `json:"content,omitempty"`
Tags []Tag `json:"tags,omitempty"`
}
// PostHistory 文章历史记录模型
type PostHistory struct {
ID uint `json:"id"`
PostID uint `json:"postId"`
Version int `json:"version"`
Title string `json:"title"`
Category string `json:"category"`
// Date Removed
ID uint `json:"id"`
PostID uint `json:"postId"`
Version int `json:"version"`
Title string `json:"title"`
CategoryID uint `json:"categoryId"`
Excerpt string `json:"excerpt"`
Content string `json:"content"`
IsPublished int `json:"isPublished"`
@@ -62,14 +47,15 @@ type PostHistory struct {
// PostHistoryResponse 文章历史记录响应模型
type PostHistoryResponse struct {
ID uint `json:"id"`
PostID uint `json:"postId"`
Version int `json:"version"`
Title string `json:"title"`
Category string `json:"category"`
Date string `json:"date"`
IsPublished int `json:"isPublished"`
ModifiedBy uint `json:"modifiedBy"`
ModifiedAt string `json:"modifiedAt"`
CreatedAt string `json:"createdAt"`
ID uint `json:"id"`
PostID uint `json:"postId"`
Version int `json:"version"`
Title string `json:"title"`
CategoryID uint `json:"categoryId"`
CategoryName string `json:"categoryName"`
Date string `json:"date"`
IsPublished int `json:"isPublished"`
ModifiedBy uint `json:"modifiedBy"`
ModifiedAt string `json:"modifiedAt"`
CreatedAt string `json:"createdAt"`
}

18
server/models/tag.go Normal file
View File

@@ -0,0 +1,18 @@
package models
// Tag 标签模型
type Tag struct {
ID uint `json:"id"`
Name string `json:"name"`
Slug string `json:"slug"`
CreatedAt int64 `json:"createdAt"`
UpdatedAt int64 `json:"updatedAt"`
DeletedAt int64 `json:"deletedAt"`
}
// PostTag 文章标签关联模型
type PostTag struct {
PostID string `json:"postId"`
TagID uint `json:"tagId"`
CreatedAt int64 `json:"createdAt"`
}

View File

@@ -5,6 +5,7 @@ type User struct {
ID uint `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
Password string `json:"password,omitempty" gorm:"-"` // Virtual field for input
PasswordHash string `json:"-"`
RoleID uint `json:"roleId"`
Role string `json:"role"` // 保持兼容或者作为Role Name

View File

@@ -11,7 +11,7 @@
Target Server Version : 80407 (8.4.7)
File Encoding : 65001
Date: 16/01/2026 13:10:06
Date: 16/01/2026 14:41:25
*/
SET NAMES utf8mb4;
@@ -66,6 +66,77 @@ CREATE TABLE `access_logs` (
-- Records of access_logs
-- ----------------------------
-- ----------------------------
-- 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 (1, 1, 0, 0);
INSERT INTO `column_posts` VALUES (2, 2, 0, 0);
INSERT INTO `column_posts` VALUES (3, 3, 0, 0);
INSERT INTO `column_posts` VALUES (4, 4, 0, 0);
INSERT INTO `column_posts` VALUES (4, 5, 0, 0);
INSERT INTO `column_posts` VALUES (4, 6, 0, 0);
-- ----------------------------
-- 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 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '专栏表' ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- Records of columns
-- ----------------------------
-- ----------------------------
-- Table structure for email_suffixes
-- ----------------------------
@@ -115,7 +186,6 @@ CREATE TABLE `inquiries` (
-- ----------------------------
-- Records of inquiries
-- ----------------------------
INSERT INTO `inquiries` VALUES (1, '李先生', '萧康云医', 'wechat', 'ngzz_9527', '1w-5w', '我需要做一个诊所小程序', 2, 0, 1768524765, 1768538954);
-- ----------------------------
-- Table structure for operation_logs
@@ -135,7 +205,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 = 307 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '操作日志表' ROW_FORMAT = DYNAMIC;
) ENGINE = InnoDB AUTO_INCREMENT = 309 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '操作日志表' ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- Records of operation_logs
@@ -446,6 +516,8 @@ INSERT INTO `operation_logs` VALUES (303, 1, 'lq', '::1', '/api/admin/dashboard/
INSERT INTO `operation_logs` VALUES (304, 1, 'lq', '::1', '/api/admin/testimonials', 'POST', '{\"author\":\"王甜甜\",\"role\":\"CTO\",\"avatar\":\"https://api.dicebear.com/7.x/avataaars/svg?seed=David\",\"rating\":5,\"content\":\"服务很贴心,技术够硬\"}', 200, 56, 0, 1768539952);
INSERT INTO `operation_logs` VALUES (305, 1, 'lq', '::1', '/api/admin/posts', 'GET', '', 200, 49, 0, 1768539988);
INSERT INTO `operation_logs` VALUES (306, 1, 'lq', '::1', '/api/admin/posts', 'GET', '', 200, 49, 0, 1768540005);
INSERT INTO `operation_logs` VALUES (307, 1, 'lq', '::1', '/api/admin/posts', 'GET', '', 200, 50, 0, 1768545569);
INSERT INTO `operation_logs` VALUES (308, 1, 'lq', '::1', '/api/admin/posts', 'GET', '', 200, 52, 0, 1768545632);
-- ----------------------------
-- Table structure for partners
@@ -523,6 +595,32 @@ INSERT INTO `permissions` VALUES (28, 'Delete Tag', 'tags', 'delete', 0, 1768452
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 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '文章历史记录表' ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- Records of post_history
-- ----------------------------
-- ----------------------------
-- Table structure for post_tags
-- ----------------------------
@@ -532,9 +630,7 @@ CREATE TABLE `post_tags` (
`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,
CONSTRAINT `post_tags_ibfk_1` FOREIGN KEY (`post_id`) REFERENCES `posts` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT,
CONSTRAINT `post_tags_ibfk_2` FOREIGN KEY (`tag_id`) REFERENCES `tags` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT
INDEX `idx_tag_id`(`tag_id` ASC) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '文章标签关联表' ROW_FORMAT = DYNAMIC;
-- ----------------------------
@@ -549,7 +645,7 @@ 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` varchar(100) 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 '文章内容',
`read_count` int UNSIGNED NULL DEFAULT 0 COMMENT '阅读量',
@@ -558,21 +654,21 @@ CREATE TABLE `posts` (
`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_is_published`(`is_published` ASC) USING BTREE COMMENT '按发布状态查询索引',
FULLTEXT INDEX `idx_title_content`(`title`, `content`) COMMENT '标题和内容全文索引,用于搜索',
INDEX `idx_original_id`(`original_id` ASC) USING BTREE
INDEX `idx_original_id`(`original_id` ASC) USING BTREE,
INDEX `idx_category_id`(`category_id` ASC) USING BTREE 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', '重构的艺术:如何优雅地处理遗留代码', '工程化', '在本文中,我们将探讨重构的核心原则和实用技巧,帮助你优雅地处理遗留代码,提高代码质量和可维护性。', '<h2>什么是代码重构?</h2><p>代码重构是在不改变代码外部行为的前提下,优化代码内部结构的过程...</p>', 5, 1, 0, 1768291814, 1768538949);
INSERT INTO `posts` VALUES (2, 'shader', '着色器魔法:从零开始写一个噪声生成器', '图形渲染', '深入了解WebGL着色器学习如何从零开始实现一个高性能的噪声生成器为你的3D作品增添独特的视觉效果。', ' <p>GLSL (OpenGL Shading Language) 是一门让人生畏但也充满魅力的语言。它运行在 GPU 上,能够并行处理数百万个像素,创造出惊人的视觉效果。</p>\r\n <h2>什么是柏林噪声?</h2>\r\n <p>柏林噪声Perlin Noise是一种梯度噪声它比普通的随机数生成的噪声看起来更自然、更平滑。它常被用来模拟云彩、地形、火焰等自然现象。</p>\r\n <h2>Three.js 中的实现</h2>\r\n <p>在 Three.js 中,我们可以通过 <code>ShaderMaterial</code> 直接编写 GLSL 代码。</p>\r\n <pre><code><span class=\"code-comment\">// 简单的顶点着色器</span>\r\n<span class=\"code-keyword\">varying</span> <span class=\"code-keyword\">vec2</span> vUv;\r\n<span class=\"code-keyword\">void</span> <span class=\"code-func\">main</span>() {\r\n vUv = uv;\r\n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\r\n}</code></pre>\r\n <p>通过调整噪声的频率和振幅,我们可以得到各种不同的纹理效果。在我的个人网站背景中,就使用了这种技术来生成流动的极光效果。</p>\r\n ', 1, 1, 0, 1768291815, 1768538949);
INSERT INTO `posts` VALUES (3, 'ux', '用户体验设计:从认知心理学到交互实践', '设计思维', '探索用户体验设计的核心原理,结合认知心理学知识,学习如何设计出真正符合用户需求的交互界面。', '<h2>认知心理学在UX设计中的应用</h2><p>了解用户的认知过程是设计良好用户体验的基础...</p>', 0, 1, 0, 1768291816, 1768538949);
INSERT INTO `posts` VALUES (4, 'goravel-guide-1', 'Goravel 入门指南 (一):环境搭建与 Hello World', 'Go语言', '本文将带你了解 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 入门指南 (二):路由与控制器', 'Go语言', '深入理解 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 操作数据库。', 901, 1, 0, 1768465933, 1768538949);
INSERT INTO `posts` VALUES (6, 'goravel-guide-3', 'Goravel 入门指南 (三)ORM 数据库操作', 'Go语言', '掌握 Goravel 强大的 ORM 功能从数据库配置、模型定义到执行增删改查CRUD操作。', '## Goravel ORM 简介\n\nGoravel 的 ORM 基于著名的 GORM 库构建,但它进行了一层优雅的封装,使其调用方式更接近 Laravel 的 Eloquent ORM。这意味着你可以使用 Facade 模式轻松调用数据库,且支持自动迁移。\n\n## 配置数据库\n\n打开 `config/database.go` 或 `.env` 文件,配置你的 MySQL 连接信息:\n\n```env\nDB_CONNECTION=mysql\nDB_HOST=127.0.0.1\nDB_PORT=3306\nDB_DATABASE=goravel\nDB_USERNAME=root\nDB_PASSWORD=password\n```\n\n## 定义模型\n\n使用 `knit` 生成模型:\n\n```bash\nknit make:model Post\n```\n\n生成的模型文件位于 `app/models/post.go`。我们需要定义结构体字段:\n\n```go\npackage models\n\nimport (\n \"github.com/goravel/framework/database/orm\"\n)\n\ntype Post struct {\n orm.Model\n Title string `gorm:\"size:255;not null\"`\n Content string `gorm:\"type:text\"`\n UserID uint\n}\n```\n\n## 数据库迁移\n\n虽然 GORM 支持 AutoMigrate但 Goravel 推荐使用迁移文件来管理数据库变更。\n\n```bash\nknit make:migration create_posts_table\n```\n\n编辑生成的迁移文件 (`database/migrations/xxxx_create_posts_table.go`),定义表结构,然后运行:\n\n```bash\nknit migrate\n```\n\n## CRUD 操作\n\n有了模型我们就可以愉快地操作数据库了。所有操作都通过 `facades.Orm()` 入口。\n\n### 创建 (Create)\n\n```go\npost := models.Post{\n Title: \"My First Post\",\n Content: \"Content goes here...\",\n}\nerr := facades.Orm().Query().Create(&post)\n```\n\n### 查询 (Read)\n\n```go\nvar post models.Post\n// 根据主键查询\nfacades.Orm().Query().Find(&post, 1)\n\n// 条件查询\nvar posts []models.Post\nfacades.Orm().Query().Where(\"title\", \"My First Post\").Get(&posts)\n```\n\n### 更新 (Update)\n\n```go\nvar post models.Post\nfacades.Orm().Query().Find(&post, 1)\n\npost.Title = \"Updated Title\"\nfacades.Orm().Query().Save(&post)\n```\n\n### 删除 (Delete)\n\n```go\nvar post models.Post\nfacades.Orm().Query().Delete(&post, 1)\n```\n\nGoravel 的 ORM 让 Go 语言的数据库操作不再繁琐,极大地提高了开发效率。配合之前的路由和控制器知识,你现在已经具备了开发完整 RESTful API 的能力!', 1563, 1, 0, 1768465934, 1768538949);
INSERT INTO `posts` VALUES (1, 'refactor', '重构的艺术:如何优雅地处理遗留代码', 4, '在本文中,我们将探讨重构的核心原则和实用技巧,帮助你优雅地处理遗留代码,提高代码质量和可维护性。', '<h2>什么是代码重构?</h2><p>代码重构是在不改变代码外部行为的前提下,优化代码内部结构的过程...</p>', 5, 1, 0, 1768291814, 1768538949);
INSERT INTO `posts` VALUES (2, 'shader', '着色器魔法:从零开始写一个噪声生成器', 2, '深入了解WebGL着色器学习如何从零开始实现一个高性能的噪声生成器为你的3D作品增添独特的视觉效果。', ' <p>GLSL (OpenGL Shading Language) 是一门让人生畏但也充满魅力的语言。它运行在 GPU 上,能够并行处理数百万个像素,创造出惊人的视觉效果。</p>\r\n <h2>什么是柏林噪声?</h2>\r\n <p>柏林噪声Perlin Noise是一种梯度噪声它比普通的随机数生成的噪声看起来更自然、更平滑。它常被用来模拟云彩、地形、火焰等自然现象。</p>\r\n <h2>Three.js 中的实现</h2>\r\n <p>在 Three.js 中,我们可以通过 <code>ShaderMaterial</code> 直接编写 GLSL 代码。</p>\r\n <pre><code><span class=\"code-comment\">// 简单的顶点着色器</span>\r\n<span class=\"code-keyword\">varying</span> <span class=\"code-keyword\">vec2</span> vUv;\r\n<span class=\"code-keyword\">void</span> <span class=\"code-func\">main</span>() {\r\n vUv = uv;\r\n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\r\n}</code></pre>\r\n <p>通过调整噪声的频率和振幅,我们可以得到各种不同的纹理效果。在我的个人网站背景中,就使用了这种技术来生成流动的极光效果。</p>\r\n ', 1, 1, 0, 1768291815, 1768538949);
INSERT INTO `posts` VALUES (3, 'ux', '用户体验设计:从认知心理学到交互实践', 3, '探索用户体验设计的核心原理,结合认知心理学知识,学习如何设计出真正符合用户需求的交互界面。', '<h2>认知心理学在UX设计中的应用</h2><p>了解用户的认知过程是设计良好用户体验的基础...</p>', 0, 1, 0, 1768291816, 1768538949);
INSERT INTO `posts` VALUES (4, 'goravel-guide-1', 'Goravel 入门指南 (一):环境搭建与 Hello World', 4, '本文将带你了解 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, '深入理解 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 操作数据库。', 901, 1, 0, 1768465933, 1768538949);
INSERT INTO `posts` VALUES (6, 'goravel-guide-3', 'Goravel 入门指南 (三)ORM 数据库操作', 4, '掌握 Goravel 强大的 ORM 功能从数据库配置、模型定义到执行增删改查CRUD操作。', '## Goravel ORM 简介\n\nGoravel 的 ORM 基于著名的 GORM 库构建,但它进行了一层优雅的封装,使其调用方式更接近 Laravel 的 Eloquent ORM。这意味着你可以使用 Facade 模式轻松调用数据库,且支持自动迁移。\n\n## 配置数据库\n\n打开 `config/database.go` 或 `.env` 文件,配置你的 MySQL 连接信息:\n\n```env\nDB_CONNECTION=mysql\nDB_HOST=127.0.0.1\nDB_PORT=3306\nDB_DATABASE=goravel\nDB_USERNAME=root\nDB_PASSWORD=password\n```\n\n## 定义模型\n\n使用 `knit` 生成模型:\n\n```bash\nknit make:model Post\n```\n\n生成的模型文件位于 `app/models/post.go`。我们需要定义结构体字段:\n\n```go\npackage models\n\nimport (\n \"github.com/goravel/framework/database/orm\"\n)\n\ntype Post struct {\n orm.Model\n Title string `gorm:\"size:255;not null\"`\n Content string `gorm:\"type:text\"`\n UserID uint\n}\n```\n\n## 数据库迁移\n\n虽然 GORM 支持 AutoMigrate但 Goravel 推荐使用迁移文件来管理数据库变更。\n\n```bash\nknit make:migration create_posts_table\n```\n\n编辑生成的迁移文件 (`database/migrations/xxxx_create_posts_table.go`),定义表结构,然后运行:\n\n```bash\nknit migrate\n```\n\n## CRUD 操作\n\n有了模型我们就可以愉快地操作数据库了。所有操作都通过 `facades.Orm()` 入口。\n\n### 创建 (Create)\n\n```go\npost := models.Post{\n Title: \"My First Post\",\n Content: \"Content goes here...\",\n}\nerr := facades.Orm().Query().Create(&post)\n```\n\n### 查询 (Read)\n\n```go\nvar post models.Post\n// 根据主键查询\nfacades.Orm().Query().Find(&post, 1)\n\n// 条件查询\nvar posts []models.Post\nfacades.Orm().Query().Where(\"title\", \"My First Post\").Get(&posts)\n```\n\n### 更新 (Update)\n\n```go\nvar post models.Post\nfacades.Orm().Query().Find(&post, 1)\n\npost.Title = \"Updated Title\"\nfacades.Orm().Query().Save(&post)\n```\n\n### 删除 (Delete)\n\n```go\nvar post models.Post\nfacades.Orm().Query().Delete(&post, 1)\n```\n\nGoravel 的 ORM 让 Go 语言的数据库操作不再繁琐,极大地提高了开发效率。配合之前的路由和控制器知识,你现在已经具备了开发完整 RESTful API 的能力!', 1563, 1, 0, 1768465934, 1768538949);
-- ----------------------------
-- Table structure for role_permissions
@@ -582,9 +678,7 @@ 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,
CONSTRAINT `role_permissions_ibfk_1` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT,
CONSTRAINT `role_permissions_ibfk_2` FOREIGN KEY (`permission_id`) REFERENCES `permissions` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT
INDEX `role_permissions_ibfk_2`(`permission_id` ASC) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '角色权限关联表' ROW_FORMAT = DYNAMIC;
-- ----------------------------
@@ -777,18 +871,6 @@ CREATE TABLE `user_access_logs` (
-- ----------------------------
-- Records of user_access_logs
-- ----------------------------
INSERT INTO `user_access_logs` VALUES (1, 0, '::1', 'Unknown', 5, 0, 1768529515);
INSERT INTO `user_access_logs` VALUES (2, 0, '::1', 'Unknown', 4, 0, 1768529590);
INSERT INTO `user_access_logs` VALUES (3, 0, '::1', 'Unknown', 5, 0, 1768529604);
INSERT INTO `user_access_logs` VALUES (4, 0, '::1', 'Unknown', 4, 0, 1768529614);
INSERT INTO `user_access_logs` VALUES (5, 0, '::1', 'Unknown', 5, 0, 1768529615);
INSERT INTO `user_access_logs` VALUES (6, 0, '::1', 'Unknown', 4, 0, 1768532921);
INSERT INTO `user_access_logs` VALUES (7, 0, '::1', 'Unknown', 5, 0, 1768533169);
INSERT INTO `user_access_logs` VALUES (8, 0, '::1', 'Unknown', 6, 0, 1768533299);
INSERT INTO `user_access_logs` VALUES (9, 0, '::1', 'Unknown', 1, 0, 1768538210);
INSERT INTO `user_access_logs` VALUES (10, 0, '::1', 'Unknown', 6, 0, 20260116125547);
INSERT INTO `user_access_logs` VALUES (11, 0, '::1', 'Unknown', 5, 0, 20260116125614);
INSERT INTO `user_access_logs` VALUES (12, 0, '::1', 'Unknown', 6, 0, 20260116130641);
-- ----------------------------
-- Table structure for users
@@ -811,8 +893,7 @@ CREATE TABLE `users` (
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,
CONSTRAINT `users_ibfk_1` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`) ON DELETE SET NULL ON UPDATE RESTRICT
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;
-- ----------------------------
@@ -836,8 +917,7 @@ CREATE TABLE `work_gallery` (
`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,
CONSTRAINT `work_gallery_ibfk_1` FOREIGN KEY (`work_id`) REFERENCES `works` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT
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;
-- ----------------------------
@@ -861,8 +941,7 @@ CREATE TABLE `work_tech_stack` (
`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,
CONSTRAINT `work_tech_stack_ibfk_1` FOREIGN KEY (`work_id`) REFERENCES `works` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT
INDEX `idx_category`(`category` ASC) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 10 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '作品技术栈表' ROW_FORMAT = DYNAMIC;
-- ----------------------------

View File

@@ -4,12 +4,60 @@ import (
"database/sql"
"encoding/json"
"log"
"strconv"
"strings"
"time"
"github.com/niangaodev/art-code/config"
"github.com/niangaodev/art-code/models"
)
// unescapeJSONString 解码转义的 JSON 字符串
// 处理两种情况:
// 1. 被引号包裹的转义 JSON 字符串:`"[\"Vue 3\",...]"`
// 2. 包含转义引号的 JSON 字符串:`[\"Vue 3\",...]`
func unescapeJSONString(s string) (string, error) {
// 如果字符串以引号开头和结尾,说明是被引号包裹的转义 JSON 字符串
if len(s) >= 2 && s[0] == '"' && s[len(s)-1] == '"' {
unquoted, err := strconv.Unquote(s)
if err != nil {
return s, err
}
return unquoted, nil
}
// 如果字符串包含转义的引号 \",需要将其转换为普通引号
// 例如:[\"Vue 3\",...] -> ["Vue 3",...]
if len(s) > 0 {
// 尝试直接解析,如果失败则尝试替换转义引号
var test interface{}
if err := json.Unmarshal([]byte(s), &test); err != nil {
// 如果解析失败,尝试将 \" 替换为 "
unescaped := s
// 替换转义的反斜杠+引号
// 注意:这里需要小心处理,因为 \\" 应该变成 \"
// 但 \" 应该变成 "
// 使用正则表达式或字符串替换
// 简单方法:将 \" 替换为 "(但需要确保不会误替换 \\"
// 更安全的方法:使用 json.Unmarshal 两次解析
// 或者使用 strings.ReplaceAll 但需要小心
// 尝试将 \" 替换为 "
unescaped = strings.ReplaceAll(unescaped, `\"`, `"`)
// 如果替换后能解析,返回替换后的字符串
if err2 := json.Unmarshal([]byte(unescaped), &test); err2 == nil {
return unescaped, nil
}
} else {
// 如果能直接解析,返回原字符串
return s, nil
}
}
// 如果都失败,返回原字符串
return s, nil
}
// GetPrimaryAboutProfile 获取主页个人资料
func GetPrimaryAboutProfile() (*models.AboutProfile, error) {
query := `
@@ -44,17 +92,22 @@ func GetPrimaryAboutProfile() (*models.AboutProfile, error) {
return nil, err
}
// Unmarshal JSON
// Unmarshal JSON - 确保 TechList 和 ExperienceList 始终是数组而不是 nil
profile.TechList = []string{}
profile.ExperienceList = []models.Experience{}
if profile.TechStack != "" {
_ = json.Unmarshal([]byte(profile.TechStack), &profile.TechList)
} else {
profile.TechList = []string{}
if err := json.Unmarshal([]byte(profile.TechStack), &profile.TechList); err != nil {
log.Printf("Error unmarshaling techStack: %v, raw: %s", err, profile.TechStack)
profile.TechList = []string{}
}
}
if profile.ExperiencesStr != "" {
_ = json.Unmarshal([]byte(profile.ExperiencesStr), &profile.ExperienceList)
} else {
profile.ExperienceList = []models.Experience{}
if err := json.Unmarshal([]byte(profile.ExperiencesStr), &profile.ExperienceList); err != nil {
log.Printf("Error unmarshaling experiences: %v, raw: %s", err, profile.ExperiencesStr)
profile.ExperienceList = []models.Experience{}
}
}
return &profile, nil
@@ -94,16 +147,100 @@ func GetFirstAboutProfile() (*models.AboutProfile, error) {
return nil, err
}
// Unmarshal JSON - 确保 TechList 和 ExperienceList 始终是数组而不是 nil
profile.TechList = []string{}
profile.ExperienceList = []models.Experience{}
if profile.TechStack != "" {
_ = json.Unmarshal([]byte(profile.TechStack), &profile.TechList)
} else {
profile.TechList = []string{}
// 先尝试解码转义的 JSON 字符串
unescaped, err := unescapeJSONString(profile.TechStack)
if err != nil {
log.Printf("Error unescaping techStack: %v, raw: %s", err, profile.TechStack)
} else {
if err := json.Unmarshal([]byte(unescaped), &profile.TechList); err != nil {
log.Printf("Error unmarshaling techStack: %v, raw: %s, unescaped: %s", err, profile.TechStack, unescaped)
profile.TechList = []string{}
}
}
}
if profile.ExperiencesStr != "" {
_ = json.Unmarshal([]byte(profile.ExperiencesStr), &profile.ExperienceList)
} else {
profile.ExperienceList = []models.Experience{}
// 先尝试解码转义的 JSON 字符串
unescaped, err := unescapeJSONString(profile.ExperiencesStr)
if err != nil {
log.Printf("Error unescaping experiences: %v, raw: %s", err, profile.ExperiencesStr)
} else {
if err := json.Unmarshal([]byte(unescaped), &profile.ExperienceList); err != nil {
log.Printf("Error unmarshaling experiences: %v, raw: %s, unescaped: %s", err, profile.ExperiencesStr, unescaped)
profile.ExperienceList = []models.Experience{}
}
}
}
return &profile, nil
}
// GetAboutProfileByID 根据 ID 获取个人资料
func GetAboutProfileByID(id uint) (*models.AboutProfile, error) {
query := `
SELECT id, name, avatar, location, bio, email, wechat, tech_stack, experiences, is_primary, created_at, updated_at, deleted_at
FROM about_profiles
WHERE id = ? AND deleted_at = 0
LIMIT 1
`
row := config.DB.QueryRow(query, id)
var profile models.AboutProfile
if err := row.Scan(
&profile.ID,
&profile.Name,
&profile.Avatar,
&profile.Location,
&profile.Bio,
&profile.Email,
&profile.Wechat,
&profile.TechStack,
&profile.ExperiencesStr,
&profile.IsPrimary,
&profile.CreatedAt,
&profile.UpdatedAt,
&profile.DeletedAt,
); err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
log.Printf("Error scanning about profile by ID: %v", err)
return nil, err
}
// Unmarshal JSON - 确保 TechList 和 ExperienceList 始终是数组而不是 nil
profile.TechList = []string{}
profile.ExperienceList = []models.Experience{}
if profile.TechStack != "" {
// 先尝试解码转义的 JSON 字符串
unescaped, err := unescapeJSONString(profile.TechStack)
if err != nil {
log.Printf("Error unescaping techStack: %v, raw: %s", err, profile.TechStack)
} else {
if err := json.Unmarshal([]byte(unescaped), &profile.TechList); err != nil {
log.Printf("Error unmarshaling techStack: %v, raw: %s, unescaped: %s", err, profile.TechStack, unescaped)
profile.TechList = []string{}
}
}
}
if profile.ExperiencesStr != "" {
// 先尝试解码转义的 JSON 字符串
unescaped, err := unescapeJSONString(profile.ExperiencesStr)
if err != nil {
log.Printf("Error unescaping experiences: %v, raw: %s", err, profile.ExperiencesStr)
} else {
if err := json.Unmarshal([]byte(unescaped), &profile.ExperienceList); err != nil {
log.Printf("Error unmarshaling experiences: %v, raw: %s, unescaped: %s", err, profile.ExperiencesStr, unescaped)
profile.ExperienceList = []models.Experience{}
}
}
}
return &profile, nil
@@ -139,15 +276,34 @@ func GetAllAboutProfiles() ([]models.AboutProfile, error) {
); err != nil {
continue
}
// Unmarshal JSON - 确保 TechList 和 ExperienceList 始终是数组而不是 nil
p.TechList = []string{}
p.ExperienceList = []models.Experience{}
if p.TechStack != "" {
_ = json.Unmarshal([]byte(p.TechStack), &p.TechList)
} else {
p.TechList = []string{}
// 先尝试解码转义的 JSON 字符串
unescaped, err := unescapeJSONString(p.TechStack)
if err != nil {
log.Printf("Error unescaping techStack: %v, raw: %s", err, p.TechStack)
} else {
if err := json.Unmarshal([]byte(unescaped), &p.TechList); err != nil {
log.Printf("Error unmarshaling techStack: %v, raw: %s, unescaped: %s", err, p.TechStack, unescaped)
p.TechList = []string{}
}
}
}
if p.ExperiencesStr != "" {
_ = json.Unmarshal([]byte(p.ExperiencesStr), &p.ExperienceList)
} else {
p.ExperienceList = []models.Experience{}
// 先尝试解码转义的 JSON 字符串
unescaped, err := unescapeJSONString(p.ExperiencesStr)
if err != nil {
log.Printf("Error unescaping experiences: %v, raw: %s", err, p.ExperiencesStr)
} else {
if err := json.Unmarshal([]byte(unescaped), &p.ExperienceList); err != nil {
log.Printf("Error unmarshaling experiences: %v, raw: %s, unescaped: %s", err, p.ExperiencesStr, unescaped)
p.ExperienceList = []models.Experience{}
}
}
}
profiles = append(profiles, p)
}
@@ -156,6 +312,14 @@ func GetAllAboutProfiles() ([]models.AboutProfile, error) {
// CreateAboutProfile 创建个人资料
func CreateAboutProfile(profile *models.AboutProfile) error {
// 确保 TechList 和 ExperienceList 不为 nil
if profile.TechList == nil {
profile.TechList = []string{}
}
if profile.ExperienceList == nil {
profile.ExperienceList = []models.Experience{}
}
// Marshal JSON
techBytes, _ := json.Marshal(profile.TechList)
profile.TechStack = string(techBytes)
@@ -194,11 +358,24 @@ func CreateAboutProfile(profile *models.AboutProfile) error {
profile.ID = uint(id)
profile.CreatedAt = now
profile.UpdatedAt = now
// 确保返回的数据包含 TechList 和 ExperienceList已从 JSON 解析)
// 这些字段已经在上面被 Marshal 了,现在需要确保它们被正确设置
// 由于我们已经 Marshal 了TechList 和 ExperienceList 应该保持原样
return nil
}
// UpdateAboutProfile 更新个人资料
func UpdateAboutProfile(profile *models.AboutProfile) error {
// 确保 TechList 和 ExperienceList 不为 nil
if profile.TechList == nil {
profile.TechList = []string{}
}
if profile.ExperienceList == nil {
profile.ExperienceList = []models.Experience{}
}
// Marshal JSON
techBytes, _ := json.Marshal(profile.TechList)
profile.TechStack = string(techBytes)

View File

@@ -0,0 +1,144 @@
package repositories
import (
"database/sql"
"log"
"time"
"github.com/niangaodev/art-code/config"
"github.com/niangaodev/art-code/models"
)
// GetCategories 获取所有分类
func GetCategories() ([]models.Category, error) {
query := "SELECT id, name, slug, description, sort_order, created_at, updated_at, deleted_at FROM categories WHERE deleted_at = 0 ORDER BY sort_order ASC, created_at DESC"
rows, err := config.DB.Query(query)
if err != nil {
log.Printf("Error querying categories: %v", err)
return nil, err
}
defer rows.Close()
var categories []models.Category
for rows.Next() {
var category models.Category
var description sql.NullString // Use NullString for nullable column
if err := rows.Scan(
&category.ID,
&category.Name,
&category.Slug,
&description, // Scan into NullString
&category.SortOrder,
&category.CreatedAt,
&category.UpdatedAt,
&category.DeletedAt,
); err != nil {
log.Printf("Error scanning category: %v", err)
continue
}
if description.Valid {
category.Description = description.String
}
categories = append(categories, category)
}
return categories, nil
}
// GetCategoryByID 根据ID获取分类
func GetCategoryByID(id uint) (*models.Category, error) {
query := "SELECT id, name, slug, description, sort_order, created_at, updated_at, deleted_at FROM categories WHERE id = ? AND deleted_at = 0"
row := config.DB.QueryRow(query, id)
var category models.Category
var description sql.NullString // Use NullString for nullable column
if err := row.Scan(
&category.ID,
&category.Name,
&category.Slug,
&description, // Scan into NullString
&category.SortOrder,
&category.CreatedAt,
&category.UpdatedAt,
&category.DeletedAt,
); err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
log.Printf("Error scanning category by ID: %v", err)
return nil, err
}
if description.Valid {
category.Description = description.String
}
return &category, nil
}
// CreateCategory 创建分类
func CreateCategory(category *models.Category) error {
now := time.Now().Unix()
query := `
INSERT INTO categories (name, slug, description, sort_order, created_at, updated_at, deleted_at)
VALUES (?, ?, ?, ?, ?, ?, 0)
`
result, err := config.DB.Exec(
query,
category.Name,
category.Slug,
category.Description,
category.SortOrder,
now,
now,
)
if err != nil {
log.Printf("Error creating category: %v", err)
return err
}
id, err := result.LastInsertId()
if err != nil {
return err
}
category.ID = uint(id)
category.CreatedAt = now
category.UpdatedAt = now
return nil
}
// UpdateCategory 更新分类
func UpdateCategory(category *models.Category) error {
now := time.Now().Unix()
query := `
UPDATE categories SET name = ?, slug = ?, description = ?, sort_order = ?, updated_at = ?
WHERE id = ? AND deleted_at = 0
`
_, err := config.DB.Exec(
query,
category.Name,
category.Slug,
category.Description,
category.SortOrder,
now,
category.ID,
)
if err != nil {
log.Printf("Error updating category: %v", err)
return err
}
return nil
}
// DeleteCategory 删除分类
func DeleteCategory(id uint) error {
now := time.Now().Unix()
query := "UPDATE categories SET deleted_at = ? WHERE id = ?"
_, err := config.DB.Exec(query, now, id)
if err != nil {
log.Printf("Error deleting category: %v", err)
return err
}
return nil
}

View File

@@ -0,0 +1,222 @@
package repositories
import (
"database/sql"
"log"
"time"
"github.com/niangaodev/art-code/config"
"github.com/niangaodev/art-code/models"
)
// GetColumns 获取所有专栏
func GetColumns() ([]models.Column, error) {
query := "SELECT id, name, description, cover, is_active, sort_order, created_at, updated_at, deleted_at FROM columns WHERE deleted_at = 0 ORDER BY sort_order ASC, created_at DESC"
rows, err := config.DB.Query(query)
if err != nil {
log.Printf("Error querying columns: %v", err)
return nil, err
}
defer rows.Close()
var columns []models.Column
for rows.Next() {
var col models.Column
var description sql.NullString // Use NullString
var cover sql.NullString // Use NullString
if err := rows.Scan(
&col.ID,
&col.Name,
&description,
&cover,
&col.IsActive,
&col.SortOrder,
&col.CreatedAt,
&col.UpdatedAt,
&col.DeletedAt,
); err != nil {
log.Printf("Error scanning column: %v", err)
continue
}
if description.Valid {
col.Description = description.String
}
if cover.Valid {
col.Cover = cover.String
}
columns = append(columns, col)
}
return columns, nil
}
// GetColumnByID 根据ID获取专栏
func GetColumnByID(id uint) (*models.Column, error) {
query := "SELECT id, name, description, cover, is_active, sort_order, created_at, updated_at, deleted_at FROM columns WHERE id = ? AND deleted_at = 0"
row := config.DB.QueryRow(query, id)
var col models.Column
var description sql.NullString // Use NullString
var cover sql.NullString // Use NullString
if err := row.Scan(
&col.ID,
&col.Name,
&description,
&cover,
&col.IsActive,
&col.SortOrder,
&col.CreatedAt,
&col.UpdatedAt,
&col.DeletedAt,
); err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
log.Printf("Error scanning column by ID: %v", err)
return nil, err
}
if description.Valid {
col.Description = description.String
}
if cover.Valid {
col.Cover = cover.String
}
return &col, nil
}
// CreateColumn 创建专栏
func CreateColumn(col *models.Column) error {
now := time.Now().Unix()
query := `
INSERT INTO columns (name, description, cover, is_active, sort_order, created_at, updated_at, deleted_at)
VALUES (?, ?, ?, ?, ?, ?, ?, 0)
`
result, err := config.DB.Exec(
query,
col.Name,
col.Description,
col.Cover,
col.IsActive,
col.SortOrder,
now,
now,
)
if err != nil {
log.Printf("Error creating column: %v", err)
return err
}
id, err := result.LastInsertId()
if err != nil {
return err
}
col.ID = uint(id)
col.CreatedAt = now
col.UpdatedAt = now
return nil
}
// UpdateColumn 更新专栏
func UpdateColumn(col *models.Column) error {
now := time.Now().Unix()
query := `
UPDATE columns SET name = ?, description = ?, cover = ?, is_active = ?, sort_order = ?, updated_at = ?
WHERE id = ? AND deleted_at = 0
`
_, err := config.DB.Exec(
query,
col.Name,
col.Description,
col.Cover,
col.IsActive,
col.SortOrder,
now,
col.ID,
)
if err != nil {
log.Printf("Error updating column: %v", err)
return err
}
return nil
}
// DeleteColumn 删除专栏
func DeleteColumn(id uint) error {
now := time.Now().Unix()
query := "UPDATE columns SET deleted_at = ? WHERE id = ?"
_, err := config.DB.Exec(query, now, id)
if err != nil {
log.Printf("Error deleting column: %v", err)
return err
}
return nil
}
// GetPostsByColumnID 获取专栏下的文章
func GetPostsByColumnID(columnID uint) ([]models.Post, error) {
query := `
SELECT p.id, p.title, p.category_id, c.name as category_name, p.excerpt, p.content, p.read_count, p.is_published, p.created_at, p.updated_at, p.deleted_at
FROM posts p
JOIN column_posts cp ON p.id = cp.post_id
LEFT JOIN categories c ON p.category_id = c.id
WHERE cp.column_id = ? AND p.deleted_at = 0 AND p.is_published = 1
ORDER BY cp.sort_order ASC, p.created_at DESC
`
rows, err := config.DB.Query(query, columnID)
if err != nil {
log.Printf("Error querying posts by column ID: %v", err)
return nil, err
}
defer rows.Close()
var posts []models.Post
for rows.Next() {
var post models.Post
var categoryName sql.NullString
if err := rows.Scan(
&post.ID,
&post.Title,
&post.CategoryID,
&categoryName,
&post.Excerpt,
&post.Content,
&post.ReadCount,
&post.IsPublished,
&post.CreatedAt,
&post.UpdatedAt,
&post.DeletedAt,
); err != nil {
log.Printf("Error scanning post: %v", err)
continue
}
if categoryName.Valid {
post.Category = &models.Category{ID: post.CategoryID, Name: categoryName.String}
}
posts = append(posts, post)
}
return posts, nil
}
// AddPostToColumn 添加文章到专栏
func AddPostToColumn(columnID, postID, sortOrder uint) error {
now := time.Now().Unix()
// Check if exists first to avoid duplicates or use INSERT IGNORE/REPLACE if simple
// Assuming unique key on (column_id, post_id)
query := `
INSERT INTO column_posts (column_id, post_id, sort_order, created_at)
VALUES (?, ?, ?, ?)
ON DUPLICATE KEY UPDATE sort_order = VALUES(sort_order)
`
_, err := config.DB.Exec(query, columnID, postID, sortOrder, now)
return err
}
// RemovePostFromColumn 从专栏移除文章
func RemovePostFromColumn(columnID, postID uint) error {
query := "DELETE FROM column_posts WHERE column_id = ? AND post_id = ?"
_, err := config.DB.Exec(query, columnID, postID)
return err
}

View File

@@ -9,34 +9,49 @@ import (
"github.com/niangaodev/art-code/models"
)
// GetPosts 获取所有博客文章(支持搜索)
func GetPosts(keyword string) ([]models.Post, error) {
var rows *sql.Rows
var err error
// TrendData 趋势数据
type TrendData struct {
Date string `json:"date"`
Count int `json:"value"`
YoY float64 `json:"yoy"`
MoM float64 `json:"mom"`
}
// Common select fields (removed date)
selectFields := "id, title, category, excerpt, content, read_count, is_published, created_at, updated_at, deleted_at"
// GetPosts 获取所有博客文章(支持搜索、分类、标签筛选)
func GetPosts(keyword string, categoryID uint, tagID uint) ([]models.Post, error) {
query := `
SELECT p.id, p.title, p.category_id, c.name, c.slug, p.excerpt, p.content, p.read_count, p.is_published, p.created_at, p.updated_at, p.deleted_at
FROM posts p
LEFT JOIN categories c ON p.category_id = c.id
`
if keyword != "" {
// 使用全文搜索
query := `
SELECT ` + selectFields + `
FROM posts
WHERE is_published = 1 AND deleted_at = 0 AND (
MATCH(title, content) AGAINST(? IN BOOLEAN MODE) OR
title LIKE ? OR
content LIKE ?
)
ORDER BY created_at DESC
`
likeKeyword := "%" + keyword + "%"
rows, err = config.DB.Query(query, keyword, likeKeyword, likeKeyword)
} else {
// 默认查询
query := "SELECT " + selectFields + " FROM posts WHERE is_published = 1 AND deleted_at = 0 ORDER BY created_at DESC"
rows, err = config.DB.Query(query)
whereClause := " WHERE p.is_published = 1 AND p.deleted_at = 0"
args := []interface{}{}
if tagID > 0 {
query += " JOIN post_tags pt ON p.id = pt.post_id"
whereClause += " AND pt.tag_id = ?"
args = append(args, tagID)
}
if categoryID > 0 {
whereClause += " AND p.category_id = ?"
args = append(args, categoryID)
}
if keyword != "" {
whereClause += ` AND (
MATCH(p.title, p.content) AGAINST(? IN BOOLEAN MODE) OR
p.title LIKE ? OR
p.content LIKE ?
)`
likeKeyword := "%" + keyword + "%"
args = append(args, keyword, likeKeyword, likeKeyword)
}
query += whereClause + " ORDER BY p.created_at DESC"
rows, err := config.DB.Query(query, args...)
if err != nil {
log.Printf("Error querying posts: %v", err)
return nil, err
@@ -46,10 +61,16 @@ func GetPosts(keyword string) ([]models.Post, error) {
var posts []models.Post
for rows.Next() {
var post models.Post
var catID sql.NullInt64
var catName sql.NullString
var catSlug sql.NullString
if err := rows.Scan(
&post.ID,
&post.Title,
&post.Category,
&catID,
&catName,
&catSlug,
&post.Excerpt,
&post.Content,
&post.ReadCount,
@@ -61,6 +82,18 @@ func GetPosts(keyword string) ([]models.Post, error) {
log.Printf("Error scanning post: %v", err)
continue
}
if catID.Valid {
post.CategoryID = uint(catID.Int64)
post.Category = &models.Category{
ID: uint(catID.Int64),
Name: catName.String,
Slug: catSlug.String,
}
}
// TODO: Fetch tags if needed, or lazy load
posts = append(posts, post)
}
@@ -69,15 +102,25 @@ func GetPosts(keyword string) ([]models.Post, error) {
// GetPostByID 根据ID获取博客文章
func GetPostByID(id uint) (*models.Post, error) {
selectFields := "id, title, category, excerpt, content, read_count, is_published, created_at, updated_at, deleted_at"
query := "SELECT " + selectFields + " FROM posts WHERE id = ? AND is_published = 1 AND deleted_at = 0"
query := `
SELECT p.id, p.title, p.category_id, c.name, c.slug, p.excerpt, p.content, p.read_count, p.is_published, p.created_at, p.updated_at, p.deleted_at
FROM posts p
LEFT JOIN categories c ON p.category_id = c.id
WHERE p.id = ? AND p.is_published = 1 AND p.deleted_at = 0
`
row := config.DB.QueryRow(query, id)
var post models.Post
var catID sql.NullInt64
var catName sql.NullString
var catSlug sql.NullString
if err := row.Scan(
&post.ID,
&post.Title,
&post.Category,
&catID,
&catName,
&catSlug,
&post.Excerpt,
&post.Content,
&post.ReadCount,
@@ -93,6 +136,21 @@ func GetPostByID(id uint) (*models.Post, error) {
return nil, err
}
if catID.Valid {
post.CategoryID = uint(catID.Int64)
post.Category = &models.Category{
ID: uint(catID.Int64),
Name: catName.String,
Slug: catSlug.String,
}
}
// 获取标签
tags, err := GetTagsByPostID(post.ID)
if err == nil {
post.Tags = tags
}
// 更新阅读量
updateReadCountQuery := "UPDATE posts SET read_count = read_count + 1 WHERE id = ?"
if _, err := config.DB.Exec(updateReadCountQuery, id); err != nil {
@@ -102,24 +160,42 @@ func GetPostByID(id uint) (*models.Post, error) {
return &post, nil
}
// GetAllPosts 获取所有博客文章(包括未发布的)
func GetAllPosts() ([]models.Post, error) {
selectFields := "id, title, category, excerpt, content, read_count, is_published, created_at, updated_at, deleted_at"
query := "SELECT " + selectFields + " FROM posts WHERE deleted_at = 0 ORDER BY created_at DESC"
rows, err := config.DB.Query(query)
// GetAllPosts 获取所有博客文章(包括未发布的,后台用
func GetAllPosts(page, pageSize int) ([]models.Post, int64, error) {
offset := (page - 1) * pageSize
// Count total
var total int64
config.DB.QueryRow("SELECT COUNT(*) FROM posts WHERE deleted_at = 0").Scan(&total)
query := `
SELECT p.id, p.title, p.category_id, c.name, c.slug, p.excerpt, p.content, p.read_count, p.is_published, p.created_at, p.updated_at, p.deleted_at
FROM posts p
LEFT JOIN categories c ON p.category_id = c.id
WHERE p.deleted_at = 0
ORDER BY p.created_at DESC
LIMIT ? OFFSET ?
`
rows, err := config.DB.Query(query, pageSize, offset)
if err != nil {
log.Printf("Error querying all posts: %v", err)
return nil, err
return nil, 0, err
}
defer rows.Close()
var posts []models.Post
for rows.Next() {
var post models.Post
var catID sql.NullInt64
var catName sql.NullString
var catSlug sql.NullString
if err := rows.Scan(
&post.ID,
&post.Title,
&post.Category,
&catID,
&catName,
&catSlug,
&post.Excerpt,
&post.Content,
&post.ReadCount,
@@ -131,23 +207,34 @@ func GetAllPosts() ([]models.Post, error) {
log.Printf("Error scanning post: %v", err)
continue
}
if catID.Valid {
post.CategoryID = uint(catID.Int64)
post.Category = &models.Category{
ID: uint(catID.Int64),
Name: catName.String,
Slug: catSlug.String,
}
}
posts = append(posts, post)
}
return posts, nil
return posts, total, nil
}
// CreatePost 创建博客文章
func CreatePost(post *models.Post) error {
now := time.Now().Unix()
// Insert Post
query := `
INSERT INTO posts (title, category, excerpt, content, is_published, created_at, updated_at, deleted_at)
INSERT INTO posts (title, category_id, excerpt, content, is_published, created_at, updated_at, deleted_at)
VALUES (?, ?, ?, ?, ?, ?, ?, 0)
`
result, err := config.DB.Exec(
query,
post.Title,
post.Category,
post.CategoryID,
post.Excerpt,
post.Content,
post.IsPublished,
@@ -167,6 +254,13 @@ func CreatePost(post *models.Post) error {
post.CreatedAt = now
post.UpdatedAt = now
// Insert Tags
if len(post.Tags) > 0 {
for _, tag := range post.Tags {
AddTagToPost(post.ID, tag.ID)
}
}
return nil
}
@@ -174,13 +268,13 @@ func CreatePost(post *models.Post) error {
func UpdatePost(post *models.Post) error {
now := time.Now().Unix()
query := `
UPDATE posts SET title = ?, category = ?, excerpt = ?, content = ?, is_published = ?, updated_at = ?
UPDATE posts SET title = ?, category_id = ?, excerpt = ?, content = ?, is_published = ?, updated_at = ?
WHERE id = ? AND deleted_at = 0
`
_, err := config.DB.Exec(
query,
post.Title,
post.Category,
post.CategoryID,
post.Excerpt,
post.Content,
post.IsPublished,
@@ -192,9 +286,26 @@ func UpdatePost(post *models.Post) error {
return err
}
// Update Tags: Delete all and re-insert
// Note: This is a simple approach. Better approach is to diff.
config.DB.Exec("DELETE FROM post_tags WHERE post_id = ?", post.ID)
if len(post.Tags) > 0 {
for _, tag := range post.Tags {
AddTagToPost(post.ID, tag.ID)
}
}
return nil
}
// UpdatePostStatus 更新文章状态
func UpdatePostStatus(id uint, status int) error {
now := time.Now().Unix()
query := "UPDATE posts SET is_published = ?, updated_at = ? WHERE id = ? AND deleted_at = 0"
_, err := config.DB.Exec(query, status, now, id)
return err
}
// DeletePost 删除博客文章 (Soft Delete)
func DeletePost(id uint) error {
now := time.Now().Unix()
@@ -204,7 +315,6 @@ func DeletePost(id uint) error {
log.Printf("Error deleting post: %v", err)
return err
}
return nil
}
@@ -228,12 +338,22 @@ func BuildPostResponse(post *models.Post, includeContent bool) *models.PostRespo
// Format CreatedAt to Date string
dateStr := time.Unix(post.CreatedAt, 0).Format("2006-01-02")
catName := ""
catSlug := ""
if post.Category != nil {
catName = post.Category.Name
catSlug = post.Category.Slug
}
response := &models.PostResponse{
ID: post.ID,
Title: post.Title,
Category: post.Category,
Date: dateStr,
Excerpt: post.Excerpt,
ID: post.ID,
Title: post.Title,
CategoryID: post.CategoryID,
CategoryName: catName,
CategorySlug: catSlug,
Date: dateStr,
Excerpt: post.Excerpt,
Tags: post.Tags,
}
if includeContent {
@@ -263,48 +383,18 @@ func SavePostHistory(post *models.Post, modifiedBy uint) error {
}
now := time.Now().Unix()
// 插入新的历史记录 (PostHistory struct updated to int64)
// Note: post_history table also needs to be updated to support bigint timestamps if not already.
// Assuming user wanted ALL tables updated, but I missed checking post_history structure explicitly in sql file scan.
// But assuming I applied "all tables" logic if it existed.
// Wait, post_history wasn't in the SQL file dump I read earlier?
// I will double check. If it's missing, I might get errors.
// The SQL dump showed `posts`, `users` etc. `post_history` was NOT in the dump I read?
// Let me check the Read output again.
// It wasn't there! `post_tags` was there. `post_history` is missing from the SQL dump provided by the user?
// Or maybe I missed it.
// If it doesn't exist, this code will fail.
// But `GetPostHistory` exists in the repo, so the table MUST exist.
// I will assume it exists and uses the same convention.
// PostHistory model has Date string?
// Check models/post.go:
// type PostHistory struct { ... Date string ... }
// The struct I updated earlier removed Date?
// No, I checked PostHistory in models/post.go, it had Date string.
// And I updated it to:
// Date string (removed?)
// Let's check my model update for PostHistory.
// I removed `Date string` from PostHistory?
// `type PostHistory struct { ... Title string; Category string; Excerpt string ... }`
// Yes, I removed Date.
// So I should remove `date` from Insert too.
insertQuery := `
INSERT INTO post_history (
post_id, version, title, category, excerpt, content,
post_id, version, title, category_id, excerpt, content,
is_published, modified_by, modified_at, created_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`
// Date string generation? Post history usually snapshots the post state.
// If post has no date column, history shouldn't either.
_, err := config.DB.Exec(
insertQuery,
post.ID,
maxVersion+1,
post.Title,
post.Category,
post.CategoryID,
post.Excerpt,
post.Content,
post.IsPublished,
@@ -320,118 +410,31 @@ func SavePostHistory(post *models.Post, modifiedBy uint) error {
return nil
}
// GetPostHistory 获取文章历史记录
func GetPostHistory(postID uint) ([]models.PostHistory, error) {
query := `
SELECT id, post_id, version, title, category, excerpt, content,
is_published, modified_by, modified_at, created_at
FROM post_history
WHERE post_id = ?
ORDER BY version DESC
`
rows, err := config.DB.Query(query, postID)
// GetTopPosts 获取热门文章 (按阅读量)
func GetTopPosts(limit int) ([]models.Post, error) {
// Simple query without category join for dashboard to avoid complexity if not needed
// Or join if needed. Dashboard usually needs Title.
query := "SELECT id, title, read_count FROM posts WHERE is_published = 1 AND deleted_at = 0 ORDER BY read_count DESC LIMIT ?"
rows, err := config.DB.Query(query, limit)
if err != nil {
log.Printf("Error querying post history: %v", err)
return nil, err
}
defer rows.Close()
var history []models.PostHistory
var posts []models.Post
for rows.Next() {
var h models.PostHistory
if err := rows.Scan(
&h.ID,
&h.PostID,
&h.Version,
&h.Title,
&h.Category,
&h.Excerpt,
&h.Content,
&h.IsPublished,
&h.ModifiedBy,
&h.ModifiedAt,
&h.CreatedAt,
); err != nil {
log.Printf("Error scanning post history: %v", err)
var post models.Post
if err := rows.Scan(&post.ID, &post.Title, &post.ReadCount); err != nil {
continue
}
history = append(history, h)
posts = append(posts, post)
}
return history, nil
return posts, nil
}
// GetPostHistoryByVersion 获取指定版本的文章历史记录
func GetPostHistoryByVersion(postID uint, version uint) (*models.PostHistory, error) {
query := `
SELECT id, post_id, version, title, category, excerpt, content,
is_published, modified_by, modified_at, created_at
FROM post_history
WHERE post_id = ? AND version = ?
`
row := config.DB.QueryRow(query, postID, version)
var h models.PostHistory
if err := row.Scan(
&h.ID,
&h.PostID,
&h.Version,
&h.Title,
&h.Category,
&h.Excerpt,
&h.Content,
&h.IsPublished,
&h.ModifiedBy,
&h.ModifiedAt,
&h.CreatedAt,
); err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
log.Printf("Error scanning post history by version: %v", err)
return nil, err
}
return &h, nil
}
// BuildPostHistoryResponse 构建文章历史记录响应
func BuildPostHistoryResponse(history *models.PostHistory) *models.PostHistoryResponse {
return &models.PostHistoryResponse{
ID: history.ID,
PostID: history.PostID,
Version: history.Version,
Title: history.Title,
Category: history.Category,
Date: time.Unix(history.CreatedAt, 0).Format("2006-01-02"), // Compute date
IsPublished: history.IsPublished,
ModifiedBy: history.ModifiedBy,
ModifiedAt: time.Unix(history.ModifiedAt, 0).Format("2006-01-02 15:04:05"),
CreatedAt: time.Unix(history.CreatedAt, 0).Format("2006-01-02 15:04:05"),
}
}
// BuildPostHistoryResponses 构建文章历史记录列表响应
func BuildPostHistoryResponses(history []models.PostHistory) []models.PostHistoryResponse {
var responses []models.PostHistoryResponse
for _, h := range history {
responses = append(responses, *BuildPostHistoryResponse(&h))
}
return responses
}
// TrendData 趋势数据结构
type TrendData struct {
Date string `json:"date"`
Count int `json:"value"`
YoY float64 `json:"yoy"` // Year-over-Year 同比
MoM float64 `json:"mom"` // Month-over-Month 环比
}
// GetNewPostsTrend 获取新增文章趋势 (带同比环比)
// 支持按日/周/月/年维度统计
// GetNewPostsTrend 获取新增文章趋势
func GetNewPostsTrend(startDate, endDate string) ([]TrendData, error) {
// Use FROM_UNIXTIME to format timestamp
// Same as before
query := `
SELECT FROM_UNIXTIME(created_at, '%Y-%m-%d') as date, COUNT(*) as count
FROM posts
@@ -444,7 +447,6 @@ func GetNewPostsTrend(startDate, endDate string) ([]TrendData, error) {
query += " AND created_at >= ?"
args = append(args, startUnix)
} else {
// Default 7 days
startUnix := time.Now().AddDate(0, 0, -6).Unix()
query += " AND created_at >= ?"
args = append(args, startUnix)
@@ -473,7 +475,6 @@ func GetNewPostsTrend(startDate, endDate string) ([]TrendData, error) {
if err := rows.Scan(&r.Date, &r.Count); err != nil {
return nil, err
}
// 暂未实现真实的同比环比计算逻辑设为0
r.YoY = 0
r.MoM = 0
results = append(results, r)
@@ -481,34 +482,75 @@ func GetNewPostsTrend(startDate, endDate string) ([]TrendData, error) {
return results, nil
}
// GetTopPosts 获取热门文章 (按阅读量)
func GetTopPosts(limit int) ([]models.Post, error) {
selectFields := "id, title, category, excerpt, content, read_count, is_published, created_at, updated_at, deleted_at"
query := "SELECT " + selectFields + " FROM posts WHERE is_published = 1 AND deleted_at = 0 ORDER BY read_count DESC LIMIT ?"
rows, err := config.DB.Query(query, limit)
// GetPostHistory 获取文章修改历史
func GetPostHistory(postID uint) ([]models.PostHistory, error) {
query := `
SELECT id, post_id, version, title, category_id, excerpt, content, is_published, modified_by, modified_at, created_at
FROM post_history
WHERE post_id = ?
ORDER BY version DESC
`
rows, err := config.DB.Query(query, postID)
if err != nil {
return nil, err
}
defer rows.Close()
var posts []models.Post
var history []models.PostHistory
for rows.Next() {
var post models.Post
var h models.PostHistory
if err := rows.Scan(
&post.ID,
&post.Title,
&post.Category,
&post.Excerpt,
&post.Content,
&post.ReadCount,
&post.IsPublished,
&post.CreatedAt,
&post.UpdatedAt,
&post.DeletedAt,
&h.ID, &h.PostID, &h.Version, &h.Title, &h.CategoryID,
&h.Excerpt, &h.Content, &h.IsPublished,
&h.ModifiedBy, &h.ModifiedAt, &h.CreatedAt,
); err != nil {
continue
}
posts = append(posts, post)
history = append(history, h)
}
return posts, nil
return history, nil
}
// GetPostHistoryByVersion 获取特定版本的历史记录
func GetPostHistoryByVersion(postID uint, version uint) (*models.PostHistory, error) {
query := `
SELECT id, post_id, version, title, category_id, excerpt, content, is_published, modified_by, modified_at, created_at
FROM post_history
WHERE post_id = ? AND version = ?
`
var h models.PostHistory
err := config.DB.QueryRow(query, postID, version).Scan(
&h.ID, &h.PostID, &h.Version, &h.Title, &h.CategoryID,
&h.Excerpt, &h.Content, &h.IsPublished,
&h.ModifiedBy, &h.ModifiedAt, &h.CreatedAt,
)
if err != nil {
return nil, err
}
return &h, nil
}
// BuildPostHistoryResponse 构建历史记录响应
func BuildPostHistoryResponse(h *models.PostHistory) *models.PostHistoryResponse {
return &models.PostHistoryResponse{
ID: h.ID,
PostID: h.PostID,
Version: h.Version,
Title: h.Title,
CategoryID: h.CategoryID,
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"),
}
}
// BuildPostHistoryResponses 构建历史记录列表响应
func BuildPostHistoryResponses(history []models.PostHistory) []models.PostHistoryResponse {
var responses []models.PostHistoryResponse
for _, h := range history {
responses = append(responses, *BuildPostHistoryResponse(&h))
}
return responses
}

View File

@@ -145,11 +145,38 @@ func BuildSettingResponse(setting *models.Setting) *models.SettingResponse {
}
}
// BuildSettingsResponse 构建系统配置列表响应
func BuildSettingsResponse(settings []models.Setting) []models.SettingResponse {
var responses []models.SettingResponse
for _, setting := range settings {
responses = append(responses, *BuildSettingResponse(&setting))
// GetAllSettings 获取所有系统配置 (Map format for easier consumption)
func GetAllSettings() (map[string]string, error) {
settings, err := GetSettings()
if err != nil {
return nil, err
}
return responses
result := make(map[string]string)
for _, s := range settings {
result[s.KeyName] = s.Value
}
return result, nil
}
// UpdateSettings 批量更新系统配置
func UpdateSettings(settings map[string]string) error {
tx, err := config.DB.Begin()
if err != nil {
return err
}
now := time.Now().Unix()
query := "UPDATE settings SET value = ?, updated_at = ? WHERE key_name = ? AND deleted_at = 0"
for key, value := range settings {
_, err := tx.Exec(query, value, now, key)
if err != nil {
tx.Rollback()
log.Printf("Error updating setting %s: %v", key, err)
return err
}
}
return tx.Commit()
}

View File

@@ -156,17 +156,53 @@ func DeleteSnippet(id string) error {
return nil
}
// GetSnippetCount 获取代码片段总数
func GetSnippetCount() (int, error) {
var count int
query := "SELECT COUNT(*) FROM snippets WHERE deleted_at = 0"
row := config.DB.QueryRow(query)
// GetAdminSnippets 获取后台代码片段列表 (分页)
func GetAdminSnippets(page, pageSize int) ([]models.Snippet, int, error) {
offset := (page - 1) * pageSize
err := row.Scan(&count)
// 获取总数
var total int
countQuery := "SELECT COUNT(*) FROM snippets WHERE deleted_at = 0"
err := config.DB.QueryRow(countQuery).Scan(&total)
if err != nil {
log.Printf("Error getting snippet count: %v", err)
return 0, err
return nil, 0, err
}
return count, nil
// 获取列表
query := `
SELECT id, title, code, type, description, view_count, created_at, updated_at, deleted_at
FROM snippets
WHERE deleted_at = 0
ORDER BY created_at DESC
LIMIT ? OFFSET ?
`
rows, err := config.DB.Query(query, pageSize, offset)
if err != nil {
log.Printf("Error querying admin snippets: %v", err)
return nil, 0, err
}
defer rows.Close()
var snippets []models.Snippet
for rows.Next() {
var snippet models.Snippet
if err := rows.Scan(
&snippet.ID,
&snippet.Title,
&snippet.Code,
&snippet.Type,
&snippet.Description,
&snippet.ViewCount,
&snippet.CreatedAt,
&snippet.UpdatedAt,
&snippet.DeletedAt,
); err != nil {
log.Printf("Error scanning snippet: %v", err)
continue
}
snippets = append(snippets, snippet)
}
return snippets, total, nil
}

View File

@@ -174,7 +174,7 @@ func DeleteTag(id uint) error {
}
// GetTagsByPostID 根据文章ID获取标签
func GetTagsByPostID(postID string) ([]models.Tag, error) {
func GetTagsByPostID(postID uint) ([]models.Tag, error) {
query := `
SELECT t.id, t.name, t.slug, t.created_at, t.updated_at, t.deleted_at
FROM tags t
@@ -210,7 +210,7 @@ func GetTagsByPostID(postID string) ([]models.Tag, error) {
}
// AddTagToPost 为文章添加标签
func AddTagToPost(postID string, tagID uint) error {
func AddTagToPost(postID uint, tagID uint) error {
now := time.Now().Unix()
query := `
INSERT IGNORE INTO post_tags (post_id, tag_id, created_at)
@@ -226,7 +226,7 @@ func AddTagToPost(postID string, tagID uint) error {
}
// RemoveTagFromPost 从文章移除标签
func RemoveTagFromPost(postID string, tagID uint) error {
func RemoveTagFromPost(postID uint, tagID uint) error {
query := "DELETE FROM post_tags WHERE post_id = ? AND tag_id = ?"
_, err := config.DB.Exec(query, postID, tagID)
if err != nil {

View File

@@ -95,19 +95,31 @@ func GetUserByID(id uint) (*models.User, error) {
return &user, nil
}
// GetUsers 获取所有用户
func GetUsers() ([]models.User, error) {
// GetUsers 获取所有用户 (分页)
func GetUsers(page, pageSize int) ([]models.User, int, error) {
offset := (page - 1) * pageSize
// 获取总数
var total int
countQuery := "SELECT COUNT(*) FROM users WHERE deleted_at = 0"
err := config.DB.QueryRow(countQuery).Scan(&total)
if err != nil {
log.Printf("Error getting user count: %v", err)
return nil, 0, err
}
query := `
SELECT u.id, u.username, u.email, u.password_hash, u.role_id, COALESCE(r.name, u.role), u.is_active, u.created_at, u.updated_at, u.deleted_at
FROM users u
LEFT JOIN roles r ON u.role_id = r.id
WHERE u.deleted_at = 0
ORDER BY u.created_at DESC
LIMIT ? OFFSET ?
`
rows, err := config.DB.Query(query)
rows, err := config.DB.Query(query, pageSize, offset)
if err != nil {
log.Printf("Error querying users: %v", err)
return nil, err
return nil, 0, err
}
defer rows.Close()
@@ -143,7 +155,7 @@ func GetUsers() ([]models.User, error) {
users = append(users, user)
}
return users, nil
return users, total, nil
}
// CreateUser 创建用户

View File

@@ -298,6 +298,74 @@ func DeleteWork(id string) error {
return nil
}
// GetAdminWorks 获取后台作品列表 (分页)
func GetAdminWorks(page, pageSize int) ([]models.Work, int, error) {
offset := (page - 1) * pageSize
// 获取总数
var total int
countQuery := "SELECT COUNT(*) FROM works WHERE deleted_at = 0"
err := config.DB.QueryRow(countQuery).Scan(&total)
if err != nil {
log.Printf("Error getting work count: %v", err)
return nil, 0, err
}
query := `
SELECT id, title, category, year, hero_img, description, is_featured, created_at, updated_at, deleted_at
FROM works
WHERE deleted_at = 0
ORDER BY created_at DESC
LIMIT ? OFFSET ?
`
rows, err := config.DB.Query(query, pageSize, offset)
if err != nil {
log.Printf("Error querying admin works: %v", err)
return nil, 0, err
}
defer rows.Close()
var works []models.Work
for rows.Next() {
var work models.Work
if err := rows.Scan(
&work.ID,
&work.Title,
&work.Category,
&work.Year,
&work.HeroImg,
&work.Description,
&work.IsFeatured,
&work.CreatedAt,
&work.UpdatedAt,
&work.DeletedAt,
); err != nil {
log.Printf("Error scanning work: %v", err)
continue
}
works = append(works, work)
}
return works, total, nil
}
// BuildWorksResponse 构建作品列表响应
func BuildWorksResponse(works []models.Work) []models.WorkResponse {
var responses []models.WorkResponse
for _, work := range works {
// 这里不包含详情,简化处理
responses = append(responses, models.WorkResponse{
ID: work.ID,
Title: work.Title,
Category: work.Category,
Year: work.Year,
HeroImg: work.HeroImg,
Desc: work.Description,
})
}
return responses
}
// GetWorkCount 获取作品总数
func GetWorkCount() (int, error) {
var count int

17
server/utils/password.go Normal file
View File

@@ -0,0 +1,17 @@
package utils
import (
"golang.org/x/crypto/bcrypt"
)
// HashPassword hashes a password using bcrypt
func HashPassword(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14)
return string(bytes), err
}
// CheckPasswordHash checks if the provided password matches the hashed password
func CheckPasswordHash(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}