提交代码

This commit is contained in:
dandan 2024-10-27 23:43:18 +08:00
parent 40bf977f5b
commit 153ae31ea8
57 changed files with 6873 additions and 4369 deletions

View File

@ -1,12 +1,15 @@
package http
import (
"bbs/app/provider/database_connect"
"github.com/Superdanda/hade/framework"
"github.com/Superdanda/hade/framework/gin"
)
// NewHttpEngine 创建了一个绑定了路由的Web引擎
func NewHttpEngine(container *framework.HadeContainer) (*gin.Engine, error) {
//绑定服务
container.Bind(&database_connect.DatabaseConnectProvider{})
// 设置为Release为的是默认在启动中不输出调试信息
gin.SetMode(gin.ReleaseMode)
// 默认启动一个Web引擎
@ -15,5 +18,9 @@ func NewHttpEngine(container *framework.HadeContainer) (*gin.Engine, error) {
// 业务绑定路由操作
Routes(r)
// 返回绑定路由后的Web引擎
// 对业务模型进行注册,通过注册名获取业务模型类型信息
TypeRegister(container)
return r, nil
}

View File

@ -1,13 +0,0 @@
package response
import (
"fmt"
"github.com/Superdanda/hade/framework/gin"
)
func ResponseHandler() gin.HandlerFunc {
return func(c *gin.Context) {
c.Next()
fmt.Println("response handler")
}
}

View File

@ -1,143 +0,0 @@
package demo
import (
"database/sql"
"github.com/Superdanda/hade/framework/contract"
"github.com/Superdanda/hade/framework/gin"
"github.com/Superdanda/hade/framework/provider/orm"
"time"
)
type DemoApi struct {
service *Service
}
func Register(r *gin.Engine) error {
api := NewDemoApi()
//r.Bind(&demoService.DemoProvider{})
r.GET("/demo/demo", api.Demo)
r.GET("/demo/demo2", api.Demo2)
r.GET("/demo/orm", api.orm)
r.POST("/demo/demo_post", api.DemoPost)
return nil
}
func NewDemoApi() *DemoApi {
service := NewService()
return &DemoApi{service: service}
}
func Demo(c *gin.Context) {
configService := c.MustMake(contract.ConfigKey).(contract.Config)
log := c.MustMake(contract.LogKey).(contract.Log)
password := configService.GetString("database.mysql.password")
log.Info(c, "ceshiceshi", map[string]interface{}{})
c.JSON(200, password+"后端测试222")
}
// Demo godoc
// @Summary 获取所有用户
// @tag.description.markdown demo.md
// @Produce json
// @Tags demo
// @Success 200 array []UserDTO
// @Router /demo/demo [get]
func (api *DemoApi) Demo(c *gin.Context) {
c.JSON(200, "this is demo for dev all")
}
func (api *DemoApi) orm(c *gin.Context) {
logger := c.MustMakeLog()
logger.Info(c, "request start", nil)
// 初始化一个orm.DB
gormService := c.MustMake(contract.ORMKey).(contract.ORMService)
db, err := gormService.GetDB(orm.WithConfigPath("database.default"))
if err != nil {
logger.Error(c, err.Error(), nil)
c.AbortWithError(50001, err)
return
}
db.WithContext(c)
// 将User模型创建到数据库中
err = db.AutoMigrate(&User{})
if err != nil {
c.AbortWithError(500, err)
return
}
logger.Info(c, "migrate ok", nil)
// 插入一条数据
email := "foo@gmail.com"
name := "foo"
age := uint8(25)
birthday := time.Date(2001, 1, 1, 1, 1, 1, 1, time.Local)
user := &User{
Name: name,
Email: &email,
Age: age,
Birthday: &birthday,
MemberNumber: sql.NullString{},
ActivatedAt: sql.NullTime{},
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
err = db.Create(user).Error
logger.Info(c, "insert user", map[string]interface{}{
"id": user.ID,
"err": err,
})
// 更新一条数据
user.Name = "bar"
err = db.Save(user).Error
logger.Info(c, "update user", map[string]interface{}{
"err": err,
"id": user.ID,
})
// 查询一条数据
queryUser := &User{ID: user.ID}
err = db.First(queryUser).Error
logger.Info(c, "query user", map[string]interface{}{
"err": err,
"name": queryUser.Name,
})
// 删除一条数据
//err = db.Delete(queryUser).Error
//logger.Info(c, "delete user", map[string]interface{}{
// "err": err,
// "id": user.ID,
//})
c.JSON(200, "ok")
}
// Demo2 for godoc
// @Summary 获取所有学生
// @Description 获取所有学生,不进行分页
// @Produce json
// @Tags demo
// @Success 200 {array} UserDTO
// @Router /demo/demo2 [get]
func (api *DemoApi) Demo2(c *gin.Context) {
//demoProvider := c.MustMake(demoService.DemoKey).(demoService.IService)
//students := demoProvider.GetAllStudent()
//usersDTO := StudentsToUserDTOs(students)
c.JSON(200, "usersDTO")
}
func (api *DemoApi) DemoPost(c *gin.Context) {
type Foo struct {
Name string
}
foo := &Foo{}
err := c.BindJSON(&foo)
if err != nil {
c.AbortWithError(500, err)
}
c.JSON(200, nil)
}

View File

@ -1,6 +0,0 @@
package demo
type UserDTO struct {
ID int `json:"id"`
Name string `json:"name"`
}

View File

@ -1 +0,0 @@
package demo

View File

@ -1,25 +0,0 @@
package demo
import (
"database/sql"
"time"
)
type UserModel struct {
UserId int
Name string
Age int
}
// User is gorm model
type User struct {
ID uint
Name string
Email *string
Age uint8
Birthday *time.Time
MemberNumber sql.NullString
ActivatedAt sql.NullTime
CreatedAt time.Time
UpdatedAt time.Time
}

View File

@ -1 +0,0 @@
package demo

View File

@ -1,19 +0,0 @@
package demo
type Service struct {
//repository *Repository
}
func NewService() *Service {
//repository := NewRepository()
return &Service{
//repository: repository,
}
}
//}
//
//func (s *Service) GetUsers() []UserModel {
// ids := s.repository.GetUserIds()
// return s.repository.GetUserByIds(ids)
//}

33
app/http/module/qa/api.go Normal file
View File

@ -0,0 +1,33 @@
package qa
import (
"bbs/app/provider/qa"
"github.com/Superdanda/hade/framework/gin"
)
type QaApi struct{}
// 注册路由
func RegisterRoutes(r *gin.Engine) error {
api := QaApi{}
if !r.IsBind(qa.QaKey) {
r.Bind(&qa.QaProvider{})
}
Group := r.Group("/")
{
questionGroup := Group.Group("/question")
{
questionGroup.POST("/create", api.QuestionCreate)
questionGroup.POST("/list", api.QuestionList)
questionGroup.POST("/detail", api.QuestionDetail)
questionGroup.POST("/delete", api.QuestionDelete)
questionGroup.POST("/edit", api.QuestionEdit)
}
answerGroup := Group.Group("/answer")
{
answerGroup.POST("/create", api.AnswerCreate)
answerGroup.POST("/delete", api.AnswerDelete)
}
}
return nil
}

View File

@ -0,0 +1,51 @@
package qa
import (
"bbs/app/http/middleware/auth"
"bbs/app/http/result"
provider "bbs/app/provider/qa"
"github.com/Superdanda/hade/framework/gin"
"net/http"
)
type answerCreateParam struct {
QuestionID int64 `json:"questionId" binding:"required"`
Context string `json:"context" binding:"required"`
}
// AnswerCreate 代表创建回答
// @Summary 创建回答
// @Description 创建回答
// @Accept json
// @Produce json
// @Tags qa
// @Param answerCreateParam body answerCreateParam true "创建回答参数"
// @Success 200 string Msg "操作成功"
// @Router /answer/create [post]
func (api *QaApi) AnswerCreate(c *gin.Context) {
qaService := c.MustMake(provider.QaKey).(provider.Service)
param := &answerCreateParam{}
if err := c.ShouldBind(param); err != nil {
c.ISetStatus(http.StatusBadRequest).IJson(result.Fail("参数错误"))
return
}
user := auth.GetAuthUser(c)
if user == nil {
c.ISetStatus(http.StatusUnauthorized).IJson(result.Fail("登录后再操作"))
return
}
newAnswer := provider.NewAnswer(param.QuestionID, param.Context, user.ID)
err := qaService.PostAnswer(c, newAnswer)
if err != nil {
c.ISetStatus(http.StatusInternalServerError).IJson(result.Fail("网络开小差,稍后再试"))
return
}
c.ISetOkStatus().IJson(result.SuccessWithOKMessage())
}

View File

@ -0,0 +1,50 @@
package qa
import (
"bbs/app/http/middleware/auth"
"bbs/app/http/result"
provider "bbs/app/provider/qa"
"github.com/Superdanda/hade/framework/gin"
"net/http"
)
type answerDeleteParam struct {
AnswerID int64 `json:"answerId" binding:"required"`
}
// AnswerDelete 代表删除回答
// @Summary 创建回答
// @Description 创建回答
// @Accept json
// @Produce json
// @Tags qa
// @Param answerDeleteParam body answerDeleteParam true "删除id"
// @Success 200 string Msg "操作成功"
// @Router /answer/delete [post]
func (api *QaApi) AnswerDelete(c *gin.Context) {
qaService := c.MustMake(provider.QaKey).(provider.Service)
param := &answerDeleteParam{}
if err := c.ShouldBind(param); err != nil {
c.ISetStatus(http.StatusBadRequest).IJson(result.Fail("参数错误"))
return
}
user := auth.GetAuthUser(c)
answerId := param.AnswerID
answer, err := qaService.GetAnswer(c, answerId)
if err != nil {
c.ISetStatus(http.StatusInternalServerError).IJson(result.Fail("数据不存在"))
return
}
if answer.AuthorID != user.ID {
c.ISetStatus(http.StatusInternalServerError).IJson(result.Fail("没有权限做此操作"))
return
}
if err := qaService.DeleteAnswer(c, answerId); err != nil {
c.ISetStatus(http.StatusInternalServerError).IJson(result.Fail(err.Error()))
return
}
c.ISetOkStatus().IJson(result.SuccessWithOKMessage())
}

View File

@ -0,0 +1,48 @@
package qa
import (
"bbs/app/http/middleware/auth"
"bbs/app/http/result"
provider "bbs/app/provider/qa"
"github.com/Superdanda/hade/framework/gin"
"net/http"
)
type questionCreateParam struct {
Title string `json:"title" binding:"required"`
Content string `json:"content" binding:"required"`
}
// QuestionCreate 代表创建问题
// @Summary 创建问题
// @Description 创建问题
// @Accept json
// @Produce json
// @Tags qa
// @Param questionCreateParam body questionCreateParam true "创建问题参数"
// @Success 200 string Msg "操作成功"
// @Router /question/create [post]
func (api *QaApi) QuestionCreate(c *gin.Context) {
qaService := c.MustMake(provider.QaKey).(provider.Service)
param := &questionCreateParam{}
if err := c.ShouldBind(param); err != nil {
c.ISetStatus(http.StatusBadRequest).IJson(result.Fail("参数错误"))
return
}
user := auth.GetAuthUser(c)
if user == nil {
c.ISetStatus(http.StatusInternalServerError).IJson(result.Fail("没有权限做此操作"))
return
}
question := provider.NewQuestion(param.Title, param.Content, user.ID)
if err := qaService.PostQuestion(c, question); err != nil {
c.ISetStatus(http.StatusInternalServerError).IJson(result.Fail(err.Error()))
return
}
c.ISetOkStatus().IJson(result.SuccessWithOKMessage())
}

View File

@ -0,0 +1,50 @@
package qa
import (
"bbs/app/http/middleware/auth"
"bbs/app/http/result"
provider "bbs/app/provider/qa"
"github.com/Superdanda/hade/framework/gin"
"net/http"
)
type questionDeleteParam struct {
QuestionID int64 `json:"questionId" binding:"required"`
}
// QuestionDelete 删除问题
// @Summary 删除问题
// @Description 删除问题,同时删除问题中的所有答案
// @Accept json
// @Produce json
// @Tags qa
// @Param questionDeleteParam body questionDeleteParam true "删除id"
// @Success 200 string Msg "操作成功"
// @Router /question/delete [post]
func (api *QaApi) QuestionDelete(c *gin.Context) {
qaService := c.MustMake(provider.QaKey).(provider.Service)
param := &questionDeleteParam{}
if err := c.ShouldBind(param); err != nil {
c.ISetStatus(http.StatusBadRequest).IJson(result.Fail("参数错误"))
return
}
question, err := qaService.GetQuestion(c, param.QuestionID)
if err != nil {
c.ISetStatus(http.StatusInternalServerError).IJson(result.Fail("数据不存在"))
return
}
user := auth.GetAuthUser(c)
if user.ID != question.AuthorID {
c.ISetStatus(http.StatusInternalServerError).IJson(result.Fail("没有权限做此操作"))
return
}
if err := qaService.DeleteQuestion(c, question.ID); err != nil {
c.ISetStatus(http.StatusInternalServerError).IJson(result.Fail(err.Error()))
return
}
c.ISetOkStatus().IJson(result.SuccessWithOKMessage())
}

View File

@ -0,0 +1,59 @@
package qa
import (
"bbs/app/http/result"
provider "bbs/app/provider/qa"
"github.com/Superdanda/hade/framework/gin"
"net/http"
)
type questionDetailParam struct {
QuestionID int64 `json:"questionId" binding:"required"`
}
// QuestionDetail 获取问题详情
// @Summary 获取问题详细
// @Description 获取问题详情,包括问题的所有回答
// @Accept json
// @Produce json
// @Tags qa
// @Param questionDetailParam body questionDetailParam true "问题id"
// @Success 200 QuestionDTO question "问题详情,带回答和作者"
// @Router /question/detail [post]
func (api *QaApi) QuestionDetail(c *gin.Context) {
qaService := c.MustMake(provider.QaKey).(provider.Service)
param := &questionDetailParam{}
if err := c.ShouldBind(param); err != nil {
c.ISetStatus(http.StatusBadRequest).IJson(result.Fail("参数错误"))
return
}
// 获取问题详情
question, err := qaService.GetQuestion(c, param.QuestionID)
if err != nil {
c.ISetStatus(http.StatusInternalServerError).IJson(result.Fail("数据不存在"))
return
}
// 加载问题作者
if err := qaService.QuestionLoadAuthor(c, question); err != nil {
c.ISetStatus(http.StatusInternalServerError).IJson(result.Fail("网络开小差"))
return
}
// 加载所有答案
if err := qaService.QuestionLoadAnswers(c, question); err != nil {
c.ISetStatus(http.StatusInternalServerError).IJson(result.Fail("网络开小差"))
return
}
// 加载答案的所有作者
if err := qaService.AnswersLoadAuthor(c, &question.Answers); err != nil {
c.ISetStatus(http.StatusInternalServerError).IJson(result.Fail("网络开小差"))
return
}
// 输出转换
questionDTO := ConvertQuestionToDTO(question, nil)
c.ISetOkStatus().IJson(questionDTO)
}

View File

@ -0,0 +1,58 @@
package qa
import (
"bbs/app/http/middleware/auth"
"bbs/app/http/result"
provider "bbs/app/provider/qa"
"github.com/Superdanda/hade/framework/gin"
"net/http"
)
type questionEditParam struct {
ID int64 `json:"id" binding:"required"`
Title string `json:"title" binding:"required"`
Content string `json:"content" binding:"required"`
}
// QuestionEdit 编辑问题
// @Summary 编辑问题
// @Description 编辑问题
// @Accept json
// @Produce json
// @Tags qa
// @Param questionEditParam body questionEditParam true "编辑问题参数"
// @Success 200 string Msg "操作成功"
// @Router /question/edit [post]
func (api *QaApi) QuestionEdit(c *gin.Context) {
qaService := c.MustMake(provider.QaKey).(provider.Service)
param := &questionEditParam{}
if err := c.ShouldBind(param); err != nil {
c.ISetStatus(http.StatusBadRequest).IJson(result.Fail("参数错误"))
return
}
questionOld, err := qaService.GetQuestion(c, param.ID)
if err != nil || questionOld == nil {
c.ISetStatus(http.StatusInternalServerError).IJson(result.Fail("操作的问题不存在"))
return
}
user := auth.GetAuthUser(c)
if user == nil || user.ID != questionOld.AuthorID {
c.ISetStatus(http.StatusUnauthorized).IJson(result.Fail("无权限操作"))
return
}
question := &provider.Question{
ID: param.ID,
Title: param.Title,
Context: param.Content,
}
if err := qaService.UpdateQuestion(c, question); err != nil {
c.ISetStatus(http.StatusInternalServerError).IJson(result.Fail(err.Error()))
return
}
c.ISetOkStatus().IJson(result.SuccessWithOKMessage())
}

View File

@ -0,0 +1,61 @@
package qa
import (
"bbs/app/http/result"
provider "bbs/app/provider/qa"
"github.com/Superdanda/hade/framework/gin"
"net/http"
)
type questionListParam struct {
Start int `json:"start" binding:"required"`
Size int `json:"size" binding:"required"`
}
// QuestionList 获取问题列表
// @Summary 获取问题列表
// @Description 获取问题列表,包含作者信息,不包含回答
// @Accept json
// @Produce json
// @Tags qa
// @Param questionListParam body questionListParam true "分页查询的参数"
// @Success 200 {array} qa.QuestionDTO "问题列表"
// @Router /question/list [post]
func (api *QaApi) QuestionList(c *gin.Context) {
qaService := c.MustMake(provider.QaKey).(provider.Service)
param := &questionListParam{}
if err := c.ShouldBind(param); err != nil {
c.ISetStatus(http.StatusBadRequest).IJson(result.Fail("参数错误"))
return
}
logger := c.MustMakeLog()
pager := provider.Pager{
Start: param.Start,
Size: param.Size,
}
logger.Debug(c, "get param", map[string]interface{}{
"pager": pager,
})
questions, err := qaService.GetQuestions(c, &pager)
if err != nil {
c.ISetStatus(http.StatusInternalServerError).IJson(result.Fail(err.Error()))
return
}
if len(questions) == 0 {
c.ISetOkStatus().IJson([]*QuestionDTO{})
return
}
if err := qaService.QuestionsLoadAuthor(c, &questions); err != nil {
c.ISetStatus(http.StatusInternalServerError).IJson(result.Fail(err.Error()))
return
}
questionsDTO := ConvertQuestionsToDTO(questions)
c.ISetOkStatus().IJson(questionsDTO)
}

29
app/http/module/qa/dto.go Normal file
View File

@ -0,0 +1,29 @@
package qa
import (
"bbs/app/http/module/user"
"time"
)
// QuestionDTO 问题列表返回结构
type QuestionDTO struct {
ID int64 `json:"id,omitempty"`
Title string `json:"title,omitempty"`
Context string `json:"context,omitempty"` // 在列表页只显示前200个字符
AnswerNum int `json:"answer_num"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Author *user.UserDTO `json:"author,omitempty"` // 作者
Answers []*AnswerDTO `json:"answers,omitempty"` // 回答
}
// AnswerDTO 回答返回结构
type AnswerDTO struct {
ID int64 `json:"id,omitempty"`
Content string `json:"content,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Author *user.UserDTO `json:"author,omitempty"` // 作者
}

View File

@ -0,0 +1,98 @@
package qa
import (
"bbs/app/http/module/user"
"bbs/app/provider/qa"
"github.com/PuerkitoBio/goquery"
"strings"
)
func ConvertAnswerToDTO(answer *qa.Answer) *AnswerDTO {
if answer == nil {
return nil
}
author := user.ConvertUserToDTO(answer.Author)
if author == nil {
author = &user.UserDTO{
ID: answer.AuthorID,
}
}
return &AnswerDTO{
ID: answer.ID,
Content: answer.Context,
CreatedAt: answer.CreatedAt,
UpdatedAt: answer.UpdatedAt,
Author: author,
}
}
// ConvertAnswersToDTO 将answers转化为带有tree结构的AnswerDTO
func ConvertAnswersToDTO(answers []*qa.Answer) []*AnswerDTO {
if answers == nil {
return nil
}
ret := make([]*AnswerDTO, 0, len(answers))
for _, answer := range answers {
ret = append(ret, ConvertAnswerToDTO(answer))
}
return ret
}
// ConvertQuestionToDTO 将question转换为DTO
func ConvertQuestionToDTO(question *qa.Question, flags map[string]string) *QuestionDTO {
if question == nil {
return nil
}
author := user.ConvertUserToDTO(question.Author)
if author == nil {
author = &user.UserDTO{
ID: question.AuthorID,
}
}
context := question.Context
if flags != nil {
if isShortContext, ok := flags["is_short_context"]; ok && isShortContext == "true" {
context = getShortContext(context)
}
}
return &QuestionDTO{
ID: question.ID,
Title: question.Title,
Context: context,
CreatedAt: question.CreatedAt,
UpdatedAt: question.UpdatedAt,
Author: author,
Answers: ConvertAnswersToDTO(question.Answers),
AnswerNum: question.AnswerNum,
}
}
func getShortContext(context string) string {
p := strings.NewReader(context)
doc, _ := goquery.NewDocumentFromReader(p)
doc.Find("script").Each(func(i int, el *goquery.Selection) {
el.Remove()
})
text := doc.Text()
if len(text) > 20 {
text = text[:20] + "..."
}
return text
}
// ConvertQuestionsToDTO 将questions转换为DTO
func ConvertQuestionsToDTO(questions []*qa.Question) []*QuestionDTO {
if questions == nil {
return nil
}
ret := make([]*QuestionDTO, 0, len(questions))
for _, question := range questions {
ret = append(ret, ConvertQuestionToDTO(question, map[string]string{"is_short_context": "true"}))
}
return ret
}

View File

@ -2,7 +2,6 @@ package user
import (
"bbs/app/http/middleware/auth"
"bbs/app/http/middleware/response"
"bbs/app/provider/user"
"github.com/Superdanda/hade/framework/gin"
)
@ -18,7 +17,6 @@ func RegisterRoutes(r *gin.Engine) error {
}
userGroup := r.Group("/user")
userGroup.Use(response.ResponseHandler())
{
// 登录
userGroup.POST("/login", api.Login)

View File

@ -1,6 +1,7 @@
package user
import (
"bbs/app/http/result"
"bbs/app/provider/user"
"github.com/Superdanda/hade/framework/gin"
"net/http"
@ -23,7 +24,7 @@ func (api *UserApi) Login(c *gin.Context) {
userService := c.MustMake(user.UserKey).(user.Service)
param := &loginParam{}
if err := c.ShouldBind(param); err != nil {
c.ISetStatus(http.StatusBadRequest).IText("参数错误")
c.ISetStatus(http.StatusBadRequest).IJson(result.Fail("参数错误"))
return
}
@ -35,11 +36,11 @@ func (api *UserApi) Login(c *gin.Context) {
userWithToken, err := userService.Login(c, model)
if err != nil {
c.ISetStatus(http.StatusInternalServerError).IText(err.Error())
c.ISetStatus(http.StatusInternalServerError).IJson(result.Fail(err.Error()))
return
}
// 输出
c.ISetOkStatus().IText(userWithToken.Token)
c.ISetOkStatus().IJson(result.Success(userWithToken.Token))
return
}

View File

@ -2,6 +2,7 @@ package user
import (
"bbs/app/http/middleware/auth"
"bbs/app/http/result"
"bbs/app/provider/user"
"github.com/Superdanda/hade/framework/gin"
)
@ -17,13 +18,13 @@ import (
func (api *UserApi) Logout(c *gin.Context) {
authUser := auth.GetAuthUser(c)
if authUser == nil {
c.ISetStatus(500).IText("用户未登录")
c.ISetStatus(500).IJson(result.Fail("用户未登录"))
return
}
userService := c.MustMake(user.UserKey).(user.Service)
if err := userService.Logout(c, authUser); err != nil {
c.ISetStatus(500).IText(err.Error())
c.ISetStatus(500).IJson(result.Fail(err.Error()))
return
}
//c.ISetOkStatus().IText("用户登出成功")

View File

@ -1,6 +1,7 @@
package user
import (
"bbs/app/http/result"
"bbs/app/provider/user"
"fmt"
"github.com/Superdanda/hade/framework/contract"
@ -30,7 +31,7 @@ func (api *UserApi) Register(c *gin.Context) {
param := &registerParam{}
if err := c.ShouldBind(param); err != nil {
c.ISetStatus(400).IText("参数错误 ")
c.ISetStatus(400).IJson(result.Fail("参数错误"))
return
}
@ -47,20 +48,20 @@ func (api *UserApi) Register(c *gin.Context) {
logger.Error(c, err.Error(), map[string]interface{}{
"stack": fmt.Sprintf("%+v", err),
})
c.ISetStatus(500).IText(err.Error())
c.ISetStatus(500).IJson(result.Fail(err.Error()))
return
}
if userWithToken == nil {
c.ISetStatus(500).IText("注册失败")
c.ISetStatus(500).IJson(result.Fail("注册失败"))
return
}
if err := userService.SendRegisterMail(c, userWithToken); err != nil {
c.ISetStatus(500).IText("发送电子邮件失败")
c.ISetStatus(500).IJson(result.Fail("发送电子邮件失败"))
return
}
c.ISetOkStatus().IText("注册成功,请前往邮箱查看邮件")
c.ISetOkStatus().IJson(result.SuccessWithMessage("注册成功,请前往邮箱查看邮件"))
return
}

View File

@ -1,6 +1,7 @@
package user
import (
"bbs/app/http/result"
"bbs/app/provider/user"
"github.com/Superdanda/hade/framework/gin"
)
@ -19,21 +20,21 @@ func (api *UserApi) Verify(c *gin.Context) {
userService := c.MustMake(user.UserKey).(user.Service)
token := c.Query("token")
if token == "" {
c.ISetStatus(400).IText("参数错误")
c.ISetStatus(400).IJson(result.Fail("参数错误"))
return
}
verified, err := userService.VerifyRegister(c, token)
if err != nil {
c.ISetStatus(500).IText(err.Error())
c.ISetStatus(500).IJson(result.Fail(err.Error()))
return
}
if !verified {
c.ISetStatus(500).IText("验证错误")
c.ISetStatus(500).IJson(result.Fail("验证错误"))
return
}
// 输出
c.IRedirect("/#/login").IText("注册成功,请进入登录页面")
c.IRedirect("/#/login").IJson(result.SuccessWithMessage("注册成功,请进入登录页面"))
}

15
app/http/register.go Normal file
View File

@ -0,0 +1,15 @@
package http
import (
"bbs/app/provider/qa"
"bbs/app/provider/user"
"github.com/Superdanda/hade/framework"
"github.com/Superdanda/hade/framework/contract"
)
func TypeRegister(container framework.Container) {
typeRegister := container.MustMake(contract.TypeRegisterKey).(contract.TypeRegisterService)
qa.RegisterType(typeRegister)
user.RegisterType(typeRegister)
}

46
app/http/result/dto.go Normal file
View File

@ -0,0 +1,46 @@
package result
type Result struct {
Code int `json:"code"`
Success bool `json:"success"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
}
// Success 方法,封装成功响应的结构体
func Success(data interface{}) Result {
return Result{
Code: 1,
Success: true,
Message: "Success",
Data: data,
}
}
// SuccessWithMessage 方法,封装成功响应的结构体
func SuccessWithMessage(message string) Result {
return Result{
Code: 1,
Success: true,
Message: message,
}
}
// SuccessWithOKMessage 方法,封装成功响应的结构体, message 操作成功
func SuccessWithOKMessage() Result {
return Result{
Code: 1,
Success: true,
Message: "操作成功",
}
}
// Fail 方法,封装失败响应的结构体
func Fail(message string) Result {
return Result{
Code: 0,
Success: false,
Message: message,
Data: nil,
}
}

View File

@ -1,7 +1,7 @@
package http
import (
"bbs/app/http/module/demo"
"bbs/app/http/module/qa"
"bbs/app/http/module/user"
"github.com/Superdanda/hade/framework/contract"
"github.com/Superdanda/hade/framework/gin"
@ -22,9 +22,8 @@ func Routes(core *gin.Engine) {
// /路径先去./dist目录下查找文件是否存在找到使用文件服务提供服务
core.Use(static.Serve("/", static.LocalFile("./dist", false)))
err := demo.Register(core)
err = user.RegisterRoutes(core)
err := user.RegisterRoutes(core)
err = qa.RegisterRoutes(core)
if err != nil {
return

View File

@ -23,48 +23,241 @@ const docTemplate = `{
"host": "{{.Host}}",
"basePath": "{{.BasePath}}",
"paths": {
"/demo/demo": {
"get": {
"/answer/create": {
"post": {
"description": "创建回答",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"demo"
"qa"
],
"summary": "创建回答",
"parameters": [
{
"description": "创建回答参数",
"name": "answerCreateParam",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/qa.answerCreateParam"
}
}
],
"summary": "获取所有用户",
"responses": {
"200": {
"description": "OK",
"description": "操作成功",
"schema": {
"type": "array",
"items": {
"type": "array",
"items": {
"$ref": "#/definitions/demo.UserDTO"
}
}
"type": "string"
}
}
}
}
},
"/demo/demo2": {
"get": {
"description": "获取所有学生,不进行分页",
"/answer/delete": {
"post": {
"description": "创建回答",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"demo"
"qa"
],
"summary": "创建回答",
"parameters": [
{
"description": "删除id",
"name": "answerDeleteParam",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/qa.answerDeleteParam"
}
}
],
"summary": "获取所有学生",
"responses": {
"200": {
"description": "OK",
"description": "操作成功",
"schema": {
"type": "string"
}
}
}
}
},
"/question/create": {
"post": {
"description": "创建问题",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"qa"
],
"summary": "创建问题",
"parameters": [
{
"description": "创建问题参数",
"name": "questionCreateParam",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/qa.questionCreateParam"
}
}
],
"responses": {
"200": {
"description": "操作成功",
"schema": {
"type": "string"
}
}
}
}
},
"/question/delete": {
"post": {
"description": "删除问题,同时删除问题中的所有答案",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"qa"
],
"summary": "删除问题",
"parameters": [
{
"description": "删除id",
"name": "questionDeleteParam",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/qa.questionDeleteParam"
}
}
],
"responses": {
"200": {
"description": "操作成功",
"schema": {
"type": "string"
}
}
}
}
},
"/question/detail": {
"post": {
"description": "获取问题详情,包括问题的所有回答",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"qa"
],
"summary": "获取问题详细",
"parameters": [
{
"description": "问题id",
"name": "questionDetailParam",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/qa.questionDetailParam"
}
}
],
"responses": {
"200": {
"description": "问题详情,带回答和作者",
"schema": {
"type": "QuestionDTO"
}
}
}
}
},
"/question/edit": {
"post": {
"description": "编辑问题",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"qa"
],
"summary": "编辑问题",
"parameters": [
{
"description": "编辑问题参数",
"name": "questionEditParam",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/qa.questionEditParam"
}
}
],
"responses": {
"200": {
"description": "操作成功",
"schema": {
"type": "string"
}
}
}
}
},
"/question/list": {
"post": {
"description": "获取问题列表,包含作者信息,不包含回答",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"qa"
],
"summary": "获取问题列表",
"parameters": [
{
"description": "分页查询的参数",
"name": "questionListParam",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/qa.questionListParam"
}
}
],
"responses": {
"200": {
"description": "问题列表",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/demo.UserDTO"
"$ref": "#/definitions/qa.QuestionDTO"
}
}
}
@ -195,13 +388,177 @@ const docTemplate = `{
}
},
"definitions": {
"demo.UserDTO": {
"qa.AnswerDTO": {
"type": "object",
"properties": {
"author": {
"description": "作者",
"allOf": [
{
"$ref": "#/definitions/user.UserDTO"
}
]
},
"content": {
"type": "string"
},
"created_at": {
"type": "string"
},
"id": {
"type": "integer"
},
"name": {
"updated_at": {
"type": "string"
}
}
},
"qa.QuestionDTO": {
"type": "object",
"properties": {
"answer_num": {
"type": "integer"
},
"answers": {
"description": "回答",
"type": "array",
"items": {
"$ref": "#/definitions/qa.AnswerDTO"
}
},
"author": {
"description": "作者",
"allOf": [
{
"$ref": "#/definitions/user.UserDTO"
}
]
},
"context": {
"description": "在列表页只显示前200个字符",
"type": "string"
},
"created_at": {
"type": "string"
},
"id": {
"type": "integer"
},
"title": {
"type": "string"
},
"updated_at": {
"type": "string"
}
}
},
"qa.answerCreateParam": {
"type": "object",
"required": [
"context",
"questionId"
],
"properties": {
"context": {
"type": "string"
},
"questionId": {
"type": "integer"
}
}
},
"qa.answerDeleteParam": {
"type": "object",
"required": [
"answerId"
],
"properties": {
"answerId": {
"type": "integer"
}
}
},
"qa.questionCreateParam": {
"type": "object",
"required": [
"content",
"title"
],
"properties": {
"content": {
"type": "string"
},
"title": {
"type": "string"
}
}
},
"qa.questionDeleteParam": {
"type": "object",
"required": [
"questionId"
],
"properties": {
"questionId": {
"type": "integer"
}
}
},
"qa.questionDetailParam": {
"type": "object",
"required": [
"questionId"
],
"properties": {
"questionId": {
"type": "integer"
}
}
},
"qa.questionEditParam": {
"type": "object",
"required": [
"content",
"id",
"title"
],
"properties": {
"content": {
"type": "string"
},
"id": {
"type": "integer"
},
"title": {
"type": "string"
}
}
},
"qa.questionListParam": {
"type": "object",
"required": [
"size",
"start"
],
"properties": {
"size": {
"type": "integer"
},
"start": {
"type": "integer"
}
}
},
"user.UserDTO": {
"type": "object",
"properties": {
"created_at": {
"type": "string"
},
"id": {
"type": "integer"
},
"user_name": {
"type": "string"
}
}

View File

@ -16,48 +16,241 @@
},
"basePath": "/",
"paths": {
"/demo/demo": {
"get": {
"/answer/create": {
"post": {
"description": "创建回答",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"demo"
"qa"
],
"summary": "创建回答",
"parameters": [
{
"description": "创建回答参数",
"name": "answerCreateParam",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/qa.answerCreateParam"
}
}
],
"summary": "获取所有用户",
"responses": {
"200": {
"description": "OK",
"description": "操作成功",
"schema": {
"type": "array",
"items": {
"type": "array",
"items": {
"$ref": "#/definitions/demo.UserDTO"
}
}
"type": "string"
}
}
}
}
},
"/demo/demo2": {
"get": {
"description": "获取所有学生,不进行分页",
"/answer/delete": {
"post": {
"description": "创建回答",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"demo"
"qa"
],
"summary": "创建回答",
"parameters": [
{
"description": "删除id",
"name": "answerDeleteParam",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/qa.answerDeleteParam"
}
}
],
"summary": "获取所有学生",
"responses": {
"200": {
"description": "OK",
"description": "操作成功",
"schema": {
"type": "string"
}
}
}
}
},
"/question/create": {
"post": {
"description": "创建问题",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"qa"
],
"summary": "创建问题",
"parameters": [
{
"description": "创建问题参数",
"name": "questionCreateParam",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/qa.questionCreateParam"
}
}
],
"responses": {
"200": {
"description": "操作成功",
"schema": {
"type": "string"
}
}
}
}
},
"/question/delete": {
"post": {
"description": "删除问题,同时删除问题中的所有答案",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"qa"
],
"summary": "删除问题",
"parameters": [
{
"description": "删除id",
"name": "questionDeleteParam",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/qa.questionDeleteParam"
}
}
],
"responses": {
"200": {
"description": "操作成功",
"schema": {
"type": "string"
}
}
}
}
},
"/question/detail": {
"post": {
"description": "获取问题详情,包括问题的所有回答",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"qa"
],
"summary": "获取问题详细",
"parameters": [
{
"description": "问题id",
"name": "questionDetailParam",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/qa.questionDetailParam"
}
}
],
"responses": {
"200": {
"description": "问题详情,带回答和作者",
"schema": {
"type": "QuestionDTO"
}
}
}
}
},
"/question/edit": {
"post": {
"description": "编辑问题",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"qa"
],
"summary": "编辑问题",
"parameters": [
{
"description": "编辑问题参数",
"name": "questionEditParam",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/qa.questionEditParam"
}
}
],
"responses": {
"200": {
"description": "操作成功",
"schema": {
"type": "string"
}
}
}
}
},
"/question/list": {
"post": {
"description": "获取问题列表,包含作者信息,不包含回答",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"qa"
],
"summary": "获取问题列表",
"parameters": [
{
"description": "分页查询的参数",
"name": "questionListParam",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/qa.questionListParam"
}
}
],
"responses": {
"200": {
"description": "问题列表",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/demo.UserDTO"
"$ref": "#/definitions/qa.QuestionDTO"
}
}
}
@ -188,13 +381,177 @@
}
},
"definitions": {
"demo.UserDTO": {
"qa.AnswerDTO": {
"type": "object",
"properties": {
"author": {
"description": "作者",
"allOf": [
{
"$ref": "#/definitions/user.UserDTO"
}
]
},
"content": {
"type": "string"
},
"created_at": {
"type": "string"
},
"id": {
"type": "integer"
},
"name": {
"updated_at": {
"type": "string"
}
}
},
"qa.QuestionDTO": {
"type": "object",
"properties": {
"answer_num": {
"type": "integer"
},
"answers": {
"description": "回答",
"type": "array",
"items": {
"$ref": "#/definitions/qa.AnswerDTO"
}
},
"author": {
"description": "作者",
"allOf": [
{
"$ref": "#/definitions/user.UserDTO"
}
]
},
"context": {
"description": "在列表页只显示前200个字符",
"type": "string"
},
"created_at": {
"type": "string"
},
"id": {
"type": "integer"
},
"title": {
"type": "string"
},
"updated_at": {
"type": "string"
}
}
},
"qa.answerCreateParam": {
"type": "object",
"required": [
"context",
"questionId"
],
"properties": {
"context": {
"type": "string"
},
"questionId": {
"type": "integer"
}
}
},
"qa.answerDeleteParam": {
"type": "object",
"required": [
"answerId"
],
"properties": {
"answerId": {
"type": "integer"
}
}
},
"qa.questionCreateParam": {
"type": "object",
"required": [
"content",
"title"
],
"properties": {
"content": {
"type": "string"
},
"title": {
"type": "string"
}
}
},
"qa.questionDeleteParam": {
"type": "object",
"required": [
"questionId"
],
"properties": {
"questionId": {
"type": "integer"
}
}
},
"qa.questionDetailParam": {
"type": "object",
"required": [
"questionId"
],
"properties": {
"questionId": {
"type": "integer"
}
}
},
"qa.questionEditParam": {
"type": "object",
"required": [
"content",
"id",
"title"
],
"properties": {
"content": {
"type": "string"
},
"id": {
"type": "integer"
},
"title": {
"type": "string"
}
}
},
"qa.questionListParam": {
"type": "object",
"required": [
"size",
"start"
],
"properties": {
"size": {
"type": "integer"
},
"start": {
"type": "integer"
}
}
},
"user.UserDTO": {
"type": "object",
"properties": {
"created_at": {
"type": "string"
},
"id": {
"type": "integer"
},
"user_name": {
"type": "string"
}
}

View File

@ -1,10 +1,116 @@
basePath: /
definitions:
demo.UserDTO:
qa.AnswerDTO:
properties:
author:
allOf:
- $ref: '#/definitions/user.UserDTO'
description: 作者
content:
type: string
created_at:
type: string
id:
type: integer
name:
updated_at:
type: string
type: object
qa.QuestionDTO:
properties:
answer_num:
type: integer
answers:
description: 回答
items:
$ref: '#/definitions/qa.AnswerDTO'
type: array
author:
allOf:
- $ref: '#/definitions/user.UserDTO'
description: 作者
context:
description: 在列表页只显示前200个字符
type: string
created_at:
type: string
id:
type: integer
title:
type: string
updated_at:
type: string
type: object
qa.answerCreateParam:
properties:
context:
type: string
questionId:
type: integer
required:
- context
- questionId
type: object
qa.answerDeleteParam:
properties:
answerId:
type: integer
required:
- answerId
type: object
qa.questionCreateParam:
properties:
content:
type: string
title:
type: string
required:
- content
- title
type: object
qa.questionDeleteParam:
properties:
questionId:
type: integer
required:
- questionId
type: object
qa.questionDetailParam:
properties:
questionId:
type: integer
required:
- questionId
type: object
qa.questionEditParam:
properties:
content:
type: string
id:
type: integer
title:
type: string
required:
- content
- id
- title
type: object
qa.questionListParam:
properties:
size:
type: integer
start:
type: integer
required:
- size
- start
type: object
user.UserDTO:
properties:
created_at:
type: string
id:
type: integer
user_name:
type: string
type: object
user.loginParam:
@ -45,37 +151,162 @@ info:
title: hade
version: "1.1"
paths:
/demo/demo:
get:
/answer/create:
post:
consumes:
- application/json
description: 创建回答
parameters:
- description: 创建回答参数
in: body
name: answerCreateParam
required: true
schema:
$ref: '#/definitions/qa.answerCreateParam'
produces:
- application/json
responses:
"200":
description: OK
description: 操作成功
schema:
items:
items:
$ref: '#/definitions/demo.UserDTO'
type: array
type: array
summary: 获取所有用户
type: string
summary: 创建回答
tags:
- demo
/demo/demo2:
get:
description: 获取所有学生,不进行分页
- qa
/answer/delete:
post:
consumes:
- application/json
description: 创建回答
parameters:
- description: 删除id
in: body
name: answerDeleteParam
required: true
schema:
$ref: '#/definitions/qa.answerDeleteParam'
produces:
- application/json
responses:
"200":
description: OK
description: 操作成功
schema:
type: string
summary: 创建回答
tags:
- qa
/question/create:
post:
consumes:
- application/json
description: 创建问题
parameters:
- description: 创建问题参数
in: body
name: questionCreateParam
required: true
schema:
$ref: '#/definitions/qa.questionCreateParam'
produces:
- application/json
responses:
"200":
description: 操作成功
schema:
type: string
summary: 创建问题
tags:
- qa
/question/delete:
post:
consumes:
- application/json
description: 删除问题,同时删除问题中的所有答案
parameters:
- description: 删除id
in: body
name: questionDeleteParam
required: true
schema:
$ref: '#/definitions/qa.questionDeleteParam'
produces:
- application/json
responses:
"200":
description: 操作成功
schema:
type: string
summary: 删除问题
tags:
- qa
/question/detail:
post:
consumes:
- application/json
description: 获取问题详情,包括问题的所有回答
parameters:
- description: 问题id
in: body
name: questionDetailParam
required: true
schema:
$ref: '#/definitions/qa.questionDetailParam'
produces:
- application/json
responses:
"200":
description: 问题详情,带回答和作者
schema:
type: QuestionDTO
summary: 获取问题详细
tags:
- qa
/question/edit:
post:
consumes:
- application/json
description: 编辑问题
parameters:
- description: 编辑问题参数
in: body
name: questionEditParam
required: true
schema:
$ref: '#/definitions/qa.questionEditParam'
produces:
- application/json
responses:
"200":
description: 操作成功
schema:
type: string
summary: 编辑问题
tags:
- qa
/question/list:
post:
consumes:
- application/json
description: 获取问题列表,包含作者信息,不包含回答
parameters:
- description: 分页查询的参数
in: body
name: questionListParam
required: true
schema:
$ref: '#/definitions/qa.questionListParam'
produces:
- application/json
responses:
"200":
description: 问题列表
schema:
items:
$ref: '#/definitions/demo.UserDTO'
$ref: '#/definitions/qa.QuestionDTO'
type: array
summary: 获取所有学生
summary: 获取问题列表
tags:
- demo
- qa
/user/login:
post:
consumes:

View File

@ -0,0 +1,10 @@
package database_connect
import "gorm.io/gorm"
const DatabaseConnectKey = "hade:database_connect"
type Service interface {
LocalDatabaseConnect() *gorm.DB
AliDataBaseConnect() *gorm.DB
}

View File

@ -0,0 +1,31 @@
package database_connect
import (
"github.com/Superdanda/hade/framework"
)
type DatabaseConnectProvider struct {
framework.ServiceProvider
c framework.Container
}
func (sp *DatabaseConnectProvider) Name() string {
return DatabaseConnectKey
}
func (sp *DatabaseConnectProvider) Register(c framework.Container) framework.NewInstance {
return NewDatabaseConnectService
}
func (sp *DatabaseConnectProvider) IsDefer() bool {
return false
}
func (sp *DatabaseConnectProvider) Params(c framework.Container) []interface{} {
return []interface{}{c}
}
func (sp *DatabaseConnectProvider) Boot(c framework.Container) error {
return nil
}

View File

@ -0,0 +1,36 @@
package database_connect
import (
"fmt"
"github.com/Superdanda/hade/framework"
"github.com/Superdanda/hade/framework/contract"
"github.com/Superdanda/hade/framework/provider/orm"
"gorm.io/gorm"
)
type DatabaseConnectService struct {
container framework.Container
}
func (d DatabaseConnectService) LocalDatabaseConnect() *gorm.DB {
return getDatabaseConnectByYaml("database.local", d)
}
func (d DatabaseConnectService) AliDataBaseConnect() *gorm.DB {
return getDatabaseConnectByYaml("database.ali", d)
}
func NewDatabaseConnectService(params ...interface{}) (interface{}, error) {
container := params[0].(framework.Container)
return &DatabaseConnectService{container: container}, nil
}
func getDatabaseConnectByYaml(yamlPath string, d DatabaseConnectService) *gorm.DB {
ormService := d.container.MustMake(contract.ORMKey).(contract.ORMService)
db, err := ormService.GetDB(orm.WithConfigPath(yamlPath))
if err != nil {
fmt.Println(yamlPath + "数据库连接失败,请检查配置")
return nil
}
return db
}

View File

@ -1 +0,0 @@
package demo

View File

@ -1,30 +0,0 @@
package demo
import (
"fmt"
"github.com/Superdanda/hade/framework"
)
type DemoServiceProvider struct {
}
func (sp *DemoServiceProvider) Name() string {
return Key
}
func (sp *DemoServiceProvider) Register(c framework.Container) framework.NewInstance {
return NewDemoService
}
func (sp *DemoServiceProvider) IsDefer() bool {
return true
}
func (sp *DemoServiceProvider) Params(c framework.Container) []interface{} {
return []interface{}{c}
}
func (sp *DemoServiceProvider) Boot(c framework.Container) error {
fmt.Println("demo services boot")
return nil
}

View File

@ -1,36 +0,0 @@
package demo
import (
"fmt"
"github.com/Superdanda/hade/framework"
)
// Key Demo 服务的 key
const Key = "hade:demo"
// Service Demo 服务的接口
type Service interface {
GetFoo() Foo
}
// Foo Demo 服务接口定义的一个数据结构
type Foo struct {
Name string
}
// DemoService serviceProvider 实现
type DemoService struct {
Service
c framework.Container
}
// GetFoo 实现接口
func (s *DemoService) GetFoo() Foo {
return Foo{Name: "i am foo"}
}
func NewDemoService(params ...interface{}) (interface{}, error) {
c := params[0].(framework.Container)
fmt.Println("new demo services")
return &DemoService{c: c}, nil
}

108
app/provider/qa/contract.go Normal file
View File

@ -0,0 +1,108 @@
package qa
import (
"bbs/app/provider/user"
"context"
"github.com/Superdanda/hade/framework/contract"
"gorm.io/gorm"
"time"
)
const QaKey = "bbs:qa"
type Service interface {
// GetQuestions 获取问题列表question简化结构
GetQuestions(ctx context.Context, pager *Pager) ([]*Question, error)
// GetQuestion 获取某个问题详情question简化结构
GetQuestion(ctx context.Context, questionID int64) (*Question, error)
// PostQuestion 上传某个问题
// ctx必须带操作人id
PostQuestion(ctx context.Context, question *Question) error
// QuestionLoadAuthor 问题加载Author字段
QuestionLoadAuthor(ctx context.Context, question *Question) error
// QuestionsLoadAuthor 批量加载Author字段
QuestionsLoadAuthor(ctx context.Context, questions *[]*Question) error
// QuestionLoadAnswers 单个问题加载Answers
QuestionLoadAnswers(ctx context.Context, question *Question) error
// QuestionsLoadAnswers 批量问题加载Answers
QuestionsLoadAnswers(ctx context.Context, questions *[]*Question) error
// PostAnswer 上传某个回答
// ctx必须带操作人信息
PostAnswer(ctx context.Context, answer *Answer) error
// GetAnswer 获取回答
GetAnswer(ctx context.Context, answerID int64) (*Answer, error)
// AnswerLoadAuthor 问题加载Author字段
AnswerLoadAuthor(ctx context.Context, question *Answer) error
// AnswersLoadAuthor 批量加载Author字段
AnswersLoadAuthor(ctx context.Context, questions *[]*Answer) error
// DeleteQuestion 删除问题,同时删除对应的回答
// ctx必须带操作人信息
DeleteQuestion(ctx context.Context, questionID int64) error
// DeleteAnswer 删除某个回答
// ctx必须带操作人信息
DeleteAnswer(ctx context.Context, answerID int64) error
// UpdateQuestion 代表更新问题, 只会对比其中的contexttitle两个字段其他字段不会对比
// ctx必须带操作人
UpdateQuestion(ctx context.Context, question *Question) error
}
func RegisterType(typeRegister contract.TypeRegisterService) {
typeRegister.Register("Question", Question{})
typeRegister.Register("Answer", Answer{})
}
// Question 代表问题
type Question struct {
ID int64 `gorm:"column:id;primaryKey"`
Title string `gorm:"column:title;comment:标题"`
Context string `gorm:"column:context;comment:内容"`
AuthorID int64 `gorm:"column:author_id;comment:作者id;not null;default:0"`
AnswerNum int `gorm:"column:answer_num;comment:回答数;not null;default:0"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间"`
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;autoCreateTime;<-:false;comment:更新时间"`
DeletedAt gorm.DeletedAt `gorm:"index"`
Author *user.User `gorm:"foreignKey:AuthorID"`
Answers []*Answer `gorm:"foreignKey:QuestionID"`
}
func NewQuestion(Title string, Context string, AuthorID int64) *Question {
return &Question{
Title: Title,
Context: Context,
AuthorID: AuthorID,
}
}
// Answer 代表一个回答
type Answer struct {
ID int64 `gorm:"column:id;primaryKey"`
QuestionID int64 `gorm:"column:question_id;index;comment:问题id;not null;default 0"`
Context string `gorm:"column:context;comment:内容"`
AuthorID int64 `gorm:"column:author_id;comment:作者id;not null;default:0"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间"`
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;autoCreateTime;<-:false;comment:更新时间"`
DeletedAt gorm.DeletedAt `gorm:"index"`
Author *user.User `gorm:"foreignKey:ID;references:author_id"`
Question *Question `gorm:"foreignKey:QuestionID"`
}
func NewAnswer(QuestionID int64, Context string, AuthorID int64) *Answer {
return &Answer{
QuestionID: QuestionID,
Context: Context,
AuthorID: AuthorID,
}
}
// Pager 代表分页机制
type Pager struct {
Total int64 // 共有多少数据,只有返回值使用
Start int // 起始位置
Size int // 每个页面个数
}

View File

@ -0,0 +1,31 @@
package qa
import (
"github.com/Superdanda/hade/framework"
)
type QaProvider struct {
framework.ServiceProvider
c framework.Container
}
func (sp *QaProvider) Name() string {
return QaKey
}
func (sp *QaProvider) Register(c framework.Container) framework.NewInstance {
return NewQaService
}
func (sp *QaProvider) IsDefer() bool {
return true
}
func (sp *QaProvider) Params(c framework.Container) []interface{} {
return []interface{}{c}
}
func (sp *QaProvider) Boot(c framework.Container) error {
return nil
}

211
app/provider/qa/service.go Normal file
View File

@ -0,0 +1,211 @@
package qa
import (
"bbs/app/provider/database_connect"
"context"
"github.com/Superdanda/hade/framework"
"github.com/Superdanda/hade/framework/contract"
"github.com/jianfengye/collection"
"github.com/pkg/errors"
"gorm.io/gorm"
"time"
)
type QaService struct {
container framework.Container
ormDB *gorm.DB // db
logger contract.Log // log
}
func (q QaService) GetQuestions(ctx context.Context, pager *Pager) ([]*Question, error) {
questions := make([]*Question, 0, pager.Size)
total := int64(0)
if err := q.ormDB.Count(&total).Error; err != nil {
pager.Total = total
}
if err := q.ormDB.WithContext(ctx).Order("created_at desc").Offset(pager.Start).Limit(pager.Size).Find(&questions).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return []*Question{}, nil
}
return nil, err
}
return questions, nil
}
func (q QaService) GetQuestion(ctx context.Context, questionID int64) (*Question, error) {
question := &Question{}
if err := q.ormDB.WithContext(ctx).First(question, questionID).Error; err != nil {
return nil, err
}
return question, nil
}
func (q QaService) PostQuestion(ctx context.Context, question *Question) error {
if err := q.ormDB.WithContext(ctx).Create(question).Error; err != nil {
return err
}
return nil
}
func (q QaService) QuestionLoadAuthor(ctx context.Context, question *Question) error {
if err := q.ormDB.WithContext(ctx).Preload("Author").First(question).Error; err != nil {
return err
}
return nil
}
func (q QaService) QuestionsLoadAuthor(ctx context.Context, questions *[]*Question) error {
if questions == nil {
return nil
}
questionColl := collection.NewObjPointCollection(*questions)
ids, err := questionColl.Pluck("ID").ToInt64s()
if err != nil {
return err
}
if len(ids) == 0 {
return nil
}
if err := q.ormDB.WithContext(ctx).Preload("Author").Order("created_at desc").Find(questions, ids).Error; err != nil {
return err
}
return nil
}
func (q QaService) QuestionLoadAnswers(ctx context.Context, question *Question) error {
if err := q.ormDB.WithContext(ctx).Preload("Answers", func(db *gorm.DB) *gorm.DB {
return db.Order("answers.created_at desc")
}).First(question).Error; err != nil {
return err
}
return nil
}
func (q QaService) QuestionsLoadAnswers(ctx context.Context, questions *[]*Question) error {
if questions == nil {
return nil
}
questionColl := collection.NewObjPointCollection(*questions)
ids, err := questionColl.Pluck("ID").ToInt64s()
if err != nil {
return err
}
if len(ids) == 0 {
return nil
}
if err := q.ormDB.WithContext(ctx).Preload("Answers").Find(questions, ids).Error; err != nil {
return err
}
return nil
}
func (q QaService) PostAnswer(ctx context.Context, answer *Answer) error {
if answer.QuestionID == 0 {
return errors.New("问题不存在")
}
// 必须使用事务
err := q.ormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
question := &Question{ID: answer.QuestionID}
// 获取问题
if err := tx.First(question).Error; err != nil {
return err
}
// 增加回答
if err := tx.Create(answer).Error; err != nil {
return err
}
// 问题回答数量+1
question.AnswerNum = question.AnswerNum + 1
if err := tx.Save(question).Error; err != nil {
return err
}
return nil
})
if err != nil {
return err
}
return nil
}
func (q QaService) GetAnswer(ctx context.Context, answerID int64) (*Answer, error) {
answer := &Answer{ID: answerID}
if err := q.ormDB.WithContext(ctx).First(answer).Error; err != nil {
return nil, err
}
return answer, nil
}
func (q QaService) AnswerLoadAuthor(ctx context.Context, question *Answer) error {
if err := q.ormDB.WithContext(ctx).Preload("Author").First(question).Error; err != nil {
return err
}
return nil
}
func (q QaService) AnswersLoadAuthor(ctx context.Context, answers *[]*Answer) error {
if answers == nil {
return nil
}
answerColl := collection.NewObjPointCollection(*answers)
ids, err := answerColl.Pluck("ID").ToInt64s()
if err != nil {
return err
}
if len(ids) == 0 {
return nil
}
// 使用PreLoad的机制获取Create方法
if err := q.ormDB.WithContext(ctx).Preload("Author").Order("created_at desc").Find(answers, ids).Error; err != nil {
return err
}
return nil
}
func (q QaService) DeleteQuestion(ctx context.Context, questionID int64) error {
question := &Question{ID: questionID}
if err := q.ormDB.WithContext(ctx).Delete(question).Error; err != nil {
return errors.WithStack(err)
}
return nil
}
func (q QaService) DeleteAnswer(ctx context.Context, answerID int64) error {
answer := &Answer{ID: answerID}
if err := q.ormDB.WithContext(ctx).Delete(answer).Error; err != nil {
return errors.WithStack(err)
}
return nil
}
func (q QaService) UpdateQuestion(ctx context.Context, question *Question) error {
questionDB := &Question{ID: question.ID}
if err := q.ormDB.WithContext(ctx).First(questionDB).Error; err != nil {
return errors.WithStack(err)
}
questionDB.UpdatedAt = time.Now()
if question.Title != "" {
questionDB.Title = question.Title
}
if question.Context != "" {
questionDB.Context = question.Context
}
if err := q.ormDB.WithContext(ctx).Save(questionDB).Error; err != nil {
return errors.WithStack(err)
}
return nil
}
func NewQaService(params ...interface{}) (interface{}, error) {
container := params[0].(framework.Container)
ormService := container.MustMake(database_connect.DatabaseConnectKey).(database_connect.Service)
logger := container.MustMake(contract.LogKey).(contract.Log)
return &QaService{container: container, ormDB: ormService.LocalDatabaseConnect(), logger: logger}, nil
}

