update view mail report
This commit is contained in:
parent
58d4809548
commit
5fea257b68
|
|
@ -117,7 +117,7 @@ export default function MailsConfig() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAdd = async (mail: string) => {
|
const handleAdd = async (mail: string) => {
|
||||||
const newMails = [...mails, mail];
|
const newMails = [...mails, mail.trim()];
|
||||||
|
|
||||||
open();
|
open();
|
||||||
const response = await upsertConfig({
|
const response = await upsertConfig({
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
{"createdAt":1748224707802}
|
{"createdAt":1748825131474}
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import axios from 'axios';
|
||||||
|
import { BidsService } from '../services/bids.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class HotItemApi {
|
||||||
|
constructor(
|
||||||
|
private readonly bidsService: BidsService,
|
||||||
|
private readonly configService: ConfigService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
listHotItem = async () => {
|
||||||
|
try {
|
||||||
|
const res = await axios({
|
||||||
|
method: 'GET',
|
||||||
|
baseURL: this.configService.get('NEW_ITEM_BASE_URL'),
|
||||||
|
url: '/disti/api/hotitem',
|
||||||
|
headers: {
|
||||||
|
// ...axios.defaults.headers.common,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: 'Bearer ' + this.configService.get('NEW_ITEM_TOKEN'),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.data.data || [];
|
||||||
|
} catch (error) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -33,6 +33,7 @@ import { Config } from './entities/configs.entity';
|
||||||
import { AdminConfigsController } from './controllers/admin/admin-configs.controller';
|
import { AdminConfigsController } from './controllers/admin/admin-configs.controller';
|
||||||
import { BidMetadatasService } from './services/bid-metadatas.service';
|
import { BidMetadatasService } from './services/bid-metadatas.service';
|
||||||
import { BidMetadata } from './entities/bid-metadata.entity';
|
import { BidMetadata } from './entities/bid-metadata.entity';
|
||||||
|
import { HotItemApi } from './apis/hot-item.api';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
|
@ -76,6 +77,7 @@ import { BidMetadata } from './entities/bid-metadata.entity';
|
||||||
TasksService,
|
TasksService,
|
||||||
ConfigsService,
|
ConfigsService,
|
||||||
BidMetadatasService,
|
BidMetadatasService,
|
||||||
|
HotItemApi,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
BotTelegramApi,
|
BotTelegramApi,
|
||||||
|
|
@ -83,6 +85,7 @@ import { BidMetadata } from './entities/bid-metadata.entity';
|
||||||
BidsService,
|
BidsService,
|
||||||
ConfigsService,
|
ConfigsService,
|
||||||
DashboardService,
|
DashboardService,
|
||||||
|
HotItemApi,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class BidsModule {}
|
export class BidsModule {}
|
||||||
|
|
|
||||||
|
|
@ -98,7 +98,8 @@ export class ImapService {
|
||||||
const result = verifyCode(emailContent);
|
const result = verifyCode(emailContent);
|
||||||
|
|
||||||
if (!result) {
|
if (!result) {
|
||||||
throw new Error('fetchLatestEmail: Name or Code is empty');
|
console.log('fetchLatestEmail: Name or Code is empty');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { code, name } = result;
|
const { code, name } = result;
|
||||||
|
|
@ -108,7 +109,8 @@ export class ImapService {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!webBid) {
|
if (!webBid) {
|
||||||
throw new Error('Not found web bid');
|
console.log('Not found web bid');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// send message event
|
// send message event
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,17 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Bid } from '@/modules/bids/entities/bid.entity';
|
||||||
import { MailerService } from '@nestjs-modules/mailer';
|
|
||||||
import { ScrapItem } from '@/modules/scraps/entities/scrap-item.entity';
|
import { ScrapItem } from '@/modules/scraps/entities/scrap-item.entity';
|
||||||
import {
|
import {
|
||||||
extractDomain,
|
extractDomain,
|
||||||
extractDomainSmart,
|
extractDomainSmart,
|
||||||
formatEndTime,
|
formatEndTime,
|
||||||
|
isHotItemFn,
|
||||||
isTimeReached,
|
isTimeReached,
|
||||||
} from '@/ultils';
|
} from '@/ultils';
|
||||||
import { Bid } from '@/modules/bids/entities/bid.entity';
|
import { MailerService } from '@nestjs-modules/mailer';
|
||||||
import { InjectQueue } from '@nestjs/bull';
|
import { InjectQueue } from '@nestjs/bull';
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
import { Queue } from 'bull';
|
import { Queue } from 'bull';
|
||||||
|
import * as _ from 'lodash';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class MailsService {
|
export class MailsService {
|
||||||
|
|
@ -52,17 +54,26 @@ export class MailsService {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
generateProductTableHTML(products: ScrapItem[]): string {
|
generateProductTableHTML(
|
||||||
|
products: ScrapItem[],
|
||||||
|
hotItems: { name: string }[],
|
||||||
|
): {
|
||||||
|
html: string;
|
||||||
|
hasHotItem: boolean;
|
||||||
|
} {
|
||||||
const from = process.env.MAIL_USER || 'no-reply@example.com';
|
const from = process.env.MAIL_USER || 'no-reply@example.com';
|
||||||
|
|
||||||
|
let hasHotItem = false;
|
||||||
|
|
||||||
if (!products.length) {
|
if (!products.length) {
|
||||||
return `
|
return {
|
||||||
|
html: `
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
<title>Products</title>
|
<title>Product Listing</title>
|
||||||
</head>
|
</head>
|
||||||
<body style="font-family: sans-serif; background: #f8f9fa; padding: 20px;">
|
<body style="font-family: sans-serif; background: #f8f9fa; padding: 20px;">
|
||||||
<h2 style="text-align: center; color: #333;">Product Listing</h2>
|
<h2 style="text-align: center; color: #333;">Product Listing</h2>
|
||||||
|
|
@ -70,33 +81,75 @@ export class MailsService {
|
||||||
<p style="text-align: center; color: #999; font-size: 12px; margin-top: 40px;">From: ${from}</p>
|
<p style="text-align: center; color: #999; font-size: 12px; margin-top: 40px;">From: ${from}</p>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
`;
|
`,
|
||||||
|
hasHotItem,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const rows = products
|
// Decorate products with isHotItem
|
||||||
.map(
|
const decorated = products.map((p) => {
|
||||||
(p) => `
|
const isHotItem = hotItems.some((obj) =>
|
||||||
<tr>
|
p.name.toLowerCase().includes(obj.name.toLowerCase()),
|
||||||
<td><img src="${p.image_url}" alt="Product Image" style="height: 40px; object-fit: contain; border-radius: 4px;" /></td>
|
);
|
||||||
<td>${p.name}</td>
|
if (isHotItem) hasHotItem = true;
|
||||||
<td style="font-weight: bold; color: #e03131;">${p.current_price ? '$' + p.current_price : 'None'}</td>
|
|
||||||
<td><a href="${p.url}" target="_blank" style="color: #007bff;">View</a></td>
|
return {
|
||||||
<td>${extractDomainSmart(p.scrap_config.web_bid.origin_url)}</td>
|
...p,
|
||||||
</tr>
|
isHotItem,
|
||||||
`,
|
isNew:
|
||||||
)
|
new Date(p.created_at).getTime() === new Date(p.updated_at).getTime(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Separate hot and non-hot items, preserving original order
|
||||||
|
const hotItemsFirst = [
|
||||||
|
...decorated.filter((p) => p.isHotItem),
|
||||||
|
...decorated.filter((p) => !p.isHotItem),
|
||||||
|
];
|
||||||
|
|
||||||
|
const rows = hotItemsFirst
|
||||||
|
.map((p) => {
|
||||||
|
const isHotItem = isHotItemFn(p, hotItems);
|
||||||
|
|
||||||
|
if (isHotItem) {
|
||||||
|
hasHotItem = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isNew =
|
||||||
|
new Date(p.created_at).getTime() === new Date(p.updated_at).getTime();
|
||||||
|
|
||||||
|
const nameStyle = isNew ? 'color: #2f9e44; font-weight: bold;' : '';
|
||||||
|
const namePrefix = isHotItem
|
||||||
|
? '<span style="font-weight: bold; color: #e03131;">[HOT ITEM]</span> '
|
||||||
|
: '';
|
||||||
|
|
||||||
|
return `
|
||||||
|
<tr ${isHotItem ? 'style="background-color: #ff848496;margin-bottom: 4px;"' : 'margin-bottom: 4px;'}>
|
||||||
|
<td style="display:flex;max-width: 60px;"><img src="${p.image_url}" alt="Product Image" style="height: 60px !important; object-fit: contain; border-radius: 4px;" /></td>
|
||||||
|
<td style="${nameStyle}">${namePrefix}${p.name}</td>
|
||||||
|
<td style="font-weight: bold; color: #e03131;">${p.current_price ? '$' + p.current_price : 'None'}</td>
|
||||||
|
<td><a href="${p.url}" target="_blank" style="color: #007bff;">View</a></td>
|
||||||
|
<td>${extractDomainSmart(p.scrap_config.web_bid.origin_url)}</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
})
|
||||||
.join('');
|
.join('');
|
||||||
|
|
||||||
return `
|
const title = hasHotItem
|
||||||
|
? '<span style="font-weight: bold; color: #e03131;">[HOT ITEMS]</span> Product Listing'
|
||||||
|
: 'Product Listing';
|
||||||
|
|
||||||
|
return {
|
||||||
|
html: `
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
<title>Products</title>
|
<title>${title}</title>
|
||||||
</head>
|
</head>
|
||||||
<body style="font-family: sans-serif; background: #f8f9fa; padding: 20px;">
|
<body style="font-family: sans-serif; background: #f8f9fa; padding: 20px;">
|
||||||
<h2 style="text-align: center; color: #333;">Product Listing</h2>
|
<h2 style="text-align: center; color: #333;">${title}</h2>
|
||||||
<div style="overflow-x: auto;">
|
<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);">
|
<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;">
|
<thead style="background: #f1f3f5;">
|
||||||
|
|
@ -116,7 +169,9 @@ export class MailsService {
|
||||||
<p style="text-align: center; color: #999; font-size: 12px; margin-top: 40px;">From: ${from}</p>
|
<p style="text-align: center; color: #999; font-size: 12px; margin-top: 40px;">From: ${from}</p>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
`;
|
`,
|
||||||
|
hasHotItem,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
getAuctionStatusEmailContent(bid: Bid): string {
|
getAuctionStatusEmailContent(bid: Bid): string {
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,11 @@ import { DashboardService } from '@/modules/bids/services/dashboard.service';
|
||||||
import { MailsService } from '@/modules/mails/services/mails.service';
|
import { MailsService } from '@/modules/mails/services/mails.service';
|
||||||
import { delay } from '@/ultils';
|
import { delay } from '@/ultils';
|
||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import { Cron } from '@nestjs/schedule';
|
import { Cron, CronExpression } from '@nestjs/schedule';
|
||||||
import * as moment from 'moment';
|
import * as moment from 'moment';
|
||||||
import { Between } from 'typeorm';
|
import { Between } from 'typeorm';
|
||||||
import { ScrapItemsService } from './scrap-item-config.service';
|
import { ScrapItemsService } from './scrap-item-config.service';
|
||||||
|
import { HotItemApi } from '@/modules/bids/apis/hot-item.api';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TasksService {
|
export class TasksService {
|
||||||
|
|
@ -17,6 +18,7 @@ export class TasksService {
|
||||||
private readonly mailsService: MailsService,
|
private readonly mailsService: MailsService,
|
||||||
private readonly configsSerivce: ConfigsService,
|
private readonly configsSerivce: ConfigsService,
|
||||||
private readonly dashboardService: DashboardService,
|
private readonly dashboardService: DashboardService,
|
||||||
|
private readonly hotItemApi: HotItemApi,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async runProcessAndSendReport(processName: string) {
|
async runProcessAndSendReport(processName: string) {
|
||||||
|
|
@ -71,13 +73,20 @@ export class TasksService {
|
||||||
updated_at: Between(startOfDay, endOfDay),
|
updated_at: Between(startOfDay, endOfDay),
|
||||||
},
|
},
|
||||||
relations: { scrap_config: { web_bid: true } },
|
relations: { scrap_config: { web_bid: true } },
|
||||||
order: { updated_at: 'DESC' },
|
order: { updated_at: 'ASC' },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const hotItems = await this.hotItemApi.listHotItem();
|
||||||
|
|
||||||
|
const { hasHotItem, html } = this.mailsService.generateProductTableHTML(
|
||||||
|
data,
|
||||||
|
hotItems,
|
||||||
|
);
|
||||||
|
|
||||||
await this.mailsService.sendHtmlMailJob({
|
await this.mailsService.sendHtmlMailJob({
|
||||||
to: mails,
|
to: mails,
|
||||||
subject: `Auction Items Matching Your Keywords – Daily Update ${moment().format('YYYY-MM-DD HH:mm')}`,
|
subject: `${hasHotItem ? '[HOT ITEMS] ' : ''}Auction Items Matching Your Keywords – Daily Update ${moment().format('YYYY-MM-DD HH:mm')}`,
|
||||||
html: this.mailsService.generateProductTableHTML(data),
|
html: html,
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('Report mail sent successfully.');
|
console.log('Report mail sent successfully.');
|
||||||
|
|
@ -86,7 +95,8 @@ export class TasksService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Cron('58 5 * * *')
|
// @Cron('59 5 * * *')
|
||||||
|
// @Cron(CronExpression.EVERY_MINUTE)
|
||||||
async handleScraps() {
|
async handleScraps() {
|
||||||
const processName = 'scrape-data-keyword';
|
const processName = 'scrape-data-keyword';
|
||||||
await this.runProcessAndSendReport(processName);
|
await this.runProcessAndSendReport(processName);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { Bid } from '@/modules/bids/entities/bid.entity';
|
import { Bid } from '@/modules/bids/entities/bid.entity';
|
||||||
|
import { ScrapItem } from '@/modules/scraps/entities/scrap-item.entity';
|
||||||
import * as moment from 'moment';
|
import * as moment from 'moment';
|
||||||
|
|
||||||
export function extractModelId(url: string): string | null {
|
export function extractModelId(url: string): string | null {
|
||||||
|
|
@ -215,3 +216,9 @@ export function formatEndTime(
|
||||||
|
|
||||||
export const delay = (ms: number) =>
|
export const delay = (ms: number) =>
|
||||||
new Promise((resolve) => setTimeout(resolve, ms));
|
new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
|
||||||
|
export const isHotItemFn = (item: ScrapItem, hotItems: any[]) => {
|
||||||
|
return hotItems.some((obj) =>
|
||||||
|
item.name.toLowerCase().includes(obj.name.toLowerCase()),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ const init = async () => {
|
||||||
try {
|
try {
|
||||||
await model.action();
|
await model.action();
|
||||||
|
|
||||||
|
console.log(`Loaded model [${model?.web_bid?.origin_url || ""}]`);
|
||||||
for (const key of Object.keys(model.results)) {
|
for (const key of Object.keys(model.results)) {
|
||||||
const dataArray = model.results[key];
|
const dataArray = model.results[key];
|
||||||
const result = await upsertScapeItems(dataArray);
|
const result = await upsertScapeItems(dataArray);
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,12 @@
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import browser from "../system/browser.js";
|
import { extractModelId } from "../system/ultils.js";
|
||||||
import { extractModelId, extractNumber } from "../system/ultils.js";
|
|
||||||
import { ScrapModel } from "./scrap-model.js";
|
import { ScrapModel } from "./scrap-model.js";
|
||||||
|
|
||||||
export class AllbidsScrapModel extends ScrapModel {
|
export class AllbidsScrapModel extends ScrapModel {
|
||||||
action = async () => {
|
action = async () => {
|
||||||
const urlsData = this.extractUrls();
|
const urlsData = this.extractUrls();
|
||||||
|
|
||||||
console.log({ urlsData });
|
|
||||||
|
|
||||||
for (let item of urlsData) {
|
for (let item of urlsData) {
|
||||||
// await this.page.goto(item.url);
|
|
||||||
|
|
||||||
// const data = await this.getItemsInHtml(item);
|
|
||||||
|
|
||||||
// const results = this.filterItemByKeyword(item.keyword, data);
|
|
||||||
|
|
||||||
// this.results[item.keyword] = results;
|
|
||||||
|
|
||||||
// console.log({ results: this.results });
|
|
||||||
|
|
||||||
const { data } = await axios({
|
const { data } = await axios({
|
||||||
url: item.url,
|
url: item.url,
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
|
@ -37,14 +24,14 @@ export class AllbidsScrapModel extends ScrapModel {
|
||||||
|
|
||||||
const auctions = data?.auctions || [];
|
const auctions = data?.auctions || [];
|
||||||
|
|
||||||
const mappedData = auctions.map((item) => {
|
const mappedData = auctions.map((auction) => {
|
||||||
return {
|
return {
|
||||||
url: item.AucDetailsUrlLink,
|
url: auction.AucDetailsUrlLink,
|
||||||
image_url: item.aucThumbnailUrl,
|
image_url: auction.aucThumbnailUrl,
|
||||||
name: item.aucTitle,
|
name: auction.aucTitle,
|
||||||
keyword: data.keyword,
|
keyword: item.keyword,
|
||||||
model: extractModelId(item.AucDetailsUrlLink),
|
model: extractModelId(auction.AucDetailsUrlLink),
|
||||||
current_price: item.aucCurrentBid,
|
current_price: auction.aucCurrentBid,
|
||||||
scrap_config_id: this.scrap_config_id,
|
scrap_config_id: this.scrap_config_id,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue