first commit

This commit is contained in:
2022-12-13 07:42:44 +08:00
commit 7ef2644e20
46 changed files with 7810 additions and 0 deletions

35
go.mod Normal file
View File

@@ -0,0 +1,35 @@
module ais
go 1.19
require (
github.com/gin-contrib/sessions v0.0.5
github.com/gin-gonic/gin v1.8.1
github.com/lib/pq v1.10.7
)
require (
github.com/brianvoe/gofakeit/v6 v6.19.0 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.0 // indirect
github.com/go-playground/universal-translator v0.18.0 // indirect
github.com/go-playground/validator/v10 v10.10.0 // indirect
github.com/goccy/go-json v0.9.7 // indirect
github.com/gorilla/context v1.1.1 // indirect
github.com/gorilla/securecookie v1.1.1 // indirect
github.com/gorilla/sessions v1.2.1 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.0.1 // indirect
github.com/stretchr/testify v1.7.5 // indirect
github.com/ugorji/go/codec v1.2.7 // indirect
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 // indirect
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 // indirect
golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f // indirect
golang.org/x/text v0.3.6 // indirect
google.golang.org/protobuf v1.28.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)

102
go.sum Normal file
View File

@@ -0,0 +1,102 @@
github.com/brianvoe/gofakeit/v6 v6.19.0 h1:g+yJ+meWVEsAmR+bV4mNM/eXI0N+0pZ3D+Mi+G5+YQo=
github.com/brianvoe/gofakeit/v6 v6.19.0/go.mod h1:Ow6qC71xtwm79anlwKRlWZW6zVq9D2XHE4QSSMP/rU8=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gin-contrib/sessions v0.0.5 h1:CATtfHmLMQrMNpJRgzjWXD7worTh7g7ritsQfmF+0jE=
github.com/gin-contrib/sessions v0.0.5/go.mod h1:vYAuaUPqie3WUSsft6HUlCjlwwoJQs97miaG2+7neKY=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8=
github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk=
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU=
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho=
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
github.com/go-playground/validator/v10 v10.10.0 h1:I7mrTYv78z8k8VXa/qJlOlEXn/nBh+BF8dHX5nt/dr0=
github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
github.com/goccy/go-json v0.9.7 h1:IcB+Aqpx/iMHu5Yooh7jEzJk1JZ7Pjtmys2ukPr7EeM=
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw=
github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.0.1 h1:8e3L2cCQzLFi2CR4g7vGFuFxX7Jl1kKX8gW+iV0GUKU=
github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.5 h1:s5PTfem8p8EbKQOctVV53k6jCJt3UX4IEJzwh+C324Q=
github.com/stretchr/testify v1.7.5/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f h1:8w7RhxzTVgUzw/AH/9mUV5q0vMgy40SQRursCcfmkCw=
golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

13
main.go Normal file
View File

@@ -0,0 +1,13 @@
package main
import (
"ais/pkg/api"
)
func main() {
router := api.NewAPI()
router.Run(":8888")
}

100
pkg/api/api.go Normal file
View File

@@ -0,0 +1,100 @@
package api
import (
"database/sql"
"log"
"os"
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/cookie"
"github.com/gin-gonic/gin"
_ "github.com/lib/pq"
)
var db *sql.DB
func init() {
var err error
db, err = sql.Open("postgres", os.Getenv("POSTGRES"))
if err != nil {
log.Fatal(err)
}
log.Println("Successfully connected to postgres database")
// install tables
if len(os.Args) >= 2 {
if os.Args[1] == "install" {
install()
os.Exit(0)
}
// drop tables
if os.Args[1] == "drop" {
drop()
os.Exit(0)
}
// reinstall
if os.Args[1] == "reinstall" {
drop()
install()
os.Exit(0)
}
// fake data
if os.Args[1] == "fake" {
fakeData()
os.Exit(0)
}
// unknown Args
log.Println("Unknown args", os.Args)
os.Exit(2)
}
}
func NewAPI() *gin.Engine {
router := gin.Default()
// session
store := cookie.NewStore([]byte("Miku saves the world!"))
router.Use(sessions.Sessions("ais", store))
// entry point html
router.GET("/", func(c *gin.Context) {
c.File("./web/public/index.html")
})
group := router.Group("/api")
// json error middleware
group.Use(func(c *gin.Context) {
c.Next()
if len(c.Errors) > 0 {
c.JSON(-1, gin.H{
"errors": c.Errors.Errors(),
"note": "gin api error handler",
})
}
})
group.POST("/login", handelLogin)
group.GET("/login", handelGetLoginSession)
group.POST("/register", handelRegister)
group.POST("/logout", handelLogout)
customer := group.Group("/customer")
customer.GET("/market", handleGetMarket)
customer.GET("/market/distance", handleGetMarketByDistance)
customer.GET("/market/:marketid", handleGetGoods)
customer.POST("/market/:marketid/:goodsid", handleBuy)
customer.GET("/purchase", handleGetPurchaseHistory)
customer.DELETE("/purchase/:purchaseid", handleDeletePurchaseHistory)
customer.GET("/purchase/report", handleCustomerReport)
supplier := group.Group("/supplier")
supplier.GET("/goods", handleGetGodsBySupplier)
supplier.POST("/goods", handleCreateGoods)
return router
}

38
pkg/api/drop.go Normal file
View File

@@ -0,0 +1,38 @@
package api
import (
"log"
"strings"
)
var dropSQLString = `
drop table purchase;
drop table tags_on_goods;
drop table goods;
drop table tag;
drop table market;
drop table users;
`
func drop() {
sqls := strings.Split(dropSQLString, "\n\n")
tx, err := db.Begin()
if err != nil {
log.Fatal(err)
}
for _, sql := range sqls {
log.Println("Dropting table with SQL", sql)
_, err = tx.Exec(sql)
if err != nil {
tx.Rollback()
log.Fatal(err)
}
}
tx.Commit()
log.Println("Successfully drop all tables")
}

22
pkg/api/encrypt.go Normal file
View File

