This commit is contained in:
nkhangg 2025-03-19 14:38:56 +07:00
parent fff5447ce1
commit b1e34be9ce
24 changed files with 581 additions and 147 deletions

View File

@ -0,0 +1,29 @@
import { generateNestParams, handleError, handleSuccess } from '.';
import axios from '../lib/axios';
const BASE_URL = 'send-message-histories';
export const getSendMessageHistories = async (params: Record<string, string | number>) => {
return await axios({
url: BASE_URL,
params: generateNestParams(params),
withCredentials: true,
method: 'GET',
});
};
export const sendMessageHistoryTest = async () => {
try {
const { data } = await axios({
url: `${BASE_URL}/send-test`,
withCredentials: true,
method: 'POST',
});
handleSuccess(data);
return data;
} catch (error) {
handleError(error);
}
};

View File

@ -1,6 +1,6 @@
import { ActionIcon, Badge, Box, Menu, Text, Tooltip } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
import { IconAd, IconAdOff, IconEdit, IconHistory, IconMenu, IconTrash } from '@tabler/icons-react';
import { IconAd, IconAdOff, IconEdit, IconHammer, IconHistory, IconMenu, IconTrash } from '@tabler/icons-react';
import _ from 'lodash';
import { useMemo, useRef, useState } from 'react';
import { deleteBid, deletesBid, getBids, toggleBid } from '../apis/bid';
@ -39,11 +39,7 @@ export default function Bids() {
title: 'Model',
typeFilter: 'text',
},
// {
// key: 'quantity',
// title: 'Qty',
// typeFilter: 'number',
// },
{
key: 'plus_price',
title: 'Plus price',
@ -253,7 +249,7 @@ export default function Bids() {
setClickData(row);
historiesGraysApiModel.open();
}}
leftSection={<IconHistory size={14} />}
leftSection={<IconHammer size={14} />}
>
Bids
</Menu.Item>

View File

@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { useEffect, useState } from 'react';
import { Box, Title } from '@mantine/core';
import { Box, Text, Title } from '@mantine/core';
import io from 'socket.io-client';
import { WorkingPage } from '../components/dashboard';
import { IBid, IWebBid } from '../system/type';
@ -71,9 +71,13 @@ export default function DashBoard() {
</Title>
<Box className="grid grid-cols-4 gap-4">
{workingData.map((item, index) => (
<WorkingPage socket={socket} data={item} key={item.id + index} />
))}
{workingData.length > 0 && workingData.map((item, index) => <WorkingPage socket={socket} data={item} key={item.id + index} />)}
{workingData.length <= 0 && (
<Box className="flex items-center justify-center col-span-4">
<Text>No Pages</Text>
</Box>
)}
</Box>
</Box>
);

View File

