feat: 优化架构
Some checks failed
Frontend CI / build-and-push-web (push) Successful in 2m39s
Frontend CI / ota-android (push) Successful in 10m22s
Frontend CI / build-android-apk (push) Has been cancelled

This commit is contained in:
lafay
2026-04-13 01:30:37 +08:00
parent 2adc9360a5
commit fe6a03da5d
93 changed files with 386 additions and 211 deletions

View File

@@ -0,0 +1,290 @@
/**
* APK 更新检查服务
* 用于检查最新版本并提示用户下载更新
*/
import { Platform, Linking, Alert } from 'react-native';
import Constants from 'expo-constants';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { showConfirm } from '../ui/dialogService';
// 条件导入原生模块(仅在 Android 上可用)
// eslint-disable-next-line @typescript-eslint/no-var-requires
const FileSystem = Platform.OS === 'android' ? require('expo-file-system/legacy') : null;
// eslint-disable-next-line @typescript-eslint/no-var-requires
const IntentLauncher = Platform.OS === 'android' ? require('expo-intent-launcher') : null;
// 更新服务器地址
const UPDATES_SERVER_URL = Constants.expoConfig?.extra?.updatesUrl
? Constants.expoConfig.extra.updatesUrl.replace('/api/manifest', '')
: 'https://updates.littlelan.cn';
// 检查间隔毫秒24小时
const CHECK_INTERVAL = 24 * 60 * 60 * 1000;
// 最后检查时间存储键
const LAST_CHECK_KEY = 'apk_update_last_check';
// APK 版本信息
export interface APKVersionInfo {
runtimeVersion: string;
versionName: string;
size: number;
downloadUrl: string;
createdAt: string;
}
// 版本比较结果
export interface VersionCheckResult {
hasUpdate: boolean;
currentVersion: string;
latestVersion: string;
downloadUrl: string;
size: number;
}
/**
* 比较版本号
* 返回: 1 表示 v1 > v2, -1 表示 v1 < v2, 0 表示相等
*/
function compareVersions(v1: string, v2: string): number {
const parts1 = v1.split('.').map(Number);
const parts2 = v2.split('.').map(Number);
for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
const p1 = parts1[i] || 0;
const p2 = parts2[i] || 0;
if (p1 > p2) return 1;
if (p1 < p2) return -1;
}
return 0;
}
/**
* 获取最新 APK 版本信息
*/
async function fetchLatestAPKVersion(): Promise<APKVersionInfo | null> {
try {
const response = await fetch(`${UPDATES_SERVER_URL}/api/apk/latest`, {
method: 'GET',
headers: {
'Accept': 'application/json',
},
});
if (!response.ok) {
console.warn('Failed to fetch latest APK version:', response.status);
return null;
}
const data: APKVersionInfo = await response.json();
return data;
} catch (error) {
console.error('Error fetching latest APK version:', error);
return null;
}
}
/**
* 获取当前应用版本
*/
function getCurrentVersion(): string {
return Constants.expoConfig?.version || '1.0.0';
}
/**
* 格式化文件大小
*/
function formatFileSize(bytes: number): string {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
}
/**
* 检查是否应该执行检查(避免频繁检查)
*/
async function shouldCheck(): Promise<boolean> {
try {
const lastCheck = await AsyncStorage.getItem(LAST_CHECK_KEY);
if (!lastCheck) return true;
const lastCheckTime = parseInt(lastCheck, 10);
return Date.now() - lastCheckTime > CHECK_INTERVAL;
} catch {
return true;
}
}
/**
* 记录检查时间
*/
async function recordCheckTime(): Promise<void> {
try {
await AsyncStorage.setItem(LAST_CHECK_KEY, Date.now().toString());
} catch (error) {
console.error('Failed to record check time:', error);
}
}
/**
* 下载并安装 APK
*/
async function downloadAndInstallAPK(downloadUrl: string, version: string): Promise<void> {
if (Platform.OS !== 'android') {
Alert.alert('提示', '此功能仅支持 Android 设备');
return;
}
if (!FileSystem) {
Alert.alert('提示', '当前环境不支持 APK 更新,请在浏览器中下载');
Linking.openURL(downloadUrl);
return;
}
try {
const downloadPath = `${FileSystem.documentDirectory}carrot-bbs-${version}.apk`;
// 显示下载进度
showConfirm({
title: '正在下载',
message: `正在下载新版本 ${version},请稍候...`,
confirmText: '后台下载',
cancelText: '取消',
onConfirm: () => {
// 用户选择后台下载,继续
},
onCancel: () => {
// 用户取消
},
});
// 下载 APK
const downloadResult = await FileSystem.downloadAsync(downloadUrl, downloadPath);
if (!downloadResult.uri) {
Alert.alert('下载失败', '无法下载安装包,请稍后重试');
return;
}
// 安装 APK
await installAPK(downloadResult.uri);
} catch (error) {
console.error('Download/Install error:', error);
Alert.alert('更新失败', '下载或安装过程中出错,请稍后重试');
}
}
/**
* 安装 APK
*/
async function installAPK(apkUri: string): Promise<void> {
try {
if (!FileSystem || !IntentLauncher) {
throw new Error('Native modules not available');
}
// 获取文件的 content URI
const contentUri = await FileSystem.getContentUriAsync(apkUri);
// 使用 Intent 安装 APK
await IntentLauncher.startActivityAsync('android.intent.action.INSTALL_PACKAGE', {
data: contentUri,
flags: 1, // FLAG_GRANT_READ_URI_PERMISSION
});
} catch (error) {
console.error('Install APK error:', error);
// 尝试使用浏览器下载
Linking.openURL(apkUri.replace('file://', ''));
}
}
/**
* 显示更新提示对话框
*/
function showUpdateDialog(result: VersionCheckResult): void {
const sizeText = formatFileSize(result.size);
showConfirm({
title: '发现新版本',
message: `当前版本: ${result.currentVersion}\n最新版本: ${result.latestVersion}\n安装包大小: ${sizeText}\n\n是否立即下载更新`,
confirmText: '立即更新',
cancelText: '稍后提醒',
onConfirm: () => {
downloadAndInstallAPK(result.downloadUrl, result.latestVersion);
},
});
}
/**
* 检查 APK 更新(主入口)
* @param force 是否强制检查(忽略时间间隔)
*/
export async function checkForAPKUpdate(force: boolean = false): Promise<VersionCheckResult | null> {
// 仅在 Android 平台检查
if (Platform.OS !== 'android') {
return null;
}
// 检查是否应该执行检查
if (!force) {
const should = await shouldCheck();
if (!should) {
console.log('APK update check skipped (too frequent)');
return null;
}
}
// 记录检查时间
await recordCheckTime();
// 获取最新版本信息
const latestAPK = await fetchLatestAPKVersion();
if (!latestAPK) {
console.log('No APK version info available');
return null;
}
const currentVersion = getCurrentVersion();
const latestVersion = latestAPK.versionName || latestAPK.runtimeVersion;
// 比较版本
const comparison = compareVersions(currentVersion, latestVersion);
const result: VersionCheckResult = {
hasUpdate: comparison < 0,
currentVersion,
latestVersion,
downloadUrl: latestAPK.downloadUrl,
size: latestAPK.size,
};
// 如果有更新,显示对话框
if (result.hasUpdate) {
showUpdateDialog(result);
}
return result;
}
/**
* 手动检查更新(用户主动触发)
*/
export async function manualCheckForUpdate(): Promise<void> {
const result = await checkForAPKUpdate(true);
if (!result) {
Alert.alert('检查失败', '无法获取版本信息,请检查网络连接');
return;
}
if (!result.hasUpdate) {
Alert.alert('已是最新', `当前版本 ${result.currentVersion} 已是最新版本`);
}
}
export const apkUpdateService = {
checkForAPKUpdate,
manualCheckForUpdate,
};

