chore: 添加项目文档和工具文件

- 新增项目文档目录
- 新增项目图片资源目录
- 添加行尾注释清理工具脚本
This commit is contained in:
Leo 2025-07-07 14:46:44 +08:00
parent 53034eb2e1
commit f84ed3326c
12 changed files with 888 additions and 0 deletions

View File

@ -0,0 +1,727 @@
# WebSocket权限实时推送技术方案设计
## 📋 项目背景
在当前的权限管理系统中管理员修改用户权限后用户需要重新登录才能获得最新权限这严重影响了用户体验。为了解决这个问题我们设计了基于WebSocket的权限实时推送方案让权限变更能够立即生效。
## 🎯 整体架构设计
### 系统架构图
```
┌─────────────────┐ WebSocket ┌─────────────────┐ 数据库操作 ┌─────────────────┐
│ 前端应用 │ ←──────────→ │ Spring Boot │ ←──────────→ │ MySQL数据库 │
│ │ │ 后端服务 │ │ │
│ - 权限缓存 │ │ - WebSocket服务 │ │ - 用户权限表 │
│ - 实时更新 │ │ - 权限管理 │ │ - 权限变更记录 │
│ - 用户界面 │ │ - 消息推送 │ │ │
└─────────────────┘ └─────────────────┘ └─────────────────┘
```
### 核心目标
- ✅ 权限变更实时生效,无需重新登录
- ✅ 支持多标签页同步更新
- ✅ 安全可靠的消息推送机制
- ✅ 良好的用户体验和性能表现
## 🔧 后端技术方案设计
### 1. WebSocket服务架构
#### 1.1 技术栈选择
```
Spring Boot + Spring WebSocket + SaToken + Redis + MySQL
```
#### 1.2 核心组件设计
```
┌── WebSocket管理层
│ ├── WebSocketConfig (配置)
│ ├── WebSocketHandler (连接处理)
│ └── WebSocketInterceptor (权限验证)
├── 权限推送服务层
│ ├── PermissionPushService (权限推送核心服务)
│ ├── UserSessionManager (用户会话管理)
│ └── MessageBroadcaster (消息广播器)
├── 权限监听层
│ ├── PermissionChangeListener (权限变更监听)
│ ├── RoleChangeListener (角色变更监听)
│ └── MenuChangeListener (菜单变更监听)
└── 数据存储层
├── Redis (会话存储 + 消息队列)
└── MySQL (权限数据 + 变更记录)
```
### 2. 核心服务设计
#### 2.1 WebSocket连接管理
```java
// 连接管理策略
- 用户登录后自动建立WebSocket连接
- 一个用户可以有多个连接(多标签页支持)
- 连接断开后自动重连机制
- 连接状态持久化到Redis
// 会话存储结构
Key: "websocket:user:{userId}"
Value: {
"connections": [
{
"sessionId": "session-123",
"connectTime": 1640995200000,
"lastHeartbeat": 1640995800000,
"browser": "Chrome",
"ip": "192.168.1.100"
}
]
}
```
#### 2.2 权限变更监听机制
```java
// 监听触发点
1. 用户角色分配/取消
2. 角色权限修改
3. 菜单权限调整
4. 用户状态变更
// 变更事件设计
@EventListener
public class PermissionChangeListener {
// 用户角色变更
@Async
public void handleUserRoleChange(UserRoleChangeEvent event)
// 角色权限变更
@Async
public void handleRolePermissionChange(RolePermissionChangeEvent event)
// 菜单权限变更
@Async
public void handleMenuPermissionChange(MenuPermissionChangeEvent event)
}
```
#### 2.3 消息推送策略
```java
// 消息类型设计
enum MessageType {
PERMISSION_UPDATE, // 权限更新
ROLE_CHANGE, // 角色变更
FORCE_LOGOUT, // 强制退出
SYSTEM_NOTICE // 系统通知
}
// 推送策略
1. 精准推送:只推送给受影响的用户
2. 批量推送:角色权限变更时推送给该角色的所有用户
3. 广播推送:系统级权限调整时推送给所有在线用户
```
### 3. 数据库设计扩展
#### 3.1 权限变更记录表
```sql
CREATE TABLE sys_permission_change_log (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
change_type VARCHAR(50) NOT NULL, -- USER_ROLE, ROLE_PERMISSION, MENU_PERMISSION
target_user_id BIGINT, -- 目标用户ID如果是用户级变更
target_role_id BIGINT, -- 目标角色ID如果是角色级变更
operator_id BIGINT NOT NULL, -- 操作者ID
change_detail JSON, -- 变更详情
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_user_time (target_user_id, create_time),
INDEX idx_role_time (target_role_id, create_time)
);
```
#### 3.2 WebSocket会话表
```sql
CREATE TABLE sys_websocket_session (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL,
session_id VARCHAR(100) NOT NULL UNIQUE,
connect_time DATETIME NOT NULL,
disconnect_time DATETIME,
client_info JSON, -- 客户端信息
status TINYINT DEFAULT 1, -- 1:连接中 0:已断开
INDEX idx_user_status (user_id, status)
);
```
### 4. 后端核心接口设计
#### 4.1 WebSocket端点配置
```java
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(new PermissionWebSocketHandler(), "/ws/permission")
.setAllowedOrigins("*")
.addInterceptors(new WebSocketAuthInterceptor());
}
}
```
#### 4.2 权限推送服务接口
```java
@Component
public class PermissionPushService {
/**
* 推送权限更新消息给指定用户
*/
public void pushPermissionUpdate(Long userId, List<String> newPermissions);
/**
* 推送角色变更消息给指定用户
*/
public void pushRoleChange(Long userId, List<String> newRoles);
/**
* 批量推送权限更新(角色权限变更时)
*/
public void batchPushPermissionUpdate(Long roleId, List<String> newPermissions);
/**
* 强制用户下线
*/
public void forceUserLogout(Long userId, String reason);
}
```
#### 4.3 消息格式定义
```java
@Data
public class WebSocketMessage {
private String type; // 消息类型
private Long userId; // 目标用户ID
private Long timestamp; // 时间戳
private Object data; // 消息数据
private String messageId; // 消息ID
private String operator; // 操作者
}
@Data
public class PermissionUpdateData {
private List<String> permissions; // 新权限列表
private List<String> roles; // 新角色列表
private String updateType; // 更新类型ADD, REMOVE, REPLACE
private String reason; // 变更原因
}
```
## 🌐 前端技术方案设计
### 1. WebSocket客户端架构
#### 1.1 技术栈
```
Vue3 + TypeScript + Pinia + WebSocket API
```
#### 1.2 组件设计
```
┌── WebSocket管理层
│ ├── WebSocketService (核心WebSocket服务)
│ ├── ConnectionManager (连接管理器)
│ └── MessageHandler (消息处理器)
├── 权限更新层
│ ├── PermissionUpdateService (权限更新服务)
│ ├── PermissionSyncManager (权限同步管理)
│ └── PermissionNotification (权限通知)
└── 用户界面层
├── PermissionUpdateNotify (权限更新提示组件)
├── ConnectionStatus (连接状态组件)
└── WebSocketDebugPanel (调试面板)
```
### 2. WebSocket服务设计
#### 2.1 连接管理策略
```typescript
class WebSocketService {
// 连接策略
- 用户登录成功后自动连接
- 连接断开后指数退避重连
- 页面可见性变化时管理连接
- 网络状态变化时重连
// 连接状态管理
enum ConnectionState {
DISCONNECTED = 'disconnected',
CONNECTING = 'connecting',
CONNECTED = 'connected',
RECONNECTING = 'reconnecting',
ERROR = 'error'
}
}
```
#### 2.2 消息处理机制
```typescript
// 消息类型定义
interface WebSocketMessage {
type: 'PERMISSION_UPDATE' | 'ROLE_CHANGE' | 'FORCE_LOGOUT' | 'SYSTEM_NOTICE'
userId: number
timestamp: number
data: any
messageId: string
}
// 消息处理器
class MessageHandler {
handlePermissionUpdate() // 处理权限更新
handleRoleChange() // 处理角色变更
handleForceLogout() // 处理强制退出
handleSystemNotice() // 处理系统通知
}
```
### 3. 权限同步机制
#### 3.1 同步策略
```typescript
class PermissionSyncManager {
// 同步时机
1. 收到权限更新消息时立即同步
2. 连接重建后检查权限版本
3. 页面激活时检查权限一致性
// 同步方式
async syncPermissions(updateType: string) {
// 1. 请求最新权限数据
// 2. 更新本地权限缓存
// 3. 触发UI重新渲染
// 4. 显示权限更新通知
}
}
```
#### 3.2 冲突处理
```typescript
// 权限冲突处理策略
1. 权限被收回立即隐藏相关UI显示权限不足提示
2. 权限被授予:立即显示新的功能按钮,显示权限获得提示
3. 强制退出:清理本地数据,跳转到登录页
4. 操作中断:保存用户操作状态,权限恢复后继续
```
### 4. 前端核心服务设计
#### 4.1 WebSocket服务接口
```typescript
export interface IWebSocketService {
// 连接管理
connect(): Promise<void>
disconnect(): void
reconnect(): Promise<void>
// 消息发送
sendMessage(message: WebSocketMessage): void
// 事件监听
onMessage(callback: (message: WebSocketMessage) => void): void
onConnected(callback: () => void): void
onDisconnected(callback: () => void): void
onError(callback: (error: Error) => void): void
// 状态查询
isConnected(): boolean
getConnectionState(): ConnectionState
}
```
#### 4.2 权限更新服务接口
```typescript
export interface IPermissionUpdateService {
// 权限同步
syncPermissions(): Promise<void>
updateLocalPermissions(permissions: string[]): void
// 通知管理
showPermissionUpdateNotification(updateInfo: PermissionUpdateInfo): void
showPermissionRevokedWarning(revokedPermissions: string[]): void
// 权限检查
checkPermissionChange(): Promise<boolean>
validateCurrentPermissions(): boolean
}
```
## 🔐 安全性设计
### 1. 连接安全
```java
// Token验证
- WebSocket握手时验证JWT Token
- 定期验证Token有效性
- Token过期时自动断开连接
// 权限验证
- 连接建立时验证用户权限
- 消息发送前验证操作权限
- 防止权限越权操作
// 示例WebSocket拦截器
@Component
public class WebSocketAuthInterceptor implements HandshakeInterceptor {
@Override
public boolean beforeHandshake(ServerHttpRequest request,
ServerHttpResponse response,
WebSocketHandler wsHandler,
Map<String, Object> attributes) {
// 1. 提取Token
String token = extractTokenFromRequest(request);
// 2. 验证Token有效性
if (!saTokenUtil.isValidToken(token)) {
return false;
}
// 3. 获取用户信息
Long userId = saTokenUtil.getUserIdFromToken(token);
attributes.put("userId", userId);
return true;
}
}
```
### 2. 消息安全
```java
// 消息加密
- 敏感消息内容加密传输
- 消息完整性校验
- 防止消息重放攻击
// 频率限制
- 连接频率限制
- 消息发送频率限制
- 异常连接自动断开
// 示例:消息安全处理
@Component
public class MessageSecurityHandler {
public WebSocketMessage encryptMessage(WebSocketMessage message) {
// 对敏感数据进行加密
if (message.getType().equals("PERMISSION_UPDATE")) {
String encryptedData = aesUtil.encrypt(message.getData().toString());
message.setData(encryptedData);
}
return message;
}
public boolean validateMessageIntegrity(WebSocketMessage message) {
// 验证消息完整性
String expectedHash = calculateMessageHash(message);
return expectedHash.equals(message.getHash());
}
}
```
### 3. 访问控制
```java
// 用户隔离
- 确保用户只能接收自己的权限变更消息
- 防止跨用户信息泄露
- 管理员权限特殊处理
// 示例:消息权限验证
@Component
public class MessagePermissionValidator {
public boolean canReceiveMessage(Long userId, WebSocketMessage message) {
// 1. 检查消息是否发给该用户
if (!message.getUserId().equals(userId)) {
return false;
}
// 2. 检查用户是否有权限接收该类型消息
return hasPermissionToReceiveMessageType(userId, message.getType());
}
}
```
## 🚀 性能优化方案
### 1. 连接优化
```java
// 连接池管理
- 合理设置连接数上限
- 空闲连接自动清理
- 连接状态监控
// 内存优化
- 及时清理断开的连接
- 消息队列大小限制
- 定期清理过期数据
// 示例:连接池配置
@Configuration
public class WebSocketPoolConfig {
@Bean
public WebSocketConnectionPool connectionPool() {
return WebSocketConnectionPool.builder()
.maxConnections(10000) // 最大连接数
.maxConnectionsPerUser(5) // 每用户最大连接数
.idleTimeout(Duration.ofMinutes(30)) // 空闲超时
.cleanupInterval(Duration.ofMinutes(5)) // 清理间隔
.build();
}
}
```
### 2. 推送优化
```java
// 批量推送
- 相同类型消息合并推送
- 延迟推送策略
- 推送优先级管理
// 缓存优化
- Redis缓存权限数据
- 权限变更增量推送
- 本地权限缓存
// 示例:批量推送实现
@Component
public class BatchMessagePusher {
private final Map<String, List<WebSocketMessage>> messageBatches = new ConcurrentHashMap<>();
@Scheduled(fixedDelay = 1000) // 每秒批量推送一次
public void flushMessageBatches() {
messageBatches.forEach((batchKey, messages) -> {
WebSocketMessage batchMessage = mergeMess
ages(messages);
webSocketHandler.broadcast(batchMessage);
});
messageBatches.clear();
}
}
```
### 3. 数据库优化
```sql
-- 权限查询优化
CREATE INDEX idx_user_permission_version ON sys_login_user(user_id, permission_version);
CREATE INDEX idx_role_permission_update ON sys_role_menu(role_id, update_time);
-- 会话查询优化
CREATE INDEX idx_websocket_user_status ON sys_websocket_session(user_id, status, connect_time);
-- 变更日志查询优化
CREATE INDEX idx_permission_log_target ON sys_permission_change_log(target_user_id, target_role_id, create_time);
```
## 📊 监控和日志
### 1. 连接监控
```java
// 监控指标
- 当前连接数
- 连接成功率
- 连接断开原因统计
- 消息推送成功率
// 示例:监控服务
@Component
public class WebSocketMonitorService {
private final MeterRegistry meterRegistry;
public void recordConnection(String result) {
Counter.builder("websocket.connections")
.tag("result", result)
.register(meterRegistry)
.increment();
}
public void recordMessagePush(String type, String result) {
Counter.builder("websocket.messages")
.tag("type", type)
.tag("result", result)
.register(meterRegistry)
.increment();
}
}
```
### 2. 错误处理和日志
```java
// 日志记录
- 连接建立/断开日志
- 消息推送日志
- 错误异常日志
- 性能统计日志
// 示例:日志配置
@Slf4j
@Component
public class WebSocketLogger {
public void logConnection(Long userId, String action, String result) {
log.info("WebSocket连接 - 用户:{}, 操作:{}, 结果:{}", userId, action, result);
}
public void logMessagePush(Long userId, String messageType, String result) {
log.info("消息推送 - 用户:{}, 类型:{}, 结果:{}", userId, messageType, result);
}
public void logError(String operation, Exception e) {
log.error("WebSocket错误 - 操作:{}, 异常:", operation, e);
}
}
```
## 📋 实施步骤
### 第一阶段基础WebSocket服务1-2周
1. **后端WebSocket服务搭建**
- 创建WebSocket配置类
- 实现WebSocket处理器
- 添加权限验证拦截器
- 基础连接管理功能
2. **前端WebSocket客户端**
- 创建WebSocket服务类
- 实现连接管理逻辑
- 添加重连机制
- 基础消息收发功能
3. **基础测试**
- 连接建立测试
- 消息收发测试
- 断线重连测试
### 第二阶段权限推送核心功能2-3周
1. **权限变更监听**
- 实现用户角色变更监听
- 实现角色权限变更监听
- 实现菜单权限变更监听
- 创建权限变更事件
2. **消息推送逻辑**
- 实现权限更新消息推送
- 实现角色变更消息推送
- 实现批量推送逻辑
- 添加消息去重机制
3. **前端权限同步**
- 实现权限数据更新
- 实现UI实时刷新
- 添加权限变更通知
- 处理权限冲突场景
### 第三阶段高级功能和优化2-3周
1. **安全性增强**
- 添加消息加密
- 实现访问控制
- 添加频率限制
- 防止攻击机制
2. **性能优化**
- 实现连接池管理
- 添加消息批量处理
- 优化数据库查询
- 添加缓存机制
3. **用户体验优化**
- 完善重连策略
- 优化通知交互
- 添加调试面板
- 处理边界情况
### 第四阶段生产环境适配1-2周
1. **集群部署支持**
- Redis消息队列
- 负载均衡配置
- 会话共享机制
2. **监控和运维**
- 添加监控指标
- 完善日志记录
- 配置告警机制
- 制定运维手册
3. **测试和部署**
- 压力测试
- 兼容性测试
- 生产环境部署
- 回滚方案准备
## 🔍 风险评估和应对方案
### 1. 技术风险
| 风险项 | 影响度 | 概率 | 应对方案 |
|--------|--------|------|----------|
| WebSocket连接不稳定 | 高 | 中 | 完善重连机制,降级到轮询 |
| 消息推送延迟 | 中 | 低 | 优化推送逻辑,添加超时机制 |
| 内存泄漏 | 高 | 低 | 定期清理,添加监控 |
| 安全漏洞 | 高 | 低 | 安全审计,权限校验 |
### 2. 业务风险
| 风险项 | 影响度 | 概率 | 应对方案 |
|--------|--------|------|----------|
| 权限同步失败 | 高 | 中 | 手动刷新机制,错误提示 |
| 用户体验下降 | 中 | 低 | 渐进式升级,用户反馈 |
| 系统复杂度增加 | 中 | 高 | 完善文档,团队培训 |
### 3. 运维风险
| 风险项 | 影响度 | 概率 | 应对方案 |
|--------|--------|------|----------|
| 服务器压力增加 | 中 | 中 | 性能监控,扩容预案 |
| 故障排查困难 | 中 | 中 | 详细日志,监控告警 |
| 部署复杂度增加 | 低 | 高 | 自动化部署,回滚机制 |
## 📈 预期效果
### 1. 用户体验提升
- ✅ 权限变更即时生效,无需重新登录
- ✅ 多标签页权限状态同步
- ✅ 清晰的权限变更通知
### 2. 系统性能
- ✅ 减少不必要的接口调用
- ✅ 提高权限检查效率
- ✅ 降低服务器负载
### 3. 管理效率
- ✅ 权限管理操作即时生效
- ✅ 减少用户投诉和支持工作
- ✅ 提高系统管理效率
## 📚 相关技术文档
1. [Spring WebSocket官方文档](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#websocket)
2. [Vue3 WebSocket最佳实践](https://vuejs.org/guide/extras/web-components.html)
3. [SaToken权限认证文档](https://sa-token.dev33.cn/)
4. [Redis消息队列使用指南](https://redis.io/docs/manual/pubsub/)
## 👥 团队分工建议
| 角色 | 职责 | 时间投入 |
|------|------|----------|
| 后端开发 | WebSocket服务、权限监听、消息推送 | 60% |
| 前端开发 | WebSocket客户端、权限同步、UI更新 | 40% |
| 测试工程师 | 功能测试、性能测试、安全测试 | 全程参与 |
| 运维工程师 | 部署配置、监控告警、性能调优 | 后期参与 |
---
**文档版本**: v1.0
**创建时间**: 2025-01-07
**更新时间**: 2025-01-07
**负责人**: 系统架构团队
**审核人**: 技术负责人

161
fix_end_of_line_comments.py Normal file
View File

@ -0,0 +1,161 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
批量修改Java文件中的行尾注释脚本
将行尾注释改为单独占行的注释
"""
import os
import re
import sys
from pathlib import Path
def find_java_files(root_path):
"""查找所有Java文件"""
java_files = []
for root, dirs, files in os.walk(root_path):
for file in files:
if file.endswith('.java'):
java_files.append(os.path.join(root, file))
return java_files
def process_file(file_path):
"""处理单个Java文件"""
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
original_content = content
changes = []
# 分行处理
lines = content.split('\n')
new_lines = []
for i, line in enumerate(lines):
# 检查是否包含行尾注释
# 模式1: 代码行后跟 // 注释
match1 = re.match(r'^(\s*)(.*?)(;)\s*(//\s*(.*))\s*$', line)
if match1:
indent = match1.group(1)
code_part = match1.group(2)
semicolon = match1.group(3)
comment_part = match1.group(4)
comment_text = match1.group(5)
# 添加单独的注释行
new_lines.append(f'{indent}{comment_part}')
# 添加代码行
new_lines.append(f'{indent}{code_part}{semicolon}')
changes.append(f'{i+1}行: {line.strip()} -> 拆分为注释行和代码行')
continue
# 模式2: 代码行后跟 /* 注释 */
match2 = re.match(r'^(\s*)(.*?)(;)\s*(/\*\s*(.*?)\s*\*/)\s*$', line)
if match2:
indent = match2.group(1)
code_part = match2.group(2)
semicolon = match2.group(3)
comment_text = match2.group(5)
# 添加单独的注释行(转换为//格式)
new_lines.append(f'{indent}// {comment_text}')
# 添加代码行
new_lines.append(f'{indent}{code_part}{semicolon}')
changes.append(f'{i+1}行: {line.strip()} -> 拆分为注释行和代码行')
continue
# 模式3: 其他行尾注释情况(不以分号结尾)
match3 = re.match(r'^(\s*)(.*?)\s*(//\s*(.*))\s*$', line)
if match3 and not line.strip().startswith('//'):
indent = match3.group(1)
code_part = match3.group(2).strip()
comment_part = match3.group(3)
comment_text = match3.group(4)
# 确保不是整行注释
if code_part and not code_part.startswith('//'):
# 添加单独的注释行
new_lines.append(f'{indent}{comment_part}')
# 添加代码行
new_lines.append(f'{indent}{code_part}')
changes.append(f'{i+1}行: {line.strip()} -> 拆分为注释行和代码行')
continue
# 模式4: 其他行尾注释情况(/* */格式,不以分号结尾)
match4 = re.match(r'^(\s*)(.*?)\s*(/\*\s*(.*?)\s*\*/)\s*$', line)
if match4 and not line.strip().startswith('/*'):
indent = match4.group(1)
code_part = match4.group(2).strip()
comment_text = match4.group(4)
# 确保不是整行注释
if code_part and not code_part.startswith('/*'):
# 添加单独的注释行(转换为//格式)
new_lines.append(f'{indent}// {comment_text}')
# 添加代码行
new_lines.append(f'{indent}{code_part}')
changes.append(f'{i+1}行: {line.strip()} -> 拆分为注释行和代码行')
continue
# 没有匹配的模式,保持原样
new_lines.append(line)
# 如果有变化,写入文件
if changes:
new_content = '\n'.join(new_lines)
with open(file_path, 'w', encoding='utf-8') as f:
f.write(new_content)
return len(changes), changes
else:
return 0, []
except Exception as e:
print(f"处理文件 {file_path} 时出错: {e}")
return 0, []
def main():
"""主函数"""
# 项目根目录
root_path = '/Users/leocoder/leocoder/develop/templates/coder-common-thin/coder-common-thin-backend'
# 查找所有Java文件
java_files = find_java_files(root_path)
print(f"找到 {len(java_files)} 个Java文件")
print("开始处理...")
total_changes = 0
modified_files = []
for file_path in java_files:
change_count, changes = process_file(file_path)
if change_count > 0:
total_changes += change_count
modified_files.append({
'file': file_path,
'changes': change_count,
'details': changes
})
print(f"✓ 修改了 {file_path} - {change_count} 处更改")
print("\n" + "="*80)
print("修改报告")
print("="*80)
print(f"总共修改了 {len(modified_files)} 个文件")
print(f"总共修改了 {total_changes} 处行尾注释")
if modified_files:
print("\n详细修改列表:")
for file_info in modified_files:
print(f"\n文件: {file_info['file']}")
print(f"修改数量: {file_info['changes']}")
for detail in file_info['details'][:5]: # 只显示前5个变化
print(f" - {detail}")
if len(file_info['details']) > 5:
print(f" ... 还有 {len(file_info['details']) - 5} 个变化")
print("\n处理完成!")
if __name__ == "__main__":
main()

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB