项目实训(校园互助平台)


前言

大三下学期,学院开设了一门名为“项目实训”的课程。一共有三种参与方式,我和我的三个舍友组队成一队,选择了第二种参与方式,也就是独立完成一个完整的项目。项目要求创新,也就是说要么立意创新,要么算法创新。我们掂量掂量了自己,发现半桶水晃啊晃的,于是果断决定立意创新。绞尽脑汁冥思苦想了两天后,一个idea终于在我们用抢红包的方式决定谁去食堂打饭时冒了出来——校园互助平台!


以下是我们项目实训的完整过程(持续更新中…)

一、开题答辩(3月11号)

直到教务老师把“答辩日期定在后天中午”的消息发在群里,我们才不得不面对这残酷的现实。正所谓“巧妇难为无米之炊”,在答辩的前一晚,我们的项目进度仅仅停留在了刚确定好了队伍的名字的程度。

意外轻松的,我们通过了答辩,但是轻松的代价是保证每日有300日活量,顺便提一句,我们一个年级300多人。

在我们看来,这几乎是一个不可能完成的任务。按照规则,如果我们最后项目没有通过,就必须选择第三种方式——花费半个暑假的时间完成项目实训。“放弃吧,直接快进到暑假吧”。我们都萌生出了这个念头。

经过了很多努力,最终目标被修改为每日完成50笔订单。虽然仍然困难,但是我们终于有了坚持做下去的动力。

二、准备阶段

1.任务分配(3月12日)

今天我们综合考虑每个队员的意愿,大致分配了任务。我的任务是完成后端的登录注册、聊天、个人信息、信誉模块,并使用Springboot提供API接口。

2.购买服务器,搭建服务器(3月13日)

为了保证最终软件产品的推广效益,我们决定开发微信小程序。要想完成这个项目,最基本的一点,就是我们需要有一个真正的服务器。

然而我们所有人都没有搭建服务器的经验。在过去我们做课设都是以自己本机作为服务器“自娱自乐”,显然,现在不行。在立项时我负责的是部分后端工作,这份任务便落在了我的身上。

对于一个像我这样的菜鸟新手来说,这个过程可以说是一步一个坑。

其中卡了我最久也最让我无语的一个坑是:当你需要打开服务器的一个端口时,不仅需要在命令窗口中打开,还需要去阿里云的控制平台手动打开防火墙!!!
在这里插入图片描述

即使过程很坎坷、服务器性能堪忧,但令人兴奋的是,Hellow World终于出现在了屏幕上。

就结果来说还算顺利,成功搭建了一个具备MySQL数据库、Java运行环境、Python运行环境的阿里云服务器。

下面是我搭建的具体过程,希望能给你一些参考:

https://blog.csdn.net/ListenMaybe/article/details/114761335

3.购买域名,申请备案(3月15日-3月20日)

微信小程序要求服务器必须绑定域名。所以一个合法、有效的域名也是必要的。

不幸的是,我对域名的申请也是两眼一抹黑,摸着石头过河。

购买域名倒是一个比较简单的活。阿里云平台就可以购买,几十块钱就能买到一个。

申请备案却是一个麻烦事。由于我没有申请的经历与经验,申请的表单被阿里云初审打回来好几次,几乎把能犯的错都翻了一遍,错误还都很弱智,比如身份证照片模糊、居住地址不一致、网站备注信息不合法等等,关键每次都是真人审核,打电话通知,几次尴尬地都想把电话挂了。

耗时一周,初审终于顺利通过,接下来就是等待管局的审核,为期大概半个月。

4.域名备案成功,添加ssl证书(3月22日)

管局的审核终于通过了,域名也就正式可以启用了。但是微信小程序还要求服务端提供HTTPS的安全请求。

ssl证书比较好获取,阿里云平台提供免费的ssl证书。

除了服务需要绑定ssl证书外,springboot项目也需要开启HTTPS请求。

1.配置文件中加入如下配置
在这里插入图片描述
2.在配置类中添加如下代码,将HTTP请求转向HTTPS。

 // 监听的http请求的端口,需要在application配置中添加http.port=端口号
    @Value("${http.port}")
    Integer httpPort;

    //正常启用的https端口 如443
    @Value("${server.port}")
    Integer httpsPort;

    @Bean
    public TomcatServletWebServerFactory servletContainer() {
        TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory() {
            @Override
            protected void postProcessContext(Context context) {
                SecurityConstraint constraint = new SecurityConstraint();
                constraint.setUserConstraint("CONFIDENTIAL");
                SecurityCollection collection = new SecurityCollection();
                collection.addPattern("/*");
                constraint.addCollection(collection);
                context.addConstraint(constraint);
            }
        };
        tomcat.addAdditionalTomcatConnectors(httpConnector());
        return tomcat;
    }

    @Bean
    public Connector httpConnector() {
        Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol");
        connector.setScheme("http");
        //Connector监听的http的端口号
        connector.setPort(httpPort);
        connector.setSecure(false);
        //监听到http的端口号后转向到的https的端口号
        connector.setRedirectPort(httpsPort);
        return connector;
    }

3.将证书文件扔到项目根目录中

经过上述步骤后,服务器便可以接受HTTPS的请求了。

5.文档生成库JApiDocs的使用(3月30日)

我们的项目是前后端分离进行的,这就需要生成一份清晰明了的接口API文档。

现在用于Springboot生成文档的主流库是swagger,我粗粗看了一下,发现有点复杂,感觉有点头秃。这个时候我惊喜的发现了一个针对与Springboot的轻量级的库,能够十分简单方便地自动生成接口API文档,唯一要求就是规范地编写注释
需要引入的依赖如下:

  		<dependency>
            <groupId>io.github.yedaxia</groupId>
            <artifactId>japidocs</artifactId>
            <version>1.4.3</version>
        </dependency>

然后按照规范书写注释:

    /**
     * 测试文档生成
     * @param testString 测试数据参数
     * @return 返回测试参数
     */
    @RequestMapping(value = "/docs")
    @ResponseBody
    public String getIn(String testString) {
        return "testRerunString";
    }

生成文档的代码,随便放入一个main方法中执行,Test方法中也可以:

        DocsConfig config = new DocsConfig();
        config.setProjectPath("E:\\"); // 项目根目录
        config.setProjectName("test-project"); // 项目名称
        config.setApiVersion("V1.0");       // 声明该API的版本
        config.setDocsPath("E:\\"); // 生成API 文档所在目录
        config.setAutoGenerate(Boolean.TRUE);  // 配置自动生成
        Docs.buildHtmlDocs(config);

这样生成的就是HTML格式的文档:
在这里插入图片描述

如果想要转换其他格式的文档,可以添加如下配置:

 config.addPlugin(new MarkdownDocPlugin());

这样生成的文件夹中就不仅有网页格式的文档,还有md格式的:
在这里插入图片描述

你还可以自行将Markdown转换成PDF或者其他你想要的格式。

到此准备阶段正式完成。


三、正式开发阶段

1.图片的存储与访问(3月20日-25日)

头像、聊天、任务等都涉及到图片的存储和访问问题。

为了提高数据库查询的效率,我打算将图片存储到服务器硬盘上,然后数据库表中存储图片的URL。

前端传送过来的图片数据是Base64码,直接存储会出问题,需要对Base64码进行一些处理:

 //处理Base64
 public String fixBase64Code(String originCode){
        //前台在用Ajax传base64值的时候会把base64中的+换成空格,所以需要替换回来。
        String baseValue = originCode.replaceAll(" ", "+");
        //去除base64值开头的声明
        baseValue = baseValue.replace("data:image/jpeg;base64,", "");
        baseValue = baseValue.replace("data:image/png;base64,", "");
        return baseValue;
 }

将处理后的Base64码转存为图片需要对其进行解密操作。我使用的是sun.misc.BASE64Decoder库的方法。

//生成图片
    public boolean generateImage(String imgStr, String path) {
        if (imgStr == null)
            return false;
        BASE64Decoder decoder = new BASE64Decoder();
        try {
            // 解密
            byte[] b = decoder.decodeBuffer(imgStr);
            // 处理数据
            for (int i = 0; i < b.length; ++i) {
                if (b[i] < 0) {
                    b[i] += 256;
                }
            }
            OutputStream out = new FileOutputStream(path);
            out.write(b);
            out.flush();
            out.close();
            return true;
        } catch (Exception e) {
            return false;
        }
    }

经过处理、解密、存储操作后,前端传来的Base64码就顺利变成了图片存在了硬盘上。接下来需要处理的就是图片路径问题。

由于项目使用的是SpringBoot框架,集成了Tomcat,所以项目发布过程变得十分简单,只需要将项目打成jar后运行即可。但是打成jar包后的项目如何存储和访问静态图片资源呢?

经过实际测试后我发现,无论是在Windows还是Linux系统中,如果图片保存的路径为项目所在文件夹,那么实际保存路径反而去到了项目jar包所在文件夹的上一级文件夹中;如果要将图片保存在与包同一级的文件夹中,那么图片的保存路径需要填写项目根路径!!

关于如何获取项目根路径,网上的答案五花八门,我试了很多都失败了。最后我找到了一种方法,无论是在Windows还是在Linux中,无论项目是否打成了Jar包,都能正确地获取根目录的绝对路径:

//获取项目根路径
File projectFile = new File(System.getProperty("user.dir"));

关于获取的绝对路径,如果是在Windows操作系统下,那么会是类似“C:\Program Files\Java”这样的,我们将它转换为我们想要的网络URL路径,还需要进一步处理:

	/**
     * 将文件磁盘路径转换为URL路径,去除盘符和更换反斜杠,末尾加/
     * @param originCode 文件磁盘路径
     * @return
     */
    public static String fixPath(String originCode){
        //将反斜杠用斜杠替换
        String newPath = originCode.replaceAll("\\\\", "/");
        //去掉盘符
        newPath = newPath.replaceAll("^[A-Z]:","");
        newPath = newPath+'/';
        return newPath;
    }

解决了图片的存储,剩下问题就是图片的访问。

我们的项目将图片存储在了项目的外面,也就是说图片静态资源不在Springboot内嵌的Tomcat服务器中了,也就不能通过同一个端口访问了。这时自然就会想到为图片专门再开一个服务器,但是我觉得那样太麻烦了,并且会占用服务器的性能。我采用的方式是配置Springboot的静态资源映射路径:

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        /**
         * 配置静态资源映射
         * 后面的 “/”一定要带上
         * 服务器的话一定要加file:
         */
        File file = base64toImage.getImgDirFile("");
        String filePath = file.getAbsolutePath();
        //System.out.println("图片存储路径为==========="+filePath);
        registry.addResourceHandler("/img/**")
                .addResourceLocations("file:"+ MyTools.fixPath(filePath));

    }

这样只要访问的资源路径是以“/images”开头的,就会被映射到服务器中存放文件的文件夹中,这样就解决了图片访问的问题。

解决了上述解码和路径问题,图片的存储和访问就没有什么障碍了。

2.一对一聊天(3月25日-31日)

我分配的任务中包含聊天,也就是实现一个一对一的及时通讯。然而,一个看似简单得不能再简单的功能却着实难倒了我。

微信小程序是一个类BS架构,也就是说一般情况下,服务器是不能主动向客户端发送消息的。但聊天是一个及时的功能,如果是传统的BS架构,前端为了保证及时性,就只能通过Ajax进行轮询或者使用Long poll技术,但是这两者都会占用较多的服务器资源,十分不合理。

问了度娘后,我才知道原来还有一种叫WebSocket的新型协议,它可以实现服务端对客户端的主动消息推送,这正是我所需要的!

Websocket是一种支持全双工通讯的协议,就和TCP和Socket一样,它可以在客户端和服务端之间建立一条平等的信道,服务器和客户端都可以主动向对方传递消息。本质上Websocket和Http是两种完全不同协议,它们之间并没有包含和被包含的关系,唯一能扯上联系的,就是WebSocket协议中客户端和服务器第一次握手时需要借助Http协议来发送连接请求。

Websocket协议只要求客户端借助Http协议向服务器发送一次连接请求,便可以在二者之间建立起一条全双工信道,既不需要Ajax轮询那样服务器被动地向客户端传递数据,也不需要像Long poll那样需要频繁地重新请求,这无疑极大地节省了服务器的资源。

具体实现如下:

在springboot配置类中开启Websocket

package pers.may.assist.config;

import org.springframework.boot.web.servlet.ServletContextInitializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
import org.springframework.web.util.WebAppRootListener;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;

@Configuration
public class WebsocketConfiguration implements ServletContextInitializer {

    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }

    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
        servletContext.addListener(WebAppRootListener.class);
        servletContext.setInitParameter("org.apache.tomcat.websocket.textBufferSize","52428800");
        servletContext.setInitParameter("org.apache.tomcat.websocket.binaryBufferSize","52428800");

    }
}

后端服务器代码:

/**
 * websocket连接接口
 * @description websocket连接接口
 * @author May
 */
@Controller
@ServerEndpoint(value = "/websocket/{phoneNum}") //phoneNum 从URL中获取
public class ChatSocketController {

    //连接用户数
    private static int onlineCount = 0;
    //concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。
    private static ConcurrentHashMap<String, ChatSocketController> webSocketMap = new ConcurrentHashMap<>();
    //与某个客户端的连接会话,需要通过它来给客户端发送数据
    private Session session;
    //连接用户的手机号
    private String phoneNum="";


    /**
     *
     * 建立连接时回调的方法
     *
     */
    @OnOpen
    public void onOpen(Session session, @PathParam("phoneNum") String phoneNum) {
        this.session = session;
        this.phoneNum=phoneNum;
   
        //将连接对象加入set中
        webSocketMap.put(phoneNum,this);
        //在线数加1
        addOnlineCount();
        
        System.out.println("用户连接:"+phoneNum+",当前在线人数为:" + getOnlineCount());

        try {
        	//向前端发送“连接成功的提示”
            sendMessage("连接成功");
        } catch (IOException e) {
            System.out.println("用户:"+phoneNum+",网络异常!!!!!!");
        }
    }

    /**
     * 连接关闭回调的方法
     */
    @OnClose
    public void onClose() {
        if(webSocketMap.containsKey(phoneNum)){
            webSocketMap.remove(phoneNum);
            //将连接对象从set中删除
            subOnlineCount();
        }
        System.out.println("用户退出:"+phoneNum+",当前在线人数为:" + getOnlineCount());
    }

    /**
     * 收到客户端消息后回调的方法
     *
     * @param message 客户端发送过来的消息,格式是JSON字符串
     * */
    @OnMessage
    public void onMessage(String message, Session session) {
        System.out.println("用户消息:"+phoneNum+",报文:"+message);

        if(StringUtils.isNotBlank(message)){
            try {
                //处理收到的消息,如存数据库
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }

    /**
     *
     * 出错回调方法
     *
     */
    @OnError
    public void onError(Session session, Throwable error) {
        System.out.println("用户错误:"+this.phoneNum+",原因:"+error.getMessage());
        error.printStackTrace();
    }
    /**
     * 实现服务器主动推送
     */
    public void sendMessage(String message) throws IOException {
        this.session.getBasicRemote().sendText(message);
    }


    public static synchronized int getOnlineCount() {
        return onlineCount;
    }

    public static synchronized void addOnlineCount() {
        ChatSocketController.onlineCount++;
    }

    public static synchronized void subOnlineCount() {
        ChatSocketController.onlineCount--;
    }
}

前端客户端代码(jq):

    function openSocket() {
        console.log("hello");
        if(typeof(WebSocket) == "undefined") {
            console.log("您的浏览器不支持WebSocket");
        }else{
            console.log("您的浏览器支持WebSocket");
            //实现化WebSocket对象,指定要连接的服务器地址与端口  建立连接
            var socketUrl="https://......"+$("#myPhoneNum").val();
            //将http协议替换成ws(websocket)协议
            socketUrl=socketUrl.replace("https","ws").replace("https","ws");
            console.log(socketUrl);
            if(socket!=null){
                socket.close();
                socket=null;
            }
            socket = new WebSocket(socketUrl);
            //监听连接事件
            socket.onopen = function() {
                console.log("websocket已打开");
                //socket.send("这是来自客户端的消息" + location.href + new Date());
            };
            //监听获得消息事件
            socket.onmessage = function(msg) {
                console.log(msg.data);
            };
            //监听连接关闭事件
            socket.onclose = function() {
                console.log("websocket已关闭");
            };
            //监听连接错误事件
            socket.onerror = function() {
                console.log("websocket发生了错误");
            }
        }
    }
    function sendMessage() {
        if(typeof(WebSocket) == "undefined") {
            console.log("您的浏览器不支持WebSocket");
        }else {
            console.log("您的浏览器支持WebSocket");
            let data={
            }

            console.log('{"toUserPhoneNum":"'+$("#toPhoneNum").val()+'","chatContent":"'+$("#content").val()+'"}');
            //将对象转成JSON字符串,发送给服务器
            socket.send(JSON.stringify(data));
        }
    }

通过上述前后端两段代码可以发现,Websocket有了相关的库,并不难使用。

在实际项目中还遇到了几个问题:

微信小程序要求服务器提供HTTPS加密的安全请求,而Websocket是通过HTTP协议请求进行第一次握手的,显然这里就行不通了。通过在网上查找后发现,Websocket提供了匹配HTTPS的协议wss,只需把ws协议换成wss协议即可。

在Springboot引入Websocket相关库后,在启动服务器或者打包的时候都会出现Test错误。网上一大堆博客我抄你,你抄我的,都说直接skip掉测试模块,纯属FP。经过实测后,在测试类上加上如下注释即可:

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)

如果要在Websocket类中调用服务层的提供的方法,像平常一样直接注入服务对象是行不通的,需要编写静态方法注入到静态对象中:

    //静态注入ChatService
    @Autowired
    public void setChatService(ChatService chatService){
        ChatSocketController.chatService = chatService;
    }

参考文章:
https://blog.csdn.net/moshowgame/article/details/80275084.

https://blog.csdn.net/qq_35623773/article/details/87868682

3.聊天模块的实现(4月1日-5日)

有了前面Websocket的基础,聊天模块编写起来也就变得比较轻松。

聊天信息分为文字和图片两种,都存在数据库的同一张表中。如果是文字,聊天内容字段直接存储文字;如果是图片,聊天内容字段存储图片URL。设计一个type字段区分图片和文字。

对于聊天中出现的图片,用聊天双方的手机号加时间戳命名,用前面提到的方式存储。而图片的访问方式则是直接通过网络地址进行访问。

3.登录注册和个人信息模块的实现(4月10日-13日)

登录注册和个人信息模块实现比较简单,没有遇到什么障碍。

因为项目是微信小程序项目,所以登录注册时是直接通过微信获取用户的信息,如果该用户在表中则为登录;如果该用户不在表中则为注册。

因为在各个模块中都会频繁遇到获取用户头像和姓名的情况,所以专门单独写了一个接口,通过用户手机号获取头像或姓名。

还有就是信誉分的问题,在扣除用户信誉分的同时还需要在信誉记录表中添加记录。

三、中期答辩(4月22日)

中期答辩很宽松,老师主要检查项目的进展,顺利通过。

四、项目收官(5月10日)

1.新增消息提示功能(5月10日-15日)

在项目将要结束时,发现小程序拥有订阅消息提示功能。使用的是weixin小程序的订阅消息模板,并且必须是后端服务器进行申请发送。

2.更改登录方式(5月15日-20日)

为了保证用户安全,我们将登录方式改成了使用微信用户登录时自动生成的code,服务器动态去申请用户的openid,来进行用户信息的核实检验。

五、小程序提交审核(5月25日)

微信小程序从开发版到正式版需要一个审核的过程。由于我们对微信小程序的规则不太了解,导致在项目最后一环出现了比较大的问题。

我们的小程序涉及到消息的发布,所以要求是属于社交类别。但是社交需要我们是企业账号。

在弄到企业账号后,我们发现事情还没有这么简单。还要求服务器主体和账号主体一致。这就比较难实现了,我们服务器的主题是我个人 ,但账号主题是一家公司。而重新申请服务器和域名肯定是来不及了。正常审核发布这条路已经被堵死。

于是我们打算骗过审核。通过后端给予的一个bool的变量,前端根据变量控制要展示的页面是真实的还是虚假的。换句话说,我们在审核的过程中,后端设置传false,这时候审核人员看到的是“假的”页面;当项目过审发布后,我们再讲后端返回值改为“true”,这样用户看到的就是真实的页面了。

使用上述方法,小程序成功发布。

  • 5
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值