← 返回工作场景纪实

🤝 会议与协作场景集

不是写代码,但占了大量时间 — 需求评审、技术讨论、跨团队沟通
💡 需求场景实战 — 模糊需求澄清、需求变更、技术选型、跨团队协作
场景 1

❓ 模棱两可的需求 — "加个导出功能"到底要导出什么?

产品说"加个导出功能"就走了,你直接去做,做完了发现理解完全不一样——这种事太常见了。收到需求先问清楚,列一个清单逐条确认。
问题:导出什么?什么格式?多大?谁有权限?全不知道就做,100%要返工。

✅ 正确做法:追问后确认的需求文档

📝 需求确认文档
数据范围:按当前筛选条件
格式:Excel (.xlsx)
数据量:单次上限10万条(超限提示缩小范围)
权限:管理员+运营
列:同列表页 + "导出时间"列
文件名:订单数据_YYYYMMDD.xlsx

🔧 代码实现

📋 数据库变更 — 导出记录表 + 权限
-- 导出记录表:记录每次导出操作,便于审计
CREATE TABLE export_log (
    id            BIGINT PRIMARY KEY AUTO_INCREMENT,
    user_id       BIGINT NOT NULL COMMENT '操作人',
    export_type   VARCHAR(32) NOT NULL COMMENT '导出类型: order/product/user',
    filter_params JSON COMMENT '筛选条件快照',
    row_count     INT COMMENT '导出行数',
    file_path     VARCHAR(255) COMMENT '文件存储路径',
    status        TINYINT DEFAULT 0 COMMENT '0-处理中 1-成功 2-失败',
    created_at    DATETIME DEFAULT CURRENT_TIMESTAMP
);

-- 权限表:控制导出权限
INSERT INTO permission (code, name, module)
VALUES ('order:export', '订单导出', 'order'),
       ('product:export', '商品导出', 'product');
🧩 后端接口设计 — 异步导出
// 提交导出任务(导出是"创建一个导出任务"资源,用POST)
POST /api/export/order
Content-Type: application/json

{
    "status": "pending",
    "dateFrom": "2026-03-01",
    "dateTo": "2026-03-18"
}

// Response
{
    "code": 0,
    "data": {
        "taskId": "export_20260318_001",
        "status": "processing",
        "estimatedTime": "30s"
    }
}

// 查询导出状态 → 导出完成后返回下载链接
GET  /api/export/task/{taskId}
🛡️ 权限校验中间件
// Spring Boot 权限校验 + 数据量限制
@PreAuthorize("hasAnyRole('ADMIN', 'OPERATOR')")
@PostMapping("/export/order")
public Result<ExportTask> exportOrder(@Valid @RequestBody OrderExportQuery query) {
    // 1. 检查数据量上限(10万条)
    long count = orderService.countByCondition(query);
    if (count > 100_000) {
        return Result.fail("数据量超过10万条,请缩小筛选范围");
    }
    // 2. 提交异步导出任务
    ExportTask task = exportService.submitExport(query, getCurrentUser());
    return Result.ok(task);
}
验证 — 导出权限配置
SELECT r.name AS role, p.code AS permission FROM role_permission rp JOIN role r ON r.id = rp.role_id JOIN permission p ON p.id = rp.permission_id WHERE p.code LIKE '%export%';
rolepermission
管理员order:export
运营order:export
管理员product:export
收到需求先问清楚,列一个清单逐条确认。花30分钟澄清,省3天返工。

场景 2

🔄 需求变更 — 开发了一半产品说要改

评价功能做完了,产品说"改成可以追评"。数据库都建了,怎么办?需求变更不可怕,可怕的是闷头改不沟通。
变更内容:一个商品只能评价一次 → 可以追评(评价下面再评价)

📋 影响评估

🔍 需求变更影响评估
数据库:t_review 需加 parent_id 字段
接口:提交评价需传 parentId,列表需查子评价
前端:评价列表需展示折叠的追评
测试:测试用例要全部重写
工作量:+2人天
风险:已开发的功能需要回归测试

🔍 数据库变更对比

