upload future report bid product

This commit is contained in:
Admin 2025-05-20 16:48:37 +07:00
parent 00cc23a46d
commit 168d458009
17 changed files with 2844 additions and 263 deletions

BIN
.DS_Store vendored

Binary file not shown.

View File

@ -1,178 +0,0 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
<head>
<meta http-equiv=3D"Content-Type" content=3D"text/html; charset=3Dutf-8">
<title>Callcentric Email</title>
<style type=3D"text/css" media=3D"all">
body {
font-family: arial,helvetica,sans-serif;
font-size: 14px;
line-height: 22px;
color: #444;
margin: 0;
padding: 20px 0;
background-color: #e5e5e5;
outline: 0;
vertical-align: top;
}
p {
margin-bottom: 1.5em;
}
img {
display: block;
}
a:link,
a:visited {
color: #006699;
text-decoration: underline;
margin: 0px;
padding: 0px;
}
a:hover {=20
text-decoration: none;
}
li {
margin-bottom: 1em;
line-height: 22px;
}=09
=2Efooter a {
color: #aaaaaa;
text-decoration: underline;
}
=2Efooter a:hover {
text-decoration: none;
}
=2Eframe {
width: 580px;
}
#btn {
background: #3498db;
background-image: -webkit-linear-gradient(top, #3498db, #2980b9);
background-image: -moz-linear-gradient(top, #3498db, #2980b9);
background-image: -ms-linear-gradient(top, #3498db, #2980b9);
background-image: -o-linear-gradient(top, #3498db, #2980b9);
background-image: linear-gradient(to bottom, #3498db, #2980b9);
-webkit-border-radius: 28;
-moz-border-radius: 28;
border-radius: 28px;
font-family: Arial;
color: #ffffff;
font-size: 20px;
padding: 10px 20px 10px 20px;
text-decoration: none;
display: inline-block;
}
#btn:hover {
background: #3cb0fd;
background-image: -webkit-linear-gradient(top, #3cb0fd, #3498db);
background-image: -moz-linear-gradient(top, #3cb0fd, #3498db);
background-image: -ms-linear-gradient(top, #3cb0fd, #3498db);
background-image: -o-linear-gradient(top, #3cb0fd, #3498db);
background-image: linear-gradient(to bottom, #3cb0fd, #3498db);
text-decoration: none;
}
=2Esms-box { font-family: arial,helvetica,sans-serif; font-size: 14px; padd=
ing:12px; border:1px solid #e0e0e0; background-color: #e0e0e0; -webkit-bord=
er-radius: 12px; -moz-border-radius: 12px; border-radius: 12px; word-wrap:b=
reak-word; display:inline-block; }
p { font-family: arial,helvetica,sans-serif; font-size: 14px; margin-bottom=
: 1.5em; line-height: 22px; }
@media only screen and (max-device-width: 480px) {
.frame {
width: 442px !important;
}
p {
font-size: 22px !important;
line-height: 28px !important;
}
ul li {
font-size: 22px !important;
line-height: 28px !important;
}
.section_header{
font-size: 28px !important;
line-height: 30px !important;
}
#btn {
font-size: 28px !important;
padding: 14px 24px 14px 24px !important;
}
}
</style>
</head>
<body style=3D"font-family: Arial, sans-serif; font-size: 14px; margin-bott=
om: 1.5em; line-height: 22px; color: black; width: 100% !important; margin:=
0; padding: 0px; background-color: #e5e5e5; outline: 0; vertical-align: to=
p;" bgcolor=3D"#e5e5e5">
<table width=3D"100%" cellpadding=3D"0" cellspacing=3D"0" border=3D"0"><tr>=
<td style=3D"padding: 0px 0px;" align=3D"center">
<table class=3D"head" width=3D"100%" bgcolor=3D"#c9c9c9" style=3D"backgroun=
d-color:#c9c9c9;">
<tr><td align=3D"center" style=3D"color:#f3f3f3; padding:5px; font-size:12=
px;">Please do not respond to this automated message, you will not receive =
a reply</td></tr>
</table>
</td></tr><tr><td style=3D"padding: 0px 10px;" align=3D"center">
<table class=3D"frame" width=3D"580" cellpadding=3D"0" cellspacing=3D"0" bo=
rder=3D"0" align=3D"center"><tbody>
<tr><td>
<table width=3D"100%" cellpadding=3D"0" cellspacing=3D"0" border=3D"0">
<tr><td style=3D"padding: 30px 0px 10px 0px; color: #444; text-align: le=
ft; vertical-align: middle;" align=3D"left"><a href=3D"https://www.callcent=
ric.com"><img src=3D"https://www.callcentric.com/img/callcentric_logo_250_t=
rans.png" width=3D"125" height=3D"33" alt=3D"Callcentric" border=3D"0"></a>=
</td></tr>
</table>
</td></tr>
<tr><td style=3D"background-color: #ffffff; padding: 0px 0px 50px; border-=
radius: 5px 5px 0px 0px;">
<table style=3D"padding-top: 40px;" width=3D"100%" cellpadding=3D"0" cell=
spacing=3D"0" border=3D"0"><tbody>
<tr><td class=3D"flex-column" style=3D"width: 40px;"></td>
<td style=3D"text-align: left;" align=3D"left">
<p>Dear Morgan Vu,</p><p>You have received an inbound text message from 223=
95 on your DID 17146893350:</p><div class=3D"sms-box">Your LANGTONS verific=
ation code is: 012152. This code will expire in 10 minutes.</div>
</td><td class=3D"flex-column" style=3D"width: 40px;"></td></tr>
</tbody></table>
</td></tr>
<tr><td align=3D"left" class=3D"footer" style=3D"background-color: #f2f2f2=
; color: #999999; padding: 0px 0px 40px; border-radius: 0px 0px 5px 5px;">
<table border=3D"0" cellpadding=3D"0" cellspacing=3D"0" style=3D"padding-=
top: 40px;" width=3D"100%"><tbody>
<tr><td style=3D"width: 40px;"></td>
<td align=3D"left" style=3D"text-align: left; font-family:Arial, Helvet=
ica, sans-serif; font-size: 15px; line-height: 22px; color: #999999;">
Follow us on Twitter at:<br>
<a href=3D"https://twitter.com/Callcentric">https://twitter.com/Callcentric=
</a>
<br><br>
If you have any questions/concerns, please contact our Customer Service Tea=
m via the following page:<br>
<a href=3D"https://www.callcentric.com/CustomerService">https://www.callcen=
tric.com/CustomerService</a><br><br>
For instructions on how to unsubscribe from our Newsletters/Service Announc=
ements please visit:<br>
<a href=3D"https://www.callcentric.com/faq/11/306">https://www.callcentric=
=2Ecom/faq/11/306</a><br><br>
<a href=3D"https://www.callcentric.com">www.callcentric.com</a><br><br>
</td><td style=3D"width: 40px;"></td>
</tr>
</tbody></table>
</td></tr>
<tr><td style=3D"padding: 20px 0; font-size: 15px; line-height: 25px; colo=
r: #999999; text-align: center;" align=3D"center"></td></tr>
</tbody></table></td></tr></table>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@ -25,6 +25,7 @@
"migration:revert": "npm run typeorm -- -d ./src/config/typeorm.ts migration:revert"
},
"dependencies": {
"@nestjs-modules/mailer": "^2.0.2",
"@nestjs/common": "^10.0.0",
"@nestjs/config": "^4.0.1",
"@nestjs/core": "^10.0.0",
@ -51,6 +52,7 @@
"multer": "^1.4.5-lts.1",
"mysql2": "^3.13.0",
"nestjs-paginate": "^11.1.0",
"nodemailer": "^7.0.3",
"reflect-metadata": "^0.2.0",
"rxjs": "^7.8.1",
"sharp": "^0.33.5",

View File

@ -16,6 +16,7 @@ import { ClientAuthenticationMiddleware } from './modules/auth/middlewares/clien
import { NotificationModule } from './modules/notification/notification.module';
import { ScrapsModule } from './modules/scraps/scraps.module';
import { ExtensionsModule } from './modules/extensions/extensions.module';
import { MailsModule } from './modules/mails/mails.module';
@Module({
imports: [
@ -28,6 +29,7 @@ import { ExtensionsModule } from './modules/extensions/extensions.module';
NotificationModule,
ScrapsModule,
ExtensionsModule,
MailsModule,
],
controllers: [],
providers: [],

View File

@ -39,7 +39,7 @@ import { Config } from './entities/configs.entity';
OutBidLog,
WebBid,
SendMessageHistory,
Config
Config,
]),
// AuthModule,
AdminsModule,
@ -69,8 +69,13 @@ import { Config } from './entities/configs.entity';
ImapService,
DashboardService,
TasksService,
ConfigsService
ConfigsService,
],
exports: [
BotTelegramApi,
SendMessageHistoriesService,
BidsService,
ConfigsService,
],
exports: [BotTelegramApi, SendMessageHistoriesService, BidsService],
})
export class BidsModule {}

View File

@ -8,22 +8,25 @@ import { Config } from '../entities/configs.entity';
export class ConfigsService {
public static CONFIG_KEYS = {
REFRESH_TOOL_TIME: 'REFRESH_TOOL_TIME',
MAIL_SCRAP_REPORT: 'MAIL_SCRAP_REPORT',
};
constructor(
@InjectRepository(Config)
readonly configRepo: Repository<Config>,
private eventEmitter: EventEmitter2,
) {}
async getConfig(key_name: keyof typeof ConfigsService.CONFIG_KEYS) {
return await this.configRepo.findOne({ where: { key_name } }) || null;
return (await this.configRepo.findOne({ where: { key_name } })) || null;
}
async setConfig(key_name: keyof typeof ConfigsService.CONFIG_KEYS, value: string, type: 'string' | 'number') {
return await this.configRepo.upsert(
{ key_name, value , type},
['key_name']
);
async setConfig(
key_name: keyof typeof ConfigsService.CONFIG_KEYS,
value: string,
type: 'string' | 'number',
) {
return await this.configRepo.upsert({ key_name, value, type }, [
'key_name',
]);
}
}

View File

@ -0,0 +1,27 @@
import { MailerModule } from '@nestjs-modules/mailer';
import { Module } from '@nestjs/common';
import { MailsService } from './services/mails.service';
import { ConfigModule, ConfigService } from '@nestjs/config';
@Module({
imports: [
MailerModule.forRootAsync({
imports: [ConfigModule],
useFactory: async (config: ConfigService) => ({
transport: {
host: config.get<string>('MAIL_SERVER'),
port: 465,
secure: true, // true nếu port là 465
auth: {
user: config.get<string>('MAIL_USER'),
pass: config.get<string>('MAIL_PASSWORD'),
},
},
}),
inject: [ConfigService],
}),
],
providers: [MailsService],
exports: [MailsService],
})
export class MailsModule {}

View File

@ -0,0 +1,89 @@
import { Injectable } from '@nestjs/common';
import { MailerService } from '@nestjs-modules/mailer';
import { ScrapItem } from '@/modules/scraps/entities/scrap-item.entity';
import { extractDomain } from '@/ultils';
@Injectable()
export class MailsService {
constructor(private readonly mailerService: MailerService) {}
async sendPlainText(to: string, subject: string, content: string) {
await this.mailerService.sendMail({
to,
subject,
text: content,
});
}
async sendPlainHtml(to: string, subject: string, html: string) {
const emails = to
.split(',')
.map((e) => e.trim())
.filter(Boolean);
const mainRecipient = emails[0];
const ccRecipients = emails.slice(1); // Những email còn lại
await this.mailerService.sendMail({
to: mainRecipient,
cc: ccRecipients.length > 0 ? ccRecipients : undefined,
subject,
html,
});
}
generateProductTableHTML(products: ScrapItem[]): string {
const rows = products
.map(
(p) => `
<tr>
<td><img src="${p.image_url}" alt="Product Image" style="height: 40px; object-fit: contain; border-radius: 4px;" /></td>
<td>${p.name}</td>
<td style="font-weight: bold; color: #e03131;">$${p.current_price}</td>
<td><a href="${p.url}" target="_blank" style="color: #007bff;">View</a></td>
<td>${extractDomain(p.scrap_config.web_bid.origin_url)}</td>
</tr>
`,
)
.join('');
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Products</title>
</head>
<body style="font-family: sans-serif; background: #f8f9fa; padding: 20px;">
<h2 style="text-align: center; color: #333;">Product Listing</h2>
<div style="overflow-x: auto;">
<table style="width: 100%; min-width: 500px; border-collapse: collapse; background: #fff; border-radius: 6px; box-shadow: 0 2px 6px rgba(0,0,0,0.05);">
<thead style="background: #f1f3f5;">
<tr>
<th style="padding: 8px; text-align: left; border-bottom: 1px solid #ccc;">Image</th>
<th style="padding: 8px; text-align: left; border-bottom: 1px solid #ccc;">Name</th>
<th style="padding: 8px; text-align: left; border-bottom: 1px solid #ccc;">Price</th>
<th style="padding: 8px; text-align: left; border-bottom: 1px solid #ccc;">Link</th>
<th style="padding: 8px; text-align: left; border-bottom: 1px solid #ccc;">Domain</th>
</tr>
</thead>
<tbody>
${rows}
</tbody>
</table>
</div>
</body>
</html>
`;
}
async sendWithTemplate(to: string, subject: string, payload: any) {
await this.mailerService.sendMail({
to,
subject,
template: './welcome', // đường dẫn tương đối trong /templates
context: payload, // dữ liệu cho template
});
}
}

View File

@ -1,16 +1,21 @@
import { MailsService } from '@/modules/mails/services/mails.service';
import { Body, Controller, Get, Param, Post, Put } from '@nestjs/common';
import { Between, IsNull, Not } from 'typeorm';
import { CreateScrapConfigDto } from '../dto/scrap-config/create-scrap-config';
import { ScrapConfigsService } from '../services/scrap-config.service';
import { UpdateScrapConfigDto } from '../dto/scrap-config/update-scrap-config';
import { ScrapConfig } from '../entities/scrap-config.entity';
import { IsNull, Not } from 'typeorm';
import { ScrapConfigsService } from '../services/scrap-config.service';
import { ScrapItemsService } from '../services/scrap-item-config.service';
import { ConfigsService } from '@/modules/bids/services/configs.service';
import * as moment from 'moment';
@Controller('admin/scrap-configs')
export class ScrapConfigsController {
constructor(
private readonly scrapConfigsService: ScrapConfigsService,
private readonly scrapItemsService: ScrapItemsService,
private readonly mailsService: MailsService,
private readonly configsSerivce: ConfigsService,
) {}
@Post()
@ -28,6 +33,11 @@ export class ScrapConfigsController {
@Get()
async test() {
const mails = (await this.configsSerivce.getConfig('MAIL_SCRAP_REPORT'))
.value;
if (!mails) return;
const scrapConfigs = await this.scrapConfigsService.scrapConfigRepo.find({
where: {
search_url: Not(IsNull()),
@ -37,24 +47,39 @@ export class ScrapConfigsController {
web_bid: true,
},
});
const models = this.scrapConfigsService.scrapModels(scrapConfigs);
await Promise.allSettled(
models.map(async (item) => {
await item.action();
Object.keys(item.results).forEach(async (key) => {
const data = item.results[key];
await this.scrapItemsService.scrapItemRepo.upsert(data, [
'model',
'scrap_config',
]);
});
for (const key of Object.keys(item.results)) {
const dataArray = item.results[key];
const result =
await this.scrapItemsService.upsertScrapItems(dataArray);
console.log(result);
}
}),
);
const startOfDay = new Date();
startOfDay.setHours(0, 0, 0, 0);
const endOfDay = new Date();
endOfDay.setHours(23, 59, 59, 999);
const data = await this.scrapItemsService.scrapItemRepo.find({
where: {
updated_at: Between(startOfDay, endOfDay),
},
relations: { scrap_config: { web_bid: true } },
order: { updated_at: 'DESC' },
});
await this.mailsService.sendPlainHtml(
mails,
`Auction Items Matching Your Keywords Daily Update ${moment(new Date()).format('YYYY-MM-DD HH:mm')}`,
this.mailsService.generateProductTableHTML(data),
);
return { a: 'abc' };
}
}

View File

@ -1,6 +1,7 @@
import {
Column,
Entity,
JoinColumn,
ManyToOne,
PrimaryGeneratedColumn,
Unique,
@ -9,7 +10,7 @@ import { ScrapConfig } from './scrap-config.entity';
import { Timestamp } from './timestamp';
@Entity('scrap-items')
@Unique(['model', 'scrap_config'])
@Unique(['model', 'scrap_config_id'])
export class ScrapItem extends Timestamp {
@PrimaryGeneratedColumn('increment')
id: number;
@ -29,11 +30,15 @@ export class ScrapItem extends Timestamp {
@Column({ nullable: true, default: null })
keyword: string;
@Column({ nullable: true, default: null })
@Column({ nullable: true, default: null, type: 'float' })
current_price: number;
@Column()
scrap_config_id: number;
@ManyToOne(() => ScrapConfig, (web) => web.scrap_items, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'scrap_config_id' })
scrap_config: ScrapConfig;
}

View File

@ -1,6 +1,6 @@
import axios from 'axios';
import { ScrapInterface } from '../../scrap-interface';
import { ScrapModel } from '../../scrap-model';
import { ScrapInterface } from '../scrap-interface';
import { ScrapModel } from '../scrap-model';
import * as cheerio from 'cheerio';
import { Element } from 'domhandler';
import { ScrapItem } from '@/modules/scraps/entities/scrap-item.entity';
@ -9,7 +9,6 @@ export class GraysScrapModel extends ScrapModel {
action = async () => {
const urls = this.extractUrls();
console.log({ urls });
const results = await Promise.allSettled(
urls.map(async (item) => ({
html: (await axios.get(item.url)).data,
@ -37,6 +36,21 @@ export class GraysScrapModel extends ScrapModel {
);
};
getPriceByEl = ($: cheerio.CheerioAPI, el: Element): number | null => {
const selectors = [
'.sc-ijDOKB.sc-bStcSt.ikmQUw.eEycyP', // Single product price
'.sc-ijDOKB.ikmQUw', // Multiple product price
];
for (const selector of selectors) {
const text = $(el).find(selector).text();
const price = extractNumber(text);
if (price) return price;
}
return null;
};
getItemsInHtml = async ({
html,
keyword,
@ -59,10 +73,8 @@ export class GraysScrapModel extends ScrapModel {
model: extractModelId(url),
keyword,
url,
current_price: extractNumber(
$(el).find('.sc-ijDOKB.sc-bStcSt.ikmQUw.eEycyP').text(),
),
scrap_config: { id: this.scrap_config_id },
current_price: this.getPriceByEl($, el),
scrap_config_id: this.scrap_config_id,
} as ScrapItem;
});

View File

@ -0,0 +1,108 @@
import { ScrapItem } from '@/modules/scraps/entities/scrap-item.entity';
import { extractModelId, extractNumber } from '@/ultils';
import axios from 'axios';
import * as cheerio from 'cheerio';
import { Element } from 'domhandler';
import { ScrapModel } from '../scrap-model';
export class LangtonsScrapModel extends ScrapModel {
action = async () => {
const urls = this.extractUrls();
console.log({ urls });
const results = await Promise.allSettled(
urls.map(async (item) => ({
html: (
await axios.get(item.url, {
headers: {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36',
Accept: 'text/html',
},
maxRedirects: 5, // default là 5, bạn có thể tăng lên
})
).data,
keyword: item.keyword,
})),
);
console.log(
results.map((r) => ({
status: r.status,
reason:
r.status === 'rejected'
? (r as PromiseRejectedResult).reason.message
: undefined,
})),
);
const htmlsData = results
.filter((res) => res.status === 'fulfilled')
.map((res) => (res as PromiseFulfilledResult<any>).value);
console.log({ htmlsData });
await Promise.all(
htmlsData.map(async (cur) => {
try {
const data = await this.getItemsInHtml(cur);
const results = this.filterItemByKeyword(cur.keyword, data);
this.results[cur.keyword] = results; // hoặc push như gợi ý trên
return results;
} catch (err) {
console.error(`❌ Error with keyword ${cur.keyword}:`, err);
return []; // fallback
}
}),
);
};
getPriceByEl = ($: cheerio.CheerioAPI, el: Element): number | null => {
const selectors = [
'.sc-ijDOKB.sc-bStcSt.ikmQUw.eEycyP', // Single product price
'.sc-ijDOKB.ikmQUw', // Multiple product price
];
for (const selector of selectors) {
const text = $(el).find(selector).text();
const price = extractNumber(text);
if (price) return price;
}
return null;
};
getItemsInHtml = async ({
html,
keyword,
}: {
html: string;
keyword: string;
}) => {
const $ = cheerio.load(html);
const container = $('.row.product-grid.grid-view');
const items = container.children('div').toArray();
const results = items.map((el) => {
const url = $(el).find('a.js-pdp-link.pdp-link-anchor').attr('href');
return {
name: $(el).find('.link.js-pdp-link').text().trim(),
image_url: $(el).find('img.tile-image.loaded').attr('src'),
model: extractModelId(url),
keyword,
url,
current_price: extractNumber(
$(el).find('div.max-bid-price.price').text(),
),
scrap_config_id: this.scrap_config_id,
} as ScrapItem;
});
console.log({ results });
return results;
};
}

View File

@ -6,9 +6,15 @@ import { ScrapConfigsService } from './services/scrap-config.service';
import { ScrapConfigsController } from './controllers/scrap-config.controller';
import { TasksService } from './services/tasks.service';
import { ScrapItemsService } from './services/scrap-item-config.service';
import { MailsModule } from '../mails/mails.module';
import { BidsModule } from '../bids/bids.module';
@Module({
imports: [TypeOrmModule.forFeature([ScrapConfig, ScrapItem])],
imports: [
TypeOrmModule.forFeature([ScrapConfig, ScrapItem]),
MailsModule,
BidsModule,
],
providers: [ScrapConfigsService, TasksService, ScrapItemsService],
exports: [ScrapConfigsService, TasksService, ScrapItemsService],
controllers: [ScrapConfigsController],

View File

@ -8,7 +8,8 @@ import { CreateScrapConfigDto } from '../dto/scrap-config/create-scrap-config';
import { UpdateScrapConfigDto } from '../dto/scrap-config/update-scrap-config';
import axios from 'axios';
import { WebBid } from '@/modules/bids/entities/wed-bid.entity';
import { GraysScrapModel } from '../models/https:/www.grays.com/grays-scrap-model';
import { GraysScrapModel } from '../models/www.grays.com/grays-scrap-model';
import { LangtonsScrapModel } from '../models/www.langtons.com.au/langtons-scrap-model';
@Injectable()
export class ScrapConfigsService {

View File

@ -9,4 +9,53 @@ export class ScrapItemsService {
@InjectRepository(ScrapItem)
readonly scrapItemRepo: Repository<ScrapItem>,
) {}
async upsertScrapItems(items: ScrapItem[]) {
if (!items.length) return { inserted: 0, updated: 0 };
// Lấy keys để query bản ghi đã tồn tại
const keys = items.map((d) => ({
model: d.model,
scrap_config_id: d.scrap_config_id,
}));
// Tìm các bản ghi đã có trong DB
const existingItems = await this.scrapItemRepo.findBy(keys);
const toUpdate = [];
const toInsert = [];
items.forEach((itemData) => {
const exist = existingItems.find(
(e) =>
e.model === itemData.model &&
e.scrap_config_id === itemData.scrap_config_id,
);
if (exist) {
toUpdate.push({
...exist,
...itemData,
updated_at: new Date(),
});
} else {
toInsert.push(itemData);
}
});
// Thực hiện insert
if (toInsert.length) {
await this.scrapItemRepo.insert(toInsert);
}
// Thực hiện update
if (toUpdate.length) {
await this.scrapItemRepo.save(toUpdate);
}
return {
inserted: toInsert.length,
updated: toUpdate.length,
};
}
}

View File

@ -1,25 +1,72 @@
import { Injectable, Logger } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';
import { IsNull, Not } from 'typeorm';
import { Between, IsNull, Not } from 'typeorm';
import { ScrapConfigsService } from './scrap-config.service';
import { ScrapItemsService } from './scrap-item-config.service';
import { MailsService } from '@/modules/mails/services/mails.service';
import { ConfigsService } from '@/modules/bids/services/configs.service';
import * as moment from 'moment';
@Injectable()
export class TasksService {
private readonly logger = new Logger(TasksService.name);
constructor(private readonly scrapConfigsService: ScrapConfigsService) {}
constructor(
private readonly scrapConfigsService: ScrapConfigsService,
private readonly scrapItemsService: ScrapItemsService,
private readonly mailsService: MailsService,
private readonly configsSerivce: ConfigsService,
) {}
@Cron(CronExpression.EVERY_MINUTE)
@Cron('0 2 * * *')
async handleScraps() {
// const scrapConfigs = await this.scrapConfigsService.scrapConfigRepo.find({
// where: {
// search_url: Not(IsNull()),
// keywords: Not(IsNull()),
// },
// relations: {
// web_bid: true,
// },
// });
// const models = this.scrapConfigsService.scrapModels(scrapConfigs);
// await Promise.allSettled(models.map(async (item) => await item.action()));
const mails = (await this.configsSerivce.getConfig('MAIL_SCRAP_REPORT'))
.value;
if (!mails) return;
const scrapConfigs = await this.scrapConfigsService.scrapConfigRepo.find({
where: {
search_url: Not(IsNull()),
keywords: Not(IsNull()),
},
relations: {
web_bid: true,
},
});
const models = this.scrapConfigsService.scrapModels(scrapConfigs);
await Promise.allSettled(
models.map(async (item) => {
await item.action();
for (const key of Object.keys(item.results)) {
const dataArray = item.results[key];
const result =
await this.scrapItemsService.upsertScrapItems(dataArray);
console.log(result);
}
}),
);
const startOfDay = new Date();
startOfDay.setHours(0, 0, 0, 0);
const endOfDay = new Date();
endOfDay.setHours(23, 59, 59, 999);
const data = await this.scrapItemsService.scrapItemRepo.find({
where: {
updated_at: Between(startOfDay, endOfDay),
},
relations: { scrap_config: { web_bid: true } },
order: { updated_at: 'DESC' },
});
await this.mailsService.sendPlainHtml(
mails,
`Auction Items Matching Your Keywords Daily Update ${moment(new Date()).format('YYYY-MM-DD HH:mm')}`,
this.mailsService.generateProductTableHTML(data),
);
console.log('Send report success');
}
}