- 统一口径定义:所有累计/总/人均/合计时长度量,一律只
计算 hours 行中 activity_id 存在于 activities 表中的记录
(排除 FK pragma 关闭时写入导致的幽灵 hours 行)
- server/users.ts:
· GET /(用户列表列的累计时长)
原 SELECT SUM(h.hours) FROM hours h → 增加 EXISTS 子查询
『AND EXISTS (SELECT 1 FROM activities a WHERE a.id=h.activity_id)』
· GET /:id 详情卡 total_hours_raw(给 enrichUser 用的原始数)
同样加 EXISTS 校验,然后仍用详情 activities 列表 reduce 覆
盖,保证卡片累计 = 列表合计 100% 一致,且与列表 SQL 同口径
- server/export.ts:
· export include_user_totals(导出报告的部员累计时长排行)
原 LEFT JOIN hours 直接 SUM → 改为 LEFT JOIN 预聚合子查
询 vh,vh 内先按 EXISTS(activities) 过滤,再 SUM/COUNT
· export include_activity_matrix(矩阵 _total、预览 KPI、
导出 Excel 的总时长列)
hoursRows 全表 SELECT → 增加 WHERE EXISTS(activities) 过
滤,matrix/remarkMatrix 只聚合有效活动行,再派生的用户级
_total 自然与 user_totals 同口径
- client/Export.tsx overviewCalc(导出报告顶部的累计总时长/
人均时长 KPI 卡片,之前只依赖 preview.hours.reduce):
· 优先级:
① 有 user_totals → 用后端同口径返回的 u.total_hours 聚合
(最靠谱)
② 否则有 activity_matrix → 用 matrix.user._total 聚合
(两者都基于同一 valid_hours 过滤)
③ Fallback:preview.hours.reduce(仅在只勾了明细单且没
勾排行/矩阵时才用)
· 计算完 totalHours 再 toFixed(1) 去浮点数误差,avgHours
同步 toFixed(1)
- client/user/Dashboard.tsx(部员侧义工号查询的个人累计页):
· 累计义工时长卡下半部新增『明细表合计』实时校验行:
data.activities.reduce 求和直接显示在卡片上,与大号累计
值左右对照,一眼确认有没有不一致
This commit is contained in:
@@ -262,11 +262,19 @@ export default function Export() {
|
||||
|
||||
const overviewCalc = (() => {
|
||||
if (!preview) return null;
|
||||
const totalHours = (preview.hours || []).reduce((s: number, h: any) => s + Number(h.hours || 0), 0);
|
||||
const userCount = (preview.users || []).length;
|
||||
const activityCount = (preview.activities || []).length;
|
||||
const hourCount = (preview.hours || []).length;
|
||||
const avgHours = userCount > 0 ? totalHours / userCount : 0;
|
||||
let totalHours: number;
|
||||
if (preview.user_totals && preview.user_totals.length > 0) {
|
||||
totalHours = (preview.user_totals).reduce((s: number, u: any) => s + Number(u.total_hours || 0), 0);
|
||||
} else if (preview.activity_matrix && Array.isArray(preview.activity_matrix.users)) {
|
||||
totalHours = (preview.activity_matrix.users).reduce((s: number, u: any) => s + Number(u._total || 0), 0);
|
||||
} else {
|
||||
totalHours = (preview.hours || []).reduce((s: number, h: any) => s + Number(h.hours || 0), 0);
|
||||
}
|
||||
totalHours = Number(totalHours.toFixed(1));
|
||||
const avgHours = userCount > 0 ? Number((totalHours / userCount).toFixed(1)) : 0;
|
||||
return { totalHours, userCount, activityCount, hourCount, avgHours };
|
||||
})();
|
||||
|
||||
|
||||
@@ -68,7 +68,19 @@ export default function UserDashboard() {
|
||||
<div className="mt-5 pt-5 border-t border-gray-200 grid grid-cols-2 gap-4">
|
||||
<div className="card p-4 bg-primary-50/50">
|
||||
<div className="muted mb-1">累计义工时长</div>
|
||||
<div className="text-2xl md:text-3xl font-semibold text-primary-600">{Number(data.total_hours || 0).toFixed(1)} <span className="text-base font-medium text-gray-600">小时</span></div>
|
||||
<div className="text-2xl md:text-3xl font-semibold text-primary-600">
|
||||
{Number(data.total_hours || 0).toFixed(1)} <span className="text-base font-medium text-gray-600">小时</span>
|
||||
</div>
|
||||
{data.activities.length > 0 && (
|
||||
<div className="mt-3 pt-3 border-t border-primary-200/60 flex items-center justify-between">
|
||||
<span className="text-[11px] text-gray-500">
|
||||
明细表合计:
|
||||
</span>
|
||||
<span className="text-sm font-bold text-primary-600">
|
||||
{data.activities.reduce((s, r) => s + Number((r as any).hours || 0), 0).toFixed(1)} 小时
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="card p-4">
|
||||
<div className="muted mb-1">参与活动数</div>
|
||||
|
||||
@@ -66,11 +66,17 @@ router.post('/', authMiddleware, (req: AuthRequest, res) => {
|
||||
FROM user_groups ug2
|
||||
LEFT JOIN work_groups g2 ON ug2.group_id = g2.id
|
||||
WHERE ug2.user_id = u.id) AS _group_names_csv,
|
||||
COALESCE(SUM(h.hours), 0) AS total_hours,
|
||||
COUNT(DISTINCT h.activity_id) AS activity_count
|
||||
COALESCE(vh.total_hours, 0) AS total_hours,
|
||||
COALESCE(vh.activity_count, 0) AS activity_count
|
||||
FROM users u
|
||||
LEFT JOIN hours h ON h.user_id = u.id
|
||||
GROUP BY u.id
|
||||
LEFT JOIN (
|
||||
SELECT user_id,
|
||||
SUM(hours) AS total_hours,
|
||||
COUNT(DISTINCT activity_id) AS activity_count
|
||||
FROM hours h
|
||||
WHERE EXISTS (SELECT 1 FROM activities a WHERE a.id = h.activity_id)
|
||||
GROUP BY user_id
|
||||
) vh ON vh.user_id = u.id
|
||||
ORDER BY total_hours DESC, u.class, u.name
|
||||
`).all();
|
||||
result.user_totals = withGroupNames(rows).map(({ _group_names_csv: _, ...rest }) => rest);
|
||||
@@ -112,7 +118,8 @@ router.post('/', authMiddleware, (req: AuthRequest, res) => {
|
||||
`).all() as any[];
|
||||
const users = withGroupNames(userRows);
|
||||
const hoursRows = db.prepareCompat(`
|
||||
SELECT user_id, activity_id, hours, remark FROM hours
|
||||
SELECT user_id, activity_id, hours, remark FROM hours h
|
||||
WHERE EXISTS (SELECT 1 FROM activities a WHERE a.id = h.activity_id)
|
||||
`).all() as any[];
|
||||
const matrix: Record<string, number> = {};
|
||||
const remarkMatrix: Record<string, string> = {};
|
||||
|
||||
@@ -31,7 +31,11 @@ router.get('/', authMiddleware, (req: AuthRequest, res: Response) => {
|
||||
SELECT u.id, u.class, u.name, u.volunteer_no,
|
||||
GROUP_CONCAT(g.id) AS group_ids_csv,
|
||||
GROUP_CONCAT(g.name) AS group_names_csv,
|
||||
COALESCE((SELECT SUM(h.hours) FROM hours h WHERE h.user_id = u.id), 0) AS total_hours
|
||||
COALESCE((
|
||||
SELECT SUM(h.hours) FROM hours h
|
||||
WHERE h.user_id = u.id
|
||||
AND EXISTS (SELECT 1 FROM activities a WHERE a.id = h.activity_id)
|
||||
), 0) AS total_hours
|
||||
FROM users u
|
||||
LEFT JOIN user_groups ug ON u.id = ug.user_id
|
||||
LEFT JOIN work_groups g ON ug.group_id = g.id
|
||||
@@ -66,7 +70,11 @@ router.get('/:id', (req: Request, res: Response) => {
|
||||
SELECT u.id, u.class, u.name, u.volunteer_no,
|
||||
GROUP_CONCAT(g.id) AS group_ids_csv,
|
||||
GROUP_CONCAT(g.name) AS group_names_csv,
|
||||
COALESCE((SELECT SUM(h.hours) FROM hours h WHERE h.user_id = u.id), 0) AS total_hours
|
||||
COALESCE((
|
||||
SELECT SUM(h.hours) FROM hours h
|
||||
WHERE h.user_id = u.id
|
||||
AND EXISTS (SELECT 1 FROM activities a WHERE a.id = h.activity_id)
|
||||
), 0) AS total_hours_raw
|
||||
FROM users u
|
||||
LEFT JOIN user_groups ug ON u.id = ug.user_id
|
||||
LEFT JOIN work_groups g ON ug.group_id = g.id
|
||||
|
||||
Reference in New Issue
Block a user