From 5d2910e89e4c6c657c5f789189e897c5f2b159b9 Mon Sep 17 00:00:00 2001 From: eafonyang Date: Mon, 6 Jul 2026 10:41:41 +0800 Subject: [PATCH] =?UTF-8?q?=E8=80=B3=E6=9C=BA=E9=98=BB=E6=8A=97=E4=B8=8A?= =?UTF-8?q?=E6=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/server/main.go | 8 + docker-compose.yml | 1 + docs/docs.go | 63 ++++++++ docs/headphone-impedance.md | 56 +++++++ docs/swagger.json | 63 ++++++++ docs/swagger.yaml | 43 ++++++ internal/config/config.go | 10 +- internal/handler/impedance.go | 115 +++++++++++++++ internal/model/user_headphone_impedance.go | 17 +++ internal/repository/headphone_impedance.go | 99 +++++++++++++ internal/router/router.go | 2 + internal/task/impedance_persist.go | 162 +++++++++++++++++++++ sql/user_headphone_impedance.sql | 39 +++++ 13 files changed, 674 insertions(+), 4 deletions(-) create mode 100644 docs/headphone-impedance.md create mode 100644 internal/handler/impedance.go create mode 100644 internal/model/user_headphone_impedance.go create mode 100644 internal/repository/headphone_impedance.go create mode 100644 internal/task/impedance_persist.go create mode 100644 sql/user_headphone_impedance.sql diff --git a/cmd/server/main.go b/cmd/server/main.go index 9d630ac..1c8a2ef 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -97,6 +97,14 @@ func main() { 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) if err != nil { log.Fatal("s3 client init failed", zap.Error(err)) diff --git a/docker-compose.yml b/docker-compose.yml index 933ef8a..b241f3c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,6 +15,7 @@ services: - REDIS_PASSWORD=${REDIS_PASSWORD:-eafon123!} - MEILISEARCH_API_KEY=${MEILISEARCH_API_KEY:-young9#!UJsD219921031} - ENABLE_PERSIST_TASK=false + - ENABLE_IMPEDANCE_PERSIST_TASK=false - AWS_REGION=eu-central-1 - S3_BUCKET=luxsin-app-bucket - SHARE_CODE_MAX_PER_MAC=${SHARE_CODE_MAX_PER_MAC:-1} diff --git a/docs/docs.go b/docs/docs.go index df31eed..f935598 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -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": { "get": { "description": "根据分享码获取他人分享的 EQ 数据", diff --git a/docs/headphone-impedance.md b/docs/headphone-impedance.md new file mode 100644 index 0000000..7dea652 --- /dev/null +++ b/docs/headphone-impedance.md @@ -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(仅描述存储需求与规则) + diff --git a/docs/swagger.json b/docs/swagger.json index 2908a7b..629582c 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -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": { "get": { "description": "根据分享码获取他人分享的 EQ 数据", diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 6373f39..a9a1c96 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -303,6 +303,49 @@ paths: summary: 上报设备信息 tags: - 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: get: description: 根据分享码获取他人分享的 EQ 数据 diff --git a/internal/config/config.go b/internal/config/config.go index 46737f7..cc96256 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -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 } diff --git a/internal/handler/impedance.go b/internal/handler/impedance.go new file mode 100644 index 0000000..be8b822 --- /dev/null +++ b/internal/handler/impedance.go @@ -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) +} diff --git a/internal/model/user_headphone_impedance.go b/internal/model/user_headphone_impedance.go new file mode 100644 index 0000000..7ea3640 --- /dev/null +++ b/internal/model/user_headphone_impedance.go @@ -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 +} diff --git a/internal/repository/headphone_impedance.go b/internal/repository/headphone_impedance.go new file mode 100644 index 0000000..109ebcc --- /dev/null +++ b/internal/repository/headphone_impedance.go @@ -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 +} diff --git a/internal/router/router.go b/internal/router/router.go index 449ea2c..bdff3da 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -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) diff --git a/internal/task/impedance_persist.go b/internal/task/impedance_persist.go new file mode 100644 index 0000000..d372ea1 --- /dev/null +++ b/internal/task/impedance_persist.go @@ -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 +} diff --git a/sql/user_headphone_impedance.sql b/sql/user_headphone_impedance.sql new file mode 100644 index 0000000..9c4c9f2 --- /dev/null +++ b/sql/user_headphone_impedance.sql @@ -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; +