0%

环境搭建

  • node.js 安装16.0以上的版本
1
2
C:\Users\Administrator>node -v
v18.17.0
  • 你的当前工作目录正是打算创建项目的目录。在命令行中运行以下命令
1
E:\proj\StudyVue>npm init vue@latest

这一指令将会安装并执行 create-vue,它是 Vue 官方的项目脚手架工具

如果不确定是否要开启某个功能,你可以直接按下回车键选择 No。在项目被创建后,通过以下步骤安装依赖并启动开发服务器1:

  • 在项目被创建后,通过以下步骤安装依赖并启动开发服务器:
1
2
3
> cd vue-project
> npm install
> npm run dev

image-20230804152932990

vue配置

  • 安装依赖文件
1
2
3
4
5
6
7
// elementUI 3.0 版本
npm install element-plus --save
// http请求
npm install axios --save
// 路由
npm install vue-router@4 --save

  • src\router\routers.js 手动新建自定义路由
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15


const routes = [
{
name: "login",
path: "/login",
component: () => import("../components/Login.vue")
},
{
name: "home",
path: "/home",
component: () => import("../components/Home.vue")
}
]
export default routes;
  • src\router\index.js 对外暴露路由
1
2
3
4
5
6
7
8
import { createRouter, createWebHistory } from "vue-router"
import routes from "./routes"
var router=createRouter({
history:createWebHistory(),
routes
})
export default router

  • src/index.js 为全局配置文件
1
2
3
4
5
6
7
8
9
10
11
12
13
import './assets/main.css'
import { createApp } from 'vue'
import App from './App.vue'
// 导入我的路由
import router from "./router/index"
// 导入ElementPlus
import ElementPlus from 'element-plus'
// 引用element的css
import 'element-plus/dist/index.css'
const app = createApp(App)
app.use(ElementPlus)
app.use(router)
app.mount('#app')
  • App.vue 代码修改如下
1
2
3
4
5
6
7
8
9
10
11
<script setup>

</script>

<template>
<router-view></router-view>
</template>

<style scoped>

</style>
  • 按需导入elementUI,在2.0版本中可以需要手动管理elementUI中各个组件,3.0中现在可以自动导入,安装如下插件:
1
2
npm install -D unplugin-vue-components unplugin-auto-import

然后把下列代码插入到你的 ViteWebpack 的配置文件中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
//vite.config.js

import { fileURLToPath, URL } from 'node:url'

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// 新增
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'

// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
// 新增下面两个
AutoImport({
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [ElementPlusResolver()],
}),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
}
})

  • src\utils\ajax.js 拦截axios 设置请求url
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import axios from 'axios'

const ajax = axios.create({
baseURL: 'http://127.0.0.1:8100/testModel',
timeout: 10000,
withCredentials: true
})

ajax.interceptors.response.use(
response => {
//拦截响应,如果发现接口返回400做统一处理跳转到登陆
if (response.data.code) {
switch (response.data.code) {
case 400:
window.location.href='/login/'

}

}
const headers = response.headers

return response
},
//接口错误状态处理,也就是说无响应时的处理
error => {
console.log("Please check your internet connection.");
console.log(error)
return Promise.reject(error) // 返回接口返回的错误信息
})


//导出我们建立的axios实例模块,ES6 export用法
export default ajax;


//导出我们建立的axios实例模块,ES6 export用法
export default ajax;
  • src\utils\html.js 所有请求都编写到这里
1
2
3
4
5
6
7
8
import ajax from "./ajax";
// export const GetPosts = () => ajax.get('posts/1')
// export const GetsearchData = (params) => ajax.get('/list',{params})
// export const PostPosts = (params) => ajax.post('posts',params)

export const LoginPost = (params) => ajax.post('/login/',params)
export const CsrfGet = () => ajax.get('/get_csrf_token/')

  • 修改package.json
1
2
3
4
5
6

"scripts": {
"dev": "vite --host 0.0.0.0", // 修改为这样,不然无法用ip访问
"build": "vite build",
"preview": "vite preview"
},

登录界面

  • 编写登录界面src\componets\Login
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
<template>
<div class="login-wrap">
<!--
:rules="rules"为表单自定义规则
:model="loginForm" 绑定js中的return 参数
-->
<el-form ref="loginFormRef" :rules="rules" :model="loginForm" class="login_form">
<h1 class="title">用户登录</h1>
<el-form-item label="用户名" prop="username">
<el-input type="text" placeholder="用户账号" v-model="loginForm.username" auto-complete="off"></el-input>
</el-form-item>
<el-form-item label="密码" prop="password" style="margin-left:10px;">
<el-input type="password" placeholder="密码" v-model="loginForm.password" auto-complete="off"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm(loginFormRef)" style="width: 100%;">登录</el-button>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="resetForm(loginFormRef)" style="width: 100%;">重置</el-button>
</el-form-item>
</el-form>
</div>
</template>

<script lang="ts" setup>
import { reactive, ref } from 'vue'
import type { FormInstance, FormRules } from 'element-plus'
import { ElMessage } from 'element-plus'

// 引用登录请求
import { LoginPost } from "../utils/html.js"

//引入路由函数
import { useRouter } from "vue-router";
const router = useRouter()

interface LoginForm {
username: string
password: string
}
const loginFormRef = ref<FormInstance>()
// 定义表单绑定的model
const loginForm = reactive({
username: "test",
password: "",
})
// 定义规则
const rules = reactive<FormRules<LoginForm>>({
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
// { min: 3, max: 5, message: 'Length should be 3 to 5', trigger: 'blur' },
],

password: [
{
required: true,
message: '请输入密码',
},
],
});

const submitForm = (formEl: FormInstance | undefined) => {
if (!formEl) return
formEl.validate(async (valid, fields) => {
if (valid) {
console.log('submit!')
LoginPost(JSON.stringify(loginForm)).then(res => {
console.log(res)
if (res["data"]["code"] == 1) {
// console.log("登录成功")
ElMessage({
message: '登录成功.',
type: 'success',
})
localStorage.setItem('username', loginForm.username);
router.push("/home")
} else {
ElMessage({
message: res.data.msg,
type: 'error',
})
}
})
} else {
console.log('error submit!', fields)

}
})
}
const resetForm = (formEl: FormInstance | undefined) => {
if (!formEl) return
formEl.resetFields()
}
</script>

<style scoped>
.login-wrap {
box-sizing: border-box;
width: 100%;
height: 100%;
padding-top: 10%;
background-image: url(zdmc+);
/* background-color: #112346; */
background-repeat: no-repeat;
background-position: center right;
background-size: 100%;
}

.login-container {
border-radius: 10px;
margin: 0px auto;
width: 350px;
padding: 30px 35px 15px 35px;
background: #fff;
border: 1px solid #eaeaea;
text-align: left;
box-shadow: 0 0 20px 2px rgba(0, 0, 0, 0.1);
}

.title {
margin: 0px auto 40px auto;
text-align: center;
color: #505458;
}
</style>

django后台

跨域配置

  • StudyDjango/StudyDjango/settings.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
INSTALLED_APPS = [
....
'TestModel', # 添加此项
'corsheaders', # 跨域

]

MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
# 'django.middleware.csrf.CsrfViewMiddleware',
# 加入这个
'corsheaders.middleware.CorsMiddleware',
]


#跨域增加忽略
CORS_ALLOW_CREDENTIALS = True
CORS_ORIGIN_ALLOW_ALL = True
CORS_ORIGIN_WHITELIST = (
'http://localhost:5173',
'http://127.0.0.1:5173',
)

CORS_ALLOW_METHODS = (
'DELETE',
'GET',
'OPTIONS',
'PATCH',
'POST',
'PUT',
'VIEW',
)

CORS_ALLOW_HEADERS = (
'XMLHttpRequest',
'X_FILENAME',
'accept-encoding',
'authorization',
'content-type',
'dnt',
'origin',
'user-agent',
'x-csrftoken',
'x-requested-with',
'Pragma',
'json'
)
  • 登录代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def user_login(request):
if request.method != "POST":
return JsonResponse({'code': -1, 'msg': 'method must is POST'})

data = json.loads(request.body)
user_name = data.get('username')
pwd = data.get("password")
print(data)
try:
user_entry = Users.objects.get(name=user_name, pwd=pwd)
if user_entry:
# 设置登录的session
request.session["user"] = user_name
return JsonResponse({'code': 1, 'msg': 'login is success'})
else:
return JsonResponse({'code': -1, 'msg': 'login is fail221'})
except ObjectDoesNotExist:
return JsonResponse({'code': -1, 'msg': 'login is fail111'})
  • 运行代码
1
2
3
4
npm run dev

> vue-project@0.0.0 dev
> vite

image-20230808190806334

  • 输入用户名和密码登录成功后,跳转到了home页面

总结

  • 本次用的setup,和vue2.0 区别比较大,比如调用方法不在放到method中

  • 解决了vue和django 交互的跨域问题

  • 自定义路由

  • 封装axios

  • 客户端一定要用127.0.0.1 访问,需要和django中设置的白名单ip保持一致

说明

紧接python django实践

聚合和分组查询

聚合查询(aggregate)

  • 聚合查询函数是对一组值执行计算,并返回单个值

  • Django 使用聚合查询前要先从 django.db.models 引入 Avg、Max、Min、Count、Sum(首字母大写)。

  • 计算所有图书的平均价格:

1
2
3
4
5
from django.db.models import Avg,Max,Min,Count,Sum  #   引入函数
...
res = models.Book.objects.aggregate(Avg("price"))
print(res, type(res))
...
  • 计算所有图书的数量、最贵价格和最便宜价格:
1
2
res=models.Book.objects.aggregate(c=Count("id"),max=Max("price"),min=Min("price"))
print(res,type(res)

分组查询(annotate)

  • 分组查询一般会用到聚合函数,所以使用前要先从 django.db.models 引入 Avg,Max,Min,Count,Sum(首字母大写)
1
from django.db.models import Avg,Max,Min,Count,Sum  #   引入函数

返回值:

  • 分组后,用 values 取值,则返回值是 QuerySet 数据类型里面为一个个字典;
  • 分组后,用 values_list 取值,则返回值是 QuerySet 数据类型里面为一个个元组。

MySQL 中的 limit 相当于 ORM 中的 QuerySet 数据类型的切片。

annotate 里面放聚合函数。

  • values 或者 values_list 放在 annotate 前面:values 或者 values_list 是声明以什么字段分组,annotate 执行分组。
  • values 或者 values_list 放在annotate后面: annotate 表示直接以当前表的pk执行分组,values 或者 values_list 表示查询哪些字段, 并且要将 annotate 里的聚合函数起别名,在 values 或者 values_list 里写其别名。

实例

  • 统计每一个出版社的最便宜的书的价格:
1
2
3
4
res = models.Publish.objects.values("name").annotate(in_price = Min("book__price"))
print(res)

<QuerySet [{'name': '菜鸟出版社', 'in_price': Decimal('100.00')}, {'name': '明教出版社', 'in_price': Decimal('300.00')}]>
  • 统计每一本书的作者个数:
1
2
3
4
res = models.Book.objects.annotate(c = Count("authors__name")).values("title","c")
print(res)

<QuerySet [{'title': '菜鸟教程', 'c': 1}, {'title': '吸星大法', 'c': 1}, {'title': '冲灵剑法', 'c': 1}]>
  • 统计每一本以”菜”开头的书籍的作者个数:
1
2
res = models.Book.objects.filter(title__startswith="菜").annotate(c = Count("authors__name")).values("title","c")
print(res)
  • 统计不止一个作者的图书名称:
1
2
3
4
res = models.Book.objects.annotate(c = Count("authors__name")).filter(c__gt=0).values("title","c")
print(res)

<QuerySet [{'title': '菜鸟教程', 'c': 1}, {'title': '吸星大法', 'c': 1}, {'title': '冲灵剑法', 'c': 1}]>
  • 根据一本图书作者数量的多少对查询集 QuerySet 进行降序排序:
1
2
res = models.Book.objects.annotate(c = Count("authors__name")).order_by("-c").values("title","c")
print(res)
  • 查询各个作者出的书的总价格:
1
2
res = models.Author.objects.annotate(all = Sum("book__price")).values("name","all")
print(res)

F() 查询

  • F() 的实例可以在查询中引用字段,来比较同一个 model 实例中两个不同字段的值

  • F 动态获取对象字段的值,可以进行运算。

  • Django 支持 F() 对象之间以及 F() 对象和常数之间的加减乘除和取余的操作。

  • 修改操作(update)也可以使用 F() 函数。

实例

  • 查询工资大于年龄的人:
1
2
3
4
from django.db.models import F
...
book=models.Emp.objects.filter(salary__gt=F("age")).values("name","age")
...
  • 将每一本书的价格提高100元:
1
2
res = models.Book.objects.update(price=F("price")+100)
print(res)

Q() 查询

  • 之前构造的过滤器里的多个条件的关系都是 and,如果需要执行更复杂的查询(例如 or 语句),就可以使用 Q 。

  • Q 对象可以使用 & | ~ (与 或 非)操作符进行组合。优先级从高到低:~ & |。

实例

  • 查询价格大于 350 或者名称以菜开头的书籍的名称和价格。
1
2
3
4
from django.db.models import Q

res=models.Book.objects.filter(Q(price__gt=350)|Q(title__startswith="菜")).values("title","price")
print(res)
  • 查询以”菜”结尾或者不是 2010 年 10 月份的书籍:
1
2
res = models.Book.objects.filter(Q(title__endswith="菜") | ~Q(Q(pub_date__year=2010) & Q(pub_date__month=10)))
print(res)
  • 查询出版日期是 2004 或者 1999 年,并且书名中包含有”菜”的书籍。
1
2
res = models.Book.objects.filter(Q(pub_date__year=2004) | Q(pub_date__year=1999), title__contains="菜")
print(res)

Q 对象和关键字混合使用,Q 对象要在所有关键字的前面:

中间件-验证登录

  • StudyDjanog/TestModel/model.py 加入user
1
2
3
4
class Users(models.Model):
name = models.CharField(max_length=32)
pwd = models.CharField(max_length=32)

  • 生成表结构
1
2
python manage.py makemigrations TestModel  
python manage.py migrate TestModel
  • 登录代码 TestModel\views.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def user_login(request):
if request.method != "POST":
return JsonResponse({'code': -1, 'msg': 'method must is POST'})

data = json.loads(request.body)
user_name = data.get('username')
pwd = data.get("pwd")
print(data)
try:
user_entry = Users.objects.get(name=user_name, pwd=pwd)
if user_entry:
# 设置登录的session
request.session["user"] = user_name
return JsonResponse({'code': 1, 'msg': 'login is success'})
except ObjectDoesNotExist:
return JsonResponse({'code': -1, 'msg': 'login is fail111'})

  • 编写中间件 TestModel\middleware\authen.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from django.http import JsonResponse
from django.shortcuts import redirect,render
from django.utils.deprecation import MiddlewareMixin
import re

#白名单
#将登陆、登出、验证登陆请求设为白名单,不进行用户验证。
#这里设置了/static/静态文件,因为我这个项目静态文件没走nginx,如果静态文件通过nginx访问,可以不需要设置
exclued_path = ["/login/","/testModel/login/","/logout/","/login_ajax_check","/static/"]

#用来验证用户是否有权限登陆的中间件
class AuthenticationMiddle(MiddlewareMixin):
def process_request(self, request):
url_path = request.path
#如果不在请求在白名单里
if url_path not in exclued_path:
#如果未登陆,则调转到登陆页面,将请求的url作为next参数
# if not request.user.is_authenticated:
if not request.session.get("user"):
# return redirect("/login/?next={url_path}".format(url_path=url_path))
return JsonResponse({'code': -1, 'msg': 'login is failed2222'})
#如果已经登陆,则通过
else:
pass

  • 引用中间件 StudyDjanog\StudyDjanog\settings.py
1
2
3
4
5
6
7
8
9
10
11
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
# 'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'TestModel.middleware.authen.AuthenticationMiddle', # 引用自定义中间件

]
  • 测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import requests

