编程新务实验四 / 使用SpringBoot + Flutter实现一个具有登录,注册,邮箱验证,增删改查页面,社区功能,个人中心功能的APP

编程新务 专栏收录该内容
1 篇文章 0 订阅

image-20210113154916263

实验介绍

这是编程务实实验lab4,我使用SpringBoot + Flutter技术实现了一个具有登录,注册,邮箱验证,增删改查页面,社区功能,个人中心功能的APP。下面是整个APP项目的页面展示:
在这里插入图片描述
在这里插入图片描述

环境:

  • 操作系统: Ubuntu / windwos
  • 编译器: IDEA / VsCode
  • 解释器: JAVA JDK15
  • Flutter 1.22.4
  • 解释器 Dart SDK 2.10.4
  • 插件: lombok
  • mySQL 8.02

技术栈

后端:

  • Java JDK
  • MySQL
  • SpringBoot
  • JPA: 持久层
  • hibernate: 面向代码编程,而不是面向数据库编程,不再受于数据库表上去思考问题,不再并数据模型的实现模式,以对象模型的表达方式去实现数据模型。先编写模型的数据结构与各模型间的关系,hibernate可以通过代码的实体模型去建表等。
  • JavaMail邮件
  • druid
  • lombok
  • SpringMysecrity
Java JDK, MySQL, SpringBoot, JPA: 持久层, hibernate: 面向代码编程,而不是面向数据库编程,不再受于数据库表上去思考问题,不再并数据模型的实现模式,以对象模型的表达方式去实现数据模型。先编写模型的数据结构与各模型间的关系,hibernate可以通过代码的实体模型去建表等。
JavaMail邮件, druid, lombok, SpringMysecrity

前端:

  • Android SDK

  • Flutter

  • 前端语言: Dart

  • Dart

  • Dio

  • OKToast

  • FlutterToast

  • image_picker

项目结构

整个目录包含两个结构,分别为
后端:springblog
前端:Welcome-Login-Signup-Page-Flutter
整个目录包含两个结构,分别为
后端:springblog
前端:Welcome-Login-Signup-Page-Flutter
  • 后端
    1.png

  • config:配置文件

  • controller:接口层

  • entity:实体类

  • repository:继承仓库

  • service:图片上传,登录注册,验证码等复杂功能

  • util:工具类

  • 前端

2.png

  • componts:组件
  • screen:页面
  • Home:主页面
  • login:登录页面
  • signup:注册页面
  • welcome:欢迎页面
  • utils:工具类

一. 后端

JavaMail邮箱验证

  • 包依赖:
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-mail</artifactId>
</dependency>
  • yaml:
# 邮箱
mail:
    host: smtp.aliyun.com
    username: qdl.cs@aliyun.com
    password: "********"
    port: 465
    properties.smtp.auth: true
    properties.smtp.timeout: 2500
    properties.mail.smtp.ssl.enable: true
  • 下面是一个测试:
@Test
void contextLoads() {
	SimpleMailMessage simpleMailMessage = new SimpleMailMessage();
	//邮件设置值
	simpleMailMessage.setSubject("测试邮件-java邮件任务");//邮件主题
	simpleMailMessage.setText("测试邮件,测试java发送邮件任务......");//邮件内容
	simpleMailMessage.setTo("qdl.cs@qq.com");//邮件发给谁
	simpleMailMessage.setFrom("qdl.cs@aliyun.com"); //邮件来自于谁
	javaMailSender.send(simpleMailMessage);
}

JPA模糊查询

  • 用下面的拼接无效!
%username%
  • 使用类似的接口:
findAllByUsernameContains
注意Like在JPA中间仅仅是不匹配大小写

image-20201208210259087

密码加密

  • 使用spring-security进行密码加密
// 使用spring-security进行密码加密
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(12);

SpringBoot跨域请求

  • 对于<version>2.3.4.RELEASE</version>的版本,可以使用如下方法处理跨域请求
package com.lin.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/*
 * Cros 跨映射
 * created by 屈德林 in 2020/11/03
 * */
@Configuration
public class CrosConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("*")
                .allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS")
                .allowCredentials(true)
                .maxAge(3600)
                .allowedHeaders("*");
    }
}
  • 由于我开始使用了<version>2.4.0</version>的版本, 造成跨域失败, 一堆BUG, 救命, 无奈只能降版本

JPA更新

  • 由于JPA中没有update方法, 只能使用save, 在save之前要先将数据删除
    @PostMapping(value="/update")
    public String update(@RequestBody UserProfile userProfile) {
        User user=userRepository.findByEmail(userProfile.getEmail());
        if(user!=null){
            user.setUsername(userProfile.getUsername());//更新用户名
            userRepository.deleteUserByEmail(user.getEmail());
            userRepository.save(user);                  //更新user

            userProfileRepository.deleteUserProfileByEmail(userProfile.getEmail());
            userProfileRepository.save(userProfile);    //更新userProfile
            return "sucess";
        }
        else
            return "User Don't Exit, Please Register First!";
    }

事务管理

  • JPA No EntityManager with actual transaction available for current thread
原因是在删除操作上没有添加事务管理。
解决方法:
在对应的@Service或组件上添加@Transactional即可

RoundedInputField 输入文本框

  • https://github.com/Chromicle/awesome-flutter-ui?ref=producthunt
RoundedInputField(
    textEditingController: controllerName,
    hintText: "Your Email",
    icon: Icons.email,
    cursorColor: Colors.black,
    editTextBackgroundColor: Colors.grey[200],
    iconColor: Colors.black,
    onChanged: (value) {
      name = value;
     },
 )

后端图片保存

  • https://github.com/wissensalt/springboot-image-gallery

image-20201208111125216

@Service
public class UploadService {

    @Value("${upload.dir}")
    private String uploadDir;

    @Transactional
    public String upload(@RequestParam("file") MultipartFile file, String fileName) {
        if (file.isEmpty()) {
            return "empty";
        }
        try {
            byte[] bytes = file.getBytes();
            //组合成文件名
            Path path = Paths.get(uploadDir +fileName);
            //写入服务器
            Files.write(path, bytes);
        }catch (Exception e) {
            return "error";
        }
        return "sucess";
    }
}

重定向 访问图片

  • 通过配置类访问/images/目录下的文件
@Configuration
@EnableWebMvc
@ComponentScan(basePackages = "com.wissensalt.rnd.springbootimagegallery")
public class WebConfig implements WebMvcConfigurer {

    @Value("${upload.dir}")
    private String uploadDir;

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        if (!registry.hasMappingForPattern("/webjars*//**")) {
            registry.addResourceHandler("/webjars*//**").addResourceLocations("classpath:/META-INF/resources/webjars/");
        }
        if (!registry.hasMappingForPattern("*//**")) {
            registry.addResourceHandler("*//**").addResourceLocations("classpath:/static");
        }
        registry.addResourceHandler("/images/**").addResourceLocations("file:"+uploadDir).setCachePeriod(0);
    }
}
  • 配置以后就能个通过IP/images访问图片了

PoastMan上传MultipartFile

image-20201208151304576

二. 客户端

Dio 请求发送

  • DIO: https://github.com/flutterchina/dio

  • 依赖:

dependencies:
  dio: 3.x #latest version
  • 基本使用方法:
response = await dio.post(
  "http://www.dtworkroom.com/doris/1/2.0.0/test",
  data: {"aa": "bb" * 22},
  onSendProgress: (int sent, int total) {
    print("$sent $total");
  },
);

image-20201207085541118

  • 对于json数据, Flutter中序列化不够完善,所以建议直接用下标
    if (response.statusCode == 200) {
      showToast("sucess");
      emailContr.text=response.data[1];
      usernameContr.text=response.data[2];
      nameContr.text=response.data[3];
      telephoneContr.text=response.data[4];
      ageContr.text=response.data[5].toString();
      jobContr.text=response.data[6];
    }

Row

  • 中防止Overflow错误

Row中,我们可以通过Flex或者Explanded防止Overflow错误。

Row(
    mainAxisAlignment: MainAxisAlignment.start,
    children: <Widget>[
        Padding(
            padding: EdgeInsets.only(left: 8.0, right: 20.0), child: Icon(Icons.account_balance),),
            Expanded(
                child: Text(
                  'Main Cotent. This is a demo to show how to avoid overflow in a Row widget'),
            ),
          ],
)
  • 设置间距:http://findsrc.com/flutter/detail/8830

页面路由 和 路由传参

  • 对于返回的功能, 可以设置一个floatingActionButton, 使用Navigator.of(context).pop(true) 进行返回
  • 如果你想返回之后刷新页面, 请根据回调判断
class LoginScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      floatingActionButton: FloatingActionButton(
          child: Icon(Icons.keyboard_backspace_rounded),
          onPressed: (){
            //页面路由
            Navigator.of(context).pop(true);
          }
      ),
      body: Body(),
    );
  }
}
  • 路由传参:
Navigator.of(context).push(MaterialPageRoute(
	builder: (BuildContext context) => Call(id: _contact.phones.first.value)
));
  • 构造函数:
class Call extends StatefulWidget {
  //必传参数
  Call({Key key, @required this.id}) : super(key: key);
  String id;

  @override
  State<StatefulWidget> createState() {
    return CallBuilder();
  }
}
  • 如果组件管理类要使用父类的参数, 要使用:
  //初始化全局状态8
  void initState() {
    if(widget.id!=null){
      phoneStr=widget.id;   //初始化数据
      for(int i=0;i<phoneStr.length;i++){
        outputnumber.add(phoneStr[i]);
      }
      telno.text = phoneStr;
    }
    super.initState();
    for (int i = 1; i < 10; i++) {
      number.add(i);
    }
    number.add('call');
    number.add(0);
    number.add('del');
  }

如何验证

  • confirmation_token表中查找user对应的token, 然后与前端传来的token对比, 注意, 使用equals
	@PostMapping(value="/register")
	public String registerUser(@RequestBody User user,@RequestParam("token") String token) {
		User existingUser = userRepository.findByEmail(user.getEmail());
		if(existingUser != null && existingUser.getIsEnabled()==1 ) {
			return "existingUser";
		}
		else {
			ConfirmToken confirmToken = confirmationTokenRepository.findByUser(existingUser);
			System.out.println(confirmToken);

			System.out.println(token);
			//验证成功
			if(confirmToken.getConfirmationToken().equals(token)){
				existingUser.setIsEnabled(1);		//激活
				userRepository.save(existingUser);
				return "sucess";
			}
			else {
				return "badToken";
			}
		}
	}

模态弹窗

  • pubspec.yaml文件添加依赖
dependencies:
  oktoast: ^2.0.0
  • main.dart在根布局添加OKToast(第一次用,忘记加这个,导致调用showToast没有作用)
class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return OKToast(
      child: MaterialApp(
        title: 'test',
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        home: MyHomePage(title: ''),
      ),
    ) ;
  }
}
  • 在需要的地方调用:
showToast("toast");

前台似乎很方便? 因为后台已经处理了大部分逻辑!

Dart数据类型转换

一,数值类型和字符传类型

  • 数值类型

    int       整数类型
    double    浮点数类型
    
  • 字符串类型

    String    字符串
    

二,互相转换的方法

  • 字符串转

    int

    数值类型

    var a = int.parse('1234'); //把字符串 1234 转换成 数值 1234
    print(a is int); //判断是否转换成功
    //输出 ture 
    
  • 字符串转double数值类型

    var b = double.parse('1234.12'); //把字符串 1234.12 转换成 数值 1234.12
    print(b is double); //判断是否转换成功
    //输出 ture 
    
  • 数值类型转字符类型

    var str = 1234.toString(); //把数值 1234 转换成 字符串 1234
    print(str is String); //判断是否转换成功
    //输出 ture
    

数据初始化

  • 在页面加载的时候Flutter会调用initState

    我们可以把要初始化的数据操作写在这里

  @override
  void initState() {
    getData();
    super.initState();
  }
  • 为了给私有变量赋值, 必须要用setState
  void getData() async {
    //获取数据
    String getUrl = baseURL+'/profile/findAll';
    Dio dio = new Dio();

    var response = await dio.get(getUrl);

    print('Respone ${response.statusCode}');

    //前台似乎很方便? 因为后台已经处理了大部分逻辑! shit, 我是个全栈, 都由我来做!
    if (response.statusCode == 200) {
      showToast("sucess");
      print(response.data);
      print("--------------------");
      setState(() {
        //必须要通过这个来更新数据,否则将不会刷新页面
        personList=response.data;
      });
    }
    else{
      showToast("服务器或网络错误!");
    }
  }

弹窗

  • 通过showAlertDialog实现弹窗
showAlertDialog(BuildContext context) {
 
  //设置按钮
  Widget cancelButton = FlatButton(
    child: Text("Cancel"),
    onPressed:  () {
        NavigatorUtils.goBack(context),
    },
  );
  Widget continueButton = FlatButton(
    child: Text("Continue"),
    onPressed:  () {},
  );
 
  //设置对话框
  AlertDialog alert = AlertDialog(
    title: Text("AlertDialog"),
    content: Text("Would you like to continue learning how to use Flutter alerts?"),
    actions: [
      cancelButton,
      continueButton,
    ],
  );
 
  //显示对话框
  showDialog(
    context: context,
    builder: (BuildContext context) {
      return alert;
    },
  );
}

前端图片上传

  • 依赖
  dio: 3.0.8
  fluttertoast: ^3.1.3
  image_picker: ^0.6.3+1
  • DIO
  //上传图片
  _upLoadImage(File image) async {
    String path = image.path;
    var name = path.substring(path.lastIndexOf("/") + 1, path.length);

    FormData formdata = FormData.fromMap({
      "file": await MultipartFile.fromFile(path, filename:name)
    });

    Dio dio = new Dio();
    var respone = await dio.post<String>("路径", data: formdata);
    if (respone.statusCode == 200) {
      Fluttertoast.showToast(
          msg: "图片上传成功",
          gravity: ToastGravity.CENTER,
          textColor: Colors.grey);
    }
  }
  • ImagePicker
  //获取图片
  Future getImage() async {
    var image = await ImagePicker.pickImage(source: ImageSource.gallery);
    _upLoadImage(image);//上传图片
    setState(() {
      _image = image;
    });

页面跳转销毁的问题

前端校验

  • https://cloud.tencent.com/developer/article/1456584
  • https://blog.csdn.net/u013491829/article/details/108555553?utm_medium=distribute.pc_relevant.none-task-blog-OPENSEARCH-4.control&depth_1-utm_source=distribute.pc_relevant.none-task-blog-OPENSEARCH-4.control
  • https://blog.csdn.net/takashi_constantine/article/details/79220955?utm_medium=distribute.pc_relevant_bbs_down.none-task-blog-baidujs-1.nonecase&depth_1-utm_source=distribute.pc_relevant_bbs_down.none-task-blog-baidujs-1.nonecase
  • https://www.jb51.net/tools/regexsc.htm 最全面最高效

带搜索功能的APPBAR

  • https://www.jianshu.com/p/520f9b579c17
  • 绑定事件
onChanged: (value){
    setState(() {
        searchUsername=value;
    });
},
onSearch: _Search,

页面销毁

页面销毁

  • 当页面被销毁的时候,最好主动销毁对应的控制器,主动释放内存,提高性能:
// 重新 dispose 函数
@override
void dispose() {
  super.dispose();
  // 主动销毁滚动控制器
  _scrollCtrl.dispose();
}

三. 运维

  • 服务器
	外网面板地址: http://39.103.148.126:8888/464ed98d
内网面板地址: http://172.26.16.204:8888/464ed98d
username: pmy35rss
password: eae10bba
If you cannot access the panel,
release the following panel port [8888] in the security group
若无法访问面板,请检查防火墙/安全组是否有放行面板[8888]端口
  • 部署
java -jar <jar-packagename>
  • 环境配置:

image-20201213095737975

  • screen会话

image-20201213095832273

  • 2
    点赞
  • 1
    评论
  • 6
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 成长之路 设计师:Amelia_0503 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值