项目缘起

这是我在国庆期间完成的第二个Vibe项目,前后端代码接近10万行,数据库28张表,全部由我一个人独立完成。

项目起源于一个非常私人的需求:大女儿青云明年就要上小学了,我在查学校、学区、政策时遇到了很多障碍。作为一个技术人,实在无法忍受这种信息不对称和查询困难,于是就有了这个项目。

开发过程很有意思:首先花了几天时间跟GPT反复讨论需求,确定产品方向;然后通过各种AI工具采集整理汇总学校的各种信息,包括对口划片、中/高考成绩、分配生政策等;最后实现了资讯、问答、用户及权限等模块,甚至还做了个排行榜。(BTW:LOGO是用GPT生成)。

老规矩,先上几张图: aixue01

aixue02

aixue03

aixue04

aixue05

aixue06

aixue07

aixue08

项目概述

这是一个面向教育行业的综合信息平台,提供学校信息查询、对口划片、升学政策、排行榜计算等功能。项目采用前后端分离架构,由我一个人独立完成全部开发工作,包括前端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.jsxueapp/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.jsxueapp/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.jsxueapp/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.jsxueapp/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/目录文件统计)

  • 主要功能模块: 学校信息管理、对口划片查询、排行榜计算、用户权限管理、内容管理、数据统计分析

技术创新点

  1. 完整的权限体系: JWT+Session双重认证,确保系统安全性
  2. 防刷机制设计: 基于IP和时间窗口的访问控制,保证数据真实性
  3. 地理信息处理: 百度地图API深度集成,支持复杂的空间计算
  4. AI能力集成: 阿里云大模型API调用,提升内容处理效率

项目总结

这个项目完全基于真实业务需求开发,没有过度设计,没有使用不必要的技术栈。每个功能都有具体的代码实现支撑,体现了务实的技术态度。作为一个人独立完成的企业级项目,它证明了扎实的基础技术能力比花哨的技术栈更重要。

项目代码量庞大,业务逻辑复杂,但最终选择了简单可靠技术方案,这正是企业级项目应有的态度。