data ={"username": "admin", "pwd": "123456"}
rs = requests.session()


resp = rs.post("http://127.0.0.1:8000/testModel/login/", json=data)
print(resp.text)

resp1 = rs.get("http://127.0.0.1:8000/testModel/userQuery")
print(resp1.text)


{"code": 1, "msg": "login is success"}
<p>runoob runoob </p>
  • 最终的session内容在django_session表中自动存储了

image-20230803172250422

其他

说明

  • 本次系列教程来自于这里

  • 本地环境

1
2
C:\Users\Administrator>python --version
Python 3.7.9

创建项目

  • 安装django
1
C:\Users\Administrator>python -m pip install Django
  • 查看版本
1
2
3
4
5
python

>>> import django
>>> django.get_version()
'3.1.3'
  • 使用django-admin 创建项目
1
E:\proj>django-admin startproject StudyDjango
  • 查看创建好的目录
1
2
3
4
5
6
7
8
9
E:\proj\StudyDjango>tree /f
│ manage.py

└─StudyDjango
asgi.py
settings.py
urls.py
wsgi.py
__init__.py
  • 启动服务器
1
E:\proj\StudyDjango>python manage.py runserver 0.0.0.0:8000
  • 浏览器打开127.0.0.1:8000

image-20230801144936413

视图和 URL 配置

在先前创建的 StudyDjango目录下的 StudyDjango目录新建一个 views.py 文件,并输入代码:

1
2
3
4
5
6
//StudyDjango/StudyDjango/views.py 文件代码:

from django.http import HttpResponse

def hello(request):
return HttpResponse("Hello world ! ")

接着,绑定 URL 与视图函数。打开 urls.py 文件,删除原来代码,将以下代码复制粘贴到 urls.py 文件中:

1
2
3
4
5
6
7
8
// StudyDjango/StudyDjango/urls.py
from django.conf.urls import url

from . import views

urlpatterns = [
url(r'^$', views.hello),
]

完成后,启动 Django 开发服务器,并在浏览器访问打开浏览器并访问:

image-20230801150043592

我们也可以修改以下规则:

1
2
3
4
5
6
7
8
9
10
11
// StudyDjango/StudyDjango/urls.py
from django.conf.urls import url
from django.urls import path

from . import views

urlpatterns = [
# url(r'^$', views.hello),
path('hello/', views.hello),

]

通过浏览器打开 http://127.0.0.1:8000/hello,输出结果如下:

image-20230801150312911

模板

  • 我们使用 django.http.HttpResponse() 来输出 “Hello World!”。该方式将数据与视图混合在一起,不符合 Django 的 MVC 思想。

  • 将在 StudyDjango目录底下创建 templates 目录并建立 runoob.html文件,runoob.html 文件代码如下:

1
2
3
// StudyDjango/templates/runoob.html 

<h1>{{ hello }}</h1>
  • 接下来我们需要向Django说明模板文件的路径,修改HelloWorld/settings.py,修改 TEMPLATES 中的 DIRS 为 [os.path.join(BASE_DIR, 'templates')],如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// StudyDjango/StudyDjango/settings.py 文件代码:

TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, 'templates')], # 修改位置
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
  • 我们现在修改 views.py,增加一个新的对象,用于向模板提交数据:
1
2
3
4
5
6
7
8
// StudyDjango/StudyDjango/views.py

from django.shortcuts import render

def runoob(request):
context = {}
context['hello'] = 'Hello World!'
return render(request, 'runoob.html', context)
  • 修改urls中的路由
1
2
3
4
5
6
7
8
9
10
11
12
13
// StudyDjango/StudyDjango/urls.py

from django.urls import path

from . import views

urlpatterns = [
# url(r'^$', views.hello),
# path('hello/', views.hello),
path('runoob/', views.runoob),

]

  • 访问最新地址

image-20230801152821208

  • 更多模板语法参考这里

模型

  • Django 对各种数据库提供了很好的支持,包括:PostgreSQL、MySQL、SQLite、Oracle。

  • Django 为这些数据库提供了统一的调用API。 我们可以根据自己业务需求选择不同的数据库

  • 本次模型使用mysql,需要安装mysq驱动

1
E:\proj\StudyDjango>pip3 install pymysql

本地需要搭建好Mysql环境

Django ORM

  • 本地数据库中需要提前创建数据库,因为ORM 无法操作到数据库级别,只能到表。

  • 使用heidisql连接本地mysql数据库后,新建一个数据库,名称为:runoob

  • 我们在项目的 settings.py 文件中找到 DATABASES 配置项,将其信息修改为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// StudyDjango/StudyDjango/settings.py

DATABASES = {
# 'default': {
# 'ENGINE': 'django.db.backends.sqlite3',
# 'NAME': BASE_DIR / 'db.sqlite3',
# }
'default':
{
'ENGINE': 'django.db.backends.mysql', # 数据库引擎
'NAME': 'runoob', # 数据库名称
'HOST': '127.0.0.1', # 数据库地址,本机 ip 地址 127.0.0.1
'PORT': 3306, # 端口
'USER': 'root', # 数据库用户名
'PASSWORD': '123456', # 数据库密码
}
}
  • 在与 settings.py 同级目录下的 __init__.py 中引入模块和进行配置
1
2
3
4
// StudyDjango/StudyDjango/__init__.py

import pymysql
pymysql.install_as_MySQLdb()

定义模型

创建APP

  • Django 规定,如果要使用模型,必须要创建一个 app。我们使用以下命令创建一个 TestModel 的 app:
1
E:\proj\StudyDjango>django-admin startapp TestModel

image-20230801154926128

  • 我们修改 TestModel/models.py 文件,代码如下:
1
2
3
4
5
6
from django.db import models


class Test(models.Model):
name = models.CharField(max_length=20)

以上的类名代表了数据库表名(test),且继承了models.Model,类里面的字段代表数据表中的字段(name),数据类型则由CharField(相当于varchar)、DateField(相当于datetime), max_length 参数限定长度。

  • 接下来在 settings.py 中找到INSTALLED_APPS这一项,如下:
1
2
3
4
5
6
7
8
9
10
11
// StudyDjango\StudyDjango\settings.py

INSTALLED_APPS = (
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'TestModel', # 添加此项
)
  • 在命令行中运行, 创建表结构,发现报错了
1
2
3
4
5
E:\proj\StudyDjango>python manage.py migrate

File "D:\app\Python37\lib\site-packages\django\db\backends\mysql\base.py", line 36, in <module>
raise ImproperlyConfigured('mysqlclient 1.4.0 or newer is required; you have %s.' % Database.__version__)
django.core.exceptions.ImproperlyConfigured: mysqlclient 1.4.0 or newer is required; you have 0.10.1.
  • 原因是 MySQLclient 目前只支持到 Python3.4,因此如果使用的更高版本的 python,把D:\app\Python37\Lib\site-packages\django\db\backends\mysql\base.py 对应报错代码注释

  • 再次执行

1
2
3
4
5
6

E:\proj\StudyDjango> python manage.py migrate # 创建表结构

E:\proj\StudyDjango> python manage.py makemigrations TestModel # 让 Django 知道我们在我们的模型有一些变更
E:\proj\StudyDjango> python manage.py migrate TestModel # 创建表结构

  • 查看表已经生成成功

image-20230801163314750

数据库操作

操作模型

新增数据

1
2
3
4
5
6
7
8
9
10
11
// StudyDjango/StudyDjango/testdb.py
from django.http import HttpResponse

from TestModel.models import Test


# 数据库操作
def testdb(request):
test1 = Test(name='runoob')
test1.save()
return HttpResponse("<p>数据添加成功!</p>")
  • 下来我们在 StudyDjango目录中添加 testdb.py 文件到urls.py中
1
2
3
4
5
6
7
8
9
10
11
from django.urls import path

from . import views,testdb

urlpatterns = [
# url(r'^$', views.hello),
# path('hello/', views.hello),
path('runoob/', views.runoob),
path('testdb/', testdb.testdb),

]
  • 访问 http://127.0.0.1:8000/testdb 就可以看到数据添加成功的提示。

image-20230801170113854

查询数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
from django.http import HttpResponse

from TestModel.models import Test

# 数据库操作
def testdb(request):
# 初始化
response = ""
response1 = ""


# 通过objects这个模型管理器的all()获得所有数据行,相当于SQL中的SELECT * FROM
list = Test.objects.all()

# filter相当于SQL中的WHERE,可设置条件过滤结果
response2 = Test.objects.filter(id=1)

# 获取单个对象
response3 = Test.objects.get(id=1)

# 限制返回的数据 相当于 SQL 中的 OFFSET 0 LIMIT 2;
Test.objects.order_by('name')[0:2]

#数据排序
Test.objects.order_by("id")

# 上面的方法可以连锁使用
Test.objects.filter(name="runoob").order_by("id")

# 输出所有数据
for var in list:
response1 += var.name + " "
response = response1
return HttpResponse("<p>" + response + "</p>")

更新

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from django.http import HttpResponse

from TestModel.models import Test

# 数据库操作
def testdb(request):
# 修改其中一个id=1的name字段,再save,相当于SQL中的UPDATE
test1 = Test.objects.get(id=1)
test1.name = 'Google'
test1.save()

# 另外一种方式
#Test.objects.filter(id=1).update(name='Google')

# 修改所有的列
# Test.objects.all().update(name='Google')

return HttpResponse("<p>修改成功</p>")

删除

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from django.http import HttpResponse

from TestModel.models import Test

# 数据库操作
def testdb(request):
# 删除id=1的数据
test1 = Test.objects.get(id=1)
test1.delete()

# 另外一种方式
# Test.objects.filter(id=1).delete()

# 删除所有数据
# Test.objects.all().delete()

return HttpResponse("<p>删除成功</p>")

路由

  • Django 项目里多个app目录共用一个 urls 容易造成混淆,后期维护也不方便
  • 在项目名(TestModel)中新增一个 TestModel/urls.py
1
2
3
4
5
6
7
8
from django.urls import path,re_path
from django.conf.urls import url

from TestModel import views # 从自己的 app 目录引入 views
urlpatterns = [
url(r'^userAdd/', views.userAdd),
path('userQuery/', views.userQuery),
]
  • TestModel/view.py 编写具体代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
from django.http import HttpResponse
from django.shortcuts import render

# Create your views here.
from TestModel.models import Test


def userAdd(request):
test1 = Test(name='runoob')
test1.save()
return HttpResponse("<p>数据添加成功!</p>")

def userQuery(request):
# 初始化
response = ""
response1 = ""

# 通过objects这个模型管理器的all()获得所有数据行,相当于SQL中的SELECT * FROM
list = Test.objects.all()

# filter相当于SQL中的WHERE,可设置条件过滤结果
response2 = Test.objects.filter(id=1)

# 获取单个对象
response3 = Test.objects.get(id=1)

# 限制返回的数据 相当于 SQL 中的 OFFSET 0 LIMIT 2;
Test.objects.order_by('name')[0:2]

# 数据排序
Test.objects.order_by("id")

# 上面的方法可以连锁使用
Test.objects.filter(name="runoob").order_by("id")

# 输出所有数据
for var in list:
response1 += var.name + " "
response = response1
return HttpResponse("<p>" + response + "</p>")
  • StudyDiango/StudyDiango/urls.py 中引用具体项目的url
1
2
3
4
5
6
7
8
9
10
11
12
13
from django.conf.urls import url
from django.urls import path, include

from . import views,testdb

urlpatterns = [
# url(r'^$', views.hello),
# path('hello/', views.hello),
path('runoob/', views.runoob),
path('testdb/', testdb.testdb),
path('testModel/', include("TestModel.urls")),

]
  • 浏览器打开http://127.0.0.1:8000/testModel/userQuery/

image-20230802105812991

ORM多表实践

表结构

书籍表 Book:title 、 price 、 pub_date 、 publish(外键,多对一) 、 authors(多对多)

出版社表 Publish:name 、 city 、 email

作者表 Author:name 、 age 、 au_detail(一对一)

作者详情表 AuthorDetail:gender 、 tel 、 addr 、 birthday

  • 关系图

image-20230802104212169

创建模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class Book(models.Model):
title = models.CharField(max_length=32)
price = models.DecimalField(max_digits=5, decimal_places=2)
pub_date = models.DateField()
# 关联出版社一对多,意思就是一个出版社可以有印刷多本书
publish = models.ForeignKey("Publish", on_delete=models.CASCADE)
# 多对多,意思就是多个作者可以编写多本书
authors = models.ManyToManyField("Author")


class Publish(models.Model):
name = models.CharField(max_length=32)
city = models.CharField(max_length=64)
email = models.EmailField()


class Author(models.Model):
name = models.CharField(max_length=32)
age = models.SmallIntegerField()
# 一对一,意思为一个作者对应一个作者详情
au_detail = models.OneToOneField("AuthorDetail", on_delete=models.CASCADE)


class AuthorDetail(models.Model):
gender_choices = (
(0, "女"),
(1, "男"),
(2, "保密"),
)
gender = models.SmallIntegerField(choices=gender_choices)
tel = models.CharField(max_length=32)
addr = models.CharField(max_length=64)
birthday = models.DateField()
  • 生成表结构
