后端软件设计文档

主要负责人:邱晓裕

审核人:邱奕浩,蒲其刚,邵梓硕

1. 技术选型及理由

本项目后端的项目架构由以下技术组成:

2. 架构设计

项目的架构设计如下图所示,该项目使用经典的CS架构,使用Nginx进行反向代理,并使用多个dockers运行后台程序,docker与数据库之间进行数据交互,可以很方便提高服务器的抗压能力、性能。

1561560075556

3. 模块划分

项目后端代码目录结构如下图所示:

文件夹 PATH 列表
卷序列号为 0002-F2B8
C:.
├─controllers   # 控制类,管理数据库
├─global		# 全局类,包括一些控制信息
├─helper		# 工具类,包括一些简单的相应类
├─main			# 包括主函数和各种路由函数
├─models		# 模型类
└─test			# 数据库相关测试代码

4. 软件设计技术

(1)面向对象编程

面向对象设计与编程是最常见的选择。多年产业实践证明,面向对象具有具有易于理解、易于复用(reuse)和可扩展(extend)的优势。因此,本项目使用面向对象编程的方式:

├─models
│      answer.go				# 答案类
│      message.go				# 消息类
│      option.go				# 选项类
│      question.go				# 问题类
│      questionnaireFormat.go	# 问卷类
│      record.go				# 答案记录类
│      taskPreview.go			# 问卷简要信息类
│      user.go					# 用户类
├─controllers
│      db_controller.go  		# 数据库核心代码
│      msg_controller.go		# 数据库管理消息模块代码
│      qFormat_controller.go	# 数据库管理问卷模块代码
│      record_controller.go		# 数据库管理用户提交的答案模块代码
│      userdb_controller.go		# 数据库管理用户信息的模块代码

实现方式是:

以用户信息User为例:

// user.go

package models

// User is our sample data structure.
// which could wrap by embedding the models.User or
// declare new fields instead butwe will use this models
// as the only one User model in our application,
// for the shake of simplicty.
type User struct {
	Phone       string `json:"phone"`
	Remain      int    `json:"remain"`
	Iscow       bool   `json:"iscow"`
	Name        string `json:"name"`
	Password    string `json:"password"`
	Gender      string `json:"gender"`
	Age         int    `json:"age"`
	University  string `json:"university"`
	Company     string `json:"company"`
	Description string `json:"description"`
	Class       string `json:"class"`
}
// userdb_controller.go 

type User_Interface interface {
    // 对用户信息进行查询
	QueryUser(phone string) (ok bool)

    // 选择指定ID的用户
	SelectUser(phone string) (user *models.User, ok bool)

    // 新建用户
	InsertUser(user *models.User) (ok bool)

    // 更新用户信息
	UpdateUser(phone string, user *models.User) (updatedUser *models.User, ok bool)

    // 更新用户余额
	UpdateMoney(phone string, money int) (ok bool)

    // 删除用户
	DeleteUser(phone string) (ok bool)
}
// QueryUser 对用户信息进行查询
func (r *DBRepository) QueryUser(phone string) (ok bool) {

	psqlInfo := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable",
		global.Host, global.Port, global.User, global.Password, global.Dbname)
	db, err := sql.Open("postgres", psqlInfo)

	var count int64
	err = db.QueryRow("select count(*) from t_user where phone=$1", phone).Scan(&count)

	ok = true
	if err != nil || count == 0 {
		ok = false
	}

	db.Close()
	return ok
}

// SelectUser 选择指定ID的用户
func (r *DBRepository) SelectUser(phone string) (user *models.User, ok bool) {

	psqlInfo := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable",
		global.Host, global.Port, global.User, global.Password, global.Dbname)
	db, err := sql.Open("postgres", psqlInfo)
	rows, err := db.Query("select * from t_user where phone=$1", phone)

	userObj := models.User{}
	rows.Next()
	err = rows.Scan(&userObj.Phone, &userObj.Remain, &userObj.Iscow, &userObj.Name, &userObj.Password, &userObj.Gender,
		&userObj.Age, &userObj.University, &userObj.Company, &userObj.Description, &userObj.Class)

	if err != nil {
		fmt.Printf("could not find user, %v", err)
		db.Close()
		return nil, false
	}

	db.Close()
	return &userObj, ok
}

