diff --git a/controller/user.go b/controller/user.go
index 8fd10b8..f04f9ca 100644
--- a/controller/user.go
+++ b/controller/user.go
@@ -79,6 +79,7 @@ func setupLogin(user *model.User, c *gin.Context) {
DisplayName: user.DisplayName,
Role: user.Role,
Status: user.Status,
+ Group: user.Group,
}
c.JSON(http.StatusOK, gin.H{
"message": "",
@@ -284,6 +285,42 @@ func GenerateAccessToken(c *gin.Context) {
return
}
+type TransferAffQuotaRequest struct {
+ Quota int `json:"quota" binding:"required"`
+}
+
+func TransferAffQuota(c *gin.Context) {
+ id := c.GetInt("id")
+ user, err := model.GetUserById(id, true)
+ if err != nil {
+ c.JSON(http.StatusOK, gin.H{
+ "success": false,
+ "message": err.Error(),
+ })
+ return
+ }
+ tran := TransferAffQuotaRequest{}
+ if err := c.ShouldBindJSON(&tran); err != nil {
+ c.JSON(http.StatusOK, gin.H{
+ "success": false,
+ "message": err.Error(),
+ })
+ return
+ }
+ err = user.TransferAffQuotaToQuota(tran.Quota)
+ if err != nil {
+ c.JSON(http.StatusOK, gin.H{
+ "success": false,
+ "message": "划转失败 " + err.Error(),
+ })
+ return
+ }
+ c.JSON(http.StatusOK, gin.H{
+ "success": true,
+ "message": "划转成功",
+ })
+}
+
func GetAffCode(c *gin.Context) {
id := c.GetInt("id")
user, err := model.GetUserById(id, true)
@@ -330,6 +367,28 @@ func GetSelf(c *gin.Context) {
return
}
+func GetUserModels(c *gin.Context) {
+ id, err := strconv.Atoi(c.Param("id"))
+ if err != nil {
+ id = c.GetInt("id")
+ }
+ user, err := model.GetUserById(id, true)
+ if err != nil {
+ c.JSON(http.StatusOK, gin.H{
+ "success": false,
+ "message": err.Error(),
+ })
+ return
+ }
+ models := model.GetGroupModels(user.Group)
+ c.JSON(http.StatusOK, gin.H{
+ "success": true,
+ "message": "",
+ "data": models,
+ })
+ return
+}
+
func UpdateUser(c *gin.Context) {
var updatedUser model.User
err := json.NewDecoder(c.Request.Body).Decode(&updatedUser)
diff --git a/model/ability.go b/model/ability.go
index 3da83be..b57ae19 100644
--- a/model/ability.go
+++ b/model/ability.go
@@ -13,6 +13,16 @@ type Ability struct {
Priority *int64 `json:"priority" gorm:"bigint;default:0;index"`
}
+func GetGroupModels(group string) []string {
+ var abilities []Ability
+ DB.Where("`group` = ?", group).Find(&abilities)
+ models := make([]string, 0, len(abilities))
+ for _, ability := range abilities {
+ models = append(models, ability.Model)
+ }
+ return models
+}
+
func GetRandomSatisfiedChannel(group string, model string) (*Channel, error) {
ability := Ability{}
groupCol := "`group`"
diff --git a/model/token.go b/model/token.go
index ee6b4a9..66b8369 100644
--- a/model/token.go
+++ b/model/token.go
@@ -220,28 +220,30 @@ func PostConsumeTokenQuota(tokenId int, userQuota int, quota int, preConsumedQuo
}
if sendEmail {
- quotaTooLow := userQuota >= common.QuotaRemindThreshold && userQuota-(quota+preConsumedQuota) < common.QuotaRemindThreshold
- noMoreQuota := userQuota-(quota+preConsumedQuota) <= 0
- if quotaTooLow || noMoreQuota {
- go func() {
- email, err := GetUserEmail(token.UserId)
- if err != nil {
- common.SysError("failed to fetch user email: " + err.Error())
- }
- prompt := "您的额度即将用尽"
- if noMoreQuota {
- prompt = "您的额度已用尽"
- }
- if email != "" {
- topUpLink := fmt.Sprintf("%s/topup", common.ServerAddress)
- err = common.SendEmail(prompt, email,
- fmt.Sprintf("%s,当前剩余额度为 %d,为了不影响您的使用,请及时充值。
充值链接:%s", prompt, userQuota, topUpLink, topUpLink))
+ if (quota + preConsumedQuota) != 0 {
+ quotaTooLow := userQuota >= common.QuotaRemindThreshold && userQuota-(quota+preConsumedQuota) < common.QuotaRemindThreshold
+ noMoreQuota := userQuota-(quota+preConsumedQuota) <= 0
+ if quotaTooLow || noMoreQuota {
+ go func() {
+ email, err := GetUserEmail(token.UserId)
if err != nil {
- common.SysError("failed to send email" + err.Error())
+ common.SysError("failed to fetch user email: " + err.Error())
}
- common.SysLog("user quota is low, consumed quota: " + strconv.Itoa(quota) + ", user quota: " + strconv.Itoa(userQuota))
- }
- }()
+ prompt := "您的额度即将用尽"
+ if noMoreQuota {
+ prompt = "您的额度已用尽"
+ }
+ if email != "" {
+ topUpLink := fmt.Sprintf("%s/topup", common.ServerAddress)
+ err = common.SendEmail(prompt, email,
+ fmt.Sprintf("%s,当前剩余额度为 %d,为了不影响您的使用,请及时充值。
充值链接:%s", prompt, userQuota, topUpLink, topUpLink))
+ if err != nil {
+ common.SysError("failed to send email" + err.Error())
+ }
+ common.SysLog("user quota is low, consumed quota: " + strconv.Itoa(quota) + ", user quota: " + strconv.Itoa(userQuota))
+ }
+ }()
+ }
}
}
diff --git a/model/user.go b/model/user.go
index 31eae3b..422d268 100644
--- a/model/user.go
+++ b/model/user.go
@@ -27,6 +27,9 @@ type User struct {
RequestCount int `json:"request_count" gorm:"type:int;default:0;"` // request number
Group string `json:"group" gorm:"type:varchar(32);default:'default'"`
AffCode string `json:"aff_code" gorm:"type:varchar(32);column:aff_code;uniqueIndex"`
+ AffCount int `json:"aff_count" gorm:"type:int;default:0;column:aff_count"`
+ AffQuota int `json:"aff_quota" gorm:"type:int;default:0;column:aff_quota"` // 邀请剩余额度
+ AffHistoryQuota int `json:"aff_history_quota" gorm:"type:int;default:0;column:aff_history"` // 邀请历史额度
InviterId int `json:"inviter_id" gorm:"type:int;column:inviter_id;index"`
}
@@ -77,6 +80,54 @@ func DeleteUserById(id int) (err error) {
return user.Delete()
}
+func inviteUser(inviterId int) (err error) {
+ user, err := GetUserById(inviterId, true)
+ if err != nil {
+ return err
+ }
+ user.AffCount++
+ user.AffQuota += common.QuotaForInviter
+ user.AffHistoryQuota += common.QuotaForInviter
+ return DB.Save(user).Error
+}
+
+func (user *User) TransferAffQuotaToQuota(quota int) error {
+ // 检查quota是否小于最小额度
+ if float64(quota) < common.QuotaPerUnit {
+ return fmt.Errorf("转移额度最小为%s!", common.LogQuota(int(common.QuotaPerUnit)))
+ }
+
+ // 开始数据库事务
+ tx := DB.Begin()
+ if tx.Error != nil {
+ return tx.Error
+ }
+ defer tx.Rollback() // 确保在函数退出时事务能回滚
+
+ // 加锁查询用户以确保数据一致性
+ err := tx.Set("gorm:query_option", "FOR UPDATE").First(&user, user.Id).Error
+ if err != nil {
+ return err
+ }
+
+ // 再次检查用户的AffQuota是否足够
+ if user.AffQuota < quota {
+ return errors.New("邀请额度不足!")
+ }
+
+ // 更新用户额度
+ user.AffQuota -= quota
+ user.Quota += quota
+
+ // 保存用户状态
+ if err := tx.Save(user).Error; err != nil {
+ return err
+ }
+
+ // 提交事务
+ return tx.Commit().Error
+}
+
func (user *User) Insert(inviterId int) error {
var err error
if user.Password != "" {
@@ -101,8 +152,9 @@ func (user *User) Insert(inviterId int) error {
RecordLog(user.Id, LogTypeSystem, fmt.Sprintf("使用邀请码赠送 %s", common.LogQuota(common.QuotaForInvitee)))
}
if common.QuotaForInviter > 0 {
- _ = IncreaseUserQuota(inviterId, common.QuotaForInviter)
+ //_ = IncreaseUserQuota(inviterId, common.QuotaForInviter)
RecordLog(inviterId, LogTypeSystem, fmt.Sprintf("邀请用户赠送 %s", common.LogQuota(common.QuotaForInviter)))
+ _ = inviteUser(inviterId)
}
}
return nil
diff --git a/router/api-router.go b/router/api-router.go
index 17758b0..038b750 100644
--- a/router/api-router.go
+++ b/router/api-router.go
@@ -39,6 +39,7 @@ func SetApiRouter(router *gin.Engine) {
selfRoute.Use(middleware.UserAuth())
{
selfRoute.GET("/self", controller.GetSelf)
+ selfRoute.GET("/models", controller.GetUserModels)
selfRoute.PUT("/self", controller.UpdateSelf)
selfRoute.DELETE("/self", controller.DeleteSelf)
selfRoute.GET("/token", controller.GenerateAccessToken)
@@ -46,6 +47,7 @@ func SetApiRouter(router *gin.Engine) {
selfRoute.POST("/topup", controller.TopUp)
selfRoute.POST("/pay", controller.RequestEpay)
selfRoute.POST("/amount", controller.RequestAmount)
+ selfRoute.POST("/aff_transfer", controller.TransferAffQuota)
}
adminRoute := userRoute.Group("/")
diff --git a/web/src/components/PersonalSetting.js b/web/src/components/PersonalSetting.js
index 6baf1f3..41ff804 100644
--- a/web/src/components/PersonalSetting.js
+++ b/web/src/components/PersonalSetting.js
@@ -1,376 +1,548 @@
-import React, { useContext, useEffect, useState } from 'react';
-import { Button, Divider, Form, Header, Image, Message, Modal } from 'semantic-ui-react';
-import { Link, useNavigate } from 'react-router-dom';
-import { API, copy, showError, showInfo, showNotice, showSuccess } from '../helpers';
+import React, {useContext, useEffect, useState} from 'react';
+import {Form, Image, Message} from 'semantic-ui-react';
+import {Link, useNavigate} from 'react-router-dom';
+import {API, copy, isRoot, showError, showInfo, showNotice, showSuccess} from '../helpers';
import Turnstile from 'react-turnstile';
-import { UserContext } from '../context/User';
-import { onGitHubOAuthClicked } from './utils';
+import {UserContext} from '../context/User';
+import {onGitHubOAuthClicked} from './utils';
+import {
+ Avatar, Banner,
+ Button,
+ Card,
+ Descriptions,
+ Divider,
+ Input, InputNumber,
+ Layout,
+ Modal,
+ Space,
+ Tag,
+ Typography
+} from "@douyinfe/semi-ui";
+import {getQuotaPerUnit, renderQuota, renderQuotaWithPrompt, stringToColor} from "../helpers/render";
+import EditToken from "../pages/Token/EditToken";
+import EditUser from "../pages/User/EditUser";
const PersonalSetting = () => {
- const [userState, userDispatch] = useContext(UserContext);
- let navigate = useNavigate();
+ const [userState, userDispatch] = useContext(UserContext);
+ let navigate = useNavigate();
- const [inputs, setInputs] = useState({
- wechat_verification_code: '',
- email_verification_code: '',
- email: '',
- self_account_deletion_confirmation: ''
- });
- const [status, setStatus] = useState({});
- const [showWeChatBindModal, setShowWeChatBindModal] = useState(false);
- const [showEmailBindModal, setShowEmailBindModal] = useState(false);
- const [showAccountDeleteModal, setShowAccountDeleteModal] = useState(false);
- const [turnstileEnabled, setTurnstileEnabled] = useState(false);
- const [turnstileSiteKey, setTurnstileSiteKey] = useState('');
- const [turnstileToken, setTurnstileToken] = useState('');
- const [loading, setLoading] = useState(false);
- const [disableButton, setDisableButton] = useState(false);
- const [countdown, setCountdown] = useState(30);
- const [affLink, setAffLink] = useState("");
- const [systemToken, setSystemToken] = useState("");
+ const [inputs, setInputs] = useState({
+ wechat_verification_code: '',
+ email_verification_code: '',
+ email: '',
+ self_account_deletion_confirmation: ''
+ });
+ const [status, setStatus] = useState({});
+ const [showWeChatBindModal, setShowWeChatBindModal] = useState(false);
+ const [showEmailBindModal, setShowEmailBindModal] = useState(false);
+ const [showAccountDeleteModal, setShowAccountDeleteModal] = useState(false);
+ const [turnstileEnabled, setTurnstileEnabled] = useState(false);
+ const [turnstileSiteKey, setTurnstileSiteKey] = useState('');
+ const [turnstileToken, setTurnstileToken] = useState('');
+ const [loading, setLoading] = useState(false);
+ const [disableButton, setDisableButton] = useState(false);
+ const [countdown, setCountdown] = useState(30);
+ const [affLink, setAffLink] = useState("");
+ const [systemToken, setSystemToken] = useState("");
+ const [models, setModels] = useState([]);
+ const [openTransfer, setOpenTransfer] = useState(false);
+ const [transferAmount, setTransferAmount] = useState(0);
- useEffect(() => {
- let status = localStorage.getItem('status');
- if (status) {
- status = JSON.parse(status);
- setStatus(status);
- if (status.turnstile_check) {
- setTurnstileEnabled(true);
- setTurnstileSiteKey(status.turnstile_site_key);
- }
- }
- }, []);
+ useEffect(() => {
+ // let user = localStorage.getItem('user');
+ // if (user) {
+ // userDispatch({ type: 'login', payload: user });
+ // }
+ // console.log(localStorage.getItem('user'))
- useEffect(() => {
- let countdownInterval = null;
- if (disableButton && countdown > 0) {
- countdownInterval = setInterval(() => {
- setCountdown(countdown - 1);
- }, 1000);
- } else if (countdown === 0) {
- setDisableButton(false);
- setCountdown(30);
- }
- return () => clearInterval(countdownInterval); // Clean up on unmount
- }, [disableButton, countdown]);
+ let status = localStorage.getItem('status');
+ if (status) {
+ status = JSON.parse(status);
+ setStatus(status);
+ if (status.turnstile_check) {
+ setTurnstileEnabled(true);
+ setTurnstileSiteKey(status.turnstile_site_key);
+ }
+ }
+ getUserData().then(
+ (res) => {
+ console.log(userState)
+ }
+ );
+ loadModels().then();
+ getAffLink().then();
+ setTransferAmount(getQuotaPerUnit())
+ }, []);
- const handleInputChange = (e, { name, value }) => {
- setInputs((inputs) => ({ ...inputs, [name]: value }));
- };
+ useEffect(() => {
+ let countdownInterval = null;
+ if (disableButton && countdown > 0) {
+ countdownInterval = setInterval(() => {
+ setCountdown(countdown - 1);
+ }, 1000);
+ } else if (countdown === 0) {
+ setDisableButton(false);
+ setCountdown(30);
+ }
+ return () => clearInterval(countdownInterval); // Clean up on unmount
+ }, [disableButton, countdown]);
- const generateAccessToken = async () => {
- const res = await API.get('/api/user/token');
- const { success, message, data } = res.data;
- if (success) {
- setSystemToken(data);
- setAffLink("");
- await copy(data);
- showSuccess(`令牌已重置并已复制到剪贴板`);
- } else {
- showError(message);
- }
- };
+ const handleInputChange = (name, value) => {
+ setInputs((inputs) => ({...inputs, [name]: value}));
+ };
- const getAffLink = async () => {
- const res = await API.get('/api/user/aff');
- const { success, message, data } = res.data;
- if (success) {
- let link = `${window.location.origin}/register?aff=${data}`;
- setAffLink(link);
- setSystemToken("");
- await copy(link);
- showSuccess(`邀请链接已复制到剪切板`);
- } else {
- showError(message);
- }
- };
+ const generateAccessToken = async () => {
+ const res = await API.get('/api/user/token');
+ const {success, message, data} = res.data;
+ if (success) {
+ setSystemToken(data);
+ await copy(data);
+ showSuccess(`令牌已重置并已复制到剪贴板`);
+ } else {
+ showError(message);
+ }
+ };
- const handleAffLinkClick = async (e) => {
- e.target.select();
- await copy(e.target.value);
- showSuccess(`邀请链接已复制到剪切板`);
- };
+ const getAffLink = async () => {
+ const res = await API.get('/api/user/aff');
+ const {success, message, data} = res.data;
+ if (success) {
+ let link = `${window.location.origin}/register?aff=${data}`;
+ setAffLink(link);
+ } else {
+ showError(message);
+ }
+ };
- const handleSystemTokenClick = async (e) => {
- e.target.select();
- await copy(e.target.value);
- showSuccess(`系统令牌已复制到剪切板`);
- };
-
- const deleteAccount = async () => {
- if (inputs.self_account_deletion_confirmation !== userState.user.username) {
- showError('请输入你的账户名以确认删除!');
- return;
+ const getUserData = async () => {
+ let res = await API.get(`/api/user/self`);
+ const {success, message, data} = res.data;
+ if (success) {
+ userDispatch({type: 'login', payload: data});
+ } else {
+ showError(message);
+ }
}
- const res = await API.delete('/api/user/self');
- const { success, message } = res.data;
-
- if (success) {
- showSuccess('账户已删除!');
- await API.get('/api/user/logout');
- userDispatch({ type: 'logout' });
- localStorage.removeItem('user');
- navigate('/login');
- } else {
- showError(message);
+ const loadModels = async () => {
+ let res = await API.get(`/api/user/models`);
+ const {success, message, data} = res.data;
+ if (success) {
+ setModels(data);
+ console.log(data)
+ } else {
+ showError(message);
+ }
}
- };
- const bindWeChat = async () => {
- if (inputs.wechat_verification_code === '') return;
- const res = await API.get(
- `/api/oauth/wechat/bind?code=${inputs.wechat_verification_code}`
+ const handleAffLinkClick = async (e) => {
+ e.target.select();
+ await copy(e.target.value);
+ showSuccess(`邀请链接已复制到剪切板`);
+ };
+
+ const handleSystemTokenClick = async (e) => {
+ e.target.select();
+ await copy(e.target.value);
+ showSuccess(`系统令牌已复制到剪切板`);
+ };
+
+ const deleteAccount = async () => {
+ if (inputs.self_account_deletion_confirmation !== userState.user.username) {
+ showError('请输入你的账户名以确认删除!');
+ return;
+ }
+
+ const res = await API.delete('/api/user/self');
+ const {success, message} = res.data;
+
+ if (success) {
+ showSuccess('账户已删除!');
+ await API.get('/api/user/logout');
+ userDispatch({type: 'logout'});
+ localStorage.removeItem('user');
+ navigate('/login');
+ } else {
+ showError(message);
+ }
+ };
+
+ const bindWeChat = async () => {
+ if (inputs.wechat_verification_code === '') return;
+ const res = await API.get(
+ `/api/oauth/wechat/bind?code=${inputs.wechat_verification_code}`
+ );
+ const {success, message} = res.data;
+ if (success) {
+ showSuccess('微信账户绑定成功!');
+ setShowWeChatBindModal(false);
+ } else {
+ showError(message);
+ }
+ };
+
+ const transfer = async () => {
+ if (transferAmount < getQuotaPerUnit()) {
+ showError('划转金额最低为' + renderQuota(getQuotaPerUnit()));
+ return;
+ }
+ const res = await API.post(
+ `/api/user/aff_transfer`,
+ {
+ quota: transferAmount
+ }
+ );
+ const {success, message} = res.data;
+ if (success) {
+ showSuccess(message);
+ setOpenTransfer(false);
+ getUserData().then();
+ } else {
+ showError(message);
+ }
+ };
+
+ const sendVerificationCode = async () => {
+ if (inputs.email === '') {
+ showError('请输入邮箱!');
+ return;
+ }
+ setDisableButton(true);
+ if (turnstileEnabled && turnstileToken === '') {
+ showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!');
+ return;
+ }
+ setLoading(true);
+ const res = await API.get(
+ `/api/verification?email=${inputs.email}&turnstile=${turnstileToken}`
+ );
+ const {success, message} = res.data;
+ if (success) {
+ showSuccess('验证码发送成功,请检查邮箱!');
+ } else {
+ showError(message);
+ }
+ setLoading(false);
+ };
+
+ const bindEmail = async () => {
+ if (inputs.email_verification_code === '') {
+ showError('请输入邮箱验证码!');
+ return;
+ }
+ setLoading(true);
+ const res = await API.get(
+ `/api/oauth/email/bind?email=${inputs.email}&code=${inputs.email_verification_code}`
+ );
+ const {success, message} = res.data;
+ if (success) {
+ showSuccess('邮箱账户绑定成功!');
+ setShowEmailBindModal(false);
+ userState.user.email = inputs.email;
+ } else {
+ showError(message);
+ }
+ setLoading(false);
+ };
+
+ const getUsername = () => {
+ if (userState.user) {
+ return userState.user.username;
+ } else {
+ return 'null';
+ }
+ }
+
+ const handleCancel = () => {
+ setOpenTransfer(false);
+ }
+
+ return (
+
+ 微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效) +
+- 微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效) -
-