1
2
E:\proj\StudyDjango> python manage.py makemigrations TestModel  # 让 Django 知道我们在我们的模型有一些变更
E:\proj\StudyDjango> python manage.py migrate TestModel # 创建表

image-20230802110229182

注意testmode_book_authors 为book多对多authors的表

插入数据

1
2
3
4
5
6
7
8
# 插入出版社
insert into testmodel_publish(name,city,email) values ("华山出版社", "华山", "hs@163.com"), ("明教出版社", "黑木崖", "mj@163.com")

# 先插入 authordetail 表中多数据
insert into testmodel_authordetail(gender,tel,addr,birthday) values (1,13432335433,"华山","1994-5-23"), (1,13943454554,"黑木崖","1961-8-13"), (0,13878934322,"黑木崖","1996-5-20")

# 再将数据插入 author,这样 author 才能找到 authordetail
insert into testmodel_author(name,age,au_detail_id) values ("令狐冲",25,1), ("任我行",58,2), ("任盈盈",23,3)

ORM添加数据

一对多(外键 ForeignKey)

方式一: 传对象的形式,返回值的数据类型是对象,书籍对象。

步骤:

  • a. 获取出版社对象
  • b. 给书籍的出版社属性 pulish 传出版社对象
1
2
3
4
5
6
7
8
9
// testmode/views.py

def add_book(request):
# 获取出版社对象
pub_obj = models.Publish.objects.filter(pk=1).first()
# 给书籍的出版社属性publish传出版社对象
book = models.Book.objects.create(title="菜鸟教程", price=200, pub_date="2010-10-10", publish=pub_obj)
print(book, type(book))
return HttpResponse(book)

方式二: 传对象 id 的形式(由于传过来的数据一般是 id,所以传对象 id 是常用的)。

一对多中,设置外键属性的类(多的表)中,MySQL 中显示的字段名是:外键属性名_id

返回值的数据类型是对象,书籍对象。

步骤:

  • a. 获取出版社对象的 id
  • b. 给书籍的关联出版社字段 pulish_id 传出版社对象的 id
1
2
3
4
5
6
7
8
9
def add_book(request):
# 获取出版社对象
pub_obj = models.Publish.objects.filter(pk=1).first()
# 获取出版社对象的id
pk = pub_obj.pk
# 给书籍的关联出版社字段 publish_id 传出版社对象的id
book = models.Book.objects.create(title="冲灵剑法", price=100, pub_date="2004-04-04", publish_id=pk)
print(book, type(book))
return HttpResponse(book)
  • testmodel/urls.py 引用views.py代码
1
2
3
4
5
6
7
8
9
from django.urls import path,re_path
from django.conf.urls import url

from TestModel import views # 从自己的 app 目录引入 views
urlpatterns = [
url(r'^userAdd/', views.userAdd),
path('userQuery/', views.userQuery),
path('add_book/', views.add_book),
]

多对多(ManyToManyField):

  • 在第三张关系表中新增数据

方式一: 传对象形式,无返回值。

步骤:

  • a. 获取作者对象
  • b. 获取书籍对象
  • c. 给书籍对象的 authors 属性用 add 方法传作者对象
1
2
3
4
5
6
7
8
9
10
def add_books(request):
# 获取作者对象
chong = models.Author.objects.filter(name="令狐冲").first()
ying = models.Author.objects.filter(name="任盈盈").first()
# 获取书籍对象
book = models.Book.objects.filter(title="菜鸟教程").first()
# 给书籍对象的 authors 属性用 add 方法传作者对象
# 菜鸟教程这本书有两个作者
book.authors.add(chong, ying)
return HttpResponse(book)

方式二: 传对象id形式,无返回值。

步骤:

  • a. 获取作者对象的 id
  • b. 获取书籍对象
  • c. 给书籍对象的 authors 属性用 add 方法传作者对象的 id
1
2
3
4
5
6
7
8
9
def add_books_1(request):
# 获取作者对象
chong = models.Author.objects.filter(name="令狐冲").first()
# 获取作者对象的id
pk = chong.pk
# 获取书籍对象
book = models.Book.objects.filter(title="冲灵剑法").first()
# 给书籍对象的 authors 属性用 add 方法传作者对象的id
book.authors.add(pk)

关联管理器(对象调用)

前提:

  • 多对多(双向均有关联管理器)
  • 一对多(只有多的那个类的对象有关联管理器,即反向才有)

语法格式:

1
2
正向:属性名,如add,create等
反向:小写类名加 _set

一对多只能反向

  • add。用于多对多,把指定的模型对象添加到关联对象集(关系表)中。
1
2
3
4
5
6
7
# 方式一:传对象
book_obj = models.Book.objects.get(id=10)
author_list = models.Author.objects.filter(id__gt=2)
book_obj.authors.add(*author_list) # 将 id 大于2的作者对象添加到这本书的作者集合中
# 方式二:传对象 id
book_obj.authors.add(*[1,3]) # 将 id=1 和 id=3 的作者对象添加到这本书的作者集合中
return HttpResponse("ok")
  • 反向:小写表名_set
1
2
3
4
ying = models.Author.objects.filter(name="任盈盈").first()
book = models.Book.objects.filter(title="冲灵剑法").first()
ying.book_set.add(book)
return HttpResponse("ok")

create:创建一个新的对象,并同时将它添加到关联对象集之中。

1
2
3
4
5
pub = models.Publish.objects.filter(name="明教出版社").first()
wo = models.Author.objects.filter(name="任我行").first()
book = wo.book_set.create(title="吸星大法", price=300, pub_date="1999-9-19", publish=pub)
print(book, type(book))
return HttpResponse("ok")

remove:从关联对象集中移除执行的模型对象。

1
2
3
4
author_obj =models.Author.objects.get(id=1)
book_obj = models.Book.objects.get(id=11)
author_obj.book_set.remove(book_obj)
return HttpResponse("ok")

clear:从关联对象集中移除一切对象,删除关联,不会删除对象。

1
2
3
#  清空独孤九剑关联的所有作者
book = models.Book.objects.filter(title="菜鸟教程").first()
book.authors.clear()

对于 ForeignKey 对象,这个方法仅在 null=True(可以为空)时存在。

ORM 查询

一对多

查询主键为 10 的书籍的出版社所在的城市(正向)。

1
2
3
4
book = models.Book.objects.filter(pk=10).first()
res = book.publish.city
print(res, type(res))
return HttpResponse("ok")
  • 查询明教出版社出版的书籍名(反向)。
1
2
3
4
5
6
pub = models.Publish.objects.filter(name="明教出版社").first()
# pub.book_set.all():取出书籍表的所有书籍对象,在一个 QuerySet 里,遍历取出一个个书籍对象。
res = pub.book_set.all()
for i in res:
print(i.title)
return HttpResponse("ok")

一对一

