古老的榕树

用 Go 开发终端接口服务--按需写 service 服务层逻辑

发表 2019-05-14 16:54 阅读(718)
service 服务层是整个项目的枢纽部分,它调用下面 dao 数据层的函数,给上面 controller 控制层输送数据,此外,项目的业务逻辑、数据事务都在服务层完成的,结合以上特征,我们对服务层做法做了一些约定:服务层每个对外函数和 controller 控制层的接口是一一对应的,每个函数只为一个接口服务,保证所有业务逻辑都在服务层函数里实现,controller 控制层的接口函数,只负责获取终端传输过来的参数,传递给服务层。

服务层的私有函数,做一些可共用的功能,给对外函数调用。私有函数和对外函数,都可以调用多个 dao 数据层的函数,调用上不做任何限制。另外 common 和 util 包的函数也是给服务层调用的。所以服务层,相当于项目的枢纽和核心,起到承上启下的作用。服务层对外的函数,一般都是统一返回 ServiceResponse 结构体实例给 controller 控制层,控制层再转成 JSON 给终端。

service 服务层,最先开始是 init 函数,它初始化一个数据库 DB,上一章节有提过,DB 放在服务层更加灵活,因为可以随时可以进行一些小功能数据查询和操作,另外遇到多个数据库操作时,保证数据事务性也是一个重要原因。虽然 dao 数据层做到了可复用性,但还会有一些受限性,很多小功能操作是无法公共抽象或不具备公用性特质的,用一次后就不再用了。

*代码清单 - 数据库 DB 对象实例化*

var (
	sqlxDB *sqlx.DB
)

func init() {
	sqlxDB = dbutil.SQLXDB
	if sqlxDB == nil {
		sqlxDB = dbutil.NewSQLXDB()
	}
}


以上代码只是引用 dbutil 包的代码,具体实例化工作在 dbutil 包里实现,我们上几个章节已经讲解过。

service 服务层还有几个重要函数,返回 ServiceResponse 结构体的,它是服务层的核心数据结构,很多函数都必须返回它,所以我们写几个快捷的函数供调用。
*代码清单 - 返回  ServiceResponse 的快捷函数*

// serviceResponseSuccess 只简单返回成功
func ServiceResponseSuccess(body ...interface{}) model.ServiceResponse {
	return SetServiceResponseCode(common.CodeSuccess, body...)
}

// serviceResponseFailure 只简单返回操作失败
func ServiceResponseFailure() model.ServiceResponse {
	return SetServiceResponseCode(common.CodeFailure)
}

// setServiceResponseCode 响应主体结构体
func SetServiceResponseCode(resultCode int, body ...interface{}) model.ServiceResponse{
	return SetServiceResponse(resultCode, common.CodeMsgMap[resultCode], body...)
}

// setServiceResponse 响应主体结构体
func SetServiceResponse(resultCode int, errMsg string, body ...interface{}) (respBody model.ServiceResponse) {
	respBody.Code = resultCode
	respBody.ErrorMsg = errMsg
	if len(body) > 0 {
		respBody.Body = body[0]
	}
	return respBody
}


ServiceResponseSuccess 直接返回调用成功的实例( code=1,errorMsg=""),有个 body 可选参数,如果 body 有数据集合或实体对象返回,可根据需求传进来。

*备注:Go 的函数可选参数和多值返回的特征非常棒,很实用。*

ServiceResponseFailure 直接返回调用失败的实例( code=0, errorMsg="操作失败" ),无参的,程序内部自行指定 code errorMsg。

SetServiceResponseCode 返回指定某个 code errorMsg  的实例,有个 code 必选参数和一个 body 可选参数,errorMsg 信息是 code 对应的默认值信息。

SetServiceResponse 是一个原始的函数,返回指定某个 code errorMsg  的实例,code errorMsg 都是必填参数,传什么进来,就响应什么出去,body 是可选参数,其实上面的三个函数都是引用了此函数。

服务层的所有函数,根据传入参数的特点自行选择以上合适的快捷函数,它们都是返回 ServiceResponse 实例。这些都是公共函数,写的时候,要考虑全面,避免 Bug 存在,以免造成服务层大范围的受影响。

业务处理才是 service 服务层花费时间比较多的部分,一个业务函数,首先和上层 controller 层是相对应的,我们按需来写,不要写多余的函数。一个对外函数处理流程一般包括:首先验证参数空缺,参数合法性,然后过滤拦截不符合要求的上层请求,再者如果参数都验证通过了,直接组合参数,传递给 dao 数据层来获取想要的数据。对外函数的参数,我们建议分散使用基本类型数据,比如 string、int、int64、float64 等类型,一眼就能看到参数的类型,含义和顺序,尽量不要采用结构体的参数,调用者不知道结构体有哪些属性,以及各自属性含义,造成产生歧义。下面就以获取产品详情页典型的函数为例:
*代码清单 - 获取产品详情页数据*

// ProductDetail 产品详情页
func ProductDetail(productID int64) (retData model.ServiceResponse) {
	if productID <= 0 {
		return SetServiceResponseCode(common.Code1003)
	}

	product, err := dao.SelectProductByID(sqlxDB, productID)
	if err != nil {
		common.ShowErr(err)
		return ServiceResponseFailure()
	}

	photos := getProductPhotosByProductID(product.ID)
	productExt := model.ProductExt{
		Product: *product,
		Photos:  photos,
	}

	return ServiceResponseSuccess(productExt)
}


