Files
smart_grid_iot/server/src/service/upload.service.ts
T
andyesfly 1c83e09fa1 feat: 完成客户拜访模块全功能开发
本次提交实现了完整的客户拜访业务系统:
1.  新增对象存储上传服务,支持S3兼容协议的RustFS存储
2.  完善客户拜访实体与数据库表结构,补充全业务字段
3.  开发拜访台账与记录的前后端接口,包含增删改查、批量创建、状态流转等能力
4.  新增拜访表单页面与路由,支持父级菜单补全面包屑
5.  新增勘察列表页预填客户ID跳转逻辑
6.  补充UI设计文档与页面截图
7.  调整日志中间件跳过上传路由,优化RBAC权限配置
2026-09-14 11:42:10 +08:00

123 lines
3.0 KiB
TypeScript

import { Provide, Config } from "@midwayjs/core";
import { S3Client, PutObjectCommand, GetObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
import { createReadStream, statSync } from "fs";
import { randomUUID } from "crypto";
import { extname } from "path";
import { UploadFileInfo } from "@midwayjs/upload";
// RustFS 配置结构(见 config.default.ts 的 rustfs 段)
interface RustFsConfig {
endpoint: string;
port: number;
useSSL: boolean;
accessKey: string;
secretKey: string;
region: string;
bucket: string;
bucketProd: string;
bucketTest: string;
forcePathStyle: boolean;
presignExpires: number;
keyPrefix: string;
}
export interface UploadResult {
key: string;
url: string;
name: string;
size: number;
}
@Provide()
export class UploadService {
@Config("rustfs")
rustfsConfig!: RustFsConfig;
private client: S3Client | null = null;
private getClient(): S3Client {
if (!this.client) {
const protocol = this.rustfsConfig.useSSL ? "https" : "http";
this.client = new S3Client({
endpoint: `${protocol}://${this.rustfsConfig.endpoint}:${this.rustfsConfig.port}`,
region: this.rustfsConfig.region,
credentials: {
accessKeyId: this.rustfsConfig.accessKey,
secretAccessKey: this.rustfsConfig.secretKey
},
// RustFS 仅支持 path-style 访问
forcePathStyle: this.rustfsConfig.forcePathStyle
});
}
return this.client;
}
private get bucket(): string {
return this.rustfsConfig.bucket;
}
/**
* 上传单个文件到 RustFS,返回对象 key 与预签名访问 URL
*/
async uploadFile(file: UploadFileInfo<string>): Promise<UploadResult> {
if (!file || !file.data) {
throw new Error("上传文件为空");
}
const ext = extname(file.filename || "").toLowerCase();
const now = new Date();
const yyyymm = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, "0")}`;
const key = `${this.rustfsConfig.keyPrefix}/${yyyymm}/${randomUUID()}${ext}`;
await this.getClient().send(
new PutObjectCommand({
Bucket: this.bucket,
Key: key,
Body: createReadStream(file.data),
ContentType: file.mimeType || "application/octet-stream"
})
);
let size = 0;
try {
size = statSync(file.data).size;
} catch {
size = 0;
}
return {
key,
url: await this.getPresignedUrl(key),
name: file.filename,
size
};
}
/**
* 按对象 key 生成预签名访问 URL(默认 1 小时有效)
*/
async getPresignedUrl(key: string): Promise<string> {
if (!key) return "";
return getSignedUrl(
this.getClient(),
new GetObjectCommand({ Bucket: this.bucket, Key: key }),
{ expiresIn: this.rustfsConfig.presignExpires }
);
}
/**
* 批量把对象 key 转成预签名 URL,返回 key → url 映射(key 非法时返回空串)
*/
async getPresignedUrlMap(keys: string[]): Promise<Record<string, string>> {
const map: Record<string, string> = {};
for (const key of keys) {
try {
map[key] = await this.getPresignedUrl(key);
} catch {
map[key] = "";
}
}
return map;
}
}