查询令狐冲的电话(正向

正向:对象.属性 (author.au_detail) 可以跳转到关联的表(作者详情表)

1
2
3
4
author = models.Author.objects.filter(name="令狐冲").first()
res = author.au_detail.tel
print(res, type(res))
return HttpResponse("ok")

查询所有住址在黑木崖的作者的姓名(反向)。

1
2
3
4
addr = models.AuthorDetail.objects.filter(addr="黑木崖").first()
res = addr.author.name
print(res, type(res))
return HttpResponse("ok")

多对多

  • 菜鸟教程所有作者的名字以及手机号(正向)。

  • 正向:**对象.属性(book.authors)**可以跳转到关联的表(作者表)。

  • 作者表里没有作者电话,因此再次通过**对象.属性(i.au_detail)**跳转到关联的表(作者详情表)

1
2
3
4
5
book = models.Book.objects.filter(title="菜鸟教程").first()
res = book.authors.all()
for i in res:
print(i.name, i.au_detail.tel)
return HttpResponse("ok")
  • 查询任我行出过的所有书籍的名字(反向)。
1
2
3
4
5
author = models.Author.objects.filter(name="任我行").first()
res = author.book_set.all()
for i in res:
print(i.title)
return HttpResponse("ok")

基于双下划线的跨表查询

正向:属性名称__跨表的属性名称

反向:小写类名__跨表的属性名称

一对多

正向:查询菜鸟出版社出版过的所有书籍的名字与价格。

1
2
# publish__name 跨表中的name
res = models.Book.objects.filter(publish__name="菜鸟出版社").values_list("title", "price")

反向:通过 小写类名__跨表的属性名称(book__title,book__price) 跨表获取数据。

1
2
res = models.Publish.objects.filter(name="菜鸟出版社").values_list("book__title","book__price")
return HttpResponse("ok")

多对多

查询任我行出过的所有书籍的名字。

正向:通过 属性名称__跨表的属性名称(authors__name) 跨表获取数据:

1
res = models.Book.objects.filter(authors__name="任我行").values_list("title")

反向:通过 小写类名__跨表的属性名称(book__title) 跨表获取数据:

1
res = models.Author.objects.filter(name="任我行").values_list("book__title")

一对一

查询任我行的手机号。

正向:通过 属性名称__跨表的属性名称(au_detail__tel) 跨表获取数据。

1
res = models.Author.objects.filter(name="任我行").values_list("au_detail__tel")

反向:通过 小写类名__跨表的属性名称(author__name) 跨表获取数据。

1
res = models.AuthorDetail.objects.filter(author__name="任我行").values_list("tel")

总结

  • 主要介绍了新建项目,应用,路由,连接数据库,对model进行增删改查等操作

说明

  • 本次笔记主要记录部署go
  • 服务信息为centos 7
1
2
3
[root@VM-24-13-centos home]# uname -a
Linux VM-24-13-centos 3.10.0-1160.11.1.el7.x86_64 #1 SMP Fri Dec 18 16:34:56 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux

直接部署

  • 分别为下载,解压go
1
2
3
4
5
6
7
8
[root@VM-24-13-centos /]# cd home
[root@VM-24-13-centos home]# wget https://dl.google.com/go/go1.20.4.linux-amd64.tar.gz
[root@VM-24-13-centos home]# tar -zxvf go1.20.4.linux-amd64.tar.gz
[root@VM-24-13-centos bin]# pwd
/home/go/bin
[root@VM-24-13-centos bin]# ./go version
go version go1.20.4 linux/amd64

  • 配置go的环境变量
1
2
3
4
5
6
7
8
9
10
[root@VM-24-13-centos bin]# vi /etc/profile 

export GOPATH=/home/go
export PATH=$PATH:/usr/local/go/bin:$GOPATH/bin

# 生效环境变量
[root@VM-24-13-centos bin]# source profile

[root@VM-24-13-centos etc]# go version
go version go1.20.4 linux/amd64
  • 服务器需要搭建好mysql环境,以及建好数据库,可以参考这篇文章中的MySQL安装过程

image-20230731105226623

  • 使用 Git 克隆你的 Gin 项目到服务器上,并设置go的代理
1
2
3
4
5
6
7
8
9
[root@VM-24-13-centos home]# git clone https://github.com/Louis-me/studyGin.git

[root@VM-24-13-centos home]# cd studyGin/
[root@VM-24-13-centos home]#go env -w GO111MODULE=on
[root@VM-24-13-centos home]# go env -w GOPROXY=https://proxy.golang.com.cn,direct
# 编译下载依赖文件
[root@VM-24-13-centos studyGin]# go build
# 启动项目
[root@VM-24-13-centos studyGin]# ./myGin
  • 注意gin的服务端口信息为8080,防火墙需要打开,若时云服务器那么规则端口也要打开
1
2
3
4
5
6
firewall-cmd --zone=public --add-port=8080/tcp --permanent
firewall-cmd --reload # 配置立即生效
firewall-cmd --zone=public --list-port # 查看防火墙所有开放的端口
firewall-cmd --state # 查看防火墙状态
netstat -lnpt # 查看监听的端口
netstat -lnpt |grep 8080 # 查看监听的具体端口
  • 测试
1
2
3
4
5
6
7
8
9
10
import requests
import json
data ={"name": "admin", "password": "123456"}
resp = requests.post("http://182.XXX.XXX.XXX:8080/login", data=data)
print(resp.text)
dic = json.loads(resp.text)
token = dic["data"]["Token"]
header = {"token": token}
resp2 = requests.get("http://182.XXX.XXX.XXXX:8080/GetUserList", headers=header)
print(resp2.text)
  • 如果需要在后台运行项目,并且在关闭终端时项目依然保持运行状态,可以使用 nohup 命令:
1
2
3
4
5
[root@VM-24-13-centos studyGin]# ./myGin

[root@VM-24-13-centos studyGin]# nohup ./myGin > gin.log &
[2] 16486
[root@VM-24-13-centos studyGin]# nohup: 忽略输入重定向错误到标准输出端

说明

  • 入门实践一 介绍了单体服务和微服务的简单实例,入门实践二 介绍了单体服务结合mysql,redis,jwt等方便的知识
  • 本篇主要介绍为微服务的具体使用场景

user rpc

  • 把user/rpc 目录内容清空
  • user/rpc/user.proto 内容如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
syntax = "proto3";

package user;

// protoc-gen-go 版本大于1.4.0, proto文件需要加上go_package,否则无法生成
option go_package = "./user";

message IdRequest {
int64 id = 1;
}

message UserResponse {
// 用户id
int64 id = 1;
// 用户名称
string username = 2;
// 用户性别
string gender = 3;
}

service User {
rpc getUser(IdRequest) returns(UserResponse); //对外开发的接口
}
  • 生成rpc目录和代码
1
E:\proj\gowork\study-gozero-demo\mall\user\rpc> goctl rpc protoc user.proto --go_out=. --go-grpc_out=. --zrpc_out=.
  • 配置user/rpc/internal/config/config.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package config

import (
"github.com/zeromicro/go-zero/core/stores/cache"
"github.com/zeromicro/go-zero/zrpc"
)

type Config struct {
zrpc.RpcServerConf

// 添加mysql配置
Mysql struct {
DataSource string
}
CacheRedis cache.CacheConf
}

  • 配置/user/rpc/etc/user.yaml
1
2
3
4
5
6
7
8
9
10
11
12
13
Name: user.rpc
ListenOn: 0.0.0.0:8081
Etcd:
Hosts:
- 127.0.0.1:2379
Key: user.rpc
# 加入MySQL连接字符串
Mysql:
DataSource: root:123456@tcp(127.0.0.1:3306)/go_zero?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai
CacheRedis:
- Host: 127.0.0.1:6379
Pass: ""
Type: node
  • 添加资源依赖/user/rpc/internal/svc/servicecontext.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package svc
import (
"study-gozero-demo/mall/user/model"
"study-gozero-demo/mall/user/rpc/internal/config"
"github.com/zeromicro/go-zero/core/stores/sqlx"
)
type ServiceContext struct {
Config config.Config
// 引入model层的增删改查
UserModel model.UserModel
}
func NewServiceContext(c config.Config) *ServiceContext {
// 初始化mysql连接
conn := sqlx.NewMysql(c.Mysql.DataSource)
return &ServiceContext{
Config: c,
// 调用user/model层代码
UserModel: model.NewUserModel(conn, c.CacheRedis),
}

}

如上那些配置其实和api层的配置基本上一样,都是加入mysql,引用mode层的代码

  • 添加rpc逻辑user/rpc/internal/logic/getuserlogic.go
1
2
3
4
5
6
7
8
9
10
11
12
13
func (l *GetUserLogic) GetUser(in *user.IdRequest) (*user.UserResponse, error) {
// todo: add your logic here and delete this line
one, err := l.svcCtx.UserModel.FindOne(l.ctx, in.Id)
if err != nil {
return nil, err
}
return &user.UserResponse{
Id: one.Id,
Username: one.Username.String,
Gender: one.Gender.String,
}, nil
}

order api

配置

  • 关于生成model代码,mysql等配置就省略了,和user api的配置一模一样,注意需要保留之前jwt配置
  • 本地数据库需要加上order表,表中字段为:id,productname,price,unit
  • 编写order/api/oder.api
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
type (
OrderReq {
Id int `json:"id"`
UserId int `json:"userid"`
}

OrderReply {
Id int `json:"id"`
Productname string `json:"productname"`
}

OrderAddReq {
Productname string `json:"productname"`
Price string `json:"price"`
Unit string `json:"unit"`
}

OrderAddRes {
Code int `json:"code"`
Messsage string `json:"message"`
}
)

// 在哪个服务头上加这个验证,就是需要jwt鉴权
@server(
jwt: Auth
)
service order {
@handler orderAdd
post /api/order/OrderAdd (OrderAddReq) returns (OrderAddRes)
}

service order {
@handler getOrder
// get /api/order/get/:id (OrderReq) returns (OrderReply)
get /api/order/get (OrderReq) returns (OrderReply)

}
  • 添加UserRpc配置及yaml配置项,order/api/internal/config/config.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package config

import (
"github.com/zeromicro/go-zero/core/stores/cache"
"github.com/zeromicro/go-zero/rest"
"github.com/zeromicro/go-zero/zrpc"
)

type Config struct {
rest.RestConf
// 添加mysql配置
Mysql struct {
DataSource string
}
// 使用缓存
CacheRedis cache.CacheConf
Auth struct {
AccessSecret string
AccessExpire int64
}
// 引用user rpc
UserRpc zrpc.RpcClientConf
}

  • order/api/etc/order.yaml 加入user rpc配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Name: order
Host: 0.0.0.0
Port: 8889
Mysql:
DataSource: root:123456@tcp(127.0.0.1:3306)/go_zero?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai
CacheRedis:
- Host: 127.0.0.1:6379
Pass: ""
Type: node
Auth:
# 生成jwt token的密钥 一般格式为udid
AccessSecret: 0c891c78-9415-ec96-deb8-3b658f9e57f3
# jwt token有效期,单位:秒,现在设置的为1小时
AccessExpire: 3600
# 加入user rpc
UserRpc:
Etcd:
Hosts:
- 127.0.0.1:2379
Key: user.rpc
  • order/api/internal/svc/servicecontext.go 添加依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package svc

import (
"study-gozero-demo/mall/order/api/internal/config"
"study-gozero-demo/mall/order/model"
"study-gozero-demo/mall/user/rpc/userclient"

"github.com/zeromicro/go-zero/core/stores/sqlx"
"github.com/zeromicro/go-zero/zrpc"
)

type ServiceContext struct {
Config config.Config
// 引入model层的增删改查
OrderModel model.OrderModel
// 引用最终生成user服务代码路径
UserRpc userclient.User
}

func NewServiceContext(c config.Config) *ServiceContext {
// 初始化mysql连接
conn := sqlx.NewMysql(c.Mysql.DataSource)
return &ServiceContext{
Config: c,
// 调用order/model层代码
OrderModel: model.NewOrderModel(conn, c.CacheRedis),
// 调用user rpc
UserRpc: userclient.NewUser(zrpc.MustNewClient(c.UserRpc)),
}
}

逻辑编写

  • 编写order\api\internal\logic\orderaddlogic.go 业务代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import (
"context"
"errors"
"fmt"

"study-gozero-demo/mall/order/api/internal/svc"
"study-gozero-demo/mall/order/api/internal/types"
"study-gozero-demo/mall/order/model"

"github.com/jinzhu/copier"
"github.com/zeromicro/go-zero/core/logx"
)
...


func (l *OrderAddLogic) OrderAdd(req *types.OrderAddReq) (resp *types.OrderAddRes, err error) {
// todo: add your logic here and delete this line
var addReq model.Order
errCopy := copier.Copy(&addReq, req)
if errCopy != nil {
return nil, errCopy
}

respd, err2 := l.svcCtx.OrderModel.Insert(l.ctx, &addReq)
fmt.Println("------respd:", respd, "error:", err2)
if err2 != nil {
fmt.Println("insert into error:", err)
return nil, errors.New(err.Error())

}

// 登录成功返回数据
return &types.OrderAddRes{
Code: 1,
Messsage: "success",
}, nil

}
  • 编写order\api\internal\logic\getorderlogic.go 业务代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import (
"context"
"errors"
"fmt"

"study-gozero-demo/mall/order/api/internal/svc"
"study-gozero-demo/mall/order/api/internal/types"
"study-gozero-demo/mall/user/rpc/user"

"github.com/zeromicro/go-zero/core/logx"
)
...

func (l *GetOrderLogic) GetOrder(req *types.OrderReq) (resp *types.OrderReply, err error) {
// todo: add your logic here and delete this line

// 使用user rpc
// 新增代码,注意这里的&user,应用的是user rpc的user/user.pb.go中内容,IdRequest其实就是对应入参(user.proto定义的)

_, err = l.svcCtx.UserRpc.GetUser(l.ctx, &user.IdRequest{
Id: int64(req.UserId),
})
if err != nil {
return nil, errors.New("用户不存在")
}

userInfo, err2 := l.svcCtx.OrderModel.FindOne(l.ctx, int64(req.Id))
fmt.Println("userInfo=", userInfo, "error:", err2)
if userInfo == nil {
fmt.Println("iiiiiiii")
return nil, errors.New("订单不存在")
}
return &types.OrderReply{
Id: req.Id,
ProductName: userInfo.Productname.String,
}, nil

}

启动各个服务

  • 启动user rpc,主要验证用户是否存在
1
2
PS E:\proj\gowork\study-gozero-demo\mall\user\rpc> go run user.go
Starting rpc server at 0.0.0.0:8080...
  • 启动user api,用来登录返回token
1
2
PS E:\proj\gowork\study-gozero-demo\mall\user\api> go run .\user.go
Starting server at 0.0.0.0:8888.
  • 启动order api,对外的接口
1
2
3
PS E:\proj\gowork\study-gozero-demo\mall\order\api> go run .\order.go
Starting server at 0.0.0.0:8889...
{"@timestamp":"2023-07-30T16:03:50.226+08:00","cal

测试

  • 测试GetOrder的接口
    • 如果传递的userid错误,就返回用户不存在错误
    • 如果传递的id错误,就返回的订单d不存在
    • 如果userid和id正确,就返回订单的信息
1
2
3
4
5
6
7
8
9
import requests
import json

data2 = {"userid": 20, "id": 1}
resp = requests.get("http://127.0.0.1:8889/api/order/get", json=data2)
print(resp.text)

{"id":1,"productname":"西瓜"}

  • 测试OrderAdd的接口
    • 如果不带正确token的头部,就返回401
    • 如果传递带正确token的头部,就返回新增成功信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
data1 ={"productname":"西瓜","price": "122", "unit": "个"}
resp = requests.post("http://127.0.0.1:8889/api/order/OrderAdd", json=data1)
print(resp)

<Response [401]>

data ={"username": "admin", "password": "123456"}
resp = requests.post("http://127.0.0.1:8888/user/login", json=data)
data = json.loads(resp.text)
token = data["accessToken"]
header = {"Authorization": token}
data1 ={"productname":"西瓜","price": "122", "unit": "个"}
resp = requests.post("http://127.0.0.1:8889/api/order/OrderAdd", headers=header,json=data1)
print(resp.text)

{"code":1,"message":"success"}

自定义错误

业务错误响应格式

  • 业务处理正常
1
2
3
4
5
6
7
{
"code": 0,
"msg": "successful",
"data": {
....
}
}
  • 业务处理异常
1
2
3
4
{
"code": 10001,
"msg": "参数错误"
}

user api之login

  • 在之前,我们在登录逻辑中处理用户名不存在时,直接返回来一个error。我们来登录并传递一个不存在的用户名看看效果。
1
2
3
4
5
6
7
8
9
curl -X POST \
http://127.0.0.1:8888/user/login \
-H 'content-type: application/json' \
-d '{
"username":"1",
"password":"123456"
}'

用户名不存在

接下来我们将其以json格式进行返回

自定义错误

  • 首先在common中添加一个baseerror.go文件,并填入代码
1
2
3
mkdir common
cd common
mkdir errorx&&cd errorx
  • 编写自定义代码mall\common\errorx\baseerror.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package errorx

const defaultCode = 1001

type CodeError struct {
Code int `json:"code"`
Msg string `json:"msg"`
}

type CodeErrorResponse struct {
Code int `json:"code"`
Msg string `json:"msg"`
}

func NewCodeError(code int, msg string) error {
return &CodeError{Code: code, Msg: msg}
}

func NewDefaultError(msg string) error {
return NewCodeError(defaultCode, msg)
}

func (e *CodeError) Error() string {
return e.Msg
}

func (e *CodeError) Data() *CodeErrorResponse {
return &CodeErrorResponse{
Code: e.Code,
Msg: e.Msg,
}
}

  • 将登录逻辑中错误用CodeError自定义错误替换
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
func (l *LoginLogic) Login(req *types.LoginReq) (resp *types.LoginReply, err error) {
// todo: add your logic here and delete this line

if len(strings.TrimSpace(req.Username)) == 0 || len(strings.TrimSpace(req.Password)) == 0 {
// return nil, errors.New("参数错误")
return nil, errorx.NewDefaultError("参数错误")

}

userInfo, err := l.svcCtx.UserModel.FindOneByUsername(l.ctx, req.Username)
fmt.Println("userInfo=", userInfo, "error:", err)
switch err {
case nil:
case model.ErrNotFound:
// return nil, errors.New("用户名不存在")
return nil, errorx.NewDefaultError("用户名不存在")

default:
return nil, err
}
fmt.Println("pwd:", userInfo.Password.String)
fmt.Println("pwd:", req.Password)

if userInfo.Password.String != req.Password {
// return nil, errors.New("用户密码不正确")
return nil, errorx.NewDefaultError("用户密码不正确")

}

// ---jwt-start---

now := time.Now().Unix()
accessExpire := l.svcCtx.Config.Auth.AccessExpire
jwtToken, err := l.getJwtToken(l.svcCtx.Config.Auth.AccessSecret, now, l.svcCtx.Config.Auth.AccessExpire, userInfo.Id)
if err != nil {
return nil, err
}
// ---jwt-end---

// 登录成功返回数据
return &types.LoginReply{
Id: userInfo.Id,
Username: userInfo.Username.String,
AccessToken: jwtToken,
AccessExpire: now + accessExpire,
RefreshAfter: now + accessExpire/2,
}, nil
}
  • 开启自定义错误,配置user/api/user.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package main

import (
"flag"
"fmt"
"net/http"

"study-gozero-demo/mall/common/errorx"
"study-gozero-demo/mall/user/api/internal/config"
"study-gozero-demo/mall/user/api/internal/handler"
"study-gozero-demo/mall/user/api/internal/svc"

"github.com/zeromicro/go-zero/core/conf"
"github.com/zeromicro/go-zero/rest"
"github.com/zeromicro/go-zero/rest/httpx"
)

var configFile = flag.String("f", "etc/user-api.yaml", "the config file")

func main() {
flag.Parse()

var c config.Config
conf.MustLoad(*configFile, &c)

server := rest.MustNewServer(c.RestConf)
defer server.Stop()

ctx := svc.NewServiceContext(c)
handler.RegisterHandlers(server, ctx)
// 自定义错误
httpx.SetErrorHandler(func(err error) (int, interface{}) {
switch e := err.(type) {
case *errorx.CodeError:
return http.StatusOK, e.Data()
default:
return http.StatusInternalServerError, nil
}
})


fmt.Printf("Starting server at %s:%d...\n", c.Host, c.Port)
server.Start()
}

  • 重新运行user\api\user.go

  • 再次测试

1
2
3
4
5
6
 curl -X POST   http://127.0.0.1:8888/user/login   -H 'content-type: application/json'   -d '{
"username":"1",
"password":"123456"
}'
{"code":1001,"msg":"用户名不存在"}

说明

  • 紧接上篇文章
  • 本篇文章开始接入mysql等具体的业务逻辑

项目结构

  • order 和 user 目录下分别新增model、api目录等

image-20230727101351298

单体服务

  • user为单体api服务,编写对外的增删改查接口
  • user/model目录下的sql脚本,自动生成model代码
1
2
3
4
5
6
7
8
9
10
CREATE TABLE `user` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`username` VARCHAR(50) NULL DEFAULT NULL COLLATE 'utf8_general_ci',
`password` TEXT NULL DEFAULT NULL COLLATE 'utf8_general_ci',
`gender` VARCHAR(50) NULL DEFAULT NULL COLLATE 'utf8_general_ci',
PRIMARY KEY (`id`) USING BTREE
)
COLLATE='utf8_general_ci'
ENGINE=InnoDB
;
  • 执行命令生成user下的model代码
1
E:\proj\gowork\study-gozero-demo\mall\user\model> goctl model mysql ddl -src user.sql -dir . -c 
  • 在user/model下面生成了对model中增删改查
1
2
3
4
5
6
7
E:\proj\gowork\study-gozero-demo\mall\user\model>ls
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a---- 2023/7/27 10:05 338 user.sql
-a---- 2023/7/27 10:35 623 usermodel.go
-a---- 2023/7/27 10:35 3995 usermodel_gen.go
-a---- 2023/7/27 10:35 106 vars.
  • 发现usermodel_gen.go 中代码没有查询用户名和密码的函数,因此新增一个,同时新增一个查询所有用户的函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
type (
userModel interface {
Insert(ctx context.Context, data *User) (sql.Result, error)
FindOne(ctx context.Context, id int64) (*User, error)
// 新增的查询用户名函数
FindOneByUsername(ctx context.Context, username string) (*User, error)
// 查询所有的用户
FindAll(ctx context.Context, name string) ([]*User, error)
Update(ctx context.Context, data *User) error
Delete(ctx context.Context, id int64) error
}

.....
// 查询用户名,若存在则返回user的数据
func (m *defaultUserModel) FindOneByUsername(ctx context.Context, username string) (*User, error) {
userUsernameKey := fmt.Sprintf("%s%v", cacheUserIdPrefix, username)
var resp User
err := m.QueryRowIndexCtx(ctx, &resp, userUsernameKey, m.formatPrimary, func(ctx context.Context, conn sqlx.SqlConn, v interface{}) (i interface{}, e error) {
query := fmt.Sprintf("select %s from %s where `username` = ? limit 1", userRows, m.table)
if err := conn.QueryRowCtx(ctx, &resp, query, username); err != nil {
return nil, err
}
return resp.Id, nil
}, m.queryPrimary)
switch err {
case nil:
return &resp, nil
case sqlc.ErrNotFound:
return nil, ErrNotFound
default:
return nil, err
}
}

func (m *defaultUserModel) FindAll(ctx context.Context, username string) ([]*User, error) {
var resp []*User
var query string
var err error
if len(strings.TrimSpace(username)) > 0 {
query = fmt.Sprintf("select %s from %s where `username` = ?", userRows, m.table)
err = m.QueryRowsNoCacheCtx(ctx, &resp, query, username)

} else {
query = fmt.Sprintf("select %s from %s", userRows, m.table)
err = m.QueryRowsNoCacheCtx(ctx, &resp, query)
}
// fmt.Println("query=", query)
fmt.Println("query=", query, "error:", err)

switch err {
case nil:
return resp, nil
case sqlc.ErrNotFound:
return nil, ErrNotFound
default:
return nil, err
}
}

需要注意的是,moder_gen.go 文件是由 goctl 工具自动生成的,每次执行 goctl 命令时都会重新生成该文件。因此,在修改表结构或需要重新生成代码时,应该先备份自定义的代码,并注意保留所需的代码或修改

  • 修改user/api/user.api 内容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
type (
User {
Id int64 `json:"id"`
Username string `json:"username"`
Gender string `json:"gender"`
Password string `json:"password"`
}

LoginReq {
Username string `json:"username"`
Password string `json:"password"`
}

LoginReply {
Id int64 `json:"id"`
Username string `json:"username"`
Gender string `json:"gender"`
// 下面三个为jwt鉴权需要用到
AccessToken string `json:"accessToken"`
AccessExpire int64 `json:"accessExpire"`
RefreshAfter int64 `json:"refreshAfter"`
}

UserAddReq {
Username string `json:"username"`
Password string `json:"password"`
Gender string `json:"gender"`

}
UserAddRes {
Id int64 `json:"id"`
Name string `json:"name"`
}
UserDelReq {
Id int64 `json:"id"`
}
UserDelRes {
Id int64 `json:"id"`
Username string `json:"username"`
}
UserEditReq {
Id int64 `json:"id"`
Username string `json:"username"`
Password string `json:"password"`
Gender string `json:"gender"`

}
UserEditRes {
Id int64 `json:"id"`
Username string `json:"username"`
}
UserQueryReq {
Username string `json:"username"`
}
UserQueryRes {
UserList []*User `json:"products"`
}
)

service user-api {
@handler login
post /user/login (LoginReq) returns (LoginReply)
@handler UserAdd
post /user/UserAdd (UserAddReq) returns (UserAddRes)
@handler UserDel
post /user/UserDel (UserDelReq) returns (UserDelRes)
@handler UserQuery
get /user/UserQuery (UserQueryReq) returns (UserQueryRes)
@handler UserEdit
post /user/UserEdit (UserEditReq) returns (UserEditRes)
}
  • 生成api服务
1
E:\proj\gowork\study-gozero-demo\mall\user\api> goctl api go -api user.api -dir . 

编写业务代码

  • 目录E:\proj\gowork\study-gozero-demo\mall\user\api\internal\config\config.go对添加mysql配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

package config

import (
"github.com/zeromicro/go-zero/core/stores/cache"
"github.com/zeromicro/go-zero/rest"
)

type Config struct {
rest.RestConf
// 添加mysql配置
Mysql struct {
DataSource string
}
// 使用缓存
CacheRedis cache.CacheConf
}

  • 修改E\proj\gowork\study-gozero-demo\mall\user\api\etc\user-api.yaml配置文件
1
2
3
4
5
6
7
8
9
Name: user-api
Host: 0.0.0.0
Port: 8888
Mysql:
DataSource: root:123456@tcp(127.0.0.1:3306)/go_zero?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai
CacheRedis:
- Host: 127.0.0.1:6379
Pass: ""
Type: node

本地环境你需要搭建好mysql和redis的环境

  • 完善依赖服务user/api/internal/svc/servicecontext.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package svc

import (
"study-gozero-demo/mall/user/api/internal/config"
"study-gozero-demo/mall/user/model"

"github.com/zeromicro/go-zero/core/stores/sqlx"
)

type ServiceContext struct {
Config config.Config
// 引入model层的增删改查
UserModel model.UserModel
}

func NewServiceContext(c config.Config) *ServiceContext {
// 初始化mysql连接
conn := sqlx.NewMysql(c.Mysql.DataSource)

return &ServiceContext{
Config: c,
// 调用user/model层代码
UserModel: model.NewUserModel(conn, c.CacheRedis),
}
}

  • 编写user/api/internal/logic 目录下的各接口信息

登录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
//loginlogic.go

func (l *LoginLogic) Login(req *types.LoginReq) (resp *types.LoginReply, err error) {
// todo: add your logic here and delete this line

if len(strings.TrimSpace(req.Username)) == 0 || len(strings.TrimSpace(req.Password)) == 0 {
return nil, errors.New("参数错误")
}

userInfo, err := l.svcCtx.UserModel.FindOneByUsername(l.ctx, req.Username)
switch err {
case nil:
case model.ErrNotFound:
return nil, errors.New("用户名不存在")
default:
return nil, err
}

if userInfo.Pwd != req.Password {
return nil, errors.New("用户密码不正确")
}
// 登录成功返回数据
return &types.LoginReply{
Id: userInfo.Id,
Username: userInfo.Username,
}, nil

}


新增用户

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// useraddlogic.go
func (l *UserAddLogic) UserAdd(req *types.UserAddReq) (resp *types.UserAddRes, err error)
if len(strings.TrimSpace(req.Username)) == 0 || len(strings.TrimSpace(req.Password)) == 0 {
return nil, errors.New("参数错误")
}

userInfo, err1 := l.svcCtx.UserModel.FindOneByUsername(l.ctx, req.Username)
fmt.Println("user_info", userInfo)
// 如果查到已存在用户名,则不能新增
if err1 == nil {
return nil, errors.New(req.Username + "用户已经存在")
}
// 组装结构体
var addReq model.User
errCopy := copier.Copy(&addReq, req)
if errCopy != nil {
return nil, errCopy
}

respd, err2 := l.svcCtx.UserModel.Insert(l.ctx, &addReq)
fmt.Println("------respd:", respd, "error:", err2)
if err2 != nil {
fmt.Println("insert into error:", err)
return nil, errors.New(err.Error())

}

// 登录成功返回数据
return &types.UserAddRes{
Name: req.Username,
}, nil
}

编辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// usereditlogic.go
func (l *UserEditLogic) UserEdit(req *types.UserEditReq) (resp *types.UserEditRes, err error) {

if len(strings.TrimSpace(req.Username)) == 0 || len(strings.TrimSpace(req.Password)) == 0 {
return nil, errors.New("参数错误")
}
if strings.TrimSpace(req.Username) == "admin" {
return nil, errors.New("此用户不许修改")

}

userInfo, _ := l.svcCtx.UserModel.FindOne(l.ctx, req.Id)
if userInfo == nil {
return nil, errors.New("用户不存在")
}
// 组装结构体
var editReq model.User
errCopy := copier.Copy(&editReq, req)
if errCopy != nil {
return nil, errCopy
}

err2 := l.svcCtx.UserModel.Update(l.ctx, &editReq)
if err2 != nil {
fmt.Println("update error:", err)
return nil, errors.New("更新错误")

}

// 返回数据
return &types.UserEditRes{
Username: req.Username,
Id: req.Id,
}, nil
}

  • 删除
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// userdellogic.go
func (l *UserDelLogic) UserDel(req *types.UserDelReq) (resp *types.UserDelRes, err error) {
// todo: add your logic here and delete this line

if req.Id == 23 {
return nil, errors.New("此用户不可删除")
}
userInfo, _ := l.svcCtx.UserModel.FindOne(l.ctx, req.Id)
if userInfo == nil {
return nil, errors.New("用户不存在")
}

err2 := l.svcCtx.UserModel.Delete(l.ctx, req.Id)
if err2 != nil {
fmt.Println("del error:", err)
return nil, errors.New("删除错误")

}

// 返回数据
return &types.UserDelRes{
Username: userInfo.Username.String,
Id: userInfo.Id,
}, nil
}
  • 查询
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func (l *UserQueryLogic) UserQuery(req *types.UserQueryReq) (resp *types.UserQueryRes, err error) {
// todo: add your logic here and delete this line

fmt.Println("username", req.Username)
res1, err := l.svcCtx.UserModel.FindAll(l.ctx, req.Username)
fmt.Println("res1:", res1, "err:", err)
if err != nil {
return nil, errors.New("查询错误")
}
var resList []*types.User
for _, u := range res1 {
var resVo types.User
_ = copier.Copy(&resVo, u)
resList = append(resList, &resVo)
}
return &types.UserQueryRes{UserList: resList}, nil

}

接口测试

  • 本地数据库中的user表,需要有如下密码

image-20230727151305396

  • 启动user/api的服务
1
E:\proj\gowork\study-gozero-demo\mall\user\api> go run .\user.go
  • python客户端进行测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import requests
data ={"username": "admin", "password": "123456"}
resp = requests.post("http://127.0.0.1:8888/user/login", json=data)
print(resp.text)

data ={"username": "admin1", "password": "1234561", "gender": "男"}
resp = requests.post("http://127.0.0.1:8888/user/UserAdd", json=data)
print(resp.text)

data ={"username": "admin551", "password": "12345776", "gender": "男1", "id": 24}
resp = requests.post("http://127.0.0.1:8888/user/UserEdit", json=data)
print(resp.text)


data ={"username": ""}
resp = requests.get("http://127.0.0.1:8888/user/UserQuery", json=data)
print(resp.text)


data ={"id": 23}
resp = requests.post("http://127.0.0.1:8888/user/UserDel", json=data)
print(resp.text)

业务鉴权jwt

  • jwt鉴权一般在api层使用,我们这次演示工程中分别在user api登录时生成jwt token,在order api查模拟新增时验证用户jwt token。

user api

  • 配置mall/user/api/internal/config/config.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package config

import (
"github.com/zeromicro/go-zero/core/stores/cache"
"github.com/zeromicro/go-zero/rest"
)

type Config struct {
rest.RestConf

// 添加mysql配置
Mysql struct {
DataSource string
}
// 使用缓存
CacheRedis cache.CacheConf
// jwt的配置字段
Auth struct {
AccessSecret string
AccessExpire int64
}
}
  • 配置/order/api/etc/api.yaml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Name: user-api
Host: 0.0.0.0
Port: 8888
Mysql:
DataSource: root:123456@tcp(127.0.0.1:3306)/go_zero?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai
CacheRedis:
- Host: 127.0.0.1:6379
Pass: ""
Type: node
Auth:
# 生成jwt token的密钥 一般格式为udid
AccessSecret: 0c891c78-9415-ec96-deb8-3b658f9e57f3
# jwt token有效期,单位:秒,现在设置的为1小时
AccessExpire: 3600
  • 在user/api/internal/logic/loginlogic.go 中加上生成jwt内容的代码,并且在登录返回的数据返回jwt内容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
func (l *LoginLogic) getJwtToken(secretKey string, iat, seconds, userId int64) (string, error) {
claims := make(jwt.MapClaims)
claims["exp"] = iat + seconds
claims["iat"] = iat
claims["userId"] = userId
token := jwt.New(jwt.SigningMethodHS256)
token.Claims = claims
return token.SignedString([]byte(secretKey))
}

func (l *LoginLogic) Login(req *types.LoginReq) (resp *types.LoginReply, err error) {
if len(strings.TrimSpace(req.Username)) == 0 || len(strings.TrimSpace(req.Password)) == 0 {
return nil, errors.New("参数错误")
}
userInfo, err := l.svcCtx.UserModel.FindOneByUsername(l.ctx, req.Username)
switch err {
case nil:
case model.ErrNotFound:
return nil, errors.New("用户名不存在")
default:
return nil, err
}
if userInfo.Password.String != req.Password {
return nil, errors.New("用户密码不正确")
}

// ---jwt-start---
now := time.Now().Unix()
accessExpire := l.svcCtx.Config.Auth.AccessExpire
jwtToken, err := l.getJwtToken(l.svcCtx.Config.Auth.AccessSecret, now, l.svcCtx.Config.Auth.AccessExpire, userInfo.Id)
if err != nil {
return nil, err
}
// ---jwt-end---

// 登录成功返回数据
return &types.LoginReply{
Id: userInfo.Id,
Username: userInfo.Username.String,
AccessToken: jwtToken,
AccessExpire: now + accessExpire,
RefreshAfter: now + accessExpire/2,
}, nil
}

order api

  • 使用jwt鉴权校验

  • 我们把之前的order/api目录内容清空

  • 编写mall/order/api/order.api

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
type (
OrderReq {
Id string `path:"id"`
}

OrderReply {
Id string `json:"id"`
ProductName string `json:"productname"`
}

OrderAddReq {
ProductName string `json:"productname"`
Price string `json:"price"`
Unit string `json:"unit"`
}

OrderAddRes {
Code int `json:"code"`
Messsage string `json:"message"`
}
)

// 在哪个服务头上加这个验证,就是需要jwt鉴权
@server(
jwt: Auth
)
service order {
@handler orderAdd
post /api/order/OrderAdd (OrderAddReq) returns (OrderAddRes)
}

service order {
@handler getOrder
get /api/order/get/:id (OrderReq) returns (OrderReply)
}
// 在哪个服务头上加这个验证,就是需要jwt鉴权
@server(
jwt: Auth
)
service order {
@handler orderAdd
post /api/order/OrderAdd (OrderAddReq) returns (OrderAddRes)
}

