项目优化 ver.2 / Day6

项目优化 ver.2 / Day6
Photo by Shamblen Studios / Unsplash

我们今天将我们hmdp的这个支付过程上一个沙箱模拟一下真正流程

链路

新建了一个「PayOrderStatus」类 目的是确认支付这一链路的状态

public enum PayOrderStatus {
    INIT(1, "已创建"),
    PAYING(2, "支付中"),
    SUCCESS(3, "支付成功"),
    FAIL(4, "支付失败"),
    CLOSED(5, "已关闭"),
    REFUND(6, "已退款");
}

新建了一个支付表 加了这些字段:

  • id, order_id, user_id, voucher_id
  • out_trade_no, third_trade_no
  • channel, amount, status, notify_status
  • notify_content, fail_reason
  • pay_time, create_time, update_time

还加了这些索引:

UNIQUE KEY `uk_out_trade_no` (`out_trade_no`) USING BTREE,                                  UNIQUE KEY `uk_order_id` (`order_id`) USING BTREE,       
KEY `idx_voucher_id` (`voucher_id`) USING BTREE,                                  
KEY `idx_create_time` (`create_time`) USING BTREE,                                 
KEY `idx_status_create_time` (`status`, `create_time`) USING BTREE,                                  KEY `idx_user_create_time` (`user_id`, `create_time`) USING BTREE

先补分库分表配置

再补entity、mapper、service、impl的骨架。

现在项目里订单状态的 NORMAL 实际承担了两种含义:

  1. “订单存在,可视为已抢到”
  2. “订单完成,可视为成功订单”

接入支付后,这两种含义必须拆开,所以应该分阶段迁移查询条件。

先把所有用到 OrderStatus.NORMAL 的地方分类,再分别替换。

第一类:防重复下单
这类地方原来查 NORMAL,现在应该改成查:

  • PENDING
  • PAID

含义是:

  • 待支付单也算已占坑,不能再抢
  • 已支付单更不能再抢

在createVoucherOrderV2()内我们的查已抢过的逻辑是

 VoucherOrder normalVoucherOrder = lambdaQuery()
                .eq(VoucherOrder::getVoucherId, messageBody.getVoucherId())
                .eq(VoucherOrder::getUserId, userId)
                .eq(VoucherOrder::getStatus,OrderStatus.NORMAL.getCode())
                .one();

那应该把

.eq(VoucherOrder::getStatus, OrderStatus.NORMAL.getCode())

改成

.in(VoucherOrder::getStatus,
    OrderStatus.PENDING.getCode(),
    OrderStatus.PAID.getCode())

第二类:查询“用户是否已买过该券”

如果页面想表达“这个券你已经占单了,不能再抢”,那也应该查:

  • PENDING
  • PAID

第三类:取消订单
现在 cancel() 是按 NORMAL 查

接支付后,这里要先明确业务规则:

  • 如果“取消领取”其实是取消待支付订单,那就只允许 PENDING
  • 如果“已支付后也允许取消”,那就不是这个接口的语义了,应该是退款链路

最后总结:

枚举:PENDING/CANCEL/PAID/TIMEOUT
查询:按 PENDING/PAID 防重
取消:只允许 PENDING
建单:显式写 PENDING

目前抢券这一块的流程是前端点“抢购”后,后端先在 Redis 层抢资格和扣库存,再异步发 Kafka,消费者落库创建订单。整个流程里没有真实支付阶段,所以“订单创建成功”就等同于“抢购成功”。

它当前的步骤是:

  1. 查秒杀券信息
    seckillVoucherService.queryByVoucherId(voucherId)
  2. 预热/加载库存到 Redis
    seckillVoucherService.loadVoucherStock(voucherId)
  3. 取当前用户 ID
  4. 校验用户等级是否满足参与条件
    verifyUserLevel(...)
  5. 生成两个 ID
    • orderId:订单号
    • traceId:追踪号
  6. 组装 Redis Lua 脚本的 keys 和 args
    包括:
    • 库存 key
    • 用户防重 key
    • trace 日志 key
    • 开始时间/结束时间
    • 券状态
    • orderId
    • traceId
    • 日志类型
    • TTL
  7. 执行 Lua 脚本
    seckillVoucherOperate.execute(keys, args)
  8. Lua 成功后,组装 Kafka 消息 SeckillVoucherMessage
    消息里带:
  9. 立刻返回 orderId 给前端
    • userId
    • voucherId
    • orderId
    • traceId
    • beforeQty
    • changeQty
    • afterQty
    • autoIssue
  10. 发送 Kafka
    seckillVoucherProducer.sendPayload(...)


