增加签到功能

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

View File

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

View File

@@ -12,14 +12,16 @@ import (
"geekai/core"
"geekai/core/types"
"geekai/service"
"geekai/store"
"geekai/store/model"
"geekai/store/vo"
"geekai/utils"
"geekai/utils/resp"
"github.com/imroc/req/v3"
"strings"
"time"
"github.com/imroc/req/v3"
"github.com/go-redis/redis/v8"
"github.com/golang-jwt/jwt/v5"
@@ -32,6 +34,7 @@ type UserHandler struct {
BaseHandler
searcher *xdb.Searcher
redis *redis.Client
levelDB *store.LevelDB
licenseService *service.LicenseService
captcha *service.CaptchaService
userService *service.UserService
@@ -42,6 +45,7 @@ func NewUserHandler(
db *gorm.DB,
searcher *xdb.Searcher,
client *redis.Client,
levelDB *store.LevelDB,
captcha *service.CaptchaService,
userService *service.UserService,
licenseService *service.LicenseService) *UserHandler {
@@ -49,6 +53,7 @@ func NewUserHandler(
BaseHandler: BaseHandler{DB: db, App: app},
searcher: searcher,
redis: client,
levelDB: levelDB,
captcha: captcha,
licenseService: licenseService,
userService: userService,
@@ -712,3 +717,30 @@ func (h *UserHandler) BindEmail(c *gin.Context) {
_ = h.redis.Del(c, key) // 删除短信验证码
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.GET("clogin", h.CLogin)
group.GET("clogin/callback", h.CLoginCallback)
group.GET("signin", h.SignIn)
}),
fx.Invoke(func(s *core.AppServer, h *handler.ChatHandler) {
group := s.Engine.Group("/api/chat/")

View File

@@ -114,7 +114,7 @@ console.log(
"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>
<style lang="stylus">

View File

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

View File

@@ -25,6 +25,9 @@
<el-form-item label="剩余算力">
<el-text type="warning">{{ user["power"] }}</el-text>
<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 label="会员到期时间" v-if="user['expired_time'] > 0">
<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 Compressor from "compressorjs";
import { dateFormat } from "@/utils/libs";
import { checkSession } from "@/store/cache";
import { checkSession, getSystemInfo } from "@/store/cache";
import { useRouter } from "vue-router";
import { showMessageError, showMessageOK } from "@/utils/dialog";
const user = ref({
vip: false,
username: "演示数据",
@@ -56,6 +60,7 @@ const user = ref({
});
const vipImg = ref("/images/menu/member.png");
const systemConfig = ref({});
const router = useRouter();
const emits = defineEmits(["hide"]);
onMounted(() => {
@@ -73,6 +78,10 @@ onMounted(() => {
.catch((e) => {
console.log(e);
});
getSystemInfo().then((res) => {
systemConfig.value = res.data;
});
});
const afterRead = (file) => {
@@ -112,6 +121,17 @@ const gotoLog = () => {
router.push("/powerLog");
emits("hide", false);
};
const signIn = () => {
httpGet("/api/user/signin")
.then(() => {
showMessageOK("签到成功");
user.value.power += systemConfig.value.daily_power;
})
.catch((e) => {
showMessageError(e.message);
});
};
</script>
<style lang="stylus" scoped>

View File

@@ -12,22 +12,22 @@ import {showConfirmDialog} from "vant";
// generate a random string
export function randString(length) {
const str = "0123456789abcdefghijklmnopqrstuvwxyz"
const size = str.length
let buf = []
const str = "0123456789abcdefghijklmnopqrstuvwxyz";
const size = str.length;
let buf = [];
for (let i = 0; i < length; i++) {
const rand = Math.random() * size
buf.push(str.charAt(rand))
const rand = Math.random() * size;
buf.push(str.charAt(rand));
}
return buf.join("")
return buf.join("");
}
export function UUID() {
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;
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) {
if (!timestamp) {
return '';
return "";
} else if (timestamp < 9680917502) {
timestamp = timestamp * 1000;
}
@@ -55,36 +55,36 @@ export function dateFormat(timestamp, format) {
mm = time.getMinutes(); // 分
ss = time.getSeconds(); // 秒
month = month < 10 ? '0' + month : month;
day = day < 10 ? '0' + day : day;
HH = HH < 10 ? '0' + HH : HH; // 时
mm = mm < 10 ? '0' + mm : mm; // 分
ss = ss < 10 ? '0' + ss : ss; // 秒
month = month < 10 ? "0" + month : month;
day = day < 10 ? "0" + day : day;
HH = HH < 10 ? "0" + HH : HH; // 时
mm = mm < 10 ? "0" + mm : mm; // 分
ss = ss < 10 ? "0" + ss : ss; // 秒
switch (format) {
case 'yyyy':
case "yyyy":
timeDate = String(year);
break;
case 'yyyy-MM':
timeDate = year + '-' + month;
case "yyyy-MM":
timeDate = year + "-" + month;
break;
case 'yyyy-MM-dd':
timeDate = year + '-' + month + '-' + day;
case "yyyy-MM-dd":
timeDate = year + "-" + month + "-" + day;
break;
case 'yyyy/MM/dd':
timeDate = year + '/' + month + '/' + day;
case "yyyy/MM/dd":
timeDate = year + "/" + month + "/" + day;
break;
case 'yyyy-MM-dd HH:mm:ss':
timeDate = year + '-' + month + '-' + day + ' ' + HH + ':' + mm + ':' + ss;
case "yyyy-MM-dd HH:mm:ss":
timeDate = year + "-" + month + "-" + day + " " + HH + ":" + mm + ":" + ss;
break;
case 'HH:mm:ss':
timeDate = HH + ':' + mm + ':' + ss;
case "HH:mm:ss":
timeDate = HH + ":" + mm + ":" + ss;
break;
case 'MM':
case "MM":
timeDate = String(month);
break;
default:
timeDate = year + '-' + month + '-' + day + ' ' + HH + ':' + mm + ':' + ss;
timeDate = year + "-" + month + "-" + day + " " + HH + ":" + mm + ":" + ss;
break;
}
return timeDate;
@@ -93,19 +93,19 @@ export function dateFormat(timestamp, format) {
export function formatTime(time) {
const minutes = 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) {
if (!array) {
return false
return false;
}
if (typeof compare !== 'function') {
if (typeof compare !== "function") {
compare = function (v1, v2) {
return v1 === v2;
}
};
}
for (let i = 0; i < array.length; i++) {
if (compare(array[i], value)) {
@@ -117,10 +117,10 @@ export function arrayContains(array, value, compare) {
// 删除数组中指定的元素
export function removeArrayItem(array, value, compare) {
if (typeof compare !== 'function') {
if (typeof compare !== "function") {
compare = function (v1, v2) {
return v1 === v2;
}
};
}
for (let i = 0; i < array.length; i++) {
if (compare(array[i], value)) {
@@ -134,7 +134,7 @@ export function removeArrayItem(array, value, compare) {
// 渲染输入的换行符
export function renderInputText(text) {
const replaceRegex = /(\n\r|\r\n|\r|\n)/g;
text = text || '';
text = text || "";
return text.replace(replaceRegex, "<br/>");
}
@@ -144,35 +144,35 @@ export function copyObj(origin) {
}
export function disabledDate(time) {
return time.getTime() < Date.now()
return time.getTime() < Date.now();
}
// 字符串截取
export function substr(str, length) {
let result = ''
let count = 0
let result = "";
let count = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charAt(i)
const char = str.charAt(i);
const charCode = str.charCodeAt(i);
// 判断字符是否为中文字符
if (charCode >= 0x4e00 && charCode <= 0x9fff) {
// 中文字符算两个字符
count += 2
count += 2;
} else {
count++
count++;
}
if (count <= length) {
result += char
result += char;
} else {
result += " ..."
break
result += " ...";
break;
}
}
return result
return result;
}
export function isImage(url) {
@@ -182,7 +182,7 @@ export function isImage(url) {
export function processContent(content) {
if (!content) {
return ""
return "";
}
// 如果是图片链接地址,则直接替换成图片标签
@@ -191,20 +191,33 @@ export function processContent(content) {
if (links) {
for (let link of links) {
if (isImage(link)) {
const index = content.indexOf(link)
const index = content.indexOf(link);
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) {
return prompt.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;");
return prompt.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
}
// 判断是否为微信浏览器
@@ -214,30 +227,29 @@ export function isWeChatBrowser() {
export function showLoginDialog(router) {
showConfirmDialog({
title: '登录',
message:
'此操作需要登录才能进行,前往登录?',
}).then(() => {
router.push("/mobile/login")
}).catch(() => {
title: "登录",
message: "此操作需要登录才能进行,前往登录?",
})
.then(() => {
router.push("/mobile/login");
})
.catch(() => {
// on cancel
});
}
export const replaceImg = (img) => {
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 localhost = "http://localhost:5678"
const devHost = process.env.VUE_APP_API_HOST;
const localhost = "http://localhost:5678";
if (img.includes(localhost)) {
return img?.replace(localhost, devHost)
}
return img
return img?.replace(localhost, devHost);
}
return img;
};
export function isChrome() {
const userAgent = navigator.userAgent.toLowerCase();
return /chrome/.test(userAgent) && !/edg/.test(userAgent);
}

View File

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

View File

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