// InsertUser 新建用户
func (r *DBRepository) InsertUser(userObj *models.User) (ok bool) {

	psqlInfo := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable",
		global.Host, global.Port, global.User, global.Password, global.Dbname)
	db, err := sql.Open("postgres", psqlInfo)
	ok = true

	stmt, err := db.Prepare("insert into t_user (phone, remain, iscow, name, password, gender, age, university, company, description, class)" +
		" values($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)")
	if err != nil {
		ok = false
	}
	_, err = stmt.Exec(userObj.Phone, 0, userObj.Iscow, userObj.Name, userObj.Password, userObj.Gender,
		userObj.Age, userObj.University, userObj.Company, userObj.Description, userObj.Class)
	if err != nil {
		fmt.Printf("could not insert user, %v", err)
		ok = false
	}

	db.Close()
	return ok
}

// UpdateUser 更行用户信息
func (r *DBRepository) UpdateUser(phone string, user *models.User) (updatedUser *models.User, ok bool) {

	psqlInfo := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable",
		global.Host, global.Port, global.User, global.Password, global.Dbname)
	db, err := sql.Open("postgres", psqlInfo)
	ok = true

	stmt, err := db.Prepare("update t_user set iscow=$1, name=$2, password=$3, gender=$4, age=$5, " +
		"university=$6, company=$7, description=$8, class=$9 WHERE phone=$10")
	if err != nil {
		ok = false
	}
	_, err = stmt.Exec(user.Iscow, user.Name, user.Password, user.Gender,
		user.Age, user.University, user.Company, user.Description, user.Class, user.Phone)
	if err != nil {
		ok = false
	}
	db.Close()

	return user, ok
}

// UpdateMoney 更新用户余额
func (r *DBRepository) UpdateMoney(phone string, money int) (ok bool) {
	psqlInfo := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable",
		global.Host, global.Port, global.User, global.Password, global.Dbname)
	db, err := sql.Open("postgres", psqlInfo)
	ok = true

	stmt, err := db.Prepare("update t_user set remain=$1 WHERE phone=$2")
	if err != nil {
		ok = false
	}
	_, err = stmt.Exec(money, phone)
	if err != nil {
		ok = false
	}
	db.Close()

	return ok
}

func checkErr(err error) {
	if err != nil {
		panic(err)
	}
}

(2)MVC架构(Model View Controller)

Iris 有一个非常强大和炽热 快速 的 MVC 支持,你可以从一个方法函数里返回你想要的任何类型的值,并且这个值会发送到你预期的客户端。

项目目录树为:

文件夹 PATH 列表
卷序列号为 0002-F2B8
C:.
│  
├─controller
│      db_controller.go  		# 数据库核心代码
│      msg_controller.go		# 数据库管理消息模块代码
│      qFormat_controller.go	# 数据库管理问卷模块代码
│      record_controller.go		# 数据库管理用户提交的答案模块代码
│      userdb_controller.go		# 数据库管理用户信息的模块代码
├─global
│      config.go				# 全局配置文件,包含一些全局配置信息
│      
├─helper
│      response.go				# 后台返回信息的一些简单模板类
│      
├─main
│      backend.go				# 后台程序,用于生成消息通知
│      main.go					# 主程序
│      msgRounter.go			# 消息相关路由
│      qFormatRouter.go			# 问卷相关路由
│      recordRouter.go			# 用户提交答案相关路由
│      userRouter.go			# 用户信息相关路由
│      
├─models
│      answer.go				# 答案类
│      message.go				# 消息类
│      option.go				# 选项类
│      question.go				# 问题类
│      questionnaireFormat.go	# 问卷类
│      record.go				# 答案记录类
│      taskPreview.go			# 问卷简要信息类
│      user.go					# 用户类
│      
└─test
        qformat_controller_test.go	# 问卷数据库测试文件
        record_controller_test.go	# 用户提交答案数据库测试文件
        user_controller_test.go		# 用户信息数据库测试文件

