こんにちは、KOUKIです。
GolangのWebフレームワークであるfiberを使って、APIを開発しています。
前回は、Checkout APIの実装に着手し、リンクの取得やオーダー作成、トランザクションの実装を行いました。
今回は、Stripeでチェックアウト機能を実装します。
尚、本記事は「React, NextJS and Golang: A Rapid Guide – Advanced」コースを参考にしています。解釈は私が勝手に付けているので、本物をみたい場合は受講をお勧めします!
事前準備
モジュールのインストール
1 |
go get -u github.com/stripe/stripe-go/v72 |
フォルダ/ファイル
1 2 |
mkdir src/internal touch src/internal/key.go |
前回
作るもの
Checkout機能を作りたいと思います。エンドポイントは、次の通りです。
- GET /api/checkout/links/{code}
- POST /api/checkout/orders
- POST /api/checkout/orders/confirm
今回は、「/api/checkout/orders/confirm」への処理を実装します。
Stripeとは
Stripeは、決算インフラサービスです。ショッピングカートなどで、商品を購入するとき決算処理などに利用できるので、大変便利です。
ある程度の機能なら無料で利用できるので、Signupしてみましょう。
シークレットキーの取得
ホームにアクセスすると、「公開可能キー」と「シークレットキー」が確認できます。

