耳机阻抗上报
This commit is contained in:
@@ -16,8 +16,9 @@ type Config struct {
|
||||
Redis RedisConfig
|
||||
Equalize EqualizeConfig
|
||||
S3 S3Config
|
||||
EnablePersistTask bool
|
||||
ShareCodeMaxPerMac int
|
||||
EnablePersistTask bool
|
||||
EnableImpedancePersistTask bool
|
||||
ShareCodeMaxPerMac int
|
||||
ShareCodeTTL time.Duration
|
||||
}
|
||||
|
||||
@@ -64,8 +65,9 @@ func Load() (*Config, error) {
|
||||
Redis: rd,
|
||||
Equalize: eq,
|
||||
S3: s3cfg,
|
||||
EnablePersistTask: getEnv("ENABLE_PERSIST_TASK", "false") == "true",
|
||||
ShareCodeMaxPerMac: getEnvInt("SHARE_CODE_MAX_PER_MAC", 1),
|
||||
EnablePersistTask: getEnv("ENABLE_PERSIST_TASK", "false") == "true",
|
||||
EnableImpedancePersistTask: getEnv("ENABLE_IMPEDANCE_PERSIST_TASK", "false") == "true",
|
||||
ShareCodeMaxPerMac: getEnvInt("SHARE_CODE_MAX_PER_MAC", 1),
|
||||
ShareCodeTTL: shareCodeTTL,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/luxsin/app-api/pkg/encode"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
const redisHeadphoneImpedancesKey = "headphone_impedances"
|
||||
|
||||
type ImpedanceHandler struct {
|
||||
redis *redis.Client
|
||||
log *zap.Logger
|
||||
}
|
||||
|
||||
func NewImpedanceHandler(redis *redis.Client, log *zap.Logger) *ImpedanceHandler {
|
||||
return &ImpedanceHandler{
|
||||
redis: redis,
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
|
||||
// ReportImpedance 上报耳机阻抗
|
||||
//
|
||||
// @Summary 上报耳机阻抗
|
||||
// @Tags Device
|
||||
// @Produce json
|
||||
// @Param mac query string true "设备 MAC 地址"
|
||||
// @Param name query string true "设备型号"
|
||||
// @Param brand query string true "耳机品牌"
|
||||
// @Param model query string true "耳机型号"
|
||||
// @Param value query int true "阻抗值(整数)"
|
||||
// @Success 200 {object} map[string]any "操作成功"
|
||||
// @Failure 500 {object} object
|
||||
// @Router /audio/reportImpedance [get]
|
||||
func (h *ImpedanceHandler) ReportImpedance(c *gin.Context) {
|
||||
mac := strings.TrimSpace(c.Query("mac"))
|
||||
deviceModel := strings.TrimSpace(c.Query("name"))
|
||||
brand := strings.TrimSpace(c.Query("brand"))
|
||||
headphoneModel := strings.TrimSpace(c.Query("model"))
|
||||
valueStr := strings.TrimSpace(c.Query("value"))
|
||||
clientIP := encode.ClientPublicIP(c)
|
||||
|
||||
if mac == "" || deviceModel == "" || brand == "" || headphoneModel == "" || valueStr == "" {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 400,
|
||||
"msg": "参数校验失败",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
impedance, err := strconv.Atoi(valueStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 400,
|
||||
"msg": "参数校验失败",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
brandNorm := normalizeHeadphoneText(brand)
|
||||
modelNorm := normalizeHeadphoneText(headphoneModel)
|
||||
|
||||
impedanceInfo := map[string]any{
|
||||
"mac_addr": mac,
|
||||
"device_model": deviceModel,
|
||||
"impedance_ohm": impedance,
|
||||
"headphone_brand": brand,
|
||||
"headphone_model": headphoneModel,
|
||||
"headphone_brand_norm": brandNorm,
|
||||
"headphone_model_norm": modelNorm,
|
||||
"ip_addr": clientIP,
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(impedanceInfo)
|
||||
if err != nil {
|
||||
h.log.Error("marshal impedance info failed", zap.Error(err))
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 500,
|
||||
"msg": "系统错误",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
field := impedanceRedisField(mac, brandNorm, modelNorm)
|
||||
ctx := c.Request.Context()
|
||||
if err := h.redis.HSet(ctx, redisHeadphoneImpedancesKey, field, 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": "操作成功",
|
||||
})
|
||||
}
|
||||
|
||||
func normalizeHeadphoneText(s string) string {
|
||||
return strings.ToLower(strings.TrimSpace(s))
|
||||
}
|
||||
|
||||
func impedanceRedisField(mac, brandNorm, modelNorm string) string {
|
||||
return fmt.Sprintf("%s|%s|%s", mac, brandNorm, modelNorm)
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
type UserHeadphoneImpedance struct {
|
||||
ID int
|
||||
MacAddr string
|
||||
DeviceModel string
|
||||
ImpedanceOhm int
|
||||
HeadphoneBrand string
|
||||
HeadphoneModel string
|
||||
HeadphoneBrandNorm string
|
||||
HeadphoneModelNorm string
|
||||
IpAddr string
|
||||
CreateAt time.Time
|
||||
UpdateAt time.Time
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
"github.com/luxsin/app-api/internal/model"
|
||||
)
|
||||
|
||||
type HeadphoneImpedanceRepository struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func NewHeadphoneImpedanceRepository(db *sql.DB) *HeadphoneImpedanceRepository {
|
||||
return &HeadphoneImpedanceRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *HeadphoneImpedanceRepository) FindByMacAndNorm(
|
||||
ctx context.Context,
|
||||
macAddr, brandNorm, modelNorm string,
|
||||
) (*model.UserHeadphoneImpedance, error) {
|
||||
const query = `
|
||||
SELECT id, mac_addr, device_model, impedance_ohm,
|
||||
headphone_brand, headphone_model,
|
||||
headphone_brand_norm, headphone_model_norm,
|
||||
ip_addr, create_at, update_at
|
||||
FROM user_headphone_impedance
|
||||
WHERE mac_addr = ? AND headphone_brand_norm = ? AND headphone_model_norm = ?`
|
||||
|
||||
var rec model.UserHeadphoneImpedance
|
||||
err := r.db.QueryRowContext(ctx, query, macAddr, brandNorm, modelNorm).Scan(
|
||||
&rec.ID,
|
||||
&rec.MacAddr,
|
||||
&rec.DeviceModel,
|
||||
&rec.ImpedanceOhm,
|
||||
&rec.HeadphoneBrand,
|
||||
&rec.HeadphoneModel,
|
||||
&rec.HeadphoneBrandNorm,
|
||||
&rec.HeadphoneModelNorm,
|
||||
&rec.IpAddr,
|
||||
&rec.CreateAt,
|
||||
&rec.UpdateAt,
|
||||
)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("query user_headphone_impedance: %w", err)
|
||||
}
|
||||
return &rec, nil
|
||||
}
|
||||
|
||||
func (r *HeadphoneImpedanceRepository) Insert(ctx context.Context, rec model.UserHeadphoneImpedance) error {
|
||||
const query = `
|
||||
INSERT INTO user_headphone_impedance (
|
||||
mac_addr, device_model, impedance_ohm,
|
||||
headphone_brand, headphone_model,
|
||||
headphone_brand_norm, headphone_model_norm,
|
||||
ip_addr
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
|
||||
|
||||
_, err := r.db.ExecContext(ctx, query,
|
||||
rec.MacAddr,
|
||||
rec.DeviceModel,
|
||||
rec.ImpedanceOhm,
|
||||
rec.HeadphoneBrand,
|
||||
rec.HeadphoneModel,
|
||||
rec.HeadphoneBrandNorm,
|
||||
rec.HeadphoneModelNorm,
|
||||
rec.IpAddr,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("insert user_headphone_impedance: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *HeadphoneImpedanceRepository) Update(ctx context.Context, rec model.UserHeadphoneImpedance) error {
|
||||
const query = `
|
||||
UPDATE user_headphone_impedance
|
||||
SET device_model = ?, impedance_ohm = ?,
|
||||
headphone_brand = ?, headphone_model = ?,
|
||||
ip_addr = ?
|
||||
WHERE id = ?`
|
||||
|
||||
_, err := r.db.ExecContext(ctx, query,
|
||||
rec.DeviceModel,
|
||||
rec.ImpedanceOhm,
|
||||
rec.HeadphoneBrand,
|
||||
rec.HeadphoneModel,
|
||||
rec.IpAddr,
|
||||
rec.ID,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("update user_headphone_impedance: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -45,6 +45,7 @@ func New(log *zap.Logger, db *sql.DB, searchClient *search.Client, rdb *redis.Cl
|
||||
model := handler.NewModelHandler(modelRepo, log)
|
||||
modelList := handler.NewModelListHandler(searchClient, log)
|
||||
device := handler.NewDeviceHandler(rdb, log)
|
||||
impedance := handler.NewImpedanceHandler(rdb, log)
|
||||
ota := handler.NewOTAHandler(otaRepo, log)
|
||||
curve := handler.NewCurveHandler(curveRepo, curveCache, eqCfg, s3, log)
|
||||
modelCSV := handler.NewModelCSVHandler(s3, log)
|
||||
@@ -66,6 +67,7 @@ func New(log *zap.Logger, db *sql.DB, searchClient *search.Client, rdb *redis.Cl
|
||||
audio.GET("/getModel", middleware.CacheControl(middleware.CacheControlList), model.GetModel)
|
||||
audio.GET("/modelList", middleware.CacheControl(middleware.CacheControlModelList), modelList.ModelList)
|
||||
audio.GET("/reportDevInfo", device.ReportDevInfo)
|
||||
audio.GET("/reportImpedance", impedance.ReportImpedance)
|
||||
audio.GET("/ota", ota.GetOTA)
|
||||
audio.GET("/getCurve", middleware.CacheControl(middleware.CacheControlCurve), curve.GetCurve)
|
||||
audio.GET("/modelCurve", middleware.CacheControl(middleware.CacheControlCurve), curve.ModelCurve)
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/luxsin/app-api/internal/model"
|
||||
"github.com/luxsin/app-api/internal/repository"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
const redisHeadphoneImpedancesKey = "headphone_impedances"
|
||||
|
||||
type ImpedancePersistTask struct {
|
||||
rdb *redis.Client
|
||||
repo *repository.HeadphoneImpedanceRepository
|
||||
log *zap.Logger
|
||||
}
|
||||
|
||||
func NewImpedancePersistTask(
|
||||
rdb *redis.Client,
|
||||
repo *repository.HeadphoneImpedanceRepository,
|
||||
log *zap.Logger,
|
||||
) *ImpedancePersistTask {
|
||||
return &ImpedancePersistTask{rdb: rdb, repo: repo, log: log}
|
||||
}
|
||||
|
||||
// Start 启动定时刷入协程,每 interval 执行一次
|
||||
func (t *ImpedancePersistTask) Start(interval time.Duration) {
|
||||
go func() {
|
||||
ticker := time.NewTicker(interval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for range ticker.C {
|
||||
t.Persist()
|
||||
}
|
||||
}()
|
||||
t.log.Info("impedance persist task started", zap.String("interval", interval.String()))
|
||||
}
|
||||
|
||||
// Persist 从 Redis Hash headphone_impedances 中读取数据刷入数据库
|
||||
func (t *ImpedancePersistTask) Persist() {
|
||||
ctx := context.Background()
|
||||
|
||||
records, err := t.rdb.HGetAll(ctx, redisHeadphoneImpedancesKey).Result()
|
||||
if err != nil {
|
||||
t.log.Error("redis HGetAll headphone_impedances failed", zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
if len(records) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
t.log.Info("persisting headphone impedances from redis", zap.Int("count", len(records)))
|
||||
|
||||
var succeeded []string
|
||||
|
||||
for field, jsonStr := range records {
|
||||
var info struct {
|
||||
MacAddr string `json:"mac_addr"`
|
||||
DeviceModel string `json:"device_model"`
|
||||
ImpedanceOhm int `json:"impedance_ohm"`
|
||||
HeadphoneBrand string `json:"headphone_brand"`
|
||||
HeadphoneModel string `json:"headphone_model"`
|
||||
HeadphoneBrandNorm string `json:"headphone_brand_norm"`
|
||||
HeadphoneModelNorm string `json:"headphone_model_norm"`
|
||||
IpAddr string `json:"ip_addr"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal([]byte(jsonStr), &info); err != nil {
|
||||
t.log.Error("unmarshal impedance info failed", zap.String("field", field), zap.Error(err))
|
||||
continue
|
||||
}
|
||||
|
||||
if t.persistOne(ctx, info) {
|
||||
succeeded = append(succeeded, field)
|
||||
}
|
||||
}
|
||||
|
||||
total := len(records)
|
||||
persisted := len(succeeded)
|
||||
failed := total - persisted
|
||||
|
||||
if persisted > 0 {
|
||||
if err := t.rdb.HDel(ctx, redisHeadphoneImpedancesKey, succeeded...).Err(); err != nil {
|
||||
t.log.Error("redis HDel failed after persist",
|
||||
zap.Int("persisted", persisted),
|
||||
zap.Error(err),
|
||||
)
|
||||
} else {
|
||||
t.log.Info("headphone impedances persisted to database and removed from redis",
|
||||
zap.Int("persisted", persisted),
|
||||
zap.Int("total", total),
|
||||
zap.Int("failed", failed),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if failed > 0 {
|
||||
t.log.Warn("some headphone impedances failed to persist, kept in redis for retry",
|
||||
zap.Int("failed", failed),
|
||||
zap.Int("total", total),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *ImpedancePersistTask) persistOne(ctx context.Context, info struct {
|
||||
MacAddr string `json:"mac_addr"`
|
||||
DeviceModel string `json:"device_model"`
|
||||
ImpedanceOhm int `json:"impedance_ohm"`
|
||||
HeadphoneBrand string `json:"headphone_brand"`
|
||||
HeadphoneModel string `json:"headphone_model"`
|
||||
HeadphoneBrandNorm string `json:"headphone_brand_norm"`
|
||||
HeadphoneModelNorm string `json:"headphone_model_norm"`
|
||||
IpAddr string `json:"ip_addr"`
|
||||
}) bool {
|
||||
existing, err := t.repo.FindByMacAndNorm(ctx, info.MacAddr, info.HeadphoneBrandNorm, info.HeadphoneModelNorm)
|
||||
if err != nil {
|
||||
t.log.Error("find headphone impedance failed",
|
||||
zap.String("mac", info.MacAddr),
|
||||
zap.String("brand_norm", info.HeadphoneBrandNorm),
|
||||
zap.String("model_norm", info.HeadphoneModelNorm),
|
||||
zap.Error(err),
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
rec := model.UserHeadphoneImpedance{
|
||||
MacAddr: info.MacAddr,
|
||||
DeviceModel: info.DeviceModel,
|
||||
ImpedanceOhm: info.ImpedanceOhm,
|
||||
HeadphoneBrand: info.HeadphoneBrand,
|
||||
HeadphoneModel: info.HeadphoneModel,
|
||||
HeadphoneBrandNorm: info.HeadphoneBrandNorm,
|
||||
HeadphoneModelNorm: info.HeadphoneModelNorm,
|
||||
IpAddr: info.IpAddr,
|
||||
}
|
||||
|
||||
if existing == nil {
|
||||
if err := t.repo.Insert(ctx, rec); err != nil {
|
||||
t.log.Error("insert user_headphone_impedance failed",
|
||||
zap.String("mac", info.MacAddr),
|
||||
zap.Error(err),
|
||||
)
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
rec.ID = existing.ID
|
||||
if err := t.repo.Update(ctx, rec); err != nil {
|
||||
t.log.Error("update user_headphone_impedance failed",
|
||||
zap.String("mac", info.MacAddr),
|
||||
zap.Error(err),
|
||||
)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
Reference in New Issue
Block a user