feat(verification): add user identity verification system
Some checks failed
Frontend CI / build-android-apk (push) Failing after 9m29s
Frontend CI / build-and-push-web (push) Failing after 1m12s
Frontend CI / ota-android (push) Failing after 7m29s

Add comprehensive user verification feature including:
- VerificationGuideScreen: Guide screen explaining the verification process
- VerificationFormScreen: Form for submitting verification applications
- VerificationSettingsScreen: Settings page to view verification status
- verificationService: API service for verification operations
- verificationStore: State management for verification state
- DTOs: Types for verification records, status, and admin operations

The system supports multiple identity types (student, teacher, staff, alumni, external)
and includes both user-facing and admin-facing functionality.
This commit is contained in:
lafay
2026-04-05 20:26:51 +08:00
parent 82c2970a85
commit 542d385d08
8 changed files with 1525 additions and 1 deletions

View File

@@ -52,6 +52,7 @@ const SETTINGS_GROUPS_BASE = [
icon: 'shield-check-outline',
items: [
{ key: 'edit_profile', title: '编辑资料', icon: 'account-edit-outline', showArrow: true },
{ key: 'verification', title: '身份认证', icon: 'check-decagram', showArrow: true, subtitle: '获得认证标识,解锁更多功能' },
{ key: 'privacy', title: '隐私设置', icon: 'shield-account-outline', showArrow: true },
{ key: 'security', title: '账号安全', icon: 'lock-outline', showArrow: true },
{ key: 'blocked_users', title: '黑名单', icon: 'account-off-outline', showArrow: true },
@@ -252,6 +253,9 @@ export const SettingsScreen: React.FC = () => {
case 'edit_profile':
router.push(hrefs.hrefProfileEdit());
break;
case 'verification':
router.push('/profile/verification');
break;
case 'notification_settings':
router.push(hrefs.hrefProfileNotifications());
break;

View File

@@ -0,0 +1,498 @@
/**
* 身份认证设置页面
* 用户查看认证状态、提交认证申请
*/
import React, { useEffect, useState } from 'react';
import {
View,
Text,
StyleSheet,
TouchableOpacity,
ScrollView,
Image,
Alert,
ActivityIndicator,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useRouter } from 'expo-router';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { useAppColors, type AppColors } from '../../theme';
import {
useVerificationStore,
useVerificationStatus,
useVerificationLoading,
} from '../../stores/verificationStore';
import { showPrompt } from '../../services/promptService';
import * as hrefs from '../../navigation/hrefs';
const THEME_COLORS = {
primary: '#FF6B35',
primaryLight: '#FF8C5A',
primaryDark: '#E55A2B',
};
const STATUS_CONFIG = {
not_submitted: {
title: '未认证',
description: '完成身份认证,解锁更多专属功能',
icon: 'account-question-outline',
color: '#999',
},
pending: {
title: '审核中',
description: '您的认证申请正在审核中,请耐心等待',
icon: 'clock-outline',
color: '#FFA500',
},
approved: {
title: '已认证',
description: '您已完成身份认证',
icon: 'check-decagram',
color: '#4CAF50',
},
rejected: {
title: '认证失败',
description: '您的认证申请被拒绝,可以重新提交',
icon: 'close-circle-outline',
color: '#F44336',
},
};
const IDENTITY_TEXT: Record<string, string> = {
general: '普通用户',
student: '在校学生',
teacher: '教职工',
staff: '工作人员',
alumni: '校友',
external: '外部人员',
};
export const VerificationSettingsScreen: React.FC = () => {
const colors = useAppColors();
const styles = createStyles(colors);
const router = useRouter();
const status = useVerificationStatus();
const loading = useVerificationLoading();
const { fetchStatus, reset } = useVerificationStore();
useEffect(() => {
fetchStatus();
return () => {
reset();
};
}, []);
const handleStartVerification = () => {
router.push('/auth/verification-guide');
};
const handleViewRecords = () => {
showPrompt({
title: '认证记录',
message: '认证记录功能即将上线',
type: 'info',
});
};
if (loading && !status) {
return (
<SafeAreaView style={styles.container}>
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color={THEME_COLORS.primary} />
</View>
</SafeAreaView>
);
}
const statusConfig = STATUS_CONFIG[status?.verification_status || 'not_submitted'];
const identityText = IDENTITY_TEXT[status?.identity || 'general'];
return (
<SafeAreaView style={styles.container}>
<View style={styles.header}>
<TouchableOpacity
style={styles.backButton}
onPress={() => router.back()}
activeOpacity={0.8}
>
<MaterialCommunityIcons
name="arrow-left"
size={24}
color={colors.text?.primary || '#333'}
/>
</TouchableOpacity>
<Text style={styles.headerTitle}></Text>
<View style={styles.headerRight} />
</View>
<ScrollView
contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false}
>
{/* 状态卡片 */}
<View style={styles.statusCard}>
<View style={[styles.statusIconContainer, { backgroundColor: statusConfig.color + '20' }]}>
<MaterialCommunityIcons
name={statusConfig.icon as any}
size={48}
color={statusConfig.color}
/>
</View>
<Text style={styles.statusTitle}>{statusConfig.title}</Text>
<Text style={styles.statusDescription}>{statusConfig.description}</Text>
{status?.verification_status === 'approved' && (
<View style={styles.identityBadge}>
<MaterialCommunityIcons name="check-circle" size={16} color={THEME_COLORS.primary} />
<Text style={styles.identityText}>{identityText}</Text>
</View>
)}
{status?.latest_record?.reject_reason && (
<View style={styles.rejectReasonContainer}>
<Text style={styles.rejectReasonLabel}></Text>
<Text style={styles.rejectReasonText}>
{status.latest_record.reject_reason}
</Text>
</View>
)}
</View>
{/* 认证优势 */}
{status?.verification_status !== 'approved' && (
<View style={styles.benefitsSection}>
<Text style={styles.sectionTitle}></Text>
<View style={styles.benefitsList}>
<View style={styles.benefitItem}>
<View style={styles.benefitIconContainer}>
<MaterialCommunityIcons
name="verified"
size={24}
color={THEME_COLORS.primary}
/>
</View>
<View style={styles.benefitContent}>
<Text style={styles.benefitTitle}></Text>
<Text style={styles.benefitDescription}>
</Text>
</View>
</View>
<View style={styles.benefitItem}>
<View style={styles.benefitIconContainer}>
<MaterialCommunityIcons
name="lock-open-variant"
size={24}
color={THEME_COLORS.primary}
/>
</View>
<View style={styles.benefitContent}>
<Text style={styles.benefitTitle}></Text>
<Text style={styles.benefitDescription}>
访
</Text>
</View>
</View>
<View style={styles.benefitItem}>
<View style={styles.benefitIconContainer}>
<MaterialCommunityIcons
name="account-group"
size={24}
color={THEME_COLORS.primary}
/>
</View>
<View style={styles.benefitContent}>
<Text style={styles.benefitTitle}></Text>
<Text style={styles.benefitDescription}>
</Text>
</View>
</View>
</View>
</View>
)}
{/* 操作按钮 */}
<View style={styles.actionSection}>
{(status?.verification_status === 'not_submitted' ||
status?.verification_status === 'rejected') && (
<TouchableOpacity
style={styles.primaryButton}
onPress={handleStartVerification}
activeOpacity={0.9}
>
<MaterialCommunityIcons name="shield-check-outline" size={20} color="#FFF" />
<Text style={styles.primaryButtonText}>
{status?.verification_status === 'rejected' ? '重新认证' : '开始认证'}
</Text>
</TouchableOpacity>
)}
{status?.verification_status === 'pending' && (
<TouchableOpacity
style={styles.secondaryButton}
onPress={handleViewRecords}
activeOpacity={0.8}
>
<MaterialCommunityIcons name="file-document-outline" size={20} color={THEME_COLORS.primary} />
<Text style={styles.secondaryButtonText}></Text>
</TouchableOpacity>
)}
{status?.verification_status === 'approved' && (
<>
<View style={styles.verifiedInfo}>
<View style={styles.verifiedInfoRow}>
<Text style={styles.verifiedInfoLabel}></Text>
<Text style={styles.verifiedInfoValue}>{identityText}</Text>
</View>
{status.latest_record?.real_name && (
<View style={styles.verifiedInfoRow}>
<Text style={styles.verifiedInfoLabel}></Text>
<Text style={styles.verifiedInfoValue}>
{status.latest_record.real_name}
</Text>
</View>
)}
{status.latest_record?.department && (
<View style={styles.verifiedInfoRow}>
<Text style={styles.verifiedInfoLabel}>/</Text>
<Text style={styles.verifiedInfoValue}>
{status.latest_record.department}
</Text>
</View>
)}
</View>
</>
)}
</View>
{/* 帮助信息 */}
<View style={styles.helpSection}>
<Text style={styles.helpTitle}></Text>
<Text style={styles.helpText}>
</Text>
</View>
</ScrollView>
</SafeAreaView>
);
};
function createStyles(colors: AppColors) {
return StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
},
loadingContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 16,
paddingVertical: 12,
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: colors.divider || '#E0E0E0',
},
backButton: {
width: 40,
height: 40,
justifyContent: 'center',
alignItems: 'center',
},
headerTitle: {
fontSize: 18,
fontWeight: '600',
color: colors.text?.primary || '#333',
},
headerRight: {
width: 40,
},
scrollContent: {
flexGrow: 1,
padding: 24,
},
statusCard: {
alignItems: 'center',
paddingVertical: 32,
paddingHorizontal: 24,
backgroundColor: '#F9F9F9',
borderRadius: 16,
marginBottom: 24,
},
statusIconContainer: {
width: 96,
height: 96,
borderRadius: 48,
justifyContent: 'center',
alignItems: 'center',
marginBottom: 16,
},
statusTitle: {
fontSize: 24,
fontWeight: '700',
color: colors.text?.primary || '#333',
marginBottom: 8,
},
statusDescription: {
fontSize: 15,
color: colors.text?.secondary || '#666',
textAlign: 'center',
lineHeight: 22,
},
identityBadge: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#FFF5F0',
borderRadius: 20,
paddingVertical: 8,
paddingHorizontal: 16,
marginTop: 16,
},
identityText: {
fontSize: 14,
fontWeight: '600',
color: THEME_COLORS.primary,
marginLeft: 6,
},
rejectReasonContainer: {
marginTop: 16,
padding: 12,
backgroundColor: '#FFF3F3',
borderRadius: 8,
width: '100%',
},
rejectReasonLabel: {
fontSize: 13,
fontWeight: '600',
color: '#F44336',
marginBottom: 4,
},
rejectReasonText: {
fontSize: 13,
color: '#D32F2F',
lineHeight: 18,
},
benefitsSection: {
marginBottom: 24,
},
sectionTitle: {
fontSize: 17,
fontWeight: '600',
color: colors.text?.primary || '#333',
marginBottom: 16,
},
benefitsList: {
gap: 12,
},
benefitItem: {
flexDirection: 'row',
alignItems: 'flex-start',
backgroundColor: '#F9F9F9',
borderRadius: 12,
padding: 16,
},
benefitIconContainer: {
width: 48,
height: 48,
borderRadius: 12,
backgroundColor: '#FFF5F0',
justifyContent: 'center',
alignItems: 'center',
marginRight: 12,
},
benefitContent: {
flex: 1,
},
benefitTitle: {
fontSize: 15,
fontWeight: '600',
color: colors.text?.primary || '#333',
marginBottom: 4,
},
benefitDescription: {
fontSize: 13,
color: colors.text?.secondary || '#666',
lineHeight: 18,
},
actionSection: {
marginBottom: 24,
},
primaryButton: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
height: 56,
borderRadius: 14,
backgroundColor: THEME_COLORS.primary,
},
primaryButtonText: {
fontSize: 17,
fontWeight: '600',
color: '#FFF',
marginLeft: 8,
},
secondaryButton: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
height: 56,
borderRadius: 14,
backgroundColor: '#FFF5F0',
borderWidth: 1,
borderColor: THEME_COLORS.primary,
},
secondaryButtonText: {
fontSize: 17,
fontWeight: '600',
color: THEME_COLORS.primary,
marginLeft: 8,
},
verifiedInfo: {
backgroundColor: '#F9F9F9',
borderRadius: 12,
padding: 16,
},
verifiedInfoRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingVertical: 8,
},
verifiedInfoLabel: {
fontSize: 15,
color: colors.text?.secondary || '#666',
},
verifiedInfoValue: {
fontSize: 15,
fontWeight: '500',
color: colors.text?.primary || '#333',
},
helpSection: {
alignItems: 'center',
paddingVertical: 16,
},
helpTitle: {
fontSize: 15,
fontWeight: '600',
color: colors.text?.primary || '#333',
marginBottom: 8,
},
helpText: {
fontSize: 13,
color: colors.text?.hint || '#999',
textAlign: 'center',
lineHeight: 18,
},
});
}
export default VerificationSettingsScreen;