分享码日志新增 model 字段,记录操作的设备的型号 X8/X9
This commit is contained in:
@@ -18,6 +18,7 @@ vendor/
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
.qoder/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
|
||||
+56
-46
@@ -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
@@ -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
@@ -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"
|
||||
|
||||
Vendored
+17
-9
@@ -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),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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 '分享码到期时间(导出时快照,导入可为空)',
|
||||
|
||||
Reference in New Issue
Block a user