耳机阻抗上报
This commit is contained in:
@@ -97,6 +97,14 @@ func main() {
|
|||||||
log.Info("device persist task disabled")
|
log.Info("device persist task disabled")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if cfg.EnableImpedancePersistTask {
|
||||||
|
impedanceRepo := repository.NewHeadphoneImpedanceRepository(db)
|
||||||
|
impedancePersistTask := task.NewImpedancePersistTask(rdb, impedanceRepo, log)
|
||||||
|
impedancePersistTask.Start(5 * time.Minute)
|
||||||
|
} else {
|
||||||
|
log.Info("impedance persist task disabled")
|
||||||
|
}
|
||||||
|
|
||||||
s3Storage, err := storage.NewS3Storage(context.Background(), cfg.S3)
|
s3Storage, err := storage.NewS3Storage(context.Background(), cfg.S3)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal("s3 client init failed", zap.Error(err))
|
log.Fatal("s3 client init failed", zap.Error(err))
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ services:
|
|||||||
- REDIS_PASSWORD=${REDIS_PASSWORD:-eafon123!}
|
- REDIS_PASSWORD=${REDIS_PASSWORD:-eafon123!}
|
||||||
- MEILISEARCH_API_KEY=${MEILISEARCH_API_KEY:-young9#!UJsD219921031}
|
- MEILISEARCH_API_KEY=${MEILISEARCH_API_KEY:-young9#!UJsD219921031}
|
||||||
- ENABLE_PERSIST_TASK=false
|
- ENABLE_PERSIST_TASK=false
|
||||||
|
- ENABLE_IMPEDANCE_PERSIST_TASK=false
|
||||||
- AWS_REGION=eu-central-1
|
- AWS_REGION=eu-central-1
|
||||||
- S3_BUCKET=luxsin-app-bucket
|
- S3_BUCKET=luxsin-app-bucket
|
||||||
- SHARE_CODE_MAX_PER_MAC=${SHARE_CODE_MAX_PER_MAC:-1}
|
- SHARE_CODE_MAX_PER_MAC=${SHARE_CODE_MAX_PER_MAC:-1}
|
||||||
|
|||||||
@@ -464,6 +464,69 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/audio/reportImpedance": {
|
||||||
|
"get": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Device"
|
||||||
|
],
|
||||||
|
"summary": "上报耳机阻抗",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "设备 MAC 地址",
|
||||||
|
"name": "mac",
|
||||||
|
"in": "query",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "设备型号",
|
||||||
|
"name": "name",
|
||||||
|
"in": "query",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "耳机品牌",
|
||||||
|
"name": "brand",
|
||||||
|
"in": "query",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "耳机型号",
|
||||||
|
"name": "model",
|
||||||
|
"in": "query",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "阻抗值(整数)",
|
||||||
|
"name": "value",
|
||||||
|
"in": "query",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "操作成功",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/audio/shareAccept": {
|
"/audio/shareAccept": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "根据分享码获取他人分享的 EQ 数据",
|
"description": "根据分享码获取他人分享的 EQ 数据",
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
# 用户上报耳机阻抗(最新值)需求文档
|
||||||
|
|
||||||
|
## 背景与目标
|
||||||
|
|
||||||
|
- 用户在设备上报其耳机阻抗值,用于后续业务使用(本需求仅覆盖**存储**与**去重更新**规则)。
|
||||||
|
- 一个 **MAC 地址代表一个用户**。
|
||||||
|
- 一个用户可以上报/维护多个耳机(以“耳机品牌 + 耳机型号”区分)。
|
||||||
|
|
||||||
|
## 核心字段
|
||||||
|
|
||||||
|
- 设备:`Luxsin-X8` / `Luxsin-X9`
|
||||||
|
- 阻抗值:整数,单位 Ω
|
||||||
|
- 耳机品牌:字符串
|
||||||
|
- 耳机型号:字符串
|
||||||
|
- 设备 MAC 地址:字符串(用户标识)
|
||||||
|
- 设备 IP 地址:服务端获取(与 `reportDevInfo` 接口一致),落库保存**最新值**
|
||||||
|
|
||||||
|
## 去重与更新规则(关键)
|
||||||
|
|
||||||
|
- 唯一判定条件:`mac_addr + headphone_brand + headphone_model`
|
||||||
|
- 品牌/型号的对比规则:对比前做 `trim`(去首尾空格)+ `lower`(忽略大小写)
|
||||||
|
- 若命中同一条(按上述规则),则:
|
||||||
|
- 更新最新 `impedance_ohm`
|
||||||
|
- 覆盖更新 `device_model`
|
||||||
|
- 覆盖更新 `ip_addr`
|
||||||
|
- 更新 `update_at`
|
||||||
|
- 不保留历史:同一条记录始终反映该用户该耳机的**最新阻抗值**
|
||||||
|
|
||||||
|
## 数据库设计
|
||||||
|
|
||||||
|
### 表
|
||||||
|
|
||||||
|
- 表名:`user_headphone_impedance`
|
||||||
|
- 建表脚本:`sql/user_headphone_impedance.sql`
|
||||||
|
|
||||||
|
### 字段说明
|
||||||
|
|
||||||
|
- `mac_addr`:用户标识(设备 MAC)
|
||||||
|
- `device_model`:设备型号(`Luxsin-X8/Luxsin-X9`)
|
||||||
|
- `impedance_ohm`:阻抗整数(Ω)
|
||||||
|
- `headphone_brand` / `headphone_model`:原始输入(用于展示/回显)
|
||||||
|
- `headphone_brand_norm` / `headphone_model_norm`:归一化值(trim+lower,用于唯一索引与去重)
|
||||||
|
- `ip_addr`:服务端获取的设备 IP(最新值)
|
||||||
|
- `create_at` / `update_at`:创建/更新时间
|
||||||
|
|
||||||
|
### 索引
|
||||||
|
|
||||||
|
- 唯一索引:`uniq_mac_brand_model(mac_addr, headphone_brand_norm, headphone_model_norm)`
|
||||||
|
- 辅助索引:`idx_mac(mac_addr)`、`idx_device_model(device_model)`
|
||||||
|
|
||||||
|
## 非目标(本期不做)
|
||||||
|
|
||||||
|
- 不做历史版本/上报流水表
|
||||||
|
- 不做品牌/型号的字典化与关联(仅存字符串)
|
||||||
|
- 不在文档中定义具体 API(仅描述存储需求与规则)
|
||||||
|
|
||||||
@@ -457,6 +457,69 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/audio/reportImpedance": {
|
||||||
|
"get": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Device"
|
||||||
|
],
|
||||||
|
"summary": "上报耳机阻抗",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "设备 MAC 地址",
|
||||||
|
"name": "mac",
|
||||||
|
"in": "query",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "设备型号",
|
||||||
|
"name": "name",
|
||||||
|
"in": "query",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "耳机品牌",
|
||||||
|
"name": "brand",
|
||||||
|
"in": "query",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "耳机型号",
|
||||||
|
"name": "model",
|
||||||
|
"in": "query",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "阻抗值(整数)",
|
||||||
|
"name": "value",
|
||||||
|
"in": "query",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "操作成功",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/audio/shareAccept": {
|
"/audio/shareAccept": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "根据分享码获取他人分享的 EQ 数据",
|
"description": "根据分享码获取他人分享的 EQ 数据",
|
||||||
|
|||||||
@@ -303,6 +303,49 @@ paths:
|
|||||||
summary: 上报设备信息
|
summary: 上报设备信息
|
||||||
tags:
|
tags:
|
||||||
- Device
|
- Device
|
||||||
|
/audio/reportImpedance:
|
||||||
|
get:
|
||||||
|
parameters:
|
||||||
|
- description: 设备 MAC 地址
|
||||||
|
in: query
|
||||||
|
name: mac
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
- description: 设备型号
|
||||||
|
in: query
|
||||||
|
name: name
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
- description: 耳机品牌
|
||||||
|
in: query
|
||||||
|
name: brand
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
- description: 耳机型号
|
||||||
|
in: query
|
||||||
|
name: model
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
- description: 阻抗值(整数)
|
||||||
|
in: query
|
||||||
|
name: value
|
||||||
|
required: true
|
||||||
|
type: integer
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: 操作成功
|
||||||
|
schema:
|
||||||
|
additionalProperties: true
|
||||||
|
type: object
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
summary: 上报耳机阻抗
|
||||||
|
tags:
|
||||||
|
- Device
|
||||||
/audio/shareAccept:
|
/audio/shareAccept:
|
||||||
get:
|
get:
|
||||||
description: 根据分享码获取他人分享的 EQ 数据
|
description: 根据分享码获取他人分享的 EQ 数据
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ type Config struct {
|
|||||||
Equalize EqualizeConfig
|
Equalize EqualizeConfig
|
||||||
S3 S3Config
|
S3 S3Config
|
||||||
EnablePersistTask bool
|
EnablePersistTask bool
|
||||||
|
EnableImpedancePersistTask bool
|
||||||
ShareCodeMaxPerMac int
|
ShareCodeMaxPerMac int
|
||||||
ShareCodeTTL time.Duration
|
ShareCodeTTL time.Duration
|
||||||
}
|
}
|
||||||
@@ -65,6 +66,7 @@ func Load() (*Config, error) {
|
|||||||
Equalize: eq,
|
Equalize: eq,
|
||||||
S3: s3cfg,
|
S3: s3cfg,
|
||||||
EnablePersistTask: getEnv("ENABLE_PERSIST_TASK", "false") == "true",
|
EnablePersistTask: getEnv("ENABLE_PERSIST_TASK", "false") == "true",
|
||||||
|
EnableImpedancePersistTask: getEnv("ENABLE_IMPEDANCE_PERSIST_TASK", "false") == "true",
|
||||||
ShareCodeMaxPerMac: getEnvInt("SHARE_CODE_MAX_PER_MAC", 1),
|
ShareCodeMaxPerMac: getEnvInt("SHARE_CODE_MAX_PER_MAC", 1),
|
||||||
ShareCodeTTL: shareCodeTTL,
|
ShareCodeTTL: shareCodeTTL,
|
||||||
}, nil
|
}, 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)
|
model := handler.NewModelHandler(modelRepo, log)
|
||||||
modelList := handler.NewModelListHandler(searchClient, log)
|
modelList := handler.NewModelListHandler(searchClient, log)
|
||||||
device := handler.NewDeviceHandler(rdb, log)
|
device := handler.NewDeviceHandler(rdb, log)
|
||||||
|
impedance := handler.NewImpedanceHandler(rdb, log)
|
||||||
ota := handler.NewOTAHandler(otaRepo, log)
|
ota := handler.NewOTAHandler(otaRepo, log)
|
||||||
curve := handler.NewCurveHandler(curveRepo, curveCache, eqCfg, s3, log)
|
curve := handler.NewCurveHandler(curveRepo, curveCache, eqCfg, s3, log)
|
||||||
modelCSV := handler.NewModelCSVHandler(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("/getModel", middleware.CacheControl(middleware.CacheControlList), model.GetModel)
|
||||||
audio.GET("/modelList", middleware.CacheControl(middleware.CacheControlModelList), modelList.ModelList)
|
audio.GET("/modelList", middleware.CacheControl(middleware.CacheControlModelList), modelList.ModelList)
|
||||||
audio.GET("/reportDevInfo", device.ReportDevInfo)
|
audio.GET("/reportDevInfo", device.ReportDevInfo)
|
||||||
|
audio.GET("/reportImpedance", impedance.ReportImpedance)
|
||||||
audio.GET("/ota", ota.GetOTA)
|
audio.GET("/ota", ota.GetOTA)
|
||||||
audio.GET("/getCurve", middleware.CacheControl(middleware.CacheControlCurve), curve.GetCurve)
|
audio.GET("/getCurve", middleware.CacheControl(middleware.CacheControlCurve), curve.GetCurve)
|
||||||
audio.GET("/modelCurve", middleware.CacheControl(middleware.CacheControlCurve), curve.ModelCurve)
|
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
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
Navicat Premium Dump SQL
|
||||||
|
|
||||||
|
Source Server : localhost
|
||||||
|
Source Server Type : MySQL
|
||||||
|
Source Schema : audio
|
||||||
|
|
||||||
|
File Encoding : 65001
|
||||||
|
|
||||||
|
Feature: 用户上报耳机阻抗(最新值)
|
||||||
|
*/
|
||||||
|
|
||||||
|
SET NAMES utf8mb4;
|
||||||
|
SET FOREIGN_KEY_CHECKS = 0;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for user_headphone_impedance
|
||||||
|
-- ----------------------------
|
||||||
|
DROP TABLE IF EXISTS `user_headphone_impedance`;
|
||||||
|
CREATE TABLE `user_headphone_impedance` (
|
||||||
|
`id` int NOT NULL AUTO_INCREMENT,
|
||||||
|
`mac_addr` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
|
||||||
|
`device_model` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT 'Luxsin-X8/Luxsin-X9',
|
||||||
|
`impedance_ohm` int NOT NULL COMMENT '阻抗(Ω)整数',
|
||||||
|
`headphone_brand` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '耳机品牌(原始输入)',
|
||||||
|
`headphone_model` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '耳机型号(原始输入)',
|
||||||
|
`headphone_brand_norm` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '耳机品牌(trim+lower,用于去重)',
|
||||||
|
`headphone_model_norm` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '耳机型号(trim+lower,用于去重)',
|
||||||
|
`ip_addr` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '服务端获取的设备IP(最新值)',
|
||||||
|
`create_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`update_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`) USING BTREE,
|
||||||
|
UNIQUE INDEX `uniq_mac_brand_model`(`mac_addr` ASC, `headphone_brand_norm` ASC, `headphone_model_norm` ASC) USING BTREE,
|
||||||
|
INDEX `idx_mac`(`mac_addr` ASC) USING BTREE,
|
||||||
|
INDEX `idx_device_model`(`device_model` ASC) USING BTREE
|
||||||
|
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC;
|
||||||
|
|
||||||
|
SET FOREIGN_KEY_CHECKS = 1;
|
||||||
|
|
||||||
Reference in New Issue
Block a user