@@ -0,0 +1,22 @@
package api
import (
"log"
"golang.org/x/crypto/bcrypt"
)
func EncryptPassword(password string) string {
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.MinCost)
if err != nil {
log.Println("Warning: Failed to hash password, fallback to plaintext password")
return password
}
return string(hash)
}
func ComparePassword(hashedPassword string, plainTextPassword string) error {
err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(plainTextPassword))
return err
}

88
pkg/api/fake.go Normal file
View File

@@ -0,0 +1,88 @@
package api
import (
"database/sql"
"fmt"
"log"
"math/rand"
"github.com/brianvoe/gofakeit/v6"
)
func fakeData() {
tx, err := db.Begin()
if err != nil {
log.Fatal(err)
}
fakeUsers(tx)
fakeSupplier(tx)
fakeMarket(tx)
tx.Commit()
}
func fakeUsers(tx *sql.Tx) {
for i := 0; i < 10; i++ {
username := gofakeit.Username()
password := gofakeit.Password(true, true, true, true, true, 1)
encryptedPassword := EncryptPassword(password)
_, err := tx.Exec(`insert into users (username, password) values ($1, $2)`,
username, encryptedPassword)
if err != nil {
tx.Rollback()
log.Fatal(err)
}
log.Println("fake users", username, password)
}
}
func fakeSupplier(tx *sql.Tx) {
for i := 0; i < 10; i++ {
username := gofakeit.Username()
password := EncryptPassword(gofakeit.Password(true, true, true, true, true, 1))
_, err := tx.Exec(`insert into users (username, password, role) values ($1, $2, 2)`,
username, password)
if err != nil {
tx.Rollback()
log.Fatal(err)
}
log.Println("fake supplier", username, password)
}
}
func fakeMarket(tx *sql.Tx) {
for i := 0; i < 10; i++ {
addr := gofakeit.Address()
name := addr.State
description := addr.Address
location := fmt.Sprintf("(%f, %f)", addr.Latitude, addr.Longitude)
row := tx.QueryRow(`insert into market (name, description, location) values ($1, $2, $3) returning id`,
name, description, location)
var mid int64
err := row.Scan(&mid)
if err != nil {
tx.Rollback()
log.Fatal(err)
}
log.Println("fake market", name, description, location)
fakeProduct(tx, int64(mid))
}
}
func fakeProduct(tx *sql.Tx, mid int64) {
for i := 0; i < 10; i++ {
name := gofakeit.BeerName()
description := gofakeit.BeerStyle()
quantity := rand.Intn(390)
price := gofakeit.Price(39, 390)
_, err := tx.Exec(`insert into goods (name, description, supplier_id, market_id, quantity, price) values ($1, $2, $3, $4, $5, $6)`,
name, description, 1, mid, quantity, price)
if err != nil {
tx.Rollback()
log.Fatal(err)
}
log.Println("fake goods", name, description, quantity, price)
}
}

1
pkg/api/handle_buy.go Normal file
View File

@@ -0,0 +1 @@
package api

169
pkg/api/handle_goods.go Normal file
View File

@@ -0,0 +1,169 @@
package api
import (
"log"
"strconv"
"time"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
)
type Goods struct {
Id int64 `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
SupplierId int64 `json:"supplier_id"`
MarketId int64 `json:"market_id"`
Quantity int64 `json:"quantity"`
Price string `json:"price"`
}
func handleGetGoods(c *gin.Context) {
marketId, err := strconv.ParseInt(c.Param("marketid"), 10, 64)
if err != nil {
c.AbortWithError(500, err)
return
}
log.Println("select", marketId)
rows, err := db.Query(`select id, name, description, quantity, price from goods where market_id = $1 order by name, description`, marketId)
if err != nil {
c.AbortWithError(500, err)
return
}
ret := make([]Goods, 0)
var g Goods
for rows.Next() {
err = rows.Scan(&g.Id, &g.Name, &g.Description, &g.Quantity, &g.Price)
if err != nil {
c.AbortWithError(500, err)
return
}
ret = append(ret, g)
}
c.JSON(200, gin.H{
"goods": ret,
})
}
func handleBuy(c *gin.Context) {
session := sessions.Default(c)
userId := session.Get("userid").(int64)
goodsId := c.Param("goodsid")
tx, err := db.Begin()
if err != nil {
c.AbortWithError(500, err)
return
}
_, err = tx.Exec(`insert into purchase (user_id, goods_id) values ($1, $2)`, userId, goodsId)
if err != nil {
tx.Rollback()
c.AbortWithError(500, err)
return
}
_, err = tx.Exec(`update goods set quantity = quantity - 1 where id = $1`, goodsId)
if err != nil {
tx.Rollback()
c.AbortWithError(500, err)
return
}
tx.Commit()
c.JSON(200, gin.H{})
}
func handleGetPurchaseHistory(c *gin.Context) {
session := sessions.Default(c)
userId := session.Get("userid").(int64)
rows, err := db.Query(`
select p.id, p.quantity, p.purchased_time, g.name, g.price
from purchase p
join goods g on p.goods_id = g.id
where p.user_id = $1
order by p.purchased_time desc
`, userId)
if err != nil {
c.AbortWithError(500, err)
return
}
type Ret struct {
Id int64 `json:"id"`
Quantity int64 `json:"quantity"`
PurchasedTime time.Time `json:"purchased_time"`
Goods Goods `json:"goods"`
}
ret := make([]Ret, 0)
for rows.Next() {
var i Ret
err = rows.Scan(&i.Id, &i.Quantity, &i.PurchasedTime, &i.Goods.Name, &i.Goods.Price)
if err != nil {
c.AbortWithError(500, err)
return
}
ret = append(ret, i)
}
c.JSON(200, ret)
}
func handleDeletePurchaseHistory(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("purchaseid"), 10, 64)
if err != nil {
c.AbortWithError(500, err)
return
}
_, err = db.Exec(`delete from purchase where id = $1`, id)
if err != nil {
c.AbortWithError(500, err)
return
}
c.JSON(200, gin.H{})
}
func handleGetGodsBySupplier(c *gin.Context) {
session := sessions.Default(c)
userId := session.Get("userid").(int64)
rows, err := db.Query(`select id, name, description, quantity, price from goods where supplier_id = $1 order by name, description`, userId)
if err != nil {
c.AbortWithError(500, err)
return
}
ret := make([]Goods, 0)
for rows.Next() {
var g Goods
err = rows.Scan(&g.Id, &g.Name, &g.Description, &g.Quantity, &g.Price)
if err != nil {
c.AbortWithError(500, err)
return
}
ret = append(ret, g)
}
c.JSON(200, ret)
}
func handleCreateGoods(c *gin.Context) {
session := sessions.Default(c)
userId := session.Get("userid").(int64)
g := &Goods{}
err := c.BindJSON(g)
if err != nil {
c.AbortWithError(500, err)
return
}
g.SupplierId = userId
_, err = db.Exec(`insert into goods (name, description, supplier_id, market_id, quantity, price) values ($1, $2, $3, $4, $5, $6)`,
g.Name, g.Description, g.SupplierId, g.MarketId, g.Quantity,g.Price)
if err != nil {
c.AbortWithError(500, err)
return
}
c.JSON(200, gin.H{})
}

60
pkg/api/handle_market.go Normal file
View File

@@ -0,0 +1,60 @@
package api
import "github.com/gin-gonic/gin"
type Market struct {
Id int64 `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Location string `json:"location"`
}
func handleGetMarket(c *gin.Context) {
markets := make([]Market, 0)
rows, err := db.Query(`select id, name, description, location from market`)
if err != nil {
c.AbortWithError(500, err)
return
}
for rows.Next() {
var market Market
err = rows.Scan(&market.Id, &market.Name, &market.Description, &market.Location)
if err != nil {
c.AbortWithError(500, err)
return
}
markets = append(markets, market)
}
c.JSON(200, gin.H{
"markets": markets,
})
}
func handleGetMarketByDistance(c *gin.Context) {
markets := make([]Market, 0)
point := c.Query("point")
rows, err := db.Query(`select id, name, description, location, location<->$1 as dist from market order by dist`, point)
if err != nil {
c.AbortWithError(500, err)
return
}
var x string
for rows.Next() {
var market Market
err = rows.Scan(&market.Id, &market.Name, &market.Description, &market.Location, &x)
if err != nil {
c.AbortWithError(500, err)
return
}
markets = append(markets, market)
}
c.JSON(200, gin.H{
"markets": markets,
})
}