View File

@@ -0,0 +1,13 @@
export { apkUpdateService, checkForAPKUpdate, manualCheckForUpdate } from './apkUpdateService';
export type { APKVersionInfo, VersionCheckResult } from './apkUpdateService';
export { scheduleService } from './scheduleService';
export type { CreateScheduleCourseRequest, SyncScheduleRequest } from './scheduleService';
export {
triggerVibration,
vibrateOnMessage,
setVibrationConfig,
getVibrationConfig,
setVibrationEnabled,
} from './messageVibrationService';

View File

@@ -0,0 +1,88 @@
import * as Haptics from 'expo-haptics';
interface VibrationConfig {
enabled: boolean;
onChatMessage: boolean;
onGroupMessage: boolean;
onNotification: boolean;
style: Haptics.ImpactFeedbackStyle;
}
const defaultVibrationConfig: VibrationConfig = {
enabled: true,
onChatMessage: true,
onGroupMessage: true,
onNotification: true,
style: Haptics.ImpactFeedbackStyle.Light,
};
let vibrationConfig: VibrationConfig = { ...defaultVibrationConfig };
export async function triggerVibration(
type: 'light' | 'medium' | 'heavy' | 'success' | 'warning' | 'error' = 'light'
): Promise<void> {
if (!vibrationConfig.enabled) {
return;
}
try {
switch (type) {
case 'light':
await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
break;
case 'medium':
await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
break;
case 'heavy':
await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Heavy);
break;
case 'success':
await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
break;
case 'warning':
await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Warning);
break;
case 'error':
await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
break;
}
} catch (error) {
console.error('[VibrationService] 震动反馈失败:', error);
}
}
export async function vibrateOnMessage(messageType: 'chat' | 'group_message' | 'notification'): Promise<void> {
if (!vibrationConfig.enabled) {
return;
}
switch (messageType) {
case 'chat':
if (vibrationConfig.onChatMessage) {
await triggerVibration('light');
}
break;
case 'group_message':
if (vibrationConfig.onGroupMessage) {
await triggerVibration('light');
}
break;
case 'notification':
if (vibrationConfig.onNotification) {
await triggerVibration('medium');
}
break;
}
}
export function setVibrationConfig(config: Partial<VibrationConfig>): void {
vibrationConfig = { ...vibrationConfig, ...config };
}
export function getVibrationConfig(): VibrationConfig {
return { ...vibrationConfig };
}
export function setVibrationEnabled(enabled: boolean): void {
vibrationConfig.enabled = enabled;
}

