1, Write in front
This is a real project. It has been a long time. Although it is very simple, there are still many thinking points. Follow the steps of the author and have a look. This article is purely fictional, and the relevant information involved has been fictitious,
2, Background
People must have faith when they live. The soul without faith is empty. You can believe in Jesus, Buddha, Islam, science and so on. In order to control the gathering of people in major religious places and add a modest force to the society, the leaders of Jingzhou decided to make a form system to count the number of visits of people at a certain time or period and control the scope of religious activities. Secretary Sha Ruijin of Handong provincial Party committee was particularly concerned about this matter and decided to check it in person. After several turns, the task fell to programmer Jiang Tao, The story unfolds.
3, Demand analysis
The following functions need to be realized:
- Entry of form data
- Query of the latest record of the entered data
- Use of SMS verification code
- Scan code and fill in form information
There are two schemes. One is to go in and choose the corresponding religious place (asymmetric distribution and three-level linkage). The other is to click the corresponding religious place to fill in the form. The place in the form cannot be changed. Different designs have different ideas. Although I have written both, here I will write this article according to the second one. If you are interested in understanding the first one, you are welcome to communicate with me.

4, System design
This time I decided not to use vue, but to use react's Taro framework to write this small project (try the multi terminal framework taro ha ha). The backend side plans to use nodejs's eggjs framework, mysql for the database, and redis for the database. Due to the limitation of server port, docker can't be moved, and there's no nginx. It doesn't matter. Egg's own web server will be used, and the project will be completed. In this way, the test tube baby of taro and egg was born.
5, Code implementation
Well, there are many and miscellaneous things. Let's talk about them. It's suggested to read these two articles together, based on Vue JS and node Design and implementation of anti fraud system based on JS https://www.cnblogs.com/cnroadbridge/p/15182552.html , design and implementation of demo based on React and GraphQL https://www.cnblogs.com/cnroadbridge/p/15318408.html
5.1 front end implementation
For the installation and use of taroJS, see https://taro-docs.jd.com/taro/docs/GETTING-STARTED
5.1.1 overall layout design
It is mainly the head and other layouts. It is relatively simple. Then pull out a public component header and throw it a method that can jump to the link. The logic is very simple, that is, a title, followed by an icon to return to the home page
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 ( <View className='header'> <Text className='header-text'>{ props.title }</Text> <Text onClick={() => goToPage('index')}> <AtIcon prefixClass='icon' className='iconfont header-reback' value='home' color='#6190e8'></AtIcon> </Text> </View> ) }
For this part, you can also see the packaging of card components under components
5.1.2 form design
There's nothing to say about form design. It's mainly that you need to write some css to adapt to the page. The specific logic implementation code is as follows:
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: 'Reservation registration', // title username: '', // full name gender: '', // Gender mobile: '', // mobile phone idcard: '', // ID orgin: '', //Visitor origin province: '', //province city: '', // city place: '', //Religious address religiousCountry: '', // Religious County religiousType: '', // Religious type matter: '', // Reason for visit visiteDate: '', // Date of visit visiteTime: '', // Visit time leaveTime: '', // Departure time genderOptions: [ { label: 'male', value: 'male' }, { label: 'female', value: 'female' }, ], // Gender options genderMap: { male: 'male', female: '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', ], // Time options orginRangeOptions: [[],[]], // Provincial and municipal options 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 === 'municipality directly under the Central Government') { 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: 'Please fill in the user name', icon: 'none', duration: 2000 }) return; } else if (!gender) { Taro.showToast({ title: 'Please select gender', 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: 'Please fill in the correct mobile phone number', 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: 'Please fill in the correct ID number.', icon: 'none', duration: 2000 }) return; } else if (!orgin) { Taro.showToast({ title: 'Please select the source', icon: 'none', duration: 2000 }) return; } else if (!place) { Taro.showToast({ title: 'Please choose a religious place', icon: 'none', duration: 2000 }) return; } else if (!visiteDate) { Taro.showToast({ title: 'Please select an appointment date', icon: 'none', duration: 2000 }) return; } else if (!visiteTime) { Taro.showToast({ title: 'Please select an appointment time', 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: 'Appointment succeeded', 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: 'Appointment failed', 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 ( <View className='visitor-registration'> <Header title={title}/> <AtForm onSubmit={this.onSubmit.bind(this)} > <View className='row'> <AtInput required type='text' name='username' className='col' title='Visitor name' placeholder='Please enter visitor name' value={username} onChange={(value) => {this.handleChange('username', value)}} /> </View> <View className='row'> <View className='col at-input'> <Text className='at-input__title at-input__title--required'> Gender </Text> <View className='at-input__input'> <RadioGroup> {genderOptions.map((genderOption, i) => { return ( <Label for={i} key={i}> <Radio value={genderOption.value} onClick={(event) => {this.handleRadioClick('gender', genderOption.value)}}> {genderOption.label} </Radio> </Label> ) })} </RadioGroup> </View> </View> </View> <View className='row'> <AtInput required type='phone' name='mobile' title='phone number' className='col' placeholder='Please enter your mobile phone number' value={mobile} onChange={(value) => {this.handleChange('mobile', value)}} /> </View> <View className='row'> <AtInput required name='idcard' type='idcard' className='col' title='ID number' placeholder='Please enter your ID number.' value={idcard} onChange={(value) => {this.handleChange('idcard', value)}} /> </View> <View className='row'> <View className='at-input col col-fix'> <Text className='at-input__title at-input__title--required'> Source </Text> <Picker mode='multiSelector' onChange={(event) => this.handleOriginRangeChange(event)} onColumnChange={(event) => this.handleOriginRangleColumnChange(event)} range={orginRangeOptions} value={orginRangeKey}> <AtList> {orgin ? ( <AtListItem className='at-list__item-fix' extraText={orgin} />) : (<Text className='input-placeholder-fix'>Please select the source of visitors</Text>)} </AtList> </Picker> </View> </View> <View className='row'> <AtInput required type='text' name='place' className='col' title='Religious sites' disabled placeholder='Please choose a religious place' value={place} onChange={(value) => {this.handleChange('place', value)}} /> </View> <View className='row'> <View className='at-input col col-fix'> <Text className='at-input__title at-input__title--required'> Appointment date </Text> <Picker mode='date' onChange={(event) => this.handleDateChange('visiteDate', event)}> <AtList> {visiteDate ? ( <AtListItem className='at-list__item-fix' extraText={visiteDate} />) : (<Text className='input-placeholder-fix'>Please select an appointment date</Text>)} </AtList> </Picker> </View> </View> <View className='row'> <View className='at-input col col-fix'> <Text className='at-input__title at-input__title--required'> time of appointment </Text> <Picker mode='selector' range={timeRangeOptions} onChange={(event) => this.handlePickerChange('visiteTime', 'timeRangeOptions', event)}> <AtList> {visiteTime ? ( <AtListItem className='at-list__item-fix' extraText={visiteTime} />) : (<Text className='input-placeholder-fix'>Please select an appointment time</Text>)} </AtList> </Picker> </View> </View> <View className='row'> <View className='at-input col col-fix'> <Text className='at-input__title'> Departure time </Text> <Picker mode='selector' range={timeRangeOptions} onChange={(event) => this.handlePickerChange('leaveTime', 'timeRangeOptions', event)}> <AtList> {leaveTime ? ( <AtListItem className='at-list__item-fix' extraText={leaveTime} />) : (<Text className='input-placeholder-fix'>Please select the departure time</Text>)} </AtList> </Picker> </View> </View> <View className='row'> <View className='col at-input'> <Text className='at-input__title'> Reason for visit </Text> <AtTextarea maxLength={200} className='textarea-fix' value={matter} onChange={(value) => {this.handleChange('matter', value)}} placeholder='Please enter the reason for visiting...' /> </View> </View> <View className='row'> <AtButton circle loading={isLoading} disabled={isLoading} type='primary' size='normal' formType='submit' className='col btn-submit'> Submit </AtButton> </View> </AtForm> </View> ); } }
5.1.3 design and implementation of SMS verification code
You can also separate a component here. The main point is that the countdown and resending after clicking can be focused on. The specific implementation logic is as follows:
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: '', // cell-phone number confirmCode: '', // Verification Code 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: 'Please fill in the correct mobile phone number', 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() { // Calibration parameters 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: 'Please fill in the correct mobile phone number', icon: 'none', duration: 2000 }) return; } else if (confirmCode.length !== 6) { Taro.showToast({ title: 'Incorrect verification code input', icon: 'none', duration: 2000 }) return; } this.props.submit({ mobile, code: confirmCode }); } render () { const { mobile, confirmCode, smsCountDown, isClick } = this.state; return ( <View className='sms-box'> <View className='row-inline'> <AtInput required type='phone' name='mobile' title='phone number' className='row-inline-col-7' placeholder='Please enter your mobile phone number' value={mobile} onChange={(value) => {this.handleChange('mobile', value)}} /> {!isClick ? ( <Text onClick={() => this.processSMSRequest()} className='row-inline-col-3 at-input__input code-fix'> Send verification code </Text>) : ( <Text onClick={() => this.processSMSRequest()} className='row-inline-col-3 at-input__input code-fix red'> {( smsCountDown === DEFAULT_SECOND ) ? 'Resend' : `${smsCountDown}Try again in seconds`} </Text>)} </View> <View> <AtInput required type='text' name='confirmCode' title='Verification Code' placeholder='Please enter the verification code' value={confirmCode} onChange={(value) => {this.handleChange('confirmCode', value)}} /> </View> <View> <AtButton circle type='primary' size='normal' onClick={() => this.submit()} className='col btn-submit'> query </AtButton> </View> </View> ) } }
5.1.4 some configurations of the front end
Encapsulation of routing page hopping module
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 }); };
Encapsulation of request method module
import Taro from '@tarojs/taro'; const baseUrl = 'http://127.0.0.1:9000'; // Requested address export function request(options, isLoading = true) { const { url, data, method, header } = options; isLoading && Taro.showLoading({ title: 'Loading' }); 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(); } }); }); }
Encapsulation of date format
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'); };
Ali mother icon library is imported and opened https://www.iconfont.cn/ , find the chart you like, download it, then import it, and add the values of iconfont and its corresponding style class in the corresponding place
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 ( <View className='header'> <Text className='header-text'>{ props.title }</Text> <Text onClick={() => goToPage('index')}> <AtIcon prefixClass='icon' className='iconfont header-reback' value='home' color='#6190e8'></AtIcon> </Text> </View> ) }
The use of redux is mainly used when sharing data on multiple pages. That's the core code
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; } }
The usage is as follows
@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: '' }) }
The packaging configuration of the development environment and the generation environment should be integrated into the egg service, so the publicPath and baseName of the production environment should be / public
module.exports = { env: { NODE_ENV: '"production"' }, defineConstants: {}, mini: {}, h5: { /** * If the compiled volume of the h5 side is too large, you can use the webpack bundle analyzer plug-in to analyze the packed volume. * Reference codes are as follows: * webpackChain (chain) { * chain.plugin('analyzer') * .use(require('webpack-bundle-analyzer').BundleAnalyzerPlugin, []) * } */ publicPath: '/public', router: { basename: '/public' } } };
The development environment name can be customized, such as:
module.exports = { env: { NODE_ENV: '"development"' }, defineConstants: {}, mini: {}, h5: { publicPath: '/', esnextModules: ['taro-ui'], router: { basename: '/religion' } } };
5.2 backend implementation
There is nothing else to say about the back-end. For details, please refer to the two articles I wrote before, or read the source code. Here we focus on preventing malicious registration of SMS verification code.
5.2.1 how to prevent malicious use of SMS verification code
This is mainly because it uses the internally implemented short message verification code interface (for home use), not some mature short message verification code interfaces on the market. Therefore, in the pre release stage, there has been an attack on Security (the server of the Contractor's head is attacked every day, but unfortunately I just hit it), and about 1W short messages have been maliciously used, Lost 8 pieces of Grandpa Mao. The following lessons are summarized, mainly from IP, sending frequency, and adding csrf Token to prevent malicious use.
That's about it.
Install class libraries relative to
"egg-ratelimiter": "^0.1.0", "egg-redis": "^2.4.0",
In config / plugin JS configuration
ratelimiter: { enable: true, package: 'egg-ratelimiter', }, redis: { enable: true, package: 'egg-redis', },
In config / config default. JS configuration
config.ratelimiter = { // db: {}, router: [ { path: '/sms/send', max: 5, time: '60s', message: 'Lying trough, you don't talk about martial virtue. You always ask for something!', }, ], }; config.redis = { client: { port: 6379, // Redis port host: '127.0.0.1', // Redis host password: null, db: 0, }, };
The effect is this

6, References
- TaroJS official website: https://taro-docs.jd.com/taro/docs/README
- ReactJS official website: https://reactjs.org/
- eggJS official website: https://eggjs.org/
7, Write at the end
That's all about UI. I should know more about UI sister to help take a look. I'll say goodbye to you here. Have you learned how to make forms by reading this article? Welcome to express your views below, and also welcome to communicate with the author!
GitHub project address: https://github.com/cnroadbridge/jingzhou-religion
Gitee project address: https://gitee.com/taoge2021/jingzhou-religion