28
pkg/api/handle_report.go Normal file
View File

@@ -0,0 +1,28 @@
package api
import (
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
)
func handleCustomerReport(c *gin.Context) {
session := sessions.Default(c)
userId := session.Get("userid").(int64)
row := db.QueryRow(`select sum(g.price)
from purchase p
join goods g on p.goods_id = g.id
where p.user_id = $1`,
userId)
var sumPrice string
err := row.Scan(&sumPrice)
if err != nil {
c.AbortWithError(500, err)
return
}
c.JSON(200, gin.H{
"sum": sumPrice,
})
}

100
pkg/api/handle_user.go Normal file
View File

@@ -0,0 +1,100 @@
package api
import (
"time"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
)
type User struct {
Id int64 `json:"id"`
Username string `json:"username"`
Password string `json:"password"`
Balance string `json:"balance"`
Location string `json:"location"`
Role int64 `json:"role"`
RegisterTime time.Time `json:"register_time"`
}
var SESSION_NAME = "ais"
func handelLogout(c *gin.Context) {
session := sessions.Default(c)
session.Clear()
session.Save()
c.JSON(200, gin.H{})
}
func handelGetLoginSession(c *gin.Context) {
session := sessions.Default(c)
userId := session.Get("userid")
if userId == nil {
c.JSON(200, gin.H{})
return
}
user := &User{}
row := db.QueryRow(`select id, username, balance, location, role from users where id=$1`, userId)
err := row.Scan(&user.Id, &user.Username, &user.Balance, &user.Location, &user.Role)
if err != nil {
c.AbortWithError(403, err)
return
}
c.JSON(200, user)
}
func handelLogin(c *gin.Context) {
user := &User{}
err := c.BindJSON(user)
if err != nil {
c.AbortWithError(500, err)
return
}
var encryptedPassowrd string
row := db.QueryRow(`select id, username, balance, location, role, password from users where username=$1`,
user.Username)
err = row.Scan(&user.Id, &user.Username, &user.Balance, &user.Location, &user.Role, &encryptedPassowrd)
if err != nil {
c.AbortWithError(403, err)
return
}
// validate password
err = ComparePassword(encryptedPassowrd, user.Password)
if err != nil {
c.AbortWithError(403, err)
return
}
// set session
session := sessions.Default(c)
session.Set("userid", user.Id)
session.Save()
c.JSON(200, user)
}
func handelRegister(c *gin.Context) {
user := &User{}
err := c.BindJSON(user)
if err != nil {
c.AbortWithError(401, err)
return
}
encryptedPassowrd := EncryptPassword(user.Password)
ret := db.QueryRow(`insert into users(username, password) values ($1, $2) returning id`,
user.Username, encryptedPassowrd)
err = ret.Scan(&user.Id)
if err != nil {
c.AbortWithError(401, err)
return
}
c.JSON(200, gin.H{})
}

75
pkg/api/install.go Normal file
View File

@@ -0,0 +1,75 @@
package api
import (
"log"
"strings"
)
var initSQLString string = `create table users(
id serial primary key,
username varchar(30) not null unique,
password varchar(64) not null,
balance money not null default 0,
role integer not null default 1,
location point not null default '(0, 0)',
registerd_time timestamp not null default now()
);
create table market (
id serial primary key,
name varchar(100) not null,
description text not null,
location point not null
)
create table tag (
id serial primary key,
name varchar(30) not null
);
create table goods (
id serial primary key,
name varchar(100) not null,
description text not null,
create_time timestamp not null default now(),
supplier_id integer not null references users(id),
market_id integer not null references market(id),
quantity numeric not null check (quantity >= 0),
price money not null,
data jsonb
);
create table tags_on_goods(
tag_id integer not null references tag(id),
goods_id integer not null references goods(id),
primary key (tag_id, goods_id)
);
create table purchase (
id serial primary key,
user_id integer not null references users(id),
goods_id integer not null references goods(id),
quantity numeric not null default 1,
purchased_time timestamp not null default now()
);
insert into users (username, password) values ('a', 'a');
`
func install() {
sqls := strings.Split(initSQLString, "\n\n")
tx, err := db.Begin()
if err != nil {
log.Fatal(err)
}
for _, sql := range sqls {
log.Println("Installing table with SQL", sql)
_, err = tx.Exec(sql)
if err != nil {
tx.Rollback()
log.Fatal(err)
}
}
tx.Commit()
log.Println("Successfully install all tables")
}

