feat: 完成客户拜访模块全功能开发
本次提交实现了完整的客户拜访业务系统: 1. 新增对象存储上传服务,支持S3兼容协议的RustFS存储 2. 完善客户拜访实体与数据库表结构,补充全业务字段 3. 开发拜访台账与记录的前后端接口,包含增删改查、批量创建、状态流转等能力 4. 新增拜访表单页面与路由,支持父级菜单补全面包屑 5. 新增勘察列表页预填客户ID跳转逻辑 6. 补充UI设计文档与页面截图 7. 调整日志中间件跳过上传路由,优化RBAC权限配置
This commit is contained in:
1 parent
68af363ca9
commit
1c83e09fa1
33 files changed
+6920
-587
No files matched your search
@@ -0,0 +1,122 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user