фиксы и статистика игроков

This commit is contained in:
Александр Кирюхин 2024-05-09 15:58:19 +03:00
parent 1ad012b8fa
commit 392b6df0d5
No known key found for this signature in database
GPG key ID: 35E33E1AB7776B39
15 changed files with 425 additions and 28 deletions

7
.env Normal file
View file

@ -0,0 +1,7 @@
POSTGRES_HOSTNAME=db
POSTGRES_DB=nquest
POSTGRES_USER=nquest
POSTGRES_PASSWORD=nquest
POSTGRES_PORT=5432
LISTEN=:8000
SECRET=d51a5056-c8ad-4c2a-939e-c60cff9c1214

View file

@ -1,8 +1,7 @@
{
debug
}
http:// { http:// {
header /* {
Cache-Control: no-cache, no-store, must-revalidate
}
handle_path /file/* { handle_path /file/* {
root * /app/store root * /app/store
file_server file_server

View file

@ -66,6 +66,19 @@ components:
schema: schema:
$ref: '#/components/schemas/userView' $ref: '#/components/schemas/userView'
description: "" description: ""
userShortResponse:
content:
application/json:
$ref: '#/components/schemas/userShortView'
description: ""
usersListResponse:
content:
application/json:
schema:
items:
$ref: '#/components/schemas/userShortView'
type: array
description: ""
schemas: schemas:
codeEdit: codeEdit:
properties: properties:
@ -213,6 +226,37 @@ components:
- text - text
- codes - codes
type: object type: object
userShortView:
properties:
expToCurrentLevel:
type: integer
expToNextLevel:
type: integer
experience:
type: integer
games:
items:
$ref: '#/components/schemas/gameView'
type: array
gamesFinished:
type: integer
gamesStarted:
type: integer
id:
format: uuid
type: string
level:
type: integer
username:
type: string
required:
- id
- username
- experience
- level
- gamesStarted
- gamesFinished
type: object
userView: userView:
properties: properties:
email: email:
@ -457,6 +501,26 @@ paths:
400: 400:
$ref: '#/components/responses/errorResponse' $ref: '#/components/responses/errorResponse'
security: [] security: []
/users:
get:
responses:
200:
$ref: '#/components/responses/usersListResponse'
description: users list
/users/{uid}:
get:
operationId: getUserInfo
parameters:
- in: path
name: uid
required: true
schema:
format: uuid
type: string
responses:
200:
$ref: '#/components/responses/userShortResponse'
description: users list
security: security:
- cookieAuth: [] - cookieAuth: []
servers: servers:

View file

@ -63,4 +63,17 @@ components:
schema: schema:
type: array type: array
items: items:
$ref: "#/components/schemas/fileItem" $ref: "#/components/schemas/fileItem"
usersListResponse:
description: ""
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/userShortView"
userShortResponse:
description: ""
content:
application/json:
$ref: "#/components/schemas/userShortView"

View file

@ -1,5 +1,36 @@
components: components:
schemas: schemas:
userShortView:
type: object
properties:
id:
type: string
format: uuid
username:
type: string
experience:
type: integer
level:
type: integer
gamesStarted:
type: integer
gamesFinished:
type: integer
expToCurrentLevel:
type: integer
expToNextLevel:
type: integer
games:
type: array
items:
$ref: "#/components/schemas/gameView"
required:
- id
- username
- experience
- level
- gamesStarted
- gamesFinished
userView: userView:
type: object type: object
properties: properties:

View file

@ -55,3 +55,23 @@ paths:
description: "success logout" description: "success logout"
400: 400:
$ref: "#/components/responses/errorResponse" $ref: "#/components/responses/errorResponse"
/users:
get:
responses:
200:
$ref: "#/components/responses/usersListResponse"
description: "users list"
/users/{uid}:
get:
operationId: getUserInfo
parameters:
- name: uid
in: path
required: true
schema:
type: string
format: uuid
responses:
200:
$ref: "#/components/responses/userShortResponse"
description: "users list"

View file

@ -60,6 +60,12 @@ type ServerInterface interface {
// (POST /user/register) // (POST /user/register)
PostUserRegister(ctx echo.Context) error PostUserRegister(ctx echo.Context) error
// (GET /users)
GetUsers(ctx echo.Context) error
// (GET /users/{uid})
GetUserInfo(ctx echo.Context, uid openapi_types.UUID) error
} }
// ServerInterfaceWrapper converts echo contexts to parameters. // ServerInterfaceWrapper converts echo contexts to parameters.
@ -248,6 +254,35 @@ func (w *ServerInterfaceWrapper) PostUserRegister(ctx echo.Context) error {
return err return err
} }
// GetUsers converts echo context to params.
func (w *ServerInterfaceWrapper) GetUsers(ctx echo.Context) error {
var err error
ctx.Set(CookieAuthScopes, []string{})
// Invoke the callback with all the unmarshaled arguments
err = w.Handler.GetUsers(ctx)
return err
}
// GetUserInfo converts echo context to params.
func (w *ServerInterfaceWrapper) GetUserInfo(ctx echo.Context) error {
var err error
// ------------- Path parameter "uid" -------------
var uid openapi_types.UUID
err = runtime.BindStyledParameterWithOptions("simple", "uid", ctx.Param("uid"), &uid, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true})
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter uid: %s", err))
}
ctx.Set(CookieAuthScopes, []string{})
// Invoke the callback with all the unmarshaled arguments
err = w.Handler.GetUserInfo(ctx, uid)
return err
}
// This is a simple interface which specifies echo.Route addition functions which // This is a simple interface which specifies echo.Route addition functions which
// are present on both echo.Echo and echo.Group, since we want to allow using // are present on both echo.Echo and echo.Group, since we want to allow using
// either of them for path registration // either of them for path registration
@ -289,33 +324,36 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL
router.POST(baseURL+"/user/login", wrapper.PostUserLogin) router.POST(baseURL+"/user/login", wrapper.PostUserLogin)
router.POST(baseURL+"/user/logout", wrapper.PostUserLogout) router.POST(baseURL+"/user/logout", wrapper.PostUserLogout)
router.POST(baseURL+"/user/register", wrapper.PostUserRegister) router.POST(baseURL+"/user/register", wrapper.PostUserRegister)
router.GET(baseURL+"/users", wrapper.GetUsers)
router.GET(baseURL+"/users/:uid", wrapper.GetUserInfo)
} }
// Base64 encoded, gzipped, json marshaled Swagger object // Base64 encoded, gzipped, json marshaled Swagger object
var swaggerSpec = []string{ var swaggerSpec = []string{
"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",
"0I910y50yiIW0sJWrst/AwAA//8rjKvDLRwAAA==", "VQtvketRgKNcx//3XfqrPIpAqV9K1VM93vkoYc2Utvzt9/JTJfldmbF7+db5dvyw5pjTartNKz8F19RQ",
"/1GTG5DytTw1fHSg5XOcJn/YC0j3b4CFDdF/K1uhJQXysYollwlZkhDH4GJV/B8AAP//bSQu1mogAAA=",
} }
// GetSwagger returns the content of the embedded swagger specification file // GetSwagger returns the content of the embedded swagger specification file

View file

@ -101,6 +101,19 @@ type TaskView struct {
// TaskViewMessage defines model for TaskView.Message. // TaskViewMessage defines model for TaskView.Message.
type TaskViewMessage string type TaskViewMessage string
// UserShortView defines model for userShortView.
type UserShortView struct {
ExpToCurrentLevel *int `json:"expToCurrentLevel,omitempty"`
ExpToNextLevel *int `json:"expToNextLevel,omitempty"`
Experience int `json:"experience"`
Games *[]GameView `json:"games,omitempty"`
GamesFinished int `json:"gamesFinished"`
GamesStarted int `json:"gamesStarted"`
Id openapi_types.UUID `json:"id"`
Level int `json:"level"`
Username string `json:"username"`
}
// UserView defines model for userView. // UserView defines model for userView.
type UserView struct { type UserView struct {
Email string `json:"email"` Email string `json:"email"`
@ -143,6 +156,12 @@ type UploadResponse struct {
// UserResponse defines model for userResponse. // UserResponse defines model for userResponse.
type UserResponse = UserView type UserResponse = UserView
// UserShortResponse defines model for userShortResponse.
type UserShortResponse interface{}
// UsersListResponse defines model for usersListResponse.
type UsersListResponse = []UserShortView
// AdminUploadFileMultipartBody defines parameters for AdminUploadFile. // AdminUploadFileMultipartBody defines parameters for AdminUploadFile.
type AdminUploadFileMultipartBody interface{} type AdminUploadFileMultipartBody interface{}

View file

@ -16,6 +16,8 @@ import User from './pages/User'
import { useRole } from './utils/roles' import { useRole } from './utils/roles'
import EditQuest from './pages/admin/Quest' import EditQuest from './pages/admin/Quest'
import AdminQuest from './pages/admin/Quests' import AdminQuest from './pages/admin/Quests'
import Users from './pages/Users'
import UserView from './pages/UserView'
const router = createBrowserRouter( const router = createBrowserRouter(
createRoutesFromElements( createRoutesFromElements(
@ -35,6 +37,18 @@ const router = createBrowserRouter(
element={<Auth><Quests /></Auth>} element={<Auth><Quests /></Auth>}
loader={() => ajax('/api/games').catch(x => { console.log(x); return null })} loader={() => ajax('/api/games').catch(x => { console.log(x); return null })}
/> />
<Route
id="users"
path="/users"
element={<Auth><Users /></Auth>}
loader={() => ajax('/api/users').catch(x => { console.log(x); return null })}
/>
<Route
id="user_view"
path="/users/:userId"
element={<Auth><UserView /></Auth>}
loader={({ params }) => ajax(`/api/users/${params.userId}`).catch(x => { console.log(x); return null })}
/>
<Route path="me" element={<User />} /> <Route path="me" element={<User />} />
<Route path="login" element={<Login />} /> <Route path="login" element={<Login />} />
<Route path="register" element={<Register />} /> <Route path="register" element={<Register />} />

View file

@ -35,6 +35,11 @@ const AppLayout = () => {
label: 'Квесты', label: 'Квесты',
link: '/quests' link: '/quests'
}, },
{
key: 'users',
label: 'Игроки',
link: '/users'
},
{ {
key: 'me', key: 'me',
label: `${user.username} [${user.level}]`, label: `${user.username} [${user.level}]`,

View file

@ -0,0 +1,34 @@
import { Avatar, Popover, Progress, Space, Typography } from 'antd'
import Markdown from 'react-markdown'
import { useLoaderData } from 'react-router-dom'
const { Paragraph } = Typography
const UserView = () => {
const user = useLoaderData()
if (user == null) {
return (<Space>Загрузка...</Space>)
}
return (<>
<h1>{user.username}</h1>
<Paragraph>Уровень: {user.level} ур</Paragraph>
<Paragraph>Очков опыта: {user.experience} ОО</Paragraph>
<Paragraph>Следующий уровень: {user.expToNextLevel} ОО</Paragraph>
<Progress
value={user.experience}
percent={((user.experience - user.expToCurrentLevel) / (user.expToNextLevel - user.expToCurrentLevel) * 100)}
size="small"
showInfo={false}
/>
{user.games.map(item => <Popover
key={item.id}
title={item.title}
content={<Markdown>{item.description}</Markdown>}
>
<Avatar size={64} style={{ marginRight: 4, marginBottom: 4 }} key={item.id} src={`/api/file/${item.icon}`} />
</Popover>)}
</>)
}
export default UserView

View file

@ -0,0 +1,44 @@
import { Table, Typography } from 'antd'
import { Link, useLoaderData } from 'react-router-dom'
const { Title } = Typography
const Users = () => {
const users = useLoaderData()
return (
<>
<Title>Статистика игроков</Title>
<Table
dataSource={users}
rowKey={'id'}
columns={[
{
title: 'Ник в игре',
dataIndex: 'username',
key: 'username',
render: (username, q) => <Link to={`/users/${q.id}`}>{username}</Link>
},
{
title: 'ОО',
dataIndex: 'experience',
key: 'experience'
},
{
title: 'Уровень',
dataIndex: 'level',
key: 'level'
},
{
title: 'Квесты начатые / Пройденные',
dataIndex: 'gamesStarted',
key: 'gamesStarted',
render: (gamesStarted, q) => (<>{gamesStarted} / {q.gamesFinished}</>)
}
]}
/>
</>
)
}
export default Users

View file

@ -11,6 +11,7 @@ const { Title } = Typography
const Quest = () => { const Quest = () => {
let { quest, files } = useLoaderData() let { quest, files } = useLoaderData()
const [error, setError] = useState() const [error, setError] = useState()
const [info, setInfo] = useState()
if (!quest) { if (!quest) {
quest = { quest = {
type: 'city', type: 'city',
@ -45,6 +46,21 @@ const Quest = () => {
} }
const onFinish = (values) => { const onFinish = (values) => {
values.tasks = values.tasks.map(task => {
if (!task.id) {
task.id = uuidv4()
}
task.codes = task.codes.map(code => {
if (!code.id) {
code.id = uuidv4()
}
return code
})
return task
})
console.log(values)
ajax('/api/admin/games', { ajax('/api/admin/games', {
method: 'POST', method: 'POST',
headers: { headers: {
@ -53,14 +69,24 @@ const Quest = () => {
}, },
body: JSON.stringify(values) body: JSON.stringify(values)
}) })
.then(g => navigate(`/admin/quests/${g.id}/`)) .then(g => {
.catch(({ message }) => setError('Ошибка создания')) navigate(`/admin/quests/${g.id}/`)
setError('')
setInfo('Сохранено')
return true
})
.catch(({ message }) => {
setInfo('')
setError('Ошибка создания:' + message)
})
} }
return ( return (
<> <>
<Title>{quest.title ? (quest.title) : ('Новый квест')}</Title> <Title>{quest.title ? (quest.title) : ('Новый квест')}</Title>
{error ? <Alert type="error" message={error} /> : null} {error ? <Alert type="error" message={error} /> : null}
{info ? <Alert type="info" message={info} /> : null}
<Row gutter={8}> <Row gutter={8}>
<Col xs={24} sm={16} md={16}> <Col xs={24} sm={16} md={16}>
<Form <Form
@ -146,6 +172,7 @@ const Quest = () => {
footer={<Button onClick={() => setPreview(false)}>Закрыть</Button>} footer={<Button onClick={() => setPreview(false)}>Закрыть</Button>}
width={'80%'} width={'80%'}
centered centered
onCancel={() => setPreview(false)}
> >
<List dataSource={fields.tasks} renderItem={(task) => ( <List dataSource={fields.tasks} renderItem={(task) => (
<List.Item key={task.id}> <List.Item key={task.id}>

View file

@ -1,8 +1,10 @@
package controller package controller
import ( import (
"log"
"net/http" "net/http"
"github.com/google/uuid"
"github.com/gorilla/sessions" "github.com/gorilla/sessions"
"github.com/labstack/echo-contrib/session" "github.com/labstack/echo-contrib/session"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
@ -94,6 +96,74 @@ func (u *User) GetUser(c echo.Context) error {
return mapUser(c, user) return mapUser(c, user)
} }
// (GET /users)
func (u *User) GetUsers(c echo.Context) error {
list, err := u.UserService.List(c.Request().Context())
if err != nil {
return err
}
result := make(api.UsersListResponse, 0, len(list))
for _, u := range list {
level := utils.ExpToLevel(u.Experience)
finished := 0
for _, g := range u.Games {
if g.Finish {
finished++
}
}
result = append(result, api.UserShortView{
Experience: u.Experience,
GamesFinished: finished,
GamesStarted: len(u.Games),
Id: u.ID,
Level: level,
Username: u.Username,
})
}
return c.JSON(200, result)
}
// (GET /users/{uid})
func (s *User) GetUserInfo(c echo.Context, uid uuid.UUID) error {
user, err := s.UserService.GetUserByID(c.Request().Context(), uid)
if err != nil {
log.Println(err)
return echo.ErrNotFound
}
level := utils.ExpToLevel(user.Experience)
expToCur := utils.LevelToExp(level)
expToNext := utils.LevelToExp(level + 1)
games := make([]api.GameView, 0, len(user.Games))
for _, gc := range user.Games {
games = append(games, api.GameView{
Id: gc.GameID,
Title: gc.Game.Title,
Description: gc.Game.Description,
Type: api.MapGameTypeReverse(gc.Game.Type),
Icon: gc.Game.IconID,
})
}
res := &api.UserShortView{
ExpToCurrentLevel: &expToCur,
ExpToNextLevel: &expToNext,
Experience: user.Experience,
Games: &games,
GamesFinished: 0,
GamesStarted: 0,
Id: user.ID,
Level: level,
Username: user.Username,
}
return c.JSON(200, res)
}
func setUser(c echo.Context, user *models.User) error { func setUser(c echo.Context, user *models.User) error {
sess, err := session.Get("session", c) sess, err := session.Get("session", c)
if err != nil { if err != nil {
@ -134,6 +204,7 @@ func mapUser(c echo.Context, user *models.User) error {
Title: gc.Game.Title, Title: gc.Game.Title,
Description: gc.Game.Description, Description: gc.Game.Description,
Type: api.MapGameTypeReverse(gc.Game.Type), Type: api.MapGameTypeReverse(gc.Game.Type),
Icon: gc.Game.IconID,
}) })
} }

View file

@ -135,6 +135,17 @@ func (s *User) GetUser(c echo.Context) *models.User {
return user return user
} }
func (s *User) List(ctx context.Context) ([]models.User, error) {
res := make([]models.User, 0)
return res, s.DB.WithContext(ctx).
Preload("Games").
Order("experience DESC").
Limit(100).
Find(&res).
Error
}
func (s *User) Update(ctx context.Context, user *models.User) error { func (s *User) Update(ctx context.Context, user *models.User) error {
return s.DB.WithContext(ctx).Session(&gorm.Session{FullSaveAssociations: true}).Save(user).Error return s.DB.WithContext(ctx).Session(&gorm.Session{FullSaveAssociations: true}).Save(user).Error
} }