main
Lei OT 10 months ago
parent 96fe313e96
commit 2f6fc68b76

28
server/.gitignore vendored

@ -0,0 +1,28 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
/package-lock.json
temp

@ -0,0 +1,57 @@
const Koa = require('koa')
const app = new Koa()
const views = require('koa-views')
const cors = require('koa2-cors')
const json = require('koa-json')
const onerror = require('koa-onerror')
const bodyparser = require('koa-bodyparser')
const logger = require('koa-logger')
const index = require('./routes/index')
const { Aids: syncAids, hotelLgcDetails: syncHotelDetails, chinaHotelDetails: syncChinas } = require('./jobs/syncHeytripJobs');
const rlog = require('./middleware/request_log');
// error handler
onerror(app)
// middlewares
app.use(bodyparser({
enableTypes:['json', 'form', 'text']
}))
app.use(cors({
origin: function(ctx){ return '*' }
}))
app.use(json())
app.use(logger())
app.use(require('koa-static')(__dirname + '/public'))
app.use(views(__dirname + '/views', {
extension: 'ejs'
}))
app.use(rlog);
// logger
app.use(async (ctx, next) => {
const start = new Date()
await next()
const ms = new Date() - start
console.log(`${ctx.method} ${ctx.url} - ${ms}ms`)
})
// schedule jobs
// syncAids();
// syncHotelDetails();
// syncChinas();
// routes
app.use(index.routes(), index.allowedMethods())
// app.use(routes(), allowedMethods())
// error-handling
app.on('error', (err, ctx) => {
console.error('server error', err, ctx)
});
module.exports = app

@ -0,0 +1,89 @@
#!/usr/bin/env node
/**
* Module dependencies.
*/
var app = require('../app');
var debug = require('debug')('demo:server');
var http = require('http');
/**
* Get port from environment and store in Express.
*/
var port = normalizePort(process.env.PORT || '3020');
// app.set('port', port);
/**
* Create HTTP server.
*/
var server = http.createServer(app.callback());
/**
* Listen on provided port, on all network interfaces.
*/
server.listen(port);
server.on('error', onError);
server.on('listening', onListening);
/**
* Normalize a port into a number, string, or false.
*/
function normalizePort(val) {
var port = parseInt(val, 10);
if (isNaN(port)) {
// named pipe
return val;
}
if (port >= 0) {
// port number
return port;
}
return false;
}
/**
* Event listener for HTTP server "error" event.
*/
function onError(error) {
if (error.syscall !== 'listen') {
throw error;
}
var bind = typeof port === 'string'
? 'Pipe ' + port
: 'Port ' + port;
// handle specific listen errors with friendly messages
switch (error.code) {
case 'EACCES':
console.error(bind + ' requires elevated privileges');
process.exit(1);
break;
case 'EADDRINUSE':
console.error(bind + ' is already in use');
process.exit(1);
break;
default:
throw error;
}
}
/**
* Event listener for HTTP server "listening" event.
*/
function onListening() {
var addr = server.address();
var bind = typeof addr === 'string'
? 'pipe ' + addr
: 'port ' + addr.port;
debug('Listening on ' + bind);
}

@ -0,0 +1,11 @@
const HEYTRIP_API = 'http://distapi-sandbox.heytripgo.com/Accommodation';
const HEYTRIP_API_PROD = 'http://distapi.heytripgo.com/Accommodation';
const LGC_MAPPED = { '1': { 'lgc': '1', locale: 'en-US' }, '2': { 'lgc': '2', locale: 'zh-CN' } };
const DEFAULT_LGC = '1';
module.exports = {
HEYTRIP_API,
HEYTRIP_API_PROD,
LGC_MAPPED,
DEFAULT_LGC,
};

@ -0,0 +1,53 @@
const { Sequelize, DataTypes, Op } = require('sequelize');
/**
*
* 配置数据库
*
*/
const testDB = new Sequelize('hotel_hub', 'admin', 'admin', {
host: 'localhost',
dialect: 'mysql',
operatorsAliases: false,
dialectOptions: {
charset: 'utf8mb4',
collate: 'utf8mb4_unicode_ci',
supportBigNumbers: true,
bigNumberStrings: true,
},
pool: {
max: 5,
min: 0,
acquire: 30000,
idle: 10000,
},
timezone: '+08:00', //东八时区
});
const DB = new Sequelize('hotel_hub_p', 'admin', 'admin', {
host: 'localhost',
dialect: 'mysql',
operatorsAliases: false,
dialectOptions: {
charset: 'utf8mb4',
collate: 'utf8mb4_unicode_ci',
supportBigNumbers: true,
bigNumberStrings: true,
},
// logging: false,
pool: {
max: 5,
min: 0,
acquire: 30000,
idle: 10000,
},
timezone: '+08:00', //东八时区
});
module.exports = {
DataTypes, Op,
// sequelize: testDB,
sequelize: DB,
};

@ -0,0 +1,29 @@
class BaseController {
response = (code = 200, msg = '', data = {}, page = 1, limit = 10) => {
const res = this.getPagingData(data, page, limit);
return {
'errcode': code,
'msg': msg,
'title': msg,
// 'data': data,
...res,
};
};
getPagination = (page, size = 10) => {
const limit = size ? +size : 3;
const offset = page ? (page-1) * limit : 0;
return { limit, offset };
};
getPagingData = (_data, page, limit) => {
const { count: totalCount, rows: data } = _data;
if ( !data || !totalCount) {
return { data: _data };
}
const currentPage = page ? +page : 0;
const totalPages = Math.ceil(totalCount / limit);
return { totalCount, data, totalPages, currentPage };
};
}
module.exports = BaseController;

@ -0,0 +1,31 @@
const BaseController = require('./BaseController');
const heytripService = require('../services/heytripService');
class Heytrip extends BaseController {
getAids = async (ctx) => {
try {
const { nextPage } = await heytripService.syncAids();
ctx.body = this.response(0, 'get_heytrip_ids', nextPage);
} catch (error) {
// console.error(error);
ctx.body = this.response(1, error.message || 'An error occurred.');
}
};
getHotelInfo = async (ctx) => {
const { data } = await heytripService.syncHotelDetails();
ctx.body = this.response(0, 'get_hotel_info', data);
};
getAvailability = async (ctx) => {
try {
const data = await heytripService.getHotelAvailability(ctx.query);
ctx.body = this.response(0, 'get_hotel_availability', data);
} catch (error) {
ctx.body = this.response(1, error.message || 'An error occurred.');
}
};
}
module.exports = new Heytrip();

@ -0,0 +1,46 @@
const BaseController = require('./BaseController');
const heytripService = require('../services/heytripService');
const { QuotedHotelsPrice } = require('../vendor/heytrip');
class Api extends BaseController {
hotelSearch = async(ctx) => {
const { keyword, checkin, checkout } = ctx.query;
const page = ctx.query.page || 1;
const size = ctx.query.pagesize || 10;
try {
// if (!keyword || !checkin || !checkout) {
if (!keyword) {
ctx.throw(400, 'Missing required parameters`keyword`.');
}
const { limit, offset } = this.getPagination(page, size);
const { rows, count } = await heytripService.hotelSearch(keyword, { limit, offset });
let quoteRes = [];
if (checkin && checkout) {
const allIds = rows.map((item) => item.hotel_id);
quoteRes = await QuotedHotelsPrice({
hotelIds: allIds,
nationality: 'CN', // 默认取中国报价
CheckInDate: checkin,
CheckOutDate: checkout,
adultNum: 1,
roomCount: 1,
});
}
const quoteMapped = quoteRes.reduce((r, c) => ({ ...r, [c.Id]: c }), {});
const res = rows.map((item) => ({ ...item, base_price: quoteMapped[item.hotel_id] || {} }));
ctx.body = this.response(0, 'hotel_search', { rows: res, count }, page, size);
} catch (error) {
// console.error(error);
ctx.statusCode = error.statusCode;
ctx.body = this.response(1, error.message || 'An error occurred.');
}
}
}
module.exports = new Api();

@ -0,0 +1,41 @@
const fs = require('fs');
var date = new Date();
// var dat= date.getDate()+"-"+(date.getMonth()+1)+"-"+date.getFullYear();
var dat= `${date.getFullYear()}-${(date.getMonth()+1)}-${date.getDate()}`;
var err_log = "./pm2/logs/err.log";
var out_log = "./pm2/logs/out.log";
fs.mkdirSync(`./pm2/logs/${dat}`, { recursive: true });
err_log = './pm2/logs/' + dat + '/error.log';
out_log = './pm2/logs/' + dat + '/output.log';
combined_log = './pm2/logs/' + dat + '/combined.log';
module.exports = {
apps: [
{
name: 'hotelhub',
script: 'bin/www',
max_memory_restart: "500M",
merge_logs: true,
max_restarts: 20,
error_file: err_log,
out_file: out_log,
// instances: 1,
watch: false
}
]
}
// export const apps = [{
// name: 'hotelhub',
// script: 'bin/www',
// max_memory_restart: "500M",
// merge_logs: true,
// max_restarts: 20,
// error_file: err_log,
// out_file: out_log,
// // instances: 1,
// watch: '.'
// }];

