use axum::extract::{FromRef, Path, Query, State}; use axum::response::Json; use axum::Extension; use chrono::Utc; use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder, Set, prelude::Decimal}; use uuid::Uuid; use erp_core::error::AppError; use erp_core::rbac::require_permission; use erp_core::types::{ApiResponse, PaginatedResponse, TenantContext}; use crate::data_dto::{ MarketEntryDetailResp, MarketEntryResp, MarketListParams, MarketReviewResp, SubmitReviewReq, }; use crate::entity::{market_entry, market_review, plugin}; use crate::state::PluginState; fn entry_to_resp(model: &market_entry::Model) -> MarketEntryResp { MarketEntryResp { id: model.id.to_string(), plugin_id: model.plugin_id.clone(), name: model.name.clone(), version: model.version.clone(), description: model.description.clone(), author: model.author.clone(), category: model.category.clone(), tags: model.tags.clone(), icon_url: model.icon_url.clone(), screenshots: model.screenshots.clone(), min_platform_version: model.min_platform_version.clone(), status: model.status.clone(), download_count: model.download_count, rating_avg: model.rating_avg.to_string().parse().unwrap_or(0.0), rating_count: model.rating_count, changelog: model.changelog.clone(), created_at: Some(model.created_at), updated_at: Some(model.updated_at), } } #[utoipa::path( get, path = "/api/v1/market/entries", params(MarketListParams), responses( (status = 200, description = "市场条目列表", body = ApiResponse>) ), tag = "Plugin Market", )] pub async fn list_market_entries( State(_state): State, Query(params): Query, Extension(ctx): Extension, ) -> Result>>, AppError> where PluginState: FromRef, S: Clone + Send + Sync + 'static, { require_permission(&ctx, "plugin.admin")?; let state: PluginState = PluginState::from_ref(&_state); let db = &state.db; let page = params.page.unwrap_or(1); let page_size = params.page_size.unwrap_or(20).min(100); let mut query = market_entry::Entity::find() .filter(market_entry::Column::Status.eq("published")); if let Some(ref category) = params.category { query = query.filter(market_entry::Column::Category.eq(category.as_str())); } if let Some(ref search) = params.search { query = query.filter( sea_orm::Condition::any() .add(market_entry::Column::Name.contains(search.as_str())) .add(market_entry::Column::Description.contains(search.as_str())) .add(market_entry::Column::Author.contains(search.as_str())), ); } query = query.order_by_desc(market_entry::Column::DownloadCount); let total = query.clone().count(db).await.map_err(|e| AppError::Internal(e.to_string()))?; let total_pages = ((total as f64) / (page_size as f64)).ceil() as u64; let models = query .paginate(db, page_size) .fetch_page(page.saturating_sub(1)) .await .map_err(|e| AppError::Internal(e.to_string()))?; let items = models.iter().map(entry_to_resp).collect(); Ok(Json(ApiResponse::ok(PaginatedResponse { data: items, total, page, page_size, total_pages, }))) } #[utoipa::path( get, path = "/api/v1/market/entries/{id}", responses( (status = 200, description = "市场条目详情", body = ApiResponse) ), tag = "Plugin Market", )] pub async fn get_market_entry( State(_state): State, Path(id): Path, Extension(ctx): Extension, ) -> Result>, AppError> where PluginState: FromRef, S: Clone + Send + Sync + 'static, { require_permission(&ctx, "plugin.admin")?; let state: PluginState = PluginState::from_ref(&_state); let db = &state.db; let model = market_entry::Entity::find_by_id(id) .one(db) .await .map_err(|e| AppError::Internal(e.to_string()))? .ok_or_else(|| AppError::NotFound("市场条目不存在".to_string()))?; // 解析 manifest 检查依赖 let mut dependency_warnings = Vec::new(); if let Ok(manifest) = crate::manifest::parse_manifest(&model.manifest_toml) { for dep_id in &manifest.metadata.dependencies { let installed = plugin::Entity::find() .filter(plugin::Column::Name.eq(dep_id.as_str())) .one(db) .await .map_err(|e| AppError::Internal(e.to_string()))?; if installed.is_none() { dependency_warnings.push(format!("依赖插件 '{}' 尚未安装", dep_id)); } } } Ok(Json(ApiResponse::ok(MarketEntryDetailResp { entry: entry_to_resp(&model), dependency_warnings, }))) } #[utoipa::path( post, path = "/api/v1/market/entries/{id}/install", responses( (status = 200, description = "从市场安装插件", body = ApiResponse) ), tag = "Plugin Market", )] pub async fn install_from_market( State(_state): State, Path(id): Path, Extension(ctx): Extension, ) -> Result>, AppError> where PluginState: FromRef, S: Clone + Send + Sync + 'static, { require_permission(&ctx, "plugin.admin")?; let state: PluginState = PluginState::from_ref(&_state); let db = &state.db; let engine = &state.engine; // 获取市场条目 let market_model = market_entry::Entity::find_by_id(id) .one(db) .await .map_err(|e| AppError::Internal(e.to_string()))? .ok_or_else(|| AppError::NotFound("市场条目不存在".to_string()))?; if market_model.status != "published" { return Err(AppError::Validation("该插件已下架,无法安装".to_string())); } // 检查是否已安装同 plugin_id 的插件 let existing = plugin::Entity::find() .filter(plugin::Column::Name.eq(market_model.plugin_id.as_str())) .filter(plugin::Column::TenantId.eq(ctx.tenant_id)) .one(db) .await .map_err(|e| AppError::Internal(e.to_string()))?; if existing.is_some() { return Err(AppError::Validation("该插件已安装,如需更新请使用升级功能".to_string())); } // upload → install → enable 一条龙 let wasm_binary = market_model.wasm_binary.clone(); let manifest_toml = market_model.manifest_toml.clone(); let plugin_resp = crate::service::PluginService::upload( ctx.tenant_id, ctx.user_id, wasm_binary, &manifest_toml, db, ).await?; let plugin_id = plugin_resp.id; let plugin_resp = crate::service::PluginService::install( plugin_id, ctx.tenant_id, ctx.user_id, db, engine, ).await?; let plugin_resp = crate::service::PluginService::enable( plugin_id, ctx.tenant_id, ctx.user_id, db, engine, ).await?; // 递增下载计数 let mut active: market_entry::ActiveModel = market_model.into(); let current = active.download_count.take().unwrap_or(0); active.download_count = Set(current + 1); let _ = active.update(db).await.map_err(|e| AppError::Internal(e.to_string()))?; Ok(Json(ApiResponse::ok(plugin_resp))) } #[utoipa::path( get, path = "/api/v1/market/entries/{id}/reviews", responses( (status = 200, description = "评论列表", body = ApiResponse>) ), tag = "Plugin Market", )] pub async fn list_market_reviews( State(_state): State, Path(id): Path, Extension(ctx): Extension, ) -> Result>>, AppError> where PluginState: FromRef, S: Clone + Send + Sync + 'static, { require_permission(&ctx, "plugin.admin")?; let state: PluginState = PluginState::from_ref(&_state); let db = &state.db; let reviews = market_review::Entity::find() .filter(market_review::Column::MarketEntryId.eq(id)) .all(db) .await .map_err(|e| AppError::Internal(e.to_string()))?; let items = reviews.iter().map(|r| MarketReviewResp { id: r.id.to_string(), user_id: r.user_id.to_string(), market_entry_id: r.market_entry_id.to_string(), rating: r.rating, review_text: r.review_text.clone(), created_at: Some(r.created_at), }).collect(); Ok(Json(ApiResponse::ok(items))) } #[utoipa::path( post, path = "/api/v1/market/entries/{id}/reviews", responses( (status = 200, description = "提交评分/评论", body = ApiResponse) ), tag = "Plugin Market", )] pub async fn submit_market_review( State(_state): State, Path(id): Path, Extension(ctx): Extension, Json(body): Json, ) -> Result>, AppError> where PluginState: FromRef, S: Clone + Send + Sync + 'static, { require_permission(&ctx, "plugin.admin")?; if body.rating < 1 || body.rating > 5 { return Err(AppError::Validation("评分必须在 1-5 之间".to_string())); } let state: PluginState = PluginState::from_ref(&_state); let db = &state.db; // 验证市场条目存在 let entry_model = market_entry::Entity::find_by_id(id) .one(db) .await .map_err(|e| AppError::Internal(e.to_string()))? .ok_or_else(|| AppError::NotFound("市场条目不存在".to_string()))?; // upsert: 同一用户同一条目只保留最新评论 let existing = market_review::Entity::find() .filter(market_review::Column::MarketEntryId.eq(id)) .filter(market_review::Column::UserId.eq(ctx.user_id)) .filter(market_review::Column::TenantId.eq(ctx.tenant_id)) .one(db) .await .map_err(|e| AppError::Internal(e.to_string()))?; let review_model = if let Some(existing) = existing { let mut active: market_review::ActiveModel = existing.into(); active.rating = Set(body.rating); active.review_text = Set(body.review_text); active.update(db).await.map_err(|e| AppError::Internal(e.to_string()))? } else { let review_id = Uuid::now_v7(); let now = Utc::now(); let model = market_review::ActiveModel { id: Set(review_id), tenant_id: Set(ctx.tenant_id), user_id: Set(ctx.user_id), market_entry_id: Set(id), rating: Set(body.rating), review_text: Set(body.review_text), created_at: Set(now), }; model.insert(db).await.map_err(|e| AppError::Internal(e.to_string()))? }; // 重新计算平均评分 let all_reviews = market_review::Entity::find() .filter(market_review::Column::MarketEntryId.eq(id)) .all(db) .await .map_err(|e| AppError::Internal(e.to_string()))?; let count = all_reviews.len() as i32; let avg: f64 = if count > 0 { all_reviews.iter().map(|r| r.rating as f64).sum::() / count as f64 } else { 0.0 }; let mut entry_active: market_entry::ActiveModel = entry_model.into(); let avg_decimal = Decimal::from_f64_retain(avg).unwrap_or_default(); entry_active.rating_avg = Set(avg_decimal); entry_active.rating_count = Set(count); let _ = entry_active.update(db).await.map_err(|e| AppError::Internal(e.to_string()))?; Ok(Json(ApiResponse::ok(MarketReviewResp { id: review_model.id.to_string(), user_id: review_model.user_id.to_string(), market_entry_id: review_model.market_entry_id.to_string(), rating: review_model.rating, review_text: review_model.review_text, created_at: Some(review_model.created_at), }))) }