0%

场景

  • 本次java 版本
1
2
3
4
C:\Users\Administrator>java -version
java version "1.8.0_381"
Java(TM) SE Runtime Environment (build 1.8.0_381-b09)
Java HotSpot(TM) 64-Bit Server VM (build 25.381-b09, mixed mode
  • 泛型的本质是为了将类型参数化,数据类型被设置为一个参数,在使用时再从外部传入一个数据类型;而一旦传入了具体的数据类型后,传入变量(实参)的数据类型如果不匹配,编译器就会直接报错

  • 在 ArrayList 集合中,可以放入所有类型的对象,假设现在需要一个只存储了 String 类型对象的 ArrayList 集合

1
2
3
4
5
6
7
ArrayList list = new ArrayList();
list.add("aaa");
list.add("bbb");
list.add("ccc");
for (int i = 0; i < list.size(); i++) {
System.out.println((String)list.get(i));
}
  • 但如果在添加 String 对象时,不小心添加了一个 Integer 对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.util.ArrayList;

public class Main {
public static void main(String args[]) {
ArrayList list = new ArrayList<>();
list.add("aaa");
list.add("bbb");
list.add("ccc");
list.add(111); //新增数字
for (int i = 0; i < list.size(); i++) {
System.out.println((String)list.get(i));
}
}
}



  • 上述代码在编译时没有报错,但在运行时却抛出了一个 ClassCastException 异常,其原因是 Integer 对象不能强转为 String 类型
1
2
   Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
at Main.main(Main.java:11)
  • 使用泛型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import java.util.ArrayList;

public class Main {
public static void main(String args[]) {
ArrayList<String> list = new ArrayList<>();
list.add("aaa");
list.add("bbb");
list.add("ccc");
list.add(111); // 编译阶段,编译器会报错
for (int i = 0; i < list.size(); i++) {
System.out.println((String)list.get(i));
}
}
}

image-20231113153841330

  • 因此,当具体的数据类型确定后,泛型又提供了一种类型安全检测机制,只有数据类型相匹配的变量才能正常的赋值,否则编译器就不通过。所以说,泛型一定程度上提高了软件的安全性,防止出现低级的失误。

  • 泛型有三种使用方式,分别为:泛型类、泛型接口、泛型方法

泛型类

  • 定义
1
2
3
4
5
6
7
class 类名称 <泛型标识> {
private 泛型标识 /*(成员变量类型)*/ 变量名;
.....

}
}

  • 尖括号 <> 中的 泛型标识被称作是类型参数,用于指代任何数据类型
  • 泛型标识是任意设置的(如果你想可以设置为 Hello都行),Java 常见的泛型标识以及其代表含义如下:
1
2
3
4
5
6
T :代表一般的任何类。
E :代表 Element 元素的意思,或者 Exception 异常的意思。
K :代表 Key 的意思。
V :代表 Value 的意思,通常与 K 一起配合使用。
S :代表 Subtype 的意思,文章后面部分会讲解示意。

  • 举例如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Generic<T> { 
// key 这个成员变量的数据类型为 T, T 的类型由外部传入
private T key;
// 泛型构造方法形参 key 的类型也为 T,T 的类型由外部传入
public Generic(T key) {
this.key = key;
}

// 泛型方法 getKey 的返回值类型为 T,T 的类型由外部指定
public T getKey(){
return key;
}
}

  • 泛型类中的静态方法和静态变量不可以使用泛型类所声明的类型参数
1
2
3
4
5
6
7
public class Test<T> {    
public static T one; // 编译错误
public static T show(T one){ // 编译错误
return null;
}
}

  • 而静态变量和静态方法在类加载时已经初始化,直接使用类名调用;在泛型类的类型参数未确定时,静态成员有可能被调用,因此泛型类的类型参数是不能在静态成员中使用的。
  • 泛型类不只接受一个类型参数,它还可以接受多个类型参数
1
2
3
4
5
6
7
8
9
10
11
12
13
public class MultiType <E,T> {
E value1;
T value2;

public E getValue1(){
return value1;
}

public T getValue2(){
return value2;
}
}

  • 看如下实例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Generic<T> {
private T key;
public Generic(T key) {
this.key = key;
}
public T getKey() {
return key;
}
}