@ -0,0 +1,347 @@
const { objectMapper: _objectMapper, isNotEmpty } = require('../utils/commons');
const { DEFAULT_LGC, LGC_MAPPED } = require('../config/constants');
const objectMapper = (input, keyMap) => _objectMapper(input || {}, keyMap, false);
const infoDataMapper = (row) => {
const item = objectMapper(row, {
HotelId: 'hotel_id',
HotelName: 'hotel_name',
CountryCode: 'country_code',
CityId: 'city_id',
Address: 'address',
Phone: 'phone',
Latitude: 'latitude',
Longitude: 'longitude',
Rating: 'rating',
HotelType: 'hotel_type',
Brand: 'brand',
PostalCode: 'postal_code',
HeroImg: 'hero_img',
SupplierType: 'supplier_type',
});
const review = objectMapper(row.Review, {
Score: 'review_score',
ReviewCount: 'review_count',
Desc: 'review_desc',
});
const location = objectMapper(row.Location, {
PoiScore: 'location_poi_score',
AirportScore: 'location_airport_score',
TransportationScore: 'location_transportation_score',
});
return {...item, ...review, ...location};
}
const info2DataMapper = (row, lgcObj) => {
const item = objectMapper(row, {
HotelId: 'hotel_id',
LocaleName: 'hi2_hotel_name',
Description: 'hi2_description',
AddressLocale: 'hi2_address',
});
const instruction = objectMapper(row.Instruction, {
Desc: 'h2_instruction_desc',
Special: 'h2_instruction_special',
FeesDesc: 'h2_instruction_fees_desc',
});
const location = objectMapper((row.Location?.WalkablePlaces), {
Title: 'h2_location_walkable_places_title',
Desc: 'h2_location_walkable_places_desc',
});
const highlight = objectMapper(row.Highlight, {
LocationHighlightDesc: 'h2_location_highlight_desc',
});
const review = objectMapper(row.Review, {
Desc: 'h2_review_desc',
});
return { ...item, ...instruction, ...location, ...highlight, ...lgcObj, ...review };
};
const roomsMapper = (row, lgcObj) => {
const rooms = (row.Rooms || []).map((rowRoom) => ({
hotel_id: row.HotelId,
...lgcObj,
...(objectMapper(rowRoom, {
RoomId: 'room_id',
RoomName: 'room_name',
LocaleName: 'locale_name',
BedTypeDesc: 'bed_type_desc',
Area: 'area',
Views: 'views',
Window: 'window',
Floor: 'floor',
WirelessWideband: 'wireless_wideband',
WiredBroadband: 'wired_broadband',
Smoking: 'smoking',
BathRoomType: 'bathroom_type',
// Images: 'images',
})),
max_occupancy: rowRoom.MaxOccupancy ? JSON.stringify(rowRoom.MaxOccupancy) : null,
bedrooms: rowRoom.BedRooms ? JSON.stringify(rowRoom.BedRooms) : null,
}));
return rooms;
};
const imagesDaraMapper = (row, lgcObj) => {
const images = (row.Rooms || []).reduce(
(r, rowRoom) =>
r.concat(
(rowRoom.Images || []).map((imgItem) => ({
hotel_id: row.HotelId,
...lgcObj,
info_source: 'rooms',
info_source_id: rowRoom.RoomId,
type: null,
url: imgItem.Url,
caption: null,
category: imgItem.Category,
}))
),
[]
);
const urls = (row.Urls || []).reduce(
(r, rowUrl) =>
r.concat(
rowUrl.Urls.map((imgItem) => ({
hotel_id: row.HotelId,
...lgcObj,
info_source: 'hotelinfo',
info_source_id: row.HotelId,
type: imgItem.Type,
url: imgItem.Url,
caption: rowUrl.Caption,
category: rowUrl.Category,
}))
),
[]
);
return [].concat(images, urls);
};
const mainhighlightsMapper = (row, lgcObj) => {
return (row.Highlight?.MainHighlights || []).map((mhl) => ({
hotel_id: row.HotelId, // hotel_id:
...lgcObj,
type: 'MainHighlights', // type:
category: mhl.Category, // category:
category_name: '', // category_name:
id: null, // id:
name: mhl.Name, // name:
symbol: null, // symbol:
tooltip: mhl.Tooltip, // tooltip:
}));
};
const facilityCMapper = (row, lgcObj) => {
return (row.Facility?.Categories || []).reduce(
(r, fc) =>
r.concat(
fc.Items.map((fcItem) => ({
hotel_id: row.HotelId, // hotel_id:
...lgcObj,
type: 'Facility', // type:
category: fc.Category, // category:
category_name: fc.Name, // category_name:
id: fcItem.Id, // id:
name: fcItem.Name, // name:
symbol: fcItem.Symbol, // symbol:
tooltip: null, // tooltip:
}))
),
[]
);
};
const facilityHLMapper = (row, lgcObj) =>
(row.Facility?.Highlights || []).map((fl) => ({
hotel_id: row.HotelId, // hotel_id:
...lgcObj,
type: 'Highlights', // type:
category: '', // category:
category_name: '', // category_name:
id: fl.Id, // id:
name: fl.Name, // name:
symbol: fl.Symbol, // symbol:
tooltip: null, // tooltip:
}));
const informationsMapper = (row, lgcObj) =>
(row.Informations || []).reduce(
(r, fc) =>
r.concat(
fc.Items.map((fcItem) => ({
hotel_id: row.HotelId, // hotel_id:
...lgcObj,
category: fc.Category, // category:
category_name: fc.CategoryName, // category_name:
id: fcItem.Id, // id:
name: fcItem.Name, // name:
value: fcItem.Value, // value:
}))
),
[]
);
const locationPlacesMapper = (sourceName, hotelId, places, lgcObj) =>
places.map((item) => ({
hotel_id: hotelId, // hotel_id:
...lgcObj,
location_type: sourceName, // location_type:
category: null, // category:
category_name: null, // category_name:
id: item.Id || null, // id:
name: item.Name, // name:
distance: item.Distance, // distance:
latitude: item.Latitude || null, // latitude:
longitude: item.Longitude || null, // longitude:
type_name: item.TypeName || null, // type_name:
type_id: item.TypeId || null, // type_id:
min_distance: null, // min_distance:
}));
const scoresMapper = (sourceName, hotelId, scores, lgcObj) => scores.map((item) => ({
hotel_id: hotelId, // hotel_id:
...lgcObj,
type: sourceName, // type:
name: sourceName==='ScoreDetails' ? item.Name : null, // name:
category: item.Category || null, // category:
score: item.Score || null, // score:
city_average: item.CityAverage || null, // city_average:
mention_name: sourceName==='PositiveMentions' ? item.Name : null, // mention_name:
mention_count: sourceName==='PositiveMentions' ? item.Count : null, // mention_count:
}));
/**
* 解析Details数据
* todo: BedRoom MaxOccupancy
*/
const resolveDetails = (res, lgcObj) => {
return res.reduce(
(rd, c) => {
rd.info.push(infoDataMapper(c));
rd.info2.push(info2DataMapper(c, lgcObj));
rd.rooms = rd.rooms.concat(roomsMapper(c, lgcObj));
rd.images = rd.images.concat(imagesDaraMapper(c, lgcObj));
// rd.images = rd.images.concat(urls);
// rd.test = rd.test.concat([c.Highlight.LocationHighlightDesc]); // test:
const MainHighlights = mainhighlightsMapper(c, lgcObj);
const facilityC = facilityCMapper(c, lgcObj);
const facilityHL = facilityHLMapper(c, lgcObj);
rd.facility = rd.facility.concat(MainHighlights, facilityC, facilityHL);
const infos = informationsMapper(c, lgcObj);
rd.infos = rd.infos.concat(infos);
const reviewScores = scoresMapper('ScoreDetails', c.HotelId, c.Review?.ScoreDetails || [], lgcObj);
const mentions = scoresMapper('PositiveMentions', c.HotelId, c.Review?.PositiveMentions || [], lgcObj);
rd.reviews = rd.reviews.concat(reviewScores, mentions);
const reviewSummaries = (c.Review?.Summaries || []).map((item) => ({
hotel_id: c.HotelId, // hotel_id:
...lgcObj,
country: item.Country, // country:
reviewer: item.Reviewer, // reviewer:
review_rating: item.ReviewRating, // review_rating:
desc: item.Desc, // desc:
review_date: item.ReviewDate, // review_date:
}));
rd.review_summaries = rd.review_summaries.concat(reviewSummaries);
const TopPlaces = locationPlacesMapper('TopPlaces', c.HotelId, c.Location?.TopPlaces || [], lgcObj);
const Shops = locationPlacesMapper('Shops', c.HotelId, c.Location?.Shops || [], lgcObj);
const Places = locationPlacesMapper('Places', c.HotelId, c.Location?.Places || [], lgcObj);
const NearbyCategories = (c.Location?.NearbyCategories || []).reduce(
(r, nc) =>
r.concat(
locationPlacesMapper('NearbyCategories', c.HotelId, nc.Places || [], lgcObj).map((item) => ({
...item,
category: nc.Category,
category_name: nc.Name,
min_distance: nc.MinDistance,
}))
),
[]
);
const WalkablePlaces = (c.Location?.WalkablePlaces?.Categories || []).reduce(
(r, nc) =>
r.concat(
locationPlacesMapper('WalkablePlaces', c.HotelId, nc.Places || [], lgcObj).map((item) => ({
...item,
category: nc.Name,
category_name: nc.Name,
}))
),
[]
);
rd.locations = rd.locations.concat(TopPlaces, Shops, Places, NearbyCategories, WalkablePlaces);
return rd;
},
{ info: [], info2: [], rooms: [], images: [], facility: [], infos: [], reviews: [], review_summaries: [], locations: [], test: [] }
);
}
/**
* 解析Availability数据
*/
/**
* 餐食类型 1明确数量(早中晚餐食看Breakfast Lunch Dinner 数量)2半包3全包4/晚二选一5+ /晚二选一
*/
const MealTypeMapped = {
'1': { value: 1, label: ({ Breakfast, Lunch, Dinner }) => `${Breakfast}份早餐` },
'2': { value: 2, label: ({ Breakfast, Lunch, Dinner }) => '半包' },
'3': { value: 3, label: ({ Breakfast, Lunch, Dinner }) => '全包' },
'4': { value: 4, label: ({ Breakfast, Lunch, Dinner }) => '午/晚二选一' },
'5': { value: 5, label: ({ Breakfast, Lunch, Dinner }) => '早+ 午/晚二选一' },
};
/**
* 付款方式 1现付 2预付
*/
const PayTypeMapped = {
'1': { value: 1, label: () => '现付' },
'2': { value: 2, label: () => '预付' },
};
/**
* 开票方式0 Unknown 1 提供方开票 2 酒店开票3 供应商开票
*/
const InvoiceTypeMapped = {
'0': { value: 0, label: () => '未知' },
'1': { value: 1, label: () => '提供方开票' },
'2': { value: 2, label: () => '酒店开票' },
'3': { value: 3, label: () => '供应商开票' },
};
/**
* 取消手续费类型0 未知 1扣首日 2扣全额 3按价格多少百分比扣 4免费取消 5扣几晚 6扣多少钱
*/
const cancelDeductTypeMapped = {
'0': { value: 0, label: () => '未知' },
'1': { value: 1, label: () => '扣首日' },
'2': { value: 2, label: () => '扣全额' },
'3': { value: 3, label: (value) => `扣款${value * 100}%` },
'4': { value: 4, label: () => '免费取消' },
'5': { value: 5, label: (value) => `${value}` },
'6': { value: 6, label: (value) => `扣款${value}` },
};
/**
* 解析报价数据
*/
const resolveRatePlans = (plans) => {
const res = plans.map((room) => ({
...room,
RatePlans: room.RatePlans.map((rp) => {
const PriceUnit = rp.Dailys[0].Price;
const CancelableText = rp.Cancelable === true ? '限时取消' : '不可取消';
const CancelRulesMapped = (rp.CancelRules || []).map((citem) => {
let OutputText = isNotEmpty(citem.StartTime) ? `北京时间: ${citem.StartTime}${citem.EndTime}, ` : '';
OutputText += cancelDeductTypeMapped[citem.DeductType].label(citem.DeductValue);
OutputText += citem.Desc;
return OutputText;
}); // 限时取消
rp.Cancelable === true ? CancelRulesMapped.push('其他时间不可取消') : false;
return { ...rp, PriceUnit, PriceTotal: rp.Price, CancelRulesText: CancelRulesMapped, CancelableText };
}),
}));
return res;
};
module.exports = { resolveDetails, resolveRatePlans };

