react-native的token认证流程
在 React Native 中实现 Token 认证是移动应用开发中的常见需求,它用于验证用户的身份并授权其访问受保护的 API 资源。
Token 认证的核心流程:
-
用户登录 (Login):
- 用户在前端输入用户名和密码。
- 前端将这些凭据发送到后端 API。
- 后端验证凭据。如果验证成功,后端会生成一个 Token(通常是 JWT - JSON Web Token)并返回给前端。
- 前端接收到 Token 后,将其安全地存储起来(例如使用
AsyncStorage)。 - 前端更新用户的登录状态(例如 Context 或 Redux 中的状态)。
-
访问受保护资源 (Access Protected Resources):
- 前端需要访问后端受保护的 API 时,将之前保存的 Token 附带在请求的
Authorization头中发送给后端。 - 后端接收到请求后,验证 Token 的有效性(例如检查签名、有效期、是否被吊销等)。
- 如果 Token 有效,后端处理请求并返回数据。
- 如果 Token 无效(过期、篡改等),后端会返回错误(通常是 401 Unauthorized 或 403 Forbidden),前端需要处理这些错误(例如引导用户重新登录)。
- 前端需要访问后端受保护的 API 时,将之前保存的 Token 附带在请求的
-
Token 刷新 (Token Refresh - 可选但推荐):
- 为了安全性,Token 通常有较短的有效期。
- 当访问 Token 过期时,前端可以使用一个 Refresh Token(通常有效期较长,也存储在客户端)向后端请求一个新的访问 Token。
- 后端验证 Refresh Token,如果有效,则签发新的访问 Token。
- 这减少了用户频繁重新登录的次数,同时保持了安全性。
-
用户登出 (Logout):
- 用户选择登出时,前端从
AsyncStorage中移除保存的 Token。 - 前端清除用户相关的状态。
- (可选)通知后端使该 Token 失效(如果后端支持)。
- 用户选择登出时,前端从
React Native 中的实现细节:
我们将使用以下技术栈:
@react-native-async-storage/async-storage: 用于在设备上持久化存储 Token。- Context API (或 Redux/Zustand 等状态管理库): 用于在整个应用中管理用户的登录状态和 Token。
fetchAPI (或axios): 用于进行网络请求。crypto-js(可选,用于密码哈希): 在发送密码到后端前进行哈希处理,增加一层客户端安全性(尽管主要安全在于 HTTPS 和后端处理)。
详细代码实现与示例
我们将构建一个简化的应用,包含:
UserContext.tsx: 管理用户登录状态和 Token。AuthScreen.tsx: 登录界面。HomeScreen.tsx: 登录后的主界面,可以访问受保护资源并登出。App.tsx: 应用入口,根据登录状态决定显示哪个界面。
1. 安装必要的库
Bash
npm install @react-native-async-storage/async-storage crypto-js @react-navigation/native @react-navigation/stack
npx expo install react-native-screens react-native-safe-area-context # For react-navigation
# 或
yarn add @react-native-async-storage/async-storage crypto-js @react-navigation/native @react-navigation/stack
yarn expo install react-native-screens react-native-safe-area-context
2. 后端模拟 (Python Flask 示例)
为了能够运行前端代码,你需要一个简单的后端。这里提供一个 Python Flask 的极简示例。
backend/app.py
from flask import Flask, request, jsonify
from flask_cors import CORS
import jwt
import datetime
import hashlib # 用于密码哈希app = Flask(__name__)
CORS(app) # 允许跨域请求SECRET_KEY = "your_super_secret_key_for_jwt" # 生产环境请使用更复杂的密钥
REFRESH_SECRET_KEY = "your_super_secret_refresh_key_for_jwt"# 简单模拟的用户数据库
users_db = {"testuser": {"password_hash": hashlib.sha256("testpassword".encode('utf-8')).hexdigest(), # 密码哈希"user_id": "user123","user_name": "Test User","email": "test@example.com","phone": "123-456-7890"}
}@app.route('/api/user_login', methods=['POST'])
def user_login():data = request.get_json()username = data.get('username')password_hash = data.get('password') # 接收前端传来的哈希后的密码if not username or not password_hash:return jsonify({"message": "用户名和密码不能为空"}), 400user_data = users_db.get(username)if user_data and user_data["password_hash"] == password_hash:# 生成访问 Token (有效期短,例如 15 分钟)access_token_payload = {"user_id": user_data["user_id"],"user_name": user_data["user_name"],"exp": datetime.datetime.utcnow() + datetime.timedelta(minutes=15)}access_token = jwt.encode(access_token_payload, SECRET_KEY, algorithm="HS256")# 生成刷新 Token (有效期长,例如 7 天)refresh_token_payload = {"user_id": user_data["user_id"],"exp": datetime.datetime.utcnow() + datetime.timedelta(days=7)}refresh_token = jwt.encode(refresh_token_payload, REFRESH_SECRET_KEY, algorithm="HS256")return jsonify({"token": access_token,"refresh_token": refresh_token, # 将 refresh_token 也返回"user_id": user_data["user_id"],"user_name": user_data["user_name"],"email": user_data["email"],"phone": user_data["phone"],"message": "登录成功"}), 200else:return jsonify({"message": "无效的用户名或密码"}), 401@app.route('/api/protected_data', methods=['GET'])
def protected_data():auth_header = request.headers.get('Authorization')if not auth_header or not auth_header.startswith('Bearer '):return jsonify({"message": "未授权:缺少或无效的Authorization头"}), 401token = auth_header.split(" ")[1]try:payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])user_id = payload.get('user_id')user_name = payload.get('user_name', '未知用户')return jsonify({"message": f"欢迎, {user_name}! 这是受保护的数据。", "user_id": user_id}), 200except jwt.ExpiredSignatureError:return jsonify({"message": "未授权:Token 已过期"}), 401except jwt.InvalidTokenError:return jsonify({"message": "未授权:无效的 Token"}), 401@app.route('/api/refresh_token', methods=['POST'])
def refresh_token():data = request.get_json()refresh_token = data.get('refresh_token')if not refresh_token:return jsonify({"message": "未授权:缺少刷新Token"}), 400try:payload = jwt.decode(refresh_token, REFRESH_SECRET_KEY, algorithms=["HS256"])user_id = payload.get('user_id')# 重新生成新的访问 Tokenaccess_token_payload = {"user_id": user_id,"user_name": users_db.get(user_id, {}).get("user_name", "未知用户"), # 重新获取用户名"exp": datetime.datetime.utcnow() + datetime.timedelta(minutes=15)}new_access_token = jwt.encode(access_token_payload, SECRET_KEY, algorithm="HS256")# 也可以同时生成新的刷新 Token,以实现刷新 Token 的滚动更新new_refresh_token_payload = {"user_id": user_id,"exp": datetime.datetime.utcnow() + datetime.timedelta(days=7)}new_refresh_token = jwt.encode(new_refresh_token_payload, REFRESH_SECRET_KEY, algorithm="HS256")return jsonify({"token": new_access_token,"refresh_token": new_refresh_token # 返回新的刷新 Token}), 200except jwt.ExpiredSignatureError:return jsonify({"message": "未授权:刷新 Token 已过期,请重新登录"}), 401except jwt.InvalidTokenError:return jsonify({"message": "未授权:无效的刷新 Token"}), 401if __name__ == '__main__':# 注意:10.0.2.2 是 Android 模拟器访问宿主机(开发机器)的默认 IP# 如果是 iOS 模拟器或真实设备,请使用你电脑的局域网 IPapp.run(host='10.0.2.2', port=5000, debug=True)
运行后端:
- 保存为
backend/app.py。 - 安装依赖:
pip install Flask Flask-Cors PyJWT - 运行:
python backend/app.py
3. React Native 前端代码
src/contexts/UserContext.tsx
这是核心,负责管理用户认证状态、Token 的存储与获取。
// src/contexts/UserContext.tsx
import React, { createContext, useContext, useState, useEffect, ReactNode, useCallback } from 'react';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { SHA256 } from 'crypto-js'; // 用于前端密码哈希// 定义用户信息接口
export interface UserInfo {user_id: string;user_name: string;email: string;phone: string;status: 'logged_in' | 'logged_out';
}// 定义 Context 的类型
interface UserContextType {user: UserInfo | null; // 当前登录的用户信息authToken: string | null; // 访问 TokenrefreshToken: string | null; // 刷新 Tokenlogin: (username: string, password: string) => Promise<void>; // 登录方法logout: () => void; // 登出方法isLoading: boolean; // 是否正在加载用户数据 (应用启动时)refreshAccessToken: () => Promise<boolean>; // 刷新访问 Token 的方法
}// 创建 Context,并设置默认值
const UserContext = createContext<UserContextType>({user: null,authToken: null,refreshToken: null,login: async () => { /* no-op */ },logout: () => { /* no-op */ },isLoading: true, // 初始加载状态为 truerefreshAccessToken: async () => false, // 默认返回 false
});// UserProvider 组件
const UserProvider = ({ children }: { children: ReactNode }) => {const [user, setUser] = useState<UserInfo | null>(null);const [authToken, setAuthToken] = useState<string | null>(null);const [refreshToken, setRefreshToken] = useState<string | null>(null);const [isLoading, setIsLoading] = useState(true);// --- 1. 应用启动时加载存储的用户和 Token 信息 ---useEffect(() => {const loadStoredAuth = async () => {try {const storedAuthToken = await AsyncStorage.getItem('authToken');const storedRefreshToken = await AsyncStorage.getItem('refreshToken');const storedUserId = await AsyncStorage.getItem('userId');const storedUserName = await AsyncStorage.getItem('userName'); // 假设也存储了用户名if (storedAuthToken && storedRefreshToken && storedUserId && storedUserName) {setAuthToken(storedAuthToken);setRefreshToken(storedRefreshToken);// 这里可以根据实际情况从 API 重新获取完整的用户信息,// 或者直接使用存储的信息(如果它足够完整)setUser({user_id: storedUserId,user_name: storedUserName,email: 'N/A', // 示例,实际应从API获取或存储phone: 'N/A', // 示例status: 'logged_in',});console.log('User and tokens loaded from AsyncStorage.');} else {console.log('No existing user data or tokens found.');setUser(null);setAuthToken(null);setRefreshToken(null);}} catch (error) {console.error('Error loading user data from AsyncStorage:', error);setUser(null);setAuthToken(null);setRefreshToken(null);} finally {setIsLoading(false); // 加载完成}};loadStoredAuth();}, []); // 空依赖数组,只在组件挂载时运行一次// --- 2. 登录方法 ---const login = useCallback(async (username: string, password: string) => {try {const hashedPassword = SHA256(password).toString(); // 客户端密码哈希console.log('Hashed Password (Frontend):', hashedPassword);const response = await fetch('http://10.0.2.2:5000/api/user_login', {method: 'POST',headers: {'Content-Type': 'application/json',},body: JSON.stringify({ username, password: hashedPassword }),});if (!response.ok) {const errorData = await response.json();throw new Error(errorData.message || 'Login failed');}const data = await response.json();if (!data.token || !data.refresh_token || !data.user_id) {throw new Error('Invalid API response structure: missing token or user_id');}// 保存 Token 和用户 IDawait AsyncStorage.setItem('authToken', data.token);await AsyncStorage.setItem('refreshToken', data.refresh_token);await AsyncStorage.setItem('userId', data.user_id);await AsyncStorage.setItem('userName', data.user_name); // 也存储用户名// 更新 Context 状态setAuthToken(data.token);setRefreshToken(data.refresh_token);setUser({user_id: data.user_id,user_name: data.user_name || username,email: data.email || 'N/A',phone: data.phone || 'N/A',status: 'logged_in',});console.log('Login successful, user and tokens set.');} catch (error) {console.error('Login error:', error);// 登录失败时清除所有状态await logout(); // 调用登出方法清除所有相关信息throw error; // 重新抛出错误以便调用方处理}}, []); // 空依赖数组,确保 login 函数的稳定性// --- 3. 登出方法 ---const logout = useCallback(async () => {console.log('Logging out...');await AsyncStorage.removeItem('authToken');await AsyncStorage.removeItem('refreshToken');await AsyncStorage.removeItem('userId');await AsyncStorage.removeItem('userName');setUser(null);setAuthToken(null);setRefreshToken(null);console.log('User logged out, tokens cleared.');}, []); // 空依赖数组,确保 logout 函数的稳定性// --- 4. 刷新访问 Token 的方法 ---const refreshAccessToken = useCallback(async (): Promise<boolean> => {if (!refreshToken) {console.warn('No refresh token available. Cannot refresh access token.');await logout(); // 没有刷新 Token,视为需要重新登录return false;}try {console.log('Attempting to refresh access token...');const response = await fetch('http://10.0.2.2:5000/api/refresh_token', {method: 'POST',headers: { 'Content-Type': 'application/json' },body: JSON.stringify({ refresh_token: refreshToken }),});if (!response.ok) {const errorData = await response.json();console.error('Failed to refresh token:', errorData.message);await logout(); // 刷新失败,强制登出return false;}const data = await response.json();if (!data.token || !data.refresh_token) { // 后端可能也返回新的刷新tokenconsole.error('Invalid refresh token response.');await logout();return false;}await AsyncStorage.setItem('authToken', data.token);await AsyncStorage.setItem('refreshToken', data.refresh_token); // 保存新的刷新 TokensetAuthToken(data.token);setRefreshToken(data.refresh_token); // 更新状态console.log('Access token refreshed successfully.');return true;} catch (error) {console.error('Error during token refresh:', error);await logout(); // 网络或其他错误,强制登出return false;}}, [refreshToken, logout]); // 依赖 refreshToken 和 logout,确保是最新的// 提供的 Context 值const contextValue = {user,authToken,refreshToken,login,logout,isLoading,refreshAccessToken,};return (<UserContext.Provider value={contextValue}>{children}</UserContext.Provider>);
};export default UserProvider;// 自定义 Hook,方便组件使用 Context
export const useUser = () => useContext(UserContext);
src/screens/AuthScreen.tsx
登录界面,用户输入凭据并调用 login 方法。
// src/screens/AuthScreen.tsx
import React, { useState } from 'react';
import { View, Text, TextInput, Button, StyleSheet, ActivityIndicator, Alert } from 'react-native';
import { useUser } from '../contexts/UserContext'; // 引入 useUser Hookfunction AuthScreen() {const [username, setUsername] = useState('testuser'); // 默认用户名const [password, setPassword] = useState('testpassword'); // 默认密码const [isAuthenticating, setIsAuthenticating] = useState(false); // 登录状态const { login } = useUser(); // 获取登录方法const handleLogin = async () => {setIsAuthenticating(true); // 开始登录try {await login(username, password); // 调用 Context 中的登录方法// 登录成功,Context 会自动更新 user 状态,App.tsx 会负责导航} catch (error: any) {Alert.alert('登录失败', error.message || '请检查用户名和密码。');console.error('Login error in AuthScreen:', error);} finally {setIsAuthenticating(false); // 结束登录}};return (<View style={styles.container}><Text style={styles.title}>用户登录</Text><TextInputstyle={styles.input}placeholder="用户名"value={username}onChangeText={setUsername}autoCapitalize="none"/><TextInputstyle={styles.input}placeholder="密码"value={password}onChangeText={setPassword}secureTextEntry/><Buttontitle={isAuthenticating ? "登录中..." : "登录"}onPress={handleLogin}disabled={isAuthenticating} // 登录中禁用按钮/>{isAuthenticating && <ActivityIndicator style={styles.spinner} size="small" color="#0000ff" />}</View>);
}const styles = StyleSheet.create({container: {flex: 1,justifyContent: 'center',alignItems: 'center',padding: 20,backgroundColor: '#f5f5f5',},title: {fontSize: 28,fontWeight: 'bold',marginBottom: 30,color: '#333',},input: {width: '100%',padding: 15,borderWidth: 1,borderColor: '#ddd',borderRadius: 8,marginBottom: 15,backgroundColor: '#fff',},spinner: {marginTop: 10,},
});export default AuthScreen;
src/screens/HomeScreen.tsx
登录后的主界面,可以访问受保护资源并登出。
TypeScript
// src/screens/HomeScreen.tsx
import React, { useState, useEffect } from 'react';
import { View, Text, Button, StyleSheet, ActivityIndicator, Alert } from 'react-native';
import { useUser } from '../contexts/UserContext'; // 引入 useUser Hookfunction HomeScreen() {const { user, authToken, logout, refreshAccessToken } = useUser(); // 获取用户信息、token、登出、刷新方法const [protectedData, setProtectedData] = useState<string | null>(null);const [isFetchingData, setIsFetchingData] = useState(false);// --- 访问受保护数据的方法 ---const fetchProtectedData = async () => {if (!authToken) {Alert.alert('错误', '没有访问 Token,请重新登录。');return;}setIsFetchingData(true);try {const response = await fetch('http://10.0.2.2:5000/api/protected_data', {headers: {'Authorization': `Bearer ${authToken}`, // 在 Authorization 头中携带 Token},});if (response.status === 401) { // Unauthorized (Token 可能过期或无效)const errorData = await response.json();if (errorData.message === "未授权:Token 已过期") {console.log('Access Token expired, attempting to refresh...');const refreshed = await refreshAccessToken(); // 尝试刷新 Tokenif (refreshed) {// 如果刷新成功,可以考虑重新尝试本次请求 (更高级的实现会在拦截器中自动重试)Alert.alert('提示', '访问 Token 已刷新,请重新尝试获取数据。');} else {Alert.alert('会话过期', '您的会话已过期,请重新登录。');}} else {Alert.alert('认证失败', errorData.message || '无效的访问 Token。');}await logout(); // 遇到 401 且无法刷新,通常需要登出return;}if (!response.ok) {const errorData = await response.json();throw new Error(errorData.message || '获取受保护数据失败。');}const data = await response.json();setProtectedData(data.message);} catch (error: any) {Alert.alert('错误', `获取数据失败: ${error.message}`);console.error('Error fetching protected data:', error);} finally {setIsFetchingData(false);}};return (<View style={styles.container}><Text style={styles.title}>欢迎,{user?.user_name || '用户'}!</Text><Text style={styles.info}>您的用户ID: {user?.user_id}</Text>{authToken && (<Text style={styles.info}>访问 Token: {authToken.substring(0, 20)}...</Text>)}{refreshToken && (<Text style={styles.info}>刷新 Token: {refreshToken.substring(0, 20)}...</Text>)}<Buttontitle={isFetchingData ? "获取中..." : "获取受保护数据"}onPress={fetchProtectedData}disabled={isFetchingData || !authToken} // 没有 Token 或正在获取时禁用/>{isFetchingData && <ActivityIndicator style={styles.spinner} size="small" color="#0000ff" />}{protectedData && (<View style={styles.dataContainer}><Text style={styles.dataTitle}>受保护数据:</Text><Text style={styles.dataText}>{protectedData}</Text></View>)}<View style={styles.logoutButtonContainer}><Button title="登出" onPress={logout} color="red" /></View></View>);
}const styles = StyleSheet.create({container: {flex: 1,justifyContent: 'center',alignItems: 'center',padding: 20,backgroundColor: '#f5f5f5',},title: {fontSize: 24,fontWeight: 'bold',marginBottom: 10,color: '#333',},info: {fontSize: 16,marginBottom: 5,color: '#555',},spinner: {marginTop: 10,},dataContainer: {marginTop: 30,padding: 15,borderWidth: 1,borderColor: '#ccc',borderRadius: 8,backgroundColor: '#e9e9e9',width: '100%',alignItems: 'center',},dataTitle: {fontSize: 18,fontWeight: 'bold',marginBottom: 10,color: '#333',},dataText: {fontSize: 16,textAlign: 'center',color: '#666',},logoutButtonContainer: {marginTop: 40,width: '80%',},
});export default HomeScreen;
src/navigation/MainNavigator.tsx (主应用内部导航)
TypeScript
// src/navigation/MainNavigator.tsx
import React from 'react';
import { createStackNavigator } from '@react-navigation/stack';
import HomeScreen from '../screens/HomeScreen';
// 你可以添加更多登录后的页面const Stack = createStackNavigator();function MainNavigator() {return (<Stack.Navigator screenOptions={{ headerShown: false }}><Stack.Screen name="Home" component={HomeScreen} />{/* <Stack.Screen name="Settings" component={SettingsScreen} /> */}</Stack.Navigator>);
}export default MainNavigator;
src/navigation/AuthNavigator.tsx (认证页面导航)
// src/navigation/AuthNavigator.tsx
import React from 'react';
import { createStackNavigator } from '@react-navigation/stack';
import AuthScreen from '../screens/AuthScreen';const Stack = createStackNavigator();function AuthNavigator() {return (<Stack.Navigator screenOptions={{ headerShown: false }}><Stack.Screen name="Login" component={AuthScreen} />{/* <Stack.Screen name="Register" component={RegisterScreen} /> */}</Stack.Navigator>);
}export default AuthNavigator;
App.tsx (应用入口)
这是应用的主文件,它设置了 UserProvider 并根据用户的登录状态来切换导航器。
TypeScript
// App.tsx
import 'react-native-gesture-handler'; // react-navigation 必备
import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
import { ActivityIndicator, View, StyleSheet, Text } from 'react-native';import UserProvider, { useUser } from './src/contexts/UserContext'; // 引入你的 UserProvider 和 useUser Hook
import MainNavigator from './src/navigation/MainNavigator';
import AuthNavigator from './src/navigation/AuthNavigator';const Stack = createStackNavigator();// 根导航器,根据认证状态切换主/认证流程
function RootNavigator() {const { user, isLoading } = useUser(); // 从 Context 获取 user 和 isLoading 状态if (isLoading) {// 如果正在加载用户数据,显示加载指示器或启动屏return (<View style={styles.loadingContainer}><ActivityIndicator size="large" color="#0000ff" /><Text style={styles.loadingText}>正在加载应用数据...</Text></View>);}return (<Stack.Navigator screenOptions={{ headerShown: false }}>{user ? (// 如果 user 不为 null (已登录),显示主应用界面<Stack.Screen name="Main" component={MainNavigator} />) : (// 如果 user 为 null (未登录),显示认证界面<Stack.Screen name="Auth" component={AuthNavigator} />)}</Stack.Navigator>);
}export default function App() {return (<NavigationContainer><UserProvider><RootNavigator /></UserProvider></NavigationContainer>);
}const styles = StyleSheet.create({loadingContainer: {flex: 1,justifyContent: 'center',alignItems: 'center',backgroundColor: '#fff',},loadingText: {marginTop: 10,fontSize: 16,color: '#555',},
});
4. 项目结构
my-react-native-app/
├── App.tsx
├── package.json
├── src/
│ ├── contexts/
│ │ └── UserContext.tsx
│ ├── navigation/
│ │ ├── AuthNavigator.tsx
│ │ └── MainNavigator.tsx
│ └── screens/
│ ├── AuthScreen.tsx
│ └── HomeScreen.tsx
└── backend/ (你的后端代码,例如 app.py)└── app.py
总结
UserProvider: 是 Token 认证的核心。它负责在应用启动时从AsyncStorage中加载 Token,管理登录/登出逻辑,并通过 Context API 将user信息、authToken、refreshToken和isLoading状态暴露给整个应用。isLoading状态: 用于在应用启动时,等待UserProvider从AsyncStorage加载完数据。在这期间,你应该显示一个加载指示器,避免闪烁或提前渲染错误内容。login方法: 用户登录成功后,从后端获取 Token 并将其保存在AsyncStorage和 Context 状态中。logout方法: 清除AsyncStorage中的 Token 和 Context 状态。refreshAccessToken方法: 处理访问 Token 过期的情况。它会尝试使用刷新 Token 获取新的访问 Token。- 网络请求: 在需要访问受保护资源时,从
useUser()中获取authToken,并将其添加到请求的Authorization: Bearer <token>头中。 - 错误处理: 特别是对于 401/403 错误,需要捕获并引导用户重新认证(例如,跳转到登录页)。
这个示例提供了一个基本但完整的 React Native Token 认证流程。在生产环境中,你可能还需要考虑更复杂的错误处理、Token 有效性检查、安全性增强(如 HTTPS)、更强大的网络请求库(如 Axios 及其拦截器)以及更完善的用户信息管理。
相关文章:
react-native的token认证流程
在 React Native 中实现 Token 认证是移动应用开发中的常见需求,它用于验证用户的身份并授权其访问受保护的 API 资源。 Token 认证的核心流程: 用户登录 (Login): 用户在前端输入用户名和密码。前端将这些凭据发送到后端 API。后端验证凭据。如果验证成…...
ERP系统中商品定价功能设计:支持渠道、会员与批发场景的灵活定价机制
在现代零售、批发与电商环境下,商品的定价策略日益复杂。一个优秀的ERP系统不仅需要管理商品基础信息、库存与订单,还必须提供一套灵活且可扩展的商品定价机制,以满足: 不同销售渠道(如线上平台、线下门店、分销商&…...
Spring是如何实现属性占位符解析
Spring属性占位符解析 核心实现思路1️⃣ 定义占位符处理器类2️⃣ 处理 BeanDefinition 中的属性3️⃣ 替换具体的占位符4️⃣ 加载配置文件5️⃣ Getter / Setter 方法 源码见:mini-spring 在使用 Spring 框架开发过程中,为了实现配置的灵活性…...
数据结构之ArrayList
系列文章目录 目录 系列文章目录 前言 一、数据结构的前置语法 1. 时空复杂度 2. 包装类 3. 泛型 二、ArrayList 和顺序表 1. 顺序表的模拟实现 2. 源码 3. ArrayList 的优缺点 前言 本文介绍数据结构的前置算法,以及 ArrayList 的模拟实现,部…...
DDR4读写压力测试
1.1测试环境 1.1.1整体环境介绍 板卡: pcie-403板卡 主控芯片: Xilinx xcvu13p-fhgb2104-2 调试软件: Vivado 2018.3 代码环境: Vscode utf-8 测试工程: pcie403_user_top 1.1.2硬件介绍 UD PCIe-403…...
uniapp 开发企业微信小程序时,如何在当前页面真正销毁前或者关闭小程序前调用一个api接口
在 UniApp 开发企业微信小程序时,若需在页面销毁或小程序关闭前调用 API 接口,需结合页面生命周期和应用生命周期实现。以下是具体实现方案及注意事项: 一、在页面销毁前调用 API(页面级) 通过页面生命周期钩子 onUnl…...
WPF 按钮点击音效实现
WPF 按钮点击音效实现 下面我将为您提供一个完整的 WPF 按钮点击音效实现方案,包含多种实现方式和高级功能: 完整实现方案 MainWindow.xaml <Window x:Class"ButtonClickSound.MainWindow"xmlns"http://schemas.microsoft.com/win…...
编写测试用例
测试用例(Test Case)是用于测试系统的要素集合 目录 编写测试用例作用 编写测试用例要包含七大元素 测试用例的设计方法 1、等价类法 2、边界值法 3、正交表法 4、判定表法 5、错误推测法 6、场景法 编写测试用例作用 1、确保功能全面覆盖…...
解释程序(Python)不需要生成机器码 逐行解析 逐行执行
在计算机组成原理中,解释程序(Interpreter)通常不会生成独立的机器码,但具体情况取决于解释器的实现方式。以下是详细分析: 1. 传统解释程序:不生成机器码 直接逐行执行: 经典的解释器ÿ…...
每日Prompt:隐形人
提示词 黑色棒球帽,白色抹胸、粉色低腰短裙、白色襪子,黑色鞋子,粉紅色背包,衣服悬浮在空中呈现动态姿势,虚幻引擎渲染风格,高清晰游戏CG质感,户外山林背景,画面聚焦在漂浮的衣服上…...
TensorFlow深度学习实战(19)——受限玻尔兹曼机
TensorFlow深度学习实战(19)——受限玻尔兹曼机 0. 前言1. 受限玻尔兹曼机1.1 受限玻尔兹曼机架构1.2 受限玻尔兹曼机的数学原理 2. 使用受限玻尔兹曼机重建图像3. 深度信念网络小结系列链接 0. 前言 受限玻尔兹曼机 (Restricted Boltzmann Machine, RB…...
告别手动绘图!基于AI的Smart Mermaid自动可视化图表工具搭建与使用指南
以下是对Smart Mermaid的简单介绍: 一款基于 AI 技术的 Web 应用程序,可将文本内容智能转换为 Mermaid 格式的代码,并将其渲染成可视化图表可以智能制作流程图、序列图、甘特图、状态图等等,并且支持在线调整、图片导出可以Docke…...
【Oracle】安装单实例
个人主页:Guiat 归属专栏:Oracle 文章目录 1. 安装前的准备工作1.1 硬件和系统要求1.2 检查系统环境1.3 下载Oracle软件 2. 系统配置2.1 创建Oracle用户和组2.2 配置内核参数2.3 配置用户资源限制2.4 安装必要的软件包 3. 目录结构和环境变量3.1 创建Ora…...
C++测开,自动化测试,业务(第一段实习)
目录 🌼前言 一,实习经历怎么写简历 🌹业务理解 🎂结构化表达 二,实习 🦂技术和流程卡点 🔑实习收获 / 代码风格 三,测试理论,用例设计,工具链 &…...
QT中更新或添加组件时出现“”qt操作至少需要一个处于启用状态的有效资料档案库“解决方法”
在MaintenanceTool.exe中点击下一步 第一个: 第二个: 第三个: 以上任意一个放入资料库中...
论文速读《UAV-Flow Colosseo: 自然语言控制无人机系统》
论文链接:https://arxiv.org/abs/2505.15725项目主页:https://prince687028.github.io/UAV-Flow/ 0. 简介 近年来,无人机技术蓬勃发展,但如何让无人机像智能助手一样理解并执行人类语言指令,仍是一个前沿挑战。现有研…...
ES6+中Promise 中错误捕捉详解——链式调用catch()或者async/await+try/catch
通过 unhandledrejection 捕捉未处理的 Promise 异常,手动将其抛出,最终让 window.onerror 捕捉,从而统一所有异常的处理逻辑 规范代码:catch(onRejected)、async...awaittry...catch 在 JavaScript 的 Pro…...
CDN安全加速:HTTPS加密最佳配置方案
CDN安全加速的HTTPS加密最佳配置方案需从证书管理、协议优化、安全策略到性能调优进行全链路设计,以下是核心实施步骤与注意事项: 一、证书配置与管理 证书选择与格式 证书类型:优先使用受信任CA机构颁发的DV/OV/EV证…...
解常微分方程组
Euler法 function euler_method % 参数设置 v_missile 450; % 导弹速度 km/h v_enemy 90; % 敌艇速度 km/h % 初始条件 x0 0; % 导弹初始位置 x y0 0; % 导弹初始位置 y xe0 120; % 敌艇初始位置 y t0 0; % 初始时间 % 时间步长和总时间 dt 0.01; % 时间步长 t_final …...
C++实现汉诺塔游戏自动完成
目录 一、汉诺塔的规则二、数学递归推导式三、步骤实现(一)汉诺塔模型(二)递归实现(三)显示1.命令行显示2.SDL图形显示 四、处理用户输入及SDL环境配置五、总结六、源码下载 一、汉诺塔的规则 游戏由3根柱子和若干大小不一的圆盘组成,初始状态下,所有的…...
在 ABP VNext 中集成 Serilog:打造可观测、结构化日志系统
🚀 在 ABP VNext 中集成 Serilog:打造可观测、结构化日志系统 📚 目录 🚀 在 ABP VNext 中集成 Serilog:打造可观测、结构化日志系统1. 为什么要使用结构化日志? 🤔2. 核心集成步骤 Ὦ…...
pikachu靶场通关笔记07 XSS关卡03-存储型XSS
目录 一、XSS 二、存储型XSS 三、源码分析 四、渗透实战 1、输入mooyuan试一试 2、注入Payload 3、查看数据库 4、再次进入留言板页面 本系列为通过《pikachu靶场通关笔记》的XSS关卡(共10关)渗透集合,通过对XSS关卡源码的代码审计找到XSS风险的…...
GitLab CI、GitHub Actions和Jenkins进行比较
特性/工具JenkinsGitLab CIGitHub Actions架构设计哲学Master/Agent分布式架构,通过插件扩展功能代码与CI/CD强耦合,内置Git仓库,基于Runner注册机制事件驱动,与GitHub深度集成,基于虚拟机的Job执行单元核心运行机制支…...
strcat及其模拟实现
#define _CRT_SECURE_NO_WARNINGS strcat 追加字符串 str "string"(字符串) cat "concatenate"(连接 / 追加) char* strcat(char* destination, const char* source); strcat的应用 方法一ÿ…...
OpenCV CUDA模块直方图计算------用于在 GPU 上执行对比度受限的自适应直方图均衡类cv::cuda::CLAHE
操作系统:ubuntu22.04 OpenCV版本:OpenCV4.9 IDE:Visual Studio Code 编程语言:C11 算法描述 cv::cuda::CLAHE 是 OpenCV 的 CUDA 模块中提供的一个类,用于在 GPU 上执行对比度受限的自适应直方图均衡(Contrast Limi…...
华为OD机试真题——矩形绘制(2025A卷:200分)Java/python/JavaScript/C/C++/GO最佳实现
2025 A卷 200分 题型 本专栏内全部题目均提供Java、python、JavaScript、C、C++、GO六种语言的最佳实现方式; 并且每种语言均涵盖详细的问题分析、解题思路、代码实现、代码详解、3个测试用例以及综合分析; 本文收录于专栏:《2025华为OD真题目录+全流程解析+备考攻略+经验分…...
通义开源视觉感知多模态 RAG 推理框架 VRAG-RL:开启多模态推理新时代
通义实验室的自然语言智能团队,凭借深厚的技术积累与创新精神,成功研发并开源了视觉感知多模态 RAG 推理框架 VRAG-RL,为 AI 在复杂视觉信息处理领域带来了重大突破。 传统 RAG 方法的局限 传统的检索增强型生成(RAG࿰…...
爬虫入门:从基础到实战全攻略
🧠 一、爬虫基础概念 1.1 爬虫定义 爬虫(Web Crawler)是模拟浏览器行为,自动向服务器发送请求并获取响应数据的一种程序。主要用于从网页中提取结构化数据,供后续分析、展示或存储使用。 1.2 爬虫特点 数据碎片化&…...
qemu安装risc-V 64
参考这篇文章https://developer.aliyun.com/article/1323996,其中在wsl下面安装可能会报错环境变量中有空格。 # clean_path.sh#!/bin/bash# 备份旧 PATH OLD_PATH"$PATH"# 过滤掉包含空格、制表符、换行的路径 CLEAN_PATH"" IFS: read -ra PA…...
JDBC连不上mysql:Unable to load authentication plugin ‘caching_sha2_password‘.
最近为一个spring-boot项目下了mysql-9.3.0,结果因为mysql版本太新一直报错连不上。 错误如下: 2025-06-01 16:19:43.516 ERROR 22088 --- [http-nio-8080-exec-2] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispat…...
