refactor: 重构项目,为所有的 AI 工具都引入算力,采用算力统一结算各个工具的调用次数和权限

This commit is contained in:
RockYang 2024-03-12 15:40:44 +08:00
parent 316636f83c
commit e746aafa2f
45 changed files with 438 additions and 298 deletions

View File

@ -57,7 +57,7 @@ type ChatModel struct {
Id uint `json:"id"` Id uint `json:"id"`
Platform Platform `json:"platform"` Platform Platform `json:"platform"`
Value string `json:"value"` Value string `json:"value"`
Weight int `json:"weight"` Power int `json:"power"`
} }
type ApiError struct { type ApiError struct {
@ -92,3 +92,21 @@ func GetModelMaxToken(model string) int {
} }
return 4096 return 4096
} }
// PowerType 算力日志类型
type PowerType int
const (
PowerRecharge = PowerType(1) // 充值
PowerConsume = PowerType(2) // 消费
PowerRefund = PowerType(3) // 任务SD,MJ执行失败退款
PowerInvite = PowerType(4) // 邀请奖励
PowerReward = PowerType(5) // 众筹
)
type PowerMark int
const (
PowerSub = PowerMark(0)
PowerAdd = PowerMark(1)
)

View File

@ -157,8 +157,7 @@ type UserChatConfig struct {
} }
type InviteReward struct { type InviteReward struct {
ChatCalls int `json:"chat_calls"` Power int `json:"power"`
ImgCalls int `json:"img_calls"`
} }
type ModelAPIConfig struct { type ModelAPIConfig struct {
@ -167,26 +166,26 @@ type ModelAPIConfig struct {
} }
type SystemConfig struct { type SystemConfig struct {
Title string `json:"title"` Title string `json:"title"`
AdminTitle string `json:"admin_title"` AdminTitle string `json:"admin_title"`
InitChatCalls int `json:"init_chat_calls"` // 新用户注册赠送对话次数 InitPower int `json:"init_power"` // 新用户注册赠送算力值
InitImgCalls int `json:"init_img_calls"` // 新用户注册赠送绘图次数
VipMonthCalls int `json:"vip_month_calls"` // VIP 会员每月赠送的对话次数
VipMonthImgCalls int `json:"vip_month_img_calls"` // VIP 会员每月赠送绘图次数
RegisterWays []string `json:"register_ways"` // 注册方式:支持手机,邮箱注册 RegisterWays []string `json:"register_ways"` // 注册方式:支持手机,邮箱注册
EnabledRegister bool `json:"enabled_register"` // 是否开放注册 EnabledRegister bool `json:"enabled_register"` // 是否开放注册
RewardImg string `json:"reward_img"` // 众筹收款二维码地址 RewardImg string `json:"reward_img"` // 众筹收款二维码地址
EnabledReward bool `json:"enabled_reward"` // 启用众筹功能 EnabledReward bool `json:"enabled_reward"` // 启用众筹功能
ChatCallPrice float64 `json:"chat_call_price"` // 对话单次调用费用 PowerPrice float64 `json:"power_price"` // 算力单价
ImgCallPrice float64 `json:"img_call_price"` // 绘图单次调用费用
OrderPayTimeout int `json:"order_pay_timeout"` //订单支付超时时间 OrderPayTimeout int `json:"order_pay_timeout"` //订单支付超时时间
DefaultModels []string `json:"default_models"` // 默认开通的 AI 模型 DefaultModels []string `json:"default_models"` // 默认开通的 AI 模型
OrderPayInfoText string `json:"order_pay_info_text"` // 订单支付页面说明文字 OrderPayInfoText string `json:"order_pay_info_text"` // 订单支付页面说明文字
InviteChatCalls int `json:"invite_chat_calls"` // 邀请用户注册奖励对话次数 InvitePower int `json:"invite_power"` // 邀请新用户赠送算力值
InviteImgCalls int `json:"invite_img_calls"` // 邀请用户注册奖励绘图次数 VipMonthPower int `json:"vip_month_power"` // VIP 会员每月赠送的算力值
MjPower int `json:"mj_power"` // MJ 绘画消耗算力
SdPower int `json:"sd_power"` // SD 绘画消耗算力
DallPower int `json:"dall_power"` // DALLE3 绘图消耗算力
WechatCardURL string `json:"wechat_card_url"` // 微信客服地址 WechatCardURL string `json:"wechat_card_url"` // 微信客服地址
} }

View File

@ -9,10 +9,9 @@ const (
) )
type OrderRemark struct { type OrderRemark struct {
Days int `json:"days"` // 有效期 Days int `json:"days"` // 有效期
Calls int `json:"calls"` // 增加对话次数 Power int `json:"power"` // 增加算力点数
ImgCalls int `json:"img_calls"` // 增加绘图次数 Name string `json:"name"` // 产品名称
Name string `json:"name"` // 产品名称
Price float64 `json:"price"` Price float64 `json:"price"`
Discount float64 `json:"discount"` Discount float64 `json:"discount"`
} }

View File

