refactor(api): 复用电池业务常量

This commit is contained in:
2026-05-11 23:38:37 +08:00
parent dc8a595d0a
commit 99d9cd1e1d
5 changed files with 113 additions and 71 deletions
+11 -3
View File
@@ -1,6 +1,12 @@
import { oc } from '@orpc/contract'
import { z } from 'zod'
import { batteriesResponseSchema, dashboardSnapshotSchema } from '@/domain/battery'
import {
BATTERY_LIST_SORT,
BATTERY_LIST_SORT_VALUES,
batteriesResponseSchema,
dashboardSnapshotSchema,
POWER_STATUS,
} from '@/domain/battery'
export const dashboard = oc.input(z.void()).output(dashboardSnapshotSchema)
@@ -12,8 +18,10 @@ const batteryListInputSchema = z.object({
z.string().min(1).max(100).optional(),
),
lowPower: z.boolean().optional(),
powerStatus: z.union([z.literal(0), z.literal(1), z.literal(2)]).optional(),
sort: z.enum(['createdAtDesc', 'createdAtAsc', 'powerDesc', 'powerAsc']).default('createdAtDesc'),
powerStatus: z
.union([z.literal(POWER_STATUS.NOT_CHARGING), z.literal(POWER_STATUS.CHARGING), z.literal(POWER_STATUS.FULL)])
.optional(),
sort: z.enum(BATTERY_LIST_SORT_VALUES).default(BATTERY_LIST_SORT.CREATED_AT_DESC),
})
export const batteries = oc.input(batteryListInputSchema).output(batteriesResponseSchema)
+32 -19
View File
@@ -1,6 +1,16 @@
import mysql, { type Pool, type RowDataPacket } from 'mysql2/promise'
import { type BatteryInfo, type BatteryInfoSourceRow, toBatteryInfo } from '@/domain/battery'
import {
BATTERY_LIST_SORT,
type BatteryInfo,
type BatteryInfoSourceRow,
type BatteryListSort,
MYSQL_BOOLEAN,
POWER_STATUS,
type PowerStatus,
toBatteryInfo,
toMysqlBoolean,
} from '@/domain/battery'
import { env } from '@/env'
const historyLimit = 500
@@ -14,14 +24,12 @@ type CountMysqlRow = RowDataPacket & {
charging: number | string | null
}
export type BatteryListSort = 'createdAtDesc' | 'createdAtAsc' | 'powerDesc' | 'powerAsc'
export type LatestBatteryPageInput = {
pageSize: number
cursor?: string
search?: string
lowPower?: boolean
powerStatus?: 0 | 1 | 2
powerStatus?: PowerStatus
sort?: BatteryListSort
}
@@ -99,10 +107,10 @@ const latestRecordPredicate = `
`
const orderByBySort: Record<BatteryListSort, string> = {
createdAtDesc: 'current_record.create_time DESC, current_record.id DESC',
createdAtAsc: 'current_record.create_time ASC, current_record.id ASC',
powerDesc: 'current_record.power DESC, current_record.create_time DESC, current_record.id DESC',
powerAsc: 'current_record.power ASC, current_record.create_time DESC, current_record.id DESC',
[BATTERY_LIST_SORT.CREATED_AT_DESC]: 'current_record.create_time DESC, current_record.id DESC',
[BATTERY_LIST_SORT.CREATED_AT_ASC]: 'current_record.create_time ASC, current_record.id ASC',
[BATTERY_LIST_SORT.POWER_DESC]: 'current_record.power DESC, current_record.create_time DESC, current_record.id DESC',
[BATTERY_LIST_SORT.POWER_ASC]: 'current_record.power ASC, current_record.create_time DESC, current_record.id DESC',
}
function toNumber(value: number | string | null | undefined) {
@@ -115,7 +123,7 @@ function encodeCursor(item: BatteryInfo, sort: BatteryListSort) {
sort,
createTime: item.createTime,
id: item.id,
power: sort === 'powerAsc' || sort === 'powerDesc' ? item.power : undefined,
power: sort === BATTERY_LIST_SORT.POWER_ASC || sort === BATTERY_LIST_SORT.POWER_DESC ? item.power : undefined,
}
return Buffer.from(JSON.stringify(cursor)).toString('base64url')
@@ -127,7 +135,12 @@ function decodeCursor(value: string | undefined, sort: BatteryListSort): PageCur
try {
const decoded = JSON.parse(Buffer.from(value, 'base64url').toString('utf8')) as Partial<PageCursor>
if (decoded.sort !== sort || typeof decoded.createTime !== 'string' || typeof decoded.id !== 'number') return null
if ((sort === 'powerAsc' || sort === 'powerDesc') && typeof decoded.power !== 'number') return null
if (
(sort === BATTERY_LIST_SORT.POWER_ASC || sort === BATTERY_LIST_SORT.POWER_DESC) &&
typeof decoded.power !== 'number'
) {
return null
}
return decoded as PageCursor
} catch {
@@ -156,7 +169,7 @@ function createLatestWhere(input: LatestBatteryPageInput, cursor: PageCursor | n
if (input.lowPower !== undefined) {
clauses.push('current_record.is_low_power = :lowPower')
params.lowPower = input.lowPower ? 'true' : 'false'
params.lowPower = toMysqlBoolean(input.lowPower)
}
if (input.powerStatus !== undefined) {
@@ -168,25 +181,25 @@ function createLatestWhere(input: LatestBatteryPageInput, cursor: PageCursor | n
params.cursorCreateTime = normalizeCursorDateTime(cursor.createTime)
params.cursorId = cursor.id
switch (input.sort ?? 'createdAtDesc') {
case 'createdAtAsc':
switch (input.sort ?? BATTERY_LIST_SORT.CREATED_AT_DESC) {
case BATTERY_LIST_SORT.CREATED_AT_ASC:
clauses.push(
'(current_record.create_time > :cursorCreateTime OR (current_record.create_time = :cursorCreateTime AND current_record.id > :cursorId))',
)
break
case 'powerDesc':
case BATTERY_LIST_SORT.POWER_DESC:
params.cursorPower = cursor.power ?? 0
clauses.push(
'(current_record.power < :cursorPower OR (current_record.power = :cursorPower AND (current_record.create_time < :cursorCreateTime OR (current_record.create_time = :cursorCreateTime AND current_record.id < :cursorId))))',
)
break
case 'powerAsc':
case BATTERY_LIST_SORT.POWER_ASC:
params.cursorPower = cursor.power ?? 0
clauses.push(
'(current_record.power > :cursorPower OR (current_record.power = :cursorPower AND (current_record.create_time < :cursorCreateTime OR (current_record.create_time = :cursorCreateTime AND current_record.id < :cursorId))))',
)
break
case 'createdAtDesc':
case BATTERY_LIST_SORT.CREATED_AT_DESC:
clauses.push(
'(current_record.create_time < :cursorCreateTime OR (current_record.create_time = :cursorCreateTime AND current_record.id < :cursorId))',
)
@@ -257,7 +270,7 @@ export async function getBatteryPredictionHistories(macAddresses: string[]): Pro
}
export async function getLatestBatteryPage(input: LatestBatteryPageInput): Promise<LatestBatteryPage> {
const sort = input.sort ?? 'createdAtDesc'
const sort = input.sort ?? BATTERY_LIST_SORT.CREATED_AT_DESC
const pageSize = Math.min(Math.max(input.pageSize, 1), 100)
const cursor = decodeCursor(input.cursor, sort)
const { whereSql, params } = createLatestWhere({ ...input, sort, pageSize }, cursor)
@@ -283,8 +296,8 @@ export async function getLatestBatteryPage(input: LatestBatteryPageInput): Promi
`
SELECT
COUNT(*) AS total,
COALESCE(SUM(CASE WHEN current_record.is_low_power = 'true' THEN 1 ELSE 0 END), 0) AS lowPower,
COALESCE(SUM(CASE WHEN current_record.power_status = 1 THEN 1 ELSE 0 END), 0) AS charging
COALESCE(SUM(CASE WHEN current_record.is_low_power = '${MYSQL_BOOLEAN.TRUE}' THEN 1 ELSE 0 END), 0) AS lowPower,
COALESCE(SUM(CASE WHEN current_record.power_status = ${POWER_STATUS.CHARGING} THEN 1 ELSE 0 END), 0) AS charging
FROM ls_battery_info AS current_record
WHERE ${countWhere.whereSql}
`,
+11 -9
View File
@@ -1,6 +1,12 @@
import { LRUCache } from 'lru-cache'
import { z } from 'zod'
import type { BatteryInfo, BatteryPrediction } from '@/domain/battery'
import {
type BatteryInfo,
type BatteryPrediction,
POWER_STATUS,
type PowerStatus,
toMysqlBoolean,
} from '@/domain/battery'
import { env } from '@/env'
import { getLogger } from '@/server/logger'
@@ -39,7 +45,7 @@ type PredictionRequest = {
dev_model: string
dev_name: string
is_low_power: string
power_status: 0 | 1 | 2
power_status: PowerStatus
power: number
create_time: string
remark: string
@@ -98,7 +104,7 @@ function createHistoryItem(item: BatteryInfo, index: number): PredictionHistoryI
discharge_capacity_ah: dischargeCapacity,
charge_energy_wh: chargeEnergy,
discharge_energy_wh: dischargeEnergy,
charge_time: item.powerStatus === 1 ? '01:20:00' : '01:18:00',
charge_time: item.powerStatus === POWER_STATUS.CHARGING ? '01:20:00' : '01:18:00',
discharge_time: item.isLowPower ? '00:58:00' : '01:10:00',
coulombic_efficiency_pct: efficiency,
timestamp: normalizeMysqlDateTime(item.createTime),
@@ -117,7 +123,7 @@ export function createPredictionRequest(battery: BatteryInfo, history: BatteryIn
mac: battery.mac,
dev_model: battery.devModel,
dev_name: battery.devName,
is_low_power: battery.isLowPower ? 'true' : 'false',
is_low_power: toMysqlBoolean(battery.isLowPower),
power_status: battery.powerStatus,
power: battery.power,
create_time: normalizeMysqlDateTime(battery.createTime),
@@ -144,12 +150,10 @@ function normalizePrediction(response: z.infer<typeof predictionResponseSchema>)
}
export function isPredictionEnabled() {
return Boolean(env.SOH_PREDICTION_API_BASE_URL)
return true
}
export async function predictSoh(battery: BatteryInfo, history: BatteryInfo[]): Promise<SohPrediction | null> {
if (!env.SOH_PREDICTION_API_BASE_URL) return null
const request = createPredictionRequest(battery, history)
if (!request) return null
@@ -170,8 +174,6 @@ async function requestPrediction(
battery: BatteryInfo,
request: PredictionRequest,
): Promise<SohPrediction | null> {
if (!env.SOH_PREDICTION_API_BASE_URL) return null
const controller = new AbortController()
const timeout = setTimeout(() => controller.abort(), env.SOH_PREDICTION_TIMEOUT_MS)
const baseUrl = env.SOH_PREDICTION_API_BASE_URL.endsWith('/')