Merge branch 'dev' into gigachat
This commit is contained in:
commit
3997a9bb93
@ -15,9 +15,18 @@ jobs:
|
|||||||
secrets:
|
secrets:
|
||||||
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
|
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
|
||||||
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
|
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
|
||||||
|
ValidateConfig:
|
||||||
|
runs-on: [squizprod]
|
||||||
|
uses: https://gitea.pena/PenaDevops/actions.git/.gitea/workflows/validate_config.yml@v1.2.1
|
||||||
|
needs: CreateImage
|
||||||
|
with:
|
||||||
|
runner: hubstaging
|
||||||
|
secrets:
|
||||||
|
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
|
||||||
|
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
|
||||||
DeployService:
|
DeployService:
|
||||||
runs-on: [squizprod]
|
runs-on: [squizprod]
|
||||||
needs: CreateImage
|
needs: ValidateConfig
|
||||||
uses: https://gitea.pena/PenaDevops/actions.git/.gitea/workflows/deploy.yml@v1.1.4-p7
|
uses: https://gitea.pena/PenaDevops/actions.git/.gitea/workflows/deploy.yml@v1.1.4-p7
|
||||||
with:
|
with:
|
||||||
runner: hubprod
|
runner: hubprod
|
||||||
|
@ -8,16 +8,35 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
CreateImage:
|
CreateImage:
|
||||||
runs-on: [hubstaging]
|
runs-on: [squizstaging]
|
||||||
uses: http://gitea.pena/PenaDevops/actions.git/.gitea/workflows/build-image.yml@v1.1.6-p
|
uses: http://gitea.pena/PenaDevops/actions.git/.gitea/workflows/build-image.yml@v1.1.6-p
|
||||||
with:
|
with:
|
||||||
runner: hubstaging
|
runner: hubstaging
|
||||||
secrets:
|
secrets:
|
||||||
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
|
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
|
||||||
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
|
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
|
||||||
DeployService:
|
ValidateConfig:
|
||||||
runs-on: [hubstaging]
|
runs-on: [squizstaging]
|
||||||
|
uses: https://gitea.pena/PenaDevops/actions.git/.gitea/workflows/validate_config.yml@v1.2.1
|
||||||
needs: CreateImage
|
needs: CreateImage
|
||||||
|
with:
|
||||||
|
runner: hubstaging
|
||||||
|
secrets:
|
||||||
|
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
|
||||||
|
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
|
||||||
|
MigrateDatabase:
|
||||||
|
runs-on: [squizstaging]
|
||||||
|
uses: http://gitea.pena/PenaDevops/actions.git/.gitea/workflows/migrate.yml@9263e22095fa40bcb36881ad81722d3049acd07f
|
||||||
|
needs: ValidateConfig
|
||||||
|
with:
|
||||||
|
runner: hubstaging
|
||||||
|
branch_name: ${{ github.ref_name }}
|
||||||
|
secrets:
|
||||||
|
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
|
||||||
|
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
|
||||||
|
DeployService:
|
||||||
|
runs-on: [squizstaging]
|
||||||
|
needs: MigrateDatabase
|
||||||
uses: http://gitea.pena/PenaDevops/actions.git/.gitea/workflows/deploy.yml@v1.1.4-p7
|
uses: http://gitea.pena/PenaDevops/actions.git/.gitea/workflows/deploy.yml@v1.1.4-p7
|
||||||
with:
|
with:
|
||||||
runner: hubstaging
|
runner: hubstaging
|
||||||
|
@ -8,7 +8,7 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
Lint:
|
Lint:
|
||||||
runs-on: [hubstaging]
|
runs-on: [squizstaging]
|
||||||
uses: http://gitea.pena/PenaDevops/actions.git/.gitea/workflows/lint.yml@v1.1.0
|
uses: http://gitea.pena/PenaDevops/actions.git/.gitea/workflows/lint.yml@v1.1.2
|
||||||
with:
|
with:
|
||||||
runner: hubstaging
|
runner: squizstaging
|
||||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -20,3 +20,5 @@ worker/worker
|
|||||||
storer/storer
|
storer/storer
|
||||||
answerer/answerer
|
answerer/answerer
|
||||||
core
|
core
|
||||||
|
/.tdlib/
|
||||||
|
/unsetrecover.bolt
|
||||||
|
3
CHANGELOG.md
Normal file
3
CHANGELOG.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
#v1.0.0
|
||||||
|
|
||||||
|
- В статистику по воронкам добавлена статистика по формам контактов
|
@ -1,12 +1,13 @@
|
|||||||
FROM gitea.pena/penadevops/container-images/golang:main as build
|
FROM gitea.pena/penadevops/container-images/golang:main as build
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
RUN apk add git
|
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN go mod download
|
RUN go mod download
|
||||||
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o core
|
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o core ./cmd/main.go
|
||||||
|
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o validator ./cmd/validator/main.go
|
||||||
|
|
||||||
FROM gitea.pena/penadevops/container-images/alpine:main as prod
|
|
||||||
|
FROM gitea.pena/penadevops/container-images/alpine:main
|
||||||
COPY --from=build /app/core .
|
COPY --from=build /app/core .
|
||||||
COPY --from=build /app/schema /schema
|
COPY --from=build /app/validator .
|
||||||
RUN apk add tzdata
|
RUN apk add tzdata
|
||||||
CMD ["/core"]
|
CMD ["/core"]
|
||||||
|
64
Makefile
64
Makefile
@ -1,64 +0,0 @@
|
|||||||
GOCMD=go
|
|
||||||
GOBUILD=$(GOCMD) build
|
|
||||||
GOCLEAN=$(GOCMD) clean
|
|
||||||
GOTEST=$(GOCMD) test
|
|
||||||
COMMIT?=$(shell git rev-parse --short HEAD)
|
|
||||||
BUILD_TIME?=$(shell date -u '+%Y-%m-%d_%H:%M:%S')
|
|
||||||
GOOS?=linux
|
|
||||||
GOARCH?=amd64
|
|
||||||
DOCKER_REGISTRY=yourRegistryHost:<port>
|
|
||||||
BINARY_NAME=$(shell basename `pwd`)
|
|
||||||
PORT?=1488
|
|
||||||
SHELL = /bin/bash
|
|
||||||
LDFLAGS=-s -w -X github.com/skeris/appInit/version.Release=${shell git describe --tags --abbrev=0} \
|
|
||||||
-X github.com/skeris/appInit/version.Commit=${COMMIT} -X github.com/skeris/appInit/version.BuildTime=${BUILD_TIME}
|
|
||||||
|
|
||||||
all: compile run
|
|
||||||
clean:
|
|
||||||
rm -f $(BINARY_NAME)
|
|
||||||
rm -f ./worker/worker
|
|
||||||
compile: clean
|
|
||||||
CGO_ENABLED=0 GOOS=${GOOS} GOARCH=${GOARCH} $(GOBUILD) -ldflags "${LDFLAGS}" -o ${BINARY_NAME}
|
|
||||||
CGO_ENABLED=0 GOOS=${GOOS} GOARCH=${GOARCH} $(GOBUILD) -ldflags "${LDFLAGS}" -o ./worker/worker
|
|
||||||
run: compile
|
|
||||||
./$(BINARY_NAME)
|
|
||||||
container: compile
|
|
||||||
docker build -t $(BINARY_NAME):${shell git describe --tags --abbrev=0} .
|
|
||||||
docker build -t $(BINARY_NAME)-worker:${shell git describe --tags --abbrev=0} ./worker
|
|
||||||
docker-push: container
|
|
||||||
docker tag $(BINARY_NAME) $(DOCKER_REGISTRY)/$(BINARY_NAME)
|
|
||||||
docker tag $(BINARY_NAME)-worker $(DOCKER_REGISTRY)/$(BINARY_NAME)-worker
|
|
||||||
pull:
|
|
||||||
docker pull $(DOCKER_REGISTRY)/$(BINARY_NAME)
|
|
||||||
docker pull $(DOCKER_REGISTRY)/$(BINARY_NAME)-worker
|
|
||||||
docker tag $(DOCKER_REGISTRY)/$(BINARY_NAME) $(BINARY_NAME)
|
|
||||||
docker tag $(DOCKER_REGISTRY)/$(BINARY_NAME)-worker $(BINARY_NAME)-worker
|
|
||||||
run-container:
|
|
||||||
docker run --rm --name squiz --network host -p 1488:1488 $(BINARY_NAME):latest
|
|
||||||
test:
|
|
||||||
$(GOTEST) -v -race ./...
|
|
||||||
commit-all:
|
|
||||||
git add -A
|
|
||||||
git commit -a
|
|
||||||
git push
|
|
||||||
push-new-release: commit-all
|
|
||||||
git tag ${shell git describe --tags --abbrev=0 | awk -F '.' '{print "v"$$1+1".0.0"}'}
|
|
||||||
git push --tags
|
|
||||||
push-new-feature: commit-all
|
|
||||||
git tag ${shell git describe --tags --abbrev=0 | awk -F '.' '{print $$1"."$$2+1".0"}'}
|
|
||||||
git push --tags
|
|
||||||
push-new-state: commit-all
|
|
||||||
git tag ${shell git describe --tags --abbrev=0 | awk -F '.' '{print $$1"."$$2"."$$3+1}'}
|
|
||||||
git push --tags
|
|
||||||
benchmark:
|
|
||||||
mv ./tests/new.txt ./tests/old.txt
|
|
||||||
go test -run=NONE -bench=. -benchmem ./tests -test.short > ./tests/new.txt
|
|
||||||
benchstat -html ./tests/old.txt ./tests/new.txt > benchmark.html
|
|
||||||
|
|
||||||
# show full set of messages
|
|
||||||
test-in-docker-debug:
|
|
||||||
docker-compose -f deployments/test/docker-compose.yaml up --build --force-recreate
|
|
||||||
|
|
||||||
# show only relevant messages
|
|
||||||
test-in-docker:
|
|
||||||
docker-compose -f deployments/test/docker-compose.yaml up --build --force-recreate --exit-code-from test-squiz 2>/dev/null | grep ^test-squiz
|
|
8
Taskfile.dist.yml
Normal file
8
Taskfile.dist.yml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
tasks:
|
||||||
|
update-linter:
|
||||||
|
cmds:
|
||||||
|
- go get -u gitea.pena/PenaSide/linters-golang
|
||||||
|
lint:
|
||||||
|
cmds:
|
||||||
|
- task: update-linter
|
||||||
|
- cmd: golangci-lint run -v -c $(go list -f '{{"{{"}}.Dir{{"}}"}}' -m gitea.pena/PenaSide/linters-golang)/.golangci.yml
|
@ -734,10 +734,22 @@ components:
|
|||||||
|
|
||||||
PipeLineStatsResp:
|
PipeLineStatsResp:
|
||||||
type: object
|
type: object
|
||||||
additionalProperties:
|
properties:
|
||||||
type: array
|
PipelineStatistic:
|
||||||
items:
|
type: object
|
||||||
$ref: '#/components/schemas/Statistic'
|
additionalProperties:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/Statistic'
|
||||||
|
description: Статистика по воронкам
|
||||||
|
|
||||||
|
ContactFormStatistic:
|
||||||
|
type: object
|
||||||
|
additionalProperties:
|
||||||
|
type: integer
|
||||||
|
format: int64
|
||||||
|
description: Количество ответов на вопрос формы контактов
|
||||||
|
description: Статистика форм контакта
|
||||||
Answer:
|
Answer:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
@ -772,8 +784,72 @@ components:
|
|||||||
Deleted:
|
Deleted:
|
||||||
type: boolean
|
type: boolean
|
||||||
description: удален?
|
description: удален?
|
||||||
|
LeadTarget:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
ID:
|
||||||
|
type: integer
|
||||||
|
format: int64
|
||||||
|
AccountID:
|
||||||
|
type: string
|
||||||
|
Type:
|
||||||
|
type: string
|
||||||
|
QuizID:
|
||||||
|
type: integer
|
||||||
|
format: int32
|
||||||
|
Target:
|
||||||
|
type: string
|
||||||
|
InviteLink:
|
||||||
|
type: string
|
||||||
|
Deleted:
|
||||||
|
type: boolean
|
||||||
|
CreatedAt:
|
||||||
|
type: string
|
||||||
|
TgAccountStatus:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- active
|
||||||
|
- inactive
|
||||||
|
- ban
|
||||||
|
TgAccount:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
ID:
|
||||||
|
type: integer
|
||||||
|
format: int64
|
||||||
|
ApiID:
|
||||||
|
type: integer
|
||||||
|
format: int32
|
||||||
|
ApiHash:
|
||||||
|
type: string
|
||||||
|
PhoneNumber:
|
||||||
|
type: string
|
||||||
|
Password:
|
||||||
|
type: string
|
||||||
|
Status:
|
||||||
|
$ref: '#/components/schemas/TgAccountStatus'
|
||||||
|
Deleted:
|
||||||
|
type: boolean
|
||||||
|
CreatedAt:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
AuthTgUserReq:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- ApiID
|
||||||
|
- ApiHash
|
||||||
|
- PhoneNumber
|
||||||
|
- Password
|
||||||
|
properties:
|
||||||
|
ApiID:
|
||||||
|
type: integer
|
||||||
|
format: int32
|
||||||
|
ApiHash:
|
||||||
|
type: string
|
||||||
|
PhoneNumber:
|
||||||
|
type: string
|
||||||
|
Password:
|
||||||
|
type: string
|
||||||
paths:
|
paths:
|
||||||
/liveness:
|
/liveness:
|
||||||
get:
|
get:
|
||||||
@ -1546,6 +1622,211 @@ paths:
|
|||||||
properties:
|
properties:
|
||||||
message:
|
message:
|
||||||
type: string
|
type: string
|
||||||
|
/account/leadtarget:
|
||||||
|
post:
|
||||||
|
description: Метод для добавления целевых мест, куда будут посылаться заявки клиенту.
|
||||||
|
security:
|
||||||
|
- Bearer: [ ]
|
||||||
|
requestBody:
|
||||||
|
content:
|
||||||
|
'application/json':
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- type
|
||||||
|
- quizID
|
||||||
|
- target
|
||||||
|
properties:
|
||||||
|
type:
|
||||||
|
type: string
|
||||||
|
description: Тип цели (mail, telegram, whatsapp).
|
||||||
|
enum:
|
||||||
|
- mail
|
||||||
|
- telegram
|
||||||
|
- whatsapp
|
||||||
|
quizID:
|
||||||
|
type: integer
|
||||||
|
format: int32
|
||||||
|
description: ID квиза, к которому прикреплено это правило (приоритет). Передавать как 0, если правило не прикрепляется к квизу и является общим.
|
||||||
|
target:
|
||||||
|
type: string
|
||||||
|
description: Адресат, куда конкретно слать (для mail - email, для telegram - ID канала, передавать не нужно канал сам создаться, для whatsapp - номер телефона, наверное).
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
description: имя например для тг канала
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: ОК, парвило добавлено если тип mail о сразу добавляется если тг то будет добавленно в воркере если ватсап пока тодо
|
||||||
|
# content:
|
||||||
|
# application/json:
|
||||||
|
# schema:
|
||||||
|
# $ref: '#/components/schemas/LeadTarget'
|
||||||
|
'400':
|
||||||
|
description: Bad request, ошибка в теле запроса
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
'401':
|
||||||
|
description: Unauthorized, не авторизован
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
'500':
|
||||||
|
description: Internal Srv Error
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
put:
|
||||||
|
description: Метод для обновления целевого места, куда будут посылаться заявки клиенту.
|
||||||
|
security:
|
||||||
|
- Bearer: [ ]
|
||||||
|
requestBody:
|
||||||
|
content:
|
||||||
|
'application/json':
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- id
|
||||||
|
- target
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: integer
|
||||||
|
format: int64
|
||||||
|
description: id этой самой цели, primary key.
|
||||||
|
target:
|
||||||
|
type: string
|
||||||
|
description: Адресат, куда конкретно слать (для mail - email, для telegram - ID чата, для whatsapp - номер телефона, наверное).
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: ОК, парвило обновлено
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/LeadTarget'
|
||||||
|
'400':
|
||||||
|
description: Bad request, ошибка в теле запроса
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
'401':
|
||||||
|
description: Unauthorized, не авторизован
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
'404':
|
||||||
|
description: NotFound, такого не существует
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
'500':
|
||||||
|
description: Internal Srv Error
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
/account/leadtarget/{id}:
|
||||||
|
delete:
|
||||||
|
description: удаление правила по id, primary key
|
||||||
|
security:
|
||||||
|
- Bearer: [ ]
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: ОК, парвило удалено
|
||||||
|
'400':
|
||||||
|
description: Bad request, ошибка в теле запроса
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
'500':
|
||||||
|
description: Internal Srv Error
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
/account/leadtarget/{quizID}:
|
||||||
|
get:
|
||||||
|
description: получение правила по quizID, так же стоит передавать 0 если правило не было привязано к определенному квизу, возвращает массив
|
||||||
|
security:
|
||||||
|
- Bearer: [ ]
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: ОК, парвила получены
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/LeadTarget'
|
||||||
|
'400':
|
||||||
|
description: Bad request, ошибка в теле запроса
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
'401':
|
||||||
|
description: Unauthorized, не авторизован
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
'404':
|
||||||
|
description: NotFound, такого не существует
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
'500':
|
||||||
|
description: Internal Srv Error
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
/statistics/:quizID/pipelines:
|
/statistics/:quizID/pipelines:
|
||||||
get:
|
get:
|
||||||
description: получение статистики по векторам прохождения респондентами опроса с ветвлением и без, на выход отдается мапа с ключем последний вопрос и массивом точек "точек прохождения пользователем вопросов" грубо говоря массив с векторами как двигался респондент по возможным путям, в этом массиве question id и count прошедших сессий через него
|
description: получение статистики по векторам прохождения респондентами опроса с ветвлением и без, на выход отдается мапа с ключем последний вопрос и массивом точек "точек прохождения пользователем вопросов" грубо говоря массив с векторами как двигался респондент по возможным путям, в этом массиве question id и count прошедших сессий через него
|
||||||
@ -1569,3 +1850,94 @@ paths:
|
|||||||
description: Bad Request
|
description: Bad Request
|
||||||
'500':
|
'500':
|
||||||
description: Internal Server Error
|
description: Internal Server Error
|
||||||
|
/telegram/pool:
|
||||||
|
get:
|
||||||
|
description: возвращает все неудаленные аккаунты тг, активные, не активные и баны, тело пустое
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: успех
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/TgAccount'
|
||||||
|
/telegram/create:
|
||||||
|
post:
|
||||||
|
description: метод для автторизации сервера в тг аккаунте
|
||||||
|
requestBody:
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/AuthTgUserReq'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: возвращает подпись, которая является идентификатором текущей сессии авторизации нужно для метода отправки кода
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
signature:
|
||||||
|
type: string
|
||||||
|
example: b7gh83j2k4l0
|
||||||
|
'400':
|
||||||
|
description: неверные данные запроса
|
||||||
|
'409':
|
||||||
|
description: аккаунт уже существует и активен
|
||||||
|
'500':
|
||||||
|
description: внутренняя ошибка сервера
|
||||||
|
/telegram/{id}:
|
||||||
|
delete:
|
||||||
|
description: метод мягкого удаления аккаунта по id primary key
|
||||||
|
parameters:
|
||||||
|
- in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
description: id primary key
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
format: int64
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: успех
|
||||||
|
'400':
|
||||||
|
description: неверные данные запроса
|
||||||
|
'500':
|
||||||
|
description: внутренняя ошибка сервера
|
||||||
|
|
||||||
|
/telegram/setCode:
|
||||||
|
post:
|
||||||
|
description: метод для отправки кода авторизации, который пришел от телеграмма
|
||||||
|
requestBody:
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- code
|
||||||
|
- signature
|
||||||
|
properties:
|
||||||
|
code:
|
||||||
|
type: string
|
||||||
|
signature:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: возвращает id primary авторизованного аккаунта
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: integer
|
||||||
|
format: int64
|
||||||
|
'204':
|
||||||
|
description: state канал закрылся до того как перешел в состояние логина или отказа от логина, возможно стоит другой статус указывать или как то побороть эту беду
|
||||||
|
'400':
|
||||||
|
description: неверные данные запроса
|
||||||
|
'403':
|
||||||
|
description: что то пошло не так связано с тг
|
||||||
|
'500':
|
||||||
|
description: внутренняя ошибка сервера
|
239
app/app.go
239
app/app.go
@ -1,239 +0,0 @@
|
|||||||
package app
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"gitea.pena/PenaSide/common/log_mw"
|
|
||||||
"gitea.pena/PenaSide/common/privilege"
|
|
||||||
"gitea.pena/PenaSide/hlog"
|
|
||||||
"gitea.pena/PenaSide/trashlog/wrappers/zaptrashlog"
|
|
||||||
"gitea.pena/SQuiz/common/dal"
|
|
||||||
"gitea.pena/SQuiz/common/healthchecks"
|
|
||||||
"gitea.pena/SQuiz/common/middleware"
|
|
||||||
"gitea.pena/SQuiz/common/model"
|
|
||||||
"gitea.pena/SQuiz/core/brokers"
|
|
||||||
"gitea.pena/SQuiz/core/clients/auth"
|
|
||||||
"gitea.pena/SQuiz/core/initialize"
|
|
||||||
"gitea.pena/SQuiz/core/models"
|
|
||||||
"gitea.pena/SQuiz/core/server"
|
|
||||||
"gitea.pena/SQuiz/core/service"
|
|
||||||
"gitea.pena/SQuiz/core/tools"
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
"github.com/skeris/appInit"
|
|
||||||
"go.uber.org/zap"
|
|
||||||
"go.uber.org/zap/zapcore"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type App struct {
|
|
||||||
logger *zap.Logger
|
|
||||||
err chan error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a App) GetLogger() *zap.Logger {
|
|
||||||
return a.logger
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a App) GetErr() chan error {
|
|
||||||
return a.err
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
errInvalidOptions = errors.New("invalid options")
|
|
||||||
)
|
|
||||||
|
|
||||||
var zapOptions = []zap.Option{
|
|
||||||
zap.AddCaller(),
|
|
||||||
zap.AddCallerSkip(2),
|
|
||||||
zap.AddStacktrace(zap.ErrorLevel),
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ appInit.CommonApp = (*App)(nil)
|
|
||||||
|
|
||||||
type Options struct {
|
|
||||||
LoggerProdMode bool `env:"IS_PROD_LOG" default:"false"`
|
|
||||||
IsProd bool `env:"IS_PROD" default:"false"`
|
|
||||||
NumberPort string `env:"PORT" default:"1488"`
|
|
||||||
CrtFile string `env:"CRT" default:"server.crt"`
|
|
||||||
KeyFile string `env:"KEY" default:"server.key"`
|
|
||||||
PostgresCredentials string `env:"PG_CRED" default:"host=localhost port=35432 user=squiz password=Redalert2 dbname=squiz sslmode=disable"`
|
|
||||||
HubAdminUrl string `env:"HUB_ADMIN_URL" default:"http://localhost:8001/"`
|
|
||||||
ServiceName string `env:"SERVICE_NAME" default:"squiz"`
|
|
||||||
AuthServiceURL string `env:"AUTH_URL" default:"http://localhost:8000/"`
|
|
||||||
GrpcHost string `env:"GRPC_HOST" default:"localhost"`
|
|
||||||
GrpcPort string `env:"GRPC_PORT" default:"9000"`
|
|
||||||
KafkaBrokers string `env:"KAFKA_BROKERS" default:"localhost:9092"`
|
|
||||||
KafkaTopic string `env:"KAFKA_TOPIC" default:"test-topic"`
|
|
||||||
KafkaGroup string `env:"KAFKA_GROUP" default:"mailnotifier"`
|
|
||||||
TrashLogHost string `env:"TRASH_LOG_HOST" default:"localhost:7113"`
|
|
||||||
ModuleLogger string `env:"MODULE_LOGGER" default:"core-local"`
|
|
||||||
ClickHouseCred string `env:"CLICK_HOUSE_CRED" default:"tcp://10.8.0.15:9000/default?sslmode=disable"`
|
|
||||||
S3Prefix string `env:"S3_PREFIX"`
|
|
||||||
KafkaGroupGigaChat string `env:"KAFKA_GROUP_GIGA_CHAT" default:"gigachat"`
|
|
||||||
KafkaTopicGigaChat string `env:"KAFKA_TOPIC_GIGA_CHAT"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func New(ctx context.Context, opts interface{}, ver appInit.Version) (appInit.CommonApp, error) {
|
|
||||||
var (
|
|
||||||
err, workerErr error
|
|
||||||
zapLogger *zap.Logger
|
|
||||||
errChan = make(chan error)
|
|
||||||
options Options
|
|
||||||
ok bool
|
|
||||||
)
|
|
||||||
|
|
||||||
if options, ok = opts.(Options); !ok {
|
|
||||||
return App{}, errInvalidOptions
|
|
||||||
}
|
|
||||||
|
|
||||||
if options.LoggerProdMode {
|
|
||||||
zapLogger, err = zap.NewProduction(zapOptions...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
zapLogger, err = zap.NewDevelopment(zapOptions...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
zapLogger = zapLogger.With(
|
|
||||||
zap.String("SvcCommit", ver.Commit),
|
|
||||||
zap.String("SvcVersion", ver.Release),
|
|
||||||
zap.String("SvcBuildTime", ver.BuildTime),
|
|
||||||
)
|
|
||||||
|
|
||||||
clickHouseLogger, err := zaptrashlog.NewCore(ctx, zap.InfoLevel, options.TrashLogHost, ver.Release, ver.Commit, time.Now().Unix())
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
loggerForHlog := zapLogger.WithOptions(zap.WrapCore(func(core zapcore.Core) zapcore.Core {
|
|
||||||
return zapcore.NewTee(core, clickHouseLogger)
|
|
||||||
}))
|
|
||||||
|
|
||||||
loggerHlog := hlog.New(loggerForHlog).Module(options.ModuleLogger)
|
|
||||||
loggerHlog.With(models.AllFields{})
|
|
||||||
loggerHlog.Emit(InfoSvcStarted{})
|
|
||||||
|
|
||||||
authClient := auth.NewAuthClient(options.AuthServiceURL)
|
|
||||||
|
|
||||||
pgdal, err := dal.New(ctx, options.PostgresCredentials, nil)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("NEW", err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
chDal, err := dal.NewClickHouseDAL(ctx, options.ClickHouseCred)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("failed init clickhouse", err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
kafkaClient, err := initialize.KafkaInit(ctx, initialize.KafkaDeps{
|
|
||||||
KafkaGroup: options.KafkaGroup,
|
|
||||||
KafkaBrokers: options.KafkaBrokers,
|
|
||||||
KafkaTopic: options.KafkaTopic,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
kafkaClientGigaChat, err := initialize.KafkaInit(ctx, initialize.KafkaDeps{
|
|
||||||
KafkaGroup: options.KafkaGroupGigaChat,
|
|
||||||
KafkaBrokers: options.KafkaBrokers,
|
|
||||||
KafkaTopic: options.KafkaTopicGigaChat,
|
|
||||||
})
|
|
||||||
|
|
||||||
producer := brokers.NewProducer(brokers.ProducerDeps{
|
|
||||||
KafkaClient: kafkaClient,
|
|
||||||
Logger: zapLogger,
|
|
||||||
})
|
|
||||||
|
|
||||||
producerGigaChat := brokers.NewProducer(brokers.ProducerDeps{
|
|
||||||
KafkaClient: kafkaClientGigaChat,
|
|
||||||
Logger: zapLogger,
|
|
||||||
})
|
|
||||||
|
|
||||||
clientData := privilege.Client{
|
|
||||||
URL: options.HubAdminUrl,
|
|
||||||
ServiceName: options.ServiceName,
|
|
||||||
Privileges: model.Privileges,
|
|
||||||
}
|
|
||||||
fiberClient := &fiber.Client{}
|
|
||||||
privilegeController := privilege.NewPrivilege(clientData, fiberClient)
|
|
||||||
go tools.PublishPrivilege(privilegeController, 10, 5*time.Minute)
|
|
||||||
|
|
||||||
// todo подумать над реализацией всего а то пока мне кажется что немного каша получается такой предикт что через некоторое время
|
|
||||||
// сложно будет разобраться что есть где
|
|
||||||
grpcControllers := initialize.InitRpcControllers(pgdal)
|
|
||||||
grpc, err := server.NewGRPC(zapLogger)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("error:", err)
|
|
||||||
panic("err init grpc server")
|
|
||||||
}
|
|
||||||
grpc.Register(grpcControllers)
|
|
||||||
go grpc.Run(server.DepsGrpcRun{
|
|
||||||
Host: options.GrpcHost,
|
|
||||||
Port: options.GrpcPort,
|
|
||||||
})
|
|
||||||
|
|
||||||
app := fiber.New()
|
|
||||||
app.Use(middleware.JWTAuth())
|
|
||||||
app.Use(log_mw.ContextLogger(loggerHlog))
|
|
||||||
app.Get("/liveness", healthchecks.Liveness)
|
|
||||||
app.Get("/readiness", healthchecks.Readiness(&workerErr)) //todo parametrized readiness. should discuss ready reason
|
|
||||||
|
|
||||||
svc := service.New(service.Deps{
|
|
||||||
Dal: pgdal,
|
|
||||||
AuthClient: authClient,
|
|
||||||
Producer: producer,
|
|
||||||
ServiceName: options.ServiceName,
|
|
||||||
ChDAL: chDal,
|
|
||||||
S3Prefix: options.S3Prefix,
|
|
||||||
ProducerGigaChat: producerGigaChat,
|
|
||||||
})
|
|
||||||
|
|
||||||
svc.Register(app)
|
|
||||||
|
|
||||||
loggerHlog.Emit(InfoSvcReady{})
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
defer func() {
|
|
||||||
if pgdal != nil {
|
|
||||||
pgdal.Close()
|
|
||||||
}
|
|
||||||
if chDal != nil {
|
|
||||||
chDal.Close(ctx)
|
|
||||||
}
|
|
||||||
err := grpc.Stop(ctx)
|
|
||||||
err = app.Shutdown()
|
|
||||||
loggerHlog.Emit(InfoSvcShutdown{Signal: err.Error()})
|
|
||||||
}()
|
|
||||||
|
|
||||||
if options.IsProd {
|
|
||||||
if err := app.ListenTLS(fmt.Sprintf(":%s", options.NumberPort), options.CrtFile, options.KeyFile); err != nil {
|
|
||||||
loggerHlog.Emit(ErrorCanNotServe{
|
|
||||||
Err: err,
|
|
||||||
})
|
|
||||||
errChan <- err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if err := app.Listen(fmt.Sprintf(":%s", options.NumberPort)); err != nil {
|
|
||||||
loggerHlog.Emit(ErrorCanNotServe{
|
|
||||||
Err: err,
|
|
||||||
})
|
|
||||||
errChan <- err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
errChan <- nil
|
|
||||||
}()
|
|
||||||
// todo implement helper func for service app type. such as server preparing, logger preparing, healthchecks and etc.
|
|
||||||
return &App{
|
|
||||||
logger: zapLogger,
|
|
||||||
err: errChan,
|
|
||||||
}, err
|
|
||||||
}
|
|
329
benchmarks/pagination_test.go
Normal file
329
benchmarks/pagination_test.go
Normal file
@ -0,0 +1,329 @@
|
|||||||
|
package benchmarks
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"log"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
_ "github.com/lib/pq"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
accountID = "64f2cd7a7047f28fdabf6d9e"
|
||||||
|
connStr = "host=localhost port=35432 user=squiz password=Redalert2 dbname=squiz sslmode=disable"
|
||||||
|
queryTotal = `
|
||||||
|
WITH user_data AS (
|
||||||
|
SELECT AmoID FROM accountsAmo WHERE accountsAmo.AccountID = $1 AND accountsAmo.Deleted = false
|
||||||
|
)
|
||||||
|
SELECT f.*, COUNT(*) OVER() as total_count
|
||||||
|
FROM fields f JOIN user_data u ON f.AccountID = u.AmoID
|
||||||
|
WHERE f.Deleted = false
|
||||||
|
ORDER BY f.ID OFFSET ($2 - 1) * $3 LIMIT $3;
|
||||||
|
`
|
||||||
|
queryCount = `
|
||||||
|
WITH user_data AS (
|
||||||
|
SELECT AmoID FROM accountsAmo WHERE accountsAmo.AccountID = $1 AND accountsAmo.Deleted = false
|
||||||
|
)
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM fields f JOIN user_data u ON f.AccountID = u.AmoID
|
||||||
|
WHERE f.Deleted = false;
|
||||||
|
`
|
||||||
|
queryData = `
|
||||||
|
WITH user_data AS (
|
||||||
|
SELECT AmoID FROM accountsAmo WHERE accountsAmo.AccountID = $1 AND accountsAmo.Deleted = false
|
||||||
|
)
|
||||||
|
SELECT f.*
|
||||||
|
FROM fields f JOIN user_data u ON f.AccountID = u.AmoID
|
||||||
|
WHERE f.Deleted = false
|
||||||
|
ORDER BY f.ID OFFSET ($2 - 1) * $3 LIMIT $3;
|
||||||
|
`
|
||||||
|
)
|
||||||
|
|
||||||
|
type GetFieldsWithPaginationRow struct {
|
||||||
|
ID int64 `db:"id" json:"id"`
|
||||||
|
Amoid int32 `db:"amoid" json:"amoid"`
|
||||||
|
Code string `db:"code" json:"code"`
|
||||||
|
Accountid int32 `db:"accountid" json:"accountid"`
|
||||||
|
Name string `db:"name" json:"name"`
|
||||||
|
Entity interface{} `db:"entity" json:"entity"`
|
||||||
|
Type interface{} `db:"type" json:"type"`
|
||||||
|
Deleted bool `db:"deleted" json:"deleted"`
|
||||||
|
Createdat sql.NullTime `db:"createdat" json:"createdat"`
|
||||||
|
TotalCount int64 `db:"total_count" json:"total_count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func initDB() *sql.DB {
|
||||||
|
db, err := sql.Open("postgres", connStr)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
|
||||||
|
// Все получаем в одном запросе не аллоцируя при этом массив
|
||||||
|
func BenchmarkAllOne(b *testing.B) {
|
||||||
|
db := initDB()
|
||||||
|
defer db.Close()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
page := 1
|
||||||
|
size := 25
|
||||||
|
rows, err := db.Query(queryTotal, accountID, page, size)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var results []GetFieldsWithPaginationRow
|
||||||
|
for rows.Next() {
|
||||||
|
var row GetFieldsWithPaginationRow
|
||||||
|
if err := rows.Scan(
|
||||||
|
&row.ID,
|
||||||
|
&row.Amoid,
|
||||||
|
&row.Code,
|
||||||
|
&row.Accountid,
|
||||||
|
&row.Name,
|
||||||
|
&row.Entity,
|
||||||
|
&row.Type,
|
||||||
|
&row.Deleted,
|
||||||
|
&row.Createdat,
|
||||||
|
&row.TotalCount,
|
||||||
|
); err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
results = append(results, row)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Все получаем в одном запросе аллоцируя при этом массив
|
||||||
|
func BenchmarkAllOnePreAllocation(b *testing.B) {
|
||||||
|
db := initDB()
|
||||||
|
defer db.Close()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
page := 1
|
||||||
|
size := 25
|
||||||
|
rows, err := db.Query(queryTotal, accountID, page, size)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
results := make([]GetFieldsWithPaginationRow, size)
|
||||||
|
for rows.Next() {
|
||||||
|
var row GetFieldsWithPaginationRow
|
||||||
|
if err := rows.Scan(
|
||||||
|
&row.ID,
|
||||||
|
&row.Amoid,
|
||||||
|
&row.Code,
|
||||||
|
&row.Accountid,
|
||||||
|
&row.Name,
|
||||||
|
&row.Entity,
|
||||||
|
&row.Type,
|
||||||
|
&row.Deleted,
|
||||||
|
&row.Createdat,
|
||||||
|
&row.TotalCount,
|
||||||
|
); err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
results = append(results, row)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Считается сначала количество потом получаются данные длину и емкость массиву не меняем
|
||||||
|
func BenchmarkCountThenGetData(b *testing.B) {
|
||||||
|
db := initDB()
|
||||||
|
defer db.Close()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
page := 1
|
||||||
|
size := 25
|
||||||
|
|
||||||
|
row := db.QueryRow(queryCount, accountID)
|
||||||
|
var totalCount int
|
||||||
|
if err := row.Scan(&totalCount); err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
var results []GetFieldsWithPaginationRow
|
||||||
|
rows, err := db.Query(queryData, accountID, page, size)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
var row GetFieldsWithPaginationRow
|
||||||
|
if err := rows.Scan(
|
||||||
|
&row.ID,
|
||||||
|
&row.Amoid,
|
||||||
|
&row.Code,
|
||||||
|
&row.Accountid,
|
||||||
|
&row.Name,
|
||||||
|
&row.Entity,
|
||||||
|
&row.Type,
|
||||||
|
&row.Deleted,
|
||||||
|
&row.Createdat,
|
||||||
|
); err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
results = append(results, row)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Параллельное вычисление данных и общего количество при этом длина слайса = size
|
||||||
|
func BenchmarkParallel(b *testing.B) {
|
||||||
|
db := initDB()
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
page := 1
|
||||||
|
size := 25
|
||||||
|
results := make([]GetFieldsWithPaginationRow, size)
|
||||||
|
channel := make(chan error, 2)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
row := db.QueryRow(queryCount, accountID)
|
||||||
|
var totalCount int
|
||||||
|
channel <- row.Scan(&totalCount)
|
||||||
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
rows, err := db.Query(queryData, accountID, page, size)
|
||||||
|
if err != nil {
|
||||||
|
channel <- err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
index := 0
|
||||||
|
for rows.Next() {
|
||||||
|
if err := rows.Scan(
|
||||||
|
&results[index].ID,
|
||||||
|
&results[index].Amoid,
|
||||||
|
&results[index].Code,
|
||||||
|
&results[index].Accountid,
|
||||||
|
&results[index].Name,
|
||||||
|
&results[index].Entity,
|
||||||
|
&results[index].Type,
|
||||||
|
&results[index].Deleted,
|
||||||
|
&results[index].Createdat,
|
||||||
|
); err != nil {
|
||||||
|
channel <- err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
index++
|
||||||
|
}
|
||||||
|
channel <- rows.Err()
|
||||||
|
}()
|
||||||
|
|
||||||
|
for i := 0; i < 2; i++ {
|
||||||
|
if err := <-channel; err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Считается сначала количество потом получаются данные создаем слайс через маке указывая ему длину начальную кап = лен
|
||||||
|
func BenchmarkWithPreAllocation(b *testing.B) {
|
||||||
|
db := initDB()
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
page := 1
|
||||||
|
size := 25
|
||||||
|
results := make([]GetFieldsWithPaginationRow, size)
|
||||||
|
|
||||||
|
row := db.QueryRow(queryCount, accountID)
|
||||||
|
var totalCount int
|
||||||
|
if err := row.Scan(&totalCount); err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
rows, err := db.Query(queryData, accountID, page, size)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
index := 0
|
||||||
|
for rows.Next() {
|
||||||
|
if err := rows.Scan(
|
||||||
|
&results[index].ID,
|
||||||
|
&results[index].Amoid,
|
||||||
|
&results[index].Code,
|
||||||
|
&results[index].Accountid,
|
||||||
|
&results[index].Name,
|
||||||
|
&results[index].Entity,
|
||||||
|
&results[index].Type,
|
||||||
|
&results[index].Deleted,
|
||||||
|
&results[index].Createdat,
|
||||||
|
); err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
index++
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkWithPreAllocationAndMonitoringTotalCount(b *testing.B) {
|
||||||
|
db := initDB()
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
page := 1
|
||||||
|
size := 50
|
||||||
|
|
||||||
|
row := db.QueryRow(queryCount, accountID)
|
||||||
|
var totalCount int
|
||||||
|
if err := row.Scan(&totalCount); err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
if totalCount < size {
|
||||||
|
size = totalCount
|
||||||
|
}
|
||||||
|
results := make([]GetFieldsWithPaginationRow, size)
|
||||||
|
rows, err := db.Query(queryData, accountID, page, size)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
index := 0
|
||||||
|
for rows.Next() {
|
||||||
|
if err := rows.Scan(
|
||||||
|
&results[index].ID,
|
||||||
|
&results[index].Amoid,
|
||||||
|
&results[index].Code,
|
||||||
|
&results[index].Accountid,
|
||||||
|
&results[index].Name,
|
||||||
|
&results[index].Entity,
|
||||||
|
&results[index].Type,
|
||||||
|
&results[index].Deleted,
|
||||||
|
&results[index].Createdat,
|
||||||
|
); err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
index++
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
35
cmd/main.go
Normal file
35
cmd/main.go
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"gitea.pena/SQuiz/core/internal/app"
|
||||||
|
"gitea.pena/SQuiz/core/internal/initialize"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
commit string = os.Getenv("COMMIT")
|
||||||
|
buildTime string = os.Getenv("BUILD_TIME")
|
||||||
|
version string = os.Getenv("VERSION")
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
config, err := initialize.LoadConfig()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Failed to load config", zap.Error(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
defer stop()
|
||||||
|
|
||||||
|
if err = app.Run(ctx, *config, app.Build{
|
||||||
|
Commit: commit,
|
||||||
|
Version: version,
|
||||||
|
}); err != nil {
|
||||||
|
log.Fatal("App exited with error", zap.Error(err))
|
||||||
|
}
|
||||||
|
}
|
85
cmd/validator/main.go
Normal file
85
cmd/validator/main.go
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"gitea.pena/PenaSide/common/validate"
|
||||||
|
"gitea.pena/SQuiz/common/dal"
|
||||||
|
"gitea.pena/SQuiz/core/internal/initialize"
|
||||||
|
"github.com/caarlos0/env/v8"
|
||||||
|
"log"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cfg, err := loadConfig()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("error loading config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = validateNotEmpty(cfg)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("error validating config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = dal.New(context.TODO(), cfg.PostgresURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("error connecting to database postgres: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = dal.NewClickHouseDAL(context.TODO(), cfg.ClickhouseURL)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("error connecting to database clickhouse: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = validate.ValidateKafka([]string{cfg.KafkaBrokers}, cfg.KafkaTopicNotifyer)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("error validating kafka: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = validate.ValidateRedis(cfg.RedisHost, cfg.RedisPassword, int(cfg.RedisDB))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("error validating redis: %v", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadConfig() (initialize.Config, error) {
|
||||||
|
var cfg initialize.Config
|
||||||
|
|
||||||
|
if err := env.Parse(&cfg); err != nil {
|
||||||
|
return cfg, err
|
||||||
|
}
|
||||||
|
return cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateNotEmpty(cfg initialize.Config) error {
|
||||||
|
if cfg.ClientHttpURL == "" {
|
||||||
|
return errors.New("client http url dont be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.GrpcURL == "" {
|
||||||
|
return errors.New("grpc url dont be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.HubadminMicroserviceURL == "" {
|
||||||
|
return errors.New("hubadmin microservice url dont be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.AuthMicroserviceURL == "" {
|
||||||
|
return errors.New("auth microservice url dont be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.TrashLogHost == "" {
|
||||||
|
return errors.New("trash log host dont be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.S3Prefix == "" {
|
||||||
|
return errors.New("s3 prefix dont be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.ServiceName == "" {
|
||||||
|
return errors.New("service name dont be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
22
deployments/staging/config.env
Normal file
22
deployments/staging/config.env
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
IS_PROD_LOG="false"
|
||||||
|
IS_PROD="false"
|
||||||
|
CLIENT_HTTP_URL="0.0.0.0:1488"
|
||||||
|
GRPC_URL="0.0.0.0:9000"
|
||||||
|
POSTGRES_URL="host=10.7.0.10 port=5432 user=squiz password=Redalert2 dbname=squiz sslmode=disable"
|
||||||
|
CLICKHOUSE_URL="clickhouse://10.7.0.5:9000/default?sslmode=disable"
|
||||||
|
HUBADMIN_MICROSERVICE_URL="http://10.7.0.6:59303"
|
||||||
|
AUTH_MICROSERVICE_URL="http://10.7.0.6:59300/user"
|
||||||
|
KAFKA_BROKERS="10.7.0.6:9092"
|
||||||
|
KAFKA_TOPIC="mailnotifier"
|
||||||
|
KAFKA_GROUP="mailnotifier"
|
||||||
|
TRASH_LOG_HOST="10.7.0.5:7113"
|
||||||
|
S3_PREFIX="3c580be9-cf31f296-d055-49cf-b39e-30c7959dc17b"
|
||||||
|
REDIS_HOST="10.7.0.6:6379"
|
||||||
|
REDIS_PASSWORD="Redalert2"
|
||||||
|
REDIS_DB=2
|
||||||
|
PUBLIC_ACCESS_SECRET_KEY="-----BEGIN PUBLIC KEY-----
|
||||||
|
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCLW1tlHyKC9AG0hGpmkksET2DE
|
||||||
|
r7ojSPemxFWAgFgcPJWQ7x3uNbsdJ3bIZFoA/FClaWKMCZmjnH9tv0bKZtY/CDhM
|
||||||
|
ZEyHpMruRSn6IKrxjtQZWy4uv/w6MzUeyBYG0OvNCiYpdvz5SkAGAUHD5ZNFqn2w
|
||||||
|
KKFD0I2Dr59BFVSGJwIDAQAB
|
||||||
|
-----END PUBLIC KEY-----"
|
@ -1,27 +1,14 @@
|
|||||||
version: "3"
|
|
||||||
services:
|
services:
|
||||||
core:
|
core:
|
||||||
hostname: squiz-core
|
hostname: squiz
|
||||||
container_name: squiz-core
|
container_name: squiz
|
||||||
image: $CI_REGISTRY_IMAGE/staging-core:$CI_COMMIT_REF_SLUG.$CI_PIPELINE_ID
|
|
||||||
tty: true
|
tty: true
|
||||||
environment:
|
image: gitea.pena/squiz/core/staging:$GITHUB_RUN_NUMBER
|
||||||
HUB_ADMIN_URL: 'http://10.8.0.6:59303'
|
labels:
|
||||||
IS_PROD_LOG: 'false'
|
com.pena.allowed_headers: content-type,authorization,device,browser,os,devicetype,response-type
|
||||||
IS_PROD: 'false'
|
env_file: config.env
|
||||||
PORT: 1488
|
|
||||||
PUBLIC_ACCESS_SECRET_KEY: $JWT_PUBLIC_KEY
|
|
||||||
PG_CRED: 'host=10.8.0.5 port=5433 user=squiz password=Redalert2 dbname=squiz sslmode=disable'
|
|
||||||
AUTH_URL: 'http://10.8.0.6:59300/user'
|
|
||||||
PUBLIC_KEY: $PEM_PUB_USERID
|
|
||||||
PRIVATE_KEY: $PEM_PRIV_USERID
|
|
||||||
REDIRECT_URL: 'https://quiz.pena.digital'
|
|
||||||
KAFKA_BROKERS: 10.8.0.6:9092
|
|
||||||
KAFKA_TOPIC: "mailnotifier"
|
|
||||||
GRPC_HOST: "0.0.0.0"
|
|
||||||
TRASH_LOG_HOST: "10.8.0.15:7113"
|
|
||||||
MODULE_LOGGER: "quiz-core-staging"
|
|
||||||
CLICK_HOUSE_CRED: "clickhouse://10.8.0.15:9000/default?sslmode=disable"
|
|
||||||
ports:
|
ports:
|
||||||
- 10.8.0.5:1488:1488
|
- 10.7.0.10:1488:1488
|
||||||
- 10.8.0.5:9000:9000
|
- 10.7.0.10:9000:9000
|
||||||
|
- 10.7.0.10:2346:2345
|
||||||
|
command: dlv --listen=:2345 --continue --headless=true --log=true --log-output=debugger,debuglineerr,gdbwire,lldbout,rpc --accept-multiclient --api-version=2 exec /core
|
||||||
|
6
deployments/staging/validate_config.yml
Normal file
6
deployments/staging/validate_config.yml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
services:
|
||||||
|
validator:
|
||||||
|
tty: true
|
||||||
|
command: ./validator
|
||||||
|
image: gitea.pena/squiz/core/staging:$GITHUB_RUN_NUMBER
|
||||||
|
env_file: config.env
|
1
go.mod
1
go.mod
@ -38,6 +38,7 @@ require (
|
|||||||
github.com/go-redis/redis/v8 v8.11.5 // indirect
|
github.com/go-redis/redis/v8 v8.11.5 // indirect
|
||||||
github.com/goccy/go-json v0.10.5 // indirect
|
github.com/goccy/go-json v0.10.5 // indirect
|
||||||
github.com/golang/protobuf v1.5.4 // indirect
|
github.com/golang/protobuf v1.5.4 // indirect
|
||||||
|
github.com/golang/snappy v0.0.1 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/klauspost/compress v1.18.0 // indirect
|
github.com/klauspost/compress v1.18.0 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
||||||
|
23
go.sum
23
go.sum
@ -35,7 +35,6 @@ github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA
|
|||||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||||
github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58 h1:F1EaeKL/ta07PY/k9Os/UFtwERei2/XzGemhpGnBKNg=
|
|
||||||
github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58/go.mod h1:EOBUe0h4xcZ5GoxqC5SDxFQ8gwyZPKQoEzownBlhI80=
|
github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58/go.mod h1:EOBUe0h4xcZ5GoxqC5SDxFQ8gwyZPKQoEzownBlhI80=
|
||||||
github.com/cloudflare/golz4 v0.0.0-20240916140612-caecf3c00c06 h1:6aQNgrBLzcUBaJHQjMk4X+jDo9rQtu5E0XNLhRV6pOk=
|
github.com/cloudflare/golz4 v0.0.0-20240916140612-caecf3c00c06 h1:6aQNgrBLzcUBaJHQjMk4X+jDo9rQtu5E0XNLhRV6pOk=
|
||||||
github.com/cloudflare/golz4 v0.0.0-20240916140612-caecf3c00c06/go.mod h1:EOBUe0h4xcZ5GoxqC5SDxFQ8gwyZPKQoEzownBlhI80=
|
github.com/cloudflare/golz4 v0.0.0-20240916140612-caecf3c00c06/go.mod h1:EOBUe0h4xcZ5GoxqC5SDxFQ8gwyZPKQoEzownBlhI80=
|
||||||
@ -59,6 +58,10 @@ github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
|
|||||||
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
|
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
|
||||||
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
|
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
|
||||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||||
|
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||||
|
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
|
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
|
||||||
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
|
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
|
||||||
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||||
@ -84,7 +87,10 @@ github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y
|
|||||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||||
|
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
|
||||||
|
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||||
|
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||||
@ -139,6 +145,8 @@ github.com/minio/minio-go/v7 v7.0.91 h1:tWLZnEfo3OZl5PoXQwcwTAPNNrjyWwOh6cbZitW5
|
|||||||
github.com/minio/minio-go/v7 v7.0.91/go.mod h1:uvMUcGrpgeSAAI6+sD3818508nUyMULw94j2Nxku/Go=
|
github.com/minio/minio-go/v7 v7.0.91/go.mod h1:uvMUcGrpgeSAAI6+sD3818508nUyMULw94j2Nxku/Go=
|
||||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
|
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
|
||||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
|
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
|
||||||
|
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe h1:iruDEfMl2E6fbMZ9s0scYfZQ84/6SPL6zC8ACM2oIL0=
|
||||||
|
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
|
||||||
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
||||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||||
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
||||||
@ -251,8 +259,8 @@ golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7
|
|||||||
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
|
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
|
||||||
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
|
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
|
||||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/image v0.14.0 h1:tNgSxAFe3jC4uYqvZdTr84SZoM1KfwdC9SKIFrLjFn4=
|
golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ=
|
||||||
golang.org/x/image v0.14.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
|
golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
|
||||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
@ -260,6 +268,7 @@ golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHl
|
|||||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
@ -288,9 +297,12 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h
|
|||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
|
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
|
||||||
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
@ -314,6 +326,7 @@ golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtn
|
|||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
@ -1,16 +0,0 @@
|
|||||||
package initialize
|
|
||||||
|
|
||||||
import (
|
|
||||||
"gitea.pena/SQuiz/common/dal"
|
|
||||||
"gitea.pena/SQuiz/core/rpc_service"
|
|
||||||
)
|
|
||||||
|
|
||||||
type RpcRegister struct {
|
|
||||||
MailNotify *rpc_service.MailNotify
|
|
||||||
}
|
|
||||||
|
|
||||||
func InitRpcControllers(dal *dal.DAL) *RpcRegister {
|
|
||||||
return &RpcRegister{
|
|
||||||
MailNotify: rpc_service.NewMailNotify(dal),
|
|
||||||
}
|
|
||||||
}
|
|
185
internal/app/app.go
Normal file
185
internal/app/app.go
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"gitea.pena/PenaSide/common/privilege"
|
||||||
|
"gitea.pena/PenaSide/hlog"
|
||||||
|
"gitea.pena/PenaSide/trashlog/wrappers/zaptrashlog"
|
||||||
|
"gitea.pena/SQuiz/common/model"
|
||||||
|
"gitea.pena/SQuiz/core/internal/brokers"
|
||||||
|
"gitea.pena/SQuiz/core/internal/initialize"
|
||||||
|
"gitea.pena/SQuiz/core/internal/models"
|
||||||
|
server "gitea.pena/SQuiz/core/internal/server/grpc"
|
||||||
|
"gitea.pena/SQuiz/core/internal/server/http"
|
||||||
|
"gitea.pena/SQuiz/core/internal/tools"
|
||||||
|
"gitea.pena/SQuiz/core/internal/workers"
|
||||||
|
"gitea.pena/SQuiz/core/pkg/closer"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/gofiber/fiber/v2/log"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"go.uber.org/zap/zapcore"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Build struct {
|
||||||
|
Commit string
|
||||||
|
Version string
|
||||||
|
}
|
||||||
|
|
||||||
|
var zapOptions = []zap.Option{
|
||||||
|
zap.AddCaller(),
|
||||||
|
zap.AddCallerSkip(2),
|
||||||
|
zap.AddStacktrace(zap.ErrorLevel),
|
||||||
|
}
|
||||||
|
|
||||||
|
func Run(ctx context.Context, cfg initialize.Config, build Build) error {
|
||||||
|
var (
|
||||||
|
err error
|
||||||
|
zapLogger *zap.Logger
|
||||||
|
)
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
log.Error("Recovered from a panic", zap.Any("error", r))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if cfg.LoggerProdMode {
|
||||||
|
zapLogger, err = zap.NewProduction(zapOptions...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
zapLogger, err = zap.NewDevelopment(zapOptions...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
zapLogger = zapLogger.With(
|
||||||
|
zap.String("SvcCommit", build.Commit),
|
||||||
|
zap.String("SvcVersion", build.Version),
|
||||||
|
zap.String("SvcBuildTime", time.Now().String()),
|
||||||
|
)
|
||||||
|
|
||||||
|
clickHouseLogger, err := zaptrashlog.NewCore(ctx, zap.InfoLevel, cfg.TrashLogHost, build.Version, build.Commit, time.Now().Unix())
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
loggerForHlog := zapLogger.WithOptions(zap.WrapCore(func(core zapcore.Core) zapcore.Core {
|
||||||
|
return zapcore.NewTee(core, clickHouseLogger)
|
||||||
|
}))
|
||||||
|
|
||||||
|
loggerHlog := hlog.New(loggerForHlog).Module(initialize.ModuleLogger)
|
||||||
|
loggerHlog.With(models.AllFields{})
|
||||||
|
loggerHlog.Emit(InfoSvcStarted{})
|
||||||
|
|
||||||
|
shutdownGroup := closer.NewCloserGroup()
|
||||||
|
|
||||||
|
dalS, err := initialize.NewDALs(ctx, cfg)
|
||||||
|
if err != nil {
|
||||||
|
zapLogger.Error("Error initializing dals", zap.Error(err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
kafkaClient, err := initialize.KafkaInit(ctx, initialize.KafkaDeps{
|
||||||
|
KafkaGroup: cfg.KafkaGroup,
|
||||||
|
KafkaBrokers: cfg.KafkaBrokers,
|
||||||
|
KafkaTopic: cfg.KafkaTopicNotifyer,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
zapLogger.Error("Error initializing kafka", zap.Error(err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
producer := brokers.NewProducer(brokers.ProducerDeps{
|
||||||
|
KafkaClient: kafkaClient,
|
||||||
|
Logger: zapLogger,
|
||||||
|
})
|
||||||
|
|
||||||
|
redisClient, err := initialize.Redis(ctx, cfg)
|
||||||
|
if err != nil {
|
||||||
|
zapLogger.Error("Error initializing redis", zap.Error(err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
go tools.PublishPrivilege(privilege.NewPrivilege(privilege.Client{
|
||||||
|
URL: cfg.HubadminMicroserviceURL,
|
||||||
|
ServiceName: cfg.ServiceName,
|
||||||
|
Privileges: model.Privileges,
|
||||||
|
}, &fiber.Client{}), 10, 5*time.Minute)
|
||||||
|
|
||||||
|
clients, err := initialize.NewClients(ctx, cfg, dalS.PgDAL)
|
||||||
|
if err != nil {
|
||||||
|
zapLogger.Error("Error initializing clients", zap.Error(err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
tgWC := workers.NewTgListenerWC(workers.Deps{
|
||||||
|
BotID: int64(6712573453), // todo убрать
|
||||||
|
Redis: redisClient,
|
||||||
|
Dal: dalS.PgDAL,
|
||||||
|
//TgClient: clients.TgClient,
|
||||||
|
})
|
||||||
|
|
||||||
|
go tgWC.Start(ctx)
|
||||||
|
|
||||||
|
controllers := initialize.NewControllers(initialize.ControllerDeps{
|
||||||
|
Clients: clients,
|
||||||
|
DALs: dalS,
|
||||||
|
Config: cfg,
|
||||||
|
Producer: producer,
|
||||||
|
RedisClient: redisClient,
|
||||||
|
})
|
||||||
|
|
||||||
|
grpc, err := server.NewGRPC(zapLogger)
|
||||||
|
if err != nil {
|
||||||
|
zapLogger.Error("Error initializing grpc", zap.Error(err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
grpc.Register(controllers.GRpcControllers)
|
||||||
|
|
||||||
|
srv := http.NewServer(http.ServerConfig{
|
||||||
|
Logger: zapLogger,
|
||||||
|
Controllers: []http.Controller{controllers.HttpControllers.Account, controllers.HttpControllers.Telegram, controllers.HttpControllers.Result,
|
||||||
|
controllers.HttpControllers.Question, controllers.HttpControllers.Quiz, controllers.HttpControllers.Statistic},
|
||||||
|
Hlogger: loggerHlog,
|
||||||
|
})
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
if err := srv.Start(cfg.ClientHttpURL); err != nil {
|
||||||
|
zapLogger.Error("HTTP server startup error", zap.Error(err))
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
go grpc.Run(cfg.GrpcURL)
|
||||||
|
|
||||||
|
srv.ListRoutes()
|
||||||
|
|
||||||
|
shutdownGroup.Add(closer.CloserFunc(srv.Shutdown))
|
||||||
|
shutdownGroup.Add(closer.CloserFunc(grpc.Stop))
|
||||||
|
shutdownGroup.Add(closer.CloserFunc(dalS.PgDAL.Close))
|
||||||
|
shutdownGroup.Add(closer.CloserFunc(dalS.ChDAL.Close))
|
||||||
|
|
||||||
|
<-ctx.Done()
|
||||||
|
|
||||||
|
timeoutCtx, timeoutCancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer timeoutCancel()
|
||||||
|
if err := shutdownGroup.Call(timeoutCtx); err != nil {
|
||||||
|
if errors.Is(err, context.DeadlineExceeded) {
|
||||||
|
zapLogger.Error("Shutdown timed out", zap.Error(err))
|
||||||
|
} else {
|
||||||
|
zapLogger.Error("Failed to shutdown services gracefully", zap.Error(err))
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
zapLogger.Info("Application has stopped")
|
||||||
|
return nil
|
||||||
|
}
|
246
internal/clients/telegram/tg.go
Normal file
246
internal/clients/telegram/tg.go
Normal file
@ -0,0 +1,246 @@
|
|||||||
|
package telegram
|
||||||
|
//
|
||||||
|
// import (
|
||||||
|
// "context"
|
||||||
|
// "errors"
|
||||||
|
// "fmt"
|
||||||
|
// "path/filepath"
|
||||||
|
// "penahub.gitlab.yandexcloud.net/backend/quiz/common.git/dal"
|
||||||
|
// "penahub.gitlab.yandexcloud.net/backend/quiz/common.git/model"
|
||||||
|
// "penahub.gitlab.yandexcloud.net/backend/quiz/common.git/pj_errors"
|
||||||
|
// "penahub.gitlab.yandexcloud.net/backend/tdlib/client"
|
||||||
|
// "sync"
|
||||||
|
// "time"
|
||||||
|
// )
|
||||||
|
//
|
||||||
|
// type TelegramClient struct {
|
||||||
|
// repo *dal.DAL
|
||||||
|
// TgClients map[int64]*client.Client
|
||||||
|
// WaitingClients map[string]WaitingClient
|
||||||
|
// mu sync.Mutex
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// type WaitingClient struct {
|
||||||
|
// PreviousReq AuthTgUserReq
|
||||||
|
// Authorizer *client.ClientAuthorizer
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// func NewTelegramClient(ctx context.Context, repo *dal.DAL) (*TelegramClient, error) {
|
||||||
|
// tgClient := &TelegramClient{
|
||||||
|
// repo: repo,
|
||||||
|
// TgClients: make(map[int64]*client.Client),
|
||||||
|
// WaitingClients: make(map[string]WaitingClient),
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// allTgAccounts, err := repo.TgRepo.GetAllTgAccounts(ctx)
|
||||||
|
// if err != nil {
|
||||||
|
// if errors.Is(err, pj_errors.ErrNotFound) {
|
||||||
|
// return tgClient, nil
|
||||||
|
// }
|
||||||
|
// return nil, err
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// for _, account := range allTgAccounts {
|
||||||
|
// if account.Status == model.ActiveTg {
|
||||||
|
// authorizer := client.ClientAuthorizerr()
|
||||||
|
// authorizer.TdlibParameters <- &client.SetTdlibParametersRequest{
|
||||||
|
// UseTestDc: false,
|
||||||
|
// DatabaseDirectory: filepath.Join(".tdlib", "database"),
|
||||||
|
// FilesDirectory: filepath.Join(".tdlib", "files"),
|
||||||
|
// UseFileDatabase: true,
|
||||||
|
// UseChatInfoDatabase: true,
|
||||||
|
// UseMessageDatabase: true,
|
||||||
|
// UseSecretChats: true,
|
||||||
|
// ApiId: account.ApiID,
|
||||||
|
// ApiHash: account.ApiHash,
|
||||||
|
// SystemLanguageCode: "en",
|
||||||
|
// DeviceModel: "Server",
|
||||||
|
// SystemVersion: "1.0.0",
|
||||||
|
// ApplicationVersion: "1.0.0",
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// _, err := client.SetLogVerbosityLevel(&client.SetLogVerbosityLevelRequest{
|
||||||
|
// NewVerbosityLevel: 1,
|
||||||
|
// })
|
||||||
|
// if err != nil {
|
||||||
|
// return nil, err
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// var tdlibClient *client.Client
|
||||||
|
// var goErr error
|
||||||
|
// go func() {
|
||||||
|
// tdlibClient, goErr = client.NewClient(authorizer)
|
||||||
|
// if goErr != nil {
|
||||||
|
// fmt.Println("new client failed", err)
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
// fmt.Println("i am down")
|
||||||
|
// }()
|
||||||
|
// if goErr != nil {
|
||||||
|
// return nil, goErr
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// for {
|
||||||
|
// state, ok := <-authorizer.State
|
||||||
|
// if !ok {
|
||||||
|
// break
|
||||||
|
// }
|
||||||
|
// fmt.Println("currnet state:", state)
|
||||||
|
// switch state.AuthorizationStateType() {
|
||||||
|
// case client.TypeAuthorizationStateWaitPhoneNumber:
|
||||||
|
// authorizer.PhoneNumber <- account.PhoneNumber
|
||||||
|
// case client.TypeAuthorizationStateWaitCode:
|
||||||
|
// err := tgClient.repo.TgRepo.UpdateStatusTg(ctx, account.ID, model.InactiveTg)
|
||||||
|
// if err != nil {
|
||||||
|
// return nil, err
|
||||||
|
// }
|
||||||
|
// case client.TypeAuthorizationStateLoggingOut, client.TypeAuthorizationStateClosing, client.TypeAuthorizationStateClosed:
|
||||||
|
// err := tgClient.repo.TgRepo.UpdateStatusTg(ctx, account.ID, model.InactiveTg)
|
||||||
|
// if err != nil {
|
||||||
|
// return nil, err
|
||||||
|
// }
|
||||||
|
// case client.TypeAuthorizationStateReady:
|
||||||
|
// // костыль так как в либе тож костыль стоит пока там ьд обновиться будет ниловый всегда клиент
|
||||||
|
// time.Sleep(3 * time.Second)
|
||||||
|
// me, err := tdlibClient.GetMe()
|
||||||
|
// if err != nil {
|
||||||
|
// return nil, err
|
||||||
|
// }
|
||||||
|
// fmt.Printf("Me: %s %s [%v]", me.FirstName, me.LastName, me.Usernames)
|
||||||
|
// tgClient.mu.Lock()
|
||||||
|
// tgClient.TgClients[account.ID] = tdlibClient
|
||||||
|
// tgClient.mu.Unlock()
|
||||||
|
// break
|
||||||
|
// case client.TypeAuthorizationStateWaitPassword:
|
||||||
|
// authorizer.Password <- account.Password
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// return tgClient, nil
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// type AuthTgUserReq struct {
|
||||||
|
// ApiID int32 `json:"api_id"`
|
||||||
|
// ApiHash string `json:"api_hash"`
|
||||||
|
// PhoneNumber string `json:"phone_number"`
|
||||||
|
// Password string `json:"password"`
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// func (tg *TelegramClient) AddedToMap(data WaitingClient, id string) {
|
||||||
|
// fmt.Println("AddedToMap")
|
||||||
|
// tg.mu.Lock()
|
||||||
|
// defer tg.mu.Unlock()
|
||||||
|
// tg.WaitingClients[id] = data
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// func (tg *TelegramClient) GetFromMap(id string) (WaitingClient, bool) {
|
||||||
|
// fmt.Println("GetFromMap")
|
||||||
|
// tg.mu.Lock()
|
||||||
|
// defer tg.mu.Unlock()
|
||||||
|
// if data, ok := tg.WaitingClients[id]; ok {
|
||||||
|
// delete(tg.WaitingClients, id)
|
||||||
|
// return data, true
|
||||||
|
// }
|
||||||
|
// return WaitingClient{}, false
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// func (tg *TelegramClient) SaveTgAccount(appID int32, appHash string, tdLibClient *client.Client) {
|
||||||
|
// account, err := tg.repo.TgRepo.SearchIDByAppIDanAppHash(context.Background(), appID, appHash)
|
||||||
|
// if err != nil {
|
||||||
|
// fmt.Println("err SaveTgAccount", err)
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
// if account.Status == model.ActiveTg {
|
||||||
|
// tg.mu.Lock()
|
||||||
|
// defer tg.mu.Unlock()
|
||||||
|
// tg.TgClients[account.ID] = tdLibClient
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// func (tg *TelegramClient) CreateChannel(channelName string, botID int64) (string, int64, error) {
|
||||||
|
// tg.mu.Lock()
|
||||||
|
// defer tg.mu.Unlock()
|
||||||
|
// if len(tg.TgClients) == 0 {
|
||||||
|
// return "", 0, errors.New("no active Telegram clients")
|
||||||
|
// }
|
||||||
|
// var lastError error
|
||||||
|
// var inviteLink string
|
||||||
|
// var channelId int64
|
||||||
|
// for _, activeClient := range tg.TgClients {
|
||||||
|
// // todo пока не понимаю это какой то рандом? в один день бот норм находится в другой уже не находится хотя абсолютно с точки зрения тг кода этой функции и бота не менялось
|
||||||
|
// _, err := activeClient.GetUser(&client.GetUserRequest{
|
||||||
|
// UserId: botID,
|
||||||
|
// })
|
||||||
|
// if err != nil {
|
||||||
|
// lastError = fmt.Errorf("not found this bot, make privacy off: %v", err)
|
||||||
|
// continue
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // todo нужно поймать ошибку, при которой либо бан либо медленный редим включается для того чтобы прервать
|
||||||
|
// // исполнение клиента текущего аккаунта и дать задачу следующему пока поймал 1 раз и не запомнил больше не получается
|
||||||
|
// channel, err := activeClient.CreateNewSupergroupChat(&client.CreateNewSupergroupChatRequest{
|
||||||
|
// Title: channelName,
|
||||||
|
// IsChannel: true,
|
||||||
|
// Description: "private channel",
|
||||||
|
// })
|
||||||
|
// if err != nil {
|
||||||
|
// lastError = fmt.Errorf("failed to create channel: %s", err.Error())
|
||||||
|
// continue
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// _, err = activeClient.SetChatMemberStatus(&client.SetChatMemberStatusRequest{
|
||||||
|
// ChatId: channel.Id,
|
||||||
|
// MemberId: &client.MessageSenderUser{UserId: botID},
|
||||||
|
// Status: &client.ChatMemberStatusAdministrator{
|
||||||
|
// CustomTitle: "bot",
|
||||||
|
// Rights: &client.ChatAdministratorRights{
|
||||||
|
// CanManageChat: true,
|
||||||
|
// CanChangeInfo: true,
|
||||||
|
// CanPostMessages: true,
|
||||||
|
// CanEditMessages: true,
|
||||||
|
// CanDeleteMessages: true,
|
||||||
|
// CanInviteUsers: true,
|
||||||
|
// CanRestrictMembers: true,
|
||||||
|
// CanPinMessages: true,
|
||||||
|
// CanManageTopics: true,
|
||||||
|
// CanPromoteMembers: true,
|
||||||
|
// CanManageVideoChats: true,
|
||||||
|
// CanPostStories: true,
|
||||||
|
// CanEditStories: true,
|
||||||
|
// CanDeleteStories: true,
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// })
|
||||||
|
// if err != nil {
|
||||||
|
// lastError = fmt.Errorf("failed to make bot admin: %s", err.Error())
|
||||||
|
// continue
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// inviteLinkResp, err := activeClient.CreateChatInviteLink(&client.CreateChatInviteLinkRequest{
|
||||||
|
// ChatId: channel.Id,
|
||||||
|
// Name: channelName,
|
||||||
|
// ExpirationDate: 0,
|
||||||
|
// MemberLimit: 0,
|
||||||
|
// CreatesJoinRequest: false,
|
||||||
|
// })
|
||||||
|
// if err != nil {
|
||||||
|
// lastError = fmt.Errorf("failed to get invite link: %s", err.Error())
|
||||||
|
// continue
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// _, err = activeClient.LeaveChat(&client.LeaveChatRequest{
|
||||||
|
// ChatId: channel.Id,
|
||||||
|
// })
|
||||||
|
// if err != nil {
|
||||||
|
// lastError = fmt.Errorf("failed to leave the channel: %s", err.Error())
|
||||||
|
// continue
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// inviteLink = inviteLinkResp.InviteLink
|
||||||
|
// channelId = channel.Id
|
||||||
|
// return inviteLink, channelId, nil
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// return "", 0, lastError
|
||||||
|
// }
|
420
internal/controllers/http_controllers/account/account.go
Normal file
420
internal/controllers/http_controllers/account/account.go
Normal file
@ -0,0 +1,420 @@
|
|||||||
|
package account
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"gitea.pena/PenaSide/common/log_mw"
|
||||||
|
"gitea.pena/SQuiz/common/dal"
|
||||||
|
"gitea.pena/SQuiz/common/middleware"
|
||||||
|
"gitea.pena/SQuiz/common/model"
|
||||||
|
"gitea.pena/SQuiz/common/pj_errors"
|
||||||
|
"gitea.pena/SQuiz/core/internal/brokers"
|
||||||
|
"gitea.pena/SQuiz/core/internal/clients/auth"
|
||||||
|
"gitea.pena/SQuiz/core/internal/models"
|
||||||
|
"github.com/go-redis/redis/v8"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Deps struct {
|
||||||
|
Dal *dal.DAL
|
||||||
|
AuthClient *auth.AuthClient
|
||||||
|
Producer *brokers.Producer
|
||||||
|
ServiceName string
|
||||||
|
RedisClient *redis.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
type Account struct {
|
||||||
|
dal *dal.DAL
|
||||||
|
authClient *auth.AuthClient
|
||||||
|
producer *brokers.Producer
|
||||||
|
serviceName string
|
||||||
|
redisClient *redis.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAccountController(deps Deps) *Account {
|
||||||
|
return &Account{
|
||||||
|
dal: deps.Dal,
|
||||||
|
authClient: deps.AuthClient,
|
||||||
|
producer: deps.Producer,
|
||||||
|
serviceName: deps.ServiceName,
|
||||||
|
redisClient: deps.RedisClient,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateAccountReq struct {
|
||||||
|
UserID string `json:"userId"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateAccountResp struct {
|
||||||
|
CreatedAccount model.Account `json:"created_account"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeleteAccountResp struct {
|
||||||
|
DeletedAccountID string `json:"account_Id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetPrivilegeByUserIDReq struct {
|
||||||
|
UserID string `json:"userId"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeleteAccountByUserIDReq struct {
|
||||||
|
UserID string `json:"userId"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeleteAccountByUserIDResp struct {
|
||||||
|
DeletedAccountUserID string `json:"userId"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetAccountsReq struct {
|
||||||
|
Limit uint64 `json:"limit"`
|
||||||
|
Page uint64 `json:"page"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetAccountsResp struct {
|
||||||
|
Count uint64 `json:"count"`
|
||||||
|
Items []model.Account `json:"items"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// getCurrentAccount обработчик для получения текущего аккаунта
|
||||||
|
func (r *Account) GetCurrentAccount(ctx *fiber.Ctx) error {
|
||||||
|
accountID, ok := middleware.GetAccountId(ctx)
|
||||||
|
if !ok {
|
||||||
|
return ctx.Status(fiber.StatusUnauthorized).SendString("account id is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
account, err := r.dal.AccountRepo.GetAccountByID(ctx.Context(), accountID)
|
||||||
|
if err != nil && err != sql.ErrNoRows {
|
||||||
|
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: fix this later
|
||||||
|
if account.ID == "" {
|
||||||
|
return ctx.Status(fiber.StatusNotFound).SendString("no account")
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.Status(fiber.StatusOK).JSON(account)
|
||||||
|
}
|
||||||
|
|
||||||
|
// createAccount обработчик для создания нового аккаунта
|
||||||
|
func (r *Account) CreateAccount(ctx *fiber.Ctx) error {
|
||||||
|
accountID, ok := middleware.GetAccountId(ctx)
|
||||||
|
if !ok {
|
||||||
|
return ctx.Status(fiber.StatusUnauthorized).SendString("account id is required")
|
||||||
|
}
|
||||||
|
hlogger := log_mw.ExtractLogger(ctx)
|
||||||
|
|
||||||
|
existingAccount, err := r.dal.AccountRepo.GetAccountByID(ctx.Context(), accountID)
|
||||||
|
if err != nil && err != sql.ErrNoRows {
|
||||||
|
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
||||||
|
}
|
||||||
|
if existingAccount.ID != "" {
|
||||||
|
return ctx.Status(fiber.StatusConflict).SendString("user with this ID already exists")
|
||||||
|
}
|
||||||
|
|
||||||
|
email, err := r.authClient.GetUserEmail(accountID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
newAccount := model.Account{
|
||||||
|
UserID: accountID,
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
Deleted: false,
|
||||||
|
Privileges: map[string]model.ShortPrivilege{
|
||||||
|
"quizUnlimTime": {
|
||||||
|
PrivilegeID: "quizUnlimTime",
|
||||||
|
PrivilegeName: "Безлимит Опросов",
|
||||||
|
Amount: 14,
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
createdAcc, err := r.dal.AccountRepo.CreateAccount(ctx.Context(), &newAccount)
|
||||||
|
if err != nil {
|
||||||
|
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
||||||
|
}
|
||||||
|
_, err = r.dal.AccountRepo.PostLeadTarget(ctx.Context(), model.LeadTarget{
|
||||||
|
AccountID: accountID,
|
||||||
|
Target: email,
|
||||||
|
Type: model.LeadTargetEmail,
|
||||||
|
QuizID: 0,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
hlogger.Emit(models.InfoAccountCreated{
|
||||||
|
CtxUserID: accountID,
|
||||||
|
CtxAccountID: createdAcc.ID,
|
||||||
|
})
|
||||||
|
|
||||||
|
err = r.producer.ToMailNotify(ctx.Context(), brokers.Message{
|
||||||
|
AccountID: accountID,
|
||||||
|
Email: email,
|
||||||
|
ServiceKey: r.serviceName,
|
||||||
|
SendAt: time.Now(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.JSON(CreateAccountResp{
|
||||||
|
CreatedAccount: newAccount,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// deleteAccount обработчик для удаления текущего аккаунта
|
||||||
|
func (r *Account) DeleteAccount(ctx *fiber.Ctx) error {
|
||||||
|
accountID, ok := middleware.GetAccountId(ctx)
|
||||||
|
if !ok {
|
||||||
|
return ctx.Status(fiber.StatusUnauthorized).SendString("account id is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
account, err := r.dal.AccountRepo.GetAccountByID(ctx.Context(), accountID)
|
||||||
|
if err != nil {
|
||||||
|
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.dal.AccountRepo.DeleteAccount(ctx.Context(), account.ID); err != nil {
|
||||||
|
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.JSON(DeleteAccountResp{
|
||||||
|
DeletedAccountID: accountID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// getPrivilegeByUserID обработчик для получения привилегий аккаунта по ID пользователя
|
||||||
|
func (r *Account) GetPrivilegeByUserID(ctx *fiber.Ctx) error {
|
||||||
|
var req GetPrivilegeByUserIDReq
|
||||||
|
if err := ctx.BodyParser(&req); err != nil {
|
||||||
|
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
|
||||||
|
}
|
||||||
|
|
||||||
|
privilege, err := r.dal.AccountRepo.GetPrivilegesByAccountID(ctx.Context(), req.UserID)
|
||||||
|
if err != nil {
|
||||||
|
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.Status(fiber.StatusOK).JSON(privilege)
|
||||||
|
}
|
||||||
|
|
||||||
|
// deleteAccountByUserID обработчик для удаления аккаунта по ID пользователя
|
||||||
|
func (r *Account) DeleteAccountByUserID(ctx *fiber.Ctx) error {
|
||||||
|
var req DeleteAccountByUserIDReq
|
||||||
|
if err := ctx.BodyParser(&req); err != nil {
|
||||||
|
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
|
||||||
|
}
|
||||||
|
|
||||||
|
existingAccount, err := r.dal.AccountRepo.GetAccountByID(ctx.Context(), req.UserID)
|
||||||
|
if err != nil {
|
||||||
|
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if existingAccount.ID == "" {
|
||||||
|
return ctx.Status(fiber.StatusInternalServerError).SendString("user with this ID not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.dal.AccountRepo.DeleteAccount(ctx.Context(), existingAccount.ID); err != nil {
|
||||||
|
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.JSON(DeleteAccountByUserIDResp{
|
||||||
|
DeletedAccountUserID: req.UserID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// getAccounts обработчик для получения списка аккаунтов с пагинацией
|
||||||
|
func (r *Account) GetAccounts(ctx *fiber.Ctx) error {
|
||||||
|
var req GetAccountsReq
|
||||||
|
if err := ctx.BodyParser(&req); err != nil {
|
||||||
|
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
|
||||||
|
}
|
||||||
|
|
||||||
|
_, ok := middleware.GetAccountId(ctx)
|
||||||
|
if !ok {
|
||||||
|
return ctx.Status(fiber.StatusUnauthorized).SendString("account id is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
accounts, totalCount, err := r.dal.AccountRepo.GetAccounts(ctx.Context(), req.Limit, req.Page)
|
||||||
|
if err != nil {
|
||||||
|
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
response := GetAccountsResp{
|
||||||
|
Count: totalCount,
|
||||||
|
Items: accounts,
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.Status(fiber.StatusOK).JSON(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Account) ManualDone(ctx *fiber.Ctx) error {
|
||||||
|
var req struct {
|
||||||
|
Id string `json:"id"`
|
||||||
|
}
|
||||||
|
if err := ctx.BodyParser(&req); err != nil {
|
||||||
|
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Id == "" {
|
||||||
|
return ctx.Status(fiber.StatusBadRequest).SendString("User id is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
err := r.dal.AccountRepo.ManualDone(ctx.Context(), req.Id)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, pj_errors.ErrNotFound) {
|
||||||
|
return ctx.Status(fiber.StatusNotFound).SendString("user don't have this privilege")
|
||||||
|
}
|
||||||
|
return ctx.Status(fiber.StatusInternalServerError).SendString("Internal Server Error")
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.SendStatus(fiber.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Account) PostLeadTarget(ctx *fiber.Ctx) error {
|
||||||
|
var req struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
QuizID int32 `json:"quizID"`
|
||||||
|
Target string `json:"target"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
if err := ctx.BodyParser(&req); err != nil {
|
||||||
|
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
|
||||||
|
}
|
||||||
|
|
||||||
|
accountID, ok := middleware.GetAccountId(ctx)
|
||||||
|
if !ok {
|
||||||
|
return ctx.Status(fiber.StatusUnauthorized).SendString("account id is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
//accountID := "64f2cd7a7047f28fdabf6d9e"
|
||||||
|
|
||||||
|
if _, ok := model.ValidLeadTargetTypes[req.Type]; !ok {
|
||||||
|
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid type")
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Type == "" || (req.Target == "" && req.Type != string(model.LeadTargetTg)) {
|
||||||
|
return ctx.Status(fiber.StatusBadRequest).SendString("Type and Target don't be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
switch req.Type {
|
||||||
|
case "mail":
|
||||||
|
_, err := r.dal.AccountRepo.PostLeadTarget(ctx.Context(), model.LeadTarget{
|
||||||
|
AccountID: accountID,
|
||||||
|
Target: req.Target,
|
||||||
|
Type: model.LeadTargetType(req.Type),
|
||||||
|
QuizID: req.QuizID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
||||||
|
}
|
||||||
|
return ctx.SendStatus(fiber.StatusOK)
|
||||||
|
case "telegram":
|
||||||
|
targets, err := r.dal.AccountRepo.GetLeadTarget(ctx.Context(), accountID, req.QuizID)
|
||||||
|
if err != nil && !errors.Is(err, pj_errors.ErrNotFound) {
|
||||||
|
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
||||||
|
}
|
||||||
|
if !errors.Is(err, pj_errors.ErrNotFound) {
|
||||||
|
for _, t := range targets {
|
||||||
|
if t.Type == model.LeadTargetTg {
|
||||||
|
return ctx.Status(fiber.StatusAlreadyReported).SendString("LeadTarget for this quiz already exist")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
task := model.TgRedisTask{
|
||||||
|
Name: req.Name,
|
||||||
|
QuizID: req.QuizID,
|
||||||
|
AccountID: accountID,
|
||||||
|
}
|
||||||
|
|
||||||
|
taskKey := fmt.Sprintf("telegram_task:%d", time.Now().UnixNano())
|
||||||
|
taskData, err := json.Marshal(task)
|
||||||
|
if err != nil {
|
||||||
|
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.redisClient.Set(ctx.Context(), taskKey, taskData, 0).Err(); err != nil {
|
||||||
|
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
||||||
|
}
|
||||||
|
case "whatsapp":
|
||||||
|
return ctx.Status(fiber.StatusOK).SendString("todo")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Account) DeleteLeadTarget(ctx *fiber.Ctx) error {
|
||||||
|
leadIDStr := ctx.Params("id")
|
||||||
|
leadID, err := strconv.ParseInt(leadIDStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid lead ID format")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = r.dal.AccountRepo.DeleteLeadTarget(ctx.Context(), leadID)
|
||||||
|
if err != nil {
|
||||||
|
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
||||||
|
}
|
||||||
|
return ctx.SendStatus(fiber.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Account) GetLeadTarget(ctx *fiber.Ctx) error {
|
||||||
|
accountID, ok := middleware.GetAccountId(ctx)
|
||||||
|
if !ok {
|
||||||
|
return ctx.Status(fiber.StatusUnauthorized).SendString("account id is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
quizIDStr := ctx.Params("quizID")
|
||||||
|
quizID, err := strconv.ParseInt(quizIDStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid quiz ID format")
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := r.dal.AccountRepo.GetLeadTarget(ctx.Context(), accountID, int32(quizID))
|
||||||
|
if err != nil {
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, pj_errors.ErrNotFound):
|
||||||
|
return ctx.Status(fiber.StatusNotFound).SendString("this lead target not found")
|
||||||
|
default:
|
||||||
|
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.Status(fiber.StatusOK).JSON(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Account) UpdateLeadTarget(ctx *fiber.Ctx) error {
|
||||||
|
var req struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Target string `json:"target"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ctx.BodyParser(&req); err != nil {
|
||||||
|
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.ID == 0 || req.Target == "" {
|
||||||
|
return ctx.Status(fiber.StatusBadRequest).SendString("ID and Target don't be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := r.dal.AccountRepo.UpdateLeadTarget(ctx.Context(), model.LeadTarget{
|
||||||
|
ID: req.ID,
|
||||||
|
Target: req.Target,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, pj_errors.ErrNotFound):
|
||||||
|
return ctx.Status(fiber.StatusNotFound).SendString("this lead target not found")
|
||||||
|
default:
|
||||||
|
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.Status(fiber.StatusOK).JSON(result)
|
||||||
|
}
|
21
internal/controllers/http_controllers/account/route.go
Normal file
21
internal/controllers/http_controllers/account/route.go
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
package account
|
||||||
|
|
||||||
|
import "github.com/gofiber/fiber/v2"
|
||||||
|
|
||||||
|
func (r *Account) Register(router fiber.Router) {
|
||||||
|
router.Get("/account/get", r.GetCurrentAccount)
|
||||||
|
router.Post("/account/create", r.CreateAccount)
|
||||||
|
router.Delete("/account/delete", r.DeleteAccount)
|
||||||
|
router.Get("/accounts", r.GetAccounts)
|
||||||
|
router.Get("/privilege/:userId", r.GetPrivilegeByUserID)
|
||||||
|
router.Delete("/account/:userId", r.DeleteAccountByUserID)
|
||||||
|
router.Post("/account/manualdone", r.ManualDone)
|
||||||
|
router.Post("/account/leadtarget", r.PostLeadTarget)
|
||||||
|
router.Delete("/account/leadtarget/:id", r.DeleteLeadTarget)
|
||||||
|
router.Get("/account/leadtarget/:quizID", r.GetLeadTarget)
|
||||||
|
router.Put("/account/leadtarget", r.UpdateLeadTarget)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Account) Name() string {
|
||||||
|
return ""
|
||||||
|
}
|
@ -1,15 +1,28 @@
|
|||||||
package service
|
package question
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"gitea.pena/PenaSide/common/log_mw"
|
"gitea.pena/PenaSide/common/log_mw"
|
||||||
|
"gitea.pena/SQuiz/common/dal"
|
||||||
"gitea.pena/SQuiz/common/middleware"
|
"gitea.pena/SQuiz/common/middleware"
|
||||||
"gitea.pena/SQuiz/common/model"
|
"gitea.pena/SQuiz/common/model"
|
||||||
"gitea.pena/SQuiz/core/models"
|
"gitea.pena/SQuiz/core/internal/models"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"github.com/lib/pq"
|
"github.com/lib/pq"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type Deps struct {
|
||||||
|
DAL *dal.DAL
|
||||||
|
}
|
||||||
|
|
||||||
|
type Question struct {
|
||||||
|
dal *dal.DAL
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewQuestionController(deps Deps) *Question {
|
||||||
|
return &Question{dal: deps.DAL}
|
||||||
|
}
|
||||||
|
|
||||||
// QuestionCreateReq request structure for creating Question
|
// QuestionCreateReq request structure for creating Question
|
||||||
type QuestionCreateReq struct {
|
type QuestionCreateReq struct {
|
||||||
QuizId uint64 `json:"quiz_id"` // relation to quiz
|
QuizId uint64 `json:"quiz_id"` // relation to quiz
|
||||||
@ -23,7 +36,7 @@ type QuestionCreateReq struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CreateQuestion service handler for creating question for quiz
|
// CreateQuestion service handler for creating question for quiz
|
||||||
func (s *Service) CreateQuestion(ctx *fiber.Ctx) error {
|
func (r *Question) CreateQuestion(ctx *fiber.Ctx) error {
|
||||||
accountID, ok := middleware.GetAccountId(ctx)
|
accountID, ok := middleware.GetAccountId(ctx)
|
||||||
if !ok {
|
if !ok {
|
||||||
return ctx.Status(fiber.StatusUnauthorized).SendString("account id is required")
|
return ctx.Status(fiber.StatusUnauthorized).SendString("account id is required")
|
||||||
@ -63,7 +76,8 @@ func (s *Service) CreateQuestion(ctx *fiber.Ctx) error {
|
|||||||
Page: req.Page,
|
Page: req.Page,
|
||||||
Content: req.Content,
|
Content: req.Content,
|
||||||
}
|
}
|
||||||
questionID, err := s.dal.QuestionRepo.CreateQuestion(ctx.Context(), &result)
|
|
||||||
|
questionID, err := r.dal.QuestionRepo.CreateQuestion(ctx.Context(), &result)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if e, ok := err.(*pq.Error); ok {
|
if e, ok := err.(*pq.Error); ok {
|
||||||
if e.Constraint == "quiz_relation" {
|
if e.Constraint == "quiz_relation" {
|
||||||
@ -103,7 +117,7 @@ type GetQuestionListResp struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetQuestionList handler for paginated list question
|
// GetQuestionList handler for paginated list question
|
||||||
func (s *Service) GetQuestionList(ctx *fiber.Ctx) error {
|
func (r *Question) GetQuestionList(ctx *fiber.Ctx) error {
|
||||||
var req GetQuestionListReq
|
var req GetQuestionListReq
|
||||||
if err := ctx.BodyParser(&req); err != nil {
|
if err := ctx.BodyParser(&req); err != nil {
|
||||||
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
|
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
|
||||||
@ -126,7 +140,7 @@ func (s *Service) GetQuestionList(ctx *fiber.Ctx) error {
|
|||||||
"'test','none','file', 'button','select','checkbox'")
|
"'test','none','file', 'button','select','checkbox'")
|
||||||
}
|
}
|
||||||
|
|
||||||
res, cnt, err := s.dal.QuestionRepo.GetQuestionList(ctx.Context(),
|
res, cnt, err := r.dal.QuestionRepo.GetQuestionList(ctx.Context(),
|
||||||
req.Limit,
|
req.Limit,
|
||||||
req.Page*req.Limit,
|
req.Page*req.Limit,
|
||||||
uint64(req.From),
|
uint64(req.From),
|
||||||
@ -165,7 +179,7 @@ type UpdateResp struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// UpdateQuestion handler for update question
|
// UpdateQuestion handler for update question
|
||||||
func (s *Service) UpdateQuestion(ctx *fiber.Ctx) error {
|
func (r *Question) UpdateQuestion(ctx *fiber.Ctx) error {
|
||||||
var req UpdateQuestionReq
|
var req UpdateQuestionReq
|
||||||
if err := ctx.BodyParser(&req); err != nil {
|
if err := ctx.BodyParser(&req); err != nil {
|
||||||
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
|
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
|
||||||
@ -195,7 +209,7 @@ func (s *Service) UpdateQuestion(ctx *fiber.Ctx) error {
|
|||||||
return ctx.Status(fiber.StatusNotAcceptable).SendString("type must be only test,button,file,checkbox,select, none or empty string")
|
return ctx.Status(fiber.StatusNotAcceptable).SendString("type must be only test,button,file,checkbox,select, none or empty string")
|
||||||
}
|
}
|
||||||
|
|
||||||
question, err := s.dal.QuestionRepo.MoveToHistoryQuestion(ctx.Context(), req.Id)
|
question, err := r.dal.QuestionRepo.MoveToHistoryQuestion(ctx.Context(), req.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
||||||
}
|
}
|
||||||
@ -227,7 +241,7 @@ func (s *Service) UpdateQuestion(ctx *fiber.Ctx) error {
|
|||||||
question.Content = req.Content
|
question.Content = req.Content
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.dal.QuestionRepo.UpdateQuestion(ctx.Context(), question); err != nil {
|
if err := r.dal.QuestionRepo.UpdateQuestion(ctx.Context(), question); err != nil {
|
||||||
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -243,7 +257,7 @@ type CopyQuestionReq struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CopyQuestion handler for copy question
|
// CopyQuestion handler for copy question
|
||||||
func (s *Service) CopyQuestion(ctx *fiber.Ctx) error {
|
func (r *Question) CopyQuestion(ctx *fiber.Ctx) error {
|
||||||
var req CopyQuestionReq
|
var req CopyQuestionReq
|
||||||
if err := ctx.BodyParser(&req); err != nil {
|
if err := ctx.BodyParser(&req); err != nil {
|
||||||
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
|
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
|
||||||
@ -253,7 +267,7 @@ func (s *Service) CopyQuestion(ctx *fiber.Ctx) error {
|
|||||||
return ctx.Status(fiber.StatusFailedDependency).SendString("no id provided")
|
return ctx.Status(fiber.StatusFailedDependency).SendString("no id provided")
|
||||||
}
|
}
|
||||||
|
|
||||||
question, err := s.dal.QuestionRepo.CopyQuestion(ctx.Context(), req.Id, req.QuizId)
|
question, err := r.dal.QuestionRepo.CopyQuestion(ctx.Context(), req.Id, req.QuizId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
||||||
}
|
}
|
||||||
@ -271,8 +285,8 @@ type GetQuestionHistoryReq struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetQuestionHistory handler for history of quiz
|
// GetQuestionHistory handler for history of quiz
|
||||||
func (s *Service) GetQuestionHistory(ctx *fiber.Ctx) error {
|
func (r *Question) GetQuestionHistory(ctx *fiber.Ctx) error {
|
||||||
var req GetQuizHistoryReq
|
var req GetQuestionHistoryReq
|
||||||
if err := ctx.BodyParser(&req); err != nil {
|
if err := ctx.BodyParser(&req); err != nil {
|
||||||
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
|
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
|
||||||
}
|
}
|
||||||
@ -281,7 +295,7 @@ func (s *Service) GetQuestionHistory(ctx *fiber.Ctx) error {
|
|||||||
return ctx.Status(fiber.StatusFailedDependency).SendString("no id provided")
|
return ctx.Status(fiber.StatusFailedDependency).SendString("no id provided")
|
||||||
}
|
}
|
||||||
|
|
||||||
history, err := s.dal.QuestionRepo.QuestionHistory(ctx.Context(), req.Id, req.Limit, req.Page*req.Limit)
|
history, err := r.dal.QuestionRepo.QuestionHistory(ctx.Context(), req.Id, req.Limit, req.Page*req.Limit)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
||||||
}
|
}
|
||||||
@ -289,15 +303,22 @@ func (s *Service) GetQuestionHistory(ctx *fiber.Ctx) error {
|
|||||||
return ctx.Status(fiber.StatusOK).JSON(history)
|
return ctx.Status(fiber.StatusOK).JSON(history)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DeactivateResp struct {
|
||||||
|
Deactivated uint64 `json:"deactivated"`
|
||||||
|
}
|
||||||
|
|
||||||
// DeleteQuestion handler for fake delete question
|
// DeleteQuestion handler for fake delete question
|
||||||
func (s *Service) DeleteQuestion(ctx *fiber.Ctx) error {
|
func (r *Question) DeleteQuestion(ctx *fiber.Ctx) error {
|
||||||
accountID, ok := middleware.GetAccountId(ctx)
|
accountID, ok := middleware.GetAccountId(ctx)
|
||||||
if !ok {
|
if !ok {
|
||||||
return ctx.Status(fiber.StatusUnauthorized).SendString("account id is required")
|
return ctx.Status(fiber.StatusUnauthorized).SendString("account id is required")
|
||||||
}
|
}
|
||||||
hlogger := log_mw.ExtractLogger(ctx)
|
hlogger := log_mw.ExtractLogger(ctx)
|
||||||
|
|
||||||
var req DeactivateReq
|
var req struct {
|
||||||
|
Id uint64 `json:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
if err := ctx.BodyParser(&req); err != nil {
|
if err := ctx.BodyParser(&req); err != nil {
|
||||||
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
|
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
|
||||||
}
|
}
|
||||||
@ -306,7 +327,7 @@ func (s *Service) DeleteQuestion(ctx *fiber.Ctx) error {
|
|||||||
return ctx.Status(fiber.StatusFailedDependency).SendString("id for deleting question is required")
|
return ctx.Status(fiber.StatusFailedDependency).SendString("id for deleting question is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
deleted, err := s.dal.QuestionRepo.DeleteQuestion(ctx.Context(), req.Id)
|
deleted, err := r.dal.QuestionRepo.DeleteQuestion(ctx.Context(), req.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
||||||
}
|
}
|
16
internal/controllers/http_controllers/question/route.go
Normal file
16
internal/controllers/http_controllers/question/route.go
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
package question
|
||||||
|
|
||||||
|
import "github.com/gofiber/fiber/v2"
|
||||||
|
|
||||||
|
func (r *Question) Register(router fiber.Router) {
|
||||||
|
router.Post("/create", r.CreateQuestion)
|
||||||
|
router.Post("/getList", r.GetQuestionList)
|
||||||
|
router.Patch("/edit", r.UpdateQuestion)
|
||||||
|
router.Post("/copy", r.CopyQuestion)
|
||||||
|
router.Post("/history", r.GetQuestionHistory)
|
||||||
|
router.Delete("/delete", r.DeleteQuestion)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Question) Name() string {
|
||||||
|
return "question"
|
||||||
|
}
|
@ -1,19 +1,31 @@
|
|||||||
package service
|
package quiz
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"gitea.pena/PenaSide/common/log_mw"
|
"gitea.pena/PenaSide/common/log_mw"
|
||||||
|
"gitea.pena/SQuiz/common/dal"
|
||||||
"gitea.pena/SQuiz/common/middleware"
|
"gitea.pena/SQuiz/common/middleware"
|
||||||
"gitea.pena/SQuiz/common/model"
|
"gitea.pena/SQuiz/common/model"
|
||||||
"gitea.pena/SQuiz/common/repository/quiz"
|
"gitea.pena/SQuiz/common/repository/quiz"
|
||||||
"gitea.pena/SQuiz/core/brokers"
|
"gitea.pena/SQuiz/core/brokers"
|
||||||
"gitea.pena/SQuiz/core/models"
|
"gitea.pena/SQuiz/core/internal/models"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"strconv"
|
|
||||||
"time"
|
"time"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type Deps struct {
|
||||||
|
DAL *dal.DAL
|
||||||
|
}
|
||||||
|
|
||||||
|
type Quiz struct {
|
||||||
|
dal *dal.DAL
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewQuizController(deps Deps) *Quiz {
|
||||||
|
return &Quiz{dal: deps.DAL}
|
||||||
|
}
|
||||||
|
|
||||||
type CreateQuizReq struct {
|
type CreateQuizReq struct {
|
||||||
Fingerprinting bool `json:"fingerprinting"` // true if you need to store device id
|
Fingerprinting bool `json:"fingerprinting"` // true if you need to store device id
|
||||||
Repeatable bool `json:"repeatable"` // make it true for allow more than one quiz checkouting
|
Repeatable bool `json:"repeatable"` // make it true for allow more than one quiz checkouting
|
||||||
@ -38,7 +50,7 @@ type CreateQuizReq struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CreateQuiz handler for quiz creating request
|
// CreateQuiz handler for quiz creating request
|
||||||
func (s *Service) CreateQuiz(ctx *fiber.Ctx) error {
|
func (r *Quiz) CreateQuiz(ctx *fiber.Ctx) error {
|
||||||
var req CreateQuizReq
|
var req CreateQuizReq
|
||||||
if err := ctx.BodyParser(&req); err != nil {
|
if err := ctx.BodyParser(&req); err != nil {
|
||||||
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
|
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
|
||||||
@ -98,7 +110,7 @@ func (s *Service) CreateQuiz(ctx *fiber.Ctx) error {
|
|||||||
GroupId: req.GroupId,
|
GroupId: req.GroupId,
|
||||||
}
|
}
|
||||||
|
|
||||||
quizID, err := s.dal.QuizRepo.CreateQuiz(ctx.Context(), &record)
|
quizID, err := r.dal.QuizRepo.CreateQuiz(ctx.Context(), &record)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
||||||
}
|
}
|
||||||
@ -132,7 +144,7 @@ type GetQuizListResp struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetQuizList handler for paginated list quiz
|
// GetQuizList handler for paginated list quiz
|
||||||
func (s *Service) GetQuizList(ctx *fiber.Ctx) error {
|
func (r *Quiz) GetQuizList(ctx *fiber.Ctx) error {
|
||||||
var req GetQuizListReq
|
var req GetQuizListReq
|
||||||
if err := ctx.BodyParser(&req); err != nil {
|
if err := ctx.BodyParser(&req); err != nil {
|
||||||
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
|
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
|
||||||
@ -155,7 +167,7 @@ func (s *Service) GetQuizList(ctx *fiber.Ctx) error {
|
|||||||
"'stop','start','draft', 'template','timeout','offlimit'")
|
"'stop','start','draft', 'template','timeout','offlimit'")
|
||||||
}
|
}
|
||||||
|
|
||||||
res, cnt, err := s.dal.QuizRepo.GetQuizList(ctx.Context(),
|
res, cnt, err := r.dal.QuizRepo.GetQuizList(ctx.Context(),
|
||||||
quiz.GetQuizListDeps{
|
quiz.GetQuizListDeps{
|
||||||
Limit: req.Limit,
|
Limit: req.Limit,
|
||||||
Offset: req.Limit * req.Page,
|
Offset: req.Limit * req.Page,
|
||||||
@ -199,7 +211,11 @@ type UpdateQuizReq struct {
|
|||||||
GroupId uint64 `json:"group_id"`
|
GroupId uint64 `json:"group_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) UpdateQuiz(ctx *fiber.Ctx) error {
|
type UpdateResp struct {
|
||||||
|
Updated uint64 `json:"updated"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Quiz) UpdateQuiz(ctx *fiber.Ctx) error {
|
||||||
var req UpdateQuizReq
|
var req UpdateQuizReq
|
||||||
if err := ctx.BodyParser(&req); err != nil {
|
if err := ctx.BodyParser(&req); err != nil {
|
||||||
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
|
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
|
||||||
@ -241,7 +257,7 @@ func (s *Service) UpdateQuiz(ctx *fiber.Ctx) error {
|
|||||||
return ctx.Status(fiber.StatusConflict).SendString("you can pause quiz only if it has deadline for passing")
|
return ctx.Status(fiber.StatusConflict).SendString("you can pause quiz only if it has deadline for passing")
|
||||||
}
|
}
|
||||||
|
|
||||||
quiz, err := s.dal.QuizRepo.MoveToHistoryQuiz(ctx.Context(), req.Id, accountId)
|
quiz, err := r.dal.QuizRepo.MoveToHistoryQuiz(ctx.Context(), req.Id, accountId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
||||||
}
|
}
|
||||||
@ -313,7 +329,7 @@ func (s *Service) UpdateQuiz(ctx *fiber.Ctx) error {
|
|||||||
|
|
||||||
quiz.ParentIds = append(quiz.ParentIds, int32(quiz.Id))
|
quiz.ParentIds = append(quiz.ParentIds, int32(quiz.Id))
|
||||||
|
|
||||||
if err := s.dal.QuizRepo.UpdateQuiz(ctx.Context(), accountId, quiz); err != nil {
|
if err := r.dal.QuizRepo.UpdateQuiz(ctx.Context(), accountId, quiz); err != nil {
|
||||||
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -341,7 +357,7 @@ type CopyQuizReq struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CopyQuiz request handler for copy quiz
|
// CopyQuiz request handler for copy quiz
|
||||||
func (s *Service) CopyQuiz(ctx *fiber.Ctx) error {
|
func (r *Quiz) CopyQuiz(ctx *fiber.Ctx) error {
|
||||||
var req CopyQuizReq
|
var req CopyQuizReq
|
||||||
if err := ctx.BodyParser(&req); err != nil {
|
if err := ctx.BodyParser(&req); err != nil {
|
||||||
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
|
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
|
||||||
@ -356,7 +372,7 @@ func (s *Service) CopyQuiz(ctx *fiber.Ctx) error {
|
|||||||
return ctx.Status(fiber.StatusFailedDependency).SendString("no id provided")
|
return ctx.Status(fiber.StatusFailedDependency).SendString("no id provided")
|
||||||
}
|
}
|
||||||
|
|
||||||
quiz, err := s.dal.QuizRepo.CopyQuiz(ctx.Context(), accountId, req.Id)
|
quiz, err := r.dal.QuizRepo.CopyQuiz(ctx.Context(), accountId, req.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
||||||
}
|
}
|
||||||
@ -374,7 +390,7 @@ type GetQuizHistoryReq struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetQuizHistory handler for history of quiz
|
// GetQuizHistory handler for history of quiz
|
||||||
func (s *Service) GetQuizHistory(ctx *fiber.Ctx) error {
|
func (r *Quiz) GetQuizHistory(ctx *fiber.Ctx) error {
|
||||||
var req GetQuizHistoryReq
|
var req GetQuizHistoryReq
|
||||||
if err := ctx.BodyParser(&req); err != nil {
|
if err := ctx.BodyParser(&req); err != nil {
|
||||||
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
|
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
|
||||||
@ -388,7 +404,7 @@ func (s *Service) GetQuizHistory(ctx *fiber.Ctx) error {
|
|||||||
return ctx.Status(fiber.StatusFailedDependency).SendString("no id provided")
|
return ctx.Status(fiber.StatusFailedDependency).SendString("no id provided")
|
||||||
}
|
}
|
||||||
|
|
||||||
history, err := s.dal.QuizRepo.QuizHistory(ctx.Context(), quiz.QuizHistoryDeps{
|
history, err := r.dal.QuizRepo.QuizHistory(ctx.Context(), quiz.QuizHistoryDeps{
|
||||||
Id: req.Id,
|
Id: req.Id,
|
||||||
Limit: req.Limit,
|
Limit: req.Limit,
|
||||||
Offset: req.Page * req.Limit,
|
Offset: req.Page * req.Limit,
|
||||||
@ -411,7 +427,7 @@ type DeactivateResp struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DeleteQuiz handler for fake delete quiz
|
// DeleteQuiz handler for fake delete quiz
|
||||||
func (s *Service) DeleteQuiz(ctx *fiber.Ctx) error {
|
func (r *Quiz) DeleteQuiz(ctx *fiber.Ctx) error {
|
||||||
var req DeactivateReq
|
var req DeactivateReq
|
||||||
if err := ctx.BodyParser(&req); err != nil {
|
if err := ctx.BodyParser(&req); err != nil {
|
||||||
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
|
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
|
||||||
@ -427,7 +443,7 @@ func (s *Service) DeleteQuiz(ctx *fiber.Ctx) error {
|
|||||||
return ctx.Status(fiber.StatusFailedDependency).SendString("id for deleting is required")
|
return ctx.Status(fiber.StatusFailedDependency).SendString("id for deleting is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
deleted, err := s.dal.QuizRepo.DeleteQuiz(ctx.Context(), accountId, req.Id)
|
deleted, err := r.dal.QuizRepo.DeleteQuiz(ctx.Context(), accountId, req.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
||||||
}
|
}
|
||||||
@ -443,7 +459,7 @@ func (s *Service) DeleteQuiz(ctx *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ArchiveQuiz handler for archiving quiz
|
// ArchiveQuiz handler for archiving quiz
|
||||||
func (s *Service) ArchiveQuiz(ctx *fiber.Ctx) error {
|
func (r *Quiz) ArchiveQuiz(ctx *fiber.Ctx) error {
|
||||||
var req DeactivateReq
|
var req DeactivateReq
|
||||||
if err := ctx.BodyParser(&req); err != nil {
|
if err := ctx.BodyParser(&req); err != nil {
|
||||||
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
|
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
|
||||||
@ -458,7 +474,7 @@ func (s *Service) ArchiveQuiz(ctx *fiber.Ctx) error {
|
|||||||
return ctx.Status(fiber.StatusFailedDependency).SendString("id for archive quiz is required")
|
return ctx.Status(fiber.StatusFailedDependency).SendString("id for archive quiz is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
archived, err := s.dal.QuizRepo.DeleteQuiz(ctx.Context(), accountId, req.Id)
|
archived, err := r.dal.QuizRepo.DeleteQuiz(ctx.Context(), accountId, req.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
||||||
}
|
}
|
||||||
@ -472,7 +488,7 @@ type QuizMoveReq struct {
|
|||||||
Qid, AccountID string
|
Qid, AccountID string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) QuizMove(ctx *fiber.Ctx) error {
|
func (r *Quiz) QuizMove(ctx *fiber.Ctx) error {
|
||||||
var req QuizMoveReq
|
var req QuizMoveReq
|
||||||
if err := ctx.BodyParser(&req); err != nil {
|
if err := ctx.BodyParser(&req); err != nil {
|
||||||
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
|
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
|
||||||
@ -482,7 +498,7 @@ func (s *Service) QuizMove(ctx *fiber.Ctx) error {
|
|||||||
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request qid and accountID is required")
|
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request qid and accountID is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := s.dal.QuizRepo.QuizMove(ctx.Context(), req.Qid, req.AccountID)
|
resp, err := r.dal.QuizRepo.QuizMove(ctx.Context(), req.Qid, req.AccountID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
||||||
}
|
}
|
||||||
@ -490,7 +506,7 @@ func (s *Service) QuizMove(ctx *fiber.Ctx) error {
|
|||||||
return ctx.Status(fiber.StatusOK).JSON(resp)
|
return ctx.Status(fiber.StatusOK).JSON(resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) TemplateCopy(ctx *fiber.Ctx) error {
|
func (r *Quiz) TemplateCopy(ctx *fiber.Ctx) error {
|
||||||
accountID, ok := middleware.GetAccountId(ctx)
|
accountID, ok := middleware.GetAccountId(ctx)
|
||||||
if !ok {
|
if !ok {
|
||||||
return ctx.Status(fiber.StatusUnauthorized).SendString("account id is required")
|
return ctx.Status(fiber.StatusUnauthorized).SendString("account id is required")
|
||||||
@ -509,7 +525,7 @@ func (s *Service) TemplateCopy(ctx *fiber.Ctx) error {
|
|||||||
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request qid is required")
|
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request qid is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
qizID, err := s.dal.QuizRepo.TemplateCopy(ctx.Context(), accountID, req.Qid)
|
qizID, err := r.dal.QuizRepo.TemplateCopy(ctx.Context(), accountID, req.Qid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("TEMPLERR", err)
|
fmt.Println("TEMPLERR", err)
|
||||||
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
19
internal/controllers/http_controllers/quiz/route.go
Normal file
19
internal/controllers/http_controllers/quiz/route.go
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
package quiz
|
||||||
|
|
||||||
|
import "github.com/gofiber/fiber/v2"
|
||||||
|
|
||||||
|
func (r *Quiz) Register(router fiber.Router) {
|
||||||
|
router.Post("/create", r.CreateQuiz)
|
||||||
|
router.Post("/getList", r.GetQuizList)
|
||||||
|
router.Patch("/edit", r.UpdateQuiz)
|
||||||
|
router.Post("/copy", r.CopyQuiz)
|
||||||
|
router.Post("/history", r.GetQuizHistory)
|
||||||
|
router.Delete("/delete", r.DeleteQuiz)
|
||||||
|
router.Patch("/archive", r.ArchiveQuiz)
|
||||||
|
router.Post("/move", r.QuizMove)
|
||||||
|
router.Post("/template", r.TemplateCopy)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Quiz) Name() string {
|
||||||
|
return "quiz"
|
||||||
|
}
|
@ -1,16 +1,34 @@
|
|||||||
package service
|
package result
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"github.com/gofiber/fiber/v2"
|
"gitea.pena/SQuiz/common/dal"
|
||||||
"gitea.pena/SQuiz/common/middleware"
|
"gitea.pena/SQuiz/common/middleware"
|
||||||
"gitea.pena/SQuiz/common/model"
|
"gitea.pena/SQuiz/common/model"
|
||||||
"gitea.pena/SQuiz/common/repository/result"
|
"gitea.pena/SQuiz/common/repository/result"
|
||||||
"gitea.pena/SQuiz/core/tools"
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"gitea.pena/SQuiz/core/internal/tools"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type Deps struct {
|
||||||
|
DAL *dal.DAL
|
||||||
|
S3Prefix string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Result struct {
|
||||||
|
dal *dal.DAL
|
||||||
|
s3Prefix string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewResultController(deps Deps) *Result {
|
||||||
|
return &Result{
|
||||||
|
dal: deps.DAL,
|
||||||
|
s3Prefix: deps.S3Prefix,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type ReqExport struct {
|
type ReqExport struct {
|
||||||
To, From time.Time
|
To, From time.Time
|
||||||
New bool
|
New bool
|
||||||
@ -23,7 +41,7 @@ type ReqExportResponse struct {
|
|||||||
Results []model.AnswerExport `json:"results"`
|
Results []model.AnswerExport `json:"results"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) GetResultsByQuizID(ctx *fiber.Ctx) error {
|
func (r *Result) GetResultsByQuizID(ctx *fiber.Ctx) error {
|
||||||
payment := true // параметр для определения существования текущих привилегий юзера
|
payment := true // параметр для определения существования текущих привилегий юзера
|
||||||
|
|
||||||
accountID, ok := middleware.GetAccountId(ctx)
|
accountID, ok := middleware.GetAccountId(ctx)
|
||||||
@ -42,7 +60,7 @@ func (s *Service) GetResultsByQuizID(ctx *fiber.Ctx) error {
|
|||||||
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid quiz ID format")
|
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid quiz ID format")
|
||||||
}
|
}
|
||||||
|
|
||||||
account, err := s.dal.AccountRepo.GetAccountByID(ctx.Context(), accountID)
|
account, err := r.dal.AccountRepo.GetAccountByID(ctx.Context(), accountID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
||||||
}
|
}
|
||||||
@ -52,7 +70,7 @@ func (s *Service) GetResultsByQuizID(ctx *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
results, totalCount, err := s.dal.ResultRepo.GetQuizResults(ctx.Context(), quizID, result.GetQuizResDeps{
|
results, totalCount, err := r.dal.ResultRepo.GetQuizResults(ctx.Context(), quizID, result.GetQuizResDeps{
|
||||||
To: req.To,
|
To: req.To,
|
||||||
From: req.From,
|
From: req.From,
|
||||||
New: req.New,
|
New: req.New,
|
||||||
@ -71,7 +89,7 @@ func (s *Service) GetResultsByQuizID(ctx *fiber.Ctx) error {
|
|||||||
return ctx.Status(fiber.StatusOK).JSON(resp)
|
return ctx.Status(fiber.StatusOK).JSON(resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) DelResultByID(ctx *fiber.Ctx) error {
|
func (r *Result) DelResultByID(ctx *fiber.Ctx) error {
|
||||||
accountID, ok := middleware.GetAccountId(ctx)
|
accountID, ok := middleware.GetAccountId(ctx)
|
||||||
if !ok {
|
if !ok {
|
||||||
return ctx.Status(fiber.StatusUnauthorized).SendString("could not get account ID from token")
|
return ctx.Status(fiber.StatusUnauthorized).SendString("could not get account ID from token")
|
||||||
@ -83,7 +101,7 @@ func (s *Service) DelResultByID(ctx *fiber.Ctx) error {
|
|||||||
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid result ID format")
|
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid result ID format")
|
||||||
}
|
}
|
||||||
|
|
||||||
isOwner, err := s.dal.ResultRepo.CheckResultOwner(ctx.Context(), resultID, accountID)
|
isOwner, err := r.dal.ResultRepo.CheckResultOwner(ctx.Context(), resultID, accountID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
||||||
}
|
}
|
||||||
@ -92,7 +110,7 @@ func (s *Service) DelResultByID(ctx *fiber.Ctx) error {
|
|||||||
return ctx.Status(fiber.StatusUnauthorized).SendString("not the owner of the result")
|
return ctx.Status(fiber.StatusUnauthorized).SendString("not the owner of the result")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.dal.ResultRepo.SoftDeleteResultByID(ctx.Context(), resultID); err != nil {
|
if err := r.dal.ResultRepo.SoftDeleteResultByID(ctx.Context(), resultID); err != nil {
|
||||||
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -104,7 +122,7 @@ type ReqSeen struct {
|
|||||||
Answers []int64
|
Answers []int64
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) SetStatus(ctx *fiber.Ctx) error {
|
func (r *Result) SetStatus(ctx *fiber.Ctx) error {
|
||||||
var req ReqSeen
|
var req ReqSeen
|
||||||
if err := ctx.BodyParser(&req); err != nil {
|
if err := ctx.BodyParser(&req); err != nil {
|
||||||
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
|
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
|
||||||
@ -115,7 +133,7 @@ func (s *Service) SetStatus(ctx *fiber.Ctx) error {
|
|||||||
return ctx.Status(fiber.StatusUnauthorized).SendString("could not get account ID from token")
|
return ctx.Status(fiber.StatusUnauthorized).SendString("could not get account ID from token")
|
||||||
}
|
}
|
||||||
|
|
||||||
answers, err := s.dal.ResultRepo.CheckResultsOwner(ctx.Context(), req.Answers, accountID)
|
answers, err := r.dal.ResultRepo.CheckResultsOwner(ctx.Context(), req.Answers, accountID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
||||||
}
|
}
|
||||||
@ -124,14 +142,14 @@ func (s *Service) SetStatus(ctx *fiber.Ctx) error {
|
|||||||
return ctx.Status(fiber.StatusNotAcceptable).SendString("could not update some answers because you don't have rights")
|
return ctx.Status(fiber.StatusNotAcceptable).SendString("could not update some answers because you don't have rights")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.dal.ResultRepo.UpdateAnswersStatus(ctx.Context(), accountID, answers); err != nil {
|
if err := r.dal.ResultRepo.UpdateAnswersStatus(ctx.Context(), accountID, answers); err != nil {
|
||||||
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
return ctx.Status(fiber.StatusOK).JSON(nil)
|
return ctx.Status(fiber.StatusOK).JSON(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) ExportResultsToCSV(ctx *fiber.Ctx) error {
|
func (r *Result) ExportResultsToCSV(ctx *fiber.Ctx) error {
|
||||||
accountID, ok := middleware.GetAccountId(ctx)
|
accountID, ok := middleware.GetAccountId(ctx)
|
||||||
if !ok {
|
if !ok {
|
||||||
return ctx.Status(fiber.StatusUnauthorized).SendString("account id is required")
|
return ctx.Status(fiber.StatusUnauthorized).SendString("account id is required")
|
||||||
@ -148,7 +166,7 @@ func (s *Service) ExportResultsToCSV(ctx *fiber.Ctx) error {
|
|||||||
return ctx.Status(fiber.StatusBadRequest).SendString("invalid request body")
|
return ctx.Status(fiber.StatusBadRequest).SendString("invalid request body")
|
||||||
}
|
}
|
||||||
|
|
||||||
account, err := s.dal.AccountRepo.GetAccountByID(ctx.Context(), accountID)
|
account, err := r.dal.AccountRepo.GetAccountByID(ctx.Context(), accountID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
||||||
}
|
}
|
||||||
@ -159,17 +177,17 @@ func (s *Service) ExportResultsToCSV(ctx *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
quiz, err := s.dal.QuizRepo.GetQuizById(ctx.Context(), accountID, quizID)
|
quiz, err := r.dal.QuizRepo.GetQuizById(ctx.Context(), accountID, quizID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ctx.Status(fiber.StatusInternalServerError).SendString("failed to get quiz")
|
return ctx.Status(fiber.StatusInternalServerError).SendString("failed to get quiz")
|
||||||
}
|
}
|
||||||
|
|
||||||
questions, err := s.dal.ResultRepo.GetQuestions(ctx.Context(), quizID)
|
questions, err := r.dal.ResultRepo.GetQuestions(ctx.Context(), quizID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ctx.Status(fiber.StatusInternalServerError).SendString("failed to get questions")
|
return ctx.Status(fiber.StatusInternalServerError).SendString("failed to get questions")
|
||||||
}
|
}
|
||||||
|
|
||||||
answers, err := s.dal.ResultRepo.GetQuizResultsCSV(ctx.Context(), quizID, result.GetQuizResDeps{
|
answers, err := r.dal.ResultRepo.GetQuizResultsCSV(ctx.Context(), quizID, result.GetQuizResDeps{
|
||||||
To: req.To,
|
To: req.To,
|
||||||
From: req.From,
|
From: req.From,
|
||||||
New: req.New,
|
New: req.New,
|
||||||
@ -182,7 +200,7 @@ func (s *Service) ExportResultsToCSV(ctx *fiber.Ctx) error {
|
|||||||
|
|
||||||
buffer := new(bytes.Buffer)
|
buffer := new(bytes.Buffer)
|
||||||
|
|
||||||
if err := tools.WriteDataToExcel(buffer, questions, answers, s.s3Prefix + quiz.Qid + "/"); err != nil {
|
if err := tools.WriteDataToExcel(buffer, questions, answers, r.s3Prefix+quiz.Qid+"/"); err != nil {
|
||||||
return ctx.Status(fiber.StatusInternalServerError).SendString("failed to write data to Excel")
|
return ctx.Status(fiber.StatusInternalServerError).SendString("failed to write data to Excel")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -192,7 +210,7 @@ func (s *Service) ExportResultsToCSV(ctx *fiber.Ctx) error {
|
|||||||
return ctx.Send(buffer.Bytes())
|
return ctx.Send(buffer.Bytes())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) GetResultAnswers(ctx *fiber.Ctx) error {
|
func (r *Result) GetResultAnswers(ctx *fiber.Ctx) error {
|
||||||
accountID, ok := middleware.GetAccountId(ctx)
|
accountID, ok := middleware.GetAccountId(ctx)
|
||||||
if !ok {
|
if !ok {
|
||||||
return ctx.Status(fiber.StatusUnauthorized).SendString("account id is required")
|
return ctx.Status(fiber.StatusUnauthorized).SendString("account id is required")
|
||||||
@ -203,7 +221,7 @@ func (s *Service) GetResultAnswers(ctx *fiber.Ctx) error {
|
|||||||
return ctx.Status(fiber.StatusBadRequest).SendString("invalid quiz ID")
|
return ctx.Status(fiber.StatusBadRequest).SendString("invalid quiz ID")
|
||||||
}
|
}
|
||||||
|
|
||||||
account, err := s.dal.AccountRepo.GetAccountByID(ctx.Context(), accountID)
|
account, err := r.dal.AccountRepo.GetAccountByID(ctx.Context(), accountID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
||||||
}
|
}
|
||||||
@ -214,11 +232,11 @@ func (s *Service) GetResultAnswers(ctx *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
answers, err := s.dal.ResultRepo.GetResultAnswers(ctx.Context(), resultID)
|
answers, err := r.dal.ResultRepo.GetResultAnswers(ctx.Context(), resultID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ctx.Status(fiber.StatusInternalServerError).SendString("failed to get result answers")
|
return ctx.Status(fiber.StatusInternalServerError).SendString("failed to get result answers")
|
||||||
}
|
}
|
||||||
sortedAnswers, err := s.dal.QuestionRepo.ForSortingResults(ctx.Context(), answers)
|
sortedAnswers, err := r.dal.QuestionRepo.ForSortingResults(ctx.Context(), answers)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ctx.Status(fiber.StatusInternalServerError).SendString("failed sort result answers")
|
return ctx.Status(fiber.StatusInternalServerError).SendString("failed sort result answers")
|
||||||
}
|
}
|
15
internal/controllers/http_controllers/result/route.go
Normal file
15
internal/controllers/http_controllers/result/route.go
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
package result
|
||||||
|
|
||||||
|
import "github.com/gofiber/fiber/v2"
|
||||||
|
|
||||||
|
func (r *Result) Register(router fiber.Router) {
|
||||||
|
router.Post("/results/getResults/:quizId", r.GetResultsByQuizID)
|
||||||
|
router.Delete("/results/delete/:resultId", r.DelResultByID)
|
||||||
|
router.Patch("/result/seen", r.SetStatus)
|
||||||
|
router.Post("/results/:quizID/export", r.ExportResultsToCSV)
|
||||||
|
router.Get("/result/:resultID", r.GetResultAnswers)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Result) Name() string {
|
||||||
|
return ""
|
||||||
|
}
|
15
internal/controllers/http_controllers/statistic/route.go
Normal file
15
internal/controllers/http_controllers/statistic/route.go
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
package statistic
|
||||||
|
|
||||||
|
import "github.com/gofiber/fiber/v2"
|
||||||
|
|
||||||
|
func (r *Statistic) Register(router fiber.Router) {
|
||||||
|
router.Post("/statistic/:quizID/devices", r.GetDeviceStatistics)
|
||||||
|
router.Post("/statistic/:quizID/general", r.GetGeneralStatistics)
|
||||||
|
router.Post("/statistic/:quizID/questions", r.GetQuestionsStatistics)
|
||||||
|
router.Post("/statistic", r.AllServiceStatistics)
|
||||||
|
router.Get("/statistics/:quizID/pipelines", r.GetPipelinesStatistics)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Statistic) Name() string {
|
||||||
|
return ""
|
||||||
|
}
|
@ -1,17 +1,35 @@
|
|||||||
package service
|
package statistic
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/gofiber/fiber/v2"
|
"gitea.pena/SQuiz/common/dal"
|
||||||
"gitea.pena/SQuiz/common/repository/statistics"
|
"gitea.pena/SQuiz/common/repository/statistics"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
"strconv"
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type Deps struct {
|
||||||
|
DAL *dal.DAL
|
||||||
|
ChDAL *dal.ClickHouseDAL
|
||||||
|
}
|
||||||
|
|
||||||
|
type Statistic struct {
|
||||||
|
dal *dal.DAL
|
||||||
|
chDAL *dal.ClickHouseDAL
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewStatisticController(deps Deps) *Statistic {
|
||||||
|
return &Statistic{
|
||||||
|
dal: deps.DAL,
|
||||||
|
chDAL: deps.ChDAL,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type DeviceStatReq struct {
|
type DeviceStatReq struct {
|
||||||
From uint64 // временные границы выбора статистики
|
From uint64 // временные границы выбора статистики
|
||||||
To uint64
|
To uint64
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) GetDeviceStatistics(ctx *fiber.Ctx) error {
|
func (r *Statistic) GetDeviceStatistics(ctx *fiber.Ctx) error {
|
||||||
quizIDStr := ctx.Params("quizID")
|
quizIDStr := ctx.Params("quizID")
|
||||||
|
|
||||||
quizID, err := strconv.ParseInt(quizIDStr, 10, 64)
|
quizID, err := strconv.ParseInt(quizIDStr, 10, 64)
|
||||||
@ -24,7 +42,7 @@ func (s *Service) GetDeviceStatistics(ctx *fiber.Ctx) error {
|
|||||||
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
|
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
|
||||||
}
|
}
|
||||||
|
|
||||||
deviceStats, err := s.dal.StatisticsRepo.GetDeviceStatistics(ctx.Context(), statistics.DeviceStatReq{
|
deviceStats, err := r.dal.StatisticsRepo.GetDeviceStatistics(ctx.Context(), statistics.DeviceStatReq{
|
||||||
QuizId: quizID,
|
QuizId: quizID,
|
||||||
From: req.From,
|
From: req.From,
|
||||||
To: req.To,
|
To: req.To,
|
||||||
@ -40,7 +58,7 @@ type GeneralStatsResp struct {
|
|||||||
Open, Result, AvTime, Conversion map[uint64]uint64
|
Open, Result, AvTime, Conversion map[uint64]uint64
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) GetGeneralStatistics(ctx *fiber.Ctx) error {
|
func (r *Statistic) GetGeneralStatistics(ctx *fiber.Ctx) error {
|
||||||
quizIDStr := ctx.Params("quizID")
|
quizIDStr := ctx.Params("quizID")
|
||||||
quizID, err := strconv.ParseInt(quizIDStr, 10, 64)
|
quizID, err := strconv.ParseInt(quizIDStr, 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -52,7 +70,7 @@ func (s *Service) GetGeneralStatistics(ctx *fiber.Ctx) error {
|
|||||||
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
|
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
|
||||||
}
|
}
|
||||||
|
|
||||||
generalStats, err := s.dal.StatisticsRepo.GetGeneralStatistics(ctx.Context(), statistics.DeviceStatReq{
|
generalStats, err := r.dal.StatisticsRepo.GetGeneralStatistics(ctx.Context(), statistics.DeviceStatReq{
|
||||||
QuizId: quizID,
|
QuizId: quizID,
|
||||||
From: req.From,
|
From: req.From,
|
||||||
To: req.To,
|
To: req.To,
|
||||||
@ -64,7 +82,7 @@ func (s *Service) GetGeneralStatistics(ctx *fiber.Ctx) error {
|
|||||||
return ctx.Status(fiber.StatusOK).JSON(generalStats)
|
return ctx.Status(fiber.StatusOK).JSON(generalStats)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) GetQuestionsStatistics(ctx *fiber.Ctx) error {
|
func (r *Statistic) GetQuestionsStatistics(ctx *fiber.Ctx) error {
|
||||||
quizIDStr := ctx.Params("quizID")
|
quizIDStr := ctx.Params("quizID")
|
||||||
quizID, err := strconv.ParseInt(quizIDStr, 0, 64)
|
quizID, err := strconv.ParseInt(quizIDStr, 0, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -73,10 +91,10 @@ func (s *Service) GetQuestionsStatistics(ctx *fiber.Ctx) error {
|
|||||||
|
|
||||||
var req DeviceStatReq
|
var req DeviceStatReq
|
||||||
if err := ctx.BodyParser(&req); err != nil {
|
if err := ctx.BodyParser(&req); err != nil {
|
||||||
ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
|
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
|
||||||
}
|
}
|
||||||
|
|
||||||
questionsStats, err := s.dal.StatisticsRepo.GetQuestionsStatistics(ctx.Context(), statistics.DeviceStatReq{
|
questionsStats, err := r.dal.StatisticsRepo.GetQuestionsStatistics(ctx.Context(), statistics.DeviceStatReq{
|
||||||
QuizId: quizID,
|
QuizId: quizID,
|
||||||
From: req.From,
|
From: req.From,
|
||||||
To: req.To,
|
To: req.To,
|
||||||
@ -92,13 +110,13 @@ type StatisticReq struct {
|
|||||||
From, To uint64 // временные границы выбора статистики
|
From, To uint64 // временные границы выбора статистики
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) AllServiceStatistics(ctx *fiber.Ctx) error {
|
func (r *Statistic) AllServiceStatistics(ctx *fiber.Ctx) error {
|
||||||
var req StatisticReq
|
var req StatisticReq
|
||||||
if err := ctx.BodyParser(&req); err != nil {
|
if err := ctx.BodyParser(&req); err != nil {
|
||||||
ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
|
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
|
||||||
}
|
}
|
||||||
|
|
||||||
allSvcStats, err := s.dal.StatisticsRepo.AllServiceStatistics(ctx.Context(), req.From, req.To)
|
allSvcStats, err := r.dal.StatisticsRepo.AllServiceStatistics(ctx.Context(), req.From, req.To)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
||||||
}
|
}
|
||||||
@ -106,10 +124,10 @@ func (s *Service) AllServiceStatistics(ctx *fiber.Ctx) error {
|
|||||||
return ctx.Status(fiber.StatusOK).JSON(allSvcStats)
|
return ctx.Status(fiber.StatusOK).JSON(allSvcStats)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) GetPipelinesStatistics(ctx *fiber.Ctx) error {
|
func (r *Statistic) GetPipelinesStatistics(ctx *fiber.Ctx) error {
|
||||||
var req StatisticReq
|
var req StatisticReq
|
||||||
if err := ctx.BodyParser(&req); err != nil {
|
if err := ctx.BodyParser(&req); err != nil {
|
||||||
ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
|
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
|
||||||
}
|
}
|
||||||
|
|
||||||
quizIDStr := ctx.Params("quizID")
|
quizIDStr := ctx.Params("quizID")
|
||||||
@ -118,7 +136,7 @@ func (s *Service) GetPipelinesStatistics(ctx *fiber.Ctx) error {
|
|||||||
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid quiz ID format")
|
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid quiz ID format")
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := s.chDAL.StatisticClickRepo.GetPipelinesStatistics(ctx.Context(), quizID, req.From, req.To)
|
result, err := r.chDAL.StatisticClickRepo.GetPipelinesStatistics(ctx.Context(), quizID, req.From, req.To)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
||||||
}
|
}
|
14
internal/controllers/http_controllers/telegram/route.go
Normal file
14
internal/controllers/http_controllers/telegram/route.go
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
package telegram
|
||||||
|
|
||||||
|
import "github.com/gofiber/fiber/v2"
|
||||||
|
|
||||||
|
func (r *Telegram) Register(router fiber.Router) {
|
||||||
|
router.Get("/pool", r.GetPoolTgAccounts)
|
||||||
|
router.Post("/create", r.AddingTgAccount)
|
||||||
|
router.Delete("/:id", r.DeleteTgAccountByID)
|
||||||
|
router.Post("/setCode", r.SettingTgCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Telegram) Name() string {
|
||||||
|
return "telegram"
|
||||||
|
}
|
194
internal/controllers/http_controllers/telegram/telegram.go
Normal file
194
internal/controllers/http_controllers/telegram/telegram.go
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
package telegram
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"gitea.pena/SQuiz/common/dal"
|
||||||
|
"gitea.pena/SQuiz/common/pj_errors"
|
||||||
|
|
||||||
|
//"fmt"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
// "github.com/rs/xid"
|
||||||
|
//"path/filepath"
|
||||||
|
// "penahub.gitlab.yandexcloud.net/backend/quiz/common.git/model"
|
||||||
|
// "gitea.pena/SQuiz/core/clients/telegram"
|
||||||
|
// "penahub.gitlab.yandexcloud.net/backend/tdlib/client"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Deps struct {
|
||||||
|
DAL *dal.DAL
|
||||||
|
//TelegramClient *telegram.TelegramClient
|
||||||
|
}
|
||||||
|
|
||||||
|
type Telegram struct {
|
||||||
|
dal *dal.DAL
|
||||||
|
//telegramClient *telegram.TelegramClient
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTelegramController(deps Deps) *Telegram {
|
||||||
|
return &Telegram{
|
||||||
|
dal: deps.DAL,
|
||||||
|
//telegramClient: deps.TelegramClient,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Message struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Data string `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Telegram) GetPoolTgAccounts(ctx *fiber.Ctx) error {
|
||||||
|
allAccounts, err := r.dal.TgRepo.GetAllTgAccounts(ctx.Context())
|
||||||
|
if err != nil {
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, pj_errors.ErrNotFound):
|
||||||
|
return ctx.Status(fiber.StatusNotFound).SendString("not found")
|
||||||
|
default:
|
||||||
|
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ctx.Status(fiber.StatusOK).JSON(allAccounts)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Telegram) AddingTgAccount(ctx *fiber.Ctx) error {
|
||||||
|
// var req telegram.AuthTgUserReq
|
||||||
|
// if err := ctx.BodyParser(&req); err != nil {
|
||||||
|
// return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
|
||||||
|
// }
|
||||||
|
// if req.ApiID == 0 || req.ApiHash == "" || req.Password == "" || req.PhoneNumber == "" {
|
||||||
|
// return ctx.Status(fiber.StatusBadRequest).SendString("empty required fields")
|
||||||
|
// }
|
||||||
|
// allAccounts, err := s.dal.TgRepo.GetAllTgAccounts(ctx.Context())
|
||||||
|
// if err != nil && !errors.Is(err, pj_errors.ErrNotFound) {
|
||||||
|
// return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
||||||
|
// }
|
||||||
|
// if !errors.Is(err, pj_errors.ErrNotFound) {
|
||||||
|
// for _, account := range allAccounts {
|
||||||
|
// if account.ApiID == req.ApiID && account.ApiHash == req.ApiHash && account.Status == model.ActiveTg {
|
||||||
|
// return ctx.Status(fiber.StatusConflict).SendString("this account already exist and active")
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// authorizer := client.ClientAuthorizerr()
|
||||||
|
// authorizer.TdlibParameters <- &client.SetTdlibParametersRequest{
|
||||||
|
// UseTestDc: false,
|
||||||
|
// DatabaseDirectory: filepath.Join(".tdlib", "database"),
|
||||||
|
// FilesDirectory: filepath.Join(".tdlib", "files"),
|
||||||
|
// UseFileDatabase: true,
|
||||||
|
// UseChatInfoDatabase: true,
|
||||||
|
// UseMessageDatabase: true,
|
||||||
|
// UseSecretChats: true,
|
||||||
|
// ApiId: req.ApiID,
|
||||||
|
// ApiHash: req.ApiHash,
|
||||||
|
// SystemLanguageCode: "en",
|
||||||
|
// DeviceModel: "Server",
|
||||||
|
// SystemVersion: "1.0.0",
|
||||||
|
// ApplicationVersion: "1.0.0",
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// _, err = client.SetLogVerbosityLevel(&client.SetLogVerbosityLevelRequest{
|
||||||
|
// NewVerbosityLevel: 1,
|
||||||
|
// })
|
||||||
|
// if err != nil {
|
||||||
|
// return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// var tdlibClient *client.Client
|
||||||
|
// // завершается уже в другом контроллере
|
||||||
|
// var goErr error
|
||||||
|
// // todo ужно продумать завершение горутины если код вставлять не пошли
|
||||||
|
// go func() {
|
||||||
|
// tdlibClient, goErr = client.NewClient(authorizer)
|
||||||
|
// if goErr != nil {
|
||||||
|
// fmt.Println("new client failed", err)
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
// s.telegramClient.SaveTgAccount(req.ApiID, req.ApiHash, tdlibClient)
|
||||||
|
// fmt.Println("i am down")
|
||||||
|
// }()
|
||||||
|
// if goErr != nil {
|
||||||
|
// return ctx.Status(fiber.StatusInternalServerError).SendString(goErr.Error())
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// for {
|
||||||
|
// state, ok := <-authorizer.State
|
||||||
|
// if !ok {
|
||||||
|
// return ctx.Status(fiber.StatusOK).SendString("state chan is close auth maybe ok")
|
||||||
|
// }
|
||||||
|
// fmt.Println("currnet state:", state)
|
||||||
|
// switch state.AuthorizationStateType() {
|
||||||
|
// case client.TypeAuthorizationStateWaitPhoneNumber:
|
||||||
|
// authorizer.PhoneNumber <- req.PhoneNumber
|
||||||
|
// case client.TypeAuthorizationStateWaitCode:
|
||||||
|
// signature := xid.New()
|
||||||
|
// s.telegramClient.AddedToMap(telegram.WaitingClient{
|
||||||
|
// PreviousReq: req,
|
||||||
|
// Authorizer: authorizer,
|
||||||
|
// }, signature.String())
|
||||||
|
// return ctx.Status(fiber.StatusOK).JSON(fiber.Map{"signature": signature.String()})
|
||||||
|
//
|
||||||
|
// case client.TypeAuthorizationStateLoggingOut, client.TypeAuthorizationStateClosing, client.TypeAuthorizationStateClosed:
|
||||||
|
// return ctx.Status(fiber.StatusForbidden).SendString(fmt.Sprintf("auth failed, last state is %s", state))
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Telegram) SettingTgCode(ctx *fiber.Ctx) error {
|
||||||
|
var req struct {
|
||||||
|
Code string `json:"code"`
|
||||||
|
Signature string `json:"signature"`
|
||||||
|
}
|
||||||
|
if err := ctx.BodyParser(&req); err != nil {
|
||||||
|
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Code == "" || req.Signature == "" {
|
||||||
|
return ctx.Status(fiber.StatusBadRequest).SendString("empty required fields")
|
||||||
|
}
|
||||||
|
|
||||||
|
// data, ok := s.telegramClient.GetFromMap(req.Signature)
|
||||||
|
// if !ok {
|
||||||
|
// return ctx.Status(fiber.StatusBadRequest).SendString("Invalid id, don't have data")
|
||||||
|
// }
|
||||||
|
// data.Authorizer.Code <- req.Code
|
||||||
|
// for {
|
||||||
|
// state, ok := <-data.Authorizer.State
|
||||||
|
// if !ok {
|
||||||
|
// return ctx.Status(fiber.StatusNoContent).SendString("state chan is close auth maybe ok")
|
||||||
|
// }
|
||||||
|
// fmt.Println("currnet state:", state)
|
||||||
|
// }
|
||||||
|
return nil
|
||||||
|
// switch state.AuthorizationStateType() {
|
||||||
|
// case client.TypeAuthorizationStateReady:
|
||||||
|
// id, err := s.dal.TgRepo.CreateTgAccount(ctx.Context(), model.TgAccount{
|
||||||
|
// ApiID: data.PreviousReq.ApiID,
|
||||||
|
// ApiHash: data.PreviousReq.ApiHash,
|
||||||
|
// PhoneNumber: data.PreviousReq.PhoneNumber,
|
||||||
|
// Status: model.ActiveTg,
|
||||||
|
// Password: data.PreviousReq.Password,
|
||||||
|
// })
|
||||||
|
// if err != nil {
|
||||||
|
// return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
||||||
|
// }
|
||||||
|
// return ctx.Status(fiber.StatusOK).JSON(fiber.Map{"id": id})
|
||||||
|
// case client.TypeAuthorizationStateWaitPassword:
|
||||||
|
// data.Authorizer.Password <- data.PreviousReq.Password
|
||||||
|
// case client.TypeAuthorizationStateLoggingOut, client.TypeAuthorizationStateClosing, client.TypeAuthorizationStateClosed:
|
||||||
|
// return ctx.Status(fiber.StatusForbidden).SendString(fmt.Sprintf("auth failed, last state is %s", state))
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Telegram) DeleteTgAccountByID(ctx *fiber.Ctx) error {
|
||||||
|
id, err := strconv.ParseInt(ctx.Params("id"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return ctx.Status(fiber.StatusBadRequest).SendString("invalid id format")
|
||||||
|
}
|
||||||
|
err = r.dal.TgRepo.SoftDeleteTgAccount(ctx.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
||||||
|
}
|
||||||
|
return ctx.SendStatus(fiber.StatusOK)
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package rpc_service
|
package rpc_controllers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
24
internal/initialize/clients.go
Normal file
24
internal/initialize/clients.go
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
package initialize
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"gitea.pena/SQuiz/common/dal"
|
||||||
|
"gitea.pena/SQuiz/core/internal/clients/auth"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Clients struct {
|
||||||
|
AuthClient *auth.AuthClient
|
||||||
|
//TgClient *telegram.TelegramClient
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewClients(ctx context.Context, cfg Config, pgDAL *dal.DAL) (*Clients, error) {
|
||||||
|
//tgClient, err := telegram.NewTelegramClient(ctx, pgDAL)
|
||||||
|
//if err != nil {
|
||||||
|
// return nil, err
|
||||||
|
//}
|
||||||
|
|
||||||
|
return &Clients{
|
||||||
|
//TgClient: tgClient,
|
||||||
|
AuthClient: auth.NewAuthClient(cfg.AuthMicroserviceURL),
|
||||||
|
}, nil
|
||||||
|
}
|
43
internal/initialize/config.go
Normal file
43
internal/initialize/config.go
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
package initialize
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/caarlos0/env/v8"
|
||||||
|
"github.com/joho/godotenv"
|
||||||
|
"log"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
LoggerProdMode bool `env:"IS_PROD_LOG" envDefault:"false"`
|
||||||
|
IsProd bool `env:"IS_PROD" envDefault:"false"`
|
||||||
|
ClientHttpURL string `env:"CLIENT_HTTP_URL" envDefault:"0.0.0.0:1488"`
|
||||||
|
GrpcURL string `env:"GRPC_URL" envDefault:"localhost:9000"`
|
||||||
|
PostgresURL string `env:"POSTGRES_URL" envDefault:"host=localhost port=35432 user=squiz password=Redalert2 dbname=squiz sslmode=disable"`
|
||||||
|
ClickhouseURL string `env:"CLICKHOUSE_URL" envDefault:"tcp://10.8.0.15:9000/default?sslmode=disable"`
|
||||||
|
HubadminMicroserviceURL string `env:"HUBADMIN_MICROSERVICE_URL" envDefault:"http://localhost:8001/"`
|
||||||
|
AuthMicroserviceURL string `env:"AUTH_MICROSERVICE_URL" envDefault:"http://localhost:8000/"`
|
||||||
|
KafkaBrokers string `env:"KAFKA_BROKERS" envDefault:"localhost:9092"`
|
||||||
|
KafkaGroup string `env:"KAFKA_GROUP" envDefault:"mailnotifier"`
|
||||||
|
KafkaTopicNotifyer string `env:"KAFKA_TOPIC" envDefault:"test-topic"`
|
||||||
|
TrashLogHost string `env:"TRASH_LOG_HOST" envDefault:"localhost:7113"`
|
||||||
|
S3Prefix string `env:"S3_PREFIX"`
|
||||||
|
RedisHost string `env:"REDIS_HOST" envDefault:"localhost:6379"`
|
||||||
|
RedisPassword string `env:"REDIS_PASSWORD" envDefault:"admin"`
|
||||||
|
RedisDB uint64 `env:"REDIS_DB" envDefault:"2"`
|
||||||
|
|
||||||
|
CrtFile string `env:"CRT" envDefault:"server.crt"`
|
||||||
|
KeyFile string `env:"KEY" envDefault:"server.key"`
|
||||||
|
ServiceName string `env:"SERVICE_NAME" envDefault:"squiz"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadConfig() (*Config, error) {
|
||||||
|
if err := godotenv.Load(); err != nil {
|
||||||
|
log.Print("No .env file found")
|
||||||
|
}
|
||||||
|
var config Config
|
||||||
|
if err := env.Parse(&config); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &config, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const ModuleLogger = "core"
|
73
internal/initialize/controllers.go
Normal file
73
internal/initialize/controllers.go
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
package initialize
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/go-redis/redis/v8"
|
||||||
|
"gitea.pena/SQuiz/core/internal/brokers"
|
||||||
|
"gitea.pena/SQuiz/core/internal/controllers/http_controllers/account"
|
||||||
|
"gitea.pena/SQuiz/core/internal/controllers/http_controllers/question"
|
||||||
|
"gitea.pena/SQuiz/core/internal/controllers/http_controllers/quiz"
|
||||||
|
"gitea.pena/SQuiz/core/internal/controllers/http_controllers/result"
|
||||||
|
"gitea.pena/SQuiz/core/internal/controllers/http_controllers/statistic"
|
||||||
|
"gitea.pena/SQuiz/core/internal/controllers/http_controllers/telegram"
|
||||||
|
"gitea.pena/SQuiz/core/internal/controllers/rpc_controllers"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ControllerDeps struct {
|
||||||
|
Clients *Clients
|
||||||
|
DALs *DALs
|
||||||
|
Config Config
|
||||||
|
Producer *brokers.Producer
|
||||||
|
RedisClient *redis.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
type Controller struct {
|
||||||
|
GRpcControllers GRpcControllers
|
||||||
|
HttpControllers HttpControllers
|
||||||
|
}
|
||||||
|
|
||||||
|
type GRpcControllers struct {
|
||||||
|
MailNotify *rpc_controllers.MailNotify
|
||||||
|
}
|
||||||
|
type HttpControllers struct {
|
||||||
|
Account *account.Account
|
||||||
|
Question *question.Question
|
||||||
|
Quiz *quiz.Quiz
|
||||||
|
Result *result.Result
|
||||||
|
Statistic *statistic.Statistic
|
||||||
|
Telegram *telegram.Telegram
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewControllers(deps ControllerDeps) *Controller {
|
||||||
|
return &Controller{
|
||||||
|
GRpcControllers: GRpcControllers{
|
||||||
|
MailNotify: rpc_controllers.NewMailNotify(deps.DALs.PgDAL),
|
||||||
|
},
|
||||||
|
HttpControllers: HttpControllers{
|
||||||
|
Account: account.NewAccountController(account.Deps{
|
||||||
|
Dal: deps.DALs.PgDAL,
|
||||||
|
AuthClient: deps.Clients.AuthClient,
|
||||||
|
Producer: deps.Producer,
|
||||||
|
ServiceName: deps.Config.ServiceName,
|
||||||
|
RedisClient: deps.RedisClient,
|
||||||
|
}),
|
||||||
|
Question: question.NewQuestionController(question.Deps{
|
||||||
|
DAL: deps.DALs.PgDAL,
|
||||||
|
}),
|
||||||
|
Quiz: quiz.NewQuizController(quiz.Deps{
|
||||||
|
DAL: deps.DALs.PgDAL,
|
||||||
|
}),
|
||||||
|
Result: result.NewResultController(result.Deps{
|
||||||
|
DAL: deps.DALs.PgDAL,
|
||||||
|
S3Prefix: deps.Config.S3Prefix,
|
||||||
|
}),
|
||||||
|
Statistic: statistic.NewStatisticController(statistic.Deps{
|
||||||
|
DAL: deps.DALs.PgDAL,
|
||||||
|
ChDAL: deps.DALs.ChDAL,
|
||||||
|
}),
|
||||||
|
Telegram: telegram.NewTelegramController(telegram.Deps{
|
||||||
|
DAL: deps.DALs.PgDAL,
|
||||||
|
//TelegramClient: deps.Clients.TgClient,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
28
internal/initialize/dals.go
Normal file
28
internal/initialize/dals.go
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
package initialize
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"gitea.pena/SQuiz/common/dal"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DALs struct {
|
||||||
|
PgDAL *dal.DAL
|
||||||
|
ChDAL *dal.ClickHouseDAL
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDALs(ctx context.Context, cfg Config) (*DALs, error) {
|
||||||
|
pgDal, err := dal.New(ctx, cfg.PostgresURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
chDal, err := dal.NewClickHouseDAL(ctx, cfg.ClickhouseURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &DALs{
|
||||||
|
PgDAL: pgDal,
|
||||||
|
ChDAL: chDal,
|
||||||
|
}, nil
|
||||||
|
}
|
21
internal/initialize/redis.go
Normal file
21
internal/initialize/redis.go
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
package initialize
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/go-redis/redis/v8"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Redis(ctx context.Context, cfg Config) (*redis.Client, error) {
|
||||||
|
rdb := redis.NewClient(&redis.Options{
|
||||||
|
Addr: cfg.RedisHost,
|
||||||
|
Password: cfg.RedisPassword,
|
||||||
|
DB: int(cfg.RedisDB),
|
||||||
|
})
|
||||||
|
|
||||||
|
status := rdb.Ping(ctx)
|
||||||
|
if err := status.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return rdb, nil
|
||||||
|
}
|
@ -2,15 +2,14 @@ package server
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware"
|
grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware"
|
||||||
grpc_zap "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap"
|
grpc_zap "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap"
|
||||||
grpc_recovery "github.com/grpc-ecosystem/go-grpc-middleware/recovery"
|
grpc_recovery "github.com/grpc-ecosystem/go-grpc-middleware/recovery"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
"net"
|
"net"
|
||||||
"gitea.pena/SQuiz/core/initialize"
|
"gitea.pena/SQuiz/core/internal/initialize"
|
||||||
"gitea.pena/SQuiz/core/proto/notifyer"
|
"gitea.pena/SQuiz/core/internal/proto/notifyer"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -36,14 +35,7 @@ func NewGRPC(logger *zap.Logger) (*GRPC, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type DepsGrpcRun struct {
|
func (g *GRPC) Run(connectionString string) {
|
||||||
Host string
|
|
||||||
Port string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *GRPC) Run(config DepsGrpcRun) {
|
|
||||||
connectionString := fmt.Sprintf("%s:%s", config.Host, config.Port)
|
|
||||||
|
|
||||||
g.logger.Info("Starting GRPC Server", zap.String("host", connectionString))
|
g.logger.Info("Starting GRPC Server", zap.String("host", connectionString))
|
||||||
|
|
||||||
if err := g.listen(connectionString); err != nil && err != grpc.ErrServerStopped {
|
if err := g.listen(connectionString); err != nil && err != grpc.ErrServerStopped {
|
||||||
@ -58,7 +50,7 @@ func (g *GRPC) Stop(_ context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *GRPC) Register(reg *initialize.RpcRegister) *GRPC {
|
func (g *GRPC) Register(reg initialize.GRpcControllers) *GRPC {
|
||||||
notifyer.RegisterQuizServiceServer(g.grpc, reg.MailNotify)
|
notifyer.RegisterQuizServiceServer(g.grpc, reg.MailNotify)
|
||||||
// another
|
// another
|
||||||
return g
|
return g
|
73
internal/server/http/http_server.go
Normal file
73
internal/server/http/http_server.go
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"gitea.pena/PenaSide/common/log_mw"
|
||||||
|
"gitea.pena/PenaSide/hlog"
|
||||||
|
"gitea.pena/SQuiz/common/middleware"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ServerConfig struct {
|
||||||
|
Logger *zap.Logger
|
||||||
|
Controllers []Controller
|
||||||
|
Hlogger hlog.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
type Server struct {
|
||||||
|
Logger *zap.Logger
|
||||||
|
Controllers []Controller
|
||||||
|
app *fiber.App
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewServer(config ServerConfig) *Server {
|
||||||
|
app := fiber.New()
|
||||||
|
app.Use(middleware.JWTAuth())
|
||||||
|
app.Use(log_mw.ContextLogger(config.Hlogger))
|
||||||
|
//app.Get("/liveness", healthchecks.Liveness)
|
||||||
|
//app.Get("/readiness", healthchecks.Readiness(&workerErr)) //todo parametrized readiness. should discuss ready reason
|
||||||
|
s := &Server{
|
||||||
|
Logger: config.Logger,
|
||||||
|
Controllers: config.Controllers,
|
||||||
|
app: app,
|
||||||
|
}
|
||||||
|
|
||||||
|
s.registerRoutes()
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) Start(addr string) error {
|
||||||
|
if err := s.app.Listen(addr); err != nil {
|
||||||
|
s.Logger.Error("Failed to start server", zap.Error(err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) Shutdown(ctx context.Context) error {
|
||||||
|
return s.app.Shutdown()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) registerRoutes() {
|
||||||
|
for _, c := range s.Controllers {
|
||||||
|
router := s.app.Group(c.Name())
|
||||||
|
c.Register(router)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Controller interface {
|
||||||
|
Register(router fiber.Router)
|
||||||
|
Name() string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) ListRoutes() {
|
||||||
|
fmt.Println("Registered routes:")
|
||||||
|
for _, stack := range s.app.Stack() {
|
||||||
|
for _, route := range stack {
|
||||||
|
fmt.Printf("%s %s\n", route.Method, route.Path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
434
internal/tools/tools.go
Normal file
434
internal/tools/tools.go
Normal file
@ -0,0 +1,434 @@
|
|||||||
|
package tools
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"gitea.pena/SQuiz/common/model"
|
||||||
|
"github.com/xuri/excelize/v2"
|
||||||
|
_ "image/gif"
|
||||||
|
_ "image/jpeg"
|
||||||
|
_ "image/png"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
bucketImages = "squizimages"
|
||||||
|
bucketFonts = "squizfonts"
|
||||||
|
bucketScripts = "squizscript"
|
||||||
|
bucketStyle = "squizstyle"
|
||||||
|
bucketAnswers = "squizanswer"
|
||||||
|
)
|
||||||
|
|
||||||
|
func WriteDataToExcel(buffer io.Writer, questions []model.Question, answers []model.Answer, s3Prefix string) error {
|
||||||
|
file := excelize.NewFile()
|
||||||
|
sheet := "Sheet1"
|
||||||
|
|
||||||
|
_, err := file.NewSheet(sheet)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(questions, func(i, j int) bool {
|
||||||
|
return questions[i].Page < questions[j].Page
|
||||||
|
})
|
||||||
|
|
||||||
|
headers, mapQueRes := prepareHeaders(questions)
|
||||||
|
headers = append([]string{"Дата и время"}, headers...)
|
||||||
|
|
||||||
|
for col, header := range headers {
|
||||||
|
cell := ToAlphaString(col+1) + "1"
|
||||||
|
if err := file.SetCellValue(sheet, cell, header); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(answers, func(i, j int) bool {
|
||||||
|
return answers[i].QuestionId < answers[j].QuestionId
|
||||||
|
})
|
||||||
|
standart, results := categorizeAnswers(answers)
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
row := 2
|
||||||
|
for session := range results {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(session string, response []model.Answer, row int) {
|
||||||
|
defer wg.Done()
|
||||||
|
processSession(file, sheet, session, s3Prefix, response, results, questions, mapQueRes, headers, row)
|
||||||
|
}(session, standart[session], row)
|
||||||
|
row++
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
return file.Write(buffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func prepareHeaders(questions []model.Question) ([]string, map[uint64]string) {
|
||||||
|
headers := []string{"Данные респондента"}
|
||||||
|
mapQueRes := make(map[uint64]string)
|
||||||
|
|
||||||
|
for _, q := range questions {
|
||||||
|
if !q.Deleted {
|
||||||
|
if q.Type == model.TypeResult {
|
||||||
|
mapQueRes[q.Id] = q.Title + "\n" + q.Description
|
||||||
|
} else {
|
||||||
|
headers = append(headers, q.Title)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
headers = append(headers, "Результат")
|
||||||
|
return headers, mapQueRes
|
||||||
|
}
|
||||||
|
|
||||||
|
func categorizeAnswers(answers []model.Answer) (map[string][]model.Answer, map[string]model.Answer) {
|
||||||
|
standart := make(map[string][]model.Answer)
|
||||||
|
results := make(map[string]model.Answer)
|
||||||
|
|
||||||
|
for _, answer := range answers {
|
||||||
|
if answer.Result {
|
||||||
|
results[answer.Session] = answer
|
||||||
|
} else {
|
||||||
|
standart[answer.Session] = append(standart[answer.Session], answer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return standart, results
|
||||||
|
}
|
||||||
|
|
||||||
|
func processSession(file *excelize.File, sheet, session, s3Prefix string, response []model.Answer, results map[string]model.Answer, questions []model.Question, mapQueRes map[uint64]string, headers []string, row int) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
fmt.Println("Recovered from panic:", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err := file.SetCellValue(sheet, "A"+strconv.Itoa(row), results[session].CreatedAt.Format("2006-01-02 15:04:05")); err != nil {
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := file.SetCellValue(sheet, "B"+strconv.Itoa(row), results[session].Content); err != nil {
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
count := 3
|
||||||
|
for _, q := range questions {
|
||||||
|
if !q.Deleted && q.Type != model.TypeResult {
|
||||||
|
cell := ToAlphaString(count) + strconv.Itoa(row)
|
||||||
|
index := binarySearch(response, q.Id)
|
||||||
|
if index != -1 {
|
||||||
|
handleAnswer(file, sheet, cell, s3Prefix, response[index], q, count, row)
|
||||||
|
} else {
|
||||||
|
if err := file.SetCellValue(sheet, cell, "-"); err != nil {
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cell := ToAlphaString(len(headers)) + strconv.Itoa(row)
|
||||||
|
if err := file.SetCellValue(sheet, cell, mapQueRes[results[session].QuestionId]); err != nil {
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleAnswer(file *excelize.File, sheet, cell, s3Prefix string, answer model.Answer, question model.Question, count, row int) {
|
||||||
|
tipe := FileSearch(answer.Content)
|
||||||
|
noAccept := make(map[string]struct{})
|
||||||
|
todoMap := make(map[string]string)
|
||||||
|
|
||||||
|
if tipe != "Text" && (question.Type == model.TypeImages || question.Type == model.TypeVarImages) {
|
||||||
|
handleImage(file, sheet, cell, answer.Content, count, row, noAccept, todoMap, question.Title)
|
||||||
|
} else if question.Type == model.TypeFile {
|
||||||
|
handleFile(file, sheet, cell, answer.Content, s3Prefix, noAccept)
|
||||||
|
} else {
|
||||||
|
todoMap[answer.Content] = cell
|
||||||
|
}
|
||||||
|
|
||||||
|
for cnt, cel := range todoMap {
|
||||||
|
if _, ok := noAccept[cnt]; !ok {
|
||||||
|
cntArr := strings.Split(cnt, "`,`")
|
||||||
|
resultCnt := cnt
|
||||||
|
if len(cntArr) > 1 {
|
||||||
|
resultCnt = strings.Join(cntArr, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(resultCnt) > 1 && resultCnt[0] == '`' && resultCnt[len(resultCnt)-1] == '`' {
|
||||||
|
resultCnt = resultCnt[1 : len(resultCnt)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(resultCnt) > 1 && resultCnt[0] == '`' {
|
||||||
|
resultCnt = resultCnt[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(resultCnt) > 1 && resultCnt[len(resultCnt)-1] == '`' {
|
||||||
|
resultCnt = resultCnt[:len(resultCnt)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := file.SetCellValue(sheet, cel, resultCnt); err != nil {
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleImage(file *excelize.File, sheet, cell, content string, count, row int, noAccept map[string]struct{}, todoMap map[string]string, questionTitle string) {
|
||||||
|
multiImgArr := strings.Split(content, "`,`")
|
||||||
|
if len(multiImgArr) > 1 {
|
||||||
|
var descriptions []string
|
||||||
|
mediaSheet := "Media"
|
||||||
|
flag, err := file.GetSheetIndex(mediaSheet)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
}
|
||||||
|
if flag == -1 {
|
||||||
|
_, _ = file.NewSheet(mediaSheet)
|
||||||
|
err = file.SetCellValue(mediaSheet, "A1", "Вопрос")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaRow := row
|
||||||
|
for i, imgContent := range multiImgArr {
|
||||||
|
if i == 0 && len(imgContent) > 1 && imgContent[0] == '`' {
|
||||||
|
imgContent = imgContent[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
if i == len(multiImgArr)-1 && len(imgContent) > 1 && imgContent[len(imgContent)-1] == '`' {
|
||||||
|
imgContent = imgContent[:len(imgContent)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
var res model.ImageContent
|
||||||
|
err := json.Unmarshal([]byte(imgContent), &res)
|
||||||
|
if err != nil {
|
||||||
|
res.Image = imgContent
|
||||||
|
}
|
||||||
|
|
||||||
|
// чек на пустой дескрипшен, есмли пустой то отмечаем как вариант ответа номер по i
|
||||||
|
if res.Description != "" {
|
||||||
|
descriptions = append(descriptions, res.Description)
|
||||||
|
} else {
|
||||||
|
descriptions = append(descriptions, fmt.Sprintf("Вариант ответа №%d", i+1))
|
||||||
|
}
|
||||||
|
|
||||||
|
urle := ExtractImageURL(res.Image)
|
||||||
|
urlData := strings.Split(urle, " ")
|
||||||
|
if len(urlData) == 1 {
|
||||||
|
u, err := url.Parse(urle)
|
||||||
|
if err == nil && u.Scheme != "" && u.Host != "" {
|
||||||
|
picture, err := downloadImage(urle)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
err = file.SetCellValue(mediaSheet, "A"+strconv.Itoa(mediaRow), questionTitle)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
col := ToAlphaString(i + 2)
|
||||||
|
err = file.SetColWidth(mediaSheet, col, col, 50)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
}
|
||||||
|
err = file.SetRowHeight(mediaSheet, mediaRow, 150)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
}
|
||||||
|
if err := file.AddPictureFromBytes(mediaSheet, col+strconv.Itoa(mediaRow), picture); err != nil {
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
}
|
||||||
|
noAccept[content] = struct{}{}
|
||||||
|
} else {
|
||||||
|
todoMap[content] = cell
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
todoMap[imgContent] = cell
|
||||||
|
}
|
||||||
|
|
||||||
|
descriptionsStr := strings.Join(descriptions, "\n")
|
||||||
|
linkText := fmt.Sprintf("%s\n Перейти в приложение %s!A%d", descriptionsStr, mediaSheet, mediaRow)
|
||||||
|
|
||||||
|
if err := file.SetCellValue(sheet, cell, linkText); err != nil {
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
}
|
||||||
|
//if err := file.SetCellHyperLink(sheet, cell, fmt.Sprintf("%s!A%d", mediaSheet, mediaRow), "Location", excelize.HyperlinkOpts{
|
||||||
|
// Display: &linkText,
|
||||||
|
//}); err != nil {
|
||||||
|
// fmt.Println(err.Error())
|
||||||
|
//}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if len(content) > 1 && content[0] == '`' && content[len(content)-1] == '`' {
|
||||||
|
content = content[1 : len(content)-1]
|
||||||
|
}
|
||||||
|
var res model.ImageContent
|
||||||
|
err := json.Unmarshal([]byte(content), &res)
|
||||||
|
if err != nil {
|
||||||
|
res.Image = content
|
||||||
|
}
|
||||||
|
urle := ExtractImageURL(res.Image)
|
||||||
|
urlData := strings.Split(urle, " ")
|
||||||
|
if len(urlData) == 1 {
|
||||||
|
u, err := url.Parse(urle)
|
||||||
|
if err == nil && u.Scheme != "" && u.Host != "" {
|
||||||
|
picture, err := downloadImage(urle)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
}
|
||||||
|
err = file.SetColWidth(sheet, ToAlphaString(count), ToAlphaString(count), 50)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
}
|
||||||
|
err = file.SetRowHeight(sheet, row, 150)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
}
|
||||||
|
if err := file.AddPictureFromBytes(sheet, cell, picture); err != nil {
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
}
|
||||||
|
noAccept[content] = struct{}{}
|
||||||
|
} else {
|
||||||
|
todoMap[content] = cell
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
todoMap[content] = cell
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func handleFile(file *excelize.File, sheet, cell, content, s3Prefix string, noAccept map[string]struct{}) {
|
||||||
|
urle := content
|
||||||
|
if urle != "" && !strings.HasPrefix(urle, "https") {
|
||||||
|
urle = s3Prefix + urle
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("ORRRRR", urle, s3Prefix)
|
||||||
|
display, tooltip := urle, urle
|
||||||
|
|
||||||
|
if err := file.SetCellValue(sheet, cell, urle); err != nil {
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
}
|
||||||
|
if err := file.SetCellHyperLink(sheet, cell, urle, "External", excelize.HyperlinkOpts{
|
||||||
|
Display: &display,
|
||||||
|
Tooltip: &tooltip,
|
||||||
|
}); err != nil {
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
}
|
||||||
|
noAccept[content] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func binarySearch(answers []model.Answer, questionID uint64) int {
|
||||||
|
left := 0
|
||||||
|
right := len(answers) - 1
|
||||||
|
for left <= right {
|
||||||
|
mid := left + (right-left)/2
|
||||||
|
if answers[mid].QuestionId == questionID {
|
||||||
|
return mid
|
||||||
|
} else if answers[mid].QuestionId < questionID {
|
||||||
|
left = mid + 1
|
||||||
|
} else {
|
||||||
|
right = mid - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
func FileSearch(content string) string {
|
||||||
|
if strings.Contains(content, bucketImages) {
|
||||||
|
return FileType(content)
|
||||||
|
} else if strings.Contains(content, bucketFonts) {
|
||||||
|
return FileType(content)
|
||||||
|
} else if strings.Contains(content, bucketScripts) {
|
||||||
|
return FileType(content)
|
||||||
|
} else if strings.Contains(content, bucketStyle) {
|
||||||
|
return FileType(content)
|
||||||
|
} else if strings.Contains(content, bucketAnswers) {
|
||||||
|
return FileType(content)
|
||||||
|
}
|
||||||
|
|
||||||
|
return "Text"
|
||||||
|
}
|
||||||
|
|
||||||
|
func FileType(filename string) string {
|
||||||
|
parts := strings.Split(filename, ".")
|
||||||
|
extension := parts[len(parts)-1]
|
||||||
|
|
||||||
|
switch extension {
|
||||||
|
case "png", "jpg", "jpeg", "gif", "bmp", "svg", "webp", "tiff", "ico":
|
||||||
|
return "Image"
|
||||||
|
default:
|
||||||
|
return "File"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func downloadImage(url string) (*excelize.Picture, error) {
|
||||||
|
resp, err := http.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if derr := resp.Body.Close(); derr != nil {
|
||||||
|
fmt.Printf("error close response body in downloadImage: %v", derr)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
imgData, err := ioutil.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ext := filepath.Ext(url)
|
||||||
|
if ext == "" {
|
||||||
|
contentType := resp.Header.Get("Content-Type")
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(contentType, "image/jpeg"):
|
||||||
|
ext = ".jpg"
|
||||||
|
case strings.HasPrefix(contentType, "image/png"):
|
||||||
|
ext = ".png"
|
||||||
|
default:
|
||||||
|
ext = ".png"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pic := &excelize.Picture{
|
||||||
|
Extension: ext,
|
||||||
|
File: imgData,
|
||||||
|
Format: &excelize.GraphicOptions{
|
||||||
|
AutoFit: true,
|
||||||
|
Positioning: "oneCell",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return pic, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToAlphaString(col int) string {
|
||||||
|
var result string
|
||||||
|
for col > 0 {
|
||||||
|
col--
|
||||||
|
result = string(rune('A'+col%26)) + result
|
||||||
|
col /= 26
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExtractImageURL(htmlContent string) string {
|
||||||
|
re := regexp.MustCompile(`(?:<img[^>]*src="([^"]+)"[^>]*>)|(?:<td[^>]*>.*?<img[^>]*src="([^"]+)"[^>]*>.*?</td>)|(?:<tr[^>]*>.*?<td[^>]*>.*?<img[^>]*src="([^"]+)"[^>]*>.*?</td>.*?</tr>)|(?:<a[^>]*\s+download[^>]*>([^<]+)<\/a>)`)
|
||||||
|
matches := re.FindAllStringSubmatch(htmlContent, -1)
|
||||||
|
|
||||||
|
for _, match := range matches {
|
||||||
|
for i := 1; i < len(match); i++ {
|
||||||
|
if match[i] != "" {
|
||||||
|
return strings.TrimSpace(match[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return htmlContent
|
||||||
|
}
|
107
internal/workers/tg_worker.go
Normal file
107
internal/workers/tg_worker.go
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
package workers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"gitea.pena/SQuiz/common/dal"
|
||||||
|
"github.com/go-redis/redis/v8"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Deps struct {
|
||||||
|
BotID int64
|
||||||
|
Redis *redis.Client
|
||||||
|
Dal *dal.DAL
|
||||||
|
//TgClient *telegram.TelegramClient
|
||||||
|
}
|
||||||
|
|
||||||
|
type TgListenerWorker struct {
|
||||||
|
botID int64
|
||||||
|
redis *redis.Client
|
||||||
|
dal *dal.DAL
|
||||||
|
//tgClient *telegram.TelegramClient
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTgListenerWC(deps Deps) *TgListenerWorker {
|
||||||
|
return &TgListenerWorker{
|
||||||
|
botID: deps.BotID,
|
||||||
|
redis: deps.Redis,
|
||||||
|
dal: deps.Dal,
|
||||||
|
//tgClient: deps.TgClient,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wc *TgListenerWorker) Start(ctx context.Context) {
|
||||||
|
ticker := time.NewTicker(10 * time.Second) //time.Minute
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
wc.processTasks(ctx)
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wc *TgListenerWorker) processTasks(ctx context.Context) {
|
||||||
|
//var cursor uint64
|
||||||
|
//for {
|
||||||
|
// var keys []string
|
||||||
|
// var err error
|
||||||
|
// keys, cursor, err = wc.redis.Scan(ctx, cursor, "telegram_task:*", 0).Result()
|
||||||
|
// if err != nil {
|
||||||
|
// fmt.Println("Failed scan for telegram tasks:", err)
|
||||||
|
// break
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// for _, key := range keys {
|
||||||
|
// func() {
|
||||||
|
// taskBytes, err := wc.redis.GetDel(ctx, key).Result()
|
||||||
|
// if err == redis.Nil {
|
||||||
|
// return
|
||||||
|
// } else if err != nil {
|
||||||
|
// fmt.Println("Failed getdel telegram task:", err)
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
// // todo logging into tg with trashlog
|
||||||
|
// var aimErr error
|
||||||
|
// defer func() {
|
||||||
|
// if r := recover(); r != nil || aimErr != nil {
|
||||||
|
// fmt.Println("recovering from panic or error setting redis value:", r, aimErr)
|
||||||
|
// _ = wc.redis.Set(ctx, key, taskBytes, 0).Err()
|
||||||
|
// }
|
||||||
|
// }()
|
||||||
|
//
|
||||||
|
// var task model.TgRedisTask
|
||||||
|
// if err = json.Unmarshal([]byte(taskBytes), &task); err != nil {
|
||||||
|
// fmt.Println("Failed unmarshal telegram task:", err)
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// var inviteLink string
|
||||||
|
// var chatID int64
|
||||||
|
// inviteLink, chatID, aimErr = wc.tgClient.CreateChannel(task.Name, wc.botID)
|
||||||
|
// if aimErr != nil {
|
||||||
|
// fmt.Println("Failed create tg channel:", aimErr)
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// _, aimErr = wc.dal.AccountRepo.PostLeadTarget(ctx, model.LeadTarget{
|
||||||
|
// AccountID: task.AccountID,
|
||||||
|
// Type: model.LeadTargetTg,
|
||||||
|
// QuizID: task.QuizID,
|
||||||
|
// Target: strconv.Itoa(int(chatID)),
|
||||||
|
// InviteLink: inviteLink,
|
||||||
|
// })
|
||||||
|
// if aimErr != nil {
|
||||||
|
// fmt.Println("Failed create lead target in db:", aimErr)
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
// }()
|
||||||
|
// }
|
||||||
|
// if cursor == 0 {
|
||||||
|
// break
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
}
|
10
main.go
10
main.go
@ -1,10 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/skeris/appInit"
|
|
||||||
"gitea.pena/SQuiz/core/app"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
appInit.Initialize(app.New, app.Options{})
|
|
||||||
}
|
|
37
pkg/closer/closer.go
Normal file
37
pkg/closer/closer.go
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
package closer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Closer interface {
|
||||||
|
Close(ctx context.Context) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type CloserFunc func(ctx context.Context) error
|
||||||
|
|
||||||
|
func (cf CloserFunc) Close(ctx context.Context) error {
|
||||||
|
return cf(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
type CloserGroup struct {
|
||||||
|
closers []Closer
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCloserGroup() *CloserGroup {
|
||||||
|
return &CloserGroup{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cg *CloserGroup) Add(c Closer) {
|
||||||
|
cg.closers = append(cg.closers, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cg *CloserGroup) Call(ctx context.Context) error {
|
||||||
|
var closeErr error
|
||||||
|
for i := len(cg.closers) - 1; i >= 0; i-- {
|
||||||
|
if err := cg.closers[i].Close(ctx); err != nil && closeErr == nil {
|
||||||
|
closeErr = err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return closeErr
|
||||||
|
}
|
@ -1,24 +0,0 @@
|
|||||||
-- Drop indexes
|
|
||||||
DROP INDEX IF EXISTS subquizes;
|
|
||||||
DROP INDEX IF EXISTS birthtime;
|
|
||||||
DROP INDEX IF EXISTS groups;
|
|
||||||
DROP INDEX IF EXISTS timeouted;
|
|
||||||
DROP INDEX IF EXISTS active ON quiz;
|
|
||||||
DROP INDEX IF EXISTS questiontype;
|
|
||||||
DROP INDEX IF EXISTS required;
|
|
||||||
DROP INDEX IF EXISTS relation;
|
|
||||||
DROP INDEX IF EXISTS active ON question;
|
|
||||||
|
|
||||||
-- Drop tables
|
|
||||||
DROP TABLE IF EXISTS privileges;
|
|
||||||
DROP TABLE IF EXISTS answer;
|
|
||||||
DROP TABLE IF EXISTS question;
|
|
||||||
DROP TABLE IF EXISTS quiz;
|
|
||||||
DROP TABLE IF EXISTS account;
|
|
||||||
|
|
||||||
-- Drop types
|
|
||||||
DO $$
|
|
||||||
BEGIN
|
|
||||||
DROP TYPE IF EXISTS question_type;
|
|
||||||
DROP TYPE IF EXISTS quiz_status;
|
|
||||||
END$$;
|
|
@ -1,120 +0,0 @@
|
|||||||
-- Create types
|
|
||||||
DO $$
|
|
||||||
BEGIN
|
|
||||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'question_type') THEN
|
|
||||||
CREATE TYPE question_type AS ENUM (
|
|
||||||
'variant',
|
|
||||||
'images',
|
|
||||||
'varimg',
|
|
||||||
'emoji',
|
|
||||||
'text',
|
|
||||||
'select',
|
|
||||||
'date',
|
|
||||||
'number',
|
|
||||||
'file',
|
|
||||||
'page',
|
|
||||||
'rating'
|
|
||||||
);
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'quiz_status') THEN
|
|
||||||
CREATE TYPE quiz_status AS ENUM (
|
|
||||||
'draft',
|
|
||||||
'template',
|
|
||||||
'stop',
|
|
||||||
'start',
|
|
||||||
'timeout',
|
|
||||||
'offlimit'
|
|
||||||
);
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
|
||||||
END$$;
|
|
||||||
|
|
||||||
-- Create tables
|
|
||||||
CREATE TABLE IF NOT EXISTS account (
|
|
||||||
id UUID PRIMARY KEY,
|
|
||||||
user_id VARCHAR(24),
|
|
||||||
email VARCHAR(50),
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
deleted BOOLEAN DEFAULT false
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS quiz (
|
|
||||||
id bigserial UNIQUE NOT NULL PRIMARY KEY,
|
|
||||||
qid uuid DEFAULT uuid_generate_v4(),
|
|
||||||
accountid varchar(30) NOT NULL,
|
|
||||||
deleted boolean DEFAULT false,
|
|
||||||
archived boolean DEFAULT false,
|
|
||||||
fingerprinting boolean DEFAULT false,
|
|
||||||
repeatable boolean DEFAULT false,
|
|
||||||
note_prevented boolean DEFAULT false,
|
|
||||||
mail_notifications boolean DEFAULT false,
|
|
||||||
unique_answers boolean DEFAULT false,
|
|
||||||
super boolean DEFAULT false,
|
|
||||||
group_id bigint DEFAULT 0,
|
|
||||||
name varchar(280),
|
|
||||||
description text,
|
|
||||||
config text,
|
|
||||||
status quiz_status DEFAULT 'draft',
|
|
||||||
limit_answers integer DEFAULT 0,
|
|
||||||
due_to integer DEFAULT 0,
|
|
||||||
time_of_passing integer DEFAULT 0,
|
|
||||||
pausable boolean DEFAULT false,
|
|
||||||
version smallint DEFAULT 0,
|
|
||||||
version_comment text DEFAULT '',
|
|
||||||
parent_ids integer[],
|
|
||||||
created_at timestamp DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at timestamp DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
questions_count integer DEFAULT 0,
|
|
||||||
answers_count integer DEFAULT 0,
|
|
||||||
average_time_passing integer DEFAULT 0
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS question (
|
|
||||||
id bigserial UNIQUE NOT NULL PRIMARY KEY,
|
|
||||||
quiz_id bigint NOT NULL,
|
|
||||||
title varchar(512) NOT NULL,
|
|
||||||
description text,
|
|
||||||
questiontype question_type DEFAULT 'text',
|
|
||||||
required boolean DEFAULT false,
|
|
||||||
deleted boolean DEFAULT false,
|
|
||||||
page smallint DEFAULT 0,
|
|
||||||
content text,
|
|
||||||
version smallint DEFAULT 0,
|
|
||||||
parent_ids integer[],
|
|
||||||
created_at timestamp DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at timestamp DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
CONSTRAINT quiz_relation FOREIGN KEY(quiz_id) REFERENCES quiz(id)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS answer (
|
|
||||||
id bigserial UNIQUE NOT NULL PRIMARY KEY,
|
|
||||||
content text,
|
|
||||||
quiz_id bigint NOT NULL REFERENCES quiz(id),
|
|
||||||
question_id bigint NOT NULL REFERENCES question(id),
|
|
||||||
fingerprint varchar(1024),
|
|
||||||
session varchar(20),
|
|
||||||
created_at timestamp DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS privileges (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
privilegeID VARCHAR(50),
|
|
||||||
account_id UUID,
|
|
||||||
privilege_name VARCHAR(255),
|
|
||||||
amount INT,
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
FOREIGN KEY (account_id) REFERENCES account (id)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Create indexes
|
|
||||||
CREATE INDEX IF NOT EXISTS active ON question(deleted) WHERE deleted=false;
|
|
||||||
CREATE INDEX IF NOT EXISTS relation ON question(quiz_id DESC);
|
|
||||||
CREATE INDEX IF NOT EXISTS required ON question(required DESC);
|
|
||||||
CREATE INDEX IF NOT EXISTS questiontype ON question(questiontype);
|
|
||||||
CREATE INDEX IF NOT EXISTS active ON quiz(deleted, archived, status) WHERE deleted = false AND archived = false AND status = 'start';
|
|
||||||
CREATE INDEX IF NOT EXISTS timeouted ON quiz(due_to DESC) WHERE deleted = false AND due_to <> 0 AND status <> 'timeout';
|
|
||||||
CREATE INDEX IF NOT EXISTS groups ON quiz(super) WHERE super = true;
|
|
||||||
CREATE INDEX IF NOT EXISTS birthtime ON quiz(created_at DESC);
|
|
||||||
CREATE INDEX IF NOT EXISTS subquizes ON quiz(group_id DESC) WHERE group_id <> 0;
|
|
@ -1 +0,0 @@
|
|||||||
ALTER TABLE answer DROP COLUMN IF EXISTS result;
|
|
@ -1 +0,0 @@
|
|||||||
ALTER TABLE answer ADD COLUMN result BOOLEAN DEFAULT FALSE;
|
|
@ -1 +0,0 @@
|
|||||||
ALTER TABLE quiz DROP COLUMN IF EXISTS sessions_count;
|
|
@ -1 +0,0 @@
|
|||||||
ALTER TABLE quiz ADD COLUMN sessions_count integer;
|
|
@ -1,2 +0,0 @@
|
|||||||
ALTER TABLE quiz DROP COLUMN IF EXISTS new;
|
|
||||||
ALTER TABLE quiz DROP COLUMN IF EXISTS deleted;
|
|
@ -1,2 +0,0 @@
|
|||||||
ALTER TABLE answer ADD COLUMN new BOOLEAN DEFAULT TRUE;
|
|
||||||
ALTER TABLE answer ADD COLUMN deleted BOOLEAN DEFAULT FALSE;
|
|
@ -1,2 +0,0 @@
|
|||||||
ALTER TABLE answer DROP COLUMN IF EXISTS email;
|
|
||||||
DROP INDEX IF EXISTS answer_email_unique_idx;
|
|
@ -1,2 +0,0 @@
|
|||||||
ALTER TABLE answer ADD COLUMN email VARCHAR(50) NOT NULL DEFAULT '';
|
|
||||||
CREATE UNIQUE INDEX IF NOT EXISTS answer_email_unique_idx ON answer (quiz_id, email) WHERE email <> '';
|
|
@ -1,6 +0,0 @@
|
|||||||
ALTER TABLE answer
|
|
||||||
DROP COLUMN device_type,
|
|
||||||
DROP COLUMN device,
|
|
||||||
DROP COLUMN os,
|
|
||||||
DROP COLUMN browser,
|
|
||||||
DROP COLUMN ip;
|
|
@ -1,6 +0,0 @@
|
|||||||
ALTER TABLE answer
|
|
||||||
ADD COLUMN device_type VARCHAR(50) NOT NULL DEFAULT '',
|
|
||||||
ADD COLUMN device VARCHAR(100) NOT NULL DEFAULT '',
|
|
||||||
ADD COLUMN os VARCHAR(100) NOT NULL DEFAULT '',
|
|
||||||
ADD COLUMN browser VARCHAR(100) NOT NULL DEFAULT '',
|
|
||||||
ADD COLUMN ip VARCHAR(50) NOT NULL DEFAULT '';
|
|
@ -1,2 +0,0 @@
|
|||||||
ALTER TABLE answer
|
|
||||||
DROP COLUMN start;
|
|
@ -1,2 +0,0 @@
|
|||||||
ALTER TABLE answer
|
|
||||||
ADD COLUMN start BOOLEAN NOT NULL DEFAULT FALSE;
|
|
@ -1,4 +0,0 @@
|
|||||||
ALTER TABLE answer
|
|
||||||
ALTER COLUMN device TYPE VARCHAR(100),
|
|
||||||
ALTER COLUMN os TYPE VARCHAR(100),
|
|
||||||
ALTER COLUMN browser TYPE VARCHAR(100);
|
|
@ -1,4 +0,0 @@
|
|||||||
ALTER TABLE answer
|
|
||||||
ALTER COLUMN device TYPE VARCHAR(1024),
|
|
||||||
ALTER COLUMN os TYPE VARCHAR(1024),
|
|
||||||
ALTER COLUMN browser TYPE VARCHAR(1024);
|
|
@ -1,2 +0,0 @@
|
|||||||
ALTER TABLE quiz
|
|
||||||
ALTER COLUMN name TYPE VARCHAR(280);
|
|
@ -1,2 +0,0 @@
|
|||||||
ALTER TABLE quiz
|
|
||||||
ALTER COLUMN name TYPE VARCHAR(1024);
|
|
@ -1,238 +0,0 @@
|
|||||||
package service
|
|
||||||
|
|
||||||
import (
|
|
||||||
"database/sql"
|
|
||||||
"errors"
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
"gitea.pena/PenaSide/common/log_mw"
|
|
||||||
"gitea.pena/SQuiz/common/middleware"
|
|
||||||
"gitea.pena/SQuiz/common/model"
|
|
||||||
"gitea.pena/SQuiz/common/pj_errors"
|
|
||||||
"gitea.pena/SQuiz/core/brokers"
|
|
||||||
"gitea.pena/SQuiz/core/models"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type CreateAccountReq struct {
|
|
||||||
UserID string `json:"userId"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type CreateAccountResp struct {
|
|
||||||
CreatedAccount model.Account `json:"created_account"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type DeleteAccountResp struct {
|
|
||||||
DeletedAccountID string `json:"account_Id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type GetPrivilegeByUserIDReq struct {
|
|
||||||
UserID string `json:"userId"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type DeleteAccountByUserIDReq struct {
|
|
||||||
UserID string `json:"userId"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type DeleteAccountByUserIDResp struct {
|
|
||||||
DeletedAccountUserID string `json:"userId"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type GetAccountsReq struct {
|
|
||||||
Limit uint64 `json:"limit"`
|
|
||||||
Page uint64 `json:"page"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type GetAccountsResp struct {
|
|
||||||
Count uint64 `json:"count"`
|
|
||||||
Items []model.Account `json:"items"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// getCurrentAccount обработчик для получения текущего аккаунта
|
|
||||||
func (s *Service) getCurrentAccount(ctx *fiber.Ctx) error {
|
|
||||||
accountID, ok := middleware.GetAccountId(ctx)
|
|
||||||
if !ok {
|
|
||||||
return ctx.Status(fiber.StatusUnauthorized).SendString("account id is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
account, err := s.dal.AccountRepo.GetAccountByID(ctx.Context(), accountID)
|
|
||||||
if err != nil && err != sql.ErrNoRows {
|
|
||||||
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
//TODO: fix this later
|
|
||||||
if account.ID == "" {
|
|
||||||
return ctx.Status(fiber.StatusNotFound).SendString("no account")
|
|
||||||
}
|
|
||||||
|
|
||||||
return ctx.Status(fiber.StatusOK).JSON(account)
|
|
||||||
}
|
|
||||||
|
|
||||||
// createAccount обработчик для создания нового аккаунта
|
|
||||||
func (s *Service) createAccount(ctx *fiber.Ctx) error {
|
|
||||||
accountID, ok := middleware.GetAccountId(ctx)
|
|
||||||
if !ok {
|
|
||||||
return ctx.Status(fiber.StatusUnauthorized).SendString("account id is required")
|
|
||||||
}
|
|
||||||
hlogger := log_mw.ExtractLogger(ctx)
|
|
||||||
|
|
||||||
existingAccount, err := s.dal.AccountRepo.GetAccountByID(ctx.Context(), accountID)
|
|
||||||
if err != nil && err != sql.ErrNoRows {
|
|
||||||
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
|
||||||
}
|
|
||||||
if existingAccount.ID != "" {
|
|
||||||
return ctx.Status(fiber.StatusConflict).SendString("user with this ID already exists")
|
|
||||||
}
|
|
||||||
|
|
||||||
email, err := s.authClient.GetUserEmail(accountID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
newAccount := model.Account{
|
|
||||||
UserID: accountID,
|
|
||||||
CreatedAt: time.Now(),
|
|
||||||
Email: email,
|
|
||||||
Deleted: false,
|
|
||||||
Privileges: map[string]model.ShortPrivilege{
|
|
||||||
"quizUnlimTime": {
|
|
||||||
PrivilegeID: "quizUnlimTime",
|
|
||||||
PrivilegeName: "Безлимит Опросов",
|
|
||||||
Amount: 14,
|
|
||||||
CreatedAt: time.Now(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
createdAcc, err := s.dal.AccountRepo.CreateAccount(ctx.Context(), &newAccount)
|
|
||||||
if err != nil {
|
|
||||||
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
hlogger.Emit(models.InfoAccountCreated{
|
|
||||||
CtxUserID: accountID,
|
|
||||||
CtxAccountID: createdAcc.ID,
|
|
||||||
})
|
|
||||||
|
|
||||||
err = s.producer.ToMailNotify(ctx.Context(), brokers.Message{
|
|
||||||
AccountID: accountID,
|
|
||||||
Email: email,
|
|
||||||
ServiceKey: s.serviceName,
|
|
||||||
SendAt: time.Now(),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
return ctx.JSON(CreateAccountResp{
|
|
||||||
CreatedAccount: newAccount,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// deleteAccount обработчик для удаления текущего аккаунта
|
|
||||||
func (s *Service) deleteAccount(ctx *fiber.Ctx) error {
|
|
||||||
accountID, ok := middleware.GetAccountId(ctx)
|
|
||||||
if !ok {
|
|
||||||
return ctx.Status(fiber.StatusUnauthorized).SendString("account id is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
account, err := s.dal.AccountRepo.GetAccountByID(ctx.Context(), accountID)
|
|
||||||
if err != nil {
|
|
||||||
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.dal.AccountRepo.DeleteAccount(ctx.Context(), account.ID); err != nil {
|
|
||||||
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
return ctx.JSON(DeleteAccountResp{
|
|
||||||
DeletedAccountID: accountID,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// getPrivilegeByUserID обработчик для получения привилегий аккаунта по ID пользователя
|
|
||||||
func (s *Service) getPrivilegeByUserID(ctx *fiber.Ctx) error {
|
|
||||||
var req GetPrivilegeByUserIDReq
|
|
||||||
if err := ctx.BodyParser(&req); err != nil {
|
|
||||||
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
|
|
||||||
}
|
|
||||||
|
|
||||||
privilege, err := s.dal.AccountRepo.GetPrivilegesByAccountID(ctx.Context(), req.UserID)
|
|
||||||
if err != nil {
|
|
||||||
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
return ctx.Status(fiber.StatusOK).JSON(privilege)
|
|
||||||
}
|
|
||||||
|
|
||||||
// deleteAccountByUserID обработчик для удаления аккаунта по ID пользователя
|
|
||||||
func (s *Service) deleteAccountByUserID(ctx *fiber.Ctx) error {
|
|
||||||
var req DeleteAccountByUserIDReq
|
|
||||||
if err := ctx.BodyParser(&req); err != nil {
|
|
||||||
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
|
|
||||||
}
|
|
||||||
|
|
||||||
existingAccount, err := s.dal.AccountRepo.GetAccountByID(ctx.Context(), req.UserID)
|
|
||||||
if err != nil {
|
|
||||||
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
if existingAccount.ID == "" {
|
|
||||||
return ctx.Status(fiber.StatusInternalServerError).SendString("user with this ID not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.dal.AccountRepo.DeleteAccount(ctx.Context(), existingAccount.ID); err != nil {
|
|
||||||
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
return ctx.JSON(DeleteAccountByUserIDResp{
|
|
||||||
DeletedAccountUserID: req.UserID,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// getAccounts обработчик для получения списка аккаунтов с пагинацией
|
|
||||||
func (s *Service) getAccounts(ctx *fiber.Ctx) error {
|
|
||||||
var req GetAccountsReq
|
|
||||||
if err := ctx.BodyParser(&req); err != nil {
|
|
||||||
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
|
|
||||||
}
|
|
||||||
|
|
||||||
_, ok := middleware.GetAccountId(ctx)
|
|
||||||
if !ok {
|
|
||||||
return ctx.Status(fiber.StatusUnauthorized).SendString("account id is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
accounts, totalCount, err := s.dal.AccountRepo.GetAccounts(ctx.Context(), req.Limit, req.Page)
|
|
||||||
if err != nil {
|
|
||||||
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
response := GetAccountsResp{
|
|
||||||
Count: totalCount,
|
|
||||||
Items: accounts,
|
|
||||||
}
|
|
||||||
|
|
||||||
return ctx.Status(fiber.StatusOK).JSON(response)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) ManualDone(ctx *fiber.Ctx) error {
|
|
||||||
var req struct {
|
|
||||||
Id string `json:"id"`
|
|
||||||
}
|
|
||||||
if err := ctx.BodyParser(&req); err != nil {
|
|
||||||
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
|
|
||||||
}
|
|
||||||
|
|
||||||
if req.Id == "" {
|
|
||||||
return ctx.Status(fiber.StatusBadRequest).SendString("User id is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
err := s.dal.AccountRepo.ManualDone(ctx.Context(), req.Id)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, pj_errors.ErrNotFound) {
|
|
||||||
return ctx.Status(fiber.StatusNotFound).SendString("user don't have this privilege")
|
|
||||||
}
|
|
||||||
return ctx.Status(fiber.StatusInternalServerError).SendString("Internal Server Error")
|
|
||||||
}
|
|
||||||
|
|
||||||
return ctx.SendStatus(fiber.StatusOK)
|
|
||||||
}
|
|
@ -1,89 +0,0 @@
|
|||||||
package service
|
|
||||||
|
|
||||||
import (
|
|
||||||
"gitea.pena/SQuiz/common/dal"
|
|
||||||
"gitea.pena/SQuiz/core/brokers"
|
|
||||||
"gitea.pena/SQuiz/core/clients/auth"
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Service is an entity for http requests handling
|
|
||||||
type Service struct {
|
|
||||||
dal *dal.DAL
|
|
||||||
authClient *auth.AuthClient
|
|
||||||
producer *brokers.Producer
|
|
||||||
serviceName string
|
|
||||||
chDAL *dal.ClickHouseDAL
|
|
||||||
s3Prefix string
|
|
||||||
producerGigaChat *brokers.Producer
|
|
||||||
}
|
|
||||||
|
|
||||||
type Deps struct {
|
|
||||||
Dal *dal.DAL
|
|
||||||
AuthClient *auth.AuthClient
|
|
||||||
Producer *brokers.Producer
|
|
||||||
ServiceName string
|
|
||||||
ChDAL *dal.ClickHouseDAL
|
|
||||||
S3Prefix string
|
|
||||||
ProducerGigaChat *brokers.Producer
|
|
||||||
}
|
|
||||||
|
|
||||||
func New(deps Deps) *Service {
|
|
||||||
return &Service{
|
|
||||||
dal: deps.Dal,
|
|
||||||
authClient: deps.AuthClient,
|
|
||||||
producer: deps.Producer,
|
|
||||||
serviceName: deps.ServiceName,
|
|
||||||
chDAL: deps.ChDAL,
|
|
||||||
s3Prefix: deps.S3Prefix,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Register is a function for add handlers of service to external multiplexer
|
|
||||||
func (s *Service) Register(app *fiber.App) {
|
|
||||||
// quiz manipulating handlers
|
|
||||||
app.Post("/quiz/create", s.CreateQuiz)
|
|
||||||
app.Post("/quiz/getList", s.GetQuizList)
|
|
||||||
app.Patch("/quiz/edit", s.UpdateQuiz)
|
|
||||||
app.Post("/quiz/copy", s.CopyQuiz)
|
|
||||||
app.Post("/quiz/history", s.GetQuizHistory)
|
|
||||||
app.Delete("/quiz/delete", s.DeleteQuiz)
|
|
||||||
app.Patch("/quiz/archive", s.ArchiveQuiz)
|
|
||||||
app.Post("/quiz/move", s.QuizMove)
|
|
||||||
app.Post("/quiz/template", s.TemplateCopy)
|
|
||||||
|
|
||||||
app.Post("/quiz/:quizID/auditory", s.CreateQuizAuditory)
|
|
||||||
app.Get("/quiz/:quizID/auditory", s.GetQuizAuditory)
|
|
||||||
app.Delete("/quiz/:quizID/auditory", s.DeleteQuizAuditory)
|
|
||||||
|
|
||||||
// question manipulating handlers
|
|
||||||
app.Post("/question/create", s.CreateQuestion)
|
|
||||||
app.Post("/question/getList", s.GetQuestionList)
|
|
||||||
app.Patch("/question/edit", s.UpdateQuestion)
|
|
||||||
app.Post("/question/copy", s.CopyQuestion)
|
|
||||||
app.Post("/question/history", s.GetQuestionHistory)
|
|
||||||
app.Delete("/question/delete", s.DeleteQuestion)
|
|
||||||
|
|
||||||
// account handlers
|
|
||||||
app.Get("/account/get", s.getCurrentAccount)
|
|
||||||
app.Post("/account/create", s.createAccount)
|
|
||||||
app.Delete("/account/delete", s.deleteAccount)
|
|
||||||
app.Get("/accounts", s.getAccounts)
|
|
||||||
app.Get("/privilege/:userId", s.getPrivilegeByUserID)
|
|
||||||
app.Delete("/account/:userId", s.deleteAccountByUserID)
|
|
||||||
app.Post("/account/manualdone", s.ManualDone)
|
|
||||||
|
|
||||||
// result handlers
|
|
||||||
app.Post("/results/getResults/:quizId", s.GetResultsByQuizID)
|
|
||||||
app.Delete("/results/delete/:resultId", s.DelResultByID)
|
|
||||||
app.Patch("/result/seen", s.SetStatus)
|
|
||||||
app.Post("/results/:quizID/export", s.ExportResultsToCSV)
|
|
||||||
app.Get("/result/:resultID", s.GetResultAnswers)
|
|
||||||
|
|
||||||
// statistics handlers
|
|
||||||
app.Post("/statistic/:quizID/devices", s.GetDeviceStatistics)
|
|
||||||
app.Post("/statistic/:quizID/general", s.GetGeneralStatistics)
|
|
||||||
app.Post("/statistic/:quizID/questions", s.GetQuestionsStatistics)
|
|
||||||
app.Post("/statistic", s.AllServiceStatistics)
|
|
||||||
app.Get("/statistics/:quizID/pipelines", s.GetPipelinesStatistics)
|
|
||||||
}
|
|
@ -2,11 +2,11 @@ package tests
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"gitea.pena/SQuiz/core/internal/brokers"
|
||||||
|
"gitea.pena/SQuiz/core/internal/initialize"
|
||||||
"github.com/pioz/faker"
|
"github.com/pioz/faker"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"log"
|
"log"
|
||||||
"gitea.pena/SQuiz/core/brokers"
|
|
||||||
"gitea.pena/SQuiz/core/initialize"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
package tests
|
package tests
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"gitea.pena/PenaSide/common/privilege"
|
"gitea.pena/PenaSide/common/privilege"
|
||||||
"gitea.pena/SQuiz/common/model"
|
"gitea.pena/SQuiz/common/model"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -2,12 +2,11 @@ package tests
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
_ "embed"
|
_ "embed"
|
||||||
|
"gitea.pena/SQuiz/common/clients"
|
||||||
|
"gitea.pena/SQuiz/common/model"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"github.com/pioz/faker"
|
"github.com/pioz/faker"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"gitea.pena/SQuiz/common/model"
|
|
||||||
"gitea.pena/SQuiz/worker/answerwc"
|
|
||||||
"gitea.pena/SQuiz/worker/clients/mailclient"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@ -19,16 +18,16 @@ var toClientTemplate string
|
|||||||
var reminderTemplate string
|
var reminderTemplate string
|
||||||
|
|
||||||
func TestProcessMessageToSMTP(t *testing.T) {
|
func TestProcessMessageToSMTP(t *testing.T) {
|
||||||
clientDeps := mailclient.ClientDeps{
|
clientDeps := clients.Deps{
|
||||||
Host: "connect.mailclient.bz",
|
SmtpHost: "connect.mailclient.bz",
|
||||||
Port: "587",
|
SmtpPort: "587",
|
||||||
Sender: "skeris@mailing.pena.digital",
|
SmtpSender: "skeris@mailing.pena.digital",
|
||||||
Auth: &mailclient.PlainAuth{Username: "kotilion.95@gmail.com", Password: "vWwbCSg4bf0p"},
|
|
||||||
ApiKey: "P0YsjUB137upXrr1NiJefHmXVKW1hmBWlpev",
|
ApiKey: "P0YsjUB137upXrr1NiJefHmXVKW1hmBWlpev",
|
||||||
FiberClient: &fiber.Client{},
|
FiberClient: &fiber.Client{},
|
||||||
|
SmtpApiUrl: "",
|
||||||
}
|
}
|
||||||
|
|
||||||
client := mailclient.NewClient(clientDeps)
|
client := clients.NewSmtpClient(clientDeps)
|
||||||
|
|
||||||
recipient := "mullinp@internet.ru"
|
recipient := "mullinp@internet.ru"
|
||||||
subject := "Test"
|
subject := "Test"
|
||||||
|
@ -111,7 +111,11 @@ func registerUser(login string) *jwt.Token {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer func() {
|
||||||
|
if derr := resp.Body.Close(); derr != nil {
|
||||||
|
fmt.Printf("error close response body in registerUser: %v", derr)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
bytes, err := io.ReadAll(resp.Body)
|
bytes, err := io.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
BIN
tools/migrate
Executable file
BIN
tools/migrate
Executable file
Binary file not shown.
449
tools/tools.go
449
tools/tools.go
@ -1,449 +0,0 @@
|
|||||||
package tools
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"github.com/xuri/excelize/v2"
|
|
||||||
_ "image/gif"
|
|
||||||
_ "image/jpeg"
|
|
||||||
_ "image/png"
|
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"path/filepath"
|
|
||||||
"gitea.pena/SQuiz/common/model"
|
|
||||||
"regexp"
|
|
||||||
"sort"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
bucketImages = "squizimages"
|
|
||||||
bucketFonts = "squizfonts"
|
|
||||||
bucketScripts = "squizscript"
|
|
||||||
bucketStyle = "squizstyle"
|
|
||||||
bucketAnswers = "squizanswer"
|
|
||||||
)
|
|
||||||
|
|
||||||
func WriteDataToExcel(buffer io.Writer, questions []model.Question, answers []model.Answer, s3Prefix string) error {
|
|
||||||
file := excelize.NewFile()
|
|
||||||
sheet := "Sheet1"
|
|
||||||
|
|
||||||
_, err := file.NewSheet(sheet)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Slice(questions, func(i, j int) bool {
|
|
||||||
return questions[i].Page < questions[j].Page
|
|
||||||
})
|
|
||||||
|
|
||||||
headers := []string{"Данные респондента"}
|
|
||||||
headers = append([]string{"Дата и время"}, headers...)
|
|
||||||
mapQueRes := make(map[uint64]string)
|
|
||||||
|
|
||||||
for _, q := range questions {
|
|
||||||
if !q.Deleted {
|
|
||||||
if q.Type == model.TypeResult {
|
|
||||||
mapQueRes[q.Id] = q.Title + "\n" + q.Description
|
|
||||||
} else {
|
|
||||||
headers = append(headers, q.Title)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
headers = append(headers, "Результат")
|
|
||||||
|
|
||||||
for col, header := range headers {
|
|
||||||
cell := ToAlphaString(col+1) + "1"
|
|
||||||
if err := file.SetCellValue(sheet, cell, header); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Slice(answers, func(i, j int) bool {
|
|
||||||
return answers[i].QuestionId < answers[j].QuestionId
|
|
||||||
})
|
|
||||||
|
|
||||||
// мапа для хранения обычных ответов респондентов
|
|
||||||
standart := make(map[string][]model.Answer)
|
|
||||||
|
|
||||||
// мапа для хранения данных респондентов
|
|
||||||
results := make(map[string]model.Answer)
|
|
||||||
|
|
||||||
// заполняем мапу ответами и данными респондентов
|
|
||||||
for _, answer := range answers {
|
|
||||||
if answer.Result {
|
|
||||||
results[answer.Session] = answer
|
|
||||||
} else {
|
|
||||||
standart[answer.Session] = append(standart[answer.Session], answer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
processSession := func(session string, response []model.Answer, row int) {
|
|
||||||
defer func() {
|
|
||||||
if r := recover(); r != nil {
|
|
||||||
fmt.Println("Recovered from panic:", r)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
if err := file.SetCellValue(sheet, "A"+strconv.Itoa(row), results[session].CreatedAt.Format("2006-01-02 15:04:05")); err != nil {
|
|
||||||
fmt.Println(err.Error())
|
|
||||||
}
|
|
||||||
if err := file.SetCellValue(sheet, "B"+strconv.Itoa(row), results[session].Content); err != nil {
|
|
||||||
fmt.Println(err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
count := 3
|
|
||||||
|
|
||||||
for _, q := range questions {
|
|
||||||
if !q.Deleted && q.Type != model.TypeResult {
|
|
||||||
index := binarySearch(response, q.Id)
|
|
||||||
if index != -1 {
|
|
||||||
cell := ToAlphaString(count) + strconv.Itoa(row)
|
|
||||||
tipe := FileSearch(response[index].Content)
|
|
||||||
noAccept := make(map[string]struct{})
|
|
||||||
todoMap := make(map[string]string)
|
|
||||||
if tipe != "Text" && q.Type == model.TypeImages || q.Type == model.TypeVarImages {
|
|
||||||
var res model.ImageContent
|
|
||||||
err := json.Unmarshal([]byte(response[index].Content), &res)
|
|
||||||
if err != nil {
|
|
||||||
res.Image = response[index].Content
|
|
||||||
}
|
|
||||||
urle := ExtractImageURL(res.Image)
|
|
||||||
urlData := strings.Split(urle, " ")
|
|
||||||
if len(urlData) == 1 {
|
|
||||||
u, err := url.Parse(urle)
|
|
||||||
if err == nil && u.Scheme != "" && u.Host != "" {
|
|
||||||
picture, err := downloadImage(urle)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err.Error())
|
|
||||||
}
|
|
||||||
file.SetColWidth(sheet, ToAlphaString(count), ToAlphaString(count), 50)
|
|
||||||
file.SetRowHeight(sheet, row, 150)
|
|
||||||
if err := file.AddPictureFromBytes(sheet, cell, picture); err != nil {
|
|
||||||
fmt.Println(err.Error())
|
|
||||||
}
|
|
||||||
noAccept[response[index].Content] = struct{}{}
|
|
||||||
} else {
|
|
||||||
todoMap[response[index].Content] = cell
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
todoMap[response[index].Content] = cell
|
|
||||||
}
|
|
||||||
} else if q.Type == model.TypeFile {
|
|
||||||
urle := response[index].Content
|
|
||||||
if urle != "" && !strings.HasPrefix(urle, "https") {
|
|
||||||
urle = strings.ReplaceAll(s3Prefix,bucketImages, bucketAnswers) + fmt.Sprint(q.Id)+ "/" + urle
|
|
||||||
}
|
|
||||||
fmt.Println("ORRRRR", urle, s3Prefix)
|
|
||||||
display, tooltip := urle, urle
|
|
||||||
if err := file.SetCellValue(sheet, cell, urle); err != nil {
|
|
||||||
fmt.Println(err.Error())
|
|
||||||
}
|
|
||||||
if err := file.SetCellHyperLink(sheet, cell, urle, "External", excelize.HyperlinkOpts{
|
|
||||||
Display: &display,
|
|
||||||
Tooltip: &tooltip,
|
|
||||||
}); err != nil {
|
|
||||||
fmt.Println(err.Error())
|
|
||||||
}
|
|
||||||
noAccept[response[index].Content] = struct{}{}
|
|
||||||
} else {
|
|
||||||
todoMap[response[index].Content] = cell
|
|
||||||
}
|
|
||||||
for cnt, cel := range todoMap {
|
|
||||||
if _, ok := noAccept[cnt]; !ok {
|
|
||||||
if err := file.SetCellValue(sheet, cel, cnt); err != nil {
|
|
||||||
fmt.Println(err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
cell := ToAlphaString(count) + strconv.Itoa(row)
|
|
||||||
if err := file.SetCellValue(sheet, cell, "-"); err != nil {
|
|
||||||
fmt.Println(err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
count++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
index := binarySearch(response, results[session].QuestionId)
|
|
||||||
content := response[index].Content
|
|
||||||
score , err := strconv.ParseInt(content, 10, 64)
|
|
||||||
cell := ToAlphaString(len(headers)) + strconv.Itoa(row)
|
|
||||||
if err != nil {
|
|
||||||
if err := file.SetCellValue(sheet, cell, mapQueRes[results[session].QuestionId]); err != nil {
|
|
||||||
fmt.Println(err.Error())
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if err := file.SetCellValue(sheet, cell, score); err != nil {
|
|
||||||
fmt.Println(err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
row := 2
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
for session, _ := range results {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(session string, response []model.Answer, row int) {
|
|
||||||
defer wg.Done()
|
|
||||||
processSession(session, standart[session], row)
|
|
||||||
}(session, standart[session], row)
|
|
||||||
row++
|
|
||||||
}
|
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
if err := file.Write(buffer); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func binarySearch(answers []model.Answer, questionID uint64) int {
|
|
||||||
left := 0
|
|
||||||
right := len(answers) - 1
|
|
||||||
for left <= right {
|
|
||||||
mid := left + (right-left)/2
|
|
||||||
if answers[mid].QuestionId == questionID {
|
|
||||||
return mid
|
|
||||||
} else if answers[mid].QuestionId < questionID {
|
|
||||||
left = mid + 1
|
|
||||||
} else {
|
|
||||||
right = mid - 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
|
|
||||||
func FileSearch(content string) string {
|
|
||||||
if strings.Contains(content, bucketImages) {
|
|
||||||
return FileType(content)
|
|
||||||
} else if strings.Contains(content, bucketFonts) {
|
|
||||||
return FileType(content)
|
|
||||||
} else if strings.Contains(content, bucketScripts) {
|
|
||||||
return FileType(content)
|
|
||||||
} else if strings.Contains(content, bucketStyle) {
|
|
||||||
return FileType(content)
|
|
||||||
} else if strings.Contains(content, bucketAnswers) {
|
|
||||||
return FileType(content)
|
|
||||||
}
|
|
||||||
|
|
||||||
return "Text"
|
|
||||||
}
|
|
||||||
|
|
||||||
func FileType(filename string) string {
|
|
||||||
parts := strings.Split(filename, ".")
|
|
||||||
extension := parts[len(parts)-1]
|
|
||||||
|
|
||||||
switch extension {
|
|
||||||
case "png", "jpg", "jpeg", "gif", "bmp", "svg", "webp", "tiff", "ico":
|
|
||||||
return "Image"
|
|
||||||
default:
|
|
||||||
return "File"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func downloadImage(url string) (*excelize.Picture, error) {
|
|
||||||
resp, err := http.Get(url)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
imgData, err := ioutil.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
ext := filepath.Ext(url)
|
|
||||||
if ext == "" {
|
|
||||||
contentType := resp.Header.Get("Content-Type")
|
|
||||||
switch {
|
|
||||||
case strings.HasPrefix(contentType, "image/jpeg"):
|
|
||||||
ext = ".jpg"
|
|
||||||
case strings.HasPrefix(contentType, "image/png"):
|
|
||||||
ext = ".png"
|
|
||||||
default:
|
|
||||||
ext = ".png"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pic := &excelize.Picture{
|
|
||||||
Extension: ext,
|
|
||||||
File: imgData,
|
|
||||||
Format: &excelize.GraphicOptions{
|
|
||||||
AutoFit: true,
|
|
||||||
Positioning: "oneCell",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return pic, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToAlphaString(col int) string {
|
|
||||||
var result string
|
|
||||||
for col > 0 {
|
|
||||||
col--
|
|
||||||
result = string(rune('A'+col%26)) + result
|
|
||||||
col /= 26
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func ExtractImageURL(htmlContent string) string {
|
|
||||||
re := regexp.MustCompile(`(?:<img[^>]*src="([^"]+)"[^>]*>)|(?:<td[^>]*>.*?<img[^>]*src="([^"]+)"[^>]*>.*?</td>)|(?:<tr[^>]*>.*?<td[^>]*>.*?<img[^>]*src="([^"]+)"[^>]*>.*?</td>.*?</tr>)|(?:<a[^>]*\s+download[^>]*>([^<]+)<\/a>)`)
|
|
||||||
matches := re.FindAllStringSubmatch(htmlContent, -1)
|
|
||||||
|
|
||||||
for _, match := range matches {
|
|
||||||
for i := 1; i < len(match); i++ {
|
|
||||||
if match[i] != "" {
|
|
||||||
return strings.TrimSpace(match[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return htmlContent
|
|
||||||
}
|
|
||||||
|
|
||||||
//func WriteDataToExcel(buffer io.Writer, questions []model.Question, answers []model.Answer) error {
|
|
||||||
// file := excelize.NewFile()
|
|
||||||
// sheet := "Sheet1"
|
|
||||||
//
|
|
||||||
// _, err := file.NewSheet(sheet)
|
|
||||||
// if err != nil {
|
|
||||||
// return err
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// sort.Slice(questions, func(i, j int) bool {
|
|
||||||
// return questions[i].Page > questions[j].Page
|
|
||||||
// })
|
|
||||||
//
|
|
||||||
// headers := []string{"Данные респондента"}
|
|
||||||
// mapQueRes := make(map[uint64]string)
|
|
||||||
//
|
|
||||||
// for _, q := range questions {
|
|
||||||
// if !q.Deleted {
|
|
||||||
// if q.Type == model.TypeResult {
|
|
||||||
// mapQueRes[q.Id] = q.Title + "\n" + q.Description
|
|
||||||
// } else {
|
|
||||||
// headers = append(headers, q.Title)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// headers = append(headers, "Результат")
|
|
||||||
//
|
|
||||||
// // добавляем заголовки в первую строку
|
|
||||||
// for col, header := range headers {
|
|
||||||
// cell := ToAlphaString(col+1) + "1"
|
|
||||||
// if err := file.SetCellValue(sheet, cell, header); err != nil {
|
|
||||||
// return err
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // мапа для хранения обычных ответов респондентов
|
|
||||||
// standart := make(map[string][]model.Answer)
|
|
||||||
//
|
|
||||||
// // мапа для хранения данных респондентов
|
|
||||||
// results := make(map[string]model.Answer)
|
|
||||||
//
|
|
||||||
// // заполняем мапу ответами и данными респондентов
|
|
||||||
// for _, answer := range answers {
|
|
||||||
// if answer.Result {
|
|
||||||
// results[answer.Session] = answer
|
|
||||||
// } else {
|
|
||||||
// standart[answer.Session] = append(standart[answer.Session], answer)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // записываем данные в файл
|
|
||||||
// row := 2
|
|
||||||
// for session, _ := range results {
|
|
||||||
// response := standart[session]
|
|
||||||
// if err := file.SetCellValue(sheet, "A"+strconv.Itoa(row), results[session].Content); err != nil {
|
|
||||||
// return err
|
|
||||||
// }
|
|
||||||
// count := 2
|
|
||||||
// for _, q := range questions {
|
|
||||||
// if !q.Deleted && q.Type != model.TypeResult {
|
|
||||||
// sort.Slice(response, func(i, j int) bool {
|
|
||||||
// return response[i].QuestionId < response[j].QuestionId
|
|
||||||
// })
|
|
||||||
// index := binarySearch(response, q.Id)
|
|
||||||
// if index != -1 {
|
|
||||||
// cell := ToAlphaString(count) + strconv.Itoa(row)
|
|
||||||
// typeMap := FileSearch(response[index].Content)
|
|
||||||
// noAccept := make(map[string]struct{})
|
|
||||||
// todoMap := make(map[string]string)
|
|
||||||
// for _, tipe := range typeMap {
|
|
||||||
// if tipe != "Text" && q.Type == model.TypeImages || q.Type == model.TypeVarImages {
|
|
||||||
// urle := ExtractImageURL(response[index].Content)
|
|
||||||
// urlData := strings.Split(urle, " ")
|
|
||||||
// for _, k := range urlData {
|
|
||||||
// u, err := url.Parse(k)
|
|
||||||
// if err == nil && u.Scheme != "" && u.Host != "" {
|
|
||||||
// picture, err := downloadImage(k)
|
|
||||||
// if err != nil {
|
|
||||||
// return err
|
|
||||||
// }
|
|
||||||
// file.SetColWidth(sheet, ToAlphaString(count), ToAlphaString(count), 50)
|
|
||||||
// file.SetRowHeight(sheet, row, 150)
|
|
||||||
// if err := file.AddPictureFromBytes(sheet, cell, picture); err != nil {
|
|
||||||
// return err
|
|
||||||
// }
|
|
||||||
// noAccept[response[index].Content] = struct{}{}
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// } else if tipe != "Text" && q.Type == model.TypeFile {
|
|
||||||
// urle := ExtractImageURL(response[index].Content)
|
|
||||||
// display, tooltip := urle, urle
|
|
||||||
// if err := file.SetCellValue(sheet, cell, response[index].Content); err != nil {
|
|
||||||
// return err
|
|
||||||
// }
|
|
||||||
// if err := file.SetCellHyperLink(sheet, cell, urle, "External", excelize.HyperlinkOpts{
|
|
||||||
// Display: &display,
|
|
||||||
// Tooltip: &tooltip,
|
|
||||||
// }); err != nil {
|
|
||||||
// return err
|
|
||||||
// }
|
|
||||||
// noAccept[response[index].Content] = struct{}{}
|
|
||||||
// } else {
|
|
||||||
// todoMap[response[index].Content] = cell
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// for cnt, cel := range todoMap {
|
|
||||||
// if _, ok := noAccept[cnt]; !ok {
|
|
||||||
// if err := file.SetCellValue(sheet, cel, cnt); err != nil {
|
|
||||||
// return err
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// } else {
|
|
||||||
// cell := ToAlphaString(count) + strconv.Itoa(row)
|
|
||||||
// if err := file.SetCellValue(sheet, cell, "-"); err != nil {
|
|
||||||
// return err
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// count++
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// cell := ToAlphaString(len(headers)) + strconv.Itoa(row)
|
|
||||||
// if err := file.SetCellValue(sheet, cell, mapQueRes[results[session].QuestionId]); err != nil {
|
|
||||||
// return err
|
|
||||||
// }
|
|
||||||
// row++
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // cохраняем данные в буфер
|
|
||||||
// if err := file.Write(buffer); err != nil {
|
|
||||||
// return err
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// return nil
|
|
||||||
//}
|
|
Loading…
Reference in New Issue
Block a user