@ -0,0 +1,58 @@
const { scheduleJob } = require('node-schedule');
const heytripService = require('../services/heytripService');
const { LGC_MAPPED } = require('../config/constants');
const Aids = () => {
const job = scheduleJob('*/2 * * * * *', async function () {
console.log('syncing heytrip, get available accommodation ids.');
const res = await heytripService.syncAids();
if (res.nextPage !== true) {
job.cancel();
}
});
};
const hotelLgcDetails = () => {
const job2 = scheduleJob('*/4 * * * * *', async function () {
console.log('-------------------------syncing heytrip, get accommodation details.-------------------------');
const isRunning = job2.pendingInvocations[0]?.job?.running == 1;
if (!isRunning) {
// const res = await heytripService.syncHotelDetails();
// const res = await heytripService.syncInitHotelLgcDetailsAction(LGC_MAPPED['1']);
const res = await heytripService.newHotelsLgc('1');
job2.cancel(); // debug: 0
if (res.next !== true) {
job2.cancel();
console.log('job completed! canceled job!');
}
} else {
console.log('pre job running! cancelNext');
job2.cancelNext();
}
});
};
const chinaHotelDetails = () => {
const job3 = scheduleJob('*/4 * * * * *', async function () {
console.log('syncing heytrip, get china accommodation details.');
const isRunning = job3.pendingInvocations[0]?.job?.running == 1;
if (!isRunning) {
const res = await heytripService.chinaHotelsLgc2('2');
if (res.next !== true) {
job3.cancel();
console.log('job completed! canceled job!');
// job3.reschedule('0 0 0 * * *');
}
} else {
console.log('pre job running! cancelNext');
job2.cancelNext();
}
});
};
module.exports = {
Aids, hotelLgcDetails, chinaHotelDetails
}

@ -0,0 +1,15 @@
const requestLogsService = require('./../services/requestLogsService');
const rlog = async (ctx, next) => {
try {
await next();
} catch (err) {
} finally {
await requestLogsService.create({
method: ctx.method,
path: ctx.method === 'GET' ? ctx.path : ctx.url,
request_data: ctx.method === 'GET' ? JSON.stringify(ctx.query) : JSON.stringify(ctx.request.body),
ip: ctx.ip,
});
}
};
module.exports = rlog;

@ -0,0 +1,36 @@
import Sequelize from 'sequelize';
export default function(sequelize, DataTypes) {
return sequelize.define('cache_availability', {
sn: {
autoIncrement: true,
type: DataTypes.BIGINT,
allowNull: false,
primaryKey: true
},
hotel_id: {
type: DataTypes.BIGINT,
allowNull: false
}
}, {
sequelize,
tableName: 'cache_availability',
timestamps: false,
indexes: [
{
name: "PRIMARY",
unique: true,
using: "BTREE",
fields: [
{ name: "sn" },
]
},
{
name: "cache_availability_hotel_id_IDX",
using: "BTREE",
fields: [
{ name: "hotel_id" },
]
},
]
});
};

@ -0,0 +1,56 @@
const Sequelize = require('sequelize');
module.exports = function(sequelize, DataTypes) {
return sequelize.define('city', {
sn: {
autoIncrement: true,
type: DataTypes.INTEGER,
allowNull: false,
primaryKey: true
},
id: {
type: DataTypes.STRING(10),
allowNull: true
},
name: {
type: DataTypes.STRING(100),
allowNull: true
},
latitude: {
type: DataTypes.DOUBLE,
allowNull: true
},
longitude: {
type: DataTypes.DOUBLE,
allowNull: true
},
lgc: {
type: DataTypes.TINYINT,
allowNull: false
},
locale: {
type: DataTypes.STRING(100),
allowNull: true
}
}, {
sequelize,
tableName: 'city',
timestamps: false,
indexes: [
{
name: "PRIMARY",
unique: true,
using: "BTREE",
fields: [
{ name: "sn" },
]
},
{
name: "city_id_IDX",
using: "BTREE",
fields: [
{ name: "id" },
]
},
]
});
};

@ -0,0 +1,56 @@
const Sequelize = require('sequelize');
module.exports = function(sequelize, DataTypes) {
return sequelize.define('country', {
sn: {
autoIncrement: true,
type: DataTypes.INTEGER,
allowNull: false,
primaryKey: true
},
id: {
type: DataTypes.STRING(10),
allowNull: true
},
name: {
type: DataTypes.STRING(100),
allowNull: true
},
latitude: {
type: DataTypes.DOUBLE,
allowNull: true
},
longitude: {
type: DataTypes.DOUBLE,
allowNull: true
},
lgc: {
type: DataTypes.TINYINT,
allowNull: false
},
locale: {
type: DataTypes.STRING(100),
allowNull: true
}
}, {
sequelize,
tableName: 'country',
timestamps: false,
indexes: [
{
name: "PRIMARY",
unique: true,
using: "BTREE",
fields: [
{ name: "sn" },
]
},
{
name: "countries_id_IDX",
using: "BTREE",
fields: [
{ name: "id" },
]
},
]
});
};

@ -0,0 +1,72 @@
const Sequelize = require('sequelize');
module.exports = function(sequelize, DataTypes) {
return sequelize.define('facility', {
sn: {
autoIncrement: true,
type: DataTypes.BIGINT,
allowNull: false,
primaryKey: true
},
hotel_id: {
type: DataTypes.BIGINT,
allowNull: false
},
lgc: {
type: DataTypes.TINYINT,
allowNull: false
},
locale: {
type: DataTypes.STRING(100),
allowNull: true
},
type: {
type: DataTypes.STRING(100),
allowNull: true
},
category: {
type: DataTypes.STRING(100),
allowNull: true
},
category_name: {
type: DataTypes.STRING(100),
allowNull: true
},
id: {
type: DataTypes.STRING(100),
allowNull: true
},
name: {
type: DataTypes.STRING(100),
allowNull: true
},
symbol: {
type: DataTypes.STRING(100),
allowNull: true
},
tooltip: {
type: DataTypes.STRING(1000),
allowNull: true
}
}, {
sequelize,
tableName: 'facility',
timestamps: false,
indexes: [
{
name: "PRIMARY",
unique: true,
using: "BTREE",
fields: [
{ name: "sn" },
]
},
{
name: "facility_hotel_id_IDX",
using: "BTREE",
fields: [
{ name: "hotel_id" },
]
},
]
});
};

@ -0,0 +1,44 @@
const Sequelize = require('sequelize');
module.exports = function(sequelize, DataTypes) {
return sequelize.define('heytrip_ids', {
hotel_id: {
type: DataTypes.BIGINT,
allowNull: false,
primaryKey: true
},
last_modify_time: {
type: DataTypes.DATE,
allowNull: true,
defaultValue: Sequelize.Sequelize.literal('CURRENT_TIMESTAMP')
},
update_flag: {
type: DataTypes.TINYINT,
allowNull: true,
defaultValue: 1,
comment: "状态0更新完成1待更新"
},
priority: {
type: DataTypes.TINYINT,
allowNull: true,
comment: "优先级:low: 10 , normal: 0 , medium: -5 , high: -10 , critical: -15"
},
page_index: {
type: DataTypes.INTEGER,
allowNull: true
}
}, {
sequelize,
tableName: 'heytrip_ids',
timestamps: false,
indexes: [
{
name: "PRIMARY",
unique: true,
using: "BTREE",
fields: [
{ name: "hotel_id" },
]
},
]
});
};

@ -0,0 +1,127 @@
const Sequelize = require('sequelize');
module.exports = function(sequelize, DataTypes) {
return sequelize.define('hotelinfo', {
hi_sn: {
autoIncrement: true,
type: DataTypes.BIGINT,
allowNull: false,
primaryKey: true
},
hotel_id: {
type: DataTypes.BIGINT,
allowNull: false,
unique: "hotelinfo_unique"
},
hotel_name: {
type: DataTypes.STRING(100),
allowNull: true
},
country_code: {
type: DataTypes.STRING(10),
allowNull: true
},
city_id: {
type: DataTypes.STRING(10),
allowNull: true
},
address: {
type: DataTypes.STRING(1000),
allowNull: true
},
phone: {
type: DataTypes.STRING(200),
allowNull: true
},
latitude: {
type: DataTypes.STRING(100),
allowNull: true
},
longitude: {
type: DataTypes.STRING(100),
allowNull: true
},
rating: {
type: DataTypes.DECIMAL(4,1),
allowNull: true
},
hotel_type: {
type: DataTypes.STRING(100),
allowNull: true
},
brand: {
type: DataTypes.STRING(100),
allowNull: true
},
postal_code: {
type: DataTypes.STRING(100),
allowNull: true
},
hero_img: {
type: DataTypes.STRING(1000),
allowNull: true
},
supplier_type: {
type: DataTypes.STRING(100),
allowNull: true,
comment: "酒店供应类型SF(自签单体) null(普通) 默认是null"
},
review_score: {
type: DataTypes.FLOAT,
allowNull: true
},
review_count: {
type: DataTypes.INTEGER,
allowNull: true
},
review_desc: {
type: DataTypes.STRING(1000),
allowNull: true
},
location_poi_score: {
type: DataTypes.FLOAT,
allowNull: true
},
location_airport_score: {
type: DataTypes.FLOAT,
allowNull: true
},
location_transportation_score: {
type: DataTypes.FLOAT,
allowNull: true
},
last_modify_time: {
type: DataTypes.DATE,
allowNull: true,
defaultValue: Sequelize.Sequelize.literal('CURRENT_TIMESTAMP')
}
}, {
sequelize,
tableName: 'hotelinfo',
timestamps: false,
indexes: [
{
name: "PRIMARY",
unique: true,
using: "BTREE",
fields: [
{ name: "hi_sn" },
]
},
{
name: "hotelinfo_unique",
unique: true,
using: "BTREE",
fields: [
{ name: "hotel_id" },
]
},
{
name: "hotelinfo_hotel_id_IDX",
using: "BTREE",
fields: [
{ name: "hotel_id" },
]
},
]
});
};

