creem 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.github/workflows/ci.yml +50 -0
- data/.github/workflows/release.yml +49 -0
- data/.gitignore +6 -0
- data/.rspec +3 -0
- data/.rubocop.yml +126 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +17 -0
- data/CLAUDE.md +38 -0
- data/Gemfile +12 -0
- data/Gemfile.lock +85 -0
- data/LICENSE +21 -0
- data/Makefile +108 -0
- data/README.md +193 -0
- data/Rakefile +9 -0
- data/creem.gemspec +49 -0
- data/docs/api_reference.md +335 -0
- data/docs/architecture.md +145 -0
- data/docs/development.md +137 -0
- data/docs/error_handling.md +85 -0
- data/docs/getting_started.md +116 -0
- data/docs/webhooks.md +179 -0
- data/lib/creem/client.rb +119 -0
- data/lib/creem/configuration.rb +18 -0
- data/lib/creem/errors.rb +24 -0
- data/lib/creem/resources/base.rb +23 -0
- data/lib/creem/resources/checkouts.rb +22 -0
- data/lib/creem/resources/customers.rb +24 -0
- data/lib/creem/resources/discounts.rb +13 -0
- data/lib/creem/resources/licenses.rb +25 -0
- data/lib/creem/resources/products.rb +17 -0
- data/lib/creem/resources/subscriptions.rb +31 -0
- data/lib/creem/resources/transactions.rb +13 -0
- data/lib/creem/version.rb +5 -0
- data/lib/creem/webhook.rb +33 -0
- data/lib/creem.rb +33 -0
- metadata +157 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# 架构说明
|
|
2
|
+
|
|
3
|
+
本文档描述 Creem Ruby SDK 的内部架构,适合需要了解源码或贡献代码的开发者。
|
|
4
|
+
|
|
5
|
+
## 三层架构
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
┌─────────────────────────────────────────┐
|
|
9
|
+
│ Creem Module + Configuration │ 全局配置层
|
|
10
|
+
│ (lib/creem.rb, configuration.rb) │
|
|
11
|
+
├─────────────────────────────────────────┤
|
|
12
|
+
│ Creem::Client │ HTTP 传输层
|
|
13
|
+
│ (lib/creem/client.rb) │
|
|
14
|
+
├─────────────────────────────────────────┤
|
|
15
|
+
│ Creem::Resources::* │ 资源映射层
|
|
16
|
+
│ (lib/creem/resources/*.rb) │
|
|
17
|
+
└─────────────────────────────────────────┘
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
### 1. 全局配置层
|
|
21
|
+
|
|
22
|
+
`Creem` 模块持有全局 `Configuration` 单例,管理 API Key、超时和环境切换:
|
|
23
|
+
|
|
24
|
+
- `Creem.configure { |c| ... }` — 设置全局配置
|
|
25
|
+
- `Creem.configuration` — 获取当前配置
|
|
26
|
+
- `Creem.reset_configuration!` — 重置(主要用于测试)
|
|
27
|
+
|
|
28
|
+
`Configuration` 根据 `test_mode` 自动切换 base URL:
|
|
29
|
+
- 生产:`https://api.creem.io/v1`
|
|
30
|
+
- 沙盒:`https://test-api.creem.io/v1`
|
|
31
|
+
|
|
32
|
+
### 2. HTTP 传输层(Client)
|
|
33
|
+
|
|
34
|
+
`Client` 是 SDK 的核心,负责:
|
|
35
|
+
|
|
36
|
+
1. **配置管理**:初始化时从全局配置拷贝一份,支持参数覆盖
|
|
37
|
+
2. **资源注册**:通过 memoized 方法暴露各资源(`products`, `checkouts` 等)
|
|
38
|
+
3. **HTTP 通信**:使用 `Net::HTTP`,所有请求经过统一管道
|
|
39
|
+
|
|
40
|
+
请求管道:
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
request(method, path, params/body)
|
|
44
|
+
→ build_uri(path, params) # 构建 URL + 查询参数
|
|
45
|
+
→ build_http(uri) # 配置 Net::HTTP(SSL、超时)
|
|
46
|
+
→ build_request(method, uri, body) # 构建请求对象(Headers、Body)
|
|
47
|
+
→ http.request(req) # 发送请求
|
|
48
|
+
→ handle_response(response) # 状态码 → 异常 映射
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
认证通过 `x-api-key` Header 发送。
|
|
52
|
+
|
|
53
|
+
### 3. 资源映射层(Resources)
|
|
54
|
+
|
|
55
|
+
每个资源类继承 `Resources::Base`:
|
|
56
|
+
|
|
57
|
+
```ruby
|
|
58
|
+
module Creem
|
|
59
|
+
module Resources
|
|
60
|
+
class Base
|
|
61
|
+
def initialize(client)
|
|
62
|
+
@client = client
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def get(path, params = {})
|
|
66
|
+
client.get(path, params)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def post(path, body = {})
|
|
70
|
+
client.post(path, body)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
资源类只负责:
|
|
78
|
+
- 将 Ruby 关键字参数映射为 API 请求参数
|
|
79
|
+
- 不解析响应,直接返回 `JSON.parse` 后的 `Hash`
|
|
80
|
+
|
|
81
|
+
### API 端点注意事项
|
|
82
|
+
|
|
83
|
+
不同资源的列表端点路径不统一(反映上游 API 设计):
|
|
84
|
+
|
|
85
|
+
| 资源 | 列表端点 |
|
|
86
|
+
|------|----------|
|
|
87
|
+
| Products | `/products/search` |
|
|
88
|
+
| Subscriptions | `/subscriptions/search` |
|
|
89
|
+
| Licenses | `/licenses/search` |
|
|
90
|
+
| Customers | `/customers/list` |
|
|
91
|
+
| Transactions | `/transactions/search` |
|
|
92
|
+
| Discounts | `/discounts/search` |
|
|
93
|
+
|
|
94
|
+
`retrieve` 通常通过查询参数传递 ID(如 `GET /subscriptions?subscription_id=...`),
|
|
95
|
+
而非路径参数。`Subscriptions#update` 和 `#cancel` 是使用路径参数的例外。
|
|
96
|
+
|
|
97
|
+
`Subscriptions#cancel` 中 `on_execute` 以 camelCase `onExecute` 发送 — 与 API 保持一致。
|
|
98
|
+
|
|
99
|
+
## 错误处理架构
|
|
100
|
+
|
|
101
|
+
见 [错误处理文档](error_handling.md)。
|
|
102
|
+
|
|
103
|
+
`Client#handle_response` 负责将 HTTP 状态码映射为对应异常类。
|
|
104
|
+
|
|
105
|
+
## 添加新资源
|
|
106
|
+
|
|
107
|
+
1. 在 `lib/creem/resources/` 下创建新文件,继承 `Base`
|
|
108
|
+
2. 在 `Client` 中注册 memoized 访问器
|
|
109
|
+
3. 在 `lib/creem.rb` 中 `require_relative` 新文件
|
|
110
|
+
4. 在 `spec/resources/` 下添加对应测试
|
|
111
|
+
|
|
112
|
+
```ruby
|
|
113
|
+
# lib/creem/resources/invoices.rb
|
|
114
|
+
module Creem
|
|
115
|
+
module Resources
|
|
116
|
+
class Invoices < Base
|
|
117
|
+
def list(page_number: 1, page_size: 10)
|
|
118
|
+
get("/invoices/search", page_number: page_number, page_size: page_size)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## 文件结构
|
|
126
|
+
|
|
127
|
+
```
|
|
128
|
+
lib/
|
|
129
|
+
├── creem.rb # 入口:require 所有模块,定义常量
|
|
130
|
+
└── creem/
|
|
131
|
+
├── version.rb # 版本号
|
|
132
|
+
├── configuration.rb # 配置类
|
|
133
|
+
├── errors.rb # 异常层级
|
|
134
|
+
├── client.rb # HTTP 客户端
|
|
135
|
+
├── webhook.rb # Webhook 签名验证
|
|
136
|
+
└── resources/
|
|
137
|
+
├── base.rb # 资源基类
|
|
138
|
+
├── products.rb
|
|
139
|
+
├── checkouts.rb
|
|
140
|
+
├── subscriptions.rb
|
|
141
|
+
├── customers.rb
|
|
142
|
+
├── transactions.rb
|
|
143
|
+
├── licenses.rb
|
|
144
|
+
└── discounts.rb
|
|
145
|
+
```
|
data/docs/development.md
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# 开发指南
|
|
2
|
+
|
|
3
|
+
本文档面向 Creem Ruby SDK 的贡献者和维护者。
|
|
4
|
+
|
|
5
|
+
## 环境准备
|
|
6
|
+
|
|
7
|
+
### 前置要求
|
|
8
|
+
|
|
9
|
+
- Ruby >= 3.1(项目使用 `.ruby-version` 锁定为 3.3)
|
|
10
|
+
- [mise](https://mise.jdx.dev/) — 运行时版本管理
|
|
11
|
+
- Bundler >= 2.0
|
|
12
|
+
|
|
13
|
+
### 安装依赖
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
make install
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## 常用命令
|
|
20
|
+
|
|
21
|
+
所有命令通过 `make` 执行,确保使用正确的 Ruby 工具链:
|
|
22
|
+
|
|
23
|
+
| 命令 | 说明 |
|
|
24
|
+
|------|------|
|
|
25
|
+
| `make install` | 安装依赖 |
|
|
26
|
+
| `make test` | 运行完整测试套件 |
|
|
27
|
+
| `make test TEST=spec/resources/checkouts_spec.rb` | 运行单个测试文件 |
|
|
28
|
+
| `make lint` | 运行 RuboCop 检查 |
|
|
29
|
+
| `make format` | 运行 RuboCop 自动修复 |
|
|
30
|
+
| `make build` | 构建 gem 包 |
|
|
31
|
+
| `make console` | 启动交互式控制台(已加载 creem) |
|
|
32
|
+
| `make clean` | 清理构建产物 |
|
|
33
|
+
| `make tag` | 自动递增 patch 版本并推送 tag |
|
|
34
|
+
| `make tag VERSION=1.0.0` | 指定版本号并推送 tag |
|
|
35
|
+
|
|
36
|
+
精确到行的测试:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
mise exec -- bundle exec rspec spec/resources/checkouts_spec.rb:15
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## 测试规范
|
|
43
|
+
|
|
44
|
+
### 约定
|
|
45
|
+
|
|
46
|
+
- 使用 WebMock 禁止真实网络请求
|
|
47
|
+
- 每个测试前自动重置 `Creem.configuration`
|
|
48
|
+
- 使用 `stub_creem_get` / `stub_creem_post` 辅助方法创建 HTTP 桩
|
|
49
|
+
- 使用 `build_client` 创建测试用客户端
|
|
50
|
+
|
|
51
|
+
### 示例
|
|
52
|
+
|
|
53
|
+
```ruby
|
|
54
|
+
RSpec.describe Creem::Resources::Products do
|
|
55
|
+
let(:client) { build_client }
|
|
56
|
+
|
|
57
|
+
describe "#list" do
|
|
58
|
+
it "returns products" do
|
|
59
|
+
stub_creem_get("/products/search", { items: [] })
|
|
60
|
+
result = client.products.list
|
|
61
|
+
expect(result).to eq({ "items" => [] })
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### 添加新资源测试
|
|
68
|
+
|
|
69
|
+
在 `spec/resources/` 下创建 `<resource>_spec.rb`,遵循现有模式:
|
|
70
|
+
|
|
71
|
+
1. 使用 `build_client` 创建客户端
|
|
72
|
+
2. 使用 `stub_creem_get` / `stub_creem_post` 模拟 HTTP
|
|
73
|
+
3. 验证返回值结构
|
|
74
|
+
4. 验证请求参数正确传递
|
|
75
|
+
|
|
76
|
+
## 代码风格
|
|
77
|
+
|
|
78
|
+
项目使用 RuboCop 进行代码风格检查,配置文件为 `.rubocop.yml`。
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
make lint # 检查
|
|
82
|
+
make format # 自动修复
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## 发布流程
|
|
86
|
+
|
|
87
|
+
### 自动版本管理
|
|
88
|
+
|
|
89
|
+
`make tag` 会自动:
|
|
90
|
+
|
|
91
|
+
1. 获取最新的 git tag
|
|
92
|
+
2. 递增 patch 版本号(或使用指定版本)
|
|
93
|
+
3. 更新 `lib/creem/version.rb`
|
|
94
|
+
4. 提交 "Release vX.Y.Z"
|
|
95
|
+
5. 创建 git tag
|
|
96
|
+
6. 推送 commit 和 tag
|
|
97
|
+
|
|
98
|
+
### CI/CD
|
|
99
|
+
|
|
100
|
+
- **CI**(`.github/workflows/ci.yml`):在 Ruby 3.1 ~ 4.0 上运行测试和 lint
|
|
101
|
+
- **Release**(`.github/workflows/release.yml`):推送 `v*` tag 时自动发布到 RubyGems 并创建 GitHub Release
|
|
102
|
+
|
|
103
|
+
### 发布步骤
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
# 1. 确保测试通过
|
|
107
|
+
make test
|
|
108
|
+
|
|
109
|
+
# 2. 更新 CHANGELOG.md
|
|
110
|
+
|
|
111
|
+
# 3. 打 tag 并推送(自动触发发布)
|
|
112
|
+
make tag # 自动递增 patch
|
|
113
|
+
# 或
|
|
114
|
+
make tag VERSION=1.0.0 # 指定版本
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## 项目结构
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
creem/
|
|
121
|
+
├── .github/workflows/ # CI/CD 配置
|
|
122
|
+
│ ├── ci.yml # 测试 + lint
|
|
123
|
+
│ └── release.yml # 发布到 RubyGems
|
|
124
|
+
├── docs/ # 文档
|
|
125
|
+
├── lib/
|
|
126
|
+
│ ├── creem.rb # 入口文件
|
|
127
|
+
│ └── creem/ # 核心代码
|
|
128
|
+
├── spec/ # 测试
|
|
129
|
+
│ ├── spec_helper.rb # 测试辅助
|
|
130
|
+
│ ├── resources/ # 资源测试
|
|
131
|
+
│ └── ...
|
|
132
|
+
├── Gemfile # 依赖声明
|
|
133
|
+
├── creem.gemspec # Gem 规格
|
|
134
|
+
├── Makefile # 开发命令
|
|
135
|
+
├── CHANGELOG.md # 变更日志
|
|
136
|
+
└── README.md # 项目说明
|
|
137
|
+
```
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# 错误处理
|
|
2
|
+
|
|
3
|
+
Creem Ruby SDK 使用结构化的异常层级来表示不同类型的错误。
|
|
4
|
+
|
|
5
|
+
## 异常层级
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
StandardError
|
|
9
|
+
└── Creem::Error
|
|
10
|
+
├── Creem::ConfigurationError
|
|
11
|
+
├── Creem::WebhookSignatureError
|
|
12
|
+
└── Creem::ApiError
|
|
13
|
+
├── Creem::BadRequestError # 400
|
|
14
|
+
├── Creem::AuthenticationError # 401
|
|
15
|
+
├── Creem::NotFoundError # 404
|
|
16
|
+
├── Creem::RateLimitError # 429
|
|
17
|
+
└── Creem::ServerError # 5xx
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## ApiError 属性
|
|
21
|
+
|
|
22
|
+
| 属性 | 类型 | 说明 |
|
|
23
|
+
|------|------|------|
|
|
24
|
+
| `message` | String | 错误描述 |
|
|
25
|
+
| `status` | Integer | HTTP 状态码 |
|
|
26
|
+
| `body` | Hash/nil | 响应体 |
|
|
27
|
+
|
|
28
|
+
## 使用示例
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
begin
|
|
32
|
+
client.products.retrieve("prod_invalid")
|
|
33
|
+
rescue Creem::AuthenticationError => e
|
|
34
|
+
puts "认证失败: #{e.message}"
|
|
35
|
+
rescue Creem::NotFoundError => e
|
|
36
|
+
puts "未找到: #{e.message}"
|
|
37
|
+
rescue Creem::RateLimitError => e
|
|
38
|
+
sleep(5)
|
|
39
|
+
retry
|
|
40
|
+
rescue Creem::BadRequestError => e
|
|
41
|
+
puts "参数错误: #{e.body}"
|
|
42
|
+
rescue Creem::ServerError => e
|
|
43
|
+
puts "服务器错误: #{e.message}"
|
|
44
|
+
rescue Creem::ApiError => e
|
|
45
|
+
puts "API 错误 (#{e.status}): #{e.message}"
|
|
46
|
+
end
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## 配置错误
|
|
50
|
+
|
|
51
|
+
```ruby
|
|
52
|
+
begin
|
|
53
|
+
client = Creem::Client.new # 缺少 api_key
|
|
54
|
+
rescue Creem::ConfigurationError => e
|
|
55
|
+
puts e.message # => "API key is required"
|
|
56
|
+
end
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## 重试策略
|
|
60
|
+
|
|
61
|
+
| 错误类型 | 可重试 | 建议 |
|
|
62
|
+
|----------|--------|------|
|
|
63
|
+
| `AuthenticationError` | ❌ | 检查 API Key |
|
|
64
|
+
| `BadRequestError` | ❌ | 检查请求参数 |
|
|
65
|
+
| `NotFoundError` | ❌ | 确认资源 ID |
|
|
66
|
+
| `RateLimitError` | ✅ | 指数退避重试 |
|
|
67
|
+
| `ServerError` | ✅ | 等待后重试(最多 3 次)|
|
|
68
|
+
|
|
69
|
+
### 指数退避示例
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
def with_retry(max_retries: 3, base_delay: 1)
|
|
73
|
+
retries = 0
|
|
74
|
+
begin
|
|
75
|
+
yield
|
|
76
|
+
rescue Creem::RateLimitError, Creem::ServerError
|
|
77
|
+
retries += 1
|
|
78
|
+
raise if retries > max_retries
|
|
79
|
+
sleep(base_delay * (2**(retries - 1)))
|
|
80
|
+
retry
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
with_retry { client.products.list }
|
|
85
|
+
```
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# Getting Started
|
|
2
|
+
|
|
3
|
+
本指南帮助你快速集成 Creem Ruby SDK,完成从安装到第一次 API 调用的全部流程。
|
|
4
|
+
|
|
5
|
+
## 系统要求
|
|
6
|
+
|
|
7
|
+
- **Ruby** >= 3.1.0, < 5.0
|
|
8
|
+
- **Bundler** >= 2.0
|
|
9
|
+
|
|
10
|
+
## 安装
|
|
11
|
+
|
|
12
|
+
### 通过 Gemfile(推荐)
|
|
13
|
+
|
|
14
|
+
```ruby
|
|
15
|
+
# Gemfile
|
|
16
|
+
gem "creem"
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
然后执行:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
bundle install
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### 直接安装
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
gem install creem
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## 配置
|
|
32
|
+
|
|
33
|
+
### 全局配置
|
|
34
|
+
|
|
35
|
+
适用于整个应用共享同一组凭据的场景(如 Rails 应用):
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
require "creem"
|
|
39
|
+
|
|
40
|
+
Creem.configure do |config|
|
|
41
|
+
config.api_key = "creem_YOUR_API_KEY" # 必填
|
|
42
|
+
config.test_mode = true # 使用沙盒环境(默认 false)
|
|
43
|
+
config.timeout = 30 # 读超时,秒(默认 30)
|
|
44
|
+
config.open_timeout = 10 # 连接超时,秒(默认 10)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
client = Creem::Client.new
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
> **Rails 项目建议**:将配置放在 `config/initializers/creem.rb` 中。
|
|
51
|
+
|
|
52
|
+
### 单独配置
|
|
53
|
+
|
|
54
|
+
也可以在创建 Client 时直接传入配置,覆盖全局设置:
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
client = Creem::Client.new(
|
|
58
|
+
api_key: "creem_YOUR_API_KEY",
|
|
59
|
+
test_mode: true,
|
|
60
|
+
timeout: 60,
|
|
61
|
+
open_timeout: 15
|
|
62
|
+
)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### 环境变量方式
|
|
66
|
+
|
|
67
|
+
推荐在生产环境中通过环境变量管理密钥:
|
|
68
|
+
|
|
69
|
+
```ruby
|
|
70
|
+
Creem.configure do |config|
|
|
71
|
+
config.api_key = ENV.fetch("CREEM_API_KEY")
|
|
72
|
+
config.test_mode = ENV["RAILS_ENV"] != "production"
|
|
73
|
+
end
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## 测试模式 vs 生产模式
|
|
77
|
+
|
|
78
|
+
| 对比项 | 生产模式 | 测试模式 |
|
|
79
|
+
|--------|----------|----------|
|
|
80
|
+
| Base URL | `https://api.creem.io/v1` | `https://test-api.creem.io/v1` |
|
|
81
|
+
| 支付 | 真实扣款 | 模拟支付 |
|
|
82
|
+
| 用途 | 正式环境 | 开发调试 |
|
|
83
|
+
|
|
84
|
+
**切换方式**:设置 `config.test_mode = true` 即可启用沙盒环境。
|
|
85
|
+
|
|
86
|
+
## 第一次 API 调用
|
|
87
|
+
|
|
88
|
+
```ruby
|
|
89
|
+
require "creem"
|
|
90
|
+
|
|
91
|
+
Creem.configure do |config|
|
|
92
|
+
config.api_key = ENV.fetch("CREEM_API_KEY")
|
|
93
|
+
config.test_mode = true
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
client = Creem::Client.new
|
|
97
|
+
|
|
98
|
+
# 获取产品列表
|
|
99
|
+
products = client.products.list
|
|
100
|
+
puts products
|
|
101
|
+
|
|
102
|
+
# 创建一个结账会话
|
|
103
|
+
checkout = client.checkouts.create(
|
|
104
|
+
product_id: "prod_123",
|
|
105
|
+
success_url: "https://yoursite.com/success"
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
puts checkout["checkout_url"] # 将客户重定向到此 URL
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## 下一步
|
|
112
|
+
|
|
113
|
+
- [API 参考](api_reference.md) — 所有资源的详细用法
|
|
114
|
+
- [Webhook 集成](webhooks.md) — 接收和验证 Webhook 事件
|
|
115
|
+
- [错误处理](error_handling.md) — 异常类型和最佳实践
|
|
116
|
+
- [架构说明](architecture.md) — SDK 内部架构和扩展方式
|
data/docs/webhooks.md
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# Webhook 集成
|
|
2
|
+
|
|
3
|
+
Creem 通过 Webhook 推送异步事件通知(如支付完成、订阅变更等)。本文档介绍如何安全接收和处理这些事件。
|
|
4
|
+
|
|
5
|
+
## 签名验证原理
|
|
6
|
+
|
|
7
|
+
Creem 使用 **HMAC-SHA256** 算法对 Webhook 请求体进行签名:
|
|
8
|
+
|
|
9
|
+
1. Creem 使用你的 Webhook Secret 对原始请求体计算 HMAC-SHA256
|
|
10
|
+
2. 签名通过 `creem-signature` HTTP Header 发送
|
|
11
|
+
3. SDK 使用 `OpenSSL.fixed_length_secure_compare` 进行恒时比较,防止时序攻击
|
|
12
|
+
|
|
13
|
+
## 基本用法
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
# 验证签名并解析事件(推荐)
|
|
17
|
+
event = Creem::Webhook.construct_event(
|
|
18
|
+
payload: raw_body, # 原始请求体(String)
|
|
19
|
+
secret: webhook_secret, # Webhook Secret
|
|
20
|
+
signature: signature # creem-signature Header
|
|
21
|
+
)
|
|
22
|
+
# event 是解析后的 Hash
|
|
23
|
+
|
|
24
|
+
# 仅验证签名(不解析)
|
|
25
|
+
valid = Creem::Webhook.verify_signature(
|
|
26
|
+
payload: raw_body,
|
|
27
|
+
secret: webhook_secret,
|
|
28
|
+
signature: signature
|
|
29
|
+
)
|
|
30
|
+
# 返回 true / false
|
|
31
|
+
|
|
32
|
+
# 验证签名,失败时抛异常
|
|
33
|
+
Creem::Webhook.verify_signature!(
|
|
34
|
+
payload: raw_body,
|
|
35
|
+
secret: webhook_secret,
|
|
36
|
+
signature: signature
|
|
37
|
+
)
|
|
38
|
+
# 签名无效时抛出 Creem::WebhookSignatureError
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Rails 集成
|
|
42
|
+
|
|
43
|
+
```ruby
|
|
44
|
+
# app/controllers/webhooks_controller.rb
|
|
45
|
+
class WebhooksController < ApplicationController
|
|
46
|
+
skip_before_action :verify_authenticity_token, only: :creem
|
|
47
|
+
|
|
48
|
+
WEBHOOK_SECRET = ENV.fetch("CREEM_WEBHOOK_SECRET")
|
|
49
|
+
|
|
50
|
+
def creem
|
|
51
|
+
payload = request.body.read
|
|
52
|
+
signature = request.headers["creem-signature"]
|
|
53
|
+
|
|
54
|
+
event = Creem::Webhook.construct_event(
|
|
55
|
+
payload: payload,
|
|
56
|
+
secret: WEBHOOK_SECRET,
|
|
57
|
+
signature: signature
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
handle_event(event)
|
|
61
|
+
head :ok
|
|
62
|
+
rescue Creem::WebhookSignatureError => e
|
|
63
|
+
Rails.logger.warn("Webhook signature verification failed: #{e.message}")
|
|
64
|
+
head :bad_request
|
|
65
|
+
rescue JSON::ParserError => e
|
|
66
|
+
Rails.logger.error("Webhook payload parse error: #{e.message}")
|
|
67
|
+
head :bad_request
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def handle_event(event)
|
|
73
|
+
case event["event"]
|
|
74
|
+
when "checkout.completed"
|
|
75
|
+
handle_checkout_completed(event)
|
|
76
|
+
when "subscription.active"
|
|
77
|
+
handle_subscription_active(event)
|
|
78
|
+
when "subscription.canceled"
|
|
79
|
+
handle_subscription_canceled(event)
|
|
80
|
+
when "subscription.renewed"
|
|
81
|
+
handle_subscription_renewed(event)
|
|
82
|
+
else
|
|
83
|
+
Rails.logger.info("Unhandled webhook event: #{event['event']}")
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def handle_checkout_completed(event)
|
|
88
|
+
# 处理结账完成事件
|
|
89
|
+
# event["object"] 包含结账详情
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def handle_subscription_active(event)
|
|
93
|
+
# 处理订阅激活事件
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def handle_subscription_canceled(event)
|
|
97
|
+
# 处理订阅取消事件
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def handle_subscription_renewed(event)
|
|
101
|
+
# 处理订阅续费事件
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
路由配置:
|
|
107
|
+
|
|
108
|
+
```ruby
|
|
109
|
+
# config/routes.rb
|
|
110
|
+
Rails.application.routes.draw do
|
|
111
|
+
post "/webhooks/creem", to: "webhooks#creem"
|
|
112
|
+
end
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Sinatra 集成
|
|
116
|
+
|
|
117
|
+
```ruby
|
|
118
|
+
require "sinatra"
|
|
119
|
+
require "creem"
|
|
120
|
+
|
|
121
|
+
WEBHOOK_SECRET = ENV.fetch("CREEM_WEBHOOK_SECRET")
|
|
122
|
+
|
|
123
|
+
post "/webhooks/creem" do
|
|
124
|
+
payload = request.body.read
|
|
125
|
+
signature = request.env["HTTP_CREEM_SIGNATURE"]
|
|
126
|
+
|
|
127
|
+
begin
|
|
128
|
+
event = Creem::Webhook.construct_event(
|
|
129
|
+
payload: payload,
|
|
130
|
+
secret: WEBHOOK_SECRET,
|
|
131
|
+
signature: signature
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
case event["event"]
|
|
135
|
+
when "checkout.completed"
|
|
136
|
+
# 处理结账完成
|
|
137
|
+
when "subscription.active"
|
|
138
|
+
# 处理订阅激活
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
status 200
|
|
142
|
+
body "OK"
|
|
143
|
+
rescue Creem::WebhookSignatureError
|
|
144
|
+
status 400
|
|
145
|
+
body "Invalid signature"
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## 常见事件类型
|
|
151
|
+
|
|
152
|
+
| 事件名 | 说明 |
|
|
153
|
+
|--------|------|
|
|
154
|
+
| `checkout.completed` | 结账完成,客户已支付 |
|
|
155
|
+
| `subscription.active` | 订阅已激活 |
|
|
156
|
+
| `subscription.canceled` | 订阅已取消 |
|
|
157
|
+
| `subscription.renewed` | 订阅已续费 |
|
|
158
|
+
| `subscription.paused` | 订阅已暂停 |
|
|
159
|
+
|
|
160
|
+
> 完整的事件列表请参考 [Creem 官方文档](https://docs.creem.io)。
|
|
161
|
+
|
|
162
|
+
## 最佳实践
|
|
163
|
+
|
|
164
|
+
1. **始终验证签名**:不要跳过签名验证,防止伪造请求。
|
|
165
|
+
2. **使用原始请求体**:签名基于原始字节流计算,不要对请求体做任何预处理。
|
|
166
|
+
3. **幂等处理**:Webhook 可能重复投递,确保处理逻辑幂等。
|
|
167
|
+
4. **快速响应**:尽快返回 `200` 状态码,将耗时操作放入后台队列。
|
|
168
|
+
5. **记录日志**:记录接收到的事件,便于排查问题。
|
|
169
|
+
6. **处理未知事件**:对不认识的事件类型做优雅降级,不要返回错误。
|
|
170
|
+
|
|
171
|
+
## 本地测试
|
|
172
|
+
|
|
173
|
+
开发时可使用 [ngrok](https://ngrok.com) 等工具将本地服务暴露到公网:
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
ngrok http 3000
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
然后在 Creem 控制台中将 Webhook URL 设置为 ngrok 提供的 HTTPS 地址。
|