View File

@ -0,0 +1 @@
package qa

View File

@ -3,6 +3,7 @@ package user
import (
"context"
"encoding/json"
"github.com/Superdanda/hade/framework/contract"
"time"
)
@ -30,6 +31,10 @@ type Service interface {
GetUser(ctx context.Context, userID int64) (*User, error)
}
func RegisterType(typeRegister contract.TypeRegisterService) {
typeRegister.Register("User", User{})
}
// User 代表一个用户,注意这里的用户信息字段在不同接口和参数可能为空
type User struct {
ID int64 `gorm:"column:id;primary_key;auto_increment" json:"id"` // 代表用户id, 只有注册成功之后才有这个id唯一表示一个用户

View File

@ -1,6 +1,7 @@
package user
import (
"bbs/app/provider/database_connect"
"context"
"crypto/rand"
"fmt"
@ -17,6 +18,7 @@ type UserService struct {
container framework.Container
logger contract.Log
config contract.Config
db *gorm.DB
}
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
@ -39,10 +41,8 @@ func genToken(n int) (string, error) {
}
func (u *UserService) Register(ctx context.Context, user *User) (*User, error) {
ormService := u.container.MustMake(contract.ORMKey).(contract.ORMService)
//验证用户是否已经存在
_, err, _ := u.checkUserNameOrEmailIfExist(ormService, user)
_, err, _ := u.checkUserNameOrEmailIfExist(user)
if err != nil {
return nil, err
}
@ -74,7 +74,7 @@ func (u *UserService) SendRegisterMail(ctx context.Context, user *User) error {
password := configer.GetString("app.smtp.password")
from := configer.GetString("app.smtp.from")
domain := configer.GetString("app.domain")
//QZwFMGzyJgetGgaj
// 实例化gomail
d := gomail.NewDialer(host, port, username, password)
@ -110,12 +110,8 @@ func (u *UserService) VerifyRegister(ctx context.Context, token string) (bool, e
if user.Token != token {
return false, nil
}
//验证邮箱,用户名的唯一
ormService := u.container.MustMake(contract.ORMKey).(contract.ORMService)
//验证用户是否已经存在
_, err, _ := u.checkUserNameOrEmailIfExist(ormService, user)
_, err, _ := u.checkUserNameOrEmailIfExist(user)
if err != nil {
return false, err
}
@ -128,38 +124,32 @@ func (u *UserService) VerifyRegister(ctx context.Context, token string) (bool, e
}
user.Password = string(hash)
// 具体在数据库创建用户
db, err := ormService.GetDB()
if err != nil {
return false, err
}
if err := db.Create(user).Error; err != nil {
if err := u.db.Create(user).Error; err != nil {
return false, err
}
return true, nil
}
func (u *UserService) Login(ctx context.Context, user *User) (*User, error) {
ormService := u.container.MustMake(contract.ORMKey).(contract.ORMService)
db, err := ormService.GetDB()
if err != nil {
return nil, err
}
userDB := &User{}
if err := db.Where("username=?", user.UserName).First(userDB).Error; err != nil {
if err := u.db.Where("username=?", user.UserName).First(userDB).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return nil, errors.Wrap(err, "该用户未注册")
return nil, errors.New("该用户未注册")
}
return nil, err
}
if err := bcrypt.CompareHashAndPassword([]byte(userDB.Password), []byte(user.Password)); err != nil {
return nil, err
return nil, errors.New("密码错误")
}
userDB.Password = ""
// 缓存session
cacheService := u.container.MustMake(contract.CacheKey).(contract.CacheService)
token, err := genToken(10)
token, _ := genToken(10)
key := fmt.Sprintf("session:%v", token)
if err := cacheService.SetObj(ctx, key, userDB, 24*time.Hour); err != nil {
return nil, err
@ -196,13 +186,8 @@ func (u *UserService) VerifyLogin(ctx context.Context, token string) (*User, err
}
func (u *UserService) GetUser(ctx context.Context, userID int64) (*User, error) {
ormService := u.container.MustMake(contract.ORMKey).(contract.ORMService)
db, err := ormService.GetDB()
if err != nil {
return nil, err
}
user := &User{ID: userID}
if err := db.WithContext(ctx).First(user).Error; err != nil {
if err := u.db.WithContext(ctx).First(user).Error; err != nil {
return nil, err
}
return user, nil
@ -212,23 +197,17 @@ func NewUserService(params ...interface{}) (interface{}, error) {
container := params[0].(framework.Container)
logger := container.MustMake(contract.LogKey).(contract.Log)
config := container.MustMake(contract.ConfigKey).(contract.Config)
return &UserService{container: container, logger: logger, config: config}, nil
databaseConnectService := container.MustMake(database_connect.DatabaseConnectKey).(database_connect.Service)
db := databaseConnectService.LocalDatabaseConnect()
return &UserService{container: container, logger: logger, config: config, db: db}, nil
}
func (s *UserService) Foo() string {
return ""
}
func (s *UserService) checkUserNameOrEmailIfExist(dbService contract.ORMService, user *User) (bool, error, *User) {
db, err := dbService.GetDB()
func (s *UserService) checkUserNameOrEmailIfExist(user *User) (bool, error, *User) {
userDB := &User{}
if err != nil {
return false, err, nil
}
if !errors.Is(db.Where(&User{Email: user.Email}).First(userDB).Error, gorm.ErrRecordNotFound) {
if !errors.Is(s.db.Where(&User{Email: user.Email}).First(userDB).Error, gorm.ErrRecordNotFound) {
return true, errors.New("邮箱已注册用户,不能重复注册"), userDB
}
if !errors.Is(db.Where(&User{UserName: user.UserName}).First(userDB).Error, gorm.ErrRecordNotFound) {
if !errors.Is(s.db.Where(&User{UserName: user.UserName}).First(userDB).Error, gorm.ErrRecordNotFound) {
return true, errors.New("邮箱已注册用户,不能重复注册"), userDB
}
return false, nil, nil

View File

@ -1,5 +1,5 @@
url: http://127.0.0.1:8066
domain: "http://127.0.0.1:8888"
name: bbs
swagger_open: true
@ -22,5 +22,5 @@ smtp:
host: "smtp.126.com"
port: 25
from: "superdanda@126.com"
username: "superdanda"
password: "A10337191315."
username: "superdanda@126.com"
password: "QZwFMGzyJgetGgaj"

View File

@ -0,0 +1 @@
driver: memory

View File

@ -3,20 +3,36 @@ conn_max_open: 100 # 通用配置,连接池最大连接数
conn_max_lifetime: 1h # 通用配置,连接数最大生命周期
driver: mysql # 连接驱动
dsn: "" # dsn如果设置了dsn, 以下的所有设置都不生效
host: 47.97.21.20 # ip地址
port: 3306 # 端口
database: questionnaire # 数据库
username: root # 用户名
password: "kerowsqw34" # 密码
charset: utf8mb4 # 字符集
collation: utf8mb4_unicode_ci # 字符序
timeout: 10s # 连接超时
read_timeout: 2s # 读超时
write_timeout: 2s # 写超时
parse_time: true # 是否解析时间
protocol: tcp # 传输协议
loc: Local # 时区
local:
driver: mysql # 连接驱动
dsn: "" # dsn如果设置了dsn, 以下的所有设置都不生效
host: 127.0.0.1 # ip地址
port: 3306 # 端口
database: bbs # 数据库
username: root # 用户名
password: "123456" # 密码
charset: utf8mb4 # 字符集
collation: utf8mb4_unicode_ci # 字符序
timeout: 10s # 连接超时
read_timeout: 2s # 读超时
write_timeout: 2s # 写超时
parse_time: true # 是否解析时间
ali:
driver: mysql # 连接驱动
dsn: "" # dsn如果设置了dsn, 以下的所有设置都不生效
host: 47.97.21.20 # ip地址
port: 3306 # 端口
database: bbs # 数据库
username: root # 用户名
password: "kerowsqw34" # 密码
charset: utf8mb4 # 字符集
collation: utf8mb4_unicode_ci # 字符序
timeout: 10s # 连接超时
read_timeout: 2s # 读超时
write_timeout: 2s # 写超时
parse_time: true # 是否解析时间
sync:
filePath: /app/provider

8
go.mod
View File

@ -3,7 +3,7 @@ module bbs
go 1.23.2
require (
github.com/Superdanda/hade v1.0.4
github.com/Superdanda/hade v1.0.6
github.com/swaggo/swag v1.16.4
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
)
@ -17,7 +17,7 @@ require (
github.com/jianfengye/collection v1.4.2 // indirect
github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pkg/errors v0.9.1
github.com/redis/go-redis/v9 v9.7.0 // indirect
github.com/robfig/cron/v3 v3.0.0 // indirect
github.com/sevlyar/go-daemon v0.1.6 // indirect
@ -33,7 +33,9 @@ require (
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/ProtonMail/go-crypto v1.0.0 // indirect
github.com/PuerkitoBio/goquery v1.10.0 // indirect
github.com/andybalholm/brotli v1.1.0 // indirect
github.com/andybalholm/cascadia v1.3.2 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/cloudflare/circl v1.3.7 // indirect
github.com/cyphar/filepath-securejoin v0.2.4 // indirect
@ -109,7 +111,7 @@ require (
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
golang.org/x/net v0.25.0 // indirect
golang.org/x/net v0.29.0 // indirect
google.golang.org/protobuf v1.34.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

13
go.sum
View File

@ -36,10 +36,14 @@ github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63n
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=
github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78=
github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
github.com/Superdanda/hade v1.0.4 h1:NFsH8BBbsHmbedkGHdpjGk3e49GEJtRWU4ENOxp0sfU=
github.com/Superdanda/hade v1.0.4/go.mod h1:z52uXdtEfX25FRCj7YeQOctw6fho9nIaIWc/picNhuA=
github.com/PuerkitoBio/goquery v1.10.0 h1:6fiXdLuUvYs2OJSvNRqlNPoBm6YABE226xrbavY5Wv4=
github.com/PuerkitoBio/goquery v1.10.0/go.mod h1:TjZZl68Q3eGHNBA8CWaxAN7rOU1EbDz3CWuolcO5Yu4=
github.com/Superdanda/hade v1.0.6 h1:oZg1y2OUYKR+JdsbAzbv5DHNhu5VM6XoKliNmYBWUvA=
github.com/Superdanda/hade v1.0.6/go.mod h1:z52uXdtEfX25FRCj7YeQOctw6fho9nIaIWc/picNhuA=
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
@ -368,10 +372,13 @@ golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -398,6 +405,7 @@ golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
@ -408,6 +416,7 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=

View File

@ -1,8 +1,8 @@
package main
import (
"bbs/app/console"
"bbs/app/http"
"github.com/Superdanda/hade/app/console"
"github.com/Superdanda/hade/framework"
"github.com/Superdanda/hade/framework/provider/app"
"github.com/Superdanda/hade/framework/provider/cache"
@ -14,6 +14,7 @@ import (
"github.com/Superdanda/hade/framework/provider/orm"
"github.com/Superdanda/hade/framework/provider/redis"
"github.com/Superdanda/hade/framework/provider/ssh"
"github.com/Superdanda/hade/framework/provider/type_register"
)
func main() {
@ -32,6 +33,7 @@ func main() {
container.Bind(&redis.RedisProvider{})
container.Bind(&cache.HadeCacheProvider{})
container.Bind(&ssh.SSHProvider{})
container.Bind(&type_register.TypeRegisterProvider{})
// 将HTTP引擎初始化,并且作为服务提供者绑定到服务容器中
if engine, err := http.NewHttpEngine(container); err == nil {

123
package-lock.json generated
View File

@ -6,6 +6,8 @@
"": {
"name": "element-starter",
"dependencies": {
"@toast-ui/editor": "^3.2.2",
"@toast-ui/vue-editor": "^3.2.3",
"axios": "^0.24.0",
"element-ui": "^2.3.4",
"js-cookie": "^3.0.1",
@ -35,6 +37,32 @@
"node": ">=6"
}
},
"node_modules/@toast-ui/editor": {
"version": "3.2.2",
"resolved": "https://registry.npmmirror.com/@toast-ui/editor/-/editor-3.2.2.tgz",
"integrity": "sha512-ASX7LFjN2ZYQJrwmkUajPs7DRr9FsM1+RQ82CfTO0Y5ZXorBk1VZS4C2Dpxinx9kl55V4F8/A2h2QF4QMDtRbA==",
"dependencies": {
"dompurify": "^2.3.3",
"prosemirror-commands": "^1.1.9",
"prosemirror-history": "^1.1.3",
"prosemirror-inputrules": "^1.1.3",
"prosemirror-keymap": "^1.1.4",
"prosemirror-model": "^1.14.1",
"prosemirror-state": "^1.3.4",
"prosemirror-view": "^1.18.7"
}
},
"node_modules/@toast-ui/vue-editor": {
"version": "3.2.3",
"resolved": "https://registry.npmmirror.com/@toast-ui/vue-editor/-/vue-editor-3.2.3.tgz",
"integrity": "sha512-IjoV5tBh/yesIuqRqmOQx1+F0oeeAbIeBA7edMTawIXHQXBeJ1qzGHLTY5NWrUQ6BBtV8CDBeedjnVsJ+mHjKQ==",
"dependencies": {
"@toast-ui/editor": "^3.2.2"
},
"peerDependencies": {
"vue": "^2.5.0"
}
},
"node_modules/accepts": {
"version": "1.3.3",
"resolved": "http://registry.npm.taobao.org/accepts/download/accepts-1.3.3.tgz",
@ -2146,6 +2174,11 @@
"domelementtype": "1"
}
},
"node_modules/dompurify": {
"version": "2.5.7",
"resolved": "https://registry.npmmirror.com/dompurify/-/dompurify-2.5.7.tgz",
"integrity": "sha512-2q4bEI+coQM8f5ez7kt2xclg1XsecaV9ASJk/54vwlfRRNQfDqJz2pzQ8t0Ix/ToBpXlVjrRIx7pFC/o8itG2Q=="
},
"node_modules/domutils": {
"version": "1.5.1",
"resolved": "http://registry.npm.taobao.org/domutils/download/domutils-1.5.1.tgz",
@ -4086,6 +4119,11 @@
"node": ">=0.10.0"
}
},
"node_modules/orderedmap": {
"version": "2.1.1",
"resolved": "https://registry.npmmirror.com/orderedmap/-/orderedmap-2.1.1.tgz",
"integrity": "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g=="
},
"node_modules/original": {
"version": "1.0.0",
"resolved": "http://registry.npm.taobao.org/original/download/original-1.0.0.tgz",
@ -4935,6 +4973,81 @@
"dev": true,
"license": "MIT"
},
"node_modules/prosemirror-commands": {
"version": "1.6.2",
"resolved": "https://registry.npmmirror.com/prosemirror-commands/-/prosemirror-commands-1.6.2.tgz",
"integrity": "sha512-0nDHH++qcf/BuPLYvmqZTUUsPJUCPBUXt0J1ErTcDIS369CTp773itzLGIgIXG4LJXOlwYCr44+Mh4ii6MP1QA==",
"dependencies": {
"prosemirror-model": "^1.0.0",
"prosemirror-state": "^1.0.0",
"prosemirror-transform": "^1.10.2"
}
},
"node_modules/prosemirror-history": {
"version": "1.4.1",
"resolved": "https://registry.npmmirror.com/prosemirror-history/-/prosemirror-history-1.4.1.tgz",
"integrity": "sha512-2JZD8z2JviJrboD9cPuX/Sv/1ChFng+xh2tChQ2X4bB2HeK+rra/bmJ3xGntCcjhOqIzSDG6Id7e8RJ9QPXLEQ==",
"dependencies": {
"prosemirror-state": "^1.2.2",
"prosemirror-transform": "^1.0.0",
"prosemirror-view": "^1.31.0",
"rope-sequence": "^1.3.0"
}
},
"node_modules/prosemirror-inputrules": {
"version": "1.4.0",
"resolved": "https://registry.npmmirror.com/prosemirror-inputrules/-/prosemirror-inputrules-1.4.0.tgz",
"integrity": "sha512-6ygpPRuTJ2lcOXs9JkefieMst63wVJBgHZGl5QOytN7oSZs3Co/BYbc3Yx9zm9H37Bxw8kVzCnDsihsVsL4yEg==",
"dependencies": {
"prosemirror-state": "^1.0.0",
"prosemirror-transform": "^1.0.0"
}
},
"node_modules/prosemirror-keymap": {
"version": "1.2.2",
"resolved": "https://registry.npmmirror.com/prosemirror-keymap/-/prosemirror-keymap-1.2.2.tgz",
"integrity": "sha512-EAlXoksqC6Vbocqc0GtzCruZEzYgrn+iiGnNjsJsH4mrnIGex4qbLdWWNza3AW5W36ZRrlBID0eM6bdKH4OStQ==",
"dependencies": {
"prosemirror-state": "^1.0.0",
"w3c-keyname": "^2.2.0"
}
},
"node_modules/prosemirror-model": {
"version": "1.23.0",
"resolved": "https://registry.npmmirror.com/prosemirror-model/-/prosemirror-model-1.23.0.tgz",
"integrity": "sha512-Q/fgsgl/dlOAW9ILu4OOhYWQbc7TQd4BwKH/RwmUjyVf8682Be4zj3rOYdLnYEcGzyg8LL9Q5IWYKD8tdToreQ==",
"dependencies": {
"orderedmap": "^2.0.0"
}
},
"node_modules/prosemirror-state": {
"version": "1.4.3",
"resolved": "https://registry.npmmirror.com/prosemirror-state/-/prosemirror-state-1.4.3.tgz",
"integrity": "sha512-goFKORVbvPuAQaXhpbemJFRKJ2aixr+AZMGiquiqKxaucC6hlpHNZHWgz5R7dS4roHiwq9vDctE//CZ++o0W1Q==",
"dependencies": {
"prosemirror-model": "^1.0.0",
"prosemirror-transform": "^1.0.0",
"prosemirror-view": "^1.27.0"
}
},
"node_modules/prosemirror-transform": {
"version": "1.10.2",
"resolved": "https://registry.npmmirror.com/prosemirror-transform/-/prosemirror-transform-1.10.2.tgz",
"integrity": "sha512-2iUq0wv2iRoJO/zj5mv8uDUriOHWzXRnOTVgCzSXnktS/2iQRa3UUQwVlkBlYZFtygw6Nh1+X4mGqoYBINn5KQ==",
"dependencies": {
"prosemirror-model": "^1.21.0"
}
},
"node_modules/prosemirror-view": {
"version": "1.34.3",
"resolved": "https://registry.npmmirror.com/prosemirror-view/-/prosemirror-view-1.34.3.tgz",
"integrity": "sha512-mKZ54PrX19sSaQye+sef+YjBbNu2voNwLS1ivb6aD2IRmxRGW64HU9B644+7OfJStGLyxvOreKqEgfvXa91WIA==",
"dependencies": {
"prosemirror-model": "^1.20.0",
"prosemirror-state": "^1.0.0",
"prosemirror-transform": "^1.1.0"
}
},
"node_modules/proxy-addr": {
"version": "1.1.4",
"resolved": "http://registry.npm.taobao.org/proxy-addr/download/proxy-addr-1.1.4.tgz",
@ -5416,6 +5529,11 @@
"dev": true,
"license": "BSD-3-Clause"
},
"node_modules/rope-sequence": {
"version": "1.3.4",
"resolved": "https://registry.npmmirror.com/rope-sequence/-/rope-sequence-1.3.4.tgz",
"integrity": "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ=="
},
"node_modules/sax": {
"version": "1.2.2",
"resolved": "http://registry.npm.taobao.org/sax/download/sax-1.2.2.tgz",
@ -6522,6 +6640,11 @@
"vue": "^2.0.0"
}
},
"node_modules/w3c-keyname": {
"version": "2.2.8",
"resolved": "https://registry.npmmirror.com/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="
},
"node_modules/watchpack": {
"version": "1.3.1",
"resolved": "http://registry.npm.taobao.org/watchpack/download/watchpack-1.3.1.tgz",

View File

@ -8,13 +8,15 @@
"build": "rimraf dist && webpack -p --progress --hide-modules"
},
"dependencies": {
"@toast-ui/editor": "^3.2.2",
"@toast-ui/vue-editor": "^3.2.3",
"axios": "^0.24.0",
"element-ui": "^2.3.4",
"js-cookie": "^3.0.1",
"vue": "^2.5.16",
"vue-router": "^3.5.3",
"vue-style-loader": "^4.1.3",
"vuex": "^3.6.2",
"axios": "^0.24.0",
"js-cookie": "^3.0.1"
"vuex": "^3.6.2"
},
"engines": {
"node": ">=6"

View File

@ -6,8 +6,8 @@
<title>element-starter</title>
</head>
<body>
<div id="app"></div>
<body style="margin: 0px;">
<div id="app"></div>
</body>
</html>

View File

@ -4,6 +4,11 @@ import Router from 'vue-router'
import ViewLogin from '../views/login/index'
import ViewRegister from '../views/register/index'
import View404 from '../views/404'
import ViewContainer from '../views/layout/container'
import ViewList from '../views/list/index'
import ViewDetail from '../views/detail/index'
import ViewCreate from '../views/create/index'
import ViewEdit from '../views/edit/index'
Vue.use(Router)
@ -29,30 +34,29 @@ export const constantRoutes = [
component: View404,
hidden: true
},
// {
// path: '/',
// redirect: '/',
// component: ViewContainer,
// children: [
// {
// path: '',
// component: ViewList
// },
// {
// path: 'detail',
// component: ViewDetail
// },
// {
// path: 'create',
// component: ViewCreate
// },
// {
// path: 'edit',
// component: ViewEdit
// }
// ]
// },
// 404 page must be placed at the end !!!
{
path: '/',
redirect: '/',
component: ViewContainer,
children: [
{
path: '',
component: ViewList
},
{
path: 'detail',
component: ViewDetail
},
{
path: 'create',
component: ViewCreate
},
{
path: 'edit',
component: ViewEdit
}
]
},
{ path: '*', redirect: '/404', hidden: true }
]

View File

@ -1,6 +1,5 @@
import axios from 'axios'
import { Message } from 'element-ui'
import {Message} from 'element-ui'
// 创建一个axios
@ -25,30 +24,43 @@ service.interceptors.request.use(
service.interceptors.response.use(
response => {
// Any status code that lie within the range of 2xx cause this function to trigger
// Do something with response data
// 判断http status是否为200
if (response.status !== 200) {
const data = response.data
if (typeof data == 'string') {
Message({
message: data,
type: 'error',
duration: 5 * 1000
})
}
// Do something with result data
// 处理 2xx 范围内的响应
const res = response.data;
// 判断后端返回的 code 是否为 0表示失败
if (res.code !== 1) {
Message({
message: res.message || '网络开小差啦!',
type: 'error',
duration: 5 * 1000
});
// 返回一个拒绝的 Promise以便调用方处理错误
console.log("执行到第一个 reject ")
console.log(res)
return Promise.reject(new Error(res.message || '网络开小差啦!'));
} else {
// 正常返回数据
console.log("执行到下面的 else ")
console.log(res)
return res;
}
return response
},
error => {
// Any status codes that falls outside the range of 2xx cause this function to trigger
// Do something with response error
// Do something with result error
console.log('err: ' + error) // for debug
const message = (error.response && error.response.data && error.response.data.message)
|| '网络开小差啦!';
// 打印Message消息
Message({
message: error.response.data,
message,
type: 'error',
duration: 5 * 1000
})
console.log("执行到下面的 error了 ")
console.log(message)
return Promise.reject(error)
}
)

View File

@ -0,0 +1,93 @@
<template>
<el-row type="flex" justify="center" align="middle">
<el-col :span="8">
<el-card class="box-card" shadow="never">
<el-form ref="form" :model="question" >
<el-form-item >
<el-input v-model="question.title"></el-input>
</el-form-item>
<el-form-item >
<editor :options="editorOptions"
height="500px"
initialEditType="wysiwyg"
previewStyle="vertical"
ref="toastuiEditor"
:initialValue="content"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="postQuestion">立即提问</el-button>
<el-button>取消</el-button>
</el-form-item>
</el-form>
</el-card>
</el-col>
</el-row>
</template>
<script>
import '@toast-ui/editor/dist/toastui-editor.css';
import { Editor } from '@toast-ui/vue-editor';
import request from "../../utils/request";
export default {
components: {
editor: Editor
},
data() {
return {
question: {
title: '',
content: ''
},
content: '<p>123213213123</p>',
editorOptions: {
minHeight: '200px',
language: 'en-US',
useCommandShortcut: true,
usageStatistics: true,
hideModeSwitch: true,
toolbarItems: [
['heading', 'bold', 'italic', 'strike'],
['hr', 'quote'],
['ul', 'ol', 'task', 'indent', 'outdent'],
['table', 'image', 'link'],
['code', 'codeblock'],
['scrollSync'],
]
}
}
},
methods: {
postQuestion: function() {
debugger
let html = this.$refs.toastuiEditor.$data.editor.getHTML()
this.question.content = html;
const that = this
request({
method: 'POST',
url: "/question/create",
data: this.question,
}).then(function () {
that.$router.push({ path: '/' })
})
},
}
}
</script>
<style scoped>
.text {
font-size: 14px;
}
.item {
padding: 18px 0;
}
.box-card {
/*width: 480px;*/
}
</style>

176
src/views/detail/index.vue Normal file
View File

@ -0,0 +1,176 @@
<template>
<el-row type="flex" justify="center" align="middle">
<el-col :span="8">
<el-card v-if="question" class="box-card" shadow="never">
<div slot="header" class="clearfix">
<span>{{ question.title }} <span class="header_name" style="margin-right: 5px; float: right;"> <span
@click="gotoQuestionEdit">修改</span> <span @click="gotoDeleteQuestion">删除</span> </span> </span>
</div>
<div>
<viewer ref="questionViewer" :options="questionViewerOptions" :initialValue="question.context"/>
</div>
</el-card>
<el-divider content-position="left">所有回答</el-divider>
<el-card v-for="answer in question.answers" style="margin-top: 5px; " class="box-card" shadow="hover">
<div slot="header" class="clearfix">
<span>{{ answer.author.user_name }} | {{ answer.created_at | formatDate }} <span class="header_name"
style="margin-right: 5px; float: right;"
@click="gotoDeleteAnswer(answer.id)">删除</span></span>
</div>
<div>
<viewer ref="answerViewer" :initialValue="answer.content"/>
</div>
</el-card>
<el-divider content-position="left">我来回答</el-divider>
<el-card class="box-card" shadow="never">
<el-form ref="form" :model="question">
<el-form-item>
<editor :options="editorOptions"
:initialValue="answerContext"
height="200px"
initialEditType="markdown"
ref="toastuiEditor"
previewStyle="vertical"/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="postAnswer">提交</el-button>
</el-form-item>
</el-form>
</el-card>
</el-col>
</el-row>
</template>
<script>
import '@toast-ui/editor/dist/toastui-editor-viewer.css';
import '@toast-ui/editor/dist/toastui-editor.css';
import {Editor, Viewer} from '@toast-ui/vue-editor';
import request from "../../utils/request";
export default {
components: {
viewer: Viewer,
editor: Editor,
},
data() {
return {
id: 0,
question: null,
questionViewerOptions: {
usageStatistics: true,
},
answerContext: '',
editorOptions: {
minHeight: '200px',
language: 'en-US',
useCommandShortcut: true,
usageStatistics: true,
hideModeSwitch: false,
toolbarItems: [
['heading', 'bold', 'italic', 'strike'],
['hr', 'quote'],
['ul', 'ol', 'task', 'indent', 'outdent'],
['table', 'link'],
['code', 'codeblock'],
['scrollSync'],
]
}
};
},
created() {
if (this.$route.query.id) {
let id = parseInt(this.$route.query.id)
this.getDetail(id);
}
},
methods: {
getDetail: function (id) {
const that = this
this.id = id
request({
url: "/question/detail",
method: 'POST',
params: {
"id": id
}
}).then(function (response) {
that.question = response.data;
})
},
postAnswer: function () {
let html = this.$refs.toastuiEditor.$data.editor.getHTML()
this.answerContext = html
const that = this
request({
method: 'POST',
url: "/answer/create",
data: {
"question_id": that.id,
"context": that.answerContext,
},
}).then(function () {
that.$router.go(0)
})
},
gotoQuestionEdit: function () {
this.$router.push({path: '/edit', query: {'id': this.id}})
},
gotoDeleteQuestion: function () {
const that = this
request({
method: 'POST',
url: "/question/delete",
data: {
"questionId": that.id,
}
}).then(function () {
that.$router.go(0)
})
},
gotoDeleteAnswer: function (id) {
const that = this
request({
method: 'POST',
url: "/answer/delete",
data: {
"answerId": id,
},
}).then(function () {
that.$router.go(0)
})
}
}
};
</script>
<style scoped>
.text {
font-size: 14px;
}
.item {
padding: 18px 0;
}
.box-card {
/*width: 480px;*/
}
.header_name {
float: right;
font-size: 13px;
font-weight: 400;
margin: 0 15px 0 0;
line-height: 34px;
background-color: transparent;
color: #486e9b;
text-decoration: none;
}
.header_name > a {
background-color: transparent;
color: #486e9b;
text-decoration: none;
}
</style>

97
src/views/edit/index.vue Normal file
View File

@ -0,0 +1,97 @@
<template>
<el-row type="flex" justify="center" align="middle">
<el-col :span="8">
<el-card class="box-card" shadow="never">
<el-form ref="form" :model="question" label-width="80px">
<el-form-item label="问题标题">
<el-input v-model="question.title">{{question.title}}</el-input>
</el-form-item>
<el-form-item label="问题描述" v-if="question.context">
<editor :options="editorOptions"
:initialValue="question.context"
height="500px"
initialEditType="wysiwyg"
previewStyle="vertical" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSubmit">更新</el-button>
</el-form-item>
</el-form>
</el-card>
</el-col>
</el-row>
</template>
<script>
import '@toast-ui/editor/dist/toastui-editor.css';
import { Editor } from '@toast-ui/vue-editor';
import request from "../../utils/request";
export default {
components: {
editor: Editor
},
created() {
if (this.$route.query.id) {
let id = parseInt(this.$route.query.id)
this.getDetail(id);
}
},
data() {
return {
question: {
title: '',
context: ''
},
editorOptions: {
minHeight: '200px',
language: 'en-US',
useCommandShortcut: true,
usageStatistics: true,
hideModeSwitch: true,
toolbarItems: [
['heading', 'bold', 'italic', 'strike'],
['hr', 'quote'],
['ul', 'ol', 'task', 'indent', 'outdent'],
['table', 'image', 'link'],
['code', 'codeblock'],
['scrollSync'],
]
}
}
},
methods: {
getDetail: function (id) {
const that = this
this.id = id
request({
url: "/question/detail",
method: 'POST',
data: {
"questionId": id
}
}).then(function (response) {
that.question = response.data;
})
},
onSubmit: function () {
}
}
}
</script>
<style scoped>
.text {
font-size: 14px;
}
.item {
padding: 18px 0;
}
.box-card {
/*width: 480px;*/
}
</style>

171
src/views/list/index.vue Normal file
View File

@ -0,0 +1,171 @@
<template>
<el-row type="flex" justify="center" align="middle">
<el-col :span="8">
<div class="infinite-list-wrapper" style="overflow:auto">
<ul
class="list"
v-infinite-scroll="load"
infinite-scroll-disabled="disabled">
<el-card v-for="question in questions" class="box-card" shadow="hover">
<div slot="header" class="clearfix">
<span>{{question.title}}</span>
</div>
<div class="text item">
{{question.context}}
</div>
<div class="bottom clearfix">
<time class="time">{{question.created_at}} {{question.author.user_name}} | {{question.answer_num}} 回答</time>
<el-button type="text" class="button" @click="gotoDetail(question.id)">去看看</el-button>
</div>
</el-card>
</ul>
<p v-if="loading" class="loading_tips">加载中...</p>
<p v-if="disabled" class="loading_tips">没有更多了</p>
</div>
</el-col>
</el-row>
</template>
<script>
import request from "../../utils/request";
export default {
data () {
return {
count: 10,
start: 0,
size: 10,
questions: [],
loading: false,
noMore: false
}
},
created() {
this.getQuestions();
},
computed: {
disabled () {
return this.loading || this.noMore
}
},
methods: {
load () {
if (this.noMore === true) {
return
}
this.loading = true
setTimeout(() => {
this.loading = false
this.getQuestions()
}, 2000)
},
getQuestions() {
const that = this;
request({
url: '/question/list',
method: 'POST',
data: {
start: this.start,
size: this.size,
}
}).then(function (response) {
const questions = response.data
if (questions === null || questions.length === 0) {
that.noMore = true
}
that.questions = that.questions.concat(questions)
that.start = that.start + questions.length
})
this.loading = false;
},
gotoDetail(id) {
// go to detail page
this.$router.push({path: '/detail', query:{'id': id}})
}
}
}
</script>
<style scoped>
.loading_tips {
text-align: center;
font-size: 13px;
color: #999;
}
.time {
font-size: 13px;
color: #999;
}
.bottom {
margin-top: 13px;
line-height: 12px;
}
.carousel {
text-align: center;
}
.box-card {
margin-top: 10px;
/*height: 240px;*/
}
.text {
font-size: 14px;
}
.item {
margin-bottom: 18px;
}
.el-carousel__item h3 {
color: #475669;
font-size: 18px;
opacity: 0.75;
line-height: 300px;
margin: 0;
}
.el-carousel__item:nth-child(2n) {
background-color: #99a9bf;
}
.el-carousel__item:nth-child(2n+1) {
background-color: #d3dce6;
}
.time {
font-size: 13px;
color: #999;
}
.bottom {
margin-top: 13px;
line-height: 12px;
}
.button {
padding: 0;
float: right;
}
.image {
width: 100%;
display: block;
}
.clearfix:before,
.clearfix:after {
display: table;
content: "";
}
.clearfix:after {
clear: both
}
</style>

7920
yarn.lock

File diff suppressed because it is too large Load Diff