u
This commit is contained in:
1 parent
c6671c8a8c
commit
f0cb963d23
76 files changed
+6467
-498
No files matched your search
@@ -0,0 +1,58 @@
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"allapp-go/internal/bootstrap/boots"
|
||||
"allapp-go/internal/config"
|
||||
"allapp-go/pkg/db"
|
||||
"allapp-go/pkg/logger"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
Config *config.Config
|
||||
PgSql *pgxpool.Pool
|
||||
}
|
||||
|
||||
// ======================
|
||||
// 初始化
|
||||
// ======================
|
||||
|
||||
func NewApp() (*App, error) {
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 日志
|
||||
boots.InitLogger(cfg)
|
||||
|
||||
// 数据库
|
||||
pgsql, err := boots.InitPgsql(cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
db.Init(pgsql)
|
||||
|
||||
// 其他组件
|
||||
boots.InitUniqueId(cfg)
|
||||
boots.InitJwt(cfg)
|
||||
|
||||
return &App{
|
||||
Config: cfg,
|
||||
PgSql: pgsql,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ======================
|
||||
// 释放资源
|
||||
// ======================
|
||||
|
||||
func (a *App) Close() {
|
||||
if a.PgSql != nil {
|
||||
a.PgSql.Close()
|
||||
}
|
||||
|
||||
logger.Sync()
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package boots
|
||||
|
||||
import (
|
||||
"allapp-go/internal/config"
|
||||
"allapp-go/pkg/logger"
|
||||
)
|
||||
|
||||
func InitLogger(cfg *config.Config) {
|
||||
logger.Init(logger.Config{
|
||||
Dir: "logs",
|
||||
Level: "info",
|
||||
MaxSize: 100, // MB
|
||||
MaxBackups: 10,
|
||||
MaxAge: 7, // 天
|
||||
Compress: true,
|
||||
})
|
||||
defer logger.Sync()
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package boots
|
||||
|
||||
import (
|
||||
"allapp-go/internal/config"
|
||||
"allapp-go/pkg/jwtx"
|
||||
)
|
||||
|
||||
// InitJwt 初始化Jwt
|
||||
func InitJwt(cfg *config.Config) {
|
||||
jwtx.Init(cfg.JWT.Secret, cfg.JWT.AccessExpiry)
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package boots
|
||||
|
||||
import (
|
||||
"allapp-go/internal/config"
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
func InitPgsql(cfg *config.Config) (*pgxpool.Pool, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
pg := cfg.Postgres
|
||||
|
||||
// ssl
|
||||
sslMode := "disable"
|
||||
if pg.SslMode {
|
||||
sslMode = "require"
|
||||
}
|
||||
|
||||
// 密码转义
|
||||
password := url.QueryEscape(pg.Password)
|
||||
|
||||
// DSN
|
||||
dsn := fmt.Sprintf(
|
||||
"postgres://%s:%s@%s:%d/%s?sslmode=%s",
|
||||
pg.User,
|
||||
password,
|
||||
pg.Host,
|
||||
pg.Port,
|
||||
pg.Dbname,
|
||||
sslMode,
|
||||
)
|
||||
|
||||
// 解析配置
|
||||
conf, err := pgxpool.ParseConfig(dsn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// ======================
|
||||
// 连接池配置
|
||||
// ======================
|
||||
|
||||
// 最大连接数
|
||||
if pg.MaxOpenConns > 0 {
|
||||
conf.MaxConns = int32(pg.MaxOpenConns)
|
||||
}
|
||||
|
||||
// 最小连接数(近似 idle)
|
||||
if pg.MaxIdleConns > 0 {
|
||||
conf.MinConns = int32(pg.MaxIdleConns)
|
||||
}
|
||||
|
||||
// 连接最大生命周期
|
||||
if pg.ConnMaxLifetime > 0 {
|
||||
conf.MaxConnLifetime = pg.ConnMaxLifetime
|
||||
}
|
||||
|
||||
// ======================
|
||||
// 时区设置(正确方式 ⭐)
|
||||
// ======================
|
||||
if pg.TimeZone != "" {
|
||||
conf.ConnConfig.RuntimeParams["TimeZone"] = pg.TimeZone
|
||||
}
|
||||
|
||||
// 创建连接池
|
||||
db, err := pgxpool.NewWithConfig(ctx, conf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 测试连接
|
||||
if err := db.Ping(ctx); err != nil {
|
||||
db.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return db, nil
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package boots
|
||||
|
||||
import (
|
||||
"allapp-go/internal/config"
|
||||
"allapp-go/pkg/uniqueid"
|
||||
)
|
||||
|
||||
// InitUniqueId 初始化雪花id
|
||||
func InitUniqueId(cfg *config.Config) {
|
||||
options := uniqueid.NewIdGeneratorOptions(cfg.UniqueID.WorkerID)
|
||||
uniqueid.SetIdGenerator(options)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"allapp-go/internal/config"
|
||||
"allapp-go/internal/errors"
|
||||
"allapp-go/internal/middleware"
|
||||
"allapp-go/internal/router"
|
||||
"allapp-go/pkg/logger"
|
||||
"fmt"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
"github.com/gofiber/fiber/v3/middleware/recover"
|
||||
"github.com/gofiber/fiber/v3/middleware/requestid"
|
||||
)
|
||||
|
||||
func RunServer(cfg *config.Config) error {
|
||||
app := fiber.New(fiber.Config{
|
||||
ErrorHandler: errors.ErrorHandler(),
|
||||
})
|
||||
|
||||
// ===== middleware =====
|
||||
app.Use(recover.New())
|
||||
app.Use(requestid.New())
|
||||
app.Use(middleware.Logger())
|
||||
|
||||
// ===== router =====
|
||||
router.SetupRouter(app)
|
||||
|
||||
addr := fmt.Sprintf(":%d", cfg.App.Port)
|
||||
|
||||
logger.Log.Info("服务启动")
|
||||
|
||||
return app.Listen(addr)
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package config
|
||||
|
||||
import "time"
|
||||
|
||||
type Config struct {
|
||||
App AppConfig `mapstructure:"app"`
|
||||
JWT JwtConfig `mapstructure:"jwt"`
|
||||
UniqueID UniqueIDConfig `mapstructure:"unique_id"`
|
||||
Postgres PostgresConfig `mapstructure:"postgres"`
|
||||
}
|
||||
|
||||
type AppConfig struct {
|
||||
Name string `mapstructure:"name"`
|
||||
Port int `mapstructure:"port"`
|
||||
}
|
||||
|
||||
type PostgresConfig struct {
|
||||
Host string `mapstructure:"host"`
|
||||
Port int `mapstructure:"port"`
|
||||
User string `mapstructure:"user"`
|
||||
Password string `mapstructure:"password"`
|
||||
Dbname string `mapstructure:"dbname"`
|
||||
SslMode bool `mapstructure:"ssl_mode"`
|
||||
TimeZone string `mapstructure:"timezone"`
|
||||
MaxOpenConns int32 `mapstructure:"max_open_conns"`
|
||||
MaxIdleConns int32 `mapstructure:"max_idle_conns"`
|
||||
ConnMaxLifetime time.Duration `mapstructure:"conn_max_lifetime"`
|
||||
}
|
||||
|
||||
type JwtConfig struct {
|
||||
Secret string `mapstructure:"secret"`
|
||||
AccessExpiry time.Duration `mapstructure:"access_expiry"`
|
||||
}
|
||||
|
||||
type UniqueIDConfig struct {
|
||||
DataCenterID uint16 `mapstructure:"datacenter_id"`
|
||||
WorkerID uint16 `mapstructure:"worker_id"`
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func Load() (*Config, error) {
|
||||
env := getEnv("APP_ENV", "dev")
|
||||
|
||||
v := viper.New()
|
||||
v.SetConfigType("yaml")
|
||||
|
||||
// ✅ 默认值
|
||||
setDefaults(v)
|
||||
|
||||
// ✅ 支持环境变量(如 APP_PORT → app.port)
|
||||
v.AutomaticEnv()
|
||||
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
||||
|
||||
// 基础配置
|
||||
v.AddConfigPath("configs")
|
||||
v.SetConfigName("config")
|
||||
|
||||
if err := v.ReadInConfig(); err != nil {
|
||||
return nil, fmt.Errorf("读取基础配置失败: %w", err)
|
||||
}
|
||||
|
||||
// 环境配置(覆盖)
|
||||
envConfigName := fmt.Sprintf("config.%s", env)
|
||||
v.SetConfigName(envConfigName)
|
||||
|
||||
if err := v.MergeInConfig(); err != nil {
|
||||
// dev 可以没有,prod 必须有
|
||||
if env == "prod" {
|
||||
return nil, fmt.Errorf("读取环境配置失败(%s): %w", env, err)
|
||||
}
|
||||
}
|
||||
|
||||
var cfg Config
|
||||
if err := v.Unmarshal(&cfg); err != nil {
|
||||
return nil, fmt.Errorf("解析配置失败: %w", err)
|
||||
}
|
||||
|
||||
return &cfg, nil
|
||||
}
|
||||
|
||||
// ✅ 默认值统一在这里管理
|
||||
func setDefaults(v *viper.Viper) {
|
||||
v.SetDefault("app.name", "allapp-go")
|
||||
v.SetDefault("app.port", 8080)
|
||||
|
||||
// 可以继续扩展
|
||||
v.SetDefault("unique_id.datacenter_id", 1)
|
||||
v.SetDefault("unique_id.worker_id", 1)
|
||||
}
|
||||
|
||||
func getEnv(key, defaultVal string) string {
|
||||
val := os.Getenv(key)
|
||||
if val == "" {
|
||||
return defaultVal
|
||||
}
|
||||
return strings.ToLower(val)
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package errors
|
||||
|
||||
import "github.com/pkg/errors"
|
||||
|
||||
func WithStack(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"allapp-go/internal/httpx"
|
||||
"allapp-go/pkg/logger"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func ErrorHandler() fiber.ErrorHandler {
|
||||
return func(c fiber.Ctx, err error) error {
|
||||
code := fiber.StatusInternalServerError
|
||||
msg := "系统异常,请稍后再试!"
|
||||
|
||||
var e *fiber.Error
|
||||
if errors.As(err, &e) {
|
||||
code = e.Code
|
||||
msg = e.Message
|
||||
}
|
||||
|
||||
logger.FromCtx(c.Context()).Error("HTTP 请求异常",
|
||||
zap.Int("status", code),
|
||||
zap.String("error", fmt.Sprintf("%+v", err)),
|
||||
)
|
||||
|
||||
return httpx.SystemError(c, msg)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package handle
|
||||
|
||||
import (
|
||||
"allapp-go/internal/errors"
|
||||
"allapp-go/internal/httpx"
|
||||
"allapp-go/internal/types"
|
||||
"allapp-go/pkg/db"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
func LoadData(c fiber.Ctx) error {
|
||||
var req types.LoadDataReq
|
||||
|
||||
err := httpx.BindAndValidate(c, &req)
|
||||
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
dbClient := db.New()
|
||||
// 查询
|
||||
data, err := dbClient.LoadData(c.Context(), req.ViewName, req.SearchCondition, req.OrderBy, req.SearchColumns, req.Args)
|
||||
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return httpx.OK(c, data)
|
||||
}
|
||||
|
||||
func LoadDataBySql(c fiber.Ctx) error {
|
||||
var req types.LoadDataBySqlReq
|
||||
|
||||
err := httpx.BindAndValidate(c, &req)
|
||||
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
dbClient := db.New()
|
||||
|
||||
data, err := dbClient.LoadDataBySQL(c.Context(), req.Sql, req.Args)
|
||||
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return httpx.OK(c, data)
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package handle
|
||||
|
||||
import (
|
||||
"allapp-go/internal/httpx"
|
||||
"allapp-go/internal/types"
|
||||
"allapp-go/pkg/db"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
func SaveData(c fiber.Ctx) error {
|
||||
var req []types.SaveDataReq
|
||||
|
||||
if err := httpx.BindAndValidate(c, &req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(req) == 0 {
|
||||
return httpx.OK(c, nil)
|
||||
}
|
||||
|
||||
ctx := c.Context()
|
||||
dbClient := db.New()
|
||||
|
||||
err := dbClient.WithTx(ctx, func(tx *db.Client) error {
|
||||
for _, dataReq := range req {
|
||||
tableName := dataReq.TableName
|
||||
keyField := dataReq.KeyField
|
||||
|
||||
for _, insert := range dataReq.Inserts {
|
||||
if err := tx.Insert(ctx, tableName, keyField, insert); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for _, update := range dataReq.Updates {
|
||||
if err := tx.Update(ctx, tableName, keyField, update); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for _, del := range dataReq.Deletes {
|
||||
if err := tx.Delete(ctx, tableName, keyField, del); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return httpx.OK(c, nil)
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package httpx
|
||||
|
||||
import (
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
var validate = validator.New()
|
||||
|
||||
func BindAndValidate(c fiber.Ctx, v any) error {
|
||||
if err := c.Bind().Body(v); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := validate.Struct(v); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package httpx
|
||||
|
||||
import "github.com/gofiber/fiber/v3"
|
||||
|
||||
type Result struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data interface{} `json:"data"`
|
||||
TrackId string `json:"track_id,omitempty"`
|
||||
}
|
||||
|
||||
var BizCode = struct {
|
||||
Success int // 成功
|
||||
Unauthorized int // 未授权/Token失效
|
||||
BusinessError int // 业务异常,例如参数错误、操作不允许
|
||||
SystemError int // 系统异常,例如数据库崩溃、未知错误
|
||||
}{
|
||||
Success: 1000,
|
||||
Unauthorized: 1001,
|
||||
BusinessError: 2000,
|
||||
SystemError: 3000,
|
||||
}
|
||||
|
||||
// ------------------- 统一响应方法 -------------------
|
||||
|
||||
// Respond 返回通用响应,code和msg必填,data可选
|
||||
func Respond(c fiber.Ctx, code int, msg string, data interface{}) error {
|
||||
trackId := ""
|
||||
if v := c.Context().Value("request_id"); v != nil {
|
||||
if s, ok := v.(string); ok {
|
||||
trackId = s
|
||||
}
|
||||
}
|
||||
|
||||
return c.JSON(Result{
|
||||
Code: code,
|
||||
Message: msg,
|
||||
Data: data,
|
||||
TrackId: trackId,
|
||||
})
|
||||
}
|
||||
|
||||
// ------------------- 便捷封装 -------------------
|
||||
|
||||
// OK 成功响应
|
||||
func OK(c fiber.Ctx, data interface{}) error {
|
||||
return Respond(c, BizCode.Success, "success", data)
|
||||
}
|
||||
|
||||
// Fail 业务异常
|
||||
func Fail(c fiber.Ctx, msg string) error {
|
||||
return Respond(c, BizCode.BusinessError, msg, nil)
|
||||
}
|
||||
|
||||
// SystemError 系统异常
|
||||
func SystemError(c fiber.Ctx, msg string) error {
|
||||
return Respond(c, BizCode.SystemError, msg, nil)
|
||||
}
|
||||
|
||||
// Unauthorized 授权失败
|
||||
func Unauthorized(c fiber.Ctx, msg string) error {
|
||||
return Respond(c, BizCode.Unauthorized, msg, nil)
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"allapp-go/internal/httpx"
|
||||
"allapp-go/pkg/jwtx"
|
||||
"strings"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
const CtxUserIDKey = "user_id"
|
||||
|
||||
// Auth 登录校验中间件
|
||||
func Auth() fiber.Handler {
|
||||
return func(c fiber.Ctx) error {
|
||||
token := strings.TrimSpace(c.Get("Token"))
|
||||
|
||||
if token == "" {
|
||||
return httpx.Unauthorized(c, "未登录")
|
||||
}
|
||||
|
||||
verifyToken, ok := jwtx.VerifyToken(c.Context(), token)
|
||||
if !ok {
|
||||
return httpx.Unauthorized(c, "登录已过期")
|
||||
}
|
||||
|
||||
c.Locals(CtxUserIDKey, verifyToken.Data["user_id"])
|
||||
|
||||
return c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func GetUserID(c fiber.Ctx) (int64, error) {
|
||||
v := c.Locals(CtxUserIDKey)
|
||||
|
||||
if v == nil {
|
||||
return 0, httpx.Unauthorized(c, "登录已过期")
|
||||
}
|
||||
|
||||
userID, ok := v.(int64)
|
||||
|
||||
if !ok {
|
||||
return 0, httpx.Unauthorized(c, "登录已过期")
|
||||
}
|
||||
|
||||
return userID, nil
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"allapp-go/pkg/logger"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func Logger() fiber.Handler {
|
||||
return func(c fiber.Ctx) error {
|
||||
traceID := c.RequestID()
|
||||
|
||||
logger.SetTraceID(c, traceID)
|
||||
|
||||
start := time.Now()
|
||||
|
||||
err := c.Next()
|
||||
|
||||
// ✅ 计算耗时
|
||||
cost := time.Since(start)
|
||||
|
||||
log := logger.FromCtx(c.Context())
|
||||
log.Info("HTTP 请求",
|
||||
zap.String("method", c.Method()),
|
||||
zap.String("path", c.Path()),
|
||||
zap.Int("status", c.Response().StatusCode()),
|
||||
zap.String("ip", c.IP()),
|
||||
zap.String("duration", cost.String()),
|
||||
)
|
||||
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"allapp-go/internal/handle"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
func SetupRouter(app *fiber.App) {
|
||||
// ==================== data ====================
|
||||
app.Post("/data/loadData", handle.LoadData)
|
||||
app.Post("/data/loadDataBySql", handle.LoadDataBySql)
|
||||
app.Post("/data/saveData", handle.SaveData)
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package types
|
||||
|
||||
type LoadDataReq struct {
|
||||
ViewName string `json:"view_name" validate:"required"`
|
||||
SearchCondition string `json:"search_condition"`
|
||||
OrderBy string `json:"order_by"`
|
||||
SearchColumns []string `json:"search_columns"`
|
||||
Args []any `json:"args"`
|
||||
}
|
||||
|
||||
type LoadDataBySqlReq struct {
|
||||
Sql string `json:"sql" validate:"required"`
|
||||
Args []any `json:"args"`
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package types
|
||||
|
||||
type SaveDataReq struct {
|
||||
TableName string `json:"table_name"`
|
||||
KeyField string `json:"key_field"`
|
||||
Inserts []map[string]any `json:"inserts"`
|
||||
Updates []map[string]any `json:"updates"`
|
||||
Deletes []map[string]any `json:"deletes"`
|
||||
}
|
||||
Reference in new issue
Block a user