first commit

This commit is contained in:
yanpeng200212
2026-10-08 10:43:59 +08:00
commit 8430ba0452
288 changed files with 29595 additions and 0 deletions
+89
View File
@@ -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;
+55
View File
@@ -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.
+28
View File
@@ -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 };
+1411
View File
File diff suppressed because it is too large Load Diff
+18
View File
@@ -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"
}
}
+75
View File
@@ -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;
+38
View File
@@ -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;
+28
View File
@@ -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;
+90
View File
@@ -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;
+50
View File
@@ -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;