fix: 全系统审计问题修复 — 安全/数据完整性/功能缺陷/UX (Phase 1-5)
Some checks failed
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
CI / frontend-build (push) Has been cancelled
CI / security-audit (push) Has been cancelled

Phase 1 安全热修复:
- P0-1: /uploads 文件服务添加 JWT 认证中间件(支持 header + query param)
- P0-2: analytics/batch 路由从 public 移到 protected_routes
- P0-3: plugin engine SQL 注入修复(format! → 参数化查询)
- P0-new: stats_service compute_avg_field 字段白名单 + FLOAT8 类型转换

Phase 2 数据完整性:
- P0-4: 组织删除级联检查(添加部门存在性校验)
- P0-5: 部门删除级联检查(添加岗位 + 用户存在性校验)
- P0-8: workflow on_tenant_deleted 实现 5 实体批量删除
- P0-7: 并行网关 race condition 修复(consumed → completed 原子转换)

Phase 3 P1 后端 Bug:
- P1-12: plugin host 表名消毒(使用 sanitize_identifier)
- P1-10: workflow deprecated 状态转换(published → deprecated)
- P1-11: workflow 更新验证条件(nodes/edges 任一变化即验证)
- P0-9: 小程序 .gitignore 添加 .env/.env.*/日志
- P1-19: 小程序加密密钥替换为 64 字符强密钥

Phase 4 消息模块:
- P1-5: 通知偏好 GET 路由 + handler
- P1-4: 消息模板 update/delete CRUD + version
- P2-8: mark_all_read SQL 添加 version + 1
- P2-7: markAsRead 改为乐观更新 + 失败回滚

Phase 5 前端修复:
- P2-9: 通知面板点击导航到 /messages
- P2-1: 随访任务患者名批量 ID 解析(替代 UUID 显示)
- P2-5: AppointmentList 分离 patient_id/doctor_id 分别调用 API
- P2-17: PluginMarket installed 字段修正(name → id)
- P3-3: 路由标题 fallback 改为模式匹配(支持 :id 动态路径)
- P2-15: workflow updateDefinition 添加 version 字段
- P3-9: Kanban 版本使用记录实际 version
- P2-21: secure-storage 生产环境无密钥时阻止存储
- P3-11: destroyOnHidden → destroyOnClose
- P3-13: PendingTasks 深色模式 Tag 颜色适配

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
iven
2026-04-26 19:16:23 +08:00
parent a19b097409
commit 83fe89cbcd
33 changed files with 1238 additions and 70 deletions

View File

@@ -9,6 +9,29 @@ use crate::dto::UpdateSubscriptionReq;
use crate::message_state::MessageState;
use crate::service::subscription_service::SubscriptionService;
#[utoipa::path(
get,
path = "/api/v1/message-subscriptions",
responses(
(status = 200, description = "成功", body = ApiResponse<crate::dto::MessageSubscriptionResp>),
(status = 401, description = "未授权"),
),
security(("bearer_auth" = [])),
tag = "消息订阅"
)]
/// 获取当前用户的消息订阅偏好。
pub async fn get_subscription<S>(
State(_state): State<MessageState>,
Extension(ctx): Extension<TenantContext>,
) -> Result<Json<ApiResponse<crate::dto::MessageSubscriptionResp>>, AppError>
where
MessageState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
let resp = SubscriptionService::get(ctx.tenant_id, ctx.user_id, &_state.db).await?;
Ok(Json(ApiResponse::ok(resp)))
}
#[utoipa::path(
put,
path = "/api/v1/message-subscriptions",

View File

@@ -1,14 +1,15 @@
use axum::Json;
use axum::extract::FromRef;
use axum::extract::{Extension, Query, State};
use axum::extract::{Extension, Path, Query, State};
use serde::Deserialize;
use uuid::Uuid;
use erp_core::error::AppError;
use erp_core::rbac::require_permission;
use erp_core::types::{ApiResponse, PaginatedResponse, TenantContext};
use validator::Validate;
use crate::dto::{CreateTemplateReq, MessageTemplateResp};
use crate::dto::{CreateTemplateReq, MessageTemplateResp, UpdateTemplateReq};
use crate::message_state::MessageState;
use crate::service::template_service::TemplateService;
@@ -88,3 +89,52 @@ where
let resp = TemplateService::create(ctx.tenant_id, ctx.user_id, &req, &_state.db).await?;
Ok(Json(ApiResponse::ok(resp)))
}
#[utoipa::path(
put,
path = "/api/v1/message-templates/{id}",
request_body = UpdateTemplateReq,
responses(
(status = 200, description = "成功", body = ApiResponse<MessageTemplateResp>),
(status = 401, description = "未授权"),
(status = 403, description = "权限不足"),
),
security(("bearer_auth" = [])),
tag = "消息模板"
)]
/// 更新消息模板。
pub async fn update_template<S>(
State(_state): State<MessageState>,
Extension(ctx): Extension<TenantContext>,
Path(id): Path<Uuid>,
Json(req): Json<UpdateTemplateReq>,
) -> Result<Json<ApiResponse<MessageTemplateResp>>, AppError>
where
MessageState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "message.template.manage")?;
req.validate()
.map_err(|e| AppError::Validation(e.to_string()))?;
let resp = TemplateService::update(id, ctx.tenant_id, ctx.user_id, &req, &_state.db).await?;
Ok(Json(ApiResponse::ok(resp)))
}
/// 删除消息模板。
#[allow(clippy::type_complexity)]
pub async fn delete_template<S>(
State(_state): State<MessageState>,
Extension(ctx): Extension<TenantContext>,
Path(id): Path<Uuid>,
) -> Result<Json<ApiResponse<()>>, AppError>
where
MessageState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "message.template.manage")?;
TemplateService::delete(id, ctx.tenant_id, ctx.user_id, &_state.db).await?;
Ok(Json(ApiResponse::ok(())))
}