├── Makefile ├── README.md ├── backend-api ├── Gopkg.lock ├── Gopkg.toml ├── Makefile ├── db │ ├── driver.go │ └── repository.go ├── domain │ ├── item.go │ └── token.go ├── handler │ ├── contecxt.go │ ├── item.go │ └── payment.go ├── infrastructure │ └── router.go ├── init │ └── init.sql └── main.go ├── frontend-spa ├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .postcssrc.js ├── README.md ├── build │ ├── build.js │ ├── check-versions.js │ ├── logo.png │ ├── utils.js │ ├── vue-loader.conf.js │ ├── webpack.base.conf.js │ ├── webpack.dev.conf.js │ └── webpack.prod.conf.js ├── config │ ├── dev.env.js │ ├── index.js │ ├── prod.env.js │ └── test.env.js ├── index.html ├── package-lock.json ├── package.json ├── src │ ├── App.vue │ ├── components │ │ ├── Home.vue │ │ ├── Item.vue │ │ └── ItemCard.vue │ ├── main.js │ └── router │ │ └── index.js ├── static │ └── .gitkeep └── test │ ├── e2e │ ├── custom-assertions │ │ └── elementCount.js │ ├── nightwatch.conf.js │ ├── runner.js │ └── specs │ │ └── test.js │ └── unit │ ├── .eslintrc │ ├── jest.conf.js │ ├── setup.js │ └── specs │ └── HelloWorld.spec.js └── payment-service ├── Makefile ├── client └── client.go ├── proto ├── pay.pb.go └── pay.proto └── server └── server.go /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: pay 2 | pay: ## run payment-service 3 | go run payment-service/server/server.go 4 | .PHONY: api 5 | api: ## run backend-api 6 | go run backend-api/main.go 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![pay-cover.png](https://qiita-image-store.s3.amazonaws.com/0/186028/3d6c3897-c9e2-b119-14e0-6b26bbef9096.png) 2 | 3 | # vue-golang-payment-app 4 | [![Go Report Card](https://goreportcard.com/badge/github.com/po3rin/vue-golang-payment-app)](https://goreportcard.com/report/github.com/po3rin/vue-golang-payment-app) 5 | 6 | # Introduction 7 | 8 | これは Qiita にも掲載されています。 9 | https://qiita.com/po3rin/items/9638eab0a6a70faca86e 10 | 11 | そろそろカード決済の実装経験しとくかと思い、PAY.JPを眺めたらかなりドキュメントが充実してたので使いやすかった。今後、カード決済するサービスを作るのを見越して決済サービスをgRPCでマイクロサービス化してみた。そのまま Vue.js と Go言語を使い、カード決済できるWEBサービスのサンプルを試しに作ってみた。その実装を簡略化してハンズオン形式で紹介します。 12 | 13 | 全コードは GitHub にあげてます。 14 | https://github.com/po3rin/vue-golang-payment-app 15 | 16 | ## 得られるもの 17 | 18 | * Vue.js + Go言語で簡易的なSPAをつくる経験 19 | * gRPC で簡単なマイクロサービスをつくる経験 20 | * PAY.JP を使ったカード決済の流れの理解 21 | 22 | ## 今回使う技術スタック 23 | 24 | フロントエンドは Vue.js。サーバーサイドは Go言語で実装します。それ以外で今回使う技術は下記! 25 | 26 | ### PAY.JP 27 | 28 | ![pay.png](https://qiita-image-store.s3.amazonaws.com/0/186028/68b5acb8-3eb6-3f1b-31b0-1e7ccc320387.png) 29 | 30 | 支払い機能をシンプルなAPIで実装できる!分かりやすい料金形態で決済を導入することが可能です。日本の企業が作ったサービスなので日本語の情報が豊富です。Go言語で実装する方法があまりまとまってないので、今回はそこもお話しします。 31 | 32 | ### gRPC 33 | ![grpc-p.png](https://qiita-image-store.s3.amazonaws.com/0/186028/921bbdc2-a113-bf8d-7792-1dd68f82724a.png) 34 | 35 | そもそもRPCとは、Remote Procedure Call と呼ばれる、別のアドレス空間にあるサブルーチンや手続きを実行することを可能にする技術です。 36 | 37 | そして gRPC はHTTP/2を標準でサポートしたRPCフレームワークです。ProtocolBufferをgRPC用に書いた上で、サポートしている言語(Go Python Ruby Javaなど)にコード書き出しを行うと、異なる言語間でも型保証された通信を行うことができます。出来たのは最近で2015年にGoogleが発表した様子。 38 | 39 | 今回はgRPCを使って決済機能をマイクロサービス化します。これによってAPIサーバーへの影響を下げれる且つ、例えば今回の目指す形(下記に記載)であれば、APIサーバーをRubyで書き換えたいとなっても、RubyからGo言語の処理を叩けるので影響範囲を抑えれます。 40 | 41 | ## 今回目指す形 42 | 43 | ![pay-go-vue.png](https://qiita-image-store.s3.amazonaws.com/0/186028/9df053de-d9e6-0317-12ba-2beade53e587.png) 44 | 45 | 上記のような形を目指していきます。payment-service と item-service 間は gRPC で通信します。本当は商品情報を扱う処理もマイクロサービス化したかったのですが、ハンズオンとしては複雑になりそうなのでやめました。 46 | 47 | データベースはMySQLを使います。ここに商品情報を格納し、フロントエンドに返したりします。 48 | 49 | ちなみにPAY.JPでは直でカード情報をサーバーに渡して処理する形も昔はできましたが、現在は推奨されていません。クレジットカード情報をいかに所持せずに決済処理を提供するかが必要になっています。 50 | 51 | 52 | ディレクトリ構造は下記のようにしました。 53 | 54 | ``` 55 | .(GOPATH) 56 | └── src 57 | └── vue-golang-payment-app 58 | ├── backend-api -------(フロントエンドとやりとりするJSON API) 59 | ├── frontend-spa ------(Vue.jsで作るフロントエンド) 60 | └── payment-service ---(gRPCでつくるカード決済マイクロサービス) 61 | ``` 62 | 63 | ## Go言語 + gRPC でカード決済サービスをつくる 64 | 65 | ![pay-grpc-pnly.png](https://qiita-image-store.s3.amazonaws.com/0/186028/baa65fa7-b279-0e81-e7aa-1e88c8c0a391.png) 66 | 67 | 68 | まずは上記の形をめざします。 69 | payment-service というディレクトリにPAY.JPのAPIを叩いて実際に支払いをするマイクロサービスをつくります。手順としては3ステップです。 70 | 71 | ![grpc3.png](https://qiita-image-store.s3.amazonaws.com/0/186028/ae79f294-323c-bce0-8e0d-714be3539846.png) 72 | 73 | protoファイルからRPC通信で使うコードを自動生成し、そのコードを使ってサーバーを実装します。 74 | 75 | ### gRPC開発環境を作る 76 | 77 | まずはgRPCを使えるようにするのと、protoファイルからGo言語のコードを自動生成するツールのインストール 78 | 79 | ```bash 80 | $ go get -u google.golang.org/grpc 81 | $ go get -u github.com/golang/protobuf/protoc-gen-go 82 | ``` 83 | ちなみにbinにパスが通っているか確認。これがないとコード自動生成時にエラーが出ます。 84 | 85 | ```bash 86 | export PATH=$PATH:$GOPATH/bin 87 | ``` 88 | 89 | そして RPC するコードを生成する protoc コンパイラーをインストールします。下記で自分のOS等に合うものをダウンロードして展開します 90 | https://github.com/google/protobuf/releases 91 | 92 | そしてそれをパスの通っている場所におきます。僕は /usr/local/bin/ に起きました 93 | 94 | ```bash 95 | $ cp ~/Download/protoc-3.6.0-osx-x86_64/bin/protoc /usr/local/bin/ 96 | ``` 97 | 98 | ここでprotocコマンドが使えるか確認しておきましょう 99 | 100 | ```bash 101 | $ protoc --version 102 | ``` 103 | 104 | ### protoファイル作成 105 | 106 | payment-service/proto/pay.proto を作ります。 107 | そこで Protocol Buffers で使う gRPC service と request と response それぞれの型を定義します。 108 | 109 | ```proto 110 | syntax = "proto3"; 111 | 112 | package paymentservice; 113 | 114 | service PayManager { 115 | // 支払いを行うサービスを定義 116 | rpc Charge (PayRequest) returns (PayResponse) {} 117 | } 118 | 119 | // カード決済に使うパラメーターをリクエストに定義 120 | message PayRequest { 121 | int64 id = 1; 122 | string token = 2; 123 | int64 amount = 3; 124 | string name = 4; 125 | string description = 5; 126 | } 127 | 128 | // カード決済後のレスポンスを定義 129 | message PayResponse { 130 | bool paid = 1; 131 | bool captured = 3; 132 | int64 amount = 2; 133 | } 134 | ``` 135 | 136 | message宣言でリクエストやレスポンス等で使う型を定義します。 137 | service宣言でサービスを定義し、定義したmessageを引数や返り値に定義できます。 138 | これだけでRPCするためのGo言語のコードが自動的に作られます。 139 | 140 | ### Protocol Buffer から Go言語への書き出し 141 | 142 | ここまででGo言語のコードを生成する準備が整いました!早速下記を実行してみましょう 143 | 144 | ```bash 145 | $ protoc --go_out=plugins=grpc:. proto/pay.proto 146 | ``` 147 | 148 | これでGo言語で書かれたソースコード proto/pay.pd.go が出来ています。中身を確認してみましょう 149 | 下記の構造体や interface が確認できるはずです。 150 | 151 | ```go 152 | // ... 省略 153 | 154 | // message宣言で定義された PayRequest の定義から生成 155 | type PayRequest struct { 156 | Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` 157 | Token string `protobuf:"bytes,2,opt,name=token,proto3" json:"token,omitempty"` 158 | Amount int64 `protobuf:"varint,3,opt,name=amount,proto3" json:"amount,omitempty"` 159 | Name string `protobuf:"bytes,4,opt,name=name,proto3" json:"name,omitempty"` 160 | Description string `protobuf:"bytes,5,opt,name=description,proto3" json:"description,omitempty"` 161 | // ... 162 | } 163 | 164 | // ... 省略 165 | 166 | // message宣言で定義された PayResponse の定義から生成 167 | type PayResponse struct { 168 | Paid bool `protobuf:"varint,1,opt,name=paid,proto3" json:"paid,omitempty"` 169 | Captured bool `protobuf:"varint,3,opt,name=captured,proto3" json:"captured,omitempty"` 170 | Amount int64 `protobuf:"varint,2,opt,name=amount,proto3" json:"amount,omitempty"` 171 | // ... 172 | } 173 | 174 | // ... 省略 175 | 176 | // 先ほど定義したserviceから生成された interface 177 | type PayManagerServer interface { 178 | Charge(context.Context, *PayRequest) (*PayResponse, error) 179 | } 180 | ``` 181 | 182 | 基本、上のコードはいじりません。変更を加える時は.protoファイルを変更して、また先ほどの生成コマンドを叩けば更新されます。このinterfaceやstructを使って、サーバー側のコードを書いていきます。 183 | 184 | ### 実際の支払い処理を実装 185 | 186 | 先ほど生成された interface を満たすようにコード書いていきます。payment-service/server/server.go を作成します。 187 | 188 | そしてついにここで PAY.JP がでてきます。PAY.JPの API を叩くクライアントのコードを実装しても良いですが、今回は https://github.com/payjp/payjp-go を使います。これは PAY.JP のAPI とのやりとりを抽象化してくれているパッケージです。詳しいドキュメントはありませんが、パッケージのGo言語のコードに日本語でコメントがついているので、使い方も簡単に理解できます。 189 | 下記はgRPCで商品情報とカードのToken情報を受け取って、実際に支払いを行います。 190 | 191 | ```go 192 | package main 193 | 194 | import ( 195 | // ... 196 | 197 | gpay "vue-golang-payment-app/payment-service/proto" 198 | 199 | payjp "github.com/payjp/payjp-go/v1" 200 | "google.golang.org/grpc" 201 | "google.golang.org/grpc/reflection" 202 | ) 203 | 204 | const ( 205 | port = ":50051" 206 | ) 207 | 208 | // server is used to implement sa 209 | type server struct{} 210 | 211 | func (s *server) Charge(ctx context.Context, req *gpay.PayRequest) (*gpay.PayResponse, error) { 212 | // PAI の初期化 213 | pay := payjp.New(os.Getenv("PAYJP_TEST_SECRET_KEY"), nil) 214 | 215 | // 支払いをします。第一引数に支払い金額、第二引数に支払いの方法や設定を入れます。 216 | charge, err := pay.Charge.Create(int(req.Amount), payjp.Charge{ 217 | // 現在はjpyのみサポート 218 | Currency: "jpy", 219 | // カード情報、顧客ID、カードトークンのいずれかを指定。今回はToken使います。 220 | CardToken: req.Token, 221 | Capture: true, 222 | // 概要のテキストを設定できます 223 | Description: req.Name + ":" + req.Description, 224 | }) 225 | if err != nil { 226 | return nil, err 227 | } 228 | 229 | // 支払った結果から、Response生成 230 | res := &gpay.PayResponse{ 231 | Paid: charge.Paid, 232 | Captured: charge.Captured, 233 | Amount: int64(charge.Amount), 234 | } 235 | return res, nil 236 | } 237 | 238 | func main() { 239 | lis, err := net.Listen("tcp", port) 240 | if err != nil { 241 | log.Fatalf("failed to listen: %v", err) 242 | } 243 | s := grpc.NewServer() 244 | gpay.RegisterPayManagerServer(s, &server{}) 245 | 246 | // Register reflection service on gRPC server. 247 | reflection.Register(s) 248 | log.Printf("gRPC Server started: localhost%s\n", port) 249 | if err := s.Serve(lis); err != nil { 250 | log.Fatalf("failed to serve: %v", err) 251 | } 252 | } 253 | ``` 254 | 255 | 環境変数である PAYJP_TEST_SECRET_KEY は PAY.JP にアカウント登録をして手に入れます。下記にアクセスして管理画面 > API > テスト秘密鍵 で手に入ります。 256 | https://pay.jp/ 257 | 258 | この鍵はテスト用のKeyなので実際に支払いが行われることはありません。この文字列を環境変数に登録しておきます。僕はdirenvを使っているので下記のように登録してます。 259 | 260 | ```bash 261 | export PAYJP_TEST_SECRET_KEY=sk_test_************** 262 | ``` 263 | 264 | これで支払いの為のマイクロサービスが完成しました。 265 | しかし、このサービスはHTTPでやりとりできません。RPCを話すためです。もちろん curlコマンドも使えません。 266 | ゆえにフロントエンドとやりとりするAPIサーバーを作り、APIサーバーからChargeメソッドをgRPCで叩くようにしましょう。 267 | 268 | ## Go言語で JSON API サーバー実装 269 | 270 | ![sever-pay.png](https://qiita-image-store.s3.amazonaws.com/0/186028/dcd85bee-568d-adb4-979b-76723ed70556.png) 271 | 272 | 上記のような構成をめざします。上には記載していませんが、DBから商品データをフロントエンドに渡すAPIもつくります。 273 | つまり下記の機能があるAPIサーバーを作ります。 274 | 275 | ``` 276 | GET /api/v1/items --> 商品を全て返す 277 | GET /api/v1/items/:id --> id で指定された商品情報を返す 278 | POST /api/v1/charge/items/:id --> id で指定された商品を購入する (Tokenを渡す必要あり) 279 | ``` 280 | 281 | ディレクトリ構成は下記。今回はハンズオンなのでゆるいアーキテクチャにしてます。 282 | 283 | ``` 284 | . 285 | ├── Makefile 286 | ├── db-----------------(DB接続とDBとのやりとり) 287 | │   ├── driver.go 288 | │   └── repository.go 289 | ├── domai--------------(entity層) 290 | │   ├── item.go 291 | │   └── token.go 292 | ├── handler-------------(handler) 293 | │   ├── contecxt.go 294 | │   ├── item.go 295 | │   └── payment.go 296 | ├── infrastructure------(ルーターの設定) 297 | │   └── router.go 298 | ├── init----------------(DBの初期化用) 299 | │   └── init.sql 300 | └── main.go 301 | ``` 302 | 303 | ### domain 層 304 | 305 | まずは domainパッケージを作りましょう。ここでサーバーで使うデータ型をまとめます。 306 | 307 | domain/item.go をつくります。 308 | 309 | ```go 310 | package domain 311 | 312 | // Item - set of item 313 | type Item struct { 314 | ID int64 315 | Name string 316 | Description string 317 | Amount int64 318 | } 319 | 320 | // Items -set of item list 321 | type Items []Item 322 | 323 | ``` 324 | 325 | domain/token.go もつくります。 326 | 327 | ```go 328 | package domain 329 | 330 | // Payment - PAY.JP payment parameter 331 | type Payment struct { 332 | Token string 333 | } 334 | ``` 335 | 336 | これでアプリケーションで使うデータ構造が定義できました。 337 | 338 | ### DBとやりとりする層 339 | 340 | 次にdbとやりとりするパッケージをつくります。 341 | 342 | db/driver.go 343 | 344 | ```go 345 | package db 346 | 347 | import ( 348 | "database/sql" 349 | "os" 350 | 351 | _ "github.com/go-sql-driver/mysql" 352 | ) 353 | 354 | // Conn - sql connection handler 355 | var Conn *sql.DB 356 | 357 | // NewSQLHandler - init sql handler 358 | func init() { 359 | user := os.Getenv("MYSQL_USER") 360 | pass := os.Getenv("MYSQL_PASSWORD") 361 | name := os.Getenv("MYSQL_DATABASE") 362 | 363 | dbconf := user + ":" + pass + "@/" + name 364 | conn, err := sql.Open("mysql", dbconf) 365 | if err != nil { 366 | panic(err.Error) 367 | } 368 | Conn = conn 369 | } 370 | 371 | ``` 372 | 373 | コードの中にある環境変数は各自設定をお願いします。僕はローカル確認用に MYSQL_USER は root。MYSQL_DATABASE は itemsDB としました。 374 | ここでのポイントは init() です。パッケージを初期化する際に呼ばれます。参考は下記 375 | [init関数のふしぎ #golang](https://qiita.com/tenntenn/items/7c70e3451ac783999b4f) 376 | 377 | ここで 初期化した Conn を通して MySQL とやりとりします。 378 | db/repository.go を作ります。ここでは商品リストを全て返す処理とid指定で商品を一つ返す処理をつくります。 379 | 380 | ```go 381 | package db 382 | 383 | import ( 384 | // ... 385 | "vue-golang-payment-app/backend-api/domain" 386 | ) 387 | 388 | // SelectAllItems - select all posts 389 | func SelectAllItems() (items domain.Items, err error) { 390 | stmt, err := Conn.Query("SELECT * FROM items") 391 | if err != nil { 392 | return 393 | } 394 | defer stmt.Close() 395 | for stmt.Next() { 396 | var id int64 397 | var name string 398 | var description string 399 | var amount int64 400 | if err := stmt.Scan(&id, &name, &description, &amount); err != nil { 401 | continue 402 | } 403 | item := domain.Item{ 404 | ID: id, 405 | Name: name, 406 | Description: description, 407 | Amount: amount, 408 | } 409 | items = append(items, item) 410 | } 411 | return 412 | } 413 | 414 | // SelectItem - select post 415 | func SelectItem(identifier int64) (item domain.Item, err error) { 416 | stmt, err := Conn.Prepare(fmt.Sprintf("SELECT * FROM items WHERE id = ? LIMIT 1")) 417 | if err != nil { 418 | fmt.Println(err) 419 | return 420 | } 421 | defer stmt.Close() 422 | var id int64 423 | var name string 424 | var description string 425 | var amount int64 426 | err = stmt.QueryRow(identifier).Scan(&id, &name, &description, &amount) 427 | if err != nil { 428 | return 429 | } 430 | item.ID = id 431 | item.Name = name 432 | item.Description = description 433 | item.Amount = amount 434 | return 435 | } 436 | ``` 437 | 438 | Go言語の面白い点で、戻り値に名前をつけて定義した関数は return だけで終了しても構いません。これでもちゃんと item と err が返ります。 439 | 参考は下記 440 | [Goは関数の戻り値に名前を付けられる / deferの驚き](http://imagawa.hatenadiary.jp/entry/2016/12/08/190000) 441 | 442 | これでデータベースを操作するパッケージができました。 443 | 444 | ### router 部分 445 | つづいて API の router 部分を作っていきましょう。 446 | まずは 起点になる main.go を作ります。 447 | 448 | ```go 449 | package main 450 | 451 | import ( 452 | // ... 453 | 454 | "vue-golang-payment-app/backend-api/infrastructure" 455 | ) 456 | 457 | func main() { 458 | infrastructure.Router.Run(os.Getenv("API_SERVER_PORT")) 459 | } 460 | ``` 461 | 462 | 環境変数 API_SERVER_PORT はAPIを走らせるPORTを渡します。 463 | 下記のように僕は8888番ポートで走らせます。 464 | 465 | ```bash 466 | export API_SERVER_PORT=:8888 467 | ``` 468 | 469 | さて、ここで読み込んでいる infrastructure パッケージを作りましょう 470 | infrastructure/router.go ですね。JSON API をなのでJSONを扱うのが少し面倒なのでここでフレームワークの gin を使いましょう。 471 | GitHubにあるドキュメントが参考になります。https://github.com/gin-gonic/gin 472 | 473 | ちなみに"github.com/gin-contrib/cors"は gin 用のCORS設定パッケージです。今回は Vue から叩くのでこちらも使います。 474 | 475 | ```go 476 | package infrastructure 477 | 478 | import ( 479 | "os" 480 | "vue-golang-payment-app/backend-api/handler" 481 | 482 | "github.com/gin-contrib/cors" 483 | gin "github.com/gin-gonic/gin" 484 | ) 485 | 486 | // Router - router api server 487 | var Router *gin.Engine 488 | 489 | func init() { 490 | router := gin.Default() 491 | 492 | router.Use(cors.New(cors.Config{ 493 | AllowOrigins: []string{os.Getenv("CLIENT_CORS_ADDR")}, 494 | AllowMethods: []string{"GET", "POST"}, 495 | AllowHeaders: []string{"Origin", "Content-Type"}, 496 | })) 497 | 498 | router.GET("/api/v1/items", func(c *gin.Context) { handler.GetLists(c) }) 499 | router.GET("/api/v1/items/:id", func(c *gin.Context) { handler.GetItem(c) }) 500 | router.POST("/api/v1/charge/items/:id", func(c *gin.Context) { handler.Charge(c) }) 501 | 502 | Router = router 503 | } 504 | ``` 505 | 506 | 環境変数 CLIENT_CORS_ADDR も忘れずに!僕は http://localhost:8080 に設定してます(あとで Vue.js を localhost:8080 で立ち上げるため) 507 | 508 | これでmain.goで呼ばれていたRouterの設定が終わりました。続いて、APIへのリクエストがあった際の実際の処理を handler パッケージに書きましょう。 509 | 510 | ### handler 部分 511 | 512 | 今回は ginフレームワークにアプリケーション全てを依存させないめに interface を使って gin.Context を抽象化します 513 | handler/contecxt.go を作ります。 514 | 515 | ```go 516 | package handler 517 | 518 | // Context - context interface 519 | type Context interface { 520 | Param(string) string 521 | Bind(interface{}) error 522 | Status(int) 523 | JSON(int, interface{}) 524 | } 525 | ``` 526 | 527 | そして、商品データをフロントに返す handler を handler/item.go に書きます。 528 | 529 | ```go 530 | package handler 531 | 532 | import ( 533 | // ... 534 | 535 | "vue-golang-payment-app/backend-api/db" 536 | ) 537 | 538 | // GetLists - get all items 539 | func GetLists(c Context) { 540 | res, err := db.SelectAllItems() 541 | if err != nil { 542 | c.JSON(http.StatusInternalServerError, nil) 543 | return 544 | } 545 | c.JSON(http.StatusOK, res) 546 | } 547 | 548 | // GetItem - get item by id 549 | func GetItem(c Context) { 550 | identifer, err := strconv.Atoi(c.Param("id")) 551 | if err != nil { 552 | c.JSON(http.StatusInternalServerError, nil) 553 | return 554 | } 555 | res, err := db.SelectItem(int64(identifer)) 556 | if err != nil { 557 | c.JSON(http.StatusInternalServerError, nil) 558 | return 559 | } 560 | c.JSON(http.StatusOK, res) 561 | } 562 | ``` 563 | 564 | そしてエラー処理は一旦全て簡易化のため 500エラーで返してます。 565 | また、支払いを行う handler を書きます。ここではレクエストで渡された id を使って DB から商品情報を取得して、最初に作った gRPCサーバーの Charge に引数として cardToken と商品情報と渡して実行します。 566 | 567 | ```go 568 | package handler 569 | 570 | import ( 571 | // ... 572 | 573 | "vue-golang-payment-app/backend-api/db" 574 | "vue-golang-payment-app/backend-api/domain" 575 | gpay "vue-golang-payment-app/payment-service/proto" 576 | 577 | "google.golang.org/grpc" 578 | ) 579 | 580 | var addr = "localhost:50051" 581 | 582 | // Charge exec payment-service charge 583 | func Charge(c Context) { 584 | //パラメータや body をうけとる 585 | t := domain.Payment{} 586 | c.Bind(&t) 587 | identifer, err := strconv.Atoi(c.Param("id")) 588 | if err != nil { 589 | c.JSON(http.StatusInternalServerError, err) 590 | } 591 | 592 | // id から item情報取得 593 | res, err := db.SelectItem(int64(identifer)) 594 | if err != nil { 595 | c.JSON(http.StatusInternalServerError, err) 596 | } 597 | // gRPC サーバーに送る Request を作成 598 | greq := &gpay.PayRequest{ 599 | Id: int64(identifer), 600 | Token: t.Token, 601 | Amount: res.Amount, 602 | Name: res.Name, 603 | Description: res.Description, 604 | } 605 | 606 | //IPアドレス(ここではlocalhost)とポート番号(ここでは50051)を指定して、サーバーと接続する 607 | conn, err := grpc.Dial(addr, grpc.WithInsecure()) 608 | if err != nil { 609 | c.JSON(http.StatusForbidden, err) 610 | } 611 | defer conn.Close() 612 | client := gpay.NewPayManagerClient(conn) 613 | 614 | // gRPCマイクロサービスの支払い処理関数を叩く 615 | gres, err := client.Charge(context.Background(), greq) 616 | if err != nil { 617 | c.JSON(http.StatusForbidden, err) 618 | return 619 | } 620 | c.JSON(http.StatusOK, gres) 621 | } 622 | ``` 623 | 624 | お疲れ様です。これで下記の機能がある APIサーバーができました。 625 | 626 | ``` 627 | GET /api/v1/items --> 商品を全て返す 628 | GET /api/v1/items/:id --> id で指定された商品情報を返す 629 | POST /api/v1/charge/items/:id --> id で指定された商品を購入する (Tokenを渡す必要あり) 630 | ``` 631 | 632 | ### 動作確認 633 | 634 | 動作確認するためにMySQLにtestデータを入れます。 635 | ローカルのMySQLで "itemsDB"という名前のデータベースを CREATE して 636 | init/init.sql を作成しましょう 637 | 638 | ```sql 639 | DROP TABLE IF EXISTS items; 640 | 641 | CREATE TABLE items ( 642 | id integer AUTO_INCREMENT, 643 | name varchar(255), 644 | description varchar(255), 645 | amount integer, 646 | primary key(id) 647 | ); 648 | 649 | INSERT INTO items (name, description, amount) 650 | VALUES 651 | ('toy', 'test-toy', 2000); 652 | 653 | INSERT INTO items (name, description, amount) 654 | VALUES 655 | ('game', 'test-game', 6000); 656 | ``` 657 | 658 | そしてこれをデータベースに注入します。 659 | 660 | ```bash 661 | mysql -p -u root itemsDB < init/init.sql 662 | ``` 663 | 664 | ちょっとここらで動くか確認しましょう。 665 | 666 | ```bash 667 | curl -X GET localhost:8888/api/v1/items/1 668 | {"ID":1,"Name":"toy","Description":"test-toy","Amount":2000} 669 | 670 | curl -X GET localhost:8888/api/v1/items 671 | [{"ID":1,"Name":"toy","Description":"test-toy","Amount":2000},{"ID":2,"Name":"game","Description":"test-game","Amount":6000}] 672 | 673 | curl -X POST localhost:8888/api/v1/charge/items/1 674 | {"code":2,"message":"Charge.Create() parameter error: One of the following parameters is required: CustomerID, CardToken, Card"} 675 | ``` 676 | 677 | 決済処理だけ必要なパラメータがないと言われています。最初に構成をお話しした通り、Tokenは Vue で直接 PAY.JP とやりとりして手に入れます。 678 | ではついに最終決戦。Vue.js でフロントを作ります。 679 | 680 | 681 | ## Vue.js でクライアントを実装しよう 682 | 683 | くー長い!もう少しで完成です。最終段階です。最初に見せた形までもっていきます。 684 | 685 | ![pay-go-vue.png](https://qiita-image-store.s3.amazonaws.com/0/186028/e4812cfb-2b49-1ce6-080a-5da0b264534f.png) 686 | 687 | 688 | 今回は vue-cli でプロジェクトのひな形を作ります。下記をプロジェクトのルート(GOPATH/src/vue-golang-payment-app)で実行 689 | 690 | ```bash 691 | $ npm install -g vue-cli                  # vue-cli がなければインストール 692 | $ vue init webpack frontend-spa    # 何か色々聞かれるが全部 Enter で可能。vue-router は必ず入れておく。 693 | ``` 694 | 695 | frontend-spa/src で下記のように.vueファイルを作ります。 696 | 697 | ```bash 698 | src 699 | ├── App.vue 700 | ├── components 701 | │   ├── Home.vue 702 | │   ├── Item.vue 703 | │   └── ItemCard.vue 704 | ├── main.js 705 | └── router 706 | └── index.js 707 | ``` 708 | 709 | Home.vue は商品リスト 710 | Item.vue は商品の詳細ページ 711 | ItemCard.vue は Home.vue で使う商品を表示するコンポーネントです。 712 | 713 | 画面はひどく殺風景ですが下のようになります。 714 | 715 | HOME画面(商品リスト表示) 716 | スクリーンショット 2018-07-23 02.02.14.png 717 | 718 | 商品詳細画面(商品リスト表示) 719 | スクリーンショット 2018-07-23 02.02.21.png 720 | 721 | 一旦サーバーとのやりとりに必要な axios モジュールを加えます。axios は Promise ベースの HTTPクライアントです。 722 | 723 | ```bash 724 | $ npm install axios --save 725 | ``` 726 | 727 | また、今回 PAY.JP で カード情報を Token化するために https://github.com/ngs/vue-payjp-checkout を使います。 728 | これは PAY.JP のカード情報入力コンポーネントを Vue.js で使えるようにしたものです。このような画面がひらくようになります。 729 | 730 | スクリーンショット 2018-07-22 16.43.02.png 731 | 732 | 上のPAY.JPのクレジットカード入力フォームを使うと、開発者はクレジットカード番号に触れることなく決済機能が提供できるようになります。クレジットカード番号は盗み取られたりすると大きなリスクになります。そのため、今回はこの入力フォームを使いましょう。 733 | 734 | ```bash 735 | $ npm install --save vue-payjp-checkout 736 | ``` 737 | 738 | vue-payjp-checkout を Vue.js で使えるように src/main.js に一行追加します。 739 | 740 | ```js 741 | // 省略... 742 | import PayjpCheckout from 'vue-payjp-checkout' 743 | 744 | Vue.config.productionTip = false 745 | 746 | Vue.use(PayjpCheckout) 747 | /* eslint-disable no-new */ 748 | new Vue({ 749 | el: '#app', 750 | router, 751 | components: { App }, 752 | template: '' 753 | }) 754 | 755 | ``` 756 | 757 | これで payjp-checkout のコンポーネントが使えるようになりました。 758 | 759 | ### router 設定 760 | 761 | 次にvue-router でルーティングを正しく設定しましょう。 762 | src/router/index.js を修正します。 763 | 764 | ```js 765 | // ...省略 766 | 767 | import Home from '@/components/Home' 768 | import Item from '@/components/Item' 769 | 770 | Vue.use(Router) 771 | 772 | export default new Router({ 773 | routes: [ 774 | { 775 | path: '/', 776 | name: 'Home', 777 | component: Home 778 | }, 779 | { 780 | path: '/items/:id', 781 | name: 'Item', 782 | component: Item 783 | } 784 | ] 785 | }) 786 | ``` 787 | 788 | :id は動的ルーティングのパラメータを表します。つまりここに商品番号が入ると、その商品詳細ページがみれるというつくりです。 789 | この時点ではまだ Homeコンポーネントも Itemコンポーネントとも作ってないのでエラーが出ます。 790 | 791 | ### コンポーネント作成 792 | 793 | 次に Home.vue をつくります。最初に APIサーバーから商品一覧をとってきて、v-for でデータの数だけ後につくる ItemCard.vue に渡してあげます。 794 | 795 | ```html 796 | 805 | 806 | 833 | 834 | 835 | ``` 836 | 837 | pageto イベントではクリックしたアイテムのidを使って、その商品の詳細ページに飛びます。 838 | この時点ではItemCard.vue がないと言われます。 839 | 840 | 841 | 次に Home.vue で読み込んでいる ItemCard.vue を作りましょう。Home.vue から props で渡ってきた商品データを描写してるだけです。 842 | 843 | ```html 844 | 851 | 852 | 860 | 861 | 868 | ``` 869 | 870 | ここまでで商品一覧ページは完成しました。 871 | 872 | あとは商品詳細ページをつくりましょう。商品のデータの表示はもちろん、ここではPAY.JP の API と直接やりとりして、カードの情報をトークン化して、そのトークンを使って、さきほどGo言語で作った API を叩きます。payjp-checkoutのコンポーネントは install した vue-payjp-checkout モジュールからもってきてます。 873 | 874 | ```html 875 | 894 | 895 | 931 | 932 | ``` 933 | 934 | << PAY.JPの管理画面にある公開テストKey >> に PAY.JP の公開テストキーをいれるのを忘れずに! 管理画面から手に入ります。 935 | 936 | vue-payjp-checkout と本家の Checkout でパラメーター名が違いますが、下記の Checkout リファレンスと vue-payjp-chackout の index.ts の中身を見比べれば vue-payjp-checkout のパラメーターがどういう意味なのか確認できます。 937 | [PAY.JP Checkout 公式リファレンス](https://pay.jp/docs/checkout) 938 | [vue-payjp-checkout の index.ts](https://github.com/ngs/vue-payjp-checkout/blob/master/src/index.ts) 939 | 940 | また、ここでのポイントは beforeDestroy() で実行される window.PayjpCheckout = null です。これがないとページを移動したりするとカード登録ボタンが消えてしまいます。これは payjp-checkout のコンポネーネントがHTMLドキュメントの読み込みを起点として決済フォームを構築するためです。 そこでインスタンスが破棄される前に呼ばれる beforeDestroy() のライフサイクルで window.PayjpCheckout を一回空にして次のページ移動でもう一度コンポーネントを構築するようにしています。 941 | 942 | 参考にしたサイトでは Timeout で待ったりしていましたので、対処の仕方は色々あります。 943 | [PAY.JPのチェックアウトのスクリプトをVue.jsのSPAで実装する](https://tackeyy.com/blog/posts/implement-payjp-checkout-with-vue-spa) 944 | 945 | 上のコードでは Token化した後すぐにその Token を使って支払いに入っていますが、もちろんToken化したあと確認ページへ遷移させるという実装も可能ですね。 946 | 947 | 948 | 949 | ## 動作確認 950 | 951 | 本当にお疲れ様です。実装は全て終わりました。 952 | 実際に動くか確認してみましょう。 953 | 954 | backend API の立ち上げ 955 | 956 | ```bash 957 | $ go run backend-api/main.go 958 | ``` 959 | 960 | gRPC サーバーの立ち上げ 961 | 962 | ```bash 963 | $ go run payment-service/server/server.go 964 | ``` 965 | 966 | Vue で 作った SPA の立ち上げ 967 | 968 | ```bash 969 | $ npm run dev 970 | ``` 971 | 972 | これで localhost:8080 にアクセスしてください。 973 | (別のポート番号で立ち上がっている場合もあるので注意。その際は API の CORS の設定もそこに合わせます。) 974 | 975 | カードデータはPAY.JPが用意しているテスト用の情報をいれます。 976 | 977 | スクリーンショット 2018-07-22 16.43.02.png 978 | 979 | 購入確定ボタンを押せば「商品の購入が完了しました!」と画面にでているはずです。 980 | ここまでいけば支払い情報が PAY.JP の管理画面で確認できます。 981 | 982 | スクリーンショット 2018-07-22 17.54.21.png 983 | 984 | ## まとめ 985 | 986 | これで Vue.js + Go言語 + PAY.JP でカード決済できるWEBアプリケーションができました。めちゃくちゃ長くなりました。あとはUI整えたり商品管理画面つくったりでプロダクトに近づけていけば良さそうです。もしミスがあったらご指摘お願いします!! 987 | 988 | 全コードは GitHub にあげてます。 989 | https://github.com/po3rin/vue-golang-payment-app 990 | 991 | -------------------------------------------------------------------------------- /backend-api/Gopkg.lock: -------------------------------------------------------------------------------- 1 | # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. 2 | 3 | 4 | [[projects]] 5 | name = "github.com/gin-contrib/cors" 6 | packages = ["."] 7 | revision = "cf4846e6a636a76237a28d9286f163c132e841bc" 8 | version = "v1.2" 9 | 10 | [[projects]] 11 | branch = "master" 12 | name = "github.com/gin-contrib/sse" 13 | packages = ["."] 14 | revision = "22d885f9ecc78bf4ee5d72b937e4bbcdc58e8cae" 15 | 16 | [[projects]] 17 | name = "github.com/gin-gonic/gin" 18 | packages = [ 19 | ".", 20 | "binding", 21 | "render" 22 | ] 23 | revision = "d459835d2b077e44f7c9b453505ee29881d5d12d" 24 | version = "v1.2" 25 | 26 | [[projects]] 27 | name = "github.com/go-sql-driver/mysql" 28 | packages = ["."] 29 | revision = "d523deb1b23d913de5bdada721a6071e71283618" 30 | version = "v1.4.0" 31 | 32 | [[projects]] 33 | name = "github.com/golang/protobuf" 34 | packages = [ 35 | "proto", 36 | "ptypes", 37 | "ptypes/any", 38 | "ptypes/duration", 39 | "ptypes/timestamp" 40 | ] 41 | revision = "b4deda0973fb4c70b50d226b1af49f3da59f5265" 42 | version = "v1.1.0" 43 | 44 | [[projects]] 45 | name = "github.com/mattn/go-isatty" 46 | packages = ["."] 47 | revision = "0360b2af4f38e8d38c7fce2a9f4e702702d73a39" 48 | version = "v0.0.3" 49 | 50 | [[projects]] 51 | name = "github.com/ugorji/go" 52 | packages = ["codec"] 53 | revision = "b4c50a2b199d93b13dc15e78929cfb23bfdf21ab" 54 | version = "v1.1.1" 55 | 56 | [[projects]] 57 | branch = "master" 58 | name = "golang.org/x/net" 59 | packages = [ 60 | "context", 61 | "http/httpguts", 62 | "http2", 63 | "http2/hpack", 64 | "idna", 65 | "internal/timeseries", 66 | "trace" 67 | ] 68 | revision = "a680a1efc54dd51c040b3b5ce4939ea3cf2ea0d1" 69 | 70 | [[projects]] 71 | branch = "master" 72 | name = "golang.org/x/sys" 73 | packages = ["unix"] 74 | revision = "ac767d655b305d4e9612f5f6e33120b9176c4ad4" 75 | 76 | [[projects]] 77 | name = "golang.org/x/text" 78 | packages = [ 79 | "collate", 80 | "collate/build", 81 | "internal/colltab", 82 | "internal/gen", 83 | "internal/tag", 84 | "internal/triegen", 85 | "internal/ucd", 86 | "language", 87 | "secure/bidirule", 88 | "transform", 89 | "unicode/bidi", 90 | "unicode/cldr", 91 | "unicode/norm", 92 | "unicode/rangetable" 93 | ] 94 | revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0" 95 | version = "v0.3.0" 96 | 97 | [[projects]] 98 | name = "google.golang.org/appengine" 99 | packages = ["cloudsql"] 100 | revision = "b1f26356af11148e710935ed1ac8a7f5702c7612" 101 | version = "v1.1.0" 102 | 103 | [[projects]] 104 | branch = "master" 105 | name = "google.golang.org/genproto" 106 | packages = ["googleapis/rpc/status"] 107 | revision = "fedd2861243fd1a8152376292b921b394c7bef7e" 108 | 109 | [[projects]] 110 | name = "google.golang.org/grpc" 111 | packages = [ 112 | ".", 113 | "balancer", 114 | "balancer/base", 115 | "balancer/roundrobin", 116 | "codes", 117 | "connectivity", 118 | "credentials", 119 | "encoding", 120 | "encoding/proto", 121 | "grpclog", 122 | "internal", 123 | "internal/backoff", 124 | "internal/channelz", 125 | "internal/grpcrand", 126 | "keepalive", 127 | "metadata", 128 | "naming", 129 | "peer", 130 | "resolver", 131 | "resolver/dns", 132 | "resolver/passthrough", 133 | "stats", 134 | "status", 135 | "tap", 136 | "transport" 137 | ] 138 | revision = "168a6198bcb0ef175f7dacec0b8691fc141dc9b8" 139 | version = "v1.13.0" 140 | 141 | [[projects]] 142 | name = "gopkg.in/go-playground/validator.v8" 143 | packages = ["."] 144 | revision = "5f1438d3fca68893a817e4a66806cea46a9e4ebf" 145 | version = "v8.18.2" 146 | 147 | [[projects]] 148 | name = "gopkg.in/yaml.v2" 149 | packages = ["."] 150 | revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183" 151 | version = "v2.2.1" 152 | 153 | [solve-meta] 154 | analyzer-name = "dep" 155 | analyzer-version = 1 156 | inputs-digest = "7a1230f3498e97d4961242601d92ea3269a7b8ffe50de72fb90767ac62f5fc48" 157 | solver-name = "gps-cdcl" 158 | solver-version = 1 159 | -------------------------------------------------------------------------------- /backend-api/Gopkg.toml: -------------------------------------------------------------------------------- 1 | # Gopkg.toml example 2 | # 3 | # Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md 4 | # for detailed Gopkg.toml documentation. 5 | # 6 | # required = ["github.com/user/thing/cmd/thing"] 7 | # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] 8 | # 9 | # [[constraint]] 10 | # name = "github.com/user/project" 11 | # version = "1.0.0" 12 | # 13 | # [[constraint]] 14 | # name = "github.com/user/project2" 15 | # branch = "dev" 16 | # source = "github.com/myfork/project2" 17 | # 18 | # [[override]] 19 | # name = "github.com/x/y" 20 | # version = "2.4.0" 21 | # 22 | # [prune] 23 | # non-go = false 24 | # go-tests = true 25 | # unused-packages = true 26 | 27 | 28 | [[constraint]] 29 | name = "github.com/gin-contrib/cors" 30 | version = "1.2.0" 31 | 32 | [[constraint]] 33 | name = "github.com/gin-gonic/gin" 34 | version = "1.2.0" 35 | 36 | [prune] 37 | go-tests = true 38 | unused-packages = true 39 | -------------------------------------------------------------------------------- /backend-api/Makefile: -------------------------------------------------------------------------------- 1 | # Golang ===================================== 2 | 3 | GOBUILD=go build 4 | GOCLEAN=go clean 5 | GOTEST=go test 6 | GOGET=go get 7 | BINARY_NAME=backend-api 8 | BINARY_UNIX=$(BINARY_NAME)_unix 9 | 10 | all: test build ## go test & go build 11 | .PHONY: build 12 | build: ## build go binary 13 | $(GOBUILD) -o $(BINARY_NAME) -v 14 | .PHONY: test 15 | test: ## go test 16 | $(GOTEST) -v ./... 17 | .PHONY: clean 18 | clean: ## remove go bainary 19 | $(GOCLEAN) 20 | rm -f $(BINARY_NAME) 21 | rm -f $(BINARY_UNIX) 22 | .PHONY: run 23 | run: ## run go bainary 24 | $(GOBUILD) -o $(BINARY_NAME) -v ./... 25 | ./$(BINARY_NAME) 26 | .PHONY: doc 27 | doc: ## exec godoc localhost:6060 28 | godoc -http=:6060 -------------------------------------------------------------------------------- /backend-api/db/driver.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "database/sql" 5 | "os" 6 | 7 | _ "github.com/go-sql-driver/mysql" 8 | ) 9 | 10 | // Conn - sql connection handler 11 | var Conn *sql.DB 12 | 13 | // NewSQLHandler - init sql handler 14 | func init() { 15 | user := os.Getenv("MYSQL_USER") 16 | // host := os.Getenv("MYSQL_HOST") 17 | pass := os.Getenv("MYSQL_PASSWORD") 18 | name := os.Getenv("MYSQL_DATABASE") 19 | // port := os.Getenv("MYSQL_PORT") 20 | 21 | dbconf := user + ":" + pass + "@/" + name 22 | conn, err := sql.Open("mysql", dbconf) 23 | if err != nil { 24 | panic(err.Error) 25 | } 26 | Conn = conn 27 | } 28 | -------------------------------------------------------------------------------- /backend-api/db/repository.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "fmt" 5 | "vue-golang-payment-app/backend-api/domain" 6 | ) 7 | 8 | // SelectAllItems - select all 9 | func SelectAllItems() (items domain.Items, err error) { 10 | stmt, err := Conn.Query("SELECT * FROM items") 11 | if err != nil { 12 | fmt.Println(err) 13 | return 14 | } 15 | defer stmt.Close() 16 | for stmt.Next() { 17 | var id int64 18 | var name string 19 | var desctiption string 20 | var amount int64 21 | if err := stmt.Scan(&id, &name, &desctiption, &amount); err != nil { 22 | continue 23 | } 24 | item := domain.Item{ 25 | ID: id, 26 | Name: name, 27 | Description: desctiption, 28 | Amount: amount, 29 | } 30 | items = append(items, item) 31 | } 32 | return 33 | } 34 | 35 | // SelectItem - select post 36 | func SelectItem(identifier int64) (item domain.Item, err error) { 37 | stmt, err := Conn.Prepare(fmt.Sprintf("SELECT * FROM items WHERE id = ? LIMIT 1")) 38 | if err != nil { 39 | fmt.Println(err) 40 | return 41 | } 42 | defer stmt.Close() 43 | var id int64 44 | var name string 45 | var desctiption string 46 | var amount int64 47 | err = stmt.QueryRow(identifier).Scan(&id, &name, &desctiption, &amount) 48 | if err != nil { 49 | return 50 | } 51 | item.ID = id 52 | item.Name = name 53 | item.Description = desctiption 54 | item.Amount = amount 55 | return 56 | } 57 | -------------------------------------------------------------------------------- /backend-api/domain/item.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | // Item - set of item 4 | type Item struct { 5 | ID int64 6 | Name string 7 | Description string 8 | Amount int64 9 | } 10 | 11 | // Items -set of item list 12 | type Items []Item 13 | -------------------------------------------------------------------------------- /backend-api/domain/token.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | //Payment - pay.jp payment parametor 4 | type Payment struct { 5 | Token string 6 | } 7 | -------------------------------------------------------------------------------- /backend-api/handler/contecxt.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | // Context - context interface 4 | type Context interface { 5 | Param(string) string 6 | Bind(interface{}) error 7 | Status(int) 8 | JSON(int, interface{}) 9 | } 10 | -------------------------------------------------------------------------------- /backend-api/handler/item.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "net/http" 5 | "strconv" 6 | "vue-golang-payment-app/backend-api/db" 7 | ) 8 | 9 | // GetLists - get all items 10 | func GetLists(c Context) { 11 | res, err := db.SelectAllItems() 12 | if err != nil { 13 | c.JSON(http.StatusInternalServerError, nil) 14 | return 15 | } 16 | c.JSON(http.StatusOK, res) 17 | } 18 | 19 | // GetItem - get item by id 20 | func GetItem(c Context) { 21 | identifier, err := strconv.Atoi(c.Param("id")) 22 | if err != nil { 23 | c.JSON(http.StatusInternalServerError, nil) 24 | return 25 | } 26 | res, err := db.SelectItem(int64(identifier)) 27 | if err != nil { 28 | c.JSON(http.StatusInternalServerError, nil) 29 | return 30 | } 31 | c.JSON(http.StatusOK, res) 32 | } 33 | -------------------------------------------------------------------------------- /backend-api/handler/payment.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "strconv" 7 | 8 | "vue-golang-payment-app/backend-api/db" 9 | "vue-golang-payment-app/backend-api/domain" 10 | gpay "vue-golang-payment-app/payment-service/proto" 11 | 12 | "google.golang.org/grpc" 13 | ) 14 | 15 | var addr = "localhost:50051" 16 | 17 | // Charge exec payment-service charge 18 | func Charge(c Context) { 19 | //パラメータや body をうけとる 20 | t := domain.Payment{} 21 | c.Bind(&t) 22 | identifier, err := strconv.Atoi(c.Param("id")) 23 | if err != nil { 24 | c.JSON(http.StatusInternalServerError, err) 25 | } 26 | 27 | // id から item情報所得 28 | res, err := db.SelectItem(int64(identifier)) 29 | if err != nil { 30 | c.JSON(http.StatusInternalServerError, err) 31 | } 32 | greq := &gpay.PayRequest{ 33 | Id: int64(identifier), 34 | Token: t.Token, 35 | Amount: res.Amount, 36 | Name: res.Name, 37 | Description: res.Description, 38 | } 39 | 40 | //IPアドレス(ここではlocalhost)とポート番号(ここでは5000)を指定して、サーバーと接続する 41 | conn, err := grpc.Dial(addr, grpc.WithInsecure()) 42 | if err != nil { 43 | c.JSON(http.StatusForbidden, err) 44 | } 45 | defer conn.Close() 46 | client := gpay.NewPayManagerClient(conn) 47 | gres, err := client.Charge(context.Background(), greq) 48 | if err != nil { 49 | c.JSON(http.StatusForbidden, err) 50 | return 51 | } 52 | c.JSON(http.StatusOK, gres) 53 | } 54 | -------------------------------------------------------------------------------- /backend-api/infrastructure/router.go: -------------------------------------------------------------------------------- 1 | package infrastructure 2 | 3 | import ( 4 | "os" 5 | "vue-golang-payment-app/backend-api/handler" 6 | 7 | "github.com/gin-contrib/cors" 8 | gin "github.com/gin-gonic/gin" 9 | ) 10 | 11 | // Router - router api server 12 | var Router *gin.Engine 13 | 14 | func init() { 15 | router := gin.Default() 16 | 17 | router.Use(cors.New(cors.Config{ 18 | AllowOrigins: []string{os.Getenv("CLIENT_CORS_ADDR")}, 19 | AllowMethods: []string{"GET", "POST"}, 20 | AllowHeaders: []string{"Origin", "Content-Type"}, 21 | })) 22 | 23 | // router.POST("/api/v1/items", func(c *gin.Context) {}) 24 | router.GET("/api/v1/items", func(c *gin.Context) { handler.GetLists(c) }) 25 | router.GET("/api/v1/items/:id", func(c *gin.Context) { handler.GetItem(c) }) 26 | router.POST("/api/v1/charge/items/:id", func(c *gin.Context) { handler.Charge(c) }) 27 | // router.DELETE("/api/v1/items/:id", func(c *gin.Context) {}) 28 | 29 | Router = router 30 | } 31 | -------------------------------------------------------------------------------- /backend-api/init/init.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS items; 2 | 3 | CREATE TABLE items ( 4 | id integer AUTO_INCREMENT, 5 | name varchar(255), 6 | desctiption varchar(255), 7 | amount integer, 8 | primary key(id) 9 | ); 10 | 11 | INSERT INTO items (name, desctiption, amount) 12 | VALUES 13 | ('toy', 'test-toy', 2000); 14 | 15 | INSERT INTO items (name, desctiption, amount) 16 | VALUES 17 | ('game', 'test-game', 6000); -------------------------------------------------------------------------------- /backend-api/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "vue-golang-payment-app/backend-api/infrastructure" 6 | ) 7 | 8 | func main() { 9 | infrastructure.Router.Run(os.Getenv("API_SERVER_PORT")) 10 | } 11 | -------------------------------------------------------------------------------- /frontend-spa/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "modules": false, 5 | "targets": { 6 | "browsers": ["> 1%", "last 2 versions", "not ie <= 8"] 7 | } 8 | }], 9 | "stage-2" 10 | ], 11 | "plugins": ["transform-vue-jsx", "transform-runtime"], 12 | "env": { 13 | "test": { 14 | "presets": ["env", "stage-2"], 15 | "plugins": ["transform-vue-jsx", "transform-es2015-modules-commonjs", "dynamic-import-node"] 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /frontend-spa/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /frontend-spa/.eslintignore: -------------------------------------------------------------------------------- 1 | /build/ 2 | /config/ 3 | /dist/ 4 | /*.js 5 | /test/unit/coverage/ 6 | -------------------------------------------------------------------------------- /frontend-spa/.eslintrc.js: -------------------------------------------------------------------------------- 1 | // https://eslint.org/docs/user-guide/configuring 2 | 3 | module.exports = { 4 | root: true, 5 | parserOptions: { 6 | parser: 'babel-eslint' 7 | }, 8 | env: { 9 | browser: true, 10 | }, 11 | extends: [ 12 | // https://github.com/vuejs/eslint-plugin-vue#priority-a-essential-error-prevention 13 | // consider switching to `plugin:vue/strongly-recommended` or `plugin:vue/recommended` for stricter rules. 14 | 'plugin:vue/essential', 15 | // https://github.com/standard/standard/blob/master/docs/RULES-en.md 16 | 'standard' 17 | ], 18 | // required to lint *.vue files 19 | plugins: [ 20 | 'vue' 21 | ], 22 | // add your custom rules here 23 | rules: { 24 | // allow async-await 25 | 'generator-star-spacing': 'off', 26 | // allow debugger during development 27 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off' 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /frontend-spa/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | /dist/ 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | /test/unit/coverage/ 8 | /test/e2e/reports/ 9 | selenium-debug.log 10 | 11 | # Editor directories and files 12 | .idea 13 | .vscode 14 | *.suo 15 | *.ntvs* 16 | *.njsproj 17 | *.sln 18 | -------------------------------------------------------------------------------- /frontend-spa/.postcssrc.js: -------------------------------------------------------------------------------- 1 | // https://github.com/michael-ciniawsky/postcss-load-config 2 | 3 | module.exports = { 4 | "plugins": { 5 | "postcss-import": {}, 6 | "postcss-url": {}, 7 | // to edit target browsers: use "browserslist" field in package.json 8 | "autoprefixer": {} 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /frontend-spa/README.md: -------------------------------------------------------------------------------- 1 | # frontend-spa -------------------------------------------------------------------------------- /frontend-spa/build/build.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | require('./check-versions')() 3 | 4 | process.env.NODE_ENV = 'production' 5 | 6 | const ora = require('ora') 7 | const rm = require('rimraf') 8 | const path = require('path') 9 | const chalk = require('chalk') 10 | const webpack = require('webpack') 11 | const config = require('../config') 12 | const webpackConfig = require('./webpack.prod.conf') 13 | 14 | const spinner = ora('building for production...') 15 | spinner.start() 16 | 17 | rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => { 18 | if (err) throw err 19 | webpack(webpackConfig, (err, stats) => { 20 | spinner.stop() 21 | if (err) throw err 22 | process.stdout.write(stats.toString({ 23 | colors: true, 24 | modules: false, 25 | children: false, // If you are using ts-loader, setting this to true will make TypeScript errors show up during build. 26 | chunks: false, 27 | chunkModules: false 28 | }) + '\n\n') 29 | 30 | if (stats.hasErrors()) { 31 | console.log(chalk.red(' Build failed with errors.\n')) 32 | process.exit(1) 33 | } 34 | 35 | console.log(chalk.cyan(' Build complete.\n')) 36 | console.log(chalk.yellow( 37 | ' Tip: built files are meant to be served over an HTTP server.\n' + 38 | ' Opening index.html over file:// won\'t work.\n' 39 | )) 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /frontend-spa/build/check-versions.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const chalk = require('chalk') 3 | const semver = require('semver') 4 | const packageConfig = require('../package.json') 5 | const shell = require('shelljs') 6 | 7 | function exec (cmd) { 8 | return require('child_process').execSync(cmd).toString().trim() 9 | } 10 | 11 | const versionRequirements = [ 12 | { 13 | name: 'node', 14 | currentVersion: semver.clean(process.version), 15 | versionRequirement: packageConfig.engines.node 16 | } 17 | ] 18 | 19 | if (shell.which('npm')) { 20 | versionRequirements.push({ 21 | name: 'npm', 22 | currentVersion: exec('npm --version'), 23 | versionRequirement: packageConfig.engines.npm 24 | }) 25 | } 26 | 27 | module.exports = function () { 28 | const warnings = [] 29 | 30 | for (let i = 0; i < versionRequirements.length; i++) { 31 | const mod = versionRequirements[i] 32 | 33 | if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) { 34 | warnings.push(mod.name + ': ' + 35 | chalk.red(mod.currentVersion) + ' should be ' + 36 | chalk.green(mod.versionRequirement) 37 | ) 38 | } 39 | } 40 | 41 | if (warnings.length) { 42 | console.log('') 43 | console.log(chalk.yellow('To use this template, you must update following to modules:')) 44 | console.log() 45 | 46 | for (let i = 0; i < warnings.length; i++) { 47 | const warning = warnings[i] 48 | console.log(' ' + warning) 49 | } 50 | 51 | console.log() 52 | process.exit(1) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /frontend-spa/build/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/po3rin/vue-golang-payment-app/b986a4e22e1b35b3467e9188e05910aec5105ee1/frontend-spa/build/logo.png -------------------------------------------------------------------------------- /frontend-spa/build/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | const config = require('../config') 4 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 5 | const packageConfig = require('../package.json') 6 | 7 | exports.assetsPath = function (_path) { 8 | const assetsSubDirectory = process.env.NODE_ENV === 'production' 9 | ? config.build.assetsSubDirectory 10 | : config.dev.assetsSubDirectory 11 | 12 | return path.posix.join(assetsSubDirectory, _path) 13 | } 14 | 15 | exports.cssLoaders = function (options) { 16 | options = options || {} 17 | 18 | const cssLoader = { 19 | loader: 'css-loader', 20 | options: { 21 | sourceMap: options.sourceMap 22 | } 23 | } 24 | 25 | const postcssLoader = { 26 | loader: 'postcss-loader', 27 | options: { 28 | sourceMap: options.sourceMap 29 | } 30 | } 31 | 32 | // generate loader string to be used with extract text plugin 33 | function generateLoaders (loader, loaderOptions) { 34 | const loaders = options.usePostCSS ? [cssLoader, postcssLoader] : [cssLoader] 35 | 36 | if (loader) { 37 | loaders.push({ 38 | loader: loader + '-loader', 39 | options: Object.assign({}, loaderOptions, { 40 | sourceMap: options.sourceMap 41 | }) 42 | }) 43 | } 44 | 45 | // Extract CSS when that option is specified 46 | // (which is the case during production build) 47 | if (options.extract) { 48 | return ExtractTextPlugin.extract({ 49 | use: loaders, 50 | fallback: 'vue-style-loader' 51 | }) 52 | } else { 53 | return ['vue-style-loader'].concat(loaders) 54 | } 55 | } 56 | 57 | // https://vue-loader.vuejs.org/en/configurations/extract-css.html 58 | return { 59 | css: generateLoaders(), 60 | postcss: generateLoaders(), 61 | less: generateLoaders('less'), 62 | sass: generateLoaders('sass', { indentedSyntax: true }), 63 | scss: generateLoaders('sass'), 64 | stylus: generateLoaders('stylus'), 65 | styl: generateLoaders('stylus') 66 | } 67 | } 68 | 69 | // Generate loaders for standalone style files (outside of .vue) 70 | exports.styleLoaders = function (options) { 71 | const output = [] 72 | const loaders = exports.cssLoaders(options) 73 | 74 | for (const extension in loaders) { 75 | const loader = loaders[extension] 76 | output.push({ 77 | test: new RegExp('\\.' + extension + '$'), 78 | use: loader 79 | }) 80 | } 81 | 82 | return output 83 | } 84 | 85 | exports.createNotifierCallback = () => { 86 | const notifier = require('node-notifier') 87 | 88 | return (severity, errors) => { 89 | if (severity !== 'error') return 90 | 91 | const error = errors[0] 92 | const filename = error.file && error.file.split('!').pop() 93 | 94 | notifier.notify({ 95 | title: packageConfig.name, 96 | message: severity + ': ' + error.name, 97 | subtitle: filename || '', 98 | icon: path.join(__dirname, 'logo.png') 99 | }) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /frontend-spa/build/vue-loader.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const utils = require('./utils') 3 | const config = require('../config') 4 | const isProduction = process.env.NODE_ENV === 'production' 5 | const sourceMapEnabled = isProduction 6 | ? config.build.productionSourceMap 7 | : config.dev.cssSourceMap 8 | 9 | module.exports = { 10 | loaders: utils.cssLoaders({ 11 | sourceMap: sourceMapEnabled, 12 | extract: isProduction 13 | }), 14 | cssSourceMap: sourceMapEnabled, 15 | cacheBusting: config.dev.cacheBusting, 16 | transformToRequire: { 17 | video: ['src', 'poster'], 18 | source: 'src', 19 | img: 'src', 20 | image: 'xlink:href' 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /frontend-spa/build/webpack.base.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | const utils = require('./utils') 4 | const config = require('../config') 5 | const vueLoaderConfig = require('./vue-loader.conf') 6 | 7 | function resolve (dir) { 8 | return path.join(__dirname, '..', dir) 9 | } 10 | 11 | const createLintingRule = () => ({ 12 | test: /\.(js|vue)$/, 13 | loader: 'eslint-loader', 14 | enforce: 'pre', 15 | include: [resolve('src'), resolve('test')], 16 | options: { 17 | formatter: require('eslint-friendly-formatter'), 18 | emitWarning: !config.dev.showEslintErrorsInOverlay 19 | } 20 | }) 21 | 22 | module.exports = { 23 | context: path.resolve(__dirname, '../'), 24 | entry: { 25 | app: './src/main.js' 26 | }, 27 | output: { 28 | path: config.build.assetsRoot, 29 | filename: '[name].js', 30 | publicPath: process.env.NODE_ENV === 'production' 31 | ? config.build.assetsPublicPath 32 | : config.dev.assetsPublicPath 33 | }, 34 | resolve: { 35 | extensions: ['.js', '.vue', '.json'], 36 | alias: { 37 | 'vue$': 'vue/dist/vue.esm.js', 38 | '@': resolve('src'), 39 | } 40 | }, 41 | module: { 42 | rules: [ 43 | ...(config.dev.useEslint ? [createLintingRule()] : []), 44 | { 45 | test: /\.vue$/, 46 | loader: 'vue-loader', 47 | options: vueLoaderConfig 48 | }, 49 | { 50 | test: /\.js$/, 51 | loader: 'babel-loader', 52 | include: [resolve('src'), resolve('test'), resolve('node_modules/webpack-dev-server/client')] 53 | }, 54 | { 55 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 56 | loader: 'url-loader', 57 | options: { 58 | limit: 10000, 59 | name: utils.assetsPath('img/[name].[hash:7].[ext]') 60 | } 61 | }, 62 | { 63 | test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/, 64 | loader: 'url-loader', 65 | options: { 66 | limit: 10000, 67 | name: utils.assetsPath('media/[name].[hash:7].[ext]') 68 | } 69 | }, 70 | { 71 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, 72 | loader: 'url-loader', 73 | options: { 74 | limit: 10000, 75 | name: utils.assetsPath('fonts/[name].[hash:7].[ext]') 76 | } 77 | } 78 | ] 79 | }, 80 | node: { 81 | // prevent webpack from injecting useless setImmediate polyfill because Vue 82 | // source contains it (although only uses it if it's native). 83 | setImmediate: false, 84 | // prevent webpack from injecting mocks to Node native modules 85 | // that does not make sense for the client 86 | dgram: 'empty', 87 | fs: 'empty', 88 | net: 'empty', 89 | tls: 'empty', 90 | child_process: 'empty' 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /frontend-spa/build/webpack.dev.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const utils = require('./utils') 3 | const webpack = require('webpack') 4 | const config = require('../config') 5 | const merge = require('webpack-merge') 6 | const path = require('path') 7 | const baseWebpackConfig = require('./webpack.base.conf') 8 | const CopyWebpackPlugin = require('copy-webpack-plugin') 9 | const HtmlWebpackPlugin = require('html-webpack-plugin') 10 | const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin') 11 | const portfinder = require('portfinder') 12 | 13 | const HOST = process.env.HOST 14 | const PORT = process.env.PORT && Number(process.env.PORT) 15 | 16 | const devWebpackConfig = merge(baseWebpackConfig, { 17 | module: { 18 | rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, usePostCSS: true }) 19 | }, 20 | // cheap-module-eval-source-map is faster for development 21 | devtool: config.dev.devtool, 22 | 23 | // these devServer options should be customized in /config/index.js 24 | devServer: { 25 | clientLogLevel: 'warning', 26 | historyApiFallback: { 27 | rewrites: [ 28 | { from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'index.html') }, 29 | ], 30 | }, 31 | hot: true, 32 | contentBase: false, // since we use CopyWebpackPlugin. 33 | compress: true, 34 | host: HOST || config.dev.host, 35 | port: PORT || config.dev.port, 36 | open: config.dev.autoOpenBrowser, 37 | overlay: config.dev.errorOverlay 38 | ? { warnings: false, errors: true } 39 | : false, 40 | publicPath: config.dev.assetsPublicPath, 41 | proxy: config.dev.proxyTable, 42 | quiet: true, // necessary for FriendlyErrorsPlugin 43 | watchOptions: { 44 | poll: config.dev.poll, 45 | } 46 | }, 47 | plugins: [ 48 | new webpack.DefinePlugin({ 49 | 'process.env': require('../config/dev.env') 50 | }), 51 | new webpack.HotModuleReplacementPlugin(), 52 | new webpack.NamedModulesPlugin(), // HMR shows correct file names in console on update. 53 | new webpack.NoEmitOnErrorsPlugin(), 54 | // https://github.com/ampedandwired/html-webpack-plugin 55 | new HtmlWebpackPlugin({ 56 | filename: 'index.html', 57 | template: 'index.html', 58 | inject: true 59 | }), 60 | // copy custom static assets 61 | new CopyWebpackPlugin([ 62 | { 63 | from: path.resolve(__dirname, '../static'), 64 | to: config.dev.assetsSubDirectory, 65 | ignore: ['.*'] 66 | } 67 | ]) 68 | ] 69 | }) 70 | 71 | module.exports = new Promise((resolve, reject) => { 72 | portfinder.basePort = process.env.PORT || config.dev.port 73 | portfinder.getPort((err, port) => { 74 | if (err) { 75 | reject(err) 76 | } else { 77 | // publish the new Port, necessary for e2e tests 78 | process.env.PORT = port 79 | // add port to devServer config 80 | devWebpackConfig.devServer.port = port 81 | 82 | // Add FriendlyErrorsPlugin 83 | devWebpackConfig.plugins.push(new FriendlyErrorsPlugin({ 84 | compilationSuccessInfo: { 85 | messages: [`Your application is running here: http://${devWebpackConfig.devServer.host}:${port}`], 86 | }, 87 | onErrors: config.dev.notifyOnErrors 88 | ? utils.createNotifierCallback() 89 | : undefined 90 | })) 91 | 92 | resolve(devWebpackConfig) 93 | } 94 | }) 95 | }) 96 | -------------------------------------------------------------------------------- /frontend-spa/build/webpack.prod.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | const utils = require('./utils') 4 | const webpack = require('webpack') 5 | const config = require('../config') 6 | const merge = require('webpack-merge') 7 | const baseWebpackConfig = require('./webpack.base.conf') 8 | const CopyWebpackPlugin = require('copy-webpack-plugin') 9 | const HtmlWebpackPlugin = require('html-webpack-plugin') 10 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 11 | const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin') 12 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin') 13 | 14 | const env = process.env.NODE_ENV === 'testing' 15 | ? require('../config/test.env') 16 | : require('../config/prod.env') 17 | 18 | const webpackConfig = merge(baseWebpackConfig, { 19 | module: { 20 | rules: utils.styleLoaders({ 21 | sourceMap: config.build.productionSourceMap, 22 | extract: true, 23 | usePostCSS: true 24 | }) 25 | }, 26 | devtool: config.build.productionSourceMap ? config.build.devtool : false, 27 | output: { 28 | path: config.build.assetsRoot, 29 | filename: utils.assetsPath('js/[name].[chunkhash].js'), 30 | chunkFilename: utils.assetsPath('js/[id].[chunkhash].js') 31 | }, 32 | plugins: [ 33 | // http://vuejs.github.io/vue-loader/en/workflow/production.html 34 | new webpack.DefinePlugin({ 35 | 'process.env': env 36 | }), 37 | new UglifyJsPlugin({ 38 | uglifyOptions: { 39 | compress: { 40 | warnings: false 41 | } 42 | }, 43 | sourceMap: config.build.productionSourceMap, 44 | parallel: true 45 | }), 46 | // extract css into its own file 47 | new ExtractTextPlugin({ 48 | filename: utils.assetsPath('css/[name].[contenthash].css'), 49 | // Setting the following option to `false` will not extract CSS from codesplit chunks. 50 | // Their CSS will instead be inserted dynamically with style-loader when the codesplit chunk has been loaded by webpack. 51 | // It's currently set to `true` because we are seeing that sourcemaps are included in the codesplit bundle as well when it's `false`, 52 | // increasing file size: https://github.com/vuejs-templates/webpack/issues/1110 53 | allChunks: true, 54 | }), 55 | // Compress extracted CSS. We are using this plugin so that possible 56 | // duplicated CSS from different components can be deduped. 57 | new OptimizeCSSPlugin({ 58 | cssProcessorOptions: config.build.productionSourceMap 59 | ? { safe: true, map: { inline: false } } 60 | : { safe: true } 61 | }), 62 | // generate dist index.html with correct asset hash for caching. 63 | // you can customize output by editing /index.html 64 | // see https://github.com/ampedandwired/html-webpack-plugin 65 | new HtmlWebpackPlugin({ 66 | filename: process.env.NODE_ENV === 'testing' 67 | ? 'index.html' 68 | : config.build.index, 69 | template: 'index.html', 70 | inject: true, 71 | minify: { 72 | removeComments: true, 73 | collapseWhitespace: true, 74 | removeAttributeQuotes: true 75 | // more options: 76 | // https://github.com/kangax/html-minifier#options-quick-reference 77 | }, 78 | // necessary to consistently work with multiple chunks via CommonsChunkPlugin 79 | chunksSortMode: 'dependency' 80 | }), 81 | // keep module.id stable when vendor modules does not change 82 | new webpack.HashedModuleIdsPlugin(), 83 | // enable scope hoisting 84 | new webpack.optimize.ModuleConcatenationPlugin(), 85 | // split vendor js into its own file 86 | new webpack.optimize.CommonsChunkPlugin({ 87 | name: 'vendor', 88 | minChunks (module) { 89 | // any required modules inside node_modules are extracted to vendor 90 | return ( 91 | module.resource && 92 | /\.js$/.test(module.resource) && 93 | module.resource.indexOf( 94 | path.join(__dirname, '../node_modules') 95 | ) === 0 96 | ) 97 | } 98 | }), 99 | // extract webpack runtime and module manifest to its own file in order to 100 | // prevent vendor hash from being updated whenever app bundle is updated 101 | new webpack.optimize.CommonsChunkPlugin({ 102 | name: 'manifest', 103 | minChunks: Infinity 104 | }), 105 | // This instance extracts shared chunks from code splitted chunks and bundles them 106 | // in a separate chunk, similar to the vendor chunk 107 | // see: https://webpack.js.org/plugins/commons-chunk-plugin/#extra-async-commons-chunk 108 | new webpack.optimize.CommonsChunkPlugin({ 109 | name: 'app', 110 | async: 'vendor-async', 111 | children: true, 112 | minChunks: 3 113 | }), 114 | 115 | // copy custom static assets 116 | new CopyWebpackPlugin([ 117 | { 118 | from: path.resolve(__dirname, '../static'), 119 | to: config.build.assetsSubDirectory, 120 | ignore: ['.*'] 121 | } 122 | ]) 123 | ] 124 | }) 125 | 126 | if (config.build.productionGzip) { 127 | const CompressionWebpackPlugin = require('compression-webpack-plugin') 128 | 129 | webpackConfig.plugins.push( 130 | new CompressionWebpackPlugin({ 131 | asset: '[path].gz[query]', 132 | algorithm: 'gzip', 133 | test: new RegExp( 134 | '\\.(' + 135 | config.build.productionGzipExtensions.join('|') + 136 | ')$' 137 | ), 138 | threshold: 10240, 139 | minRatio: 0.8 140 | }) 141 | ) 142 | } 143 | 144 | if (config.build.bundleAnalyzerReport) { 145 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin 146 | webpackConfig.plugins.push(new BundleAnalyzerPlugin()) 147 | } 148 | 149 | module.exports = webpackConfig 150 | -------------------------------------------------------------------------------- /frontend-spa/config/dev.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const merge = require('webpack-merge') 3 | const prodEnv = require('./prod.env') 4 | 5 | module.exports = merge(prodEnv, { 6 | NODE_ENV: '"development"' 7 | }) 8 | -------------------------------------------------------------------------------- /frontend-spa/config/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | // Template version: 1.3.1 3 | // see http://vuejs-templates.github.io/webpack for documentation. 4 | 5 | const path = require('path') 6 | 7 | module.exports = { 8 | dev: { 9 | 10 | // Paths 11 | assetsSubDirectory: 'static', 12 | assetsPublicPath: '/', 13 | proxyTable: {}, 14 | 15 | // Various Dev Server settings 16 | host: 'localhost', // can be overwritten by process.env.HOST 17 | port: 8080, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined 18 | autoOpenBrowser: false, 19 | errorOverlay: true, 20 | notifyOnErrors: true, 21 | poll: false, // https://webpack.js.org/configuration/dev-server/#devserver-watchoptions- 22 | 23 | // Use Eslint Loader? 24 | // If true, your code will be linted during bundling and 25 | // linting errors and warnings will be shown in the console. 26 | useEslint: true, 27 | // If true, eslint errors and warnings will also be shown in the error overlay 28 | // in the browser. 29 | showEslintErrorsInOverlay: false, 30 | 31 | /** 32 | * Source Maps 33 | */ 34 | 35 | // https://webpack.js.org/configuration/devtool/#development 36 | devtool: 'cheap-module-eval-source-map', 37 | 38 | // If you have problems debugging vue-files in devtools, 39 | // set this to false - it *may* help 40 | // https://vue-loader.vuejs.org/en/options.html#cachebusting 41 | cacheBusting: true, 42 | 43 | cssSourceMap: true 44 | }, 45 | 46 | build: { 47 | // Template for index.html 48 | index: path.resolve(__dirname, '../dist/index.html'), 49 | 50 | // Paths 51 | assetsRoot: path.resolve(__dirname, '../dist'), 52 | assetsSubDirectory: 'static', 53 | assetsPublicPath: '/', 54 | 55 | /** 56 | * Source Maps 57 | */ 58 | 59 | productionSourceMap: true, 60 | // https://webpack.js.org/configuration/devtool/#production 61 | devtool: '#source-map', 62 | 63 | // Gzip off by default as many popular static hosts such as 64 | // Surge or Netlify already gzip all static assets for you. 65 | // Before setting to `true`, make sure to: 66 | // npm install --save-dev compression-webpack-plugin 67 | productionGzip: false, 68 | productionGzipExtensions: ['js', 'css'], 69 | 70 | // Run the build command with an extra argument to 71 | // View the bundle analyzer report after build finishes: 72 | // `npm run build --report` 73 | // Set to `true` or `false` to always turn it on or off 74 | bundleAnalyzerReport: process.env.npm_config_report 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /frontend-spa/config/prod.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | module.exports = { 3 | NODE_ENV: '"production"' 4 | } 5 | -------------------------------------------------------------------------------- /frontend-spa/config/test.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const merge = require('webpack-merge') 3 | const devEnv = require('./dev.env') 4 | 5 | module.exports = merge(devEnv, { 6 | NODE_ENV: '"testing"' 7 | }) 8 | -------------------------------------------------------------------------------- /frontend-spa/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | frontend-spa 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /frontend-spa/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend-spa", 3 | "version": "1.0.0", 4 | "description": "A Vue.js project", 5 | "author": "po3rin", 6 | "private": true, 7 | "scripts": { 8 | "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js", 9 | "start": "npm run dev", 10 | "unit": "jest --config test/unit/jest.conf.js --coverage", 11 | "e2e": "node test/e2e/runner.js", 12 | "test": "npm run unit && npm run e2e", 13 | "lint": "eslint --ext .js,.vue src test/unit test/e2e/specs", 14 | "build": "node build/build.js" 15 | }, 16 | "dependencies": { 17 | "axios": "^0.18.0", 18 | "vue": "^2.5.2", 19 | "vue-payjp-checkout": "0.0.1-alpha.4", 20 | "vue-router": "^3.0.1" 21 | }, 22 | "devDependencies": { 23 | "autoprefixer": "^7.1.2", 24 | "babel-core": "^6.22.1", 25 | "babel-eslint": "^8.2.1", 26 | "babel-helper-vue-jsx-merge-props": "^2.0.3", 27 | "babel-jest": "^21.0.2", 28 | "babel-loader": "^7.1.1", 29 | "babel-plugin-dynamic-import-node": "^1.2.0", 30 | "babel-plugin-syntax-jsx": "^6.18.0", 31 | "babel-plugin-transform-es2015-modules-commonjs": "^6.26.0", 32 | "babel-plugin-transform-runtime": "^6.22.0", 33 | "babel-plugin-transform-vue-jsx": "^3.5.0", 34 | "babel-preset-env": "^1.3.2", 35 | "babel-preset-stage-2": "^6.22.0", 36 | "babel-register": "^6.22.0", 37 | "chalk": "^2.0.1", 38 | "chromedriver": "^2.27.2", 39 | "copy-webpack-plugin": "^4.0.1", 40 | "cross-spawn": "^5.0.1", 41 | "css-loader": "^0.28.0", 42 | "eslint": "^4.15.0", 43 | "eslint-config-standard": "^10.2.1", 44 | "eslint-friendly-formatter": "^3.0.0", 45 | "eslint-loader": "^1.7.1", 46 | "eslint-plugin-import": "^2.7.0", 47 | "eslint-plugin-node": "^5.2.0", 48 | "eslint-plugin-promise": "^3.4.0", 49 | "eslint-plugin-standard": "^3.0.1", 50 | "eslint-plugin-vue": "^4.0.0", 51 | "extract-text-webpack-plugin": "^3.0.0", 52 | "file-loader": "^1.1.4", 53 | "friendly-errors-webpack-plugin": "^1.6.1", 54 | "html-webpack-plugin": "^2.30.1", 55 | "jest": "^22.0.4", 56 | "jest-serializer-vue": "^0.3.0", 57 | "nightwatch": "^0.9.12", 58 | "node-notifier": "^5.1.2", 59 | "optimize-css-assets-webpack-plugin": "^3.2.0", 60 | "ora": "^1.2.0", 61 | "portfinder": "^1.0.13", 62 | "postcss-import": "^11.0.0", 63 | "postcss-loader": "^2.0.8", 64 | "postcss-url": "^7.2.1", 65 | "rimraf": "^2.6.0", 66 | "selenium-server": "^3.0.1", 67 | "semver": "^5.3.0", 68 | "shelljs": "^0.7.6", 69 | "uglifyjs-webpack-plugin": "^1.1.1", 70 | "url-loader": "^0.5.8", 71 | "vue-jest": "^1.0.2", 72 | "vue-loader": "^13.3.0", 73 | "vue-style-loader": "^3.0.1", 74 | "vue-template-compiler": "^2.5.2", 75 | "webpack": "^3.6.0", 76 | "webpack-bundle-analyzer": "^2.9.0", 77 | "webpack-dev-server": "^2.9.1", 78 | "webpack-merge": "^4.1.0" 79 | }, 80 | "engines": { 81 | "node": ">= 6.0.0", 82 | "npm": ">= 3.0.0" 83 | }, 84 | "browserslist": [ 85 | "> 1%", 86 | "last 2 versions", 87 | "not ie <= 8" 88 | ] 89 | } 90 | -------------------------------------------------------------------------------- /frontend-spa/src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | 13 | 23 | -------------------------------------------------------------------------------- /frontend-spa/src/components/Home.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 36 | 37 | 38 | 54 | -------------------------------------------------------------------------------- /frontend-spa/src/components/Item.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 53 | 54 | 55 | 71 | -------------------------------------------------------------------------------- /frontend-spa/src/components/ItemCard.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 17 | 18 | 19 | 38 | -------------------------------------------------------------------------------- /frontend-spa/src/main.js: -------------------------------------------------------------------------------- 1 | // The Vue build version to load with the `import` command 2 | // (runtime-only or standalone) has been set in webpack.base.conf with an alias. 3 | import Vue from 'vue' 4 | import App from './App' 5 | import router from './router' 6 | import PayjpCheckout from 'vue-payjp-checkout' 7 | 8 | Vue.config.productionTip = false 9 | 10 | Vue.use(PayjpCheckout) 11 | /* eslint-disable no-new */ 12 | new Vue({ 13 | el: '#app', 14 | router, 15 | components: { App }, 16 | template: '' 17 | }) 18 | -------------------------------------------------------------------------------- /frontend-spa/src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | import Home from '@/components/Home' 4 | import Item from '@/components/Item' 5 | 6 | Vue.use(Router) 7 | 8 | export default new Router({ 9 | routes: [ 10 | { 11 | path: '/', 12 | name: 'Home', 13 | component: Home 14 | }, 15 | { 16 | path: '/items/:id', 17 | name: 'Item', 18 | component: Item 19 | } 20 | ] 21 | }) 22 | -------------------------------------------------------------------------------- /frontend-spa/static/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/po3rin/vue-golang-payment-app/b986a4e22e1b35b3467e9188e05910aec5105ee1/frontend-spa/static/.gitkeep -------------------------------------------------------------------------------- /frontend-spa/test/e2e/custom-assertions/elementCount.js: -------------------------------------------------------------------------------- 1 | // A custom Nightwatch assertion. 2 | // The assertion name is the filename. 3 | // Example usage: 4 | // 5 | // browser.assert.elementCount(selector, count) 6 | // 7 | // For more information on custom assertions see: 8 | // http://nightwatchjs.org/guide#writing-custom-assertions 9 | 10 | exports.assertion = function (selector, count) { 11 | this.message = 'Testing if element <' + selector + '> has count: ' + count 12 | this.expected = count 13 | this.pass = function (val) { 14 | return val === this.expected 15 | } 16 | this.value = function (res) { 17 | return res.value 18 | } 19 | this.command = function (cb) { 20 | var self = this 21 | return this.api.execute(function (selector) { 22 | return document.querySelectorAll(selector).length 23 | }, [selector], function (res) { 24 | cb.call(self, res) 25 | }) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /frontend-spa/test/e2e/nightwatch.conf.js: -------------------------------------------------------------------------------- 1 | require('babel-register') 2 | var config = require('../../config') 3 | 4 | // http://nightwatchjs.org/gettingstarted#settings-file 5 | module.exports = { 6 | src_folders: ['test/e2e/specs'], 7 | output_folder: 'test/e2e/reports', 8 | custom_assertions_path: ['test/e2e/custom-assertions'], 9 | 10 | selenium: { 11 | start_process: true, 12 | server_path: require('selenium-server').path, 13 | host: '127.0.0.1', 14 | port: 4444, 15 | cli_args: { 16 | 'webdriver.chrome.driver': require('chromedriver').path 17 | } 18 | }, 19 | 20 | test_settings: { 21 | default: { 22 | selenium_port: 4444, 23 | selenium_host: 'localhost', 24 | silent: true, 25 | globals: { 26 | devServerURL: 'http://localhost:' + (process.env.PORT || config.dev.port) 27 | } 28 | }, 29 | 30 | chrome: { 31 | desiredCapabilities: { 32 | browserName: 'chrome', 33 | javascriptEnabled: true, 34 | acceptSslCerts: true 35 | } 36 | }, 37 | 38 | firefox: { 39 | desiredCapabilities: { 40 | browserName: 'firefox', 41 | javascriptEnabled: true, 42 | acceptSslCerts: true 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /frontend-spa/test/e2e/runner.js: -------------------------------------------------------------------------------- 1 | // 1. start the dev server using production config 2 | process.env.NODE_ENV = 'testing' 3 | 4 | const webpack = require('webpack') 5 | const DevServer = require('webpack-dev-server') 6 | 7 | const webpackConfig = require('../../build/webpack.prod.conf') 8 | const devConfigPromise = require('../../build/webpack.dev.conf') 9 | 10 | let server 11 | 12 | devConfigPromise.then(devConfig => { 13 | const devServerOptions = devConfig.devServer 14 | const compiler = webpack(webpackConfig) 15 | server = new DevServer(compiler, devServerOptions) 16 | const port = devServerOptions.port 17 | const host = devServerOptions.host 18 | return server.listen(port, host) 19 | }) 20 | .then(() => { 21 | // 2. run the nightwatch test suite against it 22 | // to run in additional browsers: 23 | // 1. add an entry in test/e2e/nightwatch.conf.js under "test_settings" 24 | // 2. add it to the --env flag below 25 | // or override the environment flag, for example: `npm run e2e -- --env chrome,firefox` 26 | // For more information on Nightwatch's config file, see 27 | // http://nightwatchjs.org/guide#settings-file 28 | let opts = process.argv.slice(2) 29 | if (opts.indexOf('--config') === -1) { 30 | opts = opts.concat(['--config', 'test/e2e/nightwatch.conf.js']) 31 | } 32 | if (opts.indexOf('--env') === -1) { 33 | opts = opts.concat(['--env', 'chrome']) 34 | } 35 | 36 | const spawn = require('cross-spawn') 37 | const runner = spawn('./node_modules/.bin/nightwatch', opts, { stdio: 'inherit' }) 38 | 39 | runner.on('exit', function (code) { 40 | server.close() 41 | process.exit(code) 42 | }) 43 | 44 | runner.on('error', function (err) { 45 | server.close() 46 | throw err 47 | }) 48 | }) 49 | -------------------------------------------------------------------------------- /frontend-spa/test/e2e/specs/test.js: -------------------------------------------------------------------------------- 1 | // For authoring Nightwatch tests, see 2 | // http://nightwatchjs.org/guide#usage 3 | 4 | module.exports = { 5 | 'default e2e tests': function (browser) { 6 | // automatically uses dev Server port from /config.index.js 7 | // default: http://localhost:8080 8 | // see nightwatch.conf.js 9 | const devServer = browser.globals.devServerURL 10 | 11 | browser 12 | .url(devServer) 13 | .waitForElementVisible('#app', 5000) 14 | .assert.elementPresent('.hello') 15 | .assert.containsText('h1', 'Welcome to Your Vue.js App') 16 | .assert.elementCount('img', 1) 17 | .end() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /frontend-spa/test/unit/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "jest": true 4 | }, 5 | "globals": { 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /frontend-spa/test/unit/jest.conf.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | rootDir: path.resolve(__dirname, '../../'), 5 | moduleFileExtensions: [ 6 | 'js', 7 | 'json', 8 | 'vue' 9 | ], 10 | moduleNameMapper: { 11 | '^@/(.*)$': '/src/$1' 12 | }, 13 | transform: { 14 | '^.+\\.js$': '/node_modules/babel-jest', 15 | '.*\\.(vue)$': '/node_modules/vue-jest' 16 | }, 17 | testPathIgnorePatterns: [ 18 | '/test/e2e' 19 | ], 20 | snapshotSerializers: ['/node_modules/jest-serializer-vue'], 21 | setupFiles: ['/test/unit/setup'], 22 | mapCoverage: true, 23 | coverageDirectory: '/test/unit/coverage', 24 | collectCoverageFrom: [ 25 | 'src/**/*.{js,vue}', 26 | '!src/main.js', 27 | '!src/router/index.js', 28 | '!**/node_modules/**' 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /frontend-spa/test/unit/setup.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | Vue.config.productionTip = false 4 | -------------------------------------------------------------------------------- /frontend-spa/test/unit/specs/HelloWorld.spec.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import HelloWorld from '@/components/HelloWorld' 3 | 4 | describe('HelloWorld.vue', () => { 5 | it('should render correct contents', () => { 6 | const Constructor = Vue.extend(HelloWorld) 7 | const vm = new Constructor().$mount() 8 | expect(vm.$el.querySelector('.hello h1').textContent) 9 | .toEqual('Welcome to Your Vue.js App') 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /payment-service/Makefile: -------------------------------------------------------------------------------- 1 | all: ## generate pd, gateway & swagger json 2 | protoc --go_out=plugins=grpc:. proto/pay.proto 3 | 4 | .PHONY: server 5 | server: ## run API gateway 6 | go run server/main.go 7 | 8 | .PHONY: client 9 | client: ## run golang server 10 | go run ./server/main.go 11 | 12 | help: ## Display help 13 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' 14 | 15 | -------------------------------------------------------------------------------- /payment-service/client/client.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "log" 8 | gpay "vue-golang-payment-app/payment-service/proto" 9 | 10 | "google.golang.org/grpc" 11 | ) 12 | 13 | var addr = "localhost:50051" 14 | 15 | func main() { 16 | //IPアドレス(ここではlocalhost)とポート番号(ここでは5000)を指定して、サーバーと接続する 17 | conn, err := grpc.Dial(addr, grpc.WithInsecure()) 18 | 19 | if err != nil { 20 | fmt.Println(err) 21 | } 22 | 23 | //接続は最後に必ず閉じる 24 | defer conn.Close() 25 | 26 | c := gpay.NewPayManagerClient(conn) 27 | 28 | //サーバーに対してリクエストを送信する 29 | req := &gpay.PayRequest{ 30 | Id: 1, 31 | Token: "token", 32 | Amount: 3000, 33 | Name: "toy", 34 | Description: "this is test toy", 35 | } 36 | resp, err := c.Charge(context.Background(), req) 37 | if err != nil { 38 | log.Fatalf("RPC error: %v", err) 39 | } 40 | log.Println(resp.Captured) 41 | } 42 | -------------------------------------------------------------------------------- /payment-service/proto/pay.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // source: proto/pay.proto 3 | 4 | package paymentservice 5 | 6 | import proto "github.com/golang/protobuf/proto" 7 | import fmt "fmt" 8 | import math "math" 9 | 10 | import ( 11 | context "golang.org/x/net/context" 12 | grpc "google.golang.org/grpc" 13 | ) 14 | 15 | // Reference imports to suppress errors if they are not otherwise used. 16 | var _ = proto.Marshal 17 | var _ = fmt.Errorf 18 | var _ = math.Inf 19 | 20 | // This is a compile-time assertion to ensure that this generated file 21 | // is compatible with the proto package it is being compiled against. 22 | // A compilation error at this line likely means your copy of the 23 | // proto package needs to be updated. 24 | const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package 25 | 26 | type PayRequest struct { 27 | Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` 28 | Token string `protobuf:"bytes,2,opt,name=token,proto3" json:"token,omitempty"` 29 | Amount int64 `protobuf:"varint,3,opt,name=amount,proto3" json:"amount,omitempty"` 30 | Name string `protobuf:"bytes,4,opt,name=name,proto3" json:"name,omitempty"` 31 | Description string `protobuf:"bytes,5,opt,name=description,proto3" json:"description,omitempty"` 32 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 33 | XXX_unrecognized []byte `json:"-"` 34 | XXX_sizecache int32 `json:"-"` 35 | } 36 | 37 | func (m *PayRequest) Reset() { *m = PayRequest{} } 38 | func (m *PayRequest) String() string { return proto.CompactTextString(m) } 39 | func (*PayRequest) ProtoMessage() {} 40 | func (*PayRequest) Descriptor() ([]byte, []int) { 41 | return fileDescriptor_pay_a9772073fe55a113, []int{0} 42 | } 43 | func (m *PayRequest) XXX_Unmarshal(b []byte) error { 44 | return xxx_messageInfo_PayRequest.Unmarshal(m, b) 45 | } 46 | func (m *PayRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 47 | return xxx_messageInfo_PayRequest.Marshal(b, m, deterministic) 48 | } 49 | func (dst *PayRequest) XXX_Merge(src proto.Message) { 50 | xxx_messageInfo_PayRequest.Merge(dst, src) 51 | } 52 | func (m *PayRequest) XXX_Size() int { 53 | return xxx_messageInfo_PayRequest.Size(m) 54 | } 55 | func (m *PayRequest) XXX_DiscardUnknown() { 56 | xxx_messageInfo_PayRequest.DiscardUnknown(m) 57 | } 58 | 59 | var xxx_messageInfo_PayRequest proto.InternalMessageInfo 60 | 61 | func (m *PayRequest) GetId() int64 { 62 | if m != nil { 63 | return m.Id 64 | } 65 | return 0 66 | } 67 | 68 | func (m *PayRequest) GetToken() string { 69 | if m != nil { 70 | return m.Token 71 | } 72 | return "" 73 | } 74 | 75 | func (m *PayRequest) GetAmount() int64 { 76 | if m != nil { 77 | return m.Amount 78 | } 79 | return 0 80 | } 81 | 82 | func (m *PayRequest) GetName() string { 83 | if m != nil { 84 | return m.Name 85 | } 86 | return "" 87 | } 88 | 89 | func (m *PayRequest) GetDescription() string { 90 | if m != nil { 91 | return m.Description 92 | } 93 | return "" 94 | } 95 | 96 | type PayResponse struct { 97 | Paid bool `protobuf:"varint,1,opt,name=paid,proto3" json:"paid,omitempty"` 98 | Captured bool `protobuf:"varint,3,opt,name=captured,proto3" json:"captured,omitempty"` 99 | Amount int64 `protobuf:"varint,2,opt,name=amount,proto3" json:"amount,omitempty"` 100 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 101 | XXX_unrecognized []byte `json:"-"` 102 | XXX_sizecache int32 `json:"-"` 103 | } 104 | 105 | func (m *PayResponse) Reset() { *m = PayResponse{} } 106 | func (m *PayResponse) String() string { return proto.CompactTextString(m) } 107 | func (*PayResponse) ProtoMessage() {} 108 | func (*PayResponse) Descriptor() ([]byte, []int) { 109 | return fileDescriptor_pay_a9772073fe55a113, []int{1} 110 | } 111 | func (m *PayResponse) XXX_Unmarshal(b []byte) error { 112 | return xxx_messageInfo_PayResponse.Unmarshal(m, b) 113 | } 114 | func (m *PayResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 115 | return xxx_messageInfo_PayResponse.Marshal(b, m, deterministic) 116 | } 117 | func (dst *PayResponse) XXX_Merge(src proto.Message) { 118 | xxx_messageInfo_PayResponse.Merge(dst, src) 119 | } 120 | func (m *PayResponse) XXX_Size() int { 121 | return xxx_messageInfo_PayResponse.Size(m) 122 | } 123 | func (m *PayResponse) XXX_DiscardUnknown() { 124 | xxx_messageInfo_PayResponse.DiscardUnknown(m) 125 | } 126 | 127 | var xxx_messageInfo_PayResponse proto.InternalMessageInfo 128 | 129 | func (m *PayResponse) GetPaid() bool { 130 | if m != nil { 131 | return m.Paid 132 | } 133 | return false 134 | } 135 | 136 | func (m *PayResponse) GetCaptured() bool { 137 | if m != nil { 138 | return m.Captured 139 | } 140 | return false 141 | } 142 | 143 | func (m *PayResponse) GetAmount() int64 { 144 | if m != nil { 145 | return m.Amount 146 | } 147 | return 0 148 | } 149 | 150 | func init() { 151 | proto.RegisterType((*PayRequest)(nil), "paymentservice.PayRequest") 152 | proto.RegisterType((*PayResponse)(nil), "paymentservice.PayResponse") 153 | } 154 | 155 | // Reference imports to suppress errors if they are not otherwise used. 156 | var _ context.Context 157 | var _ grpc.ClientConn 158 | 159 | // This is a compile-time assertion to ensure that this generated file 160 | // is compatible with the grpc package it is being compiled against. 161 | const _ = grpc.SupportPackageIsVersion4 162 | 163 | // PayManagerClient is the client API for PayManager service. 164 | // 165 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. 166 | type PayManagerClient interface { 167 | Charge(ctx context.Context, in *PayRequest, opts ...grpc.CallOption) (*PayResponse, error) 168 | } 169 | 170 | type payManagerClient struct { 171 | cc *grpc.ClientConn 172 | } 173 | 174 | func NewPayManagerClient(cc *grpc.ClientConn) PayManagerClient { 175 | return &payManagerClient{cc} 176 | } 177 | 178 | func (c *payManagerClient) Charge(ctx context.Context, in *PayRequest, opts ...grpc.CallOption) (*PayResponse, error) { 179 | out := new(PayResponse) 180 | err := c.cc.Invoke(ctx, "/paymentservice.PayManager/Charge", in, out, opts...) 181 | if err != nil { 182 | return nil, err 183 | } 184 | return out, nil 185 | } 186 | 187 | // PayManagerServer is the server API for PayManager service. 188 | type PayManagerServer interface { 189 | Charge(context.Context, *PayRequest) (*PayResponse, error) 190 | } 191 | 192 | func RegisterPayManagerServer(s *grpc.Server, srv PayManagerServer) { 193 | s.RegisterService(&_PayManager_serviceDesc, srv) 194 | } 195 | 196 | func _PayManager_Charge_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 197 | in := new(PayRequest) 198 | if err := dec(in); err != nil { 199 | return nil, err 200 | } 201 | if interceptor == nil { 202 | return srv.(PayManagerServer).Charge(ctx, in) 203 | } 204 | info := &grpc.UnaryServerInfo{ 205 | Server: srv, 206 | FullMethod: "/paymentservice.PayManager/Charge", 207 | } 208 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 209 | return srv.(PayManagerServer).Charge(ctx, req.(*PayRequest)) 210 | } 211 | return interceptor(ctx, in, info, handler) 212 | } 213 | 214 | var _PayManager_serviceDesc = grpc.ServiceDesc{ 215 | ServiceName: "paymentservice.PayManager", 216 | HandlerType: (*PayManagerServer)(nil), 217 | Methods: []grpc.MethodDesc{ 218 | { 219 | MethodName: "Charge", 220 | Handler: _PayManager_Charge_Handler, 221 | }, 222 | }, 223 | Streams: []grpc.StreamDesc{}, 224 | Metadata: "proto/pay.proto", 225 | } 226 | 227 | func init() { proto.RegisterFile("proto/pay.proto", fileDescriptor_pay_a9772073fe55a113) } 228 | 229 | var fileDescriptor_pay_a9772073fe55a113 = []byte{ 230 | // 232 bytes of a gzipped FileDescriptorProto 231 | 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x6c, 0x90, 0x41, 0x4b, 0xc4, 0x30, 232 | 0x10, 0x85, 0x6d, 0x77, 0xb7, 0xd4, 0x59, 0x58, 0x61, 0x10, 0x09, 0xf5, 0x52, 0x7a, 0xda, 0x53, 233 | 0x05, 0xfd, 0x09, 0x7b, 0x16, 0x34, 0xe0, 0x0f, 0x18, 0xdb, 0x61, 0x0d, 0xd2, 0x24, 0x26, 0xa9, 234 | 0xd0, 0x9b, 0x3f, 0x5d, 0x9c, 0x2d, 0x5a, 0x61, 0x6f, 0xef, 0x65, 0x86, 0xcc, 0xf7, 0x1e, 0x5c, 235 | 0xf9, 0xe0, 0x92, 0xbb, 0xf3, 0x34, 0xb5, 0xa2, 0x70, 0xe7, 0x69, 0x1a, 0xd8, 0xa6, 0xc8, 0xe1, 236 | 0xd3, 0x74, 0xdc, 0x7c, 0x65, 0x00, 0x4f, 0x34, 0x69, 0xfe, 0x18, 0x39, 0x26, 0xdc, 0x41, 0x6e, 237 | 0x7a, 0x95, 0xd5, 0xd9, 0x7e, 0xa5, 0x73, 0xd3, 0xe3, 0x35, 0x6c, 0x92, 0x7b, 0x67, 0xab, 0xf2, 238 | 0x3a, 0xdb, 0x5f, 0xea, 0x93, 0xc1, 0x1b, 0x28, 0x68, 0x70, 0xa3, 0x4d, 0x6a, 0x25, 0x9b, 0xb3, 239 | 0x43, 0x84, 0xb5, 0xa5, 0x81, 0xd5, 0x5a, 0x96, 0x45, 0x63, 0x0d, 0xdb, 0x9e, 0x63, 0x17, 0x8c, 240 | 0x4f, 0xc6, 0x59, 0xb5, 0x91, 0xd1, 0xf2, 0xa9, 0x79, 0x81, 0xad, 0x10, 0x44, 0xef, 0x6c, 0xe4, 241 | 0x9f, 0x4f, 0x3c, 0xcd, 0x10, 0xa5, 0x16, 0x8d, 0x15, 0x94, 0x1d, 0xf9, 0x34, 0x06, 0xee, 0xe5, 242 | 0x64, 0xa9, 0x7f, 0xfd, 0x02, 0x26, 0x5f, 0xc2, 0xdc, 0x3f, 0x4b, 0xb0, 0x47, 0xb2, 0x74, 0xe4, 243 | 0x80, 0x07, 0x28, 0x0e, 0x6f, 0x14, 0x8e, 0x8c, 0x55, 0xfb, 0xbf, 0x82, 0xf6, 0x2f, 0x7e, 0x75, 244 | 0x7b, 0x76, 0x76, 0x02, 0x6b, 0x2e, 0x5e, 0x0b, 0xe9, 0xf0, 0xe1, 0x3b, 0x00, 0x00, 0xff, 0xff, 245 | 0xde, 0x90, 0x28, 0xa8, 0x56, 0x01, 0x00, 0x00, 246 | } 247 | -------------------------------------------------------------------------------- /payment-service/proto/pay.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package paymentservice; 4 | 5 | // For grpc gateway 6 | // import "google/api/annotations.proto"; 7 | 8 | 9 | service PayManager { 10 | rpc Charge (PayRequest) returns (PayResponse) {} 11 | } 12 | 13 | message PayRequest { 14 | int64 id = 1; 15 | string token = 2; 16 | int64 amount = 3; 17 | string name = 4; 18 | string description =5; 19 | } 20 | 21 | message PayResponse { 22 | bool paid = 1; 23 | bool captured = 3; 24 | int64 amount = 2; 25 | } 26 | -------------------------------------------------------------------------------- /payment-service/server/server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "net" 7 | "os" 8 | 9 | gpay "vue-golang-payment-app/payment-service/proto" 10 | 11 | payjp "github.com/payjp/payjp-go/v1" 12 | "google.golang.org/grpc" 13 | "google.golang.org/grpc/reflection" 14 | ) 15 | 16 | const ( 17 | port = ":50051" 18 | ) 19 | 20 | // server is used to implement sa 21 | type server struct{} 22 | 23 | func (s *server) Charge(ctx context.Context, req *gpay.PayRequest) (*gpay.PayResponse, error) { 24 | // PAI の初期化 25 | pay := payjp.New(os.Getenv("PAYJP_TEST_SECRET_KEY"), nil) 26 | 27 | // 支払いをします。第一引数に支払い金額、第二引数に支払いの方法や設定を入れます。 28 | charge, err := pay.Charge.Create(int(req.Amount), payjp.Charge{ 29 | // 現在はjpyのみサポート 30 | Currency: "jpy", 31 | // カード情報、顧客ID、カードトークンのいずれかを指定 32 | CardToken: req.Token, 33 | Capture: true, 34 | // 概要のテキストを設定できます 35 | Description: req.Name + ":" + req.Description, 36 | }) 37 | if err != nil { 38 | return nil, err 39 | } 40 | 41 | // 支払った結果から、Response生成 42 | res := &gpay.PayResponse{ 43 | Paid: charge.Paid, 44 | Captured: charge.Captured, 45 | Amount: int64(charge.Amount), 46 | } 47 | return res, nil 48 | } 49 | 50 | func main() { 51 | lis, err := net.Listen("tcp", port) 52 | if err != nil { 53 | log.Fatalf("failed to listen: %v", err) 54 | } 55 | s := grpc.NewServer() 56 | gpay.RegisterPayManagerServer(s, &server{}) 57 | 58 | // Register reflection service on gRPC server. 59 | reflection.Register(s) 60 | log.Printf("gRPC Server started: localhost%s\n", port) 61 | if err := s.Serve(lis); err != nil { 62 | log.Fatalf("failed to serve: %v", err) 63 | } 64 | } 65 | --------------------------------------------------------------------------------