就业平台的三角色系统、音乐网站的协同过滤推荐、钢铁工厂的中文搜索——三个不同行业的全栈项目,每个都有独特的业务挑战。再加上两组同业务双版本(单体vs微服务)的架构对比,本文把行业实战和架构选型的完整图景讲透。
第一章:高校毕业生就业服务平台
1.1 三角色系统
| 角色 |
type值 |
权限 |
典型功能 |
| 管理员 |
0 |
全部 |
管理企业/学生/岗位 |
| 企业 |
1 |
管理自己的岗位 |
发布岗位、查看投递 |
| 学生 |
2 |
查看岗位、投递简历 |
搜索岗位、投递简历 |
1.2 登录流程
1 2 3 4 5 6 7 8 9 10 11 12 13
| @PostMapping("/login") public R login(@RequestBody Map<String, String> params) { String username = params.get("username"); String password = params.get("password"); Integer type = Integer.parseInt(params.get("type")); User user = userService.login(username, password, type); if (user == null) return R.error("账号或密码不正确"); String token = jwtUtils.generateToken( user.getId(), username, "job", type.toString()); return R.ok().put("token", token).put("type", type); }
|
1.3 角色数据隔离
1 2 3 4 5 6 7 8 9 10 11 12 13
| @GetMapping("/jobs/list") public R list(HttpServletRequest request) { Integer type = (Integer) request.getSession().getAttribute("type"); Long userId = (Long) request.getSession().getAttribute("userId"); if (type == 1) { return R.success(jobService.listByEnterpriseId(userId)); } else { return R.success(jobService.list()); } }
|
1.4 简历投递流程
1 2 3
| 学生搜索岗位 → 查看详情 → 投递简历(status=0待处理) → 企业查看投递 → 通过(status=1) / 拒绝(status=2) → 学生查看结果
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| @Override @Transactional public void apply(Long studentId, Long jobId, Long resumeId) { Application existing = applicationMapper.findByStudentAndJob(studentId, jobId); if (existing != null) throw new RuntimeException("已投递过该岗位"); Job job = jobMapper.selectById(jobId); if (job.getStatus() != 1) throw new RuntimeException("该岗位已停止招聘"); Application app = new Application(); app.setStudentId(studentId); app.setJobId(jobId); app.setResumeId(resumeId); app.setStatus(0); app.setApplyTime(new Date()); applicationMapper.insert(app); }
|
1.5 ECharts 数据可视化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| initGauge(id, rate) { const chart = echarts.init(document.getElementById(id)) chart.setOption({ series: [{ type: 'gauge', data: [{ value: rate, name: '就业率' }], detail: { formatter: '{value}%' } }] }) }
initBar(id, data) { const chart = echarts.init(document.getElementById(id)) chart.setOption({ series: [{ type: 'bar', data: data.map(item => ({ name: item.range, value: item.count })) }] }) }
|
1.6 智能推荐
1 2 3 4 5 6 7 8 9 10 11 12
| public List<Job> recommendByMajor(Long studentId) { Student student = studentMapper.selectById(studentId); return jobMapper.findByRequiredMajor(student.getMajor(), student.getDegree()); }
public List<Job> recommendByBehavior(Long studentId) { List<Long> viewedJobIds = viewHistoryMapper.findJobIdsByStudentId(studentId); List<Long> similarStudentIds = viewHistoryMapper.findSimilarStudents(viewedJobIds, studentId); return jobMapper.findJobsByViewers(similarStudentIds, viewedJobIds); }
|
第二章:音乐网站全栈开发
2.1 核心功能
歌曲管理、歌手管理、歌单管理、评论系统、评分系统、收藏系统、协同过滤推荐。
2.2 评分系统
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| @PostMapping("/song/rate") public R rate(@RequestBody Map<String, Object> params, HttpServletRequest request) { Long userId = getCurrentUserId(request); Long songId = Long.valueOf(params.get("songId").toString()); Integer score = Integer.valueOf(params.get("score").toString()); if (score < 1 || score > 5) return R.error("评分范围为1-5分"); Rating existing = ratingMapper.findByUserAndSong(userId, songId); if (existing != null) { existing.setScore(score); ratingMapper.updateById(existing); } else { Rating rating = new Rating(); rating.setUserId(userId); rating.setSongId(songId); rating.setScore(score); ratingMapper.insert(rating); } return R.success("评分成功"); }
@GetMapping("/song/{id}/rating") public R getRating(@PathVariable Long id) { String cacheKey = "song:rating:" + id; Double avgScore = redisTemplate.opsForValue().get(cacheKey); if (avgScore == null) { avgScore = ratingMapper.selectAvgScore(id); if (avgScore == null) avgScore = 0.0; redisTemplate.opsForValue().set(cacheKey, avgScore, 10, TimeUnit.MINUTES); } return R.success() .put("avgScore", Math.round(avgScore * 10) / 10.0) .put("totalRatings", ratingMapper.countBySong(id)); }
|
2.3 协同过滤推荐
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| @Service public class RecommendService {
public List<Long> recommend(Long userId, int topN) { String cacheKey = "recommend:user:" + userId + ":top" + topN; List<Object> cached = redisTemplate.opsForList().range(cacheKey, 0, -1); if (cached != null && !cached.isEmpty()) { return cached.stream().map(o -> Long.valueOf(o.toString())).collect(Collectors.toList()); }
List<Rating> userRatings = ratingMapper.findByUserId(userId); if (userRatings.isEmpty()) { return recommendHotSongs(topN); }
List<Long> recommendations = itemBasedRecommend(userRatings, topN);
redisTemplate.opsForList().rightPushAll(cacheKey, recommendations.stream().map(String::valueOf).collect(Collectors.toList())); redisTemplate.expire(cacheKey, 1, TimeUnit.HOURS);
return recommendations; } }
|
2.4 评论系统(树形结构)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| @GetMapping("/comment/song/{songId}") public R getComments(@PathVariable Long songId) { List<Comment> comments = commentMapper.findBySongId(songId); Map<Long, Comment> commentMap = comments.stream() .collect(Collectors.toMap(Comment::getId, c -> c)); List<Comment> tree = new ArrayList<>(); for (Comment comment : comments) { if (comment.getParentId() == null || comment.getParentId() == 0) { tree.add(comment); } else { Comment parent = commentMap.get(comment.getParentId()); if (parent != null) parent.getReplies().add(comment); } } return R.success(tree); }
|
第三章:钢铁工厂全栈展示系统
3.1 登录频率限制
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| private static final long LOGIN_RATE_LIMIT_SECONDS = 2 * 60 * 60;
@Override public Admin login(Account account, HttpServletRequest request) { String loginRateKey = "login_rate:admin:" + account.getUsername(); Object lastLoginTime = redisTemplate.opsForValue().get(loginRateKey); if (lastLoginTime != null) { long timeDiff = (System.currentTimeMillis() - Long.parseLong(lastLoginTime.toString())) / 1000; if (timeDiff < LOGIN_RATE_LIMIT_SECONDS) { throw new CustomerException("登录过于频繁,请2小时后再试"); } }
boolean isValid = passwordEncoder.matches( account.getPassword(), dbAdmin.getPassword()); if (!isValid) throw new CustomerException("账号或密码错误");
redisTemplate.opsForValue().set(loginRateKey, System.currentTimeMillis(), LOGIN_RATE_LIMIT_SECONDS, TimeUnit.SECONDS);
String clientIp = getClientIp(request); String token = TokenUtils.createToken( dbAdmin.getId() + "-ADMIN", dbAdmin.getPassword(), clientIp); dbAdmin.setToken(token); return dbAdmin; }
|
3.2 中文全文搜索
1 2 3 4
| ALTER TABLE `obtain_information` ADD FULLTEXT INDEX `ft_title_content` (`title`, `content`) WITH PARSER ngram;
|
1 2 3 4 5 6 7 8 9
| @Mapper public interface NewsMapper { @Select("SELECT * FROM obtain_information " + "WHERE MATCH(title, content) " + "AGAINST(#{keyword} IN NATURAL LANGUAGE MODE) " + "LIMIT #{offset}, #{limit}") List<News> search(@Param("keyword") String keyword, @Param("offset") int offset, @Param("limit") int limit); }
|
3.3 微服务版 + UniApp 小程序
1 2 3 4 5 6 7 8
| steel-pipe-factory-microservices/ ├── common/ # 公共模块 ├── api-gateway/ # API网关 (8080) ├── user-service/ # 用户服务 (8081, 库steel_user_db) ├── product-service/ # 产品服务 (8082, 库steel_product_db) ├── content-service/ # 内容服务 (8083, 库steel_content_db) ├── file-service/ # 文件服务 (8084) └── migrate-miniprogram/ # UniApp微信小程序
|
3.4 UniApp 小程序集成
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| class HttpRequest { request<T>(options: RequestOptions): Promise<ResponseData<T>> { return new Promise((resolve, reject) => { uni.request({ url: `${this.baseURL}${config.url}`, method: config.method || 'GET', data: config.data, header: config.header, success: (res) => { switch (res.statusCode) { case 200: resolve(res.data); break; case 401: uni.showToast({ title: '登录已过期', icon: 'none' }); break; } }, fail: (err) => reject(new Error('网络请求失败')) }); }); } }
|
1 2 3 4 5 6 7 8 9 10 11 12
| @RestController @RequestMapping("/uniapp") public class UniappController { @GetMapping("/getSteelPriceData") public R getSteelPriceData() { Object data = restTemplate.getForObject( "http://product-service/steelProductPrice/selectAll", Object.class); return R.success(data); } }
|
第四章:单体 vs 微服务架构对比
4.1 对比基础
| 业务 |
单体版 |
微服务版 |
服务数 |
| 社区团购 |
community-group-buying-system |
communitygroupbuyingsystemMicroserviceCase |
6 |
| 钢铁工厂 |
steel-factory-fullstack |
steel-pipe-factory-microservices |
4 |
4.2 代码量对比
| 维度 |
单体版 |
微服务版 |
变化 |
| Controller数 |
15 |
~18 |
+20% |
| 数据库数 |
1 |
5 |
+400% |
| 配置文件 |
1个yml |
Nacos + 每服务1个yml |
+500% |
| 中间件 |
MySQL + Redis |
+Nacos + Seata + RocketMQ |
+3个 |
4.3 创建订单对比
单体版(4行代码,一个事务):
1 2 3 4 5 6
| @Transactional(rollbackFor = Exception.class) public void createOrder(OrdersEntity entity) { productDao.decreaseStock(entity.getProductId(), entity.getQuantity()); ordersDao.insert(entity); yonghuDao.addPoints(entity.getUserId(), entity.getTotalPrice().doubleValue()); }
|
微服务版(涉及Feign+Seata+RocketMQ三个组件):
1 2 3 4 5 6 7 8
| @GlobalTransactional(name = "cgb-create-order", rollbackFor = Exception.class) public void createOrder(OrdersEntity entity) { var stockResult = feignProductService.decreaseStock( entity.getProductId(), entity.getQuantity()); if (stockResult.getCode() != 0) throw new EIException("库存扣减失败"); ordersDao.insert(entity); sendOrderStatusMessage(entity, MQTopics.TAG_ORDER_CREATED); }
|
4.4 部署复杂度
单体版:
1 2
| mvn package java -jar app.jar
|
微服务版:
1 2 3 4 5 6 7 8 9 10 11
| services: nacos: seata-server: rocketmq-namesrv: rocketmq-broker: mysql: redis: cgb-gateway: cgb-user-service: cgb-product-service:
|
4.5 何时该拆?
| 信号 |
说明 |
社区团购满足? |
钢铁工厂满足? |
| 团队 > 5人 |
需要独立 ownership |
❌ |
❌ |
| QPS > 1000 |
需要独立扩展 |
❌ |
❌ |
| 独立部署需求 |
改A不影响B |
❌ |
✅(小程序需求) |
| 数据库瓶颈 |
单库连接池不够 |
❌ |
❌ |
社区团购不需要微服务——毕设场景,单体完全够用。钢铁工厂微服务有合理性——小程序+管理端需要独立路由和安全策略。
4.6 资源消耗对比
| 资源 |
单体版 |
微服务版 |
| JVM进程 |
1 |
6+ |
| 内存 |
~512MB |
~2GB+ |
| 数据库连接 |
1个池(20连接) |
5个池(100连接) |
| Docker容器 |
3 |
10+ |
第五章:架构选型决策框架
5.1 三种架构对比
| 维度 |
单体 |
模块化单体 |
微服务 |
| 开发效率 |
高 |
高 |
中 |
| 运维复杂度 |
低 |
低 |
高 |
| 可扩展性 |
低 |
中 |
高 |
| 故障隔离 |
差 |
差 |
好 |
| 团队协作 |
差 |
中 |
好 |
| 适合规模 |
小型 |
中型 |
大型 |
5.2 选型决策树
1 2 3 4 5 6 7
| 团队 < 5人? ├── 是 → QPS < 1000? │ ├── 是 → 单体(不需要微服务) │ └── 否 → 模块化单体 + 按需拆分热点模块 └── 否 → 多团队协作? ├── 是 → 微服务(按业务域拆分) └── 否 → 模块化单体
|
5.3 行业项目的技术选型
| 项目 |
架构 |
选型理由 |
| 就业平台 |
单体 |
三角色系统,QPS低,单体够用 |
| 音乐网站 |
单体 |
141个测试,协同过滤推荐,单体够用 |
| 钢铁工厂单体版 |
单体 |
展示系统,QPS低 |
| 钢铁工厂微服务版 |
微服务 |
小程序+管理端需要独立路由 |
| 社区团购单体版 |
单体 |
毕设项目,单体够用 |
| 社区团购微服务版 |
微服务 |
教学演示,展示微服务架构 |
总结
| 项目 |
核心挑战 |
技术方案 |
| 就业平台 |
三角色权限+数据隔离 |
type字段区分 + 查询条件过滤 |
| 音乐网站 |
推荐算法+评分系统 |
Item-based协同过滤 + Redis缓存 |
| 钢铁工厂 |
中文搜索+登录频率限制 |
ngram全文索引 + Redis登录时间 |
| 钢铁工厂微服务 |
小程序+管理端独立 |
4微服务+3独立DB+UniApp |
核心思想:没有最好的架构,只有最适合的架构。单体没有错——当业务复杂度增长到单体无法承受时,拆分才有价值。拆分是为了解决真实痛点,不是为了”看起来更高级”。