diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..79a12ff --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true +[*.{js,ts,vue}] +charset = utf-8 # 设置文件字符集为 utf-8 +end_of_line = lf # 控制换行类型(lf | cr | crlf) +insert_final_newline = true # 始终在文件末尾插入一个新行 +indent_style = space # 缩进风格(tab | space) +indent_size = 2 # 缩进大小 +max_line_length = 100 # 最大行长度 + +[*.md] # 仅 md 文件适用以下规则 +max_line_length = off # 关闭最大行长度限制 +trim_trailing_whitespace = false # 关闭末尾空格修剪 diff --git a/.env b/.env new file mode 100644 index 0000000..c6890d4 --- /dev/null +++ b/.env @@ -0,0 +1,37 @@ +# 标题 +VITE_APP_TITLE=衣朵云管理系统 + +# 项目本地运行端口号 +VITE_PORT=80 + +# open 运行 npm run dev 时自动打开浏览器 +VITE_OPEN=true + +# 租户开关 +VITE_APP_TENANT_ENABLE=true + +# 验证码的开关 +VITE_APP_CAPTCHA_ENABLE=true + +# 文档地址的开关 +VITE_APP_DOCALERT_ENABLE=false + +# 百度统计 +VITE_APP_BAIDU_CODE = a1ff8825baa73c3a78eb96aa40325abc + +# 默认账户密码 +VITE_APP_DEFAULT_LOGIN_TENANT = 衣朵云源码 +VITE_APP_DEFAULT_LOGIN_USERNAME = +VITE_APP_DEFAULT_LOGIN_PASSWORD = + +# API 加解密 +VITE_APP_API_ENCRYPT_ENABLE = true +VITE_APP_API_ENCRYPT_HEADER = X-Api-Encrypt +VITE_APP_API_ENCRYPT_ALGORITHM = AES +VITE_APP_API_ENCRYPT_REQUEST_KEY = 52549111389893486934626385991395 +VITE_APP_API_ENCRYPT_RESPONSE_KEY = 96103715984234343991809655248883 +# VITE_APP_API_ENCRYPT_REQUEST_KEY = MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCls2rIpnGdYnLFgz1XU13GbNQ5DloyPpvW00FPGjqn5Z6JpK+kDtVlnkhwR87iRrE5Vf2WNqRX6vzbLSgveIQY8e8oqGCb829myjf1MuI+ZzN4ghf/7tEYhZJGPI9AbfxFqBUzm+kR3/HByAI22GLT96WM26QiMK8n3tIP/yiLswIDAQAB +# VITE_APP_API_ENCRYPT_RESPONSE_KEY = MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAOH8IfIFxL/MR9XIg1UDv5z1fGXQI93E8wrU4iPFovL/sEt9uSgSkjyidC2O7N+m7EKtoN6b1u7cEwXSkwf3kfK0jdWLSQaNpX5YshqXCBzbDfugDaxuyYrNA4/tIMs7mzZAk0APuRXB35Dmupou7Yw7TFW/7QhQmGfzeEKULQvnAgMBAAECgYAw8LqlQGyQoPv5p3gRxEMOCfgL0JzD3XBJKztiRd35RDh40Nx1ejgjW4dPioFwGiVWd2W8cAGHLzALdcQT2KDJh+T/tsd4SPmI6uSBBK6Ff2DkO+kFFcuYvfclQQKqxma5CaZOSqhgenacmgTMFeg2eKlY3symV6JlFNu/IKU42QJBAOhxAK/Eq3e61aYQV2JSguhMR3b8NXJJRroRs/QHEanksJtl+M+2qhkC9nQVXBmBkndnkU/l2tYcHfSBlAyFySMCQQD445tgm/J2b6qMQmuUGQAYDN8FIkHjeKmha+l/fv0igWm8NDlBAem91lNDIPBUzHL1X1+pcts5bjmq99YdOnhtAkAg2J8dN3B3idpZDiQbC8fd5bGPmdI/pSUudAP27uzLEjr2qrE/QPPGdwm2m7IZFJtK7kK1hKio6u48t/bg0iL7AkEAuUUs94h+v702Fnym+jJ2CHEkXvz2US8UDs52nWrZYiM1o1y4tfSHm8H8bv8JCAa9GHyriEawfBraILOmllFdLQJAQSRZy4wmlaG48MhVXodB85X+VZ9krGXZ2TLhz7kz9iuToy53l9jTkESt6L5BfBDCVdIwcXLYgK+8KFdHN5W7HQ== + +# 百度地图 +VITE_BAIDU_MAP_KEY = 'efHIw2qmH8RzHPxK0z0rbCgzDVLup9LD' \ No newline at end of file diff --git a/.env.dev b/.env.dev new file mode 100644 index 0000000..ab28217 --- /dev/null +++ b/.env.dev @@ -0,0 +1,38 @@ +# 开发环境:本地只启动前端项目,依赖开发环境(后端、APP) +NODE_ENV=production + +VITE_DEV=true + +# 请求路径 +VITE_BASE_URL='http://localhost:48080' +#VITE_BASE_URL='http://118.253.178.8:48080' + +# 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持S3服务 +VITE_UPLOAD_TYPE=server + +# 接口地址 +VITE_API_URL=/admin-api + +# 是否删除debugger +VITE_DROP_DEBUGGER=false + +# 是否删除console.log +VITE_DROP_CONSOLE=false + +# 是否sourcemap +VITE_SOURCEMAP=true + +# 打包路径 +VITE_BASE_PATH=/ + +# 输出路径 +VITE_OUT_DIR=dist + +# 商城H5会员端域名 +VITE_MALL_H5_DOMAIN='http://mall.yudao.iocoder.cn' + +# 验证码的开关 +VITE_APP_CAPTCHA_ENABLE=true + +# GoView域名 +VITE_GOVIEW_URL='http://118.253.178.8:3030' \ No newline at end of file diff --git a/.env.local b/.env.local new file mode 100644 index 0000000..f4adcda --- /dev/null +++ b/.env.local @@ -0,0 +1,35 @@ +# 本地开发环境:本地启动所有项目(前端、后端、APP)时使用,不依赖外部环境 +NODE_ENV=development + +VITE_DEV=true + +# 请求路径 +#VITE_BASE_URL='http://118.253.178.8:48080' +VITE_BASE_URL='http://localhost:48080' + +# 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持 S3 服务 +VITE_UPLOAD_TYPE=server + +# 接口地址 +VITE_API_URL=/admin-api + +# 是否删除debugger +VITE_DROP_DEBUGGER=false + +# 是否删除console.log +VITE_DROP_CONSOLE=false + +# 是否sourcemap +VITE_SOURCEMAP=false + +# 打包路径 +VITE_BASE_PATH=/ + +# 商城H5会员端域名 +VITE_MALL_H5_DOMAIN='http://localhost:3000' + +# 验证码的开关 +VITE_APP_CAPTCHA_ENABLE=false + +# GoView域名 +VITE_GOVIEW_URL='http://127.0.0.1:3000' \ No newline at end of file diff --git a/.env.prod b/.env.prod new file mode 100644 index 0000000..5fe4955 --- /dev/null +++ b/.env.prod @@ -0,0 +1,34 @@ +# 生产环境:只在打包时使用 +NODE_ENV=production + +VITE_DEV=true + +# 请求路径 +VITE_BASE_URL='http://118.253.178.8:48080' + +# 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持S3服务 +VITE_UPLOAD_TYPE=server + +# 接口地址 +VITE_API_URL=/admin-api + +# 是否删除debugger +VITE_DROP_DEBUGGER=true + +# 是否删除console.log +VITE_DROP_CONSOLE=true + +# 是否sourcemap +VITE_SOURCEMAP=false + +# 打包路径 +VITE_BASE_PATH=/ + +# 输出路径 +VITE_OUT_DIR=dist-prod + +# 商城H5会员端域名 +VITE_MALL_H5_DOMAIN='http://mall.yudao.iocoder.cn' + +# GoView域名 +VITE_GOVIEW_URL='http://118.253.178.8:3030' \ No newline at end of file diff --git a/.env.stage b/.env.stage new file mode 100644 index 0000000..e3364ec --- /dev/null +++ b/.env.stage @@ -0,0 +1,34 @@ +# 预发布环境:只在打包时使用 +NODE_ENV=production + +VITE_DEV=false + +# 请求路径 +VITE_BASE_URL='http://118.253.178.8:48080' + +# 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持S3服务 +VITE_UPLOAD_TYPE=server + +# 接口地址 +VITE_API_URL=/admin-api + +# 是否删除debugger +VITE_DROP_DEBUGGER=true + +# 是否删除console.log +VITE_DROP_CONSOLE=true + +# 是否sourcemap +VITE_SOURCEMAP=false + +# 打包路径 +VITE_BASE_PATH='http://static-vue3.yudao.iocoder.cn/' + +# 输出路径 +VITE_OUT_DIR=dist-stage + +# 商城H5会员端域名 +VITE_MALL_H5_DOMAIN='http://mall.yudao.iocoder.cn' + +# GoView域名 +VITE_GOVIEW_URL='http://127.0.0.1:3000' \ No newline at end of file diff --git a/.env.test b/.env.test new file mode 100644 index 0000000..2252e14 --- /dev/null +++ b/.env.test @@ -0,0 +1,34 @@ +# 测试环境:只在打包时使用 +NODE_ENV=production + +VITE_DEV=false + +# 请求路径 +VITE_BASE_URL='http://localhost:48080' + +# 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持S3服务 +VITE_UPLOAD_TYPE=server + +# 接口地址 +VITE_API_URL=/admin-api + +# 是否删除debugger +VITE_DROP_DEBUGGER=true + +# 是否删除console.log +VITE_DROP_CONSOLE=true + +# 是否sourcemap +VITE_SOURCEMAP=false + +# 打包路径 +VITE_BASE_PATH=/admin-ui-vue3/ + +# 输出路径 +VITE_OUT_DIR=dist-test + +# 商城H5会员端域名 +VITE_MALL_H5_DOMAIN='http://mall.yudao.iocoder.cn' + +# GoView域名 +VITE_GOVIEW_URL='http://127.0.0.1:3000' \ No newline at end of file diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..1e85c0f --- /dev/null +++ b/.eslintignore @@ -0,0 +1,8 @@ +/build/ +/config/ +/dist/ +/*.js +/test/unit/coverage/ +/node_modules/* +/dist* +/src/main.ts diff --git a/.eslintrc-auto-import.json b/.eslintrc-auto-import.json new file mode 100644 index 0000000..024c96a --- /dev/null +++ b/.eslintrc-auto-import.json @@ -0,0 +1,259 @@ +{ + "globals": { + "EffectScope": true, + "ElMessage": true, + "ElMessageBox": true, + "ElTag": true, + "asyncComputed": true, + "autoResetRef": true, + "computed": true, + "computedAsync": true, + "computedEager": true, + "computedInject": true, + "computedWithControl": true, + "controlledComputed": true, + "controlledRef": true, + "createApp": true, + "createEventHook": true, + "createGlobalState": true, + "createInjectionState": true, + "createReactiveFn": true, + "createSharedComposable": true, + "createUnrefFn": true, + "customRef": true, + "debouncedRef": true, + "debouncedWatch": true, + "defineAsyncComponent": true, + "defineComponent": true, + "eagerComputed": true, + "effectScope": true, + "extendRef": true, + "getCurrentInstance": true, + "getCurrentScope": true, + "h": true, + "ignorableWatch": true, + "inject": true, + "isDefined": true, + "isProxy": true, + "isReactive": true, + "isReadonly": true, + "isRef": true, + "makeDestructurable": true, + "markRaw": true, + "nextTick": true, + "onActivated": true, + "onBeforeMount": true, + "onBeforeUnmount": true, + "onBeforeUpdate": true, + "onClickOutside": true, + "onDeactivated": true, + "onErrorCaptured": true, + "onKeyStroke": true, + "onLongPress": true, + "onMounted": true, + "onRenderTracked": true, + "onRenderTriggered": true, + "onScopeDispose": true, + "onServerPrefetch": true, + "onStartTyping": true, + "onUnmounted": true, + "onUpdated": true, + "pausableWatch": true, + "provide": true, + "reactify": true, + "reactifyObject": true, + "reactive": true, + "reactiveComputed": true, + "reactiveOmit": true, + "reactivePick": true, + "readonly": true, + "ref": true, + "refAutoReset": true, + "refDebounced": true, + "refDefault": true, + "refThrottled": true, + "refWithControl": true, + "resolveComponent": true, + "resolveRef": true, + "resolveUnref": true, + "shallowReactive": true, + "shallowReadonly": true, + "shallowRef": true, + "syncRef": true, + "syncRefs": true, + "templateRef": true, + "throttledRef": true, + "throttledWatch": true, + "toRaw": true, + "toReactive": true, + "toRef": true, + "toRefs": true, + "triggerRef": true, + "tryOnBeforeMount": true, + "tryOnBeforeUnmount": true, + "tryOnMounted": true, + "tryOnScopeDispose": true, + "tryOnUnmounted": true, + "unref": true, + "unrefElement": true, + "until": true, + "useActiveElement": true, + "useArrayEvery": true, + "useArrayFilter": true, + "useArrayFind": true, + "useArrayFindIndex": true, + "useArrayJoin": true, + "useArrayMap": true, + "useArrayReduce": true, + "useArraySome": true, + "useAsyncQueue": true, + "useAsyncState": true, + "useAttrs": true, + "useBase64": true, + "useBattery": true, + "useBluetooth": true, + "useBreakpoints": true, + "useBroadcastChannel": true, + "useBrowserLocation": true, + "useCached": true, + "useClipboard": true, + "useColorMode": true, + "useConfirmDialog": true, + "useCounter": true, + "useCssModule": true, + "useCssVar": true, + "useCssVars": true, + "useCurrentElement": true, + "useCycleList": true, + "useDark": true, + "useDateFormat": true, + "useDebounce": true, + "useDebounceFn": true, + "useDebouncedRefHistory": true, + "useDeviceMotion": true, + "useDeviceOrientation": true, + "useDevicePixelRatio": true, + "useDevicesList": true, + "useDisplayMedia": true, + "useDocumentVisibility": true, + "useDraggable": true, + "useDropZone": true, + "useElementBounding": true, + "useElementByPoint": true, + "useElementHover": true, + "useElementSize": true, + "useElementVisibility": true, + "useEventBus": true, + "useEventListener": true, + "useEventSource": true, + "useEyeDropper": true, + "useFavicon": true, + "useFetch": true, + "useFileDialog": true, + "useFileSystemAccess": true, + "useFocus": true, + "useFocusWithin": true, + "useFps": true, + "useFullscreen": true, + "useGamepad": true, + "useGeolocation": true, + "useIdle": true, + "useImage": true, + "useInfiniteScroll": true, + "useIntersectionObserver": true, + "useInterval": true, + "useIntervalFn": true, + "useKeyModifier": true, + "useLastChanged": true, + "useLocalStorage": true, + "useMagicKeys": true, + "useManualRefHistory": true, + "useMediaControls": true, + "useMediaQuery": true, + "useMemoize": true, + "useMemory": true, + "useMounted": true, + "useMouse": true, + "useMouseInElement": true, + "useMousePressed": true, + "useMutationObserver": true, + "useNavigatorLanguage": true, + "useNetwork": true, + "useNow": true, + "useObjectUrl": true, + "useOffsetPagination": true, + "useOnline": true, + "usePageLeave": true, + "useParallax": true, + "usePermission": true, + "usePointer": true, + "usePointerSwipe": true, + "usePreferredColorScheme": true, + "usePreferredDark": true, + "usePreferredLanguages": true, + "useRafFn": true, + "useRefHistory": true, + "useResizeObserver": true, + "useRoute": true, + "useRouter": true, + "useScreenOrientation": true, + "useScreenSafeArea": true, + "useScriptTag": true, + "useScroll": true, + "useScrollLock": true, + "useSessionStorage": true, + "useShare": true, + "useSlots": true, + "useSpeechRecognition": true, + "useSpeechSynthesis": true, + "useStepper": true, + "useStorage": true, + "useStorageAsync": true, + "useStyleTag": true, + "useSupported": true, + "useSwipe": true, + "useTemplateRefsList": true, + "useTextDirection": true, + "useTextSelection": true, + "useTextareaAutosize": true, + "useThrottle": true, + "useThrottleFn": true, + "useThrottledRefHistory": true, + "useTimeAgo": true, + "useTimeout": true, + "useTimeoutFn": true, + "useTimeoutPoll": true, + "useTimestamp": true, + "useTitle": true, + "useToggle": true, + "useTransition": true, + "useUrlSearchParams": true, + "useUserMedia": true, + "useVModel": true, + "useVModels": true, + "useVibrate": true, + "useVirtualList": true, + "useWakeLock": true, + "useWebNotification": true, + "useWebSocket": true, + "useWebWorker": true, + "useWebWorkerFn": true, + "useWindowFocus": true, + "useWindowScroll": true, + "useWindowSize": true, + "watch": true, + "watchArray": true, + "watchAtMost": true, + "watchDebounced": true, + "watchEffect": true, + "watchIgnorable": true, + "watchOnce": true, + "watchPausable": true, + "watchPostEffect": true, + "watchSyncEffect": true, + "watchThrottled": true, + "watchTriggerable": true, + "watchWithFilter": true, + "whenever": true + } +} diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..b28255c --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,75 @@ +// @ts-check +const { defineConfig } = require('eslint-define-config') +module.exports = defineConfig({ + root: true, + env: { + browser: true, + node: true, + es6: true + }, + parser: 'vue-eslint-parser', + parserOptions: { + parser: '@typescript-eslint/parser', + ecmaVersion: 2020, + sourceType: 'module', + jsxPragma: 'React', + ecmaFeatures: { + jsx: true + } + }, + extends: [ + 'plugin:vue/vue3-recommended', + 'plugin:@typescript-eslint/recommended', + 'prettier', + 'plugin:prettier/recommended', + '@unocss' + ], + rules: { + 'vue/no-setup-props-destructure': 'off', + 'vue/script-setup-uses-vars': 'error', + 'vue/no-reserved-component-names': 'off', + '@typescript-eslint/ban-ts-ignore': 'off', + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-var-requires': 'off', + '@typescript-eslint/no-empty-function': 'off', + 'vue/custom-event-name-casing': 'off', + 'no-use-before-define': 'off', + '@typescript-eslint/no-use-before-define': 'off', + '@typescript-eslint/ban-ts-comment': 'off', + '@typescript-eslint/ban-types': 'off', + '@typescript-eslint/no-non-null-assertion': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', + '@typescript-eslint/no-unused-vars': 'off', + 'no-unused-vars': 'off', + 'space-before-function-paren': 'off', + + 'vue/attributes-order': 'off', + 'vue/one-component-per-file': 'off', + 'vue/html-closing-bracket-newline': 'off', + 'vue/max-attributes-per-line': 'off', + 'vue/multiline-html-element-content-newline': 'off', + 'vue/singleline-html-element-content-newline': 'off', + 'vue/attribute-hyphenation': 'off', + 'vue/require-default-prop': 'off', + 'vue/require-explicit-emits': 'off', + 'vue/require-toggle-inside-transition': 'off', + 'vue/html-self-closing': [ + 'error', + { + html: { + void: 'always', + normal: 'never', + component: 'always' + }, + svg: 'always', + math: 'always' + } + ], + 'vue/multi-word-component-names': 'off', + 'vue/no-v-html': 'off', + 'prettier/prettier': 'off', // 芋艿:默认关闭 prettier 的 ESLint 校验,因为我们使用的是 IDE 的 Prettier 插件 + '@unocss/order': 'off', // 芋艿:禁用 unocss 【css】顺序的提示,因为暂时不需要这么严格,警告也有点繁琐 + '@unocss/order-attributify': 'off' // 芋艿:禁用 unocss 【属性】顺序的提示,因为暂时不需要这么严格,警告也有点繁琐 + } +}) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..848638a --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +node_modules +.DS_Store +dist +dist-ssr +/dist* +pnpm-debug +auto-*.d.ts +.idea +.history diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..f68ea86 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,11 @@ +/node_modules/** +/dist/ +/dist* +/public/* +/docs/* +/vite.config.ts +/src/types/env.d.ts +/src/types/auto-components.d.ts +/src/types/auto-imports.d.ts +/docs/**/* +CHANGELOG diff --git a/.stylelintignore b/.stylelintignore new file mode 100644 index 0000000..aa605b4 --- /dev/null +++ b/.stylelintignore @@ -0,0 +1,6 @@ +/dist/* +/public/* +public/* +/dist* +/src/types/env.d.ts +/docs/**/* diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..65288b5 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,18 @@ +{ + "recommendations": [ + "christian-kohler.path-intellisense", + "vscode-icons-team.vscode-icons", + "davidanson.vscode-markdownlint", + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode", + "mrmlnc.vscode-less", + "lokalise.i18n-ally", + "redhat.vscode-yaml", + "csstools.postcss", + "mikestead.dotenv", + "eamodio.gitlens", + "antfu.iconify", + "antfu.unocss", + "Vue.volar" + ] +} diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..f43edc0 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,16 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "msedge", + "request": "launch", + "name": "Launch Edge against localhost", + "url": "http://localhost", + "webRoot": "${workspaceFolder}/src", + "sourceMaps": true + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..74ab52a --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,146 @@ +{ + "typescript.tsdk": "node_modules/typescript/lib", + "npm.packageManager": "pnpm", + "editor.tabSize": 2, + "prettier.printWidth": 100, // 超过最大值换行 + "editor.defaultFormatter": "esbenp.prettier-vscode", + "files.eol": "\n", + "search.exclude": { + "**/node_modules": true, + "**/*.log": true, + "**/*.log*": true, + "**/bower_components": true, + "**/dist": true, + "**/elehukouben": true, + "**/.git": true, + "**/.gitignore": true, + "**/.svn": true, + "**/.DS_Store": true, + "**/.idea": true, + "**/.vscode": false, + "**/yarn.lock": true, + "**/tmp": true, + "out": true, + "dist": true, + "node_modules": true, + "CHANGELOG.md": true, + "examples": true, + "res": true, + "screenshots": true, + "yarn-error.log": true, + "**/.yarn": true + }, + "files.exclude": { + "**/.cache": true, + "**/.editorconfig": true, + "**/.eslintcache": true, + "**/bower_components": true, + "**/.idea": true, + "**/tmp": true, + "**/.git": true, + "**/.svn": true, + "**/.hg": true, + "**/CVS": true, + "**/.DS_Store": true + }, + "files.watcherExclude": { + "**/.git/objects/**": true, + "**/.git/subtree-cache/**": true, + "**/.vscode/**": true, + "**/node_modules/**": true, + "**/tmp/**": true, + "**/bower_components/**": true, + "**/dist/**": true, + "**/yarn.lock": true + }, + "stylelint.enable": true, + "stylelint.validate": ["css", "less", "postcss", "scss", "vue", "sass"], + "path-intellisense.mappings": { + "@/": "${workspaceRoot}/src" + }, + "[javascriptreact]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[typescript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[typescriptreact]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[html]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[css]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[less]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[scss]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[markdown]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit", + "source.fixAll.stylelint": "explicit" + }, + "editor.formatOnSave": true, + "[vue]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "i18n-ally.localesPaths": ["src/locales"], + "i18n-ally.keystyle": "nested", + "i18n-ally.sortKeys": true, + "i18n-ally.namespace": false, + "i18n-ally.enabledParsers": ["ts"], + "i18n-ally.sourceLanguage": "en", + "i18n-ally.displayLanguage": "zh-CN", + "i18n-ally.enabledFrameworks": ["vue", "react"], + "cSpell.words": [ + "brotli", + "browserslist", + "codemirror", + "commitlint", + "cropperjs", + "echart", + "echarts", + "esnext", + "esno", + "iconify", + "INTLIFY", + "lintstagedrc", + "logicflow", + "nprogress", + "pinia", + "pnpm", + "qrcode", + "sider", + "sortablejs", + "stylelint", + "svgs", + "unocss", + "unplugin", + "unref", + "videojs", + "VITE", + "vitejs", + "vueuse", + "wangeditor", + "xingyu", + "yudao", + "zxcvbn" + ], + // 控制相关文件嵌套展示 + "explorer.fileNesting.enabled": true, + "explorer.fileNesting.expand": false, + "explorer.fileNesting.patterns": { + "*.ts": "$(capture).test.ts, $(capture).test.tsx", + "*.tsx": "$(capture).test.ts, $(capture).test.tsx", + "*.env": "$(capture).env.*", + "package.json": "pnpm-lock.yaml,yarn.lock,LICENSE,README*,CHANGELOG*,CNAME,.gitattributes,.eslintrc-auto-import.json,.gitignore,prettier.config.js,stylelint.config.js,commitlint.config.js,.stylelintignore,.prettierignore,.gitpod.yml,.eslintrc.js,.eslintignore" + }, + "terminal.integrated.scrollback": 10000, + "nuxt.isNuxtApp": false +} diff --git a/backend-idcard/README.md b/backend-idcard/README.md new file mode 100644 index 0000000..190f8e4 --- /dev/null +++ b/backend-idcard/README.md @@ -0,0 +1,51 @@ +# 身份证识别(百度 OCR + AES) + +调用百度身份证 OCR 接口,对图片做 AES 加密上传,对返回的 `result` 密文做 Base64 解码 + AES 解密后解析出所需字段。 + +## 依赖 + +- JDK 8+ +- [fastjson](https://github.com/alibaba/fastjson)(解析返回 JSON) + +## 配置 + +从百度控制台获取: + +- **access_token**:OAuth2 获取(或使用 API Key + Secret Key 换 token) +- **aesKey**:16 位 hex 字符串(控制台身份证 OCR 安全设置里) + +## 使用示例 + +```java +// 构造识别器(accessToken、aesKey 建议从配置文件或环境变量读取) +IdcardRecognizer recognizer = new IdcardRecognizer(accessToken, aesKey); + +// 方式一:本地文件 +IdcardRecognizer.IdcardResult result = recognizer.recognize("/path/to/idcard_front.jpg", "front"); +if (result != null) { + System.out.println("姓名: " + result.getName()); + System.out.println("身份证号: " + result.getIdNumber()); + System.out.println("出生: " + result.getBirth()); + System.out.println("住址: " + result.getAddress()); + // ... +} + +// 方式二:上传的图片字节(如 MultipartFile.getBytes()) +byte[] imgBytes = ...; +IdcardRecognizer.IdcardResult back = recognizer.recognize(imgBytes, "back"); +if (back != null) { + System.out.println("签发机关: " + back.getIssueAuthority()); + System.out.println("有效期限: " + back.getValidDate()); +} +``` + +## 返回字段说明 + +- **正面 (side=front)**:`name` 姓名、`gender` 性别、`nation` 民族、`birth` 出生、`address` 住址、`idNumber` 公民身份号码 +- **反面 (side=back)**:`issueAuthority` 签发机关、`validDate` 有效期限 + +按百度接口约定,请求时 `id_card_side` 传 `front` 或 `back`。 + +## 集成到现有后端 + +将 `com.ydoyun.ocr` 包拷贝到你的项目中,并加入 fastjson 依赖即可;若项目已使用 Spring,可把 `IdcardRecognizer` 做成 Bean,`accessToken`、`aesKey` 从 `@Value` 或配置类注入。 diff --git a/backend-idcard/pom.xml b/backend-idcard/pom.xml new file mode 100644 index 0000000..3ae6409 --- /dev/null +++ b/backend-idcard/pom.xml @@ -0,0 +1,27 @@ + + + 4.0.0 + + com.ydoyun + idcard-ocr + 1.0.0 + jar + Idcard OCR (Baidu) + 百度身份证 OCR 调用(AES 加解密) + + + 1.8 + 1.8 + UTF-8 + + + + + com.alibaba + fastjson + 2.0.43 + + + diff --git a/backend-idcard/src/main/java/cn/iocoder/yudao/module/car/baidu/IdcardRecognizer.java b/backend-idcard/src/main/java/cn/iocoder/yudao/module/car/baidu/IdcardRecognizer.java new file mode 100644 index 0000000..b8d3df3 --- /dev/null +++ b/backend-idcard/src/main/java/cn/iocoder/yudao/module/car/baidu/IdcardRecognizer.java @@ -0,0 +1,316 @@ +package cn.iocoder.yudao.module.car.baidu; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; + +import javax.crypto.Cipher; +import javax.crypto.spec.SecretKeySpec; +import java.io.ByteArrayOutputStream; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Base64; +import java.util.List; +import java.util.Map; + +/** + * 获取 token + 身份证识别(百度 OCR,AES 加解密)。 + */ +public class IdcardRecognizer { + + private static final String IDCARD_OCR_URL = "https://aip.baidubce.com/rest/2.0/ocr/v1/idcard"; + + private final String accessToken; + private byte[] originAesKey; + + public IdcardRecognizer(String accessToken, String aesKey) { + this.accessToken = accessToken; + try { + this.originAesKey = AesKeyUtil.parseAesKey(aesKey); + } catch (Exception e) { + throw new IllegalArgumentException("aesKey 非法,需 16 位 hex", e); + } + } + + // ===================== 获取 token(你原来的逻辑) ===================== + + /** + * 获取权限token + * @return 返回示例: + * { + * "access_token": "24.460da4889caad24cccdb1fea17221975.2592000.1491995545.282335-1234567", + * "expires_in": 2592000 + * } + */ + public static String getAuth() { + String clientId = "mYchXvYyrwXbTscZ0HWfR88s"; + String clientSecret = "AD4QSVg7OMP0GGkcqxkDSwrn7V7rkShN"; + return getAuth(clientId, clientSecret); + } + + /** + * 获取API访问token + * @param ak - 百度云官网获取的 API Key + * @param sk - 百度云官网获取的 Securet Key + * @return assess_token + */ + public static String getAuth(String ak, String sk) { + String authHost = "https://aip.baidubce.com/oauth/2.0/token?"; + String getAccessTokenUrl = authHost + + "grant_type=client_credentials" + + "&client_id=" + ak + + "&client_secret=" + sk; + try { + URL realUrl = new URL(getAccessTokenUrl); + HttpURLConnection connection = (HttpURLConnection) realUrl.openConnection(); + connection.setRequestMethod("GET"); + connection.connect(); + Map> map = connection.getHeaderFields(); + for (String key : map.keySet()) { + System.err.println(key + "--->" + map.get(key)); + } + BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream())); + String result = ""; + String line; + while ((line = in.readLine()) != null) { + result += line; + } + System.err.println("result:" + result); + JSONObject jsonObject = JSONObject.parseObject(result); + String access_token = jsonObject.getString("access_token"); + return access_token; + } catch (Exception e) { + System.err.printf("获取token失败!"); + e.printStackTrace(System.err); + } + return null; + } + + // ===================== 身份证识别(AES 加解密) ===================== + + /** + * 识别身份证图片(本地文件路径) + * @param filePath 图片本地路径 + * @param side 正面 "front" 或反面 "back" + * @return 解析后的身份证字段,识别失败返回 null + */ + public IdcardResult recognize(String filePath, String side) throws Exception { + byte[] imgData = Files.readAllBytes(Paths.get(filePath)); + return recognize(imgData, side); + } + + /** + * 识别身份证图片(字节数组,适合上传场景) + * @param imgData 图片字节 + * @param side 正面 "front" 或反面 "back" + * @return 解析后的身份证字段,识别失败返回 null + */ + public IdcardResult recognize(byte[] imgData, String side) throws Exception { + String plainJson = recognizePlainJson(imgData, side); + return (plainJson == null || plainJson.isEmpty()) ? null : parseToResult(plainJson, side); + } + + /** + * 识别并返回解密后的明文 JSON(便于调试/落库)。 + */ + public String recognizePlainJson(byte[] imgData, String side) throws Exception { + String imgStr = encryptImg(imgData); + String imgParam = URLEncoder.encode(imgStr, "UTF-8"); + String body = "id_card_side=" + side + "&image=" + imgParam + "&AESEncry=true"; + + String rawResponse = HttpUtil.post(IDCARD_OCR_URL, accessToken, body); + if (rawResponse == null || rawResponse.isEmpty()) { + return null; + } + return decryptResult(rawResponse); + } + + private String encryptImg(byte[] imgData) throws Exception { + byte[] encImgBytes = AesUtil.encrypt(imgData, originAesKey); + return Base64.getEncoder().encodeToString(encImgBytes); + } + + private String decryptResult(String encryptResponse) throws Exception { + JSONObject obj = JSON.parseObject(encryptResponse); + String result = obj.getString("result"); + if (result == null) return null; + byte[] arr = Base64.getDecoder().decode(result); + return new String(AesUtil.decrypt(arr, originAesKey), "UTF-8"); + } + + private IdcardResult parseToResult(String plainJson, String side) { + JSONObject root = JSON.parseObject(plainJson); + JSONObject wordsResult = root.getJSONObject("words_result"); + if (wordsResult == null) return null; + + IdcardResult result = new IdcardResult(); + result.setSide("front".equalsIgnoreCase(side) ? "front" : "back"); + + if ("front".equalsIgnoreCase(side)) { + result.setName(getWords(wordsResult, "姓名")); + result.setGender(getWords(wordsResult, "性别")); + result.setNation(getWords(wordsResult, "民族")); + result.setBirth(getWords(wordsResult, "出生")); + result.setAddress(getWords(wordsResult, "住址")); + result.setIdNumber(getWords(wordsResult, "公民身份号码")); + } else { + result.setIssueAuthority(getWords(wordsResult, "签发机关")); + result.setValidDate(getWords(wordsResult, "有效期限")); + } + + return result; + } + + private static String getWords(JSONObject wordsResult, String key) { + if (wordsResult == null) return null; + JSONObject item = wordsResult.getJSONObject(key); + return item == null ? null : item.getString("words"); + } + + // --------------- 结果 DTO --------------- + + public static class IdcardResult { + private String side; + private String name; + private String gender; + private String nation; + private String birth; + private String address; + private String idNumber; + private String issueAuthority; + private String validDate; + + public String getSide() { return side; } + public void setSide(String side) { this.side = side; } + public String getName() { return name; } + public void setName(String name) { this.name = name; } + public String getGender() { return gender; } + public void setGender(String gender) { this.gender = gender; } + public String getNation() { return nation; } + public void setNation(String nation) { this.nation = nation; } + public String getBirth() { return birth; } + public void setBirth(String birth) { this.birth = birth; } + public String getAddress() { return address; } + public void setAddress(String address) { this.address = address; } + public String getIdNumber() { return idNumber; } + public void setIdNumber(String idNumber) { this.idNumber = idNumber; } + public String getIssueAuthority() { return issueAuthority; } + public void setIssueAuthority(String issueAuthority) { this.issueAuthority = issueAuthority; } + public String getValidDate() { return validDate; } + public void setValidDate(String validDate) { this.validDate = validDate; } + } + + // --------------- 工具类 --------------- + + static class HttpUtil { + static String post(String url, String accessToken, String formBody) throws IOException { + String fullUrl = url + "?access_token=" + URLEncoder.encode(accessToken, "UTF-8"); + byte[] bodyBytes = formBody.getBytes(StandardCharsets.UTF_8); + + HttpURLConnection conn = (HttpURLConnection) new URL(fullUrl).openConnection(); + conn.setRequestMethod("POST"); + conn.setConnectTimeout(15000); + conn.setReadTimeout(30000); + conn.setDoOutput(true); + conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8"); + conn.setRequestProperty("Accept", "application/json"); + + OutputStream os = conn.getOutputStream(); + try { + os.write(bodyBytes); + } finally { + os.close(); + } + + int code = conn.getResponseCode(); + InputStream is = (code >= 200 && code < 300) ? conn.getInputStream() : conn.getErrorStream(); + if (is == null) return null; + try { + byte[] bytes = readAllBytes(is); + return new String(bytes, "UTF-8"); + } finally { + conn.disconnect(); + } + } + + private static byte[] readAllBytes(InputStream in) throws IOException { + byte[] buf = new byte[4096]; + int n; + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + while ((n = in.read(buf)) >= 0) { + if (n > 0) bos.write(buf, 0, n); + } + return bos.toByteArray(); + } + } + + static class AesKeyUtil { + private static final String HEX = "0123456789abcdef"; + + static byte[] parseAesKey(String hex) throws Exception { + if (hex == null || hex.length() != 16) { + throw new Exception("aes key 需为 16 位"); + } + char[] data = hex.toCharArray(); + byte[] out = new byte[data.length]; + for (int i = 0; i < data.length; i++) { + int f = HEX.indexOf(data[i]); + if (f < 0) throw new Exception("aes key 需为 hex 字符"); + out[i] = (byte) f; + } + return out; + } + } + + static class AesUtil { + private static final String ALGORITHM = "AES"; + private static final String ALGORITHM_STR = "AES/ECB/PKCS5Padding"; + + static byte[] encrypt(byte[] src, byte[] aesKey) throws Exception { + Cipher cipher = getCipher(aesKey, Cipher.ENCRYPT_MODE); + return cipher.doFinal(src); + } + + static byte[] decrypt(byte[] src, byte[] aesKey) throws Exception { + Cipher cipher = getCipher(aesKey, Cipher.DECRYPT_MODE); + return cipher.doFinal(src); + } + + private static Cipher getCipher(byte[] aesKey, int mode) throws Exception { + SecretKeySpec spec = new SecretKeySpec(aesKey, ALGORITHM); + Cipher cipher = Cipher.getInstance(ALGORITHM_STR); + cipher.init(mode, spec); + return cipher; + } + } + + // --------------- 测试 main(改下面三个变量后直接运行) --------------- + + public static void main(String[] args) throws Exception { + // 1. 用你原来的 getAuth 拿 token + String accessToken = getAuth(); + if (accessToken == null) { + System.out.println("获取 access_token 失败"); + return; + } + + // 2. 从控制台拿 16 位 aesKey(hex 字符串) + String aesKey = "填写16位aesKey"; + String filePath = "填写身份证图片路径"; + String side = "front"; // front 正面 / back 反面 + + IdcardRecognizer recognizer = new IdcardRecognizer(accessToken, aesKey); + IdcardResult result = recognizer.recognize(filePath, side); + + System.out.println("========== 识别结果 =========="); + System.out.println(JSON.toJSONString(result, true)); + } +} diff --git a/backend-idcard/src/main/java/com/ydoyun/ocr/IdcardRecognizer.java b/backend-idcard/src/main/java/com/ydoyun/ocr/IdcardRecognizer.java new file mode 100644 index 0000000..2a58050 --- /dev/null +++ b/backend-idcard/src/main/java/com/ydoyun/ocr/IdcardRecognizer.java @@ -0,0 +1,254 @@ +package com.ydoyun.ocr; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; + +import javax.crypto.Cipher; +import javax.crypto.spec.SecretKeySpec; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Base64; + +/** + * 身份证识别器:调用百度 OCR 身份证接口(AES 加密传输),解析并返回所需字段。 + * 需配置:accessToken、aesKey(16 位 hex),可从百度控制台获取。 + */ +public class IdcardRecognizer { + + private static final String IDCARD_OCR_URL = "https://aip.baidubce.com/rest/2.0/ocr/v1/idcard"; + + private final String accessToken; + private byte[] originAesKey; + + public IdcardRecognizer(String accessToken, String aesKey) { + this.accessToken = accessToken; + try { + this.originAesKey = AesKeyUtil.parseAesKey(aesKey); + } catch (Exception e) { + throw new IllegalArgumentException("aesKey 非法,需 16 位 hex", e); + } + } + + /** + * 识别身份证图片(本地文件路径) + * + * @param filePath 图片本地路径 + * @param side 正面 "front" 或反面 "back" + * @return 解析后的身份证字段,识别失败返回 null + */ + public IdcardResult recognize(String filePath, String side) throws IOException, Exception { + byte[] imgData = Files.readAllBytes(Paths.get(filePath)); + return recognize(imgData, side); + } + + /** + * 识别身份证图片(字节数组,适合上传场景) + * + * @param imgData 图片字节 + * @param side 正面 "front" 或反面 "back" + * @return 解析后的身份证字段,识别失败返回 null + */ + public IdcardResult recognize(byte[] imgData, String side) throws Exception { + String plainJson = recognizePlainJson(imgData, side); + return (plainJson == null || plainJson.isEmpty()) ? null : parseToResult(plainJson, side); + } + + /** + * 识别并返回解密后的明文 JSON(便于调试/落库/自行解析)。 + */ + public String recognizePlainJson(byte[] imgData, String side) throws Exception { + String imgStr = encryptImg(imgData); + String imgParam = URLEncoder.encode(imgStr, StandardCharsets.UTF_8.name()); + String body = "id_card_side=" + side + "&image=" + imgParam + "&AESEncry=true"; + + String rawResponse = HttpUtil.post(IDCARD_OCR_URL, accessToken, body); + if (rawResponse == null || rawResponse.isEmpty()) { + return null; + } + return decryptResult(rawResponse); + } + + private String encryptImg(byte[] imgData) throws Exception { + byte[] encImgBytes = AesUtil.encrypt(imgData, originAesKey); + return Base64.getEncoder().encodeToString(encImgBytes); + } + + private String decryptResult(String encryptResponse) throws Exception { + JSONObject obj = JSON.parseObject(encryptResponse); + String result = obj.getString("result"); + if (result == null) return null; + byte[] arr = Base64.getDecoder().decode(result); + return new String(AesUtil.decrypt(arr, originAesKey), StandardCharsets.UTF_8); + } + + private IdcardResult parseToResult(String plainJson, String side) { + JSONObject root = JSON.parseObject(plainJson); + JSONObject wordsResult = root.getJSONObject("words_result"); + if (wordsResult == null) return null; + + IdcardResult result = new IdcardResult(); + result.setSide("front".equalsIgnoreCase(side) ? "front" : "back"); + + if ("front".equalsIgnoreCase(side)) { + result.setName(getWords(wordsResult, "姓名")); + result.setGender(getWords(wordsResult, "性别")); + result.setNation(getWords(wordsResult, "民族")); + result.setBirth(getWords(wordsResult, "出生")); + result.setAddress(getWords(wordsResult, "住址")); + result.setIdNumber(getWords(wordsResult, "公民身份号码")); + } else { + result.setIssueAuthority(getWords(wordsResult, "签发机关")); + result.setValidDate(getWords(wordsResult, "有效期限")); + } + + return result; + } + + private static String getWords(JSONObject wordsResult, String key) { + if (wordsResult == null) return null; + JSONObject item = wordsResult.getJSONObject(key); + return item == null ? null : item.getString("words"); + } + + // --------------- 结果 DTO --------------- + + public static class IdcardResult { + private String side; + private String name; + private String gender; + private String nation; + private String birth; + private String address; + private String idNumber; + private String issueAuthority; + private String validDate; + + public String getSide() { return side; } + public void setSide(String side) { this.side = side; } + public String getName() { return name; } + public void setName(String name) { this.name = name; } + public String getGender() { return gender; } + public void setGender(String gender) { this.gender = gender; } + public String getNation() { return nation; } + public void setNation(String nation) { this.nation = nation; } + public String getBirth() { return birth; } + public void setBirth(String birth) { this.birth = birth; } + public String getAddress() { return address; } + public void setAddress(String address) { this.address = address; } + public String getIdNumber() { return idNumber; } + public void setIdNumber(String idNumber) { this.idNumber = idNumber; } + public String getIssueAuthority() { return issueAuthority; } + public void setIssueAuthority(String issueAuthority) { this.issueAuthority = issueAuthority; } + public String getValidDate() { return validDate; } + public void setValidDate(String validDate) { this.validDate = validDate; } + } + + // --------------- 工具类 --------------- + + static class HttpUtil { + static String post(String url, String accessToken, String formBody) throws IOException { + String fullUrl = url + "?access_token=" + URLEncoder.encode(accessToken, "UTF-8"); + byte[] bodyBytes = formBody.getBytes(StandardCharsets.UTF_8); + + HttpURLConnection conn = (HttpURLConnection) new URL(fullUrl).openConnection(); + conn.setRequestMethod("POST"); + conn.setConnectTimeout(15_000); + conn.setReadTimeout(30_000); + conn.setDoOutput(true); + conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8"); + conn.setRequestProperty("Accept", "application/json"); + + try (OutputStream os = conn.getOutputStream()) { + os.write(bodyBytes); + } + + int code = conn.getResponseCode(); + InputStream is = (code >= 200 && code < 300) ? conn.getInputStream() : conn.getErrorStream(); + if (is == null) return null; + try (InputStream in = is) { + byte[] bytes = readAllBytes(in); + return new String(bytes, StandardCharsets.UTF_8); + } finally { + conn.disconnect(); + } + } + + private static byte[] readAllBytes(InputStream in) throws IOException { + byte[] buf = new byte[4096]; + int n; + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + while ((n = in.read(buf)) >= 0) { + if (n > 0) bos.write(buf, 0, n); + } + return bos.toByteArray(); + } + } + + static class AesKeyUtil { + private static final String HEX = "0123456789abcdef"; + + static byte[] parseAesKey(String hex) throws Exception { + if (hex == null || hex.length() != 16) { + throw new Exception("aes key 需为 16 位"); + } + char[] data = hex.toCharArray(); + byte[] out = new byte[data.length]; + for (int i = 0; i < data.length; i++) { + int f = HEX.indexOf(data[i]); + if (f < 0) throw new Exception("aes key 需为 hex 字符"); + out[i] = (byte) f; + } + return out; + } + } + + static class AesUtil { + private static final String ALGORITHM = "AES"; + private static final String ALGORITHM_STR = "AES/ECB/PKCS5Padding"; + + static byte[] encrypt(byte[] src, byte[] aesKey) throws Exception { + Cipher cipher = getCipher(aesKey, Cipher.ENCRYPT_MODE); + return cipher.doFinal(src); + } + + static byte[] decrypt(byte[] src, byte[] aesKey) throws Exception { + Cipher cipher = getCipher(aesKey, Cipher.DECRYPT_MODE); + return cipher.doFinal(src); + } + + private static Cipher getCipher(byte[] aesKey, int mode) throws Exception { + SecretKeySpec spec = new SecretKeySpec(aesKey, ALGORITHM); + Cipher cipher = Cipher.getInstance(ALGORITHM_STR); + cipher.init(mode, spec); + return cipher; + } + } + + /** + * 本地快速测试入口(JDK8 可运行,直接改下面四个变量再运行)。 + */ + public static void main(String[] args) throws Exception { + // TODO:替换成你自己的配置 + String accessToken = "填写你的 access_token"; + String aesKey = "填写 16 位 aesKey(hex 字符串)"; + String side = "front"; // front 正面 / back 反面 + String filePath = "填写身份证图片本地路径"; + + IdcardRecognizer recognizer = new IdcardRecognizer(accessToken, aesKey); + IdcardResult result = recognizer.recognize(filePath, side); + System.out.println(JSON.toJSONString(result, true)); + + // 如果你要看解密后的明文 JSON(排查字段/定位问题),放开下面三行: + // byte[] img = Files.readAllBytes(Paths.get(filePath)); + // String plain = recognizer.recognizePlainJson(img, side); + // System.out.println(plain); + } +} diff --git a/backend-idcard/target/classes/cn/iocoder/yudao/module/car/baidu/IdcardRecognizer$AesKeyUtil.class b/backend-idcard/target/classes/cn/iocoder/yudao/module/car/baidu/IdcardRecognizer$AesKeyUtil.class new file mode 100644 index 0000000..8027190 Binary files /dev/null and b/backend-idcard/target/classes/cn/iocoder/yudao/module/car/baidu/IdcardRecognizer$AesKeyUtil.class differ diff --git a/backend-idcard/target/classes/cn/iocoder/yudao/module/car/baidu/IdcardRecognizer$AesUtil.class b/backend-idcard/target/classes/cn/iocoder/yudao/module/car/baidu/IdcardRecognizer$AesUtil.class new file mode 100644 index 0000000..961af2b Binary files /dev/null and b/backend-idcard/target/classes/cn/iocoder/yudao/module/car/baidu/IdcardRecognizer$AesUtil.class differ diff --git a/backend-idcard/target/classes/cn/iocoder/yudao/module/car/baidu/IdcardRecognizer$HttpUtil.class b/backend-idcard/target/classes/cn/iocoder/yudao/module/car/baidu/IdcardRecognizer$HttpUtil.class new file mode 100644 index 0000000..6dc3a57 Binary files /dev/null and b/backend-idcard/target/classes/cn/iocoder/yudao/module/car/baidu/IdcardRecognizer$HttpUtil.class differ diff --git a/backend-idcard/target/classes/cn/iocoder/yudao/module/car/baidu/IdcardRecognizer$IdcardResult.class b/backend-idcard/target/classes/cn/iocoder/yudao/module/car/baidu/IdcardRecognizer$IdcardResult.class new file mode 100644 index 0000000..57bb716 Binary files /dev/null and b/backend-idcard/target/classes/cn/iocoder/yudao/module/car/baidu/IdcardRecognizer$IdcardResult.class differ diff --git a/backend-idcard/target/classes/cn/iocoder/yudao/module/car/baidu/IdcardRecognizer.class b/backend-idcard/target/classes/cn/iocoder/yudao/module/car/baidu/IdcardRecognizer.class new file mode 100644 index 0000000..f1c994c Binary files /dev/null and b/backend-idcard/target/classes/cn/iocoder/yudao/module/car/baidu/IdcardRecognizer.class differ diff --git a/backend-idcard/target/classes/com/ydoyun/ocr/IdcardRecognizer$AesKeyUtil.class b/backend-idcard/target/classes/com/ydoyun/ocr/IdcardRecognizer$AesKeyUtil.class new file mode 100644 index 0000000..4ada677 Binary files /dev/null and b/backend-idcard/target/classes/com/ydoyun/ocr/IdcardRecognizer$AesKeyUtil.class differ diff --git a/backend-idcard/target/classes/com/ydoyun/ocr/IdcardRecognizer$AesUtil.class b/backend-idcard/target/classes/com/ydoyun/ocr/IdcardRecognizer$AesUtil.class new file mode 100644 index 0000000..2e5bf68 Binary files /dev/null and b/backend-idcard/target/classes/com/ydoyun/ocr/IdcardRecognizer$AesUtil.class differ diff --git a/backend-idcard/target/classes/com/ydoyun/ocr/IdcardRecognizer$HttpUtil.class b/backend-idcard/target/classes/com/ydoyun/ocr/IdcardRecognizer$HttpUtil.class new file mode 100644 index 0000000..c6f8c4d Binary files /dev/null and b/backend-idcard/target/classes/com/ydoyun/ocr/IdcardRecognizer$HttpUtil.class differ diff --git a/backend-idcard/target/classes/com/ydoyun/ocr/IdcardRecognizer$IdcardResult.class b/backend-idcard/target/classes/com/ydoyun/ocr/IdcardRecognizer$IdcardResult.class new file mode 100644 index 0000000..1f2e292 Binary files /dev/null and b/backend-idcard/target/classes/com/ydoyun/ocr/IdcardRecognizer$IdcardResult.class differ diff --git a/backend-idcard/target/classes/com/ydoyun/ocr/IdcardRecognizer.class b/backend-idcard/target/classes/com/ydoyun/ocr/IdcardRecognizer.class new file mode 100644 index 0000000..6d85f09 Binary files /dev/null and b/backend-idcard/target/classes/com/ydoyun/ocr/IdcardRecognizer.class differ diff --git a/backend-idcard/target/idcard-ocr-1.0.0.jar b/backend-idcard/target/idcard-ocr-1.0.0.jar new file mode 100644 index 0000000..40b9caf Binary files /dev/null and b/backend-idcard/target/idcard-ocr-1.0.0.jar differ diff --git a/backend-idcard/target/maven-archiver/pom.properties b/backend-idcard/target/maven-archiver/pom.properties new file mode 100644 index 0000000..14546db --- /dev/null +++ b/backend-idcard/target/maven-archiver/pom.properties @@ -0,0 +1,3 @@ +artifactId=idcard-ocr +groupId=com.ydoyun +version=1.0.0 diff --git a/backend-idcard/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst b/backend-idcard/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst new file mode 100644 index 0000000..5df64d8 --- /dev/null +++ b/backend-idcard/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst @@ -0,0 +1,5 @@ +com/ydoyun/ocr/IdcardRecognizer$HttpUtil.class +com/ydoyun/ocr/IdcardRecognizer$AesKeyUtil.class +com/ydoyun/ocr/IdcardRecognizer$IdcardResult.class +com/ydoyun/ocr/IdcardRecognizer$AesUtil.class +com/ydoyun/ocr/IdcardRecognizer.class diff --git a/backend-idcard/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst b/backend-idcard/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst new file mode 100644 index 0000000..504013f --- /dev/null +++ b/backend-idcard/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst @@ -0,0 +1 @@ +/Users/ouhaolan/project/百盛科技/报表/yudao-ui-admin-vue3/backend-idcard/src/main/java/com/ydoyun/ocr/IdcardRecognizer.java diff --git a/build/vite/index.ts b/build/vite/index.ts new file mode 100644 index 0000000..585759f --- /dev/null +++ b/build/vite/index.ts @@ -0,0 +1,100 @@ +import { resolve } from 'path' +import Vue from '@vitejs/plugin-vue' +import VueJsx from '@vitejs/plugin-vue-jsx' +import progress from 'vite-plugin-progress' +import EslintPlugin from 'vite-plugin-eslint' +import PurgeIcons from 'vite-plugin-purge-icons' +import { ViteEjsPlugin } from 'vite-plugin-ejs' +// @ts-ignore +import ElementPlus from 'unplugin-element-plus/vite' +import AutoImport from 'unplugin-auto-import/vite' +import Components from 'unplugin-vue-components/vite' +import { ElementPlusResolver } from 'unplugin-vue-components/resolvers' +import viteCompression from 'vite-plugin-compression' +import topLevelAwait from 'vite-plugin-top-level-await' +import VueI18nPlugin from '@intlify/unplugin-vue-i18n/vite' +import { createSvgIconsPlugin } from 'vite-plugin-svg-icons' +import UnoCSS from 'unocss/vite' + +export function createVitePlugins() { + const root = process.cwd() + + // 路径查找 + function pathResolve(dir: string) { + return resolve(root, '.', dir) + } + + return [ + Vue(), + VueJsx(), + UnoCSS(), + progress(), + PurgeIcons(), + ElementPlus({}), + AutoImport({ + include: [ + /\.[tj]sx?$/, // .ts, .tsx, .js, .jsx + /\.vue$/, + /\.vue\?vue/, // .vue + /\.md$/ // .md + ], + imports: [ + 'vue', + 'vue-router', + // 可额外添加需要 autoImport 的组件 + { + '@/hooks/web/useI18n': ['useI18n'], + '@/hooks/web/useMessage': ['useMessage'], + '@/hooks/web/useTable': ['useTable'], + '@/hooks/web/useCrudSchemas': ['useCrudSchemas'], + '@/utils/formRules': ['required'], + '@/utils/dict': ['DICT_TYPE'] + } + ], + dts: 'src/types/auto-imports.d.ts', + resolvers: [ElementPlusResolver()], + eslintrc: { + enabled: false, // Default `false` + filepath: './.eslintrc-auto-import.json', // Default `./.eslintrc-auto-import.json` + globalsPropValue: true // Default `true`, (true | false | 'readonly' | 'readable' | 'writable' | 'writeable') + } + }), + Components({ + // 生成自定义 `auto-components.d.ts` 全局声明 + dts: 'src/types/auto-components.d.ts', + // 自定义组件的解析器 + resolvers: [ElementPlusResolver()], + globs: ["src/components/**/**.{vue, md}", '!src/components/DiyEditor/components/mobile/**'] + }), + EslintPlugin({ + cache: false, + include: ['src/**/*.vue', 'src/**/*.ts', 'src/**/*.tsx'] // 检查的文件 + }), + VueI18nPlugin({ + runtimeOnly: true, + compositionOnly: true, + include: [resolve(__dirname, 'src/locales/**')] + }), + createSvgIconsPlugin({ + iconDirs: [pathResolve('src/assets/svgs')], + symbolId: 'icon-[dir]-[name]', + svgoOptions: true + }), + viteCompression({ + verbose: true, // 是否在控制台输出压缩结果 + disable: false, // 是否禁用 + threshold: 10240, // 体积大于 threshold 才会被压缩,单位 b + algorithm: 'gzip', // 压缩算法,可选 [ 'gzip' , 'brotliCompress' ,'deflate' , 'deflateRaw'] + ext: '.gz', // 生成的压缩包后缀 + deleteOriginFile: false //压缩后是否删除源文件 + }), + ViteEjsPlugin(), + topLevelAwait({ + // https://juejin.cn/post/7152191742513512485 + // The export name of top-level await promise for each chunk module + promiseExportName: '__tla', + // The function to generate import names of top-level await promise in each chunk module + promiseImportName: (i) => `__tla_${i}` + }) + ] +} diff --git a/build/vite/optimize.ts b/build/vite/optimize.ts new file mode 100644 index 0000000..aa7e68c --- /dev/null +++ b/build/vite/optimize.ts @@ -0,0 +1,122 @@ +const include = [ + 'qs', + 'url', + 'vue', + 'sass', + 'mitt', + 'axios', + 'pinia', + 'dayjs', + 'qrcode', + 'unocss', + 'vue-router', + 'vue-types', + 'vue-i18n', + 'crypto-js', + 'cropperjs', + 'lodash-es', + 'nprogress', + 'web-storage-cache', + '@iconify/iconify', + '@vueuse/core', + '@zxcvbn-ts/core', + 'echarts/core', + 'echarts/charts', + 'echarts/components', + 'echarts/renderers', + 'echarts-wordcloud', + '@wangeditor/editor', + '@wangeditor/editor-for-vue', + '@microsoft/fetch-event-source', + 'markdown-it', + 'markmap-view', + 'markmap-lib', + 'markmap-toolbar', + 'highlight.js', + 'element-plus', + 'element-plus/es', + 'element-plus/es/locale/lang/zh-cn', + 'element-plus/es/locale/lang/en', + 'element-plus/es/components/avatar/style/css', + 'element-plus/es/components/space/style/css', + 'element-plus/es/components/backtop/style/css', + 'element-plus/es/components/form/style/css', + 'element-plus/es/components/radio-group/style/css', + 'element-plus/es/components/radio/style/css', + 'element-plus/es/components/checkbox/style/css', + 'element-plus/es/components/checkbox-group/style/css', + 'element-plus/es/components/switch/style/css', + 'element-plus/es/components/time-picker/style/css', + 'element-plus/es/components/date-picker/style/css', + 'element-plus/es/components/descriptions/style/css', + 'element-plus/es/components/descriptions-item/style/css', + 'element-plus/es/components/link/style/css', + 'element-plus/es/components/tooltip/style/css', + 'element-plus/es/components/drawer/style/css', + 'element-plus/es/components/dialog/style/css', + 'element-plus/es/components/checkbox-button/style/css', + 'element-plus/es/components/option-group/style/css', + 'element-plus/es/components/radio-button/style/css', + 'element-plus/es/components/cascader/style/css', + 'element-plus/es/components/color-picker/style/css', + 'element-plus/es/components/input-number/style/css', + 'element-plus/es/components/rate/style/css', + 'element-plus/es/components/select-v2/style/css', + 'element-plus/es/components/tree-select/style/css', + 'element-plus/es/components/slider/style/css', + 'element-plus/es/components/time-select/style/css', + 'element-plus/es/components/autocomplete/style/css', + 'element-plus/es/components/image-viewer/style/css', + 'element-plus/es/components/upload/style/css', + 'element-plus/es/components/col/style/css', + 'element-plus/es/components/form-item/style/css', + 'element-plus/es/components/alert/style/css', + 'element-plus/es/components/breadcrumb/style/css', + 'element-plus/es/components/select/style/css', + 'element-plus/es/components/input/style/css', + 'element-plus/es/components/breadcrumb-item/style/css', + 'element-plus/es/components/tag/style/css', + 'element-plus/es/components/pagination/style/css', + 'element-plus/es/components/table/style/css', + 'element-plus/es/components/table-v2/style/css', + 'element-plus/es/components/table-column/style/css', + 'element-plus/es/components/card/style/css', + 'element-plus/es/components/row/style/css', + 'element-plus/es/components/button/style/css', + 'element-plus/es/components/menu/style/css', + 'element-plus/es/components/sub-menu/style/css', + 'element-plus/es/components/menu-item/style/css', + 'element-plus/es/components/option/style/css', + 'element-plus/es/components/dropdown/style/css', + 'element-plus/es/components/dropdown-menu/style/css', + 'element-plus/es/components/dropdown-item/style/css', + 'element-plus/es/components/skeleton/style/css', + 'element-plus/es/components/skeleton/style/css', + 'element-plus/es/components/backtop/style/css', + 'element-plus/es/components/menu/style/css', + 'element-plus/es/components/sub-menu/style/css', + 'element-plus/es/components/menu-item/style/css', + 'element-plus/es/components/dropdown/style/css', + 'element-plus/es/components/tree/style/css', + 'element-plus/es/components/dropdown-menu/style/css', + 'element-plus/es/components/dropdown-item/style/css', + 'element-plus/es/components/badge/style/css', + 'element-plus/es/components/breadcrumb/style/css', + 'element-plus/es/components/breadcrumb-item/style/css', + 'element-plus/es/components/image/style/css', + 'element-plus/es/components/collapse-transition/style/css', + 'element-plus/es/components/timeline/style/css', + 'element-plus/es/components/timeline-item/style/css', + 'element-plus/es/components/collapse/style/css', + 'element-plus/es/components/collapse-item/style/css', + 'element-plus/es/components/button-group/style/css', + 'element-plus/es/components/text/style/css', + 'element-plus/es/components/segmented/style/css', + '@element-plus/icons-vue', + 'element-plus/es/components/footer/style/css', + 'element-plus/es/components/empty/style/css' +] + +const exclude = ['@iconify/json'] + +export { include, exclude } diff --git a/src/views/ydoyun/customtag/CustomTagFormWithParams.vue b/src/views/ydoyun/customtag/CustomTagFormWithParams.vue new file mode 100644 index 0000000..3866199 --- /dev/null +++ b/src/views/ydoyun/customtag/CustomTagFormWithParams.vue @@ -0,0 +1,311 @@ + + + + + + 用于报表中展示的标签名称 + + + + 报表中引用的表达式,支持 ${字段名} 格式 + + + + + + + + + + + + 用于报表展示时的颜色标识 + + + + 是 + 否 + + 是否将报表参数代入 SQL 中执行 + + + + 需代入的参数名,多个用逗号分隔 + + + + + 插入参数变量: + + + + + 插入 + + + + + 预览(参数以块显示): + + + + {{ seg.text }} + + {{ seg.text }} + + + + + + 参数变量通过下拉选择插入,执行时 ${参数1}、${参数2} 等将替换为对应参数值 + 执行 SQL 获取标签选项,第一列作为显示值 + + + + + 确 定 + 取 消 + + + + + + diff --git a/src/views/ydoyun/productcustomtag/index.vue b/src/views/ydoyun/productcustomtag/index.vue new file mode 100644 index 0000000..d6a8bc7 --- /dev/null +++ b/src/views/ydoyun/productcustomtag/index.vue @@ -0,0 +1,190 @@ + + + + + + + + + + + + + + 搜索 + 重置 + + 新增 + + + 导出 + + + 批量删除 + + + + + + + + + + + + + {{ scope.row.color }} + - + + + + + 是 + 否 + + + + + + + + 编辑 + + + 删除 + + + + + + + + + + + diff --git a/src/views/ydoyun/reportdatabase/DatabaseList.vue b/src/views/ydoyun/reportdatabase/DatabaseList.vue new file mode 100644 index 0000000..e4101aa --- /dev/null +++ b/src/views/ydoyun/reportdatabase/DatabaseList.vue @@ -0,0 +1,111 @@ + + + + + + + + + 查询 + + + + + + + + + MySQL + PG + Oracle + SQL + {{ scope.row.dbType }} + + + + + + + + + diff --git a/src/views/ydoyun/storecustomtag/index.vue b/src/views/ydoyun/storecustomtag/index.vue new file mode 100644 index 0000000..056cc10 --- /dev/null +++ b/src/views/ydoyun/storecustomtag/index.vue @@ -0,0 +1,190 @@ + + + + + + + + + + + + + + 搜索 + 重置 + + 新增 + + + 导出 + + + 批量删除 + + + + + + + + + + + + + {{ scope.row.color }} + - + + + + + 是 + 否 + + + + + + + + 编辑 + + + 删除 + + + + + + + + + + + diff --git a/src/views/ydoyun/suppliercustomtag/index.vue b/src/views/ydoyun/suppliercustomtag/index.vue new file mode 100644 index 0000000..87cf95a --- /dev/null +++ b/src/views/ydoyun/suppliercustomtag/index.vue @@ -0,0 +1,190 @@ + + + + + + + + + + + + + + 搜索 + 重置 + + 新增 + + + 导出 + + + 批量删除 + + + + + + + + + + + + + {{ scope.row.color }} + - + + + + + 是 + 否 + + + + + + + + 编辑 + + + 删除 + + + + + + + + + + +