From 2fdd842b892e1dc90913f88aa1b08abfa92d00d5 Mon Sep 17 00:00:00 2001 From: sion <450702724@qq.com> Date: Mon, 23 Mar 2026 00:08:19 +0800 Subject: [PATCH] youhua --- .agents/skills/clean-code/SKILL.md | 516 ++++++++++++++++++ .agents/skills/ralph-loop/SKILL.md | 160 ++++++ .../skills/shadcn-ui-flutter/SKILL.md | 0 .../skills/shadcn-ui-flutter/skill.yaml | 0 .../lib/core/network/api_response.dart | 74 +++ .../lib/core/network/dio_client.dart | 134 ++--- flutter_monisuo/lib/main.dart | 190 ++++--- .../lib/providers/auth_provider.dart | 142 +++-- .../lib/ui/pages/auth/login_page.dart | 287 +++++----- .../lib/ui/pages/mine/mine_page.dart | 9 +- .../java/com/it/rattan/config/WebConfig.java | 64 ++- 11 files changed, 1196 insertions(+), 380 deletions(-) create mode 100644 .agents/skills/clean-code/SKILL.md create mode 100644 .agents/skills/ralph-loop/SKILL.md rename {.agent => .agents}/skills/shadcn-ui-flutter/SKILL.md (100%) rename {.agent => .agents}/skills/shadcn-ui-flutter/skill.yaml (100%) create mode 100644 flutter_monisuo/lib/core/network/api_response.dart diff --git a/.agents/skills/clean-code/SKILL.md b/.agents/skills/clean-code/SKILL.md new file mode 100644 index 0000000..2d6bc4a --- /dev/null +++ b/.agents/skills/clean-code/SKILL.md @@ -0,0 +1,516 @@ +--- +name: clean-code +description: Clean Code 代码质量审查和重构指南。用于代码审查、重构、提升代码质量、减少技术债务。触发词:代码审查、clean code、重构、代码质量、技术债务、代码规范。 +--- + +# Clean Code 技能 + +帮助编写高质量、可维护、可读性强的代码。 + +## 核心原则 + +### 1. 有意义的命名 +```typescript +// ❌ 坏 +const d = new Date(); +const ymd = date.split('-'); + +// ✅ 好 +const currentDate = new Date(); +const [year, month, day] = date.split('-'); +``` + +**规则:** +- 使用描述性名称,避免缩写 +- 名称应该表达意图 +- 避免误导性名称 +- 做有意义的区分(不是 a1, a2) +- 使用可搜索的名称 +- 类名用名词,方法名用动词 + +### 2. 函数 +```typescript +// ❌ 坏 - 做太多事 +function processUser(user: User) { + // 验证 + if (!user.email) throw new Error('Email required'); + if (!user.name) throw new Error('Name required'); + + // 保存 + database.save(user); + + // 发送邮件 + emailService.send(user.email, 'Welcome!'); + + // 记录日志 + logger.log(`User ${user.name} created`); +} + +// ✅ 好 - 单一职责 +function validateUser(user: User): void { + if (!user.email) throw new Error('Email required'); + if (!user.name) throw new Error('Name required'); +} + +function saveUser(user: User): void { + database.save(user); +} + +function sendWelcomeEmail(user: User): void { + emailService.send(user.email, 'Welcome!'); +} + +function logUserCreation(user: User): void { + logger.log(`User ${user.name} created`); +} + +function processUser(user: User) { + validateUser(user); + saveUser(user); + sendWelcomeEmail(user); + logUserCreation(user); +} +``` + +**规则:** +- 函数应该小(<20行) +- 只做一件事 +- 每个函数一个抽象层级 +- 使用描述性名称 +- 函数参数越少越好(理想0-3个) +- 避免副作用 +- 分隔指令与询问 + +### 3. 注释 +```typescript +// ❌ 坏 - 多余的注释 +// 检查用户是否已激活 +if (user.isActive) { ... } + +// ✅ 好 - 代码自解释 +if (user.isActive) { ... } + +// ✅ 好 - 解释为什么 +// 使用 setTimeout 而不是 setInterval 避免重叠执行 +setTimeout(processQueue, 1000); +``` + +**规则:** +- 好的代码是自解释的 +- 注释不能弥补糟糕的代码 +- 用代码表达意图 +- 好的注释:法律信息、解释意图、警示、TODO +- 坏的注释:喃喃自语、多余的、误导的、注释掉的代码 + +### 4. 格式 +```typescript +// ❌ 坏 +export class User{constructor(private name:string,private age:number){} +getName(){return this.name;}} + +// ✅ 好 +export class User { + constructor( + private name: string, + private age: number + ) {} + + getName(): string { + return this.name; + } +} +``` + +**规则:** +- 垂直格式:概念之间用空行分隔 +- 水平格式:行宽<120字符 +- 缩进:统一使用2或4空格 +- 团队规则:遵循项目既定风格 + +### 5. 对象和数据结构 +```typescript +// ❌ 坏 - 暴露内部结构 +class User { + public name: string; + public age: number; +} + +// ✅ 好 - 隐藏实现 +class User { + private _name: string; + private _age: number; + + get name(): string { + return this._name; + } + + set age(value: number) { + if (value < 0) throw new Error('Invalid age'); + this._age = value; + } +} +``` + +**规则:** +- 数据抽象:隐藏实现 +- 数据、对象的反对称性 +- 得墨忒耳定律:模块不应知道它操作对象的内部细节 + +### 6. 错误处理 +```typescript +// ❌ 坏 - 返回 null +function getUser(id: string): User | null { + return database.find(id); +} + +// ✅ 好 - 抛出异常 +function getUser(id: string): User { + const user = database.find(id); + if (!user) throw new UserNotFoundError(id); + return user; +} + +// ✅ 好 - 使用 Special Case 模式 +class NullUser implements User { + name = 'Guest'; + age = 0; +} +``` + +**规则:** +- 使用异常而非返回码 +- 先写 Try-Catch-Finally +- 给出异常的上下文 +- 别返回 null 值 +- 别传递 null 值 + +### 7. 边界 +```typescript +// ❌ 坏 - 直接依赖第三方类 +import { Map } from 'third-party-lib'; + +class UserCollection { + private users: Map; +} + +// ✅ 好 - 使用适配器模式 +interface UserMap { + get(key: string): User; + set(key: string, user: User): void; +} + +class ThirdPartyUserMap implements UserMap { + private map: Map; + + get(key: string): User { + return this.map.get(key); + } + + set(key: string, user: User): void { + this.map.set(key, user); + } +} +``` + +**规则:** +- 隐藏第三方代码 +- 使用适配器模式 +- 边界处的代码需要清晰的分割和测试 + +### 8. 单元测试 +```typescript +// ❌ 坏 - 不清晰的测试 +test('user', () => { + const u = new User('John', 25); + expect(u.getName()).toBe('John'); + expect(u.getAge()).toBe(25); +}); + +// ✅ 好 - BUILD-OPERATE-CHECK 模式 +test('shouldReturnUserName', () => { + // BUILD + const user = new User('John', 25); + + // OPERATE + const name = user.getName(); + + // CHECK + expect(name).toBe('John'); +}); + +// ✅ 好 - Given-When-Then 模式 +test('shouldReturnUserName', () => { + // Given + const user = new User('John', 25); + + // When + const name = user.getName(); + + // Then + expect(name).toBe('John'); +}); +``` + +**规则:** +- F.I.R.S.T 原则: + - Fast(快速) + - Independent(独立) + - Repeatable(可重复) + - Self-Validating(自验证) + - Timely(及时) +- 每个测试一个断言 +- 单一概念 +- 测试代码和生产代码一样重要 + +### 9. 类 +```typescript +// ❌ 坏 - 大类 +class UserManager { + createUser() {} + deleteUser() {} + updateUser() {} + sendEmail() {} + validateEmail() {} + generateReport() {} +} + +// ✅ 好 - 单一职责 +class UserService { + create(user: User) {} + delete(id: string) {} + update(user: User) {} +} + +class EmailService { + send(email: string, content: string) {} + validate(email: string): boolean {} +} + +class ReportService { + generate(userId: string): Report {} +} +``` + +**规则:** +- 类应该小 +- 单一职责原则(SRP) +- 内聚性:方法和数据互相依赖 +- 保持内聚性就会得到许多短小的类 + +### 10. 系统 +```typescript +// ❌ 坏 - 硬编码依赖 +class UserService { + private db = new Database(); // 硬编码 +} + +// ✅ 好 - 依赖注入 +class UserService { + constructor(private db: Database) {} +} + +// ✅ 好 - 工厂模式 +class ServiceFactory { + static createUserService(): UserService { + return new UserService(new Database()); + } +} +``` + +**规则:** +- 分离构造和使用 +- 依赖注入 +- 扩充:AOP(面向切面编程) +- 测试驱动系统架构 + +## 代码审查清单 + +### 命名 +- [ ] 变量名是否描述了其用途? +- [ ] 函数名是否描述了其行为? +- [ ] 类名是否描述了其职责? +- [ ] 名称是否一致? + +### 函数 +- [ ] 函数是否小于20行? +- [ ] 函数是否只做一件事? +- [ ] 函数参数是否<=3个? +- [ ] 函数是否有副作用? +- [ ] 函数名是否描述性? + +### 结构 +- [ ] 代码是否有清晰的层次结构? +- [ ] 类是否遵循单一职责原则? +- [ ] 是否有重复代码? +- [ ] 依赖是否清晰? + +### 测试 +- [ ] 是否有足够的测试覆盖? +- [ ] 测试是否快速? +- [ ] 测试是否独立? +- [ ] 测试是否清晰? + +### 错误处理 +- [ ] 是否处理了所有可能的错误? +- [ ] 错误信息是否清晰? +- [ ] 是否避免了返回 null? + +## 重构技巧 + +### 提取方法 +```typescript +// Before +function printOwing(invoice: Invoice) { + let outstanding = 0; + + // 打印横幅 + console.log('***********************'); + console.log('*** Customer Owes ***'); + console.log('***********************'); + + // 计算未付款 + for (const order of invoice.orders) { + outstanding += order.amount; + } + + // 打印详情 + console.log(`name: ${invoice.customer}`); + console.log(`amount: ${outstanding}`); +} + +// After +function printOwing(invoice: Invoice) { + printBanner(); + const outstanding = calculateOutstanding(invoice); + printDetails(invoice, outstanding); +} + +function printBanner() { + console.log('***********************'); + console.log('*** Customer Owes ***'); + console.log('***********************'); +} + +function calculateOutstanding(invoice: Invoice): number { + return invoice.orders.reduce((sum, order) => sum + order.amount, 0); +} + +function printDetails(invoice: Invoice, outstanding: number) { + console.log(`name: ${invoice.customer}`); + console.log(`amount: ${outstanding}`); +} +``` + +### 内联方法 +```typescript +// Before +function getRating(driver: Driver): number { + return moreThanFiveLateDeliveries(driver) ? 2 : 1; +} + +function moreThanFiveLateDeliveries(driver: Driver): boolean { + return driver.numberOfLateDeliveries > 5; +} + +// After +function getRating(driver: Driver): number { + return driver.numberOfLateDeliveries > 5 ? 2 : 1; +} +``` + +### 提取变量 +```typescript +// Before +if (platform.toUpperCase().indexOf('MAC') > -1 && + browser.toUpperCase().indexOf('IE') > -1 && + wasInitialized() && resize > 0) { + // do something +} + +// After +const isMacOs = platform.toUpperCase().indexOf('MAC') > -1; +const isIE = browser.toUpperCase().indexOf('IE') > -1; +const wasResized = resize > 0; + +if (isMacOs && isIE && wasInitialized() && wasResized) { + // do something +} +``` + +### 分解条件 +```typescript +// Before +if (date.before(SUMMER_START) || date.after(SUMMER_END)) { + charge = quantity * winterRate + winterServiceCharge; +} else { + charge = quantity * summerRate; +} + +// After +if (isSummer(date)) { + charge = summerCharge(quantity); +} else { + charge = winterCharge(quantity); +} + +function isSummer(date: Date): boolean { + return !date.before(SUMMER_START) && !date.after(SUMMER_END); +} + +function summerCharge(quantity: number): number { + return quantity * summerRate; +} + +function winterCharge(quantity: number): number { + return quantity * winterRate + winterServiceCharge; +} +``` + +### 以多态取代条件 +```typescript +// Before +function calculatePay(employee: Employee): number { + switch (employee.type) { + case 'ENGINEER': + return employee.monthlySalary; + case 'SALESMAN': + return employee.monthlySalary + employee.commission; + case 'MANAGER': + return employee.monthlySalary + employee.bonus; + default: + throw new Error('Invalid employee type'); + } +} + +// After +abstract class Employee { + abstract calculatePay(): number; +} + +class Engineer extends Employee { + calculatePay(): number { + return this.monthlySalary; + } +} + +class Salesman extends Employee { + calculatePay(): number { + return this.monthlySalary + this.commission; + } +} + +class Manager extends Employee { + calculatePay(): number { + return this.monthlySalary + this.bonus; + } +} +``` + +## 使用方法 + +1. **代码审查时**:参考清单逐项检查 +2. **重构时**:应用重构技巧 +3. **新功能开发时**:遵循核心原则 +4. **代码坏味道识别**:参考常见问题 + +告诉我需要审查的代码,我会帮你识别问题并提供改进建议! diff --git a/.agents/skills/ralph-loop/SKILL.md b/.agents/skills/ralph-loop/SKILL.md new file mode 100644 index 0000000..e6056e1 --- /dev/null +++ b/.agents/skills/ralph-loop/SKILL.md @@ -0,0 +1,160 @@ +--- +name: ralph-loop +description: Ralph Loop 自动化编码循环 - 让 AI 代理持续工作直到完成任务。支持 PLANNING(规划)和 BUILDING(构建)两种模式,使用 exec + process 工具监控进度。 +allowed-tools: Read, Write, Edit, Exec, Process +--- + +# Ralph Loop (Agent Mode) + +自动化 AI 编码代理循环工作流。 + +## 工作流程 + +``` +需求定义 → PLANNING Loop → BUILDING Loop → 完成 +``` + +1. **PLANNING Loop** - 创建/更新 IMPLEMENTATION_PLAN.md(不实现) +2. **BUILDING Loop** - 实现任务、运行测试、更新计划、提交 + +## 支持的 CLI + +| CLI | 命令模式 | TTY 需求 | +|-----|---------|---------| +| OpenCode | `opencode run --model "$(cat PROMPT.md)"` | ✅ | +| Codex | `codex exec --full-auto "$(cat PROMPT.md)"` | ✅ | +| Claude Code | `claude --dangerously-skip-permissions "$(cat PROMPT.md)"` | ✅ | +| Pi | `pi --provider -p "$(cat PROMPT.md)"` | ✅ | +| Goose | `goose run "$(cat PROMPT.md)"` | ✅ | + +## 使用方法 + +### 1. 准备项目文件 + +创建 `PROMPT.md`: +```markdown +# 目标 +<你的任务描述> + +## 参考文件 +- specs/*.md +- IMPLEMENTATION_PLAN.md +``` + +创建 `AGENTS.md`: +```markdown +# 项目说明 + +## 测试命令 +npm test + +## 构建命令 +npm run build +``` + +### 2. 启动循环 + +**PLANNING 模式**: +```bash +# 我会执行 +exec( + command: 'opencode run --model claude-opus-4 "$(cat PROMPT.md)"', + workdir: "/path/to/project", + background: true, + pty: true, + timeout: 3600 +) +``` + +**BUILDING 模式**: +```bash +# 同上,但 PROMPT.md 内容不同 +``` + +### 3. 监控进度 + +```bash +# 轮询状态 +process(action: "poll", sessionId: "xxx") + +# 查看日志 +process(action: "log", sessionId: "xxx", offset: -30) +``` + +### 4. 检测完成 + +检查 `IMPLEMENTATION_PLAN.md`: +- `STATUS: PLANNING_COMPLETE` - 规划完成 +- `STATUS: COMPLETE` - 构建完成 + +## 提示词模板 + +### PLANNING 模式 + +```markdown +You are running a Ralph PLANNING loop for this goal: . + +Read specs/* and the current codebase. Only update IMPLEMENTATION_PLAN.md. + +Rules: +- Do not implement +- Do not commit +- Create a prioritized task list +- Write down questions if unclear + +Completion: +When plan is ready, add: STATUS: PLANNING_COMPLETE +``` + +### BUILDING 模式 + +```markdown +You are running a Ralph BUILDING loop for this goal: . + +Context: specs/*, IMPLEMENTATION_PLAN.md, AGENTS.md + +Tasks: +1) Pick the most important task +2) Investigate code +3) Implement +4) Run backpressure commands from AGENTS.md +5) Update IMPLEMENTATION_PLAN.md +6) Update AGENTS.md with learnings +7) Commit with clear message + +Completion: +When all done, add: STATUS: COMPLETE +``` + +## 安全注意事项 + +- 使用沙盒环境 +- 设置合理超时 +- 重要项目先备份 +- 监控进度,不要过早终止 + +## 故障排除 + +| 问题 | 解决方案 | +|------|---------| +| CLI 卡住 | 确保 pty: true | +| 无法启动 | 检查 CLI 路径和 git 仓库 | +| 未检测到完成 | 验证 IMPLEMENTATION_PLAN.md 中的格式 | +| 超时 | 增加 timeout 参数 | + +## 示例 + +### 完整工作流 + +1. 用户: "帮我实现用户认证模块" +2. 我创建 specs/auth.md, PROMPT.md, AGENTS.md +3. 我启动 PLANNING 循环 +4. AI 生成 IMPLEMENTATION_PLAN.md +5. 我启动 BUILDING 循环 +6. AI 逐个实现任务 +7. 检测到 STATUS: COMPLETE +8. 完成! + +## License + +MIT diff --git a/.agent/skills/shadcn-ui-flutter/SKILL.md b/.agents/skills/shadcn-ui-flutter/SKILL.md similarity index 100% rename from .agent/skills/shadcn-ui-flutter/SKILL.md rename to .agents/skills/shadcn-ui-flutter/SKILL.md diff --git a/.agent/skills/shadcn-ui-flutter/skill.yaml b/.agents/skills/shadcn-ui-flutter/skill.yaml similarity index 100% rename from .agent/skills/shadcn-ui-flutter/skill.yaml rename to .agents/skills/shadcn-ui-flutter/skill.yaml diff --git a/flutter_monisuo/lib/core/network/api_response.dart b/flutter_monisuo/lib/core/network/api_response.dart new file mode 100644 index 0000000..3a50d1e --- /dev/null +++ b/flutter_monisuo/lib/core/network/api_response.dart @@ -0,0 +1,74 @@ +/// API 响应状态码 +class ResponseCode { + static const String success = '0000'; + static const String unauthorized = '0002'; +} + +/// API 响应模型 +class ApiResponse { + final bool success; + final String? message; + final T? data; + final String? code; + + ApiResponse({ + required this.success, + this.message, + this.data, + this.code, + }); + + factory ApiResponse.success(T data, [String? message]) { + return ApiResponse( + success: true, + data: data, + message: message, + code: ResponseCode.success, + ); + } + + factory ApiResponse.fail(String message, [String? code]) { + return ApiResponse( + success: false, + message: message, + code: code, + ); + } + + factory ApiResponse.unauthorized(String message) { + return ApiResponse( + success: false, + message: message, + code: ResponseCode.unauthorized, + ); + } + + factory ApiResponse.fromJson( + Map json, + T Function(dynamic)? fromJsonT, + ) { + final code = json['code'] as String? ?? ''; + final msg = json['msg'] as String? ?? ''; + + return switch (code) { + ResponseCode.success => _parseSuccess(json, msg, fromJsonT), + ResponseCode.unauthorized => ApiResponse.unauthorized(msg), + _ => ApiResponse.fail(msg, code), + }; + } + + static ApiResponse _parseSuccess( + Map json, + String msg, + T Function(dynamic)? fromJsonT, + ) { + final data = json['data']; + if (fromJsonT != null && data != null) { + return ApiResponse.success(fromJsonT(data), msg); + } + return ApiResponse.success(data as T, msg); + } + + bool get isSuccess => success; + bool get isUnauthorized => code == ResponseCode.unauthorized; +} diff --git a/flutter_monisuo/lib/core/network/dio_client.dart b/flutter_monisuo/lib/core/network/dio_client.dart index 8ce3a08..48faecf 100644 --- a/flutter_monisuo/lib/core/network/dio_client.dart +++ b/flutter_monisuo/lib/core/network/dio_client.dart @@ -1,65 +1,13 @@ import 'package:dio/dio.dart'; import '../storage/local_storage.dart'; import 'api_exception.dart'; +import 'api_response.dart'; -/// API 响应模型 -class ApiResponse { - final bool success; - final String? message; - final T? data; - final String? code; - - ApiResponse({ - required this.success, - this.message, - this.data, - this.code, - }); - - factory ApiResponse.success(T data, [String? message]) { - return ApiResponse( - success: true, - data: data, - message: message, - code: '0000', - ); - } - - factory ApiResponse.fail(String message, [String? code]) { - return ApiResponse( - success: false, - message: message, - code: code, - ); - } - - factory ApiResponse.unauthorized(String message) { - return ApiResponse( - success: false, - message: message, - code: '0002', - ); - } - - factory ApiResponse.fromJson( - Map json, - T Function(dynamic)? fromJsonT, - ) { - final code = json['code'] as String? ?? ''; - final msg = json['msg'] as String? ?? ''; - - if (code == '0000') { - final data = json['data']; - if (fromJsonT != null && data != null) { - return ApiResponse.success(fromJsonT(data), msg); - } - return ApiResponse.success(data as T, msg); - } else if (code == '0002') { - return ApiResponse.unauthorized(msg); - } else { - return ApiResponse.fail(msg, code); - } - } +/// 网络配置常量 +class NetworkConfig { + static const String baseUrl = 'http://localhost:5010'; + static const Duration connectTimeout = Duration(seconds: 30); + static const Duration receiveTimeout = Duration(seconds: 30); } /// Dio 网络客户端 @@ -67,23 +15,30 @@ class DioClient { late final Dio _dio; DioClient() { - _dio = Dio(BaseOptions( - baseUrl: 'http://localhost:5010', - connectTimeout: const Duration(seconds: 30), - receiveTimeout: const Duration(seconds: 30), - headers: { - 'Content-Type': 'application/json', - }, - )); + _dio = _createDio(); + _setupInterceptors(); + } - _dio.interceptors.add(_AuthInterceptor()); - _dio.interceptors.add(LogInterceptor( - requestHeader: false, - responseHeader: false, - error: true, + Dio _createDio() { + return Dio(BaseOptions( + baseUrl: NetworkConfig.baseUrl, + connectTimeout: NetworkConfig.connectTimeout, + receiveTimeout: NetworkConfig.receiveTimeout, + headers: {'Content-Type': 'application/json'}, )); } + void _setupInterceptors() { + _dio.interceptors.addAll([ + _AuthInterceptor(), + LogInterceptor( + requestHeader: false, + responseHeader: false, + error: true, + ), + ]); + } + /// GET 请求 Future> get( String path, { @@ -112,7 +67,6 @@ class DioClient { } } - /// 处理响应 ApiResponse _handleResponse( Response response, T Function(dynamic)? fromJson, @@ -124,13 +78,39 @@ class DioClient { return ApiResponse.fail('响应数据格式错误'); } - /// 处理错误 ApiResponse _handleError(DioException e) { - if (e.response?.statusCode == 401) { - LocalStorage.clearUserData(); + if (_isUnauthorized(e)) { + _clearUserData(); return ApiResponse.unauthorized('登录已过期,请重新登录'); } - return ApiResponse.fail(e.message ?? '网络请求失败'); + + final message = _getErrorMessage(e); + return ApiResponse.fail(message); + } + + bool _isUnauthorized(DioException e) { + return e.response?.statusCode == 401; + } + + void _clearUserData() { + LocalStorage.clearUserData(); + } + + String _getErrorMessage(DioException e) { + switch (e.type) { + case DioExceptionType.connectionTimeout: + return '连接超时,请检查网络'; + case DioExceptionType.sendTimeout: + return '发送超时,请重试'; + case DioExceptionType.receiveTimeout: + return '响应超时,请重试'; + case DioExceptionType.connectionError: + return '网络连接失败'; + case DioExceptionType.badResponse: + return '服务器错误 (${e.response?.statusCode})'; + default: + return e.message ?? '网络请求失败'; + } } } @@ -139,7 +119,7 @@ class _AuthInterceptor extends Interceptor { @override void onRequest(RequestOptions options, RequestInterceptorHandler handler) { final token = LocalStorage.getToken(); - if (token != null && token.isNotEmpty) { + if (token?.isNotEmpty == true) { options.headers['Authorization'] = 'Bearer $token'; } super.onRequest(options, handler); diff --git a/flutter_monisuo/lib/main.dart b/flutter_monisuo/lib/main.dart index ec3dafe..a8b73da 100644 --- a/flutter_monisuo/lib/main.dart +++ b/flutter_monisuo/lib/main.dart @@ -19,12 +19,9 @@ import 'ui/pages/main/main_page.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); - - // 禁用 Provider 类型检查 Provider.debugCheckInvalidValueType = null; - // 初始化本地存储 - final prefs = await SharedPreferences.getInstance(); + await SharedPreferences.getInstance(); await LocalStorage.init(); runApp(const MyApp()); @@ -36,84 +33,117 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MultiProvider( - providers: [ - // 服务 - Provider(create: (_) => DioClient()), - ProxyProvider( - create: (_) => UserService(DioClient()), - update: (_, client, previous) => previous ?? UserService(client), + providers: _buildProviders(), + child: AuthNavigator( + child: ShadApp.custom( + themeMode: ThemeMode.dark, + darkTheme: ShadThemeData( + brightness: Brightness.dark, + colorScheme: const ShadSlateColorScheme.dark(), + ), + appBuilder: _buildMaterialApp, ), - ProxyProvider( - create: (_) => MarketService(DioClient()), - update: (_, client, previous) => previous ?? MarketService(client), - ), - ProxyProvider( - create: (_) => TradeService(DioClient()), - update: (_, client, previous) => previous ?? TradeService(client), - ), - ProxyProvider( - create: (_) => AssetService(DioClient()), - update: (_, client, previous) => previous ?? AssetService(client), - ), - ProxyProvider( - create: (_) => FundService(DioClient()), - update: (_, client, previous) => previous ?? FundService(client), - ), - // 状态管理 - ProxyProvider2( - create: (_) => AuthProvider(UserService(DioClient())), - update: (_, userService, __, previous) => - previous ?? AuthProvider(userService), - ), - ProxyProvider( - create: (_) => MarketProvider(MarketService(DioClient())), - update: (_, service, previous) => - previous ?? MarketProvider(service), - ), - ProxyProvider2( - create: (_) => - AssetProvider(AssetService(DioClient()), FundService(DioClient())), - update: (_, assetService, fundService, previous) => - previous ?? AssetProvider(assetService, fundService), - ), - ], - child: ShadApp.custom( - themeMode: ThemeMode.dark, - darkTheme: ShadThemeData( - brightness: Brightness.dark, - colorScheme: const ShadSlateColorScheme.dark(), - ), - appBuilder: (context) { - return MaterialApp( - debugShowCheckedModeBanner: false, - theme: Theme.of(context), - localizationsDelegates: const [ - GlobalShadLocalizations.delegate, - GlobalMaterialLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - ], - builder: (context, child) { - return ShadAppBuilder(child: child!); - }, - home: Consumer( - builder: (context, auth, _) { - if (auth.isLoading) { - return const Scaffold( - body: Center( - child: CircularProgressIndicator(), - ), - ); - } - if (auth.isLoggedIn) { - return const MainPage(); - } - return const LoginPage(); - }, - ), - ); - }, ), ); } + + List _buildProviders() { + final dioClient = DioClient(); + + return [ + // Services + Provider.value(value: dioClient), + Provider(create: (_) => UserService(dioClient)), + Provider(create: (_) => MarketService(dioClient)), + Provider(create: (_) => TradeService(dioClient)), + Provider(create: (_) => AssetService(dioClient)), + Provider(create: (_) => FundService(dioClient)), + // State Management + ChangeNotifierProvider( + create: (ctx) => AuthProvider(ctx.read()), + ), + ChangeNotifierProvider( + create: (ctx) => MarketProvider(ctx.read()), + ), + ChangeNotifierProvider( + create: (ctx) => AssetProvider( + ctx.read(), + ctx.read(), + ), + ), + ]; + } + + Widget _buildMaterialApp(BuildContext context) { + return MaterialApp( + debugShowCheckedModeBanner: false, + theme: Theme.of(context), + localizationsDelegates: const [ + GlobalShadLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ], + builder: (context, child) => ShadAppBuilder(child: child!), + home: _buildHome(), + ); + } + + Widget _buildHome() { + return Consumer( + builder: (context, auth, _) { + if (auth.isLoading) { + return const Scaffold( + body: Center(child: CircularProgressIndicator()), + ); + } + return auth.isLoggedIn ? const MainPage() : const LoginPage(); + }, + ); + } +} + +/// 认证路由守卫 - 监听认证状态并自动导航 +class AuthNavigator extends StatefulWidget { + final Widget child; + + const AuthNavigator({super.key, required this.child}); + + @override + State createState() => _AuthNavigatorState(); +} + +class _AuthNavigatorState extends State { + bool? _wasLoggedIn; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + final isLoggedIn = context.watch().isLoggedIn; + + if (_wasLoggedIn == null) { + _wasLoggedIn = isLoggedIn; + return; + } + + if (_wasLoggedIn != isLoggedIn) { + _wasLoggedIn = isLoggedIn; + _navigateToAuthPage(isLoggedIn); + } + } + + void _navigateToAuthPage(bool isLoggedIn) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + Navigator.of(context).pushAndRemoveUntil( + MaterialPageRoute( + builder: (_) => isLoggedIn ? const MainPage() : const LoginPage(), + ), + (route) => false, + ); + }); + } + + @override + Widget build(BuildContext context) => widget.child; } diff --git a/flutter_monisuo/lib/providers/auth_provider.dart b/flutter_monisuo/lib/providers/auth_provider.dart index b017755..c00e1df 100644 --- a/flutter_monisuo/lib/providers/auth_provider.dart +++ b/flutter_monisuo/lib/providers/auth_provider.dart @@ -1,4 +1,3 @@ -import 'dart:convert'; import 'package:flutter/material.dart'; import '../core/network/dio_client.dart'; import '../core/storage/local_storage.dart'; @@ -15,7 +14,7 @@ class AuthProvider extends ChangeNotifier { String? _token; AuthProvider(this._userService) { - _checkAuth(); + _initAuth(); } // Getters @@ -24,100 +23,81 @@ class AuthProvider extends ChangeNotifier { bool get isLoading => _isLoading; String? get token => _token; - /// 检查登录状态 - Future _checkAuth() async { - _isLoading = true; - notifyListeners(); - + /// 初始化认证状态 + Future _initAuth() async { _token = LocalStorage.getToken(); - _isLoggedIn = _token != null && _token!.isNotEmpty; + _isLoggedIn = _token?.isNotEmpty == true; if (_isLoggedIn) { - final userJson = LocalStorage.getUserInfo(); - if (userJson != null) { - _user = User.fromJson(userJson); - } + _user = _loadUserFromStorage(); } - - _isLoading = false; notifyListeners(); } + User? _loadUserFromStorage() { + final userJson = LocalStorage.getUserInfo(); + return userJson != null ? User.fromJson(userJson) : null; + } + /// 登录 - Future> login(String username, String password) async { - _isLoading = true; - notifyListeners(); - - try { - final response = await _userService.login(username, password); - - if (response.success && response.data != null) { - _token = response.data!['token'] as String?; - final userJson = response.data!['user'] as Map? ?? - response.data!['userInfo'] as Map?; - - if (_token != null) { - await LocalStorage.saveToken(_token!); - } - if (userJson != null) { - await LocalStorage.saveUserInfo(userJson); - _user = User.fromJson(userJson); - } - - _isLoggedIn = true; - notifyListeners(); - return ApiResponse.success(_user!, response.message); - } - - _isLoading = false; - notifyListeners(); - return ApiResponse.fail(response.message ?? '登录失败'); - } catch (e) { - _isLoading = false; - notifyListeners(); - return ApiResponse.fail('登录失败: $e'); - } + Future> login(String username, String password) { + return _authenticate(() => _userService.login(username, password)); } /// 注册 - Future> register(String username, String password) async { - _isLoading = true; - notifyListeners(); + Future> register(String username, String password) { + return _authenticate(() => _userService.register(username, password)); + } + + /// 统一认证处理 + Future> _authenticate( + Future>> Function() action, + ) async { + _setLoading(true); try { - final response = await _userService.register(username, password); + final response = await action(); - if (response.success && response.data != null) { - _token = response.data!['token'] as String?; - final userJson = response.data!['userInfo'] as Map?; - - if (_token != null) { - await LocalStorage.saveToken(_token!); - } - if (userJson != null) { - await LocalStorage.saveUserInfo(userJson); - _user = User.fromJson(userJson); - } - - _isLoggedIn = true; - notifyListeners(); - return ApiResponse.success(_user!, response.message); + if (!response.success || response.data == null) { + return ApiResponse.fail(response.message ?? '操作失败'); } - _isLoading = false; - notifyListeners(); - return ApiResponse.fail(response.message ?? '注册失败'); + return _handleAuthSuccess(response.data!, response.message); } catch (e) { - _isLoading = false; - notifyListeners(); - return ApiResponse.fail('注册失败: $e'); + return ApiResponse.fail('操作失败: $e'); + } finally { + _setLoading(false); } } + /// 处理认证成功 + ApiResponse _handleAuthSuccess( + Map data, + String? message, + ) { + _token = data['token'] as String?; + final userJson = data['user'] as Map? ?? + data['userInfo'] as Map?; + + if (_token != null) { + LocalStorage.saveToken(_token!); + } + + if (userJson != null) { + LocalStorage.saveUserInfo(userJson); + _user = User.fromJson(userJson); + } + + _isLoggedIn = true; + + return _user != null + ? ApiResponse.success(_user!, message) + : ApiResponse.fail('用户信息获取失败'); + } + /// 退出登录 Future logout() async { - _isLoading = true; - notifyListeners(); + _setLoading(true); try { await _userService.logout(); @@ -125,12 +105,15 @@ class AuthProvider extends ChangeNotifier { // 忽略退出登录的接口错误 } - await LocalStorage.clearUserData(); + _clearAuthState(); + _setLoading(false); + } + + void _clearAuthState() { + LocalStorage.clearUserData(); _user = null; _token = null; _isLoggedIn = false; - _isLoading = false; - notifyListeners(); } /// 刷新用户信息 @@ -148,4 +131,9 @@ class AuthProvider extends ChangeNotifier { // 忽略错误 } } + + void _setLoading(bool value) { + _isLoading = value; + notifyListeners(); + } } diff --git a/flutter_monisuo/lib/ui/pages/auth/login_page.dart b/flutter_monisuo/lib/ui/pages/auth/login_page.dart index 3f08f77..b88fe22 100644 --- a/flutter_monisuo/lib/ui/pages/auth/login_page.dart +++ b/flutter_monisuo/lib/ui/pages/auth/login_page.dart @@ -3,6 +3,7 @@ import 'package:shadcn_ui/shadcn_ui.dart'; import 'package:provider/provider.dart'; import '../../../providers/auth_provider.dart'; +import '../main/main_page.dart'; import 'register_page.dart'; class LoginPage extends StatefulWidget { @@ -15,6 +16,10 @@ class LoginPage extends StatefulWidget { class _LoginPageState extends State { final formKey = GlobalKey(); + static const _maxFormWidth = 400.0; + static const _logoSize = 64.0; + static const _loadingIndicatorSize = 16.0; + @override Widget build(BuildContext context) { final theme = ShadTheme.of(context); @@ -22,7 +27,7 @@ class _LoginPageState extends State { return Scaffold( body: Center( child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 400), + constraints: const BoxConstraints(maxWidth: _maxFormWidth), child: Padding( padding: const EdgeInsets.all(24), child: ShadForm( @@ -31,135 +36,15 @@ class _LoginPageState extends State { mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - // Logo 和标题 - Icon( - LucideIcons.trendingUp, - size: 64, - color: theme.colorScheme.primary, - ), - const SizedBox(height: 24), - Text( - '模拟所', - style: theme.textTheme.h1, - textAlign: TextAlign.center, - ), - const SizedBox(height: 8), - Text( - '虚拟货币模拟交易平台', - style: theme.textTheme.muted, - textAlign: TextAlign.center, - ), + _buildHeader(theme), const SizedBox(height: 48), - - // 用户名输入 - ShadInputFormField( - id: 'username', - label: const Text('用户名'), - placeholder: const Text('请输入用户名'), - leading: const Icon(LucideIcons.user), - validator: (value) { - if (value == null || value.isEmpty) { - return '请输入用户名'; - } - if (value.length < 3) { - return '用户名至少 3 个字符'; - } - return null; - }, - ), + _buildUsernameField(), const SizedBox(height: 16), - - // 密码输入 - ShadInputFormField( - id: 'password', - label: const Text('密码'), - placeholder: const Text('请输入密码'), - obscureText: true, - leading: const Icon(LucideIcons.lock), - validator: (value) { - if (value == null || value.isEmpty) { - return '请输入密码'; - } - if (value.length < 6) { - return '密码至少 6 个字符'; - } - return null; - }, - ), + _buildPasswordField(), const SizedBox(height: 24), - - // 登录按钮 - Consumer( - builder: (context, auth, _) { - return ShadButton( - onPressed: auth.isLoading - ? null - : () async { - if (formKey.currentState!.saveAndValidate()) { - final values = formKey.currentState!.value; - final response = await auth.login( - values['username'], - values['password'], - ); - - // 登录成功后,Provider 会自动更新状态 - // MaterialApp 的 Consumer 会自动切换到 MainPage - if (!response.success && mounted) { - // 只在失败时显示错误 - showShadDialog( - context: context, - builder: (context) => ShadDialog.alert( - title: const Text('登录失败'), - description: Text( - response.message ?? '用户名或密码错误', - ), - actions: [ - ShadButton( - child: const Text('确定'), - onPressed: () => - Navigator.of(context).pop(), - ), - ], - ), - ); - } - } - }, - child: auth.isLoading - ? const SizedBox.square( - dimension: 16, - child: CircularProgressIndicator( - strokeWidth: 2, - color: Colors.white, - ), - ) - : const Text('登录'), - ); - }, - ), + _buildLoginButton(), const SizedBox(height: 16), - - // 注册链接 - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - '还没有账号?', - style: theme.textTheme.muted, - ), - ShadButton.link( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => RegisterPage(), - ), - ); - }, - child: const Text('立即注册'), - ), - ], - ), + _buildRegisterLink(theme), ], ), ), @@ -168,4 +53,154 @@ class _LoginPageState extends State { ), ); } + + Widget _buildHeader(ShadThemeData theme) { + return Column( + children: [ + Icon( + LucideIcons.trendingUp, + size: _logoSize, + color: theme.colorScheme.primary, + ), + const SizedBox(height: 24), + Text( + '模拟所', + style: theme.textTheme.h1, + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + Text( + '虚拟货币模拟交易平台', + style: theme.textTheme.muted, + textAlign: TextAlign.center, + ), + ], + ); + } + + Widget _buildUsernameField() { + return ShadInputFormField( + id: 'username', + label: const Text('用户名'), + placeholder: const Text('请输入用户名'), + leading: const Icon(LucideIcons.user), + validator: _validateUsername, + ); + } + + Widget _buildPasswordField() { + return ShadInputFormField( + id: 'password', + label: const Text('密码'), + placeholder: const Text('请输入密码'), + obscureText: true, + leading: const Icon(LucideIcons.lock), + validator: _validatePassword, + ); + } + + Widget _buildLoginButton() { + return Consumer( + builder: (context, auth, _) { + return ShadButton( + onPressed: auth.isLoading ? null : () => _handleLogin(auth), + child: auth.isLoading + ? const SizedBox.square( + dimension: _loadingIndicatorSize, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.white, + ), + ) + : const Text('登录'), + ); + }, + ); + } + + Widget _buildRegisterLink(ShadThemeData theme) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + '还没有账号?', + style: theme.textTheme.muted, + ), + ShadButton.link( + onPressed: _navigateToRegister, + child: const Text('立即注册'), + ), + ], + ); + } + + // Validators + String? _validateUsername(String? value) { + if (value == null || value.isEmpty) { + return '请输入用户名'; + } + if (value.length < 3) { + return '用户名至少 3 个字符'; + } + return null; + } + + String? _validatePassword(String? value) { + if (value == null || value.isEmpty) { + return '请输入密码'; + } + if (value.length < 6) { + return '密码至少 6 个字符'; + } + return null; + } + + // Actions + Future _handleLogin(AuthProvider auth) async { + if (!formKey.currentState!.saveAndValidate()) return; + + final values = formKey.currentState!.value; + final response = await auth.login( + values['username'], + values['password'], + ); + + if (!mounted) return; + + if (response.success) { + _navigateToMainPage(); + } else { + _showErrorDialog(response.message ?? '用户名或密码错误'); + } + } + + void _navigateToMainPage() { + Navigator.of(context).pushAndRemoveUntil( + MaterialPageRoute(builder: (_) => const MainPage()), + (route) => false, + ); + } + + void _navigateToRegister() { + Navigator.push( + context, + MaterialPageRoute(builder: (context) => const RegisterPage()), + ); + } + + void _showErrorDialog(String message) { + showShadDialog( + context: context, + builder: (context) => ShadDialog.alert( + title: const Text('登录失败'), + description: Text(message), + actions: [ + ShadButton( + child: const Text('确定'), + onPressed: () => Navigator.of(context).pop(), + ), + ], + ), + ); + } } diff --git a/flutter_monisuo/lib/ui/pages/mine/mine_page.dart b/flutter_monisuo/lib/ui/pages/mine/mine_page.dart index 0175f75..c103982 100644 --- a/flutter_monisuo/lib/ui/pages/mine/mine_page.dart +++ b/flutter_monisuo/lib/ui/pages/mine/mine_page.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:shadcn_ui/shadcn_ui.dart'; import 'package:provider/provider.dart'; import '../../../providers/auth_provider.dart'; +import '../auth/login_page.dart'; /// 我的页面 - 使用 shadcn_ui 现代化设计 class MinePage extends StatefulWidget { @@ -275,8 +276,14 @@ class _MinePageState extends State with AutomaticKeepAliveClientMixin onPressed: () async { Navigator.of(context).pop(); await auth.logout(); + // 登出成功,直接导航到登录页 if (context.mounted) { - Navigator.pushReplacementNamed(context, '/login'); + Navigator.of(context).pushAndRemoveUntil( + MaterialPageRoute( + builder: (_) => const LoginPage(), + ), + (route) => false, + ); } }, ), diff --git a/src/main/java/com/it/rattan/config/WebConfig.java b/src/main/java/com/it/rattan/config/WebConfig.java index e6ed198..b2eade3 100644 --- a/src/main/java/com/it/rattan/config/WebConfig.java +++ b/src/main/java/com/it/rattan/config/WebConfig.java @@ -1,13 +1,17 @@ package com.it.rattan.config; +import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.web.cors.CorsConfiguration; -import org.springframework.web.cors.UrlBasedCorsConfigurationSource; -import org.springframework.web.filter.CorsFilter; +import org.springframework.core.Ordered; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import javax.servlet.*; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + @Configuration public class WebConfig implements WebMvcConfigurer { @@ -17,24 +21,46 @@ public class WebConfig implements WebMvcConfigurer { } /** - * 跨域配置 + * 跨域过滤器 - 支持凭证,最高优先级 */ @Bean - public CorsFilter corsFilter() { - CorsConfiguration config = new CorsConfiguration(); - // 允许所有来源 - config.addAllowedOrigin("*"); - // 允许所有请求头 - config.addAllowedHeader("*"); - // 允许所有请求方法 - config.addAllowedMethod("*"); - // 允许携带凭证 - config.setAllowCredentials(true); - // 预检请求缓存时间 - config.setMaxAge(3600L); + public FilterRegistrationBean corsFilterRegistration() { + FilterRegistrationBean registration = new FilterRegistrationBean<>(); + registration.setFilter(new CorsFilter()); + registration.addUrlPatterns("/*"); + registration.setName("corsFilter"); + registration.setOrder(Ordered.HIGHEST_PRECEDENCE); + return registration; + } - UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); - source.registerCorsConfiguration("/**", config); - return new CorsFilter(source); + /** + * CORS 过滤器实现 + */ + private static class CorsFilter implements Filter { + @Override + public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) + throws IOException, ServletException { + HttpServletResponse response = (HttpServletResponse) res; + HttpServletRequest request = (HttpServletRequest) req; + + String origin = request.getHeader("Origin"); + if (origin != null) { + response.setHeader("Access-Control-Allow-Origin", origin); + } else { + response.setHeader("Access-Control-Allow-Origin", "*"); + } + response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS"); + response.setHeader("Access-Control-Allow-Headers", "*"); + response.setHeader("Access-Control-Allow-Credentials", "true"); + response.setHeader("Access-Control-Expose-Headers", "Authorization"); + response.setHeader("Access-Control-Max-Age", "3600"); + + if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { + response.setStatus(HttpServletResponse.SC_OK); + return; + } + + chain.doFilter(req, res); + } } }