diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fc44fc4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,548 @@ +# ---> Go +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work +go.work.sum + +# env file +.env + +# ---> Python +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +#uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/latest/usage/project/#working-with-version-control +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +# Ruff stuff: +.ruff_cache/ + +# PyPI configuration file +.pypirc + +# ---> Rust +# Generated by Cargo +# will have compiled files and executables +debug/ +target/ + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb + +# RustRover +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ +# ---> Node +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp +.cache + +# vitepress build output +**/.vitepress/dist + +# vitepress cache directory +**/.vitepress/cache + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +# ---> Xcode +## User settings +xcuserdata/ + +# ---> macOS +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# ---> Flutter +# Miscellaneous +*.class +*.lock +*.log +*.pyc +*.swp +.buildlog/ +.history + + + +# Flutter repo-specific +/bin/cache/ +/bin/internal/bootstrap.bat +/bin/internal/bootstrap.sh +/bin/mingit/ +/dev/benchmarks/mega_gallery/ +/dev/bots/.recipe_deps +/dev/bots/android_tools/ +/dev/devicelab/ABresults*.json +/dev/docs/doc/ +/dev/docs/flutter.docs.zip +/dev/docs/lib/ +/dev/docs/pubspec.yaml +/dev/integration_tests/**/xcuserdata +/dev/integration_tests/**/Pods +/packages/flutter/coverage/ +version +analysis_benchmark.json + +# packages file containing multi-root paths +.packages.generated + +# Flutter/Dart/Pub related +**/doc/api/ +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +**/generated_plugin_registrant.dart +.packages +.pub-preload-cache/ +.pub/ +build/ +flutter_*.png +linked_*.ds +unlinked.ds +unlinked_spec.ds + +# Android related +**/android/**/gradle-wrapper.jar +.gradle/ +**/android/captures/ +**/android/gradlew +**/android/gradlew.bat +**/android/local.properties +**/android/**/GeneratedPluginRegistrant.java +**/android/key.properties +*.jks + +# iOS/XCode related +**/ios/**/*.mode1v3 +**/ios/**/*.mode2v3 +**/ios/**/*.moved-aside +**/ios/**/*.pbxuser +**/ios/**/*.perspectivev3 +**/ios/**/*sync/ +**/ios/**/.sconsign.dblite +**/ios/**/.tags* +**/ios/**/.vagrant/ +**/ios/**/DerivedData/ +**/ios/**/Icon? +**/ios/**/Pods/ +**/ios/**/.symlinks/ +**/ios/**/profile +**/ios/**/xcuserdata +**/ios/.generated/ +**/ios/Flutter/.last_build_id +**/ios/Flutter/App.framework +**/ios/Flutter/Flutter.framework +**/ios/Flutter/Flutter.podspec +**/ios/Flutter/Generated.xcconfig +**/ios/Flutter/ephemeral +**/ios/Flutter/app.flx +**/ios/Flutter/app.zip +**/ios/Flutter/flutter_assets/ +**/ios/Flutter/flutter_export_environment.sh +**/ios/ServiceDefinitions.json +**/ios/Runner/GeneratedPluginRegistrant.* + +# macOS +**/Flutter/ephemeral/ +**/Pods/ +**/macos/Flutter/GeneratedPluginRegistrant.swift +**/macos/Flutter/ephemeral +**/xcuserdata/ + +# Windows +**/windows/flutter/generated_plugin_registrant.cc +**/windows/flutter/generated_plugin_registrant.h +**/windows/flutter/generated_plugins.cmake + +# Linux +**/linux/flutter/generated_plugin_registrant.cc +**/linux/flutter/generated_plugin_registrant.h +**/linux/flutter/generated_plugins.cmake + +# Coverage +coverage/ + +# Symbols +app.*.symbols + +# Exceptions to above rules. +!**/ios/**/default.mode1v3 +!**/ios/**/default.mode2v3 +!**/ios/**/default.pbxuser +!**/ios/**/default.perspectivev3 +!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages +!/dev/ci/**/Gemfile.lock +# ---> Android +# Gradle files +.gradle/ +build/ + +# Local configuration file (sdk path, etc) +local.properties + +# Log/OS Files +*.log + +# Android Studio generated files and folders +captures/ +.externalNativeBuild/ +.cxx/ +*.apk +output.json + +# IntelliJ +*.iml +.idea/ +misc.xml +deploymentTargetDropDown.xml +render.experimental.xml + +# Keystore files +*.jks +*.keystore + +# Google Services (e.g. APIs or Firebase) +google-services.json + +# Android Profiling +*.hprof + +backend/desc.md diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index c8eaa4e..0000000 --- a/AGENTS.md +++ /dev/null @@ -1,261 +0,0 @@ -# AGENTS.md - -本文件为 AI Agent 提供项目背景、结构说明和开发规范。 - -## 项目概述 - -这是一个基于 **Rust** 的微服务后端项目,采用 **Axum + Tokio** 技术栈,使用 **Nginx** 作为 API 网关,**PostgreSQL** 作为数据库,**Redis** 作为缓存。服务以 Docker 容器形式部署,每个核心功能拆分为独立的微服务二进制文件。 - -## 技术栈 - -| 层级 | 技术 | -|------|------| -| 语言 | Rust 2024 Edition | -| Web 框架 | axum 0.8, tokio 1.x, tower 0.5 | -| 数据库 | PostgreSQL 18.3 (sqlx 0.8) | -| 缓存 | Redis 8.6.2 (redis 0.29) | -| 网关 | Nginx 1.25 (Alpine) | -| 部署 | Docker, Docker Compose | -| 其他 | bcrypt, jsonwebtoken, uuid v7, chrono, tracing, validator | - -## 项目结构 - -``` -backend/ -├── services/ # 微服务目录 -│ └── user-service/ # 用户服务(当前唯一实现的服务域) -│ ├── user-login-account/ # 账号登录服务 (port 8001) -│ ├── user-register-account/ # 账号注册服务 (port 8002) -│ ├── user-login-email/ # 邮箱登录服务 (port 8003) -│ ├── user-register-email/ # 邮箱注册服务 (port 8004) -│ ├── migrations/ # 数据库初始化 SQL -│ ├── docker-compose.yml # 用户服务本地编排 -│ └── Dockerfile # 通用/遗留构建文件 -├── gateway/ # API 网关 -│ ├── Dockerfile -│ └── nginx/ -│ ├── nginx.conf -│ ├── conf.d/default.conf -│ └── conf.d/services/ # 各服务路由配置 -├── shared/ # 共享代码库(当前为空,待扩展) -├── deploy/ -│ └── local/redis.conf # 本地 Redis 配置 -├── scripts/ -│ ├── gateway.sh # 网关管理脚本(测试/重载/日志/证书) -│ └── init-multiple-databases.sh # Postgres 多库初始化 -└── README.md -``` - -## 微服务架构说明 - -### 服务拆分原则 - -每个用户功能(登录/注册)按**认证方式**拆分为独立服务: -- `user-login-account`: 账号密码登录,签发 JWT -- `user-login-email`: 邮箱密码登录,签发 JWT -- `user-register-account`: 账号注册,写入 `user_main` / `user_login_account` / `user_login_password` -- `user-register-email`: 邮箱注册,写入 `user_main` / `user_login_email` / `user_login_password` - -每个服务都是独立的 Rust Crate,拥有独立的 `Cargo.toml`、`src/main.rs` 和 `Dockerfile`。 - -### 数据库模型 - -核心表结构(见 `services/user-service/migrations/001_init.sql`): -- `user_main(id UUID PK, deleted BOOLEAN, create_date, modify_date)` -- `user_login_account(id UUID PK, user_id FK, account VARCHAR)` -- `user_login_email(id UUID PK, user_id FK, email VARCHAR)` -- `user_login_password(id UUID PK, user_id FK, password VARCHAR)` - -采用**软删除**设计(`deleted` 字段),账号/邮箱通过部分索引保证唯一性: -```sql -CREATE UNIQUE INDEX ... ON user_login_account(account) WHERE deleted = FALSE; -``` - -## 开发规范 - -### 1. API 公共约定 - -项目中存在两类接口风格,新增服务时请遵循对应场景的约定: - -#### 注册/业务类接口(使用统一包装) - -**请求包装格式:** -```json -{ - "device": 1, - "language": 1, - "data": { - // 业务字段 - } -} -``` -- `device`: 设备类型标识(`i32`) - - `1` = iOS - - `2` = Android - - `3` = Web - - `4` = iPad - - `5` = macOS - - `6` = Windows - - `7` = Linux -- `language`: 语言标识(`i32`) - - `1` = 简体中文 - - `2` = 繁体中文 - - `3` = 英文 -- `data`: 实际业务请求体 - -**响应包装格式:** -```json -{ - "success": true, - "message": "User registered successfully", - "data": { - // 业务返回数据,失败时为 null - } -} -``` -- `success`: 布尔值,表示业务是否成功 -- `message`: 可读的状态描述或错误信息 -- `data`: 业务数据,`Option`,失败时返回 `null` - -#### 登录/认证类接口(扁平响应) - -**请求格式:** 直接携带凭证字段(如 `username`/`email` + `password`)。 - -**响应格式:** -```json -{ - "success": true, - "token": "eyJhbGciOiJIUzI1NiIs...", - "message": "Login successful" -} -``` -- `success`: 布尔值 -- `token`: JWT Token,认证失败或错误时为 `null` -- `message`: 状态描述 - -#### 健康检查 - -所有服务必须暴露 `GET /health`,成功时返回 HTTP 200: -```text -OK -``` - -#### 错误响应(HTTP 非 200) - -网关层返回统一 JSON 错误: -```json -{ - "error": "Not Found", - "message": "The requested resource was not found", - "code": 404 -} -``` - -### 2. 代码风格 - -- 使用 **Rust 2024 Edition**。 -- 注释使用**中文**。 -- 服务状态通过 `Arc` 注入到 Axum Handler 中。 -- 注册类服务统一使用包装请求/响应格式: - ```rust - struct ApiRequest { device: i32, language: i32, data: T } - struct ApiResponse { success: bool, message: String, data: Option } - ``` - -### 3. 时间字段约定 - -所有表中的 `create_date` 和 `modify_date` **必须由业务层生成并传入**,数据库Schema中**不设置** `DEFAULT CURRENT_TIMESTAMP`,也不使用触发器自动更新。 - -- 建表时: - ```sql - create_date TIMESTAMP WITH TIME ZONE NOT NULL, - modify_date TIMESTAMP WITH TIME ZONE NOT NULL - ``` -- Rust 代码中使用 `chrono::Utc::now()` 生成时间戳,统一在事务开始前创建 `let now = Utc::now();`,确保同一笔业务中各表时间一致。 -- `modify_date` 更新时同样需要在业务代码中显式传入 `Utc::now()`。 - -#### 时区策略 - -项目采用**数据库存 UTC、查询按东八区显示**的策略: -- 业务层始终使用 `chrono::Utc::now()` 生成 UTC 时间写入数据库。 -- 每个服务在建立数据库连接池后,执行 `SET TIME ZONE 'Asia/Shanghai';`,确保 `TIMESTAMP WITH TIME ZONE` 字段在查询时以东八区格式返回。 -- 如需在 Rust 代码中做东八区展示转换,使用 `chrono::FixedOffset::east_opt(8 * 3600)` 处理。 - -### 4. 环境变量 - -所有服务通过环境变量读取配置: -- `DATABASE_URL` — PostgreSQL 连接串(必需) -- `REDIS_URL` — Redis 连接串 -- `SERVICE_PORT` — 服务监听端口(默认 8080) -- `JWT_SECRET` — JWT 签名密钥 -- `RUST_LOG` — 日志级别 - -### 5. Docker 构建 - -- 各微服务 Dockerfile 的构建上下文为**项目根目录**(`docker-compose.yml` 中使用 `context: ../..`)。 -- 构建采用多阶段(builder + runtime),基于 `rust:1.94.1-alpine3.23` 编译,最终运行在 `alpine:3.23`。 -- 共享代码更新时,需确保 `shared/` 目录在 Dockerfile 中被正确复制。 - -### 6. 网关与路由 - -- Nginx 监听 80/443,开发环境使用自签名证书。 -- 路由前缀约定: - - `/api/v1/users` → 用户服务通用接口 - - `/api/v1/auth` → 认证接口(更严格限流) -- 新增服务时,需在 `gateway/nginx/conf.d/services/` 下创建对应 `.conf` 文件,并在 `nginx.conf` 中添加上游 `upstream`。 - -## 常用命令 - -### 启动用户服务(本地开发) -```bash -cd services/user-service -docker-compose up --build -``` - -### 网关管理 -```bash -# 测试配置 -./scripts/gateway.sh test - -# 生成开发证书 -./scripts/gateway.sh certs - -# 查看状态 -./scripts/gateway.sh status - -# 热重载(容器运行中) -./scripts/gateway.sh reload -``` - -### 本地编译单个服务 -```bash -cd services/user-service/user-login-account -cargo run -``` - -## 扩展指南 - -### 新增微服务 - -1. 在 `services//` 下创建新目录,如 `services/order-service/order-create/`。 -2. 编写独立的 `Cargo.toml`、`src/main.rs`、`Dockerfile`。 -3. 在 `gateway/nginx/conf.d/services/` 添加路由配置。 -4. 在 `gateway/nginx/nginx.conf` 添加 `upstream`。 -5. 如需新数据库表,在对应服务域的 `migrations/` 目录添加 SQL 文件。 - -### 共享代码提取 - -当前 `shared/` 目录为空。当多个服务需要共用模型、中间件或工具函数时: -1. 在 `shared/` 下创建子模块(如 `shared/models`、`shared/middleware`)。 -2. 将共享 crate 以 path dependency 引入各微服务: - ```toml - [dependencies] - shared = { path = "../../shared" } - ``` -3. 更新各 Dockerfile,确保 `COPY shared /app/shared` 在依赖缓存步骤之前执行。 - -## 注意事项 - -- `services/user-service/Dockerfile` 是一个通用构建文件,但当前各微服务使用自己的 Dockerfile。修改时请确认影响范围。 -- 当前 `shared/` 为空,Agent 在修改代码时若发现重复逻辑,可提议提取到 `shared/`。 -- 网关配置文件中的 `api.example.com` 为占位域名,本地开发需配置 hosts 或使用 `localhost`。 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d6be502 --- /dev/null +++ b/LICENSE @@ -0,0 +1,18 @@ +MIT License + +Copyright (c) 2026 fish + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the +following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO +EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index e69de29..be22c99 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,2 @@ +# asset_helper + diff --git a/app/README.md b/app/README.md new file mode 100644 index 0000000..e69de29 diff --git a/backend b/backend new file mode 160000 index 0000000..e359a32 --- /dev/null +++ b/backend @@ -0,0 +1 @@ +Subproject commit e359a32bed8919ef9256c6e3439d4b46e549e149 diff --git a/deploy/local/redis.conf b/deploy/local/redis.conf deleted file mode 100644 index dea6ca8..0000000 --- a/deploy/local/redis.conf +++ /dev/null @@ -1,30 +0,0 @@ -# Redis 基础配置 -bind 0.0.0.0 -port 6379 -tcp-backlog 511 -timeout 0 -tcp-keepalive 300 - -# 持久化配置 -save 900 1 -save 300 10 -save 60 10000 -stop-writes-on-bgsave-error yes -rdbcompression yes -rdbchecksum yes -dbfilename dump.rdb -dir /data - -# 内存管理 -maxmemory 256mb -maxmemory-policy allkeys-lru - -# 日志 -loglevel notice - -# 安全 -protected-mode no - -# 性能优化 -hz 10 -dynamic-hz yes diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..e69de29 diff --git a/gateway/.dockerignore b/gateway/.dockerignore deleted file mode 100644 index 7d13148..0000000 --- a/gateway/.dockerignore +++ /dev/null @@ -1,5 +0,0 @@ -logs/ -ssl/*.pem -ssl/*.key -*.log -.DS_Store diff --git a/gateway/Dockerfile b/gateway/Dockerfile deleted file mode 100644 index e248ba9..0000000 --- a/gateway/Dockerfile +++ /dev/null @@ -1,27 +0,0 @@ -FROM nginx:1.25-alpine - -# 安装必要工具 -RUN apk add --no-cache curl ca-certificates - -# 创建日志目录 -RUN mkdir -p /var/log/nginx /var/www/certbot - -# 复制配置 -COPY nginx/nginx.conf /etc/nginx/nginx.conf -COPY nginx/conf.d/ /etc/nginx/conf.d/ - -# 创建自签名证书(仅用于开发,生产环境应挂载真实证书) -RUN apk add --no-cache openssl && \ - openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ - -keyout /etc/nginx/ssl/key.pem \ - -out /etc/nginx/ssl/cert.pem \ - -subj "/CN=api.example.com" && \ - apk del openssl - -# 健康检查 -HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ - CMD curl -f http://localhost/health || exit 1 - -EXPOSE 80 443 - -CMD ["nginx", "-g", "daemon off;"] diff --git a/gateway/nginx/conf.d/default.conf b/gateway/nginx/conf.d/default.conf deleted file mode 100644 index 29b9ec8..0000000 --- a/gateway/nginx/conf.d/default.conf +++ /dev/null @@ -1,84 +0,0 @@ -# 默认服务器 - 拒绝直接IP访问 -server { - listen 80 default_server; - listen [::]:80 default_server; - server_name _; - - return 444; -} - -# HTTP 重定向到 HTTPS -server { - listen 80; - listen [::]:80; - server_name api.example.com; - - location /.well-known/acme-challenge/ { - root /var/www/certbot; - } - - location / { - return 301 https://$server_name$request_uri; - } -} - -# API 网关主配置 -server { - listen 443 ssl http2; - listen [::]:443 ssl http2; - server_name api.example.com; - - # SSL 证书配置 - ssl_certificate /etc/nginx/ssl/cert.pem; - ssl_certificate_key /etc/nginx/ssl/key.pem; - ssl_session_timeout 1d; - ssl_session_cache shared:SSL:50m; - ssl_session_tickets off; - ssl_protocols TLSv1.2 TLSv1.3; - ssl_ciphers HIGH:!aNULL:!MD5; - ssl_prefer_server_ciphers off; - - # 客户端请求大小限制 - client_max_body_size 50M; - client_body_buffer_size 16k; - - # 超时配置 - proxy_connect_timeout 60s; - proxy_send_timeout 60s; - proxy_read_timeout 60s; - - # 安全响应头 - add_header X-Frame-Options "SAMEORIGIN" always; - add_header X-Content-Type-Options "nosniff" always; - add_header X-XSS-Protection "1; mode=block" always; - add_header Referrer-Policy "strict-origin-when-cross-origin" always; - - # 根路径 - 健康检查 - location / { - return 200 '{"status":"ok","service":"api-gateway","timestamp":"$time_iso8601"}\n'; - add_header Content-Type application/json; - } - - # 健康检查端点 - location /health { - access_log off; - return 200 '{"status":"healthy","timestamp":"$time_iso8601"}\n'; - add_header Content-Type application/json; - } - - # 包含各服务路由配置 - include /etc/nginx/conf.d/services/*.conf; - - # 错误处理 - error_page 404 /404.json; - location = /404.json { - return 404 '{"error":"Not Found","message":"The requested resource was not found","code":404}\n'; - add_header Content-Type application/json; - } - - error_page 500 502 503 504 /50x.json; - location = /50x.json { - return 500 '{"error":"Internal Server Error","message":"Something went wrong","code":500}\n'; - add_header Content-Type application/json; - } -} diff --git a/gateway/nginx/conf.d/services/order-service.conf b/gateway/nginx/conf.d/services/order-service.conf deleted file mode 100644 index ba9215d..0000000 --- a/gateway/nginx/conf.d/services/order-service.conf +++ /dev/null @@ -1,29 +0,0 @@ -# 订单服务路由 -location /api/v1/orders { - limit_req zone=general burst=30 nodelay; - limit_conn addr 10; - - proxy_pass http://order_service; - proxy_http_version 1.1; - - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Request-ID $request_id; -} - -# 购物车接口 -location /api/v1/cart { - limit_req zone=general burst=20 nodelay; - limit_conn addr 10; - - proxy_pass http://order_service; - proxy_http_version 1.1; - - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Request-ID $request_id; -} diff --git a/gateway/nginx/conf.d/services/payment-service.conf b/gateway/nginx/conf.d/services/payment-service.conf deleted file mode 100644 index d16c9f7..0000000 --- a/gateway/nginx/conf.d/services/payment-service.conf +++ /dev/null @@ -1,37 +0,0 @@ -# 支付服务路由(更严格的限流) -location /api/v1/payments { - limit_req zone=api_strict burst=10 nodelay; - limit_conn addr 5; - - proxy_pass http://payment_service; - proxy_http_version 1.1; - - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Request-ID $request_id; - - # 支付接口需要更长的超时时间 - proxy_read_timeout 120s; - proxy_connect_timeout 120s; - proxy_send_timeout 120s; -} - -# 支付回调接口(通常由第三方调用) -location /api/v1/webhooks/payment { - # 放宽限流,允许第三方服务调用 - limit_req zone=general burst=50 nodelay; - - proxy_pass http://payment_service; - proxy_http_version 1.1; - - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Request-ID $request_id; - - # 记录详细的访问日志以便审计 - access_log /var/log/nginx/payment-webhook.log main; -} diff --git a/gateway/nginx/conf.d/services/user-service.conf b/gateway/nginx/conf.d/services/user-service.conf deleted file mode 100644 index 6b7e18b..0000000 --- a/gateway/nginx/conf.d/services/user-service.conf +++ /dev/null @@ -1,39 +0,0 @@ -# 用户服务路由 -location /api/v1/users { - # 限流 - limit_req zone=general burst=20 nodelay; - limit_conn addr 10; - - # 代理设置 - proxy_pass http://user_service; - proxy_http_version 1.1; - - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Request-ID $request_id; - - # WebSocket 支持(如果需要) - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - - # 缓存控制 - proxy_cache_bypass $http_upgrade; - proxy_no_cache 1; -} - -# 认证相关接口(严格限流) -location /api/v1/auth { - limit_req zone=api_strict burst=5 nodelay; - limit_conn addr 3; - - proxy_pass http://user_service; - proxy_http_version 1.1; - - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Request-ID $request_id; -} diff --git a/gateway/nginx/nginx.conf b/gateway/nginx/nginx.conf deleted file mode 100644 index ab0c8a5..0000000 --- a/gateway/nginx/nginx.conf +++ /dev/null @@ -1,68 +0,0 @@ -user nginx; -worker_processes auto; -error_log /var/log/nginx/error.log warn; -pid /var/run/nginx.pid; - -events { - worker_connections 4096; - use epoll; - multi_accept on; -} - -http { - # 基础配置 - include /etc/nginx/mime.types; - default_type application/octet-stream; - - # 日志格式 - log_format main '$remote_addr - $remote_user [$time_local] "$request" ' - '$status $body_bytes_sent "$http_referer" ' - '"$http_user_agent" "$http_x_forwarded_for" ' - 'rt=$request_time uct="$upstream_connect_time" ' - 'uht="$upstream_header_time" urt="$upstream_response_time"'; - - access_log /var/log/nginx/access.log main; - - # 性能优化 - sendfile on; - tcp_nopush on; - tcp_nodelay on; - keepalive_timeout 65; - types_hash_max_size 2048; - - # 压缩 - gzip on; - gzip_vary on; - gzip_proxied any; - gzip_comp_level 6; - gzip_types text/plain text/css text/xml application/json application/javascript application/rss+xml application/atom+xml image/svg+xml; - - # 限流配置 - limit_req_zone $binary_remote_addr zone=general:10m rate=100r/s; - limit_req_zone $binary_remote_addr zone=api_strict:10m rate=10r/s; - - # 连接限制 - limit_conn_zone $binary_remote_addr zone=addr:10m; - - # 上游服务 - upstream user_service { - least_conn; - server user-service:8080 max_fails=3 fail_timeout=30s; - keepalive 32; - } - - upstream order_service { - least_conn; - server order-service:8080 max_fails=3 fail_timeout=30s; - keepalive 32; - } - - upstream payment_service { - least_conn; - server payment-service:8080 max_fails=3 fail_timeout=30s; - keepalive 32; - } - - # 包含子配置 - include /etc/nginx/conf.d/*.conf; -} diff --git a/scripts/gateway.sh b/scripts/gateway.sh deleted file mode 100755 index 537b65e..0000000 --- a/scripts/gateway.sh +++ /dev/null @@ -1,150 +0,0 @@ -#!/bin/bash - -# 网关管理脚本 - -set -e - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" -GATEWAY_DIR="$PROJECT_ROOT/gateway" -NGINX_CONF_DIR="$GATEWAY_DIR/nginx" - -cd "$PROJECT_ROOT" - -# 颜色输出 -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -NC='\033[0m' # No Color - -log_info() { - echo -e "${GREEN}[INFO]${NC} $1" -} - -log_warn() { - echo -e "${YELLOW}[WARN]${NC} $1" -} - -log_error() { - echo -e "${RED}[ERROR]${NC} $1" -} - -# 测试 nginx 配置 -test_config() { - log_info "Testing nginx configuration..." - - docker run --rm \ - -v "$NGINX_CONF_DIR/nginx.conf:/etc/nginx/nginx.conf:ro" \ - -v "$NGINX_CONF_DIR/conf.d:/etc/nginx/conf.d:ro" \ - nginx:1.25-alpine \ - nginx -t - - if [ $? -eq 0 ]; then - log_info "Configuration test passed!" - else - log_error "Configuration test failed!" - exit 1 - fi -} - -# 重新加载配置(热重载) -reload_config() { - log_info "Reloading nginx configuration..." - - CONTAINER_ID=$(docker ps -q -f name=api-gateway) - - if [ -z "$CONTAINER_ID" ]; then - log_error "Gateway container is not running" - exit 1 - fi - - docker exec "$CONTAINER_ID" nginx -s reload - log_info "Configuration reloaded successfully" -} - -# 查看网关日志 -view_logs() { - log_info "Viewing gateway logs..." - - if [ "$1" == "follow" ] || [ "$1" == "-f" ]; then - tail -f "$NGINX_CONF_DIR/logs/"*.log 2>/dev/null || docker logs -f api-gateway 2>/dev/null - else - tail -n 100 "$NGINX_CONF_DIR/logs/"*.log 2>/dev/null || docker logs --tail 100 api-gateway 2>/dev/null - fi -} - -# 生成自签名证书(开发用) -generate_certs() { - log_info "Generating self-signed certificates..." - - CERT_DIR="$NGINX_CONF_DIR/ssl" - mkdir -p "$CERT_DIR" - - openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ - -keyout "$CERT_DIR/key.pem" \ - -out "$CERT_DIR/cert.pem" \ - -subj "/CN=api.example.com" \ - -addext "subjectAltName=DNS:api.example.com,DNS:localhost,IP:127.0.0.1" - - log_info "Certificates generated in $CERT_DIR" -} - -# 显示状态 -status() { - log_info "Gateway Status:" - - CONTAINER_ID=$(docker ps -q -f name=api-gateway) - - if [ -n "$CONTAINER_ID" ]; then - echo " Container: Running ($CONTAINER_ID)" - docker exec "$CONTAINER_ID" nginx -V 2>/dev/null | head -1 - else - echo " Container: Not running" - fi - - echo "" - echo " Configuration files:" - ls -la "$NGINX_CONF_DIR/conf.d/" -} - -# 使用说明 -usage() { - echo "Gateway Management Script" - echo "" - echo "Usage: $0 " - echo "" - echo "Commands:" - echo " test Test nginx configuration" - echo " reload Reload configuration (hot reload)" - echo " logs View logs (use 'logs follow' for real-time)" - echo " certs Generate self-signed certificates (dev only)" - echo " status Show gateway status" - echo " help Show this help message" -} - -# 主逻辑 -case "${1:-help}" in - test) - test_config - ;; - reload) - reload_config - ;; - logs) - view_logs "$2" - ;; - certs) - generate_certs - ;; - status) - status - ;; - help|--help|-h) - usage - ;; - *) - log_error "Unknown command: $1" - usage - exit 1 - ;; -esac diff --git a/scripts/init-multiple-databases.sh b/scripts/init-multiple-databases.sh deleted file mode 100644 index 3597f4f..0000000 --- a/scripts/init-multiple-databases.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/bin/bash -set -e -set -u - -function create_database() { - local database=$1 - echo "Creating database '$database'" - psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" <<-EOSQL - SELECT 1 FROM pg_database WHERE datname = '$database'; - DO \$\$ - BEGIN - IF NOT EXISTS (SELECT 1 FROM pg_database WHERE datname = '$database') THEN - CREATE DATABASE $database; - END IF; - END - \$\$; - GRANT ALL PRIVILEGES ON DATABASE $database TO $POSTGRES_USER; -EOSQL -} - -if [ -n "$POSTGRES_MULTIPLE_DATABASES" ]; then - echo "Multiple database creation requested: $POSTGRES_MULTIPLE_DATABASES" - for db in $(echo "$POSTGRES_MULTIPLE_DATABASES" | tr ',' ' '); do - create_database "$db" - done - echo "Multiple databases created" -fi diff --git a/services/user-service/Dockerfile b/services/user-service/Dockerfile deleted file mode 100644 index 98d52fd..0000000 --- a/services/user-service/Dockerfile +++ /dev/null @@ -1,49 +0,0 @@ -# 构建阶段 -FROM rust:1.94.1-alpine3.23 AS builder - -# 安装构建依赖 -RUN apk add --no-cache musl-dev openssl-dev openssl-libs-static - -# 创建工作目录 -WORKDIR /app - -# 先复制共享代码和 Cargo 文件以利用缓存 -COPY shared /app/shared -COPY services/user-service/Cargo.toml services/user-service/Cargo.lock* ./ - -# 创建虚拟 main.rs 来缓存依赖 -RUN mkdir -p src && echo 'fn main() {}' > src/main.rs -RUN cargo build --release && rm -rf src - -# 复制真实源代码 -COPY services/user-service/src ./src - -# 构建(使用 touch 确保重新编译) -RUN touch src/main.rs && cargo build --release - -# 运行阶段 -FROM alpine:3.23 AS runtime - -# 安装运行依赖 -RUN apk add --no-cache ca-certificates tzdata - -# 创建非 root 用户 -RUN addgroup -g 1000 appuser && adduser -D -u 1000 -G appuser appuser - -WORKDIR /app - -# 从构建阶段复制二进制文件 -COPY --from=builder /app/target/release/user-service /app/user-service - -# 设置权限 -RUN chown -R appuser:appuser /app - -USER appuser - -# 健康检查 -HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ - CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1 - -EXPOSE 8080 - -CMD ["./user-service"] diff --git a/services/user-service/docker-compose.yml b/services/user-service/docker-compose.yml deleted file mode 100644 index 77ec314..0000000 --- a/services/user-service/docker-compose.yml +++ /dev/null @@ -1,159 +0,0 @@ -version: "3.8" - -services: - user-login-account: - build: - context: ../.. - dockerfile: services/user-service/user-login-account/Dockerfile - container_name: user-login-account - environment: - - RUST_LOG=info - - DATABASE_URL=postgres://postgres:postgres@user-db:5432/user-db - - REDIS_URL=redis://user-redis:6379/0 - - SERVICE_NAME=user-login-account - - SERVICE_PORT=8080 - - JWT_SECRET=${JWT_SECRET:-dev-secret-key} - ports: - - "8001:8080" - depends_on: - user-db: - condition: service_healthy - user-redis: - condition: service_healthy - networks: - - user-network - restart: unless-stopped - healthcheck: - test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:8080/health"] - interval: 30s - timeout: 10s - retries: 3 - - user-register-account: - build: - context: ../.. - dockerfile: services/user-service/user-register-account/Dockerfile - container_name: user-register-account - environment: - - RUST_LOG=info - - DATABASE_URL=postgres://postgres:postgres@user-db:5432/user-db - - REDIS_URL=redis://user-redis:6379/0 - - SERVICE_NAME=user-register-account - - SERVICE_PORT=8080 - ports: - - "8002:8080" - depends_on: - user-db: - condition: service_healthy - user-redis: - condition: service_healthy - networks: - - user-network - restart: unless-stopped - healthcheck: - test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:8080/health"] - interval: 30s - timeout: 10s - retries: 3 - - user-login-email: - build: - context: ../.. - dockerfile: services/user-service/user-login-email/Dockerfile - container_name: user-login-email - environment: - - RUST_LOG=info - - DATABASE_URL=postgres://postgres:postgres@user-db:5432/user-db - - REDIS_URL=redis://user-redis:6379/0 - - SERVICE_NAME=user-login-email - - SERVICE_PORT=8080 - - JWT_SECRET=${JWT_SECRET:-dev-secret-key} - ports: - - "8003:8080" - depends_on: - user-db: - condition: service_healthy - user-redis: - condition: service_healthy - networks: - - user-network - restart: unless-stopped - healthcheck: - test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:8080/health"] - interval: 30s - timeout: 10s - retries: 3 - - user-register-email: - build: - context: ../.. - dockerfile: services/user-service/user-register-email/Dockerfile - container_name: user-register-email - environment: - - RUST_LOG=info - - DATABASE_URL=postgres://postgres:postgres@user-db:5432/user-db - - REDIS_URL=redis://user-redis:6379/0 - - SERVICE_NAME=user-register-email - - SERVICE_PORT=8080 - ports: - - "8004:8080" - depends_on: - user-db: - condition: service_healthy - user-redis: - condition: service_healthy - networks: - - user-network - restart: unless-stopped - healthcheck: - test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:8080/health"] - interval: 30s - timeout: 10s - retries: 3 - - user-db: - image: postgres:18.3-alpine3.23 - container_name: user-db - environment: - - POSTGRES_USER=postgres - - POSTGRES_PASSWORD=postgres - - POSTGRES_DB=user-db - volumes: - - user-postgres-data:/var/lib/postgresql/data - - ./migrations:/docker-entrypoint-initdb.d:ro - ports: - - "5432:5432" - networks: - - user-network - restart: unless-stopped - healthcheck: - test: ["CMD-SHELL", "pg_isready -U postgres -d user-db"] - interval: 10s - timeout: 5s - retries: 5 - - user-redis: - image: redis:8.6.2-alpine - container_name: user-redis - volumes: - - user-redis-data:/data - ports: - - "6379:6379" - networks: - - user-network - restart: unless-stopped - healthcheck: - test: ["CMD", "redis-cli", "ping"] - interval: 10s - timeout: 5s - retries: 5 - -networks: - user-network: - driver: bridge - -volumes: - user-postgres-data: - name: user-postgres-data - user-redis-data: - name: user-redis-data diff --git a/services/user-service/migrations/001_init.sql b/services/user-service/migrations/001_init.sql deleted file mode 100644 index 2fdf07d..0000000 --- a/services/user-service/migrations/001_init.sql +++ /dev/null @@ -1,51 +0,0 @@ --- 用户主表 -CREATE TABLE IF NOT EXISTS user_main ( - id UUID PRIMARY KEY, - deleted BOOLEAN NOT NULL DEFAULT FALSE, - create_date TIMESTAMP WITH TIME ZONE NOT NULL, - modify_date TIMESTAMP WITH TIME ZONE NOT NULL -); - --- 用户登录账号表 -CREATE TABLE IF NOT EXISTS user_login_account ( - id UUID PRIMARY KEY, - user_id UUID NOT NULL, - account VARCHAR(100) NOT NULL, - deleted BOOLEAN NOT NULL DEFAULT FALSE, - create_date TIMESTAMP WITH TIME ZONE NOT NULL, - modify_date TIMESTAMP WITH TIME ZONE NOT NULL, - CONSTRAINT fk_user_login_account_user_main FOREIGN KEY (user_id) REFERENCES user_main(id) -); - -CREATE UNIQUE INDEX IF NOT EXISTS idx_user_login_account_active - ON user_login_account(account) - WHERE deleted = FALSE; - --- 用户密码表 -CREATE TABLE IF NOT EXISTS user_login_password ( - id UUID PRIMARY KEY, - user_id UUID NOT NULL, - password VARCHAR(255) NOT NULL, - deleted BOOLEAN NOT NULL DEFAULT FALSE, - create_date TIMESTAMP WITH TIME ZONE NOT NULL, - modify_date TIMESTAMP WITH TIME ZONE NOT NULL, - CONSTRAINT fk_user_login_password_user_main FOREIGN KEY (user_id) REFERENCES user_main(id) -); - -CREATE INDEX IF NOT EXISTS idx_user_login_password_user_id - ON user_login_password(user_id); - --- 用户登录邮箱表 -CREATE TABLE IF NOT EXISTS user_login_email ( - id UUID PRIMARY KEY, - user_id UUID NOT NULL, - email VARCHAR(255) NOT NULL, - deleted BOOLEAN NOT NULL DEFAULT FALSE, - create_date TIMESTAMP WITH TIME ZONE NOT NULL, - modify_date TIMESTAMP WITH TIME ZONE NOT NULL, - CONSTRAINT fk_user_login_email_user_main FOREIGN KEY (user_id) REFERENCES user_main(id) -); - -CREATE UNIQUE INDEX IF NOT EXISTS idx_user_login_email_active - ON user_login_email(email) - WHERE deleted = FALSE; diff --git a/services/user-service/user-login-account/Cargo.toml b/services/user-service/user-login-account/Cargo.toml deleted file mode 100644 index 47f3d1d..0000000 --- a/services/user-service/user-login-account/Cargo.toml +++ /dev/null @@ -1,45 +0,0 @@ -[package] -name = "user-login-account" -version = "0.1.0" -edition = "2024" - -[dependencies] -# Web 框架 -axum = "0.8" -tokio = { version = "1", features = ["full"] } -tower = "0.5" - -# 序列化 -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" - -# 数据库 -sqlx = { version = "0.8", features = ["runtime-tokio", "tls-rustls", "postgres", "chrono", "uuid"] } - -# UUID -uuid = { version = "1", features = ["v7", "serde"] } - -# Redis -redis = { version = "0.29", features = ["tokio-comp"] } - -# 密码哈希(bcrypt) -bcrypt = "0.17" - -# JWT -jsonwebtoken = "9.3" - -# 时间和日志 -chrono = { version = "0.4", features = ["serde"] } -tracing = "0.1" -tracing-subscriber = { version = "0.3", features = ["env-filter"] } - -# 环境变量 -dotenvy = "0.15" - -# 错误处理 -thiserror = "2.0" - -[profile.release] -opt-level = 3 -lto = true -strip = true diff --git a/services/user-service/user-login-account/Dockerfile b/services/user-service/user-login-account/Dockerfile deleted file mode 100644 index 9b1434d..0000000 --- a/services/user-service/user-login-account/Dockerfile +++ /dev/null @@ -1,39 +0,0 @@ -# 构建阶段 -FROM rust:1.94.1-alpine3.23 AS builder - -RUN apk add --no-cache musl-dev openssl-dev openssl-libs-static pkgconfig - -WORKDIR /app - -# 复制 user-login-account 代码 -COPY services/user-service/user-login-account/Cargo.toml services/user-service/user-login-account/Cargo.lock* ./ - -# 缓存依赖 -RUN mkdir -p src && echo 'fn main() {}' > src/main.rs -RUN cargo build --release 2>/dev/null || true -RUN rm -rf src - -# 复制真实源码 -COPY services/user-service/user-login-account/src ./src - -# 重新构建 -RUN touch src/main.rs && cargo build --release - -# 运行阶段 -FROM alpine:3.23 AS runtime - -RUN apk add --no-cache ca-certificates - -RUN addgroup -g 1000 appuser && adduser -D -u 1000 -G appuser appuser - -WORKDIR /app - -COPY --from=builder /app/target/release/user-login-account /app/user-login-account - -RUN chown -R appuser:appuser /app - -USER appuser - -EXPOSE 8080 - -CMD ["./user-login-account"] diff --git a/services/user-service/user-login-account/src/main.rs b/services/user-service/user-login-account/src/main.rs deleted file mode 100644 index b8203f5..0000000 --- a/services/user-service/user-login-account/src/main.rs +++ /dev/null @@ -1,196 +0,0 @@ -use axum::{ - extract::State, - http::StatusCode, - response::Json, - routing::post, - Router, -}; -use bcrypt::verify; -use chrono::{Duration, Utc}; -use jsonwebtoken::{encode, EncodingKey, Header}; -use serde::{Deserialize, Serialize}; -use sqlx::{Pool, Postgres}; -use std::env; -use std::sync::Arc; -use tracing::{info, warn}; - -// 应用状态 -#[derive(Clone)] -struct AppState { - db: Pool, - jwt_secret: String, -} - -// 登录请求 -#[derive(Deserialize)] -struct LoginRequest { - username: String, - password: String, -} - -// 统一响应包装 -#[derive(Serialize)] -struct ApiResponse { - success: bool, - message: String, - data: Option, -} - -// 登录业务数据 -#[derive(Serialize)] -struct LoginData { - token: String, -} - -// JWT Claims -#[derive(Serialize, Deserialize)] -struct Claims { - sub: String, - exp: usize, - iat: usize, -} - -#[tokio::main] -async fn main() { - // 初始化日志 - tracing_subscriber::fmt::init(); - - info!("Starting user-login-account service..."); - - // 数据库连接 - let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set"); - let pool = sqlx::postgres::PgPool::connect(&database_url) - .await - .expect("Failed to connect to database"); - - sqlx::query("SET TIME ZONE 'Asia/Shanghai'") - .execute(&pool) - .await - .expect("Failed to set timezone"); - - info!("Database connected"); - - // JWT 密钥 - let jwt_secret = env::var("JWT_SECRET").unwrap_or_else(|_| "dev-secret".to_string()); - - let state = Arc::new(AppState { - db: pool, - jwt_secret, - }); - - // 路由 - let app = Router::new() - .route("/login", post(login_handler)) - .route("/health", axum::routing::get(health_handler)) - .with_state(state); - - let port = env::var("SERVICE_PORT").unwrap_or_else(|_| "8080".to_string()); - let listener = tokio::net::TcpListener::bind(format!("0.0.0.0:{}", port)) - .await - .unwrap(); - - info!("User-login service listening on port {}", port); - - axum::serve(listener, app).await.unwrap(); -} - -// 登录处理 -async fn login_handler( - State(state): State>, - Json(payload): Json, -) -> (StatusCode, Json>) { - info!("Login attempt for user: {}", payload.username); - - // 查询用户账号与密码 - let user: Option<(uuid::Uuid, String)> = sqlx::query_as( - "SELECT a.user_id, p.password \ - FROM user_login_account a \ - JOIN user_login_password p ON a.user_id = p.user_id \ - WHERE a.account = $1 AND a.deleted = FALSE AND p.deleted = FALSE" - ) - .bind(&payload.username) - .fetch_optional(&state.db) - .await - .unwrap_or(None); - - match user { - Some((user_id, password_hash)) => { - // 验证密码 - tracing::debug!("Verifying password: input_len={}, hash_len={}", payload.password.len(), password_hash.len()); - match verify(&payload.password, &password_hash) { - Ok(true) => { - info!("User {} logged in successfully", payload.username); - - // 生成 JWT - let token = generate_token(&user_id.to_string(), &state.jwt_secret); - - ( - StatusCode::OK, - Json(ApiResponse { - success: true, - message: "Login successful".to_string(), - data: Some(LoginData { token }), - }), - ) - } - Ok(false) => { - warn!("Invalid password for user {}", payload.username); - ( - StatusCode::UNAUTHORIZED, - Json(ApiResponse { - success: false, - message: "Invalid credentials".to_string(), - data: None, - }), - ) - } - Err(e) => { - warn!("Password verification error: {:?}", e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiResponse { - success: false, - message: "Internal error".to_string(), - data: None, - }), - ) - } - } - } - None => { - warn!("User not found: {}", payload.username); - ( - StatusCode::UNAUTHORIZED, - Json(LoginResponse { - success: false, - token: None, - message: "Invalid credentials".to_string(), - }), - ) - } - } -} - -// 健康检查 -async fn health_handler() -> &'static str { - "OK" -} - -// 生成 JWT Token -fn generate_token(sub: &str, secret: &str) -> String { - let now = Utc::now(); - let exp = now + Duration::days(7); - - let claims = Claims { - sub: sub.to_string(), - iat: now.timestamp() as usize, - exp: exp.timestamp() as usize, - }; - - encode( - &Header::default(), - &claims, - &EncodingKey::from_secret(secret.as_bytes()), - ) - .unwrap() -} diff --git a/services/user-service/user-login-email/Cargo.toml b/services/user-service/user-login-email/Cargo.toml deleted file mode 100644 index d3be127..0000000 --- a/services/user-service/user-login-email/Cargo.toml +++ /dev/null @@ -1,45 +0,0 @@ -[package] -name = "user-login-email" -version = "0.1.0" -edition = "2024" - -[dependencies] -# Web 框架 -axum = "0.8" -tokio = { version = "1", features = ["full"] } -tower = "0.5" - -# 序列化 -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" - -# 数据库 -sqlx = { version = "0.8", features = ["runtime-tokio", "tls-rustls", "postgres", "chrono", "uuid"] } - -# UUID -uuid = { version = "1", features = ["v7", "serde"] } - -# Redis -redis = { version = "0.29", features = ["tokio-comp"] } - -# 密码哈希(bcrypt) -bcrypt = "0.17" - -# JWT -jsonwebtoken = "9.3" - -# 时间和日志 -chrono = { version = "0.4", features = ["serde"] } -tracing = "0.1" -tracing-subscriber = { version = "0.3", features = ["env-filter"] } - -# 环境变量 -dotenvy = "0.15" - -# 错误处理 -thiserror = "2.0" - -[profile.release] -opt-level = 3 -lto = true -strip = true diff --git a/services/user-service/user-login-email/Dockerfile b/services/user-service/user-login-email/Dockerfile deleted file mode 100644 index 79d9632..0000000 --- a/services/user-service/user-login-email/Dockerfile +++ /dev/null @@ -1,39 +0,0 @@ -# 构建阶段 -FROM rust:1.94.1-alpine3.23 AS builder - -RUN apk add --no-cache musl-dev openssl-dev openssl-libs-static pkgconfig - -WORKDIR /app - -# 复制 user-login-email 代码 -COPY services/user-service/user-login-email/Cargo.toml services/user-service/user-login-email/Cargo.lock* ./ - -# 缓存依赖 -RUN mkdir -p src && echo 'fn main() {}' > src/main.rs -RUN cargo build --release 2>/dev/null || true -RUN rm -rf src - -# 复制真实源码 -COPY services/user-service/user-login-email/src ./src - -# 重新构建 -RUN touch src/main.rs && cargo build --release - -# 运行阶段 -FROM alpine:3.23 AS runtime - -RUN apk add --no-cache ca-certificates - -RUN addgroup -g 1000 appuser && adduser -D -u 1000 -G appuser appuser - -WORKDIR /app - -COPY --from=builder /app/target/release/user-login-email /app/user-login-email - -RUN chown -R appuser:appuser /app - -USER appuser - -EXPOSE 8080 - -CMD ["./user-login-email"] diff --git a/services/user-service/user-login-email/src/main.rs b/services/user-service/user-login-email/src/main.rs deleted file mode 100644 index 9006ba2..0000000 --- a/services/user-service/user-login-email/src/main.rs +++ /dev/null @@ -1,196 +0,0 @@ -use axum::{ - extract::State, - http::StatusCode, - response::Json, - routing::post, - Router, -}; -use bcrypt::verify; -use chrono::{Duration, Utc}; -use jsonwebtoken::{encode, EncodingKey, Header}; -use serde::{Deserialize, Serialize}; -use sqlx::{Pool, Postgres}; -use std::env; -use std::sync::Arc; -use tracing::{info, warn}; - -// 应用状态 -#[derive(Clone)] -struct AppState { - db: Pool, - jwt_secret: String, -} - -// 登录请求 -#[derive(Deserialize)] -struct LoginRequest { - email: String, - password: String, -} - -// 统一响应包装 -#[derive(Serialize)] -struct ApiResponse { - success: bool, - message: String, - data: Option, -} - -// 登录业务数据 -#[derive(Serialize)] -struct LoginData { - token: String, -} - -// JWT Claims -#[derive(Serialize, Deserialize)] -struct Claims { - sub: String, - exp: usize, - iat: usize, -} - -#[tokio::main] -async fn main() { - // 初始化日志 - tracing_subscriber::fmt::init(); - - info!("Starting user-login-email service..."); - - // 数据库连接 - let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set"); - let pool = sqlx::postgres::PgPool::connect(&database_url) - .await - .expect("Failed to connect to database"); - - sqlx::query("SET TIME ZONE 'Asia/Shanghai'") - .execute(&pool) - .await - .expect("Failed to set timezone"); - - info!("Database connected"); - - // JWT 密钥 - let jwt_secret = env::var("JWT_SECRET").unwrap_or_else(|_| "dev-secret".to_string()); - - let state = Arc::new(AppState { - db: pool, - jwt_secret, - }); - - // 路由 - let app = Router::new() - .route("/login", post(login_handler)) - .route("/health", axum::routing::get(health_handler)) - .with_state(state); - - let port = env::var("SERVICE_PORT").unwrap_or_else(|_| "8080".to_string()); - let listener = tokio::net::TcpListener::bind(format!("0.0.0.0:{}", port)) - .await - .unwrap(); - - info!("User-login-email service listening on port {}", port); - - axum::serve(listener, app).await.unwrap(); -} - -// 登录处理 -async fn login_handler( - State(state): State>, - Json(payload): Json, -) -> (StatusCode, Json>) { - info!("Login attempt for email: {}", payload.email); - - // 查询用户邮箱与密码 - let user: Option<(uuid::Uuid, String)> = sqlx::query_as( - "SELECT e.user_id, p.password \ - FROM user_login_email e \ - JOIN user_login_password p ON e.user_id = p.user_id \ - WHERE e.email = $1 AND e.deleted = FALSE AND p.deleted = FALSE" - ) - .bind(&payload.email) - .fetch_optional(&state.db) - .await - .unwrap_or(None); - - match user { - Some((user_id, password_hash)) => { - // 验证密码 - tracing::debug!("Verifying password: input_len={}, hash_len={}", payload.password.len(), password_hash.len()); - match verify(&payload.password, &password_hash) { - Ok(true) => { - info!("Email {} logged in successfully", payload.email); - - // 生成 JWT - let token = generate_token(&user_id.to_string(), &state.jwt_secret); - - ( - StatusCode::OK, - Json(ApiResponse { - success: true, - message: "Login successful".to_string(), - data: Some(LoginData { token }), - }), - ) - } - Ok(false) => { - warn!("Invalid password for email {}", payload.email); - ( - StatusCode::UNAUTHORIZED, - Json(ApiResponse { - success: false, - message: "Invalid credentials".to_string(), - data: None, - }), - ) - } - Err(e) => { - warn!("Password verification error: {:?}", e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiResponse { - success: false, - message: "Internal error".to_string(), - data: None, - }), - ) - } - } - } - None => { - warn!("Email not found: {}", payload.email); - ( - StatusCode::UNAUTHORIZED, - Json(LoginResponse { - success: false, - token: None, - message: "Invalid credentials".to_string(), - }), - ) - } - } -} - -// 健康检查 -async fn health_handler() -> &'static str { - "OK" -} - -// 生成 JWT Token -fn generate_token(sub: &str, secret: &str) -> String { - let now = Utc::now(); - let exp = now + Duration::days(7); - - let claims = Claims { - sub: sub.to_string(), - iat: now.timestamp() as usize, - exp: exp.timestamp() as usize, - }; - - encode( - &Header::default(), - &claims, - &EncodingKey::from_secret(secret.as_bytes()), - ) - .unwrap() -} diff --git a/services/user-service/user-register-account/Cargo.toml b/services/user-service/user-register-account/Cargo.toml deleted file mode 100644 index 65df885..0000000 --- a/services/user-service/user-register-account/Cargo.toml +++ /dev/null @@ -1,33 +0,0 @@ -[package] -name = "user-register-account" -version = "0.1.0" -edition = "2024" - -[dependencies] -axum = "0.8" -tokio = { version = "1", features = ["full"] } -tower = "0.5" - -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" - -sqlx = { version = "0.8", features = ["runtime-tokio", "tls-rustls", "postgres", "chrono", "uuid"] } - -# UUID -uuid = { version = "1", features = ["v7", "serde"] } -redis = { version = "0.29", features = ["tokio-comp"] } - -bcrypt = "0.17" - -chrono = { version = "0.4", features = ["serde"] } -tracing = "0.1" -tracing-subscriber = { version = "0.3", features = ["env-filter"] } - -dotenvy = "0.15" -thiserror = "2.0" -validator = { version = "0.20", features = ["derive"] } - -[profile.release] -opt-level = 3 -lto = true -strip = true diff --git a/services/user-service/user-register-account/Dockerfile b/services/user-service/user-register-account/Dockerfile deleted file mode 100644 index fde8928..0000000 --- a/services/user-service/user-register-account/Dockerfile +++ /dev/null @@ -1,33 +0,0 @@ -FROM rust:1.94.1-alpine3.23 AS builder - -RUN apk add --no-cache musl-dev openssl-dev openssl-libs-static pkgconfig - -WORKDIR /app - -COPY services/user-service/user-register-account/Cargo.toml services/user-service/user-register-account/Cargo.lock* ./ - -RUN mkdir -p src && echo 'fn main() {}' > src/main.rs -RUN cargo build --release 2>/dev/null || true -RUN rm -rf src - -COPY services/user-service/user-register-account/src ./src - -RUN touch src/main.rs && cargo build --release - -FROM alpine:3.23 AS runtime - -RUN apk add --no-cache ca-certificates - -RUN addgroup -g 1000 appuser && adduser -D -u 1000 -G appuser appuser - -WORKDIR /app - -COPY --from=builder /app/target/release/user-register-account /app/user-register-account - -RUN chown -R appuser:appuser /app - -USER appuser - -EXPOSE 8080 - -CMD ["./user-register-account"] diff --git a/services/user-service/user-register-account/src/main.rs b/services/user-service/user-register-account/src/main.rs deleted file mode 100644 index a7905b2..0000000 --- a/services/user-service/user-register-account/src/main.rs +++ /dev/null @@ -1,265 +0,0 @@ -use axum::{ - extract::State, - http::StatusCode, - response::Json, - routing::{get, post}, - Router, -}; -use bcrypt::hash; -use serde::{Deserialize, Serialize}; -use sqlx::PgPool; -use std::env; -use std::sync::Arc; -use chrono::Utc; -use tracing::{info, warn}; -use uuid::Uuid; -use validator::Validate; - -#[derive(Clone)] -struct AppState { - db: PgPool, -} - -#[derive(Deserialize, Validate)] -struct RegisterRequest { - #[validate(length(min = 3, max = 50))] - username: String, - #[validate(length(min = 6))] - password: String, -} - -#[derive(Deserialize)] -struct ApiRequest { - device: i32, - language: i32, - data: T, -} - -#[derive(Serialize)] -struct ApiResponse { - success: bool, - message: String, - data: Option, -} - -#[derive(Serialize)] -struct RegisterData { - user_id: Uuid, -} - -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::init(); - - info!("Starting user-register-account service..."); - - let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set"); - let pool = sqlx::postgres::PgPool::connect(&database_url) - .await - .expect("Failed to connect to database"); - - sqlx::query("SET TIME ZONE 'Asia/Shanghai'") - .execute(&pool) - .await - .expect("Failed to set timezone"); - - info!("Database connected"); - - let state = Arc::new(AppState { db: pool }); - - let app = Router::new() - .route("/register", post(register_handler)) - .route("/health", get(health_handler)) - .with_state(state); - - let port = env::var("SERVICE_PORT").unwrap_or_else(|_| "8080".to_string()); - let listener = tokio::net::TcpListener::bind(format!("0.0.0.0:{}", port)) - .await - .unwrap(); - - info!("User-register service listening on port {}", port); - - axum::serve(listener, app).await.unwrap(); -} - -async fn register_handler( - State(state): State>, - Json(req): Json>, -) -> (StatusCode, Json>) { - info!( - "Registration attempt for user: {}, device: {}, language: {}", - req.data.username, req.device, req.language - ); - - // 参数校验 - if let Err(e) = req.data.validate() { - return ( - StatusCode::BAD_REQUEST, - Json(ApiResponse { - success: false, - message: format!("Validation error: {}", e), - data: None, - }), - ); - } - - // 检查账号是否已存在 - let existing: Option<(Uuid,)> = sqlx::query_as( - "SELECT id FROM user_login_account WHERE account = $1 AND deleted = FALSE" - ) - .bind(&req.data.username) - .fetch_optional(&state.db) - .await - .unwrap_or(None); - - if existing.is_some() { - return ( - StatusCode::CONFLICT, - Json(ApiResponse { - success: false, - message: "Username already exists".to_string(), - data: None, - }), - ); - } - - // 密码哈希 - let password_hash = match hash(&req.data.password, bcrypt::DEFAULT_COST) { - Ok(h) => h, - Err(e) => { - warn!("Password hashing failed: {}", e); - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiResponse { - success: false, - message: "Internal error".to_string(), - data: None, - }), - ); - } - }; - - // 插入用户(主从表事务) - let mut tx = match state.db.begin().await { - Ok(t) => t, - Err(e) => { - warn!("Transaction start failed: {}", e); - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiResponse { - success: false, - message: "Internal error".to_string(), - data: None, - }), - ); - } - }; - - let now = Utc::now(); - let user_id = Uuid::now_v7(); - - if let Err(e) = sqlx::query( - "INSERT INTO user_main (id, create_date, modify_date) VALUES ($1, $2, $3)" - ) - .bind(user_id) - .bind(now) - .bind(now) - .execute(&mut *tx) - .await - { - warn!("Insert user_main failed: {}", e); - let _ = tx.rollback().await; - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiResponse { - success: false, - message: "Registration failed".to_string(), - data: None, - }), - ); - } - - let account_id = Uuid::now_v7(); - if let Err(e) = sqlx::query( - "INSERT INTO user_login_account (id, user_id, account, create_date, modify_date) VALUES ($1, $2, $3, $4, $5)" - ) - .bind(account_id) - .bind(user_id) - .bind(&req.data.username) - .bind(now) - .bind(now) - .execute(&mut *tx) - .await - { - warn!("Insert user_login_account failed: {}", e); - let _ = tx.rollback().await; - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiResponse { - success: false, - message: "Registration failed".to_string(), - data: None, - }), - ); - } - - let password_id = Uuid::now_v7(); - if let Err(e) = sqlx::query( - "INSERT INTO user_login_password (id, user_id, password, create_date, modify_date) VALUES ($1, $2, $3, $4, $5)" - ) - .bind(password_id) - .bind(user_id) - .bind(&password_hash) - .bind(now) - .bind(now) - .execute(&mut *tx) - .await - { - warn!("Insert user_login_password failed: {}", e); - let _ = tx.rollback().await; - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiResponse { - success: false, - message: "Registration failed".to_string(), - data: None, - }), - ); - } - - match tx.commit().await { - Ok(()) => { - info!("User {} registered with id {}", req.data.username, user_id); - ( - StatusCode::CREATED, - Json(ApiResponse { - success: true, - message: "User registered successfully".to_string(), - data: Some(RegisterData { user_id }), - }), - ) - } - Err(e) => { - warn!("Registration failed: {}", e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiResponse { - success: false, - message: "Registration failed".to_string(), - data: None, - }), - ) - } - } -} - -async fn health_handler() -> (StatusCode, Json>) { - ( - StatusCode::OK, - Json(ApiResponse { - success: true, - message: "OK".to_string(), - data: None, - }), - ) -} diff --git a/services/user-service/user-register-email/Cargo.toml b/services/user-service/user-register-email/Cargo.toml deleted file mode 100644 index 4e37e10..0000000 --- a/services/user-service/user-register-email/Cargo.toml +++ /dev/null @@ -1,33 +0,0 @@ -[package] -name = "user-register-email" -version = "0.1.0" -edition = "2024" - -[dependencies] -axum = "0.8" -tokio = { version = "1", features = ["full"] } -tower = "0.5" - -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" - -sqlx = { version = "0.8", features = ["runtime-tokio", "tls-rustls", "postgres", "chrono", "uuid"] } - -# UUID -uuid = { version = "1", features = ["v7", "serde"] } -redis = { version = "0.29", features = ["tokio-comp"] } - -bcrypt = "0.17" - -chrono = { version = "0.4", features = ["serde"] } -tracing = "0.1" -tracing-subscriber = { version = "0.3", features = ["env-filter"] } - -dotenvy = "0.15" -thiserror = "2.0" -validator = { version = "0.20", features = ["derive"] } - -[profile.release] -opt-level = 3 -lto = true -strip = true diff --git a/services/user-service/user-register-email/Dockerfile b/services/user-service/user-register-email/Dockerfile deleted file mode 100644 index 8a24809..0000000 --- a/services/user-service/user-register-email/Dockerfile +++ /dev/null @@ -1,33 +0,0 @@ -FROM rust:1.94.1-alpine3.23 AS builder - -RUN apk add --no-cache musl-dev openssl-dev openssl-libs-static pkgconfig - -WORKDIR /app - -COPY services/user-service/user-register-email/Cargo.toml services/user-service/user-register-email/Cargo.lock* ./ - -RUN mkdir -p src && echo 'fn main() {}' > src/main.rs -RUN cargo build --release 2>/dev/null || true -RUN rm -rf src - -COPY services/user-service/user-register-email/src ./src - -RUN touch src/main.rs && cargo build --release - -FROM alpine:3.23 AS runtime - -RUN apk add --no-cache ca-certificates - -RUN addgroup -g 1000 appuser && adduser -D -u 1000 -G appuser appuser - -WORKDIR /app - -COPY --from=builder /app/target/release/user-register-email /app/user-register-email - -RUN chown -R appuser:appuser /app - -USER appuser - -EXPOSE 8080 - -CMD ["./user-register-email"] diff --git a/services/user-service/user-register-email/src/main.rs b/services/user-service/user-register-email/src/main.rs deleted file mode 100644 index 055a2d7..0000000 --- a/services/user-service/user-register-email/src/main.rs +++ /dev/null @@ -1,265 +0,0 @@ -use axum::{ - extract::State, - http::StatusCode, - response::Json, - routing::{get, post}, - Router, -}; -use bcrypt::hash; -use serde::{Deserialize, Serialize}; -use sqlx::PgPool; -use std::env; -use std::sync::Arc; -use chrono::Utc; -use tracing::{info, warn}; -use uuid::Uuid; -use validator::Validate; - -#[derive(Clone)] -struct AppState { - db: PgPool, -} - -#[derive(Deserialize, Validate)] -struct RegisterRequest { - #[validate(email)] - email: String, - #[validate(length(min = 6))] - password: String, -} - -#[derive(Deserialize)] -struct ApiRequest { - device: i32, - language: i32, - data: T, -} - -#[derive(Serialize)] -struct ApiResponse { - success: bool, - message: String, - data: Option, -} - -#[derive(Serialize)] -struct RegisterData { - user_id: Uuid, -} - -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::init(); - - info!("Starting user-register-email service..."); - - let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set"); - let pool = sqlx::postgres::PgPool::connect(&database_url) - .await - .expect("Failed to connect to database"); - - sqlx::query("SET TIME ZONE 'Asia/Shanghai'") - .execute(&pool) - .await - .expect("Failed to set timezone"); - - info!("Database connected"); - - let state = Arc::new(AppState { db: pool }); - - let app = Router::new() - .route("/register", post(register_handler)) - .route("/health", get(health_handler)) - .with_state(state); - - let port = env::var("SERVICE_PORT").unwrap_or_else(|_| "8080".to_string()); - let listener = tokio::net::TcpListener::bind(format!("0.0.0.0:{}", port)) - .await - .unwrap(); - - info!("User-register-email service listening on port {}", port); - - axum::serve(listener, app).await.unwrap(); -} - -async fn register_handler( - State(state): State>, - Json(req): Json>, -) -> (StatusCode, Json>) { - info!( - "Email registration attempt for: {}, device: {}, language: {}", - req.data.email, req.device, req.language - ); - - // 参数校验 - if let Err(e) = req.data.validate() { - return ( - StatusCode::BAD_REQUEST, - Json(ApiResponse { - success: false, - message: format!("Validation error: {}", e), - data: None, - }), - ); - } - - // 检查邮箱是否已存在 - let existing: Option<(Uuid,)> = sqlx::query_as( - "SELECT id FROM user_login_email WHERE email = $1 AND deleted = FALSE" - ) - .bind(&req.data.email) - .fetch_optional(&state.db) - .await - .unwrap_or(None); - - if existing.is_some() { - return ( - StatusCode::CONFLICT, - Json(ApiResponse { - success: false, - message: "Email already exists".to_string(), - data: None, - }), - ); - } - - // 密码哈希 - let password_hash = match hash(&req.data.password, bcrypt::DEFAULT_COST) { - Ok(h) => h, - Err(e) => { - warn!("Password hashing failed: {}", e); - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiResponse { - success: false, - message: "Internal error".to_string(), - data: None, - }), - ); - } - }; - - // 插入用户(主从表事务) - let mut tx = match state.db.begin().await { - Ok(t) => t, - Err(e) => { - warn!("Transaction start failed: {}", e); - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiResponse { - success: false, - message: "Internal error".to_string(), - data: None, - }), - ); - } - }; - - let now = Utc::now(); - let user_id = Uuid::now_v7(); - - if let Err(e) = sqlx::query( - "INSERT INTO user_main (id, create_date, modify_date) VALUES ($1, $2, $3)" - ) - .bind(user_id) - .bind(now) - .bind(now) - .execute(&mut *tx) - .await - { - warn!("Insert user_main failed: {}", e); - let _ = tx.rollback().await; - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiResponse { - success: false, - message: "Registration failed".to_string(), - data: None, - }), - ); - } - - let email_id = Uuid::now_v7(); - if let Err(e) = sqlx::query( - "INSERT INTO user_login_email (id, user_id, email, create_date, modify_date) VALUES ($1, $2, $3, $4, $5)" - ) - .bind(email_id) - .bind(user_id) - .bind(&req.data.email) - .bind(now) - .bind(now) - .execute(&mut *tx) - .await - { - warn!("Insert user_login_email failed: {}", e); - let _ = tx.rollback().await; - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiResponse { - success: false, - message: "Registration failed".to_string(), - data: None, - }), - ); - } - - let password_id = Uuid::now_v7(); - if let Err(e) = sqlx::query( - "INSERT INTO user_login_password (id, user_id, password, create_date, modify_date) VALUES ($1, $2, $3, $4, $5)" - ) - .bind(password_id) - .bind(user_id) - .bind(&password_hash) - .bind(now) - .bind(now) - .execute(&mut *tx) - .await - { - warn!("Insert user_login_password failed: {}", e); - let _ = tx.rollback().await; - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiResponse { - success: false, - message: "Registration failed".to_string(), - data: None, - }), - ); - } - - match tx.commit().await { - Ok(()) => { - info!("Email {} registered with id {}", req.data.email, user_id); - ( - StatusCode::CREATED, - Json(ApiResponse { - success: true, - message: "User registered successfully".to_string(), - data: Some(RegisterData { user_id }), - }), - ) - } - Err(e) => { - warn!("Registration failed: {}", e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiResponse { - success: false, - message: "Registration failed".to_string(), - data: None, - }), - ) - } - } -} - -async fn health_handler() -> (StatusCode, Json>) { - ( - StatusCode::OK, - Json(ApiResponse { - success: true, - message: "OK".to_string(), - data: None, - }), - ) -}