diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 51c97f5a..49619f72 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,148 +12,27 @@ on: description: 'Enable debug mode' required: false default: false - full_run: - type: boolean - description: 'Run all heavy jobs on manual dispatch' - required: false - default: false - -permissions: - contents: read - pull-requests: write env: FLUTTER_VERSION: '3.35.3' RUST_VERSION: '1.89.0' - # Docker Hub credentials - optional but recommended to avoid rate limits - DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} - DOCKER_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} - -concurrency: - group: core-ci-${{ github.ref }}-${{ github.event_name }} - cancel-in-progress: true jobs: - changes: - name: Detect Changes - runs-on: ubuntu-latest - outputs: - docs_only: ${{ steps.out.outputs.docs_only }} - flutter_changed: ${{ steps.out.outputs.flutter_changed }} - steps: - - uses: actions/checkout@v4 - - name: Paths filter - id: filter - uses: dorny/paths-filter@v3 - with: - token: ${{ secrets.GITHUB_TOKEN }} - filters: | - docs: - - '**/*.md' - - 'docs/**' - - 'README.md' - - 'AGENTS.md' - - '.github/*.md' - - 'PR_DESCRIPTIONS/**' - code: - - '!**/*.md' - flutter: - - 'jive-flutter/**' - - 'jive-flutter/pubspec.yaml' - - name: Set outputs - id: out - run: | - if [ "${{ steps.filter.outputs.docs }}" = "true" ] && [ "${{ steps.filter.outputs.code }}" != "true" ]; then - echo "docs_only=true" >> "$GITHUB_OUTPUT" - else - echo "docs_only=false" >> "$GITHUB_OUTPUT" - fi - echo "flutter_changed=${{ steps.filter.outputs.flutter }}" >> "$GITHUB_OUTPUT" - rustfmt-check: - name: Rustfmt Check - runs-on: ubuntu-latest - timeout-minutes: 10 - continue-on-error: false - needs: [changes] - env: - DOCS_ONLY: ${{ needs.changes.outputs.docs_only }} - - steps: - - uses: actions/checkout@v4 - - name: Setup Rust - uses: dtolnay/rust-toolchain@stable - with: - toolchain: ${{ env.RUST_VERSION }} - components: rustfmt - - name: Check formatting (jive-api) - if: env.DOCS_ONLY != 'true' && (github.event_name != 'workflow_dispatch' || inputs.full_run == 'true') - working-directory: jive-api - run: | - cargo fmt --all -- --check - - - name: Check formatting (jive-core) - if: env.DOCS_ONLY != 'true' && (github.event_name != 'workflow_dispatch' || inputs.full_run == 'true') - working-directory: jive-core - run: | - cargo fmt --all -- --check - - cargo-deny: - name: Cargo Deny Check - runs-on: ubuntu-latest - timeout-minutes: 10 - continue-on-error: false - needs: [changes] - env: - DOCS_ONLY: ${{ needs.changes.outputs.docs_only }} - - steps: - - uses: actions/checkout@v4 - - name: Install cargo-deny - if: env.DOCS_ONLY != 'true' && (github.event_name != 'workflow_dispatch' || inputs.full_run == 'true') - run: | - curl -sSfL https://github.com/EmbarkStudios/cargo-deny/releases/download/0.14.24/cargo-deny-0.14.24-x86_64-unknown-linux-musl.tar.gz | tar xz - sudo mv cargo-deny*/cargo-deny /usr/local/bin/cargo-deny - cargo-deny --version - - name: Run cargo-deny (API) - if: env.DOCS_ONLY != 'true' && (github.event_name != 'workflow_dispatch' || inputs.full_run == 'true') - working-directory: jive-api - run: | - set -o pipefail - cargo-deny check -c ../deny.toml 2>&1 | tee ../cargo-deny-output.txt || true - - - name: Upload cargo-deny output - if: always() - uses: actions/upload-artifact@v4 - with: - name: cargo-deny-output - path: cargo-deny-output.txt flutter-test: name: Flutter Tests runs-on: ubuntu-latest - timeout-minutes: 20 - continue-on-error: false - needs: [changes] - env: - DOCS_ONLY: ${{ needs.changes.outputs.docs_only }} - FLUTTER_CHANGED: ${{ needs.changes.outputs.flutter_changed }} + continue-on-error: true steps: - uses: actions/checkout@v4 - - name: No Flutter changes detected (short-circuit) - if: env.DOCS_ONLY != 'true' && env.FLUTTER_CHANGED != 'true' - run: | - echo "No Flutter changes; skipping analysis/tests to keep pipeline efficient." - - name: Setup Flutter - if: env.DOCS_ONLY != 'true' && env.FLUTTER_CHANGED == 'true' && (github.event_name != 'workflow_dispatch' || inputs.full_run == 'true') uses: subosito/flutter-action@v2 with: flutter-version: ${{ env.FLUTTER_VERSION }} channel: 'stable' - name: Cache Flutter dependencies - if: env.DOCS_ONLY != 'true' && env.FLUTTER_CHANGED == 'true' && (github.event_name != 'workflow_dispatch' || inputs.full_run == 'true') uses: actions/cache@v4 with: path: | @@ -165,18 +44,15 @@ jobs: ${{ runner.os }}-flutter- - name: Install dependencies - if: env.DOCS_ONLY != 'true' && env.FLUTTER_CHANGED == 'true' && (github.event_name != 'workflow_dispatch' || inputs.full_run == 'true') working-directory: jive-flutter run: flutter pub get - name: Generate code (build_runner) - if: env.DOCS_ONLY != 'true' && env.FLUTTER_CHANGED == 'true' && (github.event_name != 'workflow_dispatch' || inputs.full_run == 'true') working-directory: jive-flutter run: | flutter pub run build_runner build --delete-conflicting-outputs || true - name: Analyze code (non-fatal for now) - if: env.DOCS_ONLY != 'true' && env.FLUTTER_CHANGED == 'true' && (github.event_name != 'workflow_dispatch' || inputs.full_run == 'true') working-directory: jive-flutter run: | set -o pipefail @@ -184,14 +60,13 @@ jobs: flutter analyze --no-fatal-warnings 2>&1 | tee ../flutter-analyze-output.txt || true - name: Upload analyzer output - if: always() && env.FLUTTER_CHANGED == 'true' + if: always() uses: actions/upload-artifact@v4 with: name: flutter-analyze-output path: flutter-analyze-output.txt - name: Run tests - if: env.DOCS_ONLY != 'true' && env.FLUTTER_CHANGED == 'true' && (github.event_name != 'workflow_dispatch' || inputs.full_run == 'true') working-directory: jive-flutter run: | # Generate machine-readable test results (non-fatal for reporting) @@ -204,7 +79,7 @@ jobs: flutter test test/settings_manual_overrides_navigation_test.dart --machine > ../flutter-widget-manual-overrides.json || true - name: Upload manual-overrides widget test output - if: always() && env.FLUTTER_CHANGED == 'true' + if: always() uses: actions/upload-artifact@v4 with: name: flutter-manual-overrides-widget @@ -212,7 +87,7 @@ jobs: if-no-files-found: ignore - name: Generate test report - if: always() && env.FLUTTER_CHANGED == 'true' + if: always() working-directory: jive-flutter run: | echo "# Flutter Test Report" > ../test-report.md @@ -236,7 +111,7 @@ jobs: fi - name: Upload test report - if: always() && env.FLUTTER_CHANGED == 'true' + if: always() uses: actions/upload-artifact@v4 with: name: test-report @@ -245,10 +120,6 @@ jobs: rust-test: name: Rust API Tests runs-on: ubuntu-latest - timeout-minutes: 20 - needs: [changes] - env: - DOCS_ONLY: ${{ needs.changes.outputs.docs_only }} services: postgres: @@ -278,23 +149,13 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Login to Docker Hub - if: env.DOCKER_USERNAME != '' && env.DOCKER_TOKEN != '' - uses: docker/login-action@v3 - with: - username: ${{ env.DOCKER_USERNAME }} - password: ${{ env.DOCKER_TOKEN }} - continue-on-error: true - - name: Setup Rust - if: env.DOCS_ONLY != 'true' uses: dtolnay/rust-toolchain@stable with: toolchain: ${{ env.RUST_VERSION }} components: rustfmt, clippy - name: Cache Rust dependencies - if: env.DOCS_ONLY != 'true' uses: actions/cache@v4 with: path: | @@ -366,7 +227,7 @@ jobs: exit 1 - name: Comment SQLx diff summary to PR - if: steps.sqlx_check.outcome == 'failure' && github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository + if: steps.sqlx_check.outcome == 'failure' && github.event_name == 'pull_request' uses: actions/github-script@v7 with: script: | @@ -494,7 +355,6 @@ jobs: rust-core-check: name: Rust Core Dual Mode Check runs-on: ubuntu-latest - timeout-minutes: 20 # Restored to blocking mode (fail-fast: true) continue-on-error: false services: @@ -519,14 +379,6 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Login to Docker Hub - if: env.DOCKER_USERNAME != '' && env.DOCKER_TOKEN != '' - uses: docker/login-action@v3 - with: - username: ${{ env.DOCKER_USERNAME }} - password: ${{ env.DOCKER_TOKEN }} - continue-on-error: true - - name: Setup Rust uses: dtolnay/rust-toolchain@stable with: @@ -572,11 +424,7 @@ jobs: field-compare: name: Field Comparison Check runs-on: ubuntu-latest - timeout-minutes: 10 - needs: [changes, flutter-test, rust-test] - if: needs.changes.outputs.flutter_changed == 'true' - env: - FLUTTER_CHANGED: ${{ needs.changes.outputs.flutter_changed }} + needs: [flutter-test, rust-test] steps: - uses: actions/checkout@v4 @@ -586,17 +434,15 @@ jobs: with: name: test-report path: . - if-no-files-found: ignore - name: Download Flutter analyzer output uses: actions/download-artifact@v4 with: name: flutter-analyze-output path: . - if-no-files-found: ignore - name: Upload analyzer output for comparison - if: always() && env.FLUTTER_CHANGED == 'true' + if: always() uses: actions/upload-artifact@v4 with: name: flutter-analyze-output-comparison @@ -608,7 +454,6 @@ jobs: sudo apt-get install -y jq - name: Compare Flutter and Rust fields - if: env.FLUTTER_CHANGED == 'true' run: | echo "# Field Comparison Report" > field-compare-report.md echo "## Flutter vs Rust Model Comparison" >> field-compare-report.md @@ -647,19 +492,63 @@ jobs: fi - name: Upload field comparison report - if: always() && env.FLUTTER_CHANGED == 'true' + if: always() uses: actions/upload-artifact@v4 with: name: field-compare-report path: field-compare-report.md if-no-files-found: ignore - + cargo-deny: + name: Cargo Deny Check + runs-on: ubuntu-latest + continue-on-error: true # Non-blocking initially + steps: + - uses: actions/checkout@v4 + + - name: Install cargo-deny + run: cargo install cargo-deny --locked + + - name: Run cargo-deny + working-directory: jive-api + run: | + cargo deny check 2>&1 | tee ../cargo-deny-output.txt || true + + - name: Upload cargo-deny output + if: always() + uses: actions/upload-artifact@v4 + with: + name: cargo-deny-output + path: cargo-deny-output.txt + + rustfmt-check: + name: Rustfmt Check + runs-on: ubuntu-latest + continue-on-error: true # Non-blocking initially + steps: + - uses: actions/checkout@v4 + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + with: + toolchain: ${{ env.RUST_VERSION }} + components: rustfmt + + - name: Run rustfmt check + run: | + cd jive-api && cargo fmt --all -- --check 2>&1 | tee ../rustfmt-output.txt || true + cd ../jive-core && cargo fmt --all -- --check 2>&1 | tee -a ../rustfmt-output.txt || true + + - name: Upload rustfmt output + if: always() + uses: actions/upload-artifact@v4 + with: + name: rustfmt-output + path: rustfmt-output.txt rust-api-clippy: name: Rust API Clippy (blocking) runs-on: ubuntu-latest - timeout-minutes: 10 # Now blocking with -D warnings since we achieved 0 clippy warnings steps: - uses: actions/checkout@v4 @@ -701,11 +590,8 @@ jobs: summary: name: CI Summary runs-on: ubuntu-latest - timeout-minutes: 10 - needs: [changes, flutter-test, rust-test, rust-core-check, field-compare, rust-api-clippy, cargo-deny, rustfmt-check] + needs: [flutter-test, rust-test, rust-core-check, field-compare, rust-api-clippy, cargo-deny, rustfmt-check] if: always() - env: - DOCS_ONLY: ${{ needs.changes.outputs.docs_only }} steps: - name: Download all artifacts @@ -719,18 +605,14 @@ jobs: echo "- Branch: ${{ github.ref_name }}" >> ci-summary.md echo "- Commit: ${{ github.sha }}" >> ci-summary.md echo "" >> ci-summary.md - echo "## Optimization" >> ci-summary.md - echo "- Docs-only fast path: ${DOCS_ONLY}" >> ci-summary.md - echo "" >> ci-summary.md echo "## Test Results" >> ci-summary.md echo "- Flutter Tests: ${{ needs.flutter-test.result }}" >> ci-summary.md echo "- Rust Tests: ${{ needs.rust-test.result }}" >> ci-summary.md echo "- Rust Core Check: ${{ needs.rust-core-check.result }}" >> ci-summary.md - echo "- Rust API Clippy: ${{ needs.rust-api-clippy.result }}" >> ci-summary.md + echo "- Field Comparison: ${{ needs.field-compare.result }}" >> ci-summary.md echo "- Cargo Deny: ${{ needs.cargo-deny.result }}" >> ci-summary.md echo "- Rustfmt Check: ${{ needs.rustfmt-check.result }}" >> ci-summary.md - echo "- Field Comparison: ${{ needs.field-compare.result }}" >> ci-summary.md echo "" >> ci-summary.md if [ -f test-report/test-report.md ]; then @@ -745,15 +627,6 @@ jobs: echo '```' >> ci-summary.md fi - echo "" >> ci-summary.md - echo "## Job Durations (approx)" >> ci-summary.md - echo "- Flutter Tests: ${{ needs.flutter-test.result }}" >> ci-summary.md - echo "- Rust API Tests: ${{ needs.rust-test.result }}" >> ci-summary.md - echo "- Rust Core Check: ${{ needs.rust-core-check.result }}" >> ci-summary.md - echo "- Rust API Clippy: ${{ needs.rust-api-clippy.result }}" >> ci-summary.md - echo "- Cargo Deny: ${{ needs.cargo-deny.result }}" >> ci-summary.md - echo "- Rustfmt Check: ${{ needs.rustfmt-check.result }}" >> ci-summary.md - # Manual overrides tests summary echo "" >> ci-summary.md echo "## Manual Overrides Tests" >> ci-summary.md @@ -779,7 +652,7 @@ jobs: # Rust API Clippy 结果 echo "" >> ci-summary.md - echo "## Rust API Clippy" >> ci-summary.md + echo "## Rust API Clippy (Non-blocking)" >> ci-summary.md echo "- Status: ${{ needs.rust-api-clippy.result }}" >> ci-summary.md echo "- Artifact: api-clippy-output.txt" >> ci-summary.md diff --git a/Makefile b/Makefile index ba69acc4..5f96f818 100644 --- a/Makefile +++ b/Makefile @@ -19,14 +19,6 @@ help: @echo " make logs - 查看日志" @echo " make api-dev - 启动完整版 API (CORS_DEV=1)" @echo " make api-safe - 启动完整版 API (安全CORS模式)" - @echo " make sqlx-prepare-core - 准备 jive-core (server,db) 的 SQLx 元数据" - @echo " make api-dev-core-export - 启动 API 并启用 core_export(走核心导出路径)" - @echo " make db-dev-up - 启动 Docker 开发数据库/Redis/Adminer (15432/16379/19080)" - @echo " make db-dev-down - 停止 Docker 开发数据库/Redis/Adminer" - @echo " make api-dev-docker-db - 本地 API 连接 Docker 开发数据库 (15432)" - @echo " make db-dev-status - 显示 Docker 开发数据库/Redis/Adminer 与 API 端口状态" - @echo " make metrics-check - 基础指标一致性校验 (/health vs /metrics)" - @echo " make seed-bcrypt-user - 插入一个 bcrypt 测试用户 (触发登录重哈希)" # 安装依赖 install: @@ -73,8 +65,8 @@ build-flutter: test: test-rust test-flutter test-rust: - @echo "运行 Rust API 测试 (SQLX_OFFLINE=true)..." - @cd jive-api && SQLX_OFFLINE=true cargo test --tests + @echo "运行 Rust 测试..." + @cd jive-core && cargo test --no-default-features --features server test-flutter: @echo "运行 Flutter 测试..." @@ -152,17 +144,10 @@ api-sqlx-prepare-local: @cd jive-api && cargo install sqlx-cli --no-default-features --features postgres || true @cd jive-api && SQLX_OFFLINE=false cargo sqlx prepare -# Prepare SQLx metadata for jive-core (server,db) -sqlx-prepare-core: - @echo "准备 jive-core SQLx 元数据 (features=server,db)..." - @echo "确保数据库与迁移就绪 (优先 5433)..." - @cd jive-api && DB_PORT=$${DB_PORT:-5433} ./scripts/migrate_local.sh --force || true - @cd jive-core && cargo install sqlx-cli --no-default-features --features postgres || true - @cd jive-core && \ - DATABASE_URL=$${DATABASE_URL:-postgresql://postgres:postgres@localhost:$${DB_PORT:-5433}/jive_money} \ - SQLX_OFFLINE=false cargo sqlx prepare -- --features "server,db" - @echo "✅ 已生成 jive-core/.sqlx 元数据" - +# Enable local git hooks once per clone +hooks: + @git config core.hooksPath .githooks + @echo "✅ Git hooks enabled (pre-commit runs make api-lint)" # 启动完整版 API(宽松 CORS 开发模式,支持自定义端口 API_PORT) api-dev: @@ -173,60 +158,6 @@ api-dev: api-safe: @echo "启动完整版 API (安全 CORS 模式, 端口 $${API_PORT:-8012})..." @cd jive-api && unset CORS_DEV && API_PORT=$${API_PORT:-8012} cargo run --bin jive-api -# 启动完整版 API(宽松 CORS + 启用 core_export,导出走 jive-core Service) -api-dev-core-export: - @echo "启动 API (CORS_DEV=1, 启用 core_export, 端口 $${API_PORT:-8012})..." - @cd jive-api && CORS_DEV=1 API_PORT=$${API_PORT:-8012} cargo run --features core_export --bin jive-api - -# ---- Docker DB + Local API (Dev) ---- -db-dev-up: - @echo "启动 Docker 开发数据库/Redis/Adminer (端口: PG=5433, Redis=6380, Adminer=9080)..." - @cd jive-api && docker-compose -f docker-compose.dev.yml up -d postgres redis adminer - @echo "✅ Postgres: postgresql://postgres:postgres@localhost:5433/jive_money" - @echo "✅ Redis: redis://localhost:6380" - @echo "✅ Adminer: http://localhost:9080" - -db-dev-down: - @echo "停止 Docker 开发数据库/Redis/Adminer..." - @cd jive-api && docker-compose -f docker-compose.dev.yml down - @echo "✅ 已停止" - -api-dev-docker-db: - @echo "本地运行 API (连接 Docker 开发数据库 5433; CORS_DEV=1, SQLX_OFFLINE=true)..." - @cd jive-api && \ - CORS_DEV=1 \ - API_PORT=$${API_PORT:-8012} \ - SQLX_OFFLINE=true \ - RUST_LOG=$${RUST_LOG:-info} \ - DATABASE_URL=$${DATABASE_URL:-postgresql://postgres:postgres@localhost:5433/jive_money} \ - cargo run --bin jive-api - -db-dev-status: - @echo "🔎 Docker 开发栈容器状态 (postgres/redis/adminer):" - @docker ps --format '{{.Names}}\t{{.Status}}\t{{.Ports}}' | grep -E 'jive-(postgres|redis|adminer)-dev' || echo "(未启动)" - @echo "" - @echo "📡 建议的连接信息:" - @echo " - Postgres: postgresql://postgres:postgres@localhost:5433/jive_money" - @echo " - Redis: redis://localhost:6380" - @echo " - Adminer: http://localhost:9080" - @echo "" - @echo "🩺 API (本地) 端口状态:" - @lsof -iTCP:$${API_PORT:-8012} -sTCP:LISTEN 2>/dev/null || echo "(端口 $${API_PORT:-8012} 未监听)" - @echo "" - @echo "🌿 /health:" - @curl -fsS http://localhost:$${API_PORT:-8012}/health 2>/dev/null || echo "(API 未响应)" - -# ---- Metrics & Dev Utilities ---- -metrics-check: - @echo "运行指标一致性脚本..." - @cd jive-api && ./scripts/check_metrics_consistency.sh || true - @echo "抓取 /metrics 关键行:" && curl -fsS http://localhost:$${API_PORT:-8012}/metrics | grep -E 'password_hash_|jive_build_info|export_requests_' || true - -seed-bcrypt-user: - @echo "插入 bcrypt 测试用户 (若不存在)..." - @cd jive-api && cargo run --bin hash_password --quiet -- 'TempBcrypt123!' >/dev/null 2>&1 || true - @psql $${DATABASE_URL:-postgresql://postgres:postgres@localhost:5433/jive_money} -c "DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM users WHERE email='bcrypt_test@example.com') THEN INSERT INTO users (email,password_hash,name,is_active,created_at,updated_at) VALUES ('bcrypt_test@example.com', crypt('TempBcrypt123!','bf'), 'Bcrypt Test', true, NOW(), NOW()); END IF; END $$;" 2>/dev/null || echo "⚠️ 需要本地 Postgres 运行 (5433)" - @echo "测试登录: curl -X POST -H 'Content-Type: application/json' -d '{\"email\":\"bcrypt_test@example.com\",\"password\":\"TempBcrypt123!\"}' http://localhost:$${API_PORT:-8012}/api/v1/auth/login" # 代码格式化 format: diff --git a/README.md b/README.md index af36dec8..8c3dba0a 100644 --- a/README.md +++ b/README.md @@ -86,14 +86,6 @@ cp .env.example .env 2. 根据需要修改 `.env` 文件中的配置 -### 本地端口与钩子(建议) -- 端口约定:本地 Docker/管理脚本默认映射 PostgreSQL 到 `5433`,Redis 到 `6380`,Adminer 到 `9080`;API 默认 `8012`。 - - `jive-api/docker-compose.dev.yml` 已与 `jive-manager.sh` 对齐:`5433:5432`、`6380:6379`、`9080:8080`。 -- 启用预提交钩子(保证本地提交即跑 SQLx 严格校验与 Clippy): - ```bash - make hooks - ``` - ## 🏗️ 项目结构 ``` @@ -157,150 +149,6 @@ make db-migrate # 查看日志 make logs -### Docker 数据库 + 本地 API(推荐开发流程) - -```bash -# 1) 启动 Docker 开发数据库/Redis/Adminer(端口:PG=5433, Redis=6380, Adminer=9080) -make db-dev-up - -# 2) 本地运行 API,连接 Docker 数据库(CORS_DEV=1, SQLX_OFFLINE=true, API 默认 8012) -make api-dev-docker-db - -# 3) 健康检查 -curl -s http://localhost:8012/health - -# 4) 管理数据库(Adminer) -# 打开 http://localhost:9080 ,使用 postgres/postgres 登录,数据库 jive_money - -# 5) 停止 Docker 开发栈 -make db-dev-down -``` - -### JWT 密钥配置 - -环境变量 `JWT_SECRET` 用于签发与验证访问令牌: - -```bash -export JWT_SECRET=$(openssl rand -hex 32) -``` - -未设置时(或留空)API 会在开发 / 测试自动使用一个不安全的占位并打印警告,不可在生产依赖该默认值。 - -### 监控与指标 (Metrics) - -| Endpoint | 用途 | 认证 | 备注 | -|-------------|-------------------|------|------| -| `/health` | 探活 + 快照 | 否 | 轻量 JSON:hash 分布、rehash 状态、汇率指标等 | -| `/metrics` | Prometheus 拉取 | 否 | 文本格式指标(适合长期监控) | - -规范指标(推荐使用): -``` -password_hash_bcrypt_total # bcrypt (2a+2b+2y) -password_hash_argon2id_total # argon2id 数量 -password_hash_unknown_total # 未识别前缀 -password_hash_total_count # 总数 -password_hash_bcrypt_variant{variant="2b"} X # 每个变体 -jive_password_rehash_total # 成功重哈希次数(bcrypt→argon2id) -jive_password_rehash_fail_total # 重哈希失败次数(不会阻断登录) -jive_password_rehash_fail_breakdown_total{cause="hash"|"update"} # 重哈希失败按原因 -export_requests_buffered_total # 缓冲导出请求次数(POST CSV/JSON) -export_requests_stream_total # 流式导出请求次数(GET CSV streaming, feature=export_stream) -export_rows_buffered_total # 缓冲导出累计行数 -export_rows_stream_total # 流式导出累计行数 -jive_build_info{...} # 构建信息 (value=1) -auth_login_fail_total # 登录失败(未知用户 / 密码不匹配) -auth_login_inactive_total # 非激活账号登录尝试 -auth_login_rate_limited_total # 登录被速率限制次数 (429) -jive_build_info{commit,time,rustc,version} 1 # 构建信息 gauge -export_duration_buffered_seconds_* # 缓冲导出耗时直方图 (bucket/sum/count) -export_duration_stream_seconds_* # 流式导出耗时直方图 (bucket/sum/count) -process_uptime_seconds # 进程运行时长(秒) -jive_build_info{commit,time,rustc,version} 1 # 构建信息 gauge -``` - -兼容旧指标(DEPRECATED,将在 2 个发布周期后移除,详见 docs/METRICS_DEPRECATION_PLAN.md): -``` -jive_password_hash_users{algo="bcrypt_2b"} -``` - -Prometheus 抓取示例: -```yaml -scrape_configs: - - job_name: jive-api - metrics_path: /metrics - scrape_interval: 15s - static_configs: - - targets: ["api-host:8012"] -``` - -一致性快速校验(bcrypt 聚合与 /metrics 是否匹配): -```bash -H=$(curl -s http://localhost:8012/health) -M=$(curl -s http://localhost:8012/metrics) -echo "Health bcrypt sum:" \ - $(echo "$H" | jq '.metrics.hash_distribution.bcrypt | (."2a"+."2b"+."2y")') -echo "Metrics bcrypt total:" \ - $(grep '^password_hash_bcrypt_total' <<<"$M" | awk '{print $2}') -``` - -运维建议: -- 大规模用户场景可为 hash 查询加 30s 内存缓存(计划中)。 -- 迁移所有看板后移除旧的 jive_password_hash_users* 系列(目标 v1.2.0)。 -- 监控 `jive_password_rehash_fail_total`,持续增长提示 DB 更新/并发异常。 -- 导出耗时直方图示例: -```promql -# P95 缓冲导出耗时 -histogram_quantile(0.95, sum(rate(export_duration_buffered_seconds_bucket[5m])) by (le)) - -# 最近 1 分钟流式导出平均耗时 -sum(rate(export_duration_stream_seconds_sum[1m])) / sum(rate(export_duration_stream_seconds_count[1m])) -``` - -### 密码重哈希(bcrypt → Argon2id) - -登录成功后,如检测到旧 bcrypt 哈希,系统会在 `REHASH_ON_LOGIN` 未显式关闭时(默认开启)尝试透明升级为 Argon2id: - -```bash -# 关闭重哈希(例如压测环境需要保留原样) -export REHASH_ON_LOGIN=0 -``` - -失败不会阻断登录,仅记录 warn 日志。设计说明见 `docs/PASSWORD_REHASH_DESIGN.md`。 - -### 超级管理员默认密码说明 - -仓库历史存在两个默认密码基线: - -| 密码 | 出现来源 | 当前优先级 | -|------|----------|------------| -| `admin123` | 早期迁移:`005_create_superadmin.sql` / `006_update_superadmin_password.sql` / `016_fix_families_member_count_and_superadmin.sql` | 旧(可能仍在本地旧库残留) | -| `SuperAdmin@123` | 后续迁移:`009_create_superadmin_user.sql` 与补偿脚本 | 新(建议统一) | - -实际生效取决于“最后一次在你的数据库中执行成功的迁移顺序”。如果你基于较新的全量迁移(包含 009 及之后)初始化数据库,默认应为 `SuperAdmin@123`(Argon2)。如果本地数据库较早创建,仍可能是 `admin123`(bcrypt 或 Argon2)。 - -判定与处理建议: -1. 直接尝试两次登录(先 `SuperAdmin@123`,再 `admin123`)。 -2. 若均失败,可在本地用工具重置: - ```bash - cargo run -p jive-money-api --bin hash_password -- SuperAdmin@123 - # 得到哈希后: - psql "$DATABASE_URL" -c "UPDATE users SET password_hash='' WHERE LOWER(email)='superadmin@jive.money';" - ``` -3. 重置后立即登录并修改为你的本地私有密码(不要提交哈希)。 - -注意事项: -- 重新“干净”初始化数据库(删除数据卷 / 新建数据库)后会再次回到迁移脚本指定的默认值。 -- 请勿将生产环境实际超级管理员密码写入仓库或日志。 -- 如果团队决定最终统一为 `SuperAdmin@123` 以外的基线,请新增新的迁移并在此表格中更新来源说明。 - -快速登录测试(假设使用新基线): -```bash -curl -s -X POST http://localhost:8012/api/v1/auth/login \ - -H 'Content-Type: application/json' \ - -d '{"email":"superadmin@jive.money","password":"SuperAdmin@123"}' -``` -若返回 JSON 含 `token` 字段表示成功。生产中请务必改成强随机密码并限制暴露。 - ## 🧪 本地CI(不占用GitHub Actions分钟) 当你的GitHub Actions分钟不足时,可以使用本地CI脚本模拟CI流程: @@ -331,14 +179,12 @@ make api-lint CI 策略: - 严格检查 `.sqlx` 与查询是否一致;若不一致: - 上传 `api-sqlx-diff` 工件(含新旧缓存与 diff patch) - - 在 PR 自动评论首 80 行 diff 预览(仓库内 PR;Fork PR 仅 artifact) - - 失败退出,提示提交更新后的 `.sqlx/` + - 在 PR 自动评论首 80 行 diff 预览,便于定位 + - 失败退出,提示开发者提交更新后的 `.sqlx/` 该脚本会: - 尝试用 Docker 启动本地 Postgres/Redis(如已安装) - 运行迁移、校验 SQLx 离线缓存(仅校验,不生成) - - 可选:配置 Docker Hub 认证以避免镜像拉取限流(公共镜像 postgres/redis 等) - - 参见 `.github/DOCKER_AUTH_SETUP.md`(添加 DOCKERHUB_USERNAME / DOCKERHUB_TOKEN Secrets) - 运行 Rust 测试 + Clippy(警告视为错误) - 运行 Flutter analyze(告警致命)与测试 - 将结果保存到 `./local-artifacts` @@ -352,20 +198,6 @@ docker compose -f jive-api/docker-compose.db.yml up -d postgres cd jive-api && ./prepare-sqlx.sh && cd .. git add jive-api/.sqlx git commit -m "chore(sqlx): update offline cache" - -### CI 必要检查(main 分支保护) - -当前 main 的 Required checks: - -- `Flutter Tests` -- `Rust API Tests` -- `Rust API Clippy (blocking)`(`-D warnings`) -- `Rustfmt Check`(阻塞) -- `Cargo Deny Check`(安全与许可) - -注意: -- PR 首次不稳定阶段,可将 `Cargo Deny` 保持非阻塞,但推荐尽快修复并转为阻塞。 -- 本地建议:启用 git hooks(一次性):`make hooks`,自动在提交前执行 `make api-lint`。 ``` ``` @@ -593,17 +425,3 @@ MIT License ## 📞 联系 如有问题,请提交 Issue 或联系维护者。 -环境变量 (Metrics & 安全): -``` -AUTH_RATE_LIMIT=30/60 # 60 秒窗口内最多 30 次登录尝试 (默认 30/60) -AUTH_RATE_LIMIT_HASH_EMAIL=1 # 限流键中对 email 做哈希截断 (默认1) -ALLOW_PUBLIC_METRICS=1 # 设为 0 时启用白名单 -METRICS_ALLOW_CIDRS=127.0.0.1/32 # 逗号分隔 CIDR 列表 (ALLOW_PUBLIC_METRICS=0 生效) -METRICS_DENY_CIDRS= # 可选拒绝 CIDR (deny 优先) -METRICS_CACHE_TTL=30 # /metrics 缓存秒数 (0 禁用) -``` - -Grafana 仪表板: `docs/GRAFANA_DASHBOARD_TEMPLATE.json` -Alert 规则示例: `docs/ALERT_RULES_EXAMPLE.yaml` -安全清单: `docs/SECURITY_CHECKLIST.md` -快速验证脚本: `scripts/verify_observability.sh` diff --git a/ci-artifacts-sqlx/api-clippy-output/api-clippy-output.txt b/ci-artifacts-sqlx/api-clippy-output/api-clippy-output.txt new file mode 100644 index 00000000..41349cfe --- /dev/null +++ b/ci-artifacts-sqlx/api-clippy-output/api-clippy-output.txt @@ -0,0 +1,4 @@ + Checking jive-money-api v1.0.0 (/home/runner/work/jive-flutter-rust/jive-flutter-rust/jive-api) + Finished `dev` profile [optimized + debuginfo] target(s) in 9.76s +warning: the following packages contain code that will be rejected by a future version of Rust: sqlx-postgres v0.7.4 +note: to see what the problems were, use the option `--future-incompat-report`, or run `cargo report future-incompatibilities --id 12` diff --git a/ci-artifacts-sqlx/ci-summary/ci-summary.md b/ci-artifacts-sqlx/ci-summary/ci-summary.md new file mode 100644 index 00000000..df964a20 --- /dev/null +++ b/ci-artifacts-sqlx/ci-summary/ci-summary.md @@ -0,0 +1,204 @@ +# CI Summary Report +## Build Status +- Date: Tue Sep 23 09:28:04 UTC 2025 +- Branch: chore/flutter-analyze-cleanup-phase1-2-execution +- Commit: 80d9075adb9e9c0d8b78c033b1c361d1328649c0 + +## Test Results +- Flutter Tests: failure +- Rust Tests: failure +- Rust Core Check: failure +- Field Comparison: skipped + +## Flutter Test Details +# Flutter Test Report +## Test Summary +- Date: Tue Sep 23 09:27:55 UTC 2025 +- Flutter Version: 3.35.3 + +## Test Results +```json +Resolving dependencies... +Downloading packages... + _fe_analyzer_shared 67.0.0 (89.0.0 available) + analyzer 6.4.1 (8.2.0 available) + analyzer_plugin 0.11.3 (0.13.8 available) + build 2.4.1 (4.0.0 available) + build_config 1.1.2 (1.2.0 available) + build_resolvers 2.4.2 (3.0.4 available) + build_runner 2.4.13 (2.8.0 available) + build_runner_core 7.3.2 (9.3.2 available) + characters 1.4.0 (1.4.1 available) + custom_lint_core 0.6.3 (0.8.1 available) + dart_style 2.3.6 (3.1.2 available) + file_picker 8.3.7 (10.3.3 available) + fl_chart 0.66.2 (1.1.1 available) + flutter_launcher_icons 0.13.1 (0.14.4 available) + flutter_lints 3.0.2 (6.0.0 available) + flutter_riverpod 2.6.1 (3.0.0 available) + freezed 2.5.2 (3.2.3 available) + freezed_annotation 2.4.4 (3.1.0 available) + go_router 12.1.3 (16.2.2 available) + image_picker_android 0.8.13+2 (0.8.13+3 available) +! intl 0.19.0 (overridden) (0.20.2 available) + json_serializable 6.8.0 (6.11.1 available) + lints 3.0.0 (6.0.0 available) + material_color_utilities 0.11.1 (0.13.0 available) + meta 1.16.0 (1.17.0 available) + pool 1.5.1 (1.5.2 available) + protobuf 3.1.0 (4.2.0 available) + retrofit_generator 8.2.1 (10.0.5 available) + riverpod 2.6.1 (3.0.0 available) + riverpod_analyzer_utils 0.5.1 (0.5.10 available) + riverpod_annotation 2.6.1 (3.0.0 available) + riverpod_generator 2.4.0 (3.0.0 available) + shared_preferences_android 2.4.12 (2.4.13 available) + shelf_web_socket 2.0.1 (3.0.0 available) + source_gen 1.5.0 (4.0.1 available) + source_helper 1.3.5 (1.3.8 available) + test_api 0.7.6 (0.7.7 available) + very_good_analysis 5.1.0 (10.0.0 available) +Got dependencies! +38 packages have newer versions incompatible with dependency constraints. +Try `flutter pub outdated` for more information. +{"protocolVersion":"0.1.1","runnerVersion":null,"pid":2698,"type":"start","time":0} +{"suite":{"id":0,"platform":"vm","path":"/home/runner/work/jive-flutter-rust/jive-flutter-rust/jive-flutter/test/currency_preferences_sync_test.dart"},"type":"suite","time":0} +{"test":{"id":1,"name":"loading /home/runner/work/jive-flutter-rust/jive-flutter-rust/jive-flutter/test/currency_preferences_sync_test.dart","suiteID":0,"groupIDs":[],"metadata":{"skip":false,"skipReason":null},"line":null,"column":null,"url":null},"type":"testStart","time":1} +{"suite":{"id":2,"platform":"vm","path":"/home/runner/work/jive-flutter-rust/jive-flutter-rust/jive-flutter/test/currency_notifier_meta_test.dart"},"type":"suite","time":5} +{"test":{"id":3,"name":"loading /home/runner/work/jive-flutter-rust/jive-flutter-rust/jive-flutter/test/currency_notifier_meta_test.dart","suiteID":2,"groupIDs":[],"metadata":{"skip":false,"skipReason":null},"line":null,"column":null,"url":null},"type":"testStart","time":5} +{"count":5,"time":6,"type":"allSuites"} + +[{"event":"test.startedProcess","params":{"vmServiceUri":"http://127.0.0.1:39293/eTZiaHcrGWs=/"}}] + +[{"event":"test.startedProcess","params":{"vmServiceUri":"http://127.0.0.1:36355/nOrTWyOOTIM=/"}}] +{"testID":3,"result":"success","skipped":false,"hidden":true,"type":"testDone","time":8712} +{"group":{"id":4,"suiteID":2,"parentID":null,"name":"","metadata":{"skip":false,"skipReason":null},"testCount":1,"line":null,"column":null,"url":null},"type":"group","time":8715} +{"test":{"id":5,"name":"(setUpAll)","suiteID":2,"groupIDs":[4],"metadata":{"skip":false,"skipReason":null},"line":22,"column":3,"url":"file:///home/runner/work/jive-flutter-rust/jive-flutter-rust/jive-flutter/test/currency_notifier_meta_test.dart"},"type":"testStart","time":8715} +{"testID":1,"result":"success","skipped":false,"hidden":true,"type":"testDone","time":8732} +{"group":{"id":6,"suiteID":0,"parentID":null,"name":"","metadata":{"skip":false,"skipReason":null},"testCount":3,"line":null,"column":null,"url":null},"type":"group","time":8732} +{"test":{"id":7,"name":"(setUpAll)","suiteID":0,"groupIDs":[6],"metadata":{"skip":false,"skipReason":null},"line":104,"column":3,"url":"file:///home/runner/work/jive-flutter-rust/jive-flutter-rust/jive-flutter/test/currency_preferences_sync_test.dart"},"type":"testStart","time":8732} +{"testID":7,"result":"success","skipped":false,"hidden":true,"type":"testDone","time":8795} +{"test":{"id":8,"name":"debounce combines rapid preference pushes and succeeds","suiteID":0,"groupIDs":[6],"metadata":{"skip":false,"skipReason":null},"line":112,"column":3,"url":"file:///home/runner/work/jive-flutter-rust/jive-flutter-rust/jive-flutter/test/currency_preferences_sync_test.dart"},"type":"testStart","time":8796} +{"testID":5,"result":"success","skipped":false,"hidden":true,"type":"testDone","time":8834} +{"group":{"id":9,"suiteID":2,"parentID":4,"name":"CurrencyNotifier catalog meta","metadata":{"skip":false,"skipReason":null},"testCount":1,"line":29,"column":3,"url":"file:///home/runner/work/jive-flutter-rust/jive-flutter-rust/jive-flutter/test/currency_notifier_meta_test.dart"},"type":"group","time":8834} +{"test":{"id":10,"name":"CurrencyNotifier catalog meta initial usingFallback true when first fetch throws","suiteID":2,"groupIDs":[4,9],"metadata":{"skip":false,"skipReason":null},"line":31,"column":5,"url":"file:///home/runner/work/jive-flutter-rust/jive-flutter-rust/jive-flutter/test/currency_notifier_meta_test.dart"},"type":"testStart","time":8835} +{"testID":8,"result":"success","skipped":false,"hidden":false,"type":"testDone","time":8841} +{"test":{"id":11,"name":"failure stores pending then flush success clears it","suiteID":0,"groupIDs":[6],"metadata":{"skip":false,"skipReason":null},"line":139,"column":3,"url":"file:///home/runner/work/jive-flutter-rust/jive-flutter-rust/jive-flutter/test/currency_preferences_sync_test.dart"},"type":"testStart","time":8841} +{"testID":10,"result":"success","skipped":false,"hidden":false,"type":"testDone","time":8896} +{"test":{"id":12,"name":"(tearDownAll)","suiteID":2,"groupIDs":[4],"metadata":{"skip":false,"skipReason":null},"line":null,"column":null,"url":null},"type":"testStart","time":8897} +{"testID":12,"result":"success","skipped":false,"hidden":true,"type":"testDone","time":8900} +{"testID":11,"messageType":"print","message":"Failed to push currency preferences (will persist pending): Exception: network","type":"print","time":9360} +{"testID":11,"result":"success","skipped":false,"hidden":false,"type":"testDone","time":9527} +{"test":{"id":13,"name":"startup flush clears preexisting pending","suiteID":0,"groupIDs":[6],"metadata":{"skip":false,"skipReason":null},"line":167,"column":3,"url":"file:///home/runner/work/jive-flutter-rust/jive-flutter-rust/jive-flutter/test/currency_preferences_sync_test.dart"},"type":"testStart","time":9527} +{"testID":13,"result":"success","skipped":false,"hidden":false,"type":"testDone","time":9533} +{"test":{"id":14,"name":"(tearDownAll)","suiteID":0,"groupIDs":[6],"metadata":{"skip":false,"skipReason":null},"line":null,"column":null,"url":null},"type":"testStart","time":9533} +{"testID":14,"result":"success","skipped":false,"hidden":true,"type":"testDone","time":9536} +{"suite":{"id":15,"platform":"vm","path":"/home/runner/work/jive-flutter-rust/jive-flutter-rust/jive-flutter/test/widget_test.dart"},"type":"suite","time":9552} +{"test":{"id":16,"name":"loading /home/runner/work/jive-flutter-rust/jive-flutter-rust/jive-flutter/test/widget_test.dart","suiteID":15,"groupIDs":[],"metadata":{"skip":false,"skipReason":null},"line":null,"column":null,"url":null},"type":"testStart","time":9552} +{"suite":{"id":17,"platform":"vm","path":"/home/runner/work/jive-flutter-rust/jive-flutter-rust/jive-flutter/test/currency_selection_page_test.dart"},"type":"suite","time":10211} +{"test":{"id":18,"name":"loading /home/runner/work/jive-flutter-rust/jive-flutter-rust/jive-flutter/test/currency_selection_page_test.dart","suiteID":17,"groupIDs":[],"metadata":{"skip":false,"skipReason":null},"line":null,"column":null,"url":null},"type":"testStart","time":10211} +{"testID":16,"error":"Failed to load \"/home/runner/work/jive-flutter-rust/jive-flutter-rust/jive-flutter/test/widget_test.dart\":\nCompilation failed for testPath=/home/runner/work/jive-flutter-rust/jive-flutter-rust/jive-flutter/test/widget_test.dart: lib/screens/auth/login_screen.dart:442:36: Error: Not a constant expression.\n onPressed: _isLoading ? null : _login,\n ^^^^^^^^^^\nlib/screens/auth/login_screen.dart:442:56: Error: Not a constant expression.\n onPressed: _isLoading ? null : _login,\n ^^^^^^\nlib/screens/auth/login_screen.dart:443:47: Error: Method invocation is not a constant expression.\n style: ElevatedButton.styleFrom(\n ^^^^^^^^^\nlib/screens/auth/login_screen.dart:447:32: Error: Not a constant expression.\n child: _isLoading\n ^^^^^^^^^^\nlib/screens/auth/register_screen.dart:332:36: Error: Not a constant expression.\n onPressed: _isLoading ? null : _register,\n ^^^^^^^^^^\nlib/screens/auth/register_screen.dart:332:56: Error: Not a constant expression.\n onPressed: _isLoading ? null : _register,\n ^^^^^^^^^\nlib/screens/auth/register_screen.dart:333:47: Error: Method invocation is not a constant expression.\n style: ElevatedButton.styleFrom(\n ^^^^^^^^^\nlib/screens/auth/register_screen.dart:337:32: Error: Not a constant expression.\n child: _isLoading\n ^^^^^^^^^^\nlib/screens/dashboard/dashboard_screen.dart:337:31: Error: Not a constant expression.\n Navigator.pop(context);\n ^^^^^^^\nlib/screens/dashboard/dashboard_screen.dart:337:27: Error: Method invocation is not a constant expression.\n Navigator.pop(context);\n ^^^\nlib/screens/dashboard/dashboard_screen.dart:336:26: Error: Not a constant expression.\n onPressed: () {\n ^^\nlib/screens/dashboard/dashboard_screen.dart:335:35: Error: Cannot invoke a non-'const' factory where a const expression is expected.\nTry using a constructor or factory that is 'const'.\n child: OutlinedButton.icon(\n ^^^^\nlib/screens/settings/profile_settings_screen.dart:1004:42: Error: Not a constant expression.\n onPressed: _resetAccount,\n ^^^^^^^^^^^^^\nlib/screens/settings/profile_settings_screen.dart:1005:53: Error: Method invocation is not a constant expression.\n style: ElevatedButton.styleFrom(\n ^^^^^^^^^\nlib/screens/settings/profile_settings_screen.dart:1072:44: Error: Not a constant expression.\n context: context,\n ^^^^^^^\nlib/screens/settings/profile_settings_screen.dart:1080:72: Error: Not a constant expression.\n onPressed: () => Navigator.pop(context),\n ^^^^^^^\nlib/screens/settings/profile_settings_screen.dart:1080:68: Error: Method invocation is not a constant expression.\n onPressed: () => Navigator.pop(context),\n ^^^\nlib/screens/settings/profile_settings_screen.dart:1080:52: Error: Not a constant expression.\n onPressed: () => Navigator.pop(context),\n ^^\nlib/screens/settings/profile_settings_screen.dart:1085:57: Error: Not a constant expression.\n Navigator.pop(context);\n ^^^^^^^\nlib/screens/settings/profile_settings_screen.dart:1085:53: Error: Method invocation is not a constant expression.\n Navigator.pop(context);\n ^^^\nlib/screens/settings/profile_settings_screen.dart:1086:43: Error: Not a constant expression.\n _deleteAccount();\n ^^^^^^^^^^^^^^\nlib/screens/settings/profile_settings_screen.dart:1084:52: Error: Not a constant expression.\n onPressed: () {\n ^^\nlib/screens/settings/profile_settings_screen.dart:1073:44: Error: Not a constant expression.\n builder: (context) => AlertDialog(\n ^^^^^^^^^\nlib/screens/settings/profile_settings_screen.dart:1071:33: Error: Method invocation is not a constant expression.\n showDialog(\n ^^^^^^^^^^\nlib/screens/settings/profile_settings_screen.dart:1070:42: Error: Not a constant expression.\n onPressed: () {\n ^^\nlib/screens/settings/profile_settings_screen.dart:1097:53: Error: Method invocation is not a constant expression.\n style: ElevatedButton.styleFrom(\n ^^^^^^^^^\nlib/screens/management/currency_management_page_v2.dart:344:44: Error: Not a constant expression.\n Expanded(child: Text(d.code)),\n ^\nlib/screens/management/currency_management_page_v2.dart:348:46: Error: Not a constant expression.\n value: selectedMap[d.code],\n ^\nlib/screens/management/currency_management_page_v2.dart:348:34: Error: Not a constant expression.\n value: selectedMap[d.code],\n ^^^^^^^^^^^\nlib/screens/management/currency_management_page_v2.dart:351:42: Error: Not a constant expression.\n value: c.code,\n ^\nlib/screens/management/currency_management_page_v2.dart:352:50: Error: Not a constant expression.\n child: Text('${c.code} · ${c.nameZh}')))\n ^\nlib/screens/management/currency_management_page_v2.dart:352:62: Error: Not a constant expression.\n child: Text('${c.code} · ${c.nameZh}')))\n ^\nlib/screens/management/currency_management_page_v2.dart:350:36: Error: Not a constant expression.\n .map((c) => DropdownMenuItem(\n ^^^\nlib/screens/management/currency_management_page_v2.dart:349:34: Error: Not a constant expression.\n items: available\n ^^^^^^^^^\nlib/screens/management/currency_management_page_v2.dart:350:32: Error: Method invocation is not a constant expression.\n .map((c) => DropdownMenuItem(\n ^^^\nlib/screens/management/currency_management_page_v2.dart:353:32: Error: Method invocation is not a constant expression.\n .toList(),\n ^^^^^^\nlib/screens/management/currency_management_page_v2.dart:354:57: Error: Not a constant expression.\n onChanged: (v) => selectedMap[d.code] = v ?? d.code,\n ^\nlib/screens/management/currency_management_page_v2.dart:354:45: Error: Not a constant expression.\n onChanged: (v) => selectedMap[d.code] = v ?? d.code,\n ^^^^^^^^^^^\nlib/screens/management/currency_management_page_v2.dart:354:72: Error: Not a constant expression.\n onChanged: (v) => selectedMap[d.code] = v ?? d.code,\n ^\nlib/screens/management/currency_management_page_v2.dart:354:67: Error: Not a constant expression.\n onChanged: (v) => selectedMap[d.code] = v ?? d.code,\n ^\nlib/screens/management/currency_management_page_v2.dart:354:65: Error: Not a constant expression.\n onChanged: (v) => selectedMap[d.code] = v ?? d.code,\n ^\nlib/screens/management/currency_management_page_v2.dart:354:38: Error: Not a constant expression.\n onChanged: (v) => selectedMap[d.code] = v ?? d.code,\n ^^^\nlib/screens/management/currency_management_page_v2.dart:347:32: Error: Cannot invoke a non-'const' constructor where a const expression is expected.\nTry using a constructor or factory that is 'const'.\n child: DropdownButtonFormField(\n ^^^^^^^^^^^^^^^^^^^^^^^\nlib/screens/management/currency_management_page_v2.dart:339:40: Error: Not a constant expression.\n children: deprecated.map((d) {\n ^^^\nlib/screens/management/currency_management_page_v2.dart:339:25: Error: Not a constant expression.\n children: deprecated.map((d) {\n ^^^^^^^^^^\nlib/screens/management/currency_management_page_v2.dart:339:36: Error: Method invocation is not a constant expression.\n children: deprecated.map((d) {\n ^^^\nlib/screens/management/currency_management_page_v2.dart:362:18: Error: Method invocation is not a constant expression.\n }).toList(),\n ^^^^^^\nlib/screens/family/family_dashboard_screen.dart:330:53: Error: Not a constant expression.\n sections: _createPieChartSections(stats.accountTypeBreakdown),\n ^^^^^\nlib/screens/family/family_dashboard_screen.dart:330:29: Error: Not a constant expression.\n sections: _createPieChartSections(stats.accountTypeBreakdown),\n ^^^^^^^^^^^^^^^^^^^^^^^\nlib/screens/family/family_dashboard_screen.dart:329:17: Error: Cannot invoke a non-'const' constructor where a const expression is expected.\nTry using a constructor or factory that is 'const'.\n PieChartData(\n ^^^^^^^^^^^^\nlib/screens/family/family_dashboard_screen.dart:583:47: Error: Not a constant expression.\n getDrawingHorizontalLine: (value) {\n ^^^^^^^\nlib/screens/family/family_dashboard_screen.dart:605:31: Error: Not a constant expression.\n if (value.toInt() < months.length) {\n ^^^^^\nlib/screens/family/family_dashboard_screen.dart:605:37: Error: Method invocation is not a constant expression.\n if (value.toInt() < months.length) {\n ^^^^^\nlib/screens/family/family_dashboard_screen.dart:605:47: Error: Not a constant expression.\n if (value.toInt() < months.length) {\n ^^^^^^\nlib/screens/family/family_dashboard_screen.dart:607:38: Error: Not a constant expression.\n months[value.toInt()].substring(5),\n ^^^^^\nlib/screens/family/family_dashboard_screen.dart:607:44: Error: Method invocation is not a constant expression.\n months[value.toInt()].substring(5),\n ^^^^^\nlib/screens/family/family_dashboard_screen.dart:607:31: Error: Not a constant expression.\n months[value.toInt()].substring(5),\n ^^^^^^\nlib/screens/family/family_dashboard_screen.dart:607:53: Error: Method invocation is not a constant expression.\n months[value.toInt()].substring(5),\n ^^^^^^^^^\nlib/screens/family/family_dashboard_screen.dart:603:42: Error: Not a constant expression.\n getTitlesWidget: (value, meta) {\n ^^^^^^^^^^^^^\nlib/screens/family/family_dashboard_screen.dart:616:31: Error: Cannot invoke a non-'const' constructor where a const expression is expected.\nTry using a constructor or factory that is 'const'.\n borderData: FlBorderData(show: false),\n ^^^^^^^^^^^^\nlib/screens/family/family_dashboard_screen.dart:619:30: Error: Not a constant expression.\n spots: monthlyTrend.entries\n ^^^^^^^^^^^^\nlib/screens/family/family_dashboard_screen.dart:620:28: Error: Method invocation is not a constant expression.\n .toList()\n ^^^^^^\nlib/screens/family/family_dashboard_screen.dart:621:28: Error: Method invocation is not a constant expression.\n .asMap()\n ^^^^^\nlib/screens/family/family_dashboard_screen.dart:624:39: Error: Not a constant expression.\n return FlSpot(entry.key.toDouble(), entry.value.value);\n ^^^^^\nlib/screens/family/family_dashboard_screen.dart:624:49: Error: Method invocation is not a constant expression.\n return FlSpot(entry.key.toDouble(), entry.value.value);\n ^^^^^^^^\nlib/screens/family/family_dashboard_screen.dart:624:61: Error: Not a constant expression.\n return FlSpot(entry.key.toDouble(), entry.value.value);\n ^^^^^\nlib/screens/family/family_dashboard_screen.dart:624:67: Error: Not a constant expression.\n return FlSpot(entry.key.toDouble(), entry.value.value);\n ^^^^^\nlib/screens/family/family_dashboard_screen.dart:623:32: Error: Not a constant expression.\n .map((entry) {\n ^^^^^^^\nlib/screens/family/family_dashboard_screen.dart:623:28: Error: Method invocation is not a constant expression.\n .map((entry) {\n ^^^\nlib/screens/family/family_dashboard_screen.dart:625:26: Error: Method invocation is not a constant expression.\n }).toList(),\n ^^^^^^\nlib/screens/family/family_dashboard_screen.dart:627:39: Error: Not a constant expression.\n color: Theme.of(context).primaryColor,\n ^^^^^^^\nlib/screens/family/family_dashboard_screen.dart:627:36: Error: Method invocation is not a constant expression.\n color: Theme.of(context).primaryColor,\n ^^\nlib/screens/family/family_dashboard_screen.dart:632:41: Error: Not a constant expression.\n color: Theme.of(context).primaryColor.withValues(alpha: 0.1),\n ^^^^^^^\nlib/screens/family/family_dashboard_screen.dart:632:38: Error: Method invocation is not a constant expression.\n color: Theme.of(context).primaryColor.withValues(alpha: 0.1),\n ^^\nlib/screens/family/family_dashboard_screen.dart:632:63: Error: Method invocation is not a constant expression.\n color: Theme.of(context).primaryColor.withValues(alpha: 0.1),\n ^^^^^^^^^^\nlib/screens/family/family_dashboard_screen.dart:630:37: Error: Cannot invoke a non-'const' constructor where a const expression is expected.\nTry using a constructor or factory that is 'const'.\n belowBarData: BarAreaData(\n ^^^^^^^^^^^\nlib/screens/family/family_dashboard_screen.dart:618:21: Error: Cannot invoke a non-'const' constructor where a const expression is expected.\nTry using a constructor or factory that is 'const'.\n LineChartBarData(\n ^^^^^^^^^^^^^^^^\nlib/screens/family/family_dashboard_screen.dart:578:17: Error: Cannot invoke a non-'const' constructor where a const expression is expected.\nTry using a constructor or factory that is 'const'.\n LineChartData(\n ^^^^^^^^^^^^^\nlib/widgets/wechat_login_button.dart:85:20: Error: Not a constant expression.\n onPressed: _isLoading ? null : _handleWeChatLogin,\n ^^^^^^^^^^\nlib/widgets/wechat_login_button.dart:85:40: Error: Not a constant expression.\n onPressed: _isLoading ? null : _handleWeChatLogin,\n ^^^^^^^^^^^^^^^^^^\nlib/widgets/wechat_login_button.dart:90:40: Error: Cannot invoke a non-'const' constructor where a const expression is expected.\nTry using a constructor or factory that is 'const'.\n borderRadius: BorderRadius.circular(8),\n ^^^^^^^^\nlib/widgets/wechat_login_button.dart:86:31: Error: Method invocation is not a constant expression.\n style: OutlinedButton.styleFrom(\n ^^^^^^^^^\nlib/widgets/wechat_login_button.dart:93:15: Error: Not a constant expression.\n icon: _isLoading\n ^^^^^^^^^^\nlib/widgets/wechat_login_button.dart:104:11: Error: Not a constant expression.\n widget.buttonText,\n ^^^^^^\nlib/widgets/wechat_login_button.dart:84:29: Error: Cannot invoke a non-'const' factory where a const expression is expected.\nTry using a constructor or factory that is 'const'.\n child: OutlinedButton.icon(\n ^^^^\nlib/ui/components/dashboard/account_overview.dart:122:15: Error: Not a constant expression.\n assets,\n ^^^^^^\nlib/ui/components/dashboard/account_overview.dart:120:20: Error: Not a constant expression.\n child: _buildOverviewCard(\n ^^^^^^^^^^^^^^^^^^\nlib/ui/components/dashboard/account_overview.dart:131:15: Error: Not a constant expression.\n liabilities,\n ^^^^^^^^^^^\nlib/ui/components/dashboard/account_overview.dart:129:20: Error: Not a constant expression.\n child: _buildOverviewCard(\n ^^^^^^^^^^^^^^^^^^\nlib/ui/components/dashboard/account_overview.dart:141:15: Error: Not a constant expression.\n netWorth >= 0 ? Colors.blue : Colors.orange,\n ^^^^^^^^\nlib/ui/components/dashboard/account_overview.dart:140:15: Error: Not a constant expression.\n netWorth,\n ^^^^^^^^\nlib/ui/components/dashboard/account_overview.dart:138:20: Error: Not a constant expression.\n child: _buildOverviewCard(\n ^^^^^^^^^^^^^^^^^^\nlib/ui/components/dashboard/budget_summary.dart:181:32: Error: Not a constant expression.\n value: spentPercentage.clamp(0.0, 1.0),\n ^^^^^^^^^^^^^^^\nlib/ui/components/dashboard/budget_summary.dart:181:48: Error: Method invocation is not a constant expression.\n value: spentPercentage.clamp(0.0, 1.0),\n ^^^^^\nlib/ui/components/dashboard/budget_summary.dart:184:59: Error: Not a constant expression.\n AlwaysStoppedAnimation(warningLevel.color),\n ^^^^^^^^^^^^\nlib/widgets/dialogs/invite_member_dialog.dart:438:15: Error: Not a constant expression.\n permission,\n ^^^^^^^^^^\nlib/widgets/sheets/generate_invite_code_sheet.dart:297:30: Error: Not a constant expression.\n onPressed: _isLoading ? null : _generateInvitation,\n ^^^^^^^^^^\nlib/widgets/sheets/generate_invite_code_sheet.dart:297:50: Error: Not a constant expression.\n onPressed: _isLoading ? null : _generateInvitation,\n ^^^^^^^^^^^^^^^^^^^\nlib/widgets/sheets/generate_invite_code_sheet.dart:298:25: Error: Not a constant expression.\n icon: _isLoading\n ^^^^^^^^^^\nlib/widgets/sheets/generate_invite_code_sheet.dart:308:31: Error: Not a constant expression.\n label: Text(_isLoading ? '生成中...' : '生成邀请'),\n ^^^^^^^^^^\nlib/widgets/sheets/generate_invite_code_sheet.dart:296:37: Error: Cannot invoke a non-'const' factory where a const expression is expected.\nTry using a constructor or factory that is 'const'.\n child: FilledButton.icon(\n ^^^^\nlib/screens/auth/wechat_register_form_screen.dart:401:32: Error: Not a constant expression.\n onPressed: _isLoading ? null : _register,\n ^^^^^^^^^^\nlib/screens/auth/wechat_register_form_screen.dart:401:52: Error: Not a constant expression.\n onPressed: _isLoading ? null : _register,\n ^^^^^^^^^\nlib/screens/auth/wechat_register_form_screen.dart:402:43: Error: Method invocation is not a constant expression.\n style: ElevatedButton.styleFrom(\n ^^^^^^^^^\nlib/screens/auth/wechat_register_form_screen.dart:406:28: Error: Not a constant expression.\n child: _isLoading\n ^^^^^^^^^^\n.","stackTrace":"","isFailure":false,"type":"error","time":12358} +{"testID":16,"result":"error","skipped":false,"hidden":false,"type":"testDone","time":12361} +{"suite":{"id":19,"platform":"vm","path":"/home/runner/work/jive-flutter-rust/jive-flutter-rust/jive-flutter/test/currency_notifier_quiet_test.dart"},"type":"suite","time":12361} +{"test":{"id":20,"name":"loading /home/runner/work/jive-flutter-rust/jive-flutter-rust/jive-flutter/test/currency_notifier_quiet_test.dart","suiteID":19,"groupIDs":[],"metadata":{"skip":false,"skipReason":null},"line":null,"column":null,"url":null},"type":"testStart","time":12362} + +[{"event":"test.startedProcess","params":{"vmServiceUri":"http://127.0.0.1:36441/Z_Y8385vBiE=/"}}] +{"testID":18,"result":"success","skipped":false,"hidden":true,"type":"testDone","time":14067} +{"group":{"id":21,"suiteID":17,"parentID":null,"name":"","metadata":{"skip":false,"skipReason":null},"testCount":2,"line":null,"column":null,"url":null},"type":"group","time":14068} +{"test":{"id":22,"name":"(setUpAll)","suiteID":17,"groupIDs":[21],"metadata":{"skip":false,"skipReason":null},"line":78,"column":3,"url":"file:///home/runner/work/jive-flutter-rust/jive-flutter-rust/jive-flutter/test/currency_selection_page_test.dart"},"type":"testStart","time":14068} + +[{"event":"test.startedProcess","params":{"vmServiceUri":"http://127.0.0.1:42785/wwGOWAQ73TE=/"}}] +{"testID":22,"result":"success","skipped":false,"hidden":true,"type":"testDone","time":14132} +{"test":{"id":23,"name":"Selecting base currency returns via Navigator.pop","suiteID":17,"groupIDs":[21],"metadata":{"skip":false,"skipReason":null},"line":174,"column":5,"url":"package:flutter_test/src/widget_tester.dart","root_line":85,"root_column":3,"root_url":"file:///home/runner/work/jive-flutter-rust/jive-flutter-rust/jive-flutter/test/currency_selection_page_test.dart"},"type":"testStart","time":14132} +{"testID":20,"result":"success","skipped":false,"hidden":true,"type":"testDone","time":14300} +{"group":{"id":24,"suiteID":19,"parentID":null,"name":"","metadata":{"skip":false,"skipReason":null},"testCount":2,"line":null,"column":null,"url":null},"type":"group","time":14300} +{"test":{"id":25,"name":"(setUpAll)","suiteID":19,"groupIDs":[24],"metadata":{"skip":false,"skipReason":null},"line":66,"column":3,"url":"file:///home/runner/work/jive-flutter-rust/jive-flutter-rust/jive-flutter/test/currency_notifier_quiet_test.dart"},"type":"testStart","time":14300} +{"testID":25,"result":"success","skipped":false,"hidden":true,"type":"testDone","time":14354} +{"test":{"id":26,"name":"quiet mode: no calls before initialize; initialize triggers first load; explicit refresh triggers second","suiteID":19,"groupIDs":[24],"metadata":{"skip":false,"skipReason":null},"line":88,"column":3,"url":"file:///home/runner/work/jive-flutter-rust/jive-flutter-rust/jive-flutter/test/currency_notifier_quiet_test.dart"},"type":"testStart","time":14355} +{"testID":26,"result":"success","skipped":false,"hidden":false,"type":"testDone","time":14392} +{"test":{"id":27,"name":"initialize() is idempotent","suiteID":19,"groupIDs":[24],"metadata":{"skip":false,"skipReason":null},"line":104,"column":3,"url":"file:///home/runner/work/jive-flutter-rust/jive-flutter-rust/jive-flutter/test/currency_notifier_quiet_test.dart"},"type":"testStart","time":14392} +{"testID":27,"result":"success","skipped":false,"hidden":false,"type":"testDone","time":14418} +{"test":{"id":28,"name":"(tearDownAll)","suiteID":19,"groupIDs":[24],"metadata":{"skip":false,"skipReason":null},"line":null,"column":null,"url":null},"type":"testStart","time":14418} +{"testID":28,"result":"success","skipped":false,"hidden":true,"type":"testDone","time":14421} +{"testID":23,"result":"success","skipped":false,"hidden":false,"type":"testDone","time":15472} +{"test":{"id":29,"name":"Base currency is sorted to top and marked","suiteID":17,"groupIDs":[21],"metadata":{"skip":false,"skipReason":null},"line":174,"column":5,"url":"package:flutter_test/src/widget_tester.dart","root_line":120,"root_column":3,"root_url":"file:///home/runner/work/jive-flutter-rust/jive-flutter-rust/jive-flutter/test/currency_selection_page_test.dart"},"type":"testStart","time":15473} +{"testID":29,"result":"success","skipped":false,"hidden":false,"type":"testDone","time":15696} +{"test":{"id":30,"name":"(tearDownAll)","suiteID":17,"groupIDs":[21],"metadata":{"skip":false,"skipReason":null},"line":null,"column":null,"url":null},"type":"testStart","time":15696} +{"testID":30,"result":"success","skipped":false,"hidden":true,"type":"testDone","time":15700} +{"success":false,"type":"done","time":16083} +``` +## Coverage Summary +Coverage data generated successfully +## Rust Test Details +``` +184 | base_currency: settings.base_currency, + | ^^^^^^^^^^^^^^^^^^^^^^ expected `String`, found `Option` + | + = note: expected struct `std::string::String` + found enum `std::option::Option` +help: consider using `Option::expect` to unwrap the `std::option::Option` value, panicking if the value is an `Option::None` + | +184 | base_currency: settings.base_currency.expect("REASON"), + | +++++++++++++++++ + +warning: value assigned to `bind_idx` is never read + --> src/services/tag_service.rs:37:133 + | +37 | ...E ${}", bind_idx)); args.push((bind_idx, format!("%{}%", q))); bind_idx+=1; } + | ^^^^^^^^ + | + = help: maybe it is overwritten before being read? + +warning: unused import: `super::*` + --> src/services/currency_service.rs:582:9 + | +582 | use super::*; + | ^^^^^^^^ + | + = note: `#[warn(unused_imports)]` on by default + +warning: unused import: `rust_decimal::prelude::*` + --> src/services/currency_service.rs:583:9 + | +583 | use rust_decimal::prelude::*; + | ^^^^^^^^^^^^^^^^^^^^^^^^ + +warning: unused variable: `i` + --> src/services/avatar_service.rs:230:18 + | +230 | for (i, part) in parts.iter().take(2).enumerate() { + | ^ help: if this is intentional, prefix it with an underscore: `_i` + +warning: unused variable: `from_decimal_places` + --> src/services/currency_service.rs:386:9 + | +386 | from_decimal_places: i32, + | ^^^^^^^^^^^^^^^^^^^ help: if this is intentional, prefix it with an underscore: `_from_decimal_places` + +For more information about this error, try `rustc --explain E0308`. +warning: `jive-money-api` (lib) generated 7 warnings +error: could not compile `jive-money-api` (lib) due to 2 previous errors; 7 warnings emitted +warning: build failed, waiting for other jobs to finish... +warning: `jive-money-api` (lib test) generated 9 warnings (7 duplicates) +error: could not compile `jive-money-api` (lib test) due to 2 previous errors; 9 warnings emitted +``` + +## Manual Overrides Tests +- HTTP endpoint test (manual_overrides_http_test): executed in CI (see Rust Test Details) +- Flutter widget navigation test: attempted (no machine artifact found) + +## Manual Exchange Rate Tests +- currency_manual_rate_test: executed in CI +- currency_manual_rate_batch_test: executed in CI + +## Rust Core Dual Mode Check +- jive-core default mode: tested +- jive-core server mode: tested +- Overall status: failure + +## Rust API Clippy (Non-blocking) +- Status: success +- Artifact: api-clippy-output.txt + +## Recent EXPORT Audits (top 3) +(no audit data) diff --git a/ci-artifacts-sqlx/export-indexes-report/export-indexes-report.md b/ci-artifacts-sqlx/export-indexes-report/export-indexes-report.md new file mode 100644 index 00000000..a93e7f8a --- /dev/null +++ b/ci-artifacts-sqlx/export-indexes-report/export-indexes-report.md @@ -0,0 +1,83 @@ +# Export Indexes Report +Generated at: Tue Sep 23 09:26:04 UTC 2025 + + Table "public.transactions" + Column | Type | Collation | Nullable | Default | Storage | Compression | Stats target | Description +------------------+--------------------------+-----------+----------+--------------------------------+----------+-------------+--------------+------------- + id | uuid | | not null | gen_random_uuid() | plain | | | + ledger_id | uuid | | not null | | plain | | | + transaction_type | character varying(20) | | not null | | extended | | | + amount | numeric(15,2) | | not null | | main | | | + currency | character varying(10) | | | 'CNY'::character varying | extended | | | + category_id | uuid | | | | plain | | | + account_id | uuid | | not null | | plain | | | + to_account_id | uuid | | | | plain | | | + transaction_date | date | | not null | | plain | | | + transaction_time | time without time zone | | | | plain | | | + description | text | | | | extended | | | + notes | text | | | | extended | | | + tags | text[] | | | | extended | | | + location | text | | | | extended | | | + merchant | character varying(200) | | | | extended | | | + receipt_url | text | | | | extended | | | + is_recurring | boolean | | | false | plain | | | + recurring_id | uuid | | | | plain | | | + status | character varying(20) | | | 'completed'::character varying | extended | | | + created_by | uuid | | not null | | plain | | | + updated_by | uuid | | | | plain | | | + deleted_at | timestamp with time zone | | | | plain | | | + created_at | timestamp with time zone | | | CURRENT_TIMESTAMP | plain | | | + updated_at | timestamp with time zone | | | CURRENT_TIMESTAMP | plain | | | + reference_number | character varying(100) | | | | extended | | | + is_manual | boolean | | | true | plain | | | + import_id | character varying(100) | | | | extended | | | + recurring_rule | text | | | | extended | | | + category_name | text | | | | extended | | | + payee | text | | | | extended | | | +Indexes: + "transactions_pkey" PRIMARY KEY, btree (id) + "idx_transactions_account" btree (account_id) + "idx_transactions_category" btree (category_id) + "idx_transactions_created_by" btree (created_by) + "idx_transactions_date" btree (transaction_date) + "idx_transactions_ledger" btree (ledger_id) + "idx_transactions_type" btree (transaction_type) +Check constraints: + "transactions_status_check" CHECK (status::text = ANY (ARRAY['pending'::character varying, 'completed'::character varying, 'cancelled'::character varying]::text[])) + "transactions_transaction_type_check" CHECK (transaction_type::text = ANY (ARRAY['expense'::character varying, 'income'::character varying, 'transfer'::character varying]::text[])) +Foreign-key constraints: + "transactions_account_id_fkey" FOREIGN KEY (account_id) REFERENCES accounts(id) + "transactions_category_id_fkey" FOREIGN KEY (category_id) REFERENCES categories(id) + "transactions_created_by_fkey" FOREIGN KEY (created_by) REFERENCES users(id) + "transactions_ledger_id_fkey" FOREIGN KEY (ledger_id) REFERENCES ledgers(id) ON DELETE CASCADE + "transactions_to_account_id_fkey" FOREIGN KEY (to_account_id) REFERENCES accounts(id) + "transactions_updated_by_fkey" FOREIGN KEY (updated_by) REFERENCES users(id) +Referenced by: + TABLE "attachments" CONSTRAINT "attachments_transaction_id_fkey" FOREIGN KEY (transaction_id) REFERENCES transactions(id) ON DELETE CASCADE +Triggers: + update_transactions_updated_at BEFORE UPDATE ON transactions FOR EACH ROW EXECUTE FUNCTION update_updated_at_column() +Access method: heap + + + indexname | indexdef +-----------------------------+------------------------------------------------------------------------------------------ + idx_transactions_account | CREATE INDEX idx_transactions_account ON public.transactions USING btree (account_id) + idx_transactions_category | CREATE INDEX idx_transactions_category ON public.transactions USING btree (category_id) + idx_transactions_created_by | CREATE INDEX idx_transactions_created_by ON public.transactions USING btree (created_by) + idx_transactions_date | CREATE INDEX idx_transactions_date ON public.transactions USING btree (transaction_date) + idx_transactions_ledger | CREATE INDEX idx_transactions_ledger ON public.transactions USING btree (ledger_id) + idx_transactions_type | CREATE INDEX idx_transactions_type ON public.transactions USING btree (transaction_type) + transactions_pkey | CREATE UNIQUE INDEX transactions_pkey ON public.transactions USING btree (id) +(7 rows) + + +## Audit Indexes + indexname | indexdef +----------------------------------+--------------------------------------------------------------------------------------------------------- + family_audit_logs_pkey | CREATE UNIQUE INDEX family_audit_logs_pkey ON public.family_audit_logs USING btree (id) + idx_family_audit_logs_action | CREATE INDEX idx_family_audit_logs_action ON public.family_audit_logs USING btree (action) + idx_family_audit_logs_created_at | CREATE INDEX idx_family_audit_logs_created_at ON public.family_audit_logs USING btree (created_at DESC) + idx_family_audit_logs_family_id | CREATE INDEX idx_family_audit_logs_family_id ON public.family_audit_logs USING btree (family_id) + idx_family_audit_logs_user_id | CREATE INDEX idx_family_audit_logs_user_id ON public.family_audit_logs USING btree (user_id) +(5 rows) + diff --git a/ci-artifacts-sqlx/flutter-analyze-output/flutter-analyze-output.txt b/ci-artifacts-sqlx/flutter-analyze-output/flutter-analyze-output.txt new file mode 100644 index 00000000..370c22d9 --- /dev/null +++ b/ci-artifacts-sqlx/flutter-analyze-output/flutter-analyze-output.txt @@ -0,0 +1,2468 @@ +Resolving dependencies... +Downloading packages... + _fe_analyzer_shared 67.0.0 (89.0.0 available) + analyzer 6.4.1 (8.2.0 available) + analyzer_plugin 0.11.3 (0.13.8 available) + build 2.4.1 (4.0.0 available) + build_config 1.1.2 (1.2.0 available) + build_resolvers 2.4.2 (3.0.4 available) + build_runner 2.4.13 (2.8.0 available) + build_runner_core 7.3.2 (9.3.2 available) + characters 1.4.0 (1.4.1 available) + custom_lint_core 0.6.3 (0.8.1 available) + dart_style 2.3.6 (3.1.2 available) + file_picker 8.3.7 (10.3.3 available) + fl_chart 0.66.2 (1.1.1 available) + flutter_launcher_icons 0.13.1 (0.14.4 available) + flutter_lints 3.0.2 (6.0.0 available) + flutter_riverpod 2.6.1 (3.0.0 available) + freezed 2.5.2 (3.2.3 available) + freezed_annotation 2.4.4 (3.1.0 available) + go_router 12.1.3 (16.2.2 available) + image_picker_android 0.8.13+2 (0.8.13+3 available) +! intl 0.19.0 (overridden) (0.20.2 available) + json_serializable 6.8.0 (6.11.1 available) + lints 3.0.0 (6.0.0 available) + material_color_utilities 0.11.1 (0.13.0 available) + meta 1.16.0 (1.17.0 available) + pool 1.5.1 (1.5.2 available) + protobuf 3.1.0 (4.2.0 available) + retrofit_generator 8.2.1 (10.0.5 available) + riverpod 2.6.1 (3.0.0 available) + riverpod_analyzer_utils 0.5.1 (0.5.10 available) + riverpod_annotation 2.6.1 (3.0.0 available) + riverpod_generator 2.4.0 (3.0.0 available) + shared_preferences_android 2.4.12 (2.4.13 available) + shelf_web_socket 2.0.1 (3.0.0 available) + source_gen 1.5.0 (4.0.1 available) + source_helper 1.3.5 (1.3.8 available) + test_api 0.7.6 (0.7.7 available) + very_good_analysis 5.1.0 (10.0.0 available) +Got dependencies! +38 packages have newer versions incompatible with dependency constraints. +Try `flutter pub outdated` for more information. +Analyzing jive-flutter... + + info • Use 'const' with the constructor to improve performance • lib/app.dart:67:23 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/app.dart:71:23 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/app.dart:121:23 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/app.dart:125:23 • prefer_const_constructors +warning • The left operand can't be null, so the right operand is never executed • lib/core/app.dart:49:59 • dead_null_aware_expression + info • Don't use 'BuildContext's across async gaps • lib/core/app.dart:163:32 • use_build_context_synchronously + info • Uses 'await' on an instance of 'String', which is not a subtype of 'Future' • lib/core/app.dart:192:24 • await_only_futures + info • Uses 'await' on an instance of 'String', which is not a subtype of 'Future' • lib/core/app.dart:245:27 • await_only_futures + info • Dangling library doc comment • lib/core/constants/app_constants.dart:1:1 • dangling_library_doc_comments +warning • This default clause is covered by the previous cases • lib/core/network/http_client.dart:260:7 • unreachable_switch_default + info • Parameter 'message' could be a super parameter • lib/core/network/http_client.dart:327:3 • use_super_parameters + info • Parameter 'message' could be a super parameter • lib/core/network/http_client.dart:332:3 • use_super_parameters + info • Parameter 'message' could be a super parameter • lib/core/network/http_client.dart:337:3 • use_super_parameters + info • Parameter 'message' could be a super parameter • lib/core/network/http_client.dart:342:3 • use_super_parameters + info • Parameter 'message' could be a super parameter • lib/core/network/http_client.dart:349:3 • use_super_parameters + info • Parameter 'message' could be a super parameter • lib/core/network/http_client.dart:355:3 • use_super_parameters +warning • This default clause is covered by the previous cases • lib/core/network/interceptors/error_interceptor.dart:66:7 • unreachable_switch_default +warning • The value of the field '_lastGlobalFailure' isn't used • lib/core/network/interceptors/retry_interceptor.dart:11:20 • unused_field + info • Use 'const' with the constructor to improve performance • lib/core/router/app_router.dart:252:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/core/router/app_router.dart:252:35 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/core/router/app_router.dart:252:49 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/core/router/app_router.dart:262:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/core/router/app_router.dart:262:35 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/core/router/app_router.dart:262:49 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/core/router/app_router.dart:272:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/core/router/app_router.dart:272:35 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/core/router/app_router.dart:272:49 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/core/router/app_router.dart:304:29 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/core/router/app_router.dart:309:13 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/core/router/app_router.dart:315:13 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/core/router/app_router.dart:317:22 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/core/router/app_router.dart:328:22 • prefer_const_constructors + info • 'value' is deprecated and shouldn't be used. Use component accessors like .r or .g, or toARGB32 for an explicit conversion • lib/core/storage/adapters/account_adapter.dart:56:26 • deprecated_member_use + info • 'value' is deprecated and shouldn't be used. Use component accessors like .r or .g, or toARGB32 for an explicit conversion • lib/core/storage/adapters/account_adapter.dart:123:26 • deprecated_member_use + info • 'value' is deprecated and shouldn't be used. Use component accessors like .r or .g, or toARGB32 for an explicit conversion • lib/core/storage/adapters/transaction_adapter.dart:186:25 • deprecated_member_use + info • Use interpolation to compose strings and values • lib/core/storage/token_storage.dart:201:7 • prefer_interpolation_to_compose_strings + info • Use interpolation to compose strings and values • lib/core/storage/token_storage.dart:203:7 • prefer_interpolation_to_compose_strings + info • Use interpolation to compose strings and values • lib/core/storage/token_storage.dart:205:7 • prefer_interpolation_to_compose_strings + info • 'background' is deprecated and shouldn't be used. Use surface instead. This feature was deprecated after v3.18.0-0.1.pre • lib/core/theme/app_theme.dart:48:7 • deprecated_member_use + info • 'onBackground' is deprecated and shouldn't be used. Use onSurface instead. This feature was deprecated after v3.18.0-0.1.pre • lib/core/theme/app_theme.dart:50:7 • deprecated_member_use + info • 'background' is deprecated and shouldn't be used. Use surface instead. This feature was deprecated after v3.18.0-0.1.pre • lib/core/theme/app_theme.dart:92:7 • deprecated_member_use + info • 'onBackground' is deprecated and shouldn't be used. Use onSurface instead. This feature was deprecated after v3.18.0-0.1.pre • lib/core/theme/app_theme.dart:94:7 • deprecated_member_use + info • 'printTime' is deprecated and shouldn't be used. Use `dateTimeFormat` with `DateTimeFormat.onlyTimeAndSinceStart` or `DateTimeFormat.none` instead • lib/core/utils/logger.dart:16:9 • deprecated_member_use + info • 'dart:html' is deprecated and shouldn't be used. Use package:web and dart:js_interop instead • lib/devtools/dev_quick_actions_web.dart:3:1 • deprecated_member_use + info • Use 'const' with the constructor to improve performance • lib/devtools/dev_quick_actions_web.dart:73:13 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main.dart:108:15 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main.dart:114:15 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main.dart:116:24 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main.dart:146:24 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_currency_test.dart:41:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_currency_test.dart:69:23 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_currency_test.dart:70:24 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_currency_test.dart:88:23 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_currency_test.dart:89:24 • prefer_const_constructors + info • The import of 'package:flutter/foundation.dart' is unnecessary because all of the used elements are also provided by the import of 'package:flutter/material.dart' • lib/main_network_test.dart:1:8 • unnecessary_import + info • Use 'const' with the constructor to improve performance • lib/main_network_test.dart:40:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_network_test.dart:43:19 • prefer_const_constructors + info • Unnecessary 'const' keyword • lib/main_network_test.dart:96:21 • unnecessary_const + info • Use 'const' with the constructor to improve performance • lib/main_network_test.dart:105:21 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_network_test.dart:116:30 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_network_test.dart:136:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_network_test.dart:137:15 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_network_test.dart:209:15 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:175:23 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:488:21 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:491:27 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:504:27 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:509:35 • prefer_const_constructors + info • Use 'const' literals as arguments to constructors of '@immutable' classes • lib/main_simple.dart:510:47 • prefer_const_literals_to_create_immutables + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:522:39 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:524:48 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:550:11 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:552:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:568:11 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:570:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:701:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:702:18 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:703:21 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:726:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:727:18 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:728:21 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:729:21 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:740:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:741:18 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:742:21 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:743:21 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:753:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:754:18 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:755:21 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:756:21 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:766:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:767:18 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:768:21 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:769:21 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:779:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:780:18 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:781:21 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:782:21 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:792:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:793:18 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:794:21 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:795:21 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:818:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:819:18 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:820:21 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:821:21 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:831:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:832:18 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:833:21 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:834:21 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:844:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:845:18 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:846:21 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:856:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:857:18 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:858:21 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:868:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:869:18 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:870:21 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:875:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:876:18 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:877:21 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:952:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:953:18 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:957:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:962:20 • prefer_const_constructors + info • Don't use 'BuildContext's across async gaps • lib/main_simple.dart:1028:30 • use_build_context_synchronously + info • Don't use 'BuildContext's across async gaps • lib/main_simple.dart:1035:30 • use_build_context_synchronously + info • Don't use 'BuildContext's across async gaps • lib/main_simple.dart:1043:28 • use_build_context_synchronously + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:1062:18 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:1063:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:1067:22 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:1072:22 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:1143:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:1144:18 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:1148:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:1153:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:1210:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:1227:19 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:1229:28 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:1282:33 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:1283:34 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:1309:23 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:1311:32 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:1367:23 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:1369:32 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:1427:29 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:1429:38 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:1485:29 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:1487:38 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:1561:34 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:1586:33 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:1601:34 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:1621:23 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:1623:32 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:1635:31 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:1636:32 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:1658:33 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:1659:34 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:1662:35 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:1690:19 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:1692:28 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:1723:23 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:1725:32 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:1780:35 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:1854:19 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:1856:28 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:1869:29 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:1871:38 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:1900:32 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:1926:25 • prefer_const_constructors +warning • The declaration '_buildFamilyMember' isn't referenced • lib/main_simple.dart:1944:10 • unused_element +warning • The declaration '_formatDate' isn't referenced • lib/main_simple.dart:1974:10 • unused_element +warning • The declaration '_buildStatRow' isn't referenced • lib/main_simple.dart:1979:10 • unused_element + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:2008:29 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:2028:15 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:2030:24 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:2167:32 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:2169:34 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:2194:21 • prefer_const_constructors + info • Unnecessary 'const' keyword • lib/main_simple.dart:2211:23 • unnecessary_const + info • Unnecessary 'const' keyword • lib/main_simple.dart:2223:23 • unnecessary_const + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:2348:9 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:2350:18 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:2408:28 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:2410:30 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:2425:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:2430:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:2443:20 • prefer_const_constructors + info • Don't use 'BuildContext's across async gaps • lib/main_simple.dart:2459:32 • use_build_context_synchronously +warning • The value of the field '_totpSecret' isn't used • lib/main_simple.dart:2485:11 • unused_field + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:2507:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:2547:26 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:2582:28 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:2583:31 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:2584:31 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:2606:26 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:2666:26 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:2667:29 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:2668:29 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:2713:26 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:2714:29 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:2734:23 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:2761:26 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:2762:29 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:2763:29 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:2804:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:2805:18 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:2809:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:2829:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:2920:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:2962:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:2974:23 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:2977:23 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:2979:32 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:2982:23 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:2984:32 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:3000:23 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:3002:32 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:3013:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:3016:17 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:3041:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:3055:23 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:3057:32 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:3059:23 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:3061:32 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:3099:25 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:3100:26 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:3151:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:3183:13 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:3185:22 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:3232:27 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:3233:28 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:3245:27 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:3246:28 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:3265:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:3266:18 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:3270:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:3286:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:3315:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:3349:13 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:3351:22 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:3377:13 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:3379:22 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:3405:13 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:3407:22 • prefer_const_constructors + error • Invalid constant value • lib/main_simple.dart:3441:28 • invalid_constant + error • The constructor being called isn't a const constructor • lib/main_simple.dart:3445:35 • const_with_non_const + info • Unnecessary 'const' keyword • lib/main_simple.dart:3449:23 • unnecessary_const + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:3492:9 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:3494:18 • prefer_const_constructors + info • Don't use 'BuildContext's across async gaps • lib/main_simple.dart:3555:19 • use_build_context_synchronously + info • Don't use 'BuildContext's across async gaps • lib/main_simple.dart:3556:26 • use_build_context_synchronously +warning • The declaration '_formatLastActive' isn't referenced • lib/main_simple.dart:3624:10 • unused_element +warning • The declaration '_formatFirstLogin' isn't referenced • lib/main_simple.dart:3641:10 • unused_element + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:3655:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:3661:19 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:3662:20 • prefer_const_constructors + info • Unnecessary use of 'toList' in a spread • lib/main_simple.dart:3715:16 • unnecessary_to_list_in_spreads + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:3851:23 • prefer_const_constructors + info • Unnecessary 'const' keyword • lib/main_simple.dart:3863:25 • unnecessary_const +warning • The declaration '_toggleTrust' isn't referenced • lib/main_simple.dart:3876:8 • unused_element + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:3897:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:3913:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:3932:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:3947:13 • prefer_const_constructors + info • Unnecessary 'const' keyword • lib/main_simple.dart:3955:13 • unnecessary_const + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:3965:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:3984:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:4038:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:4044:19 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:4118:23 • prefer_const_constructors + info • Unnecessary 'const' keyword • lib/main_simple.dart:4135:25 • unnecessary_const + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:4158:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:4162:19 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:4176:19 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:4178:28 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:4194:21 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:4195:22 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:4212:17 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:4238:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:4251:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:4252:23 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:4261:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:4262:23 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:4271:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:4272:23 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:4281:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:4282:23 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:4291:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:4292:23 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:4314:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:4327:21 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:4330:27 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:4348:21 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:4351:27 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:4371:17 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:4380:17 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:4394:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:4416:13 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:4418:22 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:4420:13 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:4422:22 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:4425:13 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:4427:22 • prefer_const_constructors + info • Unnecessary 'const' keyword • lib/main_simple.dart:4441:21 • unnecessary_const + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:4451:13 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:4453:22 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:4520:11 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:4542:17 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:4544:26 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:4578:18 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:4602:17 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:4671:11 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:4691:11 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:4693:20 • prefer_const_constructors + info • 'groupValue' is deprecated and shouldn't be used. Use a RadioGroup ancestor to manage group value instead. This feature was deprecated after v3.32.0-0.0.pre • lib/main_simple.dart:4726:27 • deprecated_member_use + info • 'onChanged' is deprecated and shouldn't be used. Use RadioGroup to handle value change instead. This feature was deprecated after v3.32.0-0.0.pre • lib/main_simple.dart:4727:27 • deprecated_member_use + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:4769:18 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:4782:18 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:4846:11 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:4853:11 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:4855:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:4872:21 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:4874:30 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:4882:17 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:4887:26 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:4903:17 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:4905:26 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:4939:18 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_simple.dart:4963:17 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_temp.dart:71:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_temp.dart:124:13 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_temp.dart:126:22 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/main_temp.dart:237:16 • prefer_const_constructors + info • Unnecessary 'const' keyword • lib/main_temp.dart:246:13 • unnecessary_const + info • Use 'const' with the constructor to improve performance • lib/main_temp.dart:266:16 • prefer_const_constructors + info • Unnecessary 'const' keyword • lib/main_temp.dart:275:13 • unnecessary_const + info • Use 'const' with the constructor to improve performance • lib/main_temp.dart:295:16 • prefer_const_constructors + info • Unnecessary 'const' keyword • lib/main_temp.dart:304:13 • unnecessary_const + info • Use 'const' with the constructor to improve performance • lib/main_temp.dart:324:16 • prefer_const_constructors + info • Unnecessary 'const' keyword • lib/main_temp.dart:333:13 • unnecessary_const + info • Use 'const' with the constructor to improve performance • lib/main_temp.dart:353:16 • prefer_const_constructors + info • Unnecessary 'const' keyword • lib/main_temp.dart:362:13 • unnecessary_const + info • 'value' is deprecated and shouldn't be used. Use component accessors like .r or .g, or toARGB32 for an explicit conversion • lib/models/account.dart:104:23 • deprecated_member_use +warning • This default clause is covered by the previous cases • lib/models/account.dart:187:7 • unreachable_switch_default + info • 'value' is deprecated and shouldn't be used. Use component accessors like .r or .g, or toARGB32 for an explicit conversion • lib/models/account.dart:276:23 • deprecated_member_use + info • Unnecessary 'this.' qualifier • lib/models/admin_currency.dart:100:31 • unnecessary_this + info • Unnecessary 'this.' qualifier • lib/models/admin_currency.dart:101:43 • unnecessary_this + info • Dangling library doc comment • lib/models/audit_log.dart:2:1 • dangling_library_doc_comments + info • Use 'const' with the constructor to improve performance • lib/models/category.dart:122:9 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/models/category.dart:128:9 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/models/category.dart:134:9 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/models/category.dart:140:9 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/models/category.dart:146:9 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/models/category.dart:152:9 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/models/category.dart:158:9 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/models/category.dart:166:9 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/models/category.dart:172:9 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/models/category.dart:178:9 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/models/category.dart:184:9 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/models/category.dart:190:9 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/models/category.dart:196:9 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/models/category.dart:202:9 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/models/category.dart:208:9 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/models/category.dart:216:9 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/models/category.dart:222:9 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/models/category.dart:228:9 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/models/category.dart:234:9 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/models/category.dart:240:9 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/models/category.dart:246:9 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/models/category.dart:254:9 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/models/category.dart:260:9 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/models/category.dart:266:9 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/models/category.dart:272:9 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/models/category.dart:278:9 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/models/category.dart:286:9 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/models/category.dart:292:9 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/models/category.dart:298:9 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/models/category.dart:304:9 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/models/category.dart:310:9 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/models/category.dart:316:9 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/models/category.dart:324:9 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/models/category.dart:330:9 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/models/category.dart:336:9 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/models/category.dart:342:9 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/models/category.dart:348:9 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/models/category.dart:356:9 • prefer_const_constructors + info • Statements in an if should be enclosed in a block • lib/models/currency_api.dart:188:7 • curly_braces_in_flow_control_structures + info • Statements in an if should be enclosed in a block • lib/models/currency_api.dart:191:7 • curly_braces_in_flow_control_structures + info • Dangling library doc comment • lib/models/family.dart:1:1 • dangling_library_doc_comments + info • Dangling library doc comment • lib/models/invitation.dart:1:1 • dangling_library_doc_comments + info • 'value' is deprecated and shouldn't be used. Use component accessors like .r or .g, or toARGB32 for an explicit conversion • lib/models/theme_models.dart:260:36 • deprecated_member_use + info • 'value' is deprecated and shouldn't be used. Use component accessors like .r or .g, or toARGB32 for an explicit conversion • lib/models/theme_models.dart:261:40 • deprecated_member_use + info • 'value' is deprecated and shouldn't be used. Use component accessors like .r or .g, or toARGB32 for an explicit conversion • lib/models/theme_models.dart:262:30 • deprecated_member_use + info • 'value' is deprecated and shouldn't be used. Use component accessors like .r or .g, or toARGB32 for an explicit conversion • lib/models/theme_models.dart:263:44 • deprecated_member_use + info • 'value' is deprecated and shouldn't be used. Use component accessors like .r or .g, or toARGB32 for an explicit conversion • lib/models/theme_models.dart:264:32 • deprecated_member_use + info • 'value' is deprecated and shouldn't be used. Use component accessors like .r or .g, or toARGB32 for an explicit conversion • lib/models/theme_models.dart:265:26 • deprecated_member_use + info • 'value' is deprecated and shouldn't be used. Use component accessors like .r or .g, or toARGB32 for an explicit conversion • lib/models/theme_models.dart:266:40 • deprecated_member_use + info • 'value' is deprecated and shouldn't be used. Use component accessors like .r or .g, or toARGB32 for an explicit conversion • lib/models/theme_models.dart:267:30 • deprecated_member_use + info • 'value' is deprecated and shouldn't be used. Use component accessors like .r or .g, or toARGB32 for an explicit conversion • lib/models/theme_models.dart:268:34 • deprecated_member_use + info • 'value' is deprecated and shouldn't be used. Use component accessors like .r or .g, or toARGB32 for an explicit conversion • lib/models/theme_models.dart:269:36 • deprecated_member_use + info • 'value' is deprecated and shouldn't be used. Use component accessors like .r or .g, or toARGB32 for an explicit conversion • lib/models/theme_models.dart:270:30 • deprecated_member_use + info • 'value' is deprecated and shouldn't be used. Use component accessors like .r or .g, or toARGB32 for an explicit conversion • lib/models/theme_models.dart:271:22 • deprecated_member_use + info • 'value' is deprecated and shouldn't be used. Use component accessors like .r or .g, or toARGB32 for an explicit conversion • lib/models/theme_models.dart:272:26 • deprecated_member_use + info • 'value' is deprecated and shouldn't be used. Use component accessors like .r or .g, or toARGB32 for an explicit conversion • lib/models/theme_models.dart:273:26 • deprecated_member_use + info • 'value' is deprecated and shouldn't be used. Use component accessors like .r or .g, or toARGB32 for an explicit conversion • lib/models/theme_models.dart:274:26 • deprecated_member_use + info • 'value' is deprecated and shouldn't be used. Use component accessors like .r or .g, or toARGB32 for an explicit conversion • lib/models/theme_models.dart:275:20 • deprecated_member_use + info • 'value' is deprecated and shouldn't be used. Use component accessors like .r or .g, or toARGB32 for an explicit conversion • lib/models/theme_models.dart:276:30 • deprecated_member_use + info • 'value' is deprecated and shouldn't be used. Use component accessors like .r or .g, or toARGB32 for an explicit conversion • lib/models/theme_models.dart:277:36 • deprecated_member_use + info • 'value' is deprecated and shouldn't be used. Use component accessors like .r or .g, or toARGB32 for an explicit conversion • lib/models/theme_models.dart:278:34 • deprecated_member_use + info • 'value' is deprecated and shouldn't be used. Use component accessors like .r or .g, or toARGB32 for an explicit conversion • lib/models/theme_models.dart:279:38 • deprecated_member_use + info • 'value' is deprecated and shouldn't be used. Use component accessors like .r or .g, or toARGB32 for an explicit conversion • lib/models/theme_models.dart:280:42 • deprecated_member_use + info • 'value' is deprecated and shouldn't be used. Use component accessors like .r or .g, or toARGB32 for an explicit conversion • lib/models/theme_models.dart:281:32 • deprecated_member_use + info • 'value' is deprecated and shouldn't be used. Use component accessors like .r or .g, or toARGB32 for an explicit conversion • lib/models/theme_models.dart:282:38 • deprecated_member_use + info • 'value' is deprecated and shouldn't be used. Use component accessors like .r or .g, or toARGB32 for an explicit conversion • lib/models/theme_models.dart:283:46 • deprecated_member_use + info • 'value' is deprecated and shouldn't be used. Use component accessors like .r or .g, or toARGB32 for an explicit conversion • lib/models/theme_models.dart:284:54 • deprecated_member_use + info • 'value' is deprecated and shouldn't be used. Use component accessors like .r or .g, or toARGB32 for an explicit conversion • lib/models/transaction.dart:294:49 • deprecated_member_use + info • 'value' is deprecated and shouldn't be used. Use component accessors like .r or .g, or toARGB32 for an explicit conversion • lib/models/transaction.dart:313:22 • deprecated_member_use + info • Use 'const' with the constructor to improve performance • lib/models/travel_event.dart:73:7 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/models/travel_event.dart:90:7 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/models/travel_event.dart:112:7 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/models/travel_event.dart:127:7 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/models/travel_event.dart:143:7 • prefer_const_constructors +warning • The receiver can't be null, so the null-aware operator '?.' is unnecessary • lib/providers/auth_provider.dart:121:62 • invalid_null_aware_operator +warning • The receiver can't be null, so the null-aware operator '?.' is unnecessary • lib/providers/auth_provider.dart:138:68 • invalid_null_aware_operator + info • The private field _currencyCache could be 'final' • lib/providers/currency_provider.dart:116:25 • prefer_final_fields + info • Use 'const' with the constructor to improve performance • lib/providers/rule_provider.dart:22:11 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/providers/rule_provider.dart:30:11 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/providers/rule_provider.dart:35:11 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/providers/rule_provider.dart:55:11 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/providers/rule_provider.dart:61:11 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/providers/rule_provider.dart:69:11 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/providers/rule_provider.dart:74:11 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/providers/rule_provider.dart:94:11 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/providers/rule_provider.dart:102:11 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/providers/rule_provider.dart:107:11 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/providers/rule_provider.dart:127:11 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/providers/rule_provider.dart:135:11 • prefer_const_constructors +warning • This default clause is covered by the previous cases • lib/providers/settings_provider.dart:48:7 • unreachable_switch_default +warning • This default clause is covered by the previous cases • lib/providers/settings_provider.dart:232:7 • unreachable_switch_default +warning • The value of the local variable 'event' isn't used • lib/providers/travel_event_provider.dart:95:11 • unused_local_variable +warning • The value of the local variable 'currentLedger' isn't used • lib/screens/accounts/account_add_screen.dart:50:11 • unused_local_variable + info • Use 'const' with the constructor to improve performance • lib/screens/accounts/account_add_screen.dart:54:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/accounts/account_add_screen.dart:58:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/accounts/account_add_screen.dart:74:21 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/accounts/account_add_screen.dart:76:30 • prefer_const_constructors + info • Unnecessary 'const' keyword • lib/screens/accounts/account_add_screen.dart:114:31 • unnecessary_const + info • Unnecessary 'const' keyword • lib/screens/accounts/account_add_screen.dart:124:31 • unnecessary_const + info • Unnecessary 'const' keyword • lib/screens/accounts/account_add_screen.dart:134:31 • unnecessary_const + info • Unnecessary 'const' keyword • lib/screens/accounts/account_add_screen.dart:144:31 • unnecessary_const + info • Unnecessary 'const' keyword • lib/screens/accounts/account_add_screen.dart:154:31 • unnecessary_const + info • Unnecessary 'const' keyword • lib/screens/accounts/account_add_screen.dart:164:31 • unnecessary_const + info • Unnecessary 'const' keyword • lib/screens/accounts/account_add_screen.dart:174:31 • unnecessary_const + info • Use 'const' with the constructor to improve performance • lib/screens/accounts/account_add_screen.dart:203:37 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/accounts/account_add_screen.dart:229:21 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/accounts/account_add_screen.dart:231:30 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/accounts/account_add_screen.dart:279:25 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/accounts/account_add_screen.dart:281:34 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/accounts/account_add_screen.dart:313:39 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/accounts/account_add_screen.dart:362:30 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/accounts/account_add_screen.dart:363:33 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/accounts/account_add_screen.dart:372:30 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/accounts/account_add_screen.dart:373:33 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/accounts/account_add_screen.dart:392:15 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/accounts/account_add_screen.dart:393:16 • prefer_const_constructors +warning • The value of the local variable 'account' isn't used • lib/screens/accounts/account_add_screen.dart:411:13 • unused_local_variable + info • 'value' is deprecated and shouldn't be used. Use component accessors like .r or .g, or toARGB32 for an explicit conversion • lib/screens/accounts/account_add_screen.dart:419:33 • deprecated_member_use + info • Use 'const' with the constructor to improve performance • lib/screens/accounts/account_detail_screen.dart:15:16 • prefer_const_constructors + info • The private field _selectedGroupId could be 'final' • lib/screens/accounts/accounts_screen.dart:18:10 • prefer_final_fields +warning • The value of the field '_selectedGroupId' isn't used • lib/screens/accounts/accounts_screen.dart:18:10 • unused_field + info • Use 'const' with the constructor to improve performance • lib/screens/accounts/accounts_screen.dart:28:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/accounts/accounts_screen.dart:75:15 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/accounts/accounts_screen.dart:76:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/accounts/accounts_screen.dart:92:13 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/accounts/accounts_screen.dart:98:22 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/accounts/accounts_screen.dart:146:19 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/accounts/accounts_screen.dart:147:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/accounts/accounts_screen.dart:378:30 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/accounts/accounts_screen.dart:380:32 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/accounts/accounts_screen.dart:422:24 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/accounts/accounts_screen.dart:423:22 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/accounts/accounts_screen.dart:430:24 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/accounts/accounts_screen.dart:438:24 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/accounts/accounts_screen.dart:439:22 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/accounts/accounts_screen.dart:446:24 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/accounts/accounts_screen.dart:447:22 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/accounts/accounts_screen.dart:447:42 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/accounts/accounts_screen.dart:463:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/accounts/accounts_screen.dart:468:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/accounts/accounts_screen.dart:475:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/accounts/accounts_screen.dart:475:38 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/add_transaction_page.dart:104:27 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/add_transaction_page.dart:108:19 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/add_transaction_page.dart:110:28 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/add_transaction_page.dart:119:28 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/add_transaction_page.dart:121:30 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/add_transaction_page.dart:229:27 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/add_transaction_page.dart:231:36 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/add_transaction_page.dart:261:21 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/add_transaction_page.dart:263:30 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/add_transaction_page.dart:342:27 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/add_transaction_page.dart:344:36 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/add_transaction_page.dart:374:27 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/add_transaction_page.dart:395:36 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/admin/currency_admin_screen.dart:28:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/admin/currency_admin_screen.dart:31:19 • prefer_const_constructors + info • Don't use 'BuildContext's across async gaps, guarded by an unrelated 'mounted' check • lib/screens/admin/currency_admin_screen.dart:73:54 • use_build_context_synchronously + info • Use 'const' with the constructor to improve performance • lib/screens/admin/currency_admin_screen.dart:77:35 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/admin/currency_admin_screen.dart:78:36 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/admin/currency_admin_screen.dart:81:27 • prefer_const_constructors + info • 'surfaceVariant' is deprecated and shouldn't be used. Use surfaceContainerHighest instead. This feature was deprecated after v3.18.0-0.1.pre • lib/screens/admin/currency_admin_screen.dart:112:23 • deprecated_member_use + info • 'surfaceVariant' is deprecated and shouldn't be used. Use surfaceContainerHighest instead. This feature was deprecated after v3.18.0-0.1.pre • lib/screens/admin/currency_admin_screen.dart:128:27 • deprecated_member_use + info • Use 'const' with the constructor to improve performance • lib/screens/admin/currency_admin_screen.dart:227:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/admin/currency_admin_screen.dart:269:28 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/admin/currency_admin_screen.dart:279:26 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/admin/currency_admin_screen.dart:289:22 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/admin/currency_admin_screen.dart:292:22 • prefer_const_constructors + info • Don't use 'BuildContext's across async gaps, guarded by an unrelated 'mounted' check • lib/screens/admin/currency_admin_screen.dart:306:30 • use_build_context_synchronously + info • Use 'const' with the constructor to improve performance • lib/screens/admin/currency_admin_screen.dart:403:26 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/admin/currency_admin_screen.dart:417:26 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/admin/currency_admin_screen.dart:426:61 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/admin/currency_admin_screen.dart:427:51 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/admin/super_admin_screen.dart:101:13 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/admin/super_admin_screen.dart:143:11 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/admin/super_admin_screen.dart:145:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/admin/super_admin_screen.dart:206:11 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/admin/super_admin_screen.dart:208:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/admin/super_admin_screen.dart:254:15 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/admin/super_admin_screen.dart:255:24 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/admin/super_admin_screen.dart:266:23 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/admin/super_admin_screen.dart:267:24 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/admin/super_admin_screen.dart:325:9 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/admin/super_admin_screen.dart:327:18 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/admin/super_admin_screen.dart:383:29 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/admin/super_admin_screen.dart:396:15 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/admin/super_admin_screen.dart:397:24 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/admin/super_admin_screen.dart:484:16 • prefer_const_constructors + info • Unnecessary 'const' keyword • lib/screens/admin/super_admin_screen.dart:489:13 • unnecessary_const + info • Unnecessary 'const' keyword • lib/screens/admin/super_admin_screen.dart:491:13 • unnecessary_const + info • Use 'const' with the constructor to improve performance • lib/screens/admin/super_admin_screen.dart:498:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/admin/super_admin_screen.dart:502:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/admin/super_admin_screen.dart:531:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/admin/super_admin_screen.dart:535:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/admin/super_admin_screen.dart:546:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/admin/super_admin_screen.dart:551:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/admin/super_admin_screen.dart:556:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/admin/super_admin_screen.dart:567:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/admin/super_admin_screen.dart:572:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/admin/super_admin_screen.dart:577:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/admin/super_admin_screen.dart:588:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/admin/super_admin_screen.dart:603:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/admin/super_admin_screen.dart:607:20 • prefer_const_constructors +warning • Unused import: '../../models/user.dart' • lib/screens/admin/template_admin_page.dart:5:8 • unused_import +warning • Unused import: '../../providers/current_user_provider.dart' • lib/screens/admin/template_admin_page.dart:6:8 • unused_import +warning • Unused import: '../../widgets/common/error_widget.dart' • lib/screens/admin/template_admin_page.dart:9:8 • unused_import + info • Parameter 'key' could be a super parameter • lib/screens/admin/template_admin_page.dart:16:9 • use_super_parameters +warning • The value of the field '_editingTemplate' isn't used • lib/screens/admin/template_admin_page.dart:41:27 • unused_field + info • The type of the right operand ('AccountClassification?') isn't a subtype or a supertype of the left operand ('CategoryClassification') • lib/screens/admin/template_admin_page.dart:114:39 • unrelated_type_equality_checks + info • Don't use 'BuildContext's across async gaps • lib/screens/admin/template_admin_page.dart:142:36 • use_build_context_synchronously + info • Don't use 'BuildContext's across async gaps • lib/screens/admin/template_admin_page.dart:150:36 • use_build_context_synchronously + info • Don't use 'BuildContext's across async gaps • lib/screens/admin/template_admin_page.dart:157:27 • use_build_context_synchronously + info • Don't use 'BuildContext's across async gaps • lib/screens/admin/template_admin_page.dart:160:34 • use_build_context_synchronously + info • Use 'const' with the constructor to improve performance • lib/screens/admin/template_admin_page.dart:179:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/admin/template_admin_page.dart:184:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/admin/template_admin_page.dart:191:20 • prefer_const_constructors + info • Don't use 'BuildContext's across async gaps • lib/screens/admin/template_admin_page.dart:200:30 • use_build_context_synchronously + info • Don't use 'BuildContext's across async gaps • lib/screens/admin/template_admin_page.dart:208:30 • use_build_context_synchronously + info • Don't use 'BuildContext's across async gaps • lib/screens/admin/template_admin_page.dart:222:28 • use_build_context_synchronously + info • Don't use 'BuildContext's across async gaps • lib/screens/admin/template_admin_page.dart:231:28 • use_build_context_synchronously + info • Use 'const' with the constructor to improve performance • lib/screens/admin/template_admin_page.dart:245:18 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/admin/template_admin_page.dart:251:15 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/admin/template_admin_page.dart:272:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/admin/template_admin_page.dart:296:19 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/admin/template_admin_page.dart:301:19 • prefer_const_constructors + error • 1 positional argument expected by 'ErrorWidget.new', but 0 found • lib/screens/admin/template_admin_page.dart:311:19 • not_enough_positional_arguments + error • The named parameter 'message' isn't defined • lib/screens/admin/template_admin_page.dart:311:19 • undefined_named_parameter + error • The named parameter 'onRetry' isn't defined • lib/screens/admin/template_admin_page.dart:312:19 • undefined_named_parameter + info • Use 'const' with the constructor to improve performance • lib/screens/admin/template_admin_page.dart:345:27 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/admin/template_admin_page.dart:348:29 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/admin/template_admin_page.dart:410:19 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/admin/template_admin_page.dart:535:24 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/admin/template_admin_page.dart:537:26 • prefer_const_constructors + error • The argument type 'CategoryClassification' can't be assigned to the parameter type 'AccountClassification'. • lib/screens/admin/template_admin_page.dart:550:81 • argument_type_not_assignable + info • Use 'const' with the constructor to improve performance • lib/screens/admin/template_admin_page.dart:592:21 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/admin/template_admin_page.dart:597:21 • prefer_const_constructors +warning • This default clause is covered by the previous cases • lib/screens/admin/template_admin_page.dart:615:7 • unreachable_switch_default + error • A value of type 'CategoryClassification' can't be assigned to a variable of type 'AccountClassification' • lib/screens/admin/template_admin_page.dart:712:25 • invalid_assignment + info • Use 'const' with the constructor to improve performance • lib/screens/admin/template_admin_page.dart:907:32 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/admin/template_admin_page.dart:918:32 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/admin/template_admin_page.dart:937:30 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/admin/template_admin_page.dart:942:30 • prefer_const_constructors + error • The argument type 'AccountClassification' can't be assigned to the parameter type 'CategoryClassification'. • lib/screens/admin/template_admin_page.dart:965:25 • argument_type_not_assignable +warning • This default clause is covered by the previous cases • lib/screens/admin/template_admin_page.dart:999:7 • unreachable_switch_default + info • Use 'const' with the constructor to improve performance • lib/screens/ai_assistant_page.dart:140:13 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/ai_assistant_page.dart:143:22 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/ai_assistant_page.dart:174:27 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/ai_assistant_page.dart:184:19 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/ai_assistant_page.dart:186:28 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/ai_assistant_page.dart:227:36 • prefer_const_constructors + info • Use 'const' literals as arguments to constructors of '@immutable' classes • lib/screens/ai_assistant_page.dart:229:41 • prefer_const_literals_to_create_immutables + info • Use 'const' with the constructor to improve performance • lib/screens/ai_assistant_page.dart:240:33 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/ai_assistant_page.dart:296:29 • prefer_const_constructors + info • The import of '../../models/audit_log.dart' is unnecessary because all of the used elements are also provided by the import of '../../services/audit_service.dart' • lib/screens/audit/audit_logs_screen.dart:3:8 • unnecessary_import + error • The argument type 'AuditLogFilter' can't be assigned to the parameter type 'String?'. • lib/screens/audit/audit_logs_screen.dart:74:17 • argument_type_not_assignable + error • Too many positional arguments: 0 expected, but 1 found • lib/screens/audit/audit_logs_screen.dart:110:60 • extra_positional_arguments_could_be_named + error • A value of type 'Map' can't be assigned to a variable of type 'AuditLogStatistics?' • lib/screens/audit/audit_logs_screen.dart:112:23 • invalid_assignment + info • Use 'const' with the constructor to improve performance • lib/screens/audit/audit_logs_screen.dart:167:13 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/audit/audit_logs_screen.dart:176:19 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/audit/audit_logs_screen.dart:183:19 • prefer_const_constructors + info • 'surfaceVariant' is deprecated and shouldn't be used. Use surfaceContainerHighest instead. This feature was deprecated after v3.18.0-0.1.pre • lib/screens/audit/audit_logs_screen.dart:391:34 • deprecated_member_use + info • Use 'const' with the constructor to improve performance • lib/screens/audit/audit_logs_screen.dart:405:27 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/audit/audit_logs_screen.dart:408:29 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/audit/audit_logs_screen.dart:438:27 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/audit/audit_logs_screen.dart:447:27 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/audit/audit_logs_screen.dart:458:27 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/audit/audit_logs_screen.dart:468:28 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/audit/audit_logs_screen.dart:470:29 • prefer_const_constructors + info • 'surfaceVariant' is deprecated and shouldn't be used. Use surfaceContainerHighest instead. This feature was deprecated after v3.18.0-0.1.pre • lib/screens/audit/audit_logs_screen.dart:569:49 • deprecated_member_use + info • Use 'const' with the constructor to improve performance • lib/screens/audit/audit_logs_screen.dart:586:23 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/audit/audit_logs_screen.dart:706:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/audit/audit_logs_screen.dart:732:20 • prefer_const_constructors + error • Invalid constant value • lib/screens/audit/audit_logs_screen.dart:748:17 • invalid_constant + info • Unnecessary 'const' keyword • lib/screens/audit/audit_logs_screen.dart:749:22 • unnecessary_const + info • Use 'const' with the constructor to improve performance • lib/screens/audit/audit_logs_screen.dart:819:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/audit/audit_logs_screen.dart:820:18 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/audit/audit_logs_screen.dart:824:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/audit/audit_logs_screen.dart:834:20 • prefer_const_constructors + info • The import of 'package:flutter/foundation.dart' is unnecessary because all of the used elements are also provided by the import of 'package:flutter/material.dart' • lib/screens/auth/admin_login_screen.dart:2:8 • unnecessary_import + info • Use 'const' with the constructor to improve performance • lib/screens/auth/admin_login_screen.dart:99:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/auth/admin_login_screen.dart:122:21 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/auth/admin_login_screen.dart:124:30 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/auth/admin_login_screen.dart:131:21 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/auth/admin_login_screen.dart:133:30 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/auth/admin_login_screen.dart:190:37 • prefer_const_constructors + error • Invalid constant value • lib/screens/auth/admin_login_screen.dart:245:36 • invalid_constant + info • Unnecessary 'const' keyword • lib/screens/auth/admin_login_screen.dart:251:31 • unnecessary_const + info • Unnecessary 'const' keyword • lib/screens/auth/admin_login_screen.dart:257:40 • unnecessary_const + info • Use 'const' with the constructor to improve performance • lib/screens/auth/admin_login_screen.dart:277:33 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/auth/admin_login_screen.dart:279:42 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/auth/admin_login_screen.dart:284:29 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/auth/admin_login_screen.dart:288:38 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/auth/admin_login_screen.dart:305:30 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/auth/login_page.dart:138:25 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/auth/login_page.dart:154:26 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/auth/login_screen.dart:180:21 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/auth/login_screen.dart:182:30 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/auth/login_screen.dart:189:21 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/auth/login_screen.dart:191:30 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/auth/login_screen.dart:238:37 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/auth/login_screen.dart:283:29 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/auth/login_screen.dart:295:29 • prefer_const_constructors + info • Don't use 'BuildContext's across async gaps, guarded by an unrelated 'mounted' check • lib/screens/auth/login_screen.dart:310:56 • use_build_context_synchronously + info • Use 'const' with the constructor to improve performance • lib/screens/auth/login_screen.dart:318:38 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/auth/login_screen.dart:320:40 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/auth/login_screen.dart:338:31 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/auth/login_screen.dart:340:40 • prefer_const_constructors + error • Invalid constant value • lib/screens/auth/login_screen.dart:442:36 • invalid_constant + info • Unnecessary 'const' keyword • lib/screens/auth/login_screen.dart:448:31 • unnecessary_const + info • Use 'const' with the constructor to improve performance • lib/screens/auth/login_screen.dart:465:30 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/auth/login_screen.dart:476:30 • prefer_const_constructors + info • Don't use 'BuildContext's across async gaps • lib/screens/auth/login_screen.dart:508:48 • use_build_context_synchronously + info • Don't use 'BuildContext's across async gaps • lib/screens/auth/login_screen.dart:515:27 • use_build_context_synchronously + info • Don't use 'BuildContext's across async gaps • lib/screens/auth/login_screen.dart:517:48 • use_build_context_synchronously + info • Use 'const' with the constructor to improve performance • lib/screens/auth/login_screen.dart:567:33 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/auth/login_screen.dart:569:42 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/auth/login_screen.dart:576:29 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/auth/login_screen.dart:584:35 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/auth/register_screen.dart:121:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/auth/register_screen.dart:144:21 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/auth/register_screen.dart:146:30 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/auth/register_screen.dart:229:37 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/auth/register_screen.dart:271:37 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/auth/register_screen.dart:318:36 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/auth/register_screen.dart:320:38 • prefer_const_constructors + error • Invalid constant value • lib/screens/auth/register_screen.dart:332:36 • invalid_constant + info • Unnecessary 'const' keyword • lib/screens/auth/register_screen.dart:338:31 • unnecessary_const + info • Use 'const' with the constructor to improve performance • lib/screens/auth/register_screen.dart:400:30 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/auth/register_screen.dart:406:21 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/auth/register_screen.dart:407:30 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/auth/register_screen.dart:409:32 • prefer_const_constructors + info • Use 'const' literals as arguments to constructors of '@immutable' classes • lib/screens/auth/register_screen.dart:411:37 • prefer_const_literals_to_create_immutables + info • Use 'const' with the constructor to improve performance • lib/screens/auth/register_screen.dart:412:29 • prefer_const_constructors + info • Use 'const' literals as arguments to constructors of '@immutable' classes • lib/screens/auth/register_screen.dart:413:41 • prefer_const_literals_to_create_immutables + info • Use 'const' with the constructor to improve performance • lib/screens/auth/register_screen.dart:414:33 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/auth/register_screen.dart:416:33 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/auth/register_screen.dart:418:42 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/auth/register_screen.dart:426:29 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/auth/register_screen.dart:432:35 • prefer_const_constructors + info • The import of 'package:flutter/services.dart' is unnecessary because all of the used elements are also provided by the import of 'package:flutter/material.dart' • lib/screens/auth/registration_wizard.dart:2:8 • unnecessary_import + info • The import of 'package:flutter/foundation.dart' is unnecessary because all of the used elements are also provided by the import of 'package:flutter/material.dart' • lib/screens/auth/registration_wizard.dart:3:8 • unnecessary_import + info • Use 'const' with the constructor to improve performance • lib/screens/auth/registration_wizard.dart:284:32 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/auth/registration_wizard.dart:286:34 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/auth/registration_wizard.dart:331:13 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/auth/registration_wizard.dart:333:22 • prefer_const_constructors + info • 'MaterialStateProperty' is deprecated and shouldn't be used. Use WidgetStateProperty instead. Moved to the Widgets layer to make code available outside of Material. This feature was deprecated after v3.19.0-0.3.pre • lib/screens/auth/registration_wizard.dart:521:30 • deprecated_member_use + info • 'MaterialState' is deprecated and shouldn't be used. Use WidgetState instead. Moved to the Widgets layer to make code available outside of Material. This feature was deprecated after v3.19.0-0.3.pre • lib/screens/auth/registration_wizard.dart:522:41 • deprecated_member_use + info • Use 'const' with the constructor to improve performance • lib/screens/auth/registration_wizard.dart:557:13 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/auth/registration_wizard.dart:559:22 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/auth/registration_wizard.dart:616:21 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/auth/registration_wizard.dart:678:13 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/auth/registration_wizard.dart:680:22 • prefer_const_constructors + info • Use interpolation to compose strings and values • lib/screens/auth/registration_wizard.dart:716:21 • prefer_interpolation_to_compose_strings + info • 'value' is deprecated and shouldn't be used. Use initialValue instead. This will set the initial value for the form field. This feature was deprecated after v3.33.0-1.0.pre • lib/screens/auth/registration_wizard.dart:764:15 • deprecated_member_use + info • 'value' is deprecated and shouldn't be used. Use initialValue instead. This will set the initial value for the form field. This feature was deprecated after v3.33.0-1.0.pre • lib/screens/auth/registration_wizard.dart:795:15 • deprecated_member_use + info • 'value' is deprecated and shouldn't be used. Use initialValue instead. This will set the initial value for the form field. This feature was deprecated after v3.33.0-1.0.pre • lib/screens/auth/registration_wizard.dart:824:15 • deprecated_member_use + info • 'value' is deprecated and shouldn't be used. Use initialValue instead. This will set the initial value for the form field. This feature was deprecated after v3.33.0-1.0.pre • lib/screens/auth/registration_wizard.dart:853:15 • deprecated_member_use + info • 'value' is deprecated and shouldn't be used. Use initialValue instead. This will set the initial value for the form field. This feature was deprecated after v3.33.0-1.0.pre • lib/screens/auth/registration_wizard.dart:883:15 • deprecated_member_use + info • Don't use 'BuildContext's across async gaps • lib/screens/auth/wechat_qr_screen.dart:103:28 • use_build_context_synchronously + info • Don't use 'BuildContext's across async gaps • lib/screens/auth/wechat_qr_screen.dart:110:49 • use_build_context_synchronously + info • Don't use 'BuildContext's across async gaps • lib/screens/auth/wechat_qr_screen.dart:120:30 • use_build_context_synchronously + info • Use 'const' with the constructor to improve performance • lib/screens/auth/wechat_qr_screen.dart:156:15 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/auth/wechat_qr_screen.dart:158:24 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/auth/wechat_qr_screen.dart:259:49 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/auth/wechat_qr_screen.dart:260:49 • prefer_const_constructors + info • Use 'const' literals as arguments to constructors of '@immutable' classes • lib/screens/auth/wechat_qr_screen.dart:261:49 • prefer_const_literals_to_create_immutables + info • Use 'const' with the constructor to improve performance • lib/screens/auth/wechat_qr_screen.dart:289:43 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/auth/wechat_qr_screen.dart:294:43 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/auth/wechat_qr_screen.dart:297:43 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/auth/wechat_qr_screen.dart:302:43 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/auth/wechat_qr_screen.dart:355:24 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/auth/wechat_qr_screen.dart:357:26 • prefer_const_constructors + info • Don't use 'BuildContext's across async gaps • lib/screens/auth/wechat_register_form_screen.dart:93:24 • use_build_context_synchronously + info • Don't use 'BuildContext's across async gaps • lib/screens/auth/wechat_register_form_screen.dart:100:32 • use_build_context_synchronously + info • Don't use 'BuildContext's across async gaps • lib/screens/auth/wechat_register_form_screen.dart:107:24 • use_build_context_synchronously + info • Don't use 'BuildContext's across async gaps • lib/screens/auth/wechat_register_form_screen.dart:114:30 • use_build_context_synchronously + info • Don't use 'BuildContext's across async gaps • lib/screens/auth/wechat_register_form_screen.dart:122:28 • use_build_context_synchronously + info • Use 'const' with the constructor to improve performance • lib/screens/auth/wechat_register_form_screen.dart:141:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/auth/wechat_register_form_screen.dart:168:33 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/auth/wechat_register_form_screen.dart:178:35 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/auth/wechat_register_form_screen.dart:207:40 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/auth/wechat_register_form_screen.dart:209:42 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/auth/wechat_register_form_screen.dart:226:17 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/auth/wechat_register_form_screen.dart:228:26 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/auth/wechat_register_form_screen.dart:296:33 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/auth/wechat_register_form_screen.dart:367:33 • prefer_const_constructors + error • Invalid constant value • lib/screens/auth/wechat_register_form_screen.dart:401:32 • invalid_constant + info • Unnecessary 'const' keyword • lib/screens/auth/wechat_register_form_screen.dart:407:27 • unnecessary_const + info • Use 'const' with the constructor to improve performance • lib/screens/auth/wechat_register_form_screen.dart:432:29 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/auth/wechat_register_form_screen.dart:434:38 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/auth/wechat_register_form_screen.dart:440:25 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/auth/wechat_register_form_screen.dart:445:34 • prefer_const_constructors +warning • The value of the local variable 'currentMonth' isn't used • lib/screens/budgets/budgets_screen.dart:15:11 • unused_local_variable + info • Use 'const' with the constructor to improve performance • lib/screens/budgets/budgets_screen.dart:21:18 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/budgets/budgets_screen.dart:30:21 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/budgets/budgets_screen.dart:34:21 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/budgets/budgets_screen.dart:47:17 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/budgets/budgets_screen.dart:48:18 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/budgets/budgets_screen.dart:65:13 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/budgets/budgets_screen.dart:72:22 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/budgets/budgets_screen.dart:115:23 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/budgets/budgets_screen.dart:117:32 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/budgets/budgets_screen.dart:138:27 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/budgets/budgets_screen.dart:140:36 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/budgets/budgets_screen.dart:187:11 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/budgets/budgets_screen.dart:189:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/budgets/budgets_screen.dart:234:19 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/budgets/budgets_screen.dart:235:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/budgets/budgets_screen.dart:272:19 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/budgets/budgets_screen.dart:273:20 • prefer_const_constructors + info • Use interpolation to compose strings and values • lib/screens/budgets/budgets_screen.dart:425:23 • prefer_interpolation_to_compose_strings + info • Use interpolation to compose strings and values • lib/screens/budgets/budgets_screen.dart:438:23 • prefer_interpolation_to_compose_strings +warning • The value of the local variable 'baseCurrency' isn't used • lib/screens/currency/currency_converter_screen.dart:76:11 • unused_local_variable + info • Use 'const' with the constructor to improve performance • lib/screens/currency/currency_converter_screen.dart:82:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/currency/currency_converter_screen.dart:114:19 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/currency/currency_converter_screen.dart:141:21 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/currency/currency_converter_screen.dart:164:19 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/currency/currency_converter_screen.dart:207:23 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/currency/currency_converter_screen.dart:234:21 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/currency/currency_converter_screen.dart:235:22 • prefer_const_constructors + info • The import of '../../providers/currency_provider.dart' is unnecessary because all of the used elements are also provided by the import of '../../providers/currency_provider.dart' • lib/screens/currency/exchange_rate_screen.dart:4:8 • unnecessary_import + info • Use 'const' with the constructor to improve performance • lib/screens/currency/exchange_rate_screen.dart:111:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/currency/exchange_rate_screen.dart:123:19 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/currency/exchange_rate_screen.dart:146:21 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/currency/exchange_rate_screen.dart:164:30 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/currency/exchange_rate_screen.dart:175:21 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/currency/exchange_rate_screen.dart:191:30 • prefer_const_constructors + info • 'value' is deprecated and shouldn't be used. Use initialValue instead. This will set the initial value for the form field. This feature was deprecated after v3.33.0-1.0.pre • lib/screens/currency/exchange_rate_screen.dart:223:15 • deprecated_member_use + info • Use 'const' with the constructor to improve performance • lib/screens/currency/exchange_rate_screen.dart:264:23 • prefer_const_constructors + info • 'value' is deprecated and shouldn't be used. Use initialValue instead. This will set the initial value for the form field. This feature was deprecated after v3.33.0-1.0.pre • lib/screens/currency/exchange_rate_screen.dart:281:15 • deprecated_member_use + info • Use 'const' with the constructor to improve performance • lib/screens/currency/exchange_rate_screen.dart:336:23 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/currency/exchange_rate_screen.dart:365:21 • prefer_const_constructors + info • The import of 'package:flutter/foundation.dart' is unnecessary because all of the used elements are also provided by the import of 'package:flutter/material.dart' • lib/screens/currency_converter_page.dart:2:8 • unnecessary_import + info • Use 'const' with the constructor to improve performance • lib/screens/currency_converter_page.dart:79:27 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/currency_converter_page.dart:83:19 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/currency_converter_page.dart:85:28 • prefer_const_constructors + error • Arguments of a constant creation must be constant expressions • lib/screens/currency_converter_page.dart:101:33 • const_with_non_constant_argument + info • Use 'const' with the constructor to improve performance • lib/screens/currency_converter_page.dart:195:32 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/currency_converter_page.dart:203:35 • prefer_const_constructors + error • Arguments of a constant creation must be constant expressions • lib/screens/currency_converter_page.dart:304:55 • const_with_non_constant_argument + info • Use 'const' with the constructor to improve performance • lib/screens/dashboard/dashboard_screen.dart:28:13 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/dashboard/dashboard_screen.dart:44:19 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/dashboard/dashboard_screen.dart:126:17 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/dashboard/dashboard_screen.dart:190:18 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/dashboard/dashboard_screen.dart:235:20 • prefer_const_constructors +warning • The declaration '_showLedgerSwitcher' isn't referenced • lib/screens/dashboard/dashboard_screen.dart:255:8 • unused_element + info • Use 'const' with the constructor to improve performance • lib/screens/dashboard/dashboard_screen.dart:292:23 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/dashboard/dashboard_screen.dart:293:24 • prefer_const_constructors + error • The constructor being called isn't a const constructor • lib/screens/dashboard/dashboard_screen.dart:335:20 • const_with_non_const + info • The import of '../../models/audit_log.dart' is unnecessary because all of the used elements are also provided by the import of '../../services/audit_service.dart' • lib/screens/family/family_activity_log_screen.dart:4:8 • unnecessary_import + info • Parameter 'key' could be a super parameter • lib/screens/family/family_activity_log_screen.dart:13:9 • use_super_parameters + info • The private field _groupedLogs could be 'final' • lib/screens/family/family_activity_log_screen.dart:31:31 • prefer_final_fields + error • The named parameter 'actionType' isn't defined • lib/screens/family/family_activity_log_screen.dart:77:9 • undefined_named_parameter + error • The argument type 'AuditLogFilter' can't be assigned to the parameter type 'String?'. • lib/screens/family/family_activity_log_screen.dart:86:17 • argument_type_not_assignable + error • Too many positional arguments: 0 expected, but 1 found • lib/screens/family/family_activity_log_screen.dart:120:63 • extra_positional_arguments_could_be_named + error • A value of type 'Map' can't be assigned to a variable of type 'ActivityStatistics?' • lib/screens/family/family_activity_log_screen.dart:121:36 • invalid_assignment + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_activity_log_screen.dart:148:13 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_activity_log_screen.dart:157:19 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_activity_log_screen.dart:161:19 • prefer_const_constructors + info • 'surfaceVariant' is deprecated and shouldn't be used. Use surfaceContainerHighest instead. This feature was deprecated after v3.18.0-0.1.pre • lib/screens/family/family_activity_log_screen.dart:171:38 • deprecated_member_use + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_activity_log_screen.dart:176:29 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_activity_log_screen.dart:179:31 • prefer_const_constructors + info • 'surfaceVariant' is deprecated and shouldn't be used. Use surfaceContainerHighest instead. This feature was deprecated after v3.18.0-0.1.pre • lib/screens/family/family_activity_log_screen.dart:249:44 • deprecated_member_use + info • 'surfaceVariant' is deprecated and shouldn't be used. Use surfaceContainerHighest instead. This feature was deprecated after v3.18.0-0.1.pre • lib/screens/family/family_activity_log_screen.dart:376:38 • deprecated_member_use + error • The argument type 'Map' can't be assigned to the parameter type 'String'. • lib/screens/family/family_activity_log_screen.dart:435:23 • argument_type_not_assignable + info • 'surfaceVariant' is deprecated and shouldn't be used. Use surfaceContainerHighest instead. This feature was deprecated after v3.18.0-0.1.pre • lib/screens/family/family_activity_log_screen.dart:449:50 • deprecated_member_use + error • There's no constant named 'leave' in 'AuditActionType' • lib/screens/family/family_activity_log_screen.dart:560:28 • undefined_enum_constant + error • There's no constant named 'permission_grant' in 'AuditActionType' • lib/screens/family/family_activity_log_screen.dart:562:28 • undefined_enum_constant + error • There's no constant named 'permission_revoke' in 'AuditActionType' • lib/screens/family/family_activity_log_screen.dart:564:28 • undefined_enum_constant + error • There's no constant named 'leave' in 'AuditActionType' • lib/screens/family/family_activity_log_screen.dart:584:28 • undefined_enum_constant + error • There's no constant named 'permission_grant' in 'AuditActionType' • lib/screens/family/family_activity_log_screen.dart:586:28 • undefined_enum_constant + error • There's no constant named 'permission_revoke' in 'AuditActionType' • lib/screens/family/family_activity_log_screen.dart:587:28 • undefined_enum_constant + error • There's no constant named 'leave' in 'AuditActionType' • lib/screens/family/family_activity_log_screen.dart:610:28 • undefined_enum_constant + error • There's no constant named 'permission_grant' in 'AuditActionType' • lib/screens/family/family_activity_log_screen.dart:612:28 • undefined_enum_constant + error • There's no constant named 'permission_revoke' in 'AuditActionType' • lib/screens/family/family_activity_log_screen.dart:614:28 • undefined_enum_constant + info • 'surfaceVariant' is deprecated and shouldn't be used. Use surfaceContainerHighest instead. This feature was deprecated after v3.18.0-0.1.pre • lib/screens/family/family_activity_log_screen.dart:682:52 • deprecated_member_use + error • The argument type 'Map' can't be assigned to the parameter type 'String'. • lib/screens/family/family_activity_log_screen.dart:685:37 • argument_type_not_assignable +warning • The operand can't be 'null', so the condition is always 'true' • lib/screens/family/family_activity_log_screen.dart:688:39 • unnecessary_null_comparison +warning • The '!' will have no effect because the receiver can't be null • lib/screens/family/family_activity_log_screen.dart:692:60 • unnecessary_non_null_assertion + error • Arguments of a constant creation must be constant expressions • lib/screens/family/family_activity_log_screen.dart:715:15 • const_with_non_constant_argument + info • Unnecessary 'const' keyword • lib/screens/family/family_activity_log_screen.dart:716:22 • unnecessary_const + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_activity_log_screen.dart:767:14 • prefer_const_constructors + info • 'value' is deprecated and shouldn't be used. Use initialValue instead. This will set the initial value for the form field. This feature was deprecated after v3.33.0-1.0.pre • lib/screens/family/family_activity_log_screen.dart:776:13 • deprecated_member_use + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_activity_log_screen.dart:825:19 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_activity_log_screen.dart:841:18 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_activity_log_screen.dart:845:18 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_activity_log_screen.dart:852:18 • prefer_const_constructors +warning • The value of the local variable 'theme' isn't used • lib/screens/family/family_activity_log_screen.dart:867:11 • unused_local_variable + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_activity_log_screen.dart:870:14 • prefer_const_constructors + info • Unnecessary use of string interpolation • lib/screens/family/family_activity_log_screen.dart:882:23 • unnecessary_string_interpolations + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_activity_log_screen.dart:888:18 • prefer_const_constructors +warning • The value of the local variable 'theme' isn't used • lib/screens/family/family_dashboard_screen.dart:43:11 • unused_local_variable + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_dashboard_screen.dart:52:13 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_dashboard_screen.dart:63:19 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_dashboard_screen.dart:76:19 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_dashboard_screen.dart:170:13 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_dashboard_screen.dart:178:22 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_dashboard_screen.dart:210:17 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_dashboard_screen.dart:212:26 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_dashboard_screen.dart:318:13 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_dashboard_screen.dart:320:22 • prefer_const_constructors + error • The constructor being called isn't a const constructor • lib/screens/family/family_dashboard_screen.dart:329:17 • const_with_non_const + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_dashboard_screen.dart:418:13 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_dashboard_screen.dart:420:22 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_dashboard_screen.dart:496:17 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_dashboard_screen.dart:498:26 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_dashboard_screen.dart:507:26 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_dashboard_screen.dart:567:13 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_dashboard_screen.dart:569:22 • prefer_const_constructors + error • The constructor being called isn't a const constructor • lib/screens/family/family_dashboard_screen.dart:578:17 • const_with_non_const + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_dashboard_screen.dart:611:34 • prefer_const_constructors + error • The constructor being called isn't a const constructor • lib/screens/family/family_dashboard_screen.dart:616:31 • const_with_non_const + error • The constructor being called isn't a const constructor • lib/screens/family/family_dashboard_screen.dart:618:21 • const_with_non_const + error • The constructor being called isn't a const constructor • lib/screens/family/family_dashboard_screen.dart:630:37 • const_with_non_const + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_dashboard_screen.dart:646:12 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_dashboard_screen.dart:647:14 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_dashboard_screen.dart:649:16 • prefer_const_constructors + info • Use 'const' literals as arguments to constructors of '@immutable' classes • lib/screens/family/family_dashboard_screen.dart:651:21 • prefer_const_literals_to_create_immutables + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_dashboard_screen.dart:652:13 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_dashboard_screen.dart:654:22 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_dashboard_screen.dart:671:12 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_dashboard_screen.dart:672:14 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_dashboard_screen.dart:674:16 • prefer_const_constructors + info • Use 'const' literals as arguments to constructors of '@immutable' classes • lib/screens/family/family_dashboard_screen.dart:676:21 • prefer_const_literals_to_create_immutables + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_dashboard_screen.dart:677:13 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_dashboard_screen.dart:679:22 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_dashboard_screen.dart:706:26 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_dashboard_screen.dart:708:27 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_dashboard_screen.dart:709:27 • prefer_const_constructors +warning • Duplicate import • lib/screens/family/family_members_screen.dart:3:8 • duplicate_import +warning • The value of the field '_isLoading' isn't used • lib/screens/family/family_members_screen.dart:26:8 • unused_field + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_members_screen.dart:38:13 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_members_screen.dart:47:19 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_members_screen.dart:75:35 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_members_screen.dart:100:27 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_members_screen.dart:163:21 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_members_screen.dart:173:30 • prefer_const_constructors +warning • The value of the local variable 'theme' isn't used • lib/screens/family/family_members_screen.dart:186:11 • unused_local_variable + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_members_screen.dart:303:25 • prefer_const_constructors + info • Unnecessary 'const' keyword • lib/screens/family/family_members_screen.dart:312:29 • unnecessary_const + info • Unnecessary 'const' keyword • lib/screens/family/family_members_screen.dart:323:29 • unnecessary_const + info • Unnecessary 'const' keyword • lib/screens/family/family_members_screen.dart:334:27 • unnecessary_const + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_members_screen.dart:377:19 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_members_screen.dart:378:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_members_screen.dart:463:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_members_screen.dart:468:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_members_screen.dart:475:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_members_screen.dart:475:38 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_members_screen.dart:651:11 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_members_screen.dart:653:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_members_screen.dart:768:14 • prefer_const_constructors + info • 'groupValue' is deprecated and shouldn't be used. Use a RadioGroup ancestor to manage group value instead. This feature was deprecated after v3.32.0-0.0.pre • lib/screens/family/family_members_screen.dart:779:15 • deprecated_member_use + info • 'onChanged' is deprecated and shouldn't be used. Use RadioGroup to handle value change instead. This feature was deprecated after v3.32.0-0.0.pre • lib/screens/family/family_members_screen.dart:780:15 • deprecated_member_use + info • Unnecessary use of 'toList' in a spread • lib/screens/family/family_members_screen.dart:784:14 • unnecessary_to_list_in_spreads + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_members_screen.dart:790:18 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_members_screen.dart:794:18 • prefer_const_constructors +warning • Unused import: '../../models/family.dart' • lib/screens/family/family_permissions_audit_screen.dart:6:8 • unused_import + info • Parameter 'key' could be a super parameter • lib/screens/family/family_permissions_audit_screen.dart:15:9 • use_super_parameters + error • Too many positional arguments: 0 expected, but 1 found • lib/screens/family/family_permissions_audit_screen.dart:65:11 • extra_positional_arguments + error • The named parameter 'startDate' isn't defined • lib/screens/family/family_permissions_audit_screen.dart:66:11 • undefined_named_parameter + error • The named parameter 'endDate' isn't defined • lib/screens/family/family_permissions_audit_screen.dart:67:11 • undefined_named_parameter + error • Too many positional arguments: 0 expected, but 1 found • lib/screens/family/family_permissions_audit_screen.dart:69:48 • extra_positional_arguments_could_be_named + error • Too many positional arguments: 0 expected, but 1 found • lib/screens/family/family_permissions_audit_screen.dart:70:50 • extra_positional_arguments_could_be_named + error • Too many positional arguments: 0 expected, but 1 found • lib/screens/family/family_permissions_audit_screen.dart:71:49 • extra_positional_arguments_could_be_named + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_permissions_audit_screen.dart:98:15 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_permissions_audit_screen.dart:107:21 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_permissions_audit_screen.dart:112:21 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_permissions_audit_screen.dart:117:21 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_permissions_audit_screen.dart:276:19 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_permissions_audit_screen.dart:278:28 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_permissions_audit_screen.dart:317:19 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_permissions_audit_screen.dart:319:28 • prefer_const_constructors + error • Methods can't be invoked in constant expressions • lib/screens/family/family_permissions_audit_screen.dart:324:28 • const_eval_method_invocation + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_permissions_audit_screen.dart:339:19 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_permissions_audit_screen.dart:341:28 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_permissions_audit_screen.dart:358:19 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_permissions_audit_screen.dart:360:28 • prefer_const_constructors + error • Methods can't be invoked in constant expressions • lib/screens/family/family_permissions_audit_screen.dart:365:28 • const_eval_method_invocation + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_permissions_audit_screen.dart:389:13 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_permissions_audit_screen.dart:391:22 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_permissions_audit_screen.dart:394:13 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_permissions_audit_screen.dart:396:22 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_permissions_audit_screen.dart:455:15 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_permissions_audit_screen.dart:457:24 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_permissions_audit_screen.dart:467:17 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_permissions_audit_screen.dart:496:19 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_permissions_audit_screen.dart:498:28 • prefer_const_constructors + error • Invalid constant value • lib/screens/family/family_permissions_audit_screen.dart:508:34 • invalid_constant + info • 'surfaceVariant' is deprecated and shouldn't be used. Use surfaceContainerHighest instead. This feature was deprecated after v3.18.0-0.1.pre • lib/screens/family/family_permissions_audit_screen.dart:510:62 • deprecated_member_use + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_permissions_audit_screen.dart:555:19 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_permissions_audit_screen.dart:557:28 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_permissions_audit_screen.dart:576:21 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_permissions_audit_screen.dart:579:27 • prefer_const_constructors +warning • The value of the local variable 'date' isn't used • lib/screens/family/family_permissions_audit_screen.dart:665:13 • unused_local_variable + error • Invalid constant value • lib/screens/family/family_permissions_audit_screen.dart:819:15 • invalid_constant + info • Unnecessary 'const' keyword • lib/screens/family/family_permissions_audit_screen.dart:820:20 • unnecessary_const + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_permissions_audit_screen.dart:896:17 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_permissions_audit_screen.dart:898:26 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_permissions_audit_screen.dart:907:17 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_permissions_audit_screen.dart:909:26 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_permissions_audit_screen.dart:922:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_permissions_audit_screen.dart:929:20 • prefer_const_constructors +warning • This default clause is covered by the previous cases • lib/screens/family/family_permissions_audit_screen.dart:1007:7 • unreachable_switch_default +warning • This default clause is covered by the previous cases • lib/screens/family/family_permissions_audit_screen.dart:1023:7 • unreachable_switch_default + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_permissions_audit_screen.dart:1252:14 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_permissions_audit_screen.dart:1259:22 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_permissions_audit_screen.dart:1261:25 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_permissions_audit_screen.dart:1275:22 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_permissions_audit_screen.dart:1277:25 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_permissions_audit_screen.dart:1307:18 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_permissions_audit_screen.dart:1311:18 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_permissions_audit_screen.dart:1324:18 • prefer_const_constructors +warning • Unused import: '../../providers/auth_provider.dart' • lib/screens/family/family_permissions_editor_screen.dart:5:8 • unused_import + info • Parameter 'key' could be a super parameter • lib/screens/family/family_permissions_editor_screen.dart:13:9 • use_super_parameters + error • Too many positional arguments: 0 expected, but 1 found • lib/screens/family/family_permissions_editor_screen.dart:154:53 • extra_positional_arguments_could_be_named + error • Too many positional arguments: 0 expected, but 1 found • lib/screens/family/family_permissions_editor_screen.dart:155:63 • extra_positional_arguments_could_be_named +warning • The operand can't be 'null', so the condition is always 'true' • lib/screens/family/family_permissions_editor_screen.dart:158:25 • unnecessary_null_comparison + error • A value of type 'Map' can't be assigned to a variable of type 'List' • lib/screens/family/family_permissions_editor_screen.dart:159:30 • invalid_assignment +warning • The operand can't be 'null', so the condition is always 'true' • lib/screens/family/family_permissions_editor_screen.dart:161:25 • unnecessary_null_comparison + error • A value of type 'List' can't be assigned to a variable of type 'List' • lib/screens/family/family_permissions_editor_screen.dart:162:26 • invalid_assignment + error • The argument type 'String' can't be assigned to the parameter type 'List'. • lib/screens/family/family_permissions_editor_screen.dart:205:13 • argument_type_not_assignable + error • Too many positional arguments: 2 expected, but 3 found • lib/screens/family/family_permissions_editor_screen.dart:206:13 • extra_positional_arguments + error • This expression has a type of 'void' so its value can't be used • lib/screens/family/family_permissions_editor_screen.dart:209:15 • use_of_void_result + error • The argument type 'CustomRole' can't be assigned to the parameter type 'List'. • lib/screens/family/family_permissions_editor_screen.dart:253:15 • argument_type_not_assignable + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_permissions_editor_screen.dart:284:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_permissions_editor_screen.dart:285:18 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_permissions_editor_screen.dart:289:20 • prefer_const_constructors + error • Too many positional arguments: 1 expected, but 2 found • lib/screens/family/family_permissions_editor_screen.dart:299:19 • extra_positional_arguments + error • This expression has a type of 'void' so its value can't be used • lib/screens/family/family_permissions_editor_screen.dart:302:21 • use_of_void_result + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_permissions_editor_screen.dart:321:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_permissions_editor_screen.dart:397:15 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_permissions_editor_screen.dart:410:23 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_permissions_editor_screen.dart:411:24 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_permissions_editor_screen.dart:414:21 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_permissions_editor_screen.dart:419:21 • prefer_const_constructors + info • 'surfaceVariant' is deprecated and shouldn't be used. Use surfaceContainerHighest instead. This feature was deprecated after v3.18.0-0.1.pre • lib/screens/family/family_permissions_editor_screen.dart:476:46 • deprecated_member_use + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_permissions_editor_screen.dart:477:28 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_permissions_editor_screen.dart:479:30 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_permissions_editor_screen.dart:594:15 • prefer_const_constructors +warning • The value of the local variable 'isSystemRole' isn't used • lib/screens/family/family_permissions_editor_screen.dart:611:11 • unused_local_variable + info • 'surfaceVariant' is deprecated and shouldn't be used. Use surfaceContainerHighest instead. This feature was deprecated after v3.18.0-0.1.pre • lib/screens/family/family_permissions_editor_screen.dart:623:36 • deprecated_member_use + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_permissions_editor_screen.dart:834:14 • prefer_const_constructors + info • 'value' is deprecated and shouldn't be used. Use initialValue instead. This will set the initial value for the form field. This feature was deprecated after v3.33.0-1.0.pre • lib/screens/family/family_permissions_editor_screen.dart:864:15 • deprecated_member_use + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_permissions_editor_screen.dart:872:26 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_permissions_editor_screen.dart:876:26 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_permissions_editor_screen.dart:880:26 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_permissions_editor_screen.dart:893:18 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_permissions_editor_screen.dart:908:18 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_permissions_editor_screen.dart:931:14 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_permissions_editor_screen.dart:936:22 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_permissions_editor_screen.dart:937:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_permissions_editor_screen.dart:938:23 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_permissions_editor_screen.dart:945:22 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_permissions_editor_screen.dart:946:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_permissions_editor_screen.dart:947:23 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_permissions_editor_screen.dart:954:22 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_permissions_editor_screen.dart:955:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_permissions_editor_screen.dart:956:23 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_permissions_editor_screen.dart:963:22 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_permissions_editor_screen.dart:964:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_permissions_editor_screen.dart:965:23 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_permissions_editor_screen.dart:972:22 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_permissions_editor_screen.dart:973:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_permissions_editor_screen.dart:974:23 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_permissions_editor_screen.dart:981:22 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_permissions_editor_screen.dart:982:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_permissions_editor_screen.dart:983:23 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_permissions_editor_screen.dart:994:18 • prefer_const_constructors +warning • Unused import: '../../providers/family_provider.dart' • lib/screens/family/family_settings_screen.dart:8:8 • unused_import +warning • Unused import: '../../services/api/ledger_service.dart' • lib/screens/family/family_settings_screen.dart:9:8 • unused_import + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_settings_screen.dart:86:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_settings_screen.dart:91:22 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_settings_screen.dart:93:24 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_settings_screen.dart:142:35 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_settings_screen.dart:189:35 • prefer_const_constructors + info • 'value' is deprecated and shouldn't be used. Use initialValue instead. This will set the initial value for the form field. This feature was deprecated after v3.33.0-1.0.pre • lib/screens/family/family_settings_screen.dart:202:21 • deprecated_member_use + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_settings_screen.dart:243:35 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_settings_screen.dart:257:28 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_settings_screen.dart:258:31 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_settings_screen.dart:276:28 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_settings_screen.dart:277:26 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_settings_screen.dart:280:36 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_settings_screen.dart:281:39 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_settings_screen.dart:283:29 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_settings_screen.dart:295:28 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_settings_screen.dart:296:26 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_settings_screen.dart:297:29 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_settings_screen.dart:301:28 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_settings_screen.dart:302:26 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_settings_screen.dart:303:29 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_settings_screen.dart:304:29 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_settings_screen.dart:308:28 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_settings_screen.dart:309:26 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_settings_screen.dart:310:29 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_settings_screen.dart:311:29 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_settings_screen.dart:322:28 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_settings_screen.dart:323:26 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_settings_screen.dart:324:29 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_settings_screen.dart:325:29 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_settings_screen.dart:329:28 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_settings_screen.dart:330:26 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_settings_screen.dart:331:29 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_settings_screen.dart:332:29 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_settings_screen.dart:336:28 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_settings_screen.dart:337:26 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_settings_screen.dart:338:29 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_settings_screen.dart:339:29 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_settings_screen.dart:351:28 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_settings_screen.dart:352:26 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_settings_screen.dart:353:30 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_settings_screen.dart:354:29 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_settings_screen.dart:360:25 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_settings_screen.dart:362:25 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_settings_screen.dart:362:45 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_settings_screen.dart:363:31 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_settings_screen.dart:533:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_settings_screen.dart:534:18 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_settings_screen.dart:538:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_settings_screen.dart:548:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_settings_screen.dart:559:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_settings_screen.dart:564:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_settings_screen.dart:575:20 • prefer_const_constructors +warning • The left operand can't be null, so the right operand is never executed • lib/screens/family/family_settings_screen.dart:611:47 • dead_null_aware_expression + info • Don't use 'BuildContext's across async gaps • lib/screens/family/family_settings_screen.dart:630:7 • use_build_context_synchronously + info • Parameter 'key' could be a super parameter • lib/screens/family/family_statistics_screen.dart:12:9 • use_super_parameters + info • The private field _selectedDate could be 'final' • lib/screens/family/family_statistics_screen.dart:27:12 • prefer_final_fields + error • The named parameter 'period' isn't defined • lib/screens/family/family_statistics_screen.dart:59:9 • undefined_named_parameter + error • The named parameter 'date' isn't defined • lib/screens/family/family_statistics_screen.dart:60:9 • undefined_named_parameter + error • A value of type 'FamilyStatistics' can't be assigned to a variable of type 'FamilyStatistics?' • lib/screens/family/family_statistics_screen.dart:64:23 • invalid_assignment + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_statistics_screen.dart:86:13 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_statistics_screen.dart:111:19 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_statistics_screen.dart:114:19 • prefer_const_constructors + info • 'surfaceVariant' is deprecated and shouldn't be used. Use surfaceContainerHighest instead. This feature was deprecated after v3.18.0-0.1.pre • lib/screens/family/family_statistics_screen.dart:239:56 • deprecated_member_use + error • The constructor being called isn't a const constructor • lib/screens/family/family_statistics_screen.dart:280:23 • const_with_non_const + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_statistics_screen.dart:312:40 • prefer_const_constructors + error • The constructor being called isn't a const constructor • lib/screens/family/family_statistics_screen.dart:323:37 • const_with_non_const + error • The constructor being called isn't a const constructor • lib/screens/family/family_statistics_screen.dart:326:27 • const_with_non_const + error • The constructor being called isn't a const constructor • lib/screens/family/family_statistics_screen.dart:341:27 • const_with_non_const + error • The constructor being called isn't a const constructor • lib/screens/family/family_statistics_screen.dart:388:23 • const_with_non_const + info • Use 'const' with the constructor to improve performance • lib/screens/family/family_statistics_screen.dart:426:40 • prefer_const_constructors + error • The constructor being called isn't a const constructor • lib/screens/family/family_statistics_screen.dart:441:37 • const_with_non_const + error • The constructor being called isn't a const constructor • lib/screens/family/family_statistics_screen.dart:477:23 • const_with_non_const + info • 'surfaceVariant' is deprecated and shouldn't be used. Use surfaceContainerHighest instead. This feature was deprecated after v3.18.0-0.1.pre • lib/screens/family/family_statistics_screen.dart:613:66 • deprecated_member_use + error • The element type 'MemberStatData' can't be assigned to the list type 'Widget' • lib/screens/family/family_statistics_screen.dart:635:22 • list_element_type_not_assignable + error • This expression has a type of 'void' so its value can't be used • lib/screens/family/family_statistics_screen.dart:636:21 • use_of_void_result + info • 'surfaceVariant' is deprecated and shouldn't be used. Use surfaceContainerHighest instead. This feature was deprecated after v3.18.0-0.1.pre • lib/screens/family/family_statistics_screen.dart:860:50 • deprecated_member_use + info • The 'child' argument should be last in widget constructor invocations • lib/screens/home/home_screen.dart:88:9 • sort_child_properties_last + info • Use 'const' with the constructor to improve performance • lib/screens/home/home_screen.dart:88:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/home/home_screen.dart:107:13 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/home/home_screen.dart:109:22 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/invitations/invitation_management_screen.dart:87:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/invitations/invitation_management_screen.dart:92:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/invitations/invitation_management_screen.dart:96:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/invitations/invitation_management_screen.dart:136:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/invitations/invitation_management_screen.dart:144:24 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/invitations/invitation_management_screen.dart:151:24 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/invitations/invitation_management_screen.dart:158:24 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/invitations/invitation_management_screen.dart:165:19 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/invitations/invitation_management_screen.dart:249:15 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/invitations/invitation_management_screen.dart:250:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/invitations/invitation_management_screen.dart:362:27 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/invitations/invitation_management_screen.dart:367:27 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/invitations/invitation_management_screen.dart:374:28 • prefer_const_constructors + info • Parameter 'key' could be a super parameter • lib/screens/invitations/pending_invitations_screen.dart:11:9 • use_super_parameters +warning • The value of the field '_familyService' isn't used • lib/screens/invitations/pending_invitations_screen.dart:20:9 • unused_field + info • Use 'const' with the constructor to improve performance • lib/screens/invitations/pending_invitations_screen.dart:63:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/invitations/pending_invitations_screen.dart:79:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/invitations/pending_invitations_screen.dart:83:20 • prefer_const_constructors + info • Uses 'await' on an instance of 'List', which is not a subtype of 'Future' • lib/screens/invitations/pending_invitations_screen.dart:96:7 • await_only_futures +warning • The value of 'refresh' should be used • lib/screens/invitations/pending_invitations_screen.dart:96:17 • unused_result + info • Use 'const' with the constructor to improve performance • lib/screens/invitations/pending_invitations_screen.dart:129:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/invitations/pending_invitations_screen.dart:134:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/invitations/pending_invitations_screen.dart:141:20 • prefer_const_constructors +warning • The value of the local variable 'theme' isn't used • lib/screens/invitations/pending_invitations_screen.dart:202:11 • unused_local_variable + info • Use 'const' with the constructor to improve performance • lib/screens/invitations/pending_invitations_screen.dart:206:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/invitations/pending_invitations_screen.dart:210:19 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/invitations/pending_invitations_screen.dart:231:19 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/invitations/pending_invitations_screen.dart:271:13 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/invitations/pending_invitations_screen.dart:277:22 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/invitations/pending_invitations_screen.dart:311:24 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/invitations/pending_invitations_screen.dart:433:30 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/invitations/pending_invitations_screen.dart:438:30 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/invitations/pending_invitations_screen.dart:578:32 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/invitations/pending_invitations_screen.dart:588:32 • prefer_const_constructors + error • Arguments of a constant creation must be constant expressions • lib/screens/invitations/pending_invitations_screen.dart:626:15 • const_with_non_constant_argument + info • Use 'const' with the constructor to improve performance • lib/screens/management/category_list_page.dart:17:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/category_list_page.dart:34:25 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/category_list_page.dart:34:44 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/category_list_page.dart:48:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/category_list_page.dart:80:13 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/category_list_page.dart:82:22 • prefer_const_constructors + info • Use interpolation to compose strings and values • lib/screens/management/category_management_enhanced.dart:22:16 • prefer_interpolation_to_compose_strings + info • Use interpolation to compose strings and values • lib/screens/management/category_management_enhanced.dart:26:16 • prefer_interpolation_to_compose_strings + info • Use interpolation to compose strings and values • lib/screens/management/category_management_enhanced.dart:28:16 • prefer_interpolation_to_compose_strings + info • Use 'const' with the constructor to improve performance • lib/screens/management/category_management_enhanced.dart:39:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/category_management_enhanced.dart:43:19 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/category_management_enhanced.dart:51:15 • prefer_const_constructors + info • Statements in an if should be enclosed in a block • lib/screens/management/category_management_enhanced.dart:94:28 • curly_braces_in_flow_control_structures + info • Statements in an if should be enclosed in a block • lib/screens/management/category_management_enhanced.dart:94:53 • curly_braces_in_flow_control_structures + info • Use 'const' with the constructor to improve performance • lib/screens/management/category_management_enhanced.dart:124:22 • prefer_const_constructors + info • Unnecessary 'const' keyword • lib/screens/management/category_management_enhanced.dart:133:23 • unnecessary_const + error • The constructor being called isn't a const constructor • lib/screens/management/category_management_enhanced.dart:134:23 • const_with_non_const + info • Unnecessary 'const' keyword • lib/screens/management/category_management_enhanced.dart:136:32 • unnecessary_const + info • Unnecessary 'const' keyword • lib/screens/management/category_management_enhanced.dart:145:19 • unnecessary_const + info • Unnecessary 'const' keyword • lib/screens/management/category_management_enhanced.dart:146:19 • unnecessary_const + info • Unnecessary 'const' keyword • lib/screens/management/category_management_enhanced.dart:150:39 • unnecessary_const + error • The constructor being called isn't a const constructor • lib/screens/management/category_management_enhanced.dart:152:34 • const_with_non_const + error • The constructor being called isn't a const constructor • lib/screens/management/category_management_enhanced.dart:175:31 • const_with_non_const + info • Unnecessary 'const' keyword • lib/screens/management/category_management_enhanced.dart:187:21 • unnecessary_const + info • Unnecessary 'const' keyword • lib/screens/management/category_management_enhanced.dart:192:21 • unnecessary_const + error • The constructor being called isn't a const constructor • lib/screens/management/category_management_enhanced.dart:194:30 • const_with_non_const + info • Use 'const' with the constructor to improve performance • lib/screens/management/category_management_enhanced.dart:216:70 • prefer_const_constructors + info • Don't use 'BuildContext's across async gaps, guarded by an unrelated 'mounted' check • lib/screens/management/category_management_enhanced.dart:230:44 • use_build_context_synchronously + info • Use 'const' with the constructor to improve performance • lib/screens/management/category_management_enhanced.dart:234:24 • prefer_const_constructors + info • Don't use 'BuildContext's across async gaps • lib/screens/management/category_management_enhanced.dart:248:51 • use_build_context_synchronously + info • Use 'const' with the constructor to improve performance • lib/screens/management/category_management_enhanced.dart:254:24 • prefer_const_constructors +warning • Unused import: '../../models/category.dart' • lib/screens/management/category_template_library.dart:4:8 • unused_import +warning • Unused import: '../../utils/constants.dart' • lib/screens/management/category_template_library.dart:6:8 • unused_import + error • Target of URI doesn't exist: '../../widgets/common/custom_card.dart' • lib/screens/management/category_template_library.dart:7:8 • uri_does_not_exist +warning • Unused import: '../../widgets/common/error_widget.dart' • lib/screens/management/category_template_library.dart:9:8 • unused_import + info • Parameter 'key' could be a super parameter • lib/screens/management/category_template_library.dart:14:9 • use_super_parameters + info • The private field _templatesByGroup could be 'final' • lib/screens/management/category_template_library.dart:30:45 • prefer_final_fields + error • There's no constant named 'healthEducation' in 'CategoryGroup' • lib/screens/management/category_template_library.dart:47:19 • undefined_enum_constant + error • There's no constant named 'financial' in 'CategoryGroup' • lib/screens/management/category_template_library.dart:49:19 • undefined_enum_constant + error • There's no constant named 'business' in 'CategoryGroup' • lib/screens/management/category_template_library.dart:50:19 • undefined_enum_constant + error • The argument type 'CategoryGroup' can't be assigned to the parameter type 'String'. • lib/screens/management/category_template_library.dart:82:39 • argument_type_not_assignable + info • The type of the right operand ('AccountClassification') isn't a subtype or a supertype of the left operand ('CategoryClassification') • lib/screens/management/category_template_library.dart:123:37 • unrelated_type_equality_checks + info • Use 'const' with the constructor to improve performance • lib/screens/management/category_template_library.dart:182:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/category_template_library.dart:187:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/category_template_library.dart:191:20 • prefer_const_constructors + error • The argument type 'SystemCategoryTemplate' can't be assigned to the parameter type 'String'. • lib/screens/management/category_template_library.dart:202:59 • argument_type_not_assignable + info • Don't use 'BuildContext's across async gaps • lib/screens/management/category_template_library.dart:205:30 • use_build_context_synchronously + info • Don't use 'BuildContext's across async gaps • lib/screens/management/category_template_library.dart:216:30 • use_build_context_synchronously + info • Use 'const' with the constructor to improve performance • lib/screens/management/category_template_library.dart:230:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/category_template_library.dart:264:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/category_template_library.dart:268:20 • prefer_const_constructors + error • The argument type 'SystemCategoryTemplate' can't be assigned to the parameter type 'String'. • lib/screens/management/category_template_library.dart:276:57 • argument_type_not_assignable + info • Don't use 'BuildContext's across async gaps • lib/screens/management/category_template_library.dart:278:30 • use_build_context_synchronously + info • Don't use 'BuildContext's across async gaps • lib/screens/management/category_template_library.dart:285:30 • use_build_context_synchronously + info • Use 'const' with the constructor to improve performance • lib/screens/management/category_template_library.dart:299:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/category_template_library.dart:312:21 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/category_template_library.dart:317:21 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/category_template_library.dart:322:21 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/category_template_library.dart:335:19 • prefer_const_constructors + error • 1 positional argument expected by 'ErrorWidget.new', but 0 found • lib/screens/management/category_template_library.dart:345:19 • not_enough_positional_arguments + error • The named parameter 'message' isn't defined • lib/screens/management/category_template_library.dart:345:19 • undefined_named_parameter + error • The named parameter 'onRetry' isn't defined • lib/screens/management/category_template_library.dart:346:19 • undefined_named_parameter + info • Use 'const' with the constructor to improve performance • lib/screens/management/category_template_library.dart:388:27 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/category_template_library.dart:391:29 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/category_template_library.dart:458:19 • prefer_const_constructors + info • The type of the right operand ('AccountClassification') isn't a subtype or a supertype of the left operand ('CategoryClassification') • lib/screens/management/category_template_library.dart:498:40 • unrelated_type_equality_checks + info • Use 'const' with the constructor to improve performance • lib/screens/management/category_template_library.dart:684:36 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/category_template_library.dart:686:38 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/category_template_library.dart:727:25 • prefer_const_constructors + error • The argument type 'CategoryClassification' can't be assigned to the parameter type 'AccountClassification'. • lib/screens/management/category_template_library.dart:805:48 • argument_type_not_assignable + info • Use 'const' with the constructor to improve performance • lib/screens/management/category_template_library.dart:870:30 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/category_template_library.dart:880:29 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/category_template_library.dart:881:30 • prefer_const_constructors + error • Arguments of a constant creation must be constant expressions • lib/screens/management/category_template_library.dart:902:15 • const_with_non_constant_argument +warning • This default clause is covered by the previous cases • lib/screens/management/category_template_library.dart:940:7 • unreachable_switch_default + info • Use of 'return' in a 'finally' clause • lib/screens/management/crypto_selection_page.dart:69:21 • control_flow_in_finally +warning • The declaration '_getCryptoIcon' isn't referenced • lib/screens/management/crypto_selection_page.dart:88:10 • unused_element + info • Use 'const' with the constructor to improve performance • lib/screens/management/crypto_selection_page.dart:313:27 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/crypto_selection_page.dart:315:36 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/crypto_selection_page.dart:385:39 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/crypto_selection_page.dart:386:40 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/crypto_selection_page.dart:441:39 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/crypto_selection_page.dart:442:40 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/crypto_selection_page.dart:522:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/crypto_selection_page.dart:535:19 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/crypto_selection_page.dart:555:29 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/crypto_selection_page.dart:558:31 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/crypto_selection_page.dart:639:25 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/crypto_selection_page.dart:640:26 • prefer_const_constructors + info • The import of 'package:flutter/foundation.dart' is unnecessary because all of the used elements are also provided by the import of 'package:flutter/material.dart' • lib/screens/management/currency_management_page_v2.dart:2:8 • unnecessary_import +warning • The declaration '_buildManualRatesBanner' isn't referenced • lib/screens/management/currency_management_page_v2.dart:40:10 • unused_element + info • Use 'const' with the constructor to improve performance • lib/screens/management/currency_management_page_v2.dart:73:21 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/currency_management_page_v2.dart:74:22 • prefer_const_constructors +warning • The declaration '_promptManualRate' isn't referenced • lib/screens/management/currency_management_page_v2.dart:147:19 • unused_element + info • Use 'const' with the constructor to improve performance • lib/screens/management/currency_management_page_v2.dart:161:63 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/currency_management_page_v2.dart:167:20 • prefer_const_constructors + info • Unnecessary 'const' keyword • lib/screens/management/currency_management_page_v2.dart:181:13 • unnecessary_const + info • Use 'const' with the constructor to improve performance • lib/screens/management/currency_management_page_v2.dart:204:19 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/currency_management_page_v2.dart:206:28 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/currency_management_page_v2.dart:220:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/currency_management_page_v2.dart:227:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/currency_management_page_v2.dart:247:11 • prefer_const_constructors + info • The variable name '_DeprecatedCurrencyNotice' isn't a lowerCamelCase identifier • lib/screens/management/currency_management_page_v2.dart:293:10 • non_constant_identifier_names + info • Use 'const' with the constructor to improve performance • lib/screens/management/currency_management_page_v2.dart:317:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/currency_management_page_v2.dart:334:18 • prefer_const_constructors + error • Methods can't be invoked in constant expressions • lib/screens/management/currency_management_page_v2.dart:339:25 • const_eval_method_invocation + info • 'value' is deprecated and shouldn't be used. Use initialValue instead. This will set the initial value for the form field. This feature was deprecated after v3.33.0-1.0.pre • lib/screens/management/currency_management_page_v2.dart:348:27 • deprecated_member_use + info • Use 'const' with the constructor to improve performance • lib/screens/management/currency_management_page_v2.dart:368:24 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/currency_management_page_v2.dart:382:22 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/currency_management_page_v2.dart:403:16 • prefer_const_constructors + info • Unnecessary 'const' keyword • lib/screens/management/currency_management_page_v2.dart:412:24 • unnecessary_const + info • Use 'const' with the constructor to improve performance • lib/screens/management/currency_management_page_v2.dart:437:25 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/currency_management_page_v2.dart:439:34 • prefer_const_constructors + info • 'surfaceVariant' is deprecated and shouldn't be used. Use surfaceContainerHighest instead. This feature was deprecated after v3.18.0-0.1.pre • lib/screens/management/currency_management_page_v2.dart:528:53 • deprecated_member_use + info • Use 'const' with the constructor to improve performance • lib/screens/management/currency_management_page_v2.dart:572:25 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/currency_management_page_v2.dart:574:34 • prefer_const_constructors + info • 'activeColor' is deprecated and shouldn't be used. Use activeThumbColor instead. This feature was deprecated after v3.31.0-2.0.pre • lib/screens/management/currency_management_page_v2.dart:585:27 • deprecated_member_use + info • Use 'const' with the constructor to improve performance • lib/screens/management/currency_management_page_v2.dart:608:31 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/currency_management_page_v2.dart:610:40 • prefer_const_constructors + info • 'activeColor' is deprecated and shouldn't be used. Use activeThumbColor instead. This feature was deprecated after v3.31.0-2.0.pre • lib/screens/management/currency_management_page_v2.dart:621:33 • deprecated_member_use + info • Use 'const' with the constructor to improve performance • lib/screens/management/currency_management_page_v2.dart:669:29 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/currency_management_page_v2.dart:671:38 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/currency_management_page_v2.dart:701:32 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/currency_management_page_v2.dart:707:35 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/currency_management_page_v2.dart:725:34 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/currency_management_page_v2.dart:731:37 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/currency_management_page_v2.dart:758:25 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/currency_management_page_v2.dart:760:34 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/currency_management_page_v2.dart:769:30 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/currency_management_page_v2.dart:789:30 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/currency_management_page_v2.dart:862:33 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/currency_management_page_v2.dart:863:34 • prefer_const_constructors +warning • Dead code • lib/screens/management/currency_management_page_v2.dart:873:17 • dead_code + info • Use 'const' with the constructor to improve performance • lib/screens/management/currency_management_page_v2.dart:884:27 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/currency_management_page_v2.dart:886:36 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/currency_management_page_v2.dart:902:37 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/currency_management_page_v2.dart:903:38 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/currency_management_page_v2.dart:913:37 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/currency_management_page_v2.dart:914:38 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/currency_management_page_v2.dart:941:31 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/currency_management_page_v2.dart:996:19 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/currency_management_page_v2.dart:1019:27 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/currency_management_page_v2.dart:1025:15 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/currency_management_page_v2.dart:1026:26 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/currency_management_page_v2.dart:1032:24 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/currency_management_page_v2.dart:1042:22 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/currency_management_page_v2.dart:1088:14 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/currency_management_page_v2.dart:1093:11 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/currency_management_page_v2.dart:1095:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/currency_management_page_v2.dart:1106:17 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/currency_management_page_v2.dart:1112:23 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/currency_management_page_v2.dart:1114:32 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/currency_management_page_v2.dart:1148:25 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/currency_management_page_v2.dart:1163:18 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/currency_management_page_v2.dart:1171:18 • prefer_const_constructors + info • Use of 'return' in a 'finally' clause • lib/screens/management/currency_selection_page.dart:70:21 • control_flow_in_finally + info • 'surfaceVariant' is deprecated and shouldn't be used. Use surfaceContainerHighest instead. This feature was deprecated after v3.18.0-0.1.pre • lib/screens/management/currency_selection_page.dart:190:31 • deprecated_member_use + info • Use 'const' with the constructor to improve performance • lib/screens/management/currency_selection_page.dart:201:17 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/currency_selection_page.dart:202:17 • prefer_const_constructors + info • 'surfaceVariant' is deprecated and shouldn't be used. Use surfaceContainerHighest instead. This feature was deprecated after v3.18.0-0.1.pre • lib/screens/management/currency_selection_page.dart:275:37 • deprecated_member_use + info • Use 'const' with the constructor to improve performance • lib/screens/management/currency_selection_page.dart:345:27 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/currency_selection_page.dart:347:36 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/currency_selection_page.dart:416:39 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/currency_selection_page.dart:417:40 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/currency_selection_page.dart:482:39 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/currency_selection_page.dart:483:40 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/currency_selection_page.dart:564:21 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/currency_selection_page.dart:584:29 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/currency_selection_page.dart:587:31 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/currency_selection_page.dart:674:29 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/currency_selection_page.dart:675:30 • prefer_const_constructors +warning • The value of the field '_isCalculating' isn't used • lib/screens/management/exchange_rate_converter_page.dart:21:8 • unused_field + info • Use 'const' with the constructor to improve performance • lib/screens/management/exchange_rate_converter_page.dart:236:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/exchange_rate_converter_page.dart:302:29 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/exchange_rate_converter_page.dart:414:21 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/exchange_rate_converter_page.dart:555:17 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/exchange_rate_converter_page.dart:557:26 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/exchange_rate_converter_page.dart:565:25 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/exchange_rate_converter_page.dart:583:29 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/exchange_rate_converter_page.dart:630:25 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/payee_management_page.dart:97:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/payee_management_page.dart:103:19 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/payee_management_page.dart:143:29 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/payee_management_page.dart:182:15 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/payee_management_page.dart:183:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/payee_management_page.dart:259:23 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/payee_management_page.dart:343:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/payee_management_page.dart:344:18 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/payee_management_page.dart:348:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/payee_management_page.dart:357:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/payee_management_page.dart:369:18 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/payee_management_page.dart:373:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/payee_management_page.dart:382:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/payee_management_page.dart:393:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/payee_management_page.dart:400:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/payee_management_page.dart:410:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/payee_management_page.dart:467:13 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/payee_management_page.dart:469:22 • prefer_const_constructors +warning • Unused import: 'package:flutter_riverpod/flutter_riverpod.dart' • lib/screens/management/payee_management_page_v2.dart:2:8 • unused_import +warning • Unused import: '../../providers/currency_provider.dart' • lib/screens/management/payee_management_page_v2.dart:6:8 • unused_import + info • Don't use 'BuildContext's across async gaps • lib/screens/management/payee_management_page_v2.dart:85:28 • use_build_context_synchronously + info • Don't use 'BuildContext's across async gaps • lib/screens/management/payee_management_page_v2.dart:90:28 • use_build_context_synchronously + info • Use 'const' with the constructor to improve performance • lib/screens/management/payee_management_page_v2.dart:104:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/payee_management_page_v2.dart:127:24 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/payee_management_page_v2.dart:137:20 • prefer_const_constructors + info • Don't use 'BuildContext's across async gaps • lib/screens/management/payee_management_page_v2.dart:153:33 • use_build_context_synchronously + info • Don't use 'BuildContext's across async gaps • lib/screens/management/payee_management_page_v2.dart:154:40 • use_build_context_synchronously + info • Don't use 'BuildContext's across async gaps • lib/screens/management/payee_management_page_v2.dart:159:40 • use_build_context_synchronously + info • Use 'const' with the constructor to improve performance • lib/screens/management/payee_management_page_v2.dart:165:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/payee_management_page_v2.dart:180:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/payee_management_page_v2.dart:186:19 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/payee_management_page_v2.dart:190:19 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/payee_management_page_v2.dart:215:29 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/payee_management_page_v2.dart:235:29 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/payee_management_page_v2.dart:242:38 • prefer_const_constructors + error • The argument type 'String?' can't be assigned to the parameter type 'String'. • lib/screens/management/payee_management_page_v2.dart:311:32 • argument_type_not_assignable + info • Use 'const' with the constructor to improve performance • lib/screens/management/rules_management_page.dart:76:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/rules_management_page.dart:82:19 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/rules_management_page.dart:123:29 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/rules_management_page.dart:142:15 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/rules_management_page.dart:143:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/rules_management_page.dart:204:23 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/rules_management_page.dart:205:24 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/rules_management_page.dart:312:17 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/rules_management_page.dart:314:26 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/rules_management_page.dart:335:17 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/rules_management_page.dart:337:26 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/rules_management_page.dart:369:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/rules_management_page.dart:370:18 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/rules_management_page.dart:374:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/rules_management_page.dart:383:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/rules_management_page.dart:395:18 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/rules_management_page.dart:399:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/rules_management_page.dart:408:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/rules_management_page.dart:430:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/rules_management_page.dart:437:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/rules_management_page.dart:447:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/tag_management_page.dart:77:19 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/tag_management_page.dart:79:28 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/tag_management_page.dart:118:35 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/tag_management_page.dart:121:37 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/tag_management_page.dart:147:32 • prefer_const_constructors + info • Unnecessary use of 'toList' in a spread • lib/screens/management/tag_management_page.dart:237:20 • unnecessary_to_list_in_spreads +warning • The declaration '_buildNewGroupCard' isn't referenced • lib/screens/management/tag_management_page.dart:290:10 • unused_element + info • Use 'const' with the constructor to improve performance • lib/screens/management/tag_management_page.dart:312:16 • prefer_const_constructors + info • Use 'const' literals as arguments to constructors of '@immutable' classes • lib/screens/management/tag_management_page.dart:314:21 • prefer_const_literals_to_create_immutables + info • Use 'const' with the constructor to improve performance • lib/screens/management/tag_management_page.dart:315:13 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/tag_management_page.dart:321:13 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/tag_management_page.dart:323:22 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/tag_management_page.dart:424:25 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/tag_management_page.dart:537:31 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/tag_management_page.dart:543:31 • prefer_const_constructors +warning • The declaration '_showTagMenu' isn't referenced • lib/screens/management/tag_management_page.dart:696:8 • unused_element + info • Use 'const' with the constructor to improve performance • lib/screens/management/tag_management_page.dart:762:24 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/tag_management_page.dart:763:22 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/tag_management_page.dart:780:24 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/tag_management_page.dart:781:22 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/tag_management_page.dart:781:42 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/tag_management_page.dart:894:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/tag_management_page.dart:899:20 • prefer_const_constructors + info • Don't use 'BuildContext's across async gaps • lib/screens/management/tag_management_page.dart:905:29 • use_build_context_synchronously + info • Don't use 'BuildContext's across async gaps • lib/screens/management/tag_management_page.dart:907:36 • use_build_context_synchronously + info • Use 'const' with the constructor to improve performance • lib/screens/management/tag_management_page.dart:918:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/travel_event_management_page.dart:99:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/travel_event_management_page.dart:105:19 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/travel_event_management_page.dart:141:29 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/travel_event_management_page.dart:182:15 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/travel_event_management_page.dart:183:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/travel_event_management_page.dart:262:23 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/travel_event_management_page.dart:263:24 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/travel_event_management_page.dart:297:18 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/travel_event_management_page.dart:389:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/travel_event_management_page.dart:390:18 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/travel_event_management_page.dart:394:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/travel_event_management_page.dart:403:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/travel_event_management_page.dart:415:18 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/travel_event_management_page.dart:419:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/travel_event_management_page.dart:428:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/travel_event_management_page.dart:439:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/travel_event_management_page.dart:446:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/travel_event_management_page.dart:456:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/travel_event_management_page.dart:484:26 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/travel_event_management_page.dart:514:13 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/travel_event_management_page.dart:516:22 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/user_currency_browser.dart:47:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/user_currency_browser.dart:76:26 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/user_currency_browser.dart:82:26 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/management/user_currency_browser.dart:88:26 • prefer_const_constructors + info • Statements in an if should be enclosed in a block • lib/screens/management/user_currency_browser.dart:111:7 • curly_braces_in_flow_control_structures + info • Statements in an if should be enclosed in a block • lib/screens/management/user_currency_browser.dart:113:7 • curly_braces_in_flow_control_structures + info • 'surfaceVariant' is deprecated and shouldn't be used. Use surfaceContainerHighest instead. This feature was deprecated after v3.18.0-0.1.pre • lib/screens/management/user_currency_browser.dart:121:23 • deprecated_member_use + info • 'surfaceVariant' is deprecated and shouldn't be used. Use surfaceContainerHighest instead. This feature was deprecated after v3.18.0-0.1.pre • lib/screens/management/user_currency_browser.dart:150:29 • deprecated_member_use + info • Use 'const' with the constructor to improve performance • lib/screens/management/user_currency_browser.dart:218:20 • prefer_const_constructors + info • The import of 'package:flutter/foundation.dart' is unnecessary because all of the used elements are also provided by the import of 'package:flutter/material.dart' • lib/screens/settings/profile_settings_screen.dart:3:8 • unnecessary_import + info • Use 'const' with the constructor to improve performance • lib/screens/settings/profile_settings_screen.dart:304:21 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/profile_settings_screen.dart:306:30 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/profile_settings_screen.dart:313:29 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/profile_settings_screen.dart:408:39 • prefer_const_constructors + info • 'value' is deprecated and shouldn't be used. Use component accessors like .r or .g, or toARGB32 for an explicit conversion • lib/screens/settings/profile_settings_screen.dart:459:62 • deprecated_member_use + info • 'value' is deprecated and shouldn't be used. Use component accessors like .r or .g, or toARGB32 for an explicit conversion • lib/screens/settings/profile_settings_screen.dart:461:67 • deprecated_member_use + info • Use 'const' with the constructor to improve performance • lib/screens/settings/profile_settings_screen.dart:519:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/profile_settings_screen.dart:520:18 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/profile_settings_screen.dart:528:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/profile_settings_screen.dart:532:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/profile_settings_screen.dart:534:22 • prefer_const_constructors + info • Don't use 'BuildContext's across async gaps • lib/screens/settings/profile_settings_screen.dart:545:7 • use_build_context_synchronously + info • Unnecessary 'const' keyword • lib/screens/settings/profile_settings_screen.dart:550:13 • unnecessary_const + info • Use 'const' with the constructor to improve performance • lib/screens/settings/profile_settings_screen.dart:554:18 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/profile_settings_screen.dart:562:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/profile_settings_screen.dart:569:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/profile_settings_screen.dart:644:18 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/profile_settings_screen.dart:655:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/profile_settings_screen.dart:743:36 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/profile_settings_screen.dart:764:38 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/profile_settings_screen.dart:795:19 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/profile_settings_screen.dart:797:28 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/profile_settings_screen.dart:834:19 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/profile_settings_screen.dart:836:28 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/profile_settings_screen.dart:854:25 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/profile_settings_screen.dart:856:34 • prefer_const_constructors + info • 'value' is deprecated and shouldn't be used. Use initialValue instead. This will set the initial value for the form field. This feature was deprecated after v3.33.0-1.0.pre • lib/screens/settings/profile_settings_screen.dart:892:21 • deprecated_member_use + info • 'value' is deprecated and shouldn't be used. Use initialValue instead. This will set the initial value for the form field. This feature was deprecated after v3.33.0-1.0.pre • lib/screens/settings/profile_settings_screen.dart:910:21 • deprecated_member_use + info • 'value' is deprecated and shouldn't be used. Use initialValue instead. This will set the initial value for the form field. This feature was deprecated after v3.33.0-1.0.pre • lib/screens/settings/profile_settings_screen.dart:927:21 • deprecated_member_use + info • 'value' is deprecated and shouldn't be used. Use initialValue instead. This will set the initial value for the form field. This feature was deprecated after v3.33.0-1.0.pre • lib/screens/settings/profile_settings_screen.dart:944:21 • deprecated_member_use + info • Use 'const' with the constructor to improve performance • lib/screens/settings/profile_settings_screen.dart:969:19 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/profile_settings_screen.dart:971:28 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/profile_settings_screen.dart:987:27 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/profile_settings_screen.dart:989:36 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/profile_settings_screen.dart:996:27 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/profile_settings_screen.dart:998:36 • prefer_const_constructors + error • Invalid constant value • lib/screens/settings/profile_settings_screen.dart:1004:42 • invalid_constant + info • Use 'const' with the constructor to improve performance • lib/screens/settings/profile_settings_screen.dart:1025:27 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/profile_settings_screen.dart:1027:36 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/profile_settings_screen.dart:1034:27 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/profile_settings_screen.dart:1036:36 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/profile_settings_screen.dart:1062:40 • prefer_const_constructors + error • Invalid constant value • lib/screens/settings/profile_settings_screen.dart:1070:42 • invalid_constant + info • Use 'const' with the constructor to improve performance • lib/screens/settings/profile_settings_screen.dart:1074:44 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/profile_settings_screen.dart:1075:46 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/profile_settings_screen.dart:1081:48 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/profile_settings_screen.dart:1088:48 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/profile_settings_screen.dart:1090:50 • prefer_const_constructors +warning • The declaration '_getCurrencyItems' isn't referenced • lib/screens/settings/profile_settings_screen.dart:1157:34 • unused_element +warning • Unused import: '../management/user_currency_browser.dart' • lib/screens/settings/settings_screen.dart:9:8 • unused_import +warning • Unused import: '../../widgets/dialogs/invite_member_dialog.dart' • lib/screens/settings/settings_screen.dart:11:8 • unused_import + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:23:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:35:26 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:36:24 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:37:27 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:38:27 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:42:26 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:43:24 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:46:27 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:50:26 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:51:24 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:52:27 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:53:27 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:57:26 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:58:24 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:59:27 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:60:27 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:71:26 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:72:24 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:73:27 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:74:27 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:78:26 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:79:24 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:80:27 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:81:27 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:92:26 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:93:24 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:94:27 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:95:27 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:99:26 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:100:24 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:101:27 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:102:27 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:113:26 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:114:24 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:115:27 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:116:27 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:120:28 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:121:24 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:122:27 • prefer_const_constructors +warning • The left operand can't be null, so the right operand is never executed • lib/screens/settings/settings_screen.dart:123:56 • dead_null_aware_expression + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:138:26 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:139:24 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:140:27 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:141:27 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:145:26 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:146:24 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:147:27 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:148:27 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:152:26 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:153:24 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:154:27 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:155:27 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:166:26 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:167:24 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:168:27 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:169:27 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:173:26 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:174:24 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:175:27 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:176:27 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:180:26 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:181:24 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:182:27 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:183:27 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:187:26 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:188:24 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:189:27 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:190:27 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:194:26 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:195:24 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:196:27 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:197:27 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:208:26 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:209:24 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:210:27 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:211:27 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:215:26 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:216:24 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:217:27 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:228:21 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:229:22 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:229:42 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:279:19 • prefer_const_constructors +warning • The declaration '_navigateToLedgerManagement' isn't referenced • lib/screens/settings/settings_screen.dart:309:8 • unused_element +warning • The declaration '_navigateToLedgerSharing' isn't referenced • lib/screens/settings/settings_screen.dart:326:8 • unused_element +warning • The declaration '_showCurrencySelector' isn't referenced • lib/screens/settings/settings_screen.dart:347:8 • unused_element +warning • The declaration '_navigateToExchangeRates' isn't referenced • lib/screens/settings/settings_screen.dart:354:8 • unused_element +warning • The declaration '_showBaseCurrencyPicker' isn't referenced • lib/screens/settings/settings_screen.dart:359:8 • unused_element + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:393:27 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:403:34 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:404:36 • prefer_const_constructors + info • Unnecessary 'const' keyword • lib/screens/settings/settings_screen.dart:409:31 • unnecessary_const + info • Unnecessary 'const' keyword • lib/screens/settings/settings_screen.dart:411:31 • unnecessary_const + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:418:38 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:424:38 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:493:24 • prefer_const_constructors + info • Unnecessary 'const' keyword • lib/screens/settings/settings_screen.dart:496:9 • unnecessary_const + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:506:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:507:18 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:511:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:519:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:519:38 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:537:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:540:19 • prefer_const_constructors +warning • The declaration '_createLedger' isn't referenced • lib/screens/settings/settings_screen.dart:630:8 • unused_element +warning • The value of the local variable 'result' isn't used • lib/screens/settings/settings_screen.dart:631:11 • unused_local_variable + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:671:29 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:694:29 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/settings_screen.dart:727:29 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/theme_settings_screen.dart:15:29 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/theme_settings_screen.dart:30:27 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/theme_settings_screen.dart:31:28 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/theme_settings_screen.dart:46:27 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/theme_settings_screen.dart:47:28 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/theme_settings_screen.dart:62:27 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/theme_settings_screen.dart:63:28 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/wechat_binding_screen.dart:113:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/wechat_binding_screen.dart:114:18 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/wechat_binding_screen.dart:118:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/wechat_binding_screen.dart:123:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/wechat_binding_screen.dart:181:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/wechat_binding_screen.dart:204:31 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/wechat_binding_screen.dart:206:40 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/wechat_binding_screen.dart:214:27 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/wechat_binding_screen.dart:219:36 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/wechat_binding_screen.dart:237:29 • prefer_const_constructors + info • Use 'const' literals as arguments to constructors of '@immutable' classes • lib/screens/settings/wechat_binding_screen.dart:238:41 • prefer_const_literals_to_create_immutables + info • Use 'const' with the constructor to improve performance • lib/screens/settings/wechat_binding_screen.dart:239:33 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/wechat_binding_screen.dart:240:44 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/wechat_binding_screen.dart:242:33 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/wechat_binding_screen.dart:244:42 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/wechat_binding_screen.dart:261:41 • prefer_const_constructors + error • Invalid constant value • lib/screens/settings/wechat_binding_screen.dart:301:44 • invalid_constant + info • Unnecessary 'const' keyword • lib/screens/settings/wechat_binding_screen.dart:304:41 • unnecessary_const + info • Unnecessary 'const' keyword • lib/screens/settings/wechat_binding_screen.dart:307:39 • unnecessary_const + info • Use 'const' with the constructor to improve performance • lib/screens/settings/wechat_binding_screen.dart:332:29 • prefer_const_constructors + info • Use 'const' literals as arguments to constructors of '@immutable' classes • lib/screens/settings/wechat_binding_screen.dart:333:41 • prefer_const_literals_to_create_immutables + info • Use 'const' with the constructor to improve performance • lib/screens/settings/wechat_binding_screen.dart:334:33 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/wechat_binding_screen.dart:335:44 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/wechat_binding_screen.dart:337:33 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/wechat_binding_screen.dart:339:42 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/wechat_binding_screen.dart:347:29 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/wechat_binding_screen.dart:349:38 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/wechat_binding_screen.dart:385:31 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/wechat_binding_screen.dart:387:40 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/wechat_binding_screen.dart:395:27 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/settings/wechat_binding_screen.dart:399:36 • prefer_const_constructors + info • Don't use 'BuildContext's across async gaps • lib/screens/splash_screen.dart:41:13 • use_build_context_synchronously + info • Don't use 'BuildContext's across async gaps • lib/screens/splash_screen.dart:43:13 • use_build_context_synchronously + info • Don't use 'BuildContext's across async gaps • lib/screens/splash_screen.dart:54:7 • use_build_context_synchronously + info • Don't use 'BuildContext's across async gaps • lib/screens/splash_screen.dart:57:7 • use_build_context_synchronously + info • Use 'const' with the constructor to improve performance • lib/screens/splash_screen.dart:93:24 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/splash_screen.dart:96:26 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/splash_screen.dart:100:15 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/splash_screen.dart:102:24 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/splash_screen.dart:110:15 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/splash_screen.dart:112:24 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/theme_management_screen.dart:45:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/theme_management_screen.dart:49:19 • prefer_const_constructors + info • Unnecessary 'const' keyword • lib/screens/theme_management_screen.dart:57:21 • unnecessary_const + info • Unnecessary 'const' keyword • lib/screens/theme_management_screen.dart:67:21 • unnecessary_const + info • Unnecessary 'const' keyword • lib/screens/theme_management_screen.dart:77:21 • unnecessary_const + info • Unnecessary 'const' keyword • lib/screens/theme_management_screen.dart:87:21 • unnecessary_const + info • Unnecessary 'const' keyword • lib/screens/theme_management_screen.dart:97:21 • unnecessary_const + info • Unnecessary 'const' keyword • lib/screens/theme_management_screen.dart:107:21 • unnecessary_const + info • Unnecessary 'const' keyword • lib/screens/theme_management_screen.dart:117:21 • unnecessary_const + info • Use 'const' with the constructor to improve performance • lib/screens/theme_management_screen.dart:157:17 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/theme_management_screen.dart:159:26 • prefer_const_constructors + info • 'groupValue' is deprecated and shouldn't be used. Use a RadioGroup ancestor to manage group value instead. This feature was deprecated after v3.32.0-0.0.pre • lib/screens/theme_management_screen.dart:170:27 • deprecated_member_use + info • 'onChanged' is deprecated and shouldn't be used. Use RadioGroup to handle value change instead. This feature was deprecated after v3.32.0-0.0.pre • lib/screens/theme_management_screen.dart:171:27 • deprecated_member_use + info • Use 'const' with the constructor to improve performance • lib/screens/theme_management_screen.dart:192:19 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/theme_management_screen.dart:194:28 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/theme_management_screen.dart:219:9 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/theme_management_screen.dart:221:18 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/theme_management_screen.dart:248:25 • prefer_const_constructors + info • Unnecessary 'const' keyword • lib/screens/theme_management_screen.dart:279:19 • unnecessary_const + info • Use 'const' with the constructor to improve performance • lib/screens/theme_management_screen.dart:304:11 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/theme_management_screen.dart:306:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/theme_management_screen.dart:333:27 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/theme_management_screen.dart:338:27 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/theme_management_screen.dart:343:27 • prefer_const_constructors + info • Unnecessary 'const' keyword • lib/screens/theme_management_screen.dart:351:29 • unnecessary_const + info • Unnecessary 'const' keyword • lib/screens/theme_management_screen.dart:361:29 • unnecessary_const + info • Unnecessary 'const' keyword • lib/screens/theme_management_screen.dart:371:29 • unnecessary_const + info • Don't use 'BuildContext's across async gaps • lib/screens/theme_management_screen.dart:466:28 • use_build_context_synchronously + info • Don't use 'BuildContext's across async gaps • lib/screens/theme_management_screen.dart:483:28 • use_build_context_synchronously + info • Don't use 'BuildContext's across async gaps • lib/screens/theme_management_screen.dart:508:28 • use_build_context_synchronously + info • Don't use 'BuildContext's across async gaps • lib/screens/theme_management_screen.dart:515:28 • use_build_context_synchronously + info • Don't use 'BuildContext's across async gaps • lib/screens/theme_management_screen.dart:527:28 • use_build_context_synchronously + info • Don't use 'BuildContext's across async gaps • lib/screens/theme_management_screen.dart:534:28 • use_build_context_synchronously + info • Use 'const' with the constructor to improve performance • lib/screens/theme_management_screen.dart:547:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/theme_management_screen.dart:552:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/theme_management_screen.dart:560:20 • prefer_const_constructors + info • Don't use 'BuildContext's across async gaps • lib/screens/theme_management_screen.dart:569:30 • use_build_context_synchronously + info • Don't use 'BuildContext's across async gaps • lib/screens/theme_management_screen.dart:576:30 • use_build_context_synchronously + info • Don't use 'BuildContext's across async gaps • lib/screens/theme_management_screen.dart:590:30 • use_build_context_synchronously + info • Don't use 'BuildContext's across async gaps • lib/screens/theme_management_screen.dart:597:30 • use_build_context_synchronously + info • Don't use 'BuildContext's across async gaps • lib/screens/theme_management_screen.dart:605:28 • use_build_context_synchronously + info • Use 'const' with the constructor to improve performance • lib/screens/theme_management_screen.dart:620:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/theme_management_screen.dart:624:13 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/theme_management_screen.dart:642:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/theme_management_screen.dart:650:20 • prefer_const_constructors + info • Don't use 'BuildContext's across async gaps • lib/screens/theme_management_screen.dart:673:28 • use_build_context_synchronously + info • Don't use 'BuildContext's across async gaps • lib/screens/theme_management_screen.dart:680:28 • use_build_context_synchronously + info • Use 'const' with the constructor to improve performance • lib/screens/theme_management_screen.dart:693:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/theme_management_screen.dart:694:18 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/theme_management_screen.dart:698:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/theme_management_screen.dart:706:20 • prefer_const_constructors + info • Don't use 'BuildContext's across async gaps • lib/screens/theme_management_screen.dart:714:28 • use_build_context_synchronously +warning • The value of the local variable 'currentLedger' isn't used • lib/screens/transactions/transaction_add_screen.dart:71:11 • unused_local_variable + info • Use 'const' with the constructor to improve performance • lib/screens/transactions/transaction_add_screen.dart:79:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/transactions/transaction_add_screen.dart:96:23 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/transactions/transaction_add_screen.dart:98:32 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/transactions/transaction_add_screen.dart:141:21 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/transactions/transaction_add_screen.dart:143:30 • prefer_const_constructors +warning • The left operand can't be null, so the right operand is never executed • lib/screens/transactions/transaction_add_screen.dart:219:52 • dead_null_aware_expression +warning • The left operand can't be null, so the right operand is never executed • lib/screens/transactions/transaction_add_screen.dart:222:57 • dead_null_aware_expression + info • Use 'const' with the constructor to improve performance • lib/screens/transactions/transaction_add_screen.dart:248:23 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/transactions/transaction_add_screen.dart:250:32 • prefer_const_constructors +warning • The left operand can't be null, so the right operand is never executed • lib/screens/transactions/transaction_add_screen.dart:275:54 • dead_null_aware_expression +warning • The left operand can't be null, so the right operand is never executed • lib/screens/transactions/transaction_add_screen.dart:278:59 • dead_null_aware_expression + info • Use 'const' with the constructor to improve performance • lib/screens/transactions/transaction_add_screen.dart:316:23 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/transactions/transaction_add_screen.dart:318:32 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/transactions/transaction_add_screen.dart:353:21 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/transactions/transaction_add_screen.dart:355:30 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/transactions/transaction_add_screen.dart:363:35 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/transactions/transaction_add_screen.dart:373:35 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/transactions/transaction_add_screen.dart:395:21 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/transactions/transaction_add_screen.dart:397:30 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/transactions/transaction_add_screen.dart:416:21 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/transactions/transaction_add_screen.dart:418:30 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/transactions/transaction_add_screen.dart:448:25 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/transactions/transaction_add_screen.dart:450:34 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/transactions/transaction_add_screen.dart:496:15 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/transactions/transaction_add_screen.dart:497:16 • prefer_const_constructors +warning • The value of the local variable 'transaction' isn't used • lib/screens/transactions/transaction_add_screen.dart:554:13 • unused_local_variable + info • Use 'const' with the constructor to improve performance • lib/screens/transactions/transaction_detail_screen.dart:15:16 • prefer_const_constructors +warning • The value of the field '_selectedFilter' isn't used • lib/screens/transactions/transactions_screen.dart:20:10 • unused_field + info • Use 'const' with the constructor to improve performance • lib/screens/transactions/transactions_screen.dart:41:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/transactions/transactions_screen.dart:53:19 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/transactions/transactions_screen.dart:57:19 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/transactions/transactions_screen.dart:73:15 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/transactions/transactions_screen.dart:74:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/transactions/transactions_screen.dart:93:13 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/transactions/transactions_screen.dart:104:26 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/transactions/transactions_screen.dart:107:25 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/transactions/transactions_screen.dart:108:26 • prefer_const_constructors + info • Don't use 'BuildContext's across async gaps, guarded by an unrelated 'mounted' check • lib/screens/transactions/transactions_screen.dart:112:44 • use_build_context_synchronously + info • Use 'const' with the constructor to improve performance • lib/screens/transactions/transactions_screen.dart:192:19 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/transactions/transactions_screen.dart:235:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/transactions/transactions_screen.dart:241:24 • prefer_const_constructors + info • Don't use 'BuildContext's across async gaps • lib/screens/transactions/transactions_screen.dart:255:33 • use_build_context_synchronously + info • Use 'const' with the constructor to improve performance • lib/screens/transactions/transactions_screen.dart:261:24 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/transactions/transactions_screen.dart:262:22 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/transactions/transactions_screen.dart:269:24 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/transactions/transactions_screen.dart:270:22 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/transactions/transactions_screen.dart:286:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/transactions/transactions_screen.dart:293:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/transactions/transactions_screen.dart:324:15 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/transactions/transactions_screen.dart:326:24 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/transactions/transactions_screen.dart:339:26 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/transactions/transactions_screen.dart:341:24 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/transactions/transactions_screen.dart:342:27 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/transactions/transactions_screen.dart:355:26 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/transactions/transactions_screen.dart:357:24 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/transactions/transactions_screen.dart:358:27 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/transactions/transactions_screen.dart:371:26 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/transactions/transactions_screen.dart:373:24 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/transactions/transactions_screen.dart:374:27 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/transactions/transactions_screen.dart:398:15 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/transactions/transactions_screen.dart:409:13 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/user/edit_profile_screen.dart:98:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/user/edit_profile_screen.dart:150:37 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/user/edit_profile_screen.dart:187:23 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/user/edit_profile_screen.dart:189:32 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/user/edit_profile_screen.dart:252:23 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/user/edit_profile_screen.dart:254:32 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/user/edit_profile_screen.dart:291:23 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/user/edit_profile_screen.dart:293:32 • prefer_const_constructors + error • Invalid constant value • lib/screens/user/edit_profile_screen.dart:332:30 • invalid_constant + info • Unnecessary 'const' keyword • lib/screens/user/edit_profile_screen.dart:338:25 • unnecessary_const + info • Use 'const' with the constructor to improve performance • lib/screens/welcome_screen.dart:27:19 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/welcome_screen.dart:29:28 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/welcome_screen.dart:35:19 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/welcome_screen.dart:37:28 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/welcome_screen.dart:44:19 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/welcome_screen.dart:46:28 • prefer_const_constructors + error • Invalid constant value • lib/screens/welcome_screen.dart:90:34 • invalid_constant + error • Invalid constant value • lib/screens/welcome_screen.dart:111:34 • invalid_constant + info • Unnecessary 'const' keyword • lib/screens/welcome_screen.dart:116:31 • unnecessary_const + info • Use 'const' with the constructor to improve performance • lib/screens/welcome_screen.dart:132:28 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/screens/welcome_screen.dart:134:30 • prefer_const_constructors + info • The private field _warned could be 'final' • lib/services/admin/currency_admin_service.dart:8:8 • prefer_final_fields +warning • The value of the field '_warned' isn't used • lib/services/admin/currency_admin_service.dart:8:8 • unused_field +warning • The declaration '_isAdmin' isn't referenced • lib/services/admin/currency_admin_service.dart:10:8 • unused_element + error • Undefined class 'Ref' • lib/services/admin/currency_admin_service.dart:10:17 • undefined_class +warning • Unnecessary cast • lib/services/api/auth_service.dart:58:35 • unnecessary_cast +warning • The receiver can't be null, so the null-aware operator '?.' is unnecessary • lib/services/api/auth_service.dart:62:78 • invalid_null_aware_operator + info • Parameter 'message' could be a super parameter • lib/services/api/family_service.dart:345:3 • use_super_parameters +warning • Unnecessary type check; the result is always 'true' • lib/services/api_service.dart:59:9 • unnecessary_type_check + info • Statements in an if should be enclosed in a block • lib/services/api_service.dart:61:9 • curly_braces_in_flow_control_structures + info • Statements in an if should be enclosed in a block • lib/services/api_service.dart:63:9 • curly_braces_in_flow_control_structures +warning • Unnecessary type check; the result is always 'true' • lib/services/api_service.dart:74:9 • unnecessary_type_check + info • Statements in an if should be enclosed in a block • lib/services/api_service.dart:76:9 • curly_braces_in_flow_control_structures + info • Statements in an if should be enclosed in a block • lib/services/api_service.dart:78:9 • curly_braces_in_flow_control_structures +warning • Unnecessary type check; the result is always 'true' • lib/services/api_service.dart:89:9 • unnecessary_type_check + info • Statements in an if should be enclosed in a block • lib/services/api_service.dart:91:9 • curly_braces_in_flow_control_structures + info • Statements in an if should be enclosed in a block • lib/services/api_service.dart:93:9 • curly_braces_in_flow_control_structures +warning • Unnecessary type check; the result is always 'true' • lib/services/api_service.dart:104:9 • unnecessary_type_check + info • Statements in an if should be enclosed in a block • lib/services/api_service.dart:106:9 • curly_braces_in_flow_control_structures + info • Statements in an if should be enclosed in a block • lib/services/api_service.dart:108:9 • curly_braces_in_flow_control_structures +warning • Unnecessary type check; the result is always 'true' • lib/services/api_service.dart:133:9 • unnecessary_type_check +warning • Unnecessary type check; the result is always 'true' • lib/services/api_service.dart:147:9 • unnecessary_type_check +warning • Unnecessary type check; the result is always 'true' • lib/services/api_service.dart:161:9 • unnecessary_type_check +warning • Unnecessary type check; the result is always 'true' • lib/services/api_service.dart:174:9 • unnecessary_type_check + info • Statements in an if should be enclosed in a block • lib/services/api_service.dart:175:7 • curly_braces_in_flow_control_structures +warning • Unnecessary type check; the result is always 'true' • lib/services/api_service.dart:191:9 • unnecessary_type_check +warning • Unnecessary type check; the result is always 'true' • lib/services/api_service.dart:209:9 • unnecessary_type_check +warning • Unnecessary type check; the result is always 'true' • lib/services/api_service.dart:247:9 • unnecessary_type_check +warning • Unnecessary type check; the result is always 'true' • lib/services/api_service.dart:261:9 • unnecessary_type_check +warning • Unnecessary type check; the result is always 'true' • lib/services/api_service.dart:286:9 • unnecessary_type_check +warning • Unnecessary type check; the result is always 'true' • lib/services/api_service.dart:312:9 • unnecessary_type_check +warning • Unnecessary type check; the result is always 'true' • lib/services/api_service.dart:326:9 • unnecessary_type_check +warning • Unnecessary type check; the result is always 'true' • lib/services/api_service.dart:349:9 • unnecessary_type_check +warning • Unnecessary type check; the result is always 'true' • lib/services/api_service.dart:363:9 • unnecessary_type_check + info • Statements in an if should be enclosed in a block • lib/services/api_service.dart:364:7 • curly_braces_in_flow_control_structures +warning • Unnecessary type check; the result is always 'true' • lib/services/api_service.dart:386:9 • unnecessary_type_check +warning • Unnecessary type check; the result is always 'true' • lib/services/api_service.dart:401:9 • unnecessary_type_check +warning • Unnecessary type check; the result is always 'true' • lib/services/api_service.dart:412:9 • unnecessary_type_check +warning • Unnecessary type check; the result is always 'true' • lib/services/api_service.dart:423:9 • unnecessary_type_check +warning • The value of the field '_coincapIds' isn't used • lib/services/crypto_price_service.dart:44:36 • unused_field + info • The 'if' statement could be replaced by a null-aware assignment • lib/services/crypto_price_service.dart:89:5 • prefer_conditional_assignment +warning • The declaration '_headers' isn't referenced • lib/services/currency_service.dart:16:31 • unused_element + info • The imported package 'uni_links' isn't a dependency of the importing package • lib/services/deep_link_service.dart:2:8 • depend_on_referenced_packages + error • Target of URI doesn't exist: 'package:uni_links/uni_links.dart' • lib/services/deep_link_service.dart:2:8 • uri_does_not_exist + error • Target of URI doesn't exist: '../screens/invitations/accept_invitation_screen.dart' • lib/services/deep_link_service.dart:4:8 • uri_does_not_exist + error • The method 'getInitialLink' isn't defined for the type 'DeepLinkService' • lib/services/deep_link_service.dart:23:33 • undefined_method + info • Parameter 'key' could be a super parameter • lib/services/deep_link_service.dart:449:9 • use_super_parameters + info • Use 'const' with the constructor to improve performance • lib/services/deep_link_service.dart:523:15 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/services/deep_link_service.dart:543:24 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/services/deep_link_service.dart:553:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/services/deep_link_service.dart:560:13 • prefer_const_constructors + info • 'surfaceVariant' is deprecated and shouldn't be used. Use surfaceContainerHighest instead. This feature was deprecated after v3.18.0-0.1.pre • lib/services/deep_link_service.dart:581:42 • deprecated_member_use + info • Use 'const' with the constructor to improve performance • lib/services/deep_link_service.dart:601:28 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/services/deep_link_service.dart:608:28 • prefer_const_constructors + info • Parameter 'key' could be a super parameter • lib/services/deep_link_service.dart:637:9 • use_super_parameters + info • Use 'const' with the constructor to improve performance • lib/services/deep_link_service.dart:645:29 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/services/deep_link_service.dart:658:24 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/services/deep_link_service.dart:660:26 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/services/deep_link_service.dart:663:13 • prefer_const_constructors + error • The method 'getUserPermissions' isn't defined for the type 'FamilyService' • lib/services/dynamic_permissions_service.dart:76:32 • undefined_method + error • The method 'updateUserPermissions' isn't defined for the type 'FamilyService' • lib/services/dynamic_permissions_service.dart:185:44 • undefined_method + error • The method 'grantTemporaryPermission' isn't defined for the type 'FamilyService' • lib/services/dynamic_permissions_service.dart:240:28 • undefined_method + error • The method 'revokeTemporaryPermission' isn't defined for the type 'FamilyService' • lib/services/dynamic_permissions_service.dart:278:28 • undefined_method + error • The method 'delegatePermissions' isn't defined for the type 'FamilyService' • lib/services/dynamic_permissions_service.dart:316:28 • undefined_method + error • The method 'revokeDelegation' isn't defined for the type 'FamilyService' • lib/services/dynamic_permissions_service.dart:357:28 • undefined_method + info • The imported package 'mailer' isn't a dependency of the importing package • lib/services/email_notification_service.dart:2:8 • depend_on_referenced_packages + error • Target of URI doesn't exist: 'package:mailer/mailer.dart' • lib/services/email_notification_service.dart:2:8 • uri_does_not_exist + info • The imported package 'mailer' isn't a dependency of the importing package • lib/services/email_notification_service.dart:3:8 • depend_on_referenced_packages + error • Target of URI doesn't exist: 'package:mailer/smtp_server.dart' • lib/services/email_notification_service.dart:3:8 • uri_does_not_exist + error • Undefined class 'SmtpServer' • lib/services/email_notification_service.dart:15:8 • undefined_class + error • The method 'SmtpServer' isn't defined for the type 'EmailNotificationService' • lib/services/email_notification_service.dart:61:21 • undefined_method + info • Use 'rethrow' to rethrow a caught exception • lib/services/email_notification_service.dart:78:7 • use_rethrow_when_possible + error • The method 'gmail' isn't defined for the type 'EmailNotificationService' • lib/services/email_notification_service.dart:84:19 • undefined_method + error • The method 'SmtpServer' isn't defined for the type 'EmailNotificationService' • lib/services/email_notification_service.dart:93:19 • undefined_method + error • The method 'Message' isn't defined for the type 'EmailNotificationService' • lib/services/email_notification_service.dart:488:21 • undefined_method + error • The name 'Address' isn't a class • lib/services/email_notification_service.dart:489:22 • creation_with_non_type + error • The method 'send' isn't defined for the type 'EmailNotificationService' • lib/services/email_notification_service.dart:494:11 • undefined_method + info • The member 'dispose' overrides an inherited member but isn't annotated with '@override' • lib/services/email_notification_service.dart:572:8 • annotate_overrides +warning • The value of the local variable 'usedFallback' isn't used • lib/services/exchange_rate_service.dart:36:10 • unused_local_variable +warning • The value of the field '_keySyncStatus' isn't used • lib/services/family_settings_service.dart:9:23 • unused_field + error • The method 'getFamilySettings' isn't defined for the type 'FamilyService' • lib/services/family_settings_service.dart:92:45 • undefined_method + error • The method 'updateFamilySettings' isn't defined for the type 'FamilyService' • lib/services/family_settings_service.dart:180:46 • undefined_method + error • The method 'deleteFamilySettings' isn't defined for the type 'FamilyService' • lib/services/family_settings_service.dart:186:40 • undefined_method + error • The method 'updateUserPreferences' isn't defined for the type 'FamilyService' • lib/services/family_settings_service.dart:192:46 • undefined_method + error • The method 'getFamilySettings' isn't defined for the type 'FamilyService' • lib/services/family_settings_service.dart:233:45 • undefined_method + error • The method 'firstWhere' isn't defined for the type 'Family' • lib/services/permission_service.dart:101:31 • undefined_method +warning • This default clause is covered by the previous cases • lib/services/permission_service.dart:195:7 • unreachable_switch_default + info • The imported package 'share_plus' isn't a dependency of the importing package • lib/services/share_service.dart:2:8 • depend_on_referenced_packages + error • Target of URI doesn't exist: 'package:share_plus/share_plus.dart' • lib/services/share_service.dart:2:8 • uri_does_not_exist + info • The imported package 'screenshot' isn't a dependency of the importing package • lib/services/share_service.dart:6:8 • depend_on_referenced_packages + error • Target of URI doesn't exist: 'package:screenshot/screenshot.dart' • lib/services/share_service.dart:6:8 • uri_does_not_exist + error • The name 'XFile' isn't a type, so it can't be used as a type argument • lib/services/share_service.dart:20:40 • non_type_as_type_argument + error • Undefined class 'ScreenshotController' • lib/services/share_service.dart:29:16 • undefined_class + error • The method 'ScreenshotController' isn't defined for the type 'ShareService' • lib/services/share_service.dart:30:7 • undefined_method + info • Don't use 'BuildContext's across async gaps • lib/services/share_service.dart:66:18 • use_build_context_synchronously + info • Use 'const' with the constructor to improve performance • lib/services/share_service.dart:121:17 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/services/share_service.dart:123:26 • prefer_const_constructors + error • The method 'XFile' isn't defined for the type 'ShareService' • lib/services/share_service.dart:142:12 • undefined_method + info • Don't use 'BuildContext's across async gaps • lib/services/share_service.dart:150:18 • use_build_context_synchronously + error • The property 'isNotEmpty' can't be unconditionally accessed because the receiver can be 'null' • lib/services/share_service.dart:176:20 • unchecked_use_of_nullable_value + error • The method 'join' can't be unconditionally invoked because the receiver can be 'null' • lib/services/share_service.dart:176:60 • unchecked_use_of_nullable_value + info • Don't use 'BuildContext's across async gaps • lib/services/share_service.dart:186:18 • use_build_context_synchronously + info • Don't use 'BuildContext's across async gaps • lib/services/share_service.dart:207:18 • use_build_context_synchronously +warning • The value of the local variable 'weiboUrl' isn't used • lib/services/share_service.dart:241:17 • unused_local_variable + info • Don't use 'BuildContext's across async gaps • lib/services/share_service.dart:256:18 • use_build_context_synchronously + info • Don't use 'BuildContext's across async gaps • lib/services/share_service.dart:280:18 • use_build_context_synchronously + error • The method 'XFile' isn't defined for the type 'ShareService' • lib/services/share_service.dart:293:10 • undefined_method + info • Don't use 'BuildContext's across async gaps • lib/services/share_service.dart:297:18 • use_build_context_synchronously + error • The method 'XFile' isn't defined for the type 'ShareService' • lib/services/share_service.dart:308:43 • undefined_method + info • Don't use 'BuildContext's across async gaps • lib/services/share_service.dart:311:18 • use_build_context_synchronously + info • Parameter 'key' could be a super parameter • lib/services/share_service.dart:374:9 • use_super_parameters + info • 'surfaceVariant' is deprecated and shouldn't be used. Use surfaceContainerHighest instead. This feature was deprecated after v3.18.0-0.1.pre • lib/services/share_service.dart:409:42 • deprecated_member_use + info • Use 'const' with the constructor to improve performance • lib/services/share_service.dart:431:21 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/services/share_service.dart:441:29 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/services/share_service.dart:535:22 • prefer_const_constructors +warning • The value of the field '_keyAppSettings' isn't used • lib/services/storage_service.dart:20:23 • unused_field + info • 'window' is deprecated and shouldn't be used. Look up the current FlutterView from the context via View.of(context) or consult the PlatformDispatcher directly instead. Deprecated to prepare for the upcoming multi-window support. This feature was deprecated after v3.7.0-32.0.pre • lib/services/theme_service.dart:412:46 • deprecated_member_use + info • The imported package 'web_socket_channel' isn't a dependency of the importing package • lib/services/websocket_service.dart:4:8 • depend_on_referenced_packages + info • The imported package 'web_socket_channel' isn't a dependency of the importing package • lib/services/websocket_service.dart:5:8 • depend_on_referenced_packages + info • Use 'const' with the constructor to improve performance • lib/services/websocket_service.dart:23:37 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/ui/components/accounts/account_form.dart:416:19 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/ui/components/accounts/account_form.dart:442:19 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/ui/components/accounts/account_form.dart:477:18 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/ui/components/accounts/account_form.dart:478:21 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/ui/components/accounts/account_form.dart:483:18 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/ui/components/accounts/account_form.dart:484:21 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/ui/components/accounts/account_form.dart:603:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/ui/components/accounts/account_form.dart:647:16 • prefer_const_constructors + error • The name 'AccountData' isn't a type, so it can't be used as a type argument • lib/ui/components/accounts/account_list.dart:8:14 • non_type_as_type_argument + error • Undefined class 'AccountData' • lib/ui/components/accounts/account_list.dart:10:18 • undefined_class + error • Undefined class 'AccountData' • lib/ui/components/accounts/account_list.dart:11:18 • undefined_class + info • Use 'const' with the constructor to improve performance • lib/ui/components/accounts/account_list.dart:82:21 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/ui/components/accounts/account_list.dart:83:22 • prefer_const_constructors + error • The named parameter 'balance' is required, but there's no corresponding argument • lib/ui/components/accounts/account_list.dart:101:22 • missing_required_argument + error • The named parameter 'id' is required, but there's no corresponding argument • lib/ui/components/accounts/account_list.dart:101:22 • missing_required_argument + error • The named parameter 'name' is required, but there's no corresponding argument • lib/ui/components/accounts/account_list.dart:101:22 • missing_required_argument + error • The named parameter 'type' is required, but there's no corresponding argument • lib/ui/components/accounts/account_list.dart:101:22 • missing_required_argument + error • The named parameter 'account' isn't defined • lib/ui/components/accounts/account_list.dart:102:17 • undefined_named_parameter + error • The named parameter 'onLongPress' isn't defined • lib/ui/components/accounts/account_list.dart:104:17 • undefined_named_parameter + error • The named parameter 'balance' is required, but there's no corresponding argument • lib/ui/components/accounts/account_list.dart:137:34 • missing_required_argument + error • The named parameter 'id' is required, but there's no corresponding argument • lib/ui/components/accounts/account_list.dart:137:34 • missing_required_argument + error • The named parameter 'name' is required, but there's no corresponding argument • lib/ui/components/accounts/account_list.dart:137:34 • missing_required_argument + error • The named parameter 'type' is required, but there's no corresponding argument • lib/ui/components/accounts/account_list.dart:137:34 • missing_required_argument + error • The named parameter 'account' isn't defined • lib/ui/components/accounts/account_list.dart:138:23 • undefined_named_parameter + error • The named parameter 'onLongPress' isn't defined • lib/ui/components/accounts/account_list.dart:140:23 • undefined_named_parameter + error • The name 'AccountData' isn't a type, so it can't be used as a type argument • lib/ui/components/accounts/account_list.dart:245:47 • non_type_as_type_argument + error • The property 'balance' can't be unconditionally accessed because the receiver can be 'null' • lib/ui/components/accounts/account_list.dart:247:66 • unchecked_use_of_nullable_value + error • The name 'AccountData' isn't a type, so it can't be used as a type argument • lib/ui/components/accounts/account_list.dart:279:25 • non_type_as_type_argument + error • The name 'AccountData' isn't a type, so it can't be used as a type argument • lib/ui/components/accounts/account_list.dart:280:33 • non_type_as_type_argument + error • The property 'type' can't be unconditionally accessed because the receiver can be 'null' • lib/ui/components/accounts/account_list.dart:298:37 • unchecked_use_of_nullable_value + error • The property 'balance' can't be unconditionally accessed because the receiver can be 'null' • lib/ui/components/accounts/account_list.dart:299:52 • unchecked_use_of_nullable_value + error • The name 'AccountData' isn't a type, so it can't be used as a type argument • lib/ui/components/accounts/account_list.dart:361:26 • non_type_as_type_argument + error • Undefined class 'AccountData' • lib/ui/components/accounts/account_list.dart:362:18 • undefined_class + error • The named parameter 'balance' is required, but there's no corresponding argument • lib/ui/components/accounts/account_list.dart:420:30 • missing_required_argument + error • The named parameter 'id' is required, but there's no corresponding argument • lib/ui/components/accounts/account_list.dart:420:30 • missing_required_argument + error • The named parameter 'name' is required, but there's no corresponding argument • lib/ui/components/accounts/account_list.dart:420:30 • missing_required_argument + error • The named parameter 'type' is required, but there's no corresponding argument • lib/ui/components/accounts/account_list.dart:420:30 • missing_required_argument + error • The named parameter 'account' isn't defined • lib/ui/components/accounts/account_list.dart:421:19 • undefined_named_parameter + error • The named parameter 'margin' isn't defined • lib/ui/components/accounts/account_list.dart:423:19 • undefined_named_parameter + error • The name 'AccountData' isn't a type, so it can't be used as a type argument • lib/ui/components/accounts/account_list.dart:433:33 • non_type_as_type_argument + error • The property 'balance' can't be unconditionally accessed because the receiver can be 'null' • lib/ui/components/accounts/account_list.dart:435:66 • unchecked_use_of_nullable_value + info • Use a 'SizedBox' to add whitespace to a layout • lib/ui/components/budget/budget_chart.dart:239:12 • sized_box_for_whitespace + info • Use 'const' with the constructor to improve performance • lib/ui/components/budget/budget_chart.dart:391:30 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/ui/components/budget/budget_chart.dart:392:33 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/ui/components/budget/budget_chart.dart:394:32 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/ui/components/budget/budget_chart.dart:395:33 • prefer_const_constructors + info • Use a 'SizedBox' to add whitespace to a layout • lib/ui/components/budget/budget_chart.dart:486:12 • sized_box_for_whitespace + info • Use 'const' with the constructor to improve performance • lib/ui/components/budget/budget_form.dart:290:29 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/ui/components/budget/budget_form.dart:312:31 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/ui/components/budget/budget_form.dart:338:18 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/ui/components/budget/budget_form.dart:339:21 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/ui/components/budget/budget_form.dart:346:18 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/ui/components/budget/budget_form.dart:347:21 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/ui/components/budget/budget_form.dart:358:17 • prefer_const_constructors + error • Undefined name 'ref' • lib/ui/components/budget/budget_progress.dart:106:35 • undefined_identifier + error • Undefined name 'currencyProvider' • lib/ui/components/budget/budget_progress.dart:106:44 • undefined_identifier + error • Undefined name 'ref' • lib/ui/components/budget/budget_progress.dart:106:98 • undefined_identifier + error • Undefined name 'baseCurrencyProvider' • lib/ui/components/budget/budget_progress.dart:106:107 • undefined_identifier + error • Undefined name 'ref' • lib/ui/components/budget/budget_progress.dart:107:35 • undefined_identifier + error • Undefined name 'currencyProvider' • lib/ui/components/budget/budget_progress.dart:107:44 • undefined_identifier + error • Undefined name 'ref' • lib/ui/components/budget/budget_progress.dart:107:97 • undefined_identifier + error • Undefined name 'baseCurrencyProvider' • lib/ui/components/budget/budget_progress.dart:107:106 • undefined_identifier + info • Use 'const' with the constructor to improve performance • lib/ui/components/budget/budget_progress.dart:144:21 • prefer_const_constructors + error • Methods can't be invoked in constant expressions • lib/ui/components/budget/budget_progress.dart:236:20 • const_eval_method_invocation + error • Invalid constant value • lib/ui/components/buttons/secondary_button.dart:37:14 • invalid_constant + error • The constructor being called isn't a const constructor • lib/ui/components/buttons/secondary_button.dart:39:14 • const_with_non_const + info • Unnecessary 'const' keyword • lib/ui/components/buttons/secondary_button.dart:43:31 • unnecessary_const + error • The constructor being called isn't a const constructor • lib/ui/components/buttons/secondary_button.dart:45:27 • const_with_non_const + error • Invalid constant value • lib/ui/components/buttons/secondary_button.dart:68:13 • invalid_constant +warning • The value of the local variable 'currencyFormatter' isn't used • lib/ui/components/cards/account_card.dart:43:11 • unused_local_variable +warning • The left operand can't be null, so the right operand is never executed • lib/ui/components/cards/transaction_card.dart:88:56 • dead_null_aware_expression +warning • The declaration '_formatCurrency' isn't referenced • lib/ui/components/charts/balance_chart.dart:287:10 • unused_element +warning • The declaration '_buildTooltipItems' isn't referenced • lib/ui/components/charts/balance_chart.dart:297:25 • unused_element + info • Use 'const' with the constructor to improve performance • lib/ui/components/charts/balance_chart.dart:314:14 • prefer_const_constructors + info • Unnecessary braces in a string interpolation • lib/ui/components/charts/balance_chart.dart:343:15 • unnecessary_brace_in_string_interps + info • Use 'const' with the constructor to improve performance • lib/ui/components/dashboard/account_overview.dart:23:13 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/ui/components/dashboard/account_overview.dart:28:22 • prefer_const_constructors +warning • The value of the local variable 'groupedAccounts' isn't used • lib/ui/components/dashboard/account_overview.dart:41:43 • unused_local_variable + info • Use 'const' with the constructor to improve performance • lib/ui/components/dashboard/account_overview.dart:90:15 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/ui/components/dashboard/account_overview.dart:92:24 • prefer_const_constructors + error • Methods can't be invoked in constant expressions • lib/ui/components/dashboard/account_overview.dart:120:20 • const_eval_method_invocation + info • Unnecessary 'const' keyword • lib/ui/components/dashboard/account_overview.dart:127:11 • unnecessary_const + info • Unnecessary 'const' keyword • lib/ui/components/dashboard/account_overview.dart:136:11 • unnecessary_const + info • Use 'const' with the constructor to improve performance • lib/ui/components/dashboard/budget_summary.dart:72:13 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/ui/components/dashboard/budget_summary.dart:77:22 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/ui/components/dashboard/budget_summary.dart:100:15 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/ui/components/dashboard/budget_summary.dart:102:24 • prefer_const_constructors + error • Methods can't be invoked in constant expressions • lib/ui/components/dashboard/budget_summary.dart:181:32 • const_eval_method_invocation + error • The named parameter 'actions' isn't defined • lib/ui/components/dashboard/dashboard_overview.dart:42:15 • undefined_named_parameter + error • The named parameter 'itemsPerRow' isn't defined • lib/ui/components/dashboard/dashboard_overview.dart:43:15 • undefined_named_parameter + error • Undefined name 'context' • lib/ui/components/dashboard/dashboard_overview.dart:87:35 • undefined_identifier + error • Invalid constant value • lib/ui/components/dashboard/dashboard_overview.dart:99:23 • invalid_constant + error • Undefined name 'context' • lib/ui/components/dashboard/dashboard_overview.dart:161:35 • undefined_identifier + info • Use 'const' with the constructor to improve performance • lib/ui/components/dashboard/dashboard_overview.dart:168:26 • prefer_const_constructors + error • Undefined name 'context' • lib/ui/components/dashboard/dashboard_overview.dart:207:35 • undefined_identifier + error • Undefined name 'context' • lib/ui/components/dashboard/dashboard_overview.dart:213:35 • undefined_identifier + error • Undefined name 'context' • lib/ui/components/dashboard/dashboard_overview.dart:222:29 • undefined_identifier + error • Undefined name 'context' • lib/ui/components/dashboard/dashboard_overview.dart:249:35 • undefined_identifier + info • Use 'const' with the constructor to improve performance • lib/ui/components/dashboard/dashboard_overview.dart:256:26 • prefer_const_constructors + error • Undefined name 'context' • lib/ui/components/dashboard/dashboard_overview.dart:280:33 • undefined_identifier + error • Undefined name 'context' • lib/ui/components/dashboard/dashboard_overview.dart:287:33 • undefined_identifier + error • The name 'BalanceDataPoint' isn't a type, so it can't be used as a type argument • lib/ui/components/dashboard/dashboard_overview.dart:312:14 • non_type_as_type_argument + error • The name 'QuickActionData' isn't a type, so it can't be used as a type argument • lib/ui/components/dashboard/dashboard_overview.dart:313:14 • non_type_as_type_argument + error • The name 'TransactionData' isn't a type, so it can't be used as a type argument • lib/ui/components/dashboard/dashboard_overview.dart:314:14 • non_type_as_type_argument + info • Use a 'SizedBox' to add whitespace to a layout • lib/ui/components/dashboard/quick_actions.dart:11:12 • sized_box_for_whitespace + info • Use 'const' with the constructor to improve performance • lib/ui/components/dashboard/recent_transactions.dart:172:28 • prefer_const_constructors +warning • The value of the field '_isFocused' isn't used • lib/ui/components/inputs/text_field_widget.dart:61:8 • unused_field + error • The named parameter 'backgroundColor' isn't defined • lib/ui/components/layout/app_scaffold.dart:208:7 • undefined_named_parameter + error • Invalid constant value • lib/ui/components/loading/loading_widget.dart:27:18 • invalid_constant +warning • The value of the local variable 'theme' isn't used • lib/ui/components/loading/loading_widget.dart:120:11 • unused_local_variable + info • Use 'const' with the constructor to improve performance • lib/ui/components/transactions/transaction_filter.dart:72:26 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/ui/components/transactions/transaction_filter.dart:85:27 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/ui/components/transactions/transaction_filter.dart:145:22 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/ui/components/transactions/transaction_filter.dart:156:22 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/ui/components/transactions/transaction_filter.dart:171:22 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/ui/components/transactions/transaction_filter.dart:186:22 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/ui/components/transactions/transaction_filter.dart:265:22 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/ui/components/transactions/transaction_filter.dart:269:22 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/ui/components/transactions/transaction_filter.dart:273:22 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/ui/components/transactions/transaction_filter.dart:277:22 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/ui/components/transactions/transaction_filter.dart:281:22 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/ui/components/transactions/transaction_filter.dart:464:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/ui/components/transactions/transaction_filter.dart:471:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/ui/components/transactions/transaction_form.dart:290:23 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/ui/components/transactions/transaction_form.dart:378:21 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/ui/components/transactions/transaction_form.dart:393:23 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/ui/components/transactions/transaction_form.dart:564:14 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/ui/components/transactions/transaction_form.dart:590:18 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/ui/components/transactions/transaction_form.dart:594:18 • prefer_const_constructors + error • The name 'TransactionData' isn't a type, so it can't be used as a type argument • lib/ui/components/transactions/transaction_list.dart:11:14 • non_type_as_type_argument + error • Undefined class 'TransactionData' • lib/ui/components/transactions/transaction_list.dart:16:18 • undefined_class + error • Undefined class 'TransactionData' • lib/ui/components/transactions/transaction_list.dart:17:18 • undefined_class + error • The argument type 'Object?' can't be assigned to the parameter type 'Transaction?'. • lib/ui/components/transactions/transaction_list.dart:128:30 • argument_type_not_assignable + error • The name 'TransactionData' isn't a type, so it can't be used as a type argument • lib/ui/components/transactions/transaction_list.dart:141:27 • non_type_as_type_argument + error • The name 'TransactionData' isn't a type, so it can't be used as a type argument • lib/ui/components/transactions/transaction_list.dart:199:22 • non_type_as_type_argument + error • The name 'TransactionData' isn't a type, so it can't be used as a type argument • lib/ui/components/transactions/transaction_list.dart:200:30 • non_type_as_type_argument + error • The name 'TransactionData' isn't a type, so it can't be used as a type argument • lib/ui/components/transactions/transaction_list.dart:220:34 • non_type_as_type_argument + error • The property 'amount' can't be unconditionally accessed because the receiver can be 'null' • lib/ui/components/transactions/transaction_list.dart:221:55 • unchecked_use_of_nullable_value +warning • The declaration '_formatAmount' isn't referenced • lib/ui/components/transactions/transaction_list.dart:245:10 • unused_element + error • The name 'TransactionData' isn't a type, so it can't be used as a type argument • lib/ui/components/transactions/transaction_list.dart:253:14 • non_type_as_type_argument + error • Undefined class 'TransactionData' • lib/ui/components/transactions/transaction_list.dart:254:18 • undefined_class + error • Undefined class 'TransactionData' • lib/ui/components/transactions/transaction_list.dart:255:18 • undefined_class + error • Undefined class 'TransactionData' • lib/ui/components/transactions/transaction_list.dart:256:18 • undefined_class + error • Undefined class 'TransactionData' • lib/ui/components/transactions/transaction_list.dart:331:29 • undefined_class + info • Use 'const' with the constructor to improve performance • lib/ui/components/transactions/transaction_list.dart:355:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/ui/components/transactions/transaction_list.dart:364:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/ui/components/transactions/transaction_list.dart:381:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/ui/components/transactions/transaction_list.dart:382:22 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/ui/components/transactions/transaction_list.dart:386:24 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/ui/components/transactions/transaction_list.dart:393:24 • prefer_const_constructors + error • The name 'TransactionData' isn't a type, so it can't be used as a type argument • lib/ui/components/transactions/transaction_list.dart:401:22 • non_type_as_type_argument + error • The name 'TransactionData' isn't a type, so it can't be used as a type argument • lib/ui/components/transactions/transaction_list.dart:402:30 • non_type_as_type_argument +warning • The value of the local variable 'isTransfer' isn't used • lib/ui/components/transactions/transaction_list_item.dart:23:11 • unused_local_variable + info • Use 'const' with the constructor to improve performance • lib/ui/components/transactions/transaction_list_item.dart:83:36 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/ui/components/transactions/transaction_list_item.dart:85:38 • prefer_const_constructors + info • Dangling library doc comment • lib/utils/constants.dart:1:1 • dangling_library_doc_comments + info • The import of 'package:flutter/foundation.dart' is unnecessary because all of the used elements are also provided by the import of 'package:flutter/material.dart' • lib/utils/image_utils.dart:2:8 • unnecessary_import +warning • The value of the local variable 'path' isn't used • lib/utils/image_utils.dart:152:13 • unused_local_variable +warning • The value of the local variable 'imageExtensions' isn't used • lib/utils/image_utils.dart:153:13 • unused_local_variable + info • Use 'isNotEmpty' instead of 'length' to test whether the collection is empty • lib/utils/string_utils.dart:9:12 • prefer_is_empty + info • Parameter 'key' could be a super parameter • lib/widgets/batch_operation_bar.dart:13:9 • use_super_parameters + info • Use 'const' with the constructor to improve performance • lib/widgets/batch_operation_bar.dart:85:25 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/batch_operation_bar.dart:221:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/batch_operation_bar.dart:226:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/batch_operation_bar.dart:239:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/batch_operation_bar.dart:254:13 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/batch_operation_bar.dart:294:20 • prefer_const_constructors + info • Don't use 'BuildContext's across async gaps • lib/widgets/batch_operation_bar.dart:303:29 • use_build_context_synchronously + info • Don't use 'BuildContext's across async gaps • lib/widgets/batch_operation_bar.dart:305:36 • use_build_context_synchronously + info • Use 'const' with the constructor to improve performance • lib/widgets/batch_operation_bar.dart:317:20 • prefer_const_constructors + info • Parameter 'key' could be a super parameter • lib/widgets/batch_operation_bar.dart:330:9 • use_super_parameters + info • Use 'const' with the constructor to improve performance • lib/widgets/batch_operation_bar.dart:346:14 • prefer_const_constructors + info • 'value' is deprecated and shouldn't be used. Use initialValue instead. This will set the initial value for the form field. This feature was deprecated after v3.33.0-1.0.pre • lib/widgets/batch_operation_bar.dart:355:13 • deprecated_member_use + info • Use 'const' with the constructor to improve performance • lib/widgets/batch_operation_bar.dart:379:18 • prefer_const_constructors + info • Don't use 'BuildContext's across async gaps • lib/widgets/batch_operation_bar.dart:388:27 • use_build_context_synchronously + info • Don't use 'BuildContext's across async gaps • lib/widgets/batch_operation_bar.dart:390:34 • use_build_context_synchronously + info • Use 'const' with the constructor to improve performance • lib/widgets/batch_operation_bar.dart:396:18 • prefer_const_constructors + info • Parameter 'key' could be a super parameter • lib/widgets/batch_operation_bar.dart:408:9 • use_super_parameters + info • Use 'const' with the constructor to improve performance • lib/widgets/batch_operation_bar.dart:427:14 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/batch_operation_bar.dart:435:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/batch_operation_bar.dart:436:23 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/batch_operation_bar.dart:445:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/batch_operation_bar.dart:446:23 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/batch_operation_bar.dart:459:18 • prefer_const_constructors + info • Don't use 'BuildContext's across async gaps • lib/widgets/batch_operation_bar.dart:475:27 • use_build_context_synchronously + info • Don't use 'BuildContext's across async gaps • lib/widgets/batch_operation_bar.dart:477:34 • use_build_context_synchronously + info • Use 'const' with the constructor to improve performance • lib/widgets/batch_operation_bar.dart:483:18 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/bottom_sheets/import_details_sheet.dart:42:21 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/bottom_sheets/import_details_sheet.dart:42:41 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/bottom_sheets/import_details_sheet.dart:51:29 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/bottom_sheets/import_details_sheet.dart:52:30 • prefer_const_constructors + info • 'value' is deprecated and shouldn't be used. Use component accessors like .r or .g, or toARGB32 for an explicit conversion • lib/widgets/color_picker_dialog.dart:44:28 • deprecated_member_use + info • Use 'const' with the constructor to improve performance • lib/widgets/color_picker_dialog.dart:57:14 • prefer_const_constructors + error • The constructor being called isn't a const constructor • lib/widgets/color_picker_dialog.dart:65:13 • const_with_non_const + error • The constructor being called isn't a const constructor • lib/widgets/color_picker_dialog.dart:70:31 • const_with_non_const + error • The constructor being called isn't a const constructor • lib/widgets/color_picker_dialog.dart:71:25 • const_with_non_const + info • Unnecessary 'const' keyword • lib/widgets/color_picker_dialog.dart:75:13 • unnecessary_const + info • Unnecessary 'const' keyword • lib/widgets/color_picker_dialog.dart:80:27 • unnecessary_const + error • The constructor being called isn't a const constructor • lib/widgets/color_picker_dialog.dart:88:17 • const_with_non_const + error • The constructor being called isn't a const constructor • lib/widgets/color_picker_dialog.dart:88:51 • const_with_non_const + error • The constructor being called isn't a const constructor • lib/widgets/color_picker_dialog.dart:89:17 • const_with_non_const + info • Unnecessary 'const' keyword • lib/widgets/color_picker_dialog.dart:94:13 • unnecessary_const + info • Unnecessary 'const' keyword • lib/widgets/color_picker_dialog.dart:99:13 • unnecessary_const + info • Unnecessary 'const' keyword • lib/widgets/color_picker_dialog.dart:109:13 • unnecessary_const + info • Use 'const' with the constructor to improve performance • lib/widgets/color_picker_dialog.dart:117:18 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/color_picker_dialog.dart:128:18 • prefer_const_constructors + info • 'red' is deprecated and shouldn't be used. Use (*.r * 255.0).round() & 0xff • lib/widgets/color_picker_dialog.dart:139:26 • deprecated_member_use + info • 'green' is deprecated and shouldn't be used. Use (*.g * 255.0).round() & 0xff • lib/widgets/color_picker_dialog.dart:145:26 • deprecated_member_use + info • 'blue' is deprecated and shouldn't be used. Use (*.b * 255.0).round() & 0xff • lib/widgets/color_picker_dialog.dart:151:26 • deprecated_member_use + error • Arguments of a constant creation must be constant expressions • lib/widgets/color_picker_dialog.dart:172:15 • const_with_non_constant_argument + info • Unnecessary 'const' keyword • lib/widgets/color_picker_dialog.dart:173:22 • unnecessary_const + error • Methods can't be invoked in constant expressions • lib/widgets/color_picker_dialog.dart:197:15 • const_eval_method_invocation + info • Unnecessary 'const' keyword • lib/widgets/color_picker_dialog.dart:198:22 • unnecessary_const + info • 'value' is deprecated and shouldn't be used. Use component accessors like .r or .g, or toARGB32 for an explicit conversion • lib/widgets/color_picker_dialog.dart:212:43 • deprecated_member_use + info • 'value' is deprecated and shouldn't be used. Use component accessors like .r or .g, or toARGB32 for an explicit conversion • lib/widgets/color_picker_dialog.dart:212:58 • deprecated_member_use + info • Use 'const' with the constructor to improve performance • lib/widgets/color_picker_dialog.dart:228:19 • prefer_const_constructors + info • 'value' is deprecated and shouldn't be used. Use component accessors like .r or .g, or toARGB32 for an explicit conversion • lib/widgets/color_picker_dialog.dart:244:17 • deprecated_member_use + info • 'red' is deprecated and shouldn't be used. Use (*.r * 255.0).round() & 0xff • lib/widgets/color_picker_dialog.dart:252:31 • deprecated_member_use + info • 'green' is deprecated and shouldn't be used. Use (*.g * 255.0).round() & 0xff • lib/widgets/color_picker_dialog.dart:253:33 • deprecated_member_use + info • 'blue' is deprecated and shouldn't be used. Use (*.b * 255.0).round() & 0xff • lib/widgets/color_picker_dialog.dart:254:32 • deprecated_member_use + info • 'value' is deprecated and shouldn't be used. Use component accessors like .r or .g, or toARGB32 for an explicit conversion • lib/widgets/color_picker_dialog.dart:257:26 • deprecated_member_use + error • Methods can't be invoked in constant expressions • lib/widgets/common/refreshable_list.dart:119:21 • const_eval_method_invocation + error • Methods can't be invoked in constant expressions • lib/widgets/common/refreshable_list.dart:231:21 • const_eval_method_invocation + info • Use 'const' with the constructor to improve performance • lib/widgets/common/refreshable_list.dart:363:27 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/common/refreshable_list.dart:366:29 • prefer_const_constructors + info • Don't use 'BuildContext's across async gaps • lib/widgets/common/right_click_copy.dart:31:49 • use_build_context_synchronously + info • Unnecessary 'const' keyword • lib/widgets/common/right_click_copy.dart:57:15 • unnecessary_const + info • Don't use 'BuildContext's across async gaps • lib/widgets/common/right_click_copy.dart:65:13 • use_build_context_synchronously + info • The 'child' argument should be last in widget constructor invocations • lib/widgets/common/right_click_copy.dart:74:39 • sort_child_properties_last + info • Use 'const' with the constructor to improve performance • lib/widgets/common/selectable_text_widgets.dart:52:22 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/common/selectable_text_widgets.dart:60:22 • prefer_const_constructors + info • Unnecessary 'const' keyword • lib/widgets/common/selectable_text_widgets.dart:136:15 • unnecessary_const + error • Arguments of a constant creation must be constant expressions • lib/widgets/currency_converter.dart:184:57 • const_with_non_constant_argument + info • Use 'const' with the constructor to improve performance • lib/widgets/custom_theme_editor.dart:67:21 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/custom_theme_editor.dart:72:19 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/custom_theme_editor.dart:181:9 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/custom_theme_editor.dart:183:18 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/custom_theme_editor.dart:189:9 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/custom_theme_editor.dart:191:18 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/custom_theme_editor.dart:306:18 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/custom_theme_editor.dart:307:21 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/custom_theme_editor.dart:311:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/custom_theme_editor.dart:312:18 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/custom_theme_editor.dart:313:21 • prefer_const_constructors + info • 'value' is deprecated and shouldn't be used. Use component accessors like .r or .g, or toARGB32 for an explicit conversion • lib/widgets/custom_theme_editor.dart:523:24 • deprecated_member_use + info • Use 'const' with the constructor to improve performance • lib/widgets/custom_theme_editor.dart:632:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/custom_theme_editor.dart:665:36 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/custom_theme_editor.dart:670:36 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/custom_theme_editor.dart:685:32 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/custom_theme_editor.dart:686:35 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/custom_theme_editor.dart:689:32 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/custom_theme_editor.dart:716:20 • prefer_const_constructors + info • Don't use 'BuildContext's across async gaps • lib/widgets/custom_theme_editor.dart:756:20 • use_build_context_synchronously + info • Don't use 'BuildContext's across async gaps • lib/widgets/custom_theme_editor.dart:758:28 • use_build_context_synchronously + info • Use 'const' with the constructor to improve performance • lib/widgets/data_source_info.dart:25:19 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/data_source_info.dart:27:28 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/data_source_info.dart:31:27 • prefer_const_constructors + error • The method 'acceptInvitation' isn't defined for the type 'InvitationService' • lib/widgets/dialogs/accept_invitation_dialog.dart:52:48 • undefined_method + error • The getter 'notifier' isn't defined for the type 'Provider' • lib/widgets/dialogs/accept_invitation_dialog.dart:59:39 • undefined_getter + info • Don't use 'BuildContext's across async gaps • lib/widgets/dialogs/accept_invitation_dialog.dart:63:11 • use_build_context_synchronously + info • Don't use 'BuildContext's across async gaps • lib/widgets/dialogs/accept_invitation_dialog.dart:68:22 • use_build_context_synchronously +warning • The value of the local variable 'currentUser' isn't used • lib/widgets/dialogs/accept_invitation_dialog.dart:92:11 • unused_local_variable + info • 'surfaceVariant' is deprecated and shouldn't be used. Use surfaceContainerHighest instead. This feature was deprecated after v3.18.0-0.1.pre • lib/widgets/dialogs/accept_invitation_dialog.dart:104:40 • deprecated_member_use + error • The getter 'description' isn't defined for the type 'Family' • lib/widgets/dialogs/accept_invitation_dialog.dart:141:42 • undefined_getter + error • The getter 'description' isn't defined for the type 'Family' • lib/widgets/dialogs/accept_invitation_dialog.dart:143:42 • undefined_getter + error • The getter 'memberCount' isn't defined for the type 'Family' • lib/widgets/dialogs/accept_invitation_dialog.dart:161:37 • undefined_getter + error • The getter 'folder_outline' isn't defined for the type 'Icons' • lib/widgets/dialogs/accept_invitation_dialog.dart:166:33 • undefined_getter + error • The getter 'categoryCount' isn't defined for the type 'Family' • lib/widgets/dialogs/accept_invitation_dialog.dart:167:37 • undefined_getter + error • The getter 'transactionCount' isn't defined for the type 'Family' • lib/widgets/dialogs/accept_invitation_dialog.dart:173:37 • undefined_getter +warning • The left operand can't be null, so the right operand is never executed • lib/widgets/dialogs/accept_invitation_dialog.dart:190:38 • dead_null_aware_expression + error • The getter 'warningContainer' isn't defined for the type 'ColorScheme' • lib/widgets/dialogs/accept_invitation_dialog.dart:262:44 • undefined_getter + info • Use 'const' with the constructor to improve performance • lib/widgets/dialogs/accept_invitation_dialog.dart:286:29 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/dialogs/accept_invitation_dialog.dart:290:31 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/dialogs/create_family_dialog.dart:144:19 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/dialogs/create_family_dialog.dart:146:28 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/dialogs/create_family_dialog.dart:169:37 • prefer_const_constructors + info • 'value' is deprecated and shouldn't be used. Use initialValue instead. This will set the initial value for the form field. This feature was deprecated after v3.33.0-1.0.pre • lib/widgets/dialogs/create_family_dialog.dart:188:23 • deprecated_member_use + info • 'value' is deprecated and shouldn't be used. Use initialValue instead. This will set the initial value for the form field. This feature was deprecated after v3.33.0-1.0.pre • lib/widgets/dialogs/create_family_dialog.dart:218:23 • deprecated_member_use + info • Use 'const' with the constructor to improve performance • lib/widgets/dialogs/create_family_dialog.dart:221:37 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/dialogs/create_family_dialog.dart:266:37 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/dialogs/create_family_dialog.dart:277:30 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/dialogs/create_family_dialog.dart:278:33 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/dialogs/create_family_dialog.dart:336:28 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/dialogs/create_family_dialog.dart:358:27 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/dialogs/delete_family_dialog.dart:47:16 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/dialogs/delete_family_dialog.dart:59:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/dialogs/delete_family_dialog.dart:66:20 • prefer_const_constructors + info • Uses 'await' on an instance of 'List', which is not a subtype of 'Future' • lib/widgets/dialogs/delete_family_dialog.dart:84:7 • await_only_futures +warning • The value of 'refresh' should be used • lib/widgets/dialogs/delete_family_dialog.dart:84:17 • unused_result +warning • The operand can't be 'null', so the condition is always 'true' • lib/widgets/dialogs/delete_family_dialog.dart:91:24 • unnecessary_null_comparison + info • Uses 'await' on an instance of 'Family', which is not a subtype of 'Future' • lib/widgets/dialogs/delete_family_dialog.dart:94:13 • await_only_futures +warning • The value of 'refresh' should be used • lib/widgets/dialogs/delete_family_dialog.dart:94:23 • unused_result + info • Don't use 'BuildContext's across async gaps • lib/widgets/dialogs/delete_family_dialog.dart:98:22 • use_build_context_synchronously + info • Don't use 'BuildContext's across async gaps • lib/widgets/dialogs/delete_family_dialog.dart:99:30 • use_build_context_synchronously + info • Don't use 'BuildContext's across async gaps • lib/widgets/dialogs/delete_family_dialog.dart:107:22 • use_build_context_synchronously + info • Use 'const' with the constructor to improve performance • lib/widgets/dialogs/delete_family_dialog.dart:132:11 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/dialogs/delete_family_dialog.dart:187:23 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/dialogs/delete_family_dialog.dart:198:18 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/dialogs/delete_family_dialog.dart:214:17 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/dialogs/invite_member_dialog.dart:147:23 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/dialogs/invite_member_dialog.dart:149:32 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/dialogs/invite_member_dialog.dart:186:45 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/dialogs/invite_member_dialog.dart:210:35 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/dialogs/invite_member_dialog.dart:254:41 • prefer_const_constructors + info • 'value' is deprecated and shouldn't be used. Use initialValue instead. This will set the initial value for the form field. This feature was deprecated after v3.33.0-1.0.pre • lib/widgets/dialogs/invite_member_dialog.dart:266:25 • deprecated_member_use + info • Use 'const' with the constructor to improve performance • lib/widgets/dialogs/invite_member_dialog.dart:269:39 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/dialogs/invite_member_dialog.dart:395:28 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/dialogs/invite_member_dialog.dart:409:27 • prefer_const_constructors + error • Arguments of a constant creation must be constant expressions • lib/widgets/dialogs/invite_member_dialog.dart:438:15 • const_with_non_constant_argument + info • Unnecessary 'const' keyword • lib/widgets/dialogs/invite_member_dialog.dart:439:22 • unnecessary_const + info • Unnecessary use of 'toList' in a spread • lib/widgets/dialogs/invite_member_dialog.dart:456:14 • unnecessary_to_list_in_spreads + info • Use 'const' with the constructor to improve performance • lib/widgets/family_switcher.dart:132:40 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/family_switcher.dart:134:42 • prefer_const_constructors + info • Unnecessary use of 'toList' in a spread • lib/widgets/family_switcher.dart:191:12 • unnecessary_to_list_in_spreads + info • Use 'const' with the constructor to improve performance • lib/widgets/family_switcher.dart:209:26 • prefer_const_constructors + info • Unnecessary 'const' keyword • lib/widgets/family_switcher.dart:227:23 • unnecessary_const + info • Use 'const' with the constructor to improve performance • lib/widgets/family_switcher.dart:264:17 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/family_switcher.dart:336:11 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/family_switcher.dart:342:19 • prefer_const_constructors + info • Unnecessary braces in a string interpolation • lib/widgets/invite_member_dialog.dart:49:51 • unnecessary_brace_in_string_interps + info • Don't use 'BuildContext's across async gaps • lib/widgets/invite_member_dialog.dart:60:28 • use_build_context_synchronously + info • Use 'const' for final variables initialized to a constant value • lib/widgets/invite_member_dialog.dart:94:5 • prefer_const_declarations + info • Use 'const' for final variables initialized to a constant value • lib/widgets/invite_member_dialog.dart:95:5 • prefer_const_declarations + info • Unnecessary braces in a string interpolation • lib/widgets/invite_member_dialog.dart:102:1 • unnecessary_brace_in_string_interps + info • Unnecessary braces in a string interpolation • lib/widgets/invite_member_dialog.dart:102:23 • unnecessary_brace_in_string_interps + info • Unnecessary braces in a string interpolation • lib/widgets/invite_member_dialog.dart:104:9 • unnecessary_brace_in_string_interps + info • Unnecessary braces in a string interpolation • lib/widgets/invite_member_dialog.dart:105:8 • unnecessary_brace_in_string_interps + info • Unnecessary braces in a string interpolation • lib/widgets/invite_member_dialog.dart:106:9 • unnecessary_brace_in_string_interps + info • Unnecessary braces in a string interpolation • lib/widgets/invite_member_dialog.dart:111:13 • unnecessary_brace_in_string_interps + info • Unnecessary braces in a string interpolation • lib/widgets/invite_member_dialog.dart:122:13 • unnecessary_brace_in_string_interps + info • Use 'const' with the constructor to improve performance • lib/widgets/invite_member_dialog.dart:145:11 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/invite_member_dialog.dart:215:23 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/invite_member_dialog.dart:217:32 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/invite_member_dialog.dart:225:19 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/invite_member_dialog.dart:227:28 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/invite_member_dialog.dart:238:18 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/invite_member_dialog.dart:255:17 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/invite_member_dialog.dart:274:17 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/invite_member_dialog.dart:276:26 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/invite_member_dialog.dart:310:19 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/invite_member_dialog.dart:312:28 • prefer_const_constructors + error • The constructor being called isn't a const constructor • lib/widgets/invite_member_dialog.dart:346:26 • const_with_non_const + info • Unnecessary 'const' keyword • lib/widgets/invite_member_dialog.dart:353:32 • unnecessary_const + error • The constructor being called isn't a const constructor • lib/widgets/invite_member_dialog.dart:360:26 • const_with_non_const + info • Unnecessary 'const' keyword • lib/widgets/invite_member_dialog.dart:367:32 • unnecessary_const + error • Invalid constant value • lib/widgets/invite_member_dialog.dart:375:32 • invalid_constant + error • Invalid constant value • lib/widgets/invite_member_dialog.dart:423:17 • invalid_constant + info • Unnecessary 'const' keyword • lib/widgets/invite_member_dialog.dart:424:22 • unnecessary_const + error • The argument type 'Widget?' can't be assigned to the parameter type 'Widget'. • lib/widgets/permission_guard.dart:148:16 • argument_type_not_assignable +warning • The value of the local variable 'theme' isn't used • lib/widgets/permission_guard.dart:192:11 • unused_local_variable + error • The getter 'warningContainer' isn't defined for the type 'ColorScheme' • lib/widgets/permission_guard.dart:288:34 • undefined_getter + error • The getter 'onWarningContainer' isn't defined for the type 'ColorScheme' • lib/widgets/permission_guard.dart:291:36 • undefined_getter + error • The getter 'onWarningContainer' isn't defined for the type 'ColorScheme' • lib/widgets/permission_guard.dart:298:38 • undefined_getter + error • The getter 'onWarningContainer' isn't defined for the type 'ColorScheme' • lib/widgets/permission_guard.dart:307:42 • undefined_getter + info • The imported package 'qr_flutter' isn't a dependency of the importing package • lib/widgets/qr_code_generator.dart:3:8 • depend_on_referenced_packages + error • Target of URI doesn't exist: 'package:qr_flutter/qr_flutter.dart' • lib/widgets/qr_code_generator.dart:3:8 • uri_does_not_exist + info • The imported package 'share_plus' isn't a dependency of the importing package • lib/widgets/qr_code_generator.dart:4:8 • depend_on_referenced_packages + error • Target of URI doesn't exist: 'package:share_plus/share_plus.dart' • lib/widgets/qr_code_generator.dart:4:8 • uri_does_not_exist + info • Parameter 'key' could be a super parameter • lib/widgets/qr_code_generator.dart:23:9 • use_super_parameters + error • Undefined name 'Share' • lib/widgets/qr_code_generator.dart:91:13 • undefined_identifier + error • The method 'XFile' isn't defined for the type '_QrCodeGeneratorState' • lib/widgets/qr_code_generator.dart:92:10 • undefined_method + error • Invalid constant value • lib/widgets/qr_code_generator.dart:200:26 • invalid_constant + info • Unnecessary 'const' keyword • lib/widgets/qr_code_generator.dart:202:26 • unnecessary_const + error • The method 'QrImageView' isn't defined for the type '_QrCodeGeneratorState' • lib/widgets/qr_code_generator.dart:223:30 • undefined_method + error • Undefined name 'QrVersions' • lib/widgets/qr_code_generator.dart:225:34 • undefined_identifier + error • Undefined name 'QrErrorCorrectLevel' • lib/widgets/qr_code_generator.dart:229:47 • undefined_identifier + error • The name 'QrEmbeddedImageStyle' isn't a class • lib/widgets/qr_code_generator.dart:233:51 • creation_with_non_type + info • 'surfaceVariant' is deprecated and shouldn't be used. Use surfaceContainerHighest instead. This feature was deprecated after v3.18.0-0.1.pre • lib/widgets/qr_code_generator.dart:250:38 • deprecated_member_use + info • Use 'const' with the constructor to improve performance • lib/widgets/qr_code_generator.dart:264:23 • prefer_const_constructors + info • 'surfaceVariant' is deprecated and shouldn't be used. Use surfaceContainerHighest instead. This feature was deprecated after v3.18.0-0.1.pre • lib/widgets/qr_code_generator.dart:327:32 • deprecated_member_use + info • Parameter 'key' could be a super parameter • lib/widgets/qr_code_generator.dart:359:9 • use_super_parameters + info • Use 'const' with the constructor to improve performance • lib/widgets/qr_code_generator.dart:386:23 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/qr_code_generator.dart:441:27 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/qr_code_generator.dart:442:28 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/qr_code_generator.dart:456:27 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/qr_code_generator.dart:457:28 • prefer_const_constructors + error • Undefined name 'Share' • lib/widgets/qr_code_generator.dart:459:29 • undefined_identifier + info • Use 'const' with the constructor to improve performance • lib/widgets/sheets/generate_invite_code_sheet.dart:159:25 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/sheets/generate_invite_code_sheet.dart:177:31 • prefer_const_constructors + info • 'value' is deprecated and shouldn't be used. Use initialValue instead. This will set the initial value for the form field. This feature was deprecated after v3.33.0-1.0.pre • lib/widgets/sheets/generate_invite_code_sheet.dart:189:17 • deprecated_member_use + info • Use 'const' with the constructor to improve performance • lib/widgets/sheets/generate_invite_code_sheet.dart:192:31 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/sheets/generate_invite_code_sheet.dart:282:33 • prefer_const_constructors + error • The constructor being called isn't a const constructor • lib/widgets/sheets/generate_invite_code_sheet.dart:296:24 • const_with_non_const + info • Unnecessary 'const' keyword • lib/widgets/sheets/generate_invite_code_sheet.dart:299:25 • unnecessary_const + info • 'surfaceVariant' is deprecated and shouldn't be used. Use surfaceContainerHighest instead. This feature was deprecated after v3.18.0-0.1.pre • lib/widgets/sheets/generate_invite_code_sheet.dart:332:37 • deprecated_member_use + info • Use 'const' with the constructor to improve performance • lib/widgets/sheets/generate_invite_code_sheet.dart:370:20 • prefer_const_constructors + info • 'surfaceVariant' is deprecated and shouldn't be used. Use surfaceContainerHighest instead. This feature was deprecated after v3.18.0-0.1.pre • lib/widgets/sheets/generate_invite_code_sheet.dart:384:38 • deprecated_member_use + info • Use 'const' with the constructor to improve performance • lib/widgets/sheets/generate_invite_code_sheet.dart:392:19 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/sheets/generate_invite_code_sheet.dart:405:19 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/sheets/generate_invite_code_sheet.dart:416:19 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/sheets/generate_invite_code_sheet.dart:447:23 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/sheets/generate_invite_code_sheet.dart:470:23 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/sheets/generate_invite_code_sheet.dart:471:24 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/sheets/generate_invite_code_sheet.dart:478:23 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/sheets/generate_invite_code_sheet.dart:479:24 • prefer_const_constructors +warning • The value of the local variable 'cs' isn't used • lib/widgets/source_badge.dart:18:11 • unused_local_variable + info • Use 'const' with the constructor to improve performance • lib/widgets/states/error_state.dart:75:24 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/states/error_state.dart:278:18 • prefer_const_constructors + error • Invalid constant value • lib/widgets/states/loading_indicator.dart:30:22 • invalid_constant + error • Invalid constant value • lib/widgets/states/loading_indicator.dart:119:22 • invalid_constant +warning • The value of the field '_selectedGroupName' isn't used • lib/widgets/tag_create_dialog.dart:26:11 • unused_field + info • Use 'const' with the constructor to improve performance • lib/widgets/tag_create_dialog.dart:120:21 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/tag_create_dialog.dart:122:30 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/tag_create_dialog.dart:130:29 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/tag_create_dialog.dart:156:17 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/tag_create_dialog.dart:157:28 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/tag_create_dialog.dart:177:31 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/tag_create_dialog.dart:186:17 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/tag_create_dialog.dart:187:28 • prefer_const_constructors + info • Use a 'SizedBox' to add whitespace to a layout • lib/widgets/tag_create_dialog.dart:189:17 • sized_box_for_whitespace + info • Use 'const' with the constructor to improve performance • lib/widgets/tag_create_dialog.dart:211:36 • prefer_const_constructors + info • Unnecessary use of 'toList' in a spread • lib/widgets/tag_create_dialog.dart:236:28 • unnecessary_to_list_in_spreads + info • Use 'const' with the constructor to improve performance • lib/widgets/tag_create_dialog.dart:244:17 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/tag_create_dialog.dart:245:28 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/tag_create_dialog.dart:252:19 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/tag_create_dialog.dart:253:30 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/tag_create_dialog.dart:302:30 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/tag_create_dialog.dart:313:29 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/tag_create_dialog.dart:346:27 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/tag_create_dialog.dart:373:27 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/tag_deletion_dialog.dart:19:14 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/tag_deletion_dialog.dart:37:18 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/tag_deletion_dialog.dart:56:18 • prefer_const_constructors +warning • The value of the field '_selectedGroupName' isn't used • lib/widgets/tag_edit_dialog.dart:26:11 • unused_field + info • Use 'const' with the constructor to improve performance • lib/widgets/tag_edit_dialog.dart:119:21 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/tag_edit_dialog.dart:121:30 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/tag_edit_dialog.dart:129:29 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/tag_edit_dialog.dart:155:17 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/tag_edit_dialog.dart:156:28 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/tag_edit_dialog.dart:176:31 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/tag_edit_dialog.dart:185:17 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/tag_edit_dialog.dart:186:28 • prefer_const_constructors + info • Use a 'SizedBox' to add whitespace to a layout • lib/widgets/tag_edit_dialog.dart:188:17 • sized_box_for_whitespace + info • Use 'const' with the constructor to improve performance • lib/widgets/tag_edit_dialog.dart:210:36 • prefer_const_constructors + info • Unnecessary use of 'toList' in a spread • lib/widgets/tag_edit_dialog.dart:235:28 • unnecessary_to_list_in_spreads + info • Use 'const' with the constructor to improve performance • lib/widgets/tag_edit_dialog.dart:243:17 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/tag_edit_dialog.dart:244:28 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/tag_edit_dialog.dart:251:19 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/tag_edit_dialog.dart:252:30 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/tag_edit_dialog.dart:301:30 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/tag_edit_dialog.dart:312:29 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/tag_edit_dialog.dart:343:27 • prefer_const_constructors + info • Unnecessary braces in a string interpolation • lib/widgets/tag_edit_dialog.dart:522:41 • unnecessary_brace_in_string_interps + info • Use 'const' with the constructor to improve performance • lib/widgets/tag_group_dialog.dart:88:13 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/tag_group_dialog.dart:88:33 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/tag_group_dialog.dart:107:27 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/tag_group_dialog.dart:119:26 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/theme_appearance.dart:30:17 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/theme_appearance.dart:32:26 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/theme_appearance.dart:46:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/theme_appearance.dart:47:23 • prefer_const_constructors + info • 'activeColor' is deprecated and shouldn't be used. Use activeThumbColor instead. This feature was deprecated after v3.31.0-2.0.pre • lib/widgets/theme_appearance.dart:49:13 • deprecated_member_use + info • Use 'const' with the constructor to improve performance • lib/widgets/theme_appearance.dart:56:20 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/theme_appearance.dart:57:23 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/theme_preview_card.dart:80:36 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/theme_preview_card.dart:82:38 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/theme_preview_card.dart:455:26 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/theme_preview_card.dart:457:28 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/theme_share_dialog.dart:32:11 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/theme_share_dialog.dart:90:15 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/theme_share_dialog.dart:104:25 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/theme_share_dialog.dart:121:25 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/theme_share_dialog.dart:122:26 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/theme_share_dialog.dart:132:15 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/theme_share_dialog.dart:134:24 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/theme_share_dialog.dart:153:21 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/theme_share_dialog.dart:155:30 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/theme_share_dialog.dart:175:33 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/theme_share_dialog.dart:198:21 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/theme_share_dialog.dart:200:30 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/theme_share_dialog.dart:222:33 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/theme_share_dialog.dart:248:25 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/theme_share_dialog.dart:250:34 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/theme_share_dialog.dart:258:21 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/theme_share_dialog.dart:262:30 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/theme_share_dialog.dart:274:18 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/theme_share_dialog.dart:279:19 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/theme_share_dialog.dart:280:20 • prefer_const_constructors + info • Don't use 'BuildContext's across async gaps • lib/widgets/theme_share_dialog.dart:303:28 • use_build_context_synchronously + info • Don't use 'BuildContext's across async gaps • lib/widgets/theme_share_dialog.dart:314:28 • use_build_context_synchronously + info • Don't use 'BuildContext's across async gaps • lib/widgets/theme_share_dialog.dart:326:28 • use_build_context_synchronously + info • Don't use 'BuildContext's across async gaps • lib/widgets/theme_share_dialog.dart:333:28 • use_build_context_synchronously + info • Don't use 'BuildContext's across async gaps • lib/widgets/theme_share_dialog.dart:344:26 • use_build_context_synchronously + error • The constructor being called isn't a const constructor • lib/widgets/wechat_login_button.dart:84:14 • const_with_non_const + info • Unnecessary 'const' keyword • lib/widgets/wechat_login_button.dart:87:28 • unnecessary_const + info • Unnecessary 'const' keyword • lib/widgets/wechat_login_button.dart:88:17 • unnecessary_const + error • The constructor being called isn't a const constructor • lib/widgets/wechat_login_button.dart:90:27 • const_with_non_const + info • Unnecessary 'const' keyword • lib/widgets/wechat_login_button.dart:94:15 • unnecessary_const + info • Unnecessary 'const' keyword • lib/widgets/wechat_login_button.dart:105:18 • unnecessary_const + info • Use 'const' with the constructor to improve performance • lib/widgets/wechat_login_button.dart:138:13 • prefer_const_constructors + info • Use 'const' literals as arguments to constructors of '@immutable' classes • lib/widgets/wechat_login_button.dart:139:25 • prefer_const_literals_to_create_immutables + info • Use 'const' with the constructor to improve performance • lib/widgets/wechat_login_button.dart:140:17 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/wechat_login_button.dart:140:43 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/wechat_login_button.dart:142:17 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/wechat_login_button.dart:144:26 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/wechat_login_button.dart:162:27 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/wechat_login_button.dart:206:29 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/wechat_login_button.dart:213:15 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/wechat_login_button.dart:215:24 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/wechat_qr_binding_dialog.dart:93:29 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/wechat_qr_binding_dialog.dart:94:18 • prefer_const_constructors + info • Use 'const' literals as arguments to constructors of '@immutable' classes • lib/widgets/wechat_qr_binding_dialog.dart:96:21 • prefer_const_literals_to_create_immutables + info • Use 'const' with the constructor to improve performance • lib/widgets/wechat_qr_binding_dialog.dart:97:13 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/wechat_qr_binding_dialog.dart:103:13 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/wechat_qr_binding_dialog.dart:105:22 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/wechat_qr_binding_dialog.dart:111:13 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/wechat_qr_binding_dialog.dart:179:25 • prefer_const_constructors + info • Unnecessary braces in a string interpolation • lib/widgets/wechat_qr_binding_dialog.dart:212:21 • unnecessary_brace_in_string_interps + info • Use 'const' with the constructor to improve performance • lib/widgets/wechat_qr_binding_dialog.dart:230:23 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/wechat_qr_binding_dialog.dart:231:24 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/wechat_qr_binding_dialog.dart:234:25 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/wechat_qr_binding_dialog.dart:250:24 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/wechat_qr_binding_dialog.dart:271:23 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/wechat_qr_binding_dialog.dart:273:32 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/wechat_qr_binding_dialog.dart:281:19 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/wechat_qr_binding_dialog.dart:286:28 • prefer_const_constructors + info • Use 'const' with the constructor to improve performance • lib/widgets/wechat_qr_binding_dialog.dart:335:34 • prefer_const_constructors + info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • tag_demo.dart:94:28 • deprecated_member_use + info • The imported package 'riverpod' isn't a dependency of the importing package • test/currency_notifier_meta_test.dart:2:8 • depend_on_referenced_packages +warning • The declaration '_StubCatalogResult' isn't referenced • test/currency_notifier_meta_test.dart:10:7 • unused_element +warning • A value for optional parameter 'error' isn't ever given • test/currency_notifier_meta_test.dart:15:69 • unused_element_parameter + info • The import of 'dart:async' is unnecessary because all of the used elements are also provided by the import of 'package:flutter_test/flutter_test.dart' • test/currency_notifier_quiet_test.dart:1:8 • unnecessary_import + info • The import of 'dart:async' is unnecessary because all of the used elements are also provided by the import of 'package:flutter_test/flutter_test.dart' • test/currency_preferences_sync_test.dart:1:8 • unnecessary_import + info • The imported package 'riverpod' isn't a dependency of the importing package • test/currency_preferences_sync_test.dart:5:8 • depend_on_referenced_packages + info • 'overrideWithProvider' is deprecated and shouldn't be used. Will be removed in 3.0.0. Use overrideWith instead • test/currency_preferences_sync_test.dart:115:24 • deprecated_member_use + info • 'overrideWithProvider' is deprecated and shouldn't be used. Will be removed in 3.0.0. Use overrideWith instead • test/currency_preferences_sync_test.dart:143:24 • deprecated_member_use + info • 'overrideWithProvider' is deprecated and shouldn't be used. Will be removed in 3.0.0. Use overrideWith instead • test/currency_preferences_sync_test.dart:179:24 • deprecated_member_use + info • 'overrideWithProvider' is deprecated and shouldn't be used. Will be removed in 3.0.0. Use overrideWith instead • test/currency_selection_page_test.dart:86:39 • deprecated_member_use + info • 'overrideWithProvider' is deprecated and shouldn't be used. Will be removed in 3.0.0. Use overrideWith instead • test/currency_selection_page_test.dart:121:39 • deprecated_member_use + info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss • test_tag_functionality.dart:70:36 • deprecated_member_use + +2421 issues found. (ran in 20.9s) diff --git a/ci-artifacts-sqlx/rust-test-results/rust-test-results.txt b/ci-artifacts-sqlx/rust-test-results/rust-test-results.txt new file mode 100644 index 00000000..d28ef88b --- /dev/null +++ b/ci-artifacts-sqlx/rust-test-results/rust-test-results.txt @@ -0,0 +1,731 @@ + Updating crates.io index + Downloading crates ... + Downloaded async-stream-impl v0.3.6 + Downloaded blowfish v0.9.1 + Downloaded bytecheck v0.6.12 + Downloaded const-oid v0.9.6 + Downloaded futures-executor v0.3.31 + Downloaded event-listener v2.5.3 + Downloaded convert_case v0.6.0 + Downloaded axum v0.8.4 + Downloaded pin-project-internal v1.1.10 + Downloaded pkcs8 v0.10.2 + Downloaded ahash v0.7.8 + Downloaded dotenv v0.15.0 + Downloaded home v0.5.11 + Downloaded aho-corasick v1.1.3 + Downloaded tower-layer v0.3.3 + Downloaded tracing-attributes v0.1.30 + Downloaded tempfile v3.21.0 + Downloaded tracing-subscriber v0.3.19 + Downloaded sharded-slab v0.1.7 + Downloaded matchit v0.8.4 + Downloaded tracing-log v0.2.0 + Downloaded nu-ansi-term v0.46.0 + Downloaded unicode-bidi v0.3.18 + Downloaded utf-8 v0.7.6 + Downloaded yoke-derive v0.8.0 + Downloaded nom v7.1.3 + Downloaded indexmap v2.11.0 + Downloaded openssl-sys v0.9.109 + Downloaded pest_meta v2.8.1 + Downloaded pin-project v1.1.10 + Downloaded idna_adapter v1.2.1 + Downloaded icu_properties v2.0.1 + Downloaded icu_normalizer v2.0.0 + Downloaded indexmap v1.9.3 + Downloaded pest v2.8.1 + Downloaded icu_normalizer_data v2.0.0 + Downloaded hyper-util v0.1.16 + Downloaded libm v0.2.15 + Downloaded pin-utils v0.1.0 + Downloaded whoami v1.6.1 + Downloaded hyper-tls v0.6.0 + Downloaded icu_properties_data v2.0.1 + Downloaded hyper-rustls v0.27.7 + Downloaded httpdate v1.0.3 + Downloaded yoke v0.8.0 + Downloaded zerofrom v0.1.6 + Downloaded httparse v1.10.1 + Downloaded wyz v0.5.1 + Downloaded unicode-properties v0.1.3 + Downloaded writeable v0.6.1 + Downloaded urlencoding v2.1.3 + Downloaded proc-macro2 v1.0.101 + Downloaded powerfmt v0.2.0 + Downloaded pin-project-lite v0.2.16 + Downloaded proc-macro-crate v3.3.0 + Downloaded ppv-lite86 v0.2.21 + Downloaded linux-raw-sys v0.9.4 + Downloaded pkcs1 v0.7.5 + Downloaded openssl-macros v0.1.1 + Downloaded num-integer v0.1.46 + Downloaded jsonwebtoken v9.3.1 + Downloaded inout v0.1.4 + Downloaded icu_collections v2.0.0 + Downloaded num-iter v0.1.45 + Downloaded num-conv v0.1.0 + Downloaded matchers v0.1.0 + Downloaded lru-slab v0.1.2 + Downloaded litemap v0.8.0 + Downloaded rand v0.9.2 + Downloaded rand v0.8.5 + Downloaded once_cell v1.21.3 + Downloaded libsqlite3-sys v0.27.0 + Downloaded webpki-roots v0.25.4 + Downloaded url v2.5.7 + Downloaded webpki-roots v1.0.2 + Downloaded hyper v1.7.0 + Downloaded yaml-rust2 v0.8.1 + Downloaded unicode-segmentation v1.12.0 + Downloaded num-traits v0.2.19 + Downloaded log v0.4.27 + Downloaded winnow v0.7.13 + Downloaded zerocopy v0.8.26 + Downloaded iana-time-zone v0.1.63 + Downloaded unicode_categories v0.1.1 + Downloaded lazy_static v1.5.0 + Downloaded radium v0.7.0 + Downloaded quote v1.0.40 + Downloaded quinn v0.11.9 + Downloaded ptr_meta_derive v0.1.4 + Downloaded ptr_meta v0.1.4 + Downloaded icu_provider v2.0.0 + Downloaded pathdiff v0.2.3 + Downloaded overload v0.1.1 + Downloaded ordered-multimap v0.7.3 + Downloaded rand_core v0.9.3 + Downloaded rand_chacha v0.3.1 + Downloaded paste v1.0.15 + Downloaded password-hash v0.5.0 + Downloaded parking_lot_core v0.9.11 + Downloaded parking_lot v0.12.4 + Downloaded openssl-probe v0.1.6 + Downloaded rand_chacha v0.9.0 + Downloaded pest_generator v2.8.1 + Downloaded pest_derive v2.8.1 + Downloaded percent-encoding v2.3.2 + Downloaded memchr v2.7.5 + Downloaded libc v0.2.175 + Downloaded unicode-normalization v0.1.24 + Downloaded redis v0.27.6 + Downloaded md-5 v0.10.6 + Downloaded itertools v0.13.0 + Downloaded iri-string v0.7.8 + Downloaded idna v1.1.0 + Downloaded icu_locale_core v2.0.0 + Downloaded unicode-ident v1.0.18 + Downloaded num-bigint-dig v0.8.4 + Downloaded mio v1.0.4 + Downloaded minimal-lexical v0.2.1 + Downloaded vcpkg v0.2.15 + Downloaded openssl v0.10.73 + Downloaded lock_api v0.4.13 + Downloaded unicase v2.8.1 + Downloaded ucd-trie v0.1.7 + Downloaded num-bigint v0.4.6 + Downloaded matchit v0.7.3 + Downloaded itoa v1.0.15 + Downloaded ipnet v2.11.0 + Downloaded http v1.3.1 + Downloaded zerovec v0.11.4 + Downloaded zerotrie v0.2.2 + Downloaded zerofrom-derive v0.1.6 + Downloaded uuid v1.18.0 + Downloaded typenum v1.18.0 + Downloaded quinn-proto v0.11.13 + Downloaded syn v2.0.106 + Downloaded syn v1.0.109 + Downloaded serde_json v1.0.143 + Downloaded rustls-webpki v0.101.7 + Downloaded regex v1.11.2 + Downloaded encoding_rs v0.8.35 + Downloaded try-lock v0.2.5 + Downloaded native-tls v0.2.14 + Downloaded multer v3.1.0 + Downloaded mime_guess v2.0.5 + Downloaded utf8_iter v1.0.4 + Downloaded tungstenite v0.24.0 + Downloaded tracing v0.1.41 + Downloaded tower-http v0.6.6 + Downloaded tower-http v0.5.2 + Downloaded tower v0.4.13 + Downloaded tokio-util v0.7.16 + Downloaded time v0.3.42 + Downloaded sqlx-postgres v0.7.4 + Downloaded sqlx-core v0.7.4 + Downloaded sqlx v0.7.4 + Downloaded serde v1.0.219 + Downloaded rustls-webpki v0.103.4 + Downloaded headers v0.4.1 + Downloaded hdrhistogram v7.5.4 + Downloaded hashbrown v0.14.5 + Downloaded h2 v0.4.12 + Downloaded futures-intrusive v0.5.0 + Downloaded flume v0.11.1 + Downloaded chrono v0.4.41 + Downloaded bytes v1.10.1 + Downloaded mime v0.3.17 + Downloaded http-range-header v0.4.2 + Downloaded http-body v1.0.1 + Downloaded want v0.3.1 + Downloaded version_check v0.9.5 + Downloaded untrusted v0.9.0 + Downloaded rand_core v0.6.4 + Downloaded quinn-udp v0.5.14 + Downloaded potential_utf v0.1.3 + Downloaded pkg-config v0.3.32 + Downloaded pem-rfc7468 v0.7.0 + Downloaded pem v3.0.5 + Downloaded tracing-core v0.1.34 + Downloaded tower v0.5.2 + Downloaded toml_edit v0.22.27 + Downloaded toml v0.8.23 + Downloaded tokio-test v0.4.4 + Downloaded tinyvec v1.10.0 + Downloaded time-core v0.1.5 + Downloaded thiserror v1.0.69 + Downloaded sqlx-mysql v0.7.4 + Downloaded spin v0.9.8 + Downloaded socket2 v0.6.0 + Downloaded seahash v4.1.0 + Downloaded sct v0.7.1 + Downloaded scopeguard v1.2.0 + Downloaded ryu v1.0.20 + Downloaded rustversion v1.0.22 + Downloaded hkdf v0.12.4 + Downloaded hashbrown v0.15.5 + Downloaded hashbrown v0.12.3 + Downloaded futures-util v0.3.31 + Downloaded errno v0.3.13 + Downloaded dotenvy v0.15.7 + Downloaded dlv-list v0.5.2 + Downloaded displaydoc v0.2.5 + Downloaded der v0.7.10 + Downloaded config v0.14.1 + Downloaded combine v4.6.7 + Downloaded cfg-if v1.0.3 + Downloaded cc v1.2.34 + Downloaded tower-service v0.3.3 + Downloaded toml_write v0.1.2 + Downloaded toml_datetime v0.6.11 + Downloaded tokio-tungstenite v0.24.0 + Downloaded tokio-stream v0.1.17 + Downloaded tokio-native-tls v0.3.1 + Downloaded tokio-macros v2.5.0 + Downloaded tinyvec_macros v0.1.1 + Downloaded tinystr v0.8.1 + Downloaded time-macros v0.2.23 + Downloaded thread_local v1.1.9 + Downloaded thiserror-impl v2.0.16 + Downloaded thiserror v2.0.16 + Downloaded tap v1.0.1 + Downloaded synstructure v0.13.2 + Downloaded sync_wrapper v1.0.2 + Downloaded stringprep v0.1.5 + Downloaded stable_deref_trait v1.2.0 + Downloaded sqlx-macros-core v0.7.4 + Downloaded sqlx-macros v0.7.4 + Downloaded sqlformat v0.2.6 + Downloaded socket2 v0.5.10 + Downloaded slab v0.4.11 + Downloaded simple_asn1 v0.6.3 + Downloaded signature v2.2.0 + Downloaded rustls-pemfile v1.0.4 + Downloaded ring v0.17.14 + Downloaded hmac v0.12.1 + Downloaded hex v0.4.3 + Downloaded heck v0.4.1 + Downloaded headers-core v0.3.0 + Downloaded hashlink v0.8.4 + Downloaded getrandom v0.2.16 + Downloaded generic-array v0.14.7 + Downloaded futures-task v0.3.31 + Downloaded futures-io v0.3.31 + Downloaded futures-core v0.3.31 + Downloaded futures-channel v0.3.31 + Downloaded futures v0.3.31 + Downloaded funty v2.0.0 + Downloaded form_urlencoded v1.2.2 + Downloaded foreign-types-shared v0.1.1 + Downloaded foreign-types v0.3.2 + Downloaded fnv v1.0.7 + Downloaded fastrand v2.3.0 + Downloaded equivalent v1.0.2 + Downloaded either v1.15.0 + Downloaded deranged v0.5.3 + Downloaded crossbeam-queue v0.3.12 + Downloaded crc-catalog v2.4.0 + Downloaded crc v3.3.0 + Downloaded cpufeatures v0.2.17 + Downloaded const-random-macro v0.1.16 + Downloaded const-random v0.1.18 + Downloaded cipher v0.4.4 + Downloaded cfg_aliases v0.2.1 + Downloaded json5 v0.4.1 + Downloaded http-body-util v0.1.3 + Downloaded zerovec-derive v0.11.1 + Downloaded zeroize v1.8.1 + Downloaded tokio v1.47.1 + Downloaded tiny-keccak v2.0.2 + Downloaded subtle v2.6.1 + Downloaded sqlx-sqlite v0.7.4 + Downloaded smallvec v1.15.1 + Downloaded simdutf8 v0.1.5 + Downloaded rustls v0.23.31 + Downloaded rustls v0.21.12 + Downloaded rustix v1.0.8 + Downloaded regex-syntax v0.8.6 + Downloaded regex-automata v0.4.10 + Downloaded getrandom v0.3.3 + Downloaded futures-sink v0.3.31 + Downloaded futures-macro v0.3.31 + Downloaded crypto-common v0.1.6 + Downloaded crunchy v0.2.4 + Downloaded crossbeam-utils v0.8.21 + Downloaded borsh-derive v1.5.7 + Downloaded blake2 v0.10.6 + Downloaded bitvec v1.0.1 + Downloaded bitflags v2.9.3 + Downloaded base64 v0.22.1 + Downloaded axum-macros v0.4.2 + Downloaded axum-extra v0.10.1 + Downloaded axum v0.7.9 + Downloaded atomic-waker v1.1.2 + Downloaded arrayvec v0.7.6 + Downloaded arc-swap v1.7.1 + Downloaded allocator-api2 v0.2.21 + Downloaded serde_urlencoded v0.7.1 + Downloaded serde_spanned v0.6.9 + Downloaded rust_decimal v1.37.2 + Downloaded rkyv v0.7.45 + Downloaded regex-syntax v0.6.29 + Downloaded regex-automata v0.1.10 + Downloaded digest v0.10.7 + Downloaded data-encoding v2.9.0 + Downloaded byteorder v1.5.0 + Downloaded borsh v1.5.7 + Downloaded bcrypt v0.15.1 + Downloaded base64ct v1.8.0 + Downloaded backon v1.5.2 + Downloaded axum-core v0.5.2 + Downloaded async-trait v0.1.89 + Downloaded anyhow v1.0.99 + Downloaded signal-hook-registry v1.4.6 + Downloaded shlex v1.3.0 + Downloaded sha2 v0.10.9 + Downloaded sha1_smol v1.0.1 + Downloaded serde_derive v1.0.219 + Downloaded rustls-pki-types v1.12.0 + Downloaded rsa v0.9.8 + Downloaded ron v0.8.1 + Downloaded reqwest v0.12.23 + Downloaded bytecheck_derive v0.6.12 + Downloaded block-buffer v0.10.4 + Downloaded atoi v2.0.0 + Downloaded arraydeque v0.5.1 + Downloaded base64 v0.21.7 + Downloaded axum-core v0.4.5 + Downloaded async-stream v0.3.6 + Downloaded argon2 v0.5.3 + Downloaded tokio-rustls v0.26.2 + Downloaded sha1 v0.10.6 + Downloaded serde_path_to_error v0.1.17 + Downloaded rustc-hash v2.1.1 + Downloaded rust-ini v0.20.0 + Downloaded rkyv_derive v0.7.45 + Downloaded rend v0.4.2 + Downloaded ahash v0.8.12 + Downloaded thiserror-impl v1.0.69 + Downloaded spki v0.7.3 + Downloaded autocfg v1.5.0 + Compiling proc-macro2 v1.0.101 + Compiling unicode-ident v1.0.18 + Compiling libc v0.2.175 + Compiling cfg-if v1.0.3 + Compiling autocfg v1.5.0 + Compiling version_check v0.9.5 + Compiling serde v1.0.219 + Compiling typenum v1.18.0 + Compiling shlex v1.3.0 + Compiling parking_lot_core v0.9.11 + Compiling cc v1.2.34 + Compiling generic-array v0.14.7 + Compiling lock_api v0.4.13 + Compiling pin-project-lite v0.2.16 + Compiling bytes v1.10.1 + Compiling once_cell v1.21.3 + Compiling futures-core v0.3.31 + Compiling smallvec v1.15.1 + Compiling zerocopy v0.8.26 + Compiling itoa v1.0.15 + Compiling memchr v2.7.5 + Compiling scopeguard v1.2.0 + Compiling futures-sink v0.3.31 + Compiling log v0.4.27 + Compiling quote v1.0.40 + Compiling slab v0.4.11 + Compiling syn v2.0.106 + Compiling signal-hook-registry v1.4.6 + Compiling mio v1.0.4 + Compiling parking_lot v0.12.4 + Compiling socket2 v0.6.0 + Compiling getrandom v0.2.16 + Compiling getrandom v0.3.3 + Compiling futures-channel v0.3.31 + Compiling pin-utils v0.1.0 + Compiling subtle v2.6.1 + Compiling tracing-core v0.1.34 + Compiling icu_properties_data v2.0.1 + Compiling futures-task v0.3.31 + Compiling fnv v1.0.7 + Compiling icu_normalizer_data v2.0.0 + Compiling futures-io v0.3.31 + Compiling http v1.3.1 + Compiling crypto-common v0.1.6 + Compiling ahash v0.8.12 + Compiling stable_deref_trait v1.2.0 + Compiling block-buffer v0.10.4 + Compiling ring v0.17.14 + Compiling serde_json v1.0.143 + Compiling digest v0.10.7 + Compiling num-traits v0.2.19 + Compiling percent-encoding v2.3.2 + Compiling hashbrown v0.15.5 + Compiling equivalent v1.0.2 + Compiling thiserror v1.0.69 + Compiling http-body v1.0.1 + Compiling indexmap v2.11.0 + Compiling crossbeam-utils v0.8.21 + Compiling thiserror v2.0.16 + Compiling tower-service v0.3.3 + Compiling untrusted v0.9.0 + Compiling litemap v0.8.0 + Compiling writeable v0.6.1 + Compiling cpufeatures v0.2.17 + Compiling httparse v1.10.1 + Compiling rustls v0.21.12 + Compiling rust_decimal v1.37.2 + Compiling synstructure v0.13.2 + Compiling rand_core v0.6.4 + Compiling byteorder v1.5.0 + Compiling tower-layer v0.3.3 + Compiling mime v0.3.17 + Compiling vcpkg v0.2.15 + Compiling ryu v1.0.20 + Compiling pkg-config v0.3.32 + Compiling base64 v0.22.1 + Compiling openssl-sys v0.9.109 + Compiling httpdate v1.0.3 + Compiling paste v1.0.15 + Compiling allocator-api2 v0.2.21 + Compiling crunchy v0.2.4 + Compiling ppv-lite86 v0.2.21 + Compiling http-body-util v0.1.3 + Compiling form_urlencoded v1.2.2 + Compiling hashbrown v0.14.5 + Compiling sync_wrapper v1.0.2 + Compiling tiny-keccak v2.0.2 + Compiling zeroize v1.8.1 + Compiling serde_derive v1.0.219 + Compiling zerofrom-derive v0.1.6 + Compiling yoke-derive v0.8.0 + Compiling zerovec-derive v0.11.1 + Compiling displaydoc v0.2.5 + Compiling tokio-macros v2.5.0 + Compiling tracing-attributes v0.1.30 + Compiling tokio v1.47.1 + Compiling futures-macro v0.3.31 + Compiling thiserror-impl v1.0.69 + Compiling zerofrom v0.1.6 + Compiling yoke v0.8.0 + Compiling zerovec v0.11.4 + Compiling futures-util v0.3.31 + Compiling tracing v0.1.41 + Compiling zerotrie v0.2.2 + Compiling tinystr v0.8.1 + Compiling potential_utf v0.1.3 + Compiling icu_locale_core v2.0.0 + Compiling thiserror-impl v2.0.16 + Compiling icu_collections v2.0.0 + Compiling icu_provider v2.0.0 + Compiling icu_normalizer v2.0.0 + Compiling icu_properties v2.0.1 + Compiling tokio-util v0.7.16 + Compiling bitflags v2.9.3 + Compiling idna_adapter v1.2.1 + Compiling rand_chacha v0.3.1 + Compiling utf8_iter v1.0.4 + Compiling minimal-lexical v0.2.1 + Compiling atomic-waker v1.1.2 + Compiling rustversion v1.0.22 + Compiling try-lock v0.2.5 + Compiling tinyvec_macros v0.1.1 + Compiling idna v1.1.0 + Compiling tinyvec v1.10.0 + Compiling want v0.3.1 + Compiling h2 v0.4.12 + Compiling nom v7.1.3 + Compiling sct v0.7.1 + Compiling rustls-webpki v0.101.7 + Compiling rand v0.8.5 + Compiling rustls-pki-types v1.12.0 + Compiling openssl v0.10.73 + Compiling ucd-trie v0.1.7 + Compiling unicode_categories v0.1.1 + Compiling arrayvec v0.7.6 + Compiling crc-catalog v2.4.0 + Compiling base64 v0.21.7 + Compiling rustix v1.0.8 + Compiling iana-time-zone v0.1.63 + Compiling foreign-types-shared v0.1.1 + Compiling foreign-types v0.3.2 + Compiling chrono v0.4.41 + Compiling rustls-pemfile v1.0.4 + Compiling tokio-stream v0.1.17 + Compiling hyper v1.7.0 + Compiling crc v3.3.0 + Compiling sha2 v0.10.9 + Compiling sqlformat v0.2.6 + Compiling pest v2.8.1 + Compiling futures-intrusive v0.5.0 + Compiling crossbeam-queue v0.3.12 + Compiling const-random-macro v0.1.16 + Compiling url v2.5.7 + Compiling hashlink v0.8.4 + Compiling unicode-normalization v0.1.24 + Compiling either v1.15.0 + Compiling tower v0.5.2 + Compiling atoi v2.0.0 + Compiling openssl-macros v0.1.1 + Compiling hmac v0.12.1 + Compiling sha1 v0.10.6 + Compiling encoding_rs v0.8.35 + Compiling rustls v0.23.31 + Compiling ipnet v2.11.0 + Compiling linux-raw-sys v0.9.4 + Compiling syn v1.0.109 + Compiling unicode-bidi v0.3.18 + Compiling native-tls v0.2.14 + Compiling unicode-properties v0.1.3 + Compiling webpki-roots v0.25.4 + Compiling hex v0.4.3 + Compiling event-listener v2.5.3 + Compiling uuid v1.18.0 + Compiling stringprep v0.1.5 + Compiling sqlx-core v0.7.4 + Compiling hyper-util v0.1.16 + Compiling hkdf v0.12.4 + Compiling const-random v0.1.18 + Compiling pest_meta v2.8.1 + Compiling rustls-webpki v0.103.4 + Compiling async-trait v0.1.89 + Compiling md-5 v0.10.6 + Compiling num-integer v0.1.46 + Compiling dotenvy v0.15.7 + Compiling openssl-probe v0.1.6 + Compiling home v0.5.11 + Compiling num-conv v0.1.0 + Compiling powerfmt v0.2.0 + Compiling unicode-segmentation v1.12.0 + Compiling unicase v2.8.1 + Compiling whoami v1.6.1 + Compiling time-core v0.1.5 + Compiling fastrand v2.3.0 + Compiling mime_guess v2.0.5 + Compiling time-macros v0.2.23 + Compiling tempfile v3.21.0 + Compiling sqlx-postgres v0.7.4 + Compiling heck v0.4.1 + Compiling deranged v0.5.3 + Compiling num-bigint v0.4.6 + Compiling pest_generator v2.8.1 + Compiling dlv-list v0.5.2 + Compiling serde_spanned v0.6.9 + Compiling toml_datetime v0.6.11 + Compiling inout v0.1.4 + Compiling indexmap v1.9.3 + Compiling multer v3.1.0 + Compiling utf-8 v0.7.6 + Compiling toml_write v0.1.2 + Compiling regex-syntax v0.8.6 + Compiling regex-syntax v0.6.29 + Compiling data-encoding v2.9.0 + Compiling winnow v0.7.13 + Compiling tungstenite v0.24.0 + Compiling toml_edit v0.22.27 + Compiling regex-automata v0.1.10 + Compiling regex-automata v0.4.10 + Compiling time v0.3.42 + Compiling sqlx-macros-core v0.7.4 + Compiling cipher v0.4.4 + Compiling tokio-rustls v0.26.2 + Compiling ordered-multimap v0.7.3 + Compiling pest_derive v2.8.1 + Compiling tokio-native-tls v0.3.1 + Compiling axum-core v0.5.2 + Compiling webpki-roots v1.0.2 + Compiling serde_urlencoded v0.7.1 + Compiling futures-executor v0.3.31 + Compiling pin-project-internal v1.1.10 + Compiling headers-core v0.3.0 + Compiling spin v0.9.8 + Compiling arraydeque v0.5.1 + Compiling iri-string v0.7.8 + Compiling base64ct v1.8.0 + Compiling overload v0.1.1 + Compiling lazy_static v1.5.0 + Compiling hashbrown v0.12.3 + Compiling anyhow v1.0.99 + Compiling matchit v0.8.4 + Compiling axum v0.8.4 + Compiling password-hash v0.5.0 + Compiling sharded-slab v0.1.7 + Compiling nu-ansi-term v0.46.0 + Compiling tower-http v0.6.6 + Compiling pin-project v1.1.10 + Compiling backon v1.5.2 + Compiling yaml-rust2 v0.8.1 + Compiling convert_case v0.6.0 + Compiling headers v0.4.1 + Compiling futures v0.3.31 + Compiling sqlx-macros v0.7.4 + Compiling hyper-rustls v0.27.7 + Compiling json5 v0.4.1 + Compiling hyper-tls v0.6.0 + Compiling rust-ini v0.20.0 + Compiling blowfish v0.9.1 + Compiling simple_asn1 v0.6.3 + Compiling regex v1.11.2 + Compiling toml v0.8.23 + Compiling matchers v0.1.0 + Compiling tokio-tungstenite v0.24.0 + Compiling axum-core v0.4.5 + Compiling itertools v0.13.0 + Compiling ron v0.8.1 + Compiling combine v4.6.7 + Compiling serde_path_to_error v0.1.17 + Compiling axum-macros v0.4.2 + Compiling async-stream-impl v0.3.6 + Compiling pem v3.0.5 + Compiling hdrhistogram v7.5.4 + Compiling blake2 v0.10.6 + Compiling tracing-log v0.2.0 + Compiling socket2 v0.5.10 + Compiling thread_local v1.1.9 + Compiling arc-swap v1.7.1 + Compiling matchit v0.7.3 + Compiling sha1_smol v1.0.1 + Compiling http-range-header v0.4.2 + Compiling pathdiff v0.2.3 + Compiling tracing-subscriber v0.3.19 + Compiling config v0.14.1 + Compiling tower-http v0.5.2 + Compiling argon2 v0.5.3 + Compiling axum v0.7.9 + Compiling async-stream v0.3.6 + Compiling tower v0.4.13 + Compiling jsonwebtoken v9.3.1 + Compiling redis v0.27.6 + Compiling bcrypt v0.15.1 + Compiling sqlx v0.7.4 + Compiling reqwest v0.12.23 + Compiling axum-extra v0.10.1 + Compiling dotenv v0.15.0 + Compiling tokio-test v0.4.4 + Compiling jive-money-api v1.0.0 (/home/runner/work/jive-flutter-rust/jive-flutter-rust/jive-api) +warning: unused variable: `e` + --> src/handlers/enhanced_profile.rs:161:19 + | +161 | .map_err(|e| ApiError::InternalServerError)?; + | ^ help: if this is intentional, prefix it with an underscore: `_e` + | + = note: `#[warn(unused_variables)]` on by default + +warning: value assigned to `bind_idx` is never read + --> src/handlers/enhanced_profile.rs:347:9 + | +347 | bind_idx += 1; + | ^^^^^^^^ + | + = help: maybe it is overwritten before being read? + = note: `#[warn(unused_assignments)]` on by default + +warning: unused variable: `pool` + --> src/handlers/currency_handler.rs:275:11 + | +275 | State(pool): State, + | ^^^^ help: if this is intentional, prefix it with an underscore: `_pool` + +warning: unused variable: `pool` + --> src/handlers/currency_handler_enhanced.rs:662:11 + | +662 | State(pool): State, + | ^^^^ help: if this is intentional, prefix it with an underscore: `_pool` + +error[E0308]: mismatched types + --> src/services/currency_service.rs:89:21 + | +89 | symbol: row.symbol, + | ^^^^^^^^^^ expected `String`, found `Option` + | + = note: expected struct `std::string::String` + found enum `std::option::Option` +help: consider using `Option::expect` to unwrap the `std::option::Option` value, panicking if the value is an `Option::None` + | +89 | symbol: row.symbol.expect("REASON"), + | +++++++++++++++++ + +error[E0308]: mismatched types + --> src/services/currency_service.rs:184:32 + | +184 | base_currency: settings.base_currency, + | ^^^^^^^^^^^^^^^^^^^^^^ expected `String`, found `Option` + | + = note: expected struct `std::string::String` + found enum `std::option::Option` +help: consider using `Option::expect` to unwrap the `std::option::Option` value, panicking if the value is an `Option::None` + | +184 | base_currency: settings.base_currency.expect("REASON"), + | +++++++++++++++++ + +warning: value assigned to `bind_idx` is never read + --> src/services/tag_service.rs:37:133 + | +37 | ...E ${}", bind_idx)); args.push((bind_idx, format!("%{}%", q))); bind_idx+=1; } + | ^^^^^^^^ + | + = help: maybe it is overwritten before being read? + +warning: unused import: `super::*` + --> src/services/currency_service.rs:582:9 + | +582 | use super::*; + | ^^^^^^^^ + | + = note: `#[warn(unused_imports)]` on by default + +warning: unused import: `rust_decimal::prelude::*` + --> src/services/currency_service.rs:583:9 + | +583 | use rust_decimal::prelude::*; + | ^^^^^^^^^^^^^^^^^^^^^^^^ + +warning: unused variable: `i` + --> src/services/avatar_service.rs:230:18 + | +230 | for (i, part) in parts.iter().take(2).enumerate() { + | ^ help: if this is intentional, prefix it with an underscore: `_i` + +warning: unused variable: `from_decimal_places` + --> src/services/currency_service.rs:386:9 + | +386 | from_decimal_places: i32, + | ^^^^^^^^^^^^^^^^^^^ help: if this is intentional, prefix it with an underscore: `_from_decimal_places` + +For more information about this error, try `rustc --explain E0308`. +warning: `jive-money-api` (lib) generated 7 warnings +error: could not compile `jive-money-api` (lib) due to 2 previous errors; 7 warnings emitted +warning: build failed, waiting for other jobs to finish... +warning: `jive-money-api` (lib test) generated 9 warnings (7 duplicates) +error: could not compile `jive-money-api` (lib test) due to 2 previous errors; 9 warnings emitted diff --git a/ci-artifacts-sqlx/schema-report/schema-report.md b/ci-artifacts-sqlx/schema-report/schema-report.md new file mode 100644 index 00000000..ac6bc80c --- /dev/null +++ b/ci-artifacts-sqlx/schema-report/schema-report.md @@ -0,0 +1,64 @@ +# Database Schema Report +## Schema Information +- Date: Tue Sep 23 09:26:04 UTC 2025 +- Database: PostgreSQL + +## Migrations +``` +total 140 +drwxr-xr-x 2 runner runner 4096 Sep 23 09:25 . +drwxr-xr-x 10 runner runner 4096 Sep 23 09:25 .. +-rw-r--r-- 1 runner runner 1650 Sep 23 09:25 001_create_templates_table.sql +-rw-r--r-- 1 runner runner 10314 Sep 23 09:25 002_create_all_tables.sql +-rw-r--r-- 1 runner runner 3233 Sep 23 09:25 003_insert_test_data.sql +-rw-r--r-- 1 runner runner 2081 Sep 23 09:25 004_fix_missing_columns.sql +-rw-r--r-- 1 runner runner 1843 Sep 23 09:25 005_create_superadmin.sql +-rw-r--r-- 1 runner runner 231 Sep 23 09:25 006_update_superadmin_password.sql +-rw-r--r-- 1 runner runner 6635 Sep 23 09:25 007_enhance_family_system.sql +-rw-r--r-- 1 runner runner 8298 Sep 23 09:25 008_migrate_existing_data.sql +-rw-r--r-- 1 runner runner 1132 Sep 23 09:25 009_create_superadmin_user.sql +-rw-r--r-- 1 runner runner 5493 Sep 23 09:25 010_fix_schema_for_api.sql +-rw-r--r-- 1 runner runner 6922 Sep 23 09:25 011_add_currency_exchange_tables.sql +-rw-r--r-- 1 runner runner 154 Sep 23 09:25 011_fix_password_hash_column.sql +-rw-r--r-- 1 runner runner 1789 Sep 23 09:25 012_fix_triggers_and_ledger_nullable.sql +-rw-r--r-- 1 runner runner 499 Sep 23 09:25 013_add_payee_id_to_transactions.sql +-rw-r--r-- 1 runner runner 444 Sep 23 09:25 014_add_recurring_and_denorm_names.sql +-rw-r--r-- 1 runner runner 366 Sep 23 09:25 015_add_full_name_to_users.sql +-rw-r--r-- 1 runner runner 2902 Sep 23 09:25 016_fix_families_member_count_and_superadmin.sql +-rw-r--r-- 1 runner runner 14781 Sep 23 09:25 017_seed_full_currency_catalog.sql +-rw-r--r-- 1 runner runner 762 Sep 23 09:25 018_add_username_to_users.sql +-rw-r--r-- 1 runner runner 2357 Sep 23 09:25 019_tags_tables.sql +-rw-r--r-- 1 runner runner 2556 Sep 23 09:25 020_adjust_templates_schema.sql +-rw-r--r-- 1 runner runner 1327 Sep 23 09:25 021_extend_categories_for_user_features.sql +-rw-r--r-- 1 runner runner 1289 Sep 23 09:25 022_backfill_categories.sql +``` +## Tables + List of relations + Schema | Name | Type | Owner +--------+-----------------------------+-------+---------- + public | account_balances | table | postgres + public | accounts | table | postgres + public | attachments | table | postgres + public | audit_logs | table | postgres + public | budgets | table | postgres + public | categories | table | postgres + public | crypto_prices | table | postgres + public | currencies | table | postgres + public | exchange_conversion_history | table | postgres + public | exchange_rate_cache | table | postgres + public | exchange_rates | table | postgres + public | families | table | postgres + public | family_audit_logs | table | postgres + public | family_currency_settings | table | postgres + public | family_members | table | postgres + public | invitations | table | postgres + public | ledgers | table | postgres + public | system_category_templates | table | postgres + public | tag_groups | table | postgres + public | tags | table | postgres + public | transactions | table | postgres + public | user_currency_preferences | table | postgres + public | user_currency_settings | table | postgres + public | users | table | postgres +(24 rows) + diff --git a/ci-artifacts-sqlx/test-report/test-report.md b/ci-artifacts-sqlx/test-report/test-report.md new file mode 100644 index 00000000..35b5d7a3 --- /dev/null +++ b/ci-artifacts-sqlx/test-report/test-report.md @@ -0,0 +1,118 @@ +# Flutter Test Report +## Test Summary +- Date: Tue Sep 23 09:27:55 UTC 2025 +- Flutter Version: 3.35.3 + +## Test Results +```json +Resolving dependencies... +Downloading packages... + _fe_analyzer_shared 67.0.0 (89.0.0 available) + analyzer 6.4.1 (8.2.0 available) + analyzer_plugin 0.11.3 (0.13.8 available) + build 2.4.1 (4.0.0 available) + build_config 1.1.2 (1.2.0 available) + build_resolvers 2.4.2 (3.0.4 available) + build_runner 2.4.13 (2.8.0 available) + build_runner_core 7.3.2 (9.3.2 available) + characters 1.4.0 (1.4.1 available) + custom_lint_core 0.6.3 (0.8.1 available) + dart_style 2.3.6 (3.1.2 available) + file_picker 8.3.7 (10.3.3 available) + fl_chart 0.66.2 (1.1.1 available) + flutter_launcher_icons 0.13.1 (0.14.4 available) + flutter_lints 3.0.2 (6.0.0 available) + flutter_riverpod 2.6.1 (3.0.0 available) + freezed 2.5.2 (3.2.3 available) + freezed_annotation 2.4.4 (3.1.0 available) + go_router 12.1.3 (16.2.2 available) + image_picker_android 0.8.13+2 (0.8.13+3 available) +! intl 0.19.0 (overridden) (0.20.2 available) + json_serializable 6.8.0 (6.11.1 available) + lints 3.0.0 (6.0.0 available) + material_color_utilities 0.11.1 (0.13.0 available) + meta 1.16.0 (1.17.0 available) + pool 1.5.1 (1.5.2 available) + protobuf 3.1.0 (4.2.0 available) + retrofit_generator 8.2.1 (10.0.5 available) + riverpod 2.6.1 (3.0.0 available) + riverpod_analyzer_utils 0.5.1 (0.5.10 available) + riverpod_annotation 2.6.1 (3.0.0 available) + riverpod_generator 2.4.0 (3.0.0 available) + shared_preferences_android 2.4.12 (2.4.13 available) + shelf_web_socket 2.0.1 (3.0.0 available) + source_gen 1.5.0 (4.0.1 available) + source_helper 1.3.5 (1.3.8 available) + test_api 0.7.6 (0.7.7 available) + very_good_analysis 5.1.0 (10.0.0 available) +Got dependencies! +38 packages have newer versions incompatible with dependency constraints. +Try `flutter pub outdated` for more information. +{"protocolVersion":"0.1.1","runnerVersion":null,"pid":2698,"type":"start","time":0} +{"suite":{"id":0,"platform":"vm","path":"/home/runner/work/jive-flutter-rust/jive-flutter-rust/jive-flutter/test/currency_preferences_sync_test.dart"},"type":"suite","time":0} +{"test":{"id":1,"name":"loading /home/runner/work/jive-flutter-rust/jive-flutter-rust/jive-flutter/test/currency_preferences_sync_test.dart","suiteID":0,"groupIDs":[],"metadata":{"skip":false,"skipReason":null},"line":null,"column":null,"url":null},"type":"testStart","time":1} +{"suite":{"id":2,"platform":"vm","path":"/home/runner/work/jive-flutter-rust/jive-flutter-rust/jive-flutter/test/currency_notifier_meta_test.dart"},"type":"suite","time":5} +{"test":{"id":3,"name":"loading /home/runner/work/jive-flutter-rust/jive-flutter-rust/jive-flutter/test/currency_notifier_meta_test.dart","suiteID":2,"groupIDs":[],"metadata":{"skip":false,"skipReason":null},"line":null,"column":null,"url":null},"type":"testStart","time":5} +{"count":5,"time":6,"type":"allSuites"} + +[{"event":"test.startedProcess","params":{"vmServiceUri":"http://127.0.0.1:39293/eTZiaHcrGWs=/"}}] + +[{"event":"test.startedProcess","params":{"vmServiceUri":"http://127.0.0.1:36355/nOrTWyOOTIM=/"}}] +{"testID":3,"result":"success","skipped":false,"hidden":true,"type":"testDone","time":8712} +{"group":{"id":4,"suiteID":2,"parentID":null,"name":"","metadata":{"skip":false,"skipReason":null},"testCount":1,"line":null,"column":null,"url":null},"type":"group","time":8715} +{"test":{"id":5,"name":"(setUpAll)","suiteID":2,"groupIDs":[4],"metadata":{"skip":false,"skipReason":null},"line":22,"column":3,"url":"file:///home/runner/work/jive-flutter-rust/jive-flutter-rust/jive-flutter/test/currency_notifier_meta_test.dart"},"type":"testStart","time":8715} +{"testID":1,"result":"success","skipped":false,"hidden":true,"type":"testDone","time":8732} +{"group":{"id":6,"suiteID":0,"parentID":null,"name":"","metadata":{"skip":false,"skipReason":null},"testCount":3,"line":null,"column":null,"url":null},"type":"group","time":8732} +{"test":{"id":7,"name":"(setUpAll)","suiteID":0,"groupIDs":[6],"metadata":{"skip":false,"skipReason":null},"line":104,"column":3,"url":"file:///home/runner/work/jive-flutter-rust/jive-flutter-rust/jive-flutter/test/currency_preferences_sync_test.dart"},"type":"testStart","time":8732} +{"testID":7,"result":"success","skipped":false,"hidden":true,"type":"testDone","time":8795} +{"test":{"id":8,"name":"debounce combines rapid preference pushes and succeeds","suiteID":0,"groupIDs":[6],"metadata":{"skip":false,"skipReason":null},"line":112,"column":3,"url":"file:///home/runner/work/jive-flutter-rust/jive-flutter-rust/jive-flutter/test/currency_preferences_sync_test.dart"},"type":"testStart","time":8796} +{"testID":5,"result":"success","skipped":false,"hidden":true,"type":"testDone","time":8834} +{"group":{"id":9,"suiteID":2,"parentID":4,"name":"CurrencyNotifier catalog meta","metadata":{"skip":false,"skipReason":null},"testCount":1,"line":29,"column":3,"url":"file:///home/runner/work/jive-flutter-rust/jive-flutter-rust/jive-flutter/test/currency_notifier_meta_test.dart"},"type":"group","time":8834} +{"test":{"id":10,"name":"CurrencyNotifier catalog meta initial usingFallback true when first fetch throws","suiteID":2,"groupIDs":[4,9],"metadata":{"skip":false,"skipReason":null},"line":31,"column":5,"url":"file:///home/runner/work/jive-flutter-rust/jive-flutter-rust/jive-flutter/test/currency_notifier_meta_test.dart"},"type":"testStart","time":8835} +{"testID":8,"result":"success","skipped":false,"hidden":false,"type":"testDone","time":8841} +{"test":{"id":11,"name":"failure stores pending then flush success clears it","suiteID":0,"groupIDs":[6],"metadata":{"skip":false,"skipReason":null},"line":139,"column":3,"url":"file:///home/runner/work/jive-flutter-rust/jive-flutter-rust/jive-flutter/test/currency_preferences_sync_test.dart"},"type":"testStart","time":8841} +{"testID":10,"result":"success","skipped":false,"hidden":false,"type":"testDone","time":8896} +{"test":{"id":12,"name":"(tearDownAll)","suiteID":2,"groupIDs":[4],"metadata":{"skip":false,"skipReason":null},"line":null,"column":null,"url":null},"type":"testStart","time":8897} +{"testID":12,"result":"success","skipped":false,"hidden":true,"type":"testDone","time":8900} +{"testID":11,"messageType":"print","message":"Failed to push currency preferences (will persist pending): Exception: network","type":"print","time":9360} +{"testID":11,"result":"success","skipped":false,"hidden":false,"type":"testDone","time":9527} +{"test":{"id":13,"name":"startup flush clears preexisting pending","suiteID":0,"groupIDs":[6],"metadata":{"skip":false,"skipReason":null},"line":167,"column":3,"url":"file:///home/runner/work/jive-flutter-rust/jive-flutter-rust/jive-flutter/test/currency_preferences_sync_test.dart"},"type":"testStart","time":9527} +{"testID":13,"result":"success","skipped":false,"hidden":false,"type":"testDone","time":9533} +{"test":{"id":14,"name":"(tearDownAll)","suiteID":0,"groupIDs":[6],"metadata":{"skip":false,"skipReason":null},"line":null,"column":null,"url":null},"type":"testStart","time":9533} +{"testID":14,"result":"success","skipped":false,"hidden":true,"type":"testDone","time":9536} +{"suite":{"id":15,"platform":"vm","path":"/home/runner/work/jive-flutter-rust/jive-flutter-rust/jive-flutter/test/widget_test.dart"},"type":"suite","time":9552} +{"test":{"id":16,"name":"loading /home/runner/work/jive-flutter-rust/jive-flutter-rust/jive-flutter/test/widget_test.dart","suiteID":15,"groupIDs":[],"metadata":{"skip":false,"skipReason":null},"line":null,"column":null,"url":null},"type":"testStart","time":9552} +{"suite":{"id":17,"platform":"vm","path":"/home/runner/work/jive-flutter-rust/jive-flutter-rust/jive-flutter/test/currency_selection_page_test.dart"},"type":"suite","time":10211} +{"test":{"id":18,"name":"loading /home/runner/work/jive-flutter-rust/jive-flutter-rust/jive-flutter/test/currency_selection_page_test.dart","suiteID":17,"groupIDs":[],"metadata":{"skip":false,"skipReason":null},"line":null,"column":null,"url":null},"type":"testStart","time":10211} +{"testID":16,"error":"Failed to load \"/home/runner/work/jive-flutter-rust/jive-flutter-rust/jive-flutter/test/widget_test.dart\":\nCompilation failed for testPath=/home/runner/work/jive-flutter-rust/jive-flutter-rust/jive-flutter/test/widget_test.dart: lib/screens/auth/login_screen.dart:442:36: Error: Not a constant expression.\n onPressed: _isLoading ? null : _login,\n ^^^^^^^^^^\nlib/screens/auth/login_screen.dart:442:56: Error: Not a constant expression.\n onPressed: _isLoading ? null : _login,\n ^^^^^^\nlib/screens/auth/login_screen.dart:443:47: Error: Method invocation is not a constant expression.\n style: ElevatedButton.styleFrom(\n ^^^^^^^^^\nlib/screens/auth/login_screen.dart:447:32: Error: Not a constant expression.\n child: _isLoading\n ^^^^^^^^^^\nlib/screens/auth/register_screen.dart:332:36: Error: Not a constant expression.\n onPressed: _isLoading ? null : _register,\n ^^^^^^^^^^\nlib/screens/auth/register_screen.dart:332:56: Error: Not a constant expression.\n onPressed: _isLoading ? null : _register,\n ^^^^^^^^^\nlib/screens/auth/register_screen.dart:333:47: Error: Method invocation is not a constant expression.\n style: ElevatedButton.styleFrom(\n ^^^^^^^^^\nlib/screens/auth/register_screen.dart:337:32: Error: Not a constant expression.\n child: _isLoading\n ^^^^^^^^^^\nlib/screens/dashboard/dashboard_screen.dart:337:31: Error: Not a constant expression.\n Navigator.pop(context);\n ^^^^^^^\nlib/screens/dashboard/dashboard_screen.dart:337:27: Error: Method invocation is not a constant expression.\n Navigator.pop(context);\n ^^^\nlib/screens/dashboard/dashboard_screen.dart:336:26: Error: Not a constant expression.\n onPressed: () {\n ^^\nlib/screens/dashboard/dashboard_screen.dart:335:35: Error: Cannot invoke a non-'const' factory where a const expression is expected.\nTry using a constructor or factory that is 'const'.\n child: OutlinedButton.icon(\n ^^^^\nlib/screens/settings/profile_settings_screen.dart:1004:42: Error: Not a constant expression.\n onPressed: _resetAccount,\n ^^^^^^^^^^^^^\nlib/screens/settings/profile_settings_screen.dart:1005:53: Error: Method invocation is not a constant expression.\n style: ElevatedButton.styleFrom(\n ^^^^^^^^^\nlib/screens/settings/profile_settings_screen.dart:1072:44: Error: Not a constant expression.\n context: context,\n ^^^^^^^\nlib/screens/settings/profile_settings_screen.dart:1080:72: Error: Not a constant expression.\n onPressed: () => Navigator.pop(context),\n ^^^^^^^\nlib/screens/settings/profile_settings_screen.dart:1080:68: Error: Method invocation is not a constant expression.\n onPressed: () => Navigator.pop(context),\n ^^^\nlib/screens/settings/profile_settings_screen.dart:1080:52: Error: Not a constant expression.\n onPressed: () => Navigator.pop(context),\n ^^\nlib/screens/settings/profile_settings_screen.dart:1085:57: Error: Not a constant expression.\n Navigator.pop(context);\n ^^^^^^^\nlib/screens/settings/profile_settings_screen.dart:1085:53: Error: Method invocation is not a constant expression.\n Navigator.pop(context);\n ^^^\nlib/screens/settings/profile_settings_screen.dart:1086:43: Error: Not a constant expression.\n _deleteAccount();\n ^^^^^^^^^^^^^^\nlib/screens/settings/profile_settings_screen.dart:1084:52: Error: Not a constant expression.\n onPressed: () {\n ^^\nlib/screens/settings/profile_settings_screen.dart:1073:44: Error: Not a constant expression.\n builder: (context) => AlertDialog(\n ^^^^^^^^^\nlib/screens/settings/profile_settings_screen.dart:1071:33: Error: Method invocation is not a constant expression.\n showDialog(\n ^^^^^^^^^^\nlib/screens/settings/profile_settings_screen.dart:1070:42: Error: Not a constant expression.\n onPressed: () {\n ^^\nlib/screens/settings/profile_settings_screen.dart:1097:53: Error: Method invocation is not a constant expression.\n style: ElevatedButton.styleFrom(\n ^^^^^^^^^\nlib/screens/management/currency_management_page_v2.dart:344:44: Error: Not a constant expression.\n Expanded(child: Text(d.code)),\n ^\nlib/screens/management/currency_management_page_v2.dart:348:46: Error: Not a constant expression.\n value: selectedMap[d.code],\n ^\nlib/screens/management/currency_management_page_v2.dart:348:34: Error: Not a constant expression.\n value: selectedMap[d.code],\n ^^^^^^^^^^^\nlib/screens/management/currency_management_page_v2.dart:351:42: Error: Not a constant expression.\n value: c.code,\n ^\nlib/screens/management/currency_management_page_v2.dart:352:50: Error: Not a constant expression.\n child: Text('${c.code} · ${c.nameZh}')))\n ^\nlib/screens/management/currency_management_page_v2.dart:352:62: Error: Not a constant expression.\n child: Text('${c.code} · ${c.nameZh}')))\n ^\nlib/screens/management/currency_management_page_v2.dart:350:36: Error: Not a constant expression.\n .map((c) => DropdownMenuItem(\n ^^^\nlib/screens/management/currency_management_page_v2.dart:349:34: Error: Not a constant expression.\n items: available\n ^^^^^^^^^\nlib/screens/management/currency_management_page_v2.dart:350:32: Error: Method invocation is not a constant expression.\n .map((c) => DropdownMenuItem(\n ^^^\nlib/screens/management/currency_management_page_v2.dart:353:32: Error: Method invocation is not a constant expression.\n .toList(),\n ^^^^^^\nlib/screens/management/currency_management_page_v2.dart:354:57: Error: Not a constant expression.\n onChanged: (v) => selectedMap[d.code] = v ?? d.code,\n ^\nlib/screens/management/currency_management_page_v2.dart:354:45: Error: Not a constant expression.\n onChanged: (v) => selectedMap[d.code] = v ?? d.code,\n ^^^^^^^^^^^\nlib/screens/management/currency_management_page_v2.dart:354:72: Error: Not a constant expression.\n onChanged: (v) => selectedMap[d.code] = v ?? d.code,\n ^\nlib/screens/management/currency_management_page_v2.dart:354:67: Error: Not a constant expression.\n onChanged: (v) => selectedMap[d.code] = v ?? d.code,\n ^\nlib/screens/management/currency_management_page_v2.dart:354:65: Error: Not a constant expression.\n onChanged: (v) => selectedMap[d.code] = v ?? d.code,\n ^\nlib/screens/management/currency_management_page_v2.dart:354:38: Error: Not a constant expression.\n onChanged: (v) => selectedMap[d.code] = v ?? d.code,\n ^^^\nlib/screens/management/currency_management_page_v2.dart:347:32: Error: Cannot invoke a non-'const' constructor where a const expression is expected.\nTry using a constructor or factory that is 'const'.\n child: DropdownButtonFormField(\n ^^^^^^^^^^^^^^^^^^^^^^^\nlib/screens/management/currency_management_page_v2.dart:339:40: Error: Not a constant expression.\n children: deprecated.map((d) {\n ^^^\nlib/screens/management/currency_management_page_v2.dart:339:25: Error: Not a constant expression.\n children: deprecated.map((d) {\n ^^^^^^^^^^\nlib/screens/management/currency_management_page_v2.dart:339:36: Error: Method invocation is not a constant expression.\n children: deprecated.map((d) {\n ^^^\nlib/screens/management/currency_management_page_v2.dart:362:18: Error: Method invocation is not a constant expression.\n }).toList(),\n ^^^^^^\nlib/screens/family/family_dashboard_screen.dart:330:53: Error: Not a constant expression.\n sections: _createPieChartSections(stats.accountTypeBreakdown),\n ^^^^^\nlib/screens/family/family_dashboard_screen.dart:330:29: Error: Not a constant expression.\n sections: _createPieChartSections(stats.accountTypeBreakdown),\n ^^^^^^^^^^^^^^^^^^^^^^^\nlib/screens/family/family_dashboard_screen.dart:329:17: Error: Cannot invoke a non-'const' constructor where a const expression is expected.\nTry using a constructor or factory that is 'const'.\n PieChartData(\n ^^^^^^^^^^^^\nlib/screens/family/family_dashboard_screen.dart:583:47: Error: Not a constant expression.\n getDrawingHorizontalLine: (value) {\n ^^^^^^^\nlib/screens/family/family_dashboard_screen.dart:605:31: Error: Not a constant expression.\n if (value.toInt() < months.length) {\n ^^^^^\nlib/screens/family/family_dashboard_screen.dart:605:37: Error: Method invocation is not a constant expression.\n if (value.toInt() < months.length) {\n ^^^^^\nlib/screens/family/family_dashboard_screen.dart:605:47: Error: Not a constant expression.\n if (value.toInt() < months.length) {\n ^^^^^^\nlib/screens/family/family_dashboard_screen.dart:607:38: Error: Not a constant expression.\n months[value.toInt()].substring(5),\n ^^^^^\nlib/screens/family/family_dashboard_screen.dart:607:44: Error: Method invocation is not a constant expression.\n months[value.toInt()].substring(5),\n ^^^^^\nlib/screens/family/family_dashboard_screen.dart:607:31: Error: Not a constant expression.\n months[value.toInt()].substring(5),\n ^^^^^^\nlib/screens/family/family_dashboard_screen.dart:607:53: Error: Method invocation is not a constant expression.\n months[value.toInt()].substring(5),\n ^^^^^^^^^\nlib/screens/family/family_dashboard_screen.dart:603:42: Error: Not a constant expression.\n getTitlesWidget: (value, meta) {\n ^^^^^^^^^^^^^\nlib/screens/family/family_dashboard_screen.dart:616:31: Error: Cannot invoke a non-'const' constructor where a const expression is expected.\nTry using a constructor or factory that is 'const'.\n borderData: FlBorderData(show: false),\n ^^^^^^^^^^^^\nlib/screens/family/family_dashboard_screen.dart:619:30: Error: Not a constant expression.\n spots: monthlyTrend.entries\n ^^^^^^^^^^^^\nlib/screens/family/family_dashboard_screen.dart:620:28: Error: Method invocation is not a constant expression.\n .toList()\n ^^^^^^\nlib/screens/family/family_dashboard_screen.dart:621:28: Error: Method invocation is not a constant expression.\n .asMap()\n ^^^^^\nlib/screens/family/family_dashboard_screen.dart:624:39: Error: Not a constant expression.\n return FlSpot(entry.key.toDouble(), entry.value.value);\n ^^^^^\nlib/screens/family/family_dashboard_screen.dart:624:49: Error: Method invocation is not a constant expression.\n return FlSpot(entry.key.toDouble(), entry.value.value);\n ^^^^^^^^\nlib/screens/family/family_dashboard_screen.dart:624:61: Error: Not a constant expression.\n return FlSpot(entry.key.toDouble(), entry.value.value);\n ^^^^^\nlib/screens/family/family_dashboard_screen.dart:624:67: Error: Not a constant expression.\n return FlSpot(entry.key.toDouble(), entry.value.value);\n ^^^^^\nlib/screens/family/family_dashboard_screen.dart:623:32: Error: Not a constant expression.\n .map((entry) {\n ^^^^^^^\nlib/screens/family/family_dashboard_screen.dart:623:28: Error: Method invocation is not a constant expression.\n .map((entry) {\n ^^^\nlib/screens/family/family_dashboard_screen.dart:625:26: Error: Method invocation is not a constant expression.\n }).toList(),\n ^^^^^^\nlib/screens/family/family_dashboard_screen.dart:627:39: Error: Not a constant expression.\n color: Theme.of(context).primaryColor,\n ^^^^^^^\nlib/screens/family/family_dashboard_screen.dart:627:36: Error: Method invocation is not a constant expression.\n color: Theme.of(context).primaryColor,\n ^^\nlib/screens/family/family_dashboard_screen.dart:632:41: Error: Not a constant expression.\n color: Theme.of(context).primaryColor.withValues(alpha: 0.1),\n ^^^^^^^\nlib/screens/family/family_dashboard_screen.dart:632:38: Error: Method invocation is not a constant expression.\n color: Theme.of(context).primaryColor.withValues(alpha: 0.1),\n ^^\nlib/screens/family/family_dashboard_screen.dart:632:63: Error: Method invocation is not a constant expression.\n color: Theme.of(context).primaryColor.withValues(alpha: 0.1),\n ^^^^^^^^^^\nlib/screens/family/family_dashboard_screen.dart:630:37: Error: Cannot invoke a non-'const' constructor where a const expression is expected.\nTry using a constructor or factory that is 'const'.\n belowBarData: BarAreaData(\n ^^^^^^^^^^^\nlib/screens/family/family_dashboard_screen.dart:618:21: Error: Cannot invoke a non-'const' constructor where a const expression is expected.\nTry using a constructor or factory that is 'const'.\n LineChartBarData(\n ^^^^^^^^^^^^^^^^\nlib/screens/family/family_dashboard_screen.dart:578:17: Error: Cannot invoke a non-'const' constructor where a const expression is expected.\nTry using a constructor or factory that is 'const'.\n LineChartData(\n ^^^^^^^^^^^^^\nlib/widgets/wechat_login_button.dart:85:20: Error: Not a constant expression.\n onPressed: _isLoading ? null : _handleWeChatLogin,\n ^^^^^^^^^^\nlib/widgets/wechat_login_button.dart:85:40: Error: Not a constant expression.\n onPressed: _isLoading ? null : _handleWeChatLogin,\n ^^^^^^^^^^^^^^^^^^\nlib/widgets/wechat_login_button.dart:90:40: Error: Cannot invoke a non-'const' constructor where a const expression is expected.\nTry using a constructor or factory that is 'const'.\n borderRadius: BorderRadius.circular(8),\n ^^^^^^^^\nlib/widgets/wechat_login_button.dart:86:31: Error: Method invocation is not a constant expression.\n style: OutlinedButton.styleFrom(\n ^^^^^^^^^\nlib/widgets/wechat_login_button.dart:93:15: Error: Not a constant expression.\n icon: _isLoading\n ^^^^^^^^^^\nlib/widgets/wechat_login_button.dart:104:11: Error: Not a constant expression.\n widget.buttonText,\n ^^^^^^\nlib/widgets/wechat_login_button.dart:84:29: Error: Cannot invoke a non-'const' factory where a const expression is expected.\nTry using a constructor or factory that is 'const'.\n child: OutlinedButton.icon(\n ^^^^\nlib/ui/components/dashboard/account_overview.dart:122:15: Error: Not a constant expression.\n assets,\n ^^^^^^\nlib/ui/components/dashboard/account_overview.dart:120:20: Error: Not a constant expression.\n child: _buildOverviewCard(\n ^^^^^^^^^^^^^^^^^^\nlib/ui/components/dashboard/account_overview.dart:131:15: Error: Not a constant expression.\n liabilities,\n ^^^^^^^^^^^\nlib/ui/components/dashboard/account_overview.dart:129:20: Error: Not a constant expression.\n child: _buildOverviewCard(\n ^^^^^^^^^^^^^^^^^^\nlib/ui/components/dashboard/account_overview.dart:141:15: Error: Not a constant expression.\n netWorth >= 0 ? Colors.blue : Colors.orange,\n ^^^^^^^^\nlib/ui/components/dashboard/account_overview.dart:140:15: Error: Not a constant expression.\n netWorth,\n ^^^^^^^^\nlib/ui/components/dashboard/account_overview.dart:138:20: Error: Not a constant expression.\n child: _buildOverviewCard(\n ^^^^^^^^^^^^^^^^^^\nlib/ui/components/dashboard/budget_summary.dart:181:32: Error: Not a constant expression.\n value: spentPercentage.clamp(0.0, 1.0),\n ^^^^^^^^^^^^^^^\nlib/ui/components/dashboard/budget_summary.dart:181:48: Error: Method invocation is not a constant expression.\n value: spentPercentage.clamp(0.0, 1.0),\n ^^^^^\nlib/ui/components/dashboard/budget_summary.dart:184:59: Error: Not a constant expression.\n AlwaysStoppedAnimation(warningLevel.color),\n ^^^^^^^^^^^^\nlib/widgets/dialogs/invite_member_dialog.dart:438:15: Error: Not a constant expression.\n permission,\n ^^^^^^^^^^\nlib/widgets/sheets/generate_invite_code_sheet.dart:297:30: Error: Not a constant expression.\n onPressed: _isLoading ? null : _generateInvitation,\n ^^^^^^^^^^\nlib/widgets/sheets/generate_invite_code_sheet.dart:297:50: Error: Not a constant expression.\n onPressed: _isLoading ? null : _generateInvitation,\n ^^^^^^^^^^^^^^^^^^^\nlib/widgets/sheets/generate_invite_code_sheet.dart:298:25: Error: Not a constant expression.\n icon: _isLoading\n ^^^^^^^^^^\nlib/widgets/sheets/generate_invite_code_sheet.dart:308:31: Error: Not a constant expression.\n label: Text(_isLoading ? '生成中...' : '生成邀请'),\n ^^^^^^^^^^\nlib/widgets/sheets/generate_invite_code_sheet.dart:296:37: Error: Cannot invoke a non-'const' factory where a const expression is expected.\nTry using a constructor or factory that is 'const'.\n child: FilledButton.icon(\n ^^^^\nlib/screens/auth/wechat_register_form_screen.dart:401:32: Error: Not a constant expression.\n onPressed: _isLoading ? null : _register,\n ^^^^^^^^^^\nlib/screens/auth/wechat_register_form_screen.dart:401:52: Error: Not a constant expression.\n onPressed: _isLoading ? null : _register,\n ^^^^^^^^^\nlib/screens/auth/wechat_register_form_screen.dart:402:43: Error: Method invocation is not a constant expression.\n style: ElevatedButton.styleFrom(\n ^^^^^^^^^\nlib/screens/auth/wechat_register_form_screen.dart:406:28: Error: Not a constant expression.\n child: _isLoading\n ^^^^^^^^^^\n.","stackTrace":"","isFailure":false,"type":"error","time":12358} +{"testID":16,"result":"error","skipped":false,"hidden":false,"type":"testDone","time":12361} +{"suite":{"id":19,"platform":"vm","path":"/home/runner/work/jive-flutter-rust/jive-flutter-rust/jive-flutter/test/currency_notifier_quiet_test.dart"},"type":"suite","time":12361} +{"test":{"id":20,"name":"loading /home/runner/work/jive-flutter-rust/jive-flutter-rust/jive-flutter/test/currency_notifier_quiet_test.dart","suiteID":19,"groupIDs":[],"metadata":{"skip":false,"skipReason":null},"line":null,"column":null,"url":null},"type":"testStart","time":12362} + +[{"event":"test.startedProcess","params":{"vmServiceUri":"http://127.0.0.1:36441/Z_Y8385vBiE=/"}}] +{"testID":18,"result":"success","skipped":false,"hidden":true,"type":"testDone","time":14067} +{"group":{"id":21,"suiteID":17,"parentID":null,"name":"","metadata":{"skip":false,"skipReason":null},"testCount":2,"line":null,"column":null,"url":null},"type":"group","time":14068} +{"test":{"id":22,"name":"(setUpAll)","suiteID":17,"groupIDs":[21],"metadata":{"skip":false,"skipReason":null},"line":78,"column":3,"url":"file:///home/runner/work/jive-flutter-rust/jive-flutter-rust/jive-flutter/test/currency_selection_page_test.dart"},"type":"testStart","time":14068} + +[{"event":"test.startedProcess","params":{"vmServiceUri":"http://127.0.0.1:42785/wwGOWAQ73TE=/"}}] +{"testID":22,"result":"success","skipped":false,"hidden":true,"type":"testDone","time":14132} +{"test":{"id":23,"name":"Selecting base currency returns via Navigator.pop","suiteID":17,"groupIDs":[21],"metadata":{"skip":false,"skipReason":null},"line":174,"column":5,"url":"package:flutter_test/src/widget_tester.dart","root_line":85,"root_column":3,"root_url":"file:///home/runner/work/jive-flutter-rust/jive-flutter-rust/jive-flutter/test/currency_selection_page_test.dart"},"type":"testStart","time":14132} +{"testID":20,"result":"success","skipped":false,"hidden":true,"type":"testDone","time":14300} +{"group":{"id":24,"suiteID":19,"parentID":null,"name":"","metadata":{"skip":false,"skipReason":null},"testCount":2,"line":null,"column":null,"url":null},"type":"group","time":14300} +{"test":{"id":25,"name":"(setUpAll)","suiteID":19,"groupIDs":[24],"metadata":{"skip":false,"skipReason":null},"line":66,"column":3,"url":"file:///home/runner/work/jive-flutter-rust/jive-flutter-rust/jive-flutter/test/currency_notifier_quiet_test.dart"},"type":"testStart","time":14300} +{"testID":25,"result":"success","skipped":false,"hidden":true,"type":"testDone","time":14354} +{"test":{"id":26,"name":"quiet mode: no calls before initialize; initialize triggers first load; explicit refresh triggers second","suiteID":19,"groupIDs":[24],"metadata":{"skip":false,"skipReason":null},"line":88,"column":3,"url":"file:///home/runner/work/jive-flutter-rust/jive-flutter-rust/jive-flutter/test/currency_notifier_quiet_test.dart"},"type":"testStart","time":14355} +{"testID":26,"result":"success","skipped":false,"hidden":false,"type":"testDone","time":14392} +{"test":{"id":27,"name":"initialize() is idempotent","suiteID":19,"groupIDs":[24],"metadata":{"skip":false,"skipReason":null},"line":104,"column":3,"url":"file:///home/runner/work/jive-flutter-rust/jive-flutter-rust/jive-flutter/test/currency_notifier_quiet_test.dart"},"type":"testStart","time":14392} +{"testID":27,"result":"success","skipped":false,"hidden":false,"type":"testDone","time":14418} +{"test":{"id":28,"name":"(tearDownAll)","suiteID":19,"groupIDs":[24],"metadata":{"skip":false,"skipReason":null},"line":null,"column":null,"url":null},"type":"testStart","time":14418} +{"testID":28,"result":"success","skipped":false,"hidden":true,"type":"testDone","time":14421} +{"testID":23,"result":"success","skipped":false,"hidden":false,"type":"testDone","time":15472} +{"test":{"id":29,"name":"Base currency is sorted to top and marked","suiteID":17,"groupIDs":[21],"metadata":{"skip":false,"skipReason":null},"line":174,"column":5,"url":"package:flutter_test/src/widget_tester.dart","root_line":120,"root_column":3,"root_url":"file:///home/runner/work/jive-flutter-rust/jive-flutter-rust/jive-flutter/test/currency_selection_page_test.dart"},"type":"testStart","time":15473} +{"testID":29,"result":"success","skipped":false,"hidden":false,"type":"testDone","time":15696} +{"test":{"id":30,"name":"(tearDownAll)","suiteID":17,"groupIDs":[21],"metadata":{"skip":false,"skipReason":null},"line":null,"column":null,"url":null},"type":"testStart","time":15696} +{"testID":30,"result":"success","skipped":false,"hidden":true,"type":"testDone","time":15700} +{"success":false,"type":"done","time":16083} +``` +## Coverage Summary +Coverage data generated successfully diff --git a/deny.toml b/deny.toml index 24f6fdf0..1bb83532 100644 --- a/deny.toml +++ b/deny.toml @@ -1,32 +1,135 @@ -############################ -# Minimal cargo-deny config -############################ +# cargo-deny configuration for Jive Money project +# This configuration helps ensure security, licensing compliance, and dependency management + +# The path where the deny.toml file is located relative to the workspace root +[graph] +# The file system path to the graph file to use +# targets = [] + +# Deny certain platforms from being used +[targets] +# The field that will be checked, this value must be one of +# - triple +# - arch +# - os +# - env +# +# cfg = "triple" +# The value to match +# value = "x86_64-unknown-linux-gnu" [advisories] -vulnerabilities = "deny" -unmaintained = "warn" -yanked = "deny" -notice = "warn" -ignore = [] +# The lint level for advisories that are for crates that are not direct dependencies +db-path = "~/.cargo/advisory-db" +db-urls = ["https://github.com/rustsec/advisory-db"] +# The lint level for crates that have a vulnerability +vulnerability = "deny" +# The lint level for crates that have been marked as unmaintained +unmaintained = "warn" +# The lint level for crates that have been yanked from crates.io +yanked = "deny" +# A list of advisory IDs to ignore +ignore = [ + # These are known issues that we've evaluated and determined acceptable + # Add RUSTSEC advisory IDs here if needed +] [licenses] -unlicensed = "deny" +# List of explicitly allowed licenses +# See https://spdx.org/licenses/ for list of valid identifiers allow = [ - "MIT", "Apache-2.0", "BSD-2-Clause", "BSD-3-Clause", "ISC", - "Unicode-DFS-2016", "MPL-2.0", "Zlib", "BSL-1.0", "CC0-1.0", "Unlicense", "OpenSSL" + "MIT", + "Apache-2.0", + "Apache-2.0 WITH LLVM-exception", + "BSD-2-Clause", + "BSD-3-Clause", + "ISC", + "Unicode-DFS-2016", + "OpenSSL", + "MPL-2.0", + "CC0-1.0", + "BSL-1.0", # Boost Software License + "Zlib", + "Unlicense", +] + +# List of explicitly disallowed licenses +deny = [ + "GPL-2.0", + "GPL-3.0", + "AGPL-3.0", + "LGPL-2.0", + "LGPL-2.1", + "LGPL-3.0", + "SSPL-1.0", # Server Side Public License (MongoDB) ] -copyleft = "warn" + +# Lint level for when multiple versions of the same license are detected +copyleft = "deny" +# Blanket approval or denial for OSI-approved or FSF-approved licenses +allow-osi-fsf-free = "both" +# Lint level used when no license is detected +default = "deny" +# The confidence threshold for detecting a license from a license text. +# Expressed as a floating point number between 0.0 and 1.0 confidence-threshold = 0.8 +# Allow certain licenses for specific crates only +[[licenses.exceptions]] +allow = ["ring", "webpki"] +name = "ISC" + [bans] +# Lint level for when multiple versions of the same crate are detected multiple-versions = "warn" +# Lint level for when a crate version requirement is `*` wildcards = "allow" -deny = [] -skip = [] -skip-tree = [] +# The graph highlighting used when creating dotgraphs for crates +highlight = "all" + +# List of crates to deny +deny = [ + # Deny old/insecure crypto libraries + { name = "openssl", version = "<0.10" }, + # Deny old/vulnerable versions of common crates + { name = "serde", version = "<1.0" }, + # Deny yanked crates + { name = "chrono", version = "=0.4.20" }, # Had a security issue +] + +# Certain crates/versions that will be skipped when doing duplicate detection. +skip = [ + # Skip certain crates that commonly have multiple versions + { name = "windows-sys" }, # Often multiple versions in dependency tree + { name = "syn", version = "1.0" }, # v1 and v2 coexist +] + +# Similarly to `skip` allows you to skip certain crates from being checked. Unlike `skip`, +# `skip-tree` skips the crate and all of its dependencies entirely. +skip-tree = [ + # Skip crates and their entire dependency trees +] [sources] +# Lint level for what to happen when a crate from a crate registry that is +# not in the allow list is encountered unknown-registry = "warn" -unknown-git = "warn" -allow-registry = ["https://github.com/rust-lang/crates.io-index"] -allow-git = [] +# Lint level for what to happen when a crate from a git repository that is not +# in the allow list is encountered +unknown-git = "warn" + +# List of allowed registries +allow-registry = [ + "https://github.com/rust-lang/crates.io-index", +] + +# List of allowed Git repositories +allow-git = [ + # Allow specific git dependencies if needed + # "https://github.com/organization/repository" +] + +# Configuration specific to the jive-api workspace +[[sources.allow-org]] +github = ["jive-org"] # Replace with actual GitHub organization +gitlab = ["jive-gitlab"] # Replace with actual GitLab organization if used \ No newline at end of file diff --git a/jive-api/Cargo.toml b/jive-api/Cargo.toml index df5e44d5..3c86e941 100644 --- a/jive-api/Cargo.toml +++ b/jive-api/Cargo.toml @@ -4,7 +4,6 @@ version = "1.0.0" edition = "2021" authors = ["Jive Money Team"] description = "Jive Money API Server for category template management" -build = "build.rs" [lib] name = "jive_money_api" @@ -45,7 +44,6 @@ base64 = "0.22" # Make core optional; gate usage behind feature `core_export` jive-core = { path = "../jive-core", package = "jive-core", features = ["server", "db"], default-features = false, optional = true } bytes = "1" -sha2 = "0.10" # WebSocket支持 tokio-tungstenite = "0.24" @@ -70,7 +68,6 @@ reqwest = { version = "0.12", features = ["json", "rustls-tls"] } # 静态变量 lazy_static = "1.4" -tokio-stream = "0.1.17" [features] default = ["demo_endpoints"] @@ -79,8 +76,6 @@ demo_endpoints = [] # Enable to use jive-core export service paths # When core_export is enabled, also enable jive-core's db feature so CSV helpers are available. core_export = ["dep:jive-core"] -# Stream CSV export incrementally instead of buffering whole response -export_stream = [] [dev-dependencies] tokio-test = "0.4" diff --git a/jive-api/README.md b/jive-api/README.md index 048c05d2..156ba858 100644 --- a/jive-api/README.md +++ b/jive-api/README.md @@ -211,35 +211,12 @@ GET /api/v1/transactions/statistics?ledger_id={ledger_id} - GET 流式导出(浏览器友好) - `GET /api/v1/transactions/export.csv` - - 支持同样的过滤参数,另支持 `include_header`(可选,默认 `true`)用于控制是否输出表头行 + - 支持同样的过滤参数 - 响应头: - `Content-Type: text/csv; charset=utf-8` - `Content-Disposition: attachment; filename="transactions_export_YYYYMMDDHHMMSS.csv"` - `X-Audit-Id: `(存在时) -#### 流式导出优化 (export_stream feature) - -对于大数据集导出,可启用 `export_stream` feature 以实现内存高效的流式处理: - -```bash -# 编译时启用流式导出 -cargo build --features export_stream - -# 或运行时启用 -cargo run --features export_stream --bin jive-api -``` - -**性能特点**: -- ✅ **内存效率高**: 使用 tokio channel 流式处理,避免一次性加载所有数据 -- ✅ **响应速度快**: 立即开始返回数据,无需等待全部查询完成 -- ✅ **适合大数据集**: 可处理超过内存容量的数据集 -- ✅ **实测性能**: 5k-20k 记录导出耗时仅 10-23ms - -**注意事项**: -- 流式导出使用 `query_raw` 避免反序列化开销 -- 需要 SQLx 在线模式编译(首次编译需数据库连接) -- 生产环境建议启用此 feature 以优化性能 - 审计日志 API: - 列表:`GET /api/v1/families/:id/audit-logs` @@ -266,9 +243,9 @@ cargo run --features export_stream --bin jive-api TOKEN="" API="http://localhost:8012/api/v1" -# 请求导出(可选 include_header=false 关闭表头) +# 请求导出 resp=$(curl -s -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ - -d '{"format":"csv","start_date":"2024-09-01","end_date":"2024-09-30","include_header":false}' \ + -d '{"format":"csv","start_date":"2024-09-01","end_date":"2024-09-30"}' \ "$API/transactions/export") audit_id=$(echo "$resp" | jq -r .audit_id) diff --git a/jive-api/src/handlers/audit_handler.rs b/jive-api/src/handlers/audit_handler.rs index e10387c8..15c39371 100644 --- a/jive-api/src/handlers/audit_handler.rs +++ b/jive-api/src/handlers/audit_handler.rs @@ -36,17 +36,14 @@ pub async fn get_audit_logs( if ctx.family_id != family_id { return Err(StatusCode::FORBIDDEN); } - + // Check permission - if ctx - .require_permission(crate::models::permission::Permission::ViewAuditLog) - .is_err() - { + if ctx.require_permission(crate::models::permission::Permission::ViewAuditLog).is_err() { return Err(StatusCode::FORBIDDEN); } - + let service = AuditService::new(pool.clone()); - + let filter = AuditLogFilter { family_id: Some(family_id), user_id: query.user_id, @@ -60,7 +57,7 @@ pub async fn get_audit_logs( limit: query.limit, offset: query.offset, }; - + match service.get_audit_logs(filter).await { Ok(logs) => Ok(Json(ApiResponse::success(logs))), Err(e) => { @@ -110,7 +107,7 @@ pub async fn cleanup_audit_logs( RETURNING 1 ) SELECT COUNT(*) FROM del - "#, + "# ) .bind(family_id) .bind(days) @@ -120,25 +117,23 @@ pub async fn cleanup_audit_logs( .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; // Log this cleanup operation into audit trail (best-effort) - let _ = AuditService::new(pool.clone()) - .log_action( - family_id, - ctx.user_id, - crate::models::audit::CreateAuditLogRequest { - action: crate::models::audit::AuditAction::Delete, - entity_type: "audit_logs".to_string(), - entity_id: None, - old_values: None, - new_values: Some(serde_json::json!({ - "older_than_days": days, - "limit": limit, - "deleted": deleted, - })), - }, - None, - None, - ) - .await; + let _ = AuditService::new(pool.clone()).log_action( + family_id, + ctx.user_id, + crate::models::audit::CreateAuditLogRequest { + action: crate::models::audit::AuditAction::Delete, + entity_type: "audit_logs".to_string(), + entity_id: None, + old_values: None, + new_values: Some(serde_json::json!({ + "older_than_days": days, + "limit": limit, + "deleted": deleted, + })), + }, + None, + None, + ).await; Ok(Json(ApiResponse::success(serde_json::json!({ "deleted": deleted, @@ -163,34 +158,29 @@ pub async fn export_audit_logs( if ctx.family_id != family_id { return Err(StatusCode::FORBIDDEN); } - + // Check permission - if ctx - .require_permission(crate::models::permission::Permission::ViewAuditLog) - .is_err() - { + if ctx.require_permission(crate::models::permission::Permission::ViewAuditLog).is_err() { return Err(StatusCode::FORBIDDEN); } - + let service = AuditService::new(pool.clone()); - - match service - .export_audit_report(family_id, query.from_date, query.to_date) - .await - { - Ok(csv) => Ok(Response::builder() - .status(StatusCode::OK) - .header(header::CONTENT_TYPE, "text/csv") - .header( - header::CONTENT_DISPOSITION, - format!( - "attachment; filename=\"audit_log_{}_{}.csv\"", - query.from_date.format("%Y%m%d"), - query.to_date.format("%Y%m%d") - ), - ) - .body(csv.into()) - .unwrap()), + + match service.export_audit_report(family_id, query.from_date, query.to_date).await { + Ok(csv) => { + Ok(Response::builder() + .status(StatusCode::OK) + .header(header::CONTENT_TYPE, "text/csv") + .header( + header::CONTENT_DISPOSITION, + format!("attachment; filename=\"audit_log_{}_{}.csv\"", + query.from_date.format("%Y%m%d"), + query.to_date.format("%Y%m%d") + ) + ) + .body(csv.into()) + .unwrap()) + }, Err(e) => { eprintln!("Error exporting audit logs: {:?}", e); Err(StatusCode::INTERNAL_SERVER_ERROR) diff --git a/jive-api/src/handlers/auth.rs b/jive-api/src/handlers/auth.rs index 6fdb1775..fac516b9 100644 --- a/jive-api/src/handlers/auth.rs +++ b/jive-api/src/handlers/auth.rs @@ -2,21 +2,26 @@ //! 认证相关API处理器 //! 提供用户注册、登录、令牌刷新等功能 -use argon2::{ - password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString}, - Argon2, +use axum::{ + extract::State, + http::StatusCode, + response::Json, + Extension, }; -use axum::{extract::State, http::StatusCode, response::Json, Extension}; -use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use serde_json::Value; use sqlx::PgPool; use uuid::Uuid; +use chrono::{DateTime, Utc}; +use argon2::{ + password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString}, + Argon2, +}; -use super::family_handler::{ApiError as FamilyApiError, ApiResponse}; use crate::auth::{Claims, LoginRequest, LoginResponse, RegisterRequest, RegisterResponse}; use crate::error::{ApiError, ApiResult}; use crate::services::AuthService; +use super::family_handler::{ApiResponse, ApiError as FamilyApiError}; /// 用户模型 #[derive(Debug, Serialize, Deserialize)] @@ -43,10 +48,7 @@ pub async fn register_with_family( let (final_email, username_opt) = if input.contains('@') { (input.clone(), None) } else { - ( - format!("{}@noemail.local", input.to_lowercase()), - Some(input.clone()), - ) + (format!("{}@noemail.local", input.to_lowercase()), Some(input.clone())) }; let auth_service = AuthService::new(pool.clone()); @@ -56,22 +58,21 @@ pub async fn register_with_family( name: Some(req.name.clone()), username: username_opt, }; - + match auth_service.register_with_family(register_req).await { Ok(user_ctx) => { // Generate JWT token let token = crate::auth::generate_jwt(user_ctx.user_id, user_ctx.current_family_id)?; - + Ok(Json(RegisterResponse { user_id: user_ctx.user_id, email: user_ctx.email, token, })) + }, + Err(e) => { + Err(ApiError::BadRequest(format!("Registration failed: {:?}", e))) } - Err(e) => Err(ApiError::BadRequest(format!( - "Registration failed: {:?}", - e - ))), } } @@ -85,36 +86,36 @@ pub async fn register( let (final_email, username_opt) = if input.contains('@') { (input.clone(), None) } else { - ( - format!("{}@noemail.local", input.to_lowercase()), - Some(input.clone()), - ) + (format!("{}@noemail.local", input.to_lowercase()), Some(input.clone())) }; - + // 检查邮箱是否已存在 - let existing = sqlx::query("SELECT id FROM users WHERE LOWER(email) = LOWER($1)") - .bind(&final_email) - .fetch_optional(&pool) - .await - .map_err(|e| ApiError::DatabaseError(e.to_string()))?; - + let existing = sqlx::query( + "SELECT id FROM users WHERE LOWER(email) = LOWER($1)" + ) + .bind(&final_email) + .fetch_optional(&pool) + .await + .map_err(|e| ApiError::DatabaseError(e.to_string()))?; + if existing.is_some() { return Err(ApiError::BadRequest("Email already registered".to_string())); } - + // 若为用户名注册,校验用户名唯一 if let Some(ref username) = username_opt { - let existing_username = - sqlx::query("SELECT id FROM users WHERE LOWER(username) = LOWER($1)") - .bind(username) - .fetch_optional(&pool) - .await - .map_err(|e| ApiError::DatabaseError(e.to_string()))?; + let existing_username = sqlx::query( + "SELECT id FROM users WHERE LOWER(username) = LOWER($1)" + ) + .bind(username) + .fetch_optional(&pool) + .await + .map_err(|e| ApiError::DatabaseError(e.to_string()))?; if existing_username.is_some() { return Err(ApiError::BadRequest("Username already taken".to_string())); } } - + // 生成密码哈希 let salt = SaltString::generate(&mut OsRng); let argon2 = Argon2::default(); @@ -122,30 +123,28 @@ pub async fn register( .hash_password(req.password.as_bytes(), &salt) .map_err(|_| ApiError::InternalServerError)? .to_string(); - + // 创建用户 let user_id = Uuid::new_v4(); let family_id = Uuid::new_v4(); // 为新用户创建默认家庭 - + // 开始事务 - let mut tx = pool - .begin() - .await + let mut tx = pool.begin().await .map_err(|e| ApiError::DatabaseError(e.to_string()))?; - + // 创建家庭 sqlx::query( r#" INSERT INTO families (id, name, created_at, updated_at) VALUES ($1, $2, NOW(), NOW()) - "#, + "# ) .bind(family_id) .bind(format!("{}'s Family", req.name)) .execute(&mut *tx) .await .map_err(|e| ApiError::DatabaseError(e.to_string()))?; - + // 创建用户(将 name 写入 name 与 full_name,便于后续使用) sqlx::query( r#" @@ -155,7 +154,7 @@ pub async fn register( ) VALUES ( $1, $2, $3, $4, $5, $6, 'active', false, NOW(), NOW() ) - "#, + "# ) .bind(user_id) .bind(&final_email) @@ -166,30 +165,29 @@ pub async fn register( .execute(&mut *tx) .await .map_err(|e| ApiError::DatabaseError(e.to_string()))?; - + // 创建默认账本 let ledger_id = Uuid::new_v4(); sqlx::query( r#" INSERT INTO ledgers (id, family_id, name, currency, created_at, updated_at) VALUES ($1, $2, '默认账本', 'CNY', NOW(), NOW()) - "#, + "# ) .bind(ledger_id) .bind(family_id) .execute(&mut *tx) .await .map_err(|e| ApiError::DatabaseError(e.to_string()))?; - + // 提交事务 - tx.commit() - .await + tx.commit().await .map_err(|e| ApiError::DatabaseError(e.to_string()))?; - + // 生成JWT令牌 let claims = Claims::new(user_id, final_email.clone(), Some(family_id)); let token = claims.to_token()?; - + Ok(Json(RegisterResponse { user_id, email: final_email, @@ -199,10 +197,9 @@ pub async fn register( /// 用户登录 pub async fn login( - State(state): State, + State(pool): State, Json(req): Json, ) -> ApiResult> { - let pool = &state.pool; // 允许在输入为“superadmin”时映射为统一邮箱(便于本地/测试环境) // 不影响密码校验,仅做标识规范化 let mut login_input = req.email.trim().to_string(); @@ -212,12 +209,6 @@ pub async fn login( // 查找用户 let query_by_email = login_input.contains('@'); - if cfg!(debug_assertions) { - println!( - "DEBUG[login]: query_by_email={}, input={}", - query_by_email, &login_input - ); - } let row = if query_by_email { sqlx::query( r#" @@ -226,10 +217,10 @@ pub async fn login( created_at, updated_at FROM users WHERE LOWER(email) = LOWER($1) - "#, + "# ) .bind(&login_input) - .fetch_optional(&state.pool) + .fetch_optional(&pool) .await .map_err(|e| ApiError::DatabaseError(e.to_string()))? } else { @@ -240,142 +231,80 @@ pub async fn login( created_at, updated_at FROM users WHERE LOWER(username) = LOWER($1) - "#, + "# ) .bind(&login_input) - .fetch_optional(&state.pool) + .fetch_optional(&pool) .await .map_err(|e| ApiError::DatabaseError(e.to_string()))? } - .ok_or_else(|| { - if cfg!(debug_assertions) { println!("DEBUG[login]: user not found for input={}", &login_input); } - state.metrics.increment_login_fail(); - ApiError::Unauthorized - })?; - + .ok_or(ApiError::Unauthorized)?; + use sqlx::Row; let user = User { - id: row - .try_get("id") - .map_err(|e| ApiError::DatabaseError(e.to_string()))?, - email: row - .try_get("email") - .map_err(|e| ApiError::DatabaseError(e.to_string()))?, + id: row.try_get("id").map_err(|e| ApiError::DatabaseError(e.to_string()))?, + email: row.try_get("email").map_err(|e| ApiError::DatabaseError(e.to_string()))?, name: row.try_get("name").unwrap_or_else(|_| "".to_string()), - password_hash: row - .try_get("password_hash") - .map_err(|e| ApiError::DatabaseError(e.to_string()))?, + password_hash: row.try_get("password_hash").map_err(|e| ApiError::DatabaseError(e.to_string()))?, family_id: None, // Will fetch from family_members table if needed is_active: row.try_get("is_active").unwrap_or(true), is_verified: row.try_get("email_verified").unwrap_or(false), last_login_at: row.try_get("last_login_at").ok(), - created_at: row - .try_get("created_at") - .map_err(|e| ApiError::DatabaseError(e.to_string()))?, - updated_at: row - .try_get("updated_at") - .map_err(|e| ApiError::DatabaseError(e.to_string()))?, + created_at: row.try_get("created_at").map_err(|e| ApiError::DatabaseError(e.to_string()))?, + updated_at: row.try_get("updated_at").map_err(|e| ApiError::DatabaseError(e.to_string()))?, }; - + // 检查用户状态 if !user.is_active { - if cfg!(debug_assertions) { println!("DEBUG[login]: user inactive: {}", user.email); } - state.metrics.increment_login_inactive(); return Err(ApiError::Forbidden); } - - // 验证密码(调试信息仅在 debug 构建下输出) - #[cfg(debug_assertions)] - { - println!( - "DEBUG[login]: attempting password verify for {}", - user.email - ); - // 避免泄露完整哈希,仅打印前缀长度信息 - let hash_len = user.password_hash.len(); - let prefix: String = user.password_hash.chars().take(7).collect(); - println!("DEBUG[login]: hash prefix={} (len={})", prefix, hash_len); - } - - let hash = user.password_hash.as_str(); - // 其余详细哈希打印已在上方受限 - // Support Argon2 (preferred) and bcrypt (legacy) hashes - // Allow disabling opportunistic rehash via REHASH_ON_LOGIN=0 - let enable_rehash = std::env::var("REHASH_ON_LOGIN") - .map(|v| matches!(v.as_str(), "1" | "true" | "TRUE")) - .unwrap_or(true); - - if hash.starts_with("$argon2") { - let parsed_hash = PasswordHash::new(hash).map_err(|e| { #[cfg(debug_assertions)] println!("DEBUG[login]: failed to parse Argon2 hash: {:?}", e); state.metrics.increment_login_fail(); ApiError::InternalServerError })?; - let argon2 = Argon2::default(); - argon2 - .verify_password(req.password.as_bytes(), &parsed_hash) - .map_err(|_| ApiError::Unauthorized)?; - } else if hash.starts_with("$2") { - // bcrypt format ($2a$, $2b$, $2y$) - let ok = bcrypt::verify(&req.password, hash).unwrap_or(false); - if !ok { state.metrics.increment_login_fail(); return Err(ApiError::Unauthorized); } - - if enable_rehash { - // Password rehash: transparently upgrade bcrypt to Argon2id on successful login - // Non-blocking: failures only logged. - let argon2 = Argon2::default(); - let salt = SaltString::generate(&mut OsRng); - match argon2.hash_password(req.password.as_bytes(), &salt) { - Ok(new_hash) => { - if let Err(e) = sqlx::query( - "UPDATE users SET password_hash = $1, updated_at = NOW() WHERE id = $2", - ) - .bind(new_hash.to_string()) - .bind(user.id) - .execute(pool) - .await - { - tracing::warn!(user_id=%user.id, error=?e, "password rehash failed"); - // 记录重哈希失败次数 - state.metrics.increment_rehash_fail(); - state.metrics.inc_rehash_fail_update(); - } else { - tracing::debug!(user_id=%user.id, "password rehash succeeded: bcrypt→argon2id"); - // Increment rehash metrics - state.metrics.increment_rehash(); - } - } - Err(e) => { tracing::warn!(user_id=%user.id, error=?e, "failed to generate Argon2id hash"); state.metrics.increment_rehash_fail(); state.metrics.inc_rehash_fail_hash(); } - } - } - } else { - // Unknown format: try Argon2 parse as best-effort, otherwise unauthorized - match PasswordHash::new(hash) { - Ok(parsed) => { let argon2 = Argon2::default(); argon2.verify_password(req.password.as_bytes(), &parsed).map_err(|_| { state.metrics.increment_login_fail(); ApiError::Unauthorized })?; } - Err(_) => { state.metrics.increment_login_fail(); return Err(ApiError::Unauthorized); } - } - } - + + // 验证密码 + println!("DEBUG: Attempting to verify password for user: {}", user.email); + println!("DEBUG: Password hash from DB: {}", &user.password_hash[..50.min(user.password_hash.len())]); + + let parsed_hash = PasswordHash::new(&user.password_hash) + .map_err(|e| { + println!("DEBUG: Failed to parse password hash: {:?}", e); + ApiError::InternalServerError + })?; + + let argon2 = Argon2::default(); + argon2 + .verify_password(req.password.as_bytes(), &parsed_hash) + .map_err(|e| { + println!("DEBUG: Password verification failed: {:?}", e); + ApiError::Unauthorized + })?; + // 获取用户的family_id(如果有) - let family_row = sqlx::query("SELECT family_id FROM family_members WHERE user_id = $1 LIMIT 1") - .bind(user.id) - .fetch_optional(pool) - .await - .map_err(|e| ApiError::DatabaseError(e.to_string()))?; - + let family_row = sqlx::query( + "SELECT family_id FROM family_members WHERE user_id = $1 LIMIT 1" + ) + .bind(user.id) + .fetch_optional(&pool) + .await + .map_err(|e| ApiError::DatabaseError(e.to_string()))?; + let family_id = if let Some(row) = family_row { row.try_get("family_id").ok() } else { None }; - + // 更新最后登录时间 - sqlx::query("UPDATE users SET last_login_at = NOW() WHERE id = $1") - .bind(user.id) - .execute(pool) - .await - .map_err(|e| ApiError::DatabaseError(e.to_string()))?; - + sqlx::query( + "UPDATE users SET last_login_at = NOW() WHERE id = $1" + ) + .bind(user.id) + .execute(&pool) + .await + .map_err(|e| ApiError::DatabaseError(e.to_string()))?; + // 生成JWT令牌 let claims = Claims::new(user.id, user.email.clone(), family_id); let token = claims.to_token()?; - + // 构建用户响应对象以兼容Flutter let user_response = serde_json::json!({ "id": user.id.to_string(), @@ -389,17 +318,17 @@ pub async fn login( "created_at": user.created_at.to_rfc3339(), "updated_at": user.updated_at.to_rfc3339(), }); - + // 返回兼容Flutter的响应格式 - 包含完整的user对象 let response = serde_json::json!({ "success": true, "token": token, "user": user_response, "user_id": user.id, - "email": user.email, + "email": user.email, "family_id": family_id, }); - + Ok(Json(response)) } @@ -409,7 +338,7 @@ pub async fn refresh_token( State(pool): State, ) -> ApiResult> { let user_id = claims.user_id()?; - + // 验证用户是否仍然有效 let user = sqlx::query("SELECT email, current_family_id, is_active FROM users WHERE id = $1") .bind(user_id) @@ -417,23 +346,21 @@ pub async fn refresh_token( .await .map_err(|e| ApiError::DatabaseError(e.to_string()))? .ok_or(ApiError::Unauthorized)?; - + use sqlx::Row; - + let is_active: bool = user.try_get("is_active").unwrap_or(false); if !is_active { return Err(ApiError::Forbidden); } - - let email: String = user - .try_get("email") - .map_err(|e| ApiError::DatabaseError(e.to_string()))?; + + let email: String = user.try_get("email").map_err(|e| ApiError::DatabaseError(e.to_string()))?; let family_id: Option = user.try_get("current_family_id").ok(); - + // 生成新令牌 let new_claims = Claims::new(user_id, email.clone(), family_id); let token = new_claims.to_token()?; - + Ok(Json(LoginResponse { token, user_id, @@ -448,39 +375,31 @@ pub async fn get_current_user( State(pool): State, ) -> ApiResult> { let user_id = claims.user_id()?; - + let user = sqlx::query( r#" SELECT u.*, f.name as family_name FROM users u LEFT JOIN families f ON u.current_family_id = f.id WHERE u.id = $1 - "#, + "# ) .bind(user_id) .fetch_optional(&pool) .await .map_err(|e| ApiError::DatabaseError(e.to_string()))? .ok_or(ApiError::NotFound("User not found".to_string()))?; - + use sqlx::Row; - + Ok(Json(UserProfile { - id: user - .try_get("id") - .map_err(|e| ApiError::DatabaseError(e.to_string()))?, - email: user - .try_get("email") - .map_err(|e| ApiError::DatabaseError(e.to_string()))?, - name: user - .try_get("full_name") - .map_err(|e| ApiError::DatabaseError(e.to_string()))?, + id: user.try_get("id").map_err(|e| ApiError::DatabaseError(e.to_string()))?, + email: user.try_get("email").map_err(|e| ApiError::DatabaseError(e.to_string()))?, + name: user.try_get("full_name").map_err(|e| ApiError::DatabaseError(e.to_string()))?, family_id: user.try_get("current_family_id").ok(), family_name: user.try_get("family_name").ok(), is_verified: user.try_get("email_verified").unwrap_or(false), - created_at: user - .try_get("created_at") - .map_err(|e| ApiError::DatabaseError(e.to_string()))?, + created_at: user.try_get("created_at").map_err(|e| ApiError::DatabaseError(e.to_string()))?, })) } @@ -491,16 +410,18 @@ pub async fn update_user( Json(req): Json, ) -> ApiResult { let user_id = claims.user_id()?; - + if let Some(name) = req.name { - sqlx::query("UPDATE users SET full_name = $1, updated_at = NOW() WHERE id = $2") - .bind(name) - .bind(user_id) - .execute(&pool) - .await - .map_err(|e| ApiError::DatabaseError(e.to_string()))?; + sqlx::query( + "UPDATE users SET full_name = $1, updated_at = NOW() WHERE id = $2" + ) + .bind(name) + .bind(user_id) + .execute(&pool) + .await + .map_err(|e| ApiError::DatabaseError(e.to_string()))?; } - + Ok(StatusCode::OK) } @@ -511,43 +432,44 @@ pub async fn change_password( Json(req): Json, ) -> ApiResult { let user_id = claims.user_id()?; - + // 获取当前密码哈希 let row = sqlx::query("SELECT password_hash FROM users WHERE id = $1") .bind(user_id) .fetch_one(&pool) .await .map_err(|e| ApiError::DatabaseError(e.to_string()))?; - + use sqlx::Row; - let current_hash: String = row - .try_get("password_hash") + let current_hash: String = row.try_get("password_hash") .map_err(|e| ApiError::DatabaseError(e.to_string()))?; - + // 验证旧密码 - let parsed_hash = - PasswordHash::new(¤t_hash).map_err(|_| ApiError::InternalServerError)?; - + let parsed_hash = PasswordHash::new(¤t_hash) + .map_err(|_| ApiError::InternalServerError)?; + let argon2 = Argon2::default(); argon2 .verify_password(req.old_password.as_bytes(), &parsed_hash) .map_err(|_| ApiError::Unauthorized)?; - + // 生成新密码哈希 let salt = SaltString::generate(&mut OsRng); let new_hash = argon2 .hash_password(req.new_password.as_bytes(), &salt) .map_err(|_| ApiError::InternalServerError)? .to_string(); - + // 更新密码 - sqlx::query("UPDATE users SET password_hash = $1, updated_at = NOW() WHERE id = $2") - .bind(new_hash) - .bind(user_id) - .execute(&pool) - .await - .map_err(|e| ApiError::DatabaseError(e.to_string()))?; - + sqlx::query( + "UPDATE users SET password_hash = $1, updated_at = NOW() WHERE id = $2" + ) + .bind(new_hash) + .bind(user_id) + .execute(&pool) + .await + .map_err(|e| ApiError::DatabaseError(e.to_string()))?; + Ok(StatusCode::OK) } @@ -556,11 +478,14 @@ pub async fn get_user_context( State(pool): State, Extension(user_id): Extension, ) -> ApiResult> { + let auth_service = AuthService::new(pool); - + match auth_service.get_user_context(user_id).await { Ok(context) => Ok(Json(context)), - Err(_e) => Err(ApiError::InternalServerError), + Err(_e) => { + Err(ApiError::InternalServerError) + } } } @@ -606,7 +531,7 @@ pub async fn delete_account( Ok(id) => id, Err(_) => return Err(StatusCode::UNAUTHORIZED), }; - + if !request.confirm_delete { return Ok(Json(ApiResponse::<()> { success: false, @@ -619,98 +544,99 @@ pub async fn delete_account( timestamp: chrono::Utc::now(), })); } - + // Verify the code first if let Some(redis_conn) = redis { let verification_service = crate::services::VerificationService::new(Some(redis_conn)); - - match verification_service - .verify_code( - &user_id.to_string(), - "delete_user", - &request.verification_code, - ) - .await - { + + match verification_service.verify_code( + &user_id.to_string(), + "delete_user", + &request.verification_code + ).await { Ok(true) => { // Code is valid, proceed with account deletion - let mut tx = pool.begin().await.map_err(|e| { - eprintln!("Database error: {:?}", e); - StatusCode::INTERNAL_SERVER_ERROR - })?; - - // Check if user owns any families - let owned_families: i64 = sqlx::query_scalar( - "SELECT COUNT(*) FROM family_members WHERE user_id = $1 AND role = 'owner'", - ) + let mut tx = pool.begin().await.map_err(|e| { + eprintln!("Database error: {:?}", e); + StatusCode::INTERNAL_SERVER_ERROR + })?; + + // Check if user owns any families + let owned_families: i64 = sqlx::query_scalar( + "SELECT COUNT(*) FROM family_members WHERE user_id = $1 AND role = 'owner'" + ) + .bind(user_id) + .fetch_one(&mut *tx) + .await + .map_err(|e| { + eprintln!("Database error: {:?}", e); + StatusCode::INTERNAL_SERVER_ERROR + })?; + + if owned_families > 0 { + return Ok(Json(ApiResponse::<()> { + success: false, + data: None, + error: Some(FamilyApiError { + code: "OWNS_FAMILIES".to_string(), + message: "请先转让或删除您拥有的家庭后再删除账户".to_string(), + details: None, + }), + timestamp: chrono::Utc::now(), + })); + } + + // Remove user from all families + sqlx::query("DELETE FROM family_members WHERE user_id = $1") .bind(user_id) - .fetch_one(&mut *tx) + .execute(&mut *tx) .await .map_err(|e| { eprintln!("Database error: {:?}", e); StatusCode::INTERNAL_SERVER_ERROR })?; - - if owned_families > 0 { - return Ok(Json(ApiResponse::<()> { - success: false, - data: None, - error: Some(FamilyApiError { - code: "OWNS_FAMILIES".to_string(), - message: "请先转让或删除您拥有的家庭后再删除账户".to_string(), - details: None, - }), - timestamp: chrono::Utc::now(), - })); - } - - // Remove user from all families - sqlx::query("DELETE FROM family_members WHERE user_id = $1") - .bind(user_id) - .execute(&mut *tx) - .await - .map_err(|e| { - eprintln!("Database error: {:?}", e); - StatusCode::INTERNAL_SERVER_ERROR - })?; - - // Delete user account - sqlx::query("DELETE FROM users WHERE id = $1") - .bind(user_id) - .execute(&mut *tx) - .await - .map_err(|e| { - eprintln!("Database error: {:?}", e); - StatusCode::INTERNAL_SERVER_ERROR - })?; - - tx.commit().await.map_err(|e| { + + // Delete user account + sqlx::query("DELETE FROM users WHERE id = $1") + .bind(user_id) + .execute(&mut *tx) + .await + .map_err(|e| { eprintln!("Database error: {:?}", e); StatusCode::INTERNAL_SERVER_ERROR })?; - - Ok(Json(ApiResponse::success(()))) + + tx.commit().await.map_err(|e| { + eprintln!("Database error: {:?}", e); + StatusCode::INTERNAL_SERVER_ERROR + })?; + + Ok(Json(ApiResponse::success(()))) + } + Ok(false) => { + Ok(Json(ApiResponse::<()> { + success: false, + data: None, + error: Some(FamilyApiError { + code: "INVALID_VERIFICATION_CODE".to_string(), + message: "验证码错误或已过期".to_string(), + details: None, + }), + timestamp: chrono::Utc::now(), + })) + } + Err(_) => { + Ok(Json(ApiResponse::<()> { + success: false, + data: None, + error: Some(FamilyApiError { + code: "VERIFICATION_SERVICE_ERROR".to_string(), + message: "验证码服务暂时不可用".to_string(), + details: None, + }), + timestamp: chrono::Utc::now(), + })) } - Ok(false) => Ok(Json(ApiResponse::<()> { - success: false, - data: None, - error: Some(FamilyApiError { - code: "INVALID_VERIFICATION_CODE".to_string(), - message: "验证码错误或已过期".to_string(), - details: None, - }), - timestamp: chrono::Utc::now(), - })), - Err(_) => Ok(Json(ApiResponse::<()> { - success: false, - data: None, - error: Some(FamilyApiError { - code: "VERIFICATION_SERVICE_ERROR".to_string(), - message: "验证码服务暂时不可用".to_string(), - details: None, - }), - timestamp: chrono::Utc::now(), - })), } } else { // Redis not available, skip verification in development @@ -719,10 +645,10 @@ pub async fn delete_account( eprintln!("Database error: {:?}", e); StatusCode::INTERNAL_SERVER_ERROR })?; - + // Check if user owns any families let owned_families: i64 = sqlx::query_scalar( - "SELECT COUNT(*) FROM family_members WHERE user_id = $1 AND role = 'owner'", + "SELECT COUNT(*) FROM family_members WHERE user_id = $1 AND role = 'owner'" ) .bind(user_id) .fetch_one(&mut *tx) @@ -731,7 +657,7 @@ pub async fn delete_account( eprintln!("Database error: {:?}", e); StatusCode::INTERNAL_SERVER_ERROR })?; - + if owned_families > 0 { return Ok(Json(ApiResponse::<()> { success: false, @@ -744,7 +670,7 @@ pub async fn delete_account( timestamp: chrono::Utc::now(), })); } - + // Delete user's data sqlx::query("DELETE FROM users WHERE id = $1") .bind(user_id) @@ -754,12 +680,12 @@ pub async fn delete_account( eprintln!("Database error: {:?}", e); StatusCode::INTERNAL_SERVER_ERROR })?; - + tx.commit().await.map_err(|e| { eprintln!("Database error: {:?}", e); StatusCode::INTERNAL_SERVER_ERROR })?; - + Ok(Json(ApiResponse::success(()))) } } @@ -780,7 +706,7 @@ pub async fn update_avatar( Json(req): Json, ) -> ApiResult>> { let user_id = claims.user_id()?; - + // Update avatar fields in database sqlx::query( r#" @@ -791,7 +717,7 @@ pub async fn update_avatar( avatar_background = $4, updated_at = NOW() WHERE id = $1 - "#, + "# ) .bind(user_id) .bind(&req.avatar_type) @@ -800,6 +726,6 @@ pub async fn update_avatar( .execute(&pool) .await .map_err(|e| ApiError::DatabaseError(e.to_string()))?; - + Ok(Json(ApiResponse::success(()))) } diff --git a/jive-api/src/handlers/category_handler.rs b/jive-api/src/handlers/category_handler.rs index b7d2949c..92a1e839 100644 --- a/jive-api/src/handlers/category_handler.rs +++ b/jive-api/src/handlers/category_handler.rs @@ -98,12 +98,12 @@ pub async fn create_category( RETURNING id, ledger_id, name, color, icon, classification, parent_id, position, usage_count, last_used_at"# ) .bind(Uuid::new_v4()) - .bind(&req.ledger_id) + .bind(req.ledger_id) .bind(&req.name) .bind(&req.color) .bind(&req.icon) .bind(&req.classification) - .bind(&req.parent_id) + .bind(req.parent_id) .fetch_one(&pool).await.map_err(|e|{ eprintln!("create_category err: {:?}", e); StatusCode::BAD_REQUEST })?; Ok(Json(CategoryDto{ @@ -186,7 +186,7 @@ pub async fn import_template( RETURNING id, ledger_id, name, color, icon, classification, parent_id, position, usage_count, last_used_at"# ) .bind(id) - .bind(&req.ledger_id) + .bind(req.ledger_id) .bind::(tpl.get("name")) .bind::>(tpl.try_get("color").ok()) .bind::>(tpl.try_get("icon").ok()) @@ -314,8 +314,8 @@ pub async fn batch_import_templates( r#"SELECT id, name, name_en, name_zh, classification, color, icon, version FROM system_category_templates WHERE id = $1 AND is_active = true"# ).bind(it.template_id).fetch_optional(&pool).await { Ok(Some(row)) => row, - Ok(None) => { failed += 1; details.push(ImportActionDetail{ template_id: it.template_id, action: ImportActionKind::Failed, original_name: "".into(), final_name: None, category_id: None, reason: Some("template_not_found".into())}); continue 'outer; }, - Err(_) => { failed += 1; details.push(ImportActionDetail{ template_id: it.template_id, action: ImportActionKind::Failed, original_name: "".into(), final_name: None, category_id: None, reason: Some("template_query_error".into())}); continue 'outer; } + Ok(None) => { failed += 1; details.push(ImportActionDetail{ template_id: it.template_id, action: ImportActionKind::Failed, original_name: "".into(), final_name: None, category_id: None, reason: Some("template_not_found".into()), predicted_name: None, existing_category_id: None, existing_category_name: None, final_classification: None, final_parent_id: None }); continue 'outer; }, + Err(_) => { failed += 1; details.push(ImportActionDetail{ template_id: it.template_id, action: ImportActionKind::Failed, original_name: "".into(), final_name: None, category_id: None, reason: Some("template_query_error".into()), predicted_name: None, existing_category_id: None, existing_category_name: None, final_classification: None, final_parent_id: None }); continue 'outer; } }; // Resolve fields with overrides @@ -331,7 +331,7 @@ pub async fn batch_import_templates( // First, check existence by name (case-insensitive) for active categories within ledger let exists: Option<(Uuid,)> = sqlx::query_as( "SELECT id FROM categories WHERE ledger_id=$1 AND LOWER(name)=LOWER($2) AND is_deleted=false LIMIT 1" - ).bind(&req.ledger_id).bind(&name).fetch_optional(&pool).await.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + ).bind(req.ledger_id).bind(&name).fetch_optional(&pool).await.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; if let Some((existing_id,)) = exists { match strategy.as_str() { @@ -370,7 +370,7 @@ pub async fn batch_import_templates( let candidate = format!("{} ({})", base, suffix); let taken: Option<(Uuid,)> = sqlx::query_as( "SELECT id FROM categories WHERE ledger_id=$1 AND LOWER(name)=LOWER($2) AND is_deleted=false LIMIT 1" - ).bind(&req.ledger_id).bind(&candidate).fetch_optional(&pool).await.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + ).bind(req.ledger_id).bind(&candidate).fetch_optional(&pool).await.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; if taken.is_none() { name = candidate; break; } suffix += 1; if suffix > 100 { failed += 1; details.push(ImportActionDetail{ template_id, action: ImportActionKind::Failed, original_name: base.clone(), final_name: None, category_id: None, reason: Some("rename_exhausted".into()), predicted_name: None, existing_category_id: Some(existing_id), existing_category_name: None, final_classification: Some(classification.clone()), final_parent_id: parent_id }); continue 'outer; } @@ -398,12 +398,12 @@ pub async fn batch_import_templates( Ok(query) => { query .bind(Uuid::new_v4()) - .bind(&req.ledger_id) + .bind(req.ledger_id) .bind(&name) .bind(&color) .bind(&icon) .bind(&classification) - .bind(&parent_id) + .bind(parent_id) .bind(template_id) .bind(template_version) .fetch_one(&pool).await diff --git a/jive-api/src/handlers/currency_handler.rs b/jive-api/src/handlers/currency_handler.rs index bd2efffa..574dcd01 100644 --- a/jive-api/src/handlers/currency_handler.rs +++ b/jive-api/src/handlers/currency_handler.rs @@ -1,9 +1,9 @@ -use axum::body::Body; use axum::{ extract::{Query, State}, - http::{HeaderMap, HeaderValue, StatusCode}, response::{IntoResponse, Json, Response}, + http::{HeaderMap, HeaderValue, StatusCode}, }; +use axum::body::Body; use chrono::NaiveDate; use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; @@ -11,14 +11,12 @@ use sqlx::PgPool; // use uuid::Uuid; // 未使用 use std::collections::HashMap; -use super::family_handler::ApiResponse; use crate::auth::Claims; use crate::error::{ApiError, ApiResult}; -use crate::services::currency_service::{ - AddExchangeRateRequest, CurrencyPreference, UpdateCurrencySettingsRequest, -}; -use crate::services::currency_service::{ClearManualRateRequest, ClearManualRatesBatchRequest}; use crate::services::{CurrencyService, ExchangeRate, FamilyCurrencySettings}; +use crate::services::currency_service::{UpdateCurrencySettingsRequest, AddExchangeRateRequest, CurrencyPreference}; +use crate::services::currency_service::{ClearManualRateRequest, ClearManualRatesBatchRequest}; +use super::family_handler::ApiResponse; /// 获取所有支持的货币 pub async fn get_supported_currencies( @@ -35,9 +33,7 @@ pub async fn get_supported_currencies( .map_err(|_| ApiError::InternalServerError)?; let mut current_etag = etag_row.max_ts.unwrap_or_else(|| "0".to_string()); - if current_etag.is_empty() { - current_etag = "0".to_string(); - } + if current_etag.is_empty() { current_etag = "0".to_string(); } let current_etag_value = format!("W/\"curr-{}\"", current_etag); if let Some(if_none_match) = headers.get("if-none-match").and_then(|v| v.to_str().ok()) { @@ -59,8 +55,7 @@ pub async fn get_supported_currencies( let body = Json(ApiResponse::success(currencies)); let mut resp = body.into_response(); - resp.headers_mut() - .insert("ETag", HeaderValue::from_str(¤t_etag_value).unwrap()); + resp.headers_mut().insert("ETag", HeaderValue::from_str(¤t_etag_value).unwrap()); Ok(resp) } @@ -71,12 +66,10 @@ pub async fn get_user_currency_preferences( ) -> ApiResult>>> { let user_id = claims.user_id()?; let service = CurrencyService::new(pool); - - let preferences = service - .get_user_currency_preferences(user_id) - .await + + let preferences = service.get_user_currency_preferences(user_id).await .map_err(|_e| ApiError::InternalServerError)?; - + Ok(Json(ApiResponse::success(preferences))) } @@ -94,12 +87,11 @@ pub async fn set_user_currency_preferences( ) -> ApiResult>> { let user_id = claims.user_id()?; let service = CurrencyService::new(pool); - - service - .set_user_currency_preferences(user_id, req.currencies, req.primary_currency) + + service.set_user_currency_preferences(user_id, req.currencies, req.primary_currency) .await .map_err(|_e| ApiError::InternalServerError)?; - + Ok(Json(ApiResponse::success(()))) } @@ -108,16 +100,13 @@ pub async fn get_family_currency_settings( State(pool): State, claims: Claims, ) -> ApiResult>> { - let family_id = claims - .family_id + let family_id = claims.family_id .ok_or_else(|| ApiError::BadRequest("No family selected".to_string()))?; - + let service = CurrencyService::new(pool); - let settings = service - .get_family_currency_settings(family_id) - .await + let settings = service.get_family_currency_settings(family_id).await .map_err(|_e| ApiError::InternalServerError)?; - + Ok(Json(ApiResponse::success(settings))) } @@ -127,16 +116,13 @@ pub async fn update_family_currency_settings( claims: Claims, Json(req): Json, ) -> ApiResult>> { - let family_id = claims - .family_id + let family_id = claims.family_id .ok_or_else(|| ApiError::BadRequest("No family selected".to_string()))?; - + let service = CurrencyService::new(pool); - let settings = service - .update_family_currency_settings(family_id, req) - .await + let settings = service.update_family_currency_settings(family_id, req).await .map_err(|_e| ApiError::InternalServerError)?; - + Ok(Json(ApiResponse::success(settings))) } @@ -153,18 +139,14 @@ pub async fn get_exchange_rate( Query(query): Query, ) -> ApiResult>> { let service = CurrencyService::new(pool); - let rate = service - .get_exchange_rate(&query.from, &query.to, query.date) - .await + let rate = service.get_exchange_rate(&query.from, &query.to, query.date).await .map_err(|_e| ApiError::NotFound("Exchange rate not found".to_string()))?; - + Ok(Json(ApiResponse::success(ExchangeRateResponse { from_currency: query.from, to_currency: query.to, rate, - date: query - .date - .unwrap_or_else(|| chrono::Utc::now().date_naive()), + date: query.date.unwrap_or_else(|| chrono::Utc::now().date_naive()), }))) } @@ -189,11 +171,10 @@ pub async fn get_batch_exchange_rates( Json(req): Json, ) -> ApiResult>>> { let service = CurrencyService::new(pool); - let rates = service - .get_exchange_rates(&req.base_currency, req.target_currencies, req.date) + let rates = service.get_exchange_rates(&req.base_currency, req.target_currencies, req.date) .await .map_err(|_e| ApiError::InternalServerError)?; - + Ok(Json(ApiResponse::success(rates))) } @@ -204,11 +185,9 @@ pub async fn add_exchange_rate( Json(req): Json, ) -> ApiResult>> { let service = CurrencyService::new(pool); - let rate = service - .add_exchange_rate(req) - .await + let rate = service.add_exchange_rate(req).await .map_err(|_e| ApiError::InternalServerError)?; - + Ok(Json(ApiResponse::success(rate))) } @@ -235,9 +214,7 @@ pub async fn clear_manual_exchange_rates_batch( Json(req): Json, ) -> ApiResult>> { let service = CurrencyService::new(pool); - let affected = service - .clear_manual_rates_batch(req) - .await + let affected = service.clear_manual_rates_batch(req).await .map_err(|_e| ApiError::InternalServerError)?; Ok(Json(ApiResponse::success(serde_json::json!({ "message": "Manual rates cleared", @@ -268,29 +245,24 @@ pub async fn convert_amount( Json(req): Json, ) -> ApiResult>> { let service = CurrencyService::new(pool.clone()); - + // 获取汇率 - let rate = service - .get_exchange_rate(&req.from_currency, &req.to_currency, req.date) + let rate = service.get_exchange_rate(&req.from_currency, &req.to_currency, req.date) .await .map_err(|_e| ApiError::NotFound("Exchange rate not found".to_string()))?; - + // 获取货币信息以确定小数位数 - let currencies = service - .get_supported_currencies() - .await + let currencies = service.get_supported_currencies().await .map_err(|_e| ApiError::InternalServerError)?; - - let from_currency_info = currencies - .iter() + + let from_currency_info = currencies.iter() .find(|c| c.code == req.from_currency) .ok_or_else(|| ApiError::NotFound("From currency not found".to_string()))?; - - let to_currency_info = currencies - .iter() + + let to_currency_info = currencies.iter() .find(|c| c.code == req.to_currency) .ok_or_else(|| ApiError::NotFound("To currency not found".to_string()))?; - + // 进行转换 let converted = service.convert_amount( req.amount, @@ -298,7 +270,7 @@ pub async fn convert_amount( from_currency_info.decimal_places, to_currency_info.decimal_places, ); - + Ok(Json(ApiResponse::success(ConvertAmountResponse { original_amount: req.amount, converted_amount: converted, @@ -322,12 +294,11 @@ pub async fn get_exchange_rate_history( ) -> ApiResult>>> { let service = CurrencyService::new(pool); let days = query.days.unwrap_or(30); - - let history = service - .get_exchange_rate_history(&query.from, &query.to, days) + + let history = service.get_exchange_rate_history(&query.from, &query.to, days) .await .map_err(|_e| ApiError::InternalServerError)?; - + Ok(Json(ApiResponse::success(history))) } @@ -368,7 +339,7 @@ pub async fn get_popular_exchange_pairs( name: "美元/日元".to_string(), }, ]; - + Ok(Json(ApiResponse::success(pairs))) } @@ -385,16 +356,14 @@ pub async fn refresh_exchange_rates( _claims: Claims, // 需要管理员权限 ) -> ApiResult>> { let service = CurrencyService::new(pool); - + // 为主要货币刷新汇率 let base_currencies = vec!["CNY", "USD", "EUR"]; - + for base in base_currencies { - service - .fetch_latest_rates(base) - .await + service.fetch_latest_rates(base).await .map_err(|_e| ApiError::InternalServerError)?; } - + Ok(Json(ApiResponse::success(()))) } diff --git a/jive-api/src/handlers/currency_handler_enhanced.rs b/jive-api/src/handlers/currency_handler_enhanced.rs index 124c6f68..8477dd12 100644 --- a/jive-api/src/handlers/currency_handler_enhanced.rs +++ b/jive-api/src/handlers/currency_handler_enhanced.rs @@ -4,17 +4,17 @@ use axum::{ }; use chrono::Utc; use rust_decimal::Decimal; -use serde::de::{self, Deserializer, SeqAccess, Visitor}; use serde::{Deserialize, Serialize}; +use serde::de::{self, Deserializer, SeqAccess, Visitor}; use sqlx::{PgPool, Row}; use std::collections::HashMap; -use super::family_handler::ApiResponse; use crate::auth::Claims; use crate::error::{ApiError, ApiResult}; -use crate::services::currency_service::CurrencyPreference; +use crate::services::{CurrencyService}; use crate::services::exchange_rate_api::ExchangeRateApiService; -use crate::services::CurrencyService; +use crate::services::currency_service::{CurrencyPreference}; +use super::family_handler::ApiResponse; /// Enhanced Currency model with all fields needed by Flutter #[derive(Debug, Serialize, Deserialize, Clone)] @@ -68,10 +68,10 @@ pub async fn get_all_currencies( .fetch_all(&pool) .await .map_err(|_| ApiError::InternalServerError)?; - + let mut fiat_currencies = Vec::new(); let mut crypto_currencies = Vec::new(); - + for row in rows { let currency = Currency { code: row.code.clone(), @@ -85,14 +85,14 @@ pub async fn get_all_currencies( flag: row.flag, exchange_rate: None, // Will be populated separately if needed }; - + if currency.is_crypto { crypto_currencies.push(currency); } else { fiat_currencies.push(currency); } } - + Ok(Json(ApiResponse::success(CurrenciesResponse { fiat_currencies, crypto_currencies, @@ -111,14 +111,12 @@ pub async fn get_user_currency_settings( claims: Claims, ) -> ApiResult>> { let user_id = claims.user_id()?; - + // Get user preferences let service = CurrencyService::new(pool.clone()); - let preferences = service - .get_user_currency_preferences(user_id) - .await + let preferences = service.get_user_currency_preferences(user_id).await .map_err(|_| ApiError::InternalServerError)?; - + // Get user settings from database or use defaults let settings = sqlx::query!( r#" @@ -137,15 +135,13 @@ pub async fn get_user_currency_settings( .fetch_optional(&pool) .await .map_err(|_| ApiError::InternalServerError)?; - + let settings = if let Some(settings) = settings { UserCurrencySettings { multi_currency_enabled: settings.multi_currency_enabled.unwrap_or(false), crypto_enabled: settings.crypto_enabled.unwrap_or(false), base_currency: settings.base_currency.unwrap_or_else(|| "USD".to_string()), - selected_currencies: settings - .selected_currencies - .unwrap_or_else(|| vec!["USD".to_string(), "CNY".to_string()]), + selected_currencies: settings.selected_currencies.unwrap_or_else(|| vec!["USD".to_string(), "CNY".to_string()]), show_currency_code: settings.show_currency_code.unwrap_or(true), show_currency_symbol: settings.show_currency_symbol.unwrap_or(false), preferences, @@ -162,7 +158,7 @@ pub async fn get_user_currency_settings( preferences, } }; - + Ok(Json(ApiResponse::success(settings))) } @@ -183,7 +179,7 @@ pub async fn update_user_currency_settings( Json(req): Json, ) -> ApiResult>> { let user_id = claims.user_id()?; - + // Upsert user settings sqlx::query!( r#" @@ -216,7 +212,7 @@ pub async fn update_user_currency_settings( .execute(&pool) .await .map_err(|_| ApiError::InternalServerError)?; - + // Return updated settings get_user_currency_settings(State(pool), claims).await } @@ -227,7 +223,7 @@ pub async fn get_realtime_exchange_rates( Query(query): Query, ) -> ApiResult>> { let base_currency = query.base_currency.unwrap_or_else(|| "USD".to_string()); - + // Check if we have recent rates (within 15 minutes) let recent_rates = sqlx::query( r#" @@ -245,10 +241,10 @@ pub async fn get_realtime_exchange_rates( .fetch_all(&pool) .await .map_err(|_| ApiError::InternalServerError)?; - + let mut rates = HashMap::new(); let mut last_updated: Option = None; - + for row in recent_rates { let to_currency: String = row.get("to_currency"); let rate: Decimal = row.get("rate"); @@ -259,7 +255,7 @@ pub async fn get_realtime_exchange_rates( last_updated = Some(created_naive); } } - + // If no recent rates or not enough currencies, fetch from external API if rates.is_empty() || (query.force_refresh.unwrap_or(false)) { // TODO: Implement external API integration @@ -269,7 +265,7 @@ pub async fn get_realtime_exchange_rates( last_updated = Some(Utc::now().naive_utc()); } } - + Ok(Json(ApiResponse::success(RealtimeRatesResponse { base_currency, rates, @@ -388,8 +384,7 @@ pub async fn get_detailed_batch_rates( ) -> ApiResult>> { let mut api = ExchangeRateApiService::new(); let base = req.base_currency.to_uppercase(); - let targets: Vec = req - .target_currencies + let targets: Vec = req.target_currencies .into_iter() .map(|s| s.to_uppercase()) .filter(|c| c != &base) @@ -408,8 +403,7 @@ pub async fn get_detailed_batch_rates( // Fetch fiat rates for base if needed if !base_is_crypto { // Merge per-target from providers in priority order, so missing ones are filled by next providers - let order_env = std::env::var("FIAT_PROVIDER_ORDER") - .unwrap_or_else(|_| "exchangerate-api,frankfurter,fxrates".to_string()); + let order_env = std::env::var("FIAT_PROVIDER_ORDER").unwrap_or_else(|_| "exchangerate-api,frankfurter,fxrates".to_string()); let providers: Vec = order_env .split(',') .map(|s| s.trim().to_lowercase()) @@ -417,8 +411,7 @@ pub async fn get_detailed_batch_rates( .collect(); // Accumulator for merged rates and a map to track source per currency - let mut merged: std::collections::HashMap = - std::collections::HashMap::new(); + let mut merged: std::collections::HashMap = std::collections::HashMap::new(); // Source map lives outside for later access // Determine which targets are fiat (we only need fiat->fiat rates here) @@ -430,12 +423,9 @@ pub async fn get_detailed_batch_rates( } for p in providers { - if fiat_targets.is_empty() { - break; - } + if fiat_targets.is_empty() { break; } if let Ok((rmap, src)) = api.fetch_fiat_rates_from(&p, &base).await { - for t in fiat_targets.clone() { - // iterate over a snapshot to allow removal + for t in fiat_targets.clone() { // iterate over a snapshot to allow removal if let Some(val) = rmap.get(&t) { // fill only if not already present if !merged.contains_key(&t) { @@ -457,9 +447,7 @@ pub async fn get_detailed_batch_rates( if !merged.contains_key(t) { merged.insert(t.clone(), *val); // use cached source if available; otherwise mark as "fiat" - let src = api - .cached_fiat_source(&base) - .unwrap_or_else(|| "fiat".to_string()); + let src = api.cached_fiat_source(&base).unwrap_or_else(|| "fiat".to_string()); fiat_source_map.insert(t.clone(), src); } } @@ -485,26 +473,18 @@ pub async fn get_detailed_batch_rates( // Try to get per-currency provider label if available; otherwise fall back to cached/global let provider = match fiat_source_map.get(tgt) { Some(p) => p.clone(), - None => api - .cached_fiat_source(&base) - .unwrap_or_else(|| "fiat".to_string()), + None => api.cached_fiat_source(&base).unwrap_or_else(|| "fiat".to_string()), }; Some((*rate, provider)) - } else { - None - } - } else { - None - } + } else { None } + } else { None } } else if base_is_crypto && !tgt_is_crypto { // crypto -> fiat: need price(base, tgt) // fetch crypto price of base in target fiat; if not supported, use USD cross // First try target directly let codes = vec![base.as_str()]; if let Ok(prices) = api.fetch_crypto_prices(codes.clone(), tgt).await { - let provider = api - .cached_crypto_source(&[base.as_str()], tgt.as_str()) - .unwrap_or_else(|| "crypto".to_string()); + let provider = api.cached_crypto_source(&[base.as_str()], tgt.as_str()).unwrap_or_else(|| "crypto".to_string()); prices.get(&base).map(|price| (*price, provider)) } else { // fallback via USD: price(base, USD) and fiat USD->tgt @@ -513,28 +493,18 @@ pub async fn get_detailed_batch_rates( crypto_prices_cache = Some((p.clone(), "coingecko".to_string())); } } - if let (Some((ref cp, _)), Some((ref fr, ref provider))) = - (&crypto_prices_cache, &fiat_rates) - { + if let (Some((ref cp, _)), Some((ref fr, ref provider))) = (&crypto_prices_cache, &fiat_rates) { if let (Some(p_base_usd), Some(usd_to_tgt)) = (cp.get(&base), fr.get(tgt)) { Some((*p_base_usd * *usd_to_tgt, provider.clone())) - } else { - None - } - } else { - None - } + } else { None } + } else { None } } } else if !base_is_crypto && tgt_is_crypto { // fiat -> crypto: need price(tgt, base), then invert: 1 base = (1/price) tgt let codes = vec![tgt.as_str()]; if let Ok(prices) = api.fetch_crypto_prices(codes.clone(), &base).await { - let provider = api - .cached_crypto_source(&[tgt.as_str()], base.as_str()) - .unwrap_or_else(|| "crypto".to_string()); - prices - .get(tgt) - .map(|price| (Decimal::ONE / *price, provider)) + let provider = api.cached_crypto_source(&[tgt.as_str()], base.as_str()).unwrap_or_else(|| "crypto".to_string()); + prices.get(tgt).map(|price| (Decimal::ONE / *price, provider)) } else { // fallback via USD if crypto_prices_cache.is_none() { @@ -542,19 +512,13 @@ pub async fn get_detailed_batch_rates( crypto_prices_cache = Some((p.clone(), "coingecko".to_string())); } } - if let (Some((ref cp, _)), Some((ref fr, ref provider))) = - (&crypto_prices_cache, &fiat_rates) - { + if let (Some((ref cp, _)), Some((ref fr, ref provider))) = (&crypto_prices_cache, &fiat_rates) { if let (Some(p_tgt_usd), Some(usd_to_base)) = (cp.get(tgt), fr.get(&base)) { // price(tgt, base) = p_tgt_usd / usd_to_base; then invert for base->tgt let price_tgt_base = *p_tgt_usd / *usd_to_base; Some((Decimal::ONE / price_tgt_base, provider.clone())) - } else { - None - } - } else { - None - } + } else { None } + } else { None } } } else { // crypto -> crypto: use USD cross @@ -562,16 +526,10 @@ pub async fn get_detailed_batch_rates( if let Ok(prices) = api.fetch_crypto_prices(codes.clone(), &usd).await { if let (Some(p_base_usd), Some(p_tgt_usd)) = (prices.get(&base), prices.get(tgt)) { let rate = *p_base_usd / *p_tgt_usd; // 1 base = rate target - let provider = api - .cached_crypto_source(&[base.as_str(), tgt.as_str()], "USD") - .unwrap_or_else(|| "crypto".to_string()); + let provider = api.cached_crypto_source(&[base.as_str(), tgt.as_str()], "USD").unwrap_or_else(|| "crypto".to_string()); Some((rate, provider)) - } else { - None - } - } else { - None - } + } else { None } + } else { None } }; if let Some((rate, source)) = rate_and_source { @@ -595,19 +553,9 @@ pub async fn get_detailed_batch_rates( let is_manual: Option = r.get("is_manual"); let mre: Option> = r.get("manual_rate_expiry"); (is_manual.unwrap_or(false), mre.map(|dt| dt.naive_utc())) - } else { - (false, None) - }; - - result.insert( - tgt.clone(), - DetailedRateItem { - rate, - source, - is_manual, - manual_rate_expiry, - }, - ); + } else { (false, None) }; + + result.insert(tgt.clone(), DetailedRateItem { rate, source, is_manual, manual_rate_expiry }); } } @@ -623,10 +571,10 @@ pub async fn get_crypto_prices( Query(query): Query, ) -> ApiResult>> { let fiat_currency = query.fiat_currency.unwrap_or_else(|| "USD".to_string()); - let crypto_codes = query - .crypto_codes - .unwrap_or_else(|| vec!["BTC".to_string(), "ETH".to_string(), "USDT".to_string()]); - + let crypto_codes = query.crypto_codes.unwrap_or_else(|| { + vec!["BTC".to_string(), "ETH".to_string(), "USDT".to_string()] + }); + // Get crypto prices from exchange_rates table let prices = sqlx::query!( r#" @@ -646,26 +594,29 @@ pub async fn get_crypto_prices( .fetch_all(&pool) .await .map_err(|_| ApiError::InternalServerError)?; - + let mut crypto_prices = HashMap::new(); let mut last_updated: Option = None; - + for row in prices { let price = Decimal::ONE / row.price; crypto_prices.insert(row.crypto_code, price); // created_at 可能为可空;为空时使用当前时间 - let created_naive = row.created_at.unwrap_or_else(Utc::now).naive_utc(); + let created_naive = row + .created_at + .unwrap_or_else(Utc::now) + .naive_utc(); if last_updated.map(|lu| created_naive > lu).unwrap_or(true) { last_updated = Some(created_naive); } } - + // If no recent prices, return mock data if crypto_prices.is_empty() { crypto_prices = get_mock_crypto_prices(&fiat_currency); last_updated = Some(Utc::now().naive_utc()); } - + Ok(Json(ApiResponse::success(CryptoPricesResponse { fiat_currency, prices: crypto_prices, @@ -680,7 +631,7 @@ pub struct CryptoPricesQuery { // 支持两种格式: // 1) crypto_codes=BTC&crypto_codes=ETH // 2) crypto_codes=BTC,ETH - #[serde(default, deserialize_with = "deserialize_csv_or_vec")] + #[serde(default, deserialize_with = "deserialize_csv_or_vec")] pub crypto_codes: Option>, } @@ -718,9 +669,7 @@ where let mut items = Vec::new(); while let Some(item) = seq.next_element::()? { let s = item.trim(); - if !s.is_empty() { - items.push(s.to_uppercase()); - } + if !s.is_empty() { items.push(s.to_uppercase()); } } Ok(if items.is_empty() { None } else { Some(items) }) } @@ -763,24 +712,22 @@ pub async fn convert_currency( Json(req): Json, ) -> ApiResult>> { let service = CurrencyService::new(pool.clone()); - + // Check if either is crypto let from_is_crypto = is_crypto_currency(&pool, &req.from).await?; let to_is_crypto = is_crypto_currency(&pool, &req.to).await?; - + let rate = if from_is_crypto || to_is_crypto { // Handle crypto conversion get_crypto_rate(&pool, &req.from, &req.to).await? } else { // Regular fiat conversion - service - .get_exchange_rate(&req.from, &req.to, None) - .await + service.get_exchange_rate(&req.from, &req.to, None).await .map_err(|_| ApiError::NotFound("Exchange rate not found".to_string()))? }; - + let converted_amount = req.amount * rate; - + Ok(Json(ApiResponse::success(ConvertCurrencyResponse { from: req.from.clone(), to: req.to.clone(), @@ -816,12 +763,12 @@ pub async fn manual_refresh_rates( ) -> ApiResult>> { // TODO: Implement external API calls to update rates // For now, just mark as refreshed - + let message = format!( "Rates refreshed for base currency: {}", req.base_currency.unwrap_or_else(|| "USD".to_string()) ); - + Ok(Json(ApiResponse::success(RefreshResponse { success: true, message, @@ -845,11 +792,14 @@ pub struct RefreshResponse { // Helper functions async fn is_crypto_currency(pool: &PgPool, code: &str) -> ApiResult { - let result = sqlx::query_scalar!("SELECT is_crypto FROM currencies WHERE code = $1", code) - .fetch_optional(pool) - .await - .map_err(|_| ApiError::InternalServerError)?; - + let result = sqlx::query_scalar!( + "SELECT is_crypto FROM currencies WHERE code = $1", + code + ) + .fetch_optional(pool) + .await + .map_err(|_| ApiError::InternalServerError)?; + Ok(result.flatten().unwrap_or(false)) } @@ -870,11 +820,11 @@ async fn get_crypto_rate(pool: &PgPool, from: &str, to: &str) -> ApiResult ApiResult HashMap { let mut rates = HashMap::new(); - + match base { "USD" => { rates.insert("EUR".to_string(), decimal_from_str("0.92")); @@ -923,13 +873,13 @@ fn get_default_rates(base: &str) -> HashMap { } _ => {} } - + rates } fn get_mock_crypto_prices(fiat: &str) -> HashMap { let mut prices = HashMap::new(); - + let usd_prices = vec![ ("BTC", "67500.00"), ("ETH", "3450.00"), @@ -942,19 +892,19 @@ fn get_mock_crypto_prices(fiat: &str) -> HashMap { ("AVAX", "35.00"), ("DOGE", "0.08"), ]; - + let multiplier = match fiat { "CNY" => decimal_from_str("7.25"), "EUR" => decimal_from_str("0.92"), "GBP" => decimal_from_str("0.79"), _ => Decimal::ONE, }; - + for (code, price) in usd_prices { let base_price = decimal_from_str(price); prices.insert(code.to_string(), base_price * multiplier); } - + prices } diff --git a/jive-api/src/handlers/mod.rs b/jive-api/src/handlers/mod.rs index 611a4aeb..11b87e21 100644 --- a/jive-api/src/handlers/mod.rs +++ b/jive-api/src/handlers/mod.rs @@ -1,20 +1,20 @@ +pub mod template_handler; pub mod accounts; -pub mod audit_handler; +pub mod transactions; +pub mod payees; +pub mod rules; pub mod auth; pub mod auth_handler; pub mod family_handler; +pub mod member_handler; pub mod invitation_handler; +pub mod audit_handler; pub mod ledgers; -pub mod member_handler; -pub mod payees; -pub mod rules; -pub mod template_handler; -pub mod transactions; // Demo endpoints are optional -pub mod category_handler; -pub mod currency_handler; -pub mod currency_handler_enhanced; -pub mod enhanced_profile; #[cfg(feature = "demo_endpoints")] pub mod placeholder; +pub mod enhanced_profile; +pub mod currency_handler; +pub mod currency_handler_enhanced; pub mod tag_handler; +pub mod category_handler; diff --git a/jive-api/src/handlers/template_handler.rs b/jive-api/src/handlers/template_handler.rs index e9fd13d5..d19bd4a1 100644 --- a/jive-api/src/handlers/template_handler.rs +++ b/jive-api/src/handlers/template_handler.rs @@ -2,14 +2,14 @@ //! 提供分类模板的CRUD操作和网络同步功能 use axum::{ - extract::{Path, Query, State}, + extract::{Query, State, Path}, http::StatusCode, response::Json, }; use serde::{Deserialize, Serialize}; use sqlx::{PgPool, Row}; -use std::collections::HashMap; use uuid::Uuid; +use std::collections::HashMap; /// 模板查询参数 #[derive(Debug, Deserialize)] @@ -122,16 +122,16 @@ pub async fn get_templates( Some("zh") => "COALESCE(name_zh, name)", _ => "name", }; - + let base_select = format!( "SELECT id, {} as name, name_en, name_zh, description, classification, color, icon, \ category_group, is_featured, is_active, global_usage_count, tags, version, \ created_at, updated_at FROM system_category_templates WHERE is_active = true", name_field ); - + let mut query = sqlx::QueryBuilder::new(base_select.clone()); - + // 添加过滤条件 if let Some(classification) = ¶ms.r#type { if classification != "all" { @@ -139,17 +139,17 @@ pub async fn get_templates( query.push_bind(classification); } } - + if let Some(group) = ¶ms.group { query.push(" AND category_group = "); query.push_bind(group); } - + if let Some(featured) = params.featured { query.push(" AND is_featured = "); query.push_bind(featured); } - + // 增量同步支持 if let Some(since) = ¶ms.since { query.push(" AND updated_at > "); @@ -184,9 +184,7 @@ pub async fn get_templates( .fetch_one(&pool) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; - let max_updated: chrono::DateTime = stats_row - .try_get("max_updated") - .unwrap_or(chrono::DateTime::::from_timestamp(0, 0).unwrap()); + let max_updated: chrono::DateTime = stats_row.try_get("max_updated").unwrap_or(chrono::DateTime::::from_timestamp(0, 0).unwrap()); let total_count: i64 = stats_row.try_get("total").unwrap_or(0); // Compute a simple ETag and return 304 if matches @@ -203,11 +201,7 @@ pub async fn get_templates( let offset = (page - 1) * per_page; query.push(" ORDER BY is_featured DESC, global_usage_count DESC, name"); - query - .push(" LIMIT ") - .push_bind(per_page) - .push(" OFFSET ") - .push_bind(offset); + query.push(" LIMIT ").push_bind(per_page).push(" OFFSET ").push_bind(offset); let templates = query .build_query_as::() @@ -224,12 +218,14 @@ pub async fn get_templates( last_updated: max_updated.to_rfc3339(), total: total_count, }; - + Ok(Json(response)) } /// 获取图标列表 -pub async fn get_icons(State(_pool): State) -> Json { +pub async fn get_icons( + State(_pool): State, +) -> Json { // 模拟图标映射 let mut icons = HashMap::new(); icons.insert("💰".to_string(), "salary.png".to_string()); @@ -240,7 +236,7 @@ pub async fn get_icons(State(_pool): State) -> Json { icons.insert("🎬".to_string(), "entertainment.png".to_string()); icons.insert("💳".to_string(), "finance.png".to_string()); icons.insert("💼".to_string(), "business.png".to_string()); - + Json(IconResponse { icons, cdn_base: "http://127.0.0.1:8080/static/icons".to_string(), @@ -253,10 +249,8 @@ pub async fn get_template_updates( Query(params): Query, State(pool): State, ) -> Result, StatusCode> { - let since = params - .since - .unwrap_or_else(|| "1970-01-01T00:00:00Z".to_string()); - + let since = params.since.unwrap_or_else(|| "1970-01-01T00:00:00Z".to_string()); + let templates = sqlx::query_as::<_, SystemTemplate>( r#" SELECT id, name, name_en, name_zh, description, classification, @@ -275,7 +269,7 @@ pub async fn get_template_updates( eprintln!("Database query error: {:?}", e); StatusCode::INTERNAL_SERVER_ERROR })?; - + let updates: Vec = templates .into_iter() .map(|template| TemplateUpdate { @@ -285,7 +279,7 @@ pub async fn get_template_updates( template: Some(template), }) .collect(); - + Ok(Json(UpdateResponse { updates, has_more: false, @@ -298,7 +292,7 @@ pub async fn create_template( Json(req): Json, ) -> Result, StatusCode> { let id = Uuid::new_v4(); - + let template = sqlx::query_as::<_, SystemTemplate>( r#" INSERT INTO system_category_templates @@ -327,7 +321,7 @@ pub async fn create_template( eprintln!("Create template error: {:?}", e); StatusCode::INTERNAL_SERVER_ERROR })?; - + Ok(Json(template)) } @@ -338,90 +332,91 @@ pub async fn update_template( Json(req): Json, ) -> Result, StatusCode> { // 构建动态更新查询 - let mut query = sqlx::QueryBuilder::new( - "UPDATE system_category_templates SET updated_at = CURRENT_TIMESTAMP", - ); + let mut query = sqlx::QueryBuilder::new("UPDATE system_category_templates SET updated_at = CURRENT_TIMESTAMP"); let mut has_updates = false; - + if let Some(name) = &req.name { query.push(", name = "); query.push_bind(name); has_updates = true; } - + if let Some(name_en) = &req.name_en { query.push(", name_en = "); query.push_bind(name_en); has_updates = true; } - + if let Some(name_zh) = &req.name_zh { query.push(", name_zh = "); query.push_bind(name_zh); has_updates = true; } - + if let Some(description) = &req.description { query.push(", description = "); query.push_bind(description); has_updates = true; } - + if let Some(classification) = &req.classification { query.push(", classification = "); query.push_bind(classification); has_updates = true; } - + if let Some(color) = &req.color { query.push(", color = "); query.push_bind(color); has_updates = true; } - + if let Some(icon) = &req.icon { query.push(", icon = "); query.push_bind(icon); has_updates = true; } - + if let Some(category_group) = &req.category_group { query.push(", category_group = "); query.push_bind(category_group); has_updates = true; } - + if let Some(is_featured) = req.is_featured { query.push(", is_featured = "); query.push_bind(is_featured); has_updates = true; } - + if let Some(is_active) = req.is_active { query.push(", is_active = "); query.push_bind(is_active); has_updates = true; } - + if let Some(tags) = &req.tags { query.push(", tags = "); query.push_bind(&tags[..]); has_updates = true; } - + if !has_updates { return Err(StatusCode::BAD_REQUEST); } - + query.push(" WHERE id = "); query.push_bind(template_id); - + // 执行更新 - query.build().execute(&pool).await.map_err(|e| { - eprintln!("Update template error: {:?}", e); - StatusCode::INTERNAL_SERVER_ERROR - })?; - + query.build() + .execute(&pool) + .await + .map_err(|e| { + eprintln!("Update template error: {:?}", e); + StatusCode::INTERNAL_SERVER_ERROR + })?; + // 返回更新后的模板 let template = sqlx::query_as::<_, SystemTemplate>( r#" @@ -436,7 +431,7 @@ pub async fn update_template( .fetch_one(&pool) .await .map_err(|_| StatusCode::NOT_FOUND)?; - + Ok(Json(template)) } @@ -455,7 +450,7 @@ pub async fn delete_template( eprintln!("Delete template error: {:?}", e); StatusCode::INTERNAL_SERVER_ERROR })?; - + if result.rows_affected() == 0 { Err(StatusCode::NOT_FOUND) } else { @@ -478,6 +473,6 @@ pub async fn submit_usage( .await; } } - + StatusCode::OK } diff --git a/jive-api/src/handlers/transactions.rs b/jive-api/src/handlers/transactions.rs index e1114d25..ca289d24 100644 --- a/jive-api/src/handlers/transactions.rs +++ b/jive-api/src/handlers/transactions.rs @@ -1,35 +1,28 @@ //! 交易管理API处理器 //! 提供交易的CRUD操作接口 -use axum::body::Body; use axum::{ extract::{Path, Query, State}, - http::{header, HeaderMap, StatusCode}, - response::{IntoResponse, Json}, + http::{StatusCode, header, HeaderMap}, + response::{Json, IntoResponse}, }; +use axum::body::Body; use bytes::Bytes; -use chrono::{DateTime, NaiveDate, Utc}; -use futures_util::{stream, StreamExt}; -use rust_decimal::prelude::ToPrimitive; -use rust_decimal::Decimal; -use serde::{Deserialize, Serialize}; -#[cfg(feature = "export_stream")] -use sqlx::Execute; -use sqlx::{Executor, PgPool, QueryBuilder, Row}; +use futures_util::{StreamExt, stream}; use std::convert::Infallible; use std::pin::Pin; +use serde::{Deserialize, Serialize}; +use sqlx::{PgPool, Row, QueryBuilder, Executor}; use uuid::Uuid; +use rust_decimal::Decimal; +use rust_decimal::prelude::ToPrimitive; +use chrono::{DateTime, Utc, NaiveDate}; -use crate::{ - auth::Claims, - error::{ApiError, ApiResult}, -}; +use crate::{auth::Claims, error::{ApiError, ApiResult}}; use base64::Engine; // enable .encode on base64::engine - // Use core export when feature is enabled; otherwise fallback to local CSV writer +// Use core export when feature is enabled; otherwise fallback to local CSV writer #[cfg(feature = "core_export")] -use jive_core::application::export_service::{ - CsvExportConfig, ExportService as CoreExportService, SimpleTransactionExport, -}; +use jive_core::application::export_service::{ExportService as CoreExportService, CsvExportConfig, SimpleTransactionExport}; #[cfg(not(feature = "core_export"))] #[derive(Clone)] @@ -41,10 +34,7 @@ struct CsvExportConfig { #[cfg(not(feature = "core_export"))] impl Default for CsvExportConfig { fn default() -> Self { - Self { - delimiter: ',', - include_header: true, - } + Self { delimiter: ',', include_header: true } } } @@ -56,41 +46,17 @@ fn csv_escape_cell(mut s: String, delimiter: char) -> String { s.insert(0, '\''); } } - let must_quote = - s.contains(delimiter) || s.contains('"') || s.contains('\n') || s.contains('\r'); - let s = if s.contains('"') { - s.replace('"', "\"\"") - } else { - s - }; + let must_quote = s.contains(delimiter) || s.contains('"') || s.contains('\n') || s.contains('\r'); + let s = if s.contains('"') { s.replace('"', "\"\"") } else { s }; if must_quote { format!("\"{}\"", s) } else { s } } +use crate::services::{AuthService, AuditService}; use crate::models::permission::Permission; -use crate::services::{AuditService, AuthService}; - -// Shared query builder for multiple export paths -fn build_transactions_export_query( - mut base: QueryBuilder<'static, sqlx::Postgres>, - family_id: Uuid, - account_id: Option, - ledger_id: Option, - category_id: Option, - start_date: Option, - end_date: Option, -) -> QueryBuilder<'static, sqlx::Postgres> { - base.push_bind(family_id); - if let Some(v) = account_id { base.push(" AND t.account_id = "); base.push_bind(v); } - if let Some(v) = ledger_id { base.push(" AND t.ledger_id = "); base.push_bind(v); } - if let Some(v) = category_id { base.push(" AND t.category_id = "); base.push_bind(v); } - if let Some(v) = start_date { base.push(" AND t.transaction_date >= "); base.push_bind(v); } - if let Some(v) = end_date { base.push(" AND t.transaction_date <= "); base.push_bind(v); } - base.push(" ORDER BY t.transaction_date DESC, t.id DESC"); - base -} +use crate::services::context::ServiceContext; /// 导出交易请求 #[derive(Debug, Deserialize)] @@ -101,22 +67,17 @@ pub struct ExportTransactionsRequest { pub category_id: Option, pub start_date: Option, pub end_date: Option, - // Whether to include header row in CSV output (default: true) - pub include_header: Option, } /// 导出交易(返回 data:URL 形式的下载链接,避免服务器存储文件) pub async fn export_transactions( - State(state): State, + State(pool): State, claims: Claims, headers: HeaderMap, Json(req): Json, ) -> ApiResult { - let pool = &state.pool; let user_id = claims.user_id()?; // 验证 JWT,提取用户ID - let family_id = claims - .family_id - .ok_or(ApiError::BadRequest("缺少 family_id 上下文".to_string()))?; + let family_id = claims.family_id.ok_or(ApiError::BadRequest("缺少 family_id 上下文".to_string()))?; // 依据真实 membership 构造上下文并校验权限 let auth_service = AuthService::new(pool.clone()); let ctx = auth_service @@ -128,36 +89,33 @@ pub async fn export_transactions( // 仅实现 CSV/JSON,其他格式返回错误提示 let fmt = req.format.as_deref().unwrap_or("csv").to_lowercase(); if fmt != "csv" && fmt != "json" { - return Err(ApiError::BadRequest(format!( - "不支持的导出格式: {} (仅支持 csv/json)", - fmt - ))); + return Err(ApiError::BadRequest(format!("不支持的导出格式: {} (仅支持 csv/json)", fmt))); } - // 构建统一查询 - let mut query = build_transactions_export_query( - QueryBuilder::new( - "SELECT t.id, t.account_id, t.ledger_id, t.amount, t.transaction_type, t.transaction_date, \ - t.category_id, c.name as category_name, t.payee_id, p.name as payee_name, \ - t.description, t.notes \ - FROM transactions t \ - JOIN ledgers l ON t.ledger_id = l.id \ - LEFT JOIN categories c ON t.category_id = c.id \ - LEFT JOIN payees p ON t.payee_id = p.id \ - WHERE t.deleted_at IS NULL AND l.family_id = " - ), - ctx.family_id, - req.account_id, - req.ledger_id, - req.category_id, - req.start_date, - req.end_date, + // 复用列表查询的过滤条件(限定在当前家庭) + let mut query = QueryBuilder::new( + "SELECT t.id, t.account_id, t.ledger_id, t.amount, t.transaction_type, t.transaction_date, \ + t.category_id, c.name as category_name, t.payee_id, p.name as payee_name, \ + t.description, t.notes \ + FROM transactions t \ + JOIN ledgers l ON t.ledger_id = l.id \ + LEFT JOIN categories c ON t.category_id = c.id \ + LEFT JOIN payees p ON t.payee_id = p.id \ + WHERE t.deleted_at IS NULL AND l.family_id = " ); + query.push_bind(ctx.family_id); + + if let Some(account_id) = req.account_id { query.push(" AND t.account_id = "); query.push_bind(account_id); } + if let Some(ledger_id) = req.ledger_id { query.push(" AND t.ledger_id = "); query.push_bind(ledger_id); } + if let Some(category_id) = req.category_id { query.push(" AND t.category_id = "); query.push_bind(category_id); } + if let Some(start_date) = req.start_date { query.push(" AND t.transaction_date >= "); query.push_bind(start_date); } + if let Some(end_date) = req.end_date { query.push(" AND t.transaction_date <= "); query.push_bind(end_date); } + + query.push(" ORDER BY t.transaction_date DESC, t.id DESC"); - let start_time = std::time::Instant::now(); let rows = query .build() - .fetch_all(pool) + .fetch_all(&pool) .await .map_err(|e| ApiError::DatabaseError(format!("查询交易失败: {}", e)))?; @@ -185,8 +143,8 @@ pub async fn export_transactions( "notes": row.try_get::("notes").ok(), })); } - let bytes = - serde_json::to_vec_pretty(&items).map_err(|_e| ApiError::InternalServerError)?; + let bytes = serde_json::to_vec_pretty(&items) + .map_err(|_e| ApiError::InternalServerError)?; let encoded = base64::engine::general_purpose::STANDARD.encode(&bytes); let url = format!("data:application/json;base64,{}", encoded); @@ -200,32 +158,29 @@ pub async fn export_transactions( .or_else(|| headers.get("x-real-ip")) .and_then(|v| v.to_str().ok()) .map(|s| s.split(',').next().unwrap_or(s).trim().to_string()); - let audit_id = AuditService::new(pool.clone()) - .log_action_returning_id( - ctx.family_id, - ctx.user_id, - crate::models::audit::CreateAuditLogRequest { - action: crate::models::audit::AuditAction::Export, - entity_type: "transactions".to_string(), - entity_id: None, - old_values: None, - new_values: Some(serde_json::json!({ - "count": items.len(), - "format": "json", - "filters": { - "account_id": req.account_id, - "ledger_id": req.ledger_id, - "category_id": req.category_id, - "start_date": req.start_date, - "end_date": req.end_date, - } - })), - }, - ip, - ua, - ) - .await - .ok(); + let audit_id = AuditService::new(pool.clone()).log_action_returning_id( + ctx.family_id, + ctx.user_id, + crate::models::audit::CreateAuditLogRequest { + action: crate::models::audit::AuditAction::Export, + entity_type: "transactions".to_string(), + entity_id: None, + old_values: None, + new_values: Some(serde_json::json!({ + "count": items.len(), + "format": "json", + "filters": { + "account_id": req.account_id, + "ledger_id": req.ledger_id, + "category_id": req.category_id, + "start_date": req.start_date, + "end_date": req.end_date, + } + })), + }, + ip, + ua, + ).await.ok(); // Also mirror audit id in header-like field for client convenience // Build response with optional X-Audit-Id header let mut resp_headers = HeaderMap::new(); @@ -233,29 +188,19 @@ pub async fn export_transactions( resp_headers.insert("x-audit-id", aid.to_string().parse().unwrap()); } - // 指标:缓冲 JSON 导出(这里与 CSV 共享缓冲计数器逻辑) - state.metrics.inc_export_request_buffered(); - state - .metrics - .add_export_rows_buffered(items.len() as u64); - state.metrics.observe_export_duration_buffered(start_time.elapsed().as_secs_f64()); - return Ok(( - resp_headers, - Json(serde_json::json!({ - "success": true, - "file_name": file_name, - "mime_type": "application/json", - "download_url": url, - "size": bytes.len(), - "audit_id": audit_id, - })), - )); + return Ok((resp_headers, Json(serde_json::json!({ + "success": true, + "file_name": file_name, + "mime_type": "application/json", + "download_url": url, + "size": bytes.len(), + "audit_id": audit_id, + })))); } // 生成 CSV(core_export 启用时委托核心导出;否则使用本地安全 CSV 生成) #[cfg(feature = "core_export")] let (bytes, count_for_audit) = { - let include_header = req.include_header.unwrap_or(true); let mapped: Vec = rows .into_iter() .map(|row| { @@ -285,68 +230,53 @@ pub async fn export_transactions( }) .collect(); let core = CoreExportService {}; - let cfg = CsvExportConfig::default().with_include_header(include_header); let out = core - .generate_csv_simple(&mapped, Some(&cfg)) + .generate_csv_simple(&mapped, Some(&CsvExportConfig::default())) .map_err(|_e| ApiError::InternalServerError)?; let mapped_len = mapped.len(); (out, mapped_len) }; #[cfg(not(feature = "core_export"))] - let (bytes, count_for_audit) = - { - let cfg = CsvExportConfig { - include_header: req.include_header.unwrap_or(true), - ..CsvExportConfig::default() - }; - let mut out = String::new(); - if cfg.include_header { - out.push_str(&format!( - "Date{}Description{}Amount{}Category{}Account{}Payee{}Type\n", - cfg.delimiter, - cfg.delimiter, - cfg.delimiter, - cfg.delimiter, - cfg.delimiter, - cfg.delimiter - )); - } - for row in rows.into_iter() { - let date: NaiveDate = row.get("transaction_date"); - let desc: String = row.try_get::("description").unwrap_or_default(); - let amount: Decimal = row.get("amount"); - let category: Option = row - .try_get::("category_name") - .ok() - .and_then(|s| if s.is_empty() { None } else { Some(s) }); - let account_id: Uuid = row.get("account_id"); - let payee: Option = row - .try_get::("payee_name") - .ok() - .and_then(|s| if s.is_empty() { None } else { Some(s) }); - let ttype: String = row.get("transaction_type"); - - let fields = [ - date.to_string(), - csv_escape_cell(desc, cfg.delimiter), - amount.to_string(), - csv_escape_cell(category.unwrap_or_default(), cfg.delimiter), - account_id.to_string(), - csv_escape_cell(payee.unwrap_or_default(), cfg.delimiter), - csv_escape_cell(ttype, cfg.delimiter), - ]; - out.push_str(&fields.join(&cfg.delimiter.to_string())); - out.push('\n'); - } - let line_count = out.lines().count(); - let data_rows = if cfg.include_header { - line_count.saturating_sub(1) - } else { - line_count - }; - (out.into_bytes(), data_rows) - }; + let (bytes, count_for_audit) = { + let cfg = CsvExportConfig::default(); + let mut out = String::new(); + if cfg.include_header { + out.push_str(&format!( + "Date{}Description{}Amount{}Category{}Account{}Payee{}Type\n", + cfg.delimiter, cfg.delimiter, cfg.delimiter, cfg.delimiter, cfg.delimiter, cfg.delimiter + )); + } + for row in rows.into_iter() { + let date: NaiveDate = row.get("transaction_date"); + let desc: String = row.try_get::("description").unwrap_or_default(); + let amount: Decimal = row.get("amount"); + let category: Option = row + .try_get::("category_name") + .ok() + .and_then(|s| if s.is_empty() { None } else { Some(s) }); + let account_id: Uuid = row.get("account_id"); + let payee: Option = row + .try_get::("payee_name") + .ok() + .and_then(|s| if s.is_empty() { None } else { Some(s) }); + let ttype: String = row.get("transaction_type"); + + let fields = [ + date.to_string(), + csv_escape_cell(desc, cfg.delimiter), + amount.to_string(), + csv_escape_cell(category.unwrap_or_default(), cfg.delimiter), + account_id.to_string(), + csv_escape_cell(payee.unwrap_or_default(), cfg.delimiter), + csv_escape_cell(ttype, cfg.delimiter), + ]; + out.push_str(&fields.join(&cfg.delimiter.to_string())); + out.push('\n'); + } + let line_count = out.lines().count(); + (out.into_bytes(), line_count.saturating_sub(1)) + }; let encoded = base64::engine::general_purpose::STANDARD.encode(&bytes); let url = format!("data:text/csv;charset=utf-8;base64,{}", encoded); @@ -360,70 +290,55 @@ pub async fn export_transactions( .or_else(|| headers.get("x-real-ip")) .and_then(|v| v.to_str().ok()) .map(|s| s.split(',').next().unwrap_or(s).trim().to_string()); - let audit_id = AuditService::new(pool.clone()) - .log_action_returning_id( - ctx.family_id, - ctx.user_id, - crate::models::audit::CreateAuditLogRequest { - action: crate::models::audit::AuditAction::Export, - entity_type: "transactions".to_string(), - entity_id: None, - old_values: None, - new_values: Some(serde_json::json!({ - "count": count_for_audit, - "format": "csv", - "filters": { - "account_id": req.account_id, - "ledger_id": req.ledger_id, - "category_id": req.category_id, - "start_date": req.start_date, - "end_date": req.end_date, - } - })), - }, - ip, - ua, - ) - .await - .ok(); + let audit_id = AuditService::new(pool.clone()).log_action_returning_id( + ctx.family_id, + ctx.user_id, + crate::models::audit::CreateAuditLogRequest { + action: crate::models::audit::AuditAction::Export, + entity_type: "transactions".to_string(), + entity_id: None, + old_values: None, + new_values: Some(serde_json::json!({ + "count": count_for_audit, + "format": "csv", + "filters": { + "account_id": req.account_id, + "ledger_id": req.ledger_id, + "category_id": req.category_id, + "start_date": req.start_date, + "end_date": req.end_date, + } + })), + }, + ip, + ua, + ).await.ok(); // Build response with optional X-Audit-Id header let mut resp_headers = HeaderMap::new(); if let Some(aid) = audit_id { resp_headers.insert("x-audit-id", aid.to_string().parse().unwrap()); } - // 指标:缓冲 CSV 导出 - state.metrics.inc_export_request_buffered(); - state - .metrics - .add_export_rows_buffered(count_for_audit as u64); - state.metrics.observe_export_duration_buffered(start_time.elapsed().as_secs_f64()); // Also mirror audit id in the JSON for POST CSV - Ok(( - resp_headers, - Json(serde_json::json!({ - "success": true, - "file_name": file_name, - "mime_type": "text/csv", - "download_url": url, - "size": bytes.len(), - "audit_id": audit_id, - })), - )) + Ok((resp_headers, Json(serde_json::json!({ + "success": true, + "file_name": file_name, + "mime_type": "text/csv", + "download_url": url, + "size": bytes.len(), + "audit_id": audit_id, + })))) } /// 流式 CSV 下载(更适合浏览器原生下载) pub async fn export_transactions_csv_stream( - State(state): State, + State(pool): State, claims: Claims, headers: HeaderMap, Query(q): Query, ) -> ApiResult { - let pool = &state.pool; let user_id = claims.user_id()?; - let family_id = claims - .family_id - .ok_or(ApiError::BadRequest("缺少 family_id 上下文".to_string()))?; + let family_id = claims.family_id.ok_or(ApiError::BadRequest("缺少 family_id 上下文".to_string()))?; let auth_service = AuthService::new(pool.clone()); let ctx = auth_service .validate_family_access(user_id, family_id) @@ -432,134 +347,31 @@ pub async fn export_transactions_csv_stream( ctx.require_permission(Permission::ExportData) .map_err(|_| ApiError::Forbidden)?; - let mut query = build_transactions_export_query( - QueryBuilder::new( - "SELECT t.id, t.account_id, t.ledger_id, t.amount, t.transaction_type, t.transaction_date, \ - t.category_id, c.name as category_name, t.payee_id, p.name as payee_name, \ - t.description, t.notes \ - FROM transactions t \ - JOIN ledgers l ON t.ledger_id = l.id \ - LEFT JOIN categories c ON t.category_id = c.id \ - LEFT JOIN payees p ON t.payee_id = p.id \ - WHERE t.deleted_at IS NULL AND l.family_id = " - ), - ctx.family_id, - q.account_id, - q.ledger_id, - q.category_id, - q.start_date, - q.end_date, + // 复用查询逻辑(与 JSON/CSV data:URL 相同条件,限定家庭) + let mut query = QueryBuilder::new( + "SELECT t.id, t.account_id, t.ledger_id, t.amount, t.transaction_type, t.transaction_date, \ + t.category_id, c.name as category_name, t.payee_id, p.name as payee_name, \ + t.description, t.notes \ + FROM transactions t \ + JOIN ledgers l ON t.ledger_id = l.id \ + LEFT JOIN categories c ON t.category_id = c.id \ + LEFT JOIN payees p ON t.payee_id = p.id \ + WHERE t.deleted_at IS NULL AND l.family_id = " ); - - // When export_stream feature enabled, stream rows instead of buffering entire CSV - #[cfg(feature = "export_stream")] - { - use futures::StreamExt; - use tokio::sync::mpsc; - use tokio_stream::wrappers::ReceiverStream; - let include_header = q.include_header.unwrap_or(true); - let (tx, rx) = mpsc::channel::>(8); - // Build the query and extract the SQL string as owned - let built_query = query.build(); - let sql = built_query.sql().to_owned(); - let pool_clone = pool.clone(); - let metrics = state.metrics.clone(); - tokio::spawn(async move { - let start_time = std::time::Instant::now(); - // Execute the raw SQL query using sqlx::raw_sql - let mut stream = sqlx::raw_sql(&sql).fetch(&pool_clone); - // Header - if include_header { - if tx - .send(Ok(bytes::Bytes::from_static( - b"Date,Description,Amount,Category,Account,Payee,Type\n", - ))) - .await - .is_err() - { - return; - } - } - let mut rows_counter: u64 = 0; - while let Some(item) = stream.next().await { - match item { - Ok(row) => { - use sqlx::Row; - let date: NaiveDate = row.get("transaction_date"); - let desc: String = - row.try_get::("description").unwrap_or_default(); - let amount: Decimal = row.get("amount"); - let category: Option = row - .try_get::("category_name") - .ok() - .filter(|s| !s.is_empty()); - let account_id: Uuid = row.get("account_id"); - let payee: Option = row - .try_get::("payee_name") - .ok() - .filter(|s| !s.is_empty()); - let ttype: String = row.get("transaction_type"); - let line = format!( - "{},{},{},{},{},{},{}\n", - date, - csv_escape_cell(desc, ','), - amount, - csv_escape_cell(category.clone().unwrap_or_default(), ','), - account_id, - csv_escape_cell(payee.clone().unwrap_or_default(), ','), - csv_escape_cell(ttype, ',') - ); - rows_counter += 1; - if tx.send(Ok(bytes::Bytes::from(line))).await.is_err() { - return; - } - } - Err(e) => { - let _ = tx.send(Err(ApiError::DatabaseError(e.to_string()))).await; - return; - } - } - } - // 发送完成后更新流式导出指标 - metrics.inc_export_request_stream(); - metrics.add_export_rows_stream(rows_counter); - metrics.observe_export_duration_stream(start_time.elapsed().as_secs_f64()); - }); - let byte_stream = ReceiverStream::new(rx).map(|r| match r { - Ok(b) => Ok::<_, ApiError>(b), - Err(e) => Err(e), - }); - let body = Body::from_stream(byte_stream.map(|res| { - res.map_err(|_| std::io::Error::new(std::io::ErrorKind::Other, "stream error")) - })); - // Build headers & return early (skip buffered path below) - let mut headers_map = header::HeaderMap::new(); - headers_map.insert( - header::CONTENT_TYPE, - "text/csv; charset=utf-8".parse().unwrap(), - ); - let filename = format!( - "transactions_export_{}.csv", - Utc::now().format("%Y%m%d%H%M%S") - ); - headers_map.insert( - header::CONTENT_DISPOSITION, - format!("attachment; filename=\"{}\"", filename) - .parse() - .unwrap(), - ); - return Ok((headers_map, body)); - } - - // Execute fully and build CSV body when streaming disabled - let rows_all = query - .build() - .fetch_all(pool) - .await + query.push_bind(ctx.family_id); + if let Some(account_id) = q.account_id { query.push(" AND t.account_id = "); query.push_bind(account_id); } + if let Some(ledger_id) = q.ledger_id { query.push(" AND t.ledger_id = "); query.push_bind(ledger_id); } + if let Some(category_id) = q.category_id { query.push(" AND t.category_id = "); query.push_bind(category_id); } + if let Some(start_date) = q.start_date { query.push(" AND t.transaction_date >= "); query.push_bind(start_date); } + if let Some(end_date) = q.end_date { query.push(" AND t.transaction_date <= "); query.push_bind(end_date); } + query.push(" ORDER BY t.transaction_date DESC, t.id DESC"); + + // Execute fully and build CSV body (simple, reliable) + let rows_all = query.build().fetch_all(&pool).await .map_err(|e| ApiError::DatabaseError(format!("查询交易失败: {}", e)))?; + // Build response body bytes depending on feature flag #[cfg(feature = "core_export")] let body_bytes: Vec = { - let include_header = q.include_header.unwrap_or(true); let mapped: Vec = rows_all .into_iter() .map(|row| { @@ -589,88 +401,61 @@ pub async fn export_transactions_csv_stream( }) .collect(); let core = CoreExportService {}; - let cfg = CsvExportConfig::default().with_include_header(include_header); - core.generate_csv_simple(&mapped, Some(&cfg)) + core + .generate_csv_simple(&mapped, Some(&CsvExportConfig::default())) .map_err(|_e| ApiError::InternalServerError)? }; #[cfg(not(feature = "core_export"))] - let body_bytes: Vec = - { - let cfg = CsvExportConfig { - include_header: q.include_header.unwrap_or(true), - ..CsvExportConfig::default() - }; - let mut out = String::new(); - if cfg.include_header { - out.push_str(&format!( - "Date{}Description{}Amount{}Category{}Account{}Payee{}Type\n", - cfg.delimiter, - cfg.delimiter, - cfg.delimiter, - cfg.delimiter, - cfg.delimiter, - cfg.delimiter - )); - } - for row in rows_all.iter() { - let date: NaiveDate = row.get("transaction_date"); - let desc: String = row.try_get::("description").unwrap_or_default(); - let amount: Decimal = row.get("amount"); - let category: Option = row - .try_get::("category_name") - .ok() - .and_then(|s| if s.is_empty() { None } else { Some(s) }); - let account_id: Uuid = row.get("account_id"); - let payee: Option = row - .try_get::("payee_name") - .ok() - .and_then(|s| if s.is_empty() { None } else { Some(s) }); - let ttype: String = row.get("transaction_type"); - let fields = [ - date.to_string(), - csv_escape_cell(desc, cfg.delimiter), - amount.to_string(), - csv_escape_cell(category.clone().unwrap_or_default(), cfg.delimiter), - account_id.to_string(), - csv_escape_cell(payee.clone().unwrap_or_default(), cfg.delimiter), - csv_escape_cell(ttype, cfg.delimiter), - ]; - out.push_str(&fields.join(&cfg.delimiter.to_string())); - out.push('\n'); - } - out.into_bytes() - }; + let body_bytes: Vec = { + let cfg = CsvExportConfig::default(); + let mut out = String::new(); + if cfg.include_header { + out.push_str(&format!( + "Date{}Description{}Amount{}Category{}Account{}Payee{}Type\n", + cfg.delimiter, cfg.delimiter, cfg.delimiter, cfg.delimiter, cfg.delimiter, cfg.delimiter + )); + } + for row in rows_all.iter() { + let date: NaiveDate = row.get("transaction_date"); + let desc: String = row.try_get::("description").unwrap_or_default(); + let amount: Decimal = row.get("amount"); + let category: Option = row + .try_get::("category_name") + .ok() + .and_then(|s| if s.is_empty() { None } else { Some(s) }); + let account_id: Uuid = row.get("account_id"); + let payee: Option = row + .try_get::("payee_name") + .ok() + .and_then(|s| if s.is_empty() { None } else { Some(s) }); + let ttype: String = row.get("transaction_type"); + let fields = [ + date.to_string(), + csv_escape_cell(desc, cfg.delimiter), + amount.to_string(), + csv_escape_cell(category.clone().unwrap_or_default(), cfg.delimiter), + account_id.to_string(), + csv_escape_cell(payee.clone().unwrap_or_default(), cfg.delimiter), + csv_escape_cell(ttype, cfg.delimiter), + ]; + out.push_str(&fields.join(&cfg.delimiter.to_string())); + out.push('\n'); + } + out.into_bytes() + }; // Audit log the export action (best-effort, ignore errors). We estimate row count via a COUNT query. let mut count_q = QueryBuilder::new( "SELECT COUNT(*) AS c FROM transactions t JOIN ledgers l ON t.ledger_id = l.id WHERE t.deleted_at IS NULL AND l.family_id = " ); count_q.push_bind(ctx.family_id); - if let Some(account_id) = q.account_id { - count_q.push(" AND t.account_id = "); - count_q.push_bind(account_id); - } - if let Some(ledger_id) = q.ledger_id { - count_q.push(" AND t.ledger_id = "); - count_q.push_bind(ledger_id); - } - if let Some(category_id) = q.category_id { - count_q.push(" AND t.category_id = "); - count_q.push_bind(category_id); - } - if let Some(start_date) = q.start_date { - count_q.push(" AND t.transaction_date >= "); - count_q.push_bind(start_date); - } - if let Some(end_date) = q.end_date { - count_q.push(" AND t.transaction_date <= "); - count_q.push_bind(end_date); - } - let estimated_count: i64 = count_q - .build() - .fetch_one(pool) - .await + if let Some(account_id) = q.account_id { count_q.push(" AND t.account_id = "); count_q.push_bind(account_id); } + if let Some(ledger_id) = q.ledger_id { count_q.push(" AND t.ledger_id = "); count_q.push_bind(ledger_id); } + if let Some(category_id) = q.category_id { count_q.push(" AND t.category_id = "); count_q.push_bind(category_id); } + if let Some(start_date) = q.start_date { count_q.push(" AND t.transaction_date >= "); count_q.push_bind(start_date); } + if let Some(end_date) = q.end_date { count_q.push(" AND t.transaction_date <= "); count_q.push_bind(end_date); } + let estimated_count: i64 = count_q.build().fetch_one(&pool).await .ok() .and_then(|row| row.try_get::("c").ok()) .unwrap_or(0); @@ -686,50 +471,37 @@ pub async fn export_transactions_csv_stream( .and_then(|v| v.to_str().ok()) .map(|s| s.split(',').next().unwrap_or(s).trim().to_string()); - let audit_id = AuditService::new(pool.clone()) - .log_action_returning_id( - ctx.family_id, - ctx.user_id, - crate::models::audit::CreateAuditLogRequest { - action: crate::models::audit::AuditAction::Export, - entity_type: "transactions".to_string(), - entity_id: None, - old_values: None, - new_values: Some(serde_json::json!({ - "estimated_count": estimated_count, - "filters": { - "account_id": q.account_id, - "ledger_id": q.ledger_id, - "category_id": q.category_id, - "start_date": q.start_date, - "end_date": q.end_date, - } - })), - }, - ip, - ua, - ) - .await - .ok(); + let audit_id = AuditService::new(pool.clone()).log_action_returning_id( + ctx.family_id, + ctx.user_id, + crate::models::audit::CreateAuditLogRequest { + action: crate::models::audit::AuditAction::Export, + entity_type: "transactions".to_string(), + entity_id: None, + old_values: None, + new_values: Some(serde_json::json!({ + "estimated_count": estimated_count, + "filters": { + "account_id": q.account_id, + "ledger_id": q.ledger_id, + "category_id": q.category_id, + "start_date": q.start_date, + "end_date": q.end_date, + } + })), + }, + ip, + ua, + ).await.ok(); - let filename = format!( - "transactions_export_{}.csv", - Utc::now().format("%Y%m%d%H%M%S") - ); + let filename = format!("transactions_export_{}.csv", Utc::now().format("%Y%m%d%H%M%S")); let mut headers_map = header::HeaderMap::new(); - headers_map.insert( - header::CONTENT_TYPE, - "text/csv; charset=utf-8".parse().unwrap(), - ); + headers_map.insert(header::CONTENT_TYPE, "text/csv; charset=utf-8".parse().unwrap()); headers_map.insert( header::CONTENT_DISPOSITION, - format!("attachment; filename=\"{}\"", filename) - .parse() - .unwrap(), + format!("attachment; filename=\"{}\"", filename).parse().unwrap(), ); - if let Some(aid) = audit_id { - headers_map.insert("x-audit-id", aid.to_string().parse().unwrap()); - } + if let Some(aid) = audit_id { headers_map.insert("x-audit-id", aid.to_string().parse().unwrap()); } Ok((headers_map, Body::from(body_bytes))) } @@ -864,60 +636,60 @@ pub async fn list_transactions( FROM transactions t LEFT JOIN categories c ON t.category_id = c.id LEFT JOIN payees p ON t.payee_id = p.id - WHERE t.deleted_at IS NULL", + WHERE t.deleted_at IS NULL" ); - + // 添加过滤条件 if let Some(account_id) = params.account_id { query.push(" AND t.account_id = "); query.push_bind(account_id); } - + if let Some(ledger_id) = params.ledger_id { query.push(" AND t.ledger_id = "); query.push_bind(ledger_id); } - + if let Some(category_id) = params.category_id { query.push(" AND t.category_id = "); query.push_bind(category_id); } - + if let Some(payee_id) = params.payee_id { query.push(" AND t.payee_id = "); query.push_bind(payee_id); } - + if let Some(start_date) = params.start_date { query.push(" AND t.transaction_date >= "); query.push_bind(start_date); } - + if let Some(end_date) = params.end_date { query.push(" AND t.transaction_date <= "); query.push_bind(end_date); } - + if let Some(min_amount) = params.min_amount { query.push(" AND ABS(t.amount) >= "); query.push_bind(min_amount); } - + if let Some(max_amount) = params.max_amount { query.push(" AND ABS(t.amount) <= "); query.push_bind(max_amount); } - + if let Some(transaction_type) = params.transaction_type { query.push(" AND t.transaction_type = "); query.push_bind(transaction_type); } - + if let Some(status) = params.status { query.push(" AND t.status = "); query.push_bind(status); } - + if let Some(search) = params.search { query.push(" AND (t.description ILIKE "); query.push_bind(format!("%{}%", search)); @@ -927,35 +699,33 @@ pub async fn list_transactions( query.push_bind(format!("%{}%", search)); query.push(")"); } - + // 排序 - 处理字段名映射 - let sort_by = params - .sort_by - .unwrap_or_else(|| "transaction_date".to_string()); + let sort_by = params.sort_by.unwrap_or_else(|| "transaction_date".to_string()); let sort_column = match sort_by.as_str() { "date" => "transaction_date", other => other, }; let sort_order = params.sort_order.unwrap_or_else(|| "DESC".to_string()); query.push(format!(" ORDER BY t.{} {}", sort_column, sort_order)); - + // 分页 let page = params.page.unwrap_or(1); let per_page = params.per_page.unwrap_or(50); let offset = ((page - 1) * per_page) as i64; - + query.push(" LIMIT "); query.push_bind(per_page as i64); query.push(" OFFSET "); query.push_bind(offset); - + // 执行查询 let transactions = query .build() .fetch_all(&pool) .await .map_err(|e| ApiError::DatabaseError(e.to_string()))?; - + // 转换为响应格式 let mut response = Vec::new(); for row in transactions { @@ -971,7 +741,7 @@ pub async fn list_transactions( } else { Vec::new() }; - + response.push(TransactionResponse { id: row.get("id"), account_id: row.get("account_id"), @@ -982,10 +752,7 @@ pub async fn list_transactions( category_id: row.get("category_id"), category_name: row.try_get("category_name").ok(), payee_id: row.get("payee_id"), - payee_name: row - .try_get("payee_name") - .ok() - .or_else(|| row.get("payee_name")), + payee_name: row.try_get("payee_name").ok().or_else(|| row.get("payee_name")), description: row.get("description"), notes: row.get("notes"), tags, @@ -998,7 +765,7 @@ pub async fn list_transactions( updated_at: row.get("updated_at"), }); } - + Ok(Json(response)) } @@ -1014,14 +781,14 @@ pub async fn get_transaction( LEFT JOIN categories c ON t.category_id = c.id LEFT JOIN payees p ON t.payee_id = p.id WHERE t.id = $1 AND t.deleted_at IS NULL - "#, + "# ) .bind(id) .fetch_optional(&pool) .await .map_err(|e| ApiError::DatabaseError(e.to_string()))? .ok_or(ApiError::NotFound("Transaction not found".to_string()))?; - + let tags_json: Option = row.get("tags"); let tags = if let Some(json_val) = tags_json { if let Some(arr) = json_val.as_array() { @@ -1034,7 +801,7 @@ pub async fn get_transaction( } else { Vec::new() }; - + let response = TransactionResponse { id: row.get("id"), account_id: row.get("account_id"), @@ -1057,7 +824,7 @@ pub async fn get_transaction( created_at: row.get("created_at"), updated_at: row.get("updated_at"), }; - + Ok(Json(response)) } @@ -1068,13 +835,11 @@ pub async fn create_transaction( ) -> ApiResult> { let id = Uuid::new_v4(); let _tags_json = req.tags.map(|t| serde_json::json!(t)); - + // 开始事务 - let mut tx = pool - .begin() - .await + let mut tx = pool.begin().await .map_err(|e| ApiError::DatabaseError(e.to_string()))?; - + // 创建交易 sqlx::query( r#" @@ -1087,7 +852,7 @@ pub async fn create_transaction( $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, NOW(), NOW() ) - "#, + "# ) .bind(id) .bind(req.account_id) @@ -1096,11 +861,7 @@ pub async fn create_transaction( .bind(&req.transaction_type) .bind(req.transaction_date) .bind(req.category_id) - .bind( - req.payee_name - .clone() - .or_else(|| Some("Unknown".to_string())), - ) + .bind(req.payee_name.clone().or_else(|| Some("Unknown".to_string()))) .bind(req.payee_id) .bind(req.payee_name.clone()) .bind(req.description.clone()) @@ -1113,33 +874,32 @@ pub async fn create_transaction( .execute(&mut *tx) .await .map_err(|e| ApiError::DatabaseError(e.to_string()))?; - + // 更新账户余额 let amount_change = if req.transaction_type == "expense" { -req.amount } else { req.amount }; - + sqlx::query( r#" UPDATE accounts SET current_balance = current_balance + $1, updated_at = NOW() WHERE id = $2 - "#, + "# ) .bind(amount_change) .bind(req.account_id) .execute(&mut *tx) .await .map_err(|e| ApiError::DatabaseError(e.to_string()))?; - + // 提交事务 - tx.commit() - .await + tx.commit().await .map_err(|e| ApiError::DatabaseError(e.to_string()))?; - + // 查询完整的交易信息 get_transaction(Path(id), State(pool)).await } @@ -1152,76 +912,76 @@ pub async fn update_transaction( ) -> ApiResult> { // 构建动态更新查询 let mut query = QueryBuilder::new("UPDATE transactions SET updated_at = NOW()"); - + if let Some(amount) = req.amount { query.push(", amount = "); query.push_bind(amount); } - + if let Some(transaction_date) = req.transaction_date { query.push(", transaction_date = "); query.push_bind(transaction_date); } - + if let Some(category_id) = req.category_id { query.push(", category_id = "); query.push_bind(category_id); } - + if let Some(payee_id) = req.payee_id { query.push(", payee_id = "); query.push_bind(payee_id); } - + if let Some(payee_name) = &req.payee_name { query.push(", payee_name = "); query.push_bind(payee_name); } - + if let Some(description) = &req.description { query.push(", description = "); query.push_bind(description); } - + if let Some(notes) = &req.notes { query.push(", notes = "); query.push_bind(notes); } - + if let Some(tags) = req.tags { query.push(", tags = "); query.push_bind(serde_json::json!(tags)); } - + if let Some(location) = &req.location { query.push(", location = "); query.push_bind(location); } - + if let Some(receipt_url) = &req.receipt_url { query.push(", receipt_url = "); query.push_bind(receipt_url); } - + if let Some(status) = &req.status { query.push(", status = "); query.push_bind(status); } - + query.push(" WHERE id = "); query.push_bind(id); query.push(" AND deleted_at IS NULL"); - + let result = query .build() .execute(&pool) .await .map_err(|e| ApiError::DatabaseError(e.to_string()))?; - + if result.rows_affected() == 0 { return Err(ApiError::NotFound("Transaction not found".to_string())); } - + // 返回更新后的交易 get_transaction(Path(id), State(pool)).await } @@ -1232,11 +992,9 @@ pub async fn delete_transaction( State(pool): State, ) -> ApiResult { // 开始事务 - let mut tx = pool - .begin() - .await + let mut tx = pool.begin().await .map_err(|e| ApiError::DatabaseError(e.to_string()))?; - + // 获取交易信息以便回滚余额 let row = sqlx::query( "SELECT account_id, amount, transaction_type FROM transactions WHERE id = $1 AND deleted_at IS NULL" @@ -1246,44 +1004,45 @@ pub async fn delete_transaction( .await .map_err(|e| ApiError::DatabaseError(e.to_string()))? .ok_or(ApiError::NotFound("Transaction not found".to_string()))?; - + let account_id: Uuid = row.get("account_id"); let amount: Decimal = row.get("amount"); let transaction_type: String = row.get("transaction_type"); - + // 软删除交易 - sqlx::query("UPDATE transactions SET deleted_at = NOW(), updated_at = NOW() WHERE id = $1") - .bind(id) - .execute(&mut *tx) - .await - .map_err(|e| ApiError::DatabaseError(e.to_string()))?; - + sqlx::query( + "UPDATE transactions SET deleted_at = NOW(), updated_at = NOW() WHERE id = $1" + ) + .bind(id) + .execute(&mut *tx) + .await + .map_err(|e| ApiError::DatabaseError(e.to_string()))?; + // 回滚账户余额 let amount_change = if transaction_type == "expense" { amount } else { -amount }; - + sqlx::query( r#" UPDATE accounts SET current_balance = current_balance + $1, updated_at = NOW() WHERE id = $2 - "#, + "# ) .bind(amount_change) .bind(account_id) .execute(&mut *tx) .await .map_err(|e| ApiError::DatabaseError(e.to_string()))?; - + // 提交事务 - tx.commit() - .await + tx.commit().await .map_err(|e| ApiError::DatabaseError(e.to_string()))?; - + Ok(StatusCode::NO_CONTENT) } @@ -1296,79 +1055,81 @@ pub async fn bulk_transaction_operations( "delete" => { // 批量软删除 let mut query = QueryBuilder::new( - "UPDATE transactions SET deleted_at = NOW(), updated_at = NOW() WHERE id IN (", + "UPDATE transactions SET deleted_at = NOW(), updated_at = NOW() WHERE id IN (" ); - + let mut separated = query.separated(", "); for id in &req.transaction_ids { separated.push_bind(id); } query.push(") AND deleted_at IS NULL"); - + let result = query .build() .execute(&pool) .await .map_err(|e| ApiError::DatabaseError(e.to_string()))?; - + Ok(Json(serde_json::json!({ "operation": "delete", "affected": result.rows_affected() }))) } "update_category" => { - let category_id = req - .category_id + let category_id = req.category_id .ok_or(ApiError::BadRequest("category_id is required".to_string()))?; - - let mut query = QueryBuilder::new("UPDATE transactions SET category_id = "); + + let mut query = QueryBuilder::new( + "UPDATE transactions SET category_id = " + ); query.push_bind(category_id); query.push(", updated_at = NOW() WHERE id IN ("); - + let mut separated = query.separated(", "); for id in &req.transaction_ids { separated.push_bind(id); } query.push(") AND deleted_at IS NULL"); - + let result = query .build() .execute(&pool) .await .map_err(|e| ApiError::DatabaseError(e.to_string()))?; - + Ok(Json(serde_json::json!({ "operation": "update_category", "affected": result.rows_affected() }))) } "update_status" => { - let status = req - .status + let status = req.status .ok_or(ApiError::BadRequest("status is required".to_string()))?; - - let mut query = QueryBuilder::new("UPDATE transactions SET status = "); + + let mut query = QueryBuilder::new( + "UPDATE transactions SET status = " + ); query.push_bind(status); query.push(", updated_at = NOW() WHERE id IN ("); - + let mut separated = query.separated(", "); for id in &req.transaction_ids { separated.push_bind(id); } query.push(") AND deleted_at IS NULL"); - + let result = query .build() .execute(&pool) .await .map_err(|e| ApiError::DatabaseError(e.to_string()))?; - + Ok(Json(serde_json::json!({ "operation": "update_status", "affected": result.rows_affected() }))) } - _ => Err(ApiError::BadRequest("Invalid operation".to_string())), + _ => Err(ApiError::BadRequest("Invalid operation".to_string())) } } @@ -1377,10 +1138,9 @@ pub async fn get_transaction_statistics( Query(params): Query, State(pool): State, ) -> ApiResult> { - let ledger_id = params - .ledger_id + let ledger_id = params.ledger_id .ok_or(ApiError::BadRequest("ledger_id is required".to_string()))?; - + // 获取总体统计 let stats = sqlx::query( r#" @@ -1390,13 +1150,13 @@ pub async fn get_transaction_statistics( SUM(CASE WHEN transaction_type = 'expense' THEN amount ELSE 0 END) as total_expense FROM transactions WHERE ledger_id = $1 AND deleted_at IS NULL - "#, + "# ) .bind(ledger_id) .fetch_one(&pool) .await .map_err(|e| ApiError::DatabaseError(e.to_string()))?; - + let total_count: i64 = stats.try_get("total_count").unwrap_or(0); let total_income: Option = stats.try_get("total_income").ok(); let total_expense: Option = stats.try_get("total_expense").ok(); @@ -1408,7 +1168,7 @@ pub async fn get_transaction_statistics( } else { Decimal::ZERO }; - + // 按分类统计 let category_stats = sqlx::query( r#" @@ -1421,13 +1181,13 @@ pub async fn get_transaction_statistics( WHERE ledger_id = $1 AND deleted_at IS NULL AND category_id IS NOT NULL GROUP BY category_id, category_name ORDER BY total_amount DESC - "#, + "# ) .bind(ledger_id) .fetch_all(&pool) .await .map_err(|e| ApiError::DatabaseError(e.to_string()))?; - + let total_categorized = category_stats .iter() .map(|s| { @@ -1435,25 +1195,23 @@ pub async fn get_transaction_statistics( amount.unwrap_or(Decimal::ZERO) }) .sum::(); - + let by_category: Vec = category_stats .into_iter() .filter_map(|row| { let category_id: Option = row.try_get("category_id").ok(); let category_name: Option = row.try_get("category_name").ok(); - + if let (Some(id), Some(name)) = (category_id, category_name) { let count: i64 = row.try_get("count").unwrap_or(0); let total_amount: Option = row.try_get("total_amount").ok(); let amount = total_amount.unwrap_or(Decimal::ZERO); let percentage = if total_categorized > Decimal::ZERO { - (amount / total_categorized * Decimal::from(100)) - .to_f64() - .unwrap_or(0.0) + (amount / total_categorized * Decimal::from(100)).to_f64().unwrap_or(0.0) } else { 0.0 }; - + Some(CategoryStatistics { category_id: id, category_name: name, @@ -1466,7 +1224,7 @@ pub async fn get_transaction_statistics( } }) .collect(); - + // 按月统计(最近12个月) let monthly_stats = sqlx::query( r#" @@ -1481,13 +1239,13 @@ pub async fn get_transaction_statistics( AND transaction_date >= CURRENT_DATE - INTERVAL '12 months' GROUP BY TO_CHAR(transaction_date, 'YYYY-MM') ORDER BY month DESC - "#, + "# ) .bind(ledger_id) .fetch_all(&pool) .await .map_err(|e| ApiError::DatabaseError(e.to_string()))?; - + let by_month: Vec = monthly_stats .into_iter() .map(|row| { @@ -1495,10 +1253,10 @@ pub async fn get_transaction_statistics( let income: Option = row.try_get("income").ok(); let expense: Option = row.try_get("expense").ok(); let transaction_count: i64 = row.try_get("transaction_count").unwrap_or(0); - + let income = income.unwrap_or(Decimal::ZERO); let expense = expense.unwrap_or(Decimal::ZERO); - + MonthlyStatistics { month, income, @@ -1508,7 +1266,7 @@ pub async fn get_transaction_statistics( } }) .collect(); - + let response = TransactionStatistics { total_count, total_income, @@ -1518,6 +1276,6 @@ pub async fn get_transaction_statistics( by_category, by_month, }; - + Ok(Json(response)) } diff --git a/jive-api/src/lib.rs b/jive-api/src/lib.rs index 506857a3..42774f43 100644 --- a/jive-api/src/lib.rs +++ b/jive-api/src/lib.rs @@ -1,184 +1,22 @@ #![allow(dead_code, unused_imports)] -pub mod auth; -pub mod error; pub mod handlers; -pub mod middleware; +pub mod error; +pub mod auth; pub mod models; pub mod services; +pub mod middleware; pub mod ws; -use axum::extract::FromRef; use sqlx::PgPool; -use std::sync::atomic::{AtomicU64, Ordering}; -use std::sync::Arc; +use axum::extract::FromRef; /// 应用状态 #[derive(Clone)] pub struct AppState { pub pool: PgPool, - pub ws_manager: Option>, // Optional WebSocket manager + pub ws_manager: Option>, // Optional WebSocket manager pub redis: Option, - pub metrics: AppMetrics, -} - -/// Application metrics -#[derive(Clone)] -pub struct AppMetrics { - pub rehash_count: Arc, - pub rehash_fail_count: Arc, - pub export_request_stream_count: Arc, - pub export_request_buffered_count: Arc, - pub export_rows_stream: Arc, - pub export_rows_buffered: Arc, - pub auth_login_fail_count: Arc, - pub auth_login_inactive_count: Arc, - // Export duration histogram (buffered) - pub export_dur_buf_le_005: Arc, - pub export_dur_buf_le_02: Arc, - pub export_dur_buf_le_1: Arc, - pub export_dur_buf_le_3: Arc, - pub export_dur_buf_le_10: Arc, - pub export_dur_buf_le_inf: Arc, - pub export_dur_buf_sum_ns: Arc, - pub export_dur_buf_count: Arc, - // Export duration histogram (stream) - pub export_dur_stream_le_005: Arc, - pub export_dur_stream_le_02: Arc, - pub export_dur_stream_le_1: Arc, - pub export_dur_stream_le_3: Arc, - pub export_dur_stream_le_10: Arc, - pub export_dur_stream_le_inf: Arc, - pub export_dur_stream_sum_ns: Arc, - pub export_dur_stream_count: Arc, - pub rehash_fail_hash: Arc, - pub rehash_fail_update: Arc, - pub auth_login_rate_limited: Arc, -} - -impl Default for AppMetrics { - fn default() -> Self { - Self::new() - } -} - -impl AppMetrics { - pub fn new() -> Self { - Self { - rehash_count: Arc::new(AtomicU64::new(0)), - rehash_fail_count: Arc::new(AtomicU64::new(0)), - export_request_stream_count: Arc::new(AtomicU64::new(0)), - export_request_buffered_count: Arc::new(AtomicU64::new(0)), - export_rows_stream: Arc::new(AtomicU64::new(0)), - export_rows_buffered: Arc::new(AtomicU64::new(0)), - auth_login_fail_count: Arc::new(AtomicU64::new(0)), - auth_login_inactive_count: Arc::new(AtomicU64::new(0)), - export_dur_buf_le_005: Arc::new(AtomicU64::new(0)), - export_dur_buf_le_02: Arc::new(AtomicU64::new(0)), - export_dur_buf_le_1: Arc::new(AtomicU64::new(0)), - export_dur_buf_le_3: Arc::new(AtomicU64::new(0)), - export_dur_buf_le_10: Arc::new(AtomicU64::new(0)), - export_dur_buf_le_inf: Arc::new(AtomicU64::new(0)), - export_dur_buf_sum_ns: Arc::new(AtomicU64::new(0)), - export_dur_buf_count: Arc::new(AtomicU64::new(0)), - export_dur_stream_le_005: Arc::new(AtomicU64::new(0)), - export_dur_stream_le_02: Arc::new(AtomicU64::new(0)), - export_dur_stream_le_1: Arc::new(AtomicU64::new(0)), - export_dur_stream_le_3: Arc::new(AtomicU64::new(0)), - export_dur_stream_le_10: Arc::new(AtomicU64::new(0)), - export_dur_stream_le_inf: Arc::new(AtomicU64::new(0)), - export_dur_stream_sum_ns: Arc::new(AtomicU64::new(0)), - export_dur_stream_count: Arc::new(AtomicU64::new(0)), - rehash_fail_hash: Arc::new(AtomicU64::new(0)), - rehash_fail_update: Arc::new(AtomicU64::new(0)), - auth_login_rate_limited: Arc::new(AtomicU64::new(0)), - } - } - - pub fn increment_rehash(&self) { - self.rehash_count.fetch_add(1, Ordering::Relaxed); - } - - pub fn get_rehash_count(&self) -> u64 { - self.rehash_count.load(Ordering::Relaxed) - } - - pub fn increment_rehash_fail(&self) { - self.rehash_fail_count.fetch_add(1, Ordering::Relaxed); - } - pub fn get_rehash_fail(&self) -> u64 { self.rehash_fail_count.load(Ordering::Relaxed) } - - pub fn inc_export_request_stream(&self) { self.export_request_stream_count.fetch_add(1, Ordering::Relaxed); } - pub fn inc_export_request_buffered(&self) { self.export_request_buffered_count.fetch_add(1, Ordering::Relaxed); } - pub fn add_export_rows_stream(&self, n: u64) { self.export_rows_stream.fetch_add(n, Ordering::Relaxed); } - pub fn add_export_rows_buffered(&self, n: u64) { self.export_rows_buffered.fetch_add(n, Ordering::Relaxed); } - pub fn get_export_counts(&self) -> (u64,u64,u64,u64) { - ( - self.export_request_stream_count.load(Ordering::Relaxed), - self.export_request_buffered_count.load(Ordering::Relaxed), - self.export_rows_stream.load(Ordering::Relaxed), - self.export_rows_buffered.load(Ordering::Relaxed), - ) - } - - pub fn increment_login_fail(&self) { self.auth_login_fail_count.fetch_add(1, Ordering::Relaxed); } - pub fn increment_login_inactive(&self) { self.auth_login_inactive_count.fetch_add(1, Ordering::Relaxed); } - pub fn get_login_fail(&self) -> u64 { self.auth_login_fail_count.load(Ordering::Relaxed) } - pub fn get_login_inactive(&self) -> u64 { self.auth_login_inactive_count.load(Ordering::Relaxed) } - - fn observe_histogram( - dur_secs: f64, - sum_ns: &AtomicU64, - count: &AtomicU64, - b005: &AtomicU64, - b02: &AtomicU64, - b1: &AtomicU64, - b3: &AtomicU64, - b10: &AtomicU64, - binf: &AtomicU64, - ) { - let ns = (dur_secs * 1_000_000_000.0) as u64; - sum_ns.fetch_add(ns, Ordering::Relaxed); - count.fetch_add(1, Ordering::Relaxed); - if dur_secs <= 0.05 { b005.fetch_add(1, Ordering::Relaxed); } - if dur_secs <= 0.2 { b02.fetch_add(1, Ordering::Relaxed); } - if dur_secs <= 1.0 { b1.fetch_add(1, Ordering::Relaxed); } - if dur_secs <= 3.0 { b3.fetch_add(1, Ordering::Relaxed); } - if dur_secs <= 10.0 { b10.fetch_add(1, Ordering::Relaxed); } - binf.fetch_add(1, Ordering::Relaxed); // +Inf bucket always - } - - pub fn observe_export_duration_buffered(&self, dur_secs: f64) { - Self::observe_histogram( - dur_secs, - &self.export_dur_buf_sum_ns, - &self.export_dur_buf_count, - &self.export_dur_buf_le_005, - &self.export_dur_buf_le_02, - &self.export_dur_buf_le_1, - &self.export_dur_buf_le_3, - &self.export_dur_buf_le_10, - &self.export_dur_buf_le_inf, - ); - } - pub fn observe_export_duration_stream(&self, dur_secs: f64) { - Self::observe_histogram( - dur_secs, - &self.export_dur_stream_sum_ns, - &self.export_dur_stream_count, - &self.export_dur_stream_le_005, - &self.export_dur_stream_le_02, - &self.export_dur_stream_le_1, - &self.export_dur_stream_le_3, - &self.export_dur_stream_le_10, - &self.export_dur_stream_le_inf, - ); - } - pub fn inc_rehash_fail_hash(&self) { self.rehash_fail_hash.fetch_add(1, Ordering::Relaxed); } - pub fn inc_rehash_fail_update(&self) { self.rehash_fail_update.fetch_add(1, Ordering::Relaxed); } - pub fn get_rehash_fail_breakdown(&self) -> (u64,u64) { (self.rehash_fail_hash.load(Ordering::Relaxed), self.rehash_fail_update.load(Ordering::Relaxed)) } - pub fn inc_login_rate_limited(&self) { self.auth_login_rate_limited.fetch_add(1, Ordering::Relaxed); } - pub fn get_login_rate_limited(&self) -> u64 { self.auth_login_rate_limited.load(Ordering::Relaxed) } } // 实现FromRef trait以便子状态可以从AppState中提取 @@ -198,3 +36,5 @@ impl FromRef for Option { // Re-export commonly used types pub use error::{ApiError, ApiResult}; pub use services::{ServiceContext, ServiceError}; + + diff --git a/jive-api/src/main.rs b/jive-api/src/main.rs index 20693903..4ea0f4ea 100644 --- a/jive-api/src/main.rs +++ b/jive-api/src/main.rs @@ -5,11 +5,9 @@ use axum::{ extract::{ws::WebSocketUpgrade, Query, State}, http::StatusCode, response::{Json, Response}, - routing::{delete, get, post, put}, + routing::{get, post, put, delete}, Router, }; -use redis::aio::ConnectionManager; -use redis::Client as RedisClient; use serde::Deserialize; use serde_json::json; use sqlx::postgres::PgPoolOptions; @@ -18,47 +16,40 @@ use std::net::SocketAddr; use std::sync::Arc; use tokio::net::TcpListener; use tower::ServiceBuilder; -use tower_http::trace::TraceLayer; -use jive_money_api::middleware::rate_limit::{RateLimiter, login_rate_limit}; -use jive_money_api::middleware::metrics_guard::{metrics_guard, MetricsGuardState, Cidr}; -use tracing::{error, info, warn}; +use tower_http::{ + trace::TraceLayer, +}; +use tracing::{info, warn, error}; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; +use redis::aio::ConnectionManager; +use redis::Client as RedisClient; // 使用库中的模块 use jive_money_api::{handlers, services, ws}; -mod metrics; // 导入处理器 +use handlers::template_handler::*; use handlers::accounts::*; -#[cfg(feature = "demo_endpoints")] -use handlers::audit_handler::{cleanup_audit_logs, export_audit_logs, get_audit_logs}; +use handlers::transactions::*; +use handlers::payees::*; +use handlers::rules::*; use handlers::auth as auth_handlers; -use handlers::category_handler; +use handlers::enhanced_profile; use handlers::currency_handler; use handlers::currency_handler_enhanced; -use handlers::enhanced_profile; -use handlers::family_handler::{ - create_family, delete_family, get_family, get_family_actions, get_family_statistics, - get_role_descriptions, join_family, leave_family, list_families, request_verification_code, - transfer_ownership, update_family, -}; -use handlers::ledgers::{ - create_ledger, delete_ledger, get_current_ledger, get_ledger, get_ledger_members, - get_ledger_statistics, list_ledgers, update_ledger, -}; -use handlers::member_handler::{ - add_member, get_family_members, remove_member, update_member_permissions, update_member_role, -}; -use handlers::payees::*; -#[cfg(feature = "demo_endpoints")] -use handlers::placeholder::{activity_logs, advanced_settings, export_data, family_settings}; -use handlers::rules::*; use handlers::tag_handler; -use handlers::template_handler::*; -use handlers::transactions::*; +use handlers::category_handler; +use handlers::ledgers::{list_ledgers, create_ledger, get_current_ledger, get_ledger, + update_ledger, delete_ledger, get_ledger_statistics, get_ledger_members}; +use handlers::family_handler::{list_families, create_family, get_family, update_family, delete_family, join_family, leave_family, request_verification_code, get_family_statistics, get_family_actions, get_role_descriptions, transfer_ownership}; +use handlers::member_handler::{get_family_members, add_member, remove_member, update_member_role, update_member_permissions}; +#[cfg(feature = "demo_endpoints")] +use handlers::placeholder::{export_data, activity_logs, advanced_settings, family_settings}; +#[cfg(feature = "demo_endpoints")] +use handlers::audit_handler::{get_audit_logs, export_audit_logs, cleanup_audit_logs}; // 使用库中的 AppState -use jive_money_api::{AppMetrics, AppState}; +use jive_money_api::AppState; /// WebSocket 查询参数 #[derive(Debug, Deserialize)] @@ -81,12 +72,9 @@ async fn handle_websocket( .body("Unauthorized: Missing token".into()) .unwrap(); } - - info!( - "WebSocket connection request with token: {}", - &token[..20.min(token.len())] - ); - + + info!("WebSocket connection request with token: {}", &token[..20.min(token.len())]); + // 升级为 WebSocket 连接 ws.on_upgrade(move |socket| ws::handle_socket(socket, token, pool)) } @@ -95,11 +83,12 @@ async fn handle_websocket( async fn main() -> Result<(), Box> { // 加载环境变量 dotenv::dotenv().ok(); - + // 初始化日志 tracing_subscriber::registry() .with( - tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| "info".into()), + tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| "info".into()), ) .with(tracing_subscriber::fmt::layer()) .init(); @@ -111,14 +100,11 @@ async fn main() -> Result<(), Box> { // DATABASE_URL 回退:开发脚本使用宿主 5433 端口映射容器 5432,这里同步保持一致,避免脚本外手动运行 API 时连接被拒绝 let database_url = std::env::var("DATABASE_URL").unwrap_or_else(|_| { let db_port = std::env::var("DB_PORT").unwrap_or_else(|_| "5433".to_string()); - format!( - "postgresql://postgres:postgres@localhost:{}/jive_money", - db_port - ) + format!("postgresql://postgres:postgres@localhost:{}/jive_money", db_port) }); - + info!("📦 Connecting to database..."); - + let pool = match PgPoolOptions::new() .max_connections(20) .connect(&database_url) @@ -148,7 +134,7 @@ async fn main() -> Result<(), Box> { // 创建 WebSocket 管理器 let ws_manager = Arc::new(ws::WsConnectionManager::new()); info!("✅ WebSocket manager initialized"); - + // Redis 连接(可选) let redis_manager = match std::env::var("REDIS_URL") { Ok(redis_url) => { @@ -193,9 +179,7 @@ async fn main() -> Result<(), Box> { let mut conn = manager.clone(); match redis::cmd("PING").query_async::(&mut conn).await { Ok(_) => { - info!( - "✅ Redis connected successfully (default localhost:6379)" - ); + info!("✅ Redis connected successfully (default localhost:6379)"); Some(manager) } Err(_) => { @@ -217,33 +201,14 @@ async fn main() -> Result<(), Box> { } } }; - + // 创建应用状态 let app_state = AppState { pool: pool.clone(), ws_manager: Some(ws_manager.clone()), redis: redis_manager, - metrics: AppMetrics::new(), }; - - // Rate limiter (login) configuration - let (rl_max, rl_window) = std::env::var("AUTH_RATE_LIMIT") - .ok() - .and_then(|v| { - let parts: Vec<&str> = v.split('/').collect(); - if parts.len()==2 { Some((parts[0].parse().ok()?, parts[1].parse().ok()?)) } else { None } - }) - .unwrap_or((30u32, 60u64)); - let rate_limiter = RateLimiter::new(rl_max, rl_window); - let metrics_guard_state = { - let enabled = std::env::var("ALLOW_PUBLIC_METRICS").map(|v| v=="0").unwrap_or(false); - let allow_list = std::env::var("METRICS_ALLOW_CIDRS").unwrap_or("127.0.0.1/32".to_string()); - let deny_list = std::env::var("METRICS_DENY_CIDRS").unwrap_or_default(); - let allow = allow_list.split(',').filter_map(|c| Cidr::parse(c.trim())).collect(); - let deny = deny_list.split(',').filter_map(|c| Cidr::parse(c.trim())).collect(); - MetricsGuardState { allow, deny, enabled } - }; - + // 启动定时任务(汇率更新等) info!("🕒 Starting scheduled tasks..."); let pool_arc = Arc::new(pool.clone()); @@ -259,20 +224,21 @@ async fn main() -> Result<(), Box> { // 健康检查 .route("/health", get(health_check)) .route("/", get(api_info)) + // WebSocket 端点 .route("/ws", get(handle_websocket)) + // 分类模板 API .route("/api/v1/templates/list", get(get_templates)) .route("/api/v1/icons/list", get(get_icons)) .route("/api/v1/templates/updates", get(get_template_updates)) .route("/api/v1/templates/usage", post(submit_usage)) + // 超级管理员 API .route("/api/v1/admin/templates", post(create_template)) .route("/api/v1/admin/templates/:template_id", put(update_template)) - .route( - "/api/v1/admin/templates/:template_id", - delete(delete_template), - ) + .route("/api/v1/admin/templates/:template_id", delete(delete_template)) + // 账户管理 API .route("/api/v1/accounts", get(list_accounts)) .route("/api/v1/accounts", post(create_account)) @@ -280,29 +246,18 @@ async fn main() -> Result<(), Box> { .route("/api/v1/accounts/:id", put(update_account)) .route("/api/v1/accounts/:id", delete(delete_account)) .route("/api/v1/accounts/statistics", get(get_account_statistics)) + // 交易管理 API .route("/api/v1/transactions", get(list_transactions)) .route("/api/v1/transactions", post(create_transaction)) .route("/api/v1/transactions/export", post(export_transactions)) - .route( - "/api/v1/transactions/export.csv", - get(export_transactions_csv_stream), - ) + .route("/api/v1/transactions/export.csv", get(export_transactions_csv_stream)) .route("/api/v1/transactions/:id", get(get_transaction)) .route("/api/v1/transactions/:id", put(update_transaction)) .route("/api/v1/transactions/:id", delete(delete_transaction)) - .route( - "/api/v1/transactions/bulk", - post(bulk_transaction_operations), - ) - .route( - "/api/v1/transactions/statistics", - get(get_transaction_statistics), - ) - // Metrics endpoint - .route("/metrics", get(metrics::metrics_handler).route_layer( - axum::middleware::from_fn_with_state(metrics_guard_state.clone(), metrics_guard) - )) + .route("/api/v1/transactions/bulk", post(bulk_transaction_operations)) + .route("/api/v1/transactions/statistics", get(get_transaction_statistics)) + // 收款人管理 API .route("/api/v1/payees", get(list_payees)) .route("/api/v1/payees", post(create_payee)) @@ -312,6 +267,7 @@ async fn main() -> Result<(), Box> { .route("/api/v1/payees/suggestions", get(get_payee_suggestions)) .route("/api/v1/payees/statistics", get(get_payee_statistics)) .route("/api/v1/payees/merge", post(merge_payees)) + // 规则引擎 API .route("/api/v1/rules", get(list_rules)) .route("/api/v1/rules", post(create_rule)) @@ -319,41 +275,24 @@ async fn main() -> Result<(), Box> { .route("/api/v1/rules/:id", put(update_rule)) .route("/api/v1/rules/:id", delete(delete_rule)) .route("/api/v1/rules/execute", post(execute_rules)) + // 认证 API - .route( - "/api/v1/auth/register", - post(auth_handlers::register_with_family), - ) - .route("/api/v1/auth/login", post(auth_handlers::login).route_layer( - axum::middleware::from_fn_with_state((rate_limiter.clone(), app_state.clone()), login_rate_limit) - )) + .route("/api/v1/auth/register", post(auth_handlers::register_with_family)) + .route("/api/v1/auth/login", post(auth_handlers::login)) .route("/api/v1/auth/refresh", post(auth_handlers::refresh_token)) .route("/api/v1/auth/user", get(auth_handlers::get_current_user)) - .route("/api/v1/auth/profile", get(auth_handlers::get_current_user)) // Alias for Flutter app + .route("/api/v1/auth/profile", get(auth_handlers::get_current_user)) // Alias for Flutter app .route("/api/v1/auth/user", put(auth_handlers::update_user)) .route("/api/v1/auth/avatar", put(auth_handlers::update_avatar)) - .route( - "/api/v1/auth/password", - post(auth_handlers::change_password), - ) + .route("/api/v1/auth/password", post(auth_handlers::change_password)) .route("/api/v1/auth/delete", delete(auth_handlers::delete_account)) + // Enhanced Profile API - .route( - "/api/v1/auth/register-enhanced", - post(enhanced_profile::register_with_preferences), - ) - .route( - "/api/v1/auth/profile-enhanced", - get(enhanced_profile::get_enhanced_profile), - ) - .route( - "/api/v1/auth/preferences", - put(enhanced_profile::update_preferences), - ) - .route( - "/api/v1/locales", - get(enhanced_profile::get_supported_locales), - ) + .route("/api/v1/auth/register-enhanced", post(enhanced_profile::register_with_preferences)) + .route("/api/v1/auth/profile-enhanced", get(enhanced_profile::get_enhanced_profile)) + .route("/api/v1/auth/preferences", put(enhanced_profile::update_preferences)) + .route("/api/v1/locales", get(enhanced_profile::get_supported_locales)) + // 家庭管理 API .route("/api/v1/families", get(list_families)) .route("/api/v1/families", post(create_family)) @@ -362,36 +301,21 @@ async fn main() -> Result<(), Box> { .route("/api/v1/families/:id", get(get_family)) .route("/api/v1/families/:id", put(update_family)) .route("/api/v1/families/:id", delete(delete_family)) - .route( - "/api/v1/families/:id/statistics", - get(get_family_statistics), - ) + .route("/api/v1/families/:id/statistics", get(get_family_statistics)) .route("/api/v1/families/:id/actions", get(get_family_actions)) - .route( - "/api/v1/families/:id/transfer-ownership", - post(transfer_ownership), - ) + .route("/api/v1/families/:id/transfer-ownership", post(transfer_ownership)) .route("/api/v1/roles/descriptions", get(get_role_descriptions)) + // 家庭成员管理 API .route("/api/v1/families/:id/members", get(get_family_members)) .route("/api/v1/families/:id/members", post(add_member)) - .route( - "/api/v1/families/:id/members/:user_id", - delete(remove_member), - ) - .route( - "/api/v1/families/:id/members/:user_id/role", - put(update_member_role), - ) - .route( - "/api/v1/families/:id/members/:user_id/permissions", - put(update_member_permissions), - ) + .route("/api/v1/families/:id/members/:user_id", delete(remove_member)) + .route("/api/v1/families/:id/members/:user_id/role", put(update_member_role)) + .route("/api/v1/families/:id/members/:user_id/permissions", put(update_member_permissions)) + // 验证码 API - .route( - "/api/v1/verification/request", - post(request_verification_code), - ) + .route("/api/v1/verification/request", post(request_verification_code)) + // 账本 API (Ledgers) - 完整版特有 .route("/api/v1/ledgers", get(list_ledgers)) .route("/api/v1/ledgers", post(create_ledger)) @@ -401,101 +325,35 @@ async fn main() -> Result<(), Box> { .route("/api/v1/ledgers/:id", delete(delete_ledger)) .route("/api/v1/ledgers/:id/statistics", get(get_ledger_statistics)) .route("/api/v1/ledgers/:id/members", get(get_ledger_members)) + // 货币管理 API - 基础功能 - .route( - "/api/v1/currencies", - get(currency_handler::get_supported_currencies), - ) - .route( - "/api/v1/currencies/preferences", - get(currency_handler::get_user_currency_preferences), - ) - .route( - "/api/v1/currencies/preferences", - post(currency_handler::set_user_currency_preferences), - ) - .route( - "/api/v1/currencies/rate", - get(currency_handler::get_exchange_rate), - ) - .route( - "/api/v1/currencies/rates", - post(currency_handler::get_batch_exchange_rates), - ) - .route( - "/api/v1/currencies/rates/add", - post(currency_handler::add_exchange_rate), - ) - .route( - "/api/v1/currencies/rates/clear-manual", - post(currency_handler::clear_manual_exchange_rate), - ) - .route( - "/api/v1/currencies/rates/clear-manual-batch", - post(currency_handler::clear_manual_exchange_rates_batch), - ) - .route( - "/api/v1/currencies/convert", - post(currency_handler::convert_amount), - ) - .route( - "/api/v1/currencies/history", - get(currency_handler::get_exchange_rate_history), - ) - .route( - "/api/v1/currencies/popular-pairs", - get(currency_handler::get_popular_exchange_pairs), - ) - .route( - "/api/v1/currencies/refresh", - post(currency_handler::refresh_exchange_rates), - ) - .route( - "/api/v1/family/currency-settings", - get(currency_handler::get_family_currency_settings), - ) - .route( - "/api/v1/family/currency-settings", - put(currency_handler::update_family_currency_settings), - ) + .route("/api/v1/currencies", get(currency_handler::get_supported_currencies)) + .route("/api/v1/currencies/preferences", get(currency_handler::get_user_currency_preferences)) + .route("/api/v1/currencies/preferences", post(currency_handler::set_user_currency_preferences)) + .route("/api/v1/currencies/rate", get(currency_handler::get_exchange_rate)) + .route("/api/v1/currencies/rates", post(currency_handler::get_batch_exchange_rates)) + .route("/api/v1/currencies/rates/add", post(currency_handler::add_exchange_rate)) + .route("/api/v1/currencies/rates/clear-manual", post(currency_handler::clear_manual_exchange_rate)) + .route("/api/v1/currencies/rates/clear-manual-batch", post(currency_handler::clear_manual_exchange_rates_batch)) + .route("/api/v1/currencies/convert", post(currency_handler::convert_amount)) + .route("/api/v1/currencies/history", get(currency_handler::get_exchange_rate_history)) + .route("/api/v1/currencies/popular-pairs", get(currency_handler::get_popular_exchange_pairs)) + .route("/api/v1/currencies/refresh", post(currency_handler::refresh_exchange_rates)) + .route("/api/v1/family/currency-settings", get(currency_handler::get_family_currency_settings)) + .route("/api/v1/family/currency-settings", put(currency_handler::update_family_currency_settings)) + // 货币管理 API - 增强功能 - .route( - "/api/v1/currencies/all", - get(currency_handler_enhanced::get_all_currencies), - ) - .route( - "/api/v1/currencies/user-settings", - get(currency_handler_enhanced::get_user_currency_settings), - ) - .route( - "/api/v1/currencies/user-settings", - put(currency_handler_enhanced::update_user_currency_settings), - ) - .route( - "/api/v1/currencies/realtime-rates", - get(currency_handler_enhanced::get_realtime_exchange_rates), - ) - .route( - "/api/v1/currencies/rates-detailed", - post(currency_handler_enhanced::get_detailed_batch_rates), - ) - .route( - "/api/v1/currencies/manual-overrides", - get(currency_handler_enhanced::get_manual_overrides), - ) + .route("/api/v1/currencies/all", get(currency_handler_enhanced::get_all_currencies)) + .route("/api/v1/currencies/user-settings", get(currency_handler_enhanced::get_user_currency_settings)) + .route("/api/v1/currencies/user-settings", put(currency_handler_enhanced::update_user_currency_settings)) + .route("/api/v1/currencies/realtime-rates", get(currency_handler_enhanced::get_realtime_exchange_rates)) + .route("/api/v1/currencies/rates-detailed", post(currency_handler_enhanced::get_detailed_batch_rates)) + .route("/api/v1/currencies/manual-overrides", get(currency_handler_enhanced::get_manual_overrides)) // 保留 GET 语义,去除临时 POST 兼容,前端统一改为 GET - .route( - "/api/v1/currencies/crypto-prices", - get(currency_handler_enhanced::get_crypto_prices), - ) - .route( - "/api/v1/currencies/convert-any", - post(currency_handler_enhanced::convert_currency), - ) - .route( - "/api/v1/currencies/manual-refresh", - post(currency_handler_enhanced::manual_refresh_rates), - ) + .route("/api/v1/currencies/crypto-prices", get(currency_handler_enhanced::get_crypto_prices)) + .route("/api/v1/currencies/convert-any", post(currency_handler_enhanced::convert_currency)) + .route("/api/v1/currencies/manual-refresh", post(currency_handler_enhanced::manual_refresh_rates)) + // 标签管理 API(Phase 1 最小集) .route("/api/v1/tags", get(tag_handler::list_tags)) .route("/api/v1/tags", post(tag_handler::create_tag)) @@ -503,32 +361,16 @@ async fn main() -> Result<(), Box> { .route("/api/v1/tags/:id", delete(tag_handler::delete_tag)) .route("/api/v1/tags/merge", post(tag_handler::merge_tags)) .route("/api/v1/tags/summary", get(tag_handler::tag_summary)) + // 分类管理 API(最小可用) .route("/api/v1/categories", get(category_handler::list_categories)) - .route( - "/api/v1/categories", - post(category_handler::create_category), - ) - .route( - "/api/v1/categories/:id", - put(category_handler::update_category), - ) - .route( - "/api/v1/categories/:id", - delete(category_handler::delete_category), - ) - .route( - "/api/v1/categories/reorder", - post(category_handler::reorder_categories), - ) - .route( - "/api/v1/categories/import-template", - post(category_handler::import_template), - ) - .route( - "/api/v1/categories/import", - post(category_handler::batch_import_templates), - ) + .route("/api/v1/categories", post(category_handler::create_category)) + .route("/api/v1/categories/:id", put(category_handler::update_category)) + .route("/api/v1/categories/:id", delete(category_handler::delete_category)) + .route("/api/v1/categories/reorder", post(category_handler::reorder_categories)) + .route("/api/v1/categories/import-template", post(category_handler::import_template)) + .route("/api/v1/categories/import", post(category_handler::batch_import_templates)) + // 静态文件 .route("/static/icons/*path", get(serve_icon)); @@ -538,10 +380,7 @@ async fn main() -> Result<(), Box> { .route("/api/v1/families/:id/export", get(export_data)) .route("/api/v1/families/:id/activity-logs", get(activity_logs)) .route("/api/v1/families/:id/settings", get(family_settings)) - .route( - "/api/v1/families/:id/advanced-settings", - get(advanced_settings), - ) + .route("/api/v1/families/:id/advanced-settings", get(advanced_settings)) .route("/api/v1/export/data", post(export_data)) .route("/api/v1/activity/logs", get(activity_logs)) // 简化演示入口 @@ -554,14 +393,8 @@ async fn main() -> Result<(), Box> { #[cfg(feature = "demo_endpoints")] let app = app .route("/api/v1/families/:id/audit-logs", get(get_audit_logs)) - .route( - "/api/v1/families/:id/audit-logs/export", - get(export_audit_logs), - ) - .route( - "/api/v1/families/:id/audit-logs/cleanup", - post(cleanup_audit_logs), - ); + .route("/api/v1/families/:id/audit-logs/export", get(export_audit_logs)) + .route("/api/v1/families/:id/audit-logs/cleanup", post(cleanup_audit_logs)); let app = app .layer( @@ -576,7 +409,7 @@ async fn main() -> Result<(), Box> { let port = std::env::var("API_PORT").unwrap_or_else(|_| "8012".to_string()); let addr: SocketAddr = format!("{}:{}", host, port).parse()?; let listener = TcpListener::bind(addr).await?; - + info!("🌐 Server running at http://{}", addr); info!("🔌 WebSocket endpoint: ws://{}/ws?token=", addr); info!(""); @@ -604,43 +437,32 @@ async fn main() -> Result<(), Box> { info!(" - Use Authorization header with 'Bearer ' for authenticated requests"); info!(" - WebSocket requires token in query parameter"); info!(" - All timestamps are in UTC"); - + axum::serve(listener, app).await?; - + Ok(()) } /// 健康检查接口(扩展:模式/近期指标) async fn health_check(State(state): State) -> Json { // 运行模式:从 PID 标记或环境变量推断(最佳努力) - let mode = std::fs::read_to_string(".pids/api.mode") - .ok() - .unwrap_or_else(|| { - std::env::var("CORS_DEV") - .map(|v| { - if v == "1" { - "dev".into() - } else { - "safe".into() - } - }) - .unwrap_or_else(|_| "safe".into()) - }); + let mode = std::fs::read_to_string(".pids/api.mode").ok().unwrap_or_else(|| { + std::env::var("CORS_DEV").map(|v| if v == "1" { "dev".into() } else { "safe".into() }).unwrap_or_else(|_| "safe".into()) + }); // 轻量指标(允许失败,不影响健康响应) - let latest_updated_at = sqlx::query(r#"SELECT MAX(updated_at) AS ts FROM exchange_rates"#) - .fetch_one(&state.pool) - .await - .ok() - .and_then(|row| row.try_get::, _>("ts").ok()) - .map(|dt| dt.to_rfc3339()); + let latest_updated_at = sqlx::query( + r#"SELECT MAX(updated_at) AS ts FROM exchange_rates"# + ) + .fetch_one(&state.pool) + .await + .ok() + .and_then(|row| row.try_get::, _>("ts").ok()) + .map(|dt| dt.to_rfc3339()); - let todays_rows = - sqlx::query(r#"SELECT COUNT(*) AS c FROM exchange_rates WHERE date = CURRENT_DATE"#) - .fetch_one(&state.pool) - .await - .ok() - .and_then(|row| row.try_get::("c").ok()) - .unwrap_or(0); + let todays_rows = sqlx::query(r#"SELECT COUNT(*) AS c FROM exchange_rates WHERE date = CURRENT_DATE"#) + .fetch_one(&state.pool).await.ok() + .and_then(|row| row.try_get::("c").ok()) + .unwrap_or(0); let manual_active = sqlx::query( r#"SELECT COUNT(*) AS c FROM exchange_rates @@ -656,46 +478,16 @@ async fn health_check(State(state): State) -> Json .and_then(|row| row.try_get::("c").ok()) .unwrap_or(0); - // Detailed hash distribution (best-effort; ignore errors) - let (b2a, b2b, b2y, a2id) = if let Ok(row) = sqlx::query( - "SELECT \ - COUNT(*) FILTER (WHERE password_hash LIKE '$2a$%') AS b2a,\ - COUNT(*) FILTER (WHERE password_hash LIKE '$2b$%') AS b2b,\ - COUNT(*) FILTER (WHERE password_hash LIKE '$2y$%') AS b2y,\ - COUNT(*) FILTER (WHERE password_hash LIKE '$argon2id$%') AS a2id\ - FROM users", - ) - .fetch_one(&state.pool) - .await - { - use sqlx::Row; - ( - row.try_get::("b2a").unwrap_or(0), - row.try_get::("b2b").unwrap_or(0), - row.try_get::("b2y").unwrap_or(0), - row.try_get::("a2id").unwrap_or(0), - ) - } else { - (0, 0, 0, 0) - }; - Json(json!({ "status": "healthy", "service": "jive-money-api", "mode": mode.trim(), - "build": { - "commit": option_env!("GIT_COMMIT").unwrap_or("unknown"), - "time": option_env!("BUILD_TIME").unwrap_or("unknown"), - "rustc": option_env!("RUSTC_VERSION").unwrap_or("unknown"), - "version": env!("CARGO_PKG_VERSION") - }, "features": { "websocket": true, "database": true, "auth": true, "ledgers": true, - "redis": state.redis.is_some(), - "export_stream": cfg!(feature = "export_stream") + "redis": state.redis.is_some() }, "metrics": { "exchange_rates": { @@ -703,25 +495,6 @@ async fn health_check(State(state): State) -> Json "todays_rows": todays_rows, "manual_overrides_active": manual_active, "manual_overrides_expired": manual_expired - }, - "hash_distribution": { - "bcrypt": {"2a": b2a, "2b": b2b, "2y": b2y}, - "argon2id": a2id - }, - "rehash": { - "enabled": std::env::var("REHASH_ON_LOGIN").map(|v| !matches!(v.as_str(), "0" | "false" | "FALSE")).unwrap_or(true), - "count": state.metrics.get_rehash_count(), - "fail_count": state.metrics.get_rehash_fail() - }, - "auth_login": { - "fail": state.metrics.get_login_fail(), - "inactive": state.metrics.get_login_inactive() - }, - "export": { - "requests_stream": state.metrics.get_export_counts().0, - "requests_buffered": state.metrics.get_export_counts().1, - "rows_stream": state.metrics.get_export_counts().2, - "rows_buffered": state.metrics.get_export_counts().3 } }, "timestamp": chrono::Utc::now().to_rfc3339() diff --git a/jive-api/src/main_simple.rs b/jive-api/src/main_simple.rs index b0637824..7595ca97 100644 --- a/jive-api/src/main_simple.rs +++ b/jive-api/src/main_simple.rs @@ -1,12 +1,12 @@ //! Jive Money API Server - Simple Version -//! +//! //! 测试版本,不连接数据库,返回模拟数据 use axum::{response::Json, routing::get, Router}; -use jive_money_api::middleware::cors::create_cors_layer; use serde_json::json; use std::net::SocketAddr; use tokio::net::TcpListener; +use jive_money_api::middleware::cors::create_cors_layer; use tracing::info; // tracing_subscriber is used via fully-qualified path below // chrono is referenced via fully-qualified path below @@ -33,16 +33,16 @@ async fn main() -> Result<(), Box> { let port = std::env::var("API_PORT").unwrap_or_else(|_| "8012".to_string()); let addr: SocketAddr = format!("127.0.0.1:{}", port).parse()?; let listener = TcpListener::bind(addr).await?; - + info!("🌐 Server running at http://{}", addr); info!("📋 API Endpoints:"); info!(" GET /health - 健康检查"); info!(" GET /api/v1/templates/list - 获取模板列表"); info!(" GET /api/v1/icons/list - 获取图标列表"); info!("💡 Test with: curl http://{}/api/v1/templates/list", addr); - + axum::serve(listener, app).await?; - + Ok(()) } diff --git a/jive-api/src/main_simple_ws.rs b/jive-api/src/main_simple_ws.rs index 12975dc1..2527f020 100644 --- a/jive-api/src/main_simple_ws.rs +++ b/jive-api/src/main_simple_ws.rs @@ -1,39 +1,36 @@ //! 简化的主程序,用于测试基础功能 //! 不包含WebSocket,仅包含核心API -use axum::{ - http::StatusCode, - response::Json, - routing::{delete, get, post, put}, - Router, -}; -use jive_money_api::middleware::cors::create_cors_layer; -use jive_money_api::{AppMetrics, AppState}; +use axum::{http::StatusCode, response::Json, routing::{get, post, put, delete}, Router}; use serde_json::json; use sqlx::postgres::PgPoolOptions; use std::net::SocketAddr; use tokio::net::TcpListener; use tower::ServiceBuilder; -use tower_http::trace::TraceLayer; -use tracing::{error, info, warn}; +use tower_http::{ + trace::TraceLayer, +}; +use jive_money_api::middleware::cors::create_cors_layer; +use tracing::{info, warn, error}; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; use jive_money_api::handlers; // WebSocket模块暂时不包含,避免编译错误 +use handlers::template_handler::*; use handlers::accounts::*; -use handlers::auth as auth_handlers; +use handlers::transactions::*; use handlers::payees::*; use handlers::rules::*; -use handlers::template_handler::*; -use handlers::transactions::*; +use handlers::auth as auth_handlers; #[tokio::main] async fn main() -> Result<(), Box> { // 初始化日志 tracing_subscriber::registry() .with( - tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| "info".into()), + tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| "info".into()), ) .with(tracing_subscriber::fmt::layer()) .init(); @@ -43,12 +40,9 @@ async fn main() -> Result<(), Box> { // 数据库连接 let database_url = std::env::var("DATABASE_URL") .unwrap_or_else(|_| "postgresql://jive:jive_password@localhost/jive_money".to_string()); - - info!( - "📦 Connecting to database: {}", - database_url.replace("jive_password", "***") - ); - + + info!("📦 Connecting to database: {}", database_url.replace("jive_password", "***")); + let pool = match PgPoolOptions::new() .max_connections(10) .connect(&database_url) @@ -83,18 +77,18 @@ async fn main() -> Result<(), Box> { // 健康检查 .route("/health", get(health_check)) .route("/", get(api_info)) + // 分类模板API .route("/api/v1/templates/list", get(get_templates)) .route("/api/v1/icons/list", get(get_icons)) .route("/api/v1/templates/updates", get(get_template_updates)) .route("/api/v1/templates/usage", post(submit_usage)) + // 超级管理员API .route("/api/v1/admin/templates", post(create_template)) .route("/api/v1/admin/templates/:template_id", put(update_template)) - .route( - "/api/v1/admin/templates/:template_id", - delete(delete_template), - ) + .route("/api/v1/admin/templates/:template_id", delete(delete_template)) + // 账户管理API .route("/api/v1/accounts", get(list_accounts)) .route("/api/v1/accounts", post(create_account)) @@ -102,20 +96,16 @@ async fn main() -> Result<(), Box> { .route("/api/v1/accounts/:id", put(update_account)) .route("/api/v1/accounts/:id", delete(delete_account)) .route("/api/v1/accounts/statistics", get(get_account_statistics)) + // 交易管理API .route("/api/v1/transactions", get(list_transactions)) .route("/api/v1/transactions", post(create_transaction)) .route("/api/v1/transactions/:id", get(get_transaction)) .route("/api/v1/transactions/:id", put(update_transaction)) .route("/api/v1/transactions/:id", delete(delete_transaction)) - .route( - "/api/v1/transactions/bulk", - post(bulk_transaction_operations), - ) - .route( - "/api/v1/transactions/statistics", - get(get_transaction_statistics), - ) + .route("/api/v1/transactions/bulk", post(bulk_transaction_operations)) + .route("/api/v1/transactions/statistics", get(get_transaction_statistics)) + // 收款人管理API .route("/api/v1/payees", get(list_payees)) .route("/api/v1/payees", post(create_payee)) @@ -125,6 +115,7 @@ async fn main() -> Result<(), Box> { .route("/api/v1/payees/suggestions", get(get_payee_suggestions)) .route("/api/v1/payees/statistics", get(get_payee_statistics)) .route("/api/v1/payees/merge", post(merge_payees)) + // 规则引擎API .route("/api/v1/rules", get(list_rules)) .route("/api/v1/rules", post(create_rule)) @@ -132,35 +123,30 @@ async fn main() -> Result<(), Box> { .route("/api/v1/rules/:id", put(update_rule)) .route("/api/v1/rules/:id", delete(delete_rule)) .route("/api/v1/rules/execute", post(execute_rules)) + // 认证API .route("/api/v1/auth/register", post(auth_handlers::register)) .route("/api/v1/auth/login", post(auth_handlers::login)) .route("/api/v1/auth/refresh", post(auth_handlers::refresh_token)) .route("/api/v1/auth/user", get(auth_handlers::get_current_user)) .route("/api/v1/auth/user", put(auth_handlers::update_user)) - .route( - "/api/v1/auth/password", - post(auth_handlers::change_password), - ) + .route("/api/v1/auth/password", post(auth_handlers::change_password)) + // 静态文件 .route("/static/icons/*path", get(serve_icon)) + .layer( ServiceBuilder::new() .layer(TraceLayer::new_for_http()) .layer(cors), ) - .with_state(AppState { - pool: pool.clone(), - ws_manager: None, - redis: None, - metrics: AppMetrics::new(), - }); + .with_state(pool); // 启动服务器 let port = std::env::var("API_PORT").unwrap_or_else(|_| "8012".to_string()); let addr: SocketAddr = format!("127.0.0.1:{}", port).parse()?; let listener = TcpListener::bind(addr).await?; - + info!("🌐 Server running at http://{}", addr); info!("📋 API Documentation:"); info!(" Authentication API:"); @@ -177,9 +163,9 @@ async fn main() -> Result<(), Box> { info!(" /api/v1/payees"); info!(" /api/v1/rules"); info!(" /api/v1/templates"); - + axum::serve(listener, app).await?; - + Ok(()) } diff --git a/jive-api/src/middleware/permission.rs b/jive-api/src/middleware/permission.rs index 4c02c90a..66a480cf 100644 --- a/jive-api/src/middleware/permission.rs +++ b/jive-api/src/middleware/permission.rs @@ -1,4 +1,9 @@ -use axum::{extract::Request, http::StatusCode, middleware::Next, response::Response}; +use axum::{ + extract::Request, + http::StatusCode, + middleware::Next, + response::Response, +}; use std::collections::HashMap; use std::sync::Arc; use std::time::{Duration, Instant}; @@ -13,12 +18,7 @@ use crate::{ /// 权限中间件 - 检查单个权限 pub async fn require_permission( required: Permission, -) -> impl Fn( - Request, - Next, -) -> std::pin::Pin< - Box> + Send>, -> + Clone { +) -> impl Fn(Request, Next) -> std::pin::Pin> + Send>> + Clone { move |request: Request, next: Next| { Box::pin(async move { // 从request extensions获取ServiceContext @@ -26,12 +26,12 @@ pub async fn require_permission( .extensions() .get::() .ok_or(StatusCode::UNAUTHORIZED)?; - + // 检查权限 if !context.can_perform(required) { return Err(StatusCode::FORBIDDEN); } - + Ok(next.run(request).await) }) } @@ -40,12 +40,7 @@ pub async fn require_permission( /// 多权限中间件 - 检查多个权限(任一满足) pub async fn require_any_permission( permissions: Vec, -) -> impl Fn( - Request, - Next, -) -> std::pin::Pin< - Box> + Send>, -> + Clone { +) -> impl Fn(Request, Next) -> std::pin::Pin> + Send>> + Clone { move |request: Request, next: Next| { let value = permissions.clone(); Box::pin(async move { @@ -53,14 +48,14 @@ pub async fn require_any_permission( .extensions() .get::() .ok_or(StatusCode::UNAUTHORIZED)?; - + // 检查是否有任一权限 let has_permission = value.iter().any(|p| context.can_perform(*p)); - + if !has_permission { return Err(StatusCode::FORBIDDEN); } - + Ok(next.run(request).await) }) } @@ -69,12 +64,7 @@ pub async fn require_any_permission( /// 多权限中间件 - 检查多个权限(全部满足) pub async fn require_all_permissions( permissions: Vec, -) -> impl Fn( - Request, - Next, -) -> std::pin::Pin< - Box> + Send>, -> + Clone { +) -> impl Fn(Request, Next) -> std::pin::Pin> + Send>> + Clone { move |request: Request, next: Next| { let value = permissions.clone(); Box::pin(async move { @@ -82,14 +72,14 @@ pub async fn require_all_permissions( .extensions() .get::() .ok_or(StatusCode::UNAUTHORIZED)?; - + // 检查是否有所有权限 let has_all_permissions = value.iter().all(|p| context.can_perform(*p)); - + if !has_all_permissions { return Err(StatusCode::FORBIDDEN); } - + Ok(next.run(request).await) }) } @@ -98,19 +88,14 @@ pub async fn require_all_permissions( /// 角色中间件 - 检查最低角色要求 pub async fn require_minimum_role( minimum_role: MemberRole, -) -> impl Fn( - Request, - Next, -) -> std::pin::Pin< - Box> + Send>, -> + Clone { +) -> impl Fn(Request, Next) -> std::pin::Pin> + Send>> + Clone { move |request: Request, next: Next| { Box::pin(async move { let context = request .extensions() .get::() .ok_or(StatusCode::UNAUTHORIZED)?; - + // 检查角色级别 let role_level = match context.role { MemberRole::Owner => 4, @@ -118,48 +103,54 @@ pub async fn require_minimum_role( MemberRole::Member => 2, MemberRole::Viewer => 1, }; - + let required_level = match minimum_role { MemberRole::Owner => 4, MemberRole::Admin => 3, MemberRole::Member => 2, MemberRole::Viewer => 1, }; - + if role_level < required_level { return Err(StatusCode::FORBIDDEN); } - + Ok(next.run(request).await) }) } } /// Owner专用中间件 -pub async fn require_owner(request: Request, next: Next) -> Result { +pub async fn require_owner( + request: Request, + next: Next, +) -> Result { let context = request .extensions() .get::() .ok_or(StatusCode::UNAUTHORIZED)?; - + if context.role != MemberRole::Owner { return Err(StatusCode::FORBIDDEN); } - + Ok(next.run(request).await) } /// Admin及以上中间件 -pub async fn require_admin_or_owner(request: Request, next: Next) -> Result { +pub async fn require_admin_or_owner( + request: Request, + next: Next, +) -> Result { let context = request .extensions() .get::() .ok_or(StatusCode::UNAUTHORIZED)?; - + if !matches!(context.role, MemberRole::Owner | MemberRole::Admin) { return Err(StatusCode::FORBIDDEN); } - + Ok(next.run(request).await) } @@ -179,29 +170,29 @@ impl PermissionCache { ttl: Duration::from_secs(ttl_seconds), } } - + pub async fn get(&self, user_id: Uuid, family_id: Uuid) -> Option> { let cache = self.cache.read().await; - + if let Some((permissions, cached_at)) = cache.get(&(user_id, family_id)) { if cached_at.elapsed() < self.ttl { return Some(permissions.clone()); } } - + None } - + pub async fn set(&self, user_id: Uuid, family_id: Uuid, permissions: Vec) { let mut cache = self.cache.write().await; cache.insert((user_id, family_id), (permissions, Instant::now())); } - + pub async fn invalidate(&self, user_id: Uuid, family_id: Uuid) { let mut cache = self.cache.write().await; cache.remove(&(user_id, family_id)); } - + pub async fn clear(&self) { let mut cache = self.cache.write().await; cache.clear(); @@ -221,15 +212,12 @@ impl PermissionError { pub fn insufficient_permissions(permission: Permission) -> Self { Self { code: "INSUFFICIENT_PERMISSIONS".to_string(), - message: format!( - "You need '{}' permission to perform this action", - permission - ), + message: format!("You need '{}' permission to perform this action", permission), required_permission: Some(permission.to_string()), required_role: None, } } - + pub fn insufficient_role(role: MemberRole) -> Self { Self { code: "INSUFFICIENT_ROLE".to_string(), @@ -256,15 +244,15 @@ pub async fn check_resource_permission( ResourceOwnership::OwnedBy(owner_id) => { // 资源所有者或有权限的人可以访问 context.user_id == owner_id || context.can_perform(permission) - } + }, ResourceOwnership::SharedInFamily(family_id) => { // 必须是Family成员且有权限 context.family_id == family_id && context.can_perform(permission) - } + }, ResourceOwnership::Public => { // 公开资源,只要认证即可 true - } + }, } } @@ -312,11 +300,11 @@ impl PermissionGroup { ], } } - + pub fn check_any(&self, context: &ServiceContext) -> bool { self.permissions().iter().any(|p| context.can_perform(*p)) } - + pub fn check_all(&self, context: &ServiceContext) -> bool { self.permissions().iter().all(|p| context.can_perform(*p)) } @@ -325,7 +313,7 @@ impl PermissionGroup { #[cfg(test)] mod tests { use super::*; - + #[test] fn test_permission_group() { let context = ServiceContext::new( @@ -336,26 +324,26 @@ mod tests { "test@example.com".to_string(), None, ); - + let group = PermissionGroup::AccountManagement; assert!(group.check_any(&context)); // Has some account permissions assert!(!group.check_all(&context)); // Doesn't have all } - + #[tokio::test] async fn test_permission_cache() { let cache = PermissionCache::new(5); let user_id = Uuid::new_v4(); let family_id = Uuid::new_v4(); let permissions = vec![Permission::ViewAccounts]; - + // Set cache cache.set(user_id, family_id, permissions.clone()).await; - + // Get from cache let cached = cache.get(user_id, family_id).await; assert_eq!(cached, Some(permissions)); - + // Invalidate cache.invalidate(user_id, family_id).await; let cached = cache.get(user_id, family_id).await; diff --git a/jive-api/src/models/membership.rs b/jive-api/src/models/membership.rs index 4f0b5dd5..ad351393 100644 --- a/jive-api/src/models/membership.rs +++ b/jive-api/src/models/membership.rs @@ -109,7 +109,8 @@ impl TryFrom for MemberRole { type Error = String; fn try_from(value: String) -> Result { - MemberRole::from_str_name(&value).ok_or_else(|| format!("Invalid role: {}", value)) + MemberRole::from_str_name(&value) + .ok_or_else(|| format!("Invalid role: {}", value)) } } @@ -122,7 +123,7 @@ mod tests { let family_id = Uuid::new_v4(); let user_id = Uuid::new_v4(); let member = FamilyMember::new(family_id, user_id, MemberRole::Member, None); - + assert_eq!(member.family_id, family_id); assert_eq!(member.user_id, user_id); assert_eq!(member.role, MemberRole::Member); @@ -135,7 +136,7 @@ mod tests { let family_id = Uuid::new_v4(); let user_id = Uuid::new_v4(); let mut member = FamilyMember::new(family_id, user_id, MemberRole::Member, None); - + member.change_role(MemberRole::Admin); assert_eq!(member.role, MemberRole::Admin); assert_eq!(member.permissions, MemberRole::Admin.default_permissions()); @@ -146,10 +147,10 @@ mod tests { let family_id = Uuid::new_v4(); let user_id = Uuid::new_v4(); let mut member = FamilyMember::new(family_id, user_id, MemberRole::Viewer, None); - + member.grant_permission(Permission::CreateTransactions); assert!(member.permissions.contains(&Permission::CreateTransactions)); - + member.revoke_permission(Permission::CreateTransactions); assert!(!member.permissions.contains(&Permission::CreateTransactions)); } @@ -159,11 +160,11 @@ mod tests { let family_id = Uuid::new_v4(); let user_id = Uuid::new_v4(); let mut member = FamilyMember::new(family_id, user_id, MemberRole::Member, None); - + assert!(member.can_perform(Permission::ViewTransactions)); assert!(member.can_perform(Permission::CreateTransactions)); assert!(!member.can_perform(Permission::DeleteFamily)); - + member.deactivate(); assert!(!member.can_perform(Permission::ViewTransactions)); } @@ -172,17 +173,17 @@ mod tests { fn test_can_manage_member() { let family_id = Uuid::new_v4(); let user_id = Uuid::new_v4(); - + let owner = FamilyMember::new(family_id, user_id, MemberRole::Owner, None); assert!(owner.can_manage_member(MemberRole::Owner)); assert!(owner.can_manage_member(MemberRole::Admin)); assert!(owner.can_manage_member(MemberRole::Member)); - + let admin = FamilyMember::new(family_id, user_id, MemberRole::Admin, None); assert!(!admin.can_manage_member(MemberRole::Owner)); assert!(admin.can_manage_member(MemberRole::Admin)); assert!(admin.can_manage_member(MemberRole::Member)); - + let member = FamilyMember::new(family_id, user_id, MemberRole::Member, None); assert!(!member.can_manage_member(MemberRole::Member)); } diff --git a/jive-api/src/models/mod.rs b/jive-api/src/models/mod.rs index 5ad04b27..f510732b 100644 --- a/jive-api/src/models/mod.rs +++ b/jive-api/src/models/mod.rs @@ -17,7 +17,9 @@ pub use invitation::{ InvitationStatus, }; #[allow(unused_imports)] -pub use membership::{CreateMemberRequest, FamilyMember, MemberWithUserInfo, UpdateMemberRequest}; +pub use membership::{ + CreateMemberRequest, FamilyMember, MemberWithUserInfo, UpdateMemberRequest, +}; #[allow(unused_imports)] pub use permission::{MemberRole, Permission}; diff --git a/jive-api/src/models/permission.rs b/jive-api/src/models/permission.rs index 5591da00..581cde0b 100644 --- a/jive-api/src/models/permission.rs +++ b/jive-api/src/models/permission.rs @@ -8,36 +8,36 @@ pub enum Permission { ViewFamilyInfo, UpdateFamilyInfo, DeleteFamily, - + // 成员管理权限 ViewMembers, InviteMembers, RemoveMembers, UpdateMemberRoles, - + // 账户管理权限 ViewAccounts, CreateAccounts, EditAccounts, DeleteAccounts, - + // 交易管理权限 ViewTransactions, CreateTransactions, EditTransactions, DeleteTransactions, BulkEditTransactions, - + // 分类和预算权限 ViewCategories, ManageCategories, ViewBudgets, ManageBudgets, - + // 报表和数据权限 ViewReports, ExportData, - + // 系统管理权限 ViewAuditLog, ManageIntegrations, @@ -237,10 +237,7 @@ mod tests { #[test] fn test_permission_from_str() { - assert_eq!( - Permission::from_str_name("ViewFamilyInfo"), - Some(Permission::ViewFamilyInfo) - ); + assert_eq!(Permission::from_str_name("ViewFamilyInfo"), Some(Permission::ViewFamilyInfo)); assert_eq!(Permission::from_str_name("InvalidPermission"), None); } diff --git a/jive-api/src/services/audit_service.rs b/jive-api/src/services/audit_service.rs index c8026046..e90d5618 100644 --- a/jive-api/src/services/audit_service.rs +++ b/jive-api/src/services/audit_service.rs @@ -14,7 +14,7 @@ impl AuditService { pub fn new(pool: PgPool) -> Self { Self { pool } } - + pub async fn log_action( &self, family_id: Uuid, @@ -32,7 +32,7 @@ impl AuditService { ) .with_values(request.old_values, request.new_values) .with_request_info(ip_address, user_agent); - + sqlx::query( r#" INSERT INTO family_audit_logs ( @@ -40,7 +40,7 @@ impl AuditService { old_values, new_values, ip_address, user_agent, created_at ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) - "#, + "# ) .bind(log.id) .bind(log.family_id) @@ -55,7 +55,7 @@ impl AuditService { .bind(log.created_at) .execute(&self.pool) .await?; - + Ok(()) } @@ -85,7 +85,7 @@ impl AuditService { old_values, new_values, ip_address, user_agent, created_at ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) - "#, + "# ) .bind(log.id) .bind(log.family_id) @@ -103,72 +103,74 @@ impl AuditService { Ok(log.id) } - + pub async fn get_audit_logs( &self, filter: AuditLogFilter, ) -> Result, ServiceError> { - let mut query = String::from("SELECT * FROM family_audit_logs WHERE 1=1"); + let mut query = String::from( + "SELECT * FROM family_audit_logs WHERE 1=1" + ); let mut binds = vec![]; let mut bind_idx = 1; - + if let Some(family_id) = filter.family_id { query.push_str(&format!(" AND family_id = ${}", bind_idx)); binds.push(family_id.to_string()); bind_idx += 1; } - + if let Some(user_id) = filter.user_id { query.push_str(&format!(" AND user_id = ${}", bind_idx)); binds.push(user_id.to_string()); bind_idx += 1; } - + if let Some(action) = filter.action { query.push_str(&format!(" AND action = ${}", bind_idx)); binds.push(action.to_string()); bind_idx += 1; } - + if let Some(entity_type) = filter.entity_type { query.push_str(&format!(" AND entity_type = ${}", bind_idx)); binds.push(entity_type); bind_idx += 1; } - + if let Some(from_date) = filter.from_date { query.push_str(&format!(" AND created_at >= ${}", bind_idx)); binds.push(from_date.to_rfc3339()); bind_idx += 1; } - + if let Some(to_date) = filter.to_date { query.push_str(&format!(" AND created_at <= ${}", bind_idx)); binds.push(to_date.to_rfc3339()); // bind_idx += 1; // Last increment not needed } - + query.push_str(" ORDER BY created_at DESC"); - + if let Some(limit) = filter.limit { query.push_str(&format!(" LIMIT {}", limit)); } - + if let Some(offset) = filter.offset { query.push_str(&format!(" OFFSET {}", offset)); } - + // Execute dynamic query let mut query_builder = sqlx::query_as::<_, AuditLog>(&query); for bind in binds { query_builder = query_builder.bind(bind); } - + let logs = query_builder.fetch_all(&self.pool).await?; - + Ok(logs) } - + pub async fn log_family_created( &self, family_id: Uuid, @@ -176,10 +178,10 @@ impl AuditService { family_name: &str, ) -> Result<(), ServiceError> { let log = AuditLog::log_family_created(family_id, user_id, family_name); - + self.insert_log(log).await } - + pub async fn log_member_added( &self, family_id: Uuid, @@ -188,10 +190,10 @@ impl AuditService { role: &str, ) -> Result<(), ServiceError> { let log = AuditLog::log_member_added(family_id, actor_id, member_id, role); - + self.insert_log(log).await } - + pub async fn log_member_removed( &self, family_id: Uuid, @@ -205,10 +207,10 @@ impl AuditService { "member".to_string(), Some(member_id), ); - + self.insert_log(log).await } - + pub async fn log_role_changed( &self, family_id: Uuid, @@ -217,11 +219,17 @@ impl AuditService { old_role: &str, new_role: &str, ) -> Result<(), ServiceError> { - let log = AuditLog::log_role_changed(family_id, actor_id, member_id, old_role, new_role); - + let log = AuditLog::log_role_changed( + family_id, + actor_id, + member_id, + old_role, + new_role, + ); + self.insert_log(log).await } - + pub async fn log_invitation_sent( &self, family_id: Uuid, @@ -229,12 +237,16 @@ impl AuditService { invitation_id: Uuid, invitee_email: &str, ) -> Result<(), ServiceError> { - let log = - AuditLog::log_invitation_sent(family_id, inviter_id, invitation_id, invitee_email); - + let log = AuditLog::log_invitation_sent( + family_id, + inviter_id, + invitation_id, + invitee_email, + ); + self.insert_log(log).await } - + async fn insert_log(&self, log: AuditLog) -> Result<(), ServiceError> { sqlx::query( r#" @@ -243,7 +255,7 @@ impl AuditService { old_values, new_values, ip_address, user_agent, created_at ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) - "#, + "# ) .bind(log.id) .bind(log.family_id) @@ -258,32 +270,30 @@ impl AuditService { .bind(log.created_at) .execute(&self.pool) .await?; - + Ok(()) } - + pub async fn export_audit_report( &self, family_id: Uuid, from_date: DateTime, to_date: DateTime, ) -> Result { - let logs = self - .get_audit_logs(AuditLogFilter { - family_id: Some(family_id), - user_id: None, - action: None, - entity_type: None, - from_date: Some(from_date), - to_date: Some(to_date), - limit: None, - offset: None, - }) - .await?; - + let logs = self.get_audit_logs(AuditLogFilter { + family_id: Some(family_id), + user_id: None, + action: None, + entity_type: None, + from_date: Some(from_date), + to_date: Some(to_date), + limit: None, + offset: None, + }).await?; + // Generate CSV report let mut csv = String::from("时间,用户,操作,实体类型,实体ID,旧值,新值,IP地址\n"); - + for log in logs { csv.push_str(&format!( "{},{},{},{},{},{},{},{}\n", @@ -297,7 +307,7 @@ impl AuditService { log.ip_address.unwrap_or_default(), )); } - + Ok(csv) } } diff --git a/jive-api/src/services/auth_service.rs b/jive-api/src/services/auth_service.rs index de39b2b7..10a247a2 100644 --- a/jive-api/src/services/auth_service.rs +++ b/jive-api/src/services/auth_service.rs @@ -6,7 +6,10 @@ use chrono::Utc; use sqlx::PgPool; use uuid::Uuid; -use crate::models::{family::CreateFamilyRequest, permission::MemberRole}; +use crate::models::{ + family::CreateFamilyRequest, + permission::MemberRole, +}; use super::{FamilyService, ServiceContext, ServiceError}; @@ -48,28 +51,27 @@ impl AuthService { pub fn new(pool: PgPool) -> Self { Self { pool } } - + pub async fn register_with_family( &self, request: RegisterRequest, ) -> Result { // Check if email already exists - let exists = - sqlx::query_scalar::<_, bool>("SELECT EXISTS(SELECT 1 FROM users WHERE email = $1)") - .bind(&request.email) - .fetch_one(&self.pool) - .await?; - + let exists = sqlx::query_scalar::<_, bool>( + "SELECT EXISTS(SELECT 1 FROM users WHERE email = $1)" + ) + .bind(&request.email) + .fetch_one(&self.pool) + .await?; + if exists { - return Err(ServiceError::Conflict( - "Email already registered".to_string(), - )); + return Err(ServiceError::Conflict("Email already registered".to_string())); } // If username provided, ensure uniqueness (case-insensitive) if let Some(ref username) = request.username { let username_exists = sqlx::query_scalar::<_, bool>( - "SELECT EXISTS(SELECT 1 FROM users WHERE LOWER(username) = LOWER($1))", + "SELECT EXISTS(SELECT 1 FROM users WHERE LOWER(username) = LOWER($1))" ) .bind(username) .fetch_one(&self.pool) @@ -78,23 +80,17 @@ impl AuthService { return Err(ServiceError::Conflict("Username already taken".to_string())); } } - + let mut tx = self.pool.begin().await?; - + // Hash password let password_hash = self.hash_password(&request.password)?; - + // Create user let user_id = Uuid::new_v4(); - let user_name = request.name.clone().unwrap_or_else(|| { - request - .email - .split('@') - .next() - .unwrap_or("用户") - .to_string() - }); - + let user_name = request.name.clone() + .unwrap_or_else(|| request.email.split('@').next().unwrap_or("用户").to_string()); + sqlx::query( r#" INSERT INTO users (id, email, username, name, full_name, password_hash, created_at, updated_at) @@ -111,7 +107,7 @@ impl AuthService { .bind(Utc::now()) .execute(&mut *tx) .await?; - + // Create personal family let family_service = FamilyService::new(self.pool.clone()); let family_request = CreateFamilyRequest { @@ -120,21 +116,21 @@ impl AuthService { timezone: Some("Asia/Shanghai".to_string()), locale: Some("zh-CN".to_string()), }; - + // Note: We need to commit the user first to use FamilyService tx.commit().await?; - - let family = family_service - .create_family(user_id, family_request) - .await?; - + + let family = family_service.create_family(user_id, family_request).await?; + // Update user's current family - sqlx::query("UPDATE users SET current_family_id = $1 WHERE id = $2") - .bind(family.id) - .bind(user_id) - .execute(&self.pool) - .await?; - + sqlx::query( + "UPDATE users SET current_family_id = $1 WHERE id = $2" + ) + .bind(family.id) + .bind(user_id) + .execute(&self.pool) + .await?; + Ok(UserContext { user_id, email: request.email, @@ -147,8 +143,11 @@ impl AuthService { }], }) } - - pub async fn login(&self, request: LoginRequest) -> Result { + + pub async fn login( + &self, + request: LoginRequest, + ) -> Result { // Get user #[derive(sqlx::FromRow)] struct UserRow { @@ -158,22 +157,22 @@ impl AuthService { password_hash: String, current_family_id: Option, } - + let user = sqlx::query_as::<_, UserRow>( r#" SELECT id, email, full_name, password_hash, current_family_id FROM users WHERE email = $1 - "#, + "# ) .bind(&request.email) .fetch_optional(&self.pool) .await? .ok_or_else(|| ServiceError::AuthenticationError("Invalid credentials".to_string()))?; - + // Verify password self.verify_password(&request.password, &user.password_hash)?; - + // Get user's families #[derive(sqlx::FromRow)] struct FamilyRow { @@ -181,7 +180,7 @@ impl AuthService { family_name: String, role: String, } - + let families = sqlx::query_as::<_, FamilyRow>( r#" SELECT @@ -192,12 +191,12 @@ impl AuthService { JOIN family_members fm ON f.id = fm.family_id WHERE fm.user_id = $1 ORDER BY fm.joined_at DESC - "#, + "# ) .bind(user.id) .fetch_all(&self.pool) .await?; - + let family_info: Vec = families .into_iter() .map(|f| FamilyInfo { @@ -206,7 +205,7 @@ impl AuthService { role: MemberRole::from_str_name(&f.role).unwrap_or(MemberRole::Member), }) .collect(); - + Ok(UserContext { user_id: user.id, email: user.email, @@ -215,8 +214,11 @@ impl AuthService { families: family_info, }) } - - pub async fn get_user_context(&self, user_id: Uuid) -> Result { + + pub async fn get_user_context( + &self, + user_id: Uuid, + ) -> Result { #[derive(sqlx::FromRow)] struct UserInfoRow { id: Uuid, @@ -224,26 +226,26 @@ impl AuthService { full_name: Option, current_family_id: Option, } - + let user = sqlx::query_as::<_, UserInfoRow>( r#" SELECT id, email, full_name, current_family_id FROM users WHERE id = $1 - "#, + "# ) .bind(user_id) .fetch_optional(&self.pool) .await? .ok_or_else(|| ServiceError::not_found("User", user_id))?; - + #[derive(sqlx::FromRow)] struct FamilyInfoRow { family_id: Uuid, family_name: String, role: String, } - + let families = sqlx::query_as::<_, FamilyInfoRow>( r#" SELECT @@ -254,12 +256,12 @@ impl AuthService { JOIN family_members fm ON f.id = fm.family_id WHERE fm.user_id = $1 ORDER BY fm.joined_at DESC - "#, + "# ) .bind(user_id) .fetch_all(&self.pool) .await?; - + let family_info: Vec = families .into_iter() .map(|f| FamilyInfo { @@ -268,7 +270,7 @@ impl AuthService { role: MemberRole::from_str_name(&f.role).unwrap_or(MemberRole::Member), }) .collect(); - + Ok(UserContext { user_id: user.id, email: user.email, @@ -277,7 +279,7 @@ impl AuthService { families: family_info, }) } - + pub async fn validate_family_access( &self, user_id: Uuid, @@ -290,7 +292,7 @@ impl AuthService { email: String, full_name: Option, } - + let row = sqlx::query_as::<_, AccessRow>( r#" SELECT @@ -301,19 +303,19 @@ impl AuthService { FROM family_members fm JOIN users u ON fm.user_id = u.id WHERE fm.family_id = $1 AND fm.user_id = $2 - "#, + "# ) .bind(family_id) .bind(user_id) .fetch_optional(&self.pool) .await? .ok_or(ServiceError::PermissionDenied)?; - + let role = MemberRole::from_str_name(&row.role) .ok_or_else(|| ServiceError::ValidationError("Invalid role".to_string()))?; - + let permissions = serde_json::from_value(row.permissions)?; - + Ok(ServiceContext::new( user_id, family_id, @@ -323,21 +325,21 @@ impl AuthService { row.full_name, )) } - + fn hash_password(&self, password: &str) -> Result { let salt = SaltString::generate(&mut OsRng); let argon2 = Argon2::default(); - + argon2 .hash_password(password.as_bytes(), &salt) .map(|hash| hash.to_string()) .map_err(|_e| ServiceError::InternalError) } - + fn verify_password(&self, password: &str, hash: &str) -> Result<(), ServiceError> { let parsed_hash = PasswordHash::new(hash) .map_err(|_| ServiceError::AuthenticationError("Invalid password hash".to_string()))?; - + Argon2::default() .verify_password(password.as_bytes(), &parsed_hash) .map_err(|_| ServiceError::AuthenticationError("Invalid credentials".to_string())) diff --git a/jive-api/src/services/avatar_service.rs b/jive-api/src/services/avatar_service.rs index 292da46f..c5ce109f 100644 --- a/jive-api/src/services/avatar_service.rs +++ b/jive-api/src/services/avatar_service.rs @@ -23,11 +23,11 @@ pub struct AvatarService; impl AvatarService { // 预定义的动物头像集合 const ANIMAL_AVATARS: &'static [&'static str] = &[ - "bear", "cat", "dog", "fox", "koala", "lion", "mouse", "owl", "panda", "penguin", "pig", - "rabbit", "tiger", "wolf", "elephant", "giraffe", "hippo", "monkey", "zebra", "deer", - "squirrel", "bird", + "bear", "cat", "dog", "fox", "koala", "lion", "mouse", "owl", + "panda", "penguin", "pig", "rabbit", "tiger", "wolf", "elephant", + "giraffe", "hippo", "monkey", "zebra", "deer", "squirrel", "bird" ]; - + // 预定义的颜色主题 const COLOR_THEMES: &'static [(&'static str, &'static str)] = &[ ("#FF6B6B", "#FFE3E3"), // 红色系 @@ -43,26 +43,17 @@ impl AvatarService { ("#EC7063", "#FDEAEA"), // 珊瑚色 ("#A569BD", "#F2E9F6"), // 兰花紫 ]; - + // 预定义的抽象图案 const ABSTRACT_PATTERNS: &'static [&'static str] = &[ - "circles", - "squares", - "triangles", - "hexagons", - "waves", - "dots", - "stripes", - "zigzag", - "spiral", - "grid", - "diamonds", + "circles", "squares", "triangles", "hexagons", "waves", + "dots", "stripes", "zigzag", "spiral", "grid", "diamonds" ]; - + /// 为新用户生成随机头像 pub fn generate_random_avatar(user_name: &str, user_email: &str) -> Avatar { let mut rng = rand::thread_rng(); - + // 随机选择头像风格 let style = match rand::random::() % 4 { 0 => AvatarStyle::Initials, @@ -71,63 +62,60 @@ impl AvatarService { 3 => AvatarStyle::Gradient, _ => AvatarStyle::Pattern, }; - + // 随机选择颜色主题 let (color, background) = Self::COLOR_THEMES .choose(&mut rng) .unwrap_or(&("#4ECDC4", "#E3FFF8")); - + // 根据风格生成URL let url = match style { AvatarStyle::Initials => { // 使用用户名首字母 let initials = Self::get_initials(user_name); - format!( - "https://ui-avatars.com/api/?name={}&background={}&color={}&size=256", - initials, + format!("https://ui-avatars.com/api/?name={}&background={}&color={}&size=256", + initials, &background[1..], // 去掉#号 &color[1..] ) - } + }, AvatarStyle::Animal => { // 使用动物头像 - let animal = Self::ANIMAL_AVATARS.choose(&mut rng).unwrap_or(&"panda"); - format!( - "https://api.dicebear.com/7.x/animalz/svg?seed={}&backgroundColor={}", + let animal = Self::ANIMAL_AVATARS + .choose(&mut rng) + .unwrap_or(&"panda"); + format!("https://api.dicebear.com/7.x/animalz/svg?seed={}&backgroundColor={}", animal, &background[1..] ) - } + }, AvatarStyle::Abstract => { // 使用抽象图案 let pattern = Self::ABSTRACT_PATTERNS .choose(&mut rng) .unwrap_or(&"circles"); - format!( - "https://api.dicebear.com/7.x/shapes/svg?seed={}&backgroundColor={}", + format!("https://api.dicebear.com/7.x/shapes/svg?seed={}&backgroundColor={}", pattern, &background[1..] ) - } + }, AvatarStyle::Gradient => { // 使用渐变头像 - format!( - "https://source.boringavatars.com/beam/256/{}?colors={},{}", + format!("https://source.boringavatars.com/beam/256/{}?colors={},{}", user_email, &color[1..], &background[1..] ) - } + }, AvatarStyle::Pattern => { // 使用图案头像 - format!( - "https://api.dicebear.com/7.x/identicon/svg?seed={}&backgroundColor={}", + format!("https://api.dicebear.com/7.x/identicon/svg?seed={}&backgroundColor={}", user_email, &background[1..] ) - } + }, }; - + Avatar { style, color: color.to_string(), @@ -135,14 +123,14 @@ impl AvatarService { url, } } - + /// 根据用户ID生成确定性头像(同一ID总是生成相同头像) pub fn generate_deterministic_avatar(user_id: &str, user_name: &str) -> Avatar { // 使用用户ID的哈希值作为种子 let hash = Self::simple_hash(user_id); let theme_index = (hash % Self::COLOR_THEMES.len() as u32) as usize; let (color, background) = Self::COLOR_THEMES[theme_index]; - + // 基于哈希选择风格 let style = match hash % 5 { 0 => AvatarStyle::Initials, @@ -151,50 +139,45 @@ impl AvatarService { 3 => AvatarStyle::Gradient, _ => AvatarStyle::Pattern, }; - + let url = match style { AvatarStyle::Initials => { let initials = Self::get_initials(user_name); - format!( - "https://ui-avatars.com/api/?name={}&background={}&color={}&size=256", + format!("https://ui-avatars.com/api/?name={}&background={}&color={}&size=256", initials, &background[1..], &color[1..] ) - } + }, AvatarStyle::Animal => { let animal_index = (hash as usize / 5) % Self::ANIMAL_AVATARS.len(); let animal = Self::ANIMAL_AVATARS[animal_index]; - format!( - "https://api.dicebear.com/7.x/animalz/svg?seed={}&backgroundColor={}", + format!("https://api.dicebear.com/7.x/animalz/svg?seed={}&backgroundColor={}", animal, &background[1..] ) - } + }, AvatarStyle::Abstract => { - format!( - "https://api.dicebear.com/7.x/shapes/svg?seed={}&backgroundColor={}", + format!("https://api.dicebear.com/7.x/shapes/svg?seed={}&backgroundColor={}", user_id, &background[1..] ) - } + }, AvatarStyle::Gradient => { - format!( - "https://source.boringavatars.com/beam/256/{}?colors={},{}", + format!("https://source.boringavatars.com/beam/256/{}?colors={},{}", user_id, &color[1..], &background[1..] ) - } + }, AvatarStyle::Pattern => { - format!( - "https://api.dicebear.com/7.x/identicon/svg?seed={}&backgroundColor={}", + format!("https://api.dicebear.com/7.x/identicon/svg?seed={}&backgroundColor={}", user_id, &background[1..] ) - } + }, }; - + Avatar { style, color: color.to_string(), @@ -202,7 +185,7 @@ impl AvatarService { url, } } - + /// 获取本地默认头像路径 pub fn get_local_avatar(index: usize) -> String { // 本地预设头像(可以存储在静态资源中) @@ -219,27 +202,20 @@ impl AvatarService { "/assets/avatars/avatar_10.svg", ]; let idx = index % LOCAL_AVATARS.len(); - LOCAL_AVATARS - .get(idx) - .copied() - .unwrap_or(LOCAL_AVATARS[0]) - .to_string() + LOCAL_AVATARS.get(idx).copied().unwrap_or(LOCAL_AVATARS[0]).to_string() } - + /// 从名字获取首字母 fn get_initials(name: &str) -> String { let parts: Vec<&str> = name.split_whitespace().collect(); if parts.is_empty() { return "U".to_string(); } - + let mut initials = String::new(); - + // 如果是中文名字,取前两个字符 - if name - .chars() - .any(|c| (c as u32) > 0x4E00 && (c as u32) < 0x9FFF) - { + if name.chars().any(|c| (c as u32) > 0x4E00 && (c as u32) < 0x9FFF) { let chars: Vec = name.chars().collect(); if chars.len() >= 2 { initials.push(chars[0]); @@ -248,32 +224,33 @@ impl AvatarService { initials.push(chars[0]); } } else { - // 英文名字,取每个单词的首字母(最多2个) - for part in parts.iter().take(2) { - if let Some(first_char) = part.chars().next() { - initials.push(first_char.to_uppercase().next().unwrap_or(first_char)); - } + // 英文名字,取每个单词的首字母(最多2个) + for part in parts.iter().take(2) { + if let Some(first_char) = part.chars().next() { + initials.push(first_char.to_uppercase().next().unwrap_or(first_char)); } } - + } + if initials.is_empty() { initials = "U".to_string(); } - + initials } - + /// 简单的哈希函数 fn simple_hash(s: &str) -> u32 { - s.bytes() - .fold(0u32, |acc, b| acc.wrapping_mul(31).wrapping_add(b as u32)) + s.bytes().fold(0u32, |acc, b| { + acc.wrapping_mul(31).wrapping_add(b as u32) + }) } - + /// 生成多个候选头像供用户选择 pub fn generate_avatar_options(user_name: &str, user_email: &str, count: usize) -> Vec { let mut avatars = Vec::new(); let mut rng = rand::thread_rng(); - + // 确保每种风格至少有一个 let styles = [ AvatarStyle::Initials, @@ -282,63 +259,58 @@ impl AvatarService { AvatarStyle::Gradient, AvatarStyle::Pattern, ]; - + for (i, style) in styles.iter().enumerate() { if i >= count { break; } - + let (color, background) = Self::COLOR_THEMES .choose(&mut rng) .unwrap_or(&("#4ECDC4", "#E3FFF8")); - + let url = match style { AvatarStyle::Initials => { let initials = Self::get_initials(user_name); - format!( - "https://ui-avatars.com/api/?name={}&background={}&color={}&size=256", + format!("https://ui-avatars.com/api/?name={}&background={}&color={}&size=256", initials, &background[1..], &color[1..] ) - } + }, AvatarStyle::Animal => { - let animal = Self::ANIMAL_AVATARS.choose(&mut rng).unwrap_or(&"panda"); - format!( - "https://api.dicebear.com/7.x/animalz/svg?seed={}&backgroundColor={}", + let animal = Self::ANIMAL_AVATARS + .choose(&mut rng) + .unwrap_or(&"panda"); + format!("https://api.dicebear.com/7.x/animalz/svg?seed={}&backgroundColor={}", animal, &background[1..] ) - } + }, AvatarStyle::Abstract => { let pattern = Self::ABSTRACT_PATTERNS .choose(&mut rng) .unwrap_or(&"circles"); - format!( - "https://api.dicebear.com/7.x/shapes/svg?seed={}&backgroundColor={}", + format!("https://api.dicebear.com/7.x/shapes/svg?seed={}&backgroundColor={}", pattern, &background[1..] ) - } + }, AvatarStyle::Gradient => { - format!( - "https://source.boringavatars.com/beam/256/{}{}?colors={},{}", - user_email, - i, + format!("https://source.boringavatars.com/beam/256/{}{}?colors={},{}", + user_email, i, &color[1..], &background[1..] ) - } + }, AvatarStyle::Pattern => { - format!( - "https://api.dicebear.com/7.x/identicon/svg?seed={}{}&backgroundColor={}", - user_email, - i, + format!("https://api.dicebear.com/7.x/identicon/svg?seed={}{}&backgroundColor={}", + user_email, i, &background[1..] ) - } + }, }; - + avatars.push(Avatar { style: style.clone(), color: color.to_string(), @@ -346,7 +318,7 @@ impl AvatarService { url, }); } - + avatars } } @@ -354,7 +326,7 @@ impl AvatarService { #[cfg(test)] mod tests { use super::*; - + #[test] fn test_get_initials() { assert_eq!(AvatarService::get_initials("John Doe"), "JD"); @@ -363,7 +335,7 @@ mod tests { assert_eq!(AvatarService::get_initials(""), "U"); assert_eq!(AvatarService::get_initials("Alice Bob Charlie"), "AB"); } - + #[test] fn test_generate_random_avatar() { let avatar = AvatarService::generate_random_avatar("Test User", "test@example.com"); @@ -371,7 +343,7 @@ mod tests { assert!(!avatar.color.is_empty()); assert!(!avatar.background.is_empty()); } - + #[test] fn test_deterministic_avatar() { let avatar1 = AvatarService::generate_deterministic_avatar("user123", "Test User"); diff --git a/jive-api/src/services/budget_service.rs b/jive-api/src/services/budget_service.rs index a34728d1..b105386f 100644 --- a/jive-api/src/services/budget_service.rs +++ b/jive-api/src/services/budget_service.rs @@ -1,5 +1,5 @@ use crate::error::{ApiError, ApiResult}; -use chrono::{DateTime, Datelike, Duration, Timelike, Utc}; +use chrono::{DateTime, Datelike, Timelike, Utc, Duration}; use serde::{Deserialize, Serialize}; use sqlx::PgPool; use uuid::Uuid; @@ -64,17 +64,17 @@ impl BudgetService { /// 创建预算 pub async fn create_budget(&self, data: CreateBudgetRequest) -> ApiResult { let budget_id = Uuid::new_v4(); - + // 验证预算期间 let end_date = match data.period_type { BudgetPeriod::Monthly => { let start = data.start_date; Some(start + Duration::days(30)) - } + }, BudgetPeriod::Yearly => { let start = data.start_date; Some(start + Duration::days(365)) - } + }, BudgetPeriod::Custom => data.end_date, _ => None, }; @@ -89,7 +89,7 @@ impl BudgetService { $1, $2, $3, $4, $5, $6, $7, $8, $9, NOW(), NOW() ) RETURNING * - "#, + "# ) .bind(budget_id) .bind(data.ledger_id) @@ -110,16 +110,17 @@ impl BudgetService { /// 获取预算进度 pub async fn get_budget_progress(&self, budget_id: Uuid) -> ApiResult { // 获取预算信息 - let budget: Budget = - sqlx::query_as("SELECT * FROM budgets WHERE id = $1 AND is_active = true") - .bind(budget_id) - .fetch_one(&self.pool) - .await - .map_err(|e| ApiError::DatabaseError(e.to_string()))?; + let budget: Budget = sqlx::query_as( + "SELECT * FROM budgets WHERE id = $1 AND is_active = true" + ) + .bind(budget_id) + .fetch_one(&self.pool) + .await + .map_err(|e| ApiError::DatabaseError(e.to_string()))?; // 计算当前期间 let (period_start, period_end) = self.get_current_period(&budget)?; - + // 获取期间内的支出 let spent: (Option,) = sqlx::query_as( r#" @@ -130,7 +131,7 @@ impl BudgetService { AND transaction_date BETWEEN $2 AND $3 AND ($4::uuid IS NULL OR category_id = $4) AND status = 'cleared' - "#, + "# ) .bind(budget.ledger_id) .bind(period_start) @@ -148,7 +149,7 @@ impl BudgetService { let now = Utc::now(); let days_remaining = (period_end - now).num_days().max(0); let days_passed = (now - period_start).num_days().max(1); - + // 计算平均日支出和预测 let average_daily_spend = spent_amount / days_passed as f64; let projected_total = average_daily_spend * (days_passed + days_remaining) as f64; @@ -159,20 +160,17 @@ impl BudgetService { }; // 获取分类支出明细 - let categories = self - .get_category_spending( - &budget.ledger_id, - &period_start, - &period_end, - budget.category_id, - ) - .await?; + let categories = self.get_category_spending( + &budget.ledger_id, + &period_start, + &period_end, + budget.category_id + ).await?; Ok(BudgetProgress { budget_id: budget.id, budget_name: budget.name, - period: format!( - "{} - {}", + period: format!("{} - {}", period_start.format("%Y-%m-%d"), period_end.format("%Y-%m-%d") ), @@ -213,7 +211,7 @@ impl BudgetService { GROUP BY c.id, c.name HAVING SUM(t.amount) > 0 ORDER BY amount_spent DESC - "#, + "# ) .bind(ledger_id) .bind(start_date) @@ -229,7 +227,7 @@ impl BudgetService { /// 计算当前预算期间 fn get_current_period(&self, budget: &Budget) -> ApiResult<(DateTime, DateTime)> { let now = Utc::now(); - + match budget.period_type { BudgetPeriod::Monthly => { let start = Utc::now() @@ -243,11 +241,14 @@ impl BudgetService { .unwrap() .with_nanosecond(0) .unwrap(); - - let end = (start + Duration::days(32)).with_day(1).unwrap() - Duration::seconds(1); - + + let end = (start + Duration::days(32)) + .with_day(1) + .unwrap() + - Duration::seconds(1); + Ok((start, end)) - } + }, BudgetPeriod::Yearly => { let start = Utc::now() .with_month(1) @@ -262,43 +263,42 @@ impl BudgetService { .unwrap() .with_nanosecond(0) .unwrap(); - + let end = start + Duration::days(365) - Duration::seconds(1); - + Ok((start, end)) + }, + BudgetPeriod::Custom => { + Ok((budget.start_date, budget.end_date.unwrap_or(now + Duration::days(30)))) + }, + _ => { + Ok((budget.start_date, now + Duration::days(30))) } - BudgetPeriod::Custom => Ok(( - budget.start_date, - budget.end_date.unwrap_or(now + Duration::days(30)), - )), - _ => Ok((budget.start_date, now + Duration::days(30))), } } /// 预算预警检查 pub async fn check_budget_alerts(&self, ledger_id: Uuid) -> ApiResult> { - let budgets: Vec = - sqlx::query_as("SELECT * FROM budgets WHERE ledger_id = $1 AND is_active = true") - .bind(ledger_id) - .fetch_all(&self.pool) - .await - .map_err(|e| ApiError::DatabaseError(e.to_string()))?; + let budgets: Vec = sqlx::query_as( + "SELECT * FROM budgets WHERE ledger_id = $1 AND is_active = true" + ) + .bind(ledger_id) + .fetch_all(&self.pool) + .await + .map_err(|e| ApiError::DatabaseError(e.to_string()))?; let mut alerts = Vec::new(); for budget in budgets { let progress = self.get_budget_progress(budget.id).await?; - + // 检查预警条件 if progress.percentage_used >= 90.0 { alerts.push(BudgetAlert { budget_id: budget.id, budget_name: budget.name.clone(), alert_type: AlertType::Critical, - message: format!( - "预算 {} 已使用 {:.1}%", - budget.name, progress.percentage_used - ), + message: format!("预算 {} 已使用 {:.1}%", budget.name, progress.percentage_used), percentage_used: progress.percentage_used, remaining_amount: progress.remaining_amount, }); @@ -307,10 +307,7 @@ impl BudgetService { budget_id: budget.id, budget_name: budget.name.clone(), alert_type: AlertType::Warning, - message: format!( - "预算 {} 已使用 {:.1}%", - budget.name, progress.percentage_used - ), + message: format!("预算 {} 已使用 {:.1}%", budget.name, progress.percentage_used), percentage_used: progress.percentage_used, remaining_amount: progress.remaining_amount, }); @@ -323,10 +320,7 @@ impl BudgetService { budget_id: budget.id, budget_name: budget.name.clone(), alert_type: AlertType::Projection, - message: format!( - "按当前支出速度,预算 {} 预计超支 ¥{:.2}", - budget.name, overspend - ), + message: format!("按当前支出速度,预算 {} 预计超支 ¥{:.2}", budget.name, overspend), percentage_used: progress.percentage_used, remaining_amount: progress.remaining_amount, }); @@ -344,14 +338,15 @@ impl BudgetService { period: ReportPeriod, ) -> ApiResult { let (start_date, end_date) = self.get_report_period(period)?; - + // 获取所有预算 - let budgets: Vec = - sqlx::query_as("SELECT * FROM budgets WHERE ledger_id = $1 AND is_active = true") - .bind(ledger_id) - .fetch_all(&self.pool) - .await - .map_err(|e| ApiError::DatabaseError(e.to_string()))?; + let budgets: Vec = sqlx::query_as( + "SELECT * FROM budgets WHERE ledger_id = $1 AND is_active = true" + ) + .bind(ledger_id) + .fetch_all(&self.pool) + .await + .map_err(|e| ApiError::DatabaseError(e.to_string()))?; let mut budget_summaries = Vec::new(); let mut total_budgeted = 0.0; @@ -361,7 +356,7 @@ impl BudgetService { let progress = self.get_budget_progress(budget.id).await?; total_budgeted += budget.amount; total_spent += progress.spent_amount; - + budget_summaries.push(BudgetSummary { budget_name: budget.name, budgeted: budget.amount, @@ -384,7 +379,7 @@ impl BudgetService { WHERE ledger_id = $1 AND category_id IS NOT NULL ) AND status = 'cleared' - "#, + "# ) .bind(ledger_id) .bind(start_date) @@ -394,8 +389,7 @@ impl BudgetService { .map_err(|e| ApiError::DatabaseError(e.to_string()))?; Ok(BudgetReport { - period: format!( - "{} - {}", + period: format!("{} - {}", start_date.format("%Y-%m-%d"), end_date.format("%Y-%m-%d") ), @@ -411,7 +405,7 @@ impl BudgetService { fn get_report_period(&self, period: ReportPeriod) -> ApiResult<(DateTime, DateTime)> { let now = Utc::now(); - + match period { ReportPeriod::CurrentMonth => { let start = now @@ -426,7 +420,7 @@ impl BudgetService { .with_nanosecond(0) .unwrap(); Ok((start, now)) - } + }, ReportPeriod::LastMonth => { let end = now .with_day(1) @@ -452,7 +446,7 @@ impl BudgetService { .with_nanosecond(0) .unwrap(); Ok((start, end)) - } + }, ReportPeriod::CurrentYear => { let start = now .with_month(1) @@ -468,7 +462,7 @@ impl BudgetService { .with_nanosecond(0) .unwrap(); Ok((start, now)) - } + }, } } } diff --git a/jive-api/src/services/currency_service.rs b/jive-api/src/services/currency_service.rs index d25e9077..2a10d34c 100644 --- a/jive-api/src/services/currency_service.rs +++ b/jive-api/src/services/currency_service.rs @@ -2,10 +2,10 @@ use chrono::{DateTime, NaiveDate, Utc}; use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; use sqlx::{PgPool, Row}; +use uuid::Uuid; use std::collections::HashMap; use std::future::Future; use std::pin::Pin; -use uuid::Uuid; use super::ServiceError; // remove duplicate import of NaiveDate @@ -87,7 +87,7 @@ impl CurrencyService { pub fn new(pool: PgPool) -> Self { Self { pool } } - + /// 获取所有支持的货币 pub async fn get_supported_currencies(&self) -> Result, ServiceError> { let rows = sqlx::query!( @@ -100,25 +100,18 @@ impl CurrencyService { ) .fetch_all(&self.pool) .await?; - - let currencies = rows - .into_iter() - .map(|row| { - let _code = row.code.clone(); - Currency { - code: row.code, - name: row.name, - // symbol column非空:直接使用;保持与之前“若为空给默认”语义一致(当前 schema 下为空可能性极低) - symbol: row.symbol, - decimal_places: row.decimal_places.unwrap_or(2), - is_active: row.is_active.unwrap_or(true), - } - }) - .collect(); - + + let currencies = rows.into_iter().map(|row| Currency { + code: row.code, + name: row.name, + symbol: row.symbol.unwrap_or_default(), + decimal_places: row.decimal_places.unwrap_or(2), + is_active: row.is_active.unwrap_or(true), + }).collect(); + Ok(currencies) } - + /// 获取用户的货币偏好 pub async fn get_user_currency_preferences( &self, @@ -135,19 +128,16 @@ impl CurrencyService { ) .fetch_all(&self.pool) .await?; - - let preferences = rows - .into_iter() - .map(|row| CurrencyPreference { - currency_code: row.currency_code, - is_primary: row.is_primary.unwrap_or(false), - display_order: row.display_order.unwrap_or(0), - }) - .collect(); - + + let preferences = rows.into_iter().map(|row| CurrencyPreference { + currency_code: row.currency_code, + is_primary: row.is_primary.unwrap_or(false), + display_order: row.display_order.unwrap_or(0), + }).collect(); + Ok(preferences) } - + /// 设置用户的货币偏好 pub async fn set_user_currency_preferences( &self, @@ -156,7 +146,7 @@ impl CurrencyService { primary_currency: String, ) -> Result<(), ServiceError> { let mut tx = self.pool.begin().await?; - + // 删除现有偏好 sqlx::query!( "DELETE FROM user_currency_preferences WHERE user_id = $1", @@ -164,7 +154,7 @@ impl CurrencyService { ) .execute(&mut *tx) .await?; - + // 插入新偏好 for (index, currency) in currencies.iter().enumerate() { sqlx::query!( @@ -181,11 +171,11 @@ impl CurrencyService { .execute(&mut *tx) .await?; } - + tx.commit().await?; Ok(()) } - + /// 获取家庭的货币设置 pub async fn get_family_currency_settings( &self, @@ -202,15 +192,15 @@ impl CurrencyService { ) .fetch_optional(&self.pool) .await?; - + if let Some(settings) = settings { // 获取支持的货币列表 let supported = self.get_family_supported_currencies(family_id).await?; - + Ok(FamilyCurrencySettings { family_id, - // Handle potentially nullable base_currency field - base_currency: if settings.base_currency.is_empty() { "CNY".to_string() } else { settings.base_currency }, + // base_currency 可能为可空;兜底为 CNY + base_currency: settings.base_currency.unwrap_or_else(|| "CNY".to_string()), allow_multi_currency: settings.allow_multi_currency.unwrap_or(false), auto_convert: settings.auto_convert.unwrap_or(false), supported_currencies: supported, @@ -226,7 +216,7 @@ impl CurrencyService { }) } } - + /// 更新家庭的货币设置 pub async fn update_family_currency_settings( &self, @@ -234,7 +224,7 @@ impl CurrencyService { request: UpdateCurrencySettingsRequest, ) -> Result { let mut tx = self.pool.begin().await?; - + // 插入或更新设置 sqlx::query!( r#" @@ -255,12 +245,12 @@ impl CurrencyService { ) .execute(&mut *tx) .await?; - + tx.commit().await?; - + self.get_family_currency_settings(family_id).await } - + /// 获取汇率 pub fn get_exchange_rate<'a>( &'a self, @@ -269,11 +259,10 @@ impl CurrencyService { date: Option, ) -> Pin> + Send + 'a>> { Box::pin(async move { - self.get_exchange_rate_impl(from_currency, to_currency, date) - .await + self.get_exchange_rate_impl(from_currency, to_currency, date).await }) } - + async fn get_exchange_rate_impl( &self, from_currency: &str, @@ -283,9 +272,9 @@ impl CurrencyService { if from_currency == to_currency { return Ok(Decimal::ONE); } - + let effective_date = date.unwrap_or_else(|| Utc::now().date_naive()); - + // 尝试直接获取汇率 let rate = sqlx::query_scalar!( r#" @@ -303,11 +292,11 @@ impl CurrencyService { ) .fetch_optional(&self.pool) .await?; - + if let Some(rate) = rate { return Ok(rate); } - + // 尝试获取反向汇率 let reverse_rate = sqlx::query_scalar!( r#" @@ -325,27 +314,25 @@ impl CurrencyService { ) .fetch_optional(&self.pool) .await?; - + if let Some(rate) = reverse_rate { return Ok(Decimal::ONE / rate); } - + // 尝试通过USD中转(最常见的中转货币) - let from_to_usd = - Box::pin(self.get_exchange_rate_impl(from_currency, "USD", Some(effective_date))).await; - let usd_to_target = - Box::pin(self.get_exchange_rate_impl("USD", to_currency, Some(effective_date))).await; - + let from_to_usd = Box::pin(self.get_exchange_rate_impl(from_currency, "USD", Some(effective_date))).await; + let usd_to_target = Box::pin(self.get_exchange_rate_impl("USD", to_currency, Some(effective_date))).await; + if let (Ok(rate1), Ok(rate2)) = (from_to_usd, usd_to_target) { return Ok(rate1 * rate2); } - + Err(ServiceError::NotFound { resource_type: "ExchangeRate".to_string(), id: format!("{}-{}", from_currency, to_currency), }) } - + /// 批量获取汇率 pub async fn get_exchange_rates( &self, @@ -354,16 +341,16 @@ impl CurrencyService { date: Option, ) -> Result, ServiceError> { let mut rates = HashMap::new(); - + for currency in target_currencies { if let Ok(rate) = self.get_exchange_rate(base_currency, ¤cy, date).await { rates.insert(currency, rate); } } - + Ok(rates) } - + /// 添加或更新汇率 pub async fn add_exchange_rate( &self, @@ -374,7 +361,7 @@ impl CurrencyService { // Align with DB schema: UNIQUE(from_currency, to_currency, date) // Use business date == effective_date for upsert key let business_date = effective_date; - + let rec = sqlx::query( r#" INSERT INTO exchange_rates @@ -417,7 +404,7 @@ impl CurrencyService { .unwrap_or_else(chrono::Utc::now), }) } - + /// 货币转换 pub fn convert_amount( &self, @@ -427,14 +414,14 @@ impl CurrencyService { to_decimal_places: i32, ) -> Decimal { let converted = amount * rate; - + // 根据目标货币的小数位数进行舍入 let scale = 10_i64.pow(to_decimal_places as u32); let scaled = converted * Decimal::from(scale); let rounded = scaled.round(); rounded / Decimal::from(scale) } - + /// 获取最近的汇率历史 pub async fn get_exchange_rate_history( &self, @@ -443,7 +430,7 @@ impl CurrencyService { days: i32, ) -> Result, ServiceError> { let start_date = (Utc::now() - chrono::Duration::days(days as i64)).date_naive(); - + let rows = sqlx::query!( r#" SELECT id, from_currency, to_currency, rate, source, @@ -460,23 +447,20 @@ impl CurrencyService { ) .fetch_all(&self.pool) .await?; - - Ok(rows - .into_iter() - .map(|row| ExchangeRate { - id: row.id, - from_currency: row.from_currency, - to_currency: row.to_currency, - rate: row.rate, - source: row.source.unwrap_or_else(|| "manual".to_string()), - // effective_date 为非空(schema 约束);直接使用 - effective_date: row.effective_date, - // created_at 在 schema 中可能可空;兜底当前时间 - created_at: row.created_at.unwrap_or_else(Utc::now), - }) - .collect()) + + Ok(rows.into_iter().map(|row| ExchangeRate { + id: row.id, + from_currency: row.from_currency, + to_currency: row.to_currency, + rate: row.rate, + source: row.source.unwrap_or_else(|| "manual".to_string()), + // effective_date 为非空(schema 约束);直接使用 + effective_date: row.effective_date, + // created_at 在 schema 中可能可空;兜底当前时间 + created_at: row.created_at.unwrap_or_else(Utc::now), + }).collect()) } - + /// 获取家庭支持的货币列表 async fn get_family_supported_currencies( &self, @@ -495,9 +479,12 @@ impl CurrencyService { ) .fetch_all(&self.pool) .await?; - - let currencies: Vec = currencies.into_iter().flatten().collect(); - + + let currencies: Vec = currencies + .into_iter() + .flatten() + .collect(); + if currencies.is_empty() { // 返回默认货币 Ok(vec!["CNY".to_string(), "USD".to_string()]) @@ -505,19 +492,19 @@ impl CurrencyService { Ok(currencies) } } - + /// 自动获取最新汇率并更新到数据库 pub async fn fetch_latest_rates(&self, base_currency: &str) -> Result<(), ServiceError> { use super::exchange_rate_api::EXCHANGE_RATE_SERVICE; - + tracing::info!("Fetching latest exchange rates for {}", base_currency); - + // 获取汇率服务实例 let mut service = EXCHANGE_RATE_SERVICE.lock().await; - + // 获取最新汇率 let rates = service.fetch_fiat_rates(base_currency).await?; - + // 仅对系统已知的币种写库,避免外键错误 // 在线模式或存在 .sqlx 缓存时可查询;否则跳过过滤(保守按未知代码丢弃) let known_codes: std::collections::HashSet = std::collections::HashSet::new(); @@ -531,16 +518,14 @@ impl CurrencyService { // 批量更新到数据库 let effective_date = Utc::now().date_naive(); let business_date = effective_date; - + for (target_currency, rate) in rates.iter() { if target_currency != base_currency { // 跳过未知币种,避免外键约束失败 // 如果未加载已知币种列表,则不做过滤;否则过滤未知代码,避免外键错误 - if !known_codes.is_empty() && !known_codes.contains(target_currency) { - continue; - } + if !known_codes.is_empty() && !known_codes.contains(target_currency) { continue; } let id = Uuid::new_v4(); - + // 插入或更新汇率 let res = sqlx::query( r#" @@ -579,33 +564,23 @@ impl CurrencyService { } } } - - tracing::info!( - "Successfully updated {} exchange rates for {}", - rates.len() - 1, - base_currency - ); + + tracing::info!("Successfully updated {} exchange rates for {}", rates.len() - 1, base_currency); Ok(()) } - + /// 获取并更新加密货币价格 - pub async fn fetch_crypto_prices( - &self, - crypto_codes: Vec<&str>, - fiat_currency: &str, - ) -> Result<(), ServiceError> { + pub async fn fetch_crypto_prices(&self, crypto_codes: Vec<&str>, fiat_currency: &str) -> Result<(), ServiceError> { use super::exchange_rate_api::EXCHANGE_RATE_SERVICE; - + tracing::info!("Fetching crypto prices in {}", fiat_currency); - + // 获取汇率服务实例 let mut service = EXCHANGE_RATE_SERVICE.lock().await; - + // 获取加密货币价格 - let prices = service - .fetch_crypto_prices(crypto_codes.clone(), fiat_currency) - .await?; - + let prices = service.fetch_crypto_prices(crypto_codes.clone(), fiat_currency).await?; + // 批量更新到数据库 for (crypto_code, price) in prices.iter() { sqlx::query!( @@ -627,21 +602,13 @@ impl CurrencyService { .execute(&self.pool) .await?; } - - tracing::info!( - "Successfully updated {} crypto prices in {}", - prices.len(), - fiat_currency - ); + + tracing::info!("Successfully updated {} crypto prices in {}", prices.len(), fiat_currency); Ok(()) } /// Clear manual flag/expiry for today's business date for a given pair - pub async fn clear_manual_rate( - &self, - from_currency: &str, - to_currency: &str, - ) -> Result<(), ServiceError> { + pub async fn clear_manual_rate(&self, from_currency: &str, to_currency: &str) -> Result<(), ServiceError> { let _ = sqlx::query( r#" UPDATE exchange_rates @@ -649,7 +616,7 @@ impl CurrencyService { manual_rate_expiry = NULL, updated_at = CURRENT_TIMESTAMP WHERE from_currency = $1 AND to_currency = $2 AND date = CURRENT_DATE - "#, + "# ) .bind(from_currency) .bind(to_currency) @@ -659,13 +626,8 @@ impl CurrencyService { } /// Batch clear manual flags/expiry by filters - pub async fn clear_manual_rates_batch( - &self, - req: ClearManualRatesBatchRequest, - ) -> Result { - let target_date = req - .before_date - .unwrap_or_else(|| chrono::Utc::now().date_naive()); + pub async fn clear_manual_rates_batch(&self, req: ClearManualRatesBatchRequest) -> Result { + let target_date = req.before_date.unwrap_or_else(|| chrono::Utc::now().date_naive()); let only_expired = req.only_expired.unwrap_or(false); let mut total: u64 = 0; @@ -681,7 +643,7 @@ impl CurrencyService { AND to_currency = ANY($2) AND date <= $3 AND manual_rate_expiry IS NOT NULL AND manual_rate_expiry <= NOW() - "#, + "# ) .bind(&req.from_currency) .bind(list) @@ -699,7 +661,7 @@ impl CurrencyService { WHERE from_currency = $1 AND to_currency = ANY($2) AND date <= $3 - "#, + "# ) .bind(&req.from_currency) .bind(list) @@ -718,7 +680,7 @@ impl CurrencyService { WHERE from_currency = $1 AND date <= $2 AND manual_rate_expiry IS NOT NULL AND manual_rate_expiry <= NOW() - "#, + "# ) .bind(&req.from_currency) .bind(target_date) @@ -734,7 +696,7 @@ impl CurrencyService { updated_at = CURRENT_TIMESTAMP WHERE from_currency = $1 AND date <= $2 - "#, + "# ) .bind(&req.from_currency) .bind(target_date) diff --git a/jive-api/src/services/exchange_rate_api.rs b/jive-api/src/services/exchange_rate_api.rs index b32c9e95..85dc5336 100644 --- a/jive-api/src/services/exchange_rate_api.rs +++ b/jive-api/src/services/exchange_rate_api.rs @@ -1,4 +1,4 @@ -use chrono::{DateTime, Duration, Utc}; +use chrono::{DateTime, Utc, Duration}; use reqwest; use rust_decimal::Decimal; use serde::Deserialize; // Serialize 未用 @@ -116,13 +116,13 @@ impl ExchangeRateApiService { .timeout(std::time::Duration::from_secs(10)) .build() .unwrap(); - + Self { client, cache: HashMap::new(), } } - + /// Inspect cached provider source for fiat by base code pub fn cached_fiat_source(&self, base_currency: &str) -> Option { let key = format!("fiat_{}", base_currency); @@ -130,38 +130,27 @@ impl ExchangeRateApiService { } /// Inspect cached provider source for crypto by codes + fiat - pub fn cached_crypto_source( - &self, - crypto_codes: &[&str], - fiat_currency: &str, - ) -> Option { + pub fn cached_crypto_source(&self, crypto_codes: &[&str], fiat_currency: &str) -> Option { let key = format!("crypto_{}_{}", crypto_codes.join(","), fiat_currency); self.cache.get(&key).map(|c| c.source.clone()) } - + /// 获取法定货币汇率 - pub async fn fetch_fiat_rates( - &mut self, - base_currency: &str, - ) -> Result, ServiceError> { + pub async fn fetch_fiat_rates(&mut self, base_currency: &str) -> Result, ServiceError> { let cache_key = format!("fiat_{}", base_currency); - + // 检查缓存(15分钟有效期) if let Some(cached) = self.cache.get(&cache_key) { if !cached.is_expired(Duration::minutes(15)) { - info!( - "Using cached rates for {} from {}", - base_currency, cached.source - ); + info!("Using cached rates for {} from {}", base_currency, cached.source); return Ok(cached.rates.clone()); } } - + // 尝试多个数据源(顺序可配置:FIAT_PROVIDER_ORDER=exchangerate-api,frankfurter,fxrates) let mut rates = None; let mut source = String::new(); - let order_env = std::env::var("FIAT_PROVIDER_ORDER") - .unwrap_or_else(|_| "exchangerate-api,frankfurter,fxrates".to_string()); + let order_env = std::env::var("FIAT_PROVIDER_ORDER").unwrap_or_else(|_| "exchangerate-api,frankfurter,fxrates".to_string()); let providers: Vec = order_env .split(',') .map(|s| s.trim().to_lowercase()) @@ -170,37 +159,22 @@ impl ExchangeRateApiService { for p in providers { match p.as_str() { "frankfurter" => match self.fetch_from_frankfurter(base_currency).await { - Ok(r) => { - rates = Some(r); - source = "frankfurter".to_string(); - } + Ok(r) => { rates = Some(r); source = "frankfurter".to_string(); }, Err(e) => warn!("Failed to fetch from Frankfurter: {}", e), }, - "exchangerate-api" | "exchange-rate-api" => { - match self.fetch_from_exchangerate_api(base_currency).await { - Ok(r) => { - rates = Some(r); - source = "exchangerate-api".to_string(); - } - Err(e) => warn!("Failed to fetch from ExchangeRate-API: {}", e), - } - } - "fxrates" | "fx-rates-api" | "fxratesapi" => { - match self.fetch_from_fxrates_api(base_currency).await { - Ok(r) => { - rates = Some(r); - source = "fxrates".to_string(); - } - Err(e) => warn!("Failed to fetch from FXRates API: {}", e), - } - } + "exchangerate-api" | "exchange-rate-api" => match self.fetch_from_exchangerate_api(base_currency).await { + Ok(r) => { rates = Some(r); source = "exchangerate-api".to_string(); }, + Err(e) => warn!("Failed to fetch from ExchangeRate-API: {}", e), + }, + "fxrates" | "fx-rates-api" | "fxratesapi" => match self.fetch_from_fxrates_api(base_currency).await { + Ok(r) => { rates = Some(r); source = "fxrates".to_string(); }, + Err(e) => warn!("Failed to fetch from FXRates API: {}", e), + }, other => warn!("Unknown fiat provider: {}", other), } - if rates.is_some() { - break; - } + if rates.is_some() { break; } } - + // 如果获取成功,更新缓存 if let Some(rates) = rates { self.cache.insert( @@ -213,70 +187,61 @@ impl ExchangeRateApiService { ); return Ok(rates); } - + // 如果所有API都失败,返回默认汇率 warn!("All rate APIs failed, returning default rates"); Ok(self.get_default_rates(base_currency)) } - + /// 从 Frankfurter API 获取汇率 - async fn fetch_from_frankfurter( - &self, - base_currency: &str, - ) -> Result, ServiceError> { + async fn fetch_from_frankfurter(&self, base_currency: &str) -> Result, ServiceError> { let url = format!("https://api.frankfurter.app/latest?from={}", base_currency); - - let response = - self.client - .get(&url) - .send() - .await - .map_err(|e| ServiceError::ExternalApi { - message: format!("Failed to fetch from Frankfurter: {}", e), - })?; - + + let response = self.client + .get(&url) + .send() + .await + .map_err(|e| ServiceError::ExternalApi { + message: format!("Failed to fetch from Frankfurter: {}", e), + })?; + if !response.status().is_success() { return Err(ServiceError::ExternalApi { message: format!("Frankfurter API returned status: {}", response.status()), }); } - - let data: FrankfurterResponse = - response - .json() - .await - .map_err(|e| ServiceError::ExternalApi { - message: format!("Failed to parse Frankfurter response: {}", e), - })?; - + + let data: FrankfurterResponse = response + .json() + .await + .map_err(|e| ServiceError::ExternalApi { + message: format!("Failed to parse Frankfurter response: {}", e), + })?; + let mut rates = HashMap::new(); for (currency, rate) in data.rates { if let Ok(decimal_rate) = Decimal::from_str(&rate.to_string()) { rates.insert(currency, decimal_rate); } } - + // 添加基础货币本身 rates.insert(base_currency.to_string(), Decimal::ONE); - + Ok(rates) } /// 从 FXRates API 获取汇率 - async fn fetch_from_fxrates_api( - &self, - base_currency: &str, - ) -> Result, ServiceError> { + async fn fetch_from_fxrates_api(&self, base_currency: &str) -> Result, ServiceError> { let url = format!("https://api.fxratesapi.com/latest?base={}", base_currency); - let response = - self.client - .get(&url) - .send() - .await - .map_err(|e| ServiceError::ExternalApi { - message: format!("Failed to fetch from FXRates API: {}", e), - })?; + let response = self.client + .get(&url) + .send() + .await + .map_err(|e| ServiceError::ExternalApi { + message: format!("Failed to fetch from FXRates API: {}", e), + })?; if !response.status().is_success() { return Err(ServiceError::ExternalApi { @@ -284,13 +249,12 @@ impl ExchangeRateApiService { }); } - let data: FxRatesApiResponse = - response - .json() - .await - .map_err(|e| ServiceError::ExternalApi { - message: format!("Failed to parse FXRates response: {}", e), - })?; + let data: FxRatesApiResponse = response + .json() + .await + .map_err(|e| ServiceError::ExternalApi { + message: format!("Failed to parse FXRates response: {}", e), + })?; let mut rates = HashMap::new(); for (currency, rate) in data.rates { @@ -305,11 +269,7 @@ impl ExchangeRateApiService { } /// Fetch fiat rates from a specific provider label - pub async fn fetch_fiat_rates_from( - &self, - provider: &str, - base_currency: &str, - ) -> Result<(HashMap, String), ServiceError> { + pub async fn fetch_fiat_rates_from(&self, provider: &str, base_currency: &str) -> Result<(HashMap, String), ServiceError> { match provider.to_lowercase().as_str() { "exchangerate-api" | "exchange-rate-api" => { let r = self.fetch_from_exchangerate_api(base_currency).await?; @@ -323,45 +283,31 @@ impl ExchangeRateApiService { let r = self.fetch_from_fxrates_api(base_currency).await?; Ok((r, "fxrates".to_string())) } - other => Err(ServiceError::ExternalApi { - message: format!("Unknown fiat provider: {}", other), - }), + other => Err(ServiceError::ExternalApi { message: format!("Unknown fiat provider: {}", other) }), } } - + /// 从 ExchangeRate-API 获取汇率(兼容 open.er-api 与 exchangerate-api 两种格式) - async fn fetch_from_exchangerate_api( - &self, - base_currency: &str, - ) -> Result, ServiceError> { + async fn fetch_from_exchangerate_api(&self, base_currency: &str) -> Result, ServiceError> { // 优先尝试 open.er-api.com(无需密钥,速率较高) let try_urls = vec![ format!("https://open.er-api.com/v6/latest/{}", base_currency), - format!( - "https://api.exchangerate-api.com/v4/latest/{}", - base_currency - ), + format!("https://api.exchangerate-api.com/v4/latest/{}", base_currency), ]; let mut last_err: Option = None; for url in try_urls { let resp = match self.client.get(&url).send().await { Ok(r) => r, - Err(e) => { - last_err = Some(format!("request error: {}", e)); - continue; - } + Err(e) => { last_err = Some(format!("request error: {}", e)); continue; } }; - if !resp.status().is_success() { + if !resp.status().is_success() { last_err = Some(format!("status: {}", resp.status())); - continue; + continue; } let v: serde_json::Value = match resp.json().await { Ok(json) => json, - Err(e) => { - last_err = Some(format!("json error: {}", e)); - continue; - } + Err(e) => { last_err = Some(format!("json error: {}", e)); continue; } }; // 允许两种字段名:rates 或 conversion_rates let map_node = v.get("rates").or_else(|| v.get("conversion_rates")); @@ -376,28 +322,17 @@ impl ExchangeRateApiService { } // 添加基础货币自环 rates.insert(base_currency.to_uppercase(), Decimal::ONE); - if !rates.is_empty() { - return Ok(rates); - } + if !rates.is_empty() { return Ok(rates); } } last_err = Some("missing rates map".to_string()); } - Err(ServiceError::ExternalApi { - message: format!( - "Failed to fetch/parse ExchangeRate-API: {}", - last_err.unwrap_or_else(|| "unknown".to_string()) - ), - }) + Err(ServiceError::ExternalApi { message: format!("Failed to fetch/parse ExchangeRate-API: {}", last_err.unwrap_or_else(|| "unknown".to_string())) }) } - + /// 获取加密货币价格 - pub async fn fetch_crypto_prices( - &mut self, - crypto_codes: Vec<&str>, - fiat_currency: &str, - ) -> Result, ServiceError> { + pub async fn fetch_crypto_prices(&mut self, crypto_codes: Vec<&str>, fiat_currency: &str) -> Result, ServiceError> { let cache_key = format!("crypto_{}_{}", crypto_codes.join(","), fiat_currency); - + // 检查缓存(5分钟有效期) if let Some(cached) = self.cache.get(&cache_key) { if !cached.is_expired(Duration::minutes(5)) { @@ -405,12 +340,11 @@ impl ExchangeRateApiService { return Ok(cached.rates.clone()); } } - + // 尝试从多个加密货币提供商获取(顺序可配置:CRYPTO_PROVIDER_ORDER=coingecko,coincap) let mut prices = None; let mut source = String::new(); - let order_env = std::env::var("CRYPTO_PROVIDER_ORDER") - .unwrap_or_else(|_| "coingecko,coincap,binance".to_string()); + let order_env = std::env::var("CRYPTO_PROVIDER_ORDER").unwrap_or_else(|_| "coingecko,coincap,binance".to_string()); let providers: Vec = order_env .split(',') .map(|s| s.trim().to_lowercase()) @@ -418,50 +352,33 @@ impl ExchangeRateApiService { .collect(); for p in providers { match p.as_str() { - "coingecko" => match self - .fetch_from_coingecko(&crypto_codes, fiat_currency) - .await - { - Ok(pr) => { - prices = Some(pr); - source = "coingecko".to_string(); - } + "coingecko" => match self.fetch_from_coingecko(&crypto_codes, fiat_currency).await { + Ok(pr) => { prices = Some(pr); source = "coingecko".to_string(); }, Err(e) => warn!("Failed to fetch from CoinGecko: {}", e), }, "coincap" => { // CoinCap effectively USD; for non-USD we still return USD prices for cross computation by caller for code in &crypto_codes { if let Ok(price) = self.fetch_from_coincap(code).await { - if prices.is_none() { - prices = Some(HashMap::new()); - } - if let Some(ref mut pmap) = prices { - pmap.insert(code.to_string(), price); - } + if prices.is_none() { prices = Some(HashMap::new()); } + if let Some(ref mut pmap) = prices { pmap.insert(code.to_string(), price); } } } - if prices.is_some() { - source = "coincap".to_string(); - } + if prices.is_some() { source = "coincap".to_string(); } } "binance" => { // Binance provides USDT pairs. Only support USD (treated as USDT) directly. if fiat_currency.to_uppercase() == "USD" { if let Ok(pmap) = self.fetch_from_binance(&crypto_codes).await { - if !pmap.is_empty() { - prices = Some(pmap); - source = "binance".to_string(); - } + if !pmap.is_empty() { prices = Some(pmap); source = "binance".to_string(); } } } } other => warn!("Unknown crypto provider: {}", other), } - if prices.is_some() { - break; - } + if prices.is_some() { break; } } - + // 更新缓存 if let Some(prices) = prices { self.cache.insert( @@ -474,18 +391,14 @@ impl ExchangeRateApiService { ); return Ok(prices); } - + // 返回默认价格 warn!("All crypto APIs failed, returning default prices"); Ok(self.get_default_crypto_prices()) } - + /// 从 CoinGecko 获取加密货币价格 - async fn fetch_from_coingecko( - &self, - crypto_codes: &[&str], - fiat_currency: &str, - ) -> Result, ServiceError> { + async fn fetch_from_coingecko(&self, crypto_codes: &[&str], fiat_currency: &str) -> Result, ServiceError> { // CoinGecko ID 映射 let id_map: HashMap<&str, &str> = [ ("BTC", "bitcoin"), @@ -512,54 +425,49 @@ impl ExchangeRateApiService { ("OP", "optimism"), ("SHIB", "shiba-inu"), ("TRX", "tron"), - ] - .iter() - .cloned() - .collect(); - + ].iter().cloned().collect(); + let ids: Vec = crypto_codes .iter() .filter_map(|code| id_map.get(code).map(|id| id.to_string())) .collect(); - + if ids.is_empty() { return Ok(HashMap::new()); } - + let url = format!( "https://api.coingecko.com/api/v3/simple/price?ids={}&vs_currencies={}&include_24hr_change=true&include_market_cap=true&include_24hr_vol=true", ids.join(","), fiat_currency.to_lowercase() ); - - let response = - self.client - .get(&url) - .send() - .await - .map_err(|e| ServiceError::ExternalApi { - message: format!("Failed to fetch from CoinGecko: {}", e), - })?; - + + let response = self.client + .get(&url) + .send() + .await + .map_err(|e| ServiceError::ExternalApi { + message: format!("Failed to fetch from CoinGecko: {}", e), + })?; + if !response.status().is_success() { return Err(ServiceError::ExternalApi { message: format!("CoinGecko API returned status: {}", response.status()), }); } - - let data: HashMap> = - response - .json() - .await - .map_err(|e| ServiceError::ExternalApi { - message: format!("Failed to parse CoinGecko response: {}", e), - })?; - + + let data: HashMap> = response + .json() + .await + .map_err(|e| ServiceError::ExternalApi { + message: format!("Failed to parse CoinGecko response: {}", e), + })?; + let mut prices = HashMap::new(); - + // 反向映射回代码 let reverse_map: HashMap<&str, &str> = id_map.iter().map(|(k, v)| (*v, *k)).collect(); - + for (id, price_data) in data { if let Some(code) = reverse_map.get(id.as_str()) { if let Some(price) = price_data.get(&fiat_currency.to_lowercase()) { @@ -569,10 +477,10 @@ impl ExchangeRateApiService { } } } - + Ok(prices) } - + /// 从 CoinCap 获取单个加密货币价格 (仅USD) async fn fetch_from_coincap(&self, crypto_code: &str) -> Result { let id_map: HashMap<&str, &str> = [ @@ -592,71 +500,57 @@ impl ExchangeRateApiService { ("LTC", "litecoin"), ("UNI", "uniswap"), ("ATOM", "cosmos"), - ] - .iter() - .cloned() - .collect(); - + ].iter().cloned().collect(); + let id = id_map.get(crypto_code).ok_or(ServiceError::NotFound { resource_type: "CryptoId".to_string(), id: crypto_code.to_string(), })?; - + let url = format!("https://api.coincap.io/v2/assets/{}", id); - - let response = - self.client - .get(&url) - .send() - .await - .map_err(|e| ServiceError::ExternalApi { - message: format!("Failed to fetch from CoinCap: {}", e), - })?; - + + let response = self.client + .get(&url) + .send() + .await + .map_err(|e| ServiceError::ExternalApi { + message: format!("Failed to fetch from CoinCap: {}", e), + })?; + if !response.status().is_success() { return Err(ServiceError::ExternalApi { message: format!("CoinCap API returned status: {}", response.status()), }); } - - let data: CoinCapResponse = - response - .json() - .await - .map_err(|e| ServiceError::ExternalApi { - message: format!("Failed to parse CoinCap response: {}", e), - })?; - + + let data: CoinCapResponse = response + .json() + .await + .map_err(|e| ServiceError::ExternalApi { + message: format!("Failed to parse CoinCap response: {}", e), + })?; + Decimal::from_str(&data.data.price_usd).map_err(|e| ServiceError::ExternalApi { message: format!("Failed to parse price: {}", e), }) } /// 从 Binance 获取加密货币 USDT 价格 (近似 USD) - async fn fetch_from_binance( - &self, - crypto_codes: &[&str], - ) -> Result, ServiceError> { + async fn fetch_from_binance(&self, crypto_codes: &[&str]) -> Result, ServiceError> { let mut result = HashMap::new(); for code in crypto_codes { let uc = code.to_uppercase(); - if uc == "USD" || uc == "USDT" { + if uc == "USD" || uc == "USDT" { result.insert(uc.clone(), Decimal::ONE); - continue; + continue; } let symbol = format!("{}USDT", uc); - let url = format!( - "https://api.binance.com/api/v3/ticker/price?symbol={}", - symbol - ); - let resp = - self.client - .get(&url) - .send() - .await - .map_err(|e| ServiceError::ExternalApi { - message: format!("Failed to fetch from Binance: {}", e), - })?; + let url = format!("https://api.binance.com/api/v3/ticker/price?symbol={}", symbol); + let resp = self.client + .get(&url) + .send() + .await + .map_err(|e| ServiceError::ExternalApi { message: format!("Failed to fetch from Binance: {}", e) })?; if !resp.status().is_success() { // Skip this code silently; continue other codes continue; @@ -671,14 +565,14 @@ impl ExchangeRateApiService { } Ok(result) } - + /// 获取默认汇率(用于API失败时的备用) fn get_default_rates(&self, base_currency: &str) -> HashMap { let mut rates = HashMap::new(); - + // 基础货币 rates.insert(base_currency.to_string(), Decimal::ONE); - + // 主要货币的大概汇率(以USD为基准) let usd_rates: HashMap<&str, f64> = [ ("USD", 1.0), @@ -701,14 +595,11 @@ impl ExchangeRateApiService { ("BRL", 5.0), ("RUB", 75.0), ("ZAR", 15.0), - ] - .iter() - .cloned() - .collect(); - + ].iter().cloned().collect(); + // 获取基础货币对USD的汇率 let base_to_usd = usd_rates.get(base_currency).copied().unwrap_or(1.0); - + // 计算相对汇率 for (currency, usd_rate) in usd_rates.iter() { if *currency != base_currency { @@ -718,10 +609,10 @@ impl ExchangeRateApiService { } } } - + rates } - + /// 获取默认加密货币价格(USD) fn get_default_crypto_prices(&self) -> HashMap { let prices: HashMap<&str, f64> = [ @@ -741,31 +632,26 @@ impl ExchangeRateApiService { ("LTC", 100.0), ("UNI", 6.0), ("ATOM", 10.0), - ] - .iter() - .cloned() - .collect(); - + ].iter().cloned().collect(); + let mut result = HashMap::new(); for (code, price) in prices { if let Ok(decimal_price) = Decimal::from_str(&price.to_string()) { result.insert(code.to_string(), decimal_price); } } - + result } } impl Default for ExchangeRateApiService { - fn default() -> Self { - Self::new() - } + fn default() -> Self { Self::new() } } // 单例模式的全局服务实例 -use std::sync::Arc; use tokio::sync::Mutex; +use std::sync::Arc; lazy_static::lazy_static! { pub static ref EXCHANGE_RATE_SERVICE: Arc> = Arc::new(Mutex::new(ExchangeRateApiService::new())); diff --git a/jive-api/src/services/family_service.rs b/jive-api/src/services/family_service.rs index c617c9c3..1e446026 100644 --- a/jive-api/src/services/family_service.rs +++ b/jive-api/src/services/family_service.rs @@ -17,39 +17,38 @@ impl FamilyService { pub fn new(pool: PgPool) -> Self { Self { pool } } - + pub async fn create_family( &self, user_id: Uuid, request: CreateFamilyRequest, ) -> Result { let mut tx = self.pool.begin().await?; - + // Check if user already owns a family by checking if they are an owner in any family let existing_family_count = sqlx::query_scalar::<_, i64>( r#" SELECT COUNT(*) FROM family_members WHERE user_id = $1 AND role = 'owner' - "#, + "# ) .bind(user_id) .fetch_one(&mut *tx) .await?; - + if existing_family_count > 0 { - return Err(ServiceError::Conflict( - "用户已创建家庭,每个用户只能创建一个家庭".to_string(), - )); + return Err(ServiceError::Conflict("用户已创建家庭,每个用户只能创建一个家庭".to_string())); } - + // Get user's name for default family name - let user_name: Option = - sqlx::query_scalar("SELECT COALESCE(full_name, email) FROM users WHERE id = $1") - .bind(user_id) - .fetch_one(&mut *tx) - .await?; - + let user_name: Option = sqlx::query_scalar( + "SELECT COALESCE(full_name, email) FROM users WHERE id = $1" + ) + .bind(user_id) + .fetch_one(&mut *tx) + .await?; + // Use provided name or default to "用户名的家庭" let family_name = if let Some(name) = request.name { if name.trim().is_empty() { @@ -60,23 +59,20 @@ impl FamilyService { } else { format!("{}的家庭", user_name.unwrap_or_else(|| "我".to_string())) }; - + // Create family let family_id = Uuid::new_v4(); let invite_code = Family::generate_invite_code(); - + let family = sqlx::query_as::<_, Family>( r#" - INSERT INTO families ( - id, name, owner_id, currency, timezone, locale, invite_code, member_count, created_at, updated_at - ) - VALUES ($1, $2, $3, $4, $5, $6, $7, 1, $8, $9) + INSERT INTO families (id, name, currency, timezone, locale, invite_code, member_count, created_at, updated_at) + VALUES ($1, $2, $3, $4, $5, $6, 1, $7, $8) RETURNING * "# ) .bind(family_id) .bind(&family_name) - .bind(user_id) .bind(request.currency.as_deref().unwrap_or("CNY")) .bind(request.timezone.as_deref().unwrap_or("Asia/Shanghai")) .bind(request.locale.as_deref().unwrap_or("zh-CN")) @@ -85,16 +81,16 @@ impl FamilyService { .bind(Utc::now()) .fetch_one(&mut *tx) .await?; - + // Create owner membership let owner_permissions = MemberRole::Owner.default_permissions(); let permissions_json = serde_json::to_value(&owner_permissions)?; - + sqlx::query( r#" INSERT INTO family_members (family_id, user_id, role, permissions, joined_at) VALUES ($1, $2, $3, $4, $5) - "#, + "# ) .bind(family_id) .bind(user_id) @@ -103,11 +99,11 @@ impl FamilyService { .bind(Utc::now()) .execute(&mut *tx) .await?; - - // Create default ledger (use created_by column for author) + + // Create default ledger sqlx::query( r#" - INSERT INTO ledgers (id, family_id, name, currency, created_by, is_default, created_at, updated_at) + INSERT INTO ledgers (id, family_id, name, currency, owner_id, is_default, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, true, $6, $7) "# ) @@ -120,30 +116,30 @@ impl FamilyService { .bind(Utc::now()) .execute(&mut *tx) .await?; - + tx.commit().await?; - + Ok(family) } - + pub async fn get_family( &self, ctx: &ServiceContext, family_id: Uuid, ) -> Result { ctx.require_permission(Permission::ViewFamilyInfo)?; - + let family = sqlx::query_as::<_, Family>( - "SELECT * FROM families WHERE id = $1 AND deleted_at IS NULL", + "SELECT * FROM families WHERE id = $1 AND deleted_at IS NULL" ) .bind(family_id) .fetch_optional(&self.pool) .await? .ok_or_else(|| ServiceError::not_found("Family", family_id))?; - + Ok(family) } - + pub async fn update_family( &self, ctx: &ServiceContext, @@ -151,62 +147,64 @@ impl FamilyService { request: UpdateFamilyRequest, ) -> Result { ctx.require_permission(Permission::UpdateFamilyInfo)?; - + let mut tx = self.pool.begin().await?; - + // Build dynamic update query let mut query = String::from("UPDATE families SET updated_at = $1"); let mut bind_idx = 2; let mut binds = vec![]; - + if let Some(name) = &request.name { query.push_str(&format!(", name = ${}", bind_idx)); binds.push(name.clone()); bind_idx += 1; } - + if let Some(currency) = &request.currency { query.push_str(&format!(", currency = ${}", bind_idx)); binds.push(currency.clone()); bind_idx += 1; } - + if let Some(timezone) = &request.timezone { query.push_str(&format!(", timezone = ${}", bind_idx)); binds.push(timezone.clone()); bind_idx += 1; } - + if let Some(locale) = &request.locale { query.push_str(&format!(", locale = ${}", bind_idx)); binds.push(locale.clone()); bind_idx += 1; } - + if let Some(date_format) = &request.date_format { query.push_str(&format!(", date_format = ${}", bind_idx)); binds.push(date_format.clone()); bind_idx += 1; } - + query.push_str(&format!(" WHERE id = ${} RETURNING *", bind_idx)); - + // Execute update let mut query_builder = sqlx::query_as::<_, Family>(&query) .bind(Utc::now()) .bind(family_id); - + for bind in binds { query_builder = query_builder.bind(bind); } - - let family = query_builder.fetch_one(&mut *tx).await?; - + + let family = query_builder + .fetch_one(&mut *tx) + .await?; + tx.commit().await?; - + Ok(family) } - + pub async fn delete_family( &self, ctx: &ServiceContext, @@ -214,27 +212,32 @@ impl FamilyService { ) -> Result<(), ServiceError> { ctx.require_permission(Permission::DeleteFamily)?; ctx.require_owner()?; - + // Soft delete - just mark as deleted - sqlx::query("UPDATE families SET deleted_at = $1, updated_at = $1 WHERE id = $2") - .bind(Utc::now()) - .bind(family_id) - .execute(&self.pool) - .await?; - + sqlx::query( + "UPDATE families SET deleted_at = $1, updated_at = $1 WHERE id = $2" + ) + .bind(Utc::now()) + .bind(family_id) + .execute(&self.pool) + .await?; + // Update user's current family if this was their current one sqlx::query( "UPDATE users SET current_family_id = NULL - WHERE current_family_id = $1", + WHERE current_family_id = $1" ) .bind(family_id) .execute(&self.pool) .await?; - + Ok(()) } - - pub async fn get_user_families(&self, user_id: Uuid) -> Result, ServiceError> { + + pub async fn get_user_families( + &self, + user_id: Uuid, + ) -> Result, ServiceError> { // Only show families that: // 1. Have more than 1 member (multi-person families) // 2. Or the user is the owner (even if single-person) @@ -247,16 +250,20 @@ impl FamilyService { AND f.deleted_at IS NULL AND (f.member_count > 1 OR fm.role = 'owner') ORDER BY fm.joined_at DESC - "#, + "# ) .bind(user_id) .fetch_all(&self.pool) .await?; - + Ok(families) } - - pub async fn switch_family(&self, user_id: Uuid, family_id: Uuid) -> Result<(), ServiceError> { + + pub async fn switch_family( + &self, + user_id: Uuid, + family_id: Uuid, + ) -> Result<(), ServiceError> { // Verify user is member of the family let is_member = sqlx::query_scalar::<_, bool>( r#" @@ -264,63 +271,67 @@ impl FamilyService { SELECT 1 FROM family_members WHERE user_id = $1 AND family_id = $2 ) - "#, + "# ) .bind(user_id) .bind(family_id) .fetch_one(&self.pool) .await?; - + if !is_member { return Err(ServiceError::PermissionDenied); } - + // Update current family - sqlx::query("UPDATE users SET current_family_id = $1 WHERE id = $2") - .bind(family_id) - .bind(user_id) - .execute(&self.pool) - .await?; - + sqlx::query( + "UPDATE users SET current_family_id = $1 WHERE id = $2" + ) + .bind(family_id) + .bind(user_id) + .execute(&self.pool) + .await?; + Ok(()) } - + pub async fn join_family_by_invite_code( &self, user_id: Uuid, invite_code: String, ) -> Result { let mut tx = self.pool.begin().await?; - + // Find family by invite code - let family = sqlx::query_as::<_, Family>("SELECT * FROM families WHERE invite_code = $1") - .bind(&invite_code) - .fetch_optional(&mut *tx) - .await? - .ok_or_else(|| ServiceError::InvalidInvitation)?; - + let family = sqlx::query_as::<_, Family>( + "SELECT * FROM families WHERE invite_code = $1" + ) + .bind(&invite_code) + .fetch_optional(&mut *tx) + .await? + .ok_or_else(|| ServiceError::InvalidInvitation)?; + // Check if user is already a member let existing_member: Option = sqlx::query_scalar( - "SELECT COUNT(*) FROM family_members WHERE family_id = $1 AND user_id = $2", + "SELECT COUNT(*) FROM family_members WHERE family_id = $1 AND user_id = $2" ) .bind(family.id) .bind(user_id) .fetch_one(&mut *tx) .await?; - + if existing_member.unwrap_or(0) > 0 { return Err(ServiceError::Conflict("您已经是该家庭的成员".to_string())); } - + // Add user as a member let member_permissions = MemberRole::Member.default_permissions(); let permissions_json = serde_json::to_value(&member_permissions)?; - + sqlx::query( r#" INSERT INTO family_members (family_id, user_id, role, permissions, joined_at) VALUES ($1, $2, $3, $4, $5) - "#, + "# ) .bind(family.id) .bind(user_id) @@ -329,60 +340,66 @@ impl FamilyService { .bind(Utc::now()) .execute(&mut *tx) .await?; - + // Update member count - sqlx::query("UPDATE families SET member_count = member_count + 1 WHERE id = $1") - .bind(family.id) - .execute(&mut *tx) - .await?; - + sqlx::query( + "UPDATE families SET member_count = member_count + 1 WHERE id = $1" + ) + .bind(family.id) + .execute(&mut *tx) + .await?; + tx.commit().await?; - + Ok(family) } - + pub async fn get_family_statistics( &self, family_id: Uuid, ) -> Result { // Get member count - let member_count: i64 = - sqlx::query_scalar("SELECT COUNT(*) FROM family_members WHERE family_id = $1") - .bind(family_id) - .fetch_one(&self.pool) - .await?; - + let member_count: i64 = sqlx::query_scalar( + "SELECT COUNT(*) FROM family_members WHERE family_id = $1" + ) + .bind(family_id) + .fetch_one(&self.pool) + .await?; + // Get ledger count - let ledger_count: i64 = - sqlx::query_scalar("SELECT COUNT(*) FROM ledgers WHERE family_id = $1") - .bind(family_id) - .fetch_one(&self.pool) - .await?; - + let ledger_count: i64 = sqlx::query_scalar( + "SELECT COUNT(*) FROM ledgers WHERE family_id = $1" + ) + .bind(family_id) + .fetch_one(&self.pool) + .await?; + // Get account count - let account_count: i64 = - sqlx::query_scalar("SELECT COUNT(*) FROM accounts WHERE family_id = $1") - .bind(family_id) - .fetch_one(&self.pool) - .await?; - + let account_count: i64 = sqlx::query_scalar( + "SELECT COUNT(*) FROM accounts WHERE family_id = $1" + ) + .bind(family_id) + .fetch_one(&self.pool) + .await?; + // Get transaction count - let transaction_count: i64 = - sqlx::query_scalar("SELECT COUNT(*) FROM transactions WHERE family_id = $1") - .bind(family_id) - .fetch_one(&self.pool) - .await?; - + let transaction_count: i64 = sqlx::query_scalar( + "SELECT COUNT(*) FROM transactions WHERE family_id = $1" + ) + .bind(family_id) + .fetch_one(&self.pool) + .await?; + // Get total balance let total_balance: Option = sqlx::query_scalar( "SELECT SUM(current_balance) FROM accounts a JOIN ledgers l ON a.ledger_id = l.id - WHERE l.family_id = $1", + WHERE l.family_id = $1" ) .bind(family_id) .fetch_one(&self.pool) .await?; - + Ok(serde_json::json!({ "member_count": member_count, "ledger_count": ledger_count, @@ -391,53 +408,61 @@ impl FamilyService { "total_balance": total_balance.unwrap_or(rust_decimal::Decimal::ZERO), })) } - + pub async fn regenerate_invite_code( &self, ctx: &ServiceContext, family_id: Uuid, ) -> Result { ctx.require_permission(Permission::InviteMembers)?; - + let new_code = Family::generate_invite_code(); - - sqlx::query("UPDATE families SET invite_code = $1, updated_at = $2 WHERE id = $3") - .bind(&new_code) - .bind(Utc::now()) - .bind(family_id) - .execute(&self.pool) - .await?; - + + sqlx::query( + "UPDATE families SET invite_code = $1, updated_at = $2 WHERE id = $3" + ) + .bind(&new_code) + .bind(Utc::now()) + .bind(family_id) + .execute(&self.pool) + .await?; + Ok(new_code) } - - pub async fn leave_family(&self, user_id: Uuid, family_id: Uuid) -> Result<(), ServiceError> { + + pub async fn leave_family( + &self, + user_id: Uuid, + family_id: Uuid, + ) -> Result<(), ServiceError> { let mut tx = self.pool.begin().await?; - + // Check if user is the owner let role: Option = sqlx::query_scalar( - "SELECT role FROM family_members WHERE family_id = $1 AND user_id = $2", + "SELECT role FROM family_members WHERE family_id = $1 AND user_id = $2" ) .bind(family_id) .bind(user_id) .fetch_optional(&mut *tx) .await?; - + match role.as_deref() { Some("owner") => { // Owner cannot leave, must transfer ownership or delete family Err(ServiceError::BusinessRuleViolation( - "家庭所有者不能退出家庭,请先转让所有权或删除家庭".to_string(), + "家庭所有者不能退出家庭,请先转让所有权或删除家庭".to_string() )) } Some(_) => { // Remove member from family - sqlx::query("DELETE FROM family_members WHERE family_id = $1 AND user_id = $2") - .bind(family_id) - .bind(user_id) - .execute(&mut *tx) - .await?; - + sqlx::query( + "DELETE FROM family_members WHERE family_id = $1 AND user_id = $2" + ) + .bind(family_id) + .bind(user_id) + .execute(&mut *tx) + .await?; + // Update member count sqlx::query( "UPDATE families SET member_count = GREATEST(member_count - 1, 0) WHERE id = $1" @@ -445,24 +470,26 @@ impl FamilyService { .bind(family_id) .execute(&mut *tx) .await?; - + // Update user's current family if this was their current one sqlx::query( "UPDATE users SET current_family_id = NULL - WHERE id = $1 AND current_family_id = $2", + WHERE id = $1 AND current_family_id = $2" ) .bind(user_id) .bind(family_id) .execute(&mut *tx) .await?; - + tx.commit().await?; Ok(()) } - None => Err(ServiceError::NotFound { - resource_type: "FamilyMember".to_string(), - id: user_id.to_string(), - }), + None => { + Err(ServiceError::NotFound { + resource_type: "FamilyMember".to_string(), + id: user_id.to_string(), + }) + } } } } diff --git a/jive-api/src/services/member_service.rs b/jive-api/src/services/member_service.rs index fd5c1d21..ac5de33c 100644 --- a/jive-api/src/services/member_service.rs +++ b/jive-api/src/services/member_service.rs @@ -17,7 +17,7 @@ impl MemberService { pub fn new(pool: PgPool) -> Self { Self { pool } } - + pub async fn add_member( &self, ctx: &ServiceContext, @@ -25,7 +25,7 @@ impl MemberService { role: MemberRole, ) -> Result { ctx.require_permission(Permission::InviteMembers)?; - + // Check if already member let exists = sqlx::query_scalar::<_, bool>( r#" @@ -33,21 +33,21 @@ impl MemberService { SELECT 1 FROM family_members WHERE family_id = $1 AND user_id = $2 ) - "#, + "# ) .bind(ctx.family_id) .bind(user_id) .fetch_one(&self.pool) .await?; - + if exists { return Err(ServiceError::MemberAlreadyExists); } - + // Add member let permissions = role.default_permissions(); let permissions_json = serde_json::to_value(&permissions)?; - + let member = sqlx::query_as::<_, FamilyMember>( r#" INSERT INTO family_members ( @@ -55,7 +55,7 @@ impl MemberService { ) VALUES ($1, $2, $3, $4, $5, $6) RETURNING * - "#, + "# ) .bind(ctx.family_id) .bind(user_id) @@ -65,50 +65,52 @@ impl MemberService { .bind(Utc::now()) .fetch_one(&self.pool) .await?; - + Ok(member) } - + pub async fn remove_member( &self, ctx: &ServiceContext, user_id: Uuid, ) -> Result<(), ServiceError> { ctx.require_permission(Permission::RemoveMembers)?; - + // Get member info let member_role = sqlx::query_scalar::<_, String>( - "SELECT role FROM family_members WHERE family_id = $1 AND user_id = $2", + "SELECT role FROM family_members WHERE family_id = $1 AND user_id = $2" ) .bind(ctx.family_id) .bind(user_id) .fetch_optional(&self.pool) .await? .ok_or_else(|| ServiceError::not_found("Member", user_id))?; - + // Cannot remove owner if member_role == "owner" { return Err(ServiceError::CannotRemoveOwner); } - + // Check if actor can manage this role let target_role = MemberRole::from_str_name(&member_role) .ok_or_else(|| ServiceError::ValidationError("Invalid role".to_string()))?; - + if !ctx.can_manage_role(target_role) { return Err(ServiceError::PermissionDenied); } - + // Remove member - sqlx::query("DELETE FROM family_members WHERE family_id = $1 AND user_id = $2") - .bind(ctx.family_id) - .bind(user_id) - .execute(&self.pool) - .await?; - + sqlx::query( + "DELETE FROM family_members WHERE family_id = $1 AND user_id = $2" + ) + .bind(ctx.family_id) + .bind(user_id) + .execute(&self.pool) + .await?; + Ok(()) } - + pub async fn update_member_role( &self, ctx: &ServiceContext, @@ -116,38 +118,38 @@ impl MemberService { new_role: MemberRole, ) -> Result { ctx.require_permission(Permission::UpdateMemberRoles)?; - + // Get current role let current_role = sqlx::query_scalar::<_, String>( - "SELECT role FROM family_members WHERE family_id = $1 AND user_id = $2", + "SELECT role FROM family_members WHERE family_id = $1 AND user_id = $2" ) .bind(ctx.family_id) .bind(user_id) .fetch_optional(&self.pool) .await? .ok_or_else(|| ServiceError::not_found("Member", user_id))?; - + // Cannot change owner role if current_role == "owner" { return Err(ServiceError::CannotChangeOwnerRole); } - + // Check permissions if !ctx.can_manage_role(new_role) { return Err(ServiceError::PermissionDenied); } - + // Update role and permissions let permissions = new_role.default_permissions(); let permissions_json = serde_json::to_value(&permissions)?; - + let member = sqlx::query_as::<_, FamilyMember>( r#" UPDATE family_members SET role = $1, permissions = $2 WHERE family_id = $3 AND user_id = $4 RETURNING * - "#, + "# ) .bind(new_role.to_string()) .bind(permissions_json) @@ -155,10 +157,10 @@ impl MemberService { .bind(user_id) .fetch_one(&self.pool) .await?; - + Ok(member) } - + pub async fn update_member_permissions( &self, ctx: &ServiceContext, @@ -166,50 +168,50 @@ impl MemberService { permissions: Vec, ) -> Result { ctx.require_permission(Permission::UpdateMemberRoles)?; - + // Get member role let member_role = sqlx::query_scalar::<_, String>( - "SELECT role FROM family_members WHERE family_id = $1 AND user_id = $2", + "SELECT role FROM family_members WHERE family_id = $1 AND user_id = $2" ) .bind(ctx.family_id) .bind(user_id) .fetch_optional(&self.pool) .await? .ok_or_else(|| ServiceError::not_found("Member", user_id))?; - + // Cannot change owner permissions if member_role == "owner" { return Err(ServiceError::BusinessRuleViolation( - "Owner permissions cannot be customized".to_string(), + "Owner permissions cannot be customized".to_string() )); } - + // Update permissions let permissions_json = serde_json::to_value(&permissions)?; - + let member = sqlx::query_as::<_, FamilyMember>( r#" UPDATE family_members SET permissions = $1 WHERE family_id = $2 AND user_id = $3 RETURNING * - "#, + "# ) .bind(permissions_json) .bind(ctx.family_id) .bind(user_id) .fetch_one(&self.pool) .await?; - + Ok(member) } - + pub async fn get_family_members( &self, ctx: &ServiceContext, ) -> Result, ServiceError> { ctx.require_permission(Permission::ViewMembers)?; - + let members = sqlx::query_as::<_, MemberWithUserInfo>( r#" SELECT @@ -225,15 +227,15 @@ impl MemberService { JOIN users u ON fm.user_id = u.id WHERE fm.family_id = $1 ORDER BY fm.joined_at - "#, + "# ) .bind(ctx.family_id) .fetch_all(&self.pool) .await?; - + Ok(members) } - + pub async fn check_permission( &self, user_id: Uuid, @@ -244,13 +246,13 @@ impl MemberService { r#" SELECT permissions FROM family_members WHERE family_id = $1 AND user_id = $2 - "#, + "# ) .bind(family_id) .bind(user_id) .fetch_optional(&self.pool) .await?; - + if let Some(json) = permissions_json { let permissions: Vec = serde_json::from_value(json)?; Ok(permissions.contains(&permission)) @@ -258,7 +260,7 @@ impl MemberService { Ok(false) } } - + pub async fn get_member_context( &self, user_id: Uuid, @@ -271,7 +273,7 @@ impl MemberService { email: String, full_name: Option, } - + let row = sqlx::query_as::<_, MemberContextRow>( r#" SELECT @@ -282,19 +284,19 @@ impl MemberService { FROM family_members fm JOIN users u ON fm.user_id = u.id WHERE fm.family_id = $1 AND fm.user_id = $2 - "#, + "# ) .bind(family_id) .bind(user_id) .fetch_optional(&self.pool) .await? .ok_or(ServiceError::PermissionDenied)?; - + let role = MemberRole::from_str_name(&row.role) .ok_or_else(|| ServiceError::ValidationError("Invalid role".to_string()))?; - + let permissions: Vec = serde_json::from_value(row.permissions)?; - + Ok(ServiceContext::new( user_id, family_id, diff --git a/jive-api/src/services/mod.rs b/jive-api/src/services/mod.rs index 9ac7086c..070d640a 100644 --- a/jive-api/src/services/mod.rs +++ b/jive-api/src/services/mod.rs @@ -1,36 +1,36 @@ #![allow(dead_code)] -pub mod audit_service; -pub mod auth_service; -pub mod avatar_service; -pub mod budget_service; pub mod context; -pub mod currency_service; pub mod error; -pub mod exchange_rate_api; pub mod family_service; -pub mod invitation_service; pub mod member_service; -pub mod scheduled_tasks; -pub mod tag_service; +pub mod invitation_service; +pub mod auth_service; +pub mod audit_service; pub mod transaction_service; +pub mod budget_service; pub mod verification_service; +pub mod avatar_service; +pub mod currency_service; +pub mod exchange_rate_api; +pub mod scheduled_tasks; +pub mod tag_service; -pub use audit_service::AuditService; -pub use auth_service::AuthService; -#[allow(unused_imports)] -pub use avatar_service::{Avatar, AvatarService, AvatarStyle}; -#[allow(unused_imports)] -pub use budget_service::BudgetService; pub use context::ServiceContext; -#[allow(unused_imports)] -pub use currency_service::{Currency, CurrencyService, ExchangeRate, FamilyCurrencySettings}; pub use error::ServiceError; pub use family_service::FamilyService; -pub use invitation_service::InvitationService; pub use member_service::MemberService; -#[allow(unused_imports)] -pub use tag_service::{TagDto, TagService, TagSummary}; +pub use invitation_service::InvitationService; +pub use auth_service::AuthService; +pub use audit_service::AuditService; #[allow(unused_imports)] pub use transaction_service::TransactionService; +#[allow(unused_imports)] +pub use budget_service::BudgetService; pub use verification_service::VerificationService; +#[allow(unused_imports)] +pub use avatar_service::{Avatar, AvatarService, AvatarStyle}; +#[allow(unused_imports)] +pub use currency_service::{CurrencyService, Currency, ExchangeRate, FamilyCurrencySettings}; +#[allow(unused_imports)] +pub use tag_service::{TagService, TagDto, TagSummary}; diff --git a/jive-api/src/services/scheduled_tasks.rs b/jive-api/src/services/scheduled_tasks.rs index 373be4c5..3b604358 100644 --- a/jive-api/src/services/scheduled_tasks.rs +++ b/jive-api/src/services/scheduled_tasks.rs @@ -1,8 +1,8 @@ // Utc import not needed after refactor use sqlx::PgPool; -use std::sync::Arc; use tokio::time::{interval, Duration as TokioDuration}; -use tracing::{error, info, warn}; +use tracing::{info, error, warn}; +use std::sync::Arc; use super::currency_service::CurrencyService; @@ -15,28 +15,25 @@ impl ScheduledTaskManager { pub fn new(pool: Arc) -> Self { Self { pool } } - + /// 启动所有定时任务 pub async fn start_all_tasks(self: Arc) { info!("Starting scheduled tasks..."); - + // 延迟启动时间(秒) let startup_delay = std::env::var("STARTUP_DELAY") .unwrap_or_else(|_| "30".to_string()) .parse::() .unwrap_or(30); - + // 启动汇率更新任务(延迟30秒后开始,每15分钟执行) let manager_clone = Arc::clone(&self); tokio::spawn(async move { - info!( - "Exchange rate update task will start in {} seconds", - startup_delay - ); + info!("Exchange rate update task will start in {} seconds", startup_delay); tokio::time::sleep(TokioDuration::from_secs(startup_delay)).await; manager_clone.run_exchange_rate_update_task().await; }); - + // 启动加密货币价格更新任务(延迟20秒后开始,每5分钟执行) let manager_clone = Arc::clone(&self); tokio::spawn(async move { @@ -44,7 +41,7 @@ impl ScheduledTaskManager { tokio::time::sleep(TokioDuration::from_secs(20)).await; manager_clone.run_crypto_price_update_task().await; }); - + // 启动缓存清理任务(延迟60秒后开始,每小时执行) let manager_clone = Arc::clone(&self); tokio::spawn(async move { @@ -68,32 +65,29 @@ impl ScheduledTaskManager { .ok() .and_then(|v| v.parse::().ok()) .unwrap_or(60); - info!( - "Manual rate cleanup task will start in 90 seconds, interval: {} minutes", - mins - ); + info!("Manual rate cleanup task will start in 90 seconds, interval: {} minutes", mins); tokio::time::sleep(TokioDuration::from_secs(90)).await; manager_clone.run_manual_overrides_cleanup_task(mins).await; }); - + info!("All scheduled tasks initialized (will start after delay)"); } - + /// 汇率更新任务 async fn run_exchange_rate_update_task(&self) { let mut interval = interval(TokioDuration::from_secs(15 * 60)); // 15分钟 - + // 第一次执行汇率更新 info!("Starting initial exchange rate update"); self.update_exchange_rates().await; - + loop { interval.tick().await; info!("Running scheduled exchange rate update"); self.update_exchange_rates().await; } } - + /// 执行汇率更新 async fn update_exchange_rates(&self) { // 获取所有需要更新的基础货币 @@ -104,46 +98,43 @@ impl ScheduledTaskManager { return; } }; - + let currency_service = CurrencyService::new((*self.pool).clone()); - + for base_currency in base_currencies { match currency_service.fetch_latest_rates(&base_currency).await { Ok(_) => { info!("Successfully updated exchange rates for {}", base_currency); } Err(e) => { - warn!( - "Failed to update exchange rates for {}: {:?}", - base_currency, e - ); + warn!("Failed to update exchange rates for {}: {:?}", base_currency, e); } } - + // 避免API限流,每个请求之间等待1秒 tokio::time::sleep(TokioDuration::from_secs(1)).await; } } - + /// 加密货币价格更新任务 async fn run_crypto_price_update_task(&self) { let mut interval = interval(TokioDuration::from_secs(5 * 60)); // 5分钟 - + // 第一次执行 info!("Starting initial crypto price update"); self.update_crypto_prices().await; - + loop { interval.tick().await; info!("Running scheduled crypto price update"); self.update_crypto_prices().await; } } - + /// 执行加密货币价格更新 async fn update_crypto_prices(&self) { info!("Checking crypto price updates..."); - + // 检查是否有用户启用了加密货币 let crypto_enabled = match self.check_crypto_enabled().await { Ok(enabled) => enabled, @@ -152,20 +143,20 @@ impl ScheduledTaskManager { return; } }; - + if !crypto_enabled { return; } - + let currency_service = CurrencyService::new((*self.pool).clone()); - + // 主要加密货币列表 let crypto_codes = vec![ - "BTC", "ETH", "USDT", "BNB", "SOL", "XRP", "USDC", "ADA", "AVAX", "DOGE", "DOT", - "MATIC", "LINK", "LTC", "UNI", "ATOM", "COMP", "MKR", "AAVE", "SUSHI", "ARB", "OP", - "SHIB", "TRX", + "BTC", "ETH", "USDT", "BNB", "SOL", "XRP", "USDC", "ADA", + "AVAX", "DOGE", "DOT", "MATIC", "LINK", "LTC", "UNI", "ATOM", + "COMP", "MKR", "AAVE", "SUSHI", "ARB", "OP", "SHIB", "TRX" ]; - + // 获取需要更新的法定货币 let fiat_currencies = match self.get_crypto_base_currencies().await { Ok(currencies) => currencies, @@ -174,12 +165,9 @@ impl ScheduledTaskManager { vec!["USD".to_string()] // 默认至少更新USD } }; - + for fiat in fiat_currencies { - match currency_service - .fetch_crypto_prices(crypto_codes.clone(), &fiat) - .await - { + match currency_service.fetch_crypto_prices(crypto_codes.clone(), &fiat).await { Ok(_) => { info!("Successfully updated crypto prices in {}", fiat); } @@ -187,21 +175,21 @@ impl ScheduledTaskManager { warn!("Failed to update crypto prices in {}: {:?}", fiat, e); } } - + // 避免API限流 tokio::time::sleep(TokioDuration::from_secs(2)).await; } } - + /// 缓存清理任务 async fn run_cache_cleanup_task(&self) { let mut interval = interval(TokioDuration::from_secs(60 * 60)); // 1小时 - + loop { interval.tick().await; - + info!("Running cache cleanup task"); - + // 清理过期的汇率缓存 match sqlx::query!( r#" @@ -213,16 +201,13 @@ impl ScheduledTaskManager { .await { Ok(result) => { - info!( - "Cleaned up {} expired cache entries", - result.rows_affected() - ); + info!("Cleaned up {} expired cache entries", result.rows_affected()); } Err(e) => { error!("Failed to clean cache: {:?}", e); } } - + // 清理90天前的转换历史 match sqlx::query!( r#" @@ -234,15 +219,13 @@ impl ScheduledTaskManager { .await { Ok(result) => { - info!( - "Cleaned up {} old conversion history records", - result.rows_affected() - ); + info!("Cleaned up {} old conversion history records", result.rows_affected()); } Err(e) => { error!("Failed to clean conversion history: {:?}", e); } } + } } @@ -260,16 +243,14 @@ impl ScheduledTaskManager { WHERE is_manual = true AND manual_rate_expiry IS NOT NULL AND manual_rate_expiry <= NOW() - "#, + "# ) .execute(&*self.pool) .await { Ok(res) => { let n = res.rows_affected(); - if n > 0 { - info!("Cleared {} expired manual rate flags", n); - } + if n > 0 { info!("Cleared {} expired manual rate flags", n); } } Err(e) => { warn!("Failed to clear expired manual rates: {:?}", e); @@ -277,7 +258,7 @@ impl ScheduledTaskManager { } } } - + /// 获取所有活跃的基础货币 async fn get_active_base_currencies(&self) -> Result, sqlx::Error> { let raw = sqlx::query_scalar!( @@ -291,19 +272,15 @@ impl ScheduledTaskManager { .fetch_all(&*self.pool) .await?; let currencies: Vec = raw.into_iter().flatten().collect(); - + // 如果没有用户设置,至少更新主要货币 if currencies.is_empty() { - Ok(vec![ - "USD".to_string(), - "EUR".to_string(), - "CNY".to_string(), - ]) + Ok(vec!["USD".to_string(), "EUR".to_string(), "CNY".to_string()]) } else { Ok(currencies) } } - + /// 检查是否有用户启用了加密货币 async fn check_crypto_enabled(&self) -> Result { let count: Option = sqlx::query_scalar!( @@ -315,10 +292,10 @@ impl ScheduledTaskManager { ) .fetch_one(&*self.pool) .await?; - + Ok(count.unwrap_or(0) > 0) } - + /// 获取需要更新加密货币价格的法定货币 async fn get_crypto_base_currencies(&self) -> Result, sqlx::Error> { let raw = sqlx::query_scalar!( @@ -332,7 +309,7 @@ impl ScheduledTaskManager { .fetch_all(&*self.pool) .await?; let currencies: Vec = raw.into_iter().flatten().collect(); - + if currencies.is_empty() { Ok(vec!["USD".to_string()]) } else { diff --git a/jive-api/src/services/transaction_service.rs b/jive-api/src/services/transaction_service.rs index 2d0cfc5b..7e6aacdf 100644 --- a/jive-api/src/services/transaction_service.rs +++ b/jive-api/src/services/transaction_service.rs @@ -2,8 +2,8 @@ use crate::error::{ApiError, ApiResult}; use crate::models::transaction::{Transaction, TransactionCreate, TransactionType}; use chrono::{DateTime, Utc}; use sqlx::PgPool; -use std::collections::HashMap; use uuid::Uuid; +use std::collections::HashMap; pub struct TransactionService { pool: PgPool, @@ -16,24 +16,22 @@ impl TransactionService { /// 创建交易并更新账户余额 pub async fn create_transaction(&self, data: TransactionCreate) -> ApiResult { - let mut tx = self - .pool - .begin() - .await + let mut tx = self.pool.begin().await .map_err(|e| ApiError::DatabaseError(e.to_string()))?; // 生成交易ID let transaction_id = Uuid::new_v4(); // 克隆一份数据快照,避免后续字段 move 影响对 &data 的借用 let data_snapshot = data.clone(); - + // 获取账户当前余额 - let current_balance: Option<(f64,)> = - sqlx::query_as("SELECT current_balance FROM accounts WHERE id = $1 FOR UPDATE") - .bind(data.account_id) - .fetch_optional(&mut *tx) - .await - .map_err(|e| ApiError::DatabaseError(e.to_string()))?; + let current_balance: Option<(f64,)> = sqlx::query_as( + "SELECT current_balance FROM accounts WHERE id = $1 FOR UPDATE" + ) + .bind(data.account_id) + .fetch_optional(&mut *tx) + .await + .map_err(|e| ApiError::DatabaseError(e.to_string()))?; let current_balance = current_balance .ok_or_else(|| ApiError::NotFound("Account not found".to_string()))? @@ -57,7 +55,7 @@ impl TransactionService { $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, NOW(), NOW() ) RETURNING * - "#, + "# ) .bind(transaction_id) .bind(data.ledger_id) @@ -75,19 +73,21 @@ impl TransactionService { .map_err(|e| ApiError::DatabaseError(e.to_string()))?; // 更新账户余额 - sqlx::query("UPDATE accounts SET current_balance = $1, updated_at = NOW() WHERE id = $2") - .bind(new_balance) - .bind(data.account_id) - .execute(&mut *tx) - .await - .map_err(|e| ApiError::DatabaseError(e.to_string()))?; + sqlx::query( + "UPDATE accounts SET current_balance = $1, updated_at = NOW() WHERE id = $2" + ) + .bind(new_balance) + .bind(data.account_id) + .execute(&mut *tx) + .await + .map_err(|e| ApiError::DatabaseError(e.to_string()))?; // 记录账户余额历史 sqlx::query( r#" INSERT INTO account_balances (id, account_id, balance, balance_date, created_at) VALUES ($1, $2, $3, $4, NOW()) - "#, + "# ) .bind(Uuid::new_v4()) .bind(data.account_id) @@ -100,19 +100,12 @@ impl TransactionService { // 如果是转账,创建对应的转入交易 if data.transaction_type == TransactionType::Transfer { if let Some(target_account_id) = data.target_account_id { - self.create_transfer_target( - &mut tx, - &transaction_id, - &data_snapshot, - target_account_id, - ) - .await?; + self.create_transfer_target(&mut tx, &transaction_id, &data_snapshot, target_account_id).await?; } } // 提交事务 - tx.commit() - .await + tx.commit().await .map_err(|e| ApiError::DatabaseError(e.to_string()))?; Ok(transaction) @@ -127,12 +120,13 @@ impl TransactionService { target_account_id: Uuid, ) -> ApiResult<()> { // 获取目标账户余额 - let target_balance: Option<(f64,)> = - sqlx::query_as("SELECT current_balance FROM accounts WHERE id = $1 FOR UPDATE") - .bind(target_account_id) - .fetch_optional(&mut **tx) - .await - .map_err(|e| ApiError::DatabaseError(e.to_string()))?; + let target_balance: Option<(f64,)> = sqlx::query_as( + "SELECT current_balance FROM accounts WHERE id = $1 FOR UPDATE" + ) + .bind(target_account_id) + .fetch_optional(&mut **tx) + .await + .map_err(|e| ApiError::DatabaseError(e.to_string()))?; let target_balance = target_balance .ok_or_else(|| ApiError::NotFound("Target account not found".to_string()))? @@ -150,17 +144,14 @@ impl TransactionService { ) VALUES ( $1, $2, $3, $4, $5, 'income', '转账收入', '内部转账', $6, $7, $8, NOW(), NOW() ) - "#, + "# ) .bind(Uuid::new_v4()) .bind(data.ledger_id) .bind(target_account_id) .bind(data.transaction_date) .bind(data.amount) - .bind(format!( - "从账户转入: {}", - data.notes.as_deref().unwrap_or("") - )) + .bind(format!("从账户转入: {}", data.notes.as_deref().unwrap_or(""))) .bind(data.status.clone()) .bind(source_transaction_id) .execute(&mut **tx) @@ -168,25 +159,21 @@ impl TransactionService { .map_err(|e| ApiError::DatabaseError(e.to_string()))?; // 更新目标账户余额 - sqlx::query("UPDATE accounts SET current_balance = $1, updated_at = NOW() WHERE id = $2") - .bind(new_target_balance) - .bind(target_account_id) - .execute(&mut **tx) - .await - .map_err(|e| ApiError::DatabaseError(e.to_string()))?; + sqlx::query( + "UPDATE accounts SET current_balance = $1, updated_at = NOW() WHERE id = $2" + ) + .bind(new_target_balance) + .bind(target_account_id) + .execute(&mut **tx) + .await + .map_err(|e| ApiError::DatabaseError(e.to_string()))?; Ok(()) } /// 批量导入交易 - pub async fn bulk_import( - &self, - transactions: Vec, - ) -> ApiResult> { - let mut tx = self - .pool - .begin() - .await + pub async fn bulk_import(&self, transactions: Vec) -> ApiResult> { + let mut tx = self.pool.begin().await .map_err(|e| ApiError::DatabaseError(e.to_string()))?; let mut created_transactions = Vec::new(); @@ -194,15 +181,14 @@ impl TransactionService { // 预加载所有相关账户的余额 for trans in &transactions { - if let std::collections::hash_map::Entry::Vacant(e) = - account_balances.entry(trans.account_id) - { - let balance: Option<(f64,)> = - sqlx::query_as("SELECT current_balance FROM accounts WHERE id = $1") - .bind(trans.account_id) - .fetch_optional(&mut *tx) - .await - .map_err(|e| ApiError::DatabaseError(e.to_string()))?; + if let std::collections::hash_map::Entry::Vacant(e) = account_balances.entry(trans.account_id) { + let balance: Option<(f64,)> = sqlx::query_as( + "SELECT current_balance FROM accounts WHERE id = $1" + ) + .bind(trans.account_id) + .fetch_optional(&mut *tx) + .await + .map_err(|e| ApiError::DatabaseError(e.to_string()))?; if let Some(balance) = balance { e.insert(balance.0); @@ -216,8 +202,7 @@ impl TransactionService { // 处理每笔交易 for trans_data in sorted_transactions { - let account_balance = account_balances - .get_mut(&trans_data.account_id) + let account_balance = account_balances.get_mut(&trans_data.account_id) .ok_or_else(|| ApiError::NotFound("Account not found".to_string()))?; // 更新账户余额 @@ -238,7 +223,7 @@ impl TransactionService { $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, NOW(), NOW() ) RETURNING * - "#, + "# ) .bind(Uuid::new_v4()) .bind(trans_data.ledger_id) @@ -261,7 +246,7 @@ impl TransactionService { // 批量更新账户余额 for (account_id, new_balance) in account_balances { sqlx::query( - "UPDATE accounts SET current_balance = $1, updated_at = NOW() WHERE id = $2", + "UPDATE accounts SET current_balance = $1, updated_at = NOW() WHERE id = $2" ) .bind(new_balance) .bind(account_id) @@ -270,8 +255,7 @@ impl TransactionService { .map_err(|e| ApiError::DatabaseError(e.to_string()))?; } - tx.commit() - .await + tx.commit().await .map_err(|e| ApiError::DatabaseError(e.to_string()))?; Ok(created_transactions) @@ -280,15 +264,16 @@ impl TransactionService { /// 智能分类交易 pub async fn auto_categorize(&self, transaction_id: Uuid) -> ApiResult> { // 获取交易信息 - let transaction: Option<(String, Option, f64)> = - sqlx::query_as("SELECT payee, notes, amount FROM transactions WHERE id = $1") - .bind(transaction_id) - .fetch_optional(&self.pool) - .await - .map_err(|e| ApiError::DatabaseError(e.to_string()))?; + let transaction: Option<(String, Option, f64)> = sqlx::query_as( + "SELECT payee, notes, amount FROM transactions WHERE id = $1" + ) + .bind(transaction_id) + .fetch_optional(&self.pool) + .await + .map_err(|e| ApiError::DatabaseError(e.to_string()))?; - let (payee, notes, amount) = - transaction.ok_or_else(|| ApiError::NotFound("Transaction not found".to_string()))?; + let (payee, notes, amount) = transaction + .ok_or_else(|| ApiError::NotFound("Transaction not found".to_string()))?; // 查找匹配的规则 let rule: Option<(Uuid, Uuid)> = sqlx::query_as( @@ -304,7 +289,7 @@ impl TransactionService { ) ORDER BY priority DESC LIMIT 1 - "#, + "# ) .bind(payee) .bind(notes.unwrap_or_else(String::new)) @@ -316,7 +301,7 @@ impl TransactionService { if let Some((rule_id, category_id)) = rule { // 更新交易分类 sqlx::query( - "UPDATE transactions SET category_id = $1, updated_at = NOW() WHERE id = $2", + "UPDATE transactions SET category_id = $1, updated_at = NOW() WHERE id = $2" ) .bind(category_id) .bind(transaction_id) @@ -329,7 +314,7 @@ impl TransactionService { r#" INSERT INTO rule_matches (id, rule_id, transaction_id, matched_at) VALUES ($1, $2, $3, NOW()) - "#, + "# ) .bind(Uuid::new_v4()) .bind(rule_id) diff --git a/jive-api/src/ws.rs b/jive-api/src/ws.rs index 089f3d3e..32cf1b7e 100644 --- a/jive-api/src/ws.rs +++ b/jive-api/src/ws.rs @@ -13,7 +13,7 @@ use sqlx::PgPool; use std::collections::HashMap; use std::sync::Arc; use tokio::sync::RwLock; -use tracing::{error, info}; +use tracing::{info, error}; /// WebSocket连接管理器 pub struct WsConnectionManager { @@ -26,15 +26,15 @@ impl WsConnectionManager { connections: Arc::new(RwLock::new(HashMap::new())), } } - + pub async fn add_connection(&self, id: String, tx: tokio::sync::mpsc::UnboundedSender) { self.connections.write().await.insert(id, tx); } - + pub async fn remove_connection(&self, id: &str) { self.connections.write().await.remove(id); } - + pub async fn send_message(&self, id: &str, message: String) -> Result<(), String> { if let Some(tx) = self.connections.read().await.get(id) { tx.send(message).map_err(|e| e.to_string()) @@ -45,9 +45,7 @@ impl WsConnectionManager { } impl Default for WsConnectionManager { - fn default() -> Self { - Self::new() - } + fn default() -> Self { Self::new() } } /// WebSocket查询参数 @@ -75,14 +73,11 @@ pub async fn ws_handler( // 简单的令牌验证(实际应验证JWT) if query.token.is_empty() { return ws.on_upgrade(|mut socket| async move { - let _ = socket - .send(Message::Text( - serde_json::to_string(&WsMessage::Error { - message: "Invalid token".to_string(), - }) - .unwrap(), - )) - .await; + let _ = socket.send(Message::Text( + serde_json::to_string(&WsMessage::Error { + message: "Invalid token".to_string(), + }).unwrap() + )).await; let _ = socket.close().await; }); } @@ -93,18 +88,18 @@ pub async fn ws_handler( /// 处理WebSocket连接 pub async fn handle_socket(socket: WebSocket, token: String, _pool: PgPool) { let (mut sender, mut receiver) = socket.split(); - + // 发送连接成功消息 let connected_msg = WsMessage::Connected { user_id: "test-user".to_string(), }; - + if let Ok(msg_str) = serde_json::to_string(&connected_msg) { let _ = sender.send(Message::Text(msg_str)).await; } - + info!("WebSocket connected with token: {}", token); - + // 处理消息循环 while let Some(msg) = receiver.next().await { match msg { diff --git a/jive-api/target/.rustc_info.json b/jive-api/target/.rustc_info.json index 2e254c01..3ec253f1 100644 --- a/jive-api/target/.rustc_info.json +++ b/jive-api/target/.rustc_info.json @@ -1 +1 @@ -{"rustc_fingerprint":8876508001675379479,"outputs":{"17747080675513052775":{"success":true,"status":"","code":0,"stdout":"rustc 1.89.0 (29483883e 2025-08-04)\nbinary: rustc\ncommit-hash: 29483883eed69d5fb4db01964cdf2af4d86e9cb2\ncommit-date: 2025-08-04\nhost: aarch64-apple-darwin\nrelease: 1.89.0\nLLVM version: 20.1.7\n","stderr":""},"7971740275564407648":{"success":true,"status":"","code":0,"stdout":"___\nlib___.rlib\nlib___.dylib\nlib___.dylib\nlib___.a\nlib___.dylib\n/Users/huazhou/.rustup/toolchains/stable-aarch64-apple-darwin\noff\npacked\nunpacked\n___\ndebug_assertions\npanic=\"unwind\"\nproc_macro\ntarget_abi=\"\"\ntarget_arch=\"aarch64\"\ntarget_endian=\"little\"\ntarget_env=\"\"\ntarget_family=\"unix\"\ntarget_feature=\"aes\"\ntarget_feature=\"crc\"\ntarget_feature=\"dit\"\ntarget_feature=\"dotprod\"\ntarget_feature=\"dpb\"\ntarget_feature=\"dpb2\"\ntarget_feature=\"fcma\"\ntarget_feature=\"fhm\"\ntarget_feature=\"flagm\"\ntarget_feature=\"fp16\"\ntarget_feature=\"frintts\"\ntarget_feature=\"jsconv\"\ntarget_feature=\"lor\"\ntarget_feature=\"lse\"\ntarget_feature=\"neon\"\ntarget_feature=\"paca\"\ntarget_feature=\"pacg\"\ntarget_feature=\"pan\"\ntarget_feature=\"pmuv3\"\ntarget_feature=\"ras\"\ntarget_feature=\"rcpc\"\ntarget_feature=\"rcpc2\"\ntarget_feature=\"rdm\"\ntarget_feature=\"sb\"\ntarget_feature=\"sha2\"\ntarget_feature=\"sha3\"\ntarget_feature=\"ssbs\"\ntarget_feature=\"vh\"\ntarget_has_atomic=\"128\"\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_os=\"macos\"\ntarget_pointer_width=\"64\"\ntarget_vendor=\"apple\"\nunix\n","stderr":""}},"successes":{}} \ No newline at end of file +{"rustc_fingerprint":1863893085117187729,"outputs":{"13007759520587589747":{"success":true,"status":"","code":0,"stdout":"___\nlib___.rlib\nlib___.dylib\nlib___.dylib\nlib___.a\nlib___.dylib\n/Users/huazhou/.rustup/toolchains/stable-aarch64-apple-darwin\noff\npacked\nunpacked\n___\ndebug_assertions\npanic=\"unwind\"\nproc_macro\ntarget_abi=\"\"\ntarget_arch=\"aarch64\"\ntarget_endian=\"little\"\ntarget_env=\"\"\ntarget_family=\"unix\"\ntarget_feature=\"aes\"\ntarget_feature=\"crc\"\ntarget_feature=\"dit\"\ntarget_feature=\"dotprod\"\ntarget_feature=\"dpb\"\ntarget_feature=\"dpb2\"\ntarget_feature=\"fcma\"\ntarget_feature=\"fhm\"\ntarget_feature=\"flagm\"\ntarget_feature=\"fp16\"\ntarget_feature=\"frintts\"\ntarget_feature=\"jsconv\"\ntarget_feature=\"lor\"\ntarget_feature=\"lse\"\ntarget_feature=\"neon\"\ntarget_feature=\"paca\"\ntarget_feature=\"pacg\"\ntarget_feature=\"pan\"\ntarget_feature=\"pmuv3\"\ntarget_feature=\"ras\"\ntarget_feature=\"rcpc\"\ntarget_feature=\"rcpc2\"\ntarget_feature=\"rdm\"\ntarget_feature=\"sb\"\ntarget_feature=\"sha2\"\ntarget_feature=\"sha3\"\ntarget_feature=\"ssbs\"\ntarget_feature=\"vh\"\ntarget_has_atomic=\"128\"\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_os=\"macos\"\ntarget_pointer_width=\"64\"\ntarget_vendor=\"apple\"\nunix\n","stderr":""},"18122065246313386177":{"success":true,"status":"","code":0,"stdout":"rustc 1.89.0 (29483883e 2025-08-04)\nbinary: rustc\ncommit-hash: 29483883eed69d5fb4db01964cdf2af4d86e9cb2\ncommit-date: 2025-08-04\nhost: aarch64-apple-darwin\nrelease: 1.89.0\nLLVM version: 20.1.7\n","stderr":""}},"successes":{}} \ No newline at end of file diff --git a/jive-api/target/release/jive-api b/jive-api/target/release/jive-api index 6c82f893..5473600b 100755 Binary files a/jive-api/target/release/jive-api and b/jive-api/target/release/jive-api differ diff --git a/jive-api/tests/integration/transactions_export_test.rs b/jive-api/tests/integration/transactions_export_test.rs index 7c71d3c3..caba8e4b 100644 --- a/jive-api/tests/integration/transactions_export_test.rs +++ b/jive-api/tests/integration/transactions_export_test.rs @@ -172,26 +172,6 @@ mod tests { let audit_str = audit_hdr.to_str().unwrap(); assert!(Uuid::parse_str(audit_str).is_ok(), "invalid X-Audit-Id: {}", audit_str); - // POST CSV with include_header=false should not contain header line - let req = Request::builder() - .method("POST") - .uri("/api/v1/transactions/export") - .header(header::AUTHORIZATION, token.clone()) - .header(header::CONTENT_TYPE, "application/json") - .body(Body::from(json!({"format":"csv","include_header":false}).to_string())) - .unwrap(); - let resp = app.clone().oneshot(req).await.unwrap(); - assert_eq!(resp.status(), http::StatusCode::OK); - let body = hyper::body::to_bytes(resp.into_body()).await.unwrap(); - let v: serde_json::Value = serde_json::from_slice(&body).unwrap(); - assert_eq!(v["success"], true); - let url = v["download_url"].as_str().unwrap_or(""); - let b64_idx = url.rfind("base64,").unwrap() + "base64,".len(); - let decoded = base64::engine::general_purpose::STANDARD.decode(&url[b64_idx..]).unwrap(); - let csv_text = String::from_utf8(decoded).unwrap(); - let first_line = csv_text.lines().next().unwrap_or(""); - assert!(!first_line.starts_with("Date,"), "POST CSV header should be suppressed when include_header=false"); - // GET CSV streaming (also validate filename header) let req = Request::builder() .method("GET") @@ -211,20 +191,6 @@ mod tests { let audit_str = audit.to_str().unwrap(); assert!(Uuid::parse_str(audit_str).is_ok(), "invalid X-Audit-Id: {}", audit_str); - // GET CSV with include_header=false should not contain header line - let req = Request::builder() - .method("GET") - .uri("/api/v1/transactions/export.csv?include_header=false") - .header(header::AUTHORIZATION, token.clone()) - .body(Body::empty()) - .unwrap(); - let resp = app.clone().oneshot(req).await.unwrap(); - assert_eq!(resp.status(), http::StatusCode::OK); - let body = hyper::body::to_bytes(resp.into_body()).await.unwrap(); - let csv_text = String::from_utf8(body.to_vec()).unwrap(); - let first_line = csv_text.lines().next().unwrap_or(""); - assert!(!first_line.starts_with("Date,"), "header should be suppressed when include_header=false"); - // Filter: by ledger_id should include only rows for that ledger let req = Request::builder() .method("GET") diff --git a/jive-core/src/application/export_service.rs b/jive-core/src/application/export_service.rs index 2e971c57..c9f05511 100644 --- a/jive-core/src/application/export_service.rs +++ b/jive-core/src/application/export_service.rs @@ -1,45 +1,45 @@ //! Export service - 数据导出服务 -//! +//! //! 基于 Maybe 的导出功能转换而来,支持多种导出格式和灵活的数据选择 -use chrono::{DateTime, NaiveDate, Utc}; -use rust_decimal::Decimal; -use serde::{Deserialize, Serialize}; use std::collections::HashMap; +use serde::{Serialize, Deserialize}; +use chrono::{DateTime, Utc, NaiveDate}; +use rust_decimal::Decimal; use uuid::Uuid; #[cfg(feature = "wasm")] use wasm_bindgen::prelude::*; -use super::{PaginationParams, ServiceContext, ServiceResponse}; -use crate::domain::{Account, Category, Ledger, Transaction}; use crate::error::{JiveError, Result}; +use crate::domain::{Account, Transaction, Category, Ledger}; +use super::{ServiceContext, ServiceResponse, PaginationParams}; /// 导出格式 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[cfg_attr(feature = "wasm", wasm_bindgen)] pub enum ExportFormat { - CSV, // CSV 格式 - Excel, // Excel 格式 - JSON, // JSON 格式 - XML, // XML 格式 - PDF, // PDF 格式 - QIF, // Quicken Interchange Format - OFX, // Open Financial Exchange - Markdown, // Markdown 格式 - HTML, // HTML 格式 + CSV, // CSV 格式 + Excel, // Excel 格式 + JSON, // JSON 格式 + XML, // XML 格式 + PDF, // PDF 格式 + QIF, // Quicken Interchange Format + OFX, // Open Financial Exchange + Markdown, // Markdown 格式 + HTML, // HTML 格式 } /// 导出范围 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[cfg_attr(feature = "wasm", wasm_bindgen)] pub enum ExportScope { - All, // 所有数据 - Ledger, // 特定账本 - Account, // 特定账户 - Category, // 特定分类 - DateRange, // 日期范围 - Custom, // 自定义 + All, // 所有数据 + Ledger, // 特定账本 + Account, // 特定账户 + Category, // 特定分类 + DateRange, // 日期范围 + Custom, // 自定义 } /// 导出选项 @@ -109,12 +109,12 @@ pub struct ExportTask { #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[cfg_attr(feature = "wasm", wasm_bindgen)] pub enum ExportStatus { - Pending, // 待处理 - Processing, // 处理中 - Generating, // 生成中 - Completed, // 完成 - Failed, // 失败 - Cancelled, // 取消 + Pending, // 待处理 + Processing, // 处理中 + Generating, // 生成中 + Completed, // 完成 + Failed, // 失败 + Cancelled, // 取消 } /// 导出模板 @@ -208,14 +208,6 @@ impl Default for CsvExportConfig { } } -impl CsvExportConfig { - // Allow external crates (API) to toggle header inclusion without exposing fields. - pub fn with_include_header(mut self, include_header: bool) -> Self { - self.include_header = include_header; - self - } -} - /// 轻量导出行(供服务端快速复用,不依赖内部数据收集) #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SimpleTransactionExport { @@ -345,30 +337,19 @@ impl ExportService { if cfg.include_header { out.push_str(&format!( "Date{}Description{}Amount{}Category{}Account{}Payee{}Type\n", - cfg.delimiter, - cfg.delimiter, - cfg.delimiter, - cfg.delimiter, - cfg.delimiter, - cfg.delimiter + cfg.delimiter, cfg.delimiter, cfg.delimiter, cfg.delimiter, cfg.delimiter, cfg.delimiter )); } for r in rows { let amount_str = r.amount.to_string().replace('.', &cfg.decimal_separator); out.push_str(&format!( "{}{}{}{}{}{}{}{}{}{}{}{}{}\n", - r.date.format(&cfg.date_format), - cfg.delimiter, - escape_csv_field(&sanitize_csv_cell(&r.description), cfg.delimiter), - cfg.delimiter, - amount_str, - cfg.delimiter, - escape_csv_field(r.category.as_deref().unwrap_or(""), cfg.delimiter), - cfg.delimiter, - escape_csv_field(&r.account, cfg.delimiter), - cfg.delimiter, - escape_csv_field(r.payee.as_deref().unwrap_or(""), cfg.delimiter), - cfg.delimiter, + r.date.format(&cfg.date_format), cfg.delimiter, + escape_csv_field(&sanitize_csv_cell(&r.description), cfg.delimiter), cfg.delimiter, + amount_str, cfg.delimiter, + escape_csv_field(r.category.as_deref().unwrap_or(""), cfg.delimiter), cfg.delimiter, + escape_csv_field(&r.account, cfg.delimiter), cfg.delimiter, + escape_csv_field(r.payee.as_deref().unwrap_or(""), cfg.delimiter), cfg.delimiter, escape_csv_field(&r.transaction_type, cfg.delimiter), )); } @@ -576,21 +557,19 @@ impl ExportService { context: ServiceContext, ) -> Result { // 获取任务 - let mut task = self - ._get_export_status(task_id.clone(), context.clone()) - .await?; - + let mut task = self._get_export_status(task_id.clone(), context.clone()).await?; + // 更新状态为处理中 task.status = ExportStatus::Processing; - + // 收集数据 let export_data = self.collect_export_data(&task.options, &context).await?; - + // 计算总项数 - task.total_items = export_data.transactions.len() as u32 - + export_data.accounts.len() as u32 + task.total_items = export_data.transactions.len() as u32 + + export_data.accounts.len() as u32 + export_data.categories.len() as u32; - + // 根据格式导出 let file_data = match task.options.format { ExportFormat::CSV => self.generate_csv(&export_data, &task.options)?, @@ -602,18 +581,17 @@ impl ExportService { }); } }; - + // 保存文件 - let file_name = format!( - "export_{}_{}.{}", - context.user_id, + let file_name = format!("export_{}_{}.{}", + context.user_id, Utc::now().timestamp(), self.get_file_extension(&task.options.format) ); - + // 在实际实现中,这里会保存文件到存储服务 let download_url = format!("/downloads/{}", file_name); - + // 更新任务状态 task.status = ExportStatus::Completed; task.exported_items = task.total_items; @@ -622,7 +600,7 @@ impl ExportService { task.download_url = Some(download_url.clone()); task.completed_at = Some(Utc::now()); task.progress = 100; - + // 创建导出结果 let metadata = ExportMetadata { version: "1.0.0".to_string(), @@ -636,7 +614,7 @@ impl ExportService { tag_count: export_data.tags.len() as u32, date_range: None, }; - + Ok(ExportResult { task_id: task.id, status: task.status, @@ -679,7 +657,11 @@ impl ExportService { } /// 取消导出的内部实现 - async fn _cancel_export(&self, _task_id: String, _context: ServiceContext) -> Result { + async fn _cancel_export( + &self, + _task_id: String, + _context: ServiceContext, + ) -> Result { // 在实际实现中,取消正在进行的导出任务 Ok(true) } @@ -691,26 +673,26 @@ impl ExportService { context: ServiceContext, ) -> Result> { // 在实际实现中,从数据库获取导出历史 - let history = vec![ExportTask { - id: Uuid::new_v4().to_string(), - user_id: context.user_id.clone(), - name: "Year 2024 Export".to_string(), - description: Some("Complete export for year 2024".to_string()), - options: ExportOptions::default(), - status: ExportStatus::Completed, - progress: 100, - total_items: 5000, - exported_items: 5000, - file_size: 2048000, - // 统一改为 JSON 示例文件名 - file_path: Some("export_2024_full.json".to_string()), - download_url: Some("/downloads/export_2024_full.json".to_string()), - error_message: None, - started_at: Utc::now() - chrono::Duration::days(1), - completed_at: Some( - Utc::now() - chrono::Duration::days(1) + chrono::Duration::minutes(10), - ), - }]; + let history = vec![ + ExportTask { + id: Uuid::new_v4().to_string(), + user_id: context.user_id.clone(), + name: "Year 2024 Export".to_string(), + description: Some("Complete export for year 2024".to_string()), + options: ExportOptions::default(), + status: ExportStatus::Completed, + progress: 100, + total_items: 5000, + exported_items: 5000, + file_size: 2048000, + // 统一改为 JSON 示例文件名 + file_path: Some("export_2024_full.json".to_string()), + download_url: Some("/downloads/export_2024_full.json".to_string()), + error_message: None, + started_at: Utc::now() - chrono::Duration::days(1), + completed_at: Some(Utc::now() - chrono::Duration::days(1) + chrono::Duration::minutes(10)), + }, + ]; Ok(history.into_iter().take(limit as usize).collect()) } @@ -740,7 +722,10 @@ impl ExportService { } /// 获取导出模板的内部实现 - async fn _get_export_templates(&self, _context: ServiceContext) -> Result> { + async fn _get_export_templates( + &self, + _context: ServiceContext, + ) -> Result> { // 在实际实现中,从数据库获取模板 Ok(Vec::new()) } @@ -774,11 +759,10 @@ impl ExportService { context: ServiceContext, ) -> Result { let export_data = self.collect_export_data(&options, &context).await?; - let json = serde_json::to_string_pretty(&export_data).map_err(|e| { - JiveError::SerializationError { + let json = serde_json::to_string_pretty(&export_data) + .map_err(|e| JiveError::SerializationError { message: e.to_string(), - } - })?; + })?; Ok(json) } @@ -856,10 +840,10 @@ impl ExportService { /// 生成 CSV 数据 fn generate_csv(&self, data: &ExportData, _options: &ExportOptions) -> Result> { let mut csv = String::new(); - + // 添加标题行 csv.push_str("Date,Description,Amount,Category,Account\n"); - + // 添加交易数据 for transaction in &data.transactions { csv.push_str(&format!( @@ -871,18 +855,14 @@ impl ExportService { transaction.account_id )); } - + Ok(csv.into_bytes()) } /// 生成带配置的 CSV 数据 - fn generate_csv_with_config( - &self, - data: &ExportData, - config: &CsvExportConfig, - ) -> Result> { + fn generate_csv_with_config(&self, data: &ExportData, config: &CsvExportConfig) -> Result> { let mut csv = String::new(); - + // 添加标题行 if config.include_header { csv.push_str(&format!( @@ -890,14 +870,12 @@ impl ExportService { config.delimiter, config.delimiter, config.delimiter, config.delimiter )); } - + // 添加交易数据 for transaction in &data.transactions { - let amount_str = transaction - .amount - .to_string() + let amount_str = transaction.amount.to_string() .replace('.', &config.decimal_separator); - + csv.push_str(&format!( "{}{}{}{}{}{}{}{}{}\n", transaction.date.format(&config.date_format), @@ -911,15 +889,16 @@ impl ExportService { transaction.account_id )); } - + Ok(csv.into_bytes()) } /// 生成 JSON 数据 fn generate_json(&self, data: &ExportData) -> Result> { - let json = serde_json::to_vec_pretty(data).map_err(|e| JiveError::SerializationError { - message: e.to_string(), - })?; + let json = serde_json::to_vec_pretty(data) + .map_err(|e| JiveError::SerializationError { + message: e.to_string(), + })?; Ok(json) } @@ -981,9 +960,11 @@ mod tests { let context = ServiceContext::new("user-123".to_string()); let options = ExportOptions::default(); - let result = service - ._create_export_task("Test Export".to_string(), options, context) - .await; + let result = service._create_export_task( + "Test Export".to_string(), + options, + context + ).await; assert!(result.is_ok()); let task = result.unwrap(); diff --git a/jive-core/src/domain/category.rs b/jive-core/src/domain/category.rs index b68d5c23..bfb6b59f 100644 --- a/jive-core/src/domain/category.rs +++ b/jive-core/src/domain/category.rs @@ -1,13 +1,13 @@ //! Category domain model use chrono::{DateTime, Utc}; -use serde::{Deserialize, Serialize}; +use serde::{Serialize, Deserialize}; #[cfg(feature = "wasm")] use wasm_bindgen::prelude::*; -use super::{AccountClassification, Entity, SoftDeletable}; use crate::error::{JiveError, Result}; +use super::{Entity, SoftDeletable, AccountClassification}; /// 分类实体 #[derive(Debug, Clone, Serialize, Deserialize)] @@ -23,7 +23,7 @@ pub struct Category { icon: Option, is_active: bool, is_system: bool, // 系统预置分类 - position: u32, // 排序位置 + position: u32, // 排序位置 // 统计信息 transaction_count: u32, // 审计字段 @@ -365,8 +365,7 @@ impl Category { color.to_string(), icon.map(|s| s.to_string()), *position, - ) - .unwrap() + ).unwrap() }) .collect() } @@ -395,8 +394,7 @@ impl Category { color.to_string(), icon.map(|s| s.to_string()), *position, - ) - .unwrap() + ).unwrap() }) .collect() } @@ -419,18 +417,10 @@ impl Entity for Category { } impl SoftDeletable for Category { - fn is_deleted(&self) -> bool { - self.deleted_at.is_some() - } - fn deleted_at(&self) -> Option> { - self.deleted_at - } - fn soft_delete(&mut self) { - self.deleted_at = Some(Utc::now()); - } - fn restore(&mut self) { - self.deleted_at = None; - } + fn is_deleted(&self) -> bool { self.deleted_at.is_some() } + fn deleted_at(&self) -> Option> { self.deleted_at } + fn soft_delete(&mut self) { self.deleted_at = Some(Utc::now()); } + fn restore(&mut self) { self.deleted_at = None; } } /// 分类构建器 @@ -515,16 +505,14 @@ impl CategoryBuilder { message: "Category name is required".to_string(), })?; - let classification = self - .classification - .ok_or_else(|| JiveError::ValidationError { - message: "Classification is required".to_string(), - })?; + let classification = self.classification.ok_or_else(|| JiveError::ValidationError { + message: "Classification is required".to_string(), + })?; let color = self.color.unwrap_or_else(|| "#6B7280".to_string()); let mut category = Category::new(ledger_id, name, classification, color)?; - + category.parent_id = self.parent_id; if let Some(description) = self.description { category.set_description(Some(description))?; @@ -550,14 +538,10 @@ mod tests { "Dining".to_string(), AccountClassification::Expense, "#EF4444".to_string(), - ) - .unwrap(); + ).unwrap(); assert_eq!(category.name(), "Dining"); - assert!(matches!( - category.classification(), - AccountClassification::Expense - )); + assert!(matches!(category.classification(), AccountClassification::Expense)); assert_eq!(category.color(), "#EF4444"); assert!(!category.is_system()); assert!(category.is_active()); @@ -571,16 +555,14 @@ mod tests { "Transportation".to_string(), AccountClassification::Expense, "#F97316".to_string(), - ) - .unwrap(); + ).unwrap(); let mut child = Category::new( "ledger-123".to_string(), "Gas".to_string(), AccountClassification::Expense, "#FB923C".to_string(), - ) - .unwrap(); + ).unwrap(); child.set_parent_id(Some(parent.id())); @@ -604,17 +586,14 @@ mod tests { assert_eq!(category.name(), "Shopping"); assert_eq!(category.icon(), Some("🛍️".to_string())); - assert_eq!( - category.description(), - Some("Shopping expenses".to_string()) - ); + assert_eq!(category.description(), Some("Shopping expenses".to_string())); assert_eq!(category.position(), 3); } #[test] fn test_system_categories() { let ledger_id = "ledger-123".to_string(); - + let income_categories = Category::default_income_categories(ledger_id.clone()); let expense_categories = Category::default_expense_categories(ledger_id); @@ -639,8 +618,7 @@ mod tests { "Test Category".to_string(), AccountClassification::Expense, "#6B7280".to_string(), - ) - .unwrap(); + ).unwrap(); assert_eq!(category.transaction_count(), 0); assert!(category.can_be_deleted()); @@ -662,8 +640,7 @@ mod tests { "".to_string(), AccountClassification::Expense, "#EF4444".to_string(), - ) - .is_err()); + ).is_err()); // 测试无效颜色 assert!(Category::new( @@ -671,7 +648,6 @@ mod tests { "Valid Name".to_string(), AccountClassification::Expense, "invalid-color".to_string(), - ) - .is_err()); + ).is_err()); } } diff --git a/jive-core/src/domain/family.rs b/jive-core/src/domain/family.rs index 715773d3..fba26772 100644 --- a/jive-core/src/domain/family.rs +++ b/jive-core/src/domain/family.rs @@ -1,17 +1,17 @@ //! Family domain model - 多用户协作核心模型 -//! +//! //! 基于 Maybe 的 Family 模型设计,支持多用户共享财务数据 use chrono::{DateTime, Utc}; -use rust_decimal::Decimal; -use serde::{Deserialize, Serialize}; +use serde::{Serialize, Deserialize}; use uuid::Uuid; +use rust_decimal::Decimal; #[cfg(feature = "wasm")] use wasm_bindgen::prelude::*; -use super::{Entity, SoftDeletable}; use crate::error::{JiveError, Result}; +use super::{Entity, SoftDeletable}; /// Family - 多用户协作的核心实体 /// 对应 Maybe 的 Family 模型 @@ -37,24 +37,24 @@ pub struct FamilySettings { pub smart_defaults_enabled: bool, pub auto_detect_merchants: bool, pub use_last_selected_category: bool, - + // 审批设置 pub require_approval_for_large_transactions: bool, pub large_transaction_threshold: Option, - + // 共享设置 pub shared_categories: bool, pub shared_tags: bool, pub shared_payees: bool, pub shared_budgets: bool, - + // 通知设置 pub notification_preferences: NotificationPreferences, - + // 货币设置 pub multi_currency_enabled: bool, pub auto_update_exchange_rates: bool, - + // 隐私设置 pub show_member_transactions: bool, pub allow_member_exports: bool, @@ -128,10 +128,10 @@ pub struct FamilyMembership { #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[cfg_attr(feature = "wasm", wasm_bindgen)] pub enum FamilyRole { - Owner, // 创建者,拥有所有权限(类似 Maybe 的第一个用户) - Admin, // 管理员,可以管理成员和设置(对应 Maybe 的 admin role) - Member, // 普通成员,可以查看和编辑数据(对应 Maybe 的 member role) - Viewer, // 只读成员,只能查看数据(扩展功能) + Owner, // 创建者,拥有所有权限(类似 Maybe 的第一个用户) + Admin, // 管理员,可以管理成员和设置(对应 Maybe 的 admin role) + Member, // 普通成员,可以查看和编辑数据(对应 Maybe 的 member role) + Viewer, // 只读成员,只能查看数据(扩展功能) } #[cfg(feature = "wasm")] @@ -167,8 +167,8 @@ pub enum Permission { CreateAccounts, EditAccounts, DeleteAccounts, - ConnectBankAccounts, // 对应 Maybe 的 Plaid 连接 - + ConnectBankAccounts, // 对应 Maybe 的 Plaid 连接 + // 交易权限 ViewTransactions, CreateTransactions, @@ -177,33 +177,33 @@ pub enum Permission { BulkEditTransactions, ImportTransactions, ExportTransactions, - + // 分类权限 ViewCategories, ManageCategories, - + // 商户/收款人权限 ViewPayees, ManagePayees, - + // 标签权限 ViewTags, ManageTags, - + // 预算权限 ViewBudgets, CreateBudgets, EditBudgets, DeleteBudgets, - + // 报表权限 ViewReports, ExportReports, - + // 规则权限 ViewRules, ManageRules, - + // 管理权限 InviteMembers, RemoveMembers, @@ -211,11 +211,11 @@ pub enum Permission { ManageFamilySettings, ManageLedgers, ManageIntegrations, - + // 高级权限 ViewAuditLog, ManageSubscription, - ImpersonateMembers, // 对应 Maybe 的 impersonation + ImpersonateMembers, // 对应 Maybe 的 impersonation } impl FamilyRole { @@ -226,97 +226,47 @@ impl FamilyRole { FamilyRole::Owner => { // Owner 拥有所有权限 vec![ - ViewAccounts, - CreateAccounts, - EditAccounts, - DeleteAccounts, - ConnectBankAccounts, - ViewTransactions, - CreateTransactions, - EditTransactions, - DeleteTransactions, - BulkEditTransactions, - ImportTransactions, - ExportTransactions, - ViewCategories, - ManageCategories, - ViewPayees, - ManagePayees, - ViewTags, - ManageTags, - ViewBudgets, - CreateBudgets, - EditBudgets, - DeleteBudgets, - ViewReports, - ExportReports, - ViewRules, - ManageRules, - InviteMembers, - RemoveMembers, - ManageRoles, - ManageFamilySettings, - ManageLedgers, - ManageIntegrations, - ViewAuditLog, - ManageSubscription, - ImpersonateMembers, + ViewAccounts, CreateAccounts, EditAccounts, DeleteAccounts, ConnectBankAccounts, + ViewTransactions, CreateTransactions, EditTransactions, DeleteTransactions, + BulkEditTransactions, ImportTransactions, ExportTransactions, + ViewCategories, ManageCategories, + ViewPayees, ManagePayees, + ViewTags, ManageTags, + ViewBudgets, CreateBudgets, EditBudgets, DeleteBudgets, + ViewReports, ExportReports, + ViewRules, ManageRules, + InviteMembers, RemoveMembers, ManageRoles, ManageFamilySettings, + ManageLedgers, ManageIntegrations, + ViewAuditLog, ManageSubscription, ImpersonateMembers, ] } FamilyRole::Admin => { // Admin 拥有管理权限,但不能管理订阅和模拟用户 vec![ - ViewAccounts, - CreateAccounts, - EditAccounts, - DeleteAccounts, - ConnectBankAccounts, - ViewTransactions, - CreateTransactions, - EditTransactions, - DeleteTransactions, - BulkEditTransactions, - ImportTransactions, - ExportTransactions, - ViewCategories, - ManageCategories, - ViewPayees, - ManagePayees, - ViewTags, - ManageTags, - ViewBudgets, - CreateBudgets, - EditBudgets, - DeleteBudgets, - ViewReports, - ExportReports, - ViewRules, - ManageRules, - InviteMembers, - RemoveMembers, - ManageFamilySettings, - ManageLedgers, - ManageIntegrations, - ViewAuditLog, + ViewAccounts, CreateAccounts, EditAccounts, DeleteAccounts, ConnectBankAccounts, + ViewTransactions, CreateTransactions, EditTransactions, DeleteTransactions, + BulkEditTransactions, ImportTransactions, ExportTransactions, + ViewCategories, ManageCategories, + ViewPayees, ManagePayees, + ViewTags, ManageTags, + ViewBudgets, CreateBudgets, EditBudgets, DeleteBudgets, + ViewReports, ExportReports, + ViewRules, ManageRules, + InviteMembers, RemoveMembers, ManageFamilySettings, ManageLedgers, + ManageIntegrations, ViewAuditLog, ] } FamilyRole::Member => { // Member 可以查看和编辑数据,但不能管理 vec![ - ViewAccounts, - CreateAccounts, - EditAccounts, - ViewTransactions, - CreateTransactions, - EditTransactions, - ImportTransactions, - ExportTransactions, + ViewAccounts, CreateAccounts, EditAccounts, + ViewTransactions, CreateTransactions, EditTransactions, + ImportTransactions, ExportTransactions, ViewCategories, ViewPayees, ViewTags, ViewBudgets, - ViewReports, - ExportReports, + ViewReports, ExportReports, ViewRules, ] } @@ -348,10 +298,7 @@ impl FamilyRole { /// 检查是否可以导出数据 pub fn can_export(&self) -> bool { - matches!( - self, - FamilyRole::Owner | FamilyRole::Admin | FamilyRole::Member - ) + matches!(self, FamilyRole::Owner | FamilyRole::Admin | FamilyRole::Member) } } @@ -416,11 +363,9 @@ impl FamilyInvitation { /// 接受邀请 pub fn accept(&mut self) -> Result<()> { if !self.is_valid() { - return Err(JiveError::ValidationError { - message: "Invalid or expired invitation".into(), - }); + return Err(JiveError::ValidationError { message: "Invalid or expired invitation".into() }); } - + self.status = InvitationStatus::Accepted; self.accepted_at = Some(Utc::now()); Ok(()) @@ -455,18 +400,18 @@ pub enum AuditAction { MemberJoined, MemberRemoved, MemberRoleChanged, - + // 数据操作 DataCreated, DataUpdated, DataDeleted, DataImported, DataExported, - + // 设置变更 SettingsUpdated, PermissionsChanged, - + // 安全事件 LoginAttempt, LoginSuccess, @@ -474,7 +419,7 @@ pub enum AuditAction { PasswordChanged, MfaEnabled, MfaDisabled, - + // 集成操作 IntegrationConnected, IntegrationDisconnected, @@ -519,30 +464,16 @@ impl Family { impl Entity for Family { type Id = String; - fn id(&self) -> &Self::Id { - &self.id - } - fn created_at(&self) -> DateTime { - self.created_at - } - fn updated_at(&self) -> DateTime { - self.updated_at - } + fn id(&self) -> &Self::Id { &self.id } + fn created_at(&self) -> DateTime { self.created_at } + fn updated_at(&self) -> DateTime { self.updated_at } } impl SoftDeletable for Family { - fn is_deleted(&self) -> bool { - self.deleted_at.is_some() - } - fn deleted_at(&self) -> Option> { - self.deleted_at - } - fn soft_delete(&mut self) { - self.deleted_at = Some(Utc::now()); - } - fn restore(&mut self) { - self.deleted_at = None; - } + fn is_deleted(&self) -> bool { self.deleted_at.is_some() } + fn deleted_at(&self) -> Option> { self.deleted_at } + fn soft_delete(&mut self) { self.deleted_at = Some(Utc::now()); } + fn restore(&mut self) { self.deleted_at = None; } } #[cfg(test)] @@ -603,11 +534,11 @@ mod tests { ); assert!(family.is_feature_enabled("auto_categorize")); - + let mut settings = family.settings.clone(); settings.auto_categorize_enabled = false; family.update_settings(settings); - + assert!(!family.is_feature_enabled("auto_categorize")); } } diff --git a/jive-core/src/domain/ledger.rs b/jive-core/src/domain/ledger.rs index a1256d60..6946fa89 100644 --- a/jive-core/src/domain/ledger.rs +++ b/jive-core/src/domain/ledger.rs @@ -1,14 +1,14 @@ //! Ledger domain model use chrono::{DateTime, Utc}; -use serde::{Deserialize, Serialize}; +use serde::{Serialize, Deserialize}; use uuid::Uuid; #[cfg(feature = "wasm")] use wasm_bindgen::prelude::*; -use super::{Entity, SoftDeletable}; use crate::error::{JiveError, Result}; +use super::{Entity, SoftDeletable}; /// 账本类型枚举 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -156,7 +156,7 @@ pub struct Ledger { name: String, description: Option, ledger_type: LedgerType, - color: String, // 十六进制颜色代码 + color: String, // 十六进制颜色代码 icon: Option, // 图标名称或表情符号 is_default: bool, is_active: bool, @@ -172,7 +172,7 @@ pub struct Ledger { // 权限相关 is_shared: bool, shared_with_users: Vec, // 共享用户ID列表 - permission_level: String, // "read", "write", "admin" + permission_level: String, // "read", "write", "admin" } #[cfg_attr(feature = "wasm", wasm_bindgen)] @@ -457,8 +457,8 @@ impl Ledger { if self.user_id == user_id { return true; } - self.shared_with_users.contains(&user_id) - && (self.permission_level == "write" || self.permission_level == "admin") + self.shared_with_users.contains(&user_id) && + (self.permission_level == "write" || self.permission_level == "admin") } #[cfg_attr(feature = "wasm", wasm_bindgen)] @@ -530,9 +530,7 @@ impl Ledger { } /// 创建账本的 builder 模式 - pub fn builder() -> LedgerBuilder { - LedgerBuilder::new() - } + pub fn builder() -> LedgerBuilder { LedgerBuilder::new() } /// 复制账本(新ID) pub fn duplicate(&self, new_name: String) -> Result { @@ -568,18 +566,10 @@ impl Entity for Ledger { } impl SoftDeletable for Ledger { - fn is_deleted(&self) -> bool { - self.deleted_at.is_some() - } - fn deleted_at(&self) -> Option> { - self.deleted_at - } - fn soft_delete(&mut self) { - self.deleted_at = Some(Utc::now()); - } - fn restore(&mut self) { - self.deleted_at = None; - } + fn is_deleted(&self) -> bool { self.deleted_at.is_some() } + fn deleted_at(&self) -> Option> { self.deleted_at } + fn soft_delete(&mut self) { self.deleted_at = Some(Utc::now()); } + fn restore(&mut self) { self.deleted_at = None; } } /// 账本构建器 @@ -657,12 +647,9 @@ impl LedgerBuilder { message: "Ledger name is required".to_string(), })?; - let ledger_type = self - .ledger_type - .clone() - .ok_or_else(|| JiveError::ValidationError { - message: "Ledger type is required".to_string(), - })?; + let ledger_type = self.ledger_type.clone().ok_or_else(|| JiveError::ValidationError { + message: "Ledger type is required".to_string(), + })?; let color = self.color.clone().unwrap_or_else(|| "#3B82F6".to_string()); @@ -676,7 +663,7 @@ impl LedgerBuilder { ledger.description = self.description.clone(); ledger.icon = self.icon.clone(); ledger.is_default = self.is_default; - + if let Some(description) = self.description.clone() { ledger.set_description(Some(description))?; } @@ -706,8 +693,7 @@ mod tests { "My Personal Ledger".to_string(), LedgerType::Personal, "#3B82F6".to_string(), - ) - .unwrap(); + ).unwrap(); assert_eq!(ledger.name(), "My Personal Ledger"); assert!(matches!(ledger.ledger_type(), LedgerType::Personal)); @@ -739,14 +725,11 @@ mod tests { "Shared Ledger".to_string(), LedgerType::Family, "#FF6B6B".to_string(), - ) - .unwrap(); + ).unwrap(); assert!(!ledger.is_shared()); - - ledger - .share_with_user("user-456".to_string(), "write".to_string()) - .unwrap(); + + ledger.share_with_user("user-456".to_string(), "write".to_string()).unwrap(); assert!(ledger.is_shared()); assert!(ledger.can_user_access("user-456".to_string())); assert!(ledger.can_user_write("user-456".to_string())); @@ -771,10 +754,7 @@ mod tests { assert_eq!(ledger.name(), "Project Alpha"); assert!(matches!(ledger.ledger_type(), LedgerType::Project)); - assert_eq!( - ledger.description(), - Some("Project tracking ledger".to_string()) - ); + assert_eq!(ledger.description(), Some("Project tracking ledger".to_string())); assert_eq!(ledger.icon(), Some("📊".to_string())); assert!(ledger.is_default()); } @@ -786,8 +766,7 @@ mod tests { "Test Ledger".to_string(), LedgerType::Personal, "#3B82F6".to_string(), - ) - .unwrap(); + ).unwrap(); assert_eq!(ledger.transaction_count(), 0); @@ -809,8 +788,7 @@ mod tests { "".to_string(), LedgerType::Personal, "#3B82F6".to_string(), - ) - .is_err()); + ).is_err()); // 测试无效颜色 assert!(Ledger::new( @@ -818,7 +796,6 @@ mod tests { "Valid Name".to_string(), LedgerType::Personal, "invalid-color".to_string(), - ) - .is_err()); + ).is_err()); } } diff --git a/jive-core/src/domain/mod.rs b/jive-core/src/domain/mod.rs index a342ed87..6a453c5f 100644 --- a/jive-core/src/domain/mod.rs +++ b/jive-core/src/domain/mod.rs @@ -1,21 +1,21 @@ //! Domain layer - 领域层 -//! +//! //! 包含所有业务实体和领域模型 pub mod account; -pub mod base; +pub mod transaction; +pub mod ledger; pub mod category; pub mod category_template; -pub mod family; -pub mod ledger; -pub mod transaction; pub mod user; +pub mod family; +pub mod base; pub use account::*; -pub use base::*; +pub use transaction::*; +pub use ledger::*; pub use category::*; pub use category_template::*; -pub use family::*; -pub use ledger::*; -pub use transaction::*; pub use user::*; +pub use family::*; +pub use base::*; diff --git a/jive-core/src/domain/transaction.rs b/jive-core/src/domain/transaction.rs index 6a39c7e9..a89423b5 100644 --- a/jive-core/src/domain/transaction.rs +++ b/jive-core/src/domain/transaction.rs @@ -1,15 +1,15 @@ //! Transaction domain model -use chrono::{DateTime, NaiveDate, Utc}; +use chrono::{DateTime, Utc, NaiveDate}; use rust_decimal::Decimal; -use serde::{Deserialize, Serialize}; +use serde::{Serialize, Deserialize}; use uuid::Uuid; #[cfg(feature = "wasm")] use wasm_bindgen::prelude::*; -use super::{Entity, SoftDeletable, TransactionStatus, TransactionType}; use crate::error::{JiveError, Result}; +use super::{Entity, SoftDeletable, TransactionType, TransactionStatus}; /// 交易实体 #[derive(Debug, Clone, Serialize, Deserialize)] @@ -61,11 +61,11 @@ impl Transaction { ) -> Result { let parsed_date = NaiveDate::parse_from_str(&date, "%Y-%m-%d") .map_err(|_| JiveError::InvalidDate { date })?; - + // 验证金额 crate::utils::Validator::validate_transaction_amount(&amount)?; crate::error::validate_currency(¤cy)?; - + // 验证名称 if name.trim().is_empty() { return Err(JiveError::ValidationError { @@ -295,7 +295,7 @@ impl Transaction { message: "Tag cannot be empty".to_string(), }); } - + if !self.tags.contains(&cleaned_tag) { self.tags.push(cleaned_tag); self.updated_at = Utc::now(); @@ -355,16 +355,11 @@ impl Transaction { } #[wasm_bindgen] - pub fn set_multi_currency( - &mut self, - original_amount: String, - original_currency: String, - exchange_rate: String, - ) -> Result<()> { + pub fn set_multi_currency(&mut self, original_amount: String, original_currency: String, exchange_rate: String) -> Result<()> { crate::error::validate_currency(&original_currency)?; crate::utils::Validator::validate_transaction_amount(&original_amount)?; crate::utils::Validator::validate_transaction_amount(&exchange_rate)?; - + self.original_amount = Some(original_amount); self.original_currency = Some(original_currency); self.exchange_rate = Some(exchange_rate); @@ -472,15 +467,15 @@ impl Transaction { pub fn search_keywords(&self) -> Vec { let mut keywords = Vec::new(); keywords.push(self.name.to_lowercase()); - + if let Some(desc) = &self.description { keywords.push(desc.to_lowercase()); } - + if let Some(notes) = &self.notes { keywords.push(notes.to_lowercase()); } - + keywords.extend(self.tags.iter().map(|tag| tag.to_lowercase())); keywords } @@ -503,18 +498,10 @@ impl Entity for Transaction { } impl SoftDeletable for Transaction { - fn is_deleted(&self) -> bool { - self.deleted_at.is_some() - } - fn deleted_at(&self) -> Option> { - self.deleted_at - } - fn soft_delete(&mut self) { - self.deleted_at = Some(Utc::now()); - } - fn restore(&mut self) { - self.deleted_at = None; - } + fn is_deleted(&self) -> bool { self.deleted_at.is_some() } + fn deleted_at(&self) -> Option> { self.deleted_at } + fn soft_delete(&mut self) { self.deleted_at = Some(Utc::now()); } + fn restore(&mut self) { self.deleted_at = None; } } /// 交易构建器 @@ -662,11 +649,9 @@ impl TransactionBuilder { message: "Date is required".to_string(), })?; - let transaction_type = self - .transaction_type - .ok_or_else(|| JiveError::ValidationError { - message: "Transaction type is required".to_string(), - })?; + let transaction_type = self.transaction_type.ok_or_else(|| JiveError::ValidationError { + message: "Transaction type is required".to_string(), + })?; // 验证输入 crate::utils::Validator::validate_transaction_amount(&amount)?; @@ -725,8 +710,7 @@ mod tests { "USD".to_string(), "2023-12-25".to_string(), TransactionType::Expense, - ) - .unwrap(); + ).unwrap(); assert_eq!(transaction.name(), "Test Transaction"); assert_eq!(transaction.amount(), "100.50"); @@ -745,12 +729,11 @@ mod tests { "USD".to_string(), "2023-12-25".to_string(), TransactionType::Expense, - ) - .unwrap(); + ).unwrap(); transaction.add_tag("food".to_string()).unwrap(); transaction.add_tag("restaurant".to_string()).unwrap(); - + assert!(transaction.has_tag("food".to_string())); assert!(transaction.has_tag("restaurant".to_string())); assert!(!transaction.has_tag("travel".to_string())); @@ -791,15 +774,16 @@ mod tests { "CNY".to_string(), "2023-12-25".to_string(), TransactionType::Expense, - ) - .unwrap(); + ).unwrap(); - transaction - .set_multi_currency("100.00".to_string(), "USD".to_string(), "7.20".to_string()) - .unwrap(); + transaction.set_multi_currency( + "100.00".to_string(), + "USD".to_string(), + "7.20".to_string(), + ).unwrap(); assert!(transaction.is_multi_currency()); - + transaction.clear_multi_currency(); assert!(!transaction.is_multi_currency()); } @@ -814,8 +798,7 @@ mod tests { "USD".to_string(), "2023-12-25".to_string(), TransactionType::Income, - ) - .unwrap(); + ).unwrap(); let expense = Transaction::new( "account-123".to_string(), @@ -825,8 +808,7 @@ mod tests { "USD".to_string(), "2023-12-25".to_string(), TransactionType::Expense, - ) - .unwrap(); + ).unwrap(); assert_eq!(income.signed_amount(), "1000.00"); assert_eq!(expense.signed_amount(), "-500.00"); @@ -842,8 +824,7 @@ mod tests { "USD".to_string(), "2023-12-25".to_string(), TransactionType::Expense, - ) - .unwrap(); + ).unwrap(); assert_eq!(transaction.month_key(), "2023-12"); } diff --git a/jive-core/src/infrastructure/database/connection.rs b/jive-core/src/infrastructure/database/connection.rs index 527f103f..3032d38e 100644 --- a/jive-core/src/infrastructure/database/connection.rs +++ b/jive-core/src/infrastructure/database/connection.rs @@ -3,7 +3,7 @@ use sqlx::{postgres::PgPoolOptions, PgPool}; use std::time::Duration; -use tracing::{error, info}; +use tracing::{info, error}; /// 数据库配置 #[derive(Debug, Clone)] @@ -39,7 +39,7 @@ impl Database { /// 创建新的数据库连接池 pub async fn new(config: DatabaseConfig) -> Result { info!("Initializing database connection pool..."); - + let pool = PgPoolOptions::new() .max_connections(config.max_connections) .min_connections(config.min_connections) @@ -48,7 +48,7 @@ impl Database { .max_lifetime(Some(config.max_lifetime)) .connect(&config.url) .await?; - + info!("Database connection pool initialized successfully"); Ok(Self { pool }) } @@ -60,7 +60,9 @@ impl Database { /// 健康检查 pub async fn health_check(&self) -> Result<(), sqlx::Error> { - sqlx::query("SELECT 1").fetch_one(&self.pool).await?; + sqlx::query("SELECT 1") + .fetch_one(&self.pool) + .await?; Ok(()) } @@ -70,7 +72,9 @@ impl Database { #[cfg(feature = "embed_migrations")] { info!("Running database migrations (embedded)..."); - sqlx::migrate!("../../migrations").run(&self.pool).await?; + sqlx::migrate!("../../migrations") + .run(&self.pool) + .await?; info!("Database migrations completed"); } // 默认情况下不执行嵌入式迁移,以避免构建期需要本地 migrations 目录 @@ -78,9 +82,7 @@ impl Database { } /// 开始事务 - pub async fn begin_transaction( - &self, - ) -> Result, sqlx::Error> { + pub async fn begin_transaction(&self) -> Result, sqlx::Error> { self.pool.begin().await } @@ -109,10 +111,10 @@ impl HealthMonitor { pub async fn start_monitoring(self) { tokio::spawn(async move { let mut interval = tokio::time::interval(self.check_interval); - + loop { interval.tick().await; - + match self.database.health_check().await { Ok(_) => { info!("Database health check passed"); @@ -136,7 +138,7 @@ mod tests { let config = DatabaseConfig::default(); let db = Database::new(config).await; assert!(db.is_ok()); - + if let Ok(database) = db { let health_check = database.health_check().await; assert!(health_check.is_ok()); @@ -147,15 +149,17 @@ mod tests { async fn test_transaction() { let config = DatabaseConfig::default(); let db = Database::new(config).await.unwrap(); - + let tx = db.begin_transaction().await; assert!(tx.is_ok()); - + if let Ok(mut transaction) = tx { // 测试事务操作 - let result = sqlx::query("SELECT 1").fetch_one(&mut *transaction).await; + let result = sqlx::query("SELECT 1") + .fetch_one(&mut *transaction) + .await; assert!(result.is_ok()); - + transaction.rollback().await.unwrap(); } } diff --git a/jive-core/src/infrastructure/entities/mod.rs b/jive-core/src/infrastructure/entities/mod.rs index ad35dc75..6520a8e2 100644 --- a/jive-core/src/infrastructure/entities/mod.rs +++ b/jive-core/src/infrastructure/entities/mod.rs @@ -1,18 +1,18 @@ // Jive Money Entity Mappings // Based on Maybe's database structure +#[cfg(feature = "db")] +pub mod family; +#[cfg(feature = "db")] +pub mod user; #[cfg(feature = "db")] pub mod account; -pub mod balance; -pub mod budget; #[cfg(feature = "db")] -pub mod family; +pub mod transaction; +pub mod budget; +pub mod balance; pub mod import; pub mod rule; -#[cfg(feature = "db")] -pub mod transaction; -#[cfg(feature = "db")] -pub mod user; use chrono::{DateTime, NaiveDate, Utc}; use rust_decimal::Decimal; @@ -23,7 +23,7 @@ use uuid::Uuid; // Common trait for all entities pub trait Entity { type Id; - + fn id(&self) -> Self::Id; fn created_at(&self) -> DateTime; fn updated_at(&self) -> DateTime; @@ -32,7 +32,7 @@ pub trait Entity { // For polymorphic associations (Rails delegated_type pattern) pub trait Accountable: Send + Sync { const TYPE_NAME: &'static str; - + async fn save(&self, tx: &mut sqlx::PgConnection) -> Result; async fn load(id: Uuid, conn: &sqlx::PgPool) -> Result where @@ -42,7 +42,7 @@ pub trait Accountable: Send + Sync { // For transaction entries (Rails single table inheritance pattern) pub trait Entryable: Send + Sync { const TYPE_NAME: &'static str; - + fn to_entry(&self) -> Entry; fn from_entry(entry: Entry) -> Result where @@ -144,19 +144,18 @@ impl DateRange { pub fn new(start: NaiveDate, end: NaiveDate) -> Self { Self { start, end } } - + pub fn current_month() -> Self { let now = chrono::Local::now().naive_local().date(); let start = NaiveDate::from_ymd_opt(now.year(), now.month(), 1).unwrap(); let end = if now.month() == 12 { NaiveDate::from_ymd_opt(now.year() + 1, 1, 1).unwrap() - chrono::Duration::days(1) } else { - NaiveDate::from_ymd_opt(now.year(), now.month() + 1, 1).unwrap() - - chrono::Duration::days(1) + NaiveDate::from_ymd_opt(now.year(), now.month() + 1, 1).unwrap() - chrono::Duration::days(1) }; Self { start, end } } - + pub fn current_year() -> Self { let now = chrono::Local::now().naive_local().date(); let start = NaiveDate::from_ymd_opt(now.year(), 1, 1).unwrap(); diff --git a/jive-core/src/utils.rs b/jive-core/src/utils.rs index 78eaa326..1400f35c 100644 --- a/jive-core/src/utils.rs +++ b/jive-core/src/utils.rs @@ -1,10 +1,10 @@ //! Utility functions for Jive Core -use crate::error::{JiveError, Result}; -use chrono::{DateTime, Datelike, NaiveDate, Utc}; -use rust_decimal::Decimal; -use serde::{Deserialize, Serialize}; +use chrono::{DateTime, Utc, NaiveDate, Datelike}; use uuid::Uuid; +use rust_decimal::Decimal; +use serde::{Serialize, Deserialize}; +use crate::error::{JiveError, Result}; #[cfg(feature = "wasm")] use wasm_bindgen::prelude::*; @@ -58,51 +58,33 @@ fn get_currency_symbol(currency: &str) -> &'static str { /// 计算两个金额的加法 #[cfg_attr(feature = "wasm", wasm_bindgen)] pub fn add_amounts(amount1: &str, amount2: &str) -> Result { - let a1 = amount1 - .parse::() - .map_err(|_| JiveError::InvalidAmount { - amount: amount1.to_string(), - })?; - let a2 = amount2 - .parse::() - .map_err(|_| JiveError::InvalidAmount { - amount: amount2.to_string(), - })?; - + let a1 = amount1.parse::() + .map_err(|_| JiveError::InvalidAmount { amount: amount1.to_string() })?; + let a2 = amount2.parse::() + .map_err(|_| JiveError::InvalidAmount { amount: amount2.to_string() })?; + Ok((a1 + a2).to_string()) } /// 计算两个金额的减法 #[cfg_attr(feature = "wasm", wasm_bindgen)] pub fn subtract_amounts(amount1: &str, amount2: &str) -> Result { - let a1 = amount1 - .parse::() - .map_err(|_| JiveError::InvalidAmount { - amount: amount1.to_string(), - })?; - let a2 = amount2 - .parse::() - .map_err(|_| JiveError::InvalidAmount { - amount: amount2.to_string(), - })?; - + let a1 = amount1.parse::() + .map_err(|_| JiveError::InvalidAmount { amount: amount1.to_string() })?; + let a2 = amount2.parse::() + .map_err(|_| JiveError::InvalidAmount { amount: amount2.to_string() })?; + Ok((a1 - a2).to_string()) } /// 计算两个金额的乘法 #[cfg_attr(feature = "wasm", wasm_bindgen)] pub fn multiply_amounts(amount: &str, multiplier: &str) -> Result { - let a = amount - .parse::() - .map_err(|_| JiveError::InvalidAmount { - amount: amount.to_string(), - })?; - let m = multiplier - .parse::() - .map_err(|_| JiveError::InvalidAmount { - amount: multiplier.to_string(), - })?; - + let a = amount.parse::() + .map_err(|_| JiveError::InvalidAmount { amount: amount.to_string() })?; + let m = multiplier.parse::() + .map_err(|_| JiveError::InvalidAmount { amount: multiplier.to_string() })?; + Ok((a * m).to_string()) } @@ -125,54 +107,38 @@ impl CurrencyConverter { if from_currency == to_currency { return Ok(amount.to_string()); } - - let decimal_amount = amount - .parse::() - .map_err(|_| JiveError::InvalidAmount { - amount: amount.to_string(), - })?; - + + let decimal_amount = amount.parse::() + .map_err(|_| JiveError::InvalidAmount { amount: amount.to_string() })?; + let rate = self.get_exchange_rate(from_currency, to_currency)?; let converted = decimal_amount * rate; - + Ok(converted.to_string()) } #[cfg_attr(feature = "wasm", wasm_bindgen)] pub fn get_supported_currencies(&self) -> Vec { vec![ - "USD".to_string(), - "EUR".to_string(), - "GBP".to_string(), - "JPY".to_string(), - "CNY".to_string(), - "CAD".to_string(), - "AUD".to_string(), - "CHF".to_string(), - "SEK".to_string(), - "NOK".to_string(), - "DKK".to_string(), - "KRW".to_string(), - "SGD".to_string(), - "HKD".to_string(), - "INR".to_string(), - "BRL".to_string(), - "MXN".to_string(), - "RUB".to_string(), - "ZAR".to_string(), - "TRY".to_string(), + "USD".to_string(), "EUR".to_string(), "GBP".to_string(), + "JPY".to_string(), "CNY".to_string(), "CAD".to_string(), + "AUD".to_string(), "CHF".to_string(), "SEK".to_string(), + "NOK".to_string(), "DKK".to_string(), "KRW".to_string(), + "SGD".to_string(), "HKD".to_string(), "INR".to_string(), + "BRL".to_string(), "MXN".to_string(), "RUB".to_string(), + "ZAR".to_string(), "TRY".to_string(), ] } fn get_exchange_rate(&self, from: &str, to: &str) -> Result { // 简化的汇率表,实际应该从外部 API 获取 let rates = [ - ("USD", "CNY", Decimal::new(720, 2)), // 7.20 - ("EUR", "CNY", Decimal::new(780, 2)), // 7.80 - ("GBP", "CNY", Decimal::new(890, 2)), // 8.90 - ("USD", "EUR", Decimal::new(92, 2)), // 0.92 - ("USD", "GBP", Decimal::new(80, 2)), // 0.80 - ("USD", "JPY", Decimal::new(15000, 2)), // 150.00 + ("USD", "CNY", Decimal::new(720, 2)), // 7.20 + ("EUR", "CNY", Decimal::new(780, 2)), // 7.80 + ("GBP", "CNY", Decimal::new(890, 2)), // 8.90 + ("USD", "EUR", Decimal::new(92, 2)), // 0.92 + ("USD", "GBP", Decimal::new(80, 2)), // 0.80 + ("USD", "JPY", Decimal::new(15000, 2)), // 150.00 ("USD", "KRW", Decimal::new(133000, 2)), // 1330.00 ]; @@ -212,33 +178,24 @@ impl DateTimeUtils { /// 解析日期字符串 #[cfg_attr(feature = "wasm", wasm_bindgen)] pub fn parse_date(date_str: &str) -> Result { - let date = NaiveDate::parse_from_str(date_str, "%Y-%m-%d").map_err(|_| { - JiveError::InvalidDate { - date: date_str.to_string(), - } - })?; + let date = NaiveDate::parse_from_str(date_str, "%Y-%m-%d") + .map_err(|_| JiveError::InvalidDate { date: date_str.to_string() })?; Ok(date.to_string()) } /// 格式化日期 #[cfg_attr(feature = "wasm", wasm_bindgen)] pub fn format_date(date_str: &str, format: &str) -> Result { - let date = NaiveDate::parse_from_str(date_str, "%Y-%m-%d").map_err(|_| { - JiveError::InvalidDate { - date: date_str.to_string(), - } - })?; + let date = NaiveDate::parse_from_str(date_str, "%Y-%m-%d") + .map_err(|_| JiveError::InvalidDate { date: date_str.to_string() })?; Ok(date.format(format).to_string()) } /// 获取月初日期 #[cfg_attr(feature = "wasm", wasm_bindgen)] pub fn get_month_start(date_str: &str) -> Result { - let date = NaiveDate::parse_from_str(date_str, "%Y-%m-%d").map_err(|_| { - JiveError::InvalidDate { - date: date_str.to_string(), - } - })?; + let date = NaiveDate::parse_from_str(date_str, "%Y-%m-%d") + .map_err(|_| JiveError::InvalidDate { date: date_str.to_string() })?; let month_start = date.with_day(1).unwrap(); Ok(month_start.to_string()) } @@ -246,18 +203,15 @@ impl DateTimeUtils { /// 获取月末日期 #[cfg_attr(feature = "wasm", wasm_bindgen)] pub fn get_month_end(date_str: &str) -> Result { - let date = NaiveDate::parse_from_str(date_str, "%Y-%m-%d").map_err(|_| { - JiveError::InvalidDate { - date: date_str.to_string(), - } - })?; - + let date = NaiveDate::parse_from_str(date_str, "%Y-%m-%d") + .map_err(|_| JiveError::InvalidDate { date: date_str.to_string() })?; + let next_month = if date.month() == 12 { NaiveDate::from_ymd_opt(date.year() + 1, 1, 1).unwrap() } else { NaiveDate::from_ymd_opt(date.year(), date.month() + 1, 1).unwrap() }; - + let month_end = next_month.pred_opt().unwrap(); Ok(month_end.to_string()) } @@ -290,26 +244,22 @@ impl Validator { /// 验证交易金额 pub fn validate_transaction_amount(amount: &str) -> Result { - let decimal = amount - .parse::() - .map_err(|_| JiveError::InvalidAmount { - amount: amount.to_string(), - })?; - + let decimal = amount.parse::() + .map_err(|_| JiveError::InvalidAmount { amount: amount.to_string() })?; + if decimal.is_zero() { return Err(JiveError::ValidationError { message: "Transaction amount cannot be zero".to_string(), }); } - + // 检查金额是否过大 - if decimal.abs() > Decimal::new(999999999999i64, 2) { - // 9,999,999,999.99 + if decimal.abs() > Decimal::new(999999999999i64, 2) { // 9,999,999,999.99 return Err(JiveError::ValidationError { message: "Transaction amount too large".to_string(), }); } - + Ok(decimal) } @@ -321,19 +271,19 @@ impl Validator { message: "Email cannot be empty".to_string(), }); } - + if !trimmed.contains('@') || !trimmed.contains('.') { return Err(JiveError::ValidationError { message: "Invalid email format".to_string(), }); } - + if trimmed.len() > 254 { return Err(JiveError::ValidationError { message: "Email too long".to_string(), }); } - + Ok(()) } @@ -344,23 +294,23 @@ impl Validator { message: "Password must be at least 8 characters long".to_string(), }); } - + if password.len() > 128 { return Err(JiveError::ValidationError { message: "Password too long (max 128 characters)".to_string(), }); } - + let has_upper = password.chars().any(|c| c.is_uppercase()); let has_lower = password.chars().any(|c| c.is_lowercase()); let has_digit = password.chars().any(|c| c.is_numeric()); - + if !has_upper || !has_lower || !has_digit { return Err(JiveError::ValidationError { message: "Password must contain uppercase, lowercase, and numbers".to_string(), }); } - + Ok(()) } @@ -381,8 +331,7 @@ pub struct StringUtils; impl StringUtils { /// 清理和标准化文本 pub fn clean_text(text: &str) -> String { - text.trim() - .chars() + text.trim().chars() .filter(|c| !c.is_control() || c.is_whitespace()) .collect::() .split_whitespace() @@ -402,7 +351,7 @@ impl StringUtils { /// 生成简短的显示ID(用于UI) pub fn short_id(full_id: &str) -> String { if full_id.len() > 8 { - format!("{}...{}", &full_id[..4], &full_id[full_id.len() - 4..]) + format!("{}...{}", &full_id[..4], &full_id[full_id.len()-4..]) } else { full_id.to_string() } @@ -489,10 +438,7 @@ mod tests { #[test] fn test_string_utils() { assert_eq!(StringUtils::clean_text(" hello world "), "hello world"); - assert_eq!( - StringUtils::truncate("This is a long text", 10), - "This is..." - ); + assert_eq!(StringUtils::truncate("This is a long text", 10), "This is..."); assert_eq!(StringUtils::truncate("Short", 10), "Short"); assert_eq!(StringUtils::short_id("123456789012345678"), "1234...5678"); assert_eq!(StringUtils::short_id("12345678"), "12345678"); diff --git a/jive-core/src/wasm.rs b/jive-core/src/wasm.rs index 664847e7..85fd38a9 100644 --- a/jive-core/src/wasm.rs +++ b/jive-core/src/wasm.rs @@ -13,3 +13,4 @@ use wasm_bindgen::prelude::*; pub fn ping() -> String { "ok".to_string() } + diff --git a/jive-flutter/lib/main_network_test.dart b/jive-flutter/lib/main_network_test.dart index f02074e9..0f1e69f8 100644 --- a/jive-flutter/lib/main_network_test.dart +++ b/jive-flutter/lib/main_network_test.dart @@ -1,8 +1,7 @@ -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'providers/category_provider.dart'; -import 'models/category_template.dart'; +import 'package:jive_money/providers/category_provider.dart'; +import 'package:jive_money/models/category_template.dart'; void main() { runApp( diff --git a/jive-flutter/lib/screens/management/category_management_enhanced.dart b/jive-flutter/lib/screens/management/category_management_enhanced.dart index 3d25f3fa..7fb512c8 100644 --- a/jive-flutter/lib/screens/management/category_management_enhanced.dart +++ b/jive-flutter/lib/screens/management/category_management_enhanced.dart @@ -1,11 +1,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../../models/category.dart'; -import '../../models/category_template.dart'; -import '../../providers/category_provider.dart'; -import '../../providers/ledger_provider.dart'; -import '../../services/api/category_service.dart'; -import '../../widgets/bottom_sheets/import_details_sheet.dart'; +import 'package:jive_money/models/category.dart'; +import 'package:jive_money/models/category_template.dart'; +import 'package:jive_money/providers/category_provider.dart'; +import 'package:jive_money/providers/ledger_provider.dart'; +import 'package:jive_money/services/api/category_service.dart'; +import 'package:jive_money/widgets/bottom_sheets/import_details_sheet.dart'; class CategoryManagementEnhancedPage extends ConsumerStatefulWidget { const CategoryManagementEnhancedPage({super.key});