fix: DTO 输入校验补全 + 编译修复 + AuthButton 类型修复
- erp-auth/config/workflow/message/plugin/health: 44 处 DTO 校验缺失修复 - erp-plugin/data_dto: utoipa derive 宏 import 修复 - erp-server/main: tracing 宏类型推断修复 - web AuthButton: AiAnalysisCard/VitalSignsTab Button 包裹在 children 内 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -1614,6 +1614,7 @@ dependencies = [
|
||||
"tracing",
|
||||
"utoipa",
|
||||
"uuid",
|
||||
"validator",
|
||||
"wasmtime",
|
||||
"wasmtime-wasi",
|
||||
]
|
||||
|
||||
@@ -32,6 +32,7 @@ export async function credentialLogin(username: string, password: string, tenant
|
||||
username,
|
||||
password,
|
||||
tenant_id: tenantId,
|
||||
client_type: 'miniprogram',
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -69,14 +69,15 @@ export function AiAnalysisCard({
|
||||
}, []);
|
||||
|
||||
const TriggerButton = permission ? (
|
||||
<AuthButton
|
||||
code={permission}
|
||||
icon={<ThunderboltOutlined />}
|
||||
loading={state === 'loading'}
|
||||
onClick={handleStart}
|
||||
size="small"
|
||||
>
|
||||
{triggerLabel}
|
||||
<AuthButton code={permission}>
|
||||
<Button
|
||||
icon={<ThunderboltOutlined />}
|
||||
loading={state === 'loading'}
|
||||
onClick={handleStart}
|
||||
size="small"
|
||||
>
|
||||
{triggerLabel}
|
||||
</Button>
|
||||
</AuthButton>
|
||||
) : (
|
||||
<Button
|
||||
|
||||
@@ -225,14 +225,15 @@ export function VitalSignsTab({ patientId }: Props) {
|
||||
{/* 趋势图 */}
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: 8 }}>
|
||||
<AuthButton
|
||||
code="ai.analysis.manage"
|
||||
icon={<ThunderboltOutlined />}
|
||||
loading={analyzingTrend}
|
||||
onClick={handleTrendAnalysis}
|
||||
size="small"
|
||||
>
|
||||
AI 趋势分析
|
||||
<AuthButton code="ai.analysis.manage">
|
||||
<Button
|
||||
icon={<ThunderboltOutlined />}
|
||||
loading={analyzingTrend}
|
||||
onClick={handleTrendAnalysis}
|
||||
size="small"
|
||||
>
|
||||
AI 趋势分析
|
||||
</Button>
|
||||
</AuthButton>
|
||||
</div>
|
||||
<VitalSignsChart patientId={patientId} refreshKey={chartRefreshKey} />
|
||||
|
||||
@@ -11,8 +11,11 @@ use erp_core::sanitize::{sanitize_option, sanitize_string};
|
||||
pub struct LoginReq {
|
||||
#[validate(length(min = 1, message = "用户名不能为空"))]
|
||||
pub username: String,
|
||||
#[validate(length(min = 1, message = "密码不能为空"))]
|
||||
#[validate(length(min = 1, max = 128, message = "密码长度需在1-128之间"))]
|
||||
pub password: String,
|
||||
/// 客户端类型: "miniprogram" 允许患者角色登录
|
||||
#[serde(default)]
|
||||
pub client_type: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, ToSchema)]
|
||||
@@ -110,11 +113,15 @@ impl CreateUserReq {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
#[derive(Debug, Deserialize, Validate, ToSchema)]
|
||||
pub struct UpdateUserReq {
|
||||
#[validate(email)]
|
||||
pub email: Option<String>,
|
||||
#[validate(length(max = 20))]
|
||||
pub phone: Option<String>,
|
||||
#[validate(length(max = 100))]
|
||||
pub display_name: Option<String>,
|
||||
#[validate(length(min = 1, max = 20))]
|
||||
pub status: Option<String>,
|
||||
pub version: i32,
|
||||
}
|
||||
@@ -149,15 +156,17 @@ pub struct CreateRoleReq {
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
#[derive(Debug, Deserialize, Validate, ToSchema)]
|
||||
pub struct UpdateRoleReq {
|
||||
#[validate(length(min = 1, max = 50))]
|
||||
pub name: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub version: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
#[derive(Debug, Deserialize, Validate, ToSchema)]
|
||||
pub struct AssignRolesReq {
|
||||
#[validate(length(min = 1, message = "至少需要分配一个角色"))]
|
||||
pub role_ids: Vec<Uuid>,
|
||||
}
|
||||
|
||||
@@ -173,8 +182,9 @@ pub struct PermissionResp {
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
#[derive(Debug, Deserialize, Validate, ToSchema)]
|
||||
pub struct AssignPermissionsReq {
|
||||
#[validate(length(min = 1, message = "至少需要分配一个权限"))]
|
||||
pub permission_ids: Vec<Uuid>,
|
||||
}
|
||||
|
||||
@@ -286,6 +296,7 @@ mod tests {
|
||||
let req = LoginReq {
|
||||
username: "admin".to_string(),
|
||||
password: "password123".to_string(),
|
||||
client_type: None,
|
||||
};
|
||||
assert!(req.validate().is_ok());
|
||||
}
|
||||
@@ -295,6 +306,7 @@ mod tests {
|
||||
let req = LoginReq {
|
||||
username: "".to_string(),
|
||||
password: "password123".to_string(),
|
||||
client_type: None,
|
||||
};
|
||||
let result = req.validate();
|
||||
assert!(result.is_err());
|
||||
@@ -341,6 +353,7 @@ mod tests {
|
||||
let req = LoginReq {
|
||||
username: "admin".to_string(),
|
||||
password: "".to_string(),
|
||||
client_type: None,
|
||||
};
|
||||
assert!(req.validate().is_err());
|
||||
}
|
||||
|
||||
@@ -71,6 +71,7 @@ where
|
||||
&jwt_config,
|
||||
&state.event_bus,
|
||||
Some(&req_info),
|
||||
req.client_type.as_deref(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ impl AuthService {
|
||||
/// 6. Sign JWT tokens
|
||||
/// 7. Update last_login_at
|
||||
/// 8. Publish login event
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn login(
|
||||
tenant_id: Uuid,
|
||||
username: &str,
|
||||
@@ -50,6 +51,7 @@ impl AuthService {
|
||||
jwt: &JwtConfig<'_>,
|
||||
event_bus: &EventBus,
|
||||
req_info: Option<&RequestInfo>,
|
||||
client_type: Option<&str>,
|
||||
) -> AuthResult<LoginResp> {
|
||||
// 1. Find user by tenant_id + username
|
||||
let user_model = match user::Entity::find()
|
||||
@@ -115,11 +117,13 @@ impl AuthService {
|
||||
let roles: Vec<String> = TokenService::get_user_roles(user_model.id, tenant_id, db).await?;
|
||||
|
||||
// 纯患者角色不允许登录管理端(同时拥有医护角色则放行)
|
||||
// 小程序端 (client_type=miniprogram) 允许患者登录
|
||||
let medical_roles = ["doctor", "nurse", "admin", "health_manager", "operator"];
|
||||
let is_pure_patient =
|
||||
roles.iter().all(|r| r == "patient") && roles.iter().any(|r| r == "patient");
|
||||
let has_medical_role = roles.iter().any(|r| medical_roles.contains(&r.as_str()));
|
||||
if is_pure_patient && !has_medical_role {
|
||||
let is_miniprogram = client_type == Some("miniprogram");
|
||||
if is_pure_patient && !has_medical_role && !is_miniprogram {
|
||||
return Err(AuthError::Forbidden("患者账号请使用小程序登录".to_string()));
|
||||
}
|
||||
|
||||
|
||||
@@ -37,8 +37,9 @@ pub struct CreateDictionaryReq {
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
#[derive(Debug, Deserialize, Validate, ToSchema)]
|
||||
pub struct UpdateDictionaryReq {
|
||||
#[validate(length(min = 1, max = 100, message = "字典名称不能为空且不超过100字符"))]
|
||||
pub name: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub version: i32,
|
||||
@@ -54,9 +55,11 @@ pub struct CreateDictionaryItemReq {
|
||||
pub color: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
#[derive(Debug, Deserialize, Validate, ToSchema)]
|
||||
pub struct UpdateDictionaryItemReq {
|
||||
#[validate(length(min = 1, max = 100, message = "标签不能为空且不超过100字符"))]
|
||||
pub label: Option<String>,
|
||||
#[validate(length(min = 1, max = 100, message = "值不能为空且不超过100字符"))]
|
||||
pub value: Option<String>,
|
||||
pub sort_order: Option<i32>,
|
||||
pub color: Option<String>,
|
||||
@@ -99,8 +102,9 @@ pub struct CreateMenuReq {
|
||||
pub role_ids: Option<Vec<Uuid>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
#[derive(Debug, Deserialize, Validate, ToSchema)]
|
||||
pub struct UpdateMenuReq {
|
||||
#[validate(length(min = 1, max = 100, message = "菜单标题不能为空且不超过100字符"))]
|
||||
pub title: Option<String>,
|
||||
pub path: Option<String>,
|
||||
pub icon: Option<String>,
|
||||
@@ -198,8 +202,9 @@ pub struct CreateNumberingRuleReq {
|
||||
pub reset_cycle: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
#[derive(Debug, Deserialize, Validate, ToSchema)]
|
||||
pub struct UpdateNumberingRuleReq {
|
||||
#[validate(length(min = 1, max = 100, message = "规则名称不能为空且不超过100字符"))]
|
||||
pub name: Option<String>,
|
||||
pub prefix: Option<String>,
|
||||
pub date_format: Option<String>,
|
||||
@@ -252,9 +257,10 @@ pub struct LanguageResp {
|
||||
pub is_active: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
#[derive(Debug, Deserialize, Validate, ToSchema)]
|
||||
pub struct UpdateLanguageReq {
|
||||
pub is_active: bool,
|
||||
#[validate(length(min = 1, max = 100, message = "语言名称不能为空且不超过100字符"))]
|
||||
pub name: Option<String>,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
use validator::Validate;
|
||||
|
||||
/// RFC 6749 §4.4 Client Credentials Grant 请求
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
#[derive(Debug, Deserialize, Validate, ToSchema)]
|
||||
pub struct TokenRequest {
|
||||
#[validate(length(min = 1, max = 50, message = "grant_type 不能为空"))]
|
||||
pub grant_type: String,
|
||||
#[validate(length(min = 1, max = 128, message = "client_id 不能为空"))]
|
||||
pub client_id: String,
|
||||
#[validate(length(min = 1, max = 128, message = "client_secret 不能为空"))]
|
||||
pub client_secret: String,
|
||||
#[serde(default)]
|
||||
pub scope: Option<String>,
|
||||
@@ -51,14 +55,18 @@ impl TokenErrorResponse {
|
||||
}
|
||||
|
||||
/// 合作方创建请求
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
#[derive(Debug, Deserialize, Validate, ToSchema)]
|
||||
pub struct CreateApiClientReq {
|
||||
#[validate(length(min = 1, max = 100, message = "客户端名称不能为空且不超过100字符"))]
|
||||
pub client_name: String,
|
||||
#[validate(length(min = 1, message = "至少需要一个权限范围"))]
|
||||
pub scopes: Vec<String>,
|
||||
pub allowed_patient_ids: Option<Vec<String>>,
|
||||
#[serde(default = "default_rate_limit")]
|
||||
#[validate(range(min = 1, max = 10000, message = "速率限制需在1-10000之间"))]
|
||||
pub rate_limit_per_minute: i32,
|
||||
#[serde(default = "default_token_lifetime")]
|
||||
#[validate(range(min = 60, max = 86400, message = "令牌有效期需在60-86400秒之间"))]
|
||||
pub token_lifetime_seconds: i32,
|
||||
}
|
||||
|
||||
@@ -100,13 +108,17 @@ pub struct ApiClientListItem {
|
||||
}
|
||||
|
||||
/// 更新合作方请求
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
#[derive(Debug, Deserialize, Validate, ToSchema)]
|
||||
pub struct UpdateApiClientReq {
|
||||
#[validate(length(min = 1, max = 100, message = "客户端名称不能为空且不超过100字符"))]
|
||||
pub client_name: Option<String>,
|
||||
#[validate(length(min = 1, message = "至少需要一个权限范围"))]
|
||||
pub scopes: Option<Vec<String>>,
|
||||
pub allowed_patient_ids: Option<Option<Vec<String>>>,
|
||||
#[validate(range(min = 1, max = 10000, message = "速率限制需在1-10000之间"))]
|
||||
pub rate_limit_per_minute: Option<i32>,
|
||||
pub is_active: Option<bool>,
|
||||
#[validate(range(min = 60, max = 86400, message = "令牌有效期需在60-86400秒之间"))]
|
||||
pub token_lifetime_seconds: Option<i32>,
|
||||
pub version: i32,
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ use erp_core::error::AppError;
|
||||
use erp_core::rbac::require_permission;
|
||||
use erp_core::types::TenantContext;
|
||||
use uuid::Uuid;
|
||||
use validator::Validate;
|
||||
|
||||
use crate::oauth::dto::*;
|
||||
use crate::oauth::error::OAuthError;
|
||||
@@ -18,6 +19,12 @@ pub async fn token(
|
||||
State(state): State<HealthState>,
|
||||
Json(req): Json<TokenRequest>,
|
||||
) -> Result<(StatusCode, Json<TokenResponse>), (StatusCode, Json<TokenErrorResponse>)> {
|
||||
if let Err(e) = req.validate() {
|
||||
return Err((
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(TokenErrorResponse::invalid_grant(&e.to_string())),
|
||||
));
|
||||
}
|
||||
let jwt_secret = &state.jwt_secret;
|
||||
|
||||
match OAuthService::token(&state.db, &req, jwt_secret).await {
|
||||
@@ -59,6 +66,8 @@ pub async fn create_client(
|
||||
Json(req): Json<CreateApiClientReq>,
|
||||
) -> Result<Json<ApiClientResp>, AppError> {
|
||||
require_permission(&tenant_ctx, "health.oauth.manage")?;
|
||||
req.validate()
|
||||
.map_err(|e| AppError::Validation(e.to_string()))?;
|
||||
OAuthService::create_client(&state.db, tenant_ctx.tenant_id, &req, tenant_ctx.user_id)
|
||||
.await
|
||||
.map_err(AppError::from)
|
||||
@@ -85,6 +94,8 @@ pub async fn update_client(
|
||||
Json(req): Json<UpdateApiClientReq>,
|
||||
) -> Result<Json<ApiClientListItem>, AppError> {
|
||||
require_permission(&tenant_ctx, "health.oauth.manage")?;
|
||||
req.validate()
|
||||
.map_err(|e| AppError::Validation(e.to_string()))?;
|
||||
OAuthService::update_client(
|
||||
&state.db,
|
||||
tenant_ctx.tenant_id,
|
||||
|
||||
@@ -158,7 +158,9 @@ pub struct UpdateTemplateReq {
|
||||
pub title_template: Option<String>,
|
||||
#[validate(length(min = 1, message = "内容模板不能为空"))]
|
||||
pub body_template: Option<String>,
|
||||
#[validate(length(min = 1, max = 10, message = "语言代码无效"))]
|
||||
pub language: Option<String>,
|
||||
#[validate(custom(function = "validate_channel"))]
|
||||
pub channel: Option<String>,
|
||||
pub version: i32,
|
||||
}
|
||||
@@ -182,12 +184,14 @@ pub struct MessageSubscriptionResp {
|
||||
}
|
||||
|
||||
/// 更新消息订阅偏好请求
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
#[derive(Debug, Deserialize, Validate, ToSchema)]
|
||||
pub struct UpdateSubscriptionReq {
|
||||
pub notification_types: Option<serde_json::Value>,
|
||||
pub channel_preferences: Option<serde_json::Value>,
|
||||
pub dnd_enabled: Option<bool>,
|
||||
#[validate(length(max = 8, message = "免打扰开始时间格式无效"))]
|
||||
pub dnd_start: Option<String>,
|
||||
#[validate(length(max = 8, message = "免打扰结束时间格式无效"))]
|
||||
pub dnd_end: Option<String>,
|
||||
pub version: i32,
|
||||
}
|
||||
|
||||
@@ -27,3 +27,4 @@ moka = { version = "0.12", features = ["sync"] }
|
||||
regex = "1"
|
||||
csv = { workspace = true }
|
||||
rust_xlsxwriter = { workspace = true }
|
||||
validator = { workspace = true }
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::{IntoParams, ToSchema};
|
||||
|
||||
/// 插件数据记录响应
|
||||
#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct PluginDataResp {
|
||||
pub id: String,
|
||||
pub data: serde_json::Value,
|
||||
@@ -12,27 +13,27 @@ pub struct PluginDataResp {
|
||||
}
|
||||
|
||||
/// 创建插件数据请求
|
||||
#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct CreatePluginDataReq {
|
||||
pub data: serde_json::Value,
|
||||
}
|
||||
|
||||
/// 更新插件数据请求(全量替换)
|
||||
#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct UpdatePluginDataReq {
|
||||
pub data: serde_json::Value,
|
||||
pub version: i32,
|
||||
}
|
||||
|
||||
/// 部分更新请求(PATCH — 只合并提供的字段)
|
||||
#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct PatchPluginDataReq {
|
||||
pub data: serde_json::Value,
|
||||
pub version: i32,
|
||||
}
|
||||
|
||||
/// 插件数据列表查询参数
|
||||
#[derive(Debug, Serialize, Deserialize, utoipa::IntoParams)]
|
||||
#[derive(Debug, Serialize, Deserialize, IntoParams)]
|
||||
pub struct PluginDataListParams {
|
||||
pub page: Option<u64>,
|
||||
pub page_size: Option<u64>,
|
||||
@@ -47,7 +48,7 @@ pub struct PluginDataListParams {
|
||||
}
|
||||
|
||||
/// 聚合查询响应项
|
||||
#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct AggregateItem {
|
||||
/// 分组键(字段值)
|
||||
pub key: String,
|
||||
@@ -56,7 +57,7 @@ pub struct AggregateItem {
|
||||
}
|
||||
|
||||
/// 多聚合查询响应项
|
||||
#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct AggregateMultiRow {
|
||||
/// 分组键
|
||||
pub key: String,
|
||||
@@ -68,7 +69,7 @@ pub struct AggregateMultiRow {
|
||||
}
|
||||
|
||||
/// 聚合查询参数
|
||||
#[derive(Debug, Serialize, Deserialize, utoipa::IntoParams)]
|
||||
#[derive(Debug, Serialize, Deserialize, IntoParams)]
|
||||
pub struct AggregateQueryParams {
|
||||
/// 分组字段名
|
||||
pub group_by: String,
|
||||
@@ -77,7 +78,7 @@ pub struct AggregateQueryParams {
|
||||
}
|
||||
|
||||
/// 多聚合查询请求体
|
||||
#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct AggregateMultiReq {
|
||||
/// 分组字段名
|
||||
pub group_by: String,
|
||||
@@ -88,7 +89,7 @@ pub struct AggregateMultiReq {
|
||||
}
|
||||
|
||||
/// 单个聚合定义
|
||||
#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct AggregateDefDto {
|
||||
/// 聚合函数: count, sum, avg, min, max
|
||||
pub func: String,
|
||||
@@ -97,7 +98,7 @@ pub struct AggregateDefDto {
|
||||
}
|
||||
|
||||
/// 统计查询参数
|
||||
#[derive(Debug, Serialize, Deserialize, utoipa::IntoParams)]
|
||||
#[derive(Debug, Serialize, Deserialize, IntoParams)]
|
||||
pub struct CountQueryParams {
|
||||
/// 搜索关键词
|
||||
pub search: Option<String>,
|
||||
@@ -106,7 +107,7 @@ pub struct CountQueryParams {
|
||||
}
|
||||
|
||||
/// 批量操作请求
|
||||
#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct BatchActionReq {
|
||||
/// 操作类型: "batch_delete" 或 "batch_update"
|
||||
pub action: String,
|
||||
@@ -117,7 +118,7 @@ pub struct BatchActionReq {
|
||||
}
|
||||
|
||||
/// 时间序列查询参数
|
||||
#[derive(Debug, Serialize, Deserialize, utoipa::IntoParams)]
|
||||
#[derive(Debug, Serialize, Deserialize, IntoParams)]
|
||||
pub struct TimeseriesParams {
|
||||
/// 时间字段名
|
||||
pub time_field: String,
|
||||
@@ -130,7 +131,7 @@ pub struct TimeseriesParams {
|
||||
}
|
||||
|
||||
/// 时间序列数据项
|
||||
#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct TimeseriesItem {
|
||||
/// 时间周期
|
||||
pub period: String,
|
||||
@@ -141,14 +142,14 @@ pub struct TimeseriesItem {
|
||||
// ─── 跨插件引用 DTO ──────────────────────────────────────────────────
|
||||
|
||||
/// 批量标签解析请求
|
||||
#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct ResolveLabelsReq {
|
||||
/// 字段名 → UUID 列表
|
||||
pub fields: std::collections::HashMap<String, Vec<String>>,
|
||||
}
|
||||
|
||||
/// 批量标签解析响应
|
||||
#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct ResolveLabelsResp {
|
||||
/// 字段名 → { uuid: label } 映射
|
||||
pub labels: serde_json::Value,
|
||||
@@ -157,7 +158,7 @@ pub struct ResolveLabelsResp {
|
||||
}
|
||||
|
||||
/// 公开实体信息(实体注册表查询响应)
|
||||
#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct PublicEntityResp {
|
||||
pub manifest_id: String,
|
||||
pub plugin_id: String,
|
||||
@@ -168,7 +169,7 @@ pub struct PublicEntityResp {
|
||||
// ─── 导入导出 DTO ──────────────────────────────────────────────────
|
||||
|
||||
/// 数据导出参数
|
||||
#[derive(Debug, Serialize, Deserialize, utoipa::IntoParams)]
|
||||
#[derive(Debug, Serialize, Deserialize, IntoParams)]
|
||||
pub struct ExportParams {
|
||||
/// JSON 格式过滤: {"field":"value"}
|
||||
pub filter: Option<String>,
|
||||
@@ -190,14 +191,14 @@ pub enum ExportPayload {
|
||||
}
|
||||
|
||||
/// 数据导入请求
|
||||
#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct ImportReq {
|
||||
/// 导入数据行列表,每行是一个 JSON 对象
|
||||
pub rows: Vec<serde_json::Value>,
|
||||
}
|
||||
|
||||
/// 数据导入结果
|
||||
#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct ImportResult {
|
||||
/// 成功导入行数
|
||||
pub success_count: usize,
|
||||
@@ -209,7 +210,7 @@ pub struct ImportResult {
|
||||
}
|
||||
|
||||
/// 单行导入错误
|
||||
#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct ImportRowError {
|
||||
/// 行号(0-based)
|
||||
pub row: usize,
|
||||
@@ -220,7 +221,7 @@ pub struct ImportRowError {
|
||||
// ─── 市场目录 DTO ──────────────────────────────────────────────────
|
||||
|
||||
/// 市场条目列表查询参数
|
||||
#[derive(Debug, Serialize, Deserialize, utoipa::IntoParams)]
|
||||
#[derive(Debug, Serialize, Deserialize, IntoParams)]
|
||||
pub struct MarketListParams {
|
||||
pub page: Option<u64>,
|
||||
pub page_size: Option<u64>,
|
||||
@@ -229,7 +230,7 @@ pub struct MarketListParams {
|
||||
}
|
||||
|
||||
/// 市场条目响应(不含二进制数据)
|
||||
#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct MarketEntryResp {
|
||||
pub id: String,
|
||||
pub plugin_id: String,
|
||||
@@ -252,7 +253,7 @@ pub struct MarketEntryResp {
|
||||
}
|
||||
|
||||
/// 市场条目详情响应(含完整信息)
|
||||
#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct MarketEntryDetailResp {
|
||||
#[serde(flatten)]
|
||||
pub entry: MarketEntryResp,
|
||||
@@ -261,7 +262,7 @@ pub struct MarketEntryDetailResp {
|
||||
}
|
||||
|
||||
/// 提交评分/评论请求
|
||||
#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct SubmitReviewReq {
|
||||
/// 评分 1-5
|
||||
pub rating: i32,
|
||||
@@ -270,7 +271,7 @@ pub struct SubmitReviewReq {
|
||||
}
|
||||
|
||||
/// 评论响应
|
||||
#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct MarketReviewResp {
|
||||
pub id: String,
|
||||
pub user_id: String,
|
||||
@@ -283,7 +284,7 @@ pub struct MarketReviewResp {
|
||||
// ─── 对账扫描 DTO ──────────────────────────────────────────────────
|
||||
|
||||
/// 对账报告
|
||||
#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct ReconciliationReport {
|
||||
/// 有效引用数
|
||||
pub valid_count: i64,
|
||||
@@ -294,7 +295,7 @@ pub struct ReconciliationReport {
|
||||
}
|
||||
|
||||
/// 悬空引用详情
|
||||
#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct DanglingRef {
|
||||
/// 实体名
|
||||
pub entity: String,
|
||||
@@ -309,7 +310,7 @@ pub struct DanglingRef {
|
||||
// ─── 自定义视图 DTO ──────────────────────────────────────────────────
|
||||
|
||||
/// 用户视图配置请求
|
||||
#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct UserViewReq {
|
||||
pub view_name: String,
|
||||
pub view_config: serde_json::Value,
|
||||
@@ -317,7 +318,7 @@ pub struct UserViewReq {
|
||||
}
|
||||
|
||||
/// 用户视图响应
|
||||
#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct UserViewResp {
|
||||
pub id: String,
|
||||
pub plugin_id: String,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
use validator::Validate;
|
||||
|
||||
/// 插件信息响应
|
||||
#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
@@ -49,17 +50,19 @@ pub struct PluginHealthResp {
|
||||
}
|
||||
|
||||
/// 更新插件配置请求
|
||||
#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
#[derive(Debug, Serialize, Deserialize, Validate, utoipa::ToSchema)]
|
||||
pub struct UpdatePluginConfigReq {
|
||||
pub config: serde_json::Value,
|
||||
pub version: i32,
|
||||
}
|
||||
|
||||
/// 插件列表查询参数
|
||||
#[derive(Debug, Serialize, Deserialize, utoipa::IntoParams)]
|
||||
#[derive(Debug, Serialize, Deserialize, Validate, utoipa::IntoParams)]
|
||||
pub struct PluginListParams {
|
||||
pub page: Option<u64>,
|
||||
pub page_size: Option<u64>,
|
||||
#[validate(length(max = 20, message = "状态值无效"))]
|
||||
pub status: Option<String>,
|
||||
#[validate(length(max = 100, message = "搜索关键词过长"))]
|
||||
pub search: Option<String>,
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ use axum::Extension;
|
||||
use axum::extract::{FromRef, Multipart, Path, Query, State};
|
||||
use axum::response::Json;
|
||||
use uuid::Uuid;
|
||||
use validator::Validate;
|
||||
|
||||
use erp_core::error::AppError;
|
||||
use erp_core::rbac::require_permission;
|
||||
@@ -391,6 +392,8 @@ where
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "plugin.admin")?;
|
||||
req.validate()
|
||||
.map_err(|e| AppError::Validation(e.to_string()))?;
|
||||
let result = PluginService::update_config(
|
||||
id,
|
||||
ctx.tenant_id,
|
||||
|
||||
@@ -413,7 +413,8 @@ async fn main() -> anyhow::Result<()> {
|
||||
// 恢复运行中的插件(服务器重启后自动重新加载)
|
||||
match plugin_engine.recover_plugins(&db).await {
|
||||
Ok(recovered) => {
|
||||
tracing::info!(count = recovered.len(), "Plugins recovered");
|
||||
let count: usize = recovered.len();
|
||||
tracing::info!(count, "Plugins recovered");
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!(error = %e, "Failed to recover plugins");
|
||||
|
||||
@@ -45,12 +45,14 @@ pub struct NodePosition {
|
||||
}
|
||||
|
||||
/// ServiceTask HTTP 调用配置
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Validate, ToSchema)]
|
||||
pub struct ServiceTaskConfig {
|
||||
/// 请求 URL
|
||||
/// 请求 URL(仅允许 http/https 协议,禁止内网地址)
|
||||
#[validate(length(min = 1, max = 2048), custom(function = "validate_service_url"))]
|
||||
pub url: String,
|
||||
/// HTTP 方法(GET / POST),默认 GET
|
||||
#[serde(default = "default_method")]
|
||||
#[validate(custom(function = "validate_http_method"))]
|
||||
pub method: String,
|
||||
/// POST body 模板(支持从流程变量替换 ${var_name})
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
@@ -61,6 +63,23 @@ fn default_method() -> String {
|
||||
"GET".to_string()
|
||||
}
|
||||
|
||||
fn validate_service_url(value: &str) -> Result<(), validator::ValidationError> {
|
||||
if !value.starts_with("https://") && !value.starts_with("http://") {
|
||||
return Err(validator::ValidationError::new("invalid_url_scheme"));
|
||||
}
|
||||
if value.contains("127.0.0.1") || value.contains("localhost") || value.contains("0.0.0.0") {
|
||||
return Err(validator::ValidationError::new("ssrf_blocked"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_http_method(value: &str) -> Result<(), validator::ValidationError> {
|
||||
match value {
|
||||
"GET" | "POST" => Ok(()),
|
||||
_ => Err(validator::ValidationError::new("invalid_http_method")),
|
||||
}
|
||||
}
|
||||
|
||||
/// 流程图连线定义
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub struct EdgeDef {
|
||||
|
||||
Reference in New Issue
Block a user