Mars工作室2023后端面试题

发布于11个月前
(更新于11个月前)
李志嘉撰写

60分钟。不限开发环境,不限编程语言及生态,不允许使用AI工具直接生成代码。

一、题目

漂流瓶,也称为浮标,是一种在海洋中漂流的瓶子,用于研究海洋洋流和浪漫目的。

公元前310年,希腊哲学家提奥弗拉斯托斯试图通过放置一些漂流瓶并等待它们的回归来证明地中海是由大西洋洋流形成的,不过它们最终并没有回来。在16世纪期间,英国海军通过发送加密的瓶中信件与伊丽莎白女王一世进行通信。这些漂流密码具有极高的战略价值,以至于女王任命了一位官方的“海洋瓶子开启者”——任何其他人敢于打开它们都将面临死刑。

1914年6月,格拉斯哥航海学校的C·亨特·布朗船长放置了近2000个编号的瓶子。找到瓶子的人被要求在最近的邮局投下一张说明其位置的便条。说明中解释道:“我们的目标是找出北海深层洋流的方向”。其中一个在2012年被发现,成为世界上最古老的漂流瓶中信。

现在,让我们来尝试模拟使用漂流瓶传递信息,请编写一个程序,监听7000端口,并通过 HTTP 协议实现如下功能,你可以自行设计API风格:

  • 不需要任何用户认证和鉴权系统,所有操作完全匿名,如果需要,可以下发临时的认证token。

  • 用户可以创建一个”瓶子”并附上信息,然后”抛入海洋”,随后将有可能被其它用户收到。

  • 用户可以尝试获取一个瓶子,并查看其中信息,随后,用户可以做三个操作:

    • 保留该瓶子,后续不会继续被其它用户收到,签发给用户一个安全的token以便以后用户查询该瓶子。
    • 将该瓶子扔回”海洋”,该瓶子会继续漂流并有可能被更多用户收到。
    • 在瓶子中额外加入一段信息,然后扔回”海洋”,该瓶子会继续漂流并有可能被更多用户收到,下一个收到瓶子的用户将能看到附加的信息。
    • 但是,如果用户在30分钟后没有执行任何操作,则删除该瓶子,同时用户也无法再次访问该瓶子。
  • 额外功能(可选,加分项):

    • 有 OpenAPI 文档,内建 Swagger UI 支持。
    • 采用docker打包,并提供Dockerfile

二、参考代码(Golang实现)

模块名为git.online.njtech.edu.cn/online/backend_itv_2023_go

1. 代码

data.go

package main

import (
	"gorm.io/gorm"
)

type Bottle struct {
	gorm.Model
	Content     string
	Owner       string
	Attachments []Attachment
}

type Attachment struct {
	gorm.Model
	BottleID uint
	Content  string
}

type BottleRepo struct {
	db *gorm.DB
}

func (r *BottleRepo) Create(bottle *Bottle) error {
	return r.db.Create(bottle).Error
}

func (r *BottleRepo) GetById(id uint) (*Bottle, error) {
	var bottle Bottle
	err := r.db.Preload("Attachments").First(&bottle, id).Error
	return &bottle, err
}

func (r BottleRepo) GetByRandom() (*Bottle, error) {
	var bottle Bottle
	err := r.db.Preload("Attachments").Where("owner == ''").Order("RANDOM()").First(&bottle).Error
	return &bottle, err
}

func (r *BottleRepo) Delete(id uint) error {
	_, err := r.GetById(id)
	if err != nil {
		return err
	}
	return r.db.Delete(&Bottle{}, id).Error
}

func (r *BottleRepo) AttachMessage(id uint, content string) error {
	bottle, err := r.GetById(id)
	if err != nil {
		return err
	}
	attachment := Attachment{BottleID: bottle.ID, Content: content}
	return r.db.Create(&attachment).Error
}

func (r *BottleRepo) SetOwner(id uint, owner string) error {
	bottle, err := r.GetById(id)
	if err != nil {
		return err
	}
	bottle.Owner = owner
	return r.db.Save(bottle).Error
}

func NewBottleRepo(db *gorm.DB) *BottleRepo {
	db.AutoMigrate(&Bottle{}, &Attachment{})
	return &BottleRepo{db: db}
}

service.go

package main

import (
	"crypto/hmac"
	"crypto/rand"
	"crypto/sha256"
	"encoding/base64"
	"encoding/json"
)

type BottleService struct {
	repo *BottleRepo
}

func NewBottleService(repo *BottleRepo) *BottleService {
	return &BottleService{repo: repo}
}

func (s *BottleService) Create(bottle *Bottle) error {
	return s.repo.Create(bottle)
}

func (s *BottleService) GetBottle() (*Bottle, error) {
	return s.repo.GetByRandom()
}

func (s *BottleService) GetBottleById(id uint) (*Bottle, error) {
	return s.repo.GetById(id)
}

const hmacKey = "SuPerSeCRetKeY"

type Token struct {
	Body TokenBody `json:"b"`
	Sum  string    `json:"s,omitempty"`
}

type TokenBody struct {
	BottleId uint   `json:"b"`
	OwnerId  string `json:"o"`
}

