基于React和Node.JS的表單錄入系統(tǒng)的設(shè)計(jì)與實(shí)現(xiàn)
一、寫在前面
這是一個(gè)真實(shí)的項(xiàng)目,項(xiàng)目已經(jīng)過去好久了,雖然很簡(jiǎn)單,但還是有很多思考點(diǎn),跟隨著筆者的腳步,一起來(lái)看看吧。本文純屬虛構(gòu),涉及到的相關(guān)信息均已做虛構(gòu)處理,
二、背景
人活著一定要有信仰,沒有信仰的靈魂是空洞的。你可以信耶穌,信佛,信伊斯蘭,信科學(xué)等等。為了管控各大宗教場(chǎng)所的人員聚集,為社會(huì)增添一份綿薄之力,京州領(lǐng)導(dǎo)決定做一個(gè)表單系統(tǒng)來(lái)統(tǒng)計(jì)某個(gè)時(shí)間或者時(shí)間段的人員訪問量,控制宗教人員活動(dòng)的范圍,漢東省委沙瑞金書記特別關(guān)心這件事決定親自檢查,幾經(jīng)周轉(zhuǎn),這個(gè)任務(wù)落到了程序員江濤的頭上,故事由此展開。
三、需求分析
大致需要實(shí)現(xiàn)如下功能
表單數(shù)據(jù)的錄入 錄入數(shù)據(jù)的最近記錄查詢 短信驗(yàn)證碼的使用 掃碼填寫表單信息
有兩種方案, 一種是進(jìn)去自己選擇對(duì)應(yīng)的宗教場(chǎng)所(不對(duì)稱分布三級(jí)聯(lián)動(dòng)),第二種是點(diǎn)擊對(duì)應(yīng)的宗教場(chǎng)所進(jìn)行填寫表單,表單處的場(chǎng)所不可更改,不同的設(shè)計(jì)不同的思路。雖然兩種都寫了, 但這里我就按第二種寫這篇文章,如果有興趣了解第一種歡迎與我交流。