service order {
@handler getOrder
get /api/order/get/:id (OrderReq) returns (OrderReply)
}
  • 生成order.api代码
1
PS E:\proj\gowork\study-gozero-demo\mall\order\api>  goctl api go -api order.api -dir . 
  • 分别编写mall\order\api\internal\logic 下的测试代码

getorderlogic.go

1
2
3
4
5
6
7
8
func (l *GetOrderLogic) GetOrder(req *types.OrderReq) (resp *types.OrderReply, err error) {
fmt.Println("id=", req.Id)

return &types.OrderReply{
Id: req.Id,
ProductName: "test order",
}, nil
}

orderaddlogic.go

1
2
3
4
5
6
7
func (l *OrderAddLogic) OrderAdd(req *types.OrderAddReq) (resp *types.OrderAddRes, err error) {
fmt.Println("req=", req)
return &types.OrderAddRes{
Code: 1,
Messsage: "success",
}, nil
}
  • order/api/etc/order.yaml 端口修改和加上jwt的token
1
2
3
4
5
6
7
8
Name: order
Host: 0.0.0.0
Port: 8889
Auth:
# 生成jwt token的密钥 一般格式为udid
AccessSecret: 0c891c78-9415-ec96-deb8-3b658f9e57f3
# jwt token有效期,单位:秒,现在设置的为1小时
AccessExpire: 3600
  • 分别启动order/api/order.gouser/api/user.go

测试

1
2
3
4
5
6
7
8
9
resp = requests.get("http://127.0.0.1:8889/api/order/get/2")
print(resp.text)

data ={"productname": "西瓜", "price": "122", "unit": "个"}
resp = requests.post("http://127.0.0.1:8889/api/order/OrderAdd", json=data)
print(resp)

{"id":"2","productname":"test order"}
<Response [401]>
  • 发现OrderAdd 直接返回401

  • 我们把token加到头部

1
2
3
4
5
6
7
8
data ={"username": "admin", "password": "123456"}
resp = requests.post("http://127.0.0.1:8888/user/login", json=data)
data = json.loads(resp.text)
token = data["accessToken"]
resp = requests.post("http://127.0.0.1:8889/api/order/OrderAdd", headers=header, json=data1)
print(resp.text)

{"code":1,"message":"success"}

总结

  • 本次主要用单机服务实现了user api层结合mysql的增删改查,同时在order层加上了jwt的校验
  • 由于登录的数据缓存到了redis中,进行修改和删除时,可能出现删除不掉,因此可以在redis中删除可以
  • 下篇文章将使用为微服务,mysql,jwt等结合起来

说明

本次教程主要来自于这里

安装依赖

1
2
3
4
5
E:\proj\gowork\studyGoZero>go env
set GO111MODULE=on
set GOPATH=C:\Users\Administrator\go
set GOROOT=E:\app\Go
....
  • C:\Users\Administrator\go\bin\goctl.exe 拷贝到GOROOT\bin目录下
  • 查看版本成功
1
2
E:\proj\gowork> goctl --version
goctl version 1.5.4 windows/amd64
  • 安装protoc ,查看他的作用
1
goctl env check --install --verbose --force
  • 安装protoc 成功后,把C:\Users\Administrator\go\bin\ 的相关文件拷贝到GOROOT\bin

go-zero单体服务

  • 创建并初始化项目,生成greet服务为api层(对外接口)
1
2
3
4
5
6
7
E:\proj\gowork>mkdir go-zero-demo
E:\proj\gowork>cd go-zero-demo
E:\proj\gowork\go-zero-demo>go mod init go-zero-demo
go: creating new go.mod: module go-zero-demo
E:\proj\gowork\go-zero-demo>goctl api new greet
Done.
E:\proj\gowork\go-zero-demo>go mod tidy
  • 查看greet的服务目录
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
E:\proj\gowork\go-zero-demo>tree /f

│ go.mod
│ go.sum

└─greet
│ greet.api
│ greet.go

├─etc
│ greet-api.yaml

└─internal
├─config
│ config.go

├─handler
│ greethandler.go
│ routes.go

├─logic
│ greetlogic.go

├─svc
│ servicecontext.go

└─types
types.go

  • greet\greet.api 中就是对外的接口
1
2
3
4
5
6
7
8
9
10
11
12
type Request {
Name string `path:"name,options=you|me"`
}

type Response {
Message string `json:"message"`
}

service greet-api {
@handler GreetHandler
get /from/:name(Request) returns (Response) // 对外访问接口路径为:/from/name
}

编写逻辑

E:\proj\gowork\go-zero-demo\greet\internal\logic\greetlogic.go1 代码中编写如下逻辑:

1
2
3
4
5
func (l *GreetLogic) Greet(req *types.Request) (resp *types.Response, err error) {
return &types.Response{
Message: "Hello go-zero",
}, nil
}

启动并访问服务

  • 启动服务
1
2
go run .\greet.go 
Starting server at 0.0.0.0:8888...
  • 浏览器访问:http://localhost:8888/from/you

image-20230726113435097

go-zero 微服务

  • api部分其实和单体服务的创建逻辑是一样的,只是在单体服务中没有服务间的通讯而已, 且微服务中api服务会多一些rpc调用的配置。

  • 假设我们在开发一个商城项目,而开发者小明负责**用户模块(user)订单模块(order)**的开发,我们姑且将这两个模块拆分成两个微服务

演示功能目标

  • 订单服务(order)提供一个查询接口
  • 用户服务(user)提供一个方法供订单服务获取用户信息

服务设计分析

根据情景提要我们可以得知,订单是直接面向用户,通过http协议访问数据,而订单内部需要获取用户的一些基础数据,既然我们的服务是采用微服务的架构设计, 那么两个服务(user, order)就必须要进行数据交换,服务间的数据交换即服务间的通讯,到了这里,采用合理的通讯协议也是一个开发人员需要 考虑的事情,可以通过http,rpc等方式来进行通讯,这里我们选择rpc来实现服务间的通讯,相信这里我已经对”rpc服务存在有什么作用?”已经作了一个比较好的场景描述。 当然,一个服务开发前远不止这点设计分析,我们这里就不详细描述了。从上文得知,我们需要一个

  • user rpc
  • order api

两个服务来初步实现这个小demo。

创建工程

1
2
3
E:\proj\gowork>mkdir study-gozero-demo
E:\proj\gowork>cd study-gozero-demo
E:\proj\gowork\study-gozero-demo>go mod init study-gozero-demo

创建user rpc服务

  • 创建user rpc服务的目录里
1
E:\proj\gowork\study-gozero-demo\>mkdir -p mall/user/rpc

注意在win中,我使用的git窗口编写的mkdri命令,在cmd中这样创建有问题

  • 添加user.proto文件,增加getUser方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
syntax = "proto3";

package user;

// protoc-gen-go 版本大于1.4.0, proto文件需要加上go_package,否则无法生成
option go_package = "./user";

message IdRequest {
string id = 1;
}

message UserResponse {
// 用户id
string id = 1;
// 用户名称
string name = 2;
// 用户性别
string gender = 3;
}

service User {
rpc getUser(IdRequest) returns(UserResponse); //对外开发的接口
}
  • 生成rpc目录和代码
1
2
3
4
E:\proj\gowork\study-gozero-demo\mall\user\rpc> goctl rpc protoc user.proto --go_out=. --go-grpc_out=. --zrpc_out=.


Done.

vs code ide的终端中执行的此命令.最终生成的go服务源代码文件在目录:E:\proj\gowork\study-gozero-demo\mall\user\rpc\userclient\user.go

  • 填充业务逻辑其实就是user.proto中生成的getUser,目录为:E:\proj\gowork\study-gozero-demo\mall\user\rpc\internal\logic\getuserlogic.go
1
2
3
4
5
6
7
8
9
10
func (l *GetUserLogic) GetUser(in *user.IdRequest) (*user.UserResponse, error) {
// todo: add your logic here and delete this line

// 新增的代码
return &user.UserResponse{
Id: "1",
Name: "test",
}, nil
}

若发现这里引用包报错,可以使用go mod tidy 下载依赖

创建order api服务

  • 创建 order api服务
1
2
3
Administrator@WIN-20230710BAT MINGW64 /e/proj/gowork/study-gozero-demo/mall
$ mkdir -p order/api && cd order/api

git 窗口执行

  • 编写添加api文件目录为:E:\proj\gowork\study-gozero-demo\mall\order\api\order.api
1
2
3
4
5
6
7
8
9
10
11
12
13
14
type(
OrderReq {
Id string `path:"id"`
}

OrderReply {
Id string `json:"id"`
Name string `json:"name"`
}
)
service order {
@handler getOrder
get /api/order/get/:id (OrderReq) returns (OrderReply)
}
  • 生成order服务,非rpc
1
PS E:\proj\gowork\study-gozero-demo\mall\order\api> goctl api go -api order.api -dir .
  • 添加user rpc配置目录地址为

    E:\proj\gowork\study-gozero-demo\mall\order\api\internal\config\config.go

1
2
3
4
5
6
7
8
9
10
11
12
package config

import (
"github.com/zeromicro/go-zero/rest"
"github.com/zeromicro/go-zero/zrpc"
)

type Config struct {
rest.RestConf
UserRpc zrpc.RpcClientConf // 这个就是新增的user rpc服务
}

  • 添加yaml配置目录为E\proj\gowork\study-gozero-demo\mall\order\api\etc\order.yaml
1
2
3
4
5
6
7
8
9
Name: order
Host: 0.0.0.0
Port: 8888
UserRpc: -- 从这里开始为新增
Etcd:
Hosts:
- 127.0.0.1:2379
Key: user.rpc

  • 完善服务依赖目录为

    E:\proj\gowork\study-gozero-demo\mall\order\api\internal\svc\servicecontext.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package svc

import (
"study-gozero-demo/mall/order/api/internal/config"
"study-gozero-demo/mall/user/rpc/userclient" // 引用最终生成user服务代码路径

"github.com/zeromicro/go-zero/zrpc"
)

type ServiceContext struct {
Config config.Config
UserRpc userclient.User // 引用User服务中的GetUser函数
}

func NewServiceContext(c config.Config) *ServiceContext {
return &ServiceContext{
Config: c,
//加入user rpc服务
UserRpc: userclient.NewUser(zrpc.MustNewClient(c.UserRpc)),
}
}

  • 添加order演示逻辑

    E:\proj\gowork\study-gozero-demo\mall\order\api\internal\logic\getorderlogic 添加业务逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package logic

import (
"context"
"errors"

"study-gozero-demo/mall/order/api/internal/svc"
"study-gozero-demo/mall/order/api/internal/types"

// 引用
"study-gozero-demo/mall/user/rpc/user"

"github.com/zeromicro/go-zero/core/logx"
)

type GetOrderLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}

func NewGetOrderLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetOrderLogic {
return &GetOrderLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx, // 引用order的服务,然后才能调用order服务中的user rpc
}
}

func (l *GetOrderLogic) GetOrder(req *types.OrderReq) (resp *types.OrderReply, err error) {
// todo: add your logic here and delete this line

// 新增代码,注意这里的&user,应用的是user rpc的user/user.pb.go中内容,IdRequest其实就是对应入参(user.proto定义的)
// 获取订单之前,需要盘点用户的id是否存在,若不存在则返回用户不存在报错,若存在则返回test order
user, err := l.svcCtx.UserRpc.GetUser(l.ctx, &user.IdRequest{
Id: "1",
})
if err != nil {
return nil, err
}

if user.Name != "test" {
return nil, errors.New("用户不存在")
}

return &types.OrderReply{
Id: req.Id,
Name: "test order",
}, nil
}

启动服务并验证

  • 启动本地etcd

注意本地安装etcd,打开E:\app\etcd-v3.4.27-windows-amd64\etcd.exe启动服务

etcd是一个分布式一致性键值存储,其主要用于分布式系统的共享配置和服务发现。

  • 启动user rpc
1
E:\proj\gowork\study-gozero-demo\mall\user\rpc> go run .\user.go
  • 启动order api
1
2
3
PS E:\proj\gowork\study-gozero-demo\mall\order\api> go run order.go
Starting server at 0.0.0.0:8888...
{"@timestamp":"2023-07-26T16:36:58.057+08:00","caller":"stat/usage.go:61","content":"CPU: 0m, MEMORY: Alloc=3.2Mi, TotalAlloc=6.1Mi, Sys=17.9Mi, NumGC=3","level":"stat"}
  • 访问order api

image-20230726164009157

总结

如上代码展示了,单体服务和微服务分别运用,下一篇文章我们将会加上数据库、jwt鉴权、以及具体的增删改查

说明

  • go-zero实战系列一 中已经编写好api代码、order、product的rpc服务等,下面我们开始绩继续的实践过程
  • 本章主要对数据库的表新建,以及常见的增删改查

说明

本次主要按照这个博主的go-zero微服务实战进行学习

安装依赖

  • 安装依赖go-zero: go get -u github.com/zeromicro/go-zero@latest
  • 安装依赖goctl,用命令进行安装:go install github.com/zeromicro/go-zero/tools/goctl@latest
  • 安装成功后,输入go env 找到GOPATH
1
2
3
4
5
E:\proj\gowork\studyGoZero>go env
set GO111MODULE=on
set GOPATH=C:\Users\Administrator\go
set GOROOT=E:\app\Go
....
  • C:\Users\Administrator\go\bin\goctl.exe 拷贝到GOROOT\bin目录下
  • 查看版本成功
1
2
PS E:\proj\gowork\studyGoZero\apps\order> goctl --version
goctl version 1.5.4 windows/amd64
  • 安装protoc ,查看他的作用
1
goctl env check --install --verbose --force
  • 安装protoc 成功后,把C:\Users\Administrator\go\bin\ 的相关文件拷贝到GOROOT\bin

服务划分

image-20230721102332169

从以上思维导图可以看出整个电商系统功能还是比较多的,我们根据业务职能做如下微服务的划分:

  • 商品服务(product) - 商品的添加、信息查询、库存管理等功能
  • 购物车服务(cart) - 购物车的增删改查
  • 订单服务(order) - 生成订单,订单管理
  • 支付服务(pay) - 通过调用第三方支付实现支付功能
  • 账号服务(user) - 用户信息、等级、封禁、地址管理
  • 推荐服务(recommend) - 首页商品推荐
  • 评论服务(reply) - 商品的评论功能、评论的回复功能