影响评估 — 受影响的数据量
SELECT COUNT(*) AS total_reviews, COUNT(DISTINCT user_id) AS affected_users FROM t_review WHERE is_deleted = 0;
total_reviewsaffected_users
28,45612,103
🔴 变更前 — 一人一商品一评价
CREATE TABLE t_review (
    id           BIGINT PRIMARY KEY AUTO_INCREMENT,
    user_id      BIGINT NOT NULL,
    product_id   BIGINT NOT NULL,
    content      TEXT,
    rating       TINYINT,
    created_at   DATETIME DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uk_user_product (user_id, product_id)  -- 唯一约束
);
🟢 变更后 — 支持追评(parent_id 关联原始评价)
CREATE TABLE t_review (
    id           BIGINT PRIMARY KEY AUTO_INCREMENT,
    user_id      BIGINT NOT NULL,
    product_id   BIGINT NOT NULL,
    parent_id    BIGINT DEFAULT NULL COMMENT '追评时关联原始评价ID',
    content      TEXT,
    rating       TINYINT,
    is_append    TINYINT DEFAULT 0 COMMENT '0-首次评价 1-追评',
    created_at   DATETIME DEFAULT CURRENT_TIMESTAMP,
    KEY idx_parent (parent_id),
    KEY idx_product (product_id)
);

📝 代码变更对比

🟢 变更后代码 — 支持追评
@PostMapping("/review")
public Result submitReview(@RequestBody ReviewRequest req) {
    Long userId = getCurrentUserId();
    Long productId = req.getProductId();

    if (req.getParentId() == null) {
        // 首次评价:检查是否已有首次评价
        Review first = reviewMapper.selectOne(
            new QueryWrapper<Review>()
                .eq("user_id", userId)
                .eq("product_id", productId)
                .isNull("parent_id")
        );
        if (first != null) {
            return Result.fail("您已评价过该商品,可以使用追评功能");
        }
    } else {
        // 追评:验证原始评价存在且属于当前用户
        Review parent = reviewMapper.selectById(req.getParentId());
        if (parent == null || !parent.getUserId().equals(userId)) {
            return Result.fail("原始评价不存在");
        }
        // 限制追评次数(最多3次)
        int appendCount = reviewMapper.selectCount(
            new QueryWrapper<Review>().eq("parent_id", req.getParentId())
        );
        if (appendCount >= 3) {
            return Result.fail("追评次数已达上限(3次)");
        }
    }

    Review review = new Review();
    review.setUserId(userId);
    review.setProductId(productId);
    review.setParentId(req.getParentId());
    review.setAppend(req.getParentId() != null ? 1 : 0);
    review.setContent(req.getContent());
    review.setRating(req.getRating());
    reviewMapper.insert(review);
    return Result.ok(req.getParentId() != null ? "追评成功" : "评价成功");
}
📦 数据迁移脚本 — 兼容旧数据
-- 数据迁移:为旧数据设置默认值(所有旧行都需要设置)
UPDATE t_review SET parent_id = NULL, is_append = 0 WHERE parent_id IS NULL OR is_append IS NULL;

-- 删除原有的唯一约束(变更后允许同一用户对同一商品多次评价/追评)
ALTER TABLE t_review DROP INDEX uk_user_product;

-- 验证迁移结果
SELECT is_append, COUNT(*) AS cnt FROM t_review GROUP BY is_append;
需求变更三步走:1.评估影响(数据库/接口/前端/测试)→ 2.和产品确认优先级3.调整排期(其他需求让路)

场景 3

⚖️ 技术选型讨论 — RabbitMQ 还是 Kafka?

选型不是选"最好的",而是选"最适合当前场景的"。列出对比表让数据说话。

📊 核心对比

RabbitMQ vs Kafka 对比
🐰 RabbitMQ
吞吐量 中等(万级/s)
延迟 ✅ 微秒级(低)
适用场景 ✅ 业务消息/通知
上手难度 ✅ 低
📄 Kafka
吞吐量 ✅ 高(百万级/s)
延迟 毫秒级(中等)
适用场景 ✅ 大数据/日志采集
上手难度 中等偏高
✅ 结论:业务消息用 RabbitMQ(延迟低、上手快),日志系统保持 Kafka。各取所长。
维度RabbitMQKafka
吞吐量万级十万级
延迟微秒级毫秒级
可靠性高(确认机制)高(副本机制)
消费模式推模式(实时)拉模式
适用场景业务消息大数据/日志
运维成本

🎯 代码配置

⚙️ RabbitMQ 配置 + 生产者(业务消息)
# application-rabbitmq.yml
spring:
  rabbitmq:
    host: 10.0.1.100
    port: 5672
    username: order-service
    password: ${RABBITMQ_PASSWORD}
    virtual-host: /production
    listener:
      simple:
        acknowledge-mode: manual        # 手动确认,防止消息丢失
        prefetch: 10
        retry:
          max-attempts: 3
          initial-interval: 1000ms

order:
  mq:
    exchange: order.status.exchange
    routing-key: order.status.changed
    queue: order.status.queue
🧩 RabbitMQ 生产者 + 消费者
// 生产者
@Component
@RequiredArgsConstructor
public class OrderStatusProducer {
    private final RabbitTemplate rabbitTemplate;

    @Value("${order.mq.exchange}") private String exchange;
    @Value("${order.mq.routing-key}") private String routingKey;

    public void sendStatusChange(OrderStatusEvent event) {
        rabbitTemplate.convertAndSend(exchange, routingKey, event, message -> {
            message.getMessageProperties().setMessageId(UUID.randomUUID().toString());
            return message;
        });
    }
}

// 消费者
@Component
@RabbitListener(queues = "${order.mq.queue}")
@RequiredArgsConstructor
@Slf4j
public class OrderStatusConsumer {
    private final NotificationService notificationService;

    @RabbitHandler
    public void handleStatusChange(OrderStatusEvent event, Channel channel,
                                    @Header(AmqpHeaders.DELIVERY_TAG) long tag) throws IOException {
        try {
            notificationService.sendOrderNotification(event);
            channel.basicAck(tag, false);  // 手动确认
        } catch (Exception e) {
            log.error("处理消息失败", e);
            channel.basicNack(tag, false, true);  // 重新入队
        }
    }
}
⚙️ Kafka 配置 + 消费者(日志采集)
# application-kafka.yml
spring:
  kafka:
    bootstrap-servers: 10.0.1.200:9092,10.0.1.201:9092,10.0.1.202:9092
    consumer:
      group-id: order-log-group
      auto-offset-reset: earliest
      enable-auto-commit: false
      max-poll-records: 500
    producer:
      acks: all
      retries: 3
      batch-size: 16384

// Kafka 日志消费者
@Component
@Slf4j
public class OrderLogConsumer {
    @KafkaListener(topics = "order-access-log", groupId = "order-log-group")
    public void consumeLog(List<ConsumerRecord<String, String>> records, Acknowledgment ack) {
        try {
            List<AccessLog> logs = records.stream()
                .map(r -> JSON.parseObject(r.value(), AccessLog.class))
                .collect(Collectors.toList());
            logSearchService.bulkIndex(logs);  // 批量写入 ES
        } finally {
            ack.acknowledge();
        }
    }
}
技术选型不是选"最好的",而是选"最适合当前场景的"。列出对比表让数据说话,最终决策写进文档。

场景 4

🌐 跨团队协作 — 需要别的团队提供接口,但他们排期排不上

跨团队协作三步走:口头沟通 → 正式邮件确认(抄TL留痕)→ 准备 Plan B。

📧 正式邮件沟通(留痕)

邮件技巧:① 清晰说明需求 ② 给出截止时间 ③ 抄双方TL ④ 提供Plan B

🧪 Plan B:Mock 接口代码

🧩 Mock Server — 等正式接口后删除
@RestController
@RequestMapping("/mock/user-center")
@Profile("dev")  // 只在开发环境生效
public class MockUserCenterController {

    @GetMapping("/points/{userId}")
    public Result<UserPointsVO> queryPoints(@PathVariable Long userId) {
        UserPointsVO vo = new UserPointsVO();
        vo.setUserId(userId);
        vo.setAvailablePoints(2580);
        vo.setTotalPoints(5200);
        vo.setLevel("GOLD");
        return Result.ok(vo);
    }