四、系統(tǒng)設(shè)計(jì)
這次我決定不用vue,改用react的taro框架寫這個(gè)小項(xiàng)目(試一下多端框架taro哈哈), 后端這邊打算用nodejs的eggjs框架, 數(shù)據(jù)庫(kù)還是用mysql, 還會(huì)用到redis。由于服務(wù)器端口限制,搞不動(dòng)docker啊, 也沒有nginx,莫得關(guān)系,egg自帶web服務(wù)器將就用一下項(xiàng)目也就做完了,就這樣taro和egg的試管嬰兒誕生了。
五、代碼實(shí)現(xiàn)
額,東西又多又雜,挑著講吧, 建議結(jié)合這兩篇篇文章一起看, 基于Vue.js和Node.js的反欺詐系統(tǒng)設(shè)計(jì)與實(shí)現(xiàn) https://www.cnblogs.com/cnroadbridge/p/15182552.html, 基于React和GraphQL的demo設(shè)計(jì)與實(shí)現(xiàn) https://www.cnblogs.com/cnroadbridge/p/15318408.html
5.1 前端實(shí)現(xiàn)
taroJS的安裝使用參見https://taro-docs.jd.com/taro/docs/GETTING-STARTED
5.1.1 整體的布局設(shè)計(jì)
主要還是頭部和其他這種布局,比較簡(jiǎn)單,然后抽離出一個(gè)公共組件header,給它拋出一個(gè)可以跳轉(zhuǎn)鏈接的方法, 邏輯很簡(jiǎn)單就是一個(gè)標(biāo)題,然后后面有一個(gè)返回首頁(yè)的圖標(biāo)
import { View, Text } from '@tarojs/components';
import { AtIcon } from 'taro-ui'
import "taro-ui/dist/style/components/icon.scss";
import 'assets/iconfont/iconfont.css'
import './index.scss'
import { goToPage } from 'utils/router.js'
export default function Header(props) {
return (
{ props.title }
goToPage('index')}>
)
}
關(guān)于這一塊,還可以看下components下的card組件的封裝
5.1.2 表單的設(shè)計(jì)
表單設(shè)計(jì)這塊,感覺也沒啥好講的,主要是你要寫一些css去適配頁(yè)面,具體的邏輯實(shí)現(xiàn)代碼如下:
import Taro, { getCurrentInstance } from '@tarojs/taro';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { update } from 'actions/form';
import { View, Text, RadioGroup, Radio, Label, Picker } from '@tarojs/components';
import { AtForm, AtInput, AtButton, AtTextarea, AtList, AtListItem } from 'taro-ui';
import Header from 'components/header'
import 'taro-ui/dist/style/components/input.scss';
import 'taro-ui/dist/style/components/icon.scss';
import 'taro-ui/dist/style/components/button.scss';
import 'taro-ui/dist/style/components/radio.scss';
import 'taro-ui/dist/style/components/textarea.scss';
import 'taro-ui/dist/style/components/list.scss';
import "taro-ui/dist/style/components/loading.scss";
import './index.scss';
import cityData from 'data/city.json';
import provinceData from 'data/province.json';
import { goToPage } from 'utils/router';
import { request } from 'utils/request';
@connect(({ form }) => ({
form
}), (dispatch) => ({
updateForm (data) {
dispatch(update(data))
}
}))
export default class VisitorRegistration extends Component {
constructor (props) {
super(props);
this.state = {
title: '預(yù)約登記', // 標(biāo)題
username: '', // 姓名
gender: '', // 性別
mobile: '', // 手機(jī)
idcard: '', // 身份證
orgin: '', //訪客來(lái)源地
province: '', //省
city: '', // 市
place: '', //宗教地址
religiousCountry: '', // 宗教縣區(qū)
religiousType: '', // 宗教類型
matter: '', // 來(lái)訪事由
visiteDate: '', // 拜訪日期
visiteTime: '', // 拜訪時(shí)間
leaveTime: '', // 離開時(shí)間
genderOptions: [
{ label: '男', value: 'male' },
{ label: '女', value: 'female' },
], // 性別選項(xiàng)
genderMap: { male: '男', female: '女' },
timeRangeOptions: [
'00:00-02:00',
'02:00-04:00',
'04:00-06:00',
'06:00-08:00',
'08:00-10:00',
'10:00-12:00',
'12:00-14:00',
'14:00-16:00',
'16:00-18:00',
'18:00-20:00',
'20:00-22:00',
'22:00-24:00',
], // 時(shí)間選項(xiàng)
orginRangeOptions: [[],[]], // 省市選項(xiàng)
orginRangeKey: [0, 0],
provinces: [],
citys: {},
isLoading: false,
}
this.$instance = getCurrentInstance()
Taro.setNavigationBarTitle({
title: this.state.title
})
}
async componentDidMount () {
console.log(this.$instance.router.params)
const { place } = this.$instance.router.params;
const cityOptions = {};
const provinceOptions = {};
const provinces = [];
const citys = {};
provinceData.forEach(item => {
const { code, name } = item;
provinceOptions[code] = name;
provinces.push(name);
})
for(const key in cityData) {
cityOptions[provinceOptions[key]] = cityData[key];
citys[provinceOptions[key]] = [];
for (const item of cityData[key]) {
if (item.name === '直轄市') {
citys[provinceOptions[key]].push('');
} else {
citys[provinceOptions[key]].push(item.name);
}
}
}
const orginRangeOptions = [provinces, []]
await this.setState({
provinces,
citys,
orginRangeOptions,
place
});
}
handleOriginRangeChange = event => {
let { value: [ k1, k2 ] } = event.detail;
const { provinces, citys } = this.state;
const province = provinces[k1];
const city = citys[province][k2];
const orgin = `${province}${city}`;
this.setState({
province,
city,
orgin
})
}
handleOriginRangleColumnChange = event => {
let { orginRangeKey } = this.state;
let changeColumn = event.detail;
let { column, value } = changeColumn;
switch (column) {
case 0:
this.handleRangeData([value, 0]);
break;
case 1:
this.handleRangeData([orginRangeKey[0], value]);
}
}
handleRangeData = orginRangeKey => {
const [k0] = orginRangeKey;
const { provinces, citys } = this.state;
const cityOptions = citys[provinces[k0]]
const orginRangeOptions = [provinces, cityOptions];
this.setState({
orginRangeKey,
orginRangeOptions
})
}
handleChange (key, value) {
this.setState({
[key]: value
})
return value;
}
handleDateChange(key, event) {
const value = event.detail.value;
this.setState({
[key]: value
})
return value;
}
handleClick (key, event) {
const value = event.target.value;
this.setState({
[key]: value
})
return value;
}
handleRadioClick (key, value) {
this.setState({
[key]: value
})
return value;
}
async onSubmit (event) {
const {
username,
gender,
mobile,
idcard,
orgin,
province,
city,
place,
religiousCountry,
religiousType,
visiteDate,
visiteTime,
leaveTime,
matter,
genderMap,
} = this.state;
if (!username) {
Taro.showToast({
title: '請(qǐng)?zhí)顚懹脩裘?,
icon: 'none',
duration: 2000
})
return;
} else if (!gender) {
Taro.showToast({
title: '請(qǐng)選擇性別',
icon: 'none',
duration: 2000
})
return;
} else if (!mobile || !/^1(3[0-9]|4[579]|5[012356789]|66|7[03678]|8[0-9]|9[89])\d{8}$/.test(mobile)) {
Taro.showToast({
title: '請(qǐng)?zhí)顚懻_的手機(jī)號(hào)',
icon: 'none',
duration: 2000
})
return;
} else if (!idcard || !/^(^[1-9]\d{7}((0\d)|(1[0-2]))(([0|1|2]\d)|3[0-1])\d{3}$)|(^[1-9]\d{5}[1-9]\d{3}((0\d)|(1[0-2]))(([0|1|2]\d)|3[0-1])((\d{4})|\d{3}[Xx])$)$/.test(idcard)) {
Taro.showToast({
title: '請(qǐng)?zhí)顚懻_的身份證號(hào)',
icon: 'none',
duration: 2000
})
return;
} else if (!orgin) {
Taro.showToast({
title: '請(qǐng)選擇來(lái)源地',
icon: 'none',
duration: 2000
})
return;
} else if (!place) {
Taro.showToast({
title: '請(qǐng)選擇宗教場(chǎng)所',
icon: 'none',
duration: 2000
})
return;
} else if (!visiteDate) {
Taro.showToast({
title: '請(qǐng)選擇預(yù)約日期',
icon: 'none',
duration: 2000
})
return;
} else if (!visiteTime) {
Taro.showToast({
title: '請(qǐng)選擇預(yù)約時(shí)間',
icon: 'none',
duration: 2000
})
return;
}
await this.setState({
isLoading: true
})
const data = {
username,
gender: genderMap[gender],
mobile,
idcard,
orgin,
province,
city,
place,
religiousCountry,
religiousType,
visiteDate,
visiteTime,
leaveTime,
matter,
};
const { data: { code, status, data: formData }} = await request({
url: '/record',
method: 'post',
data
});
await this.setState({
isLoading: false
});
if (code === 0 && status === 200 && data) {
Taro.showToast({
title: '預(yù)約成功',
icon: 'success',
duration: 2000,
success: () => {
// goToPage('result-query', {}, (res) => {
// res.eventChannel.emit('formData', { data: formData })
// })
this.props.updateForm(formData)
goToPage('result-query')
}
});
} else {
Taro.showToast({
title: '預(yù)約失敗',
icon: 'none',
duration: 2000
})
return;
}
}
handlePickerChange = (key, optionName, event) => {
const options = this.state[optionName];
this.setState({
[key]: options[event.detail.value]
})
}
render() {
const { title,
username,
genderOptions,
mobile,
idcard,
visiteTime,
timeRangeOptions,
leaveTime,
matter,
visiteDate,
orgin,
orginRangeOptions,
orginRangeKey,
place,
isLoading
} = this.state;
return (
onSubmit={this.onSubmit.bind(this)}
>
required
type='text'
name='username'
className='col'
title='訪客姓名'
placeholder='請(qǐng)輸入訪客姓名'
value={username}
onChange={(value) => {this.handleChange('username', value)}}
/>
性別
{genderOptions.map((genderOption, i) => {
return (
)
})}
required
type='phone'
name='mobile'
title='手機(jī)號(hào)碼'
className='col'
placeholder='請(qǐng)輸入手機(jī)號(hào)碼'
value={mobile}
onChange={(value) => {this.handleChange('mobile', value)}}
/>
required
name='idcard'
type='idcard'
className='col'
title='身份證號(hào)'
placeholder='請(qǐng)輸入身份證號(hào)碼'
value={idcard}
onChange={(value) => {this.handleChange('idcard', value)}}
/>
來(lái)源地
onChange={(event) => this.handleOriginRangeChange(event)}
onColumnChange={(event) => this.handleOriginRangleColumnChange(event)}
range={orginRangeOptions}
value={orginRangeKey}>
{orgin ? (
className='at-list__item-fix'
extraText={orgin}
/>) : (請(qǐng)選擇訪客來(lái)源地 )}
required
type='text'
name='place'
className='col'
title='宗教場(chǎng)所'
disabled
placeholder='請(qǐng)選擇宗教場(chǎng)所'
value={place}
onChange={(value) => {this.handleChange('place', value)}}
/>
預(yù)約日期
onChange={(event) => this.handleDateChange('visiteDate', event)}>
{visiteDate ? (
className='at-list__item-fix'
extraText={visiteDate}
/>) : (請(qǐng)選擇預(yù)約日期 )}
預(yù)約時(shí)間
range={timeRangeOptions}
onChange={(event) => this.handlePickerChange('visiteTime', 'timeRangeOptions', event)}>
{visiteTime ? (
className='at-list__item-fix'
extraText={visiteTime}
/>) : (請(qǐng)選擇預(yù)約時(shí)間 )}
離開時(shí)間
range={timeRangeOptions}
onChange={(event) => this.handlePickerChange('leaveTime', 'timeRangeOptions', event)}>
{leaveTime ? (
className='at-list__item-fix'
extraText={leaveTime}
/>) : (請(qǐng)選擇離開時(shí)間 )}
來(lái)訪事由
maxLength={200}
className='textarea-fix'
value={matter}
onChange={(value) => {this.handleChange('matter', value)}}
placeholder='請(qǐng)輸入來(lái)訪事由...'
/>
circle
loading={isLoading}
disabled={isLoading}
type='primary'
size='normal'
formType='submit'
className='col btn-submit'>
提交
);
}
}
5.1.3 短信驗(yàn)證碼的設(shè)計(jì)實(shí)現(xiàn)
這里也可以單獨(dú)抽離出一個(gè)組件,主要的點(diǎn)在于,點(diǎn)擊后的倒計(jì)時(shí)和重新發(fā)送,可以重點(diǎn)看下,具體的實(shí)現(xiàn)邏輯如下:
import Taro from '@tarojs/taro';
import { Component } from 'react';
import { View, Text } from '@tarojs/components';
import { AtInput, AtButton } from 'taro-ui';
import 'taro-ui/dist/style/components/input.scss';
import 'taro-ui/dist/style/components/button.scss';
import './index.scss';
const DEFAULT_SECOND = 120;
import { request } from 'utils/request';
export default class SendSMS extends Component {
constructor(props) {
super(props);
this.state = {
mobile: '', // 手機(jī)號(hào)
confirmCode: '', // 驗(yàn)證碼
smsCountDown: DEFAULT_SECOND,
smsCount: 0,
smsIntervalId: 0,
isClick: false,
};
}
componentDidMount () { }
componentWillUnmount () {
if (this.state.smsIntervalId) {
clearInterval(this.state.smsIntervalId);
this.setState(prevState => {
return {
...prevState,
smsIntervalId: 0,
isClick: false
}
})
}
}
componentDidUpdate (prevProps, prveState) {
}
componentDidShow () { }
componentDidHide () { }
handleChange (key, value) {
this.setState({
[key]: value
})
return value;
}
processSMSRequest () {
const { mobile } = this.state;
if (!mobile || !/^1(3[0-9]|4[579]|5[012356789]|66|7[03678]|8[0-9]|9[89])\d{8}$/.test(mobile)) {
Taro.showToast({
title: '請(qǐng)?zhí)顚懻_的手機(jī)號(hào)',
icon: 'none',
duration: 2000
})
return;
}
this.countDown()
}
sendSMS () {
const { mobile } = this.state;
request({
url: '/sms/send',
method: 'post',
data: { mobile }
}, false).then(res => {
console.log(res);
const { data: { data: { description } } } = res;
Taro.showToast({
title: description,
icon: 'none',
duration: 2000
})
}).catch(err => {
console.log(err);
});
}
countDown () {
if (this.state.smsIntervalId) {
return;
}
const smsIntervalId = setInterval(() => {
const { smsCountDown } = this.state;
if (smsCountDown === DEFAULT_SECOND) {
this.sendSMS();
}
this.setState({
smsCountDown: smsCountDown - 1,
isClick: true
}, () => {
const { smsCount, smsIntervalId, smsCountDown } = this.state;
if (smsCountDown <= 0) {
this.setState({
smsCountDown: DEFAULT_SECOND,
})
smsIntervalId && clearInterval(smsIntervalId);
this.setState(prevState => {
return {
...prevState,
smsIntervalId: 0,
smsCount: smsCount + 1,
}
})
}
})
}, 1000);
this.setState({
smsIntervalId
})
}
submit() {
// 校驗(yàn)參數(shù)
const { mobile, confirmCode } = this.state;
if (!mobile || !/^1(3[0-9]|4[579]|5[012356789]|66|7[03678]|8[0-9]|9[89])\d{8}$/.test(mobile)) {
Taro.showToast({
title: '請(qǐng)?zhí)顚懻_的手機(jī)號(hào)',
icon: 'none',
duration: 2000
})
return;
} else if (confirmCode.length !== 6) {
Taro.showToast({
title: '驗(yàn)證碼輸入有誤',
icon: 'none',
duration: 2000
})
return;
}
this.props.submit({ mobile, code: confirmCode });
}
render () {
const { mobile, confirmCode, smsCountDown, isClick } = this.state;
return (
required
type='phone'
name='mobile'
title='手機(jī)號(hào)碼'
className='row-inline-col-7'
placeholder='請(qǐng)輸入手機(jī)號(hào)碼'
value={mobile}
onChange={(value) => {this.handleChange('mobile', value)}}
/>
{!isClick ? ( onClick={() => this.processSMSRequest()}
className='row-inline-col-3 at-input__input code-fix'>
發(fā)送驗(yàn)證碼
) : ( onClick={() => this.processSMSRequest()}
className='row-inline-col-3 at-input__input code-fix red'>
{( smsCountDown === DEFAULT_SECOND ) ? '重新發(fā)送' : `${smsCountDown}秒后重試`}
)}
required
type='text'
name='confirmCode'
title='驗(yàn)證碼'
placeholder='請(qǐng)輸入驗(yàn)證碼'
value={confirmCode}
onChange={(value) => {this.handleChange('confirmCode', value)}}
/>
circle
type='primary'
size='normal'
onClick={() => this.submit()}
className='col btn-submit'>
查詢
)
}
}
5.1.4 前端的一些配置
路由跳頁(yè)模塊的封裝
import?Taro?from?'@tarojs/taro';
//?https://taro-docs.jd.com/taro/docs/apis/route/navigateTo
export?const?goToPage?=?(page,?params?=?{},?success,?events)?=>?{
??let?url?=?`/pages/${page}/index`;
??if?(Object.keys(params).length?>?0)?{
????let?paramsStr?=?'';
????for?(const?key?in?params)?{
??????const?tmpStr?=?`${key}=${params[key]}`;
??????paramsStr?=?tmpStr?+?'&';
????}
????if?(paramsStr.endsWith('&'))?{
??????paramsStr?=?paramsStr.substr(0,?paramsStr.length?-?1);
????}
????if?(paramsStr)?{
??????url?=?`${url}?${paramsStr}`;
????}
??}
??Taro.navigateTo({
????url,
????success,
????events
??});
};
請(qǐng)求方法模塊的封裝
import?Taro?from?'@tarojs/taro';
const?baseUrl?=?'http://127.0.0.1:9000';?//?請(qǐng)求的地址
export?function?request(options,?isLoading?=?true)?{
??const?{?url,?data,?method,?header?}?=?options;
??isLoading?&&
????Taro.showLoading({
??????title:?'加載中'
????});
??return?new?Promise((resolve,?reject)?=>?{
????Taro.request({
??????url:?baseUrl?+?url,
??????data:?data?||?{},
??????method:?method?||?'GET',
??????header:?header?||?{},
??????success:?res?=>?{
????????resolve(res);
??????},
??????fail:?err?=>?{
????????reject(err);
??????},
??????complete:?()?=>?{
????????isLoading?&&?Taro.hideLoading();
??????}
????});
??});
}
日期格式的封裝
import?moment?from?'moment';
export?const?enumerateDaysBetweenDates?=?function(startDate,?endDate)?{
??let?daysList?=?[];
??let?SDate?=?moment(startDate);
??let?EDate?=?moment(endDate);
??let?xt;
??daysList.push(SDate.format('YYYY-MM-DD'));
??while?(SDate.add(1,?'days').isBefore(EDate))?{
????daysList.push(SDate.format('YYYY-MM-DD'));
??}
??daysList.push(EDate.format('YYYY-MM-DD'));
??return?daysList;
};
export?const?getSubTractDate?=?function(n?=?-2)?{
??return?moment()
????.subtract(n,?'months')
????.format('YYYY-MM-DD');
};
阿里媽媽圖標(biāo)庫(kù)引入, 打開https://www.iconfont.cn/ ,找到喜歡的圖表下載下來(lái), 然后引入,在對(duì)應(yīng)的地方加上iconfont和它對(duì)應(yīng)的樣式類的值
import { View, Text } from '@tarojs/components';
import { AtIcon } from 'taro-ui'
import "taro-ui/dist/style/components/icon.scss";
import 'assets/iconfont/iconfont.css'
import './index.scss'
import { goToPage } from 'utils/router.js'
export default function Header(props) {
return (
{ props.title }
goToPage('index')}>
)
}
redux的使用,這里主要是多頁(yè)面共享數(shù)據(jù)的時(shí)候用了下,核心代碼就這點(diǎn)
import?{?UPDATE?}?from?'constants/form';
const?INITIAL_STATE?=?{
??city:?'',
??createTime:?'',
??gender:?'',
??id:?'',
??idcard:?'',
??leaveTime:?'',
??matter:?'',
??mobile:?'',
??orgin:?'',
??place:?'',
??province:?'',
??religiousCountry:?'',
??religiousType:?'',
??updateTime:?'',
??username:?'',
??visiteDate:?'',
??visiteTime:?''
};
export?default?function?form(state?=?INITIAL_STATE,?action)?{
??switch?(action.type)?{
????case?UPDATE:
??????return?{
????????...state,
????????...action.data
??????};
????default:
??????return?state;
??}
}
使用方法如下
@connect(({?form?})?=>?({
??form
}),?(dispatch)?=>?({
??updateForm?(data)?{
????dispatch(update(data))
??}
}))
??componentWillUnmount?()?{
????const?{?updateForm?}?=?this.props;
????updateForm({
??????city:?'',
??????createTime:?'',
??????gender:?'',
??????id:?'',
??????idcard:?'',
??????leaveTime:?'',
??????matter:?'',
??????mobile:?'',
??????orgin:?'',
??????place:?'',
??????province:?'',
??????religiousCountry:?'',
??????religiousType:?'',
??????updateTime:?'',
??????username:?'',
??????visiteDate:?'',
??????visiteTime:?''
????})
??}
開發(fā)環(huán)境和生成環(huán)境的打包配置, ?因?yàn)樽詈笠系絜gg服務(wù)里面,所以這里生產(chǎn)環(huán)境的publicPath和baseName都應(yīng)該是 /public
module.exports?=?{
??env:?{
????NODE_ENV:?'"production"'
??},
??defineConstants:?{},
??mini:?{},
??h5:?{
????/**
?????*?如果h5端編譯后體積過大,可以使用webpack-bundle-analyzer插件對(duì)打包體積進(jìn)行分析。
?????*?參考代碼如下:
?????*?webpackChain?(chain)?{
?????*???chain.plugin('analyzer')
?????*?????.use(require('webpack-bundle-analyzer').BundleAnalyzerPlugin,?[])
?????*?}
?????*/
????publicPath:?'/public',
????router:?{
??????basename:?'/public'
????}
??}
};
開發(fā)環(huán)境名字可自定義如:
module.exports?=?{
??env:?{
????NODE_ENV:?'"development"'
??},
??defineConstants:?{},
??mini:?{},
??h5:?{
????publicPath:?'/',
????esnextModules:?['taro-ui'],
????router:?{
??????basename:?'/religion'
????}
??}
};
5.2 后端實(shí)現(xiàn)
后端這塊,其他的都沒啥好講的,具體可以參看我之前寫的兩篇文章,或者閱讀源碼,這里著重講下防止短信驗(yàn)證碼惡意注冊(cè)吧。
5.2.1 如何防止短信驗(yàn)證碼對(duì)惡意使用
這個(gè)主要是在于用的是內(nèi)部實(shí)現(xiàn)的短信驗(yàn)證碼接口(自家用的),不是市面上一些成熟的短信驗(yàn)證碼接口,所以在預(yù)發(fā)布階段安全方面曾經(jīng)收到過一次攻擊(包工頭家的服務(wù)器每天都有人去攻擊,好巧不巧剛被我撞上了),被惡意使用了1W條左右短信,痛失8張毛爺爺啊??偨Y(jié)了下這次教訓(xùn),主要是從IP、發(fā)送的頻率、以及加上csrf Token去預(yù)防被惡意使用。
大致是這樣搞得。
安裝相對(duì)于的類庫(kù)
"egg-ratelimiter":?"^0.1.0",
"egg-redis":?"^2.4.0",
在config/plugin.js下配置
?ratelimiter:?{
????enable:?true,
????package:?'egg-ratelimiter',
??},
??redis:?{
????enable:?true,
????package:?'egg-redis',
??},
在config/config.default.js下配置
?config.ratelimiter?=?{
????//?db:?{},
????router:?[
??????{
????????path:?'/sms/send',
????????max:?5,
????????time:?'60s',
????????message:?'臥槽,你不講武德,老是請(qǐng)求干嘛干嘛干嘛!',
??????},
????],
??};
??config.redis?=?{
????client:?{
??????port:?6379,?//?Redis?port
??????host:?'127.0.0.1',?//?Redis?host
??????password:?null,
??????db:?0,
????},
??};
效果是這樣的

六、參考文獻(xiàn)
TaroJS官網(wǎng):https://taro-docs.jd.com/taro/docs/README ReactJS官網(wǎng):https://reactjs.org/ eggJS官網(wǎng):https://eggjs.org/
七、寫在最后
關(guān)于UI這塊也就這樣了, 我應(yīng)該多認(rèn)識(shí)些UI小姐姐幫忙看一看的,看到這里就要和大家說(shuō)再見了, 通過閱讀本文,對(duì)于表單的制作你學(xué)會(huì)了嗎?歡迎在下方發(fā)表你的看法,也歡迎和筆者交流!
github項(xiàng)目地址:https://github.com/cnroadbridge/jingzhou-religion
gitee項(xiàng)目地址:https://gitee.com/taoge2021/jingzhou-religion
