-- 导出记录表:记录每次导出操作,便于审计
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);
}
| role | permission |
| 管理员 | order:export |
| 运营 | order:export |
| 管理员 | product:export |
parent_id 字段| total_reviews | affected_users |
| 28,456 | 12,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) -- 唯一约束
);
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;
| 维度 | RabbitMQ | Kafka |
|---|---|---|
| 吞吐量 | 万级 | 十万级 |
| 延迟 | 微秒级 | 毫秒级 |
| 可靠性 | 高(确认机制) | 高(副本机制) |
| 消费模式 | 推模式(实时) | 拉模式 |
| 适用场景 | 业务消息 | 大数据/日志 |
| 运维成本 | 低 | 中 |
# 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
// 生产者
@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); // 重新入队
}
}
}
# 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();
}
}
}
小李你好,我们电商订单组正在开发"积分兑换"功能,需要调用用户中心的积分查询接口。
接口需求:查询用户当前可用积分 + 扣减积分(兑换时调用),预计 QPS: 50
排期:我们这个 Sprint (3/18-3/29) 需要联调,看到你们排到下个 Sprint,能否提前?
如果排不开,我们可以:① 先用 Mock 数据,后续联调 ② 我们自己写临时接口(需你们审核)
@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);
}
}
@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
@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("积分扣减服务暂时不可用");
}
};
}
}
-- 先查询受影响数据(不要直接更新!) 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;
| 场景 | 关键动作 | 避坑 |
|---|---|---|
| 需求不清 | 追问清单 + 确认文档 | 别只说"好的"就开干 |
| 需求变更 | 评估影响 + 确认优先级 | 别闷头改不沟通 |
| 技术选型 | 列对比表 + 让数据说话 | 别凭感觉吵,别选"最火"的 |
| 跨团队 | 邮件正式沟通 + Plan B | 别口头答应就算了 |
| 线上故障 | 先止血再排查 + 事后复盘 | 别光顾着找原因不恢复 |
| 操作 | ❌ 不规范的写法 | ✅ RESTful 写法 | HTTP方法 |
|---|---|---|---|
| 查询订单列表 | /getOrderList | /api/orders | GET |
| 查询单个订单 | /getOrderById?id=123 | /api/orders/123 | GET |
| 创建订单 | /createOrder | /api/orders | POST |
| 更新订单 | /updateOrder | /api/orders/123 | PUT |
| 删除订单 | /deleteOrder?id=123 | /api/orders/123 | DELETE |
| 修改订单部分字段 | /updateOrderStatus | /api/orders/123 | PATCH |
| 层级 | 状态码 | 含义 | 什么时候用 | 示例 |
|---|---|---|---|---|
| 传输层 (HTTP状态码) | 200 | 请求成功 | GET/PUT/PATCH 成功 | 查询/更新成功 |
| 201 | 创建成功 | POST 创建资源成功 | 新建订单成功 | |
| 传输层 (HTTP状态码) | 400 | 请求格式错误 | 请求体格式不对/参数缺失 | JSON格式错误 |
| 401 | 未认证 | 没带Token / Token过期 | 需要先登录 | |
| 传输层 (HTTP状态码) | 403 | 无权限 | 权限不足 | 普通用户访问管理接口 |
| 500 | 服务器错误 | 代码异常 | 空指针、数据库连接失败 | |
| 业务层 (code字段) | 0 | 业务成功 | 业务操作完成 | 订单创建成功 |
| 40001 | 参数错误 | @Valid 校验失败 | 手机号格式不对 | |
| 40401 | 资源不存在 | 查不到业务数据 | 订单ID=99999不存在 |