Initial commit: 商品售卖网站
@@ -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
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
)
|
||||||
@@ -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=
|
||||||
@@ -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"})
|
||||||
|
}
|
||||||
@@ -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})
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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"})
|
||||||
|
}
|
||||||
@@ -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"})
|
||||||
|
}
|
||||||
@@ -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})
|
||||||
|
}
|
||||||
@@ -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})
|
||||||
|
}
|
||||||
@@ -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})
|
||||||
|
}
|
||||||
@@ -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"})
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
)
|
||||||
@@ -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"
|
||||||
|
)
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
)
|
||||||
@@ -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"
|
||||||
|
)
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
@@ -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"`
|
||||||
|
}
|
||||||
@@ -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"`
|
||||||
|
}
|
||||||
@@ -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"`
|
||||||
|
}
|
||||||
@@ -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"`
|
||||||
|
}
|
||||||
@@ -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"`
|
||||||
|
}
|
||||||
@@ -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"`
|
||||||
|
}
|
||||||
@@ -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"`
|
||||||
|
}
|
||||||
@@ -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"`
|
||||||
|
}
|
||||||
@@ -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")
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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")
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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('完成!')
|
||||||
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 8.4 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 8.2 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 8.1 KiB |
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 8.1 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 9.7 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 90 KiB |
|
After Width: | Height: | Size: 9.3 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 8.9 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 8.7 KiB |
|
After Width: | Height: | Size: 9.9 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 9.9 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 9.5 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 9.4 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 9.1 KiB |
|
After Width: | Height: | Size: 8.3 KiB |