@ -0,0 +1,100 @@
const Sequelize = require('sequelize');
module.exports = function(sequelize, DataTypes) {
return sequelize.define('hotelinfo2', {
hi2_sn: {
autoIncrement: true,
type: DataTypes.BIGINT,
allowNull: false,
primaryKey: true
},
hotel_id: {
type: DataTypes.BIGINT,
allowNull: false
},
lgc: {
type: DataTypes.TINYINT,
allowNull: false
},
locale: {
type: DataTypes.STRING(100),
allowNull: true
},
hi2_hotel_name: {
type: DataTypes.STRING(100),
allowNull: true
},
hi2_address: {
type: DataTypes.STRING(1000),
allowNull: true
},
hi2_description: {
type: DataTypes.TEXT,
allowNull: true
},
h2_instruction_desc: {
type: DataTypes.TEXT,
allowNull: true
},
h2_instruction_special: {
type: DataTypes.TEXT,
allowNull: true
},
h2_instruction_fees_desc: {
type: DataTypes.TEXT,
allowNull: true
},
h2_location_walkable_places_title: {
type: DataTypes.STRING(1000),
allowNull: true
},
h2_location_walkable_places_desc: {
type: DataTypes.TEXT,
allowNull: true
},
h2_location_highlight_desc: {
type: DataTypes.STRING(1000),
allowNull: true
},
h2_review_desc: {
type: DataTypes.STRING(1000),
allowNull: true
}
}, {
sequelize,
tableName: 'hotelinfo2',
timestamps: false,
indexes: [
{
name: "PRIMARY",
unique: true,
using: "BTREE",
fields: [
{ name: "hi2_sn" },
]
},
{
name: "hotelinfo2_unique",
unique: true,
using: "BTREE",
fields: [
{ name: "hotel_id" },
{ name: "lgc" },
]
},
{
name: "hotelinfo2_hotel_id_IDX",
using: "BTREE",
fields: [
{ name: "hotel_id" },
]
},
{
name: "hotelinfo2_lgc_IDX",
using: "BTREE",
fields: [
{ name: "lgc" },
]
},
]
});
};

@ -0,0 +1,83 @@
const Sequelize = require('sequelize');
module.exports = function(sequelize, DataTypes) {
return sequelize.define('images', {
sn: {
autoIncrement: true,
type: DataTypes.BIGINT,
allowNull: false,
primaryKey: true
},
hotel_id: {
type: DataTypes.BIGINT,
allowNull: false
},
info_source: {
type: DataTypes.STRING(100),
allowNull: true
},
info_source_id: {
type: DataTypes.STRING(100),
allowNull: true
},
type: {
type: DataTypes.STRING(100),
allowNull: true,
comment: "Mid小图; Max大图;"
},
url: {
type: DataTypes.STRING(1000),
allowNull: true
},
caption: {
type: DataTypes.STRING(1000),
allowNull: true
},
category: {
type: DataTypes.STRING(100),
allowNull: true
},
lgc: {
type: DataTypes.TINYINT,
allowNull: false
},
locale: {
type: DataTypes.STRING(100),
allowNull: true
}
}, {
sequelize,
tableName: 'images',
timestamps: false,
indexes: [
{
name: "PRIMARY",
unique: true,
using: "BTREE",
fields: [
{ name: "sn" },
]
},
{
name: "image_urls_hotel_id_IDX",
using: "BTREE",
fields: [
{ name: "hotel_id" },
]
},
{
name: "image_urls_source_id_IDX",
using: "BTREE",
fields: [
{ name: "info_source_id" },
]
},
{
name: "images_lgc_IDX",
using: "BTREE",
fields: [
{ name: "lgc" },
]
},
]
});
};

@ -0,0 +1,64 @@
const Sequelize = require('sequelize');
module.exports = function(sequelize, DataTypes) {
return sequelize.define('informations', {
sn: {
autoIncrement: true,
type: DataTypes.BIGINT,
allowNull: false,
primaryKey: true
},
hotel_id: {
type: DataTypes.BIGINT,
allowNull: false
},
lgc: {
type: DataTypes.TINYINT,
allowNull: false
},
locale: {
type: DataTypes.STRING(100),
allowNull: true
},
category: {
type: DataTypes.STRING(100),
allowNull: true
},
category_name: {
type: DataTypes.STRING(100),
allowNull: true
},
id: {
type: DataTypes.STRING(100),
allowNull: true
},
name: {
type: DataTypes.STRING(100),
allowNull: true
},
value: {
type: DataTypes.STRING(1000),
allowNull: true
}
}, {
sequelize,
tableName: 'informations',
timestamps: false,
indexes: [
{
name: "PRIMARY",
unique: true,
using: "BTREE",
fields: [
{ name: "sn" },
]
},
{
name: "facility_hotel_id_IDX",
using: "BTREE",
fields: [
{ name: "hotel_id" },
]
},
]
});
};

@ -0,0 +1,49 @@
var DataTypes = require("sequelize").DataTypes;
var _city = require("./city");
var _country = require("./country");
var _facility = require("./facility");
var _heytrip_ids = require("./heytrip_ids");
var _hotelinfo = require("./hotelinfo");
var _hotelinfo2 = require("./hotelinfo2");
var _images = require("./images");
var _informations = require("./informations");
var _locations = require("./locations");
var _request_logs = require("./request_logs");
var _reviews = require("./reviews");
var _reviews_summaries = require("./reviews_summaries");
var _rooms = require("./rooms");
function initModels(sequelize) {
var cityModel = _city(sequelize, DataTypes);
var countryModel = _country(sequelize, DataTypes);
var facilityModel = _facility(sequelize, DataTypes);
var heytripIdsModel = _heytrip_ids(sequelize, DataTypes);
var hotelinfoModel = _hotelinfo(sequelize, DataTypes);
var hotelinfo2Model = _hotelinfo2(sequelize, DataTypes);
var imagesModel = _images(sequelize, DataTypes);
var informationsModel = _informations(sequelize, DataTypes);
var locationsModel = _locations(sequelize, DataTypes);
var requestLogsModel = _request_logs(sequelize, DataTypes);
var reviewsModel = _reviews(sequelize, DataTypes);
var reviewsSummariesModel = _reviews_summaries(sequelize, DataTypes);
var roomsModel = _rooms(sequelize, DataTypes);
return {
cityModel,
countryModel,
facilityModel,
heytripIdsModel,
hotelinfoModel,
hotelinfo2Model,
imagesModel,
informationsModel,
locationsModel,
requestLogsModel,
reviewsModel,
reviewsSummariesModel,
roomsModel,
};
}
module.exports = initModels;
module.exports.initModels = initModels;
module.exports.default = initModels;

@ -0,0 +1,90 @@
const Sequelize = require('sequelize');
module.exports = function(sequelize, DataTypes) {
return sequelize.define('locations', {
sn: {
autoIncrement: true,
type: DataTypes.BIGINT,
allowNull: false,
primaryKey: true
},
hotel_id: {
type: DataTypes.BIGINT,
allowNull: false
},
lgc: {
type: DataTypes.TINYINT,
allowNull: false
},
locale: {
type: DataTypes.STRING(100),
allowNull: true
},
location_type: {
type: DataTypes.STRING(100),
allowNull: true,
comment: "TopPlaces; Shops; Places; NearbyCategories;"
},
category: {
type: DataTypes.STRING(100),
allowNull: true
},
category_name: {
type: DataTypes.STRING(100),
allowNull: true
},
id: {
type: DataTypes.STRING(100),
allowNull: true
},
name: {
type: DataTypes.STRING(1000),
allowNull: true
},
distance: {
type: DataTypes.FLOAT,
allowNull: true,
comment: "距离km"
},
latitude: {
type: DataTypes.DOUBLE,
allowNull: true
},
longitude: {
type: DataTypes.DOUBLE,
allowNull: true
},
type_name: {
type: DataTypes.STRING(100),
allowNull: true
},
type_id: {
type: DataTypes.STRING(100),
allowNull: true
},
min_distance: {
type: DataTypes.FLOAT,
allowNull: true
}
}, {
sequelize,
tableName: 'locations',
timestamps: false,
indexes: [
{
name: "PRIMARY",
unique: true,
using: "BTREE",
fields: [
{ name: "sn" },
]
},
{
name: "locations_hotel_id_IDX",
using: "BTREE",
fields: [
{ name: "hotel_id" },
]
},
]
});
};

@ -0,0 +1,54 @@
const Sequelize = require('sequelize');
module.exports = function(sequelize, DataTypes) {
return sequelize.define('request_logs', {
sn: {
autoIncrement: true,
type: DataTypes.BIGINT,
allowNull: false,
primaryKey: true
},
action: {
type: DataTypes.STRING(100),
allowNull: true
},
method: {
type: DataTypes.STRING(15),
allowNull: true
},
path: {
type: DataTypes.STRING(1000),
allowNull: true
},
request_data: {
type: DataTypes.TEXT,
allowNull: true
},
snapshots: {
type: DataTypes.TEXT,
allowNull: true
},
ip: {
type: DataTypes.STRING(200),
allowNull: true
},
createtime: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: Sequelize.Sequelize.literal('CURRENT_TIMESTAMP')
}
}, {
sequelize,
tableName: 'request_logs',
timestamps: false,
indexes: [
{
name: "PRIMARY",
unique: true,
using: "BTREE",
fields: [
{ name: "sn" },
]
},
]
});
};

@ -0,0 +1,74 @@
const Sequelize = require('sequelize');
module.exports = function(sequelize, DataTypes) {
return sequelize.define('reviews', {
sn: {
autoIncrement: true,
type: DataTypes.BIGINT,
allowNull: false,
primaryKey: true
},
hotel_id: {
type: DataTypes.BIGINT,
allowNull: false
},
lgc: {
type: DataTypes.TINYINT,
allowNull: false
},
locale: {
type: DataTypes.STRING(100),
allowNull: true
},
type: {
type: DataTypes.STRING(100),
allowNull: true,
comment: "ScoreDetails; Summaries; PositiveMentions"
},
name: {
type: DataTypes.STRING(100),
allowNull: true
},
category: {
type: DataTypes.STRING(100),
allowNull: true,
comment: "种类 cleanliness(环境和清洁度) facilities(设施) location(位置) roomComfort(客房舒适度) staffPerformance(服务)valueForMoney(性价比)"
},
score: {
type: DataTypes.FLOAT,
allowNull: true
},
city_average: {
type: DataTypes.FLOAT,
allowNull: true
},
mention_name: {
type: DataTypes.STRING(100),
allowNull: true
},
mention_count: {
type: DataTypes.INTEGER,
allowNull: true
}
}, {
sequelize,
tableName: 'reviews',
timestamps: false,
indexes: [
{
name: "PRIMARY",
unique: true,
using: "BTREE",
fields: [
{ name: "sn" },
]
},
{
name: "reviews_hotel_id_IDX",
using: "BTREE",
fields: [
{ name: "hotel_id" },
]
},
]
});
};

@ -0,0 +1,64 @@
const Sequelize = require('sequelize');
module.exports = function(sequelize, DataTypes) {
return sequelize.define('reviews_summaries', {
sn: {
autoIncrement: true,
type: DataTypes.BIGINT,
allowNull: false,
primaryKey: true
},
hotel_id: {
type: DataTypes.BIGINT,
allowNull: false
},
lgc: {
type: DataTypes.TINYINT,
allowNull: false
},
locale: {
type: DataTypes.STRING(100),
allowNull: true
},
country: {
type: DataTypes.STRING(100),
allowNull: true
},
reviewer: {
type: DataTypes.STRING(100),
allowNull: true
},
review_rating: {
type: DataTypes.FLOAT,
allowNull: true
},
desc: {
type: DataTypes.TEXT,
allowNull: true
},
review_date: {
type: DataTypes.DATE,
allowNull: true
}
}, {
sequelize,
tableName: 'reviews_summaries',
timestamps: false,
indexes: [
{
name: "PRIMARY",
unique: true,
using: "BTREE",
fields: [
{ name: "sn" },
]
},
{
name: "reviews_hotel_id_IDX",
using: "BTREE",
fields: [
{ name: "hotel_id" },
]
},
]
});
};

@ -0,0 +1,112 @@
const Sequelize = require('sequelize');
module.exports = function(sequelize, DataTypes) {
return sequelize.define('rooms', {
sn: {
autoIncrement: true,
type: DataTypes.BIGINT,
allowNull: false,
primaryKey: true
},
hotel_id: {
type: DataTypes.BIGINT,
allowNull: false
},
lgc: {
type: DataTypes.TINYINT,
allowNull: false
},
locale: {
type: DataTypes.STRING(100),
allowNull: true
},
room_id: {
type: DataTypes.STRING(100),
allowNull: false
},
room_name: {
type: DataTypes.STRING(500),
allowNull: true
},
locale_name: {
type: DataTypes.STRING(500),
allowNull: true
},
bed_type_desc: {
type: DataTypes.STRING(1000),
allowNull: true
},
area: {
type: DataTypes.STRING(100),
allowNull: true
},
views: {
type: DataTypes.STRING(100),
allowNull: true
},
window: {
type: DataTypes.INTEGER,
allowNull: true,
comment: "窗户信息 0未知 1有窗 2无窗 3部分有窗 4内窗 5封闭窗 6部分内窗"
},
floor: {
type: DataTypes.STRING(100),
allowNull: true
},
wireless_wideband: {
type: DataTypes.INTEGER,
allowNull: true,
comment: "无线网络信息 0未知 1无 2免费 3收费 4部分收费 5部分有且收费 6部分有且免费"
},
wired_broadband: {
type: DataTypes.INTEGER,
allowNull: true,
comment: "有线网络信息 0未知 1无 2免费 3收费 4部分收费 5部分有且收费 6部分有且免费"
},
smoking: {
type: DataTypes.INTEGER,
allowNull: true,
comment: "吸烟信息 0未知 1禁烟 2部分禁烟 3可吸烟"
},
bathroom_type: {
type: DataTypes.INTEGER,
allowNull: true,
comment: "卫浴信息 0未知 1独立卫浴 2公共卫浴"
},
max_occupancy: {
type: DataTypes.JSON,
allowNull: true
},
bedrooms: {
type: DataTypes.JSON,
allowNull: true
}
}, {
sequelize,
tableName: 'rooms',
timestamps: false,
indexes: [
{
name: "PRIMARY",
unique: true,
using: "BTREE",
fields: [
{ name: "sn" },
]
},
{
name: "rooms_room_id_IDX",
using: "BTREE",
fields: [
{ name: "room_id" },
]
},
{
name: "rooms_hotel_id_IDX",
using: "BTREE",
fields: [
{ name: "hotel_id" },
]
},
]
});
};

@ -0,0 +1,32 @@
{
"name": "HotelHub",
"version": "0.1.0",
"private": true,
"scripts": {
"start": "node bin/www",
"dev": "nodemon bin/www",
"prd": "pm2 start bin/www",
"test": "echo \"Error: no test specified\" && exit 1"
},
"dependencies": {
"axios": "^1.7.3",
"debug": "^4.1.1",
"ejs": "~2.3.3",
"koa": "^2.7.0",
"koa-bodyparser": "^4.2.1",
"koa-convert": "^1.2.0",
"koa-json": "^2.0.2",
"koa-logger": "^3.2.0",
"koa-onerror": "^4.1.0",
"koa-router": "^7.4.0",
"koa-static": "^5.0.0",
"koa-views": "^6.2.0",
"koa2-cors": "^2.0.6",
"mysql2": "^3.11.0",
"node-schedule": "^2.1.1",
"sequelize": "^6.37.3"
},
"devDependencies": {
"nodemon": "^1.19.1"
}
}

@ -0,0 +1,8 @@
body {
padding: 50px;
font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
}
a {
color: #00B7FF;
}

@ -0,0 +1,13 @@
const router = require('koa-router')();
const Heytrip = require('./../controllers/Heytrip');
const Api = require('./../controllers/Api');
// router.get('/get_heytrip_ids', Heytrip.getAids);
// router.get('/get_hotel_info', Heytrip.getHotelInfo);
router.get('/search_hotel', Api.hotelSearch);
router.get('/availability', Heytrip.getAvailability);
module.exports = router;

@ -0,0 +1,14 @@
import Router from 'koa-router';
const router = new Router();
router.prefix('/users')
router.get('/', function (ctx, next) {
ctx.body = 'this is a users response!'
})
router.get('/bar', function (ctx, next) {
ctx.body = 'this is a users/bar response'
})
export default router

