feat: 前端卡片观看数/子节点数接线后端

- dnfs2 get_notes 为每个节点附上 child_count(一次 IN 批量查询统计子节点数,best-effort)
- dnfs.js 卡片眼睛=views 观看数、转发=child_count 子节点数(原 share_count/visit_count 后端无字段恒为 0)
This commit is contained in:
qing committed 2026-08-11 10:06:33 +00:00
1 parent 986acab1c5
commit c9777cbff8
2 files changed
+49 -3

No files matched your search

+45
View File
@@ -693,6 +693,45 @@ private:
} }
} }
// 统计各节点的子节点数(get_notes 用于前端"分享数"展示)。
// 用一次 IN 批量查询所有孙节点(仅取 pid 列,避免拉取 content),按 pid 分组计数,
// 避免对每个节点单独查询。best-effort:任何失败仅记 WARNING 并返回已有结果,
// 绝不影响 get_notes 主流程(前端缺省显示 0)。
std::unordered_map<int64_t, int64_t> _child_counts(const json::array& items) {
std::unordered_map<int64_t, int64_t> counts;
try {
json::array ids;
for (const auto& item : items) {
if (item.is_object() && item.as_object().contains("id")
&& item.as_object().at("id").is_int64()) {
ids.push_back(item.as_object().at("id").as_int64());
}
}
if (ids.empty()) return counts;
json::object payload;
payload["action"] = "select";
payload["table"] = "Notes";
payload["columns"] = json::array{"pid"};
payload["where"] = json::object{{"pid", json::object{{"in", ids}}}};
DbResult r = _call_db(payload);
if (!r.success || !r.data.is_array()) {
_log("WARNING", "dnfs2", "child_count 统计失败: " + r.error);
return counts;
}
for (const auto& row : r.data.as_array()) {
if (row.is_object() && row.as_object().contains("pid")
&& row.as_object().at("pid").is_int64()) {
counts[row.as_object().at("pid").as_int64()]++;
}
}
} catch (const std::exception& e) {
_log("WARNING", "dnfs2", std::string("child_count 统计异常: ") + e.what());
}
return counts;
}
// ---- Token 验签(通过 gateway /whoami)--------------------------------- // ---- Token 验签(通过 gateway /whoami)---------------------------------
struct UserInfo { struct UserInfo {
@@ -904,6 +943,8 @@ private:
if (r.data.is_array()) { if (r.data.is_array()) {
json::array result = r.data.as_array(); json::array result = r.data.as_array();
// 一次性统计各节点的子节点数(供前端"分享数"展示),避免逐节点查询
auto child_counts = _child_counts(result);
for (auto& item : result) { for (auto& item : result) {
if (item.is_object()) { if (item.is_object()) {
json::object& note = item.as_object(); json::object& note = item.as_object();
@@ -916,6 +957,10 @@ private:
} }
} }
note["can_operate"] = can_operate; note["can_operate"] = can_operate;
int64_t nid = (note.contains("id") && note.at("id").is_int64())
? note.at("id").as_int64() : 0;
auto cit = child_counts.find(nid);
note["child_count"] = (cit != child_counts.end()) ? cit->second : 0;
} }
} }
_log("INFO", "dnfs2", "get_notes 查询 pid=" + std::to_string(pid) _log("INFO", "dnfs2", "get_notes 查询 pid=" + std::to_string(pid)
+4 -3
View File
@@ -1011,12 +1011,13 @@ function renderCards(list) {
owner_name.textContent = owner_text; owner_name.textContent = owner_text;
wrt_div.appendChild(owner_avatar); wrt_div.appendChild(owner_avatar);
wrt_div.appendChild(owner_name); wrt_div.appendChild(owner_name);
// 互动统计:眼睛=观看、爱心=点赞、转发=分享(后端 Notes 表暂无计数字段,缺省显示 0) // 互动统计:眼睛=观看数(后端 views 列)、爱心=点赞、转发=子节点数(后端 child_count)。
// views / child_count 由 dnfs2 get_notes 返回;like_count 后端暂无字段,缺省显示 0。
let sts_div = document.createElement('div'); let sts_div = document.createElement('div');
sts_div.className = "card-stats"; sts_div.className = "card-stats";
sts_div.appendChild(makeStatItem(ICON_EYE, item.visit_count)); sts_div.appendChild(makeStatItem(ICON_EYE, item.views));
sts_div.appendChild(makeStatItem(ICON_HEART, item.like_count)); sts_div.appendChild(makeStatItem(ICON_HEART, item.like_count));
sts_div.appendChild(makeStatItem(ICON_SHARE, item.share_count)); sts_div.appendChild(makeStatItem(ICON_SHARE, item.child_count));
// 卡片操作按钮:编辑 / 移动 / 删除(stopPropagation 防止触发卡片自身的点入跳转) // 卡片操作按钮:编辑 / 移动 / 删除(stopPropagation 防止触发卡片自身的点入跳转)
// 按钮组整体按 can_operate 显隐:操作权由后端 get_notes 按 owner / is_admin 判定, // 按钮组整体按 can_operate 显隐:操作权由后端 get_notes 按 owner / is_admin 判定,
// 与 delete_note / update_note 鉴权口径一致(移动本质也是 update_note); // 与 delete_note / update_note 鉴权口径一致(移动本质也是 update_note);