作者:愚坤,掘金优秀作者,一名没上高中的前端工程师,目前就职水滴筹。

https://juejin.cn/post/6898612811891474440

什么是 MVP,来自伟大的百科:

Minimum Viable Product –最简化可实行产品

是指以最低成本尽可能展现核心概念的产品策略,即是指用最快、最简明的方式建立一个可用的产品原型,这个原型要表达出你产品最终想要的效果,然后通过迭代来完善细节。该术语由弗兰克·罗宾逊和埃里克·里斯推广于Web应用程序 ,它也可能涉及到进行市场前手的分析。

前言:

Node是前端工程师的贵人,拓宽了前端工程师的能力边界,对比前几年用Dreamweaver写table页面的我来说,感受到的变化是日新月异;前端搞搞工程化和框架什么的也就算了,竟然连编辑器都自己搞????,js你说你是不是有点过分了?

当然,这个过分的js帮助了我很多,从被后端大佬揪着耳朵按到工位上温声细语的说:“我套完页面样式乱了,帮我调下样式”,演变成大佬气冲冲的跑到我工位慈眉善目的拍着桌子说:“TM接口参数传错了”。

感谢Node吧,至少我可以在自己的工位上改自己写的Bug了????。

言归正传,再这么贫真就写不下去代码了,随着Node能力的发展,我自己感觉出来自己有点飘了,因为有用Kindle看书的陋习,一直觉的市面上所有的kindle笔记软件都是垃圾????,于是自己写了一个满意的垃圾;这都不算啥,我居然因为要减肥,就写了个体重记录小程序,上线以后我冲着镜子里浑身赘肉的自己喊:“以为自己就是Node吗?过分”????。

体重记录小程序的故事并没有突兀的结束,有些用户反馈有bug,我借口taro更新太快项目跑不起来了,而且腾讯云函数我用的很不方便,于是很不负责的停更了;在年后疫情期间,因为实在太闲就打开了后台留言,看到有一个莫名其妙的留言说寻求合作????‍♂️。

我忐忑的拨通了电话,在说明了我是小程序的开发者以后,这个人上来就开始说瞎话:“你这个小程序太好了”????‍♂️,他阐述了一下自己的经历,是一位开了8年健身房的教练,后来混不下去把健身房关了,做在线减脂指导,竟然收入还不错,真是造化弄人????,他咨询我可以一起做一个减脂管理系统吗?不要钱那种,我恬不知耻的说:“好呀”

不久我们见面了,约在北京东五环外的常营龙湖·长楹天街,他问我可以吃川菜吗?我说可以,于是我们找了一家老屋川菜馆,坐在我面前的这个人因为吃了一口自己点的辣子鸡,然后拿着纸巾一把鼻涕一把泪的忏悔自己是个假重庆人????‍♂️,我羞涩的吐掉了口中的辣椒,一起构想了我们小程序的未来,现在回想起自己的高谈阔论,都有些不好意思????。

正文

上边的段子根据个人情况改编,纯属娱乐,如果没有Node的开发能力,别人可能会安慰我:“你还是去写个页面把”,???? 苦涩的泪水从眼眶涌出。

简单介绍了下最近折腾的3个项目的由来,从第一个体重记录小程序,到Kindle笔记工具,再到现在的一套小程序 + 后台,作为一个前端程序员独立作出一套可以跑起来的小系统还是比较有成就感的,虽然可能会被吐槽:不就是增删改查吗? 但是不用担心被吐槽:又没写过增删改查懂个屁?????

下边内容介绍了3个项目的积累,重点贴一下第三个项目Node用到的代码。共同交流,恳请斧正

21天体重记录小程序

累计7千用户和每天不超过20个活跃用户的数据,还有3篇实践笔记。小程序提供的Node云函数 + 数据库,可以不花一毛钱就能跑起来自己的小程序,最早是原生写法,后来切换到Taro React语法,效率提高很多,对小程序登录流程、云开发有了一些经验积累,也意识到自己对表结构设计的欠缺。

奇怪的是竟然累计了7千用户,用户从哪来的呢,难道是因为名字起的好吗?????后续准备再更新探索下。

  • 【小程序 + 云开发】体重记录小程序 上手笔记

  • 【小程序 + 云开发 】 随机读取数据并生成分享图片 上手笔记

  • 【小程序 + 云开发】体重排行榜 上手笔记

