分享码日志新增 model 字段,记录操作的设备的型号 X8/X9

This commit is contained in:
eafonyang
2026-06-17 13:52:11 +08:00
parent 1e7dc01b22
commit 7687e6a0d9
12 changed files with 214 additions and 577 deletions
+1
View File
@@ -18,6 +18,7 @@ vendor/
# IDE
.idea/
.vscode/
.qoder/
*.swp
*.swo
+56 -46
View File
@@ -464,50 +464,6 @@ const docTemplate = `{
}
}
},
"/audio/shareQuery": {
"get": {
"description": "根据分享码预览 EQ 数据,不写入导入流水",
"produces": [
"application/json"
],
"tags": [
"ShareCode"
],
"summary": "查询 EQ 分享码",
"parameters": [
{
"type": "string",
"description": "5 位分享码",
"name": "shareCode",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"description": "成功返回 eq_data、expire_at",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"400": {
"description": "参数校验失败",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"500": {
"description": "系统错误",
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
}
},
"/audio/shareAccept": {
"get": {
"description": "根据分享码获取他人分享的 EQ 数据",
@@ -526,6 +482,13 @@ const docTemplate = `{
"in": "query",
"required": true
},
{
"type": "string",
"description": "操作设备型号,取值 Luxsin-X9 或 Luxsin-X8",
"name": "model",
"in": "query",
"required": true
},
{
"type": "string",
"description": "5 位分享码",
@@ -561,7 +524,7 @@ const docTemplate = `{
},
"/audio/shareCreate": {
"post": {
"description": "将用户的 EQ 数据生成一个 5 位分享码,有效期 30 分钟",
"description": "将用户的 EQ 数据生成一个 5 位分享码,有效期由 SHARE_CODE_TTL_MIN 配置决定(支持 30、30m、24h、30d 等格式)",
"consumes": [
"application/json"
],
@@ -574,7 +537,7 @@ const docTemplate = `{
"summary": "创建 EQ 分享码",
"parameters": [
{
"description": "分享请求",
"description": "分享请求model 取值 Luxsin-X9 或 Luxsin-X8",
"name": "body",
"in": "body",
"required": true,
@@ -586,6 +549,9 @@ const docTemplate = `{
},
"mac": {
"type": "string"
},
"model": {
"type": "string"
}
}
}
@@ -659,6 +625,50 @@ const docTemplate = `{
}
}
}
},
"/audio/shareQuery": {
"get": {
"description": "根据分享码预览 EQ 数据,不写入导入流水",
"produces": [
"application/json"
],
"tags": [
"ShareCode"
],
"summary": "查询 EQ 分享码",
"parameters": [
{
"type": "string",
"description": "5 位分享码",
"name": "shareCode",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"description": "成功返回 eq_data、expire_at",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"400": {
"description": "参数校验失败",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"500": {
"description": "系统错误",
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
}
}
}
}`
+56 -46
View File
@@ -457,50 +457,6 @@
}
}
},
"/audio/shareQuery": {
"get": {
"description": "根据分享码预览 EQ 数据,不写入导入流水",
"produces": [
"application/json"
],
"tags": [
"ShareCode"
],
"summary": "查询 EQ 分享码",
"parameters": [
{
"type": "string",
"description": "5 位分享码",
"name": "shareCode",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"description": "成功返回 eq_data、expire_at",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"400": {
"description": "参数校验失败",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"500": {
"description": "系统错误",
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
}
},
"/audio/shareAccept": {
"get": {
"description": "根据分享码获取他人分享的 EQ 数据",
@@ -519,6 +475,13 @@
"in": "query",
"required": true
},
{
"type": "string",
"description": "操作设备型号,取值 Luxsin-X9 或 Luxsin-X8",
"name": "model",
"in": "query",
"required": true
},
{
"type": "string",
"description": "5 位分享码",
@@ -554,7 +517,7 @@
},
"/audio/shareCreate": {
"post": {
"description": "将用户的 EQ 数据生成一个 5 位分享码,有效期 30 分钟",
"description": "将用户的 EQ 数据生成一个 5 位分享码,有效期由 SHARE_CODE_TTL_MIN 配置决定(支持 30、30m、24h、30d 等格式)",
"consumes": [
"application/json"
],
@@ -567,7 +530,7 @@
"summary": "创建 EQ 分享码",
"parameters": [
{
"description": "分享请求",
"description": "分享请求model 取值 Luxsin-X9 或 Luxsin-X8",
"name": "body",
"in": "body",
"required": true,
@@ -579,6 +542,9 @@
},
"mac": {
"type": "string"
},
"model": {
"type": "string"
}
}
}
@@ -652,6 +618,50 @@
}
}
}
},
"/audio/shareQuery": {
"get": {
"description": "根据分享码预览 EQ 数据,不写入导入流水",
"produces": [
"application/json"
],
"tags": [
"ShareCode"
],
"summary": "查询 EQ 分享码",
"parameters": [
{
"type": "string",
"description": "5 位分享码",
"name": "shareCode",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"description": "成功返回 eq_data、expire_at",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"400": {
"description": "参数校验失败",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"500": {
"description": "系统错误",
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
}
}
}
}
+40 -32
View File
@@ -303,36 +303,6 @@ paths:
summary: 上报设备信息
tags:
- Device
/audio/shareQuery:
get:
description: 根据分享码预览 EQ 数据,不写入导入流水
parameters:
- description: 5 位分享码
in: query
name: shareCode
required: true
type: string
produces:
- application/json
responses:
"200":
description: 成功返回 eq_data、expire_at
schema:
additionalProperties: true
type: object
"400":
description: 参数校验失败
schema:
additionalProperties: true
type: object
"500":
description: 系统错误
schema:
additionalProperties: true
type: object
summary: 查询 EQ 分享码
tags:
- ShareCode
/audio/shareAccept:
get:
description: 根据分享码获取他人分享的 EQ 数据
@@ -342,6 +312,11 @@ paths:
name: mac
required: true
type: string
- description: 操作设备型号,取值 Luxsin-X9 或 Luxsin-X8
in: query
name: model
required: true
type: string
- description: 5 位分享码
in: query
name: shareCode
@@ -372,9 +347,10 @@ paths:
post:
consumes:
- application/json
description: 将用户的 EQ 数据生成一个 5 位分享码,有效期 30 分钟
description: 将用户的 EQ 数据生成一个 5 位分享码,有效期由 SHARE_CODE_TTL_MIN 配置决定(支持 30、30m、24h、30d
等格式)
parameters:
- description: 分享请求
- description: 分享请求model 取值 Luxsin-X9 或 Luxsin-X8
in: body
name: body
required: true
@@ -384,6 +360,8 @@ paths:
type: object
mac:
type: string
model:
type: string
type: object
produces:
- application/json
@@ -436,4 +414,34 @@ paths:
summary: 查询未过期分享码
tags:
- ShareCode
/audio/shareQuery:
get:
description: 根据分享码预览 EQ 数据,不写入导入流水
parameters:
- description: 5 位分享码
in: query
name: shareCode
required: true
type: string
produces:
- application/json
responses:
"200":
description: 成功返回 eq_data、expire_at
schema:
additionalProperties: true
type: object
"400":
description: 参数校验失败
schema:
additionalProperties: true
type: object
"500":
description: 系统错误
schema:
additionalProperties: true
type: object
summary: 查询 EQ 分享码
tags:
- ShareCode
swagger: "2.0"
+17 -9
View File
@@ -5,6 +5,7 @@
// share:{code} Hash, TTL = SHARE_CODE_TTL_MIN — 分享码主数据
// mac_addr 创建者 MAC 地址
// ip_addr 创建者 IP
// model 操作设备型号
// eq_data EQ 参数 JSON
// expire_at 过期时间 (RFC3339)
// persisted 是否已刷入 DB ("0"/"1")
@@ -57,6 +58,7 @@ const (
fieldMacAddr = "mac_addr"
fieldIPAddr = "ip_addr"
fieldModel = "model"
fieldEqData = "eq_data"
fieldExpireAt = "expire_at"
fieldPersisted = "persisted"
@@ -69,21 +71,23 @@ end
redis.call('HSET', KEYS[1],
'mac_addr', ARGV[1],
'ip_addr', ARGV[2],
'eq_data', ARGV[3],
'expire_at', ARGV[4],
'model', ARGV[3],
'eq_data', ARGV[4],
'expire_at', ARGV[5],
'persisted', '0'
)
redis.call('EXPIRE', KEYS[1], tonumber(ARGV[5]))
redis.call('SADD', KEYS[2], ARGV[6])
redis.call('EXPIRE', KEYS[1], tonumber(ARGV[6]))
redis.call('SADD', KEYS[2], ARGV[7])
-- ZSET: score = expire_at unix timestamp, member = share code
redis.call('ZADD', KEYS[3], tonumber(ARGV[7]), ARGV[6])
redis.call('EXPIRE', KEYS[3], tonumber(ARGV[8]))
redis.call('ZADD', KEYS[3], tonumber(ARGV[8]), ARGV[7])
redis.call('EXPIRE', KEYS[3], tonumber(ARGV[9]))
return 1
`)
type ShareImportPendingLog struct {
MacAddr string `json:"mac_addr"`
ShareCode string `json:"share_code"`
Model string `json:"model"`
IpAddr string `json:"ip_addr"`
EqData string `json:"eq_data"`
ExpireAt string `json:"expire_at"`
@@ -93,6 +97,7 @@ type ShareCodeData struct {
ShareCode string
MacAddr string
IpAddr string
Model string
EqData string
ExpireAt time.Time
Persisted bool
@@ -112,7 +117,7 @@ func (c *ShareCodeCache) ShareCodeTTL() time.Duration {
return c.codeTTL
}
func (c *ShareCodeCache) Create(ctx context.Context, macAddr, ipAddr string, eqData []byte) (*ShareCodeData, error) {
func (c *ShareCodeCache) Create(ctx context.Context, macAddr, ipAddr, model string, eqData []byte) (*ShareCodeData, error) {
expireAt := time.Now().Add(c.codeTTL)
eqJSON := string(eqData)
@@ -125,7 +130,7 @@ func (c *ShareCodeCache) Create(ctx context.Context, macAddr, ipAddr string, eqD
key := shareCodeKey(code)
macIdxKey := shareMacIndexKey(macAddr)
ok, err := shareCreateScript.Run(ctx, c.rdb, []string{key, sharePendingSet, macIdxKey},
macAddr, ipAddr, eqJSON, expireAt.Format(time.RFC3339), int(c.codeTTL.Seconds()), code, expireAt.Unix(), int(c.codeTTL.Seconds()),
macAddr, ipAddr, model, eqJSON, expireAt.Format(time.RFC3339), int(c.codeTTL.Seconds()), code, expireAt.Unix(), int(c.codeTTL.Seconds()),
).Int()
if err != nil {
return nil, fmt.Errorf("create share code in redis: %w", err)
@@ -135,6 +140,7 @@ func (c *ShareCodeCache) Create(ctx context.Context, macAddr, ipAddr string, eqD
ShareCode: code,
MacAddr: macAddr,
IpAddr: ipAddr,
Model: model,
EqData: eqJSON,
ExpireAt: expireAt,
Persisted: false,
@@ -164,6 +170,7 @@ func (c *ShareCodeCache) Get(ctx context.Context, shareCode string) (*ShareCodeD
ShareCode: shareCode,
MacAddr: values[fieldMacAddr],
IpAddr: values[fieldIPAddr],
Model: values[fieldModel],
EqData: values[fieldEqData],
ExpireAt: expireAt,
Persisted: values[fieldPersisted] == "1",
@@ -208,11 +215,12 @@ func (c *ShareCodeCache) MarkPersisted(ctx context.Context, shareCode string) er
return c.rdb.SRem(ctx, sharePendingSet, shareCode).Err()
}
func (c *ShareCodeCache) EnqueueImportLog(ctx context.Context, macAddr, shareCode, ipAddr, eqData string, expireAt time.Time) error {
func (c *ShareCodeCache) EnqueueImportLog(ctx context.Context, macAddr, shareCode, model, ipAddr, eqData string, expireAt time.Time) error {
field := fmt.Sprintf("%s:%s", macAddr, shareCode) // 同 MAC+code 幂等,避免重复导入产生多条记录
payload, err := json.Marshal(ShareImportPendingLog{
MacAddr: macAddr,
ShareCode: shareCode,
Model: model,
IpAddr: ipAddr,
EqData: eqData,
ExpireAt: expireAt.Format(time.RFC3339),
+28 -7
View File
@@ -7,6 +7,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/luxsin/app-api/internal/cache"
"github.com/luxsin/app-api/internal/model"
"github.com/luxsin/app-api/pkg/encode"
"go.uber.org/zap"
)
@@ -28,17 +29,18 @@ func NewShareCodeHandler(shareCache *cache.ShareCodeCache, log *zap.Logger, maxP
// @Tags ShareCode
// @Accept json
// @Produce json
// @Param body body object{mac=string,eq_data=object} true "分享请求"
// @Param body body object{mac=string,model=string,eq_data=object} true "分享请求model 取值 Luxsin-X9 或 Luxsin-X8"
// @Success 200 {object} map[string]any "成功返回 share_code、expire_at、eq_data"
// @Failure 400 {object} map[string]any "参数校验失败"
// @Failure 500 {object} map[string]any "系统错误"
// @Router /audio/shareCreate [post]
//
// POST /audio/shareCreate
// Body: {"mac": "xx", "eq_data": {...}}
// Body: {"mac": "xx", "model": "Luxsin-X9", "eq_data": {...}}
func (h *ShareCodeHandler) ExportShareCode(c *gin.Context) {
var req struct {
Mac string `json:"mac"`
Model string `json:"model"`
EqData json.RawMessage `json:"eq_data"`
}
if err := c.ShouldBindJSON(&req); err != nil {
@@ -50,10 +52,11 @@ func (h *ShareCodeHandler) ExportShareCode(c *gin.Context) {
}
mac := strings.TrimSpace(req.Mac)
deviceModel := strings.TrimSpace(req.Model)
eqDataRaw := strings.TrimSpace(string(req.EqData))
clientIP := encode.ClientPublicIP(c)
if mac == "" || eqDataRaw == "" {
if mac == "" || deviceModel == "" || eqDataRaw == "" {
c.JSON(http.StatusOK, gin.H{
"code": 400,
"msg": "参数校验失败",
@@ -61,6 +64,14 @@ func (h *ShareCodeHandler) ExportShareCode(c *gin.Context) {
return
}
if !model.IsValidShareDeviceModel(deviceModel) {
c.JSON(http.StatusOK, gin.H{
"code": 400,
"msg": "model 参数无效",
})
return
}
if !json.Valid([]byte(eqDataRaw)) {
c.JSON(http.StatusOK, gin.H{
"code": 400,
@@ -89,7 +100,7 @@ func (h *ShareCodeHandler) ExportShareCode(c *gin.Context) {
return
}
data, err := h.cache.Create(ctx, mac, clientIP, []byte(eqDataRaw))
data, err := h.cache.Create(ctx, mac, clientIP, deviceModel, []byte(eqDataRaw))
if err != nil {
h.log.Error("create share code failed", zap.Error(err))
c.JSON(http.StatusOK, gin.H{
@@ -205,19 +216,21 @@ func (h *ShareCodeHandler) QueryShareCode(c *gin.Context) {
// @Tags ShareCode
// @Produce json
// @Param mac query string true "设备 MAC 地址"
// @Param model query string true "操作设备型号,取值 Luxsin-X9 或 Luxsin-X8"
// @Param shareCode query string true "5 位分享码"
// @Success 200 {object} map[string]any "成功返回 eq_data"
// @Failure 400 {object} map[string]any "参数校验失败"
// @Failure 500 {object} map[string]any "系统错误"
// @Router /audio/shareAccept [get]
//
// GET /audio/shareAccept?mac=xx&shareCode=ABC12
// GET /audio/shareAccept?mac=xx&model=Luxsin-X9&shareCode=ABC12
func (h *ShareCodeHandler) ImportShareCode(c *gin.Context) {
mac := strings.TrimSpace(c.Query("mac"))
deviceModel := strings.TrimSpace(c.Query("model"))
shareCode := strings.TrimSpace(c.Query("shareCode"))
clientIP := encode.ClientPublicIP(c)
if mac == "" || shareCode == "" {
if mac == "" || deviceModel == "" || shareCode == "" {
c.JSON(http.StatusOK, gin.H{
"code": 400,
"msg": "参数校验失败",
@@ -225,13 +238,21 @@ func (h *ShareCodeHandler) ImportShareCode(c *gin.Context) {
return
}
if !model.IsValidShareDeviceModel(deviceModel) {
c.JSON(http.StatusOK, gin.H{
"code": 400,
"msg": "model 参数无效",
})
return
}
data, ok := h.lookupShareCode(c, shareCode)
if !ok {
return
}
ctx := c.Request.Context()
if err := h.cache.EnqueueImportLog(ctx, mac, shareCode, clientIP, data.EqData, data.ExpireAt); err != nil {
if err := h.cache.EnqueueImportLog(ctx, mac, shareCode, deviceModel, clientIP, data.EqData, data.ExpireAt); err != nil {
h.log.Error("enqueue share import log failed",
zap.String("share_code", shareCode),
zap.Error(err),
+10
View File
@@ -2,11 +2,21 @@ package model
import "time"
const (
ShareDeviceModelX9 = "Luxsin-X9"
ShareDeviceModelX8 = "Luxsin-X8"
)
func IsValidShareDeviceModel(model string) bool {
return model == ShareDeviceModelX9 || model == ShareDeviceModelX8
}
type ShareCodeLog struct {
ID int
MacAddr string
ShareCode string
Action string // export | import
Model string
IpAddr string
EqData []byte
ExpireAt *time.Time
+3 -3
View File
@@ -18,8 +18,8 @@ func NewShareCodeRepository(db *sql.DB) *ShareCodeRepository {
}
func (r *ShareCodeRepository) InsertLog(ctx context.Context, log model.ShareCodeLog) error {
const query = `INSERT INTO share_code_log (mac_addr, share_code, action, ip_addr, eq_data, expire_at)
VALUES (?, ?, ?, ?, ?, ?)`
const query = `INSERT INTO share_code_log (mac_addr, share_code, action, model, ip_addr, eq_data, expire_at)
VALUES (?, ?, ?, ?, ?, ?, ?)`
var expireAt any
if log.ExpireAt != nil {
@@ -27,7 +27,7 @@ func (r *ShareCodeRepository) InsertLog(ctx context.Context, log model.ShareCode
}
_, err := r.db.ExecContext(ctx, query,
log.MacAddr, log.ShareCode, log.Action, log.IpAddr, log.EqData, expireAt,
log.MacAddr, log.ShareCode, log.Action, log.Model, log.IpAddr, log.EqData, expireAt,
)
if err != nil {
return fmt.Errorf("insert share_code_log: %w", err)
+2
View File
@@ -158,6 +158,7 @@ func (t *ShareCodePersistTask) persistExportOne(ctx context.Context, shareCode s
MacAddr: data.MacAddr,
ShareCode: data.ShareCode,
Action: "export",
Model: data.Model,
IpAddr: data.IpAddr,
EqData: []byte(data.EqData),
ExpireAt: repository.ParseShareExpireAt(data.ExpireAt),
@@ -207,6 +208,7 @@ func (t *ShareCodePersistTask) persistImportOne(ctx context.Context, field, payl
MacAddr: pending.MacAddr,
ShareCode: pending.ShareCode,
Action: "import",
Model: pending.Model,
IpAddr: pending.IpAddr,
EqData: []byte(pending.EqData),
ExpireAt: repository.ParseShareExpireAt(expireAt),
-154
View File
@@ -1,154 +0,0 @@
package com.luxsin.app.controller;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.luxsin.app.annotation.Base64Response;
import com.luxsin.app.bean.OTA;
import com.luxsin.app.service.BrandService;
import com.luxsin.app.service.ModelService;
import com.luxsin.app.service.OTAService;
import lombok.extern.slf4j.Slf4j;
import org.eafon.data.EResponseTool;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDate;
import java.util.Map;
@Slf4j
@RestController
@CrossOrigin(origins = "*")
public class EqualizeController {
private final ModelService modelService;
private final OTAService otaService;
private final RedisTemplate<String, String> redisTemplate;
private final BrandService brandService;
public EqualizeController(ModelService modelService, OTAService otaService, RedisTemplate<String, String> redisTemplate, BrandService brandService) {
this.modelService = modelService;
this.otaService = otaService;
this.redisTemplate = redisTemplate;
this.brandService = brandService;
}
/**
* 耳机列表
* @param key
* @param count
* @return
*/
@GetMapping("modelList")
@Base64Response
public String getModelList(String key, @RequestParam(value = "count", defaultValue = "100") int count){
return modelService.modelList(key, count);
}
/**
* 目标曲线
* @param brand
* @param name
* @param target
* @return
*/
@GetMapping("getCurve")
@Base64Response
public String getCurve(String brand, String name, String target, @RequestParam(value = "includeRaw", defaultValue = "false") boolean includeRaw){
String result = modelService.getCurvePoint(brand, name, target);
if(!includeRaw){
JSONObject resp = JSONUtil.parseObj(result);
if(resp.containsKey("code") && resp.getInt("code") != 0){
resp.remove("fr");
}
return resp.toString();
}
return result;
}
@GetMapping("modelCurve")
@Base64Response
public String modelCurve(String brand, String name){
return modelService.getModelCurve(brand, name);
}
@GetMapping("test/modelCurve")
public String testModelCurve(String brand, String name){
return modelService.getModelCurve(brand, name);
}
@GetMapping("test/getCurve")
public String testGetCurve(String brand, String name, String target,
@RequestParam(value = "includeRaw", defaultValue = "false") boolean includeRaw){
String result = modelService.getCurvePoint(brand, name, target);
if(!includeRaw){
JSONObject resp = JSONUtil.parseObj(result);
if(resp.containsKey("code") && resp.getInt("code") != 0){
resp.remove("fr");
}
return resp.toString();
}
return result;
}
/**
* 升级
* @param model
* @param hw
* @param mac
* @param beta
* @return
*/
@GetMapping("ota")
public EResponseTool<OTA> OTA(String model, String hw, @RequestParam(value = "mac", defaultValue = "null") String mac,
@RequestParam(value = "beta", defaultValue = "0") int beta){
return otaService.getOTA(model, hw, mac, beta);
}
/**
* 上报用户设备信息
* @param mac
* @param model
* @return
*/
@GetMapping("reportDevInfo")
public EResponseTool<String> reportDevInfo(String mac, String model,String ver,
@RequestHeader(value = "X-Forwarded-For", required = false) String xForwardedFor){
if(StrUtil.isEmpty(mac) || StrUtil.isEmpty(model)){
return EResponseTool.error(EResponseTool.ResultCode.VALIDATE_FAILED);
}
log.info("report dev info remote ip = {}, now time = {}", xForwardedFor, DateUtil.now());
ver = null == ver ? "" : ver;
xForwardedFor = null == xForwardedFor ? "" : xForwardedFor;
redisTemplate.opsForHash().put("devices", mac, JSONUtil.toJsonStr(Map.of("mac_addr", mac, "model", model,
"active_date", LocalDate.now().toString(),
"ip_addr", xForwardedFor,
"ver", ver)));
return EResponseTool.success();
}
/**
* 获取品牌列表
* @param brandName
* @return
*/
@GetMapping("getBrand")
@Base64Response
public String getBrand(String brandName){
return brandService.brandList(brandName);
}
/**
* 获取型号列表
* @param brandName
* @param modelName
* @return
*/
@GetMapping("getModel")
@Base64Response
public String getModel(String brandName, String modelName){
return modelService.getModels(brandName, modelName);
}
}
-280
View File
@@ -1,280 +0,0 @@
package com.luxsin.app.service;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.luxsin.app.bean.Model;
import com.luxsin.app.bean.Target;
import com.luxsin.app.bean.table.ModelTableDef;
import com.luxsin.app.bean.table.TargetTableDef;
import com.luxsin.app.config.MyConfig;
import com.luxsin.app.dao.ModelMapper;
import com.luxsin.app.dao.TargetMapper;
import com.meilisearch.sdk.Index;
import com.meilisearch.sdk.SearchRequest;
import com.meilisearch.sdk.model.Searchable;
import com.mybatisflex.core.query.QueryWrapper;
import com.mybatisflex.spring.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.eafon.data.EResponseTool;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@Slf4j
@Service
public class ModelService extends ServiceImpl<ModelMapper, Model> {
private final RedisTemplate<String, String> redisTemplate;
private final TargetMapper targetMapper;
private final MyConfig myConfig;
private final Index modelIdx;
public ModelService(RedisTemplate<String, String> redisTemplate, TargetMapper targetMapper,
MyConfig myConfig, Index modelIdx) {
this.redisTemplate = redisTemplate;
this.targetMapper = targetMapper;
this.myConfig = myConfig;
this.modelIdx = modelIdx;
}
/**
* 获取手机列表
* @param key
* @param count
* @return
*/
public String modelList(String key, int count){
List<Model> list = Collections.emptyList();
Searchable searchable = modelIdx.search(SearchRequest.builder()
.q(key)
.attributesToRetrieve(new String[]{"rig", "form", "name", "brand_name", "source", "eq_key"})
.limit(count)
.build());
if(!searchable.getHits().isEmpty()){
return JSONUtil.toJsonStr(searchable.getHits());
}else {
return JSONUtil.toJsonStr(list);
}
}
/**
* 获取目标曲线
* @param brand
* @param name
* @param targetName
* @param raw 是否只是需要机型的raw数据
* @return
*/
public String getCurvePoint(String brand, String name, String targetName){
if(StrUtil.isEmpty(brand) || StrUtil.isEmpty(name) || StrUtil.isEmpty(targetName)){
return EResponseTool.error(EResponseTool.ResultCode.VALIDATE_FAILED).toString();
}
String cacheKey = brand + " " + name;
String cache = String.valueOf(redisTemplate.opsForHash().get(cacheKey, targetName));
if(StrUtil.isEmpty(cache) || "null".equals(cache)){
String lockKey = cacheKey+":"+targetName+":lock";
boolean locked = Boolean.TRUE.equals(redisTemplate.opsForValue().setIfAbsent(lockKey, "locked", 10, TimeUnit.SECONDS));
if(locked){
try {
cache = String.valueOf(redisTemplate.opsForHash().get(cacheKey, targetName));
if(StrUtil.isEmpty(cache) || "null".equals(cache)){
//redis中没有数据的,需要请求eq接口
JSONObject resp = getCurvePointFromPEQ(brand, name, targetName);
if(null!=resp && resp.getInt("code") == EResponseTool.ResultCode.SUCCESS.getCode()){
//将请求接口得到的数据,放入缓存
redisTemplate.opsForHash().put(brand+" "+name, targetName, resp.toString());
return resp.toString();
}
}
}finally {
redisTemplate.delete(lockKey);
}
}else{
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return getCurvePoint(brand, name, targetName);
}
}else{
log.info("brand: {} name: {} target {} read cache.", brand, name, targetName);
return cache;
}
return EResponseTool.error(EResponseTool.ResultCode.EMPTY).toString();
}
private JSONObject getCurvePointFromPEQ(String brand, String name, String targetName){
Model model = getOne(QueryWrapper.create().where(ModelTableDef.MODEL.BRAND_NAME.eq(brand)).
and(ModelTableDef.MODEL.NAME.eq(name)));
if(!Objects.isNull(model)){
Target target = targetMapper.selectOneByQuery(QueryWrapper.create().where(TargetTableDef.TARGET.LABEL.eq(targetName)));
if(!Objects.isNull(target)){
String eq_key = model.getEq_key();
String headPhone = brand+" "+name;
if(StrUtil.isNotEmpty(eq_key) && eq_key.equals("name")){
headPhone = name;
}
JSONObject measurement = null;
if("Eafonyoung".equals(model.getSource())){
measurement = getCSV(myConfig.getMeasurementBasePath()+"/Eafonyoung/data/"+model.getForm()+"/"+headPhone+".csv");
if(Objects.isNull(measurement)){
return null;
}
}
JSONObject resp;
if(target.isRead_csv()){
//读取自定义的target
JSONObject targetRaw = getCSV(myConfig.getTargetBasePath()+"/"+target.getFile());
resp = reqEqualize(headPhone, measurement, targetRaw, JSONUtil.parseObj(target.getBassBoost()),
model.getSource(), model.getRig());
}else{
resp = reqEqualize(headPhone, measurement, targetName, JSONUtil.parseObj(target.getBassBoost()),
model.getSource(), model.getRig());
}
if(resp.getInt("code") == EResponseTool.ResultCode.SUCCESS.getCode()){
return resp;
}else{
log.info("请求reqEqualize接口失败,没有数据");
}
}
}
return null;
}
private JSONObject getCSV(String path){
if(FileUtil.exist(path)){
List<String> list = FileUtil.readLines(path, "UTF-8");
JSONArray frequency = new JSONArray();
JSONArray raw = new JSONArray();
for(int i=1;i<list.size();i++){
String[] values = list.get(i).split(",");
frequency.add(NumberUtil.parseFloat(values[0]));
raw.add(NumberUtil.parseFloat(values[1]));
}
return new JSONObject(Map.of("frequency",frequency,"raw",raw));
}
log.info("找不到耳机频响文件:{}", path);
return null;
}
/**
*
* @param headPhone 耳机名称(品牌+型号)
* @param measurement 耳机csv文件,和耳机名称二选一,如果耳机名称不为空优先耳机名称
* @param target 目标曲线名称,或者目标曲线的csv文件
* @param bassBoost
* @param source
* @param rig
* @return
*/
private JSONObject reqEqualizeInternal(String headPhone,JSONObject measurement, Object target, JSONObject bassBoost, String source, String rig) {
Map<String, Object> reqMap = new HashMap<>();
reqMap.put("target", target);
reqMap.put("sound_signature", null);
reqMap.put("sound_signature_smoothing_window_size", 1);
reqMap.put("bass_boost_gain", bassBoost.getFloat("gain"));
reqMap.put("bass_boost_fc", bassBoost.getInt("fc"));
reqMap.put("bass_boost_q", bassBoost.getFloat("q"));
reqMap.put("treble_boost_gain",0);
reqMap.put("treble_boost_fc", 10000);
reqMap.put("treble_boost_q", 0.7);
reqMap.put("tilt", 0);
reqMap.put("fs",48000);
reqMap.put("bit_depth", 16);
reqMap.put("phase", "minimum");
reqMap.put("f_res", 16);
reqMap.put("preamp",0);
reqMap.put("max_gain" ,12);
reqMap.put("max_slope", 18);
reqMap.put("window_size", 0.08);
reqMap.put("treble_window_size", 2);
reqMap.put("treble_f_lower", 6000);
reqMap.put("treble_f_upper", 8000);
reqMap.put("treble_gain_k", 1);
reqMap.put("graphic_eq", false);
reqMap.put("parametric_eq", true);
reqMap.put("fixed_band_eq", false);
reqMap.put("convolution_eq", false);
if(null!=measurement){
reqMap.put("measurement", measurement);
}
else if(StrUtil.isNotEmpty(headPhone)){
reqMap.put("name", headPhone);
}
reqMap.put("source", source);
reqMap.put("rig", rig);
reqMap.put("parametric_eq_config", "MINIDSP_IL_DSP");
Map<String, Object> response = new HashMap<>();
JSONArray fr_fields = new JSONArray();
fr_fields.addAll(List.of("raw"));
response.put("fr_f_step", 1.02);
response.put("base64fp16" ,false);
response.put("fr_fields", fr_fields);
reqMap.put("response", response);
String resp = HttpUtil.post("https://autoeq.app/equalize", new JSONObject(reqMap).toString(), 10000);
JSONObject result = new JSONObject();
if(StrUtil.isNotEmpty(resp)){
JSONObject respJson = JSONUtil.parseObj(resp);
JSONObject parametric_eq = respJson.getJSONObject("parametric_eq");
if(Objects.isNull(parametric_eq)){
result.putOnce("code", 0);
result.putOnce("msg", "req param error");
result.putOnce("param", reqMap);
return result;
}else {
result.putOnce("parametric_eq", respJson.getJSONObject("parametric_eq"));
result.putOnce("fr", respJson.getJSONObject("fr"));
}
}
result.putOnce("code", EResponseTool.ResultCode.SUCCESS.getCode());
result.putOnce("msg", "ok");
return result;
}
public JSONObject reqEqualize(String headPhone, JSONObject measurement, String target, JSONObject bassBoost, String source, String rig) {
return reqEqualizeInternal(headPhone, measurement, target, bassBoost, source, rig);
}
public JSONObject reqEqualize(String headPhone, JSONObject measurement, JSONObject target, JSONObject bassBoost, String source, String rig) {
return reqEqualizeInternal(headPhone, measurement, target, bassBoost, source, rig);
}
public String getModels(String brandName, String modelName){
List<Model> list;
if(StrUtil.isNotEmpty(brandName)){
list = list(QueryWrapper.create().where(ModelTableDef.MODEL.BRAND_NAME.eq(brandName)).orderBy(ModelTableDef.MODEL.NAME, true));
}
else if(StrUtil.isNotEmpty(modelName)){
list = list(QueryWrapper.create().where(ModelTableDef.MODEL.NAME.like(modelName)).orderBy(ModelTableDef.MODEL.NAME, true));
}
else{
list = Collections.emptyList();
}
return JSONUtil.toJsonStr(list);
}
public String getModelCurve(String brand, String name){
return getCurvePoint(brand, name, "Harman over-ear 2018");
}
}
+1
View File
@@ -11,6 +11,7 @@ CREATE TABLE `share_code_log` (
`mac_addr` varchar(17) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '设备 MAC 地址,如 AA:BB:CC:DD:EE:FF',
`share_code` char(5) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '分享码(5位字符)',
`action` enum('export','import') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '操作类型:export=导出分享码,import=导入分享码',
`model` enum('Luxsin-X9','Luxsin-X8') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '操作设备:是在哪一台设备上做的导出/导入操作',
`ip_addr` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '用户 IP 地址(支持 IPv4/IPv6',
`eq_data` json NOT NULL COMMENT 'EQ 数据快照(JSON',
`expire_at` datetime NULL DEFAULT NULL COMMENT '分享码到期时间(导出时快照,导入可为空)',