init
This commit is contained in:
20
xiangj-adapter/Dockerfile
Normal file
20
xiangj-adapter/Dockerfile
Normal file
@@ -0,0 +1,20 @@
|
||||
FROM golang:1.25 AS builder
|
||||
WORKDIR /app
|
||||
ENV GOPROXY=https://goproxy.cn,direct
|
||||
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
COPY . .
|
||||
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -o /app/server ./cmd/server
|
||||
RUN mkdir -p /app/log
|
||||
|
||||
FROM gcr.io/distroless/base-debian12
|
||||
WORKDIR /app
|
||||
COPY --from=builder /app/server /app/server
|
||||
COPY --from=builder /app/config.yaml /app/config.yaml
|
||||
COPY --from=builder /app/log /app/log
|
||||
|
||||
EXPOSE 21056
|
||||
ENTRYPOINT ["/app/server"]
|
||||
CMD ["-config", "/app/config.yaml"]
|
||||
96
xiangj-adapter/cmd/server/main.go
Normal file
96
xiangj-adapter/cmd/server/main.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
fiblogger "github.com/gofiber/fiber/v2/middleware/logger"
|
||||
"github.com/gofiber/fiber/v2/middleware/recover"
|
||||
|
||||
"xiangj-adapter/internal/apperror"
|
||||
"xiangj-adapter/internal/config"
|
||||
"xiangj-adapter/internal/controller"
|
||||
"xiangj-adapter/internal/db"
|
||||
"xiangj-adapter/internal/logger"
|
||||
"xiangj-adapter/internal/model"
|
||||
"xiangj-adapter/internal/router"
|
||||
"xiangj-adapter/internal/service"
|
||||
)
|
||||
|
||||
func main() {
|
||||
configPath := flag.String("config", "config.yaml", "config file path")
|
||||
flag.Parse()
|
||||
|
||||
cfg, err := config.Load(*configPath)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
log, err := logger.New(logger.Options{
|
||||
Level: cfg.Log.Level,
|
||||
Output: cfg.Log.Output,
|
||||
FilePath: cfg.Log.FilePath,
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
pg, err := db.NewPostgres(db.PostgresConfig{
|
||||
Address: cfg.Database.Address,
|
||||
Username: cfg.Database.Username,
|
||||
Password: cfg.Database.Password,
|
||||
Database: cfg.Database.Database,
|
||||
SSLMode: cfg.Database.SSLMode,
|
||||
MaxOpenConns: cfg.Database.MaxOpenConns,
|
||||
MaxIdleConns: cfg.Database.MaxIdleConns,
|
||||
ConnMaxLifetime: cfg.Database.ConnMaxLifetime,
|
||||
ConnMaxIdleTime: cfg.Database.ConnMaxIdleTime,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("database init failed", "err", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer pg.Close()
|
||||
|
||||
app := fiber.New(fiber.Config{
|
||||
ErrorHandler: func(c *fiber.Ctx, err error) error {
|
||||
return handleError(c, err, log)
|
||||
},
|
||||
})
|
||||
|
||||
app.Use(recover.New())
|
||||
app.Use(fiblogger.New())
|
||||
|
||||
dataService := service.NewDataService(pg, cfg.Database.Schema)
|
||||
dataController := controller.NewDataController(dataService, log)
|
||||
router.Register(app, dataController)
|
||||
|
||||
log.Info("server starting", "address", cfg.Server.Address)
|
||||
if err := app.Listen(cfg.Server.Address); err != nil {
|
||||
log.Error("server stopped", "err", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func handleError(c *fiber.Ctx, err error, log *slog.Logger) error {
|
||||
status := fiber.StatusInternalServerError
|
||||
message := "internal error"
|
||||
|
||||
var appErr *apperror.AppError
|
||||
if errors.As(err, &appErr) {
|
||||
status = appErr.Status
|
||||
message = appErr.Message
|
||||
} else if fiberErr, ok := err.(*fiber.Error); ok {
|
||||
status = fiberErr.Code
|
||||
message = fiberErr.Message
|
||||
}
|
||||
|
||||
log.Error("request failed", "status", status, "path", c.Path(), "err", err)
|
||||
return c.Status(status).JSON(model.APIResponse{Data: nil, Error: message})
|
||||
}
|
||||
17
xiangj-adapter/config.yaml
Normal file
17
xiangj-adapter/config.yaml
Normal file
@@ -0,0 +1,17 @@
|
||||
server:
|
||||
address: "0.0.0.0:21056"
|
||||
log:
|
||||
level: "info"
|
||||
output: "console"
|
||||
file_path: "/app/log/app.log"
|
||||
database:
|
||||
address: "192.168.40.242:5432"
|
||||
username: "postgres"
|
||||
password: "root123"
|
||||
database: "ser"
|
||||
schema: "event"
|
||||
sslmode: "disable"
|
||||
max_open_conns: 10
|
||||
max_idle_conns: 5
|
||||
conn_max_lifetime: "30m"
|
||||
conn_max_idle_time: "5m"
|
||||
13
xiangj-adapter/docker-compose.yml
Normal file
13
xiangj-adapter/docker-compose.yml
Normal file
@@ -0,0 +1,13 @@
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
xiangj-adapter:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "21056:21056"
|
||||
volumes:
|
||||
- ./config.yaml:/app/config.yaml:ro
|
||||
- ./logs:/app/log
|
||||
restart: unless-stopped
|
||||
31
xiangj-adapter/go.mod
Normal file
31
xiangj-adapter/go.mod
Normal file
@@ -0,0 +1,31 @@
|
||||
module xiangj-adapter
|
||||
|
||||
go 1.25
|
||||
|
||||
require (
|
||||
github.com/gofiber/fiber/v2 v2.52.9
|
||||
github.com/jackc/pgx/v5 v5.6.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/andybalholm/brotli v1.1.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.1 // indirect
|
||||
github.com/klauspost/compress v1.17.9 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasthttp v1.51.0 // indirect
|
||||
github.com/valyala/tcplisten v1.0.0 // indirect
|
||||
golang.org/x/crypto v0.17.0 // indirect
|
||||
golang.org/x/sync v0.1.0 // indirect
|
||||
golang.org/x/sys v0.28.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
)
|
||||
76
xiangj-adapter/go.sum
Normal file
76
xiangj-adapter/go.sum
Normal file
@@ -0,0 +1,76 @@
|
||||
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
|
||||
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
||||
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
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/gofiber/fiber/v2 v2.52.5 h1:tWoP1MJQjGEe4GB5TUGOi7P2E0ZMMRx5ZTG4rT+yGMo=
|
||||
github.com/gofiber/fiber/v2 v2.52.5/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ=
|
||||
github.com/gofiber/fiber/v2 v2.52.9 h1:YjKl5DOiyP3j0mO61u3NTmK7or8GzzWzCFzkboyP5cw=
|
||||
github.com/gofiber/fiber/v2 v2.52.9/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
|
||||
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
|
||||
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.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-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/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.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
|
||||
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM=
|
||||
github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
||||
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
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/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
|
||||
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
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/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
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.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
|
||||
github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g=
|
||||
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
|
||||
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
||||
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
|
||||
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
||||
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
|
||||
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
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=
|
||||
20
xiangj-adapter/internal/apperror/apperror.go
Normal file
20
xiangj-adapter/internal/apperror/apperror.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package apperror
|
||||
|
||||
import "github.com/gofiber/fiber/v2"
|
||||
|
||||
type AppError struct {
|
||||
Status int
|
||||
Message string
|
||||
}
|
||||
|
||||
func (e *AppError) Error() string {
|
||||
return e.Message
|
||||
}
|
||||
|
||||
func BadRequest(message string) *AppError {
|
||||
return &AppError{Status: fiber.StatusBadRequest, Message: message}
|
||||
}
|
||||
|
||||
func Internal(message string) *AppError {
|
||||
return &AppError{Status: fiber.StatusInternalServerError, Message: message}
|
||||
}
|
||||
88
xiangj-adapter/internal/config/config.go
Normal file
88
xiangj-adapter/internal/config/config.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Server ServerConfig `yaml:"server"`
|
||||
Database DatabaseConfig `yaml:"database"`
|
||||
Log LogConfig `yaml:"log"`
|
||||
}
|
||||
|
||||
type ServerConfig struct {
|
||||
Address string `yaml:"address"`
|
||||
}
|
||||
|
||||
type DatabaseConfig struct {
|
||||
Address string `yaml:"address"`
|
||||
Username string `yaml:"username"`
|
||||
Password string `yaml:"password"`
|
||||
Database string `yaml:"database"`
|
||||
Schema string `yaml:"schema"`
|
||||
SSLMode string `yaml:"sslmode"`
|
||||
MaxOpenConns int `yaml:"max_open_conns"`
|
||||
MaxIdleConns int `yaml:"max_idle_conns"`
|
||||
ConnMaxLifetime string `yaml:"conn_max_lifetime"`
|
||||
ConnMaxIdleTime string `yaml:"conn_max_idle_time"`
|
||||
}
|
||||
|
||||
type LogConfig struct {
|
||||
Level string `yaml:"level"`
|
||||
Output string `yaml:"output"`
|
||||
FilePath string `yaml:"file_path"`
|
||||
}
|
||||
|
||||
func Load(path string) (*Config, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read config: %w", err)
|
||||
}
|
||||
|
||||
var cfg Config
|
||||
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
||||
return nil, fmt.Errorf("parse config: %w", err)
|
||||
}
|
||||
|
||||
applyDefaults(&cfg)
|
||||
return &cfg, nil
|
||||
}
|
||||
|
||||
func applyDefaults(cfg *Config) {
|
||||
if cfg.Server.Address == "" {
|
||||
cfg.Server.Address = ":21056"
|
||||
}
|
||||
if cfg.Log.Level == "" {
|
||||
cfg.Log.Level = "info"
|
||||
}
|
||||
if cfg.Log.Output == "" {
|
||||
cfg.Log.Output = "console"
|
||||
}
|
||||
if cfg.Log.FilePath == "" {
|
||||
cfg.Log.FilePath = "/app/log/app.log"
|
||||
}
|
||||
if cfg.Database.MaxOpenConns == 0 {
|
||||
cfg.Database.MaxOpenConns = 10
|
||||
}
|
||||
if cfg.Database.MaxIdleConns == 0 {
|
||||
cfg.Database.MaxIdleConns = 5
|
||||
}
|
||||
if cfg.Database.Address == "" {
|
||||
cfg.Database.Address = "localhost:5432"
|
||||
}
|
||||
if cfg.Database.Schema == "" {
|
||||
cfg.Database.Schema = "public"
|
||||
}
|
||||
if cfg.Database.SSLMode == "" {
|
||||
cfg.Database.SSLMode = "disable"
|
||||
}
|
||||
if cfg.Database.ConnMaxLifetime == "" {
|
||||
cfg.Database.ConnMaxLifetime = "30m"
|
||||
}
|
||||
if cfg.Database.ConnMaxIdleTime == "" {
|
||||
cfg.Database.ConnMaxIdleTime = "5m"
|
||||
}
|
||||
}
|
||||
106
xiangj-adapter/internal/controller/data_controller.go
Normal file
106
xiangj-adapter/internal/controller/data_controller.go
Normal file
@@ -0,0 +1,106 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
||||
"xiangj-adapter/internal/apperror"
|
||||
"xiangj-adapter/internal/model"
|
||||
"xiangj-adapter/internal/service"
|
||||
)
|
||||
|
||||
type DataController struct {
|
||||
service *service.DataService
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
func NewDataController(service *service.DataService, log *slog.Logger) *DataController {
|
||||
return &DataController{service: service, log: log}
|
||||
}
|
||||
|
||||
func (c *DataController) GetData(ctx *fiber.Ctx) error {
|
||||
op := strings.ToLower(ctx.Query("op"))
|
||||
if op == "" {
|
||||
op = "select"
|
||||
}
|
||||
switch op {
|
||||
case "select":
|
||||
return c.handleSelect(ctx)
|
||||
default:
|
||||
return apperror.BadRequest("op not supported")
|
||||
}
|
||||
}
|
||||
|
||||
func (c *DataController) handleSelect(ctx *fiber.Ctx) error {
|
||||
doid := ctx.Query("doid")
|
||||
table := strings.ToLower(lastSegment(doid))
|
||||
if table == "" {
|
||||
return apperror.BadRequest("doid is required")
|
||||
}
|
||||
|
||||
offset, err := parseNonNegativeIntDefault(ctx.Query("offset"), "offset", 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
count, err := parsePositiveIntDefault(ctx.Query("count"), "count", 100)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data, err := c.service.Select(ctx.Context(), table, offset, count)
|
||||
if err != nil {
|
||||
c.log.Error("select failed", "table", table, "err", err)
|
||||
return apperror.Internal("query failed")
|
||||
}
|
||||
|
||||
return ctx.JSON(model.APIResponse{Data: data, Error: ""})
|
||||
}
|
||||
|
||||
func lastSegment(input string) string {
|
||||
trimmed := strings.Trim(input, "/")
|
||||
if trimmed == "" {
|
||||
return ""
|
||||
}
|
||||
parts := strings.Split(trimmed, "/")
|
||||
return parts[len(parts)-1]
|
||||
}
|
||||
|
||||
func parseNonNegativeInt(value, field string) (int, error) {
|
||||
if value == "" {
|
||||
return 0, apperror.BadRequest(field + " is required")
|
||||
}
|
||||
parsed, err := strconv.Atoi(value)
|
||||
if err != nil || parsed < 0 {
|
||||
return 0, apperror.BadRequest(field + " must be a non-negative integer")
|
||||
}
|
||||
return parsed, nil
|
||||
}
|
||||
|
||||
func parsePositiveInt(value, field string) (int, error) {
|
||||
if value == "" {
|
||||
return 0, apperror.BadRequest(field + " is required")
|
||||
}
|
||||
parsed, err := strconv.Atoi(value)
|
||||
if err != nil || parsed <= 0 {
|
||||
return 0, apperror.BadRequest(field + " must be a positive integer")
|
||||
}
|
||||
return parsed, nil
|
||||
}
|
||||
|
||||
func parseNonNegativeIntDefault(value, field string, defaultValue int) (int, error) {
|
||||
if value == "" {
|
||||
return defaultValue, nil
|
||||
}
|
||||
return parseNonNegativeInt(value, field)
|
||||
}
|
||||
|
||||
func parsePositiveIntDefault(value, field string, defaultValue int) (int, error) {
|
||||
if value == "" {
|
||||
return defaultValue, nil
|
||||
}
|
||||
return parsePositiveInt(value, field)
|
||||
}
|
||||
93
xiangj-adapter/internal/db/postgres.go
Normal file
93
xiangj-adapter/internal/db/postgres.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
_ "github.com/jackc/pgx/v5/stdlib"
|
||||
)
|
||||
|
||||
type PostgresConfig struct {
|
||||
Address string
|
||||
Username string
|
||||
Password string
|
||||
Database string
|
||||
SSLMode string
|
||||
MaxOpenConns int
|
||||
MaxIdleConns int
|
||||
ConnMaxLifetime string
|
||||
ConnMaxIdleTime string
|
||||
}
|
||||
|
||||
func NewPostgres(cfg PostgresConfig) (*sql.DB, error) {
|
||||
if cfg.Address == "" {
|
||||
return nil, fmt.Errorf("database address is required")
|
||||
}
|
||||
if cfg.Username == "" {
|
||||
return nil, fmt.Errorf("database username is required")
|
||||
}
|
||||
if cfg.Database == "" {
|
||||
return nil, fmt.Errorf("database name is required")
|
||||
}
|
||||
|
||||
host, port := parseAddress(cfg.Address)
|
||||
if host == "" {
|
||||
return nil, fmt.Errorf("database address is invalid")
|
||||
}
|
||||
|
||||
connMaxLifetime, err := parseDuration(cfg.ConnMaxLifetime)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("conn_max_lifetime: %w", err)
|
||||
}
|
||||
connMaxIdleTime, err := parseDuration(cfg.ConnMaxIdleTime)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("conn_max_idle_time: %w", err)
|
||||
}
|
||||
|
||||
dsn := fmt.Sprintf(
|
||||
"host=%s port=%s user=%s password=%s dbname=%s sslmode=%s",
|
||||
host, port, cfg.Username, cfg.Password, cfg.Database, cfg.SSLMode,
|
||||
)
|
||||
|
||||
db, err := sql.Open("pgx", dsn)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open postgres: %w", err)
|
||||
}
|
||||
|
||||
db.SetMaxOpenConns(cfg.MaxOpenConns)
|
||||
db.SetMaxIdleConns(cfg.MaxIdleConns)
|
||||
db.SetConnMaxLifetime(connMaxLifetime)
|
||||
db.SetConnMaxIdleTime(connMaxIdleTime)
|
||||
|
||||
if err := db.Ping(); err != nil {
|
||||
return nil, fmt.Errorf("ping postgres: %w", err)
|
||||
}
|
||||
|
||||
return db, nil
|
||||
}
|
||||
|
||||
func parseAddress(address string) (string, string) {
|
||||
address = strings.TrimSpace(address)
|
||||
if address == "" {
|
||||
return "", ""
|
||||
}
|
||||
|
||||
if strings.Contains(address, ":") {
|
||||
host, port, err := net.SplitHostPort(address)
|
||||
if err == nil && host != "" && port != "" {
|
||||
return host, port
|
||||
}
|
||||
}
|
||||
|
||||
return address, "5432"
|
||||
}
|
||||
|
||||
func parseDuration(value string) (time.Duration, error) {
|
||||
if strings.TrimSpace(value) == "" {
|
||||
return 0, nil
|
||||
}
|
||||
return time.ParseDuration(value)
|
||||
}
|
||||
44
xiangj-adapter/internal/logger/logger.go
Normal file
44
xiangj-adapter/internal/logger/logger.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
Level string
|
||||
Output string
|
||||
FilePath string
|
||||
}
|
||||
|
||||
func New(opts Options) (*slog.Logger, error) {
|
||||
var lvl slog.Level
|
||||
if err := lvl.UnmarshalText([]byte(opts.Level)); err != nil {
|
||||
lvl = slog.LevelInfo
|
||||
}
|
||||
|
||||
output := strings.ToLower(strings.TrimSpace(opts.Output))
|
||||
switch output {
|
||||
case "", "console":
|
||||
handler := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: lvl})
|
||||
return slog.New(handler), nil
|
||||
case "file":
|
||||
if strings.TrimSpace(opts.FilePath) == "" {
|
||||
return nil, fmt.Errorf("log file path is required")
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(opts.FilePath), 0o755); err != nil {
|
||||
return nil, fmt.Errorf("create log directory: %w", err)
|
||||
}
|
||||
file, err := os.OpenFile(opts.FilePath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open log file: %w", err)
|
||||
}
|
||||
handler := slog.NewTextHandler(file, &slog.HandlerOptions{Level: lvl})
|
||||
return slog.New(handler), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported log output: %s", opts.Output)
|
||||
}
|
||||
}
|
||||
6
xiangj-adapter/internal/model/response.go
Normal file
6
xiangj-adapter/internal/model/response.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package model
|
||||
|
||||
type APIResponse struct {
|
||||
Data any `json:"data"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
11
xiangj-adapter/internal/router/router.go
Normal file
11
xiangj-adapter/internal/router/router.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
||||
"xiangj-adapter/internal/controller"
|
||||
)
|
||||
|
||||
func Register(app *fiber.App, dataController *controller.DataController) {
|
||||
app.Get("/data", dataController.GetData)
|
||||
}
|
||||
79
xiangj-adapter/internal/service/data_service.go
Normal file
79
xiangj-adapter/internal/service/data_service.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
var tableNameRegex = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*$`)
|
||||
|
||||
type DataService struct {
|
||||
db *sql.DB
|
||||
schema string
|
||||
}
|
||||
|
||||
func NewDataService(db *sql.DB, schema string) *DataService {
|
||||
return &DataService{db: db, schema: schema}
|
||||
}
|
||||
|
||||
func (s *DataService) Select(ctx context.Context, table string, offset, count int) ([]map[string]any, error) {
|
||||
if !tableNameRegex.MatchString(table) {
|
||||
return nil, fmt.Errorf("invalid table name")
|
||||
}
|
||||
if s.schema != "" && !tableNameRegex.MatchString(s.schema) {
|
||||
return nil, fmt.Errorf("invalid schema name")
|
||||
}
|
||||
|
||||
qualifiedTable := table
|
||||
if s.schema != "" {
|
||||
qualifiedTable = fmt.Sprintf("%s.%s", s.schema, table)
|
||||
}
|
||||
|
||||
query := fmt.Sprintf("SELECT * FROM %s LIMIT $2 OFFSET $1", qualifiedTable)
|
||||
rows, err := s.db.QueryContext(ctx, query, offset, count)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("query table %s: %w", table, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
return scanRows(rows)
|
||||
}
|
||||
|
||||
func scanRows(rows *sql.Rows) ([]map[string]any, error) {
|
||||
columns, err := rows.Columns()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read columns: %w", err)
|
||||
}
|
||||
|
||||
results := make([]map[string]any, 0)
|
||||
values := make([]any, len(columns))
|
||||
valuePtrs := make([]any, len(columns))
|
||||
for i := range values {
|
||||
valuePtrs[i] = &values[i]
|
||||
}
|
||||
|
||||
for rows.Next() {
|
||||
if err := rows.Scan(valuePtrs...); err != nil {
|
||||
return nil, fmt.Errorf("scan row: %w", err)
|
||||
}
|
||||
|
||||
row := make(map[string]any, len(columns))
|
||||
for i, col := range columns {
|
||||
val := values[i]
|
||||
if b, ok := val.([]byte); ok {
|
||||
row[col] = string(b)
|
||||
continue
|
||||
}
|
||||
row[col] = val
|
||||
}
|
||||
results = append(results, row)
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("iterate rows: %w", err)
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
Reference in New Issue
Block a user