kindle 笔记整理工具

最早是在本地开发,开发了用户注册、密码找回、书籍管理、笔记管理的功能,然后买服务器部署到线上。

前端使用的 Ant Pro,因为引入了Echarts,没有做按需引入,所以第一次进入比较慢,目前只有自己用,就没优化。

整套流程跑起来以后,用着自己做的小工具觉得还挺香,也有信心做一些更大的挑战了。

地址:http://nihaojob.com/

减脂管理系统开发

终于到今天的主题了,先说下应用场景,学员在报名减脂教练的课程后,教练需要先了解学员日常饮食、睡眠、运动等生活习惯,然后根据学员状况定制运动计划和饮食方案,以及日常的运营如对学员的饮食和运动的打卡审核、积分减重排行榜、知识库等。

主要的6个功能:

  1. 教练账号管理

  2. 问卷收集

  3. 方案下发

  4. 打卡审核

  5. 知识管理

  6. 积分、减重排行榜

后台预览:

小程序预览:

知识点

服务器 域名 备案

我是从滴滴云上卖的服务器,一年才几百块,域名是之前在腾讯买的nihaojob.com, 备案过程中滴滴说有政策调整,花了20多天的时间,备案建议提前做准备,备案期间可以把Nginx + Node + Mongodb环境搭建起来。

HTTPS证书申请与Nginx配置

微信小程序的开发域名必须是HTTPS,滴滴云有免费的证书。证书申请后需要在域名解析汇总增加TXT记录,不懂就问滴滴云客服,服务很nice。证书申请成功后,把证书上传到服务器,在Nginx的/etc/nginx/conf.d目录下,https.conf文件中ssl_certificatessl_certificate_key配置证书路径。