このうち、シークレットキーを使いますので、どこかにメモしておいてください。
ちなみに、このキーの値は誰にも知られないようにしておいてください。
取得専用のプログラムを実装しておきましょう。
1 2 3 4 5 6 7 |
// internal/key.go package internal // このキーは外部に公開してはならない func StripeSecretKey() string { return "sk_test_XXXX" } |
本当は、シークレットファイルを用意してそこから呼び出した方がいいと思うのですが、今回は簡単に実装しました。
オーダー作成処理
前回作成したorderController.goのCreateOrder関数にStripeを組み込んでみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 |
// controllers/orderController.go package controllers import ( "admin/internal" "admin/src/database" "admin/src/models" "github.com/gofiber/fiber/v2" "github.com/stripe/stripe-go" "github.com/stripe/stripe-go/checkout/session" ) func CreateOrder(ctx *fiber.Ctx) error { // リクエストデータを取得 // リクエストからコードを抜き出す // DB検索 // 該当データがない場合はエラー // Orderを作成する // トランザクション // stripeパラメーター var lineItems []*stripe.CheckoutSessionLineItemParams // リクエストからプロダクトを取得 for _, requestProduct := range request.Products { // product検索 // トータルを算出 // OrderItemを作成 // トランザクション // OrderItemをDBに保存 .. // stripeアイテムセット lineItems = append(lineItems, &stripe.CheckoutSessionLineItemParams{ Name: stripe.String(product.Title), Description: stripe.String(product.Description), Images: []*string{stripe.String(product.Image)}, Amount: stripe.Int64(100 * int64(product.Price)), Currency: stripe.String("usd"), Quantity: stripe.Int64(int64(requestProduct["quantity"])), }) } // stripe checkout stripe.Key = internal.StripeSecretKey() params := stripe.CheckoutSessionParams{ // http://localhost:5000はフロントエンド側のリンク(まだ作成していない) SuccessURL: stripe.String("http://localhost:5000/success?source={CHECKOUT_SESSION_ID}"), CancelURL: stripe.String("http://localhost:5000/error"), PaymentMethodTypes: stripe.StringSlice([]string{"card"}), LineItems: lineItems, } source, err := session.New(¶ms) if err != nil { tx.Rollback() ctx.Status(fiber.StatusBadRequest) return ctx.JSON(fiber.Map{ "message": err.Error(), }) } // トランザクションIDを登録 order.TransactionID = source.ID // データを書き換えたので、上書き保存 if err := tx.Save(&order).Error; err != nil { tx.Rollback() ctx.Status(fiber.StatusBadRequest) return ctx.JSON(fiber.Map{ "message": err.Error(), }) } // 実行 tx.Commit() return ctx.JSON(source) } |
検証
以下のパラメーターでリクエストを送ってみましょう。
- URL: http://localhost:8000/api/checkout/orders
- 形式: POST
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
{ "first_name": "a", "last_name": "a", "email": "a@a.com", "address": "a", "country": "a", "city": "a", "zip": "a", "code": "yfUVevT", "products": [ { "product_id": 2, "quantity": 2 }, { "product_id": 4, "quantity": 1 }, { "product_id": 5, "quantity": 3 } ] } |

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
{ "cancel_url": "http://localhost:5000/error", "client_reference_id": "", "customer": null, "customer_email": "", "deleted": false, "display_items":[ {"amount": 4200, "currency": "usd", "custom":{"description": "RRkTYHm",…}, {"amount": 8000, "currency": "usd", "custom":{"description": "DtVyXKu",…}, {"amount": 7400, "currency": "usd", "custom":{"description": "IfhGxRT",…} ], "id": "cs_test_b1VWTbcNwxjxk1fNNc1XFNE6tB0W6kc7nXknzagZni9ZysLmu5phF1zRfZ", "livemode": false, "locale": "", "metadata":{}, "mode": "payment", "object": "checkout.session", "payment_intent":{ "amount": 0, "amount_capturable": 0, "amount_received": 0, "application": null, "application_fee_amount": 0, "canceled_at": 0, "cancellation_reason": "", "capture_method": "", "charges": null, "client_secret": "", "confirmation_method": "", "created": 0, "currency": "", "customer": null, "description": "", "invoice": null, "last_payment_error": null, "livemode": false, "id": "pi_1J5IARIyqJRrPrxSDqpMi802", "metadata": null, "next_action": null, "on_behalf_of": null, "payment_method": null, "payment_method_options": null, "payment_method_types": null, "receipt_email": "", "review": null, "setup_future_usage": "", "shipping":{"address": null, "carrier": "", "name": "", "phone": "",…}, "source": null, "statement_descriptor": "", "statement_descriptor_suffix": "", "status": "", "transfer_data": null, "transfer_group": "" }, "payment_method_types":[ "card" ], "setup_intent": null, "shipping": null, "shipping_address_collection": null, "subscription": null, "submit_type": "", "success_url": "http://localhost:5000/success?source={CHECKOUT_SESSION_ID}" } |
OKですね。
idの値は、この後実装するチェックアウト処理で使いますので、どこかにメモしておきましょう。
また、Stripe側のログを見てみると、リクエストが届いていました。

チェックアウトの実装
チェックアウト処理を実装しましょう。
ルートの追加
「/api/checkout/orders/confirm」へのルートを追加します。
1 2 3 4 5 6 7 8 9 |
// routes/routes.go ... func Setup(app *fiber.App) { ... // Checkout ... checkout.Post("orders/confirm", controllers.CompleteOrder) } |
コントローラーの追加
「/api/checkout/orders/confirm」へのコントローラーを追加します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
// controllers/orderController.go func CompleteOrder(ctx *fiber.Ctx) error { var data map[string]string // リクエストデータを取得 if err := ctx.BodyParser(&data); err != nil { return err } order := models.Order{} // OrderItemを検索 database.DB.Preload("OrderItems").First(&order, models.Order{ TransactionID: data["source"], }) if order.ID == 0 { ctx.Status((fiber.StatusNotFound)) return ctx.JSON(fiber.Map{ "message": "オーダーが見つかりません", }) } // Orderを保存 order.Complete = true database.DB.Save(&order) // Redisキャッシュに保存したRankingも更新 // Rankingはユーザーの購入金額を示す go func(order models.Order) { ambassadorRevenue := 0.0 adminRevenue := 0.0 for _, item := range order.OrderItems { ambassadorRevenue += item.AmbassadorRevenue adminRevenue += item.AdminRevenue } user := models.User{} user.ID = order.UserID database.DB.First(&user) // https://redis.io/commands/zincrby database.Cache.ZIncrBy(context.Background(), "rankings", ambassadorRevenue, user.Name()) }(order) return ctx.JSON(fiber.Map{ "message": "success", }) } |
検証
以下の手順に従って、検証を行いましょう。
Rankingの確認
チェックアウトするとユーザーのRevenueが増加します。そのため、増加前のデータを確認しておきましょう。
- URL: http://localhost:8000/api/ambassador/rankings
- 形式: GET

1 2 3 4 |
{ ... "self note": 3.4000000000000004 } |
尚、空が返却される場合は、以下のコマンドを実行してください。
1 |
docker-compose run --rm backend sh -c "go run src/commands/redis/updateRankings.go" |
チェックアウトの確認
先ほど実装した、チェックアウトAPIへリクエストを送ります。
- URL: http://localhost:8000/api/checkout/orders/confirm
- 形式: POST
1 2 3 |
{ "source": "cs_test_b1VWTbcNwxjxk1fNNc1XFNE6tB0W6kc7nXknzagZni9ZysLmu5phF1zRfZ" } |
sourceに指定するidは、オーダー作成時に生成したIDです(前述の検証結果の中にあります)。

successになったので、OKですね。
Re: Rankingの確認
チェックアウト完了なので、もう一度Rankingを確認します。
- URL: http://localhost:8000/api/ambassador/rankings
- 形式: GET

1 2 3 |
{ "self note": 38.6 } |
Revenueが増加しているので、問題なさそうですね。
次回
次回は、機能を実装しましょう。
Go言語まとめ
ソースコード
ここまでのソースコードを以下に記載します。
key.go
1 2 3 4 5 6 7 |
// internal/key.go package internal // このキーは外部に公開してはならない func StripeSecretKey() string { return "sk_test_XXXX" } |
orderController.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 |
// controllers/orderController.go package controllers import ( "admin/internal" "admin/src/database" "admin/src/models" "context" "github.com/gofiber/fiber/v2" "github.com/stripe/stripe-go" "github.com/stripe/stripe-go/checkout/session" ) func Orders(ctx *fiber.Ctx) error { var orders []models.Order // DB検索 database.DB.Preload("OrderItems").Find(&orders) for i, order := range orders { orders[i].Name = order.FullName() orders[i].Total = order.GetTotal() } return ctx.JSON(orders) } type CreateOrderRequest struct { Code string FirstName string LastName string Email string Address string Country string City string Zip string Products []map[string]int } func CreateOrder(ctx *fiber.Ctx) error { var request CreateOrderRequest // リクエストデータを取得 if err := ctx.BodyParser(&request); err != nil { return err } // リクエストからコードを抜き出す link := models.Link{ Code: request.Code, } // DB検索 database.DB.Preload("User").First(&link) // 該当データがない場合はエラー if link.ID == 0 { ctx.Status(fiber.StatusBadRequest) return ctx.JSON(fiber.Map{ "message": "無効なリンクです", }) } // Orderを作成する order := models.Order{ Code: link.Code, UserID: link.UserID, AmbassadorEmail: link.User.Email, FirstName: request.FirstName, LastName: request.LastName, Email: request.Email, Address: request.Address, Country: request.Country, City: request.City, Zip: request.Zip, } // トランザクション tx := database.DB.Begin() // OrderをDBに保存 if err := tx.Create(&order).Error; err != nil { tx.Rollback() ctx.Status(fiber.StatusBadRequest) return ctx.JSON(fiber.Map{ "message": err.Error(), }) } // stripeパラメーター var lineItems []*stripe.CheckoutSessionLineItemParams // リクエストからプロダクトを取得 for _, requestProduct := range request.Products { product := models.Product{} product.ID = uint(requestProduct["product_id"]) // product検索 database.DB.First(&product) // トータルを算出 total := product.Price * float64(requestProduct["quantity"]) // OrderItemを作成 item := models.OrderItem{ OrderID: order.ID, ProductTitle: product.Title, Price: product.Price, Quantity: uint(requestProduct["quantity"]), AmbassadorRevenue: 0.1 * total, AdminRevenue: 0.9 * total, } // トランザクション // OrderItemをDBに保存 if err := tx.Create(&item).Error; err != nil { tx.Rollback() ctx.Status(fiber.StatusBadRequest) return ctx.JSON(fiber.Map{ "message": err.Error(), }) } // stripeアイテムセット lineItems = append(lineItems, &stripe.CheckoutSessionLineItemParams{ Name: stripe.String(product.Title), Description: stripe.String(product.Description), Images: []*string{stripe.String(product.Image)}, Amount: stripe.Int64(100 * int64(product.Price)), Currency: stripe.String("usd"), Quantity: stripe.Int64(int64(requestProduct["quantity"])), }) } // stripe checkout stripe.Key = internal.StripeSecretKey() params := stripe.CheckoutSessionParams{ // http://localhost:5000はフロントエンド側のリンク(まだ作成していない) SuccessURL: stripe.String("http://localhost:5000/success?source={CHECKOUT_SESSION_ID}"), CancelURL: stripe.String("http://localhost:5000/error"), PaymentMethodTypes: stripe.StringSlice([]string{"card"}), LineItems: lineItems, } source, err := session.New(¶ms) if err != nil { tx.Rollback() ctx.Status(fiber.StatusBadRequest) return ctx.JSON(fiber.Map{ "message": err.Error(), }) } // トランザクションIDを登録 order.TransactionID = source.ID // データを書き換えたので、上書き保存 if err := tx.Save(&order).Error; err != nil { tx.Rollback() ctx.Status(fiber.StatusBadRequest) return ctx.JSON(fiber.Map{ "message": err.Error(), }) } // 実行 tx.Commit() return ctx.JSON(source) } func CompleteOrder(ctx *fiber.Ctx) error { var data map[string]string // リクエストデータを取得 if err := ctx.BodyParser(&data); err != nil { return err } order := models.Order{} // OrderItemを検索 database.DB.Preload("OrderItems").First(&order, models.Order{ TransactionID: data["source"], }) if order.ID == 0 { ctx.Status((fiber.StatusNotFound)) return ctx.JSON(fiber.Map{ "message": "オーダーが見つかりません", }) } // Orderを保存 order.Complete = true database.DB.Save(&order) // Redisキャッシュに保存したRankingも更新 // Rankingはユーザーの購入金額を示す go func(order models.Order) { ambassadorRevenue := 0.0 adminRevenue := 0.0 for _, item := range order.OrderItems { ambassadorRevenue += item.AmbassadorRevenue adminRevenue += item.AdminRevenue } user := models.User{} user.ID = order.UserID database.DB.First(&user) // https://redis.io/commands/zincrby database.Cache.ZIncrBy(context.Background(), "rankings", ambassadorRevenue, user.Name()) }(order) return ctx.JSON(fiber.Map{ "message": "success", }) } |
routes.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
// routes/routes.go package routes import ( "admin/src/controllers" "admin/src/middleware" "github.com/gofiber/fiber/v2" ) func Setup(app *fiber.App) { // Group api := app.Group("api") // admin admin := api.Group("admin") admin.Post("register", controllers.Register) admin.Post("login", controllers.Login) adminAuthenticated := admin.Use(middleware.IsAuthenticate) adminAuthenticated.Get("user", controllers.User) adminAuthenticated.Post("logout", controllers.Logout) adminAuthenticated.Put("info", controllers.UpdateInfo) adminAuthenticated.Put("password", controllers.UpdatePassword) adminAuthenticated.Get("ambassadors", controllers.Ambassadors) adminAuthenticated.Get("products", controllers.Products) adminAuthenticated.Post("products", controllers.CreateProducts) adminAuthenticated.Get("products/:id", controllers.GetProduct) adminAuthenticated.Put("products/:id", controllers.UpdateProduct) adminAuthenticated.Delete("products/:id", controllers.DeleteProduct) adminAuthenticated.Get("users/:id/links", controllers.Link) adminAuthenticated.Get("orders", controllers.Orders) // Ambassador ambassador := api.Group("ambassador") ambassador.Post("register", controllers.Register) ambassador.Post("login", controllers.Login) ambassador.Get("products/frontend", controllers.ProductFrontend) ambassador.Get("products/backend", controllers.ProductBackend) ambassadorAuthentication := ambassador.Use(middleware.IsAuthenticate) ambassadorAuthentication.Get("user", controllers.User) ambassadorAuthentication.Post("logout", controllers.Logout) ambassadorAuthentication.Put("users/info", controllers.UpdateInfo) ambassadorAuthentication.Put("users/password", controllers.UpdatePassword) ambassadorAuthentication.Post("links", controllers.CreateLink) ambassadorAuthentication.Get("stats", controllers.Stats) ambassadorAuthentication.Get("rankings", controllers.Ranking) // Checkout checkout := api.Group("checkout") checkout.Get("links/:code", controllers.GetLink) checkout.Post("orders", controllers.CreateOrder) checkout.Post("orders/confirm", controllers.CompleteOrder) } |
コメントを残す
コメントを投稿するにはログインしてください。