6
web/.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
/node_modules
/dist
/.parcel-cache
/public/ais.js
/build

6
web/Caddyfile Normal file
View File

@@ -0,0 +1,6 @@
:8001 {
route {
reverse_proxy /api/* 127.0.0.1:8888
reverse_proxy * 127.0.0.1:1234
}
}

5979
web/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

24
web/package.json Normal file
View File

@@ -0,0 +1,24 @@
{
"name": "web",
"version": "1.0.0",
"description": "",
"scripts": {
"build": "esbuild src/index.jsx --bundle --minify --outfile=public/ais.js --loader:.webp=dataurl --analyze",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@emotion/react": "^11.10.5",
"@emotion/styled": "^11.10.5",
"@mui/icons-material": "^5.10.16",
"@mui/material": "^5.10.17",
"esbuild": "^0.16.4",
"parcel": "^2.8.1",
"process": "^0.11.10",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.4.5"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

13
web/public/index.html Normal file
View File

@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html>
<head>
<title>Title</title>
</head>
<body>
<div id="root">
<h1 style="text-align: center;">Loading Sam's AIS...</h1>
</div>
<script type="module" src="/ais.js"></script>
</body>
</html>

54
web/src/common.js Normal file
View File

@@ -0,0 +1,54 @@
const API_ENDPOINT = "/api";
const get = async (url) => {
const resp = await fetch(`${API_ENDPOINT}${url}`);
const json = await resp.json();
return json;
};
const post = async (url, data) => {
return await _post(url, data, "POST");
};
const del = async (url, data) => {
return await _post(url, data, "DELETE");
};
const put = async (url, data) => {
return await _post(url, data, "PUT");
};
const _post = async (url, data, method) => {
const resp = await fetch(`${API_ENDPOINT}${url}`, {
method,
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify(data),
});
const json = await resp.json();
if (json.errors) {
alert(json.errors);
throw json.errors;
}
return json;
};
const fixZero = (n) => {
if (n < 10) return `0${n}`;
return n;
};
const time = (s) => {
const t = new Date(s);
const year = t.getFullYear();
const month = fixZero(t.getMonth())
const day = fixZero(t.getDay());
const hours = fixZero(t.getHours());
const minutes = fixZero(t.getMinutes());
const second = fixZero(t.getSeconds());
return `${year}-${month}-${day} ${hours}:${minutes}:${second}`;
};
export { get, post, del, put, time };

View File

@@ -0,0 +1,38 @@
import {
TableContainer,
Paper,
Table,
TableHead,
TableRow,
TableCell,
TableBody,
} from "@mui/material";
import React from "react";
const AutoTable = ({ data }) => {
const columns = Object.keys(data[0] || []);
return (
<TableContainer component={Paper} sx={{ m: 1 }}>
<Table>
<TableHead>
<TableRow>
{columns.map((col) => (
<TableCell key={col}>{col}</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{data.map((row) => (
<TableRow key={row.id}>
{columns.map((col) => (
<TableCell key={col}>{row[col]}</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
);
};
export default AutoTable;

View File

@@ -0,0 +1,22 @@
import React from "react";
import {
Card,
CardActionArea,
CardContent,
CardMedia,
Typography,
} from "@mui/material";
const ImageCard = ({ title, description, image, onClick }) => (
<Card sx={{ m: 1 }}>
<CardActionArea onClick={onClick}>
<CardMedia component="img" height="239" image={image} alt="Card Image" />
<CardContent>
<Typography variant="h5">{title}</Typography>
<Typography>{description}</Typography>
</CardContent>
</CardActionArea>
</Card>
);
export default ImageCard;

View File

@@ -0,0 +1,36 @@
import { Button } from "@mui/material";
import React from "react";
import { useNavigate } from "react-router-dom";
import userContext from "../context/userContext";
import { get } from "../common";
const LoginButton = () => {
const { user, setUser } = React.useContext(userContext);
const navigator = useNavigate();
const handleClick = () => {
if (user.role === 1) {
navigator("/customer");
} else if (user.role === 2) {
navigator("/supplier");
} else {
navigator('/login');
}
};
React.useEffect(() => {
get("/login")
.then((resp) => {
setUser(resp);
})
.catch((e) => console.log(e));
}, []);
return (
<Button variant="contained" color="secondary" onClick={handleClick}>
{user.username || "Login"}
</Button>
);
};
export default LoginButton;

View File

@@ -0,0 +1,31 @@
import React from "react";
import { Card, CardActionArea, CardContent, Typography } from "@mui/material";
import { useNavigate } from "react-router-dom";
import LogoutIcon from "@mui/icons-material/Logout";
import { post } from "../common";
import userContext from "../context/userContext";
const LogoutCard = () => {
const nevigator = useNavigate();
const { setUser } = React.useContext(userContext);
const handelLogout = async () => {
await post("/logout", {});
setUser({});
nevigator("/login");
};
return (
<Card sx={{ m: 1 }}>
<CardActionArea onClick={handelLogout}>
<LogoutIcon />
<CardContent>
<Typography variant="h5">Logout</Typography>
<Typography>Clear session</Typography>
</CardContent>
</CardActionArea>
</Card>
);
};
export default LogoutCard;

View File

@@ -0,0 +1,23 @@
import React from "react";
import { InputLabel, FormControl, Select, MenuItem } from "@mui/material";
const SelectRole = ({ inputRole, setInputRole }) => {
return (
<FormControl fullWidth>
<InputLabel id="demo-simple-select-label">Role</InputLabel>
<Select
labelId="demo-simple-select-label"
id="demo-simple-select"
value={inputRole}
label="Role"
onChange={(event) => setInputRole(event.target.value)}
>
<MenuItem value={1}>Customer</MenuItem>
<MenuItem value={2}>Supplier</MenuItem>
<MenuItem value={3}>Admin</MenuItem>
</Select>
</FormControl>
);
};
export default SelectRole;

View File

@@ -0,0 +1,3 @@
import {createContext} from 'react';
const userContext = createContext({});
export default userContext;

12
web/src/index.html Normal file
View File

@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<title>Title</title>
</head>
<body>
<div id="root">
</div>
<script type="module" src="./index.jsx"></script>
</body>
</html>

44
web/src/index.jsx Normal file
View File

@@ -0,0 +1,44 @@
import React from "react";
import ReactDOM from "react-dom/client";
import { createHashRouter as Router, RouterProvider } from "react-router-dom";
import LoginPage from "./pages/LoginPage";
import HomePage from "./pages/HomePage";
import RegisterPage from "./pages/RegisterPage";
import CustomerPage from './pages/CustomerPage';
import CustomerMarketPage from './pages/CustomerMarketPage';
import userContext from "./context/userContext";
import CustomerMarketDeatilPage from "./pages/CustomerMarketDetailPage";
import CustomerPurchasedPage from "./pages/CustomerPurchasedPage";
import SupplierPage from './pages/SupplierPage';
import SupplierGoods from './pages/SupplierGoods';
import SupplierAddGoods from './pages/SupplierAddGoods';
const router = Router([
{ path: "/", element: <HomePage /> },
{ path: "/ping", element: <div>pong</div> },
{ path: "/login", element: <LoginPage /> },
{ path: "/register", element: <RegisterPage /> },
{ path: "/customer", element: <CustomerPage /> },
{ path: "/customer/market", element: <CustomerMarketPage /> },
{ path: "/customer/market/:marketid", element: <CustomerMarketDeatilPage /> },
{ path: "/customer/purchased", element: <CustomerPurchasedPage /> },
{ path: "/supplier", element: <SupplierPage /> },
{ path: "/supplier/goods", element: <SupplierGoods /> },
{ path: "/supplier/goods/new", element: <SupplierAddGoods /> },
]);
const App = () => {
const [user, setUser] = React.useState({});
return (
<userContext.Provider value={{ user, setUser }}>
<RouterProvider router={router}></RouterProvider>
</userContext.Provider>
);
};
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);

View File

@@ -0,0 +1,35 @@
import { AppBar, Box, CssBaseline, Toolbar, Typography } from "@mui/material";
import React from "react";
import { Link } from "react-router-dom";
import LoginButton from "../components/LoginButton";
const BasicLayout = ({ children }) => {
return (
<>
<CssBaseline />
<header>
<AppBar position="relative">
<Toolbar>
<Typography variant="h6" sx={{ flexGrow: 1 }}>
<Link style={{ textDecoration: "none", color: "unset" }} to="/">
<img
style={{
height: "1.39rem",
}}
src="data:image/svg+xml;charset=utf-8,%3Csvg width='214' height='37' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M196.157.298l16.449 16.624c.75.758.75 1.987.001 2.745l-16.445 16.63-2.84-2.87a1.987 1.987 0 010-2.79l12.204-12.341-12.1-12.229a2.142 2.142 0 010-3.007l2.731-2.762zm-5.293 2.75l-2.719-2.75-16.362 16.546a2.075 2.075 0 000 2.911l16.367 16.542 2.716-2.747c.83-.84.83-2.2 0-3.038L178.781 18.3l12.084-12.22a2.161 2.161 0 00-.001-3.033zm-30.713 17.22c0 3.678-2.362 6.457-5.807 6.457-3.407 0-5.769-2.78-5.769-6.38 0-3.64 2.362-6.496 5.769-6.496 3.445 0 5.807 2.778 5.807 6.418zm-11.653 5.791c1.317 2.857 3.833 4.344 6.814 4.344 4.994 0 8.981-3.874 8.981-10.097 0-6.262-3.987-10.136-8.981-10.136-2.981 0-5.497 1.487-6.814 4.344V2.656l-3.871.783V30.09h3.871v-4.03zm-32.861-2.387c0 4.736 2.207 6.849 6.195 6.692.348 0 .735-.039 1.084-.117v-3.835h-1.007c-1.393 0-2.4-.861-2.4-2.74V2.656l-3.872.783v20.233zM69.951 4.847h4.18l-2.438 7.71H68.48l1.471-7.71zM51.521 30.05h3.87V19.523c0-3.209 1.975-5.713 4.801-5.713 2.594 0 3.872 1.878 3.872 4.343v11.898h3.87V18.193c0-4.97-2.98-8.062-7.045-8.062-2.594 0-4.762 1.135-6.388 3.718-1.161-2.349-3.29-3.718-5.884-3.718-2.478 0-4.607 1.37-5.768 3.835v-3.483h-3.872V30.05h3.872V19.523c0-3.209 1.974-5.713 4.8-5.713 2.594 0 3.871 1.878 3.871 4.343v11.898zM0 28.29l1.742-3.562c1.278.94 3.562 1.996 5.304 1.996 2.168 0 3.02-1.056 3.02-2.192.038-1.33-1.046-1.839-3.33-2.582-3.832-1.174-6-2.544-6-6.145 0-3.287 2.477-5.635 6.968-5.635 2.323 0 4.452.743 6.156 1.957l-1.743 3.248c-1.006-.626-2.71-1.566-4.607-1.566-1.664 0-2.67.666-2.67 1.957 0 1.057.735 1.761 3.174 2.505 4.142 1.252 6.194 2.818 6.194 6.105 0 3.718-2.981 5.988-7.124 5.988-2.826 0-5.535-.94-7.084-2.074zm25.628-14.44c-3.445 0-5.807 2.778-5.807 6.457 0 3.64 2.362 6.418 5.807 6.418 3.407 0 5.769-2.857 5.769-6.496 0-3.601-2.362-6.38-5.769-6.38zm5.844 12.21c-1.316 2.857-3.832 4.344-6.813 4.344-4.994 0-8.982-3.875-8.982-10.136 0-6.223 3.988-10.098 8.982-10.098 2.98 0 5.497 1.488 6.813 4.345v-4.031h3.872v19.568h-3.872V26.06zm43.01-1.332L72.74 28.29c1.549 1.135 4.259 2.074 7.085 2.074 4.142 0 7.123-2.27 7.123-5.988 0-3.287-2.052-4.853-6.194-6.105-2.439-.744-3.175-1.448-3.175-2.505 0-1.291 1.007-1.957 2.672-1.957 1.897 0 3.6.94 4.607 1.566l1.742-3.248a10.59 10.59 0 00-6.156-1.957c-4.49 0-6.968 2.348-6.968 5.635 0 3.6 2.168 4.97 6 6.145 2.284.743 3.368 1.252 3.33 2.582 0 1.136-.852 2.192-3.02 2.192-1.742 0-4.026-1.056-5.304-1.996zm21.991-4.46c0-6.223 4.259-10.098 10.182-10.098 2.787 0 5.265.9 7.162 2.818l-2.362 2.935c-1.471-1.487-3.213-2.074-4.762-2.074-3.561 0-6.078 2.152-6.078 6.38 0 4.226 2.517 6.456 6.078 6.456 1.549 0 3.291-.626 4.801-2.191l2.361 2.896c-1.897 1.917-4.336 2.974-7.2 2.974-5.885 0-10.182-3.874-10.182-10.097zm28.077 1.721V10.483h3.871v11.82c0 2.582 1.626 4.343 4.336 4.343 2.71 0 4.336-1.76 4.336-4.344V10.483h3.871V21.99c0 5.362-3.291 8.375-8.207 8.375-4.917 0-8.207-3.013-8.207-8.375z' fill='%23fff'/%3E%3C/svg%3E"
/>
</Link>
</Typography>
<LoginButton />
</Toolbar>
</AppBar>
</header>
<main>
<Box sx={{ m: 3 }}>{children}</Box>
</main>
<footer></footer>
</>
);
};
export default BasicLayout;

View File

@@ -0,0 +1,13 @@
import React from "react";
import BasicLayout from "./BasicLayout";
import { Paper, Container } from "@mui/material";
const SmallPaperLayout = ({ children }) => (
<BasicLayout>
<Container maxWidth="sm">
<Paper sx={{ p: 3}}>{children}</Paper>
</Container>
</BasicLayout>
);
export default SmallPaperLayout;

View File

@@ -0,0 +1,56 @@
import { Button, Typography } from "@mui/material";
import React from "react";
import BasicLayout from "../layouts/BasicLayout";
import userContext from "../context/userContext";
import { useParams } from "react-router-dom";
import AutoTable from "../components/AutoTable";
import { get, post } from "../common";
const CustomerMarketDeatilPage = () => {
const { user } = React.useContext(userContext);
const [data, setData] = React.useState([]);
const params = useParams();
const buyFunc = async (goodsid) => {
await post(`/customer/market/${params.marketid}/${goodsid}`, {});
refresh();
};
const refresh = () => {
get(`/customer/market/${params.marketid}`).then((resp) => {
let goods = resp.goods.map((g) => {
delete g.supplier_id;
delete g.market_id;
g.action = (
<Button
variant="contained"
color="success"
onClick={() => {
buyFunc(g.id);
}}
>
Buy!
</Button>
);
return g;
});
setData(goods);
});
};
React.useEffect(() => {
refresh();
}, []);
return (
<BasicLayout>
<Typography variant="h4">
Hi {user.username}, you are in market {params.marketid}
</Typography>
<hr />
<AutoTable data={data} />
</BasicLayout>
);
};
export default CustomerMarketDeatilPage;

View File

@@ -0,0 +1,51 @@
import { TextField, Typography, Box, Button } from "@mui/material";
import React from "react";
import BasicLayout from "../layouts/BasicLayout";
import AutoTable from "../components/AutoTable";
import { Link } from "react-router-dom";
import { get } from "../common";
const CustomerMarketPage = () => {
const [inputPoint, setInputPoint] = React.useState("");
const [data, setData] = React.useState([]);
React.useEffect(() => {
get("/customer/market").then((resp) => {
const d = resp.markets.map((m) => {
m.action = <Link to={`${m.id}`}>Enter</Link>;
return m;
});
setData(d);
});
}, []);
const handleSearch = () => {
get(`/customer/market/distance?point=${inputPoint}`).then((resp) => {
const d = resp.markets.map((m) => {
m.action = <Link to={`${m.id}`}>Enter</Link>;
return m;
});
setData(d);
});
};
return (
<BasicLayout>
<Typography variant="h4">Find your neaset market</Typography>
<hr />
<Box style={{ display: "flex" }}>
<TextField
value={inputPoint}
onChange={(event) => setInputPoint(event.target.value)}
label="Location"
/>
<Button variant="contained" color="secondary" onClick={handleSearch}>
Search
</Button>
</Box>
<AutoTable data={data} />
</BasicLayout>
);
};
export default CustomerMarketPage;

View File

@@ -0,0 +1,40 @@
import React from "react";
import { Link } from "react-router-dom";
import userContext from "../context/userContext";
import BasicLayout from "../layouts/BasicLayout";
import ImageCard from "../components/ImageCard";
import LogoutCard from "../components/LogoutCard";
// images
import locationImage from "../../public/images/location.webp";
import purchasedImage from "../../public/images/purchased.webp";
import { Box, Typography } from "@mui/material";
const CustomerPage = () => {
const { user } = React.useContext(userContext);
return (
<BasicLayout>
<Typography variant="h4">Hello, customer {user.username}</Typography>
<hr />
<Box style={{ display: "flex", flexWrap: "wrap" }}>
<Link to="/customer/market">
<ImageCard
title="Nearby Market"
description="Find your neaest market"
image={locationImage}
/>
</Link>
<Link to="/customer/purchased">
<ImageCard
title="Purchased"
description="See what you have purchased"
image={purchasedImage}
/>
</Link>
<LogoutCard />
</Box>
</BasicLayout>
);
};
export default CustomerPage;

View File

@@ -0,0 +1,75 @@
import { Button, Typography } from "@mui/material";
import React from "react";
import BasicLayout from "../layouts/BasicLayout";
import userContext from "../context/userContext";
import AutoTable from "../components/AutoTable";
import { get, del, time } from "../common";
const CustomerPurchasedPage = () => {
const { user } = React.useContext(userContext);
const [data, setData] = React.useState([]);
const handleDelete = (id) => {
del(`/customer/purchase/${id}`).then(() => {
refresh();
});
};
const refresh = () => {
get(`/customer/purchase`).then((resp) => {
// flatten
const result = resp.map((r) => {
const { id, quantity, purchased_time } = r;
const pt = time(purchased_time);
const { name, price } = r.goods;
const deleteButton = (
<Button
variant="contained"
color="error"
onClick={() => {
handleDelete(id);
}}
>
Delete
</Button>
);
return {
id,
quantity,
pt,
name,
price,
action: deleteButton,
};
});
setData(result);
});
};
React.useEffect(() => {
refresh();
}, []);
return (
<BasicLayout>
<Typography variant="h4">
Hi {user.username}, here is what you have purchased
</Typography>
<Button
variant="contained"
color="success"
onClick={() => {
get(`/customer/purchase/report`).then((data) => {
alert(`$The sum of all your perchase is ${data.sum}`);
});
}}
>
Summary Report
</Button>
<hr />
<AutoTable data={data} />
</BasicLayout>
);
};
export default CustomerPurchasedPage;

View File

@@ -0,0 +1,57 @@
import { Box, Typography } from "@mui/material";
import React from "react";
import BasicLayout from "../layouts/BasicLayout";
import ImageCard from "../components/ImageCard";
import userContext from "../context/userContext";
// images
import CustomerImage from "../../public/images/customer.webp";
import SupplierImage from "../../public/images/supplier.webp";
import { useNavigate } from "react-router-dom";
const HomePage = () => {
const { user } = React.useContext(userContext);
const navigator = useNavigate();
return (
<BasicLayout>
<Box sx={{ m: 3 }}>
<Typography variant="h4">{"Welcome to sam's club"}</Typography>
<Typography>Select a role to continue</Typography>
<hr />
<Box
style={{
display: "flex",
flexWrap: "wrap",
}}
>
<ImageCard
title="Customer module"
description="Find a neaest market and purchase goods"
image={CustomerImage}
onClick={() => {
if (user.role !== 1) {
alert("You are not allow to use this module");
} else {
navigator("/customer");
}
}}
/>
<ImageCard
title="Supplier"
description="Manage the supply chain system and goods."
image={SupplierImage}
onClick={() => {
if (user.role !== 2) {
alert("You are not allow to use this module");
} else {
navigator("/supplier");
}
}}
/>
</Box>
</Box>
</BasicLayout>
);
};
export default HomePage;

View File

@@ -0,0 +1,80 @@
import { Button, Stack, TextField, Typography } from "@mui/material";
import React from "react";
import { Link, useNavigate } from "react-router-dom";
import SmallPaperLayout from "../layouts/SmallPaperLayout";
import SelectRole from "../components/SelectRole";
import userContext from "../context/userContext";
import { post } from "../common";
const LoginPage = () => {
const { setUser } = React.useContext(userContext);
const [inputUsername, setInputUsername] = React.useState("");
const [inputPassword, setInputPassword] = React.useState("");
const [inputRole, setInputRole] = React.useState(0);
const validateInput = () => {
if (!inputUsername) {
alert("Username can't be empty");
return;
}
if (inputUsername.length >= 30) {
alert("Username too long");
return;
}
if (!inputPassword) {
alert("Password can't be empty");
return;
}
if (!inputRole) {
alert("Role can't be empty");
return;
}
// do login
loginFunc();
};
const navigator = useNavigate();
const loginFunc = async () => {
const url = '/login';
const resp = await post(url, {
username: inputUsername,
password: inputPassword,
});
setUser(resp)
const nextURL = inputRole === 1 ? "/customer" : "/supplier";
navigator(nextURL);
};
return (
<SmallPaperLayout>
<Typography variant="h4" sx={{ pb: 2 }}>
Login to Sam AIS
</Typography>
<Stack spacing={3}>
<TextField
label="User name"
value={inputUsername}
onChange={(event) => setInputUsername(event.target.value)}
/>
<TextField
label="Password"
value={inputPassword}
onChange={(event) => setInputPassword(event.target.value)}
type="password"
/>
<SelectRole inputRole={inputRole} setInputRole={setInputRole} />
<Button variant="contained" color="success" onClick={validateInput}>
Login
</Button>
<Typography>
Click <Link to="/register">here</Link> to register a new account
</Typography>
</Stack>
</SmallPaperLayout>
);
};
export default LoginPage;

View File

@@ -0,0 +1,16 @@
import React from "react";
import SmallPaperLayout from '../layouts/SmallPaperLayout';
import userContext from "../context/userContext";
import { Box, Typography } from "@mui/material";
const ProfilePages = () => {
const { user, setUser } = React.useContext(userContext);
return (
<SmallPaperLayout>
<Typography variant="h3">Hello, {user.username}</Typography>
<hr />
</SmallPaperLayout>
);
};
export default ProfilePages;

View File

@@ -0,0 +1,122 @@
import {
Button,
Checkbox,
FormControlLabel,
FormGroup,
Stack,
TextField,
Typography,
} from "@mui/material";
import React from "react";
import { useNavigate, Link } from "react-router-dom";
import SmallPaperLayout from "../layouts/SmallPaperLayout";
import SelectRole from "../components/SelectRole";
import { post } from "../common";
const RegisterPage = () => {
const [inputUsername, setInputUsername] = React.useState("");
const [inputPassword, setInputPassword] = React.useState("");
const [repeatInputPassword, setRepeatInputPassword] = React.useState("");
const [inputRole, setInputRole] = React.useState(0);
const validateInput = () => {
if (!inputUsername) {
alert("Username can't be empty");
return;
}
if (inputUsername.length >= 30) {
alert("Username too long");
return;
}
if (!inputPassword) {
alert("Password can't be empty");
return;
}
if (inputPassword !== repeatInputPassword) {
alert("Password not matched!");
return;
}
if (!inputRole) {
alert("Role can't be empty");
return;
}
// do register
registerFunc();
};
const navigator = useNavigate();
const registerFunc = () => {
const url = "/register";
post(url, {
username: inputUsername,
password: inputPassword,
role: inputRole,
}).then(() => {
alert("Successfully registered, please go to login page");
navigator("/login");
});
};
const validatePassword = (p) => {
if (p.length === 0) {
return "";
}
if (p.length < 8) {
return "password should be at least 8 characters";
}
return "";
};
return (
<SmallPaperLayout>
<Typography variant="h4" sx={{ pb: 2 }}>
Register
</Typography>
<Stack spacing={3}>
<TextField
label="User name"
value={inputUsername}
onChange={(event) => setInputUsername(event.target.value)}
error={inputUsername.length >= 30}
helperText={inputUsername.length >= 30 && "User name too long !"}
/>
<TextField
label="Password"
value={inputPassword}
onChange={(event) => setInputPassword(event.target.value)}
type="password"
error={validatePassword(inputPassword)}
helperText={validatePassword(inputPassword)}
/>
<TextField
label="Repeat password"
value={repeatInputPassword}
onChange={(event) => setRepeatInputPassword(event.target.value)}
type="password"
error={inputPassword !== repeatInputPassword}
helperText={
inputPassword !== repeatInputPassword && "Password not matched"
}
/>
<SelectRole inputRole={inputRole} setInputRole={setInputRole} />
<FormGroup>
<FormControlLabel
control={<Checkbox />}
label={
<Typography>
I agree the{" "}
<Link to="/eula">end-user license agreement (EULA)</Link>
</Typography>
}
/>
</FormGroup>
<Button variant="contained" color="warning" onClick={validateInput}>
Register
</Button>
</Stack>
</SmallPaperLayout>
);
};
export default RegisterPage;

View File

@@ -0,0 +1,67 @@
import { Button, Stack, TextField, Typography } from "@mui/material";
import React from "react";
import SmallPaperLayout from "../layouts/SmallPaperLayout";
import { post } from "../common";
const SupplierAddGoods = () => {
const [inputName, setInputName] = React.useState("");
const [inputDescription, setInputDescription] = React.useState("");
const [inputMarketId, setInputMarketId] = React.useState(0);
const [inputQuantity, setInputQuantity] = React.useState(0);
const [inputPrice, setInputPrice] = React.useState(0);
return (
<SmallPaperLayout>
<Typography variant="h4">Add New Goods</Typography>
<hr />
<Stack spacing={2}>
<TextField
value={inputName}
onChange={(event) => setInputName(event.target.value)}
label="Product name"
/>
<TextField
value={inputDescription}
onChange={(event) => setInputDescription(event.target.value)}
label="Product description"
/>
<TextField
value={inputMarketId}
onChange={(event) => setInputMarketId(event.target.value)}
label="Market ID"
type="number"
/>
<TextField
value={inputQuantity}
onChange={(event) => setInputQuantity(event.target.value)}
label="Product quantity"
type="number"
/>
<TextField
value={inputPrice}
onChange={(event) => setInputPrice(event.target.value)}
label="Product price"
type="number"
/>
<Button
variant="contained"
color="warning"
onClick={() => {
post(`/supplier/goods`, {
name: inputName,
description: inputDescription,
market_id: parseInt(inputMarketId),
quantity: parseInt(inputQuantity),
price: inputPrice,
}).then(() => {
alert("Successfully add new goods");
});
}}
>
Add
</Button>
</Stack>
</SmallPaperLayout>
);
};
export default SupplierAddGoods;

View File

@@ -0,0 +1,40 @@
import { Button, Typography } from "@mui/material";
import React from "react";
import BasicLayout from "../layouts/BasicLayout";
import { get } from "../common";
import AutoTable from "../components/AutoTable";
import { Link } from "react-router-dom";
const SupplierGoods = () => {
const [data, setData] = React.useState([]);
const refresh = () => {
get("/supplier/goods").then((resp) => {
// delete unused data
setData(
resp.map((r) => {
delete r.supplier_id;
return r;
})
);
});
};
React.useEffect(() => {
refresh();
}, []);
return (
<BasicLayout>
<Typography variant="h4">Goods management</Typography>
<Typography>The list of your goods</Typography>
<Link to="/supplier/goods/new">
<Button variant="contained" color="info">
New Goods
</Button>
</Link>
<hr />
<AutoTable data={data} />
</BasicLayout>
);
};
export default SupplierGoods;

View File

@@ -0,0 +1,36 @@
import { Box, Typography } from "@mui/material";
import React from "react";
import ImageCard from "../components/ImageCard";
import BasicLayout from "../layouts/BasicLayout";
import supplierGoodsImage from "../../public/images/supplierGoods.webp";
import meetingImage from "../../public/images/meeting.webp";
import userContext from "../context/userContext";
import { Link } from "react-router-dom";
const SupplierPage = () => {
const { user } = React.useContext(userContext);
return (
<BasicLayout>
<Typography variant="h4">Welcome, supplier {user.username}</Typography>
<hr />
<Box style={{ display: "flex", flexWrap: "wrap" }}>
<Link to="/supplier/goods">
<ImageCard
title="Goods Management"
description="Create, View, Update, and Delete your goods"
image={supplierGoodsImage}
/>
</Link>
<Link to="/supplier/report">
<ImageCard
title="Sales Report"
description="Generate your fanaical report"
image={meetingImage}
/>
</Link>
</Box>
</BasicLayout>
);
};
export default SupplierPage;