update view mail report

This commit is contained in:
Admin 2025-06-03 10:53:33 +07:00
parent 58d4809548
commit 5fea257b68
10 changed files with 149 additions and 53 deletions

View File

@ -117,7 +117,7 @@ export default function MailsConfig() {
};
const handleAdd = async (mail: string) => {
const newMails = [...mails, mail];
const newMails = [...mails, mail.trim()];
open();
const response = await upsertConfig({

View File

@ -1 +1 @@
{"createdAt":1748224707802}
{"createdAt":1748825131474}

View File

@ -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 [];
}
};
}

View File

@ -33,6 +33,7 @@ import { Config } from './entities/configs.entity';
import { AdminConfigsController } from './controllers/admin/admin-configs.controller';
import { BidMetadatasService } from './services/bid-metadatas.service';
import { BidMetadata } from './entities/bid-metadata.entity';
import { HotItemApi } from './apis/hot-item.api';
@Module({
imports: [
@ -76,6 +77,7 @@ import { BidMetadata } from './entities/bid-metadata.entity';
TasksService,
ConfigsService,
BidMetadatasService,
HotItemApi,
],
exports: [
BotTelegramApi,
@ -83,6 +85,7 @@ import { BidMetadata } from './entities/bid-metadata.entity';
BidsService,
ConfigsService,
DashboardService,
HotItemApi,
],
})
export class BidsModule {}

View File

@ -98,7 +98,8 @@ export class ImapService {
const result = verifyCode(emailContent);
if (!result) {
throw new Error('fetchLatestEmail: Name or Code is empty');
console.log('fetchLatestEmail: Name or Code is empty');
return;
}
const { code, name } = result;
@ -108,7 +109,8 @@ export class ImapService {
});
if (!webBid) {
throw new Error('Not found web bid');
console.log('Not found web bid');
return;
}
// send message event

View File

@ -1,15 +1,17 @@
import { Injectable } from '@nestjs/common';
import { MailerService } from '@nestjs-modules/mailer';
import { Bid } from '@/modules/bids/entities/bid.entity';
import { ScrapItem } from '@/modules/scraps/entities/scrap-item.entity';
import {
extractDomain,
extractDomainSmart,
formatEndTime,
isHotItemFn,
isTimeReached,
} from '@/ultils';
import { Bid } from '@/modules/bids/entities/bid.entity';
import { MailerService } from '@nestjs-modules/mailer';
import { InjectQueue } from '@nestjs/bull';
import { Injectable } from '@nestjs/common';
import { Queue } from 'bull';
import * as _ from 'lodash';
@Injectable()
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';
let hasHotItem = false;
if (!products.length) {
return `
return {
html: `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Products</title>
<title>Product Listing</title>
</head>
<body style="font-family: sans-serif; background: #f8f9fa; padding: 20px;">
<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>
</body>
</html>
`;
`,
hasHotItem,
};
}
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 ? '$' + 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>
`,
)
// Decorate products with isHotItem
const decorated = products.map((p) => {
const isHotItem = hotItems.some((obj) =>
p.name.toLowerCase().includes(obj.name.toLowerCase()),
);
if (isHotItem) hasHotItem = true;
return {
...p,
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('');
return `
const title = hasHotItem
? '<span style="font-weight: bold; color: #e03131;">[HOT ITEMS]</span> Product Listing'
: 'Product Listing';
return {
html: `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Products</title>
<title>${title}</title>
</head>
<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;">
<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;">
@ -116,7 +169,9 @@ export class MailsService {
<p style="text-align: center; color: #999; font-size: 12px; margin-top: 40px;">From: ${from}</p>
</body>
</html>
`;
`,
hasHotItem,
};
}
getAuctionStatusEmailContent(bid: Bid): string {

View File

@ -3,10 +3,11 @@ import { DashboardService } from '@/modules/bids/services/dashboard.service';
import { MailsService } from '@/modules/mails/services/mails.service';
import { delay } from '@/ultils';
import { Injectable, Logger } from '@nestjs/common';
import { Cron } from '@nestjs/schedule';
import { Cron, CronExpression } from '@nestjs/schedule';
import * as moment from 'moment';
import { Between } from 'typeorm';
import { ScrapItemsService } from './scrap-item-config.service';
import { HotItemApi } from '@/modules/bids/apis/hot-item.api';
@Injectable()
export class TasksService {
@ -17,6 +18,7 @@ export class TasksService {
private readonly mailsService: MailsService,
private readonly configsSerivce: ConfigsService,
private readonly dashboardService: DashboardService,
private readonly hotItemApi: HotItemApi,
) {}
async runProcessAndSendReport(processName: string) {
@ -71,13 +73,20 @@ export class TasksService {
updated_at: Between(startOfDay, endOfDay),
},
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({
to: mails,
subject: `Auction Items Matching Your Keywords Daily Update ${moment().format('YYYY-MM-DD HH:mm')}`,
html: this.mailsService.generateProductTableHTML(data),
subject: `${hasHotItem ? '[HOT ITEMS] ' : ''}Auction Items Matching Your Keywords Daily Update ${moment().format('YYYY-MM-DD HH:mm')}`,
html: html,
});
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() {
const processName = 'scrape-data-keyword';
await this.runProcessAndSendReport(processName);

View File

@ -1,4 +1,5 @@
import { Bid } from '@/modules/bids/entities/bid.entity';
import { ScrapItem } from '@/modules/scraps/entities/scrap-item.entity';
import * as moment from 'moment';
export function extractModelId(url: string): string | null {
@ -215,3 +216,9 @@ export function formatEndTime(
export const delay = (ms: number) =>
new Promise((resolve) => setTimeout(resolve, ms));
export const isHotItemFn = (item: ScrapItem, hotItems: any[]) => {
return hotItems.some((obj) =>
item.name.toLowerCase().includes(obj.name.toLowerCase()),
);
};

View File

@ -22,6 +22,7 @@ const init = async () => {
try {
await model.action();
console.log(`Loaded model [${model?.web_bid?.origin_url || ""}]`);
for (const key of Object.keys(model.results)) {
const dataArray = model.results[key];
const result = await upsertScapeItems(dataArray);

View File

@ -1,25 +1,12 @@
import axios from "axios";
import browser from "../system/browser.js";
import { extractModelId, extractNumber } from "../system/ultils.js";
import { extractModelId } from "../system/ultils.js";
import { ScrapModel } from "./scrap-model.js";
export class AllbidsScrapModel extends ScrapModel {
action = async () => {
const urlsData = this.extractUrls();
console.log({ 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({
url: item.url,
method: "POST",
@ -37,14 +24,14 @@ export class AllbidsScrapModel extends ScrapModel {
const auctions = data?.auctions || [];
const mappedData = auctions.map((item) => {
const mappedData = auctions.map((auction) => {
return {
url: item.AucDetailsUrlLink,
image_url: item.aucThumbnailUrl,
name: item.aucTitle,
keyword: data.keyword,
model: extractModelId(item.AucDetailsUrlLink),
current_price: item.aucCurrentBid,
url: auction.AucDetailsUrlLink,
image_url: auction.aucThumbnailUrl,
name: auction.aucTitle,
keyword: item.keyword,
model: extractModelId(auction.AucDetailsUrlLink),
current_price: auction.aucCurrentBid,
scrap_config_id: this.scrap_config_id,
};
});