其中:models目录下为模型,表示应用程序核心,比如用户信息类,controller目录下为控制器,对数据库的各种操作进行控制,main目录下包含各种路由实现,用于将数据信息进行显示(由于后端项目不包括可视化,因此这里只是将数据进行JSON字符串转化然后发送给请求方);

(3)单例模式

单例模式(Singleton Pattern)是经典的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。单例对象的类必须保证只有一个实例存在。

在本项目中,我们的数据库管理使用了单例模型,这样整个系统只需要拥有一个的全局对象,有利于我们协调系统整体的行为。

// db_controller.go

package controllers

import (
	"database/sql"
	"fmt"
	"sync"

	"../global"
)

var once sync.Once

type DBRepository struct {
}

var dbRepository *DBRepository

func GetDBInstance() *DBRepository {
	once.Do(func() {
		dbRepository = NewDBRepository()
	})
	return dbRepository
}

// NewUserRepository returns a new user repository,
// the one and only repository type in our example.
func NewDBRepository() *DBRepository {
	psqlInfo := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable",
		global.Host, global.Port, global.User, global.Password, global.Dbname)

	db, err := sql.Open("postgres", psqlInfo)
	if err != nil {
		panic(err)
	}
	defer db.Close()

	err = db.Ping()
	if err != nil {
		panic(err)
	}

	return &DBRepository{}
}

(4)抽象工厂模式

抽象工厂模式提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。接口是负责创建一个相关对象的工厂,不需要显式指定它们的类。每个生成的工厂都能按照工厂模式提供对象。

适用性

本项目需要对多个数据库的表进行交互,因此,我们使用抽象工厂模式,根据操作的表不同定义不同的接口,然后对这些接口进行实现:

// userdb_controller.go
type User_Interface interface {
	// 对用户信息进行查询
	QueryUser(phone string) (ok bool)

	// 选择指定ID的用户
	SelectUser(phone string) (user *models.User, ok bool)

	// 新建用户
	InsertUser(user *models.User) (ok bool)

	// 更新用户信息
	UpdateUser(phone string, user *models.User) (updatedUser *models.User, ok bool)

	// 更新用户余额
	UpdateMoney(phone string, money int) (ok bool)

	// 删除用户
	DeleteUser(phone string) (ok bool)
}
// qFormat_controller.go
type QFormat_Interface interface {
	// 根据ID查询问卷是否存在
	QueryQFormat(id string) (ok bool)

	// 根据ID获取指定问卷
	SelectQFormat(id string) (qFormat *models.QuestionnaireFormat, ok bool)

	// 新建问卷
	InsertQFormat(qFormat *models.QuestionnaireFormat) (ok bool, id string)

	// 更新问卷
	UpdateQFormat(id string, qFormat *models.QuestionnaireFormat) (updatedQFormat *models.QuestionnaireFormat, ok bool)

	// 将问卷移动到回收站
	TrashQFormat(id string, inTrash int) (ok bool)

	// 删除问卷
	DeleteQFormat(id string) (ok bool)

	// 获取所有问卷
	SelectAllQFormats() (taskPreviews []models.TaskPreview, ok bool)

	// 获取所有有效问卷
	SelectValidFormats() (taskPreviews []models.TaskPreview, ok bool)

	// 根据关键字搜索问卷
	SearchQFormats(str string) (taskPreviews []models.TaskPreview, ok bool)

	// 添加问卷的回答个数
	AddOneQFormats(id string) (ok bool)
}
// record_controller.go 
type Record_Interface interface {
	// 查询记录 
	QueryRecord(taskID string, userID string) (ok bool)

	// 获取记录
	SelectRecord(taskID string, userID string) (record *models.Record, ok bool)

	// 新建记录
	InsertRecord(record *models.Record) (ok bool)

	// 更新记录
	UpdateRecord(taskID string, userID string, record *models.Record) (ok bool)

	// 删除记录
	DeleteRecord(taskID string, userID string) (ok bool)

	// 获取所有记录
	SelectAllRecords(taskID string) (records []models.Record, ok bool)
}
// msg_controller.go
type Message_Interface interface {
	// 获取指定消息
	SelectMessage(messageID string) (message *models.Message, ok bool)

	// 获取未读消息个数
	GetCount(userID string) (count int, ok bool)

	// 获取所有消息
	GetAllMessage(userID string) (messages []models.Message, ok bool)

	// 新建消息
	InsertMessage(message *models.Message) (ok bool)

	// 更改消息的已读状态
	ReadMessage(messageID string, userID string, state int) (ok bool)

	// 删除消息
	DeleteMessage(messageID string, userID string) (ok bool)

	// 获取所有的未读消息
	GetUnReadMessage(userID string) (messages []models.Message, ok bool)

	// 获取所有的已读消息
	GetReadMessage(userID string) (messages []models.Message, ok bool)
}

