这个课的主题很集中:把微信收款码和支付宝收款码合成一张能被两个 App 分别识别的二维码。它不是一个讲支付接口对接、也不涉及商户资质和结算流程的课,而是纯粹的图像与二维码编码技巧。很多人第一次听到"二合一"会以为是某种聚合支付服务,其实不是——它不碰任何支付通道,只是把两张码的信息重新排布到一张图上。理解这一点,才知道这门课真正要解决的问题边界在哪里。
它解决的是一个非常具体的场景:小摊、小店、个人收款时不想贴两张码,或者想在柜台只摆一个立牌。传统做法是把两张码并排放,用户得自己找对应 App。二合一码的思路是让同一张图里同时容纳两条编码信息,微信扫出来是微信的收款链接,支付宝扫出来是支付宝的收款链接。这背后的关键不在支付,而在二维码的容错机制、掩码选择和识别顺序。课程真正值钱的部分,就是把这个"为什么能成立"讲清楚,而不是丢一段代码让你跑。
课纲里真正有含金量的几块
从整体结构看,这门课的重心明显落在三个地方:二维码生成库的调用与参数控制、两张码的物理合并策略、以及桌面端的操作界面。生成部分用的是成熟的二维码库,这部分本身不难,难点在于你要理解版本号、纠错等级、模块尺寸这些参数怎么影响最终结果。纠错等级调高会让码更"抗污损",但模块变多、图变大;调低则相反。合并策略才是核心,常见做法有上下拼接、左右拼接、以及把一张码的有效区域嵌入另一张码的空白区,不同策略的识别成功率差别很大,课里会对比这些方案。
界面部分用的是 Python 自带的图形库,做的是文件选择、预览、保存这类操作。这块含金量中等,属于"能跑起来、能交付给非技术用户"的工程收尾。真正值得反复看的是合并逻辑那几十行,因为它决定了成品到底能不能被扫出来。很多网上流传的所谓二合一码其实是失败的,扫出来识别不了或者只认一家,差别就在合并时有没有处理好静区、有没有破坏定位图案。
适合什么基础的人看
如果你完全没写过 Python,这门课会让你有点吃力。它默认你能看懂函数、能装第三方库、能理解图像对象的基本操作。好在你不需要懂支付业务,也不需要懂网络请求,门槛主要卡在 Python 基础语法和一点图像处理常识上。会一点 PIL 或二维码相关经验的人,看这门课会非常快,可能一两个小时就能把核心逻辑吃透。
反过来说,如果你已经做过二维码生成,只是没试过合并两张码,那这门课对你最大的价值是省掉试错时间。合并这件事看起来简单,实际上静区留多少、两张码的尺寸怎么对齐、用什么方式把信息叠进去,都会影响识别。自己摸索可能要试十几次,课里直接给了可用的参数组合和调整思路。对只想快速做出一个能用的工具的人来说,这就是它最实在的地方。
建议先看哪几块,顺序怎么排
建议不要从头顺着看。先跳到合并逻辑那部分,把"一张图怎么同时装两条信息"这个原理搞明白。原理通了,再看生成参数就很容易理解每个参数在干什么。最后回头看界面部分,因为界面是壳,先看壳容易被细节淹没,抓不住重点。如果时间紧,甚至可以只看合并逻辑加生成参数两块,界面自己照着改都行。
看的时候建议边看边动手改参数。把纠错等级从默认值调高一级,看成品图变成什么样、还能不能扫出来;把两张码的尺寸比例改一改,观察识别率的变化。这种"改一个参数看一个结果"的方式,比单纯看讲解有效得多。二合一码的很多经验是手感,光看文字记不住,必须自己扫几次、失败几次,才能形成判断。
学完之后能独立做什么
最直接的产出是一个能跑的桌面小工具:选两张收款码图片,点一下生成一张合并码,保存下来就能用。你可以把它打包给不装 Python 的人用,也可以改成命令行版本批量处理。更进一步,你能把这个合并思路迁移到别的场景,比如把两张活动码、两张问卷码合成一张,原理是相通的。
但要清楚它的边界:它只做图像层面的合并,不解决支付本身的问题。收款到账、对账、退款这些它一概不管。所以学完不要指望它能帮你做聚合支付业务,它就是一个"把两张图变成一张图、且两张图都还能被认出来"的小工具。认清这一点,你就不会对它有不切实际的期待,也更能判断它值不值得花时间。
资料怎么配合练习用
资料里的代码是完整的可运行版本,建议先原样跑通一次,确认环境没问题、能生成出图。跑通之后再开始拆:把生成部分和合并部分分开,各自单独运行,看中间产物长什么样。中间产物是理解合并逻辑的关键,很多人跳过这一步直接看最终结果,结果出了问题完全不知道是哪一步坏的。把中间图存下来对比,是最高效的调试方式。
练习素材建议自己准备两张真实的收款码,不要用网上随便找的图,因为不同来源的码尺寸、留白、清晰度都不一样,正好用来测试合并逻辑的鲁棒性。生成后用微信和支付宝各扫几次,记录成功和失败的情况,慢慢你就能总结出什么样的参数组合最稳。这套"跑通—拆开—换素材—记录结果"的流程走一遍,比看十遍讲解都管用。
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
二合一支付码生成器 - Python + tkinter 版本
将微信和支付宝收款二维码物理融合为一个二维码
基于github上WeAliPay作者html版的原理,重写核心二维码生成与合并逻辑,PY实现。
"""
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import qrcode
from PIL import Image, ImageTk, ImageDraw, ImageFont
import io
import os
import tempfile
# ──────────────────────────────────────────────
# 核心引擎:生成 & 合并二维码
# ──────────────────────────────────────────────
class QRCodeEngine:
"""二维码生成与合并引擎"""
@staticmethod
def generate_qr(url: str, size: int = 400) -> Image.Image:
"""根据 URL 生成二维码 PIL Image"""
qr = qrcode.QRCode(
version=None,
error_correction=qrcode.constants.ERROR_CORRECT_H,
box_size=10,
border=4,
)
qr.add_data(url)
qr.make(fit=True)
img = qr.make_image(fill_color="black", back_color="white")
img = img.convert("RGB")
img = img.resize((size, size), Image.LANCZOS)
return img
@staticmethod
def merge_qrcodes(
wechat_url: str,
alipay_url: str,
size: int = 400,
title: str = "扫码支付",
) -> Image.Image:
"""
合并微信和支付宝二维码
原理:
1. 微信二维码作为底层完整绘制
2. 支付宝二维码旋转180°后放置到右下角(无定位图案区域)
3. 这样微信扫码读取主码,支付宝扫码识别右下角旋转后的码
"""
# 生成两个二维码
wechat_img = QRCodeEngine.generate_qr(wechat_url, size)
alipay_img = QRCodeEngine.generate_qr(alipay_url, size)
# 支付宝二维码旋转 180°
alipay_rotated = alipay_img.rotate(180)
# 右下角区域:取支付宝二维码的右下角 1/4
half = size // 2
alipay_piece = alipay_rotated.crop((0, 0, half, half))
# 将支付宝小块贴到微信二维码的右下角
merged = wechat_img.copy()
merged.paste(alipay_piece, (half, half))
return merged
@staticmethod
def create_final_image(
qr_img: Image.Image,
title: str = "扫码支付",
) -> Image.Image:
"""
创建最终输出的完整图片,包含:
- 顶部品牌区域(微信 + 支付宝标识)
- 中间二维码
- 底部提示文字
"""
qr_size = qr_img.width
canvas_w = qr_size
top_h = 80
bottom_h = 60
qr_display_size = int(qr_size * 0.6) # 二维码显示区域
padding = (qr_size - qr_display_size) // 2
total_h = top_h + qr_display_size + padding + bottom_h
canvas = Image.new("RGB", (canvas_w, total_h), "white")
draw = ImageDraw.Draw(canvas)
# ── 顶部品牌区域 ──
draw.rectangle([0, 0, canvas_w, top_h], fill="white")
# 微信图标 (绿色方块)
icon_size = 60
icon_gap = 20
total_icons_w = icon_size * 2 + icon_gap
start_x = (canvas_w - total_icons_w) // 2
icon_y = (top_h - icon_size) // 2
draw.rounded_rectangle(
[start_x, icon_y, start_x + icon_size, icon_y + icon_size],
radius=12, fill="#07C160"
)
draw.rounded_rectangle(
[start_x + icon_size + icon_gap, icon_y,
start_x + icon_size * 2 + icon_gap, icon_y + icon_size],
radius=12, fill="#1677FF"
)
# 图标文字
try:
font_icon = ImageFont.truetype("msyh.ttc", 28)
except Exception:
font_icon = ImageFont.load_default()
draw.text(
(start_x + icon_size // 2, icon_y + icon_size // 2),
"W", fill="white", font=font_icon, anchor="mm"
)
draw.text(
(start_x + icon_size + icon_gap + icon_size // 2, icon_y + icon_size // 2),
"A", fill="white", font=font_icon, anchor="mm"
)
# ── 二维码区域 ──
qr_y = top_h
draw.rectangle(
[padding, qr_y, padding + qr_display_size, qr_y + qr_display_size],
fill="white", outline="#EEEEEE", width=1
)
# 缩放二维码并粘贴
qr_display = qr_img.resize((qr_display_size, qr_display_size), Image.LANCZOS)
canvas.paste(qr_display, (padding, qr_y))
# ── 底部提示 ──
bottom_y = qr_y + qr_display_size + padding
try:
font_bottom = ImageFont.truetype("msyh.ttc", 16)
except Exception:
font_bottom = ImageFont.load_default()
draw.text(
(canvas_w // 2, bottom_y + bottom_h // 2),
"付款时请核实账户是否正确",
fill="#333333", font=font_bottom, anchor="mm"
)
return canvas
# ──────────────────────────────────────────────
# tkinter GUI 主界面
# ──────────────────────────────────────────────
class WeAliPayApp:
def __init__(self, root: tk.Tk):
self.root = root
self.root.title("微信支付宝二合一支付码生成器")
self.root.geometry("960x720")
self.root.minsize(800, 600)
self.root.configure(bg="#F5F5F5")
self.merged_qr: Image.Image | None = None
self.final_image: Image.Image | None = None
self._setup_styles()
self._build_ui()
# ── 样式 ──
def _setup_styles(self):
style = ttk.Style()
style.theme_use("clam")
style.configure("Primary.TButton", font=("微软雅黑", 11, "bold"),
background="#165DFF", foreground="white", borderwidth=0,
padding=10)
style.map("Primary.TButton", background=[("active", "#0040C9")])
style.configure("Green.TButton", font=("微软雅黑", 10),
background="#00B42A", foreground="white", borderwidth=0,
padding=8)
style.map("Green.TButton", background=[("active", "#00871A")])
style.configure("TLabel", font=("微软雅黑", 10), background="#F5F5F5")
style.configure("Title.TLabel", font=("微软雅黑", 20, "bold"),
background="#F5F5F5", foreground="#1A1A1A")
# ── UI 构建 ──
def _build_ui(self):
# 主容器
main_frame = tk.Frame(self.root, bg="#F5F5F5")
main_frame.pack(fill=tk.BOTH, expand=True, padx=20, pady=15)
# 标题栏
header = tk.Frame(main_frame, bg="#F5F5F5")
header.pack(fill=tk.X, pady=(0, 15))
tk.Label(header, text="🔲 二合一支付码生成器",
font=("微软雅黑", 22, "bold"), bg="#F5F5F5", fg="#1A1A1A").pack(side=tk.LEFT)
# 左右分栏
panes = tk.PanedWindow(main_frame, orient=tk.HORIZONTAL, bg="#E0E0E0", sashwidth=3)
panes.pack(fill=tk.BOTH, expand=True)
left_panel = tk.Frame(panes, bg="white", bd=0, highlightthickness=0)
right_panel = tk.Frame(panes, bg="white", bd=0, highlightthickness=0)
panes.add(left_panel, width=420)
panes.add(right_panel, width=500)
self._build_left_panel(left_panel)
self._build_right_panel(right_panel)
# ── 左侧:输入区 ──
def _build_left_panel(self, parent: tk.Frame):
pad = {"padx": 20, "pady": 5}
# 滚动容器
canvas = tk.Canvas(parent, bg="white", highlightthickness=0)
scrollbar = ttk.Scrollbar(parent, orient=tk.VERTICAL, command=canvas.yview)
scroll_frame = tk.Frame(canvas, bg="white")
scroll_frame.bind("<Configure>", lambda e: canvas.configure(scrollregion=canvas.bbox("all")))
canvas.create_window((0, 0), window=scroll_frame, anchor="nw")
canvas.configure(yscrollcommand=scrollbar.set)
canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
# 绑定鼠标滚轮
def _on_mousewheel(event):
canvas.yview_scroll(int(-1 * (event.delta / 120)), "units")
canvas.bind_all("<MouseWheel>", _on_mousewheel)
# ── 收款信息标题 ──
tk.Label(scroll_frame, text="📋 收款信息", font=("微软雅黑", 14, "bold"),
bg="white", fg="#333").pack(anchor=tk.W, **pad)
# 微信收款链接
tk.Label(scroll_frame, text="微信收款链接", bg="white",
fg="#555").pack(anchor=tk.W, padx=20, pady=(15, 2))
wx_frame = tk.Frame(scroll_frame, bg="white")
wx_frame.pack(fill=tk.X, padx=20)
self.wechat_url_var = tk.StringVar()
tk.Entry(wx_frame, textvariable=self.wechat_url_var, font=("微软雅黑", 10),
relief=tk.SOLID, bd=1).pack(side=tk.LEFT, fill=tk.X, expand=True, ipady=4)
tk.Button(wx_frame, text="上传", bg="#07C160", fg="white", font=("微软雅黑", 9),
relief=tk.FLAT, padx=10, cursor="hand2",
command=lambda: self._upload_qrcode("wechat")).pack(side=tk.RIGHT, padx=(5, 0))
# 微信提示
tk.Label(scroll_frame, text="打开微信 → 收付款 → 二维码收款 → 保存图片 → 扫一扫识别 → 复制链接",
bg="white", fg="#999", font=("微软雅黑", 8)).pack(anchor=tk.W, padx=20, pady=(2, 10))
# 支付宝收款链接
tk.Label(scroll_frame, text="支付宝收款链接", bg="white",
fg="#555").pack(anchor=tk.W, padx=20, pady=(5, 2))
ali_frame = tk.Frame(scroll_frame, bg="white")
ali_frame.pack(fill=tk.X, padx=20)
self.alipay_url_var = tk.StringVar()
tk.Entry(ali_frame, textvariable=self.alipay_url_var, font=("微软雅黑", 10),
relief=tk.SOLID, bd=1).pack(side=tk.LEFT, fill=tk.X, expand=True, ipady=4)
tk.Button(ali_frame, text="上传", bg="#1677FF", fg="white", font=("微软雅黑", 9),
relief=tk.FLAT, padx=10, cursor="hand2",
command=lambda: self._upload_qrcode("alipay")).pack(side=tk.RIGHT, padx=(5, 0))
# 支付宝提示
tk.Label(scroll_frame, text="打开支付宝 → 收钱 → 保存图片 → 扫一扫识别 → 复制链接",
bg="white", fg="#999", font=("微软雅黑", 8)).pack(anchor=tk.W, padx=20, pady=(2, 10))
# ── 分隔线 ──
ttk.Separator(scroll_frame, orient=tk.HORIZONTAL).pack(fill=tk.X, padx=20, pady=10)
# ── 样式设置 ──
tk.Label(scroll_frame, text="🎨 样式设置", font=("微软雅黑", 14, "bold"),
bg="white", fg="#333").pack(anchor=tk.W, **pad)
# 二维码尺寸
tk.Label(scroll_frame, text="二维码尺寸", bg="white",
fg="#555").pack(anchor=tk.W, padx=20, pady=(10, 2))
self.size_var = tk.StringVar(value="400")
size_combo = ttk.Combobox(scroll_frame, textvariable=self.size_var,
values=["300", "400", "500"],
state="readonly", font=("微软雅黑", 10))
size_combo.pack(fill=tk.X, padx=20, ipady=2)
# 合并模式
tk.Label(scroll_frame, text="合并模式", bg="white",
fg="#555").pack(anchor=tk.W, padx=20, pady=(10, 2))
self.mode_var = tk.StringVar(value="overlay")
mode_combo = ttk.Combobox(scroll_frame, textvariable=self.mode_var,
values=["overlay 重叠模式", "split 分割模式", "checkerboard 棋盘模式"],
state="readonly", font=("微软雅黑", 10))
mode_combo.pack(fill=tk.X, padx=20, ipady=2)
# 自定义标题
tk.Label(scroll_frame, text="自定义标题", bg="white",
fg="#555").pack(anchor=tk.W, padx=20, pady=(10, 2))
self.title_var = tk.StringVar(value="扫码支付")
tk.Entry(scroll_frame, textvariable=self.title_var, font=("微软雅黑", 10),
relief=tk.SOLID, bd=1).pack(fill=tk.X, padx=20, ipady=4)
# ── 生成按钮 ──
tk.Button(scroll_frame, text="✨ 生成二合一支付码",
bg="#165DFF", fg="white", font=("微软雅黑", 13, "bold"),
relief=tk.FLAT, cursor="hand2", pady=12,
command=self._generate).pack(fill=tk.X, padx=20, pady=(25, 8))
tk.Label(scroll_frame, text="注意:收款信息仅用于生成,不会被存储",
bg="white", fg="#AAA", font=("微软雅黑", 8)).pack(pady=(0, 15))
# ── 右侧:预览区 ──
def _build_right_panel(self, parent: tk.Frame):
tk.Label(parent, text="👁 预览与下载", font=("微软雅黑", 14, "bold"),
bg="white", fg="#333").pack(anchor=tk.W, padx=20, pady=(15, 10))
# 预览画布
self.preview_canvas = tk.Canvas(parent, bg="#FAFAFA", highlightthickness=1,
highlightbackground="#E0E0E0")
self.preview_canvas.pack(fill=tk.BOTH, expand=True, padx=20, pady=(0, 10))
# 占位提示
self.preview_canvas.create_text(
230, 200, text="请输入微信和支付宝收款链接\n然后点击「生成二合一支付码」",
font=("微软雅黑", 11), fill="#BBB", justify=tk.CENTER
)
# 下载按钮区域
btn_frame = tk.Frame(parent, bg="white")
btn_frame.pack(fill=tk.X, padx=20, pady=(0, 15))
tk.Button(btn_frame, text="📥 下载 PNG",
bg="#00B42A", fg="white", font=("微软雅黑", 11),
relief=tk.FLAT, cursor="hand2", pady=8,
command=lambda: self._download("png")).pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 5))
tk.Button(btn_frame, text="📥 下载 JPG",
bg="#FF7D00", fg="white", font=("微软雅黑", 11),
relief=tk.FLAT, cursor="hand2", pady=8,
command=lambda: self._download("jpg")).pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(5, 0))
# ── 上传二维码 ──
def _upload_qrcode(self, target: str):
path = filedialog.askopenfilename(
title="选择二维码图片",
filetypes=[("图片文件", "*.png *.jpg *.jpeg *.bmp"), ("所有文件", "*.*")]
)
if not path:
return
try:
from PIL import Image as PILImage
img = PILImage.open(path)
# 尝试用 pyzbar 解析二维码
try:
from pyzbar.pyzbar import decode
results = decode(img)
if results:
url = results[0].data.decode("utf-8")
if target == "wechat":
self.wechat_url_var.set(url)
else:
self.alipay_url_var.set(url)
messagebox.showinfo("识别成功", f"已识别链接:\n{url}")
else:
messagebox.showwarning("未识别", "未能从图片中识别出二维码,请手动输入链接。")
except ImportError:
messagebox.showinfo("提示",
"未安装 pyzbar 库,无法自动解析二维码。\n"
"请手动输入链接,或运行: pip install pyzbar")
except Exception as e:
messagebox.showerror("错误", f"读取图片失败: {e}")
# ── 生成 ──
def _generate(self):
wechat_url = self.wechat_url_var.get().strip()
alipay_url = self.alipay_url_var.get().strip()
title = self.title_var.get().strip() or "扫码支付"
if not wechat_url or not alipay_url:
messagebox.showwarning("提示", "请输入微信和支付宝收款链接")
return
try:
size = int(self.size_var.get())
except ValueError:
size = 400
mode_raw = self.mode_var.get()
# 目前以重叠模式为主,其他模式可扩展
if "split" in mode_raw:
mode = "split"
elif "checkerboard" in mode_raw:
mode = "checkerboard"
else:
mode = "overlay"
# 显示加载状态
self.preview_canvas.delete("all")
self.preview_canvas.create_text(
230, 200, text="⏳ 正在生成,请稍候...",
font=("微软雅黑", 12), fill="#999"
)
self.root.update_idletasks()
try:
if mode == "overlay":
merged = QRCodeEngine.merge_qrcodes(wechat_url, alipay_url, size, title)
elif mode == "split":
merged = self._merge_split(wechat_url, alipay_url, size)
else:
merged = self._merge_checkerboard(wechat_url, alipay_url, size)
self.merged_qr = merged
self.final_image = QRCodeEngine.create_final_image(merged, title)
self._show_preview()
except Exception as e:
self.preview_canvas.delete("all")
self.preview_canvas.create_text(
230, 200, text=f"❌ 生成失败\n{str(e)[:60]}",
font=("微软雅黑", 11), fill="#E74C3C", justify=tk.CENTER
)
def _merge_split(self, wx_url: str, ali_url: str, size: int) -> Image.Image:
"""分割模式:左右各一半"""
wx_img = QRCodeEngine.generate_qr(wx_url, size)
ali_img = QRCodeEngine.generate_qr(ali_url, size)
half = size // 2
merged = Image.new("RGB", (size, size), "white")
merged.paste(wx_img.crop((0, 0, half, size)), (0, 0))
merged.paste(ali_img.crop((half, 0, size, size)), (half, 0))
return merged
def _merge_checkerboard(self, wx_url: str, ali_url: str, size: int) -> Image.Image:
"""棋盘模式:8x8 格子交替"""
wx_img = QRCodeEngine.generate_qr(wx_url, size)
ali_img = QRCodeEngine.generate_qr(ali_url, size)
merged = Image.new("RGB", (size, size), "white")
grid = 8
cell = size // grid
for r in range(grid):
for c in range(grid):
src = wx_img if (r + c) % 2 == 0 else ali_img
box = (c * cell, r * cell, (c + 1) * cell, (r + 1) * cell)
merged.paste(src.crop(box), box)
return merged
# ── 预览显示 ──
def _show_preview(self):
if self.final_image is None:
return
self.preview_canvas.delete("all")
cw = self.preview_canvas.winfo_width()
ch = self.preview_canvas.winfo_height()
if cw < 10:
cw = 480
if ch < 10:
ch = 500
# 缩放适应画布
img = self.final_image.copy()
iw, ih = img.size
scale = min((cw - 20) / iw, (ch - 20) / ih, 1.0)
new_w, new_h = int(iw * scale), int(ih * scale)
img = img.resize((new_w, new_h), Image.LANCZOS)
self._preview_tk = ImageTk.PhotoImage(img)
x = (cw - new_w) // 2
y = (ch - new_h) // 2
self.preview_canvas.create_image(x, y, anchor=tk.NW, image=self._preview_tk)
# ── 下载 ──
def _download(self, fmt: str):
if self.final_image is None:
messagebox.showwarning("提示", "请先生成二维码")
return
path = filedialog.asksaveasfilename(
title=f"保存为 {fmt.upper()}",
defaultextension=f".{fmt}",
filetypes=[(f"{fmt.upper()} 文件", f"*.{fmt}")]
)
if not path:
return
try:
if fmt == "jpg":
self.final_image.convert("RGB").save(path, "JPEG", quality=95)
else:
self.final_image.save(path, "PNG")
messagebox.showinfo("成功", f"已保存到:\n{path}")
except Exception as e:
messagebox.showerror("错误", f"保存失败: {e}")
# ──────────────────────────────────────────────
# 入口
# ──────────────────────────────────────────────
def main():
root = tk.Tk()
app = WeAliPayApp(root)
# 窗口居中
root.update_idletasks()
sw = root.winfo_screenwidth()
sh = root.winfo_screenheight()
w = root.winfo_width()
h = root.winfo_height()
x = (sw - w) // 2
y = (sh - h) // 2
root.geometry(f"+{x}+{y}")
root.mainloop()
if __name__ == "__main__":
main()


