From 74f508e847cf270a65350bdcc256c47269287daf Mon Sep 17 00:00:00 2001 From: JustSong Date: Sat, 10 Jun 2023 16:04:04 +0800 Subject: [PATCH] feat: now user can check its topup & consume history (close #78, close #95) --- controller/relay.go | 2 + model/log.go | 21 +++- model/main.go | 4 + model/redemption.go | 2 + web/src/App.js | 9 ++ web/src/components/Header.js | 5 + web/src/components/LogsTable.js | 186 ++++++++++++++++++++++++++++++++ web/src/pages/Log/index.js | 14 +++ 8 files changed, 240 insertions(+), 3 deletions(-) create mode 100644 web/src/components/LogsTable.js create mode 100644 web/src/pages/Log/index.js diff --git a/controller/relay.go b/controller/relay.go index ef279e3a..ac68f73d 100644 --- a/controller/relay.go +++ b/controller/relay.go @@ -244,6 +244,8 @@ func relayHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { if err != nil { common.SysError("Error consuming token remain quota: " + err.Error()) } + userId := c.GetInt("id") + model.RecordLog(userId, model.LogTypeConsume, fmt.Sprintf("使用模型 %s 消耗 %d 点额度", textRequest.Model, quota)) } }() diff --git a/model/log.go b/model/log.go index 179955b7..493f5613 100644 --- a/model/log.go +++ b/model/log.go @@ -1,6 +1,9 @@ package model -import "one-api/common" +import ( + "gorm.io/gorm" + "one-api/common" +) type Log struct { Id int `json:"id"` @@ -10,6 +13,12 @@ type Log struct { Content string `json:"content"` } +const ( + LogTypeUnknown = iota + LogTypeTopup + LogTypeConsume +) + func RecordLog(userId int, logType int, content string) { log := &Log{ UserId: userId, @@ -29,7 +38,13 @@ func GetAllLogs(logType int, startIdx int, num int) (logs []*Log, err error) { } func GetUserLogs(userId int, logType int, startIdx int, num int) (logs []*Log, err error) { - err = DB.Where("user_id = ? and type = ?", userId, logType).Order("id desc").Limit(num).Offset(startIdx).Find(&logs).Error + var tx *gorm.DB + if logType == LogTypeUnknown { + tx = DB.Where("user_id = ?", userId) + } else { + tx = DB.Where("user_id = ? and type = ?", userId, logType) + } + err = tx.Order("id desc").Limit(num).Offset(startIdx).Omit("id").Find(&logs).Error return logs, err } @@ -39,6 +54,6 @@ func SearchAllLogs(keyword string) (logs []*Log, err error) { } func SearchUserLogs(userId int, keyword string) (logs []*Log, err error) { - err = DB.Where("user_id = ? and type = ?", userId, keyword).Order("id desc").Limit(common.MaxRecentItems).Find(&logs).Error + err = DB.Where("user_id = ? and type = ?", userId, keyword).Order("id desc").Limit(common.MaxRecentItems).Omit("id").Find(&logs).Error return logs, err } diff --git a/model/main.go b/model/main.go index 8d55cee6..c1562101 100644 --- a/model/main.go +++ b/model/main.go @@ -79,6 +79,10 @@ func InitDB() (err error) { if err != nil { return err } + err = db.AutoMigrate(&Log{}) + if err != nil { + return err + } err = createRootAccountIfNeed() return err } else { diff --git a/model/redemption.go b/model/redemption.go index efff2d20..155e3cfd 100644 --- a/model/redemption.go +++ b/model/redemption.go @@ -2,6 +2,7 @@ package model import ( "errors" + "fmt" "one-api/common" ) @@ -65,6 +66,7 @@ func Redeem(key string, userId int) (quota int, err error) { if err != nil { common.SysError("更新兑换码状态失败:" + err.Error()) } + RecordLog(userId, LogTypeTopup, fmt.Sprintf("通过兑换码充值 %d 点额度", redemption.Quota)) }() return redemption.Quota, nil } diff --git a/web/src/App.js b/web/src/App.js index b2699858..e404b7f3 100644 --- a/web/src/App.js +++ b/web/src/App.js @@ -22,6 +22,7 @@ import EditChannel from './pages/Channel/EditChannel'; import Redemption from './pages/Redemption'; import EditRedemption from './pages/Redemption/EditRedemption'; import TopUp from './pages/TopUp'; +import Log from './pages/Log'; const Home = lazy(() => import('./pages/Home')); const About = lazy(() => import('./pages/About')); @@ -250,6 +251,14 @@ function App() { } /> + + + + } + /> + {timestamp2string(timestamp)} + + ); +} + +function renderType(type) { + switch (type) { + case 1: + return ; + case 2: + return ; + default: + return ; + } +} + +const LogsTable = () => { + const [logs, setLogs] = useState([]); + const [loading, setLoading] = useState(true); + const [activePage, setActivePage] = useState(1); + const [searchKeyword, setSearchKeyword] = useState(''); + const [searching, setSearching] = useState(false); + + const loadLogs = async (startIdx) => { + const res = await API.get(`/api/log/self/?p=${startIdx}`); + const { success, message, data } = res.data; + if (success) { + if (startIdx === 0) { + setLogs(data); + } else { + let newLogs = logs; + newLogs.push(...data); + setLogs(newLogs); + } + } else { + showError(message); + } + setLoading(false); + }; + + const onPaginationChange = (e, { activePage }) => { + (async () => { + if (activePage === Math.ceil(logs.length / ITEMS_PER_PAGE) + 1) { + // In this case we have to load more data and then append them. + await loadLogs(activePage - 1); + } + setActivePage(activePage); + })(); + }; + + const refresh = async () => { + setLoading(true); + await loadLogs(0); + }; + + useEffect(() => { + loadLogs(0) + .then() + .catch((reason) => { + showError(reason); + }); + }, []); + + const searchLogs = async () => { + if (searchKeyword === '') { + // if keyword is blank, load files instead. + await loadLogs(0); + setActivePage(1); + return; + } + setSearching(true); + const res = await API.get(`/api/log/self/search?keyword=${searchKeyword}`); + const { success, message, data } = res.data; + if (success) { + setLogs(data); + setActivePage(1); + } else { + showError(message); + } + setSearching(false); + }; + + const handleKeywordChange = async (e, { value }) => { + setSearchKeyword(value.trim()); + }; + + const sortLog = (key) => { + if (logs.length === 0) return; + setLoading(true); + let sortedLogs = [...logs]; + sortedLogs.sort((a, b) => { + return ('' + a[key]).localeCompare(b[key]); + }); + if (sortedLogs[0].id === logs[0].id) { + sortedLogs.reverse(); + } + setLogs(sortedLogs); + setLoading(false); + }; + + return ( + <> + + + + { + sortLog('created_time'); + }} + width={3} + > + 时间 + + { + sortLog('type'); + }} + width={2} + > + 类型 + + { + sortLog('content'); + }} + width={11} + > + 详情 + + + + + + {logs + .slice( + (activePage - 1) * ITEMS_PER_PAGE, + activePage * ITEMS_PER_PAGE + ) + .map((log, idx) => { + if (log.deleted) return <>; + return ( + + {renderTimestamp(log.created_at)} + {renderType(log.type)} + {log.content} + + ); + })} + + + + + + + + + + +
+ + ); +}; + +export default LogsTable; diff --git a/web/src/pages/Log/index.js b/web/src/pages/Log/index.js new file mode 100644 index 00000000..8c6073a0 --- /dev/null +++ b/web/src/pages/Log/index.js @@ -0,0 +1,14 @@ +import React from 'react'; +import { Header, Segment } from 'semantic-ui-react'; +import LogsTable from '../../components/LogsTable'; + +const Token = () => ( + <> + +
额度明细
+ +
+ +); + +export default Token;