Java

行业全栈项目实战与架构选型

2026-07-15 #随手记

就业平台的三角色系统、音乐网站的协同过滤推荐、钢铁工厂的中文搜索——三个不同行业的全栈项目,每个都有独特的业务挑战。再加上两组同业务双版本(单体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")); // 0/1/2

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分");

// 检查是否已评分(更新 or 新增)
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("评分成功");
}

// 获取平均评分(Redis缓存)
@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) {
// Redis缓存优先
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); // 冷启动
}

// Item-based 协同过滤
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; // 2小时

@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小时后再试");
}
}

// BCrypt密码验证
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);

// 生成Token
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
-- Flyway迁移:添加ngram全文索引
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
// HTTP请求封装
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
// 后端UniApp专用接口
@RestController
@RequestMapping("/uniapp")
public class UniappController {
@GetMapping("/getSteelPriceData")
public R getSteelPriceData() {
// 通过@LoadBalanced RestTemplate调用product-service
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: # 必须在nacos之后
rocketmq-namesrv:
rocketmq-broker:
mysql:
redis:
cgb-gateway:
cgb-user-service:
cgb-product-service:
# ... 4 more services

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

核心思想:没有最好的架构,只有最适合的架构。单体没有错——当业务复杂度增长到单体无法承受时,拆分才有价值。拆分是为了解决真实痛点,不是为了”看起来更高级”。

评论
分享