func (s *BottleService) SignToken(id uint) string {
	mac := hmac.New(sha256.New, []byte(hmacKey))

	buf := make([]byte, 16)
	rand.Read(buf)
	ownerId := base64.URLEncoding.EncodeToString(buf)
	tokenBody := TokenBody{
		BottleId: id,
		OwnerId:  ownerId,
	}
	s.repo.SetOwner(id, ownerId)

	data, _ := json.Marshal(tokenBody)
	mac.Write([]byte(data))
	sum := mac.Sum(nil)
	tokenRaw := Token{
		Body: tokenBody,
		Sum:  base64.StdEncoding.EncodeToString(sum),
	}
	token, _ := json.Marshal(tokenRaw)
	return base64.URLEncoding.EncodeToString(token)
}

func (s *BottleService) DecodeToken(token string) (id uint, ok bool) {
	tokenRaw, err := base64.URLEncoding.DecodeString(token)
	if err != nil {
		return 0, false
	}
	var tokenBody Token
	err = json.Unmarshal(tokenRaw, &tokenBody)
	if err != nil {
		return 0, false
	}
	bottle, err := s.repo.GetById(tokenBody.Body.BottleId)
	if err != nil {
		return 0, false
	}
	if bottle.Owner != tokenBody.Body.OwnerId {
		return 0, false
	}
	mac := hmac.New(sha256.New, []byte(hmacKey))
	data, _ := json.Marshal(tokenBody.Body)
	mac.Write([]byte(data))
	sum := mac.Sum(nil)
	if base64.StdEncoding.EncodeToString(sum) != tokenBody.Sum {
		return 0, false
	}
	return tokenBody.Body.BottleId, true
}

func (s *BottleService) DecodeAndRevokeToken(token string) (uint, bool) {
	id, ok := s.DecodeToken(token)
	err := s.repo.SetOwner(id, "")
	if err != nil {
		return 0, false
	}
	return id, ok
}

func (s *BottleService) AttachMessage(id uint, message string) error {
	return s.repo.AttachMessage(id, message)
}

func (s *BottleService) DeleteBottle(id uint) error {
	return s.repo.Delete(id)
}

handlers.go

package main

import (
	"context"
	"time"

	"github.com/gin-gonic/gin"
)

type BottleHandler struct {
	srv *BottleService
}

type BottleDto struct {
	Token       string   `json:"token"`
	Content     string   `json:"content"`
	Attachments []string `json:"attachments"`
}

type CreateBottleReq struct {
	Content string `json:"content"`
}

type AttachMessageReq struct {
	Content string
}

// @Summary		Create a bottle
// @Description	Create a bottle
// @Tags			bottles
// @Accept			json
// @Produce		json
// @Param			bottle	body	CreateBottleReq	true	"The bottle to Create"
// @Success		201
// @Router			/bottles/ [post]
func (h *BottleHandler) CreateBottle(ctx *gin.Context) {
	var req CreateBottleReq
	if err := ctx.ShouldBindJSON(&req); err != nil {
		ctx.JSON(400, gin.H{"error": err.Error()})
		return
	}
	entity := &Bottle{Content: req.Content}
	if err := h.srv.Create(entity); err != nil {
		ctx.JSON(500, gin.H{"error": err.Error()})
		return
	}
	// will not respond the bottle to the client
	ctx.JSON(201, gin.H{})
}

var timeoutCancelers = make(map[uint]context.CancelFunc)

// @Summary		Get a bottle
// @Description	Get a bottle
// @Tags			bottles
// @Accept			json
// @Produce		json
// @Success		200	{object}	BottleDto
// @Router			/bottles/ [get]
func (h *BottleHandler) GetBottle(ctx *gin.Context) {
	entity, err := h.srv.GetBottle()
	if err != nil {
		ctx.JSON(500, gin.H{"error": err.Error()})
		return
	}

	// set timer to delete the bottle after 30 minutes
	// if user have made any decision, should call the corrosponding cancel Function.
	go func() {
		ctx, cancel := context.WithCancel(context.Background())
		defer cancel()
		timeoutCancelers[entity.ID] = cancel
		timer := time.NewTimer(30 * time.Minute)
		<-timer.C
		// not canceled by user, do the default behavior
		if ctx.Err() == nil {
			delete(timeoutCancelers, entity.ID)
			h.srv.DeleteBottle(entity.ID)
		}
	}()
	dto := &BottleDto{
		Token:       h.srv.SignToken(entity.ID),
		Content:     entity.Content,
		Attachments: make([]string, len(entity.Attachments)),
	}
	for i, attachment := range entity.Attachments {
		dto.Attachments[i] = attachment.Content
	}
	ctx.JSON(200, dto)
}

