深夜提醒

现在是深夜,建议您注意休息,不要熬夜哦~

🏮 🏮 🏮

新年快乐

祝君万事如意心想事成!

2024 桐庐半程马拉松
00:00:00
时间
0.00
距离(公里)
--:--
配速
--
步频
--
心率 (bpm)
--
配速
步频
|
share-image
ESC

Chrome 扩展接入支付宝当面付:从 0 到 1 实现技术变现

Chrome Extension Payment Flow

前言

作为一名独立开发者,我开发了一款 Chrome 扩展 WeRead Sync Pro——帮助用户将微信读书笔记同步到 Notion、Obsidian 等笔记平台。随着用户增长,如何实现技术变现成为一个现实问题。

本文将详细分享如何为 Chrome 扩展接入 支付宝当面付,实现扫码支付功能,让你的技术产品真正产生收入。

一、为什么选择支付宝当面付?

在众多支付方案中,支付宝当面付有以下优势:

方案 接入难度 费率 适用场景
支付宝当面付 ⭐⭐⭐ 0.6% 线上扫码支付
微信支付 JSAPI ⭐⭐⭐⭐⭐ 0.6% 需微信环境
第三方支付聚合 ⭐⭐ 1%+ 快速接入但成本高
海外支付(Stripe) ⭐⭐⭐ 3%+ 海外用户

当面付特别适合 Chrome 扩展场景

  • 用户点击购买 → 弹出二维码 → 扫码支付 → 自动激活
  • 无需跳转外部页面,体验流畅
  • 官方直连,资金安全

二、系统架构设计

2.1 整体流程

┌─────────────────┐     ┌─────────────────┐     ┌─────────────────┐
│ Chrome 扩展 │────▶│ 后端服务器 │────▶│ 支付宝开放平台 │
│ (payment.js) │ │ (Go + MySQL) │ │ (当面付 API) │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ │ │
│ │ │
▼ ▼ ▼
用户扫码支付 生成订单/授权码 异步通知支付结果

2.2 核心交互时序

sequenceDiagram
participant U as 用户
participant E as Chrome扩展
participant S as 后端服务
participant A as 支付宝

U->>E: 点击购买套餐
E->>S: POST /payment/create
S->>A: 创建支付宝订单
A-->>S: 返回二维码链接
S-->>E: 返回订单信息
E->>U: 展示支付二维码
U->>A: 扫码支付
A->>S: 异步通知支付成功
S->>S: 生成授权码
S->>U: 邮件发送授权码
E->>S: 轮询订单状态
S-->>E: 返回已支付+授权码
E->>U: 自动激活扩展

三、支付宝开放平台配置

3.1 创建应用

登录 支付宝开放平台,按以下步骤操作:

  1. 创建应用

    • 应用名称:WeRead Sync Pro
    • 应用类型:网页/移动应用
    • 应用图标:上传 256x256 PNG
  2. 添加能力

    • 搜索「当面付」并添加
    • 提交审核(通常 1-2 个工作日通过)

3.2 配置密钥(核心步骤)

支付宝当面付使用 RSA2 非对称加密,需要生成密钥对:

# 使用 OpenSSL 生成密钥对(推荐)
openssl genrsa -out app_private_key.pem 2048
openssl rsa -in app_private_key.pem -pubout -out app_public_key.pem

# 查看私钥(PEM 格式)
cat app_private_key.pem

# 查看公钥
openssl rsa -in app_private_key.pem -pubout -outform PEM

关键概念区分

  • 应用私钥:你的服务器用来签名请求,绝对保密
  • 应用公钥:上传到支付宝,用于支付宝验证你的请求
  • 支付宝公钥:支付宝给你用来验证支付宝回调,在开放平台获取

3.3 配置回调地址

应用网关: https://your-domain.com/api/v1/payment/notify
授权回调地址: https://your-domain.com/api/v1/payment/return

⚠️ 重要:必须使用 HTTPS,且域名需备案

四、后端服务实现(Go)

4.1 项目结构

