first commit
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
import Database from 'better-sqlite3';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const db = new Database(path.join(__dirname, 'life_circle.db'));
|
||||
|
||||
db.pragma('journal_mode = WAL');
|
||||
db.pragma('foreign_keys = ON');
|
||||
|
||||
// 初始化数据表
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT NOT NULL UNIQUE,
|
||||
password TEXT NOT NULL,
|
||||
nickname TEXT NOT NULL,
|
||||
avatar TEXT DEFAULT '',
|
||||
phone TEXT DEFAULT '',
|
||||
building TEXT DEFAULT '',
|
||||
bio TEXT DEFAULT '',
|
||||
role TEXT DEFAULT 'user',
|
||||
created_at TEXT DEFAULT (datetime('now', 'localtime'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS posts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
images TEXT DEFAULT '[]',
|
||||
category TEXT DEFAULT '动态',
|
||||
likes INTEGER DEFAULT 0,
|
||||
comments INTEGER DEFAULT 0,
|
||||
created_at TEXT DEFAULT (datetime('now', 'localtime')),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS comments (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
post_id INTEGER NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
created_at TEXT DEFAULT (datetime('now', 'localtime')),
|
||||
FOREIGN KEY (post_id) REFERENCES posts(id),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS likes (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
post_id INTEGER NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
UNIQUE(post_id, user_id),
|
||||
FOREIGN KEY (post_id) REFERENCES posts(id),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS notices (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
title TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
type TEXT DEFAULT '通知',
|
||||
created_at TEXT DEFAULT (datetime('now', 'localtime'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS secondhand (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
price REAL DEFAULT 0,
|
||||
images TEXT DEFAULT '[]',
|
||||
status TEXT DEFAULT '在售',
|
||||
created_at TEXT DEFAULT (datetime('now', 'localtime')),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS housekeeping (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
price TEXT DEFAULT '',
|
||||
phone TEXT DEFAULT '',
|
||||
created_at TEXT DEFAULT (datetime('now', 'localtime')),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
);
|
||||
`);
|
||||
|
||||
export default db;
|
||||
@@ -0,0 +1,55 @@
|
||||
import express from 'express';
|
||||
import cors from 'cors';
|
||||
import authRouter from './routes/auth.js';
|
||||
import postsRouter from './routes/posts.js';
|
||||
import noticesRouter from './routes/notices.js';
|
||||
import secondhandRouter from './routes/secondhand.js';
|
||||
import housekeepingRouter from './routes/housekeeping.js';
|
||||
import db from './db.js';
|
||||
import bcrypt from 'bcryptjs';
|
||||
|
||||
const app = express();
|
||||
app.use(cors());
|
||||
app.use(express.json({ limit: '10mb' }));
|
||||
|
||||
// 初始化管理员账号
|
||||
function seedAdmin() {
|
||||
const admin = db.prepare('SELECT id FROM users WHERE username = ?').get('admin');
|
||||
if (!admin) {
|
||||
const hash = bcrypt.hashSync('admin123', 10);
|
||||
db.prepare('INSERT INTO users (username, password, nickname, role, building) VALUES (?, ?, ?, ?, ?)')
|
||||
.run('admin', hash, '社区管理员', 'admin', '物业中心');
|
||||
console.log('已创建管理员账号: admin / admin123');
|
||||
}
|
||||
// 初始化示例通知
|
||||
const count = db.prepare('SELECT COUNT(*) as c FROM notices').get().c;
|
||||
if (count === 0) {
|
||||
db.prepare('INSERT INTO notices (title, content, type) VALUES (?, ?, ?)')
|
||||
.run('停水通知', '因管道维修,本周六 8:00-12:00 小区将停水,请各位业主提前储水。', '通知');
|
||||
db.prepare('INSERT INTO notices (title, content, type) VALUES (?, ?, ?)')
|
||||
.run('关于规范电动车停放的通知', '请将电动车停放在指定充电棚,严禁上楼充电,感谢配合。', '通知');
|
||||
}
|
||||
}
|
||||
seedAdmin();
|
||||
|
||||
app.use('/api/auth', authRouter);
|
||||
app.use('/api/posts', postsRouter);
|
||||
app.use('/api/notices', noticesRouter);
|
||||
app.use('/api/secondhand', secondhandRouter);
|
||||
app.use('/api/housekeeping', housekeepingRouter);
|
||||
|
||||
// 静态文件(前端构建产物)
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
app.use(express.static(path.join(__dirname, '../client/dist')));
|
||||
|
||||
app.use((err, req, res, next) => {
|
||||
console.error(err);
|
||||
res.status(500).json({ message: '服务器内部错误' });
|
||||
});
|
||||
|
||||
const PORT = process.env.PORT || 3000;
|
||||
app.listen(PORT, () => {
|
||||
console.log(`生活圈系统后端已启动: http://localhost:${PORT}`);
|
||||
});
|
||||
Binary file not shown.
@@ -0,0 +1,28 @@
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
const SECRET = 'life-circle-secret-key-2026';
|
||||
|
||||
export function auth(req, res, next) {
|
||||
const header = req.headers.authorization;
|
||||
if (!header) {
|
||||
return res.status(401).json({ message: '未登录' });
|
||||
}
|
||||
const token = header.split(' ')[1];
|
||||
try {
|
||||
const payload = jwt.verify(token, SECRET);
|
||||
req.user = payload;
|
||||
next();
|
||||
} catch (e) {
|
||||
return res.status(401).json({ message: '登录已过期,请重新登录' });
|
||||
}
|
||||
}
|
||||
|
||||
export function sign(user) {
|
||||
return jwt.sign(
|
||||
{ id: user.id, username: user.username, nickname: user.nickname, role: user.role },
|
||||
SECRET,
|
||||
{ expiresIn: '7d' }
|
||||
);
|
||||
}
|
||||
|
||||
export { SECRET };
|
||||
Generated
+1411
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "life-circle-server",
|
||||
"version": "1.0.0",
|
||||
"description": "社区生活圈系统后端服务",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "node index.js",
|
||||
"dev": "node --watch index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"bcryptjs": "^2.4.3",
|
||||
"better-sqlite3": "^11.3.0",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.19.2",
|
||||
"jsonwebtoken": "^9.0.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import { Router } from 'express';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import db from '../db.js';
|
||||
import { auth, sign } from '../middleware.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// 注册
|
||||
router.post('/register', (req, res) => {
|
||||
const { username, password, nickname, phone, building } = req.body;
|
||||
if (!username || !password || !nickname) {
|
||||
return res.status(400).json({ message: '请填写完整信息' });
|
||||
}
|
||||
if (username.length < 3 || password.length < 6) {
|
||||
return res.status(400).json({ message: '用户名至少3位,密码至少6位' });
|
||||
}
|
||||
const exists = db.prepare('SELECT id FROM users WHERE username = ?').get(username);
|
||||
if (exists) {
|
||||
return res.status(400).json({ message: '用户名已存在' });
|
||||
}
|
||||
const hash = bcrypt.hashSync(password, 10);
|
||||
const info = db
|
||||
.prepare('INSERT INTO users (username, password, nickname, phone, building) VALUES (?, ?, ?, ?, ?)')
|
||||
.run(username, hash, nickname, phone || '', building || '');
|
||||
const user = db.prepare('SELECT * FROM users WHERE id = ?').get(info.lastInsertRowid);
|
||||
res.json({ token: sign(user), user: safeUser(user) });
|
||||
});
|
||||
|
||||
// 登录
|
||||
router.post('/login', (req, res) => {
|
||||
const { username, password } = req.body;
|
||||
const user = db.prepare('SELECT * FROM users WHERE username = ?').get(username);
|
||||
if (!user || !bcrypt.compareSync(password, user.password)) {
|
||||
return res.status(401).json({ message: '用户名或密码错误' });
|
||||
}
|
||||
res.json({ token: sign(user), user: safeUser(user) });
|
||||
});
|
||||
|
||||
// 获取当前用户
|
||||
router.get('/me', auth, (req, res) => {
|
||||
const user = db.prepare('SELECT * FROM users WHERE id = ?').get(req.user.id);
|
||||
if (!user) return res.status(404).json({ message: '用户不存在' });
|
||||
res.json(safeUser(user));
|
||||
});
|
||||
|
||||
// 更新个人资料
|
||||
router.put('/me', auth, (req, res) => {
|
||||
const { nickname, phone, building, bio } = req.body;
|
||||
db.prepare('UPDATE users SET nickname = ?, phone = ?, building = ?, bio = ? WHERE id = ?')
|
||||
.run(nickname, phone || '', building || '', bio || '', req.user.id);
|
||||
const user = db.prepare('SELECT * FROM users WHERE id = ?').get(req.user.id);
|
||||
res.json(safeUser(user));
|
||||
});
|
||||
|
||||
// 修改密码
|
||||
router.put('/password', auth, (req, res) => {
|
||||
const { oldPassword, newPassword } = req.body;
|
||||
const user = db.prepare('SELECT * FROM users WHERE id = ?').get(req.user.id);
|
||||
if (!bcrypt.compareSync(oldPassword, user.password)) {
|
||||
return res.status(400).json({ message: '原密码错误' });
|
||||
}
|
||||
if (newPassword.length < 6) {
|
||||
return res.status(400).json({ message: '新密码至少6位' });
|
||||
}
|
||||
const hash = bcrypt.hashSync(newPassword, 10);
|
||||
db.prepare('UPDATE users SET password = ? WHERE id = ?').run(hash, req.user.id);
|
||||
res.json({ message: '修改成功' });
|
||||
});
|
||||
|
||||
function safeUser(u) {
|
||||
const { password, ...rest } = u;
|
||||
return rest;
|
||||
}
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,38 @@
|
||||
import { Router } from 'express';
|
||||
import db from '../db.js';
|
||||
import { auth } from '../middleware.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// 家政服务列表
|
||||
router.get('/', auth, (req, res) => {
|
||||
const items = db
|
||||
.prepare('SELECT h.*, u.nickname, u.avatar FROM housekeeping h JOIN users u ON h.user_id = u.id ORDER BY h.created_at DESC')
|
||||
.all();
|
||||
res.json(items);
|
||||
});
|
||||
|
||||
// 发布家政服务
|
||||
router.post('/', auth, (req, res) => {
|
||||
const { title, description, price, phone } = req.body;
|
||||
if (!title || !description) {
|
||||
return res.status(400).json({ message: '请填写服务名称和描述' });
|
||||
}
|
||||
const info = db
|
||||
.prepare('INSERT INTO housekeeping (user_id, title, description, price, phone) VALUES (?, ?, ?, ?, ?)')
|
||||
.run(req.user.id, title, description, price || '', phone || '');
|
||||
res.json(db.prepare('SELECT * FROM housekeeping WHERE id = ?').get(info.lastInsertRowid));
|
||||
});
|
||||
|
||||
// 删除服务
|
||||
router.delete('/:id', auth, (req, res) => {
|
||||
const item = db.prepare('SELECT * FROM housekeeping WHERE id = ?').get(req.params.id);
|
||||
if (!item) return res.status(404).json({ message: '服务不存在' });
|
||||
if (item.user_id !== req.user.id && req.user.role !== 'admin') {
|
||||
return res.status(403).json({ message: '无权删除' });
|
||||
}
|
||||
db.prepare('DELETE FROM housekeeping WHERE id = ?').run(req.params.id);
|
||||
res.json({ message: '删除成功' });
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Router } from 'express';
|
||||
import db from '../db.js';
|
||||
import { auth } from '../middleware.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// 物业通知列表
|
||||
router.get('/', (req, res) => {
|
||||
const notices = db.prepare('SELECT * FROM notices ORDER BY created_at DESC').all();
|
||||
res.json(notices);
|
||||
});
|
||||
|
||||
// 发布通知(管理员)
|
||||
router.post('/', auth, (req, res) => {
|
||||
if (req.user.role !== 'admin') {
|
||||
return res.status(403).json({ message: '仅管理员可发布通知' });
|
||||
}
|
||||
const { title, content, type } = req.body;
|
||||
if (!title || !content) {
|
||||
return res.status(400).json({ message: '请填写标题和内容' });
|
||||
}
|
||||
const info = db
|
||||
.prepare('INSERT INTO notices (title, content, type) VALUES (?, ?, ?)')
|
||||
.run(title, content, type || '通知');
|
||||
res.json(db.prepare('SELECT * FROM notices WHERE id = ?').get(info.lastInsertRowid));
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,90 @@
|
||||
import { Router } from 'express';
|
||||
import db from '../db.js';
|
||||
import { auth } from '../middleware.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// 获取动态列表(含用户信息、是否已点赞)
|
||||
router.get('/', auth, (req, res) => {
|
||||
const { category } = req.query;
|
||||
let sql = `
|
||||
SELECT p.*, u.nickname, u.avatar,
|
||||
(SELECT COUNT(*) FROM likes l WHERE l.post_id = p.id) as like_count,
|
||||
EXISTS(SELECT 1 FROM likes l WHERE l.post_id = p.id AND l.user_id = ?) as liked
|
||||
FROM posts p JOIN users u ON p.user_id = u.id
|
||||
`;
|
||||
const params = [req.user.id];
|
||||
if (category && category !== '全部') {
|
||||
sql += ' WHERE p.category = ?';
|
||||
params.push(category);
|
||||
}
|
||||
sql += ' ORDER BY p.created_at DESC';
|
||||
const posts = db.prepare(sql).all(...params);
|
||||
res.json(posts.map(p => ({ ...p, images: JSON.parse(p.images || '[]') })));
|
||||
});
|
||||
|
||||
// 发布动态
|
||||
router.post('/', auth, (req, res) => {
|
||||
const { content, images, category } = req.body;
|
||||
if (!content || !content.trim()) {
|
||||
return res.status(400).json({ message: '内容不能为空' });
|
||||
}
|
||||
const info = db
|
||||
.prepare('INSERT INTO posts (user_id, content, images, category) VALUES (?, ?, ?, ?)')
|
||||
.run(req.user.id, content, JSON.stringify(images || []), category || '动态');
|
||||
const post = db.prepare('SELECT * FROM posts WHERE id = ?').get(info.lastInsertRowid);
|
||||
res.json({ ...post, images: JSON.parse(post.images || '[]') });
|
||||
});
|
||||
|
||||
// 删除动态
|
||||
router.delete('/:id', auth, (req, res) => {
|
||||
const post = db.prepare('SELECT * FROM posts WHERE id = ?').get(req.params.id);
|
||||
if (!post) return res.status(404).json({ message: '动态不存在' });
|
||||
if (post.user_id !== req.user.id && req.user.role !== 'admin') {
|
||||
return res.status(403).json({ message: '无权删除' });
|
||||
}
|
||||
db.prepare('DELETE FROM comments WHERE post_id = ?').run(req.params.id);
|
||||
db.prepare('DELETE FROM likes WHERE post_id = ?').run(req.params.id);
|
||||
db.prepare('DELETE FROM posts WHERE id = ?').run(req.params.id);
|
||||
res.json({ message: '删除成功' });
|
||||
});
|
||||
|
||||
// 点赞/取消点赞
|
||||
router.post('/:id/like', auth, (req, res) => {
|
||||
const post = db.prepare('SELECT id FROM posts WHERE id = ?').get(req.params.id);
|
||||
if (!post) return res.status(404).json({ message: '动态不存在' });
|
||||
const existing = db
|
||||
.prepare('SELECT id FROM likes WHERE post_id = ? AND user_id = ?')
|
||||
.get(req.params.id, req.user.id);
|
||||
if (existing) {
|
||||
db.prepare('DELETE FROM likes WHERE id = ?').run(existing.id);
|
||||
res.json({ liked: false });
|
||||
} else {
|
||||
db.prepare('INSERT INTO likes (post_id, user_id) VALUES (?, ?)').run(req.params.id, req.user.id);
|
||||
res.json({ liked: true });
|
||||
}
|
||||
});
|
||||
|
||||
// 获取评论
|
||||
router.get('/:id/comments', auth, (req, res) => {
|
||||
const comments = db
|
||||
.prepare('SELECT c.*, u.nickname, u.avatar FROM comments c JOIN users u ON c.user_id = u.id WHERE c.post_id = ? ORDER BY c.created_at ASC')
|
||||
.all(req.params.id);
|
||||
res.json(comments);
|
||||
});
|
||||
|
||||
// 发表评论
|
||||
router.post('/:id/comments', auth, (req, res) => {
|
||||
const { content } = req.body;
|
||||
if (!content || !content.trim()) {
|
||||
return res.status(400).json({ message: '评论不能为空' });
|
||||
}
|
||||
db.prepare('INSERT INTO comments (post_id, user_id, content) VALUES (?, ?, ?)')
|
||||
.run(req.params.id, req.user.id, content);
|
||||
const list = db
|
||||
.prepare('SELECT c.*, u.nickname, u.avatar FROM comments c JOIN users u ON c.user_id = u.id WHERE c.post_id = ? ORDER BY c.created_at ASC')
|
||||
.all(req.params.id);
|
||||
res.json(list);
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,50 @@
|
||||
import { Router } from 'express';
|
||||
import db from '../db.js';
|
||||
import { auth } from '../middleware.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// 二手闲置列表
|
||||
router.get('/', auth, (req, res) => {
|
||||
const items = db
|
||||
.prepare('SELECT s.*, u.nickname, u.avatar FROM secondhand s JOIN users u ON s.user_id = u.id ORDER BY s.created_at DESC')
|
||||
.all();
|
||||
res.json(items.map(i => ({ ...i, images: JSON.parse(i.images || '[]') })));
|
||||
});
|
||||
|
||||
// 发布闲置
|
||||
router.post('/', auth, (req, res) => {
|
||||
const { title, description, price, images } = req.body;
|
||||
if (!title || !description) {
|
||||
return res.status(400).json({ message: '请填写标题和描述' });
|
||||
}
|
||||
const info = db
|
||||
.prepare('INSERT INTO secondhand (user_id, title, description, price, images) VALUES (?, ?, ?, ?, ?)')
|
||||
.run(req.user.id, title, description, price || 0, JSON.stringify(images || []));
|
||||
res.json(db.prepare('SELECT * FROM secondhand WHERE id = ?').get(info.lastInsertRowid));
|
||||
});
|
||||
|
||||
// 更新状态(下架/售出)
|
||||
router.put('/:id', auth, (req, res) => {
|
||||
const item = db.prepare('SELECT * FROM secondhand WHERE id = ?').get(req.params.id);
|
||||
if (!item) return res.status(404).json({ message: '物品不存在' });
|
||||
if (item.user_id !== req.user.id && req.user.role !== 'admin') {
|
||||
return res.status(403).json({ message: '无权操作' });
|
||||
}
|
||||
const { status } = req.body;
|
||||
db.prepare('UPDATE secondhand SET status = ? WHERE id = ?').run(status, req.params.id);
|
||||
res.json(db.prepare('SELECT * FROM secondhand WHERE id = ?').get(req.params.id));
|
||||
});
|
||||
|
||||
// 删除闲置
|
||||
router.delete('/:id', auth, (req, res) => {
|
||||
const item = db.prepare('SELECT * FROM secondhand WHERE id = ?').get(req.params.id);
|
||||
if (!item) return res.status(404).json({ message: '物品不存在' });
|
||||
if (item.user_id !== req.user.id && req.user.role !== 'admin') {
|
||||
return res.status(403).json({ message: '无权删除' });
|
||||
}
|
||||
db.prepare('DELETE FROM secondhand WHERE id = ?').run(req.params.id);
|
||||
res.json({ message: '删除成功' });
|
||||
});
|
||||
|
||||
export default router;
|
||||
Reference in New Issue
Block a user