From 445c1c55610cf563ac25f32009eb9736ea8b1a1d Mon Sep 17 00:00:00 2001 From: lafay <2021211506@stu.hit.edu.cn> Date: Tue, 7 Apr 2026 15:58:51 +0800 Subject: [PATCH] feat(auth): add user identity verification system with new screens Add comprehensive identity verification flow including: - New verification guide screen at /verification-guide for selecting identity type - New verification form screen at /verification-form for submitting verification documents - Verification settings screen at /profile/verification for managing verification status - Move terms and privacy policy to root-level routes (/terms, /privacy) - Update login to require active terms agreement checkbox - Database: add in-memory fallback when OPFS is unavailable on web BREAKING CHANGE: Terms and privacy policy routes moved from /profile/terms and /profile/privacy to /terms and /privacy --- app/(app)/(tabs)/profile/_layout.tsx | 1 + app/(app)/(tabs)/profile/verification.tsx | 5 ++ app/(auth)/verification-form.tsx | 5 ++ app/(auth)/verification-guide.tsx | 5 ++ app/_layout.tsx | 24 +++++++ app/privacy.tsx | 5 ++ app/terms.tsx | 5 ++ src/database/core/DatabaseConfig.ts | 4 +- src/database/core/DatabaseManager.ts | 12 +++- src/navigation/hrefs.ts | 19 +++++- src/screens/auth/LoginScreen.tsx | 66 ++++++++++++++++--- src/screens/auth/RegisterStep3Profile.tsx | 2 +- src/screens/auth/VerificationGuideScreen.tsx | 11 ++-- src/screens/auth/index.ts | 2 + src/screens/profile/SettingsScreen.tsx | 2 +- .../profile/VerificationSettingsScreen.tsx | 34 +++------- src/screens/profile/index.ts | 1 + 17 files changed, 152 insertions(+), 51 deletions(-) create mode 100644 app/(app)/(tabs)/profile/verification.tsx create mode 100644 app/(auth)/verification-form.tsx create mode 100644 app/(auth)/verification-guide.tsx create mode 100644 app/privacy.tsx create mode 100644 app/terms.tsx diff --git a/app/(app)/(tabs)/profile/_layout.tsx b/app/(app)/(tabs)/profile/_layout.tsx index 12acaac..3d62485 100644 --- a/app/(app)/(tabs)/profile/_layout.tsx +++ b/app/(app)/(tabs)/profile/_layout.tsx @@ -40,6 +40,7 @@ export default function ProfileStackLayout() { + ); } diff --git a/app/(app)/(tabs)/profile/verification.tsx b/app/(app)/(tabs)/profile/verification.tsx new file mode 100644 index 0000000..d9212f1 --- /dev/null +++ b/app/(app)/(tabs)/profile/verification.tsx @@ -0,0 +1,5 @@ +import { VerificationSettingsScreen } from '@/screens/profile/VerificationSettingsScreen'; + +export default function VerificationSettingsPage() { + return ; +} diff --git a/app/(auth)/verification-form.tsx b/app/(auth)/verification-form.tsx new file mode 100644 index 0000000..5c935ef --- /dev/null +++ b/app/(auth)/verification-form.tsx @@ -0,0 +1,5 @@ +import { VerificationFormScreen } from '@/screens/auth/VerificationFormScreen'; + +export default function VerificationFormPage() { + return ; +} diff --git a/app/(auth)/verification-guide.tsx b/app/(auth)/verification-guide.tsx new file mode 100644 index 0000000..61eab8f --- /dev/null +++ b/app/(auth)/verification-guide.tsx @@ -0,0 +1,5 @@ +import { VerificationGuideScreen } from '@/screens/auth/VerificationGuideScreen'; + +export default function VerificationGuidePage() { + return ; +} diff --git a/app/_layout.tsx b/app/_layout.tsx index 0fb9437..a200dad 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -175,6 +175,30 @@ function ThemedStack() { + router.back()} />, + }} + /> + router.back()} />, + }} + /> ; +} \ No newline at end of file diff --git a/app/terms.tsx b/app/terms.tsx new file mode 100644 index 0000000..0a85ae1 --- /dev/null +++ b/app/terms.tsx @@ -0,0 +1,5 @@ +import { TermsOfServiceScreen } from '../src/screens/profile/TermsOfServiceScreen'; + +export default function TermsRoute() { + return ; +} \ No newline at end of file diff --git a/src/database/core/DatabaseConfig.ts b/src/database/core/DatabaseConfig.ts index 0d3c5cf..0e09882 100644 --- a/src/database/core/DatabaseConfig.ts +++ b/src/database/core/DatabaseConfig.ts @@ -9,6 +9,6 @@ export const DB_CONFIG = { DB_NAME_PREFIX: 'carrot_bbs_', } as const; -export const getDbName = (userId: string | null): string => { +export function getDbName(userId: string | null): string { return userId ? `${DB_CONFIG.DB_NAME_PREFIX}${userId}.db` : DB_CONFIG.DEFAULT_DB_NAME; -}; \ No newline at end of file +} \ No newline at end of file diff --git a/src/database/core/DatabaseManager.ts b/src/database/core/DatabaseManager.ts index 36b902b..4af18ab 100644 --- a/src/database/core/DatabaseManager.ts +++ b/src/database/core/DatabaseManager.ts @@ -109,25 +109,31 @@ class DatabaseManager { const isWeb = typeof window !== 'undefined'; return LockManager.withWebLock(`${DB_CONFIG.LOCK_PREFIX}${dbName}`, async () => { + let lastError: unknown; for (let attempt = 0; attempt <= DB_CONFIG.MAX_RETRIES; attempt++) { try { const database = await SQLite.openDatabaseAsync(dbName); console.log(`[DatabaseManager] 成功打开数据库 (尝试 ${attempt + 1}/${DB_CONFIG.MAX_RETRIES + 1})`); return database; } catch (error) { + lastError = error; const msg = String(error); console.error(`[DatabaseManager] 打开失败 (尝试 ${attempt + 1}/${DB_CONFIG.MAX_RETRIES + 1}):`, msg); + if (isWeb && msg.includes('cannot create file')) { + console.log('[DatabaseManager] OPFS 不可用,回退到内存数据库'); + const memoryDb = await SQLite.openDatabaseAsync(':memory:'); + return memoryDb; + } + if (attempt < DB_CONFIG.MAX_RETRIES) { const delay = isWeb ? DB_CONFIG.WEB_RETRY_DELAY_BASE * (attempt + 1) : DB_CONFIG.NATIVE_RETRY_DELAY; console.warn(`[DatabaseManager] ${delay}ms 后重试...`); await new Promise(r => setTimeout(r, delay)); - } else { - throw error; } } } - throw new Error('打开数据库失败'); + throw lastError; }); } diff --git a/src/navigation/hrefs.ts b/src/navigation/hrefs.ts index 9a5e260..c12009e 100644 --- a/src/navigation/hrefs.ts +++ b/src/navigation/hrefs.ts @@ -185,9 +185,24 @@ export function hrefProfileAbout(): string { } export function hrefProfileTerms(): string { - return '/profile/terms'; + return '/terms'; } export function hrefProfilePrivacy(): string { - return '/profile/privacy'; + return '/privacy'; +} + +export function hrefProfileVerification(): string { + return '/profile/verification'; +} + +export function hrefVerificationGuide(): string { + return '/verification-guide'; +} + +export function hrefVerificationForm(identity?: string): string { + if (identity) { + return `/verification-form?identity=${encodeURIComponent(identity)}`; + } + return '/verification-form'; } diff --git a/src/screens/auth/LoginScreen.tsx b/src/screens/auth/LoginScreen.tsx index a25337d..3abef85 100644 --- a/src/screens/auth/LoginScreen.tsx +++ b/src/screens/auth/LoginScreen.tsx @@ -53,6 +53,7 @@ export const LoginScreen: React.FC = () => { const [password, setPassword] = useState(''); const [loading, setLoading] = useState(false); const [showPassword, setShowPassword] = useState(false); + const [agreedToTerms, setAgreedToTerms] = useState(false); // 动画值 const fadeAnim = useRef(new Animated.Value(0)).current; @@ -84,6 +85,10 @@ export const LoginScreen: React.FC = () => { showPrompt({ title: '提示', message: '请输入密码', type: 'warning' }); return false; } + if (!agreedToTerms) { + showPrompt({ title: '提示', message: '请同意用户协议和隐私政策', type: 'warning' }); + return false; + } return true; }; @@ -211,6 +216,37 @@ export const LoginScreen: React.FC = () => { 忘记密码? + {/* 服务条款勾选 */} + + setAgreedToTerms(!agreedToTerms)} + activeOpacity={0.8} + style={styles.checkboxWrapper} + > + + + + 我已阅读并同意 + router.push(hrefs.hrefProfileTerms())} + > + 《用户协议》 + + 和 + router.push(hrefs.hrefProfilePrivacy())} + > + 《隐私政策》 + + + + {/* 登录按钮 */} { - {/* 协议提示 */} - - - 登录即表示您同意 - router.push(hrefs.hrefProfileTerms())}>《用户协议》 - 和 - router.push(hrefs.hrefProfilePrivacy())}>《隐私政策》 - - + @@ -394,6 +422,26 @@ function createLoginStyles(colors: AppColors) { color: THEME_COLORS.primary, fontWeight: '500', }, + termsContainer: { + flexDirection: 'row', + alignItems: 'flex-start', + marginBottom: 20, + marginTop: 4, + }, + checkboxWrapper: { + paddingTop: 2, + }, + termsText: { + fontSize: 14, + color: colors.text.secondary, + marginLeft: 10, + flex: 1, + lineHeight: 22, + }, + termsLink: { + color: THEME_COLORS.primary, + fontWeight: '500', + }, }); } diff --git a/src/screens/auth/RegisterStep3Profile.tsx b/src/screens/auth/RegisterStep3Profile.tsx index 77a3f6d..06db465 100644 --- a/src/screens/auth/RegisterStep3Profile.tsx +++ b/src/screens/auth/RegisterStep3Profile.tsx @@ -38,7 +38,7 @@ export const RegisterStep3Profile: React.FC = ({ const [showPassword, setShowPassword] = useState(false); const [showConfirmPassword, setShowConfirmPassword] = useState(false); - const [agreedToTerms, setAgreedToTerms] = useState(true); + const [agreedToTerms, setAgreedToTerms] = useState(false); const [errors, setErrors] = useState>({}); const validateForm = (): boolean => { diff --git a/src/screens/auth/VerificationGuideScreen.tsx b/src/screens/auth/VerificationGuideScreen.tsx index 7ac84da..1711f2f 100644 --- a/src/screens/auth/VerificationGuideScreen.tsx +++ b/src/screens/auth/VerificationGuideScreen.tsx @@ -37,7 +37,7 @@ const IDENTITY_OPTIONS = [ key: 'teacher', title: '教职工', description: '需要提交工作证或教师资格证', - icon: 'chalkboard-teacher', + icon: 'account-tie', }, { key: 'staff', @@ -82,10 +82,7 @@ export const VerificationGuideScreen: React.FC = () => { return; } // 导航到认证表单页面 - router.push({ - pathname: '/auth/verification-form', - params: { identity: selectedIdentity }, - }); + router.push(hrefs.hrefVerificationForm(selectedIdentity)); }; const renderIdentityCard = (option: typeof IDENTITY_OPTIONS[0]) => { @@ -148,7 +145,7 @@ export const VerificationGuideScreen: React.FC = () => { @@ -164,7 +161,7 @@ export const VerificationGuideScreen: React.FC = () => { diff --git a/src/screens/auth/index.ts b/src/screens/auth/index.ts index d1d5bfe..fbc0e2c 100644 --- a/src/screens/auth/index.ts +++ b/src/screens/auth/index.ts @@ -7,6 +7,8 @@ export { LoginScreen } from './LoginScreen'; export { RegisterScreen } from './RegisterScreen'; export { ForgotPasswordScreen } from './ForgotPasswordScreen'; export { WelcomeScreen } from './WelcomeScreen'; +export { VerificationGuideScreen } from './VerificationGuideScreen'; +export { VerificationFormScreen } from './VerificationFormScreen'; // 注册步骤组件 export { RegisterStep1Email } from './RegisterStep1Email'; diff --git a/src/screens/profile/SettingsScreen.tsx b/src/screens/profile/SettingsScreen.tsx index c9a7511..ba65fb9 100644 --- a/src/screens/profile/SettingsScreen.tsx +++ b/src/screens/profile/SettingsScreen.tsx @@ -254,7 +254,7 @@ export const SettingsScreen: React.FC = () => { router.push(hrefs.hrefProfileEdit()); break; case 'verification': - router.push('/profile/verification'); + router.push(hrefs.hrefProfileVerification()); break; case 'notification_settings': router.push(hrefs.hrefProfileNotifications()); diff --git a/src/screens/profile/VerificationSettingsScreen.tsx b/src/screens/profile/VerificationSettingsScreen.tsx index bf1ea14..520a95e 100644 --- a/src/screens/profile/VerificationSettingsScreen.tsx +++ b/src/screens/profile/VerificationSettingsScreen.tsx @@ -84,7 +84,7 @@ export const VerificationSettingsScreen: React.FC = () => { }, []); const handleStartVerification = () => { - router.push('/auth/verification-guide'); + router.push(hrefs.hrefVerificationGuide()); }; const handleViewRecords = () => { @@ -109,27 +109,10 @@ export const VerificationSettingsScreen: React.FC = () => { const identityText = IDENTITY_TEXT[status?.identity || 'general']; return ( - - - router.back()} - activeOpacity={0.8} - > - - - 身份认证 - - - - + {/* 状态卡片 */} @@ -167,7 +150,7 @@ export const VerificationSettingsScreen: React.FC = () => { @@ -183,7 +166,7 @@ export const VerificationSettingsScreen: React.FC = () => { @@ -199,7 +182,7 @@ export const VerificationSettingsScreen: React.FC = () => { @@ -278,7 +261,6 @@ export const VerificationSettingsScreen: React.FC = () => { - ); }; diff --git a/src/screens/profile/index.ts b/src/screens/profile/index.ts index 85a8cb9..7784813 100644 --- a/src/screens/profile/index.ts +++ b/src/screens/profile/index.ts @@ -17,6 +17,7 @@ export { AccountSecurityScreen } from './AccountSecurityScreen'; export { AboutScreen } from './AboutScreen'; export { TermsOfServiceScreen } from './TermsOfServiceScreen'; export { PrivacyPolicyScreen } from './PrivacyPolicyScreen'; +export { VerificationSettingsScreen } from './VerificationSettingsScreen'; // 导出 Hook 供需要自定义的场景使用 export { useUserProfile, createSharedProfileStyles, TABS, TAB_ICONS } from './useUserProfile';