修复 curve 接口的一些问题

This commit is contained in:
eafonyang
2026-06-04 19:39:08 +08:00
parent c4cd8b6f5d
commit 4f31cd563f
12 changed files with 202 additions and 82 deletions
+4
View File
@@ -5,4 +5,8 @@ GIN_MODE=release
# Production uses built-in RDS host when DATABASE_HOST is unset.
# Password must be set via environment variable (never commit real password).
# jdbc:mysql://database-1.chmuueamo72p.eu-central-1.rds.amazonaws.com:3306/audio?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true&serverTimezone=Asia/Shanghai
# young9#!UJsD219921031
# root
DATABASE_PASSWORD=your-production-password
+25
View File
@@ -0,0 +1,25 @@
FROM golang:1.24-alpine AS builder
RUN apk add --no-cache git
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /server ./cmd/server
FROM alpine:3.20
RUN apk add --no-cache ca-certificates tzdata \
&& cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& echo "Asia/Shanghai" > /etc/timezone
WORKDIR /app
COPY --from=builder /server .
EXPOSE 8080
CMD ["./server"]
+8 -4
View File
@@ -69,10 +69,14 @@ func main() {
// 启动时预热缓存
warmUpCache(log, db, rdb)
// 启动定时刷入任务:每 5 分钟从 Redis 刷入数据库
deviceRepo := repository.NewDeviceRepository(db)
devicePersistTask := task.NewDevicePersistTask(rdb, deviceRepo, log)
devicePersistTask.Start(5 * time.Minute)
// 启动定时刷入任务:每 5 分钟从 Redis 刷入数据库(仅 ENABLE_PERSIST_TASK=true 的容器运行)
if cfg.EnablePersistTask {
deviceRepo := repository.NewDeviceRepository(db)
devicePersistTask := task.NewDevicePersistTask(rdb, deviceRepo, log)
devicePersistTask.Start(5 * time.Minute)
} else {
log.Info("device persist task disabled")
}
engine := router.New(log, db, searchClient, rdb, cfg.Equalize)
+33
View File
@@ -0,0 +1,33 @@
services:
app:
build:
context: .
dockerfile: Dockerfile
container_name: app-api
restart: always
ports:
- "8084:8080"
environment:
- APP_ENV=production
- APP_HOST=0.0.0.0
- APP_PORT=8080
- DATABASE_PASSWORD=${DATABASE_PASSWORD}
- REDIS_PASSWORD=${REDIS_PASSWORD:-eafon123!}
- MEILISEARCH_API_KEY=${MEILISEARCH_API_KEY:-young9#!UJsD219921031}
- ENABLE_PERSIST_TASK=false
- MEASUREMENT_BASE_PATH=/app/measurements
- TARGET_BASE_PATH=/app/targets
volumes:
- /data/project/autoeq/measurements:/app/measurements:ro
- /data/project/autoeq/targets:/app/targets:ro
healthcheck:
test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://localhost:8080/api/v1/health"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
+16 -14
View File
@@ -7,13 +7,14 @@ import (
)
type Config struct {
Env string
Host string
Port int
Database DatabaseConfig
Meilisearch MeilisearchConfig
Redis RedisConfig
Equalize EqualizeConfig
Env string
Host string
Port int
Database DatabaseConfig
Meilisearch MeilisearchConfig
Redis RedisConfig
Equalize EqualizeConfig
EnablePersistTask bool
}
func Load() (*Config, error) {
@@ -45,13 +46,14 @@ func Load() (*Config, error) {
eq := loadEqualize(env)
return &Config{
Env: env,
Host: getEnv("APP_HOST", "0.0.0.0"),
Port: port,
Database: db,
Meilisearch: ms,
Redis: rd,
Equalize: eq,
Env: env,
Host: getEnv("APP_HOST", "0.0.0.0"),
Port: port,
Database: db,
Meilisearch: ms,
Redis: rd,
Equalize: eq,
EnablePersistTask: getEnv("ENABLE_PERSIST_TASK", "false") == "true",
}, nil
}
+10 -10
View File
@@ -3,32 +3,32 @@ package config
import "os"
type EqualizeConfig struct {
APIURL string
APIURL string
MeasurementBasePath string
TargetBasePath string
TargetBasePath string
}
func loadEqualize(env string) EqualizeConfig {
if os.Getenv("EQ_API_URL") != "" {
return EqualizeConfig{
APIURL: os.Getenv("EQ_API_URL"),
MeasurementBasePath: getEnv("MEASUREMENT_BASE_PATH", ""),
TargetBasePath: getEnv("TARGET_BASE_PATH", ""),
MeasurementBasePath: getEnv("MEASUREMENT_BASE_PATH", "/app/measurements"),
TargetBasePath: getEnv("TARGET_BASE_PATH", "/app/targets"),
}
}
switch env {
case "production":
return EqualizeConfig{
APIURL: "https://autoeq.app/equalize",
MeasurementBasePath: getEnv("MEASUREMENT_BASE_PATH", ""),
TargetBasePath: getEnv("TARGET_BASE_PATH", ""),
APIURL: "http://172.31.18.70:8000/equalize",
MeasurementBasePath: getEnv("MEASUREMENT_BASE_PATH", "/app/measurements"),
TargetBasePath: getEnv("TARGET_BASE_PATH", "/app/targets"),
}
default:
return EqualizeConfig{
APIURL: "https://autoeq.app/equalize",
MeasurementBasePath: getEnv("MEASUREMENT_BASE_PATH", ""),
TargetBasePath: getEnv("TARGET_BASE_PATH", ""),
MeasurementBasePath: getEnv("MEASUREMENT_BASE_PATH", "/app/measurements"),
TargetBasePath: getEnv("TARGET_BASE_PATH", "/app/targets"),
}
}
}
}
+5 -4
View File
@@ -6,8 +6,8 @@ import (
"fmt"
"time"
_ "github.com/go-sql-driver/mysql"
"github.com/go-sql-driver/mysql"
_ "github.com/go-sql-driver/mysql"
"github.com/luxsin/app-api/internal/config"
)
@@ -19,9 +19,10 @@ func Open(cfg config.DatabaseConfig) (*sql.DB, error) {
Addr: fmt.Sprintf("%s:%d", cfg.Host, cfg.Port),
DBName: cfg.Name,
Params: map[string]string{
"charset": "utf8mb4",
"parseTime": "True",
"loc": "Local",
"charset": "utf8mb4",
"parseTime": "True",
"loc": "Local",
"allowNativePasswords": "true",
},
}
+88 -44
View File
@@ -8,6 +8,7 @@ import (
"fmt"
"io"
"net/http"
"net/url"
"os"
"strconv"
"strings"
@@ -18,6 +19,7 @@ import (
"github.com/luxsin/app-api/internal/config"
"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"
)
@@ -33,12 +35,12 @@ func NewCurveHandler(repo *repository.CurveRepository, curveCache *cache.CurveCa
}
// GetCurve 获取目标曲线
// GET /audio/getCurve?brand=xxx&name=xxx&target=xxx&includeRaw=false
// GET /audio/getCurve?brand=xxx&name=xxx&target=xxx&base64Resp=true
func (h *CurveHandler) GetCurve(c *gin.Context) {
brand := strings.TrimSpace(c.Query("brand"))
name := strings.TrimSpace(c.Query("name"))
target := strings.TrimSpace(c.Query("target"))
includeRaw := c.Query("includeRaw") == "true" || c.Query("includeRaw") == "1"
brand := strings.TrimSpace(queryParam(c, "brand"))
name := strings.TrimSpace(queryParam(c, "name"))
target := strings.TrimSpace(queryParam(c, "target"))
base64Resp := encode.ParseBase64Param(c)
if brand == "" || name == "" || target == "" {
response.BadRequest(c, "brand、name和target参数不能为空")
@@ -59,22 +61,33 @@ func (h *CurveHandler) GetCurve(c *gin.Context) {
return
}
// 解析 JSON 结果
// 解析 JSON 结果,只提取 parametric_eq
var resp map[string]any
if err := json.Unmarshal([]byte(result), &resp); err != nil {
response.InternalError(c, "解析曲线数据失败")
return
}
// 如果 includeRaw 为 false 且 code != 200,移除 fr 字段
if !includeRaw {
codeVal, _ := resp["code"]
if code, ok := codeVal.(float64); ok && code != 200 {
delete(resp, "fr")
}
parametricEq, _ := resp["parametric_eq"]
resultData := gin.H{
"code": 200,
"msg": "操作成功",
"parametric_eq": parametricEq,
}
response.OK(c, resp)
if base64Resp {
encoded, err := encode.EncodeJSON(resultData)
if err != nil {
h.log.Error("encode response failed", zap.Error(err))
response.InternalError(c, "编码响应失败")
return
}
c.String(http.StatusOK, encoded)
return
}
c.JSON(http.StatusOK, resultData)
}
// getCurvePoint 获取曲线数据:先查缓存,缓存不存在则请求 EQ 接口并缓存结果
@@ -92,6 +105,7 @@ func (h *CurveHandler) getCurvePoint(ctx context.Context, brand, name, target st
// 获取了锁,缓存仍然为空,需要请求 EQ 接口
if acquired {
h.log.Info("curve cache miss, requesting eq api", zap.String("brand", brand), zap.String("name", name), zap.String("target", target))
result, eqErr := h.getCurvePointFromPEQ(ctx, brand, name, target)
if eqErr != nil {
// 释放锁
@@ -193,35 +207,35 @@ func (h *CurveHandler) reqEqualize(headPhone string, measurement map[string]any,
q := floatVal(bassBoost, "q", 0.7)
reqBody := map[string]any{
"target": target,
"sound_signature": nil,
"target": target,
"sound_signature": nil,
"sound_signature_smoothing_window_size": 1,
"bass_boost_gain": gain,
"bass_boost_fc": fc,
"bass_boost_q": q,
"treble_boost_gain": 0,
"treble_boost_fc": 10000,
"treble_boost_q": 0.7,
"tilt": 0,
"fs": 48000,
"bit_depth": 16,
"phase": "minimum",
"f_res": 16,
"preamp": 0,
"max_gain": 12,
"max_slope": 18,
"window_size": 0.08,
"treble_window_size": 2,
"treble_f_lower": 6000,
"treble_f_upper": 8000,
"treble_gain_k": 1,
"graphic_eq": false,
"parametric_eq": true,
"fixed_band_eq": false,
"convolution_eq": false,
"source": source,
"rig": rig,
"parametric_eq_config": "MINIDSP_IL_DSP",
"bass_boost_gain": gain,
"bass_boost_fc": fc,
"bass_boost_q": q,
"treble_boost_gain": 0,
"treble_boost_fc": 10000,
"treble_boost_q": 0.7,
"tilt": 0,
"fs": 48000,
"bit_depth": 16,
"phase": "minimum",
"f_res": 16,
"preamp": 0,
"max_gain": 12,
"max_slope": 18,
"window_size": 0.08,
"treble_window_size": 2,
"treble_f_lower": 6000,
"treble_f_upper": 8000,
"treble_gain_k": 1,
"graphic_eq": false,
"parametric_eq": true,
"fixed_band_eq": false,
"convolution_eq": false,
"source": source,
"rig": rig,
"parametric_eq_config": "MINIDSP_IL_DSP",
"response": map[string]any{
"fr_f_step": 1.02,
"base64fp16": false,
@@ -247,9 +261,17 @@ func (h *CurveHandler) reqEqualize(headPhone string, measurement map[string]any,
}
httpReq.Header.Set("Content-Type", "application/json")
start := time.Now()
client := &http.Client{Timeout: 10 * time.Second}
httpResp, err := client.Do(httpReq)
elapsed := time.Since(start)
h.log.Info("eq api response",
zap.String("headPhone", headPhone),
zap.Duration("latency", elapsed),
zap.String("url", h.cfg.APIURL),
)
if err != nil {
h.log.Error("eq api request failed", zap.Duration("latency", elapsed), zap.Error(err))
return "", fmt.Errorf("call eq api: %w", err)
}
defer httpResp.Body.Close()
@@ -272,8 +294,8 @@ func (h *CurveHandler) reqEqualize(headPhone string, measurement map[string]any,
parametricEq, _ := eqResp["parametric_eq"].(map[string]any)
if parametricEq == nil {
result := map[string]any{
"code": 200,
"msg": "req param error",
"code": 200,
"msg": "req param error",
"param": reqBody,
}
resultJSON, _ := json.Marshal(result)
@@ -389,4 +411,26 @@ func intVal(m map[string]any, key string, defaultVal int) int {
default:
return defaultVal
}
}
}
// queryParam 从 URL 原始 query 中获取参数,保留 + 为字面量而非空格
func queryParam(c *gin.Context, key string) string {
vals, ok := c.Request.URL.Query()[key]
if !ok || len(vals) == 0 {
return ""
}
// c.Query() 会把 + 解码为空格,这里从原始 query 手动解码,+ 保留为 +
if strings.Contains(vals[0], " ") {
rawQuery := c.Request.URL.RawQuery
for _, pair := range strings.Split(rawQuery, "&") {
kv := strings.SplitN(pair, "=", 2)
if len(kv) == 2 && kv[0] == key {
decoded, err := url.PathUnescape(strings.ReplaceAll(kv[1], "+", "%2B"))
if err == nil {
return decoded
}
}
}
}
return vals[0]
}
+6
View File
@@ -9,6 +9,12 @@ import (
func Logger(log *zap.Logger) gin.HandlerFunc {
return func(c *gin.Context) {
// 跳过 health 检查的日志
if c.Request.URL.Path == "/api/v1/health" {
c.Next()
return
}
start := time.Now()
path := c.Request.URL.Path
query := c.Request.URL.RawQuery
+2 -2
View File
@@ -9,5 +9,5 @@ type Target struct {
ReadCSV BoolInt `json:"readCsv"`
File *string `json:"file,omitempty"`
BassBoost *string `json:"bassBoost,omitempty"`
CreateAt time.Time `json:"createAt"`
}
AddTime time.Time `json:"addTime"`
}
+4 -4
View File
@@ -18,7 +18,7 @@ func NewCurveRepository(db *sql.DB) *CurveRepository {
// GetModelByBrandAndName 按 brand_name + name 查询 Model(唯一索引)
func (r *CurveRepository) GetModelByBrandAndName(ctx context.Context, brandName, name string) (*model.Model, error) {
const query = `SELECT id, brand_name, name, form, rig, source, eq_key, create_at FROM model WHERE brand_name = ? AND name = ?`
const query = `SELECT id, brand_name, name, form, rig, source, eq_key, create_at FROM model WHERE BINARY brand_name = ? AND BINARY name = ?`
var m model.Model
var form, rig, source, eqKey sql.NullString
@@ -43,13 +43,13 @@ func (r *CurveRepository) GetModelByBrandAndName(ctx context.Context, brandName,
// GetTargetByLabel 按 label 查询 Target
func (r *CurveRepository) GetTargetByLabel(ctx context.Context, label string) (*model.Target, error) {
const query = `SELECT id, label, read_csv, file, bass_boost, create_at FROM target WHERE label = ?`
const query = `SELECT id, label, read_csv, file, bassBoost, addtime FROM target WHERE BINARY label = ?`
var t model.Target
var file, bassBoost sql.NullString
err := r.db.QueryRowContext(ctx, query, label).Scan(
&t.ID, &t.Label, &t.ReadCSV, &file, &bassBoost, &t.CreateAt,
&t.ID, &t.Label, &t.ReadCSV, &file, &bassBoost, &t.AddTime,
)
if err == sql.ErrNoRows {
return nil, nil
@@ -62,4 +62,4 @@ func (r *CurveRepository) GetTargetByLabel(ctx context.Context, label string) (*
t.BassBoost = nullStringPtr(bassBoost)
return &t, nil
}
}
Executable
+1
View File
@@ -0,0 +1 @@
scp -i ~/.ssh/aws_eafon.pem -r ./cmd/ ./internal/ ./pkg ./go.mod ./go.sum ./Dockerfile ./docker-compose.yml ubuntu@ec2-18-184-205-87.eu-central-1.compute.amazonaws.com:/data/project/app-api/