Rust-导入导出

Rust-导入导出

👉常见的库以及解决方法

Cargo.toml使用的库

javascript 复制代码
futures-util = "0.3" # 异步任务工具
csv = "1.1" # CSV文件处理
calamine = "0.21" # 解析Excel(.xlsx/.xls/.ods)
umya-spreadsheet = "1.1"

👉导出Excel

接下来我们开发一个路由接口/system/users/export,用于导出Excel文件

🍎添加路由

src\modules\user\routes.rs添加路由接口

javascript 复制代码
  // 新增导出
  cfg.route("/system/users/export", 
          web::post().to(crate::modules::user::handlers::export_users));

🍎非流式返回

接下来我们先写一个导出格式的excel,这个时候我们直接返回

javascript 复制代码
pub async fn export_users(pool: web::Data<MySqlPool>) -> HttpResponse {
    let rows = sqlx::query_as::<_, User>(
        "SELECT user_id, username, password, age, name, sex, address, state, phone, avatar, user_height, user_weight, disease
         FROM sys_user
         WHERE is_deleted = 0"
    )
    .fetch_all(pool.get_ref())
    .await;

    let users = match rows {
        Ok(v) => v,
        Err(e) => {
            eprintln!("/system/USER/export query error: {:?}", e);
            return HttpResponse::InternalServerError().json(crate::common::response::ApiResponse::<()>{
                code: 500,
                msg: "查询用户失败",
                data: None
            });
        }
    };

    // CSV 表头(中文)
    let mut csv = String::new();
    csv.push_str("用户ID,用户名,姓名,年龄,性别,电话,地址,状态,头像,身高,体重,疾病\n");

    for u in users {
        let line = vec![
            u.user_id.to_string(),
            escape_csv(&u.username),
            escape_csv(u.name.as_deref().unwrap_or("")),
            escape_csv(u.age.as_deref().unwrap_or("")),
            u.sex.unwrap_or(0).to_string(),
            escape_csv(u.phone.as_deref().unwrap_or("")),
            escape_csv(u.address.as_deref().unwrap_or("")),
            u.state.unwrap_or(0).to_string(),
            escape_csv(u.avatar.as_deref().unwrap_or("")),
            escape_csv(u.user_height.as_deref().unwrap_or("")),
            escape_csv(u.user_weight.as_deref().unwrap_or("")),
            escape_csv(u.disease.as_deref().unwrap_or("")),
        ].join(",");

        csv.push_str(&line);
        csv.push('\n');
    }

    // UTF-8 BOM,Excel 识别中文更好
    let mut bytes = Vec::from([0xEF, 0xBB, 0xBF]);
    bytes.extend_from_slice(csv.as_bytes());

    HttpResponse::Ok()
        .content_type("text/csv; charset=utf-8")
        .append_header(("Content-Disposition", "attachment; filename=\"users.csv\""))
        .body(bytes)
}

这里我们直接返回的数据,非流式,但是大型数据无法使用,返回的数据格式如下,很明显不适用于我们现在的数据

javascript 复制代码
用户ID,用户名,姓名,年龄,性别,电话,地址,状态,头像,身高,体重,疾病
44,666666,san,666666,1,187XXX77,666666,0,/uploads/images/1746683201317-nexusvue-feedback.png,,,18735797977
65,123456,嗯,,1,187357XXX977,111,0,/uploads/images/1748584154718-2.png,,,18735797977
67,admin,,20,0,admin,,0,/uploads/images/9ff7b427-fe1b-46c6-a7c9-20823aac07c0.png,,,
69,user_o_1,郝颖,34,2,13531503450,天津市正定县大厦737号,1,/avatar/default1.png,183cm,63kg,过敏
70,user_y_2,毛伟,75,1,13503536954,新疆维吾尔自治区桥西区大厦342号,1,/avatar/female1.jpg,171cm,99kg,健康
71,user_w_3,韩彬,28,1,13469265460,河南省井陉矿区园718号,1,/avatar/custom1.jpg,195cm,99kg,健康
72,user_h_4,高明,68,2,15298848187,吉林省裕华区山庄480号,1,/avatar/female1.jpg,195cm,69kg,健康
73,user_q_5,石欣,62,1,19619145883,青海省栾城区小区218号,1,/avatar/female2.jpg,173cm,99kg,健康
74,user_j_6,唐娜,22,2,15342003992,吉林省辛集市胡同498号,1,/avatar/female3.jpg,179cm,63kg,健康
75,user_q_7,林欣,56,2,15687021122,河南省辛集市坊301号,2,/avatar/female3.jpg,191cm,63kg,健康

