Compare commits

..

3 Commits

Author SHA1 Message Date
6ef44805f5 fix: 修复埋点查询 LIMIT/OFFSET 参数类型错误
- mysql2 execute 对 LIMIT/OFFSET 参数类型要求严格
- 改为直接嵌入 SQL 字符串(pSize 和 offset 已是安全的数字)
2026-06-08 15:56:16 +08:00
50c41d8759 fix: 埋点统计和日志查询修复
- track.vue: 修复日期格式化(处理 ISO 格式)
- track.vue: 筛选标签改为可滚动
- logs.ts: 修复 queryLogs 参数传递(移除 undefined date)
2026-06-08 15:50:30 +08:00
b21c90c560 fix: 修复 feedbacks 表 TEXT 类型默认值问题
- admin_reply TEXT 不能有 DEFAULT '',移除默认值
- 此问题导致 feedbacks 和 track_events 表都创建失败
2026-06-08 15:42:17 +08:00
5 changed files with 25 additions and 9 deletions

View File

@@ -60,7 +60,8 @@ export function queryLogs(params: {
page?: number page?: number
pageSize?: number pageSize?: number
}): Promise<{ lines: string[]; total: number; page: number; pageSize: number }> { }): Promise<{ lines: string[]; total: number; page: number; pageSize: number }> {
return request({ url: `/logs/${params.date}`, data: { ...params, date: undefined } }) const { date, ...queryParams } = params
return request({ url: `/logs/${date}`, data: queryParams })
} }
/** 获取埋点统计 */ /** 获取埋点统计 */

View File

@@ -30,7 +30,7 @@
<text class="card-title">最近7天</text> <text class="card-title">最近7天</text>
<view class="daily-list"> <view class="daily-list">
<view v-for="day in stats.daily" :key="day.date" class="daily-item"> <view v-for="day in stats.daily" :key="day.date" class="daily-item">
<text class="daily-date">{{ day.date.slice(5) }}</text> <text class="daily-date">{{ formatDate(day.date) }}</text>
<view class="daily-bar"> <view class="daily-bar">
<view class="bar-fill" :style="{ width: getBarWidth(day.count) + '%' }"></view> <view class="bar-fill" :style="{ width: getBarWidth(day.count) + '%' }"></view>
</view> </view>
@@ -57,13 +57,14 @@
<view class="query-section"> <view class="query-section">
<view class="query-header"> <view class="query-header">
<text class="section-title">事件记录</text> <text class="section-title">事件记录</text>
<view class="filter-tabs"> <scroll-view class="filter-tabs" scroll-x>
<view class="filter-tab" :class="{ active: !filterEvent }" @tap="filterEvent = ''">全部</view> <view class="filter-tab" :class="{ active: !filterEvent }" @tap="filterEvent = ''">全部</view>
<view class="filter-tab" :class="{ active: filterEvent === 'page_view' }" @tap="filterEvent = 'page_view'">页面</view> <view class="filter-tab" :class="{ active: filterEvent === 'page_view' }" @tap="filterEvent = 'page_view'">页面</view>
<view class="filter-tab" :class="{ active: filterEvent === 'action' }" @tap="filterEvent = 'action'">操作</view> <view class="filter-tab" :class="{ active: filterEvent === 'action' }" @tap="filterEvent = 'action'">操作</view>
<view class="filter-tab" :class="{ active: filterEvent === 'error' }" @tap="filterEvent = 'error'">错误</view> <view class="filter-tab" :class="{ active: filterEvent === 'error' }" @tap="filterEvent = 'error'">错误</view>
<view class="filter-tab" :class="{ active: filterEvent === 'api_error' }" @tap="filterEvent = 'api_error'">API错误</view> <view class="filter-tab" :class="{ active: filterEvent === 'api_error' }" @tap="filterEvent = 'api_error'">API错误</view>
</view> <view class="filter-tab" :class="{ active: filterEvent === 'app_launch' }" @tap="filterEvent = 'app_launch'">启动</view>
</scroll-view>
</view> </view>
<scroll-view class="event-record-list" scroll-y @scrolltolower="loadMore"> <scroll-view class="event-record-list" scroll-y @scrolltolower="loadMore">
@@ -187,6 +188,18 @@ function getEventClass(event: string): string {
return 'page' return 'page'
} }
/** 格式化日期(处理 MySQL DATE 类型的 ISO 格式) */
function formatDate(dateStr: string): string {
if (!dateStr) return ''
// 处理 2026-06-08T00:00:00.000Z 格式
const datePart = dateStr.split('T')[0]
const parts = datePart.split('-')
if (parts.length === 3) {
return `${parseInt(parts[1])}/${parseInt(parts[2])}`
}
return dateStr
}
function formatTime(dateStr: string): string { function formatTime(dateStr: string): string {
const d = new Date(dateStr) const d = new Date(dateStr)
const now = new Date() const now = new Date()
@@ -371,7 +384,8 @@ function goBack() {
.filter-tabs { .filter-tabs {
display: flex; display: flex;
gap: $space-xs; gap: $space-xs;
flex-wrap: wrap; white-space: nowrap;
width: 100%;
} }
.filter-tab { .filter-tab {

View File

@@ -214,7 +214,7 @@ async function runMigrations(conn: mysql.Connection) {
content TEXT NOT NULL COMMENT '反馈内容', content TEXT NOT NULL COMMENT '反馈内容',
contact VARCHAR(100) DEFAULT '' COMMENT '联系方式', contact VARCHAR(100) DEFAULT '' COMMENT '联系方式',
status ENUM('pending', 'processed', 'ignored') DEFAULT 'pending' COMMENT '处理状态', status ENUM('pending', 'processed', 'ignored') DEFAULT 'pending' COMMENT '处理状态',
admin_reply TEXT DEFAULT '' COMMENT '管理员回复', admin_reply TEXT COMMENT '管理员回复',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_user (user_id), INDEX idx_user (user_id),

View File

@@ -123,7 +123,7 @@ CREATE TABLE IF NOT EXISTS feedbacks (
content TEXT NOT NULL COMMENT '反馈内容', content TEXT NOT NULL COMMENT '反馈内容',
contact VARCHAR(100) DEFAULT '' COMMENT '联系方式', contact VARCHAR(100) DEFAULT '' COMMENT '联系方式',
status ENUM('pending', 'processed', 'ignored') DEFAULT 'pending' COMMENT '处理状态', status ENUM('pending', 'processed', 'ignored') DEFAULT 'pending' COMMENT '处理状态',
admin_reply TEXT DEFAULT '' COMMENT '管理员回复', admin_reply TEXT COMMENT '管理员回复',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_user (user_id), INDEX idx_user (user_id),

View File

@@ -157,14 +157,15 @@ router.get('/track/list', requireAdmin, async (req, res) => {
) )
const total = (countResult as any[])[0].total const total = (countResult as any[])[0].total
// LIMIT/OFFSET 直接嵌入 SQLmysql2 execute 对这些参数类型要求严格)
const [rows] = await pool.execute( const [rows] = await pool.execute(
`SELECT t.*, u.nickname `SELECT t.*, u.nickname
FROM track_events t FROM track_events t
LEFT JOIN users u ON t.user_id = u.id LEFT JOIN users u ON t.user_id = u.id
WHERE ${where} WHERE ${where}
ORDER BY t.created_at DESC ORDER BY t.created_at DESC
LIMIT ? OFFSET ?`, LIMIT ${pSize} OFFSET ${offset}`,
[...params, pSize, offset] params
) )
res.json({ res.json({