You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

527 lines
22 KiB
JavaScript

const db = require('../config/db');
const initModels = require('./../models/init-models');
const { AvailableAccommodationIds, AccommodationsDetails, Availability } = require('../vendor/heytrip');
const { isEmpty, groupBy } = require('../utils/commons');
const { resolveDetails, resolveRatePlans } = require('../helper/heytripDataHelper');
const { DEFAULT_LGC, LGC_MAPPED, HEYTRIP_API_PROD } = 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;
const Logs = models.requestLogsModel;
const foreignOption = { onDelete: 'NO ACTION', onUpdate: 'NO ACTION' };
// 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.belongsTo(HeytripIds, { as: 'aid', foreignKey: 'hotel_id', ...foreignOption });
Hotelinfo.hasMany(Hotelinfo2, { as: 'locale_info', sourceKey: 'hotel_id', foreignKey: 'hotel_id', ...foreignOption });
// Hotelinfo2.belongsTo(Hotelinfo, { as: 'locale_info2', targetKey: 'hotel_id', foreignKey: 'hotel_id', ...foreignOption, });
Hotelinfo.hasOne(City, { as: 'city', sourceKey: 'city_id', foreignKey: 'id', ...foreignOption, }); // 多语种, 所以实际是 hasMany , 用 hasOne 要指定 lgc= 1 或者2
Hotelinfo.hasOne(Country, { as: 'country', sourceKey: 'country_code', foreignKey: 'id', ...foreignOption, }); // 多语种, 所以实际是 hasMany , 用 hasOne 要指定 lgc= 1 或者2
Hotelinfo.hasMany(Images, { sourceKey: 'hotel_id', foreignKey: 'hotel_id', ...foreignOption });
Rooms.hasMany(Images, { sourceKey: 'room_id', foreignKey: 'info_source_id', ...foreignOption });
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: HeytripIds, as: 'aid', attributes: [], }, // where: { update_flag: { [Op.ne]: 99 } }
{
model: Hotelinfo2,
as: 'locale_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
{
model: Images,
attributes: { exclude: ['lgc', 'locale'] },
where: { lgc: [0, 1], type: Sequelize.where(Sequelize.fn('IFNULL', Sequelize.col('type'), 'Mid'), 'Mid') },
order: ['info_source'],
required: false,
separate: true,
}, // separate: true
],
where: {
[Op.or]: keywordSearch,
// hotel_id: findIds,
},
order: keywordOrder,
...options,
// raw: true,
// nest: true,
});
return { count, rows: rows.map((item) => item.toJSON()) };
// return { count, rows };
};
getLastPageIndex = async (where = {}) => {
const ret = await HeytripIds.max('page_index', { where });
return ret;
};
getFirstPageIndex = async (where = {}) => {
const ret = await HeytripIds.min('page_index', { where });
return ret;
};
/**
* ************************************************************************************************************
* 同步heytrip的酒店
* 1. 获取可用的酒店id, 缺少的设为[99=下架]
* 2. 根据ID获取酒店详情, 无信息设为下架
*/
/**
* @deprecated
* 第一次同步录库, 完成
*/
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,
};
};
/**
* 获取酒店ID
*/
syncAidState = async () => {
const today = new Date();
today.setHours(0, 0, 0, 0); // set the time to 00:00:00.000
let lastPageIndex;
let pageIndex;
// lastPageIndex = await this.getFirstPageIndex({ last_modify_time: { [Op.lt]: today }, page_index: { [Op.lt]: 9999 } });
// pageIndex = lastPageIndex;
if (isEmpty(lastPageIndex)) {
lastPageIndex = await this.getLastPageIndex({ last_modify_time: { [Op.gt]: today }, page_index: { [Op.gt]: 0 } });
// console.log('syncAidState', 'lastPageIndex', lastPageIndex);
pageIndex = (lastPageIndex || 0) + 1;
}
console.log('syncAidState', lastPageIndex, pageIndex);
const validIds = (await AvailableAccommodationIds(pageIndex)).map((id) => String(id));
if (isEmpty(validIds)) {
// await HeytripIds.update({ update_flag: 99, priority: 99 }, { where: { pageIndex: [Op.gt]: pageIndex } });
// 同步结束; 本次没有的ID: 更新: 99=失效
const stateOff = await HeytripIds.findAll({
raw: true, logging: false,
where: {
// update_flag: 99,
// page_index: { [Op.lt]: 0 },
// last_modify_time: { [Op.gt]: today },
last_modify_time: { [Op.lt]: today },
},
attributes: ['hotel_id'],
});
const stateOffIds = stateOff.map((item) => item.hotel_id);
if (!isEmpty(stateOffIds)) {
// await HeytripIds.update({ update_flag: 99, priority: 99 }, { where: { hotel_id: stateOffIds }, logging: false, });
// console.log('updated stateOff', stateOffIds.length);
await HeytripIds.update({ update_flag: 99, priority: 99, page_index: Sequelize.literal('-page_index'), last_modify_time: Sequelize.fn('NOW') }, { where: { last_modify_time: { [Op.lt]: today }, }, logging: false, });
}
return {
nextPage: false,
};
}
const savedIds = await HeytripIds.findAll({
raw: true, logging: false,
where: { hotel_id: validIds },
attributes: ['hotel_id'],
}); // savedIds <= validIds
const savedPageIds = await HeytripIds.findAll({
raw: true, logging: false,
where: { page_index: pageIndex, },
attributes: ['hotel_id'],
});
// 已存在ID: 更新: 状态, 页码, 时间
const stateNormal = savedIds.filter((item) => validIds.includes((item.hotel_id))).map((item) => item.hotel_id);
if (!isEmpty(stateNormal)) {
await HeytripIds.update({ update_flag: 0, priority: 10, page_index: pageIndex, last_modify_time: Sequelize.fn('NOW') }, { where: { hotel_id: stateNormal } ,logging: false, });
}
// 新增ID
const newIds = validIds.filter((id) => !savedIds.map((item) => item.hotel_id).includes(id));
if (!isEmpty(newIds)) {
const insertRows = newIds.map((id) => ({ hotel_id: id, page_index: pageIndex, update_flag: 1, priority: -10 }));
await HeytripIds.bulkCreate(insertRows);
}
// 页码滚动
const oldToNext = savedPageIds.filter((item) => !validIds.includes((item.hotel_id))).map((item) => item.hotel_id);
if (!isEmpty(oldToNext)) {
await HeytripIds.update({ page_index: -Number(pageIndex), update_flag: 99, last_modify_time: Sequelize.fn('NOW') }, { where: { hotel_id: oldToNext },logging: false, });
}
return {
nextPage: true,
pageIndex,
};
}
/**
* 获取新添加的酒店
* 在`syncAidState`中设置了`update_flag=1`, `priority=-10`
*/
newHotels = async (lgc) => {
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 = 1
ORDER BY info_exists LIMIT 10`
, { logging: false }
);
const res = await this.syncInitHotelLgcDetailsAction(rows, LGC_MAPPED[lgc]);
return res;
};
/**
* 获取缺少的语种详情
*/
newHotelsLgc = async (lgc) => {
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
-- AND update_flag = 0
ORDER BY priority LIMIT 10`
, { logging: false }
);
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;
};
/**
* 中国酒店: 获取缺少的语种详情
* * 中国: id > 20000000
*/
chinaHotelsLgc2 = async (lgc) => {
const [rows] = await Sequelize.query(
`SELECT i.hotel_id
-- FROM hotelinfo AS h
FROM heytrip_ids AS i
LEFT JOIN hotelinfo2 AS h2 ON i.hotel_id =h2.hotel_id
AND h2.lgc = ${lgc}
WHERE h2.hi2_sn IS NULL
AND i.hotel_id > 20000000
AND i.update_flag != 99
LIMIT 10`
, { logging: false }
);
// 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;
};
/**
* @deprecated
* 第一次录库, 已执行
*/
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 result = await Sequelize.transaction(async (transaction) => {
const sequelizeOptions = { logging: false, transaction };
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 };
}
};
/**
* 录入酒店的信息
* todo: 更新详情
*/
syncInitHotelLgcDetailsAction = async (rows, lgcObj = LGC_MAPPED[DEFAULT_LGC]) => {
try {
const 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 }, attributes: ['hotel_id', 'hotel_name', 'address'] });
const _BaseInfoExistsMapped = _BaseInfoExists.reduce((ru, c) => ({...ru, [`${c.hotel_id}`]: c }), {});
const existsIds = _BaseInfoExists.map((item) => `${item.hotel_id}`);
const res = await AccommodationsDetails({
Language: lgcObj.locale,
AccommodationIds: allIds,
});
// hotel info
const insertData = resolveDetails(res, lgcObj);
// return insertData; // debug: 0
const resIds = insertData.info.map((item) => `${item.hotel_id}`);
/** 开始Database */
const result = await Sequelize.transaction(async (transaction) => {
const sequelizeOptions = { logging: false, transaction };
let Info;
/**
* 无返回数据, 设为失效`99`
* * 但部分中国酒店, 只有中文数据, 请求英文无返回
*/
const offInfo = allIds.filter((iitem) => !resIds.includes(`${iitem}`));
if (!isEmpty(offInfo)) {
const flag = Number(lgcObj.lgc) === 1 ? 2 : 99; // 等待获取中文数据
const priority = Number(lgcObj.lgc) === 1 ? 5 : 99;
await HeytripIds.update({ update_flag: flag, priority, last_modify_time: Sequelize.fn('NOW') }, { where: { hotel_id: offInfo }, ...sequelizeOptions });
}
const updateInfo = insertData.info.filter((iitem) => existsIds.includes(`${iitem.hotel_id}`));
if (!isEmpty(updateInfo)) {
await HeytripIds.update({ update_flag: 0, priority: 5, last_modify_time: Sequelize.fn('NOW') }, { where: { hotel_id: updateInfo.map((x) => x.hotel_id) }, ...sequelizeOptions });
for await (const updateRow of updateInfo) {
// 有中英文的, 把名称合并, eg.上海意家人酒店Yijiaren Hotel
const _BaseName = _BaseInfoExistsMapped[`${updateRow.hotel_id}`].hotel_name;
if ((_BaseName || '').includes((updateRow.hotel_name || '').substring(0, 4))) {
continue;
}
const _BaseAddress = _BaseInfoExistsMapped[`${updateRow.hotel_id}`].address;
await Hotelinfo.update(
{
update_flag: 0, priority: 0,
hotel_name: Number(lgcObj.lgc) === 1 ? `${_BaseName}${updateRow.hotel_name}` : `${updateRow.hotel_name}${_BaseName}`,
address: Number(lgcObj.lgc) === 1 ? `${_BaseAddress}${updateRow.address}` : `${updateRow.address}${_BaseAddress}`,
},
{ where: { hotel_id: updateRow.hotel_id }, ...sequelizeOptions }
);
}
}
const newInfo = insertData.info.filter((iitem) => !existsIds.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, priority: 10, last_modify_time: Sequelize.fn('NOW') }, { where: { hotel_id: newInfo.map((x) => x.hotel_id) }, ...sequelizeOptions });
if (!isEmpty(updateInfo)) await HeytripIds.update({ update_flag: 0, priority: 10, last_modify_time: Sequelize.fn('NOW') }, { where: { hotel_id: updateInfo.map((x) => x.hotel_id) }, ...sequelizeOptions });
return Info;
});
return { next: !isEmpty(allIds), data: allIds };
} catch (error) {
console.log(error);
return { next: false, restart: true, };
}
};
hotelRooms = async (aid) => {
const res = await Rooms.findAll({
where: { hotel_id: aid, },
attributes: [ 'room_id', 'lgc', 'locale',
['room_name', 'RoomName'],
// ['locale_name', 'LocaleName'],
['bed_type_desc', 'BedTypeDesc'],
['smoking', 'Smoking'],
['area', 'Area'],
],
// include: [
// { model: Images, where: { lgc: [0, 1] }, required: false, separate: true },
// ],
});
return res.map(row => row.toJSON());
};
/**
* 获取实时的报价
*/
getHotelAvailability = async (param) => {
const { hotel_id, checkin, checkout, adults, children_ages, rooms, nationality } = param;
const _hotelRooms = await this.hotelRooms(hotel_id);
const hotelRoomsMappedByLgc = groupBy(_hotelRooms, 'lgc');
const roomRes = hotelRoomsMappedByLgc['2'] || hotelRoomsMappedByLgc['1'];
const roomMappedByID = (roomRes || []).reduce((rr, c) => ({...rr, [c.room_id]: c}), {});
const langIfCN = Number(hotel_id) > 20000000 ? 'zh-CN' : 'en-US';
const paramBody = {
Language: langIfCN,
// 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);
const roomsQuote = quoteRes.map((row) => ({...row, ...(roomMappedByID[row.RoomId] || {})}));
this.writeHeytripRequestLog({
action: 'heytripAvailability',
body: paramBody,
});
return roomsQuote;
};
writeHeytripRequestLog = async ({ action, body }) => {
const actionMapped = {
'heytripAvailability': '/Accommodation/Availability',
'heytripDetails': '/Accommodation/AccommodationsDetails',
'heytripIds': '/Accommodation/AvailableAccommodationIds',
'heytripBasePrice': '/Accommodation/QuotedHotelsPrice',
};
const data = {
action: action,
// method: ctx.method,
path: actionMapped[action],
request_data: JSON.stringify(body),
// ip: ctx.ip,
};
return await Logs.create(data, { logging: false });
};
}
module.exports = new Heytrip();