Kafka 消费阶段
收到消息后流程是:

  1. beforeConsume() 先检查消息是否超时
    如果消息延迟超过阈值:
    • 回滚 Redis 里的库存和用户抢购标记
    • 记录对账日志
    • 丢弃消息
  2. 没超时就进入 doConsume()
    调:
    voucherOrderService.createVoucherOrderV2(message)

真正落库

  1. 从消息里拿出 userId / voucherId / orderId
  2. 查数据库里是否已有同用户同券且状态为 NORMAL 的订单
    如果有,说明重复,直接抛异常
  3. 扣减 DB 库存
    stock = stock - 1 and stock > 0
  4. 创建 VoucherOrder
    当前只设置了:
    • id
    • userId
    • voucherId
    • createTime
  5. save(voucherOrder) 写入订单表
  6. 创建 VoucherOrderRouter
    用于后续分片路由查询
  7. 把订单写到 Redis 缓存,供前端轮询查订单
  8. 记录对账日志
    "order created"
  9. 返回 true

我们先把支付这一整条状态流转连起来,添加以下方法

saveInitPayOrder、startPay、markPaySuccess、markPayFail、closePayOrder、refundPayOrder,外加 getByOrderId 和 getByOutTradeNo 查询入口。每一个状态都要检测前面的状态是否正确确保不会随意跳转出问题

在更新时候用了id+oldStatus的CAS的判断,避免并发把状态跳乱。重复回调/重复操作如果已经是目标状态,会直接返回当前单据,保持幂等。

然后把生产的消息字段扩充了支付金额 因为金额是在订购的时候有一个快照 然后还有一个支付方式以便后续扩展 现在先写死支付宝的支付

消费消息的时候直接调支付最顶接口然后去init我们的payorder存入 然后我继续接了“取消单”和“超时单”的逻辑 具体上是cancel()方法不仅仅是rollback和改order状态了 是再加了个我开的closePayOrder去关支付订单
补了一个新的服务入口 IVoucherOrderService.java:
timeoutCloseVoucherOrder(Long orderId)
这条方法会做和取消单同一套关闭流程,只是把业务订单状态改成 TIMEOUT,并记录 BusinessType.TIMEOUT 的对账日志。也就是说后面不管是接定时任务、延迟队列,还是支付超时扫描,都可以直接调这个方法,不需要再重复写一套库存回滚和支付关单逻辑。

  1. 在 VoucherOrderServiceImpl.java 里,createVoucherOrderV2() 事务提交后会注册一个 afterCommit 钩子,投递支付超时检查消息。
  2. 延迟消息体是新加的 DelayedPayOrderTimeoutMessage.java,带了 orderId/userId/voucherId/orderCreateTime。
  3. 延迟消费者是新加的 ConsumerDelayedPayOrderTimeout.java。它消费后会先查订单:
    如果订单不存在,跳过。
    如果订单已经不是 PENDING,跳过。
    只有仍然 PENDING 时,才调用 voucherOrderService.timeoutCloseVoucherOrder(orderId)。
  4. topic 常量也补了,在 Constant.java 里新增了 DELAY_PAY_ORDER_TIMEOUT。

然后现在去打通支付流程

mock打通了测试完改一下 我开始接支付宝沙箱,生成了一下公私钥配了个环境,预期流程应该是

  1. 秒杀下单,生成 orderId
  2. 调 /pay-order/start
  3. 后端调用支付宝沙箱下单接口,前端跳支付宝收银台
  4. 用沙箱买家账号登录付款
  5. 支付宝回调 notify
  6. 系统把 pay_order 改成 SUCCESS,把 voucher_order 改成 PAID
  7. 前端再查 /pay-order/{orderId} 或订单状态

然后他回调的时候要notify到一个公网ip我正好买了这个ecs所以直接穿透打一下就可以

QQ20260316-161913.png

生成支付单之后直接去调支付宝下单

前后端好难调。。。呃呃呃先吃饭 等等再更新

一点问题总结

支付宝端超时关单问题

