Compare commits
9 Commits
ebb066b3b0
...
f66b6221fd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f66b6221fd | ||
|
|
7ddba306e8 | ||
|
|
124dbd019a | ||
|
|
581e736f1b | ||
|
|
7031411b3d | ||
|
|
e95bc4d196 | ||
|
|
49af914f9b | ||
|
|
e3550fedac | ||
|
|
738fa72ec0 |
548
.gitignore
vendored
548
.gitignore
vendored
@@ -1,548 +0,0 @@
|
||||
# ---> 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
|
||||
18
LICENSE
18
LICENSE
@@ -1,18 +0,0 @@
|
||||
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.
|
||||
@@ -1,15 +0,0 @@
|
||||
-- 用户表初始化
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
username VARCHAR(50) UNIQUE NOT NULL,
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
email VARCHAR(100) UNIQUE,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- 插入测试用户(密码: 123456)
|
||||
-- bcrypt hash: $2b$12$REwMlLDCbzR4UpL6MWnzE.AacihwpFvQhGs7vDKTwwyNMb1qBWOTm
|
||||
INSERT INTO users (username, password_hash, email)
|
||||
VALUES ('admin', '$2b$12$REwMlLDCbzR4UpL6MWnzE.AacihwpFvQhGs7vDKTwwyNMb1qBWOTm', 'admin@example.com')
|
||||
ON CONFLICT (username) DO NOTHING;
|
||||
@@ -1,185 +0,0 @@
|
||||
use axum::{
|
||||
extract::State,
|
||||
http::StatusCode,
|
||||
response::Json,
|
||||
routing::post,
|
||||
Router,
|
||||
};
|
||||
use bcrypt::hash;
|
||||
use chrono::Utc;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::PgPool;
|
||||
use std::env;
|
||||
use std::sync::Arc;
|
||||
use tracing::{info, warn};
|
||||
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,
|
||||
#[validate(email)]
|
||||
email: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct RegisterResponse {
|
||||
success: bool,
|
||||
user_id: Option<i32>,
|
||||
message: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ErrorResponse {
|
||||
error: String,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
info!("Starting user-register 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");
|
||||
|
||||
info!("Database connected");
|
||||
|
||||
let state = Arc::new(AppState { db: pool });
|
||||
|
||||
let app = Router::new()
|
||||
.route("/register", post(register_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-register service listening on port {}", port);
|
||||
|
||||
axum::serve(listener, app).await.unwrap();
|
||||
}
|
||||
|
||||
async fn register_handler(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(payload): Json<RegisterRequest>,
|
||||
) -> (StatusCode, Json<RegisterResponse>) {
|
||||
info!("Registration attempt for user: {}", payload.username);
|
||||
|
||||
// 参数校验
|
||||
if let Err(e) = payload.validate() {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(RegisterResponse {
|
||||
success: false,
|
||||
user_id: None,
|
||||
message: format!("Validation error: {}", e),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// 检查用户名是否存在
|
||||
let existing: Option<(i32,)> = sqlx::query_as("SELECT id FROM users WHERE username = $1")
|
||||
.bind(&payload.username)
|
||||
.fetch_optional(&state.db)
|
||||
.await
|
||||
.unwrap_or(None);
|
||||
|
||||
if existing.is_some() {
|
||||
return (
|
||||
StatusCode::CONFLICT,
|
||||
Json(RegisterResponse {
|
||||
success: false,
|
||||
user_id: None,
|
||||
message: "Username already exists".to_string(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// 检查邮箱是否存在
|
||||
let existing_email: Option<(i32,)> = sqlx::query_as("SELECT id FROM users WHERE email = $1")
|
||||
.bind(&payload.email)
|
||||
.fetch_optional(&state.db)
|
||||
.await
|
||||
.unwrap_or(None);
|
||||
|
||||
if existing_email.is_some() {
|
||||
return (
|
||||
StatusCode::CONFLICT,
|
||||
Json(RegisterResponse {
|
||||
success: false,
|
||||
user_id: None,
|
||||
message: "Email already exists".to_string(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// 密码哈希
|
||||
let password_hash = match hash(&payload.password, bcrypt::DEFAULT_COST) {
|
||||
Ok(h) => h,
|
||||
Err(e) => {
|
||||
warn!("Password hashing failed: {}", e);
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(RegisterResponse {
|
||||
success: false,
|
||||
user_id: None,
|
||||
message: "Internal error".to_string(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// 插入用户
|
||||
let result = sqlx::query_as::<_, (i32,)>(
|
||||
"INSERT INTO users (username, password_hash, email, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $4)
|
||||
RETURNING id"
|
||||
)
|
||||
.bind(&payload.username)
|
||||
.bind(&password_hash)
|
||||
.bind(&payload.email)
|
||||
.bind(Utc::now())
|
||||
.fetch_one(&state.db)
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok((user_id,)) => {
|
||||
info!("User {} registered with id {}", payload.username, user_id);
|
||||
(
|
||||
StatusCode::CREATED,
|
||||
Json(RegisterResponse {
|
||||
success: true,
|
||||
user_id: Some(user_id),
|
||||
message: "User registered successfully".to_string(),
|
||||
}),
|
||||
)
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Registration failed: {}", e);
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(RegisterResponse {
|
||||
success: false,
|
||||
user_id: None,
|
||||
message: "Registration failed".to_string(),
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn health_handler() -> &'static str {
|
||||
"OK"
|
||||
}
|
||||
@@ -8,7 +8,7 @@ services:
|
||||
container_name: user-login
|
||||
environment:
|
||||
- RUST_LOG=info
|
||||
- DATABASE_URL=postgres://postgres:postgres@user-db:5432/user_db
|
||||
- DATABASE_URL=postgres://postgres:postgres@user-db:5432/user-db
|
||||
- REDIS_URL=redis://user-redis:6379/0
|
||||
- SERVICE_NAME=user-login
|
||||
- SERVICE_PORT=8080
|
||||
@@ -36,7 +36,7 @@ services:
|
||||
container_name: user-register
|
||||
environment:
|
||||
- RUST_LOG=info
|
||||
- DATABASE_URL=postgres://postgres:postgres@user-db:5432/user_db
|
||||
- DATABASE_URL=postgres://postgres:postgres@user-db:5432/user-db
|
||||
- REDIS_URL=redis://user-redis:6379/0
|
||||
- SERVICE_NAME=user-register
|
||||
- SERVICE_PORT=8080
|
||||
@@ -62,9 +62,9 @@ services:
|
||||
environment:
|
||||
- POSTGRES_USER=postgres
|
||||
- POSTGRES_PASSWORD=postgres
|
||||
- POSTGRES_DB=user_db
|
||||
- POSTGRES_DB=user-db
|
||||
volumes:
|
||||
- user_postgres_data:/var/lib/postgresql/data
|
||||
- user-postgres-data:/var/lib/postgresql/data
|
||||
- ./migrations:/docker-entrypoint-initdb.d:ro
|
||||
ports:
|
||||
- "5432:5432"
|
||||
@@ -72,7 +72,7 @@ services:
|
||||
- user-network
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U postgres -d user_db"]
|
||||
test: ["CMD-SHELL", "pg_isready -U postgres -d user-db"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
@@ -81,7 +81,7 @@ services:
|
||||
image: redis:8.6.2-alpine
|
||||
container_name: user-redis
|
||||
volumes:
|
||||
- user_redis_data:/data
|
||||
- user-redis-data:/data
|
||||
ports:
|
||||
- "6379:6379"
|
||||
networks:
|
||||
@@ -98,5 +98,7 @@ networks:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
user_postgres_data:
|
||||
user_redis_data:
|
||||
user-postgres-data:
|
||||
name: user-postgres-data
|
||||
user-redis-data:
|
||||
name: user-redis-data
|
||||
36
services/user-service/migrations/001_init.sql
Normal file
36
services/user-service/migrations/001_init.sql
Normal file
@@ -0,0 +1,36 @@
|
||||
-- 用户主表
|
||||
CREATE TABLE IF NOT EXISTS user_main (
|
||||
id UUID PRIMARY KEY,
|
||||
deleted BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
createdate TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
modifydate TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- 用户登录账号表
|
||||
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,
|
||||
createdate TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
modifydate TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
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,
|
||||
createdate TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
modifydate TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
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);
|
||||
@@ -14,7 +14,10 @@ serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
|
||||
# 数据库
|
||||
sqlx = { version = "0.8", features = ["runtime-tokio", "tls-rustls", "postgres", "chrono"] }
|
||||
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"] }
|
||||
@@ -90,9 +90,12 @@ async fn login_handler(
|
||||
) -> (StatusCode, Json<LoginResponse>) {
|
||||
info!("Login attempt for user: {}", payload.username);
|
||||
|
||||
// 查询用户
|
||||
// 查询用户账号与密码
|
||||
let user: Option<(String,)> = sqlx::query_as(
|
||||
"SELECT password_hash FROM users WHERE username = $1"
|
||||
"SELECT 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)
|
||||
@@ -11,7 +11,10 @@ tower = "0.5"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
|
||||
sqlx = { version = "0.8", features = ["runtime-tokio", "tls-rustls", "postgres", "chrono"] }
|
||||
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"
|
||||
252
services/user-service/user-register/src/main.rs
Normal file
252
services/user-service/user-register/src/main.rs
Normal file
@@ -0,0 +1,252 @@
|
||||
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 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<T> {
|
||||
device: i32,
|
||||
language: i32,
|
||||
data: T,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ApiResponse<T> {
|
||||
success: bool,
|
||||
message: String,
|
||||
data: Option<T>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct RegisterData {
|
||||
user_id: Uuid,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
info!("Starting user-register 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");
|
||||
|
||||
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<Arc<AppState>>,
|
||||
Json(req): Json<ApiRequest<RegisterRequest>>,
|
||||
) -> (StatusCode, Json<ApiResponse<RegisterData>>) {
|
||||
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 user_id = Uuid::now_v7();
|
||||
|
||||
if let Err(e) = sqlx::query(
|
||||
"INSERT INTO user_main (id) VALUES ($1)"
|
||||
)
|
||||
.bind(user_id)
|
||||
.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) VALUES ($1, $2, $3)"
|
||||
)
|
||||
.bind(account_id)
|
||||
.bind(user_id)
|
||||
.bind(&req.data.username)
|
||||
.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) VALUES ($1, $2, $3)"
|
||||
)
|
||||
.bind(password_id)
|
||||
.bind(user_id)
|
||||
.bind(&password_hash)
|
||||
.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<ApiResponse<()>>) {
|
||||
(
|
||||
StatusCode::OK,
|
||||
Json(ApiResponse {
|
||||
success: true,
|
||||
message: "OK".to_string(),
|
||||
data: None,
|
||||
}),
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user