09:00 早站会 (Daily Standup)
小张打开电脑,钉钉弹出群消息提醒。10点准时站会,每个人轮流说三件事:昨天做了什么、今天做什么、有没有卡住的地方。
—— 2026-03-18 09:03 ——
张
小张(后端)
昨天:完成了订单导出接口,优化了慢查询(从3s降到200ms)
今天:做商品评价功能的接口开发
风险:无
李
小李(前端)
昨天:商品详情页改版,拆了3个组件
今天:对接评价展示模块,需要后端先出接口文档
风险:评价列表的分页逻辑需要确认
王
王姐(QA)
昨天:回归测试v2.3,发现2个P3 bug已提单
今天:准备评价功能的测试用例
风险:无
陈
老陈(TL)
评价功能本周要上线,大家注意联调时间。小张今天先把接口文档给到前端,下午争取联调。
另外 v2.3.1 hotfix 今天下午发生产,王姐盯一下。
—— 09:12 站会结束 ——
场景说明每天站会大概 10-15 分钟,每个人说三件事:昨天做了什么、今天做什么、有没有卡住的地方。站会不是汇报工作,而是同步信息、发现阻塞。如果有人遇到问题,会后单独讨论,不在站会上展开。
09:20 收到需求 — JIRA 单子来了
站会结束,小张打开 JIRA,看到新分配过来的需求单。先仔细读需求,搞清楚要做什么。
Assignee: 小张
Sprint: Sprint 23
Story Points: 5
In Progress
Description
用户可以在订单完成后对商品进行评价,支持评分(1-5星)和文字评价(最多500字)。
功能清单
- 新增评价表
t_review
- 提交评价接口
POST /api/v1/reviews
- 查询商品评价列表接口
GET /api/v1/products/{id}/reviews
- 评价统计接口
GET /api/v1/products/{id}/review-stats
Acceptance Criteria
- 用户只能评价已完成的订单
- 每个商品每个用户只能评价一次
- 评分必填(1-5),文字选填(最多500字)
- 评价列表支持分页(默认每页20条)
Created: 2026-03-17 14:30
Reporter: 产品组-小赵
Labels: order-module, user-feature
收到需求后不要马上写代码!先想清楚三件事:① 表怎么设计(有哪些字段、需要什么索引)、② 接口参数和返回值是什么(先和前端确认)、③ 有哪些边界条件(权限、重复提交、数据校验)。磨刀不误砍柴工,想清楚再动手能省很多返工。
小张的思考
看完需求,先在脑子里过一遍:
需要一张评价表,关联用户、订单、商品
提交评价要校验:订单是否完成、是否重复评价
评价列表要分页,还需要关联用户信息(昵称、头像)
统计接口要算平均分和总评价数
好,先去建表。
09:40 设计表结构 — 打开 DataGrip
小张打开 DataGrip,连接到开发数据库。先看看现有的表结构,再设计新的评价表。
建完表后习惯用 EXPLAIN 验证索引是否生效,确保未来查询能走索引而不是全表扫描。特别是在数据量大的表上,这一步不能省。
设计表的时候几个关键决策:
① user_id + order_id 联合索引 — 用来校验"是否已评价"的查询
② product_id 单独索引 — 用来查询商品评价列表
③ status 字段 — 方便管理员隐藏不当评价,不需要物理删除
④ images 用 VARCHAR 存多个 URL — 简单方案,JSON 类型也可以,但 VARCHAR 够用了
10:30 写代码 — Controller 层
表建好了,打开 IntelliJ IDEA 开始写代码。小张习惯从 Controller 开始写,先定义接口的"入口",再实现具体逻辑。
ReviewController.java — myshop-backend
Java 17 · Spring Boot 3.2
ReviewController.java
application.yml
pom.xml
📁 myshop-backend
📁 src/main/java
📁 com.myshop
📁 controller
📄 ReviewController.java
📄 OrderController.java
📁 service
📁 mapper
📁 entity
📁 dto
📁 config
1package com.myshop.controller;
2
3import com.myshop.dto.ReviewDTO;
4import com.myshop.entity.vo.ReviewVO;
5import com.myshop.entity.vo.PageResult;
6import com.myshop.service.ReviewService;
7import com.myshop.common.Result;
8import org.springframework.beans.factory.annotation.Autowired;
9import org.springframework.web.bind.annotation.*;
10
11@RestController
12@RequestMapping("/api/v1/reviews")
13public class ReviewController {
14
15 @Autowired
16 private ReviewService reviewService;
17
18 @PostMapping ●
19 public Result<Long> submitReview(@RequestBody ReviewDTO dto) {
20 Long reviewId = reviewService.submitReview(dto);
21 return Result.success(reviewId);
22 }
23
24 @GetMapping("/products/{productId}")
25 public Result<PageResult<ReviewVO>> getProductReviews(
26 @PathVariable Long productId,
27 @RequestParam(defaultValue = "1") Integer page,
28 @RequestParam(defaultValue = "20") Integer size) {
29 return Result.success(reviewService.getProductReviews(productId, page, size));
30 }
31
32 @GetMapping("/products/{productId}/stats")
33 public Result<ReviewStatsVO> getReviewStats(@PathVariable Long productId) {
34 return Result.success(reviewService.getReviewStats(productId));
35 }
36}
UTF-8
LF
Java
Ln 18, Col 5
Controller 的角色
Controller 是"门口保安",它不处理业务逻辑,只负责三件事:
@RestController — 告诉 Spring 这是一个 REST 接口类
@PostMapping / @GetMapping — 定义 URL 和 HTTP 方法
@RequestBody / @PathVariable / @RequestParam — 接收参数的方式
真正的业务逻辑在 Service 层。
三个接口一览
| HTTP 方法 | URL | 功能 | 参数 | 返回值 |
| POST |
/api/v1/reviews |
提交评价 |
ReviewDTO (JSON Body) |
评价ID |
| GET |
/api/v1/reviews/products/{id} |
查询评价列表 |
productId(路径) + page/size(查询) |
分页评价列表 |
| GET |
/api/v1/reviews/products/{id}/stats |
评价统计 |
productId(路径) |
平均分 + 总数 |
11:00 写代码 — Service 层(业务逻辑)
Controller 定义好了接口入口,现在写 Service — 这里是真正处理业务逻辑的地方。小张习惯:先写校验,再写主流程。
ReviewServiceImpl.java — myshop-backend
ReviewController.java
ReviewServiceImpl.java
ReviewMapper.java
📁 myshop-backend
📁 src/main/java
📁 com.myshop
📁 controller
📁 service
📄 ReviewService.java
📄 ReviewServiceImpl.java
📁 mapper
📁 entity
1package com.myshop.service.impl;
2
3import com.myshop.dto.ReviewDTO;
4import com.myshop.entity.*;
5import com.myshop.entity.vo.*;
6import com.myshop.mapper.ReviewMapper;
7import com.myshop.mapper.OrderMapper;
8import com.myshop.common.BusinessException;
9import com.myshop.enums.OrderStatus;
10import org.springframework.stereotype.Service;
11import org.springframework.transaction.annotation.Transactional;
12import org.springframework.beans.BeanUtils;
13
14@Service
15public class ReviewServiceImpl implements ReviewService {
16
17 @Autowired
18 private ReviewMapper reviewMapper;
19 @Autowired
20 private OrderMapper orderMapper;
21
22 @Override
23 @Transactional
24 public Long submitReview(ReviewDTO dto) {
25
26 // 1. 校验订单是否已完成
27 Order order = orderMapper.selectById(dto.getOrderId());
28 if (order == null || order.getStatus() != OrderStatus.COMPLETED) {
29 throw new BusinessException("订单未完成,无法评价");
30 }
31
32 // 2. 校验是否已评价(同一用户同一商品只能评价一次)
33 int count = reviewMapper.countByUserAndProduct(dto.getUserId(), dto.getProductId());
34 if (count > 0) {
35 throw new BusinessException("您已评价过该商品");
36 }
37
38 // 3. 保存评价
39 Review review = new Review();
40 BeanUtils.copyProperties(dto, review);
41 reviewMapper.insert(review);
42 return review.getId();
43 }
44
45 @Override
46 public PageResult<ReviewVO> getProductReviews(Long productId, Integer page, Integer size) {
47 int offset = (page - 1) * size;
48 List<ReviewVO> list = reviewMapper.getProductReviews(productId, offset, size);
49 int total = reviewMapper.countByProductId(productId);
50 return new PageResult<>(list, total, page, size);
51 }
52}
UTF-8
LF
Java
Ln 43, Col 5
写 Service 的习惯:先想好有哪些校验,再写业务逻辑。先把异常情况处理完(订单不存在、订单未完成、重复评价),主流程就清晰了。这样写出来的代码读起来像在讲故事:先检查前提条件,再执行核心操作。
关键注解解释
@Service — 标记为 Spring 管理的 Service 组件
@Transactional — 开启数据库事务。如果方法中途报错,已执行的 SQL 会自动回滚
@Autowired — 自动注入依赖的 Mapper(操作数据库的对象)
submitReview 方法的执行流程
1
收到请求参数 — DTO 包含 userId, orderId, productId, rating, content
前端传过来的 JSON 数据被 Spring 自动解析成 Java 对象
2
校验订单状态 — 查数据库,确认订单存在且状态为 COMPLETED
如果订单不存在或未完成 → 抛异常 "订单未完成,无法评价"
3
校验是否重复评价 — 查数据库,同一用户对同一商品只能评价一次
如果已存在 → 抛异常 "您已评价过该商品"
4
保存评价数据 — 把 DTO 的字段复制到 Entity,insert 到数据库
BeanUtils.copyProperties 自动把同名字段从 DTO 复制到 Entity
5
返回评价 ID — 新插入的记录自增 ID 返回给前端
前端可以用这个 ID 跳转到评价详情页
11:40 写 Mapper — SQL 层
Service 调用的数据库操作在 Mapper 里定义。小张用 MyBatis,SQL 写在 XML 文件中。这个查询需要关联用户表来获取评价者的昵称和头像。
ReviewMapper.xml — myshop-backend
ReviewController.java
ReviewServiceImpl.java
ReviewMapper.xml
📁 resources
📄 application.yml
📁 mapper
📄 OrderMapper.xml
📄 ReviewMapper.xml
1<?xml version="1.0" encoding="UTF-8"?>
2<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
3 "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
4<mapper namespace="com.myshop.mapper.ReviewMapper">
5
6 <!-- 查询商品评价列表(关联用户信息) -->
7 <select id="getProductReviews"
8 resultType="com.myshop.entity.vo.ReviewVO">
9 SELECT r.id, r.rating, r.content, r.create_time,
10 u.nickname, u.avatar
11 FROM t_review r
12 LEFT JOIN t_user u ON r.user_id = u.id
13 WHERE r.product_id = #{productId}
14 AND r.status = 1
15 ORDER BY r.create_time DESC
16 LIMIT #{offset}, #{size}
17 </select>
18
19 <!-- 统计商品评价数据 -->
20 <select id="getReviewStats"
21 resultType="com.myshop.entity.vo.ReviewStatsVO">
22 SELECT COUNT(*) AS total_count,
23 ROUND(AVG(rating), 1) AS avg_rating
24 FROM t_review
25 WHERE product_id = #{productId} AND status = 1
26 </select>
27
28</mapper>
UTF-8
LF
XML
Ln 16, Col 1
SQL 小细节
这个查询用了 LEFT JOIN t_user 来关联用户表,这样评价列表就能直接返回用户的昵称和头像,前端不用再发额外请求。LIMIT #{offset}, #{size} 实现分页,MyBatis 会把 Java 参数安全地填入 SQL 中(防止 SQL 注入)。
上午的代码写完了!现在去吃午饭。下午回来先自测接口,确保功能正常。
13:30 自测接口 — Postman 走起
午休回来,启动本地项目,用 Postman 测试刚写的接口。先测"提交评价"接口。
Body → raw → JSON
{
"userId": 10086,
"orderId": 20260317001,
"productId": 5001,
"rating": 5,
"content": "质量非常好!做工精细,性价比很高,下次还买!"
}
Response
200 OK 62ms
{
"code": 200,
"message": "success",
"data": 1
}
提交评价接口通了!返回 data: 1 表示新建的评价 ID 为 1。接下来测试"查询评价列表"接口。
Response
200 OK 45ms
{
"code": 200,
"message": "success",
"data": {
"list": [
{
"id": 1,
"rating": 5,
"content": "质量非常好!做工精细,性价比很高,下次还买!",
"createTime": 1710745133000,
"nickname": "技术爱好者",
"avatar": "https://cdn.myshop.com/avatar/10086.jpg"
}
],
"total": 1,
"page": 1,
"size": 10
}
}
自测要点
两个接口都返回 200,数据格式也正确。但注意看 createTime 返回的是时间戳 1710745133000,这个格式前端处理不太方便,后面联调时前端可能会提出来。先记着,继续测其他场景。
自测 checklist — 对照 JIRA 验收标准
| # | 验收标准 | 测试方法 | 结果 |
| 1 |
用户只能评价已完成的订单 |
用一个未完成订单的 orderId 提交评价 |
✓ 返回 "订单未完成,无法评价" |
| 2 |
每个商品每个用户只能评价一次 |
同一用户对同一商品提交第二次评价 |
✓ 返回 "您已评价过该商品" |
| 3 |
评分必填(1-5) |
不传 rating 字段 |
✓ Spring 参数校验拦截 |
| 4 |
评价列表支持分页 |
传 page=1&size=10 查询 |
✓ 返回分页结构 {list, total, page, size} |
14:00 Debug — 接口报错了!
小张想测试一下"重复评价"的校验逻辑,故意用同样的数据再发一次请求。但这次换了一个订单ID来测……结果报 500 了。
Body → raw → JSON
{
"userId": 10086,
"orderId": 20260318001,
"productId": 5002,
"rating": 4,
"content": "还不错,配送很快"
}
Response
500 Internal Server Error 23ms
{
"code": 500,
"message": "NullPointerException",
"data": null
}
报错了!500 是服务端错误。NullPointerException(空指针异常)是最常见的 Java 报错之一。不要慌,打开 IDEA 的 Debug 模式,打个断点看看是哪里出了问题。
🔍 Debug — ReviewServiceImpl.java
⏸ Paused at breakpoint
ReviewServiceImpl.java
Debug (attached)
24 public Long submitReview(ReviewDTO dto) {
25
26 // 1. 校验订单是否已完成
27 Order order = orderMapper.selectById(dto.getOrderId()); ● ← NPE here!
28 if (order == null || order.getStatus() != OrderStatus.COMPLETED) {
29 throw new BusinessException("订单未完成,无法评价");
Variables
dto = ReviewDTO{userId=10086, orderId=20260318001, productId=5002, rating=4, content="还不错..."}
order = null ← THIS IS THE PROBLEM
▶ Resume
⏭ Step Over
⬇ Step Into
Thread: http-nio-8080-exec-3
发现了!order 查出来是 null,说明数据库里没有 id = 20260318001 这个订单。为什么?去数据库看看这个订单是否存在。
找到原因了!
Postman 里传的 orderId: 20260318001 根本不存在,测试数据用的是 20260317001。这不是代码的 bug,是测试数据的问题。改一下 Postman 里的 orderId 重新发请求。
Body → raw → JSON (orderId 已修正)
{
"userId": 10086,
"orderId": 20260317001, ← 修正了
"productId": 5002,
"rating": 4,
"content": "还不错,配送很快"
}
Response
200 OK 38ms
{
"code": 200,
"message": "success",
"data": 2
}
✓ 自测通过! 三个场景都测过了:正常提交评价 → 200 ✓,重复评价 → 业务异常提示 ✓,订单不存在 → 业务异常提示 ✓(不过空指针的问题说明代码的防御性编程还不够好,Code Review 的时候 TL 应该会提到)。
Debug 心得 — 新手常见错误
1
看到 500 不要慌 — 先看错误类型。NullPointerException 是最常见的,意思是代码中有个对象是 null,你却去调用它的方法了。
2
找到报错行号 — 日志里会写着哪个文件第几行。小张这次的 NPE 在 ReviewServiceImpl 第 27 行。
3
打断点看变量 — 在 IDEA 里点行号旁边打个红点(断点),重新发请求,程序会暂停在那一行。然后看每个变量的值。
4
顺藤摸瓜 — 发现 order 是 null,就去数据库查为什么。最终定位到是测试数据写错了 ID。Debug 就像侦探破案:跟着线索走。
15:00 提交代码 — Git 工作流
代码写完、自测通过,现在提交到 Git。团队使用 Git Flow 工作流,新功能从 develop 分支拉出 feature 分支。
~/myshop-backend $ git checkout develop && git pull origin develop
Already on 'develop'.
From github.com:myteam/myshop-backend
* [new ref] refs/remotes/origin/develop -> origin/develop
Already up to date.
~/myshop-backend $ git checkout -b feature/review
Switched to a new branch 'feature/review'
~/myshop-backend $ git add .
~/myshop-backend $ git status
On branch feature/review
Changes to be committed:
new file: src/main/java/com/myshop/controller/ReviewController.java
new file: src/main/java/com/myshop/service/ReviewService.java
new file: src/main/java/com/myshop/service/impl/ReviewServiceImpl.java
new file: src/main/java/com/myshop/mapper/ReviewMapper.java
new file: src/main/resources/mapper/ReviewMapper.xml
new file: src/main/java/com/myshop/dto/ReviewDTO.java
new file: src/main/java/com/myshop/entity/vo/ReviewVO.java
new file: src/main/java/com/myshop/entity/vo/ReviewStatsVO.java
new file: src/main/java/com/myshop/entity/Review.java
~/myshop-backend $ git commit -m "feat: 商品评价功能 - 新增评价表/提交评价接口/评价列表查询接口/评价统计接口"
[feature/review 7a3f2b1] feat: 商品评价功能 - 新增评价表/提交评价接口/评价列表查询接口/评价统计接口
10 files changed, 287 insertions(+)
create mode 100644 src/main/java/com/myshop/controller/ReviewController.java
create mode 100644 ...
~/myshop-backend $ git push origin feature/review
Enumerating objects: 21, done.
Counting objects: 100% (21/21), done.
To github.com:myteam/myshop-backend.git
* [new branch] feature/review -> feature/review
Branch: feature/review → develop
+287 lines
Reviewer: 老陈(TL)
Changed Files:
📄 ReviewController.java
+36
📄 ReviewServiceImpl.java
+52
📄 ReviewMapper.xml
+28
📄 ReviewDTO.java
+18
📄 ReviewVO.java
+15
📄 ReviewStatsVO.java
+12
📄 Review.java
+20
📄 ReviewService.java
+8
📄 ReviewMapper.java
+10
📄 sql/t_review.sql
+16
Git 工作流
从 develop 拉出 feature 分支 → 开发 → 自测通过 → push → 创建 PR
Commit message 用约定格式:feat: 新功能、fix: 修复、refactor: 重构
PR 需要 TL Code Review 通过后才能合并到 develop
15:30 Code Review — 老陈审代码
PR 提上去半小时,老陈在 GitLab 上审完了代码,在群里 @ 小张反馈意见。
—— 2026-03-18 15:32 ——
陈
老陈(TL)
看了你的代码,整体写得不错,几个地方改一下:
1. ReviewController 第 18 行
@PostMapping 需要加权限校验,用户只能评价自己的订单,不能让别人帮你提交评价。
2. ReviewServiceImpl 第 33 行
countByUserAndProduct 查询在高并发场景下有风险。两个请求同时到达、都查出 count=0、就都插入成功了。需要加分布式锁。
3. ReviewServiceImpl 第 27 行
order 查出来之后先判断 null 再调 getStatus(),否则像今天这样传不存在的 orderId 就 NPE 了。把 null 判断放前面。
张
小张(后端)
好的,我改一下。1 加 @PreAuthorize 注解,2 加 Redis 分布式锁,3 调整 null 判断顺序。
陈
老陈(TL)
👍 改完 push 上来我再看一眼,没问题就合了。
小张开始改代码。以下是修改的 diff 视图:
修改 1:Controller 加权限校验
diff --git a/ReviewController.java b/ReviewController.java
@@ -15,6 +15,7 @@
private ReviewService reviewService;
+ @PostMapping
+ @PreAuthorize("hasRole('USER')")
public Result<Long> submitReview(@RequestBody ReviewDTO dto) {
Long reviewId = reviewService.submitReview(dto);
return Result.success(reviewId);
修改 2:Service 加 Redis 分布式锁防止重复提交
diff --git a/ReviewServiceImpl.java b/ReviewServiceImpl.java
@@ -22,6 +22,14 @@
public Long submitReview(ReviewDTO dto) {
+ // 添加 Redis 分布式锁防止重复提交
+ String lockKey = "review:lock:" + dto.getUserId() + ":" + dto.getProductId();
+ if (!redisTemplate.opsForValue().setIfAbsent(
+ lockKey, "1", 10, TimeUnit.SECONDS)) {
+ throw new BusinessException("请勿重复提交");
+ }
+
// 1. 校验订单是否已完成
Order order = orderMapper.selectById(dto.getOrderId());
if (order == null || ...) {
修改 3:调整 null 判断顺序(null 放前面,避免 NPE)
diff --git a/ReviewServiceImpl.java b/ReviewServiceImpl.java
@@ -27,2 +27,4 @@
Order order = orderMapper.selectById(dto.getOrderId());
- if (order == null || order.getStatus() != OrderStatus.COMPLETED) {
+ if (order == null) {
+ throw new BusinessException("订单不存在");
+ }
+ if (order.getStatus() != OrderStatus.COMPLETED) {
throw new BusinessException("订单未完成,无法评价");
Code Review 的价值
自己写代码容易"当局者迷",TL 审过之后能发现你没想到的边界情况:
权限校验 — 安全问题,不加的话任何人都能替别人提交评价
分布式锁 — 并发问题,高流量下可能出现重复数据
null 判断 — 防御性编程,先判断 null 再访问属性
这就是为什么团队要有 Code Review 制度。
16:00 部署测试环境 — SSH 到服务器
Code Review 改完,老陈通过了 PR,代码合并到 develop。现在需要部署到测试环境,让 QA 和前端也能访问。
~/myshop-backend $ ssh dev@192.168.1.100
Welcome to Ubuntu 22.04.3 LTS (GNU/Linux 5.15.0-91-generic x86_64)
* Documentation: https://help.ubuntu.com
* Management: https://landscape.canonical.com
Last login: Mon Mar 17 16:45:22 2026 from 10.0.2.15
dev@ubuntu:~$ cd /opt/myshop && git pull origin develop
remote: Enumerating objects: 7, done.
remote: Counting objects: 100% (7/7), done.
remote: Compressing objects: 100% (4/4), done.
Unpacking objects: 100% (4/4), done.
From github.com:myteam/myshop-backend
* branch develop -> FETCH_HEAD
a1b2c3d..e4f5g6h develop -> origin/develop
Updating a1b2c3d..e4f5g6h
Fast-forward
.../controller/ReviewController.java | 2 +
.../service/impl/ReviewServiceImpl.java | 14 +++--
10 files changed, 287 insertions(+)
create mode 100644 ...
dev@ubuntu:/opt/myshop$ mvn clean package -DskipTests
[INFO] Scanning for projects...
[INFO]
[INFO] ----------< com.myshop:myshop-backend >----------
[INFO] Building myshop-backend 1.0.0-SNAPSHOT
[INFO] --------------------------------[jar]---------
[INFO]
[INFO] --- maven-clean-plugin:3.2.0:clean ---
[INFO] --- maven-resources-plugin:3.3.0:resources ---
[INFO] --- maven-compiler-plugin:3.11.0:compile ---
[INFO] --- maven-package-plugin:3.3.0:package ---
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 12.847 s
[INFO] Finished at: 2026-03-18T16:08:43+08:00
dev@ubuntu:/opt/myshop$ docker-compose build app
Building app
Step 1/8 : FROM openjdk:17-jdk-slim
---> 73834b5238dc
Step 2/8 : WORKDIR /app
---> Using cache
Step 3/8 : COPY target/*.jar app.jar
Successfully built 8f3a2b1c4d5e
Successfully tagged myshop-app:latest
dev@ubuntu:/opt/myshop$ docker-compose up -d app
Recreating myshop-app ... done
dev@ubuntu:/opt/myshop$ docker-compose logs -f app --tail=50
2026-03-18 16:12:31 [main] Starting Application v1.0.0
2026-03-18 16:12:31 [main] No active profiles set, falling back to default profiles: default
2026-03-18 16:12:32 [main] Bean 'reviewServiceImpl' of type [com.myshop.service.impl.ReviewServiceImpl] registered
2026-03-18 16:12:32 [main] Bean 'reviewController' of type [com.myshop.controller.ReviewController] registered
2026-03-18 16:12:33 [main] Started Application in 8.2 seconds (process running for 9.1)
2026-03-18 16:12:33 [main] JVM running for 10.3
2026-03-18 16:12:33 [main] ✓ Application is ready!
部署完记得看启动日志
确认几件事:
有没有报错(红色的 Exception)
新的 Bean 是否注册成功(reviewServiceImpl, reviewController)
启动时间是否正常(8.2秒,没有卡在数据库连接上)
全部 OK,通知前端可以联调了。
16:30 前端联调 — 接口对齐
部署完成,通知前端小李开始联调。联调就是前后端一起跑通完整的业务流程。
—— 16:35 ——
张
小张(后端)
评价接口部署到测试环境了,你联调下。接口文档在 Swagger 上:http://192.168.1.100:8080/swagger-ui.html
—— 5 分钟后 ——
李
小李(前端)
接口返回的数据格式和文档不太一样。reviewVO 里的 createTime 是时间戳还是字符串?我这边解析出来是 1710745133000,前端显示需要格式化。
张
小张(后端)
是时间戳(long 类型)。要不我改成格式化的字符串吧?"2026-03-18 13:25:33" 这样,前端直接展示就行。
张
小张(后端)
改好了,你重新试下。commit 已经 push 到 feature 分支了。
小李在浏览器 DevTools 里看到的网络请求:
🌐 Chrome DevTools
Network
Console
Request
GET http://192.168.1.100:8080/api/v1/reviews/products/5001?page=1&size=10
Status: 200 OK Time: 52ms
Response Body:
{
"code":
200,
"data": {
"list": [{
"id":
1,
"rating":
5,
"content":
"质量非常好!做工精细,性价比很高,下次还买!",
"createTime":
"2026-03-18 13:25:33" ← 格式化了 ✓
}]
}
}
小张改的代码(ReviewVO.java):
diff --git a/ReviewVO.java b/ReviewVO.java
@@ -8,7 +8,8 @@
private String nickname;
private String avatar;
- private Long createTime;
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ private String createTime;
}
联调的本质
联调是前后端一起验证接口是否真的能用。文档写得再详细,实际对接时总会发现一些"双方理解不一致"的地方。createTime 的格式就是一个典型的例子 — 后端觉得返回时间戳很正常,前端希望直接拿到可展示的字符串。这就是为什么要尽早联调。
联调常见问题 Top 5
| # | 问题 | 今天是否遇到 | 解决方式 |
| 1 |
字段类型不一致(时间戳 vs 字符串) |
✓ 遇到了 |
后端加 @JsonFormat 注解格式化 |
| 2 |
字段命名风格不一致(驼峰 vs 下划线) |
— |
Spring 默认配置通常能自动处理 |
| 3 |
空值处理(null vs 空字符串 vs 不返回字段) |
— |
在 VO 中使用 @JsonInclude 注解控制 |
| 4 |
分页参数理解不一致(page 从 0 还是 1 开始) |
— |
在接口文档中明确标注 |
| 5 |
HTTP 状态码 vs 业务错误码混淆 |
— |
统一用 code 字段表示业务状态,HTTP 状态码保持 200 |
17:30 下班前收尾
联调完成,提交最后一版代码,更新 JIRA 状态,准备下班。
~/myshop-backend $ git add . && git commit -m "fix: 评价列表返回格式化时间 + Code Review 修改(权限校验/分布式锁/null判断)"
[feature/review 9c1d2e3] fix: 评价列表返回格式化时间 + Code Review 修改
3 files changed, 25 insertions(+), 3 deletions(-)
~/myshop-backend $ git push origin feature/review
To github.com:myteam/myshop-backend.git
7a3f2b1..9c1d2e3 feature/review -> feature/review
下班前的习惯
提交所有未提交的代码(别留在本地过夜)
更新 JIRA 状态(让 QA 知道可以开始测了)
看一眼明天的站会要说什么(今天做了评价功能,明天要处理 QA 反馈的 bug)
今天的工作总结
| 时间段 | 工作内容 | 耗时 |
| 09:00 早站会 |
同步昨天进度,确认今天的任务 |
15 min |
| 09:20 看需求 |
读 JIRA 单子,理清思路,确认表设计和接口参数 |
20 min |
| 09:40 建表 |
设计 t_review 表,写 SQL,验证索引 |
50 min |
| 10:30 写代码 |
Controller + Service + Mapper,三层写完 |
2 h |
| 13:30 自测 |
Postman 测试接口,发现 500 错误,Debug 修复 |
1 h |
| 15:00 提交代码 |
Git commit + push + 创建 PR |
10 min |
| 15:30 Code Review |
TL 审代码 → 提了 3 个意见 → 改完重新提交 |
30 min |
| 16:00 部署 |
SSH 到服务器,git pull + Maven 打包 + Docker 部署 |
20 min |
| 16:30 前端联调 |
和小李对接接口,修复时间格式问题 |
1 h |
回顾今天:真正写代码的时间大概只有 2 小时(10:30-12:00),其他时间都在需求分析、设计、自测、Debug、Code Review 改意见、部署、联调。
这就是后端开发的常态 — 写代码只是工作的一部分,甚至不是最大的一部分。沟通、设计、测试、修 bug、部署、联调,这些占据了大部分时间。但每一环都很重要,漏掉任何一环都可能出线上事故。
后端工作观察笔记
通过跟随小张的一天,我们可以总结出后端开发的几个关键观察:
工作节奏
需求分析 → 建表设计 → 写代码 → 自测 → Code Review → 部署 → 联调
每个环节都有其价值,跳过任何一步都可能埋下隐患。
沟通占比很大
和 PM 确认需求细节(这个字段是什么意思?边界条件是什么?)
和前端对接口(参数格式、返回值结构、时间格式)
和 TL 过代码(权限、并发、防御性编程)
和 QA 协调测试(部署环境、测试数据)
Debug 是常态
代码不是写完就能跑的。今天遇到一个 NullPointerException(空指针异常),原因不是代码逻辑错误,而是测试数据传错了订单 ID。
Debug 的套路:看报错信息 → 定位行号 → 打断点 → 看变量值 → 回溯数据来源。
工具使用
| 工具 | 用途 | 今天用到的场景 |
| IntelliJ IDEA | 写 Java 代码、Debug | 写 Controller / Service / Mapper,打断点查 NPE |
| DataGrip | 数据库管理、写 SQL | 设计表结构、验证索引、查测试数据 |
| Postman | 测试 HTTP 接口 | POST 提交评价、GET 查询列表、验证 500 错误 |
| Git | 版本管理 | feature 分支、commit、push、PR |
| Terminal (SSH) | 部署到服务器 | Maven 打包、Docker 部署、看日志 |
| 钉钉 | 团队沟通 | 站会、通知部署完成、联调沟通 |
代码评审很重要
TL(老陈)在 Code Review 中发现了三个问题:
① 安全问题 — 没加权限校验,任何人都能替别人提交评价
② 并发问题 — 高流量下可能出现重复提交,需要分布式锁
③ 防御性编程 — null 判断应该放在前面,避免 NPE
这些都是小张一个人写代码时没想到的。Code Review 是团队保障代码质量的关键环节。
后端开发的核心能力
不是会写 Java 语法就是后端开发者。真正需要的能力:
数据库设计 — 知道怎么建表、加索引、写高效 SQL
系统思维 — 考虑并发、安全、异常处理、事务
调试能力 — 能快速定位和解决问题
沟通协作 — 和各方对齐需求、接口、时间点
工程规范 — Git 工作流、Code Review、部署流程
附录 A — 后端开发常用术语速查
今天小张的工作中出现了很多后端术语,这里统一解释。
| 术语 | 英文 | 含义 | 今天出现的位置 |
| Controller |
Controller Layer |
控制层,负责接收 HTTP 请求、解析参数、调用 Service、返回响应 |
10:30 写 Controller |
| Service |
Service Layer |
业务逻辑层,处理核心业务规则(校验、计算、状态流转) |
11:00 写 Service |
| Mapper / DAO |
Data Access Object |
数据访问层,封装对数据库的操作(增删改查) |
11:40 写 Mapper |
| DTO |
Data Transfer Object |
数据传输对象,用于接收前端传来的参数 |
ReviewDTO.java |
| VO |
View Object |
视图对象,用于返回给前端展示的数据结构 |
ReviewVO.java |
| NPE |
NullPointerException |
空指针异常,对 null 对象调用方法时报错,Java 中最常见的运行时错误 |
14:00 Debug |
| 事务 |
Transaction (@Transactional) |
一组数据库操作要么全部成功,要么全部失败回滚 |
Service 的 @Transactional |
| 索引 |
Index |
数据库中用于加速查询的数据结构,类似书的目录 |
09:40 建表时的 idx_product_id |
| EXPLAIN |
EXPLAIN (MySQL) |
SQL 优化命令,查看 MySQL 如何执行查询(走了哪个索引、扫了多少行) |
09:40 验证索引 |
| PR |
Pull Request |
代码合并请求,将 feature 分支的代码请求合并到 develop/main 分支 |
15:00 提交代码 |
| Code Review |
Code Review |
代码评审,由同事(通常是 TL)审查代码质量、发现潜在问题 |
15:30 老陈审代码 |
| 分布式锁 |
Distributed Lock |
在分布式系统中防止多个节点同时执行同一操作的锁机制(常用 Redis 实现) |
15:30 Code Review 反馈 |
| 联调 |
Integration Testing |
前后端一起测试接口对接,验证数据格式和业务流程是否正确 |
16:30 前端联调 |
附录 B — 今天创建的代码文件总览
小张今天一共新建了 10 个文件,属于一个完整的功能模块。
Controller 层
ReviewController.java
3个接口:提交评价、评价列表、评价统计
Service 层
ReviewService.java (接口)
ReviewServiceImpl.java (实现)
业务逻辑:校验订单、防重复、保存评价
Mapper 层 (SQL)
ReviewMapper.java (接口)
ReviewMapper.xml (SQL映射)
数据库操作:CRUD + 统计查询
Entity / DTO / VO
Review.java (实体)
ReviewDTO.java (入参)
ReviewVO.java (出参)
ReviewStatsVO.java (统计)
数据模型定义
SQL
t_review.sql (建表语句)
表结构 + 索引定义
分层架构的核心职责:Controller 负责接收请求和返回响应(协议层)、Service 负责业务逻辑编排(业务层)、Mapper 负责数据库读写(数据访问层)。DTO 是入参封装对象,VO 是出参封装对象。
附录 C — 今天的工具截图索引
记录小张今天使用的所有工具和对应的场景。
💬
钉钉 (DingTalk)
09:00 早站会群聊 · 15:30 Code Review 反馈 · 16:30 联调沟通
团队即时通讯工具,后端开发中最常用的沟通工具。群聊用于同步信息,私聊用于一对一协作。
📋
JIRA
09:20 收到需求 PROJ-2847
项目管理工具,记录需求、分配任务、跟踪进度。后端开发者的工作通常从 JIRA 单子开始。
🗄️
DataGrip
09:40 设计表结构 · 09:40 EXPLAIN 验证 · 14:00 查询测试数据
JetBrains 出品的数据库管理工具。写 SQL、建表、查数据、分析索引,后端开发者每天都要打开它。
💻
IntelliJ IDEA
10:30 写 Controller · 11:00 写 Service · 14:00 Debug 断点调试
Java 开发者最常用的 IDE。代码编辑、自动补全、Debug 断点、Git 操作都能在一个工具里完成。
📮
Postman
13:30 测试 POST 提交评价 · 13:30 测试 GET 评价列表 · 14:00 发现 500 错误
HTTP 接口测试工具。后端写完代码后第一件事就是用 Postman 自测,确认接口能正常返回数据。
🔀
Git + GitHub/GitLab
15:00 提交代码 · 15:00 创建 PR · 15:30 查看 Code Review diff
版本管理工具。分支管理、代码提交、合并请求、Code Review,团队协作的基础设施。
🖥️
Terminal (SSH)
16:00 SSH 登录服务器 · Maven 打包 · Docker 部署 · 查看日志
命令行终端,后端部署的必备工具。通过 SSH 远程连接服务器,执行各种运维操作。