@ -0,0 +1,307 @@
const db = require('../config/db');
const initModels = require('./../models/init-models');
const { AvailableAccommodationIds, AccommodationsDetails, Availability } = require('../vendor/heytrip');
const { isEmpty } = require('../utils/commons');
const { resolveDetails, resolveRatePlans } = require('../helper/heytripDataHelper');
const { DEFAULT_LGC, LGC_MAPPED } = require('../config/constants');
const { sequelize: Sequelize, Op } = db;
const models = initModels(Sequelize);
const Country = models.countryModel;
const City = models.cityModel;
const HeytripIds = models.heytripIdsModel;
const Hotelinfo = models.hotelinfoModel;
const Hotelinfo2 = models.hotelinfo2Model;
const Rooms = models.roomsModel;
const Facility = models.facilityModel;
const Images = models.imagesModel;
const Informations = models.informationsModel;
const Reviews = models.reviewsModel;
const ReviewsSummaries = models.reviewsSummariesModel;
const Locations = models.locationsModel;
// const CacheAvailability = models.cacheAvailabilityModel;
// HeytripIds.hasMany(Hotelinfo, {
// foreignKey: 'hotel_id',
// onDelete: 'NO ACTION',
// onUpdate: 'NO ACTION',
// });
// Hotelinfo.belongsTo(HeytripIds, { as: 'aid', foreignKey: 'hotel_id', onDelete: 'NO ACTION', onUpdate: 'NO ACTION' });
// Hotelinfo.hasMany(Rooms, { sourceKey: 'hotel_id', foreignKey: 'hotel_id', onDelete: 'NO ACTION', onUpdate: 'NO ACTION' });
// Rooms.belongsTo(Hotelinfo, { targetKey: 'hotel_id', foreignKey: 'hotel_id', onDelete: 'NO ACTION', onUpdate: 'NO ACTION', });
Hotelinfo.hasMany(Hotelinfo2, { as: 'lgc_info', sourceKey: 'hotel_id', foreignKey: 'hotel_id', onDelete: 'NO ACTION', onUpdate: 'NO ACTION' });
Hotelinfo.hasMany(City, { as: 'city', sourceKey: 'city_id', foreignKey: 'id', onDelete: 'NO ACTION', onUpdate: 'NO ACTION', }); // 多语种, 所以实际是 hasMany , 用 hasOne 要指定 lgc= 1 或者2
Hotelinfo.hasMany(Country, { as: 'country', sourceKey: 'country_code', foreignKey: 'id', onDelete: 'NO ACTION', onUpdate: 'NO ACTION', }); // 多语种, 所以实际是 hasMany , 用 hasOne 要指定 lgc= 1 或者2
// Hotelinfo2.belongsTo(Hotelinfo, { targetKey: 'hotel_id', foreignKey: 'hotel_id', onDelete: 'NO ACTION', onUpdate: 'NO ACTION', });
class Heytrip {
/**
* 搜索酒店
*/
hotelSearch = async (keyword, options) => {
const keywordSplit = keyword.split(' ');
const keywordWhere = (field) => keywordSplit.map((word) => Sequelize.where(Sequelize.fn('instr', Sequelize.col(field), word), { [Op.gt]: 0 }));
// 'hotelinfo.address'
const keywordSearch = ['hotelinfo.hotel_name', ].map((field) => ({ [Op.and]: keywordWhere(field) }));
const keywordOrder = ['hotelinfo.hotel_name',].reduce((ro, field) => ro.concat(keywordSplit.map((word) => Sequelize.fn('instr', Sequelize.col(field), word))), []);
// const keywordSearchCount = ['hi2_hotel_name', 'hi2_address'].map((field) => ({ [Op.and]: keywordWhere(field) }));
// const countRows = await Hotelinfo2.count({
// where: {
// [Op.or]: keywordSearchCount,
// },
// // distinct: true,
// group: ['hotel_id'],
// });
// const count = countRows.length;
// const findIds = countRows.map((item) => item.hotel_id);
const { count, rows } = await Hotelinfo.findAndCountAll({
// const [ rows ] = await Hotelinfo.findAll({
include: [
{
model: Hotelinfo2,
as: 'lgc_info',
attributes: [
['hi2_sn', 'sn'],
'hotel_id',
'lgc',
'locale',
['hi2_hotel_name', 'hotel_name'],
['hi2_address', 'address'],
['hi2_description', 'description'],
['h2_review_desc', 'review_desc'],
['h2_location_highlight_desc', 'location_highlight_desc'],
],
required: false,
separate: true,
},
{ model: City, as: 'city', attributes: ['id', 'name'], where: { lgc: 2 }, required: false, separate: true, },
{ model: Country, as: 'country', attributes: ['id', 'name'], where: { lgc: 2 }, required: false, separate: true, },
],
// include: [{ model: Hotelinfo2, as: 'h2', required: true }],
where: {
[Op.or]: keywordSearch,
// hotel_id: findIds,
},
order: keywordOrder,
...options,
// raw: true,
nest: true,
});
return { count, rows: rows.map((item) => item.dataValues) };
// return { count, rows };
};
getLastPageIndex = async () => {
const ret = await HeytripIds.max('page_index');
return ret;
};
syncAids = async () => {
const lastPageIndex = await this.getLastPageIndex();
const pageIndex = lastPageIndex + 1;
const ids = await AvailableAccommodationIds(pageIndex);
if (isEmpty(ids)) {
return {
nextPage: false,
pageIndex,
};
}
const insertRows = ids.map((id) => ({ hotel_id: id, page_index: pageIndex }));
await HeytripIds.bulkCreate(insertRows);
return {
nextPage: true,
pageIndex,
};
};
newHotels = async () => {
const [rows] = await Sequelize.query(
'SELECT i.hotel_id, IFNULL(h.hi_sn ,0) info_exists FROM heytrip_ids as i LEFT JOIN hotelinfo AS h ON h.hotel_id = i.hotel_id WHERE h.hi_sn IS NULL AND update_flag != 0 ORDER BY info_exists LIMIT 10'
);
return rows;
};
newHotelsLgc = async (lgc) => {
// const [rows] = await Sequelize.query(
// `SELECT h.hotel_id
// FROM hotelinfo AS h
// LEFT JOIN hotelinfo2 AS h2 ON h.hotel_id =h2.hotel_id
// AND h2.lgc = ${lgc}
// WHERE h2.hi2_sn IS NULL LIMIT 10`
// );
const [rows] = await Sequelize.query(
`SELECT i.hotel_id ,IFNULL(h.hi2_sn, 0) info_exists
FROM heytrip_ids AS i
LEFT JOIN hotelinfo2 AS h ON h.hotel_id = i.hotel_id
AND h.lgc = ${lgc}
WHERE h.hi2_sn IS NULL
AND update_flag != 99
ORDER BY info_exists LIMIT 10`
);
const res = await this.syncInitHotelLgcDetailsAction(rows, LGC_MAPPED[lgc]);
return res;
};
chinaHotels = async () => {
const [rows] = await Sequelize.query(
'SELECT i.hotel_id, IFNULL(h.hi_sn ,0) info_exists FROM heytrip_ids as i LEFT JOIN hotelinfo AS h ON h.hotel_id = i.hotel_id WHERE h.hi_sn IS NULL AND update_flag != 0 AND i.hotel_id > 20000000 ORDER BY info_exists LIMIT 10'
);
const res = await this.syncInitHotelDetailsAction(rows, LGC_MAPPED['2']);
return res;
};
chinaHotelsLgc2 = async (lgc) => {
// const [rows] = await Sequelize.query(
// `SELECT h.hotel_id
// FROM hotelinfo AS h
// LEFT JOIN hotelinfo2 AS h2 ON h.hotel_id =h2.hotel_id
// AND h2.lgc = ${lgc}
// WHERE h2.hi2_sn IS NULL LIMIT 10`
// );
const [rows] = await Sequelize.query(
`SELECT i.hotel_id ,IFNULL(h.hi2_sn, 0) info_exists
FROM heytrip_ids AS i
INNER JOIN hotelinfo AS h1 ON h1.hotel_id =i.hotel_id
LEFT JOIN hotelinfo2 AS h ON h.hotel_id = i.hotel_id
AND h.lgc = ${lgc}
WHERE h.hi2_sn IS NULL
AND update_flag != 99
AND h1.country_code ='CN'
-- AND i.hotel_id > 20000000
ORDER BY info_exists LIMIT 10`
);
const res = await this.syncInitHotelLgcDetailsAction(rows, LGC_MAPPED[lgc]);
return res;
};
syncInitHotelDetailsAction = async (rows, lgcObj = LGC_MAPPED[DEFAULT_LGC]) => {
let allIds = [];
try {
allIds = rows.map((item) => item.hotel_id);
const updateIds = rows.filter((item) => item.info_exists !== 0).map((item) => item.hotel_id);
const newIds = rows.filter((item) => item.info_exists === 0).map((item) => item.hotel_id);
if (isEmpty(rows)) {
return { next: !isEmpty(allIds), data: allIds };
}
const res = await AccommodationsDetails({
Language: lgcObj.locale,
AccommodationIds: allIds,
});
const resIds = res.map((item) => item.HotelId);
// hotel info
const insertData = extractDetails(res, lgcObj);
// return insertData; // debug: 0
/** 开始Database */
const sequelizeOptions = { logging: false };
const result = await Sequelize.transaction(async t => {
let Info;
if ( ! isEmpty(insertData.info)) Info = await Hotelinfo.bulkCreate(insertData.info, sequelizeOptions);
if ( ! isEmpty(insertData.info2)) await Hotelinfo2.bulkCreate(insertData.info2, sequelizeOptions);
if ( ! isEmpty(insertData.rooms)) await Rooms.bulkCreate(insertData.rooms, sequelizeOptions);
if ( ! isEmpty(insertData.images)) await Images.bulkCreate(insertData.images, sequelizeOptions);
if ( ! isEmpty(insertData.facility)) await Facility.bulkCreate(insertData.facility, sequelizeOptions);
if ( ! isEmpty(insertData.infos)) await Informations.bulkCreate(insertData.infos, sequelizeOptions);
if ( ! isEmpty(insertData.reviews)) await Reviews.bulkCreate(insertData.reviews, sequelizeOptions);
if ( ! isEmpty(insertData.reviews_summaries)) await ReviewsSummaries.bulkCreate(insertData.reviews_summaries, sequelizeOptions);
if ( ! isEmpty(insertData.locations)) await Locations.bulkCreate(insertData.locations, sequelizeOptions);
if ( ! isEmpty(allIds)) await HeytripIds.update({ update_flag: 0 }, { where: { hotel_id: allIds } });
return Info;
});
return { next: !isEmpty(allIds), data: allIds };
} catch (error) {
console.log(error);
return { next: false, data: allIds };
}
}
syncInitHotelLgcDetailsAction = async (rows, lgcObj = LGC_MAPPED[DEFAULT_LGC]) => {
let allIds = [];
try {
allIds = rows.map((item) => item.hotel_id);
if (isEmpty(rows)) {
return { next: !isEmpty(allIds), data: allIds };
}
const _BaseInfoExists = await Hotelinfo.findAll({ where: { hotel_id: allIds } });
const updateIds = _BaseInfoExists.map((item) => `${item.hotel_id}`);
console.log('updateIds', updateIds);
const res = await AccommodationsDetails({
Language: lgcObj.locale,
AccommodationIds: allIds,
});
const resIds = res.map((item) => item.HotelId);
// hotel info
const insertData = resolveDetails(res, lgcObj);
// return insertData; // debug: 0
/** 开始Database */
const result = await Sequelize.transaction(async transaction => {
const sequelizeOptions = { logging: false, transaction };
let Info;
const newInfo = insertData.info.filter(iitem => !updateIds.includes(`${iitem.hotel_id}`));
console.log('newInfo', newInfo.map(xx => xx.hotel_id));
if ( ! isEmpty(newInfo)) Info = await Hotelinfo.bulkCreate(newInfo, sequelizeOptions);
if ( ! isEmpty(insertData.info2)) await Hotelinfo2.bulkCreate(insertData.info2, sequelizeOptions);
if ( ! isEmpty(insertData.rooms)) await Rooms.bulkCreate(insertData.rooms, sequelizeOptions);
if ( ! isEmpty(insertData.images)) await Images.bulkCreate(insertData.images, sequelizeOptions);
if ( ! isEmpty(insertData.facility)) await Facility.bulkCreate(insertData.facility, sequelizeOptions);
if ( ! isEmpty(insertData.infos)) await Informations.bulkCreate(insertData.infos, sequelizeOptions);
if ( ! isEmpty(insertData.reviews)) await Reviews.bulkCreate(insertData.reviews, sequelizeOptions);
if ( ! isEmpty(insertData.reviews_summaries)) await ReviewsSummaries.bulkCreate(insertData.reviews_summaries, sequelizeOptions);
if ( ! isEmpty(insertData.locations)) await Locations.bulkCreate(insertData.locations, sequelizeOptions);
if ( ! isEmpty(newInfo)) await HeytripIds.update({ update_flag: 0 }, { where: { hotel_id: newInfo.map(x => x.hotel_id) } });
return Info;
});
return { next: !isEmpty(allIds), data: allIds };
} catch (error) {
console.log(error);
return { next: false, data: allIds };
}
};
getHotelAvailability = async (param) => {
const { hotel_id, checkin, checkout, adults, children_ages, rooms, nationality } = param;
const paramBody = {
Language: 'en-US',
// Language: 'zh-CN',
AccommodationIds: [Number(hotel_id)],
CheckInDate: checkin,
CheckOutDate: checkout,
Nationality: nationality || 'CN', // 默认取中国报价
NumberOfAdults: adults || 1,
ChildrenAges: children_ages || null, // 入住每个儿童年龄 [6,8]
// ChildrenAges: null,
NumberOfRooms: rooms || 1,
Currency: 'CNY',
};
const _quoteRes = await Availability(paramBody);
const quoteRes = resolveRatePlans(_quoteRes);
return quoteRes;
};
}
module.exports = new Heytrip();