public class Main {
public static void main(String args[]) {
Generic<String> generic = new Generic<>("hello");
System.out.println(generic.getKey());

// // <> 中什么都不传入,等价于 Generic<Object> generic = new Generic<>();
Generic generic1 = new Generic("word");
System.out.println(generic1.getKey());
}
}

泛型接口

  • 定义
1
2
3
4
5
6
7
8
9
public interface 接口名<类型参数> {
...
}

public interface Inter<T> {
public abstract void show(T t) ;
}


  • 在泛型接口中,静态成员也不能使用泛型接口定义的类型参数
1
2
3
4
5
6
7
8
9
10
interface IUsb<U, R> {
int n = 10;
U name;// 报错! 接口中的属性默认是静态的,因此不能使用类型参数声明
R get(U u);// 普通方法中,可以使用类型参数
void hi(R r);// 抽象方法中,可以使用类型参数
default R method(U u) {
return null;
}
}

  • 定义一个接口 IA 继承了 泛型接口 IUsb,在 接口 IA 定义时必须确定泛型接口 IUsb 中的类型参数
1
2
3
4
//IA.java

// 在继承泛型接口时,必须确定泛型接口的类型参数
interface IA extends IUsb<String, Double> { }
  • AA继承了IA的接口,而IA接口继承了IUsb并指定了特定的泛型参数,因此AA其实就是实现了IUsb接口的方法get和hi方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 当去实现 IA 接口时,因为 IA 在继承 IUsu 接口时,指定了类型参数 U 为 String,R 为 Double
// 所以在实现 IUsb 接口的方法时,使用 String 替换 U,用 Double 替换 R
class AA implements IA {
public Double get(String s) {
System.out.println("AA 的Double get=" + s);
return null;
}
public void hi(Double d) {
System.out.println("AA hi=" +d);

}
public void test() {
System.out.println("test");

}
}
  • 运行代码
1
2
3
4
5
6
7
8
9
10
11
12
13
public class Main {
public static void main(String args[]) {
AA aa = new AA();
aa.get("哈哈");
aa.hi(10.01);
aa.test();
}
}

// 结果
AA 的Double get=哈哈
AA hi=10.01
test
  • 当然也可以直接用:定义一个类 BB 实现了 泛型接口 IUsb,在 类 BB 定义时需要确定泛型接口 IUsb 中的类型参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 实现接口时,需要指定泛型接口的类型参数
// 给 U 指定 Integer, 给 R 指定了 Float
// 所以,当我们实现 IUsb 方法时,会使用 Integer 替换 U, 使用 Float 替换 R
class BB implements IUsb<Integer, Float> {
@Override
public Float get(Integer integer) {
System.out.println("BB Float get=" + integer);
return 10.11F;
}
@Override
public void hi(Float afloat) {
System.out.println("BB hi=" +afloat);

}
}
  • 定义一个类 CC 实现了 泛型接口 IUsb 时,若是没有确定泛型接口 IUsb 中的类型参数,则默认为 Object
1
2
3
4
5
6
7
8
9
10
11
12
13
public class CC implements IUsb{
@Override
public Object get(Object o) {
System.out.println("CC object get=" + o.toString());
return null;
}
@Override
public void hi(Object o) {
System.out.println("cc object hi=" + o.toString());

}
}

  • 定义一个类 DD 实现了 泛型接口 IUsb 时,若是没有确定泛型接口 IUsb 中的类型参数,也可以将 DD 类也定义为泛型类,其声明的类型参数必须要和接口 IUsb 中的类型参数相同
1
2
3
4
5
6
// DD 类定义为 泛型类,则不需要确定 接口的类型参数
// 但 DD 类定义的类型参数要和接口中类型参数的一致
class DD<U, R> implements IUsb<U, R> {
...
}

泛型方法

  • 当在一个方法签名中的返回值前面声明了一个 < T > 时,该方法就被声明为一个泛型方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public <类型参数> 返回类型 方法名(类型参数 变量名) {
...
}

public class Test<U> {
// 该方法只是使用了泛型类定义的类型参数,不是泛型方法
public void testMethod(U u){
System.out.println(u);
}

// <T> 真正声明了下面的方法是一个泛型方法
public <T> T testMethod1(T t){
return t;
}
}

  • 实现了普通方法和泛型方法
