From 29614fdb707d4ae2227b20fe849b155ac66b7f65 Mon Sep 17 00:00:00 2001
From: wangxiaogang <1433729587@qq.com>
Date: Mon, 8 Jun 2026 10:16:28 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20=E9=9A=90=E7=A7=81=E6=94=BF=E7=AD=96=20?=
=?UTF-8?q?+=20=E6=84=8F=E8=A7=81=E5=8F=8D=E9=A6=88=E7=B3=BB=E7=BB=9F?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 新增隐私政策页面,满足小程序审核要求
- 新增意见反馈功能(用户提交 + 管理员查看)
- 后端新增 feedbacks 表和反馈 API
- 管理后台新增反馈管理入口
- 我的页面添加意见反馈、隐私政策入口
---
client/src/api/feedback.ts | 20 ++
client/src/pages.json | 21 ++
client/src/pages/admin/feedback.vue | 418 ++++++++++++++++++++++++++++
client/src/pages/admin/index.vue | 6 +
client/src/pages/feedback/index.vue | 262 +++++++++++++++++
client/src/pages/privacy/index.vue | 155 +++++++++++
client/src/pages/profile/index.vue | 16 ++
server/src/db/schema.sql | 16 ++
server/src/index.ts | 2 +
server/src/routes/feedback.ts | 110 ++++++++
10 files changed, 1026 insertions(+)
create mode 100644 client/src/api/feedback.ts
create mode 100644 client/src/pages/admin/feedback.vue
create mode 100644 client/src/pages/feedback/index.vue
create mode 100644 client/src/pages/privacy/index.vue
create mode 100644 server/src/routes/feedback.ts
diff --git a/client/src/api/feedback.ts b/client/src/api/feedback.ts
new file mode 100644
index 0000000..73e7088
--- /dev/null
+++ b/client/src/api/feedback.ts
@@ -0,0 +1,20 @@
+import { request } from '@/utils/request'
+
+/** 提交反馈 */
+export function submitFeedback(data: {
+ type: string
+ content: string
+ contact?: string
+}) {
+ return request('/feedback', { method: 'POST', data })
+}
+
+/** 获取反馈列表(管理员) */
+export function getFeedbackList(params?: { page?: number; pageSize?: number; status?: string }) {
+ return request('/feedback', { params })
+}
+
+/** 更新反馈状态(管理员) */
+export function updateFeedbackStatus(id: number, status: string) {
+ return request(`/feedback/${id}/status`, { method: 'PUT', data: { status } })
+}
diff --git a/client/src/pages.json b/client/src/pages.json
index 573f229..c03dba8 100644
--- a/client/src/pages.json
+++ b/client/src/pages.json
@@ -96,6 +96,27 @@
"navigationBarTitleText": "公告管理",
"enablePullDownRefresh": true
}
+ },
+ {
+ "path": "pages/privacy/index",
+ "style": {
+ "navigationStyle": "custom",
+ "navigationBarTitleText": "隐私政策"
+ }
+ },
+ {
+ "path": "pages/feedback/index",
+ "style": {
+ "navigationStyle": "custom",
+ "navigationBarTitleText": "意见反馈"
+ }
+ },
+ {
+ "path": "pages/admin/feedback",
+ "style": {
+ "navigationStyle": "custom",
+ "navigationBarTitleText": "反馈管理"
+ }
}
],
"globalStyle": {
diff --git a/client/src/pages/admin/feedback.vue b/client/src/pages/admin/feedback.vue
new file mode 100644
index 0000000..8a0de9e
--- /dev/null
+++ b/client/src/pages/admin/feedback.vue
@@ -0,0 +1,418 @@
+
+
+
+
+
+
+
+ 全部
+ 待处理
+ 已处理
+ 已忽略
+
+
+
+
+
+
+
+
+
+ {{ typeMap[item.type] }}
+
+
+ {{ item.content }}
+
+
+ 联系方式:
+ {{ item.contact }}
+
+
+ {{ formatTime(item.created_at) }}
+
+
+
+
+ 标记处理
+
+
+ 忽略
+
+
+ 重新打开
+
+
+
+
+
+ 加载中...
+
+
+
+ 没有更多了
+
+
+
+
+ 暂无反馈
+
+
+
+
+
+
+
+
diff --git a/client/src/pages/admin/index.vue b/client/src/pages/admin/index.vue
index f652ae2..bdab582 100644
--- a/client/src/pages/admin/index.vue
+++ b/client/src/pages/admin/index.vue
@@ -72,6 +72,11 @@
公告管理
+
+
+ 反馈管理
+
+
@@ -114,6 +119,7 @@ onMounted(async () => {
function goBack() { uni.navigateBack() }
function goUsers() { uni.navigateTo({ url: '/pages/admin/users' }) }
function goNotificationManage() { uni.navigateTo({ url: '/pages/admin/notifications' }) }
+function goFeedbackManage() { uni.navigateTo({ url: '/pages/admin/feedback' }) }
diff --git a/client/src/pages/privacy/index.vue b/client/src/pages/privacy/index.vue
new file mode 100644
index 0000000..171189a
--- /dev/null
+++ b/client/src/pages/privacy/index.vue
@@ -0,0 +1,155 @@
+
+
+
+
+
+ 更新日期:2026年6月8日
+
+ 一、引言
+ 小菜记账(以下简称"我们")非常重视用户的隐私保护。本隐私政策旨在向您说明我们如何收集、使用、存储和保护您的个人信息,请您仔细阅读。
+
+ 二、我们收集的信息
+ 在您使用本小程序时,我们可能收集以下信息:
+ 1. 微信 openid:用于识别用户身份,实现登录功能。
+ 2. 用户昵称:用于个性化展示,您可以自行设置或修改。
+ 3. 用户头像:用于个性化展示,您可以选择上传自定义头像。
+ 4. 记账数据:包括金额、分类、备注、日期等,仅用于为您提供记账服务。
+
+ 三、信息的使用目的
+ 我们收集的信息仅用于以下目的:
+ 1. 提供记账、统计、预算等核心功能服务。
+ 2. 实现群组记账功能,与您邀请的好友共享账单数据。
+ 3. 发送系统公告和重要通知。
+ 4. 改善产品功能和用户体验。
+
+ 四、信息的存储与保护
+ 1. 您的数据存储在我们的服务器数据库中,采用加密传输和访问控制。
+ 2. 我们不会将您的个人信息出售给第三方。
+ 3. 除法律法规要求外,我们不会向任何第三方披露您的个人信息。
+
+ 五、群组功能说明
+ 1. 群组功能仅用于聚合展示成员的记账数据,记录始终属于记录者本人。
+ 2. 群组成员可以看到其他成员的记账记录(金额、分类、日期)。
+ 3. 您可以随时退出群组,退出后您的记录将回到个人账本。
+
+ 六、您的权利
+ 您享有以下权利:
+ 1. 查看和修改您的个人信息(昵称、头像)。
+ 2. 删除您的记账记录。
+ 3. 退出或解散群组。
+ 4. 导出您的账单数据。
+
+ 七、未成年人保护
+ 如果您是未满 18 周岁的未成年人,请在监护人的陪同下阅读本政策,并在监护人同意后使用本服务。
+
+ 八、政策更新
+ 我们可能会不时更新本隐私政策,更新后的政策将在小程序内公布。继续使用本服务即视为您同意更新后的政策。
+
+ 九、联系我们
+ 如您对本隐私政策有任何疑问,请通过小程序内的「关于小菜」联系我们。
+
+
+
+
+
+
+
+
+
diff --git a/client/src/pages/profile/index.vue b/client/src/pages/profile/index.vue
index bc78ff4..9516e41 100644
--- a/client/src/pages/profile/index.vue
+++ b/client/src/pages/profile/index.vue
@@ -58,10 +58,18 @@
@@ -193,6 +201,14 @@ function goCategoryManage() {
uni.navigateTo({ url: '/pages/category-manage/index' })
}
+function goPrivacy() {
+ uni.navigateTo({ url: '/pages/privacy/index' })
+}
+
+function goFeedback() {
+ uni.navigateTo({ url: '/pages/feedback/index' })
+}
+
function goProfileEdit() {
uni.navigateTo({ url: '/pages/profile-edit/index' })
}
diff --git a/server/src/db/schema.sql b/server/src/db/schema.sql
index 4c8b32c..c8265d8 100644
--- a/server/src/db/schema.sql
+++ b/server/src/db/schema.sql
@@ -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;
diff --git a/server/src/index.ts b/server/src/index.ts
index c052c17..ffdd491 100644
--- a/server/src/index.ts
+++ b/server/src/index.ts
@@ -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 {
diff --git a/server/src/routes/feedback.ts b/server/src/routes/feedback.ts
new file mode 100644
index 0000000..9677b94
--- /dev/null
+++ b/server/src/routes/feedback.ts
@@ -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