weread-payment-server/
├── cmd/
│ └── server/
│ └── main.go # 入口
├── internal/
│ ├── config/ # 配置管理
│ ├── handler/
│ │ └── payment.go # HTTP 处理器
│ ├── payment/
│ │ └── alipay.go # 支付宝 SDK 封装
│ ├── service/
│ │ └── order.go # 订单业务逻辑
│ └── models/
│ └── order.go # 数据模型
├── configs/
│ └── config.yaml # 配置文件
└── go.mod

4.2 支付宝 SDK 封装

// internal/payment/alipay.go
package payment

import (
"crypto/rsa"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"fmt"
"net/url"
"sort"
"strings"

"github.com/go-resty/resty/v2"
)

// Client 支付宝客户端
type Client struct {
AppID string
PrivateKey *rsa.PrivateKey
AlipayPublicKey *rsa.PublicKey
GatewayURL string
NotifyURL string
ReturnURL string
Sandbox bool
}

// NewClient 创建支付宝客户端
func NewClient(appID, privateKeyPEM, alipayPublicKeyPEM string, sandbox bool) (*Client, error) {
// 解析应用私钥
block, _ := pem.Decode([]byte(privateKeyPEM))
if block == nil {
return nil, fmt.Errorf("私钥格式错误")
}
privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
return nil, fmt.Errorf("解析私钥失败: %w", err)
}

// 解析支付宝公钥
block, _ = pem.Decode([]byte(alipayPublicKeyPEM))
if block == nil {
return nil, fmt.Errorf("公钥格式错误")
}
pubInterface, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
return nil, fmt.Errorf("解析公钥失败: %w", err)
}
publicKey, ok := pubInterface.(*rsa.PublicKey)
if !ok {
return nil, fmt.Errorf("不是有效的 RSA 公钥")
}

gateway := "https://openapi.alipay.com/gateway.do"
if sandbox {
gateway = "https://openapi-sandbox.dl.alipaydev.com/gateway.do"
}

return &Client{
AppID: appID,
PrivateKey: privateKey,
AlipayPublicKey: publicKey,
GatewayURL: gateway,
Sandbox: sandbox,
}, nil
}

// CreateOrderRequest 创建订单请求
type CreateOrderRequest struct {
OutTradeNo string
Subject string
TotalAmount float64
}

// CreateOrderResponse 创建订单响应
type CreateOrderResponse struct {
Code string `json:"code"`
Msg string `json:"msg"`
QRCode string `json:"qr_code"` // 二维码内容
}

// CreateOrder 创建当面付订单
func (c *Client) CreateOrder(req *CreateOrderRequest) (*CreateOrderResponse, error) {
bizContent := fmt.Sprintf(`{
"out_trade_no": "%s",
"total_amount": "%.2f",
"subject": "%s",
"product_code": "FACE_TO_FACE_PAYMENT"
}`, req.OutTradeNo, req.TotalAmount, req.Subject)

params := map[string]string{
"app_id": c.AppID,
"method": "alipay.trade.precreate",
"format": "JSON",
"charset": "utf-8",
"sign_type": "RSA2",
"timestamp": time.Now().Format("2006-01-02 15:04:05"),
"version": "1.0",
"notify_url": c.NotifyURL,
"biz_content": bizContent,
}

// 生成签名
sign, err := c.sign(params)
if err != nil {
return nil, err
}
params["sign"] = sign

// 发送请求
client := resty.New()
resp, err := client.R().
SetFormData(params).
Post(c.GatewayURL)

if err != nil {
return nil, fmt.Errorf("请求支付宝失败: %w", err)
}

// 解析响应...
return parseResponse(resp.Body())
}

// sign 生成 RSA2 签名
func (c *Client) sign(params map[string]string) (string, error) {
// 1. 过滤空值和 sign 字段
filtered := make(map[string]string)
for k, v := range params {
if k != "sign" && v != "" {
filtered[k] = v
}
}

// 2. 按键排序
var keys []string
for k := range filtered {
keys = append(keys, k)
}
sort.Strings(keys)

// 3. 拼接成字符串
var parts []string
for _, k := range keys {
parts = append(parts, fmt.Sprintf("%s=%s", k, filtered[k]))
}
content := strings.Join(parts, "&")

// 4. SHA256WithRSA 签名
hash := sha256.Sum256([]byte(content))
signature, err := rsa.SignPKCS1v15(
rand.Reader,
c.PrivateKey,
crypto.SHA256,
hash[:],
)
if err != nil {
return "", err
}

return base64.StdEncoding.EncodeToString(signature), nil
}

