Commit 3eb7843b by CaIon

完善个人中心

parent 89c04a2c
......@@ -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)
......
......@@ -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`"
......
......@@ -220,6 +220,7 @@ func PostConsumeTokenQuota(tokenId int, userQuota int, quota int, preConsumedQuo
}
if sendEmail {
if (quota + preConsumedQuota) != 0 {
quotaTooLow := userQuota >= common.QuotaRemindThreshold && userQuota-(quota+preConsumedQuota) < common.QuotaRemindThreshold
noMoreQuota := userQuota-(quota+preConsumedQuota) <= 0
if quotaTooLow || noMoreQuota {
......@@ -244,6 +245,7 @@ func PostConsumeTokenQuota(tokenId int, userQuota int, quota int, preConsumedQuo
}()
}
}
}
if !token.UnlimitedQuota {
if quota > 0 {
......
......@@ -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
......
......@@ -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("/")
......
......@@ -37,6 +37,12 @@ export function renderNumber(num) {
}
}
export function getQuotaPerUnit() {
let quotaPerUnit = localStorage.getItem('quota_per_unit');
quotaPerUnit = parseFloat(quotaPerUnit);
return quotaPerUnit;
}
export function renderQuota(quota, digits = 2) {
let quotaPerUnit = localStorage.getItem('quota_per_unit');
let displayInCurrency = localStorage.getItem('display_in_currency');
......
import React from 'react';
import { Segment, Tab } from 'semantic-ui-react';
import SystemSetting from '../../components/SystemSetting';
import { isRoot } from '../../helpers';
import {isRoot} from '../../helpers';
import OtherSetting from '../../components/OtherSetting';
import PersonalSetting from '../../components/PersonalSetting';
import OperationSetting from '../../components/OperationSetting';
import {Layout, TabPane, Tabs} from "@douyinfe/semi-ui";
const Setting = () => {
let panes = [
{
menuItem: '个人设置',
render: () => (
<Tab.Pane attached={false}>
<PersonalSetting />
</Tab.Pane>
)
tab: '个人设置',
content: <PersonalSetting/>,
itemKey: '1'
}
];
if (isRoot()) {
panes.push({
menuItem: '运营设置',
render: () => (
<Tab.Pane attached={false}>
<OperationSetting />
</Tab.Pane>
)
tab: '运营设置',
content: <OperationSetting/>,
itemKey: '2'
});
panes.push({
menuItem: '系统设置',
render: () => (
<Tab.Pane attached={false}>
<SystemSetting />
</Tab.Pane>
)
tab: '系统设置',
content: <SystemSetting/>,
itemKey: '3'
});
panes.push({
menuItem: '其他设置',
render: () => (
<Tab.Pane attached={false}>
<OtherSetting />
</Tab.Pane>
)
tab: '其他设置',
content: <OtherSetting/>,
itemKey: '4'
});
}
return (
<Segment>
<Tab menu={{ secondary: true, pointing: true }} panes={panes} />
</Segment>
<div>
<Layout>
<Layout.Content>
<Tabs type="line" defaultActiveKey="1">
{panes.map(pane => (
<TabPane itemKey={pane.itemKey} tab={pane.tab}>
{pane.content}
</TabPane>
))}
</Tabs>
</Layout.Content>
</Layout>
</div>
);
};
......
import React, { useEffect, useState } from 'react';
import { Button, Form, Header, Segment } from 'semantic-ui-react';
import { useParams, useNavigate } from 'react-router-dom';
import { API, showError, showSuccess } from '../../helpers';
import {API, isMobile, showError, showSuccess} from '../../helpers';
import { renderQuota, renderQuotaWithPrompt } from '../../helpers/render';
import Title from "@douyinfe/semi-ui/lib/es/typography/title";
import {SideSheet, Space} from "@douyinfe/semi-ui";
const EditUser = () => {
const EditUser = (props) => {
const params = useParams();
const userId = params.id;
const [loading, setLoading] = useState(true);
......@@ -84,8 +86,24 @@ const EditUser = () => {
return (
<>
<Segment loading={loading}>
<Header as='h3'>更新用户信息</Header>
<SideSheet
placement={'left'}
title={<Title level={3}>更新用户信息</Title>}
headerStyle={{borderBottom: '1px solid var(--semi-color-border)'}}
bodyStyle={{borderBottom: '1px solid var(--semi-color-border)'}}
visible={props.visiable}
footer={
<div style={{display: 'flex', justifyContent: 'flex-end'}}>
<Space>
<Button theme='solid' size={'large'} onClick={submit}>提交</Button>
<Button theme='solid' size={'large'} type={'tertiary'} onClick={handleCancel}>取消</Button>
</Space>
</div>
}
closeIcon={null}
onCancel={() => handleCancel()}
width={isMobile() ? '100%' : 600}
>
<Form autoComplete='new-password'>
<Form.Field>
<Form.Input
......@@ -182,7 +200,8 @@ const EditUser = () => {
<Button onClick={handleCancel}>取消</Button>
<Button positive onClick={submit}>提交</Button>
</Form>
</Segment>
</SideSheet>
</>
);
};
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment