From 9708e1f673e7c18a3cc055de78ded962bf264cd7 Mon Sep 17 00:00:00 2001 From: xuecong <> Date: Tue, 16 Nov 2021 20:42:59 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E9=97=A8=E7=A6=81=E3=80=81?= =?UTF-8?q?=E8=AE=BF=E5=AE=A2=E7=A0=81=E3=80=81=E4=B8=9A=E4=B8=BB=E4=BA=8C?= =?UTF-8?q?=E7=BB=B4=E7=A0=81=E5=8A=A0=E5=AF=86=E6=96=B9=E5=BC=8F&?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=99=BA=E8=83=BD=E9=97=A8=E7=A6=81=E7=89=A9?= =?UTF-8?q?=E8=81=94=E7=BD=91=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- iot/README.MD | 17 ++ ower-mp/project.config.json | 2 +- ower-mp/src/pages/vistor/create.js | 2 +- ower-mp/src/pages/vistor/create.wxml | 2 +- resource/db.sql | 8 - server/src/app.ts | 16 +- server/src/constant/code.ts | 16 ++ server/src/iot/entrance/access.ts | 124 +++++++++++ server/src/iot/entrance/index.ts | 197 ++++++++++++++++++ server/src/iot/entrance/status.ts | 51 +++++ server/src/iot/index.ts | 18 ++ .../src/module/mp/controller/access/list.ts | 23 +- .../controller/community/binding_by_family.ts | 2 +- .../community/binding_by_property.ts | 2 +- .../mp/controller/community/family_code.ts | 2 +- server/src/module/mp/controller/user/card.ts | 10 +- .../src/module/mp/controller/vistor/create.ts | 8 - .../src/module/mp/controller/vistor/detail.ts | 7 +- server/src/module/mp/controller/vistor/use.ts | 8 +- .../module/pc/controller/building/import.ts | 13 -- .../src/module/pc/controller/option/card.ts | 14 +- .../src/module/pc/controller/ower/approve.ts | 2 +- .../src/module/pc/controller/vistor/create.ts | 9 +- .../src/module/pc/controller/vistor/detail.ts | 5 + .../src/module/pc/controller/vistor/scan.ts | 2 +- server/src/types/model.d.ts | 9 - .../src/utils/{open_access.ts => access.ts} | 36 ++-- server/src/utils/community.ts | 22 +- server/src/utils/crypto.ts | 24 +++ server/src/utils/index.ts | 8 +- server/src/utils/redis.ts | 58 ++++++ web/src/views/basic/vistor/detail.vue | 5 +- 32 files changed, 599 insertions(+), 123 deletions(-) create mode 100644 server/src/iot/entrance/access.ts create mode 100644 server/src/iot/entrance/index.ts create mode 100644 server/src/iot/entrance/status.ts create mode 100644 server/src/iot/index.ts rename server/src/utils/{open_access.ts => access.ts} (65%) create mode 100644 server/src/utils/redis.ts diff --git a/iot/README.MD b/iot/README.MD index 9fa8bac..21636b8 100644 --- a/iot/README.MD +++ b/iot/README.MD @@ -1,3 +1,20 @@ ## 「e家宜业」物联网服务端 > 代码整理中,敬请期待 + +### 智能门禁 + +> 目前仅支持卡帕斯品牌 + +#### 接口地址 + +- 心跳检测 `域名/iot/entrance?id={{接口形参第一位}}&community_id={{接口形参第二位}}&method=status` +- 开门刷卡 `域名/iot/entrance?id={{接口形参第一位}}&community_id={{接口形参第二位}}&method=access` + +#### 门禁配置 + +* 选择`启用AES128加密`,秘钥配置中台对应的秘钥 +* 心跳间隔设置为`60` +* `本机信息-标识`配置为中台对应的标识名称 +* `通讯协议`选择`启用Http通讯` +* `验证模式`选择`脱机验证优先`(主要针对老旧小区用的RFID卡) diff --git a/ower-mp/project.config.json b/ower-mp/project.config.json index a8c05d6..af8eca2 100644 --- a/ower-mp/project.config.json +++ b/ower-mp/project.config.json @@ -72,4 +72,4 @@ "list": [] } } -} +} \ No newline at end of file diff --git a/ower-mp/src/pages/vistor/create.js b/ower-mp/src/pages/vistor/create.js index 1a43916..96bb828 100644 --- a/ower-mp/src/pages/vistor/create.js +++ b/ower-mp/src/pages/vistor/create.js @@ -97,7 +97,7 @@ CwPage({ }, chooseContact() { wx.chooseContact({ - success: (res) => { + success: res => { this.setData({ vistor_name: res.displayName, vistor_phone: res.phoneNumber diff --git a/ower-mp/src/pages/vistor/create.wxml b/ower-mp/src/pages/vistor/create.wxml index 9d241ad..76bf7b8 100644 --- a/ower-mp/src/pages/vistor/create.wxml +++ b/ower-mp/src/pages/vistor/create.wxml @@ -10,7 +10,7 @@ is-detail url="/pages/community/index" /> - + { diff --git a/server/src/constant/code.ts b/server/src/constant/code.ts index bf65578..781b959 100644 --- a/server/src/constant/code.ts +++ b/server/src/constant/code.ts @@ -148,3 +148,19 @@ export const PARK_NAME_EXIST = -171; export const PARK_BLACKLIST_EXIST = -172; // 预警中控名称存在 export const WARNING_NAME_EXIST = -173; +// 门禁非法 +export const IOT_ENTRANCE_ILLEGAL = -174; +// 梯控非法 +export const IOT_ELEVATOR_ILLEGAL = -175; +// 灯控非法 +export const IOT_LAMP_ILLEGAL = -176; +// 中继器非法 +export const IOT_REPEATER_ILLEGAL = -177; +// 仪表非法 +export const IOT_METER_ILLEGAL = -178; +// 停车差非法 +export const IOT_PARK_ILLEGAL = -179; +// 预警中控非法 +export const IOT_WARNING_ILLEGAL = -180; +// 物联网设备秘钥错误 +export const IOT_SECRET_ERROR = -181; diff --git a/server/src/iot/entrance/access.ts b/server/src/iot/entrance/access.ts new file mode 100644 index 0000000..7bd4c8b --- /dev/null +++ b/server/src/iot/entrance/access.ts @@ -0,0 +1,124 @@ +/** + * +---------------------------------------------------------------------- + * | 「e家宜业」 —— 助力物业服务升级,用心服务万千业主 + * +---------------------------------------------------------------------- + * | Copyright (c) 2020~2021 https://www.chowa.com All rights reserved. + * +---------------------------------------------------------------------- + * | Licensed 未经许可不能去掉「e家宜业」和「卓瓦科技」相关版权 + * +---------------------------------------------------------------------- + * | Author: jixuecong@chowa.cn + * +---------------------------------------------------------------------- + */ + +import moment from 'moment'; +import Knex from 'Knex'; +import { EjyyIotEntrance } from '~/types/model'; +import { SELF_ACCESS_QRCODE, VISTOR_ACCESS_QRCODE } from '~/constant/open_access'; +import utils from '~/utils'; +import { IOT_METHOD_QRCODE, IOT_METHOD_NFC, IOT_METHOD_ICCARD } from '~/constant/iot'; + +const TYPE_CARD = '0'; +const TYPE_SERIAL = '1'; +const TYPE_PASSWORD = '2'; +const TYPE_BUTTON = '3'; +const TYPE_ID_CARD = '6'; +const TYPE_QRCODE = '9'; +const TYPE_FINGERPRINT = '10'; +const TYPE_PULSE = '11'; +const TYPE_RFID = '12'; +const TYPE_FACE = '13'; + +const OPEN = '1'; +const CLOSE = '3'; + +const ENTER = '0'; +const LEAVE = '1'; + +interface AccessParams { + type: + | typeof TYPE_CARD + | typeof TYPE_SERIAL + | typeof TYPE_PASSWORD + | typeof TYPE_BUTTON + | typeof TYPE_ID_CARD + | typeof TYPE_QRCODE + | typeof TYPE_FINGERPRINT + | typeof TYPE_PULSE + | typeof TYPE_RFID + | typeof TYPE_FACE; + Reader: string; + DataLen: string; + Index: string; + Serial: string; + Status: string; + Input: string; + Ver: string; + ID: string; + IP: string; + MAC: string; + Card: string; +} + +interface AccessResponse { + AcsRes: typeof OPEN | typeof CLOSE; + ActIndex?: typeof ENTER | typeof LEAVE; + Time?: string; +} + +async function accessMethod(params: AccessParams, entranceInfo: EjyyIotEntrance, model: Knex): Promise { + const mom = moment(); + await utils.redis.set(`entrance${entranceInfo.id}`, mom.valueOf().toString()); + + // 卡 二维码 + if ( + params.type === TYPE_CARD || + params.type === TYPE_RFID || + params.type === TYPE_SERIAL || + params.type === TYPE_QRCODE + ) { + const reuslt = + params.type === TYPE_SERIAL || params.type === TYPE_QRCODE + ? utils.access.decrypt(Buffer.from(params.Card, 'base64').toString()) + : utils.access.decrypt(params.Card); + + if (!reuslt.success) { + return { AcsRes: CLOSE }; + } + + const existBuilding = await model + .from('ejyy_building_info') + .where('id', reuslt.building_id) + .andWhere('community_id', entranceInfo.community_id) + .first(); + + if (!existBuilding) { + return { AcsRes: CLOSE }; + } + + let method: typeof IOT_METHOD_NFC | typeof IOT_METHOD_ICCARD | typeof IOT_METHOD_QRCODE = IOT_METHOD_QRCODE; + + if (params.type === TYPE_CARD) { + method = IOT_METHOD_NFC; + } else if (params.type === TYPE_RFID) { + method = IOT_METHOD_ICCARD; + } + + await model.from('ejyy_iot_entrance_log').insert({ + wechat_mp_user_id: reuslt.type === VISTOR_ACCESS_QRCODE ? null : reuslt.id, + vistor_id: reuslt.type === SELF_ACCESS_QRCODE ? null : reuslt.id, + entrance_id: entranceInfo.id, + method, + created_at: mom.valueOf() + }); + + return { AcsRes: OPEN, ActIndex: ENTER, Time: '5' }; + } + // 按钮 + else if (params.type === TYPE_BUTTON) { + return { AcsRes: OPEN, ActIndex: LEAVE }; + } else { + return { AcsRes: CLOSE }; + } +} + +export default accessMethod; diff --git a/server/src/iot/entrance/index.ts b/server/src/iot/entrance/index.ts new file mode 100644 index 0000000..33ee5f8 --- /dev/null +++ b/server/src/iot/entrance/index.ts @@ -0,0 +1,197 @@ +/** + * +---------------------------------------------------------------------- + * | 「e家宜业」 —— 助力物业服务升级,用心服务万千业主 + * +---------------------------------------------------------------------- + * | Copyright (c) 2020~2021 https://www.chowa.com All rights reserved. + * +---------------------------------------------------------------------- + * | Licensed 未经许可不能去掉「e家宜业」和「卓瓦科技」相关版权 + * +---------------------------------------------------------------------- + * | Author: jixuecong@chowa.cn + * +---------------------------------------------------------------------- + */ + +import path from 'path'; +import crypto from 'crypto'; +import RawBody from 'raw-body'; +import KoaRouter from 'koa-router'; +import { Context } from 'koa'; +import { Action } from '~/types/action'; +import { IOT_ENTRANCE_ILLEGAL, PARAMS_ERROR, IOT_SECRET_ERROR } from '~/constant/code'; +import validatorService from '~/service/validator'; +import statusMethod from './status'; +import accessMethod from './access'; +import config from '~/config'; +import cwlog from 'chowa-log'; + +function zeroPad(str: string, length = 8): string { + str = Buffer.from(str, 'utf8').toString('hex'); + const bitLength = str.length * length; + if (bitLength < 256) { + for (let i = bitLength; i < 256; i += length) { + str += 0; + } + } else if (bitLength > 256) { + while ((str.length * length) % 256 != 0) { + str += 0; + } + } + return Buffer.from(str, 'hex').toString('utf8'); +} + +function encrypt(str: string, secret: string): string { + const cipher = crypto.createCipheriv('aes-128-ecb', secret, ''); + + cipher.setAutoPadding(false); + + let crypted = cipher.update(zeroPad(str), 'utf8', 'hex'); + + crypted += cipher.final('hex'); + + return crypted; +} + +function decrypt(str: string, secret: string): string { + const cipher = crypto.createDecipheriv('aes-128-ecb', secret, ''); + const isBase64 = Buffer.from(str, 'base64').toString('base64') === str; + + cipher.setAutoPadding(false); + + let decrypted = cipher.update(zeroPad(str), isBase64 ? 'base64' : 'hex', 'utf8'); + decrypted = decrypted + cipher.final('utf8'); + + return decrypted.replace(/[\u0000-\u0019]+/g, ''); +} + +const IotEntranceAction = { + router: { + path: '/entrance', + method: 'post', + authRequired: false + }, + validator: { + query: [ + { + name: 'community_id', + required: true, + regex: /^\d+$/ + }, + { + name: 'id', + required: true, + regex: /^\d+$/ + }, + { + name: 'method', + required: true, + regex: /^status|access$/ + } + ] + }, + response: async ctx => { + const { community_id, id, method } = ctx.request.query; + + const entranceInfo = await ctx.model + .from('ejyy_iot_entrance') + .where('id', id) + .andWhere('community_id', community_id) + .first(); + + if (!entranceInfo) { + return (ctx.body = { + code: IOT_ENTRANCE_ILLEGAL, + message: '非法的物联网门禁' + }); + } + + const raw = await RawBody(ctx.req, { + length: ctx.request.length, + limit: '1mb', + encoding: ctx.request.charset || 'utf-8' + }); + + const datas = raw.replace('DATAS=', ''); + + if (!datas) { + return (ctx.body = { + code: PARAMS_ERROR, + message: '物联网门禁受控参数错误' + }); + } + + let decrypted = null; + + try { + decrypted = decrypt(datas, entranceInfo.secret); + } catch (e) { + return (ctx.bod = { + code: IOT_SECRET_ERROR, + message: '物联网门禁秘钥错误' + }); + } + + let params = null; + + try { + params = JSON.parse(decrypted); + } catch (e) { + return (ctx.body = { + code: IOT_SECRET_ERROR, + message: '物联网门禁秘钥错误' + }); + } + + if (!('Serial' in params) || !('ID' in params)) { + return (ctx.body = { + code: PARAMS_ERROR, + message: '物联网门禁控制参数错误' + }); + } + + if (params.ID !== entranceInfo.sign) { + return (ctx.body = { + code: PARAMS_ERROR, + message: '物联网门禁自定义标识符配置错误' + }); + } + + let res = {}; + console.log(params); + switch (method) { + // 心跳 + case 'status': + res = await statusMethod(params, entranceInfo); + break; + + // 开门 + case 'access': + res = await accessMethod(params, entranceInfo, ctx.model); + break; + + default: + } + + ctx.res.setHeader('Content-Type', 'text/html'); + ctx.body = `DATAS={${encrypt(JSON.stringify(res), entranceInfo.secret)}}`; + } +}; + +export default (appRouter: KoaRouter) => { + const { router, validator, response } = IotEntranceAction; + + appRouter[router.method](path.posix.join('/iot', router.path), async (ctx: Context, next) => { + const vs = validatorService(ctx, validator); + + if (!vs.success) { + return (ctx.body = { + code: PARAMS_ERROR, + message: vs.message + }); + } + + await response.apply(this, [ctx, next]); + }); + + if (config.debug) { + cwlog.info(`IotEntrance mounted and request from ${path.posix.join('/iot', router.path)} by ${router.method}`); + } +}; diff --git a/server/src/iot/entrance/status.ts b/server/src/iot/entrance/status.ts new file mode 100644 index 0000000..167f769 --- /dev/null +++ b/server/src/iot/entrance/status.ts @@ -0,0 +1,51 @@ +/** + * +---------------------------------------------------------------------- + * | 「e家宜业」 —— 助力物业服务升级,用心服务万千业主 + * +---------------------------------------------------------------------- + * | Copyright (c) 2020~2021 https://www.chowa.com All rights reserved. + * +---------------------------------------------------------------------- + * | Licensed 未经许可不能去掉「e家宜业」和「卓瓦科技」相关版权 + * +---------------------------------------------------------------------- + * | Author: jixuecong@chowa.cn + * +---------------------------------------------------------------------- + */ + +import moment from 'moment'; +import { EjyyIotEntrance } from '~/types/model'; +import utils from '~/utils'; + +interface StatusParams { + Serial: string; + Status: string; + Input: string; + Ver: string; + ID: string; + IP: string; + MAC: string; + Now: string; + Crc: string; + T1: string; + H1: string; + T2: string; + H2: string; + NextNum: string; + Key: string; +} + +interface StatusResponse { + Key: string; + Now: string; +} + +async function statusMethod(params: StatusParams, entranceInfo: EjyyIotEntrance): Promise { + const mom = moment(); + + await utils.redis.set(`entrance${entranceInfo.id}`, mom.valueOf().toString()); + + return { + Key: params.Key, + Now: `${mom.format('YYYYMMDDHHmmss')}${mom.day()}` + }; +} + +export default statusMethod; diff --git a/server/src/iot/index.ts b/server/src/iot/index.ts new file mode 100644 index 0000000..f853f16 --- /dev/null +++ b/server/src/iot/index.ts @@ -0,0 +1,18 @@ +/** + * +---------------------------------------------------------------------- + * | 「e家宜业」 —— 助力物业服务升级,用心服务万千业主 + * +---------------------------------------------------------------------- + * | Copyright (c) 2020~2021 https://www.chowa.com All rights reserved. + * +---------------------------------------------------------------------- + * | Licensed 未经许可不能去掉「e家宜业」和「卓瓦科技」相关版权 + * +---------------------------------------------------------------------- + * | Author: jixuecong@chowa.cn + * +---------------------------------------------------------------------- + */ + +import KoaRouter from 'koa-router'; +import IotEntrance from './entrance'; + +export function init(appRouter: KoaRouter) { + IotEntrance(appRouter); +} diff --git a/server/src/module/mp/controller/access/list.ts b/server/src/module/mp/controller/access/list.ts index a26a20c..7267283 100644 --- a/server/src/module/mp/controller/access/list.ts +++ b/server/src/module/mp/controller/access/list.ts @@ -12,6 +12,8 @@ import { Action } from '~/types/action'; import { SUCCESS } from '~/constant/code'; +import utils from '~/utils'; +import { SELF_ACCESS_QRCODE } from '~/constant/open_access'; interface RequestBody { building_ids: number[]; @@ -43,11 +45,22 @@ const MpAccessListAction = { response: async ctx => { const { building_ids, community_id } = ctx.request.body; - const cardList = await ctx.model - .from('ejyy_building_access') - .whereIn('building_id', building_ids) - .select('building_id', 'uid') - .orderBy('id', 'desc'); + const buildings = await ctx.model + .from('ejyy_user_building') + .leftJoin('ejyy_building_info', 'ejyy_building_info.id', 'ejyy_user_building.building_id') + .where('ejyy_building_info.community_id', community_id) + .whereIn('ejyy_user_building.building_id', building_ids) + .select('ejyy_user_building.building_id') + .orderBy('ejyy_user_building.id', 'desc'); + + const cardList = []; + + buildings.forEach(({ building_id }) => { + cardList.push({ + building_id, + uid: utils.access.encrypt(ctx.mpUserInfo.id, building_id, SELF_ACCESS_QRCODE) + }); + }); const entranceList = await ctx.model .from('ejyy_iot_entrance') diff --git a/server/src/module/mp/controller/community/binding_by_family.ts b/server/src/module/mp/controller/community/binding_by_family.ts index e9f0642..51efba7 100644 --- a/server/src/module/mp/controller/community/binding_by_family.ts +++ b/server/src/module/mp/controller/community/binding_by_family.ts @@ -41,7 +41,7 @@ const MpCommunityBindingByFamliyAction = { response: async ctx => { const { qrcontent } = ctx.request.body; - const qrInfo = utils.community.decode(qrcontent); + const qrInfo = utils.community.decrypt(qrcontent); if ( !qrInfo.success || diff --git a/server/src/module/mp/controller/community/binding_by_property.ts b/server/src/module/mp/controller/community/binding_by_property.ts index 2a24fd7..7a352fb 100644 --- a/server/src/module/mp/controller/community/binding_by_property.ts +++ b/server/src/module/mp/controller/community/binding_by_property.ts @@ -39,7 +39,7 @@ const MpCommunityBindingByPropertyAction = { }, response: async ctx => { const { qrcontent } = ctx.request.body; - const qrInfo = utils.community.decode(qrcontent); + const qrInfo = utils.community.decrypt(qrcontent); if (!qrInfo.success || qrInfo.authenticated_type !== AUTHENTICTED_BY_PROPERTY_COMPANY) { return (ctx.body = { diff --git a/server/src/module/mp/controller/community/family_code.ts b/server/src/module/mp/controller/community/family_code.ts index 7a4e3d2..20a776e 100644 --- a/server/src/module/mp/controller/community/family_code.ts +++ b/server/src/module/mp/controller/community/family_code.ts @@ -41,7 +41,7 @@ const MpCommunityFamliyCodeAction = { response: async ctx => { const { building_ids } = ctx.request.body; - const content = utils.community.encode(building_ids, AUTHENTICTED_BY_FAMILY, ctx.mpUserInfo.id); + const content = utils.community.encrypt(building_ids, AUTHENTICTED_BY_FAMILY, ctx.mpUserInfo.id); ctx.body = { code: SUCCESS, diff --git a/server/src/module/mp/controller/user/card.ts b/server/src/module/mp/controller/user/card.ts index 7477691..53f559f 100644 --- a/server/src/module/mp/controller/user/card.ts +++ b/server/src/module/mp/controller/user/card.ts @@ -12,8 +12,7 @@ import { Action } from '~/types/action'; import { SUCCESS } from '~/constant/code'; -import config from '~/config'; -import crypto from 'crypto'; +import utils from '~/utils'; const MpUserCardAction = { router: { @@ -24,15 +23,10 @@ const MpUserCardAction = { }, response: async ctx => { - const cipher = crypto.createCipheriv('aes-256-cbc', config.crypto.key, config.crypto.iv); - let crypted = cipher.update(`${ctx.mpUserInfo.id}-${Date.now()}`, 'utf8', 'hex'); - - crypted += cipher.final('hex'); - ctx.body = { code: SUCCESS, data: { - uid: crypted + uid: utils.crypto.encrypt(`${ctx.mpUserInfo.id}-${Date.now()}`) } }; } diff --git a/server/src/module/mp/controller/vistor/create.ts b/server/src/module/mp/controller/vistor/create.ts index 5530ada..7c0cda8 100644 --- a/server/src/module/mp/controller/vistor/create.ts +++ b/server/src/module/mp/controller/vistor/create.ts @@ -13,7 +13,6 @@ import { Action } from '~/types/action'; import { SUCCESS, STATUS_ERROR } from '~/constant/code'; import { BINDING_BUILDING, TRUE, FALSE } from '~/constant/status'; -import { VISTOR_ACCESS_QRCODE } from '~/constant/open_access'; import utils from '~/utils'; import * as vistorService from '~/service/vistor'; import moment from 'moment'; @@ -117,13 +116,6 @@ const MpVistorCreateAction = { created_at: Date.now() }); - const uid = utils.openAccess.encode(id, building_id, VISTOR_ACCESS_QRCODE); - - await ctx.model - .from('ejyy_vistor') - .update({ uid }) - .where('id', id); - if (vistorInfo) { vistorService.pushAccessToVistor(ctx.model, vistorInfo, id, expire); } diff --git a/server/src/module/mp/controller/vistor/detail.ts b/server/src/module/mp/controller/vistor/detail.ts index 03b4e1a..2feec64 100644 --- a/server/src/module/mp/controller/vistor/detail.ts +++ b/server/src/module/mp/controller/vistor/detail.ts @@ -12,6 +12,7 @@ import { Action } from '~/types/action'; import { SUCCESS } from '~/constant/code'; +import { VISTOR_ACCESS_QRCODE } from '~/constant/open_access'; import utils from '~/utils'; interface RequestParams { @@ -51,6 +52,7 @@ const MpVistorDetailAction = { 'ejyy_vistor.expire', 'ejyy_vistor.used_at', 'ejyy_vistor.created_at', + 'ejyy_vistor.building_id', 'ejyy_building_info.type', 'ejyy_building_info.area', 'ejyy_building_info.building', @@ -65,10 +67,13 @@ const MpVistorDetailAction = { detail.vistor_phone = utils.phone.hide(detail.vistor_phone); + const uid = utils.access.encrypt(detail.id, detail.building_id, VISTOR_ACCESS_QRCODE); + ctx.body = { code: SUCCESS, data: { - ...detail + ...detail, + uid } }; } diff --git a/server/src/module/mp/controller/vistor/use.ts b/server/src/module/mp/controller/vistor/use.ts index bcae657..b759489 100644 --- a/server/src/module/mp/controller/vistor/use.ts +++ b/server/src/module/mp/controller/vistor/use.ts @@ -12,6 +12,8 @@ import { Action } from '~/types/action'; import { SUCCESS } from '~/constant/code'; +import { VISTOR_ACCESS_QRCODE } from '~/constant/open_access'; +import utils from '~/utils'; interface RequestParams { id: number; @@ -47,6 +49,7 @@ const MpVistorUseAction = { 'ejyy_vistor.expire', 'ejyy_vistor.used_at', 'ejyy_vistor.created_at', + 'ejyy_vistor.building_id', 'ejyy_building_info.type', 'ejyy_building_info.area', 'ejyy_building_info.building', @@ -59,10 +62,13 @@ const MpVistorUseAction = { .where('ejyy_vistor.id', id) .first(); + const uid = utils.access.encrypt(detail.id, detail.building_id, VISTOR_ACCESS_QRCODE); + ctx.body = { code: SUCCESS, data: { - ...detail + ...detail, + uid } }; } diff --git a/server/src/module/pc/controller/building/import.ts b/server/src/module/pc/controller/building/import.ts index ccb2b53..798da23 100644 --- a/server/src/module/pc/controller/building/import.ts +++ b/server/src/module/pc/controller/building/import.ts @@ -15,7 +15,6 @@ import { SUCCESS } from '~/constant/code'; import * as ROLE from '~/constant/role_access'; import utils from '~/utils'; import { HOUSE, CARPORT, WAREHOUSE, MERCHANT, GARAGE } from '~/constant/building'; -import { SELF_ACCESS_QRCODE } from '~/constant/open_access'; interface Building { type: typeof HOUSE | typeof CARPORT | typeof WAREHOUSE; @@ -127,18 +126,6 @@ const PcBuildingImportAction = { }); } - if (type === HOUSE) { - const [accessId] = await ctx.model.from('ejyy_building_access').insert({ - building_id: insertId, - created_at - }); - const uid = utils.openAccess.encode(accessId, insertId, SELF_ACCESS_QRCODE); - await ctx.model - .from('ejyy_building_access') - .update({ uid }) - .where('id', accessId); - } - ids.push(insertId); } diff --git a/server/src/module/pc/controller/option/card.ts b/server/src/module/pc/controller/option/card.ts index 4501482..dcd4b3a 100644 --- a/server/src/module/pc/controller/option/card.ts +++ b/server/src/module/pc/controller/option/card.ts @@ -17,8 +17,6 @@ import { NORMAL_STATUS, TRUE, BINDING_BUILDING } from '~/constant/status'; import { HOUSE, CARPORT, WAREHOUSE, MERCHANT, GARAGE } from '~/constant/building'; import * as ROLE from '~/constant/role_access'; import utils from '~/utils'; -import config from '~/config'; -import crypto from 'crypto'; interface RequestBody { uid: string; @@ -57,21 +55,17 @@ const PcOptionCardAction = { }, response: async ctx => { const { uid, community_id } = ctx.request.body; - let userId = null; - let stamp = null; + const origin = utils.crypto.decrypt(uid); - try { - const cipher = crypto.createDecipheriv('aes-256-cbc', config.crypto.key, config.crypto.iv); - const decrypted = cipher.update(uid, 'hex', 'utf8'); - const origin = decrypted + cipher.final('utf8'); - [userId, stamp] = origin.split('-'); - } catch (e) { + if (!origin) { return (ctx.body = { code: QUERY_ILLEFAL, message: '非法业主名片' }); } + const [userId, stamp] = origin.split('-'); + if (!/^\d+$/.test(userId) || !/^\d{13}$/.test(stamp)) { return (ctx.body = { code: QUERY_ILLEFAL, diff --git a/server/src/module/pc/controller/ower/approve.ts b/server/src/module/pc/controller/ower/approve.ts index 8f83605..89eee42 100644 --- a/server/src/module/pc/controller/ower/approve.ts +++ b/server/src/module/pc/controller/ower/approve.ts @@ -61,7 +61,7 @@ const PcOwerApproveAction = { }); } - const res = utils.community.encode(building_ids, AUTHENTICTED_BY_PROPERTY_COMPANY, ctx.pcUserInfo.id); + const res = utils.community.encrypt(building_ids, AUTHENTICTED_BY_PROPERTY_COMPANY, ctx.pcUserInfo.id); ctx.body = { code: SUCCESS, diff --git a/server/src/module/pc/controller/vistor/create.ts b/server/src/module/pc/controller/vistor/create.ts index 7902271..6ccc473 100644 --- a/server/src/module/pc/controller/vistor/create.ts +++ b/server/src/module/pc/controller/vistor/create.ts @@ -110,17 +110,12 @@ const PcVistorCreateAction = { created_at: Date.now() }); - const uid = utils.openAccess.encode(id, building_id, VISTOR_ACCESS_QRCODE); - - await ctx.model - .from('ejyy_vistor') - .update({ uid }) - .where('id', id); - if (vistorInfo) { vistorService.pushAccessToVistor(ctx.model, vistorInfo, id, expire); } + const uid = utils.access.encrypt(id, building_id, VISTOR_ACCESS_QRCODE); + ctx.body = { code: SUCCESS, data: { diff --git a/server/src/module/pc/controller/vistor/detail.ts b/server/src/module/pc/controller/vistor/detail.ts index e185b30..f4077b5 100644 --- a/server/src/module/pc/controller/vistor/detail.ts +++ b/server/src/module/pc/controller/vistor/detail.ts @@ -13,6 +13,8 @@ import { Action } from '~/types/action'; import { SUCCESS, QUERY_ILLEFAL } from '~/constant/code'; import * as ROLE from '~/constant/role_access'; +import { VISTOR_ACCESS_QRCODE } from '~/constant/open_access'; +import utils from '~/utils'; interface RequestBody { id: number; @@ -94,10 +96,13 @@ const PcVistorDetailAction = { delete info.property_company_user_id; + const uid = utils.access.encrypt(info.id, info.building_id, VISTOR_ACCESS_QRCODE); + ctx.body = { code: SUCCESS, data: { info, + uid, registrant } }; diff --git a/server/src/module/pc/controller/vistor/scan.ts b/server/src/module/pc/controller/vistor/scan.ts index 78fe959..2e0c5ff 100644 --- a/server/src/module/pc/controller/vistor/scan.ts +++ b/server/src/module/pc/controller/vistor/scan.ts @@ -45,7 +45,7 @@ const PcVistorScanAction = { }, response: async ctx => { const { uid, community_id } = ctx.request.body; - const { id, building_id, type, success } = utils.openAccess.decode(uid); + const { id, building_id, type, success } = utils.access.decrypt(uid); if (!success || type !== VISTOR_ACCESS_QRCODE) { return (ctx.body = { diff --git a/server/src/types/model.d.ts b/server/src/types/model.d.ts index 9cb4ab3..35809b5 100644 --- a/server/src/types/model.d.ts +++ b/server/src/types/model.d.ts @@ -415,13 +415,6 @@ declare namespace EjyyModel { replyed_at?: number; } - interface EjyyBuildingAccess { - id?: number; - building_id: number; - uid?: string; - created_at: number; - } - interface EjyyRepair { id?: number; wechat_mp_user_id?: number; @@ -656,7 +649,6 @@ declare namespace EjyyModel { vistor_phone: string; car_number?: string; have_vistor_info: typeof TRUE | typeof FALSE; - uid?: string; expire: number; used_at?: number; scan_by?: number; @@ -1355,7 +1347,6 @@ declare module 'knex/types/tables' { ejyy_notice_to_user_readed: EjyyModel.EjyyNoticeToUserReaded; ejyy_notice_tpl: EjyyModel.EjyyNoticeTpl; ejyy_feedback: EjyyModel.EjyyFeedback; - ejyy_building_access: EjyyModel.EjyyBuildingAccess; ejyy_repair: EjyyModel.EjyyRepair; ejyy_repair_urge: EjyyModel.EjyyRepairUrge; ejyy_complain: EjyyModel.EjyyComplain; diff --git a/server/src/utils/open_access.ts b/server/src/utils/access.ts similarity index 65% rename from server/src/utils/open_access.ts rename to server/src/utils/access.ts index 9c4a037..5489456 100644 --- a/server/src/utils/open_access.ts +++ b/server/src/utils/access.ts @@ -10,39 +10,32 @@ * +---------------------------------------------------------------------- */ -import crypto from 'crypto'; -import config from '~/config'; +import * as crypto from './crypto'; + import { SELF_ACCESS_QRCODE, VISTOR_ACCESS_QRCODE } from '~/constant/open_access'; -interface DecodeResult { +interface DecryptResult { id?: number; building_id?: number; type?: typeof SELF_ACCESS_QRCODE | typeof VISTOR_ACCESS_QRCODE; + stamp?: number; success: boolean; } // 区分访客二维码和自己的二维码 -export function encode( +// 访客二维码 id 为 vistor_id 自己的二维码为user_id +export function encrypt( id: number, building_id: number, type: typeof SELF_ACCESS_QRCODE | typeof VISTOR_ACCESS_QRCODE ): string { - const cipher = crypto.createCipheriv('aes-256-cbc', config.crypto.key, config.crypto.iv); - let crypted = cipher.update(`${id}#${building_id}#${type}`, 'utf8', 'hex'); - - crypted += cipher.final('hex'); - - return crypted; + return crypto.encrypt(`${id}#${building_id}#${type}${Date.now()}`); } -export function decode(uid: string): DecodeResult { - let origin = null; +export function decrypt(uid: string): DecryptResult { + const origin = crypto.decrypt(uid); - try { - const cipher = crypto.createDecipheriv('aes-256-cbc', config.crypto.key, config.crypto.iv); - const decrypted = cipher.update(uid, 'hex', 'utf8'); - origin = decrypted + cipher.final('utf8'); - } catch (e) { + if (!origin) { return { success: false }; } @@ -50,9 +43,15 @@ export function decode(uid: string): DecodeResult { const id = parseInt(arr[0], 10); const building_id = parseInt(arr[1], 10); const type = parseInt(arr[2], 10); + const stamp = parseInt(arr[3], 10); let success = true; - if ((type !== SELF_ACCESS_QRCODE && type !== VISTOR_ACCESS_QRCODE) || building_id === NaN || id === NaN) { + if ( + (type !== SELF_ACCESS_QRCODE && type !== VISTOR_ACCESS_QRCODE) || + building_id === NaN || + id === NaN || + /^\d{13}$/.test(stamp.toString()) + ) { success = false; } @@ -60,6 +59,7 @@ export function decode(uid: string): DecodeResult { id, building_id, type, + stamp, success }; } diff --git a/server/src/utils/community.ts b/server/src/utils/community.ts index d668ae1..8143fa0 100644 --- a/server/src/utils/community.ts +++ b/server/src/utils/community.ts @@ -10,8 +10,7 @@ * +---------------------------------------------------------------------- */ -import crypto from 'crypto'; -import config from '~/config'; +import * as crypto from './crypto'; import { AUTHENTICTED_BY_PROPERTY_COMPANY, AUTHENTICTED_BY_FAMILY } from '~/constant/authenticated_type'; interface DecodeResult { @@ -106,7 +105,7 @@ function fake(len: number): string { return ret.join(''); } -export function encode( +export function encrypt( building_ids: number[], authenticated_type: typeof AUTHENTICTED_BY_PROPERTY_COMPANY | typeof AUTHENTICTED_BY_FAMILY, user_id: number @@ -120,25 +119,16 @@ export function encode( ret.push(fake(8)); - const cipher = crypto.createCipheriv('aes-256-cbc', config.crypto.key, config.crypto.iv); - let crypted = cipher.update(ret.join(''), 'utf8', 'base64'); - - crypted += cipher.final('base64'); - return { - text: crypted, + text: crypto.encrypt(ret.join('')), stamp }; } -export function decode(text: string): DecodeResult { - let str = null; +export function decrypt(text: string): DecodeResult { + let str = crypto.decrypt(text); - try { - const cipher = crypto.createDecipheriv('aes-256-cbc', config.crypto.key, config.crypto.iv); - let decrypted = cipher.update(text, 'base64', 'utf8'); - str = decrypted + cipher.final('utf8'); - } catch (e) { + if (!str) { return { success: false }; } diff --git a/server/src/utils/crypto.ts b/server/src/utils/crypto.ts index eef80f7..b062c1e 100644 --- a/server/src/utils/crypto.ts +++ b/server/src/utils/crypto.ts @@ -11,6 +11,7 @@ */ import crypto from 'crypto'; +import config from '~/config'; export function md5(str: string, upperCase = false): string { const hash = crypto.createHash('md5'); @@ -21,3 +22,26 @@ export function md5(str: string, upperCase = false): string { return upperCase ? ret.toUpperCase() : ret; } + +export function encrypt(str: string): string { + const cipher = crypto.createCipheriv('aes-256-cbc', config.crypto.key, config.crypto.iv); + let crypted = cipher.update(str, 'utf8', 'hex'); + + crypted += cipher.final('hex'); + + return crypted; +} + +export function decrypt(str: string): string { + let result = null; + + try { + const cipher = crypto.createDecipheriv('aes-256-cbc', config.crypto.key, config.crypto.iv); + const decrypted = cipher.update(str, 'hex', 'utf8'); + result = decrypted + cipher.final('utf8'); + } catch (e) { + return result; + } + + return result; +} diff --git a/server/src/utils/index.ts b/server/src/utils/index.ts index f4b7d9e..271cb65 100644 --- a/server/src/utils/index.ts +++ b/server/src/utils/index.ts @@ -11,7 +11,7 @@ */ import * as crypto from './crypto'; -import * as openAccess from './open_access'; +import * as access from './access'; import * as phone from './phone'; import * as community from './community'; import * as building from './building'; @@ -20,10 +20,11 @@ import * as order from './order'; import * as text from './text'; import * as idcard from './idcard'; import * as mail from './mail'; +import * as redis from './redis'; const utils = { crypto, - openAccess, + access, phone, community, building, @@ -31,7 +32,8 @@ const utils = { order, text, idcard, - mail + mail, + redis }; export default utils; diff --git a/server/src/utils/redis.ts b/server/src/utils/redis.ts new file mode 100644 index 0000000..2a87e61 --- /dev/null +++ b/server/src/utils/redis.ts @@ -0,0 +1,58 @@ +/** + * +---------------------------------------------------------------------- + * | 「e家宜业」 —— 助力物业服务升级,用心服务万千业主 + * +---------------------------------------------------------------------- + * | Copyright (c) 2020~2021 https://www.chowa.com All rights reserved. + * +---------------------------------------------------------------------- + * | Licensed 未经许可不能去掉「e家宜业」和「卓瓦科技」相关版权 + * +---------------------------------------------------------------------- + * | Author: jixuecong@chowa.cn + * +---------------------------------------------------------------------- + */ + +import redis from 'redis'; +import config from '~/config'; + +const client = config.debug ? null : redis.createClient(config.redis); + +const ERROR = 'REDIS_ERROR'; +const SUCCESS = 'OK'; + +export type ERROR_TYPE = typeof ERROR; +export type SUCCESS_TYPE = typeof SUCCESS; + +const storage = {}; + +export async function set(key: string, value: string): Promise { + if (config.debug) { + storage[key] = value; + + return SUCCESS; + } + + return new Promise(resolve => { + client.set(key, value, (err, reply) => { + if (err) { + resolve(ERROR); + } else { + resolve(reply); + } + }); + }); +} + +export async function get(key: string): Promise { + if (config.debug) { + return storage[key]; + } + + return new Promise(resolve => { + client.get(key, (err, reply) => { + if (err) { + resolve(ERROR); + } else { + resolve(reply); + } + }); + }); +} diff --git a/web/src/views/basic/vistor/detail.vue b/web/src/views/basic/vistor/detail.vue index 1e859d4..8879fd4 100644 --- a/web/src/views/basic/vistor/detail.vue +++ b/web/src/views/basic/vistor/detail.vue @@ -151,7 +151,8 @@ export default { fetching: true, detail: { info: {}, - registrant: {} + registrant: {}, + uid: '' }, now: Date.now() }; @@ -175,7 +176,7 @@ export default { this.detail = res.data; this.$nextTick(() => { - qrcode.toCanvas(this.$refs.canvas, res.data.info.uid, { + qrcode.toCanvas(this.$refs.canvas, res.data.uid, { width: 220, height: 220, margin: 2