refactor: 数据层迁往服务端 (Express+DuckDB), APP 变瘦客户端只留缓存
- server: 新增 DuckDB 数据层 (db.js) 与鉴权 (auth.js: 验证码+token), 挂载 /api 业务路由 (衣物/搭配/日历/统计/MCP), 错误统一中间件 - 前端 src/api/index.ts 全部业务调用改为 HTTP (Bearer token), 客户端不再持有数据库 - 删除 Rust 数据层 (db.rs/auth.rs/mcp.rs/models.rs), Cargo.toml 裁剪 duckdb/chrono 等依赖 - Dockerfile: 基镜像 alpine→slim (@duckdb/node-api glibc 预编译), DB 落 /app/data 卷 - docker-build.yml: 移除 prune --volumes, 挂具名卷 wardrobe-data 持久化 - Landing/Profile 文案改为云端同步口径; 退出登录调服务端会话销毁
This commit is contained in:
@@ -14,17 +14,6 @@ tauri = { version = "2", features = ["tray-icon"] }
|
||||
tauri-plugin-shell = "2"
|
||||
# 前端 OTA 升级 (运行时覆盖 tauri://localhost 资源; 替换 Assets provider)
|
||||
tauri-plugin-hotswap = "0.0.4"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
duckdb = { version = "0.10", features = ["bundled"] }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
uuid = { version = "1", features = ["v4"] }
|
||||
# 版本上限: chrono 0.4.40 起 Datelike 新增 quarter(), 与 arrow-arith 51 的
|
||||
# ChronoDateExt::quarter (duckdb 0.10.2 非可选依赖 arrow 51) 产生 E0034 方法歧义
|
||||
chrono = { version = ">=0.4.38, <0.4.40", features = ["serde"] }
|
||||
sha2 = "0.10"
|
||||
hex = "0.4"
|
||||
ctrlc = "3"
|
||||
log = "0.4"
|
||||
env_logger = "0.11"
|
||||
|
||||
|
||||
@@ -1,151 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Mutex;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use log::info;
|
||||
use crate::db::Database;
|
||||
use crate::models::*;
|
||||
|
||||
const VERIFICATION_CODE: &str = "1111";
|
||||
const CODE_EXPIRY_SECONDS: i64 = 300;
|
||||
|
||||
pub struct AuthManager {
|
||||
codes: Mutex<HashMap<String, VerificationCode>>,
|
||||
}
|
||||
|
||||
impl AuthManager {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
codes: Mutex::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
fn now_secs() -> i64 {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs() as i64
|
||||
}
|
||||
|
||||
pub fn send_verification(&self, target: &str) -> Result<String, String> {
|
||||
let code = VERIFICATION_CODE.to_string();
|
||||
let now = Self::now_secs();
|
||||
let vc = VerificationCode {
|
||||
target: target.to_string(),
|
||||
code: code.clone(),
|
||||
created_at: now,
|
||||
expires_at: now + CODE_EXPIRY_SECONDS,
|
||||
};
|
||||
self.codes.lock().unwrap().insert(target.to_string(), vc);
|
||||
info!("Verification code sent to {}: {}", target, code);
|
||||
Ok(format!("验证码已发送至 {}", target))
|
||||
}
|
||||
|
||||
fn verify_code(&self, target: &str, code: &str) -> bool {
|
||||
let codes = self.codes.lock().unwrap();
|
||||
if let Some(vc) = codes.get(target) {
|
||||
let now = Self::now_secs();
|
||||
vc.code == code && now < vc.expires_at
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn register(
|
||||
&self,
|
||||
db: &Database,
|
||||
email: Option<&str>,
|
||||
phone: Option<&str>,
|
||||
code: &str,
|
||||
nickname: &str,
|
||||
) -> Result<AuthResult, String> {
|
||||
let target = email.or(phone).ok_or("请提供邮箱或手机号")?;
|
||||
|
||||
if !self.verify_code(target, code) {
|
||||
return Ok(AuthResult {
|
||||
success: false,
|
||||
message: "验证码无效或已过期".to_string(),
|
||||
user: None,
|
||||
token: None,
|
||||
});
|
||||
}
|
||||
|
||||
if email.is_some() {
|
||||
if db.get_user_by_email(target).map_err(|e| e.to_string())?.is_some() {
|
||||
return Ok(AuthResult {
|
||||
success: false,
|
||||
message: "该邮箱已注册".to_string(),
|
||||
user: None,
|
||||
token: None,
|
||||
});
|
||||
}
|
||||
} else if phone.is_some() {
|
||||
if db.get_user_by_phone(target).map_err(|e| e.to_string())?.is_some() {
|
||||
return Ok(AuthResult {
|
||||
success: false,
|
||||
message: "该手机号已注册".to_string(),
|
||||
user: None,
|
||||
token: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let id = uuid::Uuid::new_v4().to_string();
|
||||
let user = db.create_user(&id, email, phone, nickname).map_err(|e| e.to_string())?;
|
||||
let token = format!("tok_{}", uuid::Uuid::new_v4().to_string().replace('-', ""));
|
||||
|
||||
self.codes.lock().unwrap().remove(target);
|
||||
|
||||
info!("User registered: {}", id);
|
||||
Ok(AuthResult {
|
||||
success: true,
|
||||
message: "注册成功".to_string(),
|
||||
user: Some(user),
|
||||
token: Some(token),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn login(
|
||||
&self,
|
||||
db: &Database,
|
||||
email: Option<&str>,
|
||||
phone: Option<&str>,
|
||||
code: &str,
|
||||
) -> Result<AuthResult, String> {
|
||||
let target = email.or(phone).ok_or("请提供邮箱或手机号")?;
|
||||
|
||||
if !self.verify_code(target, code) {
|
||||
return Ok(AuthResult {
|
||||
success: false,
|
||||
message: "验证码无效或已过期".to_string(),
|
||||
user: None,
|
||||
token: None,
|
||||
});
|
||||
}
|
||||
|
||||
let user = if email.is_some() {
|
||||
db.get_user_by_email(target).map_err(|e| e.to_string())?
|
||||
} else {
|
||||
db.get_user_by_phone(target).map_err(|e| e.to_string())?
|
||||
};
|
||||
|
||||
self.codes.lock().unwrap().remove(target);
|
||||
|
||||
match user {
|
||||
Some(u) => {
|
||||
let token = format!("tok_{}", uuid::Uuid::new_v4().to_string().replace('-', ""));
|
||||
Ok(AuthResult {
|
||||
success: true,
|
||||
message: "登录成功".to_string(),
|
||||
user: Some(u),
|
||||
token: Some(token),
|
||||
})
|
||||
}
|
||||
None => Ok(AuthResult {
|
||||
success: false,
|
||||
message: "账号不存在,请先注册".to_string(),
|
||||
user: None,
|
||||
token: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,395 +0,0 @@
|
||||
use duckdb::{Connection, params};
|
||||
use std::sync::Mutex;
|
||||
use std::path::PathBuf;
|
||||
use log::info;
|
||||
use crate::models::*;
|
||||
|
||||
pub struct Database {
|
||||
conn: Mutex<Connection>,
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
impl Database {
|
||||
pub fn new(data_dir: &PathBuf) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
std::fs::create_dir_all(data_dir)?;
|
||||
let db_path = data_dir.join("wardrobe.duckdb");
|
||||
let conn = Connection::open(&db_path)?;
|
||||
|
||||
let db = Self {
|
||||
conn: Mutex::new(conn),
|
||||
path: db_path,
|
||||
};
|
||||
db.init_tables()?;
|
||||
info!("DuckDB initialized at {:?}", db.path);
|
||||
Ok(db)
|
||||
}
|
||||
|
||||
fn init_tables(&self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
conn.execute_batch("
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id VARCHAR PRIMARY KEY,
|
||||
email VARCHAR UNIQUE,
|
||||
phone VARCHAR UNIQUE,
|
||||
nickname VARCHAR NOT NULL DEFAULT '',
|
||||
avatar_path VARCHAR,
|
||||
created_at VARCHAR NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS clothing_items (
|
||||
id VARCHAR PRIMARY KEY,
|
||||
user_id VARCHAR NOT NULL,
|
||||
name VARCHAR NOT NULL,
|
||||
category VARCHAR NOT NULL,
|
||||
color VARCHAR NOT NULL,
|
||||
brand VARCHAR,
|
||||
material VARCHAR,
|
||||
seasons VARCHAR DEFAULT '[]',
|
||||
occasions VARCHAR DEFAULT '[]',
|
||||
formality INTEGER DEFAULT 50,
|
||||
thickness INTEGER DEFAULT 50,
|
||||
image_path VARCHAR,
|
||||
purchase_date VARCHAR,
|
||||
purchase_price DOUBLE,
|
||||
last_cleaned VARCHAR,
|
||||
clean_method VARCHAR,
|
||||
wear_count INTEGER DEFAULT 0,
|
||||
last_worn VARCHAR,
|
||||
is_favorite BOOLEAN DEFAULT FALSE,
|
||||
created_at VARCHAR NOT NULL,
|
||||
updated_at VARCHAR NOT NULL,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS outfits (
|
||||
id VARCHAR PRIMARY KEY,
|
||||
user_id VARCHAR NOT NULL,
|
||||
name VARCHAR NOT NULL,
|
||||
item_ids VARCHAR DEFAULT '[]',
|
||||
occasion VARCHAR,
|
||||
season VARCHAR,
|
||||
match_score INTEGER,
|
||||
ai_analysis VARCHAR,
|
||||
is_saved BOOLEAN DEFAULT FALSE,
|
||||
created_at VARCHAR NOT NULL,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS daily_records (
|
||||
id VARCHAR PRIMARY KEY,
|
||||
user_id VARCHAR NOT NULL,
|
||||
date VARCHAR NOT NULL,
|
||||
outfit_id VARCHAR,
|
||||
weather_temp DOUBLE,
|
||||
weather_condition VARCHAR,
|
||||
notes VARCHAR,
|
||||
created_at VARCHAR NOT NULL,
|
||||
UNIQUE(user_id, date),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_clothing_user ON clothing_items(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_clothing_category ON clothing_items(category);
|
||||
CREATE INDEX IF NOT EXISTS idx_outfits_user ON outfits(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_daily_user_date ON daily_records(user_id, date);
|
||||
")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ── User CRUD ──
|
||||
|
||||
pub fn create_user(&self, id: &str, email: Option<&str>, phone: Option<&str>, nickname: &str) -> Result<User, Box<dyn std::error::Error>> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
conn.execute(
|
||||
"INSERT INTO users (id, email, phone, nickname, created_at) VALUES (?1, ?2, ?3, ?4, ?5)",
|
||||
params![id, email, phone, nickname, now],
|
||||
)?;
|
||||
Ok(User {
|
||||
id: id.to_string(),
|
||||
email: email.map(String::from),
|
||||
phone: phone.map(String::from),
|
||||
nickname: nickname.to_string(),
|
||||
avatar_path: None,
|
||||
created_at: now,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_user_by_email(&self, email: &str) -> Result<Option<User>, Box<dyn std::error::Error>> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let mut stmt = conn.prepare("SELECT id, email, phone, nickname, avatar_path, created_at FROM users WHERE email = ?1")?;
|
||||
let mut rows = stmt.query_map(params![email], |row| {
|
||||
Ok(User {
|
||||
id: row.get(0)?,
|
||||
email: row.get(1)?,
|
||||
phone: row.get(2)?,
|
||||
nickname: row.get(3)?,
|
||||
avatar_path: row.get(4)?,
|
||||
created_at: row.get(5)?,
|
||||
})
|
||||
})?;
|
||||
match rows.next() {
|
||||
Some(row) => Ok(Some(row?)),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_user_by_phone(&self, phone: &str) -> Result<Option<User>, Box<dyn std::error::Error>> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let mut stmt = conn.prepare("SELECT id, email, phone, nickname, avatar_path, created_at FROM users WHERE phone = ?1")?;
|
||||
let mut rows = stmt.query_map(params![phone], |row| {
|
||||
Ok(User {
|
||||
id: row.get(0)?,
|
||||
email: row.get(1)?,
|
||||
phone: row.get(2)?,
|
||||
nickname: row.get(3)?,
|
||||
avatar_path: row.get(4)?,
|
||||
created_at: row.get(5)?,
|
||||
})
|
||||
})?;
|
||||
match rows.next() {
|
||||
Some(row) => Ok(Some(row?)),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
// ── Clothing CRUD ──
|
||||
|
||||
pub fn create_clothing(&self, item: &ClothingItem) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let seasons_json = serde_json::to_string(&item.seasons)?;
|
||||
let occasions_json = serde_json::to_string(&item.occasions)?;
|
||||
conn.execute(
|
||||
"INSERT INTO clothing_items (id, user_id, name, category, color, brand, material, seasons, occasions, formality, thickness, image_path, purchase_date, purchase_price, last_cleaned, clean_method, wear_count, last_worn, is_favorite, created_at, updated_at)
|
||||
VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,?13,?14,?15,?16,?17,?18,?19,?20,?21)",
|
||||
params![
|
||||
item.id, item.user_id, item.name, item.category, item.color,
|
||||
item.brand, item.material, seasons_json, occasions_json,
|
||||
item.formality, item.thickness, item.image_path,
|
||||
item.purchase_date, item.purchase_price, item.last_cleaned,
|
||||
item.clean_method, item.wear_count, item.last_worn,
|
||||
item.is_favorite, item.created_at, item.updated_at
|
||||
],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn list_clothing(&self, user_id: &str, category: Option<&str>) -> Result<Vec<ClothingItem>, Box<dyn std::error::Error>> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let base = "SELECT id, user_id, name, category, color, brand, material, seasons, occasions, formality, thickness, image_path, purchase_date, purchase_price, last_cleaned, clean_method, wear_count, last_worn, is_favorite, created_at, updated_at FROM clothing_items WHERE user_id = ?1";
|
||||
let (sql, params_clause): (String, Vec<Box<dyn duckdb::ToSql>>) = match category {
|
||||
Some(cat) => (
|
||||
format!("{} AND category = ?2 ORDER BY created_at DESC", base),
|
||||
vec![Box::new(cat.to_string())],
|
||||
),
|
||||
None => (
|
||||
format!("{} ORDER BY created_at DESC", base),
|
||||
Vec::new(),
|
||||
),
|
||||
};
|
||||
let mut stmt = conn.prepare(&sql)?;
|
||||
|
||||
// Build unified param list: user_id + optional category
|
||||
let mut all_params: Vec<Box<dyn duckdb::ToSql>> = Vec::with_capacity(1 + params_clause.len());
|
||||
all_params.push(Box::new(user_id.to_string()));
|
||||
all_params.extend(params_clause);
|
||||
|
||||
let param_refs: Vec<&dyn duckdb::ToSql> = all_params.iter().map(|b| b.as_ref()).collect();
|
||||
|
||||
let mut rows = stmt.query(param_refs.as_slice())?;
|
||||
let mut items = Vec::new();
|
||||
while let Some(row) = rows.next()? {
|
||||
items.push(Self::map_clothing_row(row)?);
|
||||
}
|
||||
Ok(items)
|
||||
}
|
||||
|
||||
pub fn get_clothing(&self, id: &str) -> Result<Option<ClothingItem>, Box<dyn std::error::Error>> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let mut stmt = conn.prepare("SELECT id, user_id, name, category, color, brand, material, seasons, occasions, formality, thickness, image_path, purchase_date, purchase_price, last_cleaned, clean_method, wear_count, last_worn, is_favorite, created_at, updated_at FROM clothing_items WHERE id = ?1")?;
|
||||
let mut rows = stmt.query_map(params![id], Self::map_clothing_row)?;
|
||||
match rows.next() {
|
||||
Some(row) => Ok(Some(row?)),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_clothing(&self, id: &str, name: &str, category: &str, color: &str) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
conn.execute(
|
||||
"UPDATE clothing_items SET name = ?1, category = ?2, color = ?3, updated_at = ?4 WHERE id = ?5",
|
||||
params![name, category, color, now, id],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn delete_clothing(&self, id: &str) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
conn.execute("DELETE FROM clothing_items WHERE id = ?1", params![id])?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn map_clothing_row(row: &duckdb::Row) -> Result<ClothingItem, duckdb::Error> {
|
||||
let seasons_str: String = row.get(7)?;
|
||||
let occasions_str: String = row.get(8)?;
|
||||
Ok(ClothingItem {
|
||||
id: row.get(0)?,
|
||||
user_id: row.get(1)?,
|
||||
name: row.get(2)?,
|
||||
category: row.get(3)?,
|
||||
color: row.get(4)?,
|
||||
brand: row.get(5)?,
|
||||
material: row.get(6)?,
|
||||
seasons: serde_json::from_str(&seasons_str).unwrap_or_default(),
|
||||
occasions: serde_json::from_str(&occasions_str).unwrap_or_default(),
|
||||
formality: row.get(9)?,
|
||||
thickness: row.get(10)?,
|
||||
image_path: row.get(11)?,
|
||||
purchase_date: row.get(12)?,
|
||||
purchase_price: row.get(13)?,
|
||||
last_cleaned: row.get(14)?,
|
||||
clean_method: row.get(15)?,
|
||||
wear_count: row.get(16)?,
|
||||
last_worn: row.get(17)?,
|
||||
is_favorite: row.get(18)?,
|
||||
created_at: row.get(19)?,
|
||||
updated_at: row.get(20)?,
|
||||
})
|
||||
}
|
||||
|
||||
// ── Outfit CRUD ──
|
||||
|
||||
pub fn create_outfit(&self, outfit: &Outfit) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let item_ids_json = serde_json::to_string(&outfit.item_ids)?;
|
||||
conn.execute(
|
||||
"INSERT INTO outfits (id, user_id, name, item_ids, occasion, season, match_score, ai_analysis, is_saved, created_at)
|
||||
VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10)",
|
||||
params![
|
||||
outfit.id, outfit.user_id, outfit.name, item_ids_json,
|
||||
outfit.occasion, outfit.season, outfit.match_score,
|
||||
outfit.ai_analysis, outfit.is_saved, outfit.created_at
|
||||
],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn list_outfits(&self, user_id: &str) -> Result<Vec<Outfit>, Box<dyn std::error::Error>> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let mut stmt = conn.prepare("SELECT id, user_id, name, item_ids, occasion, season, match_score, ai_analysis, is_saved, created_at FROM outfits WHERE user_id = ?1 ORDER BY created_at DESC")?;
|
||||
let rows = stmt.query_map(params![user_id], |row| {
|
||||
let item_ids_str: String = row.get(3)?;
|
||||
Ok(Outfit {
|
||||
id: row.get(0)?,
|
||||
user_id: row.get(1)?,
|
||||
name: row.get(2)?,
|
||||
item_ids: serde_json::from_str(&item_ids_str).unwrap_or_default(),
|
||||
occasion: row.get(4)?,
|
||||
season: row.get(5)?,
|
||||
match_score: row.get(6)?,
|
||||
ai_analysis: row.get(7)?,
|
||||
is_saved: row.get(8)?,
|
||||
created_at: row.get(9)?,
|
||||
})
|
||||
})?;
|
||||
let mut outfits = Vec::new();
|
||||
for row in rows {
|
||||
outfits.push(row?);
|
||||
}
|
||||
Ok(outfits)
|
||||
}
|
||||
|
||||
// ── Daily Record CRUD ──
|
||||
|
||||
pub fn upsert_daily_record(&self, record: &DailyRecord) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
conn.execute(
|
||||
"INSERT INTO daily_records (id, user_id, date, outfit_id, weather_temp, weather_condition, notes, created_at)
|
||||
VALUES (?1,?2,?3,?4,?5,?6,?7,?8)
|
||||
ON CONFLICT (user_id, date) DO UPDATE SET outfit_id = ?4, weather_temp = ?5, weather_condition = ?6, notes = ?7",
|
||||
params![
|
||||
record.id, record.user_id, record.date, record.outfit_id,
|
||||
record.weather_temp, record.weather_condition, record.notes, record.created_at
|
||||
],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn list_daily_records(&self, user_id: &str, limit: i32) -> Result<Vec<DailyRecord>, Box<dyn std::error::Error>> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let mut stmt = conn.prepare("SELECT id, user_id, date, outfit_id, weather_temp, weather_condition, notes, created_at FROM daily_records WHERE user_id = ?1 ORDER BY date DESC LIMIT ?2")?;
|
||||
let rows = stmt.query_map(params![user_id, limit], |row| {
|
||||
Ok(DailyRecord {
|
||||
id: row.get(0)?,
|
||||
user_id: row.get(1)?,
|
||||
date: row.get(2)?,
|
||||
outfit_id: row.get(3)?,
|
||||
weather_temp: row.get(4)?,
|
||||
weather_condition: row.get(5)?,
|
||||
notes: row.get(6)?,
|
||||
created_at: row.get(7)?,
|
||||
})
|
||||
})?;
|
||||
let mut records = Vec::new();
|
||||
for row in rows {
|
||||
records.push(row?);
|
||||
}
|
||||
Ok(records)
|
||||
}
|
||||
|
||||
// ── Stats ──
|
||||
|
||||
pub fn get_stats(&self, user_id: &str) -> Result<WardrobeStats, Box<dyn std::error::Error>> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
|
||||
let total_items: i32 = conn.query_row(
|
||||
"SELECT COUNT(*) FROM clothing_items WHERE user_id = ?1", params![user_id], |r| r.get(0)
|
||||
)?;
|
||||
let total_outfits: i32 = conn.query_row(
|
||||
"SELECT COUNT(*) FROM outfits WHERE user_id = ?1", params![user_id], |r| r.get(0)
|
||||
)?;
|
||||
let favorite_count: i32 = conn.query_row(
|
||||
"SELECT COUNT(*) FROM clothing_items WHERE user_id = ?1 AND is_favorite = TRUE", params![user_id], |r| r.get(0)
|
||||
)?;
|
||||
let unworn_30d: i32 = conn.query_row(
|
||||
"SELECT COUNT(*) FROM clothing_items WHERE user_id = ?1 AND (last_worn IS NULL OR last_worn < strftime('%Y-%m-%d', 'now', '-30 days'))",
|
||||
params![user_id], |r| r.get(0)
|
||||
)?;
|
||||
let needs_cleaning: i32 = conn.query_row(
|
||||
"SELECT COUNT(*) FROM clothing_items WHERE user_id = ?1 AND (last_cleaned IS NULL OR last_cleaned < strftime('%Y-%m-%d', 'now', '-30 days'))",
|
||||
params![user_id], |r| r.get(0)
|
||||
)?;
|
||||
|
||||
let utilization_rate = if total_items > 0 {
|
||||
((total_items - unworn_30d) as f64 / total_items as f64 * 100.0).round()
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
let mut category_counts = std::collections::HashMap::new();
|
||||
let cats = ["top", "bottom", "outer", "dress", "shoes", "accessory"];
|
||||
for cat in cats {
|
||||
let count: i32 = conn.query_row(
|
||||
"SELECT COUNT(*) FROM clothing_items WHERE user_id = ?1 AND category = ?2",
|
||||
params![user_id, cat],
|
||||
|r| r.get(0),
|
||||
)?;
|
||||
category_counts.insert(cat.to_string(), count);
|
||||
}
|
||||
|
||||
Ok(WardrobeStats {
|
||||
total_items,
|
||||
total_outfits,
|
||||
utilization_rate,
|
||||
category_counts,
|
||||
favorite_count,
|
||||
unworn_30d,
|
||||
needs_cleaning,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn db_path(&self) -> &PathBuf {
|
||||
&self.path
|
||||
}
|
||||
}
|
||||
+6
-268
@@ -1,233 +1,19 @@
|
||||
mod auth;
|
||||
mod db;
|
||||
mod mcp;
|
||||
mod models;
|
||||
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
use log::info;
|
||||
use tauri::Manager;
|
||||
|
||||
use auth::AuthManager;
|
||||
use db::Database;
|
||||
use mcp::McpService;
|
||||
use models::*;
|
||||
|
||||
struct AppState {
|
||||
db: Arc<Database>,
|
||||
auth: Arc<AuthManager>,
|
||||
mcp: Arc<McpService>,
|
||||
shutdown_flag: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════
|
||||
// Tauri IPC Commands
|
||||
// ══════════════════════════════════════════════════
|
||||
|
||||
#[tauri::command]
|
||||
fn send_code(state: tauri::State<AppState>, target: String) -> Result<String, String> {
|
||||
state.auth.send_verification(&target)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn register(
|
||||
state: tauri::State<AppState>,
|
||||
email: Option<String>,
|
||||
phone: Option<String>,
|
||||
code: String,
|
||||
nickname: String,
|
||||
) -> Result<AuthResult, String> {
|
||||
state.auth.register(&state.db, email.as_deref(), phone.as_deref(), &code, &nickname)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn login(
|
||||
state: tauri::State<AppState>,
|
||||
email: Option<String>,
|
||||
phone: Option<String>,
|
||||
code: String,
|
||||
) -> Result<AuthResult, String> {
|
||||
state.auth.login(&state.db, email.as_deref(), phone.as_deref(), &code)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn add_clothing(
|
||||
state: tauri::State<AppState>,
|
||||
user_id: String,
|
||||
name: String,
|
||||
category: String,
|
||||
color: String,
|
||||
brand: Option<String>,
|
||||
seasons: Option<Vec<String>>,
|
||||
occasions: Option<Vec<String>>,
|
||||
) -> Result<ClothingItem, String> {
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
let item = ClothingItem {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
user_id,
|
||||
name,
|
||||
category,
|
||||
color,
|
||||
brand,
|
||||
material: None,
|
||||
seasons: seasons.unwrap_or_default(),
|
||||
occasions: occasions.unwrap_or_default(),
|
||||
formality: 50,
|
||||
thickness: 50,
|
||||
image_path: None,
|
||||
purchase_date: None,
|
||||
purchase_price: None,
|
||||
last_cleaned: None,
|
||||
clean_method: None,
|
||||
wear_count: 0,
|
||||
last_worn: None,
|
||||
is_favorite: false,
|
||||
created_at: now.clone(),
|
||||
updated_at: now,
|
||||
};
|
||||
state.db.create_clothing(&item).map_err(|e| e.to_string())?;
|
||||
Ok(item)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn list_clothing(
|
||||
state: tauri::State<AppState>,
|
||||
user_id: String,
|
||||
category: Option<String>,
|
||||
) -> Result<Vec<ClothingItem>, String> {
|
||||
state.db.list_clothing(&user_id, category.as_deref()).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn get_clothing(state: tauri::State<AppState>, id: String) -> Result<Option<ClothingItem>, String> {
|
||||
state.db.get_clothing(&id).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn update_clothing(
|
||||
state: tauri::State<AppState>,
|
||||
id: String,
|
||||
name: String,
|
||||
category: String,
|
||||
color: String,
|
||||
) -> Result<(), String> {
|
||||
state.db.update_clothing(&id, &name, &category, &color).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn delete_clothing(state: tauri::State<AppState>, id: String) -> Result<(), String> {
|
||||
state.db.delete_clothing(&id).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn add_outfit(
|
||||
state: tauri::State<AppState>,
|
||||
user_id: String,
|
||||
name: String,
|
||||
item_ids: Vec<String>,
|
||||
occasion: Option<String>,
|
||||
season: Option<String>,
|
||||
) -> Result<Outfit, String> {
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
let outfit = Outfit {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
user_id,
|
||||
name,
|
||||
item_ids,
|
||||
occasion,
|
||||
season,
|
||||
match_score: None,
|
||||
ai_analysis: None,
|
||||
is_saved: false,
|
||||
created_at: now,
|
||||
};
|
||||
state.db.create_outfit(&outfit).map_err(|e| e.to_string())?;
|
||||
Ok(outfit)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn list_outfits(state: tauri::State<AppState>, user_id: String) -> Result<Vec<Outfit>, String> {
|
||||
state.db.list_outfits(&user_id).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn record_daily(
|
||||
state: tauri::State<AppState>,
|
||||
user_id: String,
|
||||
date: String,
|
||||
outfit_id: Option<String>,
|
||||
notes: Option<String>,
|
||||
) -> Result<DailyRecord, String> {
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
let record = DailyRecord {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
user_id,
|
||||
date,
|
||||
outfit_id,
|
||||
weather_temp: None,
|
||||
weather_condition: None,
|
||||
notes,
|
||||
created_at: now,
|
||||
};
|
||||
state.db.upsert_daily_record(&record).map_err(|e| e.to_string())?;
|
||||
Ok(record)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn list_daily_records(
|
||||
state: tauri::State<AppState>,
|
||||
user_id: String,
|
||||
limit: Option<i32>,
|
||||
) -> Result<Vec<DailyRecord>, String> {
|
||||
state.db.list_daily_records(&user_id, limit.unwrap_or(30)).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn get_stats(state: tauri::State<AppState>, user_id: String) -> Result<WardrobeStats, String> {
|
||||
state.db.get_stats(&user_id).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
// ── MCP Commands ──
|
||||
|
||||
#[tauri::command]
|
||||
fn mcp_list_tools(state: tauri::State<AppState>) -> Result<Vec<mcp::McpTool>, String> {
|
||||
Ok(state.mcp.list_tools())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn mcp_call_tool(
|
||||
state: tauri::State<AppState>,
|
||||
tool_name: String,
|
||||
args: serde_json::Value,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
state.mcp.call_tool(&tool_name, &args)
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════
|
||||
// Main (桌面 + Android 共用)
|
||||
// ══════════════════════════════════════════════════
|
||||
|
||||
// 必须用 tauri_plugin_hotswap::init 包装 generate_context,
|
||||
// 返回 (插件实例, 新的 context), 并把插件注入 Builder。
|
||||
// Android 上通过 mobile_entry_point 从这里启动。
|
||||
// 业务数据全部放在服务端 (Express + DuckDB), 客户端是瘦客户端:
|
||||
// 前端统一走 HTTP API, 本地不再持有任何数据库。
|
||||
// 这里只保留 Tauri 引导 + hotswap (OTA) + shell 插件。
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
env_logger::init();
|
||||
info!("Wardrobe starting...");
|
||||
|
||||
let shutdown_flag = Arc::new(AtomicBool::new(false));
|
||||
let ctrlc_flag = shutdown_flag.clone();
|
||||
let setup_flag = shutdown_flag.clone();
|
||||
let event_flag = shutdown_flag.clone();
|
||||
|
||||
ctrlc::set_handler(move || {
|
||||
info!("Ctrl+C received, shutting down gracefully...");
|
||||
ctrlc_flag.store(true, Ordering::SeqCst);
|
||||
})
|
||||
.expect("Error setting Ctrl-C handler");
|
||||
|
||||
// 初始化 hotswap 插件 (OTA 升级), 返回改装后的 context
|
||||
// 必须用 tauri_plugin_hotswap::init 包装 generate_context,
|
||||
// 返回 (插件实例, 新的 context), 并把插件注入 Builder。
|
||||
// Android 上 OTA 可热替换前端资源。
|
||||
let context = tauri::generate_context!();
|
||||
let (hotswap, context) = match tauri_plugin_hotswap::init(context) {
|
||||
Ok(ok) => ok,
|
||||
@@ -240,54 +26,6 @@ pub fn run() {
|
||||
let app = tauri::Builder::default()
|
||||
.plugin(hotswap)
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.setup(move |app| {
|
||||
info!("Initializing database...");
|
||||
|
||||
let data_dir = app.path().app_data_dir().unwrap_or_else(|_| std::path::PathBuf::from("."));
|
||||
let database = match Database::new(&data_dir) {
|
||||
Ok(db) => Arc::new(db),
|
||||
Err(e) => {
|
||||
eprintln!("Failed to initialize database: {}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
let auth = Arc::new(AuthManager::new());
|
||||
let mcp = Arc::new(McpService::new(database.clone()));
|
||||
|
||||
app.manage(AppState {
|
||||
db: database,
|
||||
auth,
|
||||
mcp,
|
||||
shutdown_flag: setup_flag,
|
||||
});
|
||||
|
||||
info!("Wardrobe app initialized, data dir: {:?}", data_dir);
|
||||
Ok(())
|
||||
})
|
||||
.on_window_event(move |_window, event| {
|
||||
if let tauri::WindowEvent::Destroyed = event {
|
||||
info!("Window destroyed, shutting down gracefully...");
|
||||
event_flag.store(true, Ordering::SeqCst);
|
||||
}
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
send_code,
|
||||
register,
|
||||
login,
|
||||
add_clothing,
|
||||
list_clothing,
|
||||
get_clothing,
|
||||
update_clothing,
|
||||
delete_clothing,
|
||||
add_outfit,
|
||||
list_outfits,
|
||||
record_daily,
|
||||
list_daily_records,
|
||||
get_stats,
|
||||
mcp_list_tools,
|
||||
mcp_call_tool,
|
||||
])
|
||||
.build(context)
|
||||
.expect("error while building tauri application");
|
||||
|
||||
|
||||
@@ -1,205 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
use log::info;
|
||||
use crate::db::Database;
|
||||
use crate::models::*;
|
||||
|
||||
pub struct McpService {
|
||||
db: Arc<Database>,
|
||||
}
|
||||
|
||||
impl McpService {
|
||||
pub fn new(db: Arc<Database>) -> Self {
|
||||
Self { db }
|
||||
}
|
||||
|
||||
pub fn list_tools(&self) -> Vec<McpTool> {
|
||||
vec![
|
||||
McpTool {
|
||||
name: "list_clothing".to_string(),
|
||||
description: "获取用户衣橱中的所有衣物列表".to_string(),
|
||||
input_schema: r#"{"type":"object","properties":{"user_id":{"type":"string"},"category":{"type":"string"}},"required":["user_id"]}"#.to_string(),
|
||||
},
|
||||
McpTool {
|
||||
name: "get_clothing".to_string(),
|
||||
description: "根据ID获取单件衣物详情".to_string(),
|
||||
input_schema: r#"{"type":"object","properties":{"id":{"type":"string"}},"required":["id"]}"#.to_string(),
|
||||
},
|
||||
McpTool {
|
||||
name: "add_clothing".to_string(),
|
||||
description: "添加新衣物到衣橱".to_string(),
|
||||
input_schema: r#"{"type":"object","properties":{"user_id":{"type":"string"},"name":{"type":"string"},"category":{"type":"string"},"color":{"type":"string"},"brand":{"type":"string"},"seasons":{"type":"array","items":{"type":"string"}},"occasions":{"type":"array","items":{"type":"string"}}},"required":["user_id","name","category","color"]}"#.to_string(),
|
||||
},
|
||||
McpTool {
|
||||
name: "update_clothing".to_string(),
|
||||
description: "更新衣物信息".to_string(),
|
||||
input_schema: r#"{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"category":{"type":"string"},"color":{"type":"string"}},"required":["id"]}"#.to_string(),
|
||||
},
|
||||
McpTool {
|
||||
name: "delete_clothing".to_string(),
|
||||
description: "从衣橱中删除衣物".to_string(),
|
||||
input_schema: r#"{"type":"object","properties":{"id":{"type":"string"}},"required":["id"]}"#.to_string(),
|
||||
},
|
||||
McpTool {
|
||||
name: "list_outfits".to_string(),
|
||||
description: "获取用户的所有搭配方案".to_string(),
|
||||
input_schema: r#"{"type":"object","properties":{"user_id":{"type":"string"}},"required":["user_id"]}"#.to_string(),
|
||||
},
|
||||
McpTool {
|
||||
name: "add_outfit".to_string(),
|
||||
description: "创建新的搭配方案".to_string(),
|
||||
input_schema: r#"{"type":"object","properties":{"user_id":{"type":"string"},"name":{"type":"string"},"item_ids":{"type":"array","items":{"type":"string"}},"occasion":{"type":"string"},"season":{"type":"string"}},"required":["user_id","name","item_ids"]}"#.to_string(),
|
||||
},
|
||||
McpTool {
|
||||
name: "get_wardrobe_stats".to_string(),
|
||||
description: "获取衣橱统计数据(总数、利用率、分类统计等)".to_string(),
|
||||
input_schema: r#"{"type":"object","properties":{"user_id":{"type":"string"}},"required":["user_id"]}"#.to_string(),
|
||||
},
|
||||
McpTool {
|
||||
name: "record_daily_outfit".to_string(),
|
||||
description: "记录每日穿搭".to_string(),
|
||||
input_schema: r#"{"type":"object","properties":{"user_id":{"type":"string"},"date":{"type":"string"},"outfit_id":{"type":"string"},"notes":{"type":"string"}},"required":["user_id","date"]}"#.to_string(),
|
||||
},
|
||||
McpTool {
|
||||
name: "list_daily_records".to_string(),
|
||||
description: "获取穿搭日历记录".to_string(),
|
||||
input_schema: r#"{"type":"object","properties":{"user_id":{"type":"string"},"limit":{"type":"integer"}},"required":["user_id"]}"#.to_string(),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
pub fn call_tool(&self, name: &str, args: &serde_json::Value) -> Result<serde_json::Value, String> {
|
||||
info!("MCP tool call: {}", name);
|
||||
match name {
|
||||
"list_clothing" => {
|
||||
let user_id = args["user_id"].as_str().ok_or("missing user_id")?;
|
||||
let category = args["category"].as_str();
|
||||
let items = self.db.list_clothing(user_id, category).map_err(|e| e.to_string())?;
|
||||
Ok(serde_json::to_value(items).unwrap())
|
||||
}
|
||||
"get_clothing" => {
|
||||
let id = args["id"].as_str().ok_or("missing id")?;
|
||||
match self.db.get_clothing(id).map_err(|e| e.to_string())? {
|
||||
Some(item) => Ok(serde_json::to_value(item).unwrap()),
|
||||
None => Ok(serde_json::json!({"error": "衣物不存在"})),
|
||||
}
|
||||
}
|
||||
"add_clothing" => {
|
||||
let user_id = args["user_id"].as_str().ok_or("missing user_id")?;
|
||||
let name = args["name"].as_str().ok_or("missing name")?;
|
||||
let category = args["category"].as_str().ok_or("missing category")?;
|
||||
let color = args["color"].as_str().ok_or("missing color")?;
|
||||
let brand = args["brand"].as_str();
|
||||
let seasons: Vec<String> = args["seasons"].as_array()
|
||||
.map(|a| a.iter().filter_map(|v| v.as_str().map(String::from)).collect())
|
||||
.unwrap_or_default();
|
||||
let occasions: Vec<String> = args["occasions"].as_array()
|
||||
.map(|a| a.iter().filter_map(|v| v.as_str().map(String::from)).collect())
|
||||
.unwrap_or_default();
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
let item = ClothingItem {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
user_id: user_id.to_string(),
|
||||
name: name.to_string(),
|
||||
category: category.to_string(),
|
||||
color: color.to_string(),
|
||||
brand: brand.map(String::from),
|
||||
material: None,
|
||||
seasons,
|
||||
occasions,
|
||||
formality: args["formality"].as_i64().unwrap_or(50) as i32,
|
||||
thickness: args["thickness"].as_i64().unwrap_or(50) as i32,
|
||||
image_path: None,
|
||||
purchase_date: None,
|
||||
purchase_price: None,
|
||||
last_cleaned: None,
|
||||
clean_method: None,
|
||||
wear_count: 0,
|
||||
last_worn: None,
|
||||
is_favorite: false,
|
||||
created_at: now.clone(),
|
||||
updated_at: now,
|
||||
};
|
||||
self.db.create_clothing(&item).map_err(|e| e.to_string())?;
|
||||
Ok(serde_json::to_value(item).unwrap())
|
||||
}
|
||||
"update_clothing" => {
|
||||
let id = args["id"].as_str().ok_or("missing id")?;
|
||||
let name = args["name"].as_str().unwrap_or("");
|
||||
let category = args["category"].as_str().unwrap_or("");
|
||||
let color = args["color"].as_str().unwrap_or("");
|
||||
self.db.update_clothing(id, name, category, color).map_err(|e| e.to_string())?;
|
||||
Ok(serde_json::json!({"success": true}))
|
||||
}
|
||||
"delete_clothing" => {
|
||||
let id = args["id"].as_str().ok_or("missing id")?;
|
||||
self.db.delete_clothing(id).map_err(|e| e.to_string())?;
|
||||
Ok(serde_json::json!({"success": true}))
|
||||
}
|
||||
"list_outfits" => {
|
||||
let user_id = args["user_id"].as_str().ok_or("missing user_id")?;
|
||||
let outfits = self.db.list_outfits(user_id).map_err(|e| e.to_string())?;
|
||||
Ok(serde_json::to_value(outfits).unwrap())
|
||||
}
|
||||
"add_outfit" => {
|
||||
let user_id = args["user_id"].as_str().ok_or("missing user_id")?;
|
||||
let name = args["name"].as_str().ok_or("missing name")?;
|
||||
let item_ids: Vec<String> = args["item_ids"].as_array()
|
||||
.ok_or("missing item_ids")?
|
||||
.iter()
|
||||
.filter_map(|v| v.as_str().map(String::from))
|
||||
.collect();
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
let outfit = Outfit {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
user_id: user_id.to_string(),
|
||||
name: name.to_string(),
|
||||
item_ids,
|
||||
occasion: args["occasion"].as_str().map(String::from),
|
||||
season: args["season"].as_str().map(String::from),
|
||||
match_score: None,
|
||||
ai_analysis: None,
|
||||
is_saved: false,
|
||||
created_at: now,
|
||||
};
|
||||
self.db.create_outfit(&outfit).map_err(|e| e.to_string())?;
|
||||
Ok(serde_json::to_value(outfit).unwrap())
|
||||
}
|
||||
"get_wardrobe_stats" => {
|
||||
let user_id = args["user_id"].as_str().ok_or("missing user_id")?;
|
||||
let stats = self.db.get_stats(user_id).map_err(|e| e.to_string())?;
|
||||
Ok(serde_json::to_value(stats).unwrap())
|
||||
}
|
||||
"record_daily_outfit" => {
|
||||
let user_id = args["user_id"].as_str().ok_or("missing user_id")?;
|
||||
let date = args["date"].as_str().ok_or("missing date")?;
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
let record = DailyRecord {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
user_id: user_id.to_string(),
|
||||
date: date.to_string(),
|
||||
outfit_id: args["outfit_id"].as_str().map(String::from),
|
||||
weather_temp: args["weather_temp"].as_f64(),
|
||||
weather_condition: args["weather_condition"].as_str().map(String::from),
|
||||
notes: args["notes"].as_str().map(String::from),
|
||||
created_at: now,
|
||||
};
|
||||
self.db.upsert_daily_record(&record).map_err(|e| e.to_string())?;
|
||||
Ok(serde_json::to_value(record).unwrap())
|
||||
}
|
||||
"list_daily_records" => {
|
||||
let user_id = args["user_id"].as_str().ok_or("missing user_id")?;
|
||||
let limit = args["limit"].as_i64().unwrap_or(30) as i32;
|
||||
let records = self.db.list_daily_records(user_id, limit).map_err(|e| e.to_string())?;
|
||||
Ok(serde_json::to_value(records).unwrap())
|
||||
}
|
||||
_ => Err(format!("Unknown tool: {}", name)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
pub struct McpTool {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub input_schema: String,
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ClothingItem {
|
||||
pub id: String,
|
||||
pub user_id: String,
|
||||
pub name: String,
|
||||
pub category: String,
|
||||
pub color: String,
|
||||
pub brand: Option<String>,
|
||||
pub material: Option<String>,
|
||||
pub seasons: Vec<String>,
|
||||
pub occasions: Vec<String>,
|
||||
pub formality: i32,
|
||||
pub thickness: i32,
|
||||
pub image_path: Option<String>,
|
||||
pub purchase_date: Option<String>,
|
||||
pub purchase_price: Option<f64>,
|
||||
pub last_cleaned: Option<String>,
|
||||
pub clean_method: Option<String>,
|
||||
pub wear_count: i32,
|
||||
pub last_worn: Option<String>,
|
||||
pub is_favorite: bool,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Outfit {
|
||||
pub id: String,
|
||||
pub user_id: String,
|
||||
pub name: String,
|
||||
pub item_ids: Vec<String>,
|
||||
pub occasion: Option<String>,
|
||||
pub season: Option<String>,
|
||||
pub match_score: Option<i32>,
|
||||
pub ai_analysis: Option<String>,
|
||||
pub is_saved: bool,
|
||||
pub created_at: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DailyRecord {
|
||||
pub id: String,
|
||||
pub user_id: String,
|
||||
pub date: String,
|
||||
pub outfit_id: Option<String>,
|
||||
pub weather_temp: Option<f64>,
|
||||
pub weather_condition: Option<String>,
|
||||
pub notes: Option<String>,
|
||||
pub created_at: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct User {
|
||||
pub id: String,
|
||||
pub email: Option<String>,
|
||||
pub phone: Option<String>,
|
||||
pub nickname: String,
|
||||
pub avatar_path: Option<String>,
|
||||
pub created_at: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct VerificationCode {
|
||||
pub target: String,
|
||||
pub code: String,
|
||||
pub created_at: i64,
|
||||
pub expires_at: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AuthResult {
|
||||
pub success: bool,
|
||||
pub message: String,
|
||||
pub user: Option<User>,
|
||||
pub token: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct WardrobeStats {
|
||||
pub total_items: i32,
|
||||
pub total_outfits: i32,
|
||||
pub utilization_rate: f64,
|
||||
pub category_counts: std::collections::HashMap<String, i32>,
|
||||
pub favorite_count: i32,
|
||||
pub unworn_30d: i32,
|
||||
pub needs_cleaning: i32,
|
||||
}
|
||||
Reference in New Issue
Block a user