    @PostMapping("/points/deduct")
    public Result<Boolean> deductPoints(@RequestBody DeductRequest req) {
        if (req.getPoints() > 5000) return Result.fail("积分不足");
        return Result.ok(true);
    }
}
📦 Feign 客户端 — 对接正式接口只需改URL
@FeignClient(
    name = "user-center",
    url = "${user-center.url:http://localhost:8081}",
    fallbackFactory = UserCenterFallbackFactory.class
)
public interface UserCenterClient {
    @GetMapping("/api/points/{userId}")
    Result<UserPointsVO> queryPoints(@PathVariable("userId") Long userId);

    @PostMapping("/api/points/deduct")
    Result<Boolean> deductPoints(@RequestBody DeductRequest req);
}

// application-dev.yml  → url: http://localhost:8080/mock/user-center
// application-prod.yml → url: http://user-center-service:8081
🛡️ 降级兜底 — FallbackFactory
@Component
@Slf4j
public class UserCenterFallbackFactory implements FallbackFactory<UserCenterClient> {
    @Override
    public UserCenterClient create(Throwable cause) {
        log.error("用户中心接口调用失败,触发降级", cause);
        return new UserCenterClient() {
            @Override
            public Result<UserPointsVO> queryPoints(Long userId) {
                return Result.fail("积分服务暂时不可用,请稍后重试");
            }
            @Override
            public Result<Boolean> deductPoints(DeductRequest req) {
                return Result.fail("积分扣减服务暂时不可用");
            }
        };
    }
}

🔥 紧急修复场景 — 线上订单状态异常

🔥 紧急修复流程

$ git checkout -b hotfix/order-status-fix main
$ git add . && git commit -m "fix: correct order status transition"
$ git push origin hotfix/order-status-fix
$ kubectl rollout restart deployment/order-service
deployment.apps/order-service restarted
$ kubectl rollout status deployment/order-service --timeout=120s
deployment "order-service" successfully rolled out
$ git checkout main && git merge hotfix/order-status-fix
🔴 紧急数据修复 — 订单状态矫正SQL
-- 先查询受影响数据(不要直接更新!)
SELECT id, order_no, amount, status, paid_at
FROM order_info
WHERE status = 'pending'
  AND paid_at IS NOT NULL
  AND paid_at < DATE_SUB(NOW(), INTERVAL 5 MINUTE);

-- 确认无误后执行更新(带事务)
BEGIN;
UPDATE order_info SET status = 'paid', updated_at = NOW()
WHERE status = 'pending'
  AND paid_at IS NOT NULL
  AND paid_at < DATE_SUB(NOW(), INTERVAL 5 MINUTE);
-- 影响行数: 17
COMMIT;
跨团队协作三步走:1.先口头沟通2.正式邮件确认(抄TL留痕)→ 3.准备Plan B(Mock/临时方案)。千万别等着什么都不做。

📋 协作场景速查表

