diff --git a/backend/db.js b/backend/db.js
index 4417ceb..2bcc470 100644
--- a/backend/db.js
+++ b/backend/db.js
@@ -200,6 +200,23 @@ async function initSchema(connection) {
INDEX idx_poll (poll_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`);
+ await createIfNotExists('feature_items', `CREATE TABLE feature_items (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ title VARCHAR(200) NOT NULL,
+ description TEXT,
+ status ENUM('pending','planned','done') NOT NULL DEFAULT 'pending',
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`);
+
+ await createIfNotExists('feature_votes', `CREATE TABLE feature_votes (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ item_id INT NOT NULL,
+ user_id INT NOT NULL,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ UNIQUE KEY uk_item_user (item_id, user_id),
+ INDEX idx_item (item_id)
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`);
+
await createIfNotExists('audit_logs', `CREATE TABLE audit_logs (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT,
diff --git a/backend/routes/features.js b/backend/routes/features.js
new file mode 100644
index 0000000..d92f3a9
--- /dev/null
+++ b/backend/routes/features.js
@@ -0,0 +1,51 @@
+const express = require('express');
+const { query, getRow } = require('../db');
+const { authenticate, requireRole } = require('../middleware/auth');
+
+const router = express.Router();
+
+router.get('/', authenticate, async (req, res) => {
+ const items = await query(`SELECT fi.*,
+ (SELECT COUNT(*) FROM feature_votes WHERE item_id = fi.id) as votes_count,
+ (SELECT COUNT(*) FROM feature_votes WHERE item_id = fi.id AND user_id = ?) as my_vote
+ FROM feature_items fi ORDER BY votes_count DESC, fi.created_at DESC`, [req.user.id]);
+ res.json(items);
+});
+
+router.post('/', authenticate, requireRole('owner'), async (req, res) => {
+ const { title, description } = req.body;
+ if (!title) return res.status(400).json({ error: '标题为必填' });
+ const r = await query('INSERT INTO feature_items(title, description) VALUES (?,?)', [title, description||'']);
+ res.status(201).json({ id: r.insertId });
+});
+
+router.put('/:id', authenticate, requireRole('owner'), async (req, res) => {
+ const fields = {};
+ if (req.body.title) fields.title = req.body.title;
+ if (req.body.description !== undefined) fields.description = req.body.description;
+ if (req.body.status) fields.status = req.body.status;
+ if (!Object.keys(fields).length) return res.status(400).json({ error: '无更新内容' });
+ const sets = Object.keys(fields).map(k => `${k} = ?`).join(', ');
+ await query(`UPDATE feature_items SET ${sets} WHERE id = ?`, [...Object.values(fields), req.params.id]);
+ res.json({ message: '更新成功' });
+});
+
+router.delete('/:id', authenticate, requireRole('owner'), async (req, res) => {
+ await query('DELETE FROM feature_votes WHERE item_id = ?', [req.params.id]);
+ await query('DELETE FROM feature_items WHERE id = ?', [req.params.id]);
+ res.json({ message: '已删除' });
+});
+
+router.post('/:id/vote', authenticate, async (req, res) => {
+ const item = await getRow('SELECT id FROM feature_items WHERE id = ?', [req.params.id]);
+ if (!item) return res.status(404).json({ error: '不存在' });
+ const voted = await getRow('SELECT id FROM feature_votes WHERE item_id = ? AND user_id = ?', [item.id, req.user.id]);
+ if (voted) {
+ await query('DELETE FROM feature_votes WHERE item_id = ? AND user_id = ?', [item.id, req.user.id]);
+ return res.json({ message: '已取消投票' });
+ }
+ await query('INSERT INTO feature_votes(item_id, user_id) VALUES (?,?)', [item.id, req.user.id]);
+ res.json({ message: '投票成功' });
+});
+
+module.exports = router;
diff --git a/backend/routes/polls.js b/backend/routes/polls.js
index 89c9df0..17aacce 100644
--- a/backend/routes/polls.js
+++ b/backend/routes/polls.js
@@ -4,9 +4,10 @@ const { authenticate, requireRole } = require('../middleware/auth');
const router = express.Router();
-router.get('/', authenticate, async (req, res) => {
- const polls = await query('SELECT id, title, description, active, created_at FROM polls ORDER BY created_at DESC');
- res.json(polls);
+router.get('/:id', authenticate, requireRole('owner'), async (req, res) => {
+ const p = await getRow('SELECT * FROM polls WHERE id = ?', [req.params.id]);
+ if (!p) return res.status(404).json({ error: '不存在' });
+ res.json(p);
});
router.get('/active', authenticate, async (req, res) => {
diff --git a/backend/server.js b/backend/server.js
index 87a2529..ddb6b41 100644
--- a/backend/server.js
+++ b/backend/server.js
@@ -105,6 +105,7 @@ if (isInstalled()) {
app.use('/api/uploads', methodGuard(['GET']), generalLimiter, require('./routes/uploads'));
app.use('/api/notifications', methodGuard(['GET','POST','PUT','DELETE']), generalLimiter, require('./routes/notifications'));
app.use('/api/polls', methodGuard(['GET','POST','PUT','DELETE']), generalLimiter, require('./routes/polls'));
+ app.use('/api/features', methodGuard(['GET','POST','PUT','DELETE']), generalLimiter, require('./routes/features'));
app.use('/api/external', require('./routes/external'));
app.get('/api/verify', (req, res) => res.redirect(`/#/verify?token=${req.query.token}`));
diff --git a/public/index.html b/public/index.html
index b6863a2..c3401fd 100644
--- a/public/index.html
+++ b/public/index.html
@@ -70,6 +70,7 @@
+