feat(mp): AES-256-GCM 加密存储 + 安全日志 + ErrorBoundary 升级 + BLE 并发修复

- secure-storage-aes: AES-256-GCM 替代 XOR,保留 XOR 迁移读取
- crypto-polyfill: wx.getRandomValuesSync → crypto.getRandomValues
- logger.ts: dev/prod 区分日志级别,生产不输出详情
- ErrorBoundary: 错误分类(network/render/unknown) + 结构化日志
- DataSyncScheduler: isSyncing 互斥防并发重复同步
- app.tsx 首行导入 crypto-polyfill
This commit is contained in:
iven
2026-05-22 00:13:37 +08:00
parent 29543ef0e7
commit 21f8040994
7 changed files with 329 additions and 115 deletions

View File

@@ -1,31 +1,77 @@
import React, { Component } from 'react';
import { Component } from 'react';
import { View, Text } from '@tarojs/components';
import './index.scss';
interface Props {
children: React.ReactNode;
fallback?: React.ReactNode;
}
interface State {
hasError: boolean;
retryCount: number;
errorCategory: ErrorCategory;
}
type ErrorCategory = 'network' | 'render' | 'unknown';
const MAX_RETRIES = 3;
function classifyError(error: Error): ErrorCategory {
const msg = error.message?.toLowerCase() || '';
if (
msg.includes('network') ||
msg.includes('fetch') ||
msg.includes('timeout') ||
msg.includes('request:fail')
) {
return 'network';
}
if (
msg.includes('cannot read properties') ||
msg.includes('is not defined') ||
msg.includes('is not a function') ||
msg.includes('render')
) {
return 'render';
}
return 'unknown';
}
function logError(error: Error, info: React.ErrorInfo, category: ErrorCategory): void {
const isDev = process.env.NODE_ENV === 'development';
const entry = {
ts: new Date().toISOString(),
category,
message: error.message,
stack: isDev ? error.stack : undefined,
componentStack: isDev ? info.componentStack : undefined,
};
if (isDev) {
console.error('[ErrorBoundary]', JSON.stringify(entry, null, 2));
} else {
console.error('[ErrorBoundary]', entry.ts, entry.category, entry.message);
}
}
export default class ErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { hasError: false, retryCount: 0 };
this.state = { hasError: false, retryCount: 0, errorCategory: 'unknown' };
}
static getDerivedStateFromError(): Partial<State> {
return { hasError: true };
static getDerivedStateFromError(error: Error): Partial<State> {
return { hasError: true, errorCategory: classifyError(error) };
}
componentDidCatch(error: Error, info: React.ErrorInfo) {
console.error('[ErrorBoundary]', error, info.componentStack);
this.setState((prev) => ({ retryCount: prev.retryCount + 1 }));
const category = classifyError(error);
logError(error, info, category);
this.setState((prev) => ({
retryCount: prev.retryCount + 1,
errorCategory: category,
}));
}
handleRetry = () => {
@@ -34,21 +80,28 @@ export default class ErrorBoundary extends Component<Props, State> {
render() {
if (this.state.hasError) {
if (this.props.fallback) {
return this.props.fallback;
}
const exceeded = this.state.retryCount >= MAX_RETRIES;
const isNetwork = this.state.errorCategory === 'network';
const title = isNetwork ? '网络连接失败' : '页面出了点问题';
const desc = exceeded
? '请重启小程序后重试'
: isNetwork
? '请检查网络后重试'
: '请返回重试';
return (
<View className='error-boundary'>
<View className='error-icon-wrap'>
<Text className='error-icon-text'>!</Text>
</View>
<Text className='error-title'></Text>
<Text className='error-desc'>
{exceeded ? '请重启小程序' : '请返回重试'}
</Text>
<Text className='error-title'>{title}</Text>
<Text className='error-desc'>{desc}</Text>
{!exceeded && (
<View
className='error-retry-btn'
onClick={this.handleRetry}
>
<View className='error-retry-btn' onClick={this.handleRetry}>
<Text className='error-retry-text'></Text>
</View>
)}