🍎流返回Excel

接下来我们返回一个流,处理我们的Excel功能,返回类型改为分块传输(chunked 流

这里我们依然基本实现,然后优化

javascript 复制代码
// 流式导出 CSV
pub async fn export_users(pool: web::Data<MySqlPool>) -> HttpResponse {
    use actix_web::web::Bytes;
    use futures_util::stream::{self, StreamExt};

    // BOM + 表头
    let head = {
        let mut v = vec![0xEF, 0xBB, 0xBF];
        v.extend_from_slice("用户ID,用户名,姓名,年龄,性别,电话,地址,状态,头像,身高,体重,疾病\n".as_bytes());
        v
    };
    let head_stream = stream::once(async { Ok::<Bytes, actix_web::Error>(Bytes::from(head)) });

    // 分页流:每次查询一批,转换为 Bytes 后产出
    let pool = pool.get_ref().clone();
    let rows_stream = stream::unfold(Some((pool, 0i32)), |state| async move {
        let (pool, last_id) = state?;

        let rows = match sqlx::query_as::<_, User>(
            "SELECT user_id, username, password, age, name, sex, address, state, phone, avatar, user_height, user_weight, disease
             FROM sys_user
             WHERE is_deleted = 0 AND user_id > ?
             ORDER BY user_id
             LIMIT 1000"
        )
        .bind(last_id)
        .fetch_all(&pool)
        .await
        {
            Ok(v) => v,
            Err(e) => {
                eprintln!("export_users query error: {e:?}");
                return Some((Err(actix_web::error::ErrorInternalServerError("查询用户失败")), None));
            }
        };

        if rows.is_empty() {
            return None;
        }

        let mut next_last = last_id;
        let mut buf = String::with_capacity(rows.len() * 128);
        for u in rows {
            next_last = u.user_id.max(next_last);
            let line = format!(
                "{},{},{},{},{},{},{},{},{},{},{},{}\n",
                u.user_id,
                escape_csv(&u.username),
                escape_csv(u.name.as_deref().unwrap_or("")),
                escape_csv(u.age.as_deref().unwrap_or("")),
                u.sex.unwrap_or(0),
                escape_csv(u.phone.as_deref().unwrap_or("")),
                escape_csv(u.address.as_deref().unwrap_or("")),
                u.state.unwrap_or(0),
                escape_csv(u.avatar.as_deref().unwrap_or("")),
                escape_csv(u.user_height.as_deref().unwrap_or("")),
                escape_csv(u.user_weight.as_deref().unwrap_or("")),
                escape_csv(u.disease.as_deref().unwrap_or("")),
            );
            buf.push_str(&line);
        }

        Some((Ok(Bytes::from(buf)), Some((pool, next_last))))
    });

    HttpResponse::Ok()
        .content_type("application/x-www-form-urlencoded; charset=utf-8")
        .append_header(("Content-Disposition", "attachment; filename=\"users.csv\""))
        .append_header(("Access-Control-Expose-Headers", "Content-Disposition, Content-Type"))
        .streaming(head_stream.chain(rows_stream))
}

测试接口,ok 。

现在我们返回Excel 导出就没问题了

👉导出Excel模板接口system/users/exporttemplate

🍎添加路由

src\modules\user\routes.rs添加路由接口

javascript 复制代码
// src\modules\user\routes.rs
 // 导出模板
  cfg.route("/system/users/exporttemplate", 
          web::post().to(crate::modules::user::handlers::export_users_template));

🍎数据逻辑

javascript 复制代码
// 流式导出用户模板
pub async fn export_users_template() -> HttpResponse {
    use actix_web::web::Bytes;
    use futures_util::stream::{self, StreamExt};

    // BOM + 表头
    let head = {
        let mut v = vec![0xEF, 0xBB, 0xBF];
        v.extend_from_slice("用户ID,用户名,姓名,年龄,性别,电话,地址,状态,头像,身高,体重,疾病\n".as_bytes());
        v
    };
    let head_stream = stream::once(async { Ok::<Bytes, actix_web::Error>(Bytes::from(head)) });

    // 空数据流,保持结构但不查询数据
    let empty_data_stream = stream::once(async {
        Ok::<Bytes, actix_web::Error>(Bytes::from("")) // No user data, just empty content
    });

    HttpResponse::Ok()
        .content_type("application/x-www-form-urlencoded; charset=utf-8")
        .append_header(("Content-Disposition", "attachment; filename=\"empty_users_template.csv\""))
        .append_header(("Access-Control-Expose-Headers", "Content-Disposition, Content-Type"))
        .streaming(head_stream.chain(empty_data_stream))
}

测试接口 ok

🍎优化接口为标准XLSX文件

javascript 复制代码
use umya_spreadsheet::*;
use std::io::Cursor;

pub async fn export_users_template() -> HttpResponse {
  // 创建一个带默认 Sheet1 的工作簿
  let mut book = new_file();
  let sheet = book.get_sheet_by_name_mut("Sheet1").unwrap();

  // 表头
  sheet.get_cell_mut((1, 1)).set_value("用户名");
  sheet.get_cell_mut((2, 1)).set_value("姓名");
  sheet.get_cell_mut((3, 1)).set_value("年龄");
  sheet.get_cell_mut((4, 1)).set_value("电话");

  // 写到内存
  let mut buffer = Cursor::new(Vec::new());
  writer::xlsx::write_writer(&book, &mut buffer).unwrap();
  let bytes = buffer.into_inner();

  HttpResponse::Ok()
      .append_header(("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"))
      .append_header(("Content-Disposition", "attachment; filename=\"users_template.xlsx\""))
      .append_header(("Access-Control-Expose-Headers", "Content-Disposition, Content-Type"))
      .body(bytes)
}

👉导入Excel数据接口

system/users/import

🍎添加路由

src\modules\user\routes.rs添加路由接口

javascript 复制代码
// src\modules\user\routes.rs

// 导入数据
  cfg.route("/system/users/import",
        web::post().to(crate::modules::user::handlers::import_users));

🍎数据逻辑

接下来,我们实现一个导入用户数据的接口,你需要处理上传的 CSV 文件,并解析其中的用户数据。然后,你可以将这些数据保存到数据库中。

添加依赖

javascript 复制代码
calamine = "0.21" # 解析Excel(.xlsx/.xls/.ods)

这里我以自己最简单的数据为主,并且我们给一个uuid,确保文件唯一性

javascript 复制代码
use actix_web::{post};
use actix_multipart::Multipart;
use csv;
use csv::ReaderBuilder;
use uuid::Uuid;
use std::fs::File;
use std::io::Write;
use calamine::{open_workbook_auto, Reader, RangeDeserializerBuilder};

#[derive(Debug, Deserialize)]
struct UserRow {
    username: String,
    name: String,
    age: i32,
    phone: String,
}

pub async fn import_users(
    mut payload: Multipart,
    db_pool: web::Data<Pool<MySql>>,
) -> impl Responder {
    while let Some(item) = payload.next().await {
        let mut field = item.unwrap();
        let file_id = Uuid::new_v4().to_string();
        let file_path = format!("/uploads/tmp/{}.xlsx", file_id);

        let mut f = File::create(&file_path).unwrap();

        while let Some(chunk) = field.next().await {
            let data = chunk.unwrap();
            f.write_all(&data).unwrap();
        }

        match open_workbook_auto(&file_path) {
            Ok(mut workbook) => {
                if let Some(Ok(range)) = workbook.worksheet_range("Sheet1") {

                    let mut iter = RangeDeserializerBuilder::new()
                        .has_headers(true)
                        .from_range::<_, UserRow>(&range) // 这里指定类型
                        .unwrap();
                    let mut count = 0;
                    while let Some(Ok(row)) = iter.next() {
                        // row 已经是 UserRow 类型
                        let hashed_pwd = hash("123456", DEFAULT_COST).unwrap();

                        sqlx::query!(
                            r#"
                            INSERT INTO sys_user (username, name, age, phone, password)
                            VALUES (?, ?, ?, ?, ?)
                            "#,
                            row.username,
                            row.name,
                            row.age,
                            row.phone,
                            hashed_pwd
                        )
                        .execute(db_pool.get_ref())
                        .await
                        .unwrap();

                        count += 1;
                    }

                    return format!("成功导入 {} 条用户数据", count);
                } else {
                    return "未找到 Sheet1 或解析失败".to_string();
                }
            }
            Err(e) => {
                return format!("Excel 解析失败: {}", e);
            }
        }
    }

    "未收到文件".to_string()
}

🍎Windows系统和linux文件兼容

测试接口问题

测试一下我们的接口,发现报错如下

javascript 复制代码
thread 'actix-rt|system:0|arbiter:0' panicked at src\modules\user\handlers.rs:503:46:
called `Result::unwrap()` on an `Err` value: 
Os { code: 3, kind: NotFound, message: " 系统找不到指定的路径。" }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

thread 'actix-rt|system:0|arbiter:1' panicked at src\modules\user\handlers.rs:503:46:
called `Result::unwrap()` on an `Err` 
value: Os { code: 3, kind: NotFound, message: " 系统找不到指定的路径。" }

这是因为我使用Windows系统,我写法采用的linux模式,这里我们切换一种win和linux都适用的

javascript 复制代码
// linux命令模式
let file_path = format!("/uploads/tmp/{}.xlsx", file_id);

更改以后,这里我们采用两种写法

方式一 自己创建目录
javascript 复制代码
//自己创建目录
pub async fn import_users(
    mut payload: Multipart,
    db_pool: web::Data<Pool<MySql>>,
) -> impl Responder {

    // 自己创建目录
    fs::create_dir_all("uploads").unwrap();

    while let Some(item) = payload.next().await {
        let mut field = item.unwrap();
        let file_id = Uuid::new_v4().to_string();
        let file_path = format!("uploads/tmp/{}.xlsx", file_id);
        let mut f = File::create(&file_path).unwrap();
        while let Some(chunk) = field.next().await {
            let data = chunk.unwrap();
            f.write_all(&data).unwrap();
        }
        match open_workbook_auto(&file_path) {
            Ok(mut workbook) => {
                if let Some(Ok(range)) = workbook.worksheet_range("Sheet1") {
                    let mut iter = RangeDeserializerBuilder::new()
                        .has_headers(true)
                        .from_range::<_, UserRow>(&range) // 这里指定类型
                        .unwrap();
                    let mut count = 0;
                    while let Some(Ok(row)) = iter.next() {
                        // row 已经是 UserRow 类型
                        let hashed_pwd = hash("123456", DEFAULT_COST).unwrap();

                        sqlx::query!(
                            r#"
                            INSERT INTO sys_user (username, name, age, phone, password)
                            VALUES (?, ?, ?, ?, ?)
                            "#,
                            row.username,
                            row.name,
                            row.age,
                            row.phone,
                            hashed_pwd
                        )
                        .execute(db_pool.get_ref())
                        .await
                        .unwrap();

                        count += 1;
                    }

                    return format!("成功导入 {} 条用户数据", count);
                } else {
                    return "未找到 Sheet1 或解析失败".to_string();
                }
            }
            Err(e) => {
                return format!("Excel 解析失败: {}", e);
            }
        }
    }
    "未收到文件".to_string()
}
方式二 借助临时目录

std::env::temp_dir() 获取跨平台的临时目录,然后拼接文件名

javascript 复制代码
use std::path::PathBuf;
use uuid::Uuid;

let file_id = Uuid::new_v4().to_string();
let mut file_path: PathBuf = std::env::temp_dir();
file_path.push(format!("{}.xlsx", file_id));

let mut f = File::create(&file_path).unwrap();

🍎报错处理-未找到 Sheet1 或解析失败

过程之中我们遇到了一个报错,未找到 Sheet1 或解析失败

主要写法出现就是这部分

javascript 复制代码
workbook.worksheet_range("Sheet1")

主要原因

我们的 **calamine**是严格大小写匹配表名

如果你的 Excel 里的表名是 "Sheet2""用户信息""sheet1" 或其他名字,它就会返回 None

解决方法 → 自动获取第一个 Sheet 名称

javascript 复制代码
if let Some(Ok(range)) = workbook.worksheet_range_at(0) {
    // 0 表示第一个 sheet
}

测试接口 ,上传成功,但是我们还没存入数据库之中

🍎XlsxXls格式报错

javascript 复制代码
Excel 解析失败: 
Xlsx error: Zip error: invalid Zip archive: Could not find central directory end

这是因为文件不是标准 .xlsx,我们将导出模板接口和导入都更改为标准的xlsx格式

现在的导出是 CSV ,导入是用 calamine 解析 Excel ,这两个格式不一致,

所以即使扩展名写成 .xlsx,导出文件在 WPS 或 Excel 打开没问题,但上传解析会直接报错(因为实际上是 CSV,不是 ZIP 的 .xlsx

想**完全兼容 WPS / Excel 的 ****.xlsx**,导出时就必须真的生成 .xlsx 文件,而不是伪装成 .xlsx 的 CSV。

在 Rust 里,可以用 umya-spreadsheet 这个 crate 来生成标准的 Excel 2007+ 格式文件。

完善一下,测试接口。ok

javascript 复制代码
#[derive(Debug, Deserialize)]
struct UserRow {
    #[serde(rename = "用户名")]
    username: String,
    #[serde(rename = "姓名")]
    name: String,
    #[serde(rename = "年龄")]
    age: i32,
    #[serde(rename = "电话")]
    phone: String,
}

pub async fn import_users(
    mut payload: Multipart,
    db_pool: web::Data<Pool<MySql>>,
) -> impl Responder {
    fs::create_dir_all("uploads/tmp").unwrap();

    println!("收到上传请求");

    // 打印当前连接的数据库(调试用)
    match sqlx::query!("SELECT DATABASE() AS db")
        .fetch_one(db_pool.get_ref())
        .await
    {
        Ok(db) => println!("当前连接数据库: {:?}", db.db),
        Err(e) => println!("无法获取数据库信息: {}", e),
    }

    while let Some(item) = payload.next().await {
        let mut field = match item {
            Ok(f) => f,
            Err(_) => return "文件读取失败".to_string(),
        };

        let file_id = Uuid::new_v4().to_string();
        let file_path = format!("uploads/tmp/{}.xlsx", file_id);
        let mut f = File::create(&file_path).unwrap();

        while let Some(chunk) = field.next().await {
            let data = match chunk {
                Ok(d) => d,
                Err(_) => return "文件写入失败".to_string(),
            };
            f.write_all(&data).unwrap();
        }

        // 解析 Excel
        match open_workbook_auto(&file_path) {
            Ok(mut workbook) => {
                if let Some(Ok(range)) = workbook.worksheet_range_at(0) {
                    let mut iter = RangeDeserializerBuilder::new()
                        .has_headers(true)
                        .from_range::<_, UserRow>(&range)
                        .unwrap();

                    let mut count = 0;
                    while let Some(Ok(row)) = iter.next() {
                        println!("读取到行数据: {:?}", row);

                        let hashed_pwd = match hash("123456", DEFAULT_COST) {
                            Ok(p) => p,
                            Err(e) => {
                                println!("密码加密失败: {}", e);
                                continue;
                            }
                        };

                        // 增加 is_deleted 字段
                        match sqlx::query!(
                            r#"
                            INSERT INTO sys_user (username, name, age, phone, password, is_deleted)
                            VALUES (?, ?, ?, ?, ?, '0')
                            "#,
                            row.username,
                            row.name,
                            row.age,
                            row.phone,
                            hashed_pwd
                        )
                        .execute(db_pool.get_ref())
                        .await
                        {
                            Ok(result) => {
                                println!("插入成功,影响行数: {}", result.rows_affected());
                                if result.rows_affected() > 0 {
                                    count += 1;
                                }
                            }
                            Err(e) => {
                                println!("插入失败: {}", e);
                            }
                        }
                    }
                    return format!("成功导入 {} 条用户数据", count);
                } else {
                    return "未找到工作表或解析失败".to_string();
                }
            }
            Err(e) => {
                return format!("Excel 解析失败: {}", e);
            }
        }
    }

    "未收到文件".to_string()
}

测试接口,功能ok

🍎优化提示(可用)

接下来优化一下我们接口给我们返回的提示

现在的提示

javascript 复制代码
成功导入 1 条用户数据

改成提示

javascript 复制代码
{
    "code": 200,
    "msg": "成功导入 1 条用户数据"
}

优化以后的写法

javascript 复制代码
// 版本2 
#[derive(Serialize)]
pub struct BasicImportResponse {
    pub code: i32,
    pub msg: String,
}

impl BasicImportResponse {
    pub fn new(code: i32, msg: impl Into<String>) -> Self {
        Self {
            code,
            msg: msg.into(),
        }
    }
}

#[derive(Debug, Deserialize)]
struct UserRow {
    #[serde(rename = "用户名")]
    username: String,
    #[serde(rename = "姓名")]
    name: String,
    #[serde(rename = "年龄")]
    age: i32,
    #[serde(rename = "电话")]
    phone: String,
}

pub async fn import_users(
    mut payload: Multipart,
    db_pool: web::Data<Pool<MySql>>,
) -> impl Responder {
    fs::create_dir_all("uploads/tmp").unwrap();
    println!("收到上传请求");

    while let Some(item) = payload.next().await {
        let mut field = match item {
            Ok(f) => f,
            Err(_) => {
                return web::Json(BasicImportResponse::new(400, "文件读取失败"));
            }
        };

        let file_id = Uuid::new_v4().to_string();
        let file_path = format!("uploads/tmp/{}.xlsx", file_id);
        let mut f = File::create(&file_path).unwrap();

        while let Some(chunk) = field.next().await {
            let data = match chunk {
                Ok(d) => d,
                Err(_) => {
                    return web::Json(BasicImportResponse::new(400, "文件写入失败"));
                }
            };
            f.write_all(&data).unwrap();
        }

        match open_workbook_auto(&file_path) {
            Ok(mut workbook) => {
                if let Some(Ok(range)) = workbook.worksheet_range_at(0) {
                    let mut iter = RangeDeserializerBuilder::new()
                        .has_headers(true)
                        .from_range::<_, UserRow>(&range)
                        .unwrap();

                    let mut count = 0;
                    let mut duplicated = 0;

                    while let Some(Ok(row)) = iter.next() {
                        let hashed_pwd = match hash("123456", DEFAULT_COST) {
                            Ok(p) => p,
                            Err(_) => continue,
                        };

                        match sqlx::query!(
                            r#"
                            INSERT INTO sys_user (username, name, age, phone, password, is_deleted)
                            VALUES (?, ?, ?, ?, ?, '0')
                            "#,
                            row.username,
                            row.name,
                            row.age,
                            row.phone,
                            hashed_pwd
                        )
                        .execute(db_pool.get_ref())
                        .await
                        {
                            Ok(result) => {
                                if result.rows_affected() > 0 {
                                    count += 1;
                                }
                            }
                            Err(_) => {
                                duplicated += 1;
                            }
                        }
                    }

                    let msg = if duplicated > 0 {
                        format!("成功导入 {} 条用户数据,{} 条已存在", count, duplicated)
                    } else {
                        format!("成功导入 {} 条用户数据", count)
                    };

                    return web::Json(BasicImportResponse::new(200, msg));
                } else {
                    return web::Json(BasicImportResponse::new(400, "未找到工作表或解析失败"));
                }
            }
            Err(e) => {
                return web::Json(BasicImportResponse::new(400, format!("Excel 解析失败: {}", e)));
            }
        }
    }

    web::Json(BasicImportResponse::new(400, "未收到文件"))
}

然后我们按照心意自己完善一下即可

相关推荐
黄智勇1 分钟前
xlsx-handlebars 一个用于处理 XLSX 文件 Handlebars 模板的 Rust 库,支持多平台使
前端
沐雨橙风ιε32 分钟前
Spring Boot整合Apache Shiro权限认证框架(应用篇)
java·spring boot·后端·apache shiro
考虑考虑36 分钟前
fastjson调用is方法开头注意
java·后端·java ee
小蒜学长1 小时前
springboot基于javaweb的小零食销售系统的设计与实现(代码+数据库+LW)
java·开发语言·数据库·spring boot·后端
brzhang1 小时前
为什么 OpenAI 不让 LLM 生成 UI?深度解析 OpenAI Apps SDK 背后的新一代交互范式
前端·后端·架构
EnCi Zheng1 小时前
JPA 连接 PostgreSQL 数据库完全指南
java·数据库·spring boot·后端·postgresql
brzhang1 小时前
OpenAI Apps SDK ,一个好的 App,不是让用户知道它该怎么用,而是让用户自然地知道自己在做什么。
前端·后端·架构
LucianaiB2 小时前
从玩具到工业:基于 CodeBuddy code CLI 构建电力变压器绕组短路智能诊断系统
后端
爱看书的小沐2 小时前
【小沐学WebGIS】基于Three.JS绘制飞行轨迹Flight Tracker(Three.JS/ vue / react / WebGL)
javascript·vue·webgl·three.js·航班·航迹·飞行轨迹
井柏然2 小时前
前端工程化—实战npm包深入理解 external 及实例唯一性
前端·javascript·前端工程化