数据结构优化

This commit is contained in:
李琦
2026-01-20 14:31:39 +08:00
parent 05b315013c
commit 946855bff2
16 changed files with 104045 additions and 157 deletions

View File

@@ -347,7 +347,7 @@ const runCode = async () => {
doc.write(loadingTemplate)
try {
const response = await fetch('http://localhost:8081/api/run', {
const response = await fetch('/api/run', {
method: 'POST',
headers: {
'Content-Type': 'application/json'

View File

@@ -115,7 +115,7 @@ const uploadFile = async (file: File) => {
formData.append('storageType', 'local')
const token = localStorage.getItem('token')
const response = await fetch('http://localhost:8081/api/admin/attachments/upload', {
const response = await fetch('/api/admin/attachments/upload', {
method: 'POST',
headers: {
...(token ? { Authorization: `Bearer ${token}` } : {})

View File

@@ -95,12 +95,16 @@ const drawStars = () => {
}
}
ctx.globalAlpha = star.alpha
ctx.beginPath()
ctx.arc(star.x, star.y, star.radius, 0, Math.PI * 2)
ctx.fill()
if (ctx) {
ctx.globalAlpha = star.alpha
ctx.beginPath()
ctx.arc(star.x, star.y, star.radius, 0, Math.PI * 2)
ctx.fill()
}
})
ctx.globalAlpha = 1
if (ctx) {
ctx.globalAlpha = 1
}
}
// 生成流星

File diff suppressed because it is too large Load Diff

View File

@@ -191,7 +191,7 @@ const uploadCategoryOptions = computed(() => {
})
const API_BASE = 'http://localhost:8081/api'
const API_BASE = '/api'
const getAuthHeaders = () => {
const token = localStorage.getItem('token')
return {

View File

@@ -344,7 +344,7 @@ const handlePaste = async (e: ClipboardEvent) => {
formData.append('storageType', 'local')
const token = localStorage.getItem('token')
const response = await fetch('http://localhost:8081/api/admin/attachments/upload', {
const response = await fetch('/api/admin/attachments/upload', {
method: 'POST',
headers: {
...(token ? { Authorization: `Bearer ${token}` } : {})

View File

@@ -64,12 +64,12 @@
</div>
<!-- Charts Row 2 -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- User Regions -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
<!-- Operation Trend -->
<div class="chart-card relative">
<h3 class="chart-title">用户地域分布</h3>
<div class="h-80" v-if="!loading && hasData(stats.userRegions)">
<v-chart class="chart" :option="regionOption" autoresize />
<h3 class="chart-title">管理员操作统计</h3>
<div class="h-80" v-if="!loading && hasData(stats.operationTrend)">
<v-chart class="chart" :option="operationTrendOption" autoresize />
</div>
<div v-else-if="loading" class="h-80 flex items-center justify-center text-white/20">
加载中...
@@ -93,6 +93,26 @@
</div>
</div>
</div>
<!-- User Regions - Full Width -->
<div class="chart-card relative">
<h3 class="chart-title">用户地域分布</h3>
<div class="h-[800px]" v-if="!loading && hasData(stats.userRegions) && mapDataLoaded">
<v-chart class="chart" :option="regionOption" autoresize />
</div>
<div v-else-if="loading" class="h-[800px] flex items-center justify-center text-white/20">
加载中...
</div>
<div v-else-if="!mapDataLoaded" class="h-[800px] flex items-center justify-center text-white/40">
<div class="text-center">
<p class="mb-2">地图数据加载失败</p>
<p class="text-sm text-white/60">请检查网络连接或稍后重试</p>
</div>
</div>
<div v-else class="h-[800px] flex items-center justify-center text-white/20">
暂无数据
</div>
</div>
</div>
</template>
@@ -113,6 +133,7 @@ import * as echarts from 'echarts/core'
import VChart from 'vue-echarts'
import { getDashboardStats } from '../../../services/api'
import { useToast } from '../../../composables/useToast'
import chinaMapData from '../../../composables/full.json'
use([
CanvasRenderer,
@@ -278,44 +299,6 @@ const uvTrendOption = computed(() => ({
}]
}))
// 省份名称映射(确保与地图数据一致)
const provinceNameMap: Record<string, string> = {
'北京': '北京',
'天津': '天津',
'河北': '河北',
'山西': '山西',
'内蒙古': '内蒙古',
'辽宁': '辽宁',
'吉林': '吉林',
'黑龙江': '黑龙江',
'上海': '上海',
'江苏': '江苏',
'浙江': '浙江',
'安徽': '安徽',
'福建': '福建',
'江西': '江西',
'山东': '山东',
'河南': '河南',
'湖北': '湖北',
'湖南': '湖南',
'广东': '广东',
'广西': '广西',
'海南': '海南',
'重庆': '重庆',
'四川': '四川',
'贵州': '贵州',
'云南': '云南',
'西藏': '西藏',
'陕西': '陕西',
'甘肃': '甘肃',
'青海': '青海',
'宁夏': '宁夏',
'新疆': '新疆',
'台湾': '台湾',
'香港': '香港',
'澳门': '澳门'
}
// 所有省份列表(用于设置默认值)
const allProvinces = [
'北京', '天津', '河北', '山西', '内蒙古', '辽宁', '吉林', '黑龙江',
@@ -354,6 +337,20 @@ const getMapData = () => {
}
const regionOption = computed(() => {
// 如果地图数据未加载,返回空配置
if (!mapDataLoaded.value) {
return {
title: {
text: '地图数据加载中...',
left: 'center',
top: 'middle',
textStyle: {
color: '#fff'
}
}
}
}
const mapData = getMapData()
const maxValue = mapData.length > 0 ? Math.max(...mapData.map((d: any) => d[1] || 0)) : 1
@@ -372,7 +369,7 @@ const regionOption = computed(() => {
min: 0,
max: maxValue || 100,
left: 'left',
top: 'bottom',
top: 'middle',
text: ['高', '低'],
inRange: {
color: ['#1a1a1a', '#d4b383']
@@ -380,11 +377,15 @@ const regionOption = computed(() => {
textStyle: {
color: '#fff'
},
calculable: true
calculable: true,
itemWidth: 12,
itemHeight: 200
},
geo: {
map: 'china',
roam: false,
layoutCenter: ['55%', '50%'],
layoutSize: '95%',
itemStyle: {
areaColor: 'rgba(255, 255, 255, 0.05)',
borderColor: 'rgba(255, 255, 255, 0.2)',
@@ -398,7 +399,7 @@ const regionOption = computed(() => {
label: {
show: true,
color: 'rgba(255, 255, 255, 0.8)',
fontSize: 10
fontSize: 14
}
},
series: [
@@ -421,7 +422,7 @@ const regionOption = computed(() => {
},
label: {
color: '#fff',
fontSize: 12
fontSize: 16
}
}
}
@@ -429,6 +430,33 @@ const regionOption = computed(() => {
}
})
const operationTrendOption = computed(() => ({
tooltip: {
trigger: 'axis',
backgroundColor: 'rgba(0,0,0,0.8)',
borderColor: '#333',
textStyle: { color: '#fff' }
},
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
xAxis: {
type: 'category',
data: stats.value.operationTrend?.map((i: any) => i.date) || [],
axisLabel: { color: '#ccc' }
},
yAxis: {
type: 'value',
axisLabel: { color: '#ccc' },
splitLine: { lineStyle: { color: 'rgba(255,255,255,0.1)' } }
},
series: [{
name: '操作次数',
data: stats.value.operationTrend?.map((i: any) => i.value) || [],
type: 'bar',
itemStyle: { color: '#f59e0b' },
barWidth: '40%'
}]
}))
const topPostsOption = computed(() => ({
tooltip: {
trigger: 'axis',
@@ -475,21 +503,69 @@ const loadData = async () => {
}
}
// 地图数据加载状态
const mapDataLoaded = ref(false)
// 加载中国地图数据
const loadChinaMap = async () => {
try {
// 使用 echarts 官方的中国地图数据(从 CDN 加载)
const response = await fetch('https://geo.datav.aliyun.com/areas_v3/bound/100000_full.json')
if (response.ok) {
const geoJson = await response.json()
// 注册地图数据
echarts.registerMap('china', geoJson)
} else {
console.warn('Failed to load China map data, using fallback')
// 优先使用本地地图数据
if (chinaMapData && chinaMapData.type === 'FeatureCollection') {
echarts.registerMap('china', chinaMapData as any)
mapDataLoaded.value = true
console.log('China map data loaded successfully from local file')
return
}
} catch (error) {
console.error('Error loading China map:', error)
console.warn('Failed to load local map data:', error)
}
// 如果本地数据加载失败,尝试备用数据源
const mapDataSources = [
'https://geo.datav.aliyun.com/areas_v3/bound/100000_full.json',
'https://cdn.jsdelivr.net/npm/echarts@5/map/json/china.json',
'https://unpkg.com/echarts@5/map/json/china.json'
]
for (const url of mapDataSources) {
try {
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), 10000) // 10秒超时
const response = await fetch(url, {
mode: 'cors',
credentials: 'omit',
signal: controller.signal
})
clearTimeout(timeoutId)
if (response.ok) {
const geoJson = await response.json()
// 验证数据格式
if (geoJson && geoJson.type === 'FeatureCollection') {
// 注册地图数据
echarts.registerMap('china', geoJson)
mapDataLoaded.value = true
console.log('China map data loaded successfully from:', url)
return
} else {
console.warn(`Invalid map data format from ${url}`)
}
}
} catch (error: any) {
if (error.name === 'AbortError') {
console.warn(`Timeout loading map data from ${url}`)
} else {
console.warn(`Failed to load map data from ${url}:`, error)
}
continue
}
}
// 所有数据源都失败
console.error('All map data sources failed, map will not be displayed')
mapDataLoaded.value = false
}
onMounted(async () => {

View File

@@ -1,4 +1,3 @@
// export const API_BASE = 'http://localhost:8081/api'
export const API_BASE = '/api'
// 通用请求头配置
@@ -145,6 +144,7 @@ export interface OperationLog {
userId: number
username: string
ip: string
region?: string
path: string
method: string
params: string

View File

@@ -1,6 +1,9 @@
package handlers
import (
"net/url"
"strings"
"github.com/gin-gonic/gin"
"github.com/niangaodev/art-code/repositories"
"github.com/niangaodev/art-code/utils"
@@ -11,6 +14,20 @@ func GetDashboardStats(c *gin.Context) {
startDate := c.Query("startDate")
endDate := c.Query("endDate")
// URL解码日期参数处理+号被编码的情况
if startDate != "" {
if decoded, err := url.QueryUnescape(startDate); err == nil {
// 将+号替换为空格URL编码中+可能代表空格)
startDate = strings.ReplaceAll(decoded, "+", " ")
}
}
if endDate != "" {
if decoded, err := url.QueryUnescape(endDate); err == nil {
// 将+号替换为空格URL编码中+可能代表空格)
endDate = strings.ReplaceAll(decoded, "+", " ")
}
}
// 1. 获取博文增长趋势
postsTrend, err := repositories.GetNewPostsTrend(startDate, endDate)
if err != nil {
@@ -26,10 +43,8 @@ func GetDashboardStats(c *gin.Context) {
// 3. 用户画像-地域分布
userRegions, err := repositories.GetUserRegions(startDate, endDate)
if err != nil {
userRegions = []struct {
Region string
Count int
}{}
// 即使出错也返回所有34个省份的默认数据Count为0
userRegions = repositories.GetDefaultUserRegions()
}
// 4. 热门文章 Top 5
@@ -82,13 +97,20 @@ func GetDashboardStats(c *gin.Context) {
// 7. 文章总数
postCount, _ := repositories.GetPostCount()
// 8. 管理员操作统计
operationTrend, err := repositories.GetOperationTrend(startDate, endDate)
if err != nil {
operationTrend = []repositories.OperationTrendData{}
}
utils.Success(c, gin.H{
"postsTrend": postsTrend,
"uvTrend": uvTrend,
"userRegions": userRegions,
"topPosts": topPosts,
"inquiryCount": inquiryCount,
"posts": postCount,
"works": workCount,
"postsTrend": postsTrend,
"uvTrend": uvTrend,
"userRegions": userRegions,
"topPosts": topPosts,
"inquiryCount": inquiryCount,
"posts": postCount,
"works": workCount,
"operationTrend": operationTrend,
})
}

View File

@@ -113,11 +113,35 @@ func GetPost(c *gin.Context) {
// 记录用户访问日志 (异步执行,不阻塞响应)
go func() {
// 添加 panic recover 保护
defer func() {
if r := recover(); r != nil {
log.Printf("Panic in user access log goroutine for post %d: %v", post.ID, r)
}
}()
// 获取客户端IP
ip := c.ClientIP()
// 获取归属地
location := utils.GetRegion(ip)
// 获取归属地,使用 recover 保护
var location string
func() {
defer func() {
if r := recover(); r != nil {
log.Printf("Panic in GetRegion for IP %s (post %d): %v", ip, post.ID, r)
location = "Unknown"
}
}()
location = utils.GetRegion(ip)
}()
// 确保 location 不为空,如果为空则设置为 "Unknown"
if location == "" {
location = "Unknown"
}
// 记录调试信息
log.Printf("Creating user access log: PostID=%d, IP=%s, Location=%s", post.ID, ip, location)
// 获取用户ID (如果已登录)
var userID uint = 0
@@ -133,7 +157,9 @@ func GetPost(c *gin.Context) {
}
if err := repositories.CreateUserAccessLog(logEntry); err != nil {
log.Printf("Failed to create access log: %v", err)
log.Printf("Failed to create user access log for PostID=%d, IP=%s, Location=%s: %v", post.ID, ip, location, err)
} else {
log.Printf("Successfully created user access log: PostID=%d, IP=%s, Location=%s", post.ID, ip, location)
}
}()

View File

@@ -1,6 +1,7 @@
package middleware
import (
"log"
"time"
"github.com/gin-gonic/gin"
@@ -12,6 +13,13 @@ import (
// AccessLogMiddleware 访问日志中间件 (用于流量统计)
func AccessLogMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// 添加 panic recover 保护整个中间件
defer func() {
if r := recover(); r != nil {
log.Printf("Panic in AccessLogMiddleware: %v", r)
}
}()
start := time.Now()
c.Next()
@@ -24,9 +32,27 @@ func AccessLogMiddleware() gin.HandlerFunc {
ip := c.ClientIP()
// 获取归属地 (需要先初始化ip2region)
region := utils.GetRegion(ip)
// 使用 recover 保护,确保即使 GetRegion 失败也能继续记录日志
var region string
func() {
defer func() {
if r := recover(); r != nil {
log.Printf("Panic in GetRegion for IP %s (access log): %v", ip, r)
region = "Unknown"
}
}()
region = utils.GetRegion(ip)
}()
log := &models.AccessLog{
// 如果 region 为空,设置为 Unknown
if region == "" {
region = "Unknown"
}
// 记录调试信息
log.Printf("Creating access log: IP=%s, Region=%s, Path=%s, Method=%s", ip, region, c.Request.URL.Path, c.Request.Method)
logEntry := &models.AccessLog{
IP: ip,
UserAgent: c.Request.UserAgent(),
Path: c.Request.URL.Path,
@@ -36,9 +62,19 @@ func AccessLogMiddleware() gin.HandlerFunc {
Region: region,
}
// 异步入库
// 异步入库,添加错误处理和 panic 保护
go func() {
repositories.CreateAccessLog(log)
defer func() {
if r := recover(); r != nil {
log.Printf("Panic in CreateAccessLog goroutine: %v", r)
}
}()
if err := repositories.CreateAccessLog(logEntry); err != nil {
log.Printf("Error creating access log for IP=%s, Region=%s, Path=%s: %v", ip, region, c.Request.URL.Path, err)
} else {
log.Printf("Successfully created access log: IP=%s, Region=%s, Path=%s", ip, region, c.Request.URL.Path)
}
}()
}
}

View File

@@ -49,7 +49,25 @@ func OperationLogMiddleware() gin.HandlerFunc {
// 获取IP归属地
ip := c.ClientIP()
region := utils.GetRegion(ip)
var region string
// 使用 recover 保护 GetRegion 调用
func() {
defer func() {
if r := recover(); r != nil {
log.Printf("Panic in GetRegion for IP %s (operation log): %v", ip, r)
region = "Unknown"
}
}()
region = utils.GetRegion(ip)
}()
// 确保 region 不为空,如果为空则设置为 "Unknown"
if region == "" {
region = "Unknown"
}
// 记录调试信息
log.Printf("Creating operation log: UserID=%d, IP=%s, Region=%s, Path=%s", userID.(uint), ip, region, c.Request.URL.Path)
// 构建操作日志
operationLog := &models.OperationLog{
@@ -66,8 +84,16 @@ func OperationLogMiddleware() gin.HandlerFunc {
// 异步记录日志,避免影响响应
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("Panic in CreateOperationLog goroutine: %v", r)
}
}()
if err := repositories.CreateOperationLog(operationLog); err != nil {
log.Printf("Error creating operation log: %v", err)
log.Printf("Error creating operation log for UserID=%d, IP=%s, Region=%s: %v", userID.(uint), ip, region, err)
} else {
log.Printf("Successfully created operation log: UserID=%d, IP=%s, Region=%s", userID.(uint), ip, region)
}
}()
}

View File

@@ -11,7 +11,7 @@
Target Server Version : 80407 (8.4.7)
File Encoding : 65001
Date: 19/01/2026 21:19:15
Date: 20/01/2026 13:33:04
*/
SET NAMES utf8mb4;
@@ -259,6 +259,7 @@ CREATE TABLE `operation_logs` (
`user_id` bigint UNSIGNED NOT NULL COMMENT '操作用户ID',
`username` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '操作用户名',
`ip` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '操作IP地址',
`region` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT 'IP归属地',
`path` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '操作路径',
`method` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'HTTP方法',
`params` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '请求参数',
@@ -268,11 +269,235 @@ 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 = 644 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '操作日志表' ROW_FORMAT = DYNAMIC;
) ENGINE = InnoDB AUTO_INCREMENT = 225 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '操作日志表' ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- Records of operation_logs
-- ----------------------------
INSERT INTO `operation_logs` VALUES (1, 1, 'lq', '::1', NULL, '/api/admin/oss-configs', 'GET', '', 200, 97, 0, 1768828795);
INSERT INTO `operation_logs` VALUES (2, 1, 'lq', '::1', NULL, '/api/admin/columns', 'GET', '', 200, 51, 0, 1768828812);
INSERT INTO `operation_logs` VALUES (3, 1, 'lq', '::1', NULL, '/api/admin/columns', 'GET', '', 200, 45, 0, 1768828890);
INSERT INTO `operation_logs` VALUES (4, 1, 'lq', '::1', NULL, '/api/admin/settings', 'GET', '', 200, 44, 0, 1768828891);
INSERT INTO `operation_logs` VALUES (5, 1, 'lq', '::1', NULL, '/api/admin/operation-logs', 'GET', '', 200, 98, 0, 1768828901);
INSERT INTO `operation_logs` VALUES (6, 1, 'lq', '::1', NULL, '/api/admin/attachment-categories', 'GET', '', 200, 48, 0, 1768829023);
INSERT INTO `operation_logs` VALUES (7, 1, 'lq', '::1', NULL, '/api/admin/attachments', 'GET', '', 200, 144, 0, 1768829023);
INSERT INTO `operation_logs` VALUES (8, 1, 'lq', '::1', NULL, '/api/admin/works', 'GET', '', 200, 85, 0, 1768829039);
INSERT INTO `operation_logs` VALUES (9, 1, 'lq', '::1', NULL, '/api/admin/attachment-categories', 'GET', '', 200, 42, 0, 1768829041);
INSERT INTO `operation_logs` VALUES (10, 1, 'lq', '::1', NULL, '/api/admin/attachment-categories', 'GET', '', 200, 46, 0, 1768829041);
INSERT INTO `operation_logs` VALUES (11, 1, 'lq', '::1', NULL, '/api/admin/attachments', 'GET', '', 200, 133, 0, 1768829045);
INSERT INTO `operation_logs` VALUES (12, 1, 'lq', '::1', NULL, '/api/admin/attachment-categories', 'GET', '', 200, 44, 0, 1768829482);
INSERT INTO `operation_logs` VALUES (13, 1, 'lq', '::1', NULL, '/api/admin/attachment-categories', 'GET', '', 200, 44, 0, 1768829482);
INSERT INTO `operation_logs` VALUES (14, 1, 'lq', '::1', NULL, '/api/admin/posts', 'GET', '', 200, 265, 0, 1768829814);
INSERT INTO `operation_logs` VALUES (15, 1, 'lq', '::1', NULL, '/api/admin/columns', 'GET', '', 200, 49, 0, 1768869064);
INSERT INTO `operation_logs` VALUES (16, 1, 'lq', '::1', NULL, '/api/admin/attachment-categories', 'GET', '', 200, 44, 0, 1768869065);
INSERT INTO `operation_logs` VALUES (17, 1, 'lq', '::1', NULL, '/api/admin/attachments', 'GET', '', 200, 140, 0, 1768869068);
INSERT INTO `operation_logs` VALUES (18, 1, 'lq', '::1', NULL, '/api/admin/columns', 'GET', '', 200, 49, 0, 1768869074);
INSERT INTO `operation_logs` VALUES (19, 1, 'lq', '::1', NULL, '/api/admin/about', 'GET', '', 200, 47, 0, 1768869173);
INSERT INTO `operation_logs` VALUES (20, 1, 'lq', '::1', NULL, '/api/admin/attachment-categories', 'GET', '', 200, 51, 0, 1768869180);
INSERT INTO `operation_logs` VALUES (21, 1, 'lq', '::1', NULL, '/api/admin/attachments', 'GET', '', 200, 139, 0, 1768869189);
INSERT INTO `operation_logs` VALUES (22, 1, 'lq', '::1', NULL, '/api/admin/partners/2', 'PUT', '{\"name\":\"Supabase\",\"logo\":\"/uploads/2026/01/19/cc_upload_HiPDFg6oL0Q5ijfo693ce1f5eb57c_1768825204.jpg\",\"description\":\"开源 Firebase 替代方案\"}', 200, 100, 0, 1768869197);
INSERT INTO `operation_logs` VALUES (23, 1, 'lq', '::1', NULL, '/api/admin/snippets', 'GET', '', 200, 95, 0, 1768869231);
INSERT INTO `operation_logs` VALUES (24, 1, 'lq', '::1', NULL, '/api/admin/dashboard/stats', 'GET', '', 200, 379, 0, 1768869446);
INSERT INTO `operation_logs` VALUES (25, 1, 'lq', '::1', NULL, '/api/admin/dashboard/stats', 'GET', '', 200, 376, 0, 1768869486);
INSERT INTO `operation_logs` VALUES (26, 1, 'lq', '::1', NULL, '/api/admin/dashboard/stats', 'GET', '', 200, 327, 0, 1768869688);
INSERT INTO `operation_logs` VALUES (27, 1, 'lq', '::1', NULL, '/api/admin/dashboard/stats', 'GET', '', 200, 372, 0, 1768869729);
INSERT INTO `operation_logs` VALUES (28, 1, 'lq', '::1', NULL, '/api/admin/dashboard/stats', 'GET', '', 200, 325, 0, 1768869747);
INSERT INTO `operation_logs` VALUES (29, 1, 'lq', '::1', NULL, '/api/admin/dashboard/stats', 'GET', '', 200, 375, 0, 1768869756);
INSERT INTO `operation_logs` VALUES (30, 1, 'lq', '::1', NULL, '/api/admin/dashboard/stats', 'GET', '', 200, 375, 0, 1768869758);
INSERT INTO `operation_logs` VALUES (31, 1, 'lq', '::1', NULL, '/api/admin/dashboard/stats', 'GET', '', 200, 375, 0, 1768869760);
INSERT INTO `operation_logs` VALUES (32, 1, 'lq', '::1', NULL, '/api/admin/dashboard/stats', 'GET', '', 200, 358, 0, 1768869781);
INSERT INTO `operation_logs` VALUES (33, 1, 'lq', '::1', NULL, '/api/admin/dashboard/stats', 'GET', '', 200, 376, 0, 1768869786);
INSERT INTO `operation_logs` VALUES (34, 1, 'lq', '::1', NULL, '/api/admin/dashboard/stats', 'GET', '', 200, 386, 0, 1768869793);
INSERT INTO `operation_logs` VALUES (35, 1, 'lq', '::1', NULL, '/api/admin/dashboard/stats', 'GET', '', 200, 385, 0, 1768869795);
INSERT INTO `operation_logs` VALUES (36, 1, 'lq', '::1', NULL, '/api/admin/dashboard/stats', 'GET', '', 200, 388, 0, 1768869800);
INSERT INTO `operation_logs` VALUES (37, 1, 'lq', '::1', NULL, '/api/admin/dashboard/stats', 'GET', '', 200, 341, 0, 1768870363);
INSERT INTO `operation_logs` VALUES (38, 1, 'lq', '::1', NULL, '/api/admin/dashboard/stats', 'GET', '', 200, 340, 0, 1768870495);
INSERT INTO `operation_logs` VALUES (39, 1, 'lq', '::1', NULL, '/api/admin/dashboard/stats', 'GET', '', 200, 354, 0, 1768870522);
INSERT INTO `operation_logs` VALUES (40, 1, 'lq', '::1', NULL, '/api/admin/dashboard/stats', 'GET', '', 200, 335, 0, 1768870535);
INSERT INTO `operation_logs` VALUES (41, 1, 'lq', '::1', NULL, '/api/admin/dashboard/stats', 'GET', '', 200, 334, 0, 1768870539);
INSERT INTO `operation_logs` VALUES (42, 1, 'lq', '::1', NULL, '/api/admin/dashboard/stats', 'GET', '', 200, 338, 0, 1768870541);
INSERT INTO `operation_logs` VALUES (43, 1, 'lq', '::1', NULL, '/api/admin/dashboard/stats', 'GET', '', 200, 325, 0, 1768870546);
INSERT INTO `operation_logs` VALUES (44, 1, 'lq', '::1', NULL, '/api/admin/dashboard/stats', 'GET', '', 200, 322, 0, 1768870579);
INSERT INTO `operation_logs` VALUES (45, 1, 'lq', '::1', NULL, '/api/admin/dashboard/stats', 'GET', '', 200, 321, 0, 1768870581);
INSERT INTO `operation_logs` VALUES (46, 1, 'lq', '::1', NULL, '/api/admin/dashboard/stats', 'GET', '', 200, 321, 0, 1768870587);
INSERT INTO `operation_logs` VALUES (47, 1, 'lq', '::1', NULL, '/api/admin/operation-logs', 'GET', '', 200, 95, 0, 1768870656);
INSERT INTO `operation_logs` VALUES (48, 1, 'lq', '::1', NULL, '/api/admin/dashboard/stats', 'GET', '', 200, 280, 0, 1768870656);
INSERT INTO `operation_logs` VALUES (49, 1, 'lq', '::1', NULL, '/api/admin/dashboard/stats', 'GET', '', 200, 376, 0, 1768870657);
INSERT INTO `operation_logs` VALUES (50, 1, 'lq', '::1', NULL, '/api/admin/posts', 'GET', '', 200, 275, 0, 1768870658);
INSERT INTO `operation_logs` VALUES (51, 1, 'lq', '::1', NULL, '/api/admin/categories', 'GET', '', 200, 47, 0, 1768870659);
INSERT INTO `operation_logs` VALUES (52, 1, 'lq', '::1', NULL, '/api/admin/columns', 'GET', '', 200, 48, 0, 1768870660);
INSERT INTO `operation_logs` VALUES (53, 1, 'lq', '::1', NULL, '/api/admin/works', 'GET', '', 200, 92, 0, 1768870660);
INSERT INTO `operation_logs` VALUES (54, 1, 'lq', '::1', NULL, '/api/admin/snippets', 'GET', '', 200, 92, 0, 1768870661);
INSERT INTO `operation_logs` VALUES (55, 1, 'lq', '::1', NULL, '/api/admin/tags', 'GET', '', 200, 44, 0, 1768870661);
INSERT INTO `operation_logs` VALUES (56, 1, 'lq', '::1', NULL, '/api/admin/attachment-categories', 'GET', '', 200, 47, 0, 1768870663);
INSERT INTO `operation_logs` VALUES (57, 1, 'lq', '::1', NULL, '/api/admin/about', 'GET', '', 200, 47, 0, 1768870663);
INSERT INTO `operation_logs` VALUES (58, 1, 'lq', '::1', NULL, '/api/admin/attachment-categories', 'GET', '', 200, 47, 0, 1768870664);
INSERT INTO `operation_logs` VALUES (59, 1, 'lq', '::1', NULL, '/api/admin/about', 'GET', '', 200, 47, 0, 1768870664);
INSERT INTO `operation_logs` VALUES (60, 1, 'lq', '::1', NULL, '/api/admin/inquiries', 'GET', '', 200, 47, 0, 1768870668);
INSERT INTO `operation_logs` VALUES (61, 1, 'lq', '::1', NULL, '/api/admin/email-suffixes', 'GET', '', 200, 49, 0, 1768870669);
INSERT INTO `operation_logs` VALUES (62, 1, 'lq', '::1', NULL, '/api/admin/users', 'GET', '', 200, 93, 0, 1768870671);
INSERT INTO `operation_logs` VALUES (63, 1, 'lq', '::1', NULL, '/api/admin/roles', 'GET', '', 200, 138, 0, 1768870672);
INSERT INTO `operation_logs` VALUES (64, 1, 'lq', '::1', NULL, '/api/admin/attachment-categories', 'GET', '', 200, 49, 0, 1768870674);
INSERT INTO `operation_logs` VALUES (65, 1, 'lq', '::1', NULL, '/api/admin/attachments', 'GET', '', 200, 104, 0, 1768870674);
INSERT INTO `operation_logs` VALUES (66, 1, 'lq', '::1', NULL, '/api/admin/attachment-categories', 'GET', '', 200, 47, 0, 1768870675);
INSERT INTO `operation_logs` VALUES (67, 1, 'lq', '::1', NULL, '/api/admin/oss-configs', 'GET', '', 200, 97, 0, 1768870676);
INSERT INTO `operation_logs` VALUES (68, 1, 'lq', '::1', NULL, '/api/admin/settings', 'GET', '', 200, 49, 0, 1768870678);
INSERT INTO `operation_logs` VALUES (69, 1, 'lq', '::1', NULL, '/api/admin/operation-logs', 'GET', '', 200, 95, 0, 1768870678);
INSERT INTO `operation_logs` VALUES (70, 1, 'lq', '::1', NULL, '/api/admin/settings', 'GET', '', 200, 46, 0, 1768870679);
INSERT INTO `operation_logs` VALUES (71, 1, 'lq', '::1', NULL, '/api/admin/settings', 'PUT', '{\"id\":4,\"keyName\":\"site_keywords\",\"value\":\"前端, 设计, 技术博客\",\"description\":\"网站关键词\"}', 400, 5, 0, 1768870685);
INSERT INTO `operation_logs` VALUES (72, 1, 'lq', '::1', NULL, '/api/admin/settings', 'PUT', '{\"id\":5,\"keyName\":\"posts_per_page\",\"value\":\"12\",\"description\":\"每页显示的文章数量\"}', 400, 2, 0, 1768870685);
INSERT INTO `operation_logs` VALUES (73, 1, 'lq', '::1', NULL, '/api/admin/settings', 'PUT', '{\"id\":3,\"keyName\":\"site_author\",\"value\":\"年糕崽崽\",\"description\":\"网站作者\"}', 400, 4, 0, 1768870685);
INSERT INTO `operation_logs` VALUES (74, 1, 'lq', '::1', NULL, '/api/admin/settings', 'PUT', '{\"id\":2,\"keyName\":\"site_description\",\"value\":\"分享前端技术、交互设计以及数字艺术的深度思考\",\"description\":\"网站描述\"}', 400, 3, 0, 1768870685);
INSERT INTO `operation_logs` VALUES (75, 1, 'lq', '::1', NULL, '/api/admin/settings', 'PUT', '{\"id\":1,\"keyName\":\"site_title\",\"value\":\"年糕博客\",\"description\":\"网站标题\"}', 400, 6, 0, 1768870685);
INSERT INTO `operation_logs` VALUES (76, 1, 'lq', '::1', NULL, '/api/admin/settings', 'PUT', '{\"id\":7,\"keyName\":\"snippets_per_page\",\"value\":\"8\",\"description\":\"每页显示的代码片段数量\"}', 400, 3, 0, 1768870685);
INSERT INTO `operation_logs` VALUES (77, 1, 'lq', '::1', NULL, '/api/admin/settings', 'PUT', '{\"id\":6,\"keyName\":\"works_per_page\",\"value\":\"6\",\"description\":\"每页显示的作品数量\"}', 400, 4, 0, 1768870685);
INSERT INTO `operation_logs` VALUES (78, 1, 'lq', '::1', NULL, '/api/admin/settings', 'PUT', '{\"id\":5,\"keyName\":\"posts_per_page\",\"value\":\"12\",\"description\":\"每页显示的文章数量\"}', 200, 95, 0, 1768870798);
INSERT INTO `operation_logs` VALUES (79, 1, 'lq', '::1', NULL, '/api/admin/settings', 'PUT', '{\"id\":3,\"keyName\":\"site_author\",\"value\":\"年糕崽崽\",\"description\":\"网站作者\"}', 200, 97, 0, 1768870798);
INSERT INTO `operation_logs` VALUES (80, 1, 'lq', '::1', NULL, '/api/admin/settings', 'PUT', '{\"id\":2,\"keyName\":\"site_description\",\"value\":\"分享前端技术、交互设计以及数字艺术的深度思考\",\"description\":\"网站描述\"}', 200, 96, 0, 1768870799);
INSERT INTO `operation_logs` VALUES (81, 1, 'lq', '::1', NULL, '/api/admin/settings', 'PUT', '{\"id\":4,\"keyName\":\"site_keywords\",\"value\":\"前端, 设计, 技术博客\",\"description\":\"网站关键词\"}', 200, 96, 0, 1768870799);
INSERT INTO `operation_logs` VALUES (82, 1, 'lq', '::1', NULL, '/api/admin/settings', 'PUT', '{\"id\":1,\"keyName\":\"site_title\",\"value\":\"年糕博客\",\"description\":\"网站标题\"}', 200, 94, 0, 1768870799);
INSERT INTO `operation_logs` VALUES (83, 1, 'lq', '::1', NULL, '/api/admin/settings', 'PUT', '{\"id\":7,\"keyName\":\"snippets_per_page\",\"value\":\"8\",\"description\":\"每页显示的代码片段数量\"}', 200, 96, 0, 1768870799);
INSERT INTO `operation_logs` VALUES (84, 1, 'lq', '::1', NULL, '/api/admin/settings', 'PUT', '{\"id\":6,\"keyName\":\"works_per_page\",\"value\":\"6\",\"description\":\"每页显示的作品数量\"}', 200, 96, 0, 1768870799);
INSERT INTO `operation_logs` VALUES (85, 1, 'lq', '::1', NULL, '/api/admin/settings', 'GET', '', 200, 46, 0, 1768870799);
INSERT INTO `operation_logs` VALUES (86, 1, 'lq', '::1', NULL, '/api/admin/settings', 'GET', '', 200, 47, 0, 1768870807);
INSERT INTO `operation_logs` VALUES (87, 1, 'lq', '::1', NULL, '/api/admin/settings', 'GET', '', 200, 49, 0, 1768870895);
INSERT INTO `operation_logs` VALUES (88, 1, 'lq', '::1', NULL, '/api/admin/settings', 'GET', '', 200, 48, 0, 1768870907);
INSERT INTO `operation_logs` VALUES (89, 1, 'lq', '::1', NULL, '/api/admin/settings', 'GET', '', 200, 45, 0, 1768870951);
INSERT INTO `operation_logs` VALUES (90, 1, 'lq', '::1', NULL, '/api/admin/settings', 'GET', '', 200, 53, 0, 1768871123);
INSERT INTO `operation_logs` VALUES (91, 1, 'lq', '::1', NULL, '/api/admin/settings', 'GET', '', 200, 50, 0, 1768871142);
INSERT INTO `operation_logs` VALUES (92, 1, 'lq', '::1', NULL, '/api/admin/settings', 'GET', '', 200, 47, 0, 1768871185);
INSERT INTO `operation_logs` VALUES (93, 1, 'lq', '::1', NULL, '/api/admin/settings', 'GET', '', 200, 50, 0, 1768871213);
INSERT INTO `operation_logs` VALUES (94, 1, 'lq', '::1', NULL, '/api/admin/settings', 'GET', '', 200, 49, 0, 1768871273);
INSERT INTO `operation_logs` VALUES (95, 1, 'lq', '::1', NULL, '/api/admin/settings', 'GET', '', 200, 50, 0, 1768871314);
INSERT INTO `operation_logs` VALUES (96, 1, 'lq', '::1', NULL, '/api/admin/settings', 'GET', '', 200, 49, 0, 1768871359);
INSERT INTO `operation_logs` VALUES (97, 1, 'lq', '::1', NULL, '/api/admin/settings', 'GET', '', 200, 47, 0, 1768871360);
INSERT INTO `operation_logs` VALUES (98, 1, 'lq', '::1', NULL, '/api/admin/settings', 'GET', '', 200, 48, 0, 1768871381);
INSERT INTO `operation_logs` VALUES (99, 1, 'lq', '::1', NULL, '/api/admin/settings', 'GET', '', 200, 57, 0, 1768871394);
INSERT INTO `operation_logs` VALUES (100, 1, 'lq', '::1', NULL, '/api/admin/settings', 'GET', '', 200, 49, 0, 1768871427);
INSERT INTO `operation_logs` VALUES (101, 1, 'lq', '::1', NULL, '/api/admin/settings', 'GET', '', 200, 44, 0, 1768871860);
INSERT INTO `operation_logs` VALUES (102, 1, 'lq', '::1', NULL, '/api/admin/operation-logs', 'GET', '', 200, 97, 0, 1768872916);
INSERT INTO `operation_logs` VALUES (103, 1, 'lq', '::1', NULL, '/api/admin/dashboard/stats', 'GET', '', 200, 334, 0, 1768872916);
INSERT INTO `operation_logs` VALUES (104, 1, 'lq', '::1', NULL, '/api/admin/operation-logs', 'GET', '', 200, 98, 0, 1768872983);
INSERT INTO `operation_logs` VALUES (105, 1, 'lq', '::1', NULL, '/api/admin/dashboard/stats', 'GET', '', 200, 338, 0, 1768872983);
INSERT INTO `operation_logs` VALUES (106, 1, 'lq', '::1', NULL, '/api/admin/posts', 'GET', '', 200, 277, 0, 1768873803);
INSERT INTO `operation_logs` VALUES (107, 1, 'lq', '::1', NULL, '/api/admin/posts', 'GET', '', 200, 290, 0, 1768877345);
INSERT INTO `operation_logs` VALUES (108, 1, 'lq', '::1', NULL, '/api/admin/settings', 'GET', '', 200, 51, 0, 1768877347);
INSERT INTO `operation_logs` VALUES (109, 1, 'lq', '::1', NULL, '/api/admin/settings', 'PUT', '{\"id\":5,\"keyName\":\"posts_per_page\",\"value\":\"12\",\"description\":\"每页显示的文章数量\"}', 200, 98, 0, 1768877365);
INSERT INTO `operation_logs` VALUES (110, 1, 'lq', '::1', NULL, '/api/admin/settings', 'PUT', '{\"id\":3,\"keyName\":\"site_author\",\"value\":\"年糕崽崽1\",\"description\":\"网站作者\"}', 200, 93, 0, 1768877365);
INSERT INTO `operation_logs` VALUES (111, 1, 'lq', '::1', NULL, '/api/admin/settings', 'PUT', '{\"id\":2,\"keyName\":\"site_description\",\"value\":\"分享前端技术、交互设计以及数字艺术的深度思考\",\"description\":\"网站描述\"}', 200, 96, 0, 1768877365);
INSERT INTO `operation_logs` VALUES (112, 1, 'lq', '::1', NULL, '/api/admin/settings', 'PUT', '{\"id\":4,\"keyName\":\"site_keywords\",\"value\":\"前端, 设计, 技术博客\",\"description\":\"网站关键词\"}', 200, 98, 0, 1768877365);
INSERT INTO `operation_logs` VALUES (113, 1, 'lq', '::1', NULL, '/api/admin/settings', 'PUT', '{\"id\":1,\"keyName\":\"site_title\",\"value\":\"年糕博客\",\"description\":\"网站标题\"}', 200, 96, 0, 1768877365);
INSERT INTO `operation_logs` VALUES (114, 1, 'lq', '::1', NULL, '/api/admin/settings', 'PUT', '{\"id\":7,\"keyName\":\"snippets_per_page\",\"value\":\"8\",\"description\":\"每页显示的代码片段数量\"}', 200, 97, 0, 1768877365);
INSERT INTO `operation_logs` VALUES (115, 1, 'lq', '::1', NULL, '/api/admin/settings', 'PUT', '{\"id\":6,\"keyName\":\"works_per_page\",\"value\":\"6\",\"description\":\"每页显示的作品数量\"}', 200, 96, 0, 1768877365);
INSERT INTO `operation_logs` VALUES (116, 1, 'lq', '::1', NULL, '/api/admin/settings', 'GET', '', 200, 47, 0, 1768877365);
INSERT INTO `operation_logs` VALUES (117, 1, 'lq', '::1', NULL, '/api/admin/operation-logs', 'GET', '', 200, 101, 0, 1768877421);
INSERT INTO `operation_logs` VALUES (118, 1, 'lq', '::1', NULL, '/api/admin/dashboard/stats', 'GET', '', 200, 332, 0, 1768877421);
INSERT INTO `operation_logs` VALUES (119, 1, 'lq', '::1', NULL, '/api/admin/dashboard/stats', 'GET', '', 200, 373, 0, 1768877441);
INSERT INTO `operation_logs` VALUES (120, 1, 'lq', '::1', NULL, '/api/admin/dashboard/stats', 'GET', '', 200, 373, 0, 1768877446);
INSERT INTO `operation_logs` VALUES (121, 1, 'lq', '::1', 'Internal', '/api/admin/dashboard/stats', 'GET', '', 200, 332, 0, 1768877578);
INSERT INTO `operation_logs` VALUES (122, 1, 'lq', '::1', 'Internal', '/api/admin/dashboard/stats', 'GET', '', 200, 328, 0, 1768877614);
INSERT INTO `operation_logs` VALUES (123, 1, 'lq', '::1', 'Internal', '/api/admin/dashboard/stats', 'GET', '', 200, 328, 0, 1768877620);
INSERT INTO `operation_logs` VALUES (124, 1, 'lq', '::1', 'Internal', '/api/admin/dashboard/stats', 'GET', '', 200, 329, 0, 1768877627);
INSERT INTO `operation_logs` VALUES (125, 1, 'lq', '::1', 'Internal', '/api/admin/dashboard/stats', 'GET', '', 200, 330, 0, 1768877629);
INSERT INTO `operation_logs` VALUES (126, 1, 'lq', '::1', 'Internal', '/api/admin/access-logs', 'GET', '', 200, 95, 0, 1768877636);
INSERT INTO `operation_logs` VALUES (127, 1, 'lq', '::1', 'Internal', '/api/admin/operation-logs', 'GET', '', 200, 97, 0, 1768877637);
INSERT INTO `operation_logs` VALUES (128, 1, 'lq', '::1', 'Internal', '/api/admin/access-logs', 'GET', '', 200, 94, 0, 1768877641);
INSERT INTO `operation_logs` VALUES (129, 1, 'lq', '::1', 'Internal', '/api/admin/operation-logs', 'GET', '', 200, 94, 0, 1768877642);
INSERT INTO `operation_logs` VALUES (130, 1, 'lq', '::1', 'Internal', '/api/admin/settings', 'GET', '', 200, 48, 0, 1768877642);
INSERT INTO `operation_logs` VALUES (131, 1, 'lq', '::1', 'Internal', '/api/admin/dashboard/stats', 'GET', '', 200, 330, 0, 1768877655);
INSERT INTO `operation_logs` VALUES (132, 1, 'lq', '::1', 'Internal', '/api/admin/dashboard/stats', 'GET', '', 200, 328, 0, 1768877658);
INSERT INTO `operation_logs` VALUES (133, 1, 'lq', '::1', 'Internal', '/api/admin/dashboard/stats', 'GET', '', 200, 329, 0, 1768877716);
INSERT INTO `operation_logs` VALUES (134, 1, 'lq', '::1', 'Internal', '/api/admin/settings', 'GET', '', 200, 48, 0, 1768877809);
INSERT INTO `operation_logs` VALUES (135, 1, 'lq', '::1', 'Internal', '/api/admin/settings', 'GET', '', 200, 52, 0, 1768878153);
INSERT INTO `operation_logs` VALUES (136, 1, 'lq', '::1', 'Internal', '/api/admin/settings', 'GET', '', 200, 49, 0, 1768878166);
INSERT INTO `operation_logs` VALUES (137, 1, 'lq', '::1', 'Internal', '/api/admin/settings', 'GET', '', 200, 55, 0, 1768878174);
INSERT INTO `operation_logs` VALUES (138, 1, 'lq', '::1', 'Internal', '/api/admin/settings', 'GET', '', 200, 57, 0, 1768878177);
INSERT INTO `operation_logs` VALUES (139, 1, 'lq', '::1', 'Internal', '/api/admin/settings', 'GET', '', 200, 52, 0, 1768878188);
INSERT INTO `operation_logs` VALUES (140, 1, 'lq', '::1', 'Internal', '/api/admin/settings', 'GET', '', 200, 51, 0, 1768878472);
INSERT INTO `operation_logs` VALUES (141, 1, 'lq', '::1', 'Internal', '/api/admin/settings', 'GET', '', 200, 47, 0, 1768878512);
INSERT INTO `operation_logs` VALUES (142, 1, 'lq', '::1', 'Internal', '/api/admin/settings', 'GET', '', 200, 52, 0, 1768878616);
INSERT INTO `operation_logs` VALUES (143, 1, 'lq', '::1', 'Internal', '/api/admin/settings', 'PUT', '{\"id\":5,\"keyName\":\"posts_per_page\",\"value\":\"12\",\"description\":\"每页显示的文章数量\"}', 200, 96, 0, 1768878627);
INSERT INTO `operation_logs` VALUES (144, 1, 'lq', '::1', 'Internal', '/api/admin/settings', 'PUT', '{\"id\":3,\"keyName\":\"site_author\",\"value\":\"年糕崽崽1\",\"description\":\"网站作者\"}', 200, 98, 0, 1768878627);
INSERT INTO `operation_logs` VALUES (145, 1, 'lq', '::1', 'Internal', '/api/admin/settings', 'PUT', '{\"id\":2,\"keyName\":\"site_description\",\"value\":\"分享前端技术、交互设计以及数字艺术的深度思考\",\"description\":\"网站描述\"}', 200, 96, 0, 1768878627);
INSERT INTO `operation_logs` VALUES (146, 1, 'lq', '::1', 'Internal', '/api/admin/settings', 'PUT', '{\"id\":4,\"keyName\":\"site_keywords\",\"value\":\"前端, 设计, 技术博客\",\"description\":\"网站关键词\"}', 200, 98, 0, 1768878628);
INSERT INTO `operation_logs` VALUES (147, 1, 'lq', '::1', 'Internal', '/api/admin/settings', 'PUT', '{\"id\":1,\"keyName\":\"site_title\",\"value\":\"年糕崽崽\",\"description\":\"网站标题\"}', 200, 97, 0, 1768878628);
INSERT INTO `operation_logs` VALUES (148, 1, 'lq', '::1', 'Internal', '/api/admin/settings', 'PUT', '{\"id\":7,\"keyName\":\"snippets_per_page\",\"value\":\"8\",\"description\":\"每页显示的代码片段数量\"}', 200, 98, 0, 1768878628);
INSERT INTO `operation_logs` VALUES (149, 1, 'lq', '::1', 'Internal', '/api/admin/settings', 'PUT', '{\"id\":6,\"keyName\":\"works_per_page\",\"value\":\"6\",\"description\":\"每页显示的作品数量\"}', 200, 97, 0, 1768878628);
INSERT INTO `operation_logs` VALUES (150, 1, 'lq', '::1', 'Internal', '/api/admin/settings', 'GET', '', 200, 50, 0, 1768878628);
INSERT INTO `operation_logs` VALUES (151, 1, 'lq', '::1', 'Internal', '/api/admin/settings', 'GET', '', 200, 50, 0, 1768878630);
INSERT INTO `operation_logs` VALUES (152, 1, 'lq', '::1', 'Internal', '/api/admin/settings', 'GET', '', 200, 52, 0, 1768878632);
INSERT INTO `operation_logs` VALUES (153, 1, 'lq', '::1', 'Internal', '/api/admin/settings', 'GET', '', 200, 51, 0, 1768878651);
INSERT INTO `operation_logs` VALUES (154, 1, 'lq', '::1', 'Internal', '/api/admin/settings', 'GET', '', 200, 51, 0, 1768878667);
INSERT INTO `operation_logs` VALUES (155, 1, 'lq', '::1', 'Internal', '/api/admin/settings', 'PUT', '{\"id\":5,\"keyName\":\"posts_per_page\",\"value\":\"12\",\"description\":\"每页显示的文章数量\"}', 200, 98, 0, 1768878783);
INSERT INTO `operation_logs` VALUES (156, 1, 'lq', '::1', 'Internal', '/api/admin/settings', 'PUT', '{\"id\":3,\"keyName\":\"site_author\",\"value\":\"年糕崽崽1\",\"description\":\"网站作者\"}', 200, 96, 0, 1768878783);
INSERT INTO `operation_logs` VALUES (157, 1, 'lq', '::1', 'Internal', '/api/admin/settings', 'PUT', '{\"id\":2,\"keyName\":\"site_description\",\"value\":\"分享前端技术、交互设计以及数字艺术的深度思考\",\"description\":\"网站描述\"}', 200, 97, 0, 1768878783);
INSERT INTO `operation_logs` VALUES (158, 1, 'lq', '::1', 'Internal', '/api/admin/settings', 'PUT', '{\"id\":4,\"keyName\":\"site_keywords\",\"value\":\"前端, 设计, 技术博客\",\"description\":\"网站关键词\"}', 200, 98, 0, 1768878783);
INSERT INTO `operation_logs` VALUES (159, 1, 'lq', '::1', 'Internal', '/api/admin/settings', 'PUT', '{\"id\":1,\"keyName\":\"site_title\",\"value\":\"年糕崽崽\",\"description\":\"网站标题\"}', 200, 96, 0, 1768878783);
INSERT INTO `operation_logs` VALUES (160, 1, 'lq', '::1', 'Internal', '/api/admin/settings', 'PUT', '{\"id\":7,\"keyName\":\"snippets_per_page\",\"value\":\"8\",\"description\":\"每页显示的代码片段数量\"}', 200, 98, 0, 1768878783);
INSERT INTO `operation_logs` VALUES (161, 1, 'lq', '::1', 'Internal', '/api/admin/settings', 'PUT', '{\"id\":6,\"keyName\":\"works_per_page\",\"value\":\"6\",\"description\":\"每页显示的作品数量\"}', 200, 104, 0, 1768878783);
INSERT INTO `operation_logs` VALUES (162, 1, 'lq', '::1', 'Internal', '/api/admin/settings', 'GET', '', 200, 48, 0, 1768878783);
INSERT INTO `operation_logs` VALUES (163, 1, 'lq', '::1', 'Internal', '/api/admin/settings', 'GET', '', 200, 45, 0, 1768878798);
INSERT INTO `operation_logs` VALUES (164, 1, 'lq', '::1', 'Internal', '/api/admin/settings', 'GET', '', 200, 47, 0, 1768878805);
INSERT INTO `operation_logs` VALUES (165, 1, 'lq', '::1', 'Internal', '/api/admin/settings', 'GET', '', 200, 46, 0, 1768878844);
INSERT INTO `operation_logs` VALUES (166, 1, 'lq', '::1', 'Internal', '/api/admin/settings', 'GET', '', 200, 50, 0, 1768878871);
INSERT INTO `operation_logs` VALUES (167, 1, 'lq', '::1', 'Internal', '/api/admin/settings', 'GET', '', 200, 50, 0, 1768878985);
INSERT INTO `operation_logs` VALUES (168, 1, 'lq', '::1', 'Internal', '/api/admin/settings', 'PUT', '{\"id\":15,\"keyName\":\"visible_menus\",\"value\":\"[\\\"home\\\",\\\"blog\\\",\\\"columns\\\",\\\"works\\\",\\\"about\\\",\\\"services\\\"]\",\"description\":\"前台显示的菜单项JSON数组格式\"}', 200, 95, 0, 1768878990);
INSERT INTO `operation_logs` VALUES (169, 1, 'lq', '::1', 'Internal', '/api/admin/settings', 'GET', '', 200, 44, 0, 1768878990);
INSERT INTO `operation_logs` VALUES (170, 1, 'lq', '::1', 'Internal', '/api/admin/settings', 'GET', '', 200, 47, 0, 1768879150);
INSERT INTO `operation_logs` VALUES (171, 1, 'lq', '::1', 'Internal', '/api/admin/settings', 'GET', '', 200, 46, 0, 1768879154);
INSERT INTO `operation_logs` VALUES (172, 1, 'lq', '::1', 'Internal', '/api/admin/settings', 'GET', '', 200, 48, 0, 1768879158);
INSERT INTO `operation_logs` VALUES (173, 1, 'lq', '::1', 'Internal', '/api/admin/settings', 'GET', '', 200, 16, 0, 1768880799);
INSERT INTO `operation_logs` VALUES (174, 1, 'lq', '::1', 'Internal', '/api/admin/settings', 'GET', '', 200, 53, 0, 1768880809);
INSERT INTO `operation_logs` VALUES (175, 1, 'lq', '::1', 'Internal', '/api/admin/settings', 'PUT', '{\"id\":15,\"keyName\":\"visible_menus\",\"value\":\"[\\\"home\\\",\\\"blog\\\",\\\"columns\\\",\\\"works\\\",\\\"about\\\"]\",\"description\":\"前台显示的菜单项JSON数组格式\"}', 200, 99, 0, 1768881703);
INSERT INTO `operation_logs` VALUES (176, 1, 'lq', '::1', 'Internal', '/api/admin/settings', 'GET', '', 200, 50, 0, 1768881703);
INSERT INTO `operation_logs` VALUES (177, 1, 'lq', '::1', 'Internal', '/api/admin/dashboard/stats', 'GET', '', 200, 312, 0, 1768881885);
INSERT INTO `operation_logs` VALUES (178, 1, 'lq', '::1', 'Internal', '/api/admin/dashboard/stats', 'GET', '', 200, 289, 0, 1768881900);
INSERT INTO `operation_logs` VALUES (179, 1, 'lq', '::1', 'Internal', '/api/admin/dashboard/stats', 'GET', '', 200, 329, 0, 1768882129);
INSERT INTO `operation_logs` VALUES (180, 1, 'lq', '::1', 'Internal', '/api/admin/dashboard/stats', 'GET', '', 200, 342, 0, 1768882288);
INSERT INTO `operation_logs` VALUES (181, 1, 'lq', '::1', 'Internal', '/api/admin/dashboard/stats', 'GET', '', 200, 330, 0, 1768882702);
INSERT INTO `operation_logs` VALUES (182, 1, 'lq', '::1', 'Internal', '/api/admin/dashboard/stats', 'GET', '', 200, 406, 0, 1768882750);
INSERT INTO `operation_logs` VALUES (183, 1, 'lq', '::1', 'Internal', '/api/admin/dashboard/stats', 'GET', '', 200, 387, 0, 1768882851);
INSERT INTO `operation_logs` VALUES (184, 1, 'lq', '::1', 'Internal', '/api/admin/dashboard/stats', 'GET', '', 200, 390, 0, 1768882853);
INSERT INTO `operation_logs` VALUES (185, 1, 'lq', '::1', 'Internal', '/api/admin/dashboard/stats', 'GET', '', 200, 392, 0, 1768882899);
INSERT INTO `operation_logs` VALUES (186, 1, 'lq', '::1', 'Internal', '/api/admin/dashboard/stats', 'GET', '', 200, 390, 0, 1768882943);
INSERT INTO `operation_logs` VALUES (187, 1, 'lq', '::1', 'Internal', '/api/admin/dashboard/stats', 'GET', '', 200, 344, 0, 1768883044);
INSERT INTO `operation_logs` VALUES (188, 1, 'lq', '::1', 'Internal', '/api/admin/dashboard/stats', 'GET', '', 200, 393, 0, 1768883048);
INSERT INTO `operation_logs` VALUES (189, 1, 'lq', '::1', 'Internal', '/api/admin/dashboard/stats', 'GET', '', 200, 337, 0, 1768883061);
INSERT INTO `operation_logs` VALUES (190, 1, 'lq', '::1', 'Internal', '/api/admin/dashboard/stats', 'GET', '', 200, 403, 0, 1768883292);
INSERT INTO `operation_logs` VALUES (191, 1, 'lq', '::1', 'Internal', '/api/admin/dashboard/stats', 'GET', '', 200, 385, 0, 1768883350);
INSERT INTO `operation_logs` VALUES (192, 1, 'lq', '::1', 'Internal', '/api/admin/dashboard/stats', 'GET', '', 200, 390, 0, 1768883362);
INSERT INTO `operation_logs` VALUES (193, 1, 'lq', '::1', 'Internal', '/api/admin/dashboard/stats', 'GET', '', 200, 380, 0, 1768883369);
INSERT INTO `operation_logs` VALUES (194, 1, 'lq', '::1', 'Internal', '/api/admin/dashboard/stats', 'GET', '', 200, 386, 0, 1768883370);
INSERT INTO `operation_logs` VALUES (195, 1, 'lq', '::1', 'Internal', '/api/admin/dashboard/stats', 'GET', '', 200, 382, 0, 1768883398);
INSERT INTO `operation_logs` VALUES (196, 1, 'lq', '::1', 'Internal', '/api/admin/dashboard/stats', 'GET', '', 200, 380, 0, 1768883424);
INSERT INTO `operation_logs` VALUES (197, 1, 'lq', '::1', 'Internal', '/api/admin/dashboard/stats', 'GET', '', 200, 379, 0, 1768883445);
INSERT INTO `operation_logs` VALUES (198, 1, 'lq', '::1', 'Internal', '/api/admin/dashboard/stats', 'GET', '', 200, 383, 0, 1768883471);
INSERT INTO `operation_logs` VALUES (199, 1, 'lq', '::1', 'Internal', '/api/admin/attachment-categories', 'GET', '', 200, 49, 0, 1768885354);
INSERT INTO `operation_logs` VALUES (200, 1, 'lq', '::1', 'Internal', '/api/admin/attachments', 'GET', '', 200, 101, 0, 1768885354);
INSERT INTO `operation_logs` VALUES (201, 1, 'lq', '::1', 'Internal', '/api/admin/attachment-categories', 'GET', '', 200, 47, 0, 1768885377);
INSERT INTO `operation_logs` VALUES (202, 1, 'lq', '::1', 'Internal', '/api/admin/attachments', 'GET', '', 200, 143, 0, 1768885377);
INSERT INTO `operation_logs` VALUES (203, 1, 'lq', '::1', 'Internal', '/api/admin/attachment-categories', 'GET', '', 200, 51, 0, 1768885548);
INSERT INTO `operation_logs` VALUES (204, 1, 'lq', '::1', 'Internal', '/api/admin/attachments', 'GET', '', 200, 101, 0, 1768885548);
INSERT INTO `operation_logs` VALUES (205, 1, 'lq', '::1', 'Internal', '/api/admin/attachment-categories', 'GET', '', 200, 52, 0, 1768886215);
INSERT INTO `operation_logs` VALUES (206, 1, 'lq', '::1', 'Internal', '/api/admin/attachments', 'GET', '', 200, 98, 0, 1768886215);
INSERT INTO `operation_logs` VALUES (207, 1, 'lq', '::1', 'Internal', '/api/admin/attachment-categories', 'GET', '', 200, 55, 0, 1768886263);
INSERT INTO `operation_logs` VALUES (208, 1, 'lq', '::1', 'Internal', '/api/admin/attachments', 'GET', '', 200, 99, 0, 1768886263);
INSERT INTO `operation_logs` VALUES (209, 1, 'lq', '::1', 'Internal', '/api/admin/attachment-categories', 'GET', '', 200, 54, 0, 1768886265);
INSERT INTO `operation_logs` VALUES (210, 1, 'lq', '::1', 'Internal', '/api/admin/attachments', 'GET', '', 200, 102, 0, 1768886265);
INSERT INTO `operation_logs` VALUES (211, 1, 'lq', '::1', 'Internal', '/api/admin/attachment-categories', 'GET', '', 200, 52, 0, 1768886268);
INSERT INTO `operation_logs` VALUES (212, 1, 'lq', '::1', 'Internal', '/api/admin/attachments', 'GET', '', 200, 99, 0, 1768886268);
INSERT INTO `operation_logs` VALUES (213, 1, 'lq', '::1', 'Internal', '/api/admin/attachment-categories', 'GET', '', 200, 48, 0, 1768886279);
INSERT INTO `operation_logs` VALUES (214, 1, 'lq', '::1', 'Internal', '/api/admin/attachments', 'GET', '', 200, 98, 0, 1768886279);
INSERT INTO `operation_logs` VALUES (215, 1, 'lq', '::1', 'Internal', '/api/admin/attachment-categories', 'GET', '', 200, 45, 0, 1768886289);
INSERT INTO `operation_logs` VALUES (216, 1, 'lq', '::1', 'Internal', '/api/admin/attachments', 'GET', '', 200, 133, 0, 1768886289);
INSERT INTO `operation_logs` VALUES (217, 1, 'lq', '::1', 'Internal', '/api/admin/attachment-categories', 'GET', '', 200, 47, 0, 1768886433);
INSERT INTO `operation_logs` VALUES (218, 1, 'lq', '::1', 'Internal', '/api/admin/attachments', 'GET', '', 200, 144, 0, 1768886433);
INSERT INTO `operation_logs` VALUES (219, 1, 'lq', '::1', 'Internal', '/api/admin/attachment-categories', 'GET', '', 200, 55, 0, 1768886433);
INSERT INTO `operation_logs` VALUES (220, 1, 'lq', '::1', 'Internal', '/api/admin/attachments', 'GET', '', 200, 152, 0, 1768886433);
INSERT INTO `operation_logs` VALUES (221, 1, 'lq', '::1', 'Internal', '/api/admin/attachment-categories', 'GET', '', 200, 47, 0, 1768886790);
INSERT INTO `operation_logs` VALUES (222, 1, 'lq', '::1', 'Internal', '/api/admin/attachments', 'GET', '', 200, 147, 0, 1768886790);
INSERT INTO `operation_logs` VALUES (223, 1, 'lq', '::1', 'Internal', '/api/admin/attachment-categories', 'GET', '', 200, 49, 0, 1768887180);
INSERT INTO `operation_logs` VALUES (224, 1, 'lq', '::1', 'Internal', '/api/admin/attachments', 'GET', '', 200, 97, 0, 1768887180);
-- ----------------------------
-- Table structure for oss_configs
@@ -325,7 +550,7 @@ CREATE TABLE `partners` (
-- Records of partners
-- ----------------------------
INSERT INTO `partners` VALUES (1, 'Vercel', 'https://upload.wikimedia.org/wikipedia/commons/5/5e/Vercel_logo_black.svg', '前端部署平台', 'https://vercel.com', 0, 0, 1768482993, 1768538953);
INSERT INTO `partners` VALUES (2, 'Supabase', 'https://seeklogo.com/images/S/supabase-logo-DCC676FFE2-seeklogo.com.png', '开源 Firebase 替代方案', 'https://supabase.com', 0, 0, 1768482993, 1768538953);
INSERT INTO `partners` VALUES (2, 'Supabase', '/uploads/2026/01/19/cc_upload_HiPDFg6oL0Q5ijfo693ce1f5eb57c_1768825204.jpg', '开源 Firebase 替代方案', '', 0, 0, 1768482993, 1768869197);
INSERT INTO `partners` VALUES (3, 'Stripe', 'https://upload.wikimedia.org/wikipedia/commons/b/ba/Stripe_Logo%2C_revised_2016.svg', '在线支付基础设施', 'https://stripe.com', 0, 0, 1768482993, 1768538953);
INSERT INTO `partners` VALUES (4, 'Algolia', 'https://upload.wikimedia.org/wikipedia/commons/6/69/Algolia-logo.svg', '搜索即服务 API', 'https://algolia.com', 0, 0, 1768482993, 1768538953);
INSERT INTO `partners` VALUES (5, 'Prisma', 'https://seeklogo.com/images/P/prisma-logo-3805665B69-seeklogo.com.png', '下一代 ORM', 'https://prisma.io', 0, 0, 1768482993, 1768538953);
@@ -457,12 +682,12 @@ CREATE TABLE `posts` (
-- ----------------------------
-- Records of posts
-- ----------------------------
INSERT INTO `posts` VALUES (1, 'refactor', '重构的艺术:如何优雅地处理遗留代码', 4, NULL, '在本文中,我们将探讨重构的核心原则和实用技巧,帮助你优雅地处理遗留代码,提高代码质量和可维护性。', '<h2>什么是代码重构?</h2><p>代码重构是在不改变代码外部行为的前提下,优化代码内部结构的过程...</p>\n\n\n![cc_upload_opYFPBlXkfyOSrH269152a21424c4.jpg](/uploads/2026/01/19/cc_upload_opYFPBlXkfyOSrH269152a21424c4_1768811098.jpg)\n', 9, 1, 0, 1768291814, 1768811113);
INSERT INTO `posts` VALUES (1, 'refactor', '重构的艺术:如何优雅地处理遗留代码', 4, NULL, '在本文中,我们将探讨重构的核心原则和实用技巧,帮助你优雅地处理遗留代码,提高代码质量和可维护性。', '<h2>什么是代码重构?</h2><p>代码重构是在不改变代码外部行为的前提下,优化代码内部结构的过程...</p>\n\n\n![cc_upload_opYFPBlXkfyOSrH269152a21424c4.jpg](/uploads/2026/01/19/cc_upload_opYFPBlXkfyOSrH269152a21424c4_1768811098.jpg)\n', 10, 1, 0, 1768291814, 1768811113);
INSERT INTO `posts` VALUES (2, 'shader', '着色器魔法:从零开始写一个噪声生成器', 2, NULL, '深入了解WebGL着色器学习如何从零开始实现一个高性能的噪声生成器为你的3D作品增添独特的视觉效果。', ' <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, NULL, '探索用户体验设计的核心原理,结合认知心理学知识,学习如何设计出真正符合用户需求的交互界面。', '<h2>认知心理学在UX设计中的应用</h2><p>了解用户的认知过程是设计良好用户体验的基础...</p>', 0, 1, 0, 1768291816, 1768538949);
INSERT INTO `posts` VALUES (4, 'goravel-guide-1', 'Goravel 入门指南 (一):环境搭建与 Hello World', 4, 1, '本文将带你了解 Go 语言领域的 Laravel —— Goravel 框架,并演示如何快速搭建环境及运行第一个 Web 服务。', '## 什么是 Goravel\n\nGoravel 是一个基于 Go 语言的 Web 开发框架,它的设计理念深受 PHP Laravel 框架的启发。如果你是一名从 PHP 转 Go 的开发者,或者你喜欢 Laravel 那种“开箱即用、优雅简洁”的开发体验,那么 Goravel 绝对是你的不二之选。\n\n它集成了丰富的功能模块包括但不限于\n- 强大的路由系统\n- ORM基于 GORM 封装)\n- 依赖注入容器\n- 队列与任务调度\n- 缓存与文件存储\n\n## 环境搭建\n\nGoravel 提供了一个名为 `knit` 的命令行工具(类似 Laravel 的 artisan可以帮助我们快速初始化项目。\n\n### 1. 安装 Knit CLI\n\n确保你已经安装了 Go (1.20+),然后运行以下命令:\n\n```bash\ngo install github.com/goravel/knit/cmd/knit@latest\n```\n\n### 2. 创建新项目\n\n使用 `knit new` 命令创建项目:\n\n```bash\nknit new my-goravel-app\ncd my-goravel-app\n```\n\n### 3. 安装依赖\n\n```bash\ngo mod tidy\n```\n\n## 目录结构\n\n打开项目你会发现它的目录结构非常清晰带有浓厚的 Laravel 风格:\n\n- **app/**: 核心业务代码Http 控制器、模型、服务提供者等)\n- **config/**: 配置文件(应用配置、数据库配置等)\n- **routes/**: 路由定义文件\n- **database/**: 数据库迁移与填充\n- **public/**: 静态资源文件\n\n## 运行 Hello World\n\nGoravel 的入口文件是根目录下的 `main.go`。在运行之前,我们先看一眼路由定义。打开 `routes/web.go`\n\n```go\npackage routes\n\nimport (\n \"github.com/goravel/framework/contracts/http\"\n \"github.com/goravel/framework/facades\"\n)\n\nfunc Web() {\n facades.Route().Get(\"/\", func(ctx http.Context) http.Response {\n return ctx.Response().Json(200, http.Json{\n \"Hello\": \"Goravel\",\n })\n })\n}\n```\n\n非常直观现在让我们启动服务\n\n```bash\ngo run .\n```\n\n默认情况下服务会运行在 `http://localhost:3000`。打开浏览器访问,你应该能看到 JSON 响应:\n\n```json\n{\n \"Hello\": \"Goravel\"\n}\n```\n\n至此你已经成功运行了你的第一个 Goravel 应用!在下一篇文章中,我们将深入探讨路由与控制器的使用。', 1210, 1, 0, 1768465932, 1768538949);
INSERT INTO `posts` VALUES (5, 'goravel-guide-2', 'Goravel 入门指南 (二):路由与控制器', 4, 1, '深入理解 Goravel 的 HTTP 层,学习如何定义 RESTful 路由、创建控制器以及处理 HTTP 请求与响应。', '## 路由系统\n\n在 Goravel 中,路由定义通常位于 `routes/` 目录下。`api.go` 用于定义 API 路由,`web.go` 用于定义网页路由。Goravel 使用 `facades.Route()` 来定义路由,这得益于其强大的依赖注入系统。\n\n### 基础路由\n\n```go\n// GET 请求\nfacades.Route().Get(\"/users\", func(ctx http.Context) http.Response {\n return ctx.Response().String(200, \"User List\")\n})\n\n// POST 请求\nfacades.Route().Post(\"/users\", func(ctx http.Context) http.Response {\n return ctx.Response().Success().Json(http.Json{\"id\": 1})\n})\n```\n\n### 路由参数\n\n获取 URL 中的动态参数非常简单:\n\n```go\nfacades.Route().Get(\"/users/{id}\", func(ctx http.Context) http.Response {\n id := ctx.Request().Input(\"id\")\n return ctx.Response().Success().Json(http.Json{\"user_id\": id})\n})\n```\n\n## 控制器 (Controllers)\n\n随着应用变大我们不可能把所有逻辑都写在路由闭包里。这时候就需要控制器了。\n\n### 创建控制器\n\n使用 `knit` 工具可以快速生成控制器:\n\n```bash\nknit make:controller UserController\n```\n\n这会在 `app/http/controllers` 目录下生成 `user_controller.go`。让我们修改它来添加一个 `Show` 方法:\n\n```go\npackage controllers\n\nimport (\n \"github.com/goravel/framework/contracts/http\"\n)\n\ntype UserController struct {\n // 可以在这里注入服务\n}\n\nfunc NewUserController() *UserController {\n return &UserController{}\n}\n\nfunc (r *UserController) Show(ctx http.Context) http.Response {\n id := ctx.Request().Input(\"id\")\n // 模拟数据库查询\n return ctx.Response().Success().Json(http.Json{\n \"id\": id,\n \"name\": \"Goravel User\",\n })\n}\n```\n\n### 注册控制器路由\n\n回到 `routes/api.go`,我们需要先实例化控制器,然后绑定路由:\n\n```go\nimport \"my-goravel-app/app/http/controllers\"\n\nfunc Api() {\n userController := controllers.NewUserController()\n \n // 绑定到控制器方法\n facades.Route().Get(\"/users/{id}\", userController.Show)\n}\n```\n\n## 请求与响应\n\n在控制器方法中`ctx` (http.Context) 是核心:\n\n- **获取输入**: `ctx.Request().Input(\"key\")`\n- **获取 JSON**: `ctx.Request().Bind(&user)`\n- **返回 JSON**: `ctx.Response().Json(200, data)`\n- **设置状态码**: `ctx.Response().Status(404)`\n\n通过这种方式Goravel 让 HTTP 层的处理变得异常清晰和标准化。下一章,我们将学习如何通过 ORM 操作数据库。', 904, 1, 0, 1768465933, 1768538949);
INSERT INTO `posts` VALUES (6, 'goravel-guide-3', 'Goravel 入门指南 (三)ORM 数据库操作', 4, 1, '掌握 Goravel 强大的 ORM 功能从数据库配置、模型定义到执行增删改查CRUD操作。', '## Goravel ORM 简介\r\nGoravel 的 ORM 基于著名的 GORM 库构建,但它进行了一层优雅的封装,使其调用方式更接近 Laravel 的 Eloquent ORM。这意味着你可以使用 Facade 模式轻松调用数据库,且支持自动迁移。\r\n## 配置数据库\r\n打开 `config/database.go` 或 `.env` 文件,配置你的 MySQL 连接信息:\r\n```env\r\nDB_CONNECTION=mysql\r\nDB_HOST=127.0.0.1\r\nDB_PORT=3306\r\nDB_DATABASE=goravel\r\nDB_USERNAME=root\r\nDB_PASSWORD=password\r\n```\r\n## 定义模型\r\n使用 `knit` 生成模型:\r\n```bash\r\nknit make:model Post\r\n```\r\n生成的模型文件位于 `app/models/post.go`。我们需要定义结构体字段:\r\n```go\r\npackage models\r\nimport (\r\n \\\"github.com/goravel/framework/database/orm\\\"\r\n )\r\ntype Post struct {\r\n orm.Model\r\n Title string `gorm:\\\"size:255;not null\\\"`\r\n Content string `gorm:\\\"type:text\\\"`\r\n UserID uint\r\n }\r\n ```\r\n## 数据库迁移\r\n虽然 GORM 支持 AutoMigrate但 Goravel 推荐使用迁移文件来管理数据库变更。\r\n```bash\r\nknit make:migration create_posts_table\r\n```\r\n编辑生成的迁移文件 (`database/migrations/xxxx_create_posts_table.go`),定义表结构,然后运行:\r\n```bash\r\nknit migrate\r\n```\r\n## CRUD 操作\r\n有了模型我们就可以愉快地操作数据库了。所有操作都通过 `facades.Orm()` 入口。\r\n### 创建 (Create)\r\n```go\r\npost := models.Post{\r\n Title: \\\"My First Post\\\",\r\n Content: \\\"Content goes here...\\\",\r\n }\r\n err := facades.Orm().Query().Create(&post)\r\n ```\r\n### 查询 (Read)\r\n```go\r\nvar post models.Post\r\n// 根据主键查询\r\nfacades.Orm().Query().Find(&post, 1)\r\n// 条件查询\r\nvar posts []models.Post\r\nfacades.Orm().Query().Where(\\\"title\\\", \\\"My First Post\\\").Get(&posts)\r\n```\r\n### 更新 (Update)\r\n```go\r\nvar post models.Post\r\nfacades.Orm().Query().Find(&post, 1)\r\npost.Title = \\\"Updated Title\\\"\r\nfacades.Orm().Query().Save(&post)\r\n```\r\n### 删除 (Delete)\r\n```go\r\nvar post models.Post\r\nfacades.Orm().Query().Delete(&post, 1)\r\n```\r\nGoravel 的 ORM 让 Go 语言的数据库操作不再繁琐,极大地提高了开发效率。配合之前的路由和控制器知识,你现在已经具备了开发完整 RESTful API 的能力!', 1593, 1, 0, 1768465934, 1768809351);
INSERT INTO `posts` VALUES (4, 'goravel-guide-1', 'Goravel 入门指南 (一):环境搭建与 Hello World', 4, 1, '本文将带你了解 Go 语言领域的 Laravel —— Goravel 框架,并演示如何快速搭建环境及运行第一个 Web 服务。', '## 什么是 Goravel\n\nGoravel 是一个基于 Go 语言的 Web 开发框架,它的设计理念深受 PHP Laravel 框架的启发。如果你是一名从 PHP 转 Go 的开发者,或者你喜欢 Laravel 那种“开箱即用、优雅简洁”的开发体验,那么 Goravel 绝对是你的不二之选。\n\n它集成了丰富的功能模块包括但不限于\n- 强大的路由系统\n- ORM基于 GORM 封装)\n- 依赖注入容器\n- 队列与任务调度\n- 缓存与文件存储\n\n## 环境搭建\n\nGoravel 提供了一个名为 `knit` 的命令行工具(类似 Laravel 的 artisan可以帮助我们快速初始化项目。\n\n### 1. 安装 Knit CLI\n\n确保你已经安装了 Go (1.20+),然后运行以下命令:\n\n```bash\ngo install github.com/goravel/knit/cmd/knit@latest\n```\n\n### 2. 创建新项目\n\n使用 `knit new` 命令创建项目:\n\n```bash\nknit new my-goravel-app\ncd my-goravel-app\n```\n\n### 3. 安装依赖\n\n```bash\ngo mod tidy\n```\n\n## 目录结构\n\n打开项目你会发现它的目录结构非常清晰带有浓厚的 Laravel 风格:\n\n- **app/**: 核心业务代码Http 控制器、模型、服务提供者等)\n- **config/**: 配置文件(应用配置、数据库配置等)\n- **routes/**: 路由定义文件\n- **database/**: 数据库迁移与填充\n- **public/**: 静态资源文件\n\n## 运行 Hello World\n\nGoravel 的入口文件是根目录下的 `main.go`。在运行之前,我们先看一眼路由定义。打开 `routes/web.go`\n\n```go\npackage routes\n\nimport (\n \"github.com/goravel/framework/contracts/http\"\n \"github.com/goravel/framework/facades\"\n)\n\nfunc Web() {\n facades.Route().Get(\"/\", func(ctx http.Context) http.Response {\n return ctx.Response().Json(200, http.Json{\n \"Hello\": \"Goravel\",\n })\n })\n}\n```\n\n非常直观现在让我们启动服务\n\n```bash\ngo run .\n```\n\n默认情况下服务会运行在 `http://localhost:3000`。打开浏览器访问,你应该能看到 JSON 响应:\n\n```json\n{\n \"Hello\": \"Goravel\"\n}\n```\n\n至此你已经成功运行了你的第一个 Goravel 应用!在下一篇文章中,我们将深入探讨路由与控制器的使用。', 1223, 1, 0, 1768465932, 1768538949);
INSERT INTO `posts` VALUES (5, 'goravel-guide-2', 'Goravel 入门指南 (二):路由与控制器', 4, 1, '深入理解 Goravel 的 HTTP 层,学习如何定义 RESTful 路由、创建控制器以及处理 HTTP 请求与响应。', '## 路由系统\n\n在 Goravel 中,路由定义通常位于 `routes/` 目录下。`api.go` 用于定义 API 路由,`web.go` 用于定义网页路由。Goravel 使用 `facades.Route()` 来定义路由,这得益于其强大的依赖注入系统。\n\n### 基础路由\n\n```go\n// GET 请求\nfacades.Route().Get(\"/users\", func(ctx http.Context) http.Response {\n return ctx.Response().String(200, \"User List\")\n})\n\n// POST 请求\nfacades.Route().Post(\"/users\", func(ctx http.Context) http.Response {\n return ctx.Response().Success().Json(http.Json{\"id\": 1})\n})\n```\n\n### 路由参数\n\n获取 URL 中的动态参数非常简单:\n\n```go\nfacades.Route().Get(\"/users/{id}\", func(ctx http.Context) http.Response {\n id := ctx.Request().Input(\"id\")\n return ctx.Response().Success().Json(http.Json{\"user_id\": id})\n})\n```\n\n## 控制器 (Controllers)\n\n随着应用变大我们不可能把所有逻辑都写在路由闭包里。这时候就需要控制器了。\n\n### 创建控制器\n\n使用 `knit` 工具可以快速生成控制器:\n\n```bash\nknit make:controller UserController\n```\n\n这会在 `app/http/controllers` 目录下生成 `user_controller.go`。让我们修改它来添加一个 `Show` 方法:\n\n```go\npackage controllers\n\nimport (\n \"github.com/goravel/framework/contracts/http\"\n)\n\ntype UserController struct {\n // 可以在这里注入服务\n}\n\nfunc NewUserController() *UserController {\n return &UserController{}\n}\n\nfunc (r *UserController) Show(ctx http.Context) http.Response {\n id := ctx.Request().Input(\"id\")\n // 模拟数据库查询\n return ctx.Response().Success().Json(http.Json{\n \"id\": id,\n \"name\": \"Goravel User\",\n })\n}\n```\n\n### 注册控制器路由\n\n回到 `routes/api.go`,我们需要先实例化控制器,然后绑定路由:\n\n```go\nimport \"my-goravel-app/app/http/controllers\"\n\nfunc Api() {\n userController := controllers.NewUserController()\n \n // 绑定到控制器方法\n facades.Route().Get(\"/users/{id}\", userController.Show)\n}\n```\n\n## 请求与响应\n\n在控制器方法中`ctx` (http.Context) 是核心:\n\n- **获取输入**: `ctx.Request().Input(\"key\")`\n- **获取 JSON**: `ctx.Request().Bind(&user)`\n- **返回 JSON**: `ctx.Response().Json(200, data)`\n- **设置状态码**: `ctx.Response().Status(404)`\n\n通过这种方式Goravel 让 HTTP 层的处理变得异常清晰和标准化。下一章,我们将学习如何通过 ORM 操作数据库。', 925, 1, 0, 1768465933, 1768538949);
INSERT INTO `posts` VALUES (6, 'goravel-guide-3', 'Goravel 入门指南 (三)ORM 数据库操作', 4, 1, '掌握 Goravel 强大的 ORM 功能从数据库配置、模型定义到执行增删改查CRUD操作。', '## Goravel ORM 简介\r\nGoravel 的 ORM 基于著名的 GORM 库构建,但它进行了一层优雅的封装,使其调用方式更接近 Laravel 的 Eloquent ORM。这意味着你可以使用 Facade 模式轻松调用数据库,且支持自动迁移。\r\n## 配置数据库\r\n打开 `config/database.go` 或 `.env` 文件,配置你的 MySQL 连接信息:\r\n```env\r\nDB_CONNECTION=mysql\r\nDB_HOST=127.0.0.1\r\nDB_PORT=3306\r\nDB_DATABASE=goravel\r\nDB_USERNAME=root\r\nDB_PASSWORD=password\r\n```\r\n## 定义模型\r\n使用 `knit` 生成模型:\r\n```bash\r\nknit make:model Post\r\n```\r\n生成的模型文件位于 `app/models/post.go`。我们需要定义结构体字段:\r\n```go\r\npackage models\r\nimport (\r\n \\\"github.com/goravel/framework/database/orm\\\"\r\n )\r\ntype Post struct {\r\n orm.Model\r\n Title string `gorm:\\\"size:255;not null\\\"`\r\n Content string `gorm:\\\"type:text\\\"`\r\n UserID uint\r\n }\r\n ```\r\n## 数据库迁移\r\n虽然 GORM 支持 AutoMigrate但 Goravel 推荐使用迁移文件来管理数据库变更。\r\n```bash\r\nknit make:migration create_posts_table\r\n```\r\n编辑生成的迁移文件 (`database/migrations/xxxx_create_posts_table.go`),定义表结构,然后运行:\r\n```bash\r\nknit migrate\r\n```\r\n## CRUD 操作\r\n有了模型我们就可以愉快地操作数据库了。所有操作都通过 `facades.Orm()` 入口。\r\n### 创建 (Create)\r\n```go\r\npost := models.Post{\r\n Title: \\\"My First Post\\\",\r\n Content: \\\"Content goes here...\\\",\r\n }\r\n err := facades.Orm().Query().Create(&post)\r\n ```\r\n### 查询 (Read)\r\n```go\r\nvar post models.Post\r\n// 根据主键查询\r\nfacades.Orm().Query().Find(&post, 1)\r\n// 条件查询\r\nvar posts []models.Post\r\nfacades.Orm().Query().Where(\\\"title\\\", \\\"My First Post\\\").Get(&posts)\r\n```\r\n### 更新 (Update)\r\n```go\r\nvar post models.Post\r\nfacades.Orm().Query().Find(&post, 1)\r\npost.Title = \\\"Updated Title\\\"\r\nfacades.Orm().Query().Save(&post)\r\n```\r\n### 删除 (Delete)\r\n```go\r\nvar post models.Post\r\nfacades.Orm().Query().Delete(&post, 1)\r\n```\r\nGoravel 的 ORM 让 Go 语言的数据库操作不再繁琐,极大地提高了开发效率。配合之前的路由和控制器知识,你现在已经具备了开发完整 RESTful API 的能力!', 1602, 1, 0, 1768465934, 1768809351);
-- ----------------------------
-- Table structure for role_permissions
@@ -580,18 +805,19 @@ CREATE TABLE `settings` (
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `key_name`(`key_name` ASC) USING BTREE,
INDEX `idx_key_name`(`key_name` ASC) USING BTREE COMMENT '按键名查询索引'
) ENGINE = InnoDB AUTO_INCREMENT = 15 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '网站配置表' ROW_FORMAT = DYNAMIC;
) ENGINE = InnoDB AUTO_INCREMENT = 16 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '网站配置表' ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- Records of settings
-- ----------------------------
INSERT INTO `settings` VALUES (1, 'site_title', '年糕博客', '网站标题', 0, 1768291814, 1768538952);
INSERT INTO `settings` VALUES (2, 'site_description', '分享前端技术、交互设计以及数字艺术的深度思考', '网站描述', 0, 1768291814, 1768538952);
INSERT INTO `settings` VALUES (3, 'site_author', '年糕崽崽', '网站作者', 0, 1768291814, 1768538952);
INSERT INTO `settings` VALUES (4, 'site_keywords', '前端, 设计, 技术博客', '网站关键词', 0, 1768291814, 1768538952);
INSERT INTO `settings` VALUES (5, 'posts_per_page', '10', '每页显示的文章数量', 0, 1768291814, 1768538952);
INSERT INTO `settings` VALUES (6, 'works_per_page', '6', '每页显示的作品数量', 0, 1768291814, 1768538952);
INSERT INTO `settings` VALUES (7, 'snippets_per_page', '8', '每页显示的代码片段数量', 0, 1768291814, 1768538952);
INSERT INTO `settings` VALUES (1, 'site_title', '年糕崽崽', '网站标题', 0, 1768291814, 1768878783);
INSERT INTO `settings` VALUES (2, 'site_description', '分享前端技术、交互设计以及数字艺术的深度思考', '网站描述', 0, 1768291814, 1768878783);
INSERT INTO `settings` VALUES (3, 'site_author', '年糕崽崽1', '网站作者', 0, 1768291814, 1768878783);
INSERT INTO `settings` VALUES (4, 'site_keywords', '前端, 设计, 技术博客', '网站关键词', 0, 1768291814, 1768878783);
INSERT INTO `settings` VALUES (5, 'posts_per_page', '12', '每页显示的文章数量', 0, 1768291814, 1768878783);
INSERT INTO `settings` VALUES (6, 'works_per_page', '6', '每页显示的作品数量', 0, 1768291814, 1768878783);
INSERT INTO `settings` VALUES (7, 'snippets_per_page', '8', '每页显示的代码片段数量', 0, 1768291814, 1768878783);
INSERT INTO `settings` VALUES (15, 'visible_menus', '[\"home\",\"blog\",\"columns\",\"works\",\"about\"]', '前台显示的菜单项JSON数组格式', 0, 1768878978, 1768881703);
-- ----------------------------
-- Table structure for snippets
@@ -685,11 +911,56 @@ CREATE TABLE `user_access_logs` (
PRIMARY KEY (`id`) USING BTREE,
INDEX `idx_user_id`(`user_id` ASC) USING BTREE,
INDEX `idx_article_id`(`article_id` ASC) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 19 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '用户访问记录表' ROW_FORMAT = DYNAMIC;
) ENGINE = InnoDB AUTO_INCREMENT = 46 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '用户访问记录表' ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- Records of user_access_logs
-- ----------------------------
INSERT INTO `user_access_logs` VALUES (1, 0, '::1', 'Internal', 1, 0, 1768829817);
INSERT INTO `user_access_logs` VALUES (2, 0, '::1', 'Internal', 4, 0, 1768829860);
INSERT INTO `user_access_logs` VALUES (3, 0, '::1', 'Internal', 4, 0, 1768869688);
INSERT INTO `user_access_logs` VALUES (4, 0, '::1', 'Internal', 4, 0, 1768869746);
INSERT INTO `user_access_logs` VALUES (5, 0, '::1', 'Internal', 4, 0, 1768870362);
INSERT INTO `user_access_logs` VALUES (6, 0, '::1', 'Internal', 4, 0, 1768870495);
INSERT INTO `user_access_logs` VALUES (7, 0, '::1', 'Internal', 4, 0, 1768870534);
INSERT INTO `user_access_logs` VALUES (8, 0, '::1', 'Internal', 4, 0, 1768870538);
INSERT INTO `user_access_logs` VALUES (9, 0, '::1', 'Internal', 4, 0, 1768870540);
INSERT INTO `user_access_logs` VALUES (10, 0, '::1', 'Internal', 4, 0, 1768870545);
INSERT INTO `user_access_logs` VALUES (11, 0, '::1', 'Internal', 4, 0, 1768870579);
INSERT INTO `user_access_logs` VALUES (12, 0, '::1', 'Internal', 4, 0, 1768870580);
INSERT INTO `user_access_logs` VALUES (13, 0, '::1', 'Internal', 4, 0, 1768870586);
INSERT INTO `user_access_logs` VALUES (14, 0, '::1', 'Internal', 4, 0, 1768870637);
INSERT INTO `user_access_logs` VALUES (15, 0, '::1', 'Internal', 5, 0, 1768870640);
INSERT INTO `user_access_logs` VALUES (16, 1, '::1', 'Internal', 0, 0, 1768872916);
INSERT INTO `user_access_logs` VALUES (17, 0, '::1', 'Internal', 6, 0, 1768878230);
INSERT INTO `user_access_logs` VALUES (18, 0, '::1', 'Internal', 6, 0, 1768878635);
INSERT INTO `user_access_logs` VALUES (19, 0, '::1', 'Internal', 6, 0, 1768878639);
INSERT INTO `user_access_logs` VALUES (20, 0, '::1', 'Internal', 6, 0, 1768878649);
INSERT INTO `user_access_logs` VALUES (21, 0, '::1', 'Internal', 6, 0, 1768878651);
INSERT INTO `user_access_logs` VALUES (22, 0, '::1', 'Internal', 6, 0, 1768878667);
INSERT INTO `user_access_logs` VALUES (23, 0, '::1', 'Internal', 6, 0, 1768878844);
INSERT INTO `user_access_logs` VALUES (24, 0, '::1', 'Internal', 6, 0, 1768878871);
INSERT INTO `user_access_logs` VALUES (25, 0, '::1', 'Internal', 6, 0, 1768878875);
INSERT INTO `user_access_logs` VALUES (26, 0, '::1', 'Internal', 5, 0, 1768879449);
INSERT INTO `user_access_logs` VALUES (27, 0, '::1', 'Internal', 5, 0, 1768879452);
INSERT INTO `user_access_logs` VALUES (28, 0, '::1', 'Internal', 5, 0, 1768879513);
INSERT INTO `user_access_logs` VALUES (29, 0, '::1', 'Internal', 5, 0, 1768880797);
INSERT INTO `user_access_logs` VALUES (30, 0, '::1', 'Internal', 5, 0, 1768880799);
INSERT INTO `user_access_logs` VALUES (31, 0, '::1', 'Internal', 5, 0, 1768880810);
INSERT INTO `user_access_logs` VALUES (32, 0, '::1', 'Internal', 5, 0, 1768881631);
INSERT INTO `user_access_logs` VALUES (33, 0, '::1', 'Internal', 5, 0, 1768881636);
INSERT INTO `user_access_logs` VALUES (34, 0, '::1', 'Internal', 5, 0, 1768881706);
INSERT INTO `user_access_logs` VALUES (35, 0, '::1', 'Internal', 5, 0, 1768883044);
INSERT INTO `user_access_logs` VALUES (36, 0, '::1', 'Internal', 5, 0, 1768883060);
INSERT INTO `user_access_logs` VALUES (37, 0, '::1', 'Internal', 5, 0, 1768883290);
INSERT INTO `user_access_logs` VALUES (38, 0, '::1', 'Internal', 5, 0, 1768883291);
INSERT INTO `user_access_logs` VALUES (39, 0, '::1', 'Internal', 5, 0, 1768886263);
INSERT INTO `user_access_logs` VALUES (40, 0, '::1', 'Internal', 5, 0, 1768886279);
INSERT INTO `user_access_logs` VALUES (41, 0, '::1', 'Internal', 5, 0, 1768886433);
INSERT INTO `user_access_logs` VALUES (42, 0, '::1', 'Internal', 5, 0, 1768886788);
INSERT INTO `user_access_logs` VALUES (43, 0, '::1', 'Internal', 5, 0, 1768886790);
INSERT INTO `user_access_logs` VALUES (44, 0, '::1', 'Internal', 5, 0, 1768887178);
INSERT INTO `user_access_logs` VALUES (45, 0, '::1', 'Internal', 5, 0, 1768887180);
-- ----------------------------
-- Table structure for users
@@ -718,7 +989,7 @@ CREATE TABLE `users` (
-- ----------------------------
-- Records of users
-- ----------------------------
INSERT INTO `users` VALUES (1, 'lq', 'liqiworker@gmail.com', '$2a$10$Bq6rv7714W3jGyXruYx4puXjflMTSkq2QM9kF54x9iZnk.0AD4B1G', 1, 'admin', 1, 0, 1768291814, 1768538951);
INSERT INTO `users` VALUES (1, 'lq', 'liqiworker@gmail.com', '$2a$10$b.KMnG261WjsvdKciKn01uRpuZs2eIiiIIXhpwIu6rgQ8AL5oxA8e', 1, 'admin', 1, 0, 1768291814, 1768538951);
INSERT INTO `users` VALUES (2, 'editor', 'editor@example.com', '$2a$14$OqZ7svgz3S68yuuXTJWz1.0CuqI2i3WvwC1jIGBy4l0mcJW9Kqvy2', 2, 'editor', 1, 0, 1768291814, 1768538951);
INSERT INTO `users` VALUES (5, 'cs', 'cs@nailaoyun.cn', '$2a$14$OqZ7svgz3S68yuuXTJWz1.0CuqI2i3WvwC1jIGBy4l0mcJW9Kqvy2', 3, 'viewer', 1, 0, 1768462115, 1768538951);

View File

@@ -3,6 +3,7 @@ package repositories
import (
// "log"
"fmt"
"net/url"
"strings"
"time"
@@ -15,8 +16,17 @@ func parseDateToUnix(dateStr string, isEnd bool) int64 {
if dateStr == "" {
return 0
}
// 处理URL编码格式先尝试URL解码然后将+替换为空格
decoded := dateStr
if decodedStr, err := url.QueryUnescape(dateStr); err == nil {
decoded = decodedStr
}
// URL编码中+可能代表空格,需要替换
decoded = strings.ReplaceAll(decoded, "+", " ")
// Try parsing with time first
t, err := time.ParseInLocation("2006-01-02 15:04", dateStr, time.Local)
t, err := time.ParseInLocation("2006-01-02 15:04", decoded, time.Local)
if err == nil {
if isEnd {
// HH:mm:59
@@ -26,7 +36,7 @@ func parseDateToUnix(dateStr string, isEnd bool) int64 {
}
// Try parsing just date
t, err = time.ParseInLocation("2006-01-02", dateStr, time.Local)
t, err = time.ParseInLocation("2006-01-02", decoded, time.Local)
if err == nil {
if isEnd {
// 23:59:59
@@ -242,3 +252,24 @@ func GetUserRegions(startDate, endDate string) ([]struct {
return finalResults, nil
}
// GetDefaultUserRegions 返回所有34个省份的默认数据Count为0
func GetDefaultUserRegions() []struct {
Region string
Count int
} {
var defaultResults []struct {
Region string
Count int
}
for _, province := range allProvinces {
defaultResults = append(defaultResults, struct {
Region string
Count int
}{
Region: province,
Count: 0,
})
}
return defaultResults
}

View File

@@ -74,3 +74,73 @@ func BuildOperationLogsResponse(logs []models.OperationLog) []models.OperationLo
}
return responses
}
// OperationTrendData 操作趋势数据
type OperationTrendData struct {
Date string `json:"date"`
Count int `json:"value"`
}
// GetOperationTrend 获取操作趋势(按日期统计)
func GetOperationTrend(startDate, endDate string) ([]OperationTrendData, error) {
// 确定日期范围
var startTime, endTime time.Time
if startDate != "" {
startUnix := parseDateToUnix(startDate, false)
startTime = time.Unix(startUnix, 0)
} else {
startTime = time.Now().AddDate(0, 0, -6)
startTime = time.Date(startTime.Year(), startTime.Month(), startTime.Day(), 0, 0, 0, 0, time.Local)
}
if endDate != "" {
endUnix := parseDateToUnix(endDate, true)
endTime = time.Unix(endUnix, 0)
} else {
endTime = time.Now()
endTime = time.Date(endTime.Year(), endTime.Month(), endTime.Day(), 23, 59, 59, 0, time.Local)
}
// 查询数据库
query := config.DB.Model(&models.OperationLog{}).
Select("FROM_UNIXTIME(created_at, '%Y-%m-%d') as date, COUNT(*) as count").
Where("deleted_at = ?", 0).
Where("created_at >= ?", startTime.Unix()).
Where("created_at <= ?", endTime.Unix())
var results []OperationTrendData
err := query.Group("date").
Order("date ASC").
Scan(&results).Error
if err != nil {
return nil, err
}
// 创建日期到数据的映射
resultMap := make(map[string]OperationTrendData)
for _, r := range results {
resultMap[r.Date] = r
}
// 生成完整日期列表
var fullResults []OperationTrendData
current := time.Date(startTime.Year(), startTime.Month(), startTime.Day(), 0, 0, 0, 0, time.Local)
endDay := time.Date(endTime.Year(), endTime.Month(), endTime.Day(), 0, 0, 0, 0, time.Local)
for !current.After(endDay) {
dateStr := current.Format("2006-01-02")
if data, exists := resultMap[dateStr]; exists {
fullResults = append(fullResults, data)
} else {
// 填充0值
fullResults = append(fullResults, OperationTrendData{
Date: dateStr,
Count: 0,
})
}
current = current.AddDate(0, 0, 1)
}
return fullResults, nil
}

View File

@@ -11,9 +11,8 @@ import (
)
var (
searcher *xdb.Searcher
once sync.Once
mu sync.RWMutex // 保护 searcher 的并发访问
ipBuff []byte // 全局保存 xdb 文件内容,加载后只读,天然线程安全
once sync.Once // 确保只初始化一次
)
// InitIP2Region 初始化 ip2region
@@ -53,36 +52,31 @@ func InitIP2Region(dbPath string) {
// 如果最终路径为空,说明找不到文件
if finalPath == "" {
log.Printf("IP2Region database file not found. Region lookup will be disabled.")
mu.Lock()
searcher = nil
mu.Unlock()
return
}
// 5. 尝试加载整个xdb到内存,性能最好
// 5. 尝试加载整个xdb到内存
cBuff, err := xdb.LoadContentFromFile(finalPath)
if err != nil {
log.Printf("Failed to load ip2region.xdb from %s: %v. Region lookup will be disabled.", finalPath, err)
mu.Lock()
searcher = nil
mu.Unlock()
return
}
newSearcher, err := xdb.NewWithBuffer(nil, cBuff)
if err != nil {
log.Printf("Failed to create searcher: %v", err)
mu.Lock()
searcher = nil
mu.Unlock()
// 验证 cBuff 是否有效
if cBuff == nil || len(cBuff) == 0 {
log.Printf("Invalid ip2region.xdb buffer (nil or empty) from %s. Region lookup will be disabled.", finalPath)
return
}
// 使用写锁设置 searcher
mu.Lock()
searcher = newSearcher
mu.Unlock()
// 验证 buffer 长度是否合理
if len(cBuff) < 1024 {
log.Printf("ip2region.xdb buffer too small (%d bytes) from %s. Region lookup will be disabled.", len(cBuff), finalPath)
return
}
// 赋值给全局变量
ipBuff = cBuff
log.Printf("Loaded ip2region.xdb buffer: %d bytes from %s", len(ipBuff), finalPath)
log.Printf("IP2Region loaded successfully from %s", finalPath)
})
}
@@ -90,49 +84,58 @@ func InitIP2Region(dbPath string) {
// GetRegion 获取IP归属地
// 返回格式: 国家|区域|省份|城市|ISP
func GetRegion(ip string) string {
// 添加 recover 保护,防止 panic 导致整个请求失败
defer func() {
if r := recover(); r != nil {
log.Printf("Panic in GetRegion for IP %s: %v", ip, r)
}
}()
// 使用读锁保护并发访问
mu.RLock()
s := searcher
mu.RUnlock()
// 再次检查 searcher 是否为 nil
if s == nil {
return "Unknown"
}
// 过滤内网IP
if isPrivateIP(ip) {
return "Internal"
}
// 再次检查 searcher 是否为 nil防止在检查后、调用前被设置为 nil
mu.RLock()
s = searcher
mu.RUnlock()
if s == nil {
// 检查数据是否已加载
if len(ipBuff) == 0 {
return "Unknown"
}
// 使用 defer recover 保护 SearchByStr 调用
defer func() {
if r := recover(); r != nil {
log.Printf("Panic in searcher.SearchByStr for IP %s: %v", ip, r)
}
// 核心改进:每次请求创建一个新的 Searcher 对象
// xdb.NewWithBuffer 只是引用了 ipBuff并没有发生内存拷贝所以创建速度极快且开销很小。
// 这样做彻底避免了多个 goroutine 共用同一个 searcher 对象可能导致的内部状态并发问题。
// 根据官方文档,第一个参数应该是 version (xdb.IPv4 或 xdb.IPv6),而不是 nil
searcher, err := xdb.NewWithBuffer(xdb.IPv4, ipBuff)
if err != nil {
log.Printf("Failed to create searcher for IP %s: %v", ip, err)
return "Unknown"
}
// 检查 searcher 是否为 nil防御性编程
// 即使 err == nilsearcher 也可能为 nil需要显式检查
if searcher == nil {
log.Printf("Searcher is nil for IP %s (err was nil, ipBuff length: %d)", ip, len(ipBuff))
return "Unknown"
}
// 注意searcher 是局部变量,用完即毁,无需 Close如果是基于 buffer 创建的)
// 安全调用 SearchByStr
// 依然保留 recover 保护,防止库内部处理特殊 IP 字符串时发生 Panic
var region string
func() {
defer func() {
if r := recover(); r != nil {
log.Printf("Panic in searcher.SearchByStr for IP %s: %v", ip, r)
region = "Unknown"
}
}()
region, err = searcher.SearchByStr(ip)
}()
region, err := s.SearchByStr(ip)
if err != nil {
log.Printf("Error searching region for IP %s: %v", ip, err)
return "Unknown"
}
if region == "" {
return "Unknown"
}
return region
}
@@ -152,7 +155,20 @@ func isPrivateIP(ipStr string) bool {
return false // 暂不处理IPv6内网判断
}
return ip4[0] == 10 ||
(ip4[0] == 172 && ip4[1] >= 16 && ip4[1] <= 31) ||
(ip4[0] == 192 && ip4[1] == 168)
// 10.0.0.0/8
if ip4[0] == 10 {
return true
}
// 172.16.0.0/12
if ip4[0] == 172 && ip4[1] >= 16 && ip4[1] <= 31 {
return true
}
// 192.168.0.0/16
if ip4[0] == 192 && ip4[1] == 168 {
return true
}
return false
}