export default {
async fetch(request, env) {
const url = new URL(request.url)
const pathname = url.pathname
const method = request.method.toUpperCase()
if (method === 'OPTIONS') {
return createJsonResponse({ success: true }, 200)
}
if (method === 'GET' && pathname === '/health') {
return createJsonResponse({ success: true, message: 'usage api ok' }, 200)
}
if (method === 'POST' && pathname === '/v1/admin/init') {
return await handleInitDatabase(env)
}
if (method === 'POST' && pathname === '/v1/device/register') {
return await handleRegisterDevice(request, env)
}
if (method === 'POST' && pathname === '/v1/usage/batch') {
return await handleUsageBatch(request, env)
}
if (method === 'GET' && pathname === '/v1/usage/export') {
return await handleExportUsage(url, env)
}
return createJsonResponse({ success: false, message: 'Not Found' }, 404)
}
}
function createJsonResponse(data, status = 200) {
return new Response(JSON.stringify(data, null, 2), {
status,
headers: {
'content-type': 'application/json; charset=utf-8',
'access-control-allow-origin': '*',
'access-control-allow-methods': 'GET,POST,OPTIONS',
'access-control-allow-headers': 'content-type,authorization'
}
})
}
async function readJsonBody(request) {
return await request.json()
}
async function handleInitDatabase(env) {
const sql = ` // 定义建表 SQL
CREATE TABLE IF NOT EXISTS device_info (
id INTEGER PRIMARY KEY AUTOINCREMENT,
device_id TEXT NOT NULL UNIQUE,
brand TEXT,
manufacturer TEXT,
model TEXT,
token TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS usage_session (
id INTEGER PRIMARY KEY AUTOINCREMENT,
event_id TEXT NOT NULL UNIQUE,
device_id TEXT NOT NULL,
package_name TEXT NOT NULL,
app_name TEXT,
start_time TEXT NOT NULL,
end_time TEXT NOT NULL,
duration_sec INTEGER NOT NULL,
source TEXT,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_usage_device_id ON usage_session(device_id);
CREATE INDEX IF NOT EXISTS idx_usage_start_time ON usage_session(start_time);
`
await env.DB.exec(sql)
return createJsonResponse({ success: true, message: 'database initialized' }, 200)
}
async function handleRegisterDevice(request, env) {
try {
const body = await readJsonBody(request)
const deviceId = String(body.deviceId || '').trim()
const brand = String(body.brand || '').trim()
const manufacturer = String(body.manufacturer || '').trim()
const model = String(body.model || '').trim()
const token = String(body.token || '').trim()
if (!deviceId || !token) {
return createJsonResponse({ success: false, message: 'deviceId and token are required' }, 400)
}
await env.DB.prepare(
`INSERT INTO device_info (device_id, brand, manufacturer, model, token)
VALUES (?, ?, ?, ?, ?)
ON CONFLICT(device_id) DO UPDATE SET
brand = excluded.brand,
manufacturer = excluded.manufacturer,
model = excluded.model,
token = excluded.token`
).bind(deviceId, brand, manufacturer, model, token).run()
return createJsonResponse({ success: true, deviceId, message: 'device registered' }, 200)
} catch (error) {
return createJsonResponse({ success: false, message: String(error) }, 500)
}
}
async function handleUsageBatch(request, env) {
try {
const body = await readJsonBody(request)
const deviceId = String(body.deviceId || '').trim()
const token = String(body.token || '').trim()
const items = Array.isArray(body.items) ? body.items : []
if (!deviceId || !token) {
return createJsonResponse({ success: false, message: 'deviceId and token are required' }, 400)
}
const deviceRow = await env.DB.prepare(
`SELECT device_id, token FROM device_info WHERE device_id = ?`
).bind(deviceId).first()
if (!deviceRow) {
return createJsonResponse({ success: false, message: 'device not registered' }, 403)
}
if (deviceRow.token !== token) {
return createJsonResponse({ success: false, message: 'invalid token' }, 403)
}
let accepted = 0
let duplicated = 0
for (const item of items) {
const eventId = String(item.eventId || '').trim()
const packageName = String(item.packageName || '').trim()
const appName = String(item.appName || '').trim()
const startTime = String(item.startTime || '').trim()
const endTime = String(item.endTime || '').trim()
const durationSec = Number(item.durationSec || 0)
const source = String(item.source || 'UsageEvents').trim()
if (!eventId || !packageName || !startTime || !endTime || durationSec <= 0) {
continue
}
const result = await env.DB.prepare(
`INSERT OR IGNORE INTO usage_session
(event_id, device_id, package_name, app_name, start_time, end_time, duration_sec, source)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
).bind(eventId, deviceId, packageName, appName, startTime, endTime, durationSec, source).run()
if ((result.meta?.changes || 0) > 0) {
accepted += 1
} else {
duplicated += 1
}
}
return createJsonResponse({
success: true,
accepted,
duplicated,
serverTime: new Date().toISOString()
}, 200)
} catch (error) {
return createJsonResponse({ success: false, message: String(error) }, 500)
}
}
async function handleExportUsage(url, env) {
try {
const deviceId = String(url.searchParams.get('deviceId') || '').trim()
const date = String(url.searchParams.get('date') || '').trim()
const from = String(url.searchParams.get('from') || '').trim()
const to = String(url.searchParams.get('to') || '').trim()
if (!deviceId) {
return createJsonResponse({ success: false, message: 'deviceId is required' }, 400)
}
let sql = ''
let params = []
if (date) {
sql = `SELECT event_id, package_name, app_name, start_time, end_time, duration_sec, source
FROM usage_session
WHERE device_id = ?
AND substr(start_time, 1, 10) = ?
ORDER BY start_time ASC`
params = [deviceId, date]
} else if (from && to) {
sql = `SELECT event_id, package_name, app_name, start_time, end_time, duration_sec, source
FROM usage_session
WHERE device_id = ?
AND substr(start_time, 1, 10) >= ?
AND substr(start_time, 1, 10) <= ?
ORDER BY start_time ASC`
params = [deviceId, from, to]
} else {
return createJsonResponse({ success: false, message: 'date or from/to is required' }, 400)
}
const result = await env.DB.prepare(sql).bind(...params).all()
const items = Array.isArray(result.results) ? result.results : []
return createJsonResponse({
success: true,
deviceId,
count: items.length,
items
}, 200)
} catch (error) {
return createJsonResponse({ success: false, message: String(error) }, 500)
}
}