4.3 订单服务实现

// internal/service/order.go
package service

type OrderService struct {
db *gorm.DB
alipay *payment.Client
email *email.Client
}

// CreateOrder 创建订单
func (s *OrderService) CreateOrder(req *CreateOrderRequest) (*CreateOrderResponse, error) {
// 1. 获取套餐配置(价格由后端控制)
plan, ok := s.config.Pricing.GetPlan(req.PlanType)
if !ok {
return nil, fmt.Errorf("无效的套餐类型: %s", req.PlanType)
}

// 2. 生成订单号(带前缀便于识别)
orderNo := utils.GenerateOrderNo() // e.g., WEREAD2024031812304512345
subject := fmt.Sprintf("WeRead Sync Pro - %s", plan.Name)

// 3. 调用支付宝创建订单
aliResp, err := s.alipay.CreateOrder(&payment.CreateOrderRequest{
OutTradeNo: orderNo,
Subject: subject,
TotalAmount: plan.Price,
})
if err != nil {
return nil, fmt.Errorf("创建支付宝订单失败: %w", err)
}

// 4. 保存到数据库
order := &models.Order{
OrderNo: orderNo,
Status: models.OrderStatusPending,
PlanType: models.PlanType(req.PlanType),
Amount: plan.Price,
Subject: subject,
BuyerEmail: req.BuyerEmail,
}

if err := s.db.Create(order).Error; err != nil {
return nil, fmt.Errorf("保存订单失败: %w", err)
}

// 5. 返回二维码链接
return &CreateOrderResponse{
OrderNo: orderNo,
QRCodeURL: aliResp.QRCode, // 支付宝返回的二维码链接
Amount: plan.Price,
ExpireTime: time.Now().Add(30 * time.Minute).Unix(),
}, nil
}

// HandleAlipayNotify 处理支付宝异步通知
func (s *OrderService) HandleAlipayNotify(notifyData map[string]string) error {
// 1. 验证签名(防止伪造通知)
ok, err := s.alipay.VerifyNotify(notifyData)
if err != nil || !ok {
return fmt.Errorf("签名验证失败")
}

// 2. 解析通知数据
tradeStatus := notifyData["trade_status"]
orderNo := notifyData["out_trade_no"]
tradeNo := notifyData["trade_no"] // 支付宝交易号
buyerEmail := notifyData["buyer_email"]

// 3. 查找订单
var order models.Order
if err := s.db.Where("order_no = ?", orderNo).First(&order).Error; err != nil {
return fmt.Errorf("订单不存在: %s", orderNo)
}

// 4. 幂等性检查:已处理的直接返回
if order.Status == models.OrderStatusPaid {
return nil
}

// 5. 处理支付成功
if tradeStatus == "TRADE_SUCCESS" {
return s.handlePaymentSuccess(&order, tradeNo, buyerEmail)
}

return nil
}

// handlePaymentSuccess 支付成功处理
func (s *OrderService) handlePaymentSuccess(order *models.Order, tradeNo, buyerEmail string) error {
// 1. 生成授权码
licenseKey := utils.GenerateLicenseKey(
s.config.License.Prefix, // e.g., "WS"
string(order.PlanType), // e.g., "yearly"
s.config.License.Secret, // 签名密钥
)

// 2. 更新订单状态
now := time.Now()
order.Status = models.OrderStatusPaid
order.AlipayTradeNo = tradeNo
order.PaidAt = &now
order.LicenseKey = licenseKey
if buyerEmail != "" {
order.BuyerEmail = buyerEmail
}

if err := s.db.Save(order).Error; err != nil {
return err
}

// 3. 创建授权记录
plan, _ := s.config.Pricing.GetPlan(string(order.PlanType))
license := &models.License{
LicenseKey: licenseKey,
PlanType: order.PlanType,
Status: "unused",
TotalDays: plan.Days,
OrderID: order.ID,
}
s.db.Create(license)

// 4. 异步发送邮件通知
if s.email != nil && order.BuyerEmail != "" {
go s.email.SendLicenseEmail(order.BuyerEmail, licenseKey, string(order.PlanType))
}

return nil
}

