简介:这套源码面向电商支付场景的开发者与商家,聚焦淘宝天猫虚拟卡密代付、京东中石油充值及聚合支付三类业务,可支持卡密店铺协议回调,实现支付后自动发货。包内共2000个文件,以1412个js脚本、209个html页面、117个css样式、111个json配置为主,另含131个md说明、2个sql建表脚本及少量docx、pptx文档,压缩包约75.33MB,前后端结构完整,便于二次开发与部署调试。目前已有157人学习下载。需注意系统仅完成天猫代付与京东中石油模块,京东中石化、比心、快手小店仅有名称未开发;天猫模块附带ck软件与使用教程,中石油模块需自行研究。整体适合具备一定支付系统基础、希望快速搭建卡密代付与聚合支付平台的开发者参考。
1. 代付、卡密、聚合支付:三套源码到底在解决什么生意问题
你在淘宝天猫下单,结算时选了“找人代付”,朋友点开链接用微信付了钱,订单状态实时变成“已付款”——这背后跑的就是淘宝天猫代付系统。你在京东买了张加油卡,收到一串卡密,去加油站圈存时系统核销成功——这是京东油卡卡密系统。你的小商城同时接了微信、支付宝、云闪付,用户选哪个都能付,对账时统一在一个后台看——这是聚合支付系统。三套源码,三个场景,但底层逻辑高度重合:都是围绕“订单—支付—回调—对账”这条链路做文章。热搜里“聚合支付”“源码”“淘宝天猫”“京东”这几个词反复出现,说明需求真实存在,但大多数人卡在同一个地方:拿到源码跑不起来,或者跑起来了不敢上生产。这篇把我自己踩过的路讲清楚,从环境搭建到回调验签到对账文件解析,每一步都给可复现的命令和参数。
2. 三套系统的技术底座:订单、支付通道、回调与对账
2.1 代付系统的核心状态机
代付系统跟普通支付最大的区别在于:付款人和下单人不是同一个。淘宝天猫代付的流程是——买家A创建订单,选择“找人代付”,系统生成一个代付链接和代付单号;被委托人B打开链接,用自己的支付账户完成付款;支付成功后,平台通过异步回调通知订单系统,订单状态从“待代付”变为“已付款”。
这里面最关键的是状态机设计。我一般会把代付单的状态定义为这几个:INIT(已创建)、PENDING(等待付款)、PAID(已付款)、EXPIRED(已过期)、REFUNDED(已退款)。状态流转必须是单向的,不允许从PAID回到PENDING。很多源码翻车就翻在这里——回调重复触发时没有做幂等,导致订单状态被反复改写。
# 代付单状态机核心逻辑(简化版) from enum import Enum class PayOrderStatus(Enum): INIT = "INIT" PENDING = "PENDING" PAID = "PAID" EXPIRED = "EXPIRED" REFUNDED = "REFUNDED" # 合法状态流转表 VALID_TRANSITIONS = { PayOrderStatus.INIT: [PayOrderStatus.PENDING], PayOrderStatus.PENDING: [PayOrderStatus.PAID, PayOrderStatus.EXPIRED], PayOrderStatus.PAID: [PayOrderStatus.REFUNDED], PayOrderStatus.EXPIRED: [], PayOrderStatus.REFUNDED: [], } def transition(current, target): if target not in VALID_TRANSITIONS.get(current, []): raise ValueError(f"非法状态流转: {current} -> {target}") return target这段代码的逻辑很直白:用枚举锁死所有可能的状态,用字典定义合法流转路径。参数说明——current是当前状态,target是目标状态,任何不在白名单里的跳转直接抛异常。实际项目中我会再加一层数据库乐观锁,用version字段防止并发更新。
2.2 卡密系统的加密与核销
京东油卡卡密系统的核心是两件事:生成时加密存储,核销时原子扣减。卡密本质上是一串有面额的兑换码,生成时用AES加密后入库,核销时解密比对并标记已使用。
常见做法是卡密分两段:前8位是批次号,后16位是随机串。批次号用于批量管理和对账,随机串用于唯一性校验。存储时只存哈希值(比如SHA256),不存明文——这样即使数据库泄露,卡密也不会被直接盗用。
import hashlib import secrets def generate_card(batch_no: str, face_value: int): """生成一张卡密,返回明文和哈希""" random_part = secrets.token_hex(8) # 16位随机串 card_no = f"{batch_no}{random_part}" card_hash = hashlib.sha256(card_no.encode()).hexdigest() # 入库: card_hash, face_value, status='UNUSED' return card_no, card_hash def redeem_card(card_no: str, db): """核销卡密,原子操作""" card_hash = hashlib.sha256(card_no.encode()).hexdigest() # 用UPDATE ... WHERE status='UNUSED'保证原子性 affected = db.execute( "UPDATE cards SET status='USED', used_at=NOW() " "WHERE card_hash=%s AND status='UNUSED'", (card_hash,) ) if affected == 0: raise ValueError("卡密无效或已使用") return True参数说明:batch_no是批次号,建议用日期加渠道码,比如20250101JD;face_value是面额,单位分。核销时用UPDATE ... WHERE status='UNUSED'这一条SQL完成原子扣减,返回影响行数为0就说明卡密已经被用过或者不存在。这个设计比“先查再改”安全得多,高并发下不会出现同一张卡被核销两次的情况。
2.3 聚合支付的路由与对账
聚合支付系统要解决的核心问题是:一笔订单来了,走哪个通道。常见策略有几种——按费率优先(选费率最低的)、按成功率优先(选近期成功率最高的)、按权重轮询(按配置比例分流)。我一般会做一个简单的路由引擎,把通道配置放在数据库里,支持热更新。
对账是聚合支付最容易被忽视的环节。每天凌晨,系统需要下载各通道的对账文件,跟本地订单逐笔比对,找出“本地成功但通道失败”“通道成功但本地失败”“金额不一致”这三类差异。对账文件格式各通道不同,有的是CSV,有的是定长文本,解析时要特别注意编码和分隔符。
import csv from decimal import Decimal def reconcile(local_orders: dict, channel_file: str): """对账核心逻辑""" diffs = [] with open(channel_file, 'r', encoding='utf-8') as f: reader = csv.DictReader(f) for row in reader: order_no = row['order_no'] channel_amount = Decimal(row['amount']) local = local_orders.get(order_no) if not local: diffs.append(('CHANNEL_ONLY', order_no, channel_amount)) elif local['amount'] != channel_amount: diffs.append(('AMOUNT_MISMATCH', order_no, local['amount'], channel_amount)) else: local_orders[order_no]['reconciled'] = True # 剩下的就是本地有但通道没有的 for order_no, order in local_orders.items(): if not order.get('reconciled'): diffs.append(('LOCAL_ONLY', order_no, order['amount'])) return diffs这段代码用Decimal处理金额,避免浮点误差——这是血泪经验,用float对账迟早出问题。local_orders是以订单号为key的字典,channel_file是通道对账文件路径。返回的diffs列表包含三类差异,后续可以入库告警或自动冲正。
3. 从零跑通一套聚合支付源码:环境、配置与联调
3.1 环境准备与依赖安装
拿到一套聚合支付源码,第一步不是急着改代码,而是把环境跑起来。我一般用Docker Compose编排,把MySQL、Redis、应用服务放在一个网络里,避免“在我机器上能跑”的玄学问题。
# docker-compose.yml 核心片段 version: '3.8' services: mysql: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: pay123456 MYSQL_DATABASE: payment ports: - "3306:3306" volumes: - ./init.sql:/docker-entrypoint-initdb.d/init.sql redis: image: redis:7-alpine ports: - "6379:6379" app: build: . ports: - "8080:8080" depends_on: - mysql - redis environment: DB_HOST: mysql REDIS_HOST: redis参数说明:MySQL用8.0版本,字符集建议在init.sql里显式指定utf8mb4;Redis用7-alpine够轻量;应用服务的depends_on只保证启动顺序,不保证服务就绪,生产环境要加健康检查。init.sql里放建表语句和初始通道配置。
启动命令就一行:docker-compose up -d。起来之后用docker-compose logs -f app看日志,如果看到“Started Application”就说明服务起来了。常见翻车点是MySQL连接超时——应用启动太快,MySQL还没初始化完。解决办法是在应用里加重试逻辑,或者用wait-for-it.sh脚本。
3.2 支付通道配置的关键参数
聚合支付源码里,通道配置是最容易配错的地方。我整理了一张必调参数表:
| 参数名 | 说明 | 典型值 | 坑点 |
|---|---|---|---|
| channel_code | 通道标识 | WXPAY_01 | 必须与代码里枚举一致 |
| app_id | 应用ID | wx1234567890 | 各通道不同,别混用 |
| merchant_id | 商户号 | 1900000109 | 跟app_id配对 |
| api_key | 接口密钥 | 32位字符串 | 不要提交到Git |
| notify_url | 异步回调地址 | https://your.domain/notify/wx | 必须公网可达 |
| sign_type | 签名类型 | MD5/RSA2 | 与通道要求一致 |
配置写完后,先用通道提供的沙箱环境测一笔。微信支付有沙箱,支付宝也有。测试时重点看三件事:下单是否返回支付链接、回调是否收到、验签是否通过。验签失败最常见的原因是api_key配错或者签名串拼接顺序不对——每个通道的签名规则都不一样,必须对着文档逐字核对。
3.3 回调验签与幂等处理
回调是支付系统里最脆弱的一环。通道会重复推送回调,网络抖动会导致回调丢失,恶意用户可能伪造回调。所以回调接口必须做三件事:验签、幂等、返回正确响应。
from flask import Flask, request import hashlib app = Flask(__name__) @app.route('/notify/wx', methods=['POST']) def wx_notify(): data = request.json # 1. 验签 sign = data.pop('sign') raw = '&'.join(f'{k}={v}' for k, v in sorted(data.items())) raw += f'&key={WX_API_KEY}' expected = hashlib.md5(raw.encode()).hexdigest().upper() if sign != expected: return {'code': 'FAIL', 'msg': 'sign error'} # 2. 幂等:用订单号做唯一索引,重复插入会失败 order_no = data['out_trade_no'] try: db.execute( "INSERT INTO pay_notify (order_no, raw) VALUES (%s, %s)", (order_no, str(data)) ) except DuplicateKeyError: return {'code': 'SUCCESS', 'msg': 'OK'} # 已处理过,直接返回成功 # 3. 更新订单状态 db.execute( "UPDATE orders SET status='PAID' WHERE order_no=%s AND status='PENDING'", (order_no,) ) return {'code': 'SUCCESS', 'msg': 'OK'}逻辑说明:先验签,防止伪造;再用pay_notify表的唯一索引做幂等,重复回调直接返回成功;最后更新订单状态,WHERE status='PENDING'保证只更新一次。参数说明:WX_API_KEY是微信商户平台的API密钥,out_trade_no是商户订单号。返回给通道的响应必须是通道要求的格式,否则通道会一直重推。
4. 代付与卡密系统落地时最容易翻车的五个地方
4.1 回调地址配了内网IP,通道推不过来
现象:本地测试回调正常,部署到服务器后订单一直显示“待付款”,通道后台显示“回调失败”。
原因:notify_url配的是http://192.168.x.x/notify,通道服务器在公网,根本访问不到内网地址。
解决:回调地址必须是公网可达的域名或IP,并且用HTTPS。开发阶段可以用内网穿透工具临时映射,但生产环境必须用正式域名。另外检查防火墙是否放行了回调端口。
4.2 卡密生成用了随机数但没做唯一性校验
现象:批量生成10万张卡密,导入时发现有几张重复,导致核销时出现“一码多用”。
原因:用了random模块而不是secrets,随机性不够;或者生成后没有对数据库做唯一索引。
解决:用secrets.token_hex()生成随机串,数据库对card_hash字段加唯一索引。批量生成时用INSERT IGNORE或ON DUPLICATE KEY UPDATE跳过重复。
4.3 代付链接没有设置过期时间
现象:用户创建代付单后忘了付款,三个月后朋友点开链接还能付,但商品早就下架了。
原因:代付单没有过期机制,状态一直是PENDING。
解决:创建代付单时设置expire_at字段,比如30分钟。用一个定时任务扫描过期订单,把状态改为EXPIRED。付款时先检查expire_at,过期直接拒绝。
4.4 对账文件解析时编码搞错导致金额错乱
现象:对账时发现大量金额不一致,但逐笔核对又没问题。
原因:通道对账文件是GBK编码,代码用UTF-8读取,中文乱码导致字段错位。
解决:先确认通道对账文件的编码格式,用chardet检测或直接问通道技术支持。读取时显式指定编码,解析后用Decimal转换金额。
4.5 聚合支付路由没有降级策略
现象:某个通道故障,所有走该通道的订单全部失败,但系统没有自动切换。
原因:路由引擎只按配置分流,没有健康检查。
解决:给每个通道加健康检查,连续失败N次后自动降级,把流量切到备用通道。降级阈值我一般设5次失败或成功率低于80%持续1分钟。
5. 把三套系统串起来:统一订单中心与灰度上线技巧
三套系统单独跑通之后,下一步是串起来。我一般会做一个统一订单中心,所有代付单、卡密单、聚合支付单都往这里写,用一个biz_type字段区分业务类型。这样做的好处是对账统一、报表统一、风控统一。
-- 统一订单表核心字段 CREATE TABLE unified_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(64) NOT NULL UNIQUE, biz_type ENUM('DAIFU', 'KAMI', 'AGGREGATE') NOT NULL, channel_code VARCHAR(32), amount DECIMAL(12,2) NOT NULL, status VARCHAR(16) NOT NULL, ext JSON, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_biz_status (biz_type, status), INDEX idx_created (created_at) );ext字段用JSON存各业务的扩展信息,比如代付单存代付人ID,卡密单存批次号。索引建在biz_type+status和created_at上,方便按业务和日期查询。
灰度上线时,我习惯先把新系统跟老系统并行跑一段时间,用影子流量验证。具体做法是:新系统接收真实请求但不真正扣款,只记录日志和比对结果。跑一周后看差异率,低于万分之一再切正式流量。切的时候按用户ID哈希分批切,先切1%,观察24小时,没问题再切10%、50%、100%。
最后说一个具体技巧:回调日志一定要存原始报文。我吃过亏,通道说回调成功了,我说没收到,双方扯皮。后来在回调接口第一行就把request.get_data()原样存到日志表,包括请求头和请求体。再遇到争议,直接拿日志说话。这个习惯帮我省了无数扯皮时间。
希望帮到你。
本文还有配套的精品资源,点击获取