BFF层

  • 但对于一个复杂的高并发的系统来说,我们需要处理各种异常的场景,比如某个页面需要依赖多个微服务提供的数据,为了避免串行请求导致的耗时过长,我们一般会并行的请求多个微服务,这个时候其中的某个服务请求异常的话我们可能需要做一些特殊的处理,比如提供一些降级的数据等。我们的解决方案就是加一层,即BFF层,通过BFF对外提供HTTP接口,客户端只与BFF进行交互,我们的这个项目为了简化只会采用一个BFF服务

image-20230721102651764

  • 我们可以提供多个BFF吗?答案是当然可以。BFF的目的是为客户端提供一个集中的接口,例如移动端页面和浏览器页面的数据协议不同,这种情况下为了更好的表示数据,可以使用两个BFF,同时只供一个BFF如果该BFF异常就会导致所有的业务受影响,提供多个BFF也可以提高服务的可用性,降低业务异常的影响面。多个BFF架构图如下:

image-20230721102751280

项目结构

  • 初始化项目
1
2
3
4
cd E:\proj\gowork
mkdir studyGoZero
cd studyGoZero
go mod init example.com/studyGoZero
  • 目录结构如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
─apps
│ ├─app
│ │ └─api
│ ├─cart
│ │ ├─admin
│ │ ├─rmq
│ │ └─rpc
│ ├─order
│ │ ├─admin
│ │ ├─rmq
│ │ └─rpc
│ ├─paly
│ │ ├─admin
│ │ ├─rmq
│ │ └─rpc
│ ├─product
│ │ ├─admin
│ │ ├─rmq
│ │ └─rpc
│ ├─recommend
│ │ ├─admin
│ │ ├─rmq
│ │ └─rpc
│ ├─reply
│ │ ├─admin
│ │ ├─rmq
│ │ └─rpc
│ └─user
│ ├─admin
│ ├─rmq
│ └─rpc
└─pkg
  • 其中apps存放的是我们所有的微服务,比如order为订单相关的微服务,pkg目录为所有服务共同依赖的包的存放路径,比如所有的服务都需要依赖鉴权就可以放到pkg目录下
  • app - BFF服务
  • cart - 购物车服务
  • order - 订单服务
  • pay - 支付服务
  • product - 商品服务
  • recommend - 推荐服务
  • reply - 评论服务
  • user - 账号服务

在每个服务目录下我们又会分为多个服务,主要会有如下几类服务:

  • api - 对外的BFF服务,接受来自客户端的请求,暴露HTTP接口
  • rpc - 对内的微服务,仅接受来自内部其他微服务或者BFF的请求,暴露gRPC接口
  • rmq - 负责进行流式任务处理,上游一般依赖消息队列,比如kafka等
  • admin - 也是对内的服务,区别于rpc,更多的是面向运营侧的且数据权限较高,通过隔离可带来更好的代码级别的安全,直接提供HTTP接口

API 定义

首页API

首页功能主要分为四个部分,搜索、Banner图、限时抢购和推荐商品列表,点击搜索框会跳转到搜索页,推荐部分是分页展示的,用户通过不断地往上滑动可以加载下一页。通过分析首页我们大致需要提供三个接口,分别是Banner接口,限时抢购接口和推荐接口。

image-20230720110620328

  • 打开apps\app\api\api.api文件,写入对外api的代码定义
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
syntax = "v1"
type (
// 首页的banner响应请求
HomeBannerResponse {
Banners []*Banner `json:"banners"`
}
.....

)

service api-api {

@doc "首页Banner"
@handler HomeBannerHandler
get /v1/home/banner returns (HomeBannerResponse)

@doc "限时抢购"
@handler FlashSaleHandler
get /v1/flashsale returns (FlashSaleResponse)

@doc "推荐商品列表"
@handler RecommendHandler
get /v1/recommend (RecommendRequest) returns (RecommendResponse)

@doc "分类商品列表"
@handler CategoryListHandler
get /v1/category/list (CategoryListRequest) returns (CategoryListResponse)
@doc "购物车列表"
@handler CartListHandler
get /v1/cart/list (CartListRequest) returns (CartListResponse)

@doc "商品评论列表"
@handler ProductCommentHandler
get /v1/product/comment (ProductCommentRequest) returns (ProductCommentResponse)

@doc "订单列表"
@handler OrderListHandler
get /v1/order/list (OrderListRequest) returns (OrderListResponse)

@doc "商品详情"
@handler ProductDetailHandler
get /v1/product/detail (ProductDetailRequest) returns (ProductDetailResponse)
}
  • 定义好api后,我们使用如下命令重新生成项目代码,输出如下信息表明生成成功
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
PS E:\proj\gowork\studyGoZero\apps\app\api> goctl api go -api api.api -dir .
Done.

// 查看目录
PS E:\proj\gowork\studyGoZero\apps\app\api> tree /f
E:.
│ api.api
│ api.go

├─etc
│ api-api.yaml

└─internal
├─config
│ config.go

├─handler
│ cartlisthandler.go
│ categorylisthandler.go
│ flashsalehandler.go
│ homebannerhandler.go
│ orderlisthandler.go
│ productcommenthandler.go
│ productdetailhandler.go
│ recommendhandler.go
│ routes.go

├─logic
│ cartlistlogic.go
│ categorylistlogic.go
│ flashsalelogic.go
│ homebannerlogic.go
│ orderlistlogic.go
│ productcommentlogic.go
│ productdetaillogic.go
│ recommendlogic.go

├─svc
│ servicecontext.go

└─types
types.go

admin 层代码

order目录

  • 目录为: E:\proj\gowork\studyGoZero\apps\order

  • 执行如下命令即可初始化order admin代码,注意order adminapi服务,直接对前端提供HTTP接口

    1
    E:\proj\gowork\studyGoZero\apps\order> goctl api new admin
  • 代码结构如下

1
2
3
4
5
6
7
─admin
│ │ admin.api
│ │ admin.go
│ │
│ ├─etc
│ │ admin-api.yaml
│ │
  • 生成的服务代码我们可以直接运行go run admin.go,默认侦听在8888端口

    1
    2
    PS E:\proj\gowork\studyGoZero\apps\order\admin> go run .\admin.go 
    Starting server at 0.0.0.0:8888...
  • 对于rmq服务我们会使用go-zero提供的 kq 功能,这里先初始化main.go

其他层

  • pay、pruduct、recommend、reply、user 等按照同样的方式进行初始化

rpc服务层

  • Goctl Rpc是goctl脚手架下的一个rpc服务代码生成模块,支持proto模板生成和rpc服务代码生成,通过此工具生成代码你只需要关注业务逻辑编写而不用去编写一些重复性的代码

  • 作者博客中初始化rpc使用的这种 goctl rpc new rpc ,并不推荐,研究代码后发现和作者提供的仓库代码不一致,使用的第二种方式生成的rpc服务层的代码和目录:通过指定proto生成rpc服务

  • 因为BFF只负责数据的组装工作,数据真正的来源是各个微服务通过RPC接口提供,接下来我们来定义各个微服务的proto。如下展示的订单列表页面由两部分数据组成,分别是订单数据和商品数据,也就是我们的BFF需要依赖order-rpc和product-rpc来完成该页面数据的组装,下面我们分别来定义order-rpc和product-rpc

image-20230725100643383

order层

  • E:\proj\gowork\studyGoZero\apps\order\rpc\order.proto,定义如下,service名字为Order,添加了Orders获取订单列表rpc接口。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
syntax = "proto3";

package order;
option go_package = "./order";


service Order {
rpc Orders(OrdersRequest) returns(OrdersResponse);
rpc CreateOrder(CreateOrderRequest) returns (CreateOrderResponse);
rpc CreateOrderCheck(CreateOrderRequest) returns (CreateOrderResponse);
rpc RollbackOrder (CreateOrderRequest) returns (CreateOrderResponse);
rpc CreateOrderDTM(AddOrderReq) returns (AddOrderResp);
rpc CreateOrderDTMRevert(AddOrderReq) returns(AddOrderResp);
rpc GetOrderById(GetOrderByIdReq) returns (GetOrderByIdResp);
}

....

使用如下命令重新生成代码,注意这里需要依赖protoc-gen-goprotoc-gen-go-grpc两个插件,木有安装的话执行下面命令会报错:

1
2
PS E:\proj\gowork\studyGoZero\apps\order\rpc>  goctl rpc protoc order.proto --go_out=. --go-grpc_out=. --zrpc_out=.
Done.

注意最终生成的服务端的代码目录为:E:\proj\gowork\studyGoZero\apps\order\rpc\orderclient\order.go

目录结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
PS E:\proj\gowork\studyGoZero\apps\order\rpc> tree /f
卷 软件盘 的文件夹 PATH 列表
卷序列号为 240F-1787
E:.
│ order.go
│ order.proto

├─etc
│ order.yaml

├─internal
│ ├─config
│ │ config.go
│ │
│ ├─logic
│ │ createorderchecklogic.go
│ │ createorderdtmlogic.go
│ │ createorderdtmrevertlogic.go
│ │ createorderlogic.go
│ │ getorderbyidlogic.go
│ │ orderslogic.go
│ │ rollbackorderlogic.go
│ │
│ ├─server
│ │ orderserver.go
│ │
│ └─svc
│ servicecontext.go

├─order
│ order.pb.go
│ order_grpc.pb.go

└─orderclient
order.go
  • 本地安装etcd,打开E:\app\etcd-v3.4.27-windows-amd64\etcd.exe启动服务

etcd是一个分布式一致性键值存储,其主要用于分布式系统的共享配置和服务发现。

etcd由Go语言编写

  • 生成好后然后启动order-rpc服务,需要连接etcd服务
