九色国产,午夜在线视频,新黄色网址,九九色综合,天天做夜夜做久久做狠狠,天天躁夜夜躁狠狠躁2021a,久久不卡一区二区三区

打開APP
userphoto
未登錄

開通VIP,暢享免費(fèi)電子書等14項(xiàng)超值服

開通VIP
基于Vue2和Node.js的反欺詐系統(tǒng)設(shè)計(jì)與實(shí)現(xiàn)

最近包工頭喊農(nóng)民工小鄭搬磚,小鄭搬完磚后沉思片刻,決定寫篇小作文分享下,作為一個(gè)初學(xué)者的全棧項(xiàng)目,去學(xué)習(xí)它的搭建,到落地,再到部署維護(hù),是非常好的。

------題記

寫在前面

通過本文的學(xué)習(xí),你可以學(xué)到

  • vue2、element ui、vue-element-admin在前端的使用
  • 組件設(shè)計(jì)
  • echarts在前端中的使用
  • eggjs在后端node項(xiàng)目中的使用
  • docker一鍵化部署

需求分析

背景

近些年,網(wǎng)絡(luò)詐騙案頻發(fā),有假扮家里茶葉滯銷的茶花女,有假扮和男朋友分手去山區(qū)支教的女教師,有告知你中了非常6+1的大獎(jiǎng)主持人,有假扮越南那邊過來結(jié)婚的妹子,各類案件層出不窮。作為公民,我們應(yīng)該在社會(huì)主義新時(shí)代下積極學(xué)習(xí)組織上宣傳反詐騙知識(shí),提高防范意識(shí)。除此之外,對(duì)于種種詐騙案件,是網(wǎng)站的我們就應(yīng)該封其網(wǎng)站,是電話的我們就應(yīng)該封其電話,是銀行的我們就該封其銀行賬號(hào),是虛擬賬號(hào)的我們就應(yīng)該封其虛擬賬號(hào)。我相信,在我們的不懈努力之下,我們的社會(huì)將會(huì)更和諧更美好!

需求

長話短說,需求大致是這樣子的:有管理員、市局接警員、縣區(qū)局接警員、電話追查專員、網(wǎng)站追查專員、銀行追查專員、虛擬賬號(hào)專員這幾類角色, 相關(guān)的角色可以進(jìn)入相關(guān)的頁面進(jìn)行相關(guān)的操作,其中市局和管理員的警情錄入是不需要審核,直接派單下去,而縣區(qū)局的警情錄入需要進(jìn)行審核。當(dāng)審核通過后,會(huì)進(jìn)行相應(yīng)的派單。各類追查員將結(jié)果反饋給該警單。系統(tǒng)管理員這邊還可以進(jìn)行人員、機(jī)構(gòu)、警情類別,銀行卡、數(shù)據(jù)統(tǒng)計(jì)、導(dǎo)出等功能。希望是越快越好,越簡(jiǎn)單越好,領(lǐng)導(dǎo)要看的。

部分效果如圖:

技術(shù)預(yù)研

這個(gè)項(xiàng)目不是很大,復(fù)雜度也不是很高,并發(fā)量也不會(huì)太大,畢竟是部署在public police network下的。所以我這邊選用vue2,結(jié)合花褲衩大佬的vue-element-admin,前端這邊就差不多了,后端這邊用的是阿里開源的eggjs,因?yàn)樗褂闷饋砗芊奖?。?shù)據(jù)庫用的是mysql。部署這邊提供了兩套方案,一套是傳統(tǒng)的nginx、mysql、node、一個(gè)一個(gè)單獨(dú)安裝配置。另一種是docker部署的方式。

功能實(shí)現(xiàn)

前端

vue代碼規(guī)范

參見:https://www.yuque.com/ng46ql/tynary

vue工程目錄結(jié)構(gòu)

參見:https://panjiachen.gitee.io/vue-element-admin-site/zh/guide/#%E7%9B%AE%E5%BD%95%E7%BB%93%E6%9E%84

vue組件設(shè)計(jì)與封裝

這里我選了幾個(gè)有代表性的典型的組件來講解,我們先來看一張圖找找組件設(shè)計(jì)和封裝的感覺。

通過觀察我們發(fā)現(xiàn),在后臺(tái)管理界面中,蠻多的頁面是長這樣子的,我們不可能來一個(gè)頁面我們就再寫一次布局,這樣人都要搞沒掉。所以我們會(huì)有想法地把它封裝成一個(gè)container.vue,它主要包含頭部的標(biāo)題和右邊的新增按鈕、中間的過濾面板以及下方的表格。

container.vue是一個(gè)布局組件,它主要是框定了你一個(gè)頁面大致的布局, 在適當(dāng)?shù)奈恢茫覀兗尤氩宀?code style="font-size: 14px;overflow-wrap: break-word;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(239, 112, 96);">slot去表示這塊未知的區(qū)域,container.vue代碼實(shí)現(xiàn)如下:

<template>
<div>
<el-row class="top">
<el-col :span="24">
<el-row>
<el-col :span="12">
<div
v-if="title"
class="title"
>
{{ title }}
</div>
</el-col>
<el-col
:span="12"
class="btn-group"
>
<slot name="topExtra" />
<el-col />
</el-col>
</el-row>
</el-col>
<el-col :span="24">
<slot name="tab" />
</el-col>
</el-row>
<div class="content">
<slot name="content" />
</div>
</div>
</template>

<script>
export default {
name: 'CommonContainer',
props: {
title: { type: String, default: '' }
}
}
</script>

<style lang="scss" scoped>
.top {
padding: 15px;
min-height: 100px;
background-color: #fff;
box-shadow: 0 3px 5px -3px rgba(0, 0, 0, 0.1);
}

.title-box {
height: 100px;
line-height: 100px;
display: flex;
justify-content: space-between;
align-items: center;
}

.title {
font-size: 30px;
font-weight: 700;
}
.content {
margin: 20px 5px 0;
padding: 20px 10px;
background: #fff;
}

.btn-group {
text-align: right;
padding: 0 10px;
}
</style>

往下走,我們會(huì)想到怎么去設(shè)計(jì)表格這個(gè)組件,在設(shè)計(jì)這個(gè)組件的時(shí)候,我們需要清楚的知道,這個(gè)組件的輸入以及輸出是什么?比如說table-query.vue這個(gè)組件,從名字我們能夠看出,它是有查詢請(qǐng)求的,那么對(duì)于請(qǐng)求,很容易抽象出的一些東西是,請(qǐng)求地址,請(qǐng)求參數(shù),請(qǐng)求方法等等,所以這邊的props大致可以這么敲定。

props: {
    // 請(qǐng)求表格數(shù)據(jù)的url地址
    url: { typeStringrequiredtrue },
    // 默認(rèn)分頁數(shù)
    pageSize: { typeNumberdefault10 },
    // 是否展示序號(hào)
    index: { typeBooleandefaulttrue },
    // 表格的列的結(jié)構(gòu)
    columns: { typeArrayrequiredtrue },
    orgId: { typeStringrequiredfalsedefault'' },
    // 請(qǐng)求表格數(shù)據(jù)的方法
    method: { typeStringdefault'post' },
    // 請(qǐng)求表格數(shù)據(jù)的參數(shù)
    params: { typeObjectdefault() => ({}) },
    // 是否支持高亮選中
    isHighlightRow: { typeBooleandefaultfalse },
    // 是否顯示分頁
    isShowPagination: { typeBooleandefaulttrue },
    // 是否顯示迷你分頁
    isPaginationSizeSmall: { typeBooleandefaultfalse }
  },

這里的輸出,我們期望的是,當(dāng)用戶點(diǎn)擊詳情、查看、刪除的時(shí)候,我要知道這一行的具體數(shù)據(jù),那么大致可以這么敲定。

 handleClick(row, type, title) {
    this.$emit('click-action', row, type, title)
 },

這邊作為組件的數(shù)據(jù)通信已經(jīng)敲定了,剩下的也就是一些封裝請(qǐng)求的邏輯,頁面交互的邏輯,具體地可以看一下table-query.vue的實(shí)現(xiàn)

<template>
<div>
<el-table
ref="table"
border
:data="data"
:loading="isLoading"
:highlight-row="isHighlightRow"
:row-class-name="tableRowClassName"
>
<template v-for="column in columns">
<template v-if="column.key === 'actions'">
<el-table-column
:key="column.key"
align="center"
:width="column.width"
:label="column.title"
>
<template slot-scope="scope">
<el-button
v-for="action in filterOperate(
column.actions,
scope.row.btnList
)"
:key="action.type"
type="text"
size="small"
@click="handleClick(scope.row, action.type, action.title)"
>{{ action.title }}</el-button>
</template>
</el-table-column>
</template>
<template v-else-if="column.key === 'NO'">
<el-table-column
:key="column.key"
type="index"
width="80"
align="center"
:label="column.title"
/>
</template>
<template v-else>
<el-table-column
:key="column.key"
align="center"
:prop="column.key"
:width="column.width"
:label="column.title"
:formatter="column.formatter"
:show-overflow-tooltip="true"
/>
</template>
</template>
</el-table>
<el-row type="flex" justify="center" style="margin-top: 10px;">
<el-col :span="24">
<el-pagination
v-if="isShowPagination"
:small="true"
:total="total"
:background="true"
:page-sizes="pageSizeOptions"
:current-page="pagination.page"
:page-size="pagination.pageSize"
@current-change="changePage"
@size-change="changePageSize"
/>
</el-col>
</el-row>
</div>
</template>

<script>
import request from '@/utils/request'
import { getLength } from '@/utils/tools'

