增加签到功能

This commit is contained in:
RockYang
2025-02-07 18:02:11 +08:00
parent f8e32148c8
commit 8ced447a14
11 changed files with 246 additions and 170 deletions

View File

@@ -4,6 +4,10 @@
- 功能优化:优化聊天页面 Notice 组件样式,采用 Vuepress 文档样式 - 功能优化:优化聊天页面 Notice 组件样式,采用 Vuepress 文档样式
- Bug 修复:修复主题切换的组件显示异常问题 - Bug 修复:修复主题切换的组件显示异常问题
- 功能优化:支持 DeepSeek-R1 推理模型,优化推理样式输出
- 功能优化:优化 Suno 歌曲播放按钮样式,居中显示
- 功能优化:后台管理新增模型的时候,可以绑定所有的 API KEY而不只是能绑定 Chat 类型的 API KEY
- 功能新增:新增每日签到功能,每日签到可以获得算力奖励
## v4.1.9 ## v4.1.9

View File

@@ -95,6 +95,7 @@ const (
PowerInvite = PowerType(4) // 邀请奖励 PowerInvite = PowerType(4) // 邀请奖励
PowerRedeem = PowerType(5) // 众筹 PowerRedeem = PowerType(5) // 众筹
PowerGift = PowerType(6) // 系统赠送 PowerGift = PowerType(6) // 系统赠送
PowerSignIn = PowerType(7) // 每日签到
) )
func (t PowerType) String() string { func (t PowerType) String() string {
@@ -111,6 +112,8 @@ func (t PowerType) String() string {
return "赠送" return "赠送"
case PowerInvite: case PowerInvite:
return "邀请" return "邀请"
case PowerSignIn:
return "签到"
} }
return "其他" return "其他"
} }

View File

