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