View File

@@ -0,0 +1,88 @@
import { api } from '../core/api';
import { Course } from '@/types/schedule';
interface ScheduleCourseDTO {
id: string;
name: string;
teacher?: string;
location?: string;
day_of_week: number;
start_section: number;
end_section: number;
weeks: number[];
color?: string;
}
interface ScheduleCourseListResponse {
list: ScheduleCourseDTO[];
}
interface CreateScheduleCourseRequest {
name: string;
teacher?: string;
location?: string;
day_of_week: number;
start_section: number;
end_section: number;
weeks: number[];
color?: string;
}
interface CreateScheduleCourseResponse {
course: ScheduleCourseDTO;
}
interface SyncScheduleRequest {
username: string;
password: string;
semester?: string;
}
interface SyncScheduleResponse {
success: boolean;
message: string;
synced_count: number;
error_message?: string;
}
const toCourse = (dto: ScheduleCourseDTO): Course => ({
id: dto.id,
name: dto.name,
teacher: dto.teacher,
location: dto.location,
dayOfWeek: dto.day_of_week,
startSection: dto.start_section,
endSection: dto.end_section,
weeks: dto.weeks ?? [],
color: dto.color,
});
class ScheduleService {
async getCourses(week?: number): Promise<Course[]> {
const params = week ? { week } : undefined;
const response = await api.get<ScheduleCourseListResponse>('/schedule/courses', params);
return response.data.list.map(toCourse);
}
async createCourse(input: CreateScheduleCourseRequest): Promise<Course> {
const response = await api.post<CreateScheduleCourseResponse>('/schedule/courses', input);
return toCourse(response.data.course);
}
async updateCourse(courseId: string, input: CreateScheduleCourseRequest): Promise<Course> {
const response = await api.put<CreateScheduleCourseResponse>(`/schedule/courses/${courseId}`, input);
return toCourse(response.data.course);
}
async deleteCourse(courseId: string): Promise<void> {
await api.delete(`/schedule/courses/${courseId}`);
}
async syncSchedule(req: SyncScheduleRequest): Promise<SyncScheduleResponse> {
const response = await api.post<SyncScheduleResponse>('/schedule/sync', req);
return response.data;
}
}
export const scheduleService = new ScheduleService();
export type { CreateScheduleCourseRequest, SyncScheduleRequest };