Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion env.example
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
# ALIYUN_SECRET=

# 火山引擎短信
# VOLCENGINE_SMS_ACCOUNT=
# VOLCENGINE_SMS_ACCOUNT= # 默认消息组 ID;国际短信可由调用方在 sendSms 请求中传 account 覆盖
# VOLCENGINE_KEY=
# VOLCENGINE_SECRET=

Expand Down
18 changes: 17 additions & 1 deletion openapi.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"hash": "784097a775e52168883b76a7f9a709a61104d472edba7731896d3f0369068f3f",
"hash": "ef6200b85a5a096b716c3bc8e84e84d57e7c515a9a8c4e3e24ee300bd1079bdc",
"openapi": "3.0.0",
"paths": {
"/hello": {
Expand Down Expand Up @@ -7368,6 +7368,10 @@
},
"params": {
"type": "object"
},
"account": {
"type": "string",
"description": "火山引擎消息组 ID;未传时使用 VOLCENGINE_SMS_ACCOUNT"
}
},
"required": [
Expand Down Expand Up @@ -7411,6 +7415,10 @@
"type": "string",
"description": "参数"
},
"account": {
"type": "string",
"description": "火山引擎消息组 ID"
},
"sentAt": {
"format": "date-time",
"type": "string",
Expand Down Expand Up @@ -7451,6 +7459,10 @@
"type": "string",
"description": "参数"
},
"account": {
"type": "string",
"description": "火山引擎消息组 ID"
},
"sentAt": {
"format": "date-time",
"type": "string",
Expand Down Expand Up @@ -7514,6 +7526,10 @@
"type": "string",
"description": "参数"
},
"account": {
"type": "string",
"description": "火山引擎消息组 ID"
},
"sentAt": {
"format": "date-time",
"type": "string",
Expand Down
5 changes: 5 additions & 0 deletions src/sms/dto/send-sms.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,9 @@ export class SendSmsDto {

@IsOptional()
params?: Record<string, string>;

/** 火山引擎消息组 ID;未传时使用 VOLCENGINE_SMS_ACCOUNT */
@IsOptional()
@IsString()
account?: string;
}
8 changes: 8 additions & 0 deletions src/sms/entities/sms-record.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,14 @@ export class SmsRecordDoc {
@Prop()
params?: string;

/**
* 火山引擎消息组 ID
*/
@IsOptional()
@IsString()
@Prop()
account?: string;

/**
* 发送时间
*/
Expand Down
5 changes: 4 additions & 1 deletion src/sms/sms.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@ export class SmsController {
@Post('@sendSms')
async sendSms(@Body() body: SendSmsDto) {
const dto: CreateSmsRecordDto = {
...body,
phone: body.phone,
sign: body.sign,
template: body.template,
account: this.smsService.resolveAccount(body),
status: SmsStatus.PENDING,
params: body.params ? JSON.stringify(body.params) : undefined,
};
Expand Down
83 changes: 83 additions & 0 deletions src/sms/sms.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { Test, TestingModule } from '@nestjs/testing';

import * as config from 'src/config';

import { SmsService } from './sms.service';

jest.mock('src/config', () => ({
sms: {
provider: 'volcengine',
aliyun: { keyId: '', keySecret: '' },
volcengine: {
account: 'default-account',
accessKeyId: 'key',
secretKey: 'secret',
},
},
}));

const sendMock = jest.fn().mockResolvedValue({ ResponseMetadata: {} });

jest.mock('@volcengine/openapi/lib/services/sms', () => ({
SmsService: jest.fn().mockImplementation(() => ({
setAccessKeyId: jest.fn(),
setSecretKey: jest.fn(),
Send: sendMock,
})),
}));

describe('SmsService', () => {
let service: SmsService;

beforeEach(async () => {
sendMock.mockClear();
const module: TestingModule = await Test.createTestingModule({
providers: [SmsService],
}).compile();
service = module.get(SmsService);
});

it('uses account from dto when provided', async () => {
await service.send({
phone: '+8613800138000',
sign: 'sign',
template: 'tpl',
account: 'foreign-account',
});

expect(sendMock).toHaveBeenCalledWith(
expect.objectContaining({ SmsAccount: 'foreign-account' })
);
});

it('falls back to env default account when dto.account is omitted', async () => {
await service.send({
phone: '+8613800138000',
sign: 'sign',
template: 'tpl',
});

expect(sendMock).toHaveBeenCalledWith(
expect.objectContaining({ SmsAccount: config.sms.volcengine.account })
);
});

it('resolveAccount returns dto account or env default for volcengine', () => {
expect(
service.resolveAccount({
phone: '+8613800138000',
sign: 'sign',
template: 'tpl',
account: 'foreign-account',
})
).toBe('foreign-account');

expect(
service.resolveAccount({
phone: '+8613800138000',
sign: 'sign',
template: 'tpl',
})
).toBe(config.sms.volcengine.account);
});
});
10 changes: 9 additions & 1 deletion src/sms/sms.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,18 @@ export class SmsService {
return this.volcengineClient;
}

/** 实际用于发送的消息组 ID(volcengine 未传时回退 env 默认) */
resolveAccount(dto: SendSmsDto): string | undefined {
if (this.getProvider() !== 'volcengine') {
return dto.account;
}
return dto.account ?? config.sms.volcengine.account;
}

private async sendByVolcengine(dto: SendSmsDto) {
const { phone, sign, template, params } = dto;
const res = await this.getVolcengineClient().Send({
SmsAccount: config.sms.volcengine.account,
SmsAccount: this.resolveAccount(dto),
Sign: sign,
TemplateID: template,
PhoneNumbers: phone,
Expand Down
Loading