第 0 阶段:项目初始化
This commit is contained in:
+24
@@ -0,0 +1,24 @@
|
||||
.snow
|
||||
.vscode
|
||||
.idea
|
||||
*.log
|
||||
*.tmp
|
||||
*.bak
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Go
|
||||
bin/
|
||||
coverage.out
|
||||
|
||||
# Node / Vue
|
||||
node_modules/
|
||||
dist/
|
||||
.vite/
|
||||
*.tsbuildinfo
|
||||
|
||||
# Docker / local data
|
||||
.docker-data/
|
||||
@@ -0,0 +1,30 @@
|
||||
# HFB Sys
|
||||
|
||||
哈夫币租号平台,面向《三角洲行动》账号租赁场景。
|
||||
|
||||
## 开发方式
|
||||
|
||||
- 后端:Go + Gin + GORM + MySQL + Redis
|
||||
- 前端:Vue 3 + TypeScript + Vite + Element Plus
|
||||
- 开发依赖:Docker Compose 启动 MySQL、Redis、MinIO
|
||||
|
||||
## 快速开始
|
||||
|
||||
```bash
|
||||
docker compose -f deploy/docker-compose.dev.yml up -d
|
||||
|
||||
cd backend
|
||||
cp .env.example .env
|
||||
go run ./cmd/api
|
||||
|
||||
cd ../frontend
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## 文档
|
||||
|
||||
- [项目计划](docs/project-plan.md)
|
||||
- [数据库设计](docs/database.md)
|
||||
- [接口规划](docs/api.md)
|
||||
- [业务规则](docs/business-rules.md)
|
||||
@@ -0,0 +1,11 @@
|
||||
APP_ENV=development
|
||||
APP_ADDR=:8080
|
||||
|
||||
MYSQL_DSN=hfb:secret@tcp(127.0.0.1:3306)/hfb_sys?charset=utf8mb4&parseTime=True&loc=Local
|
||||
REDIS_ADDR=127.0.0.1:6379
|
||||
REDIS_PASSWORD=
|
||||
REDIS_DB=0
|
||||
|
||||
JWT_SECRET=change-me
|
||||
STORAGE_ENDPOINT=http://127.0.0.1:9000
|
||||
STORAGE_BUCKET=hfb-sys
|
||||
@@ -0,0 +1,14 @@
|
||||
FROM golang:1.26-alpine AS build
|
||||
|
||||
WORKDIR /src
|
||||
COPY go.mod go.sum* ./
|
||||
RUN go mod download
|
||||
COPY . .
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -o /out/hfb-api ./cmd/api
|
||||
|
||||
FROM alpine:3.22
|
||||
|
||||
WORKDIR /app
|
||||
COPY --from=build /out/hfb-api /app/hfb-api
|
||||
EXPOSE 8080
|
||||
CMD ["/app/hfb-api"]
|
||||
@@ -0,0 +1,54 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"hfb_sys/backend/internal/config"
|
||||
"hfb_sys/backend/internal/router"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cfg := config.Load()
|
||||
logger, err := zap.NewProduction()
|
||||
if cfg.AppEnv == "development" {
|
||||
logger, err = zap.NewDevelopment()
|
||||
}
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer func() {
|
||||
_ = logger.Sync()
|
||||
}()
|
||||
|
||||
engine := router.New(cfg, logger)
|
||||
server := &http.Server{
|
||||
Addr: cfg.AppAddr,
|
||||
Handler: engine,
|
||||
ReadHeaderTimeout: 5 * time.Second,
|
||||
}
|
||||
|
||||
go func() {
|
||||
logger.Info("api server starting", zap.String("addr", cfg.AppAddr))
|
||||
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
logger.Fatal("api server failed", zap.Error(err))
|
||||
}
|
||||
}()
|
||||
|
||||
quit := make(chan os.Signal, 1)
|
||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-quit
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
if err := server.Shutdown(ctx); err != nil {
|
||||
logger.Fatal("api server shutdown failed", zap.Error(err))
|
||||
}
|
||||
logger.Info("api server stopped")
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
module hfb_sys/backend
|
||||
|
||||
go 1.26
|
||||
|
||||
require (
|
||||
github.com/gin-gonic/gin v1.11.0
|
||||
github.com/redis/go-redis/v9 v9.17.0
|
||||
go.uber.org/zap v1.27.0
|
||||
gorm.io/driver/mysql v1.6.0
|
||||
gorm.io/gorm v1.31.1
|
||||
)
|
||||
|
||||
require (
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/bytedance/sonic v1.14.0 // indirect
|
||||
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
|
||||
github.com/gin-contrib/sse v1.1.0 // 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.28.0 // indirect
|
||||
github.com/go-sql-driver/mysql v1.8.1 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/goccy/go-yaml v1.18.0 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/quic-go/qpack v0.5.1 // indirect
|
||||
github.com/quic-go/quic-go v0.54.0 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.3.0 // indirect
|
||||
go.uber.org/mock v0.5.0 // indirect
|
||||
go.uber.org/multierr v1.10.0 // indirect
|
||||
golang.org/x/arch v0.20.0 // indirect
|
||||
golang.org/x/crypto v0.42.0 // indirect
|
||||
golang.org/x/mod v0.27.0 // indirect
|
||||
golang.org/x/net v0.43.0 // indirect
|
||||
golang.org/x/sync v0.17.0 // indirect
|
||||
golang.org/x/sys v0.36.0 // indirect
|
||||
golang.org/x/text v0.29.0 // indirect
|
||||
golang.org/x/tools v0.36.0 // indirect
|
||||
google.golang.org/protobuf v1.36.9 // indirect
|
||||
)
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||
github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
|
||||
github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
|
||||
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
|
||||
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
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/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.10/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.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
|
||||
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
|
||||
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.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
|
||||
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
|
||||
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.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
|
||||
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/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/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
||||
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
||||
github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
|
||||
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
|
||||
github.com/redis/go-redis/v9 v9.17.0 h1:K6E+ZlYN95KSMmZeEQPbU/c++wfmEvfFB17yEAq/VhM=
|
||||
github.com/redis/go-redis/v9 v9.17.0/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
|
||||
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
|
||||
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
|
||||
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
|
||||
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
|
||||
golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
|
||||
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
|
||||
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
|
||||
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
|
||||
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
|
||||
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
||||
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
|
||||
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
|
||||
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
|
||||
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
|
||||
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
|
||||
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg=
|
||||
gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo=
|
||||
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
|
||||
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
||||
@@ -0,0 +1,58 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
AppEnv string
|
||||
AppAddr string
|
||||
MySQLDSN string
|
||||
RedisAddr string
|
||||
RedisPassword string
|
||||
RedisDB int
|
||||
JWTSecret string
|
||||
Storage StorageConfig
|
||||
}
|
||||
|
||||
type StorageConfig struct {
|
||||
Endpoint string
|
||||
Bucket string
|
||||
}
|
||||
|
||||
func Load() Config {
|
||||
return Config{
|
||||
AppEnv: getEnv("APP_ENV", "development"),
|
||||
AppAddr: getEnv("APP_ADDR", ":8080"),
|
||||
MySQLDSN: getEnv("MYSQL_DSN", "hfb:secret@tcp(127.0.0.1:3306)/hfb_sys?charset=utf8mb4&parseTime=True&loc=Local"),
|
||||
RedisAddr: getEnv("REDIS_ADDR", "127.0.0.1:6379"),
|
||||
RedisPassword: getEnv("REDIS_PASSWORD", ""),
|
||||
RedisDB: getEnvInt("REDIS_DB", 0),
|
||||
JWTSecret: getEnv("JWT_SECRET", "change-me"),
|
||||
Storage: StorageConfig{
|
||||
Endpoint: getEnv("STORAGE_ENDPOINT", "http://127.0.0.1:9000"),
|
||||
Bucket: getEnv("STORAGE_BUCKET", "hfb-sys"),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func getEnv(key, fallback string) string {
|
||||
value := os.Getenv(key)
|
||||
if value == "" {
|
||||
return fallback
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func getEnvInt(key string, fallback int) int {
|
||||
value := os.Getenv(key)
|
||||
if value == "" {
|
||||
return fallback
|
||||
}
|
||||
parsed, err := strconv.Atoi(value)
|
||||
if err != nil {
|
||||
return fallback
|
||||
}
|
||||
return parsed
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func OpenMySQL(dsn string) (*gorm.DB, error) {
|
||||
return gorm.Open(mysql.Open(dsn), &gorm.Config{})
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
type RedisConfig struct {
|
||||
Addr string
|
||||
Password string
|
||||
DB int
|
||||
}
|
||||
|
||||
func OpenRedis(ctx context.Context, cfg RedisConfig) (*redis.Client, error) {
|
||||
client := redis.NewClient(&redis.Options{
|
||||
Addr: cfg.Addr,
|
||||
Password: cfg.Password,
|
||||
DB: cfg.DB,
|
||||
})
|
||||
if err := client.Ping(ctx).Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type HealthHandler struct {
|
||||
startedAt time.Time
|
||||
}
|
||||
|
||||
func NewHealthHandler() *HealthHandler {
|
||||
return &HealthHandler{startedAt: time.Now()}
|
||||
}
|
||||
|
||||
func (h *HealthHandler) Check(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": "ok",
|
||||
"service": "hfb-api",
|
||||
"started_at": h.startedAt.Format(time.RFC3339),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func RequestLogger(logger *zap.Logger) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
start := time.Now()
|
||||
c.Next()
|
||||
|
||||
logger.Info("http request",
|
||||
zap.String("method", c.Request.Method),
|
||||
zap.String("path", c.Request.URL.Path),
|
||||
zap.Int("status", c.Writer.Status()),
|
||||
zap.Duration("latency", time.Since(start)),
|
||||
zap.String("client_ip", c.ClientIP()),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
# Auth Module
|
||||
|
||||
手机号短信登录、JWT、验证码限流。
|
||||
@@ -0,0 +1,3 @@
|
||||
# Dispute Module
|
||||
|
||||
纠纷举证、客服仲裁、裁决落账。
|
||||
@@ -0,0 +1,3 @@
|
||||
# Listing Module
|
||||
|
||||
账号发布、资产快照、审核、上下架。
|
||||
@@ -0,0 +1,3 @@
|
||||
# Order Module
|
||||
|
||||
订单状态机、账号交接、租期归还、超时处理。
|
||||
@@ -0,0 +1,3 @@
|
||||
# Wallet Module
|
||||
|
||||
押金冻结、租金结算、不可变资金流水。
|
||||
@@ -0,0 +1,30 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"hfb_sys/backend/internal/config"
|
||||
"hfb_sys/backend/internal/handler"
|
||||
"hfb_sys/backend/internal/middleware"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func New(cfg config.Config, logger *zap.Logger) *gin.Engine {
|
||||
if cfg.AppEnv == "production" {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
}
|
||||
|
||||
engine := gin.New()
|
||||
engine.Use(gin.Recovery())
|
||||
engine.Use(middleware.RequestLogger(logger))
|
||||
|
||||
health := handler.NewHealthHandler()
|
||||
engine.GET("/health", health.Check)
|
||||
|
||||
api := engine.Group("/api")
|
||||
{
|
||||
api.GET("/health", health.Check)
|
||||
}
|
||||
|
||||
return engine
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
CREATE TABLE users (
|
||||
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
|
||||
phone VARCHAR(32) NOT NULL,
|
||||
nickname VARCHAR(64) NOT NULL DEFAULT '',
|
||||
avatar_url VARCHAR(512) NOT NULL DEFAULT '',
|
||||
realname_status VARCHAR(32) NOT NULL DEFAULT 'unverified',
|
||||
risk_status VARCHAR(32) NOT NULL DEFAULT 'normal',
|
||||
credit_score INT NOT NULL DEFAULT 100,
|
||||
status VARCHAR(32) NOT NULL DEFAULT 'active',
|
||||
last_login_at DATETIME NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY uk_users_phone (phone)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE user_realname (
|
||||
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
|
||||
user_id BIGINT UNSIGNED NOT NULL,
|
||||
provider VARCHAR(32) NOT NULL,
|
||||
provider_order_no VARCHAR(128) NOT NULL DEFAULT '',
|
||||
status VARCHAR(32) NOT NULL DEFAULT 'pending',
|
||||
masked_name VARCHAR(64) NOT NULL DEFAULT '',
|
||||
masked_id_no VARCHAR(64) NOT NULL DEFAULT '',
|
||||
verified_at DATETIME NULL,
|
||||
fail_reason VARCHAR(255) NOT NULL DEFAULT '',
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY uk_user_realname_user_id (user_id),
|
||||
KEY idx_user_realname_provider_order_no (provider_order_no)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE game_accounts (
|
||||
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
|
||||
owner_id BIGINT UNSIGNED NOT NULL,
|
||||
game_name VARCHAR(64) NOT NULL DEFAULT 'delta_force',
|
||||
server_region VARCHAR(64) NOT NULL,
|
||||
login_platform VARCHAR(64) NOT NULL,
|
||||
title VARCHAR(128) NOT NULL,
|
||||
description TEXT NULL,
|
||||
rank_level VARCHAR(64) NOT NULL DEFAULT '',
|
||||
haf_coin_amount BIGINT NOT NULL DEFAULT 0,
|
||||
asset_summary JSON NULL,
|
||||
season_tags JSON NULL,
|
||||
screenshot_urls JSON NULL,
|
||||
status VARCHAR(32) NOT NULL DEFAULT 'draft',
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
KEY idx_game_accounts_owner_id (owner_id),
|
||||
KEY idx_game_accounts_filter (game_name, server_region, login_platform)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE rental_listings (
|
||||
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
|
||||
account_id BIGINT UNSIGNED NOT NULL,
|
||||
owner_id BIGINT UNSIGNED NOT NULL,
|
||||
price_hourly DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
||||
price_daily DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
||||
price_weekly DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
||||
deposit_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
||||
min_rent_hours INT NOT NULL DEFAULT 1,
|
||||
max_rent_hours INT NOT NULL DEFAULT 168,
|
||||
status VARCHAR(32) NOT NULL DEFAULT 'draft',
|
||||
review_status VARCHAR(32) NOT NULL DEFAULT 'none',
|
||||
review_reason VARCHAR(255) NOT NULL DEFAULT '',
|
||||
published_at DATETIME NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
KEY idx_rental_listings_account_id (account_id),
|
||||
KEY idx_rental_listings_owner_id (owner_id),
|
||||
KEY idx_rental_listings_filter (status, review_status, price_hourly)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE rental_orders (
|
||||
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
|
||||
order_no VARCHAR(64) NOT NULL,
|
||||
listing_id BIGINT UNSIGNED NOT NULL,
|
||||
account_id BIGINT UNSIGNED NOT NULL,
|
||||
owner_id BIGINT UNSIGNED NOT NULL,
|
||||
renter_id BIGINT UNSIGNED NOT NULL,
|
||||
rent_start_at DATETIME NULL,
|
||||
rent_end_at DATETIME NULL,
|
||||
rent_hours INT NOT NULL,
|
||||
rent_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
||||
deposit_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
||||
platform_fee DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
||||
account_snapshot JSON NULL,
|
||||
status VARCHAR(32) NOT NULL DEFAULT 'pending_confirm',
|
||||
handoff_status VARCHAR(32) NOT NULL DEFAULT 'none',
|
||||
settlement_status VARCHAR(32) NOT NULL DEFAULT 'unsettled',
|
||||
owner_settled_at DATETIME NULL,
|
||||
settled_at DATETIME NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY uk_rental_orders_order_no (order_no),
|
||||
KEY idx_rental_orders_renter_status (renter_id, status),
|
||||
KEY idx_rental_orders_owner_status (owner_id, status),
|
||||
KEY idx_rental_orders_listing_id (listing_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE handoff_records (
|
||||
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
|
||||
order_id BIGINT UNSIGNED NOT NULL,
|
||||
from_user_id BIGINT UNSIGNED NOT NULL,
|
||||
to_user_id BIGINT UNSIGNED NOT NULL,
|
||||
type VARCHAR(32) NOT NULL,
|
||||
content TEXT NULL,
|
||||
attachment_urls JSON NULL,
|
||||
confirmed_by_renter_at DATETIME NULL,
|
||||
confirmed_by_owner_at DATETIME NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
KEY idx_handoff_records_order_id (order_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE wallet_accounts (
|
||||
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
|
||||
user_id BIGINT UNSIGNED NOT NULL,
|
||||
available_balance DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
||||
frozen_balance DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
||||
status VARCHAR(32) NOT NULL DEFAULT 'active',
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY uk_wallet_accounts_user_id (user_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE wallet_ledger (
|
||||
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
|
||||
ledger_no VARCHAR(64) NOT NULL,
|
||||
user_id BIGINT UNSIGNED NOT NULL,
|
||||
order_id BIGINT UNSIGNED NULL,
|
||||
direction VARCHAR(16) NOT NULL,
|
||||
amount DECIMAL(12,2) NOT NULL,
|
||||
balance_after DECIMAL(12,2) NOT NULL,
|
||||
balance_type VARCHAR(32) NOT NULL,
|
||||
biz_type VARCHAR(32) NOT NULL,
|
||||
biz_no VARCHAR(64) NOT NULL,
|
||||
remark VARCHAR(255) NOT NULL DEFAULT '',
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY uk_wallet_ledger_ledger_no (ledger_no),
|
||||
KEY idx_wallet_ledger_user_created (user_id, created_at),
|
||||
KEY idx_wallet_ledger_biz (biz_type, biz_no)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE disputes (
|
||||
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
|
||||
order_id BIGINT UNSIGNED NOT NULL,
|
||||
initiator_id BIGINT UNSIGNED NOT NULL,
|
||||
target_user_id BIGINT UNSIGNED NOT NULL,
|
||||
type VARCHAR(32) NOT NULL,
|
||||
status VARCHAR(32) NOT NULL DEFAULT 'open',
|
||||
description TEXT NULL,
|
||||
evidence_urls JSON NULL,
|
||||
arbitration_result VARCHAR(32) NOT NULL DEFAULT '',
|
||||
arbitration_remark TEXT NULL,
|
||||
handled_by BIGINT UNSIGNED NULL,
|
||||
handled_at DATETIME NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
KEY idx_disputes_order_id (order_id),
|
||||
KEY idx_disputes_status (status)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE notifications (
|
||||
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
|
||||
user_id BIGINT UNSIGNED NOT NULL,
|
||||
type VARCHAR(32) NOT NULL,
|
||||
title VARCHAR(128) NOT NULL,
|
||||
content TEXT NULL,
|
||||
biz_type VARCHAR(32) NOT NULL DEFAULT '',
|
||||
biz_id BIGINT UNSIGNED NULL,
|
||||
read_at DATETIME NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
KEY idx_notifications_user_read_created (user_id, read_at, created_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE audit_logs (
|
||||
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
|
||||
actor_type VARCHAR(32) NOT NULL,
|
||||
actor_id BIGINT UNSIGNED NOT NULL,
|
||||
action VARCHAR(64) NOT NULL,
|
||||
biz_type VARCHAR(32) NOT NULL,
|
||||
biz_id BIGINT UNSIGNED NULL,
|
||||
ip VARCHAR(64) NOT NULL DEFAULT '',
|
||||
user_agent VARCHAR(512) NOT NULL DEFAULT '',
|
||||
detail JSON NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
KEY idx_audit_logs_actor_created (actor_id, created_at),
|
||||
KEY idx_audit_logs_biz (biz_type, biz_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE admin_users (
|
||||
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
|
||||
username VARCHAR(64) NOT NULL,
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
nickname VARCHAR(64) NOT NULL DEFAULT '',
|
||||
status VARCHAR(32) NOT NULL DEFAULT 'active',
|
||||
last_login_at DATETIME NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY uk_admin_users_username (username)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE roles (
|
||||
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
|
||||
code VARCHAR(64) NOT NULL,
|
||||
name VARCHAR(64) NOT NULL,
|
||||
description VARCHAR(255) NOT NULL DEFAULT '',
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY uk_roles_code (code)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE permissions (
|
||||
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
|
||||
code VARCHAR(128) NOT NULL,
|
||||
name VARCHAR(64) NOT NULL,
|
||||
resource VARCHAR(128) NOT NULL,
|
||||
action VARCHAR(64) NOT NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY uk_permissions_code (code)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE admin_user_roles (
|
||||
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
|
||||
admin_user_id BIGINT UNSIGNED NOT NULL,
|
||||
role_id BIGINT UNSIGNED NOT NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY uk_admin_user_roles_user_role (admin_user_id, role_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE role_permissions (
|
||||
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
|
||||
role_id BIGINT UNSIGNED NOT NULL,
|
||||
permission_id BIGINT UNSIGNED NOT NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY uk_role_permissions_role_permission (role_id, permission_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE system_configs (
|
||||
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
|
||||
`key` VARCHAR(128) NOT NULL,
|
||||
`value` TEXT NOT NULL,
|
||||
description VARCHAR(255) NOT NULL DEFAULT '',
|
||||
updated_by BIGINT UNSIGNED NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY uk_system_configs_key (`key`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
@@ -0,0 +1,59 @@
|
||||
services:
|
||||
mysql:
|
||||
image: mysql:8.4
|
||||
container_name: hfb-mysql
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: rootsecret
|
||||
MYSQL_DATABASE: hfb_sys
|
||||
MYSQL_USER: hfb
|
||||
MYSQL_PASSWORD: secret
|
||||
TZ: Asia/Shanghai
|
||||
ports:
|
||||
- "3306:3306"
|
||||
volumes:
|
||||
- mysql_data:/var/lib/mysql
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-prootsecret"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
|
||||
redis:
|
||||
image: redis:7.4-alpine
|
||||
container_name: hfb-redis
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "6379:6379"
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
|
||||
minio:
|
||||
image: minio/minio:RELEASE.2025-09-07T16-13-09Z
|
||||
container_name: hfb-minio
|
||||
restart: unless-stopped
|
||||
command: server /data --console-address ":9001"
|
||||
environment:
|
||||
MINIO_ROOT_USER: minioadmin
|
||||
MINIO_ROOT_PASSWORD: minioadmin
|
||||
TZ: Asia/Shanghai
|
||||
ports:
|
||||
- "9000:9000"
|
||||
- "9001:9001"
|
||||
volumes:
|
||||
- minio_data:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "mc", "ready", "local"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
|
||||
volumes:
|
||||
mysql_data:
|
||||
redis_data:
|
||||
minio_data:
|
||||
@@ -0,0 +1,19 @@
|
||||
services:
|
||||
backend:
|
||||
build:
|
||||
context: ../backend
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- ../backend/.env
|
||||
ports:
|
||||
- "8080:8080"
|
||||
|
||||
frontend:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: frontend/Dockerfile
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- backend
|
||||
ports:
|
||||
- "80:80"
|
||||
@@ -0,0 +1,24 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
location /api/ {
|
||||
proxy_pass http://backend:8080/api/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location /health {
|
||||
proxy_pass http://backend:8080/health;
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
# API 规划
|
||||
|
||||
API 规划以 [项目计划](project-plan.md) 第 10 章为准。
|
||||
|
||||
一期接口按模块推进:
|
||||
|
||||
- 认证:短信验证码、手机号登录、Token 刷新、退出登录。
|
||||
- 实名:发起认证、查询状态、服务回调。
|
||||
- 发布:创建、修改、提交审核、下架、列表、详情。
|
||||
- 订单:创建、取消、确认收号、提交归还、确认归还、发起申诉。
|
||||
- 交接:提交交接说明、查看交接记录。
|
||||
- 钱包:余额、流水、提现预留。
|
||||
- 通知:站内信列表、标记已读。
|
||||
- 后台:审核、订单、仲裁、资金流水、系统配置、审计日志。
|
||||
@@ -0,0 +1,12 @@
|
||||
# 业务规则
|
||||
|
||||
业务规则以 [项目计划](project-plan.md) 为准。
|
||||
|
||||
一期关键边界:
|
||||
|
||||
- 哈夫币是游戏账号内资产,不是平台币。
|
||||
- 哈夫币数量由号主手动填报,订单创建时生成账号资产快照。
|
||||
- 平台不保存游戏账号密码,不代收验证码,不绕过游戏安全机制。
|
||||
- 账号交接采用号主手动交接。
|
||||
- 资金先做账务模型,不直接接真实支付和提现。
|
||||
- 押金、交接超时、归还超时、短信限流等阈值进入系统配置。
|
||||
@@ -0,0 +1,25 @@
|
||||
# 数据库设计
|
||||
|
||||
数据库设计以 [项目计划](project-plan.md) 第 9 章为准。
|
||||
|
||||
一期优先落表:
|
||||
|
||||
- `users`
|
||||
- `user_realname`
|
||||
- `game_accounts`
|
||||
- `rental_listings`
|
||||
- `rental_orders`
|
||||
- `handoff_records`
|
||||
- `wallet_accounts`
|
||||
- `wallet_ledger`
|
||||
- `disputes`
|
||||
- `notifications`
|
||||
- `audit_logs`
|
||||
- `admin_users`
|
||||
- `roles`
|
||||
- `permissions`
|
||||
- `admin_user_roles`
|
||||
- `role_permissions`
|
||||
- `system_configs`
|
||||
|
||||
后续实现迁移时,资金相关表必须保证流水只追加,订单相关表必须保留账号资产快照。
|
||||
@@ -0,0 +1,926 @@
|
||||
# 哈夫币租号平台项目计划
|
||||
|
||||
## 1. 项目定位与业务边界
|
||||
|
||||
本项目是面向《三角洲行动》账号租赁场景的前后端分离平台,核心能力包括手机号登录、实名认证、号主发布租号、租客下单、账号交接、租期归还、押金结算、纠纷仲裁和后台审核管理。
|
||||
|
||||
哈夫币在本系统中只被定义为游戏账号内资产,不是平台自发行虚拟币,不提供平台内铸币、转赠、交易、提现或链上能力。平台只记录账号资产描述、租赁订单、押金、租金、结算流水和纠纷处理结果。
|
||||
|
||||
一期不做自动上号器、不自动改密、不强制踢下线、不绕过游戏安全机制,也不开发外挂、盗号、破解、规避封禁等能力。账号租赁可能涉及游戏官方协议、账号安全、未成年人保护、支付合规和纠纷处理风险,上线前需要补充用户协议、隐私政策、交易规则和合规评估。
|
||||
|
||||
## 2. 技术栈与开发部署方式
|
||||
|
||||
前端技术栈:
|
||||
|
||||
- Vue 3
|
||||
- TypeScript
|
||||
- Vite
|
||||
- Element Plus
|
||||
- Pinia
|
||||
- Vue Router
|
||||
- Axios
|
||||
|
||||
后端技术栈:
|
||||
|
||||
- Go
|
||||
- Gin
|
||||
- GORM
|
||||
- MySQL 8
|
||||
- Redis
|
||||
- JWT
|
||||
- Casbin
|
||||
- Zap 或 Zerolog
|
||||
- Viper
|
||||
|
||||
开源借鉴策略:
|
||||
|
||||
- 项目路线为借鉴成熟开源后台的通用工程能力,租号业务核心从头自研。
|
||||
- 可重点参考 `gin-vue-admin`、`go-admin` 这类 Go/Vue 后台项目的 JWT、Casbin、RBAC、菜单权限、Swagger、GORM 分层、配置管理、日志、文件上传和基础 CRUD 组织方式。
|
||||
- 不直接二开租号源码,不把未知项目的交易、资金和风控逻辑作为基础。
|
||||
- 不直接接入未知质量的租号系统源码,不复用不可信的订单、资金、押金、交接或仲裁逻辑。
|
||||
- 必须自研的核心模块包括租号发布、订单状态机、账号交接、押金冻结、资金流水、纠纷仲裁、风控规则和哈夫币资产快照。
|
||||
- 后台 UI 布局、菜单管理、权限管理可以参考开源项目;用户端体验、号主端流程和租号业务页面从头设计。
|
||||
|
||||
文件存储:
|
||||
|
||||
- 开发环境使用 MinIO。
|
||||
- 生产环境使用阿里云 OSS 或腾讯云 COS。
|
||||
- 文件主要用于账号资产截图、租赁前后凭证、纠纷证据、后台公告图片、实名认证服务回执或材料引用。
|
||||
- 一期不要求平台长期保存身份证原图,实名认证资料优先交由第三方实名服务处理。
|
||||
|
||||
开发方式:
|
||||
|
||||
- 本机直接运行 Go 后端,便于调试和热重载。
|
||||
- 本机直接运行 Vue 前端,便于页面开发。
|
||||
- Docker Compose 启动 MySQL、Redis、MinIO。
|
||||
- 后端可使用 `air` 做本地热重载。
|
||||
- 前端使用 `pnpm dev` 本地开发。
|
||||
|
||||
生产部署:
|
||||
|
||||
- 后端构建 Docker 镜像部署。
|
||||
- 前端构建静态资源后由 Nginx 托管。
|
||||
- MySQL 生产优先使用云数据库。
|
||||
- Redis 生产优先使用云 Redis。
|
||||
- 文件存储生产使用 OSS/COS。
|
||||
- Nginx 负责 HTTPS、静态资源、反向代理和基础限流。
|
||||
|
||||
## 3. 三角洲行动特有规则
|
||||
|
||||
哈夫币填报:
|
||||
|
||||
- 一期不接游戏 API 同步,哈夫币数量由号主手动填报。
|
||||
- 号主发布时必须上传租前资产截图,作为哈夫币数量、段位、资产描述和纠纷仲裁依据。
|
||||
- `game_accounts.haf_coin_amount` 表示号主最近一次填报值,不代表实时值。
|
||||
- 下单时必须生成订单账号快照,记录哈夫币数量、区服、登录平台、段位、赛季限定资产、账号截图和资产描述。
|
||||
- 租赁前后哈夫币差异以订单快照、交接记录、租前租后截图和客服仲裁为准。
|
||||
|
||||
安全验证:
|
||||
|
||||
- 如果账号登录需要安全锁、二次验证、手机验证码或设备确认,由号主在交接阶段手动配合。
|
||||
- 号主超时未配合安全验证时,租客可取消订单或发起申诉。
|
||||
- 平台不保存游戏账号密码,不代收验证码,不绕过游戏登录、二次验证、安全锁或风控机制。
|
||||
|
||||
区服与资产:
|
||||
|
||||
- 区服资产默认隔离,商品必须明确区服。
|
||||
- 不同区服的哈夫币、段位和资产不合并展示。
|
||||
- 跨区服租赁不作为一期核心能力。
|
||||
- 赛季限定资产以标签和资产说明形式展示,可作为号主定价和押金建议参考,但一期不参与自动估值。
|
||||
|
||||
## 4. 核心业务模块
|
||||
|
||||
用户与认证:
|
||||
|
||||
- 手机号短信验证码登录。
|
||||
- JWT 登录态。
|
||||
- 用户角色包括租客、号主、管理员相关角色。
|
||||
- 支持实名状态、风控状态、冻结状态。
|
||||
|
||||
实名认证:
|
||||
|
||||
- 接入阿里云实人认证或腾讯云身份认证。
|
||||
- 用户发起认证后,平台记录认证渠道、认证流水号、认证状态和脱敏结果。
|
||||
- 未实名用户不能发布租号,后台可配置是否限制未实名租客下单。
|
||||
|
||||
租号发布:
|
||||
|
||||
- 号主实名后可发布账号。
|
||||
- 发布内容包括游戏、区服、平台、段位、哈夫币数量、账号资产描述、截图、租金、押金、租期范围。
|
||||
- 商品发布后进入待审核状态,后台审核通过后才能上架。
|
||||
|
||||
订单租赁:
|
||||
|
||||
- 租客选择商品和租期后创建订单。
|
||||
- 系统锁定商品库存,避免同一账号被重复出租。
|
||||
- 订单流转覆盖待确认、待交接、租赁中、待归还确认、已完成、已取消、申诉中。
|
||||
|
||||
钱包账务:
|
||||
|
||||
- 一期先做账务模型,不直接接真实支付和提现。
|
||||
- 系统记录租金、押金、冻结、解冻、扣款、退款、结算等流水。
|
||||
- 后续可接微信支付、支付宝支付、人工打款或第三方提现通道。
|
||||
|
||||
后台管理:
|
||||
|
||||
- 用户管理。
|
||||
- 实名状态查看。
|
||||
- 商品审核。
|
||||
- 订单管理。
|
||||
- 纠纷仲裁。
|
||||
- 资金流水。
|
||||
- 风控名单。
|
||||
- 审计日志。
|
||||
|
||||
## 5. 账号交接与回收流程
|
||||
|
||||
一期采用号主手动交接模式,平台不保存游戏账号密码,不托管明文账号凭据。
|
||||
|
||||
交接流程:
|
||||
|
||||
1. 租客创建订单,系统校验商品状态、租期、押金和余额账务状态。
|
||||
2. 订单确认后,商品进入锁定状态,订单进入待交接。
|
||||
3. 号主在订单内提交交接说明,例如登录方式、注意事项、联系说明或外部交接备注。
|
||||
4. 租客确认收到账号后,订单进入租赁中,并记录租赁开始时间和预计结束时间。
|
||||
5. 租期即将结束时,系统通过站内信和短信提醒租客归还。
|
||||
6. 租客提交归还申请,上传必要的租后截图或说明。
|
||||
7. 号主确认账号状态无误后,订单进入结算。
|
||||
8. 如任一方对交接、使用、归还或资产变化有异议,订单进入申诉仲裁。
|
||||
|
||||
回收边界:
|
||||
|
||||
- 平台不自动修改账号密码。
|
||||
- 平台不强制踢下线。
|
||||
- 平台不绕过游戏登录、二次验证、安全锁或风控机制。
|
||||
- 平台不承诺技术性收回账号,只通过订单规则、押金、证据和仲裁处理争议。
|
||||
|
||||
超时机制:
|
||||
|
||||
- 号主待交接超时:订单进入待交接后,默认 30 分钟未提交交接说明,租客可取消订单或发起申诉。
|
||||
- 租客确认收号超时:号主提交交接说明后,默认 30 分钟未确认收号,订单进入客服介入,不自动开始租期。
|
||||
- 租客归还超时:超过租期结束时间仍未提交归还,系统提醒后进入逾期中,号主可发起申诉。
|
||||
- 号主确认归还超时:租客提交归还后,默认 2 小时未确认,订单进入自动确认候选或客服复核。
|
||||
- 所有超时阈值必须进入 `system_configs` 管理,后台可调整。
|
||||
|
||||
订单状态机:
|
||||
|
||||
| From | To | 触发者 | 条件 |
|
||||
| --- | --- | --- | --- |
|
||||
| 待确认 | 待交接 | 系统 | 订单确认,商品锁定,账务校验通过 |
|
||||
| 待确认 | 已取消 | 租客/系统 | 租客主动取消或确认超时 |
|
||||
| 待交接 | 待收号确认 | 号主 | 号主提交交接说明 |
|
||||
| 待交接 | 已取消 | 租客 | 号主交接超时且未进入申诉 |
|
||||
| 待交接 | 申诉中 | 租客/号主 | 交接争议或安全验证无法完成 |
|
||||
| 待收号确认 | 租赁中 | 租客 | 租客确认收到账号并可正常登录 |
|
||||
| 待收号确认 | 申诉中 | 租客/系统 | 租客确认超时、无法登录或描述不符 |
|
||||
| 租赁中 | 逾期中 | 系统 | 超过租期结束时间仍未提交归还 |
|
||||
| 租赁中 | 待归还确认 | 租客 | 租客提交归还和租后凭证 |
|
||||
| 逾期中 | 待归还确认 | 租客 | 租客补交归还申请 |
|
||||
| 逾期中 | 申诉中 | 号主/系统 | 逾期未归还或疑似资产损失 |
|
||||
| 待归还确认 | 已完成 | 号主/系统 | 号主确认归还,或超时后客服/系统确认 |
|
||||
| 待归还确认 | 申诉中 | 号主/租客 | 归还争议、资产损失或封号异常 |
|
||||
| 申诉中 | 已完成 | 客服 | 仲裁为正常完成或部分扣款后完成 |
|
||||
| 申诉中 | 已取消 | 客服 | 仲裁为订单取消和退款 |
|
||||
| 申诉中 | 已关闭 | 客服 | 仲裁为异常关闭、冻结用户或冻结商品 |
|
||||
|
||||
终态规则:
|
||||
|
||||
- `已完成`、`已取消`、`已关闭` 为终态,普通业务流程不能逆转。
|
||||
- 终态订单如需调整,只能通过后台复核流程生成新的资金流水和审计日志,不能直接改旧流水。
|
||||
- `申诉中` 会冻结相关押金和结算金额,直到客服仲裁。
|
||||
|
||||
## 6. 押金、价格与结算模型
|
||||
|
||||
定价模型:
|
||||
|
||||
- 租金由号主自主设置。
|
||||
- 支持按小时、按天、按周配置价格。
|
||||
- 平台可在后续提供建议价,但一期不强制。
|
||||
- 商品需配置最短租期和最长租期。
|
||||
|
||||
押金模型:
|
||||
|
||||
- 押金由号主设置,平台校验最低押金。
|
||||
- 最低押金可根据账号估值、哈夫币数量、稀有资产、租期长度、号主历史纠纷率、租客风险等级给出动态建议。
|
||||
- 一期先实现规则化最低押金,动态建议可后续迭代。
|
||||
- 押金不足以覆盖损失时,订单进入人工仲裁,不自动产生超额扣款。
|
||||
|
||||
账务模型:
|
||||
|
||||
- 一期先做平台内账务,不接真实支付和提现。
|
||||
- 钱包流水采用不可变账本设计。
|
||||
- 余额可通过流水汇总或账户余额表事务更新获得。
|
||||
- 每笔余额变化必须有业务单据来源,例如订单、押金、仲裁、退款、结算。
|
||||
- 平台抽成比例、号主结算周期、提现门槛作为系统配置项预留。
|
||||
|
||||
结算规则:
|
||||
|
||||
- 正常完成订单后,租金按平台抽成规则分账给号主。
|
||||
- 押金在号主确认归还后释放给租客。
|
||||
- 如发生资产损失、无法登录、封号或超时归还,可由客服仲裁后扣除部分或全部押金。
|
||||
- 如号主虚假描述、超时未交接或账号无法使用,可裁定全额或部分退款。
|
||||
- 仲裁导致的扣款、退款、赔付必须生成资金流水和审计日志。
|
||||
|
||||
## 7. 纠纷仲裁规则
|
||||
|
||||
纠纷类型:
|
||||
|
||||
- 无法登录。
|
||||
- 账号描述不符。
|
||||
- 哈夫币数量争议。
|
||||
- 账号资产损失。
|
||||
- 账号封禁或异常。
|
||||
- 号主超时未交接。
|
||||
- 租客超时未归还。
|
||||
- 交接凭证争议。
|
||||
- 退款、押金、赔付争议。
|
||||
|
||||
证据要求:
|
||||
|
||||
- 租前账号资产截图。
|
||||
- 租后账号资产截图。
|
||||
- 哈夫币数量截图。
|
||||
- 系统订单时间线。
|
||||
- 交接记录。
|
||||
- 站内沟通记录。
|
||||
- 第三方沟通截图。
|
||||
- 游戏内异常或封禁截图。
|
||||
|
||||
仲裁流程:
|
||||
|
||||
1. 任一方在订单详情发起申诉。
|
||||
2. 订单进入申诉中,相关押金和结算金额保持冻结。
|
||||
3. 双方在限定时间内补充证据。
|
||||
4. 客服查看订单、交接、截图、聊天和资金流水。
|
||||
5. 客服选择裁决结果并填写裁决说明。
|
||||
6. 系统根据裁决结果调整订单状态和资金流水。
|
||||
7. 双方收到站内信,关键结果可短信通知。
|
||||
|
||||
裁决结果:
|
||||
|
||||
- 全额退款。
|
||||
- 部分退款。
|
||||
- 释放押金。
|
||||
- 扣除部分押金。
|
||||
- 扣除全部押金。
|
||||
- 赔付号主。
|
||||
- 订单关闭。
|
||||
- 商品冻结。
|
||||
- 用户冻结。
|
||||
- 进入二次复核。
|
||||
|
||||
## 8. 消息通知机制
|
||||
|
||||
一期采用站内信加关键节点短信。
|
||||
|
||||
站内信场景:
|
||||
|
||||
- 商品审核通过或拒绝。
|
||||
- 租客下单。
|
||||
- 订单进入待交接。
|
||||
- 号主提交交接说明。
|
||||
- 租客确认收号。
|
||||
- 租期即将结束。
|
||||
- 租客提交归还。
|
||||
- 号主确认归还。
|
||||
- 订单完成结算。
|
||||
- 发起申诉。
|
||||
- 仲裁结果。
|
||||
- 账户冻结或解冻。
|
||||
|
||||
短信场景:
|
||||
|
||||
- 登录验证码。
|
||||
- 租期即将到期。
|
||||
- 订单申诉被受理。
|
||||
- 仲裁结果。
|
||||
- 高风险账号安全提醒。
|
||||
|
||||
扩展方向:
|
||||
|
||||
- 后续可增加 WebSocket 或 SSE,实现订单状态和客服消息实时推送。
|
||||
- 短信必须做手机号、IP、设备、场景维度限流,防止短信轰炸。
|
||||
|
||||
## 9. 数据库设计概要
|
||||
|
||||
核心关系:
|
||||
|
||||
- 一个用户可以同时是租客和号主。
|
||||
- 一个用户最多对应一条当前实名信息。
|
||||
- 一个号主可以发布多个游戏账号。
|
||||
- 一个游戏账号同一时间只能有一个上架商品或一个有效租赁订单。
|
||||
- 一个租赁商品关联一个游戏账号。
|
||||
- 一个订单关联一个租客、一个号主、一个商品和一个游戏账号快照。
|
||||
- 一个订单可以有多条交接记录、通知、审计日志和纠纷证据。
|
||||
- 资金流水只追加,不覆盖。
|
||||
- 后台管理员和角色为多对多关系,角色和权限为多对多关系。
|
||||
- 可调业务阈值统一放入 `system_configs`,例如最低押金、交接超时、归还超时、短信限流和实名下单开关。
|
||||
|
||||
核心表:
|
||||
|
||||
`users`
|
||||
|
||||
- `id`
|
||||
- `phone`
|
||||
- `nickname`
|
||||
- `avatar_url`
|
||||
- `realname_status`
|
||||
- `risk_status`
|
||||
- `credit_score`
|
||||
- `status`
|
||||
- `last_login_at`
|
||||
- `created_at`
|
||||
- `updated_at`
|
||||
|
||||
`user_realname`
|
||||
|
||||
- `id`
|
||||
- `user_id`
|
||||
- `provider`
|
||||
- `provider_order_no`
|
||||
- `status`
|
||||
- `masked_name`
|
||||
- `masked_id_no`
|
||||
- `verified_at`
|
||||
- `fail_reason`
|
||||
- `created_at`
|
||||
- `updated_at`
|
||||
|
||||
`game_accounts`
|
||||
|
||||
- `id`
|
||||
- `owner_id`
|
||||
- `game_name`
|
||||
- `server_region`
|
||||
- `login_platform`
|
||||
- `title`
|
||||
- `description`
|
||||
- `rank_level`
|
||||
- `haf_coin_amount`
|
||||
- `asset_summary`
|
||||
- `season_tags`
|
||||
- `screenshot_urls`
|
||||
- `status`
|
||||
- `created_at`
|
||||
- `updated_at`
|
||||
|
||||
说明:`haf_coin_amount` 是号主最近一次填报值,不代表实时值;订单创建时必须写入 `rental_orders.account_snapshot`。
|
||||
|
||||
`rental_listings`
|
||||
|
||||
- `id`
|
||||
- `account_id`
|
||||
- `owner_id`
|
||||
- `price_hourly`
|
||||
- `price_daily`
|
||||
- `price_weekly`
|
||||
- `deposit_amount`
|
||||
- `min_rent_hours`
|
||||
- `max_rent_hours`
|
||||
- `status`
|
||||
- `review_status`
|
||||
- `review_reason`
|
||||
- `published_at`
|
||||
- `created_at`
|
||||
- `updated_at`
|
||||
|
||||
`rental_orders`
|
||||
|
||||
- `id`
|
||||
- `order_no`
|
||||
- `listing_id`
|
||||
- `account_id`
|
||||
- `owner_id`
|
||||
- `renter_id`
|
||||
- `rent_start_at`
|
||||
- `rent_end_at`
|
||||
- `rent_hours`
|
||||
- `rent_amount`
|
||||
- `deposit_amount`
|
||||
- `platform_fee`
|
||||
- `account_snapshot`
|
||||
- `status`
|
||||
- `handoff_status`
|
||||
- `settlement_status`
|
||||
- `owner_settled_at`
|
||||
- `settled_at`
|
||||
- `created_at`
|
||||
- `updated_at`
|
||||
|
||||
`handoff_records`
|
||||
|
||||
- `id`
|
||||
- `order_id`
|
||||
- `from_user_id`
|
||||
- `to_user_id`
|
||||
- `type`
|
||||
- `content`
|
||||
- `attachment_urls`
|
||||
- `confirmed_by_renter_at`
|
||||
- `confirmed_by_owner_at`
|
||||
- `created_at`
|
||||
|
||||
`wallet_accounts`
|
||||
|
||||
- `id`
|
||||
- `user_id`
|
||||
- `available_balance`
|
||||
- `frozen_balance`
|
||||
- `status`
|
||||
- `created_at`
|
||||
- `updated_at`
|
||||
|
||||
`wallet_ledger`
|
||||
|
||||
- `id`
|
||||
- `ledger_no`
|
||||
- `user_id`
|
||||
- `order_id`
|
||||
- `direction`
|
||||
- `amount`
|
||||
- `balance_after`
|
||||
- `balance_type`
|
||||
- `biz_type`
|
||||
- `biz_no`
|
||||
- `remark`
|
||||
- `created_at`
|
||||
|
||||
说明:`balance_after` 记录本次流水发生后对应余额类型的余额,用于审计和对账。
|
||||
|
||||
`disputes`
|
||||
|
||||
- `id`
|
||||
- `order_id`
|
||||
- `initiator_id`
|
||||
- `target_user_id`
|
||||
- `type`
|
||||
- `status`
|
||||
- `description`
|
||||
- `evidence_urls`
|
||||
- `arbitration_result`
|
||||
- `arbitration_remark`
|
||||
- `handled_by`
|
||||
- `handled_at`
|
||||
- `created_at`
|
||||
- `updated_at`
|
||||
|
||||
`notifications`
|
||||
|
||||
- `id`
|
||||
- `user_id`
|
||||
- `type`
|
||||
- `title`
|
||||
- `content`
|
||||
- `biz_type`
|
||||
- `biz_id`
|
||||
- `read_at`
|
||||
- `created_at`
|
||||
|
||||
`audit_logs`
|
||||
|
||||
- `id`
|
||||
- `actor_type`
|
||||
- `actor_id`
|
||||
- `action`
|
||||
- `biz_type`
|
||||
- `biz_id`
|
||||
- `ip`
|
||||
- `user_agent`
|
||||
- `detail`
|
||||
- `created_at`
|
||||
|
||||
`admin_users`
|
||||
|
||||
- `id`
|
||||
- `username`
|
||||
- `password_hash`
|
||||
- `nickname`
|
||||
- `status`
|
||||
- `last_login_at`
|
||||
- `created_at`
|
||||
- `updated_at`
|
||||
|
||||
`roles`
|
||||
|
||||
- `id`
|
||||
- `code`
|
||||
- `name`
|
||||
- `description`
|
||||
- `created_at`
|
||||
- `updated_at`
|
||||
|
||||
`permissions`
|
||||
|
||||
- `id`
|
||||
- `code`
|
||||
- `name`
|
||||
- `resource`
|
||||
- `action`
|
||||
- `created_at`
|
||||
- `updated_at`
|
||||
|
||||
`admin_user_roles`
|
||||
|
||||
- `id`
|
||||
- `admin_user_id`
|
||||
- `role_id`
|
||||
- `created_at`
|
||||
|
||||
`role_permissions`
|
||||
|
||||
- `id`
|
||||
- `role_id`
|
||||
- `permission_id`
|
||||
- `created_at`
|
||||
|
||||
`system_configs`
|
||||
|
||||
- `id`
|
||||
- `key`
|
||||
- `value`
|
||||
- `description`
|
||||
- `updated_by`
|
||||
- `created_at`
|
||||
- `updated_at`
|
||||
|
||||
索引策略:
|
||||
|
||||
- `users.phone` 唯一索引。
|
||||
- `game_accounts.owner_id` 普通索引。
|
||||
- `game_accounts.game_name, server_region, login_platform` 组合索引。
|
||||
- `rental_listings.status, review_status, price_hourly` 组合索引。
|
||||
- `rental_orders.order_no` 唯一索引。
|
||||
- `rental_orders.renter_id, status` 组合索引。
|
||||
- `rental_orders.owner_id, status` 组合索引。
|
||||
- `wallet_ledger.user_id, created_at` 组合索引。
|
||||
- `wallet_ledger.ledger_no` 唯一索引。
|
||||
- `disputes.order_id` 普通索引。
|
||||
- `notifications.user_id, read_at, created_at` 组合索引。
|
||||
- `audit_logs.actor_id, created_at` 组合索引。
|
||||
- `admin_user_roles.admin_user_id, role_id` 唯一索引。
|
||||
- `role_permissions.role_id, permission_id` 唯一索引。
|
||||
- `system_configs.key` 唯一索引。
|
||||
|
||||
## 10. API 接口规划
|
||||
|
||||
用户认证:
|
||||
|
||||
- `POST /api/auth/sms/send`:发送短信验证码。
|
||||
- `POST /api/auth/sms/login`:手机号验证码登录。
|
||||
- `POST /api/auth/refresh`:刷新 Token。
|
||||
- `POST /api/auth/logout`:退出登录。
|
||||
|
||||
实名认证:
|
||||
|
||||
- `POST /api/realname/start`:发起实名认证。
|
||||
- `GET /api/realname/status`:查询实名状态。
|
||||
- `POST /api/realname/callback`:实名认证服务回调。
|
||||
|
||||
租号发布:
|
||||
|
||||
- `GET /api/listings`:租号列表。
|
||||
- `GET /api/listings/{id}`:租号详情。
|
||||
- `POST /api/listings`:创建发布。
|
||||
- `PUT /api/listings/{id}`:修改发布。
|
||||
- `POST /api/listings/{id}/submit-review`:提交审核。
|
||||
- `DELETE /api/listings/{id}`:下架发布。
|
||||
|
||||
订单:
|
||||
|
||||
- `POST /api/orders`:创建订单。
|
||||
- `GET /api/orders`:订单列表。
|
||||
- `GET /api/orders/{id}`:订单详情。
|
||||
- `POST /api/orders/{id}/cancel`:取消订单。
|
||||
- `POST /api/orders/{id}/confirm-receive`:租客确认收号。
|
||||
- `POST /api/orders/{id}/return`:租客提交归还。
|
||||
- `POST /api/orders/{id}/confirm-return`:号主确认归还。
|
||||
- `POST /api/orders/{id}/dispute`:发起申诉。
|
||||
|
||||
交接:
|
||||
|
||||
- `POST /api/orders/{id}/handoff`:号主提交交接说明。
|
||||
- `GET /api/orders/{id}/handoff-records`:查看交接记录。
|
||||
|
||||
钱包:
|
||||
|
||||
- `GET /api/wallet/balance`:查询余额。
|
||||
- `GET /api/wallet/ledger`:查询流水。
|
||||
- `POST /api/wallet/withdraw`:提现申请预留接口。
|
||||
|
||||
通知:
|
||||
|
||||
- `GET /api/notifications`:站内信列表。
|
||||
- `POST /api/notifications/{id}/read`:标记已读。
|
||||
|
||||
后台:
|
||||
|
||||
- `GET /admin/dashboard`:后台仪表盘。
|
||||
- `GET /admin/users`:用户列表。
|
||||
- `POST /admin/users/{id}/freeze`:冻结用户。
|
||||
- `POST /admin/users/{id}/unfreeze`:解冻用户。
|
||||
- `GET /admin/listings/pending`:待审核商品。
|
||||
- `POST /admin/listings/{id}/approve`:审核通过。
|
||||
- `POST /admin/listings/{id}/reject`:审核拒绝。
|
||||
- `GET /admin/orders`:订单列表。
|
||||
- `GET /admin/disputes`:纠纷列表。
|
||||
- `POST /admin/orders/{id}/arbitrate`:订单仲裁。
|
||||
- `GET /admin/wallet/ledger`:资金流水。
|
||||
- `GET /admin/system-configs`:系统配置列表。
|
||||
- `PUT /admin/system-configs/{key}`:更新系统配置。
|
||||
- `GET /admin/audit-logs`:审计日志。
|
||||
|
||||
## 11. 安全与风控
|
||||
|
||||
认证安全:
|
||||
|
||||
- 短信验证码存 Redis,设置过期时间。
|
||||
- 验证码一次性使用。
|
||||
- 限制手机号、IP、设备、场景维度发送频率。
|
||||
- 登录失败、验证码错误、异常设备需要计入风控。
|
||||
|
||||
接口安全:
|
||||
|
||||
- 所有写接口校验 JWT。
|
||||
- 后台接口使用 Casbin 做 RBAC 权限控制。
|
||||
- 关键写操作防重复提交。
|
||||
- 金额、状态流转、订单归属必须由后端校验。
|
||||
- 所有 SQL 通过 ORM 参数化,禁止拼接用户输入。
|
||||
|
||||
业务风控:
|
||||
|
||||
- 检测同一用户、同一设备、同一 IP 的异常注册和下单。
|
||||
- 检测号主自己租自己或关联账号刷单。
|
||||
- 检测重复发布同一账号。
|
||||
- 检测频繁取消、频繁申诉、频繁仲裁失败用户。
|
||||
- 检测高价值账号低押金异常发布。
|
||||
- 检测短时间大量短信请求。
|
||||
|
||||
账号安全:
|
||||
|
||||
- 平台不保存游戏账号明文密码。
|
||||
- 交接说明和证据只对订单相关用户和管理员可见。
|
||||
- 敏感字段脱敏展示。
|
||||
- 实名信息加密存储。
|
||||
- 账号截图和纠纷证据使用私有文件访问,避免公开 URL 长期暴露。
|
||||
|
||||
审计要求:
|
||||
|
||||
- 登录、实名、发布、审核、下单、交接、归还、申诉、仲裁、冻结、退款、结算都写审计日志。
|
||||
- 后台管理员的每次裁决必须记录操作者、时间、IP、说明和变更前后关键状态。
|
||||
|
||||
## 12. 后台管理
|
||||
|
||||
后台角色:
|
||||
|
||||
- 超级管理员:拥有全部权限。
|
||||
- 审核员:处理商品审核和资料审核。
|
||||
- 客服:处理订单、交接问题和纠纷。
|
||||
- 财务:查看流水、处理结算和提现预留。
|
||||
- 风控:处理冻结、黑名单和异常用户。
|
||||
|
||||
后台功能:
|
||||
|
||||
- 仪表盘:用户数、商品数、订单数、待审核、待仲裁、流水概览。
|
||||
- 用户管理:查看用户、实名状态、风险状态、冻结/解冻。
|
||||
- 商品审核:查看账号资产、截图、价格、押金、审核通过/拒绝。
|
||||
- 订单管理:查看订单时间线、交接记录、资金状态。
|
||||
- 仲裁中心:查看纠纷、证据、双方说明,执行裁决。
|
||||
- 资金流水:查看租金、押金、冻结、解冻、扣款、退款、结算。
|
||||
- 通知管理:查看站内信发送记录和短信发送记录。
|
||||
- 系统配置:管理交接超时、归还超时、短信限流、最低押金、平台抽成和实名下单开关。
|
||||
- 审计日志:查看管理员操作和高风险业务操作。
|
||||
|
||||
## 13. 前端页面与路由规划
|
||||
|
||||
用户端页面:
|
||||
|
||||
- `/`:首页,展示推荐租号、热门区服和平台公告。
|
||||
- `/listings`:租号列表,支持区服、平台、价格、押金、段位、哈夫币数量筛选。
|
||||
- `/listings/:id`:租号详情,展示账号资产、租金、押金、租期、号主信用和风险提示。
|
||||
- `/orders/create`:下单页,选择租期并确认租金、押金和规则。
|
||||
- `/orders`:我的订单。
|
||||
- `/orders/:id`:订单详情,展示状态机时间线、交接记录、归还入口和申诉入口。
|
||||
- `/wallet`:钱包余额和流水。
|
||||
- `/notifications`:站内信。
|
||||
- `/realname`:实名认证。
|
||||
|
||||
号主端页面:
|
||||
|
||||
- `/seller/listings`:我的发布。
|
||||
- `/seller/listings/create`:发布租号。
|
||||
- `/seller/listings/:id/edit`:编辑发布。
|
||||
- `/seller/handoffs`:交接管理。
|
||||
- `/seller/disputes`:纠纷处理。
|
||||
- `/seller/earnings`:收益流水。
|
||||
|
||||
后台端页面:
|
||||
|
||||
- `/admin/dashboard`:仪表盘。
|
||||
- `/admin/users`:用户管理。
|
||||
- `/admin/listings/review`:商品审核。
|
||||
- `/admin/orders`:订单管理。
|
||||
- `/admin/disputes`:仲裁中心。
|
||||
- `/admin/wallet-ledger`:资金流水。
|
||||
- `/admin/system-configs`:系统配置。
|
||||
- `/admin/audit-logs`:审计日志。
|
||||
|
||||
前端实现原则:
|
||||
|
||||
- 后台布局、菜单和权限控制可参考开源后台项目。
|
||||
- 租客端、号主端的交易体验和业务页面从头实现。
|
||||
- 路由权限需要区分游客、已登录用户、已实名用户、号主和管理员。
|
||||
|
||||
## 14. 项目目录规划
|
||||
|
||||
```text
|
||||
hfb_sys/
|
||||
├── backend/
|
||||
│ ├── cmd/
|
||||
│ │ └── api/
|
||||
│ ├── config/
|
||||
│ ├── internal/
|
||||
│ │ ├── handler/
|
||||
│ │ ├── service/
|
||||
│ │ ├── repo/
|
||||
│ │ ├── model/
|
||||
│ │ ├── middleware/
|
||||
│ │ ├── modules/
|
||||
│ │ │ ├── auth/
|
||||
│ │ │ ├── user/
|
||||
│ │ │ ├── realname/
|
||||
│ │ │ ├── listing/
|
||||
│ │ │ ├── order/
|
||||
│ │ │ ├── wallet/
|
||||
│ │ │ ├── dispute/
|
||||
│ │ │ ├── notification/
|
||||
│ │ │ └── admin/
|
||||
│ │ └── integrations/
|
||||
│ │ ├── sms/
|
||||
│ │ ├── realname/
|
||||
│ │ └── storage/
|
||||
│ ├── pkg/
|
||||
│ └── migrations/
|
||||
├── frontend/
|
||||
│ ├── src/
|
||||
│ │ ├── api/
|
||||
│ │ ├── router/
|
||||
│ │ ├── stores/
|
||||
│ │ ├── views/
|
||||
│ │ ├── components/
|
||||
│ │ └── layouts/
|
||||
│ └── public/
|
||||
├── deploy/
|
||||
│ ├── docker-compose.dev.yml
|
||||
│ ├── docker-compose.prod.yml
|
||||
│ └── nginx/
|
||||
├── docs/
|
||||
│ ├── project-plan.md
|
||||
│ ├── database.md
|
||||
│ ├── api.md
|
||||
│ └── business-rules.md
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## 15. 阶段开发计划
|
||||
|
||||
第 0 阶段:项目初始化
|
||||
|
||||
- 创建前后端目录。
|
||||
- 初始化 Go 项目。
|
||||
- 初始化 Vue 项目。
|
||||
- 参考 `gin-vue-admin`、`go-admin` 的目录分层、RBAC、日志、配置、Swagger 和文件上传实现方式。
|
||||
- 配置 Docker Compose 开发环境。
|
||||
- 接入 MySQL、Redis、MinIO。
|
||||
- 建立基础配置、日志、错误码和响应结构。
|
||||
|
||||
第 1 阶段:用户与认证
|
||||
|
||||
- 实现短信验证码发送接口。
|
||||
- 实现短信验证码登录。
|
||||
- 实现 JWT 鉴权中间件。
|
||||
- 实现用户资料查询。
|
||||
- 加入短信限流和验证码一次性校验。
|
||||
|
||||
第 2 阶段:实名认证
|
||||
|
||||
- 建立实名表和状态流转。
|
||||
- 接入实名服务适配器。
|
||||
- 实现实名发起、查询、回调。
|
||||
- 限制未实名用户发布租号。
|
||||
|
||||
第 3 阶段:租号发布与审核
|
||||
|
||||
- 实现游戏账号和发布商品模型。
|
||||
- 实现哈夫币手动填报、区服隔离、赛季标签和账号资产截图。
|
||||
- 实现发布、修改、下架、提交审核。
|
||||
- 实现后台审核通过和拒绝。
|
||||
- 实现租号列表、详情和筛选。
|
||||
|
||||
第 4 阶段:订单、交接与账务
|
||||
|
||||
- 实现创建订单和账号锁定。
|
||||
- 实现订单状态机和合法状态转换校验。
|
||||
- 实现订单账号资产快照。
|
||||
- 实现号主提交交接说明。
|
||||
- 实现租客确认收号。
|
||||
- 实现租客提交归还、号主确认归还。
|
||||
- 实现交接、确认收号、归还、确认归还的超时处理。
|
||||
- 实现钱包账户、押金冻结、租金流水、结算流水。
|
||||
- 预留支付和提现接口。
|
||||
|
||||
第 5 阶段:纠纷、通知与后台
|
||||
|
||||
- 实现纠纷发起、举证、客服仲裁。
|
||||
- 实现站内信通知。
|
||||
- 接入关键节点短信通知。
|
||||
- 完善后台仪表盘、用户管理、订单管理、资金流水、系统配置、审计日志。
|
||||
|
||||
第 6 阶段:测试、风控与部署
|
||||
|
||||
- 补齐单元测试和集成测试。
|
||||
- 做并发下单测试。
|
||||
- 做越权、重复提交、短信轰炸基础安全测试。
|
||||
- 完成 Docker 生产部署配置。
|
||||
- 准备上线前用户协议、隐私政策、交易规则和风险提示。
|
||||
|
||||
## 16. 测试计划
|
||||
|
||||
文档检查:
|
||||
|
||||
- `docs/project-plan.md` 是完整项目方案,不再是生成说明。
|
||||
- 文档明确写入借鉴开源、自研业务核心的路线。
|
||||
- 文档不把直接二开租号源码作为推荐方案。
|
||||
- 技术栈统一为 Go 方案,无 Java 或 Spring Boot 推荐残留。
|
||||
- MySQL 明确为主数据库。
|
||||
- Redis 明确用于验证码、缓存、锁和限流。
|
||||
- Docker 方案区分本地开发和生产部署。
|
||||
- 哈夫币边界明确,不描述为平台虚拟币。
|
||||
- 三角洲行动特有规则有独立章节。
|
||||
- 哈夫币数量明确为号主填报和订单快照,不承诺实时同步。
|
||||
- 一期账号交接明确为号主手动交接。
|
||||
- 交接超时阈值和处理策略明确。
|
||||
- 订单状态机有状态转换表。
|
||||
- 一期资金模型明确为先做账务,不直接接真实支付提现。
|
||||
- 一期通知明确为站内信加关键短信。
|
||||
- 数据库概要包含 `wallet_ledger.balance_after`、`system_configs`、`owner_settled_at`、`account_snapshot`、`admin_user_roles`、`role_permissions`。
|
||||
- 前端路由规划覆盖用户端、号主端和后台端。
|
||||
|
||||
功能测试:
|
||||
|
||||
- 短信验证码正确、错误、过期、重复使用。
|
||||
- 手机号、IP、设备限流生效。
|
||||
- JWT 过期、伪造、缺失时被拒绝。
|
||||
- 未实名用户不能发布租号。
|
||||
- 实名认证成功、失败、认证中状态流转正确。
|
||||
- 商品草稿、待审核、已上架、已下架、审核拒绝状态流转正确。
|
||||
- 审核未通过商品不能下单。
|
||||
- 同一账号并发下单只能成功一单。
|
||||
- 订单待交接、租赁中、待归还确认、已完成、申诉中状态流转正确。
|
||||
- 非法订单状态转换会被拒绝。
|
||||
- 交接、收号、归还、确认归还超时会按配置触发取消、申诉、逾期或客服复核。
|
||||
- 押金冻结、释放、扣款、退款流水一致。
|
||||
- 每笔 `wallet_ledger` 正确记录 `balance_after`。
|
||||
- 租金、平台抽成、号主结算流水一致。
|
||||
- 订单结算后正确写入 `owner_settled_at` 或 `settled_at`。
|
||||
- 纠纷仲裁不同裁决结果正确落账。
|
||||
- 站内信和短信在关键节点触发。
|
||||
|
||||
安全测试:
|
||||
|
||||
- 普通用户不能访问后台接口。
|
||||
- 后台不同角色只能访问授权功能。
|
||||
- 用户不能查看他人订单、钱包、交接记录或纠纷证据。
|
||||
- SQL 注入、越权访问、重复提交有基础防护。
|
||||
- 短信轰炸被限流。
|
||||
- 高风险操作全部写入审计日志。
|
||||
|
||||
性能与稳定性测试:
|
||||
|
||||
- 租号列表分页和筛选在基础数据量下响应稳定。
|
||||
- 并发创建订单时库存锁定正确。
|
||||
- 钱包流水在并发结算时不出现负数或重复入账。
|
||||
- Redis、MySQL 短暂异常时接口返回可识别错误,不产生脏状态。
|
||||
|
||||
## 17. 风险与合规说明
|
||||
|
||||
账号租赁存在天然风险,包括账号找回、账号封禁、虚假描述、资产损失、租客恶意破坏、号主恶意交接、未成年人交易、资金纠纷和平台责任边界不清。
|
||||
|
||||
上线前必须补充:
|
||||
|
||||
- 用户协议。
|
||||
- 隐私政策。
|
||||
- 租号交易规则。
|
||||
- 押金与赔付规则。
|
||||
- 纠纷仲裁规则。
|
||||
- 未成年人保护说明。
|
||||
- 实名认证授权说明。
|
||||
- 支付和提现合规方案。
|
||||
- 游戏官方规则风险评估。
|
||||
|
||||
平台规则必须明确:
|
||||
|
||||
- 哈夫币不是平台发行资产。
|
||||
- 平台不保证游戏账号不会被官方限制、封禁或找回。
|
||||
- 平台不提供绕过游戏安全机制的技术服务。
|
||||
- 平台基于订单记录、交接记录、证据和仲裁规则处理争议。
|
||||
- 真实支付和提现上线前必须完成资质、风控、对账、发票或税务相关评估。
|
||||
@@ -0,0 +1,13 @@
|
||||
FROM node:24-alpine AS build
|
||||
|
||||
WORKDIR /src
|
||||
COPY frontend/package*.json ./
|
||||
RUN npm install
|
||||
COPY frontend/ .
|
||||
RUN npm run build
|
||||
|
||||
FROM nginx:1.29-alpine
|
||||
|
||||
COPY --from=build /src/dist /usr/share/nginx/html
|
||||
COPY deploy/nginx/default.conf /etc/nginx/conf.d/default.conf
|
||||
EXPOSE 80
|
||||
@@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>哈夫币租号平台</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
Generated
+2201
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "hfb-sys-frontend",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --host 0.0.0.0",
|
||||
"build": "vue-tsc -b && vite build",
|
||||
"preview": "vite preview --host 0.0.0.0",
|
||||
"typecheck": "vue-tsc -b"
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.1",
|
||||
"axios": "^1.13.2",
|
||||
"element-plus": "^2.11.9",
|
||||
"pinia": "^3.0.4",
|
||||
"vue": "^3.5.24",
|
||||
"vue-router": "^4.6.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.10.1",
|
||||
"@vitejs/plugin-vue": "^6.0.2",
|
||||
"@vue/tsconfig": "^0.8.1",
|
||||
"typescript": "~5.9.3",
|
||||
"vite": "^7.2.4",
|
||||
"vue-tsc": "^3.1.5"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { RouterView } from 'vue-router'
|
||||
|
||||
import AppLayout from './layouts/AppLayout.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AppLayout>
|
||||
<RouterView />
|
||||
</AppLayout>
|
||||
</template>
|
||||
@@ -0,0 +1,6 @@
|
||||
import axios from 'axios'
|
||||
|
||||
export const apiClient = axios.create({
|
||||
baseURL: '/api',
|
||||
timeout: 10000,
|
||||
})
|
||||
@@ -0,0 +1,32 @@
|
||||
<script setup lang="ts">
|
||||
import { Bell, House, Shop, Tickets, UserFilled, Wallet } from '@element-plus/icons-vue'
|
||||
|
||||
const navItems = [
|
||||
{ label: '首页', to: '/', icon: House },
|
||||
{ label: '租号', to: '/listings', icon: Shop },
|
||||
{ label: '订单', to: '/orders', icon: Tickets },
|
||||
{ label: '钱包', to: '/wallet', icon: Wallet },
|
||||
{ label: '通知', to: '/notifications', icon: Bell },
|
||||
{ label: '实名', to: '/realname', icon: UserFilled },
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="app-shell">
|
||||
<aside class="sidebar">
|
||||
<RouterLink class="brand" to="/">
|
||||
<span class="brand-mark">H</span>
|
||||
<span>哈夫币租号</span>
|
||||
</RouterLink>
|
||||
<nav class="nav">
|
||||
<RouterLink v-for="item in navItems" :key="item.to" class="nav-link" :to="item.to">
|
||||
<el-icon><component :is="item.icon" /></el-icon>
|
||||
<span>{{ item.label }}</span>
|
||||
</RouterLink>
|
||||
</nav>
|
||||
</aside>
|
||||
<main class="main">
|
||||
<slot />
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,11 @@
|
||||
import 'element-plus/dist/index.css'
|
||||
import './styles/base.css'
|
||||
|
||||
import ElementPlus from 'element-plus'
|
||||
import { createPinia } from 'pinia'
|
||||
import { createApp } from 'vue'
|
||||
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
|
||||
createApp(App).use(createPinia()).use(router).use(ElementPlus).mount('#app')
|
||||
@@ -0,0 +1,24 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes: [
|
||||
{ path: '/', name: 'home', component: () => import('@/views/public/HomeView.vue') },
|
||||
{ path: '/listings', name: 'listings', component: () => import('@/views/public/ListingsView.vue') },
|
||||
{ path: '/listings/:id', name: 'listing-detail', component: () => import('@/views/public/ListingDetailView.vue') },
|
||||
{ path: '/orders/create', name: 'order-create', component: () => import('@/views/account/OrderCreateView.vue') },
|
||||
{ path: '/orders', name: 'orders', component: () => import('@/views/account/OrdersView.vue') },
|
||||
{ path: '/orders/:id', name: 'order-detail', component: () => import('@/views/account/OrderDetailView.vue') },
|
||||
{ path: '/wallet', name: 'wallet', component: () => import('@/views/account/WalletView.vue') },
|
||||
{ path: '/notifications', name: 'notifications', component: () => import('@/views/account/NotificationsView.vue') },
|
||||
{ path: '/realname', name: 'realname', component: () => import('@/views/account/RealnameView.vue') },
|
||||
{ path: '/seller/listings', name: 'seller-listings', component: () => import('@/views/seller/SellerListingsView.vue') },
|
||||
{ path: '/seller/handoffs', name: 'seller-handoffs', component: () => import('@/views/seller/SellerHandoffsView.vue') },
|
||||
{ path: '/seller/earnings', name: 'seller-earnings', component: () => import('@/views/seller/SellerEarningsView.vue') },
|
||||
{ path: '/admin/dashboard', name: 'admin-dashboard', component: () => import('@/views/admin/AdminDashboardView.vue') },
|
||||
{ path: '/admin/disputes', name: 'admin-disputes', component: () => import('@/views/admin/AdminDisputesView.vue') },
|
||||
{ path: '/admin/system-configs', name: 'admin-system-configs', component: () => import('@/views/admin/AdminSystemConfigsView.vue') },
|
||||
],
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -0,0 +1,9 @@
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useSessionStore = defineStore('session', {
|
||||
state: () => ({
|
||||
token: '',
|
||||
phone: '',
|
||||
realnameStatus: 'unknown',
|
||||
}),
|
||||
})
|
||||
@@ -0,0 +1,164 @@
|
||||
:root {
|
||||
color: #1f2933;
|
||||
background: #f6f8fb;
|
||||
font-family:
|
||||
Inter, "PingFang SC", "Microsoft YaHei", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
|
||||
sans-serif;
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.app-shell {
|
||||
display: grid;
|
||||
min-height: 100vh;
|
||||
grid-template-columns: 248px 1fr;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
border-right: 1px solid #e4e7ed;
|
||||
background: #ffffff;
|
||||
padding: 20px 16px;
|
||||
}
|
||||
|
||||
.brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-height: 44px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.brand-mark {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 8px;
|
||||
background: #0f766e;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.nav {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-height: 40px;
|
||||
border-radius: 8px;
|
||||
padding: 0 12px;
|
||||
color: #52616f;
|
||||
}
|
||||
|
||||
.nav-link.router-link-active {
|
||||
background: #e8f5f2;
|
||||
color: #0f766e;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.main {
|
||||
min-width: 0;
|
||||
padding: 32px;
|
||||
}
|
||||
|
||||
.page {
|
||||
max-width: 1120px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
max-width: 720px;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
margin: 0 0 8px;
|
||||
color: #0f766e;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: 32px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.page-header p:last-child {
|
||||
margin: 12px 0 0;
|
||||
color: #52616f;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.metric-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 16px;
|
||||
margin-top: 28px;
|
||||
}
|
||||
|
||||
.metric-card {
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.metric-card span {
|
||||
display: block;
|
||||
color: #6b7785;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.metric-card strong {
|
||||
display: block;
|
||||
margin-top: 8px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.app-shell {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
border-right: 0;
|
||||
border-bottom: 1px solid #e4e7ed;
|
||||
}
|
||||
|
||||
.nav {
|
||||
grid-auto-flow: column;
|
||||
grid-auto-columns: max-content;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.main {
|
||||
padding: 24px 16px;
|
||||
}
|
||||
|
||||
.metric-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<section class="page">
|
||||
<div class="page-header">
|
||||
<p class="eyebrow">Notifications</p>
|
||||
<h1>站内信</h1>
|
||||
<p>接收审核、交接、归还、申诉和仲裁结果通知。</p>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
@@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<section class="page">
|
||||
<div class="page-header">
|
||||
<p class="eyebrow">Create Order</p>
|
||||
<h1>创建订单</h1>
|
||||
<p>确认租期、租金、押金和账号资产快照。</p>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
@@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<section class="page">
|
||||
<div class="page-header">
|
||||
<p class="eyebrow">Order Timeline</p>
|
||||
<h1>订单详情</h1>
|
||||
<p>展示状态机时间线、交接记录、归还入口和申诉入口。</p>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
@@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<section class="page">
|
||||
<div class="page-header">
|
||||
<p class="eyebrow">Orders</p>
|
||||
<h1>我的订单</h1>
|
||||
<p>跟踪待交接、租赁中、待归还、申诉中和已完成订单。</p>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
@@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<section class="page">
|
||||
<div class="page-header">
|
||||
<p class="eyebrow">Realname</p>
|
||||
<h1>实名认证</h1>
|
||||
<p>号主发布前必须完成实名认证。</p>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
@@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<section class="page">
|
||||
<div class="page-header">
|
||||
<p class="eyebrow">Wallet</p>
|
||||
<h1>钱包</h1>
|
||||
<p>查看可用余额、冻结余额和不可变资金流水。</p>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
@@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<section class="page">
|
||||
<div class="page-header">
|
||||
<p class="eyebrow">Admin</p>
|
||||
<h1>后台仪表盘</h1>
|
||||
<p>待审核、待仲裁、订单和流水概览。</p>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
@@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<section class="page">
|
||||
<div class="page-header">
|
||||
<p class="eyebrow">Arbitration</p>
|
||||
<h1>仲裁中心</h1>
|
||||
<p>处理无法登录、资产损失、哈夫币争议和超时归还。</p>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
@@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<section class="page">
|
||||
<div class="page-header">
|
||||
<p class="eyebrow">Configs</p>
|
||||
<h1>系统配置</h1>
|
||||
<p>管理交接超时、归还超时、短信限流、最低押金和抽成比例。</p>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
@@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<section class="page">
|
||||
<div class="page-header">
|
||||
<p class="eyebrow">Delta Force Rental</p>
|
||||
<h1>哈夫币租号平台</h1>
|
||||
<p>先把账号发布、交接、订单、押金和仲裁链路打牢,再接真实支付和更复杂的风控。</p>
|
||||
</div>
|
||||
<div class="metric-grid">
|
||||
<div class="metric-card">
|
||||
<span>一期路线</span>
|
||||
<strong>自研业务核心</strong>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<span>交接模式</span>
|
||||
<strong>号主手动交接</strong>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<span>资金模型</span>
|
||||
<strong>先做账务</strong>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
@@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<section class="page">
|
||||
<div class="page-header">
|
||||
<p class="eyebrow">Listing Detail</p>
|
||||
<h1>租号详情</h1>
|
||||
<p>展示账号资产、哈夫币快照、租金押金、号主信用和风险提示。</p>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
@@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<section class="page">
|
||||
<div class="page-header">
|
||||
<p class="eyebrow">Listings</p>
|
||||
<h1>租号列表</h1>
|
||||
<p>后续支持区服、平台、价格、押金、段位和哈夫币数量筛选。</p>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
@@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<section class="page">
|
||||
<div class="page-header">
|
||||
<p class="eyebrow">Earnings</p>
|
||||
<h1>收益流水</h1>
|
||||
<p>查看租金、平台抽成、结算和冻结金额。</p>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
@@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<section class="page">
|
||||
<div class="page-header">
|
||||
<p class="eyebrow">Handoffs</p>
|
||||
<h1>交接管理</h1>
|
||||
<p>处理待交接、待归还确认和超时订单。</p>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
@@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<section class="page">
|
||||
<div class="page-header">
|
||||
<p class="eyebrow">Seller</p>
|
||||
<h1>我的发布</h1>
|
||||
<p>管理账号发布、审核状态、上架和下架。</p>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo",
|
||||
"types": ["vite/client"],
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"target": "ES2023",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"types": ["node"],
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"noEmit": true,
|
||||
"strict": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
port: 5173,
|
||||
proxy: {
|
||||
'/api': 'http://127.0.0.1:8080',
|
||||
},
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user