/** * 文章统计相关接口 */ const common = require("../../common"); const dayjs = require("dayjs"); let db = null; let statsDB = null; exports.main = async (content, mongodb) => { db = mongodb; statsDB = db.collection("article-stats"); switch (content.type) { case "addArticleSendRecord": return await exports.addArticleSendRecord(content); case "addArticleReadRecord": return await exports.addArticleReadRecord(content); case "getArticleStats": return await exports.getArticleStats(content); case "getArticleTrend": return await exports.getArticleTrend(content); case "getSendDetail": return await exports.getSendDetail(content); case "getReadDetail": return await exports.getReadDetail(content); case "getArticleListStats": return await exports.getArticleListStats(content); case "getArticleListReadStats": return await exports.getArticleListReadStats(content); case "getArticleCount": return await getArticleCount(content); } }; /** * 新增文章的发送记录 * @param {string} corpId - 机构id * @param {string} userId - 员工id * @param {string} articleId - 文章id * @param {string} customerId - 发送的客户id */ exports.addArticleSendRecord = async (context) => { const { corpId, articleId, userId, customerId } = context; if (!corpId) return { success: false, message: "机构id不能为空" }; if (!userId) return { success: false, message: "成员id不能为空" }; if (!articleId) return { success: false, message: "文章id不能为空" }; if (!customerId) return { success: false, message: "客户id不能为空" }; try { await statsDB.insertOne({ _id: common.generateRandomString(24), corpId, articleId, userId, customerId, type: "send", sendTime: new Date().getTime(), }); return { success: true, message: "新增文章发送记录成功" }; } catch (e) { return { success: false, message: e.message }; } }; /** * 新增文章的阅读记录 * @param {string} corpId - 机构id * @param {string} articleId - 文章id * @param {string} unionid - 阅读用户的unionid */ exports.addArticleReadRecord = async (context) => { const { corpId, articleId, unionid, accountId } = context; if (!corpId) return { success: false, message: "机构id不能为空" }; if (!articleId) return { success: false, message: "文章id不能为空" }; try { const readerId = typeof accountId === "string" && accountId.trim() ? accountId.trim() : Array.isArray(unionid) ? unionid.filter(Boolean)[0] : typeof unionid === "string" ? unionid.trim() : ""; // 支付宝小程序:已登录优先使用 accountId;未登录使用 visitorId(通过 unionid 传入)。 if (!readerId) { return { success: false, message: "accountId/visitorId 不能为空" }; } await upsertAnonymousReadRecord({ corpId, articleId, unionid: readerId }); return { success: true, message: "新增文章阅读记录成功" }; } catch (e) { return { success: false, message: e.message }; } }; async function upsertAnonymousReadRecord({ corpId, articleId, unionid }) { const timestamp = new Date().getTime(); const record = await statsDB.findOne( { corpId, articleId, unionid, type: "read" }, { projection: { _id: 1, readTimes: 1 } } ); const readTimes = record && Array.isArray(record.readTimes) ? [...record.readTimes, timestamp] : [timestamp]; if (record) { await statsDB.updateOne( { _id: record._id }, { $set: { customers: [], readTimes, updateTime: timestamp } } ); return; } await statsDB.insertOne({ _id: common.generateRandomString(24), corpId, articleId, unionid, customers: [], readTimes, type: "read", createTime: timestamp, updateTime: timestamp, }); } /** * 获取文章的统计数据 * @param {string} corpId - 机构id * @param {string} articleId - 文章id * @returns {number} sendCount - 总发送次数 * @returns {number} todaySendCount - 今日发送次数 * @returns {number} readCount - 总阅读次数 * @returns {number} todayReadCount - 今日阅读次数 */ exports.getArticleStats = async (context) => { const { corpId, articleId } = context; if (!corpId) return { success: false, message: "机构id不能为空" }; if (!articleId) return { success: false, message: "文章id不能为空" }; try { const sendCount = await statsDB.countDocuments({ corpId, articleId, type: "send", }); const todaySendCount = await statsDB.countDocuments({ corpId, articleId, type: "send", sendTime: { $gte: dayjs().startOf("day").valueOf(), $lte: dayjs().endOf("day").valueOf(), }, }); const readCount = await getReadCount(corpId, articleId); const todayReadCount = await getReadCount( corpId, articleId, dayjs().startOf("day").valueOf(), dayjs().endOf("day").valueOf() ); return { success: true, message: "查询成功", data: { sendCount, todaySendCount, readCount, todayReadCount, }, }; } catch (e) { return { success: false, message: e.message }; } }; /** * 查询阅读次数 * @param {string} corpId 机构id * @param {string} articleId 文章id * @param {number} startTime 开始时间 * @param {number} endTime 结束时间 * @returns {number} 返回阅读次数 */ async function getReadCount(corpId, articleId, startTime, endTime) { try { const query = { corpId, articleId, type: "read" }; const readTimes = []; if (startTime) readTimes.push({ $gte: startTime }); if (endTime) readTimes.push({ $lte: endTime }); if (readTimes.length) query.readTimes = { $and: readTimes }; const res = await statsDB .aggregate([ { $unwind: "$readTimes" }, { $match: query }, { $group: { _id: null, count: { $sum: 1 } } }, ]) .toArray(); const count = res[0] && res[0].count; return count || 0; } catch (e) { return 0; } } /** * 获取文章 发送以及阅读趋势 * @param {string} corpId 机构id * @param {string} articleId 文章id * @param {number} startTime 开始时间 * @param {number} endTime 结束时间 * @returns */ exports.getArticleTrend = async ({ corpId, articleId, startTime, endTime }) => { if (!corpId) return { success: false, message: "机构id不能为空" }; if (!articleId) return { success: false, message: "文章id不能为空" }; if (!startTime || !dayjs(startTime).isValid()) return { success: false, message: "开始时间不能为空或者格式不正确" }; if (!endTime || !dayjs(endTime).isValid()) return { success: false, message: "结束时间不能为空或者格式不正确" }; try { startTime = dayjs(startTime).startOf("day").valueOf(); endTime = dayjs(endTime).endOf("day").valueOf(); const readTrend = getReadTrend({ corpId, articleId, startTime, endTime }); const sendTrend = getSendTrend({ corpId, articleId, startTime, endTime }); const [read, send] = await Promise.all([readTrend, sendTrend]); return { read, send, success: true, message: "获取趋势数据成功" }; } catch (e) { return { success: false, message: e.message }; } }; /** * 获取发送趋势 */ async function getSendTrend({ corpId, articleId, startTime, endTime }) { try { const res = await statsDB .aggregate([ { $match: { corpId, articleId, type: "send", sendTime: { $gte: startTime, $lte: endTime }, }, }, { $addFields: { _sendTimeStamp: { $toDate: "$sendTime" }, }, }, { $group: { _id: { $dateToString: { format: "%Y-%m-%d", date: "$_sendTimeStamp" }, }, count: { $sum: 1 }, }, }, ]) .toArray(); const result = {}; res.forEach((item) => { result[item._id] = item.count; }); return result; } catch (e) { return Promise.reject(e.message); } } /** * 获取阅读趋势 * @param {*} corpId * @param {*} articleId * @param {*} startTime * @param {*} endTime * @returns */ async function getReadTrend({ corpId, articleId, startTime, endTime }) { try { const res = await statsDB .aggregate([ { $unwind: "$readTimes" }, { $match: { type: "read", corpId, articleId, readTimes: { $gte: startTime, $lte: endTime }, }, }, { $addFields: { _readTimeStamp: { $toDate: "$readTimes" }, }, }, { $group: { _id: { $dateToString: { format: "%Y-%m-%d", date: "$_readTimeStamp" }, }, count: { $sum: 1 }, }, }, ]) .toArray(); const result = {}; res.forEach((item) => { result[item._id] = item.count; }); return result; } catch (e) { return Promise.reject(e.message); } } /** * 获取文章发送明细 * @param {string} corpId 机构id * @param {string} articleId 文章id * @returns */ exports.getSendDetail = async ({ corpId, articleId }) => { if (!corpId) return { success: false, message: "机构id不能为空" }; if (!articleId) return { success: false, message: "文章id不能为空" }; try { const res = await statsDB .aggregate([ { $match: { corpId, articleId, type: "send" } }, { $group: { _id: "$userId", count: { $sum: 1 } } }, { $lookup: { from: "corp-member", let: { userId: "$_id" }, pipeline: [ { $match: { $expr: { $eq: ["$userid", "$$userId"] } } }, { $project: { _id: 0, avatar: 1 } }, { $limit: 1 }, ], as: "avatarInfo", }, }, ]) .toArray(); return { success: true, message: "查询成功", list: res }; } catch (e) { return { success: false, message: e.message }; } }; /** * 获取文章阅读明细 * @param {string} corpId 机构id * @param {string} articleId 文章id * @param {number} page 页码 * @param {number} pageSize 每页数量 * @returns */ exports.getReadDetail = async ({ corpId, articleId, page = 1, pageSize = 10, }) => { if (!corpId) return { success: false, message: "机构id不能为空" }; if (!articleId) return { success: false, message: "文章id不能为空" }; try { page = Math.max(1, page); pageSize = Math.max(1, pageSize); const total = await statsDB.countDocuments({ corpId, articleId, type: "read" }); const list = await statsDB .aggregate([ { $match: { corpId, articleId, type: "read" } }, { $project: { _id: 1, unionid: 1, count: { $size: "$readTimes" }, }, }, { $sort: { updateTime: -1 } }, { $skip: (page - 1) * pageSize }, { $limit: pageSize }, ]) .toArray(); return { success: true, message: "查询成功", total, list, pages: Math.ceil(total / pageSize), }; } catch (e) { return { success: false, message: e.message }; } }; /** * 获取文章列表获取阅读量的统计情况 */ exports.getArticleListReadStats = async ({ corpId }) => { const data = await statsDB .aggregate([ { $match: { corpId, type: "read" } }, { $unwind: "$readTimes" }, { $group: { _id: "$articleId", count: { $sum: 1 } } }, { $sort: { count: -1 } }, { $limit: 10 }, ]) .toArray(); const result = data.reduce((acc, item) => { acc[item._id] = (acc[item._id] || 0) + item.count; return acc; }, {}); let list = Object.entries(result).map(([articleId, count]) => ({ articleId, count, })); list.sort((a, b) => b.count - a.count); const ids = list.map((i) => i.articleId); const articleList = await db .collection("article") .find({ corpId, _id: { $in: ids } }, { projection: { _id: 1, title: 1 } }) .toArray(); list = list .map((item) => { const article = articleList.find( (article) => article._id === item.articleId ); return { ...item, title: article ? article.title : "" }; }) .filter((i) => i.title); list = list.slice(0, 4); return { success: true, message: "获取成功", list, }; }; /** * 根据ids查询文章列表的统计情况 * @param {string} corpId 机构id * @param {string[]} ids 文章id数组 */ exports.getArticleListStats = async (corpId, ids) => { if (!corpId) return {}; if (!Array.isArray(ids) || !ids.length) return {}; try { const sendStats = await statsDB .aggregate([ { $match: { corpId, type: "send", articleId: { $in: ids } } }, { $group: { _id: "$articleId", count: { $sum: 1 } } }, ]) .toArray(); const readStats = await statsDB .aggregate([ { $unwind: "$readTimes" }, { $match: { corpId, type: "read", articleId: { $in: ids } } }, { $group: { _id: "$articleId", count: { $sum: 1 } } }, ]) .toArray(); const send = sendStats.reduce((val, item) => { val[item._id] = item.count; return val; }, {}); const read = readStats.reduce((val, item) => { val[item._id] = item.count; return val; }, {}); return { read, send }; } catch (e) { return {}; } }; async function getArticleCount(context) { const { corpId, cateIds } = context; if (!corpId) return { success: false, msg: "缺少corpId参数" }; if (!Array.isArray(cateIds)) return { success: false, msg: "缺少cateIds参数" }; try { const total = await db .collection("article") .countDocuments({ corpId, cateId: { $in: cateIds } }); return { success: true, data: total }; } catch (e) { return { success: false, msg: e.message }; } }