server {listen    443 ssl;server_name  api.nihaojob.com;ssl_session_timeout 5m;ssl_protocols TLSv1 TLSv1.1 TLSv1.2; #按照这个协议配置ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE;#按照这个套件配置ssl_prefer_server_ciphers on;ssl_certificate /root/sslcert/cert.crt;ssl_certificate_key /root/sslcert/private.key;location / {alias /root/coach/coach-fe/dist/;}location /prod-api/ {proxy_pass http://127.0.0.1:3000/;}}

跨域设置

这里设置了跨域请求头,因为Origin是根据入参来的,很容易造成CROS攻击,对安全系数有要求的系统还是用别的方案吧,也可以使用express推荐的cors中间件。

app.all('*', function (req, res, next) {res.header("Access-Control-Allow-Origin", req.headers.origin);res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, token');res.header("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS");res.header("Access-Control-Allow-Credentials", "true");res.header("Content-Type", "application/json;charset=utf-8");if (req.method == 'OPTIONS') {res.send(200); /*让options请求快速返回*/}else {next();}
});

express jwt使用

前端在登录时根据用户id生成一个Token发给前端,前端之后的所有请求都携带这个Token,后端根据Token解开后的用户id来进行数据操作。

利用jsonwebtoken生成Tokenexpress-jwt进行校验和非必需登录接口检查。

个人认为开发同学都应该深挖一下无状态Token机制与有状态session机制的知识点。

//引入
const jwt = require('jsonwebtoken');
const expressJwt = require('express-jwt');//定义签名字符串
const secret = 'anyThingString';//使用中间件验证token合法性
app.use(expressJwt({secret: secret,getToken: function fromHeaderOrQuerystring(req) {if (req.cookies && req.cookies.token || req.headers.token) { // 使用query.tokenreturn req.headers.token;}return null;}
}).unless({path: ['/login','/users/register', '/students/login', ]  //除了这些地址,其他的URL都需要验证
}));//拦截器
app.use(function (err, req, res, next) {//当token验证失败时会抛出如下错误if (err.name === 'UnauthorizedError') {//这个需要根据自己的业务逻辑来处理( 具体的err值 请看下面)res.status(401).send({ code: -1, msg: '未登录', status: 41002 });}
});// 解析token中间件 后续所有接口可直接使用req.tokenDecode获取参数
app.use((req, res, next) => {// 获取tokenlet token = req.cookies.token || req.headers.token;if (token) {let decoded = jwt.decode(token, secret);req.tokenDecode = decoded}next()
});//返回token给客户端.
app.use('/login', async function (req, res) {const { mail, password }= req.body;// 格式校验if( mail === '' || password === '' ){return res.send({ code: -1, msg: '非法参数' });}const userInfo  = await userModel.findOne({ mail: mail, password: password  })if (!userInfo) {res.send({ code: -1, msg: '信息错误' });} else {const { _id, mail, mobile, type } = userInfo;//生成tokenconst token = jwt.sign({ _id, mail, mobile, type, }, secret, {expiresIn: 3600 * 2 //秒到期时间});res.cookie('token', token, {maxAge: 1000 * 60 * 60 * 24 * 30,httpOnly: true});res.send({ code: 1, msg: '登录成功', status: 'ok', data:{ userInfo, token } });}
});

环境变量

环境变量在npm script中设置,本地开发时用nodemon yarn start,部署线上环境时使用pm2 start --name coEnd npm -- run startPro

需要根据环境变量走不同的数据库连接地址和图片前缀地址,如果公众号或者小程序有区分测试和正式环境,也可以在这里配置APPIDSECRET

"scripts": {"start": "NODE_ENV=development node ./bin/www","startPro": "NODE_ENV=production node ./bin/www"},
// 全局配置文件 /utils/config.js
const configBase = {mongoDb: { // 数据库development:'mongodb://127.0.0.1:27017/coach',production:'mongodb://127.0.0.1:27017/coach',},fileUrl:{ // 图片地址development:'http://127.0.0.1:3000/',production:'https://api.nihaojob.com/prod-api/',}
}
// 引用的配置对象 在各分模块中调用
let infoConfig = {}
let envKey = process.env.NODE_ENV
// 预知环境变量
let keys = ['development','production']
// 拼接引用的配置对象
Object.keys(configBase).forEach(item => {// 预知环境变量if(keys.includes(envKey)){infoConfig[item] = configBase[item][envKey]}else{// 本地infoConfig[item] = configBase[item].host}
})
module.exports = infoConfig;// 使用
const { fileUrl } = require("../utils/config");

Mongooes 连接

app.js中执行 require('./utils/dbs')(),并且把DB实例挂到global上。

// 配置文件
var baseConfig = require('./config.js');
const dbs = async function (env) {const mongoose = require('mongoose');mongoose.connect(baseConfig.mongoDb, { useNewUrlParser: true, auto_reconnect: true, poolSize: 10 });const db = mongoose.connection;db.on('error', console.error.bind(console, '数据库链接失败'));db.once('open', callback => {console.log(`数据库链接成功,地址:${baseConfig.mongoDb}`)});global.db = db;
}
module.exports = dbs;

Mongooes 增删改

这部分不多说了,利ORM框架Mongooes增删改特别简单,先创建模型再根据模型操作。

const mongoose = require('mongoose');
const { db } = global;
// 创建Model
const model = new mongoose.Schema({coachId:String, // 教练ID
}, {timestamps: true, // 自动增加创建、更新时间
});const dbManage = db.model('tag', model);
// 创建
dbManage.create({ name:'' })
// 更新 前边为查询条件 后边为更新内容
dbManage.updateOne({_id:'id'},{ name:'' })
// 删除
dbManage.remove({_id : 'id'})
// 查找
dbManage.find({_id : 'id' })
dbManage.findOne({_id : 'id' })

Mongooes 查询

查询的功能比较多了,比如字符串模糊查询,常见的分页、排序,时间范围搜索等。直接贴一个模板吧,copy直接用版,哈哈。

router.get('/getList', async function(req, res, next) {const { nickName = '', name = '', tel = '', newStatus = '', start = '1900-01-01', end = '2222-01-01'  } = req.queryconst { current = 1, pageSize = 20  } = req.queryconst startTime = moment(start).startOf('day')const endTime = moment(end).startOf('day')const queryParams = {$and: [{ nickName: { $regex: new RegExp(nickName, 'i') } }, // 昵称模糊查询{ name: { $regex: new RegExp(name, 'i') } }, // 姓名模糊查询{ tel: { $regex: new RegExp(tel, 'i') } },   //手机号模糊查询],coachId: req.tokenDecode._id,createdAt: {$gte: startTime.toDate(),$lte: moment(endTime).endOf('day').toDate()// $lte: moment(today).endOf('day').toDate() // 查询当天}}if (newStatus !== ''){// 数字状态模糊查询queryParams.$and.push({ newStatus: newStatus },)}// 分页 skip跳过数 limit每页数 sort排序方式const resault = await studentsModel.find(queryParams).skip(Number(pageSize) * Number(current-1)).limit(Number(pageSize)).sort({ createdAt: 'desc' });// 总数const total = await studentsModel.find(queryParams).count()res.send({code:1,data:{list: resault,page: {total,current: Number(current), // 当前页pageSize: Number(pageSize)}},});
});

图片上传

很多地方都要用到图片上传,使用formidable插件,设置上传路径为public,根据环境变量 + 文件名拼接图片地址,单独把图片地址存到一张表中,方便其他地方复用。

var express = require('express');
var router = express.Router();
const path = require('path');
const formidable = require('formidable');
var inFileModel = require("../model/inFile");
// 环境变量配置
const { fileUrl } = require("../utils/config");
// 上传文件
router.post('/', async function (req, res, next) {var imgPath = path.dirname(__dirname) + '/public';var form = new formidable.IncomingForm();form.encoding = 'utf-8'; //设置编辑form.uploadDir = imgPath; //设置上传目录form.keepExtensions = true; //保留后缀form.maxFieldsSize = 2 * 1024 * 1024; //文件大小form.type = true;const awaitFn = () => {return new Promise((resolve, reject) => {form.parse(req, (err, fields, files) => {let src = files.file.path.split('/');let urlString = src[src.length - 1]if (err) { reject(err); }resolve(urlString);});});};// 文件名const filePath = await awaitFn();// 储存上传记录const fileInfo = await inFileModel.create({ fileName:fileUrl + filePath, })res.send({ code:1, msg:'上传成功', data:fileInfo })
});
module.exports = router;

定时获取accesstoken

有过微信开发经验的同学都知道,调用微信服务端api需要accesstoken,时效2小时,利用CronJob定时获取accesstoken并保存成文件,获取失败时利用nodemailer发送报警邮件。

在部署时单独跑一个PM2进程,pm2 start cronTask.js

// cronTask.js
var CronJob = require('cron').CronJob;
const moment = require('moment');
const { writeFile } = require('fs').promises
const axios = require('axios')
var weConfig = require('./utils/weConfig');
var nodemailer = require('nodemailer');// 获取微信token
var getWeToken = new CronJob('0 0 0/1 * * *', async function () {console.log('获取token任务执行:', moment().format('YYYY-MM-DD HH:mm:ss'));const { APPID, SECRET } = weConfigconst url = `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${APPID}&secret=${SECRET}`axios.get(url).then(res => {if (res && res.status === 200 && res.data) {// 保存accesstoken文件writeFile('./accesstoken.txt', res.data.access_token, 'utf8')} else {// 发送报警内容sendMail('nihaojob@163.com', '保存错误:' + JSON.stringify(res))}})
}, null, true, 'America/Los_Angeles');function sendMail(mail, text) {// 发送邮箱配置var mailTransport = nodemailer.createTransport({host: 'smtp.163.com',port: 465,secureConnection: true, // 使用SSL方式(安全方式,防止被窃取信息)auth: {user: '邮箱',pass: '密码'},});const options = Object.assign(mailTransport, { from: 'nihaojob@163.com', to: mail }, {subject: 'token获取失败报警',text: text,})mailTransport.sendMail(options, (err, msg) => {if (err) {res.send({ code: -1, msg: err });return}mailTransport.close();});
}getWeToken.start();

生成小程序参数二维码

读取accesstoken.txt获取token,利用axios发送给微信服务器获取图片,这块有个点需要注意,请求会直接返回图片,需设置responseType: 'arraybuffer'直接把buffer数据保存为图片。

var express = require('express');
var router = express.Router();
const { fileUrl } = require("../utils/config");
const { writeFile, readFile } = require('fs').promisesconst axios = require('axios')
router.post('/getQrcode', async function (req, res, next) {const { path = '', scene = '' } = req.body;// 格式校验if (path === ''){res.send({ code: -1, msg: '非法参数' });return}const accessToken = await readFile('./accesstoken.txt')const url = `https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=${accessToken.toString()}`const data = await axios.post(url,{scene: scene,page: path,width:430,},{headers:{"Content-Type": "application/json"},responseType: 'arraybuffer',})if (data.data){const fileName =  'qrCode_' + new Date().getTime() + '.jpg'await writeFile('./public/' + fileName, data.data)res.send({ code: 1, data: fileUrl + fileName });}else{res.send({ code:-1, msg:'请求错误' })}
});
module.exports = router;

for await使用

我们有一个用户列表,需要根据用户列表里的用户id查询另外一张列表里的用户详情,将他们拼接成一个新的列表返回给前端,我不太会用用、关联查询,探索出一个比较笨的方法,用for await这种方法实现的。

router.get('/getTopList', async function (req, res, next) {const { coachId } = req.tokenDecode;let data = await topModel.find({ coachId }).sort({ integral: 'desc' });if (data && data.length !== 0) {data = JSON.parse(JSON.stringify(data))for (const item of data) {const info = await studentsModel.findOne({ _id: item.studentsId})item.info = info}}res.send({code: 1, data })
});

taro小程序

这篇笔记的重点在Node上,就不展开聊了,简单写下登录、request封装、环境变量。

登录

登录的流程是,用户点击openTypegetUserInfo的按钮发起授权,授权成功后调用Taro.login获取code,再把code发给后端,后端通过codeAPPIDSECRET获取openid,剩下的就是用openid来绑定用户关系了。

getUserInfo按钮 => 授权 => getCode => 获取openid

taro-request

taro官网上有一个taro-request的封装,蛮好用的地址。

环境变量

Taro的环境变量从process.env.NODE_ENV中读取,内置环境变量为developmentproduction,前端需要根据环境变量走不同的环境。

const getBaseUrl = (url) => {let BASE_URL = '';if (process.env.NODE_ENV === 'development') {//开发环境BASE_URL = 'http://localhost:3000'} else {// 生产环境BASE_URL = 'https://api.nihaojob.com/prod-api'}return BASE_URL
}
export default getBaseUrl;

后台部分

后台使用vue-element-admin模板,几乎没有复杂的内容,接入了图表、富文本、图片上传,就不展开了,后续会开发发菜单、权限管理,有可能使用node-casbinacl实现。

部署

前端静态文件直接使用Nginx指定静态目录,后端接口通过PM2启动服务,并用Nginxproxy_pass转到后端服务端口上。HTTPS证书申请与Nginx配置小节中有贴出,列一下自己最常用的几个PM2命令。

// npm script 启动应用
$ pm2 start --name 应用名称 npm -- run 'npm sctipt名称'
// 应用列表
$ pm2 list
// 查看日志
$ pm2 log
// 重启应用
$ pm2 restart '应用id'

总结

开头段子中有调侃后端大佬,纯粹只是玩笑;互联网技术日新月异,大家都在齐头并进,前端的内容都学不完,又怎敢对不懂的行业指手画脚。希望自己拥抱变化,保持敬畏

听说每个程序员都有一个创业梦,前端工程师真的可以借助Node跑起来自己的第一个MVP。

❤️爱心三连击1.看到这里了就点个在看支持下吧,你的「点赞,在看」是我创作的动力。
2.关注公众号程序员成长指北,回复「1」加入高级前端交流群!「在这里有好多 前端 开发者,会讨论 前端 Node 知识,互相学习」!
3.也可添加微信【ikoala520】,一起成长。“在看转发”是最大的支持

Node.js 开发实践,前端工程师的MVP利器相关推荐

  1. js array formdata_携程机票Node.js开发实践

    Nodejs自从2009年被开发出来以后,至今已经走过了9个年头,目前最新的稳定版已经到了10.13.从问世以后,Nodejs就深受前端工程师的喜欢. 在携程内部,Nodejs也是应用广泛,从开发工具 ...

  2. 腾讯高级工程师带你完整体验Node.js开发实战

    前几天,跟我一朋友聊天,他现在是阿里的架构师,说:「他们根本不知道,现在的电商大促有多么依赖 Node.js.」 说真的,我倒并不意外.作为一个定位明确的高性能 Web 服务器,Node.js 目前非 ...

  3. 爱奇艺 PC Web Node.js 中间层实践

    文章转载自公众号  爱奇艺技术产品团队 , 作者 前端研发团队 黑夜无论怎样悠长,白昼总会到来. 爱奇艺作为中国最大的互联网视频综合门户,一直致力于给用户提供更好的使用体验及观影品质.PC主站作为爱奇 ...

  4. 《Node.js开发指南》书评汇总

    刚查了下库存,发现订阅<Node.js开发指南>的读者大增,这是为什么呢?看了下近期本书在豆瓣的评论,口碑很好,现将豆瓣的书评汇总如下: ------------------------- ...

  5. 基于阿里云的 Node.js 稳定性实践

    前言 如果你看过 2018 Node.js 的用户报告,你会发现 Node.js 的使用有了进一步的增长,同时也出现了一些新的趋势. Node.js 的开发者更多的开始使用容器并积极的拥抱 Serve ...

  6. js提交出现post错误_阿里云的 Node.js 稳定性实践

    整理人:前端自习课 前言 如果你看过 2018 Node.js 的用户报告,你会发现 Node.js 的使用有了进一步的增长,同时也出现了一些新的趋势. Node.js 的开发者更多的开始使用容器并积 ...

  7. 2021年Node.js开发人员学习路线图

    Node.js 自发布以来,已成为业界重要破局者之一.Uber.Medium.PayPal 和沃尔玛等大型企业,纷纷将技术栈转向 Node.js.Node.js 支持开发功能强大的应用,例如实时追踪 ...

  8. 读《Node.js项目实践:构建可扩展的Web应用》 ——引编程慢慢变成系统化的“砌砖活”...

    读<Node.js项目实践:构建可扩展的Web应用> --引编程慢慢变成系统化的"砌砖活" 眼里的Node.JS 初初接触node是一年前的事,那时候年少不更事.还在纠 ...

  9. 《Node.js开发实战》代码下载、简介与前言

    请下载代码评估:https://pan.baidu.com/s/1qYC3cVa   (密码: bba3). 内容简介 本书以实战开发为原则,以Node.js原生知识和框架实战为主线,详细介绍Node ...

最新文章

  1. java多线程查询_利用Java函数式接口处理多线程查询
  2. ASP.NET MVC 5 - 视图
  3. R语言glm模型预测(predict)过程及Error in eval(predvars, data, env) 错误原因
  4. [logstash-input-file]插件使用详解
  5. android wifi
  6. 理解云计算备份与灾难恢复
  7. 北京理工大学计算机学院赵曜,北理工学子参加第十届蓝桥杯全国软件和专业人才大赛取得佳绩...
  8. 带着canvas去流浪系列之七 绘制水球图
  9. (转)Spring简介
  10. redis mysql 事务_Mysql与Redis事务
  11. java 数组转化为arraylist_在Java中怎样把数组转换为ArrayList?
  12. Eclipse,新建web项目后 出现jax-ws webservice
  13. ASP.NET Web程序设计 第九章 初识 Web Pages
  14. Java爬携程_Java数据爬取——爬取携程酒店数据(一)
  15. sql server分组排序
  16. 34个国外最好的DevOps工具
  17. 计算机能连上手机热点却无法连上无线网络,笔记本电脑win10系统无法连接手机热点,却能连上WiFi怎么办?...
  18. 服务器2008系统安全狗,win2008 r2 服务器安全设置之安全狗设置图文教程
  19. 可视化讲解:什么是宠物收养所问题?
  20. android 盒子 关闭电视,电视盒子正确使用方法,速速来取!

热门文章

  1. 苹果浏览器safari打不开网页怎么办?参考方法在这!
  2. oracle数据库应用技术
  3. php 进程管理,PHP 进程管理器 PHP-FPM
  4. 关于Anaconda中创建的虚拟环境使用Jupyter notebook出现“内核似乎挂掉“问题总结
  5. 佳能c3320怎么设置接收方_佳能c3320技术简报
  6. INSAR和SAR辨析
  7. JAVA 实现远程文件下载
  8. 2020年 TPAMI长文, Ball k-means:一种无界的快速自适应精确k-means算法
  9. 创维电视开机停留在Android4.0,“创维酷开”电视打开后就一直停留在开机界面,也关不...-创维电视关不机...
  10. 平面螺纹的lisp文件下载_CAD实用LISP文件