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 , 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 ;
// 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' , onDelete : 'NO ACTION' , onUpdate : 'NO ACTION' } ) ;
Hotelinfo . hasMany ( Hotelinfo2 , { as : 'locale_info' , sourceKey : 'hotel_id' , foreignKey : 'hotel_id' , onDelete : 'NO ACTION' , onUpdate : 'NO ACTION' } ) ;
// Hotelinfo2.belongsTo(Hotelinfo, { as: 'locale_info2', targetKey: '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
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 } ,
] ,
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 } } ) ;
pageIndex = lastPageIndex ;
if ( isEmpty ( lastPageIndex ) ) {
lastPageIndex = await this . getLastPageIndex ( { last _modify _time : { [ Op . gt ] : today } , page _index : { [ Op . lt ] : 9999 } } ) ;
// console.log('syncAidState', 'lastPageIndex', lastPageIndex);
pageIndex = lastPageIndex + 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 ,
where : {
update _flag : { [ Op . notIn ] : [ 0 , 1 ] } ,
last _modify _time : { [ Op . gt ] : 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 } } ) ;
console . log ( 'updated stateOff' , stateOffIds . length ) ;
}
return {
nextPage : false ,
} ;
}
const savedIds = await HeytripIds . findAll ( {
raw : true ,
where : { hotel _id : validIds } ,
attributes : [ 'hotel_id' ] ,
logging : false ,
} ) ; // savedIds <= validIds
const savedPageIds = await HeytripIds . findAll ( {
raw : true ,
where : { page _index : pageIndex , } ,
attributes : [ 'hotel_id' ] ,
// logging: false,
} ) ;
// 已存在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 , page _index : pageIndex , last _modify _time : Sequelize . fn ( 'NOW' ) } , { where : { hotel _id : stateNormal } } ) ;
}
// 新增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 ) + 9999 , update _flag : 99 } , { where : { hotel _id : oldToNext } } ) ;
}
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 info _exists 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 } , } ) ;
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 ? 0 : 99 ;
await HeytripIds . update ( { update _flag : flag , priority } , { 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 : 0 } , { 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 : 0 } , { where : { hotel _id : newInfo . map ( ( x ) => x . hotel _id ) } , ... sequelizeOptions } ) ;
if ( ! isEmpty ( updateInfo ) ) await HeytripIds . update ( { update _flag : 0 , priority : 0 } , { 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 , } ;
}
} ;
/ * *
* 获取实时的报价
* /
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 ;
} ;
writeHeytripRequestLog = async ( { action , body } ) => {
const actionMapped = {
'getHotelAvailability' : '/Accommodation/Availability' ,
'getHotelInfo' : '/Accommodation/AccommodationsDetails' ,
'getHotelIds' : '/Accommodation/AvailableAccommodationIds' ,
} ;
const data = {
action : action ,
// method: ctx.method,
path : actionMapped [ action ] ,
request _data : body ,
// ip: ctx.ip,
} ;
return await Logs . create ( data , { logging : false } ) ;
} ;
}
module . exports = new Heytrip ( ) ;