509 lines
14 KiB
JavaScript
Raw Permalink Normal View History

2026-07-27 11:28:33 +08:00
/**
* 文章统计相关接口
*/
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 };
}
}