You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Actix-web中实现API函数重载,复用加密与非加密端点逻辑?

解决Actix-web中加密/非加密端点的代码重复问题

背景

我需要为API服务创建两类端点,分别支持生产环境的加密参数和开发环境的非加密参数,最终调用同一业务逻辑,例如:

  • /api/movie/get(生产环境,接收加密参数)
  • /dev/movie/get(开发环境,接收非加密参数)

当前Actix-web实现

我用Actix-web的路由(非宏定义方式)配置路径路由,示例代码如下:

use actix_web::{web, App, HttpServer, Responder};

async fn index() -> impl Responder {
    "Hello world!"
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new().service(
            // 为所有附加的资源和路由添加前缀...
            web::scope("/app")
                // ...因此这个路由会处理`GET /app/index.html`请求
                .route("/index.html", web::get().to(index)),
        )
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}

目前每个API都要写两个处理函数,分别对应加密和非加密请求:

/// 加密参数版本
/// 路径: /api/movie/get
pub async fn handle_get_movie(
    app_data: Data<AppState>,
    Json(payload): Json<EncryptedPayload>,
) -> Result<impl Responder, Error> {

    // 解密内容
    // 这是加密与非加密端点唯一的区别
    let params = payload
        .unload::<GetMovieInf>()
        .map_err(|_| MainError::Malformatted)?;

    // 其他业务逻辑代码...

    Ok(Json(ReplyInf {ok: true}))
}
/// 非加密参数版本
/// 路径: /dev/movie/get
pub async fn handle_get_movie(
    app_data: Data<AppState>,
    Json(payload): Json<UnencryptedPayload>,
) -> Result<impl Responder, Error> {

    // 其他业务逻辑代码,和上面的函数完全一致...

    Ok(Json(ReplyInf {ok: true}))
}

两个函数只有解密步骤不同,其余业务逻辑完全重复。

问题

能不能把这两个相似函数合并成一个?试过用泛型Json<T>但没成功,也考虑过通过环境变量控制启用EncryptedPayload还是UnencryptedPayload,想避免重复写代码。

解决方案

方案1:用Trait抽象参数提取逻辑

通过自定义Trait统一加密/非加密参数的解析行为,让泛型函数能兼容两种类型:

首先定义Trait,抽象出获取业务参数的方法:

use actix_web::Json;

// 定义Trait,统一参数提取逻辑
trait ExtractParams<T> {
    fn extract_params(self) -> Result<T, MainError>;
}

// 为加密Payload实现Trait:解密并转换为业务参数
impl<T: serde::de::DeserializeOwned> ExtractParams<T> for EncryptedPayload {
    fn extract_params(self) -> Result<T, MainError> {
        self.unload::<T>().map_err(|_| MainError::Malformatted)
    }
}

// 为非加密Payload实现Trait:直接返回或转换为业务参数
// 假设UnencryptedPayload可以直接转为业务参数类型GetMovieInf
impl ExtractParams<GetMovieInf> for UnencryptedPayload {
    fn extract_params(self) -> Result<GetMovieInf, MainError> {
        Ok(GetMovieInf::from(self))
        // 如果UnencryptedPayload就是GetMovieInf本身,直接写Ok(self)即可
    }
}

然后编写泛型处理函数,复用业务逻辑:

pub async fn handle_get_movie<T, P>(
    app_data: Data<AppState>,
    Json(payload): Json<T>,
) -> Result<impl Responder, Error>
where
    T: ExtractParams<P>,
    P: std::fmt::Debug, // 根据实际业务需求添加必要约束
{
    // 统一提取业务参数
    let params = payload.extract_params()?;

    // 这里编写共享的业务逻辑
    // ...

    Ok(Json(ReplyInf { ok: true }))
}

最后在路由中注册两个端点,绑定到泛型函数的具体实例:

// 在App的路由配置里
App::new()
    .service(
        web::scope("/api")
            .route("/movie/get", web::post().to(handle_get_movie::<EncryptedPayload, GetMovieInf>))
    )
    .service(
        web::scope("/dev")
            .route("/movie/get", web::post().to(handle_get_movie::<UnencryptedPayload, GetMovieInf>))
    );

方案2:环境变量控制中间件,统一处理解密

如果不需要同时运行两类端点(比如生产环境只启/api,开发环境只启/dev),可以用环境变量控制中间件的启用,让业务函数只处理一种参数:

  1. 定义解密中间件,生产环境自动解密请求体:
use actix_web::{dev::Service, middleware::Middleware, HttpRequest, HttpResponse, Error};
use futures_util::future::BoxFuture;

struct DecryptMiddleware;

impl<S, B> Middleware<S> for DecryptMiddleware
where
    S: Service<Request = HttpRequest, Response = HttpResponse<B>, Error = Error>,
    S::Future: 'static,
    B: 'static,
{
    type Service = DecryptService<S>;

    fn wrap(&self, service: S) -> Self::Service {
        DecryptService { inner: service }
    }
}

struct DecryptService<S> {
    inner: S,
}

impl<S, B> Service for DecryptService<S>
where
    S: Service<Request = HttpRequest, Response = HttpResponse<B>, Error = Error>,
    S::Future: 'static,
    B: 'static,
{
    type Request = HttpRequest;
    type Response = HttpResponse<B>;
    type Error = Error;
    type Future = BoxFuture<'static, Result<Self::Response, Self::Error>>;

    fn poll_ready(&self, cx: &mut std::task::Context<'_>) -> std::task::Poll<Result<(), Self::Error>> {
        self.inner.poll_ready(cx)
    }

    fn call(&self, mut req: HttpRequest) -> Self::Future {
        // 这里实现读取请求体、解密、替换回请求体的逻辑
        // 简化示例,实际需要处理请求体的异步读取和修改
        // ...

        self.inner.call(req).boxed()
    }
}
  1. 根据环境变量配置路由和中间件:
#[actix_web::main]
async fn main() -> std::io::Result<()> {
    let run_mode = std::env::var("RUN_MODE").unwrap_or_else(|_| "dev".to_string());

    let app = match run_mode.as_str() {
        "prod" => {
            // 生产环境:启用解密中间件,绑定/api前缀的路由
            App::new()
                .wrap(DecryptMiddleware)
                .service(
                    web::scope("/api")
                        .route("/movie/get", web::post().to(handle_get_movie_unified))
                )
        }
        "dev" => {
            // 开发环境:直接绑定/dev前缀的路由,不用解密
            App::new()
                .service(
                    web::scope("/dev")
                        .route("/movie/get", web::post().to(handle_get_movie_unified))
                )
        }
        _ => panic!("无效的RUN_MODE值:{}", run_mode),
    };

    HttpServer::new(move || app.clone())
        .bind(("127.0.0.1", 8080))?
        .run()
        .await
}

这里的handle_get_movie_unified只需要处理非加密的业务参数GetMovieInf,中间件在生产环境自动完成解密,彻底避免了代码重复。


内容的提问来源于stack exchange,提问作者DennyHiu

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.31 20:15:51