仰望星辰工作室

clearlove2.1

clearlove2.1/ plugins/signin/main.js 6.0 KB · 137 行 原始文件
1// 每日签到 —— 应用型插件(kind=app)示例
2//
3// 演示能力:
4// 1. 自定义数据表 clv.table
5// 2. 前台路由与页面 clv.route + clv.view.template
6// 3. 后台页面与菜单 clv.adminPage + clv.adminMenu
7// 4. UI 注入 clv.slot
8// 5. 定时任务 clv.job
9// 6. 事件广播 clv.emit
10//
11// 注意:本插件没有 hooks 字段 —— 全部能力通过 setup() 中的运行时注册获得。
12//
13// 应用型插件可用的其它能力(本示例未使用,供参考):
14// clv.on("post.created", fn) 订阅站点事件(发帖/评论/点赞/注册/登录…)
15// clv.filter("filter.content", 10, fn) 链式改写内容(另见 filter.card / filter.api.response
16// / filter.post.visible / filter.theme.css /
17// filter.auth.login / filter.compose.guard /
18// filter.admin.stats / filter.settings.save)
19// clv.middleware("http.after", fn) 请求结束后观察状态码与耗时(http.before 可改写或短路)
20// clv.http.request({...}) 出网请求(需 net 权限,内网地址被拦截)
21// clv.mail.send(to, subject, body) 发邮件(需 mail 权限)
22// clv.media.saveImage/saveVideo({name,data}) 保存 base64 媒体(需 media.write)
23// clv.crypto.* clv.cache.* clv.post.* clv.comment.* clv.user.* clv.stats.*
24// 插件目录下放 templates/<内核模板名>.html 可覆盖站点模板片段
25
26var T = clv.db.table("log"); // 实际表名 pl_signin_log
27
28function setup() {
29 // 1) 建表(幂等,装/启用时都会执行)
30 clv.table("log", {
31 user_id: "int",
32 day: "string",
33 points: "int",
34 created_at: "time"
35 }, { unique: [["user_id", "day"]], indexes: [["day"]] });
36
37 // 2) 前台路由(实际路径:/x/signin/、/x/signin/do、/x/signin/rank)
38 clv.route("GET", "/", "pageSignin", { auth: "user" });
39 clv.route("POST", "/do", "doSignin", { auth: "user" });
40 clv.route("GET", "/rank", "apiRank", { auth: "none", json: true });
41
42 // 3) 后台页面与菜单(实际路径:/admin/plugins/signin/)
43 clv.adminPage("/", "adminPage", { perm: "plugins" });
44 clv.adminMenu({ label: "签到管理", path: "/", perm: "plugins", order: 50 });
45
46 // 4) UI 注入:详情页显示作者签到天数 + 前台悬浮签到按钮
47 clv.slot("post.detail.after", "detailBadge");
48 clv.slot("body.end", "floatingEntry");
49
50 // 5) 定时任务:每天清理 90 天前的记录
51 clv.job("daily-clean", "24h", "onDaily");
52}
53
54function today() {
55 var d = new Date();
56 return d.getFullYear() + "-" +
57 ("0" + (d.getMonth() + 1)).slice(-2) + "-" +
58 ("0" + d.getDate()).slice(-2);
59}
60
61/* ---------------- 前台 ---------------- */
62
63function pageSignin(ctx) {
64 var day = today();
65 var done = clv.db.get("SELECT id FROM " + T + " WHERE user_id=? AND day=?", ctx.userId, day);
66 var total = clv.db.get("SELECT IFNULL(SUM(points),0) AS points, COUNT(1) AS days FROM " + T + " WHERE user_id=?", ctx.userId);
67 return {
68 template: "page.html",
69 data: {
70 done: !!done,
71 points: total ? total.points : 0,
72 days: total ? total.days : 0,
73 gain: clv.plugin.config("points"),
74 csrf: ctx.csrf
75 }
76 };
77}
78
79function doSignin(ctx) {
80 var day = today();
81 if (clv.db.get("SELECT id FROM " + T + " WHERE user_id=? AND day=?", ctx.userId, day)) {
82 return { status: 400, json: { ok: false, msg: "今天已经签到过了" } };
83 }
84 var gain = Number(clv.plugin.config("points")) || 1;
85 clv.db.exec("INSERT INTO " + T + "(user_id,day,points,created_at) VALUES(?,?,?,?)",
86 ctx.userId, day, gain, new Date().toISOString());
87 clv.emit("signin.done", { user_id: ctx.userId, day: day, points: gain });
88 clv.log.info("用户 " + ctx.userId + " 签到成功,积分 +" + gain);
89 return { json: { ok: true, msg: "签到成功,积分 +" + gain } };
90}
91
92function apiRank(ctx) {
93 var rows = clv.db.query(
94 "SELECT user_id, SUM(points) AS points, COUNT(1) AS days FROM " + T +
95 " GROUP BY user_id ORDER BY points DESC LIMIT 20");
96 return { json: { ok: true, rank: rows } };
97}
98
99/* ---------------- UI 注入 ---------------- */
100
101// 详情页:显示帖子作者的累计签到天数(服务端渲染,可直接查库)
102function detailBadge(data) {
103 var post = data && data.post;
104 if (!post || !post.user_id) return "";
105 var row = clv.db.get("SELECT COUNT(1) AS n FROM " + T + " WHERE user_id=?", post.user_id);
106 var n = row ? row.n : 0;
107 if (!n) return "";
108 return '<div class="card clv-signin-days" style="margin-top:12px;font-size:13px;opacity:.85">' +
109 clv.view.escape(post.nickname || "该用户") + " 已累计签到 " + n + " 天</div>";
110}
111
112// 前台悬浮入口
113function floatingEntry() {
114 return '<a class="clv-signin-fab" href="/x/signin/" title="每日签到">' +
115 '<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2">' +
116 '<path d="M12 3l8 4.5v9L12 21l-8-4.5v-9z"/><path d="M9 12l2 2 4-4"/></svg></a>' +
117 '<style>.clv-signin-fab{position:fixed;left:18px;bottom:22px;z-index:900;width:46px;height:46px;' +
118 'border-radius:50%;display:flex;align-items:center;justify-content:center;color:#fff;' +
119 'background:linear-gradient(135deg,var(--clv-accent,#ff7a9c),var(--clv-accent-2,#8a6cff));' +
120 'box-shadow:0 8px 22px rgba(0,0,0,.22)}</style>';
121}
122
123/* ---------------- 后台 ---------------- */
124
125function adminPage(ctx) {
126 var days = clv.db.query("SELECT day, COUNT(1) AS n FROM " + T + " GROUP BY day ORDER BY day DESC LIMIT 30");
127 var total = clv.db.get("SELECT COUNT(1) AS n, IFNULL(SUM(points),0) AS p FROM " + T);
128 return { template: "admin.html", data: { days: days, total: total } };
129}
130
131/* ---------------- 定时任务 ---------------- */
132
133function onDaily(ctx) {
134 var before = new Date(Date.now() - 90 * 86400000).toISOString();
135 var r = clv.db.exec("DELETE FROM " + T + " WHERE created_at < ?", before);
136 clv.log.info("清理 90 天前的签到记录:" + (r ? r.rowsAffected : 0) + " 条");
137}