场景关键动作避坑
需求不清追问清单 + 确认文档别只说"好的"就开干
需求变更评估影响 + 确认优先级别闷头改不沟通
技术选型列对比表 + 让数据说话别凭感觉吵,别选"最火"的
跨团队邮件正式沟通 + Plan B别口头答应就算了
线上故障先止血再排查 + 事后复盘别光顾着找原因不恢复
📐 RESTful API 设计规范
为什么API设计要规范?
没规范的API:/getOrderList、/deleteUser、/updateUserInfo
有规范的API:GET /api/orders、DELETE /api/users/123、PUT /api/users/123
规范的API一看URL就知道"对什么资源做什么操作",新人不用问就能用。
📌 核心原则一:URL 用名词,HTTP方法用动词
操作❌ 不规范的写法✅ RESTful 写法HTTP方法
查询订单列表/getOrderList/api/ordersGET
查询单个订单/getOrderById?id=123/api/orders/123GET
创建订单/createOrder/api/ordersPOST
更新订单/updateOrder/api/orders/123PUT
删除订单/deleteOrder?id=123/api/orders/123DELETE
修改订单部分字段/updateOrderStatus/api/orders/123PATCH
记忆口诀:URL 是资源(名词),HTTP方法是动作(动词)。/orders 是订单集合,/orders/123 是特定订单。
📌 核心原则二:统一响应格式
// 所有接口统一返回这个格式 { "code": 0, // 业务状态码(0=成功,非0=失败,详见错误码表) "message": "success", // 提示信息 "data": { // 实际数据(失败时为null) "id": 123, "productName": "快递站", "amount": 15.0 } } // ⚠️ 重要区分:HTTP状态码 vs 业务状态码 // HTTP状态码:用于传输层,表示请求是否被服务器正确接收和处理(200/201/400/401/404/500) // 业务状态码:用于业务逻辑,code: 0 表示业务成功,非0表示具体业务错误 // 不要把HTTP状态码直接当业务码用!例:code: 400 不是"参数错误",code: 40001 才是 // 常见业务错误码示例: // code: 0 → 成功 // code: 40001 → 参数错误(如手机号格式不对) // code: 40101 → 未认证(未登录或Token过期) // code: 40301 → 无权限 // code: 40401 → 资源不存在(如订单ID=99999查不到) // code: 50001 → 系统内部错误 // 对应的Java类: public class Result<T> { private int code; private String message; private T data; public static <T> Result<T> success(T data) { return new Result<>(0, "success", data); } public static Result<Void> fail(int code, String msg) { return new Result<>(code, msg, null); } }
📌 核心原则三:HTTP 状态码与业务状态码的区分
HTTP状态码用于传输层,表示请求是否被服务器正确接收和处理;业务状态码用于业务逻辑,表示业务操作的结果。两者各司其职,不要混用。
层级状态码含义什么时候用示例
传输层
(HTTP状态码)
200请求成功GET/PUT/PATCH 成功查询/更新成功
201创建成功POST 创建资源成功新建订单成功
传输层
(HTTP状态码)
400请求格式错误请求体格式不对/参数缺失JSON格式错误
401未认证没带Token / Token过期需要先登录
传输层
(HTTP状态码)
403无权限权限不足普通用户访问管理接口
500服务器错误代码异常空指针、数据库连接失败
业务层
(code字段)
0业务成功业务操作完成订单创建成功
40001参数错误@Valid 校验失败手机号格式不对
40401资源不存在查不到业务数据订单ID=99999不存在
关键区分:HTTP返回200 + code返回40401 = "请求成功到达服务器,但业务上资源不存在"。HTTP返回404 = "接口路径本身不存在"。两者含义完全不同!
📌 核心原则四:分页、排序、过滤
// 分页查询:page(第几页)+ size(每页多少条) GET /api/orders?page=1&size=20 // 排序:sort=字段,方向 GET /api/orders?page=1&size=20&sort=createTime,desc // 过滤:用查询参数 GET /api/orders?status=PAID&userId=123 // 时间范围 GET /api/orders?startTime=2026-01-01&endTime=2026-06-30 // 后端 Spring Boot 接收分页参数: @GetMapping("/orders") public Result<PageResult<OrderVO>> listOrders( @RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "20") int size, @RequestParam(required = false) String status, @RequestParam(required = false) Long userId) { PageRequest pageRequest = PageRequest.of(page - 1, size, Sort.by(Sort.Direction.DESC, "createTime")); Page<OrderVO> result = orderService.list(pageRequest, status, userId); return Result.success(PageResult.of(result)); } // 分页响应格式: { "code": 0, "data": { "list": [...], // 当前页数据 "total": 1520, // 总记录数 "page": 1, // 当前页 "size": 20, // 每页条数 "totalPages": 76 // 总页数 } }
📌 核心原则五:API 版本管理
✅ 推荐:URL路径版本
/api/v1/orders
/api/v2/orders
最直观,客户端容易理解
也可:Header版本
Accept: application/vnd.myapi.v2+json
URL更干净,但不够直观
📋 API 设计 Checklist
URL 设计 ✅
☐ URL用名词复数(/orders)
☐ 用HTTP方法区分操作
☐ 路径参数标识资源(/orders/123)
☐ 查询参数做过滤(?status=PAID)
响应设计 ✅
☐ 统一格式 {code, message, data}
☐ 正确使用HTTP状态码
☐ 分页返回 total/page/size
☐ 错误返回明确错误信息
安全设计 ✅
☐ 敏感操作用 POST 不用 GET
☐ 接口有权限校验
☐ 参数有 @Valid 校验
☐ 版本号管理