diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dbf4200 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +database.db diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..5f26036 --- /dev/null +++ b/Makefile @@ -0,0 +1,3 @@ +default: + go run main.go + \ No newline at end of file diff --git a/README.md b/README.md index 7ec9f3e..bc00e05 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,15 @@ # Step up Go for students 2-1 see: http://bit.ly/devlopwithgo + +## Structure +``` +├── pkg: +│ ├── application: サービスロジック +│ ├── di: 依存性の注入 +│ ├── infra: 外部との通信(API) +│ └── server +│ ├── handler: ハンドラ +│ └── server.go: ルーティング +└── main.go: +``` diff --git a/main.go b/main.go index 34194b1..36b417f 100644 --- a/main.go +++ b/main.go @@ -10,229 +10,10 @@ TODO */ import ( - "database/sql" - "encoding/json" - "fmt" - "net" - "net/http" - "os" - "strconv" - _ "github.com/mattn/go-sqlite3" + "github.com/stepupgo/stepupgo2-1/pkg/server" ) func main() { - - db, err := sql.Open("sqlite3", "database.db") - if err != nil { - panic(err) - } - - if err := initDB(db); err != nil { - panic(err) - } - - http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - resp, err := http.Get("https://lottery-dot-tenntenn-samples.appspot.com/available_lotteries") - if err != nil { - const status = http.StatusInternalServerError - http.Error(w, http.StatusText(status), status) - return - } - defer resp.Body.Close() - - var lotteries []*Lottery - if err := json.NewDecoder(resp.Body).Decode(&lotteries); err != nil { - const status = http.StatusInternalServerError - http.Error(w, http.StatusText(status), status) - return - } - - if err := listTmpl.Execute(w, lotteries); err != nil { - const status = http.StatusInternalServerError - http.Error(w, http.StatusText(status), status) - return - } - }) - - http.HandleFunc("/purchase_page", func(w http.ResponseWriter, r *http.Request) { - resp, err := http.Get("https://lottery-dot-tenntenn-samples.appspot.com/lottery?id=" + r.FormValue("id")) - if err != nil { - const status = http.StatusInternalServerError - http.Error(w, http.StatusText(status), status) - return - } - defer resp.Body.Close() - - var l Lottery - if err := json.NewDecoder(resp.Body).Decode(&l); err != nil { - const status = http.StatusInternalServerError - http.Error(w, http.StatusText(status), status) - return - } - - data := struct { - Lottery - Remain int64 - }{ - Lottery: l, - Remain: l.Num, // TODO: 残りを計算する - } - if err := purchasePageTmpl.Execute(w, data); err != nil { - const status = http.StatusInternalServerError - http.Error(w, http.StatusText(status), status) - return - } - }) - - http.HandleFunc("/purchase", func(w http.ResponseWriter, r *http.Request) { - id := r.FormValue("id") - num, err := strconv.Atoi(r.FormValue("num")) - if err != nil { - const status = http.StatusInternalServerError - http.Error(w, http.StatusText(status), status) - return - } - // TODO: パラメタのバリデーション - - resp, err := http.Get("https://lottery-dot-tenntenn-samples.appspot.com/lottery?id=" + id) - if err != nil { - const status = http.StatusInternalServerError - http.Error(w, http.StatusText(status), status) - return - } - defer resp.Body.Close() - - var l Lottery - if err := json.NewDecoder(resp.Body).Decode(&l); err != nil { - const status = http.StatusInternalServerError - http.Error(w, http.StatusText(status), status) - return - } - - var count int - if err := db.QueryRow("SELECT COUNT(*) FROM purchased WHERE lottery_id = ?", l.ID).Scan(&count); err != nil { - const status = http.StatusInternalServerError - http.Error(w, http.StatusText(status), status) - return - } - - for i := 1; i <= num; i++ { - const sql = "INSERT INTO purchased(lottery_id, number) values (?,?)" - format := fmt.Sprintf(`%%0%dd`, len(strconv.FormatInt(l.Num-1, 10))) - n := fmt.Sprintf(format, count+i) - if _, err := db.Exec(sql, id, n); err != nil { - const status = http.StatusInternalServerError - http.Error(w, http.StatusText(status), status) - return - } - } - - http.Redirect(w, r, "/purchase_page?id="+l.ID, http.StatusFound) - }) - - http.HandleFunc("/result", func(w http.ResponseWriter, r *http.Request) { - resp1, err := http.Get("https://lottery-dot-tenntenn-samples.appspot.com/result?id=" + r.FormValue("id")) - if err != nil { - const status = http.StatusInternalServerError - http.Error(w, http.StatusText(status), status) - return - } - defer resp1.Body.Close() - - var result Result - if err := json.NewDecoder(resp1.Body).Decode(&result); err != nil { - const status = http.StatusInternalServerError - http.Error(w, http.StatusText(status), status) - return - } - - resp2, err := http.Get("https://lottery-dot-tenntenn-samples.appspot.com/lottery?id=" + r.FormValue("id")) - if err != nil { - const status = http.StatusInternalServerError - http.Error(w, http.StatusText(status), status) - return - } - defer resp2.Body.Close() - - var l Lottery - if err := json.NewDecoder(resp2.Body).Decode(&l); err != nil { - const status = http.StatusInternalServerError - http.Error(w, http.StatusText(status), status) - return - } - - type winner struct { - Prize *Prize - Numbers []string - } - - data := struct { - Lottery - Winners map[string]*winner - }{ - Lottery: l, - Winners: map[string]*winner{}, - } - - rows, err := db.Query("SELECT number FROM purchased WHERE lottery_id = ?", l.ID) - if err != nil { - const status = http.StatusInternalServerError - http.Error(w, http.StatusText(status), status) - return - } - for rows.Next() { - var number string - if err := rows.Scan(&number); err != nil { - const status = http.StatusInternalServerError - http.Error(w, http.StatusText(status), status) - return - } - - for i := range result.Winners { - for _, n := range result.Winners[i].Numbers { - if number == n { - prizeID := result.Winners[i].PrizeID - if data.Winners[prizeID] == nil { - for _, p := range l.Prizes { - if p.ID == prizeID { - data.Winners[prizeID] = &winner{ - Prize: p, - } - } - } - } - data.Winners[prizeID].Numbers = append(data.Winners[prizeID].Numbers, n) - } - } - } - } - - if err := resultTmpl.Execute(w, data); err != nil { - const status = http.StatusInternalServerError - http.Error(w, http.StatusText(status), status) - return - } - }) - - port := os.Getenv("PORT") - if port == "" { - port = "8080" - } - addr := net.JoinHostPort("", port) - http.ListenAndServe(addr, nil) -} - -func initDB(db *sql.DB) error { - const sql = ` -CREATE TABLE IF NOT EXISTS purchased ( - lottery_id TEXT NOT NULL, - number TEXT NOT NULL, - PRIMARY KEY(lottery_id, number) -); -` - if _, err := db.Exec(sql); err != nil { - return err - } - return nil + server.Run() } diff --git a/pkg/application/top.go b/pkg/application/top.go new file mode 100644 index 0000000..daf489c --- /dev/null +++ b/pkg/application/top.go @@ -0,0 +1,24 @@ +package application + +import ( + "net/http" + + "github.com/stepupgo/stepupgo2-1/pkg/infra/api" +) + +type top struct { + api api.ITop +} + +type ATop interface { + GetAvailable() (*http.Response, error) +} + +func NewTopApp(t api.ITop) ATop { + return &top{t} +} + +func (t *top) GetAvailable() (*http.Response, error) { + resp, err := t.api.GetAvailableLotteries() + return resp, err +} diff --git a/pkg/db/db.go b/pkg/db/db.go new file mode 100644 index 0000000..ed88dd4 --- /dev/null +++ b/pkg/db/db.go @@ -0,0 +1,43 @@ +package db + +import ( + "database/sql" + "log" +) + +var ( + DB *sql.DB +) + +func createTableSQL() string { + const sql = ` + CREATE TABLE IF NOT EXISTS purchased ( + lottery_id TEXT NOT NULL, + number TEXT NOT NULL, + PRIMARY KEY(lottery_id, number) + ); + ` + + return sql +} + +func initDB() error { + sql := createTableSQL() + if _, err := DB.Exec(sql); err != nil { + return err + } + return nil +} + +func Init() { + db, err := sql.Open("sqlite3", "database.db") + if err != nil { + log.Println(err) + } + + DB = db + + if err := initDB(); err != nil { + log.Println(err) + } +} diff --git a/pkg/di/di.go b/pkg/di/di.go new file mode 100644 index 0000000..6d39d3b --- /dev/null +++ b/pkg/di/di.go @@ -0,0 +1,19 @@ +package di + +import ( + app "github.com/stepupgo/stepupgo2-1/pkg/application" + "github.com/stepupgo/stepupgo2-1/pkg/infra/api" +) + +var ( + Top app.ATop +) + +func Init() { + initTop() +} + +func initTop() { + t := api.NewITop() + Top = app.NewTopApp(t) +} diff --git a/pkg/infra/api/top.go b/pkg/infra/api/top.go new file mode 100644 index 0000000..06d69d4 --- /dev/null +++ b/pkg/infra/api/top.go @@ -0,0 +1,18 @@ +package api + +import "net/http" + +type top struct{} + +type ITop interface { + GetAvailableLotteries() (*http.Response, error) +} + +func NewITop() ITop { + return &top{} +} + +func (t *top) GetAvailableLotteries() (*http.Response, error) { + resp, err := http.Get("https://lottery-dot-tenntenn-samples.appspot.com/available_lotteries") + return resp, err +} diff --git a/pkg/server/handler/purchase.go b/pkg/server/handler/purchase.go new file mode 100644 index 0000000..e7004e8 --- /dev/null +++ b/pkg/server/handler/purchase.go @@ -0,0 +1,92 @@ +package handler + +import ( + "encoding/json" + "fmt" + "net/http" + "strconv" + + "github.com/stepupgo/stepupgo2-1/pkg/db" + "github.com/stepupgo/stepupgo2-1/pkg/server/model" + "github.com/stepupgo/stepupgo2-1/pkg/view" +) + +func PurchasePage() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + resp, err := http.Get("https://lottery-dot-tenntenn-samples.appspot.com/lottery?id=" + r.FormValue("id")) + if err != nil { + const status = http.StatusInternalServerError + http.Error(w, http.StatusText(status), status) + return + } + defer resp.Body.Close() + + var l model.Lottery + if err := json.NewDecoder(resp.Body).Decode(&l); err != nil { + const status = http.StatusInternalServerError + http.Error(w, http.StatusText(status), status) + return + } + + data := struct { + model.Lottery + Remain int64 + }{ + Lottery: l, + Remain: l.Num, // TODO: 残りを計算する + } + if err := view.PurchasePageTmpl.Execute(w, data); err != nil { + const status = http.StatusInternalServerError + http.Error(w, http.StatusText(status), status) + return + } + } +} + +func Purchase() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + id := r.FormValue("id") + num, err := strconv.Atoi(r.FormValue("num")) + if err != nil { + const status = http.StatusInternalServerError + http.Error(w, http.StatusText(status), status) + return + } + // TODO: パラメタのバリデーション + + resp, err := http.Get("https://lottery-dot-tenntenn-samples.appspot.com/lottery?id=" + id) + if err != nil { + const status = http.StatusInternalServerError + http.Error(w, http.StatusText(status), status) + return + } + defer resp.Body.Close() + + var l model.Lottery + if err := json.NewDecoder(resp.Body).Decode(&l); err != nil { + const status = http.StatusInternalServerError + http.Error(w, http.StatusText(status), status) + return + } + + var count int + if err := db.DB.QueryRow("SELECT COUNT(*) FROM purchased WHERE lottery_id = ?", l.ID).Scan(&count); err != nil { + const status = http.StatusInternalServerError + http.Error(w, http.StatusText(status), status) + return + } + + for i := 1; i <= num; i++ { + const sql = "INSERT INTO purchased(lottery_id, number) values (?,?)" + format := fmt.Sprintf(`%%0%dd`, len(strconv.FormatInt(l.Num-1, 10))) + n := fmt.Sprintf(format, count+i) + if _, err := db.DB.Exec(sql, id, n); err != nil { + const status = http.StatusInternalServerError + http.Error(w, http.StatusText(status), status) + return + } + } + + http.Redirect(w, r, "/purchase_page?id="+l.ID, http.StatusFound) + } +} diff --git a/pkg/server/handler/result.go b/pkg/server/handler/result.go new file mode 100644 index 0000000..69b13b8 --- /dev/null +++ b/pkg/server/handler/result.go @@ -0,0 +1,96 @@ +package handler + +import ( + "encoding/json" + "net/http" + + "github.com/stepupgo/stepupgo2-1/pkg/db" + "github.com/stepupgo/stepupgo2-1/pkg/server/model" + "github.com/stepupgo/stepupgo2-1/pkg/view" +) + +func Result() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + resp1, err := http.Get("https://lottery-dot-tenntenn-samples.appspot.com/result?id=" + r.FormValue("id")) + if err != nil { + const status = http.StatusInternalServerError + http.Error(w, http.StatusText(status), status) + return + } + defer resp1.Body.Close() + + var result model.Result + if err := json.NewDecoder(resp1.Body).Decode(&result); err != nil { + const status = http.StatusInternalServerError + http.Error(w, http.StatusText(status), status) + return + } + + resp2, err := http.Get("https://lottery-dot-tenntenn-samples.appspot.com/lottery?id=" + r.FormValue("id")) + if err != nil { + const status = http.StatusInternalServerError + http.Error(w, http.StatusText(status), status) + return + } + defer resp2.Body.Close() + + var l model.Lottery + if err := json.NewDecoder(resp2.Body).Decode(&l); err != nil { + const status = http.StatusInternalServerError + http.Error(w, http.StatusText(status), status) + return + } + + type winner struct { + Prize *model.Prize + Numbers []string + } + + data := struct { + model.Lottery + Winners map[string]*winner + }{ + Lottery: l, + Winners: map[string]*winner{}, + } + + rows, err := db.DB.Query("SELECT number FROM purchased WHERE lottery_id = ?", l.ID) + if err != nil { + const status = http.StatusInternalServerError + http.Error(w, http.StatusText(status), status) + return + } + for rows.Next() { + var number string + if err := rows.Scan(&number); err != nil { + const status = http.StatusInternalServerError + http.Error(w, http.StatusText(status), status) + return + } + + for i := range result.Winners { + for _, n := range result.Winners[i].Numbers { + if number == n { + prizeID := result.Winners[i].PrizeID + if data.Winners[prizeID] == nil { + for _, p := range l.Prizes { + if p.ID == prizeID { + data.Winners[prizeID] = &winner{ + Prize: p, + } + } + } + } + data.Winners[prizeID].Numbers = append(data.Winners[prizeID].Numbers, n) + } + } + } + } + + if err := view.ResultTmpl.Execute(w, data); err != nil { + const status = http.StatusInternalServerError + http.Error(w, http.StatusText(status), status) + return + } + } +} diff --git a/pkg/server/handler/top.go b/pkg/server/handler/top.go new file mode 100644 index 0000000..624efb5 --- /dev/null +++ b/pkg/server/handler/top.go @@ -0,0 +1,35 @@ +package handler + +import ( + "encoding/json" + "net/http" + + "github.com/stepupgo/stepupgo2-1/pkg/di" + "github.com/stepupgo/stepupgo2-1/pkg/server/model" + "github.com/stepupgo/stepupgo2-1/pkg/view" +) + +func TopPage() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + resp, err := di.Top.GetAvailable() + if err != nil { + const status = http.StatusInternalServerError + http.Error(w, http.StatusText(status), status) + return + } + defer resp.Body.Close() + + var lotteries []*model.Lottery + if err := json.NewDecoder(resp.Body).Decode(&lotteries); err != nil { + const status = http.StatusInternalServerError + http.Error(w, http.StatusText(status), status) + return + } + + if err := view.ListTmpl.Execute(w, lotteries); err != nil { + const status = http.StatusInternalServerError + http.Error(w, http.StatusText(status), status) + return + } + } +} diff --git a/pkg/server/model/definition.go b/pkg/server/model/definition.go new file mode 100644 index 0000000..68c24ab --- /dev/null +++ b/pkg/server/model/definition.go @@ -0,0 +1,9 @@ +package model + +type DrawStatus int64 + +const ( + DrawStatusNotDrawn DrawStatus = 0 + DrawStatusMidDrawn DrawStatus = 1 + DrawStatusDrawn DrawStatus = 2 +) diff --git a/pkg/server/model/lottery.go b/pkg/server/model/lottery.go new file mode 100644 index 0000000..373e0e1 --- /dev/null +++ b/pkg/server/model/lottery.go @@ -0,0 +1,13 @@ +package model + +import "time" + +type Lottery struct { + ID string `json:"id"` + Name string `json:"name"` + Price int64 `json:"price"` + Num int64 `json:"num"` + Prizes []*Prize `json:"prizes"` + StartAt time.Time `json:"start_at"` + DrawAt time.Time `json:"draw_at"` +} diff --git a/pkg/server/model/prize.go b/pkg/server/model/prize.go new file mode 100644 index 0000000..b2a038b --- /dev/null +++ b/pkg/server/model/prize.go @@ -0,0 +1,8 @@ +package model + +type Prize struct { + ID string `json:"id"` + Name string `json:"name` + Num int64 `json:"num"` + Amount int64 `json:"amount"` +} diff --git a/pkg/server/model/result.go b/pkg/server/model/result.go new file mode 100644 index 0000000..12f84ac --- /dev/null +++ b/pkg/server/model/result.go @@ -0,0 +1,6 @@ +package model + +type Result struct { + Status DrawStatus `json:"status"` + Winners []*Winner `json:"winners"` +} diff --git a/pkg/server/model/types.go b/pkg/server/model/types.go new file mode 100644 index 0000000..8b53790 --- /dev/null +++ b/pkg/server/model/types.go @@ -0,0 +1 @@ +package model diff --git a/pkg/server/model/winner.go b/pkg/server/model/winner.go new file mode 100644 index 0000000..4a73431 --- /dev/null +++ b/pkg/server/model/winner.go @@ -0,0 +1,6 @@ +package model + +type Winner struct { + PrizeID string `json:"prize_id"` + Numbers []string `json:"numbers"` +} diff --git a/pkg/server/server.go b/pkg/server/server.go new file mode 100644 index 0000000..564b14a --- /dev/null +++ b/pkg/server/server.go @@ -0,0 +1,37 @@ +package server + +import ( + "net" + "net/http" + "os" + + "github.com/stepupgo/stepupgo2-1/pkg/db" + "github.com/stepupgo/stepupgo2-1/pkg/di" + "github.com/stepupgo/stepupgo2-1/pkg/server/handler" +) + +func routing() { + http.HandleFunc("/", handler.TopPage()) + http.HandleFunc("/purchase_page", handler.PurchasePage()) + http.HandleFunc("/purchase", handler.Purchase()) + http.HandleFunc("/result", handler.Result()) +} + +func connection() { + port := os.Getenv("PORT") + if port == "" { + port = "8080" + } + addr := net.JoinHostPort("", port) + http.ListenAndServe(addr, nil) +} + +func Run() { + // Initialize + db.Init() + di.Init() + + // Service + routing() + connection() +} diff --git a/pkg/view/list.go b/pkg/view/list.go new file mode 100644 index 0000000..5761a10 --- /dev/null +++ b/pkg/view/list.go @@ -0,0 +1,25 @@ +package view + +import "html/template" + +var ListTmpl = template.Must(template.New("list").Parse( + ` + + + + + 宝くじ + + + + + + `), +) diff --git a/pkg/view/purchase.go b/pkg/view/purchase.go new file mode 100644 index 0000000..fce2663 --- /dev/null +++ b/pkg/view/purchase.go @@ -0,0 +1,35 @@ +package view + +import "html/template" + +var PurchasePageTmpl = template.Must(template.New("purchase_page").Parse( + ` + + + + + Purchase {{.Name}} + + +

