こんにちは、KOUKIです。
GolangのWebフレームワークであるfiberを使って、APIを開発しています。
ここでは、本記事で実装したソースコードのまとめています。
尚、「React, NextJS and Golang: A Rapid Guide – Advanced」コースを参考にしています。解釈は私が勝手に付けているので、本物をみたい場合は受講をお勧めします!
ディレクトリツリー
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
. ├── Dockerfile ├── Makefile ├── README.md ├── docker-compose.yml ├── go.mod ├── go.sum ├── main.go └── src ├── commands │ ├── order │ │ └── populateOrders.go │ ├── product │ │ └── populateProducts.go │ ├── redis │ │ └── updateRankings.go │ └── user │ └── populateUsers.go ├── controllers │ ├── authController.go │ ├── linkController.go │ ├── orderController.go │ ├── productController.go │ └── userController.go ├── database │ ├── db.go │ └── redis.go ├── internal │ └── key.go ├── middleware │ └── auth.go ├── models │ ├── link.go │ ├── model.go │ ├── order.go │ ├── product.go │ └── user.go └── routes └── routes.go |
Docker
Dockerfile
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
FROM golang:1.16 WORKDIR /app # go mod init xxx でgo.modファイルを作成しておくこと COPY go.mod . COPY go.sum . # go modからパッケージをダウンロード RUN go mod download # /app にすべてのコードをコピー COPY . . # Live Reloading RUN curl -sSfL https://raw.githubusercontent.com/cosmtrek/air/master/install.sh | sh -s -- -b $(go env GOPATH)/bin # エントリポイント(air) CMD ["air"] |
docker-compose.yml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
# docker-compose.yml version: "3.3" services: backend: # docker-composeファイルと同階層のDockerfileをビルド build: . ports: # ローカル:Docker - 8000:3000 # DockerとローカルのFSをマウント volumes: - .:/app # dbを先に起動させる # ただし、初回起動時はDBの準備に手間取るので、コネクトに失敗する # 可能性がある depends_on: - db - redis db: image: mysql:5.7.22 # restart: always environment: MYSQL_DATABASE: ambassador MYSQL_USER: admin MYSQL_PASSWORD: admin MYSQL_ROOT_PASSWORD: root # ローカルに.dbdataを作成し、dbコンテナとFSをマウントする volumes: - .dbdata:/var/lib/mysql ports: - 33066:3306 redis: image: redis:latest ports: - 6379:6379 smtp: image: mailhog/mailhog ports: - "1025:1025" - "8025:8025" |
Mailefile
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
.PHONY: create-user create-product create-order update-ranking up # テストデータ作成 create-user: docker-compose run --rm backend sh -c "go run src/commands/user/populateUsers.go" create-product: docker-compose run --rm backend sh -c "go run src/commands/product/populateProducts.go" create-order: docker-compose run --rm backend sh -c "go run src/commands/order/populateOrders.go" update-ranking: docker-compose run --rm backend sh -c "go run src/commands/redis/updateRankings.go" # コンテナ起動 up: docker-compose up |
main.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
// main.go package main import ( "admin/src/database" "admin/src/routes" "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/middleware/cors" ) func main() { // Connection To Mysql database.Connect() // Migration database.AutoMigrate() // Redis database.SetupRedis() // Cache database.SetupCacheChannel() // fiber API app := fiber.New() // CORSの設定 app.Use(cors.New(cors.Config{ // 認証にcookieなどの情報を必要とするかどうか AllowCredentials: true, })) // Setup Routes routes.Setup(app) app.Listen(":3000") } |
go.mod
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
module admin go 1.15 require ( github.com/bxcodec/faker/v3 v3.6.0 github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/go-redis/redis/v8 v8.10.0 github.com/gofiber/fiber/v2 v2.11.0 github.com/stripe/stripe-go v70.15.0+incompatible golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a gorm.io/driver/mysql v1.1.0 gorm.io/gorm v1.21.10 ) |
※go.sumは自動生成されるので、割愛
src
populateOrders.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
// orderProducts.go package main import ( "admin/src/database" "admin/src/models" "fmt" "log" "math/rand" "github.com/bxcodec/faker/v3" ) func main() { // DB接続 database.Connect() log.Println("Creating Test Order...") for i := 0; i < 30; i++ { var orderItems []models.OrderItem for j := 0; j < rand.Intn(5); j++ { price := float64(rand.Intn(90) + 10) qty := uint(rand.Intn(5)) orderItems = append(orderItems, models.OrderItem{ ProductTitle: faker.Word(), Price: price, Quantity: qty, AdminRevenue: 0.9 * price * float64(qty), AmbassadorRevenue: 0.1 * price * float64(qty), }) database.DB.Create( &models.Order{ UserID: uint(rand.Intn(38) + 1), Code: faker.Username(), AmbassadorEmail: faker.Email(), FirstName: faker.FirstName(), LastName: faker.LastName(), Email: faker.Email(), Complete: true, OrderItems: orderItems, }) log.Println(fmt.Sprintf("Created Test Order %d", i+1)) } } log.Println("Finish Test Order!") } |
populateProducts.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
// populateProducts.go package main import ( "admin/src/database" "admin/src/models" "fmt" "log" "math/rand" "github.com/bxcodec/faker/v3" ) func main() { // DB接続 database.Connect() log.Println("Creating Test Product...") for i := 0; i < 30; i++ { product := models.Product{ Title: faker.Username(), Description: faker.Username(), Image: faker.URL(), Price: float64(rand.Intn(90) + 10), } database.DB.Create(&product) log.Println(fmt.Sprintf("Created Test Product %d", i+1)) } log.Println("Finish Test Product!") } |
updateRankings.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
// commands/redis/updateRankings.go package main import ( "admin/src/database" "admin/src/models" "context" "github.com/go-redis/redis/v8" ) func main() { database.Connect() database.SetupRedis() ctx := context.Background() var users []models.User database.DB.Find(&users, models.User{ IsAmbassador: true, }) for _, user := range users { ambassador := models.Ambassador(user) ambassador.CalculateRevenue(database.DB) // ZADD key // http://mogile.web.fc2.com/redis/commands/zadd.html#sorted-sets-101 database.Cache.ZAdd(ctx, "rankings", &redis.Z{ Score: *ambassador.Revenue, Member: user.Name(), }) } } |
populateUsers.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
// populateUsers.go package main import ( "admin/src/database" "admin/src/models" "fmt" "log" "github.com/bxcodec/faker/v3" ) func main() { // DB接続 database.Connect() log.Println("Creating Test User...") for i := 0; i < 30; i++ { ambassador := models.User{ FirstName: faker.FirstName(), LastName: faker.LastName(), Email: faker.Email(), IsAmbassador: true, } ambassador.SetPassword("1234") database.DB.Create(&ambassador) log.Println(fmt.Sprintf("Created Test User %d", i+1)) } log.Println("Finish Test User!") } |
authController.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 |
// controllers/auathController.go package controllers import ( "admin/src/database" "admin/src/middleware" "admin/src/models" "strings" "time" "github.com/gofiber/fiber/v2" "golang.org/x/crypto/bcrypt" ) func Register(ctx *fiber.Ctx) error { var data map[string]string if err := ctx.BodyParser(&data); err != nil { return err } if data["password"] != data["password_confirm"] { ctx.Status(fiber.StatusBadRequest) // 400 return ctx.JSON(fiber.Map{ "message": "パスワードに誤りがあります", }) } // ハッシュパスワードを作成 pwd, _ := bcrypt.GenerateFromPassword([]byte(data["password"]), 12) user := models.User{ FirstName: data["first_name"], LastName: data["last_name"], Email: data["email"], Password: pwd, IsAmbassador: strings.Contains(ctx.Path(), "/api/ambassador"), } // パスワードセット user.SetPassword(data["password"]) // ユーザー作成 result := database.DB.Create(&user) if result.Error != nil { ctx.Status(fiber.StatusBadRequest) return ctx.JSON(fiber.Map{ "message": "そのEmailは既に登録されています", }) } return ctx.JSON(user) } func Login(ctx *fiber.Ctx) error { var data map[string]string if err := ctx.BodyParser(&data); err != nil { return err } var user models.User database.DB.Where("email = ?", data["email"]).First(&user) if user.ID == 0 { ctx.Status(fiber.StatusBadRequest) return ctx.JSON(fiber.Map{ "message": "ログイン情報に誤りがあります", }) } // パスワードチェック err := user.ComparePassword(data["password"]) if err != nil { ctx.Status(fiber.StatusBadRequest) return ctx.JSON(fiber.Map{ "message": "ログイン情報に誤りがあります", }) } // ambassador判定 isAmbassador := strings.Contains(ctx.Path(), "/api/ambassador") var scope string if isAmbassador { scope = "ambassador" } else { scope = "admin" } if !isAmbassador && user.IsAmbassador { ctx.Status(fiber.StatusUnauthorized) // 401 return ctx.JSON(fiber.Map{ "message": "認証が許可されていません", }) } token, err := middleware.GenerateJWT(user.ID, scope) if err != nil { ctx.Status(fiber.StatusBadRequest) return ctx.JSON(fiber.Map{ "message": "ログイン情報に誤りがあります", }) } // Cookieに保存 cookie := fiber.Cookie{ Name: "jwt", Value: token, Expires: time.Now().Add(time.Hour * 24), HTTPOnly: true, } ctx.Cookie(&cookie) return ctx.JSON(fiber.Map{ "message": "success", }) } func User(ctx *fiber.Ctx) error { id, _ := middleware.GetUserID(ctx) // ユーザー検索 var user models.User database.DB.Where("id = ?", id).First(&user) if strings.Contains(ctx.Path(), "/api/ambassador") { ambassador := models.Ambassador(user) ambassador.CalculateRevenue(database.DB) return ctx.JSON(ambassador) } return ctx.JSON(user) } func Logout(ctx *fiber.Ctx) error { // cookieをクリアする cookie := fiber.Cookie{ Name: "jwt", Value: "", Expires: time.Now().Add(-time.Hour * 24), // -を指定 HTTPOnly: true, } ctx.Cookie(&cookie) return ctx.JSON(fiber.Map{ "message": "success", }) } func UpdateInfo(ctx *fiber.Ctx) error { var data map[string]string // リクエストデータをパースする if err := ctx.BodyParser(&data); err != nil { return err } // cookieからidを取得する id, _ := middleware.GetUserID(ctx) user := models.User{ FirstName: data["first_name"], LastName: data["last_name"], Email: data["email"], } user.ID = id // ユーザー情報更新 database.DB.Model(&user).Updates(&user) return ctx.JSON(user) } func UpdatePassword(ctx *fiber.Ctx) error { var data map[string]string // リクエストデータをパースする if err := ctx.BodyParser(&data); err != nil { return err } // パスワードチェック if data["password"] != data["password_confirm"] { ctx.Status(fiber.StatusBadRequest) // 400 return ctx.JSON(fiber.Map{ "message": "パスワードに誤りがあります", }) } // cookieからidを取得する id, _ := middleware.GetUserID(ctx) user := models.User{} user.ID = id // パスワードセット user.SetPassword(data["password"]) // ユーザー情報更新 database.DB.Model(&user).Updates(&user) return ctx.JSON(user) } |
linkController.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 |
// linkController.go package controllers import ( "admin/src/database" "admin/src/middleware" "admin/src/models" "fmt" "strconv" "github.com/bxcodec/faker/v3" "github.com/gofiber/fiber/v2" ) func Link(ctx *fiber.Ctx) error { id, _ := strconv.Atoi(ctx.Params("id")) var links []models.Link database.DB.Where("user_id = ?", id).Find(&links) for i, link := range links { var orders []models.Order database.DB.Where("code = ? and complete = true", link.Code).Find(&orders) links[i].Orders = orders } return ctx.JSON(links) } type CreateLinkRequest struct { ProductIDs []int } func CreateLink(ctx *fiber.Ctx) error { var request CreateLinkRequest // リクエストデータからProductIDを取得 if err := ctx.BodyParser(&request); err != nil { return err } fmt.Println(request) // ユーザーIDを取得 id, _ := middleware.GetUserID(ctx) // リンク作成 link := models.Link{ UserID: id, Code: faker.Username(), } // プロダクトID, ユーザーIDを紐づける for _, productID := range request.ProductIDs { product := models.Product{} product.ID = uint(productID) link.Products = append(link.Products, product) } // DB保存 database.DB.Create(&link) return ctx.JSON(link) } func Stats(ctx *fiber.Ctx) error { // ユーザーIDを取得 id, _ := middleware.GetUserID(ctx) // DB検索 var links []models.Link database.DB.Find(&links, models.Link{ UserID: id, }) var result []interface{} var orders []models.Order for _, link := range links { // OrderItemsを先にロードしてからOrderデータを検索 database.DB.Preload("OrderItems").Find(&orders, &models.Order{ Code: link.Code, Complete: true, }) var revenue float64 = 0 for _, order := range orders { revenue += order.GetTotal() } result = append(result, fiber.Map{ "code": link.Code, "count": len(orders), "revenue": revenue, }) } return ctx.JSON(result) } func GetLink(ctx *fiber.Ctx) error { // URL(links/:code)からcodeを取得する code := ctx.Params("code") link := models.Link{ Code: code, } // データベース検索 // linkデータを取得する前にUser, Productsデータを取得する database.DB.Preload("User").Preload("Products").First(&link) return ctx.JSON(link) } |
orderController.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 |
// controllers/orderController.go package controllers import ( "admin/src/database" "admin/src/internal" "admin/src/models" "context" "fmt" "net/smtp" "github.com/gofiber/fiber/v2" "github.com/stripe/stripe-go" "github.com/stripe/stripe-go/checkout/session" ) func Orders(ctx *fiber.Ctx) error { var orders []models.Order // DB検索 database.DB.Preload("OrderItems").Find(&orders) for i, order := range orders { orders[i].Name = order.FullName() orders[i].Total = order.GetTotal() } return ctx.JSON(orders) } type CreateOrderRequest struct { Code string FirstName string LastName string Email string Address string Country string City string Zip string Products []map[string]int } func CreateOrder(ctx *fiber.Ctx) error { var request CreateOrderRequest // リクエストデータを取得 if err := ctx.BodyParser(&request); err != nil { return err } // リクエストからコードを抜き出す link := models.Link{ Code: request.Code, } // DB検索 database.DB.Preload("User").First(&link) // 該当データがない場合はエラー if link.ID == 0 { ctx.Status(fiber.StatusBadRequest) return ctx.JSON(fiber.Map{ "message": "無効なリンクです", }) } // Orderを作成する order := models.Order{ Code: link.Code, UserID: link.UserID, AmbassadorEmail: link.User.Email, FirstName: request.FirstName, LastName: request.LastName, Email: request.Email, Address: request.Address, Country: request.Country, City: request.City, Zip: request.Zip, } // トランザクション tx := database.DB.Begin() // OrderをDBに保存 if err := tx.Create(&order).Error; err != nil { tx.Rollback() ctx.Status(fiber.StatusBadRequest) return ctx.JSON(fiber.Map{ "message": err.Error(), }) } // stripeパラメーター var lineItems []*stripe.CheckoutSessionLineItemParams // リクエストからプロダクトを取得 for _, requestProduct := range request.Products { product := models.Product{} product.ID = uint(requestProduct["product_id"]) // product検索 database.DB.First(&product) // トータルを算出 total := product.Price * float64(requestProduct["quantity"]) // OrderItemを作成 item := models.OrderItem{ OrderID: order.ID, ProductTitle: product.Title, Price: product.Price, Quantity: uint(requestProduct["quantity"]), AmbassadorRevenue: 0.1 * total, AdminRevenue: 0.9 * total, } // トランザクション // OrderItemをDBに保存 if err := tx.Create(&item).Error; err != nil { tx.Rollback() ctx.Status(fiber.StatusBadRequest) return ctx.JSON(fiber.Map{ "message": err.Error(), }) } // stripeアイテムセット lineItems = append(lineItems, &stripe.CheckoutSessionLineItemParams{ Name: stripe.String(product.Title), Description: stripe.String(product.Description), Images: []*string{stripe.String(product.Image)}, Amount: stripe.Int64(100 * int64(product.Price)), Currency: stripe.String("usd"), Quantity: stripe.Int64(int64(requestProduct["quantity"])), }) } // stripe checkout stripe.Key = internal.StripeSecretKey() params := stripe.CheckoutSessionParams{ // http://localhost:5000はフロントエンド側のリンク(まだ作成していない) SuccessURL: stripe.String("http://localhost:5000/success?source={CHECKOUT_SESSION_ID}"), CancelURL: stripe.String("http://localhost:5000/error"), PaymentMethodTypes: stripe.StringSlice([]string{"card"}), LineItems: lineItems, } source, err := session.New(¶ms) if err != nil { tx.Rollback() ctx.Status(fiber.StatusBadRequest) return ctx.JSON(fiber.Map{ "message": err.Error(), }) } // トランザクションIDを登録 order.TransactionID = source.ID // データを書き換えたので、上書き保存 if err := tx.Save(&order).Error; err != nil { tx.Rollback() ctx.Status(fiber.StatusBadRequest) return ctx.JSON(fiber.Map{ "message": err.Error(), }) } // 実行 tx.Commit() return ctx.JSON(source) } func CompleteOrder(ctx *fiber.Ctx) error { var data map[string]string // リクエストデータを取得 if err := ctx.BodyParser(&data); err != nil { return err } order := models.Order{} // OrderItemを検索 database.DB.Preload("OrderItems").First(&order, models.Order{ TransactionID: data["source"], }) if order.ID == 0 { ctx.Status((fiber.StatusNotFound)) return ctx.JSON(fiber.Map{ "message": "オーダーが見つかりません", }) } // Orderを保存 order.Complete = true database.DB.Save(&order) // Redisキャッシュに保存したRankingも更新 // Rankingはユーザーの購入金額を示す go func(order models.Order) { ambassadorRevenue := 0.0 adminRevenue := 0.0 for _, item := range order.OrderItems { ambassadorRevenue += item.AmbassadorRevenue adminRevenue += item.AdminRevenue } user := models.User{} user.ID = order.UserID database.DB.First(&user) // https://redis.io/commands/zincrby database.Cache.ZIncrBy(context.Background(), "rankings", ambassadorRevenue, user.Name()) // Email to ambassador ambassadorMessage := []byte( fmt.Sprintf("貴方の収益は、$%fです。Link: #%s", ambassadorRevenue, order.Code)) // MailhogへのPort smtp.SendMail( ":1025", // 宛先のアドレス nil, // Authentication "no-reply@email.com", // from []string{order.AmbassadorEmail}, // To ambassadorMessage) // Message // Email to admin adminMessage := []byte( fmt.Sprintf("Order: #%d, Total: %f", order.ID, adminRevenue)) // MailhogへのPort smtp.SendMail( ":1025", // 宛先のアドレス nil, // Authentication "no-reply@email.com", // from []string{"admin@admin.com"}, // To adminMessage) // Message }(order) return ctx.JSON(fiber.Map{ "message": "success", }) } |
productController.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 |
// productController.go package controllers import ( "admin/src/database" "admin/src/models" "context" "encoding/json" "sort" "strconv" "strings" "time" "github.com/gofiber/fiber/v2" ) var ( products_frontend = "products_frontend" products_backend = "products_backend" ) func Products(ctx *fiber.Ctx) error { var products []models.Product // 全てのプロダクトを取得 database.DB.Find(&products) return ctx.JSON(products) } func CreateProducts(ctx *fiber.Ctx) error { var product models.Product // リクエストデータをパースする if err := ctx.BodyParser(&product); err != nil { return err } // プロダクト取得 database.DB.Create(&product) // キャッシュ削除(goroutineで呼び出す) go database.ClearCache(products_frontend, products_backend) return ctx.JSON(product) } func GetProduct(ctx *fiber.Ctx) error { // リクエストからIDを取得 id, _ := strconv.Atoi(ctx.Params("id")) var product models.Product product.ID = uint(id) // プロダクト検索 database.DB.Find(&product) return ctx.JSON(product) } func UpdateProduct(ctx *fiber.Ctx) error { // リクエストからIDを取得 id, _ := strconv.Atoi(ctx.Params("id")) product := models.Product{} product.ID = uint(id) if err := ctx.BodyParser(&product); err != nil { return err } // プロダクト更新 database.DB.Model(&product).Updates(&product) // キャッシュ削除(goroutineで呼び出す) go database.ClearCache(products_frontend, products_backend) return ctx.JSON(product) } func DeleteProduct(ctx *fiber.Ctx) error { // リクエストからIDを取得 id, _ := strconv.Atoi(ctx.Params("id")) product := models.Product{} product.ID = uint(id) // プロダクト削除 database.DB.Delete(&product) // キャッシュ削除(goroutineで呼び出す) go database.ClearCache(products_frontend, products_backend) return nil } func ProductFrontend(ctx *fiber.Ctx) error { var products []models.Product var c = context.Background() expiredTime := 30 * time.Minute // products_frontend keyでRedisからデータを取得 result, err := database.Cache.Get(c, products_frontend).Result() if err != nil { database.DB.Find(&products) // Redisにデータを格納するため、エンコードする productBytes, err := json.Marshal(&products) if err != nil { panic(err) } // products_fronend keyでRedisにデータを格納 err = database.Cache.Set(c, products_frontend, productBytes, expiredTime).Err() if err != nil { panic(err) } } else { // デコードする json.Unmarshal([]byte(result), &products) } return ctx.JSON(products) } func ProductBackend(ctx *fiber.Ctx) error { var products []models.Product var c = context.Background() expiredTime := 30 * time.Minute // キャッシュ操作 result, err := database.Cache.Get(c, products_backend).Result() if err != nil { database.DB.Find(&products) productBytes, err := json.Marshal(&products) if err != nil { panic(err) } database.Cache.Set(c, products_backend, productBytes, expiredTime).Err() } else { json.Unmarshal([]byte(result), &products) } var searchProducts []models.Product // 検索 // urlの?q=XXXXから文字列を取得 if q := ctx.Query("q"); q != "" { // 大文字小文字の区別をなくすため、全て小文字扱いにする lower := strings.ToLower(q) for _, product := range products { // 検索条件1: Title if strings.Contains(strings.ToLower(product.Title), lower) || // 検索条件2: Description strings.Contains(strings.ToLower(product.Description), lower) { searchProducts = append(searchProducts, product) } } } else { // 検索しない場合は、全てのデータを返却 searchProducts = products } // ソート if sortParam := ctx.Query("sort"); sortParam != "" { sortLower := strings.ToLower(sortParam) if sortLower == "asc" { sort.Slice(searchProducts, func(i, j int) bool { return searchProducts[i].Price < searchProducts[j].Price }) } else if sortLower == "desc" { sort.Slice(searchProducts, func(i, j int) bool { return searchProducts[i].Price > searchProducts[j].Price }) } } // ページネーション var total = len(searchProducts) // デフォルトは"1"ページ page, _ := strconv.Atoi(ctx.Query("page", "1")) // 1ページ最大9個の商品 perPage := 9 var data []models.Product if total <= page*perPage && total >= (page-1)*perPage { data = searchProducts[(page-1)*perPage : total] } else if total >= page*perPage { data = searchProducts[(page-1)*perPage : page*perPage] } else { data = []models.Product{} } // 1ページ目 -> 0 ~ 8 // 2パージ目 -> 9 ~ 17 return ctx.JSON(fiber.Map{ "data": data, "total": total, "page": page, "last_page": total/perPage + 1, }) } |
userController.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
// userController.go package controllers import ( "admin/src/database" "admin/src/models" "context" "github.com/go-redis/redis/v8" "github.com/gofiber/fiber/v2" ) func Ambassadors(ctx *fiber.Ctx) error { var users []models.User database.DB.Where("is_ambassador = true").Find(&users) return ctx.JSON(users) } func Ranking(ctx *fiber.Ctx) error { // zrevrangebyscore //http://mogile.web.fc2.com/redis/commands/zrevrangebyscore.html rankings, err := database.Cache.ZRevRangeByScoreWithScores( context.Background(), "rankings", // updateRankings.goでkeyとして設定した値 &redis.ZRangeBy{ Min: "-inf", Max: "+inf", }).Result() if err != nil { return err } result := make(map[string]float64) for _, ranking := range rankings { result[ranking.Member.(string)] = ranking.Score } return ctx.JSON(result) } |
db.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
// database/db.go package database import ( "admin/src/models" "gorm.io/driver/mysql" "gorm.io/gorm" ) const ( // ユーザー:パスワード@tcp(dockerのサービス名(db):port)/db名 dsn = "admin:admin@tcp(db:3306)/ambassador?charset=utf8mb4&parseTime=True&loc=Local" ) var DB *gorm.DB func Connect() { var err error DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{}) if err != nil { panic("Could not connect with the database!") } } func AutoMigrate() { // User構造体に沿ってテーブルのスキーマーを作成する DB.AutoMigrate(models.User{}, models.Product{}, models.Link{}, models.Order{}, models.OrderItem{}) } |
redis.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
// database/redis.go package database import ( "context" "log" "time" "github.com/go-redis/redis/v8" ) var Cache *redis.Client var CacheChannel chan string func SetupRedis() { Cache = redis.NewClient(&redis.Options{ // docker-compose.ymlに指定したservice名+port Addr: "redis:6379", DB: 0, }) } func SetupCacheChannel() { CacheChannel = make(chan string) go func(ch chan string) { for { time.Sleep(3 * time.Second) key := <-ch // メッセージが入ってくるまでブロック // キャッシュ削除 Cache.Del(context.Background(), key) log.Printf("Cache cleared %s", key) } }(CacheChannel) } func ClearCache(keys ...string) { for _, key := range keys { CacheChannel <- key } } |
key.go
1 2 3 4 5 6 7 |
// internal/key.go package internal // このキーは外部に公開してはならない func StripeSecretKey() string { return "sk_test_hoge" # シークレットキーの値は変えています } |
auth.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 |
// auth.go package middleware import ( "fmt" "strconv" "strings" "time" "github.com/dgrijalva/jwt-go" "github.com/gofiber/fiber/v2" ) const SecretKey = "secret" type ClaimsWithScope struct { jwt.StandardClaims Scope string } func IsAuthenticate(ctx *fiber.Ctx) error { // cookieから情報を取得 cookie := ctx.Cookies("jwt") // token取得 token, err := jwt.ParseWithClaims( cookie, &ClaimsWithScope{}, func(token *jwt.Token) (interface{}, error) { return []byte(SecretKey), nil }, ) if err != nil || !token.Valid { ctx.Status(fiber.StatusUnauthorized) // 401 return ctx.JSON(fiber.Map{ "message": "認証がされていません", }) } payload := token.Claims.(*ClaimsWithScope) IsAmbassador := strings.Contains(ctx.Path(), "/api/ambassador") if (payload.Scope == "admin" && IsAmbassador) || (payload.Scope == "ambassador" && !IsAmbassador) { ctx.Status(fiber.StatusUnauthorized) // 401 return ctx.JSON(fiber.Map{ "message": "認証が許可されません", }) } return ctx.Next() } func GenerateJWT(id uint, scope string) (string, error) { // トークンの発行 payload := ClaimsWithScope{} payload.Subject = strconv.Itoa(int(id)) payload.ExpiresAt = time.Now().Add(time.Hour * 24).Unix() payload.Scope = scope return jwt.NewWithClaims(jwt.SigningMethodHS256, payload).SignedString([]byte(SecretKey)) } func GetUserID(ctx *fiber.Ctx) (uint, error) { // cookieから情報を取得 cookie := ctx.Cookies("jwt") // token取得 token, err := jwt.ParseWithClaims( cookie, &ClaimsWithScope{}, func(token *jwt.Token) (interface{}, error) { return []byte(SecretKey), nil }, ) if err != nil { fmt.Println(err) return 0, err } payload := token.Claims.(*ClaimsWithScope) id, _ := strconv.Atoi(payload.Subject) return uint(id), nil } |
link.go
1 2 3 4 5 6 7 8 9 10 11 |
// link.go package models type Link struct { Model Code string `json:"code"` UserID uint `json:"user_id"` User User `json:"user" gorm:"foreignKey:UserID"` Products []Product `json:"products" gorm:"many2many:link_products"` Orders []Order `json:"orders,omitempty" gorm:"-"` } |
model.go
1 2 3 4 5 6 |
// model.go package models type Model struct { ID uint `json:"id"` } |
order.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
// order.go package models type Order struct { Model TransactionID string `json:"transaction_id" gorm:"null"` UserID uint `json:"user_id"` Code string `json:"code"` AmbassadorEmail string `json:"ambassador_email"` FirstName string `json:"-"` LastName string `json:"-"` Name string `json:"name" gorm:"-"` Email string `json:"email"` Address string `json:"address" gorm:"null"` City string `json:"city" gorm:"null"` Country string `json:"country" gorm:"null"` Zip string `json:"zip" gorm:"null"` Complete bool `json:"complete" gorm:"default:false"` Total float64 `json:"total" gorm:"-"` OrderItems []OrderItem `json:"order_items" gorm:"foreignKey:OrderID"` } type OrderItem struct { Model OrderID uint `json:"order_id"` ProductTitle string `json:"product_title"` Price float64 `json:"price"` Quantity uint `json:"quantity"` AdminRevenue float64 `json:"admin_revenue"` AmbassadorRevenue float64 `json:"ambassador_revenue"` } func (o *Order) FullName() string { return o.FirstName + " " + o.LastName } func (o *Order) GetTotal() float64 { var total float64 = 0 for _, orderItem := range o.OrderItems { total += orderItem.Price * float64(orderItem.Quantity) } return total } |
product.go
1 2 3 4 5 6 7 8 9 10 |
// product.go package models type Product struct { Model Title string `json:"title"` Description string `json:"description"` Image string `json:"image"` Price float64 `json:"price"` } |
user.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 |
// models/user.go package models import ( "fmt" "golang.org/x/crypto/bcrypt" "gorm.io/gorm" ) type User struct { Model FirstName string `json:"first_name"` LastName string `json:"last_name"` Email string `json:"email" gorm:"unique"` Password []byte `json:"-"` IsAmbassador bool `json:"-"` Revenue *float64 `json:"revenue,omitempty" gorm:"-"` // 空の場合はエンコードから除外 } func (u *User) SetPassword(pwd string) { // ハッシュパスワードを作成 hashPwd, _ := bcrypt.GenerateFromPassword([]byte(pwd), 12) u.Password = hashPwd } func (u *User) ComparePassword(pwd string) error { return bcrypt.CompareHashAndPassword(u.Password, []byte(pwd)) } func (u *User) Name() string { return u.FirstName + " " + u.LastName } type Admin User func (a *Admin) CalculateRevenue(db *gorm.DB) { var orders []Order // Preloadで他のSQL内のリレーションを事前読み込む db.Preload("OrderItems").Find(&orders, &Order{ UserID: a.ID, Complete: true, }) var revenue float64 = 0.0 for _, order := range orders { for _, orderItem := range order.OrderItems { revenue += orderItem.AdminRevenue } } a.Revenue = &revenue } type Ambassador User func (a *Ambassador) CalculateRevenue(db *gorm.DB) { var orders []Order fmt.Println(a.ID) // Preloadで他のSQL内のリレーションを事前読み込む db.Preload("OrderItems").Find(&orders, &Order{ UserID: a.ID, Complete: true, }) var revenue float64 = 0.0 for _, order := range orders { for _, orderItem := range order.OrderItems { revenue += orderItem.AmbassadorRevenue } } a.Revenue = &revenue } |
routes.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
// routes/routes.go package routes import ( "admin/src/controllers" "admin/src/middleware" "github.com/gofiber/fiber/v2" ) func Setup(app *fiber.App) { // Group api := app.Group("api") // admin admin := api.Group("admin") admin.Post("register", controllers.Register) admin.Post("login", controllers.Login) adminAuthenticated := admin.Use(middleware.IsAuthenticate) adminAuthenticated.Get("user", controllers.User) adminAuthenticated.Post("logout", controllers.Logout) adminAuthenticated.Put("info", controllers.UpdateInfo) adminAuthenticated.Put("password", controllers.UpdatePassword) adminAuthenticated.Get("ambassadors", controllers.Ambassadors) adminAuthenticated.Get("products", controllers.Products) adminAuthenticated.Post("products", controllers.CreateProducts) adminAuthenticated.Get("products/:id", controllers.GetProduct) adminAuthenticated.Put("products/:id", controllers.UpdateProduct) adminAuthenticated.Delete("products/:id", controllers.DeleteProduct) adminAuthenticated.Get("users/:id/links", controllers.Link) adminAuthenticated.Get("orders", controllers.Orders) // Ambassador ambassador := api.Group("ambassador") ambassador.Post("register", controllers.Register) ambassador.Post("login", controllers.Login) ambassador.Get("products/frontend", controllers.ProductFrontend) ambassador.Get("products/backend", controllers.ProductBackend) ambassadorAuthentication := ambassador.Use(middleware.IsAuthenticate) ambassadorAuthentication.Get("user", controllers.User) ambassadorAuthentication.Post("logout", controllers.Logout) ambassadorAuthentication.Put("users/info", controllers.UpdateInfo) ambassadorAuthentication.Put("users/password", controllers.UpdatePassword) ambassadorAuthentication.Post("links", controllers.CreateLink) ambassadorAuthentication.Get("stats", controllers.Stats) ambassadorAuthentication.Get("rankings", controllers.Ranking) // Checkout checkout := api.Group("checkout") checkout.Get("links/:code", controllers.GetLink) checkout.Post("orders", controllers.CreateOrder) checkout.Post("orders/confirm", controllers.CompleteOrder) } |
コメントを残す
コメントを投稿するにはログインしてください。