Skip to content

Commit 5745bf2

Browse files
committed
Update: support sending emails
1 parent 99cfb8a commit 5745bf2

10 files changed

Lines changed: 160 additions & 549 deletions

File tree

packages/backend/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
"@nestjs/swagger": "^4.8.0",
2727
"@nestjs/typeorm": "^7.1.5",
2828
"@nestjs/websockets": "^7.6.15",
29-
"acm-client": "^3.1.0",
3029
"cache-manager": "^3.4.1",
3130
"class-transformer": "^0.4.0",
3231
"class-validator": "^0.13.1",

packages/backend/src/constants/consts.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ import { Group, Period, Step } from '@constants/enums';
33
export const QR_API = 'https://open.work.weixin.qq.com/wwopen/sso';
44
export const WX_API = 'https://qyapi.weixin.qq.com/cgi-bin';
55
export const SMS_API = 'https://open.hustunique.com/message/sms';
6+
export const ACM_API = 'http://acm.aliyun.com';
7+
export const EMAIL_HOST = 'smtpdm.aliyun.com';
8+
export const EMAIL_PORT = 465;
69
export const STEP_MAP: Record<Step, string> = {
710
[Step.报名]: '报名流程',
811
[Step.笔试]: '笔试流程',

packages/backend/src/controllers/candidates.controller.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import { RecruitmentsGateway } from '@gateways/recruitments.gateway';
4141
import { CodeGuard } from '@guards/code.guard';
4242
import { CandidatesService } from '@services/candidates.service';
4343
import { ConfigService } from '@services/config.service';
44+
import { EmailService } from '@services/email.service';
4445
import { InterviewsService } from '@services/interviews.service';
4546
import { RecruitmentsService } from '@services/recruitments.service';
4647
import { SMSService } from '@services/sms.service';
@@ -56,6 +57,7 @@ export class CandidatesController {
5657
private readonly recruitmentsService: RecruitmentsService,
5758
private readonly interviewsService: InterviewsService,
5859
private readonly smsService: SMSService,
60+
private readonly emailService: EmailService,
5961
private readonly configService: ConfigService,
6062
) {
6163
}
@@ -108,7 +110,7 @@ export class CandidatesController {
108110
});
109111
try {
110112
await this.smsService.sendSMS(phone, 670908, [name, '成功提交报名表单']);
111-
// TODO: send email
113+
await this.emailService.sendEmail(candidate);
112114
} catch ({ message }) {
113115
throw new InternalServerErrorException(message);
114116
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export interface Questionnaire {
2+
uri: string;
3+
description: string;
4+
}

packages/backend/src/modules/app.module.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { CandidatesModule } from '@modules/candidates.module';
1717
import { ChatModule } from '@modules/chat.module';
1818
import { CommentsModule } from '@modules/comments.module';
1919
import { ConfigModule } from '@modules/config.module';
20+
import { EmailModule } from '@modules/email.module';
2021
import { RecruitmentsModule } from '@modules/recruitments.module';
2122
import { SMSModule } from '@modules/sms.module';
2223
import { TasksModule } from '@modules/tasks.module';
@@ -27,22 +28,33 @@ import { ConfigService } from '@services/config.service';
2728
imports: [
2829
ConfigModule.forRoot({
2930
validationSchema: Joi.object({
31+
// app config
3032
NODE_ENV: Joi.string().valid(Env.dev, Env.prod, Env.test, Env.migration).default(Env.dev),
3133
RESUME_TEMPORARY_PATH: Joi.string().default('/tmp/resumes'),
3234
RESUME_PERSISTENT_PATH: Joi.string().default('./data/resumes'),
3335
PORT: Joi.number().default(5000),
36+
JWT_KEY: Joi.string().required(),
37+
// db config
3438
POSTGRES_HOST: Joi.string().default('postgres'),
3539
POSTGRES_PORT: Joi.number().default(5432),
3640
POSTGRES_USER: Joi.string().required(),
3741
POSTGRES_PASSWORD: Joi.string().required(),
3842
POSTGRES_DB: Joi.string().required(),
39-
JWT_KEY: Joi.string().required(),
43+
// ali config
44+
ACM_SECRET_KEY: Joi.string().required(),
45+
ACM_ACCESS_KEY: Joi.string().required(),
46+
ACM_DATA_ID: Joi.string().required(),
47+
ACM_GROUP: Joi.string().required(),
48+
ACM_NAMESPACE: Joi.string().required(),
49+
// tencent config
4050
APP_ID: Joi.string().required(),
4151
AGENT_ID: Joi.string().required(),
4252
REDIRECT_URI: Joi.string().required(),
43-
CORP_ID: Joi.string().required(),
4453
CORP_SECRET: Joi.string().required(),
54+
// sms and email config
4555
SMS_API_TOKEN: Joi.string().required(),
56+
EMAIL_USER: Joi.string().required(),
57+
EMAIL_PASS: Joi.string().required(),
4658
}),
4759
}),
4860
TypeOrmModule.forRootAsync({
@@ -69,6 +81,7 @@ import { ConfigService } from '@services/config.service';
6981
SMSModule,
7082
RecruitmentsModule,
7183
UsersModule,
84+
EmailModule,
7285
],
7386
providers: [
7487
ConfigService,

packages/backend/src/modules/candidates.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { CandidatesController } from '@controllers/candidates.controller';
66
import { CandidateEntity } from '@entities/candidate.entity';
77
import { CandidatesGateway } from '@gateways/candidates.gateway';
88
import { AuthModule } from '@modules/auth.module';
9+
import { EmailModule } from '@modules/email.module';
910
import { RecruitmentsModule } from '@modules/recruitments.module';
1011
import { SMSModule } from '@modules/sms.module';
1112
import { CandidatesService } from '@services/candidates.service';
@@ -25,6 +26,7 @@ import { ConfigService } from '@services/config.service';
2526
}),
2627
forwardRef(() => AuthModule),
2728
SMSModule,
29+
EmailModule,
2830
RecruitmentsModule,
2931
],
3032
controllers: [CandidatesController],
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { Module } from '@nestjs/common';
2+
3+
import { EmailService } from '@services/email.service';
4+
5+
@Module({
6+
providers: [EmailService],
7+
exports: [EmailService],
8+
})
9+
export class EmailModule {
10+
}

packages/backend/src/services/config.service.ts

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
import { createHmac } from 'crypto';
2+
13
import { Injectable } from '@nestjs/common';
24
import { ConfigService as DefaultConfigService } from '@nestjs/config';
35

4-
import { QR_API, SMS_API, WX_API } from '@constants/consts';
6+
import { ACM_API, EMAIL_HOST, EMAIL_PORT, QR_API, SMS_API, WX_API } from '@constants/consts';
57
import { Env } from '@constants/enums';
68

79
@Injectable()
@@ -53,9 +55,9 @@ export class ConfigService extends DefaultConfigService {
5355
}
5456

5557
get accessTokenURL() {
56-
const CORP_ID = this.get<string>('CORP_ID')!;
58+
const APP_ID = this.get<string>('APP_ID')!;
5759
const CORP_SECRET = this.get<string>('CORP_SECRET')!;
58-
return `${WX_API}/gettoken?corpid=${CORP_ID}&corpsecret=${CORP_SECRET}`;
60+
return `${WX_API}/gettoken?corpid=${APP_ID}&corpsecret=${CORP_SECRET}`;
5961
}
6062

6163
uidURL(accessToken: string, code: string) {
@@ -69,4 +71,41 @@ export class ConfigService extends DefaultConfigService {
6971
get smsURL() {
7072
return SMS_API;
7173
}
74+
75+
get acmEndpoint() {
76+
return `${ACM_API}:8080/diamond-server/diamond`;
77+
}
78+
79+
acmServer(ip: string) {
80+
const DATA_ID = this.get<string>('ACM_DATA_ID')!;
81+
const GROUP = this.get<string>('ACM_GROUP')!;
82+
const NAMESPACE = this.get<string>('ACM_NAMESPACE')!;
83+
return `http://${ip}:8080/diamond-server/config.co?dataId=${DATA_ID}&group=${GROUP}&tenant=${NAMESPACE}`;
84+
}
85+
86+
get acmHeaders() {
87+
const ACCESS_KEY = this.get<string>('ACM_ACCESS_KEY')!;
88+
const SECRET_KEY = this.get<string>('ACM_SECRET_KEY')!;
89+
const GROUP = this.get<string>('ACM_GROUP')!;
90+
const NAMESPACE = this.get<string>('ACM_NAMESPACE')!;
91+
const timestamp = Date.now();
92+
const signature = createHmac('sha1', SECRET_KEY).update(`${NAMESPACE}+${GROUP}+${timestamp}`).digest('base64');
93+
return {
94+
'Spas-AccessKey': ACCESS_KEY,
95+
'Spas-Signature': signature,
96+
timeStamp: timestamp.toString(),
97+
};
98+
}
99+
100+
get emailConfig() {
101+
return {
102+
host: EMAIL_HOST,
103+
port: EMAIL_PORT,
104+
auth: {
105+
user: this.get<string>('EMAIL_USER')!,
106+
pass: this.get<string>('EMAIL_PASS')!,
107+
},
108+
secure: true,
109+
};
110+
}
72111
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { TextDecoder } from 'util';
2+
3+
import { Injectable } from '@nestjs/common';
4+
import got from 'got';
5+
import { createTransport, Transporter } from 'nodemailer';
6+
7+
import { Group } from '@constants/enums';
8+
import { CandidateEntity } from '@entities/candidate.entity';
9+
import { Questionnaire } from '@interfaces/questionnaire';
10+
import { ConfigService } from '@services/config.service';
11+
12+
@Injectable()
13+
export class EmailService {
14+
gbkDecoder = new TextDecoder('gbk');
15+
16+
transporter: Transporter;
17+
18+
constructor(
19+
private readonly configService: ConfigService,
20+
) {
21+
this.transporter = createTransport(configService.emailConfig);
22+
}
23+
24+
private async getQuestionnaires() {
25+
const endpoint = this.configService.acmEndpoint;
26+
const ip = (await got(endpoint).text()).split('\n')[0];
27+
const gbkBuffer = await got(this.configService.acmServer(ip), {
28+
headers: this.configService.acmHeaders,
29+
}).buffer();
30+
return JSON.parse(this.gbkDecoder.decode(gbkBuffer)) as Record<Group, Questionnaire>;
31+
}
32+
33+
async sendEmail({ name, group, recruitment, mail }: CandidateEntity) {
34+
if (this.configService.isDev) {
35+
return;
36+
}
37+
const questionnaires = await this.getQuestionnaires();
38+
if (questionnaires?.[group]?.uri) {
39+
const { uri, description } = questionnaires[group];
40+
await this.transporter.sendMail({
41+
from: '联创团队<robot@mail.hustunique.com>',
42+
to: {
43+
name,
44+
address: mail,
45+
},
46+
subject: `联创团队${recruitment.name}报名成功通知`,
47+
// eslint-disable-next-line max-len
48+
html: `<title>联创团队${recruitment.name}</title><body style=background:#f6f6f6;font-family:sans-serif><div style='margin:20px auto;max-width:580px;padding:10px'><div style='padding:30px 20px;background:#fff;border-radius:16px'><h2 style=font-weight:400;margin-top:0>联创团队${recruitment.name}报名成功通知</h2><hr style='border:0;height:1px;background-image:linear-gradient(to right,#ccc,#aaa,#ccc)'><p>${name},你好!<p>${description}</p><a href='${uri}' style='background:#3fa9f5;border-radius:5px;color:#fff;display:inline-block;font-weight:700;padding:12px 25px;text-decoration:none' target='_blank'>Join Us</a></div><p style=margin-top:10px;text-align:center;font-size:12px;color:#999>联创团队 | <a href='mailto:contact@hustunique.com' style=color:#999>Contact Us</a></div>`,
49+
});
50+
}
51+
}
52+
}

0 commit comments

Comments
 (0)