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 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 ], 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.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, logging: false, 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, 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: 0, 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)+9999, update_flag: 99 }, { 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 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, }; } }; 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 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); 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();