export default {
name: 'CommonTableQuery',
props: {
// 請(qǐng)求表格數(shù)據(jù)的url地址
url: { type: String, required: true },
// 默認(rèn)分頁數(shù)
pageSize: { type: Number, default: 10 },
// 是否展示序號(hào)
index: { type: Boolean, default: true },
// 表格的列的結(jié)構(gòu)
columns: { type: Array, required: true },
orgId: { type: String, required: false, default: '' },
// 請(qǐng)求表格數(shù)據(jù)的方法
method: { type: String, default: 'post' },
// 請(qǐng)求表格數(shù)據(jù)的參數(shù)
params: { type: Object, default: () => ({}) },
// 是否支持高亮選中
isHighlightRow: { type: Boolean, default: false },
// 是否顯示分頁
isShowPagination: { type: Boolean, default: true },
// 是否顯示迷你分頁
isPaginationSizeSmall: { type: Boolean, default: false }
},
data() {
return {
// 表格的行
data: [],
// 分頁總數(shù)
total: 0,
// 表格數(shù)據(jù)是否加載
isLoading: false,
// 是否全選
isSelectAll: false,
// 渲染后的列數(shù)據(jù)字段
renderColumns: [],
// 分頁
pagination: {
page: 1,
pageSize: this.pageSize
}
}
},
computed: {
// 是否有數(shù)據(jù)
hasData() {
return getLength(this.data) > 0
},
// 分頁條數(shù)
pageSizeOptions() {
return this.isPaginationSizeSmall ? [10, 20, 30] : [10, 20, 30, 50, 100]
}
},
created() {
this.getTableData()
},
methods: {
tableRowClassName({ row, rowIndex }) {
// if (rowIndex === 1) {
// return 'warning-row'
// } else if (rowIndex === 3) {
// return 'success-row'
// }
if (row.alarmNo && row.alarmNo.startsWith('FZYG')) {
return 'warning-row'
}
return ''
},
// 改變分頁
changePage(page) {
this.pagination.page = page
this.getTableData()
},
// 改變分頁大小
changePageSize(pageSize) {
this.pagination.pageSize = pageSize
this.getTableData()
},
// 獲取表格的數(shù)據(jù)
getTableData() {
if (!this.url) {
return
}
const {
url,
params,
orgId,
pagination: { page, pageSize },
isShowPagination,
method
} = this
this.isLoading = true
this.isSelectAll = false
const parameter = isShowPagination
? { page, pageSize, orgId, ...params }
: { orgId, ...params }
request({
method,
url,
[method === 'post' ? 'data' : 'params']: parameter
})
.then(res => {
const {
data: { list = [], total, page, pageSize }
} = res || {}
this.isLoading = false
this.data = list
if (this.isShowPagination) {
this.total = total === null ? 0 : total
this.pagination = {
page,
pageSize
}
}
})
.catch(err => {
this.isLoading = false
console.log(err)
})
},
// 手動(dòng)擋分頁查詢
query(page = 1, pageSize = 10) {
this.pagination = { page, pageSize }
this.getTableData()
},
handleClick(row, type, title) {
this.$emit('click-action', row, type, title)
},
filterOperate(actions, btnList) {
return actions.filter(action => btnList.includes(action.type))
}
}
}
</script>

<style>
.el-table .warning-row {
background: oldlace;
}
.el-table .success-row {
background: #f0f9eb;
}
.el-tooltip__popper {
max-width: 80%;
}
.el-tooltip__popper,
.el-tooltip__popper.is-dark {
background: #f5f5f5 !important;
color: #303133 !important;
}
</style>

element-table: https://element.eleme.cn/#/zh-CN/component/table

element-pagination: https://element.eleme.cn/#/zh-CN/component/pagination

文件上傳與下載,這個(gè)是點(diǎn)開警情、追查的相關(guān)頁面進(jìn)去的功能,大體上和樓上的表格類似,就是在原來的基礎(chǔ)上,去掉了分頁,加上了文件上傳的組件。


“DO NOT REPEAT"原則, 我們期望的是寫一次核心代碼就好,剩下的我們每次只需要在用到的地方引入table-file.vue 就好了,這樣子維護(hù)起來也方便,這就有個(gè)這個(gè)組件的想法。

我們還是想一下,對(duì)于文件我們不外乎有這些操作,上傳、下載、刪除、修改、預(yù)覽等等,所以這邊組件的輸入大致可以這么敲定。

 props: {
    canUpload: { typeBooleandefaulttrue },
    canDelete: { typeBooleandefaulttrue },
    canDownload: { typeBooleandefaulttrue },
    columns: { typeArraydefault() => [] },
    affix: { typeStringdefault'' }
  },

輸出的話,跟樓上的table-query.vue差不多

 handleClick(row, type, title) {
      this.$emit('click-action', row, type, title)
 },

具體地可以看下table-file.vue 的實(shí)現(xiàn)

<template>
<el-row>
<el-col v-if="canUpload" :span="24">
<el-upload
ref="upload"
:action="url"
drag
:limit="9"
name="affix"
:multiple="true"
:auto-upload="false"
:with-credentials="true"
:on-error="onError"
:file-list="fileList"
:on-remove="onRemove"
:on-change="onChange"
:on-exceed="onExceed"
:on-success="onSuccess"
:on-preview="onPreview"
:before-upload="beforeUpload"
:before-remove="beforeRemove"
:on-progress="onProgress"
:headers="headers"
>
<!-- <el-button size="small" type="primary">選擇文件</el-button> -->
<i class="el-icon-upload" />
<div class="el-upload__text">將文件拖到此處,或<em>選擇文件</em></div>
<div slot="tip" class="el-upload__tip">
文件格式不限,一次最多只能上傳9個(gè)文件,單個(gè)文件允許最大100MB
</div>
</el-upload>
</el-col>
<el-col v-if="canUpload" style="margin: 10px auto;">
<el-button
size="small"
type="primary"
@click="upload"
>確認(rèn)上傳</el-button>
</el-col>
<el-col :span="24">
<el-table
ref="table"
border
:data="data"
style="width: 100%; margin: 20px auto;"
>
<template v-for="column in mapColumns">
<template v-if="column.key === 'actions'">
<el-table-column
:key="column.key"
align="center"
:label="column.title"
>
<template slot-scope="scope">
<el-button
v-for="action in column.actions"
:key="action.type"
type="text"
size="small"
@click="handleClick(scope.row, action.type, action.title)"
>{{ action.title }}</el-button>
</template>
</el-table-column>
</template>
<template v-else-if="column.key === 'NO'">
<el-table-column
:key="column.key"
type="index"
width="80"
align="center"
:label="column.title"
/>
</template>
<template v-else>
<el-table-column
:key="column.key"
:prop="column.key"
align="center"
:label="column.title"
/>
</template>
</template>
</el-table>
</el-col>
</el-row>
</template>

<script>
import Cookies from 'js-cookie'
import { getByIds } from '@/api/file'
import { formatDate } from '@/utils/tools'

export default {
name: 'TableFile',
props: {
canUpload: { type: Boolean, default: true },
canDelete: { type: Boolean, default: true },
canDownload: { type: Boolean, default: true },
columns: { type: Array, default: () => [] },
affix: { type: String, default: '' }
},
data() {
return {
fileList: [],
data: [],
ids: [],
headers: {
'x-csrf-token': Cookies.get('csrfToken')
},
mapColumns: [],
url: process.env.VUE_APP_UPLOAD_API
}
},
watch: {
affix: {
async handler(newAffix) {
this.data = []
this.ids = []
if (newAffix) {
this.ids = newAffix.split(',').map(id => Number(id))
if (this.ids.length > 0) {
const { data } = await getByIds({ ids: this.ids })
this.data = data.map(item => {
const { createTime, ...rest } = item
return {
createTime: formatDate(
'YYYY-MM-DD HH:mm:ss',
createTime * 1000
),
...rest
}
})
}
}
},
immediate: true
},
canDelete: {
handler(newVal) {
if (newVal) {
this.mapColumns = JSON.parse(JSON.stringify(this.columns))
} else {
if (this.mapColumns[this.mapColumns.length - 1]) {
this.mapColumns[this.mapColumns.length - 1].actions = [
{
title: '下載',
type: 'download'
}
]
}
}
},
immediate: true
}
},
created() {
this.mapColumns = JSON.parse(JSON.stringify(this.columns))
if (!this.canDelete) {
if (this.mapColumns[this.mapColumns.length - 1]) {
this.mapColumns[this.mapColumns.length - 1].actions = [
{
title: '下載',
type: 'download'
}
]
}
}
},
methods: {
beforeUpload(file, fileList) {
console.log('beforeUpload: ', file, fileList)
},
onSuccess(response, file, fileList) {
const {
data: { id, createTime, ...rest }
} = response
this.data.push({
id,
createTime: formatDate('YYYY-MM-DD HH:mm:ss', createTime * 1000),
...rest
})
this.ids.push(id)
this.clear()
},
onError(err, file, fileList) {
console.log(err, file, fileList)
},
onPreview(file, fileList) {
console.log('onPreview: ', file, fileList)
},
beforeRemove(file, fileList) {
console.log('beforeRemove: ', file, fileList)
},
onExceed(files, fileList) {
console.log('onExceed: ', files, fileList)
// this.$message.warning(`當(dāng)前限制選擇 3 個(gè)文件,本次選擇了 ${files.length} 個(gè)文件,共選擇了 ${files.length + fileList.length} 個(gè)文件`)
},
onRemove(file, fileList) {
console.log('onRemove: ', file, fileList)
},
onChange(file, fileList) {
console.log('onChange: ', file, fileList)
},
onProgress(file, fileList) {
console.log('onProgress: ', file, fileList)
},
upload() {
this.$refs.upload.submit()
},
clear() {
this.$refs.upload.clearFiles()
this.fileList = []
},
handleClick(row, type, title) {
this.$emit('click-action', row, type, title)
},
deleteData(id) {
const index = this.ids.indexOf(id)
this.ids.splice(index, 1)
this.data.splice(index, 1)
}
}
}
</script>

<style scoped>
.center {
display: flex;
justify-content: center;
}
</style>

element-upload: https://element.eleme.cn/#/zh-CN/component/upload

功能實(shí)現(xiàn)-文件導(dǎo)出

數(shù)據(jù)的導(dǎo)出也是這種后臺(tái)管理系統(tǒng)比較常見的場(chǎng)景,這件事情可以前端做,也可以后端做。那么在這里結(jié)合xlsxfile-saver這兩個(gè)包,在src下新建一個(gè)excel文件夾, 然后新建一個(gè)js文件export2Excel.js

/* eslint-disable */
import { saveAs } from 'file-saver'
import XLSX from 'xlsx'

