新增耳机阻抗

This commit is contained in:
eafonyang
2026-08-17 15:41:32 +08:00
parent 19ea48ffc8
commit acd3d4b92c
15 changed files with 733 additions and 0 deletions
@@ -0,0 +1,239 @@
import { Router, type Request, type Response, type Router as RouterType } from 'express';
import { eq, like, and, desc, count } from 'drizzle-orm';
import { db } from '../config/database.js';
import { userHeadphoneImpedances } from '../schemas/index.js';
import logger from '../config/logger.js';
import { ApiResponse } from '../utils/response.js';
import { authMiddleware } from '../middleware/auth.js';
import { HeadphoneImpedanceCreateSchema, HeadphoneImpedanceUpdateSchema } from '../validators/headphoneImpedance.js';
const router: RouterType = Router();
// 所有接口需登录
router.use('/api/headphone-impedance', authMiddleware);
function toDict(item: typeof userHeadphoneImpedances.$inferSelect) {
return {
id: item.id,
mac_addr: item.macAddr,
device_model: item.deviceModel,
impedance_ohm: item.impedanceOhm,
headphone_brand: item.headphoneBrand,
headphone_model: item.headphoneModel,
headphone_brand_norm: item.headphoneBrandNorm,
headphone_model_norm: item.headphoneModelNorm,
ip_addr: item.ipAddr,
create_at: item.createAt ? item.createAt.toISOString() : null,
update_at: item.updateAt ? item.updateAt.toISOString() : null,
};
}
/** norm 字段生成规则:原始值 trim + 小写(与设备端上报行为一致) */
function norm(value: string) {
return value.trim().toLowerCase();
}
// GET /api/headphone-impedance/ —— 分页列表(支持 mac/型号/品牌/耳机型号筛选)
router.get('/api/headphone-impedance/', async (req: Request, res: Response) => {
try {
const skip = parseInt(req.query.skip as string || '0', 10);
const limit = Math.min(parseInt(req.query.limit as string || '100', 10), 1000);
const macAddr = req.query.mac_addr as string | undefined;
const deviceModel = req.query.device_model as string | undefined;
const headphoneBrand = req.query.headphone_brand as string | undefined;
const headphoneModel = req.query.headphone_model as string | undefined;
const conditions = [];
if (macAddr) conditions.push(like(userHeadphoneImpedances.macAddr, `%${macAddr}%`));
if (deviceModel) conditions.push(eq(userHeadphoneImpedances.deviceModel, deviceModel));
// 品牌筛选对原始值与 norm 值分别模糊匹配(输入小写时也能命中大写原始值)
if (headphoneBrand) {
conditions.push(
like(userHeadphoneImpedances.headphoneBrand, `%${headphoneBrand}%`),
);
}
if (headphoneModel) {
conditions.push(
like(userHeadphoneImpedances.headphoneModel, `%${headphoneModel}%`),
);
}
const whereClause = conditions.length > 0 ? and(...conditions) : undefined;
const [totalResult] = await db
.select({ value: count() })
.from(userHeadphoneImpedances)
.where(whereClause);
const total = totalResult?.value ?? 0;
const rows = await db
.select()
.from(userHeadphoneImpedances)
.where(whereClause)
.orderBy(desc(userHeadphoneImpedances.id))
.offset(skip)
.limit(limit);
logger.info(`Found ${rows.length} headphone impedance records, total=${total}`);
if (!rows.length) {
res.json(ApiResponse.noData('empty'));
return;
}
res.json(ApiResponse.success({ items: rows.map(toDict), total, skip, limit }));
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
logger.error(`Error getting headphone impedance list: ${msg}`);
res.json(ApiResponse.error('error'));
}
});
// GET /api/headphone-impedance/:id
router.get('/api/headphone-impedance/:id', async (req: Request, res: Response) => {
try {
const id = parseInt(req.params.id as string, 10);
logger.info(`Getting headphone impedance: id=${id}`);
const [item] = await db
.select()
.from(userHeadphoneImpedances)
.where(eq(userHeadphoneImpedances.id, id))
.limit(1);
if (!item) {
logger.warn(`Headphone impedance not found: id=${id}`);
res.json(ApiResponse.noData('empty'));
return;
}
res.json(ApiResponse.success(toDict(item)));
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
logger.error(`Error getting headphone impedance: ${msg}`);
res.json(ApiResponse.error('error'));
}
});
// POST /api/headphone-impedance/
router.post('/api/headphone-impedance/', async (req: Request, res: Response) => {
try {
const parsed = HeadphoneImpedanceCreateSchema.safeParse(req.body);
if (!parsed.success) {
const errors = parsed.error.issues.map((e: { message: string }) => e.message).join('; ');
res.json(ApiResponse.error(errors));
return;
}
const data = parsed.data;
logger.info(`Creating headphone impedance: mac=${data.mac_addr}, model=${data.device_model}, ohm=${data.impedance_ohm}`);
const result = await db.insert(userHeadphoneImpedances).values({
macAddr: data.mac_addr.trim(),
deviceModel: data.device_model.trim(),
impedanceOhm: data.impedance_ohm,
headphoneBrand: data.headphone_brand.trim(),
headphoneModel: data.headphone_model.trim(),
headphoneBrandNorm: norm(data.headphone_brand),
headphoneModelNorm: norm(data.headphone_model),
ipAddr: data.ip_addr ?? '',
});
const [created] = await db
.select()
.from(userHeadphoneImpedances)
.where(eq(userHeadphoneImpedances.id, result[0].insertId))
.limit(1);
logger.info(`Headphone impedance created: id=${created.id}`);
res.json(ApiResponse.success(toDict(created)));
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
logger.error(`Error creating headphone impedance: ${msg}`);
res.json(ApiResponse.error('error'));
}
});
// PUT /api/headphone-impedance/:id
router.put('/api/headphone-impedance/:id', async (req: Request, res: Response) => {
try {
const id = parseInt(req.params.id as string, 10);
const parsed = HeadphoneImpedanceUpdateSchema.safeParse(req.body);
if (!parsed.success) {
const errors = parsed.error.issues.map((e: { message: string }) => e.message).join('; ');
res.json(ApiResponse.error(errors));
return;
}
const data = parsed.data;
logger.info(`Updating headphone impedance: id=${id}`);
const [item] = await db
.select()
.from(userHeadphoneImpedances)
.where(eq(userHeadphoneImpedances.id, id))
.limit(1);
if (!item) {
logger.warn(`Headphone impedance not found: id=${id}`);
res.json(ApiResponse.noData('empty'));
return;
}
const updateData: Partial<typeof userHeadphoneImpedances.$inferInsert> = {};
if (data.mac_addr !== undefined && data.mac_addr !== null) updateData.macAddr = data.mac_addr.trim();
if (data.device_model !== undefined && data.device_model !== null) updateData.deviceModel = data.device_model.trim();
if (data.impedance_ohm !== undefined && data.impedance_ohm !== null) updateData.impedanceOhm = data.impedance_ohm;
if (data.headphone_brand !== undefined && data.headphone_brand !== null) {
updateData.headphoneBrand = data.headphone_brand.trim();
updateData.headphoneBrandNorm = norm(data.headphone_brand);
}
if (data.headphone_model !== undefined && data.headphone_model !== null) {
updateData.headphoneModel = data.headphone_model.trim();
updateData.headphoneModelNorm = norm(data.headphone_model);
}
if (data.ip_addr !== undefined && data.ip_addr !== null) updateData.ipAddr = data.ip_addr;
if (Object.keys(updateData).length > 0) {
await db.update(userHeadphoneImpedances).set(updateData).where(eq(userHeadphoneImpedances.id, id));
}
logger.info(`Headphone impedance updated: id=${id}`);
const [updated] = await db
.select()
.from(userHeadphoneImpedances)
.where(eq(userHeadphoneImpedances.id, id))
.limit(1);
res.json(ApiResponse.success(toDict(updated)));
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
logger.error(`Error updating headphone impedance: ${msg}`);
res.json(ApiResponse.error('error'));
}
});
// DELETE /api/headphone-impedance/:id
router.delete('/api/headphone-impedance/:id', async (req: Request, res: Response) => {
try {
const id = parseInt(req.params.id as string, 10);
logger.info(`Deleting headphone impedance: id=${id}`);
const [item] = await db
.select()
.from(userHeadphoneImpedances)
.where(eq(userHeadphoneImpedances.id, id))
.limit(1);
if (!item) {
logger.warn(`Headphone impedance not found: id=${id}`);
res.json(ApiResponse.noData('empty'));
return;
}
await db.delete(userHeadphoneImpedances).where(eq(userHeadphoneImpedances.id, id));
logger.info(`Headphone impedance deleted: id=${id}`);
res.json(ApiResponse.success(null, 'success'));
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
logger.error(`Error deleting headphone impedance: ${msg}`);
res.json(ApiResponse.error('error'));
}
});
export default router;
+2
View File
@@ -6,6 +6,7 @@ import otaRouter from './ota.js';
import blacklistRouter from './blacklist.js';
import otaTargetDeviceRouter from './otaTargetDevice.js';
import shareCodeLogsRouter from './shareCodeLogs.js';
import headphoneImpedanceRouter from './headphoneImpedance.js';
import usersRouter from './users.js';
import dashboardRouter from './dashboard.js';
import siteRouter from './site.js';
@@ -35,6 +36,7 @@ const routes: Router[] = [
blacklistRouter,
otaTargetDeviceRouter,
shareCodeLogsRouter,
headphoneImpedanceRouter,
usersRouter,
dashboardRouter,
meilisearchRouter,
@@ -0,0 +1,22 @@
import { mysqlTable, int, varchar, datetime } from 'drizzle-orm/mysql-core';
import { sql } from 'drizzle-orm';
/**
* 用户耳机阻抗上报记录(表已存在于数据库,无需建表)
*
* 设备端上报:连接耳机后测得的阻抗 + 识别出的耳机品牌/型号。
* *_norm 字段为对应原始值的小写规范化形式,由写入方生成。
*/
export const userHeadphoneImpedances = mysqlTable('user_headphone_impedance', {
id: int('id').primaryKey().autoincrement(),
macAddr: varchar('mac_addr', { length: 255 }).notNull(),
deviceModel: varchar('device_model', { length: 50 }).notNull(),
impedanceOhm: int('impedance_ohm').notNull(),
headphoneBrand: varchar('headphone_brand', { length: 255 }).notNull(),
headphoneModel: varchar('headphone_model', { length: 255 }).notNull(),
headphoneBrandNorm: varchar('headphone_brand_norm', { length: 255 }).notNull(),
headphoneModelNorm: varchar('headphone_model_norm', { length: 255 }).notNull(),
ipAddr: varchar('ip_addr', { length: 100 }).notNull(),
createAt: datetime('create_at').notNull().default(sql`NOW()`),
updateAt: datetime('update_at').notNull().default(sql`NOW()`),
});
+1
View File
@@ -10,6 +10,7 @@ export { dashboardUsers } from './dashboardUser.js';
export { shareCodeLogs } from './shareCodeLog.js';
export { userActives } from './userActive.js';
export { userDevices } from './userDevice.js';
export { userHeadphoneImpedances } from './headphoneImpedance.js';
// 官网 CMS
export * from './www/index.js';
@@ -0,0 +1,32 @@
import { z } from 'zod';
export const HeadphoneImpedanceCreateSchema = z.object({
mac_addr: z.string().min(1, 'MAC 地址不能为空').max(255, 'MAC 地址最多 255 字符'),
device_model: z.string().min(1, '设备型号不能为空').max(50, '设备型号最多 50 字符'),
impedance_ohm: z
.number({ message: '阻抗须为数值' })
.int('阻抗须为整数')
.min(0, '阻抗不能为负')
.max(10000, '阻抗不能超过 10000'),
headphone_brand: z.string().min(1, '耳机品牌不能为空').max(255, '耳机品牌最多 255 字符'),
headphone_model: z.string().min(1, '耳机型号不能为空').max(255, '耳机型号最多 255 字符'),
ip_addr: z.string().max(100, 'IP 地址最多 100 字符').optional().default(''),
});
export const HeadphoneImpedanceUpdateSchema = z.object({
mac_addr: z.string().min(1).max(255).optional().nullable(),
device_model: z.string().min(1).max(50).optional().nullable(),
impedance_ohm: z
.number({ message: '阻抗须为数值' })
.int('阻抗须为整数')
.min(0, '阻抗不能为负')
.max(10000, '阻抗不能超过 10000')
.optional()
.nullable(),
headphone_brand: z.string().min(1).max(255).optional().nullable(),
headphone_model: z.string().min(1).max(255).optional().nullable(),
ip_addr: z.string().max(100).optional().nullable(),
});
export type HeadphoneImpedanceCreateInput = z.infer<typeof HeadphoneImpedanceCreateSchema>;
export type HeadphoneImpedanceUpdateInput = z.infer<typeof HeadphoneImpedanceUpdateSchema>;