{{.Name}}

+

Prizes

+ + + {{if .Remain}} +
+ + + + +
+ {{else}} + SOLD OUT + {{end}} + + + `), +) diff --git a/pkg/view/result.go b/pkg/view/result.go new file mode 100644 index 0000000..b714183 --- /dev/null +++ b/pkg/view/result.go @@ -0,0 +1,31 @@ +package view + +import "html/template" + +var ResultTmpl = template.Must(template.New("result").Parse( + ` + + + + + Result of {{.Name}} + + + {{range .Prizes}} + {{$winner := index $.Winners .ID}} + {{if $winner }} +

{{.Name}} (${{.Amount}})

+ + {{else}} +

{{.Name}} (${{.Amount}})

+ No winners + {{end}} + {{end}} + + + `), +) diff --git a/pkg/view/template.go b/pkg/view/template.go new file mode 100644 index 0000000..ef1189a --- /dev/null +++ b/pkg/view/template.go @@ -0,0 +1 @@ +package view diff --git a/template.go b/template.go deleted file mode 100644 index e654236..0000000 --- a/template.go +++ /dev/null @@ -1,73 +0,0 @@ -package main - -import "html/template" - -var listTmpl = template.Must(template.New("list").Parse(` - - - - 宝くじ - - - - -`)) - -var purchasePageTmpl = template.Must(template.New("purchase_page").Parse(` - - - - Purchase {{.Name}} - - -

