Initial commit: 商品售卖网站

This commit is contained in:
2026-04-13 07:20:09 +08:00
commit c6154273f2
865 changed files with 26573 additions and 0 deletions
+31
View File
@@ -0,0 +1,31 @@
# Dependencies
node_modules/
vendor/
# Build outputs
frontend/dist/
backend/sale.db
backend/sale
# IDE
.idea/
.vscode/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# Env files
.env
.env.local
.env.*.local
# Logs
*.log
npm-debug.log*
# Temp files
*.tmp
*.temp
+32
View File
@@ -0,0 +1,32 @@
package main
import (
"log"
"sale/internal/api/routes"
"sale/internal/config"
"sale/internal/utils"
"github.com/gin-gonic/gin"
)
func main() {
config.Load()
utils.InitDB()
utils.AutoMigrate()
if config.AppConfig.Server.Mode == "release" {
gin.SetMode(gin.ReleaseMode)
}
r := gin.Default()
routes.SetupRoutes(r)
addr := ":" + config.AppConfig.Server.Port
log.Printf("Server starting on %s", addr)
if err := r.Run(addr); err != nil {
log.Fatalf("Failed to start server: %v", err)
}
}
+62
View File
@@ -0,0 +1,62 @@
module sale
go 1.26.2
require (
github.com/gin-gonic/gin v1.12.0
github.com/glebarez/sqlite v1.11.0
github.com/golang-jwt/jwt/v5 v5.3.1
github.com/joho/godotenv v1.5.1
golang.org/x/crypto v0.50.0
gorm.io/driver/mysql v1.6.0
gorm.io/driver/postgres v1.6.0
gorm.io/gorm v1.31.1
)
require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic v1.15.0 // indirect
github.com/bytedance/sonic/loader v0.5.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.12 // 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.30.1 // indirect
github.com/go-sql-driver/mysql v1.8.1 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/goccy/go-yaml v1.19.2 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.6.0 // indirect
github.com/jackc/puddle/v2 v2.2.2 // 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.6.0 // indirect
github.com/quic-go/quic-go v0.59.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.1 // indirect
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
golang.org/x/arch v0.22.0 // indirect
golang.org/x/net v0.52.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.43.0 // indirect
golang.org/x/text v0.36.0 // indirect
google.golang.org/protobuf v1.36.10 // 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
)
+139
View File
@@ -0,0 +1,139 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
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.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
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.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=
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.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
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.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
github.com/golang-jwt/jwt/v5 v5.3.1/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/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
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/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
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.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
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/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
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.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
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.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
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/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg=
gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo=
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
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=
+124
View File
@@ -0,0 +1,124 @@
package handlers
import (
"net/http"
"strconv"
"sale/internal/models"
"sale/internal/schemas"
"sale/internal/utils"
"github.com/gin-gonic/gin"
)
type AddressHandler struct{}
func NewAddressHandler() *AddressHandler {
return &AddressHandler{}
}
func (h *AddressHandler) List(c *gin.Context) {
userID := c.GetUint("user_id")
var addresses []models.Address
utils.DB.Where("user_id = ?", userID).Order("is_default DESC, created_at DESC").Find(&addresses)
c.JSON(http.StatusOK, gin.H{"data": addresses})
}
func (h *AddressHandler) Create(c *gin.Context) {
userID := c.GetUint("user_id")
var req schemas.CreateAddressRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if req.IsDefault {
utils.DB.Model(&models.Address{}).Where("user_id = ?", userID).Update("is_default", false)
}
address := models.Address{
UserID: userID,
Name: req.Name,
Phone: req.Phone,
Province: req.Province,
City: req.City,
District: req.District,
Address: req.Address,
IsDefault: req.IsDefault,
}
utils.DB.Create(&address)
c.JSON(http.StatusCreated, gin.H{"data": address})
}
func (h *AddressHandler) Update(c *gin.Context) {
userID := c.GetUint("user_id")
id, _ := strconv.Atoi(c.Param("id"))
var address models.Address
if err := utils.DB.Where("id = ? AND user_id = ?", id, userID).First(&address).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Address not found"})
return
}
var req schemas.UpdateAddressRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
updates := make(map[string]interface{})
if req.Name != nil {
updates["name"] = *req.Name
}
if req.Phone != nil {
updates["phone"] = *req.Phone
}
if req.Province != nil {
updates["province"] = *req.Province
}
if req.City != nil {
updates["city"] = *req.City
}
if req.District != nil {
updates["district"] = *req.District
}
if req.Address != nil {
updates["address"] = *req.Address
}
if req.IsDefault != nil && *req.IsDefault {
utils.DB.Model(&models.Address{}).Where("user_id = ?", userID).Update("is_default", false)
updates["is_default"] = true
}
utils.DB.Model(&address).Updates(updates)
c.JSON(http.StatusOK, gin.H{"data": address})
}
func (h *AddressHandler) Delete(c *gin.Context) {
userID := c.GetUint("user_id")
id, _ := strconv.Atoi(c.Param("id"))
if err := utils.DB.Where("id = ? AND user_id = ?", id, userID).Delete(&models.Address{}).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete address"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Address deleted successfully"})
}
func (h *AddressHandler) SetDefault(c *gin.Context) {
userID := c.GetUint("user_id")
id, _ := strconv.Atoi(c.Param("id"))
var address models.Address
if err := utils.DB.Where("id = ? AND user_id = ?", id, userID).First(&address).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Address not found"})
return
}
utils.DB.Model(&models.Address{}).Where("user_id = ?", userID).Update("is_default", false)
utils.DB.Model(&address).Update("is_default", true)
c.JSON(http.StatusOK, gin.H{"message": "Default address set successfully"})
}
+164
View File
@@ -0,0 +1,164 @@
package handlers
import (
"math"
"net/http"
"strconv"
"sale/internal/models"
"sale/internal/schemas"
"sale/internal/utils"
"github.com/gin-gonic/gin"
)
type ArticleHandler struct{}
func NewArticleHandler() *ArticleHandler {
return &ArticleHandler{}
}
func (h *ArticleHandler) List(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
var total int64
utils.DB.Model(&models.Article{}).Where("is_published = ?", true).Count(&total)
var articles []models.Article
offset := (page - 1) * pageSize
utils.DB.Where("is_published = ?", true).
Order("is_pinned DESC, sort_order ASC, created_at DESC").
Offset(offset).Limit(pageSize).Find(&articles)
totalPages := int(math.Ceil(float64(total) / float64(pageSize)))
c.JSON(http.StatusOK, gin.H{
"data": articles,
"pagination": gin.H{
"page": page,
"page_size": pageSize,
"total": total,
"total_pages": totalPages,
},
})
}
func (h *ArticleHandler) GetByID(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var article models.Article
if err := utils.DB.Preload("Author").First(&article, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Article not found"})
return
}
c.JSON(http.StatusOK, gin.H{"data": article})
}
func (h *ArticleHandler) AdminList(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
var total int64
utils.DB.Model(&models.Article{}).Count(&total)
var articles []models.Article
offset := (page - 1) * pageSize
utils.DB.Preload("Author").
Order("is_pinned DESC, sort_order ASC, created_at DESC").
Offset(offset).Limit(pageSize).Find(&articles)
totalPages := int(math.Ceil(float64(total) / float64(pageSize)))
c.JSON(http.StatusOK, gin.H{
"data": articles,
"pagination": gin.H{
"page": page,
"page_size": pageSize,
"total": total,
"total_pages": totalPages,
},
})
}
func (h *ArticleHandler) Create(c *gin.Context) {
userID := c.GetUint("user_id")
var req schemas.CreateArticleRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
article := models.Article{
Title: req.Title,
Content: req.Content,
Summary: req.Summary,
CoverImage: req.CoverImage,
IsPinned: req.IsPinned,
IsPublished: req.IsPublished,
SortOrder: req.SortOrder,
AuthorID: &userID,
}
utils.DB.Create(&article)
c.JSON(http.StatusCreated, gin.H{"data": article})
}
func (h *ArticleHandler) Update(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var article models.Article
if err := utils.DB.First(&article, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Article not found"})
return
}
var req schemas.UpdateArticleRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
updates := make(map[string]interface{})
if req.Title != nil {
updates["title"] = *req.Title
}
if req.Content != nil {
updates["content"] = *req.Content
}
if req.Summary != nil {
updates["summary"] = *req.Summary
}
if req.CoverImage != nil {
updates["cover_image"] = *req.CoverImage
}
if req.IsPinned != nil {
updates["is_pinned"] = *req.IsPinned
}
if req.IsPublished != nil {
updates["is_published"] = *req.IsPublished
}
if req.SortOrder != nil {
updates["sort_order"] = *req.SortOrder
}
utils.DB.Model(&article).Updates(updates)
c.JSON(http.StatusOK, gin.H{"data": article})
}
func (h *ArticleHandler) Delete(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
if err := utils.DB.Delete(&models.Article{}, id).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete article"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Article deleted successfully"})
}
func (h *ArticleHandler) TogglePin(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var article models.Article
if err := utils.DB.First(&article, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Article not found"})
return
}
utils.DB.Model(&article).Update("is_pinned", !article.IsPinned)
c.JSON(http.StatusOK, gin.H{"data": article})
}
+322
View File
@@ -0,0 +1,322 @@
package handlers
import (
"net/http"
"time"
"sale/internal/models"
"sale/internal/schemas"
"sale/internal/utils"
"github.com/gin-gonic/gin"
)
type AuthHandler struct{}
func NewAuthHandler() *AuthHandler {
return &AuthHandler{}
}
func (h *AuthHandler) Register(c *gin.Context) {
var req schemas.RegisterRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
var existingUser models.User
if err := utils.DB.Where("email = ?", req.Email).First(&existingUser).Error; err == nil {
c.JSON(http.StatusConflict, gin.H{"error": "Email already registered"})
return
}
if err := utils.DB.Where("username = ?", req.Username).First(&existingUser).Error; err == nil {
c.JSON(http.StatusConflict, gin.H{"error": "Username already taken"})
return
}
hashedPassword, err := utils.HashPassword(req.Password)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to hash password"})
return
}
user := models.User{
Username: req.Username,
Email: req.Email,
PasswordHash: hashedPassword,
Role: "user",
InviteCode: utils.GenerateInviteCode(),
IsActive: true,
}
if req.InviteCode != "" {
var referrer models.User
if err := utils.DB.Where("invite_code = ?", req.InviteCode).First(&referrer).Error; err == nil {
user.ReferredBy = &referrer.ID
}
}
if err := utils.DB.Create(&user).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create user"})
return
}
if user.ReferredBy != nil {
utils.DB.Model(&models.User{}).Where("id = ?", *user.ReferredBy).
UpdateColumn("purchase_credits", utils.DB.Raw("purchase_credits + 1"))
}
verifyCode := utils.GenerateVerifyCode()
utils.SendVerifyEmail(req.Email, verifyCode)
token, _ := utils.GenerateToken(user.ID, user.Role)
c.JSON(http.StatusCreated, gin.H{
"token": token,
"user": schemas.UserResponse{
ID: user.ID,
Username: user.Username,
Email: user.Email,
Role: user.Role,
PurchaseCredits: user.PurchaseCredits,
InviteCode: user.InviteCode,
EmailVerified: user.EmailVerified,
},
})
}
func (h *AuthHandler) Login(c *gin.Context) {
var req schemas.LoginRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
var user models.User
if err := utils.DB.Where("email = ?", req.Email).First(&user).Error; err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid email or password"})
return
}
if !utils.CheckPassword(req.Password, user.PasswordHash) {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid email or password"})
return
}
if !user.IsActive {
c.JSON(http.StatusForbidden, gin.H{"error": "Account is disabled"})
return
}
token, _ := utils.GenerateToken(user.ID, user.Role)
c.JSON(http.StatusOK, gin.H{
"token": token,
"user": schemas.UserResponse{
ID: user.ID,
Username: user.Username,
Email: user.Email,
Role: user.Role,
PurchaseCredits: user.PurchaseCredits,
InviteCode: user.InviteCode,
EmailVerified: user.EmailVerified,
},
})
}
func (h *AuthHandler) ForgotPassword(c *gin.Context) {
var req schemas.ForgotPasswordRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
var user models.User
if err := utils.DB.Where("email = ?", req.Email).First(&user).Error; err != nil {
c.JSON(http.StatusOK, gin.H{"message": "If the email exists, a verification code has been sent"})
return
}
code := utils.GenerateVerifyCode()
utils.SendResetPasswordEmail(req.Email, code)
c.JSON(http.StatusOK, gin.H{"message": "If the email exists, a verification code has been sent"})
}
func (h *AuthHandler) ResetPassword(c *gin.Context) {
var req schemas.ResetPasswordRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
var user models.User
if err := utils.DB.Where("email = ?", req.Email).First(&user).Error; err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid email"})
return
}
hashedPassword, err := utils.HashPassword(req.Password)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to hash password"})
return
}
utils.DB.Model(&user).Update("password_hash", hashedPassword)
c.JSON(http.StatusOK, gin.H{"message": "Password reset successfully"})
}
func (h *AuthHandler) VerifyEmail(c *gin.Context) {
var req schemas.VerifyEmailRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
var user models.User
if err := utils.DB.Where("email = ?", req.Email).First(&user).Error; err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "User not found"})
return
}
utils.DB.Model(&user).Update("email_verified", true)
c.JSON(http.StatusOK, gin.H{"message": "Email verified successfully"})
}
func (h *AuthHandler) GetProfile(c *gin.Context) {
userID := c.GetUint("user_id")
var user models.User
if err := utils.DB.First(&user, userID).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
return
}
c.JSON(http.StatusOK, schemas.UserResponse{
ID: user.ID,
Username: user.Username,
Email: user.Email,
Role: user.Role,
PurchaseCredits: user.PurchaseCredits,
InviteCode: user.InviteCode,
EmailVerified: user.EmailVerified,
})
}
func (h *AuthHandler) UpdateProfile(c *gin.Context) {
userID := c.GetUint("user_id")
var user models.User
if err := utils.DB.First(&user, userID).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
return
}
var updateData map[string]interface{}
if err := c.ShouldBindJSON(&updateData); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if username, ok := updateData["username"].(string); ok && username != "" {
user.Username = username
}
utils.DB.Save(&user)
c.JSON(http.StatusOK, gin.H{"message": "Profile updated successfully"})
}
func (h *AuthHandler) ChangePassword(c *gin.Context) {
userID := c.GetUint("user_id")
var req schemas.ChangePasswordRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
var user models.User
if err := utils.DB.First(&user, userID).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
return
}
if !utils.CheckPassword(req.OldPassword, user.PasswordHash) {
c.JSON(http.StatusBadRequest, gin.H{"error": "Old password is incorrect"})
return
}
hashedPassword, _ := utils.HashPassword(req.NewPassword)
utils.DB.Model(&user).Update("password_hash", hashedPassword)
c.JSON(http.StatusOK, gin.H{"message": "Password changed successfully"})
}
func (h *AuthHandler) SendVerifyCode(c *gin.Context) {
userID := c.GetUint("user_id")
var user models.User
if err := utils.DB.First(&user, userID).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
return
}
if user.EmailVerified {
c.JSON(http.StatusBadRequest, gin.H{"error": "Email already verified"})
return
}
code := utils.GenerateVerifyCode()
utils.SendVerifyEmail(user.Email, code)
c.JSON(http.StatusOK, gin.H{"message": "Verification code sent", "expires_at": time.Now().Add(30 * time.Minute)})
}
func (h *AuthHandler) CheckInstalled(c *gin.Context) {
var count int64
utils.DB.Model(&models.User{}).Where("role = ?", "admin").Count(&count)
c.JSON(http.StatusOK, gin.H{"installed": count > 0})
}
func (h *AuthHandler) Install(c *gin.Context) {
var count int64
utils.DB.Model(&models.User{}).Where("role = ?", "admin").Count(&count)
if count > 0 {
c.JSON(http.StatusForbidden, gin.H{"error": "System already installed"})
return
}
var req schemas.InstallRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
hashedPassword, err := utils.HashPassword(req.Password)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to hash password"})
return
}
admin := models.User{
Username: req.Username,
Email: req.Email,
PasswordHash: hashedPassword,
Role: "admin",
InviteCode: utils.GenerateInviteCode(),
IsActive: true,
}
if err := utils.DB.Create(&admin).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create admin user"})
return
}
token, _ := utils.GenerateToken(admin.ID, admin.Role)
c.JSON(http.StatusCreated, gin.H{
"token": token,
"user": schemas.UserResponse{
ID: admin.ID,
Username: admin.Username,
Email: admin.Email,
Role: admin.Role,
PurchaseCredits: admin.PurchaseCredits,
InviteCode: admin.InviteCode,
EmailVerified: admin.EmailVerified,
},
})
}
+53
View File
@@ -0,0 +1,53 @@
package handlers
import (
"net/http"
"sale/internal/models"
"sale/internal/utils"
"github.com/gin-gonic/gin"
)
type BrandHandler struct{}
func NewBrandHandler() *BrandHandler {
return &BrandHandler{}
}
func (h *BrandHandler) List(c *gin.Context) {
var brands []models.Brand
utils.DB.Where("deleted_at IS NULL").Order("sort_order ASC, name ASC").Find(&brands)
c.JSON(http.StatusOK, gin.H{"data": brands})
}
func (h *BrandHandler) Create(c *gin.Context) {
var brand models.Brand
if err := c.ShouldBindJSON(&brand); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
utils.DB.Create(&brand)
c.JSON(http.StatusOK, brand)
}
func (h *BrandHandler) Update(c *gin.Context) {
id := c.Param("id")
var brand models.Brand
if err := utils.DB.First(&brand, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Brand not found"})
return
}
var input models.Brand
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
utils.DB.Model(&brand).Updates(input)
c.JSON(http.StatusOK, brand)
}
func (h *BrandHandler) Delete(c *gin.Context) {
id := c.Param("id")
utils.DB.Delete(&models.Brand{}, id)
c.JSON(http.StatusOK, gin.H{"message": "Brand deleted"})
}
+263
View File
@@ -0,0 +1,263 @@
package handlers
import (
"math/rand"
"net/http"
"strconv"
"time"
"sale/internal/models"
"sale/internal/schemas"
"sale/internal/utils"
"github.com/gin-gonic/gin"
)
type LotteryHandler struct{}
func NewLotteryHandler() *LotteryHandler {
return &LotteryHandler{}
}
func (h *LotteryHandler) List(c *gin.Context) {
var lotteries []models.Lottery
utils.DB.Where("is_active = ?", true).Preload("Prizes").Order("created_at DESC").Find(&lotteries)
c.JSON(http.StatusOK, gin.H{"data": lotteries})
}
func (h *LotteryHandler) GetByID(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var lottery models.Lottery
if err := utils.DB.Preload("Prizes").First(&lottery, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Lottery not found"})
return
}
c.JSON(http.StatusOK, gin.H{"data": lottery})
}
func (h *LotteryHandler) Register(c *gin.Context) {
userID := c.GetUint("user_id")
id, _ := strconv.Atoi(c.Param("id"))
var lottery models.Lottery
if err := utils.DB.First(&lottery, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Lottery not found"})
return
}
now := time.Now()
if now.Before(lottery.StartTime) || now.After(lottery.EndTime) {
c.JSON(http.StatusBadRequest, gin.H{"error": "Lottery is not in registration period"})
return
}
var existing models.LotteryParticipant
if err := utils.DB.Where("lottery_id = ? AND user_id = ?", id, userID).First(&existing).Error; err == nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Already registered"})
return
}
var totalSpent float64
utils.DB.Model(&models.Order{}).
Where("user_id = ? AND status = ?", userID, models.OrderStatusCompleted).
Select("COALESCE(SUM(total_amount), 0)").
Scan(&totalSpent)
participant := models.LotteryParticipant{
LotteryID: lottery.ID,
UserID: userID,
PurchaseWeight: int(totalSpent),
}
utils.DB.Create(&participant)
c.JSON(http.StatusOK, gin.H{"message": "Registered successfully"})
}
func (h *LotteryHandler) Create(c *gin.Context) {
var req schemas.CreateLotteryRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
startTime, _ := time.Parse(time.RFC3339, req.StartTime)
endTime, _ := time.Parse(time.RFC3339, req.EndTime)
lottery := models.Lottery{
Name: req.Name,
Description: req.Description,
StartTime: startTime,
EndTime: endTime,
Cycle: req.Cycle,
DailyQuota: req.DailyQuota,
TotalQuota: req.TotalQuota,
RegistrationValidity: req.RegistrationValidity,
IsActive: true,
}
utils.DB.Create(&lottery)
c.JSON(http.StatusCreated, gin.H{"data": lottery})
}
func (h *LotteryHandler) Update(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var lottery models.Lottery
if err := utils.DB.First(&lottery, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Lottery not found"})
return
}
var req schemas.UpdateLotteryRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
updates := make(map[string]interface{})
if req.Name != nil {
updates["name"] = *req.Name
}
if req.Description != nil {
updates["description"] = *req.Description
}
if req.StartTime != nil {
t, _ := time.Parse(time.RFC3339, *req.StartTime)
updates["start_time"] = t
}
if req.EndTime != nil {
t, _ := time.Parse(time.RFC3339, *req.EndTime)
updates["end_time"] = t
}
if req.Cycle != nil {
updates["cycle"] = *req.Cycle
}
if req.DailyQuota != nil {
updates["daily_quota"] = *req.DailyQuota
}
if req.TotalQuota != nil {
updates["total_quota"] = *req.TotalQuota
}
if req.RegistrationValidity != nil {
updates["registration_validity"] = *req.RegistrationValidity
}
if req.IsActive != nil {
updates["is_active"] = *req.IsActive
}
utils.DB.Model(&lottery).Updates(updates)
c.JSON(http.StatusOK, gin.H{"data": lottery})
}
func (h *LotteryHandler) Delete(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
utils.DB.Delete(&models.Lottery{}, id)
c.JSON(http.StatusOK, gin.H{"message": "Lottery deleted successfully"})
}
func (h *LotteryHandler) AddPrize(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var req schemas.AddLotteryPrizeRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
prize := models.LotteryPrize{
LotteryID: uint(id),
Name: req.Name,
Type: req.Type,
Quantity: req.Quantity,
Weight: req.Weight,
CreditReward: req.CreditReward,
DrawMode: req.DrawMode,
}
utils.DB.Create(&prize)
c.JSON(http.StatusCreated, gin.H{"data": prize})
}
func (h *LotteryHandler) Draw(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var lottery models.Lottery
if err := utils.DB.Preload("Prizes").First(&lottery, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Lottery not found"})
return
}
var participants []models.LotteryParticipant
utils.DB.Where("lottery_id = ?", id).Find(&participants)
if len(participants) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "No participants"})
return
}
var winners []models.LotteryWinner
rng := rand.New(rand.NewSource(time.Now().UnixNano()))
for _, prize := range lottery.Prizes {
remaining := prize.Quantity
var weightParticipants []models.LotteryParticipant
var randomParticipants []models.LotteryParticipant
if prize.DrawMode == models.DrawModeWeight {
weightParticipants = participants
} else {
randomParticipants = participants
}
selectedParticipants := weightParticipants
if len(selectedParticipants) == 0 {
selectedParticipants = randomParticipants
}
totalWeight := 0
for _, p := range selectedParticipants {
w := p.PurchaseWeight
if w < 1 {
w = 1
}
totalWeight += w
}
selected := make(map[uint]bool)
for remaining > 0 && len(selected) < len(selectedParticipants) {
r := rng.Intn(totalWeight)
cumWeight := 0
for _, p := range selectedParticipants {
if selected[p.UserID] {
continue
}
w := p.PurchaseWeight
if w < 1 {
w = 1
}
cumWeight += w
if cumWeight > r {
winner := models.LotteryWinner{
LotteryID: lottery.ID,
PrizeID: prize.ID,
UserID: p.UserID,
DrawnAt: time.Now(),
}
winners = append(winners, winner)
selected[p.UserID] = true
remaining--
if prize.Type == models.PrizeTypeCredit && prize.CreditReward != nil {
utils.DB.Model(&models.User{}).Where("id = ?", p.UserID).
UpdateColumn("purchase_credits", utils.DB.Raw("purchase_credits + ?", *prize.CreditReward))
}
break
}
}
}
}
if len(winners) > 0 {
utils.DB.Create(&winners)
}
c.JSON(http.StatusOK, gin.H{"data": winners, "message": "Draw completed successfully"})
}
+414
View File
@@ -0,0 +1,414 @@
package handlers
import (
"math"
"net/http"
"strconv"
"sale/internal/models"
"sale/internal/schemas"
"sale/internal/utils"
"github.com/gin-gonic/gin"
)
type CartHandler struct{}
func NewCartHandler() *CartHandler {
return &CartHandler{}
}
func (h *CartHandler) List(c *gin.Context) {
userID := c.GetUint("user_id")
var carts []models.Cart
utils.DB.Where("user_id = ?", userID).Preload("Product").Find(&carts)
c.JSON(http.StatusOK, gin.H{"data": carts})
}
func (h *CartHandler) Add(c *gin.Context) {
userID := c.GetUint("user_id")
var req schemas.AddToCartRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
var cart models.Cart
result := utils.DB.Where("user_id = ? AND product_id = ?", userID, req.ProductID).First(&cart)
if result.Error == nil {
cart.Quantity += req.Quantity
utils.DB.Save(&cart)
} else {
cart = models.Cart{
UserID: userID,
ProductID: req.ProductID,
Quantity: req.Quantity,
}
utils.DB.Create(&cart)
}
utils.DB.Preload("Product").First(&cart, cart.ID)
c.JSON(http.StatusOK, gin.H{"data": cart})
}
func (h *CartHandler) Update(c *gin.Context) {
userID := c.GetUint("user_id")
id, _ := strconv.Atoi(c.Param("id"))
var cart models.Cart
if err := utils.DB.Where("id = ? AND user_id = ?", id, userID).First(&cart).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Cart item not found"})
return
}
var req schemas.UpdateCartRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
cart.Quantity = req.Quantity
utils.DB.Save(&cart)
utils.DB.Preload("Product").First(&cart, cart.ID)
c.JSON(http.StatusOK, gin.H{"data": cart})
}
func (h *CartHandler) Delete(c *gin.Context) {
userID := c.GetUint("user_id")
id, _ := strconv.Atoi(c.Param("id"))
if err := utils.DB.Where("id = ? AND user_id = ?", id, userID).Delete(&models.Cart{}).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete cart item"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Cart item deleted successfully"})
}
type OrderHandler struct{}
func NewOrderHandler() *OrderHandler {
return &OrderHandler{}
}
func (h *OrderHandler) List(c *gin.Context) {
userID := c.GetUint("user_id")
role, _ := c.Get("role")
var orders []models.Order
query := utils.DB.Model(&models.Order{})
if role == "user" {
query = query.Where("user_id = ?", userID)
} else if role == "supplier" {
query = query.Where("supplier_id = ?", userID)
}
query.Preload("OrderItems.Product").Preload("ShippingAddress").
Order("created_at DESC").Find(&orders)
c.JSON(http.StatusOK, gin.H{"data": orders})
}
func (h *OrderHandler) GetByID(c *gin.Context) {
userID := c.GetUint("user_id")
role, _ := c.Get("role")
id, _ := strconv.Atoi(c.Param("id"))
var order models.Order
query := utils.DB.Preload("OrderItems.Product").Preload("ShippingAddress").Preload("User")
if role == "user" {
query = query.Where("user_id = ?", userID)
} else if role == "supplier" {
query = query.Where("supplier_id = ?", userID)
}
if err := query.First(&order, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Order not found"})
return
}
c.JSON(http.StatusOK, gin.H{"data": order})
}
func (h *OrderHandler) Create(c *gin.Context) {
userID := c.GetUint("user_id")
var req schemas.CreateOrderRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
var carts []models.Cart
utils.DB.Where("user_id = ?", userID).Preload("Product").Find(&carts)
if len(carts) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "Cart is empty"})
return
}
var subtotal float64
var totalQuantity int
var orderItems []models.OrderItem
supplierMap := make(map[uint]bool)
for _, cart := range carts {
if cart.Product.RequireCredit {
var user models.User
utils.DB.First(&user, userID)
if user.PurchaseCredits < cart.Product.CreditCost*cart.Quantity {
c.JSON(http.StatusBadRequest, gin.H{"error": "Insufficient purchase credits for " + cart.Product.Name})
return
}
}
var inventory models.Inventory
if err := utils.DB.Where("product_id = ?", cart.ProductID).First(&inventory).Error; err == nil {
if inventory.Quantity < cart.Quantity {
c.JSON(http.StatusBadRequest, gin.H{"error": "Insufficient stock for " + cart.Product.Name})
return
}
}
subtotal += cart.Product.Price * float64(cart.Quantity)
totalQuantity += cart.Quantity
orderItems = append(orderItems, models.OrderItem{
ProductID: cart.ProductID,
Quantity: cart.Quantity,
Price: cart.Product.Price,
})
if inventory.SupplierID != 0 {
supplierMap[inventory.SupplierID] = true
}
}
var supplierID *uint
for sid := range supplierMap {
sid := sid
supplierID = &sid
break
}
var shippingFeeFirstWeight, shippingFeePerGram, serviceFeeRate, taxRate float64
var setting models.SystemSetting
if err := utils.DB.Where("`key` = ?", "shipping_fee_first_weight").First(&setting).Error; err == nil {
shippingFeeFirstWeight, _ = strconv.ParseFloat(setting.Value, 64)
}
if err := utils.DB.Where("`key` = ?", "shipping_fee_per_gram").First(&setting).Error; err == nil {
shippingFeePerGram, _ = strconv.ParseFloat(setting.Value, 64)
}
if err := utils.DB.Where("`key` = ?", "service_fee_rate").First(&setting).Error; err == nil {
serviceFeeRate, _ = strconv.ParseFloat(setting.Value, 64)
}
if err := utils.DB.Where("`key` = ?", "tax_rate").First(&setting).Error; err == nil {
taxRate, _ = strconv.ParseFloat(setting.Value, 64)
}
shippingFee := shippingFeeFirstWeight
if subtotal >= 99 || shippingFeeFirstWeight == 0 {
shippingFee = 0
} else if totalQuantity > 500 {
shippingFee += shippingFeePerGram * float64(totalQuantity-500)
}
serviceFee := subtotal * serviceFeeRate / 100
tax := subtotal * taxRate / 100
totalAmount := subtotal + shippingFee + serviceFee + tax
order := models.Order{
UserID: userID,
SupplierID: supplierID,
Subtotal: subtotal,
ShippingFee: shippingFee,
ServiceFee: serviceFee,
Tax: tax,
TotalAmount: totalAmount,
Status: models.OrderStatusPendingPayment,
ShippingAddressID: &req.ShippingAddressID,
PaymentMethod: req.PaymentMethod,
}
if err := utils.DB.Create(&order).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create order"})
return
}
for i := range orderItems {
orderItems[i].OrderID = order.ID
}
utils.DB.Create(&orderItems)
for _, cart := range carts {
if cart.Product.RequireCredit {
utils.DB.Model(&models.User{}).Where("id = ?", userID).
UpdateColumn("purchase_credits", utils.DB.Raw("purchase_credits - ?", cart.Product.CreditCost*cart.Quantity))
}
if cart.Product.CreditReward > 0 {
utils.DB.Model(&models.User{}).Where("id = ?", userID).
UpdateColumn("purchase_credits", utils.DB.Raw("purchase_credits + ?", cart.Product.CreditReward*cart.Quantity))
}
}
utils.DB.Where("user_id = ?", userID).Delete(&models.Cart{})
utils.DB.Preload("OrderItems.Product").Preload("ShippingAddress").First(&order, order.ID)
c.JSON(http.StatusCreated, gin.H{"data": order})
}
func (h *OrderHandler) Refund(c *gin.Context) {
userID := c.GetUint("user_id")
id, _ := strconv.Atoi(c.Param("id"))
var order models.Order
if err := utils.DB.Where("id = ? AND user_id = ?", id, userID).First(&order).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Order not found"})
return
}
if order.Status == models.OrderStatusShipped || order.Status == models.OrderStatusCompleted {
c.JSON(http.StatusBadRequest, gin.H{"error": "Cannot refund a shipped or completed order"})
return
}
if order.RefundStatus == models.RefundStatusPending {
c.JSON(http.StatusBadRequest, gin.H{"error": "Refund already requested"})
return
}
var req schemas.RefundRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
utils.DB.Model(&order).Updates(map[string]interface{}{
"refund_status": models.RefundStatusPending,
"refund_reason": req.Reason,
"status": models.OrderStatusRefunding,
})
c.JSON(http.StatusOK, gin.H{"message": "Refund request submitted successfully"})
}
func (h *OrderHandler) ConfirmOrder(c *gin.Context) {
userID := c.GetUint("user_id")
id, _ := strconv.Atoi(c.Param("id"))
var order models.Order
if err := utils.DB.Where("id = ? AND supplier_id = ?", id, userID).First(&order).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Order not found"})
return
}
if order.Status != models.OrderStatusPendingConfirm {
c.JSON(http.StatusBadRequest, gin.H{"error": "Order cannot be confirmed"})
return
}
utils.DB.Model(&order).Update("status", models.OrderStatusPendingShip)
c.JSON(http.StatusOK, gin.H{"message": "Order confirmed successfully"})
}
func (h *OrderHandler) ShipOrder(c *gin.Context) {
userID := c.GetUint("user_id")
id, _ := strconv.Atoi(c.Param("id"))
var order models.Order
if err := utils.DB.Where("id = ? AND supplier_id = ?", id, userID).First(&order).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Order not found"})
return
}
if order.Status != models.OrderStatusPendingShip {
c.JSON(http.StatusBadRequest, gin.H{"error": "Order cannot be shipped"})
return
}
var req schemas.ShipOrderRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
utils.DB.Model(&order).Updates(map[string]interface{}{
"status": models.OrderStatusShipped,
"tracking_number": req.TrackingNumber,
"shipping_photo": req.ShippingPhoto,
"express_photo": req.ExpressPhoto,
"customs_photo": req.CustomsPhoto,
})
c.JSON(http.StatusOK, gin.H{"message": "Order shipped successfully"})
}
func (h *OrderHandler) AdminList(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
var total int64
utils.DB.Model(&models.Order{}).Count(&total)
var orders []models.Order
offset := (page - 1) * pageSize
utils.DB.Preload("OrderItems.Product").Preload("ShippingAddress").Preload("User").
Order("created_at DESC").Offset(offset).Limit(pageSize).Find(&orders)
totalPages := int(math.Ceil(float64(total) / float64(pageSize)))
c.JSON(http.StatusOK, gin.H{
"data": orders,
"pagination": gin.H{
"page": page,
"page_size": pageSize,
"total": total,
"total_pages": totalPages,
},
})
}
func (h *OrderHandler) ProcessRefund(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var order models.Order
if err := utils.DB.First(&order, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Order not found"})
return
}
var req schemas.ProcessRefundRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if req.Status == "approved" {
var setting models.SystemSetting
feeRate := 0.0
if err := utils.DB.Where("`key` = ?", "payment_channel_fee_rate").First(&setting).Error; err == nil {
feeRate, _ = strconv.ParseFloat(setting.Value, 64)
}
refundAmount := order.TotalAmount * (1 - feeRate/100)
utils.DB.Model(&order).Updates(map[string]interface{}{
"refund_status": models.RefundStatusCompleted,
"refund_amount": refundAmount,
"status": models.OrderStatusRefunded,
})
} else {
utils.DB.Model(&order).Updates(map[string]interface{}{
"refund_status": models.RefundStatusRejected,
"status": models.OrderStatusPendingConfirm,
})
}
c.JSON(http.StatusOK, gin.H{"message": "Refund processed successfully"})
}
func (h *OrderHandler) Export(c *gin.Context) {
var orders []models.Order
utils.DB.Preload("OrderItems.Product").Preload("ShippingAddress").Preload("User").
Order("created_at DESC").Find(&orders)
c.JSON(http.StatusOK, gin.H{"data": orders})
}
+356
View File
@@ -0,0 +1,356 @@
package handlers
import (
"math"
"net/http"
"strconv"
"sale/internal/models"
"sale/internal/schemas"
"sale/internal/utils"
"github.com/gin-gonic/gin"
)
type CategoryHandler struct{}
func NewCategoryHandler() *CategoryHandler {
return &CategoryHandler{}
}
func (h *CategoryHandler) List(c *gin.Context) {
var categories []models.Category
utils.DB.Where("parent_id IS NULL").Order("sort_order ASC, id ASC").Find(&categories)
for i := range categories {
utils.DB.Where("parent_id = ?", categories[i].ID).Order("sort_order ASC, id ASC").Find(&categories[i].Children)
}
c.JSON(http.StatusOK, gin.H{"data": categories})
}
func (h *CategoryHandler) Create(c *gin.Context) {
var req schemas.CreateCategoryRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
category := models.Category{
Name: req.Name,
Description: req.Description,
ParentID: req.ParentID,
MinAmount: req.MinAmount,
MaxAmount: req.MaxAmount,
MinQuantity: req.MinQuantity,
MaxQuantity: req.MaxQuantity,
MinWeight: req.MinWeight,
MaxWeight: req.MaxWeight,
SortOrder: req.SortOrder,
}
if err := utils.DB.Create(&category).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create category"})
return
}
c.JSON(http.StatusCreated, gin.H{"data": category})
}
func (h *CategoryHandler) Update(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var category models.Category
if err := utils.DB.First(&category, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Category not found"})
return
}
var req schemas.UpdateCategoryRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
updates := make(map[string]interface{})
if req.Name != nil {
updates["name"] = *req.Name
}
if req.Description != nil {
updates["description"] = *req.Description
}
if req.ParentID != nil {
updates["parent_id"] = *req.ParentID
}
if req.MinAmount != nil {
updates["min_amount"] = *req.MinAmount
}
if req.MaxAmount != nil {
updates["max_amount"] = *req.MaxAmount
}
if req.MinQuantity != nil {
updates["min_quantity"] = *req.MinQuantity
}
if req.MaxQuantity != nil {
updates["max_quantity"] = *req.MaxQuantity
}
if req.MinWeight != nil {
updates["min_weight"] = *req.MinWeight
}
if req.MaxWeight != nil {
updates["max_weight"] = *req.MaxWeight
}
if req.SortOrder != nil {
updates["sort_order"] = *req.SortOrder
}
utils.DB.Model(&category).Updates(updates)
c.JSON(http.StatusOK, gin.H{"data": category})
}
func (h *CategoryHandler) Delete(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
if err := utils.DB.Delete(&models.Category{}, id).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete category"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Category deleted successfully"})
}
type ProductHandler struct{}
func NewProductHandler() *ProductHandler {
return &ProductHandler{}
}
func (h *ProductHandler) List(c *gin.Context) {
var req schemas.ProductListRequest
if err := c.ShouldBindQuery(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
query := utils.DB.Model(&models.Product{}).Where("is_active = ?", true)
if req.CategoryID != nil {
query = query.Joins("JOIN product_categories ON product_categories.product_id = products.id").
Where("product_categories.category_id = ?", *req.CategoryID)
}
if req.BrandID != nil {
query = query.Where("brand_id = ?", *req.BrandID)
}
if req.Keyword != "" {
query = query.Where("name ILIKE ?", "%"+req.Keyword+"%")
}
if req.MinPrice != nil {
query = query.Where("price >= ?", *req.MinPrice)
}
if req.MaxPrice != nil {
query = query.Where("price <= ?", *req.MaxPrice)
}
var total int64
query.Count(&total)
var products []models.Product
offset := (req.Page - 1) * req.PageSize
query.Preload("Categories").Preload("CustomFields").Preload("Brand").
Order("created_at DESC").
Offset(offset).Limit(req.PageSize).
Find(&products)
totalPages := int(math.Ceil(float64(total) / float64(req.PageSize)))
c.JSON(http.StatusOK, gin.H{
"data": products,
"pagination": gin.H{
"page": req.Page,
"page_size": req.PageSize,
"total": total,
"total_pages": totalPages,
},
})
}
func (h *ProductHandler) GetByID(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var product models.Product
if err := utils.DB.Preload("Categories").Preload("CustomFields").First(&product, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Product not found"})
return
}
c.JSON(http.StatusOK, gin.H{"data": product})
}
func (h *ProductHandler) AdminList(c *gin.Context) {
var products []models.Product
utils.DB.Preload("Categories").Preload("Brand").Order("created_at DESC").Find(&products)
c.JSON(http.StatusOK, gin.H{"data": products})
}
func (h *ProductHandler) Create(c *gin.Context) {
var req schemas.CreateProductRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
product := models.Product{
Name: req.Name,
Description: req.Description,
Price: req.Price,
MinPurchase: req.MinPurchase,
MaxPurchase: req.MaxPurchase,
MinWeight: req.MinWeight,
MaxWeight: req.MaxWeight,
MinAmount: req.MinAmount,
MaxAmount: req.MaxAmount,
RequireCredit: req.RequireCredit,
CreditCost: req.CreditCost,
CreditReward: req.CreditReward,
Images: req.Images,
BrandID: req.BrandID,
IsActive: true,
}
if err := utils.DB.Create(&product).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create product"})
return
}
if len(req.CategoryIDs) > 0 {
var categories []models.Category
utils.DB.Where("id IN ?", req.CategoryIDs).Find(&categories)
utils.DB.Model(&product).Association("Categories").Replace(categories)
}
if len(req.CustomFields) > 0 {
for _, cf := range req.CustomFields {
utils.DB.Create(&models.ProductCustomField{
ProductID: product.ID,
FieldName: cf.FieldName,
FieldValue: cf.FieldValue,
})
}
}
utils.DB.Preload("Categories").Preload("CustomFields").Preload("Brand").First(&product, product.ID)
c.JSON(http.StatusCreated, gin.H{"data": product})
}
func (h *ProductHandler) Update(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var product models.Product
if err := utils.DB.First(&product, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Product not found"})
return
}
var req schemas.UpdateProductRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
updates := make(map[string]interface{})
if req.Name != nil {
updates["name"] = *req.Name
}
if req.Description != nil {
updates["description"] = *req.Description
}
if req.Price != nil {
updates["price"] = *req.Price
}
if req.MinPurchase != nil {
updates["min_purchase"] = *req.MinPurchase
}
if req.MaxPurchase != nil {
updates["max_purchase"] = *req.MaxPurchase
}
if req.MinWeight != nil {
updates["min_weight"] = *req.MinWeight
}
if req.MaxWeight != nil {
updates["max_weight"] = *req.MaxWeight
}
if req.MinAmount != nil {
updates["min_amount"] = *req.MinAmount
}
if req.MaxAmount != nil {
updates["max_amount"] = *req.MaxAmount
}
if req.RequireCredit != nil {
updates["require_credit"] = *req.RequireCredit
}
if req.CreditCost != nil {
updates["credit_cost"] = *req.CreditCost
}
if req.CreditReward != nil {
updates["credit_reward"] = *req.CreditReward
}
if req.Images != nil {
updates["images"] = *req.Images
}
if req.IsActive != nil {
updates["is_active"] = *req.IsActive
}
if req.BrandID != nil {
updates["brand_id"] = *req.BrandID
}
utils.DB.Model(&product).Updates(updates)
if req.CategoryIDs != nil {
var categories []models.Category
utils.DB.Where("id IN ?", req.CategoryIDs).Find(&categories)
utils.DB.Model(&product).Association("Categories").Replace(categories)
}
if req.CustomFields != nil {
utils.DB.Where("product_id = ?", product.ID).Delete(&models.ProductCustomField{})
for _, cf := range req.CustomFields {
utils.DB.Create(&models.ProductCustomField{
ProductID: product.ID,
FieldName: cf.FieldName,
FieldValue: cf.FieldValue,
})
}
}
utils.DB.Preload("Categories").Preload("CustomFields").Preload("Brand").First(&product, product.ID)
c.JSON(http.StatusOK, gin.H{"data": product})
}
func (h *ProductHandler) Delete(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
if err := utils.DB.Delete(&models.Product{}, id).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete product"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Product deleted successfully"})
}
func (h *ProductHandler) AddCustomField(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var product models.Product
if err := utils.DB.First(&product, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Product not found"})
return
}
var req schemas.CustomFieldRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
field := models.ProductCustomField{
ProductID: product.ID,
FieldName: req.FieldName,
FieldValue: req.FieldValue,
}
utils.DB.Create(&field)
c.JSON(http.StatusCreated, gin.H{"data": field})
}
+247
View File
@@ -0,0 +1,247 @@
package handlers
import (
"net/http"
"strconv"
"sale/internal/models"
"sale/internal/schemas"
"sale/internal/utils"
"github.com/gin-gonic/gin"
)
type SystemHandler struct{}
func NewSystemHandler() *SystemHandler {
return &SystemHandler{}
}
func (h *SystemHandler) GetSettings(c *gin.Context) {
var settings []models.SystemSetting
utils.DB.Find(&settings)
result := make(map[string]string)
for _, s := range settings {
result[s.Key] = s.Value
}
c.JSON(http.StatusOK, gin.H{"data": result})
}
func (h *SystemHandler) GetPublicSettings(c *gin.Context) {
publicKeys := []string{
"payment_channel_fee_rate",
"shipping_fee_first_weight",
"shipping_fee_per_gram",
"service_fee_rate",
"tax_rate",
"enabled_payments",
}
var settings []models.SystemSetting
utils.DB.Where("`key` IN ?", publicKeys).Find(&settings)
result := make(map[string]string)
for _, s := range settings {
result[s.Key] = s.Value
}
c.JSON(http.StatusOK, gin.H{"data": result})
}
func (h *SystemHandler) GetStats(c *gin.Context) {
var userCount, productCount, orderCount int64
var totalRevenue float64
utils.DB.Model(&models.User{}).Count(&userCount)
utils.DB.Model(&models.Product{}).Count(&productCount)
utils.DB.Model(&models.Order{}).Count(&orderCount)
utils.DB.Model(&models.Order{}).Where("status = ?", "completed").Select("COALESCE(SUM(total), 0)").Scan(&totalRevenue)
c.JSON(http.StatusOK, gin.H{
"data": gin.H{
"users": userCount,
"products": productCount,
"orders": orderCount,
"revenue": totalRevenue,
},
})
}
func (h *SystemHandler) UpdateSettings(c *gin.Context) {
var req schemas.UpdateSettingsRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
for key, value := range req.Settings {
var setting models.SystemSetting
result := utils.DB.Where("`key` = ?", key).First(&setting)
if result.Error != nil {
utils.DB.Create(&models.SystemSetting{Key: key, Value: value})
} else {
utils.DB.Model(&setting).Update("value", value)
}
}
c.JSON(http.StatusOK, gin.H{"message": "Settings updated successfully"})
}
func (h *SystemHandler) UpdateSMTP(c *gin.Context) {
var req map[string]string
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
smtpKeys := []string{"smtp_host", "smtp_port", "smtp_user", "smtp_password", "smtp_from"}
for _, key := range smtpKeys {
if value, ok := req[key]; ok {
var setting models.SystemSetting
result := utils.DB.Where("`key` = ?", key).First(&setting)
if result.Error != nil {
utils.DB.Create(&models.SystemSetting{Key: key, Value: value})
} else {
utils.DB.Model(&setting).Update("value", value)
}
}
}
c.JSON(http.StatusOK, gin.H{"message": "SMTP settings updated successfully"})
}
func (h *SystemHandler) UpdatePayment(c *gin.Context) {
var req map[string]string
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
for key, value := range req {
var setting models.SystemSetting
result := utils.DB.Where("`key` = ?", key).First(&setting)
if result.Error != nil {
utils.DB.Create(&models.SystemSetting{Key: key, Value: value})
} else {
utils.DB.Model(&setting).Update("value", value)
}
}
c.JSON(http.StatusOK, gin.H{"message": "Payment settings updated successfully"})
}
type SupplierHandler struct{}
func NewSupplierHandler() *SupplierHandler {
return &SupplierHandler{}
}
func (h *SupplierHandler) List(c *gin.Context) {
var suppliers []models.User
utils.DB.Where("role = ?", "supplier").Find(&suppliers)
c.JSON(http.StatusOK, gin.H{"data": suppliers})
}
func (h *SupplierHandler) Create(c *gin.Context) {
var req schemas.AddSupplierRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
hashedPassword, _ := utils.HashPassword(req.Password)
supplier := models.User{
Username: req.Username,
Email: req.Email,
PasswordHash: hashedPassword,
Role: "supplier",
InviteCode: utils.GenerateInviteCode(),
IsActive: true,
}
utils.DB.Create(&supplier)
c.JSON(http.StatusCreated, gin.H{"data": supplier})
}
func (h *SupplierHandler) Update(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var supplier models.User
if err := utils.DB.Where("id = ? AND role = ?", id, "supplier").First(&supplier).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Supplier not found"})
return
}
var updateData map[string]interface{}
if err := c.ShouldBindJSON(&updateData); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
utils.DB.Model(&supplier).Updates(updateData)
c.JSON(http.StatusOK, gin.H{"data": supplier})
}
func (h *SupplierHandler) Delete(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
utils.DB.Where("id = ? AND role = ?", id, "supplier").Delete(&models.User{})
c.JSON(http.StatusOK, gin.H{"message": "Supplier deleted successfully"})
}
func (h *SupplierHandler) Authorize(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var req schemas.AuthorizeSupplierRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
auth := models.SupplierAuthorization{
SupplierID: uint(id),
Type: req.Type,
RefID: req.RefID,
}
utils.DB.Create(&auth)
c.JSON(http.StatusCreated, gin.H{"data": auth})
}
type InventoryHandler struct{}
func NewInventoryHandler() *InventoryHandler {
return &InventoryHandler{}
}
func (h *InventoryHandler) SupplierList(c *gin.Context) {
userID := c.GetUint("user_id")
var inventory []models.Inventory
utils.DB.Where("supplier_id = ?", userID).Preload("Product").Find(&inventory)
c.JSON(http.StatusOK, gin.H{"data": inventory})
}
func (h *InventoryHandler) Update(c *gin.Context) {
userID := c.GetUint("user_id")
id, _ := strconv.Atoi(c.Param("id"))
var inventory models.Inventory
if err := utils.DB.Where("id = ? AND supplier_id = ?", id, userID).First(&inventory).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Inventory not found"})
return
}
var req schemas.UpdateInventoryRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
utils.DB.Model(&inventory).Update("quantity", req.Quantity)
c.JSON(http.StatusOK, gin.H{"data": inventory})
}
func (h *InventoryHandler) AdminList(c *gin.Context) {
var inventory []models.Inventory
utils.DB.Preload("Product").Find(&inventory)
c.JSON(http.StatusOK, gin.H{"data": inventory})
}
+166
View File
@@ -0,0 +1,166 @@
package handlers
import (
"math"
"net/http"
"strconv"
"sale/internal/models"
"sale/internal/schemas"
"sale/internal/utils"
"github.com/gin-gonic/gin"
)
type TicketHandler struct{}
func NewTicketHandler() *TicketHandler {
return &TicketHandler{}
}
func (h *TicketHandler) List(c *gin.Context) {
userID := c.GetUint("user_id")
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
var total int64
utils.DB.Model(&models.Ticket{}).Where("user_id = ?", userID).Count(&total)
var tickets []models.Ticket
offset := (page - 1) * pageSize
utils.DB.Where("user_id = ?", userID).Order("created_at DESC").Offset(offset).Limit(pageSize).Find(&tickets)
totalPages := int(math.Ceil(float64(total) / float64(pageSize)))
c.JSON(http.StatusOK, gin.H{
"data": tickets,
"pagination": gin.H{
"page": page,
"page_size": pageSize,
"total": total,
"total_pages": totalPages,
},
})
}
func (h *TicketHandler) Create(c *gin.Context) {
userID := c.GetUint("user_id")
var req schemas.CreateTicketRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
category := req.Category
if category == "" {
category = models.TicketCategoryOther
}
ticket := models.Ticket{
UserID: userID,
Title: req.Title,
Content: req.Content,
Category: category,
Status: models.TicketStatusPending,
}
utils.DB.Create(&ticket)
c.JSON(http.StatusCreated, gin.H{"data": ticket})
}
func (h *TicketHandler) GetByID(c *gin.Context) {
userID := c.GetUint("user_id")
id, _ := strconv.Atoi(c.Param("id"))
var ticket models.Ticket
if err := utils.DB.Where("id = ? AND user_id = ?", id, userID).First(&ticket).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Ticket not found"})
return
}
c.JSON(http.StatusOK, gin.H{"data": ticket})
}
func (h *TicketHandler) AdminList(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
status := c.Query("status")
var total int64
query := utils.DB.Model(&models.Ticket{})
if status != "" {
query = query.Where("status = ?", status)
}
query.Count(&total)
var tickets []models.Ticket
offset := (page - 1) * pageSize
q := utils.DB.Preload("User").Preload("Assignee")
if status != "" {
q = q.Where("status = ?", status)
}
q.Order("created_at DESC").Offset(offset).Limit(pageSize).Find(&tickets)
totalPages := int(math.Ceil(float64(total) / float64(pageSize)))
c.JSON(http.StatusOK, gin.H{
"data": tickets,
"pagination": gin.H{
"page": page,
"page_size": pageSize,
"total": total,
"total_pages": totalPages,
},
})
}
func (h *TicketHandler) Update(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var ticket models.Ticket
if err := utils.DB.First(&ticket, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Ticket not found"})
return
}
var req schemas.UpdateTicketRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
updates := make(map[string]interface{})
if req.Status != nil {
updates["status"] = *req.Status
}
if req.AssignedTo != nil {
updates["assigned_to"] = *req.AssignedTo
}
if req.Reply != nil {
updates["reply"] = *req.Reply
}
utils.DB.Model(&ticket).Updates(updates)
c.JSON(http.StatusOK, gin.H{"data": ticket})
}
func (h *TicketHandler) Assign(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var ticket models.Ticket
if err := utils.DB.First(&ticket, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Ticket not found"})
return
}
var req struct {
AssignedTo uint `json:"assigned_to" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
utils.DB.Model(&ticket).Updates(map[string]interface{}{
"assigned_to": req.AssignedTo,
"status": models.TicketStatusProcessing,
})
c.JSON(http.StatusOK, gin.H{"message": "Ticket assigned successfully"})
}
+138
View File
@@ -0,0 +1,138 @@
package handlers
import (
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
type UploadHandler struct{}
func NewUploadHandler() *UploadHandler {
return &UploadHandler{}
}
func (h *UploadHandler) UploadImage(c *gin.Context) {
file, err := c.FormFile("file")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "No file uploaded"})
return
}
ext := strings.ToLower(filepath.Ext(file.Filename))
allowedExts := map[string]bool{".jpg": true, ".jpeg": true, ".png": true, ".gif": true, ".webp": true}
if !allowedExts[ext] {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid file type. Only jpg, jpeg, png, gif, webp are allowed"})
return
}
uploadDir := "uploads/images"
if err := os.MkdirAll(uploadDir, 0755); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create upload directory"})
return
}
filename := fmt.Sprintf("%s%s", uuid.New().String(), ext)
filepath := filepath.Join(uploadDir, filename)
if err := c.SaveUploadedFile(file, filepath); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save file"})
return
}
url := fmt.Sprintf("/uploads/images/%s", filename)
c.JSON(http.StatusOK, gin.H{
"url": url,
"filename": filename,
})
}
func (h *UploadHandler) UploadMultiple(c *gin.Context) {
form, err := c.MultipartForm()
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "No files uploaded"})
return
}
files := form.File["files"]
if len(files) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "No files uploaded"})
return
}
uploadDir := "uploads/images"
if err := os.MkdirAll(uploadDir, 0755); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create upload directory"})
return
}
allowedExts := map[string]bool{".jpg": true, ".jpeg": true, ".png": true, ".gif": true, ".webp": true}
var urls []string
for _, file := range files {
ext := strings.ToLower(filepath.Ext(file.Filename))
if !allowedExts[ext] {
continue
}
filename := fmt.Sprintf("%s%s", uuid.New().String(), ext)
filepath := filepath.Join(uploadDir, filename)
if err := c.SaveUploadedFile(file, filepath); err != nil {
continue
}
urls = append(urls, fmt.Sprintf("/uploads/images/%s", filename))
}
c.JSON(http.StatusOK, gin.H{
"urls": urls,
})
}
func (h *UploadHandler) DeleteImage(c *gin.Context) {
filename := c.Param("filename")
if filename == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Filename is required"})
return
}
filepath := filepath.Join("uploads/images", filename)
if err := os.Remove(filepath); err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "File not found"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "File deleted successfully"})
}
func (h *UploadHandler) ExportOrders(c *gin.Context) {
filename := fmt.Sprintf("orders_%s.csv", time.Now().Format("20060102150405"))
filepath := filepath.Join("uploads", filename)
file, err := os.Create(filepath)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create file"})
return
}
defer file.Close()
c.Header("Content-Description", "File Transfer")
c.Header("Content-Transfer-Encoding", "binary")
c.Header("Content-Disposition", "attachment; filename="+filename)
c.Header("Content-Type", "text/csv")
file.Seek(0, 0)
_, err = io.Copy(c.Writer, file)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to send file"})
return
}
}
+79
View File
@@ -0,0 +1,79 @@
package middlewares
import (
"net/http"
"strings"
"sale/internal/utils"
"github.com/gin-gonic/gin"
)
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header is required"})
c.Abort()
return
}
parts := strings.SplitN(authHeader, " ", 2)
if len(parts) != 2 || parts[0] != "Bearer" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid authorization header format"})
c.Abort()
return
}
claims, err := utils.ParseToken(parts[1])
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid or expired token"})
c.Abort()
return
}
c.Set("user_id", claims.UserID)
c.Set("role", claims.Role)
c.Next()
}
}
func RoleMiddleware(roles ...string) gin.HandlerFunc {
roleMap := make(map[string]bool)
for _, r := range roles {
roleMap[r] = true
}
return func(c *gin.Context) {
role, exists := c.Get("role")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
c.Abort()
return
}
if !roleMap[role.(string)] {
c.JSON(http.StatusForbidden, gin.H{"error": "Permission denied"})
c.Abort()
return
}
c.Next()
}
}
func CORSMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE")
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(204)
return
}
c.Next()
}
}
+207
View File
@@ -0,0 +1,207 @@
package routes
import (
"sale/internal/api/handlers"
"sale/internal/api/middlewares"
"sale/internal/models"
"sale/internal/utils"
"github.com/gin-gonic/gin"
)
func SetupRoutes(r *gin.Engine) {
authHandler := handlers.NewAuthHandler()
categoryHandler := handlers.NewCategoryHandler()
brandHandler := handlers.NewBrandHandler()
productHandler := handlers.NewProductHandler()
cartHandler := handlers.NewCartHandler()
orderHandler := handlers.NewOrderHandler()
addressHandler := handlers.NewAddressHandler()
lotteryHandler := handlers.NewLotteryHandler()
ticketHandler := handlers.NewTicketHandler()
systemHandler := handlers.NewSystemHandler()
supplierHandler := handlers.NewSupplierHandler()
inventoryHandler := handlers.NewInventoryHandler()
articleHandler := handlers.NewArticleHandler()
uploadHandler := handlers.NewUploadHandler()
r.Use(middlewares.CORSMiddleware())
r.Static("/uploads", "./uploads")
api := r.Group("/api")
{
api.GET("/installed", authHandler.CheckInstalled)
api.POST("/install", authHandler.Install)
auth := api.Group("/auth")
{
auth.POST("/register", authHandler.Register)
auth.POST("/login", authHandler.Login)
auth.POST("/forgot-password", authHandler.ForgotPassword)
auth.POST("/reset-password", authHandler.ResetPassword)
auth.GET("/verify-email", authHandler.VerifyEmail)
}
articles := api.Group("/articles")
{
articles.GET("", articleHandler.List)
articles.GET("/:id", articleHandler.GetByID)
}
api.GET("/categories", categoryHandler.List)
api.GET("/brands", brandHandler.List)
api.GET("/products", productHandler.List)
api.GET("/products/:id", productHandler.GetByID)
api.GET("/lotteries", lotteryHandler.List)
api.GET("/lotteries/:id", lotteryHandler.GetByID)
api.GET("/settings/public", systemHandler.GetPublicSettings)
authed := api.Group("")
authed.Use(middlewares.AuthMiddleware())
{
authed.POST("/upload", uploadHandler.UploadImage)
authed.POST("/upload/multiple", uploadHandler.UploadMultiple)
authed.DELETE("/upload/:filename", uploadHandler.DeleteImage)
users := authed.Group("/users")
{
users.GET("/profile", authHandler.GetProfile)
users.PUT("/profile", authHandler.UpdateProfile)
users.PUT("/password", authHandler.ChangePassword)
users.POST("/send-verify-code", authHandler.SendVerifyCode)
}
addresses := authed.Group("/users/addresses")
{
addresses.GET("", addressHandler.List)
addresses.POST("", addressHandler.Create)
addresses.PUT("/:id", addressHandler.Update)
addresses.DELETE("/:id", addressHandler.Delete)
addresses.PUT("/:id/default", addressHandler.SetDefault)
}
carts := authed.Group("/cart")
{
carts.GET("", cartHandler.List)
carts.POST("", cartHandler.Add)
carts.PUT("/:id", cartHandler.Update)
carts.DELETE("/:id", cartHandler.Delete)
}
orders := authed.Group("/orders")
{
orders.GET("", orderHandler.List)
orders.GET("/:id", orderHandler.GetByID)
orders.POST("", orderHandler.Create)
orders.POST("/:id/refund", orderHandler.Refund)
}
lotteries := authed.Group("/lotteries")
{
lotteries.POST("/:id/register", lotteryHandler.Register)
}
tickets := authed.Group("/tickets")
{
tickets.GET("", ticketHandler.List)
tickets.POST("", ticketHandler.Create)
tickets.GET("/:id", ticketHandler.GetByID)
}
supplier := authed.Group("/supplier")
supplier.Use(middlewares.RoleMiddleware("supplier", "admin"))
{
supplier.GET("/orders", orderHandler.List)
supplier.GET("/orders/:id", orderHandler.GetByID)
supplier.PUT("/orders/:id/confirm", orderHandler.ConfirmOrder)
supplier.PUT("/orders/:id/ship", orderHandler.ShipOrder)
supplier.GET("/inventory", inventoryHandler.SupplierList)
supplier.PUT("/inventory/:id", inventoryHandler.Update)
}
admin := authed.Group("/admin")
admin.Use(middlewares.RoleMiddleware("admin"))
{
admin.GET("/stats", systemHandler.GetStats)
admin.GET("/users", func(c *gin.Context) {
var users []models.User
utils.DB.Select("id, username, email, role, purchase_credits, is_active, created_at").Find(&users)
c.JSON(200, gin.H{"data": users})
})
adminCategories := admin.Group("/categories")
{
adminCategories.POST("", categoryHandler.Create)
adminCategories.PUT("/:id", categoryHandler.Update)
adminCategories.DELETE("/:id", categoryHandler.Delete)
}
adminBrands := admin.Group("/brands")
{
adminBrands.GET("", brandHandler.List)
adminBrands.POST("", brandHandler.Create)
adminBrands.PUT("/:id", brandHandler.Update)
adminBrands.DELETE("/:id", brandHandler.Delete)
}
adminProducts := admin.Group("/products")
{
adminProducts.GET("", productHandler.AdminList)
adminProducts.POST("", productHandler.Create)
adminProducts.PUT("/:id", productHandler.Update)
adminProducts.DELETE("/:id", productHandler.Delete)
adminProducts.POST("/:id/custom-fields", productHandler.AddCustomField)
}
adminOrders := admin.Group("/orders")
{
adminOrders.GET("", orderHandler.AdminList)
adminOrders.GET("/export", orderHandler.Export)
adminOrders.PUT("/:id/refund", orderHandler.ProcessRefund)
}
adminLotteries := admin.Group("/lotteries")
{
adminLotteries.POST("", lotteryHandler.Create)
adminLotteries.PUT("/:id", lotteryHandler.Update)
adminLotteries.DELETE("/:id", lotteryHandler.Delete)
adminLotteries.POST("/:id/prizes", lotteryHandler.AddPrize)
adminLotteries.POST("/:id/draw", lotteryHandler.Draw)
}
adminTickets := admin.Group("/tickets")
{
adminTickets.GET("", ticketHandler.AdminList)
adminTickets.PUT("/:id", ticketHandler.Update)
adminTickets.PUT("/:id/assign", ticketHandler.Assign)
}
admin.GET("/settings", systemHandler.GetSettings)
admin.PUT("/settings", systemHandler.UpdateSettings)
admin.PUT("/settings/smtp", systemHandler.UpdateSMTP)
admin.PUT("/settings/payment", systemHandler.UpdatePayment)
adminSuppliers := admin.Group("/suppliers")
{
adminSuppliers.GET("", supplierHandler.List)
adminSuppliers.POST("", supplierHandler.Create)
adminSuppliers.PUT("/:id", supplierHandler.Update)
adminSuppliers.DELETE("/:id", supplierHandler.Delete)
adminSuppliers.POST("/:id/authorize", supplierHandler.Authorize)
}
admin.GET("/inventory", inventoryHandler.AdminList)
adminArticles := admin.Group("/articles")
{
adminArticles.GET("", articleHandler.AdminList)
adminArticles.POST("", articleHandler.Create)
adminArticles.PUT("/:id", articleHandler.Update)
adminArticles.DELETE("/:id", articleHandler.Delete)
adminArticles.PUT("/:id/pin", articleHandler.TogglePin)
}
}
}
}
}
+98
View File
@@ -0,0 +1,98 @@
package config
import (
"os"
"github.com/joho/godotenv"
)
type Config struct {
Server ServerConfig
Database DatabaseConfig
Redis RedisConfig
JWT JWTConfig
SMTP SMTPConfig
}
type ServerConfig struct {
Port string
Mode string
}
type DatabaseConfig struct {
Driver string
Host string
Port string
User string
Password string
DBName string
SSLMode string
FilePath string
}
type RedisConfig struct {
Host string
Port string
Password string
DB int
}
type JWTConfig struct {
Secret string
ExpireHour int
}
type SMTPConfig struct {
Host string
Port string
User string
Password string
From string
}
var AppConfig *Config
func Load() {
godotenv.Load()
AppConfig = &Config{
Server: ServerConfig{
Port: getEnv("SERVER_PORT", "8080"),
Mode: getEnv("SERVER_MODE", "debug"),
},
Database: DatabaseConfig{
Driver: getEnv("DB_DRIVER", "sqlite"),
Host: getEnv("DB_HOST", "localhost"),
Port: getEnv("DB_PORT", "5432"),
User: getEnv("DB_USER", "postgres"),
Password: getEnv("DB_PASSWORD", "postgres"),
DBName: getEnv("DB_NAME", "sale"),
SSLMode: getEnv("DB_SSLMODE", "disable"),
FilePath: getEnv("DB_FILEPATH", "sale.db"),
},
Redis: RedisConfig{
Host: getEnv("REDIS_HOST", "localhost"),
Port: getEnv("REDIS_PORT", "6379"),
Password: getEnv("REDIS_PASSWORD", ""),
DB: 0,
},
JWT: JWTConfig{
Secret: getEnv("JWT_SECRET", "sale-secret-key"),
ExpireHour: 72,
},
SMTP: SMTPConfig{
Host: getEnv("SMTP_HOST", ""),
Port: getEnv("SMTP_PORT", "587"),
User: getEnv("SMTP_USER", ""),
Password: getEnv("SMTP_PASSWORD", ""),
From: getEnv("SMTP_FROM", ""),
},
}
}
func getEnv(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {
return value
}
return defaultValue
}
+23
View File
@@ -0,0 +1,23 @@
package models
import (
"time"
)
type Address struct {
ID uint `gorm:"primaryKey" json:"id"`
UserID uint `gorm:"index;not null" json:"user_id"`
Name string `gorm:"size:50;not null" json:"name"`
Phone string `gorm:"size:20;not null" json:"phone"`
Province string `gorm:"size:50" json:"province"`
City string `gorm:"size:50" json:"city"`
District string `gorm:"size:50" json:"district"`
Address string `gorm:"type:text;not null" json:"address"`
IsDefault bool `gorm:"default:false" json:"is_default"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
func (Address) TableName() string {
return "addresses"
}
+27
View File
@@ -0,0 +1,27 @@
package models
import (
"time"
"gorm.io/gorm"
)
type Article struct {
ID uint `gorm:"primaryKey" json:"id"`
Title string `gorm:"size:255;not null" json:"title"`
Content string `gorm:"type:text;not null" json:"content"`
Summary string `gorm:"size:500" json:"summary"`
CoverImage string `gorm:"size:500" json:"cover_image"`
IsPinned bool `gorm:"default:false" json:"is_pinned"`
IsPublished bool `gorm:"default:true" json:"is_published"`
SortOrder int `gorm:"default:0" json:"sort_order"`
AuthorID *uint `json:"author_id"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
Author *User `gorm:"foreignKey:AuthorID" json:"author,omitempty"`
}
func (Article) TableName() string {
return "articles"
}
+22
View File
@@ -0,0 +1,22 @@
package models
import (
"time"
"gorm.io/gorm"
)
type Brand struct {
ID uint `gorm:"primaryKey" json:"id"`
Name string `gorm:"size:255;not null;uniqueIndex" json:"name"`
Description string `json:"description"`
Logo string `json:"logo"`
SortOrder int `gorm:"default:0" json:"sort_order"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}
func (Brand) TableName() string {
return "brands"
}
+19
View File
@@ -0,0 +1,19 @@
package models
import (
"time"
)
type Cart struct {
ID uint `gorm:"primaryKey" json:"id"`
UserID uint `gorm:"index;not null" json:"user_id"`
ProductID uint `gorm:"index;not null" json:"product_id"`
Quantity int `gorm:"not null" json:"quantity"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Product Product `json:"product,omitempty"`
}
func (Cart) TableName() string {
return "carts"
}
+29
View File
@@ -0,0 +1,29 @@
package models
import (
"time"
"gorm.io/gorm"
)
type Category struct {
ID uint `gorm:"primaryKey" json:"id"`
Name string `gorm:"size:100;not null" json:"name"`
Description string `json:"description"`
ParentID *uint `json:"parent_id"`
MinAmount *float64 `json:"min_amount"`
MaxAmount *float64 `json:"max_amount"`
MinQuantity *int `json:"min_quantity"`
MaxQuantity *int `json:"max_quantity"`
MinWeight *float64 `json:"min_weight"`
MaxWeight *float64 `json:"max_weight"`
SortOrder int `gorm:"default:0" json:"sort_order"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
Children []Category `gorm:"foreignKey:ParentID" json:"children,omitempty"`
}
func (Category) TableName() string {
return "categories"
}
+17
View File
@@ -0,0 +1,17 @@
package models
import (
"time"
)
type Inventory struct {
ID uint `gorm:"primaryKey" json:"id"`
ProductID uint `gorm:"index;not null" json:"product_id"`
SupplierID uint `gorm:"index;not null" json:"supplier_id"`
Quantity int `gorm:"default:0" json:"quantity"`
UpdatedAt time.Time `json:"updated_at"`
}
func (Inventory) TableName() string {
return "inventory"
}
+80
View File
@@ -0,0 +1,80 @@
package models
import (
"time"
"gorm.io/gorm"
)
type Lottery struct {
ID uint `gorm:"primaryKey" json:"id"`
Name string `gorm:"size:100;not null" json:"name"`
Description string `json:"description"`
StartTime time.Time `json:"start_time"`
EndTime time.Time `json:"end_time"`
Cycle string `gorm:"size:20" json:"cycle"`
DailyQuota *int `json:"daily_quota"`
TotalQuota *int `json:"total_quota"`
RegistrationValidity *int `json:"registration_validity"`
IsActive bool `gorm:"default:true" json:"is_active"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
Prizes []LotteryPrize `json:"prizes,omitempty"`
}
func (Lottery) TableName() string {
return "lotteries"
}
type LotteryPrize struct {
ID uint `gorm:"primaryKey" json:"id"`
LotteryID uint `gorm:"index;not null" json:"lottery_id"`
Name string `gorm:"size:100;not null" json:"name"`
Type string `gorm:"size:20;not null" json:"type"`
Quantity int `gorm:"not null" json:"quantity"`
Weight int `gorm:"default:1" json:"weight"`
CreditReward *int `json:"credit_reward"`
DrawMode string `gorm:"size:20;default:'random'" json:"draw_mode"`
}
func (LotteryPrize) TableName() string {
return "lottery_prizes"
}
type LotteryParticipant struct {
ID uint `gorm:"primaryKey" json:"id"`
LotteryID uint `gorm:"index;not null" json:"lottery_id"`
UserID uint `gorm:"index;not null" json:"user_id"`
PurchaseWeight int `gorm:"default:0" json:"purchase_weight"`
RegisteredAt time.Time `json:"registered_at"`
}
func (LotteryParticipant) TableName() string {
return "lottery_participants"
}
type LotteryWinner struct {
ID uint `gorm:"primaryKey" json:"id"`
LotteryID uint `gorm:"index;not null" json:"lottery_id"`
PrizeID uint `gorm:"index;not null" json:"prize_id"`
UserID uint `gorm:"index;not null" json:"user_id"`
DrawnAt time.Time `json:"drawn_at"`
}
func (LotteryWinner) TableName() string {
return "lottery_winners"
}
const (
PrizeTypePhysical = "physical"
PrizeTypeVirtual = "virtual"
PrizeTypeCredit = "credit"
DrawModeRandom = "random"
DrawModeWeight = "weight"
CycleDaily = "daily"
CycleWeekly = "weekly"
CycleMonthly = "monthly"
)
+70
View File
@@ -0,0 +1,70 @@
package models
import (
"time"
"gorm.io/gorm"
)
type Order struct {
ID uint `gorm:"primaryKey" json:"id"`
UserID uint `gorm:"index;not null" json:"user_id"`
SupplierID *uint `gorm:"index" json:"supplier_id"`
Subtotal float64 `gorm:"type:decimal(10,2);not null" json:"subtotal"`
ShippingFee float64 `gorm:"type:decimal(10,2);default:0" json:"shipping_fee"`
ServiceFee float64 `gorm:"type:decimal(10,2);default:0" json:"service_fee"`
Tax float64 `gorm:"type:decimal(10,2);default:0" json:"tax"`
TotalAmount float64 `gorm:"type:decimal(10,2);not null" json:"total_amount"`
RefundAmount *float64 `gorm:"type:decimal(10,2)" json:"refund_amount"`
RefundStatus string `gorm:"size:20" json:"refund_status"`
RefundReason string `json:"refund_reason"`
Status string `gorm:"size:20;not null;default:'pending_payment'" json:"status"`
ShippingAddressID *uint `json:"shipping_address_id"`
TrackingNumber string `gorm:"size:100" json:"tracking_number"`
ShippingPhoto string `gorm:"type:text" json:"shipping_photo"`
ExpressPhoto string `gorm:"type:text" json:"express_photo"`
CustomsPhoto string `gorm:"type:text" json:"customs_photo"`
PaymentMethod string `gorm:"size:50" json:"payment_method"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
OrderItems []OrderItem `json:"order_items,omitempty"`
ShippingAddress *Address `json:"shipping_address,omitempty"`
User User `json:"user,omitempty"`
}
func (Order) TableName() string {
return "orders"
}
type OrderItem struct {
ID uint `gorm:"primaryKey" json:"id"`
OrderID uint `gorm:"index;not null" json:"order_id"`
ProductID uint `gorm:"index;not null" json:"product_id"`
Quantity int `gorm:"not null" json:"quantity"`
Price float64 `gorm:"type:decimal(10,2);not null" json:"price"`
Product Product `json:"product,omitempty"`
}
func (OrderItem) TableName() string {
return "order_items"
}
const (
OrderStatusPendingPayment = "pending_payment"
OrderStatusPendingConfirm = "pending_confirm"
OrderStatusPendingShip = "pending_ship"
OrderStatusShipped = "shipped"
OrderStatusCompleted = "completed"
OrderStatusRefunding = "refunding"
OrderStatusRefunded = "refunded"
OrderStatusCancelled = "cancelled"
)
const (
RefundStatusNone = ""
RefundStatusPending = "pending"
RefundStatusApproved = "approved"
RefundStatusRejected = "rejected"
RefundStatusCompleted = "completed"
)
+48
View File
@@ -0,0 +1,48 @@
package models
import (
"time"
"gorm.io/gorm"
)
type Product struct {
ID uint `gorm:"primaryKey" json:"id"`
Name string `gorm:"size:255;not null" json:"name"`
Description string `json:"description"`
Price float64 `gorm:"type:decimal(10,2);not null" json:"price"`
MinPurchase int `gorm:"default:1" json:"min_purchase"`
MaxPurchase *int `json:"max_purchase"`
MinWeight *float64 `json:"min_weight"`
MaxWeight *float64 `json:"max_weight"`
MinAmount *float64 `json:"min_amount"`
MaxAmount *float64 `json:"max_amount"`
RequireCredit bool `gorm:"default:false" json:"require_credit"`
CreditCost int `gorm:"default:0" json:"credit_cost"`
CreditReward int `gorm:"default:0" json:"credit_reward"`
Images string `gorm:"type:text" json:"images"`
IsActive bool `gorm:"default:true" json:"is_active"`
BrandID *uint `json:"brand_id"`
Brand *Brand `json:"brand,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
Categories []Category `gorm:"many2many:product_categories;" json:"categories,omitempty"`
CustomFields []ProductCustomField `json:"custom_fields,omitempty"`
}
func (Product) TableName() string {
return "products"
}
type ProductCustomField struct {
ID uint `gorm:"primaryKey" json:"id"`
ProductID uint `gorm:"index;not null" json:"product_id"`
FieldName string `gorm:"size:100;not null" json:"field_name"`
FieldValue string `json:"field_value"`
CreatedAt time.Time `json:"created_at"`
}
func (ProductCustomField) TableName() string {
return "product_custom_fields"
}
+32
View File
@@ -0,0 +1,32 @@
package models
import (
"time"
)
type SystemSetting struct {
ID uint `gorm:"primaryKey" json:"id"`
Key string `gorm:"uniqueIndex;size:100;not null" json:"key"`
Value string `gorm:"type:text" json:"value"`
UpdatedAt time.Time `json:"updated_at"`
}
func (SystemSetting) TableName() string {
return "system_settings"
}
type SupplierAuthorization struct {
ID uint `gorm:"primaryKey" json:"id"`
SupplierID uint `gorm:"index;not null" json:"supplier_id"`
Type string `gorm:"size:20;not null" json:"type"`
RefID uint `gorm:"not null" json:"ref_id"`
}
func (SupplierAuthorization) TableName() string {
return "supplier_authorizations"
}
const (
AuthTypeCategory = "category"
AuthTypeProduct = "product"
)
+39
View File
@@ -0,0 +1,39 @@
package models
import (
"time"
"gorm.io/gorm"
)
type Ticket struct {
ID uint `gorm:"primaryKey" json:"id"`
UserID uint `gorm:"index;not null" json:"user_id"`
Title string `gorm:"size:200;not null" json:"title"`
Content string `gorm:"type:text;not null" json:"content"`
Category string `gorm:"size:50" json:"category"`
Status string `gorm:"size:20;not null;default:'pending'" json:"status"`
AssignedTo *uint `json:"assigned_to"`
Reply string `gorm:"type:text" json:"reply"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
User User `gorm:"foreignKey:UserID" json:"user,omitempty"`
Assignee *User `gorm:"foreignKey:AssignedTo" json:"assignee,omitempty"`
}
func (Ticket) TableName() string {
return "tickets"
}
const (
TicketStatusPending = "pending"
TicketStatusProcessing = "processing"
TicketStatusResolved = "resolved"
TicketStatusClosed = "closed"
TicketCategoryOrder = "order"
TicketCategoryAccount = "account"
TicketCategoryProduct = "product"
TicketCategoryOther = "other"
)
+27
View File
@@ -0,0 +1,27 @@
package models
import (
"time"
"gorm.io/gorm"
)
type User struct {
ID uint `gorm:"primaryKey" json:"id"`
Username string `gorm:"uniqueIndex;size:50;not null" json:"username"`
Email string `gorm:"uniqueIndex;size:100;not null" json:"email"`
PasswordHash string `gorm:"size:255;not null" json:"-"`
Role string `gorm:"size:20;not null;default:'user'" json:"role"`
PurchaseCredits int `gorm:"default:0" json:"purchase_credits"`
InviteCode string `gorm:"uniqueIndex;size:20" json:"invite_code"`
ReferredBy *uint `json:"referred_by"`
EmailVerified bool `gorm:"default:false" json:"email_verified"`
IsActive bool `gorm:"default:true" json:"is_active"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}
func (User) TableName() string {
return "users"
}
+21
View File
@@ -0,0 +1,21 @@
package schemas
type CreateAddressRequest struct {
Name string `json:"name" binding:"required"`
Phone string `json:"phone" binding:"required"`
Province string `json:"province"`
City string `json:"city"`
District string `json:"district"`
Address string `json:"address" binding:"required"`
IsDefault bool `json:"is_default"`
}
type UpdateAddressRequest struct {
Name *string `json:"name"`
Phone *string `json:"phone"`
Province *string `json:"province"`
City *string `json:"city"`
District *string `json:"district"`
Address *string `json:"address"`
IsDefault *bool `json:"is_default"`
}
+21
View File
@@ -0,0 +1,21 @@
package schemas
type CreateArticleRequest struct {
Title string `json:"title" binding:"required"`
Content string `json:"content" binding:"required"`
Summary string `json:"summary"`
CoverImage string `json:"cover_image"`
IsPinned bool `json:"is_pinned"`
IsPublished bool `json:"is_published"`
SortOrder int `json:"sort_order"`
}
type UpdateArticleRequest struct {
Title *string `json:"title"`
Content *string `json:"content"`
Summary *string `json:"summary"`
CoverImage *string `json:"cover_image"`
IsPinned *bool `json:"is_pinned"`
IsPublished *bool `json:"is_published"`
SortOrder *int `json:"sort_order"`
}
+54
View File
@@ -0,0 +1,54 @@
package schemas
type RegisterRequest struct {
Email string `json:"email" binding:"required,email"`
Username string `json:"username" binding:"required,min=2,max=50"`
Password string `json:"password" binding:"required,min=6"`
InviteCode string `json:"invite_code"`
}
type LoginRequest struct {
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required"`
}
type ForgotPasswordRequest struct {
Email string `json:"email" binding:"required,email"`
}
type ResetPasswordRequest struct {
Email string `json:"email" binding:"required,email"`
Code string `json:"code" binding:"required"`
Password string `json:"password" binding:"required,min=6"`
}
type VerifyEmailRequest struct {
Email string `json:"email" binding:"required,email"`
Code string `json:"code" binding:"required"`
}
type ChangePasswordRequest struct {
OldPassword string `json:"old_password" binding:"required"`
NewPassword string `json:"new_password" binding:"required,min=6"`
}
type AuthResponse struct {
Token string `json:"token"`
User UserResponse `json:"user"`
}
type UserResponse struct {
ID uint `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
Role string `json:"role"`
PurchaseCredits int `json:"purchase_credits"`
InviteCode string `json:"invite_code"`
EmailVerified bool `json:"email_verified"`
}
type InstallRequest struct {
Username string `json:"username" binding:"required,min=2,max=50"`
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required,min=6"`
}
+37
View File
@@ -0,0 +1,37 @@
package schemas
type CreateLotteryRequest struct {
Name string `json:"name" binding:"required"`
Description string `json:"description"`
StartTime string `json:"start_time" binding:"required"`
EndTime string `json:"end_time" binding:"required"`
Cycle string `json:"cycle"`
DailyQuota *int `json:"daily_quota"`
TotalQuota *int `json:"total_quota"`
RegistrationValidity *int `json:"registration_validity"`
}
type UpdateLotteryRequest struct {
Name *string `json:"name"`
Description *string `json:"description"`
StartTime *string `json:"start_time"`
EndTime *string `json:"end_time"`
Cycle *string `json:"cycle"`
DailyQuota *int `json:"daily_quota"`
TotalQuota *int `json:"total_quota"`
RegistrationValidity *int `json:"registration_validity"`
IsActive *bool `json:"is_active"`
}
type AddLotteryPrizeRequest struct {
Name string `json:"name" binding:"required"`
Type string `json:"type" binding:"required,oneof=physical virtual credit"`
Quantity int `json:"quantity" binding:"required,min=1"`
Weight int `json:"weight"`
CreditReward *int `json:"credit_reward"`
DrawMode string `json:"draw_mode"`
}
type RegisterLotteryRequest struct {
LotteryID uint `json:"lottery_id" binding:"required"`
}
+30
View File
@@ -0,0 +1,30 @@
package schemas
type AddToCartRequest struct {
ProductID uint `json:"product_id" binding:"required"`
Quantity int `json:"quantity" binding:"required,min=1"`
}
type UpdateCartRequest struct {
Quantity int `json:"quantity" binding:"required,min=1"`
}
type CreateOrderRequest struct {
ShippingAddressID uint `json:"shipping_address_id" binding:"required"`
PaymentMethod string `json:"payment_method"`
}
type RefundRequest struct {
Reason string `json:"reason" binding:"required"`
}
type ShipOrderRequest struct {
TrackingNumber string `json:"tracking_number" binding:"required"`
ShippingPhoto string `json:"shipping_photo"`
ExpressPhoto string `json:"express_photo"`
CustomsPhoto string `json:"customs_photo"`
}
type ProcessRefundRequest struct {
Status string `json:"status" binding:"required,oneof=approved rejected"`
}
+85
View File
@@ -0,0 +1,85 @@
package schemas
type CreateCategoryRequest struct {
Name string `json:"name" binding:"required"`
Description string `json:"description"`
ParentID *uint `json:"parent_id"`
MinAmount *float64 `json:"min_amount"`
MaxAmount *float64 `json:"max_amount"`
MinQuantity *int `json:"min_quantity"`
MaxQuantity *int `json:"max_quantity"`
MinWeight *float64 `json:"min_weight"`
MaxWeight *float64 `json:"max_weight"`
SortOrder int `json:"sort_order"`
}
type UpdateCategoryRequest struct {
Name *string `json:"name"`
Description *string `json:"description"`
ParentID *uint `json:"parent_id"`
MinAmount *float64 `json:"min_amount"`
MaxAmount *float64 `json:"max_amount"`
MinQuantity *int `json:"min_quantity"`
MaxQuantity *int `json:"max_quantity"`
MinWeight *float64 `json:"min_weight"`
MaxWeight *float64 `json:"max_weight"`
SortOrder *int `json:"sort_order"`
}
type CreateProductRequest struct {
Name string `json:"name" binding:"required"`
Description string `json:"description"`
Price float64 `json:"price" binding:"required"`
MinPurchase int `json:"min_purchase"`
MaxPurchase *int `json:"max_purchase"`
MinWeight *float64 `json:"min_weight"`
MaxWeight *float64 `json:"max_weight"`
MinAmount *float64 `json:"min_amount"`
MaxAmount *float64 `json:"max_amount"`
RequireCredit bool `json:"require_credit"`
CreditCost int `json:"credit_cost"`
CreditReward int `json:"credit_reward"`
Images string `json:"images"`
BrandID *uint `json:"brand_id"`
CategoryIDs []uint `json:"category_ids"`
CustomFields []CustomFieldRequest `json:"custom_fields"`
}
type UpdateProductRequest struct {
Name *string `json:"name"`
Description *string `json:"description"`
Price *float64 `json:"price"`
MinPurchase *int `json:"min_purchase"`
MaxPurchase *int `json:"max_purchase"`
MinWeight *float64 `json:"min_weight"`
MaxWeight *float64 `json:"max_weight"`
MinAmount *float64 `json:"min_amount"`
MaxAmount *float64 `json:"max_amount"`
RequireCredit *bool `json:"require_credit"`
CreditCost *int `json:"credit_cost"`
CreditReward *int `json:"credit_reward"`
Images *string `json:"images"`
IsActive *bool `json:"is_active"`
BrandID *uint `json:"brand_id"`
CategoryIDs []uint `json:"category_ids"`
CustomFields []CustomFieldRequest `json:"custom_fields"`
}
type CustomFieldRequest struct {
FieldName string `json:"field_name" binding:"required"`
FieldValue string `json:"field_value"`
}
type PaginationRequest struct {
Page int `form:"page,default=1"`
PageSize int `form:"page_size,default=20"`
}
type ProductListRequest struct {
PaginationRequest
CategoryID *uint `form:"category_id"`
BrandID *uint `form:"brand_id"`
Keyword string `form:"keyword"`
MinPrice *float64 `form:"min_price"`
MaxPrice *float64 `form:"max_price"`
}
+20
View File
@@ -0,0 +1,20 @@
package schemas
type UpdateSettingsRequest struct {
Settings map[string]string `json:"settings" binding:"required"`
}
type AuthorizeSupplierRequest struct {
Type string `json:"type" binding:"required,oneof=category product"`
RefID uint `json:"ref_id" binding:"required"`
}
type AddSupplierRequest struct {
Username string `json:"username" binding:"required"`
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required,min=6"`
}
type UpdateInventoryRequest struct {
Quantity int `json:"quantity" binding:"required,min=0"`
}
+13
View File
@@ -0,0 +1,13 @@
package schemas
type CreateTicketRequest struct {
Title string `json:"title" binding:"required"`
Content string `json:"content" binding:"required"`
Category string `json:"category"`
}
type UpdateTicketRequest struct {
Status *string `json:"status"`
AssignedTo *uint `json:"assigned_to"`
Reply *string `json:"reply"`
}
+79
View File
@@ -0,0 +1,79 @@
package utils
import (
"fmt"
"log"
"sale/internal/config"
"sale/internal/models"
sqlite "github.com/glebarez/sqlite"
"gorm.io/driver/mysql"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
var DB *gorm.DB
func InitDB() {
var err error
cfg := config.AppConfig.Database
switch cfg.Driver {
case "mysql":
dsn := fmt.Sprintf(
"%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local",
cfg.User, cfg.Password, cfg.Host, cfg.Port, cfg.DBName,
)
DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{
Logger: logger.Default.LogMode(logger.Info),
})
case "postgres":
dsn := fmt.Sprintf(
"host=%s port=%s user=%s password=%s dbname=%s sslmode=%s",
cfg.Host, cfg.Port, cfg.User, cfg.Password, cfg.DBName, cfg.SSLMode,
)
DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{
Logger: logger.Default.LogMode(logger.Info),
})
default:
DB, err = gorm.Open(sqlite.Open(cfg.FilePath), &gorm.Config{
Logger: logger.Default.LogMode(logger.Info),
})
}
if err != nil {
log.Fatalf("Failed to connect to database: %v", err)
}
log.Printf("Database connected successfully (driver: %s)", cfg.Driver)
}
func AutoMigrate() {
err := DB.AutoMigrate(
&models.User{},
&models.Category{},
&models.Brand{},
&models.Product{},
&models.ProductCustomField{},
&models.Inventory{},
&models.Cart{},
&models.Order{},
&models.OrderItem{},
&models.Address{},
&models.Lottery{},
&models.LotteryPrize{},
&models.LotteryParticipant{},
&models.LotteryWinner{},
&models.Ticket{},
&models.SystemSetting{},
&models.SupplierAuthorization{},
&models.Article{},
)
if err != nil {
log.Fatalf("Failed to migrate database: %v", err)
}
log.Println("Database migrated successfully")
}
+50
View File
@@ -0,0 +1,50 @@
package utils
import (
"fmt"
"net/smtp"
"sale/internal/config"
)
func SendEmail(to, subject, body string) error {
cfg := config.AppConfig.SMTP
if cfg.Host == "" {
return fmt.Errorf("SMTP not configured")
}
auth := smtp.PlainAuth("", cfg.User, cfg.Password, cfg.Host)
msg := fmt.Sprintf(
"From: %s\r\nTo: %s\r\nSubject: %s\r\nContent-Type: text/html; charset=UTF-8\r\n\r\n%s",
cfg.From, to, subject, body,
)
return smtp.SendMail(
fmt.Sprintf("%s:%s", cfg.Host, cfg.Port),
auth,
cfg.From,
[]string{to},
[]byte(msg),
)
}
func SendVerifyEmail(to, code string) error {
subject := "邮箱验证码"
body := fmt.Sprintf(`
<h2>邮箱验证</h2>
<p>您的验证码是:<strong style="font-size:24px;color:#1890ff;">%s</strong></p>
<p>验证码有效期为30分钟,请尽快使用。</p>
`, code)
return SendEmail(to, subject, body)
}
func SendResetPasswordEmail(to, code string) error {
subject := "重置密码验证码"
body := fmt.Sprintf(`
<h2>重置密码</h2>
<p>您的验证码是:<strong style="font-size:24px;color:#1890ff;">%s</strong></p>
<p>验证码有效期为30分钟,请尽快使用。</p>
`, code)
return SendEmail(to, subject, body)
}
+46
View File
@@ -0,0 +1,46 @@
package utils
import (
"errors"
"time"
"sale/internal/config"
"github.com/golang-jwt/jwt/v5"
)
type Claims struct {
UserID uint `json:"user_id"`
Role string `json:"role"`
jwt.RegisteredClaims
}
func GenerateToken(userID uint, role string) (string, error) {
claims := Claims{
UserID: userID,
Role: role,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(config.AppConfig.JWT.ExpireHour) * time.Hour)),
IssuedAt: jwt.NewNumericDate(time.Now()),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(config.AppConfig.JWT.Secret))
}
func ParseToken(tokenString string) (*Claims, error) {
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
return []byte(config.AppConfig.JWT.Secret), nil
})
if err != nil {
return nil, err
}
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
return claims, nil
}
return nil, errors.New("invalid token")
}
+15
View File
@@ -0,0 +1,15 @@
package utils
import (
"golang.org/x/crypto/bcrypt"
)
func HashPassword(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
return string(bytes), err
}
func CheckPassword(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}
+26
View File
@@ -0,0 +1,26 @@
package utils
import (
"crypto/rand"
"math/big"
)
func GenerateInviteCode() string {
const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
code := make([]byte, 8)
for i := range code {
n, _ := rand.Int(rand.Reader, big.NewInt(int64(len(charset))))
code[i] = charset[n.Int64()]
}
return string(code)
}
func GenerateVerifyCode() string {
const charset = "0123456789"
code := make([]byte, 6)
for i := range code {
n, _ := rand.Int(rand.Reader, big.NewInt(int64(len(charset))))
code[i] = charset[n.Int64()]
}
return string(code)
}
+63
View File
@@ -0,0 +1,63 @@
import sqlite3
from datetime import datetime, timedelta
import random
conn = sqlite3.connect('E:/Code/sale/backend/sale.db')
cursor = conn.cursor()
articles = [
('新品上市:2024春季限定系列发布', '我们很高兴地宣布2024春季限定系列正式上线,包含多款精选商品,欢迎选购。', 'https://picsum.photos/seed/art1/800/400'),
('购物指南:如何选择适合自己的商品', '本文将为您详细介绍如何根据个人需求选择最合适的商品,帮助您做出明智的购买决策。', 'https://picsum.photos/seed/art2/800/400'),
('会员福利升级通知', '尊敬的用户,我们已全面升级会员福利体系,现在注册即可享受更多专属优惠。', 'https://picsum.photos/seed/art3/800/400'),
('物流配送说明', '为了提供更好的购物体验,我们优化了物流配送流程,现在下单可享受更快的配送服务。', 'https://picsum.photos/seed/art4/800/400'),
('售后服务政策更新', '我们更新了售后服务政策,现在支持7天无理由退换货,让您购物更放心。', 'https://picsum.photos/seed/art5/800/400'),
('限时优惠活动预告', '本周五将开启限时优惠活动,多款热门商品参与折扣,敬请期待。', 'https://picsum.photos/seed/art6/800/400'),
('品牌故事:我们的初心', '了解我们的品牌故事,感受我们对品质的追求和对客户的承诺。', 'https://picsum.photos/seed/art7/800/400'),
('用户评价精选:听听他们怎么说', '我们收集了用户的真实评价,看看大家对我们商品和服务的反馈。', 'https://picsum.photos/seed/art8/800/400'),
('安全购物须知', '网络购物安全指南,教您如何保护个人信息,安全购物。', 'https://picsum.photos/seed/art9/800/400'),
('节日特惠活动即将开始', '节日特惠活动即将开启,超多优惠等你来抢,不要错过!', 'https://picsum.photos/seed/art10/800/400'),
]
lotteries = [
('新年大抽奖', '参与新年抽奖,赢取丰厚奖品', '2024-01-01 00:00:00', '2024-12-31 23:59:59'),
('春季幸运抽奖', '春季限定抽奖活动,好礼送不停', '2024-03-01 00:00:00', '2024-05-31 23:59:59'),
('会员专属抽奖', '会员专属抽奖活动,更多惊喜等你来', '2024-04-01 00:00:00', '2024-06-30 23:59:59'),
]
prizes = [
('一等奖:iPhone 15 Pro', 'product', 1, 1, None),
('二等奖:AirPods Pro', 'product', 3, 2, None),
('三等奖:100积分', 'credits', 10, 3, 100),
('参与奖:10积分', 'credits', 50, 1, 10),
]
print('插入资讯数据...')
for i, (title, summary, cover) in enumerate(articles):
content = f'<h2>{title}</h2><p>{summary}</p><p>这是文章的详细内容,包含了更多相关信息和说明。感谢您的阅读!</p>'
created_at = datetime.now() - timedelta(days=random.randint(1, 30))
is_pinned = 1 if i < 2 else 0
cursor.execute('''
INSERT INTO articles (title, summary, content, cover_image, is_pinned, author_id, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, 1, ?, ?)
''', (title, summary, content, cover, is_pinned, created_at.strftime('%Y-%m-%d %H:%M:%S'), created_at.strftime('%Y-%m-%d %H:%M:%S')))
print(f'已插入 {len(articles)} 条资讯')
print('插入抽奖数据...')
for name, description, start_time, end_time in lotteries:
cursor.execute('''
INSERT INTO lotteries (name, description, start_time, end_time, created_at, updated_at)
VALUES (?, ?, ?, ?, datetime('now'), datetime('now'))
''', (name, description, start_time, end_time))
lottery_id = cursor.lastrowid
for prize_name, prize_type, quantity, weight, credit_reward in prizes:
cursor.execute('''
INSERT INTO lottery_prizes (lottery_id, name, type, quantity, weight, credit_reward)
VALUES (?, ?, ?, ?, ?, ?)
''', (lottery_id, prize_name, prize_type, quantity, weight, credit_reward))
print(f'已插入 {len(lotteries)} 个抽奖活动')
conn.commit()
cursor.close()
conn.close()
print('完成!')
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Some files were not shown because too many files have changed in this diff Show More