1
2
3
4
5
6
7
8
9
10
public class Test<T> {
// 是泛型类中的普通方法
public void testMethod(T t) {
System.out.println(t);
}
// 是一个泛型方法
public <T> T testMethod1(T t) {
return t;
}
}
  • 运行
1
2
3
4
5
6
7
8
9
public class Main {
public static void main(String args[]) {
Test test = new Test<Integer>();
test.testMethod("112");
String tt = (String) test.testMethod1("你好");
System.out.println(tt);
}
}

  • 当然泛型方法也可用到普通类中
1
2
3
4
5
6
7
8
9
10
public class Test {
// 普通方法
public void testMethod(String t) {
System.out.println(t);
}
// 是一个泛型方法
public <T> T testMethod1(T t) {
return t;
}
}
  • 在调用泛型方法的时候,可以显式地指定类型参数,也可以不指定
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
public class Test {

// 这是一个简单的泛型方法
public static <T> T add(T x, T y) {
return y;
}

public static void main(String[] args) {
// 一、不显式地指定类型参数
//(1)传入的两个实参都是 Integer,所以泛型方法中的<T> == <Integer>
int i = Test.add(1, 2);

//(2)传入的两个实参一个是 Integer,另一个是 Float,
// 所以<T>取共同父类的最小级,<T> == <Number>
Number f = Test.add(1, 1.2);

// 传入的两个实参一个是 Integer,另一个是 String,
// 所以<T>取共同父类的最小级,<T> == <Object>
Object o = Test.add(1, "asd");

// 二、显式地指定类型参数
//(1)指定了<T> = <Integer>,所以传入的实参只能为 Integer 对象
int a = Test.<Integer>add(1, 2);

//(2)指定了<T> = <Integer>,所以不能传入 Float 对象
int b = Test.<Integer>add(1, 2.2);// 编译错误

//(3)指定<T> = <Number>,所以可以传入 Number 对象
// Integer 和 Float 都是 Number 的子类,因此可以传入两者的对象
Number c = Test.<Number>add(1, 2.2);
}
}

泛型类,在创建类的对象的时候确定类型参数的具体类型;
泛型方法,在调用方法的时候再确定类型参数的具体类型。

总结

  • 后续还有更多高级的用法,比如类型擦除、通配符等有空继续按照此博主内容进行学习

说明

本次笔记主要记录面向对象中的继承、重载重写、多态、抽象、封装、接口、包等概念

java版本为:java version “1.8.0_381”

继承

  • 继承格式有两种,方式一:
1
2
3
4
5
class 父类 {
}

class 子类 extends 父类 {
}
  • 方式二
1
2
3
4
5
6
7
8
9
10
11
public interface A {
public void eat();
public void sleep();
}

public interface B {
public void show();
}

public class C implements A,B {
}
  • 代码实例
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
// 动物类为父类主要包的动作为吃、睡觉、介绍等方法
public class Animal {
private String name;
private int id;
// 重载,主要用来初始化name 和id
public Animal(String myName, int myId) {
name = myName;
id = myId;
}
public void eat(){
System.out.println(name+"正在吃");
}
public void sleep(){
System.out.println(name+"正在睡");
}
public void introduction() {
System.out.println("大家好!我是" + id + "号" + name + ".");
}
}
// 继承父类
public class Mouse extends Animal{
public Mouse(String myName,int myId) {
// 调用的就是Animal父类的重载函数Animal(String myName, int myId)
super(myName, myId);
}
public void eat() {
System.out.println("子类:我吃完了");
}
public void eatTest() {
// 调用父类方法
super.eat();
// this指向子类本身
this.eat();

}
}

//super用来调用父类方法
// this指向子类本身

  • 得到结果
1
2
3
4
5
6
7
8
9
10

public class Main {
public static void main(String args[]) {
Mouse mose = new Mouse("老鼠",1);
mose.eatTest();
}
}
----
老鼠正在吃
子类:我吃完了

重写

  • 重写是子类对父类的允许访问的方法的实现过程进行重新编写, 返回值和形参都不能改变。即外壳不变,核心重写!
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
// 动物类为父类主要包的动作为吃、睡觉、介绍等方法
public class Animal {
private String name;
private int id;
// 重载,主要用来初始化name 和id
public Animal(String myName, int myId) {
name = myName;
id = myId;
}
public void eat(){
System.out.println(name+"正在吃");
}
public void sleep(){
System.out.println(name+"正在睡");
}
public void introduction() {
System.out.println("大家好!我是" + id + "号" + name + ".");
}
}

