From 861e35602aefbec22a2ed542f4d0042b1e94dbe7 Mon Sep 17 00:00:00 2001 From: mina_yiban Date: Fri, 21 Nov 2025 16:03:52 +0800 Subject: [PATCH] =?UTF-8?q?test.=E6=B8=AC=E8=A9=A6=E6=AA=94=E6=A1=88?= =?UTF-8?q?=E9=83=A8=E5=B1=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 40 +++++++ backend/data/coffee_mall.db | Bin 0 -> 45056 bytes backend/go.mod | 57 +++++++++ backend/go.sum | 120 +++++++++++++++++++ backend/internal/database/database.go | 98 +++++++++++++++ backend/internal/handlers/auth_handler.go | 81 +++++++++++++ backend/internal/handlers/mall_handler.go | 52 ++++++++ backend/internal/handlers/member_handler.go | 64 ++++++++++ backend/internal/handlers/order_handler.go | 71 +++++++++++ backend/internal/handlers/points_handler.go | 48 ++++++++ backend/internal/handlers/product_handler.go | 78 ++++++++++++ backend/internal/handlers/ticket_handler.go | 70 +++++++++++ backend/internal/middleware/auth.go | 34 ++++++ backend/internal/middleware/idle.go | 47 ++++++++ backend/internal/models/models.go | 67 +++++++++++ backend/internal/utils/jwt.go | 51 ++++++++ backend/internal/utils/response.go | 17 +++ backend/main.go | 87 ++++++++++++++ frontend | 1 + 19 files changed, 1083 insertions(+) create mode 100644 README.md create mode 100644 backend/data/coffee_mall.db create mode 100644 backend/go.mod create mode 100644 backend/go.sum create mode 100644 backend/internal/database/database.go create mode 100644 backend/internal/handlers/auth_handler.go create mode 100644 backend/internal/handlers/mall_handler.go create mode 100644 backend/internal/handlers/member_handler.go create mode 100644 backend/internal/handlers/order_handler.go create mode 100644 backend/internal/handlers/points_handler.go create mode 100644 backend/internal/handlers/product_handler.go create mode 100644 backend/internal/handlers/ticket_handler.go create mode 100644 backend/internal/middleware/auth.go create mode 100644 backend/internal/middleware/idle.go create mode 100644 backend/internal/models/models.go create mode 100644 backend/internal/utils/jwt.go create mode 100644 backend/internal/utils/response.go create mode 100644 backend/main.go create mode 160000 frontend diff --git a/README.md b/README.md new file mode 100644 index 0000000..7e4a819 --- /dev/null +++ b/README.md @@ -0,0 +1,40 @@ +# 商用咖啡机商城演示系统 + +## 功能概览 +- 前端:React + Ant Design + React Router,包含商城首页、商品详情、支付演示、登录/注册以及商家后台。 +- 后端:Go Gin + GORM + SQLite,提供 JWT 登录、注册、商城数据及后台 CRUD 接口。 +- 商家后台:商品、订单、会员、积分、工单模块,支持增删改查与 1 分钟无操作自动登出。 +- 登录演示账号:`demo / demo123`。 + +## 快速开始 +### 启动后端 +```bash +cd backend +go run main.go +``` +默认监听 `http://localhost:8080`,首次启动自动在 `backend/data/coffee_mall.db` 生成 SQLite 数据库与种子数据。 + +### 启动前端 +```bash +cd frontend +npm install +npm start +``` +前端默认运行在 `http://localhost:3000`。构建产物可通过 `npm run build` 生成。 + +## 接口说明 +- 公共接口:`/api/home`、`/api/products`、`/api/products/:id`、`/api/orders`、`/api/pay/demo`。 +- 认证接口:`/api/auth/login`、`/api/auth/register`(JWT)。 +- 后台接口需附带 `Authorization: Bearer `:`/api/admin/products|orders|members|points|tickets`。 +- 1 分钟无请求自动判定登录超时,返回 401 并提示前端跳转商城首页。 + +## 目录结构 +``` +backend/ # Go Gin 服务 +frontend/ # React + Ant Design 前端 +README.md # 使用说明 +``` + +## 其他 +- 数据库存储在 `backend/data/coffee_mall.db`,如需重置可删除该文件后重新启动后端。 +- 可根据需要扩展真实支付、权限角色、图表等高级特性。 diff --git a/backend/data/coffee_mall.db b/backend/data/coffee_mall.db new file mode 100644 index 0000000000000000000000000000000000000000..2333b14fb1e6eae5b42566a92c05a783ffe332e9 GIT binary patch literal 45056 zcmeI*Pi))P9S3m!aU#o3%&u9WdRZ0%&X5RkWl=Ik$-|0xj-0u%CD(sCyBCp>Xj_dW z$|RM#MuA37-DX)+FPYPJf6!rDF&o>V?WG9PWxMP)tk`kDj*DcchaQJvr#+FBELjQ` zcROV6mw-j;@saPn4}YY46xzV(#@^hM=RZKoeJweelb(ZHTih7d#b&|j8AacNa@8cmRLp`= zeV5nMnw~aauD(y|s$#0Cgks8kiY%GwtlBFV@|ESrRPRcU+vO1i`s$+9jJ!rFmoRlD zXDG=^fktymM`PofGwh+ASv7l6)f06py>fCv$<33-J50SYX^pYd^p=eqipM6WCI&}h zQ@lKPDbZY~gk6%%5635>Bj;ll`e}Kmd@nx{9ga>!V?)tN-bpuP>wtN^7u#K)zGHOJ zmCmKqrF3e^*`1M4vr0N+@5OLCFw?P-=cc3lNbFp6h9^gSS8kb)$LdS4b|HIFKD^i8Z)ix)gZDANr=@BLreJ9QDq!-dA`+6 z7f;l@G(Dy2M#7pUa)ruKx075CMQgE%O=qtl$EJR7vUg?pfXma}O}{p80ou9j*Mp6G z&JG^^bZjWdflC$)Q_GUzt*6cFTB?vV8v}Z=pybF2O-|36rIOVOIdXQA(9ZPAjc4MJ!S1Z6MSJnZHkar4ar){h`Y*F__WeUcvg2Id&ilBo-=3YS*` zrAEDEOK(x2T^^5zUKzCZX%%U`Fb&&Vbs#74BY*JJ>Ri1%D2DM8iL1ndVk}g7b#3Rv zX%Ezudvm7iuN0RfFPIgUszTP3(`vq!%}!;p)aDV-_087f^Y2HoVh&t?B&a`=*xOolZ>%TqG(>2AJ>=i z&rgyLrAq&&8Sn?hrv>3@LE;5*Knx8CL7x~B`a_|>8Gm@d@Bfb(iX7ce9W5U4#N#P7 zt2vdL(K5<(cqu!6VflOhxyzSkGtt>TpMN>D{Oox}2rY^)hGr(0hG)d{FPs|N)-6?-`ul%c9&%8>#-Dmw8 zfYFx~efivc%Teo6BrN({j#?L`fk4m~@JnGK&~Fd4FMgdN)#{EjG^tkk$9K1{-rK(V zap|Ymcd9iMuv471#r3UwH~)U6SYH32{AO`$;qy=34ERL>by zUtj;?X}Y4O*wuUIo$YHMmTtekwZ6Giy&0jjx@oJG*4})uzPh#YeNw&cjbE4F+9-Xr zMwTC3St^;>4pNe@HhDBW0KK$3!ipb!d(LGrXenV6naEz6V! z`mLV_a56WS*)qc-={ATQX(}`5mqejokSdK71ooL%A6BF|5cK;bF(68U)UwFt43+W! zEcYSBmAOBW872ro00Izz00bZa0SG_<0uX=z1Reu{udxxjb^iz3u&SY_g6U=>?%GxX zsjfr8`u%^djpA0w3nmCa00Izz00bZa0SG_<0uX?}J{EX|aZ}$EdyX>kIC-Yw{@vf& zqwNhPqZ((E+T5I~<}(Gum(;RCFybdu<*WbmS|)YqO8r0s^6k3)3;barA_?KE(BSPM#3h+PGQ1ekG~pO!Dtu&yxQ`soQn%C38YBTq&N60uY`b8E4w} zSUP!?V4h+ncgr4y%J=`&6Z?n&O@ROeAOHafKmY;|fB*y_009U m.timeout { + delete(m.lastSeen, tokenStr) + m.mu.Unlock() + utils.JSONError(c, 401, "登录超时,请重新登录") + c.Abort() + return + } + m.lastSeen[tokenStr] = now + m.mu.Unlock() + c.Next() + } +} diff --git a/backend/internal/models/models.go b/backend/internal/models/models.go new file mode 100644 index 0000000..ce6b3fa --- /dev/null +++ b/backend/internal/models/models.go @@ -0,0 +1,67 @@ +package models + +import "time" + +type User struct { + ID uint `gorm:"primaryKey" json:"id"` + Username string `gorm:"uniqueIndex;size:64" json:"username"` + PasswordHash string `json:"-"` + Role string `gorm:"size:32" json:"role"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` +} + +type Product struct { + ID uint `gorm:"primaryKey" json:"id"` + Name string `gorm:"size:128" json:"name"` + Category string `gorm:"size:64" json:"category"` + Description string `gorm:"size:512" json:"description"` + Price float64 `json:"price"` + Inventory int `json:"inventory"` + ImageURL string `json:"imageUrl"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` +} + +type Order struct { + ID uint `gorm:"primaryKey" json:"id"` + OrderNumber string `gorm:"size:64;uniqueIndex" json:"orderNumber"` + CustomerName string `gorm:"size:128" json:"customerName"` + ProductID uint `json:"productId"` + Product Product `json:"product"` + Quantity int `json:"quantity"` + Amount float64 `json:"amount"` + Status string `gorm:"size:32" json:"status"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` +} + +type Member struct { + ID uint `gorm:"primaryKey" json:"id"` + Name string `gorm:"size:128" json:"name"` + Email string `gorm:"size:128;uniqueIndex" json:"email"` + Phone string `gorm:"size:32" json:"phone"` + Tier string `gorm:"size:32" json:"tier"` + Points int `json:"points"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` +} + +type PointTransaction struct { + ID uint `gorm:"primaryKey" json:"id"` + MemberID uint `json:"memberId"` + Member Member `json:"member"` + Change int `json:"change"` + Reason string `gorm:"size:128" json:"reason"` + CreatedAt time.Time `json:"createdAt"` +} + +type Ticket struct { + ID uint `gorm:"primaryKey" json:"id"` + Title string `gorm:"size:128" json:"title"` + Description string `gorm:"size:512" json:"description"` + Status string `gorm:"size:32" json:"status"` + Priority string `gorm:"size:32" json:"priority"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` +} diff --git a/backend/internal/utils/jwt.go b/backend/internal/utils/jwt.go new file mode 100644 index 0000000..8501508 --- /dev/null +++ b/backend/internal/utils/jwt.go @@ -0,0 +1,51 @@ +package utils + +import ( + "time" + + "github.com/golang-jwt/jwt/v5" +) + +type JWTManager struct { + secret []byte + ttl time.Duration +} + +func NewJWTManager(secret string, ttl time.Duration) *JWTManager { + return &JWTManager{secret: []byte(secret), ttl: ttl} +} + +type Claims struct { + UserID uint `json:"userId"` + Username string `json:"username"` + Role string `json:"role"` + jwt.RegisteredClaims +} + +func (j *JWTManager) Generate(userID uint, username, role string) (string, error) { + now := time.Now() + token := jwt.NewWithClaims(jwt.SigningMethodHS256, Claims{ + UserID: userID, + Username: username, + Role: role, + RegisteredClaims: jwt.RegisteredClaims{ + IssuedAt: jwt.NewNumericDate(now), + ExpiresAt: jwt.NewNumericDate(now.Add(j.ttl)), + ID: username + now.Format("20060102150405"), + }, + }) + return token.SignedString(j.secret) +} + +func (j *JWTManager) Parse(tokenString string) (*Claims, error) { + token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) { + return j.secret, nil + }) + if err != nil { + return nil, err + } + if claims, ok := token.Claims.(*Claims); ok && token.Valid { + return claims, nil + } + return nil, jwt.ErrTokenInvalidClaims +} diff --git a/backend/internal/utils/response.go b/backend/internal/utils/response.go new file mode 100644 index 0000000..9cf005a --- /dev/null +++ b/backend/internal/utils/response.go @@ -0,0 +1,17 @@ +package utils + +import "github.com/gin-gonic/gin" + +func JSONSuccess(c *gin.Context, data interface{}) { + c.JSON(200, gin.H{ + "success": true, + "data": data, + }) +} + +func JSONError(c *gin.Context, status int, message string) { + c.JSON(status, gin.H{ + "success": false, + "message": message, + }) +} diff --git a/backend/main.go b/backend/main.go new file mode 100644 index 0000000..47f8086 --- /dev/null +++ b/backend/main.go @@ -0,0 +1,87 @@ +package main + +import ( + "log" + "time" + + "github.com/gin-contrib/cors" + "github.com/gin-gonic/gin" + + "soda-api/backend/internal/database" + "soda-api/backend/internal/handlers" + "soda-api/backend/internal/middleware" + "soda-api/backend/internal/utils" +) + +func main() { + database.InitDB() + db := database.GetDB() + + jwtManager := utils.NewJWTManager("coffee-mall-secret", 4*time.Hour) + idleManager := middleware.NewIdleManager(time.Minute) + + authHandler := handlers.NewAuthHandler(db, jwtManager) + productHandler := handlers.NewProductHandler(db) + orderHandler := handlers.NewOrderHandler(db) + memberHandler := handlers.NewMemberHandler(db) + pointsHandler := handlers.NewPointsHandler(db) + ticketHandler := handlers.NewTicketHandler(db) + mallHandler := handlers.NewMallHandler(db) + + router := gin.Default() + router.Use(cors.New(cors.Config{ + AllowOrigins: []string{"*"}, + AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, + AllowHeaders: []string{"Authorization", "Content-Type"}, + })) + + router.GET("/health", func(c *gin.Context) { + c.JSON(200, gin.H{"status": "ok"}) + }) + + api := router.Group("/api") + { + api.GET("/home", mallHandler.Home) + api.GET("/products", productHandler.List) + api.GET("/products/:id", productHandler.Get) + + api.POST("/orders", orderHandler.Create) + api.POST("/pay/demo", mallHandler.PayDemo) + + api.POST("/auth/login", authHandler.Login) + api.POST("/auth/register", authHandler.Register) + } + + protected := api.Group("/admin") + protected.Use(middleware.NewAuthMiddleware(jwtManager), idleManager.Middleware()) + { + protected.GET("/products", productHandler.List) + protected.POST("/products", productHandler.Create) + protected.GET("/products/:id", productHandler.Get) + protected.PUT("/products/:id", productHandler.Update) + protected.DELETE("/products/:id", productHandler.Delete) + + protected.GET("/orders", orderHandler.List) + protected.PUT("/orders/:id", orderHandler.Update) + protected.DELETE("/orders/:id", orderHandler.Delete) + + protected.GET("/members", memberHandler.List) + protected.POST("/members", memberHandler.Create) + protected.PUT("/members/:id", memberHandler.Update) + protected.DELETE("/members/:id", memberHandler.Delete) + + protected.GET("/points", pointsHandler.List) + protected.POST("/points", pointsHandler.Create) + protected.DELETE("/points/:id", pointsHandler.Delete) + + protected.GET("/tickets", ticketHandler.List) + protected.POST("/tickets", ticketHandler.Create) + protected.PUT("/tickets/:id", ticketHandler.Update) + protected.DELETE("/tickets/:id", ticketHandler.Delete) + } + + log.Println("Coffee mall backend running on :8080") + if err := router.Run(":8080"); err != nil { + log.Fatalf("server error: %v", err) + } +} diff --git a/frontend b/frontend new file mode 160000 index 0000000..204654c --- /dev/null +++ b/frontend @@ -0,0 +1 @@ +Subproject commit 204654c6c0b90f8e573326c050420c41ff331e5c