@ -48,7 +48,7 @@ func (h *ChatModelHandler) Save(c *gin.Context) {
Enabled: data.Enabled, Enabled: data.Enabled,
SortNum: data.SortNum, SortNum: data.SortNum,
Open: data.Open, Open: data.Open,
Weight: data.Weight} Power: data.Weight}
item.Id = data.Id item.Id = data.Id
if item.Id > 0 { if item.Id > 0 {
item.CreatedAt = time.Unix(data.CreatedAt, 0) item.CreatedAt = time.Unix(data.CreatedAt, 0)

View File

@ -32,8 +32,7 @@ func (h *ProductHandler) Save(c *gin.Context) {
Discount float64 `json:"discount"` Discount float64 `json:"discount"`
Enabled bool `json:"enabled"` Enabled bool `json:"enabled"`
Days int `json:"days"` Days int `json:"days"`
Calls int `json:"calls"` Power int `json:"power"`
ImgCalls int `json:"img_calls"`
CreatedAt int64 `json:"created_at"` CreatedAt int64 `json:"created_at"`
} }
if err := c.ShouldBindJSON(&data); err != nil { if err := c.ShouldBindJSON(&data); err != nil {
@ -46,8 +45,7 @@ func (h *ProductHandler) Save(c *gin.Context) {
Price: data.Price, Price: data.Price,
Discount: data.Discount, Discount: data.Discount,
Days: data.Days, Days: data.Days,
Calls: data.Calls, Power: data.Power,
ImgCalls: data.ImgCalls,
Enabled: data.Enabled} Enabled: data.Enabled}
item.Id = data.Id item.Id = data.Id
if item.Id > 0 { if item.Id > 0 {

View File

@ -66,8 +66,6 @@ func (h *UserHandler) Save(c *gin.Context) {
Id uint `json:"id"` Id uint `json:"id"`
Password string `json:"password"` Password string `json:"password"`
Username string `json:"username"` Username string `json:"username"`
Calls int `json:"calls"`
ImgCalls int `json:"img_calls"`
ChatRoles []string `json:"chat_roles"` ChatRoles []string `json:"chat_roles"`
ChatModels []string `json:"chat_models"` ChatModels []string `json:"chat_models"`
ExpiredTime string `json:"expired_time"` ExpiredTime string `json:"expired_time"`
@ -86,8 +84,6 @@ func (h *UserHandler) Save(c *gin.Context) {
// 此处需要用 map 更新,用结构体无法更新 0 值 // 此处需要用 map 更新,用结构体无法更新 0 值
res = h.db.Model(&user).Updates(map[string]interface{}{ res = h.db.Model(&user).Updates(map[string]interface{}{
"username": data.Username, "username": data.Username,
"calls": data.Calls,
"img_calls": data.ImgCalls,
"status": data.Status, "status": data.Status,
"vip": data.Vip, "vip": data.Vip,
"chat_roles_json": utils.JsonEncode(data.ChatRoles), "chat_roles_json": utils.JsonEncode(data.ChatRoles),
@ -113,8 +109,6 @@ func (h *UserHandler) Save(c *gin.Context) {
types.ChatGLM: "", types.ChatGLM: "",
}, },
}), }),
Calls: data.Calls,
ImgCalls: data.ImgCalls,
} }
res = h.db.Create(&u) res = h.db.Create(&u)
_ = utils.CopyObject(u, &userVo) _ = utils.CopyObject(u, &userVo)

View File

@ -103,8 +103,6 @@ func (h *ChatHandler) sendAzureMessage(
// 消息发送成功 // 消息发送成功
if len(contents) > 0 { if len(contents) > 0 {
// 更新用户的对话次数
h.subUserCalls(userVo, session)
if message.Role == "" { if message.Role == "" {
message.Role = "assistant" message.Role = "assistant"
@ -145,8 +143,8 @@ func (h *ChatHandler) sendAzureMessage(
} }
// 计算本次对话消耗的总 token 数量 // 计算本次对话消耗的总 token 数量
totalTokens, _ := utils.CalcTokens(message.Content, req.Model) replyTokens, _ := utils.CalcTokens(message.Content, req.Model)
totalTokens += getTotalTokens(req) replyTokens += getTotalTokens(req)
historyReplyMsg := model.ChatMessage{ historyReplyMsg := model.ChatMessage{
UserId: userVo.Id, UserId: userVo.Id,
@ -155,7 +153,7 @@ func (h *ChatHandler) sendAzureMessage(
Type: types.ReplyMsg, Type: types.ReplyMsg,
Icon: role.Icon, Icon: role.Icon,
Content: message.Content, Content: message.Content,
Tokens: totalTokens, Tokens: replyTokens,
UseContext: true, UseContext: true,
Model: req.Model, Model: req.Model,
} }
@ -166,8 +164,8 @@ func (h *ChatHandler) sendAzureMessage(
logger.Error("failed to save reply history message: ", res.Error) logger.Error("failed to save reply history message: ", res.Error)
} }
// 更新用户信息 // 更新用户算力
h.incUserTokenFee(userVo.Id, totalTokens) h.subUserPower(userVo, session, promptToken, replyTokens)
} }
// 保存当前会话 // 保存当前会话

View File

@ -128,9 +128,6 @@ func (h *ChatHandler) sendBaiduMessage(
// 消息发送成功 // 消息发送成功
if len(contents) > 0 { if len(contents) > 0 {
// 更新用户的对话次数
h.subUserCalls(userVo, session)
if message.Role == "" { if message.Role == "" {
message.Role = "assistant" message.Role = "assistant"
} }
@ -171,8 +168,8 @@ func (h *ChatHandler) sendBaiduMessage(
// for reply // for reply
// 计算本次对话消耗的总 token 数量 // 计算本次对话消耗的总 token 数量
replyToken, _ := utils.CalcTokens(message.Content, req.Model) replyTokens, _ := utils.CalcTokens(message.Content, req.Model)
totalTokens := replyToken + getTotalTokens(req) totalTokens := replyTokens + getTotalTokens(req)
historyReplyMsg := model.ChatMessage{ historyReplyMsg := model.ChatMessage{
UserId: userVo.Id, UserId: userVo.Id,
ChatId: session.ChatId, ChatId: session.ChatId,
@ -190,8 +187,8 @@ func (h *ChatHandler) sendBaiduMessage(
if res.Error != nil { if res.Error != nil {
logger.Error("failed to save reply history message: ", res.Error) logger.Error("failed to save reply history message: ", res.Error)
} }
// 更新用户信息 // 更新用户算力
h.incUserTokenFee(userVo.Id, totalTokens) h.subUserPower(userVo, session, promptToken, replyTokens)
} }
// 保存当前会话 // 保存当前会话

View File

@ -111,7 +111,7 @@ func (h *ChatHandler) ChatHandle(c *gin.Context) {
session.Model = types.ChatModel{ session.Model = types.ChatModel{
Id: chatModel.Id, Id: chatModel.Id,
Value: chatModel.Value, Value: chatModel.Value,
Weight: chatModel.Weight, Power: chatModel.Power,
Platform: types.Platform(chatModel.Platform)} Platform: types.Platform(chatModel.Platform)}
logger.Infof("New websocket connected, IP: %s, Username: %s", c.ClientIP(), session.Username) logger.Infof("New websocket connected, IP: %s, Username: %s", c.ClientIP(), session.Username)
var chatRole model.ChatRole var chatRole model.ChatRole
@ -207,13 +207,13 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSessio
return nil return nil
} }
if userVo.Calls < session.Model.Weight { if userVo.Power < session.Model.Power {
utils.ReplyMessage(ws, fmt.Sprintf("您当前剩余对话次数(%d已不足以支付当前模型的单次对话需要消耗的对话额度%d", userVo.Calls, session.Model.Weight)) utils.ReplyMessage(ws, fmt.Sprintf("您当前剩余对话次数(%d已不足以支付当前模型的单次对话需要消耗的对话额度%d", userVo.Power, session.Model.Power))
utils.ReplyMessage(ws, ErrImg) utils.ReplyMessage(ws, ErrImg)
return nil return nil
} }
if userVo.Calls <= 0 && userVo.ChatConfig.ApiKeys[session.Model.Platform] == "" { if userVo.Power <= 0 && userVo.ChatConfig.ApiKeys[session.Model.Platform] == "" {
utils.ReplyMessage(ws, "您的对话次数已经用尽,请联系管理员或者充值点卡继续对话!") utils.ReplyMessage(ws, "您的对话次数已经用尽,请联系管理员或者充值点卡继续对话!")
utils.ReplyMessage(ws, ErrImg) utils.ReplyMessage(ws, ErrImg)
return nil return nil
@ -533,23 +533,28 @@ func (h *ChatHandler) doRequest(ctx context.Context, req types.ApiRequest, platf
return client.Do(request) return client.Do(request)
} }
// 扣减用户的对话次数 // 扣减用户算力
func (h *ChatHandler) subUserCalls(userVo vo.User, session *types.ChatSession) { func (h *ChatHandler) subUserPower(userVo vo.User, session *types.ChatSession, promptTokens int, replyTokens int) {
// 仅当用户没有导入自己的 API KEY 时才进行扣减 power := 1
if userVo.ChatConfig.ApiKeys[session.Model.Platform] == "" { if session.Model.Power > 0 {
num := 1 power = session.Model.Power
if session.Model.Weight > 0 { }
num = session.Model.Weight res := h.db.Model(&model.User{}).Where("id = ?", userVo.Id).UpdateColumn("power", gorm.Expr("power - ?", power))
} if res.Error == nil {
h.db.Model(&model.User{}).Where("id = ?", userVo.Id).UpdateColumn("calls", gorm.Expr("calls - ?", num)) // 记录算力消费日志
h.db.Debug().Create(&model.PowerLog{
UserId: userVo.Id,
Username: userVo.Username,
Type: types.PowerConsume,
Amount: power,
Mark: types.PowerSub,
Balance: userVo.Power - power,
Model: session.Model.Value,
Remark: fmt.Sprintf("提问长度:%d回复长度%d", promptTokens, replyTokens),
CreatedAt: time.Now(),
})
} }
}
func (h *ChatHandler) incUserTokenFee(userId uint, tokens int) {
h.db.Model(&model.User{}).Where("id = ?", userId).
UpdateColumn("total_tokens", gorm.Expr("total_tokens + ?", tokens))
h.db.Model(&model.User{}).Where("id = ?", userId).
UpdateColumn("tokens", gorm.Expr("tokens + ?", tokens))
} }
// 将AI回复消息中生成的图片链接下载到本地 // 将AI回复消息中生成的图片链接下载到本地

View File

@ -107,9 +107,6 @@ func (h *ChatHandler) sendChatGLMMessage(
// 消息发送成功 // 消息发送成功
if len(contents) > 0 { if len(contents) > 0 {
// 更新用户的对话次数
h.subUserCalls(userVo, session)
if message.Role == "" { if message.Role == "" {
message.Role = "assistant" message.Role = "assistant"
} }
@ -150,8 +147,8 @@ func (h *ChatHandler) sendChatGLMMessage(
// for reply // for reply
// 计算本次对话消耗的总 token 数量 // 计算本次对话消耗的总 token 数量
replyToken, _ := utils.CalcTokens(message.Content, req.Model) replyTokens, _ := utils.CalcTokens(message.Content, req.Model)
totalTokens := replyToken + getTotalTokens(req) totalTokens := replyTokens + getTotalTokens(req)
historyReplyMsg := model.ChatMessage{ historyReplyMsg := model.ChatMessage{
UserId: userVo.Id, UserId: userVo.Id,
ChatId: session.ChatId, ChatId: session.ChatId,
@ -169,8 +166,9 @@ func (h *ChatHandler) sendChatGLMMessage(
if res.Error != nil { if res.Error != nil {
logger.Error("failed to save reply history message: ", res.Error) logger.Error("failed to save reply history message: ", res.Error)
} }
// 更新用户信息
h.incUserTokenFee(userVo.Id, totalTokens) // 更新用户算力
h.subUserPower(userVo, session, promptToken, replyTokens)
} }
// 保存当前会话 // 保存当前会话

View File

@ -173,9 +173,6 @@ func (h *ChatHandler) sendOpenAiMessage(
// 消息发送成功 // 消息发送成功
if len(contents) > 0 { if len(contents) > 0 {
// 更新用户的对话次数
h.subUserCalls(userVo, session)
if message.Role == "" { if message.Role == "" {
message.Role = "assistant" message.Role = "assistant"
} }
@ -220,16 +217,16 @@ func (h *ChatHandler) sendOpenAiMessage(
} }
// 计算本次对话消耗的总 token 数量 // 计算本次对话消耗的总 token 数量
var totalTokens = 0 var replyTokens = 0
if toolCall { // prompt + 函数名 + 参数 token if toolCall { // prompt + 函数名 + 参数 token
tokens, _ := utils.CalcTokens(function.Name, req.Model) tokens, _ := utils.CalcTokens(function.Name, req.Model)
totalTokens += tokens replyTokens += tokens
tokens, _ = utils.CalcTokens(utils.InterfaceToString(arguments), req.Model) tokens, _ = utils.CalcTokens(utils.InterfaceToString(arguments), req.Model)
totalTokens += tokens replyTokens += tokens
} else { } else {
totalTokens, _ = utils.CalcTokens(message.Content, req.Model) replyTokens, _ = utils.CalcTokens(message.Content, req.Model)
} }
totalTokens += getTotalTokens(req) replyTokens += getTotalTokens(req)
historyReplyMsg := model.ChatMessage{ historyReplyMsg := model.ChatMessage{
UserId: userVo.Id, UserId: userVo.Id,
@ -238,7 +235,7 @@ func (h *ChatHandler) sendOpenAiMessage(
Type: types.ReplyMsg, Type: types.ReplyMsg,
Icon: role.Icon, Icon: role.Icon,
Content: h.extractImgUrl(message.Content), Content: h.extractImgUrl(message.Content),
Tokens: totalTokens, Tokens: replyTokens,
UseContext: useContext, UseContext: useContext,
Model: req.Model, Model: req.Model,
} }
@ -249,8 +246,8 @@ func (h *ChatHandler) sendOpenAiMessage(
logger.Error("failed to save reply history message: ", res.Error) logger.Error("failed to save reply history message: ", res.Error)
} }
// 更新用户信息 // 更新用户算力
h.incUserTokenFee(userVo.Id, totalTokens) h.subUserPower(userVo, session, promptToken, replyTokens)
} }
// 保存当前会话 // 保存当前会话

View File

@ -128,9 +128,6 @@ func (h *ChatHandler) sendQWenMessage(
// 消息发送成功 // 消息发送成功
if len(contents) > 0 { if len(contents) > 0 {
// 更新用户的对话次数
h.subUserCalls(userVo, session)
if message.Role == "" { if message.Role == "" {
message.Role = "assistant" message.Role = "assistant"
} }
@ -171,8 +168,8 @@ func (h *ChatHandler) sendQWenMessage(
// for reply // for reply
// 计算本次对话消耗的总 token 数量 // 计算本次对话消耗的总 token 数量
replyToken, _ := utils.CalcTokens(message.Content, req.Model) replyTokens, _ := utils.CalcTokens(message.Content, req.Model)
totalTokens := replyToken + getTotalTokens(req) totalTokens := replyTokens + getTotalTokens(req)
historyReplyMsg := model.ChatMessage{ historyReplyMsg := model.ChatMessage{
UserId: userVo.Id, UserId: userVo.Id,
ChatId: session.ChatId, ChatId: session.ChatId,
@ -190,8 +187,9 @@ func (h *ChatHandler) sendQWenMessage(
if res.Error != nil { if res.Error != nil {
logger.Error("failed to save reply history message: ", res.Error) logger.Error("failed to save reply history message: ", res.Error)
} }
// 更新用户信息
h.incUserTokenFee(userVo.Id, totalTokens) // 更新用户算力
h.subUserPower(userVo, session, promptToken, replyTokens)
} }
// 保存当前会话 // 保存当前会话

View File

@ -166,9 +166,6 @@ func (h *ChatHandler) sendXunFeiMessage(
// 消息发送成功 // 消息发送成功
if len(contents) > 0 { if len(contents) > 0 {
// 更新用户的对话次数
h.subUserCalls(userVo, session)
if message.Role == "" { if message.Role == "" {
message.Role = "assistant" message.Role = "assistant"
} }
@ -209,8 +206,8 @@ func (h *ChatHandler) sendXunFeiMessage(
// for reply // for reply
// 计算本次对话消耗的总 token 数量 // 计算本次对话消耗的总 token 数量
replyToken, _ := utils.CalcTokens(message.Content, req.Model) replyTokens, _ := utils.CalcTokens(message.Content, req.Model)
totalTokens := replyToken + getTotalTokens(req) totalTokens := replyTokens + getTotalTokens(req)
historyReplyMsg := model.ChatMessage{ historyReplyMsg := model.ChatMessage{
UserId: userVo.Id, UserId: userVo.Id,
ChatId: session.ChatId, ChatId: session.ChatId,
@ -228,8 +225,9 @@ func (h *ChatHandler) sendXunFeiMessage(
if res.Error != nil { if res.Error != nil {
logger.Error("failed to save reply history message: ", res.Error) logger.Error("failed to save reply history message: ", res.Error)
} }
// 更新用户信息
h.incUserTokenFee(userVo.Id, totalTokens) // 更新用户算力
h.subUserPower(userVo, session, promptToken, replyTokens)
} }
// 保存当前会话 // 保存当前会话

View File

@ -200,8 +200,8 @@ func (h *FunctionHandler) Dall3(c *gin.Context) {
return return
} }
if user.ImgCalls <= 0 { if user.Power < h.App.SysConfig.DallPower {
resp.ERROR(c, "当前用户的绘图次数额度不足") resp.ERROR(c, "当前用户剩余算力不足以完成本次绘画")
return return
} }
@ -275,7 +275,21 @@ func (h *FunctionHandler) Dall3(c *gin.Context) {
content := fmt.Sprintf("下面是根据您的描述创作的图片,它描绘了 【%s】 的场景。 \n\n![](%s)\n", prompt, imgURL) content := fmt.Sprintf("下面是根据您的描述创作的图片,它描绘了 【%s】 的场景。 \n\n![](%s)\n", prompt, imgURL)
// update user's img_calls // update user's img_calls
h.db.Model(&model.User{}).Where("id = ?", user.Id).UpdateColumn("img_calls", gorm.Expr("img_calls - ?", 1)) tx = h.db.Model(&model.User{}).Where("id = ?", user.Id).UpdateColumn("power", gorm.Expr("power - ?", h.App.SysConfig.DallPower))
// 记录算力变化日志
if tx.Error == nil && tx.RowsAffected > 0 {
h.db.Create(&model.PowerLog{
UserId: user.Id,
Username: user.Username,
Type: types.PowerConsume,
Amount: h.App.SysConfig.DallPower,
Balance: user.Power - h.App.SysConfig.DallPower,
Mark: types.PowerSub,
Model: "dall-e-3",
Remark: "",
CreatedAt: time.Now(),
})
}
resp.SUCCESS(c, content) resp.SUCCESS(c, content)
} }

View File

@ -48,8 +48,8 @@ func (h *MidJourneyHandler) preCheck(c *gin.Context) bool {
return false return false
} }
if user.ImgCalls <= 0 { if user.Power < h.App.SysConfig.MjPower {
resp.ERROR(c, "您的绘图次数不足,请联系管理员充值") resp.ERROR(c, "当前用户剩余算力不足以完成本次绘画")
return false return false
} }
@ -160,13 +160,18 @@ func (h *MidJourneyHandler) Image(c *gin.Context) {
TaskId: taskId, TaskId: taskId,
Progress: 0, Progress: 0,
Prompt: prompt, Prompt: prompt,
Power: h.App.SysConfig.MjPower,
CreatedAt: time.Now(), CreatedAt: time.Now(),
} }
opt := "绘图"
if data.TaskType == types.TaskBlend.String() { if data.TaskType == types.TaskBlend.String() {
data.Prompt = "融图:" + strings.Join(data.ImgArr, ",") job.Prompt = "融图:" + strings.Join(data.ImgArr, ",")
opt = "融图"
} else if data.TaskType == types.TaskSwapFace.String() { } else if data.TaskType == types.TaskSwapFace.String() {
data.Prompt = "换脸:" + strings.Join(data.ImgArr, ",") job.Prompt = "换脸:" + strings.Join(data.ImgArr, ",")
opt = "换脸"
} }
if res := h.db.Create(&job); res.Error != nil || res.RowsAffected == 0 { if res := h.db.Create(&job); res.Error != nil || res.RowsAffected == 0 {
resp.ERROR(c, "添加任务失败:"+res.Error.Error()) resp.ERROR(c, "添加任务失败:"+res.Error.Error())
return return
@ -187,8 +192,23 @@ func (h *MidJourneyHandler) Image(c *gin.Context) {
_ = client.Send([]byte("Task Updated")) _ = client.Send([]byte("Task Updated"))
} }
// update user's img calls // update user's power
h.db.Model(&model.User{}).Where("id = ?", job.UserId).UpdateColumn("img_calls", gorm.Expr("img_calls - ?", 1)) tx := h.db.Model(&model.User{}).Where("id = ?", job.UserId).UpdateColumn("power", gorm.Expr("power - ?", job.Power))
// 记录算力变化日志
if tx.Error == nil && tx.RowsAffected > 0 {
user, _ := utils.GetLoginUser(c, h.db)
h.db.Create(&model.PowerLog{
UserId: user.Id,
Username: user.Username,
Type: types.PowerConsume,
Amount: job.Power,
Balance: user.Power - job.Power,
Mark: types.PowerSub,
Model: "mid-journey",
Remark: fmt.Sprintf("%s操作任务ID%s", opt, job.TaskId),
CreatedAt: time.Now(),
})
}
resp.SUCCESS(c) resp.SUCCESS(c)
} }
@ -276,6 +296,7 @@ func (h *MidJourneyHandler) Variation(c *gin.Context) {
TaskId: taskId, TaskId: taskId,
Progress: 0, Progress: 0,
Prompt: data.Prompt, Prompt: data.Prompt,
Power: h.App.SysConfig.MjPower,
CreatedAt: time.Now(), CreatedAt: time.Now(),
} }
if res := h.db.Create(&job); res.Error != nil || res.RowsAffected == 0 { if res := h.db.Create(&job); res.Error != nil || res.RowsAffected == 0 {
@ -300,8 +321,23 @@ func (h *MidJourneyHandler) Variation(c *gin.Context) {
_ = client.Send([]byte("Task Updated")) _ = client.Send([]byte("Task Updated"))
} }
// update user's img calls // update user's power
h.db.Model(&model.User{}).Where("id = ?", job.UserId).UpdateColumn("img_calls", gorm.Expr("img_calls - ?", 1)) tx := h.db.Model(&model.User{}).Where("id = ?", job.UserId).UpdateColumn("power", gorm.Expr("power - ?", job.Power))
// 记录算力变化日志
if tx.Error == nil && tx.RowsAffected > 0 {
user, _ := utils.GetLoginUser(c, h.db)
h.db.Create(&model.PowerLog{
UserId: user.Id,
Username: user.Username,
Type: types.PowerConsume,
Amount: job.Power,
Balance: user.Power - job.Power,
Mark: types.PowerSub,
Model: "mid-journey",
Remark: fmt.Sprintf("Variation 操作任务ID%s", job.TaskId),
CreatedAt: time.Now(),
})
}
resp.SUCCESS(c) resp.SUCCESS(c)
} }
@ -368,13 +404,6 @@ func (h *MidJourneyHandler) getData(finish bool, userId uint, page int, pageSize
continue continue
} }
// 失败的任务直接删除
if job.Progress == -1 {
h.db.Delete(&model.MidJourneyJob{Id: job.Id})
jobs = append(jobs, job)
continue
}
if item.Progress < 100 && item.ImgURL == "" && item.OrgURL != "" { if item.Progress < 100 && item.ImgURL == "" && item.OrgURL != "" {
// discord 服务器图片需要使用代理转发图片数据流 // discord 服务器图片需要使用代理转发图片数据流
if strings.HasPrefix(item.OrgURL, "https://cdn.discordapp.com") { if strings.HasPrefix(item.OrgURL, "https://cdn.discordapp.com") {

View File

@ -202,8 +202,7 @@ func (h *PaymentHandler) PayQrcode(c *gin.Context) {
// 创建订单 // 创建订单
remark := types.OrderRemark{ remark := types.OrderRemark{
Days: product.Days, Days: product.Days,
Calls: product.Calls, Power: product.Power,
ImgCalls: product.ImgCalls,
Name: product.Name, Name: product.Name,
Price: product.Price, Price: product.Price,
Discount: product.Discount, Discount: product.Discount,
@ -313,20 +312,16 @@ func (h *PaymentHandler) notify(orderNo string, tradeNo string) error {
if remark.Days > 0 { // 只延期 VIP不增加调用次数 if remark.Days > 0 { // 只延期 VIP不增加调用次数
user.ExpiredTime = time.Unix(user.ExpiredTime, 0).AddDate(0, 0, remark.Days).Unix() user.ExpiredTime = time.Unix(user.ExpiredTime, 0).AddDate(0, 0, remark.Days).Unix()
} else { // 充值点卡,直接增加次数即可 } else { // 充值点卡,直接增加次数即可
user.Calls += remark.Calls user.Power += remark.Power
user.ImgCalls += remark.ImgCalls
} }
} else { // 非 VIP 用户 } else { // 非 VIP 用户
if remark.Days > 0 { // vip 套餐days > 0, calls == 0 if remark.Days > 0 { // vip 套餐days > 0, calls == 0
user.ExpiredTime = time.Now().AddDate(0, 0, remark.Days).Unix() user.ExpiredTime = time.Now().AddDate(0, 0, remark.Days).Unix()
user.Calls += h.App.SysConfig.VipMonthCalls user.Power += h.App.SysConfig.VipMonthPower
user.ImgCalls += h.App.SysConfig.VipMonthImgCalls
user.Vip = true user.Vip = true
} else { //点卡days == 0, calls > 0 } else { //点卡days == 0, calls > 0
user.Calls += remark.Calls user.Power += remark.Power
user.ImgCalls += remark.ImgCalls
} }
} }

View File

@ -7,11 +7,13 @@ import (
"chatplus/store/vo" "chatplus/store/vo"
"chatplus/utils" "chatplus/utils"
"chatplus/utils/resp" "chatplus/utils/resp"
"fmt"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"gorm.io/gorm" "gorm.io/gorm"
"math" "math"
"strings" "strings"
"sync" "sync"
"time"
) )
type RewardHandler struct { type RewardHandler struct {
@ -30,7 +32,6 @@ func NewRewardHandler(server *core.AppServer, db *gorm.DB) *RewardHandler {
func (h *RewardHandler) Verify(c *gin.Context) { func (h *RewardHandler) Verify(c *gin.Context) {
var data struct { var data struct {
TxId string `json:"tx_id"` TxId string `json:"tx_id"`
Type string `json:"type"`
} }
if err := c.ShouldBindJSON(&data); err != nil { if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs) resp.ERROR(c, types.InvalidArgs)
@ -63,16 +64,11 @@ func (h *RewardHandler) Verify(c *gin.Context) {
tx := h.db.Begin() tx := h.db.Begin()
exchange := vo.RewardExchange{} exchange := vo.RewardExchange{}
if data.Type == "chat" { power := math.Ceil(item.Amount / h.App.SysConfig.PowerPrice)
calls := math.Ceil(item.Amount / h.App.SysConfig.ChatCallPrice) exchange.Power = int(power)
exchange.Calls = int(calls) res = tx.Model(&user).UpdateColumn("power", gorm.Expr("power + ?", exchange.Power))
res = h.db.Model(&user).UpdateColumn("calls", gorm.Expr("calls + ?", calls))
} else if data.Type == "img" {
calls := math.Ceil(item.Amount / h.App.SysConfig.ImgCallPrice)
exchange.ImgCalls = int(calls)
res = h.db.Model(&user).UpdateColumn("img_calls", gorm.Expr("img_calls + ?", calls))
}
if res.Error != nil { if res.Error != nil {
tx.Rollback()
resp.ERROR(c, "更新数据库失败!") resp.ERROR(c, "更新数据库失败!")
return return
} }
@ -81,13 +77,25 @@ func (h *RewardHandler) Verify(c *gin.Context) {
item.Status = true item.Status = true
item.UserId = user.Id item.UserId = user.Id
item.Exchange = utils.JsonEncode(exchange) item.Exchange = utils.JsonEncode(exchange)
res = h.db.Updates(&item) res = tx.Updates(&item)
if res.Error != nil { if res.Error != nil {
tx.Rollback() tx.Rollback()
resp.ERROR(c, "更新数据库失败!") resp.ERROR(c, "更新数据库失败!")
return return
} }
// 记录算力充值日志
h.db.Create(&model.PowerLog{
UserId: user.Id,
Username: user.Username,
Type: types.PowerReward,
Amount: exchange.Power,
Balance: user.Power + exchange.Power,
Mark: types.PowerAdd,
Model: "",
Remark: fmt.Sprintf("众筹充值算力,金额:%f价格%f", item.Amount, h.App.SysConfig.PowerPrice),
CreatedAt: time.Now(),
})
tx.Commit() tx.Commit()
resp.SUCCESS(c) resp.SUCCESS(c)

View File

@ -72,8 +72,8 @@ func (h *SdJobHandler) checkLimits(c *gin.Context) bool {
return false return false
} }
if user.ImgCalls <= 0 { if user.Power < h.App.SysConfig.SdPower {
resp.ERROR(c, "您的绘图次数不足,请联系管理员充值") resp.ERROR(c, "当前用户剩余算力不足以完成本次绘画")
return false return false
} }
@ -140,6 +140,7 @@ func (h *SdJobHandler) Image(c *gin.Context) {
Params: utils.JsonEncode(params), Params: utils.JsonEncode(params),
Prompt: data.Prompt, Prompt: data.Prompt,
Progress: 0, Progress: 0,
Power: h.App.SysConfig.SdPower,
CreatedAt: time.Now(), CreatedAt: time.Now(),
} }
res := h.db.Create(&job) res := h.db.Create(&job)
@ -162,8 +163,23 @@ func (h *SdJobHandler) Image(c *gin.Context) {
_ = client.Send([]byte("Task Updated")) _ = client.Send([]byte("Task Updated"))
} }
// update user's img calls // update user's power
h.db.Model(&model.User{}).Where("id = ?", job.UserId).UpdateColumn("img_calls", gorm.Expr("img_calls - ?", 1)) tx := h.db.Model(&model.User{}).Where("id = ?", job.UserId).UpdateColumn("power", gorm.Expr("power - ?", job.Power))
// 记录算力变化日志
if tx.Error == nil && tx.RowsAffected > 0 {
user, _ := utils.GetLoginUser(c, h.db)
h.db.Create(&model.PowerLog{
UserId: user.Id,
Username: user.Username,
Type: types.PowerConsume,
Amount: job.Power,
Balance: user.Power - job.Power,
Mark: types.PowerSub,
Model: "stable-diffusion",
Remark: fmt.Sprintf("绘图操作任务ID%s", job.TaskId),
CreatedAt: time.Now(),
})
}
resp.SUCCESS(c) resp.SUCCESS(c)
} }
@ -232,18 +248,7 @@ func (h *SdJobHandler) getData(finish bool, userId uint, page int, pageSize int,
continue continue
} }
if job.Progress == -1 {
h.db.Delete(&model.SdJob{Id: job.Id})
}
if item.Progress < 100 { if item.Progress < 100 {
// 5 分钟还没完成的任务直接删除
if time.Now().Sub(item.CreatedAt) > time.Minute*5 {
h.db.Delete(&item)
// 退回绘图次数
h.db.Model(&model.User{}).Where("id = ?", item.UserId).UpdateColumn("img_calls", gorm.Expr("img_calls + ?", 1))
continue
}
// 正在运行中任务使用代理访问图片 // 正在运行中任务使用代理访问图片
image, err := utils.DownloadImage(item.ImgURL, "") image, err := utils.DownloadImage(item.ImgURL, "")
if err == nil { if err == nil {

View File

@ -102,8 +102,7 @@ func (h *UserHandler) Register(c *gin.Context) {
types.ChatGLM: "", types.ChatGLM: "",
}, },
}), }),
Calls: h.App.SysConfig.InitChatCalls, Power: h.App.SysConfig.InitPower,
ImgCalls: h.App.SysConfig.InitImgCalls,
} }
res = h.db.Create(&user) res = h.db.Create(&user)
@ -117,11 +116,8 @@ func (h *UserHandler) Register(c *gin.Context) {
if data.InviteCode != "" { if data.InviteCode != "" {
// 增加邀请数量 // 增加邀请数量
h.db.Model(&model.InviteCode{}).Where("code = ?", data.InviteCode).UpdateColumn("reg_num", gorm.Expr("reg_num + ?", 1)) h.db.Model(&model.InviteCode{}).Where("code = ?", data.InviteCode).UpdateColumn("reg_num", gorm.Expr("reg_num + ?", 1))
if h.App.SysConfig.InviteChatCalls > 0 { if h.App.SysConfig.InvitePower > 0 {
h.db.Model(&model.User{}).Where("id = ?", inviteCode.UserId).UpdateColumn("calls", gorm.Expr("calls + ?", h.App.SysConfig.InviteChatCalls)) h.db.Model(&model.User{}).Where("id = ?", inviteCode.UserId).UpdateColumn("power", gorm.Expr("power + ?", h.App.SysConfig.InvitePower))
}
if h.App.SysConfig.InviteImgCalls > 0 {
h.db.Model(&model.User{}).Where("id = ?", inviteCode.UserId).UpdateColumn("img_calls", gorm.Expr("img_calls + ?", h.App.SysConfig.InviteImgCalls))
} }
// 添加邀请记录 // 添加邀请记录
@ -130,7 +126,7 @@ func (h *UserHandler) Register(c *gin.Context) {
UserId: user.Id, UserId: user.Id,
Username: user.Username, Username: user.Username,
InviteCode: inviteCode.Code, InviteCode: inviteCode.Code,
Reward: utils.JsonEncode(types.InviteReward{ChatCalls: h.App.SysConfig.InviteChatCalls, ImgCalls: h.App.SysConfig.InviteImgCalls}), Reward: utils.JsonEncode(types.InviteReward{Power: h.App.SysConfig.InvitePower}),
}) })
} }
@ -254,10 +250,7 @@ type userProfile struct {
Username string `json:"username"` Username string `json:"username"`
Avatar string `json:"avatar"` Avatar string `json:"avatar"`
ChatConfig types.UserChatConfig `json:"chat_config"` ChatConfig types.UserChatConfig `json:"chat_config"`
Calls int `json:"calls"` Power int `json:"power"`
ImgCalls int `json:"img_calls"`
TotalTokens int64 `json:"total_tokens"`
Tokens int `json:"tokens"`
ExpiredTime int64 `json:"expired_time"` ExpiredTime int64 `json:"expired_time"`
Vip bool `json:"vip"` Vip bool `json:"vip"`
} }

View File

@ -96,12 +96,6 @@ func (s *Service) Run() {
s.db.Updates(&job) s.db.Updates(&job)
// 任务失败,通知前端 // 任务失败,通知前端
s.notifyQueue.RPush(task.UserId) s.notifyQueue.RPush(task.UserId)
// restore img_call quota
if task.Type.String() != types.TaskUpscale.String() {
s.db.Model(&model.User{}).Where("id = ?", task.UserId).UpdateColumn("img_calls", gorm.Expr("img_calls + ?", 1))
}
// TODO: 任务提交失败,加入队列重试
continue continue
} }
logger.Infof("任务提交成功:%+v", res) logger.Infof("任务提交成功:%+v", res)

View File

@ -191,28 +191,43 @@ func (p *ServicePool) SyncTaskProgress() {
go func() { go func() {
var items []model.MidJourneyJob var items []model.MidJourneyJob
for { for {
res := p.db.Where("progress >= ? AND progress < ?", 0, 100).Find(&items) res := p.db.Where("progress < ?", 100).Find(&items)
if res.Error != nil { if res.Error != nil {
continue continue
} }
for _, v := range items { for _, job := range items {
// 30 分钟还没完成的任务直接删除 // 失败或者 30 分钟还没完成的任务删除并退回算力
if time.Now().Sub(v.CreatedAt) > time.Minute*30 { if time.Now().Sub(job.CreatedAt) > time.Minute*30 || job.Progress == -1 {
p.db.Delete(&v) p.db.Delete(&job)
// 非放大任务,退回绘图次数 // 略过 Upscale 任务
if v.Type != types.TaskUpscale.String() { if job.Type != types.TaskUpscale.String() {
p.db.Model(&model.User{}).Where("id = ?", v.UserId).UpdateColumn("img_calls", gorm.Expr("img_calls + ?", 1)) continue
} }
tx := p.db.Model(&model.User{}).Where("id = ?", job.UserId).UpdateColumn("power", gorm.Expr("power + ?", job.Power))
if tx.Error == nil && tx.RowsAffected > 0 {
var user model.User
p.db.Where("id = ?", job.UserId).First(&user)
p.db.Create(&model.PowerLog{
UserId: user.Id,
Username: user.Username,
Type: types.PowerConsume,
Amount: job.Power,
Balance: user.Power + job.Power,
Mark: types.PowerAdd,
Model: "mid-journey",
Remark: fmt.Sprintf("绘画任务失败退回算力。任务ID%s", job.TaskId),
CreatedAt: time.Now(),
})
}
}
if !strings.HasPrefix(job.ChannelId, "mj-service-plus") {
continue continue
} }
if !strings.HasPrefix(v.ChannelId, "mj-service-plus") { if servicePlus := p.getServicePlus(job.ChannelId); servicePlus != nil {
continue _ = servicePlus.Notify(job)
}
if servicePlus := p.getServicePlus(v.ChannelId); servicePlus != nil {
_ = servicePlus.Notify(v)
} }
} }

View File

@ -84,7 +84,7 @@ func (s *Service) Run() {
if err != nil { if err != nil {
logger.Error("绘画任务执行失败:", err.Error()) logger.Error("绘画任务执行失败:", err.Error())
// update the task progress // update the task progress
s.db.Model(&model.MidJourneyJob{Id: uint(task.Id)}).UpdateColumns(map[string]interface{}{ s.db.Model(&model.MidJourneyJob{Id: task.Id}).UpdateColumns(map[string]interface{}{
"progress": -1, "progress": -1,
"err_msg": err.Error(), "err_msg": err.Error(),
}) })

View File

@ -4,7 +4,9 @@ import (
"chatplus/core/types" "chatplus/core/types"
"chatplus/service/oss" "chatplus/service/oss"
"chatplus/store" "chatplus/store"
"chatplus/store/model"
"fmt" "fmt"
"time"
"github.com/go-redis/redis/v8" "github.com/go-redis/redis/v8"
"gorm.io/gorm" "gorm.io/gorm"
@ -14,6 +16,7 @@ type ServicePool struct {
services []*Service services []*Service
taskQueue *store.RedisQueue taskQueue *store.RedisQueue
notifyQueue *store.RedisQueue notifyQueue *store.RedisQueue
db *gorm.DB
Clients *types.LMap[uint, *types.WsClient] // UserId => Client Clients *types.LMap[uint, *types.WsClient] // UserId => Client
} }
@ -42,6 +45,7 @@ func NewServicePool(db *gorm.DB, redisCli *redis.Client, manager *oss.UploaderMa
taskQueue: taskQueue, taskQueue: taskQueue,
notifyQueue: notifyQueue, notifyQueue: notifyQueue,
services: services, services: services,
db: db,
Clients: types.NewLMap[uint, *types.WsClient](), Clients: types.NewLMap[uint, *types.WsClient](),
} }
} }
@ -72,6 +76,46 @@ func (p *ServicePool) CheckTaskNotify() {
}() }()
} }
// CheckTaskStatus 检查任务状态,自动删除过期或者失败的任务
func (p *ServicePool) CheckTaskStatus() {
go func() {
for {
var jobs []model.SdJob
res := p.db.Where("progress < ?", 100).Find(&jobs)
if res.Error != nil {
time.Sleep(5 * time.Second)
continue
}
for _, job := range jobs {
// 5 分钟还没完成的任务直接删除
if time.Now().Sub(job.CreatedAt) > time.Minute*5 || job.Progress == -1 {
p.db.Delete(&job)
var user model.User
p.db.Where("id = ?", job.UserId).First(&user)
// 退回绘图次数
res = p.db.Model(&model.User{}).Where("id = ?", job.UserId).UpdateColumn("power", gorm.Expr("power + ?", job.Power))
if res.Error == nil && res.RowsAffected > 0 {
p.db.Create(&model.PowerLog{
UserId: user.Id,
Username: user.Username,
Type: types.PowerConsume,
Amount: job.Power,
Balance: user.Power + job.Power,
Mark: types.PowerAdd,
Model: "stable-diffusion",
Remark: fmt.Sprintf("任务失败退回算力。任务ID%s", job.TaskId),
CreatedAt: time.Now(),
})
}
continue
}
}
}
}()
}
// HasAvailableService check if it has available mj service in pool // HasAvailableService check if it has available mj service in pool
func (p *ServicePool) HasAvailableService() bool { func (p *ServicePool) HasAvailableService() bool {
return len(p.services) > 0 return len(p.services) > 0

View File

@ -74,8 +74,6 @@ func (s *Service) Run() {
"progress": -1, "progress": -1,
"err_msg": err.Error(), "err_msg": err.Error(),
}) })
// restore img_call quota
s.db.Model(&model.User{}).Where("id = ?", task.UserId).UpdateColumn("img_calls", gorm.Expr("img_calls + ?", 1))
// release task num // release task num
atomic.AddInt32(&s.handledTaskNum, -1) atomic.AddInt32(&s.handledTaskNum, -1)
// 通知前端,任务失败 // 通知前端,任务失败
@ -307,8 +305,6 @@ func (s *Service) callback(data CBReq) {
"progress": -1, "progress": -1,
"err_msg": data.Message, "err_msg": data.Message,
}) })
// restore img_calls
s.db.Model(&model.User{}).Where("id = ? AND img_calls > 0", data.UserId).UpdateColumn("img_calls", gorm.Expr("img_calls + ?", 1))
} }
// 发送更新状态信号 // 发送更新状态信号

View File

@ -39,7 +39,7 @@ func NewXXLJobExecutor(config *types.AppConfig, db *gorm.DB) *XXLJobExecutor {
func (e *XXLJobExecutor) Run() error { func (e *XXLJobExecutor) Run() error {
e.executor.RegTask("ClearOrders", e.ClearOrders) e.executor.RegTask("ClearOrders", e.ClearOrders)
e.executor.RegTask("ResetVipCalls", e.ResetVipCalls) e.executor.RegTask("ResetVipPower", e.ResetVipPower)
return e.executor.Run() return e.executor.Run()
} }
@ -68,8 +68,8 @@ func (e *XXLJobExecutor) ClearOrders(cxt context.Context, param *xxl.RunReq) (ms
return fmt.Sprintf("Clear order successfully, affect rows: %d", res.RowsAffected) return fmt.Sprintf("Clear order successfully, affect rows: %d", res.RowsAffected)
} }
// ResetVipCalls 清理过期的 VIP 会员 // ResetVipPower 清理过期的 VIP 会员
func (e *XXLJobExecutor) ResetVipCalls(cxt context.Context, param *xxl.RunReq) (msg string) { func (e *XXLJobExecutor) ResetVipPower(cxt context.Context, param *xxl.RunReq) (msg string) {
logger.Info("开始进行月底账号盘点...") logger.Info("开始进行月底账号盘点...")
var users []model.User var users []model.User
res := e.db.Where("vip = ?", 1).Find(&users) res := e.db.Where("vip = ?", 1).Find(&users)
@ -89,55 +89,27 @@ func (e *XXLJobExecutor) ResetVipCalls(cxt context.Context, param *xxl.RunReq) (
return "error with decode system config: " + err.Error() return "error with decode system config: " + err.Error()
} }
// 获取本月月初时间
currentTime := time.Now()
year, month, _ := currentTime.Date()
firstOfMonth := time.Date(year, month, 1, 0, 0, 0, 0, currentTime.Location()).Unix()
for _, u := range users { for _, u := range users {
// 账号到期,直接清零 if u.Power <= 0 {
if u.ExpiredTime <= currentTime.Unix() { u.Power = 0
logger.Info("账号过期:", u.Username)
u.Calls = 0
u.Vip = false
} else {
if u.Calls <= 0 {
u.Calls = 0
}
if u.ImgCalls <= 0 {
u.ImgCalls = 0
}
// 如果该用户当月有充值点卡,则将点卡中未用完的点数结余到下个月
var orders []model.Order
e.db.Debug().Where("user_id = ? AND pay_time > ?", u.Id, firstOfMonth).Find(&orders)
var calls = 0
var imgCalls = 0
for _, o := range orders {
var remark types.OrderRemark
err = utils.JsonDecode(o.Remark, &remark)
if err != nil {
continue
}
if remark.Days > 0 { // 会员续费
continue
}
calls += remark.Calls
imgCalls += remark.ImgCalls
}
if u.Calls > calls { // 本月套餐没有用完
u.Calls = calls + config.VipMonthCalls
} else {
u.Calls = u.Calls + config.VipMonthCalls
}
if u.ImgCalls > imgCalls { // 本月套餐没有用完
u.ImgCalls = imgCalls + config.VipMonthImgCalls
} else {
u.ImgCalls = u.ImgCalls + config.VipMonthImgCalls
}
logger.Infof("%s 点卡结余:%d", u.Username, calls)
} }
u.Tokens = 0 u.Power += config.VipMonthPower
// update user // update user
e.db.Updates(&u) tx := e.db.Updates(&u)
// 记录算力充值日志
if tx.Error == nil {
e.db.Create(&model.PowerLog{
UserId: u.Id,
Username: u.Username,
Type: types.PowerRecharge,
Amount: config.VipMonthPower,
Mark: types.PowerAdd,
Balance: u.Power + config.VipMonthPower,
Model: "",
Remark: fmt.Sprintf("月底盘点,会员每月赠送算力:%d", config.VipMonthPower),
CreatedAt: time.Now(),
})
}
} }
logger.Info("月底盘点完成!") logger.Info("月底盘点完成!")
return "success" return "success"

View File

@ -7,6 +7,6 @@ type ChatModel struct {
Value string // API Key 的值 Value string // API Key 的值
SortNum int SortNum int
Enabled bool Enabled bool
Weight int // 对话权重,每次对话扣减多少次对话额度 Power int // 每次对话消耗算力
Open bool // 是否开放模型给所有人使用 Open bool // 是否开放模型给所有人使用
} }

View File

@ -18,6 +18,7 @@ type MidJourneyJob struct {
UseProxy bool // 是否使用反代加载图片 UseProxy bool // 是否使用反代加载图片
Publish bool //是否发布图片到画廊 Publish bool //是否发布图片到画廊
ErrMsg string // 报错信息 ErrMsg string // 报错信息
Power int // 消耗算力
CreatedAt time.Time CreatedAt time.Time
} }

View File

@ -0,0 +1,20 @@
package model
import (
"chatplus/core/types"
"time"
)
// PowerLog 算力消费日志
type PowerLog struct {
Id uint `gorm:"primarykey;column:id"`
UserId uint
Username string
Type types.PowerType
Amount int
Balance int
Model string // 模型
Remark string // 备注
Mark types.PowerMark // 资金类型
CreatedAt time.Time
}

View File

@ -7,8 +7,7 @@ type Product struct {
Price float64 Price float64
Discount float64 Discount float64
Days int Days int
Calls int Power int
ImgCalls int
Enabled bool Enabled bool
Sales int Sales int
SortNum int SortNum int

View File

@ -13,6 +13,7 @@ type SdJob struct {
Params string Params string
Publish bool //是否发布图片到画廊 Publish bool //是否发布图片到画廊
ErrMsg string // 报错信息 ErrMsg string // 报错信息
Power int // 消耗算力
CreatedAt time.Time CreatedAt time.Time
} }

View File

@ -7,9 +7,7 @@ type User struct {
Password string Password string
Avatar string Avatar string
Salt string // 密码盐 Salt string // 密码盐
TotalTokens int64 // 总消耗 tokens Power int // 剩余算力
Calls int // 剩余对话次数
ImgCalls int // 剩余绘图次数
ChatConfig string `gorm:"column:chat_config_json"` // 聊天配置 json ChatConfig string `gorm:"column:chat_config_json"` // 聊天配置 json
ChatRoles string `gorm:"column:chat_roles_json"` // 聊天角色 ChatRoles string `gorm:"column:chat_roles_json"` // 聊天角色
ChatModels string `gorm:"column:chat_models_json"` // AI 模型,不同的用户拥有不同的聊天模型 ChatModels string `gorm:"column:chat_models_json"` // AI 模型,不同的用户拥有不同的聊天模型
@ -18,5 +16,4 @@ type User struct {
LastLoginAt int64 // 最后登录时间 LastLoginAt int64 // 最后登录时间
LastLoginIp string // 最后登录 IP LastLoginIp string // 最后登录 IP
Vip bool // 是否 VIP 会员 Vip bool // 是否 VIP 会员
Tokens int
} }

View File

@ -18,5 +18,6 @@ type MidJourneyJob struct {
UseProxy bool `json:"use_proxy"` UseProxy bool `json:"use_proxy"`
Publish bool `json:"publish"` Publish bool `json:"publish"`
ErrMsg string `json:"err_msg"` ErrMsg string `json:"err_msg"`
Power int `json:"power"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
} }

16
api/store/vo/power_log.go Normal file
View File

@ -0,0 +1,16 @@
package vo
import "chatplus/core/types"
type PowerLog struct {
Id uint `json:"id"`
UserId uint `json:"user_id"`
Username string `json:"username"`
Type types.PowerType `json:"name"`
Amount int `json:"amount"`
Mark types.PowerMark `json:"fund_type"`
Balance int `json:"balance"`
Model string `json:"model"`
Remark string `json:"remark"`
CreatedAt int64 `json:"created_at"`
}

View File

@ -6,8 +6,7 @@ type Product struct {
Price float64 `json:"price"` Price float64 `json:"price"`
Discount float64 `json:"discount"` Discount float64 `json:"discount"`
Days int `json:"days"` Days int `json:"days"`
Calls int `json:"calls"` Power int `json:"power"`
ImgCalls int `json:"img_calls"`
Enabled bool `json:"enabled"` Enabled bool `json:"enabled"`
Sales int `json:"sales"` Sales int `json:"sales"`
SortNum int `json:"sort_num"` SortNum int `json:"sort_num"`

View File

@ -12,6 +12,5 @@ type Reward struct {
} }
type RewardExchange struct { type RewardExchange struct {
Calls int `json:"calls"` Power int `json:"calls"`
ImgCalls int `json:"img_calls"`
} }

View File

@ -16,5 +16,6 @@ type SdJob struct {
Prompt string `json:"prompt"` Prompt string `json:"prompt"`
Publish bool `json:"publish"` Publish bool `json:"publish"`
ErrMsg string `json:"err_msg"` ErrMsg string `json:"err_msg"`
Power int `json:"power"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
} }

View File

@ -7,10 +7,8 @@ type User struct {
Username string `json:"username"` Username string `json:"username"`
Nickname string `json:"nickname"` Nickname string `json:"nickname"`
Avatar string `json:"avatar"` Avatar string `json:"avatar"`
Salt string `json:"salt"` // 密码盐 Salt string `json:"salt"` // 密码盐
TotalTokens int64 `json:"total_tokens"` // 总消耗tokens Power int `json:"calls"` // 剩余算力
Calls int `json:"calls"` // 剩余对话次数
ImgCalls int `json:"img_calls"`
ChatConfig types.UserChatConfig `json:"chat_config"` // 聊天配置 ChatConfig types.UserChatConfig `json:"chat_config"` // 聊天配置
ChatRoles []string `json:"chat_roles"` // 聊天角色集合 ChatRoles []string `json:"chat_roles"` // 聊天角色集合
ChatModels []string `json:"chat_models"` // AI模型集合 ChatModels []string `json:"chat_models"` // AI模型集合
@ -19,5 +17,4 @@ type User struct {
LastLoginAt int64 `json:"last_login_at"` // 最后登录时间 LastLoginAt int64 `json:"last_login_at"` // 最后登录时间
LastLoginIp string `json:"last_login_ip"` // 最后登录 IP LastLoginIp string `json:"last_login_ip"` // 最后登录 IP
Vip bool `json:"vip"` Vip bool `json:"vip"`
Tokens int `json:"token"` // 当月消耗的 fee
} }

View File

@ -1,23 +0,0 @@
-- 删除用户名重复的用户,只保留一条
DELETE FROM chatgpt_users
WHERE username IN (
SELECT username
FROM (
SELECT username
FROM chatgpt_users
GROUP BY username
HAVING COUNT(*) > 1
) AS temp
) AND id NOT IN (
SELECT MIN(id)
FROM (
SELECT id, username
FROM chatgpt_users
GROUP BY id, username
HAVING COUNT(*) > 1
) AS temp
GROUP BY username
);
-- 给 username 字段建立唯一索引
ALTER TABLE `chatgpt_users` ADD UNIQUE(`username`)

View File

@ -0,0 +1,44 @@
-- 删除用户名重复的用户,只保留一条
DELETE FROM chatgpt_users
WHERE username IN (
SELECT username
FROM (
SELECT username
FROM chatgpt_users
GROUP BY username
HAVING COUNT(*) > 1
) AS temp
) AND id NOT IN (
SELECT MIN(id)
FROM (
SELECT id, username
FROM chatgpt_users
GROUP BY id, username
HAVING COUNT(*) > 1
) AS temp
GROUP BY username
);
-- 给 username 字段建立唯一索引
ALTER TABLE `chatgpt_users` ADD UNIQUE(`username`)
-- 当前用户剩余算力
ALTER TABLE `chatgpt_users` CHANGE `calls` `power` INT NOT NULL DEFAULT '0' COMMENT '剩余算力';
ALTER TABLE `chatgpt_users`
DROP `total_tokens`,
DROP `tokens`,
DROP `img_calls`;
ALTER TABLE `chatgpt_chat_models` CHANGE `weight` `power` TINYINT NOT NULL COMMENT '消耗算力点数';
ALTER TABLE `chatgpt_chat_models` ADD `temperature` FLOAT(3,2) NOT NULL DEFAULT '1' COMMENT '模型创意度' AFTER `power`, ADD `max_tokens` INT(11) NOT NULL DEFAULT '1024' COMMENT '最大响应长度' AFTER `temperature`, ADD `max_context` INT(11) NOT NULL DEFAULT '4096' COMMENT '最大上下文长度' AFTER `max_tokens`;
CREATE TABLE `chatgpt_plus`.`chatgpt_power_logs` ( `id` INT(11) NOT NULL AUTO_INCREMENT , `user_id` INT(11) NOT NULL COMMENT '用户ID' , `username` VARCHAR(30) NOT NULL COMMENT '用户名' , `type` TINYINT(1) NOT NULL COMMENT '类型1充值2消费3退费' , `amount` SMALLINT(3) NOT NULL COMMENT '算力花费' , `balance` INT(11) NOT NULL COMMENT '余额' , `model` VARCHAR(30) NOT NULL COMMENT '模型' , `remark` VARCHAR(255) NOT NULL COMMENT '备注' , `created_at` DATETIME NOT NULL COMMENT '创建时间' , PRIMARY KEY (`id`)) ENGINE = InnoDB COMMENT = '用户算力消费日志';
ALTER TABLE `chatgpt_products` CHANGE `calls` `power` INT(11) NOT NULL DEFAULT '0' COMMENT '增加算力值';
ALTER TABLE `chatgpt_products` DROP `img_calls`;
ALTER TABLE `chatgpt_power_logs` CHANGE `amount` `amount` SMALLINT NOT NULL COMMENT '算力数值';
ALTER TABLE `chatgpt_power_logs` ADD `mark` TINYINT(1) NOT NULL COMMENT '资金类型0支出1收入' AFTER `remark`;
ALTER TABLE `chatgpt_mj_jobs` ADD `power` SMALLINT(5) NOT NULL DEFAULT '0' COMMENT '消耗算力' AFTER `err_msg`;
ALTER TABLE `chatgpt_sd_jobs` ADD `power` SMALLINT(5) NOT NULL DEFAULT '0' COMMENT '消耗算力' AFTER `err_msg`;

2
gpt-vue/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
node_modules
dist

View File

@ -9,7 +9,7 @@
<div class="content" v-html="content"></div> <div class="content" v-html="content"></div>
<div class="bar" v-if="createdAt !== ''"> <div class="bar" v-if="createdAt !== ''">
<span class="bar-item"><el-icon><Clock/></el-icon> {{ createdAt }}</span> <span class="bar-item"><el-icon><Clock/></el-icon> {{ createdAt }}</span>
<span class="bar-item">算力消耗: {{ finalTokens }}</span> <span class="bar-item">Tokens: {{ finalTokens }}</span>
</div> </div>
</div> </div>
</div> </div>

View File

@ -9,12 +9,12 @@
<div class="content" v-html="content"></div> <div class="content" v-html="content"></div>
<div class="bar" v-if="createdAt !== ''"> <div class="bar" v-if="createdAt !== ''">
<span class="bar-item"><el-icon><Clock/></el-icon> {{ createdAt }}</span> <span class="bar-item"><el-icon><Clock/></el-icon> {{ createdAt }}</span>
<span class="bar-item">算力消耗: {{ tokens }}</span> <span class="bar-item">Tokens: {{ tokens }}</span>
<el-tooltip <el-tooltip
class="box-item" class="box-item"
effect="light" effect="dark"
content="复制回答" content="复制回答"
placement="top" placement="bottom"
> >
<el-button type="info" class="copy-reply" :data-clipboard-text="orgContent"> <el-button type="info" class="copy-reply" :data-clipboard-text="orgContent">
<el-icon> <el-icon>
@ -24,7 +24,6 @@
</el-tooltip> </el-tooltip>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</template> </template>

View File

@ -438,7 +438,7 @@
<template #default> <template #default>
<div class="mj-list-item-prompt"> <div class="mj-list-item-prompt">
<span>{{ scope.item.prompt }}</span> <span>{{ scope.item.prompt }}</span>
<el-icon class="copy-prompt" <el-icon class="copy-prompt-mj"
:data-clipboard-text="scope.item.prompt"> :data-clipboard-text="scope.item.prompt">
<DocumentCopy/> <DocumentCopy/>
</el-icon> </el-icon>
@ -485,7 +485,7 @@
</template> </template>
<script setup> <script setup>
import {nextTick, onMounted, ref} from "vue" import {nextTick, onMounted, onUnmounted, ref} from "vue"
import { import {
ChromeFilled, ChromeFilled,
Delete, Delete,
@ -662,6 +662,7 @@ const connect = () => {
}); });
} }
const clipboard = ref(null)
onMounted(() => { onMounted(() => {
checkSession().then(user => { checkSession().then(user => {
imgCalls.value = user['img_calls'] imgCalls.value = user['img_calls']
@ -675,16 +676,20 @@ onMounted(() => {
router.push('/login') router.push('/login')
}); });
const clipboard = new Clipboard('.copy-prompt'); clipboard.value = new Clipboard('.copy-prompt-mj');
clipboard.on('success', () => { clipboard.value.on('success', () => {
ElMessage.success("复制成功!"); ElMessage.success("复制成功!");
}) })
clipboard.on('error', () => { clipboard.value.on('error', () => {
ElMessage.error('复制失败!'); ElMessage.error('复制失败!');
}) })
}) })
onUnmounted(() => {
clipboard.value.destroy()
})
// //
const fetchRunningJobs = () => { const fetchRunningJobs = () => {
httpGet(`/api/mj/jobs?status=0`).then(res => { httpGet(`/api/mj/jobs?status=0`).then(res => {
@ -745,10 +750,14 @@ const fetchFinishJobs = (page) => {
jobs[i]['can_opt'] = true jobs[i]['can_opt'] = true
} }
} }
finishedJobs.value = finishedJobs.value.concat(jobs)
if (jobs.length < pageSize.value) { if (jobs.length < pageSize.value) {
isOver.value = true isOver.value = true
} }
if (page === 1) {
finishedJobs.value = jobs
} else {
finishedJobs.value = finishedJobs.value.concat(jobs)
}
nextTick(() => loading.value = false) nextTick(() => loading.value = false)
}).catch(e => { }).catch(e => {
loading.value = false loading.value = false

View File

@ -411,7 +411,7 @@
</el-divider> </el-divider>
<div class="prompt"> <div class="prompt">
<span>{{ item.prompt }}</span> <span>{{ item.prompt }}</span>
<el-icon class="copy-prompt" :data-clipboard-text="item.prompt"> <el-icon class="copy-prompt-sd" :data-clipboard-text="item.prompt">
<DocumentCopy/> <DocumentCopy/>
</el-icon> </el-icon>
</div> </div>
@ -424,7 +424,7 @@
</el-divider> </el-divider>
<div class="prompt"> <div class="prompt">
<span>{{ item.params.negative_prompt }}</span> <span>{{ item.params.negative_prompt }}</span>
<el-icon class="copy-prompt" :data-clipboard-text="item.params.negative_prompt"> <el-icon class="copy-prompt-sd" :data-clipboard-text="item.params.negative_prompt">
<DocumentCopy/> <DocumentCopy/>
</el-icon> </el-icon>
</div> </div>
@ -511,7 +511,7 @@
</template> </template>
<script setup> <script setup>
import {onMounted, ref} from "vue" import {onMounted, onUnmounted, ref} from "vue"
import {Delete, DocumentCopy, InfoFilled, Orange, Picture, Refresh} from "@element-plus/icons-vue"; import {Delete, DocumentCopy, InfoFilled, Orange, Picture, Refresh} from "@element-plus/icons-vue";
import {httpGet, httpPost} from "@/utils/http"; import {httpGet, httpPost} from "@/utils/http";
import {ElMessage, ElMessageBox, ElNotification} from "element-plus"; import {ElMessage, ElMessageBox, ElNotification} from "element-plus";
@ -629,6 +629,7 @@ const connect = () => {
}); });
} }
const clipboard = ref(null)
onMounted(() => { onMounted(() => {
checkSession().then(user => { checkSession().then(user => {
imgCalls.value = user['img_calls'] imgCalls.value = user['img_calls']
@ -639,16 +640,20 @@ onMounted(() => {
}).catch(() => { }).catch(() => {
router.push('/login') router.push('/login')
}); });
const clipboard = new Clipboard('.copy-prompt'); clipboard.value = new Clipboard('.copy-prompt-sd');
clipboard.on('success', () => { clipboard.value.on('success', () => {
ElMessage.success("复制成功!"); ElMessage.success("复制成功!");
}) })
clipboard.on('error', () => { clipboard.value.on('error', () => {
ElMessage.error('复制失败!'); ElMessage.error('复制失败!');
}) })
}) })
onUnmounted(() => {
clipboard.value.destroy()
})
const fetchRunningJobs = (userId) => { const fetchRunningJobs = (userId) => {
// //
httpGet(`/api/sd/jobs?status=0&user_id=${userId}`).then(res => { httpGet(`/api/sd/jobs?status=0&user_id=${userId}`).then(res => {
@ -694,7 +699,11 @@ const fetchFinishJobs = (page) => {
if (res.data.length < pageSize.value) { if (res.data.length < pageSize.value) {
isOver.value = true isOver.value = true
} }
finishedJobs.value = finishedJobs.value.concat(res.data) if (page === 1) {
finishedJobs.value = res.data
} else {
finishedJobs.value = finishedJobs.value.concat(res.data)
}
loading.value = false loading.value = false
}).catch(e => { }).catch(e => {
loading.value = false loading.value = false

View File

@ -53,7 +53,7 @@
content="复制提示词" content="复制提示词"
placement="top" placement="top"
> >
<el-icon class="copy-prompt" :data-clipboard-text="slotProp.item.prompt"> <el-icon class="copy-prompt-wall" :data-clipboard-text="slotProp.item.prompt">
<DocumentCopy/> <DocumentCopy/>
</el-icon> </el-icon>
</el-tooltip> </el-tooltip>
@ -143,7 +143,7 @@
</el-divider> </el-divider>
<div class="prompt"> <div class="prompt">
<span>{{ item.prompt }}</span> <span>{{ item.prompt }}</span>
<el-icon class="copy-prompt" :data-clipboard-text="item.prompt"> <el-icon class="copy-prompt-wall" :data-clipboard-text="item.prompt">
<DocumentCopy/> <DocumentCopy/>
</el-icon> </el-icon>
</div> </div>
@ -156,7 +156,7 @@
</el-divider> </el-divider>
<div class="prompt"> <div class="prompt">
<span>{{ item.params.negative_prompt }}</span> <span>{{ item.params.negative_prompt }}</span>
<el-icon class="copy-prompt" :data-clipboard-text="item.params.negative_prompt"> <el-icon class="copy-prompt-wall" :data-clipboard-text="item.params.negative_prompt">
<DocumentCopy/> <DocumentCopy/>
</el-icon> </el-icon>
</div> </div>
@ -243,7 +243,7 @@
</template> </template>
<script setup> <script setup>
import {nextTick, onMounted, ref} from "vue" import {nextTick, onMounted, onUnmounted, ref} from "vue"
import {DocumentCopy, Picture} from "@element-plus/icons-vue"; import {DocumentCopy, Picture} from "@element-plus/icons-vue";
import {httpGet} from "@/utils/http"; import {httpGet} from "@/utils/http";
import {ElMessage} from "element-plus"; import {ElMessage} from "element-plus";
@ -314,17 +314,22 @@ const getNext = () => {
getNext() getNext()
const clipboard = ref(null)
onMounted(() => { onMounted(() => {
const clipboard = new Clipboard('.copy-prompt'); clipboard.value = new Clipboard('.copy-prompt-wall');
clipboard.on('success', () => { clipboard.value.on('success', () => {
ElMessage.success("复制成功!"); ElMessage.success("复制成功!");
}) })
clipboard.on('error', () => { clipboard.value.on('error', () => {
ElMessage.error('复制失败!'); ElMessage.error('复制失败!');
}) })
}) })
onUnmounted(() => {
clipboard.value.destroy()
})
const changeImgType = () => { const changeImgType = () => {
document.getElementById('waterfall-box').scrollTo(0, 0) document.getElementById('waterfall-box').scrollTo(0, 0)
page.value = 0 page.value = 0