{{.Name}}

-

Prizes

- - - {{if .Remain}} -
- - - - -
- {{else}} - SOLD OUT - {{end}} - -`)) - -var resultTmpl = template.Must(template.New("result").Parse(` - - - - Result of {{.Name}} - - - {{range .Prizes}} - {{$winner := index $.Winners .ID}} - {{if $winner }} -

{{.Name}} (${{.Amount}})

- - {{else}} -

{{.Name}} (${{.Amount}})

- No winners - {{end}} - {{end}} - -`)) diff --git a/types.go b/types.go deleted file mode 100644 index 0f88802..0000000 --- a/types.go +++ /dev/null @@ -1,38 +0,0 @@ -package main - -import "time" - -type Lottery struct { - ID string `json:"id"` - Name string `json:"name"` - Price int64 `json:"price"` - Num int64 `json:"num"` - Prizes []*Prize `json:"prizes"` - StartAt time.Time `json:"start_at"` - DrawAt time.Time `json:"draw_at"` -} - -type Prize struct { - ID string `json:"id"` - Name string `json:"name` - Num int64 `json:"num"` - Amount int64 `json:"amount"` -} - -type Result struct { - Status DrawStatus `json:"status"` - Winners []*Winner `json:"winners"` -} - -type DrawStatus int64 - -const ( - DrawStatusNotDrawn DrawStatus = 0 - DrawStatusMidDrawn DrawStatus = 1 - DrawStatusDrawn DrawStatus = 2 -) - -type Winner struct { - PrizeID string `json:"prize_id"` - Numbers []string `json:"numbers"` -}