项目缘起
这是我在国庆期间完成的第二个Vibe项目,前后端代码接近10万行,数据库28张表,全部由我一个人独立完成。
项目起源于一个非常私人的需求:大女儿青云明年就要上小学了,我在查学校、学区、政策时遇到了很多障碍。作为一个技术人,实在无法忍受这种信息不对称和查询困难,于是就有了这个项目。
开发过程很有意思:首先花了几天时间跟GPT反复讨论需求,确定产品方向;然后通过各种AI工具采集整理汇总学校的各种信息,包括对口划片、中/高考成绩、分配生政策等;最后实现了资讯、问答、用户及权限等模块,甚至还做了个排行榜。(BTW:LOGO是用GPT生成)。
老规矩,先上几张图:








项目概述
这是一个面向教育行业的综合信息平台,提供学校信息查询、对口划片、升学政策、排行榜计算等功能。项目采用前后端分离架构,由我一个人独立完成全部开发工作,包括前端30+页面组件、后端20+业务模块、28张数据库表的设计与实现。
技术架构
前端技术栈
框架: React 18 + TypeScript + Vite
路由: React Router v6
状态管理: Zustand
UI组件: 自研组件库
SEO优化: React Helmet Async
构建工具: Vite
后端技术栈
运行环境: Node.js + Express
数据库: PostgreSQL
认证授权: JWT + Session双重认证
文件存储: 腾讯云COS对象存储
邮箱服务: 腾讯云邮箱
地图服务: 百度地图API
搜索服务:腾讯云WSA+阿里云IQS
AI服务: 阿里云QWEN大模型
前端功能架构
基于React Router的路由架构,实现了完整的前端功能模块:
1. 学校信息模块
graph TD
A[首页] --> B[学校信息模块]
B --> B1[学校列表]
B --> B2[学校详情]
B2 --> B2_1[学校概览]
B2 --> B2_2[学校简介]
B2 --> B2_3[荣誉奖项]
B2 --> B2_4[特色项目]
B2 --> B2_5[校园风采]
B2 --> B2_6[相关资讯和问答]
B2 --> B2_7[对口小区和初中]
B2 --> B2_8[中考高考成绩]
B2 --> B2_9[综合评价]
B --> B3[学区查询]
B --> B4[学校地图]
2. 排行榜模块
graph TD
A[首页] --> C[排行榜模块]
C --> C1[小学排行]
C --> C2[初中排行]
C --> C3[高中排行]
3. 社区问答模块
graph TD
A[首页] --> D[问答模块]
D --> D1[问答首页]
D --> D2[问题详情]
D --> D3[提问页面]
4. 文章资讯模块
graph TD
A[首页] --> E[文章资讯模块]
E --> E1[文章列表]
E --> E2[文章详情]
E --> E3[文章编辑]
E --> E4[文章发布]
style E3 fill:#ffcccc
style E4 fill:#ffcccc
5. 用户中心模块
graph TD
A[首页] --> F[用户中心模块]
F --> F1[用户注册]
F --> F2[用户登录]
F --> F3[用户中心]
F --> F4[安全设置]
F --> F5[收藏页面]
6. 分配生政策模块
graph TD
A[首页] --> G[分配生政策模块]
G --> G1[分配生详情]
G --> G2[配额管理]
style G2 fill:#ffcccc
7. 关系管理模块
graph TD
A[首页] --> H[关系管理模块]
H --> H1[小区对口关系]
H --> H2[初中对口关系]
H --> H3[关系操作]
style H3 fill:#ffcccc
8. 管理模块
graph TD
A[首页] --> I[管理模块]
I --> I1[学校管理]
I --> I2[地图管理]
I --> I3[排名管理]
style I1 fill:#ffcccc
style I2 fill:#ffcccc
style I3 fill:#ffcccc
真实技术难点
1. 腾讯云COS对象存储集成
文件路径: xueapp/routes/photos.js
实现了完整的图片上传、管理、删除功能,包括:
// COS配置与初始化
const COS = require('cos-nodejs-sdk-v5');
const cos = new COS({
SecretId: process.env.TENCENT_COS_SECRET_ID,
SecretKey: process.env.TENCENT_COS_SECRET_KEY
});
// 图片上传处理
router.post('/upload', requireAuth, upload.single('image'), async (req, res) => {
const fileName = `school-photos/${Date.now()}-${req.file.originalname}`;
const result = await cos.putObject({
Bucket: process.env.TENCENT_COS_BUCKET,
Region: process.env.TENCENT_COS_REGION,
Key: fileName,
Body: req.file.buffer,
ContentType: req.file.mimetype
});
// 数据库记录保存
await db.query(
'INSERT INTO school_photos (school_id, image_url, uploaded_by) VALUES ($1, $2, $3)',
[schoolId, result.Location, req.user.id]
);
});
2. 复杂地理信息处理
文件路径: xueapp/routes/baidu_api.js、xueapp/routes/poi.js
实现了百度地图API集成、坐标转换、AOI区域计算:
// 坐标系转换(GCJ-02转BD-09)
function gcj02ToBd09(lng, lat) {
const z = Math.sqrt(lng * lng + lat * lat) + 0.00002 * Math.sin(lat * PI * 3000.0 / 180.0);
const theta = Math.atan2(lat, lng) + 0.000003 * Math.cos(lng * PI * 3000.0 / 180.0);
const bd_lng = z * Math.cos(theta) + 0.0065;
const bd_lat = z * Math.sin(theta) + 0.006;
return { lng: bd_lng, lat: bd_lat };
}
// AOI区域查询
async function queryAOI(schoolId, distance = 3000) {
const school = await getSchoolLocation(schoolId);
const aois = await baiduMapClient.placeAOI({
location: `${school.lat},${school.lng}`,
radius: distance,
filter: 'scope:2'
});
return aois.map(aoi => ({
name: aoi.name,
location: aoi.location,
area: calculateArea(aoi.boundary),
type: aoi.type
}));
}
3. AI内容整理
文件路径: xueapp/routes/ai.js
首先通过阿里云IQS和腾讯云WSA搜索相关信息,集成阿里云QWEN大模型,实现文章内容智能整理:
const { DashScopeClient } = require('@alicloud/dashscope-sdk');
const dashscope = new DashScopeClient({
apiKey: process.env.DASHSCOPE_API_KEY,
model: 'qwen-turbo'
});
// 文章内容整理
router.post('/organize-content', requireAdmin, async (req, res) => {
const { content, type } = req.body;
const prompt = `请将以下内容整理成结构化的教育信息:\n${content}\n\n请提取关键信息点,包括学校特色、师资力量、课程设置等。`;
const result = await dashscope.chat({
messages: [{ role: 'user', content: prompt }],
temperature: 0.3,
max_tokens: 2000
});
// 解析AI返回结果
const organizedContent = parseAIResponse(result.output.choices[0].message.content);
res.json({
success: true,
data: organizedContent
});
});
4. 权限系统设计
文件路径: xueapp/middleware/auth.js
实现了JWT+Session双重认证、RBAC权限模型:
// JWT验证
function authenticateToken(req, res, next) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'Access token required' });
}
jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
if (err) return res.status(403).json({ error: 'Invalid token' });
req.user = user;
next();
});
}
// 角色权限控制
function requireRole(role) {
return (req, res, next) => {
if (!req.user || req.user.role !== role) {
return res.status(403).json({ error: 'Insufficient permissions' });
}
next();
};
}
// 数据权限验证
function requireOwnerOrAdmin(req, res, next) {
const resourceId = req.params.id;
const userId = req.user.id;
const userRole = req.user.role;
if (userRole === 'admin') {
return next();
}
// 验证资源所有权
db.query('SELECT user_id FROM resources WHERE id = $1', [resourceId])
.then(result => {
if (result.rows[0].user_id !== userId) {
return res.status(403).json({ error: 'Access denied' });
}
next();
});
}
5. 访问统计防刷机制
文件路径: xueapp/routes/views.js、xueapp/middleware/auth.js
实现了5分钟防重复、IP限制:
// 访问统计中间件
const viewTracker = async (req, res, next) => {
const schoolId = req.params.id;
const userIp = req.ip || req.connection.remoteAddress;
const userAgent = req.get('User-Agent');
// 防刷检查:5分钟内同一IP不重复计数
const recentView = await db.query(
`SELECT id FROM page_views
WHERE school_id = $1 AND ip_address = $2
AND created_at > NOW() - INTERVAL '5 minutes'`,
[schoolId, userIp]
);
if (recentView.rows.length === 0) {
// 记录有效访问
await db.query(
`INSERT INTO page_views (school_id, ip_address, user_agent, view_date)
VALUES ($1, $2, $3, CURRENT_DATE)`,
[schoolId, userIp, userAgent]
);
// 更新学校访问统计
await db.query(
`UPDATE school_rankings SET view_count = view_count + 1 WHERE school_id = $1`,
[schoolId]
);
}
next();
};
// 速率限制
const rateLimit = (windowMs = 60000, max = 100) => {
const requests = new Map();
return (req, res, next) => {
const ip = req.ip;
const now = Date.now();
if (!requests.has(ip)) {
requests.set(ip, []);
}
const ipRequests = requests.get(ip).filter(time => now - time < windowMs);
if (ipRequests.length >= max) {
return res.status(429).json({ error: 'Too many requests' });
}
ipRequests.push(now);
requests.set(ip, ipRequests);
next();
};
};
6. 复杂SQL查询
文件路径: xueapp/routes/questions.js、xueapp/routes/relation.js
实现了学校与小区多对多关系、数组字段查询:
// 学校与小区对口关系查询
async function getSchoolDistrictRelations(schoolId) {
const query = `
SELECT
gsl.name as school_name,
gsl.level,
gsl.attributes,
array_agg(
DISTINCT jsonb_build_object(
'id', h.id,
'name', h.name,
'address', h.address,
'distance', ST_Distance(
gsl.location::geography,
h.location::geography
)
)
) as matched_houses
FROM gov_school_list gsl
LEFT JOIN house_school_relations hsr ON gsl.id = hsr.school_id
LEFT JOIN houses h ON hsr.house_id = h.id
WHERE gsl.id = $1
GROUP BY gsl.id, gsl.name, gsl.level, gsl.attributes
`;
const result = await db.query(query, [schoolId]);
return result.rows[0];
}
7. 数据迁移与清洗
文件路径: xueapp/migrate_content_to_markdown.js、xueapp/convert_gcj02_to_bd09.js
实现了内容转Markdown、坐标转换:
// HTML转Markdown迁移
const TurndownService = require('turndown');
const turndownService = new TurndownService({
headingStyle: 'atx',
codeBlockStyle: 'fenced'
});
async function migrateContentToMarkdown() {
const client = await db.connect();
try {
await client.query('BEGIN');
// 查询所有HTML内容
const result = await client.query(
'SELECT id, content FROM articles WHERE content LIKE <%>'
);
for (const row of result.rows) {
const markdownContent = turndownService.turndown(row.content);
await client.query(
'UPDATE articles SET content_md = $1 WHERE id = $2',
[markdownContent, row.id]
);
}
await client.query('COMMIT');
console.log(`成功迁移 ${result.rows.length} 条记录`);
} catch (error) {
await client.query('ROLLBACK');
throw error;
} finally {
client.release();
}
}
// 坐标批量转换脚本
async function batchConvertCoordinates() {
const schools = await db.query(
'SELECT id, lat, lng FROM schools WHERE coordinate_system = gcj-02'
);
for (const school of schools.rows) {
const bd09Coord = gcj02ToBd09(school.lng, school.lat);
await db.query(
'UPDATE schools SET lat = $1, lng = $2, coordinate_system = $3 WHERE id = $4',
[bd09Coord.lat, bd09Coord.lng, 'bd-09', school.id]
);
}
console.log(`完成 ${schools.rows.length} 个学校的坐标转换`);
}
项目规模统计
基于实际代码文件统计:
前端页面组件: 30个(基于
/web/src/pages/目录文件统计)后端路由模块: 22个(基于
/xueapp/routes/目录文件统计)数据库表: 28张(基于
/xueapp/migrations/目录文件统计)主要功能模块: 学校信息管理、对口划片查询、排行榜计算、用户权限管理、内容管理、数据统计分析
技术创新点
- 完整的权限体系: JWT+Session双重认证,确保系统安全性
- 防刷机制设计: 基于IP和时间窗口的访问控制,保证数据真实性
- 地理信息处理: 百度地图API深度集成,支持复杂的空间计算
- AI能力集成: 阿里云大模型API调用,提升内容处理效率
项目总结
这个项目完全基于真实业务需求开发,没有过度设计,没有使用不必要的技术栈。每个功能都有具体的代码实现支撑,体现了务实的技术态度。作为一个人独立完成的企业级项目,它证明了扎实的基础技术能力比花哨的技术栈更重要。
项目代码量庞大,业务逻辑复杂,但最终选择了简单可靠技术方案,这正是企业级项目应有的态度。