oss1
This commit is contained in:
@@ -21,4 +21,21 @@ WX_MCH_ID=
|
||||
WX_MCH_SERIAL_NO=
|
||||
WX_MCH_PRIVATE_KEY=
|
||||
WX_API_V3_KEY=
|
||||
WX_PAY_NOTIFY_URL=https://your-domain.com/api/v1/callbacks/wechat/pay
|
||||
WX_PAY_NOTIFY_URL=https://your-domain.com/api/v1/callbacks/wechat/pay
|
||||
|
||||
# 阿里云 OSS(OSS_ENABLED=true 且下方密钥齐全时生效;否则 Mock 占位 URL)
|
||||
# RAM 用户需具备 PutObject 权限;可在控制台 Bucket 授权策略中为该 RAM UID 授予读写
|
||||
# 文档:https://help.aliyun.com/zh/oss/user-guide/use-bucket-policy-to-grant-permission-to-access-oss/
|
||||
OSS_ENABLED=false
|
||||
OSS_ACCESS_KEY_ID=
|
||||
OSS_ACCESS_KEY_SECRET=
|
||||
OSS_BUCKET=
|
||||
OSS_REGION=oss-cn-hangzhou
|
||||
# 公网访问域名:自定义 CDN 或 https://{bucket}.{region}.aliyuncs.com
|
||||
OSS_CDN_BASE=
|
||||
# 直传对象前缀(默认 uploads)
|
||||
OSS_UPLOAD_PREFIX=uploads
|
||||
# 直传凭证有效期(秒,默认 900)
|
||||
OSS_UPLOAD_EXPIRE_SECONDS=900
|
||||
# 单文件大小上限(字节,默认 10MB)
|
||||
OSS_MAX_UPLOAD_BYTES=10485760
|
||||
@@ -2,3 +2,4 @@ export const SMS_PROVIDER = 'SMS_PROVIDER';
|
||||
export const PAY_PROVIDER = 'PAY_PROVIDER';
|
||||
export const DELIVERY_PROVIDER = 'DELIVERY_PROVIDER';
|
||||
export const WECHAT_PROVIDER = 'WECHAT_PROVIDER';
|
||||
export const OSS_PROVIDER = 'OSS_PROVIDER';
|
||||
|
||||
@@ -7,15 +7,19 @@ import { PayWechatProvider } from './pay/pay.wechat.provider';
|
||||
import { DeliveryMockProvider } from './delivery/delivery.mock.provider';
|
||||
import { WechatApiProvider } from './wechat/wechat.api.provider';
|
||||
import { WechatDisabledProvider } from './wechat/wechat.disabled.provider';
|
||||
import { OssMockProvider } from './oss/oss.mock.provider';
|
||||
import { OssAliyunProvider } from './oss/oss.aliyun.provider';
|
||||
import {
|
||||
SMS_PROVIDER,
|
||||
PAY_PROVIDER,
|
||||
DELIVERY_PROVIDER,
|
||||
WECHAT_PROVIDER,
|
||||
OSS_PROVIDER,
|
||||
} from './integrations.constants';
|
||||
import { DELIVERY_QUEUE } from '../jobs/jobs.constants';
|
||||
import type { IWechatProvider } from './wechat/wechat.interface';
|
||||
import type { IPayProvider } from './pay/pay.interface';
|
||||
import type { IOssProvider } from './oss/oss.interface';
|
||||
|
||||
@Module({
|
||||
imports: [BullModule.registerQueue({ name: DELIVERY_QUEUE })],
|
||||
@@ -42,9 +46,19 @@ import type { IPayProvider } from './pay/pay.interface';
|
||||
inject: [PayMockProvider, PayWechatProvider],
|
||||
},
|
||||
{ provide: DELIVERY_PROVIDER, useClass: DeliveryMockProvider },
|
||||
OssMockProvider,
|
||||
OssAliyunProvider,
|
||||
{
|
||||
provide: OSS_PROVIDER,
|
||||
useFactory: (mock: OssMockProvider, aliyun: OssAliyunProvider): IOssProvider => {
|
||||
const cfg = loadAppConfig();
|
||||
return cfg.ossEnabled && aliyun.isEnabled() ? aliyun : mock;
|
||||
},
|
||||
inject: [OssMockProvider, OssAliyunProvider],
|
||||
},
|
||||
SmsMockProvider,
|
||||
DeliveryMockProvider,
|
||||
],
|
||||
exports: [SMS_PROVIDER, PAY_PROVIDER, DELIVERY_PROVIDER, WECHAT_PROVIDER],
|
||||
exports: [SMS_PROVIDER, PAY_PROVIDER, DELIVERY_PROVIDER, WECHAT_PROVIDER, OSS_PROVIDER],
|
||||
})
|
||||
export class IntegrationsModule {}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import { createHmac } from 'crypto';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { randomUUID } from 'crypto';
|
||||
import type { IOssProvider, OssUploadTokenInput, OssUploadTokenResult } from './oss.interface';
|
||||
|
||||
const DEFAULT_EXPIRE_SECONDS = 15 * 60;
|
||||
const DEFAULT_MAX_BYTES = 10 * 1024 * 1024;
|
||||
|
||||
@Injectable()
|
||||
export class OssAliyunProvider implements IOssProvider {
|
||||
private readonly accessKeyId = process.env.OSS_ACCESS_KEY_ID ?? '';
|
||||
private readonly accessKeySecret = process.env.OSS_ACCESS_KEY_SECRET ?? '';
|
||||
private readonly bucket = process.env.OSS_BUCKET ?? '';
|
||||
private readonly region = process.env.OSS_REGION ?? 'oss-cn-hangzhou';
|
||||
private readonly cdnBase = process.env.OSS_CDN_BASE ?? '';
|
||||
private readonly uploadPrefix = (process.env.OSS_UPLOAD_PREFIX ?? 'uploads').replace(/\/$/, '');
|
||||
private readonly expireSeconds = Number(process.env.OSS_UPLOAD_EXPIRE_SECONDS ?? DEFAULT_EXPIRE_SECONDS);
|
||||
private readonly maxUploadBytes = Number(process.env.OSS_MAX_UPLOAD_BYTES ?? DEFAULT_MAX_BYTES);
|
||||
|
||||
isEnabled() {
|
||||
return !!(this.accessKeyId && this.accessKeySecret && this.bucket);
|
||||
}
|
||||
|
||||
buildPublicUrl(ossKey: string) {
|
||||
const key = ossKey.replace(/^\//, '');
|
||||
if (this.cdnBase) {
|
||||
return `${this.cdnBase.replace(/\/$/, '')}/${key}`;
|
||||
}
|
||||
return `https://${this.bucket}.${this.region}.aliyuncs.com/${key}`;
|
||||
}
|
||||
|
||||
getUploadToken(dto: OssUploadTokenInput): OssUploadTokenResult {
|
||||
if (!this.isEnabled()) {
|
||||
throw new Error('OSS credentials are not configured');
|
||||
}
|
||||
|
||||
const ext = dto.fileName.includes('.') ? dto.fileName.split('.').pop() : 'bin';
|
||||
const dir = `${this.uploadPrefix}/${dto.bizType.toLowerCase()}/`;
|
||||
const ossKey = `${dir}${Date.now()}-${randomUUID().slice(0, 8)}.${ext}`;
|
||||
const expireAt = new Date(Date.now() + this.expireSeconds * 1000);
|
||||
const host = `https://${this.bucket}.${this.region}.aliyuncs.com`;
|
||||
|
||||
const policy = {
|
||||
expiration: expireAt.toISOString(),
|
||||
conditions: [
|
||||
['content-length-range', 0, this.maxUploadBytes],
|
||||
['eq', '$bucket', this.bucket],
|
||||
['starts-with', '$key', dir],
|
||||
],
|
||||
};
|
||||
const policyBase64 = Buffer.from(JSON.stringify(policy)).toString('base64');
|
||||
const signature = createHmac('sha1', this.accessKeySecret).update(policyBase64).digest('base64');
|
||||
|
||||
return {
|
||||
bucket: this.bucket,
|
||||
region: this.region,
|
||||
ossKey,
|
||||
url: this.buildPublicUrl(ossKey),
|
||||
mock: false,
|
||||
expireAt: expireAt.toISOString(),
|
||||
mediaType: dto.mediaType,
|
||||
bizType: dto.bizType,
|
||||
host,
|
||||
policy: policyBase64,
|
||||
signature,
|
||||
accessKeyId: this.accessKeyId,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
export interface OssUploadTokenInput {
|
||||
bizType: string;
|
||||
mediaType: string;
|
||||
fileName: string;
|
||||
}
|
||||
|
||||
export interface OssUploadTokenResult {
|
||||
bucket: string;
|
||||
region: string;
|
||||
ossKey: string;
|
||||
url: string;
|
||||
mock: boolean;
|
||||
expireAt: string;
|
||||
mediaType: string;
|
||||
bizType: string;
|
||||
/** OSS PostObject 直传 host,mock 时为空 */
|
||||
host?: string;
|
||||
/** Base64 policy,mock 时为空 */
|
||||
policy?: string;
|
||||
/** HMAC-SHA1 签名,mock 时为空 */
|
||||
signature?: string;
|
||||
/** RAM AccessKeyId,mock 时为空 */
|
||||
accessKeyId?: string;
|
||||
}
|
||||
|
||||
export interface IOssProvider {
|
||||
isEnabled(): boolean;
|
||||
getUploadToken(input: OssUploadTokenInput): OssUploadTokenResult;
|
||||
buildPublicUrl(ossKey: string): string;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { randomUUID } from 'crypto';
|
||||
import type { IOssProvider, OssUploadTokenInput, OssUploadTokenResult } from './oss.interface';
|
||||
|
||||
@Injectable()
|
||||
export class OssMockProvider implements IOssProvider {
|
||||
private readonly bucket = process.env.OSS_BUCKET || 'mock-dukang';
|
||||
private readonly region = process.env.OSS_REGION || 'oss-cn-hangzhou';
|
||||
private readonly cdnBase = process.env.OSS_CDN_BASE || 'https://mock-cdn.dukang.local';
|
||||
|
||||
isEnabled() {
|
||||
return false;
|
||||
}
|
||||
|
||||
buildPublicUrl(ossKey: string) {
|
||||
return `${this.cdnBase.replace(/\/$/, '')}/${ossKey.replace(/^\//, '')}`;
|
||||
}
|
||||
|
||||
getUploadToken(dto: OssUploadTokenInput): OssUploadTokenResult {
|
||||
const ext = dto.fileName.includes('.') ? dto.fileName.split('.').pop() : 'bin';
|
||||
const key = `uploads/${dto.bizType.toLowerCase()}/${Date.now()}-${randomUUID().slice(0, 8)}.${ext}`;
|
||||
return {
|
||||
bucket: this.bucket,
|
||||
region: this.region,
|
||||
ossKey: key,
|
||||
url: this.buildPublicUrl(key),
|
||||
mock: true,
|
||||
expireAt: new Date(Date.now() + 15 * 60 * 1000).toISOString(),
|
||||
mediaType: dto.mediaType,
|
||||
bizType: dto.bizType,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,31 +1,22 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { Inject, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import type { ResourceBizType, ResourceMediaType, ResourceOwnerType } from '@prisma/client';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { randomUUID } from 'crypto';
|
||||
import { PrismaService } from '../../common/prisma/prisma.module';
|
||||
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
|
||||
import { OSS_PROVIDER } from '../../integrations/integrations.constants';
|
||||
import type { IOssProvider } from '../../integrations/oss/oss.interface';
|
||||
import type { ResourceListQueryDto } from './dto/common-query.dto';
|
||||
import type { RegisterResourceDto, UpdateResourceDto, UploadTokenDto } from './dto/common-mutate.dto';
|
||||
|
||||
@Injectable()
|
||||
export class ResourceService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
@Inject(OSS_PROVIDER) private readonly oss: IOssProvider,
|
||||
) {}
|
||||
|
||||
getUploadToken(dto: UploadTokenDto) {
|
||||
const bucket = process.env.OSS_BUCKET || 'mock-dukang';
|
||||
const ext = dto.fileName.includes('.') ? dto.fileName.split('.').pop() : 'bin';
|
||||
const key = `uploads/${dto.bizType.toLowerCase()}/${Date.now()}-${randomUUID().slice(0, 8)}.${ext}`;
|
||||
const cdnBase = process.env.OSS_CDN_BASE || 'https://mock-cdn.dukang.local';
|
||||
return {
|
||||
bucket,
|
||||
region: process.env.OSS_REGION || 'oss-cn-hangzhou',
|
||||
ossKey: key,
|
||||
url: `${cdnBase}/${key}`,
|
||||
mock: true,
|
||||
expireAt: new Date(Date.now() + 15 * 60 * 1000).toISOString(),
|
||||
mediaType: dto.mediaType,
|
||||
bizType: dto.bizType,
|
||||
};
|
||||
return this.oss.getUploadToken(dto);
|
||||
}
|
||||
|
||||
async register(dto: RegisterResourceDto) {
|
||||
@@ -37,7 +28,7 @@ export class ResourceService {
|
||||
mediaType: dto.mediaType as ResourceMediaType,
|
||||
ossBucket: dto.ossBucket ?? process.env.OSS_BUCKET ?? 'mock-dukang',
|
||||
ossKey: dto.ossKey,
|
||||
url: dto.url,
|
||||
url: dto.url || this.oss.buildPublicUrl(dto.ossKey),
|
||||
fileName: dto.fileName,
|
||||
sortOrder: dto.sortOrder ?? 0,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user