feat: a big update
1. merge woj-runner scripts into woj-server 2. add woj-runner app 3. refactor submission status problem ... 4. jwt middleware update Co-authored-by: cxy004 <cxy004@qq.com> Co-authored-by: wzt <w.zhongtao@qq.com>
This commit is contained in:
parent
062c5ef964
commit
d42ee0ce54
11
.github/workflows/docker-image.yml
vendored
11
.github/workflows/docker-image.yml
vendored
@ -18,14 +18,15 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
- name: Docker meta
|
- name: Server Meta
|
||||||
id: meta
|
id: server_meta
|
||||||
uses: docker/metadata-action@v4
|
uses: docker/metadata-action@v4
|
||||||
with:
|
with:
|
||||||
images: panpaul/woj-server
|
images: panpaul/woj-server
|
||||||
- name: Build and Push the Docker Image
|
- name: Build and Push the Server Image
|
||||||
uses: docker/build-push-action@v3
|
uses: docker/build-push-action@v3
|
||||||
with:
|
with:
|
||||||
push: ${{ github.event_name != 'pull_request' }}
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.server_meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
file: ./Dockerfile.server
|
||||||
|
labels: ${{ steps.server_meta.outputs.labels }}
|
||||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,5 +1,6 @@
|
|||||||
### Project
|
### Project
|
||||||
/server
|
/server
|
||||||
|
/runner
|
||||||
my.secrets
|
my.secrets
|
||||||
|
|
||||||
### JetBrains template
|
### JetBrains template
|
||||||
|
11
Dockerfile.runner
Normal file
11
Dockerfile.runner
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
FROM golang:latest AS builder
|
||||||
|
WORKDIR /builder
|
||||||
|
COPY . /builder
|
||||||
|
RUN make runner
|
||||||
|
|
||||||
|
FROM alpine:latest
|
||||||
|
WORKDIR /app
|
||||||
|
RUN apk --no-cache add tzdata ca-certificates libc6-compat
|
||||||
|
COPY --from=builder /builder/server /app
|
||||||
|
COPY --from=builder /builder/resource/runner /app/resource/runner
|
||||||
|
ENTRYPOINT ["/app/runner"]
|
@ -1,12 +1,12 @@
|
|||||||
FROM golang:latest AS builder
|
FROM golang:latest AS builder
|
||||||
WORKDIR /builder
|
WORKDIR /builder
|
||||||
COPY . /builder
|
COPY . /builder
|
||||||
RUN make build
|
RUN make server
|
||||||
|
|
||||||
FROM alpine:latest
|
FROM alpine:latest
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
RUN apk --no-cache add tzdata ca-certificates libc6-compat
|
RUN apk --no-cache add tzdata ca-certificates libc6-compat
|
||||||
COPY --from=builder /builder/server /app
|
COPY --from=builder /builder/server /app
|
||||||
COPY --from=builder /builder/resource /app/resource
|
COPY --from=builder /builder/resource/frontend /app/resource/frontend
|
||||||
EXPOSE 8000
|
EXPOSE 8000
|
||||||
ENTRYPOINT ["/app/server"]
|
ENTRYPOINT ["/app/server"]
|
25
Makefile
25
Makefile
@ -1,28 +1,29 @@
|
|||||||
PROJECT=server
|
|
||||||
|
|
||||||
GO := go
|
GO := go
|
||||||
|
|
||||||
LDFLAGS += -X main.BuildTime=$(shell date -u '+%Y-%m-%d-%I-%M-%S')
|
LDFLAGS += -X cmd.BuildTime=$(shell date -u '+%Y-%m-%d-%I-%M-%S')
|
||||||
LDFLAGS += -X main.Version=$(shell cat VERSION)+$(shell git rev-parse HEAD)
|
LDFLAGS += -X cmd.Version=$(shell cat VERSION)+$(shell git rev-parse HEAD)
|
||||||
LDFLAGS += -s -w
|
LDFLAGS += -s -w
|
||||||
|
|
||||||
GOBUILD := $(GO) build -o $(PROJECT) -ldflags '$(LDFLAGS)' ./cmd/app
|
GOBUILD := $(GO) build -ldflags '$(LDFLAGS)'
|
||||||
GOBIN := $(shell go env GOPATH)/bin
|
GOBIN := $(shell go env GOPATH)/bin
|
||||||
|
|
||||||
.PHONY: all build clean run dep swagger fmt
|
.PHONY: all server runner build clean dep swagger fmt
|
||||||
|
|
||||||
default: all
|
default: all
|
||||||
|
|
||||||
all: clean build
|
all: clean build
|
||||||
|
|
||||||
build: swagger dep
|
server: swagger dep
|
||||||
$(GOBUILD)
|
$(GOBUILD) -o server ./cmd/server
|
||||||
|
|
||||||
|
runner: dep
|
||||||
|
$(GOBUILD) -o runner ./cmd/runner
|
||||||
|
|
||||||
|
build: runner server
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -f $(PROJECT)
|
rm -f runner
|
||||||
|
rm -f server
|
||||||
run: clean swagger dep build
|
|
||||||
./$(PROJECT) run
|
|
||||||
|
|
||||||
dep:
|
dep:
|
||||||
go mod tidy && go mod download
|
go mod tidy && go mod download
|
||||||
|
@ -1,68 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/urfave/cli/v2"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
a := &cli.App{
|
|
||||||
Name: "OJ",
|
|
||||||
Usage: "woj-server",
|
|
||||||
Compiled: getBuildTime(),
|
|
||||||
Version: Version,
|
|
||||||
EnableBashCompletion: true,
|
|
||||||
Authors: []*cli.Author{
|
|
||||||
{
|
|
||||||
Name: "Paul",
|
|
||||||
Email: "i@0x7f.app",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "cxy004",
|
|
||||||
Email: "cxy004@qq.com",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "wzt",
|
|
||||||
Email: "w.zhongtao@qq.com",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Flags: []cli.Flag{
|
|
||||||
&cli.StringFlag{
|
|
||||||
Name: "config",
|
|
||||||
Aliases: []string{"c"},
|
|
||||||
Usage: "path to the config file",
|
|
||||||
Value: "config.yaml",
|
|
||||||
EnvVars: []string{"APP_CONFIG"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Commands: []*cli.Command{
|
|
||||||
{
|
|
||||||
Name: "run",
|
|
||||||
Aliases: []string{"r"},
|
|
||||||
Usage: "start the server",
|
|
||||||
Action: run,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
err := a.Run(os.Args)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
BuildTime = "2022-09-06-01-00-00"
|
|
||||||
Version = "1.0.0+None"
|
|
||||||
)
|
|
||||||
|
|
||||||
func getBuildTime() time.Time {
|
|
||||||
build, err := time.Parse("2006-01-02-15-04-05", BuildTime)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("failed to parse build time: %v", err)
|
|
||||||
build = time.Now()
|
|
||||||
}
|
|
||||||
return build
|
|
||||||
}
|
|
@ -1,21 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/WHUPRJ/woj-server/internal/app"
|
|
||||||
"github.com/WHUPRJ/woj-server/internal/global"
|
|
||||||
"github.com/urfave/cli/v2"
|
|
||||||
"math/rand"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func run(c *cli.Context) error {
|
|
||||||
rand.Seed(time.Now().Unix())
|
|
||||||
|
|
||||||
g := new(global.Global)
|
|
||||||
g.SetupConfig(c.String("config"))
|
|
||||||
g.SetupZap()
|
|
||||||
defer func() { _ = g.Log.Sync() }()
|
|
||||||
|
|
||||||
g.Log.Info("starting server...")
|
|
||||||
return app.Run(g)
|
|
||||||
}
|
|
75
cmd/common.go
Normal file
75
cmd/common.go
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/WHUPRJ/woj-server/internal/global"
|
||||||
|
"github.com/urfave/cli/v2"
|
||||||
|
"log"
|
||||||
|
"math/rand"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var App = &cli.App{
|
||||||
|
Name: "WOJ",
|
||||||
|
EnableBashCompletion: true,
|
||||||
|
Authors: []*cli.Author{
|
||||||
|
{
|
||||||
|
Name: "Paul",
|
||||||
|
Email: "i@0x7f.app",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "cxy004",
|
||||||
|
Email: "cxy004@qq.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "wzt",
|
||||||
|
Email: "w.zhongtao@qq.com",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "config",
|
||||||
|
Aliases: []string{"c"},
|
||||||
|
Usage: "path to the config file",
|
||||||
|
Value: "config.yaml",
|
||||||
|
EnvVars: []string{"APP_CONFIG"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
BuildTime string
|
||||||
|
Version string
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
if BuildTime == "" {
|
||||||
|
BuildTime = "2022-09-06-01-00-00"
|
||||||
|
}
|
||||||
|
App.Compiled = getBuildTime()
|
||||||
|
|
||||||
|
if Version == "" {
|
||||||
|
Version = "0.0.0+None"
|
||||||
|
}
|
||||||
|
App.Version = Version
|
||||||
|
}
|
||||||
|
|
||||||
|
func getBuildTime() time.Time {
|
||||||
|
build, err := time.Parse("2006-01-02-15-04-05", BuildTime)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("failed to parse build time: %v", err)
|
||||||
|
build = time.Now()
|
||||||
|
}
|
||||||
|
return build
|
||||||
|
}
|
||||||
|
|
||||||
|
func CommonSetup(c *cli.Context) *global.Global {
|
||||||
|
rand.Seed(time.Now().Unix())
|
||||||
|
|
||||||
|
g := new(global.Global)
|
||||||
|
g.SetupConfig(c.String("config"))
|
||||||
|
g.SetupZap()
|
||||||
|
|
||||||
|
g.Log.Info("starting...")
|
||||||
|
|
||||||
|
return g
|
||||||
|
}
|
34
cmd/runner/main.go
Normal file
34
cmd/runner/main.go
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/WHUPRJ/woj-server/cmd"
|
||||||
|
"github.com/WHUPRJ/woj-server/internal/app/runner"
|
||||||
|
"github.com/urfave/cli/v2"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
a := cmd.App
|
||||||
|
a.Usage = "woj-runner"
|
||||||
|
a.Commands = []*cli.Command{
|
||||||
|
{
|
||||||
|
Name: "run",
|
||||||
|
Aliases: []string{"r"},
|
||||||
|
Usage: "start the runner",
|
||||||
|
Action: run,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := a.Run(os.Args)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func run(c *cli.Context) error {
|
||||||
|
g := cmd.CommonSetup(c)
|
||||||
|
defer func() { _ = g.Log.Sync() }()
|
||||||
|
|
||||||
|
return runner.RunRunner(g)
|
||||||
|
}
|
34
cmd/server/main.go
Normal file
34
cmd/server/main.go
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/WHUPRJ/woj-server/cmd"
|
||||||
|
"github.com/WHUPRJ/woj-server/internal/app/server"
|
||||||
|
"github.com/urfave/cli/v2"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
a := cmd.App
|
||||||
|
a.Usage = "woj-server"
|
||||||
|
a.Commands = []*cli.Command{
|
||||||
|
{
|
||||||
|
Name: "run",
|
||||||
|
Aliases: []string{"r"},
|
||||||
|
Usage: "start the server",
|
||||||
|
Action: run,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := a.Run(os.Args)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func run(c *cli.Context) error {
|
||||||
|
g := cmd.CommonSetup(c)
|
||||||
|
defer func() { _ = g.Log.Sync() }()
|
||||||
|
|
||||||
|
return server.RunServer(g)
|
||||||
|
}
|
@ -6,7 +6,7 @@ WebServer:
|
|||||||
|
|
||||||
Redis:
|
Redis:
|
||||||
Db: 0
|
Db: 0
|
||||||
QueueDb: 0
|
QueueDb: 1
|
||||||
Address: '127.0.0.1:6379'
|
Address: '127.0.0.1:6379'
|
||||||
Password: ''
|
Password: ''
|
||||||
|
|
||||||
|
5
go.mod
5
go.mod
@ -10,7 +10,7 @@ require (
|
|||||||
github.com/go-redis/redis/v8 v8.11.5
|
github.com/go-redis/redis/v8 v8.11.5
|
||||||
github.com/golang-jwt/jwt/v4 v4.4.2
|
github.com/golang-jwt/jwt/v4 v4.4.2
|
||||||
github.com/hibiken/asynq v0.23.0
|
github.com/hibiken/asynq v0.23.0
|
||||||
github.com/lib/pq v1.10.2
|
github.com/jackc/pgtype v1.11.0
|
||||||
github.com/prometheus/client_golang v1.13.0
|
github.com/prometheus/client_golang v1.13.0
|
||||||
github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a
|
github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a
|
||||||
github.com/swaggo/gin-swagger v1.5.3
|
github.com/swaggo/gin-swagger v1.5.3
|
||||||
@ -18,6 +18,7 @@ require (
|
|||||||
github.com/urfave/cli/v2 v2.14.1
|
github.com/urfave/cli/v2 v2.14.1
|
||||||
go.uber.org/zap v1.23.0
|
go.uber.org/zap v1.23.0
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519
|
||||||
|
golang.org/x/text v0.3.7
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
gorm.io/driver/postgres v1.3.9
|
gorm.io/driver/postgres v1.3.9
|
||||||
gorm.io/gorm v1.23.8
|
gorm.io/gorm v1.23.8
|
||||||
@ -49,7 +50,6 @@ require (
|
|||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
github.com/jackc/pgproto3/v2 v2.3.0 // indirect
|
github.com/jackc/pgproto3/v2 v2.3.0 // indirect
|
||||||
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect
|
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect
|
||||||
github.com/jackc/pgtype v1.11.0 // indirect
|
|
||||||
github.com/jackc/pgx/v4 v4.16.1 // indirect
|
github.com/jackc/pgx/v4 v4.16.1 // indirect
|
||||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
github.com/jinzhu/now v1.1.4 // indirect
|
github.com/jinzhu/now v1.1.4 // indirect
|
||||||
@ -74,7 +74,6 @@ require (
|
|||||||
go.uber.org/multierr v1.7.0 // indirect
|
go.uber.org/multierr v1.7.0 // indirect
|
||||||
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 // indirect
|
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 // indirect
|
||||||
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec // indirect
|
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec // indirect
|
||||||
golang.org/x/text v0.3.7 // indirect
|
|
||||||
golang.org/x/time v0.0.0-20220922220347-f3bd1da661af // indirect
|
golang.org/x/time v0.0.0-20220922220347-f3bd1da661af // indirect
|
||||||
golang.org/x/tools v0.1.10 // indirect
|
golang.org/x/tools v0.1.10 // indirect
|
||||||
google.golang.org/protobuf v1.28.1 // indirect
|
google.golang.org/protobuf v1.28.1 // indirect
|
||||||
|
45
internal/api/problem/details.go
Normal file
45
internal/api/problem/details.go
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
package problem
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/WHUPRJ/woj-server/internal/e"
|
||||||
|
"github.com/WHUPRJ/woj-server/internal/global"
|
||||||
|
"github.com/WHUPRJ/woj-server/internal/model"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type detailsRequest struct {
|
||||||
|
Pid uint `form:"pid"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Details
|
||||||
|
// @Summary get details of a problem
|
||||||
|
// @Description get details of a problem
|
||||||
|
// @Accept application/x-www-form-urlencoded
|
||||||
|
// @Produce json
|
||||||
|
// @Param pid formData int true "problem id"
|
||||||
|
// @Response 200 {object} e.Response "problem details"
|
||||||
|
// @Router /v1/problem/details [post]
|
||||||
|
func (h *handler) Details(c *gin.Context) {
|
||||||
|
req := new(detailsRequest)
|
||||||
|
|
||||||
|
if err := c.ShouldBind(req); err != nil {
|
||||||
|
e.Pong(c, e.InvalidParameter, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
claim, exist := c.Get("claim")
|
||||||
|
shouldEnable := !exist || claim.(*global.Claim).Role < model.RoleAdmin
|
||||||
|
|
||||||
|
p, status := h.problemService.Query(req.Pid, true, shouldEnable)
|
||||||
|
if status != e.Success {
|
||||||
|
e.Pong(c, status, nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pv, status := h.problemService.QueryLatestVersion(req.Pid)
|
||||||
|
e.Pong(c, status, gin.H{
|
||||||
|
"problem": p,
|
||||||
|
"context": pv.Context,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
@ -10,23 +10,25 @@ import (
|
|||||||
var _ Handler = (*handler)(nil)
|
var _ Handler = (*handler)(nil)
|
||||||
|
|
||||||
type Handler interface {
|
type Handler interface {
|
||||||
Update(c *gin.Context)
|
Details(c *gin.Context)
|
||||||
Search(c *gin.Context)
|
Search(c *gin.Context)
|
||||||
|
Update(c *gin.Context)
|
||||||
}
|
}
|
||||||
|
|
||||||
type handler struct {
|
type handler struct {
|
||||||
log *zap.Logger
|
log *zap.Logger
|
||||||
problemService problem.Service
|
|
||||||
jwtService global.JwtService
|
jwtService global.JwtService
|
||||||
|
problemService problem.Service
|
||||||
}
|
}
|
||||||
|
|
||||||
func RouteRegister(g *global.Global, group *gin.RouterGroup) {
|
func RouteRegister(g *global.Global, group *gin.RouterGroup) {
|
||||||
app := &handler{
|
app := &handler{
|
||||||
log: g.Log,
|
log: g.Log,
|
||||||
problemService: problem.NewService(g),
|
|
||||||
jwtService: g.Jwt,
|
jwtService: g.Jwt,
|
||||||
|
problemService: problem.NewService(g),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
group.POST("/details", app.jwtService.Handler(false), app.Details)
|
||||||
group.POST("/search", app.Search)
|
group.POST("/search", app.Search)
|
||||||
group.POST("/update", app.jwtService.Handler(), app.Update)
|
group.POST("/update", app.jwtService.Handler(true), app.Update)
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,6 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type searchRequest struct {
|
type searchRequest struct {
|
||||||
Pid uint `form:"pid"`
|
|
||||||
Search string `form:"search"`
|
Search string `form:"search"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -15,9 +14,8 @@ type searchRequest struct {
|
|||||||
// @Description get detail of a problem
|
// @Description get detail of a problem
|
||||||
// @Accept application/x-www-form-urlencoded
|
// @Accept application/x-www-form-urlencoded
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param pid formData int false "problem id"
|
// @Param search formData string false "word search"
|
||||||
// @Param search formData string false "search problem"
|
// @Response 200 {object} e.Response "problemset"
|
||||||
// @Response 200 {object} e.Response "problem info"
|
|
||||||
// @Router /v1/problem/search [post]
|
// @Router /v1/problem/search [post]
|
||||||
func (h *handler) Search(c *gin.Context) {
|
func (h *handler) Search(c *gin.Context) {
|
||||||
req := new(searchRequest)
|
req := new(searchRequest)
|
||||||
@ -27,17 +25,14 @@ func (h *handler) Search(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.Pid == 0 && req.Search == "" {
|
// TODO: pagination
|
||||||
e.Pong(c, e.InvalidParameter, nil)
|
if req.Search == "" {
|
||||||
return
|
// TODO: query without LIKE
|
||||||
}
|
problem, status := h.problemService.QueryFuzz(req.Search, true, true)
|
||||||
|
|
||||||
if req.Pid != 0 {
|
|
||||||
problem, status := h.problemService.Query(req.Pid)
|
|
||||||
e.Pong(c, status, problem)
|
e.Pong(c, status, problem)
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
problem, status := h.problemService.QueryFuzz(req.Search)
|
problem, status := h.problemService.QueryFuzz(req.Search, true, true)
|
||||||
e.Pong(c, status, problem)
|
e.Pong(c, status, problem)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -4,16 +4,15 @@ import (
|
|||||||
"github.com/WHUPRJ/woj-server/internal/e"
|
"github.com/WHUPRJ/woj-server/internal/e"
|
||||||
"github.com/WHUPRJ/woj-server/internal/global"
|
"github.com/WHUPRJ/woj-server/internal/global"
|
||||||
"github.com/WHUPRJ/woj-server/internal/model"
|
"github.com/WHUPRJ/woj-server/internal/model"
|
||||||
|
"github.com/WHUPRJ/woj-server/internal/service/problem"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
type updateRequest struct {
|
type updateRequest struct {
|
||||||
Pid uint `form:"pid"`
|
Pid uint `form:"pid"`
|
||||||
Title string `form:"title" binding:"required"`
|
Title string `form:"title" binding:"required"`
|
||||||
Content string `form:"content" binding:"required"`
|
Statement string `form:"statement" binding:"required"`
|
||||||
TimeLimit uint `form:"time_limit" binding:"required"`
|
IsEnabled bool `form:"is_enabled"`
|
||||||
MemoryLimit uint `form:"memory_limit" binding:"required"`
|
|
||||||
IsEnabled bool `form:"is_enabled"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update
|
// Update
|
||||||
@ -23,9 +22,7 @@ type updateRequest struct {
|
|||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param pid formData int false "problem id, 0 for create"
|
// @Param pid formData int false "problem id, 0 for create"
|
||||||
// @Param title formData string true "title"
|
// @Param title formData string true "title"
|
||||||
// @Param content formData string true "content"
|
// @Param statement formData string true "statement"
|
||||||
// @Param time_limit formData int true "time limit in ms"
|
|
||||||
// @Param memory_limit formData int true "memory limit in kb"
|
|
||||||
// @Param is_enabled formData bool false "is enabled"
|
// @Param is_enabled formData bool false "is enabled"
|
||||||
// @Response 200 {object} e.Response "problem info without provider information"
|
// @Response 200 {object} e.Response "problem info without provider information"
|
||||||
// @Security Authentication
|
// @Security Authentication
|
||||||
@ -50,32 +47,33 @@ func (h *handler) Update(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
problem := &model.Problem{
|
|
||||||
Title: req.Title,
|
|
||||||
Content: req.Content,
|
|
||||||
TimeLimit: req.TimeLimit,
|
|
||||||
MemoryLimit: req.MemoryLimit,
|
|
||||||
IsEnabled: req.IsEnabled,
|
|
||||||
}
|
|
||||||
|
|
||||||
if req.Pid == 0 {
|
if req.Pid == 0 {
|
||||||
problem, status := h.problemService.Create(uid, problem)
|
createData := &problem.CreateData{
|
||||||
e.Pong(c, status, problem)
|
Title: req.Title,
|
||||||
|
Statement: req.Statement,
|
||||||
|
ProviderID: uid,
|
||||||
|
IsEnabled: false,
|
||||||
|
}
|
||||||
|
p, status := h.problemService.Create(createData)
|
||||||
|
e.Pong(c, status, p)
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
inDb, status := h.problemService.Query(req.Pid)
|
p, status := h.problemService.Query(req.Pid, true, false)
|
||||||
if status != e.Success && status != e.ProblemNotAvailable {
|
if status != e.Success {
|
||||||
e.Pong(c, status, nil)
|
e.Pong(c, status, nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if p.ProviderID != uid {
|
||||||
if inDb.ProviderID != uid {
|
|
||||||
e.Pong(c, e.UserUnauthorized, nil)
|
e.Pong(c, e.UserUnauthorized, nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
problem, status := h.problemService.Update(req.Pid, problem)
|
p.Title = req.Title
|
||||||
e.Pong(c, status, problem)
|
p.Statement = req.Statement
|
||||||
|
p.IsEnabled = req.IsEnabled
|
||||||
|
|
||||||
|
p, status = h.problemService.Update(p)
|
||||||
|
e.Pong(c, status, p)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
34
internal/api/runner/build.go
Normal file
34
internal/api/runner/build.go
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
package runner
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"github.com/WHUPRJ/woj-server/internal/model"
|
||||||
|
"github.com/hibiken/asynq"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (h *handler) Build(_ context.Context, t *asynq.Task) error {
|
||||||
|
// TODO: configure timeout with context
|
||||||
|
|
||||||
|
var p model.ProblemBuildPayload
|
||||||
|
if err := json.Unmarshal(t.Payload(), &p); err != nil {
|
||||||
|
return fmt.Errorf("json.Unmarshal failed: %v: %w", err, asynq.SkipRetry)
|
||||||
|
}
|
||||||
|
|
||||||
|
h.log.Info("build", zap.Any("payload", p))
|
||||||
|
|
||||||
|
config, status := h.runnerService.NewProblem(p.ProblemVersionID, p.ProblemFile)
|
||||||
|
|
||||||
|
for i := range config.Languages {
|
||||||
|
config.Languages[i].Type = ""
|
||||||
|
config.Languages[i].Script = ""
|
||||||
|
config.Languages[i].Cmp = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
b, _ := json.Marshal(config)
|
||||||
|
h.taskService.ProblemUpdate(status, p.ProblemVersionID, string(b))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
41
internal/api/runner/handler.go
Normal file
41
internal/api/runner/handler.go
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
package runner
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"github.com/WHUPRJ/woj-server/internal/e"
|
||||||
|
"github.com/WHUPRJ/woj-server/internal/global"
|
||||||
|
"github.com/WHUPRJ/woj-server/internal/service/runner"
|
||||||
|
"github.com/WHUPRJ/woj-server/internal/service/task"
|
||||||
|
"github.com/hibiken/asynq"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ Handler = (*handler)(nil)
|
||||||
|
|
||||||
|
type Handler interface {
|
||||||
|
Build(_ context.Context, t *asynq.Task) error
|
||||||
|
Judge(_ context.Context, t *asynq.Task) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type handler struct {
|
||||||
|
log *zap.Logger
|
||||||
|
runnerService runner.Service
|
||||||
|
taskService task.Service
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRunner(g *global.Global) (Handler, error) {
|
||||||
|
hnd := &handler{
|
||||||
|
log: g.Log,
|
||||||
|
runnerService: runner.NewService(g),
|
||||||
|
taskService: task.NewService(g),
|
||||||
|
}
|
||||||
|
|
||||||
|
status := hnd.runnerService.EnsureDeps(false)
|
||||||
|
if status != e.Success {
|
||||||
|
g.Log.Error("failed to ensure runner dependencies", zap.String("status", status.String()))
|
||||||
|
return nil, errors.New("failed to ensure dependencies")
|
||||||
|
}
|
||||||
|
|
||||||
|
return hnd, nil
|
||||||
|
}
|
73
internal/api/runner/judge.go
Normal file
73
internal/api/runner/judge.go
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
package runner
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"github.com/WHUPRJ/woj-server/internal/e"
|
||||||
|
"github.com/WHUPRJ/woj-server/internal/model"
|
||||||
|
"github.com/WHUPRJ/woj-server/internal/service/runner"
|
||||||
|
"github.com/WHUPRJ/woj-server/pkg/utils"
|
||||||
|
"github.com/hibiken/asynq"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (h *handler) Judge(_ context.Context, t *asynq.Task) error {
|
||||||
|
var p model.SubmitJudgePayload
|
||||||
|
if err := json.Unmarshal(t.Payload(), &p); err != nil {
|
||||||
|
return fmt.Errorf("json.Unmarshal failed: %v: %w", err, asynq.SkipRetry)
|
||||||
|
}
|
||||||
|
|
||||||
|
user := utils.RandomString(16)
|
||||||
|
h.log.Info("judge", zap.Any("payload", p), zap.String("user", user))
|
||||||
|
|
||||||
|
// common
|
||||||
|
systemError := runner.JudgeStatus{Message: "System Error"}
|
||||||
|
|
||||||
|
// write code
|
||||||
|
userCode := filepath.Join(runner.UserDir, user, fmt.Sprintf("%s.%s", user, p.Submission.Language))
|
||||||
|
if !utils.FileTouch(userCode) {
|
||||||
|
h.log.Info("Touch file failed", zap.String("userCode", userCode))
|
||||||
|
h.taskService.SubmitUpdate(e.InternalError, p.ProblemVersionId, 0, systemError)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
err := utils.FileWrite(userCode, []byte(p.Submission.Code))
|
||||||
|
if err != nil {
|
||||||
|
h.log.Info("Write file failed", zap.String("code", p.Submission.Code))
|
||||||
|
h.taskService.SubmitUpdate(e.InternalError, p.ProblemVersionId, 0, systemError)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// compile
|
||||||
|
result, status := h.runnerService.Compile(p.ProblemVersionId, user, p.Submission.Language)
|
||||||
|
if status == e.RunnerProblemNotExist {
|
||||||
|
_, status := h.runnerService.NewProblem(p.ProblemVersionId, p.StorageKey)
|
||||||
|
if status != e.Success {
|
||||||
|
h.log.Warn("download problem failed",
|
||||||
|
zap.Any("status", status),
|
||||||
|
zap.Uint("pvid", p.ProblemVersionId),
|
||||||
|
zap.String("storageKey", p.StorageKey))
|
||||||
|
h.taskService.SubmitUpdate(status, p.ProblemVersionId, 0, systemError)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
} else if status != e.Success {
|
||||||
|
h.taskService.SubmitUpdate(status, p.Submission.ID, 0, result)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// config
|
||||||
|
config, err := h.runnerService.ParseConfig(p.ProblemVersionId, true)
|
||||||
|
if err != nil {
|
||||||
|
h.log.Info("parse config failed", zap.Error(err), zap.Uint("pvid", p.ProblemVersionId))
|
||||||
|
h.taskService.SubmitUpdate(e.InternalError, p.ProblemVersionId, 0, systemError)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// run
|
||||||
|
var points int32
|
||||||
|
result, points, status = h.runnerService.RunAndJudge(p.ProblemVersionId, user, p.Submission.Language, &config)
|
||||||
|
h.taskService.SubmitUpdate(status, p.Submission.ID, points, result)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
32
internal/api/status/handler.go
Normal file
32
internal/api/status/handler.go
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
package status
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/WHUPRJ/woj-server/internal/global"
|
||||||
|
"github.com/WHUPRJ/woj-server/internal/service/status"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ Handler = (*handler)(nil)
|
||||||
|
|
||||||
|
type Handler interface {
|
||||||
|
Query(c *gin.Context)
|
||||||
|
QueryByProblemVersion(c *gin.Context)
|
||||||
|
}
|
||||||
|
|
||||||
|
type handler struct {
|
||||||
|
log *zap.Logger
|
||||||
|
statusService status.Service
|
||||||
|
jwtService global.JwtService
|
||||||
|
}
|
||||||
|
|
||||||
|
func RouteRegister(g *global.Global, group *gin.RouterGroup) {
|
||||||
|
app := &handler{
|
||||||
|
log: g.Log,
|
||||||
|
statusService: status.NewService(g),
|
||||||
|
jwtService: g.Jwt,
|
||||||
|
}
|
||||||
|
|
||||||
|
group.POST("/query", app.Query)
|
||||||
|
group.POST("/query/problem_version", app.jwtService.Handler(true), app.QueryByProblemVersion)
|
||||||
|
}
|
24
internal/api/status/query.go
Normal file
24
internal/api/status/query.go
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
package status
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/WHUPRJ/woj-server/internal/e"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type queryRequest struct {
|
||||||
|
SubmissionID uint `form:"sid"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *handler) Query(c *gin.Context) {
|
||||||
|
req := new(queryRequest)
|
||||||
|
|
||||||
|
if err := c.ShouldBind(req); err != nil {
|
||||||
|
e.Pong(c, e.InvalidParameter, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
status, eStatus := h.statusService.Query(req.SubmissionID, true)
|
||||||
|
|
||||||
|
e.Pong(c, eStatus, status)
|
||||||
|
return
|
||||||
|
}
|
42
internal/api/status/queryByVersion.go
Normal file
42
internal/api/status/queryByVersion.go
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
package status
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/WHUPRJ/woj-server/internal/e"
|
||||||
|
"github.com/WHUPRJ/woj-server/internal/global"
|
||||||
|
"github.com/WHUPRJ/woj-server/internal/model"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type queryByVersionRequest struct {
|
||||||
|
ProblemVersionID uint `form:"pvid"`
|
||||||
|
Offset int `form:"offset"`
|
||||||
|
Limit int `form:"limit"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *handler) QueryByProblemVersion(c *gin.Context) {
|
||||||
|
|
||||||
|
claim, exist := c.Get("claim")
|
||||||
|
if !exist {
|
||||||
|
e.Pong(c, e.UserUnauthenticated, nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
role := claim.(*global.Claim).Role
|
||||||
|
|
||||||
|
req := new(queryByVersionRequest)
|
||||||
|
|
||||||
|
if err := c.ShouldBind(req); err != nil {
|
||||||
|
e.Pong(c, e.InvalidParameter, nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if role < model.RoleAdmin {
|
||||||
|
e.Pong(c, e.UserUnauthorized, nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
statuses, eStatus := h.statusService.QueryByVersion(req.ProblemVersionID, req.Offset, req.Limit)
|
||||||
|
|
||||||
|
e.Pong(c, eStatus, statuses)
|
||||||
|
return
|
||||||
|
}
|
64
internal/api/submission/create.go
Normal file
64
internal/api/submission/create.go
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
package submission
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/WHUPRJ/woj-server/internal/e"
|
||||||
|
"github.com/WHUPRJ/woj-server/internal/global"
|
||||||
|
"github.com/WHUPRJ/woj-server/internal/service/submission"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type createRequest struct {
|
||||||
|
Pid uint `form:"pid" binding:"required"`
|
||||||
|
Language string `form:"language" binding:"required"`
|
||||||
|
Code string `form:"statement" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create
|
||||||
|
// @Summary create a submission
|
||||||
|
// @Description create a submission
|
||||||
|
// @Accept application/x-www-form-urlencoded
|
||||||
|
// @Produce json
|
||||||
|
// @Param pid formData int true "problem id"
|
||||||
|
// @Param language formData string true "language"
|
||||||
|
// @Param code formData string true "code"
|
||||||
|
// @Response 200 {object} e.Response ""
|
||||||
|
// @Security Authentication
|
||||||
|
// @Router /v1/submission/create [post]
|
||||||
|
func (h *handler) Create(c *gin.Context) {
|
||||||
|
claim, exist := c.Get("claim")
|
||||||
|
if !exist {
|
||||||
|
e.Pong(c, e.UserUnauthenticated, nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
uid := claim.(*global.Claim).UID
|
||||||
|
|
||||||
|
req := new(createRequest)
|
||||||
|
|
||||||
|
if err := c.ShouldBind(req); err != nil {
|
||||||
|
e.Pong(c, e.InvalidParameter, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
createData := &submission.CreateData{
|
||||||
|
ProblemID: req.Pid,
|
||||||
|
UserID: uid,
|
||||||
|
Language: req.Language,
|
||||||
|
Code: req.Code,
|
||||||
|
}
|
||||||
|
s, status := h.submissionService.Create(createData)
|
||||||
|
if status != e.Success {
|
||||||
|
e.Pong(c, status, nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pv, status := h.problemService.QueryLatestVersion(req.Pid)
|
||||||
|
if status != e.Success {
|
||||||
|
e.Pong(c, status, nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, status = h.taskService.SubmitJudge(pv.ID, pv.StorageKey, *s)
|
||||||
|
e.Pong(c, status, nil)
|
||||||
|
return
|
||||||
|
}
|
41
internal/api/submission/handler.go
Normal file
41
internal/api/submission/handler.go
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
package submission
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/WHUPRJ/woj-server/internal/global"
|
||||||
|
"github.com/WHUPRJ/woj-server/internal/service/problem"
|
||||||
|
"github.com/WHUPRJ/woj-server/internal/service/status"
|
||||||
|
"github.com/WHUPRJ/woj-server/internal/service/submission"
|
||||||
|
"github.com/WHUPRJ/woj-server/internal/service/task"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ Handler = (*handler)(nil)
|
||||||
|
|
||||||
|
type Handler interface {
|
||||||
|
Create(c *gin.Context)
|
||||||
|
Query(c *gin.Context)
|
||||||
|
}
|
||||||
|
|
||||||
|
type handler struct {
|
||||||
|
log *zap.Logger
|
||||||
|
jwtService global.JwtService
|
||||||
|
problemService problem.Service
|
||||||
|
statusService status.Service
|
||||||
|
submissionService submission.Service
|
||||||
|
taskService task.Service
|
||||||
|
}
|
||||||
|
|
||||||
|
func RouteRegister(g *global.Global, group *gin.RouterGroup) {
|
||||||
|
app := &handler{
|
||||||
|
log: g.Log,
|
||||||
|
jwtService: g.Jwt,
|
||||||
|
problemService: problem.NewService(g),
|
||||||
|
statusService: status.NewService(g),
|
||||||
|
submissionService: submission.NewService(g),
|
||||||
|
taskService: task.NewService(g),
|
||||||
|
}
|
||||||
|
|
||||||
|
group.POST("/create", app.jwtService.Handler(true), app.Create)
|
||||||
|
group.POST("/query", app.Query)
|
||||||
|
}
|
73
internal/api/submission/query.go
Normal file
73
internal/api/submission/query.go
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
package submission
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/WHUPRJ/woj-server/internal/e"
|
||||||
|
"github.com/WHUPRJ/woj-server/internal/model"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type queryRequest struct {
|
||||||
|
Pid uint `form:"pid"`
|
||||||
|
Uid uint `form:"uid"`
|
||||||
|
Offset int `form:"offset"`
|
||||||
|
Limit int `form:"limit"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type queryResponse struct {
|
||||||
|
Submission model.Submission `json:"submission"`
|
||||||
|
Point int32 `json:"point"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query
|
||||||
|
// @Summary Query submissions
|
||||||
|
// @Description Query submissions
|
||||||
|
// @Accept application/x-www-form-urlencoded
|
||||||
|
// @Produce json
|
||||||
|
// @Param pid formData uint false "problem id"
|
||||||
|
// @Param uid formData uint false "user id"
|
||||||
|
// @Param offset formData int false "start position"
|
||||||
|
// @Param limit formData int false "limit number of records"
|
||||||
|
// @Response 200 {object} e.Response "queryResponse"
|
||||||
|
// @Router /v1/submission/query [post]
|
||||||
|
|
||||||
|
func (h *handler) Query(c *gin.Context) {
|
||||||
|
req := new(queryRequest)
|
||||||
|
|
||||||
|
if err := c.ShouldBind(req); err != nil {
|
||||||
|
e.Pong(c, e.InvalidParameter, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Pid == 0 && req.Uid == 0 {
|
||||||
|
e.Pong(c, e.InvalidParameter, nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
submissions, status := h.submissionService.Query(req.Pid, req.Uid, req.Offset, req.Limit)
|
||||||
|
|
||||||
|
var response []*queryResponse
|
||||||
|
|
||||||
|
for _, submission := range submissions {
|
||||||
|
currentStatus, _ := h.statusService.Query(submission.ID, false)
|
||||||
|
var currentPoint int32
|
||||||
|
|
||||||
|
if currentStatus == nil {
|
||||||
|
currentPoint = -1
|
||||||
|
} else {
|
||||||
|
currentPoint = currentStatus.Point
|
||||||
|
}
|
||||||
|
|
||||||
|
newResponse := &queryResponse{
|
||||||
|
Submission: *submission,
|
||||||
|
Point: currentPoint,
|
||||||
|
}
|
||||||
|
|
||||||
|
newResponse.Submission.Code = ""
|
||||||
|
|
||||||
|
response = append(response, newResponse)
|
||||||
|
}
|
||||||
|
|
||||||
|
e.Pong(c, status, submissions)
|
||||||
|
return
|
||||||
|
|
||||||
|
}
|
@ -8,9 +8,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type createRequest struct {
|
type createRequest struct {
|
||||||
Username string `form:"username" binding:"required"`
|
UserName string `form:"username" binding:"required"`
|
||||||
Nickname string `form:"nickname" binding:"required"`
|
|
||||||
Password string `form:"password" binding:"required"`
|
Password string `form:"password" binding:"required"`
|
||||||
|
NickName string `form:"nickname" binding:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create
|
// Create
|
||||||
@ -32,11 +32,10 @@ func (h *handler) Create(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
createData := &user.CreateData{
|
createData := &user.CreateData{
|
||||||
Username: req.Username,
|
UserName: req.UserName,
|
||||||
Nickname: req.Nickname,
|
|
||||||
Password: req.Password,
|
Password: req.Password,
|
||||||
|
NickName: req.NickName,
|
||||||
}
|
}
|
||||||
|
|
||||||
u, status := h.userService.Create(createData)
|
u, status := h.userService.Create(createData)
|
||||||
if status != e.Success {
|
if status != e.Success {
|
||||||
e.Pong(c, status, nil)
|
e.Pong(c, status, nil)
|
||||||
@ -48,6 +47,7 @@ func (h *handler) Create(c *gin.Context) {
|
|||||||
e.Pong(c, status, nil)
|
e.Pong(c, status, nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
claim := &global.Claim{
|
claim := &global.Claim{
|
||||||
UID: u.ID,
|
UID: u.ID,
|
||||||
Role: u.Role,
|
Role: u.Role,
|
||||||
@ -55,4 +55,5 @@ func (h *handler) Create(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
token, status := h.jwtService.SignClaim(claim)
|
token, status := h.jwtService.SignClaim(claim)
|
||||||
e.Pong(c, status, token)
|
e.Pong(c, status, token)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
@ -18,19 +18,19 @@ type Handler interface {
|
|||||||
|
|
||||||
type handler struct {
|
type handler struct {
|
||||||
log *zap.Logger
|
log *zap.Logger
|
||||||
userService user.Service
|
|
||||||
jwtService global.JwtService
|
jwtService global.JwtService
|
||||||
|
userService user.Service
|
||||||
}
|
}
|
||||||
|
|
||||||
func RouteRegister(g *global.Global, group *gin.RouterGroup) {
|
func RouteRegister(g *global.Global, group *gin.RouterGroup) {
|
||||||
app := &handler{
|
app := &handler{
|
||||||
log: g.Log,
|
log: g.Log,
|
||||||
userService: user.NewService(g),
|
|
||||||
jwtService: g.Jwt,
|
jwtService: g.Jwt,
|
||||||
|
userService: user.NewService(g),
|
||||||
}
|
}
|
||||||
|
|
||||||
group.POST("/login", app.Login)
|
|
||||||
group.POST("/create", app.Create)
|
group.POST("/create", app.Create)
|
||||||
group.POST("/logout", app.jwtService.Handler(), app.Logout)
|
group.POST("/login", app.Login)
|
||||||
group.POST("/profile", app.jwtService.Handler(), app.Profile)
|
group.POST("/logout", app.jwtService.Handler(true), app.Logout)
|
||||||
|
group.POST("/profile", app.jwtService.Handler(true), app.Profile)
|
||||||
}
|
}
|
||||||
|
@ -3,12 +3,12 @@ package user
|
|||||||
import (
|
import (
|
||||||
"github.com/WHUPRJ/woj-server/internal/e"
|
"github.com/WHUPRJ/woj-server/internal/e"
|
||||||
"github.com/WHUPRJ/woj-server/internal/global"
|
"github.com/WHUPRJ/woj-server/internal/global"
|
||||||
"github.com/WHUPRJ/woj-server/internal/model"
|
"github.com/WHUPRJ/woj-server/internal/service/user"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
type loginRequest struct {
|
type loginRequest struct {
|
||||||
Username string `form:"username" binding:"required"`
|
UserName string `form:"username" binding:"required"`
|
||||||
Password string `form:"password" binding:"required"`
|
Password string `form:"password" binding:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -30,30 +30,30 @@ func (h *handler) Login(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// check password
|
// check password
|
||||||
userData := &model.User{
|
loginData := &user.LoginData{
|
||||||
UserName: req.Username,
|
UserName: req.UserName,
|
||||||
Password: []byte(req.Password),
|
Password: req.Password,
|
||||||
}
|
}
|
||||||
user, status := h.userService.Login(userData)
|
u, status := h.userService.Login(loginData)
|
||||||
if status != e.Success {
|
if status != e.Success {
|
||||||
e.Pong(c, status, nil)
|
e.Pong(c, status, nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// sign and return token
|
// sign and return token
|
||||||
version, status := h.userService.IncrVersion(user.ID)
|
version, status := h.userService.IncrVersion(u.ID)
|
||||||
if status != e.Success {
|
if status != e.Success {
|
||||||
e.Pong(c, status, nil)
|
e.Pong(c, status, nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
claim := &global.Claim{
|
claim := &global.Claim{
|
||||||
UID: user.ID,
|
UID: u.ID,
|
||||||
Role: user.Role,
|
Role: u.Role,
|
||||||
Version: version,
|
Version: version,
|
||||||
}
|
}
|
||||||
token, status := h.jwtService.SignClaim(claim)
|
token, status := h.jwtService.SignClaim(claim)
|
||||||
e.Pong(c, status, gin.H{
|
e.Pong(c, status, gin.H{
|
||||||
"token": token,
|
"token": token,
|
||||||
"nickname": user.NickName,
|
"nickname": u.NickName,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -23,4 +23,5 @@ func (h *handler) Logout(c *gin.Context) {
|
|||||||
|
|
||||||
_, status := h.userService.IncrVersion(claim.(*global.Claim).UID)
|
_, status := h.userService.IncrVersion(claim.(*global.Claim).UID)
|
||||||
e.Pong(c, status, nil)
|
e.Pong(c, status, nil)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
@ -31,18 +31,22 @@ func (h *handler) Profile(c *gin.Context) {
|
|||||||
|
|
||||||
uid := claim.(*global.Claim).UID
|
uid := claim.(*global.Claim).UID
|
||||||
role := claim.(*global.Claim).Role
|
role := claim.(*global.Claim).Role
|
||||||
|
|
||||||
req := new(profileRequest)
|
req := new(profileRequest)
|
||||||
if err := c.ShouldBind(req); err == nil {
|
|
||||||
if req.UID != 0 && req.UID != uid {
|
if err := c.ShouldBind(req); err != nil {
|
||||||
if role >= model.RoleGeneral {
|
e.Pong(c, e.InvalidParameter, nil)
|
||||||
uid = req.UID
|
return
|
||||||
} else {
|
|
||||||
e.Pong(c, e.UserUnauthorized, nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
user, status := h.userService.Profile(uid)
|
if req.UID == 0 {
|
||||||
|
req.UID = uid
|
||||||
|
} else if req.UID != uid && role < model.RoleGeneral {
|
||||||
|
e.Pong(c, e.UserUnauthorized, nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user, status := h.userService.Profile(req.UID)
|
||||||
e.Pong(c, status, user)
|
e.Pong(c, status, user)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
43
internal/app/runner/runner.go
Normal file
43
internal/app/runner/runner.go
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
package runner
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/WHUPRJ/woj-server/internal/api/runner"
|
||||||
|
"github.com/WHUPRJ/woj-server/internal/global"
|
||||||
|
"github.com/WHUPRJ/woj-server/internal/model"
|
||||||
|
"github.com/WHUPRJ/woj-server/pkg/utils"
|
||||||
|
"github.com/WHUPRJ/woj-server/pkg/zapasynq"
|
||||||
|
"github.com/hibiken/asynq"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RunRunner(g *global.Global) error {
|
||||||
|
hnd, err := runner.NewRunner(g)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
mux := asynq.NewServeMux()
|
||||||
|
mux.HandleFunc(model.TypeProblemBuild, hnd.Build)
|
||||||
|
mux.HandleFunc(model.TypeSubmitJudge, hnd.Judge)
|
||||||
|
|
||||||
|
srv := asynq.NewServer(
|
||||||
|
asynq.RedisClientOpt{
|
||||||
|
Addr: g.Conf.Redis.Address,
|
||||||
|
Password: g.Conf.Redis.Password,
|
||||||
|
DB: g.Conf.Redis.QueueDb,
|
||||||
|
},
|
||||||
|
asynq.Config{
|
||||||
|
Concurrency: utils.If(runtime.NumCPU() > 1, runtime.NumCPU()-1, 1).(int),
|
||||||
|
Logger: zapasynq.New(g.Log),
|
||||||
|
Queues: map[string]int{model.QueueRunner: 1},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := srv.Run(mux); err != nil {
|
||||||
|
g.Log.Warn("could not run server", zap.Error(err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
@ -1,22 +1,27 @@
|
|||||||
package app
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/WHUPRJ/woj-server/internal/global"
|
"github.com/WHUPRJ/woj-server/internal/global"
|
||||||
|
"github.com/WHUPRJ/woj-server/internal/model"
|
||||||
"github.com/WHUPRJ/woj-server/internal/repo/postgresql"
|
"github.com/WHUPRJ/woj-server/internal/repo/postgresql"
|
||||||
"github.com/WHUPRJ/woj-server/internal/repo/redis"
|
"github.com/WHUPRJ/woj-server/internal/repo/redis"
|
||||||
"github.com/WHUPRJ/woj-server/internal/router"
|
"github.com/WHUPRJ/woj-server/internal/router"
|
||||||
"github.com/WHUPRJ/woj-server/internal/service/jwt"
|
"github.com/WHUPRJ/woj-server/internal/service/jwt"
|
||||||
|
"github.com/WHUPRJ/woj-server/pkg/utils"
|
||||||
|
"github.com/WHUPRJ/woj-server/pkg/zapasynq"
|
||||||
|
"github.com/hibiken/asynq"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
"runtime"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Run(g *global.Global) error {
|
func RunServer(g *global.Global) error {
|
||||||
// Setup Database
|
// Setup Database
|
||||||
g.Db = new(postgresql.Repo)
|
g.Db = new(postgresql.Repo)
|
||||||
g.Db.Setup(g)
|
g.Db.Setup(g)
|
||||||
@ -29,13 +34,13 @@ func Run(g *global.Global) error {
|
|||||||
g.Jwt = jwt.NewJwtService(g)
|
g.Jwt = jwt.NewJwtService(g)
|
||||||
|
|
||||||
// Prepare Router
|
// Prepare Router
|
||||||
handler := router.InitRouters(g)
|
routers := router.InitRouters(g)
|
||||||
|
|
||||||
// Create Server
|
// Create Server
|
||||||
addr := fmt.Sprintf("%s:%d", g.Conf.WebServer.Address, g.Conf.WebServer.Port)
|
addr := fmt.Sprintf("%s:%d", g.Conf.WebServer.Address, g.Conf.WebServer.Port)
|
||||||
server := &http.Server{
|
server := &http.Server{
|
||||||
Addr: addr,
|
Addr: addr,
|
||||||
Handler: handler,
|
Handler: routers,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run Server
|
// Run Server
|
||||||
@ -45,19 +50,47 @@ func Run(g *global.Global) error {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
// Create Queue
|
||||||
|
queueMux := asynq.NewServeMux()
|
||||||
|
// TODO: fill
|
||||||
|
queueMux.HandleFunc(model.TypeProblemUpdate, func(ctx context.Context, t *asynq.Task) error { return nil })
|
||||||
|
queueMux.HandleFunc(model.TypeSubmitUpdate, func(ctx context.Context, t *asynq.Task) error { return nil })
|
||||||
|
queueSrv := asynq.NewServer(
|
||||||
|
asynq.RedisClientOpt{
|
||||||
|
Addr: g.Conf.Redis.Address,
|
||||||
|
Password: g.Conf.Redis.Password,
|
||||||
|
DB: g.Conf.Redis.QueueDb,
|
||||||
|
},
|
||||||
|
asynq.Config{
|
||||||
|
Concurrency: utils.If(runtime.NumCPU() > 1, runtime.NumCPU()-1, 1).(int),
|
||||||
|
Logger: zapasynq.New(g.Log),
|
||||||
|
Queues: map[string]int{model.QueueServer: 1},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// Run Queue
|
||||||
|
if err := queueSrv.Start(queueMux); err != nil {
|
||||||
|
g.Log.Fatal("queueSrv.Start Failed", zap.Error(err))
|
||||||
|
}
|
||||||
|
|
||||||
// Handle SIGINT and SIGTERM.
|
// Handle SIGINT and SIGTERM.
|
||||||
quit := make(chan os.Signal, 1)
|
quit := make(chan os.Signal, 1)
|
||||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||||
<-quit
|
<-quit
|
||||||
g.Log.Info("Shutting down server ...")
|
g.Log.Info("Shutting down server ...")
|
||||||
|
|
||||||
// Graceful Shutdown
|
// Graceful Shutdown Server
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
err := server.Shutdown(ctx)
|
err := server.Shutdown(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
g.Log.Warn("Server Shutdown Failed", zap.Error(err))
|
g.Log.Warn("Server Shutdown Failed", zap.Error(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Graceful Shutdown Queue
|
||||||
|
queueSrv.Shutdown()
|
||||||
|
|
||||||
|
// Graceful Shutdown Database
|
||||||
err = g.Db.Close()
|
err = g.Db.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
g.Log.Warn("Database Close Failed", zap.Error(err))
|
g.Log.Warn("Database Close Failed", zap.Error(err))
|
@ -1,35 +1,61 @@
|
|||||||
package e
|
package e
|
||||||
|
|
||||||
const (
|
const (
|
||||||
Success Status = 0
|
Success Status = iota
|
||||||
Unknown Status = 1
|
Unknown
|
||||||
|
)
|
||||||
|
|
||||||
InternalError Status = 100
|
const (
|
||||||
InvalidParameter Status = 101
|
InternalError Status = 100 + iota
|
||||||
NotFound Status = 102
|
InvalidParameter
|
||||||
DatabaseError Status = 103
|
NotFound
|
||||||
RedisError Status = 104
|
DatabaseError
|
||||||
|
RedisError
|
||||||
|
)
|
||||||
|
|
||||||
TokenUnknown Status = 200
|
const (
|
||||||
TokenEmpty Status = 201
|
TokenUnknown Status = 200 + iota
|
||||||
TokenMalformed Status = 202
|
TokenEmpty
|
||||||
TokenTimeError Status = 203
|
TokenMalformed
|
||||||
TokenInvalid Status = 204
|
TokenTimeError
|
||||||
TokenSignError Status = 205
|
TokenInvalid
|
||||||
TokenRevoked Status = 206
|
TokenSignError
|
||||||
|
TokenRevoked
|
||||||
|
)
|
||||||
|
|
||||||
UserNotFound Status = 300
|
const (
|
||||||
UserWrongPassword Status = 301
|
UserNotFound Status = 300 + iota
|
||||||
UserDuplicated Status = 302
|
UserWrongPassword
|
||||||
UserUnauthenticated Status = 303
|
UserDuplicated
|
||||||
UserUnauthorized Status = 304
|
UserUnauthenticated
|
||||||
UserDisabled Status = 305
|
UserUnauthorized
|
||||||
|
UserDisabled
|
||||||
|
)
|
||||||
|
|
||||||
ProblemNotFound Status = 500
|
const (
|
||||||
ProblemNotAvailable Status = 501
|
ProblemNotFound Status = 500 + iota
|
||||||
|
ProblemNotAvailable
|
||||||
|
ProblemVersionNotFound
|
||||||
|
ProblemVersionNotAvailable
|
||||||
|
StatusNotFound
|
||||||
|
)
|
||||||
|
|
||||||
TaskEnqueueFailed Status = 600
|
const (
|
||||||
TaskGetInfoFailed Status = 601
|
TaskEnqueueFailed Status = 600 + iota
|
||||||
|
TaskGetInfoFailed
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
RunnerDepsBuildFailed Status = 700 + iota
|
||||||
|
RunnerDownloadFailed
|
||||||
|
RunnerUnzipFailed
|
||||||
|
RunnerProblemNotExist
|
||||||
|
RunnerProblemPrebuildFailed
|
||||||
|
RunnerProblemParseFailed
|
||||||
|
RunnerUserNotExist
|
||||||
|
RunnerUserCompileFailed
|
||||||
|
RunnerRunFailed
|
||||||
|
RunnerJudgeFailed
|
||||||
)
|
)
|
||||||
|
|
||||||
var msgText = map[Status]string{
|
var msgText = map[Status]string{
|
||||||
@ -57,9 +83,24 @@ var msgText = map[Status]string{
|
|||||||
UserUnauthorized: "User Unauthorized",
|
UserUnauthorized: "User Unauthorized",
|
||||||
UserDisabled: "User Disabled",
|
UserDisabled: "User Disabled",
|
||||||
|
|
||||||
ProblemNotFound: "Problem Not Found",
|
ProblemNotFound: "Problem Not Found",
|
||||||
ProblemNotAvailable: "Problem Not Available",
|
ProblemNotAvailable: "Problem Not Available",
|
||||||
|
ProblemVersionNotFound: "Problem Version Not Found",
|
||||||
|
ProblemVersionNotAvailable: "Problem Version Not Available",
|
||||||
|
|
||||||
|
StatusNotFound: "Status Not Found",
|
||||||
|
|
||||||
TaskEnqueueFailed: "Task Enqueue Failed",
|
TaskEnqueueFailed: "Task Enqueue Failed",
|
||||||
TaskGetInfoFailed: "Task Get Info Failed",
|
TaskGetInfoFailed: "Task Get Info Failed",
|
||||||
|
|
||||||
|
RunnerDepsBuildFailed: "Runner Deps Build Failed",
|
||||||
|
RunnerDownloadFailed: "Runner Download Failed",
|
||||||
|
RunnerUnzipFailed: "Runner Unzip Failed",
|
||||||
|
RunnerProblemNotExist: "Runner Problem Not Exist",
|
||||||
|
RunnerProblemPrebuildFailed: "Runner Problem Prebuild Failed",
|
||||||
|
RunnerProblemParseFailed: "Runner Problem Parse Failed",
|
||||||
|
RunnerUserNotExist: "Runner User Not Exist",
|
||||||
|
RunnerUserCompileFailed: "Runner User Compile Failed",
|
||||||
|
RunnerRunFailed: "Runner Run Failed",
|
||||||
|
RunnerJudgeFailed: "Runner Judge Failed",
|
||||||
}
|
}
|
||||||
|
@ -19,5 +19,5 @@ type JwtService interface {
|
|||||||
SignClaim(claim *Claim) (string, e.Status)
|
SignClaim(claim *Claim) (string, e.Status)
|
||||||
Validate(claim *Claim) bool
|
Validate(claim *Claim) bool
|
||||||
|
|
||||||
Handler() gin.HandlerFunc
|
Handler(forced bool) gin.HandlerFunc
|
||||||
}
|
}
|
||||||
|
@ -1,6 +0,0 @@
|
|||||||
package model
|
|
||||||
|
|
||||||
const (
|
|
||||||
LangC int32 = iota
|
|
||||||
LangCPP
|
|
||||||
)
|
|
@ -1,20 +1,23 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/lib/pq"
|
"github.com/jackc/pgtype"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Problem struct {
|
type Problem struct {
|
||||||
gorm.Model `json:"meta"`
|
gorm.Model `json:"meta"`
|
||||||
Title string `json:"title" gorm:"not null"`
|
Title string `json:"title" gorm:"not null"`
|
||||||
Content string `json:"content" gorm:"not null"`
|
Statement string `json:"statement" gorm:"not null"`
|
||||||
TimeLimit uint `json:"time_limit" gorm:"not null"`
|
ProviderID uint `json:"-" gorm:"not null;index"`
|
||||||
MemoryLimit uint `json:"memory_limit" gorm:"not null"`
|
Provider User `json:"provider" gorm:"foreignKey:ProviderID"`
|
||||||
ProviderID uint `json:"provider_id" gorm:"not null;index"`
|
IsEnabled bool `json:"is_enabled" gorm:"not null;index"`
|
||||||
Provider User `json:"-" gorm:"foreignKey:ProviderID"`
|
}
|
||||||
Languages pq.Int32Array `json:"languages" gorm:"type:int[]"`
|
|
||||||
Points pq.Int32Array `json:"points" gorm:"type:int[]"`
|
type ProblemVersion struct {
|
||||||
StorageKey string `json:"storage_key" gorm:"not null"`
|
gorm.Model `json:"meta"`
|
||||||
IsEnabled bool `json:"is_enabled" gorm:"not null;index"`
|
ProblemID uint `json:"-" gorm:"not null;index"`
|
||||||
|
Context pgtype.JSON `json:"context" gorm:"type:json"`
|
||||||
|
StorageKey string `json:"-" gorm:"not null"`
|
||||||
|
IsEnabled bool `json:"is_enabled" gorm:"not null;index"`
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,16 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
import "gorm.io/gorm"
|
import (
|
||||||
|
"github.com/jackc/pgtype"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
type Status struct {
|
type Status struct {
|
||||||
gorm.Model `json:"-"`
|
gorm.Model `json:"meta"`
|
||||||
SubmissionID uint `json:"submission_id" gorm:"not null;index"`
|
SubmissionID uint `json:"-" gorm:"not null;index"`
|
||||||
Submission Submission `json:"-" gorm:"foreignKey:SubmissionID"`
|
Submission Submission `json:"submission" gorm:"foreignKey:SubmissionID"`
|
||||||
Verdict Verdict `json:"verdict" gorm:"not null"`
|
ProblemVersionID uint `json:"problem_version_id" gorm:"not null;index"`
|
||||||
Point int32 `json:"point" gorm:"not null"`
|
Context pgtype.JSON `json:"context" gorm:"type:json;not null"`
|
||||||
|
Point int32 `json:"point" gorm:"not null"`
|
||||||
|
IsEnabled bool `json:"is_enabled" gorm:"not null;index"`
|
||||||
}
|
}
|
||||||
|
@ -3,11 +3,10 @@ package model
|
|||||||
import "gorm.io/gorm"
|
import "gorm.io/gorm"
|
||||||
|
|
||||||
type Submission struct {
|
type Submission struct {
|
||||||
gorm.Model `json:"-"`
|
gorm.Model `json:"meta"`
|
||||||
ProblemID uint `json:"problem_id" gorm:"not null;index"`
|
ProblemID uint `json:"problem_id" gorm:"not null;index"`
|
||||||
Problem Problem `json:"-" gorm:"foreignKey:ProblemID"`
|
UserID uint `json:"-" gorm:"not null;index"`
|
||||||
UserID uint `json:"user_id" gorm:"not null;index"`
|
User User `json:"user" gorm:"foreignKey:UserID"`
|
||||||
User User `json:"-" gorm:"foreignKey:UserID"`
|
Language string `json:"language" gorm:"not null"`
|
||||||
Language int32 `json:"language" gorm:"not null"`
|
Code string `json:"code" gorm:"not null"`
|
||||||
Code string `json:"code" gorm:"not null"`
|
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,41 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
const (
|
import (
|
||||||
TypeProblemPush = "problem:push"
|
"github.com/WHUPRJ/woj-server/internal/e"
|
||||||
TypeSubmitJudge = "submit:judge"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type ProblemPushPayload struct {
|
const (
|
||||||
ProblemID uint
|
TypeProblemBuild = "problem:build"
|
||||||
ProblemFile string
|
TypeProblemUpdate = "problem:update"
|
||||||
|
TypeSubmitJudge = "submit:judge"
|
||||||
|
TypeSubmitUpdate = "submit:update"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
QueueServer = "server"
|
||||||
|
QueueRunner = "runner"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProblemBuildPayload struct {
|
||||||
|
ProblemVersionID uint
|
||||||
|
ProblemFile string
|
||||||
}
|
}
|
||||||
|
|
||||||
type SubmitJudge struct {
|
type ProblemUpdatePayload struct {
|
||||||
Submission Submission
|
Status e.Status
|
||||||
|
ProblemVersionID uint
|
||||||
|
Context string
|
||||||
|
}
|
||||||
|
|
||||||
|
type SubmitJudgePayload struct {
|
||||||
|
ProblemVersionId uint
|
||||||
|
StorageKey string
|
||||||
|
Submission Submission
|
||||||
|
}
|
||||||
|
|
||||||
|
type SubmitUpdatePayload struct {
|
||||||
|
Status e.Status
|
||||||
|
Sid uint
|
||||||
|
Point int32
|
||||||
|
Context string
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
gorm.Model `json:"-"`
|
gorm.Model `json:"meta"`
|
||||||
UserName string `json:"user_name" gorm:"not null;uniqueIndex"`
|
UserName string `json:"user_name" gorm:"not null;uniqueIndex"`
|
||||||
NickName string `json:"nick_name" gorm:"not null"`
|
NickName string `json:"nick_name" gorm:"not null"`
|
||||||
Role Role `json:"role" gorm:"not null"`
|
Role Role `json:"role" gorm:"not null"`
|
||||||
|
@ -1,17 +0,0 @@
|
|||||||
package model
|
|
||||||
|
|
||||||
type Verdict int
|
|
||||||
|
|
||||||
const (
|
|
||||||
VerdictJudging Verdict = iota
|
|
||||||
VerdictAccepted
|
|
||||||
VerdictWrongAnswer
|
|
||||||
VerdictTimeLimitExceeded
|
|
||||||
VerdictMemoryLimitExceeded
|
|
||||||
VerdictRuntimeError
|
|
||||||
VerdictCompileError
|
|
||||||
VerdictSystemError
|
|
||||||
VerdictJuryFailed
|
|
||||||
VerdictSkipped
|
|
||||||
VerdictPartiallyCorrect
|
|
||||||
)
|
|
@ -3,6 +3,8 @@ package router
|
|||||||
import (
|
import (
|
||||||
"github.com/WHUPRJ/woj-server/internal/api/debug"
|
"github.com/WHUPRJ/woj-server/internal/api/debug"
|
||||||
"github.com/WHUPRJ/woj-server/internal/api/problem"
|
"github.com/WHUPRJ/woj-server/internal/api/problem"
|
||||||
|
"github.com/WHUPRJ/woj-server/internal/api/status"
|
||||||
|
"github.com/WHUPRJ/woj-server/internal/api/submission"
|
||||||
"github.com/WHUPRJ/woj-server/internal/api/user"
|
"github.com/WHUPRJ/woj-server/internal/api/user"
|
||||||
"github.com/WHUPRJ/woj-server/internal/global"
|
"github.com/WHUPRJ/woj-server/internal/global"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@ -25,4 +27,6 @@ var endpoints = []global.EndpointInfo{
|
|||||||
{Version: "", Path: "/debug", Register: debug.RouteRegister},
|
{Version: "", Path: "/debug", Register: debug.RouteRegister},
|
||||||
{Version: "/v1", Path: "/user", Register: user.RouteRegister},
|
{Version: "/v1", Path: "/user", Register: user.RouteRegister},
|
||||||
{Version: "/v1", Path: "/problem", Register: problem.RouteRegister},
|
{Version: "/v1", Path: "/problem", Register: problem.RouteRegister},
|
||||||
|
{Version: "/v1", Path: "/submission", Register: submission.RouteRegister},
|
||||||
|
{Version: "/v1", Path: "/status", Register: status.RouteRegister},
|
||||||
}
|
}
|
||||||
|
@ -2,35 +2,40 @@ package jwt
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/WHUPRJ/woj-server/internal/e"
|
"github.com/WHUPRJ/woj-server/internal/e"
|
||||||
|
"github.com/WHUPRJ/woj-server/internal/global"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *service) Handler() gin.HandlerFunc {
|
func (s *service) Handler(forced bool) gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
const tokenPrefix = "bearer "
|
claim, status := func() (*global.Claim, e.Status) {
|
||||||
tokenHeader := c.GetHeader("Authorization")
|
const tokenPrefix = "bearer "
|
||||||
if tokenHeader == "" || !strings.HasPrefix(strings.ToLower(tokenHeader), tokenPrefix) {
|
tokenHeader := c.GetHeader("Authorization")
|
||||||
e.Pong(c, e.TokenEmpty, nil)
|
if tokenHeader == "" || !strings.HasPrefix(strings.ToLower(tokenHeader), tokenPrefix) {
|
||||||
c.Abort()
|
return nil, e.TokenEmpty
|
||||||
return
|
}
|
||||||
}
|
|
||||||
token := tokenHeader[len(tokenPrefix):]
|
|
||||||
|
|
||||||
claim, status := s.ParseToken(token)
|
token := tokenHeader[len(tokenPrefix):]
|
||||||
if status != e.Success {
|
claim, status := s.ParseToken(token)
|
||||||
|
if status != e.Success {
|
||||||
|
return nil, status
|
||||||
|
}
|
||||||
|
|
||||||
|
if !s.Validate(claim) {
|
||||||
|
return nil, e.TokenRevoked
|
||||||
|
}
|
||||||
|
return claim, e.Success
|
||||||
|
}()
|
||||||
|
|
||||||
|
if status == e.Success {
|
||||||
|
c.Set("claim", claim)
|
||||||
|
}
|
||||||
|
if forced && status != e.Success {
|
||||||
e.Pong(c, status, nil)
|
e.Pong(c, status, nil)
|
||||||
c.Abort()
|
c.Abort()
|
||||||
return
|
} else {
|
||||||
|
c.Next()
|
||||||
}
|
}
|
||||||
|
|
||||||
if !s.Validate(claim) {
|
|
||||||
e.Pong(c, e.TokenRevoked, nil)
|
|
||||||
c.Abort()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.Set("claim", claim)
|
|
||||||
c.Next()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,12 +6,24 @@ import (
|
|||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *service) Create(uid uint, problem *model.Problem) (*model.Problem, e.Status) {
|
type CreateData struct {
|
||||||
problem.ProviderID = uid
|
Title string
|
||||||
problem.IsEnabled = true
|
Statement string
|
||||||
|
ProviderID uint
|
||||||
|
IsEnabled bool
|
||||||
|
}
|
||||||
|
|
||||||
if err := s.db.Create(problem).Error; err != nil {
|
func (s *service) Create(data *CreateData) (*model.Problem, e.Status) {
|
||||||
s.log.Debug("create problem error", zap.Error(err), zap.Any("problem", problem))
|
problem := &model.Problem{
|
||||||
|
Title: data.Title,
|
||||||
|
Statement: data.Statement,
|
||||||
|
ProviderID: data.ProviderID,
|
||||||
|
IsEnabled: data.IsEnabled,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := s.db.Create(problem).Error
|
||||||
|
if err != nil {
|
||||||
|
s.log.Warn("DatabaseError", zap.Error(err), zap.Any("problem", problem))
|
||||||
return nil, e.DatabaseError
|
return nil, e.DatabaseError
|
||||||
}
|
}
|
||||||
|
|
||||||
|
27
internal/service/problem/createVersion.go
Normal file
27
internal/service/problem/createVersion.go
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
package problem
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/WHUPRJ/woj-server/internal/e"
|
||||||
|
"github.com/WHUPRJ/woj-server/internal/model"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CreateVersionData struct {
|
||||||
|
ProblemID uint
|
||||||
|
StorageKey string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) CreateVersion(data *CreateVersionData) (*model.ProblemVersion, e.Status) {
|
||||||
|
problemVersion := &model.ProblemVersion{
|
||||||
|
ProblemID: data.ProblemID,
|
||||||
|
StorageKey: data.StorageKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := s.db.Create(problemVersion).Error
|
||||||
|
if err != nil {
|
||||||
|
s.log.Warn("DatabaseError", zap.Error(err), zap.Any("problemVersion", problemVersion))
|
||||||
|
return nil, e.DatabaseError
|
||||||
|
}
|
||||||
|
|
||||||
|
return problemVersion, e.Success
|
||||||
|
}
|
@ -4,21 +4,29 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"github.com/WHUPRJ/woj-server/internal/e"
|
"github.com/WHUPRJ/woj-server/internal/e"
|
||||||
"github.com/WHUPRJ/woj-server/internal/model"
|
"github.com/WHUPRJ/woj-server/internal/model"
|
||||||
"github.com/WHUPRJ/woj-server/pkg/utils"
|
"go.uber.org/zap"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"gorm.io/gorm/clause"
|
"gorm.io/gorm/clause"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *service) Query(problemId uint) (*model.Problem, e.Status) {
|
func (s *service) Query(pid uint, associations bool, shouldEnable bool) (*model.Problem, e.Status) {
|
||||||
problem := new(model.Problem)
|
problem := new(model.Problem)
|
||||||
|
|
||||||
err := s.db.Preload(clause.Associations).First(&problem, problemId).Error
|
query := s.db
|
||||||
|
if associations {
|
||||||
|
query = query.Preload(clause.Associations)
|
||||||
|
}
|
||||||
|
err := query.First(&problem, pid).Error
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return nil, e.ProblemNotFound
|
return nil, e.ProblemNotFound
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
s.log.Warn("DatabaseError", zap.Error(err), zap.Any("pid", pid))
|
||||||
return nil, e.DatabaseError
|
return nil, e.DatabaseError
|
||||||
}
|
}
|
||||||
|
|
||||||
return problem, utils.If(problem.IsEnabled, e.Success, e.ProblemNotAvailable).(e.Status)
|
if shouldEnable && !problem.IsEnabled {
|
||||||
|
return nil, e.ProblemNotAvailable
|
||||||
|
}
|
||||||
|
return problem, e.Success
|
||||||
}
|
}
|
||||||
|
@ -1,25 +1,28 @@
|
|||||||
package problem
|
package problem
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"github.com/WHUPRJ/woj-server/internal/e"
|
"github.com/WHUPRJ/woj-server/internal/e"
|
||||||
"github.com/WHUPRJ/woj-server/internal/model"
|
"github.com/WHUPRJ/woj-server/internal/model"
|
||||||
"gorm.io/gorm"
|
"go.uber.org/zap"
|
||||||
"gorm.io/gorm/clause"
|
"gorm.io/gorm/clause"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *service) QueryFuzz(search string) ([]*model.Problem, e.Status) {
|
func (s *service) QueryFuzz(search string, associations bool, shouldEnable bool) ([]*model.Problem, e.Status) {
|
||||||
var problems []*model.Problem
|
problems := make([]*model.Problem, 0)
|
||||||
|
|
||||||
err := s.db.Preload(clause.Associations).
|
query := s.db
|
||||||
Where("is_enabled = true").
|
if associations {
|
||||||
Where(s.db.Where("title LIKE ?", "%"+search+"%").
|
query = query.Preload(clause.Associations)
|
||||||
Or("content LIKE ?", "%"+search+"%")).
|
|
||||||
Find(&problems).Error
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return nil, e.ProblemNotFound
|
|
||||||
}
|
}
|
||||||
|
if shouldEnable {
|
||||||
|
query = query.Where("is_enabled = true")
|
||||||
|
}
|
||||||
|
query = query.
|
||||||
|
Where(s.db.Where("title LIKE ?", "%"+search+"%").
|
||||||
|
Or("statement LIKE ?", "%"+search+"%"))
|
||||||
|
err := query.Find(&problems).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
s.log.Warn("DatabaseError", zap.Error(err), zap.Any("search", search))
|
||||||
return nil, e.DatabaseError
|
return nil, e.DatabaseError
|
||||||
}
|
}
|
||||||
|
|
||||||
|
29
internal/service/problem/queryLatestVersion.go
Normal file
29
internal/service/problem/queryLatestVersion.go
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
package problem
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"github.com/WHUPRJ/woj-server/internal/e"
|
||||||
|
"github.com/WHUPRJ/woj-server/internal/model"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *service) QueryLatestVersion(pid uint) (*model.ProblemVersion, e.Status) {
|
||||||
|
problemVersion := &model.ProblemVersion{
|
||||||
|
ProblemID: pid,
|
||||||
|
IsEnabled: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := s.db.
|
||||||
|
Where(problemVersion).
|
||||||
|
Last(&problemVersion).Error
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, e.ProblemVersionNotFound
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
s.log.Warn("DatabaseError", zap.Error(err), zap.Any("problemVersion", problemVersion))
|
||||||
|
return nil, e.DatabaseError
|
||||||
|
}
|
||||||
|
|
||||||
|
return problemVersion, e.Success
|
||||||
|
}
|
27
internal/service/problem/queryVersion.go
Normal file
27
internal/service/problem/queryVersion.go
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
package problem
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"github.com/WHUPRJ/woj-server/internal/e"
|
||||||
|
"github.com/WHUPRJ/woj-server/internal/model"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *service) QueryVersion(pvid uint, shouldEnable bool) (*model.ProblemVersion, e.Status) {
|
||||||
|
problemVersion := new(model.ProblemVersion)
|
||||||
|
|
||||||
|
err := s.db.First(&problemVersion, pvid).Error
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, e.ProblemVersionNotFound
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
s.log.Warn("DatabaseError", zap.Error(err), zap.Any("pvid", pvid))
|
||||||
|
return nil, e.DatabaseError
|
||||||
|
}
|
||||||
|
|
||||||
|
if shouldEnable && !problemVersion.IsEnabled {
|
||||||
|
return nil, e.ProblemVersionNotAvailable
|
||||||
|
}
|
||||||
|
return problemVersion, e.Success
|
||||||
|
}
|
@ -11,10 +11,15 @@ import (
|
|||||||
var _ Service = (*service)(nil)
|
var _ Service = (*service)(nil)
|
||||||
|
|
||||||
type Service interface {
|
type Service interface {
|
||||||
Create(uint, *model.Problem) (*model.Problem, e.Status)
|
Create(data *CreateData) (*model.Problem, e.Status)
|
||||||
Update(uint, *model.Problem) (*model.Problem, e.Status)
|
Update(problem *model.Problem) (*model.Problem, e.Status)
|
||||||
Query(uint) (*model.Problem, e.Status)
|
Query(pid uint, associations bool, shouldEnable bool) (*model.Problem, e.Status)
|
||||||
QueryFuzz(string) ([]*model.Problem, e.Status)
|
QueryFuzz(search string, associations bool, shouldEnable bool) ([]*model.Problem, e.Status)
|
||||||
|
|
||||||
|
CreateVersion(data *CreateVersionData) (*model.ProblemVersion, e.Status)
|
||||||
|
UpdateVersion(problemVersion *model.ProblemVersion) (*model.ProblemVersion, e.Status)
|
||||||
|
QueryVersion(pvid uint, shouldEnable bool) (*model.ProblemVersion, e.Status)
|
||||||
|
QueryLatestVersion(pid uint) (*model.ProblemVersion, e.Status)
|
||||||
}
|
}
|
||||||
|
|
||||||
type service struct {
|
type service struct {
|
||||||
|
@ -4,15 +4,12 @@ import (
|
|||||||
"github.com/WHUPRJ/woj-server/internal/e"
|
"github.com/WHUPRJ/woj-server/internal/e"
|
||||||
"github.com/WHUPRJ/woj-server/internal/model"
|
"github.com/WHUPRJ/woj-server/internal/model"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"gorm.io/gorm/clause"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *service) Update(pid uint, problem *model.Problem) (*model.Problem, e.Status) {
|
func (s *service) Update(problem *model.Problem) (*model.Problem, e.Status) {
|
||||||
if err := s.db.Clauses(clause.Returning{}).Model(problem).
|
err := s.db.Save(problem).Error
|
||||||
Where("ID = (?)", pid).
|
if err != nil {
|
||||||
Select("Title", "Content", "TimeLimit", "MemoryLimit", "IsEnabled").
|
s.log.Warn("DatabaseError", zap.Error(err), zap.Any("problem", problem))
|
||||||
Updates(problem).Error; err != nil {
|
|
||||||
s.log.Debug("update problem error", zap.Error(err), zap.Any("problem", problem))
|
|
||||||
return nil, e.DatabaseError
|
return nil, e.DatabaseError
|
||||||
}
|
}
|
||||||
|
|
||||||
|
17
internal/service/problem/updateVersion.go
Normal file
17
internal/service/problem/updateVersion.go
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
package problem
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/WHUPRJ/woj-server/internal/e"
|
||||||
|
"github.com/WHUPRJ/woj-server/internal/model"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *service) UpdateVersion(problemVersion *model.ProblemVersion) (*model.ProblemVersion, e.Status) {
|
||||||
|
err := s.db.Save(problemVersion).Error
|
||||||
|
if err != nil {
|
||||||
|
s.log.Warn("DatabaseError", zap.Error(err), zap.Any("problemVersion", problemVersion))
|
||||||
|
return nil, e.DatabaseError
|
||||||
|
}
|
||||||
|
|
||||||
|
return problemVersion, e.Success
|
||||||
|
}
|
75
internal/service/runner/common.go
Normal file
75
internal/service/runner/common.go
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
package runner
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/WHUPRJ/woj-server/internal/e"
|
||||||
|
"github.com/WHUPRJ/woj-server/pkg/utils"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
Prefix = "./resource/runner"
|
||||||
|
ProblemDir = "./problems/"
|
||||||
|
ScriptsDir = "./scripts/"
|
||||||
|
UserDir = "./users/"
|
||||||
|
TmpDir = "./tmp/"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
wd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
Prefix = path.Join(wd, Prefix)
|
||||||
|
ProblemDir = path.Join(Prefix, ProblemDir)
|
||||||
|
ScriptsDir = path.Join(Prefix, ScriptsDir)
|
||||||
|
UserDir = path.Join(Prefix, UserDir)
|
||||||
|
TmpDir = path.Join(Prefix, TmpDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) execute(script string, args ...string) error {
|
||||||
|
p := filepath.Join(ScriptsDir, script)
|
||||||
|
cmd := exec.Command(p, args...)
|
||||||
|
cmd.Dir = ScriptsDir
|
||||||
|
return cmd.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) checkAndExecute(version uint, user string, lang string, script string, fail e.Status) e.Status {
|
||||||
|
if !s.problemExists(version) {
|
||||||
|
s.log.Info("problem not exists", zap.Uint("version", version))
|
||||||
|
return e.RunnerProblemNotExist
|
||||||
|
}
|
||||||
|
|
||||||
|
if !s.userExists(user, fmt.Sprintf("%s.%s", user, lang)) {
|
||||||
|
s.log.Info("user program not exists", zap.String("user", user), zap.String("lang", lang))
|
||||||
|
return e.RunnerUserNotExist
|
||||||
|
}
|
||||||
|
|
||||||
|
err := s.execute(script, fmt.Sprintf("%d", version), fmt.Sprintf("%s", user), lang)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
s.log.Info("execute failed",
|
||||||
|
zap.Error(err),
|
||||||
|
zap.Uint("version", version),
|
||||||
|
zap.String("user", user),
|
||||||
|
zap.String("lang", lang))
|
||||||
|
return fail
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.Success
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) problemExists(version uint) bool {
|
||||||
|
problemPath := filepath.Join(ProblemDir, fmt.Sprintf("%d", version))
|
||||||
|
return utils.FileExist(problemPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) userExists(user string, file string) bool {
|
||||||
|
userPath := filepath.Join(UserDir, fmt.Sprintf("%s", user), file)
|
||||||
|
return utils.FileExist(userPath)
|
||||||
|
}
|
30
internal/service/runner/compile.go
Normal file
30
internal/service/runner/compile.go
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
package runner
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/WHUPRJ/woj-server/internal/e"
|
||||||
|
"github.com/WHUPRJ/woj-server/pkg/utils"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *service) Compile(version uint, user string, lang string) (JudgeStatus, e.Status) {
|
||||||
|
target := filepath.Join(UserDir, fmt.Sprintf("%s", user), fmt.Sprintf("%s.out", user))
|
||||||
|
|
||||||
|
_ = os.Remove(target)
|
||||||
|
status := s.checkAndExecute(version, user, lang, "problem_compile.sh", e.RunnerUserCompileFailed)
|
||||||
|
|
||||||
|
log := filepath.Join(UserDir, fmt.Sprintf("%s.compile.log", user))
|
||||||
|
msg, err := utils.FileRead(log)
|
||||||
|
msg = utils.If(err == nil, msg, nil).([]byte)
|
||||||
|
msgText := string(msg)
|
||||||
|
|
||||||
|
if utils.FileExist(target) {
|
||||||
|
return JudgeStatus{}, e.Success
|
||||||
|
} else {
|
||||||
|
return JudgeStatus{
|
||||||
|
Message: "compile failed",
|
||||||
|
Tasks: []TaskStatus{{Verdict: VerdictCompileError, Message: msgText}}},
|
||||||
|
utils.If(status == e.Success, e.RunnerUserCompileFailed, status).(e.Status)
|
||||||
|
}
|
||||||
|
}
|
125
internal/service/runner/config.go
Normal file
125
internal/service/runner/config.go
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
package runner
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Runtime struct {
|
||||||
|
TimeLimit int `json:"TimeLimit"`
|
||||||
|
MemoryLimit int `json:"MemoryLimit"`
|
||||||
|
NProcLimit int `json:"NProcLimit"`
|
||||||
|
} `json:"Runtime"`
|
||||||
|
Languages []struct {
|
||||||
|
Lang string `json:"Lang"`
|
||||||
|
Type string `json:"Type"`
|
||||||
|
Script string `json:"Script"`
|
||||||
|
Cmp string `json:"Cmp"`
|
||||||
|
} `json:"Languages"`
|
||||||
|
Tasks []struct {
|
||||||
|
Id int `json:"Id"`
|
||||||
|
Points int32 `json:"Points"`
|
||||||
|
} `json:"Tasks"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) ParseConfig(version uint, skipCheck bool) (Config, error) {
|
||||||
|
base := filepath.Join(ProblemDir, fmt.Sprintf("%d", version))
|
||||||
|
file := filepath.Join(base, "config.json")
|
||||||
|
|
||||||
|
data, err := os.ReadFile(file)
|
||||||
|
if err != nil {
|
||||||
|
return Config{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
config := Config{}
|
||||||
|
err = json.Unmarshal(data, &config)
|
||||||
|
if err != nil {
|
||||||
|
return Config{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if skipCheck {
|
||||||
|
return config, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.checkConfig(&config, base)
|
||||||
|
if err != nil {
|
||||||
|
return Config{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return config, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) checkConfig(config *Config, base string) error {
|
||||||
|
if config.Runtime.TimeLimit < 0 {
|
||||||
|
return errors.New("time limit is negative")
|
||||||
|
}
|
||||||
|
if config.Runtime.MemoryLimit < 0 {
|
||||||
|
return errors.New("memory limit is negative")
|
||||||
|
}
|
||||||
|
if config.Runtime.NProcLimit < 0 {
|
||||||
|
return errors.New("nproc limit is negative")
|
||||||
|
}
|
||||||
|
|
||||||
|
allowedLang := map[string]struct{}{
|
||||||
|
"c": {},
|
||||||
|
"cpp": {},
|
||||||
|
}
|
||||||
|
for _, lang := range config.Languages {
|
||||||
|
if _, ok := allowedLang[lang.Lang]; !ok {
|
||||||
|
return fmt.Errorf("language %s is not allowed", lang.Lang)
|
||||||
|
}
|
||||||
|
|
||||||
|
if lang.Type != "custom" && lang.Type != "default" {
|
||||||
|
return fmt.Errorf("language %s has invalid type %s", lang.Lang, lang.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
if lang.Type == "custom" {
|
||||||
|
if lang.Script == "" {
|
||||||
|
return fmt.Errorf("language %s has empty script", lang.Lang)
|
||||||
|
}
|
||||||
|
|
||||||
|
file := filepath.Join(base, "judge", lang.Script)
|
||||||
|
_, err := os.Stat(file)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("language %s has invalid script %s", lang.Lang, lang.Script)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if lang.Type == "default" {
|
||||||
|
if lang.Cmp == "" {
|
||||||
|
return fmt.Errorf("language %s has empty cmp", lang.Lang)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(config.Tasks) == 0 {
|
||||||
|
return errors.New("no tasks")
|
||||||
|
}
|
||||||
|
ids := map[int]struct{}{}
|
||||||
|
total := (1 + len(config.Tasks)) * len(config.Tasks) / 2
|
||||||
|
for _, task := range config.Tasks {
|
||||||
|
if task.Id <= 0 {
|
||||||
|
return fmt.Errorf("task %d has non-positive id", task.Id)
|
||||||
|
}
|
||||||
|
|
||||||
|
if task.Points < 0 {
|
||||||
|
return fmt.Errorf("task %d has negative points", task.Id)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := ids[task.Id]; ok {
|
||||||
|
return fmt.Errorf("task %d has duplicate id", task.Id)
|
||||||
|
}
|
||||||
|
|
||||||
|
total -= task.Id
|
||||||
|
ids[task.Id] = struct{}{}
|
||||||
|
}
|
||||||
|
if total != 0 {
|
||||||
|
return errors.New("task ids are not continuous")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
31
internal/service/runner/deps.go
Normal file
31
internal/service/runner/deps.go
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
package runner
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/WHUPRJ/woj-server/internal/e"
|
||||||
|
"github.com/WHUPRJ/woj-server/pkg/utils"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *service) EnsureDeps(force bool) e.Status {
|
||||||
|
mark := filepath.Join(Prefix, ".mark.docker")
|
||||||
|
|
||||||
|
if force {
|
||||||
|
_ = os.Remove(mark)
|
||||||
|
} else if utils.FileExist(mark) {
|
||||||
|
return e.Success
|
||||||
|
}
|
||||||
|
|
||||||
|
script := filepath.Join(ScriptsDir, "prepare_container.sh")
|
||||||
|
cmd := exec.Command(script)
|
||||||
|
cmd.Dir = ScriptsDir
|
||||||
|
err := cmd.Run()
|
||||||
|
if err != nil {
|
||||||
|
s.log.Warn("prebuild docker images failed", zap.Error(err))
|
||||||
|
return e.RunnerDepsBuildFailed
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.Success
|
||||||
|
}
|
72
internal/service/runner/newProblem.go
Normal file
72
internal/service/runner/newProblem.go
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
package runner
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/WHUPRJ/woj-server/internal/e"
|
||||||
|
"github.com/WHUPRJ/woj-server/pkg/down"
|
||||||
|
"github.com/WHUPRJ/woj-server/pkg/unzip"
|
||||||
|
"github.com/WHUPRJ/woj-server/pkg/utils"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *service) download(version uint, url string) e.Status {
|
||||||
|
zipPath := filepath.Join(TmpDir, fmt.Sprintf("%d.zip", version))
|
||||||
|
problemPath := filepath.Join(ProblemDir, fmt.Sprintf("%d", version))
|
||||||
|
|
||||||
|
err := down.Down(zipPath, url)
|
||||||
|
if err != nil {
|
||||||
|
s.log.Error("download problem failed", zap.Error(err))
|
||||||
|
return e.RunnerDownloadFailed
|
||||||
|
}
|
||||||
|
|
||||||
|
err = unzip.Unzip(zipPath, problemPath)
|
||||||
|
if err != nil {
|
||||||
|
s.log.Warn("unzip problem failed", zap.Error(err))
|
||||||
|
return e.RunnerUnzipFailed
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.Success
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) prebuild(version uint, force bool) e.Status {
|
||||||
|
if !s.problemExists(version) {
|
||||||
|
return e.RunnerProblemNotExist
|
||||||
|
}
|
||||||
|
|
||||||
|
mark := filepath.Join(ProblemDir, fmt.Sprintf("%d", version), ".mark.prebuild")
|
||||||
|
if force {
|
||||||
|
_ = os.Remove(mark)
|
||||||
|
} else if utils.FileExist(mark) {
|
||||||
|
return e.Success
|
||||||
|
}
|
||||||
|
|
||||||
|
err := s.execute("problem_prebuild.sh", fmt.Sprintf("%d", version))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
s.log.Warn("prebuild problem failed", zap.Error(err), zap.Uint("version", version))
|
||||||
|
return e.RunnerProblemPrebuildFailed
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.Success
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) NewProblem(version uint, url string) (Config, e.Status) {
|
||||||
|
status := s.download(version, url)
|
||||||
|
if status != e.Success {
|
||||||
|
return Config{}, status
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg, err := s.ParseConfig(version, false)
|
||||||
|
if err != nil {
|
||||||
|
return Config{}, e.RunnerProblemParseFailed
|
||||||
|
}
|
||||||
|
|
||||||
|
status = s.prebuild(version, true)
|
||||||
|
if status != e.Success {
|
||||||
|
return Config{}, status
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfg, e.Success
|
||||||
|
}
|
22
internal/service/runner/runAndJudge.go
Normal file
22
internal/service/runner/runAndJudge.go
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
package runner
|
||||||
|
|
||||||
|
import "github.com/WHUPRJ/woj-server/internal/e"
|
||||||
|
|
||||||
|
func (s *service) RunAndJudge(version uint, user string, lang string, config *Config) (JudgeStatus, int32, e.Status) {
|
||||||
|
// run user program
|
||||||
|
status := s.checkAndExecute(version, user, lang, "problem_run.sh", e.RunnerRunFailed)
|
||||||
|
if status != e.Success {
|
||||||
|
return JudgeStatus{Message: "run failed"}, 0, status
|
||||||
|
}
|
||||||
|
|
||||||
|
// run judger
|
||||||
|
status = s.checkAndExecute(version, user, lang, "problem_judge.sh", e.RunnerJudgeFailed)
|
||||||
|
if status != e.Success {
|
||||||
|
return JudgeStatus{Message: "judge failed"}, 0, status
|
||||||
|
}
|
||||||
|
|
||||||
|
// check result
|
||||||
|
result, pts := s.checkResults(user, config)
|
||||||
|
|
||||||
|
return result, pts, e.Success
|
||||||
|
}
|
34
internal/service/runner/service.go
Normal file
34
internal/service/runner/service.go
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
package runner
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/WHUPRJ/woj-server/internal/e"
|
||||||
|
"github.com/WHUPRJ/woj-server/internal/global"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ Service = (*service)(nil)
|
||||||
|
|
||||||
|
type Service interface {
|
||||||
|
// EnsureDeps build docker images
|
||||||
|
EnsureDeps(force bool) e.Status
|
||||||
|
// NewProblem = Download + Parse + Prebuild
|
||||||
|
NewProblem(version uint, url string) (Config, e.Status)
|
||||||
|
|
||||||
|
// Compile compile user submission
|
||||||
|
Compile(version uint, user string, lang string) (JudgeStatus, e.Status)
|
||||||
|
// RunAndJudge execute user program
|
||||||
|
RunAndJudge(version uint, user string, lang string, config *Config) (JudgeStatus, int32, e.Status)
|
||||||
|
|
||||||
|
// ParseConfig parse config file
|
||||||
|
ParseConfig(version uint, skipCheck bool) (Config, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type service struct {
|
||||||
|
log *zap.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewService(g *global.Global) Service {
|
||||||
|
return &service{
|
||||||
|
log: g.Log,
|
||||||
|
}
|
||||||
|
}
|
227
internal/service/runner/status.go
Normal file
227
internal/service/runner/status.go
Normal file
@ -0,0 +1,227 @@
|
|||||||
|
package runner
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"encoding/xml"
|
||||||
|
"fmt"
|
||||||
|
"github.com/WHUPRJ/woj-server/pkg/utils"
|
||||||
|
"golang.org/x/text/encoding/charmap"
|
||||||
|
"io"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
VerdictAccepted = iota
|
||||||
|
VerdictWrongAnswer
|
||||||
|
VerdictJuryFailed
|
||||||
|
VerdictPartialCorrect
|
||||||
|
VerdictTimeLimitExceeded
|
||||||
|
VerdictMemoryLimitExceeded
|
||||||
|
VerdictRuntimeError
|
||||||
|
VerdictCompileError
|
||||||
|
VerdictSystemError
|
||||||
|
)
|
||||||
|
|
||||||
|
type TestLibReport struct {
|
||||||
|
XMLName xml.Name `xml:"result"`
|
||||||
|
Outcome string `xml:"outcome,attr"`
|
||||||
|
PCType int `xml:"pctype,attr"`
|
||||||
|
Points float64 `xml:"points,attr"`
|
||||||
|
Result string `xml:",chardata"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TaskStatus struct {
|
||||||
|
Id int `json:"id"`
|
||||||
|
Points int32 `json:"points"`
|
||||||
|
RealTime int `json:"real_time"`
|
||||||
|
CpuTime int `json:"cpu_time"`
|
||||||
|
Memory int `json:"memory"`
|
||||||
|
Verdict int `json:"verdict"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
|
||||||
|
infoText []byte
|
||||||
|
info map[string]interface{}
|
||||||
|
judgeText string
|
||||||
|
judge TestLibReport
|
||||||
|
}
|
||||||
|
|
||||||
|
type JudgeStatus struct {
|
||||||
|
Message string `json:"message"`
|
||||||
|
Tasks []TaskStatus `json:"tasks"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TaskStatus) getInfoText(infoFile string) *TaskStatus {
|
||||||
|
if t.Verdict != VerdictAccepted {
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
t.infoText, err = utils.FileRead(infoFile)
|
||||||
|
if err != nil {
|
||||||
|
t.Verdict = VerdictSystemError
|
||||||
|
t.Message = "cannot read info file"
|
||||||
|
}
|
||||||
|
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TaskStatus) getInfo() *TaskStatus {
|
||||||
|
if t.Verdict != VerdictAccepted {
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
err := json.Unmarshal(t.infoText, &t.info)
|
||||||
|
if err != nil {
|
||||||
|
t.Verdict = VerdictSystemError
|
||||||
|
t.Message = "cannot parse info file"
|
||||||
|
} else {
|
||||||
|
t.RealTime = int(t.info["real_time"].(float64))
|
||||||
|
t.CpuTime = int(t.info["cpu_time"].(float64))
|
||||||
|
t.Memory = int(t.info["memory"].(float64))
|
||||||
|
}
|
||||||
|
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TaskStatus) checkExit() *TaskStatus {
|
||||||
|
if t.Verdict != VerdictAccepted {
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.info["status"] != "exited" || t.info["code"] != 0.0 {
|
||||||
|
t.Verdict = VerdictRuntimeError
|
||||||
|
t.Message = fmt.Sprintf("status: %v, code: %v", t.info["status"], t.info["code"])
|
||||||
|
}
|
||||||
|
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TaskStatus) checkTime(config *Config) *TaskStatus {
|
||||||
|
if t.Verdict != VerdictAccepted {
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.info["real_time"].(float64) > float64(config.Runtime.TimeLimit)+5 {
|
||||||
|
t.Verdict = VerdictTimeLimitExceeded
|
||||||
|
t.Message = fmt.Sprintf("real_time: %v cpu_time: %v", t.info["real_time"], t.info["cpu_time"])
|
||||||
|
}
|
||||||
|
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TaskStatus) checkMemory(config *Config) *TaskStatus {
|
||||||
|
if t.Verdict != VerdictAccepted {
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.info["memory"].(float64) > float64((config.Runtime.MemoryLimit+1)*1024) {
|
||||||
|
t.Verdict = VerdictMemoryLimitExceeded
|
||||||
|
t.Message = fmt.Sprintf("memory: %v", t.info["memory"])
|
||||||
|
}
|
||||||
|
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TaskStatus) getJudgeText(judgeFile string) *TaskStatus {
|
||||||
|
if t.Verdict != VerdictAccepted {
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
j, err := utils.FileRead(judgeFile)
|
||||||
|
if err != nil {
|
||||||
|
t.Verdict = VerdictSystemError
|
||||||
|
t.Message = "cannot read judge file"
|
||||||
|
} else {
|
||||||
|
t.judgeText = string(j)
|
||||||
|
}
|
||||||
|
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TaskStatus) getJudge() *TaskStatus {
|
||||||
|
if t.Verdict != VerdictAccepted {
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
b := bytes.NewReader([]byte(t.judgeText))
|
||||||
|
d := xml.NewDecoder(b)
|
||||||
|
d.CharsetReader = func(charset string, input io.Reader) (io.Reader, error) {
|
||||||
|
switch charset {
|
||||||
|
case "windows-1251":
|
||||||
|
return charmap.Windows1251.NewDecoder().Reader(input), nil
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unknown charset: %s", charset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err := d.Decode(&t.judge)
|
||||||
|
if err != nil {
|
||||||
|
t.Verdict = VerdictSystemError
|
||||||
|
t.Message = "cannot parse judge file"
|
||||||
|
}
|
||||||
|
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TaskStatus) checkJudge(pts *map[int]int32) *TaskStatus {
|
||||||
|
if t.Verdict != VerdictAccepted {
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
mp := map[string]int{
|
||||||
|
"accepted": VerdictAccepted,
|
||||||
|
"wrong-answer": VerdictWrongAnswer,
|
||||||
|
"presentation-error": VerdictWrongAnswer,
|
||||||
|
"points": VerdictPartialCorrect,
|
||||||
|
"relative-scoring": VerdictPartialCorrect,
|
||||||
|
}
|
||||||
|
|
||||||
|
if v, ok := mp[t.judge.Outcome]; ok {
|
||||||
|
t.Verdict = v
|
||||||
|
t.Message = t.judge.Result
|
||||||
|
if v == VerdictAccepted {
|
||||||
|
t.Points = (*pts)[t.Id]
|
||||||
|
} else if v == VerdictPartialCorrect {
|
||||||
|
t.Points = int32(t.judge.Points) + int32(t.judge.PCType)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
t.Verdict = VerdictJuryFailed
|
||||||
|
t.Message = fmt.Sprintf("unknown outcome: %v, result: %v", t.judge.Outcome, t.judge.Result)
|
||||||
|
}
|
||||||
|
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) checkResults(user string, config *Config) (JudgeStatus, int32) {
|
||||||
|
// CE will be processed in phase compile
|
||||||
|
|
||||||
|
pts := map[int]int32{}
|
||||||
|
for _, task := range config.Tasks {
|
||||||
|
pts[task.Id] = task.Points
|
||||||
|
}
|
||||||
|
|
||||||
|
var results []TaskStatus
|
||||||
|
dir := filepath.Join(UserDir, fmt.Sprintf("%s", user))
|
||||||
|
var sum int32 = 0
|
||||||
|
|
||||||
|
for i := 1; i <= len(config.Tasks); i++ {
|
||||||
|
result := TaskStatus{Id: i, Verdict: VerdictAccepted, Points: 0}
|
||||||
|
|
||||||
|
info := filepath.Join(dir, fmt.Sprintf("%d.info", i))
|
||||||
|
judge := filepath.Join(dir, fmt.Sprintf("%d.judge", i))
|
||||||
|
|
||||||
|
result.getInfoText(info).
|
||||||
|
getInfo().
|
||||||
|
checkExit().
|
||||||
|
checkTime(config).
|
||||||
|
checkMemory(config).
|
||||||
|
getJudgeText(judge).
|
||||||
|
getJudge().
|
||||||
|
checkJudge(&pts)
|
||||||
|
|
||||||
|
sum += result.Points
|
||||||
|
}
|
||||||
|
|
||||||
|
return JudgeStatus{Message: "", Tasks: results}, sum
|
||||||
|
}
|
33
internal/service/status/create.go
Normal file
33
internal/service/status/create.go
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
package status
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/WHUPRJ/woj-server/internal/e"
|
||||||
|
"github.com/WHUPRJ/woj-server/internal/model"
|
||||||
|
"github.com/jackc/pgtype"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CreateData struct {
|
||||||
|
SubmissionID uint
|
||||||
|
ProblemVersionID uint
|
||||||
|
Context pgtype.JSON
|
||||||
|
Point int32
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s service) Create(data *model.Status) (*model.Status, e.Status) {
|
||||||
|
status := &model.Status{
|
||||||
|
SubmissionID: data.SubmissionID,
|
||||||
|
ProblemVersionID: data.ProblemVersionID,
|
||||||
|
Context: data.Context,
|
||||||
|
Point: data.Point,
|
||||||
|
IsEnabled: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := s.db.Create(status).Error
|
||||||
|
if err != nil {
|
||||||
|
s.log.Warn("DatabaseError", zap.Error(err), zap.Any("status", status))
|
||||||
|
return nil, e.DatabaseError
|
||||||
|
}
|
||||||
|
|
||||||
|
return status, e.Success
|
||||||
|
}
|
56
internal/service/status/query.go
Normal file
56
internal/service/status/query.go
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
package status
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"github.com/WHUPRJ/woj-server/internal/e"
|
||||||
|
"github.com/WHUPRJ/woj-server/internal/model"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/clause"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s service) Query(sid uint, associations bool) (*model.Status, e.Status) {
|
||||||
|
|
||||||
|
status := &model.Status{
|
||||||
|
SubmissionID: sid,
|
||||||
|
IsEnabled: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
query := s.db
|
||||||
|
if associations {
|
||||||
|
query = query.Preload(clause.Associations)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := query.
|
||||||
|
Where(status).
|
||||||
|
Last(&status).Error
|
||||||
|
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, e.StatusNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
s.log.Warn("DatabaseError", zap.Error(err), zap.Any("status", status))
|
||||||
|
return nil, e.DatabaseError
|
||||||
|
}
|
||||||
|
return status, e.Success
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s service) QueryByVersion(pvid uint, offset int, limit int) ([]*model.Status, e.Status) {
|
||||||
|
var statuses []*model.Status
|
||||||
|
status := &model.Status{
|
||||||
|
ProblemVersionID: pvid,
|
||||||
|
IsEnabled: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := s.db.Preload(clause.Associations).
|
||||||
|
Where(status).
|
||||||
|
Limit(limit).
|
||||||
|
Offset(offset).
|
||||||
|
Find(&statuses).Error
|
||||||
|
if err != nil {
|
||||||
|
s.log.Warn("DatabaseError", zap.Error(err), zap.Any("status", status))
|
||||||
|
return nil, e.DatabaseError
|
||||||
|
}
|
||||||
|
return statuses, e.Success
|
||||||
|
}
|
11
internal/service/status/rejudge.go
Normal file
11
internal/service/status/rejudge.go
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
package status
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/WHUPRJ/woj-server/internal/e"
|
||||||
|
"github.com/WHUPRJ/woj-server/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s service) Rejudge(statusID uint) ([]*model.Status, e.Status) {
|
||||||
|
//TODO implement me
|
||||||
|
panic("implement me")
|
||||||
|
}
|
30
internal/service/status/service.go
Normal file
30
internal/service/status/service.go
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
package status
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/WHUPRJ/woj-server/internal/e"
|
||||||
|
"github.com/WHUPRJ/woj-server/internal/global"
|
||||||
|
"github.com/WHUPRJ/woj-server/internal/model"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ Service = (*service)(nil)
|
||||||
|
|
||||||
|
type Service interface {
|
||||||
|
Create(*model.Status) (*model.Status, e.Status)
|
||||||
|
Query(sid uint, associations bool) (*model.Status, e.Status)
|
||||||
|
QueryByVersion(pvid uint, offset int, limit int) ([]*model.Status, e.Status)
|
||||||
|
Rejudge(statusID uint) ([]*model.Status, e.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
type service struct {
|
||||||
|
log *zap.Logger
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewService(g *global.Global) Service {
|
||||||
|
return &service{
|
||||||
|
log: g.Log,
|
||||||
|
db: g.Db.Get().(*gorm.DB),
|
||||||
|
}
|
||||||
|
}
|
31
internal/service/submission/create.go
Normal file
31
internal/service/submission/create.go
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
package submission
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/WHUPRJ/woj-server/internal/e"
|
||||||
|
"github.com/WHUPRJ/woj-server/internal/model"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CreateData struct {
|
||||||
|
ProblemID uint
|
||||||
|
UserID uint
|
||||||
|
Language string
|
||||||
|
Code string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) Create(data *CreateData) (*model.Submission, e.Status) {
|
||||||
|
submission := &model.Submission{
|
||||||
|
ProblemID: data.ProblemID,
|
||||||
|
UserID: data.UserID,
|
||||||
|
Language: data.Language,
|
||||||
|
Code: data.Code,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := s.db.Create(submission).Error
|
||||||
|
if err != nil {
|
||||||
|
s.log.Warn("DatabaseError", zap.Error(err), zap.Any("submission", submission))
|
||||||
|
return nil, e.DatabaseError
|
||||||
|
}
|
||||||
|
|
||||||
|
return submission, e.Success
|
||||||
|
}
|
33
internal/service/submission/query.go
Normal file
33
internal/service/submission/query.go
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
package submission
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/WHUPRJ/woj-server/internal/e"
|
||||||
|
"github.com/WHUPRJ/woj-server/internal/model"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"gorm.io/gorm/clause"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *service) Query(pid uint, uid uint, offset int, limit int) ([]*model.Submission, e.Status) {
|
||||||
|
submissions := make([]*model.Submission, 0)
|
||||||
|
|
||||||
|
submission := &model.Submission{
|
||||||
|
ProblemID: pid,
|
||||||
|
UserID: uid,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := s.db.Preload(clause.Associations).
|
||||||
|
Where(submission).
|
||||||
|
Limit(limit).
|
||||||
|
Offset(offset).
|
||||||
|
Find(&submissions).Error
|
||||||
|
|
||||||
|
//if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
// return nil, e.ProblemNotFound
|
||||||
|
//}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
s.log.Warn("DatabaseError", zap.Error(err), zap.Any("pid", pid), zap.Any("uid", uid))
|
||||||
|
return nil, e.DatabaseError
|
||||||
|
}
|
||||||
|
return submissions, e.Success
|
||||||
|
}
|
28
internal/service/submission/service.go
Normal file
28
internal/service/submission/service.go
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
package submission
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/WHUPRJ/woj-server/internal/e"
|
||||||
|
"github.com/WHUPRJ/woj-server/internal/global"
|
||||||
|
"github.com/WHUPRJ/woj-server/internal/model"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ Service = (*service)(nil)
|
||||||
|
|
||||||
|
type Service interface {
|
||||||
|
Create(data *CreateData) (*model.Submission, e.Status)
|
||||||
|
Query(pid uint, uid uint, offset int, limit int) ([]*model.Submission, e.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
type service struct {
|
||||||
|
log *zap.Logger
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewService(g *global.Global) Service {
|
||||||
|
return &service{
|
||||||
|
log: g.Log,
|
||||||
|
db: g.Db.Get().(*gorm.DB),
|
||||||
|
}
|
||||||
|
}
|
@ -6,10 +6,10 @@ import (
|
|||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *service) submit(typename string, payload []byte) (*asynq.TaskInfo, e.Status) {
|
func (s *service) submit(typename string, payload []byte, queue string) (*asynq.TaskInfo, e.Status) {
|
||||||
task := asynq.NewTask(typename, payload)
|
task := asynq.NewTask(typename, payload)
|
||||||
|
|
||||||
info, err := s.queue.Enqueue(task)
|
info, err := s.queue.Enqueue(task, asynq.Queue(queue))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.log.Warn("failed to enqueue task", zap.Error(err), zap.Any("task", task))
|
s.log.Warn("failed to enqueue task", zap.Error(err), zap.Any("task", task))
|
||||||
return nil, e.TaskEnqueueFailed
|
return nil, e.TaskEnqueueFailed
|
||||||
@ -20,8 +20,8 @@ func (s *service) submit(typename string, payload []byte) (*asynq.TaskInfo, e.St
|
|||||||
return info, e.Success
|
return info, e.Success
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *service) GetTaskInfo(id string) (*asynq.TaskInfo, e.Status) {
|
func (s *service) GetTaskInfo(id string, queue string) (*asynq.TaskInfo, e.Status) {
|
||||||
task, err := s.inspector.GetTaskInfo("default", id)
|
task, err := s.inspector.GetTaskInfo(queue, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.log.Debug("get task info failed", zap.Error(err), zap.String("id", id))
|
s.log.Debug("get task info failed", zap.Error(err), zap.String("id", id))
|
||||||
return nil, e.TaskGetInfoFailed
|
return nil, e.TaskGetInfoFailed
|
||||||
|
@ -1,20 +0,0 @@
|
|||||||
package task
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"github.com/WHUPRJ/woj-server/internal/e"
|
|
||||||
"github.com/WHUPRJ/woj-server/internal/model"
|
|
||||||
"go.uber.org/zap"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (s *service) NewJudge(submission model.Submission) (string, e.Status) {
|
|
||||||
payload, err := json.Marshal(model.SubmitJudge{Submission: submission})
|
|
||||||
if err != nil {
|
|
||||||
s.log.Warn("json marshal error", zap.Error(err), zap.Any("payload", submission))
|
|
||||||
return "", e.InternalError
|
|
||||||
}
|
|
||||||
|
|
||||||
info, status := s.submit(model.TypeSubmitJudge, payload)
|
|
||||||
|
|
||||||
return info.ID, status
|
|
||||||
}
|
|
46
internal/service/task/problem.go
Normal file
46
internal/service/task/problem.go
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
package task
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"github.com/WHUPRJ/woj-server/internal/e"
|
||||||
|
"github.com/WHUPRJ/woj-server/internal/model"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *service) ProblemBuild(pvId uint, file string) (string, e.Status) {
|
||||||
|
payload, err := json.Marshal(model.ProblemBuildPayload{
|
||||||
|
ProblemVersionID: pvId,
|
||||||
|
ProblemFile: file,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
s.log.Warn("json marshal error",
|
||||||
|
zap.Error(err),
|
||||||
|
zap.Any("ProblemVersionID", pvId),
|
||||||
|
zap.String("ProblemFile", file))
|
||||||
|
return "", e.InternalError
|
||||||
|
}
|
||||||
|
|
||||||
|
info, status := s.submit(model.TypeProblemBuild, payload, model.QueueRunner)
|
||||||
|
|
||||||
|
return info.ID, status
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) ProblemUpdate(status e.Status, pvId uint, ctx string) (string, e.Status) {
|
||||||
|
payload, err := json.Marshal(model.ProblemUpdatePayload{
|
||||||
|
Status: status,
|
||||||
|
ProblemVersionID: pvId,
|
||||||
|
Context: ctx,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
s.log.Warn("json marshal error",
|
||||||
|
zap.Error(err),
|
||||||
|
zap.Any("Status", status),
|
||||||
|
zap.Any("ProblemVersionID", pvId),
|
||||||
|
zap.Any("Context", ctx))
|
||||||
|
return "", e.InternalError
|
||||||
|
}
|
||||||
|
|
||||||
|
info, status := s.submit(model.TypeProblemUpdate, payload, model.QueueServer)
|
||||||
|
|
||||||
|
return info.ID, status
|
||||||
|
}
|
@ -1,23 +0,0 @@
|
|||||||
package task
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"github.com/WHUPRJ/woj-server/internal/e"
|
|
||||||
"github.com/WHUPRJ/woj-server/internal/model"
|
|
||||||
"go.uber.org/zap"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (s *service) PushProblem(id uint, file string) (string, e.Status) {
|
|
||||||
payload, err := json.Marshal(model.ProblemPushPayload{
|
|
||||||
ProblemID: id,
|
|
||||||
ProblemFile: file,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
s.log.Warn("json marshal error", zap.Error(err), zap.Any("id", id), zap.String("file", file))
|
|
||||||
return "", e.InternalError
|
|
||||||
}
|
|
||||||
|
|
||||||
info, status := s.submit(model.TypeSubmitJudge, payload)
|
|
||||||
|
|
||||||
return info.ID, status
|
|
||||||
}
|
|
@ -4,6 +4,7 @@ import (
|
|||||||
"github.com/WHUPRJ/woj-server/internal/e"
|
"github.com/WHUPRJ/woj-server/internal/e"
|
||||||
"github.com/WHUPRJ/woj-server/internal/global"
|
"github.com/WHUPRJ/woj-server/internal/global"
|
||||||
"github.com/WHUPRJ/woj-server/internal/model"
|
"github.com/WHUPRJ/woj-server/internal/model"
|
||||||
|
"github.com/WHUPRJ/woj-server/internal/service/runner"
|
||||||
"github.com/hibiken/asynq"
|
"github.com/hibiken/asynq"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
@ -11,10 +12,12 @@ import (
|
|||||||
var _ Service = (*service)(nil)
|
var _ Service = (*service)(nil)
|
||||||
|
|
||||||
type Service interface {
|
type Service interface {
|
||||||
NewJudge(submission model.Submission) (string, e.Status)
|
ProblemBuild(pvId uint, file string) (string, e.Status)
|
||||||
PushProblem(id uint, file string) (string, e.Status)
|
ProblemUpdate(status e.Status, pvId uint, ctx string) (string, e.Status)
|
||||||
GetTaskInfo(id string) (*asynq.TaskInfo, e.Status)
|
SubmitJudge(pvid uint, storageKey string, submission model.Submission) (string, e.Status)
|
||||||
submit(typename string, payload []byte) (*asynq.TaskInfo, e.Status)
|
SubmitUpdate(status e.Status, sid uint, point int32, ctx runner.JudgeStatus) (string, e.Status)
|
||||||
|
|
||||||
|
GetTaskInfo(string, string) (*asynq.TaskInfo, e.Status)
|
||||||
}
|
}
|
||||||
|
|
||||||
type service struct {
|
type service struct {
|
||||||
|
55
internal/service/task/submit.go
Normal file
55
internal/service/task/submit.go
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
package task
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"github.com/WHUPRJ/woj-server/internal/e"
|
||||||
|
"github.com/WHUPRJ/woj-server/internal/model"
|
||||||
|
"github.com/WHUPRJ/woj-server/internal/service/runner"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *service) SubmitJudge(pvid uint, storageKey string, submission model.Submission) (string, e.Status) {
|
||||||
|
payload, err := json.Marshal(
|
||||||
|
model.SubmitJudgePayload{
|
||||||
|
ProblemVersionId: pvid,
|
||||||
|
StorageKey: storageKey,
|
||||||
|
Submission: submission,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
s.log.Warn("json marshal error", zap.Error(err), zap.Any("Submission", submission))
|
||||||
|
return "", e.InternalError
|
||||||
|
}
|
||||||
|
|
||||||
|
info, status := s.submit(model.TypeSubmitJudge, payload, model.QueueRunner)
|
||||||
|
|
||||||
|
return info.ID, status
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) SubmitUpdate(status e.Status, sid uint, point int32, ctx runner.JudgeStatus) (string, e.Status) {
|
||||||
|
ctxText, err := json.Marshal(ctx)
|
||||||
|
if err != nil {
|
||||||
|
s.log.Warn("json marshal error",
|
||||||
|
zap.Error(err),
|
||||||
|
zap.Any("ctx", ctx))
|
||||||
|
return "", e.InternalError
|
||||||
|
}
|
||||||
|
|
||||||
|
payload, err := json.Marshal(model.SubmitUpdatePayload{
|
||||||
|
Status: status,
|
||||||
|
Sid: sid,
|
||||||
|
Point: point,
|
||||||
|
Context: string(ctxText),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
s.log.Warn("json marshal error",
|
||||||
|
zap.Error(err),
|
||||||
|
zap.Any("Status", status),
|
||||||
|
zap.Int32("Point", point),
|
||||||
|
zap.Any("Context", ctx))
|
||||||
|
return "", e.InternalError
|
||||||
|
}
|
||||||
|
|
||||||
|
info, status := s.submit(model.TypeSubmitUpdate, payload, model.QueueServer)
|
||||||
|
|
||||||
|
return info.ID, status
|
||||||
|
}
|
@ -9,31 +9,32 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type CreateData struct {
|
type CreateData struct {
|
||||||
Username string
|
UserName string
|
||||||
Nickname string
|
|
||||||
Password string
|
Password string
|
||||||
|
NickName string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *service) Create(data *CreateData) (*model.User, e.Status) {
|
func (s *service) Create(data *CreateData) (*model.User, e.Status) {
|
||||||
hashed, err := bcrypt.GenerateFromPassword([]byte(data.Password), bcrypt.DefaultCost)
|
hashed, err := bcrypt.GenerateFromPassword([]byte(data.Password), bcrypt.DefaultCost)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.log.Debug("bcrypt error", zap.Error(err), zap.String("password", data.Password))
|
s.log.Warn("BcryptError", zap.Error(err), zap.String("password", data.Password))
|
||||||
return nil, e.InternalError
|
return nil, e.InternalError
|
||||||
}
|
}
|
||||||
|
|
||||||
user := &model.User{
|
user := &model.User{
|
||||||
UserName: data.Username,
|
UserName: data.UserName,
|
||||||
Password: hashed,
|
Password: hashed,
|
||||||
NickName: data.Nickname,
|
NickName: data.NickName,
|
||||||
Role: model.RoleGeneral,
|
Role: model.RoleGeneral,
|
||||||
IsEnabled: true,
|
IsEnabled: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.db.Create(user).Error; err != nil {
|
err = s.db.Create(user).Error
|
||||||
if strings.Contains(err.Error(), "duplicate key") {
|
if err != nil && strings.Contains(err.Error(), "duplicate key") {
|
||||||
return nil, e.UserDuplicated
|
return nil, e.UserDuplicated
|
||||||
}
|
}
|
||||||
s.log.Debug("create user error", zap.Error(err), zap.Any("data", data))
|
if err != nil {
|
||||||
|
s.log.Warn("DatabaseError", zap.Error(err), zap.Any("user", user))
|
||||||
return nil, e.DatabaseError
|
return nil, e.DatabaseError
|
||||||
}
|
}
|
||||||
return user, e.Success
|
return user, e.Success
|
||||||
|
@ -4,11 +4,17 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"github.com/WHUPRJ/woj-server/internal/e"
|
"github.com/WHUPRJ/woj-server/internal/e"
|
||||||
"github.com/WHUPRJ/woj-server/internal/model"
|
"github.com/WHUPRJ/woj-server/internal/model"
|
||||||
|
"go.uber.org/zap"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *service) Login(data *model.User) (*model.User, e.Status) {
|
type LoginData struct {
|
||||||
|
UserName string
|
||||||
|
Password string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) Login(data *LoginData) (*model.User, e.Status) {
|
||||||
user := &model.User{UserName: data.UserName}
|
user := &model.User{UserName: data.UserName}
|
||||||
|
|
||||||
err := s.db.Where(user).First(&user).Error
|
err := s.db.Where(user).First(&user).Error
|
||||||
@ -16,6 +22,7 @@ func (s *service) Login(data *model.User) (*model.User, e.Status) {
|
|||||||
return nil, e.UserNotFound
|
return nil, e.UserNotFound
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
s.log.Warn("DatabaseError", zap.Error(err), zap.Any("user", user))
|
||||||
return nil, e.DatabaseError
|
return nil, e.DatabaseError
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -23,7 +30,7 @@ func (s *service) Login(data *model.User) (*model.User, e.Status) {
|
|||||||
return nil, e.UserDisabled
|
return nil, e.UserDisabled
|
||||||
}
|
}
|
||||||
|
|
||||||
err = bcrypt.CompareHashAndPassword(user.Password, data.Password)
|
err = bcrypt.CompareHashAndPassword(user.Password, []byte(data.Password))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, e.UserWrongPassword
|
return nil, e.UserWrongPassword
|
||||||
}
|
}
|
||||||
|
@ -4,17 +4,19 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"github.com/WHUPRJ/woj-server/internal/e"
|
"github.com/WHUPRJ/woj-server/internal/e"
|
||||||
"github.com/WHUPRJ/woj-server/internal/model"
|
"github.com/WHUPRJ/woj-server/internal/model"
|
||||||
|
"go.uber.org/zap"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *service) Profile(id uint) (*model.User, e.Status) {
|
func (s *service) Profile(uid uint) (*model.User, e.Status) {
|
||||||
user := new(model.User)
|
user := new(model.User)
|
||||||
|
|
||||||
err := s.db.First(&user, id).Error
|
err := s.db.First(&user, uid).Error
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return nil, e.UserNotFound
|
return nil, e.UserNotFound
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
s.log.Warn("DatabaseError", zap.Error(err), zap.Any("uid", uid))
|
||||||
return nil, e.DatabaseError
|
return nil, e.DatabaseError
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13,9 +13,9 @@ var _ Service = (*service)(nil)
|
|||||||
|
|
||||||
type Service interface {
|
type Service interface {
|
||||||
Create(data *CreateData) (*model.User, e.Status)
|
Create(data *CreateData) (*model.User, e.Status)
|
||||||
Login(data *model.User) (*model.User, e.Status)
|
Login(data *LoginData) (*model.User, e.Status)
|
||||||
IncrVersion(id uint) (int64, e.Status)
|
IncrVersion(uid uint) (int64, e.Status)
|
||||||
Profile(id uint) (*model.User, e.Status)
|
Profile(uid uint) (*model.User, e.Status)
|
||||||
}
|
}
|
||||||
|
|
||||||
type service struct {
|
type service struct {
|
||||||
|
@ -7,11 +7,12 @@ import (
|
|||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *service) IncrVersion(id uint) (int64, e.Status) {
|
func (s *service) IncrVersion(uid uint) (int64, e.Status) {
|
||||||
version, err := s.redis.Incr(context.Background(), fmt.Sprintf("Version:%d", id)).Result()
|
version, err := s.redis.Incr(context.Background(), fmt.Sprintf("Version:%d", uid)).Result()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.log.Debug("redis.Incr error", zap.Error(err))
|
s.log.Warn("RedisError", zap.Error(err), zap.Any("uid", uid))
|
||||||
return -1, e.RedisError
|
return -1, e.RedisError
|
||||||
}
|
}
|
||||||
|
|
||||||
return version, e.Success
|
return version, e.Success
|
||||||
}
|
}
|
||||||
|
55
pkg/down/down.go
Normal file
55
pkg/down/down.go
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
package down
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/WHUPRJ/woj-server/pkg/utils"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Down(dest string, url string) error {
|
||||||
|
dir := filepath.Dir(dest)
|
||||||
|
err := os.MkdirAll(dir, 0755)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
tmp := fmt.Sprintf("%s.%s", dest, utils.RandomString(5))
|
||||||
|
f, err := os.Create(tmp)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := http.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer func(Body io.ReadCloser) {
|
||||||
|
_ = Body.Close()
|
||||||
|
}(resp.Body)
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
_ = os.Remove(tmp)
|
||||||
|
return fmt.Errorf("bad status %s when accessing %s", resp.Status, url)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = io.Copy(f, resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = f.Close()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = os.Rename(tmp, dest)
|
||||||
|
if err != nil {
|
||||||
|
_ = os.Remove(dest)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
60
pkg/unzip/unzip.go
Normal file
60
pkg/unzip/unzip.go
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
package unzip
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/zip"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Unzip(src, dest string) error {
|
||||||
|
r, err := zip.OpenReader(src)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer func(r *zip.ReadCloser) {
|
||||||
|
_ = r.Close()
|
||||||
|
}(r)
|
||||||
|
|
||||||
|
handler := func(f *zip.File, dest string) error {
|
||||||
|
rc, err := f.Open()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer func(rc io.ReadCloser) {
|
||||||
|
_ = rc.Close()
|
||||||
|
}(rc)
|
||||||
|
|
||||||
|
path := filepath.Join(dest, f.Name)
|
||||||
|
if f.FileInfo().IsDir() {
|
||||||
|
_ = os.MkdirAll(path, 0755)
|
||||||
|
} else {
|
||||||
|
_ = os.MkdirAll(filepath.Dir(path), 0755)
|
||||||
|
|
||||||
|
file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer func(file *os.File) {
|
||||||
|
_ = file.Close()
|
||||||
|
}(file)
|
||||||
|
|
||||||
|
_, err = io.Copy(file, rc)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, f := range r.File {
|
||||||
|
err := handler(f, dest)
|
||||||
|
if err != nil {
|
||||||
|
_ = os.RemoveAll(dest)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
@ -3,6 +3,7 @@ package utils
|
|||||||
import (
|
import (
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
)
|
)
|
||||||
|
|
||||||
func FileRead(filePath string) ([]byte, error) {
|
func FileRead(filePath string) ([]byte, error) {
|
||||||
@ -23,6 +24,8 @@ func FileExist(filePath string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func FileTouch(filePath string) bool {
|
func FileTouch(filePath string) bool {
|
||||||
|
base := filepath.Dir(filePath)
|
||||||
|
_ = os.MkdirAll(base, 0755)
|
||||||
_, err := os.OpenFile(filePath, os.O_RDONLY|os.O_CREATE, 0644)
|
_, err := os.OpenFile(filePath, os.O_RDONLY|os.O_CREATE, 0644)
|
||||||
return If(err == nil, true, false).(bool)
|
return If(err == nil, true, false).(bool)
|
||||||
}
|
}
|
||||||
|
25
pkg/zapasynq/logger.go
Normal file
25
pkg/zapasynq/logger.go
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
package zapasynq
|
||||||
|
|
||||||
|
import (
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Logger struct {
|
||||||
|
logger *zap.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(zapLogger *zap.Logger) Logger {
|
||||||
|
return Logger{
|
||||||
|
logger: zapLogger,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l Logger) Debug(args ...interface{}) { l.logger.Sugar().Debugf(args[0].(string), args[1:]...) }
|
||||||
|
|
||||||
|
func (l Logger) Info(args ...interface{}) { l.logger.Sugar().Infof(args[0].(string), args[1:]...) }
|
||||||
|
|
||||||
|
func (l Logger) Warn(args ...interface{}) { l.logger.Sugar().Warnf(args[0].(string), args[1:]...) }
|
||||||
|
|
||||||
|
func (l Logger) Error(args ...interface{}) { l.logger.Sugar().Errorf(args[0].(string), args[1:]...) }
|
||||||
|
|
||||||
|
func (l Logger) Fatal(args ...interface{}) { l.logger.Sugar().Fatalf(args[0].(string), args[1:]...) }
|
@ -5,7 +5,7 @@ rm -rf woj-sandbox
|
|||||||
git clone https://github.com/WHUPRJ/woj-sandbox.git >/dev/null 2>&1 || exit 1
|
git clone https://github.com/WHUPRJ/woj-sandbox.git >/dev/null 2>&1 || exit 1
|
||||||
cd woj-sandbox && ./build_libseccomp.sh || exit 1
|
cd woj-sandbox && ./build_libseccomp.sh || exit 1
|
||||||
|
|
||||||
mkdir -p build && cd build
|
mkdir -p build && cd build || exit 1
|
||||||
cmake .. -DCMAKE_BUILD_TYPE=Release || exit 1
|
cmake .. -DCMAKE_BUILD_TYPE=Release || exit 1
|
||||||
make -j || exit 1
|
make -j || exit 1
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
include ${TEMPLATE}/c.mk ${TEMPLATE}/Judger.mk
|
include ${TEMPLATE}/c.mk ${TEMPLATE}/Judger.mk
|
||||||
|
|
||||||
compile:
|
compile:
|
||||||
$(CC) $(CFLAGS) -o $(PREFIX)/user/$(USER_PROG).out $(PREFIX)/user/$(USER_PROG).$(LANG)
|
@$(CC) $(CFLAGS) -o $(PREFIX)/user/$(USER_PROG).out $(PREFIX)/user/$(USER_PROG).$(LANG)
|
||||||
|
|
||||||
judge:
|
judge:
|
||||||
$($(CMP)) $(PREFIX)/data/input/$(TEST_NUM).input $(PREFIX)/user/$(TEST_NUM).out.usr $(PREFIX)/data/output/$(TEST_NUM).output > $(PREFIX)/user/$(TEST_NUM).judge 2>&1
|
$($(CMP)) $(PREFIX)/data/input/$(TEST_NUM).input $(PREFIX)/user/$(TEST_NUM).out.usr $(PREFIX)/data/output/$(TEST_NUM).output $(PREFIX)/user/$(TEST_NUM).judge -appes
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
include ${TEMPLATE}/cpp.mk ${TEMPLATE}/Judger.mk
|
include ${TEMPLATE}/cpp.mk ${TEMPLATE}/Judger.mk
|
||||||
|
|
||||||
compile:
|
compile:
|
||||||
$(CXX) $(CFLAGS) -o $(PREFIX)/user/$(USER_PROG).out $(PREFIX)/user/$(USER_PROG).$(LANG)
|
@$(CXX) $(CFLAGS) -o $(PREFIX)/user/$(USER_PROG).out $(PREFIX)/user/$(USER_PROG).$(LANG)
|
||||||
|
|
||||||
judge:
|
judge:
|
||||||
$($(CMP)) $(PREFIX)/data/input/$(TEST_NUM).input $(PREFIX)/user/$(TEST_NUM).out.usr $(PREFIX)/data/output/$(TEST_NUM).output > $(PREFIX)/user/$(TEST_NUM).judge 2>&1
|
$($(CMP)) $(PREFIX)/data/input/$(TEST_NUM).input $(PREFIX)/user/$(TEST_NUM).out.usr $(PREFIX)/data/output/$(TEST_NUM).output $(PREFIX)/user/$(TEST_NUM).judge -appes
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -x
|
set -x
|
||||||
rm -rf testlib
|
rm -rf testlib
|
||||||
git clone https://github.com/MikeMirzayanov/testlib.git >/dev/null 2>&1 || exit 1
|
git clone --depth=1 https://github.com/MikeMirzayanov/testlib.git >/dev/null 2>&1 || exit 1
|
||||||
rm -rf testlib/.git
|
rm -rf testlib/.git
|
||||||
rm -rf testlib/tests
|
rm -rf testlib/tests
|
||||||
cd testlib/checkers || exit 1
|
cd testlib/checkers || exit 1
|
||||||
|
@ -19,4 +19,4 @@ compile:
|
|||||||
judge:
|
judge:
|
||||||
# Rename on *.out.usr or *.judge is not allowed
|
# Rename on *.out.usr or *.judge is not allowed
|
||||||
sed '/gadgets/d' $(PREFIX)/user/$(TEST_NUM).out.usr > $(PREFIX)/user/$(TEST_NUM).out.usr1
|
sed '/gadgets/d' $(PREFIX)/user/$(TEST_NUM).out.usr > $(PREFIX)/user/$(TEST_NUM).out.usr1
|
||||||
$(NCMP) $(PREFIX)/data/input/$(TEST_NUM).input $(PREFIX)/user/$(TEST_NUM).out.usr1 $(PREFIX)/data/output/$(TEST_NUM).output > $(PREFIX)/user/$(TEST_NUM).judge 2>&1
|
$(NCMP) $(PREFIX)/data/input/$(TEST_NUM).input $(PREFIX)/user/$(TEST_NUM).out.usr1 $(PREFIX)/data/output/$(TEST_NUM).output $(PREFIX)/user/$(TEST_NUM).judge -appes
|
||||||
|
@ -6,7 +6,7 @@ cd "$(dirname "$0")"/../ || exit 1
|
|||||||
|
|
||||||
if [ -f ./.mark.docker ]; then
|
if [ -f ./.mark.docker ]; then
|
||||||
log_warn "Docker containers already prepared"
|
log_warn "Docker containers already prepared"
|
||||||
log_warn "If you want to re-prepare the containers, please remove the file `pwd`/.mark.docker"
|
log_warn "If you want to re-prepare the containers, please remove the file $(pwd)/.mark.docker"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@ get_problem_info "$WORKSPACE" "$1" "$3"
|
|||||||
|
|
||||||
SRC_FILE="$WORKSPACE"/user/"$2"/"$2"."$3"
|
SRC_FILE="$WORKSPACE"/user/"$2"/"$2"."$3"
|
||||||
EXE_FILE="$WORKSPACE"/user/"$2"/"$2".out
|
EXE_FILE="$WORKSPACE"/user/"$2"/"$2".out
|
||||||
LOG_FILE="$WORKSPACE"/user/"$2"/"$2".compile.log
|
export LOG_FILE="$WORKSPACE"/user/"$2"/"$2".compile.log
|
||||||
|
|
||||||
rm -f "$EXE_FILE" && touch "$EXE_FILE"
|
rm -f "$EXE_FILE" && touch "$EXE_FILE"
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@ fi
|
|||||||
|
|
||||||
get_problem_info "$WORKSPACE" "$1" "$3"
|
get_problem_info "$WORKSPACE" "$1" "$3"
|
||||||
|
|
||||||
TIMEOUT=${4:-60}
|
export TIMEOUT=${4:-60}
|
||||||
for test_num in $(seq "$Info_Num"); do
|
for test_num in $(seq "$Info_Num"); do
|
||||||
std_file="$WORKSPACE/problem/$1/data/output/$test_num.output"
|
std_file="$WORKSPACE/problem/$1/data/output/$test_num.output"
|
||||||
ans_file="$WORKSPACE/user/$2/$test_num.out.usr"
|
ans_file="$WORKSPACE/user/$2/$test_num.out.usr"
|
||||||
|
@ -21,7 +21,7 @@ if [ ! -f "$WORKSPACE/problem/$1/judge/prebuild.Makefile" ]; then
|
|||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
TIMEOUT=${2:-300}
|
export TIMEOUT=${2:-300}
|
||||||
docker_run \
|
docker_run \
|
||||||
-v "$WORKSPACE/problem/$1/data":/woj/problem/data \
|
-v "$WORKSPACE/problem/$1/data":/woj/problem/data \
|
||||||
-v "$WORKSPACE/problem/$1/judge":/woj/problem/judge \
|
-v "$WORKSPACE/problem/$1/judge":/woj/problem/judge \
|
||||||
|
Loading…
Reference in New Issue
Block a user