@@ -12,14 +12,16 @@ import (
"geekai/core" "geekai/core"
"geekai/core/types" "geekai/core/types"
"geekai/service" "geekai/service"
"geekai/store"
"geekai/store/model" "geekai/store/model"
"geekai/store/vo" "geekai/store/vo"
"geekai/utils" "geekai/utils"
"geekai/utils/resp" "geekai/utils/resp"
"github.com/imroc/req/v3"
"strings" "strings"
"time" "time"
"github.com/imroc/req/v3"
"github.com/go-redis/redis/v8" "github.com/go-redis/redis/v8"
"github.com/golang-jwt/jwt/v5" "github.com/golang-jwt/jwt/v5"
@@ -32,6 +34,7 @@ type UserHandler struct {
BaseHandler BaseHandler
searcher *xdb.Searcher searcher *xdb.Searcher
redis *redis.Client redis *redis.Client
levelDB *store.LevelDB
licenseService *service.LicenseService licenseService *service.LicenseService
captcha *service.CaptchaService captcha *service.CaptchaService
userService *service.UserService userService *service.UserService
@@ -42,6 +45,7 @@ func NewUserHandler(
db *gorm.DB, db *gorm.DB,
searcher *xdb.Searcher, searcher *xdb.Searcher,
client *redis.Client, client *redis.Client,
levelDB *store.LevelDB,
captcha *service.CaptchaService, captcha *service.CaptchaService,
userService *service.UserService, userService *service.UserService,
licenseService *service.LicenseService) *UserHandler { licenseService *service.LicenseService) *UserHandler {
@@ -49,6 +53,7 @@ func NewUserHandler(
BaseHandler: BaseHandler{DB: db, App: app}, BaseHandler: BaseHandler{DB: db, App: app},
searcher: searcher, searcher: searcher,
redis: client, redis: client,
levelDB: levelDB,
captcha: captcha, captcha: captcha,
licenseService: licenseService, licenseService: licenseService,
userService: userService, userService: userService,
@@ -712,3 +717,30 @@ func (h *UserHandler) BindEmail(c *gin.Context) {
_ = h.redis.Del(c, key) // 删除短信验证码 _ = h.redis.Del(c, key) // 删除短信验证码
resp.SUCCESS(c) resp.SUCCESS(c)
} }
// SignIn 每日签到
func (h *UserHandler) SignIn(c *gin.Context) {
// 获取当前日期
date := time.Now().Format("2006-01-02")
// 检查是否已经签到
userId := h.GetLoginUserId(c)
key := fmt.Sprintf("signin/%d/%s", userId, date)
var signIn bool
err := h.levelDB.Get(key, &signIn)
if err == nil && signIn {
resp.ERROR(c, "今日已签到,请明日再来!")
return
}
// 签到
h.levelDB.Put(key, true)
if h.App.SysConfig.DailyPower > 0 {
h.userService.IncreasePower(int(userId), h.App.SysConfig.DailyPower, model.PowerLog{
Type: types.PowerSignIn,
Model: "SignIn",
Remark: fmt.Sprintf("每日签到奖励,金额:%d", h.App.SysConfig.DailyPower),
})
}
resp.SUCCESS(c)
}

View File

@@ -244,6 +244,7 @@ func main() {
group.POST("resetPass", h.ResetPass) group.POST("resetPass", h.ResetPass)
group.GET("clogin", h.CLogin) group.GET("clogin", h.CLogin)
group.GET("clogin/callback", h.CLoginCallback) group.GET("clogin/callback", h.CLoginCallback)
group.GET("signin", h.SignIn)
}), }),
fx.Invoke(func(s *core.AppServer, h *handler.ChatHandler) { fx.Invoke(func(s *core.AppServer, h *handler.ChatHandler) {
group := s.Engine.Group("/api/chat/") group := s.Engine.Group("/api/chat/")

View File

@@ -114,7 +114,7 @@ console.log(
"color: red;font-size: 20px;font-family: '微软雅黑';" "color: red;font-size: 20px;font-family: '微软雅黑';"
); );
console.log("%c 一曲肝肠断,天涯何处觅知音?愿你出走半生,归来仍是少年!", "color: #7c39ed;font-size: 18px;font-family: '微软雅黑';"); console.log("%c 愿你出走半生,归来仍是少年!大奉武夫许七安,前来凿阵", "color: #7c39ed;font-size: 18px;font-family: '微软雅黑';");
</script> </script>
<style lang="stylus"> <style lang="stylus">

View File

@@ -243,6 +243,9 @@
opacity 0 opacity 0
transform: translate(-50%, 0px); transform: translate(-50%, 0px);
transition opacity 0.3s ease 0s transition opacity 0.3s ease 0s
display flex
justify-content center
align-items center
} }
&:hover { &:hover {

View File

@@ -90,9 +90,9 @@
</template> </template>
<script setup> <script setup>
import {Clock, DocumentCopy, Refresh} from "@element-plus/icons-vue"; import { Clock, DocumentCopy, Refresh } from "@element-plus/icons-vue";
import {ElMessage} from "element-plus"; import { ElMessage } from "element-plus";
import {dateFormat, processContent} from "@/utils/libs"; import { dateFormat, processContent } from "@/utils/libs";
import hl from "highlight.js"; import hl from "highlight.js";
import emoji from "markdown-it-emoji"; import emoji from "markdown-it-emoji";
import mathjaxPlugin from "markdown-it-mathjax3"; import mathjaxPlugin from "markdown-it-mathjax3";

View File

@@ -25,6 +25,9 @@
<el-form-item label="剩余算力"> <el-form-item label="剩余算力">
<el-text type="warning">{{ user["power"] }}</el-text> <el-text type="warning">{{ user["power"] }}</el-text>
<el-tag type="info" size="small" class="ml-2 cursor-pointer" @click="gotoLog">算力日志</el-tag> <el-tag type="info" size="small" class="ml-2 cursor-pointer" @click="gotoLog">算力日志</el-tag>
<el-tooltip :content="`每日签到可获得 ${systemConfig.daily_power} 算力`" placement="top" v-if="systemConfig.daily_power > 0">
<el-button type="primary" size="small" @click="signIn" class="ml-2">签到</el-button>
</el-tooltip>
</el-form-item> </el-form-item>
<el-form-item label="会员到期时间" v-if="user['expired_time'] > 0"> <el-form-item label="会员到期时间" v-if="user['expired_time'] > 0">
<el-tag type="danger">{{ dateFormat(user["expired_time"]) }}</el-tag> <el-tag type="danger">{{ dateFormat(user["expired_time"]) }}</el-tag>
@@ -44,8 +47,9 @@ import { ElMessage } from "element-plus";
import { Plus } from "@element-plus/icons-vue"; import { Plus } from "@element-plus/icons-vue";
import Compressor from "compressorjs"; import Compressor from "compressorjs";
import { dateFormat } from "@/utils/libs"; import { dateFormat } from "@/utils/libs";
import { checkSession } from "@/store/cache"; import { checkSession, getSystemInfo } from "@/store/cache";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { showMessageError, showMessageOK } from "@/utils/dialog";
const user = ref({ const user = ref({
vip: false, vip: false,
username: "演示数据", username: "演示数据",
@@ -56,6 +60,7 @@ const user = ref({
}); });
const vipImg = ref("/images/menu/member.png"); const vipImg = ref("/images/menu/member.png");
const systemConfig = ref({});
const router = useRouter(); const router = useRouter();
const emits = defineEmits(["hide"]); const emits = defineEmits(["hide"]);
onMounted(() => { onMounted(() => {
@@ -73,6 +78,10 @@ onMounted(() => {
.catch((e) => { .catch((e) => {
console.log(e); console.log(e);
}); });
getSystemInfo().then((res) => {
systemConfig.value = res.data;
});
}); });
const afterRead = (file) => { const afterRead = (file) => {
@@ -112,6 +121,17 @@ const gotoLog = () => {
router.push("/powerLog"); router.push("/powerLog");
emits("hide", false); emits("hide", false);
}; };
const signIn = () => {
httpGet("/api/user/signin")
.then(() => {
showMessageOK("签到成功");
user.value.power += systemConfig.value.daily_power;
})
.catch((e) => {
showMessageError(e.message);
});
};
</script> </script>
<style lang="stylus" scoped> <style lang="stylus" scoped>

View File

@@ -8,26 +8,26 @@
/** /**
* Util lib functions * Util lib functions
*/ */
import {showConfirmDialog} from "vant"; import { showConfirmDialog } from "vant";
// generate a random string // generate a random string
export function randString(length) { export function randString(length) {
const str = "0123456789abcdefghijklmnopqrstuvwxyz" const str = "0123456789abcdefghijklmnopqrstuvwxyz";
const size = str.length const size = str.length;
let buf = [] let buf = [];
for (let i = 0; i < length; i++) { for (let i = 0; i < length; i++) {
const rand = Math.random() * size const rand = Math.random() * size;
buf.push(str.charAt(rand)) buf.push(str.charAt(rand));
} }
return buf.join("") return buf.join("");
} }
export function UUID() { export function UUID() {
let d = new Date().getTime(); let d = new Date().getTime();
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
const r = (d + Math.random() * 16) % 16 | 0; const r = (d + Math.random() * 16) % 16 | 0;
d = Math.floor(d / 16); d = Math.floor(d / 16);
return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16); return (c === "x" ? r : (r & 0x3) | 0x8).toString(16);
}); });
} }
@@ -41,7 +41,7 @@ export function isMobile() {
// 格式化日期 // 格式化日期
export function dateFormat(timestamp, format) { export function dateFormat(timestamp, format) {
if (!timestamp) { if (!timestamp) {
return ''; return "";
} else if (timestamp < 9680917502) { } else if (timestamp < 9680917502) {
timestamp = timestamp * 1000; timestamp = timestamp * 1000;
} }
@@ -55,36 +55,36 @@ export function dateFormat(timestamp, format) {
mm = time.getMinutes(); // 分 mm = time.getMinutes(); // 分
ss = time.getSeconds(); // 秒 ss = time.getSeconds(); // 秒
month = month < 10 ? '0' + month : month; month = month < 10 ? "0" + month : month;
day = day < 10 ? '0' + day : day; day = day < 10 ? "0" + day : day;
HH = HH < 10 ? '0' + HH : HH; // 时 HH = HH < 10 ? "0" + HH : HH; // 时
mm = mm < 10 ? '0' + mm : mm; // 分 mm = mm < 10 ? "0" + mm : mm; // 分
ss = ss < 10 ? '0' + ss : ss; // 秒 ss = ss < 10 ? "0" + ss : ss; // 秒
switch (format) { switch (format) {
case 'yyyy': case "yyyy":
timeDate = String(year); timeDate = String(year);
break; break;
case 'yyyy-MM': case "yyyy-MM":
timeDate = year + '-' + month; timeDate = year + "-" + month;
break; break;
case 'yyyy-MM-dd': case "yyyy-MM-dd":
timeDate = year + '-' + month + '-' + day; timeDate = year + "-" + month + "-" + day;
break; break;
case 'yyyy/MM/dd': case "yyyy/MM/dd":
timeDate = year + '/' + month + '/' + day; timeDate = year + "/" + month + "/" + day;
break; break;
case 'yyyy-MM-dd HH:mm:ss': case "yyyy-MM-dd HH:mm:ss":
timeDate = year + '-' + month + '-' + day + ' ' + HH + ':' + mm + ':' + ss; timeDate = year + "-" + month + "-" + day + " " + HH + ":" + mm + ":" + ss;
break; break;
case 'HH:mm:ss': case "HH:mm:ss":
timeDate = HH + ':' + mm + ':' + ss; timeDate = HH + ":" + mm + ":" + ss;
break; break;
case 'MM': case "MM":
timeDate = String(month); timeDate = String(month);
break; break;
default: default:
timeDate = year + '-' + month + '-' + day + ' ' + HH + ':' + mm + ':' + ss; timeDate = year + "-" + month + "-" + day + " " + HH + ":" + mm + ":" + ss;
break; break;
} }
return timeDate; return timeDate;
@@ -93,19 +93,19 @@ export function dateFormat(timestamp, format) {
export function formatTime(time) { export function formatTime(time) {
const minutes = Math.floor(time / 60); const minutes = Math.floor(time / 60);
const seconds = Math.floor(time % 60); const seconds = Math.floor(time % 60);
return `${minutes}:${seconds < 10 ? '0' : ''}${seconds}`; return `${minutes}:${seconds < 10 ? "0" : ""}${seconds}`;
} }
// 判断数组中是否包含某个元素 // 判断数组中是否包含某个元素
export function arrayContains(array, value, compare) { export function arrayContains(array, value, compare) {
if (!array) { if (!array) {
return false return false;
} }
if (typeof compare !== 'function') { if (typeof compare !== "function") {
compare = function (v1, v2) { compare = function (v1, v2) {
return v1 === v2; return v1 === v2;
} };
} }
for (let i = 0; i < array.length; i++) { for (let i = 0; i < array.length; i++) {
if (compare(array[i], value)) { if (compare(array[i], value)) {
@@ -117,10 +117,10 @@ export function arrayContains(array, value, compare) {
// 删除数组中指定的元素 // 删除数组中指定的元素
export function removeArrayItem(array, value, compare) { export function removeArrayItem(array, value, compare) {
if (typeof compare !== 'function') { if (typeof compare !== "function") {
compare = function (v1, v2) { compare = function (v1, v2) {
return v1 === v2; return v1 === v2;
} };
} }
for (let i = 0; i < array.length; i++) { for (let i = 0; i < array.length; i++) {
if (compare(array[i], value)) { if (compare(array[i], value)) {
@@ -134,7 +134,7 @@ export function removeArrayItem(array, value, compare) {
// 渲染输入的换行符 // 渲染输入的换行符
export function renderInputText(text) { export function renderInputText(text) {
const replaceRegex = /(\n\r|\r\n|\r|\n)/g; const replaceRegex = /(\n\r|\r\n|\r|\n)/g;
text = text || ''; text = text || "";
return text.replace(replaceRegex, "<br/>"); return text.replace(replaceRegex, "<br/>");
} }
@@ -144,35 +144,35 @@ export function copyObj(origin) {
} }
export function disabledDate(time) { export function disabledDate(time) {
return time.getTime() < Date.now() return time.getTime() < Date.now();
} }
// 字符串截取 // 字符串截取
export function substr(str, length) { export function substr(str, length) {
let result = '' let result = "";
let count = 0 let count = 0;
for (let i = 0; i < str.length; i++) { for (let i = 0; i < str.length; i++) {
const char = str.charAt(i) const char = str.charAt(i);
const charCode = str.charCodeAt(i); const charCode = str.charCodeAt(i);
// 判断字符是否为中文字符 // 判断字符是否为中文字符
if (charCode >= 0x4e00 && charCode <= 0x9fff) { if (charCode >= 0x4e00 && charCode <= 0x9fff) {
// 中文字符算两个字符 // 中文字符算两个字符
count += 2 count += 2;
} else { } else {
count++ count++;
} }
if (count <= length) { if (count <= length) {
result += char result += char;
} else { } else {
result += " ..." result += " ...";
break break;
} }
} }
return result return result;
} }
export function isImage(url) { export function isImage(url) {
@@ -182,7 +182,7 @@ export function isImage(url) {
export function processContent(content) { export function processContent(content) {
if (!content) { if (!content) {
return "" return "";
} }
// 如果是图片链接地址,则直接替换成图片标签 // 如果是图片链接地址,则直接替换成图片标签
@@ -191,53 +191,65 @@ export function processContent(content) {
if (links) { if (links) {
for (let link of links) { for (let link of links) {
if (isImage(link)) { if (isImage(link)) {
const index = content.indexOf(link) const index = content.indexOf(link);
if (content.substring(index - 1, 2) !== "]") { if (content.substring(index - 1, 2) !== "]") {
content = content.replace(link, "\n![](" + link + ")\n") content = content.replace(link, "\n![](" + link + ")\n");
} }
} }
} }
} }
return content // 处理推理标签
if (content.includes("<think>")) {
content = content.replace(/<think>(.*?)<\/think>/gs, (match, content) => {
if (content.length > 10) {
return `<blockquote>${content}</blockquote>`;
}
return "";
});
content = content.replace(/<think>(.*?)$/gs, (match, content) => {
if (content.length > 10) {
return `<blockquote>${content}</blockquote>`;
}
return "";
});
}
return content;
} }
export function processPrompt(prompt) { export function processPrompt(prompt) {
return prompt.replace(/&/g, "&amp;") return prompt.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;");
} }
// 判断是否为微信浏览器 // 判断是否为微信浏览器
export function isWeChatBrowser() { export function isWeChatBrowser() {
return /MicroMessenger/i.test( navigator.userAgent); return /MicroMessenger/i.test(navigator.userAgent);
} }
export function showLoginDialog(router) { export function showLoginDialog(router) {
showConfirmDialog({ showConfirmDialog({
title: '登录', title: "登录",
message: message: "此操作需要登录才能进行,前往登录?",
'此操作需要登录才能进行,前往登录?', })
}).then(() => { .then(() => {
router.push("/mobile/login") router.push("/mobile/login");
}).catch(() => { })
.catch(() => {
// on cancel // on cancel
}); });
} }
export const replaceImg =(img) => { export const replaceImg = (img) => {
if (!img.startsWith("http")) { if (!img.startsWith("http")) {
img = `${location.protocol}//${location.host}/${img}` img = `${location.protocol}//${location.host}/${img}`;
} }
const devHost = process.env.VUE_APP_API_HOST const devHost = process.env.VUE_APP_API_HOST;
const localhost = "http://localhost:5678" const localhost = "http://localhost:5678";
if (img.includes(localhost)) { if (img.includes(localhost)) {
return img?.replace(localhost, devHost) return img?.replace(localhost, devHost);
} }
return img return img;
} };
export function isChrome() { export function isChrome() {
const userAgent = navigator.userAgent.toLowerCase(); const userAgent = navigator.userAgent.toLowerCase();
return /chrome/.test(userAgent) && !/edg/.test(userAgent); return /chrome/.test(userAgent) && !/edg/.test(userAgent);
} }

View File

@@ -141,6 +141,7 @@ const types = ref([
{ label: "Suno文生歌", value: "suno" }, { label: "Suno文生歌", value: "suno" },
{ label: "Luma视频", value: "luma" }, { label: "Luma视频", value: "luma" },
{ label: "Realtime API", value: "realtime" }, { label: "Realtime API", value: "realtime" },
{ label: "其他", value: "other" },
]); ]);
const isEdit = ref(false); const isEdit = ref(false);
const clipboard = ref(null); const clipboard = ref(null);

View File

@@ -195,7 +195,7 @@ const type = ref([
// 获取 API KEY // 获取 API KEY
const apiKeys = ref([]); const apiKeys = ref([]);
httpGet("/api/admin/apikey/list?type=chat|dalle") httpGet("/api/admin/apikey/list")
.then((res) => { .then((res) => {
apiKeys.value = res.data; apiKeys.value = res.data;
}) })