function generateArray(table{
  var out = [];
  var rows = table.querySelectorAll('tr');
  var ranges = [];
  for (var R = 0; R < rows.length; ++R) {
    var outRow = [];
    var row = rows[R];
    var columns = row.querySelectorAll('td');
    for (var C = 0; C < columns.length; ++C) {
      var cell = columns[C];
      var colspan = cell.getAttribute('colspan');
      var rowspan = cell.getAttribute('rowspan');
      var cellValue = cell.innerText;
      if (cellValue !== "" && cellValue == +cellValue) cellValue = +cellValue;

      //Skip ranges
      ranges.forEach(function (range{
        if (R >= range.s.r && R <= range.e.r && outRow.length >= range.s.c && outRow.length <= range.e.c) {
          for (var i = 0; i <= range.e.c - range.s.c; ++i) outRow.push(null);
        }
      });

      //Handle Row Span
      if (rowspan || colspan) {
        rowspan = rowspan || 1;
        colspan = colspan || 1;
        ranges.push({
          s: {
            r: R,
            c: outRow.length
          },
          e: {
            r: R + rowspan - 1,
            c: outRow.length + colspan - 1
          }
        });
      };

      //Handle Value
      outRow.push(cellValue !== "" ? cellValue : null);

      //Handle Colspan
      if (colspan)
        for (var k = 0; k < colspan - 1; ++k) outRow.push(null);
    }
    out.push(outRow);
  }
  return [out, ranges];
};

function datenum(v, date1904{
  if (date1904) v += 1462;
  var epoch = Date.parse(v);
  return (epoch - new Date(Date.UTC(18991130))) / (24 * 60 * 60 * 1000);
}

function sheet_from_array_of_arrays(data, opts{
  var ws = {};
  var range = {
    s: {
      c10000000,
      r10000000
    },
    e: {
      c0,
      r0
    }
  };
  for (var R = 0; R != data.length; ++R) {
    for (var C = 0; C != data[R].length; ++C) {
      if (range.s.r > R) range.s.r = R;
      if (range.s.c > C) range.s.c = C;
      if (range.e.r < R) range.e.r = R;
      if (range.e.c < C) range.e.c = C;
      var cell = {
        v: data[R][C]
      };
      if (cell.v == nullcontinue;
      var cell_ref = XLSX.utils.encode_cell({
        c: C,
        r: R
      });

      if (typeof cell.v === 'number') cell.t = 'n';
      else if (typeof cell.v === 'boolean') cell.t = 'b';
      else if (cell.v instanceof Date) {
        cell.t = 'n';
        cell.z = XLSX.SSF._table[14];
        cell.v = datenum(cell.v);
      } else cell.t = 's';

      ws[cell_ref] = cell;
    }
  }
  if (range.s.c < 10000000) ws['!ref'] = XLSX.utils.encode_range(range);
  return ws;
}

function Workbook({
  if (!(this instanceof Workbook)) return new Workbook();
  this.SheetNames = [];
  this.Sheets = {};
}

function s2ab(s{
  var buf = new ArrayBuffer(s.length);
  var view = new Uint8Array(buf);
  for (var i = 0; i != s.length; ++i) view[i] = s.charCodeAt(i) & 0xFF;
  return buf;
}

export function export_table_to_excel(id{
  var theTable = document.getElementById(id);
  var oo = generateArray(theTable);
  var ranges = oo[1];

  /* original data */
  var data = oo[0];
  var ws_name = "SheetJS";

  var wb = new Workbook(),
    ws = sheet_from_array_of_arrays(data);

  /* add ranges to worksheet */
  // ws['!cols'] = ['apple', 'banan'];
  ws['!merges'] = ranges;

  /* add worksheet to workbook */
  wb.SheetNames.push(ws_name);
  wb.Sheets[ws_name] = ws;

  var wbout = XLSX.write(wb, {
    bookType'xlsx',
    bookSSTfalse,
    type'binary'
  });

  saveAs(new Blob([s2ab(wbout)], {
    type"application/octet-stream"
  }), "test.xlsx")
}

export function export_json_to_excel({
  multiHeader = [],
  header,
  data,
  filename,
  merges = [],
  autoWidth = true,
  bookType = 'xlsx'
} = {}
{
  /* original data */
  filename = filename || 'excel-list'
  data = [...data]
  data.unshift(header);

  for (let i = multiHeader.length - 1; i > -1; i--) {
    data.unshift(multiHeader[i])
  }

  var ws_name = "SheetJS";
  var wb = new Workbook(),
    ws = sheet_from_array_of_arrays(data);

  if (merges.length > 0) {
    if (!ws['!merges']) ws['!merges'] = [];
    merges.forEach(item => {
      ws['!merges'].push(XLSX.utils.decode_range(item))
    })
  }

  if (autoWidth) {
    /*設(shè)置worksheet每列的最大寬度*/
    const colWidth = data.map(row => row.map(val => {
      /*先判斷是否為null/undefined*/
      if (val == null) {
        return {
          'wch'10
        };
      }
      /*再判斷是否為中文*/
      else if (val.toString().charCodeAt(0) > 255) {
        return {
          'wch': val.toString().length * 2
        };
      } else {
        return {
          'wch': val.toString().length
        };
      }
    }))
    /*以第一行為初始值*/
    let result = colWidth[0];
    for (let i = 1; i < colWidth.length; i++) {
      for (let j = 0; j < colWidth[i].length; j++) {
        if (result[j]['wch'] < colWidth[i][j]['wch']) {
          result[j]['wch'] = colWidth[i][j]['wch'];
        }
      }
    }
    ws['!cols'] = result;
  }

  /* add worksheet to workbook */
  wb.SheetNames.push(ws_name);
  wb.Sheets[ws_name] = ws;

  var wbout = XLSX.write(wb, {
    bookType: bookType,
    bookSSTfalse,
    type'binary'
  });
  saveAs(new Blob([s2ab(wbout)], {
    type"application/octet-stream"
  }), `${filename}.${bookType}`);
}

邏輯代碼如下

  downloadExcel() {
      this.$confirm('將導(dǎo)出為excel文件,確認(rèn)導(dǎo)出?''提示', {
        confirmButtonText'確定',
        cancelButtonText'取消',
        type'warning'
      })
        .then(() => {
          this.export2Excel()
        })
        .catch((e) => {
          this.$Message.error(e);
      })
    },
    // 數(shù)據(jù)寫入excel
    export2Excel() {
      import('@/excel/export2Excel').then(excel => {
        const tHeader = [
          '警情編號(hào)',
          '警情性質(zhì)',
          '受害人姓名',
          '受害人賬號(hào)',
          '嫌疑人賬號(hào)',
          '嫌疑人電話',
          '涉案總金額',
          '案發(fā)時(shí)間',
          '警情狀態(tài)'
        ] // 導(dǎo)出的excel的表頭字段
        const filterVal = [
          'alarmNo',
          'alarmProp',
          'informantName',
          'informantBankAccount',
          'suspectsAccount',
          'suspectsMobile',
          'fraudAmount',
          'crimeTime',
          'alarmStatus'
        ] // 對(duì)象屬性,對(duì)應(yīng)于tHeader
        const list = this.$refs.inputTable.data
        const data = this.formatJson(filterVal, list)
        excel.export_json_to_excel({
          header: tHeader,
          data,
          filenamethis.filename,
          autoWidththis.autoWidth,
          bookTypethis.bookType
        })
        this.downloadLoading = false
      })
    },
    // 格式轉(zhuǎn)換,直接復(fù)制即可
    formatJson(filterVal, jsonData) {
      return jsonData.map(v =>
        filterVal.map(j => {
          if (j === 'crimeTime') {
            return formatDate('YYYY-MM-DD HH:mm:ss', v[j] * 1000)
          } else if (j === 'alarmProp') {
            return this.alarmPropOptionsArr[v[j]]
          } else if (j === 'alarmStatus') {
            return this.alarmStatusOptionsArr[v[j]]
          } else {
            return v[j]
          }
        })
      )
    }

參見:https://panjiachen.gitee.io/vue-element-admin-site/zh/feature/component/excel.html

功能實(shí)現(xiàn)-數(shù)據(jù)統(tǒng)計(jì)與展示

單純的數(shù)據(jù)只有存儲(chǔ)的價(jià)值,而對(duì)存儲(chǔ)下來的數(shù)據(jù)進(jìn)行相應(yīng)的分析,并加以圖表的形式輸出,可以更直觀地看到數(shù)據(jù)的變化,體現(xiàn)數(shù)據(jù)的價(jià)值,實(shí)現(xiàn)新生代農(nóng)民工的勞動(dòng)價(jià)值。這邊結(jié)合echarts對(duì)某一個(gè)時(shí)間段的警情中各部分追查的占比進(jìn)行了一個(gè)統(tǒng)計(jì),除此之外,對(duì)該時(shí)間段的每月的止付金額進(jìn)行了一個(gè)統(tǒng)計(jì),最終結(jié)合扇形和柱形對(duì)其進(jìn)行展示。

翻一翻npm包,筆者物色到了兩位包包可以做這件事,考慮到針對(duì)本項(xiàng)目對(duì)于圖表的需求量不是特別大,我也懶得看兩套API,就還是用了echarts。

vue-echarts:  https://www.npmjs.com/package/vue-echarts

echarts:  https://www.npmjs.com/package/echarts

我們會(huì)有一個(gè)數(shù)據(jù)接口,前端帶上相關(guān)的請(qǐng)求參數(shù)通過請(qǐng)求/prod-api/statistics/calculate這個(gè)接口就能夠拿到后端的從數(shù)據(jù)庫處理出來的相關(guān)數(shù)據(jù),這里因?yàn)榍昂蠖硕际俏覍懙?,所以我制定的?guī)則就是,所有的計(jì)算都有后端去完成,前端只負(fù)責(zé)展示,并且約定了相關(guān)的參數(shù)格式。這樣做的一個(gè)好處是,省去了前端這邊對(duì)數(shù)據(jù)的封裝處理。返回的格式如下:

{
    "status"200,
    "message""success",
    "data": {
        "pieData": [
            {
                "name""銀行查控",
                "count"13
            },
            {
                "name""電話查控",
                "count"10
            },
            {
                "name""虛擬賬號(hào)查控",
                "count"3
            },
            {
                "name""網(wǎng)站查控",
                "count"5
            }
        ],
        "barData": [
            {
                "name""2021年1月",
                "amount"0
            },
            {
                "name""2021年2月",
                "amount"0
            },
            {
                "name""2021年3月",
                "amount"0
            },
            {
                "name""2021年4月",
                "amount"0
            },
            {
                "name""2021年5月",
                "amount"0
            },
            {
                "name""2021年6月",
                "amount"0
            },
            {
                "name""2021年7月",
                "amount"0
            },
            {
                "name""2021年8月",
                "amount"1311601
            }
        ],
        "totalAmount"1311601
    }
}

這里以畫餅圖和柱形圖為例,其他的也是類似的,可以參考https://echarts.apache.org/examples/zh/index.html

公共部分

npm i echarts -S安裝echarts的npm包,然后在相應(yīng)的文件引入它。

import echarts from 'echarts'
畫餅圖

在template中我們搞一個(gè)餅圖的div

 <div ref="pieChart" class="chart" />

在vue的方法里面,我們定義一個(gè)畫餅的方法,這里定義的輸入就是請(qǐng)求后端返回的數(shù)據(jù),其他的看echarts的配置項(xiàng),這邊都配好了(如果寫成單個(gè)組件,需要根據(jù)業(yè)務(wù)考慮相關(guān)的配置項(xiàng),目前這邊就care數(shù)據(jù)項(xiàng))。邏輯是這樣子的,定義了一個(gè)基于數(shù)據(jù)項(xiàng)變動(dòng)的配置項(xiàng)options,然后當(dāng)執(zhí)行drawPie方法的時(shí)候,如果沒有初始化echarts,那么我們這邊就初始化一個(gè)echarts的餅,如果有,那么我們就只有更新相關(guān)的options就好了。

drawPie(source) {
      const options = {
        title: {
          text'各追查類型占比統(tǒng)計(jì)'
        },
        tooltip: {
          trigger'item',
          formatter' : (c9ozetgccsir%)'
        },
        legend: {
          orient'vertical',
          x'left',
          y'bottom',
          data: ['銀行查控''電話查控''虛擬賬號(hào)查控''網(wǎng)站查控']
        },
        dataset: {
          source
        },
        series: {
          type'pie',
          label: {
            position'outer',
            alignTo'edge',
            margin10,
            formatter'{@name}: {@count} (c9ozetgccsir%)'
          },
          encode: {
            itemName'name',
            value'count'
          }
        }
      }
      if (this.pieChart) {
        this.pieChart.setOption(options, true)
      } else {
        this.pieChart = echarts.init(this.$refs.pieChart)
        this.pieChart.setOption(options, true)
      }
    }
畫柱形圖

跟樓上的類似的,畫柱子如樓下所示:

 drawBar(source) {
      const options = {
        title: {
          text`各月份止付金額之和統(tǒng)計(jì), 合計(jì): ${this.totalAmount}元`
        },
        dataset: {
          source
        },
        xAxis: {
          type'category',
          name'時(shí)間'
        },
        yAxis: [
          {
            type'value',
            name'止付金額'
          }
        ],
        series: [
          {
            type'bar',
            encode: {
              x'name',
              y'amount'
            },
            label: {
              normal: {
                showtrue,
                position'top'
              }
            }
          }
        ]
      }
      if (this.barChart) {
        this.barChart.setOption(options, true)
      } else {
        this.barChart = echarts.init(this.$refs.barChart)
        this.barChart.setOption(options, true)
      }
    },

備注:考慮到需求量不大,這里筆者是為了趕進(jìn)度偷懶寫成這樣的,學(xué)習(xí)的話,建議封裝成一個(gè)個(gè)組件,例如pie.vue,bar.vue這樣子去搞。

功能實(shí)現(xiàn)-頁面權(quán)限控制和頁面權(quán)限的按鈕權(quán)限粒度控制

因?yàn)檫@個(gè)項(xiàng)目涉及到多個(gè)角色,這就涉及到對(duì)多個(gè)角色的頁面控制了,每個(gè)角色分配的頁面權(quán)限是不一樣的,第二個(gè)就是進(jìn)入到頁面后,針對(duì)某一條記錄,該登錄用戶按鈕的權(quán)限控制。

頁面權(quán)限控制

頁面的權(quán)限這邊有兩種做法,分別是控制權(quán)在前端,和控制權(quán)在后端兩種,在前端的話是通過獲取用戶信息的角色,根據(jù)角色去匹配,匹配中了就加到路由里面。在后端的話,就是登錄的時(shí)候后端就把相應(yīng)的路由返回給你,前端這邊注冊(cè)路由。

借著vue-element-admin的東風(fēng),筆者這邊是將控制權(quán)放在前端,在路由的meta中加入roles角色去做頁面的權(quán)限控制的。

參見 vue-element-admin - 路由和側(cè)邊欄:https://panjiachen.gitee.io/vue-element-admin-site/zh/guide/essentials/router-and-nav.html#%E9%85%8D%E7%BD%AE%E9%A1%B9

參見 vue-element-admin - 權(quán)限驗(yàn)證:https://panjiachen.gitee.io/vue-element-admin-site/zh/guide/essentials/permission.html#%E9%80%BB%E8%BE%91%E4%BF%AE%E6%94%B9

按鈕權(quán)限控制

首先我們來分析下,針對(duì)我們這個(gè)系統(tǒng),不外乎刪除、修改、詳情、審核、追查等等按鈕權(quán)限,不是特別多,所以我們可以用detail、modifydelete、audit、check等去表示這些按鈕,后端在service層進(jìn)行相關(guān)業(yè)務(wù)處理,把它們這些包到一個(gè)數(shù)組btnList里面返回給前端,跟前端這邊做對(duì)比,如果命中那么我們就展示按鈕。

核心代碼如下:

template

  <template v-if="column.key === 'actions'">
<el-table-column
:key="column.key"
align="center"
:width="column.width"
:label="column.title"
>
<template slot-scope="scope">
<el-button
v-for="action in filterOperate(
column.actions,
scope.row.btnList
)"
:key="action.type"
type="text"
size="small"
@click="handleClick(scope.row, action.type, action.title)"
>{{ action.title }}</el-button>
</template>
</el-table-column>
</template>
filterOperate(actions, btnList) {
 return actions.filter(action => btnList.includes(action.type))
}

那么我們就可以這么使用了

columns: [
 ......
  {
    title'操作',
    key'actions',
    align'center',
    actions: [
      {
        title'詳情',
        type'detail'
      },
      {
        title'修改',
        type'modify'
      }
    ]
  }
]

關(guān)于權(quán)限校驗(yàn)這塊,筆者所在的供應(yīng)鏈金融團(tuán)隊(duì)是這么去實(shí)現(xiàn)的,在保理業(yè)務(wù)中,會(huì)有很多個(gè)部門,比如市場(chǎng)部、財(cái)務(wù)部、風(fēng)控部、董事會(huì)等等。每個(gè)部門里又有經(jīng)辦、審核、復(fù)核等等角色。所以在處理這類業(yè)務(wù)中的權(quán)限控制,需要將用戶身上綁定一個(gè)按鈕權(quán)限,比如說他是市場(chǎng)的經(jīng)辦角色,那么他就可以綁定市場(chǎng)經(jīng)辦這個(gè)角色的按鈕碼子上。前端這邊除了要在我們樓上的基礎(chǔ)上對(duì)列表返回的做對(duì)比之外,還有對(duì)用戶的做進(jìn)一步對(duì)比。這里的按鈕也不能夠像上面一樣detail、modify這樣去寫,因?yàn)榻巧嗔嗣總€(gè)角色這么叫不好,更科學(xué)的應(yīng)該是,整理成一份excel表,然后按照相應(yīng)的按鈕權(quán)限去配置相應(yīng)的code(比如說 20001, 20002),然后根據(jù)這個(gè)去處理業(yè)務(wù)。

后端

eggjs中的三層模型(model-service-controller)

model層是對(duì)數(shù)據(jù)庫的相關(guān)表進(jìn)行相應(yīng)的映射和CRUD,service層是處理相關(guān)的業(yè)務(wù)邏輯,controller層是為相關(guān)的業(yè)務(wù)邏輯暴露接口。這三者層序漸進(jìn),一環(huán)扣一環(huán)。

Model
一些約定
  • 原則上,不允許對(duì)Model層SQL語句返回的結(jié)果進(jìn)行相關(guān)操作,返回什么就是什么。
  • 統(tǒng)一下數(shù)據(jù)返回的格式
    • 語法錯(cuò)誤 null
    • 查不到數(shù)據(jù) false
    • 查到數(shù)據(jù) JSON | Number
  • 統(tǒng)一下model層文件類的通用方法
    • add:新增
    • set:更新
    • del:刪除(本系統(tǒng)由于數(shù)據(jù)需要,所以不會(huì)真的刪除這條數(shù)據(jù),而是取一個(gè)isDelete字段去軟刪除它)
    • get: 獲取單條數(shù)據(jù),getById可簡(jiǎn)寫成get, 若有查詢條件, 按getByCondition
    • getAll: 獲取多條記錄,若有查詢條件 按getAllByCondition
    • getAllLimit: 分頁獲取 若有查詢條件 按getAllLimitByCondition
    • has: 若有查詢條件, 按hasAttributes

目前本系統(tǒng)業(yè)務(wù)就用到這么多,其他的參見sequelize文檔:http://sequelize.org/

這樣做的好處是,一些概念和語義更加清晰了,比如有個(gè)user.js,里面用add表示新增還是addUser表示新增好,我認(rèn)為是前者,在user.js里面, 除了新增user用戶,難不成還有別的新增,還能新增個(gè)鬼啊。除此之外,還方便了新生代農(nóng)民工的復(fù)制粘貼,提高編碼效率。

抄表字段真的好累啊

試想一下這樣一個(gè)場(chǎng)景,這個(gè)數(shù)據(jù)庫有一百張表,每張表有100個(gè)字段,難道你真的要人肉去一個(gè)一個(gè)敲出來對(duì)應(yīng)的數(shù)據(jù)庫映射嗎?那要敲到什么時(shí)候啊,人都快搞沒了,我們可是新生代農(nóng)民工唉,當(dāng)然要跟上時(shí)代。這里介紹一下egg-sequelize-auto, 它可以快速的將數(shù)據(jù)庫的字段映射到你的代碼中,減少很多工作量。

安裝

npm i egg-sequelize-auto -g
npm i mysql2 -g

使用

egg-sequelize-auto -h 'your ip' -d 'your database' -u 'db user' -x 'db password' -e mysql -o 'project model path' -t 'table name'                                                 

egg-sequelize-auto:https://www.npmjs.com/package/egg-sequelize-auto

sequelize連表查詢的應(yīng)用

在表的關(guān)系中,有一對(duì)一,一對(duì)多,多對(duì)多。本系統(tǒng)一對(duì)多用的比較多,這里就以銀行卡結(jié)合銀行的的連表做個(gè)演示。

主要是三個(gè)地方,一個(gè)是引入相關(guān)表的Model, 第二個(gè)是字段初始化,第三個(gè)是通過associate方法建立聯(lián)系,閹割后的示例代碼如下:

'use strict';

const OrganizationModel = require('./organization');

module.exports = app => {
  const { logger, Sequelize, utils } = app;
  const { DataTypes, Model, Op } = Sequelize;
  class BankcardModel extends Model {
    static associate() {
      const { Organization } = app.model;
      BankcardModel.belongsTo(Organization, {
        foreignKey'bankId',
        targetKey'id',
        as'bank',
      });
    }

    static async getAllLimit(name, prefix, bankId, { page = 0, limit = 10 }) {
      let where = {};
      if (name) {
        where = { name: { [Op.like]: `%${name}%` } };
      }
      if (prefix) {
        where.prefix = { [Op.like]: `%${prefix}%` };
      }
      if (bankId) {
        where.bankId = bankId;
      }
      where.isDelete = 0;
      try {
        const offset = page < 1 ? 1 : (page - 1) * limit;
        const total = await this.count({ where });
        const last = Math.ceil(total / limit);
        const list =
          total === 0
            ? []
            : await this.findAll({
                rawtrue,
                where,
                order: [
                  ['createTime''DESC'],
                  ['updateTime''DESC'],
                ],
                offset,
                limit,
                attributes: [
                  'id',
                  'name',
                  'prefix',
                  'bankId',
                  [Sequelize.col('bank.name'), 'bankName'],
                ],
                include: {
                  model: app.model.Organization,
                  as'bank',
                  attributes: [],
                },
              });
        logger.info(this.getAllLimit, page, limit, where, list);
        return {
          page,
          pageSize: limit,
          list,
          total,
          last,
        };
      } catch (e) {
        logger.error(e);
        return false;
      }
    }
  }

  BankcardModel.init(
    {
      id: {
        type: DataTypes.UUID,
        defaultValue() {
          return utils.generator.generateUUID();
        },
        allowNullfalse,
        primaryKeytrue,
      },
      name: {
        type: DataTypes.STRING(255),
        allowNulltrue,
      },
      prefix: {
        type: DataTypes.STRING(255),
        allowNulltrue,
      },
      bankId: {
        type: DataTypes.STRING(255),
        allowNullfalse,
        references: {
          model: OrganizationModel,
          key'id',
        },
      },
      isDelete: {
        type: DataTypes.INTEGER(1),
        allowNulltrue,
        defaultValue0,
      },
      createTime: {
        type: DataTypes.INTEGER(10),
        allowNulltrue,
      },
      updateTime: {
        type: DataTypes.INTEGER(10),
        allowNulltrue,
      },
    },
    {
      sequelize: app.model,
      tableName't_bankcard',
    }
  );

  return BankcardModel;
};

sequelize中的表關(guān)系:https://sequelize.org/master/manual/assocs.html

Service

這里就是引入相關(guān)的model層寫好的,然后根據(jù)業(yè)務(wù)邏輯去調(diào)用下,還是以銀行卡為例

'use strict';

const { Service } = require('egg');

class BankcardService extends Service {
  constructor(ctx) {
    super(ctx);
    this.Bankcard = this.ctx.model.Bankcard;
  }

  async add(name, prefix, bankId) {
    const { ctx, Bankcard } = this;
    let result = await Bankcard.hasPrefix(prefix);
    if (result) {
      ctx.throw('卡號(hào)前綴已存在');
    }
    result = await Bankcard.add(name, prefix, bankId);
    if (!result) {
      ctx.throw('添加卡號(hào)失敗');
    }
    return result;
  }

  async getAllLimit(name, prefix, bankId, page, limit) {
    const { ctx, Bankcard } = this;
    const result = await Bankcard.getAllLimit(name, prefix, bankId, {
      page,
      limit,
    });
    if (!result) {
      ctx.throw('暫無數(shù)據(jù)');
    }
    return result;
  }

  async set(id, name, prefix, bankId, isDelete) {
    const { ctx, Bankcard } = this;
    const result = await Bankcard.set(id, name, prefix, bankId, isDelete);
    if (result === null) {
      ctx.throw('更新失敗');
    }
    return result;
  }
}

module.exports = BankcardService;

Controller
restful API接口

只要在相應(yīng)的controller層定義相關(guān)的方法,egg程序就能夠根據(jù)restful api去解析。

MethodPathRoute NameController.Action
GET/postspostsapp.controllers.posts.index
GET/posts/newnew_postapp.controllers.posts.new
GET/posts/:idpostapp.controllers.posts.show
GET/posts/:id/editedit_postapp.controllers.posts.edit
POST/postspostsapp.controllers.posts.create
PUT/posts/:idpostapp.controllers.posts.update
DELETE/posts/:idpostapp.controllers.posts.destroy

參見:https://eggjs.org/zh-cn/basics/router.html

非restful API接口

這里主要是針對(duì)于樓上的情況,進(jìn)行一個(gè)補(bǔ)充,比如說用戶,除了這些,他還有登錄,登出等等操作,那這個(gè)就需要單獨(dú)在router中制定了, 這里筆者封裝了一個(gè)resource方法,來解析restful api的函數(shù)接口,具體如下:

'use strict';

/**
 * @param {Egg.Application} app - egg application
 */

module.exports = app => {
  const { router, controller } = app;
  router.get('/', controller.home.index);
  router.post('/user/login', controller.user.login);
  router.post('/user/logout', controller.user.logout);
  router.post('/user/info', controller.user.getUserInfo);
  router.post('/file/upload', controller.file.upload);
  router.post('/file/getall', controller.file.getAllByIds);
  router.post('/organization/by-type', controller.organization.getAllByType);
  router.post('/statistics/calculate', controller.statistics.calculate);

  function resource(path{
    const pathArr = path.split('/');

    // 刪掉第一個(gè)空白的
    pathArr.shift();

    let controllers = controller;
    for (const val of pathArr) {
      controllers = controllers[val];
    }

    router.resources(path, path, controllers);
  }

  resource('/alarm');
  resource('/bank');
  resource('/bankcard');
  resource('/mobile');
  resource('/organization');
  resource('/user');
  resource('/virtual');
  resource('/website');
  resource('/file');
  resource('/alarmCategory');
};

這里還是以銀行卡為例

'use strict';

const { Controller } = require('egg');

class BankCardController extends Controller {
  async index() {
    const { ctx, service } = this;
    const { name, prefix, bankId, page, pageSize } = ctx.request.query;
    const { list, ...rest } = await service.bankcard.getAllLimit(
      name,
      prefix,
      bankId,
      Number(page),
      Number(pageSize)
    );
    const data = list.map(item => {
      const { role } = ctx.session.userinfo;
      let btnList = [];
      if (role === 'admin') {
        btnList = ['detail''modify''delete'];
      }
      return {
        btnList,
        ...item,
      };
    });
    ctx.success({ list: data, ...rest });
  }

  async create() {
    const { ctx, service } = this;
    const { name, prefix, bankId } = ctx.request.body;
    ctx.validate(
      {
        name: { type'string'requiredtrue },
        prefix: { type'string'requiredtrue },
        bankId: { type'string'requiredtrue },
      },
      { name, prefix, bankId }
    );
    const result = await service.bankcard.add(name, prefix, bankId);
    ctx.success(result);
  }

  // async destory() {
  //   const { ctx, service } = this;
  //   const { method } = ctx;
  //   this.ctx.body = '刪除';
  // }

  async update() {
    const { ctx, service } = this;
    const { id } = ctx.params;
    const { name, prefix, bankId, isDelete } = ctx.request.body;
    const result = await service.bankcard.set(
      id,
      name,
      prefix,
      bankId,
      isDelete
    );
    ctx.success(result);
  }

  async show() {
    const { ctx, service } = this;
    const { method } = ctx;
    this.ctx.body = '查詢';
  }

  async new() {
    const { ctx, service } = this;
    const { method } = ctx;
    this.ctx.body = '創(chuàng)建頁面';
  }

  async edit() {
    const { ctx, service } = this;
    const { method } = ctx;
    this.ctx.body = '修改頁面';
  }
}

module.exports = BankCardController;

至此,打通這樣一個(gè)從model到service再到controller的流程,

eggjs中的定時(shí)任務(wù)schedule

原系統(tǒng)是接入了第三方的數(shù)據(jù)源去定時(shí)讀取更新數(shù)據(jù),再將數(shù)據(jù)清洗更新到我們自己的t_alarm表,一些原因這里我不方便做演示,所以筆者又新建了一張?zhí)鞖獗?,來向大家介紹eggjs中的定時(shí)任務(wù)。

在這里,我相中了萬年歷的接口,準(zhǔn)備嫖一嫖給大家做一個(gè)演示的例子,它返回的數(shù)據(jù)格式如下

{
  "data": {
    "yesterday": {
      "date""19日星期四",
      "high""高溫 33℃",
      "fx""東風(fēng)",
      "low""低溫 24℃",
      "fl""<![CDATA[1級(jí)]]>",
      "type""小雨"
    },
    "city""杭州",
    "forecast": [
      {
        "date""20日星期五",
        "high""高溫 34℃",
        "fengli""<![CDATA[2級(jí)]]>",
        "low""低溫 25℃",
        "fengxiang""西南風(fēng)",
        "type""小雨"
      },
      {
        "date""21日星期六",
        "high""高溫 33℃",
        "fengli""<![CDATA[2級(jí)]]>",
        "low""低溫 25℃",
        "fengxiang""西南風(fēng)",
        "type""中雨"
      },
      {
        "date""22日星期天",
        "high""高溫 33℃",
        "fengli""<![CDATA[1級(jí)]]>",
        "low""低溫 26℃",
        "fengxiang""東風(fēng)",
        "type""小雨"
      },
      {
        "date""23日星期一",
        "high""高溫 32℃",
        "fengli""<![CDATA[1級(jí)]]>",
        "low""低溫 26℃",
        "fengxiang""南風(fēng)",
        "type""中雨"
      },
      {
        "date""24日星期二",
        "high""高溫 33℃",
        "fengli""<![CDATA[1級(jí)]]>",
        "low""低溫 25℃",
        "fengxiang""西南風(fēng)",
        "type""小雨"
      }
    ],
    "ganmao""感冒低發(fā)期,天氣舒適,請(qǐng)注意多吃蔬菜水果,多喝水哦。",
    "wendu""31"
  },
  "status"1000,
  "desc""OK"
}

我分別選取了天朝的一線城市和一些地域性比較強(qiáng)的城市去搞數(shù)據(jù)(等跑個(gè)兩三個(gè)月,存了點(diǎn)數(shù)據(jù),俺又可以寫一篇基于echarts的天氣可視化展示了,233333333),最后的效果如圖

首先我們創(chuàng)建一個(gè)類,繼承了egg的Subscription類, 然后有一個(gè)schedule方法

  static get schedule() {
    return {
      interval'12h',
      type'worker',
    };
  }

interval表示時(shí)間間隔,從樓上可以看出是每12小時(shí)去執(zhí)行一次,type表示執(zhí)行這個(gè)定時(shí)任務(wù)的進(jìn)程,可以選allworker,這邊表示只在一個(gè)worker進(jìn)程中執(zhí)行該任務(wù)。

核心的業(yè)務(wù)邏輯,寫在subscribe方法中,這里表示去請(qǐng)求萬年歷的數(shù)據(jù),然后進(jìn)行相應(yīng)的數(shù)據(jù)清洗

async subscribe() {
    try {
      const result = [];
      for (const city of CITYS) {
        result.push(this.fetchData(city));
      }
      await Promise.all(result);
    } catch (e) {
      this.ctx.app.logger.error(e);
    }
  }

最終實(shí)現(xiàn)代碼如下:

const { Subscription } = require('egg');

const URL_PREFIX = 'http://wthrcdn.etouch.cn/weather_mini?city=';
const CITYS = [
  '杭州',
  '北京',
  '南京',
  '上海',
  '廣州',
  '深圳',
  '成都',
  '武漢',
  '鄭州',
  '哈爾濱',
  '海口',
  '三亞',
  '烏魯木齊',
  '呼和浩特',
  '拉薩',
  '大理',
  '麗江',
];
const DAY_TIMESTAMP = 86400000;

class WeatherSchedule extends Subscription {
  static get schedule() {
    return {
      interval'12h',
      type'worker',
    };
  }

  async refreshWeatherData(
    date,
    high,
    low,
    wendu = null,
    fengli,
    fengxiang,
    type,
    ganmao = null,
    city,
    weatherDate
  ) {
    const weather = await this.service.weather.getWeather(weatherDate, city);
    if (weather) {
      const { id, wendu: oldWendu, ganmao: oldGanmao } = weather;
      const newWendu = oldWendu || wendu;
      const newGanmao = oldGanmao || ganmao;
      await this.service.weather.set(
        id,
        date,
        high,
        low,
        newWendu,
        fengli,
        fengxiang,
        type,
        newGanmao,
        city,
        weatherDate
      );
    } else {
      await this.service.weather.add(
        date,
        high,
        low,
        wendu,
        fengli,
        fengxiang,
        type,
        ganmao,
        city,
        weatherDate
      );
    }
  }

  async fetchData(queryCity) {
    const res = await this.ctx.curl(`${URL_PREFIX}${queryCity}`, {
      dataType'json',
    });
    const {
      data: { city, forecast = [], ganmao, wendu },
    } = res.data;
    const result = [];
    const now = this.ctx.app.utils.date.now() * 1000;
    for (let i = 0; i < forecast.length; i++) {
      const { date, high, fengli, low, fengxiang, type } = forecast[i];
      const weatherDate = this.ctx.app.utils.date.format2Date(
        now + i * DAY_TIMESTAMP
      );
      if (i === 0) {
        result.push(
          this.refreshWeatherData(
            date,
            high,
            low,
            wendu,
            fengli,
            fengxiang,
            type,
            ganmao,
            city,
            weatherDate
          )
        );
      } else {
        result.push(
          this.refreshWeatherData(
            date,
            high,
            low,
            null,
            fengli,
            fengxiang,
            type,
            null,
            city,
            weatherDate
          )
        );
      }
    }
    await Promise.all(result);
  }

  async subscribe() {
    try {
      const result = [];
      for (const city of CITYS) {
        result.push(this.fetchData(city));
      }
      await Promise.all(result);
    } catch (e) {
      this.ctx.app.logger.error(e);
    }
  }
}

module.exports = WeatherSchedule;

egg中的schedule:https://eggjs.org/zh-cn/basics/schedule.html

eggjs中的配置項(xiàng)config

eggjs提供了根據(jù)開發(fā)、生產(chǎn)、測(cè)試環(huán)境的配置文件,具體的以config.env.js表示,因?yàn)轫?xiàng)目不是很復(fù)雜,而且都是我一個(gè)人寫的,這里就簡(jiǎn)單點(diǎn)都寫在了一個(gè)文件config.default.js里面。

在這里面可以對(duì)中間件、安全、數(shù)據(jù)庫、日志、文件上傳、session、loader等進(jìn)行配置,具體的如下:

/* eslint valid-jsdoc: "off" */

'use strict';

/**
 * @param {Egg.EggAppInfo} appInfo app info
 */

module.exports = appInfo => {
  /**
   * built-in config
   * @type {Egg.EggAppConfig}
   * */

  const config = (exports = {});

  // use for cookie sign key, should change to your own and keep security
  config.keys = `${appInfo.name}_ataola`;

  // add your middleware config here
  config.middleware = ['cost''errorHandler'];

  // add your user config here
  const userConfig = {
    myAppName'egg',
  };

  config.security = {
    xframe: {
      enabletrue,
    },
    csrf: {
      enabletrue,
      ignore'/user/login',
      // queryName: '_csrf',
      // bodyName: '_csrf',
      headerName'x-csrf-token',
    },
    domainWhiteList: [
      'http://localhost:7001',
      'http://127.0.0.1:7001',
      'http://localhost:9528',
      'http://localhost',
      'http://127.0.0.1',
    ],
  };

  // https://github.com/eggjs/egg-sequelize
  config.sequelize = {
    dialect'mysql'// support: mysql, mariadb, postgres, mssql
    database'anti-fraud',
    host'hzga-mysql',
    port3306,
    username'root',
    password'ataola',
    // delegate: 'myModel', // load all models to `app[delegate]` and `ctx[delegate]`, default to `model`
    // baseDir: 'my_model', // load all files in `app/${baseDir}` as models, default to `model`
    // exclude: 'index.js', // ignore `app/${baseDir}/index.js` when load models, support glob and array
    // more sequelize options
    define: {
      timestampsfalse,
      underscoredfalse,
    },
  };

  exports.multipart = {
    mode'file',
    fileSize'100mb',
    whitelist: [
      // images
      '.jpg',
      '.jpeg'// image/jpeg
      '.png'// image/png, image/x-png
      '.gif'// image/gif
      '.bmp'// image/bmp
      '.wbmp'// image/vnd.wap.wbmp
      '.webp',
      '.tif',
      '.psd',
      // text
      '.svg',
      '.js',
      '.jsx',
      '.json',
      '.css',
      '.less',
      '.html',
      '.htm',
      '.xml',
      '.xlsx',
      '.xls',
      '.doc',
      '.docx',
      '.ppt',
      '.pptx',
      '.pdf',
      // tar
      '.zip',
      '.rar',
      '.gz',
      '.tgz',
      '.gzip',
      // video
      '.mp3',
      '.mp4',
      '.avi',
    ],
  };

  config.session = {
    key'SESSION_ID'// 設(shè)置session key,cookie里面的key
    maxAge24 * 3600 * 1000// 1 天
    httpOnlytrue// 是否允許js訪問session,默認(rèn)為true,表示不允許js訪問
    encrypttrue// 是否加密
    renewtrue// 重置session的過期時(shí)間,延長session過期時(shí)間
  };

  config.logger = {
    level'NONE',
    consoleLevel'DEBUG',
    disableConsoleAfterReadyfalse,
  };

  config.errorHandler = {
    match'/',
  };

  config.customLoader = {
    enum: {
      directory'app/enum',
      inject'app',
      loadunittrue,
    },
    utils: {
      directory'app/utils',
      inject'app',
      loadunittrue,
    },
  };

  config.cluster = {
    listen: {
      path'',
      port7001,
      hostname'0.0.0.0',
    },
  };

  return {
    ...config,
    ...userConfig,
  };
};

eggjs中的配置:https://eggjs.org/zh-cn/basics/config.html

eggjs中的插件

這里主要是針對(duì)一些egg集成的插件進(jìn)行配置,比如sequelize, cors等等

plugin.js具體的如下:

'use strict';

/** @type Egg.EggPlugin */
module.exports = {
  // had enabled by egg
  static: {
    enabletrue,
  },
  sequelize: {
    enabletrue,
    package'egg-sequelize',
  },
  cors: {
    enabletrue,
    package'egg-cors',
  },
  validate: {
    enabletrue,
    package'egg-validate',
  },
};

eggjs中的插件:https://eggjs.org/zh-cn/basics/plugin.html

eggjs中的擴(kuò)展extend

在app文件夾下新建extend文件夾,它可以對(duì)egg的agent,application,context,helper,request,response,validator內(nèi)置對(duì)象進(jìn)行擴(kuò)展。

這里以context.js為例,我想封裝一下上下文返回的格式,可以這么寫:

'use strict';

module.exports = {
  success(data, message = 'success') {
    const res = {
      status200,
      message,
      data,
    };

    this.app.logger.info(JSON.stringify(res));

    this.body = res;
  },
};

調(diào)用的時(shí)候ctx.success(data)。

eggjs中的擴(kuò)展:https://eggjs.org/zh-cn/basics/extend.html

eggjs中的中間件

比如說我想編寫一個(gè)請(qǐng)求響應(yīng)時(shí)間的中間件,那么可以在app文件夾下新建middleware文件夾,然后新建cost.js文件

// app/middleware/cost.js
module.exports = options => {
  const header = options.header || 'X-Response-Time';
  return async function cost(ctx, next{
    const now = Date.now();
    await next();
    ctx.set(header, `${Date.now() - now}ms`);
  };
};

config/config.default.js文件中,我們注冊(cè)它

  // add your middleware config here
  config.middleware = ['cost''errorHandler'];

這樣在請(qǐng)求響應(yīng)的時(shí)候就會(huì)帶上一個(gè)x-Response-Time

eggjs中的中間件:https://eggjs.org/zh-cn/basics/middleware.html

eggjs中的通用工具包

比如你想寫一些通用的工具類, 那么可以這么去做,在app目錄下新建utils文件夾,然后創(chuàng)建一個(gè)generator.js(這里以生成id舉例), 代碼如下:

const { v4: uuidv4 } = require('uuid');

function generateUUID({
  return uuidv4().replace(/-/g'');
}

function getNo(num{
  const numStr = `000${(num % 1000).toString()}`;
  return numStr.slice(-3);
}

module.exports = {
  generateUUID,
  getNo,
};

然后再config/config.default.js中配置

config.customLoader = {
    utils: {
      directory'app/utils',
      inject'app',
      loadunittrue,
    },
  };

它表示加載app/utils下面的文件,注入到application對(duì)象中。調(diào)用的時(shí)候就可以直接app.utils.generateUUID()

功能實(shí)現(xiàn)-文件上傳與下載

egg內(nèi)置了multipart插件,通過這個(gè)插件我們很容易實(shí)現(xiàn)文件上傳

async upload() {
    const { ctx, service } = this;
    const file = ctx.request.files[0];
    if (!file) return ctx.throw(404);

    // const filename = path.extname(file.filename).toLowerCase();
    const { filename } = file;
    const type = path.extname(filename).toLowerCase();
    const { username, nickname } = ctx.session.userinfo;
    const createBy = nickname || username;
    const uuid = ctx.app.utils.generator.generateUUID();
    const targetPathPrefix = path.join(this.config.baseDir, 'app/public', uuid);
    const targetPath = path.join(
      this.config.baseDir,
      'app/public',
      uuid,
      filename
    );
    const source = fs.createReadStream(file.filepath);
    await mkdirp(targetPathPrefix);
    const target = fs.createWriteStream(targetPath);
    let result = '';
    try {
      await pump(source, target);
      const stats = fs.statSync(targetPath);
      const size = ctx.app.utils.compute.bytesToSize(stats.size);
      const url = `public/${uuid}/${filename}`;
      result = await service.file.add(filename, type, size, url, createBy);
      ctx.logger.info('save %s to %s', file.filepath, targetPath, stats, size);
    } finally {
      // delete those request tmp files
      await ctx.cleanupRequestFiles();
    }
    ctx.success(result);
  }

上面的代碼就是在讀取前端傳過來的文件后,在app/public文件夾下創(chuàng)建文件,并將記錄寫到數(shù)據(jù)庫中

config/config.default.js中可以對(duì)文件進(jìn)行相關(guān)配置,比如說模式是流還是文件,相關(guān)文件的大小,相關(guān)文件的格式等。

exports.multipart = {
    mode'file',
    fileSize'100mb',
    whitelist: [
      // images
      '.jpg',
      '.jpeg'// image/jpeg
      '.png'// image/png, image/x-png
      '.gif'// image/gif
      '.bmp'// image/bmp
      '.wbmp'// image/vnd.wap.wbmp
      '.webp',
      '.tif',
      '.psd',
      // text
      '.svg',
      '.js',
      '.jsx',
      '.json',
      '.css',
      '.less',
      '.html',
      '.htm',
      '.xml',
      '.xlsx',
      '.xls',
      '.doc',
      '.docx',
      '.ppt',
      '.pptx',
      '.pdf',
      // tar
      '.zip',
      '.rar',
      '.gz',
      '.tgz',
      '.gzip',
      // video
      '.mp3',
      '.mp4',
      '.avi',
    ],
  };

eggjs中的文件上傳:https://eggjs.github.io/zh/guide/upload.html

egg-multipart: https://github.com/eggjs/egg-multipart

功能實(shí)現(xiàn)-數(shù)據(jù)統(tǒng)計(jì)

在Model層,需要對(duì)進(jìn)行一定范圍的查詢,如下:

 static async getTotal(start, end) {
      const where = {
        crimeTime: {
          [Op.between]: [start, end],
        },
      };
      try {
        const ret = await this.count({ where });
        logger.info(this.getTotal, start, end, ret);
        return ret;
      } catch (e) {
        logger.error(e);
        return false;
      }
    }

可能細(xì)心的讀者發(fā)現(xiàn)了,我這邊存儲(chǔ)數(shù)據(jù)用的是時(shí)間戳,一個(gè)月的時(shí)間戳是2592000。所以這邊想要取一個(gè)月的數(shù)據(jù)就很簡(jiǎn)單了框定范圍[start, start + 2592000]就好了。

以餅圖的為例, service層代碼如下:

  async calculateCount(start, end) {
    const { Bank, Mobile, Virtual, Website } = this;
    const bankCount = await Bank.getTotal(start, end + 2592000);
    const mobileCount = await Mobile.getTotal(start, end + 2592000);
    const virtualCount = await Virtual.getTotal(start, end + 2592000);
    const websiteCount = await Website.getTotal(start, end + 2592000);
    return [
      { name: '銀行查控', count: bankCount || 0 },
      { name: '電話查控', count: mobileCount || 0 },
      { name: '虛擬賬號(hào)查控', count: virtualCount || 0 },
      { name: '網(wǎng)站查控', count: websiteCount || 0 },
    ];
  }

controller層的代碼如下(這邊其實(shí)可以用Promise.all進(jìn)行優(yōu)化,參考schedule那個(gè)例子,讀者試著改一下吧,因?yàn)樗|發(fā)了eslint的這個(gè)規(guī)則,/* eslint-disable no-await-in-loop */, 不建議在循環(huán)中用await):

 async calculate() {
    const { ctx, service } = this;
    const { start, end } = ctx.request.body;
    const startUinx = this.ctx.app.utils.date.transformDate(start);
    const endUnix = this.ctx.app.utils.date.transformDate(end);
    const pieData = await service.statistics.calculateCount(startUinx, endUnix);
    const monthArr = this.ctx.app.utils.date.getMonthArr(start, end);
    const barData = [];
    let totalAmount = 0;
    for (const month of monthArr) {
      const { name, value } = month;
      const unix = this.ctx.app.utils.date.transformDate(value);
      const amount = await service.statistics.calculateAmount(unix);
      totalAmount += amount;
      barData.push({ name, amount });
    }

    ctx.success({ pieData, barData, totalAmount });
  }

功能實(shí)現(xiàn)-按鈕權(quán)限控制

這里以銀行查控為例,主要是根據(jù)相關(guān)的用戶角色和記錄的狀態(tài)去判斷它有什么按鈕權(quán)限,具體的代碼如下:

 async index() {
    const { ctx, service } = this;
    const { alarmNo, account, bankId, accountType, page, pageSize } =
      ctx.request.query;
    const { list, ...rest } = await service.bank.getAllLimit(
      alarmNo,
      account,
      bankId,
      accountType,
      Number(page),
      Number(pageSize)
    );
    const data = list.map(item => {
      const { role } = ctx.session.userinfo;
      const { status } = item;
      let btnList = [];
      if (role === 'operator') {
        if (status === 0) {
          btnList = ['check''detail'];
        } else if (status === 1) {
          btnList = ['detail'];
        }
      } else {
        btnList = ['detail'];
      }
      return {
        btnList,
        ...item,
      };
    });
    ctx.success({ list: data, ...rest });
  }

部署實(shí)施

傳統(tǒng)方式一個(gè)一個(gè)來(以u(píng)buntu21.04舉例)

能聯(lián)網(wǎng)

第一步:

mysql的安裝: apt-get install mysql-server

nginx的安裝: apt-get install nginx

nodejs的安裝 : apt-get install nodejs

第二步:

配置nignx、mysql開機(jī)自啟動(dòng)(略),nodejs這邊的程序建議用pm2管理,具體的步驟如下

安裝pm2

npm i pm2 -g

egg程序設(shè)置pm2管理,根目錄新增server.js

// server.js
const egg = require('egg');

// eslint-disable-next-line global-require
const workers = Number(process.argv[2] || require('os').cpus().length);

egg.startCluster({
  workers,
  baseDir: __dirname,
  port7001,
});

對(duì)pm2程序進(jìn)行配置,根目錄新增ecosystem.config.js

module.exports = {
  apps: [
    {
      name'server',
      script'./server.js',
      instances'1',
      exec_mode'cluster',
      env: {
        COMMON_VARIABLE'true',
        NODE_ENV'production',
        PORT7001,
      },
    },
  ],
};

package.json的scripts中中添加腳本

 "pm2""pm2 start server.js --env ecosystem.config.js"

設(shè)置pm2開機(jī)自啟動(dòng)

pm2 startup
pm2 save

pm2文檔:https://pm2.keymetrics.io/docs/usage/quick-start/

eggjs程序用pm2管理:https://eggjs.org/zh-cn/faq.html#%E8%BF%9B%E7%A8%8B%E7%AE%A1%E7%90%86%E4%B8%BA%E4%BB%80%E4%B9%88%E6%B2%A1%E6%9C%89%E9%80%89%E5%9E%8B-pm2

這里寫的eggjs程序官網(wǎng)是不推薦用pm2管理的,在開發(fā)環(huán)境有egg-bin,生產(chǎn)環(huán)境有egg-script,我這邊主要是考慮到,服務(wù)器那邊環(huán)境沒有我們阿里云或者騰訊云上面操作方便,需要考慮宕機(jī)后系統(tǒng)的重啟,而PM2剛好具備這些特性(管理node程序,開機(jī)自啟動(dòng)管理的node程序),俺也懶得寫啟動(dòng)腳本,就在選型上用pm2去管理,后面上了docker以后,那就沒這么多雜事了。

不能聯(lián)網(wǎng)

阿西吧,這個(gè)就比較頭大了。這邊就提供兩個(gè)思路,第一個(gè)是組網(wǎng),就是用你的電腦拉一根網(wǎng)線跟服務(wù)器組一個(gè)局域網(wǎng),然后共享你的網(wǎng)絡(luò),這里需要注意的是,服務(wù)器可能 會(huì)有多個(gè)網(wǎng)卡,你需要確保你所配置的那張網(wǎng)卡是對(duì)的,這邊有兩個(gè)辦法,第一個(gè)是眼睛睜大看網(wǎng)口上有沒有標(biāo)號(hào), 第二個(gè)就是暴力組網(wǎng),在你的宿主機(jī)上,使用ping 服務(wù)器內(nèi)網(wǎng)配置ip -t 去測(cè),直到發(fā)現(xiàn)那個(gè)正確的網(wǎng)卡。組完網(wǎng),參照樓上的就跑一遍唄。

第二個(gè)就異常痛苦啦,實(shí)在連不上網(wǎng),就需要提前下載相關(guān)的源碼包(這里比較頭大的是,一些不確定的依賴,比如編譯安裝的時(shí)候,可能需要下c++的庫),掛載到服務(wù)器上一個(gè)一個(gè)解壓編譯安裝,emmmmmmmm,太痛苦了,放棄治療吧,我選docker。

優(yōu)劣勢(shì)

沒看出有啥優(yōu)勢(shì),頭皮發(fā)麻,2333333333。

docker一把梭

Dockerfile的編寫

通過docker build命令運(yùn)行Dockerfile,我們可以得到相應(yīng)的鏡像,然后通過docker run相應(yīng)的鏡像我們可以得到相應(yīng)的容器,注意這里run命令要慎用,因?yàn)槊繄?zhí)行一次都會(huì)創(chuàng)建一層鏡像,你可以把多條命令用&&放到一起,或者放到CMD命令中,CMD是容器跑起來的時(shí)候執(zhí)行的命令。

前端(以hzga-fe為例)

這里表示是基于node14.8.0的鏡像,創(chuàng)建人是ataola,以/app為工作目錄,拷貝相關(guān)的文件到工作目錄,然后運(yùn)行相關(guān)的命令構(gòu)建鏡像,在構(gòu)建完以后,基于nginx1.17.2的鏡像,將打包好后的文件拷貝到nginx中,暴露80端口,在容器跑起來的時(shí)候執(zhí)行CMD的命令。

# build stage
FROM node:14.8.0 as build-stage
MAINTAINER ataola <zjt613@gmail.com>

WORKDIR /app

COPY package.json ./
COPY nginx ./nginx/
COPY public ./public/
COPY .editorconfig .env.* .eslintrc.js .eslintignore .prettierrc jsconfig.json *.config.js ./
COPY src ./src/
COPY build ./build/

RUN npm install --registry=https://registry.npm.taobao.org cnpm -g \
    && SASS_BINARY_SITE=https://npm.taobao.org/mirrors/node-sass/ cnpm install --registry=https://registry.npm.taobao.org \
    && npm rebuild node-sass \
    && npm run build:prod


# production stage
FROM nginx:1.17.2-alpine-perl as production-stage

COPY --from=build-stage /app/dist /usr/share/nginx/html
COPY --from=build-stage /app/nginx /etc/nginx/

VOLUME /app

EXPOSE 80

CMD ["nginx""-g""daemon off;"]

nginx.conf配置文件如下

#user  nobody;
worker_processes 1;

#error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;

#pid logs/nginx.pid;
events {
worker_connections 1024;
}


http {
include mime.types;
default_type application/octet-stream;

#log_format main '$remote_addr - $remote_user [$time_local] "$request" '
# '$status $body_bytes_sent "$http_referer" '
# '"$http_user_agent" "$http_x_forwarded_for"';

#access_log logs/access.log main;
sendfile on;
#tcp_nopush on;

#keepalive_timeout 0;
keepalive_timeout 65;

#gzip on;
upstream eggServer {
server hzga-be:7001;
}

server {
listen 80;
server_name hzga-fe;

#charset koi8-r;

#access_log logs/host.access.log main;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ =404;
}

location /prod-api {
rewrite /prod-api/(.*) /$1 break;
client_max_body_size 100M;
proxy_pass http://eggServer;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}

#error_page 404 /404.html;

# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}

# proxy the PHP scripts to Apache listening on 127.0.0.1:80
#
#location ~ \.php$ {
# proxy_pass http://127.0.0.1;
#}

# pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
#
#location ~ \.php$ {
# root html;
# fastcgi_pass 127.0.0.1:9000;
# fastcgi_index index.php;
# fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name;
# include fastcgi_params;
#}
# deny access to .htaccess files, if Apache's document root
# concurs with nginx's one
#
#location ~ /\.ht {
# deny all;
#}
}


# another virtual host using mix of IP-, name-, and port-based configuration
#
#server {
# listen 8000;
# listen somename:8080;
# server_name somename alias another.alias;
# location / {
# root html;
# index index.html index.htm;
# }
#}
# HTTPS server
#
#server {
# listen 443 ssl;
# server_name localhost;
# ssl_certificate cert.pem;
# ssl_certificate_key cert.key;
# ssl_session_cache shared:SSL:1m;
# ssl_session_timeout 5m;
# ssl_ciphers HIGH:!aNULL:!MD5;
# ssl_prefer_server_ciphers on;
# location / {
# root html;
# index index.html index.htm;
# }
#}
}

后端 (以hzga-be為例)

參考樓上hzga-fe的釋義。

FROM node:14.8.0
MAINTAINER ataola <zjt613@gmail.com>

WORKDIR /app

COPY package.json ./

RUN npm install --registry=https://registry.npm.taobao.org --production

COPY app ./app
COPY config ./config
COPY .eslintrc .eslintignore .prettierrc .autod.conf.js .editorconfig app.js jsconfig.json ./

VOLUME /app

EXPOSE 7001

CMD ["npm""run""docker"]

MySQL數(shù)據(jù)庫(以hzga-mysql為例)

參考樓上的樓上hzga-fe的釋義, 與之不同的是,這里通過配置設(shè)置了建庫腳本,用戶名密碼。

FROM mysql:8.0.16
MAINTAINER ataola<zjt613@gmail.com>

ENV MYSQL_DATABASE anti-fraud
ENV MYSQL_ROOT_PASSWORD ataola
ENV MYSQL_ROOT_HOST '%'

ENV AUTO_RUN_DIR ./docker-entrypoint-initdb.d

ENV INIT_SQL anti-fraud.sql

COPY ./$INIT_SQL $AUTO_RUN_DIR/

RUN chmod a+x $AUTO_RUN_DIR/$INIT_SQL

VOLUME /app

EXPOSE 3306

docker-compose.yml的編寫

我們開發(fā)會(huì)涉及到前端、后端、數(shù)據(jù)庫。docker-compose可以把多個(gè)容器放在一起管理,默認(rèn)會(huì)創(chuàng)建一個(gè)網(wǎng)絡(luò),通過相關(guān)的服務(wù)名就可以訪問,比如說,hzga-be的后端服務(wù)想要訪問hzga-mysql的數(shù)據(jù)庫,那么就可以直接在配置文件中,將ip改成hzga-mysql。同理,前端nginx這邊的代理,如果要訪問后端,那么可以在代理的位置直接寫haga-be。

docker-compose.yml文件如下:

這里表示是基于docker-compose 3.3版本的, 然后有三個(gè)service,分別是hzga-fe(前端),hzga-be(后端),hzga-mysql(MYSQL數(shù)據(jù)庫),然后指定了Dockerfile的位置,制作成鏡像后的名字,暴露了相應(yīng)的端口,然后容器的名字,失敗后的重啟策略,以及建立的網(wǎng)絡(luò)的名字,其中后端的服務(wù)hzga-be基于數(shù)據(jù)庫hzga-mysql

version: '3.3'
services:
  hzga-fe:
    build:
      context: ./anti-fraud-system-fe
      dockerfile: Dockerfile
    image: ataola/hzga-fe:0.0.1
    ports:
      - "80:80"
    networks:
      - net-hzga
    container_name: hzga-fe
    restart: on-failure
  hzga-be:
    build:
      context: ./anti-fraud-system-be
      dockerfile: Dockerfile
    image: ataola/hzga-be:0.0.1
    ports:
      - "7001:7001"
    depends_on:
      - hzga-mysql
    networks:
      - net-hzga
    container_name: hzga-be
    restart: on-failure
  hzga-mysql:
    build:
      context: ./database
      dockerfile: Dockerfile
    image: ataola/hzga-mysql:0.0.1
    ports:
      - "3306:3306"
    networks:
      - net-hzga
    container_name: hzga-mysql
    restart: on-failure
networks:
  net-hzga:
    driver: bridge

下面介紹下通過docker-compose管理

部署這套服務(wù): docker-compose up -d

暫停這套服務(wù): docker-compose pause

下線這套服務(wù): docker-compose down

查看相關(guān)的日志: docker-compose logs, 后面可以跟容器名字

如果是docker的命令 可以用docker help查看,如果是docker-compose的命令可以用docker-compose help查看

docker-compose的介紹:https://docs.docker.com/compose/

優(yōu)勢(shì)

部署很爽啊,配置文件一寫,命令一敲,起!包括后續(xù)的一些維護(hù),重啟啊、暫停啊等等很方便,方便搭建相關(guān)的集群,相關(guān)的環(huán)境(開發(fā)、測(cè)試、發(fā)布)

劣勢(shì)

增加了學(xué)習(xí)成本。

心得感悟

這個(gè)項(xiàng)目到這里,第一個(gè)初代版本算上OK了。我這邊也羅列了一些思考和問題供讀者們交流

網(wǎng)絡(luò)安全

  • 讓用戶不能同時(shí)在線,盡可以在一端登錄
  • 對(duì)于敏感數(shù)據(jù),比如說身份證、手機(jī)號(hào)等等前后端的交互肯定是不能明文的,怎么處理?
  • 對(duì)用戶多次密碼錯(cuò)誤嘗試鎖定用戶

性能優(yōu)化

  • SPA應(yīng)用一個(gè)是白屏、一個(gè)是對(duì)SEO不友好,假設(shè)這邊要處理的話,怎么去做?
  • 假如數(shù)據(jù)量很大了,數(shù)據(jù)庫怎么優(yōu)化,或者查詢?cè)趺磧?yōu)化?
  • 加入用戶量很大的話,如何接入redis做持久化?