@ -0,0 +1,17 @@
const db = require('../config/db');
const initModels = require('./../models/init-models');
const Sequelize = db.sequelize;
const models = initModels(Sequelize);
const Logs = models.requestLogsModel;
// Logs.sync({ force: false });
class requestLogs {
static async create(data) {
return await Logs.create(data, { logging: false });
}
}
module.exports = requestLogs;

@ -0,0 +1,683 @@
function copy(obj) {
return JSON.parse(JSON.stringify(obj));
}
function formatDate(date) {
if (isEmpty(date)) {
return 'NaN';
}
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
const monthStr = ('' + month).padStart(2, 0);
const dayStr = ('' + day).padStart(2, 0);
const formatted = year + '-' + monthStr + '-' + dayStr;
return formatted;
}
function formatTime(date) {
const hours = date.getHours();
const minutes = date.getMinutes();
const hoursStr = ('' + hours).padStart(2, 0);
const minutesStr = ('' + minutes).padStart(2, 0);
const formatted = hoursStr + ':' + minutesStr;
return formatted;
}
function formatDatetime(date) {
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
const monthStr = ('' + month).padStart(2, 0);
const dayStr = ('' + day).padStart(2, 0);
const hours = date.getHours();
const minutes = date.getMinutes();
const hoursStr = ('' + hours).padStart(2, 0);
const minutesStr = ('' + minutes).padStart(2, 0);
const formatted = year + '-' + monthStr + '-' + dayStr + ' ' + hoursStr + ':' + minutesStr;
return formatted;
}
function camelCase(name) {
return name.substr(0, 1).toLowerCase() + name.substr(1);
}
class UrlBuilder {
constructor(url) {
this.url = url;
this.paramList = [];
}
append(name, value) {
if (isNotEmpty(value)) {
this.paramList.push({ name: name, value: value });
}
return this;
}
build() {
this.paramList.forEach((e, i, a) => {
if (i === 0) {
this.url += '?';
} else {
this.url += '&';
}
this.url += e.name + '=' + e.value;
});
return this.url;
}
}
function isNotEmpty(val) {
return val !== undefined && val !== null && val !== '';
}
function prepareUrl(url) {
return new UrlBuilder(url);
}
function throttle(fn, delay, atleast) {
let timeout = null,
startTime = new Date();
return function () {
let curTime = new Date();
clearTimeout(timeout);
if (curTime - startTime >= atleast) {
fn();
startTime = curTime;
} else {
timeout = setTimeout(fn, delay);
}
};
}
function clickUrl(url) {
const httpLink = document.createElement('a');
httpLink.href = url;
httpLink.target = '_blank';
httpLink.click();
}
function escape2Html(str) {
var temp = document.createElement('div');
temp.innerHTML = str;
var output = temp.innerText || temp.textContent;
temp = null;
return output;
}
function formatPrice(price) {
return Math.ceil(price).toLocaleString();
}
function formatPercent(number) {
return Math.round(number * 100) + '%';
}
/**
* ! 不支持计算 Set Map
* @param {*} val
* @example
* true if: 0, [], {}, null, '', undefined
* false if: 'false', 'undefined'
*/
function isEmpty(val) {
// return val === undefined || val === null || val === "";
return [Object, Array].includes((val || {}).constructor) && !Object.entries(val || {}).length;
}
/**
* 数组排序
*/
const sortBy = (key) => {
return (a, b) => (getNestedValue(a, key) > getNestedValue(b, key) ? 1 : getNestedValue(b, key) > getNestedValue(a, key) ? -1 : 0);
};
/**
* Object排序keys
*/
const sortKeys = (obj) =>
Object.keys(obj)
.sort()
.reduce((a, k2) => ({ ...a, [k2]: obj[k2] }), {});
/**
* 数组排序, 给定排序数组
* @param {array} items 需要排序的数组
* @param {array} keyName 排序的key
* @param {array} keyOrder 给定排序
* @returns
*/
const sortArrayByOrder = (items, keyName, keyOrder) => {
return items.sort((a, b) => {
return keyOrder.indexOf(a[keyName]) - keyOrder.indexOf(b[keyName]);
});
};
/**
* 合并Object, 递归地
*/
function merge(...objects) {
const isDeep = objects.some((obj) => obj !== null && typeof obj === 'object');
const result = objects[0] || (isDeep ? {} : objects[0]);
for (let i = 1; i < objects.length; i++) {
const obj = objects[i];
if (!obj) continue;
Object.keys(obj).forEach((key) => {
const val = obj[key];
if (isDeep) {
if (Array.isArray(val)) {
result[key] = [].concat(Array.isArray(result[key]) ? result[key] : [result[key]], val);
} else if (typeof val === 'object') {
result[key] = merge(result[key], val);
} else {
result[key] = val;
}
} else {
result[key] = typeof val === 'boolean' ? val : result[key];
}
});
}
return result;
}
/**
* 数组分组
* - 相当于 lodash _.groupBy
* @see https://www.lodashjs.com/docs/lodash.groupBy#_groupbycollection-iteratee_identity
*/
function groupBy(array = [], callback) {
return array.reduce((groups, item) => {
const key = typeof callback === 'function' ? callback(item) : item[callback];
if (!groups[key]) {
groups[key] = [];
}
groups[key].push(item);
return groups;
}, {});
}
/**
* 创建一个从 object 中选中的属性的对象
* @param {*} object
* @param {array} keys
*/
function pick(object, keys) {
return keys.reduce((obj, key) => {
if (object && Object.prototype.hasOwnProperty.call(object, key)) {
obj[key] = object[key];
}
return obj;
}, {});
}
/**
* 返回对象的副本经过筛选以省略指定的键
* @param {*} object
* @param {string[]} keysToOmit
* @returns
*/
function omit(object, keysToOmit) {
return Object.fromEntries(Object.entries(object).filter(([key]) => !keysToOmit.includes(key)));
}
/**
* 深拷贝
*/
function cloneDeep(value, visited = new WeakMap()) {
// 处理循环引用
if (visited.has(value)) {
return visited.get(value);
}
// 特殊对象和基本类型处理
if (value instanceof Date) {
return new Date(value);
}
if (value instanceof RegExp) {
return new RegExp(value.source, value.flags);
}
if (value === null || typeof value !== 'object') {
return value;
}
// 创建一个新的WeakMap项以避免内存泄漏
let result;
if (Array.isArray(value)) {
result = [];
visited.set(value, result);
} else {
result = {};
visited.set(value, result);
}
for (const key of Object.getOwnPropertySymbols(value)) {
// 处理Symbol属性
result[key] = cloneDeep(value[key], visited);
}
for (const key in value) {
if (Object.prototype.hasOwnProperty.call(value, key)) {
// 处理普通属性
result[key] = cloneDeep(value[key], visited);
}
}
return result;
}
/**
* 向零四舍五入, 固定精度设置
*/
function curriedFix(precision = 0) {
return function (number) {
// Shift number by precision places
const shift = Math.pow(10, precision);
const shiftedNumber = number * shift;
// Round to nearest integer
const roundedNumber = Math.round(shiftedNumber);
// Shift back decimal place
return roundedNumber / shift;
};
}
/**
* 向零四舍五入, 保留2位小数
*/
const fixTo2Decimals = curriedFix(2);
/**
* 向零四舍五入, 保留4位小数
*/
const fixTo4Decimals = curriedFix(4);
const fixTo1Decimals = curriedFix(1);
const fixToInt = curriedFix(0);
/**
* 创建一个按大小分组的元素数组
*/
const chunk = (input = [], size = 0) => {
return input.reduce((arr, item, idx) => {
return idx % size === 0 ? [...arr, [item]] : [...arr.slice(0, -1), [...arr.slice(-1)[0], item]];
}, []);
};
/**
* 映射
* @example
* const keyMap = {
a: [{key: 'a1'}, {key: 'a2', transform: v => v * 2}],
b: {key: 'b1'}
};
const result = objectMapper({a: 1, b: 3}, keyMap);
// result = {a1: 1, a2: 2, b1: 3}
*
*/
function objectMapper(input, keyMap, keep = true) {
// Loop through array mapping
if (Array.isArray(input)) {
return input.map((obj) => objectMapper(obj, keyMap));
}
if (typeof input === 'object') {
const mappedObj = {};
Object.keys(input).forEach((key) => {
// Keep original keys not in keyMap
if (!keyMap[key] && keep) {
mappedObj[key] = input[key];
}
// Handle array of maps
if (Array.isArray(keyMap[key])) {
keyMap[key].forEach((map) => {
let value = input[key];
if (map.transform) value = map.transform(value);
mappedObj[map.key] = value;
});
// Handle single map
} else {
const map = keyMap[key];
if (map) {
let value = input[key];
if (map.transform) value = map.transform(value);
mappedObj[map.key || map] = value;
}
}
});
return mappedObj;
}
return input;
}
/**
* 创建一个对应于对象路径的值数组
*/
function at(obj, path) {
let result;
if (Array.isArray(obj)) {
// array case
const indexes = path.split('.').map((i) => parseInt(i));
result = [];
for (let i = 0; i < indexes.length; i++) {
result.push(obj[indexes[i]]);
}
} else {
// object case
const indexes = path.split('.').map((i) => i);
result = [obj];
for (let i = 0; i < indexes.length; i++) {
result = [result[0]?.[indexes[i]] || undefined];
}
}
return result;
}
/**
* 删除 null/undefined
*/
function flush(collection) {
let result, len, i;
if (!collection) {
return undefined;
}
if (Array.isArray(collection)) {
result = [];
len = collection.length;
for (i = 0; i < len; i++) {
const elem = collection[i];
if (elem != null) {
result.push(elem);
}
}
return result;
}
if (typeof collection === 'object') {
result = {};
const keys = Object.keys(collection);
len = keys.length;
for (i = 0; i < len; i++) {
const key = keys[i];
const value = collection[key];
if (value != null) {
result[key] = value;
}
}
return result;
}
return undefined;
}
/**
* 千分位 格式化数字
*/
const numberFormatter = (number) => {
return new Intl.NumberFormat().format(number);
};
/**
* @example
* const obj = { a: { b: 'c' } };
* const keyArr = ['a', 'b'];
* getNestedValue(obj, keyArr); // Returns: 'c'
*/
const getNestedValue = (obj, keyArr) => {
return keyArr.reduce((acc, curr) => {
return acc && Object.prototype.hasOwnProperty.call(acc, curr) ? acc[curr] : undefined;
// return acc && acc[curr];
}, obj);
};
/**
* 计算笛卡尔积
*/
const cartesianProductArray = (arr, sep = '_', index = 0, prefix = '') => {
let result = [];
if (index === arr.length) {
return [prefix];
}
arr[index].forEach((item) => {
result = result.concat(cartesianProductArray(arr, sep, index + 1, prefix ? `${prefix}${sep}${item}` : `${item}`));
});
return result;
};
const stringToColour = (str) => {
var hash = 0;
for (let i = 0; i < str.length; i++) {
hash = str.charCodeAt(i) + ((hash << 5) - hash);
}
var colour = '#';
for (let i = 0; i < 3; i++) {
var value = (hash >> (i * 8)) & 0xff;
value = (value % 150) + 50;
colour += ('00' + value.toString(16)).substr(-2);
}
return colour;
};
const debounce = (func, wait, immediate) => {
var timeout;
return function () {
var context = this,
args = arguments;
clearTimeout(timeout);
if (immediate && !timeout) func.apply(context, args);
timeout = setTimeout(function () {
timeout = null;
if (!immediate) func.apply(context, args);
}, wait);
};
};
const removeFormattingChars = (str) => {
const regex = /[\r\n\t\v\f]/g;
str = str.replace(regex, ' ');
// Replace more than four consecutive spaces with a single space
str = str.replace(/\s{4,}/g, ' ');
return str;
};
const olog = (text, ...args) => {
console.log(`%c ${text} `, 'background:#fb923c ; padding: 1px; border-radius: 3px; color: #fff', ...args);
};
const sanitizeFilename = (str) => {
// Remove whitespace and replace with hyphens
str = str.replace(/\s+/g, '-');
// Remove invalid characters and replace with hyphens
str = str.replace(/[^a-zA-Z0-9.-]/g, '-');
// Replace consecutive hyphens with a single hyphen
str = str.replace(/-+/g, '-');
// Trim leading and trailing hyphens
str = str.replace(/^-+|-+$/g, '');
return str;
};
const formatBytes = (bytes, decimals = 2) => {
if (bytes === 0) return '';
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
};
const calcCacheSizes = async () => {
try {
let swCacheSize = 0;
let diskCacheSize = 0;
let indexedDBSize = 0;
// 1. Get the service worker cache size
if ('caches' in window) {
const cacheNames = await caches.keys();
for (const name of cacheNames) {
const cache = await caches.open(name);
const requests = await cache.keys();
for (const request of requests) {
const response = await cache.match(request);
swCacheSize += Number(response.headers.get('Content-Length')) || 0;
}
}
}
// 2. Get the disk cache size
// const diskCacheName = 'disk-cache';
// const diskCache = await caches.open(diskCacheName);
// const diskCacheKeys = await diskCache.keys();
// for (const request of diskCacheKeys) {
// const response = await diskCache.match(request);
// diskCacheSize += Number(response.headers.get('Content-Length')) || 0;
// }
// 3. Get the IndexedDB cache size
// const indexedDBNames = await window.indexedDB.databases();
// for (const dbName of indexedDBNames) {
// const db = await window.indexedDB.open(dbName.name);
// const objectStoreNames = db.objectStoreNames;
// if (objectStoreNames !== undefined) {
// const objectStores = Array.from(objectStoreNames).map((storeName) => db.transaction([storeName], 'readonly').objectStore(storeName));
// for (const objectStore of objectStores) {
// const request = objectStore.count();
// request.onsuccess = () => {
// indexedDBSize += request.result;
// };
// }
// }
// }
return { swCacheSize, diskCacheSize, indexedDBSize, totalSize: Number(swCacheSize) + Number(diskCacheSize) + indexedDBSize };
} catch (error) {
console.error('Error getting cache sizes:', error);
}
};
const clearAllCaches = async (cb) => {
try {
// 1. Clear the service worker cache
if ('caches' in window) {
// if (navigator.serviceWorker) {
const cacheNames = await caches.keys();
await Promise.all(cacheNames.map((name) => caches.delete(name)));
}
// 2. Clear the disk cache (HTTP cache)
// const diskCacheName = 'disk-cache';
// await window.caches.delete(diskCacheName);
// const diskCache = await window.caches.open(diskCacheName);
// const diskCacheKeys = await diskCache.keys();
// await Promise.all(diskCacheKeys.map((request) => diskCache.delete(request)));
// 3. Clear the IndexedDB cache
const indexedDBNames = await window.indexedDB.databases();
await Promise.all(indexedDBNames.map((dbName) => window.indexedDB.deleteDatabase(dbName.name)));
// Unregister the service worker
const registration = await navigator.serviceWorker.getRegistration();
if (registration) {
await registration.unregister();
console.log('Service worker unregistered');
} else {
console.log('No service worker registered');
}
if (typeof cb === 'function') {
cb();
}
} catch (error) {
console.error('Error clearing caches or unregistering service worker:', error);
}
};
const loadScript = (src) => {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.type = 'text/javascript';
script.onload = resolve;
script.onerror = reject;
script.crossOrigin = 'anonymous';
script.src = src;
if (document.head.append) {
document.head.append(script);
} else {
document.getElementsByTagName('head')[0].appendChild(script);
}
});
};
//格式化为冒号时间2010转为20:10
const formatColonTime = (text) => {
const hours = text.substring(0, 2);
const minutes = text.substring(2);
return `${hours}:${minutes}`;
};
// 生成唯一 36 位数字,用于新增记录 ID 赋值React key 属性等
const generateId = () => new Date().getTime().toString(36) + Math.random().toString(36).substring(2, 9);
module.exports = {
copy,
formatDate,
formatTime,
formatDatetime,
camelCase,
prepareUrl,
throttle,
clickUrl,
escape2Html,
formatPrice,
formatPercent,
isEmpty,
isNotEmpty,
sortBy,
sortKeys,
sortArrayByOrder,
merge,
groupBy,
pick,
omit,
cloneDeep,
curriedFix,
fixTo2Decimals,
fixTo4Decimals,
fixTo1Decimals,
fixToInt,
chunk,
objectMapper,
at,
flush,
numberFormatter,
getNestedValue,
cartesianProductArray,
stringToColour,
debounce,
removeFormattingChars,
olog,
sanitizeFilename,
formatBytes,
calcCacheSizes,
clearAllCaches,
loadScript,
formatColonTime,
generateId,
};

