Repository navigation
Expand file tree
/
Copy pathClickStats.cs
More file actions
196 lines (168 loc) · 7.68 KB
/
Copy pathClickStats.cs
File metadata and controls
196 lines (168 loc) · 7.68 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
using System.Text.Json;
namespace MouseClickCounter;
/// <summary>
/// 点击统计数据:总计数 + 按天计数。
/// 数据保存为 exe 同目录 stats.json,每 10 秒自动落盘 + 退出时落盘。
/// 落盘为原子替换(先写 .tmp 再 Move),断电/崩溃不会留下半个 JSON。
/// </summary>
public sealed class ClickStats
{
private readonly object _lock = new();
// 总计数,key = MouseButton 枚举名
public Dictionary<string, long> Total { get; set; } = new();
// 按天计数:date -> (buttonName -> count)
public Dictionary<string, Dictionary<string, long>> Daily { get; set; } = new();
// 键盘按键总计数:key = 按键名(如 A、Enter、Ctrl)
public Dictionary<string, long> Keyboard { get; set; } = new();
// 组合快捷键总计数:key = 组合名(如 Ctrl+C、Alt+Tab)
public Dictionary<string, long> Shortcuts { get; set; } = new();
// 键盘按键按天计数:date -> (keyName -> count)
public Dictionary<string, Dictionary<string, long>> KeyboardDaily { get; set; } = new();
// 组合快捷键按天计数:date -> (shortcutName -> count)
public Dictionary<string, Dictionary<string, long>> ShortcutDaily { get; set; } = new();
public static string DataFile => Path.Combine(AppContext.BaseDirectory, "stats.json");
/// <summary>落盘失败时触发(如目录只读);订阅方负责一次性提示用户</summary>
public static event Action? SaveFailed;
// MouseButton 枚举名缓存:避免高频事件里 Enum.ToString() 的反射与装箱分配
private static readonly string[] MouseButtonNames = Enum.GetNames<MouseButton>();
// 今日日期字符串缓存:高频事件里不再每次格式化,仅在跨天时重建
private static string? _cachedToday;
private static DateTime _cachedTodayDate;
/// <summary>今日日期字符串(yyyy-MM-dd);跨天时自动刷新,良性竞态至多重复计算一次</summary>
internal static string TodayString()
{
var now = DateTime.Now;
if (_cachedToday == null || now.Date != _cachedTodayDate)
{
_cachedTodayDate = now.Date;
_cachedToday = now.ToString("yyyy-MM-dd");
}
return _cachedToday;
}
/// <summary>昨日日期字符串(yyyy-MM-dd);仅 UI 刷新路径调用(高频钩子路径不用),无需缓存</summary>
internal static string YesterdayString() => DateTime.Now.AddDays(-1).ToString("yyyy-MM-dd");
/// <summary>记录一次点击(线程安全)</summary>
public void Record(MouseButton button)
{
string key = MouseButtonNames[(int)button];
string today = TodayString();
lock (_lock)
{
Total.TryGetValue(key, out long t);
Total[key] = t + 1;
IncrDaily(Daily, today, key);
}
}
/// <summary>按天计数 +1(须在 _lock 内调用)</summary>
private static void IncrDaily(Dictionary<string, Dictionary<string, long>> daily, string date, string key)
{
if (!daily.TryGetValue(date, out var day))
{
day = new Dictionary<string, long>();
daily[date] = day;
}
day.TryGetValue(key, out long v);
day[key] = v + 1;
}
/// <summary>记录一次键盘按键(线程安全,含按天计数);返回该键最新总计数(供热力图增量刷新)</summary>
public long RecordKeyboard(string key)
{
string today = TodayString();
lock (_lock)
{
Keyboard.TryGetValue(key, out long v);
Keyboard[key] = v + 1;
IncrDaily(KeyboardDaily, today, key);
return v + 1;
}
}
/// <summary>记录一次组合快捷键(线程安全,含按天计数)</summary>
public void RecordShortcut(string name)
{
string today = TodayString();
lock (_lock)
{
Shortcuts.TryGetValue(name, out long v);
Shortcuts[name] = v + 1;
IncrDaily(ShortcutDaily, today, name);
}
}
/// <summary>读取某按钮总次数</summary>
public long GetTotal(MouseButton button) => Total.TryGetValue(MouseButtonNames[(int)button], out long v) ? v : 0;
/// <summary>今日某按钮次数</summary>
public long GetToday(MouseButton button) => GetDayCount(Daily, TodayString(), MouseButtonNames[(int)button]);
/// <summary>昨日某按钮次数</summary>
public long GetYesterday(MouseButton button) => GetDayCount(Daily, YesterdayString(), MouseButtonNames[(int)button]);
/// <summary>今日某键盘按键次数</summary>
public long GetKeyboardToday(string key) => GetDayCount(KeyboardDaily, TodayString(), key);
/// <summary>昨日某键盘按键次数</summary>
public long GetKeyboardYesterday(string key) => GetDayCount(KeyboardDaily, YesterdayString(), key);
/// <summary>今日某组合快捷键次数</summary>
public long GetShortcutToday(string name) => GetDayCount(ShortcutDaily, TodayString(), name);
/// <summary>昨日某组合快捷键次数</summary>
public long GetShortcutYesterday(string name) => GetDayCount(ShortcutDaily, YesterdayString(), name);
private long GetDayCount(Dictionary<string, Dictionary<string, long>> daily, string date, string key)
{
lock (_lock)
{
return daily.TryGetValue(date, out var day) && day.TryGetValue(key, out long v) ? v : 0;
}
}
public void Save()
{
try
{
string json;
lock (_lock)
{
json = JsonSerializer.Serialize(this, new JsonSerializerOptions { WriteIndented = true });
}
// 原子替换:先写临时文件再 Move 覆盖。直接 WriteAllText 是"截断再写",
// 写到一半进程被杀/断电会留下半个 JSON,下次启动 Load 失败 → 历史数据清零。
string tmp = DataFile + ".tmp";
File.WriteAllText(tmp, json);
File.Move(tmp, DataFile, overwrite: true);
}
catch
{
// 写入失败不影响主程序;通知订阅方做一次性提示(如目录只读)
SaveFailed?.Invoke();
}
}
public static ClickStats Load()
{
try
{
if (File.Exists(DataFile))
{
var loaded = JsonSerializer.Deserialize<ClickStats>(File.ReadAllText(DataFile));
if (loaded != null)
{
loaded.Total ??= new Dictionary<string, long>();
loaded.Daily ??= new Dictionary<string, Dictionary<string, long>>();
loaded.Keyboard ??= new Dictionary<string, long>();
loaded.Shortcuts ??= new Dictionary<string, long>();
loaded.KeyboardDaily ??= new Dictionary<string, Dictionary<string, long>>();
loaded.ShortcutDaily ??= new Dictionary<string, Dictionary<string, long>>();
return loaded;
}
}
}
catch
{
// 数据损坏:先把坏文件改名备份(用户可人工找回),再从零开始。
// 静默清零会让用户数月统计无声蒸发,必须留痕。
try
{
string backup = DataFile + ".corrupt-" + DateTime.Now.ToString("yyyyMMdd-HHmmss");
File.Move(DataFile, backup);
Logger.Log($"stats.json 损坏,已备份为 {Path.GetFileName(backup)},统计从零开始");
}
catch
{
// 备份失败(如目录只读)不再打扰,仍从零开始
}
}
return new ClickStats();
}
}