cleaning pj

This commit is contained in:
pasha1coil 2025-07-09 14:40:02 +03:00
parent f42cb1ce44
commit ff8956355f
8 changed files with 94 additions and 709 deletions

@ -4,27 +4,15 @@ import (
"context"
"errors"
"fmt"
"gitea.pena/PenaSide/common/log_mw"
"gitea.pena/PenaSide/hlog"
"gitea.pena/PenaSide/trashlog/wrappers/zaptrashlog"
"gitea.pena/SQuiz/answerer/clients"
dalBS "gitea.pena/SQuiz/answerer/dal"
"gitea.pena/SQuiz/answerer/models"
"gitea.pena/SQuiz/answerer/savewc"
"gitea.pena/SQuiz/answerer/service"
"gitea.pena/SQuiz/common/dal"
"gitea.pena/SQuiz/common/healthchecks"
"gitea.pena/SQuiz/common/middleware"
"gitea.pena/SQuiz/common/model"
"gitea.pena/SQuiz/common/utils"
"github.com/go-redis/redis/v8"
"github.com/gofiber/fiber/v2"
"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
"github.com/skeris/appInit"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"time"
)
type App struct {
@ -55,21 +43,13 @@ var _ appInit.CommonApp = (*App)(nil)
type Options struct {
LoggerProdMode bool `env:"IS_PROD_LOG" default:"false"`
IsProd bool `env:"IS_PROD" default:"false"`
MinioEP string `env:"MINIO_EP" default:"localhost:3002"`
MinioAK string `env:"MINIO_AK" default:"minio"`
MinioSK string `env:"MINIO_SK" default:"miniostorage"`
NumberPort string `env:"PORT" default:"1490"`
CrtFile string `env:"CRT" default:"server.crt"`
KeyFile string `env:"KEY" default:"server.key"`
PostgresCredentials string `env:"PG_CRED" default:"host=localhost port=5432 user=squiz password=Redalert2 dbname=squiz sslmode=disable"`
RedisHost string `env:"REDIS_HOST"`
RedisPassword string `env:"REDIS_PASSWORD"`
RedisDB uint64 `env:"REDIS_DB"`
RedirectURL string `env:"REDIRECT_URL" default:"https://squiz.pena.digital"`
PubKey string `env:"PUBLIC_KEY"`
PrivKey string `env:"PRIVATE_KEY"`
TrashLogHost string `env:"TRASH_LOG_HOST" default:"localhost:7113"`
ModuleLogger string `env:"MODULE_LOGGER" default:"answerer-local"`
}
func New(ctx context.Context, opts interface{}, ver appInit.Version) (appInit.CommonApp, error) {
@ -102,77 +82,26 @@ func New(ctx context.Context, opts interface{}, ver appInit.Version) (appInit.Co
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{})
// Initialize minio client object.
minioClient, err := minio.New(options.MinioEP, &minio.Options{
Creds: credentials.NewStaticV4(options.MinioAK, options.MinioSK, ""),
Secure: options.IsProd,
})
if err != nil {
fmt.Println("MINIOERR", options.MinioEP, err)
return nil, err
}
pgdal, err := dal.New(ctx, options.PostgresCredentials, minioClient)
pgdal, err := dal.New(ctx, options.PostgresCredentials, nil)
if err != nil {
return nil, err
}
zapLogger.Info("config", zap.Any("options", options))
//init redis
redisClient := redis.NewClient(&redis.Options{
Addr: options.RedisHost,
Password: options.RedisPassword,
DB: int(options.RedisDB),
})
encrypt := utils.NewEncrypt(options.PubKey, options.PrivKey)
workerSendClientCh := make(chan model.Answer, 50)
workerRespondentCh := make(chan []model.Answer, 50)
blobstore, err := dalBS.New(ctx, minioClient)
svc, err := service.New(service.ServiceDeps{
Dal: pgdal,
Encrypt: encrypt,
RedirectURl: options.RedirectURL,
AiClient: clients.NewAiClient("https://alvatar.com/api/engine/send_answer"),
})
if err != nil {
zapLogger.Error("failed to create service", zap.Error(err))
return nil, err
}
//svc := service.New(blobstore, pgdal, workerRespondentCh, workerSendClientCh)
svc := service.New(service.ServiceDeps{
Store: blobstore,
Dal: pgdal,
WorkerSendClientCh: workerSendClientCh,
WorkerRespondentCh: workerRespondentCh,
Encrypt: encrypt,
RedirectURl: options.RedirectURL,
AiClient: clients.NewAiClient("https://alvatar.com/api/engine/send_answer"),
})
saveRespWcData := savewc.DepsForResp{
WorkerRespondentCh: workerRespondentCh,
Redis: redisClient,
}
saveClientWcData := savewc.DepsForClient{
WorkerSendClientCh: workerSendClientCh,
Redis: redisClient,
}
saveRespWorker := savewc.NewSaveRespWorker(saveRespWcData, errChan, loggerHlog)
saveClientWorker := savewc.NewSaveClientWorker(saveClientWcData, errChan, loggerHlog)
go saveRespWorker.Start(ctx)
go saveClientWorker.Start(ctx)
app := fiber.New(fiber.Config{BodyLimit: 70 * 1024 * 1024})
@ -186,38 +115,28 @@ func New(ctx context.Context, opts interface{}, ver appInit.Version) (appInit.Co
})
app.Use(middleware.AnswererChain())
app.Use(log_mw.ContextLogger(loggerHlog))
app.Get("/liveness", healthchecks.Liveness)
app.Get("/readiness", healthchecks.Readiness(&workerErr)) //todo parametrized readiness. should discuss ready reason
app = svc.Register(app)
fmt.Println("SERVERSTART", fmt.Sprintf(":%s", options.NumberPort))
loggerHlog.Emit(InfoSvcReady{})
go func() {
defer func() {
//if pgdal != nil {
// pgdal.CloseAnswerer()
//}
err := app.Shutdown()
if err != nil {
loggerHlog.Emit(InfoSvcShutdown{Signal: err.Error()})
zapLogger.Error("failed graceful shutdown", zap.Error(err))
}
}()
if options.IsProd {
if err := app.ListenTLS(fmt.Sprintf(":%s", options.NumberPort), options.CrtFile, options.KeyFile); err != nil {
loggerHlog.Emit(ErrorCanNotServe{
Err: err,
})
zapLogger.Error("failed start server", zap.Error(err))
errChan <- err
}
} else {
if err := app.Listen(fmt.Sprintf(":%s", options.NumberPort)); err != nil {
loggerHlog.Emit(ErrorCanNotServe{
Err: err,
})
zapLogger.Error("failed start server", zap.Error(err))
errChan <- err
}
}

@ -1,32 +0,0 @@
package dal
import (
"context"
"fmt"
"github.com/minio/minio-go/v7"
"io"
)
const (
bucket = "3c580be9-cf31f296-d055-49cf-b39e-30c7959dc17b"
bucketAnswers = "squizanswer"
)
type Storer struct {
client *minio.Client
}
func New(ctx context.Context, minioClient *minio.Client) (*Storer, error) {
return &Storer{
client: minioClient,
}, nil
}
func (s *Storer) PutAnswer(ctx context.Context, file io.Reader, quizId, name string, question uint64, size int64) error {
if _, err := s.client.PutObject(ctx, bucket, bucketAnswers + "/" +fmt.Sprintf("%s/%d/%s", quizId, question, name), file, size,
minio.PutObjectOptions{}); err != nil {
return err
}
return nil
}

@ -1,70 +0,0 @@
package models
type AllFields struct {
CtxUserIP string
CtxUserPort string
KeyDomain string
KeyPath string
KeyOS string
KeyDevice string
KeyDeviceType string
KeyBrowser string
CtxQuiz string
CtxReferrer string
CtxIDInt int64
CtxSession string
CtxQuizID int64
CtxQuestionID int64
}
type InfoQuizOpen struct { // при получении настроек квиза
KeyOS string
KeyDevice string
KeyDeviceType string
KeyBrowser string // то самое, что получается из заголовков и складывается в модель ответа. на самом деле, ему место тут
CtxQuiz string // айдишник квиза, который qid
CtxQuizID int64 // айдишник квиза
CtxReferrer string // тоже из заголовков
CtxIDInt int64 // айдишник ответа
CtxSession string // сессия
}
type InfoAnswer struct { // при любом ответе на вопрос
KeyOS string
KeyDevice string
KeyDeviceType string
KeyBrowser string // то самое, что получается из заголовков и складывается в модель ответа. на самом деле, ему место тут
CtxQuiz string // айдишник квиза, который qid
CtxQuizID int64 // айдишник квиза
CtxReferrer string // тоже из заголовков
CtxQuestionID int64 // айдишник вопроса, на который отвечено
CtxIDInt int64 // айдишник ответа
CtxSession string // сессия
}
type InfoResult struct { // если ответ на вопрос с типом result
KeyOS string
KeyDevice string
KeyDeviceType string
KeyBrowser string // то самое, что получается из заголовков и складывается в модель ответа. на самом деле, ему место тут
CtxQuiz string // айдишник квиза, который qid
CtxQuizID int64 // айдишник квиза
CtxReferrer string // тоже из заголовков
CtxQuestionID int64 // айдишник вопроса, на который отвечено
CtxIDInt int64 // айдишник ответа
CtxSession string // сессия
}
// todo понять для чего это событие вроде как контакты приходят в ответахс с result = true там парситься контент с контактной информацией
type InfoContactForm struct { // если ответ на вопрос с типом result, без result == true (возможно перепутал с предыдущим. в этом ответе приходят контактные данные респондента)
KeyOS string
KeyDevice string
KeyDeviceType string
KeyBrowser string // то самое, что получается из заголовков и складывается в модель ответа. на самом деле, ему место тут
CtxQuiz string // айдишник квиза, который qid
CtxQuizID int64 // айдишник квиза
CtxReferrer string // тоже из заголовков
CtxQuestionID int64 // айдишник вопроса, на который отвечено
CtxIDInt int64 // айдишник ответа
CtxSession string // сессия
}

3
quiz_config.json Normal file

@ -0,0 +1,3 @@
{
}

@ -1,72 +0,0 @@
package savewc
import (
"context"
"encoding/json"
"fmt"
"gitea.pena/PenaSide/hlog"
"gitea.pena/SQuiz/common/model"
"github.com/go-redis/redis/v8"
"time"
)
type DepsForClient struct {
WorkerSendClientCh chan model.Answer
Redis *redis.Client
}
type SaveForClient struct {
deps DepsForClient
errChan chan<- error
logger hlog.Logger
}
func NewSaveClientWorker(deps DepsForClient, errChan chan<- error, logger hlog.Logger) *SaveForClient {
return &SaveForClient{
deps: deps,
errChan: errChan,
logger: logger,
}
}
func (w *SaveForClient) Start(ctx context.Context) {
for {
select {
case answer, ok := <-w.deps.WorkerSendClientCh:
if !ok {
return
}
fmt.Println("SAVECLINT")
err := w.saveAnswer(ctx, answer)
if err != nil {
fmt.Println("Error save answer")
w.errChan <- err
}
case <-ctx.Done():
fmt.Println("Save for client worker terminated")
return
}
}
}
func (w *SaveForClient) saveAnswer(ctx context.Context, answer model.Answer) error {
answerJSON, err := json.Marshal(answer)
if err != nil {
fmt.Println("Error marshal answer to redis", err)
w.errChan <- err
return err
}
key := fmt.Sprintf("answer:%d", time.Now().UnixNano())
err = w.deps.Redis.Set(ctx, key, answerJSON, 0).Err()
if err != nil {
fmt.Println("Error saving answer to redis", err)
w.errChan <- err
return err
}
return nil
}

@ -1,71 +0,0 @@
package savewc
import (
"context"
"encoding/json"
"fmt"
"gitea.pena/PenaSide/hlog"
"gitea.pena/SQuiz/common/model"
"github.com/go-redis/redis/v8"
"time"
)
type DepsForResp struct {
WorkerRespondentCh chan []model.Answer
Redis *redis.Client
}
type SaveForRespondent struct {
deps DepsForResp
errChan chan<- error
logger hlog.Logger
}
func NewSaveRespWorker(deps DepsForResp, errChan chan<- error, logger hlog.Logger) *SaveForRespondent {
return &SaveForRespondent{
deps: deps,
errChan: errChan,
logger: logger,
}
}
func (w *SaveForRespondent) Start(ctx context.Context) {
for {
select {
case answer, ok := <-w.deps.WorkerRespondentCh:
if !ok {
return
}
fmt.Println("SAVERESP")
err := w.saveAnswers(ctx, answer)
if err != nil {
w.logger.Module("Error save answers")
w.errChan <- err
}
case <-ctx.Done():
w.logger.Module("Save for respondent worker terminated")
return
}
}
}
func (w *SaveForRespondent) saveAnswers(ctx context.Context, answers []model.Answer) error {
for _, answer := range answers {
answerJSON, err := json.Marshal(answer)
if err != nil {
fmt.Println("Error marshal answer", err)
w.errChan <- err
}
key := fmt.Sprintf("toRespondent:%d", time.Now().UnixNano())
err = w.deps.Redis.Set(ctx, key, answerJSON, 0).Err()
if err != nil {
fmt.Println("Error setting to redis", err)
w.errChan <- err
}
}
return nil
}

21
service/config.go Normal file

@ -0,0 +1,21 @@
package service
import (
"encoding/json"
"fmt"
"os"
)
func loadQuizDataConfig() (GetQuizDataResp, error) {
configPath := "./quiz_config.json"
data, err := os.ReadFile(configPath)
if err != nil {
return GetQuizDataResp{}, fmt.Errorf("failed to read config file %s: %w", configPath, err)
}
var config GetQuizDataResp
if err := json.Unmarshal(data, &config); err != nil {
return GetQuizDataResp{}, fmt.Errorf("failed to parse config file: %w", err)
}
return config, nil
}

@ -1,23 +1,19 @@
package service
import (
"unicode/utf8"
"encoding/json"
"fmt"
"gitea.pena/PenaSide/common/log_mw"
"gitea.pena/SQuiz/answerer/clients"
"gitea.pena/SQuiz/answerer/dal"
"gitea.pena/SQuiz/answerer/models"
quizdal "gitea.pena/SQuiz/common/dal"
"gitea.pena/SQuiz/common/middleware"
"gitea.pena/SQuiz/common/model"
"gitea.pena/SQuiz/common/utils"
"github.com/gofiber/fiber/v2"
"net/url"
"strconv"
"strings"
"sync"
"time"
"unicode/utf8"
"github.com/rs/xid"
)
@ -28,58 +24,45 @@ const (
)
type Service struct {
store *dal.Storer
dal *quizdal.DAL
batch []model.Answer
m sync.Mutex
workerRespondentCh chan<- []model.Answer
workerSendClientCh chan<- model.Answer
encrypt *utils.Encrypt
redirectURl string
aiClient *clients.AIClient
dal *quizdal.DAL
batch []model.Answer
m sync.Mutex
encrypt *utils.Encrypt
redirectURl string
aiClient *clients.AIClient
quizConfigData GetQuizDataResp
}
type ServiceDeps struct {
Store *dal.Storer
Dal *quizdal.DAL
WorkerRespondentCh chan<- []model.Answer
WorkerSendClientCh chan<- model.Answer
Encrypt *utils.Encrypt
RedirectURl string
AiClient *clients.AIClient
Dal *quizdal.DAL
Encrypt *utils.Encrypt
RedirectURl string
AiClient *clients.AIClient
}
func New(deps ServiceDeps) *Service {
return &Service{
store: deps.Store,
dal: deps.Dal,
m: sync.Mutex{},
batch: []model.Answer{},
workerRespondentCh: deps.WorkerRespondentCh,
workerSendClientCh: deps.WorkerSendClientCh,
encrypt: deps.Encrypt,
redirectURl: deps.RedirectURl,
aiClient: deps.AiClient,
func New(deps ServiceDeps) (*Service, error) {
quizData, err := loadQuizDataConfig()
if err != nil {
return nil, err
}
return &Service{
dal: deps.Dal,
m: sync.Mutex{},
batch: []model.Answer{},
encrypt: deps.Encrypt,
redirectURl: deps.RedirectURl,
aiClient: deps.AiClient,
quizConfigData: quizData,
}, nil
}
func (s *Service) Register(app *fiber.App) *fiber.App {
app.Post("/answer", s.PutAnswersOnePiece)
app.Post("/settings", s.GetQuizData)
app.Get("/logo", s.MiniPart)
return app
}
// GetQuizDataReq request data for get data for user
type GetQuizDataReq struct {
QuizId string `json:"quiz_id"` // relation to quiz
Limit uint64 `json:"limit"`
Page uint64 `json:"page"`
NeedConfig bool `json:"need_config"` // true if you need not only question page
Auditory int64 `json:"auditory"`
}
// GetQuizDataResp response with prepared data for user
type GetQuizDataResp struct {
Settings *ShavedQuiz `json:"settings,omitempty"`
Items []ShavedQuestion `json:"items"`
@ -87,7 +70,6 @@ type GetQuizDataResp struct {
ShowBadge bool `json:"show_badge"`
}
// ShavedQuiz shortened struct for delivery data to customer
type ShavedQuiz struct {
Fingerprinting bool `json:"fp"`
Repeatable bool `json:"rep"`
@ -100,7 +82,6 @@ type ShavedQuiz struct {
Status string `json:"status"`
}
// ShavedQuestion shortened struct for delivery data to customer
type ShavedQuestion struct {
Id uint64 `json:"id"`
Title string `json:"title"`
@ -113,209 +94,7 @@ type ShavedQuestion struct {
// GetQuizData handler for obtaining data for quiz front rendering
func (s *Service) GetQuizData(c *fiber.Ctx) error {
cs, ok := c.Context().Value(middleware.ContextKey(middleware.SessionKey)).(string)
if !ok {
return c.Status(fiber.StatusUnauthorized).SendString("no session in cookie")
}
hlogger := log_mw.ExtractLogger(c)
var req GetQuizDataReq
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).SendString("Invalid request data")
}
if req.QuizId == "" {
return c.Status(fiber.StatusBadRequest).SendString("Invalid request data")
}
if req.Limit == 0 && !req.NeedConfig {
return c.Status(fiber.StatusLengthRequired).SendString("no data requested")
}
quiz, err := s.dal.QuizRepo.GetQuizByQid(c.Context(), req.QuizId)
if err != nil {
return c.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
if req.Limit == 0 && req.NeedConfig {
quizDto := dao2dtoQuiz(quiz)
return c.Status(fiber.StatusOK).JSON(GetQuizDataResp{
Settings: &quizDto,
})
}
if quiz.UniqueAnswers {
//todo implement after creating store answers
}
if quiz.Status != model.StatusStart && quiz.Status != model.StatusAI {
return c.Status(fiber.StatusLocked).SendString("quiz is inactive")
}
if quiz.Limit > 0 {
// todo implement after creating store answer
}
if quiz.DueTo < uint64(time.Now().Unix()) && quiz.DueTo > 0 {
return c.Status(fiber.StatusGone).SendString("quiz timeouted")
}
account, err := s.dal.AccountRepo.GetAccountByID(c.Context(), quiz.AccountId)
if err != nil {
return c.Status(fiber.StatusInternalServerError).SendString("can`t get account by quiz.AccountId")
}
showBadge := true
if priv, ok := account.Privileges["squizHideBadge"]; ok {
expiration := priv.CreatedAt.Add(time.Duration(priv.Amount) * 24 * time.Hour)
if time.Now().Before(expiration) {
showBadge = false
}
}
var questions []model.Question
var cnt uint64
if quiz.Status == model.StatusAI {
if req.Page >= 1 {
req.Page+=1
}
questions, cnt, err = s.dal.QuestionRepo.GetQuestionsAI(c.Context(), int64(quiz.Id), cs, int32(req.Limit), int32(req.Page*req.Limit), req.Auditory)
if err != nil {
return c.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
} else {
questions, cnt, err = s.dal.QuestionRepo.GetQuestionList(
c.Context(),
req.Limit,
req.Page*req.Limit,
0, 0, quiz.Id, false, false, "", "", req.Auditory,
)
if err != nil {
return c.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
}
result := GetQuizDataResp{
Count: cnt,
Items: []ShavedQuestion{},
ShowBadge: showBadge,
}
if req.NeedConfig {
quizDto := dao2dtoQuiz(quiz)
result.Settings = &quizDto
}
for _, q := range questions {
result.Items = append(result.Items, dao2dtoQuestion(q))
}
utmData := model.UTMSavingMap{
"utm_content": c.Query("utm_content"),
"utm_medium": c.Query("utm_medium"),
"utm_campaign": c.Query("utm_campaign"),
"utm_source": c.Query("utm_source"),
"utm_term": c.Query("utm_term"),
"utm_referrer": c.Query("utm_referrer"),
"roistat": c.Query("roistat"),
"referrer": c.Query("referrer"),
"openstat_service": c.Query("openstat_service"),
"openstat_campaign": c.Query("openstat_campaign"),
"openstat_ad": c.Query("openstat_ad"),
"openstat_source": c.Query("openstat_source"),
"from": c.Query("from"),
"gclientid": c.Query("gclientid"),
"_ym_uid": c.Query("_ym_uid"),
"_ym_counter": c.Query("_ym_counter"),
"gclid": c.Query("gclid"),
"yclid": c.Query("yclid"),
"fbclid": c.Query("fbclid"),
}
deviceType := c.Get("DeviceType")
os := c.Get("OS")
browser := c.Get("Browser")
ip := c.IP()
device := c.Get("Device")
referrer := c.Get("Referer")
fp := ""
if cfp := c.Cookies(fingerprintCookie); cfp != "" {
fp = cfp
}
if req.NeedConfig {
if len(questions) == 0 {
if req.Auditory != 0 {
return c.Status(fiber.StatusAccepted).SendString("questions are not ready yet")
}
return c.Status(fiber.StatusNotFound).SendString("question not found")
}
answers, errs := s.dal.AnswerRepo.CreateAnswers(c.Context(), []model.Answer{{
Content: "start",
QuestionId: questions[0].Id,
QuizId: quiz.Id,
Start: true,
DeviceType: deviceType,
Device: device,
Browser: browser,
IP: ip,
OS: os,
Utm: utmData,
}}, cs, fp, quiz.Id)
if len(errs) != 0 {
return c.Status(fiber.StatusInternalServerError).SendString(errs[0].Error())
}
hlogger.Emit(models.InfoQuizOpen{
KeyOS: os,
KeyDevice: device,
KeyDeviceType: deviceType,
KeyBrowser: browser,
CtxQuiz: req.QuizId,
CtxQuizID: int64(quiz.Id),
CtxReferrer: referrer,
CtxIDInt: int64(answers[0].Id),
CtxSession: cs,
})
}
fmt.Println("SETTIIIIII", cnt <= req.Limit, result)
if cnt <= req.Limit {
return c.Status(fiber.StatusOK).JSON(result)
} else {
return c.Status(fiber.StatusPartialContent).JSON(result)
}
}
func dao2dtoQuestion(question model.Question) ShavedQuestion {
return ShavedQuestion{
Id: question.Id,
Title: question.Title,
Description: question.Description,
Type: question.Type,
Required: false,
Page: question.Page,
Content: question.Content,
}
}
func dao2dtoQuiz(quiz model.Quiz) ShavedQuiz {
return ShavedQuiz{
Fingerprinting: quiz.Fingerprinting,
Repeatable: quiz.Repeatable,
Name: quiz.Name,
Config: quiz.Config,
Limit: quiz.Limit,
DueTo: quiz.DueTo,
TimeOfPassing: quiz.TimeOfPassing,
Pausable: quiz.Pausable,
Status: quiz.Status,
}
return c.Status(fiber.StatusOK).JSON(s.quizConfigData)
}
// MB Size constants
@ -329,17 +108,12 @@ type PutAnswersResponse struct {
Stored []uint64 `json:"stored"`
}
// todo не отдавать ответы с типом start но сохранять брать из контента ответа
// поле бул также в GetQuizdata не отдавать его, но по запросам идет GetQuizdata получаем данные квиза
// аотом отправляем ответы в PutAnswers и сохраяняем их подумать как делать
func (s *Service) PutAnswersOnePiece(c *fiber.Ctx) error {
cs, ok := c.Context().Value(middleware.ContextKey(middleware.SessionKey)).(string)
if !ok {
return c.Status(fiber.StatusUnauthorized).SendString("no session in cookie")
}
hlogger := log_mw.ExtractLogger(c)
form, err := c.MultipartForm()
if err != nil || form == nil || form.File == nil {
return c.Status(fiber.StatusBadRequest).SendString("expecting multipart form file")
@ -361,8 +135,8 @@ func (s *Service) PutAnswersOnePiece(c *fiber.Ctx) error {
}
var (
answersRaw, answers, trueRes []model.Answer
errs []error
answersRaw, answers []model.Answer
errs []error
)
deviceType := c.Get("DeviceType")
@ -370,7 +144,6 @@ func (s *Service) PutAnswersOnePiece(c *fiber.Ctx) error {
browser := c.Get("Browser")
ip := c.IP()
device := c.Get("Device")
referrer := c.Get("Referer")
if err := json.Unmarshal([]byte(answersStr[0]), &answersRaw); err != nil {
return c.Status(fiber.StatusBadRequest).SendString("not valid answers string")
@ -394,42 +167,39 @@ func (s *Service) PutAnswersOnePiece(c *fiber.Ctx) error {
fileIDMap := make(map[uint64]string)
for _, ans := range answersRaw {
if quiz.Status == model.StatusAI {
final := false
if finalStr, exists := form.Value["final"]; exists && len(finalStr) > 0 {
parsedFinal, err := strconv.ParseBool(finalStr[0])
if err != nil {
return c.Status(fiber.StatusBadRequest).SendString("invalid final value")
}
final = parsedFinal
}
question, err := s.dal.QuestionRepo.GetQuestionListByIDs(c.Context(), []int32{int32(ans.QuestionId)})
final := false
if finalStr, exists := form.Value["final"]; exists && len(finalStr) > 0 {
parsedFinal, err := strconv.ParseBool(finalStr[0])
if err != nil {
return c.Status(fiber.StatusInternalServerError).SendString("can not get questions")
return c.Status(fiber.StatusBadRequest).SendString("invalid final value")
}
final = parsedFinal
}
if len(question) == 0 {
return c.Status(fiber.StatusNotFound).SendString("no questions found")
}
question, err := s.dal.QuestionRepo.GetQuestionListByIDs(c.Context(), []int32{int32(ans.QuestionId)})
if err != nil {
return c.Status(fiber.StatusInternalServerError).SendString("can not get questions")
}
questionText, err := s.aiClient.SendAnswerer(final, question[0].Type, ans.Content, cs)
if err != nil {
return c.Status(fiber.StatusInternalServerError).SendString(fmt.Sprintf("failed send answer to ai, err: %s", err.Error()))
}
if len(question) == 0 {
return c.Status(fiber.StatusNotFound).SendString("no questions found")
}
_, err = s.dal.QuestionRepo.CreateQuestion(c.Context(), &model.Question{
QuizId: quiz.Id,
Title: truncateUTF8(questionText,512),
Type: model.TypeText,
Session: cs,
Content: `{"id":"gcZ-9SET-sM6stZSzQMmu","hint":{"text":" ","video":" "},"rule":{"children":[],"main":[],"parentId":" ","default":" "},"back":" ","originalBack":" ","autofill":false,"placeholder":" ","innerNameCheck":false,"innerName":" ","required":false,"answerType":"single","onlyNumbers":false}`,
Description: questionText,
})
if err != nil {
return c.Status(fiber.StatusInternalServerError).SendString(fmt.Sprintf("failed create question type ai, err: %s", err.Error()))
}
questionText, err := s.aiClient.SendAnswerer(final, question[0].Type, ans.Content, cs)
if err != nil {
return c.Status(fiber.StatusInternalServerError).SendString(fmt.Sprintf("failed send answer to ai, err: %s", err.Error()))
}
_, err = s.dal.QuestionRepo.CreateQuestion(c.Context(), &model.Question{
QuizId: quiz.Id,
Title: truncateUTF8(questionText, 512),
Type: model.TypeText,
Session: cs,
Content: `{"id":"gcZ-9SET-sM6stZSzQMmu","hint":{"text":" ","video":" "},"rule":{"children":[],"main":[],"parentId":" ","default":" "},"back":" ","originalBack":" ","autofill":false,"placeholder":" ","innerNameCheck":false,"innerName":" ","required":false,"answerType":"single","onlyNumbers":false}`,
Description: questionText,
})
if err != nil {
return c.Status(fiber.StatusInternalServerError).SendString(fmt.Sprintf("failed create question type ai, err: %s", err.Error()))
}
ans.DeviceType = deviceType
@ -449,16 +219,8 @@ func (s *Service) PutAnswersOnePiece(c *fiber.Ctx) error {
continue
}
r, err := fileparts[0].Open()
if err != nil {
errs = append(errs, fmt.Errorf("can not open part for file: %s", filekey))
continue
}
fname := fmt.Sprintf("%s.%s", xid.New().String(), filetail)
if err := s.store.PutAnswer(c.Context(), r, quizID[0], fname, ans.QuestionId, fileparts[0].Size); err != nil {
return c.Status(fiber.StatusInternalServerError).SendString("can not upload file answers")
}
ans.Content = fname
fileIDMap[ans.QuestionId] = fname
@ -475,22 +237,6 @@ func (s *Service) PutAnswersOnePiece(c *fiber.Ctx) error {
return c.Status(fiber.StatusInternalServerError).SendString("error unmarshalling answer content: " + err.Error())
}
ans.Email = content.Email
hlogger.Emit(models.InfoContactForm{
KeyOS: os,
KeyDevice: device,
KeyDeviceType: deviceType,
KeyBrowser: browser,
CtxQuiz: quizID[0],
CtxQuizID: int64(quiz.Id),
CtxReferrer: referrer,
CtxQuestionID: int64(ans.QuestionId),
CtxIDInt: int64(ans.Id),
CtxSession: cs,
})
s.workerSendClientCh <- ans
trueRes = append(trueRes, ans)
}
}
@ -500,10 +246,6 @@ func (s *Service) PutAnswersOnePiece(c *fiber.Ctx) error {
return c.Status(fiber.StatusInternalServerError).SendString("can not unmarshal quiz config")
}
if quizConfig.Mailing.When == "email" && len(trueRes) > 0 {
s.workerRespondentCh <- trueRes
}
stored, ers := s.dal.AnswerRepo.CreateAnswers(c.Context(), answers, cs, fp, quiz.Id)
if len(ers) != 0 {
for _, err := range ers {
@ -517,33 +259,6 @@ func (s *Service) PutAnswersOnePiece(c *fiber.Ctx) error {
var questionIDs []uint64
for _, ans := range stored {
questionIDs = append(questionIDs, ans.QuestionId)
if ans.Result {
hlogger.Emit(models.InfoResult{
KeyOS: os,
KeyDevice: device,
KeyDeviceType: deviceType,
KeyBrowser: browser,
CtxQuiz: quizID[0],
CtxQuizID: int64(quiz.Id),
CtxReferrer: referrer,
CtxQuestionID: int64(ans.QuestionId),
CtxIDInt: int64(ans.Id),
CtxSession: cs,
})
continue
}
hlogger.Emit(models.InfoAnswer{
KeyOS: os,
KeyDevice: device,
KeyDeviceType: deviceType,
KeyBrowser: browser,
CtxQuiz: quizID[0],
CtxQuizID: int64(quiz.Id),
CtxReferrer: referrer,
CtxQuestionID: int64(ans.QuestionId),
CtxIDInt: int64(ans.Id),
CtxSession: cs,
})
}
response := PutAnswersResponse{
@ -554,39 +269,11 @@ func (s *Service) PutAnswersOnePiece(c *fiber.Ctx) error {
return c.Status(fiber.StatusOK).JSON(response)
}
func (s *Service) MiniPart(ctx *fiber.Ctx) error {
qid := ctx.Query("q")
if qid == "" {
return ctx.Status(fiber.StatusBadRequest).SendString("qid is nil")
}
ctx.Cookie(&fiber.Cookie{
Name: "quizFrom",
Value: qid,
})
userID, err := s.dal.AccountRepo.GetQidOwner(ctx.Context(), qid)
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
shifr, err := s.encrypt.EncryptStr(userID)
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
ctx.Cookie(&fiber.Cookie{
Name: "quizUser",
Value: url.QueryEscape(string(shifr)),
})
return ctx.Redirect(s.redirectURl, fiber.StatusFound)
}
func truncateUTF8(s string, maxLen int) string {
if utf8.RuneCountInString(s) <= maxLen {
return s
}
// Конвертируем строку в руны для корректной обработки Unicode
runes := []rune(s)
return string(runes[:maxLen])