@ -0,0 +1,89 @@
import { Box, Text } from '@mantine/core';
import { useMemo, useRef } from 'react';
import { getSendMessageHistories, sendMessageHistoryTest } from '../apis/send-message-histories';
import Table from '../lib/table/table';
import { IColumn, TRefTableFn } from '../lib/table/type';
import { ISendMessageHistory } from '../system/type';
import { formatTime } from '../utils';
export default function SendMessageHistories() {
const refTableFn: TRefTableFn<ISendMessageHistory> = useRef({});
const columns: IColumn<ISendMessageHistory>[] = [
{
key: 'id',
title: 'ID',
typeFilter: 'text',
},
{
key: 'bid',
title: 'Product name',
typeFilter: 'none',
renderRow(row) {
return <Text>{row.bid?.name || 'None'}</Text>;
},
},
{
key: 'message',
title: 'Message',
typeFilter: 'text',
renderRow(row) {
return <Box className="max-w-[500px]" dangerouslySetInnerHTML={{ __html: row.message }}></Box>;
},
},
{
key: 'created_at',
title: 'Create at',
typeFilter: 'none',
renderRow(row) {
return <span className="text-sm">{formatTime(row.created_at)}</span>;
},
},
];
const table = useMemo(() => {
return (
<Table
actionsOptions={{
actions: [
{
key: 'send-test',
title: 'Send test',
callback: async () => {
await sendMessageHistoryTest();
},
},
],
}}
refTableFn={refTableFn}
striped
showLoading={true}
highlightOnHover
styleDefaultHead={{
justifyContent: 'flex-start',
width: 'fit-content',
}}
options={{
query: getSendMessageHistories,
pathToData: 'data.data',
keyOptions: {
last_page: 'lastPage',
per_page: 'perPage',
from: 'from',
to: 'to',
total: 'total',
},
}}
rows={[]}
withColumnBorders
showChooses={true}
withTableBorder
columns={columns}
rowKey="id"
/>
);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return <Box>{table}</Box>;
}

View File

@ -1,11 +1,13 @@
import { IconHammer, IconHome2, IconOutlet, IconPageBreak } from '@tabler/icons-react';
import { IconHammer, IconHome2, IconMessage, IconOutlet, IconPageBreak } from '@tabler/icons-react';
import { Bids, Dashboard, OutBidsLog } from '../pages';
import WebBids from '../pages/web-bids';
import SendMessageHistories from '../pages/send-message-histories';
export default class Links {
public static DASHBOARD = '/dashboard';
public static BIDS = '/bids';
public static WEBS = '/webs';
public static OUT_BIDS_LOG = '/out-bids-log';
public static SEND_MESSAGE_HISTORIES = '/send-message-histories';
public static HOME = '/';
public static LOGIN = '/login';
@ -35,5 +37,11 @@ export default class Links {
icon: IconOutlet,
element: OutBidsLog,
},
{
path: this.SEND_MESSAGE_HISTORIES,
title: 'Send message histories',
icon: IconMessage,
element: SendMessageHistories,
},
];
}

View File

@ -61,3 +61,8 @@ export interface IPermission extends ITimestamp {
name: string;
description: string;
}
export interface ISendMessageHistory extends ITimestamp {
id: number;
message: string;
bid: IBid;
}

View File

@ -4,6 +4,7 @@ import axios from 'axios';
import { escapeMarkdownV2 } from 'src/ultils';
import { Bid } from '../entities/bid.entity';
import * as dayjs from 'dayjs';
import { SendMessageHistoriesService } from '../services/send-message-histories.service';
@Injectable()
export class BotTelegramApi {
@ -63,12 +64,18 @@ export class BotTelegramApi {
}
}
async sendBidInfo(bid: Bid): Promise<void> {
const text = this.formatBidMessage(bid);
async sendBidInfo(bid: Bid): Promise<boolean> {
try {
const text = this.formatBidMessage(bid);
console.log(text);
await this.sendMessage(text, {
parse_mode: 'HTML',
});
await this.sendMessage(text, {
parse_mode: 'HTML',
});
return true;
} catch (error) {
console.log('SEND MESSAGE FAILURE');
return false;
}
}
}

View File

@ -19,10 +19,19 @@ import { OutBidLogsService } from './services/out-bid-logs.service';
import { WebBidsService } from './services/web-bids.service';
import { BotTelegramApi } from './apis/bot-telegram.api';
import { GraysApi } from './apis/grays.api';
import { SendMessageHistory } from './entities/send-message-histories.entity';
import { SendMessageHistoriesService } from './services/send-message-histories.service';
import { AdminSendMessageHistoriesController } from './controllers/admin/admin-send-message-histories.controller';
@Module({
imports: [
TypeOrmModule.forFeature([Bid, BidHistory, OutBidLog, WebBid]),
TypeOrmModule.forFeature([
Bid,
BidHistory,
OutBidLog,
WebBid,
SendMessageHistory,
]),
EventEmitterModule.forRoot({
wildcard: true,
}),
@ -35,6 +44,7 @@ import { GraysApi } from './apis/grays.api';
AdminBidsController,
AdminOutBidLogsController,
AdminWebBidsController,
AdminSendMessageHistoriesController,
],
providers: [
BidsService,
@ -44,6 +54,7 @@ import { GraysApi } from './apis/grays.api';
WebBidsService,
BotTelegramApi,
GraysApi,
SendMessageHistoriesService,
],
})
export class BidsModule {}

View File

@ -0,0 +1,20 @@
import { Controller, Get, Post } from '@nestjs/common';
import { Paginate, PaginateQuery } from 'nestjs-paginate';
import { SendMessageHistoriesService } from '../../services/send-message-histories.service';
@Controller('admin/send-message-histories')
export class AdminSendMessageHistoriesController {
constructor(
private readonly sendMessageService: SendMessageHistoriesService,
) {}
@Get()
async index(@Paginate() query: PaginateQuery) {
return await this.sendMessageService.index(query);
}
@Post('send-test')
async sendTest() {
return await this.sendMessageService.sendTestMessage();
}
}

View File

@ -8,6 +8,7 @@ import {
import { Timestamp } from './timestamp';
import { BidHistory } from './bid-history.entity';
import { WebBid } from './wed-bid.entity';
import { SendMessageHistory } from './send-message-histories.entity';
@Entity('bids')
export class Bid extends Timestamp {
@ -58,6 +59,11 @@ export class Bid extends Timestamp {
})
histories: BidHistory[];
@OneToMany(() => SendMessageHistory, (sendMessage) => sendMessage.bid, {
cascade: true,
})
sendMessageHistories: SendMessageHistory[];
@ManyToOne(() => WebBid, (web) => web.children, { onDelete: 'CASCADE' })
web_bid: WebBid;
}

View File

@ -0,0 +1,17 @@
import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
import { Timestamp } from './timestamp';
import { Bid } from './bid.entity';
@Entity('send_message_histories')
export class SendMessageHistory extends Timestamp {
@PrimaryGeneratedColumn('increment')
id: number;
@Column({ default: null, nullable: true, type: 'text' })
message: string;
@ManyToOne(() => Bid, (bid) => bid.sendMessageHistories, {
onDelete: 'CASCADE',
})
bid: Bid;
}

View File

@ -12,6 +12,7 @@ import { BidHistory } from '../entities/bid-history.entity';
import { Bid } from '../entities/bid.entity';
import { CreateBidHistoryDto } from '../dto/bid-history/create-bid-history.dto';
import { BotTelegramApi } from '../apis/bot-telegram.api';
import { SendMessageHistoriesService } from './send-message-histories.service';
@Injectable()
export class BidHistoriesService {
@ -21,6 +22,7 @@ export class BidHistoriesService {
@InjectRepository(Bid)
readonly bidsRepo: Repository<Bid>,
private readonly botTelegramApi: BotTelegramApi,
readonly sendMessageHistoriesService: SendMessageHistoriesService,
) {}
async index() {
@ -72,7 +74,14 @@ export class BidHistoriesService {
this.bidsRepo.update(bid_id, { first_bid: false });
}
this.botTelegramApi.sendBidInfo({ ...bid, histories: response });
const botData = { ...bid, histories: response };
this.botTelegramApi.sendBidInfo(botData);
this.sendMessageHistoriesService.sendMessageRepo.save({
message: this.botTelegramApi.formatBidMessage(botData),
bid,
});
return AppResponse.toResponse(plainToClass(BidHistory, response));
}

View File

@ -205,9 +205,12 @@ export class BidsService {
bid.status = 'out-bid';
}
console.log('Update ' + id);
const result = await this.bidsRepo.save({
...bid,
...data,
updated_at: new Date(),
});
this.emitAllBidEvent();

View File

@ -0,0 +1,106 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { OutBidLog } from '../entities/out-bid-log.entity';
import AppResponse from 'src/response/app-response';
import {
FilterOperator,
FilterSuffix,
paginate,
PaginateQuery,
} from 'nestjs-paginate';
import { Column } from 'nestjs-paginate/lib/helper';
import { CreateOutBidLogDto } from '../dto/out-bid-log/create-out-bid-log.dto';
import { SendMessageHistory } from '../entities/send-message-histories.entity';
import { BotTelegramApi } from '../apis/bot-telegram.api';
import { BidsService } from './bids.service';
@Injectable()
export class SendMessageHistoriesService {
constructor(
@InjectRepository(SendMessageHistory)
readonly sendMessageRepo: Repository<SendMessageHistory>,
private readonly botTelegramApi: BotTelegramApi,
private readonly bidsService: BidsService,
) {}
async index(query: PaginateQuery) {
const filterableColumns: {
[key in Column<SendMessageHistory> | (string & {})]?:
| (FilterOperator | FilterSuffix)[]
| true;
} = {
id: true,
message: true,
'bid.name': true,
'bid.lot_id': true,
};
query.filter = AppResponse.processFilters(query.filter, filterableColumns);
const data = await paginate(query, this.sendMessageRepo, {
sortableColumns: ['id', 'message', 'bid.name'],
searchableColumns: ['id', 'message', 'bid.name'],
defaultLimit: 15,
filterableColumns,
defaultSortBy: [['id', 'DESC']],
relations: {
bid: true,
},
maxLimit: 100,
});
return AppResponse.toPagination<SendMessageHistory>(
data,
true,
SendMessageHistory,
);
}
async sendTestMessage() {
const date = new Date().toUTCString();
const mockBid = this.bidsService.bidsRepo.create({
id: 12345,
max_price: 5000,
reserve_price: 1000,
current_price: 1200,
name: 'Laptop Gaming ASUS ROG',
quantity: 1,
url: 'https://example.com/product/12345',
model: 'ROG Strix G15',
lot_id: 'LOT-67890',
plus_price: 50,
close_time: date,
start_bid_time: date,
first_bid: false,
status: 'biding',
histories: [
{
id: 1,
price: 1000,
created_at: date,
updated_at: date,
},
{
id: 2,
price: 1100,
created_at: date,
updated_at: date,
},
{
id: 3,
price: 1200,
created_at: date,
updated_at: date,
},
],
created_at: date,
updated_at: date,
});
const result = await this.botTelegramApi.sendBidInfo(mockBid);
return AppResponse.toResponse(result);
}
}

View File

@ -1,14 +1,9 @@
{
"AuctionOutBidLots": [
{
"Id": "23464489",
"Sku": "0002-2566250",
"Bid": "AU $59"
},
{
"Id": "23478598",
"Sku": "0153-3032503",
"Bid": "AU $12"
"Id": "23482715",
"Sku": "0045-10734155",
"Bid": "AU $50"
}
],
"NumOfLosingBids": 3

View File

@ -1,7 +1,7 @@
import 'dotenv/config';
import _ from 'lodash';
import { io } from 'socket.io-client';
import { createApiBid, createBidProduct, deleteProfile } from './service/app-service.js';
import { createApiBid, createBidProduct, deleteProfile, shouldUpdateProductTab } from './service/app-service.js';
import browser from './system/browser.js';
import configs from './system/config.js';
import { isTimeReached, safeClosePage } from './system/utils.js';
@ -48,6 +48,84 @@ const handleUpdateProductTabs = (data) => {
MANAGER_BIDS = newDataManager;
};
// const tracking = async () => {
// if (_INTERVAL_TRACKING_ID) {
// clearInterval(_INTERVAL_TRACKING_ID);
// _INTERVAL_TRACKING_ID = null;
// }
// _INTERVAL_TRACKING_ID = setInterval(async () => {
// const productTabs = _.flatMap(MANAGER_BIDS, 'children');
// for (const productTab of productTabs) {
// if (!productTab.parent_browser_context) {
// const parent = _.find(MANAGER_BIDS, { id: productTab.web_bid.id });
// productTab.parent_browser_context = parent.browser_context;
// if (!productTab.parent_browser_context) {
// console.log(`🔄 Waiting for parent process to start... (Product ID: ${productTab.id})`);
// continue;
// }
// }
// if (!productTab.first_bid) {
// console.log(`🎯 Tracking out-bid event for Product ID: ${productTab.id}`);
// const updatedAt = new Date(productTab.updated_at).getTime();
// const now = Date.now();
// if (!productTab.page_context) {
// await productTab.puppeteer_connect();
// }
// if (productTab.page_context.url() !== productTab.url) {
// await productTab.gotoLink();
// }
// if (now - updatedAt < ONE_MINUTE) {
// console.log(`⏳ Product ID: ${productTab.id} was updated recently. Skipping update.`);
// }
// await productTab.update();
// console.log(`🔄 Updating Product ID: ${productTab.id}...`);
// continue;
// }
// if (productTab.start_bid_time && !isTimeReached(productTab.start_bid_time)) {
// console.log(`⏳ Not yet time to bid. Skipping Product ID: ${productTab.id}`);
// const updatedAt = new Date(productTab.updated_at).getTime();
// const now = Date.now();
// if (!productTab.page_context) {
// await productTab.puppeteer_connect();
// }
// if (productTab.page_context.url() !== productTab.url) {
// await productTab.gotoLink();
// }
// if (now - updatedAt < ONE_MINUTE) {
// console.log(`⏳ Product ID: ${productTab.id} was updated recently. Skipping update.`);
// }
// await productTab.update();
// continue;
// }
// if (!productTab.page_context) {
// console.log(`🔌 Connecting to page for Product ID: ${productTab.id}`);
// await productTab.puppeteer_connect();
// }
// console.log(`🚀 Executing action for Product ID: ${productTab.id}`);
// await productTab.action();
// }
// }, configs.AUTO_TRACKING_DELAY);
// };
const tracking = async () => {
if (_INTERVAL_TRACKING_ID) {
clearInterval(_INTERVAL_TRACKING_ID);
@ -58,10 +136,10 @@ const tracking = async () => {
const productTabs = _.flatMap(MANAGER_BIDS, 'children');
for (const productTab of productTabs) {
// Tìm parent context nếu chưa có
if (!productTab.parent_browser_context) {
const parent = _.find(MANAGER_BIDS, { id: productTab.web_bid.id });
productTab.parent_browser_context = parent.browser_context;
productTab.parent_browser_context = parent?.browser_context;
if (!productTab.parent_browser_context) {
console.log(`🔄 Waiting for parent process to start... (Product ID: ${productTab.id})`);
@ -69,85 +147,43 @@ const tracking = async () => {
}
}
if (!productTab.first_bid) {
console.log(`🎯 Tracking out-bid event for Product ID: ${productTab.id}`);
if (!productTab.page_context) {
console.log(`🔌 Establishing connection for Product ID: ${productTab.id}`);
await productTab.puppeteer_connect();
await productTab.handleTakeWorkSnapshot();
}
continue;
}
if (productTab.start_bid_time && !isTimeReached(productTab.start_bid_time)) {
console.log(`⏳ Not yet time to bid. Skipping Product ID: ${productTab.id}`);
continue;
}
// Kết nối Puppeteer nếu chưa có page_context
if (!productTab.page_context) {
console.log(`🔌 Connecting to page for Product ID: ${productTab.id}`);
await productTab.puppeteer_connect();
}
// Nếu URL thay đổi, điều hướng đến URL mới
if (productTab.page_context.url() !== productTab.url) {
await productTab.gotoLink();
}
// Kiểm tra nếu cần cập nhật trước khi gọi update()
if (shouldUpdateProductTab(productTab)) {
console.log(`🔄 Updating Product ID: ${productTab.id}...`);
await productTab.update();
} else {
console.log(`⏳ Product ID: ${productTab.id} was updated recently. Skipping update.`);
}
// Nếu chưa có first_bid (trạng thái chưa đặt giá)
if (!productTab.first_bid) {
console.log(`🎯 Tracking out-bid event for Product ID: ${productTab.id}`);
continue;
}
// Nếu chưa đến giờ bid
if (productTab.start_bid_time && !isTimeReached(productTab.start_bid_time)) {
console.log(`⏳ Not yet time to bid. Skipping Product ID: ${productTab.id}`);
continue;
}
console.log(`🚀 Executing action for Product ID: ${productTab.id}`);
await productTab.action();
}
}, configs.AUTO_TRACKING_DELAY);
};
// const clearLazyTab = async () => {
// if (_CLEAR_LAZY_TAB_ID) {
// clearInterval(_CLEAR_LAZY_TAB_ID);
// _CLEAR_LAZY_TAB_ID = null; // Reset tránh memory leak
// }
// try {
// _CLEAR_LAZY_TAB_ID = setInterval(async () => {
// if (!browser) {
// console.warn('⚠️ Browser is not available.');
// return;
// }
// try {
// const pages = await browser.pages();
// // Lấy danh sách URL từ flattenedArray
// const activeUrls = _.flatMap(MANAGER_BIDS, (item) => [
// // item.url,
// ...item.children.map((child) => child.url),
// ]).filter(Boolean); // Lọc bỏ null hoặc undefined
// console.log(
// '🔍 Page URLs:',
// pages.map((page) => page.url()),
// );
// for (const page of pages) {
// const pageUrl = page.url();
// if (!activeUrls.includes(pageUrl)) {
// if (!page.isClosed()) {
// try {
// await page.close();
// console.log(`🛑 Closing unused tab: ${pageUrl}`);
// } catch (err) {
// console.warn(`⚠️ Error closing tab ${pageUrl}:`, err.message);
// }
// }
// }
// }
// } catch (err) {
// console.error('❌ Error in clearLazyTab:', err.message);
// }
// }, configs.AUTO_TRACKING_CLEANING);
// } catch (error) {
// console.log('CLEAR LAZY TAB: ', error.message);
// }
// };
const clearLazyTab = async () => {
if (_CLEAR_LAZY_TAB_ID) {
clearInterval(_CLEAR_LAZY_TAB_ID);

View File

@ -15,29 +15,29 @@ export class GrayApiBid extends ApiBid {
async polling(page) {
try {
// // 🔥 Xóa tất cả event chặn request trước khi thêm mới
// page.removeAllListeners('request');
// await page.setRequestInterception(true);
// 🔥 Xóa tất cả event chặn request trước khi thêm mới
page.removeAllListeners('request');
await page.setRequestInterception(true);
// page.on('request', (request) => {
// if (request.url().includes('api/Notifications/GetOutBidLots')) {
// console.log('🚀 Fake response cho request:', request.url());
page.on('request', (request) => {
if (request.url().includes('api/Notifications/GetOutBidLots')) {
console.log('🚀 Fake response cho request:', request.url());
// const fakeData = fs.readFileSync('./data/fake-out-lots.json', 'utf8');
const fakeData = fs.readFileSync('./data/fake-out-lots.json', 'utf8');
// request.respond({
// status: 200,
// contentType: 'application/json',
// body: fakeData,
// });
// } else {
// try {
// request.continue(); // ⚠️ Chỉ tiếp tục nếu request chưa bị chặn
// } catch (error) {
// console.error('⚠️ Lỗi khi tiếp tục request:', error.message);
// }
// }
// });
request.respond({
status: 200,
contentType: 'application/json',
body: fakeData,
});
} else {
try {
request.continue(); // ⚠️ Chỉ tiếp tục nếu request chưa bị chặn
} catch (error) {
console.error('⚠️ Lỗi khi tiếp tục request:', error.message);
}
}
});
console.log('🔄 Starting polling process...');

View File

@ -1,6 +1,6 @@
import { outBid, pushPrice, updateBid, updateStatusByPrice } from '../../system/apis/bid.js';
import CONSTANTS from '../../system/constants.js';
import { delay, extractNumber, isNumber, isTimeReached, safeClosePage, takeSnapshot } from '../../system/utils.js';
import { delay, extractNumber, isNumber, isTimeReached, removeFalsyValues, safeClosePage, takeSnapshot } from '../../system/utils.js';
import { ProductBid } from '../product-bid.js';
export class GraysProductBid extends ProductBid {
@ -17,7 +17,7 @@ export class GraysProductBid extends ProductBid {
if (!isNumber(price_value)) {
console.log("Can't get PRICE_VALUE ❌");
await takeSnapshot(page, this, 'price-value-null');
await safeClosePage(this);
// await safeClosePage(this);
return { result: false, bid_price: 0 };
}
@ -27,7 +27,7 @@ export class GraysProductBid extends ProductBid {
if (bid_price > this.max_price) {
console.log('PRICE BID is more than MAX_VALUE => STOP BID THIS PRODUCT ❌');
await takeSnapshot(page, this, 'price-bid-more-than');
await safeClosePage(this);
// await safeClosePage(this);
await outBid(this.id);
@ -41,7 +41,7 @@ export class GraysProductBid extends ProductBid {
if (!response.status) {
// await this.handleReturnProductPage(page);
await safeClosePage(this);
// await safeClosePage(this);
return { result: false, bid_price: 0 };
}
@ -97,7 +97,7 @@ export class GraysProductBid extends ProductBid {
if (!close_time || new Date(close_time).getTime() <= new Date().getTime()) {
console.log(`Product is close ${close_time}`);
await safeClosePage(this);
// await safeClosePage(this);
return { result: true, close_time };
}
@ -131,7 +131,7 @@ export class GraysProductBid extends ProductBid {
console.log({ error: error.message });
console.log('❌ Timeout to loading');
await takeSnapshot(page, this, 'timeout to loading');
await safeClosePage(this);
// await safeClosePage(this);
return false;
}
}
@ -142,7 +142,7 @@ export class GraysProductBid extends ProductBid {
}
async handleUpdateBid({ lot_id, close_time, name, current_price, reserve_price }) {
if (close_time && this.close_time == close_time) return;
// if (close_time && this.close_time == close_time) return;
const response = await updateBid(this.id, { lot_id, close_time, name, current_price, reserve_price: Number(reserve_price) || 0 });
@ -153,19 +153,75 @@ export class GraysProductBid extends ProductBid {
}
}
// update = async () => {
// if (!this.page_context) return;
// const page = this.page_context;
// const close_time = await this.getCloseTime();
// const price_value = (await page.$eval('#priceValue', (el) => el.value)) || null;
// const lot_id = await page.$eval('#lotId', (el) => el.value);
// const name = (await page.$eval('#placebid-sticky > div:nth-child(2) > div > h3', (el) => el.innerText)) || null;
// const current_price =
// (await page.$eval('#biddableLot > form > div > div:nth-child(1) > div:nth-child(1) > div:nth-child(2) > div > span > span', (el) => el.innerText)) || null;
// console.log(`📌 Product Info: Lot ID: ${lot_id}, Name: ${name}, Current Price: ${current_price}, Reserve price ${price_value}`);
// this.handleUpdateBid({ lot_id, reserve_price: price_value, close_time, name, current_price: current_price ? extractNumber(current_price) : null });
// return { price_value, lot_id, name, current_price };
// };
update = async () => {
if (!this.page_context) return;
const page = this.page_context;
try {
const close_time = await this.getCloseTime();
// Chờ phần tử xuất hiện trước khi lấy giá trị
await page.waitForSelector('#priceValue', { timeout: 5000 }).catch(() => null);
const price_value = await page.$eval('#priceValue', (el) => el.value).catch(() => null);
await page.waitForSelector('#lotId', { timeout: 5000 }).catch(() => null);
const lot_id = await page.$eval('#lotId', (el) => el.value).catch(() => null);
await page.waitForSelector('#placebid-sticky > div:nth-child(2) > div > h3', { timeout: 5000 }).catch(() => null);
const name = await page.$eval('#placebid-sticky > div:nth-child(2) > div > h3', (el) => el.innerText).catch(() => null);
await page
.waitForSelector('#biddableLot > form > div > div:nth-child(1) > div:nth-child(1) > div:nth-child(2) > div > span > span', { timeout: 5000 })
.catch(() => null);
const current_price = await page
.$eval('#biddableLot > form > div > div:nth-child(1) > div:nth-child(1) > div:nth-child(2) > div > span > span', (el) => el.innerText)
.catch(() => null);
console.log(`📌 Product Info: Lot ID: ${lot_id}, Name: ${name}, Current Price: ${current_price}, Reserve price: ${price_value}`);
const data = removeFalsyValues({
lot_id,
reserve_price: price_value,
close_time: String(close_time),
name,
current_price: current_price ? extractNumber(current_price) : null,
});
this.handleUpdateBid(data);
return { price_value, lot_id, name, current_price };
} catch (error) {
console.error(`🚨 Error updating product info: ${error.message}`);
return null;
}
};
action = async () => {
try {
const page = this.page_context;
console.log('🔄 Starting the bidding process...');
await page.goto(this.url, { waitUntil: 'networkidle2' });
console.log(`✅ Navigated to: ${this.url}`);
await page.bringToFront();
console.log('👀 Brought the tab to the foreground.');
await this.gotoLink();
await delay(1000);
// this.handleTakeWorkSnapshot();
const { close_time, ...isCloseProduct } = await this.isCloseProduct(page);
if (isCloseProduct.result) {
@ -175,15 +231,9 @@ export class GraysProductBid extends ProductBid {
await delay(500);
const price_value = (await page.$eval('#priceValue', (el) => el.value)) || null;
const lot_id = await page.$eval('#lotId', (el) => el.value);
const name = (await page.$eval('#placebid-sticky > div:nth-child(2) > div > h3', (el) => el.innerText)) || null;
const current_price =
(await page.$eval('#biddableLot > form > div > div:nth-child(1) > div:nth-child(1) > div:nth-child(2) > div > span > span', (el) => el.innerText)) || null;
const { price_value } = await this.update();
console.log(`📌 Product Info: Lot ID: ${lot_id}, Name: ${name}, Current Price: ${current_price}, Reserve price ${price_value}`);
this.handleUpdateBid({ lot_id, reserve_price: price_value, close_time, name, current_price: current_price ? extractNumber(current_price) : null });
if (!price_value) return;
const { result, bid_price } = await this.validate({ page, price_value });
@ -203,7 +253,7 @@ export class GraysProductBid extends ProductBid {
if (!resultPlaceBid) {
console.log('❌ Error occurred while placing the bid.');
await takeSnapshot(page, this, 'place-bid-action');
await safeClosePage(this);
// await safeClosePage(this);
return;
}
@ -212,7 +262,7 @@ export class GraysProductBid extends ProductBid {
await this.handleReturnProductPage(page);
} catch (error) {
console.error(`🚨 Error navigating the page: ${error.message}`);
safeClosePage(this);
// safeClosePage(this);
}
};
}

View File

@ -24,6 +24,7 @@ export class ProductBid extends Bid {
current_price;
name;
reserve_price;
update;
constructor({
url,
@ -133,4 +134,15 @@ export class ProductBid extends Bid {
return true;
}
async gotoLink() {
const page = this.page_context;
console.log('🔄 Starting the bidding process...');
await page.goto(this.url, { waitUntil: 'networkidle2' });
console.log(`✅ Navigated to: ${this.url}`);
await page.bringToFront();
console.log('👀 Brought the tab to the foreground.');
}
}

View File

@ -5,6 +5,9 @@ import configs from '../system/config.js';
import CONSTANTS from '../system/constants.js';
import { sanitizeFileName } from '../system/utils.js';
import * as fs from 'fs';
const ONE_MINUTE = 60 * 1000;
export const handleCloseRemoveProduct = (data) => {
if (!Array.isArray(data)) return;
@ -41,3 +44,9 @@ export const deleteProfile = (data) => {
return false;
};
export const shouldUpdateProductTab = (productTab) => {
const updatedAt = new Date(productTab.updated_at).getTime();
const now = Date.now();
return now - updatedAt >= ONE_MINUTE;
};

View File

@ -84,7 +84,7 @@ export const updateStatusByPrice = async (id, current_price) => {
method: 'POST',
url: 'bids/update-status/' + id,
data: {
current_price,
current_price: Number(current_price) | 0,
},
});

View File

@ -17,7 +17,7 @@ export const createOutBidLog = async (values) => {
return data.data;
} catch (error) {
console.log('❌ ERROR IN SERVER (OUT BID LOG): ', error);
console.log('❌ ERROR IN SERVER (OUT BID LOG): ', error.message);
return false;
}
};

View File

@ -7,6 +7,7 @@ puppeteer.use(StealthPlugin());
const browser = await puppeteer.launch({
headless: process.env.ENVIRONMENT === 'prod' ? true : false,
// userDataDir: CONSTANTS.PROFILE_PATH, // Thư mục lưu profile
timeout: 60000,
args: [
'--no-sandbox',
'--disable-setuid-sandbox',

View File

@ -6,7 +6,7 @@ import { updateStatusWork } from './apis/bid.js';
export const isNumber = (value) => !isNaN(value) && !isNaN(parseFloat(value));
export const takeSnapshot = async (page, item, imageName, type = CONSTANTS.TYPE_IMAGE.ERRORS) => {
if (page.isClosed()) return;
if (!page || page.isClosed()) return;
try {
const baseDir = path.join(CONSTANTS.ERROR_IMAGES_PATH, item.type, String(item.id)); // Thư mục theo lot_id
@ -23,7 +23,19 @@ export const takeSnapshot = async (page, item, imageName, type = CONSTANTS.TYPE_
console.log(`📂 Save at folder: ${typeDir}`);
}
await page.waitForSelector('body', { visible: true, timeout: 5000 });
// await page.waitForSelector('body', { visible: true, timeout: 5000 });
// Kiểm tra có thể điều hướng trang không
const isPageResponsive = await page.evaluate(() => document.readyState === 'complete');
if (!isPageResponsive) {
console.log('🚫 Page is unresponsive, skipping snapshot.');
return;
}
// Chờ tối đa 15 giây, nếu không thấy thì bỏ qua
await page.waitForSelector('body', { visible: true, timeout: 15000 }).catch(() => {
console.log('⚠️ Body selector not found, skipping snapshot.');
return;
});
// Chụp ảnh màn hình và lưu vào filePath
await page.screenshot({ path: filePath, fullPage: true });
@ -79,3 +91,12 @@ export const sanitizeFileName = (url) => {
export const getPathProfile = (origin_url) => {
return path.join(CONSTANTS.PROFILE_PATH, sanitizeFileName(origin_url) + '.json');
};
export function removeFalsyValues(obj, excludeKeys = []) {
return Object.entries(obj).reduce((acc, [key, value]) => {
if (value || excludeKeys.includes(key)) {
acc[key] = value;
}
return acc;
}, {});
}