2026-07-27 11:28:33 +08:00

509 lines
14 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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