发现本地超时和支付宝侧交易没有闭环对齐。下单模型里没设置 timeout_express,而超时关单只做了本地 VoucherOrderServiceImpl.java:781 的 closePayOrder,没有 alipay.trade.close。结果就是本地订单 15 分钟后可能已经CLOSED/TIMEOUT,但支付宝收银台还可付款;一旦用户用旧页面付成功,回调会被拒掉,形成“真付了钱,本地不认”。解决的方法是发起支付时会把 pay.order.timeout.seconds 转成 timeout_express 带给支付宝,本地取消/超时关单后,还会在事务提交后补一次 alipay.trade.close。

voucher_order 的路由现在和支付链路的访问方式错位

因为我们这个支付天然只能拿到orderid 但是我们直接想拿到订单order信息 真正的路由键不是 orderid,而是 user_id + voucher_id。我们有一个路由表忘了用
在建单时写了 tb_voucher_order_router,里面存的是:

  • orderId
  • userId
  • voucherId

真实应该是orderid to user+voucher

再去userId + voucherId + orderId -> voucher_order

没理清导致的

Read more

Agent Ai全栈开发记录

Agent Ai全栈开发记录

持续更新 star感谢! GitHub - xianingawa-wq/context-verify-agent: Contract Review AgentContract Review Agent. Contribute to xianingawa-wq/context-verify-agent development by creating an account on GitHub.GitHubxianingawa-wq 关于该Agent系统的想法 该系统灵感由大创项目而来 具体目标是 RAG + MCP 的合同校审Agent 我将该项目扩大为公司内部合同校审的SaaS平台 写完这一版后把后端平台重构为Java来承接 并尝试使用RPC进行调用 这样我的RPC也有了真实的使用场景 模型(现在先调api 以后美美白嫖老师的服务器用ollama): Qwen-max Qwen-text-embedding-v4 Qwen-rerank-v3 关于基础 看了一点hello agent 也多了解了一下概念之类的东西 觉得亲手做更好更沉浸理解本质 所以开始做 我认为这种就是

By XnLemon

面经

腾讯PCG-大数据ai平台一面 处女面 50min 面试官人超级好 可惜我真的软脚然后答偏/答不出来还给自己挖坑乱跳.... 1.自我介绍 2.有没有用过你刚刚讲过的一些技术去实现一些项目之类的?(java技术栈+常用中间件) 夏柠从这里开始节奏直接爆炸,引到了第二个虽然是自己写过的 但是拷打会很去死的项目 3.你从rpc提到了微服务,你知道有哪些比较常见的微服务架构? 其实我简历上根本没写微服务相关而且我也没学过 引到这里纯粹是节奏爆炸了 答了springcloud 4.springcloud常用的模块? 5.你做的这个rpc项目的框架是怎么做的,你怎么去思考如何做一个rpc项目的(从最小可用开始 服务消费者 -> rpcrequest -> 序列化到注册中心并负载均衡找到一个实例 -> 序列化服务名和方法名反射调用 -> 服务提供者) 6.服务提供者某个节点挂了怎么办?答的主动下线有钩子 被动挂掉有心跳机制 7.如果节点挂掉了,但是心跳机制还没发现这个时间段中间 调用方法正好调用到这个实例怎么办?(设置连接超时和请求超时,避免一直卡住。

By XnLemon
测试数据报告/Day7

测试数据报告/Day7

关于我简历内数据吞吐量“2-3”倍怎么来的这回事 一、测试目标 * 验证本项目秒杀接口在“异步链路(Kafka)”与“同步链路(直落库)”下的性能差异。 * 在可接受延迟目标下评估可用吞吐量。 * 本轮最终对比口径采用:HTTP p95 < 300ms。 二、测试对象 * 接口:POST /voucher-order/seckill/{id} * 券ID:voucher_id=1 * 压测脚本:k6_seckill_ab.js 三、测试环境 * 时间:2026-03-18(本地) * 后端:hmdp-core-service 本地 java -jar 启动,端口 8085 * 中间件:Docker 启动

By XnLemon

Day4 / 5

这两天脑子昏昏的 感觉就是笔试题害怕出hot100之外然后写不上然后被卡 但是越刷脑子越昏越不想思考 是不是有点太累了 day4去了招聘会 感觉能碰到门槛的其他厂也挺不错的 好像不一定要冲很高。 想了一会打算回去更新一波简历让各位大佬能看到确实有不一样的地方 那就足够了 我的代码能力可能是真的很差?已经在保持手感了刷一次忘一次是真的很难过 我觉得更应该去理解开发流程和项目 正常开发时候代码量什么的也不会很爆炸...? 打算今天把脑子放松一会

By XnLemon