4.4 签名验证详解

支付宝回调的签名验证是安全的核心:

// VerifyNotify 验证支付宝通知签名
func (c *Client) VerifyNotify(params map[string]string) (bool, error) {
sign := params["sign"]
signType := params["sign_type"]

// 1. 移除 sign 和 sign_type
filtered := make(map[string]string)
for k, v := range params {
if k != "sign" && k != "sign_type" && v != "" {
filtered[k] = v
}
}

// 2. 拼接待验签字符串
var keys []string
for k := range filtered {
keys = append(keys, k)
}
sort.Strings(keys)

var parts []string
for _, k := range keys {
parts = append(parts, fmt.Sprintf("%s=%s", k, filtered[k]))
}
content := strings.Join(parts, "&")

// 3. 验签
signBytes, _ := base64.StdEncoding.DecodeString(sign)
hash := sha256.Sum256([]byte(content))

err := rsa.VerifyPKCS1v15(
c.AlipayPublicKey,
crypto.SHA256,
hash[:],
signBytes,
)

return err == nil, nil
}

五、前端集成(Chrome 扩展)

5.1 支付页面实现

// payment.js - 支付客户端

const PAYMENT_API_BASE = 'https://pay.awen.me/api/v1';

/**
* 创建订单并显示二维码
*/
async function initiatePayment(planType, buyerEmail) {
try {
// 1. 创建订单
const response = await fetch(`${PAYMENT_API_BASE}/payment/create`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Extension-ID': chrome.runtime.id
},
body: JSON.stringify({
plan_type: planType,
buyer_email: buyerEmail
})
});

const result = await response.json();
if (result.code !== 0) {
throw new Error(result.message);
}

const orderData = result.data;

// 2. 显示二维码
showQRCode(orderData);

// 3. 开始轮询订单状态
startPolling(orderData.order_no);

} catch (error) {
console.error('支付初始化失败:', error);
showError(error.message);
}
}