1
2
3
4
5
PS E:\proj\gowork\studyGoZero\apps\order\rpc> go run .\order.go
Starting rpc server at 0.0.0.0:8080...
{"@timestamp":"2023-07-24T17:03:51.723+08:00","caller":"stat/usage.go:61","content":"CPU: 0m, MEMORY: Alloc=3.2Mi, TotalAlloc=6.1Mi, Sys=17.9Mi, NumGC=3","level":"stat"}
{"@timestamp":"2023-07-24T17:03:51.737+08:00","caller":"load/sheddingstat.go:61","content":"(rpc) shedding_stat [1m], cpu: 0, total: 0, pass: 0, drop: 0","level":"stat"}
{"@timestamp":"2023-07-24T17:04:51.711+08:00","caller":"stat/usage.go:61","content":

在E:\proj\gowork\studyGoZero\apps\order\rpc\etc\order.yaml中,可以看到etcd的连接服务器地址,和order-rpc启动的地址信息:

1
2
3
4
5
6
Name: order.rpc
ListenOn: 0.0.0.0:8080
Etcd:
Hosts:
127.0.0.1:2379
Key: order.rpc

product

E:\proj\gowork\studyGoZero\apps\product\rpc\product.proto,定义如下,service名字为Order,添加了product产品的rpc接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
syntax = "proto3";

package product;
option go_package = "./product";

service Product {
rpc Product(ProductItemRequest) returns (ProductItem) ;
rpc Products(ProductRequest) returns(ProductResponse);
rpc ProductList(ProductListRequest) returns(ProductListResponse);
rpc OperationProducts(OperationProductsRequest) returns (OperationProductsResponse);
rpc UpdateProductStock(UpdateProductStockRequest) returns (UpdateProductStockResponse);
rpc CheckAndUpdateStock(CheckAndUpdateStockRequest) returns (CheckAndUpdateStockResponse);
rpc CheckProductStock(UpdateProductStockRequest) returns (UpdateProductStockResponse);
rpc RollbackProductStock(UpdateProductStockRequest) returns (UpdateProductStockResponse);
rpc DecrStock(DecrStockRequest) returns(DecrStockResponse);
rpc DecrStockRevert(DecrStockRequest) returns(DecrStockResponse);
}
...

  • E:\proj\gowork\studyGoZero\apps\product\rpc\etc\product.yaml配置启动服务地址端口为8081
1
2
3
4
5
6
Name: product.rpc
ListenOn: 0.0.0.0:8081
Etcd:
Hosts:
- 127.0.0.1:2379
Key: product.rpc
  • 启动product-rpc服务,需要连接etcd服务
1
2
PS E:\proj\gowork\studyGoZero\apps\product\rpc> go run .\product.go
Starting rpc server at 0.0.0.0:8081...

BUFF 层配置

  • 因为我们的BFF需要依赖order.rpc和product.rpc,我们需要先添加配置文件(E:\proj\gowork\studyGoZero\apps\app\api\etc\api-api.yaml),如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    Name: api-api
    Host: 0.0.0.0
    Port: 8888
    OrderRPC:
    Etcd:
    Hosts:
    - 127.0.0.1:2379
    Key: order.rpc
    ProductRPC:
    Etcd:
    Hosts:
    - 127.0.0.1:2379
    Key: product.rpc
  • 然后在E:\proj\gowork\studyGoZero\apps\app\api\internal\svc\servicecontext.go中添加RPC的客户端,如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    package svc

    import (
    "example.com/studyGoZero/apps/app/api/internal/config"
    "example.com/studyGoZero/apps/order/rpc/orderclient"
    "example.com/studyGoZero/apps/product/rpc/productclient"
    )

    type ServiceContext struct {
    Config config.Config
    OrderRPC orderclient.Order
    ProductRPC productclient.Product
    }

    func NewServiceContext(c config.Config) *ServiceContext {
    return &ServiceContext{
    Config: c,
    }
    }

最后只要在订单接口的E:\proj\gowork\studyGoZero\apps\app\api\internal\logic\orderlistlogic.go方法中添加逻辑就可以啦,这里只是演示,所以会比较简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
func (l *OrderListLogic) OrderList(req *types.OrderListRequest) (resp *types.OrderListResponse, err error) {
orderRet, err := l.svcCtx.OrderRPC.Orders(l.ctx, &order.OrdersRequest{UserId: req.UID})
if err != nil {
return nil, err
}
var pids []string
for _, o := range orderRet.Orders {
pids = append(pids, strconv.Itoa(int(o.Id)))
}
productRet, err := l.svcCtx.ProductRPC.Products(l.ctx, &product.ProductRequest{ProductIds: strings.Join(pids, ",")})
if err != nil {
return nil, err
}
var orders []*types.Order
for _, o := range orderRet.Orders {
if p, ok := productRet.Products[o.Id]; ok {
orders = append(orders, &types.Order{
OrderID: o.Orderid,
ProductName: p.Name,
})
}
}
return &types.OrderListResponse{Orders: orders}, nil
}

测试

  • 请确保order、product 的rpc服务器已经启动
1
2
3
4
5
PS E:\proj\gowork\studyGoZero\apps\product\rpc> go run .\product.go
Starting rpc server at 0.0.0.0:8081...

PS E:\proj\gowork\studyGoZero\apps\order\rpc> go run .\order.go
Starting rpc server at 0.0.0.0:8080...
  • 启动api层服务
1
2
E:\proj\gowork\studyGoZero\apps\app\api>go run .\api.go
Starting rpc server at 0.0.0.0:8888...
  • 浏览器打开http://127.0.0.1:8888/v1/order/list?uid=123 发现报错空指针
1
"content":"(/v1/order/list?uid=123 - 127.0.0.1:57933) runtime error: invalid memory address or nil pointer dereference\ngoroutine 57 [running]:\nruntime/debug.Stack()\n\tE:/app/Go/src/runtime/debug/stack.go:24 +0x65\ngithub.com/zeromicro/go-zero/rest/handler.RecoverHandler.func1.1()\n\tC:/Users/Administrator/go/pkg/mod/github.com/zeromicro/go-zero@v1.5.4/rest/handler/recoverhandler.go:16 +0x66\npanic({0x1f56680, 0x331f9d0})\n\tE:/app/Go/src/runtime/panic.go:884 +0x213\nexample.com/studyGoZero/apps/app/api/internal/logic.(*OrderListLogic).OrderList(0xc0002cfcf8, 0xc00011e380)\n\tE:/proj/gowork/studyGoZero/apps/app/api/internal/logic/orderlistlogic.go:31
  • 虽然报错 了,但是可以知道整个服务已经是通的,因为现在并没有订单的任何数据

总结

  • api层- 对外的BFF服务。接受来自客户端的请求,暴露HTTP接口。本次实例启动了api.go,并且连接了order和product的rpc
  • api/api.api 为具体的对我暴露的请求
  • app\api\internal\logic 对应api/api.api为对外暴露请求的具体逻辑,具体代码又调用了order和product的rpc的逻辑

image-20230725145056412

说明

  • 本次主要在win10, go 1.20.4下,利用go 操作mongodb

安装启动

  • 打开地址下载安装包
  • 解压后目录如下,添加两个文件夹data和logs文件夹,用来存储mongodb的数据和日志文件
1
2
3
4
5
6
7
8
9
D:\app\mongodb-win32-x86_64-windows-6.0.6>tree
卷 项目盘 的文件夹 PATH 列表
卷序列号为 8C07-3198
D:.
├─bin
├─data
│ ├─diagnostic.data
│ └─journal
└─logs
  • 启动服务
1
D:\app\mongodb-win32-x86_64-windows-6.0.6\bin>mongod.exe --dbpath D:\app\mongodb-win32-x86_64-windows-6.0.6\data --logpath D:\app\mongodb-win32-x86_64-windows-6.0.6\logs\mongodb.log
  • 启动成功后data目录下会自动生成MongoDB数据库的一些信息,logs目录下存的则是日志文件,文件内容是启动信息

  • 安装连接工具mongodb compass,连接mongodb,地址是本地127.0.0.1,默认端口是27017

image-20230712171639801

  • 默认连接没有用户名和密码,需要进行设置
  • 点击底部菜单,输入use admin

image-20230712180544757

  • 然后输入
1
2
3
4
5
6
7
8
db.createUser({
user: 'admin', // 用户名
pwd: '123456', // 密码
roles:[{
role: 'root', // 角色---超级管理员才可以使用该角色
db: 'admin' // 数据库
}]
})

image-20230712180758846

  • 其他常用语句
1
2
3
4
5
6
7
8
9

> show dbs
admin 132.00 KiB
config 108.00 KiB
local 72.00 KiB
user 40.00 KiB
>db.auth("admin","123456") -- 授权
{ ok: 1 }

  • mongodb compass 中为admin数据库中加入users的collection

image-20230712194613916

代码编写

服务端代码

  • 初始化项目
1
2
3
4
cd E:\proj\gowork
mkdir studyMongoDB
cd studyMongoDB
go mod init example.com/studyMongoDB
  • 用vscode 打开文件夹,安装依赖
1
2
3
go get -u github.com/gin-gonic/gin
go get go.mongodb.org/mongo-driver/mongo

  • 编写测试代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
//main.go

package main

import (
"context"
"fmt"
"log"
"time"

"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
"go.mongodb.org/mongo-driver/mongo/readpref"
)

type Student struct {
Id int32
Name string
Age int
}

// pool 连接池模式
func ConnectToDB(uri, name string, timeout time.Duration, num uint64) (*mongo.Database, error) {
// 设置连接超时时间
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
// 通过传进来的uri连接相关的配置
o := options.Client().ApplyURI(uri)
// 设置最大连接数 - 默认是100 ,不设置就是最大 max 64
o.SetMaxPoolSize(num)
// 发起链接
client, err := mongo.Connect(ctx, o)
if err != nil {
fmt.Println(err)
return nil, err
}
// 判断服务是不是可用
if err = client.Ping(context.Background(), readpref.Primary()); err != nil {
log.Fatal(err)
return nil, err
}
// 返回 client
return client.Database(name), nil
}

func main() {

toDB, err := ConnectToDB("mongodb://admin:123456@localhost:27017", "admin", time.Duration(2), 50)
fmt.Println(err)
if err != nil {
fmt.Println(err)
fmt.Println(toDB)
return
}
fmt.Println("sussces")
doc := Student{Name: "刘刘", Age: 18, Id: 1}

coll := toDB.Collection("users")
objId, err := coll.InsertOne(context.TODO(), doc)
fmt.Println(objId)
if err != nil {
return
}
fmt.Println("新增成功")
var one Student
filter := bson.D{{"id", 1}}
var ctx context.Context
var cancel func()
ctx, cancel = context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
err = coll.FindOne(ctx, filter).Decode(&one)
if err == mongo.ErrNoDocuments {
// Do something when no record was found
fmt.Println("record does not exist")
} else if err != nil {
log.Fatal(err)
}
fmt.Println(one)

}

  • 运行结果
1
2
3
4
5
6
7
PS E:\proj\gowork\studyMongoDb> go run .\main.go
<nil>
sussces
&{ObjectID("64ae992a5063fc95642035ed")}
新增成功
{1 刘刘 18}
PS E:\proj\gowork\studyMongoDb>

gin和mongodb

  • dbs\db.go为连接数据库代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package dbs

import (
"context"
"fmt"
"log"
"time"

"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
"go.mongodb.org/mongo-driver/mongo/readpref"
)

var (
Client *mongo.Client // database 话柄
Collection *mongo.Collection // collection 话柄
)

// pool 连接池模式
func ConnectToDB(uri, name string) {

cb := context.Background()
// 设置连接超时时间
ctx1, cancel := context.WithTimeout(cb, time.Duration(2))
defer cancel()
// 通过传进来的uri连接相关的配置
o := options.Client().ApplyURI(uri)
// 设置最大连接数 - 默认是100 ,不设置就是最大 max 64
o.SetMaxPoolSize(50)
// 发起链接
client, err := mongo.Connect(ctx1, o)
if err != nil {
fmt.Println(err)
return
}
// 判断服务是不是可用
if err = client.Ping(cb, readpref.Primary()); err != nil {
log.Fatal(err)
return
}
// 默认连接的数据库
_db := client.Database(name)

Client = client
// 默认连接集合为users
Collection = _db.Collection("users")
}
  • models/user.go 操作实体类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
package models

import (
"context"
"fmt"
"log"

"example.com/studyMongoDb/dbs"
"go.mongodb.org/mongo-driver/bson"
)

type User struct {
Key string `json:"key"`
Name string `json:"name"`
Password string `json:"password"`
Id int `json:"id"`
}


func (u *User) GetList(ctx context.Context) (users []User, err1 error) {
cur, err := dbs.Collection.Find(ctx, bson.D{})
if err != nil {
log.Fatal(err)
return
}
if err := cur.Err(); err != nil {
log.Fatal(err)
return
}
err = cur.All(ctx, &users)
if err != nil {
log.Fatal(err)
return
}
cur.Close(ctx)
return

}

func (u *User) Query() (user []User, err1 error) {
cb := context.Background()
fil := bson.M{"key": u.Key}
// 模糊查询
// bson.M{"Name": primitive.Regex{Pattern: u.name}}
cur, err := dbs.Collection.Find(cb, fil)
// dbs.Collection.FindOne(cb, fil).Decode(&user)
if err != nil {
log.Fatal(err)
return
}
if err := cur.Err(); err != nil {
log.Fatal(err)
return
}
err = cur.All(cb, &user)
if err != nil {
log.Fatal(err)
return
}
cur.Close(cb)
return

}

func (u *User) Add() (err error) {
objId, err1 := dbs.Collection.InsertOne(context.TODO(), &u)
if err1 != nil {
fmt.Println("insert into error:", err)

}
fmt.Println("录入数据成功,", objId)
return
}
func (u *User) EditOne() (err1 error) {
fil := bson.M{"key": u.Key}
update := bson.M{"$set": u}
// .update({条件},{修改后的数据})
updateResult, err := dbs.Collection.UpdateOne(context.Background(), fil, update)
if err != nil {
log.Fatal(err)
}
log.Println("collection.UpdateOne:", updateResult)
return
}

func (u *User) Delete() (err1 error) {
fil := bson.M{"key": u.Key}
deleteResult, err := dbs.Collection.DeleteOne(context.Background(), fil)
if err != nil {
log.Fatal(err)
}
log.Println("collection.DeleteOne:", deleteResult)
return
}

  • api\users.go 调用model层代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
package api

import (
"context"
"fmt"
"net/http"

"example.com/studyMongoDb/models"
"github.com/gin-gonic/gin"
)

func UserAdd(c *gin.Context) {
var user models.User
if c.Bind(&user) == nil { //把客户端格式传过来的数据绑定到结构体user中去
fmt.Println("data=", user)
err := user.Add() // 调用model层的对应方法
if err != nil {

c.JSON(http.StatusOK, gin.H{
"msg": "新增失败",
"code": -1,
})

} else {
c.JSON(http.StatusOK, gin.H{
"msg": "新增成功",
"code": 1,
})
}
} else {
c.JSON(400, gin.H{"JSON=== status": "binding JSON error!"})
}
}

func UserList(c *gin.Context) {
var user models.User
if c.Bind(&user) == nil { //把客户端格式传过来的数据绑定到结构体user中去
users, err := user.GetList(context.Background()) // 调用model层的对应方法
if err != nil {

c.JSON(http.StatusOK, gin.H{
"msg": "获取列表失败",
"code": -1,
})

} else {
c.JSON(http.StatusOK, gin.H{
"msg": "获取列表成功",
"code": 1,
"users": users,
})
}
} else {
c.JSON(400, gin.H{"JSON=== status": "binding JSON error!"})
}
}

func UserEditOne(c *gin.Context) {
var user models.User
if c.Bind(&user) == nil {
err := user.EditOne()
if err != nil {

c.JSON(http.StatusOK, gin.H{
"msg": "编辑失败",
"code": -1,
})

} else {
c.JSON(http.StatusOK, gin.H{
"msg": "编辑成功",
"code": 1,
})
}
} else {
c.JSON(400, gin.H{"JSON=== status": "binding JSON error!"})
}
}

func UserDelete(c *gin.Context) {
var user models.User
if c.Bind(&user) == nil {
err := user.Delete()
if err != nil {

c.JSON(http.StatusOK, gin.H{
"msg": "删除失败",
"code": -1,
})

} else {
c.JSON(http.StatusOK, gin.H{
"msg": "删除成功",
"code": 1,
})
}
} else {
c.JSON(400, gin.H{"JSON=== status": "binding JSON error!"})
}
}
func UserQuery(c *gin.Context, key string) {
user := models.User{
Key: key,
}
if c.Bind(&user) == nil {
user, err := user.Query()
if err != nil {

c.JSON(http.StatusOK, gin.H{
"msg": "查询失败",
"code": -1,
})

} else {
c.JSON(http.StatusOK, gin.H{
"msg": "查询成功",
"code": 1,
"user": user,
})
}
} else {
c.JSON(400, gin.H{"JSON=== status": "binding JSON error!"})
}
}

  • routers/router.go 路由层,调用api层的代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package routers

import (
"example.com/studyMongoDb/api"
"github.com/gin-gonic/gin"
)

func InitRouter() *gin.Engine {

router := gin.Default()
router.POST("/UserAdd", api.UserAdd)
router.GET("/UserList", api.UserList)
router.POST("/UserEditOne", api.UserEditOne)
router.POST("/UserDelete", api.UserDelete)
router.GET("/UserQuery/:key", func(c *gin.Context) {
key := c.Param("key")
api.UserQuery(c, key)
})

return router
}

  • main.go 代码入口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import (
"context"

"example.com/studyMongoDb/dbs"
"example.com/studyMongoDb/routers"
)

func main() {

dbs.ConnectToDB("mongodb://admin:123456@localhost:27017", "admin")
router := routers.InitRouter()
router.Run(":8000")
// 优雅关闭连接
defer dbs.Client.Disconnect(context.TODO())

}

  • 运行main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
PS E:\proj\gowork\studyMongoDb> go run .\main.go
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.

[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
- using env: export GIN_MODE=release
- using code: gin.SetMode(gin.ReleaseMode)

[GIN-debug] POST /UserAdd --> example.com/studyMongoDb/api.UserAdd (3 handlers)
[GIN-debug] GET /UserList --> example.com/studyMongoDb/api.UserList (3 handlers)
[GIN-debug] POST /UserEditOne --> example.com/studyMongoDb/api.UserEditOne (3 handlers)
[GIN-debug] POST /UserDelete --> example.com/studyMongoDb/api.UserDelete (3 handlers)
[GIN-debug] GET /UserQuery/:key --> example.com/studyMongoDb/routers.InitRouter.func1 (3 handlers)
[GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.
Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.
[GIN-debug] Listening and serving HTTP on :8000

客户端代码

  • 测试的客户端代码,使用python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import requests

data ={"name": "test111", "password": "1123456", "id": 2, "key": "t_key2"}
resp = requests.post("http://127.0.0.1:8000/UserAdd", json=data)
print(resp.text)


data ={"name": "test1131", "password": "123456811", "id": 1112, "key": "t_key1"}
resp = requests.post("http://127.0.0.1:8000/UserEditOne", json=data)
print(resp.text)
#
data ={"key": "t_key1"}
resp = requests.post("http://127.0.0.1:8000/UserDelete", json=data)
print(resp.text)


resp = requests.get("http://127.0.0.1:8000/UserQuery/t_key2/")
print(resp.text)

resp = requests.get("http://127.0.0.1:8000/UserList")
print(resp.text)