feat: 隐私政策 + 意见反馈系统

- 新增隐私政策页面,满足小程序审核要求
- 新增意见反馈功能(用户提交 + 管理员查看)
- 后端新增 feedbacks 表和反馈 API
- 管理后台新增反馈管理入口
- 我的页面添加意见反馈、隐私政策入口
This commit is contained in:
2026-06-08 10:16:28 +08:00
parent 8cf240f76b
commit 29614fdb70
10 changed files with 1026 additions and 0 deletions

View File

@@ -114,3 +114,19 @@ CREATE TABLE IF NOT EXISTS saved_filters (
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS feedbacks (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
type ENUM('bug', 'feature', 'ux', 'other') NOT NULL DEFAULT 'other' COMMENT '反馈类型',
content TEXT NOT NULL COMMENT '反馈内容',
contact VARCHAR(100) DEFAULT '' COMMENT '联系方式',
status ENUM('pending', 'processed', 'ignored') DEFAULT 'pending' COMMENT '处理状态',
admin_reply TEXT DEFAULT '' COMMENT '管理员回复',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_user (user_id),
INDEX idx_status (status),
INDEX idx_created (created_at),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

View File

@@ -18,6 +18,7 @@ import groupRoutes from './routes/group'
import notificationRoutes from './routes/notification'
import filterRoutes from './routes/filter'
import adminRoutes from './routes/admin'
import feedbackRoutes from './routes/feedback'
import { backupDatabase } from './utils/backup'
// Warn if token secret is using default fallback
@@ -149,6 +150,7 @@ app.use('/api/groups', apiLimiter, groupRoutes)
app.use('/api/notifications', apiLimiter, notificationRoutes)
app.use('/api/filters', apiLimiter, filterRoutes)
app.use('/api/admin', apiLimiter, adminRoutes)
app.use('/api/feedback', apiLimiter, feedbackRoutes)
app.get('/api/health', async (_req, res) => {
try {

View File

@@ -0,0 +1,110 @@
import { Router } from 'express'
import { pool } from '../db/connection'
import { authMiddleware } from '../middleware/auth'
const router = Router()
/** 用户提交反馈 */
router.post('/', authMiddleware, async (req, res) => {
try {
const userId = (req as any).userId
const { type, content, contact } = req.body
if (!content || !content.trim()) {
return res.status(400).json({ error: '请填写反馈内容' })
}
const validTypes = ['bug', 'feature', 'ux', 'other']
const feedbackType = validTypes.includes(type) ? type : 'other'
await pool.execute(
'INSERT INTO feedbacks (user_id, type, content, contact) VALUES (?, ?, ?, ?)',
[userId, feedbackType, content.trim(), contact || '']
)
res.json({ success: true })
} catch (error) {
console.error('Submit feedback error:', error)
res.status(500).json({ error: '提交失败' })
}
})
/** 管理员获取反馈列表 */
router.get('/', authMiddleware, async (req, res) => {
try {
const userId = (req as any).userId
const [users] = await pool.execute('SELECT role FROM users WHERE id = ?', [userId])
const user = (users as any[])[0]
if (!user || user.role !== 'admin') {
return res.status(403).json({ error: '无权限' })
}
const page = Math.max(1, parseInt(req.query.page as string) || 1)
const pageSize = Math.min(100, Math.max(1, parseInt(req.query.pageSize as string) || 20))
const status = req.query.status as string
const offset = (page - 1) * pageSize
let where = '1=1'
const params: any[] = []
if (status && ['pending', 'processed', 'ignored'].includes(status)) {
where += ' AND f.status = ?'
params.push(status)
}
const [countResult] = await pool.execute(
`SELECT COUNT(*) as total FROM feedbacks f WHERE ${where}`,
params
)
const total = (countResult as any[])[0].total
const [rows] = await pool.execute(
`SELECT f.*, u.nickname, u.avatar_url
FROM feedbacks f
LEFT JOIN users u ON f.user_id = u.id
WHERE ${where}
ORDER BY f.created_at DESC
LIMIT ? OFFSET ?`,
[...params, pageSize, offset]
)
res.json({ list: rows, total, page, pageSize })
} catch (error) {
console.error('Get feedback list error:', error)
res.status(500).json({ error: '获取失败' })
}
})
/** 管理员更新反馈状态 */
router.put('/:id/status', authMiddleware, async (req, res) => {
try {
const userId = (req as any).userId
const [users] = await pool.execute('SELECT role FROM users WHERE id = ?', [userId])
const user = (users as any[])[0]
if (!user || user.role !== 'admin') {
return res.status(403).json({ error: '无权限' })
}
const { id } = req.params
const { status, admin_reply } = req.body
const validStatuses = ['pending', 'processed', 'ignored']
if (!validStatuses.includes(status)) {
return res.status(400).json({ error: '无效的状态' })
}
await pool.execute(
'UPDATE feedbacks SET status = ?, admin_reply = ? WHERE id = ?',
[status, admin_reply || '', id]
)
res.json({ success: true })
} catch (error) {
console.error('Update feedback status error:', error)
res.status(500).json({ error: '更新失败' })
}
})
export default router