// @Summary		Get a bottle by a signd token
// @Description	Get a bottle by a signd token
// @Tags			bottles
// @Accept			json
// @Produce		json
// @Param			token	path		string	true	"The token of the bottle"
// @Success		200		{object}	BottleDto
// @Router			/bottles/{token} [get]
func (h *BottleHandler) GetBottleByToken(ctx *gin.Context) {
	token := ctx.Param("token")
	id, ok := h.srv.DecodeToken(token)
	if !ok {
		ctx.JSON(400, gin.H{"error": "invalid token"})
		return
	}
	entity, err := h.srv.GetBottleById(id)
	if err != nil {
		ctx.JSON(500, gin.H{"error": err.Error()})
		return
	}
	dto := &BottleDto{
		Token:       token,
		Content:     entity.Content,
		Attachments: make([]string, len(entity.Attachments)),
	}
	for i, attachment := range entity.Attachments {
		dto.Attachments[i] = attachment.Content
	}
	ctx.JSON(200, dto)
}

// @Summary		Keep the bottle
// @Description	Keep the bottle the user have got by token
// @Tags			bottles
// @Accept			json
// @Produce		json
// @Param			token	path	string	true	"The token of the bottle"
// @Success		200
// @Router			/bottles/{token} [put]
func (h *BottleHandler) KeepBottle(ctx *gin.Context) {
	token := ctx.Param("token")
	id, ok := h.srv.DecodeToken(token)
	if !ok {
		ctx.JSON(400, gin.H{"error": "you cannot do this"})
		return
	}
	if cancel, ok := timeoutCancelers[uint(id)]; ok {
		cancel()
	} else {
		ctx.JSON(400, gin.H{"error": "bad request"})
	}
	return
}

// @Summary		Throw back the bottle
// @Description	Throw back bottle the user have got by token
// @Tags			bottles
// @Accept			json
// @Produce		json
// @Param			token	path	string	true	"The token of the bottle"
// @Success		200
// @Router			/bottles/{token} [post]
func (h *BottleHandler) ThrowBackBottle(ctx *gin.Context) {
	token := ctx.Param("token")

	id, ok := h.srv.DecodeAndRevokeToken(token)
	if !ok {
		ctx.JSON(400, gin.H{"error": "invalid token"})
		return
	}
	if cancel, ok := timeoutCancelers[uint(id)]; ok {
		cancel()
	} else {
		ctx.JSON(400, gin.H{"error": "bad request"})
	}
	return
}

// @Summary		Attach message to the bottle
// @Description	Attach message to the bottle the user have got by token
// @Tags			bottles
// @Accept			json
// @Produce		json
// @Param			token		path	string				true	"The token of the bottle"
// @param			attachment	body	AttachMessageReq	true	"The message to attach"
// @Success		200
// @Router			/bottles/{token}/attachments/ [post]
func (h *BottleHandler) AttachMessage(ctx *gin.Context) {
	token := ctx.Param("token")
	var req AttachMessageReq
	if err := ctx.ShouldBindJSON(&req); err != nil {
		ctx.JSON(400, gin.H{"error": err.Error()})
		return
	}

	id, ok := h.srv.DecodeAndRevokeToken(token)
	if !ok {
		ctx.JSON(400, gin.H{"error": "invalid token"})
		return
	}
	if cancel, ok := timeoutCancelers[uint(id)]; ok {
		cancel()
		h.srv.AttachMessage(uint(id), req.Content)
	} else {
		ctx.JSON(400, gin.H{"error": "bad request"})
	}
	return
}

func NewBottleHandler(srv *BottleService) *BottleHandler {
	return &BottleHandler{
		srv,
	}
}

main.go

package main

import (
	"log"

	_ "git.online.njtech.edu.cn/online/backend_itv_2023_go/docs"
	"github.com/gin-gonic/gin"
	ginSwagger "github.com/swaggo/gin-swagger"
	"gorm.io/driver/sqlite"
	"gorm.io/gorm"

	swaggerFiles "github.com/swaggo/files"
)

//	@title			Drifting Bottle Simulating (MARS Studio 2023 interview of backend)
//	@version		0.1.0
//	@description	This is an implement of the backend of the Drifting Bottle Simulating project, which is a part of the interview of MARS Studio 2023.

//	@contact.name	MARS Studio
//	@contact.url	https://njtechmars.online/
//	@contact.email	[email protected]

//	@license.name	CC BY-NC-SA 4.0
//	@license.url	https://creativecommons.org/licenses/by-nc-sa/4.0/

// @host		localhost:7000
// @BasePath	/api/v1
func main() {
	db, err := gorm.Open(sqlite.Open("drifting_bottle.db"), &gorm.Config{})
	if err != nil {
		log.Fatal(err)
	}
	repo := NewBottleRepo(db)
	service := NewBottleService(repo)
	handler := NewBottleHandler(service)

	r := gin.Default()
	r.GET("/api/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
	v1 := r.Group("/api/v1")
	v1.GET("/bottles/", handler.GetBottle)
	v1.POST("/bottles/", handler.CreateBottle)
	v1.GET("/bottles/:token", handler.GetBottleByToken)
	v1.PUT("/bottles/:token", handler.KeepBottle)
	v1.POST("/bottles/:token", handler.ThrowBackBottle)
	v1.POST("/bottles/:token/attachments/", handler.AttachMessage)

	r.Run(":7000")
}

2. 运行

# 使用 swaggo 生成 openapi 文档
go install github.com/swaggo/swag/cmd/swag@latest
swag init
go run .