UNPKG

@hosoft/restful-api-framework

Version:

Base framework of the headless cms HoServer provided by http://helloreact.cn

333 lines (285 loc) 11 kB
const _ = require('lodash') const moment = require('moment') const { CacheManager, Constants, ErrorCodes } = require('../../../base') const { DbHelper } = require('../../../../helpers') const { Message, MessageReadStatus, User } = require('../../../models') /** * System message service */ class MessageService { /** * Get the message list, query the messages within 1 year at most * @param userId */ async getMessageList(userId, args) { let { to_scope, to, type, offset, page_size, read_status, timestamp } = args page_size = parseInt(page_size) || Constants.PAGE_SIZE offset = parseInt(offset) || 0 const query = {} if (to_scope) { query.to_scope = to_scope } if (type) { query.type = type } if (to) { query.to = to } if (read_status !== undefined) { query.read_status = parseInt(read_status) === 1 ? 1 : { $ne: 1 } } const maxDate = moment().add(-1, 'years') if (timestamp) { query.created_at = { $lte: new Date(timestamp), $gt: maxDate.toDate() } } else { query.created_at = { $gte: maxDate.toDate() } } const aggregateQuery = [{ $match: query }] if (to_scope !== 'personal') { aggregateQuery.push({ $lookup: { from: 'message_read_status', localField: 'id', foreignField: 'message_id', as: 'read_status_rel' } }) } if (query.read_status) { aggregateQuery.push({ $match: { 'read_status_rel.read_status': query.read_status } }) } aggregateQuery.push({ $sort: { created_at: -1 } }) aggregateQuery.push({ $skip: offset }) aggregateQuery.push({ $limit: page_size * 2 }) // there is filtering, read twice as much data each time const result = { offset: offset, page_size: page_size, records: [] } const resultHash = {} while (result.records.length < page_size) { const messages = await Message.aggregate(aggregateQuery) if (!messages || messages.length == 0) break // organize data await DbHelper.populateData(messages, 'from', User, 'user_id') if (to_scope !== 'personal') { for (const message of messages) { if (message.to_scope === 'site') { message.read_status = _.get(message.read_status_rel, 'read_status', 0) } delete message.read_status_rel } } // merge data for (let i = 0; i < messages.length; i++) { const message = messages[i] const newMsgRow = await this._mergeMessage(message, resultHash) if (newMsgRow) { if (result.records.length === page_size) break result.records.push(newMsgRow) resultHash[newMsgRow.hash] = newMsgRow } result.offset++ } aggregateQuery[aggregateQuery.length - 2].$skip = result.offset if (messages.length < page_size * 2) break } // keep up to 3 records this._trimSameKindMessage(result.records) // TODO: reset badge // this._getImController().resetNotificationHistory(userId, 'message', null) // remove hash if (result.records) { for (const record of result.records) { if (record.hash) { delete record.hash } } } return result } /** * create message * @param context * @returns {Promise<void>} */ async createMessage(args) { if (!(args.to_scope && args.action)) { return Promise.reject({ message: tf('errParameter'), code: ErrorCodes.GENERAL_ERR_PARAM }) } const newMessage = await Message.create(args) logger.info(`New message created, id: ${newMessage.id}, action: ${newMessage.action}`) // reset cache if (args.to_scope === 'personal') { await CacheManager.deleteCache('UnreadMessages', args.to) // TODO: notice client to refresh badge // emitter.pushNotice(message.to + '', { data: null }) } return newMessage } /** * set message status as read * @returns {Promise<void>} */ async setMessageReadStatus(userId, msgIds) { if (typeof msgIds === 'string') { msgIds = msgIds.split(',') } const messagesToSet = await Message.find({ to: userId, id: { $in: msgIds } }) if (messagesToSet.length > 0) { const messageStatusIds = [] for (const message of messagesToSet) { if (!message.to_scope || message.to_scope === 'personal') { message.read_status = 1 await message.save() logger.info(`Personal message status set: ${message.id}, ${message.read_status}`) } else { messageStatusIds.push(message.id) } } if (messageStatusIds.length > 0) { const existMessageStatus = await MessageReadStatus.find({ message_id: { $in: messageStatusIds }, to: userId }) for (const msgId of messageStatusIds) { const existMsgStat = existMessageStatus.find((item) => item.message_id == msgId) if (existMsgStat) { existMsgStat.read_status = 1 existMsgStat.read_time = Date.now() await existMsgStat.save() logger.info(`Scope message status set: ${msgId}, ${existMsgStat.read_status}`) } else { const newRecord = await MessageReadStatus.create({ message_id: msgId, to: userId, read_status: 1, read_time: Date.now() }) logger.info( `Message status newRecord created, msg: ${msgId}, to: ${userId}, id: ${newRecord.id}` ) } } } // emitter.pushNotice(to + '', { data: unreadMessasges }) } } /// /////////// private functions ///////////// /** * get time range display string * @param time * @returns {{slot: number, range: number, range_unit: string}} */ _getTimeRange(time) { const now = moment().utc() time = time.utc() // unit is second const rangeSec = (now - time) / 1000.0 / 60.0 let slot = 1.0 let range = 1.0 let range_unit = '' // within 1 hour if (rangeSec < 60) { slot = 1 range = Math.ceil(rangeSec) range_unit = tf('minute') } // within 1 day else if (rangeSec < 1440 /* 60*24 */) { slot = 2 range = Math.ceil(rangeSec / 60) range_unit = tf('hour') } // witin 1 week else if (rangeSec < 10080 /* 60*24*7 */) { slot = 3 range = Math.ceil(rangeSec / 1440) range_unit = tf('day') } // within 1 month else if (rangeSec < 43200 /* 60*24*30 */) { slot = 4 range = Math.ceil(rangeSec / 10080) range_unit = tf('week') } // within 3 month else if (rangeSec < 131040 /* 60*24*91 */) { slot = 5 range = Math.ceil(rangeSec / 43200) range_unit = tf('month') } // more than 3 months else { slot = 6 range = Math.ceil(rangeSec / 43200) range_unit = tf('month') } return { slot: slot, range: range, range_unit: range_unit } } /** * meerge same type records, keep up to 3 records */ _trimSameKindMessage(messages) { for (let i = 0; i < messages.length; i++) { const msgData = messages[i] if (msgData.id && msgData.id.length > 2) { msgData.id_count = msgData.id.length msgData.id.splice(2) } if (msgData.user && msgData.user.length > 2) { msgData.user_count = msgData.user.length msgData.user.splice(2) } } } /** * Message merge, the merge rules are as follows: * * Displayed N hours ago for time of current day, N is rounded down, for example: * 1 hour and 30 minutes, then display 2 hours ago * Less than 1 hour, display N minutes ago, N rounded down, for example: * 55 minutes and 30 seconds, display 56 minutes ago * Less than 1 minute, display 1 minute ago * Within a week, if more than one day is displayed N days ago, N is rounded up, for example: * 1 day and 1 hour, display 1 day ago * Two weeks-within one month, merge one week before the other, showing 2 weeks ago, 3 weeks ago, 4 weeks ago * One month-3 months, combined one month and one month, showing 1 month ago, 2 months ago, 3 months ago * More than three months, all merged, show as three months ago */ async _mergeMessage(message, resultHash) { const createTime = moment(message.created_at) const timeRange = this._getTimeRange(createTime) const hash = `${timeRange.slot}_${message.from}_${message.action}_${message.type}_${message.type_id}` const user = message.from_rel || {} if (resultHash[hash]) { const existRow = resultHash[hash] // The same user only count once in the same time slot const existUser = existRow.user.findIndex((item) => item.user_id === user.user_id) if (existUser < 0) { existRow.id.push(message.id) // message id existRow.user.push(user) existRow.count++ } return null } else { const newRow = { hash: hash, id: [message.id], user: [user], action: message.action, message: message.message, timestamp: message.created_at, time_range: timeRange, type: message.type, type_id: message.type_id, read_status: message.read_status, count: 1 } return newRow } } // END: for } module.exports = new MessageService()