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.
@@ -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
+ ```
@@ -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 地址。