feat: 重构为前后端分离架构并完善设备端演示

- 新增 Web 用户端(登录体系 + 统一 REST API 调用)
- 后端增加用户认证、统一 ApiResponse、CORS 支持
- Android 设备端迁移至 MVVM + DataBinding + Retrofit 网络层
- 完善 README 架构说明与密码学原理文档
- 新增 .gitignore 与持久化数据表说明
This commit is contained in:
TongTongStudio
2026-08-21 04:34:58 +08:00
parent eda03d241d
commit 93499fa189
63 changed files with 4398 additions and 455 deletions

253
web-client/app.js Normal file
View File

@@ -0,0 +1,253 @@
// ============================================================
// Secure Device 用户端Web—— 前后端分离演示
//
// 后端地址:默认 localhost:8080CORS 已放开)。
// 认证:登录/注册后获得 Bearer Token存 localStorage
// 后续所有用户端接口自动携带 Authorization: Bearer <token>。
// ============================================================
const API_BASE = 'http://localhost:8080';
// ---------- 状态 ----------
let token = localStorage.getItem('sd_token') || '';
let userId = localStorage.getItem('sd_userId') || '';
// ---------- DOM 引用 ----------
const $ = (id) => document.getElementById(id);
const sections = {
auth: $('authSection'),
device: $('deviceSection'),
recover: $('recoverSection'),
photo: $('photoSection'),
};
// 登录后才需要显示的面板(登录区 auth 始终显示,不在其中)
const authedSections = {
device: $('deviceSection'),
recover: $('recoverSection'),
photo: $('photoSection'),
};
// ---------- 通用请求 ----------
async function api(path, { method = 'GET', body = null } = {}) {
const headers = { 'Content-Type': 'application/json' };
if (token) headers['Authorization'] = 'Bearer ' + token;
const resp = await fetch(API_BASE + path, {
method,
headers,
body: body ? JSON.stringify(body) : null,
});
// 统一响应格式 { code, message, data },即使 HTTP 4xx/5xx 也是该结构
const json = await resp.json().catch(() => null);
showRaw(method, path, resp.status, json);
if (!resp.ok || !json || json.code !== 0) {
throw new Error((json && json.message) || `HTTP ${resp.status}`);
}
return json.data;
}
// ---------- 原始响应展示 ----------
function showRaw(method, path, status, json) {
$('rawOutput').textContent =
`HTTP ${status} ${method} ${API_BASE}${path}\n` +
JSON.stringify(json, null, 2);
}
// ---------- 结果展示 ----------
function showResult(elId, text, isErr = false) {
const el = $(elId);
el.textContent = text;
el.className = 'result ' + (isErr ? 'err' : 'ok');
}
// ---------- 登录态 UI ----------
function refreshAuthUI() {
const loggedIn = !!token;
$('userInfo').classList.toggle('hidden', !loggedIn);
$('userLabel').textContent = loggedIn ? `当前用户:${userId}` : '';
// 登录区始终显示;仅「设备/恢复/照片」三个面板在登录后才显示
$('authSection').classList.toggle('hidden', false);
Object.values(authedSections).forEach((s) => s.classList.toggle('hidden', !loggedIn));
if (!loggedIn) $('authResult').textContent = '';
}
function login(user, pass) {
token = user;
userId = user;
localStorage.setItem('sd_token', token);
localStorage.setItem('sd_userId', userId);
refreshAuthUI();
}
function logout() {
token = '';
userId = '';
localStorage.removeItem('sd_token');
localStorage.removeItem('sd_userId');
refreshAuthUI();
}
// ============================================================
// 事件绑定
// ============================================================
// 登录
$('btnLogin').addEventListener('click', async () => {
const uid = $('authUserId').value.trim();
const pass = $('authPassword').value || '123456';
if (!uid) return showResult('authResult', '请输入用户名', true);
try {
const data = await api('/api/auth/login', {
method: 'POST',
body: { userId: uid, password: pass },
});
login(data.userId, data.token);
showResult('authResult', `登录成功Token: ${data.token.slice(0, 16)}...`);
} catch (e) {
showResult('authResult', `登录失败:${e.message}`, true);
}
});
// 注册
$('btnRegister').addEventListener('click', async () => {
const uid = $('authUserId').value.trim();
const pass = $('authPassword').value || '123456';
const phone = $('authPhone').value.trim() || '13800138000';
if (!uid) return showResult('authResult', '请输入用户名', true);
try {
const data = await api('/api/auth/register', {
method: 'POST',
body: { userId: uid, phone, password: pass },
});
login(data.userId, data.token);
showResult('authResult', `注册成功并已登录Token: ${data.token.slice(0, 16)}...`);
} catch (e) {
showResult('authResult', `注册失败:${e.message}`, true);
}
});
// 退出登录
$('btnLogout').addEventListener('click', () => {
logout();
showResult('authResult', '已退出登录');
});
// 绑定设备
$('btnBind').addEventListener('click', async () => {
const sn = $('bindSn').value.trim();
if (!sn) return showResult('bindResult', '请输入设备 SN', true);
try {
const data = await api('/api/device/bind', {
method: 'POST',
body: { sn },
});
showResult('bindResult', data.message || '绑定成功');
} catch (e) {
showResult('bindResult', `绑定失败:${e.message}`, true);
}
});
// 设备列表
$('btnListDevices').addEventListener('click', async () => {
try {
const data = await api('/api/user/devices');
const devices = data.devices || [];
if (devices.length === 0) {
showResult('devicesResult', '暂无绑定设备');
return;
}
const lines = devices.map(
(d) => `${d.sn} | deviceId: ${d.deviceId} | 状态: ${d.active ? '启用' : '停用'}`
);
showResult('devicesResult', lines.join('\n'));
} catch (e) {
showResult('devicesResult', `获取设备列表失败:${e.message}`, true);
}
});
// 发送短信验证码
$('btnSendSms').addEventListener('click', async () => {
const phone = $('recoverPhone').value.trim() || '13800138000';
try {
const data = await api('/api/device/sms/send', {
method: 'POST',
body: { phone },
});
showResult('recoverResult', data.message || '验证码已发送demo 固定 000000');
} catch (e) {
showResult('recoverResult', `发送失败:${e.message}`, true);
}
});
// 恢复授权
$('btnRecover').addEventListener('click', async () => {
const sn = $('recoverSn').value.trim();
const newPubKey = $('recoverNewPubKey').value.trim();
const smsCode = $('recoverSmsCode').value.trim();
if (!sn) return showResult('recoverResult', '请输入设备 SN', true);
if (!newPubKey) return showResult('recoverResult', '请输入新设备公钥(从设备端复制)', true);
if (!smsCode) return showResult('recoverResult', '请输入短信验证码', true);
try {
const data = await api('/api/device/recover', {
method: 'POST',
body: { sn, smsCode, newPublicKeyBase64: newPubKey },
});
showResult(
'recoverResult',
`恢复授权成功deviceId: ${data.deviceId}\n` +
`encryptedRecoveryToken: ${data.encryptedRecoveryToken.slice(0, 32)}...\n` +
`(设备端可凭此 Token 取回照片 DEK`
);
} catch (e) {
showResult('recoverResult', `恢复授权失败:${e.message}`, true);
}
});
// 照片列表
$('btnListPhotos').addEventListener('click', async () => {
try {
const data = await api('/api/user/photos');
const photos = data.photos || [];
if (photos.length === 0) {
showResult('photosResult', '暂无照片(请先在设备端上传)');
return;
}
showResult('photosResult', photos.map((p) => `${p}`).join('\n'));
} catch (e) {
showResult('photosResult', `获取照片列表失败:${e.message}`, true);
}
});
// 下载解密照片
$('btnDecrypt').addEventListener('click', async () => {
const photoId = $('decryptPhotoId').value.trim();
if (!photoId) return showResult('decryptResult', '请输入 photoId', true);
try {
const data = await api(`/api/photo/${photoId}/decrypt`);
const plaintext = decodeBase64Text(data.plaintextBase64);
showResult(
'decryptResult',
`解密成功photoId: ${data.photoId}\n明文内容:\n${plaintext}`
);
} catch (e) {
showResult('decryptResult', `解密失败:${e.message}`, true);
}
});
// ---------- 工具 ----------
function decodeBase64Text(b64) {
try {
const bin = atob(b64);
const bytes = new Uint8Array(bin.length);
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
return new TextDecoder('utf-8').decode(bytes);
} catch {
return '<无法解码为文本,可能为二进制照片数据>';
}
}
// ---------- 初始化 ----------
refreshAuthUI();