509 lines
14 KiB
JavaScript
509 lines
14 KiB
JavaScript
|
|
/**
|
|||
|
|
* 文章统计相关接口
|
|||
|
|
*/
|
|||
|
|
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 };
|
|||
|
|
}
|
|||
|
|
}
|