ℹ️关于云审批
云审批(cloud approve)
,一款专为小微企业打造,支持多租户的在线审批神器。它简化了申请和审批流程,让您随时随地通过手机或电脑完成请款操作。员工一键提交申请,审批者即时响应,方便快捷。同时,云审批提供全面的数据记录与分析,助力企业实现财务管理透明化、智能化,安全高效,让企业的信息数字化管理变得简单轻松!最后,重要的事情说三遍📢:开源、开源、所有代码开源。
👉GITHUB开源地址 👈
👉飞书在线文档👈
概述
钉钉免登
(此处专指自建H5微应用,官方文档)是一种便捷的登录机制,当用户已在钉钉客户端(包括PC端和移动端)完成登录后,通过工作台访问我们的网站时,系统能够自动识别并完成用户身份验证,无需重复输入登录信息。该功能广泛应用于微信、飞书、抖音等主流平台,为用户提供无缝的跨平台使用体验。
流程详解
数据表
登录模块设计到两个表:账号表/Account、员工表/Staff。
账号/Account
此表为登录到平台的账户信息,支持传统的账密方式、钉钉免登等方式,并记录与之关联的员工ID
字段名 | 中文名 | 类型 | 必填 | 默认值 | 说明 |
---|---|---|---|---|---|
id | 编号 | Int | 是 | 唯一标识 | |
cid | 企业ID | Int | 是 | 关联企业 | |
name | 账号名称 | String | 是 | ||
pwd | 密码 | String | 加密 | ||
type | 类型 | String | 是 | 登录类型 | |
sid | 员工ID | Int | 关联员工 | ||
active | 是否生效 | Boolean | 是 | false | |
addOn | 录入日期 | Int | 是 |
登录类型:
- dingding=钉钉
- wechat=微信
- phone=手机号(未来支持手机验证码登录)
- other=其它(传统密码登录)
员工/Staff
字段名 | 中文名 | 类型 | 必填 | 默认值 | 说明 |
---|---|---|---|---|---|
id | 编号 | Int | 是 | 唯一标识 | |
cid | 企业ID | Int | 是 | 关联企业 | |
name | 员工名称 | String | 是 | ||
phone | 电话号码 | String | |||
summary | 描述 | String |
免登流程
- 准备阶段
- 企业管理员登录
钉钉开发者后台
,创建应用并配置网页功能
- 获取应用的
AppKey
与AppSecrect
- 企业管理员登录
- 逻辑实现
- 新建钉钉登录专用页面(dingding.html)
- 在页面中获取两个参数
cid(企业ID)
、corpId(钉钉内企业ID)
- 前端调用钉钉接口获取授权码/CODE
- 后端拿到上述 CODE 后通过
AppKey
与AppSecrect
获取到用户信息(包含唯一编号 unionid、姓名 name 等) - 构建唯一账户名:D_{unionid}_{name}
- 检查强求账户名是否存在于 Account 表
- 如存在则判断是否生效,若生效返回token,否则报错
- 若不存在
- 自动创建
账户信息
- 检索企业下
同名员工
,若不存在则自动创建并关联到账户对象 - 若配置了账户自动生效,返回 token,否则前端提示账户未激活请联系管理员
- 自动创建
- 部署上线
- 部署平台获取到登录页 URL(https://{域名}/dingding.html)
- 在钉钉后台填入上述地址后发布应用版本
- 用户在钉钉客户端
工作台
添加应用后即可访问
新建 dingding.html
我们在前端项目代码下新建对应页面:
并在 rsbuild.config.mjs
中配置多页面:
export default defineConfig({
source:{
entry:{
index: './src/index.js',
dingding: './src/pages/dingding/index.js'
}
}
})
至此,我们可以通过 http://{IP}/dingding.html
访问到该页面,作为钉钉免登的入口😄。
编写登录页面逻辑
登录页主要组件 App.vue 代码如下:
<template>
<div style="width: 80%; margin: 40px auto;">
<div class="text-center" v-if="!errMsg">
<n-spin :show="working">
<template #description>
钉钉客户端登录中,请稍候...
</template>
</n-spin>
</div>
<n-alert v-else :type show-icon title="钉钉自动登录失败" :bordered="false">
{{ errMsg }}
</n-alert>
</div>
</template>
<script setup>
import { NSpin, NAlert, useMessage, NMessageProvider } from 'naive-ui'
import { requestAuth } from "./dingding"
import { checkLocalToken, saveLocalToken } from "../login"
const msg = useMessage()
let cid = undefined
let corpId = undefined
const debug = import.meta.env.DEV;
let working = ref(true)
let errMsg = ref("")
let type = ref("info")
const onMsg = (msg, isError=true)=>{
errMsg.value = msg
type.value = isError?"error":"info"
}
const tryToAutoLogin = ()=>{
requestAuth(corpId)
.then(code=>{
msg.info(`CODE=${code}`)
fetch(
"/common/login-with-dingding",
{
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ cid, code })
}
)
.then(response => response.json())
.then(({ success, data, message }) => {
if(success==true){
msg.success(`登陆成功`)
saveLocalToken(data)
gotoIndex()
}
else{
const messages = {
E01 : "您的钉钉账户为首次登录,请先联系管理员完成激活",
E02 : "您的钉钉账户关联未激活,请联系企业管理员",
E03 : "找不到当前钉钉账户关联的员工"
}
onMsg(messages[message]||message, !messages[message])
}
})
})
.catch(e=>onMsg(typeof(e)=='string'?e:e.message))
}
const gotoIndex = ()=> location.href = "/"
onMounted(() => {
let u = new URL(location.href)
cid = u.searchParams.get('cid')
corpId = u.searchParams.get('corpId')
if(checkLocalToken())
return gotoIndex()
tryToAutoLogin()
})
</script>
// dingding.js
import { runtime } from 'dingtalk-jsapi'
export const requestAuth = async (corpId)=> {
let { code } = await runtime.permission.requestAuthCode({corpId})
return code
}
//login.js
const NAME = import.meta.env.PUBLIC_HEADER_TOKEN
const CREATED = `${NAME}_CREATED`
/**
* 检查本地 token 是否在有效期内
* @param {Number} expired - token 有效期,默认12小时,单位毫秒
* @returns {Boolean} true 时为 token 有效
*/
export const checkLocalToken = (expired=12*60*60*1000)=>{
let token = localStorage.getItem(NAME)
if(!token) return false
let expire = localStorage.getItem(CREATED)||0
if(Date.now() - expire>=expired)
return false
return true
}
export const saveLocalToken = token=>{
localStorage.setItem(NAME, token)
localStorage.setItem(CREATED, Date.now())
}
这里不得不吐槽下钉钉开发平台的官网文档,新旧版 API 文档特别容易让人混乱,引入dingtalk-jsapi
的话需要查看旧版文档😔。
编写后端与钉钉服务器的交互
const { get, post } = require('axios')
const { loadWithCidAndName } = require("./ConstantService")
const logger = require('../common/logger')
/**
* @typedef {Object} DDTokenResponse - 钉钉获取token效应值
* @property {String} access_token - token值
* @property {Number} expires_in - 有效期(单位秒)
* @property {Number} errcode - 错误代码
* @property {String} errmsg - 错误信息
*
* @typedef {Object} DDUser - 钉钉用户信息
* @property {String} userid
* @property {String} unionid - 唯一编号
* @property {String} name - 用户名称
*
* @typedef {Object} DDUserResponse - 钉钉用户信息响应值
* @property {DDUser} result
* @property {String} request_id
* @property {Number} errcode - 错误代码
* @property {String} errmsg - 错误信息
*/
const DING_HOST = "https://oapi.dingtalk.com"
let localToken = {
value: "",
expire: 0
}
const isTokenExpired = ()=> !localToken.value || localToken.expire<=Date.now()
const log = (msg, level='info')=> logger[level](`[钉钉] ${msg}`)
exports.loginWithCode = async (cid, code)=>{
if(isTokenExpired()){
/**@type {CompanyConfig} */
let cfg = await loadWithCidAndName(cid)
if(!cfg || !(cfg.ddAppKey && cfg.ddAppSecret))
throw `企业未配置钉钉登录`
let url = `${DING_HOST}/gettoken?appkey=${cfg.ddAppKey}&appsecret=${cfg.ddAppSecret}`
/**@type {{data:DDTokenResponse}} */
let { data } = await get(url)
if(data.errcode != 0){
log(`获取企业 token 失败:${data.errcode}|${data.errmsg}`, 'error')
throw data.errmsg
}
localToken.value = data.access_token
localToken.expire = Date.now() + data.expires_in*1000
log(`更新 TOKEN 为 ${localToken.value}(EXPIRED=${data.expires_in})`)
}
let url = `${DING_HOST}/topapi/v2/user/getuserinfo?access_token=${localToken.value}`
/**@type {{data:DDUserResponse}} */
let { data } = await post(url, { code })
if(data.errcode != 0){
log(`[钉钉] 获取用户信息失败:${data.errcode}|${data.errmsg}`, 'error')
throw data.errmsg
}
global.isDebug && log(`获取用户信息 ${data.result.userid}/${data.result.name}`, 'debug')
return data.result
}
部署及上线
创建钉钉H5微应用
- 登录钉钉开发者后台。
- 单击应用开发 > 企业内部应用 > 钉钉应用 > 创建应用。
- 填写应用信息。
配置项 | 是否必填 | 配置说明 |
---|---|---|
应用名称 | 是 | 输入应用名称,应用名称最小长度为 2 个字符。 |
应用描述 | 是 | 简要描述应用提供的产品或服务,应用描述最小长度为 4 个字符。 |
应用图标 | 否 | 上传应用图标,图标要求 JPG/PNG 格式、240 px * 240 px 以上、1:1 、2 MB 以内的无圆角图标。 |
- 单击保存,进入应用详情页。
- 如果你需要开发 AI 应用、小程序、网页应用、酷应用和机器人功能,你需要添加应用能力。
发布应用
创建应用后,需要发布才能看到噢
接着在钉钉客户端
就能看到此应用啦🎉