//子类
public class Mouse extends Animal{
private String Name;
private int Id;
public Mouse(String myName,int myId) {
// 调用的就是Animal父类的重载函数Animal(String myName, int myId)
super(myName, myId);
this.Name = myName;
this.Id = myId;
}
public void eat() {
System.out.println(this.Name+ ":我吃完了");
}
// 自定义方法
public void dark() {
System.out.println("老鼠可以在叫");
}
}


import java.io.*;

public class Main {
public static void main(String args[]) {
Animal a = new Animal("我是动物父类",1001);
Animal b = new Mouse("老鼠",1);
a.eat(); // 调用父类的eat方法,结果为:我是动物父类正在吃
b.eat(); // 尽管b属于Animal类型,但是它运行的是 Mouse子类的eat,这里重写了父类的eat方法,结果为:老鼠:我吃完
b.sleep(); //同时b调用分类本身的sleep
// 总结:当分类指向子类时
// 1.若子类和父类使用相同方法eat,那么子类中的方法eat重写了父类中的方法eat
// 2.父类可直接调用父类中方法
// 3.子类中的方法在父类中不存在,那么指定调用子类中的方法报错

// 这里标红报错了
b.dark();

}
}

当需要在子类中调用父类的被重写方法时,要使用 super 关键字。继承中已经介绍

重载

  • 重载(overloading) 是在一个类里面,方法名字相同,而参数不同。返回类型可以相同也可以不同
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
public class Overloading {
public int test(){
System.out.println("test1");
return 1;
}

public void test(int a){
System.out.println("test2");
}

//以下两个参数类型顺序不同
public String test(int a,String s){
System.out.println("test3");
return "returntest3";
}

public String test(String s,int a){
System.out.println("test4");
return "returntest4";
}

public static void main(String[] args){
Overloading o = new Overloading();
System.out.println(o.test());
o.test(1);
System.out.println(o.test(1,"test3"));
System.out.println(o.test("test4",1));
}
}

实例中重载方法为test

多态

  • 多态存在的三个条件:继承、重写、父类引用指向子类对象,在重写实例中刚好包含了这三要素。

抽象

抽象类

  • 如果一个类中没有包含足够的信息来描绘一个具体的对象,这样的类就是抽象类。

  • 抽象类除了不能实例化对象之外,类的其它功能依然存在,抽象类必须被继承,才能被使

  • 一个类只能继承一个抽象类,而一个类却可以实现多个接口(留作后续)

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
// 动物类为父类主要包的动作为吃、睡觉、介绍等方法
public abstract class Animal {
private String name;
private int id;
// 重载,主要用来初始化name 和id
public Animal(String myName, int myId) {
name = myName;
id = myId;
}
public void eat(){
System.out.println(name+"正在吃");
}
public void sleep(){
System.out.println(name+"正在睡");
}
public void introduction() {
System.out.println("大家好!我是" + id + "号" + name + ".");
}
}

// 子类
public class Mouse extends Animal{
private String Name;
private int Id;
public Mouse(String myName,int myId) {
// 调用的就是Animal父类的重载函数Animal(String myName, int myId)
super(myName, myId);
this.Name = myName;
this.Id = myId;
}
public void eat() {
System.out.println(this.Name+ ":我吃完了");
}
// 自定义方法
public void dark() {
System.out.println("老鼠可以在叫");
}
}


public class Main {
public static void main(String args[]) {
// 这样不行报错了,不可用直接实例化
// Animal a = new Animal("我是动物父类",1001);
Mouse mouse = new Mouse("老鼠", 1001);
Animal a = new Mouse("老鼠2号", 1002);
a.eat(); //老鼠2号:我吃完了
mouse.eat(); // 老鼠:我吃完了
mouse.sleep();
// 子类中的方法在父类中不存在,调用子类中的方法报错
//a.dark();
}
}

  • 和重写的那个实例非常接近,唯一区别就是不用实例化父类
  • 根据本实例发现既然无法实例化父类,那么父类中的里具体逻辑存在没有必要,只要存在一个方法名就可以了

抽象方法

  • 如果你想设计这样一个类,该类包含一个特别的成员方法,该方法的具体实现由它的子类确定,那么你可以在父类中声明该方法为抽象方法。
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
// 分类
public abstract class Animal {
// 重载,主要用来初始化name 和id
public abstract void eat();
public abstract void sleep(String myName);
public abstract String test();
}

// 子类
public class Mouse extends Animal{
private String Name;
private int Id;
public Mouse(String myName,int myId) {
this.Name = myName;
this.Id = myId;
}
public void eat() {
System.out.println(this.Name+ ":我吃完了");
}
public void sleep(String myName) {
System.out.println(myName+ "在睡觉");
}
public void dark() {
System.out.println("老鼠可以在叫");
}
public String test() {
return "我是一只老鼠";
}
}


public class Main {
public static void main(String args[]) {
Mouse mouse = new Mouse("老鼠", 1001);
Animal a = new Mouse("老鼠2号", 1002);
a.eat(); //老鼠2号:我吃完了
mouse.eat(); // 老鼠:我吃完了
String ts = a.test();
System.out.println(ts); // 我是一只老鼠
a.sleep("我是老鼠啊");// 我是老鼠啊在睡觉
mouse.dark();
// a.dark(); 子类中的方法在父类中不存在,调用子类中的方法报错
}
}
  • 抽象类中不一定包含抽象方法,但是有抽象方法的类必定是抽象类。
  • 抽象类中的抽象方法只是声明,不包含方法体,就是不给出方法的具体实现也就是方法的具体功能
  • 抽象类的子类必须给出抽象类中的抽象方法的具体实现,除非该子类也是抽象类

接口

  • 接口和抽象类很像,他们是有区别的

  • 接口和抽象类区别

    • 抽象类中的方法可以有方法体,就是能实现方法的具体功能,但是接口中的方法不行

    • 抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是 public static final 类型的

    • 一个类只能继承一个抽象类,而一个类却可以实现多个接口

    • 接口是隐式抽象的,当声明一个接口和方法的时候,不必使用abstract关键字

  • 实战代码

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
// 定义接口
interface Animal {
public void eat();
public void sleep(String myName);
public String test();
}

// 实现接口
public class Mouse implements Animal{
private String Name;
private int Id;
public Mouse(String myName,int myId) {
this.Name = myName;
this.Id = myId;
}
public void eat() {
System.out.println(this.Name+ ":我吃完了");
}
public void sleep(String myName) {
System.out.println(myName+ "在睡觉");
}
public void dark() {
System.out.println("老鼠可以在叫");
}
//如果不实现接口中的test方法,那么就报错了
public String test() {
return "我是一只老鼠";
}
}


public class Main {
public static void main(String args[]) {
Mouse mouse = new Mouse("老鼠", 1001);
mouse.eat(); // 老鼠:我吃完了
String ts = mouse.test();
System.out.println(ts); // 我是一只老鼠
mouse.sleep("我是老鼠啊");// 我是老鼠啊在睡觉
mouse.dark(); // 老鼠可以在叫
}
}

当类实现接口的时候,类要实现(重写)接口中所有的方法,不然报错。否则,类必须声明为抽象的类

接口继承

  • 感觉这里太复杂,不做详细介绍,做个简单笔记。
  • 一个接口能继承另一个接口,和类之间的继承方式比较相似。接口的继承使用extends关键字,子接口继承父接口的方法。

下面的Sports接口被Hockey和Football接口继承:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 文件名: Sports.java
public interface Sports
{
public void setHomeTeam(String name);
public void setVisitingTeam(String name);
}

// 文件名: Football.java
public interface Football extends Sports
{
public void homeTeamScored(int points);
public void visitingTeamScored(int points);
public void endOfQuarter(int quarter);
}

// 文件名: Hockey.java
public interface Hockey extends Sports
{
public void homeGoalScored();
public void visitingGoalScored();
public void endOfPeriod(int period);
public void overtimePeriod(int ot);
}

在Java中,类的多继承是不合法,但接口允许多继承。

在接口的多继承中extends关键字只需要使用一次,在其后跟着继承接口。 如下所示

1
public interface Hockey extends Sports, Event

环境搭建

  • 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服务等,下面我们开始绩继续的实践过程
  • 本章主要对数据库的表新建,以及常见的增删改查