该函数首先检查 productID 是否小于等于零,如果是直接返回错误码  Code1003 的 ServiceResponse 实例,如果不是,组合参数,调用 dao 数据层 SelectProductByID 函数来获取产品信息实例,有错误直接返回调用失败 ServiceResponse 实例;无错则调用成功 ServiceResponseSuccess 函数,body 参数是 ProductExt 结构体,是一个扩展结构体,除了包括一个产品信息实体之外,还有一个图片集合,把产品信息实例和产品图片集合填充到 ProductExt 扩展结构体里,最后赋值给 ServiceResponse 实例的 body 可选参数,返回给上层。

服务层还有另一个重要的部分要特别说明一下,就是图片的上传,通常流程是这样的,我们新增一个产品信息的时候,客户端首先传递文件的 []byte、fileExt、seq 参数,调用公共上传文件函数,单独上传文件,函数会把图片压缩以及剪切为四套规格的图(原图、大图、小图、切图),完成后返回 src、big、small、cut 四套规格图的后缀路径(不带域名的后半部分路径)给终端,最后终端得到路径之后,取 big 大图路径信息和产品信息一起传递给新增产品函数,进行保存。

*代码清单 - 单独上传图片文件函数*

// UploadProductPhoto 上传产品图片
func UploadProductPhoto(file []byte, fileExt string, seq int) (retData model.ServiceResponse) {
	if file == nil {
		return SetServiceResponse(common.Code1003, "缺少文件数据")
	}

	if seq <= 0 {
		seq = 1
	}

	localUploadHandler := common.LocalUploadHandler{}
	retImageData, err := localUploadHandler.UploadImage(file, fileExt, seq, "product")
	if err != nil && retImageData == nil {
		common.ShowErr(err)
		return SetServiceResponse(common.CodeFailure, "文件上传失败!")
	}

	return ServiceResponseSuccess(retImageData)
}
图片上传函数 UploadImage 是来自 common 工具包的一个公用函数。每次上传一张图片,都会生成四套规则的图片保存到服务器。

*代码清单 - 新增产品函数*

// ProductAddNew 新增一个产品
func ProductAddNew(category int64, name, intro string, price float64, photoEdit []model.ViewPhotoRespArgs) (retData model.ServiceResponse) {
	if category <= 0 {
		return SetServiceResponseCode(common.Code1003)
	}

	if name == "" || intro == "" {
		return SetServiceResponseCode(common.Code1003)
	}

	if price <= 0 {
		return SetServiceResponseCode(common.Code1003)
	}

	product := model.Product{
		Category: category,
		Name:     name,
		Intro:    intro,
		Price:    price,
		Status:   1,
	}

	productID, err := dao.InsertProduct(sqlxDB, product)
	if err != nil {
		common.ShowErr(err)
		return ServiceResponseFailure()
	}

	// 新增图片记录
	for i, s := 0, len(photoEdit); i < s; i++ {
		productPhoto := model.ProductPhoto{
			ProductID: productID,
			Seq:       photoEdit[i].Seq,
			Path:      photoEdit[i].Path,
		}
		_, err = dao.InsertProductPhoto(sqlxDB, productPhoto)
		if err != nil {
			common.ShowErr(err)
		}
	}

	return ServiceResponseSuccess()
}
photoEdit []model.ViewPhotoRespArgs 就是图片信息集合,结构体包含了 ID Path URL Seq 等字段。其中 Path 就是大图的后缀路径,它作为存入数据库的字符串,URL 是查看产品信息的时候用于显示,终端图片控件需要完整的图片 URL,此处包新增产品暂时用不上 URL,而在查询产品列表页和详情页的时候会用上。实体结构体的 omitempty 标签 ,是很有用的属性,意思是如果该字段的值等于默认值,输出 JSON 的时候不忽略显示出来的。比如 ID = 0 的时候,JSON 将不显示 id 属性。

*代码清单 - 图片信息结构体*
// ViewPhotoRespArgs 查看图片响应的结构体
type ViewPhotoRespArgs struct {
	ID   int64  `db:"id" json:"id,omitempty"`
	Path string `db:"path" json:"path"`
	URL  string `db:"url" json:"url"`
	Seq  int    `db:"seq" json:"seq"`
}
新增产品函数,会把图片信息集合一一循环出来,调用 dao 的 InsertProductPhoto 函数来保存图片信息记录。


// 新增图片记录
for i, s := 0, len(photoEdit); i < s; i++ {
    productPhoto := model.ProductPhoto{
        ProductID: productID,
        Seq:       photoEdit[i].Seq,
        Path:      photoEdit[i].Path,
    }
    _, err = dao.InsertProductPhoto(sqlxDB, productPhoto)
    if err != nil {
        common.ShowErr(err)
    }
}
服务层每个一个函数一般都调用了 dao 数据层,common 公共工具层,model 实体层,dbutil、 util 公共层的东西,比如错误信息的输出、数据库 DB 的实例化实现,产品扩展结构体,图片上传函数等,这些都是比较分散的,在各个层的章节会提及到,在这里就不一一说明了。

小结
如果仔细观察,你会发现其实每个层(包)都有一个 init.go 文件,这个做法不是必须的,但这是常见的约定规则,我们约定在  
init.go 文件定义一个 init 函数里做一些最早进行的初始化操作,比如 service 服务层,我们首先进行数据库 DB 实例化。service 服务层是一个包,里面有若干个 go 文件,每个文件都可以定义 init 函数,为了减少混乱,我们建议写在 init.go 文件里,其实 Go 语言编译的时候,会把该包所有文件 import、const、var 、func 等所有对象根据内部规则排序,归集到一个文件里进行优化排序编译,我们要从开发者角度上考量和约定,尽力编写可维护性的代码。



《用 Go 开发终端接口服务》 目录


Donate

如果文章对您有帮助,请使用手机支付宝扫描二维码,捐赠X元,作者离不开读者的支持!