技术派微信支付解锁付费阅读的两种实现方案(👍强烈推荐)
上面两篇分别介绍了,如何再技术派中设计一个支付系统 以及 技术派集成微信支付的实现说明;接下来自然就应该进入微信支付实现付费阅读了,那么这个应该怎么实现呢?
下面我将以两种实现方式,给大家演示一下这个任务的交付过程
- 方案1:基于效率迭代的增量式实现方案
- 方案2:基于充分设计的重构式实现方案
方案1:基于效率迭代的增量式实现方案
本方案的实现,可以在 https://github.com/itwanger/paicoding/releases/tag/1.0.3-%E5%BE%AE%E4%BF%A1%E6%94%AF%E4%BB%98 这个版本获取
实现方案名词解释
先解释一下这个实现方式的说明,什么叫基于效率迭代的增量式实现方案 ?
两个关键词:效率 + 增量
其中效率主要是指快速实现需求交付,哪种方式实现成本低、耗时短,那就选择哪种方式
其中增量则主要是指尽量不要影响原有的功能,能不改原来的逻辑就不改原来的逻辑,新的要求就写个else出来走新的流程
将上面两个关键词组合起来,其实就是我们日常工作中最常见的工作模式,以最快的时间、最小的影响范围来实现需求交付,在原有的流程中加if/else分支来实现,不用太考虑设计模式、实现优雅行,业务代码要的就是平铺、理解简单...
微信支付适配
2.1 发布付费文章适配
发布文章:
现有的实现,主要是基于个人收款码,无法设置每篇文章的付费金额;但是微信支付就没有这个烦恼了,可以设置付费金额,因此我们需要一个地方来存储这个金额
此外就是我们新增了一种支付渠道,那么在发布文章时,就多了一个选项,选择个人收款码支付呢,还是微信支付
因此我们的文章表,可以新增两个字段来存储上面的信息:pay_way + pay_amount
-- 文章添加支付相关信息
alter table `article`
add column `pay_amount` int not null default '0' comment '付费阅读金额,单位为分',
add column `pay_way` varchar(12) null comment '支付方式: email-个人收款码, wx_h5-微信h5支付, wx_jsapi-微信jsapi, wx_native-微信native';
2.2 用户文章付费关系适配
文章支付记录:
我们先看一下现有的文章支付记录表:
CREATE TABLE `article_pay_record` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`article_id` bigint unsigned NOT NULL DEFAULT '0' COMMENT '文章ID',
`pay_user_id` bigint unsigned NOT NULL DEFAULT '0' COMMENT '支付用户id',
`receive_user_id` bigint unsigned NOT NULL DEFAULT '0' COMMENT '收款用户id',
`pay_status` tinyint DEFAULT '0' COMMENT '支付状态 0-未支付 1-支付中 2-支付成功 3-支付失败',
`notes` varchar(128) DEFAULT '' COMMENT '备注信息',
`verify_code` char(16) not null DEFAULT '' COMMENT '用于校验回调合法的随机code',
`notify_cnt` int not null default 0 COMMENT '邮件通知次数',
`notify_time` timestamp NULL COMMENT '邮件通知收款用户时间',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后更新时间',
PRIMARY KEY (`id`),
KEY `i_article_id` (`article_id`),
KEY `i_pay_user_id` (`pay_user_id`),
KEY `i_receive_user_id` (`receive_user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文章支付记录表';
上面的实现,对于个人收款码的支付方式完全够用;但是在微信支付这里,我们则需要新增一些关键信息,比如
- 支付成功时间:微信回调的支付成功时间
- 微信支付流水号:微信这边记录的支付记录
- 唤起微信支付的code 及 这个code的失效时间
- 支付方式
- 支付金额
- 用于绑定微信订单与我们业务单据的out_trade_no
因此我们调整一下上面的表结构
-- 新增微信支付相关信息
alter table article_pay_record
add column pre_pay_id varchar(256) null comment '微信支付创建订单回传的关键信息',
add column pre_pay_expire_time timestamp null comment 'prePayId失效时间',
add column pay_callback_time timestamp null comment '支付成功时间',
add column third_trans_code varchar(50) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '第三方交易流水号',
add column pay_way varchar(12) not null default 'email' comment '支付方式: email-个人收款码, wx_h5-微信h5支付, wx_jsapi-微信jsapi, wx_native-微信native',
add column pay_amount int NULL COMMENT '支付金额,单位为分';
-- 支付记录,将verify_code标记为支付唯一标识,传递给外部系统使用
alter table `article_pay_record` modify `verify_code` varchar (32) not null DEFAULT '' COMMENT '唯一code,会传递给外部支付系统';
上面的改动直接给出了结果,但是我会有几个疑问
- 为什么要存
pre_pay_id+pre_pay_expire_time
- 我们拿这个外部单号到微信下单之后,它返回一个用于唤起支付的prePayId/code,这个值是有时效的,在这个时间内,这个值是一直都可以正常唤起支付的
- 缓存一下微信返回的这个值,可以避免每次都实时查询微信;
- 其次到微信下单时,要求外部单号唯一,因此每次给微信下单时传入新的唯一外部单号,若不缓存上面的结果,则可能出现重复支付场景
- 如第一次唤起支付,此时用户成功支付,但是没有拿到微信返回结果,db中依然记录待支付
- 用户看到是未支付,再次唤起支付,从而导致了重复支付
- 为什么不直接使用 id 作为外部单号?
- 对于微信,同一个外部订单号,在微信后台只能用一次
- 一个用户,对一个文章只有一条 article_pay_record 记录,会存在第一次唤起支付但实际不付钱的场景,若直接使用主键,那么下次支付就会因为单号重复被拦截
- 我们直接使用个人收款码 verify_code 来记录唯一外部单号,并定义对应的生成规则:
- 支付方式 + 随机序列号 +
article_pay_record.id
基于上面的变更,微信支付场景下,用户唤起支付的几种交互情况将如下图:
- 首次唤起支付,后台给微信下单,并将结果缓存下来,直接返回用户用于唤起支付
- 再次唤起支付
- 若上次微信返回的在有效期内,则直接返回用户用于唤起支付
- 若上次微信返回已失效,则重新生成一个外部单号,向微信下单,并重复首次唤起支付的流程

2.3 发布文章时,保存支付方式/付费金额
在上面的数据结构变更完成之后,接下来就是业务流程的变更,发布付费文章,之前没得选,只能是个人付款码;那么这里需要新增一个选项,支持个人收款码 + 微信支付流程
对应的改造点:
- 文章发布页,付费阅读下新增支付方式选项
- 统一微信支付,要求输入付费金额
- 个人收款码,不需要输入付费金额(因为控制不了用户给你的收款码转账多少)

- 文章保存
- 页面新增两个参数,后端需要存储下来
- 请求参数新增 payWay + payAmount
- 文章实体类新增 payWay + payAmount

- 文章返回付费方式 + 付费金额 (基于ArticleDTO返回的支付方式来适配展示)

- 个人收款码方式,见下图左
- 微信支付方式,见下图右

2.4 唤起支付接口实现
在上面的文章详情,用户唤起支付时,需要新增微信支付的场景,在之前的article/api/pay/toPay的接口实现上,完成微信支付的集成
- 首次唤起支付:建立映射关系
- 再次唤起支付:
- 已经支付完成的做幂等返回
- 对于微信支付:若微信返回唤起支付的code失效,则重新生成;否则直接使用上次的

- 对于个人收款码:返回用户的收款码给前端
- 关键实现如下(基本就是在原有的实现上加if/else走分支流程😀)
/**
* 唤起支付
*
* @param articleId 文章
* @param currentUserId 当前用户
*/
public ArticlePayInfoDTO toPay(Long articleId, Long currentUserId, String notes, ThirdPayWayEnum payWay) {
ArticlePayRecordDO dbRecord = articlePayDao.queryRecordByArticleId(articleId, currentUserId);
if (dbRecord == null) {
// 不存在时,创建一个
dbRecord = createPayRecord(articleId, currentUserId, notes, payWay);
} else if (Objects.equals(dbRecord.getPayStatus(), PayStatusEnum.SUCCEED.getStatus())) {
// 已经支付成功了
return PayConverter.toPay(dbRecord);
} else if (payWay.wxPay() && !dbRecord.getPayStatus().equals(PayStatusEnum.PAYING.getStatus())) {
// 在线支付场景,如果没有 prePayInfo 或者已经失效了,或者支付失败,则需要重新生成
if (dbRecord.getPayStatus().equals(PayStatusEnum.FAIL.getStatus())
|| dbRecord.getPrePayId() == null
|| dbRecord.getPrePayExpireTime() == null
|| System.currentTimeMillis() >= dbRecord.getPrePayExpireTime().getTime()) {
String newCode = IdUtil.genPayCode(payWay, dbRecord.getId());
if (Objects.equals(newCode, dbRecord.getVerifyCode())) {
// 做一个重复判断
newCode = IdUtil.genPayCode(payWay, dbRecord.getId());
}
dbRecord.setVerifyCode(newCode);
// 支付金额, 需要重新从文章库中获取一下,防止文章更新了价格,但是这里没有同步刷新的问题
ArticleDO article = articleReadService.queryBasicArticle(articleId);
dbRecord.setPayAmount(article.getPayAmount());
dbRecord.setNotes(String.format("支付解锁阅读《%s》-- %s", article.getTitle(), notes == null ? "" : notes));
ThirdPayOrderReqBo req = new ThirdPayOrderReqBo();
req.setTotal(dbRecord.getPayAmount());
req.setOutTradeNo(dbRecord.getVerifyCode());
req.setDescription(StrUtil.pickWxSupportTxt(dbRecord.getNotes()));
req.setPayWay(payWay);
PrePayInfoResBo res = thirdPayService.createPayOrder(req);
if (res != null) {
dbRecord.setPrePayId(res.getPrePayId());
dbRecord.setPrePayExpireTime(new Date(res.getExpireTime()));
if (PayStatusEnum.FAIL.getStatus().equals(dbRecord.getPayStatus())) {
// 支付失败,重新支付时,重置支付状态
dbRecord.setPayStatus(PayStatusEnum.NOT_PAY.getStatus());
}
}
articlePayDao.updateById(dbRecord);
}
} else if (PayStatusEnum.FAIL.getStatus().equals(dbRecord.getPayStatus())) {
// 个人收款码支付场景:对于原是支付失败的场景,再次支付时,重置相关信息
dbRecord.setNotifyTime(null);
dbRecord.setPayStatus(PayStatusEnum.NOT_PAY.getStatus());
articlePayDao.updateById(dbRecord);
}
// 收款用户信息
ArticlePayInfoDTO dto = PayConverter.toPay(dbRecord);
if (!payWay.wxPay()) {
BaseUserInfoDTO receiveUserInfo = userService.queryBasicUserInfo(dbRecord.getReceiveUserId());
dto.setPayQrCodeMap(...未能定位到原文的划线
企业级Agent工作流编排项目PaiFlow
Vibe Coding版本的PaiAgent
派聪明RAG AI知识库Java版本+Go版本
微服务 PmHub、技术派、MYDB
求职派JobClaw(OpenClaw/Hermes架构
PaiCLI(类似Claude Code的Agent
派简历(代码已完成)
等实战项目。
1. 微信扫右侧的优惠券加入知识星球
2. 解锁星球的实战项目教程和源码: 项目源码+教程获取
真诚点赞 诚不我欺
回复