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 };
|
||
}
|
||
}
|