diff --git a/backend/src/models/ShareCodeLog.js b/backend/src/models/ShareCodeLog.js new file mode 100644 index 0000000..77c7f91 --- /dev/null +++ b/backend/src/models/ShareCodeLog.js @@ -0,0 +1,59 @@ +const { DataTypes } = require('sequelize'); +const sequelize = require('../config/database'); + +const ShareCodeLog = sequelize.define('ShareCodeLog', { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true, + comment: '主键', + }, + mac_addr: { + type: DataTypes.STRING(17), + allowNull: false, + comment: '设备 MAC 地址,如 AA:BB:CC:DD:EE:FF', + }, + share_code: { + type: DataTypes.CHAR(5), + allowNull: false, + comment: '分享码(5位字符)', + }, + action: { + type: DataTypes.ENUM('export', 'import'), + allowNull: false, + comment: '操作类型:export=导出分享码,import=导入分享码', + }, + ip_addr: { + type: DataTypes.STRING(45), + allowNull: false, + defaultValue: '', + comment: '用户 IP 地址(支持 IPv4/IPv6)', + }, + eq_data: { + type: DataTypes.JSON, + allowNull: false, + comment: 'EQ 数据快照(JSON)', + }, + expire_at: { + type: DataTypes.DATE, + allowNull: true, + comment: '分享码到期时间(导出时快照,导入可为空)', + }, + create_at: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: DataTypes.NOW, + comment: '操作时间', + }, +}, { + tableName: 'share_code_log', + comment: '分享码流水表', + indexes: [ + { name: 'idx_mac_addr', fields: ['mac_addr'] }, + { name: 'idx_share_code', fields: ['share_code'] }, + { name: 'idx_create_at', fields: ['create_at'] }, + ], +}); + +module.exports = ShareCodeLog; + diff --git a/backend/src/models/index.js b/backend/src/models/index.js index b365f9a..b4f4be3 100644 --- a/backend/src/models/index.js +++ b/backend/src/models/index.js @@ -1,5 +1,6 @@ const Brand = require('./Brand'); const Model = require('./Model'); const Ota = require('./Ota'); +const ShareCodeLog = require('./ShareCodeLog'); -module.exports = { Brand, Model, Ota }; +module.exports = { Brand, Model, Ota, ShareCodeLog }; diff --git a/backend/src/routes/index.js b/backend/src/routes/index.js index e8f36cc..a0898ff 100644 --- a/backend/src/routes/index.js +++ b/backend/src/routes/index.js @@ -5,5 +5,6 @@ const authRouter = require('./auth'); const brandsRouter = require('./brands'); const modelsRouter = require('./models'); const otaRouter = require('./ota'); +const shareCodeLogsRouter = require('./shareCodeLogs'); -module.exports = [authRouter, brandsRouter, modelsRouter, otaRouter]; +module.exports = [authRouter, brandsRouter, modelsRouter, otaRouter, shareCodeLogsRouter]; diff --git a/backend/src/routes/shareCodeLogs.js b/backend/src/routes/shareCodeLogs.js new file mode 100644 index 0000000..dc5f676 --- /dev/null +++ b/backend/src/routes/shareCodeLogs.js @@ -0,0 +1,87 @@ +/** + * 分享码日志路由 + */ +const router = require('express').Router(); +const { Op } = require('sequelize'); +const ShareCodeLog = require('../models/ShareCodeLog'); +const logger = require('../config/logger'); +const { ApiResponse } = require('../utils/response'); +const { authMiddleware } = require('../middleware/auth'); + +// 全部接口需登录 +router.use(authMiddleware); + +// GET /api/share-code/logs +router.get('/api/share-code/logs', async (req, res) => { + try { + const skip = parseInt(req.query.skip || '0', 10); + const limit = Math.min(parseInt(req.query.limit || '100', 10), 1000); + + const macAddr = (req.query.mac_addr || '').trim(); + const shareCode = (req.query.share_code || '').trim(); + const action = (req.query.action || '').trim(); + const ipAddr = (req.query.ip_addr || '').trim(); + + const startAt = (req.query.start_at || '').trim(); + const endAt = (req.query.end_at || '').trim(); + + const sortBy = (req.query.sort_by || 'id').trim(); + const sortOrder = (req.query.sort_order || 'desc').toLowerCase(); + + const where = {}; + if (macAddr) where.mac_addr = { [Op.like]: `%${macAddr}%` }; + if (shareCode) where.share_code = { [Op.like]: `%${shareCode}%` }; + if (ipAddr) where.ip_addr = { [Op.like]: `%${ipAddr}%` }; + if (action === 'export' || action === 'import') where.action = action; + + if (startAt || endAt) { + const range = {}; + if (startAt) { + const d = new Date(startAt); + if (!Number.isNaN(d.getTime())) range[Op.gte] = d; + } + if (endAt) { + const d = new Date(endAt); + if (!Number.isNaN(d.getTime())) range[Op.lte] = d; + } + if (Object.keys(range).length) where.create_at = range; + } + + const total = await ShareCodeLog.count({ where }); + + const orderDir = sortOrder === 'asc' ? 'ASC' : 'DESC'; + const orderCol = sortBy === 'create_at' ? 'create_at' : 'id'; + + const rows = await ShareCodeLog.findAll({ + where, + offset: skip, + limit, + order: [[orderCol, orderDir]], + }); + + logger.info(`Found ${rows.length} share_code_log rows, total=${total}`); + + if (!rows.length) { + return res.json(ApiResponse.noData('empty')); + } + + const items = rows.map((r) => ({ + id: r.id, + mac_addr: r.mac_addr, + share_code: r.share_code, + action: r.action, + ip_addr: r.ip_addr, + eq_data: r.eq_data, + expire_at: r.expire_at ? r.expire_at.toISOString() : null, + create_at: r.create_at ? r.create_at.toISOString() : null, + })); + + return res.json(ApiResponse.success({ items, total, skip, limit })); + } catch (e) { + logger.error(`Error getting share code logs: ${e.message}`); + return res.json(ApiResponse.error('error')); + } +}); + +module.exports = router; + diff --git a/frontend/index.html b/frontend/index.html index c517803..a172871 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -6,7 +6,7 @@ - + 耳机管理平台 diff --git a/frontend/src/components/TabsView.vue b/frontend/src/components/TabsView.vue new file mode 100644 index 0000000..a499adc --- /dev/null +++ b/frontend/src/components/TabsView.vue @@ -0,0 +1,351 @@ + + + + + + diff --git a/frontend/src/layout/index.vue b/frontend/src/layout/index.vue index 6211b92..a33aba8 100644 --- a/frontend/src/layout/index.vue +++ b/frontend/src/layout/index.vue @@ -7,47 +7,91 @@ - -