本次提交实现了完整的客户拜访业务系统: 1. 新增对象存储上传服务,支持S3兼容协议的RustFS存储 2. 完善客户拜访实体与数据库表结构,补充全业务字段 3. 开发拜访台账与记录的前后端接口,包含增删改查、批量创建、状态流转等能力 4. 新增拜访表单页面与路由,支持父级菜单补全面包屑 5. 新增勘察列表页预填客户ID跳转逻辑 6. 补充UI设计文档与页面截图 7. 调整日志中间件跳过上传路由,优化RBAC权限配置
123 lines
3.0 KiB
TypeScript
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;
|
|
}
|
|
}
|