首次提交
This commit is contained in:
Vendored
+16
@@ -0,0 +1,16 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/luxsin/app-api/internal/config"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
func NewClient(cfg config.RedisConfig) *redis.Client {
|
||||
return redis.NewClient(&redis.Options{
|
||||
Addr: fmt.Sprintf("%s:%d", cfg.Host, cfg.Port),
|
||||
Password: cfg.Password,
|
||||
DB: cfg.Database,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Env string
|
||||
Host string
|
||||
Port int
|
||||
Database DatabaseConfig
|
||||
Meilisearch MeilisearchConfig
|
||||
Redis RedisConfig
|
||||
}
|
||||
|
||||
func Load() (*Config, error) {
|
||||
port, err := strconv.Atoi(getEnv("APP_PORT", "8080"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid APP_PORT: %w", err)
|
||||
}
|
||||
|
||||
env := getEnv("APP_ENV", "development")
|
||||
|
||||
db, err := loadDatabase(env)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := db.validate(env); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ms := loadMeilisearch(env)
|
||||
if err := ms.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rd := loadRedis(env)
|
||||
if err := rd.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Config{
|
||||
Env: env,
|
||||
Host: getEnv("APP_HOST", "0.0.0.0"),
|
||||
Port: port,
|
||||
Database: db,
|
||||
Meilisearch: ms,
|
||||
Redis: rd,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Config) Addr() string {
|
||||
return fmt.Sprintf("%s:%d", c.Host, c.Port)
|
||||
}
|
||||
|
||||
func getEnv(key, fallback string) string {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
return v
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type DatabaseConfig struct {
|
||||
Host string
|
||||
Port int
|
||||
Name string
|
||||
User string
|
||||
Password string
|
||||
}
|
||||
|
||||
func loadDatabase(env string) (DatabaseConfig, error) {
|
||||
if os.Getenv("DATABASE_HOST") != "" {
|
||||
return databaseFromEnv()
|
||||
}
|
||||
|
||||
switch env {
|
||||
case "production":
|
||||
return DatabaseConfig{
|
||||
Host: "database-1.chmuueamo72p.eu-central-1.rds.amazonaws.com",
|
||||
Port: 3306,
|
||||
Name: "audio",
|
||||
User: "root",
|
||||
Password: os.Getenv("DATABASE_PASSWORD"),
|
||||
}, nil
|
||||
default:
|
||||
return DatabaseConfig{
|
||||
Host: "localhost",
|
||||
Port: 3306,
|
||||
Name: "audio",
|
||||
User: "root",
|
||||
Password: "root123",
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func databaseFromEnv() (DatabaseConfig, error) {
|
||||
port, err := strconv.Atoi(getEnv("DATABASE_PORT", "3306"))
|
||||
if err != nil {
|
||||
return DatabaseConfig{}, fmt.Errorf("invalid DATABASE_PORT: %w", err)
|
||||
}
|
||||
|
||||
return DatabaseConfig{
|
||||
Host: os.Getenv("DATABASE_HOST"),
|
||||
Port: port,
|
||||
Name: getEnv("DATABASE_NAME", "audio"),
|
||||
User: getEnv("DATABASE_USER", "root"),
|
||||
Password: os.Getenv("DATABASE_PASSWORD"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d DatabaseConfig) validate(env string) error {
|
||||
if d.Host == "" {
|
||||
return fmt.Errorf("database host is required")
|
||||
}
|
||||
if d.Name == "" {
|
||||
return fmt.Errorf("database name is required")
|
||||
}
|
||||
if d.User == "" {
|
||||
return fmt.Errorf("database user is required")
|
||||
}
|
||||
if env == "production" && d.Password == "" {
|
||||
return fmt.Errorf("DATABASE_PASSWORD is required in production")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
type MeilisearchConfig struct {
|
||||
Host string
|
||||
APIKey string
|
||||
Index string
|
||||
}
|
||||
|
||||
func loadMeilisearch(env string) MeilisearchConfig {
|
||||
if os.Getenv("MEILISEARCH_HOST") != "" {
|
||||
return MeilisearchConfig{
|
||||
Host: os.Getenv("MEILISEARCH_HOST"),
|
||||
APIKey: os.Getenv("MEILISEARCH_API_KEY"),
|
||||
Index: getEnv("MEILISEARCH_INDEX", "models"),
|
||||
}
|
||||
}
|
||||
|
||||
switch env {
|
||||
case "production":
|
||||
return MeilisearchConfig{
|
||||
Host: "http://ip-172-31-22-170.eu-central-1.compute.internal:7700",
|
||||
APIKey: getEnv("MEILISEARCH_API_KEY", "young9#!UJsD219921031"),
|
||||
Index: "models",
|
||||
}
|
||||
default:
|
||||
return MeilisearchConfig{
|
||||
Host: "http://ec2-18-184-205-87.eu-central-1.compute.amazonaws.com:7700",
|
||||
APIKey: getEnv("MEILISEARCH_API_KEY", "young9#!UJsD219921031"),
|
||||
Index: "models",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m MeilisearchConfig) validate() error {
|
||||
if m.Host == "" {
|
||||
return fmt.Errorf("meilisearch host is required")
|
||||
}
|
||||
if m.APIKey == "" {
|
||||
return fmt.Errorf("meilisearch api key is required")
|
||||
}
|
||||
if m.Index == "" {
|
||||
return fmt.Errorf("meilisearch index is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type RedisConfig struct {
|
||||
Host string
|
||||
Port int
|
||||
Password string
|
||||
Database int
|
||||
}
|
||||
|
||||
func loadRedis(env string) RedisConfig {
|
||||
if os.Getenv("REDIS_HOST") != "" {
|
||||
return redisFromEnv()
|
||||
}
|
||||
|
||||
switch env {
|
||||
case "production":
|
||||
return RedisConfig{
|
||||
Host: "172.31.38.162",
|
||||
Port: 16279,
|
||||
Password: getEnv("REDIS_PASSWORD", "eafon123!"),
|
||||
Database: 1,
|
||||
}
|
||||
default:
|
||||
return RedisConfig{
|
||||
Host: "ec2-3-69-138-29.eu-central-1.compute.amazonaws.com",
|
||||
Port: 16279,
|
||||
Password: getEnv("REDIS_PASSWORD", "eafon123!"),
|
||||
Database: 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func redisFromEnv() RedisConfig {
|
||||
port, _ := strconv.Atoi(getEnv("REDIS_PORT", "16279"))
|
||||
db, _ := strconv.Atoi(getEnv("REDIS_DATABASE", "1"))
|
||||
|
||||
return RedisConfig{
|
||||
Host: os.Getenv("REDIS_HOST"),
|
||||
Port: port,
|
||||
Password: os.Getenv("REDIS_PASSWORD"),
|
||||
Database: db,
|
||||
}
|
||||
}
|
||||
|
||||
func (r RedisConfig) validate() error {
|
||||
if r.Host == "" {
|
||||
return fmt.Errorf("redis host is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
"github.com/go-sql-driver/mysql"
|
||||
"github.com/luxsin/app-api/internal/config"
|
||||
)
|
||||
|
||||
func Open(cfg config.DatabaseConfig) (*sql.DB, error) {
|
||||
mc := mysql.Config{
|
||||
User: cfg.User,
|
||||
Passwd: cfg.Password,
|
||||
Net: "tcp",
|
||||
Addr: fmt.Sprintf("%s:%d", cfg.Host, cfg.Port),
|
||||
DBName: cfg.Name,
|
||||
Params: map[string]string{
|
||||
"charset": "utf8mb4",
|
||||
"parseTime": "True",
|
||||
"loc": "Local",
|
||||
},
|
||||
}
|
||||
|
||||
db, err := sql.Open("mysql", mc.FormatDSN())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open mysql: %w", err)
|
||||
}
|
||||
|
||||
db.SetMaxOpenConns(25)
|
||||
db.SetMaxIdleConns(5)
|
||||
db.SetConnMaxLifetime(5 * time.Minute)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := db.PingContext(ctx); err != nil {
|
||||
_ = db.Close()
|
||||
return nil, fmt.Errorf("ping mysql: %w", err)
|
||||
}
|
||||
|
||||
return db, nil
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/luxsin/app-api/internal/repository"
|
||||
"github.com/luxsin/app-api/internal/response"
|
||||
"github.com/luxsin/app-api/pkg/encode"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type BrandHandler struct {
|
||||
repo *repository.BrandRepository
|
||||
log *zap.Logger
|
||||
}
|
||||
|
||||
func NewBrandHandler(db *sql.DB, log *zap.Logger) *BrandHandler {
|
||||
return &BrandHandler{
|
||||
repo: repository.NewBrandRepository(db),
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *BrandHandler) GetBrand(c *gin.Context) {
|
||||
brandName := c.Query("brandName")
|
||||
base64Resp := encode.ParseBase64Param(c)
|
||||
|
||||
list, err := h.repo.List(c.Request.Context(), brandName)
|
||||
if err != nil {
|
||||
h.log.Error("get brand list failed", zap.Error(err))
|
||||
response.InternalError(c, "failed to get brand list")
|
||||
return
|
||||
}
|
||||
|
||||
if base64Resp {
|
||||
encoded, err := encode.EncodeJSON(list)
|
||||
if err != nil {
|
||||
h.log.Error("encode response failed", zap.Error(err))
|
||||
response.InternalError(c, "failed to encode response")
|
||||
return
|
||||
}
|
||||
c.String(http.StatusOK, encoded)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, list)
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type DeviceHandler struct {
|
||||
redis *redis.Client
|
||||
log *zap.Logger
|
||||
}
|
||||
|
||||
func NewDeviceHandler(redis *redis.Client, log *zap.Logger) *DeviceHandler {
|
||||
return &DeviceHandler{
|
||||
redis: redis,
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *DeviceHandler) ReportDevInfo(c *gin.Context) {
|
||||
mac := strings.TrimSpace(c.Query("mac"))
|
||||
model := strings.TrimSpace(c.Query("model"))
|
||||
ver := strings.TrimSpace(c.Query("ver"))
|
||||
xForwardedFor := c.GetHeader("X-Forwarded-For")
|
||||
|
||||
if mac == "" || model == "" {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 400,
|
||||
"msg": "参数校验失败",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if ver == "" {
|
||||
ver = ""
|
||||
}
|
||||
if xForwardedFor == "" {
|
||||
xForwardedFor = ""
|
||||
}
|
||||
|
||||
h.log.Info("report dev info",
|
||||
zap.String("remote_ip", xForwardedFor),
|
||||
zap.String("time", time.Now().Format("yyyy-MM-dd HH:mm:ss")),
|
||||
)
|
||||
|
||||
deviceInfo := map[string]string{
|
||||
"mac_addr": mac,
|
||||
"model": model,
|
||||
"active_date": time.Now().Format("yyyy-MM-dd"),
|
||||
"ip_addr": xForwardedFor,
|
||||
"ver": ver,
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(deviceInfo)
|
||||
if err != nil {
|
||||
h.log.Error("marshal device info failed", zap.Error(err))
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 500,
|
||||
"msg": "系统错误",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
if err := h.redis.HSet(ctx, "devices", mac, jsonData).Err(); err != nil {
|
||||
h.log.Error("redis hset failed", zap.Error(err))
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 500,
|
||||
"msg": "系统错误",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 200,
|
||||
"msg": "操作成功",
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/luxsin/app-api/internal/response"
|
||||
)
|
||||
|
||||
type HealthHandler struct{}
|
||||
|
||||
func NewHealthHandler() *HealthHandler {
|
||||
return &HealthHandler{}
|
||||
}
|
||||
|
||||
func (h *HealthHandler) Check(c *gin.Context) {
|
||||
response.OK(c, gin.H{
|
||||
"status": "up",
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/luxsin/app-api/internal/repository"
|
||||
"github.com/luxsin/app-api/internal/response"
|
||||
"github.com/luxsin/app-api/pkg/encode"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type ModelHandler struct {
|
||||
repo *repository.ModelRepository
|
||||
log *zap.Logger
|
||||
}
|
||||
|
||||
func NewModelHandler(db *sql.DB, log *zap.Logger) *ModelHandler {
|
||||
return &ModelHandler{
|
||||
repo: repository.NewModelRepository(db),
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *ModelHandler) GetModel(c *gin.Context) {
|
||||
brandName := c.Query("brandName")
|
||||
modelName := c.Query("modelName")
|
||||
base64Resp := encode.ParseBase64Param(c)
|
||||
|
||||
list, err := h.repo.List(c.Request.Context(), brandName, modelName)
|
||||
if err != nil {
|
||||
h.log.Error("get model list failed", zap.Error(err))
|
||||
response.InternalError(c, "failed to get model list")
|
||||
return
|
||||
}
|
||||
|
||||
if base64Resp {
|
||||
encoded, err := encode.EncodeJSON(list)
|
||||
if err != nil {
|
||||
h.log.Error("encode response failed", zap.Error(err))
|
||||
response.InternalError(c, "failed to encode response")
|
||||
return
|
||||
}
|
||||
c.String(http.StatusOK, encoded)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, list)
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/luxsin/app-api/internal/response"
|
||||
"github.com/luxsin/app-api/internal/search"
|
||||
"github.com/luxsin/app-api/pkg/encode"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type ModelListHandler struct {
|
||||
search *search.Client
|
||||
log *zap.Logger
|
||||
}
|
||||
|
||||
func NewModelListHandler(searchClient *search.Client, log *zap.Logger) *ModelListHandler {
|
||||
return &ModelListHandler{
|
||||
search: searchClient,
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *ModelListHandler) ModelList(c *gin.Context) {
|
||||
key := c.Query("key")
|
||||
base64Resp := encode.ParseBase64Param(c)
|
||||
|
||||
count := 100
|
||||
if v := c.Query("count"); v != "" {
|
||||
if n, err := strconv.Atoi(v); err == nil && n > 0 {
|
||||
count = n
|
||||
}
|
||||
}
|
||||
|
||||
list, err := h.search.ModelList(c.Request.Context(), key, count)
|
||||
if err != nil {
|
||||
h.log.Error("model list search failed", zap.Error(err))
|
||||
response.InternalError(c, "failed to search models")
|
||||
return
|
||||
}
|
||||
|
||||
if base64Resp {
|
||||
encoded, err := encode.EncodeJSON(list)
|
||||
if err != nil {
|
||||
h.log.Error("encode response failed", zap.Error(err))
|
||||
response.InternalError(c, "failed to encode response")
|
||||
return
|
||||
}
|
||||
c.String(http.StatusOK, encoded)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, list)
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func CORS() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
c.Header("Access-Control-Allow-Origin", "*")
|
||||
c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS")
|
||||
c.Header("Access-Control-Allow-Headers", "Origin, Content-Type, Accept, Authorization, X-Request-ID")
|
||||
c.Header("Access-Control-Expose-Headers", "X-Request-ID")
|
||||
|
||||
if c.Request.Method == "OPTIONS" {
|
||||
c.AbortWithStatus(204)
|
||||
return
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func Logger(log *zap.Logger) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
start := time.Now()
|
||||
path := c.Request.URL.Path
|
||||
query := c.Request.URL.RawQuery
|
||||
|
||||
c.Next()
|
||||
|
||||
latency := time.Since(start)
|
||||
status := c.Writer.Status()
|
||||
requestID, _ := c.Get(RequestIDKey)
|
||||
|
||||
fields := []zap.Field{
|
||||
zap.Int("status", status),
|
||||
zap.String("method", c.Request.Method),
|
||||
zap.String("path", path),
|
||||
zap.Duration("latency", latency),
|
||||
zap.String("ip", c.ClientIP()),
|
||||
zap.Any("request_id", requestID),
|
||||
}
|
||||
if query != "" {
|
||||
fields = append(fields, zap.String("query", query))
|
||||
}
|
||||
if len(c.Errors) > 0 {
|
||||
fields = append(fields, zap.String("errors", c.Errors.String()))
|
||||
}
|
||||
|
||||
if status >= 500 {
|
||||
log.Error("request", fields...)
|
||||
} else if status >= 400 {
|
||||
log.Warn("request", fields...)
|
||||
} else {
|
||||
log.Info("request", fields...)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
const RequestIDKey = "X-Request-ID"
|
||||
|
||||
func newRequestID() string {
|
||||
b := make([]byte, 16)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
return "unknown"
|
||||
}
|
||||
return hex.EncodeToString(b)
|
||||
}
|
||||
|
||||
func RequestID() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
id := c.GetHeader(RequestIDKey)
|
||||
if id == "" {
|
||||
id = newRequestID()
|
||||
}
|
||||
c.Set(RequestIDKey, id)
|
||||
c.Header(RequestIDKey, id)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package model
|
||||
|
||||
type Brand struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
type Model struct {
|
||||
ID int `json:"id"`
|
||||
BrandName string `json:"brandName"`
|
||||
Name string `json:"name"`
|
||||
Form *string `json:"form,omitempty"`
|
||||
Rig *string `json:"rig,omitempty"`
|
||||
Source *string `json:"source,omitempty"`
|
||||
EqKey *string `json:"eqKey,omitempty"`
|
||||
CreateAt time.Time `json:"createAt"`
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/luxsin/app-api/internal/model"
|
||||
)
|
||||
|
||||
type BrandRepository struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func NewBrandRepository(db *sql.DB) *BrandRepository {
|
||||
return &BrandRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *BrandRepository) List(ctx context.Context, brandName string) ([]model.Brand, error) {
|
||||
query := "SELECT id, name FROM brand"
|
||||
args := []any{}
|
||||
|
||||
if strings.TrimSpace(brandName) != "" {
|
||||
query += " WHERE name LIKE ?"
|
||||
args = append(args, "%"+brandName+"%")
|
||||
}
|
||||
|
||||
query += " ORDER BY name ASC"
|
||||
|
||||
rows, err := r.db.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("query brand: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
list := make([]model.Brand, 0)
|
||||
for rows.Next() {
|
||||
var b model.Brand
|
||||
if err := rows.Scan(&b.ID, &b.Name); err != nil {
|
||||
return nil, fmt.Errorf("scan brand: %w", err)
|
||||
}
|
||||
list = append(list, b)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("iterate brand: %w", err)
|
||||
}
|
||||
|
||||
return list, nil
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/luxsin/app-api/internal/model"
|
||||
)
|
||||
|
||||
type ModelRepository struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func NewModelRepository(db *sql.DB) *ModelRepository {
|
||||
return &ModelRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *ModelRepository) List(ctx context.Context, brandName, modelName string) ([]model.Model, error) {
|
||||
brandName = strings.TrimSpace(brandName)
|
||||
modelName = strings.TrimSpace(modelName)
|
||||
|
||||
const baseQuery = `SELECT id, brand_name, name, form, rig, source, eq_key, create_at FROM model`
|
||||
|
||||
var (
|
||||
query string
|
||||
args []any
|
||||
)
|
||||
|
||||
switch {
|
||||
case brandName != "":
|
||||
query = baseQuery + " WHERE brand_name = ? ORDER BY name ASC"
|
||||
args = []any{brandName}
|
||||
case modelName != "":
|
||||
query = baseQuery + " WHERE name LIKE ? ORDER BY name ASC"
|
||||
args = []any{"%" + modelName + "%"}
|
||||
default:
|
||||
return []model.Model{}, nil
|
||||
}
|
||||
|
||||
rows, err := r.db.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("query model: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
list := make([]model.Model, 0)
|
||||
for rows.Next() {
|
||||
m, err := scanModel(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
list = append(list, m)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("iterate model: %w", err)
|
||||
}
|
||||
|
||||
return list, nil
|
||||
}
|
||||
|
||||
func scanModel(rows *sql.Rows) (model.Model, error) {
|
||||
var m model.Model
|
||||
var form, rig, source, eqKey sql.NullString
|
||||
|
||||
if err := rows.Scan(
|
||||
&m.ID,
|
||||
&m.BrandName,
|
||||
&m.Name,
|
||||
&form,
|
||||
&rig,
|
||||
&source,
|
||||
&eqKey,
|
||||
&m.CreateAt,
|
||||
); err != nil {
|
||||
return model.Model{}, fmt.Errorf("scan model: %w", err)
|
||||
}
|
||||
|
||||
m.Form = nullStringPtr(form)
|
||||
m.Rig = nullStringPtr(rig)
|
||||
m.Source = nullStringPtr(source)
|
||||
m.EqKey = nullStringPtr(eqKey)
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func nullStringPtr(ns sql.NullString) *string {
|
||||
if !ns.Valid {
|
||||
return nil
|
||||
}
|
||||
s := ns.String
|
||||
return &s
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package response
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type Body struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data any `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
func OK(c *gin.Context, data any) {
|
||||
c.JSON(http.StatusOK, Body{
|
||||
Code: 0,
|
||||
Message: "ok",
|
||||
Data: data,
|
||||
})
|
||||
}
|
||||
|
||||
func Fail(c *gin.Context, httpStatus int, code int, message string) {
|
||||
c.JSON(httpStatus, Body{
|
||||
Code: code,
|
||||
Message: message,
|
||||
})
|
||||
}
|
||||
|
||||
func BadRequest(c *gin.Context, message string) {
|
||||
Fail(c, http.StatusBadRequest, 40000, message)
|
||||
}
|
||||
|
||||
func InternalError(c *gin.Context, message string) {
|
||||
Fail(c, http.StatusInternalServerError, 50000, message)
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/luxsin/app-api/internal/handler"
|
||||
"github.com/luxsin/app-api/internal/middleware"
|
||||
"github.com/luxsin/app-api/internal/search"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func New(log *zap.Logger, db *sql.DB, searchClient *search.Client, redis *redis.Client) *gin.Engine {
|
||||
r := gin.New()
|
||||
r.Use(gin.Recovery())
|
||||
r.Use(middleware.RequestID())
|
||||
r.Use(middleware.Logger(log))
|
||||
r.Use(middleware.CORS())
|
||||
|
||||
health := handler.NewHealthHandler()
|
||||
brand := handler.NewBrandHandler(db, log)
|
||||
model := handler.NewModelHandler(db, log)
|
||||
modelList := handler.NewModelListHandler(searchClient, log)
|
||||
device := handler.NewDeviceHandler(redis, log)
|
||||
|
||||
v1 := r.Group("/api/v1")
|
||||
{
|
||||
v1.GET("/health", health.Check)
|
||||
}
|
||||
|
||||
audio := r.Group("/audio")
|
||||
{
|
||||
audio.GET("/getBrand", brand.GetBrand)
|
||||
audio.GET("/getModel", model.GetModel)
|
||||
audio.GET("/modelList", modelList.ModelList)
|
||||
audio.GET("/reportDevInfo", device.ReportDevInfo)
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package search
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/luxsin/app-api/internal/config"
|
||||
"github.com/meilisearch/meilisearch-go"
|
||||
)
|
||||
|
||||
var modelListAttributes = []string{"rig", "form", "name", "brand_name", "source", "eq_key"}
|
||||
|
||||
type Client struct {
|
||||
index meilisearch.IndexManager
|
||||
}
|
||||
|
||||
func NewClient(cfg config.MeilisearchConfig) *Client {
|
||||
ms := meilisearch.New(cfg.Host, meilisearch.WithAPIKey(cfg.APIKey))
|
||||
return &Client{index: ms.Index(cfg.Index)}
|
||||
}
|
||||
|
||||
func (c *Client) ModelList(ctx context.Context, key string, count int) ([]map[string]any, error) {
|
||||
resp, err := c.index.SearchWithContext(ctx, key, &meilisearch.SearchRequest{
|
||||
Limit: int64(count),
|
||||
AttributesToRetrieve: modelListAttributes,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("meilisearch search: %w", err)
|
||||
}
|
||||
|
||||
if len(resp.Hits) == 0 {
|
||||
return []map[string]any{}, nil
|
||||
}
|
||||
|
||||
list := make([]map[string]any, 0, len(resp.Hits))
|
||||
for _, hit := range resp.Hits {
|
||||
item := make(map[string]any)
|
||||
if err := hit.DecodeInto(&item); err != nil {
|
||||
return nil, fmt.Errorf("decode meilisearch hit: %w", err)
|
||||
}
|
||||
list = append(list, item)
|
||||
}
|
||||
|
||||
return list, nil
|
||||
}
|
||||
Reference in New Issue
Block a user