5. 数据库设计

(1)数据库物理模型

Field Type Key Description
phone text PRIMARY KEY 用户的唯一身份标识
remain integer   用户的余额
iscow boolean   是否是奶牛
name text   用户名
password text   用户密码
gender text   用户的性别
age integer   用户的年龄
university text   用户所在的大学,(学生特有属性)
company text   用户所在的组织,(奶牛特有属性)
description text   用户的个人描述
class text   用户所在的年级,(学生特有属性)
Field Type Key Description
taskID text PRIMARY KEY 任务的唯一身份标识
taskName text   任务名
InTrash integer   任务是否在回收站之中
taskType text   任务的类型
creator text FOREIGN KEY 任务的创建者
description text   任务的描述信息
money integer   完成任务可以获得的奖励
number integer   任务预计完成的数量
finishedNumber integer   任务已经完成的数量
publishTime text   任务的发布时间
endTime text   任务的结束时间
chooseData text   任务的信息,json字符串
Field Type Key Description
taskID text PRIMARY KEY 任务的ID
userID text PRIMARY KEY 用户的ID
data text   提交的答案数据,json字符串
Field Type Key Description
msgID text PRIMARY KEY 消息的ID
state integer   消息的状态(已读和未读)
receiver text FOREIGN KEY 消息的接受者
time text   消息的时间
title text   消息的标题
content text   消息的内容

(2)用户及权限系统数据库设计

1561349829988

(3)ER模型

1561351902461

6. API 设计

(1)REST API 设计规范

服务器向用户返回的状态码和提示信息,常见的有以下一些(方括号中是该状态码对应的HTTP动词)。

200 – OK – 一切正常

201 – OK – 新的资源已经成功创建

204 – OK – 资源已经成功擅长

304 – Not Modified – 客户端使用缓存数据

400 – Bad Request – 请求无效,需要附加细节解释如 “JSON无效”

401 – Unauthorized – 请求需要用户验证

403 – Forbidden – 服务器已经理解了请求,但是拒绝服务或这种请求的访问是不允许的。

404 – Not found – 没有发现该资源

422 – Unprocessable Entity – 只有服务器不能处理实体时使用,比如图像不能被格式化,或者重要字段丢失。

500 – Internal Server Error – API开发者应该避免这种错误。

更新和创建操作应该返回对应的状态码,防止用户多次的API调用。

如果状态码是4xx,就应该向用户返回出错信息。一般来说,返回的信息中将error作为键名,出错信息作为键值即可。 使用详细的错误包装错误:

{
  "errors": [
   {
    "userMessage": "Sorry, the requested resource does not exist",
    "internalMessage": "No car found in the database",
    "code": 34,
    "more info": "http://dev.mwaysolutions.com/blog/api/v1/errors/12345"
   }
  ]
}

(2)API 设计文档

本项目使用ApiPost生成API设计文档:https://swsad-dalaotelephone.github.io/docs/api/