業(yè)務(wù)優(yōu)化

  • 前端的相關(guān)表單加入詳細(xì)的格式校驗(yàn)
  • 后端的參數(shù)加入詳細(xì)的格式校驗(yàn)
  • 整理完備的電話號(hào)碼段數(shù)據(jù)庫、銀行卡號(hào)數(shù)據(jù)庫等等,進(jìn)一步優(yōu)化,各機(jī)構(gòu)角色用戶只能看自己的(參考警情)
  • 相關(guān)的定時(shí)任務(wù)能不能手動(dòng)停掉去,或者做出可配置化
  • 前端的相關(guān)下拉框做成可搜索,優(yōu)化用戶體驗(yàn)
  • 前端的表格做的花里胡哨,紅黃綠啥的安排下,讓用戶更直觀地看到狀態(tài)和操作

代碼優(yōu)化

  • 前端針對(duì)圖表封裝相應(yīng)的組件
  • 文中所示的表格其實(shí)并不是通用性的,還是具有特定場(chǎng)景的,在這個(gè)基礎(chǔ)上改造一個(gè)通用性的組件

寫在最后

docker鏡像地址

反欺詐系統(tǒng)前端: https://hub.docker.com/repository/docker/ataola/hzga-fe

反欺詐系統(tǒng)后端: https://hub.docker.com/repository/docker/ataola/hzga-be