/**
* 显示二维码
*/
function showQRCode(orderData) {
const qrcodeContainer = document.getElementById('qrcode');

// 使用 QRCode.js 生成二维码
new QRCode(qrcodeContainer, {
text: orderData.qrcode_url, // 支付宝返回的二维码链接
width: 200,
height: 200,
colorDark: '#000000',
colorLight: '#ffffff',
correctLevel: QRCode.CorrectLevel.H
});

// 显示订单信息
document.getElementById('order-no').textContent = orderData.order_no;
document.getElementById('amount').textContent = ${orderData.amount}`;
}

/**
* 轮询订单状态
*/
function startPolling(orderNo) {
const pollInterval = setInterval(async () => {
try {
const response = await fetch(
`${PAYMENT_API_BASE}/payment/order/${orderNo}`
);
const result = await response.json();

if (result.code === 0 && result.data.status === 'paid') {
clearInterval(pollInterval);

// 保存授权码
await saveLicenseKey(result.data.license_key);

// 显示支付成功
showPaymentSuccess(result.data.license_key);
}
} catch (error) {
console.error('查询订单状态失败:', error);
}
}, 3000); // 每 3 秒查询一次

// 30 分钟后停止轮询
setTimeout(() => clearInterval(pollInterval), 30 * 60 * 1000);
}

/**
* 保存授权码到 Chrome Storage
*/
async function saveLicenseKey(licenseKey) {
await chrome.storage.local.set({
'license_key': licenseKey,
'license_activated': true,
'license_activated_at': Date.now()
});
}

5.2 优雅的二维码展示

<!-- pricing.html -->
<div class="payment-modal" id="paymentModal">
<div class="modal-content">
<h3>扫码支付</h3>
<p class="plan-name">年度订阅</p>
<p class="amount">¥48</p>

<!-- 邮箱输入 -->
<div class="email-form" id="emailForm">
<input type="email" id="email" placeholder="请输入邮箱接收授权码" />
<button onclick="submitEmail()">确认并生成二维码</button>
</div>

<!-- 二维码区域 -->
<div class="qrcode-container hidden" id="qrcodeContainer">
<div id="qrcode"></div>
<p class="tip">请使用支付宝扫码支付</p>
</div>

<!-- 支付成功 -->
<div class="success-container hidden" id="successContainer">
<div class="checkmark"></div>
<p>支付成功!</p>
<p class="license-key" id="licenseKey"></p>
<p class="tip">授权码已发送到您的邮箱</p>
</div>
</div>
</div>

5.3 manifest.json 配置

{
"manifest_version": 3,
"name": "WeRead Sync Pro",
"version": "1.1.0",
"permissions": [
"storage",
"activeTab"
],
"host_permissions": [
"https://weread.qq.com/*",
"https://pay.awen.me/*"
],
"web_accessible_resources": [
{
"resources": ["pricing.html", "payment.js"],
"matches": ["<all_urls>"]
}
]
}

六、安全注意事项

6.1 密钥管理

# ❌ 错误:将密钥提交到 Git
config.yaml # 包含支付宝密钥

# ✅ 正确:使用环境变量
export ALIPAY_APP_ID="2024XXXXXX"
export ALIPAY_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----..."
export LICENSE_SECRET="random-secret-key"

6.2 防重放攻击

// 检查通知是否已处理(幂等性)
func (s *OrderService) isNotifyProcessed(tradeNo string) bool {
var count int64
s.db.Model(&models.PaymentLog{}).
Where("trade_no = ? AND type = 'notify'", tradeNo).
Count(&count)
return count > 0
}

6.3 HTTPS 强制

# nginx.conf - 强制 HTTPS
server {
listen 80;
server_name pay.awen.me;
return 301 https://$server_name$request_uri;
}

server {
listen 443 ssl http2;
server_name pay.awen.me;

ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;

# 安全头部
add_header Strict-Transport-Security "max-age=31536000" always;
}

七、部署上线

7.1 使用 Docker 部署

# Dockerfile
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY . .
RUN go build -o server cmd/server/main.go

FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /app/server .
COPY --from=builder /app/configs ./configs
EXPOSE 8085
CMD ["./server"]
# docker-compose.yml
version: '3.8'
services:
app:
build: .
ports:
- "8085:8085"
environment:
- DB_HOST=mysql
- ALIPAY_APP_ID=${ALIPAY_APP_ID}
- ALIPAY_PRIVATE_KEY=${ALIPAY_PRIVATE_KEY}
depends_on:
- mysql

mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
MYSQL_DATABASE: weread_payment
volumes:
- mysql_data:/var/lib/mysql

volumes:
mysql_data:

7.2 监控与日志

# 查看实时日志
docker logs -f weread-payment-server

# 查看支付统计
mysql -e "
SELECT
DATE(created_at) as date,
COUNT(*) as orders,
SUM(amount) as revenue
FROM orders
WHERE status = 'paid'
GROUP BY DATE(created_at)
ORDER BY date DESC;
"

八、总结

通过本文,我们完整实现了 Chrome 扩展的支付宝当面付集成:

  1. 支付宝配置:创建应用、生成密钥、配置回调
  2. 后端开发:订单创建、签名验证、异步通知处理
  3. 前端集成:二维码展示、状态轮询、授权码管理
  4. 安全加固:密钥管理、幂等性、HTTPS

关键代码仓库

下一步优化

  • 添加优惠码系统
  • 实现自动退款接口
  • 接入微信扫码支付
  • 添加支付数据分析面板

希望本文对你有帮助!如果有问题,欢迎在评论区留言交流。

本文首发于 awen.me,转载请注明出处。

文章作者:阿文
文章链接: https://www.awen.me/post/8fee5356.html
版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 阿文的博客

评论

0 条评论
😀😃😄 😁😅😂 🤣😊😇 🙂🙃😉 😌😍🥰 😘😗😙 😚😋😛 😝😜🤪 🤨🧐🤓 😎🥸🤩 🥳😏😒 😞😔😟 😕🙁☹️ 😣😖😫 😩🥺😢 😭😤😠 😡🤬🤯 😳🥵🥶 😱😨😰 😥😓🤗 🤔🤭🤫 🤥😶😐 😑😬🙄 😯😦😧 😮😲🥱 😴🤤😪 😵🤐🥴 🤢🤮🤧 😷🤒🤕 🤑🤠😈 👿👹👺 🤡💩👻 💀☠️👽 👾🤖🎃 😺😸😹 😻😼😽 🙀😿😾 👍👎👏 🙌👐🤲 🤝🤜🤛 ✌️🤞🤟 🤘👌🤏 👈👉👆 👇☝️ 🤚🖐️🖖 👋🤙💪 🦾🖕✍️ 🙏💅🤳 💯💢💥 💫💦💨 🕳️💣💬 👁️‍🗨️🗨️🗯️ 💭💤❤️ 🧡💛💚 💙💜🖤 🤍🤎💔 ❣️💕💞 💓💗💖 💘💝💟 ☮️✝️☪️ 🕉️☸️✡️ 🔯🕎☯️ ☦️🛐 🆔⚛️🉑 ☢️☣️📴 📳🈶🈚 🈸🈺🈷️ ✴️🆚💮 🉐㊙️㊗️ 🈴🈵🈹 🈲🅰️🅱️ 🆎🆑🅾️ 🆘 🛑📛 🚫💯💢 ♨️🚷🚯 🚳🚱🔞 📵🚭 ‼️⁉️🔅 🔆〽️⚠️ 🚸🔱⚜️ 🔰♻️ 🈯💹❇️ ✳️🌐 💠Ⓜ️🌀 💤🏧🚾 🅿️🈳 🈂🛂🛃 🛄🛅🛗 🚀🛸🚁 🚉🚆🚅 ✈️🛫🛬 🛩️💺🛰️
您的评论由 AI 智能审核,一般1分钟内会展示,若不展示请确认你的评论是否符合社区和法律规范
加载中...

选择联系方式

留言反馈

😀😃😄 😁😅😂 🤣😊😇 🙂🙃😉 😌😍🥰 😘😗😙 😚😋😛 😝😜🤪 🤨🧐🤓 😎🥸🤩 🥳😏😒 😞😔😟 😕🙁☹️ 😣😖😫 😩🥺😢 😭😤😠 😡🤬🤯 😳🥵🥶 😱😨😰 😥😓🤗 🤔🤭🤫 🤥😶😐 😑😬🙄 😯😦😧 😮😲🥱 😴🤤😪 😵🤐🥴 🤢🤮🤧 😷🤒🤕 🤑🤠😈 👿👹👺 🤡💩👻 💀☠️👽 👾🤖🎃 😺😸😹 😻😼😽 🙀😿😾 👍👎👏 🙌👐🤲 🤝🤜🤛 ✌️🤞🤟 🤘👌🤏 👈👉👆 👇☝️ 🤚🖐️🖖 👋🤙💪 🦾🖕✍️ 🙏💅🤳 💯💢💥 💫💦💨 🕳️💣💬 👁️‍🗨️🗨️🗯️ 💭💤❤️ 🧡💛💚 💙💜🖤 🤍🤎💔 ❣️💕💞 💓💗💖 💘💝💟 ☮️✝️☪️ 🕉️☸️✡️ 🔯🕎☯️ ☦️🛐 🆔⚛️🉑 ☢️☣️📴 📳🈶🈚 🈸🈺🈷️ ✴️🆚💮 🉐㊙️㊗️ 🈴🈵🈹 🈲🅰️🅱️ 🆎🆑🅾️ 🆘 🛑📛 🚫💯💢 ♨️🚷🚯 🚳🚱🔞 📵🚭 ‼️⁉️🔅 🔆〽️⚠️ 🚸🔱⚜️ 🔰♻️ 🈯💹❇️ ✳️🌐 💠Ⓜ️🌀 💤🏧🚾 🅿️🈳 🈂🛂🛃 🛄🛅🛗 🚀🛸🚁 🚉🚆🚅 ✈️🛫🛬 🛩️💺🛰️