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