反欺詐系統(tǒng)MySQL: https://hub.docker.com/repository/docker/ataola/hzga-mysql

源碼地址

github: https://github.com/cnroadbridge/anti-fraud-system

gitee: https://gitee.com/taoge2021/anti-fraud-system

假如人與人之間多一點(diǎn)真誠,少一些欺騙,那么就不需要我們新生代農(nóng)民工開發(fā)什么反欺詐系統(tǒng)了,世界將會(huì)變得更美好,烏拉!烏拉!烏拉!

以上就是今天的全部?jī)?nèi)容,因?yàn)閮?nèi)容比較多,寫的也比較倉促,所以有些地方我也是潦草地一筆帶過,如有問題,歡迎與我交流探討,謝謝!

本站僅提供存儲(chǔ)服務(wù),所有內(nèi)容均由用戶發(fā)布,如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請(qǐng)點(diǎn)擊舉報(bào)
打開APP,閱讀全文并永久保存 查看更多類似文章
猜你喜歡
類似文章
vite —— 一種新的、更快地 web 開發(fā)工具
Vue Koa從零打造一個(gè)H5頁面可視化編輯器——Quark-h5
錯(cuò)過了Vue CLI2,還要錯(cuò)過Vue CLI3?
vue-cli中使用axios
Vue 服務(wù)端渲染實(shí)踐 ——Web應(yīng)用首屏耗時(shí)最優(yōu)化方案
Vue基礎(chǔ)知識(shí)總結(jié)(絕對(duì)經(jīng)典)
更多類似文章 >>
生活服務(wù)
熱點(diǎn)新聞
分享 收藏 導(dǎo)長圖 關(guān)注 下載文章
綁定賬號(hào)成功
后續(xù)可登錄賬號(hào)暢享VIP特權(quán)!
如果VIP功能使用有故障,
可點(diǎn)擊這里聯(lián)系客服!

聯(lián)系客服