@ -0,0 +1,87 @@
const crypto = require('crypto');
const axios = require('axios');
const { get, post } = axios;
const { HEYTRIP_API, HEYTRIP_API_PROD } = require('./../config/constants');
const { chunk, isEmpty } = require('../utils/commons');
const make_token = () => {
var apiKey = '18daad53d0ec4003a207c41ddaf63b78';
var secret = 'f76e547e55964812bf94cc0d31f74333';
var timestamp = Math.round(new Date().getTime() / 1000);
var hash = crypto
.createHash('sha512')
.update(apiKey + secret + timestamp)
.digest('hex');
var authHeaderValue = 'Bearer apikey=' + apiKey + ',signature=' + hash + ',timestamp=' + timestamp;
return authHeaderValue;
};
const AvailableAccommodationIds = async (pageIndex) => {
const response = await get(`${HEYTRIP_API_PROD}/AvailableAccommodationIds?pageIndex=${pageIndex}`, {
headers: {
Authorization: make_token(),
},
});
console.log('Call pageIndex', pageIndex, response.data.TotalPage);
return response.data.Data;
};
const AccommodationsDetails = async (body) => {
if (isEmpty(body.AccommodationIds)) {
return [];
}
const response = await post(
`${HEYTRIP_API_PROD}/AccommodationsDetails`,
{ ...body },
{
headers: {
Authorization: make_token(),
},
}
);
return Object.values(response.data.Data || {});
};
const Availability = async (body) => {
// console.log('Call Heytrip');
const response = await post(
`${HEYTRIP_API_PROD}/Availability`,
{ ...body },
{
headers: {
Authorization: make_token(),
},
}
);
// console.log(response.config);
return response.data.Data || [];
};
const QuotedHotelsPrice = async (body) => {
const idsChunk = body.hotelIds.length > 10 ? chunk(body.hotelIds, 10) : [body.hotelIds];
let quoteRes = [];
for await (const piece of idsChunk) {
const response = await post(
`${HEYTRIP_API_PROD}/QuotedHotelsPrice`,
{ ...body, hotelIds: piece },
{
headers: {
Authorization: make_token(),
},
}
);
quoteRes = quoteRes.concat(response.data.Data || []);
}
return quoteRes;
};
const Country = async (param) => {};
const City = async (param) => {};
module.exports = {
AvailableAccommodationIds,
AccommodationsDetails,
Availability,
QuotedHotelsPrice,
};

@ -0,0 +1,3 @@
<h1><%= message %></h1>
<h2><%= error.status %></h2>
<pre><%= error.stack %></pre>

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html>
<head>
<title><%= title %></title>
<link rel='stylesheet' href='/stylesheets/style.css' />
</head>
<body>
<h1><%= title %></h1>
<p>EJS Welcome to <%= title %></p>
</body>
</html>
Loading…
Cancel
Save