test.測試檔案部屬
This commit is contained in:
commit
861e35602a
40
README.md
Normal file
40
README.md
Normal file
@ -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 <token>`:`/api/admin/products|orders|members|points|tickets`。
|
||||
- 1 分钟无请求自动判定登录超时,返回 401 并提示前端跳转商城首页。
|
||||
|
||||
## 目录结构
|
||||
```
|
||||
backend/ # Go Gin 服务
|
||||
frontend/ # React + Ant Design 前端
|
||||
README.md # 使用说明
|
||||
```
|
||||
|
||||
## 其他
|
||||
- 数据库存储在 `backend/data/coffee_mall.db`,如需重置可删除该文件后重新启动后端。
|
||||
- 可根据需要扩展真实支付、权限角色、图表等高级特性。
|
||||
BIN
backend/data/coffee_mall.db
Normal file
BIN
backend/data/coffee_mall.db
Normal file
Binary file not shown.
57
backend/go.mod
Normal file
57
backend/go.mod
Normal file
@ -0,0 +1,57 @@
|
||||
module soda-api/backend
|
||||
|
||||
go 1.24.0
|
||||
|
||||
toolchain go1.24.10
|
||||
|
||||
require (
|
||||
github.com/gin-contrib/cors v1.7.6
|
||||
github.com/gin-gonic/gin v1.11.0
|
||||
github.com/glebarez/sqlite v1.11.0
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0
|
||||
golang.org/x/crypto v0.44.0
|
||||
gorm.io/gorm v1.31.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/bytedance/sonic v1.14.0 // indirect
|
||||
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
|
||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||
github.com/glebarez/go-sqlite v1.21.2 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.27.0 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/goccy/go-yaml v1.18.0 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/quic-go/qpack v0.5.1 // indirect
|
||||
github.com/quic-go/quic-go v0.54.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.3.0 // indirect
|
||||
go.uber.org/mock v0.5.0 // indirect
|
||||
golang.org/x/arch v0.20.0 // indirect
|
||||
golang.org/x/mod v0.29.0 // indirect
|
||||
golang.org/x/net v0.46.0 // indirect
|
||||
golang.org/x/sync v0.18.0 // indirect
|
||||
golang.org/x/sys v0.38.0 // indirect
|
||||
golang.org/x/text v0.31.0 // indirect
|
||||
golang.org/x/tools v0.38.0 // indirect
|
||||
google.golang.org/protobuf v1.36.9 // indirect
|
||||
modernc.org/libc v1.22.5 // indirect
|
||||
modernc.org/mathutil v1.5.0 // indirect
|
||||
modernc.org/memory v1.5.0 // indirect
|
||||
modernc.org/sqlite v1.23.1 // indirect
|
||||
)
|
||||
120
backend/go.sum
Normal file
120
backend/go.sum
Normal file
@ -0,0 +1,120 @@
|
||||
github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
|
||||
github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
|
||||
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
|
||||
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||
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/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
|
||||
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
|
||||
github.com/gin-contrib/cors v1.7.6 h1:3gQ8GMzs1Ylpf70y8bMw4fVpycXIeX1ZemuSQIsnQQY=
|
||||
github.com/gin-contrib/cors v1.7.6/go.mod h1:Ulcl+xN4jel9t1Ry8vqph23a60FwH9xVLd+3ykmTjOk=
|
||||
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
|
||||
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
|
||||
github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=
|
||||
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
|
||||
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
|
||||
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
|
||||
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
|
||||
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
|
||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
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/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/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.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
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/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
||||
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
||||
github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
|
||||
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
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/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
|
||||
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
|
||||
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
|
||||
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
|
||||
golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
|
||||
golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
|
||||
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
|
||||
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
|
||||
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
|
||||
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
|
||||
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
|
||||
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
||||
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
|
||||
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
||||
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
|
||||
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/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=
|
||||
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
|
||||
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
||||
modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE=
|
||||
modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY=
|
||||
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
|
||||
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||
modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
|
||||
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
|
||||
modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM=
|
||||
modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk=
|
||||
98
backend/internal/database/database.go
Normal file
98
backend/internal/database/database.go
Normal file
@ -0,0 +1,98 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"github.com/glebarez/sqlite"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"soda-api/backend/internal/models"
|
||||
)
|
||||
|
||||
var (
|
||||
db *gorm.DB
|
||||
once sync.Once
|
||||
)
|
||||
|
||||
func GetDB() *gorm.DB {
|
||||
if db == nil {
|
||||
log.Fatal("database not initialized")
|
||||
}
|
||||
return db
|
||||
}
|
||||
|
||||
func InitDB() {
|
||||
once.Do(func() {
|
||||
if err := os.MkdirAll("data", 0o755); err != nil {
|
||||
log.Fatalf("failed to create data dir: %v", err)
|
||||
}
|
||||
dbPath := filepath.Join("data", "coffee_mall.db")
|
||||
conn, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{})
|
||||
if err != nil {
|
||||
log.Fatalf("failed to open database: %v", err)
|
||||
}
|
||||
if err := conn.AutoMigrate(
|
||||
&models.User{},
|
||||
&models.Product{},
|
||||
&models.Order{},
|
||||
&models.Member{},
|
||||
&models.PointTransaction{},
|
||||
&models.Ticket{},
|
||||
); err != nil {
|
||||
log.Fatalf("auto migrate failed: %v", err)
|
||||
}
|
||||
seed(conn)
|
||||
db = conn
|
||||
})
|
||||
}
|
||||
|
||||
func seed(conn *gorm.DB) {
|
||||
var count int64
|
||||
conn.Model(&models.User{}).Count(&count)
|
||||
if count == 0 {
|
||||
demo := models.User{Username: "demo", Role: "admin"}
|
||||
demo.PasswordHash = "$2a$10$XolaU8xmPKzW0fvkYlEY/.0z6zJGa16q5j6XSxFX5GZDL8apALgxC" // demo123
|
||||
if err := conn.Create(&demo).Error; err != nil {
|
||||
log.Printf("seed user failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
conn.Model(&models.Product{}).Count(&count)
|
||||
if count == 0 {
|
||||
products := []models.Product{
|
||||
{Name: "旗舰商用咖啡机 X1", Category: "全自动", Description: "双锅炉、触控屏、适合大型连锁", Price: 29999, Inventory: 10, ImageURL: "/images/x1.png"},
|
||||
{Name: "智能胶囊咖啡机 C2", Category: "胶囊", Description: "智能联网、支持积分兑换", Price: 8999, Inventory: 25, ImageURL: "/images/c2.png"},
|
||||
{Name: "经典意式咖啡机 M5", Category: "半自动", Description: "配备专业蒸汽棒", Price: 15999, Inventory: 15, ImageURL: "/images/m5.png"},
|
||||
}
|
||||
if err := conn.Create(&products).Error; err != nil {
|
||||
log.Printf("seed products failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
conn.Model(&models.Member{}).Count(&count)
|
||||
if count == 0 {
|
||||
members := []models.Member{
|
||||
{Name: "星咖科技", Email: "contact@starcoffee.cn", Phone: "13800001111", Tier: "VIP", Points: 5200},
|
||||
{Name: "啡享连锁", Email: "sales@coffeeplus.com", Phone: "13900002222", Tier: "Gold", Points: 3200},
|
||||
}
|
||||
if err := conn.Create(&members).Error; err != nil {
|
||||
log.Printf("seed members failed: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Close() {
|
||||
if db == nil {
|
||||
return
|
||||
}
|
||||
sqlDB, err := db.DB()
|
||||
if err != nil {
|
||||
fmt.Printf("close db err: %v\n", err)
|
||||
return
|
||||
}
|
||||
sqlDB.Close()
|
||||
}
|
||||
81
backend/internal/handlers/auth_handler.go
Normal file
81
backend/internal/handlers/auth_handler.go
Normal file
@ -0,0 +1,81 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"soda-api/backend/internal/models"
|
||||
"soda-api/backend/internal/utils"
|
||||
)
|
||||
|
||||
type AuthHandler struct {
|
||||
db *gorm.DB
|
||||
jwtManager *utils.JWTManager
|
||||
}
|
||||
|
||||
func NewAuthHandler(db *gorm.DB, jwt *utils.JWTManager) *AuthHandler {
|
||||
return &AuthHandler{db: db, jwtManager: jwt}
|
||||
}
|
||||
|
||||
type LoginRequest struct {
|
||||
Username string `json:"username" binding:"required"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
}
|
||||
|
||||
type RegisterRequest struct {
|
||||
Username string `json:"username" binding:"required,min=3"`
|
||||
Password string `json:"password" binding:"required,min=6"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
func (h *AuthHandler) Login(c *gin.Context) {
|
||||
var req LoginRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
utils.JSONError(c, 400, "请输入用户名与密码")
|
||||
return
|
||||
}
|
||||
var user models.User
|
||||
if err := h.db.Where("LOWER(username)=?", strings.ToLower(req.Username)).First(&user).Error; err != nil {
|
||||
utils.JSONError(c, 401, "账号或密码错误")
|
||||
return
|
||||
}
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.Password)); err != nil {
|
||||
utils.JSONError(c, 401, "账号或密码错误")
|
||||
return
|
||||
}
|
||||
token, err := h.jwtManager.Generate(user.ID, user.Username, user.Role)
|
||||
if err != nil {
|
||||
utils.JSONError(c, 500, "生成令牌失败")
|
||||
return
|
||||
}
|
||||
utils.JSONSuccess(c, gin.H{
|
||||
"token": token,
|
||||
"user": gin.H{
|
||||
"id": user.ID,
|
||||
"username": user.Username,
|
||||
"role": user.Role,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (h *AuthHandler) Register(c *gin.Context) {
|
||||
var req RegisterRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
utils.JSONError(c, 400, "请填写完整注册信息")
|
||||
return
|
||||
}
|
||||
passwordHash, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
utils.JSONError(c, 500, "密码加密失败")
|
||||
return
|
||||
}
|
||||
user := models.User{Username: req.Username, PasswordHash: string(passwordHash), Role: "merchant"}
|
||||
if err := h.db.Create(&user).Error; err != nil {
|
||||
utils.JSONError(c, 400, "用户名已存在")
|
||||
return
|
||||
}
|
||||
utils.JSONSuccess(c, gin.H{"message": "注册成功,请使用新账号登录"})
|
||||
}
|
||||
52
backend/internal/handlers/mall_handler.go
Normal file
52
backend/internal/handlers/mall_handler.go
Normal file
@ -0,0 +1,52 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"soda-api/backend/internal/models"
|
||||
"soda-api/backend/internal/utils"
|
||||
)
|
||||
|
||||
type MallHandler struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewMallHandler(db *gorm.DB) *MallHandler {
|
||||
return &MallHandler{db: db}
|
||||
}
|
||||
|
||||
func (h *MallHandler) Home(c *gin.Context) {
|
||||
var products []models.Product
|
||||
h.db.Limit(6).Find(&products)
|
||||
|
||||
categories := []gin.H{
|
||||
{"name": "全自动", "description": "旗舰咖啡解决方案"},
|
||||
{"name": "胶囊", "description": "智能云胶囊"},
|
||||
{"name": "半自动", "description": "专业咖啡师体验"},
|
||||
}
|
||||
utils.JSONSuccess(c, gin.H{
|
||||
"hero": gin.H{
|
||||
"title": "商用咖啡机一站式采购",
|
||||
"subtitle": "覆盖全场景的智能咖啡解决方案",
|
||||
},
|
||||
"categories": categories,
|
||||
"products": products,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *MallHandler) PayDemo(c *gin.Context) {
|
||||
var payload struct {
|
||||
OrderID uint `json:"orderId"`
|
||||
Amount float64 `json:"amount"`
|
||||
Method string `json:"method"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&payload); err != nil {
|
||||
utils.JSONError(c, 400, "支付信息不完整")
|
||||
return
|
||||
}
|
||||
utils.JSONSuccess(c, gin.H{
|
||||
"status": "success",
|
||||
"message": "支付演示完成,系统已记录",
|
||||
})
|
||||
}
|
||||
64
backend/internal/handlers/member_handler.go
Normal file
64
backend/internal/handlers/member_handler.go
Normal file
@ -0,0 +1,64 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"soda-api/backend/internal/models"
|
||||
"soda-api/backend/internal/utils"
|
||||
)
|
||||
|
||||
type MemberHandler struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewMemberHandler(db *gorm.DB) *MemberHandler {
|
||||
return &MemberHandler{db: db}
|
||||
}
|
||||
|
||||
func (h *MemberHandler) List(c *gin.Context) {
|
||||
var members []models.Member
|
||||
if err := h.db.Find(&members).Error; err != nil {
|
||||
utils.JSONError(c, 500, "获取会员失败")
|
||||
return
|
||||
}
|
||||
utils.JSONSuccess(c, members)
|
||||
}
|
||||
|
||||
func (h *MemberHandler) Create(c *gin.Context) {
|
||||
var member models.Member
|
||||
if err := c.ShouldBindJSON(&member); err != nil {
|
||||
utils.JSONError(c, 400, "请填写完整的会员信息")
|
||||
return
|
||||
}
|
||||
if err := h.db.Create(&member).Error; err != nil {
|
||||
utils.JSONError(c, 500, "创建会员失败")
|
||||
return
|
||||
}
|
||||
utils.JSONSuccess(c, member)
|
||||
}
|
||||
|
||||
func (h *MemberHandler) Update(c *gin.Context) {
|
||||
var member models.Member
|
||||
if err := h.db.First(&member, c.Param("id")).Error; err != nil {
|
||||
utils.JSONError(c, 404, "会员不存在")
|
||||
return
|
||||
}
|
||||
if err := c.ShouldBindJSON(&member); err != nil {
|
||||
utils.JSONError(c, 400, "数据无效")
|
||||
return
|
||||
}
|
||||
if err := h.db.Save(&member).Error; err != nil {
|
||||
utils.JSONError(c, 500, "更新失败")
|
||||
return
|
||||
}
|
||||
utils.JSONSuccess(c, member)
|
||||
}
|
||||
|
||||
func (h *MemberHandler) Delete(c *gin.Context) {
|
||||
if err := h.db.Delete(&models.Member{}, c.Param("id")).Error; err != nil {
|
||||
utils.JSONError(c, 500, "删除失败")
|
||||
return
|
||||
}
|
||||
utils.JSONSuccess(c, gin.H{"message": "删除成功"})
|
||||
}
|
||||
71
backend/internal/handlers/order_handler.go
Normal file
71
backend/internal/handlers/order_handler.go
Normal file
@ -0,0 +1,71 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"soda-api/backend/internal/models"
|
||||
"soda-api/backend/internal/utils"
|
||||
)
|
||||
|
||||
type OrderHandler struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewOrderHandler(db *gorm.DB) *OrderHandler {
|
||||
return &OrderHandler{db: db}
|
||||
}
|
||||
|
||||
func (h *OrderHandler) List(c *gin.Context) {
|
||||
var orders []models.Order
|
||||
if err := h.db.Preload("Product").Order("id desc").Find(&orders).Error; err != nil {
|
||||
utils.JSONError(c, 500, "獲取訂單資料錯誤")
|
||||
return
|
||||
}
|
||||
utils.JSONSuccess(c, orders)
|
||||
}
|
||||
|
||||
func (h *OrderHandler) Create(c *gin.Context) {
|
||||
var payload models.Order
|
||||
if err := c.ShouldBindJSON(&payload); err != nil {
|
||||
utils.JSONError(c, 400, "訂單資料不完整")
|
||||
return
|
||||
}
|
||||
payload.OrderNumber = fmt.Sprintf("OD-%d", time.Now().UnixNano())
|
||||
if payload.Status == "" {
|
||||
payload.Status = "待支付"
|
||||
}
|
||||
if err := h.db.Create(&payload).Error; err != nil {
|
||||
utils.JSONError(c, 500, "創建訂單失敗")
|
||||
return
|
||||
}
|
||||
utils.JSONSuccess(c, payload)
|
||||
}
|
||||
|
||||
func (h *OrderHandler) Update(c *gin.Context) {
|
||||
var order models.Order
|
||||
if err := h.db.First(&order, c.Param("id")).Error; err != nil {
|
||||
utils.JSONError(c, 404, "订单不存在")
|
||||
return
|
||||
}
|
||||
if err := c.ShouldBindJSON(&order); err != nil {
|
||||
utils.JSONError(c, 400, "订单数据无效")
|
||||
return
|
||||
}
|
||||
if err := h.db.Save(&order).Error; err != nil {
|
||||
utils.JSONError(c, 500, "更新订单失败")
|
||||
return
|
||||
}
|
||||
utils.JSONSuccess(c, order)
|
||||
}
|
||||
|
||||
func (h *OrderHandler) Delete(c *gin.Context) {
|
||||
if err := h.db.Delete(&models.Order{}, c.Param("id")).Error; err != nil {
|
||||
utils.JSONError(c, 500, "删除订单失败")
|
||||
return
|
||||
}
|
||||
utils.JSONSuccess(c, gin.H{"message": "删除成功"})
|
||||
}
|
||||
48
backend/internal/handlers/points_handler.go
Normal file
48
backend/internal/handlers/points_handler.go
Normal file
@ -0,0 +1,48 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"soda-api/backend/internal/models"
|
||||
"soda-api/backend/internal/utils"
|
||||
)
|
||||
|
||||
type PointsHandler struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewPointsHandler(db *gorm.DB) *PointsHandler {
|
||||
return &PointsHandler{db: db}
|
||||
}
|
||||
|
||||
func (h *PointsHandler) List(c *gin.Context) {
|
||||
var records []models.PointTransaction
|
||||
if err := h.db.Preload("Member").Order("id desc").Find(&records).Error; err != nil {
|
||||
utils.JSONError(c, 500, "获取积分记录失败")
|
||||
return
|
||||
}
|
||||
utils.JSONSuccess(c, records)
|
||||
}
|
||||
|
||||
func (h *PointsHandler) Create(c *gin.Context) {
|
||||
var record models.PointTransaction
|
||||
if err := c.ShouldBindJSON(&record); err != nil {
|
||||
utils.JSONError(c, 400, "请填写积分变更信息")
|
||||
return
|
||||
}
|
||||
if err := h.db.Create(&record).Error; err != nil {
|
||||
utils.JSONError(c, 500, "写入失败")
|
||||
return
|
||||
}
|
||||
h.db.Model(&models.Member{}).Where("id = ?", record.MemberID).UpdateColumn("points", gorm.Expr("points + ?", record.Change))
|
||||
utils.JSONSuccess(c, record)
|
||||
}
|
||||
|
||||
func (h *PointsHandler) Delete(c *gin.Context) {
|
||||
if err := h.db.Delete(&models.PointTransaction{}, c.Param("id")).Error; err != nil {
|
||||
utils.JSONError(c, 500, "删除失败")
|
||||
return
|
||||
}
|
||||
utils.JSONSuccess(c, gin.H{"message": "删除成功"})
|
||||
}
|
||||
78
backend/internal/handlers/product_handler.go
Normal file
78
backend/internal/handlers/product_handler.go
Normal file
@ -0,0 +1,78 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"soda-api/backend/internal/models"
|
||||
"soda-api/backend/internal/utils"
|
||||
)
|
||||
|
||||
type ProductHandler struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewProductHandler(db *gorm.DB) *ProductHandler {
|
||||
return &ProductHandler{db: db}
|
||||
}
|
||||
|
||||
func (h *ProductHandler) List(c *gin.Context) {
|
||||
category := c.Query("category")
|
||||
var products []models.Product
|
||||
query := h.db
|
||||
if category != "" {
|
||||
query = query.Where("category = ?", category)
|
||||
}
|
||||
if err := query.Order("id desc").Find(&products).Error; err != nil {
|
||||
utils.JSONError(c, 500, "获取商品失败")
|
||||
return
|
||||
}
|
||||
utils.JSONSuccess(c, products)
|
||||
}
|
||||
|
||||
func (h *ProductHandler) Get(c *gin.Context) {
|
||||
var product models.Product
|
||||
if err := h.db.First(&product, c.Param("id")).Error; err != nil {
|
||||
utils.JSONError(c, 404, "商品不存在")
|
||||
return
|
||||
}
|
||||
utils.JSONSuccess(c, product)
|
||||
}
|
||||
|
||||
func (h *ProductHandler) Create(c *gin.Context) {
|
||||
var product models.Product
|
||||
if err := c.ShouldBindJSON(&product); err != nil {
|
||||
utils.JSONError(c, 400, "请填写完整的商品信息")
|
||||
return
|
||||
}
|
||||
if err := h.db.Create(&product).Error; err != nil {
|
||||
utils.JSONError(c, 500, "创建商品失败")
|
||||
return
|
||||
}
|
||||
utils.JSONSuccess(c, product)
|
||||
}
|
||||
|
||||
func (h *ProductHandler) Update(c *gin.Context) {
|
||||
var product models.Product
|
||||
if err := h.db.First(&product, c.Param("id")).Error; err != nil {
|
||||
utils.JSONError(c, 404, "商品不存在")
|
||||
return
|
||||
}
|
||||
if err := c.ShouldBindJSON(&product); err != nil {
|
||||
utils.JSONError(c, 400, "请填写正确的商品信息")
|
||||
return
|
||||
}
|
||||
if err := h.db.Save(&product).Error; err != nil {
|
||||
utils.JSONError(c, 500, "更新失败")
|
||||
return
|
||||
}
|
||||
utils.JSONSuccess(c, product)
|
||||
}
|
||||
|
||||
func (h *ProductHandler) Delete(c *gin.Context) {
|
||||
if err := h.db.Delete(&models.Product{}, c.Param("id")).Error; err != nil {
|
||||
utils.JSONError(c, 500, "删除失败")
|
||||
return
|
||||
}
|
||||
utils.JSONSuccess(c, gin.H{"message": "删除成功"})
|
||||
}
|
||||
70
backend/internal/handlers/ticket_handler.go
Normal file
70
backend/internal/handlers/ticket_handler.go
Normal file
@ -0,0 +1,70 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"soda-api/backend/internal/models"
|
||||
"soda-api/backend/internal/utils"
|
||||
)
|
||||
|
||||
type TicketHandler struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewTicketHandler(db *gorm.DB) *TicketHandler {
|
||||
return &TicketHandler{db: db}
|
||||
}
|
||||
|
||||
func (h *TicketHandler) List(c *gin.Context) {
|
||||
var tickets []models.Ticket
|
||||
if err := h.db.Find(&tickets).Error; err != nil {
|
||||
utils.JSONError(c, 500, "获取工单失败")
|
||||
return
|
||||
}
|
||||
utils.JSONSuccess(c, tickets)
|
||||
}
|
||||
|
||||
func (h *TicketHandler) Create(c *gin.Context) {
|
||||
var ticket models.Ticket
|
||||
if err := c.ShouldBindJSON(&ticket); err != nil {
|
||||
utils.JSONError(c, 400, "工单信息不完整")
|
||||
return
|
||||
}
|
||||
if ticket.Status == "" {
|
||||
ticket.Status = "处理中"
|
||||
}
|
||||
if ticket.Priority == "" {
|
||||
ticket.Priority = "中"
|
||||
}
|
||||
if err := h.db.Create(&ticket).Error; err != nil {
|
||||
utils.JSONError(c, 500, "创建工单失败")
|
||||
return
|
||||
}
|
||||
utils.JSONSuccess(c, ticket)
|
||||
}
|
||||
|
||||
func (h *TicketHandler) Update(c *gin.Context) {
|
||||
var ticket models.Ticket
|
||||
if err := h.db.First(&ticket, c.Param("id")).Error; err != nil {
|
||||
utils.JSONError(c, 404, "工单不存在")
|
||||
return
|
||||
}
|
||||
if err := c.ShouldBindJSON(&ticket); err != nil {
|
||||
utils.JSONError(c, 400, "工单数据无效")
|
||||
return
|
||||
}
|
||||
if err := h.db.Save(&ticket).Error; err != nil {
|
||||
utils.JSONError(c, 500, "更新失败")
|
||||
return
|
||||
}
|
||||
utils.JSONSuccess(c, ticket)
|
||||
}
|
||||
|
||||
func (h *TicketHandler) Delete(c *gin.Context) {
|
||||
if err := h.db.Delete(&models.Ticket{}, c.Param("id")).Error; err != nil {
|
||||
utils.JSONError(c, 500, "删除失败")
|
||||
return
|
||||
}
|
||||
utils.JSONSuccess(c, gin.H{"message": "删除成功"})
|
||||
}
|
||||
34
backend/internal/middleware/auth.go
Normal file
34
backend/internal/middleware/auth.go
Normal file
@ -0,0 +1,34 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"soda-api/backend/internal/utils"
|
||||
)
|
||||
|
||||
type AuthMiddleware struct {
|
||||
jwtManager *utils.JWTManager
|
||||
}
|
||||
|
||||
func NewAuthMiddleware(jwtManager *utils.JWTManager) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
authHeader := c.GetHeader("Authorization")
|
||||
if authHeader == "" || !strings.HasPrefix(authHeader, "Bearer ") {
|
||||
utils.JSONError(c, 401, "缺少或无效的认证信息")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
tokenString := strings.TrimPrefix(authHeader, "Bearer ")
|
||||
claims, err := jwtManager.Parse(tokenString)
|
||||
if err != nil {
|
||||
utils.JSONError(c, 401, "认证失败: "+err.Error())
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
c.Set("claims", claims)
|
||||
c.Set("tokenString", tokenString)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
47
backend/internal/middleware/idle.go
Normal file
47
backend/internal/middleware/idle.go
Normal file
@ -0,0 +1,47 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"soda-api/backend/internal/utils"
|
||||
)
|
||||
|
||||
type IdleManager struct {
|
||||
mu sync.Mutex
|
||||
lastSeen map[string]time.Time
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
func NewIdleManager(timeout time.Duration) *IdleManager {
|
||||
return &IdleManager{
|
||||
lastSeen: make(map[string]time.Time),
|
||||
timeout: timeout,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *IdleManager) Middleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
token, _ := c.Get("tokenString")
|
||||
tokenStr, _ := token.(string)
|
||||
if tokenStr == "" {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
m.mu.Lock()
|
||||
last, ok := m.lastSeen[tokenStr]
|
||||
now := time.Now()
|
||||
if ok && now.Sub(last) > 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()
|
||||
}
|
||||
}
|
||||
67
backend/internal/models/models.go
Normal file
67
backend/internal/models/models.go
Normal file
@ -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"`
|
||||
}
|
||||
51
backend/internal/utils/jwt.go
Normal file
51
backend/internal/utils/jwt.go
Normal file
@ -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
|
||||
}
|
||||
17
backend/internal/utils/response.go
Normal file
17
backend/internal/utils/response.go
Normal file
@ -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,
|
||||
})
|
||||
}
|
||||
87
backend/main.go
Normal file
87
backend/main.go
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
1
frontend
Submodule
1
frontend
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit 204654c6c0b90f8e573326c050420c41ff331e5c
|
||||
Loading…
Reference in New Issue
Block a user