Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
phsl
/
new-api
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Members
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Commit
3eb7843b
authored
Nov 21, 2023
by
CaIon
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
完善个人中心
parent
89c04a2c
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
9 changed files
with
283 additions
and
135 deletions
+283
-135
controller/user.go
+59
-0
model/ability.go
+10
-0
model/token.go
+22
-20
model/user.go
+53
-1
router/api-router.go
+2
-0
web/src/components/PersonalSetting.js
+0
-0
web/src/helpers/render.js
+6
-0
web/src/pages/Setting/index.js
+41
-43
web/src/pages/User/EditUser.js
+90
-71
No files found.
controller/user.go
View file @
3eb7843b
...
...
@@ -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
)
...
...
model/ability.go
View file @
3eb7843b
...
...
@@ -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`"
...
...
model/token.go
View file @
3eb7843b
...
...
@@ -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,为了不影响您的使用,请及时充值。<br/>充值链接:<a href='%s'>%s</a>"
,
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,为了不影响您的使用,请及时充值。<br/>充值链接:<a href='%s'>%s</a>"
,
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
))
}
}()
}
}
}
...
...
model/user.go
View file @
3eb7843b
...
...
@@ -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
...
...
router/api-router.go
View file @
3eb7843b
...
...
@@ -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
(
"/"
)
...
...
web/src/components/PersonalSetting.js
View file @
3eb7843b
This diff is collapsed.
Click to expand it.
web/src/helpers/render.js
View file @
3eb7843b
...
...
@@ -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'
);
...
...
web/src/pages/Setting/index.js
View file @
3eb7843b
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
>
)
}
];
let
panes
=
[
{
tab
:
'个人设置'
,
content
:
<
PersonalSetting
/>
,
itemKey
:
'1'
}
];
if
(
isRoot
())
{
panes
.
push
({
menuItem
:
'运营设置'
,
render
:
()
=>
(
<
Tab
.
Pane
attached
=
{
false
}
>
<
OperationSetting
/>
<
/Tab.Pane
>
)
});
panes
.
push
({
menuItem
:
'系统设置'
,
render
:
()
=>
(
<
Tab
.
Pane
attached
=
{
false
}
>
<
SystemSetting
/>
<
/Tab.Pane
>
)
});
panes
.
push
({
menuItem
:
'其他设置'
,
render
:
()
=>
(
<
Tab
.
Pane
attached
=
{
false
}
>
<
OtherSetting
/>
<
/Tab.Pane
>
)
});
}
if
(
isRoot
())
{
panes
.
push
({
tab
:
'运营设置'
,
content
:
<
OperationSetting
/>
,
itemKey
:
'2'
});
panes
.
push
({
tab
:
'系统设置'
,
content
:
<
SystemSetting
/>
,
itemKey
:
'3'
});
panes
.
push
({
tab
:
'其他设置'
,
content
:
<
OtherSetting
/>
,
itemKey
:
'4'
});
}
return
(
<
Segment
>
<
Tab
menu
=
{{
secondary
:
true
,
pointing
:
true
}}
panes
=
{
panes
}
/
>
<
/Segment
>
);
return
(
<
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
>
);
};
export
default
Setting
;
web/src/pages/User/EditUser.js
View file @
3eb7843b
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,105 +86,122 @@ 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
label
=
'用户名'
name
=
'username'
placeholder
=
{
'请输入新的用户名'
}
onChange
=
{
handleInputChange
}
value
=
{
username
}
autoComplete
=
'new-password'
label
=
'用户名'
name
=
'username'
placeholder
=
{
'请输入新的用户名'
}
onChange
=
{
handleInputChange
}
value
=
{
username
}
autoComplete
=
'new-password'
/>
<
/Form.Field
>
<
Form
.
Field
>
<
Form
.
Input
label
=
'密码'
name
=
'password'
type
=
{
'password'
}
placeholder
=
{
'请输入新的密码,最短 8 位'
}
onChange
=
{
handleInputChange
}
value
=
{
password
}
autoComplete
=
'new-password'
label
=
'密码'
name
=
'password'
type
=
{
'password'
}
placeholder
=
{
'请输入新的密码,最短 8 位'
}
onChange
=
{
handleInputChange
}
value
=
{
password
}
autoComplete
=
'new-password'
/>
<
/Form.Field
>
<
Form
.
Field
>
<
Form
.
Input
label
=
'显示名称'
name
=
'display_name'
placeholder
=
{
'请输入新的显示名称'
}
onChange
=
{
handleInputChange
}
value
=
{
display_name
}
autoComplete
=
'new-password'
label
=
'显示名称'
name
=
'display_name'
placeholder
=
{
'请输入新的显示名称'
}
onChange
=
{
handleInputChange
}
value
=
{
display_name
}
autoComplete
=
'new-password'
/>
<
/Form.Field
>
{
userId
&&
<>
<
Form
.
Field
>
<
Form
.
Dropdown
label
=
'分组'
placeholder
=
{
'请选择分组'
}
name
=
'group'
fluid
search
selection
allowAdditions
additionLabel
=
{
'请在系统设置页面编辑分组倍率以添加新的分组:'
}
onChange
=
{
handleInputChange
}
value
=
{
inputs
.
group
}
autoComplete
=
'new-password'
options
=
{
groupOptions
}
/
>
<
/Form.Field
>
<
Form
.
Field
>
<
Form
.
Input
label
=
{
`剩余额度
${
renderQuotaWithPrompt
(
quota
)}
`
}
name
=
'quota'
placeholder
=
{
'请输入新的剩余额度'
}
onChange
=
{
handleInputChange
}
value
=
{
quota
}
type
=
{
'number'
}
autoComplete
=
'new-password'
/>
<
/Form.Field
>
<
/
>
userId
&&
<>
<
Form
.
Field
>
<
Form
.
Dropdown
label
=
'分组'
placeholder
=
{
'请选择分组'
}
name
=
'group'
fluid
search
selection
allowAdditions
additionLabel
=
{
'请在系统设置页面编辑分组倍率以添加新的分组:'
}
onChange
=
{
handleInputChange
}
value
=
{
inputs
.
group
}
autoComplete
=
'new-password'
options
=
{
groupOptions
}
/
>
<
/Form.Field
>
<
Form
.
Field
>
<
Form
.
Input
label
=
{
`剩余额度
${
renderQuotaWithPrompt
(
quota
)}
`
}
name
=
'quota'
placeholder
=
{
'请输入新的剩余额度'
}
onChange
=
{
handleInputChange
}
value
=
{
quota
}
type
=
{
'number'
}
autoComplete
=
'new-password'
/>
<
/Form.Field
>
<
/
>
}
<
Form
.
Field
>
<
Form
.
Input
label
=
'已绑定的 GitHub 账户'
name
=
'github_id'
value
=
{
github_id
}
autoComplete
=
'new-password'
placeholder
=
'此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改'
readOnly
label
=
'已绑定的 GitHub 账户'
name
=
'github_id'
value
=
{
github_id
}
autoComplete
=
'new-password'
placeholder
=
'此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改'
readOnly
/>
<
/Form.Field
>
<
Form
.
Field
>
<
Form
.
Input
label
=
'已绑定的微信账户'
name
=
'wechat_id'
value
=
{
wechat_id
}
autoComplete
=
'new-password'
placeholder
=
'此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改'
readOnly
label
=
'已绑定的微信账户'
name
=
'wechat_id'
value
=
{
wechat_id
}
autoComplete
=
'new-password'
placeholder
=
'此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改'
readOnly
/>
<
/Form.Field
>
<
Form
.
Field
>
<
Form
.
Input
label
=
'已绑定的邮箱账户'
name
=
'email'
value
=
{
email
}
autoComplete
=
'new-password'
placeholder
=
'此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改'
readOnly
label
=
'已绑定的邮箱账户'
name
=
'email'
value
=
{
email
}
autoComplete
=
'new-password'
placeholder
=
'此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改'
readOnly
/>
<
/Form.Field
>
<
Button
onClick
=
{
handleCancel
}
>
取消
<
/Button
>
<
Button
positive
onClick
=
{
submit
}
>
提交
<
/Button
>
<
/Form
>
<
/Segment
>
<
/SideSheet
>
<
/
>
);
};
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment