聊聊毕业设计系列 --- 系统实现

效果展示

管理系统

WebApp

github

moment-server github地址

moment github地址

moment-manage github地址

articles

聊聊毕业设计系列 --- 项目介绍

聊聊毕业设计系列 --- 系统实现

前言

在上一篇文章中,主要是对项目做了介绍,并且对系统分析和系统设计做了大概的介绍。那么接下来这篇文章会对系统的实现做介绍,主要是选择一些比较主要的模块或者说可拿出来与大家分享的模块。好了,接入正题吧~~

MongoDB

服务端这边使用的是Express框架,数据库使用的是MongoDB,通过Mongoose模块来操作数据库。这边主要是想下对MongoDB做个介绍,当然看官了解的话直接往下划~~

在项目开始前要确保电脑是否安装mongoDB,下载点我,图像化工具Robo 3T 点我,下载好具体怎么配置还请问度娘或Google吧,本文不做介绍了哈。注意:安装完mongoDB的时候进行项目时要把lib目录下的mongod服务器打开哈~~

MongoDB 是一个基于分布式文件存储的数据库,是一个介于关系型数据库和非关系型数据库之间的开源产品,它是功能最为丰富的非关系型数据库,也是最像关系型数据库的。但是和关系型数据库不同,MongoDB没有表和行的概念,而是一个面向集合、文档的数据库。其中的文档是一个键值对,采用BSON(Binary Serialized Document Format),BSON是一种类似于JSON的二进制形式的存储格式,并且BSON具有表示数据类型的扩展,因此支持的数据非常丰富。MongoDB有两个很重要的数据类型就是内嵌文档和数组,而且在数组内可以嵌入其他文档,这样一条记录就能表示非常复杂的关系。

Mongoose是在node.js异步环境下对MongoDB进行简便操作的对象模型工具,能从数据库提取任何信息,可以用面向对象的方法来读写数据,从而使操作MongoDB数据库非常便捷。Mongoose中有三个非常重要的概念,便是Schema(模式),Model(模型),Entity(实体)。

  1. Schema: 一种以文件形式存储的数据库模型骨架,不具备数据库的操作能力,创建它的过程如同关系型数据库建表的过程,如下:
//Schema
const mongoose = require('mongoose');
const Schema = mongoose.Schema;const UserSchema = new Schema({token: String,is_banned: {type: Boolean, default: false}, //是否禁言enable: { type: Boolean, default: true }, //用户是否有效is_actived: {type: Boolean, default: false}, //邮件激活username: String,password: String,email: String,  //email唯一性code: String,email_time: {type: Date},phone: {type: String},description: { type: String, default: "这个人很懒,什么都没有留下..." },avatar: { type: String, default: "http://p89inamdb.bkt.clouddn.com/default_avatar.png" },bg_url: { type: String, default: "http://p89inamdb.bkt.clouddn.com/FkagpurBWZjB98lDrpSrCL8zeaTU"},ip: String,ip_location: { type: Object },agent: { type: String }, // 用户ualast_login_time: { type: Date },.....
});
  1. Model: 由Schema发布生成的模型,具有抽象属性和行为的数据库操作对象
//生成一个具体User的model并导出
const User = mongoose.model("User", UserSchema);  //第一个参数是集合名,在数据库中会把Model名字字母全部变小写和在后面加复数s//执行到这个时候你的数据库中就有了 users 这个集合module.exports = User;
  1. Entity: 由Model创建的实体,他的操作也会影响数据库,但是它操作数据库的能力比Model弱
const newUser = new UserModel({          //UserModel 为导出来的 Useremail: req.body.email,code: getCode(),email_time: Date.now()}); 

Mongoose中有一个东西个人感觉非常主要,那便是populate,通过populate他可以很方便的与另一个集合建立关系。如下,user集合可以与article集合、user集合本身进行关联,根据其内嵌文档的特性,这样子他便可以内嵌子文档,子文档中有可以内嵌子文档,这样子它返回的数据就会异常的丰富。

const user = await UserModel.findOne({_id: req.query._id, is_actived: true}, {password: 0}).populate({path: 'image_article',model: 'ImageArticle',populate: {path: 'author',model: 'User'}}).populate({path: 'collection_film_article',model: 'FilmArticle',}).populate({path: 'following_user',model: 'User',}).populate({path: 'follower_user',model: 'User',}).exec();

服务端主要是操作数据库,对数据库进行增删改查(CRUD)等操作。项目中的接口,Mongoose的各种方法这边就不对其做详细介绍,大家可以查看Mongoose文档。

用户身份认证实现

介绍

本系统的用户身份认证机制采用的是JSON Web Token(JWT),它是一种轻量的认证规范,也用于接口的认证。我们知道,HTTP协议是一种无状态的协议,这便意味着每个请求都是独立的,当用户提供了用户名和密码来对我们的应用进行用户认证,那么在下一次请求的时候,用户需要再进行一次用户的认证才可以,因为根据HTTP协议,我们并不能知道是哪个用户发出的请求,本系统采用了token的鉴权机制。这个token必须要在每次请求时传递给服务端,它应该保存在请求头里,另外,服务端要支持CORS(跨来源资源共享)策略,一般我们在服务端这么做就可以了Access-Control-Allow-Origin: *。

在用户身份认证这一块有很多方法,最常见的像cookie ,session。那么他们三之间又有什么区别,这里有两篇文章介绍的挺全面。

  • 正确理解HTTP短连接中的Cookie、Session和Token
  • 小白必读:闲话HTTP短连接中的Session和Token

token 与 session的区别在于,它不同于传统的session认证机制,它不需要在服务端去保留用户的认证信息或其会话的信息。系统一旦比较大,都会采用机器集群来做负载均衡,这需要多台机器,由于session是保存在服务端,那么就要 去考虑用户到底是在哪一台服务器上进行登录的,这便是一个很大的负担。

那么就有人想问了,你这个系统这么小,为什么不使用传统的session机制呢?哈~因为之前自己的项目一般都是使用session做登录,没使用过token,想尝试尝试入入坑~~哈哈哈~

实现思路

JWT主要的实现思路如下:

  1. 在用户登录成功的时候创建token保存于数据库中,并返回给客户端。
  2. 客户端之后的每一次请求都要带上token,在请求头里加入Authorization,并加上token.
  3. 在服务端进行验证token的有效性,在有效期内返回200状态码,token过期则返回401状态码

如下图所示:

JWT请求图
<center>JWT请求图</center>

在node中主要用了jsonwebtoken这个模块来创建JWT,jsonwebtoken的使用请查看jsonwebtoken文档。项目中创建token的中间件createToken如下

/*** createToken.js*/
const jwt = require('jsonwebtoken');  // 引入jsonwebtoken模块
const secret = '我是密钥'//登录时:核对用户名和密码成功后,应用将用户的id(user_id)作为JWT Payload的一个属性
module.exports = function(user_id){const token = jwt.sign({user_id: user_id}, secret, {  //密钥expiresIn: '24h' //过期时间设置为24h。那么decode这个token的时候得到的过期时间为:创建token的时间+设置的值});return token;
};

return 出来的 token 类似eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiYWRtaW4iLCJpYXQiOjE1MzQ2ODQwNzAsImV4cCI6MTUzNDc3MDQ3MH0.Y3kaglqW9Fpe1YxF_uF7zwTV224W4W97MArU0aI0JgM。我们仔细看这字符串,分为三段,分别被 "." 隔开。现在我们分别对前两段进行base64解码如下:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9  ===> {"alg":"HS256","typ":"JWT"}  其中 alg是加密算法名字,typ是类型eyJ1c2VyX2lkIjoiYWRtaW4iLCJpYXQiOjE1MzQ2ODQwNzAsImV4cCI6MTUzNDc3MDQ3MH0  ===>  {"user_id":"admin","iat":1534684070,"exp":1534770470}  其中 name是我们储存的内容,iat创建的时间戳,exp到期时间戳。Y3kaglqW9Fpe1YxF_uF7zwTV224W4W97MArU0aI0JgM  ===> 最后一段是由前面两段字符串,HS256加密后得到。所以前面的任何一个字段修改,都会导致加密后的字符串不匹配。

当我们根据用户的id创建获取到token之后,我们需要把token返回到客户端,客户端对其在本地(localStorage)保存, 客户端之后的每一次请求都要带上token,在请求头里加入Authorization,并加上token,服务端进行验证token的有效性。那么我们如何验证token的有效性呢? 所以我们需要checkToken这个中间件来检测token的有效性。

/*** checkToken*/
const jwt = require('jsonwebtoken');
const secret = '我是密钥'module.exports = async ( req, res, next ) => {const authorization = req.get('Authorization');if (!authorization) {res.status(401).end();  //接口需要认证但是有没带上token,返回401未授权状态码return}const token = authorization.split(' ')[1];try {let tokenContent = await jwt.verify(token, secret);   //如果token过期或验证失败,将抛出错误next();     //执行下一个中间件} catch (err) {console.log(err)res.status(401).end();  //token过期或者验证失败返回401状态码}
}

那么现在咱们只要在需要用户认证的接口上,在操作数据之前,加上checkToken中间件即可,如下调用:

//更新用户信息
router.post('/updateUserInfo', checkToken, User.updateUserInfo)  //如果checkToken检测不成功,它便返回401状态码,不会对User.updateUserInfo做任何操作, 只有检测token成功,才能处理User.updateUserInfo

我们如何保证每次请求都能在请求头里加入Authorization,并加上token,这就要用到Axios的请求拦截,并且也用到了它的响应拦截,因为在服务端返回401状态码之后应要执行登出操作,清楚本地token的存储,具体代码如下:

//request拦截器
instance.interceptors.request.use(config => {//每次发送请求之前检测本地是否存有token,都要放在请求头发送给服务器if(localStorage.getItem('token')){if (config.url.indexOf('upload-z0.qiniup.com/putb64') > -1){config.headers.Authorization = config.headers['UpToken'];  //加上七牛云上传token}else {config.headers.Authorization = `token ${localStorage.getItem('token')}`.replace(/(^\")|(\"$)/g, '');  //加上系统接口token}}console.log('config',config)return config;},err => {console.log('err',err)return Promise.reject(err);}
);//response拦截器
instance.interceptors.response.use(response => {return response;},error => { //默认除了2XX之外的都是错误的,就会走这里if(error.response){switch(error.response.status){case 401:console.log(error.response)store.dispatch('ADMIN_LOGINOUT'); //可能是token过期,清除它router.replace({ //跳转到登录页面path: '/login',query: { redirect: '/dashboard' } // 将跳转的路由path作为参数,登录成功后跳转到该路由});}}return Promise.reject(error.response);}
);

其中的if else 是因为本系统的图片,音视频是放在七牛云,上传需要七牛云上传base64图片的时候token是放在请求头的,正常的图片上传不是放在请求头,所以这边对token做了区分,如何接入七牛云也会在下面模块介绍到。

七牛云接入

本系统的图片,音视频是放在七牛云,所以需要接入七牛云。七牛云分了两种情况,正常图片和音视频的上传和base64图片的上传,因为七牛云在对他们两者上传的Content-Typedomain(域)有所不同,正常图片和音视频的Content-Type是headers: {'Content-Type':'multipart/form-data'}domain是domain='https://upload-z0.qiniup.com',而base64图片的上传则是headers:{'Content-Type':'application/octet-stream'}domain是domain='https://upload-z0.qiniup.com/putb64/-1',所以他们请求的时候token放的地方不同,base64就像上面所说的放在请求头Authorization中,而正常的放在form-data中。在服务端通过接口请求来获取七牛云上传token,客户端获取到七牛云token,通过不同方案将token带上。

  1. base64的上传: headers:{'Content-Type':'application/octet-stream'}domain='https://upload-z0.qiniup.com/putb64/-1',token放在请求头Authorization中。
  2. 正常图片和音视频的上传: headers: {'Content-Type':'multipart/form-data'}domain='https://upload-z0.qiniup.com',token 放在 form-data中。

服务端通过qiniu这个模块进行创建token,服务端代码如下:

/*** 构建一个七牛云上传凭证类* @class QN*/
const qiniu = require('qiniu')  //导入qiniu模块
const config = require('../config')
class QN {/*** Creates an instance of qn.* @param {string} accessKey -七牛云AK* @param {string} secretKey -七牛云SK* @param {string} bucket -七牛云空间名称* @param {string} origin -七牛云默认外链域名,(可选参数)*/constructor (accessKey, secretKey, bucket, origin) {this.ak = accessKeythis.sk = secretKeythis.bucket = bucketthis.origin = origin}/*** 获取七牛云文件上传凭证* @param {number} time - 七牛云凭证过期时间,以秒为单位,如果为空,默认为7200,有效时间为2小时*/upToken (time) {const mac = new qiniu.auth.digest.Mac(this.ak, this.sk)const options = {scope: this.bucket,expires: time || 7200}const putPolicy = new qiniu.rs.PutPolicy(options)const uploadToken = putPolicy.uploadToken(mac)return uploadToken}
}exports.QN = QN;exports.upToken = () => {return new QN(config.qiniu.accessKey, config.qiniu.secretKey, config.qiniu.bucket, config.qiniu.origin).upToken()  //每次调用都创建一个token
}
//获取七牛云token接口
const {upToken} = require('../utils/qiniu')app.get('/api/uploadToken', (req, res, next) => {const token = upToken()res.send({status: 1,message: '上传凭证获取成功',upToken: token,})})

由于正常图片和音视频的上传和base64图片的上传,因为七牛云在对他们两者上传的Content-Typedomain(域)有所不同,所以的token请求存放的位置有所不同,因此要区分,客户端调用上传代码如下:

//根据获取到的上传凭证uploadToken上传文件到指定域//正常图片和音视频的上传uploadFile(formdata, domain='https://upload-z0.qiniup.com',config={headers:{'Content-Type':'multipart/form-data'}}){console.log(domain)console.log(formdata)return instance.post(domain, formdata, config)},//base64图片的上传//根据获取到的上传凭证uploadToken上传base64到指定域uploadBase64File(base64, token, domain = 'https://upload-z0.qiniup.com/putb64/-1', config = {headers: {'Content-Type': 'application/octet-stream',},}){const pic = base64.split(',')[1];config.headers['UpToken'] = `UpToken ${token}`return instance.post(domain, pic, config)},
function upload(Vue, data, callbackSuccess, callbackFail) {//获取上传token之后处理Vue.prototype.axios.getUploadToken().then(res => {if (typeof data === 'string'){  //如果是base64const token = res.data.upTokenVue.prototype.axios.uploadBase64File(data, token).then(res => {if (res.status === 200){callbackSuccess && callbackSuccess({data: res.data,result_url: `http://p89inamdb.bkt.clouddn.com/${res.data.key}`})}}).catch((error) => {callbackFail && callbackFail({error})})}else if (data instanceof FormData){  //如果是FormDatadata.append('token', res.data.upToken)data.append('key', `moment${Date.now()}${Math.floor(Math.random() * 100)}`)Vue.prototype.axios.uploadFile(data).then(res => {if (res.status === 200){callbackSuccess && callbackSuccess({data: res.data,result_url: `http://p89inamdb.bkt.clouddn.com/${res.data.key}`})}}).catch((error) => {callbackFail && callbackFail({error})})}else {const formdata = new FormData()  //如果不是formData 就创建formDataformdata.append('token', res.data.upToken)formdata.append('file', data.file || data)formdata.append('key', `moment${Date.now()}${Math.floor(Math.random() * 100)}.${data.file.type.split('/')[1]}`)// 获取到凭证之后再将文件上传到七牛云空间console.log('formdata',formdata)Vue.prototype.axios.uploadFile(formdata).then(res => {console.log('res',res)if (res.status === 200){callbackSuccess && callbackSuccess({data: res.data,result_url: `http://p89inamdb.bkt.clouddn.com/${res.data.key}` //返回的图片链接})}}).catch((error) => {console.log(error)callbackFail && callbackFail({error})})}})
}export default upload

路由权限模块

系统的后台管理面向的是合作作者和管理员,涉及到两种角色,故此要做权限管理。不同的权限对应着不同的路由,同时侧边栏的菜单也需根据不同的权限,异步生成,不同于以往的服务端直接返回路由表,由前端动态生成,接下来介绍下登录和权限验证的思路:

  1. 登录:当用户填写完账号和密码后向服务端验证是否正确,验证通过之后,服务端会返回一个token,拿到token之后前端会根据token再去拉取一个getAdminInfo的接口来获取用户的详细信息(如用户权限,用户名等等信息)。
  2. 权限验证:通过token获取用户对应的role,动态根据用户的role算出其对应有权限的路由,通过vue-router的beforeEach进行全局前置守卫再通过router.addRoutes动态挂载这些路由。

代码有点多,这边就直接放流程图哈~~

权限路由流程图
<center>权限路由流程图</center>

最近正好也在公司做中后台项目,公司的中后台项目的这边是由服务端生成路由表,前端进行直接渲染,毕竟公司的一整套业务比较成熟。但是我们会在想能不能由前端维护路由表,这样不用到时候项目迭代,前端每增加页面都要让服务端兄弟配一下路由和权限,当然前提可能是项目比较小的时候。

账号模块

账号模块是业务中最为基础的模块,承担着整个系统所有的账号相关的功能。系统实现了用户注册、用户登录、密码修改、找回密码功能。

系统的账号模块使用了邮件服务,针对普通用户的注册采用了邮件服务来发送验证码,以及密码的修改等操作都采用了邮件服务。在node.js中主要采用了Nodemailer,Nodemailer是一个简单易用的Node.js邮件发送组件,它的使用可以摸我摸我摸我,通过此模块进行邮件的发送。你们可能会问,为什么不用短信服务呢?哈~因为短信服务要钱,哈哈哈

/*
* email 邮件模块
*/const nodemailer = require('nodemailer');
const smtpTransport = require('nodemailer-smtp-transport');
const config = require('../config')const transporter = nodemailer.createTransport(smtpTransport({host: 'smtp.qq.com',secure: true,port: 465,  // SMTP 端口auth: {user: config.email.account,pass: config.email.password  //这里密码不是qq密码,是你设置的smtp授权码}
}));let clientIsValid = false;
const verifyClient = () => {transporter.verify((error, success) => {if (error) {clientIsValid = false;console.warn('邮件客户端初始化连接失败,将在一小时后重试');setTimeout(verifyClient, 1000 * 60 * 60);} else {clientIsValid = true;console.log('邮件客户端初始化连接成功,随时可发送邮件');}});
};
verifyClient();const sendMail = mailOptions => {if (!clientIsValid) {console.warn('由于未初始化成功,邮件客户端发送被拒绝');return false;}mailOptions.from = '"ShineTomorrow" <admin@momentin.cn>'transporter.sendMail(mailOptions, (error, info) => {if (error) return console.warn('邮件发送失败', error);console.log('邮件发送成功', info.messageId, info.response);});
};exports.sendMail = sendMail;

账号的注册先是填写email,填写好邮箱之后会通过Nodemailer发送一封含有有效期的验证码邮件,之后填写验证码、昵称和密码即可完成注册,并且为了安全考虑,对密码采用了安全哈希算法(Secure Hash Algorithm)进行加密。账号的登录以账号或者邮箱号加上密码进行登录,并且采用上文所说的JSON Web Token(JWT)身份认证机制,从而实现用户和用户登录状态数据的对应。

邮件长这样
<center>我的邮件长这样?(可自己写邮件模板)</center>

实时消息推送

当用户被人关注、评论被他人回复和点赞等一些社交性的操作的时候,在数据存储完成后,服务端应需要及时向用户推送消息来提醒用户。消息推送模块采用了Socket.io来实现,socket.io封装了websocket,不支持websocket的情况还提供了降级AJAX轮询,功能完备,设计优雅,是开发实时双向通讯的不二手段。

通过 socket.io,用户每打开一个页面,这个页面都会和服务端建立一个连接。在服务端可以通过连接的socket的id属性来匹配到一个建立连接的页面。所以用户的ID和socket的id,是一对多的关系,即一个用户可能在登录后打开多个页面。而socket.io没有提供从服务端向某个用户单独发送消息的功能,更没有提供向某个用户打开的所有页面推送消息的功能。但是socket.io提供了room的概念,即群组。在建立websocket时,客户端可以选择加入某个room,如果这个room没有存在则自动新建一个,否则直接加入,服务端可以向某个room中的所有客户端推送消息。

根据这个特性,设计将用户的ID作为room的名字,当某个用户打开页面建立连接时,会选择加入以自己用户ID为名字的room。这样,在用户ID为名字的 room中,加入的都是用户自己打开的页面建立的连接。从而向某个用户推送消息,可以直接通过向以此用户的ID为名字的room发送消息,这样就会推送到用户打开的所有页面。

有了想法后我们就开始鲁吧~,在服务端中socket.io在客户端中使用vue-socket.io, 服务端代码如下:

/*
* app.js中
*/
const server = require('http').createServer(app);
const io = require('socket.io')(server);
global.io = io;  //全局设上io值, 因为在其他模块要用到
io.on('connection', function (socket) {// setTimeout(()=>{//     socket.emit('nodeEvent', { hello: 'world' });// }, 5000)socket.on('login_success', (data) => {  //接受客户端触发的login_success事件//使用user_id作为房间号socket.join(data.user_id);console.log('login_success',data);});
});
io.on('disconnect', function (socket) {socket.emit('user disconnected');
});server.listen(config.port, () => {console.log(`The server is running at http://localhost:${config.port}`);
});
/*
* 某业务模块
*/
//例如某文章增加评论
io.in(newMusicArticle.author.user_id._id).emit('receive_message', newMessage); //实时通知客户端receive_message事件
sendMail({       //发送邮件to: newMusicArticle.author.user_id.email,subject: `Moment | 你有未读消息哦~`,text: `啦啦啦,我是卖报的小行家~~ ?`,html: emailTemplate.comment(sender, newMusicArticle, content, !!req.body.reply_to_id)
})

客服端代码:

<script>export default {name: 'App',data () {return {}},sockets:{connect(){},receive_message(val){  //接受服务端触发的事件,进行客户端实时更新数据if (val){console.log('服务端实时通信', val)this.$notify(val.content)console.log('this method was fired by the socket server. eg: io.emit("customEmit", data)')}}},mixins: [mixin],mounted(){if (!!JSON.parse(window.localStorage.getItem('user_info'))){this.$socket.emit('login_success', {  //通知服务端login_success 事件, 传入iduser_id: JSON.parse(window.localStorage.getItem('user_info'))._id})}},}
</script>

评论模块

评论模块是为了移动端WebApp下的文章下为用户提供关于评论的一些操作。系统实现了对文章的评论,评论的点赞功能,热门评论置顶以及评论的回复功能。在评论方面存在着各种各样的安全性问题,比如XSS攻击(Cross Site Scripting,跨站脚本攻击)以及敏感词等问题。预防XSS攻击使用了xss模块, 敏感词过滤使用text-censor模块。

一些思考

  1. 接口数据问题

在开发的时候经常会遇到这个问题,接口数据问题。有时候服务端返回的数据并不是我们想要的数据,前端要对数据进行再一步的处理。

例如服务端返回的某个字段为null或者服务端返回的数据结构太深,前端需要不断去判断数据结构是否真的返回了正确的东西,而不是个null 或者undefined~

我们前端都要这么去处理过滤:

<div class="author">文 / {{(musicArticleInfo.author && musicArticleInfo.author.user_id) ? musicArticleInfo.author.user_id.username : '我叫这个名字'}}
</div>

这就引出了一个思考:

对数据的进一步封装处理,必然渲染性能方面会存在问题,而且我们要时刻担心数据返回的问题。如果应用到公司的业务,我们应该如何处理呢 ?

  1. 页面性能优化和SEO问题

首屏渲染问题一直是单页应用的痛点,那么除了常用的性能优化,我们还有什么方法优化的吗 ? 这个项目虽然面向的是移动端用户,可能不存在SEO问题,如果做成pc端的话,像文章这类的应用,SEO都是必须品。



对于上面提出的问题,node的出现让我们看到了解决方案,那就常说的Node中间层,当然本项目中是不存在Node中间层,而是直接作为后端语言处理数据库。

由于大部分的公司后端要么是php要么是java,一般不把node直接作为后端语言,如果有使用到node,一般是作为一个中间层的形式存在。

对于第一个问题的解决:我们可以在中间层做接口转发,在转发的过程中做数据处理。而不用担心数据返回的问题。

对于第二个问题的解决:有了Node中间层的话,那么我们可以把首屏渲染的任务交给nodejs去做,次屏的渲染依然走之前的浏览器渲染。



有Node中间层的话,新的架构如下:

新架构

前后端的职能:

总结

已经毕业一段时间了,写文章是为了回顾。本人水平一般,见谅见谅。这个产品的实现,一个人扛,在其中充当了各种角色,要有一点点产品思维,要有一点点设计的想法,要会数据库设计,要会后端开发,挺繁琐的。最难的点个人感觉还是数据库设计,数据库要一开始就要设计的很完整,不然到后面的添添补补,就会很乱很乱,当然这个基础是产品要非常清晰,刚开始自己心中对产品可能是个模糊的定义,想想差不多是那样,于是乎就开始搞~~导致于后面数据库设计的不是很满意。由于时间关系,现在的产品中有些小模块还没完成,但是大部分的功能结构已经完成,算是个成型的产品,当然是一个没有经过测试的产品哈哈哈哈,要是有测试的话,那就哈哈哈哈你懂得。

前路漫漫,吾将上下而求索~

谢谢~~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/450830.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

js 实现用window.print()打印页面中的部分内容,局部打印

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到教程。 如下方法&#xff1a; function printProof(){var printData document.getElementById("forPrint").innerHTML; // 只打印 f…

Spring 之注解事务 @Transactional

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到教程。 先让我们看代码吧&#xff01; 以下代码为在“Spring3事务管理——基于tx/aop命名空间的配置”基础上修改。首先修改applicationContext…

超级程序员神话

摘要&#xff1a;大部分的程序员在思想里都会某种程度的承认&#xff0c;承认自己只是一个普通的程序员&#xff0c;但这世界上确实有一些超级程序员&#xff0c;在一个为企业开发应用的程序员和一个为谷歌写搜索算法的程序员之间&#xff0c;或和一个开发用来控制读写头从磁盘…

HashMap30连问,彻底搞懂HashMap

文章目录一、背景知识1、什么是Map&#xff1f;2、什么是Hash&#xff1f;3、什么是哈希表&#xff1f;4、什么是HashMap?5、如何使用HashMap&#xff1f;6、HashMap有哪些核心参数&#xff1f;7、HashMap与HashTable的对比&#xff1f;8、HashMap和HashSet的区别&#xff1f;…

如何使用postman做接口测试

1、get请求传参 只要是get请求都可以在浏览器中直接发&#xff1a; 在访问地址后面拼 ?keyvalue&keyvalue 例如&#xff1a;在浏览器中直接输入访问地址&#xff0c;后面直接拼需要传给服务器的参数http://api.nnzhp.cn/api/user/stu_info?stu_name小黑2、post请求&…

【狂神说】分析前后端分离开源项目?

文章目录1.如何分析开源项目项目简介项目源码2.观察开源项目3.开源项目下载4.跑起来是第一步5.前后端分离项目固定套路6.如何找到一个开源项目1.如何分析开源项目 学习的方式&#xff1a; 不知道这个代码怎么来的这个代码跑不起来这个项目对我们有什么帮助&#xff0c;不会模…

设计公共API的六个注意事项

摘要&#xff1a;俗话说&#xff1a;“好东西就要贡献出来和大家一起分享”&#xff0c;尤其是在互联网业务高度发达的今天&#xff0c;如果你的创业公司提供了一项很酷的技术或者服务&#xff0c;并且其他用户也非常喜欢该产品&#xff0c;在这种情况下&#xff0c;最好的解决…

go 交叉编译

golang中windows交叉编译 env GOOSlinux GOARCHamd64 go build .打包镜像 FROM alpineMAINTAINER "congge"ADD ./casino_niuniu /usr/local/casino_niuniu/bin/casino_niuniu ADD ./templates /usr/loca/lcasino_niuniu/bin/templates ADD ./public /usr/local/casin…

P3193 [HNOI2008]GT考试

传送门 容易看出是道DP 考虑一位一位填数字 设 f [ i ] [ j ] 表示填到第 i 位&#xff0c;在不吉利串上匹配到第 j 位时不出现不吉利数字的方案数 设 g [ i ] [ j ] 表示不吉利串匹配到第 i 位&#xff0c;再添加一个数字&#xff0c;使串匹配到第 j 位的方案数 那么方程显然为…

SQLserver数据库反编译生成Hibernate实体类和映射文件

一、建立项目和sqlserver数据库 eclipse&#xff0c;我使用的版本是neon3 二、Data Source Explorer 选择OK 在data source Explorer的Database Connections 选择New 填写好General的连接信息 新建New Driver Definition 填写完选择OK 选择刚才的Drivers Test Connetion测试 N…

最受欢迎的5大Linux发行版

摘要&#xff1a;要统计有多少人在使用那款Linux发行版几乎是不可能的事情&#xff0c;但我们可以使用一些在线分析工具来大概地看看哪些Linux发行版更受欢迎。 Google Trends的数据显示&#xff0c;Ubuntu用户正在流向Mint&#xff0c;但依然在各方面都比其它Linux发行版更有优…

使用IntelliJ IDEA 配置Maven(入门)

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到教程。 1. 下载Maven 官方地址&#xff1a;http://maven.apache.org/download.cgi 解压并新建一个本地仓库文件夹 2.配置本地仓库路径 3.配…

为什么程序员不擅长估算时间?

摘要&#xff1a;时间估算是困难的&#xff0c;每一个程序员都有一个现实的估计区间&#xff0c;低于这个区间的估计意味着&#xff08;构件&#xff0c;测试&#xff0c;检查代码的&#xff09;时间开销被低估了&#xff0c;超过这个区间的估计意味着这个任务太大而很难预估。…

red hat enterprise linux 7关闭防火墙的方法

2019独角兽企业重金招聘Python工程师标准>>> red hat enterprise linux 7发布后&#xff0c;发现防火墙也变了&#xff0c;如何关闭防火墙呢&#xff0c;下面是方法 1.查看firewall的状态 [rootsztech7 ~]# systemctl status firewalld firewalld.service - firewal…

IOS —— 网络那些事(上) - http协议

作为一名并不太合格的程序员&#xff0c;今天要分享学习的成果&#xff0c;竟然讲的是网络相关HTTP协议的事情。&#xff08;也算是复习了&#xff09; 乍看HTTP协议的内容着实是十分复杂的&#xff0c;涉及到十分多互联网"底层"框架的东西。今天就先撇开这部分详细内…

老派程序员——徒手实现伟大成就

摘要&#xff1a;本文介绍了三位非常著名的程序员&#xff1a;Ken Thompson,Joe Armstrong 和 Jamie Zawinski&#xff0c;他们是如何发明一门新语言&#xff0c;他们开发软件时会像我们一样使用当今流行的开发工具吗&#xff1f;当读Peter Seibel的精彩著作《编程人生:15位软件…

互联网大厂项目研发流程

文章目录阶段一&#xff1a;阶段二&#xff1a;阶段三&#xff1a;阶段四&#xff1a;阶段五&#xff1a;开发人员&#xff1a;测试人员&#xff1a;设计师&#xff1a;阶段六&#xff1a;阶段七&#xff1a;总结&#xff1a;本文章学习自&#xff1a;https://www.bilibili.com…

解决 java.lang.IllegalArgumentException: Repository interface must not be null on initialization!

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到教程。 报错&#xff1a;Caused by: java.lang.IllegalArgumentException: Repository interface must not be null on initialization! Cause…

【狂神说】JVM

文章目录1.JVM的位置2.JVM的体系结构3.类加载器4.双亲委派机制&#xff08;重要&#xff09;5.沙箱安全机制(了解)6.native&#xff08;核心&#xff09;7.PC寄存器&#xff08;了解&#xff09;8.方法区9.栈10.三种JVM11.堆&#xff08;Heap&#xff09;12.新生区、老年区13.永…

我们真的需要统一的编程规范?

摘要&#xff1a;仁者见仁智者见智&#xff0c;编码风格的不同&#xff0c;对项目也会有不同的影响&#xff0c;统一的编码规范有益于项目的维护。俗话说&#xff0c;没有规矩不成方圆&#xff0c;在2004年&#xff0c;UNIX创始人之一的Ken Arnold就发表了一篇很幽默文章&#…