后端开发者的一天 — 工作纪实

2026-03-18 (Wednesday) · 小张 · 3年经验 Java 后端 · 电商业务线订单组

这不是面试指南。这是一个真实的后端开发者工作日全记录。你会看到他屏幕上的每一个细节。

09:00 早站会 (Daily Standup)

小张打开电脑,钉钉弹出群消息提醒。10点准时站会,每个人轮流说三件事:昨天做了什么、今天做什么、有没有卡住的地方。

💬 订单组日报群 (8)
—— 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,看到新分配过来的需求单。先仔细读需求,搞清楚要做什么。

PROJ-2847 · 商品评价功能 Medium Priority
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,连接到开发数据库。先看看现有的表结构,再设计新的评价表。

DataGrip 2026.1 my_shop@dev-mysql:3306
my_shop
tables
📋 t_order
📋 t_order_item
📋 t_product
📋 t_user
📋 t_category
⭐ t_review ← NEW
CREATE TABLE t_review ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '评价ID', user_id BIGINT NOT NULL COMMENT '用户ID', order_id BIGINT NOT NULL COMMENT '订单ID', product_id BIGINT NOT NULL COMMENT '商品ID', rating TINYINT NOT NULL COMMENT '评分1-5', content VARCHAR(500) DEFAULT NULL COMMENT '评价内容', images VARCHAR(1000) DEFAULT NULL COMMENT '评价图片URL', status TINYINT DEFAULT 1 COMMENT '状态 1-正常 2-隐藏', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_product_id (product_id), INDEX idx_user_order (user_id, order_id) ) COMMENT '商品评价表';
✓ Query OK, 0 rows affected (0.02 sec)
-- t_review created successfully in my_shop
建完表后习惯用 EXPLAIN 验证索引是否生效,确保未来查询能走索引而不是全表扫描。特别是在数据量大的表上,这一步不能省。
Console 2 — EXPLAIN 验证
EXPLAIN SELECT * FROM t_review WHERE product_id = 1001 AND status = 1 ORDER BY create_time DESC LIMIT 20;
id
type
key
rows
Extra
1
ref
idx_product_id
45
Using where; Backward index scan
✓ 走了索引,OK! type=ref 表示使用了索引查找,rows=45 表示预估扫描45行,性能没问题。
设计表的时候几个关键决策:
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 测试刚写的接口。先测"提交评价"接口。

POST http://localhost:8080/api/v1/reviews
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。接下来测试"查询评价列表"接口。

GET http://localhost:8080/api/v1/reviews/products/5001?page=1&size=10
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 了。

POST http://localhost:8080/api/v1/reviews
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 这个订单。为什么?去数据库看看这个订单是否存在。
Console 3 — 查订单
SELECT * FROM t_order WHERE id = 20260318001;
Empty set (0.00 sec)
-- 换个ID试试,看看有没有测试数据 SELECT id, status FROM t_order WHERE user_id = 10086 LIMIT 5;
id
status
20260317001
COMPLETED
20260315002
COMPLETED
20260312003
SHIPPING
找到原因了! Postman 里传的 orderId: 20260318001 根本不存在,测试数据用的是 20260317001。这不是代码的 bug,是测试数据的问题。改一下 Postman 里的 orderId 重新发请求。
POST http://localhost:8080/api/v1/reviews
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 # 提交,写清楚 commit message ~/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
🔀 Pull Request #147 — feat: 商品评价功能
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 上审完了代码,在群里 @ 小张反馈意见。

💬 代码评审 — PR #147 商品评价功能
—— 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 和前端也能访问。

# SSH 连接到测试服务器 ~/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 ... # Maven 打包 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 # Docker 构建和启动 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 状态 # 打开浏览器 → PROJ-2847 → 拖到 "QA 验证" 列
下班前的习惯 提交所有未提交的代码(别留在本地过夜) 更新 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 远程连接服务器,执行各种运维操作。